From d5f2ff479d496b74dd6ceb14da7ba85dc98f2c55 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BCrg=20Lehni?= <juerg@scratchdisk.com>
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