(root)/
Python-3.11.7/
Lib/
tkinter/
dnd.py
       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 am 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  import tkinter
     103  
     104  __all__ = ["dnd_start", "DndHandler"]
     105  
     106  
     107  # The factory function
     108  
     109  def dnd_start(source, event):
     110      h = DndHandler(source, event)
     111      if h.root is not None:
     112          return h
     113      else:
     114          return None
     115  
     116  
     117  # The class that does the work
     118  
     119  class ESC[4;38;5;81mDndHandler:
     120  
     121      root = None
     122  
     123      def __init__(self, source, event):
     124          if event.num > 5:
     125              return
     126          root = event.widget._root()
     127          try:
     128              root.__dnd
     129              return # Don't start recursive dnd
     130          except AttributeError:
     131              root.__dnd = self
     132              self.root = root
     133          self.source = source
     134          self.target = None
     135          self.initial_button = button = event.num
     136          self.initial_widget = widget = event.widget
     137          self.release_pattern = "<B%d-ButtonRelease-%d>" % (button, button)
     138          self.save_cursor = widget['cursor'] or ""
     139          widget.bind(self.release_pattern, self.on_release)
     140          widget.bind("<Motion>", self.on_motion)
     141          widget['cursor'] = "hand2"
     142  
     143      def __del__(self):
     144          root = self.root
     145          self.root = None
     146          if root is not None:
     147              try:
     148                  del root.__dnd
     149              except AttributeError:
     150                  pass
     151  
     152      def on_motion(self, event):
     153          x, y = event.x_root, event.y_root
     154          target_widget = self.initial_widget.winfo_containing(x, y)
     155          source = self.source
     156          new_target = None
     157          while target_widget is not None:
     158              try:
     159                  attr = target_widget.dnd_accept
     160              except AttributeError:
     161                  pass
     162              else:
     163                  new_target = attr(source, event)
     164                  if new_target is not None:
     165                      break
     166              target_widget = target_widget.master
     167          old_target = self.target
     168          if old_target is new_target:
     169              if old_target is not None:
     170                  old_target.dnd_motion(source, event)
     171          else:
     172              if old_target is not None:
     173                  self.target = None
     174                  old_target.dnd_leave(source, event)
     175              if new_target is not None:
     176                  new_target.dnd_enter(source, event)
     177                  self.target = new_target
     178  
     179      def on_release(self, event):
     180          self.finish(event, 1)
     181  
     182      def cancel(self, event=None):
     183          self.finish(event, 0)
     184  
     185      def finish(self, event, commit=0):
     186          target = self.target
     187          source = self.source
     188          widget = self.initial_widget
     189          root = self.root
     190          try:
     191              del root.__dnd
     192              self.initial_widget.unbind(self.release_pattern)
     193              self.initial_widget.unbind("<Motion>")
     194              widget['cursor'] = self.save_cursor
     195              self.target = self.source = self.initial_widget = self.root = None
     196              if target is not None:
     197                  if commit:
     198                      target.dnd_commit(source, event)
     199                  else:
     200                      target.dnd_leave(source, event)
     201          finally:
     202              source.dnd_end(target, event)
     203  
     204  
     205  # ----------------------------------------------------------------------
     206  # The rest is here for testing and demonstration purposes only!
     207  
     208  class ESC[4;38;5;81mIcon:
     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 is not None:
     219              self.detach()
     220          if canvas is None:
     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 canvas is None:
     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  
     269  class ESC[4;38;5;81mTester:
     270  
     271      def __init__(self, root):
     272          self.top = tkinter.Toplevel(root)
     273          self.canvas = tkinter.Canvas(self.top, width=100, height=100)
     274          self.canvas.pack(fill="both", expand=1)
     275          self.canvas.dnd_accept = self.dnd_accept
     276  
     277      def dnd_accept(self, source, event):
     278          return self
     279  
     280      def dnd_enter(self, source, event):
     281          self.canvas.focus_set() # Show highlight border
     282          x, y = source.where(self.canvas, event)
     283          x1, y1, x2, y2 = source.canvas.bbox(source.id)
     284          dx, dy = x2-x1, y2-y1
     285          self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dy)
     286          self.dnd_motion(source, event)
     287  
     288      def dnd_motion(self, source, event):
     289          x, y = source.where(self.canvas, event)
     290          x1, y1, x2, y2 = self.canvas.bbox(self.dndid)
     291          self.canvas.move(self.dndid, x-x1, y-y1)
     292  
     293      def dnd_leave(self, source, event):
     294          self.top.focus_set() # Hide highlight border
     295          self.canvas.delete(self.dndid)
     296          self.dndid = None
     297  
     298      def dnd_commit(self, source, event):
     299          self.dnd_leave(source, event)
     300          x, y = source.where(self.canvas, event)
     301          source.attach(self.canvas, x, y)
     302  
     303  
     304  def test():
     305      root = tkinter.Tk()
     306      root.geometry("+1+1")
     307      tkinter.Button(command=root.quit, text="Quit").pack()
     308      t1 = Tester(root)
     309      t1.top.geometry("+1+60")
     310      t2 = Tester(root)
     311      t2.top.geometry("+120+60")
     312      t3 = Tester(root)
     313      t3.top.geometry("+240+60")
     314      i1 = Icon("ICON1")
     315      i2 = Icon("ICON2")
     316      i3 = Icon("ICON3")
     317      i1.attach(t1.canvas)
     318      i2.attach(t2.canvas)
     319      i3.attach(t3.canvas)
     320      root.mainloop()
     321  
     322  
     323  if __name__ == '__main__':
     324      test()