diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html index 60eaf1fa64..0a685da2fc 100644 --- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html @@ -14,6 +14,88 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> + + @@ -21,10 +103,188 @@ limitations under the License. (function(window) { 'use strict'; + const DOC_ONLY = 'DOC_ONLY'; + const GO_KEY = 'GO_KEY'; + + // The maximum age of a keydown event to be used in a jump navigation. This + // is only for cases when the keyup event is lost. + const GO_KEY_TIMEOUT_MS = 1000; + + const ShortcutSection = { + ACTIONS: 'Actions', + DIFFS: 'Diffs', + EVERYWHERE: 'Everywhere', + FILE_LIST: 'File list', + NAVIGATION: 'Navigation', + REPLY_DIALOG: 'Reply dialog', + }; + + const Shortcut = { + OPEN_SHORTCUT_HELP_DIALOG: 'OPEN_SHORTCUT_HELP_DIALOG', + GO_TO_OPENED_CHANGES: 'GO_TO_OPENED_CHANGES', + GO_TO_MERGED_CHANGES: 'GO_TO_MERGED_CHANGES', + GO_TO_ABANDONED_CHANGES: 'GO_TO_ABANDONED_CHANGES', + + CURSOR_NEXT_CHANGE: 'CURSOR_NEXT_CHANGE', + CURSOR_PREV_CHANGE: 'CURSOR_PREV_CHANGE', + OPEN_CHANGE: 'OPEN_CHANGE', + NEXT_PAGE: 'NEXT_PAGE', + PREV_PAGE: 'PREV_PAGE', + TOGGLE_CHANGE_REVIEWED: 'TOGGLE_CHANGE_REVIEWED', + TOGGLE_CHANGE_STAR: 'TOGGLE_CHANGE_STAR', + REFRESH_CHANGE_LIST: 'REFRESH_CHANGE_LIST', + + OPEN_REPLY_DIALOG: 'OPEN_REPLY_DIALOG', + OPEN_DOWNLOAD_DIALOG: 'OPEN_DOWNLOAD_DIALOG', + EXPAND_ALL_MESSAGES: 'EXPAND_ALL_MESSAGES', + COLLAPSE_ALL_MESSAGES: 'COLLAPSE_ALL_MESSAGES', + UP_TO_DASHBOARD: 'UP_TO_DASHBOARD', + UP_TO_CHANGE: 'UP_TO_CHANGE', + TOGGLE_DIFF_MODE: 'TOGGLE_DIFF_MODE', + REFRESH_CHANGE: 'REFRESH_CHANGE', + + NEXT_LINE: 'NEXT_LINE', + PREV_LINE: 'PREV_LINE', + NEXT_CHUNK: 'NEXT_CHUNK', + PREV_CHUNK: 'PREV_CHUNK', + EXPAND_ALL_DIFF_CONTEXT: 'EXPAND_ALL_DIFF_CONTEXT', + NEXT_COMMENT_THREAD: 'NEXT_COMMENT_THREAD', + PREV_COMMENT_THREAD: 'PREV_COMMENT_THREAD', + EXPAND_ALL_COMMENT_THREADS: 'EXPAND_ALL_COMMENT_THREADS', + COLLAPSE_ALL_COMMENT_THREADS: 'COLLAPSE_ALL_COMMENT_THREADS', + LEFT_PANE: 'LEFT_PANE', + RIGHT_PANE: 'RIGHT_PANE', + TOGGLE_LEFT_PANE: 'TOGGLE_LEFT_PANE', + NEW_COMMENT: 'NEW_COMMENT', + SAVE_COMMENT: 'SAVE_COMMENT', + OPEN_DIFF_PREFS: 'OPEN_DIFF_PREFS', + TOGGLE_DIFF_REVIEWED: 'TOGGLE_DIFF_REVIEWED', + + NEXT_FILE: 'NEXT_FILE', + PREV_FILE: 'PREV_FILE', + NEXT_FILE_WITH_COMMENTS: 'NEXT_FILE_WITH_COMMENTS', + PREV_FILE_WITH_COMMENTS: 'PREV_FILE_WITH_COMMENTS', + CURSOR_NEXT_FILE: 'CURSOR_NEXT_FILE', + CURSOR_PREV_FILE: 'CURSOR_PREV_FILE', + OPEN_FILE: 'OPEN_FILE', + TOGGLE_FILE_REVIEWED: 'TOGGLE_FILE_REVIEWED', + TOGGLE_ALL_INLINE_DIFFS: 'TOGGLE_ALL_INLINE_DIFFS', + TOGGLE_INLINE_DIFF: 'TOGGLE_INLINE_DIFF', + + OPEN_FIRST_FILE: 'OPEN_FIRST_FILE', + OPEN_LAST_FILE: 'OPEN_LAST_FILE', + + SEARCH: 'SEARCH', + SEND_REPLY: 'SEND_REPLY', + }; + + const _help = new Map(); + + function _describe(shortcut, section, text) { + if (!_help.has(section)) { + _help.set(section, []); + } + _help.get(section).push({shortcut, text}); + } + + _describe(Shortcut.SEARCH, ShortcutSection.EVERYWHERE, 'Search'); + _describe(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, ShortcutSection.EVERYWHERE, + 'Show this dialog'); + _describe(Shortcut.GO_TO_OPENED_CHANGES, ShortcutSection.EVERYWHERE, + 'Go to Opened Changes'); + _describe(Shortcut.GO_TO_MERGED_CHANGES, ShortcutSection.EVERYWHERE, + 'Go to Merged Changes'); + _describe(Shortcut.GO_TO_ABANDONED_CHANGES, ShortcutSection.EVERYWHERE, + 'Go to Abandoned Changes'); + + _describe(Shortcut.CURSOR_NEXT_CHANGE, ShortcutSection.ACTIONS, + 'Select next change'); + _describe(Shortcut.CURSOR_PREV_CHANGE, ShortcutSection.ACTIONS, + 'Select previous change'); + _describe(Shortcut.OPEN_CHANGE, ShortcutSection.ACTIONS, + 'Show selected change'); + _describe(Shortcut.NEXT_PAGE, ShortcutSection.ACTIONS, 'Go to next page'); + _describe(Shortcut.PREV_PAGE, ShortcutSection.ACTIONS, 'Go to previous page'); + _describe(Shortcut.OPEN_REPLY_DIALOG, ShortcutSection.ACTIONS, + 'Open reply dialog to publish comments and add reviewers'); + _describe(Shortcut.OPEN_DOWNLOAD_DIALOG, ShortcutSection.ACTIONS, + 'Open download overlay'); + _describe(Shortcut.EXPAND_ALL_MESSAGES, ShortcutSection.ACTIONS, + 'Expand all messages'); + _describe(Shortcut.COLLAPSE_ALL_MESSAGES, ShortcutSection.ACTIONS, + 'Collapse all messages'); + _describe(Shortcut.REFRESH_CHANGE, ShortcutSection.ACTIONS, + 'Reload the change at the latest patch'); + _describe(Shortcut.TOGGLE_CHANGE_REVIEWED, ShortcutSection.ACTIONS, + 'Mark/unmark change as reviewed'); + _describe(Shortcut.TOGGLE_FILE_REVIEWED, ShortcutSection.ACTIONS, + 'Toggle review flag on selected file'); + _describe(Shortcut.REFRESH_CHANGE_LIST, ShortcutSection.ACTIONS, + 'Refresh list of changes'); + _describe(Shortcut.TOGGLE_CHANGE_STAR, ShortcutSection.ACTIONS, + 'Star/unstar change'); + + _describe(Shortcut.NEXT_LINE, ShortcutSection.DIFFS, 'Go to next line'); + _describe(Shortcut.PREV_LINE, ShortcutSection.DIFFS, 'Go to previous line'); + _describe(Shortcut.NEXT_CHUNK, ShortcutSection.DIFFS, + 'Go to next diff chunk'); + _describe(Shortcut.PREV_CHUNK, ShortcutSection.DIFFS, + 'Go to previous diff chunk'); + _describe(Shortcut.EXPAND_ALL_DIFF_CONTEXT, ShortcutSection.DIFFS, + 'Expand all diff context'); + _describe(Shortcut.NEXT_COMMENT_THREAD, ShortcutSection.DIFFS, + 'Go to next comment thread'); + _describe(Shortcut.PREV_COMMENT_THREAD, ShortcutSection.DIFFS, + 'Go to previous comment thread'); + _describe(Shortcut.EXPAND_ALL_COMMENT_THREADS, ShortcutSection.DIFFS, + 'Expand all comment threads'); + _describe(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS, + 'Collapse all comment threads'); + _describe(Shortcut.LEFT_PANE, ShortcutSection.DIFFS, 'Select left pane'); + _describe(Shortcut.RIGHT_PANE, ShortcutSection.DIFFS, 'Select right pane'); + _describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS, + 'Hide/show left diff'); + _describe(Shortcut.NEW_COMMENT, ShortcutSection.DIFFS, 'Draft new comment'); + _describe(Shortcut.SAVE_COMMENT, ShortcutSection.DIFFS, 'Save comment'); + _describe(Shortcut.OPEN_DIFF_PREFS, ShortcutSection.DIFFS, + 'Show diff preferences'); + _describe(Shortcut.TOGGLE_DIFF_REVIEWED, ShortcutSection.DIFFS, + 'Mark/unmark file as reviewed'); + _describe(Shortcut.TOGGLE_DIFF_MODE, ShortcutSection.DIFFS, + 'Toggle unified/side-by-side diff'); + + _describe(Shortcut.NEXT_FILE, ShortcutSection.NAVIGATION, 'Select next file'); + _describe(Shortcut.PREV_FILE, ShortcutSection.NAVIGATION, + 'Select previous file'); + _describe(Shortcut.NEXT_FILE_WITH_COMMENTS, ShortcutSection.NAVIGATION, + 'Select next file that has comments'); + _describe(Shortcut.PREV_FILE_WITH_COMMENTS, ShortcutSection.NAVIGATION, + 'Select previous file that has comments'); + _describe(Shortcut.OPEN_FIRST_FILE, ShortcutSection.NAVIGATION, + 'Show first file'); + _describe(Shortcut.OPEN_LAST_FILE, ShortcutSection.NAVIGATION, + 'Show last file'); + _describe(Shortcut.UP_TO_DASHBOARD, ShortcutSection.NAVIGATION, + 'Up to dashboard'); + _describe(Shortcut.UP_TO_CHANGE, ShortcutSection.NAVIGATION, 'Up to change'); + + _describe(Shortcut.CURSOR_NEXT_FILE, ShortcutSection.FILE_LIST, + 'Select next file'); + _describe(Shortcut.CURSOR_PREV_FILE, ShortcutSection.FILE_LIST, + 'Select previous file'); + _describe(Shortcut.OPEN_FILE, ShortcutSection.FILE_LIST, + 'Go to selected file'); + _describe(Shortcut.TOGGLE_ALL_INLINE_DIFFS, ShortcutSection.FILE_LIST, + 'Show/hide all inline diffs'); + _describe(Shortcut.TOGGLE_INLINE_DIFF, ShortcutSection.FILE_LIST, + 'Show/hide selected inline diff'); + + _describe(Shortcut.SEND_REPLY, ShortcutSection.REPLY_DIALOG, 'Send reply'); + // Must be declared outside behavior implementation to be accessed inside // behavior functions. - /** @return {!Object} */ + /** @return {!(Event|PolymerDomApi|PolymerEventApi)} */ const getKeyboardEvent = function(e) { e = Polymer.dom(e.detail ? e.detail.keyboardEvent : e); // When e is a keyboardEvent, e.event is not null. @@ -32,44 +292,307 @@ limitations under the License. return e; }; + class ShortcutManager { + constructor() { + this.activeHosts = new Map(); + this.bindings = new Map(); + this.listeners = new Set(); + } + + bindShortcut(shortcut, ...bindings) { + this.bindings.set(shortcut, bindings); + } + + getBindingsForShortcut(shortcut) { + return this.bindings.get(shortcut); + } + + attachHost(host) { + if (!host.keyboardShortcuts) { return; } + const shortcuts = host.keyboardShortcuts(); + this.activeHosts.set(host, new Map(Object.entries(shortcuts))); + this.notifyListeners(); + return shortcuts; + } + + detachHost(host) { + if (this.activeHosts.delete(host)) { + this.notifyListeners(); + return true; + } + return false; + } + + addListener(listener) { + this.listeners.add(listener); + listener(this.directoryView()); + } + + removeListener(listener) { + return this.listeners.delete(listener); + } + + activeShortcutsBySection() { + const activeShortcuts = new Set(); + this.activeHosts.forEach(shortcuts => { + shortcuts.forEach((_, shortcut) => activeShortcuts.add(shortcut)); + }); + + const activeShortcutsBySection = new Map(); + _help.forEach((shortcutList, section) => { + shortcutList.forEach(shortcutHelp => { + if (activeShortcuts.has(shortcutHelp.shortcut)) { + if (!activeShortcutsBySection.has(section)) { + activeShortcutsBySection.set(section, []); + } + activeShortcutsBySection.get(section).push(shortcutHelp); + } + }); + }); + return activeShortcutsBySection; + } + + directoryView() { + const view = new Map(); + this.activeShortcutsBySection().forEach((shortcutHelps, section) => { + const sectionView = []; + shortcutHelps.forEach(shortcutHelp => { + const bindingDesc = this.describeBindings(shortcutHelp.shortcut); + if (!bindingDesc) { return; } + this.distributeBindingDesc(bindingDesc).forEach(bindingDesc => { + sectionView.push({ + binding: bindingDesc, + text: shortcutHelp.text, + }); + }); + }); + view.set(section, sectionView); + }); + return view; + } + + distributeBindingDesc(bindingDesc) { + if (bindingDesc.length === 1 || + this.comboSetDisplayWidth(bindingDesc) < 21) { + return [bindingDesc]; + } + // Find the largest prefix of bindings that is under the + // size threshold. + const head = [bindingDesc[0]]; + for (let i = 1; i < bindingDesc.length; i++) { + head.push(bindingDesc[i]); + if (this.comboSetDisplayWidth(head) >= 21) { + head.pop(); + return [head].concat( + this.distributeBindingDesc(bindingDesc.slice(i))); + } + } + } + + comboSetDisplayWidth(bindingDesc) { + const bindingSizer = binding => binding.reduce( + (acc, key) => acc + key.length, 0); + // Width is the sum of strings + (n-1) * 2 to account for the word + // "or" joining them. + return bindingDesc.reduce( + (acc, binding) => acc + bindingSizer(binding), 0) + + 2 * (bindingDesc.length - 1); + } + + describeBindings(shortcut) { + const bindings = this.bindings.get(shortcut); + if (!bindings) { return null; } + if (bindings[0] === GO_KEY) { + return [['g'].concat(bindings.slice(1))]; + } + return bindings + .filter(binding => binding !== DOC_ONLY) + .map(binding => this.describeBinding(binding)); + } + + describeBinding(binding) { + return binding.split(':')[0].split('+').map(part => { + switch (part) { + case 'shift': + return 'Shift'; + case 'meta': + return 'Meta'; + case 'ctrl': + return 'Ctrl'; + case 'enter': + return 'Enter'; + case 'up': + return '↑'; + case 'down': + return '↓'; + case 'left': + return '←'; + case 'right': + return '→'; + default: + return part; + } + }); + } + + notifyListeners() { + const view = this.directoryView(); + this.listeners.forEach(listener => listener(view)); + } + } + + const shortcutManager = new ShortcutManager(); + window.Gerrit = window.Gerrit || {}; /** @polymerBehavior KeyboardShortcutBehavior */ - Gerrit.KeyboardShortcutBehavior = [{ - modifierPressed(e) { - e = getKeyboardEvent(e); - return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey; - }, - - isModifierPressed(e, modifier) { - return getKeyboardEvent(e)[modifier]; - }, - - shouldSuppressKeyboardShortcut(e) { - e = getKeyboardEvent(e); - const tagName = Polymer.dom(e).rootTarget.tagName; - if (tagName === 'INPUT' || tagName === 'TEXTAREA' || - (e.keyCode === 13 && tagName === 'A')) { - // Suppress shortcuts if the key is 'enter' and target is an anchor. - return true; - } - for (let i = 0; e.path && i < e.path.length; i++) { - if (e.path[i].tagName === 'GR-OVERLAY') { return true; } - } - return false; - }, - - // Alias for getKeyboardEvent. - /** @return {!Object} */ - getKeyboardEvent(e) { - return getKeyboardEvent(e); - }, - - getRootTarget(e) { - return Polymer.dom(getKeyboardEvent(e)).rootTarget; - }, - }, + Gerrit.KeyboardShortcutBehavior = [ Polymer.IronA11yKeysBehavior, + { + // Exports for convenience. Note: Closure compiler crashes when + // object-shorthand syntax is used here. + // eslint-disable-next-line object-shorthand + DOC_ONLY: DOC_ONLY, + // eslint-disable-next-line object-shorthand + GO_KEY: GO_KEY, + // eslint-disable-next-line object-shorthand + Shortcut: Shortcut, + + properties: { + _shortcut_go_key_last_pressed: { + type: Number, + value: null, + }, + + _shortcut_go_table: { + type: Array, + value() { return new Map(); }, + }, + }, + + modifierPressed(e) { + e = getKeyboardEvent(e); + return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey; + }, + + isModifierPressed(e, modifier) { + return getKeyboardEvent(e)[modifier]; + }, + + shouldSuppressKeyboardShortcut(e) { + e = getKeyboardEvent(e); + const tagName = Polymer.dom(e).rootTarget.tagName; + if (tagName === 'INPUT' || tagName === 'TEXTAREA' || + (e.keyCode === 13 && tagName === 'A')) { + // Suppress shortcuts if the key is 'enter' and target is an anchor. + return true; + } + for (let i = 0; e.path && i < e.path.length; i++) { + if (e.path[i].tagName === 'GR-OVERLAY') { return true; } + } + return false; + }, + + // Alias for getKeyboardEvent. + /** @return {!(Event|PolymerDomApi|PolymerEventApi)} */ + getKeyboardEvent(e) { + return getKeyboardEvent(e); + }, + + getRootTarget(e) { + return Polymer.dom(getKeyboardEvent(e)).rootTarget; + }, + + bindShortcut(shortcut, ...bindings) { + shortcutManager.bindShortcut(shortcut, ...bindings); + }, + + _addOwnKeyBindings(shortcut, handler) { + const bindings = shortcutManager.getBindingsForShortcut(shortcut); + if (!bindings) { + return; + } + if (bindings[0] === DOC_ONLY) { + return; + } + if (bindings[0] === GO_KEY) { + this._shortcut_go_table.set(bindings[1], handler); + } else { + this.addOwnKeyBinding(bindings.join(' '), handler); + } + }, + + attached() { + const shortcuts = shortcutManager.attachHost(this); + if (!shortcuts) { return; } + + for (const key of Object.keys(shortcuts)) { + this._addOwnKeyBindings(key, shortcuts[key]); + } + + // If any of the shortcuts utilized GO_KEY, then they are handled + // directly by this behavior. + if (this._shortcut_go_table.size > 0) { + this.addOwnKeyBinding('g:keydown', '_handleGoKeyDown'); + this.addOwnKeyBinding('g:keyup', '_handleGoKeyUp'); + this._shortcut_go_table.forEach((handler, key) => { + this.addOwnKeyBinding(key, '_handleGoAction'); + }); + } + }, + + detached() { + if (shortcutManager.detachHost(this)) { + this.removeOwnKeyBindings(); + } + }, + + keyboardShortcuts() { + return {}; + }, + + addKeyboardShortcutDirectoryListener(listener) { + shortcutManager.addListener(listener); + }, + + removeKeyboardShortcutDirectoryListener(listener) { + shortcutManager.removeListener(listener); + }, + + _handleGoKeyDown(e) { + if (this.modifierPressed(e)) { return; } + this._shortcut_go_key_last_pressed = Date.now(); + }, + + _handleGoKeyUp(e) { + this._shortcut_go_key_last_pressed = null; + }, + + _handleGoAction(e) { + if (!this._shortcut_go_key_last_pressed || + (Date.now() - this._shortcut_go_key_last_pressed > + GO_KEY_TIMEOUT_MS) || + !this._shortcut_go_table.has(e.detail.key) || + this.shouldSuppressKeyboardShortcut(e)) { + return; + } + e.preventDefault(); + const handler = this._shortcut_go_table.get(e.detail.key); + this[handler](e); + }, + }, ]; + + Gerrit.KeyboardShortcutBinder = { + DOC_ONLY, + GO_KEY, + Shortcut, + ShortcutManager, + ShortcutSection, + + bindShortcut(shortcut, ...bindings) { + shortcutManager.bindShortcut(shortcut, ...bindings); + }, + }; })(window); diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html index 04193dde5a..dac90f8606 100644 --- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html @@ -40,6 +40,8 @@ limitations under the License. diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js index cfe97bd847..708a730c01 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js @@ -106,17 +106,6 @@ Gerrit.URLEncodingBehavior, ], - keyBindings: { - 'j': '_handleJKey', - 'k': '_handleKKey', - 'n ]': '_handleNKey', - 'o': '_handleOKey', - 'p [': '_handlePKey', - 'r': '_handleRKey', - 'shift+r': '_handleShiftRKey', - 's': '_handleSKey', - }, - listeners: { keydown: '_scopedKeydownHandler', }, @@ -126,6 +115,19 @@ '_computePreferences(account, preferences)', ], + keyboardShortcuts() { + return { + [this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange', + [this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange', + [this.Shortcut.NEXT_PAGE]: '_nextPage', + [this.Shortcut.PREV_PAGE]: '_prevPage', + [this.Shortcut.OPEN_CHANGE]: '_openChange', + [this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed', + [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar', + [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList', + }; + }, + /** * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard * events must be scoped to a component level (e.g. `enter`) in order to not @@ -136,7 +138,7 @@ _scopedKeydownHandler(e) { if (e.keyCode === 13) { // Enter. - this._handleOKey(e); + this._openChange(e); } }, @@ -238,7 +240,7 @@ return account._account_id === change.assignee._account_id; }, - _handleJKey(e) { + _nextChange(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -246,7 +248,7 @@ this.$.cursor.next(); }, - _handleKKey(e) { + _prevChange(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -254,7 +256,7 @@ this.$.cursor.previous(); }, - _handleOKey(e) { + _openChange(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -262,7 +264,7 @@ Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex)); }, - _handleNKey(e) { + _nextPage(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) { return; @@ -272,7 +274,7 @@ this.fire('next-page'); }, - _handlePKey(e) { + _prevPage(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) { return; @@ -282,7 +284,7 @@ this.fire('previous-page'); }, - _handleRKey(e) { + _toggleChangeReviewed(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -300,7 +302,7 @@ changeEl.toggleReviewed(); }, - _handleShiftRKey(e) { + _refreshChangeList(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); @@ -311,7 +313,7 @@ window.location.reload(); }, - _handleSKey(e) { + _toggleChangeStar(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html index bb904b5f1d..d20d40a9b3 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html @@ -42,6 +42,17 @@ limitations under the License. + diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js new file mode 100644 index 0000000000..89d1091086 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js @@ -0,0 +1,36 @@ +/** + * @license + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + + Polymer({ + is: 'gr-key-binding-display', + + properties: { + /** @type {Array} */ + binding: Array, + }, + + _computeModifiers(binding) { + return binding.slice(0, binding.length - 1); + }, + + _computeKey(binding) { + return binding[binding.length - 1]; + }, + }); +})(); diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html new file mode 100644 index 0000000000..0361d76d93 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html @@ -0,0 +1,66 @@ + + + +gr-key-binding-display + + + + + + + + + + + + + diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html index 9fda898f83..e3552ccd25 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html @@ -16,7 +16,9 @@ limitations under the License. --> + + @@ -54,15 +56,6 @@ limitations under the License. font-weight: var(--font-weight-bold); padding-top: 1em; } - .key { - background-color: var(--chip-background-color); - border: 1px solid var(--border-color); - border-radius: 3px; - display: inline-block; - font-weight: var(--font-weight-bold); - padding: .1em .5em; - text-align: center; - } .modifier { font-weight: normal; } @@ -74,449 +67,42 @@ limitations under the License.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Everywhere
/Search
?Show this dialog
- g - o - Go to Opened Changes
- g - m - Go to Merged Changes
- g - a - Go to Abandoned Changes
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js index e5dd019eec..5b29972e40 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js @@ -17,6 +17,8 @@ (function() { 'use strict'; + const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder; + Polymer({ is: 'gr-keyboard-shortcuts-dialog', @@ -27,20 +29,97 @@ */ properties: { - view: String, + _left: Array, + _right: Array, + + _propertyBySection: { + type: Object, + value() { + return { + [ShortcutSection.EVERYWHERE]: '_everywhere', + [ShortcutSection.NAVIGATION]: '_navigation', + [ShortcutSection.DASHBOARD]: '_dashboard', + [ShortcutSection.CHANGE_LIST]: '_changeList', + [ShortcutSection.ACTIONS]: '_actions', + [ShortcutSection.REPLY_DIALOG]: '_replyDialog', + [ShortcutSection.FILE_LIST]: '_fileList', + [ShortcutSection.DIFFS]: '_diffs', + }; + }, + }, }, + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + hostAttributes: { role: 'dialog', }, - _computeInView(currentView, view) { - return view === currentView; + attached() { + this.addKeyboardShortcutDirectoryListener( + this._onDirectoryUpdated.bind(this)); + }, + + detached() { + this.removeKeyboardShortcutDirectoryListener( + this._onDirectoryUpdated.bind(this)); }, _handleCloseTap(e) { e.preventDefault(); this.fire('close', null, {bubbles: false}); }, + + _onDirectoryUpdated(directory) { + const left = []; + const right = []; + + if (directory.has(ShortcutSection.EVERYWHERE)) { + left.push({ + section: ShortcutSection.EVERYWHERE, + shortcuts: directory.get(ShortcutSection.EVERYWHERE), + }); + } + + if (directory.has(ShortcutSection.NAVIGATION)) { + left.push({ + section: ShortcutSection.NAVIGATION, + shortcuts: directory.get(ShortcutSection.NAVIGATION), + }); + } + + if (directory.has(ShortcutSection.ACTIONS)) { + right.push({ + section: ShortcutSection.ACTIONS, + shortcuts: directory.get(ShortcutSection.ACTIONS), + }); + } + + if (directory.has(ShortcutSection.REPLY_DIALOG)) { + right.push({ + section: ShortcutSection.REPLY_DIALOG, + shortcuts: directory.get(ShortcutSection.REPLY_DIALOG), + }); + } + + if (directory.has(ShortcutSection.FILE_LIST)) { + right.push({ + section: ShortcutSection.FILE_LIST, + shortcuts: directory.get(ShortcutSection.FILE_LIST), + }); + } + + if (directory.has(ShortcutSection.DIFFS)) { + right.push({ + section: ShortcutSection.DIFFS, + shortcuts: directory.get(ShortcutSection.DIFFS), + }); + } + + this.set('_left', left); + this.set('_right', right); + }, }); })(); diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html new file mode 100644 index 0000000000..50579dda40 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html @@ -0,0 +1,179 @@ + + + +gr-key-binding-display + + + + + + + + + + + + + + diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js index 1513a7f343..a81526ceb1 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js @@ -110,10 +110,6 @@ Gerrit.URLEncodingBehavior, ], - keyBindings: { - '/': '_handleForwardSlashKey', - }, - properties: { value: { type: String, @@ -156,6 +152,12 @@ }, }, + keyboardShortcuts() { + return { + [this.Shortcut.SEARCH]: '_handleSearch', + }; + }, + _valueChanged(value) { this._inputVal = value; }, @@ -274,7 +276,7 @@ }); }, - _handleForwardSlashKey(e) { + _handleSearch(e) { const keyboardEvent = this.getKeyboardEvent(e); if (this.shouldSuppressKeyboardShortcut(e) || (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; } diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html index 9a7023e4fa..93e0e30739 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html @@ -37,6 +37,9 @@ limitations under the License. diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index 08324597f8..5b9ae1576e 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html @@ -92,6 +92,8 @@ limitations under the License. 'core/gr-account-dropdown/gr-account-dropdown_test.html', 'core/gr-error-dialog/gr-error-dialog_test.html', 'core/gr-error-manager/gr-error-manager_test.html', + 'core/gr-key-binding-display/gr-key-binding-display_test.html', + 'core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html', 'core/gr-main-header/gr-main-header_test.html', 'core/gr-navigation/gr-navigation_test.html', 'core/gr-reporting/gr-jank-detector_test.html',