From: Serhiy Storchaka Date: Sun, 5 Jul 2026 12:49:43 +0000 (+0300) Subject: gh-43699: Defer the drag cursor in tkinter.dnd until the pointer moves (GH-152371) X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=8f06112b8fbc716b2f11fc77fe5fa0731e4f5cd3;p=thirdparty%2FPython%2Fcpython.git 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 --- diff --git a/Lib/test/test_tkinter/test_dnd.py b/Lib/test/test_tkinter/test_dnd.py index fe3685aefe48..79c868b68562 100644 --- a/Lib/test/test_tkinter/test_dnd.py +++ b/Lib/test/test_tkinter/test_dnd.py @@ -40,10 +40,12 @@ class Source: 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 @@ class DndTest(AbstractTkTest, unittest.TestCase): 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 acec61ba71f1..3a306705c9b1 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 @@ class DndHandler: 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 = "" % (button, button) self.save_cursor = widget['cursor'] or "" widget.bind(self.release_pattern, self.on_release) widget.bind("", self.on_motion) - widget['cursor'] = "hand2" def __del__(self): root = self.root @@ -175,6 +180,17 @@ class DndHandler: 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 000000000000..93a9864d2445 --- /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.