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() 322