From ca3bbf1c572acfb2d658aab84669bc5cd5e8069f Mon Sep 17 00:00:00 2001 From: jdescottes Date: Sat, 10 Oct 2015 19:32:25 +0200 Subject: [PATCH] Issue #287 : Shortcuts now rely on Shortcut instances. Shortcut key can be changed dynamically. --- src/js/controller/DrawingController.js | 7 +- src/js/controller/LayersListController.js | 8 +- src/js/controller/PaletteController.js | 5 +- src/js/controller/PalettesListController.js | 7 +- src/js/controller/ToolController.js | 17 +- .../controller/dialogs/DialogsController.js | 12 +- .../piskel/PublicPiskelController.js | 9 +- .../controller/preview/PreviewController.js | 11 +- src/js/selection/SelectionManager.js | 12 +- src/js/service/HistoryService.js | 5 +- src/js/service/keyboard/CheatsheetService.js | 90 +++--- src/js/service/keyboard/Shortcut.js | 60 ++++ src/js/service/keyboard/ShortcutService.js | 141 +++++---- src/js/service/keyboard/Shortcuts.js | 70 +++++ src/js/service/storage/StorageService.js | 7 +- src/js/tools/IconMarkupRenderer.js | 8 +- src/js/tools/drawing/Circle.js | 2 +- src/js/tools/drawing/ColorPicker.js | 2 +- src/js/tools/drawing/ColorSwap.js | 2 +- src/js/tools/drawing/DitheringTool.js | 2 +- src/js/tools/drawing/Eraser.js | 2 +- src/js/tools/drawing/Lighten.js | 2 +- src/js/tools/drawing/Move.js | 2 +- src/js/tools/drawing/PaintBucket.js | 2 +- src/js/tools/drawing/Rectangle.js | 2 +- src/js/tools/drawing/SimplePen.js | 2 +- src/js/tools/drawing/Stroke.js | 2 +- src/js/tools/drawing/VerticalMirrorPen.js | 2 +- src/js/tools/drawing/selection/LassoSelect.js | 2 +- .../drawing/selection/RectangleSelect.js | 2 +- src/js/tools/drawing/selection/ShapeSelect.js | 2 +- src/js/utils/TooltipFormatter.js | 2 +- src/js/utils/UserSettings.js | 4 + src/piskel-script-list.js | 2 + .../service/keyboard/ShortcutServiceTest.js | 275 ++++++++++++++++++ 35 files changed, 597 insertions(+), 185 deletions(-) create mode 100644 src/js/service/keyboard/Shortcut.js create mode 100644 src/js/service/keyboard/Shortcuts.js create mode 100644 test/js/service/keyboard/ShortcutServiceTest.js diff --git a/src/js/controller/DrawingController.js b/src/js/controller/DrawingController.js index c0f2ea5a..56bc9161 100644 --- a/src/js/controller/DrawingController.js +++ b/src/js/controller/DrawingController.js @@ -62,9 +62,10 @@ $.subscribe(Events.USER_SETTINGS_CHANGED, this.onUserSettingsChange_.bind(this)); $.subscribe(Events.FRAME_SIZE_CHANGED, this.onFrameSizeChange_.bind(this)); - pskl.app.shortcutService.registerShortcut('0', this.resetZoom_.bind(this)); - pskl.app.shortcutService.registerShortcut('+', this.increaseZoom_.bind(this, 1)); - pskl.app.shortcutService.registerShortcut('-', this.decreaseZoom_.bind(this, 1)); + var shortcuts = pskl.service.keyboard.Shortcuts; + pskl.app.shortcutService.registerShortcut(shortcuts.MISC.RESET_ZOOM, this.resetZoom_.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.MISC.INCREASE_ZOOM, this.increaseZoom_.bind(this, 1)); + pskl.app.shortcutService.registerShortcut(shortcuts.MISC.DECREASE_ZOOM, this.decreaseZoom_.bind(this, 1)); window.setTimeout(function () { this.afterWindowResize_(); diff --git a/src/js/controller/LayersListController.js b/src/js/controller/LayersListController.js index deebdd9f..7df48d0a 100644 --- a/src/js/controller/LayersListController.js +++ b/src/js/controller/LayersListController.js @@ -5,6 +5,7 @@ ns.LayersListController = function (piskelController) { this.piskelController = piskelController; + this.layerPreviewShortcut = pskl.service.keyboard.Shortcuts.MISC.LAYER_PREVIEW ; }; ns.LayersListController.prototype.init = function () { @@ -36,10 +37,9 @@ var descriptors = [{description : 'Opacity defined in PREFERENCES'}]; var helpText = 'Preview all layers'; - var toggleLayerPreviewTooltip = pskl.utils.TooltipFormatter.format(helpText, TOGGLE_LAYER_SHORTCUT, descriptors); - this.toggleLayerPreviewEl.setAttribute('title', toggleLayerPreviewTooltip); - - pskl.app.shortcutService.registerShortcut(TOGGLE_LAYER_SHORTCUT, this.toggleLayerPreview_.bind(this)); + pskl.app.shortcutService.registerShortcut(this.layerPreviewShortcut, this.toggleLayerPreview_.bind(this)); + var tooltip = pskl.utils.TooltipFormatter.format(helpText, this.layerPreviewShortcut, descriptors); + this.toggleLayerPreviewEl.setAttribute('title', tooltip); }; ns.LayersListController.prototype.updateButtonStatus_ = function () { diff --git a/src/js/controller/PaletteController.js b/src/js/controller/PaletteController.js index b0949852..5ffa2091 100644 --- a/src/js/controller/PaletteController.js +++ b/src/js/controller/PaletteController.js @@ -10,8 +10,9 @@ $.subscribe(Events.SELECT_PRIMARY_COLOR, this.onColorSelected_.bind(this, {isPrimary:true})); $.subscribe(Events.SELECT_SECONDARY_COLOR, this.onColorSelected_.bind(this, {isPrimary:false})); - pskl.app.shortcutService.registerShortcut('X', this.swapColors.bind(this)); - pskl.app.shortcutService.registerShortcut('D', this.resetColors.bind(this)); + var shortcuts = pskl.service.keyboard.Shortcuts; + pskl.app.shortcutService.registerShortcut(shortcuts.COLOR.SWAP, this.swapColors.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.COLOR.RESET, this.resetColors.bind(this)); var spectrumCfg = { showPalette: true, diff --git a/src/js/controller/PalettesListController.js b/src/js/controller/PalettesListController.js index a9525c54..a4f57489 100644 --- a/src/js/controller/PalettesListController.js +++ b/src/js/controller/PalettesListController.js @@ -37,9 +37,10 @@ $.subscribe(Events.SECONDARY_COLOR_SELECTED, this.highlightSelectedColors.bind(this)); $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); - pskl.app.shortcutService.registerShortcuts(['>', 'shift+>'], this.selectNextColor_.bind(this)); - pskl.app.shortcutService.registerShortcuts('123465789'.split(''), this.selectColorForKey_.bind(this)); - pskl.app.shortcutService.registerShortcut('<', this.selectPreviousColor_.bind(this)); + var shortcuts = pskl.service.keyboard.Shortcuts; + pskl.app.shortcutService.registerShortcut(shortcuts.COLOR.PREVIOUS_COLOR, this.selectPreviousColor_.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.COLOR.NEXT_COLOR, this.selectNextColor_.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.COLOR.SELECT_COLOR, this.selectColorForKey_.bind(this)); this.fillPaletteList(); this.updateFromUserSettings(); diff --git a/src/js/controller/ToolController.js b/src/js/controller/ToolController.js index 31f596a8..4f3803dc 100644 --- a/src/js/controller/ToolController.js +++ b/src/js/controller/ToolController.js @@ -21,7 +21,7 @@ new pskl.tools.drawing.ColorPicker() ]; - this.toolIconRenderer = new pskl.tools.IconMarkupRenderer(); + this.iconMarkupRenderer = new pskl.tools.IconMarkupRenderer(); }; /** @@ -93,12 +93,10 @@ } }; - ns.ToolController.prototype.onKeyboardShortcut_ = function(charkey) { - for (var i = 0 ; i < this.tools.length ; i++) { - var tool = this.tools[i]; - if (tool.shortcut.toLowerCase() === charkey.toLowerCase()) { - this.selectTool_(tool); - } + ns.ToolController.prototype.onKeyboardShortcut_ = function(toolId, charkey) { + var tool = this.getToolById_(toolId); + if (tool !== null) { + this.selectTool_(tool); } }; @@ -115,14 +113,15 @@ var html = ''; for (var i = 0 ; i < this.tools.length ; i++) { var tool = this.tools[i]; - html += this.toolIconRenderer.render(tool, tool.shortcut); + html += this.iconMarkupRenderer.render(tool); } $('#tools-container').html(html); }; ns.ToolController.prototype.addKeyboardShortcuts_ = function () { for (var i = 0 ; i < this.tools.length ; i++) { - pskl.app.shortcutService.registerShortcut(this.tools[i].shortcut, this.onKeyboardShortcut_.bind(this)); + var tool = this.tools[i]; + pskl.app.shortcutService.registerShortcut(tool.shortcut, this.onKeyboardShortcut_.bind(this, tool.toolId)); } }; })(); diff --git a/src/js/controller/dialogs/DialogsController.js b/src/js/controller/dialogs/DialogsController.js index a3924bf5..48097f4f 100644 --- a/src/js/controller/dialogs/DialogsController.js +++ b/src/js/controller/dialogs/DialogsController.js @@ -18,6 +18,7 @@ ns.DialogsController = function (piskelController) { this.piskelController = piskelController; + this.closePopupShortcut = pskl.service.keyboard.Shortcuts.MISC.CLOSE_POPUP; this.currentDialog_ = null; }; @@ -28,11 +29,16 @@ $.subscribe(Events.DIALOG_HIDE, this.onDialogHideEvent_.bind(this)); // TODO : JD : should be moved to a main controller - pskl.app.shortcutService.registerShortcut('alt+P', this.onDialogDisplayEvent_.bind(this, null, 'create-palette')); + var createPaletteShortcut = pskl.service.keyboard.Shortcuts.COLOR.CREATE_PALETTE; + pskl.app.shortcutService.registerShortcut(createPaletteShortcut, this.onCreatePaletteShortcut_.bind(this)); this.dialogWrapper_.classList.add('animated'); }; + ns.DialogsController.prototype.onCreatePaletteShortcut_ = function () { + this.onDialogDisplayEvent_(null, 'create-palette'); + }; + ns.DialogsController.prototype.onDialogDisplayEvent_ = function (evt, args) { var dialogId, initArgs; if (typeof args === 'string') { @@ -66,7 +72,7 @@ }; ns.DialogsController.prototype.showDialogWrapper_ = function () { - pskl.app.shortcutService.registerShortcut('ESC', this.hideDialog.bind(this)); + pskl.app.shortcutService.registerShortcut(this.closePopupShortcut, this.hideDialog.bind(this)); this.dialogWrapper_.classList.add('show'); }; @@ -85,7 +91,7 @@ }; ns.DialogsController.prototype.hideDialogWrapper_ = function () { - pskl.app.shortcutService.unregisterShortcut('ESC'); + pskl.app.shortcutService.unregisterShortcut(this.closePopupShortcut); this.dialogWrapper_.classList.remove('show'); }; diff --git a/src/js/controller/piskel/PublicPiskelController.js b/src/js/controller/piskel/PublicPiskelController.js index d3794e6b..033d578a 100644 --- a/src/js/controller/piskel/PublicPiskelController.js +++ b/src/js/controller/piskel/PublicPiskelController.js @@ -30,10 +30,11 @@ this.saveWrap_('moveLayerDown', true); this.saveWrap_('removeCurrentLayer', true); - pskl.app.shortcutService.registerShortcut('up', this.selectPreviousFrame.bind(this)); - pskl.app.shortcutService.registerShortcut('down', this.selectNextFrame.bind(this)); - pskl.app.shortcutService.registerShortcut('n', this.addFrameAtCurrentIndex.bind(this)); - pskl.app.shortcutService.registerShortcut('shift+n', this.duplicateCurrentFrame.bind(this)); + var shortcuts = pskl.service.keyboard.Shortcuts; + pskl.app.shortcutService.registerShortcut(shortcuts.MISC.PREVIOUS_FRAME, this.selectPreviousFrame.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.MISC.NEXT_FRAME, this.selectNextFrame.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.MISC.NEW_FRAME, this.addFrameAtCurrentIndex.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.MISC.DUPLICATE_FRAME, this.duplicateCurrentFrame.bind(this)); }; ns.PublicPiskelController.prototype.setPiskel = function (piskel, preserveState) { diff --git a/src/js/controller/preview/PreviewController.js b/src/js/controller/preview/PreviewController.js index 5207c90b..33278b72 100644 --- a/src/js/controller/preview/PreviewController.js +++ b/src/js/controller/preview/PreviewController.js @@ -14,6 +14,9 @@ this.elapsedTime = 0; this.currentIndex = 0; + this.onionSkinShortcut = pskl.service.keyboard.Shortcuts.MISC.ONION_SKIN; + this.originalSizeShortcut = pskl.service.keyboard.Shortcuts.MISC.X1_PREVIEW; + this.renderFlag = true; /** @@ -47,8 +50,8 @@ pskl.utils.Event.addEventListener(this.openPopupPreview, 'click', this.onOpenPopupPreviewClick_, this); pskl.utils.Event.addEventListener(this.originalSizeButton, 'click', this.onOriginalSizeButtonClick_, this); - pskl.app.shortcutService.registerShortcut(ONION_SKIN_SHORTCUT, this.toggleOnionSkin_.bind(this)); - pskl.app.shortcutService.registerShortcut(ORIGINAL_SIZE_SHORTCUT, this.onOriginalSizeButtonClick_.bind(this)); + pskl.app.shortcutService.registerShortcut(this.onionSkinShortcut, this.toggleOnionSkin_.bind(this)); + pskl.app.shortcutService.registerShortcut(this.originalSizeShortcut, this.onOriginalSizeButtonClick_.bind(this)); $.subscribe(Events.FRAME_SIZE_CHANGED, this.onFrameSizeChange_.bind(this)); $.subscribe(Events.USER_SETTINGS_CHANGED, $.proxy(this.onUserSettingsChange_, this)); @@ -66,9 +69,9 @@ }; ns.PreviewController.prototype.initTooltips_ = function () { - var onionSkinTooltip = pskl.utils.TooltipFormatter.format('Toggle onion skin', ONION_SKIN_SHORTCUT); + var onionSkinTooltip = pskl.utils.TooltipFormatter.format('Toggle onion skin', this.onionSkinShortcut); this.toggleOnionSkinButton.setAttribute('title', onionSkinTooltip); - var originalSizeTooltip = pskl.utils.TooltipFormatter.format('Original size preview', ORIGINAL_SIZE_SHORTCUT); + var originalSizeTooltip = pskl.utils.TooltipFormatter.format('Original size preview', this.originalSizeShortcut); this.originalSizeButton.setAttribute('title', originalSizeTooltip); }; diff --git a/src/js/selection/SelectionManager.js b/src/js/selection/SelectionManager.js index 9b95e6f7..6df6f923 100644 --- a/src/js/selection/SelectionManager.js +++ b/src/js/selection/SelectionManager.js @@ -18,11 +18,11 @@ $.subscribe(Events.SELECTION_DISMISSED, $.proxy(this.onSelectionDismissed_, this)); $.subscribe(Events.SELECTION_MOVE_REQUEST, $.proxy(this.onSelectionMoved_, this)); - pskl.app.shortcutService.registerShortcut('ctrl+V', this.paste.bind(this)); - pskl.app.shortcutService.registerShortcut('ctrl+X', this.cut.bind(this)); - pskl.app.shortcutService.registerShortcut('ctrl+C', this.copy.bind(this)); - pskl.app.shortcutService.registerShortcut('del', this.erase.bind(this)); - pskl.app.shortcutService.registerShortcut('back', this.onBackPressed_.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)); $.subscribe(Events.TOOL_SELECTED, $.proxy(this.onToolSelected_, this)); }; @@ -54,7 +54,7 @@ this.cleanSelection_(); }; - ns.SelectionManager.prototype.onBackPressed_ = function(evt) { + ns.SelectionManager.prototype.onDeleteShortcut_ = function(evt) { if (this.currentSelection) { this.erase(); } else { diff --git a/src/js/service/HistoryService.js b/src/js/service/HistoryService.js index 653f0f26..8c24ebed 100644 --- a/src/js/service/HistoryService.js +++ b/src/js/service/HistoryService.js @@ -26,8 +26,9 @@ ns.HistoryService.prototype.init = function () { $.subscribe(Events.PISKEL_SAVE_STATE, this.onSaveStateEvent.bind(this)); - this.shortcutService.registerShortcut('ctrl+Z', this.undo.bind(this)); - this.shortcutService.registerShortcuts(['ctrl+Y', 'ctrl+shift+Z'] , this.redo.bind(this)); + var shortcuts = pskl.service.keyboard.Shortcuts; + this.shortcutService.registerShortcut(shortcuts.MISC.UNDO, this.undo.bind(this)); + this.shortcutService.registerShortcut(shortcuts.MISC.REDO, this.redo.bind(this)); this.saveState({ type : ns.HistoryService.SNAPSHOT diff --git a/src/js/service/keyboard/CheatsheetService.js b/src/js/service/keyboard/CheatsheetService.js index faf6a19e..de306db8 100644 --- a/src/js/service/keyboard/CheatsheetService.js +++ b/src/js/service/keyboard/CheatsheetService.js @@ -1,8 +1,13 @@ (function () { var ns = $.namespace('pskl.service.keyboard'); + /** + * TODO : JD : This is not a service, but a controller + * Moreover this should be handled by the DialogsController + */ ns.CheatsheetService = function () { this.isDisplayed = false; + this.closePopupShortcut = pskl.service.keyboard.Shortcuts.MISC.CLOSE_POPUP; }; ns.CheatsheetService.prototype.init = function () { @@ -13,7 +18,9 @@ } this.initMarkup_(); - pskl.app.shortcutService.registerShortcuts(['?', 'shift+?'], this.toggleCheatsheet_.bind(this)); + + var cheatsheetShortcut = pskl.service.keyboard.Shortcuts.MISC.CHEATSHEET; + pskl.app.shortcutService.registerShortcut(cheatsheetShortcut, this.toggleCheatsheet_.bind(this)); pskl.utils.Event.addEventListener(document.body, 'click', this.onBodyClick_, this); @@ -46,13 +53,13 @@ }; ns.CheatsheetService.prototype.showCheatsheet_ = function () { - pskl.app.shortcutService.registerShortcut('ESC', this.hideCheatsheet_.bind(this)); + pskl.app.shortcutService.registerShortcut(this.closePopupShortcut, this.hideCheatsheet_.bind(this)); this.cheatsheetEl.style.display = 'block'; this.isDisplayed = true; }; ns.CheatsheetService.prototype.hideCheatsheet_ = function () { - pskl.app.shortcutService.unregisterShortcut('ESC'); + pskl.app.shortcutService.unregisterShortcut(this.closePopupShortcut); this.cheatsheetEl.style.display = 'none'; this.isDisplayed = false; }; @@ -65,71 +72,56 @@ }; ns.CheatsheetService.prototype.initMarkupForTools_ = function () { - var descriptors = pskl.app.toolController.tools.map(function (tool) { - return this.toDescriptor_(tool.shortcut, tool.getHelpText(), 'tool-icon ' + tool.toolId); - }.bind(this)); + var descriptors = this.createShortcutDescriptors_(ns.Shortcuts.TOOL, this.getToolShortcutClassname_); + this.initMarkupForDescriptors_(descriptors, '.cheatsheet-tool-shortcuts'); + }; - var container = this.cheatsheetEl.querySelector('.cheatsheet-tool-shortcuts'); - this.initMarkupForDescriptors_(descriptors, container); + ns.CheatsheetService.prototype.getToolShortcutClassname_ = function (shortcut) { + return 'tool-icon ' + shortcut.getId(); }; ns.CheatsheetService.prototype.initMarkupForMisc_ = function () { - var descriptors = [ - this.toDescriptor_('0', 'Reset zoom level'), - this.toDescriptor_('+/-', 'Zoom in/Zoom out'), - this.toDescriptor_('ctrl + Z', 'Undo'), - this.toDescriptor_('ctrl + Y', 'Redo'), - this.toDescriptor_('↑', 'Select previous frame'), /* ASCII for up-arrow */ - this.toDescriptor_('↓', 'Select next frame'), /* ASCII for down-arrow */ - this.toDescriptor_('N', 'Create new frame'), - this.toDescriptor_('shift + N', 'Duplicate selected frame'), - this.toDescriptor_('shift + ?', 'Open/Close this popup'), - this.toDescriptor_('alt + 1', 'Toggle original size preview'), - this.toDescriptor_('alt + O', 'Toggle Onion Skin'), - this.toDescriptor_('alt + L', 'Toggle Layer Preview') - ]; - - var container = this.cheatsheetEl.querySelector('.cheatsheet-misc-shortcuts'); - this.initMarkupForDescriptors_(descriptors, container); + var descriptors = this.createShortcutDescriptors_(ns.Shortcuts.MISC); + this.initMarkupForDescriptors_(descriptors, '.cheatsheet-misc-shortcuts'); }; ns.CheatsheetService.prototype.initMarkupForColors_ = function () { - var descriptors = [ - this.toDescriptor_('X', 'Swap primary/secondary colors'), - this.toDescriptor_('D', 'Reset default colors'), - this.toDescriptor_('alt + P', 'Create a Palette'), - this.toDescriptor_('</>', 'Select prev/next palette color'), - this.toDescriptor_('1 to 9', 'Select palette color at index') - ]; - - var container = this.cheatsheetEl.querySelector('.cheatsheet-colors-shortcuts'); - this.initMarkupForDescriptors_(descriptors, container); + var descriptors = this.createShortcutDescriptors_(ns.Shortcuts.COLOR); + this.initMarkupForDescriptors_(descriptors, '.cheatsheet-colors-shortcuts'); }; ns.CheatsheetService.prototype.initMarkupForSelection_ = function () { - var descriptors = [ - this.toDescriptor_('ctrl + X', 'Cut selection'), - this.toDescriptor_('ctrl + C', 'Copy selection'), - this.toDescriptor_('ctrl + V', 'Paste selection'), - this.toDescriptor_('del', 'Delete selection') - ]; - - var container = this.cheatsheetEl.querySelector('.cheatsheet-selection-shortcuts'); - this.initMarkupForDescriptors_(descriptors, container); + var descriptors = this.createShortcutDescriptors_(ns.Shortcuts.SELECTION); + this.initMarkupForDescriptors_(descriptors, '.cheatsheet-selection-shortcuts'); }; - ns.CheatsheetService.prototype.toDescriptor_ = function (shortcut, description, icon) { + ns.CheatsheetService.prototype.createShortcutDescriptors_ = function (shortcutMap, classnameProvider) { + return Object.keys(shortcutMap).map(function (shortcutKey) { + var shortcut = shortcutMap[shortcutKey]; + var classname = typeof classnameProvider == 'function' ? classnameProvider(shortcut) : ''; + return this.toDescriptor_(shortcut.getKey(), shortcut.getDescription(), classname); + }.bind(this)); + }; + + ns.CheatsheetService.prototype.toDescriptor_ = function (key, description, icon) { if (pskl.utils.UserAgent.isMac) { - shortcut = shortcut.replace('ctrl', 'cmd'); + key = key.replace('ctrl', 'cmd'); } + key = key.replace('up', '↑'); + key = key.replace('down', '↓'); + key = key.replace(/>/g, '>'); + key = key.replace(/} defaultKey combination of modifiers + ([a-z0-9] or a special key) + * Special keys are defined in KeycodeTranslator. If the shortcut supports several keys, + * use an array of String keys + */ + ns.Shortcut = function (id, description, defaultKey, displayKey) { + this.id_ = id; + this.description_ = description; + this.defaultKey_ = defaultKey; + this.displayKey_ = displayKey; + }; + + ns.Shortcut.USER_SETTINGS_PREFIX = 'shortcut.'; + + ns.Shortcut.prototype.getId = function () { + return this.id_; + }; + + ns.Shortcut.prototype.getDescription = function () { + return this.description_; + }; + + /** + * Retrieve the array of String keys that match this shortcut + * @return {Array} array of keys + */ + ns.Shortcut.prototype.getKeys = function () { + var keys = pskl.UserSettings.get(ns.Shortcut.USER_SETTINGS_PREFIX + this.id_) || this.defaultKey_; + if (typeof keys === 'string') { + keys = [keys]; + } + + return keys; + }; + + /** + * Get the key to be displayed for this shortcut, if + * @return {[type]} [description] + */ + ns.Shortcut.prototype.getKey = function () { + if (this.displayKey_) { + return this.displayKey_; + } + + var keys = this.getKeys(); + if (Array.isArray(keys) && keys.length > 0) { + return keys[0]; + } + + return ''; + }; + +})(); diff --git a/src/js/service/keyboard/ShortcutService.js b/src/js/service/keyboard/ShortcutService.js index c68b11dd..1ad4edd7 100644 --- a/src/js/service/keyboard/ShortcutService.js +++ b/src/js/service/keyboard/ShortcutService.js @@ -2,7 +2,7 @@ var ns = $.namespace('pskl.service.keyboard'); ns.ShortcutService = function () { - this.shortcuts_ = {}; + this.shortcuts_ = []; }; /** @@ -14,43 +14,34 @@ /** * Add a keyboard shortcut - * @param {String} rawKey (case insensitive) a key is a combination of modifiers + ([a-z0-9] or - * a special key) (check list of supported special keys in KeycodeTranslator) - * eg. 'ctrl+A', - * 'del' - * 'ctrl+shift+S' + * @param {pskl.service.keyboard.Shortcut} shortcut * @param {Function} callback should return true to let the original event perform its default action */ - ns.ShortcutService.prototype.registerShortcut = function (rawKey, callback) { - var parsedKey = this.parseKey_(rawKey.toLowerCase()); - - var key = parsedKey.key; - var meta = parsedKey.meta; - - this.shortcuts_[key] = this.shortcuts_[key] || {}; - - if (this.shortcuts_[key][meta]) { - var keyStr = (meta !== 'normal' ? meta + ' + ' : '') + key; - console.error('[ShortcutService] >>> Shortcut [' + keyStr + '] already registered'); - } else { - this.shortcuts_[key][meta] = callback; + ns.ShortcutService.prototype.registerShortcut = function (shortcut, callback) { + if (!(shortcut instanceof ns.Shortcut)) { + throw 'Invalid shortcut argument, please use instances of pskl.service.keyboard.Shortcut'; } + + if (typeof callback != 'function') { + throw 'Invalid callback argument, please provide a function'; + } + + this.shortcuts_.push({ + shortcut : shortcut, + callback : callback + }); }; - ns.ShortcutService.prototype.registerShortcuts = function (keys, callback) { - keys.forEach(function (key) { - this.registerShortcut(key, callback); - }.bind(this)); - }; - - ns.ShortcutService.prototype.unregisterShortcut = function (rawKey) { - var parsedKey = this.parseKey_(rawKey.toLowerCase()); - var key = parsedKey.key; - var meta = parsedKey.meta; - - this.shortcuts_[key] = this.shortcuts_[key] || {}; - - this.shortcuts_[key][meta] = null; + ns.ShortcutService.prototype.unregisterShortcut = function (shortcut) { + var index = -1; + this.shortcuts_.forEach(function (s, i) { + if (s.shortcut === shortcut) { + index = i; + } + }); + if (index != -1) { + this.shortcuts_.splice(index, 1); + } }; ns.ShortcutService.prototype.parseKey_ = function (key) { @@ -62,51 +53,58 @@ var parts = key.split(/\+(?!$)/); key = parts[parts.length - 1]; - return {meta : meta, key : key}; + return {meta : meta, key : key.toLowerCase()}; }; + /** + * Retrieve a comparable representation of a meta information for a key + * 'alt' 'ctrl' and 'shift' will always be in the same order for the same meta + */ ns.ShortcutService.prototype.getMetaKey_ = function (meta) { var keyBuffer = []; - ['alt', 'ctrl', 'shift'].forEach(function (metaKey) { - if (meta[metaKey]) { - keyBuffer.push(metaKey); - } - }); - if (keyBuffer.length > 0) { - return keyBuffer.join('+'); - } else { - return 'normal'; + if (meta.alt) { + keyBuffer.push('alt'); } + if (meta.ctrl) { + keyBuffer.push('ctrl'); + } + if (meta.shift) { + keyBuffer.push('shift'); + } + + return keyBuffer.join('+') || 'normal'; }; /** * @private */ ns.ShortcutService.prototype.onKeyUp_ = function(evt) { - if (!this.isInInput_(evt)) { - // jquery names FTW ... - var keycode = evt.which; - var charkey = pskl.service.keyboard.KeycodeTranslator.toChar(keycode); - - var keyShortcuts = this.shortcuts_[charkey]; - if (keyShortcuts) { - var meta = this.getMetaKey_({ - alt : this.isAltKeyPressed_(evt), - shift : this.isShiftKeyPressed_(evt), - ctrl : this.isCtrlKeyPressed_(evt) - }); - var cb = keyShortcuts[meta]; - - if (cb) { - var bubble = cb(charkey); - if (bubble !== true) { - evt.preventDefault(); - } - $.publish(Events.KEYBOARD_EVENT, [evt]); - } - } + if (this.isInInput_(evt)) { + return; } + + var keycode = evt.which; + var eventKey = pskl.service.keyboard.KeycodeTranslator.toChar(keycode); + var eventMeta = this.getMetaKey_({ + alt : evt.altKey, + shift : evt.shiftKey, + ctrl : this.isCtrlKeyPressed_(evt) + }); + + this.shortcuts_.forEach(function (shortcutInfo) { + shortcutInfo.shortcut.getKeys().forEach(function (key) { + if (!this.isKeyMatching_(key, eventKey, eventMeta)) { + return; + } + + var bubble = shortcutInfo.callback(eventKey); + if (bubble !== true) { + evt.preventDefault(); + } + $.publish(Events.KEYBOARD_EVENT, [evt]); + }.bind(this)); + }.bind(this)); }; ns.ShortcutService.prototype.isInInput_ = function (evt) { @@ -114,15 +112,12 @@ return targetTagName === 'INPUT' || targetTagName === 'TEXTAREA'; }; + ns.ShortcutService.prototype.isKeyMatching_ = function (key, eventKey, eventMeta) { + var parsedKey = this.parseKey_(key); + return parsedKey.key === eventKey && parsedKey.meta === eventMeta; + }; + ns.ShortcutService.prototype.isCtrlKeyPressed_ = function (evt) { return pskl.utils.UserAgent.isMac ? evt.metaKey : evt.ctrlKey; }; - - ns.ShortcutService.prototype.isShiftKeyPressed_ = function (evt) { - return evt.shiftKey; - }; - - ns.ShortcutService.prototype.isAltKeyPressed_ = function (evt) { - return evt.altKey; - }; })(); diff --git a/src/js/service/keyboard/Shortcuts.js b/src/js/service/keyboard/Shortcuts.js new file mode 100644 index 00000000..77cfdc9f --- /dev/null +++ b/src/js/service/keyboard/Shortcuts.js @@ -0,0 +1,70 @@ +(function () { + var ns = $.namespace('pskl.service.keyboard'); + + var createShortcut = function (id, description, defaultKey, displayKey) { + return new ns.Shortcut(id, description, defaultKey, displayKey); + }; + + ns.Shortcuts = { + /** + * Syntax : createShortcut(id, description, default key(s)) + */ + TOOL : { + PEN : createShortcut('tool-pen', 'Pen tool', 'P'), + MIRROR_PEN : createShortcut('tool-vertical-mirror-pen', 'Vertical mirror pen tool', 'V'), + PAINT_BUCKET : createShortcut('tool-paint-bucket', 'Paint bucket tool', 'B'), + COLORSWAP : createShortcut('tool-colorswap', 'Magic bucket tool', 'A'), + ERASER : createShortcut('tool-eraser', 'Eraser pen tool', 'E'), + STROKE : createShortcut('tool-stroke', 'Stroke tool', 'L'), + RECTANGLE : createShortcut('tool-rectangle', 'Rectangle tool', 'R'), + CIRCLE : createShortcut('tool-circle', 'Circle tool', 'C'), + MOVE : createShortcut('tool-move', 'Move tool', 'M'), + SHAPE_SELECT : createShortcut('tool-shape-select', 'Shape selection', 'Z'), + RECTANGLE_SELECT : createShortcut('tool-rectangle-select', 'Rectangle selection', 'S'), + LASSO_SELECT : createShortcut('tool-lasso-select', 'Lasso selection', 'H'), + LIGHTEN : createShortcut('tool-lighten', 'Lighten tool', 'U'), + DITHERING : createShortcut('tool-dithering', 'Dithering tool', 'T'), + COLORPICKER : createShortcut('tool-colorpicker', 'Color picker', 'O') + }, + + SELECTION : { + CUT : createShortcut('selection-cut', 'Cut selection', 'ctrl+X'), + COPY : createShortcut('selection-copy', 'Copy selection', 'ctrl+C'), + PASTE : createShortcut('selection-cut', 'Paste selection', 'ctrl+V'), + DELETE : createShortcut('selection-delete', 'Delete selection', ['del', 'back']) + }, + + MISC : { + RESET_ZOOM : createShortcut('reset-zoom', 'Reset zoom level', '0'), + INCREASE_ZOOM : createShortcut('increase-zoom', 'Increase zoom level', '+'), + DECREASE_ZOOM : createShortcut('decrease-zoom', 'Decrease zoom level', '-'), + UNDO : createShortcut('undo', 'Undo', 'ctrl+Z'), + REDO : createShortcut('redo', 'Redo', ['ctrl+Y', 'ctrl+shift+Z']), + PREVIOUS_FRAME : createShortcut('previous-frame', 'Select previous frame', 'up'), + NEXT_FRAME : createShortcut('next-frame', 'Select next frame', 'down'), + NEW_FRAME : createShortcut('new-frame', 'Create new empty frame', 'N'), + DUPLICATE_FRAME : createShortcut('duplicate-frame', 'Duplicate selected frame', 'shift+N'), + CHEATSHEET : createShortcut('cheatsheet', 'Open the keyboard shortcut cheatsheet', 'shift+?'), + X1_PREVIEW : createShortcut('x1-preview', 'Toggle original size preview', 'alt+1'), + ONION_SKIN : createShortcut('onion-skin', 'Toggle onion skin', 'alt+O'), + LAYER_PREVIEW : createShortcut('layer-preview', 'Toggle layer preview', 'alt+L'), + CLOSE_POPUP : createShortcut('close-popup', 'Close an opened popup', 'ESC') + }, + + STORAGE : { + OPEN : createShortcut('open', '(Desktop only) Open a .piskel file', 'ctrl+O'), + SAVE : createShortcut('save', 'Save the current sprite', 'ctrl+S'), + SAVE_AS : createShortcut('save-as', '(Desktop only) Save as a new .piskel file', 'ctrl+shift+S') + }, + + COLOR : { + SWAP : createShortcut('swap-colors', 'Swap primary/secondary colors', 'X'), + RESET : createShortcut('reset-colors', 'Reset default colors', 'D'), + CREATE_PALETTE : createShortcut('create-palette', 'Open the palette creation popup', 'alt+P'), + PREVIOUS_COLOR : createShortcut('previous-color', 'Select the previous color in the current palette', '<'), + NEXT_COLOR : createShortcut('next-color', 'Select the next color in the current palette', '>'), + SELECT_COLOR : createShortcut('select-color', 'Select a palette color in the current palette', + '123456789'.split(''), '1 to 9') + } + }; +})(); diff --git a/src/js/service/storage/StorageService.js b/src/js/service/storage/StorageService.js index 583b9aee..0973fd19 100644 --- a/src/js/service/storage/StorageService.js +++ b/src/js/service/storage/StorageService.js @@ -10,9 +10,10 @@ }; ns.StorageService.prototype.init = function () { - pskl.app.shortcutService.registerShortcut('ctrl+o', this.onOpenKey_.bind(this)); - pskl.app.shortcutService.registerShortcut('ctrl+s', this.onSaveKey_.bind(this)); - pskl.app.shortcutService.registerShortcut('ctrl+shift+s', this.onSaveAsKey_.bind(this)); + var shortcuts = pskl.service.keyboard.Shortcuts; + pskl.app.shortcutService.registerShortcut(shortcuts.STORAGE.OPEN, this.onOpenKey_.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.STORAGE.SAVE, this.onSaveKey_.bind(this)); + pskl.app.shortcutService.registerShortcut(shortcuts.STORAGE.SAVE_AS, this.onSaveAsKey_.bind(this)); $.subscribe(Events.BEFORE_SAVING_PISKEL, this.setSavingFlag_.bind(this, true)); $.subscribe(Events.AFTER_SAVING_PISKEL, this.setSavingFlag_.bind(this, false)); diff --git a/src/js/tools/IconMarkupRenderer.js b/src/js/tools/IconMarkupRenderer.js index 03ba1369..572a41da 100644 --- a/src/js/tools/IconMarkupRenderer.js +++ b/src/js/tools/IconMarkupRenderer.js @@ -3,19 +3,19 @@ ns.IconMarkupRenderer = function () {}; - ns.IconMarkupRenderer.prototype.render = function (tool, shortcut, tooltipPosition) { + ns.IconMarkupRenderer.prototype.render = function (tool, tooltipPosition) { tooltipPosition = tooltipPosition || 'right'; var tpl = pskl.utils.Template.get('drawingTool-item-template'); return pskl.utils.Template.replace(tpl, { cssclass : ['tool-icon', tool.toolId].join(' '), toolid : tool.toolId, - title : this.getTooltipText(tool, shortcut), + title : this.getTooltipText(tool), tooltipposition : tooltipPosition }); }; - ns.IconMarkupRenderer.prototype.getTooltipText = function(tool, shortcut) { + ns.IconMarkupRenderer.prototype.getTooltipText = function(tool) { var descriptors = tool.tooltipDescriptors; - return pskl.utils.TooltipFormatter.format(tool.getHelpText(), shortcut, descriptors); + return pskl.utils.TooltipFormatter.format(tool.getHelpText(), tool.shortcut, descriptors); }; })(); diff --git a/src/js/tools/drawing/Circle.js b/src/js/tools/drawing/Circle.js index 42c12364..92bf9a36 100644 --- a/src/js/tools/drawing/Circle.js +++ b/src/js/tools/drawing/Circle.js @@ -11,7 +11,7 @@ this.toolId = 'tool-circle'; this.helpText = 'Circle tool'; - this.shortcut = 'C'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.CIRCLE; }; pskl.utils.inherit(ns.Circle, ns.ShapeTool); diff --git a/src/js/tools/drawing/ColorPicker.js b/src/js/tools/drawing/ColorPicker.js index d25dedb5..9a2b7213 100644 --- a/src/js/tools/drawing/ColorPicker.js +++ b/src/js/tools/drawing/ColorPicker.js @@ -9,7 +9,7 @@ ns.ColorPicker = function() { this.toolId = 'tool-colorpicker'; this.helpText = 'Color picker'; - this.shortcut = 'O'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.COLORPICKER; }; pskl.utils.inherit(ns.ColorPicker, ns.BaseTool); diff --git a/src/js/tools/drawing/ColorSwap.js b/src/js/tools/drawing/ColorSwap.js index a043a623..42c648c9 100644 --- a/src/js/tools/drawing/ColorSwap.js +++ b/src/js/tools/drawing/ColorSwap.js @@ -8,7 +8,7 @@ ns.ColorSwap = function() { this.toolId = 'tool-colorswap'; this.helpText = 'Paint all pixels of the same color'; - this.shortcut = 'A'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.COLORSWAP; this.tooltipDescriptors = [ {key : 'ctrl', description : 'Apply to all layers'}, diff --git a/src/js/tools/drawing/DitheringTool.js b/src/js/tools/drawing/DitheringTool.js index 4fd2b5b7..a7ce716e 100644 --- a/src/js/tools/drawing/DitheringTool.js +++ b/src/js/tools/drawing/DitheringTool.js @@ -10,7 +10,7 @@ ns.SimplePen.call(this); this.toolId = 'tool-dithering'; this.helpText = 'Dithering tool'; - this.shortcut = 'T'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.DITHERING; }; pskl.utils.inherit(ns.DitheringTool, ns.SimplePen); diff --git a/src/js/tools/drawing/Eraser.js b/src/js/tools/drawing/Eraser.js index 036b0892..2b7b01c3 100644 --- a/src/js/tools/drawing/Eraser.js +++ b/src/js/tools/drawing/Eraser.js @@ -12,7 +12,7 @@ this.toolId = 'tool-eraser'; this.helpText = 'Eraser tool'; - this.shortcut = 'E'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.ERASER; }; pskl.utils.inherit(ns.Eraser, ns.SimplePen); diff --git a/src/js/tools/drawing/Lighten.js b/src/js/tools/drawing/Lighten.js index 7be4ef64..3f6e7254 100644 --- a/src/js/tools/drawing/Lighten.js +++ b/src/js/tools/drawing/Lighten.js @@ -13,7 +13,7 @@ this.toolId = 'tool-lighten'; this.helpText = 'Lighten'; - this.shortcut = 'U'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.LIGHTEN; this.tooltipDescriptors = [ {key : 'ctrl', description : 'Darken'}, diff --git a/src/js/tools/drawing/Move.js b/src/js/tools/drawing/Move.js index a5fd0ea8..fc4fae03 100644 --- a/src/js/tools/drawing/Move.js +++ b/src/js/tools/drawing/Move.js @@ -9,7 +9,7 @@ ns.Move = function() { this.toolId = ns.Move.TOOL_ID; this.helpText = 'Move tool'; - this.shortcut = 'M'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.MOVE; this.tooltipDescriptors = [ {key : 'ctrl', description : 'Apply to all layers'}, diff --git a/src/js/tools/drawing/PaintBucket.js b/src/js/tools/drawing/PaintBucket.js index d264b939..7f8deb89 100644 --- a/src/js/tools/drawing/PaintBucket.js +++ b/src/js/tools/drawing/PaintBucket.js @@ -9,7 +9,7 @@ ns.PaintBucket = function() { this.toolId = 'tool-paint-bucket'; this.helpText = 'Paint bucket tool'; - this.shortcut = 'B'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.PAINT_BUCKET; }; pskl.utils.inherit(ns.PaintBucket, ns.BaseTool); diff --git a/src/js/tools/drawing/Rectangle.js b/src/js/tools/drawing/Rectangle.js index 8074e97b..f74721db 100644 --- a/src/js/tools/drawing/Rectangle.js +++ b/src/js/tools/drawing/Rectangle.js @@ -11,7 +11,7 @@ this.toolId = 'tool-rectangle'; this.helpText = 'Rectangle tool'; - this.shortcut = 'R'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.RECTANGLE; }; pskl.utils.inherit(ns.Rectangle, ns.ShapeTool); diff --git a/src/js/tools/drawing/SimplePen.js b/src/js/tools/drawing/SimplePen.js index 7b8c5a2c..19741732 100644 --- a/src/js/tools/drawing/SimplePen.js +++ b/src/js/tools/drawing/SimplePen.js @@ -9,7 +9,7 @@ ns.SimplePen = function() { this.toolId = 'tool-pen'; this.helpText = 'Pen tool'; - this.shortcut = 'P'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.PEN; this.previousCol = null; this.previousRow = null; diff --git a/src/js/tools/drawing/Stroke.js b/src/js/tools/drawing/Stroke.js index b86622f5..9d8d0b57 100644 --- a/src/js/tools/drawing/Stroke.js +++ b/src/js/tools/drawing/Stroke.js @@ -9,7 +9,7 @@ ns.Stroke = function() { this.toolId = 'tool-stroke'; this.helpText = 'Stroke tool'; - this.shortcut = 'L'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.STROKE; // Stroke's first point coordinates (set in applyToolAt) this.startCol = null; diff --git a/src/js/tools/drawing/VerticalMirrorPen.js b/src/js/tools/drawing/VerticalMirrorPen.js index 22a0d805..d51ce73b 100644 --- a/src/js/tools/drawing/VerticalMirrorPen.js +++ b/src/js/tools/drawing/VerticalMirrorPen.js @@ -6,7 +6,7 @@ this.toolId = 'tool-vertical-mirror-pen'; this.helpText = 'Vertical Mirror pen'; - this.shortcut = 'V'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.MIRROR_PEN; this.tooltipDescriptors = [ {key : 'ctrl', description : 'Use horizontal axis'}, diff --git a/src/js/tools/drawing/selection/LassoSelect.js b/src/js/tools/drawing/selection/LassoSelect.js index d34ae20d..7c9afa2c 100644 --- a/src/js/tools/drawing/selection/LassoSelect.js +++ b/src/js/tools/drawing/selection/LassoSelect.js @@ -11,7 +11,7 @@ this.toolId = 'tool-lasso-select'; this.helpText = 'Lasso selection'; - this.shortcut = 'H'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.LASSO_SELECT; }; pskl.utils.inherit(ns.LassoSelect, ns.AbstractDragSelect); diff --git a/src/js/tools/drawing/selection/RectangleSelect.js b/src/js/tools/drawing/selection/RectangleSelect.js index 26dfcc71..ede06430 100644 --- a/src/js/tools/drawing/selection/RectangleSelect.js +++ b/src/js/tools/drawing/selection/RectangleSelect.js @@ -11,7 +11,7 @@ this.toolId = 'tool-rectangle-select'; this.helpText = 'Rectangle selection'; - this.shortcut = 'S'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.RECTANGLE_SELECT; }; diff --git a/src/js/tools/drawing/selection/ShapeSelect.js b/src/js/tools/drawing/selection/ShapeSelect.js index 7bd72c9a..488a2af6 100644 --- a/src/js/tools/drawing/selection/ShapeSelect.js +++ b/src/js/tools/drawing/selection/ShapeSelect.js @@ -11,7 +11,7 @@ this.toolId = 'tool-shape-select'; this.helpText = 'Shape selection'; - this.shortcut = 'Z'; + this.shortcut = pskl.service.keyboard.Shortcuts.TOOL.SHAPE_SELECT; }; pskl.utils.inherit(ns.ShapeSelect, ns.BaseSelect); diff --git a/src/js/utils/TooltipFormatter.js b/src/js/utils/TooltipFormatter.js index b8b339dd..be3a3aec 100644 --- a/src/js/utils/TooltipFormatter.js +++ b/src/js/utils/TooltipFormatter.js @@ -5,7 +5,7 @@ ns.TooltipFormatter.format = function(helpText, shortcut, descriptors) { var tpl = pskl.utils.Template.get('tooltip-container-template'); - shortcut = shortcut ? '(' + shortcut + ')' : ''; + shortcut = shortcut ? '(' + shortcut.getKey() + ')' : ''; return pskl.utils.Template.replace(tpl, { helptext : helpText, shortcut : shortcut, diff --git a/src/js/utils/UserSettings.js b/src/js/utils/UserSettings.js index 27ca4387..a4d94dbe 100644 --- a/src/js/utils/UserSettings.js +++ b/src/js/utils/UserSettings.js @@ -91,6 +91,10 @@ * @private */ checkKeyValidity_ : function(key) { + if (key.indexOf(pskl.service.keyboard.Shortcut.USER_SETTINGS_PREFIX) === 0) { + return true; + } + var isValidKey = key in this.KEY_TO_DEFAULT_VALUE_MAP_; if (!isValidKey) { console.error('UserSettings key <' + key + '> not found in supported keys.'); diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index 2795063b..915a8e4e 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -157,6 +157,8 @@ "js/service/palette/reader/PaletteTxtReader.js", "js/service/palette/PaletteImportService.js", "js/service/SavedStatusService.js", + "js/service/keyboard/Shortcut.js", + "js/service/keyboard/Shortcuts.js", "js/service/keyboard/ShortcutService.js", "js/service/keyboard/KeycodeTranslator.js", "js/service/keyboard/CheatsheetService.js", diff --git a/test/js/service/keyboard/ShortcutServiceTest.js b/test/js/service/keyboard/ShortcutServiceTest.js new file mode 100644 index 00000000..a93d1c7d --- /dev/null +++ b/test/js/service/keyboard/ShortcutServiceTest.js @@ -0,0 +1,275 @@ +describe("ShortcutService test suite", function() { + + var A_KEY = 'A'; + var B_KEY = 'B'; + var A_KEYCODE = 65; + var B_KEYCODE = 66; + + var service; + + beforeEach(function() { + service = new pskl.service.keyboard.ShortcutService(); + }); + + var createEvent = function (keycode) { + return { + which : keycode, + altKey : false, + withAltKey : function () { + this.altKey = true; + return this; + }, + ctrlKey : false, + withCtrlKey : function () { + this.ctrlKey = true; + return this; + }, + shiftKey : false, + withShiftKey : function () { + this.shiftKey = true; + return this; + }, + preventDefaultCalled : false, + preventDefault : function () { + this.preventDefaultCalled = true; + }, + target : { + nodeName : 'div' + }, + setNodeName : function (nodeName) { + this.target.nodeName = nodeName; + return this; + } + }; + }; + + var setTargetName = function (evt, targetName) { + evt.target = { + nodeName : targetName + }; + }; + + it("accepts only shortcut instances", function() { + console.log('[ShortcutService] accepts only shortcut instances'); + + console.log('[ShortcutService] ... fails for missing shortcut'); + expect(function () { + service.registerShortcut(); + }).toThrow('Invalid shortcut argument, please use instances of pskl.service.keyboard.Shortcut'); + + console.log('[ShortcutService] ... fails for shortcut as empty object'); + expect(function () { + service.registerShortcut({}); + }).toThrow('Invalid shortcut argument, please use instances of pskl.service.keyboard.Shortcut'); + + console.log('[ShortcutService] ... fails for shortcut as a string'); + expect(function () { + service.registerShortcut('alt+F4'); + }).toThrow('Invalid shortcut argument, please use instances of pskl.service.keyboard.Shortcut'); + + var shortcut = new pskl.service.keyboard.Shortcut('shortcut-id', '', A_KEY); + + console.log('[ShortcutService] ... fails for missing callback'); + expect(function () { + service.registerShortcut(shortcut); + }).toThrow('Invalid callback argument, please provide a function'); + + console.log('[ShortcutService] ... fails for invalid callback'); + expect(function () { + service.registerShortcut(shortcut, {callback : function () {}}); + }).toThrow('Invalid callback argument, please provide a function'); + + console.log('[ShortcutService] ... is ok for valid arguments'); + service.registerShortcut(shortcut, function () {}); + }); + + it ("triggers shortcut", function () { + console.log('[ShortcutService] triggers shortcut'); + var callbackCalled = false; + + console.log('[ShortcutService] ... register shortcut for A'); + var shortcutA = new pskl.service.keyboard.Shortcut('shortcut-a', '', A_KEY); + service.registerShortcut(shortcutA, function () { + callbackCalled = true; + }); + + console.log('[ShortcutService] ... verify shortcut is called'); + service.onKeyUp_(createEvent(A_KEYCODE)); + expect(callbackCalled).toBe(true); + }); + + it ("triggers shortcuts independently", function () { + console.log('[ShortcutService] registers shortcuts'); + + var shortcutA = new pskl.service.keyboard.Shortcut('shortcut-a', '', A_KEY); + var shortcutB = new pskl.service.keyboard.Shortcut('shortcut-b', '', B_KEY); + var shortcutA_B = new pskl.service.keyboard.Shortcut('shortcut-a&b', '', [A_KEY, B_KEY]); + + var counters = { + a : 0, + b : 0, + a_b : 0 + }; + + console.log('[ShortcutService] ... register separate shortcuts for A and B'); + service.registerShortcut(shortcutA, function () { + counters.a++; + }); + service.registerShortcut(shortcutB, function () { + counters.b++; + }); + service.registerShortcut(shortcutA_B, function () { + counters.a_b++; + }); + + console.log('[ShortcutService] ... trigger A, expect counter A at 1, B at 0, A_B at 1'); + service.onKeyUp_(createEvent(A_KEYCODE)); + expect(counters.a).toBe(1); + expect(counters.b).toBe(0); + expect(counters.a_b).toBe(1); + + console.log('[ShortcutService] ... trigger A, expect counter A at 1, B at 1, A_B at 2'); + service.onKeyUp_(createEvent(B_KEYCODE)); + expect(counters.a).toBe(1); + expect(counters.b).toBe(1); + expect(counters.a_b).toBe(2); + }); + + it ("unregisters shortcut", function () { + console.log('[ShortcutService] unregisters shortcut'); + var callbackCalled = false; + + console.log('[ShortcutService] ... register shortcut for A'); + var shortcutA = new pskl.service.keyboard.Shortcut('shortcut-a', '', A_KEY); + service.registerShortcut(shortcutA, function () { + callbackCalled = true; + }); + + console.log('[ShortcutService] ... unregister shortcut A'); + service.unregisterShortcut(shortcutA); + + console.log('[ShortcutService] ... verify shortcut callback is not called'); + service.onKeyUp_(createEvent(A_KEYCODE)); + expect(callbackCalled).toBe(false); + }); + + it ("unregisters shortcut without removing other shortcuts", function () { + console.log('[ShortcutService] unregisters shortcut'); + var callbackCalled = false; + + console.log('[ShortcutService] ... register shortcut for A & B'); + var shortcutA = new pskl.service.keyboard.Shortcut('shortcut-a', '', A_KEY); + var shortcutB = new pskl.service.keyboard.Shortcut('shortcut-b', '', B_KEY); + service.registerShortcut(shortcutA, function () {}); + service.registerShortcut(shortcutB, function () { + callbackCalled = true; + }); + + console.log('[ShortcutService] ... unregister shortcut A'); + service.unregisterShortcut(shortcutA); + + console.log('[ShortcutService] ... verify shortcut callback for B can still be called'); + service.onKeyUp_(createEvent(B_KEYCODE)); + expect(callbackCalled).toBe(true); + }); + + it ("supports unregistering unknown shortcuts", function () { + console.log('[ShortcutService] unregisters shortcut'); + var callbackCalled = false; + + console.log('[ShortcutService] ... register shortcut for A'); + var shortcutA = new pskl.service.keyboard.Shortcut('shortcut-a', '', A_KEY); + service.registerShortcut(shortcutA, function () { + callbackCalled = true; + }); + + console.log('[ShortcutService] ... unregister shortcut B, which was not registered in the first place'); + var shortcutB = new pskl.service.keyboard.Shortcut('shortcut-b', '', B_KEY); + service.unregisterShortcut(shortcutB); + + console.log('[ShortcutService] ... verify shortcut callback for A can still be called'); + callbackCalled = false; + service.onKeyUp_(createEvent(A_KEYCODE)); + expect(callbackCalled).toBe(true); + }); + + it ("does not trigger shortcuts from INPUT or TEXTAREA", function () { + console.log('[ShortcutService] triggers shortcut'); + var callbackCalled = false; + + console.log('[ShortcutService] ... register shortcut for A'); + var shortcutA = new pskl.service.keyboard.Shortcut('shortcut-a', '', A_KEY); + service.registerShortcut(shortcutA, function () { + callbackCalled = true; + }); + + console.log('[ShortcutService] ... verify shortcut is not called from event on INPUT'); + service.onKeyUp_(createEvent(A_KEYCODE).setNodeName('INPUT')); + expect(callbackCalled).toBe(false); + + console.log('[ShortcutService] ... verify shortcut is not called from event on TEXTAREA'); + service.onKeyUp_(createEvent(A_KEYCODE).setNodeName('TEXTAREA')); + expect(callbackCalled).toBe(false); + + console.log('[ShortcutService] ... verify shortcut is called from event on LINK'); + service.onKeyUp_(createEvent(A_KEYCODE).setNodeName('A')); + expect(callbackCalled).toBe(true); + }); + + it ("supports meta modifiers", function () { + console.log('[ShortcutService] triggers shortcut'); + var callbackCalled = false; + + console.log('[ShortcutService] ... create various A shortcuts with modifiers'); + var shortcuts = [ + new pskl.service.keyboard.Shortcut('a', '', A_KEY), + new pskl.service.keyboard.Shortcut('a_ctrl', '', 'ctrl+' + A_KEY), + new pskl.service.keyboard.Shortcut('a_ctrl_shift', '', 'ctrl+shift+' + A_KEY), + new pskl.service.keyboard.Shortcut('a_ctrl_shift_alt', '', 'ctrl+shift+alt+' + A_KEY), + new pskl.service.keyboard.Shortcut('a_alt', '', 'alt+' + A_KEY) + ]; + + var counters = { + a : 0, + a_ctrl : 0, + a_ctrl_shift : 0, + a_ctrl_shift_alt : 0, + a_alt : 0, + }; + + shortcuts.forEach(function (shortcut) { + service.registerShortcut(shortcut, function () { + counters[shortcut.getId()]++; + }); + }); + + var verifyCounters = function (a, a_c, a_cs, a_csa, a_a) { + expect(counters.a).toBe(a); + expect(counters.a_ctrl).toBe(a_c); + expect(counters.a_ctrl_shift).toBe(a_cs); + expect(counters.a_ctrl_shift_alt).toBe(a_csa); + expect(counters.a_alt).toBe(a_a); + }; + + console.log('[ShortcutService] ... trigger A, expect counters CTRL+A, CTRL+SHIFT+A, CTRL+SHIFT+ALT+A, ALT+A to remain at 0'); + service.onKeyUp_(createEvent(A_KEYCODE)); + verifyCounters(1,0,0,0,0); + + console.log('[ShortcutService] ... trigger CTRL+A, expect counters CTRL+SHIFT+A, CTRL+SHIFT+ALT+A, ALT+A to remain at 0'); + service.onKeyUp_(createEvent(A_KEYCODE).withCtrlKey()); + verifyCounters(1,1,0,0,0); + + console.log('[ShortcutService] ... trigger CTRL+A, expect counters CTRL+SHIFT+ALT+A, ALT+A to remain at 0'); + service.onKeyUp_(createEvent(A_KEYCODE).withCtrlKey().withShiftKey()); + verifyCounters(1,1,1,0,0); + + console.log('[ShortcutService] ... trigger CTRL+A, expect counter ALT+A to remain at 0'); + service.onKeyUp_(createEvent(A_KEYCODE).withCtrlKey().withShiftKey().withAltKey()); + verifyCounters(1,1,1,1,0); + + console.log('[ShortcutService] ... trigger CTRL+A, expect all counters at 1'); + service.onKeyUp_(createEvent(A_KEYCODE).withAltKey()); + verifyCounters(1,1,1,1,1); + }); + +}); \ No newline at end of file