bpo-37902: IDLE: Add scrolling for IDLE browsers. (GH-15368)


Modify the wheel event handler so it can also be used for module, path, and stack browsers.
Patch by George Zhang.
(cherry picked from commit 2cd902585815582eb059e3b40e014ebe4e7fdee7)

Co-authored-by: GeeTransit <geetransit@gmail.com>
diff --git a/Lib/idlelib/NEWS.txt b/Lib/idlelib/NEWS.txt
index 47c2291..c9e846a 100644
--- a/Lib/idlelib/NEWS.txt
+++ b/Lib/idlelib/NEWS.txt
@@ -3,6 +3,9 @@
 ======================================
 
 
+bpo-37092: Add mousewheel scrolling for IDLE module, path, and stack
+browsers.  Patch by George Zhang.
+
 bpo-35771: To avoid occasional spurious test_idle failures on slower
 machines, increase the ``hover_delay`` in test_tooltip.
 
diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py
index 793ed3a..5cbf704 100644
--- a/Lib/idlelib/editor.py
+++ b/Lib/idlelib/editor.py
@@ -26,6 +26,7 @@
 from idlelib import query
 from idlelib import replace
 from idlelib import search
+from idlelib.tree import wheel_event
 from idlelib import window
 
 # The default tab setting for a Text widget, in average-width characters.
@@ -151,9 +152,10 @@
         else:
             # Elsewhere, use right-click for popup menus.
             text.bind("<3>",self.right_menu_event)
-        text.bind('<MouseWheel>', self.mousescroll)
-        text.bind('<Button-4>', self.mousescroll)
-        text.bind('<Button-5>', self.mousescroll)
+
+        text.bind('<MouseWheel>', wheel_event)
+        text.bind('<Button-4>', wheel_event)
+        text.bind('<Button-5>', wheel_event)
         text.bind('<Configure>', self.handle_winconfig)
         text.bind("<<cut>>", self.cut)
         text.bind("<<copy>>", self.copy)
@@ -502,23 +504,6 @@
         self.text.yview(event, *args)
         return 'break'
 
-    def mousescroll(self, event):
-        """Handle scrollwheel event.
-
-        For wheel up, event.delta = 120*n on Windows, -1*n on darwin,
-        where n can be > 1 if one scrolls fast.  Flicking the wheel
-        generates up to maybe 20 events with n up to 10 or more 1.
-        Macs use wheel down (delta = 1*n) to scroll up, so positive
-        delta means to scroll up on both systems.
-
-        X-11 sends Control-Button-4 event instead.
-        """
-        up = {EventType.MouseWheel: event.delta > 0,
-              EventType.Button: event.num == 4}
-        lines = -5 if up[event.type] else 5
-        self.text.yview_scroll(lines, 'units')
-        return 'break'
-
     rmenu = None
 
     def right_menu_event(self, event):
diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py
index 68156a7..ba582bb 100644
--- a/Lib/idlelib/idle_test/test_multicall.py
+++ b/Lib/idlelib/idle_test/test_multicall.py
@@ -35,6 +35,14 @@
         mctext = self.mc(self.root)
         self.assertIsInstance(mctext._MultiCall__binders, list)
 
+    def test_yview(self):
+        # Added for tree.wheel_event
+        # (it depends on yview to not be overriden)
+        mc = self.mc
+        self.assertIs(mc.yview, Text.yview)
+        mctext = self.mc(self.root)
+        self.assertIs(mctext.yview.__func__, Text.yview)
+
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_tree.py b/Lib/idlelib/idle_test/test_tree.py
index 9be9abe..b3e4c10 100644
--- a/Lib/idlelib/idle_test/test_tree.py
+++ b/Lib/idlelib/idle_test/test_tree.py
@@ -4,7 +4,7 @@
 import unittest
 from test.support import requires
 requires('gui')
-from tkinter import Tk
+from tkinter import Tk, EventType, SCROLL
 
 
 class TreeTest(unittest.TestCase):
@@ -29,5 +29,32 @@
         node.expand()
 
 
+class TestScrollEvent(unittest.TestCase):
+
+    def test_wheel_event(self):
+        # Fake widget class containing `yview` only.
+        class _Widget:
+            def __init__(widget, *expected):
+                widget.expected = expected
+            def yview(widget, *args):
+                self.assertTupleEqual(widget.expected, args)
+        # Fake event class
+        class _Event:
+            pass
+        #        (type, delta, num, amount)
+        tests = ((EventType.MouseWheel, 120, -1, -5),
+                 (EventType.MouseWheel, -120, -1, 5),
+                 (EventType.ButtonPress, -1, 4, -5),
+                 (EventType.ButtonPress, -1, 5, 5))
+
+        event = _Event()
+        for ty, delta, num, amount in tests:
+            event.type = ty
+            event.delta = delta
+            event.num = num
+            res = tree.wheel_event(event, _Widget(SCROLL, amount, "units"))
+            self.assertEqual(res, "break")
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/Lib/idlelib/tree.py b/Lib/idlelib/tree.py
index 21426cb..6229be4 100644
--- a/Lib/idlelib/tree.py
+++ b/Lib/idlelib/tree.py
@@ -56,6 +56,30 @@
             column = 0
     root.images = images
 
+def wheel_event(event, widget=None):
+    """Handle scrollwheel event.
+
+    For wheel up, event.delta = 120*n on Windows, -1*n on darwin,
+    where n can be > 1 if one scrolls fast.  Flicking the wheel
+    generates up to maybe 20 events with n up to 10 or more 1.
+    Macs use wheel down (delta = 1*n) to scroll up, so positive
+    delta means to scroll up on both systems.
+
+    X-11 sends Control-Button-4,5 events instead.
+
+    The widget parameter is needed so browser label bindings can pass
+    the underlying canvas.
+
+    This function depends on widget.yview to not be overridden by
+    a subclass.
+    """
+    up = {EventType.MouseWheel: event.delta > 0,
+          EventType.ButtonPress: event.num == 4}
+    lines = -5 if up[event.type] else 5
+    widget = event.widget if widget is None else widget
+    widget.yview(SCROLL, lines, 'units')
+    return 'break'
+
 
 class TreeNode:
 
@@ -260,6 +284,9 @@
                                        anchor="nw", window=self.label)
         self.label.bind("<1>", self.select_or_edit)
         self.label.bind("<Double-1>", self.flip)
+        self.label.bind("<MouseWheel>", lambda e: wheel_event(e, self.canvas))
+        self.label.bind("<Button-4>", lambda e: wheel_event(e, self.canvas))
+        self.label.bind("<Button-5>", lambda e: wheel_event(e, self.canvas))
         self.text_id = id
 
     def select_or_edit(self, event=None):
@@ -410,6 +437,7 @@
 # A canvas widget with scroll bars and some useful bindings
 
 class ScrolledCanvas:
+
     def __init__(self, master, **opts):
         if 'yscrollincrement' not in opts:
             opts['yscrollincrement'] = 17
@@ -431,6 +459,9 @@
         self.canvas.bind("<Key-Next>", self.page_down)
         self.canvas.bind("<Key-Up>", self.unit_up)
         self.canvas.bind("<Key-Down>", self.unit_down)
+        self.canvas.bind("<MouseWheel>", wheel_event)
+        self.canvas.bind("<Button-4>", wheel_event)
+        self.canvas.bind("<Button-5>", wheel_event)
         #if isinstance(master, Toplevel) or isinstance(master, Tk):
         self.canvas.bind("<Alt-Key-2>", self.zoom_height)
         self.canvas.focus_set()
diff --git a/Misc/ACKS b/Misc/ACKS
index def874b..290f8ea 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1863,6 +1863,7 @@
 Yuxiao Zeng
 Uwe Zessin
 Cheng Zhang
+George Zhang
 Kai Zhu
 Tarek Ziadé
 Jelle Zijlstra
diff --git a/Misc/NEWS.d/next/IDLE/2019-08-21-16-02-49.bpo-37902._R_adE.rst b/Misc/NEWS.d/next/IDLE/2019-08-21-16-02-49.bpo-37902._R_adE.rst
new file mode 100644
index 0000000..24b4142
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2019-08-21-16-02-49.bpo-37902._R_adE.rst
@@ -0,0 +1,2 @@
+Add mousewheel scrolling for IDLE module, path, and stack browsers.
+Patch by George Zhang.