python-2.5.2/win32/Lib/lib-tk/Tkdnd.py
changeset 0 ae805ac0140d
equal deleted inserted replaced
-1:000000000000 0:ae805ac0140d
       
     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()