• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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