| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | """Drag-and-drop support for Tkinter.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | This is very preliminary.  I currently only support dnd *within* one | 
					
						
							|  |  |  | application, between different windows (or within the same window). | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | I an trying to make this as generic as possible -- not dependent on | 
					
						
							|  |  |  | the use of a particular widget or icon type, etc.  I also hope that | 
					
						
							|  |  |  | this will work with Pmw. | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | To enable an object to be dragged, you must create an event binding | 
					
						
							|  |  |  | for it that starts the drag-and-drop process. Typically, you should | 
					
						
							|  |  |  | bind <ButtonPress> to a callback function that you write. The function | 
					
						
							|  |  |  | should call Tkdnd.dnd_start(source, event), where 'source' is the | 
					
						
							|  |  |  | object to be dragged, and 'event' is the event that invoked the call | 
					
						
							|  |  |  | (the argument to your callback function).  Even though this is a class | 
					
						
							|  |  |  | instantiation, the returned instance should not be stored -- it will | 
					
						
							|  |  |  | be kept alive automatically for the duration of the drag-and-drop. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | When a drag-and-drop is already in process for the Tk interpreter, the | 
					
						
							|  |  |  | call is *ignored*; this normally averts starting multiple simultaneous | 
					
						
							|  |  |  | dnd processes, e.g. because different button callbacks all | 
					
						
							|  |  |  | dnd_start(). | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | The object is *not* necessarily a widget -- it can be any | 
					
						
							|  |  |  | application-specific object that is meaningful to potential | 
					
						
							|  |  |  | drag-and-drop targets. | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | Potential drag-and-drop targets are discovered as follows.  Whenever | 
					
						
							|  |  |  | the mouse moves, and at the start and end of a drag-and-drop move, the | 
					
						
							|  |  |  | Tk widget directly under the mouse is inspected.  This is the target | 
					
						
							|  |  |  | widget (not to be confused with the target object, yet to be | 
					
						
							|  |  |  | determined).  If there is no target widget, there is no dnd target | 
					
						
							|  |  |  | object.  If there is a target widget, and it has an attribute | 
					
						
							|  |  |  | dnd_accept, this should be a function (or any callable object).  The | 
					
						
							|  |  |  | function is called as dnd_accept(source, event), where 'source' is the | 
					
						
							|  |  |  | object being dragged (the object passed to dnd_start() above), and | 
					
						
							|  |  |  | 'event' is the most recent event object (generally a <Motion> event; | 
					
						
							|  |  |  | it can also be <ButtonPress> or <ButtonRelease>).  If the dnd_accept() | 
					
						
							|  |  |  | function returns something other than None, this is the new dnd target | 
					
						
							|  |  |  | object.  If dnd_accept() returns None, or if the target widget has no | 
					
						
							|  |  |  | dnd_accept attribute, the target widget's parent is considered as the | 
					
						
							|  |  |  | target widget, and the search for a target object is repeated from | 
					
						
							|  |  |  | there.  If necessary, the search is repeated all the way up to the | 
					
						
							|  |  |  | root widget.  If none of the target widgets can produce a target | 
					
						
							|  |  |  | object, there is no target object (the target object is None). | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | The target object thus produced, if any, is called the new target | 
					
						
							|  |  |  | object.  It is compared with the old target object (or None, if there | 
					
						
							|  |  |  | was no old target widget).  There are several cases ('source' is the | 
					
						
							|  |  |  | source object, and 'event' is the most recent event object): | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | - Both the old and new target objects are None.  Nothing happens. | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | - The old and new target objects are the same object.  Its method | 
					
						
							|  |  |  | dnd_motion(source, event) is called. | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | - The old target object was None, and the new target object is not | 
					
						
							|  |  |  | None.  The new target object's method dnd_enter(source, event) is | 
					
						
							|  |  |  | called. | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | - The new target object is None, and the old target object is not | 
					
						
							|  |  |  | None.  The old target object's method dnd_leave(source, event) is | 
					
						
							|  |  |  | called. | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | - The old and new target objects differ and neither is None.  The old | 
					
						
							|  |  |  | target object's method dnd_leave(source, event), and then the new | 
					
						
							|  |  |  | target object's method dnd_enter(source, event) is called. | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | Once this is done, the new target object replaces the old one, and the | 
					
						
							|  |  |  | Tk mainloop proceeds.  The return value of the methods mentioned above | 
					
						
							|  |  |  | is ignored; if they raise an exception, the normal exception handling | 
					
						
							|  |  |  | mechanisms take over. | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | The drag-and-drop processes can end in two ways: a final target object | 
					
						
							|  |  |  | is selected, or no final target object is selected.  When a final | 
					
						
							|  |  |  | target object is selected, it will always have been notified of the | 
					
						
							|  |  |  | potential drop by a call to its dnd_enter() method, as described | 
					
						
							|  |  |  | above, and possibly one or more calls to its dnd_motion() method; its | 
					
						
							|  |  |  | dnd_leave() method has not been called since the last call to | 
					
						
							|  |  |  | dnd_enter().  The target is notified of the drop by a call to its | 
					
						
							|  |  |  | method dnd_commit(source, event). | 
					
						
							| 
									
										
										
										
											2000-10-23 18:31:14 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | If no final target object is selected, and there was an old target | 
					
						
							|  |  |  | object, its dnd_leave(source, event) method is called to complete the | 
					
						
							|  |  |  | dnd sequence. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Finally, the source object is notified that the drag-and-drop process | 
					
						
							|  |  |  | is over, by a call to source.dnd_end(target, event), specifying either | 
					
						
							|  |  |  | the selected target object, or None if no target object was selected. | 
					
						
							|  |  |  | The source object can use this to implement the commit action; this is | 
					
						
							|  |  |  | sometimes simpler than to do it in the target's dnd_commit().  The | 
					
						
							|  |  |  | target's dnd_commit() method could then simply be aliased to | 
					
						
							|  |  |  | dnd_leave(). | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | At any time during a dnd sequence, the application can cancel the | 
					
						
							|  |  |  | sequence by calling the cancel() method on the object returned by | 
					
						
							|  |  |  | dnd_start().  This will call dnd_leave() if a target is currently | 
					
						
							|  |  |  | active; it will never call dnd_commit(). | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-05-17 18:39:55 +00:00
										 |  |  | import tkinter | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # The factory function | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def dnd_start(source, event): | 
					
						
							|  |  |  |     h = DndHandler(source, event) | 
					
						
							|  |  |  |     if h.root: | 
					
						
							|  |  |  |         return h | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # The class that does the work | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class DndHandler: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     root = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, source, event): | 
					
						
							|  |  |  |         if event.num > 5: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         root = event.widget._root() | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             root.__dnd | 
					
						
							|  |  |  |             return # Don't start recursive dnd | 
					
						
							|  |  |  |         except AttributeError: | 
					
						
							|  |  |  |             root.__dnd = self | 
					
						
							|  |  |  |             self.root = root | 
					
						
							|  |  |  |         self.source = source | 
					
						
							|  |  |  |         self.target = None | 
					
						
							|  |  |  |         self.initial_button = button = event.num | 
					
						
							|  |  |  |         self.initial_widget = widget = event.widget | 
					
						
							|  |  |  |         self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button) | 
					
						
							|  |  |  |         self.save_cursor = widget['cursor'] or "" | 
					
						
							|  |  |  |         widget.bind(self.release_pattern, self.on_release) | 
					
						
							|  |  |  |         widget.bind("<Motion>", self.on_motion) | 
					
						
							|  |  |  |         widget['cursor'] = "hand2" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __del__(self): | 
					
						
							|  |  |  |         root = self.root | 
					
						
							|  |  |  |         self.root = None | 
					
						
							|  |  |  |         if root: | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 del root.__dnd | 
					
						
							|  |  |  |             except AttributeError: | 
					
						
							|  |  |  |                 pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def on_motion(self, event): | 
					
						
							|  |  |  |         x, y = event.x_root, event.y_root | 
					
						
							|  |  |  |         target_widget = self.initial_widget.winfo_containing(x, y) | 
					
						
							|  |  |  |         source = self.source | 
					
						
							|  |  |  |         new_target = None | 
					
						
							|  |  |  |         while target_widget: | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 attr = target_widget.dnd_accept | 
					
						
							|  |  |  |             except AttributeError: | 
					
						
							|  |  |  |                 pass | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 new_target = attr(source, event) | 
					
						
							|  |  |  |                 if new_target: | 
					
						
							|  |  |  |                     break | 
					
						
							|  |  |  |             target_widget = target_widget.master | 
					
						
							|  |  |  |         old_target = self.target | 
					
						
							|  |  |  |         if old_target is new_target: | 
					
						
							|  |  |  |             if old_target: | 
					
						
							|  |  |  |                 old_target.dnd_motion(source, event) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             if old_target: | 
					
						
							|  |  |  |                 self.target = None | 
					
						
							|  |  |  |                 old_target.dnd_leave(source, event) | 
					
						
							|  |  |  |             if new_target: | 
					
						
							|  |  |  |                 new_target.dnd_enter(source, event) | 
					
						
							|  |  |  |                 self.target = new_target | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def on_release(self, event): | 
					
						
							|  |  |  |         self.finish(event, 1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def cancel(self, event=None): | 
					
						
							|  |  |  |         self.finish(event, 0) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def finish(self, event, commit=0): | 
					
						
							|  |  |  |         target = self.target | 
					
						
							|  |  |  |         source = self.source | 
					
						
							|  |  |  |         widget = self.initial_widget | 
					
						
							|  |  |  |         root = self.root | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             del root.__dnd | 
					
						
							|  |  |  |             self.initial_widget.unbind(self.release_pattern) | 
					
						
							|  |  |  |             self.initial_widget.unbind("<Motion>") | 
					
						
							|  |  |  |             widget['cursor'] = self.save_cursor | 
					
						
							|  |  |  |             self.target = self.source = self.initial_widget = self.root = None | 
					
						
							|  |  |  |             if target: | 
					
						
							|  |  |  |                 if commit: | 
					
						
							|  |  |  |                     target.dnd_commit(source, event) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     target.dnd_leave(source, event) | 
					
						
							|  |  |  |         finally: | 
					
						
							|  |  |  |             source.dnd_end(target, event) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # ---------------------------------------------------------------------- | 
					
						
							|  |  |  | # The rest is here for testing and demonstration purposes only! | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Icon: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, name): | 
					
						
							|  |  |  |         self.name = name | 
					
						
							|  |  |  |         self.canvas = self.label = self.id = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def attach(self, canvas, x=10, y=10): | 
					
						
							|  |  |  |         if canvas is self.canvas: | 
					
						
							|  |  |  |             self.canvas.coords(self.id, x, y) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         if self.canvas: | 
					
						
							|  |  |  |             self.detach() | 
					
						
							|  |  |  |         if not canvas: | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2008-05-17 18:39:55 +00:00
										 |  |  |         label = tkinter.Label(canvas, text=self.name, | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  |                               borderwidth=2, relief="raised") | 
					
						
							|  |  |  |         id = canvas.create_window(x, y, window=label, anchor="nw") | 
					
						
							|  |  |  |         self.canvas = canvas | 
					
						
							|  |  |  |         self.label = label | 
					
						
							|  |  |  |         self.id = id | 
					
						
							|  |  |  |         label.bind("<ButtonPress>", self.press) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def detach(self): | 
					
						
							|  |  |  |         canvas = self.canvas | 
					
						
							|  |  |  |         if not canvas: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         id = self.id | 
					
						
							|  |  |  |         label = self.label | 
					
						
							|  |  |  |         self.canvas = self.label = self.id = None | 
					
						
							|  |  |  |         canvas.delete(id) | 
					
						
							|  |  |  |         label.destroy() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def press(self, event): | 
					
						
							|  |  |  |         if dnd_start(self, event): | 
					
						
							|  |  |  |             # where the pointer is relative to the label widget: | 
					
						
							|  |  |  |             self.x_off = event.x | 
					
						
							|  |  |  |             self.y_off = event.y | 
					
						
							|  |  |  |             # where the widget is relative to the canvas: | 
					
						
							|  |  |  |             self.x_orig, self.y_orig = self.canvas.coords(self.id) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def move(self, event): | 
					
						
							|  |  |  |         x, y = self.where(self.canvas, event) | 
					
						
							|  |  |  |         self.canvas.coords(self.id, x, y) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def putback(self): | 
					
						
							|  |  |  |         self.canvas.coords(self.id, self.x_orig, self.y_orig) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def where(self, canvas, event): | 
					
						
							|  |  |  |         # where the corner of the canvas is relative to the screen: | 
					
						
							|  |  |  |         x_org = canvas.winfo_rootx() | 
					
						
							|  |  |  |         y_org = canvas.winfo_rooty() | 
					
						
							|  |  |  |         # where the pointer is relative to the canvas widget: | 
					
						
							|  |  |  |         x = event.x_root - x_org | 
					
						
							|  |  |  |         y = event.y_root - y_org | 
					
						
							|  |  |  |         # compensate for initial pointer offset | 
					
						
							|  |  |  |         return x - self.x_off, y - self.y_off | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def dnd_end(self, target, event): | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Tester: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, root): | 
					
						
							| 
									
										
										
										
											2008-05-17 18:39:55 +00:00
										 |  |  |         self.top = tkinter.Toplevel(root) | 
					
						
							|  |  |  |         self.canvas = tkinter.Canvas(self.top, width=100, height=100) | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  |         self.canvas.pack(fill="both", expand=1) | 
					
						
							|  |  |  |         self.canvas.dnd_accept = self.dnd_accept | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def dnd_accept(self, source, event): | 
					
						
							|  |  |  |         return self | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def dnd_enter(self, source, event): | 
					
						
							| 
									
										
										
										
											2000-07-16 12:04:32 +00:00
										 |  |  |         self.canvas.focus_set() # Show highlight border | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  |         x, y = source.where(self.canvas, event) | 
					
						
							|  |  |  |         x1, y1, x2, y2 = source.canvas.bbox(source.id) | 
					
						
							|  |  |  |         dx, dy = x2-x1, y2-y1 | 
					
						
							|  |  |  |         self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy) | 
					
						
							|  |  |  |         self.dnd_motion(source, event) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def dnd_motion(self, source, event): | 
					
						
							|  |  |  |         x, y = source.where(self.canvas, event) | 
					
						
							|  |  |  |         x1, y1, x2, y2 = self.canvas.bbox(self.dndid) | 
					
						
							|  |  |  |         self.canvas.move(self.dndid, x-x1, y-y1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def dnd_leave(self, source, event): | 
					
						
							|  |  |  |         self.top.focus_set() # Hide highlight border | 
					
						
							|  |  |  |         self.canvas.delete(self.dndid) | 
					
						
							|  |  |  |         self.dndid = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def dnd_commit(self, source, event): | 
					
						
							|  |  |  |         self.dnd_leave(source, event) | 
					
						
							|  |  |  |         x, y = source.where(self.canvas, event) | 
					
						
							|  |  |  |         source.attach(self.canvas, x, y) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def test(): | 
					
						
							| 
									
										
										
										
											2008-05-17 18:39:55 +00:00
										 |  |  |     root = tkinter.Tk() | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  |     root.geometry("+1+1") | 
					
						
							| 
									
										
										
										
											2008-05-17 18:39:55 +00:00
										 |  |  |     tkinter.Button(command=root.quit, text="Quit").pack() | 
					
						
							| 
									
										
										
										
											1998-04-10 16:14:34 +00:00
										 |  |  |     t1 = Tester(root) | 
					
						
							|  |  |  |     t1.top.geometry("+1+60") | 
					
						
							|  |  |  |     t2 = Tester(root) | 
					
						
							|  |  |  |     t2.top.geometry("+120+60") | 
					
						
							|  |  |  |     t3 = Tester(root) | 
					
						
							|  |  |  |     t3.top.geometry("+240+60") | 
					
						
							|  |  |  |     i1 = Icon("ICON1") | 
					
						
							|  |  |  |     i2 = Icon("ICON2") | 
					
						
							|  |  |  |     i3 = Icon("ICON3") | 
					
						
							|  |  |  |     i1.attach(t1.canvas) | 
					
						
							|  |  |  |     i2.attach(t2.canvas) | 
					
						
							|  |  |  |     i3.attach(t3.canvas) | 
					
						
							|  |  |  |     root.mainloop() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							|  |  |  |     test() |