From d5f2ff479d496b74dd6ceb14da7ba85dc98f2c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 14 Jan 2016 02:08:33 +0100 Subject: [PATCH] Further overhaul and streamline handling of view and tool mouse-events. --- src/basic/Rectangle.js | 2 +- src/core/PaperScope.js | 15 ++-- src/item/Item.js | 4 +- src/tool/Tool.js | 6 +- src/view/View.js | 171 ++++++++++++++++++----------------------- 5 files changed, 90 insertions(+), 108 deletions(-) diff --git a/src/basic/Rectangle.js b/src/basic/Rectangle.js index 4b9033bf..fe9db86e 100644 --- a/src/basic/Rectangle.js +++ b/src/basic/Rectangle.js @@ -572,7 +572,7 @@ var Rectangle = Base.extend(/** @lends Rectangle# */{ // or by looking at the amount of elements in the arguments list, // or the passed array: 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._containsPoint(Point.read(arguments)); }, diff --git a/src/core/PaperScope.js b/src/core/PaperScope.js index 609d4e9e..3a13294d 100644 --- a/src/core/PaperScope.js +++ b/src/core/PaperScope.js @@ -257,12 +257,15 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{ clear: function() { // Remove all projects, views and tools. // This also removes the installed event handlers. - for (var i = this.projects.length - 1; i >= 0; i--) - this.projects[i].remove(); - for (var i = this.tools.length - 1; i >= 0; i--) - this.tools[i].remove(); - for (var i = this.palettes.length - 1; i >= 0; i--) - this.palettes[i].remove(); + var projects = this.projects, + tools = this.tools, + palettes = this.palettes; + for (var i = projects.length - 1; i >= 0; i--) + projects[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() { diff --git a/src/item/Item.js b/src/item/Item.js index 599c4596..4d5bdcf1 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -136,11 +136,11 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ function(name) { this[name] = { install: function(type) { - this.getView()._installEvent(type); + this.getView()._countItemEvent(type, 1); }, uninstall: function(type) { - this.getView()._uninstallEvent(type); + this.getView()._countItemEvent(type, -1); } }; }, { diff --git a/src/tool/Tool.js b/src/tool/Tool.js index 6b79061b..0508b19b 100644 --- a/src/tool/Tool.js +++ b/src/tool/Tool.js @@ -46,9 +46,9 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{ _class: 'Tool', _list: 'tools', _reference: 'tool', - _events: [ 'onActivate', 'onDeactivate', 'onEditOptions', - 'onMouseDown', 'onMouseUp', 'onMouseDrag', 'onMouseMove', - 'onKeyDown', 'onKeyUp' ], + _events: ['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onMouseMove', + 'onActivate', 'onDeactivate', 'onEditOptions', 'onKeyDown', + 'onKeyUp'], // DOCS: rewrite Tool constructor explanation initialize: function Tool(props) { diff --git a/src/view/View.js b/src/view/View.js index 7d37d5e9..a3a1a8ca 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -115,8 +115,8 @@ var View = Base.extend(Emitter, /** @lends View# */{ // Items that need the onFrame handler called on them this._frameItems = {}; this._frameItemCount = 0; - // Count the installed events, see _installEvent() / _uninstallEvent(). - this._eventCounters = {}; + // Count the installed item events, see _countItemEvent(). + this._itemEvents = {}; }, /** @@ -152,15 +152,7 @@ var View = Base.extend(Emitter, /** @lends View# */{ _events: Base.each(['onResize', 'onMouseDown', 'onMouseUp', 'onMouseMove', 'onMouseDrag', 'onMouseEnter', 'onMouseLeave'], function(name) { - this[name] = { - install: function(type) { - this._installEvent(type); - }, - - uninstall: function(type) { - this._uninstallEvent(type); - } - }; + this[name] = {}; }, { onFrame: { install: function() { @@ -704,8 +696,7 @@ new function() { // Injection scope for mouse events on the browser * delegation to view and tool objects. */ - var tool, - prevFocus, + var prevFocus, tempFocus, 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) { - handleEvent('mousemove', view, event, point); + view._handleEvent('mousemove', event, point); } // Touch handling inspired by Hammer.js @@ -822,7 +783,7 @@ new function() { // Injection scope for mouse events on the browser // should receive keyboard input. var view = View._focused = getView(event); mouseDown = true; - handleEvent('mousedown', view, event); + view._handleEvent('mousedown', event); }; docEvents[mousemove] = function(event) { @@ -853,7 +814,7 @@ new function() { // Injection scope for mouse events on the browser docEvents[mouseup] = function(event) { var view = View._focused; if (view && mouseDown) - handleEvent('mouseup', view, event); + view._handleEvent('mouseup', event); mouseDown = false; }; @@ -877,17 +838,7 @@ new function() { // Injection scope for mouse events on the browser fallbacks = { doubleclick: 'click', mousedrag: 'mousemove' - }, - downPoint, - lastPoint, - downItem, - overItem, - dragItem, - clickItem, - clickTime, - dblClick, - overView; - + }; // Returns true if event was stopped, false otherwise, whether handler was // 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 - * as required for counting amount of necessary natives events. - * The mapping is native -> virtual + * Lookup defining which native events are required by which item events. + * Required by code that is counting the amount of required natives events. + * The mapping is native -> virtual. */ - var mouseFlags = { + var itemEvents = { mousedown: { mousedown: 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 { _viewEvents: viewEvents, /** * 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) { - // Run the hit-test first - var hit = this._project.hitTest(point, { + var handleItems = this._itemEvents[type], + 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, fill: true, stroke: true }), - item = hit && hit.item, + item = hit && hit.item || undefined, inView = this.getBounds().contains(point), + wasInView = this === overView, stopped = false, mouse = {}; // Create a simple lookup object to quickly check for different // mouse event types. 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 // event but the mouse has moved since the last event, but do not // 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); } overItem = item; - if (overView && !overView.getBounds().contains(point)) { - emitEvent(overView, 'mouseleave', event, point); - overView = null; - } - if (this !== overView && inView) { - emitEvent(this, 'mouseenter', event, point); - overView = this; + if (wasInView ^ inView) { + emitEvent(this, inView ? 'mouseenter' : 'mouseleave', event, + point); + overView = inView ? this : null; } if (inView || mouse.drag && !lastPoint.equals(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 // double-click time. Firefox uses 300ms as the max time // difference: - dblClick = clickItem === item - && (Date.now() - clickTime < 300); - downItem = clickItem = item; + if (item) { + dblClick = item === clickItem + && (Date.now() - clickTime < 300); + downItem = clickItem = item; + // Only start dragging if the mousedown event has not + // stopped propagation. + dragItem = !stopped && item; + } downPoint = lastPoint = point; - // Only start dragging if the mousedown event has not - // stopped propagation. - dragItem = !stopped && item; } else if (mouse.up) { // Emulate click / doubleclick, but only on item, not view if (!stopped && item && item === downItem) { @@ -1053,27 +1030,29 @@ new function() { // Injection scope for mouse events on the browser } } 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, - // increase the counters now according to mouseFlags - var counters = this._eventCounters; - for (var key in mouseFlags) { - counters[key] = (counters[key] || 0) - + (mouseFlags[key][type] || 0); + // change the event counters now according to itemEvents. + var events = this._itemEvents; + for (var key in itemEvents) { + events[key] = (events[key] || 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: { /** * Loops through all views and sets the focus on the first