1 <!-- 2 -- Copyright (c) 2013 The Chromium Authors. All rights reserved. 3 -- Use of this source code is governed by a BSD-style license that can be 4 -- found in the LICENSE file. 5 --> 6 7 <polymer-element name="kb-keyboard" on-key-over="keyOver" on-key-up="keyUp" 8 on-key-down="keyDown" on-key-longpress="keyLongpress" on-pointerup="up" 9 on-pointerdown="down" on-enable-sel="enableSel" 10 on-enable-dbl="enableDbl" attributes="keyset layout rows"> 11 <template> 12 <style> 13 @host { 14 * { 15 position: relative; 16 } 17 } 18 </style> 19 <!-- The ID for a keyset follows the naming convention of combining the 20 -- layout name with a base keyset name. This convention is used to 21 -- allow multiple layouts to be loaded (enablign fast switching) while 22 -- allowing the shift and spacebar keys to be common across multiple 23 -- keyboard layouts. 24 --> 25 <content select="#{{layout}}-{{keyset}}"></content> 26 </template> 27 <script> 28 /** 29 * The repeat delay in milliseconds before a key starts repeating. Use the 30 * same rate as Chromebook. 31 * (See chrome/browser/chromeos/language_preferences.cc) 32 * @const 33 * @type {number} 34 */ 35 var REPEAT_DELAY_MSEC = 500; 36 37 /** 38 * The repeat interval or number of milliseconds between subsequent 39 * keypresses. Use the same rate as Chromebook. 40 * @const 41 * @type {number} 42 */ 43 var REPEAT_INTERVAL_MSEC = 50; 44 45 /** 46 * The double click/tap interval. 47 * @const 48 * @type {number} 49 */ 50 var DBL_INTERVAL_MSEC = 300; 51 52 /** 53 * The boolean to decide if keyboard should transit to upper case keyset 54 * when spacebar is pressed. If a closing punctuation is followed by a 55 * spacebar, keyboard should automatically transit to upper case. 56 * @type {boolean} 57 */ 58 var enterUpperOnSpace = false; 59 60 /** 61 * A structure to track the currently repeating key on the keyboard. 62 */ 63 var repeatKey = { 64 65 /** 66 * The timer for the delay before repeating behaviour begins. 67 * @type {number|undefined} 68 */ 69 timer: undefined, 70 71 /** 72 * The interval timer for issuing keypresses of a repeating key. 73 * @type {number|undefined} 74 */ 75 interval: undefined, 76 77 /** 78 * The key which is currently repeating. 79 * @type {BaseKey|undefined} 80 */ 81 key: undefined, 82 83 /** 84 * Cancel the repeat timers of the currently active key. 85 */ 86 cancel: function() { 87 clearTimeout(this.timer); 88 clearInterval(this.interval); 89 this.timer = undefined; 90 this.interval = undefined; 91 this.key = undefined; 92 } 93 }; 94 95 /** 96 * The minimum movement interval needed to trigger cursor move on 97 * horizontal and vertical way. 98 * @const 99 * @type {number} 100 */ 101 var MIN_SWIPE_DIST = 30; 102 103 /** 104 * The flags constants when shift is on. It is according to the EventFlags 105 * in event_constants.h in chromium c++ code. 106 * @const 107 * @type {number} 108 * TODO(zyaozhujun): Might add more flags here according to the defination 109 * in EventFlags. 110 */ 111 var SHIFT = 2; 112 113 /** 114 * The boolean to decide if it is swipe in process or finished. 115 * @const 116 * @type {boolean} 117 */ 118 var swipeInProgress = false; 119 120 /** 121 * The enumeration of swipe directions. 122 * @const 123 * @type {Enum} 124 */ 125 var SWIPE_DIRECTION = { 126 RIGHT: 0x1, 127 LEFT: 0x2, 128 UP: 0x4, 129 DOWN: 0x8 130 }; 131 132 /** 133 * A structure to track the current swipe status. 134 */ 135 var swipeStatus = { 136 137 /** 138 * The count of horizontal and vertical movement. 139 * @type {number} 140 */ 141 offset_x : 0, 142 offset_y : 0, 143 144 /** 145 * Last touch coordinate. 146 * @type {number} 147 */ 148 pre_x : 0, 149 pre_y : 0, 150 151 /** 152 * The flag of current modifier key. 153 * @type {number} 154 */ 155 swipeFlags : 0, 156 157 /** 158 * Current swipe direction. 159 * @type {number} 160 */ 161 swipeDirection : 0, 162 163 /** 164 * Reset all the values when swipe finished. 165 */ 166 resetAll: function() { 167 this.offset_x = 0; 168 this.offset_y = 0; 169 this.pre_x = 0; 170 this.pre_y = 0; 171 this.swipeFlags = 0; 172 this.swipeDirection = 0; 173 } 174 }; 175 176 Polymer('kb-keyboard', { 177 lastPressedKey: null, 178 voiceInput_: null, 179 dblDetail_: null, 180 dblTimer_: null, 181 swipeHandler: null, 182 183 ready: function() { 184 this.voiceInput_ = new VoiceInput(this); 185 this.swipeHandler = this.onSwipeUpdate.bind(this); 186 }, 187 188 /** 189 * When double click/tap event is enabled, the second key-down and key-up 190 * events on the same key should be skipped. Return true when the event 191 * with |detail| should be skipped. 192 * @param {Object} detail The detail of key-up or key-down event. 193 */ 194 skipEvent: function(detail) { 195 if (this.dblDetail_) { 196 if (this.dblDetail_.char != detail.char) { 197 // The second key down is not on the same key. Double click/tap 198 // should be ignored. 199 this.dblDetail_ = null; 200 clearTimeout(this.dblTimer_); 201 } else if (this.dblDetail_.clickCount == 1) { 202 return true; 203 } 204 } 205 return false; 206 }, 207 208 /** 209 * This function is bound to swipeHandler. And swipeHandler handle 210 * the pointermove event after pointerdown event happened. 211 * @para {PointerEvent} event. 212 */ 213 onSwipeUpdate: function(event) { 214 swipeStatus.offset_x += event.screenX - swipeStatus.pre_x; 215 swipeStatus.offset_y += event.screenY - swipeStatus.pre_y; 216 if (Math.abs(swipeStatus.offset_x) > MIN_SWIPE_DIST || 217 Math.abs(swipeStatus.offset_y) > MIN_SWIPE_DIST) { 218 swipeInProgress = true; 219 this.lastPressedKey.classList.remove('active'); 220 } 221 if (swipeStatus.offset_x > MIN_SWIPE_DIST) { 222 swipeStatus.swipeDirection |= SWIPE_DIRECTION.RIGHT; 223 swipeStatus.offset_x = 0; 224 } 225 else if (swipeStatus.offset_x < -MIN_SWIPE_DIST) { 226 swipeStatus.swipeDirection |= SWIPE_DIRECTION.LEFT; 227 swipeStatus.offset_x = 0; 228 } 229 // Swipe vertically only when the swipe reaches the gradient of 45 230 // degree. This can also be larger. 231 if (Math.abs(event.screenY - swipeStatus.pre_y) > 232 Math.abs(event.screenX - swipeStatus.pre_x)) { 233 if (swipeStatus.offset_y > MIN_SWIPE_DIST) { 234 swipeStatus.swipeDirection |= SWIPE_DIRECTION.DOWN; 235 swipeStatus.offset_y = 0; 236 } 237 else if (swipeStatus.offset_y < -MIN_SWIPE_DIST) { 238 swipeStatus.swipeDirection |= SWIPE_DIRECTION.UP; 239 swipeStatus.offset_y = 0; 240 } 241 } 242 if (swipeStatus.swipeDirection) { 243 MoveCursor(swipeStatus.swipeDirection, swipeStatus.swipeFlags); 244 swipeStatus.swipeDirection = 0; 245 } 246 swipeStatus.pre_x = event.screenX; 247 swipeStatus.pre_y = event.screenY; 248 }, 249 250 /** 251 * Handles key-down event that is sent by kb-key-base. 252 * @param {CustomEvent} event The key-down event dispatched by 253 * kb-key-base. 254 * @param {Object} detail The detail of pressed kb-key. 255 */ 256 keyDown: function(event, detail) { 257 if (this.skipEvent(detail)) 258 return; 259 260 if (this.lastPressedKey) 261 this.lastPressedKey.classList.remove('active'); 262 this.lastPressedKey = event.target; 263 this.lastPressedKey.classList.add('active'); 264 repeatKey.cancel(); 265 var toKeyset = detail.toKeyset; 266 if (toKeyset) { 267 this.keyset = toKeyset; 268 this.querySelector('#' + this.layout + '-' + this.keyset).nextKeyset = 269 detail.nextKeyset; 270 return; 271 } 272 273 if (detail.repeat) { 274 insertText(detail.char); 275 repeatKey.key = this.lastPressedKey; 276 repeatKey.timer = setTimeout(function() { 277 repeatKey.timer = undefined; 278 repeatKey.interval = setInterval(function() { 279 insertText(detail.char); 280 }, REPEAT_INTERVAL_MSEC); 281 }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC)); 282 } 283 }, 284 285 /** 286 * Enable/start double click/tap event recognition. 287 * @param {CustomEvent} event The enable-dbl event dispatched by 288 * kb-shift-key. 289 * @param {Object} detail The detail of pressed kb-shift-key. 290 */ 291 enableDbl: function(event, detail) { 292 if (!this.dblDetail_) { 293 this.dblDetail_ = detail; 294 this.dblDetail_.clickCount = 0; 295 var self = this; 296 this.dblTimer_ = setTimeout(function() { 297 self.dblDetail_ = null; 298 }, DBL_INTERVAL_MSEC); 299 } 300 }, 301 302 /** 303 * Enable the selection while swipe. 304 * @param {CustomEvent} event The enable-dbl event dispatched by 305 * kb-shift-key. 306 */ 307 enableSel: function(event) { 308 swipeStatus.swipeFlags = SHIFT; 309 }, 310 311 /** 312 * Handles pointerdown event. This is used for swipe selection process. 313 * to get the start pre_x and pre_y. And also add a pointermove handler 314 * to start handling the swipe selection event. 315 * @param {PointerEvent} event The pointerup event that received by 316 * kb-keyboard. 317 */ 318 down: function(event) { 319 swipeStatus.pre_x = event.screenX; 320 swipeStatus.pre_y = event.screenY; 321 this.addEventListener("pointermove", this.swipeHandler, false); 322 }, 323 324 /** 325 * Handles pointerup event. This is used for double tap/click events. 326 * @param {PointerEvent} event The pointerup event that bubbled to 327 * kb-keyboard. 328 */ 329 up: function(event) { 330 if (this.dblDetail_) { 331 this.dblDetail_.clickCount++; 332 if (this.dblDetail_.clickCount == 2) { 333 this.keyset = this.dblDetail_.toKeyset; 334 var keysetId = '#' + this.layout + '-' + this.keyset 335 this.querySelector(keysetId).nextKeyset = this.dblTimer_.nextKeyset; 336 clearTimeout(this.dblTimer_); 337 this.dblDetail_ = null; 338 } 339 } 340 341 // TODO(zyaozhujun): There are some edge cases to deal with later. 342 // (for instance, what if a second finger trigger a down and up 343 // event sequence while swiping). 344 // When pointer up from the screen, a swipe selection session finished, 345 // all the data should be reset to prepare for the next session. 346 if (swipeInProgress) { 347 swipeInProgress = false; 348 swipeStatus.resetAll(); 349 } 350 // Remove the pointermove event hander here. 351 this.removeEventListener('pointermove', this.swipeHandler, false); 352 }, 353 354 /** 355 * Handles key-up event that is sent by kb-key-base. 356 * @param {CustomEvent} event The key-up event dispatched by kb-key-base. 357 * @param {Object} detail The detail of pressed kb-key. 358 */ 359 keyUp: function(event, detail) { 360 if (this.skipEvent(detail)) 361 return; 362 if (swipeInProgress) 363 return; 364 this.lastPressedKey.classList.remove('active'); 365 if (this.lastPressedKey != event.target) 366 return; 367 if (repeatKey.key == event.target) { 368 repeatKey.cancel(); 369 return; 370 } 371 var toKeyset = detail.toKeyset; 372 // Keyset transition key. 373 if (toKeyset) { 374 this.keyset = toKeyset; 375 this.querySelector('#' + this.layout + '-' + this.keyset).nextKeyset = 376 detail.nextKeyset; 377 } 378 var toLayout = detail.toLayout; 379 // Layout transition key. 380 if (toLayout) 381 this.layout = toLayout; 382 var char = detail.char; 383 if (enterUpperOnSpace) { 384 enterUpperOnSpace = false; 385 if (char == ' ') 386 this.keyset = 'upper'; 387 } 388 switch(char) { 389 case 'Invalid': 390 case 'Shift': 391 swipeStatus.swipeFlags = 0; 392 return; 393 case 'Microphone': 394 this.voiceInput_.onDown(); 395 return; 396 case '.': 397 case '?': 398 case '!': 399 enterUpperOnSpace = true; 400 break; 401 default: 402 break; 403 } 404 insertText(char); 405 }, 406 407 /* 408 * Handles key-longpress event that is sent by kb-key-base. 409 * @param {CustomEvent} event The key-longpress event dispatched by 410 * kb-key-base. 411 * @param {Object} detail The detail of pressed key. 412 */ 413 keyLongpress: function(event, detail) { 414 var toKeyset = detail.toKeyset; 415 // Keyset transtion key. 416 if (toKeyset) { 417 this.keyset = toKeyset; 418 this.querySelector('#' + this.layout + '-' + this.keyset).nextKeyset = 419 detail.nextKeyset; 420 // Makes last pressed key inactive if transit to a new keyset on long 421 // press. 422 this.lastPressedKey.classList.remove('active'); 423 } 424 }, 425 426 /** 427 * Handles a change in the keyboard layout. Auto-selects the default 428 * keyset for the new layout. 429 */ 430 layoutChanged: function() { 431 if (!this.selectDefaultKeyset()) { 432 // Keyset selection fails if the keysets have not been loaded yet. 433 var keysets = document.querySelector('#' + this.layout); 434 if (keysets) { 435 keyboard.appendChild(flattenKeysets(keysets.content)); 436 this.selectDefaultKeyset(); 437 } else { 438 console.error('Unable to find layout ' + this.layout); 439 } 440 } 441 }, 442 443 /** 444 * Selects the default keyset for a layout. 445 * @return {boolean} True if successful. This method can fail if the 446 * keysets corresponding to the layout have not been injected. 447 */ 448 selectDefaultKeyset: function() { 449 var keysets = this.querySelectorAll('kb-keyset'); 450 // Full name of the keyset is of the form 'layout-keyset'. 451 var regex = new RegExp('^' + this.layout + '-(.+)'); 452 var keysetsLoaded = false; 453 for (var i = 0; i < keysets.length; i++) { 454 var matches = keysets[i].id.match(regex); 455 if (matches && matches.length == 2) { 456 keysetsLoaded = true; 457 if (keysets[i].isDefault) { 458 this.keyset = matches[1]; 459 return true; 460 } 461 } 462 } 463 if (keysetsLoaded) 464 console.error('No default keyset found for ' + this.layout); 465 return false; 466 } 467 }); 468 </script> 469 </polymer-element> 470 471