home *** CD-ROM | disk | FTP | other *** search
/ PC World 2005 June / PCWorld_2005-06_cd.bin / software / vyzkuste / firewally / firewally.exe / framework-2.3.exe / Tkdnd.py < prev    next >
Text File  |  2003-12-30  |  11KB  |  322 lines

  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.