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('<', '<', text) 156 mod_text = re.sub('>', '>', 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