| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="/base/event.html"> |
| <link rel="import" href="/base/iteration_helpers.html"> |
| <link rel="import" href="/ui/base/hotkey_controller.html"> |
| <link rel="import" href="/ui/base/mouse_tracker.html"> |
| <link rel="import" href="/ui/base/ui.html"> |
| <link rel="import" href="/ui/base/utils.html"> |
| <link rel="import" href="/ui/base/mouse_modes.html"> |
| <link rel="import" href="/ui/base/mouse_mode_icon.html"> |
| |
| <polymer-element name="tr-ui-b-mouse-mode-selector"> |
| <template> |
| <style> |
| :host { |
| |
| -webkit-user-drag: element; |
| -webkit-user-select: none; |
| |
| background: #DDD; |
| border: 1px solid #BBB; |
| border-radius: 4px; |
| box-shadow: 0 1px 2px rgba(0,0,0,0.2); |
| left: calc(100% - 120px); |
| position: absolute; |
| top: 100px; |
| user-select: none; |
| width: 29px; |
| z-index: 20; |
| } |
| |
| .drag-handle { |
| background: url(../images/ui-states.png) 2px 3px no-repeat; |
| background-repeat: no-repeat; |
| border-bottom: 1px solid #BCBCBC; |
| cursor: move; |
| display: block; |
| height: 13px; |
| width: 27px; |
| } |
| |
| .tool-button { |
| background-position: center center; |
| background-repeat: no-repeat; |
| border-bottom: 1px solid #BCBCBC; |
| border-top: 1px solid #F1F1F1; |
| cursor: pointer; |
| } |
| |
| .buttons > .tool-button:last-child { |
| border-bottom: none; |
| } |
| |
| </style> |
| <div class="drag-handle"></div> |
| <div class="buttons"> |
| </div> |
| </template> |
| </polymer-element> |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.ui.b', function() { |
| var MOUSE_SELECTOR_MODE = tr.ui.b.MOUSE_SELECTOR_MODE; |
| var MOUSE_SELECTOR_MODE_INFOS = tr.ui.b.MOUSE_SELECTOR_MODE_INFOS; |
| |
| |
| var MIN_MOUSE_SELECTION_DISTANCE = 4; |
| |
| var MODIFIER = { |
| SHIFT: 0x1, |
| SPACE: 0x2, |
| CMD_OR_CTRL: 0x4 |
| }; |
| |
| /** |
| * Provides a panel for switching the interaction mode of the mouse. |
| * It handles the user interaction and dispatches events for the various |
| * modes. |
| */ |
| Polymer('tr-ui-b-mouse-mode-selector', { |
| __proto__: HTMLDivElement.prototype, |
| |
| created: function() { |
| this.supportedModeMask_ = MOUSE_SELECTOR_MODE.ALL_MODES; |
| |
| this.initialRelativeMouseDownPos_ = {x: 0, y: 0}; |
| |
| this.defaultMode_ = MOUSE_SELECTOR_MODE.PANSCAN; |
| this.settingsKey_ = undefined; |
| this.mousePos_ = {x: 0, y: 0}; |
| this.mouseDownPos_ = {x: 0, y: 0}; |
| |
| this.onMouseDown_ = this.onMouseDown_.bind(this); |
| this.onMouseMove_ = this.onMouseMove_.bind(this); |
| this.onMouseUp_ = this.onMouseUp_.bind(this); |
| |
| this.onKeyDown_ = this.onKeyDown_.bind(this); |
| this.onKeyUp_ = this.onKeyUp_.bind(this); |
| |
| this.mode_ = undefined; |
| this.modeToKeyCodeMap_ = {}; |
| this.modifierToModeMap_ = {}; |
| |
| this.targetElement_ = undefined; |
| this.modeBeforeAlternativeModeActivated_ = null; |
| |
| this.isInteracting_ = false; |
| this.isClick_ = false; |
| }, |
| |
| ready: function() { |
| this.buttonsEl_ = this.shadowRoot.querySelector('.buttons'); |
| this.dragHandleEl_ = this.shadowRoot.querySelector('.drag-handle'); |
| this.supportedModeMask = MOUSE_SELECTOR_MODE.ALL_MODES; |
| |
| this.dragHandleEl_.addEventListener('mousedown', |
| this.onDragHandleMouseDown_.bind(this)); |
| |
| this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_); |
| this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_); |
| this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this)); |
| }, |
| |
| attached: function() { |
| document.addEventListener('keydown', this.onKeyDown_); |
| document.addEventListener('keyup', this.onKeyUp_); |
| }, |
| |
| detached: function() { |
| document.removeEventListener('keydown', this.onKeyDown_); |
| document.removeEventListener('keyup', this.onKeyUp_); |
| }, |
| |
| get targetElement() { |
| return this.targetElement_; |
| }, |
| |
| set targetElement(target) { |
| if (this.targetElement_) |
| this.targetElement_.removeEventListener('mousedown', this.onMouseDown_); |
| this.targetElement_ = target; |
| if (this.targetElement_) |
| this.targetElement_.addEventListener('mousedown', this.onMouseDown_); |
| }, |
| |
| get defaultMode() { |
| return this.defaultMode_; |
| }, |
| |
| set defaultMode(defaultMode) { |
| this.defaultMode_ = defaultMode; |
| }, |
| |
| get settingsKey() { |
| return this.settingsKey_; |
| }, |
| |
| set settingsKey(settingsKey) { |
| this.settingsKey_ = settingsKey; |
| if (!this.settingsKey_) |
| return; |
| |
| var mode = tr.b.Settings.get(this.settingsKey_ + '.mode', undefined); |
| // Modes changed from 1,2,3,4 to 0x1, 0x2, 0x4, 0x8. Fix any stray |
| // settings to the best of our abilities. |
| if (MOUSE_SELECTOR_MODE_INFOS[mode] === undefined) |
| mode = undefined; |
| |
| // Restoring settings against unsupported modes should just go back to the |
| // default mode. |
| if ((mode & this.supportedModeMask_) === 0) |
| mode = undefined; |
| |
| if (!mode) |
| mode = this.defaultMode_; |
| this.mode = mode; |
| |
| var pos = tr.b.Settings.get(this.settingsKey_ + '.pos', undefined); |
| if (pos) |
| this.pos = pos; |
| }, |
| |
| get supportedModeMask() { |
| return this.supportedModeMask_; |
| }, |
| |
| /** |
| * Sets the supported modes. Should be an OR-ing of MOUSE_SELECTOR_MODE |
| * values. |
| */ |
| set supportedModeMask(supportedModeMask) { |
| if (this.mode && (supportedModeMask & this.mode) === 0) |
| throw new Error('supportedModeMask must include current mode.'); |
| |
| function createButtonForMode(mode) { |
| return button; |
| } |
| |
| this.supportedModeMask_ = supportedModeMask; |
| this.buttonsEl_.textContent = ''; |
| for (var modeName in MOUSE_SELECTOR_MODE) { |
| if (modeName == 'ALL_MODES') |
| continue; |
| var mode = MOUSE_SELECTOR_MODE[modeName]; |
| if ((this.supportedModeMask_ & mode) === 0) |
| continue; |
| |
| var button = document.createElement('tr-ui-b-mouse-mode-icon'); |
| button.mode = mode; |
| button.classList.add('tool-button'); |
| |
| this.buttonsEl_.appendChild(button); |
| } |
| }, |
| |
| getButtonForMode_: function(mode) { |
| for (var i = 0; i < this.buttonsEl_.children.length; i++) { |
| var buttonEl = this.buttonsEl_.children[i]; |
| if (buttonEl.mode === mode) |
| return buttonEl; |
| } |
| return undefined; |
| }, |
| |
| get mode() { |
| return this.currentMode_; |
| }, |
| |
| set mode(newMode) { |
| if (newMode !== undefined) { |
| if (typeof newMode !== 'number') |
| throw new Error('Mode must be a number'); |
| if ((newMode & this.supportedModeMask_) === 0) |
| throw new Error('Cannot switch to this mode, it is not supported'); |
| if (MOUSE_SELECTOR_MODE_INFOS[newMode] === undefined) |
| throw new Error('Unrecognized mode'); |
| } |
| |
| var modeInfo; |
| |
| if (this.currentMode_ === newMode) |
| return; |
| |
| if (this.currentMode_) { |
| var buttonEl = this.getButtonForMode_(this.currentMode_); |
| if (buttonEl) |
| buttonEl.active = false; |
| |
| // End event. |
| if (this.isInteracting_) { |
| |
| var mouseEvent = this.createEvent_( |
| MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end); |
| this.dispatchEvent(mouseEvent); |
| } |
| |
| // Exit event. |
| modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_]; |
| tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true); |
| } |
| |
| this.currentMode_ = newMode; |
| |
| if (this.currentMode_) { |
| var buttonEl = this.getButtonForMode_(this.currentMode_); |
| if (buttonEl) |
| buttonEl.active = true; |
| |
| // Entering a new mode resets mouse down pos. |
| this.mouseDownPos_.x = this.mousePos_.x; |
| this.mouseDownPos_.y = this.mousePos_.y; |
| |
| // Enter event. |
| modeInfo = MOUSE_SELECTOR_MODE_INFOS[this.currentMode_]; |
| if (!this.isInAlternativeMode_) |
| tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true); |
| |
| // Begin event. |
| if (this.isInteracting_) { |
| var mouseEvent = this.createEvent_( |
| MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin); |
| this.dispatchEvent(mouseEvent); |
| } |
| |
| |
| } |
| |
| if (this.settingsKey_ && !this.isInAlternativeMode_) |
| tr.b.Settings.set(this.settingsKey_ + '.mode', this.mode); |
| }, |
| |
| setKeyCodeForMode: function(mode, keyCode) { |
| if ((mode & this.supportedModeMask_) === 0) |
| throw new Error('Mode not supported'); |
| this.modeToKeyCodeMap_[mode] = keyCode; |
| |
| if (!this.buttonsEl_) |
| return; |
| |
| var buttonEl = this.getButtonForMode_(mode); |
| if (buttonEl) |
| buttonEl.acceleratorKey = String.fromCharCode(keyCode); |
| }, |
| |
| setCurrentMousePosFromEvent_: function(e) { |
| this.mousePos_.x = e.clientX; |
| this.mousePos_.y = e.clientY; |
| }, |
| |
| createEvent_: function(eventName, sourceEvent) { |
| var event = new tr.b.Event(eventName, true); |
| event.clientX = this.mousePos_.x; |
| event.clientY = this.mousePos_.y; |
| event.deltaX = this.mousePos_.x - this.mouseDownPos_.x; |
| event.deltaY = this.mousePos_.y - this.mouseDownPos_.y; |
| event.mouseDownX = this.mouseDownPos_.x; |
| event.mouseDownY = this.mouseDownPos_.y; |
| event.didPreventDefault = false; |
| event.preventDefault = function() { |
| event.didPreventDefault = true; |
| if (sourceEvent) |
| sourceEvent.preventDefault(); |
| }; |
| event.stopPropagation = function() { |
| sourceEvent.stopPropagation(); |
| }; |
| event.stopImmediatePropagation = function() { |
| throw new Error('Not implemented'); |
| }; |
| return event; |
| }, |
| |
| onMouseDown_: function(e) { |
| if (e.button !== 0) |
| return; |
| this.setCurrentMousePosFromEvent_(e); |
| var mouseEvent = this.createEvent_( |
| MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.begin, e); |
| this.dispatchEvent(mouseEvent); |
| this.isInteracting_ = true; |
| this.isClick_ = true; |
| tr.ui.b.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_); |
| }, |
| |
| onMouseMove_: function(e) { |
| this.setCurrentMousePosFromEvent_(e); |
| |
| var mouseEvent = this.createEvent_( |
| MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.update, e); |
| this.dispatchEvent(mouseEvent); |
| |
| if (this.isInteracting_) |
| this.checkIsClick_(e); |
| }, |
| |
| onMouseUp_: function(e) { |
| if (e.button !== 0) |
| return; |
| |
| var mouseEvent = this.createEvent_( |
| MOUSE_SELECTOR_MODE_INFOS[this.mode].eventNames.end, e); |
| mouseEvent.isClick = this.isClick_; |
| this.dispatchEvent(mouseEvent); |
| |
| if (this.isClick_ && !mouseEvent.didPreventDefault) |
| this.dispatchClickEvents_(e); |
| |
| this.isInteracting_ = false; |
| this.updateAlternativeModeState_(e); |
| }, |
| |
| onButtonMouseDown_: function(e) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| }, |
| |
| onButtonMouseUp_: function(e) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| }, |
| |
| onButtonPress_: function(e) { |
| this.modeBeforeAlternativeModeActivated_ = undefined; |
| this.mode = e.target.mode; |
| e.preventDefault(); |
| }, |
| |
| onKeyDown_: function(e) { |
| // Keys dispatched to INPUT elements still bubble, even when they're |
| // handled. So, skip any events that targeted the input element. |
| if (e.path[0].tagName == 'INPUT') |
| return; |
| |
| if (e.keyCode === ' '.charCodeAt(0)) |
| this.spacePressed_ = true; |
| this.updateAlternativeModeState_(e); |
| }, |
| |
| onKeyUp_: function(e) { |
| // Keys dispatched to INPUT elements still bubble, even when they're |
| // handled. So, skip any events that targeted the input element. |
| if (e.path[0].tagName == 'INPUT') |
| return; |
| |
| if (e.keyCode === ' '.charCodeAt(0)) |
| this.spacePressed_ = false; |
| |
| var didHandleKey = false; |
| tr.b.iterItems(this.modeToKeyCodeMap_, function(modeStr, keyCode) { |
| if (e.keyCode === keyCode) { |
| this.modeBeforeAlternativeModeActivated_ = undefined; |
| var mode = parseInt(modeStr); |
| this.mode = mode; |
| didHandleKey = true; |
| } |
| }, this); |
| |
| if (didHandleKey) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| return; |
| } |
| this.updateAlternativeModeState_(e); |
| }, |
| |
| updateAlternativeModeState_: function(e) { |
| var shiftPressed = e.shiftKey; |
| var spacePressed = this.spacePressed_; |
| var cmdOrCtrlPressed = |
| (tr.isMac && e.metaKey) || (!tr.isMac && e.ctrlKey); |
| |
| // Figure out the new mode |
| var smm = this.supportedModeMask_; |
| var newMode; |
| var isNewModeAnAlternativeMode = false; |
| if (shiftPressed && |
| (this.modifierToModeMap_[MODIFIER.SHIFT] & smm) !== 0) { |
| newMode = this.modifierToModeMap_[MODIFIER.SHIFT]; |
| isNewModeAnAlternativeMode = true; |
| } else if (spacePressed && |
| (this.modifierToModeMap_[MODIFIER.SPACE] & smm) !== 0) { |
| newMode = this.modifierToModeMap_[MODIFIER.SPACE]; |
| isNewModeAnAlternativeMode = true; |
| } else if (cmdOrCtrlPressed && |
| (this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL] & smm) !== 0) { |
| newMode = this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL]; |
| isNewModeAnAlternativeMode = true; |
| } else { |
| // Go to the old mode, if there is one. |
| if (this.isInAlternativeMode_) { |
| newMode = this.modeBeforeAlternativeModeActivated_; |
| isNewModeAnAlternativeMode = false; |
| } else { |
| newMode = undefined; |
| } |
| } |
| |
| // Maybe a mode change isn't needed. |
| if (this.mode === newMode || newMode === undefined) |
| return; |
| |
| // Okay, we're changing. |
| if (isNewModeAnAlternativeMode) |
| this.modeBeforeAlternativeModeActivated_ = this.mode; |
| this.mode = newMode; |
| }, |
| |
| get isInAlternativeMode_() { |
| return !!this.modeBeforeAlternativeModeActivated_; |
| }, |
| |
| setModifierForAlternateMode: function(mode, modifier) { |
| this.modifierToModeMap_[modifier] = mode; |
| }, |
| |
| get pos() { |
| return { |
| x: parseInt(this.style.left), |
| y: parseInt(this.style.top) |
| }; |
| }, |
| |
| set pos(pos) { |
| pos = this.constrainPositionToBounds_(pos); |
| |
| this.style.left = pos.x + 'px'; |
| this.style.top = pos.y + 'px'; |
| |
| if (this.settingsKey_) |
| tr.b.Settings.set(this.settingsKey_ + '.pos', this.pos); |
| }, |
| |
| constrainPositionToBounds_: function(pos) { |
| var parent = this.offsetParent || document.body; |
| var parentRect = tr.ui.b.windowRectForElement(parent); |
| |
| var top = 0; |
| var bottom = parentRect.height - this.offsetHeight; |
| var left = 0; |
| var right = parentRect.width - this.offsetWidth; |
| |
| var res = {}; |
| res.x = Math.max(pos.x, left); |
| res.x = Math.min(res.x, right); |
| |
| res.y = Math.max(pos.y, top); |
| res.y = Math.min(res.y, bottom); |
| return res; |
| }, |
| |
| onDragHandleMouseDown_: function(e) { |
| e.preventDefault(); |
| e.stopImmediatePropagation(); |
| |
| var mouseDownPos = { |
| x: e.clientX - this.offsetLeft, |
| y: e.clientY - this.offsetTop |
| }; |
| tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { |
| var pos = {}; |
| pos.x = e.clientX - mouseDownPos.x; |
| pos.y = e.clientY - mouseDownPos.y; |
| this.pos = pos; |
| }.bind(this)); |
| }, |
| |
| checkIsClick_: function(e) { |
| if (!this.isInteracting_ || !this.isClick_) |
| return; |
| |
| var deltaX = this.mousePos_.x - this.mouseDownPos_.x; |
| var deltaY = this.mousePos_.y - this.mouseDownPos_.y; |
| var minDist = MIN_MOUSE_SELECTION_DISTANCE; |
| |
| if (deltaX * deltaX + deltaY * deltaY > minDist * minDist) |
| this.isClick_ = false; |
| }, |
| |
| dispatchClickEvents_: function(e) { |
| if (!this.isClick_) |
| return; |
| |
| var modeInfo = MOUSE_SELECTOR_MODE_INFOS[MOUSE_SELECTOR_MODE.SELECTION]; |
| var eventNames = modeInfo.eventNames; |
| |
| var mouseEvent = this.createEvent_(eventNames.begin); |
| this.dispatchEvent(mouseEvent); |
| |
| mouseEvent = this.createEvent_(eventNames.end); |
| this.dispatchEvent(mouseEvent); |
| } |
| }); |
| |
| return { |
| MIN_MOUSE_SELECTION_DISTANCE: MIN_MOUSE_SELECTION_DISTANCE, |
| MODIFIER: MODIFIER |
| }; |
| }); |
| </script> |