| <!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/events.html"> |
| <link rel="import" href="/base/iteration_helpers.html"> |
| <link rel="import" href="/base/utils.html"> |
| <link rel="import" href="/base/key_event_manager.html"> |
| <link rel="import" href="/base/ui.html"> |
| <link rel="import" href="/base/ui/mouse_tracker.html"> |
| |
| <link rel="stylesheet" href="/base/ui/mouse_mode_selector.css"> |
| <link rel="stylesheet" href="/base/ui/tool_button.css"> |
| |
| <template id="mouse-mode-selector-template"> |
| <div class="drag-handle"></div> |
| <div class="buttons"> |
| </div> |
| </template> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.b.ui', function() { |
| |
| var THIS_DOC = document.currentScript.ownerDocument; |
| |
| var MIN_MOUSE_SELECTION_DISTANCE = 4; |
| |
| var MOUSE_SELECTOR_MODE = {}; |
| MOUSE_SELECTOR_MODE.SELECTION = 0x1; |
| MOUSE_SELECTOR_MODE.PANSCAN = 0x2; |
| MOUSE_SELECTOR_MODE.ZOOM = 0x4; |
| MOUSE_SELECTOR_MODE.TIMING = 0x8; |
| MOUSE_SELECTOR_MODE.ROTATE = 0x10; |
| MOUSE_SELECTOR_MODE.ALL_MODES = 0x1F; |
| |
| var allModeInfo = {}; |
| allModeInfo[MOUSE_SELECTOR_MODE.PANSCAN] = { |
| title: 'pan', |
| className: 'pan-scan-mode-button', |
| eventNames: { |
| enter: 'enterpan', |
| begin: 'beginpan', |
| update: 'updatepan', |
| end: 'endpan', |
| exit: 'exitpan' |
| } |
| }; |
| allModeInfo[MOUSE_SELECTOR_MODE.SELECTION] = { |
| title: 'selection', |
| className: 'selection-mode-button', |
| eventNames: { |
| enter: 'enterselection', |
| begin: 'beginselection', |
| update: 'updateselection', |
| end: 'endselection', |
| exit: 'exitselection' |
| } |
| }; |
| |
| allModeInfo[MOUSE_SELECTOR_MODE.ZOOM] = { |
| title: 'zoom', |
| className: 'zoom-mode-button', |
| eventNames: { |
| enter: 'enterzoom', |
| begin: 'beginzoom', |
| update: 'updatezoom', |
| end: 'endzoom', |
| exit: 'exitzoom' |
| } |
| }; |
| allModeInfo[MOUSE_SELECTOR_MODE.TIMING] = { |
| title: 'timing', |
| className: 'timing-mode-button', |
| eventNames: { |
| enter: 'entertiming', |
| begin: 'begintiming', |
| update: 'updatetiming', |
| end: 'endtiming', |
| exit: 'exittiming' |
| } |
| }; |
| allModeInfo[MOUSE_SELECTOR_MODE.ROTATE] = { |
| title: 'rotate', |
| className: 'rotate-mode-button', |
| eventNames: { |
| enter: 'enterrotate', |
| begin: 'beginrotate', |
| update: 'updaterotate', |
| end: 'endrotate', |
| exit: 'exitrotate' |
| } |
| }; |
| |
| 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. |
| * |
| * @constructor |
| * @extends {HTMLDivElement} |
| */ |
| var MouseModeSelector = tr.b.ui.define('div'); |
| |
| MouseModeSelector.prototype = { |
| __proto__: HTMLDivElement.prototype, |
| |
| decorate: function(opt_targetElement) { |
| this.classList.add('mouse-mode-selector'); |
| |
| var node = tr.b.instantiateTemplate('#mouse-mode-selector-template', |
| THIS_DOC); |
| this.appendChild(node); |
| |
| this.buttonsEl_ = this.querySelector('.buttons'); |
| this.dragHandleEl_ = this.querySelector('.drag-handle'); |
| |
| 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.dragHandleEl_.addEventListener('mousedown', |
| this.onDragHandleMouseDown_.bind(this)); |
| |
| this.onMouseDown_ = this.onMouseDown_.bind(this); |
| this.onMouseMove_ = this.onMouseMove_.bind(this); |
| this.onMouseUp_ = this.onMouseUp_.bind(this); |
| |
| this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_); |
| this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_); |
| this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this)); |
| |
| tr.b.KeyEventManager.instance.addListener( |
| 'keydown', this.onKeyDown_, this); |
| tr.b.KeyEventManager.instance.addListener( |
| 'keyup', this.onKeyUp_, this); |
| this.keyCodeCondition = undefined; |
| |
| this.mode_ = undefined; |
| this.modeToKeyCodeMap_ = {}; |
| this.modifierToModeMap_ = {}; |
| |
| this.targetElement = opt_targetElement; |
| this.spacePressed_ = false; |
| this.modeBeforeAlternativeModeActivated_ = null; |
| |
| this.isInteracting_ = false; |
| this.isClick_ = false; |
| }, |
| |
| 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 (allModeInfo[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) { |
| var button = document.createElement('div'); |
| button.mode = mode; |
| button.title = allModeInfo[mode].title; |
| button.classList.add('tool-button'); |
| button.classList.add(allModeInfo[mode].className); |
| 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; |
| this.buttonsEl_.appendChild(createButtonForMode(mode)); |
| } |
| }, |
| |
| 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 (allModeInfo[newMode] === undefined) |
| throw new Error('Unrecognized mode'); |
| } |
| |
| var modeInfo; |
| |
| if (this.currentMode_ === newMode) |
| return; |
| |
| if (this.currentMode_) { |
| modeInfo = allModeInfo[this.currentMode_]; |
| var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className); |
| if (buttonEl) |
| buttonEl.classList.remove('active'); |
| |
| // End event. |
| if (this.isInteracting_) { |
| |
| var mouseEvent = this.createEvent_( |
| allModeInfo[this.mode].eventNames.end); |
| this.dispatchEvent(mouseEvent); |
| } |
| |
| // Exit event. |
| tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true); |
| } |
| |
| this.currentMode_ = newMode; |
| |
| if (this.currentMode_) { |
| modeInfo = allModeInfo[this.currentMode_]; |
| var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className); |
| if (buttonEl) |
| buttonEl.classList.add('active'); |
| |
| // Entering a new mode resets mouse down pos. |
| this.mouseDownPos_.x = this.mousePos_.x; |
| this.mouseDownPos_.y = this.mousePos_.y; |
| |
| // Enter event. |
| if (!this.isInAlternativeMode_) |
| tr.b.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true); |
| |
| // Begin event. |
| if (this.isInteracting_) { |
| var mouseEvent = this.createEvent_( |
| allModeInfo[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 modeInfo = allModeInfo[mode]; |
| var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className); |
| if (buttonEl) { |
| buttonEl.title = |
| modeInfo.title + ' (' + String.fromCharCode(keyCode) + ')'; |
| } |
| }, |
| |
| setKeyCodeCondition: function(callback) { |
| this.keyCodeCondition = callback; |
| }, |
| |
| 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_( |
| allModeInfo[this.mode].eventNames.begin, e); |
| this.dispatchEvent(mouseEvent); |
| this.isInteracting_ = true; |
| this.isClick_ = true; |
| tr.b.ui.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_); |
| }, |
| |
| onMouseMove_: function(e) { |
| this.setCurrentMousePosFromEvent_(e); |
| |
| var mouseEvent = this.createEvent_( |
| allModeInfo[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_( |
| allModeInfo[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) { |
| if (e.keyCode === ' '.charCodeAt(0)) |
| this.spacePressed_ = true; |
| this.updateAlternativeModeState_(e); |
| }, |
| |
| onKeyUp_: function(e) { |
| if (e.keyCode === ' '.charCodeAt(0)) |
| this.spacePressed_ = false; |
| |
| if (this.keyCodeCondition != undefined && !this.keyCodeCondition()) { |
| // If keyCodeCondition is false when the FindControl is active, |
| // ignore the keyUp event. |
| return; |
| } |
| |
| 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.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.b.ui.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 eventNames = allModeInfo[MOUSE_SELECTOR_MODE.SELECTION].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, |
| MouseModeSelector: MouseModeSelector, |
| MOUSE_SELECTOR_MODE: MOUSE_SELECTOR_MODE, |
| MODIFIER: MODIFIER |
| }; |
| }); |
| </script> |