From 6eabf01ffc50c6970f11c96d83b17faef4c1acbe Mon Sep 17 00:00:00 2001 From: jdescottes Date: Tue, 19 Nov 2013 23:46:33 +0100 Subject: [PATCH] feature : add keyboard shortcuts + decentralized shortcut declaration + each service/controller is now responsible for declaring its shorcuts - documentation (cheatsheet) is still to be maintained manually - init order matters (shortcutService has to be instanciated before everyone else) => should have a standalone KeyboardService singleton which is ready as soon as it is loaded --- js/Events.js | 9 +-- js/app.js | 12 ++-- js/controller/PaletteController.js | 11 ++- js/controller/PiskelController.js | 19 +++++ js/controller/ToolController.js | 43 ++++++----- js/selection/SelectionManager.js | 31 +++----- js/service/HistoryService.js | 5 +- js/service/KeyboardEventService.js | 72 ------------------- js/service/keyboard/CheatsheetService.js | 18 ++++- js/service/keyboard/KeyboardEvent.js | 17 ----- js/service/keyboard/KeycodeTranslator.js | 7 +- js/service/keyboard/ShortcutService.js | 90 ++++++++++++++++++++++++ piskel-script-list.js | 2 +- 13 files changed, 190 insertions(+), 146 deletions(-) delete mode 100644 js/service/KeyboardEventService.js delete mode 100644 js/service/keyboard/KeyboardEvent.js create mode 100644 js/service/keyboard/ShortcutService.js diff --git a/js/Events.js b/js/Events.js index 329b5221..28e1107f 100644 --- a/js/Events.js +++ b/js/Events.js @@ -39,12 +39,5 @@ var Events = { HIDE_NOTIFICATION: "HIDE_NOTIFICATION", // Events triggered by keyboard - UNDO: "UNDO", - REDO: "REDO", - CUT: "CUT", - COPY: "COPY", - PASTE: "PASTE", - SELECT_TOOL : "SELECT_TOOL", - TOGGLE_HELP : "TOGGLE_HELP", - SWAP_COLORS : "SWAP_COLORS" + SELECT_TOOL : "SELECT_TOOL" }; \ No newline at end of file diff --git a/js/app.js b/js/app.js index 33f34f9c..18681b8a 100644 --- a/js/app.js +++ b/js/app.js @@ -10,6 +10,9 @@ ns.app = { init : function () { + this.shortcutService = new pskl.service.keyboard.ShortcutService(); + this.shortcutService.init(); + var size = this.readSizeFromURL_(); var piskel = new pskl.model.Piskel(size.width, size.height); @@ -20,6 +23,7 @@ piskel.addLayer(layer); this.piskelController = new pskl.controller.PiskelController(piskel); + this.piskelController.init(); this.paletteController = new pskl.controller.PaletteController(); this.paletteController.init(); @@ -39,15 +43,15 @@ this.settingsController = new pskl.controller.settings.SettingsController(this.piskelController); this.settingsController.init(); + this.toolController = new pskl.controller.ToolController(); + this.toolController.init(); + this.selectionManager = new pskl.selection.SelectionManager(this.piskelController); this.selectionManager.init(); this.historyService = new pskl.service.HistoryService(this.piskelController); this.historyService.init(); - this.keyboardEventService = new pskl.service.KeyboardEventService(); - this.keyboardEventService.init(); - this.notificationController = new pskl.controller.NotificationController(); this.notificationController.init(); @@ -57,8 +61,6 @@ this.imageUploadService = new pskl.service.ImageUploadService(); this.imageUploadService.init(); - this.toolController = new pskl.controller.ToolController(); - this.toolController.init(); this.cheatsheetService = new pskl.service.keyboard.CheatsheetService(); this.cheatsheetService.init(); diff --git a/js/controller/PaletteController.js b/js/controller/PaletteController.js index 6c0c58ee..98013cd1 100644 --- a/js/controller/PaletteController.js +++ b/js/controller/PaletteController.js @@ -15,7 +15,9 @@ $.subscribe(Events.SELECT_PRIMARY_COLOR, this.onColorSelected_.bind(this, {isPrimary:true})); $.subscribe(Events.SELECT_SECONDARY_COLOR, this.onColorSelected_.bind(this, {isPrimary:false})); - $.subscribe(Events.SWAP_COLORS, this.onSwapColorsEvent_.bind(this)); + + pskl.app.shortcutService.addShortcut('X', this.swapColors.bind(this)); + pskl.app.shortcutService.addShortcut('D', this.resetColors.bind(this)); // Initialize colorpickers: var colorPicker = $('#color-picker'); @@ -72,12 +74,17 @@ return this.secondaryColor; }; - ns.PaletteController.prototype.onSwapColorsEvent_ = function () { + ns.PaletteController.prototype.swapColors = function () { var primaryColor = this.getPrimaryColor(); this.setPrimaryColor(this.getSecondaryColor()); this.setSecondaryColor(primaryColor); }; + ns.PaletteController.prototype.resetColors = function () { + this.setPrimaryColor(Constants.DEFAULT_PEN_COLOR); + this.setSecondaryColor(Constants.TRANSPARENT_COLOR); + }; + /** * @private */ diff --git a/js/controller/PiskelController.js b/js/controller/PiskelController.js index 8d102322..c3351d58 100644 --- a/js/controller/PiskelController.js +++ b/js/controller/PiskelController.js @@ -15,6 +15,11 @@ this.currentFrameIndex = 0; this.layerIdCounter = 1; + }; + + ns.PiskelController.prototype.init = function () { + pskl.app.shortcutService.addShortcut('up', this.selectPreviousFrame.bind(this)); + pskl.app.shortcutService.addShortcut('down', this.selectNextFrame.bind(this)); $.publish(Events.PISKEL_RESET); $.publish(Events.FRAME_SIZE_CHANGED); @@ -116,6 +121,20 @@ $.publish(Events.PISKEL_RESET); }; + ns.PiskelController.prototype.selectNextFrame = function () { + var nextIndex = this.currentFrameIndex + 1; + if (nextIndex < this.getFrameCount()) { + this.setCurrentFrameIndex(nextIndex); + } + }; + + ns.PiskelController.prototype.selectPreviousFrame = function () { + var nextIndex = this.currentFrameIndex - 1; + if (nextIndex >= 0) { + this.setCurrentFrameIndex(nextIndex); + } + }; + ns.PiskelController.prototype.setCurrentLayerIndex = function (index) { this.currentLayerIndex = index; $.publish(Events.PISKEL_RESET); diff --git a/js/controller/ToolController.js b/js/controller/ToolController.js index 4fa8b712..0e2d42dc 100644 --- a/js/controller/ToolController.js +++ b/js/controller/ToolController.js @@ -29,15 +29,14 @@ * @public */ ns.ToolController.prototype.init = function() { - this.createToolMarkup_(); + this.createToolsDom_(); + this.addKeyboardShortcuts_(); // Initialize tool: // Set SimplePen as default selected tool: this.selectTool_(this.tools[0]); // Activate listener on tool panel: $("#tool-section").click($.proxy(this.onToolIconClicked_, this)); - - $.subscribe(Events.SELECT_TOOL, $.proxy(this.onKeyboardShortcut_, this)); }; /** @@ -85,7 +84,7 @@ } }; - ns.ToolController.prototype.onKeyboardShortcut_ = function(evt, charkey) { + 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()) { @@ -107,20 +106,32 @@ /** * @private */ - ns.ToolController.prototype.createToolMarkup_ = function() { - var currentTool, toolMarkup = '', extraClass; - + ns.ToolController.prototype.createToolsDom_ = function() { + var toolMarkup = ''; for(var i = 0 ; i < this.tools.length ; i++) { - var tool = this.tools[i]; - var instance = tool.instance; - - extraClass = instance.toolId; - if (this.currentSelectedTool == tool) { - extraClass = extraClass + " selected"; - } - toolMarkup += '
  • '; + toolMarkup += this.getToolMarkup_(this.tools[i]); } $('#tools-container').html(toolMarkup); }; + + /** + * @private + */ + ns.ToolController.prototype.getToolMarkup_ = function(tool) { + var instance = tool.instance; + + var classList = ['tool-icon', instance.toolId]; + if (this.currentSelectedTool == tool) { + classList.push('selected'); + } + + return '
  • '; + }; + + ns.ToolController.prototype.addKeyboardShortcuts_ = function () { + for(var i = 0 ; i < this.tools.length ; i++) { + pskl.app.shortcutService.addShortcut(this.tools[i].shortcut, this.onKeyboardShortcut_.bind(this)); + } + }; })(); \ No newline at end of file diff --git a/js/selection/SelectionManager.js b/js/selection/SelectionManager.js index 5f6f3790..578e64b3 100644 --- a/js/selection/SelectionManager.js +++ b/js/selection/SelectionManager.js @@ -13,9 +13,9 @@ $.subscribe(Events.SELECTION_DISMISSED, $.proxy(this.onSelectionDismissed_, this)); $.subscribe(Events.SELECTION_MOVE_REQUEST, $.proxy(this.onSelectionMoved_, this)); - $.subscribe(Events.PASTE, $.proxy(this.onPaste_, this)); - $.subscribe(Events.COPY, $.proxy(this.onCopy_, this)); - $.subscribe(Events.CUT, $.proxy(this.onCut_, this)); + pskl.app.shortcutService.addShortcut('ctrl+V', this.paste.bind(this)); + pskl.app.shortcutService.addShortcut('ctrl+X', this.cut.bind(this)); + pskl.app.shortcutService.addShortcut('ctrl+C', this.copy.bind(this)); $.subscribe(Events.TOOL_SELECTED, $.proxy(this.onToolSelected_, this)); }; @@ -46,10 +46,7 @@ this.cleanSelection_(); }; - /** - * @private - */ - ns.SelectionManager.prototype.onCut_ = function(evt) { + ns.SelectionManager.prototype.cut = function() { if(this.currentSelection) { // Put cut target into the selection: this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame()); @@ -59,9 +56,8 @@ for(var i=0, l=pixels.length; i= 65 && keycode <= 90) { - // key is a-z, we'll use base 36 to get the string representation + // key is a-z, use base 36 to get the string representation return (keycode - 65 + 10).toString(36); } else { return specialKeys[keycode]; diff --git a/js/service/keyboard/ShortcutService.js b/js/service/keyboard/ShortcutService.js new file mode 100644 index 00000000..07419af7 --- /dev/null +++ b/js/service/keyboard/ShortcutService.js @@ -0,0 +1,90 @@ +(function () { + var ns = $.namespace('pskl.service.keyboard'); + + ns.ShortcutService = function () { + this.shortcuts_ = {}; + }; + + /** + * @public + */ + ns.ShortcutService.prototype.init = function() { + $(document.body).keydown($.proxy(this.onKeyUp_, this)); + }; + + ns.ShortcutService.prototype.addShortcut = function (rawKey, callback) { + var parsedKey = this.parseKey_(rawKey.toLowerCase()); + + var key = parsedKey.key, + meta = parsedKey.meta; + + this.shortcuts_[key] = this.shortcuts_[key] || {}; + + if (this.shortcuts_[key][meta]) { + throw 'Shortcut ' + meta + ' + ' + key + ' already registered'; + } else { + this.shortcuts_[key][meta] = callback; + } + }; + + ns.ShortcutService.prototype.removeShortcut = function (rawKey) { + var parsedKey = this.parseKey_(rawKey.toLowerCase()); + + var key = parsedKey.key, + meta = parsedKey.meta; + + this.shortcuts_[key] = this.shortcuts_[key] || {}; + + this.shortcuts_[key][meta] = null; + }; + + ns.ShortcutService.prototype.parseKey_ = function (key) { + var meta = 'normal'; + if (key.indexOf('ctrl+') === 0) { + meta = 'ctrl'; + key = key.replace('ctrl+', ''); + } else if (key.indexOf('shift+') === 0) { + meta = 'shift'; + key = key.replace('shift+', ''); + } + return {meta : meta, key : key}; + }; + + /** + * @private + */ + ns.ShortcutService.prototype.onKeyUp_ = function(evt) { + // jquery names FTW ... + var keycode = evt.which; + var charkey = pskl.service.keyboard.KeycodeTranslator.toChar(keycode); + + var keyShortcuts = this.shortcuts_[charkey]; + if(keyShortcuts) { + var cb; + if (this.isCtrlKeyPressed_(evt)) { + cb = keyShortcuts.ctrl; + } else if (this.isShiftKeyPressed_(evt)) { + cb = keyShortcuts.shift; + } else { + cb = keyShortcuts.normal; + } + + if(cb) { + cb(charkey); + evt.preventDefault(); + } + } + }; + + ns.ShortcutService.prototype.isCtrlKeyPressed_ = function (evt) { + return this.isMac_() ? evt.metaKey : evt.ctrlKey; + }; + + ns.ShortcutService.prototype.isShiftKeyPressed_ = function (evt) { + return evt.shiftKey; + }; + + ns.ShortcutService.prototype.isMac_ = function () { + return navigator.appVersion.indexOf("Mac") != -1; + }; +})(); \ No newline at end of file diff --git a/piskel-script-list.js b/piskel-script-list.js index 23580119..f7661cb9 100644 --- a/piskel-script-list.js +++ b/piskel-script-list.js @@ -62,9 +62,9 @@ exports.scripts = [ // Services "js/service/LocalStorageService.js", "js/service/HistoryService.js", + "js/service/keyboard/ShortcutService.js", "js/service/keyboard/KeycodeTranslator.js", "js/service/keyboard/CheatsheetService.js", - "js/service/KeyboardEventService.js", "js/service/ImageUploadService.js", // Tools