blob: 0cffc9a9ec386690d132518d087f164b2c5f3600 [file] [log] [blame] [edit]
# 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 constants
import trackpad_util
from common_util import simple_system, simple_system_output
# Declare X property names
X_PROP_SCROLL_BUTTONS = 'Scroll Buttons'
X_PROP_TAP_ENABLE = 'Tap Enable'
# Declare NOP as a instance containing NOP related constants
NOP = constants.NOP()
class Xinput(object):
''' Manipulation of xinput properties
An example usage for instantiating a trackpad xinput device:
xi = Xinput('t(?:ouch|rack)pad')
'''
def __init__(self, device_re_str):
self.device_re_str = device_re_str
self.xinput_str = 'DISPLAY=:0 xinput %s'
# list command looks like
# DISPLAY=:0 xinput list
self.xinput_list_cmd = self.xinput_str % 'list'
self.dev_id = self.lookup_device_id()
# list-props command looks like
# DISPLAY=:0 xinput list-props 11
list_props_str = 'list-props %s' % self.dev_id
self.xinput_list_props_cmd = self.xinput_str % list_props_str
def device_exists(self):
''' Indicating whether the device exists or not. '''
return self.dev_id is not None
def lookup_device_id(self):
''' Look up device id with the specified device string
For example, a trackpad device looks like
SynPS/2 Synaptics TouchPad id=11 [slave pointer (2)]
'''
dev_patt_str = '%s\s*id=(\d+)\s*\[' % self.device_re_str
dev_patt = re.compile(dev_patt_str, re.I)
dev_list = simple_system_output(self.xinput_list_cmd)
if dev_list:
for line in dev_list.splitlines():
result = dev_patt.search(line)
if result is not None:
return result.group(1)
return None
def lookup_int_prop_id_and_value(self, prop_name):
''' Look up integer property id based on property name
For example, a property looks like
Scroll Buttons (271): 0
'''
prop_re_str = '\s*%s\s*\((\d+)\):\s*(\d+)' % prop_name
prop_patt = re.compile(prop_re_str, re.I)
prop_list = simple_system_output(self.xinput_list_props_cmd)
if prop_list:
for line in prop_list.splitlines():
result = prop_patt.search(line)
if result:
return (result.group(1), int(result.group(2)))
return (None, None)
def set_int_prop_value(self, prop_id, prop_val):
''' Set integer property value
For example, to enable Scroll Buttons (id=271) at device 11
DISPLAY=:0 xinput set-prop 11 271 1
'''
# set-prop command looks like
# DISPLAY=:0 xinput set-prop 11 271 1
set_int_prop_str = 'set-prop %s %s %d' % (self.dev_id, prop_id,
prop_val)
self.xinput_set_int_prop_cmd = self.xinput_str % set_int_prop_str
simple_system(self.xinput_set_int_prop_cmd)
class XIntProp(Xinput):
''' A special class to manipulate xinput Int Property. '''
def __init__(self, prop_name):
''' Look up the id and value of the X int property '''
self.name = prop_name
super(XIntProp, self).__init__(self._get_trackpad_re_str())
# Look up the property only when the device exists
if self.device_exists():
prop_result = self.lookup_int_prop_id_and_value(prop_name)
self.prop_id, self.orig_prop_val = prop_result
else:
self.prop_id = self.orig_prop_val = None
def _get_trackpad_re_str(self):
xinput_trackpad_string = trackpad_util.read_trackpad_test_conf(
'xinput_trackpad_string', '.')
return '(?:%s)' % '|'.join(xinput_trackpad_string)
def exists(self):
''' Indicating whether the property exists or not. '''
return self.prop_id is not None
def set_prop(self):
''' Enable the int property if it is not enabled yet. '''
if self.orig_prop_val == 0:
self.set_int_prop_value(self.prop_id, 1)
def reset_prop(self):
''' Disable the int property if it was originally disabled. '''
if self.orig_prop_val == 0:
self.set_int_prop_value(self.prop_id, 0)
def set_x_input_prop(prop_name):
''' Enable the specified property if it is not enabled yet. '''
prop = XIntProp(prop_name)
if prop.exists():
prop.set_prop()
logging.info(' The property %s has been set.' % prop_name)
else:
logging.info(' The property %s does not exist.' % prop_name)
return prop
def reset_x_input_prop(prop):
''' Disable the specified property if it was originally disabled. '''
if prop.exists():
prop.reset_prop()
logging.info(' The property %s has been reset.' % prop.name)
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
DEFAULT_BUTTON_LABELS = ('Button Left', 'Button Middle', 'Button Right',
'Button Back', 'Button Forward')
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()
def _get_trackpad_dev_id(self):
trackpad_dev_id = None
if os.system('which xinput') == 0:
input_dev_str = simple_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
if self.trackpad_dev_id is not None:
xinput_dev_cmd = self.xinput_dev_cmd % self.trackpad_dev_id
features = simple_system_output(xinput_dev_cmd)
if features is not None:
button_labels_list = [line.lstrip().lstrip('Button labels:')
for line in features.splitlines()
if line.lstrip().startswith('Button labels:')]
button_labels = button_labels_list[0]
self.button_labels = tuple(['Button ' + b.strip() for b in
button_labels.split('Button') if b])
if self.button_labels is None:
logging.warn('Cannot find trackpad device in xinput. '
'Using default Button Labels instead.')
self.button_labels = self.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}',
}
self.motion_trace_before = trackpad_util.read_trackpad_test_conf(
'motion_trace_before', '.')
self.motion_trace_after = trackpad_util.read_trackpad_test_conf(
'motion_trace_after', '.')
self.LONG_MOTIOIN_TRACE = trackpad_util.read_trackpad_test_conf(
'LONG_MOTION_TRACE', '.')
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 not xevent_str:
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 not line_words:
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 _reset_coord():
return [None, 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 _is_finger_off(line):
''' Is finger off the trackpad? '''
ev_name, ev_dict = line
if isinstance(ev_dict, dict) and ev_dict.has_key('event'):
return ev_dict['event'] == 'Finger Off'
def _reset_motion_trace(self):
''' Reset motion_trace and its state '''
self.motion_trace_state = None
self.motion_trace = []
self.motion_trace_len = self.LONG_MOTIOIN_TRACE
def _extract_motion_trace_state(self, line):
''' Extract motion_trace_state which determines motion trace
length and buffer strategy.
For typical physical clicks:
----------------------------
Finger On
optional Motion events Use trace: motion_trace_before
Device Mouse Click Press
optional Motion events
ButtonPress
optional Motion events
Device Mouse Click Release
optional Motion events
ButtonRelease
optional Motion events Use trace: motion_trace_after
optional Motion events Use trace: motion_trace_before
Device Mouse Click Press
optional Motion events
ButtonPress
optional Motion events
Device Mouse Click Release
optional Motion events
ButtonRelease
optional Motion events Use trace: motion_trace_after
...
Finger Off
(1) state == 'Finger On':
Set trace length: LONG_MOTIOIN_TRACE
Note: its next state could be either 'Device Mouse Click Press'
or 'ButtonPress'.
motion_trace_state = state
(2) state == 'Device Mouse Click Press':
Set trace length: motion_trace_before
Motion report: Use only the motion_trace_before portion in
motion_trace.
Reason: Users may move cursor around and, without finger
leaving trackpad, make a physical click. We do not want
to take into account the cursor movement during
finger tracking.
Set trace length: LONG_MOTIOIN_TRACE
motion_trace_state = state
(3) state == 'ButtonPress':
Motion report: Use all motion events in motion_trace
since it is in the middle of a physical click.
Note: its previoius state could be either 'Device Mouse Click
Press' or 'Finger On'
Set trace length: LONG_MOTIOIN_TRACE
motion_trace_state = state
(4) state == 'Device Mouse Click Release':
(No need to handle 'Device Mouse Click Release' explicitly)
(5) state == 'ButtonRelease':
Motion report: Use all motion events in motion_trace
since it is in the middle of a physical click.
Set trace length: motion_trace_after
Reason: After releasing the physical click, without finger
leaving trackpad, users may move cursor around. So we
only want to collect the motion events afterwards in
limited time.
Note: After motion_trace_after has been collected.
Append a motion report. And then turn to using
"Set trace length: LONG_MOTIOIN_TRACE"
for next possible clicks or
tap-to-clicks.
motion_trace_state = state
(6) state == 'Finger Off':
if self.motion_trace_state == 'ButtonRelease':
Motion report: Use motion events in the motion_trace
for up to motion_trace_after elements.
Set trace length: LONG_MOTIOIN_TRACE
motion_trace_state = state
(7) event_name == 'Motion':
if (self.motion_trace_state == 'ButtonRelease' and
len(self.motion_trace) >= self.motion_trace_len - 1):
_append_motion(self)
self.motion_trace_len = self.LONG_MOTIOIN_TRACE
self.motion_trace_state = 'ButtonRelease.TraceAfterDone'
Note: Next state could be either 'Device Mouse Click Press'
or 'Finger Off'
'''
ev_name, ev_dict = line
state = None
if ev_name in ['ButtonPress', 'ButtonRelease', 'MotionNotify']:
state = ev_name
elif (ev_name == 'NOP' and
ev_dict['event'] in ['Finger On', 'Finger Off',
'Device Mouse Click Press']):
state = ev_dict['event']
return state
def _insert_motion_trace_entry(self, event_coord, event_time):
# Insert an entry into motion_trace
mtrace_dict = {'coord': event_coord, 'time': event_time}
self.motion_trace.append(mtrace_dict)
if len(self.motion_trace) > self.motion_trace_len:
self.motion_trace.pop(0)
def _reduce_motion_trace(self, target_trace_len):
# Reduce motion_trace according to target_trace_len
trace_len = len(self.motion_trace)
if trace_len > target_trace_len:
begin_index = trace_len - target_trace_len
self.motion_trace = self.motion_trace[begin_index:]
def _add_motion_trace(self, event_coord, event_time, line):
''' Add the new coordinate into motion_trace.
If the trace lenght exceeds a predefined length, pop off the
oldest entry from the trace buffer.
Refer to _extract_motion_trace_state() for details about
the operations of motion_trace.
'''
state = _extract_motion_trace_state(self, line)
# Determine motion trace length based on motion trace state
if state == 'Finger On':
self.motion_trace_len = self.LONG_MOTIOIN_TRACE
self.motion_trace_state = state
elif state == 'Device Mouse Click Press':
_reduce_motion_trace(self, self.motion_trace_before)
_append_motion(self)
self.motion_trace_len = self.LONG_MOTIOIN_TRACE
self.motion_trace_state = state
elif state == 'ButtonPress':
_insert_motion_trace_entry(self, event_coord, event_time)
_append_motion(self)
self.motion_trace_len = self.LONG_MOTIOIN_TRACE
self.motion_trace_state = state
elif state == 'ButtonRelease':
_insert_motion_trace_entry(self, event_coord, event_time)
_append_motion(self)
self.motion_trace_len = self.motion_trace_after
self.motion_trace_state = state
elif state == 'Finger Off':
# if the state is not 'ButtonRelease.TraceAfterDone' yet
if self.motion_trace_state == 'ButtonRelease':
_append_motion(self)
self.motion_trace_len = self.LONG_MOTIOIN_TRACE
self.motion_trace_state = state
elif state == 'MotionNotify':
_insert_motion_trace_entry(self, event_coord, event_time)
if (self.motion_trace_state == 'ButtonRelease' and
len(self.motion_trace) >= self.motion_trace_len - 1):
_append_motion(self)
self.motion_trace_len = self.LONG_MOTIOIN_TRACE
self.motion_trace_state = 'ButtonRelease.TraceAfterDone'
elif line[0] == 'NOP':
_append_motion(self)
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(self):
''' Append Motion events to xevent_seq '''
if not self.motion_trace:
return
# Compute the accumulated movement and the time span
# in motion_trace buffer.
pre_xy = _reset_coord()
seg_move = _reset_seg_move()
seg_time_span = [None, None]
for m in self.motion_trace:
cur_xy = m['coord']
cur_time = m['time']
if pre_xy == [None, None]:
pre_xy = cur_xy
seg_time_span = [cur_time, cur_time]
else:
move = self._calc_distance(*(pre_xy + cur_xy))
seg_move = _add_seg_move(seg_move, move)
pre_xy = cur_xy
seg_time_span[1] = cur_time
# Append the motion report data to xevent_seq
# The format of reported motion data looks like:
# ('Motion', motion_data, time_span)
#
# E.g.,
# ('Motion', (2.0, ('Motion_x', 2), ('Motion_y', 0)),
# [351613962, 351614138])
(move_xy, (move_x, move_y)) = seg_move
if move_x > 0 or move_y > 0:
move_val = (move_xy, ('Motion_x', move_x), ('Motion_y', move_y))
event = ('Motion', move_val, seg_time_span)
_append_event(event)
# Clear the motion_trace buffer and keep the last entry
last_entry = self.motion_trace.pop()
_reset_motion_trace(self)
self.motion_trace.append(last_entry)
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.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 = _reset_coord()
button_label = pre_button_label = None
event_name = None
event_button = pre_event_button = None
self.xevent_seq = []
seg_move_time = _reset_time_interval()
button_time = _reset_time_interval()
self.sum_move = 0
_reset_motion_trace(self)
indent = ' ' * 8
logging.info(indent + 'X events detected:')
for line in self.xevent_data:
flag_finger_off = _is_finger_off(line)
event_name = line[0]
event_coord = _reset_coord()
event_time = None
if event_name != NOP.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'])
# _extract_motion_trace_state(self, line)
_add_motion_trace(self, event_coord, event_time, line)
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
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'):
seg_move_time = _reset_time_interval()
button_label = self.xbutton.get_label(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
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.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()
seg_move_time = _reset_time_interval()
_append_NOP(NOP.NOP, line[1]['event'], line[1]['time'])
if flag_finger_off:
pre_xy = _reset_coord()
_reset_motion_trace(self)
# Append aggregated button wheel events and motion events
_append_button_wheel(button_label, event_button, button_time)
_append_motion(self)
# Convert dictionary to tuple
self.button_states = tuple(self.button_states.values())
self.count_buttons = tuple(self.count_buttons.values())