~richardjones/withgui/trunk

« back to all changes in this revision

Viewing changes to withgui/withtk.py

  • Committer: Richard Jones
  • Date: 2009-08-27 05:57:06 UTC
  • Revision ID: richard@l-rjones.off.ekorp.com-20090827055706-rm3nnqy4sonkot95
reorg

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import sys
 
2
import time
 
3
import urllib
 
4
import functools
 
5
 
 
6
# py3k compat
 
7
try:
 
8
    from io import StringIO
 
9
except ImportError:
 
10
    from StringIO import StringIO
 
11
try:
 
12
    import tkinter as tk
 
13
except ImportError:
 
14
    import Tkinter as tk
 
15
 
 
16
from PIL import Image as PIL_Image
 
17
from PIL import ImageTk
 
18
 
 
19
import withgui
 
20
 
 
21
class PackLayout(object):
 
22
    pack_side = tk.TOP
 
23
    def layout(self, child):
 
24
        # TODO check child DOES NOT have x, y and raise sensible error
 
25
        pack = dict(side=self.pack_side)
 
26
        if 'fill' in child.args:
 
27
            pack['fill'] = child.args['fill']
 
28
        if 'anchor' in child.args:
 
29
            pack['anchor'] = child.args['anchor']
 
30
        if 'expand' in child.args:
 
31
            pack['expand'] = child.args['expand']
 
32
        child.tk_widget.pack(**pack)
 
33
 
 
34
class GUI(PackLayout):
 
35
    '''The owner of the implementaton of a withgui spec.
 
36
 
 
37
    Holds the toplevel widget Implementation, the animators and the
 
38
    after_actions.
 
39
    '''
 
40
    def __init__(self, spec):
 
41
        self.animators = []
 
42
 
 
43
        self.tk_widget = tk.Tk()
 
44
 
 
45
        # TODO more window properties
 
46
        self.tk_widget.title(spec.window_settings['title'])
 
47
        #self.tk_widget.minsize(200, 100)
 
48
        self.tk_widget.geometry('+100+100')
 
49
 
 
50
        # add space at the bottom to account for OS X's resize handle
 
51
        if sys.platform == 'darwin':
 
52
            status = tk.Label(self.tk_widget, text="")
 
53
            status.pack(side=tk.BOTTOM, fill=tk.X)
 
54
 
 
55
        # construct the spec
 
56
        self.gui = self
 
57
        self.toplevel = Implementation.construct(spec, self)
 
58
 
 
59
        # add delayed actions
 
60
        for delay, action in spec.after_actions:
 
61
            self.tk_widget.after(int(delay*1000), action)
 
62
 
 
63
        # set up top-level event handlers
 
64
        for name, action in spec.event_handlers.items():
 
65
            if name == 'on_mouse':
 
66
                # TODO all buttons
 
67
                toplevel.bind("<Button-1>", MouseEvent(spec, action, left=True))
 
68
 
 
69
    def update(self):
 
70
        dt = time.time() - self.ts
 
71
        self.ts = time.time()
 
72
        for animator in list(self.animators):
 
73
            try:
 
74
                animator.send(dt)
 
75
            except StopIteration:
 
76
                self.animators.remove(animator)
 
77
        # TODO handle timing skew
 
78
        # TODO only schedule this if there's active animators
 
79
        self.tk_widget.after(int(1000/20), self.update)
 
80
 
 
81
    def run(self):
 
82
        self.ts = time.time()
 
83
        # TODO only schedule this if there's active animators
 
84
        self.tk_widget.after(int(1000/20), self.update)
 
85
        self.tk_widget.focus()
 
86
        self.tk_widget.lift()
 
87
        self.tk_widget.mainloop()
 
88
 
 
89
    def stop(self, data=None):
 
90
        self.tk_widget.destroy()
 
91
        return data
 
92
 
 
93
class HasProperties(object):
 
94
    # mapping withgui properties to Tkinter properties
 
95
    prop_int = lambda self, v: int(v)
 
96
    # TODO: check ALL uses of this for better type
 
97
    prop_passthrough = lambda self, v: v
 
98
    prop_color = lambda self, v: '#'+v.to_hex()
 
99
    prop_fill = lambda self, v: dict(x=tk.X, y=tk.Y, both=tk.BOTH)[v]
 
100
    def prop_image(self, spec):
 
101
        # TODO accept more URLs
 
102
        if spec.startswith('http://'):
 
103
            s = StringIO(urllib.urlopen(spec).read())
 
104
            return ImageTk.PhotoImage(PIL_Image.open(s))
 
105
 
 
106
        # find the image
 
107
        path = self.spec.gui.resource.lookup(spec)
 
108
        if path is None:
 
109
            raise ValueError("can't locate image file %s"%spec)
 
110
        return ImageTk.PhotoImage(PIL_Image.open(path))
 
111
    def prop_color_or_image(self, spec):
 
112
        # can't isinstance() here because __main__.Color != withgui.Color!
 
113
        if hasattr(spec, 'to_hex'):
 
114
            return self.prop_color(spec)
 
115
        return self.prop_image(spec)
 
116
    # TODO more anchors
 
117
    prop_anchor = lambda self, v: dict(center=tk.CENTER, sw=tk.SW,
 
118
        nw=tk.NW).get(v)
 
119
 
 
120
    # properties are names mapped to the Tkinter name and conversion callable
 
121
    base_properties = dict(
 
122
        justify=('justify', prop_passthrough),
 
123
        x=('x', prop_int),
 
124
        y=('y', prop_int),
 
125
        width=('width', prop_passthrough),
 
126
        height=('height', prop_passthrough),
 
127
        foreground=('foreground', prop_color),
 
128
        background=('background', prop_color),
 
129
 
 
130
        # packing args
 
131
        fill=('fill', prop_fill),
 
132
        anchor=('anchor', prop_anchor),
 
133
        expand=('expand', prop_int),
 
134
 
 
135
        value=('value', prop_passthrough),
 
136
    )
 
137
    properties = {}
 
138
 
 
139
    def generate_properties(self, spec):
 
140
        if spec.animate:
 
141
            self.gui.animators.append(spec.animate)
 
142
        d = {}
 
143
        for k,v in spec.settings.items():
 
144
            if k in self.properties:
 
145
                k, p = self.properties[k]
 
146
            else:
 
147
                k, p = self.base_properties[k]
 
148
            d[k] = p(self, v)
 
149
        return d
 
150
 
 
151
    def translate_value(self, name, value):
 
152
        if name in self.properties:
 
153
            k, p = self.properties[name]
 
154
        else:
 
155
            k, p = self.base_properties[name]
 
156
        return k, p(self, value)
 
157
 
 
158
class Implementation(HasProperties):
 
159
    '''Wrapper around a Tkinter widget and optionally a separate Tkinter
 
160
    variable allowing read/write access to the widget's properties
 
161
    including its current value.
 
162
    '''
 
163
    implementation_classes = {}
 
164
    @classmethod
 
165
    def register_class(cls, klass, name=None):
 
166
        if name is None:
 
167
            name = klass.__name__
 
168
        cls.implementation_classes[name] = klass
 
169
 
 
170
    @classmethod
 
171
    def construct(cls, spec, parent):
 
172
        klass = cls.implementation_classes[spec.__class__.__name__]
 
173
        return klass(spec, parent)
 
174
 
 
175
    def __init__(self, spec, parent):
 
176
        self.spec = spec
 
177
        self.parent = parent
 
178
        self.gui = parent.gui
 
179
        self.tk_widget = None
 
180
        self.args = self.generate_properties(spec)
 
181
 
 
182
        self.create_widget()
 
183
 
 
184
        parent.layout(self)
 
185
 
 
186
        # set up event handlers
 
187
        if 'on_mouse' in spec.event_handlers:
 
188
            # TODO all buttons
 
189
            image.bind("<Button-1>", MouseEvent(spec,
 
190
                spec.event_handlers['on_mouse'], left=True))
 
191
 
 
192
        self.spec.implementation = self
 
193
 
 
194
    def create_widget(self):
 
195
        '''Create the underlying tk_widget / tk_variable.
 
196
        '''
 
197
        raise NotImplementeError('create_widget must be implemented')
 
198
 
 
199
    def set_value(self, value):
 
200
        self.tk_variable.set(value)
 
201
    def get_value(self):
 
202
        return self.tk_variable.get()
 
203
    def set_property(self, name, value):
 
204
        name, value = self.translate_value(name, value)
 
205
        if name == 'value':
 
206
            self.set_value(value)
 
207
            return
 
208
        if name == 'x':
 
209
            self.tk_widget.x = value
 
210
            self.tk_widget.place(x=value)
 
211
        elif name == 'y':
 
212
            self.tk_widget.y = value
 
213
            self.tk_widget.place(y=value)
 
214
        else:
 
215
            self.tk_widget.config(**{name: value})
 
216
    def get_property(self, name):
 
217
        if name == 'value':
 
218
            return self.get_value()
 
219
        elif name == 'x':
 
220
            return self.tk_widget.x
 
221
        elif name == 'y':
 
222
            return self.tk_widget.y
 
223
        else:
 
224
            return getattr(self.tk_widget, name)
 
225
 
 
226
    def destroy(self):
 
227
        self.spec.implementation = None
 
228
        self.parent = None
 
229
        self.gui = None
 
230
        self.args = None
 
231
        self.tk_widget.unbind("<Button-1>")
 
232
        self.tk_widget.destroy()
 
233
        self.tk_widget = self.tk_variable = None
 
234
 
 
235
class Frame(Implementation):
 
236
    properties = dict(
 
237
        side=('side', HasProperties.prop_passthrough),
 
238
        background=('background', HasProperties.prop_color_or_image),
 
239
    )
 
240
    conprops = 'width height side background'.split()
 
241
 
 
242
    def create_widget(self):
 
243
        # oh, for dict comprehensions
 
244
        kw = {}
 
245
        for name in self.conprops:
 
246
            if name in self.args:
 
247
                kw[name] = self.args[name]
 
248
 
 
249
        image = self.args.get('image')
 
250
        if image is not None:
 
251
            if 'width' not in kw:
 
252
                kw['width'] = image.width()
 
253
            if 'height' not in kw:
 
254
                kw['height'] = image.height()
 
255
 
 
256
        self.tk_widget = tk.Frame(self.parent.tk_widget, **kw)
 
257
 
 
258
        if image is not None:
 
259
            self._image = image
 
260
            self.image_widget = tk.Label(self.tk_widget,
 
261
                image=self._image)
 
262
            self.image_widget.place(x=0, y=0, anchor=tk.NW)
 
263
 
 
264
        # create children
 
265
        # this is is done here so it may be customised per widget (well,
 
266
        # frame vs. canvas)
 
267
        for child in self.spec.children:
 
268
            Implementation.construct(child, self)
 
269
 
 
270
    def add_child(self, spec):
 
271
        Implementation.construct(spec, self)
 
272
 
 
273
    def layout(self, child):
 
274
        '''Add children by x, y coordinates.
 
275
        '''
 
276
        # TODO check child has x, y and raise sensible error
 
277
        x = child.args['x']
 
278
        y = child.args['y']
 
279
        anchor = child.args.get('anchor', tk.CENTER)
 
280
        child.tk_widget.place(x=x, y=y, anchor=anchor)
 
281
        child.x = x
 
282
        child.y = y
 
283
        
 
284
 
 
285
Implementation.register_class(Frame)
 
286
 
 
287
class Column(PackLayout, Frame):
 
288
    pass
 
289
Implementation.register_class(Column)
 
290
 
 
291
class Row(PackLayout, Frame):
 
292
    pack_side = tk.LEFT
 
293
Implementation.register_class(Row)
 
294
 
 
295
class Label(Implementation):
 
296
    properties = dict(
 
297
        value=('text', HasProperties.prop_passthrough),
 
298
    )
 
299
    # TODO better name
 
300
    conprops = 'background foreground'.split()
 
301
 
 
302
    def create_widget(self):
 
303
        # oh, for dict comprehensions
 
304
        kw = {}
 
305
        for name in self.conprops:
 
306
            if name in self.args:
 
307
                kw[name] = self.args[name]
 
308
 
 
309
        kw['textvariable'] = self.tk_variable = tk.StringVar()
 
310
        self.tk_variable.set(self.args.get('text', ''))
 
311
        self.tk_widget = tk.Label(self.parent.tk_widget, **kw)
 
312
 
 
313
Implementation.register_class(Label)
 
314
 
 
315
class Help(Label):
 
316
    def __init__(self, spec, parent, pack={}):
 
317
        return self.add_Label(spec, parent, pack=pack)
 
318
Implementation.register_class(Help)
 
319
 
 
320
class Image(Implementation):
 
321
    properties = dict(
 
322
        value=('image', HasProperties.prop_image),
 
323
    )
 
324
 
 
325
    def create_widget(self):
 
326
        self.tk_widget = tk.Label(self.parent.tk_widget,
 
327
            image=self.args['image'])
 
328
 
 
329
    def set_value(self, value):
 
330
        self.tk_widget.config(image=value)
 
331
        self.args['image'] = value
 
332
 
 
333
Implementation.register_class(Image)
 
334
 
 
335
class Text(Implementation):
 
336
    properties = dict(
 
337
        value=('text', HasProperties.prop_passthrough),
 
338
    )
 
339
    conprops = 'background foreground'.split()
 
340
    def create_widget(self):
 
341
        initial = self.args.get('text', '')
 
342
        kw = {}
 
343
        for name in self.conprops:
 
344
            if name in self.args:
 
345
                kw[name] = self.args[name]
 
346
        self.tk_widget = tk.Entry(self.parent.tk_widget, **kw)
 
347
        if initial:
 
348
            self.tk_widget.insert(0, initial)
 
349
    def set_value(self, value):
 
350
        self.tk_widget.delete(0, tk.END)
 
351
        self.tk_widget.insert(0, value)
 
352
    def get_value(self):
 
353
        return self.tk_widget.get()
 
354
Implementation.register_class(Text)
 
355
 
 
356
class Selection(Implementation):
 
357
    properties = dict(
 
358
        options=('options', HasProperties.prop_passthrough),
 
359
        value=('value', HasProperties.prop_passthrough),
 
360
    )
 
361
    conprops = 'background foreground'.split()
 
362
    def create_widget(self):
 
363
        self.tk_variable = tk.StringVar()
 
364
        options = self.args['options']
 
365
        value = self.args.get('value', options[0])
 
366
        self.tk_variable.set(value)
 
367
 
 
368
        args = (self.parent.tk_widget, self.tk_variable) + tuple(options)
 
369
        kw = {}
 
370
        for name in self.conprops:
 
371
            if name in self.args:
 
372
                kw[name] = self.args[name]
 
373
        self.tk_widget = tk.OptionMenu(*args, **kw)
 
374
Implementation.register_class(Selection)
 
375
 
 
376
class Button(Label):
 
377
    properties = dict(
 
378
        image=('image', HasProperties.prop_image),
 
379
        value=('text', HasProperties.prop_passthrough),
 
380
    )
 
381
    # TODO better name
 
382
    conprops = 'background foreground'.split()
 
383
    def create_widget(self):
 
384
        kw = {}
 
385
        for name in self.conprops:
 
386
            if name in self.args:
 
387
                kw[name] = self.args[name]
 
388
        kw['textvariable'] = self.tk_variable = tk.StringVar()
 
389
        self.tk_variable.set(self.args.get('text', ''))
 
390
        kw['command'] = functools.partial(
 
391
            self.spec.event_handlers.get('on_click'), self.spec)
 
392
        self.tk_widget = tk.Button(self.parent.tk_widget, **kw)
 
393
Implementation.register_class(Button)
 
394
Implementation.register_class(Button, 'Submit')
 
395
Implementation.register_class(Button, 'Cancel')
 
396
 
 
397
class Form(Implementation):
 
398
    def create_widget(self):
 
399
        self.tk_widget = tk.Frame(self.parent.tk_widget)
 
400
 
 
401
        for n, row in enumerate(self.spec.rows):
 
402
            if row.label is not None:
 
403
                l = Label(row.label, self)  # text=row.label.value, justify=tk.RIGHT)
 
404
                l.tk_widget.grid(row=n, column=0, sticky=tk.E)
 
405
 
 
406
            if row.widget is not None:
 
407
                # figure the name for this widget in the form
 
408
                w = Implementation.construct(row.widget, self)
 
409
                w.tk_widget.grid(row=n, column=1, sticky=tk.W)
 
410
 
 
411
            if row.help is not None:
 
412
                l = Label(row.help, self)  # text=row.help.value, justify=tk.LEFT)
 
413
                l.tk_widget.grid(row=n, column=2, sticky=tk.W)
 
414
 
 
415
        # switcheroo to put these buttons in a different parent
 
416
        old, self.tk_widget = self.tk_widget, tk.Frame(self.tk_widget)
 
417
        for button in self.spec.buttons:
 
418
            w = Implementation.construct(button, self)
 
419
            w.tk_widget.pack(side=tk.LEFT)
 
420
        self.tk_widget.grid(row=n+1, column=1, columnspan=2, sticky=tk.W)
 
421
        self.tk_widget = old
 
422
 
 
423
    def layout(self, child):
 
424
        pass
 
425
 
 
426
Implementation.register_class(Form, 'Form')
 
427
 
 
428
 
 
429
 
 
430
class CanvasObject(HasProperties):
 
431
    implementation_classes = {}
 
432
    @classmethod
 
433
    def register_class(cls, klass, name=None):
 
434
        if name is None:
 
435
            name = klass.__name__
 
436
        cls.implementation_classes[name] = klass
 
437
 
 
438
    @classmethod
 
439
    def construct(cls, spec, canvas):
 
440
        klass = cls.implementation_classes[spec.__class__.__name__]
 
441
        impl = klass(spec, canvas)
 
442
 
 
443
    def __init__(self, spec, canvas):
 
444
        self.canvas = canvas
 
445
        self.gui = canvas.gui
 
446
        self.spec = spec
 
447
 
 
448
        self.args = self.generate_properties(spec)
 
449
        self.x, self.y = position = self.args['x'], self.args['y']
 
450
 
 
451
        self.create_object(position, spec)
 
452
 
 
453
        if spec.event_handlers:
 
454
            self.canvas.event_handlers.append((spec, self.id,
 
455
                spec.event_handlers))
 
456
            # TODO self.handlers_required
 
457
        
 
458
        spec.implementation = self
 
459
 
 
460
    def set_value(self, value):
 
461
        raise NotImplementedError('set_value not implemented for %s'%
 
462
            self.tk_widget.type(self.id))
 
463
 
 
464
    def set_property(self, name, value):
 
465
        tname, value = self.translate_value(name, value)
 
466
        if name == 'value':
 
467
            self.set_value(value)
 
468
            return
 
469
        name = tname
 
470
        if name == 'x':
 
471
            pos = (value, self.y)
 
472
            self.canvas.tk_widget.coords(self.id, pos)
 
473
        elif name == 'y':
 
474
            pos = (self.x, value)
 
475
            self.canvas.tk_widget.coords(self.id, pos)
 
476
        else:
 
477
            self.canvas.tk_widget.itemconfig(self.id, **{name: value})
 
478
 
 
479
    def get_value(self):
 
480
        raise NotImplementedError('get_value not implemented for %s'%
 
481
            self.tk_widget.type(self.id))
 
482
 
 
483
    def get_property(self, name):
 
484
        if name == 'value':
 
485
            self.get_value()
 
486
        elif name == 'x':
 
487
            return self.canvas.tk_widget.coords(self.id)[0]
 
488
        elif name == 'y':
 
489
            return self.canvas.tk_widget.coords(self.id)[1]
 
490
        else:
 
491
            return self.canvas.tk_widget.itemcget(self.id, name)
 
492
 
 
493
    def destroy(self):
 
494
        self.canvas.delete(self)
 
495
        self.canvas = self.id = None
 
496
 
 
497
class CanvasImage(CanvasObject):
 
498
    properties = dict(
 
499
        value=('image', HasProperties.prop_image),
 
500
        x=('x', HasProperties.prop_int),
 
501
        y=('y', HasProperties.prop_int),
 
502
        anchor=('anchor', HasProperties.prop_anchor),
 
503
    )
 
504
    def create_object(self, position, spec):
 
505
        self.id = self.canvas.tk_widget.create_image(position,
 
506
            image=self.args['image'],
 
507
            anchor=self.args.get('anchor', tk.NW))
 
508
    def set_value(self, value):
 
509
        self.args['image'] = value
 
510
        return self.canvas.tk_widget.itemconfig(self.id, image=value)
 
511
    def get_value(self):
 
512
        return self.canvas.tk_widget.itemcget(self.id, 'image')
 
513
CanvasObject.register_class(CanvasImage, 'Image')
 
514
 
 
515
class CanvasLabel(CanvasObject):
 
516
    properties = dict(
 
517
        value=('text', HasProperties.prop_passthrough),
 
518
        x=('x', HasProperties.prop_int),
 
519
        y=('y', HasProperties.prop_int),
 
520
        anchor=('anchor', HasProperties.prop_anchor),
 
521
    )
 
522
    def create_object(self, position, spec):
 
523
        self.id = self.canvas.tk_widget.create_text(position,
 
524
            text=self.args.get('text', ''),
 
525
            anchor=self.args.get('anchor', tk.NW))
 
526
    def set_value(self, value):
 
527
        return self.canvas.tk_widget.itemconfig(self.id, text=value)
 
528
    def get_value(self):
 
529
        return self.canvas.tk_widget.itemcget(self.id, 'text')
 
530
CanvasObject.register_class(CanvasLabel, 'Label')
 
531
 
 
532
class Canvas(Implementation):
 
533
    properties = dict(
 
534
        width=('width', HasProperties.prop_passthrough),
 
535
        height=('height', HasProperties.prop_passthrough),
 
536
        background=('background', HasProperties.prop_color_or_image),
 
537
    )
 
538
    def create_widget(self):
 
539
        self.event_handlers = []
 
540
 
 
541
        # background image?
 
542
        image = self.args.get('background')
 
543
        if isinstance(image, ImageTk.PhotoImage):
 
544
            if 'width' not in self.args:
 
545
                self.args['width'] = image.width()
 
546
            if 'height' not in self.args:
 
547
                self.args['height'] = image.height()
 
548
        elif image is not None:
 
549
            kw['background'] = image
 
550
            image = None
 
551
 
 
552
        # basic args
 
553
        kw = {}
 
554
        for name in 'width height'.split():
 
555
            if name in self.args:
 
556
                kw[name] = self.args[name]
 
557
 
 
558
        # create the widget
 
559
        self.tk_widget = tk.Canvas(self.parent.tk_widget, **kw)
 
560
 
 
561
        # add bg image?
 
562
        if image is not None:
 
563
            self.bg_image_id = self.tk_widget.create_image((0,0),
 
564
                image=image, anchor=tk.NW)
 
565
 
 
566
        for child in self.spec.children:
 
567
            CanvasObject.construct(child, self)
 
568
 
 
569
        # TODO if mouse required
 
570
        self.tk_widget.bind("<Button-1>", self.on_mouse)
 
571
 
 
572
    def on_mouse(self, event):
 
573
        for w, id, handlers in self.event_handlers:
 
574
            if 'on_mouse' not in handlers: continue
 
575
            x1, y1, x2, y2 = self.tk_widget.bbox(id)
 
576
            if event.x < x1 or event.x > x2: continue
 
577
            if event.y < y1 or event.y > y2: continue
 
578
            MouseEvent(w, handlers['on_mouse'])(event)
 
579
            break
 
580
 
 
581
    def delete(self, child):
 
582
        self.event_handlers = [h
 
583
            for h in self.event_handlers
 
584
                if h[1] != child.id
 
585
        ]
 
586
        self.tk_widget.delete(child.id)
 
587
    
 
588
    def add_child(self, spec):
 
589
        return CanvasObject.construct(spec, self)
 
590
 
 
591
Implementation.register_class(Canvas)
 
592
 
 
593
class MouseEvent(object):
 
594
    def __init__(self, spec, action, left=False, middle=False, right=False):
 
595
        self.spec = spec
 
596
        self.left = left
 
597
        self.middle = middle
 
598
        self.right = right
 
599
        self.action = action
 
600
        
 
601
    def __call__(self, event):
 
602
        self.x , self.y = event.x, event.y
 
603
        self.action(self.spec, self)
 
604