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('', self.canvas.bind()) self.assertIn('', self.canvas.bind()) handler.cancel() self.assertNotIn('', self.canvas.bind()) self.assertNotIn('', 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()