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