blob: 3120ff342f8c0e633760fddee569365647e6baae [file] [log] [blame]
Fred Draked038ca82000-10-23 18:31:14 +00001"""Drag-and-drop support for Tkinter.
2
Guido van Rossum4cee3c41998-04-10 16:14:34 +00003This is very preliminary. I currently only support dnd *within* one
4application, between different windows (or within the same window).
Fred Draked038ca82000-10-23 18:31:14 +00005
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +03006I am trying to make this as generic as possible -- not dependent on
Guido van Rossum4cee3c41998-04-10 16:14:34 +00007the use of a particular widget or icon type, etc. I also hope that
8this will work with Pmw.
Fred Draked038ca82000-10-23 18:31:14 +00009
Guido van Rossum4cee3c41998-04-10 16:14:34 +000010To enable an object to be dragged, you must create an event binding
11for it that starts the drag-and-drop process. Typically, you should
12bind <ButtonPress> to a callback function that you write. The function
13should call Tkdnd.dnd_start(source, event), where 'source' is the
14object 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
16instantiation, the returned instance should not be stored -- it will
17be kept alive automatically for the duration of the drag-and-drop.
18
19When a drag-and-drop is already in process for the Tk interpreter, the
20call is *ignored*; this normally averts starting multiple simultaneous
21dnd processes, e.g. because different button callbacks all
22dnd_start().
Fred Draked038ca82000-10-23 18:31:14 +000023
Guido van Rossum4cee3c41998-04-10 16:14:34 +000024The object is *not* necessarily a widget -- it can be any
25application-specific object that is meaningful to potential
26drag-and-drop targets.
Fred Draked038ca82000-10-23 18:31:14 +000027
Guido van Rossum4cee3c41998-04-10 16:14:34 +000028Potential drag-and-drop targets are discovered as follows. Whenever
29the mouse moves, and at the start and end of a drag-and-drop move, the
30Tk widget directly under the mouse is inspected. This is the target
31widget (not to be confused with the target object, yet to be
32determined). If there is no target widget, there is no dnd target
33object. If there is a target widget, and it has an attribute
34dnd_accept, this should be a function (or any callable object). The
35function is called as dnd_accept(source, event), where 'source' is the
36object being dragged (the object passed to dnd_start() above), and
37'event' is the most recent event object (generally a <Motion> event;
38it can also be <ButtonPress> or <ButtonRelease>). If the dnd_accept()
39function returns something other than None, this is the new dnd target
40object. If dnd_accept() returns None, or if the target widget has no
41dnd_accept attribute, the target widget's parent is considered as the
42target widget, and the search for a target object is repeated from
43there. If necessary, the search is repeated all the way up to the
44root widget. If none of the target widgets can produce a target
45object, there is no target object (the target object is None).
Fred Draked038ca82000-10-23 18:31:14 +000046
Guido van Rossum4cee3c41998-04-10 16:14:34 +000047The target object thus produced, if any, is called the new target
48object. It is compared with the old target object (or None, if there
49was no old target widget). There are several cases ('source' is the
50source object, and 'event' is the most recent event object):
Fred Draked038ca82000-10-23 18:31:14 +000051
Guido van Rossum4cee3c41998-04-10 16:14:34 +000052- Both the old and new target objects are None. Nothing happens.
Fred Draked038ca82000-10-23 18:31:14 +000053
Guido van Rossum4cee3c41998-04-10 16:14:34 +000054- The old and new target objects are the same object. Its method
55dnd_motion(source, event) is called.
Fred Draked038ca82000-10-23 18:31:14 +000056
Guido van Rossum4cee3c41998-04-10 16:14:34 +000057- The old target object was None, and the new target object is not
58None. The new target object's method dnd_enter(source, event) is
59called.
Fred Draked038ca82000-10-23 18:31:14 +000060
Guido van Rossum4cee3c41998-04-10 16:14:34 +000061- The new target object is None, and the old target object is not
62None. The old target object's method dnd_leave(source, event) is
63called.
Fred Draked038ca82000-10-23 18:31:14 +000064
Guido van Rossum4cee3c41998-04-10 16:14:34 +000065- The old and new target objects differ and neither is None. The old
66target object's method dnd_leave(source, event), and then the new
67target object's method dnd_enter(source, event) is called.
Fred Draked038ca82000-10-23 18:31:14 +000068
Guido van Rossum4cee3c41998-04-10 16:14:34 +000069Once this is done, the new target object replaces the old one, and the
70Tk mainloop proceeds. The return value of the methods mentioned above
71is ignored; if they raise an exception, the normal exception handling
72mechanisms take over.
Fred Draked038ca82000-10-23 18:31:14 +000073
Guido van Rossum4cee3c41998-04-10 16:14:34 +000074The drag-and-drop processes can end in two ways: a final target object
75is selected, or no final target object is selected. When a final
76target object is selected, it will always have been notified of the
77potential drop by a call to its dnd_enter() method, as described
78above, and possibly one or more calls to its dnd_motion() method; its
79dnd_leave() method has not been called since the last call to
80dnd_enter(). The target is notified of the drop by a call to its
81method dnd_commit(source, event).
Fred Draked038ca82000-10-23 18:31:14 +000082
Guido van Rossum4cee3c41998-04-10 16:14:34 +000083If no final target object is selected, and there was an old target
84object, its dnd_leave(source, event) method is called to complete the
85dnd sequence.
86
87Finally, the source object is notified that the drag-and-drop process
88is over, by a call to source.dnd_end(target, event), specifying either
89the selected target object, or None if no target object was selected.
90The source object can use this to implement the commit action; this is
91sometimes simpler than to do it in the target's dnd_commit(). The
92target's dnd_commit() method could then simply be aliased to
93dnd_leave().
94
95At any time during a dnd sequence, the application can cancel the
96sequence by calling the cancel() method on the object returned by
97dnd_start(). This will call dnd_leave() if a target is currently
98active; it will never call dnd_commit().
99
100"""
101
Georg Brandl14fc4272008-05-17 18:39:55 +0000102import tkinter
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000103
Flavian Hautbois76b64512019-07-26 03:30:33 +0200104__all__ = ["dnd_start", "DndHandler"]
105
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000106
107# The factory function
108
109def dnd_start(source, event):
110 h = DndHandler(source, event)
111 if h.root:
112 return h
113 else:
114 return None
115
116
117# The class that does the work
118
119class DndHandler:
120
121 root = None
122
123 def __init__(self, source, event):
124 if event.num > 5:
125 return
126 root = event.widget._root()
127 try:
128 root.__dnd
129 return # Don't start recursive dnd
130 except AttributeError:
131 root.__dnd = self
132 self.root = root
133 self.source = source
134 self.target = None
135 self.initial_button = button = event.num
136 self.initial_widget = widget = event.widget
137 self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button)
138 self.save_cursor = widget['cursor'] or ""
139 widget.bind(self.release_pattern, self.on_release)
140 widget.bind("<Motion>", self.on_motion)
141 widget['cursor'] = "hand2"
142
143 def __del__(self):
144 root = self.root
145 self.root = None
146 if root:
147 try:
148 del root.__dnd
149 except AttributeError:
150 pass
151
152 def on_motion(self, event):
153 x, y = event.x_root, event.y_root
154 target_widget = self.initial_widget.winfo_containing(x, y)
155 source = self.source
156 new_target = None
157 while target_widget:
158 try:
159 attr = target_widget.dnd_accept
160 except AttributeError:
161 pass
162 else:
163 new_target = attr(source, event)
164 if new_target:
165 break
166 target_widget = target_widget.master
167 old_target = self.target
168 if old_target is new_target:
169 if old_target:
170 old_target.dnd_motion(source, event)
171 else:
172 if old_target:
173 self.target = None
174 old_target.dnd_leave(source, event)
175 if new_target:
176 new_target.dnd_enter(source, event)
177 self.target = new_target
178
179 def on_release(self, event):
180 self.finish(event, 1)
181
182 def cancel(self, event=None):
183 self.finish(event, 0)
184
185 def finish(self, event, commit=0):
186 target = self.target
187 source = self.source
188 widget = self.initial_widget
189 root = self.root
190 try:
191 del root.__dnd
192 self.initial_widget.unbind(self.release_pattern)
193 self.initial_widget.unbind("<Motion>")
194 widget['cursor'] = self.save_cursor
195 self.target = self.source = self.initial_widget = self.root = None
196 if target:
197 if commit:
198 target.dnd_commit(source, event)
199 else:
200 target.dnd_leave(source, event)
201 finally:
202 source.dnd_end(target, event)
203
204
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000205# ----------------------------------------------------------------------
206# The rest is here for testing and demonstration purposes only!
207
208class 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
Georg Brandl14fc4272008-05-17 18:39:55 +0000222 label = tkinter.Label(canvas, text=self.name,
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000223 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
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300268
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000269class Tester:
270
271 def __init__(self, root):
Georg Brandl14fc4272008-05-17 18:39:55 +0000272 self.top = tkinter.Toplevel(root)
273 self.canvas = tkinter.Canvas(self.top, width=100, height=100)
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000274 self.canvas.pack(fill="both", expand=1)
275 self.canvas.dnd_accept = self.dnd_accept
276
277 def dnd_accept(self, source, event):
278 return self
279
280 def dnd_enter(self, source, event):
Thomas Wouters7e474022000-07-16 12:04:32 +0000281 self.canvas.focus_set() # Show highlight border
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000282 x, y = source.where(self.canvas, event)
283 x1, y1, x2, y2 = source.canvas.bbox(source.id)
284 dx, dy = x2-x1, y2-y1
285 self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy)
286 self.dnd_motion(source, event)
287
288 def dnd_motion(self, source, event):
289 x, y = source.where(self.canvas, event)
290 x1, y1, x2, y2 = self.canvas.bbox(self.dndid)
291 self.canvas.move(self.dndid, x-x1, y-y1)
292
293 def dnd_leave(self, source, event):
294 self.top.focus_set() # Hide highlight border
295 self.canvas.delete(self.dndid)
296 self.dndid = None
297
298 def dnd_commit(self, source, event):
299 self.dnd_leave(source, event)
300 x, y = source.where(self.canvas, event)
301 source.attach(self.canvas, x, y)
302
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300303
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000304def test():
Georg Brandl14fc4272008-05-17 18:39:55 +0000305 root = tkinter.Tk()
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000306 root.geometry("+1+1")
Georg Brandl14fc4272008-05-17 18:39:55 +0000307 tkinter.Button(command=root.quit, text="Quit").pack()
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000308 t1 = Tester(root)
309 t1.top.geometry("+1+60")
310 t2 = Tester(root)
311 t2.top.geometry("+120+60")
312 t3 = Tester(root)
313 t3.top.geometry("+240+60")
314 i1 = Icon("ICON1")
315 i2 = Icon("ICON2")
316 i3 = Icon("ICON3")
317 i1.attach(t1.canvas)
318 i2.attach(t2.canvas)
319 i3.attach(t3.canvas)
320 root.mainloop()
321
Serhiy Storchakadc0d5712018-10-12 19:01:00 +0300322
Guido van Rossum4cee3c41998-04-10 16:14:34 +0000323if __name__ == '__main__':
324 test()