summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaladox none <thomasmulhall410@yahoo.com>2018-10-25 21:46:38 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2018-10-25 21:46:38 +0000
commite311accd71ec06194ed339d5c4b4ecb7cb8f2429 (patch)
treecfa02910a5739f2ea79c66405cb355103d34344d
parent063bdc632d3bc523036fb62029ac37d4cffcc458 (diff)
parent2c03a906de795d148062ac8c24ce2f6987530d1c (diff)
Merge "Redesign the keyboard shortcuts system" into stable-2.16
-rw-r--r--polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html583
-rw-r--r--polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html275
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js42
-rw-r--r--polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html11
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js42
-rw-r--r--polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html16
-rw-r--r--polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js72
-rw-r--r--polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html28
-rw-r--r--polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html48
-rw-r--r--polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js36
-rw-r--r--polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html66
-rw-r--r--polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html486
-rw-r--r--polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js85
-rw-r--r--polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html179
-rw-r--r--polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js12
-rw-r--r--polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html3
-rw-r--r--polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js79
-rw-r--r--polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html31
-rw-r--r--polygerrit-ui/app/elements/gr-app.js160
-rw-r--r--polygerrit-ui/app/elements/gr-app_test.html50
-rw-r--r--polygerrit-ui/app/test/index.html2
21 files changed, 1627 insertions, 679 deletions
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.
-->
+
+<!--
+
+How to Add a Keyboard Shortcut
+==============================
+
+A keyboard shortcut is composed of the following parts:
+
+ 1. A semantic identifier (e.g. OPEN_CHANGE, NEXT_PAGE)
+ 2. Documentation for the keyboard shortcut help dialog
+ 3. A binding between key combos and the semantic identifier
+ 4. A binding between the semantic identifier and a listener
+
+Parts (1) and (2) for all shortcuts are defined in this file. The semantic
+identifier is declared in the Shortcut enum near the head of this script:
+
+ const Shortcut = {
+ // ...
+ TOGGLE_LEFT_PANE: 'TOGGLE_LEFT_PANE',
+ // ...
+ };
+
+Immediately following the Shortcut enum definition, there is a _describe
+function defined which is then invoked many times to populate the help dialog.
+Add a new invocation here to document the shortcut:
+
+ _describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS,
+ 'Hide/show left diff');
+
+When an attached view binds one or more key combos to this shortcut, the help
+dialog will display this text in the given section (in this case, "Diffs"). See
+the ShortcutSection enum immediately below for the list of supported sections.
+
+Part (3), the actual key bindings, are declared by gr-app. In the future, this
+system may be expanded to allow key binding customizations by plugins or user
+preferences. Key bindings are defined in the following forms:
+
+ // Ordinary shortcut with a single binding.
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+
+ // Ordinary shortcut with multiple bindings.
+ this.bindShortcut(
+ this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+
+ // A "go-key" keyboard shortcut, which is combined with a previously and
+ // continuously pressed "go" key (the go-key is hard-coded as 'g').
+ this.bindShortcut(
+ this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
+
+ // A "doc-only" keyboard shortcut. This declares the key-binding for help
+ // dialog purposes, but doesn't actually implement the binding. It is up
+ // to some element to implement this binding using iron-a11y-keys-behavior's
+ // keyBindings property.
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
+
+Part (4), the listener definitions, are declared by the view or element that
+implements the shortcut behavior. This is done by implementing a method named
+keyboardShortcuts() in an element that mixes in this behavior, returning an
+object that maps semantic identifiers (as property names) to listener method
+names, like this:
+
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
+ };
+ },
+
+You can implement key bindings in an element that is hosted by a view IF that
+element is always attached exactly once under that view (e.g. the search bar in
+gr-app). When that is not the case, you will have to define a doc-only binding
+in gr-app, declare the shortcut in the view that hosts the element, and use
+iron-a11y-keys-behavior's keyBindings attribute to implement the binding in the
+element. An example of this is in comment threads. A diff view supports actions
+on comment threads, but there may be zero or many comment threads attached at
+any given point. So the shortcut is declared as doc-only by the diff view and
+by gr-app, and actually implemented by gr-diff-comment-thread.
+
+NOTE: doc-only shortcuts will not be customizable in the same way that other
+shortcuts are.
+-->
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
@@ -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;
};
- window.Gerrit = window.Gerrit || {};
+ class ShortcutManager {
+ constructor() {
+ this.activeHosts = new Map();
+ this.bindings = new Map();
+ this.listeners = new Set();
+ }
- /** @polymerBehavior KeyboardShortcutBehavior */
- Gerrit.KeyboardShortcutBehavior = [{
- modifierPressed(e) {
- e = getKeyboardEvent(e);
- return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
- },
+ bindShortcut(shortcut, ...bindings) {
+ this.bindings.set(shortcut, bindings);
+ }
- isModifierPressed(e, modifier) {
- return getKeyboardEvent(e)[modifier];
- },
+ getBindingsForShortcut(shortcut) {
+ return this.bindings.get(shortcut);
+ }
- 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.
+ 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;
}
- 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);
- },
+ addListener(listener) {
+ this.listeners.add(listener);
+ listener(this.directoryView());
+ }
- getRootTarget(e) {
- return Polymer.dom(getKeyboardEvent(e)).rootTarget;
- },
- },
+ 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 = [
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);
</script>
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.
<script>
suite('keyboard-shortcut-behavior tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+
let element;
let overlay;
let sandbox;
@@ -67,6 +69,228 @@ limitations under the License.
sandbox.restore();
});
+ suite('ShortcutManager', () => {
+ test('bindings management', () => {
+ const mgr = new kb.ShortcutManager();
+ const {NEXT_FILE} = kb.Shortcut;
+
+ assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
+ mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
+ assert.deepEqual(
+ mgr.getBindingsForShortcut(NEXT_FILE),
+ [']', '}', 'right']);
+ });
+
+ suite('binding descriptions', () => {
+ function mapToObject(m) {
+ const o = {};
+ m.forEach((v, k) => o[k] = v);
+ return o;
+ }
+
+ test('single combo description', () => {
+ const mgr = new kb.ShortcutManager();
+ assert.deepEqual(mgr.describeBinding('a'), ['a']);
+ assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
+ assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
+ assert.deepEqual(
+ mgr.describeBinding('ctrl+shift+up:keyup'),
+ ['Ctrl', 'Shift', '↑']);
+ });
+
+ test('combo set description', () => {
+ const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
+ const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
+
+ const mgr = new ShortcutManager();
+ assert.isNull(mgr.describeBindings(NEXT_FILE));
+
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
+ assert.deepEqual(
+ mgr.describeBindings(GO_TO_OPENED_CHANGES),
+ [['g', 'o']]);
+
+ mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
+ assert.deepEqual(
+ mgr.describeBindings(NEXT_FILE),
+ [[']'], ['Ctrl', 'Shift', '→']]);
+
+ mgr.bindShortcut(PREV_FILE, '[');
+ assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
+ });
+
+ test('combo set description width', () => {
+ const mgr = new kb.ShortcutManager();
+ assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
+ assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
+ assert.strictEqual(
+ mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
+ 12);
+ });
+
+ test('distribute shortcut help', () => {
+ const mgr = new kb.ShortcutManager();
+ assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([['g', 'o']]),
+ [[['g', 'o']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
+ [[['ctrl', 'shift', 'meta', 'enter']]]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([
+ ['ctrl', 'shift', 'meta', 'enter'],
+ ['o'],
+ ]),
+ [
+ [['ctrl', 'shift', 'meta', 'enter']],
+ [['o']],
+ ]);
+ assert.deepEqual(
+ mgr.distributeBindingDesc([
+ ['ctrl', 'enter'],
+ ['meta', 'enter'],
+ ['ctrl', 's'],
+ ['meta', 's'],
+ ]),
+ [
+ [['ctrl', 'enter'], ['meta', 'enter']],
+ [['ctrl', 's'], ['meta', 's']],
+ ]);
+ });
+
+ test('active shortcuts by section', () => {
+ const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
+ kb.Shortcut;
+ const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
+
+ const mgr = new kb.ShortcutManager();
+ mgr.bindShortcut(NEXT_FILE, ']');
+ mgr.bindShortcut(NEXT_LINE, 'j');
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
+ mgr.bindShortcut(SEARCH, '/');
+
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {});
+
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [NEXT_FILE]: null,
+ };
+ },
+ });
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Select next file'},
+ ],
+ });
+
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [NEXT_LINE]: null,
+ };
+ },
+ });
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [DIFFS]: [
+ {shortcut: NEXT_LINE, text: 'Go to next line'},
+ ],
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Select next file'},
+ ],
+ });
+
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [SEARCH]: null,
+ [GO_TO_OPENED_CHANGES]: null,
+ };
+ },
+ });
+ assert.deepEqual(
+ mapToObject(mgr.activeShortcutsBySection()),
+ {
+ [DIFFS]: [
+ {shortcut: NEXT_LINE, text: 'Go to next line'},
+ ],
+ [EVERYWHERE]: [
+ {shortcut: SEARCH, text: 'Search'},
+ {
+ shortcut: GO_TO_OPENED_CHANGES,
+ text: 'Go to Opened Changes',
+ },
+ ],
+ [NAVIGATION]: [
+ {shortcut: NEXT_FILE, text: 'Select next file'},
+ ],
+ });
+ });
+
+ test('directory view', () => {
+ const {
+ NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
+ SAVE_COMMENT,
+ } = kb.Shortcut;
+ const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
+ const {GO_KEY, ShortcutManager} = kb;
+
+ const mgr = new ShortcutManager();
+ mgr.bindShortcut(NEXT_FILE, ']');
+ mgr.bindShortcut(NEXT_LINE, 'j');
+ mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
+ mgr.bindShortcut(SEARCH, '/');
+ mgr.bindShortcut(
+ SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
+
+ assert.deepEqual(mapToObject(mgr.directoryView()), {});
+
+ mgr.attachHost({
+ keyboardShortcuts() {
+ return {
+ [GO_TO_OPENED_CHANGES]: null,
+ [NEXT_FILE]: null,
+ [NEXT_LINE]: null,
+ [SAVE_COMMENT]: null,
+ [SEARCH]: null,
+ };
+ },
+ });
+ assert.deepEqual(
+ mapToObject(mgr.directoryView()),
+ {
+ [DIFFS]: [
+ {binding: [['j']], text: 'Go to next line'},
+ {
+ binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
+ text: 'Save comment',
+ },
+ {
+ binding: [['Ctrl', 's'], ['Meta', 's']],
+ text: 'Save comment',
+ },
+ ],
+ [EVERYWHERE]: [
+ {binding: [['/']], text: 'Search'},
+ {binding: [['g', 'o']], text: 'Go to Opened Changes'},
+ ],
+ [NAVIGATION]: [
+ {binding: [[']']], text: 'Select next file'},
+ ],
+ });
+ });
+ });
+ });
+
test('doesn’t block kb shortcuts for non-whitelisted els', done => {
const divEl = document.createElement('div');
element.appendChild(divEl);
@@ -160,5 +384,56 @@ limitations under the License.
MockInteractions.keyDownOn(element, 75, 'alt', 'k');
assert.isFalse(spy.lastCall.returnValue);
});
+
+ suite('GO_KEY timing', () => {
+ let handlerStub;
+
+ setup(() => {
+ element._shortcut_go_table.set('a', '_handleA');
+ handlerStub = element._handleA = sinon.stub();
+ sandbox.stub(Date, 'now').returns(10000);
+ });
+
+ test('success', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isTrue(handlerStub.calledOnce);
+ assert.strictEqual(handlerStub.lastCall.args[0], e);
+ });
+
+ test('go key not pressed', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = null;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('go key pressed too long ago', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 3000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('should suppress', () => {
+ const e = {detail: {key: 'a'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+
+ test('unrecognized key', () => {
+ const e = {detail: {key: 'f'}, preventDefault: () => {}};
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ element._shortcut_go_key_last_pressed = 9000;
+ element._handleGoAction(e);
+ assert.isFalse(handlerStub.called);
+ });
+ });
});
</script>
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 ba768d9acd..de97e62507 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.
<script>
suite('gr-change-list basic tests', () => {
+ // Define keybindings before attaching other fixtures.
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+ kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
+ kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
+ kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
+ kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
+ kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
+
let element;
let sandbox;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 6391d70065..42aae9cb6a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -268,16 +268,20 @@
'_patchNumChanged(_patchRange.patchNum)',
],
- keyBindings: {
- 'shift+r': '_handleCapitalRKey',
- 'a': '_handleAKey',
- 'd': '_handleDKey',
- 'm': '_handleMKey',
- 's': '_handleSKey',
- 'u': '_handleUKey',
- 'x': '_handleXKey',
- 'z': '_handleZKey',
- ',': '_handleCommaKey',
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
+ [this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange',
+ [this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog',
+ [this.Shortcut.OPEN_DOWNLOAD_DIALOG]:
+ '_handleOpenDownloadDialogShortcut',
+ [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
+ [this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar',
+ [this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard',
+ [this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages',
+ [this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages',
+ [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut',
+ };
},
attached() {
@@ -341,7 +345,7 @@
});
},
- _handleMKey(e) {
+ _handleToggleDiffMode(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -899,7 +903,7 @@
return label;
},
- _handleAKey(e) {
+ _handleOpenReplyDialog(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) {
return;
@@ -915,7 +919,7 @@
});
},
- _handleDKey(e) {
+ _handleOpenDownloadDialogShortcut(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -923,13 +927,13 @@
this.$.downloadOverlay.open();
},
- _handleCapitalRKey(e) {
+ _handleRefreshChange(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
e.preventDefault();
Gerrit.Nav.navigateToChange(this._change);
},
- _handleSKey(e) {
+ _handleToggleChangeStar(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -937,7 +941,7 @@
this.$.changeStar.toggleStar();
},
- _handleUKey(e) {
+ _handleUpToDashboard(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -945,7 +949,7 @@
this._determinePageBack();
},
- _handleXKey(e) {
+ _handleExpandAllMessages(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -953,7 +957,7 @@
this.messagesList.handleExpandCollapse(true);
},
- _handleZKey(e) {
+ _handleCollapseAllMessages(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -961,7 +965,7 @@
this.messagesList.handleExpandCollapse(false);
},
- _handleCommaKey(e) {
+ _handleOpenDiffPrefsShortcut(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 4e6cb6efac..ef0a376628 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -43,6 +43,18 @@ limitations under the License.
<script>
suite('gr-change-view tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
+ kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
+ kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
+ kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+ kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+ kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
+
let element;
let sandbox;
let navigateToChangeStub;
@@ -277,11 +289,11 @@ limitations under the License.
flushAsynchronousOperations();
element.viewState.diffMode = 'SIDE_BY_SIDE';
- element._handleMKey(e);
+ element._handleToggleDiffMode(e);
assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF'));
element.viewState.diffMode = 'UNIFIED_DIFF';
- element._handleMKey(e);
+ element._handleToggleDiffMode(e);
assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE'));
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index f54e0583a8..42c9e88ec0 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -197,23 +197,33 @@
],
keyBindings: {
- 'shift+left': '_handleShiftLeftKey',
- 'shift+right': '_handleShiftRightKey',
- 'i:keyup': '_handleIKey',
- 'shift+i:keyup': '_handleCapitalIKey',
- 'down j': '_handleDownKey',
- 'up k': '_handleUpKey',
- 'c': '_handleCKey',
- '[': '_handleLeftBracketKey',
- ']': '_handleRightBracketKey',
- 'o': '_handleOKey',
- 'n': '_handleNKey',
- 'p': '_handlePKey',
- 'r': '_handleRKey',
- 'shift+a': '_handleCapitalAKey',
- 'esc': '_handleEscKey',
+ esc: '_handleEscKey',
+ },
+
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
+ [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
+ [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
+ [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
+ [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
+ [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
+ [this.Shortcut.NEXT_LINE]: '_handleCursorNext',
+ [this.Shortcut.PREV_LINE]: '_handleCursorPrev',
+ [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
+ [this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
+ [this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
+ [this.Shortcut.OPEN_FILE]: '_handleOpenFile',
+ [this.Shortcut.NEXT_CHUNK]: '_handleNextChunk',
+ [this.Shortcut.PREV_CHUNK]: '_handlePrevChunk',
+ [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+ [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
+
+ // Final two are actually handled by gr-diff-comment-thread.
+ [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+ [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+ };
},
-
listeners: {
keydown: '_scopedKeydownHandler',
},
@@ -232,7 +242,7 @@
_scopedKeydownHandler(e) {
if (e.keyCode === 13) {
// Enter.
- this._handleOKey(e);
+ this._handleOpenFile(e);
}
},
@@ -536,7 +546,7 @@
this._togglePathExpanded(path);
},
- _handleShiftLeftKey(e) {
+ _handleLeftPane(e) {
if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
return;
}
@@ -545,7 +555,7 @@
this.$.diffCursor.moveLeft();
},
- _handleShiftRightKey(e) {
+ _handleRightPane(e) {
if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
return;
}
@@ -554,7 +564,7 @@
this.$.diffCursor.moveRight();
},
- _handleIKey(e) {
+ _handleToggleInlineDiff(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e) ||
this.$.fileCursor.index === -1) { return; }
@@ -563,14 +573,14 @@
this._togglePathExpandedByIndex(this.$.fileCursor.index);
},
- _handleCapitalIKey(e) {
+ _handleToggleAllInlineDiffs(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
e.preventDefault();
this._toggleInlineDiffs();
},
- _handleDownKey(e) {
+ _handleCursorNext(e) {
if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
return;
}
@@ -588,7 +598,7 @@
}
},
- _handleUpKey(e) {
+ _handleCursorPrev(e) {
if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
return;
}
@@ -606,7 +616,7 @@
}
},
- _handleCKey(e) {
+ _handleNewComment(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -619,7 +629,7 @@
}
},
- _handleLeftBracketKey(e) {
+ _handleOpenLastFile(e) {
// Check for meta key to avoid overriding native chrome shortcut.
if (this.shouldSuppressKeyboardShortcut(e) ||
this.getKeyboardEvent(e).metaKey) { return; }
@@ -628,7 +638,7 @@
this._openSelectedFile(this._files.length - 1);
},
- _handleRightBracketKey(e) {
+ _handleOpenFirstFile(e) {
// Check for meta key to avoid overriding native chrome shortcut.
if (this.shouldSuppressKeyboardShortcut(e) ||
this.getKeyboardEvent(e).metaKey) { return; }
@@ -637,7 +647,7 @@
this._openSelectedFile(0);
},
- _handleOKey(e) {
+ _handleOpenFile(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
e.preventDefault();
@@ -650,7 +660,7 @@
this._openSelectedFile();
},
- _handleNKey(e) {
+ _handleNextChunk(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
(this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
this._noDiffsExpanded()) {
@@ -665,7 +675,7 @@
}
},
- _handlePKey(e) {
+ _handlePrevChunk(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
(this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
this._noDiffsExpanded()) {
@@ -680,7 +690,7 @@
}
},
- _handleRKey(e) {
+ _handleToggleFileReviewed(e) {
if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
return;
}
@@ -690,7 +700,7 @@
this._reviewFile(this._files[this.$.fileCursor.index].__path);
},
- _handleCapitalAKey(e) {
+ _handleToggleLeftPane(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
e.preventDefault();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 88b5f66e3f..df92b1e149 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -49,6 +49,24 @@ limitations under the License.
<script>
suite('gr-file-list tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
+ kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+ kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
+ kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
+ kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
+ kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
+ kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
+ kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+
let element;
let commentApiWrapper;
let sandbox;
@@ -668,7 +686,7 @@ limitations under the License.
assert.equal(getNumReviewed(), 0);
});
- suite('_handleOKey', () => {
+ suite('_handleOpenFile', () => {
let interact;
setup(() => {
@@ -686,7 +704,7 @@ limitations under the License.
const e = new CustomEvent('fake-keyboard-event', opt_payload);
sinon.stub(e, 'preventDefault');
- element._handleOKey(e);
+ element._handleOpenFile(e);
assert.isTrue(e.preventDefault.called);
const result = {};
if (openCursorStub.called) {
@@ -1545,7 +1563,7 @@ limitations under the License.
setup(() => {
sandbox.stub(element, '_renderInOrder').returns(Promise.resolve());
- nKeySpy = sandbox.spy(element, '_handleNKey');
+ nKeySpy = sandbox.spy(element, '_handleNextChunk');
nextCommentStub = sandbox.stub(element.$.diffCursor,
'moveToNextCommentThread');
nextChunkStub = sandbox.stub(element.$.diffCursor,
@@ -1632,11 +1650,11 @@ limitations under the License.
const mockEvent = {preventDefault() {}};
element._displayLine = false;
- element._handleDownKey(mockEvent);
+ element._handleCursorNext(mockEvent);
assert.isTrue(element._displayLine);
element._displayLine = false;
- element._handleUpKey(mockEvent);
+ element._handleCursorPrev(mockEvent);
assert.isTrue(element._displayLine);
element._displayLine = true;
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
new file mode 100644
index 0000000000..2ff7953a37
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.html
@@ -0,0 +1,48 @@
+<!--
+@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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-key-binding-display">
+ <template>
+ <style include="shared-styles">
+ .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;
+ }
+ </style>
+ <template is="dom-repeat" items="[[binding]]">
+ <template is="dom-if" if="[[index]]">
+ or
+ </template>
+ <template
+ is="dom-repeat"
+ items="[[_computeModifiers(item)]]"
+ as="modifier">
+ <span class="key modifier">[[modifier]]</span>
+ </template>
+ <span class="key">[[_computeKey(item)]]</span>
+ </template>
+ </template>
+ <script src="gr-key-binding-display.js"></script>
+</dom-module>
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<string>} */
+ 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 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-key-binding-display</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-key-binding-display.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-key-binding-display></gr-key-binding-display>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-key-binding-display tests', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ suite('_computeKey', () => {
+ test('unmodified key', () => {
+ assert.strictEqual(element._computeKey(['x']), 'x');
+ });
+
+ test('key with modifiers', () => {
+ assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x');
+ assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x');
+ });
+ });
+
+ suite('_computeModifiers', () => {
+ test('single unmodified key', () => {
+ assert.deepEqual(element._computeModifiers(['x']), []);
+ });
+
+ test('key with modifiers', () => {
+ assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']);
+ assert.deepEqual(
+ element._computeModifiers(['Shift', 'Meta', 'x']),
+ ['Shift', 'Meta']);
+ });
+ });
+ });
+</script>
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.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../gr-key-binding-display/gr-key-binding-display.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-keyboard-shortcuts-dialog">
@@ -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.
<main>
<table>
<tbody>
- <tr>
- <td></td><td class="header">Everywhere</td>
- </tr>
- <tr>
- <td><span class="key">/</span></td>
- <td>Search</td>
- </tr>
- <tr>
- <td><span class="key">?</span></td>
- <td>Show this dialog</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">g</span>
- <span class="key">o</span>
- </td>
- <td>Go to Opened Changes</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">g</span>
- <span class="key">m</span>
- </td>
- <td>Go to Merged Changes</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">g</span>
- <span class="key">a</span>
- </td>
- <td>Go to Abandoned Changes</td>
- </tr>
- </tbody>
- <!-- Change View -->
- <tbody hidden$="[[!_computeInView(view, 'change')]]" hidden>
- <tr>
- <td></td><td class="header">Navigation</td>
- </tr>
- <tr>
- <td><span class="key">]</span></td>
- <td>Show first file</td>
- </tr>
- <tr>
- <td><span class="key">[</span></td>
- <td>Show last file</td>
- </tr>
- <tr>
- <td><span class="key">u</span></td>
- <td>Up to dashboard</td>
- </tr>
- </tbody>
- <!-- Diff View -->
- <tbody hidden$="[[!_computeInView(view, 'diff')]]" hidden>
- <tr>
- <td></td><td class="header">Navigation</td>
- </tr>
- <tr>
- <td><span class="key">]</span></td>
- <td>Show next file</td>
- </tr>
- <tr>
- <td><span class="key">[</span></td>
- <td>Show previous file</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">j</span>
- </td>
- <td>Show next file that has comments</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">k</span>
- </td>
- <td>Show previous file that has comments</td>
- </tr>
- <tr>
- <td><span class="key">u</span></td>
- <td>Up to change</td>
- </tr>
- </tbody>
- </table>
-
- <table>
- <!-- Change List -->
- <tbody hidden$="[[!_computeInView(view, 'search')]]" hidden>
- <tr>
- <td></td><td class="header">Change list</td>
- </tr>
- <tr>
- <td><span class="key">j</span></td>
- <td>Select next change</td>
- </tr>
- <tr>
- <td><span class="key">k</span></td>
- <td>Show previous change</td>
- </tr>
- <tr>
- <td><span class="key">n</span> or <span class="key">]</span></td>
- <td>Go to next page</td>
- </tr>
- <tr>
- <td><span class="key">p</span> or <span class="key">[</span></td>
- <td>Go to previous page</td>
- </tr>
- <tr>
- <td>
- <span class="key">Enter</span> or
- <span class="key">o</span>
- </td>
- <td>Show selected change</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">r</span>
- </td>
- <td>Refresh list of changes</td>
- </tr>
- <tr>
- <td><span class="key">s</span></td>
- <td>Star (or unstar) change</td>
- </tr>
- </tbody>
- <!-- Dashboard -->
- <tbody hidden$="[[!_computeInView(view, 'dashboard')]]" hidden>
- <tr>
- <td></td><td class="header">Dashboard</td>
- </tr>
- <tr>
- <td><span class="key">j</span></td>
- <td>Select next change</td>
- </tr>
- <tr>
- <td><span class="key">k</span></td>
- <td>Show previous change</td>
- </tr>
- <tr>
- <td>
- <span class="key">Enter</span> or
- <span class="key">o</span>
- </td>
- <td>Show selected change</td>
- </tr>
- <tr>
- <td><span class="key">r</span></td>
- <td>Mark (or unmark) change as reviewed</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">r</span>
- </td>
- <td>Refresh list of changes</td>
- </tr>
- <tr>
- <td><span class="key">s</span></td>
- <td>Star (or unstar) change</td>
- </tr>
- </tbody>
- <!-- Change View -->
- <tbody hidden$="[[!_computeInView(view, 'change')]]" hidden>
- <tr>
- <td></td><td class="header">Actions</td>
- </tr>
- <tr>
- <td><span class="key">a</span></td>
- <td>Open reply dialog to publish comments and add reviewers</td>
- </tr>
- <tr>
- <td><span class="key">d</span></td>
- <td>Open download overlay</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">r</span>
- </td>
- <td>Reload the change at the latest patch</td>
- </tr>
- <tr>
- <td><span class="key">s</span></td>
- <td>Star (or unstar) change</td>
- </tr>
- <tr>
- <td><span class="key">x</span></td>
- <td>Expand all messages</td>
- </tr>
- <tr>
- <td><span class="key">z</span></td>
- <td>Collapse all messages</td>
- </tr>
- <tr>
- <td></td><td class="header">Reply dialog</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Ctrl</span>
- <span class="key">Enter</span><br/>
- </td>
- <td>Send reply</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Meta</span>
- <span class="key">Enter</span>
- </td>
- <td>Send reply</td>
- </tr>
- <tr>
- <td></td><td class="header">File list</td>
- </tr>
- <tr>
- <td><span class="key">j</span> or <span class="key">↓</span></td>
- <td>Select next file</td>
- </tr>
- <tr>
- <td><span class="key">k</span> or <span class="key">↑</span></td>
- <td>Select previous file</td>
- </tr>
- <tr>
- <td>
- <span class="key">Enter</span> or
- <span class="key">o</span>
- </td>
- <td>Go to selected file</td>
- </tr>
- <tr>
- <td><span class="key">r</span></td>
- <td>Toggle review flag on selected file</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">i</span>
- </td>
- <td>Show/hide all inline diffs</td>
- </tr>
- <tr>
- <td><span class="key">i</span></td>
- <td>Show/hide selected inline diff</td>
- </tr>
- <tr>
- <td></td><td class="header">Diffs</td>
- </tr>
- <tr>
- <td><span class="key">j</span> or <span class="key">↓</span></td>
- <td>Go to next line</td>
- </tr>
- <tr>
- <td><span class="key">k</span> or <span class="key">↑</span></td>
- <td>Go to previous line</td>
- </tr>
- <tr>
- <td><span class="key">n</span></td>
- <td>Go to next diff chunk</td>
- </tr>
- <tr>
- <td><span class="key">p</span></td>
- <td>Go to previous diff chunk</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">n</span>
- </td>
- <td>Go to next comment thread</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">p</span>
- </td>
- <td>Go to previous comment thread</td>
- </tr>
- <tr>
- <td><span class="key">e</span></td>
- <td>Expand all comment threads</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">e</span>
- </td>
- <td>Collapse all comment threads</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">←</span>
- </td>
- <td>Select left pane</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">→</span>
- </td>
- <td>Select right pane</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">a</span>
- </td>
- <td>Hide/show left diff</td>
- </tr>
- <tr>
- <td>
- <span class="key">m</span>
- </td>
- <td>Toggle unified/side-by-side diff</td>
- </tr>
- <tr>
- <td>
- <span class="key">c</span>
- </td>
- <td>Draft new comment</td>
- </tr>
- </tbody>
- <!-- Diff View -->
- <tbody hidden$="[[!_computeInView(view, 'diff')]]" hidden>
- <tr>
- <td></td><td class="header">Actions</td>
- </tr>
- <tr>
- <td><span class="key">j</span> or <span class="key">↓</span></td>
- <td>Show next line</td>
- </tr>
- <tr>
- <td><span class="key">k</span> or <span class="key">↑</span></td>
- <td>Show previous line</td>
- </tr>
- <tr>
- <td><span class="key">n</span></td>
- <td>Show next diff chunk</td>
- </tr>
- <tr>
- <td><span class="key">p</span></td>
- <td>Show previous diff chunk</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">x</span>
- </td>
- <td>Expand all diff context</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">n</span>
- </td>
- <td>Show next comment thread</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">p</span>
- </td>
- <td>Show previous comment thread</td>
- </tr>
- <tr>
- <td><span class="key">e</span></td>
- <td>Expand all comment threads</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">e</span>
- </td>
- <td>Collapse all comment threads</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">←</span>
- </td>
- <td>Select left pane</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">→</span>
- </td>
- <td>Select right pane</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">a</span>
- </td>
- <td>Hide/show left diff</td>
- </tr>
- <tr>
- <td>
- <span class="key">m</span>
- </td>
- <td>Toggle unified/side-by-side diff</td>
- </tr>
- <tr>
- <td>
- <span class="key">c</span>
- </td>
- <td>Draft new comment</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Ctrl</span>
- <span class="key">s</span><br/>
- </td>
- <td>Save comment</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Ctrl</span>
- <span class="key">Enter</span><br/>
- </td>
- <td>Save comment</td>
- </tr>
- <tr>
- <td>
- <span class="key modifier">Meta</span>
- <span class="key">Enter</span>
- </td>
- <td>Save comment</td>
- </tr>
- <tr>
- <td><span class="key">a</span></td>
- <td>Open reply dialog to publish comments and add reviewers</td>
- </tr>
- <tr>
- <td><span class="key">,</span></td>
- <td>Show diff preferences</td>
- </tr>
- <tr>
- <td><span class="key">r</span></td>
- <td>Mark/unmark file as reviewed</td>
- </tr>
+ <template is="dom-repeat" items="[[_left]]">
+ <tr>
+ <td></td><td class="header">[[item.section]]</td>
+ </tr>
+ <template is="dom-repeat" items="[[item.shortcuts]]" as="shortcut">
+ <tr>
+ <td>
+ <gr-key-binding-display binding="[[shortcut.binding]]">
+ </gr-key-binding-display>
+ </td>
+ <td>[[shortcut.text]]</td>
+ </tr>
+ </template>
+ </template>
</tbody>
</table>
+ <template is="dom-if" if="[[_right]]">
+ <table>
+ <tbody>
+ <template is="dom-repeat" items="[[_right]]">
+ <tr>
+ <td></td><td class="header">[[item.section]]</td>
+ </tr>
+ <template is="dom-repeat" items="[[item.shortcuts]]" as="shortcut">
+ <tr>
+ <td>
+ <gr-key-binding-display binding="[[shortcut.binding]]">
+ </gr-key-binding-display>
+ </td>
+ <td>[[shortcut.text]]</td>
+ </tr>
+ </template>
+ </template>
+ </tbody>
+ </table>
+ </template>
</main>
<footer></footer>
</template>
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 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-key-binding-display</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-keyboard-shortcuts-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-keyboard-shortcuts-dialog></gr-keyboard-shortcuts-dialog>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-keyboard-shortcuts-dialog tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ function update(directory) {
+ element._onDirectoryUpdated(directory);
+ flushAsynchronousOperations();
+ }
+
+ suite('_left and _right contents', () => {
+ test('empty dialog', () => {
+ assert.strictEqual(element._left.length, 0);
+ assert.strictEqual(element._right.length, 0);
+ });
+
+ test('everywhere goes on left', () => {
+ update(new Map([
+ [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._left,
+ [
+ {
+ section: kb.ShortcutSection.EVERYWHERE,
+ shortcuts: ['everywhere shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._right.length, 0);
+ });
+
+ test('navigation goes on left', () => {
+ update(new Map([
+ [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._left,
+ [
+ {
+ section: kb.ShortcutSection.NAVIGATION,
+ shortcuts: ['navigation shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._right.length, 0);
+ });
+
+ test('actions go on right', () => {
+ update(new Map([
+ [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.ACTIONS,
+ shortcuts: ['actions shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._left.length, 0);
+ });
+
+ test('reply dialog goes on right', () => {
+ update(new Map([
+ [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.REPLY_DIALOG,
+ shortcuts: ['reply dialog shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._left.length, 0);
+ });
+
+ test('file list goes on right', () => {
+ update(new Map([
+ [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.FILE_LIST,
+ shortcuts: ['file list shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._left.length, 0);
+ });
+
+ test('diffs go on right', () => {
+ update(new Map([
+ [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.DIFFS,
+ shortcuts: ['diffs shortcuts'],
+ },
+ ]);
+ assert.strictEqual(element._left.length, 0);
+ });
+
+ test('multiple sections on each side', () => {
+ update(new Map([
+ [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
+ [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
+ [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
+ [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
+ ]));
+ assert.deepEqual(
+ element._left,
+ [
+ {
+ section: kb.ShortcutSection.EVERYWHERE,
+ shortcuts: ['everywhere shortcuts'],
+ },
+ {
+ section: kb.ShortcutSection.NAVIGATION,
+ shortcuts: ['navigation shortcuts'],
+ },
+ ]);
+ assert.deepEqual(
+ element._right,
+ [
+ {
+ section: kb.ShortcutSection.ACTIONS,
+ shortcuts: ['actions shortcuts'],
+ },
+ {
+ section: kb.ShortcutSection.DIFFS,
+ shortcuts: ['diffs shortcuts'],
+ },
+ ]);
+ });
+ });
+ });
+</script>
+
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.
<script>
suite('gr-search-bar tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.SEARCH, '/');
+
let element;
let sandbox;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index b0eb42337b..0f717e7b46 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -177,22 +177,41 @@
],
keyBindings: {
- 'esc': '_handleEscKey',
- 'shift+left': '_handleShiftLeftKey',
- 'shift+right': '_handleShiftRightKey',
- 'up k': '_handleUpKey',
- 'down j': '_handleDownKey',
- 'c': '_handleCKey',
- '[': '_handleLeftBracketKey',
- ']': '_handleRightBracketKey',
- 'n shift+n': '_handleNKey',
- 'p shift+p': '_handlePKey',
- 'a shift+a': '_handleAKey',
- 'u': '_handleUKey',
- ',': '_handleCommaKey',
- 'm': '_handleMKey',
- 'r': '_handleRKey',
- 'shift+x': '_handleShiftXKey',
+ esc: '_handleEscKey',
+ },
+
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
+ [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
+ [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
+ [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
+ [this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
+ '_handleNextLineOrFileWithComments',
+ [this.Shortcut.PREV_FILE_WITH_COMMENTS]:
+ '_handlePrevLineOrFileWithComments',
+ [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
+ [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
+ [this.Shortcut.NEXT_FILE]: '_handleNextFile',
+ [this.Shortcut.PREV_FILE]: '_handlePrevFile',
+ [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
+ [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
+ [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
+ [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
+ [this.Shortcut.OPEN_REPLY_DIALOG]:
+ '_handleOpenReplyDialogOrToggleLeftPane',
+ [this.Shortcut.TOGGLE_LEFT_PANE]:
+ '_handleOpenReplyDialogOrToggleLeftPane',
+ [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
+ [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
+ [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
+ [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+ [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
+
+ // Final two are actually handled by gr-diff-comment-thread.
+ [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+ [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+ };
},
attached() {
@@ -263,7 +282,7 @@
this._patchRange.patchNum, this._path, reviewed);
},
- _handleRKey(e) {
+ _handleToggleFileReviewed(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -279,21 +298,21 @@
this.$.diffHost.displayLine = false;
},
- _handleShiftLeftKey(e) {
+ _handleLeftPane(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
e.preventDefault();
this.$.cursor.moveLeft();
},
- _handleShiftRightKey(e) {
+ _handleRightPane(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
e.preventDefault();
this.$.cursor.moveRight();
},
- _handleUpKey(e) {
+ _handlePrevLineOrFileWithComments(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
if (e.detail.keyboardEvent.shiftKey &&
e.detail.keyboardEvent.keyCode === 75) { // 'K'
@@ -307,7 +326,7 @@
this.$.cursor.moveUp();
},
- _handleDownKey(e) {
+ _handleNextLineOrFileWithComments(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
if (e.detail.keyboardEvent.shiftKey &&
e.detail.keyboardEvent.keyCode === 74) { // 'J'
@@ -348,7 +367,7 @@
this._patchRange.patchNum, this._patchRange.basePatchNum);
},
- _handleCKey(e) {
+ _handleNewComment(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
if (this.$.diffHost.isRangeSelected()) { return; }
if (this.modifierPressed(e)) { return; }
@@ -360,7 +379,7 @@
}
},
- _handleLeftBracketKey(e) {
+ _handlePrevFile(e) {
// Check for meta key to avoid overriding native chrome shortcut.
if (this.shouldSuppressKeyboardShortcut(e) ||
this.getKeyboardEvent(e).metaKey) { return; }
@@ -369,7 +388,7 @@
this._navToFile(this._path, this._fileList, -1);
},
- _handleRightBracketKey(e) {
+ _handleNextFile(e) {
// Check for meta key to avoid overriding native chrome shortcut.
if (this.shouldSuppressKeyboardShortcut(e) ||
this.getKeyboardEvent(e).metaKey) { return; }
@@ -378,7 +397,7 @@
this._navToFile(this._path, this._fileList, 1);
},
- _handleNKey(e) {
+ _handleNextChunkOrCommentThread(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
e.preventDefault();
@@ -390,7 +409,7 @@
}
},
- _handlePKey(e) {
+ _handlePrevChunkOrCommentThread(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
e.preventDefault();
@@ -402,7 +421,7 @@
}
},
- _handleAKey(e) {
+ _handleOpenReplyDialogOrToggleLeftPane(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
@@ -420,7 +439,7 @@
this._navToChangeView();
},
- _handleUKey(e) {
+ _handleUpToChange(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -436,7 +455,7 @@
this.$.diffPreferences.open();
},
- _handleMKey(e) {
+ _handleToggleDiffMode(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -989,7 +1008,7 @@
return '';
},
- _handleShiftXKey(e) {
+ _handleExpandAllDiffContext(e) {
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
this.$.diffHost.expandAllContext();
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 00527e4b28..2a0a0f3963 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -43,6 +43,31 @@ limitations under the License.
<script>
suite('gr-diff-view tests', () => {
+ const kb = window.Gerrit.KeyboardShortcutBinder;
+ kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
+ kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
+ kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
+ kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
+ kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+ kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+ kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
+ kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
+ kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
+ kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
+ kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
+ kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+ kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
+ kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+ kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+ kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
+ kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+ kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
+ kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
+
let element;
let sandbox;
@@ -838,16 +863,16 @@ limitations under the License.
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
});
- test('_handleMKey', () => {
+ test('_handleToggleDiffMode', () => {
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const e = {preventDefault: () => {}};
// Initial state.
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- element._handleMKey(e);
+ element._handleToggleDiffMode(e);
assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
- element._handleMKey(e);
+ element._handleToggleDiffMode(e);
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
});
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 0508608ed2..b0cc514d60 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -17,10 +17,6 @@
(function() {
'use strict';
- // 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 G_KEY_TIMEOUT_MS = 1000;
-
// Eagerly render Polymer components when backgrounded. (Skips
// requestAnimationFrame.)
// @see https://github.com/Polymer/polymer/issues/3851
@@ -112,11 +108,17 @@
Gerrit.KeyboardShortcutBehavior,
],
- keyBindings: {
- '?': '_showKeyboardShortcuts',
- 'g:keydown': '_gKeyDown',
- 'g:keyup': '_gKeyUp',
- 'a m o': '_jumpKeyPressed',
+ keyboardShortcuts() {
+ return {
+ [this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
+ [this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
+ [this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
+ [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
+ };
+ },
+
+ created() {
+ this._bindKeyboardShortcuts();
},
ready() {
@@ -171,6 +173,118 @@
};
},
+ _bindKeyboardShortcuts() {
+ this.bindShortcut(this.Shortcut.SEND_REPLY,
+ this.DOC_ONLY, 'ctrl+enter', 'meta+enter');
+
+ this.bindShortcut(
+ this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
+ this.bindShortcut(
+ this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
+
+ this.bindShortcut(
+ this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_PREV_CHANGE, 'k');
+ this.bindShortcut(
+ this.Shortcut.OPEN_CHANGE, 'o');
+ this.bindShortcut(
+ this.Shortcut.NEXT_PAGE, 'n', ']');
+ this.bindShortcut(
+ this.Shortcut.PREV_PAGE, 'p', '[');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_CHANGE_STAR, 's');
+ this.bindShortcut(
+ this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
+
+ this.bindShortcut(
+ this.Shortcut.OPEN_REPLY_DIALOG, 'a');
+ this.bindShortcut(
+ this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+ this.bindShortcut(
+ this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+ this.bindShortcut(
+ this.Shortcut.REFRESH_CHANGE, 'shift+r');
+ this.bindShortcut(
+ this.Shortcut.UP_TO_DASHBOARD, 'u');
+ this.bindShortcut(
+ this.Shortcut.UP_TO_CHANGE, 'u');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_DIFF_MODE, 'm');
+
+ this.bindShortcut(
+ this.Shortcut.NEXT_LINE, 'j', 'down');
+ this.bindShortcut(
+ this.Shortcut.PREV_LINE, 'k', 'up');
+ this.bindShortcut(
+ this.Shortcut.NEXT_CHUNK, 'n');
+ this.bindShortcut(
+ this.Shortcut.PREV_CHUNK, 'p');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+ this.bindShortcut(
+ this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+ this.bindShortcut(
+ this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+ this.bindShortcut(
+ this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
+ this.bindShortcut(
+ this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
+ this.DOC_ONLY, 'shift+e');
+ this.bindShortcut(
+ this.Shortcut.LEFT_PANE, 'shift+left');
+ this.bindShortcut(
+ this.Shortcut.RIGHT_PANE, 'shift+right');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+ this.bindShortcut(
+ this.Shortcut.NEW_COMMENT, 'c');
+ this.bindShortcut(
+ this.Shortcut.SAVE_COMMENT,
+ 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
+ this.bindShortcut(
+ this.Shortcut.OPEN_DIFF_PREFS, ',');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r');
+
+ this.bindShortcut(
+ this.Shortcut.NEXT_FILE, ']');
+ this.bindShortcut(
+ this.Shortcut.PREV_FILE, '[');
+ this.bindShortcut(
+ this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+ this.bindShortcut(
+ this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+ this.bindShortcut(
+ this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+ this.bindShortcut(
+ this.Shortcut.OPEN_FILE, 'o', 'enter');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+
+ this.bindShortcut(
+ this.Shortcut.OPEN_FIRST_FILE, ']');
+ this.bindShortcut(
+ this.Shortcut.OPEN_LAST_FILE, '[');
+
+ this.bindShortcut(
+ this.Shortcut.SEARCH, '/');
+ },
+
_accountChanged(account) {
if (!account) { return; }
@@ -293,32 +407,16 @@
return isShadowDom ? 'shadow' : '';
},
- _gKeyDown(e) {
- if (this.modifierPressed(e)) { return; }
- this._lastGKeyPressTimestamp = Date.now();
+ _goToOpenedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('open');
},
- _gKeyUp() {
- this._lastGKeyPressTimestamp = null;
+ _goToMergedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('merged');
},
- _jumpKeyPressed(e) {
- if (!this._lastGKeyPressTimestamp ||
- (Date.now() - this._lastGKeyPressTimestamp > G_KEY_TIMEOUT_MS) ||
- this.shouldSuppressKeyboardShortcut(e)) { return; }
- e.preventDefault();
-
- let status = null;
- if (e.detail.key === 'a') {
- status = 'abandoned';
- } else if (e.detail.key === 'm') {
- status = 'merged';
- } else if (e.detail.key === 'o') {
- status = 'open';
- }
- if (status !== null) {
- Gerrit.Nav.navigateToStatusSearch(status);
- }
+ _goToAbandonedChanges() {
+ Gerrit.Nav.navigateToStatusSearch('abandoned');
},
_computePluginScreenName({plugin, screen}) {
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index fb1b241c7e..734d2fe96c 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -114,55 +114,5 @@ limitations under the License.
element._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
assert.ok(element._lastSearchPage);
});
-
- suite('_jumpKeyPressed', () => {
- let navStub;
-
- setup(() => {
- navStub = sandbox.stub(Gerrit.Nav, 'navigateToStatusSearch');
- sandbox.stub(Date, 'now').returns(10000);
- });
-
- test('success', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._lastGKeyPressTimestamp = 9000;
- element._jumpKeyPressed(e);
- assert.isTrue(navStub.calledOnce);
- assert.equal(navStub.lastCall.args[0], 'abandoned');
- });
-
- test('no g key', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._lastGKeyPressTimestamp = null;
- element._jumpKeyPressed(e);
- assert.isFalse(navStub.called);
- });
-
- test('g key too long ago', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._lastGKeyPressTimestamp = 3000;
- element._jumpKeyPressed(e);
- assert.isFalse(navStub.called);
- });
-
- test('should suppress', () => {
- const e = {detail: {key: 'a'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
- element._lastGKeyPressTimestamp = 9000;
- element._jumpKeyPressed(e);
- assert.isFalse(navStub.called);
- });
-
- test('unrecognized key', () => {
- const e = {detail: {key: 'f'}, preventDefault: () => {}};
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- element._lastGKeyPressTimestamp = 9000;
- element._jumpKeyPressed(e);
- assert.isFalse(navStub.called);
- });
- });
});
</script>
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',