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 = "" % (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 @@ 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.