1"""Drag-and-drop support for Tkinter. 2 3This is very preliminary. I currently only support dnd *within* one 4application, between different windows (or within the same window). 5 6I am trying to make this as generic as possible -- not dependent on 7the use of a particular widget or icon type, etc. I also hope that 8this will work with Pmw. 9 10To 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(). 23 24The object is *not* necessarily a widget -- it can be any 25application-specific object that is meaningful to potential 26drag-and-drop targets. 27 28Potential 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). 46 47The 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): 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 55dnd_motion(source, event) is called. 56 57- 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. 60 61- 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. 64 65- 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. 68 69Once 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. 73 74The 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). 82 83If 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 102import tkinter 103 104__all__ = ["dnd_start", "DndHandler"] 105 106 107# The factory function 108 109def dnd_start(source, event): 110 h = DndHandler(source, event) 111 if h.root is not None: 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 is not None: 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 is not None: 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 is not None: 165 break 166 target_widget = target_widget.master 167 old_target = self.target 168 if old_target is new_target: 169 if old_target is not None: 170 old_target.dnd_motion(source, event) 171 else: 172 if old_target is not None: 173 self.target = None 174 old_target.dnd_leave(source, event) 175 if new_target is not None: 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 is not None: 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 205# ---------------------------------------------------------------------- 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 is not None: 219 self.detach() 220 if canvas is None: 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 canvas is None: 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 269class Tester: 270 271 def __init__(self, root): 272 self.top = tkinter.Toplevel(root) 273 self.canvas = tkinter.Canvas(self.top, width=100, height=100) 274 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): 281 self.canvas.focus_set() # Show highlight border 282 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 303 304def test(): 305 root = tkinter.Tk() 306 root.geometry("+1+1") 307 tkinter.Button(command=root.quit, text="Quit").pack() 308 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 322 323if __name__ == '__main__': 324 test() 325