1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""This module provides GUI for touch device firmware test using GTK."""
6
7import os
8import re
9import shutil
10
11import gobject
12import gtk
13import gtk.gdk
14import pango
15import tempfile
16
17import common_util
18import firmware_utils
19import test_conf as conf
20
21from firmware_constants import TFK
22
23
24TITLE = "Touch Firmware Test"
25
26
27class BaseFrame(object):
28    """A simple base frame class."""
29    def __init__(self, label=None, size=None, aspect=False):
30        # Create a regular/aspect frame
31        self.frame = gtk.AspectFrame() if aspect else gtk.Frame()
32        self.frame.set_shadow_type(gtk.SHADOW_ETCHED_OUT)
33        self.size = size
34        if label:
35            self.frame.set_label(label)
36            self.frame.set_label_align(0.0, 0.0)
37            frame_label = self.frame.get_label_widget()
38            markup_str = '<span foreground="%s" size="x-large">%s</span>'
39            frame_label.set_markup(markup_str % ('black', label))
40        if size:
41            width, height = size
42            self.frame.set_size_request(width, height)
43            if aspect:
44                self.frame.set(ratio=(float(width) / height))
45
46
47class PromptFrame(BaseFrame):
48    """A simple frame widget to display the prompt.
49
50    It consists of:
51      - A frame
52      - a label showing the gesture name
53      - a label showing the prompt
54      - a label showing the keyboard interactions
55    """
56
57    def __init__(self, label=None, size=None):
58        super(PromptFrame, self).__init__(label, size)
59
60        # Create a vertical packing box.
61        self.vbox = gtk.VBox(False, 0)
62        self.frame.add(self.vbox)
63
64        # Create a label to show the gesture name
65        self.label_gesture = gtk.Label('Gesture Name')
66        self.label_gesture.set_justify(gtk.JUSTIFY_LEFT)
67        self.vbox.pack_start(self.label_gesture, True, True, 0)
68        # Expand the lable to be wider and wrap the line if necessary.
69        if self.size:
70            _, label_height = self.label_gesture.get_size_request()
71            width, _ = self.size
72            label_width = int(width * 0.9)
73            self.label_gesture.set_size_request(label_width, label_height)
74        self.label_gesture.set_line_wrap(True)
75
76        # Pack a horizontal separator
77        self.vbox.pack_start(gtk.HSeparator(), True, True, 0)
78
79        # Create a label to show the prompt
80        self.label_prompt = gtk.Label('Prompt')
81        self.label_prompt.set_justify(gtk.JUSTIFY_CENTER)
82        self.vbox.pack_start(self.label_prompt, True, True, 0)
83
84        # Create a label to show the choice
85        self.label_choice = gtk.Label('')
86        self.label_choice.set_justify(gtk.JUSTIFY_LEFT)
87        self.vbox.pack_start(self.label_choice, True, True, 0)
88
89        # Show all widgets added to this frame
90        self.frame.show_all()
91
92    def set_gesture_name(self, string, color='blue'):
93        """Set the gesture name in label_gesture."""
94        markup_str = '<b><span foreground="%s" size="xx-large"> %s </span></b>'
95        self.label_gesture.set_markup(markup_str % (color, string))
96
97    def set_prompt(self, string, color='black'):
98        """Set the prompt in label_prompt."""
99        markup_str = '<span foreground="%s" size="x-large"> %s </span>'
100        self.label_prompt.set_markup(markup_str % (color, string))
101
102    def set_choice(self, string):
103        """Set the choice in label_choice."""
104        self.label_choice.set_text(string)
105
106
107class ResultFrame(BaseFrame):
108    """A simple frame widget to display the test result.
109
110    It consists of:
111      - A frame
112      - a scrolled window
113      - a label showing the test result
114    """
115    SCROLL_STEP = 100.0
116
117    def __init__(self, label=None, size=None):
118        super(ResultFrame, self).__init__(label, size)
119
120        # Create a scrolled window widget
121        self.scrolled_window = gtk.ScrolledWindow()
122        self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC,
123                                        gtk.POLICY_AUTOMATIC)
124        self.frame.add(self.scrolled_window)
125
126        # Create a vertical packing box.
127        self.vbox = gtk.VBox(False, 0)
128        self.scrolled_window.add_with_viewport(self.vbox)
129
130        # Create a label to show the gesture name
131        self.result = gtk.Label()
132        self.vbox.pack_start(self.result , False, False, 0)
133
134        # Show all widgets added to this frame
135        self.frame.show_all()
136
137        # Get the vertical and horizontal adjustments
138        self.vadj = self.scrolled_window.get_vadjustment()
139        self.hadj = self.scrolled_window.get_hadjustment()
140
141        self._scroll_func_dict = {TFK.UP: self._scroll_up,
142                                  TFK.DOWN: self._scroll_down,
143                                  TFK.LEFT: self._scroll_left,
144                                  TFK.RIGHT: self._scroll_right}
145
146    def _calc_result_font_size(self):
147        """Calculate the font size so that it does not overflow."""
148        label_width_in_px, _ = self.size
149        font_size = int(float(label_width_in_px) / conf.num_chars_per_row *
150                        pango.SCALE)
151        return font_size
152
153    def set_result(self, text, color='black'):
154        """Set the text in the result label."""
155        mod_text = re.sub('<', '&lt;', text)
156        mod_text = re.sub('>', '&gt;', mod_text)
157        markup_str = '<b><span foreground="%s" size="%d"> %s </span></b>'
158        font_size = self._calc_result_font_size()
159        self.result.set_markup(markup_str % (color, font_size, mod_text))
160
161    def _calc_inc_value(self, adj):
162        """Calculate new increased value of the specified adjustement object."""
163        value = adj.get_value()
164        new_value = min(value + self.SCROLL_STEP, adj.upper - adj.page_size)
165        return new_value
166
167    def _calc_dec_value(self, adj):
168        """Calculate new decreased value of the specified adjustement object."""
169        value = adj.get_value()
170        new_value = max(value - self.SCROLL_STEP, adj.lower)
171        return new_value
172
173    def _scroll_down(self):
174        """Scroll the scrolled_window down."""
175        self.vadj.set_value(self._calc_inc_value(self.vadj))
176
177    def _scroll_up(self):
178        """Scroll the scrolled_window up."""
179        self.vadj.set_value(self._calc_dec_value(self.vadj))
180
181    def _scroll_right(self):
182        """Scroll the scrolled_window to the right."""
183        self.hadj.set_value(self._calc_inc_value(self.hadj))
184
185    def _scroll_left(self):
186        """Scroll the scrolled_window to the left."""
187        self.hadj.set_value(self._calc_dec_value(self.hadj))
188
189    def scroll(self, choice):
190        """Scroll the result frame using the choice key."""
191        scroll_method = self._scroll_func_dict.get(choice)
192        if scroll_method:
193            scroll_method()
194        else:
195            print 'Warning: the key choice "%s" is not legal!' % choice
196
197
198class ImageFrame(BaseFrame):
199    """A simple frame widget to display the mtplot window.
200
201    It consists of:
202      - An aspect frame
203      - an image widget showing mtplot
204    """
205
206    def __init__(self, label=None, size=None):
207        super(ImageFrame, self).__init__(label, size, aspect=True)
208
209        # Use a fixed widget to display the image.
210        self.fixed = gtk.Fixed()
211        self.frame.add(self.fixed)
212
213        # Create an image widget.
214        self.image = gtk.Image()
215        self.fixed.put(self.image, 0, 0)
216
217        # Show all widgets added to this frame
218        self.frame.show_all()
219
220    def set_from_file(self, filename):
221        """Set the image file."""
222        self.image.set_from_file(filename)
223        self.frame.show_all()
224
225
226class FirmwareWindow(object):
227    """A simple window class to display the touch firmware test window."""
228
229    def __init__(self, size=None, prompt_size=None, result_size=None,
230                 image_size=None):
231        # Setup gtk environment correctly.
232        self._setup_gtk_environment()
233
234        # Create a new window
235        self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
236        if size:
237            self.win_size = size
238            self.win.resize(*size)
239        self.win.set_title(TITLE)
240        self.win.set_border_width(0)
241
242        # Create the prompt frame
243        self.prompt_frame = PromptFrame(TITLE, prompt_size)
244
245        # Create the result frame
246        self.result_frame = ResultFrame("Test results:", size=result_size)
247
248        # Create the image frame for mtplot
249        self.image_frame = ImageFrame(size=image_size)
250
251        # Handle layout below
252        self.box0 = gtk.VBox(False, 0)
253        self.box1 = gtk.HBox(False, 0)
254        # Arrange the layout about box0
255        self.win.add(self.box0)
256        self.box0.pack_start(self.prompt_frame.frame, True, True, 0)
257        self.box0.pack_start(self.box1, True, True, 0)
258        # Arrange the layout about box1
259        self.box1.pack_start(self.image_frame.frame, True, True, 0)
260        self.box1.pack_start(self.result_frame.frame, True, True, 0)
261
262        # Capture keyboard events.
263        self.win.add_events(gtk.gdk.KEY_PRESS_MASK | gtk.gdk.KEY_RELEASE_MASK)
264
265        # Set a handler for delete_event that immediately exits GTK.
266        self.win.connect("delete_event", self.delete_event)
267
268        # Show all widgets.
269        self.win.show_all()
270
271    def _setup_gtk_environment(self):
272        """Set up the gtk environment correctly."""
273
274        def _warning(msg=None):
275            print 'Warning: fail to setup gtk environment.'
276            if msg:
277                print '\t' + msg
278            print '\tImage files would not be shown properly.'
279            print '\tIt does not affect the test results though.'
280
281        def _make_symlink(path, symlink):
282            """Remove the symlink if exists. Create a new symlink to point to
283            the given path.
284            """
285            if os.path.islink(symlink):
286                os.remove(symlink)
287            os.symlink(real_gtk_dir, self.gtk_symlink)
288            self.new_symlink = True
289
290        self.gtk_symlink = None
291        self.tmp = tempfile.mkdtemp()
292        self.moved_flag = False
293        self.original_gtk_realpath = None
294        self.new_symlink = False
295
296        # Get LoaderDir:
297        # The output of gdk-pixbuf-query-loaders looks like:
298        #
299        #   GdkPixbuf Image Loader Modules file
300        #   Automatically generated file, do not edit
301        #   Created by gdk-pixbuf-query-loaders from gtk+-2.20.1
302        #
303        #   LoaderDir = /usr/lib64/gtk-2.0/2.10.0/loaders
304        loader_dir_str = common_util.simple_system_output(
305                'gdk-pixbuf-query-loaders | grep LoaderDir')
306        result = re.search('(/.*?)/(gtk-.*?)/', loader_dir_str)
307        if result:
308            prefix = result.group(1)
309            self.gtk_version = result.group(2)
310        else:
311            _warning('Cannot derive gtk version from LoaderDir.')
312            return
313
314        # Verify the existence of the loaders file.
315        gdk_pixbuf_loaders = ('/usr/local/etc/%s/gdk-pixbuf.loaders' %
316                              self.gtk_version)
317        if not os.path.isfile(gdk_pixbuf_loaders):
318            msg = 'The loaders file "%s" does not exist.' % gdk_pixbuf_loaders
319            _warning(msg)
320            return
321
322        # Setup the environment variable for GdkPixbuf Image Loader Modules file
323        # so that gtk library could find it.
324        os.environ['GDK_PIXBUF_MODULE_FILE'] = gdk_pixbuf_loaders
325
326        # In the loaders file, it specifies the paths of various
327        # sharable objects (.so) which are used to load images of corresponding
328        # image formats. For example, for png loader, the path looks like
329        #
330        # "/usr/lib64/gtk-2.0/2.10.0/loaders/libpixbufloader-png.so"
331        # "png" 5 "gtk20" "The PNG image format" "LGPL"
332        # "image/png" ""
333        # "png" ""
334        # "\211PNG\r\n\032\n" "" 100
335        #
336        # However, the real path for the .so file is under
337        # "/usr/local/lib64/..."
338        # Hence, we would like to make a temporary symlink so that
339        # gtk library could find the .so file correctly.
340        self.gtk_symlink = os.path.join(prefix, self.gtk_version)
341        prefix_list = prefix.split('/')
342        prefix_list.insert(prefix_list.index('usr') + 1, 'local')
343        real_gtk_dir = os.path.join('/', *(prefix_list + [self.gtk_version]))
344
345        # Make sure that the directory of .so files does exist.
346        if not os.path.isdir(real_gtk_dir):
347            msg = 'The directory of gtk image loaders "%s" does not exist.'
348            _warning(msg % real_gtk_dir)
349            return
350
351        # Take care of an existing symlink.
352        if os.path.islink(self.gtk_symlink):
353            # If the symlink does not point to the correct path,
354            # save the real path of the symlink and re-create the symlink.
355            if not os.path.samefile(self.gtk_symlink, real_gtk_dir):
356                self.original_gtk_realpath = os.path.realpath(self.gtk_symlink)
357                _make_symlink(real_gtk_dir, self.gtk_symlink)
358
359        # Take care of an existing directory.
360        elif os.path.isdir(self.gtk_symlink):
361            # Move the directory only if it is not what we expect.
362            if not os.path.samefile(self.gtk_symlink, real_gtk_dir):
363                shutil.move(self.gtk_symlink, self.tmp)
364                self.moved_flag = True
365                _make_symlink(real_gtk_dir, self.gtk_symlink)
366
367        # Take care of an existing file.
368        # Such a file is not supposed to exist here. Move it anyway.
369        elif os.path.isfile(self.gtk_symlink):
370            shutil.move(self.gtk_symlink, self.tmp)
371            self.moved_flag = True
372            _make_symlink(real_gtk_dir, self.gtk_symlink)
373
374        # Just create the temporary symlink since there is nothing here.
375        else:
376            _make_symlink(real_gtk_dir, self.gtk_symlink)
377
378    def close(self):
379        """Cleanup by restoring any symlink, file, or directory if necessary."""
380        # Remove the symlink that the test created.
381        if self.new_symlink:
382            os.remove(self.gtk_symlink)
383
384        # Restore the original symlink.
385        if self.original_gtk_realpath:
386            os.symlink(self.original_gtk_realpath, self.gtk_symlink)
387        # Restore the original file or directory.
388        elif self.moved_flag:
389            tmp_gtk_path = os.path.join(self.tmp, self.gtk_version)
390            if (os.path.isdir(tmp_gtk_path) or os.path.isfile(tmp_gtk_path)):
391                shutil.move(tmp_gtk_path, os.path.dirname(self.gtk_symlink))
392                self.moved_flag = False
393                shutil.rmtree(self.tmp)
394
395    def register_callback(self, event, callback):
396        """Register a callback function for an event."""
397        self.win.connect(event, callback)
398
399    def register_timeout_add(self, callback, timeout):
400        """Register a callback function for gobject.timeout_add."""
401        return gobject.timeout_add(timeout, callback)
402
403    def register_io_add_watch(self, callback, fd, data=None,
404                              condition=gobject.IO_IN):
405        """Register a callback function for gobject.io_add_watch."""
406        if data:
407            return gobject.io_add_watch(fd, condition, callback, data)
408        else:
409            return gobject.io_add_watch(fd, condition, callback)
410
411    def create_key_press_event(self, keyval):
412        """Create a key_press_event."""
413        event = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
414        # Assign current time to the event
415        event.time = 0
416        event.keyval = keyval
417        self.win.emit('key_press_event', event)
418
419    def remove_event_source(self, tag):
420        """Remove the registered callback."""
421        gobject.source_remove(tag)
422
423    def delete_event(self, widget, event, data=None):
424        """A handler to exit the window."""
425        self.stop()
426        return False
427
428    def set_gesture_name(self, string, color='blue'):
429        """A helper method to set gesture name."""
430        self.prompt_frame.set_gesture_name(string, color)
431
432    def set_prompt(self, string, color='black'):
433        """A helper method to set the prompt."""
434        self.prompt_frame.set_prompt(string, color)
435
436    def set_choice(self, string):
437        """A helper method to set the choice."""
438        self.prompt_frame.set_choice(string)
439
440    def set_result(self, text):
441        """A helper method to set the text in the result."""
442        self.result_frame.set_result(text)
443
444    def set_image(self, filename):
445        """Set an image in the image frame."""
446        self.image_frame.set_from_file(filename)
447
448    def scroll(self, choice):
449        """Scroll the result frame using the choice key."""
450        self.result_frame.scroll(choice)
451
452    def stop(self):
453        """Quit the window."""
454        self.close()
455        gtk.main_quit()
456
457    def main(self):
458        """Main function of the window."""
459        try:
460            gtk.main()
461        except KeyboardInterrupt:
462            self.close()
463