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.
8 <link rel="import" href="/tvcm/events.html">
9 <link rel="import" href="/tvcm/iteration_helpers.html">
10 <link rel="import" href="/tvcm/utils.html">
11 <link rel="import" href="/tvcm/key_event_manager.html">
12 <link rel="import" href="/tvcm/ui.html">
13 <link rel="import" href="/tvcm/ui/mouse_tracker.html">
15 <link rel="stylesheet" href="/tvcm/ui/mouse_mode_selector.css">
16 <link rel="stylesheet" href="/tvcm/ui/tool_button.css">
18 <template id="mouse-mode-selector-template">
19 <div class="drag-handle"></div>
27 tvcm.exportTo('tvcm.ui', function() {
29 var THIS_DOC = document.currentScript.ownerDocument;
31 var MIN_MOUSE_SELECTION_DISTANCE = 4;
33 var MOUSE_SELECTOR_MODE = {};
34 MOUSE_SELECTOR_MODE.SELECTION = 0x1;
35 MOUSE_SELECTOR_MODE.PANSCAN = 0x2;
36 MOUSE_SELECTOR_MODE.ZOOM = 0x4;
37 MOUSE_SELECTOR_MODE.TIMING = 0x8;
38 MOUSE_SELECTOR_MODE.ROTATE = 0x10;
39 MOUSE_SELECTOR_MODE.ALL_MODES = 0x1F;
42 allModeInfo[MOUSE_SELECTOR_MODE.PANSCAN] = {
44 className: 'pan-scan-mode-button',
53 allModeInfo[MOUSE_SELECTOR_MODE.SELECTION] = {
55 className: 'selection-mode-button',
57 enter: 'enterselection',
58 begin: 'beginselection',
59 update: 'updateselection',
65 allModeInfo[MOUSE_SELECTOR_MODE.ZOOM] = {
67 className: 'zoom-mode-button',
76 allModeInfo[MOUSE_SELECTOR_MODE.TIMING] = {
78 className: 'timing-mode-button',
82 update: 'updatetiming',
87 allModeInfo[MOUSE_SELECTOR_MODE.ROTATE] = {
89 className: 'rotate-mode-button',
93 update: 'updaterotate',
106 * Provides a panel for switching the interaction mode of the mouse.
107 * It handles the user interaction and dispatches events for the various
111 * @extends {HTMLDivElement}
113 var MouseModeSelector = tvcm.ui.define('div');
115 MouseModeSelector.prototype = {
116 __proto__: HTMLDivElement.prototype,
118 decorate: function(opt_targetElement) {
119 this.classList.add('mouse-mode-selector');
121 var node = tvcm.instantiateTemplate('#mouse-mode-selector-template',
123 this.appendChild(node);
125 this.buttonsEl_ = this.querySelector('.buttons');
126 this.dragHandleEl_ = this.querySelector('.drag-handle');
128 this.supportedModeMask = MOUSE_SELECTOR_MODE.ALL_MODES;
130 this.initialRelativeMouseDownPos_ = {x: 0, y: 0};
132 this.defaultMode_ = MOUSE_SELECTOR_MODE.PANSCAN;
133 this.settingsKey_ = undefined;
134 this.mousePos_ = {x: 0, y: 0};
135 this.mouseDownPos_ = {x: 0, y: 0};
137 this.dragHandleEl_.addEventListener('mousedown',
138 this.onDragHandleMouseDown_.bind(this));
140 this.onMouseDown_ = this.onMouseDown_.bind(this);
141 this.onMouseMove_ = this.onMouseMove_.bind(this);
142 this.onMouseUp_ = this.onMouseUp_.bind(this);
144 this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_);
145 this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_);
146 this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this));
148 tvcm.KeyEventManager.instance.addListener(
149 'keydown', this.onKeyDown_, this);
150 tvcm.KeyEventManager.instance.addListener(
151 'keyup', this.onKeyUp_, this);
153 this.mode_ = undefined;
154 this.modeToKeyCodeMap_ = {};
155 this.modifierToModeMap_ = {};
157 this.targetElement = opt_targetElement;
158 this.spacePressed_ = false;
159 this.modeBeforeAlternativeModeActivated_ = null;
161 this.isInteracting_ = false;
162 this.isClick_ = false;
165 get targetElement() {
166 return this.targetElement_;
169 set targetElement(target) {
170 if (this.targetElement_)
171 this.targetElement_.removeEventListener('mousedown', this.onMouseDown_);
172 this.targetElement_ = target;
173 if (this.targetElement_)
174 this.targetElement_.addEventListener('mousedown', this.onMouseDown_);
178 return this.defaultMode_;
181 set defaultMode(defaultMode) {
182 this.defaultMode_ = defaultMode;
186 return this.settingsKey_;
189 set settingsKey(settingsKey) {
190 this.settingsKey_ = settingsKey;
191 if (!this.settingsKey_)
194 var mode = tvcm.Settings.get(this.settingsKey_ + '.mode', undefined);
195 // Modes changed from 1,2,3,4 to 0x1, 0x2, 0x4, 0x8. Fix any stray
196 // settings to the best of our abilities.
197 if (allModeInfo[mode] === undefined)
200 // Restoring settings against unsupported modes should just go back to the
202 if ((mode & this.supportedModeMask_) === 0)
206 mode = this.defaultMode_;
209 var pos = tvcm.Settings.get(this.settingsKey_ + '.pos', undefined);
214 get supportedModeMask() {
215 return this.supportedModeMask_;
219 * Sets the supported modes. Should be an OR-ing of MOUSE_SELECTOR_MODE
222 set supportedModeMask(supportedModeMask) {
223 if (this.mode && (supportedModeMask & this.mode) === 0)
224 throw new Error('supportedModeMask must include current mode.');
226 function createButtonForMode(mode) {
227 var button = document.createElement('div');
229 button.title = allModeInfo[mode].title;
230 button.classList.add('tool-button');
231 button.classList.add(allModeInfo[mode].className);
235 this.supportedModeMask_ = supportedModeMask;
236 this.buttonsEl_.textContent = '';
237 for (var modeName in MOUSE_SELECTOR_MODE) {
238 if (modeName == 'ALL_MODES')
240 var mode = MOUSE_SELECTOR_MODE[modeName];
241 if ((this.supportedModeMask_ & mode) === 0)
243 this.buttonsEl_.appendChild(createButtonForMode(mode));
248 return this.currentMode_;
252 if (newMode !== undefined) {
253 if (typeof newMode !== 'number')
254 throw new Error('Mode must be a number');
255 if ((newMode & this.supportedModeMask_) === 0)
256 throw new Error('Cannot switch to this mode, it is not supported');
257 if (allModeInfo[newMode] === undefined)
258 throw new Error('Unrecognized mode');
263 if (this.currentMode_ === newMode)
266 if (this.currentMode_) {
267 modeInfo = allModeInfo[this.currentMode_];
268 var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className);
270 buttonEl.classList.remove('active');
273 if (this.isInteracting_) {
275 var mouseEvent = this.createEvent_(
276 allModeInfo[this.mode].eventNames.end);
277 this.dispatchEvent(mouseEvent);
281 tvcm.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true);
284 this.currentMode_ = newMode;
286 if (this.currentMode_) {
287 modeInfo = allModeInfo[this.currentMode_];
288 var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className);
290 buttonEl.classList.add('active');
292 // Entering a new mode resets mouse down pos.
293 this.mouseDownPos_.x = this.mousePos_.x;
294 this.mouseDownPos_.y = this.mousePos_.y;
297 if (!this.isInAlternativeMode_)
298 tvcm.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true);
301 if (this.isInteracting_) {
302 var mouseEvent = this.createEvent_(
303 allModeInfo[this.mode].eventNames.begin);
304 this.dispatchEvent(mouseEvent);
310 if (this.settingsKey_ && !this.isInAlternativeMode_)
311 tvcm.Settings.set(this.settingsKey_ + '.mode', this.mode);
314 setKeyCodeForMode: function(mode, keyCode) {
315 if ((mode & this.supportedModeMask_) === 0)
316 throw new Error('Mode not supported');
317 this.modeToKeyCodeMap_[mode] = keyCode;
319 if (!this.buttonsEl_)
322 var modeInfo = allModeInfo[mode];
323 var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className);
326 modeInfo.title + ' (' + String.fromCharCode(keyCode) + ')';
330 setCurrentMousePosFromEvent_: function(e) {
331 this.mousePos_.x = e.clientX;
332 this.mousePos_.y = e.clientY;
335 createEvent_: function(eventName, sourceEvent) {
336 var event = new tvcm.Event(eventName, true);
337 event.clientX = this.mousePos_.x;
338 event.clientY = this.mousePos_.y;
339 event.deltaX = this.mousePos_.x - this.mouseDownPos_.x;
340 event.deltaY = this.mousePos_.y - this.mouseDownPos_.y;
341 event.mouseDownX = this.mouseDownPos_.x;
342 event.mouseDownY = this.mouseDownPos_.y;
343 event.didPreventDefault = false;
344 event.preventDefault = function() {
345 event.didPreventDefault = true;
347 sourceEvent.preventDefault();
349 event.stopPropagation = function() {
350 sourceEvent.stopPropagation();
352 event.stopImmediatePropagation = function() {
353 throw new Error('Not implemented');
358 onMouseDown_: function(e) {
361 this.setCurrentMousePosFromEvent_(e);
362 var mouseEvent = this.createEvent_(
363 allModeInfo[this.mode].eventNames.begin, e);
364 this.dispatchEvent(mouseEvent);
365 this.isInteracting_ = true;
366 this.isClick_ = true;
367 tvcm.ui.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_);
370 onMouseMove_: function(e) {
371 this.setCurrentMousePosFromEvent_(e);
373 var mouseEvent = this.createEvent_(
374 allModeInfo[this.mode].eventNames.update, e);
375 this.dispatchEvent(mouseEvent);
377 if (this.isInteracting_)
378 this.checkIsClick_(e);
381 onMouseUp_: function(e) {
385 var mouseEvent = this.createEvent_(
386 allModeInfo[this.mode].eventNames.end, e);
387 mouseEvent.isClick = this.isClick_;
388 this.dispatchEvent(mouseEvent);
390 if (this.isClick_ && !mouseEvent.didPreventDefault)
391 this.dispatchClickEvents_(e);
393 this.isInteracting_ = false;
394 this.updateAlternativeModeState_(e);
397 onButtonMouseDown_: function(e) {
399 e.stopImmediatePropagation();
402 onButtonMouseUp_: function(e) {
404 e.stopImmediatePropagation();
407 onButtonPress_: function(e) {
408 this.modeBeforeAlternativeModeActivated_ = undefined;
409 this.mode = e.target.mode;
413 onKeyDown_: function(e) {
414 if (e.keyCode === ' '.charCodeAt(0))
415 this.spacePressed_ = true;
416 this.updateAlternativeModeState_(e);
419 onKeyUp_: function(e) {
420 if (e.keyCode === ' '.charCodeAt(0))
421 this.spacePressed_ = false;
423 var didHandleKey = false;
424 tvcm.iterItems(this.modeToKeyCodeMap_, function(modeStr, keyCode) {
425 if (e.keyCode === keyCode) {
426 this.modeBeforeAlternativeModeActivated_ = undefined;
427 var mode = parseInt(modeStr);
438 this.updateAlternativeModeState_(e);
441 updateAlternativeModeState_: function(e) {
442 var shiftPressed = e.shiftKey;
443 var spacePressed = this.spacePressed_;
444 var cmdOrCtrlPressed =
445 (tvcm.isMac && e.metaKey) || (!tvcm.isMac && e.ctrlKey);
447 // Figure out the new mode
448 var smm = this.supportedModeMask_;
450 var isNewModeAnAlternativeMode = false;
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;
464 // Go to the old mode, if there is one.
465 if (this.isInAlternativeMode_) {
466 newMode = this.modeBeforeAlternativeModeActivated_;
467 isNewModeAnAlternativeMode = false;
473 // Maybe a mode change isn't needed.
474 if (this.mode === newMode || newMode === undefined)
477 // Okay, we're changing.
478 if (isNewModeAnAlternativeMode)
479 this.modeBeforeAlternativeModeActivated_ = this.mode;
483 get isInAlternativeMode_() {
484 return !!this.modeBeforeAlternativeModeActivated_;
487 setModifierForAlternateMode: function(mode, modifier) {
488 this.modifierToModeMap_[modifier] = mode;
493 x: parseInt(this.style.left),
494 y: parseInt(this.style.top)
499 pos = this.constrainPositionToBounds_(pos);
501 this.style.left = pos.x + 'px';
502 this.style.top = pos.y + 'px';
504 if (this.settingsKey_)
505 tvcm.Settings.set(this.settingsKey_ + '.pos', this.pos);
508 constrainPositionToBounds_: function(pos) {
509 var parent = this.offsetParent || document.body;
510 var parentRect = tvcm.windowRectForElement(parent);
513 var bottom = parentRect.height - this.offsetHeight;
515 var right = parentRect.width - this.offsetWidth;
518 res.x = Math.max(pos.x, left);
519 res.x = Math.min(res.x, right);
521 res.y = Math.max(pos.y, top);
522 res.y = Math.min(res.y, bottom);
526 onDragHandleMouseDown_: function(e) {
528 e.stopImmediatePropagation();
531 x: e.clientX - this.offsetLeft,
532 y: e.clientY - this.offsetTop
534 tvcm.ui.trackMouseMovesUntilMouseUp(function(e) {
536 pos.x = e.clientX - mouseDownPos.x;
537 pos.y = e.clientY - mouseDownPos.y;
542 checkIsClick_: function(e) {
543 if (!this.isInteracting_ || !this.isClick_)
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;
550 if (deltaX * deltaX + deltaY * deltaY > minDist * minDist)
551 this.isClick_ = false;
554 dispatchClickEvents_: function(e) {
558 var eventNames = allModeInfo[MOUSE_SELECTOR_MODE.SELECTION].eventNames;
560 var mouseEvent = this.createEvent_(eventNames.begin);
561 this.dispatchEvent(mouseEvent);
563 mouseEvent = this.createEvent_(eventNames.end);
564 this.dispatchEvent(mouseEvent);
569 MIN_MOUSE_SELECTION_DISTANCE: MIN_MOUSE_SELECTION_DISTANCE,
570 MouseModeSelector: MouseModeSelector,
571 MOUSE_SELECTOR_MODE: MOUSE_SELECTOR_MODE,