1#! /usr/bin/env python
2
3from Tkinter import *
4from Canvas import Oval, Group, CanvasText
5
6
7# Fix a bug in Canvas.Group as distributed in Python 1.4.  The
8# distributed bind() method is broken.  This is what should be used:
9
10class Group(Group):
11    def bind(self, sequence=None, command=None):
12        return self.canvas.tag_bind(self.id, sequence, command)
13
14class Object:
15
16    """Base class for composite graphical objects.
17
18    Objects belong to a canvas, and can be moved around on the canvas.
19    They also belong to at most one ``pile'' of objects, and can be
20    transferred between piles (or removed from their pile).
21
22    Objects have a canonical ``x, y'' position which is moved when the
23    object is moved.  Where the object is relative to this position
24    depends on the object; for simple objects, it may be their center.
25
26    Objects have mouse sensitivity.  They can be clicked, dragged and
27    double-clicked.  The behavior may actually be determined by the pile
28    they are in.
29
30    All instance attributes are public since the derived class may
31    need them.
32
33    """
34
35    def __init__(self, canvas, x=0, y=0, fill='red', text='object'):
36        self.canvas = canvas
37        self.x = x
38        self.y = y
39        self.pile = None
40        self.group = Group(self.canvas)
41        self.createitems(fill, text)
42
43    def __str__(self):
44        return str(self.group)
45
46    def createitems(self, fill, text):
47        self.__oval = Oval(self.canvas,
48                           self.x-20, self.y-10, self.x+20, self.y+10,
49                           fill=fill, width=3)
50        self.group.addtag_withtag(self.__oval)
51        self.__text = CanvasText(self.canvas,
52                           self.x, self.y, text=text)
53        self.group.addtag_withtag(self.__text)
54
55    def moveby(self, dx, dy):
56        if dx == dy == 0:
57            return
58        self.group.move(dx, dy)
59        self.x = self.x + dx
60        self.y = self.y + dy
61
62    def moveto(self, x, y):
63        self.moveby(x - self.x, y - self.y)
64
65    def transfer(self, pile):
66        if self.pile:
67            self.pile.delete(self)
68            self.pile = None
69        self.pile = pile
70        if self.pile:
71            self.pile.add(self)
72
73    def tkraise(self):
74        self.group.tkraise()
75
76
77class Bottom(Object):
78
79    """An object to serve as the bottom of a pile."""
80
81    def createitems(self, *args):
82        self.__oval = Oval(self.canvas,
83                           self.x-20, self.y-10, self.x+20, self.y+10,
84                           fill='gray', outline='')
85        self.group.addtag_withtag(self.__oval)
86
87
88class Pile:
89
90    """A group of graphical objects."""
91
92    def __init__(self, canvas, x, y, tag=None):
93        self.canvas = canvas
94        self.x = x
95        self.y = y
96        self.objects = []
97        self.bottom = Bottom(self.canvas, self.x, self.y)
98        self.group = Group(self.canvas, tag=tag)
99        self.group.addtag_withtag(self.bottom.group)
100        self.bindhandlers()
101
102    def bindhandlers(self):
103        self.group.bind('<1>', self.clickhandler)
104        self.group.bind('<Double-1>', self.doubleclickhandler)
105
106    def add(self, object):
107        self.objects.append(object)
108        self.group.addtag_withtag(object.group)
109        self.position(object)
110
111    def delete(self, object):
112        object.group.dtag(self.group)
113        self.objects.remove(object)
114
115    def position(self, object):
116        object.tkraise()
117        i = self.objects.index(object)
118        object.moveto(self.x + i*4, self.y + i*8)
119
120    def clickhandler(self, event):
121        pass
122
123    def doubleclickhandler(self, event):
124        pass
125
126
127class MovingPile(Pile):
128
129    def bindhandlers(self):
130        Pile.bindhandlers(self)
131        self.group.bind('<B1-Motion>', self.motionhandler)
132        self.group.bind('<ButtonRelease-1>', self.releasehandler)
133
134    movethis = None
135
136    def clickhandler(self, event):
137        tags = self.canvas.gettags('current')
138        for i in range(len(self.objects)):
139            o = self.objects[i]
140            if o.group.tag in tags:
141                break
142        else:
143            self.movethis = None
144            return
145        self.movethis = self.objects[i:]
146        for o in self.movethis:
147            o.tkraise()
148        self.lastx = event.x
149        self.lasty = event.y
150
151    doubleclickhandler = clickhandler
152
153    def motionhandler(self, event):
154        if not self.movethis:
155            return
156        dx = event.x - self.lastx
157        dy = event.y - self.lasty
158        self.lastx = event.x
159        self.lasty = event.y
160        for o in self.movethis:
161            o.moveby(dx, dy)
162
163    def releasehandler(self, event):
164        objects = self.movethis
165        if not objects:
166            return
167        self.movethis = None
168        self.finishmove(objects)
169
170    def finishmove(self, objects):
171        for o in objects:
172            self.position(o)
173
174
175class Pile1(MovingPile):
176
177    x = 50
178    y = 50
179    tag = 'p1'
180
181    def __init__(self, demo):
182        self.demo = demo
183        MovingPile.__init__(self, self.demo.canvas, self.x, self.y, self.tag)
184
185    def doubleclickhandler(self, event):
186        try:
187            o = self.objects[-1]
188        except IndexError:
189            return
190        o.transfer(self.other())
191        MovingPile.doubleclickhandler(self, event)
192
193    def other(self):
194        return self.demo.p2
195
196    def finishmove(self, objects):
197        o = objects[0]
198        p = self.other()
199        x, y = o.x, o.y
200        if (x-p.x)**2 + (y-p.y)**2 < (x-self.x)**2 + (y-self.y)**2:
201            for o in objects:
202                o.transfer(p)
203        else:
204            MovingPile.finishmove(self, objects)
205
206class Pile2(Pile1):
207
208    x = 150
209    y = 50
210    tag = 'p2'
211
212    def other(self):
213        return self.demo.p1
214
215
216class Demo:
217
218    def __init__(self, master):
219        self.master = master
220        self.canvas = Canvas(master,
221                             width=200, height=200,
222                             background='yellow',
223                             relief=SUNKEN, borderwidth=2)
224        self.canvas.pack(expand=1, fill=BOTH)
225        self.p1 = Pile1(self)
226        self.p2 = Pile2(self)
227        o1 = Object(self.canvas, fill='red', text='o1')
228        o2 = Object(self.canvas, fill='green', text='o2')
229        o3 = Object(self.canvas, fill='light blue', text='o3')
230        o1.transfer(self.p1)
231        o2.transfer(self.p1)
232        o3.transfer(self.p2)
233
234
235# Main function, run when invoked as a stand-alone Python program.
236
237def main():
238    root = Tk()
239    demo = Demo(root)
240    root.protocol('WM_DELETE_WINDOW', root.quit)
241    root.mainloop()
242
243if __name__ == '__main__':
244    main()
245