blob: c29a04f5861714ecc5d784e22b7e466a32b2ebf0 [file] [log] [blame]
<!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>