1 // Copyright 2014 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.
6 * The repeat delay in milliseconds before a key starts repeating. Use the
7 * same rate as Chromebook.
8 * (See chrome/browser/chromeos/language_preferences.cc)
12 var REPEAT_DELAY_MSEC = 500;
15 * The repeat interval or number of milliseconds between subsequent
16 * keypresses. Use the same rate as Chromebook.
20 var REPEAT_INTERVAL_MSEC = 50;
23 * The double click/tap interval.
27 var DBL_INTERVAL_MSEC = 300;
30 * The index of the name of the keyset when searching for all keysets.
34 var REGEX_KEYSET_INDEX = 1;
37 * The integer number of matches when searching for keysets.
41 var REGEX_MATCH_COUNT = 2;
44 * The boolean to decide if keyboard should transit to upper case keyset
45 * when spacebar is pressed. If a closing punctuation is followed by a
46 * spacebar, keyboard should automatically transit to upper case.
49 var enterUpperOnSpace = false;
52 * A structure to track the currently repeating key on the keyboard.
57 * The timer for the delay before repeating behaviour begins.
58 * @type {number|undefined}
63 * The interval timer for issuing keypresses of a repeating key.
64 * @type {number|undefined}
69 * The key which is currently repeating.
70 * @type {BaseKey|undefined}
75 * Cancel the repeat timers of the currently active key.
78 clearTimeout(this.timer);
79 clearInterval(this.interval);
80 this.timer = undefined;
81 this.interval = undefined;
87 * The minimum movement interval needed to trigger cursor move on
88 * horizontal and vertical way.
92 var MIN_SWIPE_DIST_X = 50;
93 var MIN_SWIPE_DIST_Y = 20;
96 * The maximum swipe distance that will trigger hintText of a key
101 var MAX_SWIPE_FLICK_DIST = 60;
104 * The boolean to decide if it is swipe in process or finished.
107 var swipeInProgress = false;
109 // Flag values for ctrl, alt and shift as defined by EventFlags
110 // in "event_constants.h".
120 * A structure to track the current swipe status.
124 * The latest PointerMove event in the swipe.
127 currentEvent: undefined,
130 * Whether or not a swipe changes direction.
136 * The count of horizontal and vertical movement.
143 * Last touch coordinate.
150 * The PointerMove event which triggered the swipe.
153 startEvent: undefined,
156 * The flag of current modifier key.
162 * Current swipe direction.
168 * The number of times we've swiped within a single swipe.
174 * Returns the combined direction of the x and y offsets.
175 * @return {number} The latest direction.
177 getOffsetDirection: function() {
178 // TODO (rsadam): Use angles to figure out the direction.
180 // Checks for horizontal swipe.
181 if (Math.abs(this.offset_x) > MIN_SWIPE_DIST_X) {
182 if (this.offset_x > 0) {
183 direction |= SwipeDirection.RIGHT;
185 direction |= SwipeDirection.LEFT;
188 // Checks for vertical swipe.
189 if (Math.abs(this.offset_y) > MIN_SWIPE_DIST_Y) {
190 if (this.offset_y < 0) {
191 direction |= SwipeDirection.UP;
193 direction |= SwipeDirection.DOWN;
200 * Populates the swipe update details.
201 * @param {boolean} endSwipe Whether this is the final event for this
203 * @return {Object} The current state of the swipeTracker.
205 populateDetails: function(endSwipe) {
207 detail.direction = this.swipeDirection;
208 detail.index = this.swipeIndex;
209 detail.status = this.swipeStatus;
210 detail.endSwipe = endSwipe;
211 detail.startEvent = this.startEvent;
212 detail.currentEvent = this.currentEvent;
213 detail.isComplex = this.isComplex;
218 * Reset all the values when swipe finished.
220 resetAll: function() {
226 this.swipeDirection = 0;
228 this.startEvent = undefined;
229 this.currentEvent = undefined;
230 this.isComplex = false;
234 * Updates the swipe path with the current event.
235 * @param {Object} event The PointerEvent that triggered this update.
236 * @return {boolean} Whether or not to notify swipe observers.
238 update: function(event) {
242 this.offset_x += event.screenX - this.pre_x;
243 this.offset_y += event.screenY - this.pre_y;
244 this.pre_x = event.screenX;
245 this.pre_y = event.screenY;
247 // Check if movement crosses minimum thresholds in each direction.
248 var direction = this.getOffsetDirection();
251 // If swipeIndex is zero the current event is triggering the swipe.
252 if (this.swipeIndex == 0) {
253 this.startEvent = event;
254 } else if (direction != this.swipeDirection) {
255 // Toggle the isComplex flag.
256 this.isComplex = true;
258 // Update the swipe tracker.
259 this.swipeDirection = direction;
262 this.currentEvent = event;
269 Polymer('kb-keyboard', {
276 lastPressedKey: null,
282 //TODO(rsadam@): Add a control to let users change this.
283 volume: DEFAULT_VOLUME,
286 * The default input type to keyboard layout map. The key must be one of
287 * the input box type values.
290 inputTypeToLayoutMap: {
297 * Caches the specified sound on the keyboard.
298 * @param {string} soundId The name of the .wav file in the "sounds"
301 addSound: function(soundId) {
302 // Check if already loaded.
303 if (soundId == Sound.NONE || this.sounds[soundId])
306 for (var i = 0; i < SOUND_POOL_SIZE; i++) {
307 var audio = document.createElement('audio');
308 audio.preload = "auto";
310 audio.src = "../sounds/" + soundId + ".wav";
311 audio.volume = this.volume;
314 this.sounds[soundId] = pool;
318 * Changes the current keyset.
319 * @param {Object} detail The detail of the event that called this
322 changeKeyset: function(detail) {
323 if (detail.relegateToShift && this.shift) {
324 this.keyset = this.shift.textKeyset;
325 this.activeKeyset.nextKeyset = undefined;
328 var toKeyset = detail.toKeyset;
330 this.keyset = toKeyset;
331 this.activeKeyset.nextKeyset = detail.nextKeyset;
337 keysetChanged: function() {
338 var keyset = this.activeKeyset;
339 // Show the keyset if it has been initialized.
344 configChanged: function() {
345 this.layout = this.config.layout;
349 this.voiceInput_ = new VoiceInput(this);
350 this.swipeHandler = this.move.bind(this);
352 getKeyboardConfig(function(config) {
353 self.config = config;
358 * Registers a callback for state change events.
359 * @param{!Function} callback Callback function to register.
361 addKeysetChangedObserver: function(callback) {
362 this.addEventListener('stateChange', callback);
366 * Called when the type of focused input box changes. If a keyboard layout
367 * is defined for the current input type, that layout will be loaded.
368 * Otherwise, the keyboard layout for 'text' type will be loaded.
370 inputTypeChanged: function() {
371 // Disable layout switching at accessbility mode.
372 if (this.config && this.config.a11ymode)
375 // TODO(bshe): Toggle visibility of some keys in a keyboard layout
376 // according to the input type.
377 var layout = this.inputTypeToLayoutMap[this.inputType];
379 layout = this.inputTypeToLayoutMap.text;
380 this.layout = layout;
384 * When double click/tap event is enabled, the second key-down and key-up
385 * events on the same key should be skipped. Return true when the event
386 * with |detail| should be skipped.
387 * @param {Object} detail The detail of key-up or key-down event.
389 skipEvent: function(detail) {
390 if (this.dblDetail_) {
391 if (this.dblDetail_.char != detail.char) {
392 // The second key down is not on the same key. Double click/tap
393 // should be ignored.
394 this.dblDetail_ = null;
395 clearTimeout(this.dblTimer_);
396 } else if (this.dblDetail_.clickCount == 1) {
404 * Handles a swipe update.
405 * param {Object} detail The swipe update details.
407 onSwipeUpdate: function(detail) {
408 var direction = detail.direction;
410 console.error("Swipe direction cannot be: " + direction);
411 // Triggers swipe editting if it's a purely horizontal swipe.
412 if (!(direction & (SwipeDirection.UP | SwipeDirection.DOWN))) {
413 // Nothing to do if the swipe has ended.
417 // TODO (rsadam): This doesn't take into account index shifts caused
418 // by vertical swipes.
419 if (detail.index % 2 != 0) {
420 modifiers |= Modifier.SHIFT;
421 modifiers |= Modifier.CONTROL;
423 MoveCursor(direction, modifiers);
426 // Triggers swipe hintText if it's a purely vertical swipe.
427 if (!(direction & (SwipeDirection.LEFT | SwipeDirection.RIGHT))) {
428 // Check if event is relevant to us.
429 if ((!detail.endSwipe) || (detail.isComplex))
432 var distance = Math.abs(detail.startEvent.screenY -
433 detail.currentEvent.screenY);
434 if (distance > MAX_SWIPE_FLICK_DIST)
436 var triggerKey = detail.startEvent.target;
437 if (triggerKey && triggerKey.onFlick)
438 triggerKey.onFlick(detail);
443 * This function is bound to swipeHandler. Updates the current swipe
444 * status so that PointerEvents can be converted to Swipe events.
445 * @param {PointerEvent} event.
447 move: function(event) {
448 if (!swipeTracker.update(event))
450 // Conversion was successful, swipe is now in progress.
451 swipeInProgress = true;
452 if (this.lastPressedKey) {
453 this.lastPressedKey.classList.remove('active');
454 this.lastPressedKey = null;
456 this.onSwipeUpdate(swipeTracker.populateDetails(false));
460 * Handles key-down event that is sent by kb-key-base.
461 * @param {CustomEvent} event The key-down event dispatched by
463 * @param {Object} detail The detail of pressed kb-key.
465 keyDown: function(event, detail) {
466 if (this.skipEvent(detail))
469 if (this.lastPressedKey) {
470 this.lastPressedKey.classList.remove('active');
471 this.lastPressedKey.autoRelease();
473 this.lastPressedKey = event.target;
474 this.lastPressedKey.classList.add('active');
476 this.playSound(detail.sound);
478 var char = detail.char;
481 this.classList.remove('caps-locked');
485 var modifier = char.toLowerCase() + "-active";
486 // Removes modifier if already active.
487 if (this.classList.contains(modifier))
488 this.classList.remove(modifier);
491 // Not all Invalid keys are transition keys. Reset control keys if
492 // we pressed a transition key.
493 if (event.target.toKeyset || detail.relegateToShift)
494 this.onNonControlKeyTyped();
499 this.shift.onNonControlKeyDown();
501 this.ctrl.onNonControlKeyDown();
503 this.alt.onNonControlKeyDown();
506 if(this.changeKeyset(detail))
509 this.keyTyped(detail);
510 this.onNonControlKeyTyped();
511 repeatKey.key = this.lastPressedKey;
513 repeatKey.timer = setTimeout(function() {
514 repeatKey.timer = undefined;
515 repeatKey.interval = setInterval(function() {
516 self.playSound(detail.sound);
517 self.keyTyped(detail);
518 }, REPEAT_INTERVAL_MSEC);
519 }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC));
524 * Handles key-out event that is sent by kb-shift-key.
525 * @param {CustomEvent} event The key-out event dispatched by
527 * @param {Object} detail The detail of pressed kb-shift-key.
529 keyOut: function(event, detail) {
530 this.changeKeyset(detail);
534 * Enable/start double click/tap event recognition.
535 * @param {CustomEvent} event The enable-dbl event dispatched by
537 * @param {Object} detail The detail of pressed kb-shift-key.
539 enableDbl: function(event, detail) {
540 if (!this.dblDetail_) {
541 this.dblDetail_ = detail;
542 this.dblDetail_.clickCount = 0;
544 this.dblTimer_ = setTimeout(function() {
545 self.dblDetail_.callback = null;
546 self.dblDetail_ = null;
547 }, DBL_INTERVAL_MSEC);
552 * Enable the selection while swipe.
553 * @param {CustomEvent} event The enable-dbl event dispatched by
556 enableSel: function(event) {
557 // TODO(rsadam): Disabled for now. May come back if we revert swipe
558 // selection to not do word selection.
562 * Handles pointerdown event. This is used for swipe selection process.
563 * to get the start pre_x and pre_y. And also add a pointermove handler
564 * to start handling the swipe selection event.
565 * @param {PointerEvent} event The pointerup event that received by
568 down: function(event) {
569 var layout = getKeysetLayout(this.activeKeysetId);
570 var key = layout.findClosestKey(event.clientX, event.clientY);
573 if (event.isPrimary) {
574 swipeTracker.pre_x = event.screenX;
575 swipeTracker.pre_y = event.screenY;
576 this.addEventListener("pointermove", this.swipeHandler, false);
581 * Handles pointerup event. This is used for double tap/click events.
582 * @param {PointerEvent} event The pointerup event that bubbled to
585 up: function(event) {
586 var layout = getKeysetLayout(this.activeKeysetId);
587 var key = layout.findClosestKey(event.clientX, event.clientY);
590 // When touch typing, it is very possible that finger moves slightly out
591 // of the key area before releases. The key should not be dropped in
593 // TODO(rsadam@) Change behaviour such that the key drops and the second
595 if (this.lastPressedKey &&
596 this.lastPressedKey.pointerId == event.pointerId) {
597 this.lastPressedKey.autoRelease();
600 if (this.dblDetail_) {
601 this.dblDetail_.clickCount++;
602 if (this.dblDetail_.clickCount == 2) {
603 this.dblDetail_.callback();
604 this.changeKeyset(this.dblDetail_);
605 clearTimeout(this.dblTimer_);
607 this.classList.add('caps-locked');
609 this.dblDetail_ = null;
613 // TODO(zyaozhujun): There are some edge cases to deal with later.
614 // (for instance, what if a second finger trigger a down and up
615 // event sequence while swiping).
616 // When pointer up from the screen, a swipe selection session finished,
617 // all the data should be reset to prepare for the next session.
618 if (event.isPrimary && swipeInProgress) {
619 swipeInProgress = false;
620 this.onSwipeUpdate(swipeTracker.populateDetails(true))
621 swipeTracker.resetAll();
623 this.removeEventListener('pointermove', this.swipeHandler, false);
627 * Handles PointerOut event. This is used for when a swipe gesture goes
628 * outside of the keyboard window.
629 * @param {Object} event The pointerout event that bubbled to the
632 out: function(event) {
633 // Ignore if triggered from one of the keys.
634 if (this.compareDocumentPosition(event.relatedTarget) &
635 Node.DOCUMENT_POSITION_CONTAINED_BY)
638 this.onSwipeUpdate(swipeTracker.populateDetails(true))
639 // Touched outside of the keyboard area, so disables swipe.
640 swipeInProgress = false;
641 swipeTracker.resetAll();
642 this.removeEventListener('pointermove', this.swipeHandler, false);
646 * Handles a TypeKey event. This is used for when we programmatically
647 * want to type a specific key.
648 * @param {CustomEvent} event The TypeKey event that bubbled to the
651 type: function(event) {
652 this.keyTyped(event.detail);
656 * Handles key-up event that is sent by kb-key-base.
657 * @param {CustomEvent} event The key-up event dispatched by kb-key-base.
658 * @param {Object} detail The detail of pressed kb-key.
660 keyUp: function(event, detail) {
661 if (this.skipEvent(detail))
665 if (detail.activeModifier) {
666 var modifier = detail.activeModifier.toLowerCase() + "-active";
667 this.classList.add(modifier);
669 // Adds the current keyboard modifiers to the detail.
671 detail.controlModifier = this.ctrl.isActive();
673 detail.altModifier = this.alt.isActive();
674 if (this.lastPressedKey)
675 this.lastPressedKey.classList.remove('active');
676 // Keyset transition key. This is needed to transition from upper
677 // to lower case when we are not in caps mode, as well as when
678 // we're ending chording.
679 this.changeKeyset(detail);
681 if (this.lastPressedKey &&
682 this.lastPressedKey.charValue != event.target.charValue) {
685 if (repeatKey.key == event.target) {
687 this.lastPressedKey = null;
690 var toLayoutId = detail.toLayout;
691 // Layout transition key.
693 this.layout = toLayoutId;
694 var char = detail.char;
695 this.lastPressedKey = null;
696 // Characters that should not be typed.
702 enterUpperOnSpace = false;
703 swipeTracker.swipeFlags = 0;
706 this.voiceInput_.onDown();
711 // Tries to type the character. Resorts to insertText if that fails.
712 if(!this.keyTyped(detail))
714 // Post-typing logic.
718 if(enterUpperOnSpace) {
719 enterUpperOnSpace = false;
721 var shiftDetail = this.shift.onSpaceAfterPunctuation();
722 // Check if transition defined.
723 this.changeKeyset(shiftDetail);
725 console.error('Capitalization on space after punctuation \
726 enabled, but cannot find target keyset.');
728 // Immediately return to maintain shift-state. Space is a
729 // non-control key and would otherwise trigger a reset of the
730 // shift key, causing a transition to lower case.
737 enterUpperOnSpace = this.shouldUpperOnSpace();
740 enterUpperOnSpace = false;
743 // Reset control keys.
744 this.onNonControlKeyTyped();
748 * Handles key-longpress event that is sent by kb-key-base.
749 * @param {CustomEvent} event The key-longpress event dispatched by
751 * @param {Object} detail The detail of pressed key.
753 keyLongpress: function(event, detail) {
754 // If the gesture is long press, remove the pointermove listener.
755 this.removeEventListener('pointermove', this.swipeHandler, false);
756 // Keyset transtion key.
757 if (this.changeKeyset(detail)) {
758 // Locks the keyset before removing active to prevent flicker.
759 this.classList.add('caps-locked');
760 // Makes last pressed key inactive if transit to a new keyset on long
762 if (this.lastPressedKey)
763 this.lastPressedKey.classList.remove('active');
768 * Plays the specified sound.
769 * @param {Sound} sound The id of the audio tag.
771 playSound: function(sound) {
772 if (!sound || sound == Sound.NONE)
774 var pool = this.sounds[sound];
776 console.error("Cannot find audio tag: " + sound);
779 // Search the sound pool for a free resource.
780 for (var i = 0; i < pool.length; i++) {
781 if (pool[i].paused) {
789 * Whether we should transit to upper case when seeing a space after
793 shouldUpperOnSpace: function() {
794 // TODO(rsadam): Add other input types in which we should not
795 // transition to upper after a space.
796 return this.inputTypeValue != 'password';
800 * Handler for the 'set-layout' event.
801 * @param {!Event} event The triggering event.
802 * @param {{layout: string}} details Details of the event, which contains
803 * the name of the layout to activate.
805 setLayout: function(event, details) {
806 this.layout = details.layout;
810 * Handles a change in the keyboard layout. Auto-selects the default
811 * keyset for the new layout.
813 layoutChanged: function() {
815 if (!this.selectDefaultKeyset()) {
816 console.error('No default keyset found for layout: ' + this.layout);
819 this.activeKeyset.show();
823 * Notifies the modifier keys that a non-control key was typed. This
824 * lets them reset sticky behaviour. A non-control key is defined as
825 * any key that is not Control, Alt, or Shift.
827 onNonControlKeyTyped: function() {
829 this.shift.onNonControlKeyTyped();
831 this.ctrl.onNonControlKeyTyped();
833 this.alt.onNonControlKeyTyped();
834 this.classList.remove('ctrl-active');
835 this.classList.remove('alt-active');
839 * Callback function for when volume is changed.
841 volumeChanged: function() {
842 var toChange = Object.keys(this.sounds);
843 for (var i = 0; i < toChange.length; i++) {
844 var pool = this.sounds[toChange[i]];
845 for (var j = 0; j < pool.length; j++) {
846 pool[j].volume = this.volume;
852 * Id for the active keyset.
855 get activeKeysetId() {
856 return this.layout + '-' + this.keyset;
860 * The active keyset DOM object.
864 return this.querySelector('#' + this.activeKeysetId);
868 * The current input type.
871 get inputTypeValue() {
872 return this.inputType;
876 * Changes the input type if it's different from the current
877 * type, else resets the keyset to the default keyset.
880 set inputTypeValue(value) {
881 if (value == this.inputType)
882 this.selectDefaultKeyset();
884 this.inputType = value;
888 * The keyboard is ready for input once the target keyset appears
889 * in the distributed nodes for the keyboard.
890 * @return {boolean} Indicates if the keyboard is ready for input.
892 isReady: function() {
893 var keyset = this.activeKeyset;
896 var nodes = this.$.content.getDistributedNodes();
897 for (var i = 0; i < nodes.length; i++) {
898 if (nodes[i].id && nodes[i].id == keyset.id)
905 * Generates fabricated key events to simulate typing on a
907 * @param {Object} detail Attributes of the key being typed.
908 * @return {boolean} Whether the key type succeeded.
910 keyTyped: function(detail) {
911 var builder = this.$.keyCodeMetadata;
913 detail.controlModifier = this.ctrl.isActive();
915 detail.altModifier = this.alt.isActive();
916 var downEvent = builder.createVirtualKeyEvent(detail, "keydown");
918 sendKeyEvent(downEvent);
919 sendKeyEvent(builder.createVirtualKeyEvent(detail, "keyup"));
926 * Selects the default keyset for a layout.
927 * @return {boolean} True if successful. This method can fail if the
928 * keysets corresponding to the layout have not been injected.
930 selectDefaultKeyset: function() {
931 var keysets = this.querySelectorAll('kb-keyset');
932 // Full name of the keyset is of the form 'layout-keyset'.
933 var regex = new RegExp('^' + this.layout + '-(.+)');
934 var keysetsLoaded = false;
935 for (var i = 0; i < keysets.length; i++) {
936 var matches = keysets[i].id.match(regex);
937 if (matches && matches.length == REGEX_MATCH_COUNT) {
938 keysetsLoaded = true;
939 // Without both tests for a default keyset, it is possible to get
940 // into a state where multiple layouts are displayed. A
941 // reproducable test case is do the following set of keyset
942 // transitions: qwerty -> system -> dvorak -> qwerty.
943 // TODO(kevers): Investigate why this is the case.
944 if (keysets[i].isDefault ||
945 keysets[i].getAttribute('isDefault') == 'true') {
946 this.keyset = matches[REGEX_KEYSET_INDEX];
947 this.classList.remove('caps-locked');
948 this.classList.remove('alt-active');
949 this.classList.remove('ctrl-active');
951 this.shift = this.querySelector('kb-shift-key');
954 // Caches control key.
955 this.ctrl = this.querySelector('kb-modifier-key[char=Ctrl]');
959 this.alt = this.querySelector('kb-modifier-key[char=Alt]');
962 this.fire('stateChange', {
963 state: 'keysetLoaded',
972 console.error('No default keyset found for ' + this.layout);