summaryrefslogtreecommitdiffstats
path: root/chromium/ui/keyboard/resources/elements/kb-keyboard.js
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/ui/keyboard/resources/elements/kb-keyboard.js')
-rw-r--r--chromium/ui/keyboard/resources/elements/kb-keyboard.js977
1 files changed, 977 insertions, 0 deletions
diff --git a/chromium/ui/keyboard/resources/elements/kb-keyboard.js b/chromium/ui/keyboard/resources/elements/kb-keyboard.js
new file mode 100644
index 00000000000..5933271ef26
--- /dev/null
+++ b/chromium/ui/keyboard/resources/elements/kb-keyboard.js
@@ -0,0 +1,977 @@
+// Copyright 2014 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.
+
+/**
+ * The repeat delay in milliseconds before a key starts repeating. Use the
+ * same rate as Chromebook.
+ * (See chrome/browser/chromeos/language_preferences.cc)
+ * @const
+ * @type {number}
+ */
+var REPEAT_DELAY_MSEC = 500;
+
+/**
+ * The repeat interval or number of milliseconds between subsequent
+ * keypresses. Use the same rate as Chromebook.
+ * @const
+ * @type {number}
+ */
+var REPEAT_INTERVAL_MSEC = 50;
+
+/**
+ * The double click/tap interval.
+ * @const
+ * @type {number}
+ */
+var DBL_INTERVAL_MSEC = 300;
+
+/**
+ * The index of the name of the keyset when searching for all keysets.
+ * @const
+ * @type {number}
+ */
+var REGEX_KEYSET_INDEX = 1;
+
+/**
+ * The integer number of matches when searching for keysets.
+ * @const
+ * @type {number}
+ */
+var REGEX_MATCH_COUNT = 2;
+
+/**
+ * The boolean to decide if keyboard should transit to upper case keyset
+ * when spacebar is pressed. If a closing punctuation is followed by a
+ * spacebar, keyboard should automatically transit to upper case.
+ * @type {boolean}
+ */
+var enterUpperOnSpace = false;
+
+/**
+ * A structure to track the currently repeating key on the keyboard.
+ */
+var repeatKey = {
+
+ /**
+ * The timer for the delay before repeating behaviour begins.
+ * @type {number|undefined}
+ */
+ timer: undefined,
+
+ /**
+ * The interval timer for issuing keypresses of a repeating key.
+ * @type {number|undefined}
+ */
+ interval: undefined,
+
+ /**
+ * The key which is currently repeating.
+ * @type {BaseKey|undefined}
+ */
+ key: undefined,
+
+ /**
+ * Cancel the repeat timers of the currently active key.
+ */
+ cancel: function() {
+ clearTimeout(this.timer);
+ clearInterval(this.interval);
+ this.timer = undefined;
+ this.interval = undefined;
+ this.key = undefined;
+ }
+};
+
+/**
+ * The minimum movement interval needed to trigger cursor move on
+ * horizontal and vertical way.
+ * @const
+ * @type {number}
+ */
+var MIN_SWIPE_DIST_X = 50;
+var MIN_SWIPE_DIST_Y = 20;
+
+/**
+ * The maximum swipe distance that will trigger hintText of a key
+ * to be typed.
+ * @const
+ * @type {number}
+ */
+var MAX_SWIPE_FLICK_DIST = 60;
+
+/**
+ * The boolean to decide if it is swipe in process or finished.
+ * @type {boolean}
+ */
+var swipeInProgress = false;
+
+// Flag values for ctrl, alt and shift as defined by EventFlags
+// in "event_constants.h".
+// @enum {number}
+var Modifier = {
+ NONE: 0,
+ ALT: 8,
+ CONTROL: 4,
+ SHIFT: 2
+};
+
+/**
+ * A structure to track the current swipe status.
+ */
+var swipeTracker = {
+ /**
+ * The latest PointerMove event in the swipe.
+ * @type {Object}
+ */
+ currentEvent: undefined,
+
+ /**
+ * Whether or not a swipe changes direction.
+ * @type {false}
+ */
+ isComplex: false,
+
+ /**
+ * The count of horizontal and vertical movement.
+ * @type {number}
+ */
+ offset_x : 0,
+ offset_y : 0,
+
+ /**
+ * Last touch coordinate.
+ * @type {number}
+ */
+ pre_x : 0,
+ pre_y : 0,
+
+ /**
+ * The PointerMove event which triggered the swipe.
+ * @type {Object}
+ */
+ startEvent: undefined,
+
+ /**
+ * The flag of current modifier key.
+ * @type {number}
+ */
+ swipeFlags : 0,
+
+ /**
+ * Current swipe direction.
+ * @type {number}
+ */
+ swipeDirection : 0,
+
+ /**
+ * The number of times we've swiped within a single swipe.
+ * @type {number}
+ */
+ swipeIndex: 0,
+
+ /**
+ * Returns the combined direction of the x and y offsets.
+ * @return {number} The latest direction.
+ */
+ getOffsetDirection: function() {
+ // TODO (rsadam): Use angles to figure out the direction.
+ var direction = 0;
+ // Checks for horizontal swipe.
+ if (Math.abs(this.offset_x) > MIN_SWIPE_DIST_X) {
+ if (this.offset_x > 0) {
+ direction |= SwipeDirection.RIGHT;
+ } else {
+ direction |= SwipeDirection.LEFT;
+ }
+ }
+ // Checks for vertical swipe.
+ if (Math.abs(this.offset_y) > MIN_SWIPE_DIST_Y) {
+ if (this.offset_y < 0) {
+ direction |= SwipeDirection.UP;
+ } else {
+ direction |= SwipeDirection.DOWN;
+ }
+ }
+ return direction;
+ },
+
+ /**
+ * Populates the swipe update details.
+ * @param {boolean} endSwipe Whether this is the final event for this
+ * swipe.
+ * @return {Object} The current state of the swipeTracker.
+ */
+ populateDetails: function(endSwipe) {
+ var detail = {};
+ detail.direction = this.swipeDirection;
+ detail.index = this.swipeIndex;
+ detail.status = this.swipeStatus;
+ detail.endSwipe = endSwipe;
+ detail.startEvent = this.startEvent;
+ detail.currentEvent = this.currentEvent;
+ detail.isComplex = this.isComplex;
+ return detail;
+ },
+
+ /**
+ * Reset all the values when swipe finished.
+ */
+ resetAll: function() {
+ this.offset_x = 0;
+ this.offset_y = 0;
+ this.pre_x = 0;
+ this.pre_y = 0;
+ this.swipeFlags = 0;
+ this.swipeDirection = 0;
+ this.swipeIndex = 0;
+ this.startEvent = undefined;
+ this.currentEvent = undefined;
+ this.isComplex = false;
+ },
+
+ /**
+ * Updates the swipe path with the current event.
+ * @param {Object} event The PointerEvent that triggered this update.
+ * @return {boolean} Whether or not to notify swipe observers.
+ */
+ update: function(event) {
+ if(!event.isPrimary)
+ return false;
+ // Update priors.
+ this.offset_x += event.screenX - this.pre_x;
+ this.offset_y += event.screenY - this.pre_y;
+ this.pre_x = event.screenX;
+ this.pre_y = event.screenY;
+
+ // Check if movement crosses minimum thresholds in each direction.
+ var direction = this.getOffsetDirection();
+ if (direction == 0)
+ return false;
+ // If swipeIndex is zero the current event is triggering the swipe.
+ if (this.swipeIndex == 0) {
+ this.startEvent = event;
+ } else if (direction != this.swipeDirection) {
+ // Toggle the isComplex flag.
+ this.isComplex = true;
+ }
+ // Update the swipe tracker.
+ this.swipeDirection = direction;
+ this.offset_x = 0;
+ this.offset_y = 0;
+ this.currentEvent = event;
+ this.swipeIndex++;
+ return true;
+ },
+
+};
+
+Polymer('kb-keyboard', {
+ alt: null,
+ config: null,
+ control: null,
+ dblDetail_: null,
+ dblTimer_: null,
+ inputType: null,
+ lastPressedKey: null,
+ shift: null,
+ sounds: {},
+ stale: true,
+ swipeHandler: null,
+ voiceInput_: null,
+ //TODO(rsadam@): Add a control to let users change this.
+ volume: DEFAULT_VOLUME,
+
+ /**
+ * The default input type to keyboard layout map. The key must be one of
+ * the input box type values.
+ * @type {object}
+ */
+ inputTypeToLayoutMap: {
+ number: "numeric",
+ text: "qwerty",
+ password: "qwerty"
+ },
+
+ /**
+ * Caches the specified sound on the keyboard.
+ * @param {string} soundId The name of the .wav file in the "sounds"
+ directory.
+ */
+ addSound: function(soundId) {
+ // Check if already loaded.
+ if (soundId == Sound.NONE || this.sounds[soundId])
+ return;
+ var pool = [];
+ for (var i = 0; i < SOUND_POOL_SIZE; i++) {
+ var audio = document.createElement('audio');
+ audio.preload = "auto";
+ audio.id = soundId;
+ audio.src = "../sounds/" + soundId + ".wav";
+ audio.volume = this.volume;
+ pool.push(audio);
+ }
+ this.sounds[soundId] = pool;
+ },
+
+ /**
+ * Changes the current keyset.
+ * @param {Object} detail The detail of the event that called this
+ * function.
+ */
+ changeKeyset: function(detail) {
+ if (detail.relegateToShift && this.shift) {
+ this.keyset = this.shift.textKeyset;
+ this.activeKeyset.nextKeyset = undefined;
+ return true;
+ }
+ var toKeyset = detail.toKeyset;
+ if (toKeyset) {
+ this.keyset = toKeyset;
+ this.activeKeyset.nextKeyset = detail.nextKeyset;
+ return true;
+ }
+ return false;
+ },
+
+ keysetChanged: function() {
+ var keyset = this.activeKeyset;
+ // Show the keyset if it has been initialized.
+ if (keyset)
+ keyset.show();
+ },
+
+ configChanged: function() {
+ this.layout = this.config.layout;
+ },
+
+ ready: function() {
+ this.voiceInput_ = new VoiceInput(this);
+ this.swipeHandler = this.move.bind(this);
+ var self = this;
+ getKeyboardConfig(function(config) {
+ self.config = config;
+ });
+ },
+
+ /**
+ * Registers a callback for state change events.
+ * @param{!Function} callback Callback function to register.
+ */
+ addKeysetChangedObserver: function(callback) {
+ this.addEventListener('stateChange', callback);
+ },
+
+ /**
+ * Called when the type of focused input box changes. If a keyboard layout
+ * is defined for the current input type, that layout will be loaded.
+ * Otherwise, the keyboard layout for 'text' type will be loaded.
+ */
+ inputTypeChanged: function() {
+ // Disable layout switching at accessbility mode.
+ if (this.config && this.config.a11ymode)
+ return;
+
+ // TODO(bshe): Toggle visibility of some keys in a keyboard layout
+ // according to the input type.
+ var layout = this.inputTypeToLayoutMap[this.inputType];
+ if (!layout)
+ layout = this.inputTypeToLayoutMap.text;
+ this.layout = layout;
+ },
+
+ /**
+ * When double click/tap event is enabled, the second key-down and key-up
+ * events on the same key should be skipped. Return true when the event
+ * with |detail| should be skipped.
+ * @param {Object} detail The detail of key-up or key-down event.
+ */
+ skipEvent: function(detail) {
+ if (this.dblDetail_) {
+ if (this.dblDetail_.char != detail.char) {
+ // The second key down is not on the same key. Double click/tap
+ // should be ignored.
+ this.dblDetail_ = null;
+ clearTimeout(this.dblTimer_);
+ } else if (this.dblDetail_.clickCount == 1) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Handles a swipe update.
+ * param {Object} detail The swipe update details.
+ */
+ onSwipeUpdate: function(detail) {
+ var direction = detail.direction;
+ if (!direction)
+ console.error("Swipe direction cannot be: " + direction);
+ // Triggers swipe editting if it's a purely horizontal swipe.
+ if (!(direction & (SwipeDirection.UP | SwipeDirection.DOWN))) {
+ // Nothing to do if the swipe has ended.
+ if (detail.endSwipe)
+ return;
+ var modifiers = 0;
+ // TODO (rsadam): This doesn't take into account index shifts caused
+ // by vertical swipes.
+ if (detail.index % 2 != 0) {
+ modifiers |= Modifier.SHIFT;
+ modifiers |= Modifier.CONTROL;
+ }
+ MoveCursor(direction, modifiers);
+ return;
+ }
+ // Triggers swipe hintText if it's a purely vertical swipe.
+ if (this.activeKeyset.flick &&
+ !(direction & (SwipeDirection.LEFT | SwipeDirection.RIGHT))) {
+ // Check if event is relevant to us.
+ if ((!detail.endSwipe) || (detail.isComplex))
+ return;
+ // Too long a swipe.
+ var distance = Math.abs(detail.startEvent.screenY -
+ detail.currentEvent.screenY);
+ if (distance > MAX_SWIPE_FLICK_DIST)
+ return;
+ var triggerKey = detail.startEvent.target;
+ if (triggerKey && triggerKey.onFlick)
+ triggerKey.onFlick(detail);
+ }
+ },
+
+ /**
+ * This function is bound to swipeHandler. Updates the current swipe
+ * status so that PointerEvents can be converted to Swipe events.
+ * @param {PointerEvent} event.
+ */
+ move: function(event) {
+ if (!swipeTracker.update(event))
+ return;
+ // Conversion was successful, swipe is now in progress.
+ swipeInProgress = true;
+ if (this.lastPressedKey) {
+ this.lastPressedKey.classList.remove('active');
+ this.lastPressedKey = null;
+ }
+ this.onSwipeUpdate(swipeTracker.populateDetails(false));
+ },
+
+ /**
+ * Handles key-down event that is sent by kb-key-base.
+ * @param {CustomEvent} event The key-down event dispatched by
+ * kb-key-base.
+ * @param {Object} detail The detail of pressed kb-key.
+ */
+ keyDown: function(event, detail) {
+ if (this.skipEvent(detail))
+ return;
+
+ if (this.lastPressedKey) {
+ this.lastPressedKey.classList.remove('active');
+ this.lastPressedKey.autoRelease();
+ }
+ this.lastPressedKey = event.target;
+ this.lastPressedKey.classList.add('active');
+ repeatKey.cancel();
+ this.playSound(detail.sound);
+
+ var char = detail.char;
+ switch(char) {
+ case 'Shift':
+ this.classList.remove('caps-locked');
+ break;
+ case 'Alt':
+ case 'Ctrl':
+ var modifier = char.toLowerCase() + "-active";
+ // Removes modifier if already active.
+ if (this.classList.contains(modifier))
+ this.classList.remove(modifier);
+ break;
+ case 'Invalid':
+ // Not all Invalid keys are transition keys. Reset control keys if
+ // we pressed a transition key.
+ if (event.target.toKeyset || detail.relegateToShift)
+ this.onNonControlKeyTyped();
+ break;
+ default:
+ // Notify shift key.
+ if (this.shift)
+ this.shift.onNonControlKeyDown();
+ if (this.ctrl)
+ this.ctrl.onNonControlKeyDown();
+ if (this.alt)
+ this.alt.onNonControlKeyDown();
+ break;
+ }
+ if(this.changeKeyset(detail))
+ return;
+ if (detail.repeat) {
+ this.keyTyped(detail);
+ this.onNonControlKeyTyped();
+ repeatKey.key = this.lastPressedKey;
+ var self = this;
+ repeatKey.timer = setTimeout(function() {
+ repeatKey.timer = undefined;
+ repeatKey.interval = setInterval(function() {
+ self.playSound(detail.sound);
+ self.keyTyped(detail);
+ }, REPEAT_INTERVAL_MSEC);
+ }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC));
+ }
+ },
+
+ /**
+ * Handles key-out event that is sent by kb-shift-key.
+ * @param {CustomEvent} event The key-out event dispatched by
+ * kb-shift-key.
+ * @param {Object} detail The detail of pressed kb-shift-key.
+ */
+ keyOut: function(event, detail) {
+ this.changeKeyset(detail);
+ },
+
+ /**
+ * Enable/start double click/tap event recognition.
+ * @param {CustomEvent} event The enable-dbl event dispatched by
+ * kb-shift-key.
+ * @param {Object} detail The detail of pressed kb-shift-key.
+ */
+ enableDbl: function(event, detail) {
+ if (!this.dblDetail_) {
+ this.dblDetail_ = detail;
+ this.dblDetail_.clickCount = 0;
+ var self = this;
+ this.dblTimer_ = setTimeout(function() {
+ self.dblDetail_.callback = null;
+ self.dblDetail_ = null;
+ }, DBL_INTERVAL_MSEC);
+ }
+ },
+
+ /**
+ * Enable the selection while swipe.
+ * @param {CustomEvent} event The enable-dbl event dispatched by
+ * kb-shift-key.
+ */
+ enableSel: function(event) {
+ // TODO(rsadam): Disabled for now. May come back if we revert swipe
+ // selection to not do word selection.
+ },
+
+ /**
+ * Handles pointerdown event. This is used for swipe selection process.
+ * to get the start pre_x and pre_y. And also add a pointermove handler
+ * to start handling the swipe selection event.
+ * @param {PointerEvent} event The pointerup event that received by
+ * kb-keyboard.
+ */
+ down: function(event) {
+ var layout = getKeysetLayout(this.activeKeysetId);
+ var key = layout.findClosestKey(event.clientX, event.clientY);
+ if (key)
+ key.down(event);
+ if (event.isPrimary) {
+ swipeTracker.pre_x = event.screenX;
+ swipeTracker.pre_y = event.screenY;
+ this.addEventListener("pointermove", this.swipeHandler, false);
+ }
+ },
+
+ /**
+ * Handles pointerup event. This is used for double tap/click events.
+ * @param {PointerEvent} event The pointerup event that bubbled to
+ * kb-keyboard.
+ */
+ up: function(event) {
+ var layout = getKeysetLayout(this.activeKeysetId);
+ var key = layout.findClosestKey(event.clientX, event.clientY);
+ if (key)
+ key.up(event);
+ // When touch typing, it is very possible that finger moves slightly out
+ // of the key area before releases. The key should not be dropped in
+ // this case.
+ // TODO(rsadam@) Change behaviour such that the key drops and the second
+ // key gets pressed.
+ if (this.lastPressedKey &&
+ this.lastPressedKey.pointerId == event.pointerId) {
+ this.lastPressedKey.autoRelease();
+ }
+
+ if (this.dblDetail_) {
+ this.dblDetail_.clickCount++;
+ if (this.dblDetail_.clickCount == 2) {
+ this.dblDetail_.callback();
+ this.changeKeyset(this.dblDetail_);
+ clearTimeout(this.dblTimer_);
+
+ this.classList.add('caps-locked');
+
+ this.dblDetail_ = null;
+ }
+ }
+
+ // TODO(zyaozhujun): There are some edge cases to deal with later.
+ // (for instance, what if a second finger trigger a down and up
+ // event sequence while swiping).
+ // When pointer up from the screen, a swipe selection session finished,
+ // all the data should be reset to prepare for the next session.
+ if (event.isPrimary && swipeInProgress) {
+ swipeInProgress = false;
+ this.onSwipeUpdate(swipeTracker.populateDetails(true))
+ swipeTracker.resetAll();
+ }
+ this.removeEventListener('pointermove', this.swipeHandler, false);
+ },
+
+ /**
+ * Handles PointerOut event. This is used for when a swipe gesture goes
+ * outside of the keyboard window.
+ * @param {Object} event The pointerout event that bubbled to the
+ * kb-keyboard.
+ */
+ out: function(event) {
+ repeatKey.cancel();
+ // Ignore if triggered from one of the keys.
+ if (this.compareDocumentPosition(event.relatedTarget) &
+ Node.DOCUMENT_POSITION_CONTAINED_BY)
+ return;
+ if (swipeInProgress)
+ this.onSwipeUpdate(swipeTracker.populateDetails(true))
+ // Touched outside of the keyboard area, so disables swipe.
+ swipeInProgress = false;
+ swipeTracker.resetAll();
+ this.removeEventListener('pointermove', this.swipeHandler, false);
+ },
+
+ /**
+ * Handles a TypeKey event. This is used for when we programmatically
+ * want to type a specific key.
+ * @param {CustomEvent} event The TypeKey event that bubbled to the
+ * kb-keyboard.
+ */
+ type: function(event) {
+ this.keyTyped(event.detail);
+ },
+
+ /**
+ * Handles key-up event that is sent by kb-key-base.
+ * @param {CustomEvent} event The key-up event dispatched by kb-key-base.
+ * @param {Object} detail The detail of pressed kb-key.
+ */
+ keyUp: function(event, detail) {
+ if (this.skipEvent(detail))
+ return;
+ if (swipeInProgress)
+ return;
+ if (detail.activeModifier) {
+ var modifier = detail.activeModifier.toLowerCase() + "-active";
+ this.classList.add(modifier);
+ }
+ // Adds the current keyboard modifiers to the detail.
+ if (this.ctrl)
+ detail.controlModifier = this.ctrl.isActive();
+ if (this.alt)
+ detail.altModifier = this.alt.isActive();
+ if (this.lastPressedKey)
+ this.lastPressedKey.classList.remove('active');
+ // Keyset transition key. This is needed to transition from upper
+ // to lower case when we are not in caps mode, as well as when
+ // we're ending chording.
+ this.changeKeyset(detail);
+
+ if (this.lastPressedKey &&
+ this.lastPressedKey.charValue != event.target.charValue) {
+ return;
+ }
+ if (repeatKey.key == event.target) {
+ repeatKey.cancel();
+ this.lastPressedKey = null;
+ return;
+ }
+ var toLayoutId = detail.toLayout;
+ // Layout transition key.
+ if (toLayoutId)
+ this.layout = toLayoutId;
+ var char = detail.char;
+ this.lastPressedKey = null;
+ // Characters that should not be typed.
+ switch(char) {
+ case 'Invalid':
+ case 'Shift':
+ case 'Ctrl':
+ case 'Alt':
+ enterUpperOnSpace = false;
+ swipeTracker.swipeFlags = 0;
+ return;
+ case 'Microphone':
+ this.voiceInput_.onDown();
+ return;
+ default:
+ break;
+ }
+ // Tries to type the character. Resorts to insertText if that fails.
+ if(!this.keyTyped(detail))
+ insertText(char);
+ // Post-typing logic.
+ switch(char) {
+ case '\n':
+ case ' ':
+ if(enterUpperOnSpace) {
+ enterUpperOnSpace = false;
+ if (this.shift) {
+ var shiftDetail = this.shift.onSpaceAfterPunctuation();
+ // Check if transition defined.
+ this.changeKeyset(shiftDetail);
+ } else {
+ console.error('Capitalization on space after punctuation \
+ enabled, but cannot find target keyset.');
+ }
+ // Immediately return to maintain shift-state. Space is a
+ // non-control key and would otherwise trigger a reset of the
+ // shift key, causing a transition to lower case.
+ return;
+ }
+ break;
+ case '.':
+ case '?':
+ case '!':
+ enterUpperOnSpace = this.shouldUpperOnSpace();
+ break;
+ default:
+ enterUpperOnSpace = false;
+ break;
+ }
+ // Reset control keys.
+ this.onNonControlKeyTyped();
+ },
+
+ /*
+ * Handles key-longpress event that is sent by kb-key-base.
+ * @param {CustomEvent} event The key-longpress event dispatched by
+ * kb-key-base.
+ * @param {Object} detail The detail of pressed key.
+ */
+ keyLongpress: function(event, detail) {
+ // If the gesture is long press, remove the pointermove listener.
+ this.removeEventListener('pointermove', this.swipeHandler, false);
+ // Keyset transtion key.
+ if (this.changeKeyset(detail)) {
+ // Locks the keyset before removing active to prevent flicker.
+ this.classList.add('caps-locked');
+ // Makes last pressed key inactive if transit to a new keyset on long
+ // press.
+ if (this.lastPressedKey)
+ this.lastPressedKey.classList.remove('active');
+ }
+ },
+
+ /**
+ * Plays the specified sound.
+ * @param {Sound} sound The id of the audio tag.
+ */
+ playSound: function(sound) {
+ if (!SOUND_ENABLED || !sound || sound == Sound.NONE)
+ return;
+ var pool = this.sounds[sound];
+ if (!pool) {
+ console.error("Cannot find audio tag: " + sound);
+ return;
+ }
+ // Search the sound pool for a free resource.
+ for (var i = 0; i < pool.length; i++) {
+ if (pool[i].paused) {
+ pool[i].play();
+ return;
+ }
+ }
+ },
+
+ /**
+ * Whether we should transit to upper case when seeing a space after
+ * punctuation.
+ * @return {boolean}
+ */
+ shouldUpperOnSpace: function() {
+ // TODO(rsadam): Add other input types in which we should not
+ // transition to upper after a space.
+ return this.inputTypeValue != 'password';
+ },
+
+ /**
+ * Handler for the 'set-layout' event.
+ * @param {!Event} event The triggering event.
+ * @param {{layout: string}} details Details of the event, which contains
+ * the name of the layout to activate.
+ */
+ setLayout: function(event, details) {
+ this.layout = details.layout;
+ },
+
+ /**
+ * Handles a change in the keyboard layout. Auto-selects the default
+ * keyset for the new layout.
+ */
+ layoutChanged: function() {
+ this.stale = true;
+ if (!this.selectDefaultKeyset()) {
+ console.error('No default keyset found for layout: ' + this.layout);
+ return;
+ }
+ this.activeKeyset.show();
+ },
+
+ /**
+ * Notifies the modifier keys that a non-control key was typed. This
+ * lets them reset sticky behaviour. A non-control key is defined as
+ * any key that is not Control, Alt, or Shift.
+ */
+ onNonControlKeyTyped: function() {
+ if (this.shift)
+ this.shift.onNonControlKeyTyped();
+ if (this.ctrl)
+ this.ctrl.onNonControlKeyTyped();
+ if (this.alt)
+ this.alt.onNonControlKeyTyped();
+ this.classList.remove('ctrl-active');
+ this.classList.remove('alt-active');
+ },
+
+ /**
+ * Callback function for when volume is changed.
+ */
+ volumeChanged: function() {
+ var toChange = Object.keys(this.sounds);
+ for (var i = 0; i < toChange.length; i++) {
+ var pool = this.sounds[toChange[i]];
+ for (var j = 0; j < pool.length; j++) {
+ pool[j].volume = this.volume;
+ }
+ }
+ },
+
+ /**
+ * Id for the active keyset.
+ * @type {string}
+ */
+ get activeKeysetId() {
+ return this.layout + '-' + this.keyset;
+ },
+
+ /**
+ * The active keyset DOM object.
+ * @type {kb-keyset}
+ */
+ get activeKeyset() {
+ return this.querySelector('#' + this.activeKeysetId);
+ },
+
+ /**
+ * The current input type.
+ * @type {string}
+ */
+ get inputTypeValue() {
+ return this.inputType;
+ },
+
+ /**
+ * Changes the input type if it's different from the current
+ * type, else resets the keyset to the default keyset.
+ * @type {string}
+ */
+ set inputTypeValue(value) {
+ if (value == this.inputType)
+ this.selectDefaultKeyset();
+ else
+ this.inputType = value;
+ },
+
+ /**
+ * The keyboard is ready for input once the target keyset appears
+ * in the distributed nodes for the keyboard.
+ * @return {boolean} Indicates if the keyboard is ready for input.
+ */
+ isReady: function() {
+ var keyset = this.activeKeyset;
+ if (!keyset)
+ return false;
+ var nodes = this.$.content.getDistributedNodes();
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i].id && nodes[i].id == keyset.id)
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Generates fabricated key events to simulate typing on a
+ * physical keyboard.
+ * @param {Object} detail Attributes of the key being typed.
+ * @return {boolean} Whether the key type succeeded.
+ */
+ keyTyped: function(detail) {
+ var builder = this.$.keyCodeMetadata;
+ if (this.ctrl)
+ detail.controlModifier = this.ctrl.isActive();
+ if (this.alt)
+ detail.altModifier = this.alt.isActive();
+ var downEvent = builder.createVirtualKeyEvent(detail, "keydown");
+ if (downEvent) {
+ sendKeyEvent(downEvent);
+ sendKeyEvent(builder.createVirtualKeyEvent(detail, "keyup"));
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Selects the default keyset for a layout.
+ * @return {boolean} True if successful. This method can fail if the
+ * keysets corresponding to the layout have not been injected.
+ */
+ selectDefaultKeyset: function() {
+ var keysets = this.querySelectorAll('kb-keyset');
+ // Full name of the keyset is of the form 'layout-keyset'.
+ var regex = new RegExp('^' + this.layout + '-(.+)');
+ var keysetsLoaded = false;
+ for (var i = 0; i < keysets.length; i++) {
+ var matches = keysets[i].id.match(regex);
+ if (matches && matches.length == REGEX_MATCH_COUNT) {
+ keysetsLoaded = true;
+ // Without both tests for a default keyset, it is possible to get
+ // into a state where multiple layouts are displayed. A
+ // reproducable test case is do the following set of keyset
+ // transitions: qwerty -> system -> dvorak -> qwerty.
+ // TODO(kevers): Investigate why this is the case.
+ if (keysets[i].isDefault ||
+ keysets[i].getAttribute('isDefault') == 'true') {
+ this.keyset = matches[REGEX_KEYSET_INDEX];
+ this.classList.remove('caps-locked');
+ this.classList.remove('alt-active');
+ this.classList.remove('ctrl-active');
+ // Caches shift key.
+ this.shift = this.querySelector('kb-shift-key');
+ if (this.shift)
+ this.shift.reset();
+ // Caches control key.
+ this.ctrl = this.querySelector('kb-modifier-key[char=Ctrl]');
+ if (this.ctrl)
+ this.ctrl.reset();
+ // Caches alt key.
+ this.alt = this.querySelector('kb-modifier-key[char=Alt]');
+ if (this.alt)
+ this.alt.reset();
+ this.fire('stateChange', {
+ state: 'keysetLoaded',
+ value: this.keyset,
+ });
+ keyboardLoaded();
+ return true;
+ }
+ }
+ }
+ if (keysetsLoaded)
+ console.error('No default keyset found for ' + this.layout);
+ return false;
+ }
+});