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 <noreply@anthropic.com>
This commit is contained in:
Serhiy Storchaka
2026-07-05 15:49:43 +03:00
committed by GitHub
parent 3d9d6c8ccb
commit 8f06112b8f
3 changed files with 31 additions and 4 deletions
+11 -3
View File
@@ -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')
+17 -1
View File
@@ -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 = "<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 @@ 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)
@@ -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.