8
from io import StringIO
10
from StringIO import StringIO
16
from PIL import Image as PIL_Image
17
from PIL import ImageTk
21
class PackLayout(object):
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)
34
class GUI(PackLayout):
35
'''The owner of the implementaton of a withgui spec.
37
Holds the toplevel widget Implementation, the animators and the
40
def __init__(self, spec):
43
self.tk_widget = tk.Tk()
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')
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)
57
self.toplevel = Implementation.construct(spec, self)
60
for delay, action in spec.after_actions:
61
self.tk_widget.after(int(delay*1000), action)
63
# set up top-level event handlers
64
for name, action in spec.event_handlers.items():
65
if name == 'on_mouse':
67
toplevel.bind("<Button-1>", MouseEvent(spec, action, left=True))
70
dt = time.time() - self.ts
72
for animator in list(self.animators):
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)
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()
87
self.tk_widget.mainloop()
89
def stop(self, data=None):
90
self.tk_widget.destroy()
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))
107
path = self.spec.gui.resource.lookup(spec)
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)
117
prop_anchor = lambda self, v: dict(center=tk.CENTER, sw=tk.SW,
120
# properties are names mapped to the Tkinter name and conversion callable
121
base_properties = dict(
122
justify=('justify', prop_passthrough),
125
width=('width', prop_passthrough),
126
height=('height', prop_passthrough),
127
foreground=('foreground', prop_color),
128
background=('background', prop_color),
131
fill=('fill', prop_fill),
132
anchor=('anchor', prop_anchor),
133
expand=('expand', prop_int),
135
value=('value', prop_passthrough),
139
def generate_properties(self, spec):
141
self.gui.animators.append(spec.animate)
143
for k,v in spec.settings.items():
144
if k in self.properties:
145
k, p = self.properties[k]
147
k, p = self.base_properties[k]
151
def translate_value(self, name, value):
152
if name in self.properties:
153
k, p = self.properties[name]
155
k, p = self.base_properties[name]
156
return k, p(self, value)
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.
163
implementation_classes = {}
165
def register_class(cls, klass, name=None):
167
name = klass.__name__
168
cls.implementation_classes[name] = klass
171
def construct(cls, spec, parent):
172
klass = cls.implementation_classes[spec.__class__.__name__]
173
return klass(spec, parent)
175
def __init__(self, spec, parent):
178
self.gui = parent.gui
179
self.tk_widget = None
180
self.args = self.generate_properties(spec)
186
# set up event handlers
187
if 'on_mouse' in spec.event_handlers:
189
image.bind("<Button-1>", MouseEvent(spec,
190
spec.event_handlers['on_mouse'], left=True))
192
self.spec.implementation = self
194
def create_widget(self):
195
'''Create the underlying tk_widget / tk_variable.
197
raise NotImplementeError('create_widget must be implemented')
199
def set_value(self, value):
200
self.tk_variable.set(value)
202
return self.tk_variable.get()
203
def set_property(self, name, value):
204
name, value = self.translate_value(name, value)
206
self.set_value(value)
209
self.tk_widget.x = value
210
self.tk_widget.place(x=value)
212
self.tk_widget.y = value
213
self.tk_widget.place(y=value)
215
self.tk_widget.config(**{name: value})
216
def get_property(self, name):
218
return self.get_value()
220
return self.tk_widget.x
222
return self.tk_widget.y
224
return getattr(self.tk_widget, name)
227
self.spec.implementation = None
231
self.tk_widget.unbind("<Button-1>")
232
self.tk_widget.destroy()
233
self.tk_widget = self.tk_variable = None
235
class Frame(Implementation):
237
side=('side', HasProperties.prop_passthrough),
238
background=('background', HasProperties.prop_color_or_image),
240
conprops = 'width height side background'.split()
242
def create_widget(self):
243
# oh, for dict comprehensions
245
for name in self.conprops:
246
if name in self.args:
247
kw[name] = self.args[name]
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()
256
self.tk_widget = tk.Frame(self.parent.tk_widget, **kw)
258
if image is not None:
260
self.image_widget = tk.Label(self.tk_widget,
262
self.image_widget.place(x=0, y=0, anchor=tk.NW)
265
# this is is done here so it may be customised per widget (well,
267
for child in self.spec.children:
268
Implementation.construct(child, self)
270
def add_child(self, spec):
271
Implementation.construct(spec, self)
273
def layout(self, child):
274
'''Add children by x, y coordinates.
276
# TODO check child has x, y and raise sensible error
279
anchor = child.args.get('anchor', tk.CENTER)
280
child.tk_widget.place(x=x, y=y, anchor=anchor)
285
Implementation.register_class(Frame)
287
class Column(PackLayout, Frame):
289
Implementation.register_class(Column)
291
class Row(PackLayout, Frame):
293
Implementation.register_class(Row)
295
class Label(Implementation):
297
value=('text', HasProperties.prop_passthrough),
300
conprops = 'background foreground'.split()
302
def create_widget(self):
303
# oh, for dict comprehensions
305
for name in self.conprops:
306
if name in self.args:
307
kw[name] = self.args[name]
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)
313
Implementation.register_class(Label)
316
def __init__(self, spec, parent, pack={}):
317
return self.add_Label(spec, parent, pack=pack)
318
Implementation.register_class(Help)
320
class Image(Implementation):
322
value=('image', HasProperties.prop_image),
325
def create_widget(self):
326
self.tk_widget = tk.Label(self.parent.tk_widget,
327
image=self.args['image'])
329
def set_value(self, value):
330
self.tk_widget.config(image=value)
331
self.args['image'] = value
333
Implementation.register_class(Image)
335
class Text(Implementation):
337
value=('text', HasProperties.prop_passthrough),
339
conprops = 'background foreground'.split()
340
def create_widget(self):
341
initial = self.args.get('text', '')
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)
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)
353
return self.tk_widget.get()
354
Implementation.register_class(Text)
356
class Selection(Implementation):
358
options=('options', HasProperties.prop_passthrough),
359
value=('value', HasProperties.prop_passthrough),
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)
368
args = (self.parent.tk_widget, self.tk_variable) + tuple(options)
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)
378
image=('image', HasProperties.prop_image),
379
value=('text', HasProperties.prop_passthrough),
382
conprops = 'background foreground'.split()
383
def create_widget(self):
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')
397
class Form(Implementation):
398
def create_widget(self):
399
self.tk_widget = tk.Frame(self.parent.tk_widget)
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)
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)
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)
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)
423
def layout(self, child):
426
Implementation.register_class(Form, 'Form')
430
class CanvasObject(HasProperties):
431
implementation_classes = {}
433
def register_class(cls, klass, name=None):
435
name = klass.__name__
436
cls.implementation_classes[name] = klass
439
def construct(cls, spec, canvas):
440
klass = cls.implementation_classes[spec.__class__.__name__]
441
impl = klass(spec, canvas)
443
def __init__(self, spec, canvas):
445
self.gui = canvas.gui
448
self.args = self.generate_properties(spec)
449
self.x, self.y = position = self.args['x'], self.args['y']
451
self.create_object(position, spec)
453
if spec.event_handlers:
454
self.canvas.event_handlers.append((spec, self.id,
455
spec.event_handlers))
456
# TODO self.handlers_required
458
spec.implementation = self
460
def set_value(self, value):
461
raise NotImplementedError('set_value not implemented for %s'%
462
self.tk_widget.type(self.id))
464
def set_property(self, name, value):
465
tname, value = self.translate_value(name, value)
467
self.set_value(value)
471
pos = (value, self.y)
472
self.canvas.tk_widget.coords(self.id, pos)
474
pos = (self.x, value)
475
self.canvas.tk_widget.coords(self.id, pos)
477
self.canvas.tk_widget.itemconfig(self.id, **{name: value})
480
raise NotImplementedError('get_value not implemented for %s'%
481
self.tk_widget.type(self.id))
483
def get_property(self, name):
487
return self.canvas.tk_widget.coords(self.id)[0]
489
return self.canvas.tk_widget.coords(self.id)[1]
491
return self.canvas.tk_widget.itemcget(self.id, name)
494
self.canvas.delete(self)
495
self.canvas = self.id = None
497
class CanvasImage(CanvasObject):
499
value=('image', HasProperties.prop_image),
500
x=('x', HasProperties.prop_int),
501
y=('y', HasProperties.prop_int),
502
anchor=('anchor', HasProperties.prop_anchor),
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)
512
return self.canvas.tk_widget.itemcget(self.id, 'image')
513
CanvasObject.register_class(CanvasImage, 'Image')
515
class CanvasLabel(CanvasObject):
517
value=('text', HasProperties.prop_passthrough),
518
x=('x', HasProperties.prop_int),
519
y=('y', HasProperties.prop_int),
520
anchor=('anchor', HasProperties.prop_anchor),
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)
529
return self.canvas.tk_widget.itemcget(self.id, 'text')
530
CanvasObject.register_class(CanvasLabel, 'Label')
532
class Canvas(Implementation):
534
width=('width', HasProperties.prop_passthrough),
535
height=('height', HasProperties.prop_passthrough),
536
background=('background', HasProperties.prop_color_or_image),
538
def create_widget(self):
539
self.event_handlers = []
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
554
for name in 'width height'.split():
555
if name in self.args:
556
kw[name] = self.args[name]
559
self.tk_widget = tk.Canvas(self.parent.tk_widget, **kw)
562
if image is not None:
563
self.bg_image_id = self.tk_widget.create_image((0,0),
564
image=image, anchor=tk.NW)
566
for child in self.spec.children:
567
CanvasObject.construct(child, self)
569
# TODO if mouse required
570
self.tk_widget.bind("<Button-1>", self.on_mouse)
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)
581
def delete(self, child):
582
self.event_handlers = [h
583
for h in self.event_handlers
586
self.tk_widget.delete(child.id)
588
def add_child(self, spec):
589
return CanvasObject.construct(spec, self)
591
Implementation.register_class(Canvas)
593
class MouseEvent(object):
594
def __init__(self, spec, action, left=False, middle=False, right=False):
601
def __call__(self, event):
602
self.x , self.y = event.x, event.y
603
self.action(self.spec, self)