| # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| ''' A module for parsing X events and manipulating X Button labels ''' |
| |
| import logging |
| import math |
| import os |
| import re |
| import time |
| import utils |
| |
| import trackpad_util |
| |
| |
| class XButton: |
| ''' Manipulation of X Button labels and values ''' |
| |
| # Define some common X button values |
| Left = 1 |
| Middle = 2 |
| Right = 3 |
| Wheel_Up = 4 |
| Wheel_Down = 5 |
| Wheel_Left = 6 |
| Wheel_Right = 7 |
| Mouse_Wheel_Up = 8 |
| Mouse_Wheel_Down = 9 |
| |
| def __init__(self): |
| self.display_environ = trackpad_util.Display().get_environ() |
| self.xinput_list_cmd = ' '.join([self.display_environ, 'xinput --list']) |
| self.xinput_dev_cmd = ' '.join([self.display_environ, |
| 'xinput --list --long %s']) |
| self.trackpad_dev_id = self._get_trackpad_dev_id() |
| self.button_labels = None |
| self.get_supported_buttons() |
| self.wheel_label_dict = {'up': self.get_label(XButton.Wheel_Up), |
| 'down': self.get_label(XButton.Wheel_Down), |
| 'left': self.get_label(XButton.Wheel_Left), |
| 'right': self.get_label(XButton.Wheel_Right),} |
| |
| def _get_trackpad_dev_id(self): |
| trackpad_dev_id = None |
| if os.system('which xinput') == 0: |
| input_dev_str = utils.system_output(self.xinput_list_cmd) |
| for dev_str in input_dev_str.splitlines(): |
| res = re.search(r'(t(ouch|rack)pad\s+id=)(\d+)', dev_str, re.I) |
| if res is not None: |
| trackpad_dev_id = res.group(3) |
| break |
| return trackpad_dev_id |
| |
| def get_supported_buttons(self): |
| ''' Get supported button labels from xinput |
| |
| a device returned from 'xinput --list' looks like: |
| | SynPS/2 Synaptics TouchPad id=11 [slave pointer (2)] |
| |
| Button labels returned from 'xinput --list <device_id>' looks like: |
| Button labels: Button Left Button Middle Button Right Button Wheel Up |
| Button Wheel Down Button Horiz Wheel Left Button Horiz Wheel Right |
| Button 0 Button 1 Button 2 Button 3 Button 4 Button 5 Button 6 |
| Button 7 |
| ''' |
| if self.button_labels is not None: |
| return self.button_labels |
| |
| DEFAULT_BUTTON_LABELS = ( |
| 'Button Left', 'Button Middle', 'Button Right', |
| 'Button Wheel Up', 'Button Wheel Down', |
| 'Button Horiz Wheel Left', 'Button Horiz Wheel Right', |
| 'Button 0', 'Button 1', 'Button 2', 'Button 3', |
| 'Button 4', 'Button 5', 'Button 6', 'Button 7') |
| |
| if self.trackpad_dev_id is not None: |
| features = utils.system_output(self.xinput_dev_cmd % |
| self.trackpad_dev_id) |
| button_labels_str = [line for line in features.splitlines() |
| if line.lstrip().startswith('Button labels:')] |
| strip_str = button_labels_str[0].lstrip().lstrip('Button labels:') |
| self.button_labels = tuple(['Button ' + b.strip() for b in |
| strip_str.split('Button') |
| if len(b) > 0]) |
| else: |
| logging.warn('Cannot find trackpad device in xinput. ' |
| 'Using default Button Labels instead.') |
| self.button_labels = DEFAULT_BUTTON_LABELS |
| logging.info('Button Labels (%d) in the trackpad: %s' % |
| (len(self.button_labels), self.button_labels)) |
| |
| return self.button_labels |
| |
| def get_value(self, button_label): |
| ''' Mapping an X button label to an X button value |
| |
| For example, 'Button Left' returns 1 |
| 'Button Wheel Up' returns 4 |
| 'Button Wheel Down' returns 5 |
| ''' |
| return self.button_labels.index(button_label) + 1 |
| |
| def get_label(self, button_value): |
| ''' Mapping an X button value to an X button label |
| |
| For example, 1 returns 'Button Left' |
| 4 returns 'Button Wheel Up' |
| 5 returns 'Button Wheel Down' |
| ''' |
| return self.button_labels[button_value - 1] |
| |
| def get_index(self, button_label): |
| ''' Mapping an X button label to its index in a button tuple |
| |
| Generally, a button index is equal to its button value decreased by 1. |
| For example, 'Button Left' returns 0 |
| 'Button Wheel Up' returns 3 |
| 'Button Wheel Down' returns 4 |
| ''' |
| return self.button_labels.index(button_label) |
| |
| def init_button_struct(self, value): |
| ''' Initialize a button dictionary to the given values. ''' |
| return dict(map(lambda b: (self.get_value(b), value), |
| self.button_labels)) |
| |
| def init_button_struct_with_time(self, value, time): |
| ''' Initialize a button dictionary with time to the given values. ''' |
| return dict(map(lambda b: (self.get_value(b), [value, list(time)]), |
| self.button_labels)) |
| |
| def is_button_wheel(self, button_label): |
| ''' Is this button a wheel button? ''' |
| return button_label in ['Button Wheel Up', |
| 'Button Wheel Down', |
| 'Button Horiz Wheel Left', |
| 'Button Horiz Wheel Right'] |
| |
| |
| class XEvent: |
| ''' A class for X event parsing ''' |
| |
| def __init__(self, xbutton): |
| self.xbutton = xbutton |
| # Declare the format to extract information from X event structures |
| self.raw_format_dict = { |
| 'Motion_coord' : '{6}', |
| 'Motion_time' : '{5}', |
| 'Motion_tv' : '{7}', |
| 'Button_coord' : '{6}', |
| 'Button_button' : '{3}', |
| 'Button_time' : '{5}', |
| 'Button_tv' : '{7}', |
| } |
| |
| def _extract_prop(self, event_name, line, prop_key): |
| ''' Extract property from X events ''' |
| if line is None: |
| logging.warn(' X event format may not be correct.') |
| return None |
| |
| event_format_str = self.raw_format_dict[event_name] |
| try: |
| prop_val = event_format_str.format(*line.strip().split()).strip(',') |
| except IndexError, err: |
| logging.warn(' %s in X event data.' % str(err)) |
| return None |
| return (prop_key, prop_val) |
| |
| def _calc_distance(self, x0, y0, x1, y1): |
| ''' A simple Manhattan distance ''' |
| delta_x = abs(x1 - x0) |
| delta_y = abs(y1 - y0) |
| dist = round(math.sqrt(delta_x * delta_x + delta_y * delta_y)) |
| return [dist, [delta_x, delta_y]] |
| |
| def parse_raw_string(self, xevent_str): |
| ''' Parse X raw event string |
| |
| The event information of a single X event may span across multiple |
| lines. This function extracts the important event information of |
| an event into a dictionary so that it is easier to process in |
| subsequent stages. |
| |
| For example: |
| A MotionNotify event looks like: |
| MotionNotify event, serial 25, synthetic NO, window 0xa00001, |
| root 0xab, subw 0x0, time 925196, (750,395), root:(750,395), |
| state 0x0, is_hint 0, same_screen YES |
| |
| A ButtonPress event looks like: |
| ButtonPress event, serial 25, synthetic NO, window 0xa00001, |
| root 0xab, subw 0x0, time 1098904, (770,422), root:(770,422), |
| state 0x0, button 1, same_screen YES |
| |
| The property extracted for the MotionNotify event looks like: |
| ['MotionNotify', {'coord': (150,200), 'time': ...] |
| |
| The property extracted for the ButtonPress event looks like: |
| ['ButtonPress', {'coord': (150,200), 'button': 5}, 'time': ...] |
| ''' |
| |
| if len(xevent_str) == 0: |
| logging.warn(' No X events were captured.') |
| return False |
| |
| xevent_iter = iter(xevent_str) |
| self.xevent_data = [] |
| while True: |
| line = next(xevent_iter, None) |
| if line is None: |
| break |
| line_words = line.split() |
| if len(line_words) == 0: |
| continue |
| event_name = line_words[0] |
| |
| # Extract event information for important event types |
| if event_name == 'MotionNotify' or event_name == 'EnterNotify': |
| line1 = next(xevent_iter, None) |
| line2 = next(xevent_iter, None) |
| prop_coord = self._extract_prop('Motion_coord', line1, 'coord') |
| prop_time = self._extract_prop('Motion_time', line1, 'time') |
| if prop_coord is not None and prop_time is not None: |
| event_dict = dict([prop_coord, prop_time]) |
| self.xevent_data.append([event_name, event_dict]) |
| elif line.startswith('Button'): |
| line1 = next(xevent_iter, None) |
| line2 = next(xevent_iter, None) |
| prop_coord = self._extract_prop('Button_coord', line1, 'coord') |
| prop_time = self._extract_prop('Button_time', line1, 'time') |
| prop_button = self._extract_prop('Button_button', line2, |
| 'button') |
| if (prop_coord is not None and prop_button is not None |
| and prop_time is not None): |
| event_dict = dict([prop_coord, prop_button, prop_time]) |
| self.xevent_data.append([event_name, event_dict]) |
| return True |
| |
| def parse_button_and_motion(self): |
| ''' Parse X button events and motion events |
| |
| This method parses original X button events and motion events from |
| self.xevent_data, and saves the aggregated results in self.xevent_seq |
| |
| The variable seg_move accumulates the motions of the contiguous events |
| segmented by some boundary events such as Button events and other |
| NOP events. |
| |
| A NOP (no operation) event is a fake X event which is used to indicate |
| the occurrence of some important trackpad device events. It can be |
| use to compute the latency between a device event and a resultant X |
| event. It can also be used to partition X events into groups. |
| ''' |
| |
| # Define some functions for seg_move |
| def _reset_seg_move(): |
| ''' Reset seg_move in x+y, x, and y to 0 ''' |
| return [0, [0, 0]] |
| |
| def _reset_time_interval(begin_time=None): |
| ''' Reset time interval ''' |
| return [begin_time, None] |
| |
| def _add_seg_move(seg_move, move): |
| ''' Accumulate seg_move in x+y, x, and y respectively ''' |
| list_add = lambda list1, list2: map(sum, zip(list1, list2)) |
| return [seg_move[0] + move[0], list_add(seg_move[1], move[1])] |
| |
| def _append_event(event): |
| ''' Append the event into xevent_seq ''' |
| self.xevent_seq.append(event) |
| indent = ' ' * 14 |
| logging.info(indent + str(event)) |
| |
| def _append_motion(pre_event_name, seg_move, seg_move_time): |
| ''' Append Motion events ''' |
| (move_xy, (move_x, move_y)) = seg_move |
| if move_x > 0 or move_y > 0: |
| event = ('Motion', (move_xy, ('Motion_x', move_x), |
| ('Motion_y', move_y)), |
| seg_move_time) |
| _append_event(event) |
| |
| def _append_button(event_name, button_label, event_time): |
| ''' Append non-wheel Buttons |
| |
| Typically, they include Button Left, Button Middle, Button Right, |
| and other non-wheel buttons etc. |
| |
| TODO(josephsih): creating a event class, with more formalized, |
| named members (name, details, time). Or, using a dict, ('name': , |
| 'details':, 'time':). |
| ''' |
| if not self.xbutton.is_button_wheel(button_label): |
| event = (event_name, button_label, event_time) |
| _append_event(event) |
| |
| def _append_button_wheel(button_label, event_button, button_time): |
| ''' Append Button Wheel count ''' |
| if self.xbutton.is_button_wheel(button_label): |
| count = self.seg_count_buttons[event_button] |
| count = int(count) if count == int(count) else count |
| if count > 0: |
| event = (button_label, count, button_time) |
| _append_event(event) |
| |
| def _append_NOP(event_name, event_description, event_time): |
| ''' Append NOP event ''' |
| if event_name == 'NOP': |
| event = (event_name, event_description, event_time) |
| _append_event(event) |
| |
| self.count_buttons = self.xbutton.init_button_struct(0) |
| self.seg_count_buttons = self.xbutton.init_button_struct(0) |
| self.count_buttons_press = self.xbutton.init_button_struct(0) |
| self.count_buttons_release = self.xbutton.init_button_struct(0) |
| self.button_states = self.xbutton.init_button_struct('ButtonRelease') |
| |
| pre_xy = [None, None] |
| button_label = pre_button_label = None |
| event_name = pre_event_name = None |
| event_button = pre_event_button = None |
| self.xevent_seq = [] |
| seg_move = _reset_seg_move() |
| seg_move_time = _reset_time_interval() |
| button_time = _reset_time_interval() |
| self.sum_move = 0 |
| |
| indent = ' ' * 8 |
| precede_state = {'ButtonPress': 'ButtonRelease', |
| 'ButtonRelease': 'ButtonPress',} |
| logging.info(indent + 'X events detected:') |
| |
| for line in self.xevent_data: |
| event_name = line[0] |
| if event_name != 'NOP': |
| event_dict = line[1] |
| if event_dict.has_key('coord'): |
| event_coord = list(eval(event_dict['coord'])) |
| if event_dict.has_key('button'): |
| event_button = eval(event_dict['button']) |
| if event_dict.has_key('time'): |
| event_time = eval(event_dict['time']) |
| |
| if event_name == 'EnterNotify': |
| if pre_xy == [None, None]: |
| pre_xy = event_coord |
| seg_move_time[0] = event_time |
| self.seg_count_buttons = self.xbutton.init_button_struct(0) |
| if seg_move_time[0] is None: |
| seg_move_time = [event_time, event_time] |
| else: |
| seg_move_time[1] = event_time |
| |
| elif event_name == 'MotionNotify': |
| if pre_xy == [None, None]: |
| pre_xy = event_coord |
| else: |
| cur_xy = event_coord |
| move = self._calc_distance(*(pre_xy + cur_xy)) |
| pre_xy = cur_xy |
| seg_move = _add_seg_move(seg_move, move) |
| self.sum_move += move[0] |
| if seg_move_time[0] is None: |
| seg_move_time = [event_time, event_time] |
| else: |
| seg_move_time[1] = event_time |
| |
| elif event_name.startswith('Button'): |
| _append_motion(pre_event_name, seg_move, seg_move_time) |
| seg_move = _reset_seg_move() |
| seg_move_time = _reset_time_interval() |
| button_label = self.xbutton.get_label(event_button) |
| pre_button_state = self.button_states[event_button] |
| self.button_states[event_button] = event_name |
| |
| if button_label == pre_button_label: |
| button_time[1] = event_time |
| else: |
| # Append Button Wheel count when event button is changed |
| _append_button_wheel(pre_button_label, pre_event_button, |
| button_time) |
| self.seg_count_buttons = self.xbutton.init_button_struct(0) |
| button_time = _reset_time_interval(begin_time=event_time) |
| |
| # Append button events except button wheel events |
| _append_button(event_name, button_label, event_time) |
| |
| # A ButtonRelease should precede ButtonPress |
| # A ButtonPress should precede ButtonRelease |
| precede_flag = pre_button_state == precede_state[event_name] |
| if event_name == 'ButtonPress': |
| self.count_buttons_press[event_button] += 1 |
| elif event_name == 'ButtonRelease': |
| self.count_buttons_release[event_button] += 1 |
| self.count_buttons[event_button] += 0.5 |
| self.seg_count_buttons[event_button] += 0.5 |
| pre_button_label = button_label |
| pre_event_button = event_button |
| elif event_name == 'NOP': |
| _append_button_wheel(pre_button_label, pre_event_button, |
| button_time) |
| pre_button_label = None |
| self.seg_count_buttons = self.xbutton.init_button_struct(0) |
| button_time = _reset_time_interval() |
| _append_motion(pre_event_name, seg_move, seg_move_time) |
| seg_move = _reset_seg_move() |
| seg_move_time = _reset_time_interval() |
| _append_NOP('NOP', line[1], line[2]) |
| pre_event_name = event_name |
| |
| # Append aggregated button wheel events and motion events |
| _append_button_wheel(button_label, event_button, button_time) |
| _append_motion(pre_event_name, seg_move, seg_move_time) |
| |
| # Convert dictionary to tuple |
| self.button_states = tuple(self.button_states.values()) |
| self.count_buttons= tuple(self.count_buttons.values()) |