mirror of
https://github.com/python/cpython.git
synced 2026-06-28 03:41:13 +00:00
Cover the drag cursor, the Motion and ButtonRelease bindings, switching
between targets, the target search up the master chain, dnd_accept()
returning None, and restarting after a drag has finished.
(cherry picked from commit 389e00f13f)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
171 lines
6.4 KiB
Python
171 lines
6.4 KiB
Python
import unittest
|
|
import tkinter
|
|
from tkinter import dnd
|
|
from test.support import requires
|
|
from test.test_tkinter.support import setUpModule # noqa: F401
|
|
from test.test_tkinter.support import AbstractTkTest
|
|
|
|
requires('gui')
|
|
|
|
|
|
class Target:
|
|
def __init__(self, widget, log):
|
|
self.widget = widget
|
|
self.log = log
|
|
widget.dnd_accept = self.dnd_accept
|
|
|
|
def dnd_accept(self, source, event):
|
|
self.log.append('accept')
|
|
return self
|
|
|
|
def dnd_enter(self, source, event):
|
|
self.log.append('enter')
|
|
|
|
def dnd_motion(self, source, event):
|
|
self.log.append('motion')
|
|
|
|
def dnd_leave(self, source, event):
|
|
self.log.append('leave')
|
|
|
|
def dnd_commit(self, source, event):
|
|
self.log.append('commit')
|
|
|
|
|
|
class Source:
|
|
def __init__(self, log):
|
|
self.log = log
|
|
|
|
def dnd_end(self, target, event):
|
|
self.log.append('end')
|
|
|
|
|
|
class FakeEvent:
|
|
def __init__(self, widget, num=1):
|
|
self.num = num
|
|
self.widget = widget
|
|
self.x = self.y = self.x_root = self.y_root = 0
|
|
|
|
|
|
class DndTest(AbstractTkTest, unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.canvas = tkinter.Canvas(self.root)
|
|
self.canvas.pack()
|
|
# on_motion() locates the target with winfo_containing(). Bypass that
|
|
# real screen lookup, which depends on the window being visible and
|
|
# unobscured, so the test does not hinge on the window manager.
|
|
self.canvas.winfo_containing = lambda x, y: self.canvas
|
|
self.log = []
|
|
self.source = Source(self.log)
|
|
self.target = Target(self.canvas, self.log)
|
|
|
|
def tearDown(self):
|
|
# Make sure no drag-and-drop is left active between tests: the
|
|
# recursion guard is a name-mangled attribute on the root.
|
|
try:
|
|
del self.root._DndHandler__dnd
|
|
except AttributeError:
|
|
pass
|
|
super().tearDown()
|
|
|
|
def test_drag_and_drop(self):
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
self.assertIsNotNone(handler)
|
|
handler.on_motion(FakeEvent(self.canvas)) # Enter the target.
|
|
handler.on_motion(FakeEvent(self.canvas)) # Move within the target.
|
|
handler.on_release(FakeEvent(self.canvas)) # Drop on the target.
|
|
self.assertEqual(self.log,
|
|
['accept', 'enter', 'accept', 'motion', 'commit', 'end'])
|
|
|
|
def test_cancel(self):
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
handler.on_motion(FakeEvent(self.canvas)) # Enter the target.
|
|
handler.cancel() # Leaves the target without committing.
|
|
self.assertEqual(self.log, ['accept', 'enter', 'leave', 'end'])
|
|
|
|
def test_no_target(self):
|
|
# Nothing under the pointer accepts the drag.
|
|
self.canvas.winfo_containing = lambda x, y: None
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
handler.on_motion(FakeEvent(self.canvas))
|
|
handler.on_release(FakeEvent(self.canvas))
|
|
self.assertEqual(self.log, ['end'])
|
|
|
|
def test_no_recursive_start(self):
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
self.assertIsNotNone(handler)
|
|
# A drag is already in progress, so a second start is ignored.
|
|
self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas)))
|
|
handler.cancel()
|
|
|
|
def test_high_button_number_ignored(self):
|
|
self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas, num=6)))
|
|
|
|
def test_restart_after_finish(self):
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
handler.cancel()
|
|
# Once a drag has finished a new one can start.
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
self.assertIsNotNone(handler)
|
|
handler.cancel()
|
|
|
|
def test_drag_cursor(self):
|
|
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']), 'hand2')
|
|
handler.cancel()
|
|
self.assertEqual(str(self.canvas['cursor']), 'watch')
|
|
|
|
def test_bindings_added_and_removed(self):
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
self.assertIn('<Motion>', self.canvas.bind())
|
|
self.assertIn('<B1-ButtonRelease-1>', self.canvas.bind())
|
|
handler.cancel()
|
|
self.assertNotIn('<Motion>', self.canvas.bind())
|
|
self.assertNotIn('<B1-ButtonRelease-1>', self.canvas.bind())
|
|
|
|
def test_switch_target(self):
|
|
log1, log2 = [], []
|
|
w1, w2 = tkinter.Frame(self.root), tkinter.Frame(self.root)
|
|
target1, target2 = Target(w1, log1), Target(w2, log2)
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
self.canvas.winfo_containing = lambda x, y: w1
|
|
handler.on_motion(FakeEvent(self.canvas)) # Enter target1.
|
|
self.canvas.winfo_containing = lambda x, y: w2
|
|
handler.on_motion(FakeEvent(self.canvas)) # Leave target1, enter target2.
|
|
self.assertIs(handler.target, target2)
|
|
self.assertEqual(log1, ['accept', 'enter', 'leave'])
|
|
self.assertEqual(log2, ['accept', 'enter'])
|
|
handler.cancel()
|
|
|
|
def test_target_in_ancestor(self):
|
|
# The widget under the pointer has no dnd_accept, but an ancestor
|
|
# does: the search walks up the master chain to find it.
|
|
parent = tkinter.Frame(self.root)
|
|
target = Target(parent, self.log)
|
|
child = tkinter.Frame(parent)
|
|
self.canvas.winfo_containing = lambda x, y: child
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
handler.on_motion(FakeEvent(self.canvas))
|
|
self.assertIs(handler.target, target)
|
|
self.assertEqual(self.log, ['accept', 'enter'])
|
|
handler.cancel()
|
|
|
|
def test_accept_returning_none_continues(self):
|
|
# dnd_accept() returning None means "not me, keep looking up".
|
|
parent = tkinter.Frame(self.root)
|
|
target = Target(parent, self.log)
|
|
child = tkinter.Frame(parent)
|
|
child.dnd_accept = lambda source, event: None
|
|
self.canvas.winfo_containing = lambda x, y: child
|
|
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
|
|
handler.on_motion(FakeEvent(self.canvas))
|
|
self.assertIs(handler.target, target)
|
|
handler.cancel()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|