Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
dnd.py325 linesDownload Raw Back to tkinter
1"""Drag-and-drop support for Tkinter.2 3This is very preliminary.  I currently only support dnd *within* one4application, between different windows (or within the same window).5 6I am trying to make this as generic as possible -- not dependent on7the use of a particular widget or icon type, etc.  I also hope that8this will work with Pmw.9 10To enable an object to be dragged, you must create an event binding11for it that starts the drag-and-drop process. Typically, you should12bind <ButtonPress> to a callback function that you write. The function13should call Tkdnd.dnd_start(source, event), where 'source' is the14object to be dragged, and 'event' is the event that invoked the call15(the argument to your callback function).  Even though this is a class16instantiation, the returned instance should not be stored -- it will17be 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, the20call is *ignored*; this normally averts starting multiple simultaneous21dnd processes, e.g. because different button callbacks all22dnd_start().23 24The object is *not* necessarily a widget -- it can be any25application-specific object that is meaningful to potential26drag-and-drop targets.27 28Potential drag-and-drop targets are discovered as follows.  Whenever29the mouse moves, and at the start and end of a drag-and-drop move, the30Tk widget directly under the mouse is inspected.  This is the target31widget (not to be confused with the target object, yet to be32determined).  If there is no target widget, there is no dnd target33object.  If there is a target widget, and it has an attribute34dnd_accept, this should be a function (or any callable object).  The35function is called as dnd_accept(source, event), where 'source' is the36object being dragged (the object passed to dnd_start() above), and37'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 target40object.  If dnd_accept() returns None, or if the target widget has no41dnd_accept attribute, the target widget's parent is considered as the42target widget, and the search for a target object is repeated from43there.  If necessary, the search is repeated all the way up to the44root widget.  If none of the target widgets can produce a target45object, there is no target object (the target object is None).46 47The target object thus produced, if any, is called the new target48object.  It is compared with the old target object (or None, if there49was no old target widget).  There are several cases ('source' is the50source 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 method55dnd_motion(source, event) is called.56 57- The old target object was None, and the new target object is not58None.  The new target object's method dnd_enter(source, event) is59called.60 61- The new target object is None, and the old target object is not62None.  The old target object's method dnd_leave(source, event) is63called.64 65- The old and new target objects differ and neither is None.  The old66target object's method dnd_leave(source, event), and then the new67target object's method dnd_enter(source, event) is called.68 69Once this is done, the new target object replaces the old one, and the70Tk mainloop proceeds.  The return value of the methods mentioned above71is ignored; if they raise an exception, the normal exception handling72mechanisms take over.73 74The drag-and-drop processes can end in two ways: a final target object75is selected, or no final target object is selected.  When a final76target object is selected, it will always have been notified of the77potential drop by a call to its dnd_enter() method, as described78above, and possibly one or more calls to its dnd_motion() method; its79dnd_leave() method has not been called since the last call to80dnd_enter().  The target is notified of the drop by a call to its81method dnd_commit(source, event).82 83If no final target object is selected, and there was an old target84object, its dnd_leave(source, event) method is called to complete the85dnd sequence.86 87Finally, the source object is notified that the drag-and-drop process88is over, by a call to source.dnd_end(target, event), specifying either89the selected target object, or None if no target object was selected.90The source object can use this to implement the commit action; this is91sometimes simpler than to do it in the target's dnd_commit().  The92target's dnd_commit() method could then simply be aliased to93dnd_leave().94 95At any time during a dnd sequence, the application can cancel the96sequence by calling the cancel() method on the object returned by97dnd_start().  This will call dnd_leave() if a target is currently98active; it will never call dnd_commit().99 100"""101 102import tkinter103 104__all__ = ["dnd_start", "DndHandler"]105 106 107# The factory function108 109def dnd_start(source, event):110    h = DndHandler(source, event)111    if h.root is not None:112        return h113    else:114        return None115 116 117# The class that does the work118 119class DndHandler:120 121    root = None122 123    def __init__(self, source, event):124        if event.num > 5:125            return126        root = event.widget._root()127        try:128            root.__dnd129            return # Don't start recursive dnd130        except AttributeError:131            root.__dnd = self132            self.root = root133        self.source = source134        self.target = None135        self.initial_button = button = event.num136        self.initial_widget = widget = event.widget137        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.root145        self.root = None146        if root is not None:147            try:148                del root.__dnd149            except AttributeError:150                pass151 152    def on_motion(self, event):153        x, y = event.x_root, event.y_root154        target_widget = self.initial_widget.winfo_containing(x, y)155        source = self.source156        new_target = None157        while target_widget is not None:158            try:159                attr = target_widget.dnd_accept160            except AttributeError:161                pass162            else:163                new_target = attr(source, event)164                if new_target is not None:165                    break166            target_widget = target_widget.master167        old_target = self.target168        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 = None174                old_target.dnd_leave(source, event)175            if new_target is not None:176                new_target.dnd_enter(source, event)177                self.target = new_target178 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.target187        source = self.source188        widget = self.initial_widget189        root = self.root190        try:191            del root.__dnd192            self.initial_widget.unbind(self.release_pattern)193            self.initial_widget.unbind("<Motion>")194            widget['cursor'] = self.save_cursor195            self.target = self.source = self.initial_widget = self.root = None196            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 208class Icon:209 210    def __init__(self, name):211        self.name = name212        self.canvas = self.label = self.id = None213 214    def attach(self, canvas, x=10, y=10):215        if canvas is self.canvas:216            self.canvas.coords(self.id, x, y)217            return218        if self.canvas is not None:219            self.detach()220        if canvas is None:221            return222        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 = canvas226        self.label = label227        self.id = id228        label.bind("<ButtonPress>", self.press)229 230    def detach(self):231        canvas = self.canvas232        if canvas is None:233            return234        id = self.id235        label = self.label236        self.canvas = self.label = self.id = None237        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.x244            self.y_off = event.y245            # 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_org261        y = event.y_root - y_org262        # compensate for initial pointer offset263        return x - self.x_off, y - self.y_off264 265    def dnd_end(self, target, event):266        pass267 268 269class Tester: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_accept276 277    def dnd_accept(self, source, event):278        return self279 280    def dnd_enter(self, source, event):281        self.canvas.focus_set() # Show highlight border282        x, y = source.where(self.canvas, event)283        x1, y1, x2, y2 = source.canvas.bbox(source.id)284        dx, dy = x2-x1, y2-y1285        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 border295        self.canvas.delete(self.dndid)296        self.dndid = None297 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 304def 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 323if __name__ == '__main__':324    test()325 
codekingpro/portable-devtools · Team Ai