• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
102
103import Tkinter
104
105
106# The factory function
107
108def 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
118class 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
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
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
268class 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
302def 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
320if __name__ == '__main__':
321    test()
322