blob: affdc088b54845b025053face588de4c21a2ce85 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @enum {number} */
const ShortcutError = {
NO_ERROR: 0,
INCLUDE_START_MODIFIER: 1,
TOO_MANY_MODIFIERS: 2,
NEED_CHARACTER: 3,
};
cr.define('extensions', function() {
'use strict';
// The UI to display and manage keyboard shortcuts set for extension commands.
const ShortcutInput = Polymer({
is: 'extensions-shortcut-input',
properties: {
/** @type {!extensions.KeyboardShortcutDelegate} */
delegate: Object,
item: {
type: String,
value: '',
},
commandName: {
type: String,
value: '',
},
shortcut: {
type: String,
value: '',
},
/** @private */
capturing_: {
type: Boolean,
value: false,
},
/** @private {!ShortcutError} */
error_: {
type: Number,
value: 0,
},
/** @private */
pendingShortcut_: {
type: String,
value: '',
},
},
/** @override */
ready: function() {
const node = this.$.input;
node.addEventListener('mouseup', this.startCapture_.bind(this));
node.addEventListener('blur', this.endCapture_.bind(this));
node.addEventListener('focus', this.startCapture_.bind(this));
node.addEventListener('keydown', this.onKeyDown_.bind(this));
node.addEventListener('keyup', this.onKeyUp_.bind(this));
},
/** @private */
startCapture_: function() {
if (this.capturing_)
return;
this.capturing_ = true;
this.delegate.setShortcutHandlingSuspended(true);
},
/** @private */
endCapture_: function() {
if (!this.capturing_)
return;
this.pendingShortcut_ = '';
this.capturing_ = false;
const input = this.$.input;
input.blur();
input.invalid = false;
this.delegate.setShortcutHandlingSuspended(false);
},
/**
* @param {!KeyboardEvent} e
* @private
*/
onKeyDown_: function(e) {
if (e.target == this.$.clear)
return;
if (e.keyCode == extensions.Key.Escape) {
if (!this.capturing_) {
// If we're not currently capturing, allow escape to propagate.
return;
}
// Otherwise, escape cancels capturing.
this.endCapture_();
e.preventDefault();
e.stopPropagation();
return;
}
if (e.keyCode == extensions.Key.Tab) {
// Allow tab propagation for keyboard navigation.
return;
}
if (!this.capturing_)
this.startCapture_();
this.handleKey_(e);
},
/**
* @param {!KeyboardEvent} e
* @private
*/
onKeyUp_: function(e) {
// Ignores pressing 'Space' or 'Enter' on the clear button. In 'Enter's
// case, the clear button disappears before key-up, so 'Enter's key-up
// target becomes the input field, not the clear button, and needs to
// be caught explicitly.
if (e.target == this.$.clear || e.key == 'Enter')
return;
if (e.keyCode == extensions.Key.Escape || e.keyCode == extensions.Key.Tab)
return;
this.handleKey_(e);
},
/**
* @param {!ShortcutError} error
* @param {string} includeStartModifier
* @param {string} tooManyModifiers
* @param {string} needCharacter
* @return {string} UI string.
* @private
*/
getErrorString_: function(
error, includeStartModifier, tooManyModifiers, needCharacter) {
if (error == ShortcutError.TOO_MANY_MODIFIERS)
return tooManyModifiers;
if (error == ShortcutError.NEED_CHARACTER)
return needCharacter;
return includeStartModifier;
},
/**
* @param {!KeyboardEvent} e
* @private
*/
handleKey_: function(e) {
// While capturing, we prevent all events from bubbling, to prevent
// shortcuts lacking the right modifier (F3 for example) from activating
// and ending capture prematurely.
e.preventDefault();
e.stopPropagation();
// We don't allow both Ctrl and Alt in the same keybinding.
// TODO(devlin): This really should go in extensions.hasValidModifiers,
// but that requires updating the existing page as well.
if (e.ctrlKey && e.altKey) {
this.error_ = ShortcutError.TOO_MANY_MODIFIERS;
this.$.input.invalid = true;
return;
}
if (!extensions.hasValidModifiers(e)) {
this.pendingShortcut_ = '';
this.error_ = ShortcutError.INCLUDE_START_MODIFIER;
this.$.input.invalid = true;
return;
}
this.pendingShortcut_ = extensions.keystrokeToString(e);
if (!extensions.isValidKeyCode(e.keyCode)) {
this.error_ = ShortcutError.NEED_CHARACTER;
this.$.input.invalid = true;
return;
}
this.$.input.invalid = false;
this.commitPending_();
this.endCapture_();
},
/** @private */
commitPending_: function() {
this.shortcut = this.pendingShortcut_;
this.delegate.updateExtensionCommandKeybinding(
this.item, this.commandName, this.shortcut);
},
/**
* @return {string} The text to be displayed in the shortcut field.
* @private
*/
computeText_: function() {
const shortcutString =
this.capturing_ ? this.pendingShortcut_ : this.shortcut;
return shortcutString.split('+').join(' + ');
},
/**
* @return {boolean} Whether the clear button is hidden.
* @private
*/
computeClearHidden_: function() {
// We don't want to show the clear button if the input is currently
// capturing a new shortcut or if there is no shortcut to clear.
return this.capturing_ || !this.shortcut;
},
/** @private */
onClearTap_: function() {
assert(this.shortcut);
this.pendingShortcut_ = '';
this.commitPending_();
this.endCapture_();
},
});
return {ShortcutInput: ShortcutInput};
});