~ubuntu-branches/ubuntu/maverick/python3.1/maverick

« back to all changes in this revision

Viewing changes to Lib/tkinter/dnd.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2009-03-23 00:01:27 UTC
  • Revision ID: james.westby@ubuntu.com-20090323000127-5fstfxju4ufrhthq
Tags: upstream-3.1~a1+20090322
ImportĀ upstreamĀ versionĀ 3.1~a1+20090322

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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()