ozone: evdev: Sync caps lock LED state to evdev
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_command_list.js
blob0a184dc8330f82ae17cf558cf986c535866ba6a2
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /** @typedef {{active: boolean,
6  *             command_name: string,
7  *             description: string,
8  *             extension_action: boolean,
9  *             extension_id: string,
10  *             global: boolean,
11  *             keybinding: string}}
12  */
13 var ExtensionCommand;
15 cr.define('options', function() {
16   'use strict';
18   /**
19    * Creates a new list of extension commands.
20    * @param {Object=} opt_propertyBag Optional properties.
21    * @constructor
22    * @extends {HTMLDivElement}
23    */
24   var ExtensionCommandList = cr.ui.define('div');
26   /** @const */ var keyComma = 188;
27   /** @const */ var keyDel = 46;
28   /** @const */ var keyDown = 40;
29   /** @const */ var keyEnd = 35;
30   /** @const */ var keyEscape = 27;
31   /** @const */ var keyHome = 36;
32   /** @const */ var keyIns = 45;
33   /** @const */ var keyLeft = 37;
34   /** @const */ var keyMediaNextTrack = 176;
35   /** @const */ var keyMediaPlayPause = 179;
36   /** @const */ var keyMediaPrevTrack = 177;
37   /** @const */ var keyMediaStop = 178;
38   /** @const */ var keyPageDown = 34;
39   /** @const */ var keyPageUp = 33;
40   /** @const */ var keyPeriod = 190;
41   /** @const */ var keyRight = 39;
42   /** @const */ var keySpace = 32;
43   /** @const */ var keyTab = 9;
44   /** @const */ var keyUp = 38;
46   /**
47    * Enum for whether we require modifiers of a keycode.
48    * @enum {number}
49    */
50   var Modifiers = {
51     ARE_NOT_ALLOWED: 0,
52     ARE_REQUIRED: 1
53   };
55   /**
56    * Returns whether the passed in |keyCode| is a valid extension command
57    * char or not. This is restricted to A-Z and 0-9 (ignoring modifiers) at
58    * the moment.
59    * @param {number} keyCode The keycode to consider.
60    * @return {boolean} Returns whether the char is valid.
61    */
62   function validChar(keyCode) {
63     return keyCode == keyComma ||
64            keyCode == keyDel ||
65            keyCode == keyDown ||
66            keyCode == keyEnd ||
67            keyCode == keyHome ||
68            keyCode == keyIns ||
69            keyCode == keyLeft ||
70            keyCode == keyMediaNextTrack ||
71            keyCode == keyMediaPlayPause ||
72            keyCode == keyMediaPrevTrack ||
73            keyCode == keyMediaStop ||
74            keyCode == keyPageDown ||
75            keyCode == keyPageUp ||
76            keyCode == keyPeriod ||
77            keyCode == keyRight ||
78            keyCode == keySpace ||
79            keyCode == keyTab ||
80            keyCode == keyUp ||
81            (keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
82            (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0));
83   }
85   /**
86    * Convert a keystroke event to string form, while taking into account
87    * (ignoring) invalid extension commands.
88    * @param {Event} event The keyboard event to convert.
89    * @return {string} The keystroke as a string.
90    */
91   function keystrokeToString(event) {
92     var output = [];
93     if (cr.isMac && event.metaKey)
94       output.push('Command');
95     if (cr.isChromeOS && event.metaKey)
96       output.push('Search');
97     if (event.ctrlKey)
98       output.push('Ctrl');
99     if (!event.ctrlKey && event.altKey)
100       output.push('Alt');
101     if (event.shiftKey)
102       output.push('Shift');
104     var keyCode = event.keyCode;
105     if (validChar(keyCode)) {
106       if ((keyCode >= 'A'.charCodeAt(0) && keyCode <= 'Z'.charCodeAt(0)) ||
107           (keyCode >= '0'.charCodeAt(0) && keyCode <= '9'.charCodeAt(0))) {
108         output.push(String.fromCharCode('A'.charCodeAt(0) + keyCode - 65));
109       } else {
110         switch (keyCode) {
111           case keyComma:
112             output.push('Comma'); break;
113           case keyDel:
114             output.push('Delete'); break;
115           case keyDown:
116             output.push('Down'); break;
117           case keyEnd:
118             output.push('End'); break;
119           case keyHome:
120             output.push('Home'); break;
121           case keyIns:
122             output.push('Insert'); break;
123           case keyLeft:
124             output.push('Left'); break;
125           case keyMediaNextTrack:
126             output.push('MediaNextTrack'); break;
127           case keyMediaPlayPause:
128             output.push('MediaPlayPause'); break;
129           case keyMediaPrevTrack:
130             output.push('MediaPrevTrack'); break;
131           case keyMediaStop:
132             output.push('MediaStop'); break;
133           case keyPageDown:
134             output.push('PageDown'); break;
135           case keyPageUp:
136             output.push('PageUp'); break;
137           case keyPeriod:
138             output.push('Period'); break;
139           case keyRight:
140             output.push('Right'); break;
141           case keySpace:
142             output.push('Space'); break;
143           case keyTab:
144             output.push('Tab'); break;
145           case keyUp:
146             output.push('Up'); break;
147         }
148       }
149     }
151     return output.join('+');
152   }
154   /**
155    * Returns whether the passed in |keyCode| require modifiers. Currently only
156    * "MediaNextTrack", "MediaPrevTrack", "MediaStop", "MediaPlayPause" are
157    * required to be used without any modifier.
158    * @param {number} keyCode The keycode to consider.
159    * @return {Modifiers} Returns whether the keycode require modifiers.
160    */
161   function modifiers(keyCode) {
162     switch (keyCode) {
163       case keyMediaNextTrack:
164       case keyMediaPlayPause:
165       case keyMediaPrevTrack:
166       case keyMediaStop:
167         return Modifiers.ARE_NOT_ALLOWED;
168       default:
169         return Modifiers.ARE_REQUIRED;
170     }
171   }
173   /**
174    * Return true if the specified keyboard event has any one of following
175    * modifiers: "Ctrl", "Alt", "Cmd" on Mac, and "Shift" when the
176    * countShiftAsModifier is true.
177    * @param {Event} event The keyboard event to consider.
178    * @param {boolean} countShiftAsModifier Whether the 'ShiftKey' should be
179    *     counted as modifier.
180    */
181   function hasModifier(event, countShiftAsModifier) {
182     return event.ctrlKey || event.altKey || (cr.isMac && event.metaKey) ||
183            (cr.isChromeOS && event.metaKey) ||
184            (countShiftAsModifier && event.shiftKey);
185   }
187   ExtensionCommandList.prototype = {
188     __proto__: HTMLDivElement.prototype,
190     /**
191      * While capturing, this records the current (last) keyboard event generated
192      * by the user. Will be |null| after capture and during capture when no
193      * keyboard event has been generated.
194      * @type {KeyboardEvent}.
195      * @private
196      */
197     currentKeyEvent_: null,
199     /**
200      * While capturing, this keeps track of the previous selection so we can
201      * revert back to if no valid assignment is made during capture.
202      * @type {string}.
203      * @private
204      */
205     oldValue_: '',
207     /**
208      * While capturing, this keeps track of which element the user asked to
209      * change.
210      * @type {HTMLElement}.
211      * @private
212      */
213     capturingElement_: null,
215     decorate: function() {
216       this.textContent = '';
218       // Iterate over the extension data and add each item to the list.
219       this.data_.commands.forEach(this.createNodeForExtension_.bind(this));
220     },
222     /**
223      * Synthesizes and initializes an HTML element for the extension command
224      * metadata given in |extension|.
225      * @param {Object} extension A dictionary of extension metadata.
226      * @private
227      */
228     createNodeForExtension_: function(extension) {
229       var template = $('template-collection-extension-commands').querySelector(
230           '.extension-command-list-extension-item-wrapper');
231       var node = template.cloneNode(true);
233       var title = node.querySelector('.extension-title');
234       title.textContent = extension.name;
236       this.appendChild(node);
238       // Iterate over the commands data within the extension and add each item
239       // to the list.
240       extension.commands.forEach(this.createNodeForCommand_.bind(this));
241     },
243     /**
244      * Synthesizes and initializes an HTML element for the extension command
245      * metadata given in |command|.
246      * @param {ExtensionCommand} command A dictionary of extension command
247      *     metadata.
248      * @private
249      */
250     createNodeForCommand_: function(command) {
251       var template = $('template-collection-extension-commands').querySelector(
252           '.extension-command-list-command-item-wrapper');
253       var node = template.cloneNode(true);
254       node.id = this.createElementId_(
255           'command', command.extension_id, command.command_name);
257       var description = node.querySelector('.command-description');
258       description.textContent = command.description;
260       var shortcutNode = node.querySelector('.command-shortcut-text');
261       shortcutNode.addEventListener('mouseup',
262                                     this.startCapture_.bind(this));
263       shortcutNode.addEventListener('focus', this.handleFocus_.bind(this));
264       shortcutNode.addEventListener('blur', this.handleBlur_.bind(this));
265       shortcutNode.addEventListener('keydown', this.handleKeyDown_.bind(this));
266       shortcutNode.addEventListener('keyup', this.handleKeyUp_.bind(this));
267       if (!command.active) {
268         shortcutNode.textContent =
269             loadTimeData.getString('extensionCommandsInactive');
271         var commandShortcut = node.querySelector('.command-shortcut');
272         commandShortcut.classList.add('inactive-keybinding');
273       } else {
274         shortcutNode.textContent = command.keybinding;
275       }
277       var commandClear = node.querySelector('.command-clear');
278       commandClear.id = this.createElementId_(
279           'clear', command.extension_id, command.command_name);
280       commandClear.title = loadTimeData.getString('extensionCommandsDelete');
281       commandClear.addEventListener('click', this.handleClear_.bind(this));
283       var select = node.querySelector('.command-scope');
284       select.id = this.createElementId_(
285           'setCommandScope', command.extension_id, command.command_name);
286       select.hidden = false;
287       // Add the 'In Chrome' option.
288       var option = document.createElement('option');
289       option.textContent = loadTimeData.getString('extensionCommandsRegular');
290       select.appendChild(option);
291       if (command.extension_action) {
292         // Extension actions cannot be global, so we might as well disable the
293         // combo box, to signify that.
294         select.disabled = true;
295       } else {
296         // Add the 'Global' option.
297         option = document.createElement('option');
298         option.textContent = loadTimeData.getString('extensionCommandsGlobal');
299         select.appendChild(option);
300         select.selectedIndex = command.global ? 1 : 0;
302         select.addEventListener(
303             'change', this.handleSetCommandScope_.bind(this));
304       }
306       this.appendChild(node);
307     },
309     /**
310      * Starts keystroke capture to determine which key to use for a particular
311      * extension command.
312      * @param {Event} event The keyboard event to consider.
313      * @private
314      */
315     startCapture_: function(event) {
316       if (this.capturingElement_)
317         return;  // Already capturing.
319       chrome.send('setShortcutHandlingSuspended', [true]);
321       var shortcutNode = event.target;
322       this.oldValue_ = shortcutNode.textContent;
323       shortcutNode.textContent =
324           loadTimeData.getString('extensionCommandsStartTyping');
325       shortcutNode.parentElement.classList.add('capturing');
327       var commandClear =
328           shortcutNode.parentElement.querySelector('.command-clear');
329       commandClear.hidden = true;
331       this.capturingElement_ = /** @type {HTMLElement} */(event.target);
332     },
334     /**
335      * Ends keystroke capture and either restores the old value or (if valid
336      * value) sets the new value as active..
337      * @param {Event} event The keyboard event to consider.
338      * @private
339      */
340     endCapture_: function(event) {
341       if (!this.capturingElement_)
342         return;  // Not capturing.
344       chrome.send('setShortcutHandlingSuspended', [false]);
346       var shortcutNode = this.capturingElement_;
347       var commandShortcut = shortcutNode.parentElement;
349       commandShortcut.classList.remove('capturing');
350       commandShortcut.classList.remove('contains-chars');
352       // When the capture ends, the user may have not given a complete and valid
353       // input (or even no input at all). Only a valid key event followed by a
354       // valid key combination will cause a shortcut selection to be activated.
355       // If no valid selection was made, however, revert back to what the
356       // textbox had before to indicate that the shortcut registration was
357       // canceled.
358       if (!this.currentKeyEvent_ || !validChar(this.currentKeyEvent_.keyCode))
359         shortcutNode.textContent = this.oldValue_;
361       var commandClear = commandShortcut.querySelector('.command-clear');
362       if (this.oldValue_ == '') {
363         commandShortcut.classList.remove('clearable');
364         commandClear.hidden = true;
365       } else {
366         commandShortcut.classList.add('clearable');
367         commandClear.hidden = false;
368       }
370       this.oldValue_ = '';
371       this.capturingElement_ = null;
372       this.currentKeyEvent_ = null;
373     },
375     /**
376      * Handles focus event and adds visual indication for active shortcut.
377      * @param {Event} event to consider.
378      * @private
379      */
380     handleFocus_: function(event) {
381       var commandShortcut = event.target.parentElement;
382       commandShortcut.classList.add('focused');
383     },
385     /**
386      * Handles lost focus event and removes visual indication of active shortcut
387      * also stops capturing on focus lost.
388      * @param {Event} event to consider.
389      * @private
390      */
391     handleBlur_: function(event) {
392       this.endCapture_(event);
393       var commandShortcut = event.target.parentElement;
394       commandShortcut.classList.remove('focused');
395     },
397     /**
398      * The KeyDown handler.
399      * @param {Event} event The keyboard event to consider.
400      * @private
401      */
402     handleKeyDown_: function(event) {
403       event = /** @type {KeyboardEvent} */(event);
404       if (event.keyCode == keyEscape) {
405         // Escape cancels capturing.
406         this.endCapture_(event);
407         var parsed = this.parseElementId_('clear',
408             event.target.parentElement.querySelector('.command-clear').id);
409         chrome.send('setExtensionCommandShortcut',
410             [parsed.extensionId, parsed.commandName, '']);
411         event.preventDefault();
412         event.stopPropagation();
413         return;
414       }
415       if (event.keyCode == keyTab) {
416         // Allow tab propagation for keyboard navigation.
417         return;
418       }
420       if (!this.capturingElement_)
421         this.startCapture_(event);
423       this.handleKey_(event);
424     },
426     /**
427      * The KeyUp handler.
428      * @param {Event} event The keyboard event to consider.
429      * @private
430      */
431     handleKeyUp_: function(event) {
432       event = /** @type {KeyboardEvent} */(event);
433       if (event.keyCode == keyTab) {
434         // Allow tab propagation for keyboard navigation.
435         return;
436       }
438       // We want to make it easy to change from Ctrl+Shift+ to just Ctrl+ by
439       // releasing Shift, but we also don't want it to be easy to lose for
440       // example Ctrl+Shift+F to Ctrl+ just because you didn't release Ctrl
441       // as fast as the other two keys. Therefore, we process KeyUp until you
442       // have a valid combination and then stop processing it (meaning that once
443       // you have a valid combination, we won't change it until the next
444       // KeyDown message arrives).
445       if (!this.currentKeyEvent_ || !validChar(this.currentKeyEvent_.keyCode)) {
446         if (!event.ctrlKey && !event.altKey ||
447             ((cr.isMac || cr.isChromeOS) && !event.metaKey)) {
448           // If neither Ctrl nor Alt is pressed then it is not a valid shortcut.
449           // That means we're back at the starting point so we should restart
450           // capture.
451           this.endCapture_(event);
452           this.startCapture_(event);
453         } else {
454           this.handleKey_(event);
455         }
456       }
457     },
459     /**
460      * A general key handler (used for both KeyDown and KeyUp).
461      * @param {KeyboardEvent} event The keyboard event to consider.
462      * @private
463      */
464     handleKey_: function(event) {
465       // While capturing, we prevent all events from bubbling, to prevent
466       // shortcuts lacking the right modifier (F3 for example) from activating
467       // and ending capture prematurely.
468       event.preventDefault();
469       event.stopPropagation();
471       if (modifiers(event.keyCode) == Modifiers.ARE_REQUIRED &&
472           !hasModifier(event, false)) {
473         // Ctrl or Alt (or Cmd on Mac) is a must for most shortcuts.
474         return;
475       }
477       if (modifiers(event.keyCode) == Modifiers.ARE_NOT_ALLOWED &&
478           hasModifier(event, true)) {
479         return;
480       }
482       var shortcutNode = this.capturingElement_;
483       var keystroke = keystrokeToString(event);
484       shortcutNode.textContent = keystroke;
485       event.target.classList.add('contains-chars');
486       this.currentKeyEvent_ = event;
488       if (validChar(event.keyCode)) {
489         var node = event.target;
490         while (node && !node.id)
491           node = node.parentElement;
493         this.oldValue_ = keystroke;  // Forget what the old value was.
494         var parsed = this.parseElementId_('command', node.id);
496         // Ending the capture must occur before calling
497         // setExtensionCommandShortcut to ensure the shortcut is set.
498         this.endCapture_(event);
499         chrome.send('setExtensionCommandShortcut',
500                     [parsed.extensionId, parsed.commandName, keystroke]);
501       }
502     },
504     /**
505      * A handler for the delete command button.
506      * @param {Event} event The mouse event to consider.
507      * @private
508      */
509     handleClear_: function(event) {
510       var parsed = this.parseElementId_('clear', event.target.id);
511       chrome.send('setExtensionCommandShortcut',
512           [parsed.extensionId, parsed.commandName, '']);
513     },
515     /**
516      * A handler for the setting the scope of the command.
517      * @param {Event} event The mouse event to consider.
518      * @private
519      */
520     handleSetCommandScope_: function(event) {
521       var parsed = this.parseElementId_('setCommandScope', event.target.id);
522       var element = document.getElementById(
523           'setCommandScope-' + parsed.extensionId + '-' + parsed.commandName);
524       chrome.send('setCommandScope',
525           [parsed.extensionId, parsed.commandName, element.selectedIndex == 1]);
526     },
528     /**
529      * A utility function to create a unique element id based on a namespace,
530      * extension id and a command name.
531      * @param {string} namespace   The namespace to prepend the id with.
532      * @param {string} extensionId The extension ID to use in the id.
533      * @param {string} commandName The command name to append the id with.
534      * @private
535      */
536     createElementId_: function(namespace, extensionId, commandName) {
537       return namespace + '-' + extensionId + '-' + commandName;
538     },
540     /**
541      * A utility function to parse a unique element id based on a namespace,
542      * extension id and a command name.
543      * @param {string} namespace   The namespace to prepend the id with.
544      * @param {string} id          The id to parse.
545      * @return {{extensionId: string, commandName: string}} The parsed id.
546      * @private
547      */
548     parseElementId_: function(namespace, id) {
549       var kExtensionIdLength = 32;
550       return {
551         extensionId: id.substring(namespace.length + 1,
552                                   namespace.length + 1 + kExtensionIdLength),
553         commandName: id.substring(namespace.length + 1 + kExtensionIdLength + 1)
554       };
555     },
556   };
558   return {
559     ExtensionCommandList: ExtensionCommandList
560   };