1<!DOCTYPE html> 2<!-- 3Copyright (c) 2013 The Chromium Authors. All rights reserved. 4Use of this source code is governed by a BSD-style license that can be 5found in the LICENSE file. 6--> 7 8<link rel="import" href="/tracing/base/event.html"> 9<link rel="import" href="/tracing/base/iteration_helpers.html"> 10<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> 11<link rel="import" href="/tracing/ui/base/mouse_tracker.html"> 12<link rel="import" href="/tracing/ui/base/ui.html"> 13<link rel="import" href="/tracing/ui/base/utils.html"> 14<link rel="import" href="/tracing/ui/base/mouse_modes.html"> 15<link rel="import" href="/tracing/ui/base/mouse_mode_icon.html"> 16 17<polymer-element name="tr-ui-b-mouse-mode-selector"> 18 <template> 19 <style> 20 :host { 21 22 -webkit-user-drag: element; 23 -webkit-user-select: none; 24 25 background: #DDD; 26 border: 1px solid #BBB; 27 border-radius: 4px; 28 box-shadow: 0 1px 2px rgba(0,0,0,0.2); 29 left: calc(100% - 120px); 30 position: absolute; 31 top: 100px; 32 user-select: none; 33 width: 29px; 34 z-index: 20; 35 } 36 37 .drag-handle { 38 background: url(../images/ui-states.png) 2px 3px no-repeat; 39 background-repeat: no-repeat; 40 border-bottom: 1px solid #BCBCBC; 41 cursor: move; 42 display: block; 43 height: 13px; 44 width: 27px; 45 } 46 47 .tool-button { 48 background-position: center center; 49 background-repeat: no-repeat; 50 border-bottom: 1px solid #BCBCBC; 51 border-top: 1px solid #F1F1F1; 52 cursor: pointer; 53 } 54 55 .buttons > .tool-button:last-child { 56 border-bottom: none; 57 } 58 59 </style> 60 <div class="drag-handle"></div> 61 <div class="buttons"> 62 </div> 63 </template> 64</polymer-element> 65<script> 66'use strict'; 67 68tr.exportTo('tr.ui.b', function() { 69 var MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE; 70 var MOUSE_SELECTOR_MODE_INFOS = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS; 71 72 73 var MIN_MOUSE_SELECTION_DISTANCE = 4; 74 75 var MODIFIER = { 76 SHIFT: 0x1, 77 SPACE: 0x2, 78 CMD_OR_CTRL: 0x4 79 }; 80 81 function isCmdOrCtrlPressed(event) { 82 if (tr.isMac) 83 return event.metaKey; 84 else 85 return event.ctrlKey; 86 } 87 88 /** 89 * Provides a panel for switching the interaction mode of the mouse. 90 * It handles the user interaction and dispatches events for the various 91 * modes. 92 */ 93 Polymer('tr-ui-b-mouse-mode-selector', { 94 __proto__: HTMLDivElement.prototype, 95 96 created: function() { 97 this.supportedModeMask_ = MOUSE_SELECTOR_MODE.ALL_MODES; 98 99 this.initialRelativeMouseDownPos_ = {x: 0, y: 0}; 100 101 this.defaultMode_ = MOUSE_SELECTOR_MODE.PANSCAN; 102 this.settingsKey_ = undefined; 103 this.mousePos_ = {x: 0, y: 0}; 104 this.mouseDownPos_ = {x: 0, y: 0}; 105 106 this.onMouseDown_ = this.onMouseDown_.bind(this); 107 this.onMouseMove_ = this.onMouseMove_.bind(this); 108 this.onMouseUp_ = this.onMouseUp_.bind(this); 109 110 this.onKeyDown_ = this.onKeyDown_.bind(this); 111 this.onKeyUp_ = this.onKeyUp_.bind(this); 112 113 this.mode_ = undefined; 114 this.modeToKeyCodeMap_ = {}; 115 this.modifierToModeMap_ = {}; 116 117 this.targetElement_ = undefined; 118 this.modeBeforeAlternativeModeActivated_ = null; 119 120 this.isInteracting_ = false; 121 this.isClick_ = false; 122 }, 123 124 ready: function() { 125 this.buttonsEl_ = this.shadowRoot.querySelector('.buttons'); 126 this.dragHandleEl_ = this.shadowRoot.querySelector('.drag-handle'); 127 this.supportedModeMask = MOUSE_SELECTOR_MODE.ALL_MODES; 128 129 this.dragHandleEl_.addEventListener('mousedown', 130 this.onDragHandleMouseDown_.bind(this)); 131 132 this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_); 133 this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_); 134 this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this)); 135 }, 136 137 attached: function() { 138 document.addEventListener('keydown', this.onKeyDown_); 139 document.addEventListener('keyup', this.onKeyUp_); 140 }, 141 142 detached: function() { 143 document.removeEventListener('keydown', this.onKeyDown_); 144 document.removeEventListener('keyup', this.onKeyUp_); 145 }, 146 147 get targetElement() { 148 return this.targetElement_; 149 }, 150 151 set targetElement(target) { 152 if (this.targetElement_) 153 this.targetElement_.removeEventListener('mousedown', this.onMouseDown_); 154 this.targetElement_ = target; 155 if (this.targetElement_) 156 this.targetElement_.addEventListener('mousedown', this.onMouseDown_); 157 }, 158 159 get defaultMode() { 160 return this.defaultMode_; 161 }, 162 163 set defaultMode(defaultMode) { 164 this.defaultMode_ = defaultMode; 165 }, 166 167 get settingsKey() { 168 return this.settingsKey_; 169 }, 170 171 set settingsKey(settingsKey) { 172 this.settingsKey_ = settingsKey; 173 if (!this.settingsKey_) 174 return; 175 176 var mode = tr.b.Settings.get(this.settingsKey_ + '.mode', undefined); 177 // Modes changed from 1,2,3,4 to 0x1, 0x2, 0x4, 0x8. Fix any stray 178 // settings to the best of our abilities. 179 if (MOUSE_SELECTOR_MODE_INFOS[mode] === undefined) 180 mode = undefined; 181 182 // Restoring settings against unsupported modes should just go back to the 183 // default mode. 184 if ((mode & this.supportedModeMask_) === 0) 185 mode = undefined; 186 187 if (!mode) 188 mode = this.defaultMode_; 189 this.mode = mode; 190 191 var pos = tr.b.Settings.get(this.settingsKey_ + '.pos', undefined); 192 if (pos) 193 this.pos = pos; 194 }, 195 196 get supportedModeMask() { 197 return this.supportedModeMask_; 198 }, 199 200 /** 201 * Sets the supported modes. Should be an OR-ing of MOUSE_SELECTOR_MODE 202 * values. 203 */ 204 set supportedModeMask(supportedModeMask) { 205 if (this.mode && (supportedModeMask & this.mode) === 0) 206 throw new Error('supportedModeMask must include current mode.'); 207 208 function createButtonForMode(mode) { 209 return button; 210 } 211 212 this.supportedModeMask_ = supportedModeMask; 213 this.buttonsEl_.textContent = ''; 214 for (var modeName in MOUSE_SELECTOR_MODE) { 215 if (modeName == 'ALL_MODES') 216 continue; 217 var mode = MOUSE_SELECTOR_MODE[modeName]; 218 if ((this.supportedModeMask_ & mode) === 0) 219 continue; 220 221 var button = document.createElement('tr-ui-b-mouse-mode-icon'); 222 button.mode = mode; 223 button.classList.add('tool-button'); 224 225 this.buttonsEl_.appendChild(button); 226 } 227 }, 228 229 getButtonForMode_: function(mode) { 230 for (var i = 0; i < this.buttonsEl_.children.length; i++) { 231 var buttonEl = this.buttonsEl_.children[i]; 232 if (buttonEl.mode === mode) 233 return buttonEl; 234 } 235 return undefined; 236 }, 237 238 get mode() { 239 return this.currentMode_; 240 }, 241 242 set mode(newMode) { 243 if (newMode !== undefined) { 244 if (typeof newMode !== 'number') 245 throw new Error('Mode must be a number'); 246 if ((newMode & this.supportedModeMask_) === 0) 247 throw new Error('Cannot switch to this mode, it is not supported'); 248 if (MOUSE_SELECTOR_MODE_INFOS[newMode] === undefined) 249 throw new Error('Unrecognized mode'); 250 } 251 252 var modeInfo; 253 254 if (this.currentMode_ === newMode) 255 return; 256 257 if (this.currentMode_) { 258 var buttonEl = this.getButtonForMode_(this.currentMode_); 259 if (buttonEl) 260 buttonEl.active = false; 261 262 // End event. 263 if (this.isInteracting_) { 264 265 var mouseEvent = this.createEvent_( 266 MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end); 267 this.dispatchEvent(mouseEvent); 268 } 269 270 // Exit event. 271 modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_]; 272 tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true); 273 } 274 275 this.currentMode_ = newMode; 276 277 if (this.currentMode_) { 278 var buttonEl = this.getButtonForMode_(this.currentMode_); 279 if (buttonEl) 280 buttonEl.active = true; 281 282 // Entering a new mode resets mouse down pos. 283 this.mouseDownPos_.x = this.mousePos_.x; 284 this.mouseDownPos_.y = this.mousePos_.y; 285 286 // Enter event. 287 modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_]; 288 if (!this.isInAlternativeMode_) 289 tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true); 290 291 // Begin event. 292 if (this.isInteracting_) { 293 var mouseEvent = this.createEvent_( 294 MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin); 295 this.dispatchEvent(mouseEvent); 296 } 297 298 299 } 300 301 if (this.settingsKey_ && !this.isInAlternativeMode_) 302 tr.b.Settings.set(this.settingsKey_ + '.mode', this.mode); 303 }, 304 305 setKeyCodeForMode: function(mode, keyCode) { 306 if ((mode & this.supportedModeMask_) === 0) 307 throw new Error('Mode not supported'); 308 this.modeToKeyCodeMap_[mode] = keyCode; 309 310 if (!this.buttonsEl_) 311 return; 312 313 var buttonEl = this.getButtonForMode_(mode); 314 if (buttonEl) 315 buttonEl.acceleratorKey = String.fromCharCode(keyCode); 316 }, 317 318 setCurrentMousePosFromEvent_: function(e) { 319 this.mousePos_.x = e.clientX; 320 this.mousePos_.y = e.clientY; 321 }, 322 323 createEvent_: function(eventName, sourceEvent) { 324 var event = new tr.b.Event(eventName, true); 325 event.clientX = this.mousePos_.x; 326 event.clientY = this.mousePos_.y; 327 event.deltaX = this.mousePos_.x - this.mouseDownPos_.x; 328 event.deltaY = this.mousePos_.y - this.mouseDownPos_.y; 329 event.mouseDownX = this.mouseDownPos_.x; 330 event.mouseDownY = this.mouseDownPos_.y; 331 event.didPreventDefault = false; 332 event.preventDefault = function() { 333 event.didPreventDefault = true; 334 if (sourceEvent) 335 sourceEvent.preventDefault(); 336 }; 337 event.stopPropagation = function() { 338 sourceEvent.stopPropagation(); 339 }; 340 event.stopImmediatePropagation = function() { 341 throw new Error('Not implemented'); 342 }; 343 return event; 344 }, 345 346 onMouseDown_: function(e) { 347 if (e.button !== 0) 348 return; 349 this.setCurrentMousePosFromEvent_(e); 350 var mouseEvent = this.createEvent_( 351 MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin, e); 352 if (this.mode === MOUSE_SELECTOR_MODE.SELECTION) 353 mouseEvent.appendSelection = isCmdOrCtrlPressed(e); 354 this.dispatchEvent(mouseEvent); 355 this.isInteracting_ = true; 356 this.isClick_ = true; 357 tr.ui.b.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_); 358 }, 359 360 onMouseMove_: function(e) { 361 this.setCurrentMousePosFromEvent_(e); 362 363 var mouseEvent = this.createEvent_( 364 MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.update, e); 365 this.dispatchEvent(mouseEvent); 366 367 if (this.isInteracting_) 368 this.checkIsClick_(e); 369 }, 370 371 onMouseUp_: function(e) { 372 if (e.button !== 0) 373 return; 374 375 var mouseEvent = this.createEvent_( 376 MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end, e); 377 mouseEvent.isClick = this.isClick_; 378 this.dispatchEvent(mouseEvent); 379 380 if (this.isClick_ && !mouseEvent.didPreventDefault) 381 this.dispatchClickEvents_(e); 382 383 this.isInteracting_ = false; 384 this.updateAlternativeModeState_(e); 385 }, 386 387 onButtonMouseDown_: function(e) { 388 e.preventDefault(); 389 e.stopImmediatePropagation(); 390 }, 391 392 onButtonMouseUp_: function(e) { 393 e.preventDefault(); 394 e.stopImmediatePropagation(); 395 }, 396 397 onButtonPress_: function(e) { 398 this.modeBeforeAlternativeModeActivated_ = undefined; 399 this.mode = e.target.mode; 400 e.preventDefault(); 401 }, 402 403 onKeyDown_: function(e) { 404 // Keys dispatched to INPUT elements still bubble, even when they're 405 // handled. So, skip any events that targeted the input element. 406 if (e.path[0].tagName == 'INPUT') 407 return; 408 409 if (e.keyCode === ' '.charCodeAt(0)) 410 this.spacePressed_ = true; 411 this.updateAlternativeModeState_(e); 412 }, 413 414 onKeyUp_: function(e) { 415 // Keys dispatched to INPUT elements still bubble, even when they're 416 // handled. So, skip any events that targeted the input element. 417 if (e.path[0].tagName == 'INPUT') 418 return; 419 420 if (e.keyCode === ' '.charCodeAt(0)) 421 this.spacePressed_ = false; 422 423 var didHandleKey = false; 424 tr.b.iterItems(this.modeToKeyCodeMap_, function(modeStr, keyCode) { 425 if (e.keyCode === keyCode) { 426 this.modeBeforeAlternativeModeActivated_ = undefined; 427 var mode = parseInt(modeStr); 428 this.mode = mode; 429 didHandleKey = true; 430 } 431 }, this); 432 433 if (didHandleKey) { 434 e.preventDefault(); 435 e.stopPropagation(); 436 return; 437 } 438 this.updateAlternativeModeState_(e); 439 }, 440 441 updateAlternativeModeState_: function(e) { 442 var shiftPressed = e.shiftKey; 443 var spacePressed = this.spacePressed_; 444 var cmdOrCtrlPressed = isCmdOrCtrlPressed(e); 445 446 // Figure out the new mode 447 var smm = this.supportedModeMask_; 448 var newMode; 449 var isNewModeAnAlternativeMode = false; 450 if (shiftPressed && 451 (this.modifierToModeMap_[MODIFIER.SHIFT] & smm) !== 0) { 452 newMode = this.modifierToModeMap_[MODIFIER.SHIFT]; 453 isNewModeAnAlternativeMode = true; 454 } else if (spacePressed && 455 (this.modifierToModeMap_[MODIFIER.SPACE] & smm) !== 0) { 456 newMode = this.modifierToModeMap_[MODIFIER.SPACE]; 457 isNewModeAnAlternativeMode = true; 458 } else if (cmdOrCtrlPressed && 459 (this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL] & smm) !== 0) { 460 newMode = this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL]; 461 isNewModeAnAlternativeMode = true; 462 } else { 463 // Go to the old mode, if there is one. 464 if (this.isInAlternativeMode_) { 465 newMode = this.modeBeforeAlternativeModeActivated_; 466 isNewModeAnAlternativeMode = false; 467 } else { 468 newMode = undefined; 469 } 470 } 471 472 // Maybe a mode change isn't needed. 473 if (this.mode === newMode || newMode === undefined) 474 return; 475 476 // Okay, we're changing. 477 if (isNewModeAnAlternativeMode) 478 this.modeBeforeAlternativeModeActivated_ = this.mode; 479 this.mode = newMode; 480 }, 481 482 get isInAlternativeMode_() { 483 return !!this.modeBeforeAlternativeModeActivated_; 484 }, 485 486 setModifierForAlternateMode: function(mode, modifier) { 487 this.modifierToModeMap_[modifier] = mode; 488 }, 489 490 get pos() { 491 return { 492 x: parseInt(this.style.left), 493 y: parseInt(this.style.top) 494 }; 495 }, 496 497 set pos(pos) { 498 pos = this.constrainPositionToBounds_(pos); 499 500 this.style.left = pos.x + 'px'; 501 this.style.top = pos.y + 'px'; 502 503 if (this.settingsKey_) 504 tr.b.Settings.set(this.settingsKey_ + '.pos', this.pos); 505 }, 506 507 constrainPositionToBounds_: function(pos) { 508 var parent = this.offsetParent || document.body; 509 var parentRect = tr.ui.b.windowRectForElement(parent); 510 511 var top = 0; 512 var bottom = parentRect.height - this.offsetHeight; 513 var left = 0; 514 var right = parentRect.width - this.offsetWidth; 515 516 var res = {}; 517 res.x = Math.max(pos.x, left); 518 res.x = Math.min(res.x, right); 519 520 res.y = Math.max(pos.y, top); 521 res.y = Math.min(res.y, bottom); 522 return res; 523 }, 524 525 onDragHandleMouseDown_: function(e) { 526 e.preventDefault(); 527 e.stopImmediatePropagation(); 528 529 var mouseDownPos = { 530 x: e.clientX - this.offsetLeft, 531 y: e.clientY - this.offsetTop 532 }; 533 tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { 534 var pos = {}; 535 pos.x = e.clientX - mouseDownPos.x; 536 pos.y = e.clientY - mouseDownPos.y; 537 this.pos = pos; 538 }.bind(this)); 539 }, 540 541 checkIsClick_: function(e) { 542 if (!this.isInteracting_ || !this.isClick_) 543 return; 544 545 var deltaX = this.mousePos_.x - this.mouseDownPos_.x; 546 var deltaY = this.mousePos_.y - this.mouseDownPos_.y; 547 var minDist = MIN_MOUSE_SELECTION_DISTANCE; 548 549 if (deltaX * deltaX + deltaY * deltaY > minDist * minDist) 550 this.isClick_ = false; 551 }, 552 553 dispatchClickEvents_: function(e) { 554 if (!this.isClick_) 555 return; 556 557 var modeInfo = MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.SELECTION]; 558 var eventNames = modeInfo.eventNames; 559 560 var mouseEvent = this.createEvent_(eventNames.begin); 561 mouseEvent.appendSelection = isCmdOrCtrlPressed(e); 562 this.dispatchEvent(mouseEvent); 563 564 mouseEvent = this.createEvent_(eventNames.end); 565 this.dispatchEvent(mouseEvent); 566 } 567 }); 568 569 return { 570 MIN_MOUSE_SELECTION_DISTANCE: MIN_MOUSE_SELECTION_DISTANCE, 571 MODIFIER: MODIFIER 572 }; 573}); 574</script> 575