Further overhaul and streamline handling of view and tool mouse-events.

This commit is contained in:
Jürg Lehni 2016-01-14 02:08:33 +01:00
parent 0743f1b7b8
commit d5f2ff479d
5 changed files with 90 additions and 108 deletions

View file

@ -572,7 +572,7 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{
// or by looking at the amount of elements in the arguments list, // or by looking at the amount of elements in the arguments list,
// or the passed array: // or the passed array:
return arg && arg.width !== undefined return arg && arg.width !== undefined
|| (Array.isArray(arg) ? arg : arguments).length == 4 || (Array.isArray(arg) ? arg : arguments).length === 4
? this._containsRectangle(Rectangle.read(arguments)) ? this._containsRectangle(Rectangle.read(arguments))
: this._containsPoint(Point.read(arguments)); : this._containsPoint(Point.read(arguments));
}, },

View file

@ -257,12 +257,15 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{
clear: function() { clear: function() {
// Remove all projects, views and tools. // Remove all projects, views and tools.
// This also removes the installed event handlers. // This also removes the installed event handlers.
for (var i = this.projects.length - 1; i >= 0; i--) var projects = this.projects,
this.projects[i].remove(); tools = this.tools,
for (var i = this.tools.length - 1; i >= 0; i--) palettes = this.palettes;
this.tools[i].remove(); for (var i = projects.length - 1; i >= 0; i--)
for (var i = this.palettes.length - 1; i >= 0; i--) projects[i].remove();
this.palettes[i].remove(); for (var i = tools.length - 1; i >= 0; i--)
tools[i].remove();
for (var i = palettes.length - 1; i >= 0; i--)
palettes[i].remove();
}, },
remove: function() { remove: function() {

View file

@ -136,11 +136,11 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
function(name) { function(name) {
this[name] = { this[name] = {
install: function(type) { install: function(type) {
this.getView()._installEvent(type); this.getView()._countItemEvent(type, 1);
}, },
uninstall: function(type) { uninstall: function(type) {
this.getView()._uninstallEvent(type); this.getView()._countItemEvent(type, -1);
} }
}; };
}, { }, {

View file

@ -46,9 +46,9 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
_class: 'Tool', _class: 'Tool',
_list: 'tools', _list: 'tools',
_reference: 'tool', _reference: 'tool',
_events: [ 'onActivate', 'onDeactivate', 'onEditOptions', _events: ['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onMouseMove',
'onMouseDown', 'onMouseUp', 'onMouseDrag', 'onMouseMove', 'onActivate', 'onDeactivate', 'onEditOptions', 'onKeyDown',
'onKeyDown', 'onKeyUp' ], 'onKeyUp'],
// DOCS: rewrite Tool constructor explanation // DOCS: rewrite Tool constructor explanation
initialize: function Tool(props) { initialize: function Tool(props) {

View file

@ -115,8 +115,8 @@ var View = Base.extend(Emitter, /** @lends View# */{
// Items that need the onFrame handler called on them // Items that need the onFrame handler called on them
this._frameItems = {}; this._frameItems = {};
this._frameItemCount = 0; this._frameItemCount = 0;
// Count the installed events, see _installEvent() / _uninstallEvent(). // Count the installed item events, see _countItemEvent().
this._eventCounters = {}; this._itemEvents = {};
}, },
/** /**
@ -152,15 +152,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
_events: Base.each(['onResize', 'onMouseDown', 'onMouseUp', 'onMouseMove', _events: Base.each(['onResize', 'onMouseDown', 'onMouseUp', 'onMouseMove',
'onMouseDrag', 'onMouseEnter', 'onMouseLeave'], 'onMouseDrag', 'onMouseEnter', 'onMouseLeave'],
function(name) { function(name) {
this[name] = { this[name] = {};
install: function(type) {
this._installEvent(type);
},
uninstall: function(type) {
this._uninstallEvent(type);
}
};
}, { }, {
onFrame: { onFrame: {
install: function() { install: function() {
@ -704,8 +696,7 @@ new function() { // Injection scope for mouse events on the browser
* delegation to view and tool objects. * delegation to view and tool objects.
*/ */
var tool, var prevFocus,
prevFocus,
tempFocus, tempFocus,
mouseDown = false; mouseDown = false;
@ -730,38 +721,8 @@ new function() { // Injection scope for mouse events on the browser
} }
} }
function handleEvent(type, view, event, point) {
var eventType = type === 'mousemove' && mouseDown ? 'mousedrag' : type,
project = paper.project,
tool = view._scope.tool,
prevent = false;
function handle(obj) {
prevent = obj._handleEvent(eventType, event, point) || prevent;
}
if (!point)
point = view.getEventPoint(event);
if (project)
project.removeOn(eventType);
// Always first call the view's mouse handlers, as required by
// CanvasView, and then handle the active tool, if any.
// No need to call view if it doesn't have event handlers for this type.
if (view._eventCounters[type])
handle(view);
if (tool)
handle(tool);
// Prevent default if at least one mouse event handler was called, to
// prevent scrolling on touch devices.
if (prevent)
event.preventDefault();
// In the end we always call update(), which only updates the view if
// anything has changed in the above calls.
view.update();
}
function handleMouseMove(view, event, point) { function handleMouseMove(view, event, point) {
handleEvent('mousemove', view, event, point); view._handleEvent('mousemove', event, point);
} }
// Touch handling inspired by Hammer.js // Touch handling inspired by Hammer.js
@ -822,7 +783,7 @@ new function() { // Injection scope for mouse events on the browser
// should receive keyboard input. // should receive keyboard input.
var view = View._focused = getView(event); var view = View._focused = getView(event);
mouseDown = true; mouseDown = true;
handleEvent('mousedown', view, event); view._handleEvent('mousedown', event);
}; };
docEvents[mousemove] = function(event) { docEvents[mousemove] = function(event) {
@ -853,7 +814,7 @@ new function() { // Injection scope for mouse events on the browser
docEvents[mouseup] = function(event) { docEvents[mouseup] = function(event) {
var view = View._focused; var view = View._focused;
if (view && mouseDown) if (view && mouseDown)
handleEvent('mouseup', view, event); view._handleEvent('mouseup', event);
mouseDown = false; mouseDown = false;
}; };
@ -877,17 +838,7 @@ new function() { // Injection scope for mouse events on the browser
fallbacks = { fallbacks = {
doubleclick: 'click', doubleclick: 'click',
mousedrag: 'mousemove' mousedrag: 'mousemove'
}, };
downPoint,
lastPoint,
downItem,
overItem,
dragItem,
clickItem,
clickTime,
dblClick,
overView;
// Returns true if event was stopped, false otherwise, whether handler was // Returns true if event was stopped, false otherwise, whether handler was
// called or not! // called or not!
@ -951,11 +902,11 @@ new function() { // Injection scope for mouse events on the browser
} }
/** /**
* Flags defining which native events are required by which Paper events * Lookup defining which native events are required by which item events.
* as required for counting amount of necessary natives events. * Required by code that is counting the amount of required natives events.
* The mapping is native -> virtual * The mapping is native -> virtual.
*/ */
var mouseFlags = { var itemEvents = {
mousedown: { mousedown: {
mousedown: 1, mousedown: 1,
mousedrag: 1, mousedrag: 1,
@ -976,30 +927,57 @@ new function() { // Injection scope for mouse events on the browser
} }
}; };
/**
* Various variables required by #_handleEvent()
*/
var downPoint,
lastPoint,
downItem,
overItem,
dragItem,
clickItem,
clickTime,
dblClick,
overView;
return { return {
_viewEvents: viewEvents, _viewEvents: viewEvents,
/** /**
* Private method to handle view and item events. * Private method to handle view and item events.
*
* @return {@true if the default event should be prevented}. This is if
* at least one event handler was called and none of the called
* handlers wants to enforce the default.
*/ */
_handleEvent: function(type, event, point) { _handleEvent: function(type, event, point) {
// Run the hit-test first var handleItems = this._itemEvents[type],
var hit = this._project.hitTest(point, { project = paper.project,
tool = this._scope.tool;
// If it's a native mousemove event but the mouse is clicke, convert
// it to a mousedrag.
// NOTE: emitEvent(), as well as Tool#_handleEvent() fall back to
// mousemove if the objects don't respond to mousedrag.
if (type === 'mousemove' && mouseDown)
type = 'mousedrag';
// Before handling events, process removeOn() calls for cleanup.
if (project)
project.removeOn(type);
if (!point)
point = this.getEventPoint(event);
// Run the hit-test on items first, but only if we're required to do
// so for this given mouse event, see #_countItemEvent().
var hit = handleItems && this._project.hitTest(point, {
tolerance: 0, tolerance: 0,
fill: true, fill: true,
stroke: true stroke: true
}), }),
item = hit && hit.item, item = hit && hit.item || undefined,
inView = this.getBounds().contains(point), inView = this.getBounds().contains(point),
wasInView = this === overView,
stopped = false, stopped = false,
mouse = {}; mouse = {};
// Create a simple lookup object to quickly check for different // Create a simple lookup object to quickly check for different
// mouse event types. // mouse event types.
mouse[type.substr(5)] = true; mouse[type.substr(5)] = true;
// Always first call the view's mouse handlers, as required by
// CanvasView, and then handle the active tool after, if any.
// Handle mousemove first, even if this is not actually a mousemove // Handle mousemove first, even if this is not actually a mousemove
// event but the mouse has moved since the last event, but do not // event but the mouse has moved since the last event, but do not
// allow it to stop the other events in that case. // allow it to stop the other events in that case.
@ -1015,13 +993,10 @@ new function() { // Injection scope for mouse events on the browser
emitEvent(item, 'mouseenter', event, point); emitEvent(item, 'mouseenter', event, point);
} }
overItem = item; overItem = item;
if (overView && !overView.getBounds().contains(point)) { if (wasInView ^ inView) {
emitEvent(overView, 'mouseleave', event, point); emitEvent(this, inView ? 'mouseenter' : 'mouseleave', event,
overView = null; point);
} overView = inView ? this : null;
if (this !== overView && inView) {
emitEvent(this, 'mouseenter', event, point);
overView = this;
} }
if (inView || mouse.drag && !lastPoint.equals(point)) if (inView || mouse.drag && !lastPoint.equals(point))
stopped = emitEvents(this, item, moveType, event, point, stopped = emitEvents(this, item, moveType, event, point,
@ -1034,13 +1009,15 @@ new function() { // Injection scope for mouse events on the browser
// See if we're clicking again on the same item, within the // See if we're clicking again on the same item, within the
// double-click time. Firefox uses 300ms as the max time // double-click time. Firefox uses 300ms as the max time
// difference: // difference:
dblClick = clickItem === item if (item) {
&& (Date.now() - clickTime < 300); dblClick = item === clickItem
downItem = clickItem = item; && (Date.now() - clickTime < 300);
downItem = clickItem = item;
// Only start dragging if the mousedown event has not
// stopped propagation.
dragItem = !stopped && item;
}
downPoint = lastPoint = point; downPoint = lastPoint = point;
// Only start dragging if the mousedown event has not
// stopped propagation.
dragItem = !stopped && item;
} else if (mouse.up) { } else if (mouse.up) {
// Emulate click / doubleclick, but only on item, not view // Emulate click / doubleclick, but only on item, not view
if (!stopped && item && item === downItem) { if (!stopped && item && item === downItem) {
@ -1053,27 +1030,29 @@ new function() { // Injection scope for mouse events on the browser
} }
} }
lastPoint = point; lastPoint = point;
return called && !enforced; // Now finally call the tool events, but filter mouse move events
// to only be fired if we're inside the view or if we just left it.
// Prevent default if at least one handler was called, and none of
// them enforces default, to prevent scrolling on touch devices.
if (tool && (mouse.move || inView || wasInView)
&& tool._handleEvent(type, event, point)
|| called && !enforced)
event.preventDefault();
// In the end we always call update(), which only updates the view
// if anything has changed in the above calls.
this.update();
}, },
_installEvent: function(type) { _countItemEvent: function(type, sign) {
// If the view requires counting of installed mouse events, // If the view requires counting of installed mouse events,
// increase the counters now according to mouseFlags // change the event counters now according to itemEvents.
var counters = this._eventCounters; var events = this._itemEvents;
for (var key in mouseFlags) { for (var key in itemEvents) {
counters[key] = (counters[key] || 0) events[key] = (events[key] || 0)
+ (mouseFlags[key][type] || 0); + (itemEvents[key][type] || 0) * sign;
} }
}, },
_uninstallEvent: function(type) {
// If the view requires counting of installed mouse events,
// decrease the counters now according to mouseFlags
var counters = this._eventCounters;
for (var key in mouseFlags)
counters[key] -= mouseFlags[key][type] || 0;
},
statics: { statics: {
/** /**
* Loops through all views and sets the focus on the first * Loops through all views and sets the focus on the first