├── .gitignore ├── resources ├── linux │ ├── 16.png │ ├── 24.png │ ├── 32.png │ ├── 48.png │ ├── 64.png │ ├── 1024.png │ ├── 128.png │ ├── 256.png │ └── 512.png ├── mac │ └── app.icns └── win │ └── app.ico ├── CONTRIBUTING.md ├── script ├── run.cmd ├── build.cmd ├── test.cmd ├── compile.cmd ├── bootstrap.cmd ├── test ├── compile ├── run ├── bootstrap └── build ├── src ├── editor-window │ ├── template-helper.js │ ├── sidebar-view.js │ ├── index.js │ ├── object-editor-view.js │ ├── color-editor-view.js │ ├── svg-editor-model.js │ ├── index.html │ ├── curve.js │ ├── styles │ │ └── index.less │ └── svg-editor.js └── browser │ ├── application-window.js │ ├── application.js │ ├── menu-linux.js │ ├── menu-win32.js │ └── menu-darwin.js ├── spec ├── fixtures │ └── sample.svg ├── object-editor-view-spec.js ├── curve-spec.js └── svg-editor-spec.js ├── main.js ├── keymaps ├── darwin.json └── win32.json ├── package.json ├── README.md └── vendor └── command-registry.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | release/ 3 | compile-cache/ 4 | -------------------------------------------------------------------------------- /resources/linux/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/16.png -------------------------------------------------------------------------------- /resources/linux/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/24.png -------------------------------------------------------------------------------- /resources/linux/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/32.png -------------------------------------------------------------------------------- /resources/linux/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/48.png -------------------------------------------------------------------------------- /resources/linux/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/64.png -------------------------------------------------------------------------------- /resources/mac/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/mac/app.icns -------------------------------------------------------------------------------- /resources/win/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/win/app.ico -------------------------------------------------------------------------------- /resources/linux/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/1024.png -------------------------------------------------------------------------------- /resources/linux/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/128.png -------------------------------------------------------------------------------- /resources/linux/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/256.png -------------------------------------------------------------------------------- /resources/linux/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benogle/curve-app/HEAD/resources/linux/512.png -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This app is built with [Electron](http://electron.atom.io/). 4 | -------------------------------------------------------------------------------- /script/run.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\run" %* 3 | ) ELSE ( 4 | node "%~dp0\run" %* 5 | ) 6 | -------------------------------------------------------------------------------- /script/build.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\build" %* 3 | ) ELSE ( 4 | node "%~dp0\build" %* 5 | ) 6 | -------------------------------------------------------------------------------- /script/test.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\test" %* 3 | ) ELSE ( 4 | node "%~dp0\test" %* 5 | ) 6 | -------------------------------------------------------------------------------- /script/compile.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\compile" %* 3 | ) ELSE ( 4 | node "%~dp0\compile" %* 5 | ) 6 | -------------------------------------------------------------------------------- /script/bootstrap.cmd: -------------------------------------------------------------------------------- 1 | @IF EXIST "%~dp0\node.exe" ( 2 | "%~dp0\node.exe" "%~dp0\bootstrap" %* 3 | ) ELSE ( 4 | node "%~dp0\bootstrap" %* 5 | ) 6 | 7 | -------------------------------------------------------------------------------- /src/editor-window/template-helper.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | renderTemplate: function(template) { 3 | let div = document.createElement('div') 4 | div.innerHTML = template.trim() 5 | return div.childNodes[0] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/sample.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with Curve 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require("path"); 4 | var proc = require("child_process"); 5 | 6 | var runPath = path.join("script", "run"); 7 | var args = [ 8 | "--test" 9 | ].concat(process.argv.slice(2)); 10 | 11 | if (process.platform === 'win32') { 12 | runPath += '.cmd'; 13 | } 14 | 15 | proc.spawn(runPath, args, {stdio: "inherit"}); 16 | -------------------------------------------------------------------------------- /script/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require("path"); 4 | var proc = require("child_process"); 5 | 6 | var modulePath = path.join(".", "node_modules"); 7 | var compilerPath = path.join(modulePath, ".bin", "electron-compile"); 8 | var pathsToCompile = [ 9 | path.join(".", "src"), 10 | path.join(".", "vendor") 11 | ] 12 | var compileCachepath = path.join(".", "compile-cache"); 13 | 14 | console.log("Compiling..."); 15 | 16 | if (process.platform === 'win32') { 17 | compilerPath += '.cmd'; 18 | } 19 | 20 | var args = [ 21 | "--target", compileCachepath 22 | ].concat(pathsToCompile); 23 | proc.spawn(compilerPath, args, {stdio: "inherit"}); 24 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var argv = require('yargs') 3 | .default('test', false) 4 | .default('environment', 'production') 5 | .argv 6 | 7 | if (argv.test) { 8 | require('electron-compile').init() 9 | var TestApplication = require('electron-jasmine').TestApplication 10 | new TestApplication({specDirectory: 'spec'}) 11 | } 12 | else { 13 | if (argv.environment == 'production') { 14 | require('electron-compile').initForProduction(path.join(__dirname, 'compile-cache')) 15 | } 16 | else { 17 | console.log('In development mode') 18 | require('electron-compile').init() 19 | } 20 | 21 | var Application = require('./src/browser/application') 22 | new Application(argv) 23 | } 24 | -------------------------------------------------------------------------------- /keymaps/darwin.json: -------------------------------------------------------------------------------- 1 | { 2 | ".platform-darwin": { 3 | "escape": "core:cancel", 4 | "delete": "core:delete", 5 | "backspace": "core:delete", 6 | 7 | "up": "editor:move-selection-up", 8 | "down": "editor:move-selection-down", 9 | "left": "editor:move-selection-left", 10 | "right": "editor:move-selection-right", 11 | "shift-up": "editor:move-selection-up-by-ten", 12 | "shift-down": "editor:move-selection-down-by-ten", 13 | "shift-left": "editor:move-selection-left-by-ten", 14 | "shift-right": "editor:move-selection-right-by-ten", 15 | 16 | "p": "editor:pen-tool", 17 | "o": "editor:ellipse-tool", 18 | "v": "editor:pointer-tool", 19 | "r": "editor:rectangle-tool" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /keymaps/win32.json: -------------------------------------------------------------------------------- 1 | { 2 | ".platform-darwin": { 3 | "escape": "core:cancel", 4 | "delete": "core:delete", 5 | "backspace": "core:delete", 6 | 7 | "up": "editor:move-selection-up", 8 | "down": "editor:move-selection-down", 9 | "left": "editor:move-selection-left", 10 | "right": "editor:move-selection-right", 11 | "shift-up": "editor:move-selection-up-by-ten", 12 | "shift-down": "editor:move-selection-down-by-ten", 13 | "shift-left": "editor:move-selection-left-by-ten", 14 | "shift-right": "editor:move-selection-right-by-ten", 15 | 16 | "p": "editor:pen-tool", 17 | "o": "editor:ellipse-tool", 18 | "v": "editor:pointer-tool", 19 | "r": "editor:rectangle-tool" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require("path"); 4 | var proc = require("child_process"); 5 | var argv = require('yargs') 6 | .default('environment', 'development') 7 | .argv; 8 | 9 | var modulePath = path.join(".", "node_modules"); 10 | var electronPath = path.join(modulePath, ".bin", "electron"); 11 | 12 | if (process.platform === 'win32') { 13 | electronPath += '.cmd'; 14 | } 15 | 16 | var args = [ 17 | ".", 18 | "--environment", argv.environment 19 | ]; 20 | 21 | var ignoreArgs = ['environment', '_', '$0'] 22 | for(var arg in argv) { 23 | if (ignoreArgs.indexOf(arg) > -1) continue; 24 | 25 | if (argv[arg] === true) 26 | args.push('--' + arg) 27 | else 28 | args.push('--' + arg, argv[arg]) 29 | } 30 | args = args.concat(argv._); 31 | 32 | proc.spawn(electronPath, args, {stdio: "inherit"}); 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "curve", 3 | "appName": "Curve", 4 | "version": "0.0.1", 5 | "main": "main.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/benogle/curve-app.git" 10 | }, 11 | "compileCacheDir": "./compile-cache", 12 | "compileDirs": [ 13 | "src" 14 | ], 15 | "scripts": { 16 | "test": "script/test" 17 | }, 18 | "devDependencies": { 19 | "electron-compile": "^1.0.0", 20 | "electron-compilers": "^1.0.1", 21 | "electron-jasmine": "^0.2.0", 22 | "electron-packager": "^5.1.1", 23 | "electron-prebuilt": "0.34.2", 24 | "electron-rebuild": "^1.0.2" 25 | }, 26 | "dependencies": { 27 | "atom-keymap": "^6.1.0", 28 | "clear-cut": "^2.0.1", 29 | "curve": "^0.1.1", 30 | "dom-listener": "^0.1.2", 31 | "event-kit": "^1.4.1", 32 | "object-assign": "^3.0.0", 33 | "yargs": "^3.16.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require("fs"); 4 | var path = require("path"); 5 | var proc = require("child_process"); 6 | 7 | var modulePath = path.join(".", "node_modules"); 8 | var rebuildPath = path.join(modulePath, ".bin", "electron-rebuild"); 9 | var prebuiltPath = path.join(modulePath, "electron-prebuilt"); 10 | var prebuiltPackageJSONPath = path.join(prebuiltPath, "package.json"); 11 | var npmCmd = "npm"; 12 | 13 | function jsonValue(path, value) { return JSON.parse(fs.readFileSync(path).toString())[value]; }; 14 | 15 | if (process.platform === 'win32') { 16 | npmCmd += '.cmd'; 17 | rebuildPath += '.cmd'; 18 | } 19 | 20 | var install = proc.spawn(npmCmd, ["install"], {stdio: "inherit"}); 21 | install.on('close', function() { 22 | var electronVersion = jsonValue(prebuiltPackageJSONPath, "version"); 23 | var args = [ 24 | "--version", electronVersion, 25 | "--electron-prebuilt-dir", prebuiltPath, 26 | "--module-dir", modulePath 27 | ]; 28 | proc.spawn(rebuildPath, args, {stdio: "inherit"}); 29 | }); 30 | -------------------------------------------------------------------------------- /src/editor-window/sidebar-view.js: -------------------------------------------------------------------------------- 1 | let DOMListener = require('dom-listener') 2 | let ObjectEditorView = require('./object-editor-view') 3 | 4 | class SidebarView { 5 | constructor(svgEditor, {element}={}) { 6 | this.svgEditor = svgEditor 7 | this.svgEditor.getDocument().on('change:tool', this.didChangeTool.bind(this)) 8 | 9 | this.element = element || document.createElement('div') 10 | this.element.id = 'sidebar' 11 | 12 | this.objectEditor = new ObjectEditorView(svgEditor) 13 | this.element.appendChild(this.objectEditor.element) 14 | 15 | this.domListener = new DOMListener(this.element) 16 | this.domListener.add('.tool-button', 'click', this.didClickToolButton.bind(this)) 17 | } 18 | 19 | didClickToolButton(event) { 20 | let toolType = event.currentTarget.getAttribute('data-tool') 21 | this.svgEditor.getDocument().setActiveToolType(toolType) 22 | } 23 | 24 | didChangeTool({toolType}) { 25 | let button = this.element.querySelector('.tool-button.active') 26 | if (button) button.classList.remove('active') 27 | 28 | button = this.element.querySelector(`.tool-button[data-tool="${toolType}"]`) 29 | if (button) button.classList.add('active') 30 | } 31 | } 32 | 33 | module.exports = SidebarView 34 | -------------------------------------------------------------------------------- /src/editor-window/index.js: -------------------------------------------------------------------------------- 1 | var Curve = require('./curve'); 2 | var SVGEditor = require('./svg-editor'); 3 | var SidebarView = require('./sidebar-view'); 4 | 5 | window.onload = function() { 6 | var hash, args, editor, sidebar 7 | hash = window.location.hash.slice(1) 8 | args = Object.freeze(JSON.parse(decodeURIComponent(hash))) 9 | 10 | document.body.classList.add(`platform-${process.platform}`) 11 | 12 | global.curve = new Curve(args) 13 | editor = new SVGEditor(args.fileName, document.querySelector('#canvas'), args) 14 | global.EDITOR = editor // debugging 15 | 16 | sidebar = new SidebarView(editor, {element: document.querySelector('#sidebar')}) 17 | 18 | nicelyCenter(editor) 19 | 20 | window.onbeforeunload = function() { 21 | return curve.confirmClose() 22 | } 23 | 24 | document.addEventListener('keydown', function(event) { 25 | curve.keymaps.handleKeyboardEvent(event) 26 | }) 27 | } 28 | 29 | function nicelyCenter(editor) { 30 | let top, left, scroller, canvas = editor.getCanvas() 31 | 32 | scroller = document.querySelector('#canvas-scroller') 33 | 34 | top = canvas.offsetTop - 20 35 | left = (canvas.offsetWidth / 2 + canvas.offsetLeft) - window.innerWidth / 2 - scroller.offsetLeft/2 36 | 37 | scroller.scrollTop = top 38 | scroller.scrollLeft = left 39 | } 40 | -------------------------------------------------------------------------------- /src/editor-window/object-editor-view.js: -------------------------------------------------------------------------------- 1 | let {CompositeDisposable} = require('event-kit') 2 | let ColorEditorView = require('./color-editor-view') 3 | let {renderTemplate} = require('./template-helper') 4 | 5 | let Template = ` 6 |
7 |
8 |
9 |
10 | ` 11 | 12 | class ObjectEditorView { 13 | constructor(svgEditor) { 14 | this.svgEditor = svgEditor 15 | this.selectionModel = this.svgEditor.getDocument().getSelectionModel() 16 | 17 | this.selectionModel.on('change:selected', this.didChangeSelection.bind(this)) 18 | 19 | this.element = renderTemplate(Template) 20 | 21 | this.fillEditor = new ColorEditorView('fill') 22 | this.element.appendChild(this.fillEditor.element) 23 | this.hide() 24 | } 25 | 26 | didChangeSelection({object}) { 27 | if (object) { 28 | this.setTitle(object.getType()) 29 | this.show() 30 | } 31 | else { 32 | this.hide() 33 | } 34 | this.fillEditor.setObject(object) 35 | } 36 | 37 | show() { 38 | this.element.style.display = null 39 | } 40 | 41 | hide() { 42 | this.element.style.display = 'none' 43 | } 44 | 45 | setTitle(title) { 46 | this.element.querySelector('.object-editor-title').textContent = title 47 | } 48 | } 49 | 50 | module.exports = ObjectEditorView 51 | -------------------------------------------------------------------------------- /src/browser/application-window.js: -------------------------------------------------------------------------------- 1 | var BrowserWindow, Menu, app, url; 2 | 3 | BrowserWindow = require('browser-window'); 4 | app = require('app'); 5 | url = require('url'); 6 | Menu = require('menu'); 7 | 8 | class ApplicationWindow { 9 | // `indexPath` - {String} path to the HTML page 10 | // `browserWindowOptions` - {Object} options for the BrowserWindow 11 | // `rendererArgs` - {Object} arguments that are passed to the renderer process 12 | constructor(indexPath, browserWindowOptions, rendererArgs) { 13 | var indexUrl; 14 | this.window = new BrowserWindow(browserWindowOptions); 15 | 16 | // Arguments are passed to the renderer via the URL hash as JSON. 17 | // e.g. file:///some/path/to/index.html#{filePath: '/path/to/file/to/open.svg'} 18 | indexUrl = url.format({ 19 | protocol: 'file', 20 | pathname: indexPath, 21 | slashes: true, 22 | hash: encodeURIComponent(JSON.stringify(rendererArgs)) 23 | }); 24 | this.window.loadUrl(indexUrl); 25 | this.menu = Menu.buildFromTemplate(require('./menu-'+process.platform)(app, this.window)); 26 | Menu.setApplicationMenu(this.menu); 27 | } 28 | 29 | on() { 30 | var args = 1 <= arguments.length ? Array.prototype.slice.call(arguments, 0) : []; 31 | return this.window.on.apply(this.window, args); 32 | }; 33 | 34 | close() { 35 | this.window.close() 36 | }; 37 | } 38 | 39 | module.exports = ApplicationWindow; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Curve.app 2 | 3 | Curve App is a vector drawing desktop application written in JavaScript and based on Electron. It is mostly an Electron wrapper over the [Curve](http://github.com/benogle/curve) vector drawing library. 4 | 5 | ![shot](https://cloud.githubusercontent.com/assets/69169/9296032/f8031768-4436-11e5-9917-d186d15c9c38.png) 6 | 7 | Note: at this point it is a toy (MVP!) intended to serve as a real-ish example of an Electron app. It has all the trimmings most apps will need: 8 | 9 | * Window management 10 | * File management (open, save, save as, dealing with modified files) 11 | * Menus 12 | * Keyboard shortcuts 13 | * Passing command line parameters from the browser process to the renderer process 14 | 15 | ## Features 16 | 17 | * Open and save SVG files 18 | * Create Paths (pen tool) 19 | * Create Rectangles (rectangle tool) 20 | * Create Ellipses (ellipse tool) 21 | * Edit object shapes (rectangles, ellipses, paths: nodes and their handles) 22 | * Edit object fill color 23 | 24 | ## TODO 25 | 26 | * Undo 27 | * Zoom 28 | * Multi-select 29 | * Better handle management on nodes (break, join, pull) 30 | * Legit color picker that allows alpha 31 | * The editing of more parameters (more than just fill!) 32 | * Layer management 33 | * Like everything else a legit vector drawing app has... 34 | 35 | ## Developing 36 | 37 | ```bash 38 | script/bootstrap 39 | script/run 40 | 41 | # To open a file from the command line 42 | script/run path/to/file.svg 43 | ``` 44 | 45 | ## License 46 | 47 | MIT License 48 | -------------------------------------------------------------------------------- /spec/object-editor-view-spec.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs') 2 | let path = require('path') 3 | let Point = require('curve').Point 4 | let Curve = require('../src/editor-window/curve') 5 | let SVGEditor = require('../src/editor-window/svg-editor') 6 | let ObjectEditorView = require('../src/editor-window/object-editor-view') 7 | 8 | describe('ObjectEditorView', function() { 9 | let editor, samplePath, objectEditor, object 10 | 11 | beforeEach(function(){ 12 | global.curve = new Curve({}) 13 | samplePath = path.join(__dirname, 'fixtures', 'sample.svg') 14 | editor = new SVGEditor(samplePath) 15 | objectEditor = new ObjectEditorView(editor) 16 | jasmine.attachToDOM(objectEditor.element) 17 | }) 18 | 19 | describe("when there no object selected", function(){ 20 | it("is hidden", function(){ 21 | expect(objectEditor.element.style.display).toBe('none') 22 | }) 23 | }) 24 | 25 | describe("when objects are selected", function(){ 26 | beforeEach(function() { 27 | object = editor.svgDocument.getObjects()[0] 28 | }) 29 | it("shows when there is a selected object", function(){ 30 | editor.getDocument().getSelectionModel().setSelected(object) 31 | expect(objectEditor.element.style.display).not.toBe('none') 32 | expect(objectEditor.element.querySelector('.object-editor-title').textContent).toContain('Path') 33 | 34 | editor.getDocument().getSelectionModel().setSelected(null) 35 | expect(objectEditor.element.style.display).toBe('none') 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/editor-window/color-editor-view.js: -------------------------------------------------------------------------------- 1 | let {CompositeDisposable} = require('event-kit') 2 | let {renderTemplate} = require('./template-helper') 3 | 4 | let Template = ` 5 |
6 | 7 |
8 | 9 |
10 |
11 | ` 12 | 13 | class ColorEditorView { 14 | constructor(propertyName) { 15 | this.propertyName = propertyName 16 | this.element = renderTemplate(Template) 17 | 18 | this.colorInput = this.element.querySelector('input') 19 | this.colorInput.addEventListener('change', this.didChangeColor.bind(this)) 20 | } 21 | 22 | setObject(object) { 23 | if (this.subscriptions) this.subscriptions.dispose() 24 | 25 | this.object = object 26 | if (object) { 27 | this.subscriptions = new CompositeDisposable 28 | this.subscriptions.add(object.on('change', this.didChangeObject.bind(this))) 29 | this.updateInputColorForObject() 30 | } 31 | } 32 | 33 | updateInputColorForObject() { 34 | this.colorInput.value = this.object.get(this.propertyName) 35 | } 36 | 37 | didChangeColor() { 38 | if (this.object) { 39 | let value = {} 40 | value[this.propertyName] = this.colorInput.value 41 | this.object.set(value) 42 | } 43 | } 44 | 45 | didChangeObject({object, value}) { 46 | if (value[this.propertyName] != null) { 47 | this.updateInputColorForObject() 48 | } 49 | } 50 | } 51 | 52 | module.exports = ColorEditorView 53 | -------------------------------------------------------------------------------- /src/editor-window/svg-editor-model.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let path = require('path'); 3 | let Emitter = require('event-kit').Emitter 4 | 5 | class SVGEditorModel { 6 | constructor(filePath) { 7 | this.emitter = new Emitter 8 | this.modified = false 9 | this.documentSubscription = null 10 | 11 | this.filePath = filePath 12 | if (filePath) this.filePath = path.resolve(filePath) 13 | } 14 | 15 | /* 16 | Section: Events 17 | */ 18 | 19 | onDidChangeFilePath(callback) { 20 | this.emitter.on('did-change-file-path', callback) 21 | } 22 | 23 | onDidChangeModified(callback) { 24 | this.emitter.on('did-change-modified', callback) 25 | } 26 | 27 | /* 28 | Section: Document Details 29 | */ 30 | 31 | observeDocument(svgDocument) { 32 | if (this.documentSubscription) 33 | this.documentSubscription.dispose() 34 | this.documentSubscription = svgDocument.on('change', () => this.setModified(true)) 35 | } 36 | 37 | getFilePath() { 38 | return this.filePath 39 | } 40 | 41 | setFilePath(filePath) { 42 | if(this.filePath === filePath) return; 43 | 44 | this.filePath = filePath 45 | this.emitter.emit('did-change-file-path', filePath) 46 | } 47 | 48 | isModified() { 49 | return this.modified 50 | } 51 | 52 | /* 53 | Section: File Management 54 | */ 55 | 56 | readFileSync() { 57 | let filePath = this.getFilePath() 58 | if (!filePath) return null 59 | return fs.readFileSync(filePath, {encoding: 'utf8'}) 60 | } 61 | 62 | writeFile(filePath, data, callback) { 63 | let options = { encoding: 'utf8' } 64 | this.setFilePath(filePath) 65 | fs.writeFile(filePath, data, options, () => { 66 | this.setModified(false) 67 | if (callback) callback() 68 | }) 69 | } 70 | 71 | /* 72 | Section: Private 73 | */ 74 | 75 | setModified(modified) { 76 | if (this.modified === modified) return; 77 | 78 | this.modified = modified 79 | this.emitter.emit('did-change-modified', modified) 80 | } 81 | } 82 | 83 | module.exports = SVGEditorModel; 84 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var path = require("path"); 5 | var proc = require('child_process'); 6 | var argv = require('yargs') 7 | .default('arch', process.arch) 8 | .default('platform', process.platform) 9 | .argv; 10 | 11 | function jsonValue(path, value) { return JSON.parse(fs.readFileSync(path).toString())[value]; }; 12 | 13 | var modulePath = path.join(".", "node_modules"); 14 | var packagerCmd = path.join(modulePath, "electron-packager", "cli.js"); 15 | var prebuiltPath = path.join(modulePath, "electron-prebuilt"); 16 | var prebuiltPackageJSONPath = path.join(prebuiltPath, "package.json"); 17 | var buildOutputPath = path.join(".", "release"); 18 | var compileCmd = path.join(".", "script", "compile"); 19 | 20 | var ignorePaths='node_modules/electron-compile/node_modules/electron-compilers|node_modules/\\.bin|node_modules/electron-rebuild|node_modules/electron-jasmine|(/release$)|(/script$)|(/spec$)'; 21 | 22 | var electronVersion = jsonValue(prebuiltPackageJSONPath, 'version'); 23 | var appName = jsonValue('package.json', 'appName'); 24 | 25 | if (process.platform === 'win32') { 26 | compileCmd += '.cmd'; 27 | } 28 | 29 | var iconPath; 30 | if (argv.platform === 'win32') { 31 | iconPath = path.join(".", "resources", "win", "app.ico") 32 | } 33 | else if (argv.platform === 'darwin') { 34 | iconPath = path.join(".", "resources", "mac", "app.icns") 35 | } 36 | else if (argv.platform === 'linux') { 37 | iconPath = path.join(".", "resources", "linux") 38 | } 39 | 40 | var compile = proc.spawn(compileCmd, [], {stdio: 'inherit'}); 41 | compile.on('close', function() { 42 | console.log('Building ', appName); 43 | 44 | var args = [ 45 | packagerCmd, 46 | './', 47 | appName, 48 | '--overwrite', 49 | '--platform', argv.platform, 50 | '--arch', argv.arch, 51 | '--version', electronVersion, 52 | '--icon', iconPath, 53 | '--ignore', ignorePaths, 54 | '--out', buildOutputPath 55 | ]; 56 | 57 | var ignoreArgs = ['overwrite', 'platform', 'arch', 'version', 'ignore', 'out', '_', '$0'] 58 | for(var arg in argv) { 59 | if (ignoreArgs.indexOf(arg) > -1) continue; 60 | 61 | if (argv[arg] === true) 62 | args.push('--' + arg) 63 | else 64 | args.push('--' + arg, argv[arg]) 65 | } 66 | 67 | args = args.concat(argv._); 68 | proc.spawn(process.execPath, args, {stdio: 'inherit'}); 69 | }); 70 | -------------------------------------------------------------------------------- /src/browser/application.js: -------------------------------------------------------------------------------- 1 | var ApplicationWindow, BrowserWindow, app, ipc, path; 2 | 3 | ipc = require('ipc'); 4 | app = require('app'); 5 | dialog = require('dialog'); 6 | path = require('path'); 7 | BrowserWindow = require('browser-window'); 8 | ObjectAssign = require('object-assign'); 9 | ApplicationWindow = require('./application-window'); 10 | 11 | class Application { 12 | constructor(argv) { 13 | global.application = this; 14 | require('crash-reporter').start(); 15 | 16 | var fileNamesToOpen = argv._ 17 | app.on('ready', () => this.onReady(fileNamesToOpen)); 18 | 19 | ipc.on('call-window-method', (event, method, ...args) => { 20 | let win = BrowserWindow.fromWebContents(event.sender) 21 | win[method](...args) 22 | }) 23 | 24 | this.windows = []; 25 | this.gettingStartedWindow = null 26 | } 27 | 28 | // Called when electron is ready 29 | onReady(fileNamesToOpen) { 30 | if (fileNamesToOpen.length) 31 | this.openFiles(fileNamesToOpen); 32 | else 33 | this.openWindow(null, {showWelcomeFile: true}) 34 | } 35 | 36 | openNewWindow() { 37 | this.openWindow() 38 | } 39 | 40 | saveActiveFile() { 41 | let win = BrowserWindow.getFocusedWindow() 42 | if (win) 43 | win.webContents.send('save-active-file') 44 | } 45 | 46 | saveActiveFileAs() { 47 | let win = BrowserWindow.getFocusedWindow() 48 | if (win) 49 | win.webContents.send('save-active-file-as') 50 | } 51 | 52 | // Called when the user clicks the open menu 53 | openFileDialog() { 54 | var options = { 55 | title: 'Open an SVG file', 56 | properties: ['openFile', 'multiSelections'], 57 | filters: [ 58 | { name: 'SVG files', extensions: ['svg'] } 59 | ] 60 | }; 61 | 62 | dialog.showOpenDialog(null, options, (fileNames) => { 63 | this.openFiles(fileNames); 64 | }); 65 | } 66 | 67 | openFiles(fileNames) { 68 | if (fileNames && fileNames.length){ 69 | for (let fileName of fileNames) 70 | this.openWindow(fileName) 71 | } 72 | } 73 | 74 | openWindow(fileName, options) { 75 | var win, windowPath; 76 | windowPath = path.resolve(__dirname, "..", "editor-window", "index.html"); 77 | win = new ApplicationWindow(windowPath, { 78 | width: 1200, 79 | height: 800 80 | }, ObjectAssign({fileName: fileName}, options)); 81 | this.addWindow(win); 82 | } 83 | 84 | removeWindow(win) { 85 | this.windows.splice(this.windows.indexOf(win), 1); 86 | } 87 | 88 | addWindow(win) { 89 | this.windows.push(win); 90 | win.on("closed", (function(_this) { 91 | return function() { 92 | return _this.removeWindow(win); 93 | }; 94 | })(this)); 95 | } 96 | } 97 | 98 | module.exports = Application; 99 | -------------------------------------------------------------------------------- /spec/curve-spec.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs') 2 | let path = require('path') 3 | let Curve = require('../src/editor-window/curve') 4 | 5 | class TestEditor { 6 | getFilePath() { return null; } 7 | save() {} 8 | saveAs() {} 9 | } 10 | 11 | describe('Curve', function() { 12 | let editor, samplePath 13 | 14 | beforeEach(function(){ 15 | global.curve = new Curve({}) 16 | editor = new TestEditor 17 | curve.setActiveEditor(editor) 18 | spyOn(editor, 'save') 19 | spyOn(editor, 'saveAs') 20 | spyOn(curve, 'showSaveAsDialog') 21 | }) 22 | 23 | describe("::saveActiveEditor", function() { 24 | it("does nothing when no initial filePath and user chooses no file", function() { 25 | curve.showSaveAsDialog.and.callFake(function(defaultPath, callback) { 26 | expect(defaultPath).toBe(null) 27 | callback() 28 | }) 29 | 30 | curve.saveActiveEditor() 31 | 32 | expect(editor.save).not.toHaveBeenCalled() 33 | expect(editor.saveAs).not.toHaveBeenCalled() 34 | }) 35 | 36 | it("calls saveAs with the new path when no initial filePath and user chooses a file", function() { 37 | curve.showSaveAsDialog.and.callFake(function(defaultPath, callback) { 38 | expect(defaultPath).toBe(null) 39 | callback('/some/file') 40 | }) 41 | 42 | curve.saveActiveEditor() 43 | 44 | expect(editor.save).not.toHaveBeenCalled() 45 | expect(editor.saveAs).toHaveBeenCalledWith('/some/file') 46 | }) 47 | 48 | it("calls save when the editor already has a filePath", function() { 49 | spyOn(editor, 'getFilePath').and.returnValue('/a-file.omg') 50 | 51 | curve.showSaveAsDialog.and.callFake(function(defaultPath, callback) { 52 | expect(defaultPath).toBe('/a-file.omg') 53 | callback('/some/file') 54 | }) 55 | 56 | curve.saveActiveEditor() 57 | 58 | expect(editor.save).toHaveBeenCalled() 59 | expect(editor.saveAs).not.toHaveBeenCalled() 60 | }) 61 | }) 62 | 63 | describe("::saveActiveEditorAs", function() { 64 | it("does nothing when user chooses no file", function() { 65 | curve.showSaveAsDialog.and.callFake(function(defaultPath, callback) { 66 | expect(defaultPath).toBe(null) 67 | callback() 68 | }) 69 | 70 | curve.saveActiveEditorAs() 71 | 72 | expect(editor.save).not.toHaveBeenCalled() 73 | expect(editor.saveAs).not.toHaveBeenCalled() 74 | }) 75 | }) 76 | 77 | it("calls with the path when the user choses a new path", function() { 78 | spyOn(editor, 'getFilePath').and.returnValue('/a-file.omg') 79 | 80 | curve.showSaveAsDialog.and.callFake(function(defaultPath, callback) { 81 | expect(defaultPath).toBe('/a-file.omg') 82 | callback('/some/file') 83 | }) 84 | 85 | curve.saveActiveEditorAs() 86 | 87 | expect(editor.save).not.toHaveBeenCalled() 88 | expect(editor.saveAs).toHaveBeenCalledWith('/some/file') 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /src/browser/menu-linux.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, window) { 2 | return [ 3 | { 4 | label: 'App', 5 | submenu: [ 6 | { 7 | label: 'About', 8 | selector: 'orderFrontStandardAboutPanel:' 9 | }, 10 | { 11 | type: 'separator' 12 | }, 13 | { 14 | label: 'Hide', 15 | accelerator: 'Control+H', 16 | selector: 'hide:' 17 | }, 18 | { 19 | label: 'Hide Others', 20 | accelerator: 'Control+Shift+H', 21 | selector: 'hideOtherApplications:' 22 | }, 23 | { 24 | label: 'Show All', 25 | selector: 'unhideAllApplications:' 26 | }, 27 | { 28 | type: 'separator' 29 | }, 30 | { 31 | label: 'Quit', 32 | accelerator: 'Control+Q', 33 | click: () => app.quit() 34 | } 35 | ] 36 | }, 37 | { 38 | label: 'File', 39 | submenu: [ 40 | { 41 | label: 'New File', 42 | accelerator: 'Control+n', 43 | click: () => global.application.openNewWindow() 44 | }, 45 | { 46 | label: 'Open…', 47 | accelerator: 'Control+o', 48 | click: () => global.application.openFileDialog() 49 | }, 50 | { 51 | label: 'Save', 52 | accelerator: 'Control+s', 53 | click: () => global.application.saveActiveFile() 54 | }, 55 | { 56 | label: 'Save As…', 57 | accelerator: 'Control+Shift+s', 58 | click: () => global.application.saveActiveFileAs() 59 | }, 60 | { 61 | type: 'separator' 62 | }, 63 | { 64 | label: 'Close Window', 65 | accelerator: 'Control+W', 66 | click: () => window.close() 67 | } 68 | ] 69 | }, 70 | { 71 | label: 'View', 72 | submenu: [ 73 | { 74 | label: 'Reload', 75 | accelerator: 'Control+R', 76 | click: () => window.restart() 77 | }, 78 | { 79 | label: 'Toggle Full Screen', 80 | accelerator: 'Control+Shift+F', 81 | click: () => window.setFullScreen(!window.isFullScreen()) 82 | }, 83 | { 84 | label: 'Toggle Developer Tools', 85 | accelerator: 'Alt+Control+I', 86 | click: () => window.toggleDevTools() 87 | } 88 | ] 89 | }, 90 | { 91 | label: 'Window', 92 | submenu: [ 93 | { 94 | label: 'Minimize', 95 | accelerator: 'Control+M', 96 | selector: 'performMiniaturize:' 97 | }, 98 | { 99 | label: 'Close', 100 | accelerator: 'Control+W', 101 | selector: 'performClose:' 102 | }, 103 | { 104 | type: 'separator' 105 | }, 106 | { 107 | label: 'Bring All to Front', 108 | selector: 'arrangeInFront:' 109 | } 110 | ] 111 | }, 112 | { 113 | label: 'Help', 114 | submenu: [ 115 | { 116 | label: 'Repository', 117 | click: () => require('shell').openExternal('http://github.com/benogle/electron-sample') 118 | } 119 | ] 120 | } 121 | ]; 122 | }; 123 | -------------------------------------------------------------------------------- /src/browser/menu-win32.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, window) { 2 | return [ 3 | { 4 | label: 'App', 5 | submenu: [ 6 | { 7 | label: 'About', 8 | selector: 'orderFrontStandardAboutPanel:' 9 | }, 10 | { 11 | type: 'separator' 12 | }, 13 | { 14 | label: 'Hide', 15 | accelerator: 'Control+H', 16 | selector: 'hide:' 17 | }, 18 | { 19 | label: 'Hide Others', 20 | accelerator: 'Control+Shift+H', 21 | selector: 'hideOtherApplications:' 22 | }, 23 | { 24 | label: 'Show All', 25 | selector: 'unhideAllApplications:' 26 | }, 27 | { 28 | type: 'separator' 29 | }, 30 | { 31 | label: 'Quit', 32 | accelerator: 'Control+Q', 33 | click: () => app.quit() 34 | } 35 | ] 36 | }, 37 | { 38 | label: 'File', 39 | submenu: [ 40 | { 41 | label: 'New File', 42 | accelerator: 'Control+n', 43 | click: () => global.application.openNewWindow() 44 | }, 45 | { 46 | label: 'Open…', 47 | accelerator: 'Control+o', 48 | click: () => global.application.openFileDialog() 49 | }, 50 | { 51 | label: 'Save', 52 | accelerator: 'Control+s', 53 | click: () => global.application.saveActiveFile() 54 | }, 55 | { 56 | label: 'Save As…', 57 | accelerator: 'Control+Shift+s', 58 | click: () => global.application.saveActiveFileAs() 59 | }, 60 | { 61 | type: 'separator' 62 | }, 63 | { 64 | label: 'Close Window', 65 | accelerator: 'Control+W', 66 | click: () => window.close() 67 | } 68 | ] 69 | }, 70 | { 71 | label: 'View', 72 | submenu: [ 73 | { 74 | label: 'Reload', 75 | accelerator: 'Control+R', 76 | click: () => window.restart() 77 | }, 78 | { 79 | label: 'Toggle Full Screen', 80 | accelerator: 'Control+Shift+F', 81 | click: () => window.setFullScreen(!window.isFullScreen()) 82 | }, 83 | { 84 | label: 'Toggle Developer Tools', 85 | accelerator: 'Alt+Control+I', 86 | click: () => window.toggleDevTools() 87 | } 88 | ] 89 | }, 90 | { 91 | label: 'Window', 92 | submenu: [ 93 | { 94 | label: 'Minimize', 95 | accelerator: 'Control+M', 96 | selector: 'performMiniaturize:' 97 | }, 98 | { 99 | label: 'Close', 100 | accelerator: 'Control+W', 101 | selector: 'performClose:' 102 | }, 103 | { 104 | type: 'separator' 105 | }, 106 | { 107 | label: 'Bring All to Front', 108 | selector: 'arrangeInFront:' 109 | } 110 | ] 111 | }, 112 | { 113 | label: 'Help', 114 | submenu: [ 115 | { 116 | label: 'Repository', 117 | click: () => require('shell').openExternal('http://github.com/benogle/electron-sample') 118 | } 119 | ] 120 | } 121 | ]; 122 | }; 123 | -------------------------------------------------------------------------------- /src/browser/menu-darwin.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, window) { 2 | return [ 3 | { 4 | label: 'App', 5 | submenu: [ 6 | { 7 | label: 'About', 8 | selector: 'orderFrontStandardAboutPanel:' 9 | }, 10 | { 11 | type: 'separator' 12 | }, 13 | { 14 | label: 'Hide', 15 | accelerator: 'Command+H', 16 | selector: 'hide:' 17 | }, 18 | { 19 | label: 'Hide Others', 20 | accelerator: 'Command+Shift+H', 21 | selector: 'hideOtherApplications:' 22 | }, 23 | { 24 | label: 'Show All', 25 | selector: 'unhideAllApplications:' 26 | }, 27 | { 28 | type: 'separator' 29 | }, 30 | { 31 | label: 'Quit', 32 | accelerator: 'Command+Q', 33 | click: () => app.quit() 34 | } 35 | ] 36 | }, 37 | { 38 | label: 'File', 39 | submenu: [ 40 | { 41 | label: 'New File', 42 | accelerator: 'Command+n', 43 | click: () => global.application.openNewWindow() 44 | }, 45 | { 46 | label: 'Open…', 47 | accelerator: 'Command+o', 48 | click: () => global.application.openFileDialog() 49 | }, 50 | { 51 | label: 'Save', 52 | accelerator: 'Command+s', 53 | click: () => global.application.saveActiveFile() 54 | }, 55 | { 56 | label: 'Save As…', 57 | accelerator: 'Command+Shift+s', 58 | click: () => global.application.saveActiveFileAs() 59 | }, 60 | { 61 | type: 'separator' 62 | }, 63 | { 64 | label: 'Close Window', 65 | accelerator: 'Command+W', 66 | click: () => window.close() 67 | } 68 | ] 69 | }, 70 | { 71 | label: 'View', 72 | submenu: [ 73 | { 74 | label: 'Reload', 75 | accelerator: 'Command+R', 76 | click: () => window.restart() 77 | }, 78 | { 79 | label: 'Toggle Full Screen', 80 | accelerator: 'Command+Shift+F', 81 | click: () => window.setFullScreen(!window.isFullScreen()) 82 | }, 83 | { 84 | label: 'Toggle Developer Tools', 85 | accelerator: 'Alt+Command+I', 86 | click: () => window.toggleDevTools() 87 | } 88 | ] 89 | }, 90 | { 91 | label: 'Window', 92 | submenu: [ 93 | { 94 | label: 'Minimize', 95 | accelerator: 'Command+M', 96 | selector: 'performMiniaturize:' 97 | }, 98 | { 99 | label: 'Close', 100 | accelerator: 'Command+W', 101 | selector: 'performClose:' 102 | }, 103 | { 104 | type: 'separator' 105 | }, 106 | { 107 | label: 'Bring All to Front', 108 | selector: 'arrangeInFront:' 109 | } 110 | ] 111 | }, 112 | { 113 | label: 'Help', 114 | submenu: [ 115 | { 116 | label: 'Repository', 117 | click: () => require('shell').openExternal('http://github.com/benogle/electron-sample') 118 | } 119 | ] 120 | } 121 | ]; 122 | }; 123 | -------------------------------------------------------------------------------- /src/editor-window/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Curve 5 | 6 | 7 | 8 | 9 | 10 | 30 |
31 |
32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/editor-window/curve.js: -------------------------------------------------------------------------------- 1 | let remote, ipc, path, KeymapManager, CommandRegistry 2 | 3 | ipc = require('ipc') 4 | path = require('path') 5 | remote = require('remote') 6 | KeymapManager = require('atom-keymap') 7 | CommandRegistry = require('../../vendor/command-registry') 8 | 9 | module.exports = 10 | class Curve { 11 | constructor(argv) { 12 | this.argv = argv 13 | ipc.on('save-active-file', this.saveActiveEditor.bind(this)) 14 | ipc.on('save-active-file-as', this.saveActiveEditorAs.bind(this)) 15 | 16 | this.keymaps = new KeymapManager 17 | this.keymaps.defaultTarget = document.body 18 | this.keymaps.loadKeymap(path.join(__dirname, '..', '..', 'keymaps')) 19 | 20 | this.commands = new CommandRegistry 21 | } 22 | 23 | setActiveEditor(activeEditor) { 24 | this.activeEditor = activeEditor 25 | if (this.activeEditor.onDidChangeFilePath) 26 | this.activeEditor.onDidChangeFilePath(this.updateTitle.bind(this)) 27 | if (this.activeEditor.onDidChangeModified) 28 | this.activeEditor.onDidChangeModified(this.updateTitle.bind(this)) 29 | this.updateTitle() 30 | } 31 | 32 | updateTitle() { 33 | let filePath, isModified = false 34 | 35 | filePath = this.activeEditor.getFilePath() 36 | if (filePath) 37 | ipc.send('call-window-method', 'setRepresentedFilename', filePath) 38 | 39 | if (this.activeEditor.isModified) { 40 | isModified = this.activeEditor.isModified() 41 | ipc.send('call-window-method', 'setDocumentEdited', isModified) 42 | } 43 | 44 | if (this.activeEditor.getTitle) 45 | document.title = `${this.activeEditor.getTitle()}${isModified ? ' (edited)' : ''} - Curve` 46 | else 47 | document.title = 'Curve' 48 | } 49 | 50 | confirmClose() { 51 | if (!this.activeEditor.isModified()) return true 52 | 53 | let options, chosen, filePath, title 54 | 55 | title = this.activeEditor.getTitle() 56 | 57 | options = { 58 | message: `'${title}' has changes, do you want to save them?`, 59 | detailedMessage: "Your changes will be lost if you close this item without saving.", 60 | buttons: ["Save", "Cancel", "Don't Save"] 61 | } 62 | chosen = this.showConfirmDialog(options) 63 | 64 | switch (chosen) { 65 | case 0: 66 | this.saveActiveEditor() 67 | return true 68 | case 1: 69 | return false 70 | case 2: 71 | return true 72 | } 73 | } 74 | 75 | saveActiveEditor() { 76 | let filePath = this.activeEditor.getFilePath() 77 | if (filePath) 78 | this.activeEditor.save() 79 | else 80 | this.saveActiveEditorAs() 81 | } 82 | 83 | saveActiveEditorAs() { 84 | let filePath 85 | 86 | filePath = this.activeEditor.getFilePath() 87 | this.showSaveAsDialog(filePath, (newFileName) => { 88 | if (newFileName) 89 | this.activeEditor.saveAs(newFileName) 90 | }) 91 | } 92 | 93 | showSaveAsDialog(defaultPath, callback) { 94 | let dialog, options 95 | 96 | dialog = remote.require('dialog') 97 | options = { 98 | title: 'Save SVG File As', 99 | defaultPath: defaultPath, 100 | filters: [ 101 | { name: 'SVG files', extensions: ['svg'] } 102 | ] 103 | } 104 | 105 | dialog.showSaveDialog(null, options, callback) 106 | } 107 | 108 | showConfirmDialog(options) { 109 | let dialog, chosen 110 | 111 | dialog = remote.require('dialog') 112 | chosen = dialog.showMessageBox(remote.getCurrentWindow(),{ 113 | type: 'info', 114 | message: options.message, 115 | detail: options.detailedMessage, 116 | buttons: options.buttons 117 | }) 118 | 119 | return chosen 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /spec/svg-editor-spec.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs') 2 | let path = require('path') 3 | let Point = require('curve').Point 4 | let Curve = require('../src/editor-window/curve') 5 | let SVGEditor = require('../src/editor-window/svg-editor') 6 | 7 | describe('SVGEditor', function() { 8 | let editor, samplePath 9 | 10 | beforeEach(function(){ 11 | global.curve = new Curve({}) 12 | samplePath = path.join(__dirname, 'fixtures', 'sample.svg') 13 | }) 14 | 15 | describe("when there no filePath is specified", function(){ 16 | beforeEach(function(){ 17 | editor = new SVGEditor(null) 18 | }) 19 | 20 | it("is labeled untitled", function(){ 21 | expect(editor.getTitle()).toBe('untitled') 22 | expect(document.title).toEqual(editor.getTitle() + ' - Curve') 23 | }) 24 | 25 | it("renders no paths", function(){ 26 | let canvas = editor.getCanvas() 27 | expect(canvas.querySelector('path')).toBeNull() 28 | }) 29 | }) 30 | 31 | describe("when a filePath is specified", function(){ 32 | beforeEach(function(){ 33 | editor = new SVGEditor(samplePath) 34 | }) 35 | 36 | it("has the path in the title", function(){ 37 | expect(editor.getTitle()).toBe(samplePath) 38 | expect(document.title).toEqual(editor.getTitle() + ' - Curve') 39 | }) 40 | 41 | it("reads the file and renders the svg", function(){ 42 | let canvas = editor.getCanvas() 43 | expect(canvas.querySelector('path')).not.toBeNull() 44 | }) 45 | }) 46 | 47 | describe("when the file is modified", function() { 48 | beforeEach(function(){ 49 | editor = new SVGEditor(samplePath) 50 | }) 51 | 52 | it("updates the isModified state when the file is edited, and resets when the file is saved", function() { 53 | let modifiedSpy = jasmine.createSpy() 54 | editor.onDidChangeModified(modifiedSpy) 55 | spyOn(fs, 'writeFile').and.callFake((filePath, data, options, callback) => { 56 | callback() 57 | }) 58 | 59 | expect(editor.isModified()).toBe(false) 60 | 61 | object = editor.svgDocument.getObjects()[0] 62 | node = object.getSubpaths()[0].nodes[0] 63 | node.setPoint(new Point(200, 250)) 64 | 65 | expect(editor.isModified()).toBe(true) 66 | expect(modifiedSpy).toHaveBeenCalled() 67 | modifiedSpy.calls.reset() 68 | 69 | node.setPoint(new Point(200, 280)) 70 | expect(editor.isModified()).toBe(true) 71 | expect(modifiedSpy).not.toHaveBeenCalled() 72 | 73 | editor.save() 74 | expect(editor.isModified()).toBe(false) 75 | expect(modifiedSpy).toHaveBeenCalled() 76 | }) 77 | }) 78 | 79 | describe("::save", function() { 80 | beforeEach(function(){ 81 | editor = new SVGEditor(samplePath) 82 | }) 83 | 84 | it("saves the file and keeps the filePath", function() { 85 | let filePathSpy = jasmine.createSpy() 86 | editor.onDidChangeFilePath(filePathSpy) 87 | spyOn(fs, 'writeFile').and.callFake((filePath, data, options, callback) => { 88 | expect(filePath).toBe(samplePath) 89 | expect(options.encoding).toBe('utf8') 90 | expect(data).toContain(' { 111 | expect(filePath).toBe(newFilePath) 112 | expect(options.encoding).toBe('utf8') 113 | expect(data).toContain('svg { 42 | width: 100% !important; 43 | // overflow: hidden; 44 | // This allows drawing outside the canvas boundary. But it's buggy :( 45 | overflow: visible; 46 | } 47 | 48 | svg .object-selection{ 49 | fill: none; 50 | stroke: #09C; 51 | stroke-width: 1; 52 | stroke-linecap: round; 53 | } 54 | svg .object-preselection{ 55 | fill: none; 56 | stroke: #F00; 57 | stroke-width: 1; 58 | stroke-linecap: round; 59 | } 60 | 61 | svg .node-editor-node{ 62 | fill: #fff; 63 | stroke: #069; 64 | stroke-width: 1; 65 | stroke-linecap: round; 66 | } 67 | svg .node-editor-lines{ 68 | fill: none; 69 | stroke: #ccc; 70 | stroke-width: 1; 71 | stroke-dasharray: . ; 72 | } 73 | svg .node-editor-handle{ 74 | fill: #fff; 75 | stroke: #069; 76 | stroke-width: 1; 77 | stroke-linecap: round; 78 | } 79 | 80 | svg .shape-editor-handle{ 81 | fill: #fff; 82 | stroke: #069; 83 | stroke-width: 1; 84 | stroke-linecap: round; 85 | } 86 | 87 | 88 | // Sidebar 89 | 90 | @sidebar-width: 150px; 91 | @sidebar-padding: 20px; 92 | @sidebar-button-height: 70px; 93 | @sidebar-border-color: #505050; 94 | @sidebar-background-color: #3e3e3e; 95 | @sidebar-active-button-color: rgba(0, 0, 0, 0.32); 96 | @sidebar-text-color: #e0e0e0; 97 | 98 | #sidebar { 99 | flex-shrink: 0; 100 | width: @sidebar-width; 101 | background: @sidebar-background-color; 102 | color: white; 103 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 104 | 105 | -webkit-user-select: none; 106 | user-select: none; 107 | 108 | .tool-button-row { 109 | border-bottom: 1px solid @sidebar-border-color; 110 | height: @sidebar-button-height + 1; 111 | } 112 | 113 | .tool-button { 114 | color: fadeout(white, 10%); 115 | display: block; 116 | float: left; 117 | margin: 0; 118 | padding: 0; 119 | width: 50%; 120 | height: @sidebar-button-height; 121 | background: 0; 122 | border: 0; 123 | outline: 0; 124 | 125 | &:first-child { 126 | border-right: 1px solid @sidebar-border-color; 127 | } 128 | 129 | &:active { 130 | padding-top: 2px; 131 | } 132 | 133 | &.active { 134 | color: white; 135 | background: @sidebar-active-button-color; 136 | } 137 | } 138 | } 139 | 140 | .tool-button > .icon { 141 | width: 30px; 142 | height: 30px; 143 | vertical-align: middle; 144 | fill: currentColor; 145 | } 146 | 147 | .object-editor { 148 | color: @sidebar-text-color; 149 | padding: @sidebar-padding; 150 | 151 | .object-editor-title { 152 | margin: 0 0 @sidebar-padding 0; 153 | } 154 | } 155 | 156 | .attribute-editor { 157 | label { 158 | display: block; 159 | margin-bottom: 5px; 160 | font-size: 12px; 161 | color: #8F8F8F; 162 | } 163 | input { 164 | display: block; 165 | } 166 | } 167 | 168 | .color-editor { 169 | input { 170 | display: block; 171 | width: 100%; 172 | height: 30px; 173 | padding: 0; 174 | border: none; 175 | border-radius: 2px; 176 | outline: 0; 177 | -webkit-appearance: none; 178 | box-shadow: @material-shadow-1; 179 | 180 | &::-webkit-color-swatch-wrapper { 181 | padding: 0; 182 | border-radius: 2px; 183 | } 184 | 185 | &::-webkit-color-swatch { 186 | border: none; 187 | border-radius: 2px; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/editor-window/svg-editor.js: -------------------------------------------------------------------------------- 1 | let SVGDocument = require('curve').SVGDocument 2 | let Point = require('curve').Point 3 | let SVGEditorModel = require('./svg-editor-model') 4 | 5 | class SVGEditor { 6 | constructor(filePath, canvasNode, options={}) { 7 | this.model = new SVGEditorModel(filePath) 8 | if (global.curve) curve.setActiveEditor(this) 9 | 10 | this.createCanvas(canvasNode) 11 | this.createDocument() 12 | this.observeDocument() 13 | this.open(options) 14 | 15 | this.model.observeDocument(this.svgDocument) 16 | 17 | this.bindToCommands() 18 | } 19 | 20 | /* 21 | Section: Events 22 | */ 23 | 24 | onDidChangeFilePath(callback) { 25 | this.model.onDidChangeFilePath(callback) 26 | } 27 | 28 | onDidChangeModified(callback) { 29 | this.model.onDidChangeModified(callback) 30 | } 31 | 32 | /* 33 | Section: Document Details 34 | */ 35 | 36 | isModified() { 37 | return this.model.isModified() 38 | } 39 | 40 | getFilePath() { 41 | return this.model.getFilePath() 42 | } 43 | 44 | getTitle() { 45 | return this.model.getFilePath() || 'untitled' 46 | } 47 | 48 | getCanvas() { 49 | return this.canvas 50 | } 51 | 52 | getDocument() { 53 | return this.svgDocument 54 | } 55 | 56 | /* 57 | Section: File Management 58 | */ 59 | 60 | open(options) { 61 | try { 62 | let svg = this.model.readFileSync() 63 | if (svg) 64 | this.svgDocument.deserialize(svg) 65 | else{ 66 | // Initializes the drawing layer when an empty file 67 | this.svgDocument.getObjectLayer() 68 | if (options.showWelcomeFile) 69 | this.svgDocument.deserialize(WelcomeFile) 70 | } 71 | } 72 | catch (error) { 73 | console.error(error.stack); 74 | } 75 | } 76 | 77 | save() { 78 | this.saveAs(this.getFilePath()) 79 | } 80 | 81 | saveAs(filePath) { 82 | filePath = filePath || this.getFilePath() 83 | try { 84 | let data = this.svgDocument.serialize() 85 | this.model.writeFile(filePath, data) 86 | } 87 | catch (error) { 88 | console.error(error.stack) 89 | } 90 | } 91 | 92 | /* 93 | Section: Private 94 | */ 95 | 96 | bindToCommands() { 97 | let translateSelected = (delta) => { 98 | this.svgDocument.translateSelectedObjects(delta) 99 | } 100 | 101 | let removeSelected = () => { 102 | this.svgDocument.removeSelectedObjects() 103 | } 104 | 105 | let setActiveToolType = (toolType) => { 106 | this.svgDocument.setActiveToolType(toolType) 107 | } 108 | 109 | let cancel = () => { 110 | if (this.svgDocument.selectionModel.getSelected() != null) 111 | this.svgDocument.selectionModel.setSelected(null) 112 | else 113 | setActiveToolType('pointer') 114 | } 115 | 116 | curve.commands.add('body', { 117 | 'core:cancel': () => cancel(), 118 | 'core:delete': () => removeSelected(), 119 | 'editor:pen-tool': () => setActiveToolType('pen'), 120 | 'editor:ellipse-tool': () => setActiveToolType('ellipse'), 121 | 'editor:pointer-tool': () => setActiveToolType('pointer'), 122 | 'editor:rectangle-tool': () => setActiveToolType('rectangle'), 123 | 'editor:move-selection-up': () => translateSelected(new Point(0, -1)), 124 | 'editor:move-selection-down': () => translateSelected(new Point(0, 1)), 125 | 'editor:move-selection-left': () => translateSelected(new Point(-1, 0)), 126 | 'editor:move-selection-right': () => translateSelected(new Point(1, 0)), 127 | 'editor:move-selection-up-by-ten': () => translateSelected(new Point(0, -10)), 128 | 'editor:move-selection-down-by-ten': () => translateSelected(new Point(0, 10)), 129 | 'editor:move-selection-left-by-ten': () => translateSelected(new Point(-10, 0)), 130 | 'editor:move-selection-right-by-ten': () => translateSelected(new Point(10, 0)) 131 | }) 132 | } 133 | 134 | createCanvas(canvasNode) { 135 | if (canvasNode) { 136 | this.canvas = canvasNode 137 | } 138 | else { 139 | this.canvas = document.createElement('div') 140 | this.canvas.id = 'canvas' 141 | } 142 | } 143 | 144 | createDocument() { 145 | this.svgDocument = new SVGDocument(this.canvas) 146 | this.svgDocument.initializeTools() 147 | } 148 | 149 | observeDocument() { 150 | let updateCanvasSize = () => { 151 | let size = this.svgDocument.getSize() 152 | if (size && this.canvas) { 153 | this.canvas.style.width = `${size.width}px` 154 | this.canvas.style.height = `${size.height}px` 155 | 156 | // HACK to get the padding reveal on the right when window < canvas size 157 | // There is probably a nice CSS way to do this... 158 | if (this.canvas.parentNode) 159 | this.canvas.parentNode.style.minWidth = `${size.width}px` 160 | } 161 | } 162 | 163 | this.svgDocument.on('change:size', updateCanvasSize) 164 | updateCanvasSize() 165 | } 166 | } 167 | 168 | var WelcomeFile = ` 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | ` 188 | 189 | module.exports = SVGEditor; 190 | -------------------------------------------------------------------------------- /vendor/command-registry.coffee: -------------------------------------------------------------------------------- 1 | # From Atom: https://github.com/atom/atom/blob/master/src/command-registry.coffee 2 | 3 | {Emitter, Disposable, CompositeDisposable} = require 'event-kit' 4 | {calculateSpecificity, validateSelector} = require 'clear-cut' 5 | 6 | SequenceCount = 0 7 | 8 | # Public: Associates listener functions with commands in a context-sensitive way 9 | # using CSS selectors. 10 | module.exports = 11 | class CommandRegistry 12 | constructor: (@rootNode) -> 13 | @registeredCommands = {} 14 | @selectorBasedListenersByCommandName = {} 15 | @inlineListenersByCommandName = {} 16 | @emitter = new Emitter 17 | 18 | destroy: -> 19 | for commandName of @registeredCommands 20 | window.removeEventListener(commandName, @handleCommandEvent, true) 21 | return 22 | 23 | # Public: Add one or more command listeners associated with a selector. 24 | # 25 | # * `target` A {String} containing a CSS selector or a DOM element. If you 26 | # pass a selector, the commands will be globally associated with all 27 | # matching elements. The `,` combinator is not currently supported. 28 | # If you pass a DOM element, the command will be associated with just that 29 | # element. 30 | # * `commands` An {Object} mapping command names like `user:insert-date` to 31 | # listener {Function}s. 32 | # 33 | # Returns a {Disposable} on which `.dispose()` can be called to remove the 34 | # added command handler(s). 35 | add: (target, commandName, callback) -> 36 | if typeof commandName is 'object' 37 | commands = commandName 38 | disposable = new CompositeDisposable 39 | for commandName, callback of commands 40 | disposable.add @add(target, commandName, callback) 41 | return disposable 42 | 43 | if typeof callback isnt 'function' 44 | throw new Error("Can't register a command with non-function callback.") 45 | 46 | if typeof target is 'string' 47 | validateSelector(target) 48 | @addSelectorBasedListener(target, commandName, callback) 49 | else 50 | @addInlineListener(target, commandName, callback) 51 | 52 | addSelectorBasedListener: (selector, commandName, callback) -> 53 | @selectorBasedListenersByCommandName[commandName] ?= [] 54 | listenersForCommand = @selectorBasedListenersByCommandName[commandName] 55 | listener = new SelectorBasedListener(selector, callback) 56 | listenersForCommand.push(listener) 57 | 58 | @commandRegistered(commandName) 59 | 60 | new Disposable => 61 | listenersForCommand.splice(listenersForCommand.indexOf(listener), 1) 62 | delete @selectorBasedListenersByCommandName[commandName] if listenersForCommand.length is 0 63 | 64 | addInlineListener: (element, commandName, callback) -> 65 | @inlineListenersByCommandName[commandName] ?= new WeakMap 66 | 67 | listenersForCommand = @inlineListenersByCommandName[commandName] 68 | unless listenersForElement = listenersForCommand.get(element) 69 | listenersForElement = [] 70 | listenersForCommand.set(element, listenersForElement) 71 | listener = new InlineListener(callback) 72 | listenersForElement.push(listener) 73 | 74 | @commandRegistered(commandName) 75 | 76 | new Disposable -> 77 | listenersForElement.splice(listenersForElement.indexOf(listener), 1) 78 | listenersForCommand.delete(element) if listenersForElement.length is 0 79 | 80 | # Public: Simulate the dispatch of a command on a DOM node. 81 | # 82 | # This can be useful for testing when you want to simulate the invocation of a 83 | # command on a detached DOM node. Otherwise, the DOM node in question needs to 84 | # be attached to the document so the event bubbles up to the root node to be 85 | # processed. 86 | # 87 | # * `target` The DOM node at which to start bubbling the command event. 88 | # * `commandName` {String} indicating the name of the command to dispatch. 89 | dispatch: (target, commandName, detail) -> 90 | event = new CustomEvent(commandName, {bubbles: true, detail}) 91 | eventWithTarget = Object.create event, 92 | target: value: target 93 | preventDefault: value: -> 94 | stopPropagation: value: -> 95 | stopImmediatePropagation: value: -> 96 | @handleCommandEvent(eventWithTarget) 97 | 98 | onWillDispatch: (callback) -> 99 | @emitter.on 'will-dispatch', callback 100 | 101 | getSnapshot: -> 102 | snapshot = {} 103 | for commandName, listeners of @selectorBasedListenersByCommandName 104 | snapshot[commandName] = listeners.slice() 105 | snapshot 106 | 107 | restoreSnapshot: (snapshot) -> 108 | @selectorBasedListenersByCommandName = {} 109 | for commandName, listeners of snapshot 110 | @selectorBasedListenersByCommandName[commandName] = listeners.slice() 111 | return 112 | 113 | handleCommandEvent: (originalEvent) => 114 | propagationStopped = false 115 | immediatePropagationStopped = false 116 | matched = false 117 | currentTarget = originalEvent.target 118 | 119 | syntheticEvent = Object.create originalEvent, 120 | eventPhase: value: Event.BUBBLING_PHASE 121 | currentTarget: get: -> currentTarget 122 | preventDefault: value: -> 123 | originalEvent.preventDefault() 124 | stopPropagation: value: -> 125 | originalEvent.stopPropagation() 126 | propagationStopped = true 127 | stopImmediatePropagation: value: -> 128 | originalEvent.stopImmediatePropagation() 129 | propagationStopped = true 130 | immediatePropagationStopped = true 131 | abortKeyBinding: value: -> 132 | originalEvent.abortKeyBinding?() 133 | 134 | @emitter.emit 'will-dispatch', syntheticEvent 135 | 136 | loop 137 | listeners = @inlineListenersByCommandName[originalEvent.type]?.get(currentTarget) ? [] 138 | if currentTarget.webkitMatchesSelector? 139 | selectorBasedListeners = 140 | (@selectorBasedListenersByCommandName[originalEvent.type] ? []) 141 | .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) 142 | .sort (a, b) -> a.compare(b) 143 | listeners = listeners.concat(selectorBasedListeners) 144 | 145 | matched = true if listeners.length > 0 146 | 147 | for listener in listeners 148 | break if immediatePropagationStopped 149 | listener.callback.call(currentTarget, syntheticEvent) 150 | 151 | break if currentTarget is window 152 | break if propagationStopped 153 | currentTarget = currentTarget.parentNode ? window 154 | 155 | matched 156 | 157 | commandRegistered: (commandName) -> 158 | unless @registeredCommands[commandName] 159 | window.addEventListener(commandName, @handleCommandEvent, true) 160 | @registeredCommands[commandName] = true 161 | 162 | class SelectorBasedListener 163 | constructor: (@selector, @callback) -> 164 | @specificity = calculateSpecificity(@selector) 165 | @sequenceNumber = SequenceCount++ 166 | 167 | compare: (other) -> 168 | other.specificity - @specificity or 169 | other.sequenceNumber - @sequenceNumber 170 | 171 | class InlineListener 172 | constructor: (@callback) -> 173 | --------------------------------------------------------------------------------