├── .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 |
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 |
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 | 
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 |
35 |
36 |
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('