Mercurial > cpython-lru_cache-weakref
annotate Lib/tkinter/dnd.py @ 87690:66c1c9f32567 default tip
Add use_weakref option to functools.lru_cache
The use_weakref option makes lru_cache create weak references to the function
arguments when creating the cache key, which prevents them from staying in the
memory forever even after all other references to them are gone.
This is a proof of concept for http://bugs.python.org/issue19859
| author | Radomir Dopieralski <devel@sheep.art.pl> |
|---|---|
| date | Tue, 03 Dec 2013 16:02:48 +0100 |
| parents | ef4cca2d6171 |
| children |
| rev | line source |
|---|---|
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
1 """Drag-and-drop support for Tkinter. |
|
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
2 |
| 7953 | 3 This is very preliminary. I currently only support dnd *within* one |
| 4 application, between different windows (or within the same window). | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
5 |
| 7953 | 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. | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
9 |
| 7953 | 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(). | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
23 |
| 7953 | 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. | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
27 |
| 7953 | 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). | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
46 |
| 7953 | 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): | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
51 |
| 7953 | 52 - Both the old and new target objects are None. Nothing happens. |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
53 |
| 7953 | 54 - The old and new target objects are the same object. Its method |
| 55 dnd_motion(source, event) is called. | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
56 |
| 7953 | 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. | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
60 |
| 7953 | 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. | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
64 |
| 7953 | 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. | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
68 |
| 7953 | 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. | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
73 |
| 7953 | 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). | |
|
15308
847ae40074d4
Make reindent.py happy (convert everything to 4-space indents!).
Fred Drake <fdrake@acm.org>
parents:
13620
diff
changeset
|
82 |
| 7953 | 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 | |
|
47130
ef4cca2d6171
Tkinter, step 2: adapt imports.
Georg Brandl <georg@python.org>
parents:
47129
diff
changeset
|
103 import tkinter |
| 7953 | 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 | |
|
47130
ef4cca2d6171
Tkinter, step 2: adapt imports.
Georg Brandl <georg@python.org>
parents:
47129
diff
changeset
|
222 label = tkinter.Label(canvas, text=self.name, |
| 7953 | 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): | |
|
47130
ef4cca2d6171
Tkinter, step 2: adapt imports.
Georg Brandl <georg@python.org>
parents:
47129
diff
changeset
|
271 self.top = tkinter.Toplevel(root) |
|
ef4cca2d6171
Tkinter, step 2: adapt imports.
Georg Brandl <georg@python.org>
parents:
47129
diff
changeset
|
272 self.canvas = tkinter.Canvas(self.top, width=100, height=100) |
| 7953 | 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): | |
|
13620
5b727ab37092
Spelling fixes supplied by Rob W. W. Hooft. All these are fixes in either
Thomas Wouters <thomas@python.org>
parents:
7953
diff
changeset
|
280 self.canvas.focus_set() # Show highlight border |
| 7953 | 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(): | |
|
47130
ef4cca2d6171
Tkinter, step 2: adapt imports.
Georg Brandl <georg@python.org>
parents:
47129
diff
changeset
|
303 root = tkinter.Tk() |
| 7953 | 304 root.geometry("+1+1") |
|
47130
ef4cca2d6171
Tkinter, step 2: adapt imports.
Georg Brandl <georg@python.org>
parents:
47129
diff
changeset
|
305 tkinter.Button(command=root.quit, text="Quit").pack() |
| 7953 | 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() |
