def init_event_handling(self, use_gtk, use_raw_events): if use_gtk: # GTK event handling self._device_manager = None event_mask = Gdk.EventMask.BUTTON_PRESS_MASK | \ Gdk.EventMask.BUTTON_RELEASE_MASK | \ Gdk.EventMask.POINTER_MOTION_MASK | \ Gdk.EventMask.LEAVE_NOTIFY_MASK | \ Gdk.EventMask.ENTER_NOTIFY_MASK if self._touch_events_enabled: event_mask |= Gdk.EventMask.TOUCH_MASK self.add_events(event_mask) self.connect("button-press-event", self._on_button_press_event) self.connect("button_release_event", self._on_button_release_event) self.connect("motion-notify-event", self._on_motion_event) self.connect("touch-event", self._on_touch_event) else: # XInput event handling self._device_manager = XIDeviceManager() self._device_manager.connect("device-event", self._device_event_handler) devices = self._device_manager.get_slave_pointer_devices() _logger.warning("listening to XInput devices: {}" \ .format([(d.name, d.id, d.get_config_string()) \ for d in devices])) # Select events af all slave pointer devices if use_raw_events: event_mask = XIEventMask.RawButtonPressMask | \ XIEventMask.RawButtonReleaseMask | \ XIEventMask.RawMotionMask if self._touch_events_enabled: event_mask |= XIEventMask.RawTouchMask else: event_mask = XIEventMask.ButtonPressMask | \ XIEventMask.ButtonReleaseMask | \ XIEventMask.MotionMask if self._touch_events_enabled: event_mask |= XIEventMask.TouchMask for device in devices: device.select_events(event_mask) self._selected_devices = devices self._selected_device_ids = [d.id for d in devices] self._use_raw_events = use_raw_events
class TouchInput: """ Unified handling of multi-touch sequences and conventional pointer input. """ GESTURE_DETECTION_SPAN = 100 # [ms] until two finger tap&drag is detected GESTURE_DELAY_PAUSE = 3000 # [ms] Suspend delayed sequence begin for this # amount of time after the last key press. delay_sequence_begin = True # No delivery, i.e. no key-presses after # gesture detection, but delays press-down. def __init__(self): self._input_sequences = {} self._touch_events_enabled = self.is_touch_enabled() self._multi_touch_enabled = config.keyboard.touch_input == \ TouchInputEnum.MULTI self._gestures_enabled = self._touch_events_enabled self._last_event_was_touch = False self._last_sequence_time = 0 self._gesture = NO_GESTURE self._gesture_begin_point = (0, 0) self._gesture_begin_time = 0 self._gesture_detected = False self._gesture_cancelled = False self._num_tap_sequences = 0 self._gesture_timer = Timer() self._order_timer = Timer() self._queued_events = [] self.init_event_handling( config.keyboard.event_handling == EventHandlingEnum.GTK, False) self._pytime_start = None self._evtime_start = None def cleanup(self): if self._device_manager: self._device_manager.disconnect("device-event", self._device_event_handler) self._device_manager = None def init_event_handling(self, use_gtk, use_raw_events): if use_gtk: # GTK event handling self._device_manager = None event_mask = Gdk.EventMask.BUTTON_PRESS_MASK | \ Gdk.EventMask.BUTTON_RELEASE_MASK | \ Gdk.EventMask.POINTER_MOTION_MASK | \ Gdk.EventMask.LEAVE_NOTIFY_MASK | \ Gdk.EventMask.ENTER_NOTIFY_MASK if self._touch_events_enabled: event_mask |= Gdk.EventMask.TOUCH_MASK self.add_events(event_mask) self.connect("button-press-event", self._on_button_press_event) self.connect("button_release_event", self._on_button_release_event) self.connect("motion-notify-event", self._on_motion_event) self.connect("touch-event", self._on_touch_event) else: # XInput event handling self._device_manager = XIDeviceManager() self._device_manager.connect("device-event", self._device_event_handler) devices = self._device_manager.get_slave_pointer_devices() _logger.warning("listening to XInput devices: {}" \ .format([(d.name, d.id, d.get_config_string()) \ for d in devices])) # Select events af all slave pointer devices if use_raw_events: event_mask = XIEventMask.RawButtonPressMask | \ XIEventMask.RawButtonReleaseMask | \ XIEventMask.RawMotionMask if self._touch_events_enabled: event_mask |= XIEventMask.RawTouchMask else: event_mask = XIEventMask.ButtonPressMask | \ XIEventMask.ButtonReleaseMask | \ XIEventMask.MotionMask if self._touch_events_enabled: event_mask |= XIEventMask.TouchMask for device in devices: device.select_events(event_mask) self._selected_devices = devices self._selected_device_ids = [d.id for d in devices] self._use_raw_events = use_raw_events def _device_event_handler(self, event): """ Handler for XI2 events. """ if not event.device_id in self._selected_device_ids: return #print("device {}, xi_type {}, type {}, point {} {}, xid {}" \ # .format(event.device_id, event.xi_type, event.type, event.x, event.y, event.xid_event)) win = self.get_window() if not win: return # Reject initial initial presses/touch_begins outside our window. # Allow all subsequent ones to simulate grabbing the device. if not self._input_sequences: # Is the hit window ours? # Note: only initial clicks and taps supply a valid window id. xid_event = event.xid_event if xid_event != 0 and \ xid_event != win.get_xid(): return # Convert from root to window relative coordinates. # We don't get window coordinates for more than the first touch. rx, ry = win.get_root_coords(0, 0) event.x = event.x_root - rx event.y = event.y_root - ry event_type = event.xi_type if self._use_raw_events: if event_type == XIEventType.RawMotion: self._on_motion_event(self, event) elif event_type == XIEventType.RawButtonPress: self._on_button_press_event(self, event) elif event_type == XIEventType.RawButtonRelease: self._on_button_release_event(self, event) elif event_type == XIEventType.RawTouchBegin or \ event_type == XIEventType.RawTouchUpdate or \ event_type == XIEventType.RawTouchEnd: self._on_touch_event(self, event) else: if event_type == XIEventType.Motion: self._on_motion_event(self, event) elif event_type == XIEventType.ButtonPress: self._on_button_press_event(self, event) elif event_type == XIEventType.ButtonRelease: self._on_button_release_event(self, event) elif event_type == XIEventType.TouchBegin or \ event_type == XIEventType.TouchUpdate or \ event_type == XIEventType.TouchEnd: self._on_touch_event(self, event) def is_touch_enabled(self): return config.keyboard.touch_input != TouchInputEnum.NONE def has_input_sequences(self): """ Are any clicks/touches still ongoing? """ return bool(self._input_sequences) def last_event_was_touch(self): """ Was there just a touch event? """ return self._last_event_was_touch def has_touch_source(self, event): source_device = event.get_source_device() source = source_device.get_source() return source == Gdk.InputSource.TOUCHSCREEN def _on_button_press_event(self, widget, event): if self._touch_events_enabled and \ self.has_touch_source(event): return sequence = InputSequence() sequence.init_from_button_event(event) sequence.primary = True self._last_event_was_touch = False self._input_sequence_begin(sequence) def _on_motion_event(self, widget, event): if self._touch_events_enabled and \ self.has_touch_source(event): return sequence = self._input_sequences.get(POINTER_SEQUENCE) if sequence is None: sequence = InputSequence() sequence.primary = True sequence.init_from_motion_event(event) self._last_event_was_touch = False self._input_sequence_update(sequence) def _on_button_release_event(self, widget, event): sequence = self._input_sequences.get(POINTER_SEQUENCE) if not sequence is None: sequence.point = (event.x, event.y) sequence.root_point = (event.x_root, event.y_root) sequence.time = event.get_time() self._input_sequence_end(sequence) def _on_touch_event(self, widget, event): if not self.has_touch_source(event): return touch = event.touch id = str(touch.sequence) self._last_event_was_touch = True event_type = event.type if event_type == Gdk.EventType.TOUCH_BEGIN: if self._pytime_start == None: self._pytime_start = time.time() self._evtime_start = event.get_time() #print("DOWN",time.time()-self._pytime_start,event.get_time()-self._evtime_start) evlog.append(event) sequence = InputSequence() sequence.init_from_touch_event(touch, id) if len(self._input_sequences) == 0: sequence.primary = True for ev, qseq in self._queued_events: if qseq.time < event.get_time(): #print("Yielded to queued") self._input_sequence_end(qseq) self._queued_events.remove((ev, qseq)) self._input_sequence_begin(sequence) elif event_type == Gdk.EventType.TOUCH_UPDATE: sequence = self._input_sequences.get(id) if not sequence is None: sequence.point = (touch.x, touch.y) sequence.root_point = (touch.x_root, touch.y_root) sequence.time = event.get_time() sequence.updated = time.time() self._input_sequence_update(sequence) else: if event_type == Gdk.EventType.TOUCH_END: pass elif event_type == Gdk.EventType.TOUCH_CANCEL: pass #print("UP",time.time()-self._pytime_start,event.get_time()-self._evtime_start) evlog.append(event) sequence = self._input_sequences.get(id) if not sequence is None: sequence.time = event.get_time() self._queued_events.append((Gdk.EventType.TOUCH_END,sequence)) self._order_timer.start(0.05,self._delayed_release) def _delayed_release(self): for ev, seq in self._queued_events: if ev == Gdk.EventType.TOUCH_END: #print("D:UP",time.time()-self._pytime_start,seq.time-self._evtime_start) self._input_sequence_end(seq) #elif ev == Gdk.EventType.TOUCH_BEGIN: self._queued_events.clear() return False def _input_sequence_begin(self, sequence): """ Button press/touch begin """ self._gesture_sequence_begin(sequence) first_sequence = len(self._input_sequences) == 0 if first_sequence or \ self._multi_touch_enabled: self._input_sequences[sequence.id] = sequence if not self._gesture_detected: if first_sequence and \ self._multi_touch_enabled and \ self.delay_sequence_begin and \ sequence.time - self._last_sequence_time > \ self.GESTURE_DELAY_PAUSE: # Delay the first tap; we may have to stop it # from reaching the keyboard. self._gesture_timer.start(self.GESTURE_DETECTION_SPAN / 1000.0, self.on_delayed_sequence_begin, sequence, sequence.point) else: # Tell the keyboard right away. self.deliver_input_sequence_begin(sequence) self._last_sequence_time = sequence.time def on_delayed_sequence_begin(self, sequence, point): if not self._gesture_detected: # work around race condition sequence.point = point # return to the original begin point self.deliver_input_sequence_begin(sequence) self._gesture_cancelled = True return False def deliver_input_sequence_begin(self, sequence): self.on_input_sequence_begin(sequence) sequence.delivered = True def _input_sequence_update(self, sequence): """ Pointer motion/touch update """ self._gesture_sequence_update(sequence) if not sequence.state & BUTTON123_MASK or \ not self.in_gesture_detection_delay(sequence): self._gesture_timer.finish() # don't run begin out of order self.on_input_sequence_update(sequence) def _input_sequence_end(self, sequence): """ Button release/touch end """ self._gesture_sequence_end(sequence) self._gesture_timer.finish() # run delayed sequence before end if sequence.id in self._input_sequences: del self._input_sequences[sequence.id] if sequence.delivered: self._gesture_timer.finish() # run delayed sequence before end self.on_input_sequence_end(sequence) if self._input_sequences: self._discard_stuck_input_sequences() self._last_sequence_time = sequence.time def _discard_stuck_input_sequences(self): """ Input sequence handling requires guaranteed balancing of begin, update and end events. There is no indication yet this isn't always the case, but still, at this time it seems like a good idea to prepare for the worst. -> Clear out aged input sequences, so Onboard can start from a fresh slate and not become terminally unresponsive. """ expired_time = time.time() - 30 for id, sequence in list(self._input_sequences.items()): if sequence.updated < expired_time: _logger.warning("discarding expired input sequence " + str(id)) del self._input_sequences[id] def in_gesture_detection_delay(self, sequence): span = sequence.time - self._gesture_begin_time return span < self.GESTURE_DETECTION_SPAN #FIXME later #tap Gestures should not swallow sequences #Drag gestures should send cancel events def _gesture_sequence_begin(self, sequence): return True if self._num_tap_sequences == 0: self._gesture = NO_GESTURE self._gesture_detected = False self._gesture_cancelled = False self._gesture_begin_point = sequence.point self._gesture_begin_time = sequence.time # event time else: if self.in_gesture_detection_delay(sequence) and \ not self._gesture_cancelled: self._gesture_timer.stop() # cancel delayed sequence begin self._gesture_detected = True self._num_tap_sequences += 1 def _gesture_sequence_update(self, sequence): return True if self._gesture_detected and \ sequence.state & BUTTON123_MASK and \ self._gesture == NO_GESTURE: point = sequence.point dx = self._gesture_begin_point[0] - point[0] dy = self._gesture_begin_point[1] - point[1] d2 = dx * dx + dy * dy # drag gesture? if d2 >= DRAG_GESTURE_THRESHOLD2: num_touches = len(self._input_sequences) self._gesture = DRAG_GESTURE self.on_drag_gesture_begin(num_touches) return True def _gesture_sequence_end(self, sequence): return True if len(self._input_sequences) == 1: # last sequence of the gesture? if self._gesture_detected: gesture = self._gesture if gesture == NO_GESTURE: # tap gesture? elapsed = sequence.time - self._gesture_begin_time if elapsed <= 300: self.on_tap_gesture(self._num_tap_sequences) elif gesture == DRAG_GESTURE: self.on_drag_gesture_end(0) self._num_tap_sequences = 0 def on_tap_gesture(self, num_touches): return False def on_drag_gesture_begin(self, num_touches): return False def on_drag_gesture_end(self, num_touches): return False