diff --git a/src/js/selection/BaseSelection.js b/src/js/selection/BaseSelection.js index 0907b5dd..8de479ed 100644 --- a/src/js/selection/BaseSelection.js +++ b/src/js/selection/BaseSelection.js @@ -5,9 +5,23 @@ this.reset(); }; + ns.BaseSelection.prototype.stringify = function () { + return JSON.stringify({ + pixels: this.pixels, + time: this.time + }); + }; + + ns.BaseSelection.prototype.parse = function (str) { + var selectionData = JSON.parse(str); + this.pixels = selectionData.pixels; + this.time = selectionData.time; + }; + ns.BaseSelection.prototype.reset = function () { this.pixels = []; this.hasPastedContent = false; + this.time = -1; }; ns.BaseSelection.prototype.move = function (colDiff, rowDiff) { @@ -30,5 +44,8 @@ }); this.hasPastedContent = true; + // Keep track of the selection time to compare between local selection and + // paste event selections. + this.time = Date.now(); }; })(); diff --git a/src/js/selection/SelectionManager.js b/src/js/selection/SelectionManager.js index 3f0dddb1..1bb224b3 100644 --- a/src/js/selection/SelectionManager.js +++ b/src/js/selection/SelectionManager.js @@ -22,12 +22,14 @@ $.subscribe(Events.SELECTION_PASTE, this.paste.bind(this)); var shortcuts = pskl.service.keyboard.Shortcuts; - pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.PASTE, this.paste.bind(this)); - pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.CUT, this.cut.bind(this)); - pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.COPY, this.copy.bind(this)); pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.DELETE, this.onDeleteShortcut_.bind(this)); pskl.app.shortcutService.registerShortcut(shortcuts.SELECTION.COMMIT, this.commit.bind(this)); + // These 3 events should be handled by a new separated service + window.addEventListener('cut', this.cut.bind(this), true); + window.addEventListener('copy', this.copy.bind(this), true); + window.addEventListener('paste', this.paste.bind(this), true); + $.subscribe(Events.TOOL_SELECTED, $.proxy(this.onToolSelected_, this)); }; @@ -78,29 +80,90 @@ scope : this, replay : { type : SELECTION_REPLAY.ERASE, - pixels : JSON.parse(JSON.stringify(pixels.slice(0))) + pixels : JSON.parse(JSON.stringify(pixels)) } }); }; - ns.SelectionManager.prototype.cut = function() { - if (this.currentSelection) { + ns.SelectionManager.prototype.copy = function(event) { + if (this.currentSelection && this.piskelController.getCurrentFrame()) { + this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame()); + event.clipboardData.setData('text/plain', this.currentSelection.stringify()); + event.preventDefault(); + } + }; + + ns.SelectionManager.prototype.cut = function(event) { + if (this.currentSelection && this.piskelController.getCurrentFrame()) { // Put cut target into the selection: this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame()); + event.clipboardData.setData('text/plain', JSON.stringify(this.currentSelection)); + event.preventDefault(); this.erase(); } }; ns.SelectionManager.prototype.paste = function() { - if (!this.currentSelection || !this.currentSelection.hasPastedContent) { - if (window.localStorage.getItem('piskel.clipboard')) { - this.currentSelection = JSON.parse(window.localStorage.getItem('piskel.clipboard')); - } else { - return; + var items = event.clipboardData.items; + + try { + for (var i = 0 ; i < items.length ; i++) { + var item = items[i]; + + if (/^image/i.test(item.type)) { + this.pasteImage_(item); + event.stopPropagation(); + return; + } + + if (/^text\/plain/i.test(item.type)) { + this.pasteText_(item); + event.stopPropagation(); + return; + } } + } catch (e) { + // Some of the clipboard APIs are not available on Safari/IE + // Allow Piskel to fallback on local currentSelection pasting. } - var pixels = this.currentSelection.pixels; + // temporarily keeping this code path for tests and fallbacks. + if (this.currentSelection && this.currentSelection.hasPastedContent) { + this.pastePixelsOnCurrentFrame_(this.currentSelection.pixels); + } + }; + + ns.SelectionManager.prototype.pasteImage_ = function(clipboardItem) { + var blob = clipboardItem.getAsFile(); + pskl.utils.FileUtils.readImageFile(blob, function (image) { + pskl.app.fileDropperService.dropPosition_ = {x: 0, y: 0}; + pskl.app.fileDropperService.onImageLoaded_(image, blob); + }.bind(this)); + }; + + ns.SelectionManager.prototype.pasteText_ = function(clipboardItem) { + var blob = clipboardItem.getAsString(function (selectionString) { + var selectionData = JSON.parse(selectionString); + var time = selectionData.time; + var pixels = selectionData.pixels; + + if (this.currentSelection && this.currentSelection.time >= time) { + // If the local selection is newer or equal to the one coming from the clipboard event + // use the local one. The reason is that the "move" information is only updated locally + // without synchronizing it to the clipboard. + // TODO: the selection should store the origin of the selection and the selection itself + // separately. + pixels = this.currentSelection.pixels; + } + + if (pixels) { + // If the current clipboard data is some random text, pixels will not be defined. + this.pastePixelsOnCurrentFrame_(pixels); + } + }.bind(this)); + }; + + ns.SelectionManager.prototype.pastePixelsOnCurrentFrame_ = function (pixels) { var frame = this.piskelController.getCurrentFrame(); this.pastePixels_(frame, pixels); @@ -146,13 +209,6 @@ }); }; - ns.SelectionManager.prototype.copy = function() { - if (this.currentSelection && this.piskelController.getCurrentFrame()) { - this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame()); - window.localStorage.setItem('piskel.clipboard', JSON.stringify(this.currentSelection)); - } - }; - /** * @private */