diff --git a/locales/en-US/menus-en-US.json b/locales/en-US/menus-en-US.json index 623f990..a83c74f 100644 --- a/locales/en-US/menus-en-US.json +++ b/locales/en-US/menus-en-US.json @@ -15,6 +15,13 @@ "save": "&Save Project", "saveas": "Save Project &as…" }, + "edit" : { + "cut": "Cut", + "copy": "&Copy", + "paste": "Paste", + "delete": "Delete", + "selectall": "Select &All" + }, "view" : { "settings": "Advanced Settings" }, diff --git a/menus/menu-darwin.js b/menus/menu-darwin.js index d19b045..a4e733e 100644 --- a/menus/menu-darwin.js +++ b/menus/menu-darwin.js @@ -96,6 +96,31 @@ module.exports = function applyTemplate() { } ] }, + { + label: 'Edit ', // space prevents OS X from adding Dictation/Emoji menu items + submenu: [ + { + key: 'edit.cut', + accelerator: 'Command+X' + }, + { + key: 'edit.copy', + accelerator: 'Command+C' + }, + { + key: 'edit.paste', + accelerator: 'Command+V' + }, + { + key: 'edit.delete', + accelerator: 'Backspace' + }, + { + key: 'edit.selectall', + accelerator: 'Command+A' + } + ] + }, { label: 'View', submenu: [ diff --git a/menus/menu-win32.js b/menus/menu-win32.js index c4a4cc1..70d42da 100644 --- a/menus/menu-win32.js +++ b/menus/menu-win32.js @@ -46,6 +46,31 @@ module.exports = function applyTemplate() { } ] }, + { + label: 'Edit', + submenu: [ + { + key: 'edit.cut', + accelerator: 'Control+X' + }, + { + key: 'edit.copy', + accelerator: 'Control+C' + }, + { + key: 'edit.paste', + accelerator: 'Control+V' + }, + { + key: 'edit.delete', + accelerator: 'Delete' + }, + { + key: 'edit.selectall', + accelerator: 'Control+A' + } + ] + }, { label: 'View', submenu: [ diff --git a/src/app.js b/src/app.js index 50995be..f158ce6 100644 --- a/src/app.js +++ b/src/app.js @@ -247,6 +247,14 @@ function activateToolItem(item) { paper.view.update(); } +function switchToSelectTool() { + var selectTool = _.findWhere(paper.tools, { key: 'select'}); + if (selectTool) { + selectTool.activate(); + activateToolItem($("#tool-select")); + } +} + // Build the elements for the colorpicker non-tool item function buildColorPicker() { var $picker = $('
').attr('id', 'picker'); @@ -288,17 +296,17 @@ function selectColor(index) { // Change selected path's color if (paper.selectRect) { - if (paper.selectRect.ppath) { - if (paper.selectRect.ppath.data.fill === true) { - paper.selectRect.ppath.fillColor = paper.pancakeShades[index]; + _.each(paper.selectRect.paths, function (selectedPath) { + if (selectedPath.data.fill === true) { + selectedPath.fillColor = paper.pancakeShades[index]; } else { - paper.selectRect.ppath.strokeColor = paper.pancakeShades[index]; + selectedPath.strokeColor = paper.pancakeShades[index]; } - paper.selectRect.ppath.data.color = index; + selectedPath.data.color = index; paper.view.update(); currentFile.changed = true; - } + }); } } @@ -417,6 +425,41 @@ function bindControls() { paper.newPBP(); }); break; + case 'edit.cut': + if (paper.tool.key === 'select') { + paper.tool.copySelectionToBuffer(); + paper.tool.deleteSelection(); + currentFile.changed = true; + paper.view.update(); + } + break; + case 'edit.copy': + if (paper.tool.key === 'select') { + paper.tool.copySelectionToBuffer(); + } + break; + case 'edit.paste': + if (paper.tool.key !== 'select') { + switchToSelectTool(); + } + paper.tool.pasteFromBuffer(); + currentFile.changed = true; + paper.view.update(); + break; + case 'edit.delete': + if (paper.tool.key === 'select') { + paper.tool.deleteSelection(); + currentFile.changed = true; + paper.view.update(); + } + break; + case 'edit.selectall': + if (paper.tool.key !== 'select') { + switchToSelectTool(); + } + paper.tool.selectAll(); + paper.view.update(); + break; case 'view.settings': toggleOverlay(true, function(){ $('#settings').fadeIn('slow'); diff --git a/src/tools/tool.select.js b/src/tools/tool.select.js index c97751a..126abbe 100644 --- a/src/tools/tool.select.js +++ b/src/tools/tool.select.js @@ -26,6 +26,9 @@ module.exports = function(paper) { fill: true, tolerance: 5 }; + var pasteBuffer = []; + var rubberBandStartPoint = null; + var rubberBandPath = null; // Externalize deseletion paper.deselect = function() { @@ -82,6 +85,7 @@ module.exports = function(paper) { if (paper.selectRect !== null) { paper.deselect(); } + rubberBandStartPoint = event.point; return; } @@ -91,7 +95,6 @@ module.exports = function(paper) { if (hitResult.type === 'segment') { hitResult.segment.remove(); } - return; } if (hitResult) { @@ -125,15 +128,30 @@ module.exports = function(paper) { segment = hitResult.segment; } } else if (hitResult.type === 'stroke' && path !== paper.selectRect) { - if (paper.selectRect && paper.selectRect.ppath === path) { + if (!event.modifiers.shift && paper.selectRect && _.contains(paper.selectRect.paths, path)) { // Add new segment node to the path (if it's already selected) var location = hitResult.location; segment = path.insert(location.index + 1, event.point); } } - if ((paper.selectRect === null || paper.selectRect.ppath !== path) && paper.selectRect !== path) { - initSelectionRectangle(path); + // Selection + var isMultiSelect = event.modifiers.command || event.modifiers.control; + if ((paper.selectRect === null || !_.contains(paper.selectRect.paths, path)) && paper.selectRect !== path) { + if (!isMultiSelect || paper.selectRect === null) { + // Start a new selection with this path + initSelectionRectangle([path]); + } else { + // Multiselect key held, add this path to the existing selection + var newSelection = paper.selectRect.paths; + newSelection.push(path); + initSelectionRectangle(newSelection); + } + } + // When multiselect key is held and path is already selected, remove it from selection + else if (isMultiSelect && paper.selectRect !== null && _.contains(paper.selectRect.paths, path)) { + var newSelection = _.filter(paper.selectRect.paths, function (p) { return p !== path }); + initSelectionRectangle(newSelection); } } @@ -149,12 +167,17 @@ module.exports = function(paper) { var ratio = event.point.subtract(paper.selectRect.bounds.center).length / selectionRectangleScale; var scaling = new Point(ratio, ratio); paper.selectRect.scaling = scaling; - paper.selectRect.ppath.scaling = scaling; + _.each(paper.selectRect.paths, function (selectedPath) { + selectedPath.scaling = scaling; + selectedPath.strokeWidth = 4 / ratio; + }); return; } else if (selectionRectangleRotation !== null) { // Path rotation adjustment var rotation = event.point.subtract(paper.selectRect.pivot).angle + 90; - paper.selectRect.ppath.rotation = rotation; + _.each(paper.selectRect.paths, function (selectedPath) { + selectedPath.rotation = rotation; + }); paper.selectRect.rotation = rotation; return; } @@ -167,18 +190,22 @@ module.exports = function(paper) { // Smooth -only- non-polygonal paths //if (!path.data.isPolygonal) path.smooth(); - initSelectionRectangle(path); - } else if (path) { + initSelectionRectangle([path]); + } else if (paper.selectRect !== null && paper.selectRect.paths.length > 0) { // Path translate position adjustment - if (path !== paper.selectRect) { - path.position.x += event.delta.x; - path.position.y += event.delta.y; - paper.selectRect.position.x += event.delta.x; - paper.selectRect.position.y += event.delta.y; - } else { - paper.selectRect.position = paper.selectRect.position.add(event.delta); - paper.selectRect.ppath.position = paper.selectRect.ppath.position.add(event.delta); - } + _.each(paper.selectRect.paths, function (selectedPath) { + selectedPath.applyMatrix = true; + selectedPath.applyMatrix = false; + selectedPath.strokeWidth = 4; + selectedPath.position = selectedPath.position.add(event.delta); + }); + paper.selectRect.position = paper.selectRect.position.add(event.delta); + } else { + if (rubberBandPath) { rubberBandPath.remove(); } + rubberBandPath = Path.Rectangle(rubberBandStartPoint, event.point); + rubberBandPath.dashArray = [12, 6]; + rubberBandPath.strokeWidth = 2; + rubberBandPath.strokeColor = 'cyan'; } }; @@ -217,6 +244,19 @@ module.exports = function(paper) { selectionRectangleScale = null; selectionRectangleRotation = null; + if (rubberBandPath !== null) { + var rbRect = rubberBandPath.bounds; + rubberBandPath.remove(); + rubberBandPath = null; + + var rbItems = project.activeLayer.getItems({ + inside: rbRect, + class: Path + }); + + initSelectionRectangle(rbItems); + } + // If we have a mouse up with either of these, the file has changed! if (path || segment) { paper.cleanPath(path); @@ -224,38 +264,83 @@ module.exports = function(paper) { } }; - tool.onKeyDown = function (event) { - if (paper.selectRect) { - // Delete a selected path - if (event.key === 'delete' || event.key === 'backspace') { - paper.selectRect.ppath.remove(); - if (paper.imageTraceMode) paper.traceImage = null; - paper.deselect(); + function cancelSelection() { + if (paper.selectRect !== null) { + _.each(paper.selectRect.paths, function (selectedPath) { + selectedPath.fullySelected = false; + }); + } + paper.deselect(); + } + + tool.deleteSelection = function() { + if (paper.selectRect !== null) { + _.each(paper.selectRect.paths, function (selectedPath) { + selectedPath.remove(); + }); + } + if (paper.imageTraceMode) paper.traceImage = null; + paper.deselect(); + } + + tool.copySelectionToBuffer = function() { + pasteBuffer = []; + if (paper.selectRect !== null) { + var itemsToCopy = paper.selectRect.paths; + _.each(itemsToCopy, function (copyItem) { + pasteBuffer.push(copyItem.exportJSON({ asString: false })); + }); + } + } + + tool.pasteFromBuffer = function() { + var addedItems = []; + _.each(pasteBuffer, function (pasteItem) { + if (pasteItem[0] === 'Path') { + var newPath = new Path(); + newPath.importJSON(JSON.stringify(pasteItem)); + newPath.position.x += 15; + newPath.position.y += 15; + addedItems.push(newPath); } + }); + + if (addedItems.length > 0) { + initSelectionRectangle(addedItems); + } + } + tool.selectAll = function() { + initSelectionRectangle(project.activeLayer.getItems({ class: Path })); + } + + tool.onKeyDown = function (event) { + if (paper.selectRect) { // Deselect if (event.key === 'escape') { - paper.selectRect.ppath.fullySelected = false; - paper.deselect(); + cancelSelection(); } - } }; - function initSelectionRectangle(path) { - if (paper.selectRect !== null) paper.selectRect.remove(); - var reset = path.rotation === 0 && path.scaling.x === 1 && path.scaling.y === 1; - var bounds; + function initSelectionRectangle(paths) { + paths = _.filter(paths, function(p) { return p !== paper.selectRect }); + cancelSelection(); - if (reset) { - // Actually reset bounding box - bounds = path.bounds; - path.pInitialBounds = path.bounds; - } else { - // No bounding box reset - bounds = path.pInitialBounds; + if (paths.length <= 0) { + return; } + var bounds = undefined; + + _.each(paths, function (selectedPath) { + if (bounds) { + bounds = bounds.unite(selectedPath.bounds); + } else { + bounds = selectedPath.bounds; + } + }); + var b = bounds.clone().expand(25, 25); paper.selectRect = new Path.Rectangle(b); @@ -264,18 +349,19 @@ module.exports = function(paper) { paper.selectRect.insert(2, new Point(b.center.x, b.top - 25)); paper.selectRect.insert(2, new Point(b.center.x, b.top)); - if (!reset) { - paper.selectRect.position = path.bounds.center; - paper.selectRect.rotation = path.rotation; - paper.selectRect.scaling = path.scaling; - } - paper.selectRect.strokeWidth = 2; paper.selectRect.strokeColor = 'blue'; paper.selectRect.name = "selection rectangle"; paper.selectRect.selected = true; - paper.selectRect.ppath = path; - paper.selectRect.ppath.pivot = paper.selectRect.pivot; + paper.selectRect.paths = paths; + + _.each(paths, function (selectedPath) { + // Apply previous rotation to segments before moving pivot + selectedPath.applyMatrix = true; + selectedPath.applyMatrix = false; + selectedPath.strokeWidth = 4; + selectedPath.pivot = paper.selectRect.pivot; + }); } function getBoundSelection(point) { @@ -309,7 +395,7 @@ module.exports = function(paper) { paper.imageTraceMode = toggle; if (toggle) { - initSelectionRectangle(paper.traceImage); + initSelectionRectangle([paper.traceImage]); tool.activate(); } else { path = null;