ยปCore Development>Code coverage>Lib/tkinter/dnd.py

Python code coverage for Lib/tkinter/dnd.py

#countcontent
1n/a"""Drag-and-drop support for Tkinter.
2n/a
3n/aThis is very preliminary. I currently only support dnd *within* one
4n/aapplication, between different windows (or within the same window).
5n/a
6n/aI am trying to make this as generic as possible -- not dependent on
7n/athe use of a particular widget or icon type, etc. I also hope that
8n/athis will work with Pmw.
9n/a
10n/aTo enable an object to be dragged, you must create an event binding
11n/afor it that starts the drag-and-drop process. Typically, you should
12n/abind <ButtonPress> to a callback function that you write. The function
13n/ashould call Tkdnd.dnd_start(source, event), where 'source' is the
14n/aobject to be dragged, and 'event' is the event that invoked the call
15n/a(the argument to your callback function). Even though this is a class
16n/ainstantiation, the returned instance should not be stored -- it will
17n/abe kept alive automatically for the duration of the drag-and-drop.
18n/a
19n/aWhen a drag-and-drop is already in process for the Tk interpreter, the
20n/acall is *ignored*; this normally averts starting multiple simultaneous
21n/adnd processes, e.g. because different button callbacks all
22n/adnd_start().
23n/a
24n/aThe object is *not* necessarily a widget -- it can be any
25n/aapplication-specific object that is meaningful to potential
26n/adrag-and-drop targets.
27n/a
28n/aPotential drag-and-drop targets are discovered as follows. Whenever
29n/athe mouse moves, and at the start and end of a drag-and-drop move, the
30n/aTk widget directly under the mouse is inspected. This is the target
31n/awidget (not to be confused with the target object, yet to be
32n/adetermined). If there is no target widget, there is no dnd target
33n/aobject. If there is a target widget, and it has an attribute
34n/adnd_accept, this should be a function (or any callable object). The
35n/afunction is called as dnd_accept(source, event), where 'source' is the
36n/aobject being dragged (the object passed to dnd_start() above), and
37n/a'event' is the most recent event object (generally a <Motion> event;
38n/ait can also be <ButtonPress> or <ButtonRelease>). If the dnd_accept()
39n/afunction returns something other than None, this is the new dnd target
40n/aobject. If dnd_accept() returns None, or if the target widget has no
41n/adnd_accept attribute, the target widget's parent is considered as the
42n/atarget widget, and the search for a target object is repeated from
43n/athere. If necessary, the search is repeated all the way up to the
44n/aroot widget. If none of the target widgets can produce a target
45n/aobject, there is no target object (the target object is None).
46n/a
47n/aThe target object thus produced, if any, is called the new target
48n/aobject. It is compared with the old target object (or None, if there
49n/awas no old target widget). There are several cases ('source' is the
50n/asource object, and 'event' is the most recent event object):
51n/a
52n/a- Both the old and new target objects are None. Nothing happens.
53n/a
54n/a- The old and new target objects are the same object. Its method
55n/adnd_motion(source, event) is called.
56n/a
57n/a- The old target object was None, and the new target object is not
58n/aNone. The new target object's method dnd_enter(source, event) is
59n/acalled.
60n/a
61n/a- The new target object is None, and the old target object is not
62n/aNone. The old target object's method dnd_leave(source, event) is
63n/acalled.
64n/a
65n/a- The old and new target objects differ and neither is None. The old
66n/atarget object's method dnd_leave(source, event), and then the new
67n/atarget object's method dnd_enter(source, event) is called.
68n/a
69n/aOnce this is done, the new target object replaces the old one, and the
70n/aTk mainloop proceeds. The return value of the methods mentioned above
71n/ais ignored; if they raise an exception, the normal exception handling
72n/amechanisms take over.
73n/a
74n/aThe drag-and-drop processes can end in two ways: a final target object
75n/ais selected, or no final target object is selected. When a final
76n/atarget object is selected, it will always have been notified of the
77n/apotential drop by a call to its dnd_enter() method, as described
78n/aabove, and possibly one or more calls to its dnd_motion() method; its
79n/adnd_leave() method has not been called since the last call to
80n/adnd_enter(). The target is notified of the drop by a call to its
81n/amethod dnd_commit(source, event).
82n/a
83n/aIf no final target object is selected, and there was an old target
84n/aobject, its dnd_leave(source, event) method is called to complete the
85n/adnd sequence.
86n/a
87n/aFinally, the source object is notified that the drag-and-drop process
88n/ais over, by a call to source.dnd_end(target, event), specifying either
89n/athe selected target object, or None if no target object was selected.
90n/aThe source object can use this to implement the commit action; this is
91n/asometimes simpler than to do it in the target's dnd_commit(). The
92n/atarget's dnd_commit() method could then simply be aliased to
93n/adnd_leave().
94n/a
95n/aAt any time during a dnd sequence, the application can cancel the
96n/asequence by calling the cancel() method on the object returned by
97n/adnd_start(). This will call dnd_leave() if a target is currently
98n/aactive; it will never call dnd_commit().
99n/a
100n/a"""
101n/a
102n/a
103n/aimport tkinter
104n/a
105n/a
106n/a# The factory function
107n/a
108n/adef dnd_start(source, event):
109n/a h = DndHandler(source, event)
110n/a if h.root:
111n/a return h
112n/a else:
113n/a return None
114n/a
115n/a
116n/a# The class that does the work
117n/a
118n/aclass DndHandler:
119n/a
120n/a root = None
121n/a
122n/a def __init__(self, source, event):
123n/a if event.num > 5:
124n/a return
125n/a root = event.widget._root()
126n/a try:
127n/a root.__dnd
128n/a return # Don't start recursive dnd
129n/a except AttributeError:
130n/a root.__dnd = self
131n/a self.root = root
132n/a self.source = source
133n/a self.target = None
134n/a self.initial_button = button = event.num
135n/a self.initial_widget = widget = event.widget
136n/a self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button)
137n/a self.save_cursor = widget['cursor'] or ""
138n/a widget.bind(self.release_pattern, self.on_release)
139n/a widget.bind("<Motion>", self.on_motion)
140n/a widget['cursor'] = "hand2"
141n/a
142n/a def __del__(self):
143n/a root = self.root
144n/a self.root = None
145n/a if root:
146n/a try:
147n/a del root.__dnd
148n/a except AttributeError:
149n/a pass
150n/a
151n/a def on_motion(self, event):
152n/a x, y = event.x_root, event.y_root
153n/a target_widget = self.initial_widget.winfo_containing(x, y)
154n/a source = self.source
155n/a new_target = None
156n/a while target_widget:
157n/a try:
158n/a attr = target_widget.dnd_accept
159n/a except AttributeError:
160n/a pass
161n/a else:
162n/a new_target = attr(source, event)
163n/a if new_target:
164n/a break
165n/a target_widget = target_widget.master
166n/a old_target = self.target
167n/a if old_target is new_target:
168n/a if old_target:
169n/a old_target.dnd_motion(source, event)
170n/a else:
171n/a if old_target:
172n/a self.target = None
173n/a old_target.dnd_leave(source, event)
174n/a if new_target:
175n/a new_target.dnd_enter(source, event)
176n/a self.target = new_target
177n/a
178n/a def on_release(self, event):
179n/a self.finish(event, 1)
180n/a
181n/a def cancel(self, event=None):
182n/a self.finish(event, 0)
183n/a
184n/a def finish(self, event, commit=0):
185n/a target = self.target
186n/a source = self.source
187n/a widget = self.initial_widget
188n/a root = self.root
189n/a try:
190n/a del root.__dnd
191n/a self.initial_widget.unbind(self.release_pattern)
192n/a self.initial_widget.unbind("<Motion>")
193n/a widget['cursor'] = self.save_cursor
194n/a self.target = self.source = self.initial_widget = self.root = None
195n/a if target:
196n/a if commit:
197n/a target.dnd_commit(source, event)
198n/a else:
199n/a target.dnd_leave(source, event)
200n/a finally:
201n/a source.dnd_end(target, event)
202n/a
203n/a
204n/a
205n/a# ----------------------------------------------------------------------
206n/a# The rest is here for testing and demonstration purposes only!
207n/a
208n/aclass Icon:
209n/a
210n/a def __init__(self, name):
211n/a self.name = name
212n/a self.canvas = self.label = self.id = None
213n/a
214n/a def attach(self, canvas, x=10, y=10):
215n/a if canvas is self.canvas:
216n/a self.canvas.coords(self.id, x, y)
217n/a return
218n/a if self.canvas:
219n/a self.detach()
220n/a if not canvas:
221n/a return
222n/a label = tkinter.Label(canvas, text=self.name,
223n/a borderwidth=2, relief="raised")
224n/a id = canvas.create_window(x, y, window=label, anchor="nw")
225n/a self.canvas = canvas
226n/a self.label = label
227n/a self.id = id
228n/a label.bind("<ButtonPress>", self.press)
229n/a
230n/a def detach(self):
231n/a canvas = self.canvas
232n/a if not canvas:
233n/a return
234n/a id = self.id
235n/a label = self.label
236n/a self.canvas = self.label = self.id = None
237n/a canvas.delete(id)
238n/a label.destroy()
239n/a
240n/a def press(self, event):
241n/a if dnd_start(self, event):
242n/a # where the pointer is relative to the label widget:
243n/a self.x_off = event.x
244n/a self.y_off = event.y
245n/a # where the widget is relative to the canvas:
246n/a self.x_orig, self.y_orig = self.canvas.coords(self.id)
247n/a
248n/a def move(self, event):
249n/a x, y = self.where(self.canvas, event)
250n/a self.canvas.coords(self.id, x, y)
251n/a
252n/a def putback(self):
253n/a self.canvas.coords(self.id, self.x_orig, self.y_orig)
254n/a
255n/a def where(self, canvas, event):
256n/a # where the corner of the canvas is relative to the screen:
257n/a x_org = canvas.winfo_rootx()
258n/a y_org = canvas.winfo_rooty()
259n/a # where the pointer is relative to the canvas widget:
260n/a x = event.x_root - x_org
261n/a y = event.y_root - y_org
262n/a # compensate for initial pointer offset
263n/a return x - self.x_off, y - self.y_off
264n/a
265n/a def dnd_end(self, target, event):
266n/a pass
267n/a
268n/aclass Tester:
269n/a
270n/a def __init__(self, root):
271n/a self.top = tkinter.Toplevel(root)
272n/a self.canvas = tkinter.Canvas(self.top, width=100, height=100)
273n/a self.canvas.pack(fill="both", expand=1)
274n/a self.canvas.dnd_accept = self.dnd_accept
275n/a
276n/a def dnd_accept(self, source, event):
277n/a return self
278n/a
279n/a def dnd_enter(self, source, event):
280n/a self.canvas.focus_set() # Show highlight border
281n/a x, y = source.where(self.canvas, event)
282n/a x1, y1, x2, y2 = source.canvas.bbox(source.id)
283n/a dx, dy = x2-x1, y2-y1
284n/a self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy)
285n/a self.dnd_motion(source, event)
286n/a
287n/a def dnd_motion(self, source, event):
288n/a x, y = source.where(self.canvas, event)
289n/a x1, y1, x2, y2 = self.canvas.bbox(self.dndid)
290n/a self.canvas.move(self.dndid, x-x1, y-y1)
291n/a
292n/a def dnd_leave(self, source, event):
293n/a self.top.focus_set() # Hide highlight border
294n/a self.canvas.delete(self.dndid)
295n/a self.dndid = None
296n/a
297n/a def dnd_commit(self, source, event):
298n/a self.dnd_leave(source, event)
299n/a x, y = source.where(self.canvas, event)
300n/a source.attach(self.canvas, x, y)
301n/a
302n/adef test():
303n/a root = tkinter.Tk()
304n/a root.geometry("+1+1")
305n/a tkinter.Button(command=root.quit, text="Quit").pack()
306n/a t1 = Tester(root)
307n/a t1.top.geometry("+1+60")
308n/a t2 = Tester(root)
309n/a t2.top.geometry("+120+60")
310n/a t3 = Tester(root)
311n/a t3.top.geometry("+240+60")
312n/a i1 = Icon("ICON1")
313n/a i2 = Icon("ICON2")
314n/a i3 = Icon("ICON3")
315n/a i1.attach(t1.canvas)
316n/a i2.attach(t2.canvas)
317n/a i3.attach(t3.canvas)
318n/a root.mainloop()
319n/a
320n/aif __name__ == '__main__':
321n/a test()