~moovida-developers/moovida/widget_demo

« back to all changes in this revision

Viewing changes to elisa-plugins/elisa/plugins/pigment/widgets/onscreen_keyboard.py

  • Committer: Guido Amoruso
  • Date: 2008-06-27 10:01:54 UTC
  • mfrom: (59.8.290 upicek)
  • Revision ID: guidonte@fluendo.com-20080627100154-xq7jqpj6y6qyhy4g
MergedĀ fromĀ upicek.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# Elisa - Home multimedia server
 
3
# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
 
4
# All rights reserved.
 
5
#
 
6
# This file is available under one of two license agreements.
 
7
#
 
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.
 
11
#
 
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.
 
16
 
 
17
 
 
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
 
23
 
 
24
import gobject
 
25
 
 
26
import pgm
 
27
from pgm.timing import implicit
 
28
from pgm.timing.implicit import AnimatedObject
 
29
 
 
30
from pkg_resources import resource_filename
 
31
 
 
32
from xml.etree import ElementTree
 
33
import logging
 
34
import pprint
 
35
import copy
 
36
import sys
 
37
 
 
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
 
42
 
 
43
class LayoutParsingError(Exception):
 
44
    pass
 
45
 
 
46
class UnknownLayerError(Exception):
 
47
    pass
 
48
 
 
49
class Key(object):
 
50
    """One key as found in the layout file. Private API."""
 
51
 
 
52
    def __init__ (self, **kwargs):
 
53
        try:
 
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(','))
 
60
        except ValueError, e:
 
61
            raise LayoutParsingError(e)
 
62
 
 
63
        self.row = None
 
64
 
 
65
    @property
 
66
    def label(self):
 
67
        """The actual label to display on the key."""
 
68
        return str(self.display or self.value or '')
 
69
 
 
70
    def __repr__(self):
 
71
        return pprint.pformat(self.__dict__)
 
72
 
 
73
 
 
74
class Row(object):
 
75
    """A row of keys in the layout file. Private API."""
 
76
 
 
77
    def __init__(self):
 
78
        self.keys = []
 
79
 
 
80
    def append_key(self, key):
 
81
        """Add a key in the row, adding a back reference in the key."""
 
82
        self.keys.append(key)
 
83
        key.row = self
 
84
 
 
85
 
 
86
class Layer(object):
 
87
     """A layer of the keybord in the layout file. Private API."""
 
88
 
 
89
     def __init__(self, id):
 
90
         self.id = id
 
91
         self.rows = []
 
92
 
 
93
 
 
94
class KeyboardButton(Button):
 
95
    """Key widget.
 
96
 
 
97
       Not using Button directly allows to style keyboard buttons indipendently
 
98
       from others.
 
99
    """
 
100
 
 
101
    def _update_style_properties(self, props=None):
 
102
        super(KeyboardButton, self)._update_style_properties(props)
 
103
 
 
104
        if props is None:
 
105
            return
 
106
 
 
107
        for key, value in props.iteritems():
 
108
            if key == 'padding':
 
109
                pass
 
110
 
 
111
 
 
112
class KeyboardCursor(Widget):
 
113
    """Keyboard cursor."""
 
114
 
 
115
 
 
116
class OnScreenKeyboard(Widget):
 
117
    """On-screen keyboard widget.
 
118
 
 
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
 
123
    """
 
124
 
 
125
    __gsignals__ = {'key-press': (gobject.SIGNAL_RUN_LAST,
 
126
                                  gobject.TYPE_BOOLEAN,
 
127
                                  (str,)),
 
128
                    'key-release': (gobject.SIGNAL_RUN_LAST,
 
129
                                    gobject.TYPE_BOOLEAN,
 
130
                                    (str,))}
 
131
 
 
132
    def __init__(self, layout_file=None, layer="layer1"):
 
133
        super(OnScreenKeyboard, self).__init__()
 
134
 
 
135
        self._background = Image()
 
136
        self.add(self._background, forward_signals=False)
 
137
        self._background.visible = True
 
138
 
 
139
        self.selected_key = None
 
140
 
 
141
        self._layout_file = layout_file \
 
142
                            or resource_filename('elisa.plugins.pigment.widgets',
 
143
                                                 'mockup_keyboard.xml')
 
144
        self._layers = {}
 
145
        self._layer_widgets = {}
 
146
        self._fixed_layer = None
 
147
        self._selected_layer = layer
 
148
        self._keys = []
 
149
        self._fixed_keys = []
 
150
        self._cursor = None
 
151
        self._animated_cursor = None
 
152
 
 
153
        self._parse_layout()
 
154
        self._render()
 
155
 
 
156
        self._set_cursor()
 
157
 
 
158
        self._update_style_properties(self._style.get_items())
 
159
 
 
160
    def _update_style_properties(self, props=None):
 
161
        super(OnScreenKeyboard, self)._update_style_properties(props)
 
162
 
 
163
        if props is None:
 
164
            return
 
165
 
 
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)
 
176
                self.regenerate()
 
177
            elif key == 'background-color':
 
178
                self._background.bg_color = value
 
179
                self.opacity = value[-1]
 
180
 
 
181
    def _set_cursor(self):
 
182
        if self._cursor:
 
183
            self.remove(self._cursor)
 
184
 
 
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,
 
193
                    'resolution': 5}
 
194
        self._animated_cursor.setup_next_animations(**settings)
 
195
 
 
196
    def _parse_layout(self):
 
197
        keyboard = ElementTree.parse(self._layout_file)
 
198
 
 
199
        for layer_el in keyboard.getiterator('layer'):
 
200
            layer_id = layer_el.attrib.get('id')
 
201
            if not layer_id:
 
202
                print "Skipping layer without 'id' attribute"
 
203
                continue
 
204
            layer = Layer(layer_id)
 
205
            for row_el in layer_el.getiterator('row'):
 
206
                row = Row()
 
207
                for key_el in row_el.getiterator('key'):
 
208
                    key = Key(**key_el.attrib)
 
209
                    row.append_key(key)
 
210
                layer.rows.append (row)
 
211
 
 
212
            if layer_id == 'fixed':
 
213
                self._fixed_layer = layer
 
214
            else:
 
215
                self._layers[layer_id] = layer
 
216
 
 
217
    def _render(self):
 
218
        for layer_id, layer_widget in self._layer_widgets.items():
 
219
            self.remove(layer_widget)
 
220
            self._layer_widgets.pop(layer_id)
 
221
 
 
222
        self._render_layer('fixed')
 
223
        self._render_layer(self._selected_layer)
 
224
 
 
225
    def _render_layer(self, id):
 
226
        if id == 'fixed':
 
227
            if self._fixed_layer == None:
 
228
                print "No fixed layer"
 
229
                return
 
230
            else:
 
231
                layer = self._fixed_layer
 
232
                self._fixed_keys = []
 
233
                keys = self._fixed_keys
 
234
        else:
 
235
            layer = self._layers.get(id)
 
236
            self._keys = []
 
237
            # copy the keys of the fixed layer
 
238
            for r in self._fixed_keys:
 
239
                 self._keys.append(copy.copy(r))
 
240
            keys = self._keys
 
241
            if not layer:
 
242
                print "No layer named", id
 
243
                return
 
244
 
 
245
        if len(layer.rows) == 0:
 
246
            print "No rows in the layer"
 
247
            return
 
248
 
 
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)
 
253
 
 
254
        theme = Theme.get_default()
 
255
 
 
256
        row_height = 1.0 / len(layer.rows)
 
257
        for i, row in enumerate(layer.rows):
 
258
            try:
 
259
                keys[i]
 
260
            except:
 
261
                keys.append([])
 
262
                assert len(keys) == (i + 1)
 
263
 
 
264
            if len(row.keys) == 0:
 
265
                continue
 
266
 
 
267
            key_width = 1.0 / sum([k.width for k in row.keys])
 
268
            key_x = 0
 
269
            for j, key in enumerate(row.keys):
 
270
                # FIXME: this stops the row! why?!
 
271
                if key.type == 'placeholder':
 
272
                    key_x += key.width
 
273
                    continue
 
274
 
 
275
                key_widget = KeyboardButton()
 
276
 
 
277
                keys[i].append(key_widget)
 
278
 
 
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)
 
286
 
 
287
                layer_widget.add(key_widget, forward_signals=False)
 
288
 
 
289
                key_widget.width = key_width * key.width
 
290
                key_widget.height = row_height - (key_widget.style.padding * 2)
 
291
                key_widget.x = key_x
 
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
 
296
 
 
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
 
307
 
 
308
                if key.image_resource:
 
309
                    image_file = theme.get_resource(key.image_resource)
 
310
                    if image_file:
 
311
                        label_width = key_widget.style.label_width
 
312
                        image_width = key_widget.style.image_width
 
313
 
 
314
                        image = Image()
 
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)
 
321
                        image.visible = True
 
322
 
 
323
                        if key_widget.label == '':
 
324
                            image.x = (1.0 - image_width) / 2.0
 
325
                        else:
 
326
                            image.x = (1.0 - label_width - image_width) / 2.0
 
327
 
 
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
 
332
 
 
333
                key_x += key_widget.width
 
334
                key_widget.width = (key_width * key.width) - (key_widget.style.padding * 2)
 
335
 
 
336
                key_widget.visible = True
 
337
 
 
338
        layer_widget.opacity = 0
 
339
        layer_widget.visible = True
 
340
 
 
341
        widget_animated = AnimatedObject(layer_widget)
 
342
        widget_animated.update_animation_settings(duration=400)
 
343
        widget_animated.opacity = 255
 
344
 
 
345
        self._layer_widgets[id] = layer_widget
 
346
 
 
347
    def _get_key_buttons(self):
 
348
        for row in self._keys:
 
349
            for key in row:
 
350
                yield key
 
351
 
 
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
 
359
                    return
 
360
 
 
361
    def _swap_layers(self, key, x, y, z, type, time, data, layers):
 
362
        try:
 
363
            current = layers.index(self.get_layer())
 
364
        except ValueError:
 
365
            print "Cannot swap layers: current layer is unknown"
 
366
            raise UnknownLayerError()
 
367
 
 
368
        self.set_layer(layers[(current + 1) % len(layers)])
 
369
 
 
370
    def _emit_text(self, key, x, y, z, type, time, data, value):
 
371
        self.emit("key-press", value)
 
372
 
 
373
    @property
 
374
    def layers(self):
 
375
        """Get the list of swappable layers' ids defined for the keyboard.
 
376
 
 
377
        @rtype: list of strings
 
378
        """
 
379
        return self._layers.keys()
 
380
 
 
381
    def get_language(self):
 
382
        """Get the language of the keyabord.
 
383
 
 
384
        @returns: the ISO 639-1 code of the language
 
385
        @rtype: string
 
386
        """
 
387
 
 
388
    def get_layer(self):
 
389
        """Get the current active layer.
 
390
 
 
391
        @returns: the layer id
 
392
        @rtype: string
 
393
        """
 
394
        return self._selected_layer
 
395
 
 
396
    def set_layer(self, id):
 
397
        """Set the active layer.
 
398
 
 
399
        @param id: the id of the layer to set
 
400
        @type id: string
 
401
        """
 
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)
 
406
 
 
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)
 
412
                    self._set_cursor()
 
413
 
 
414
                widget_animated.update_animation_settings(duration=400,
 
415
                                                          end_callback=end_animation)
 
416
                widget_animated.opacity = 0
 
417
 
 
418
    def move_cursor(self, direction):
 
419
        """Move the cursor in the specified direction.
 
420
 
 
421
        @param direction: where to move the cursor
 
422
        @type direction: one of L[elisa.plugins.pigment.widgets.const.[LEFT|RIGHT|TOP|BOTTOM]}
 
423
        """
 
424
        if not self.selected_key:
 
425
            row, column = (0, 0)
 
426
        else:
 
427
            row = self.selected_key[0]
 
428
            column = self.selected_key[1]
 
429
            current = self._keys[row][column]
 
430
 
 
431
            if direction == LEFT:
 
432
                if column == 0:
 
433
                    column = len(self._keys[row]) - 1
 
434
                else:
 
435
                    column -= 1
 
436
            elif direction == RIGHT:
 
437
                if column >= len(self._keys[row]) - 1:
 
438
                    column = 0
 
439
                else:
 
440
                    column += 1
 
441
            elif direction == TOP:
 
442
                if row == 0:
 
443
                    row = len(self._keys) - 1
 
444
                else:
 
445
                    while len(self._keys[row - 1]) == 0:
 
446
                        row -= 1
 
447
                    row -= 1
 
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:
 
452
                    row = 0
 
453
                else:
 
454
                    while len(self._keys[row + 1]) == 0:
 
455
                        row += 1
 
456
                    row += 1
 
457
                deltas = map(lambda w: abs(current.x - w.x), self._keys[row])
 
458
                column = deltas.index(min(deltas))
 
459
 
 
460
        self._set_selected_key(self._keys[row][column])
 
461
        self._keys[row][column].focus = True
 
462
 
 
463
    def do_key_press_event(self, viewport, event, widget):
 
464
        """Default handler for the key-press-event"""
 
465
 
 
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)
 
479
        else:
 
480
            found = None
 
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)
 
487
                    key.focus = True
 
488
                    break
 
489
 
 
490
    def do_key_release_event(self, viewport, event, widget):
 
491
        """Default handler for the key-release-event"""
 
492
        self.emit("key-release")
 
493
 
 
494
    @classmethod
 
495
    def _demo_widget(cls, *args, **kwargs):
 
496
        if sys.argv > 1:
 
497
            widget = cls(*sys.argv[1:])
 
498
        else:
 
499
            widget = cls()
 
500
 
 
501
        widget.visible = True
 
502
 
 
503
        def on_key_press(keyboard, text):
 
504
            print "OnScreenKeyboard key-press:", text
 
505
 
 
506
        widget.connect("key-press", on_key_press)
 
507
 
 
508
        return widget
 
509
 
 
510
    @classmethod
 
511
    def _set_demo_widget_defaults(cls, widget, canvas, viewport):
 
512
        Widget._set_demo_widget_defaults(widget, canvas, viewport)
 
513
        widget.width = 4.0
 
514
        widget.height = 3.0
 
515
        widget.regenerate()
 
516
 
 
517
 
 
518
if __name__ == '__main__':
 
519
    logger = logging.getLogger()
 
520
    logger.setLevel(logging.WARNING)
 
521
 
 
522
    keyboard = OnScreenKeyboard.demo()
 
523
    try:
 
524
        __IPYTHON__
 
525
    except NameError:
 
526
        pgm.main()
 
527