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