| OLD | NEW |
| (Empty) |
| 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. | |
| 4 | |
| 5 /** | |
| 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) | |
| 9 * @const | |
| 10 * @type {number} | |
| 11 */ | |
| 12 var REPEAT_DELAY_MSEC = 500; | |
| 13 | |
| 14 /** | |
| 15 * The repeat interval or number of milliseconds between subsequent | |
| 16 * keypresses. Use the same rate as Chromebook. | |
| 17 * @const | |
| 18 * @type {number} | |
| 19 */ | |
| 20 var REPEAT_INTERVAL_MSEC = 50; | |
| 21 | |
| 22 /** | |
| 23 * The double click/tap interval. | |
| 24 * @const | |
| 25 * @type {number} | |
| 26 */ | |
| 27 var DBL_INTERVAL_MSEC = 300; | |
| 28 | |
| 29 /** | |
| 30 * The index of the name of the keyset when searching for all keysets. | |
| 31 * @const | |
| 32 * @type {number} | |
| 33 */ | |
| 34 var REGEX_KEYSET_INDEX = 1; | |
| 35 | |
| 36 /** | |
| 37 * The integer number of matches when searching for keysets. | |
| 38 * @const | |
| 39 * @type {number} | |
| 40 */ | |
| 41 var REGEX_MATCH_COUNT = 2; | |
| 42 | |
| 43 /** | |
| 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. | |
| 47 * @type {boolean} | |
| 48 */ | |
| 49 var enterUpperOnSpace = false; | |
| 50 | |
| 51 /** | |
| 52 * A structure to track the currently repeating key on the keyboard. | |
| 53 */ | |
| 54 var repeatKey = { | |
| 55 | |
| 56 /** | |
| 57 * The timer for the delay before repeating behaviour begins. | |
| 58 * @type {number|undefined} | |
| 59 */ | |
| 60 timer: undefined, | |
| 61 | |
| 62 /** | |
| 63 * The interval timer for issuing keypresses of a repeating key. | |
| 64 * @type {number|undefined} | |
| 65 */ | |
| 66 interval: undefined, | |
| 67 | |
| 68 /** | |
| 69 * The key which is currently repeating. | |
| 70 * @type {BaseKey|undefined} | |
| 71 */ | |
| 72 key: undefined, | |
| 73 | |
| 74 /** | |
| 75 * Cancel the repeat timers of the currently active key. | |
| 76 */ | |
| 77 cancel: function() { | |
| 78 clearTimeout(this.timer); | |
| 79 clearInterval(this.interval); | |
| 80 this.timer = undefined; | |
| 81 this.interval = undefined; | |
| 82 this.key = undefined; | |
| 83 } | |
| 84 }; | |
| 85 | |
| 86 /** | |
| 87 * The minimum movement interval needed to trigger cursor move on | |
| 88 * horizontal and vertical way. | |
| 89 * @const | |
| 90 * @type {number} | |
| 91 */ | |
| 92 var MIN_SWIPE_DIST_X = 50; | |
| 93 var MIN_SWIPE_DIST_Y = 20; | |
| 94 | |
| 95 /** | |
| 96 * The maximum swipe distance that will trigger hintText of a key | |
| 97 * to be typed. | |
| 98 * @const | |
| 99 * @type {number} | |
| 100 */ | |
| 101 var MAX_SWIPE_FLICK_DIST = 60; | |
| 102 | |
| 103 /** | |
| 104 * The boolean to decide if it is swipe in process or finished. | |
| 105 * @type {boolean} | |
| 106 */ | |
| 107 var swipeInProgress = false; | |
| 108 | |
| 109 // Flag values for ctrl, alt and shift as defined by EventFlags | |
| 110 // in "event_constants.h". | |
| 111 // @enum {number} | |
| 112 var Modifier = { | |
| 113 NONE: 0, | |
| 114 ALT: 8, | |
| 115 CONTROL: 4, | |
| 116 SHIFT: 2 | |
| 117 }; | |
| 118 | |
| 119 /** | |
| 120 * A structure to track the current swipe status. | |
| 121 */ | |
| 122 var swipeTracker = { | |
| 123 /** | |
| 124 * The latest PointerMove event in the swipe. | |
| 125 * @type {Object} | |
| 126 */ | |
| 127 currentEvent: undefined, | |
| 128 | |
| 129 /** | |
| 130 * Whether or not a swipe changes direction. | |
| 131 * @type {false} | |
| 132 */ | |
| 133 isComplex: false, | |
| 134 | |
| 135 /** | |
| 136 * The count of horizontal and vertical movement. | |
| 137 * @type {number} | |
| 138 */ | |
| 139 offset_x : 0, | |
| 140 offset_y : 0, | |
| 141 | |
| 142 /** | |
| 143 * Last touch coordinate. | |
| 144 * @type {number} | |
| 145 */ | |
| 146 pre_x : 0, | |
| 147 pre_y : 0, | |
| 148 | |
| 149 /** | |
| 150 * The PointerMove event which triggered the swipe. | |
| 151 * @type {Object} | |
| 152 */ | |
| 153 startEvent: undefined, | |
| 154 | |
| 155 /** | |
| 156 * The flag of current modifier key. | |
| 157 * @type {number} | |
| 158 */ | |
| 159 swipeFlags : 0, | |
| 160 | |
| 161 /** | |
| 162 * Current swipe direction. | |
| 163 * @type {number} | |
| 164 */ | |
| 165 swipeDirection : 0, | |
| 166 | |
| 167 /** | |
| 168 * The number of times we've swiped within a single swipe. | |
| 169 * @type {number} | |
| 170 */ | |
| 171 swipeIndex: 0, | |
| 172 | |
| 173 /** | |
| 174 * Returns the combined direction of the x and y offsets. | |
| 175 * @return {number} The latest direction. | |
| 176 */ | |
| 177 getOffsetDirection: function() { | |
| 178 // TODO (rsadam): Use angles to figure out the direction. | |
| 179 var direction = 0; | |
| 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; | |
| 184 } else { | |
| 185 direction |= SwipeDirection.LEFT; | |
| 186 } | |
| 187 } | |
| 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; | |
| 192 } else { | |
| 193 direction |= SwipeDirection.DOWN; | |
| 194 } | |
| 195 } | |
| 196 return direction; | |
| 197 }, | |
| 198 | |
| 199 /** | |
| 200 * Populates the swipe update details. | |
| 201 * @param {boolean} endSwipe Whether this is the final event for this | |
| 202 * swipe. | |
| 203 * @return {Object} The current state of the swipeTracker. | |
| 204 */ | |
| 205 populateDetails: function(endSwipe) { | |
| 206 var detail = {}; | |
| 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; | |
| 214 return detail; | |
| 215 }, | |
| 216 | |
| 217 /** | |
| 218 * Reset all the values when swipe finished. | |
| 219 */ | |
| 220 resetAll: function() { | |
| 221 this.offset_x = 0; | |
| 222 this.offset_y = 0; | |
| 223 this.pre_x = 0; | |
| 224 this.pre_y = 0; | |
| 225 this.swipeFlags = 0; | |
| 226 this.swipeDirection = 0; | |
| 227 this.swipeIndex = 0; | |
| 228 this.startEvent = undefined; | |
| 229 this.currentEvent = undefined; | |
| 230 this.isComplex = false; | |
| 231 }, | |
| 232 | |
| 233 /** | |
| 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. | |
| 237 */ | |
| 238 update: function(event) { | |
| 239 if(!event.isPrimary) | |
| 240 return false; | |
| 241 // Update priors. | |
| 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; | |
| 246 | |
| 247 // Check if movement crosses minimum thresholds in each direction. | |
| 248 var direction = this.getOffsetDirection(); | |
| 249 if (direction == 0) | |
| 250 return false; | |
| 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; | |
| 257 } | |
| 258 // Update the swipe tracker. | |
| 259 this.swipeDirection = direction; | |
| 260 this.offset_x = 0; | |
| 261 this.offset_y = 0; | |
| 262 this.currentEvent = event; | |
| 263 this.swipeIndex++; | |
| 264 return true; | |
| 265 }, | |
| 266 | |
| 267 }; | |
| 268 | |
| 269 Polymer('kb-keyboard', { | |
| 270 alt: null, | |
| 271 config: null, | |
| 272 control: null, | |
| 273 dblDetail_: null, | |
| 274 dblTimer_: null, | |
| 275 inputType: null, | |
| 276 lastPressedKey: null, | |
| 277 shift: null, | |
| 278 sounds: {}, | |
| 279 stale: true, | |
| 280 swipeHandler: null, | |
| 281 voiceInput_: null, | |
| 282 //TODO(rsadam@): Add a control to let users change this. | |
| 283 volume: DEFAULT_VOLUME, | |
| 284 | |
| 285 /** | |
| 286 * The default input type to keyboard layout map. The key must be one of | |
| 287 * the input box type values. | |
| 288 * @type {object} | |
| 289 */ | |
| 290 inputTypeToLayoutMap: { | |
| 291 number: "numeric", | |
| 292 text: "qwerty", | |
| 293 password: "qwerty" | |
| 294 }, | |
| 295 | |
| 296 /** | |
| 297 * Caches the specified sound on the keyboard. | |
| 298 * @param {string} soundId The name of the .wav file in the "sounds" | |
| 299 directory. | |
| 300 */ | |
| 301 addSound: function(soundId) { | |
| 302 // Check if already loaded. | |
| 303 if (soundId == Sound.NONE || this.sounds[soundId]) | |
| 304 return; | |
| 305 var pool = []; | |
| 306 for (var i = 0; i < SOUND_POOL_SIZE; i++) { | |
| 307 var audio = document.createElement('audio'); | |
| 308 audio.preload = "auto"; | |
| 309 audio.id = soundId; | |
| 310 audio.src = "../sounds/" + soundId + ".wav"; | |
| 311 audio.volume = this.volume; | |
| 312 pool.push(audio); | |
| 313 } | |
| 314 this.sounds[soundId] = pool; | |
| 315 }, | |
| 316 | |
| 317 /** | |
| 318 * Changes the current keyset. | |
| 319 * @param {Object} detail The detail of the event that called this | |
| 320 * function. | |
| 321 */ | |
| 322 changeKeyset: function(detail) { | |
| 323 if (detail.relegateToShift && this.shift) { | |
| 324 this.keyset = this.shift.textKeyset; | |
| 325 this.activeKeyset.nextKeyset = undefined; | |
| 326 return true; | |
| 327 } | |
| 328 var toKeyset = detail.toKeyset; | |
| 329 if (toKeyset) { | |
| 330 this.keyset = toKeyset; | |
| 331 this.activeKeyset.nextKeyset = detail.nextKeyset; | |
| 332 return true; | |
| 333 } | |
| 334 return false; | |
| 335 }, | |
| 336 | |
| 337 keysetChanged: function() { | |
| 338 var keyset = this.activeKeyset; | |
| 339 // Show the keyset if it has been initialized. | |
| 340 if (keyset) | |
| 341 keyset.show(); | |
| 342 }, | |
| 343 | |
| 344 configChanged: function() { | |
| 345 this.layout = this.config.layout; | |
| 346 }, | |
| 347 | |
| 348 ready: function() { | |
| 349 this.voiceInput_ = new VoiceInput(this); | |
| 350 this.swipeHandler = this.move.bind(this); | |
| 351 var self = this; | |
| 352 getKeyboardConfig(function(config) { | |
| 353 self.config = config; | |
| 354 }); | |
| 355 }, | |
| 356 | |
| 357 /** | |
| 358 * Registers a callback for state change events. | |
| 359 * @param{!Function} callback Callback function to register. | |
| 360 */ | |
| 361 addKeysetChangedObserver: function(callback) { | |
| 362 this.addEventListener('stateChange', callback); | |
| 363 }, | |
| 364 | |
| 365 /** | |
| 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. | |
| 369 */ | |
| 370 inputTypeChanged: function() { | |
| 371 // Disable layout switching at accessibility mode. | |
| 372 if (this.config && this.config.a11ymode) | |
| 373 return; | |
| 374 | |
| 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]; | |
| 378 if (!layout) | |
| 379 layout = this.inputTypeToLayoutMap.text; | |
| 380 this.layout = layout; | |
| 381 }, | |
| 382 | |
| 383 /** | |
| 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. | |
| 388 */ | |
| 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) { | |
| 397 return true; | |
| 398 } | |
| 399 } | |
| 400 return false; | |
| 401 }, | |
| 402 | |
| 403 /** | |
| 404 * Handles a swipe update. | |
| 405 * param {Object} detail The swipe update details. | |
| 406 */ | |
| 407 onSwipeUpdate: function(detail) { | |
| 408 var direction = detail.direction; | |
| 409 if (!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. | |
| 414 if (detail.endSwipe) | |
| 415 return; | |
| 416 var modifiers = 0; | |
| 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; | |
| 422 } | |
| 423 MoveCursor(direction, modifiers); | |
| 424 return; | |
| 425 } | |
| 426 // Triggers swipe hintText if it's a purely vertical swipe. | |
| 427 if (this.activeKeyset.flick && | |
| 428 !(direction & (SwipeDirection.LEFT | SwipeDirection.RIGHT))) { | |
| 429 // Check if event is relevant to us. | |
| 430 if ((!detail.endSwipe) || (detail.isComplex)) | |
| 431 return; | |
| 432 // Too long a swipe. | |
| 433 var distance = Math.abs(detail.startEvent.screenY - | |
| 434 detail.currentEvent.screenY); | |
| 435 if (distance > MAX_SWIPE_FLICK_DIST) | |
| 436 return; | |
| 437 var triggerKey = detail.startEvent.target; | |
| 438 if (triggerKey && triggerKey.onFlick) | |
| 439 triggerKey.onFlick(detail); | |
| 440 } | |
| 441 }, | |
| 442 | |
| 443 /** | |
| 444 * This function is bound to swipeHandler. Updates the current swipe | |
| 445 * status so that PointerEvents can be converted to Swipe events. | |
| 446 * @param {PointerEvent} event. | |
| 447 */ | |
| 448 move: function(event) { | |
| 449 if (!swipeTracker.update(event)) | |
| 450 return; | |
| 451 // Conversion was successful, swipe is now in progress. | |
| 452 swipeInProgress = true; | |
| 453 if (this.lastPressedKey) { | |
| 454 this.lastPressedKey.classList.remove('active'); | |
| 455 this.lastPressedKey = null; | |
| 456 } | |
| 457 this.onSwipeUpdate(swipeTracker.populateDetails(false)); | |
| 458 }, | |
| 459 | |
| 460 /** | |
| 461 * Handles key-down event that is sent by kb-key-base. | |
| 462 * @param {CustomEvent} event The key-down event dispatched by | |
| 463 * kb-key-base. | |
| 464 * @param {Object} detail The detail of pressed kb-key. | |
| 465 */ | |
| 466 keyDown: function(event, detail) { | |
| 467 if (this.skipEvent(detail)) | |
| 468 return; | |
| 469 | |
| 470 if (this.lastPressedKey) { | |
| 471 this.lastPressedKey.classList.remove('active'); | |
| 472 this.lastPressedKey.autoRelease(); | |
| 473 } | |
| 474 this.lastPressedKey = event.target; | |
| 475 this.lastPressedKey.classList.add('active'); | |
| 476 repeatKey.cancel(); | |
| 477 this.playSound(detail.sound); | |
| 478 | |
| 479 var char = detail.char; | |
| 480 switch(char) { | |
| 481 case 'Shift': | |
| 482 this.classList.remove('caps-locked'); | |
| 483 break; | |
| 484 case 'Alt': | |
| 485 case 'Ctrl': | |
| 486 var modifier = char.toLowerCase() + "-active"; | |
| 487 // Removes modifier if already active. | |
| 488 if (this.classList.contains(modifier)) | |
| 489 this.classList.remove(modifier); | |
| 490 break; | |
| 491 case 'Invalid': | |
| 492 // Not all Invalid keys are transition keys. Reset control keys if | |
| 493 // we pressed a transition key. | |
| 494 if (event.target.toKeyset || detail.relegateToShift) | |
| 495 this.onNonControlKeyTyped(); | |
| 496 break; | |
| 497 default: | |
| 498 // Notify shift key. | |
| 499 if (this.shift) | |
| 500 this.shift.onNonControlKeyDown(); | |
| 501 if (this.ctrl) | |
| 502 this.ctrl.onNonControlKeyDown(); | |
| 503 if (this.alt) | |
| 504 this.alt.onNonControlKeyDown(); | |
| 505 break; | |
| 506 } | |
| 507 if(this.changeKeyset(detail)) | |
| 508 return; | |
| 509 if (detail.repeat) { | |
| 510 this.keyTyped(detail); | |
| 511 this.onNonControlKeyTyped(); | |
| 512 repeatKey.key = this.lastPressedKey; | |
| 513 var self = this; | |
| 514 repeatKey.timer = setTimeout(function() { | |
| 515 repeatKey.timer = undefined; | |
| 516 repeatKey.interval = setInterval(function() { | |
| 517 self.playSound(detail.sound); | |
| 518 self.keyTyped(detail); | |
| 519 }, REPEAT_INTERVAL_MSEC); | |
| 520 }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC)); | |
| 521 } | |
| 522 }, | |
| 523 | |
| 524 /** | |
| 525 * Handles key-out event that is sent by kb-shift-key. | |
| 526 * @param {CustomEvent} event The key-out event dispatched by | |
| 527 * kb-shift-key. | |
| 528 * @param {Object} detail The detail of pressed kb-shift-key. | |
| 529 */ | |
| 530 keyOut: function(event, detail) { | |
| 531 this.changeKeyset(detail); | |
| 532 }, | |
| 533 | |
| 534 /** | |
| 535 * Enable/start double click/tap event recognition. | |
| 536 * @param {CustomEvent} event The enable-dbl event dispatched by | |
| 537 * kb-shift-key. | |
| 538 * @param {Object} detail The detail of pressed kb-shift-key. | |
| 539 */ | |
| 540 enableDbl: function(event, detail) { | |
| 541 if (!this.dblDetail_) { | |
| 542 this.dblDetail_ = detail; | |
| 543 this.dblDetail_.clickCount = 0; | |
| 544 var self = this; | |
| 545 this.dblTimer_ = setTimeout(function() { | |
| 546 self.dblDetail_.callback = null; | |
| 547 self.dblDetail_ = null; | |
| 548 }, DBL_INTERVAL_MSEC); | |
| 549 } | |
| 550 }, | |
| 551 | |
| 552 /** | |
| 553 * Enable the selection while swipe. | |
| 554 * @param {CustomEvent} event The enable-dbl event dispatched by | |
| 555 * kb-shift-key. | |
| 556 */ | |
| 557 enableSel: function(event) { | |
| 558 // TODO(rsadam): Disabled for now. May come back if we revert swipe | |
| 559 // selection to not do word selection. | |
| 560 }, | |
| 561 | |
| 562 /** | |
| 563 * Handles pointerdown event. This is used for swipe selection process. | |
| 564 * to get the start pre_x and pre_y. And also add a pointermove handler | |
| 565 * to start handling the swipe selection event. | |
| 566 * @param {PointerEvent} event The pointerup event that received by | |
| 567 * kb-keyboard. | |
| 568 */ | |
| 569 down: function(event) { | |
| 570 var layout = getKeysetLayout(this.activeKeysetId); | |
| 571 var key = layout.findClosestKey(event.clientX, event.clientY); | |
| 572 if (key) | |
| 573 key.down(event); | |
| 574 if (event.isPrimary) { | |
| 575 swipeTracker.pre_x = event.screenX; | |
| 576 swipeTracker.pre_y = event.screenY; | |
| 577 this.addEventListener("pointermove", this.swipeHandler, false); | |
| 578 } | |
| 579 }, | |
| 580 | |
| 581 /** | |
| 582 * Handles pointerup event. This is used for double tap/click events. | |
| 583 * @param {PointerEvent} event The pointerup event that bubbled to | |
| 584 * kb-keyboard. | |
| 585 */ | |
| 586 up: function(event) { | |
| 587 var layout = getKeysetLayout(this.activeKeysetId); | |
| 588 var key = layout.findClosestKey(event.clientX, event.clientY); | |
| 589 if (key) | |
| 590 key.up(event); | |
| 591 // When touch typing, it is very possible that finger moves slightly out | |
| 592 // of the key area before releases. The key should not be dropped in | |
| 593 // this case. | |
| 594 // TODO(rsadam@) Change behaviour such that the key drops and the second | |
| 595 // key gets pressed. | |
| 596 if (this.lastPressedKey && | |
| 597 this.lastPressedKey.pointerId == event.pointerId) { | |
| 598 this.lastPressedKey.autoRelease(); | |
| 599 } | |
| 600 | |
| 601 if (this.dblDetail_) { | |
| 602 this.dblDetail_.clickCount++; | |
| 603 if (this.dblDetail_.clickCount == 2) { | |
| 604 this.dblDetail_.callback(); | |
| 605 this.changeKeyset(this.dblDetail_); | |
| 606 clearTimeout(this.dblTimer_); | |
| 607 | |
| 608 this.classList.add('caps-locked'); | |
| 609 | |
| 610 this.dblDetail_ = null; | |
| 611 } | |
| 612 } | |
| 613 | |
| 614 // TODO(zyaozhujun): There are some edge cases to deal with later. | |
| 615 // (for instance, what if a second finger trigger a down and up | |
| 616 // event sequence while swiping). | |
| 617 // When pointer up from the screen, a swipe selection session finished, | |
| 618 // all the data should be reset to prepare for the next session. | |
| 619 if (event.isPrimary && swipeInProgress) { | |
| 620 swipeInProgress = false; | |
| 621 this.onSwipeUpdate(swipeTracker.populateDetails(true)) | |
| 622 swipeTracker.resetAll(); | |
| 623 } | |
| 624 this.removeEventListener('pointermove', this.swipeHandler, false); | |
| 625 }, | |
| 626 | |
| 627 /** | |
| 628 * Handles PointerOut event. This is used for when a swipe gesture goes | |
| 629 * outside of the keyboard window. | |
| 630 * @param {Object} event The pointerout event that bubbled to the | |
| 631 * kb-keyboard. | |
| 632 */ | |
| 633 out: function(event) { | |
| 634 repeatKey.cancel(); | |
| 635 // Ignore if triggered from one of the keys. | |
| 636 if (this.compareDocumentPosition(event.relatedTarget) & | |
| 637 Node.DOCUMENT_POSITION_CONTAINED_BY) | |
| 638 return; | |
| 639 if (swipeInProgress) | |
| 640 this.onSwipeUpdate(swipeTracker.populateDetails(true)) | |
| 641 // Touched outside of the keyboard area, so disables swipe. | |
| 642 swipeInProgress = false; | |
| 643 swipeTracker.resetAll(); | |
| 644 this.removeEventListener('pointermove', this.swipeHandler, false); | |
| 645 }, | |
| 646 | |
| 647 /** | |
| 648 * Handles a TypeKey event. This is used for when we programmatically | |
| 649 * want to type a specific key. | |
| 650 * @param {CustomEvent} event The TypeKey event that bubbled to the | |
| 651 * kb-keyboard. | |
| 652 */ | |
| 653 type: function(event) { | |
| 654 this.keyTyped(event.detail); | |
| 655 }, | |
| 656 | |
| 657 /** | |
| 658 * Handles key-up event that is sent by kb-key-base. | |
| 659 * @param {CustomEvent} event The key-up event dispatched by kb-key-base. | |
| 660 * @param {Object} detail The detail of pressed kb-key. | |
| 661 */ | |
| 662 keyUp: function(event, detail) { | |
| 663 if (this.skipEvent(detail)) | |
| 664 return; | |
| 665 if (swipeInProgress) | |
| 666 return; | |
| 667 if (detail.activeModifier) { | |
| 668 var modifier = detail.activeModifier.toLowerCase() + "-active"; | |
| 669 this.classList.add(modifier); | |
| 670 } | |
| 671 // Adds the current keyboard modifiers to the detail. | |
| 672 if (this.ctrl) | |
| 673 detail.controlModifier = this.ctrl.isActive(); | |
| 674 if (this.alt) | |
| 675 detail.altModifier = this.alt.isActive(); | |
| 676 if (this.lastPressedKey) | |
| 677 this.lastPressedKey.classList.remove('active'); | |
| 678 // Keyset transition key. This is needed to transition from upper | |
| 679 // to lower case when we are not in caps mode, as well as when | |
| 680 // we're ending chording. | |
| 681 this.changeKeyset(detail); | |
| 682 | |
| 683 if (this.lastPressedKey && | |
| 684 this.lastPressedKey.charValue != event.target.charValue) { | |
| 685 return; | |
| 686 } | |
| 687 if (repeatKey.key == event.target) { | |
| 688 repeatKey.cancel(); | |
| 689 this.lastPressedKey = null; | |
| 690 return; | |
| 691 } | |
| 692 var toLayoutId = detail.toLayout; | |
| 693 // Layout transition key. | |
| 694 if (toLayoutId) | |
| 695 this.layout = toLayoutId; | |
| 696 var char = detail.char; | |
| 697 this.lastPressedKey = null; | |
| 698 // Characters that should not be typed. | |
| 699 switch(char) { | |
| 700 case 'Invalid': | |
| 701 case 'Shift': | |
| 702 case 'Ctrl': | |
| 703 case 'Alt': | |
| 704 enterUpperOnSpace = false; | |
| 705 swipeTracker.swipeFlags = 0; | |
| 706 return; | |
| 707 case 'Microphone': | |
| 708 this.voiceInput_.onDown(); | |
| 709 return; | |
| 710 default: | |
| 711 break; | |
| 712 } | |
| 713 // Tries to type the character. Resorts to insertText if that fails. | |
| 714 if(!this.keyTyped(detail)) | |
| 715 insertText(char); | |
| 716 // Post-typing logic. | |
| 717 switch(char) { | |
| 718 case '\n': | |
| 719 case ' ': | |
| 720 if(enterUpperOnSpace) { | |
| 721 enterUpperOnSpace = false; | |
| 722 if (this.shift) { | |
| 723 var shiftDetail = this.shift.onSpaceAfterPunctuation(); | |
| 724 // Check if transition defined. | |
| 725 this.changeKeyset(shiftDetail); | |
| 726 } else { | |
| 727 console.error('Capitalization on space after punctuation \ | |
| 728 enabled, but cannot find target keyset.'); | |
| 729 } | |
| 730 // Immediately return to maintain shift-state. Space is a | |
| 731 // non-control key and would otherwise trigger a reset of the | |
| 732 // shift key, causing a transition to lower case. | |
| 733 return; | |
| 734 } | |
| 735 break; | |
| 736 case '.': | |
| 737 case '?': | |
| 738 case '!': | |
| 739 enterUpperOnSpace = this.shouldUpperOnSpace(); | |
| 740 break; | |
| 741 default: | |
| 742 enterUpperOnSpace = false; | |
| 743 break; | |
| 744 } | |
| 745 // Reset control keys. | |
| 746 this.onNonControlKeyTyped(); | |
| 747 }, | |
| 748 | |
| 749 /** | |
| 750 * Handles key-longpress event that is sent by kb-key-base. | |
| 751 * @param {CustomEvent} event The key-longpress event dispatched by | |
| 752 * kb-key-base. | |
| 753 * @param {Object} detail The detail of pressed key. | |
| 754 */ | |
| 755 keyLongpress: function(event, detail) { | |
| 756 // If the gesture is long press, remove the pointermove listener. | |
| 757 this.removeEventListener('pointermove', this.swipeHandler, false); | |
| 758 // Keyset transtion key. | |
| 759 if (this.changeKeyset(detail)) { | |
| 760 // Locks the keyset before removing active to prevent flicker. | |
| 761 this.classList.add('caps-locked'); | |
| 762 // Makes last pressed key inactive if transit to a new keyset on long | |
| 763 // press. | |
| 764 if (this.lastPressedKey) | |
| 765 this.lastPressedKey.classList.remove('active'); | |
| 766 } | |
| 767 }, | |
| 768 | |
| 769 /** | |
| 770 * Plays the specified sound. | |
| 771 * @param {Sound} sound The id of the audio tag. | |
| 772 */ | |
| 773 playSound: function(sound) { | |
| 774 if (!SOUND_ENABLED || !sound || sound == Sound.NONE) | |
| 775 return; | |
| 776 var pool = this.sounds[sound]; | |
| 777 if (!pool) { | |
| 778 console.error("Cannot find audio tag: " + sound); | |
| 779 return; | |
| 780 } | |
| 781 // Search the sound pool for a free resource. | |
| 782 for (var i = 0; i < pool.length; i++) { | |
| 783 if (pool[i].paused) { | |
| 784 pool[i].play(); | |
| 785 return; | |
| 786 } | |
| 787 } | |
| 788 }, | |
| 789 | |
| 790 /** | |
| 791 * Whether we should transit to upper case when seeing a space after | |
| 792 * punctuation. | |
| 793 * @return {boolean} | |
| 794 */ | |
| 795 shouldUpperOnSpace: function() { | |
| 796 // TODO(rsadam): Add other input types in which we should not | |
| 797 // transition to upper after a space. | |
| 798 return this.inputTypeValue != 'password'; | |
| 799 }, | |
| 800 | |
| 801 /** | |
| 802 * Handler for the 'set-layout' event. | |
| 803 * @param {!Event} event The triggering event. | |
| 804 * @param {{layout: string}} details Details of the event, which contains | |
| 805 * the name of the layout to activate. | |
| 806 */ | |
| 807 setLayout: function(event, details) { | |
| 808 this.layout = details.layout; | |
| 809 }, | |
| 810 | |
| 811 /** | |
| 812 * Handles a change in the keyboard layout. Auto-selects the default | |
| 813 * keyset for the new layout. | |
| 814 */ | |
| 815 layoutChanged: function() { | |
| 816 this.stale = true; | |
| 817 if (!this.selectDefaultKeyset()) { | |
| 818 console.error('No default keyset found for layout: ' + this.layout); | |
| 819 return; | |
| 820 } | |
| 821 this.activeKeyset.show(); | |
| 822 }, | |
| 823 | |
| 824 /** | |
| 825 * Notifies the modifier keys that a non-control key was typed. This | |
| 826 * lets them reset sticky behaviour. A non-control key is defined as | |
| 827 * any key that is not Control, Alt, or Shift. | |
| 828 */ | |
| 829 onNonControlKeyTyped: function() { | |
| 830 if (this.shift) | |
| 831 this.shift.onNonControlKeyTyped(); | |
| 832 if (this.ctrl) | |
| 833 this.ctrl.onNonControlKeyTyped(); | |
| 834 if (this.alt) | |
| 835 this.alt.onNonControlKeyTyped(); | |
| 836 this.classList.remove('ctrl-active'); | |
| 837 this.classList.remove('alt-active'); | |
| 838 }, | |
| 839 | |
| 840 /** | |
| 841 * Callback function for when volume is changed. | |
| 842 */ | |
| 843 volumeChanged: function() { | |
| 844 var toChange = Object.keys(this.sounds); | |
| 845 for (var i = 0; i < toChange.length; i++) { | |
| 846 var pool = this.sounds[toChange[i]]; | |
| 847 for (var j = 0; j < pool.length; j++) { | |
| 848 pool[j].volume = this.volume; | |
| 849 } | |
| 850 } | |
| 851 }, | |
| 852 | |
| 853 /** | |
| 854 * Id for the active keyset. | |
| 855 * @type {string} | |
| 856 */ | |
| 857 get activeKeysetId() { | |
| 858 return this.layout + '-' + this.keyset; | |
| 859 }, | |
| 860 | |
| 861 /** | |
| 862 * The active keyset DOM object. | |
| 863 * @type {kb-keyset} | |
| 864 */ | |
| 865 get activeKeyset() { | |
| 866 return this.querySelector('#' + this.activeKeysetId); | |
| 867 }, | |
| 868 | |
| 869 /** | |
| 870 * The current input type. | |
| 871 * @type {string} | |
| 872 */ | |
| 873 get inputTypeValue() { | |
| 874 return this.inputType; | |
| 875 }, | |
| 876 | |
| 877 /** | |
| 878 * Changes the input type if it's different from the current | |
| 879 * type, else resets the keyset to the default keyset. | |
| 880 * @type {string} | |
| 881 */ | |
| 882 set inputTypeValue(value) { | |
| 883 if (value == this.inputType) | |
| 884 this.selectDefaultKeyset(); | |
| 885 else | |
| 886 this.inputType = value; | |
| 887 }, | |
| 888 | |
| 889 /** | |
| 890 * The keyboard is ready for input once the target keyset appears | |
| 891 * in the distributed nodes for the keyboard. | |
| 892 * @return {boolean} Indicates if the keyboard is ready for input. | |
| 893 */ | |
| 894 isReady: function() { | |
| 895 var keyset = this.activeKeyset; | |
| 896 if (!keyset) | |
| 897 return false; | |
| 898 var nodes = this.$.content.getDistributedNodes(); | |
| 899 for (var i = 0; i < nodes.length; i++) { | |
| 900 if (nodes[i].id && nodes[i].id == keyset.id) | |
| 901 return true; | |
| 902 } | |
| 903 return false; | |
| 904 }, | |
| 905 | |
| 906 /** | |
| 907 * Generates fabricated key events to simulate typing on a | |
| 908 * physical keyboard. | |
| 909 * @param {Object} detail Attributes of the key being typed. | |
| 910 * @return {boolean} Whether the key type succeeded. | |
| 911 */ | |
| 912 keyTyped: function(detail) { | |
| 913 var builder = this.$.keyCodeMetadata; | |
| 914 if (this.ctrl) | |
| 915 detail.controlModifier = this.ctrl.isActive(); | |
| 916 if (this.alt) | |
| 917 detail.altModifier = this.alt.isActive(); | |
| 918 var downEvent = builder.createVirtualKeyEvent(detail, "keydown"); | |
| 919 if (downEvent) { | |
| 920 sendKeyEvent(downEvent); | |
| 921 sendKeyEvent(builder.createVirtualKeyEvent(detail, "keyup")); | |
| 922 return true; | |
| 923 } | |
| 924 return false; | |
| 925 }, | |
| 926 | |
| 927 /** | |
| 928 * Selects the default keyset for a layout. | |
| 929 * @return {boolean} True if successful. This method can fail if the | |
| 930 * keysets corresponding to the layout have not been injected. | |
| 931 */ | |
| 932 selectDefaultKeyset: function() { | |
| 933 var keysets = this.querySelectorAll('kb-keyset'); | |
| 934 // Full name of the keyset is of the form 'layout-keyset'. | |
| 935 var regex = new RegExp('^' + this.layout + '-(.+)'); | |
| 936 var keysetsLoaded = false; | |
| 937 for (var i = 0; i < keysets.length; i++) { | |
| 938 var matches = keysets[i].id.match(regex); | |
| 939 if (matches && matches.length == REGEX_MATCH_COUNT) { | |
| 940 keysetsLoaded = true; | |
| 941 // Without both tests for a default keyset, it is possible to get | |
| 942 // into a state where multiple layouts are displayed. A | |
| 943 // reproducable test case is do the following set of keyset | |
| 944 // transitions: qwerty -> system -> dvorak -> qwerty. | |
| 945 // TODO(kevers): Investigate why this is the case. | |
| 946 if (keysets[i].isDefault || | |
| 947 keysets[i].getAttribute('isDefault') == 'true') { | |
| 948 this.keyset = matches[REGEX_KEYSET_INDEX]; | |
| 949 this.classList.remove('caps-locked'); | |
| 950 this.classList.remove('alt-active'); | |
| 951 this.classList.remove('ctrl-active'); | |
| 952 // Caches shift key. | |
| 953 this.shift = this.querySelector('kb-shift-key'); | |
| 954 if (this.shift) | |
| 955 this.shift.reset(); | |
| 956 // Caches control key. | |
| 957 this.ctrl = this.querySelector('kb-modifier-key[char=Ctrl]'); | |
| 958 if (this.ctrl) | |
| 959 this.ctrl.reset(); | |
| 960 // Caches alt key. | |
| 961 this.alt = this.querySelector('kb-modifier-key[char=Alt]'); | |
| 962 if (this.alt) | |
| 963 this.alt.reset(); | |
| 964 this.fire('stateChange', { | |
| 965 state: 'keysetLoaded', | |
| 966 value: this.keyset, | |
| 967 }); | |
| 968 keyboardLoaded(); | |
| 969 return true; | |
| 970 } | |
| 971 } | |
| 972 } | |
| 973 if (keysetsLoaded) | |
| 974 console.error('No default keyset found for ' + this.layout); | |
| 975 return false; | |
| 976 } | |
| 977 }); | |
| OLD | NEW |