7
# a whole lotta constants
23
# color code below originally from pyglet contrib/layout (with extensions)
25
def __new__(cls, r, g, b, a=1):
26
return tuple.__new__(cls, (r, g, b, a))
28
def to_hex(self, a=False):
30
return '%02x%02x%02x%02x'%(self[0]*255, self[1]*255,
31
self[2]*255, self[3]*255)
33
return '%02x%02x%02x'%(self[0]*255, self[1]*255, self[2]*255)
36
def from_hex(cls, hex):
38
return Color(int(hex[0], 16) / 15.,
39
int(hex[1], 16) / 15.,
40
int(hex[2], 16) / 15.)
42
return Color(int(hex[0], 16) / 15.,
43
int(hex[1], 16) / 15.,
44
int(hex[2], 16) / 15.,
45
int(hex[3], 16) / 15.)
47
return Color(int(hex[0:2], 16) / 255.,
48
int(hex[2:4], 16) / 255.,
49
int(hex[4:6], 16) / 255.)
51
return Color(int(hex[0:2], 16) / 255.,
52
int(hex[2:4], 16) / 255.,
53
int(hex[4:6], 16) / 255.,
54
int(hex[6:8], 16) / 255.)
58
'maroon': Color.from_hex('800000'),
59
'red': Color.from_hex('ff0000'),
60
'orange': Color.from_hex('ffa500'),
61
'yellow': Color.from_hex('ffff00'),
62
'olive': Color.from_hex('808000'),
63
'purple': Color.from_hex('800080'),
64
'fuschia': Color.from_hex('ff00ff'),
65
'white': Color.from_hex('ffffff'),
66
'lime': Color.from_hex('00ff00'),
67
'green': Color.from_hex('008000'),
68
'navy': Color.from_hex('000080'),
69
'blue': Color.from_hex('0000ff'),
70
'aqua': Color.from_hex('00ffff'),
71
'teal': Color.from_hex('008080'),
72
'black': Color.from_hex('000000'),
73
'silver': Color.from_hex('c0c0c0'),
74
'gray': Color.from_hex('808080'),
78
maroon = Color.names['maroon']
79
red = Color.names['red']
80
orange = Color.names['orange']
81
yellow = Color.names['yellow']
82
olive = Color.names['olive']
83
purple = Color.names['purple']
84
fuschia = Color.names['fuschia']
85
white = Color.names['white']
86
lime = Color.names['lime']
87
green = Color.names['green']
88
navy = Color.names['navy']
89
blue = Color.names['blue']
90
aqua = Color.names['aqua']
91
teal = Color.names['teal']
92
black = Color.names['black']
93
silver = Color.names['silver']
94
gray = Color.names['gray']
96
def prop_color(value):
97
'''Parse a color value which is one of:
99
name a color name (CSS 2.1 standard colors)
100
RGB a three-value hex color
101
RRGGBB a three-value hex color
103
if not isinstance(value, str):
105
if value in Color.names:
106
return Color.names[value]
107
return Color.from_hex(value)
108
def prop_color_or_image(value):
109
if not isinstance(value, str):
111
if value in Color.names:
112
return Color.names[value]
113
if re.match(r'[0-9a-f]{3-8}', value, re.I):
114
return Color.from_hex(value)
117
def prop_image(value):
120
def prop_direction(value):
123
def prop_justify_options(value):
124
assert value in ('left', 'right', 'center')
126
def prop_fill(value):
127
assert value in ('x', 'y', 'both')
129
def prop_boolean(value):
131
def prop_number(value):
134
def prop_width(value):
137
def prop_height(value):
140
def prop_passthrough(value): return value
142
class PropertiesDict(dict):
143
'''Extension of dict that limits the keys to those in the properties
144
dict and enforces value types (and handles some type conversions).
146
def __init__(self, properties, d):
147
self.properties = properties
149
def __setitem__(self, key, value):
150
if key not in self.properties:
151
raise KeyError('%s is not a valid property'%key)
152
value = self.properties[key](value)
153
super(PropertiesDict, self).__setitem__(key, value)
156
if key not in self.properties:
157
raise KeyError('%s is not a valid property'%key)
158
d[key] = self.properties[key](d[key])
159
super(PropertiesDict, self).update(d)
161
class WidgetBase(object):
165
def register_class(cls, klass, name=None):
167
name = klass.__name__.lower()
168
cls.widget_classes[name] = klass
170
# instance attribute defaults
172
implementation = None
174
name = None # this will be filled in by the locals() magic
182
def __init__(self, *args, **kw):
185
self.parent = kw.pop('parent')
186
self.gui = self.parent.gui
187
self.parent.add_child(self)
189
# I'm the top-level widget so act like one
190
self.stack = [self] # CIRCULAR REF here
191
self.gui = self # CIRCULAR REF here
192
self.after_actions = []
193
self.window_settings = dict(title='withgui')
194
self.resource = Resource()
197
self.event_handlers = {}
198
self.named_widgets = {}
200
# TODO allow layouts to accept a list of child widgets
201
# TODO rename "parent" to "container" or "box" or something
203
settings = dict(self.defaults)
206
kw.update(dict(zip(self.fixed_args, args)))
208
# callbacks / animate
210
if k == 'animate' or k.startswith('on_'):
211
self.add_handler(k, kw.pop(k))
214
self.settings = PropertiesDict(self.properties, settings)
216
# if we're already physical then add the widget to the actual gui
217
if self.parent and self.parent.implementation:
218
# TODO handle no parent - create new top-level window
219
self.parent.implementation.add_child(self)
221
def __getattr__(self, name):
222
if name in self.widget_classes:
224
parent = self.gui.stack[-1]
227
return functools.partial(self.widget_classes[name],
230
if self.implementation is not None:
231
return self.implementation.get_property(name)
232
elif name in self.properties:
233
return self.settings[name]
235
return self.__dict__[name]
237
raise AttributeError('%r has no attribute %s'%(self, name))
239
def __setattr__(self, name, value):
240
if name in ('parent', 'implementation', 'gui', 'extra'):
241
self.__dict__[name] = value
242
elif self.implementation is not None:
243
self.implementation.set_property(name, value)
244
elif name in self.properties:
245
self.settings[name] = self.properties[name](value)
247
self.__dict__[name] = value
250
def __call__(self, func):
251
'''Decorate some function - or rather schlurp it into this widget
252
spec as some event handler or animator.
254
# TODO refactor so this isn't duplicated in __exit__
255
# are we invoked as a function decorator?
256
if not hasattr(func, '__call__'):
257
raise ValueError('only callable as a decorator')
258
name = func.func_name
259
self.add_handler(name, func)
260
# this for the locals() hax
263
def add_handler(self, name, callable):
264
if name == 'animate':
265
# hi, I'm a generator, so generate me
266
self.animate = callable(self)
267
self.animate.send(None)
268
elif name.startswith('on_'):
270
self.event_handlers[name] = callable
272
# not a name I recognise - ignore it
276
# overridable child adder (for example see Form.add_child)
277
def add_child(self, child):
278
self.children.append(child)
281
def remove_child(self, child):
282
self.children.remove(child)
285
def __getitem__(self, item):
286
if isinstance(item, str):
288
name = item[1:].lower()
289
return [child for child in self.children
290
if child.__class__.__name__.lower() == name]
291
return self.named_widgets[item]
293
return self.children[item]
296
# save off the current set of names in the locals()
298
locals = inspect.getargvalues(f)[3]
299
self._names = set((k, id(v)) for k, v in locals.items())
300
self.gui.stack.append(self)
303
def __exit__(self, exc_type, exc_val, exc_tb):
304
# determine whether any new local variables were created during the
305
# context manager's lifespan and if so (and they're not already
306
# claimed) then add to this object's named objects / event handlers
308
locals = inspect.getargvalues(f)[3]
310
# TODO don't store locals names on the object, be smarter about
311
# with "scopes" and store a new stack or something
312
# TODO this is currently buggy - see the second on_click in
319
if key in self._names:
322
if getattr(o, 'name', None) is not None:
326
# this is my with statement, don't name me!
329
if isinstance(o, WidgetBase):
330
self.named_widgets[name] = o
332
elif hasattr(o, '__call__'):
333
if name == 'animate':
334
# TODO check signature for dt?
335
# hi, I'm a generator, so generate and kick me off
336
self.animate = o(self)
337
self.animate.send(None)
338
elif name.startswith('on_'):
339
self.event_handlers[name] = o
341
# not a name I recognise - ignore ir
345
# not a widget or callable, ignore it
351
def dump(self, level=''):
352
print '%s%s %r'%(level, self.__class__.__name__.lower(),
354
for c in self.children:
357
def after(self, delay, action=None):
358
'''Schedule some delayed action.
370
if action is not None:
371
self.gui.after_actions.append((delay, action))
374
self.gui.after_actions.append((delay, action))
379
#from withsimplui import GUI
380
#from withqt4 import GUI
381
#from withtk import GUI
382
from withkytten import GUI
384
# the assignment here isn't strictly necessary
388
def stop(self, data=None):
389
return self.gui.stop(data)
391
def window(self, **kw):
392
# TODO validate the keywords / values
393
self.gui.window_settings.update(kw)
396
# remove me from my parent and if I'm implemented then destroy that
398
self.parent.remove_child(self)
399
if self.implementation:
400
self.implementation.destroy()
405
# TODO Marked-up text
406
class Frame(WidgetBase):
407
# TODO other properties
410
background=prop_color_or_image,
414
WidgetBase.register_class(Frame)
416
class Label(WidgetBase):
417
fixed_args = ('value',)
418
defaults = dict(value='', anchor=nw)
420
foreground=prop_color,
421
background=prop_color,
422
anchor=prop_direction,
424
justify=prop_justify_options,
429
value=prop_passthrough,
431
WidgetBase.register_class(Label)
433
class Image(WidgetBase):
434
fixed_args = ('value',)
435
defaults = dict(value=None, anchor=nw)
437
anchor=prop_direction,
438
justify=prop_justify_options,
444
WidgetBase.register_class(Image)
446
class Canvas(WidgetBase):
447
# TODO other properties?
452
background=prop_color_or_image,
454
widget_classes = dict(
458
WidgetBase.register_class(Canvas)
462
WidgetBase.register_class(Column)
466
WidgetBase.register_class(Row)
468
class FormRow(object):
469
'''This encapsulates a label, widget and help text for a single row
475
def __init__(self, gui=None):
477
def needs(self, widget):
478
if isinstance(widget, Label):
479
return self.label is None
480
elif isinstance(widget, Help):
481
return self.help is None
482
return self.widget is None
483
def add(self, widget):
484
if isinstance(widget, Label):
486
elif isinstance(widget, Help):
491
class Form(WidgetBase):
492
def __init__(self, *args, **kw):
493
super(Form, self).__init__(*args, **kw)
494
self.rows = [FormRow(self.gui)]
497
def add_child(self, child):
498
if isinstance(child, FormRow):
499
self.rows.append(child)
500
elif isinstance(child, FormButton):
501
self.buttons.append(child)
503
self.children.append(child)
505
if not self.rows[-1].needs(child):
506
self.rows.append(FormRow())
507
self.rows[-1].add(child)
509
def row(self, label=None, widget=None, help=None):
510
'''Create a row in one go. The label and help are passed in as
511
strings. The widget is separately constructed.
513
Returns the widget for convenience (may be bound to a local name
514
for later access or manipulation).
516
if label is not None:
517
assert not isinstance(label, Label)
518
Label(label, parent=self)
519
if widget is not None:
520
assert isinstance(widget, WidgetBase)
522
assert not isinstance(help, Help)
523
Help(help, parent=self)
524
# for convenience this will allow local binding of the widget to a
528
WidgetBase.register_class(Form)
530
class Text(WidgetBase):
531
fixed_args = ('value',)
533
value=prop_passthrough,
535
WidgetBase.register_class(Text)
537
class Selection(WidgetBase):
538
fixed_args = ('options', 'value')
540
options=prop_passthrough,
541
value=prop_passthrough,
543
WidgetBase.register_class(Selection)
545
class Help(WidgetBase):
546
fixed_args = ('value',)
548
value=prop_passthrough,
550
WidgetBase.register_class(Help)
552
class Button(WidgetBase):
553
fixed_args = ('value',)
555
foreground=prop_color,
556
background=prop_color,
557
anchor=prop_direction,
558
justify=prop_justify_options,
563
value=prop_passthrough,
565
def on_click(self, handler):
566
self.event_handlers['on_click'] = handler
567
WidgetBase.register_class(Button)
569
class FormButton(Button):
570
# group submit / cancel button classes
572
class Submit(FormButton):
574
WidgetBase.register_class(Submit)
576
class Cancel(FormButton):
578
WidgetBase.register_class(Cancel)
580
class Resource(object):
582
self.paths = [os.path.abspath(os.path.dirname(sys.argv[0]))]
585
# TODO allow ZIP files etc.
586
self.paths.append(path)
588
def lookup(self, name):
589
for path in self.paths:
590
p = os.path.join(path, name)
591
if os.path.isfile(os.path.join(path, name)):
594
def run_for(seconds):
595
'''Become a generator over the other animation generator f() which
596
expects to be passed values from 0 to 1 over the specified time in
600
def animate(w, seconds=seconds, f=f):
615
if __name__ == '__main__':
616
with Column() as gui:
617
gui.resource.add(os.path.abspath(os.path.dirname(sys.argv[1])))
618
exec(open(sys.argv[1]).read())