1
# -*- coding: utf-8 -*-
2
# Elisa - Home multimedia server
3
# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
6
# This file is available under one of two license agreements.
8
# This file is licensed under the GPL version 3.
9
# See "LICENSE.GPL" in the root of this distribution including a special
10
# exception to use Elisa with Fluendo's plugins.
12
# The GPL part of Elisa is also available under a commercial licensing
13
# agreement from Fluendo.
14
# See "LICENSE.Elisa" in the root directory of this distribution package
15
# for details on that license.
18
from elisa.plugins.pigment.graph.image import Image
19
from elisa.plugins.pigment.graph.text import Text
20
from elisa.plugins.pigment.widgets import Widget, Button
21
from elisa.plugins.pigment.widgets.const import *
22
from elisa.plugins.pigment.widgets.theme import Theme
27
from pgm.timing import implicit
28
from pgm.timing.implicit import AnimatedObject
30
from pkg_resources import resource_filename
32
from xml.etree import ElementTree
38
# TODO: implement style properties
39
# TODO: implement image/widget cursor
40
# FIXME: use a real logging system --> port the widgets to Loggable
41
# FIXME: better default style
43
class LayoutParsingError(Exception):
46
class UnknownLayerError(Exception):
50
"""One key as found in the layout file. Private API."""
52
def __init__ (self, **kwargs):
54
self.type = kwargs.get('type', 'char')
55
self.value = kwargs.get('value')
56
self.display = kwargs.get('display')
57
self.image_resource = kwargs.get('image-resource')
58
self.width = float(kwargs.get('width', 1))
59
self.layers = map(str.strip, kwargs.get('layers', '').split(','))
61
raise LayoutParsingError(e)
67
"""The actual label to display on the key."""
68
return str(self.display or self.value or '')
71
return pprint.pformat(self.__dict__)
75
"""A row of keys in the layout file. Private API."""
80
def append_key(self, key):
81
"""Add a key in the row, adding a back reference in the key."""
87
"""A layer of the keybord in the layout file. Private API."""
89
def __init__(self, id):
94
class KeyboardButton(Button):
97
Not using Button directly allows to style keyboard buttons indipendently
101
def _update_style_properties(self, props=None):
102
super(KeyboardButton, self)._update_style_properties(props)
107
for key, value in props.iteritems():
112
class KeyboardCursor(Widget):
113
"""Keyboard cursor."""
116
class OnScreenKeyboard(Widget):
117
"""On-screen keyboard widget.
119
@ivar selected_key: the currently selected key, as (row, column)
120
@type selected_key: tuple of int
121
@ivar layers: the list of swappable layers' ids defined for the keyboard
122
@type layers: list of strings
125
__gsignals__ = {'key-press': (gobject.SIGNAL_RUN_LAST,
126
gobject.TYPE_BOOLEAN,
128
'key-release': (gobject.SIGNAL_RUN_LAST,
129
gobject.TYPE_BOOLEAN,
132
def __init__(self, layout_file=None, layer="layer1"):
133
super(OnScreenKeyboard, self).__init__()
135
self._background = Image()
136
self.add(self._background, forward_signals=False)
137
self._background.visible = True
139
self.selected_key = None
141
self._layout_file = layout_file \
142
or resource_filename('elisa.plugins.pigment.widgets',
143
'mockup_keyboard.xml')
145
self._layer_widgets = {}
146
self._fixed_layer = None
147
self._selected_layer = layer
149
self._fixed_keys = []
151
self._animated_cursor = None
158
self._update_style_properties(self._style.get_items())
160
def _update_style_properties(self, props=None):
161
super(OnScreenKeyboard, self)._update_style_properties(props)
166
for key, value in props.iteritems():
167
if key == 'text-color':
168
for key in self._get_key_buttons():
169
# FIXME: using internal API of Button
170
key._label.fg_color = value
171
elif key == 'text-height':
172
for key in self._get_key_buttons():
173
# FIXME: using internal API of Button
174
key._label.height = value
175
key._label.y = 0.5 - (value / 2.0)
177
elif key == 'background-color':
178
self._background.bg_color = value
179
self.opacity = value[-1]
181
def _set_cursor(self):
183
self.remove(self._cursor)
185
self._cursor = Image()
186
self.add(self._cursor, forward_signals=False)
187
self._cursor.bg_color = (0, 255, 0, 200)
188
self._cursor.x, self._cursor.y, self._cursor.width, self._cursor.height = (0, 0, 0, 0)
189
self._cursor.visible = True
190
self._animated_cursor = AnimatedObject(self._cursor)
191
settings = {'duration': 300,
192
'transformation': implicit.DECELERATE,
194
self._animated_cursor.setup_next_animations(**settings)
196
def _parse_layout(self):
197
keyboard = ElementTree.parse(self._layout_file)
199
for layer_el in keyboard.getiterator('layer'):
200
layer_id = layer_el.attrib.get('id')
202
print "Skipping layer without 'id' attribute"
204
layer = Layer(layer_id)
205
for row_el in layer_el.getiterator('row'):
207
for key_el in row_el.getiterator('key'):
208
key = Key(**key_el.attrib)
210
layer.rows.append (row)
212
if layer_id == 'fixed':
213
self._fixed_layer = layer
215
self._layers[layer_id] = layer
218
for layer_id, layer_widget in self._layer_widgets.items():
219
self.remove(layer_widget)
220
self._layer_widgets.pop(layer_id)
222
self._render_layer('fixed')
223
self._render_layer(self._selected_layer)
225
def _render_layer(self, id):
227
if self._fixed_layer == None:
228
print "No fixed layer"
231
layer = self._fixed_layer
232
self._fixed_keys = []
233
keys = self._fixed_keys
235
layer = self._layers.get(id)
237
# copy the keys of the fixed layer
238
for r in self._fixed_keys:
239
self._keys.append(copy.copy(r))
242
print "No layer named", id
245
if len(layer.rows) == 0:
246
print "No rows in the layer"
249
print "Rendering layer", id
250
layer_widget = Widget()
251
self.add(layer_widget, forward_signals=False)
252
layer_widget.width, layer_widget.height = (1.0, 1.0)
254
theme = Theme.get_default()
256
row_height = 1.0 / len(layer.rows)
257
for i, row in enumerate(layer.rows):
262
assert len(keys) == (i + 1)
264
if len(row.keys) == 0:
267
key_width = 1.0 / sum([k.width for k in row.keys])
269
for j, key in enumerate(row.keys):
270
# FIXME: this stops the row! why?!
271
if key.type == 'placeholder':
275
key_widget = KeyboardButton()
277
keys[i].append(key_widget)
279
key_widget.connect("clicked", self._set_selected_key)
280
# HACK: store the value of the key
281
key_widget._value = key.value
282
if key.type == 'layer_switch':
283
key_widget.connect("clicked", self._swap_layers, key.layers)
284
elif key.type == 'char':
285
key_widget.connect("clicked", self._emit_text, key.value)
287
layer_widget.add(key_widget, forward_signals=False)
289
key_widget.width = key_width * key.width
290
key_widget.height = row_height - (key_widget.style.padding * 2)
292
key_widget.y = (row_height * i) + key_widget.style.padding
293
key_widget.bg_color = (0, 0, 0, 0)
294
key_widget.fg_color = (255, 255, 255, 255)
295
key_widget.label = key.label
297
# FIXME: using internal API of Button
298
key_widget._label.x = 0.0
299
key_widget._label.y = (1.0 - self.style.text_height) / 2.0
300
key_widget._label.height = self.style.text_height
301
key_widget._label.width = 1.0
302
key_widget._label.font_height = 1.0
303
#key_widget._label.bg_color = (255, 0, 0, 100)
304
key_widget._label.fg_color = self.style.text_color
305
key_widget._label.weight = pgm.TEXT_WEIGHT_BOLD
306
key_widget._label.alignment = pgm.TEXT_ALIGN_CENTER
308
if key.image_resource:
309
image_file = theme.get_resource(key.image_resource)
311
label_width = key_widget.style.label_width
312
image_width = key_widget.style.image_width
315
key_widget.add(image)
316
image.set_from_file(image_file)
317
image.height = self.style.text_height
318
image.width = image_width
319
image.bg_color = (0, 0, 0, 0)
320
image.y = 0.5 - (self.style.text_height / 2.0)
323
if key_widget.label == '':
324
image.x = (1.0 - image_width) / 2.0
326
image.x = (1.0 - label_width - image_width) / 2.0
328
# FIXME: using internal API of Button
329
key_widget._label.width = label_width
330
key_widget._label.x = image.x + image.width + key_widget.style.spacing
331
key_widget._label.alignment = pgm.TEXT_ALIGN_LEFT
333
key_x += key_widget.width
334
key_widget.width = (key_width * key.width) - (key_widget.style.padding * 2)
336
key_widget.visible = True
338
layer_widget.opacity = 0
339
layer_widget.visible = True
341
widget_animated = AnimatedObject(layer_widget)
342
widget_animated.update_animation_settings(duration=400)
343
widget_animated.opacity = 255
345
self._layer_widgets[id] = layer_widget
347
def _get_key_buttons(self):
348
for row in self._keys:
352
def _set_selected_key(self, key, *placeholders):
353
for i, row in enumerate(self._keys):
354
for j, item in enumerate(row):
355
if self._keys[i][j] == key:
356
self.selected_key = (i, j)
357
self._animated_cursor.x, self._animated_cursor.y = key.x, key.y
358
self._animated_cursor.width, self._animated_cursor.height = key.width, key.height
361
def _swap_layers(self, key, x, y, z, type, time, data, layers):
363
current = layers.index(self.get_layer())
365
print "Cannot swap layers: current layer is unknown"
366
raise UnknownLayerError()
368
self.set_layer(layers[(current + 1) % len(layers)])
370
def _emit_text(self, key, x, y, z, type, time, data, value):
371
self.emit("key-press", value)
375
"""Get the list of swappable layers' ids defined for the keyboard.
377
@rtype: list of strings
379
return self._layers.keys()
381
def get_language(self):
382
"""Get the language of the keyabord.
384
@returns: the ISO 639-1 code of the language
389
"""Get the current active layer.
391
@returns: the layer id
394
return self._selected_layer
396
def set_layer(self, id):
397
"""Set the active layer.
399
@param id: the id of the layer to set
402
for layer_id, layer_widget in self._layer_widgets.items():
403
if layer_id != 'fixed':
404
self._selected_layer = id
405
widget_animated = AnimatedObject(layer_widget)
407
# Attention! Pass the correct context here.
408
def end_animation(timer, layer_widget=layer_widget, layer_id=layer_id):
409
self.remove(layer_widget)
410
self._layer_widgets.pop(layer_id)
411
self._render_layer(id)
414
widget_animated.update_animation_settings(duration=400,
415
end_callback=end_animation)
416
widget_animated.opacity = 0
418
def move_cursor(self, direction):
419
"""Move the cursor in the specified direction.
421
@param direction: where to move the cursor
422
@type direction: one of L[elisa.plugins.pigment.widgets.const.[LEFT|RIGHT|TOP|BOTTOM]}
424
if not self.selected_key:
427
row = self.selected_key[0]
428
column = self.selected_key[1]
429
current = self._keys[row][column]
431
if direction == LEFT:
433
column = len(self._keys[row]) - 1
436
elif direction == RIGHT:
437
if column >= len(self._keys[row]) - 1:
441
elif direction == TOP:
443
row = len(self._keys) - 1
445
while len(self._keys[row - 1]) == 0:
448
deltas = map(lambda w: abs(current.x - w.x), self._keys[row])
449
column = deltas.index(min(deltas))
450
elif direction == BOTTOM:
451
if row >= len(self._keys) - 1:
454
while len(self._keys[row + 1]) == 0:
457
deltas = map(lambda w: abs(current.x - w.x), self._keys[row])
458
column = deltas.index(min(deltas))
460
self._set_selected_key(self._keys[row][column])
461
self._keys[row][column].focus = True
463
def do_key_press_event(self, viewport, event, widget):
464
"""Default handler for the key-press-event"""
466
if event.keyval == pgm.keysyms.Return:
467
key = self._keys[self.selected_key[0]][self.selected_key[1]]
468
key.emit("pressed", 0, 0, 0, pgm.BUTTON_LEFT, 0, 0)
469
key.emit("clicked", 0, 0, 0, pgm.BUTTON_LEFT, 0, 0)
470
key.emit("released", 0, 0, 0, pgm.BUTTON_LEFT, 0)
471
elif event.keyval == pgm.keysyms.Left:
472
self.move_cursor(LEFT)
473
elif event.keyval == pgm.keysyms.Right:
474
self.move_cursor(RIGHT)
475
elif event.keyval == pgm.keysyms.Up:
476
self.move_cursor(TOP)
477
elif event.keyval == pgm.keysyms.Down:
478
self.move_cursor(BOTTOM)
481
for key in self._get_key_buttons():
482
value = unicode((key._value or '').lower())
483
pgm_value = pgm.keyval_to_unicode(event.keyval)
484
unicode_value = unichr(pgm_value)
485
if value == unicode_value:
486
self._set_selected_key(key)
490
def do_key_release_event(self, viewport, event, widget):
491
"""Default handler for the key-release-event"""
492
self.emit("key-release")
495
def _demo_widget(cls, *args, **kwargs):
497
widget = cls(*sys.argv[1:])
501
widget.visible = True
503
def on_key_press(keyboard, text):
504
print "OnScreenKeyboard key-press:", text
506
widget.connect("key-press", on_key_press)
511
def _set_demo_widget_defaults(cls, widget, canvas, viewport):
512
Widget._set_demo_widget_defaults(widget, canvas, viewport)
518
if __name__ == '__main__':
519
logger = logging.getLogger()
520
logger.setLevel(logging.WARNING)
522
keyboard = OnScreenKeyboard.demo()