https://github.com/python/cpython/commit/8f06112b8fbc716b2f11fc77fe5fa0731e4f5cd3
commit: 8f06112b8fbc716b2f11fc77fe5fa0731e4f5cd3
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-05T15:49:43+03:00
summary:

gh-43699: Defer the drag cursor in tkinter.dnd until the pointer moves 
(GH-152371)

The drag cursor is now changed only once the pointer starts moving past
a small threshold rather than on the initial button press, so that a
plain click no longer flashes it.

Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-27-12-00-00.gh-issue-43699.Zt46MI.rst
M Lib/test/test_tkinter/test_dnd.py
M Lib/tkinter/dnd.py

diff --git a/Lib/test/test_tkinter/test_dnd.py 
b/Lib/test/test_tkinter/test_dnd.py
index fe3685aefe48be9..79c868b68562bce 100644
--- a/Lib/test/test_tkinter/test_dnd.py
+++ b/Lib/test/test_tkinter/test_dnd.py
@@ -40,10 +40,12 @@ def dnd_end(self, target, event):
 
 
 class FakeEvent:
-    def __init__(self, widget, num=1):
+    def __init__(self, widget, num=1, x_root=0, y_root=0):
         self.num = num
         self.widget = widget
-        self.x = self.y = self.x_root = self.y_root = 0
+        self.x = self.y = 0
+        self.x_root = x_root
+        self.y_root = y_root
 
 
 class DndTest(AbstractTkTest, unittest.TestCase):
@@ -111,10 +113,16 @@ def test_restart_after_finish(self):
         handler.cancel()
 
     def test_drag_cursor(self):
+        # The drag cursor is not shown on the initial press, only once the
+        # pointer moves past the threshold, so a plain click does not flash
+        # it (gh-43699).  The original cursor is restored afterwards.
         self.canvas['cursor'] = 'watch'
         handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
-        # The drag cursor is shown while dragging, the original restored after.
         self.assertEqual(handler.save_cursor, 'watch')
+        self.assertEqual(str(self.canvas['cursor']), 'watch')
+        handler.on_motion(FakeEvent(self.canvas, x_root=2))  # below threshold
+        self.assertEqual(str(self.canvas['cursor']), 'watch')
+        handler.on_motion(FakeEvent(self.canvas, x_root=20))  # past threshold
         self.assertEqual(str(self.canvas['cursor']), 'hand2')
         handler.cancel()
         self.assertEqual(str(self.canvas['cursor']), 'watch')
diff --git a/Lib/tkinter/dnd.py b/Lib/tkinter/dnd.py
index acec61ba71f1c9f..3a306705c9b1063 100644
--- a/Lib/tkinter/dnd.py
+++ b/Lib/tkinter/dnd.py
@@ -120,6 +120,10 @@ class DndHandler:
 
     root = None
 
+    # The drag cursor is shown only once the pointer has moved this many
+    # pixels from the initial press, so that a plain click does not flash it.
+    threshold = 3
+
     def __init__(self, source, event):
         if event.num > 5:
             return
@@ -134,11 +138,12 @@ def __init__(self, source, event):
         self.target = None
         self.initial_button = button = event.num
         self.initial_widget = widget = event.widget
+        self.dragging = False
+        self.x_origin, self.y_origin = event.x_root, event.y_root
         self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button)
         self.save_cursor = widget['cursor'] or ""
         widget.bind(self.release_pattern, self.on_release)
         widget.bind("<Motion>", self.on_motion)
-        widget['cursor'] = "hand2"
 
     def __del__(self):
         root = self.root
@@ -175,6 +180,17 @@ def on_motion(self, event):
             if new_target is not None:
                 new_target.dnd_enter(source, event)
                 self.target = new_target
+        self.update_cursor(x, y)
+
+    def update_cursor(self, x, y):
+        # Show the drag cursor only once the pointer has actually started
+        # moving past the threshold, so that a plain click does not flash it.
+        if not self.dragging:
+            if (abs(x - self.x_origin) <= self.threshold and
+                    abs(y - self.y_origin) <= self.threshold):
+                return
+            self.dragging = True
+            self.initial_widget['cursor'] = "hand2"
 
     def on_release(self, event):
         self.finish(event, 1)
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-27-12-00-00.gh-issue-43699.Zt46MI.rst 
b/Misc/NEWS.d/next/Library/2026-06-27-12-00-00.gh-issue-43699.Zt46MI.rst
new file mode 100644
index 000000000000000..93a9864d2445e94
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-27-12-00-00.gh-issue-43699.Zt46MI.rst
@@ -0,0 +1,3 @@
+The drag cursor in :mod:`tkinter.dnd` is now changed only once the pointer
+starts moving rather than on the initial button press, so that a plain
+click no longer flashes it.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to