def _register_input_events_xinput(self, register): """ Setup XInput event handling """ success = True if register: self._device_manager = XIDeviceManager() if self._device_manager.is_valid(): self._device_manager.connect("device-event", self._on_device_event) self._device_manager.connect("device-grab", self._on_device_grab) self._device_manager.connect("devices-updated", self._on_devices_updated) self._select_xinput_devices() else: success = False self._device_manager = None else: if self._device_manager: self._device_manager.disconnect("device-event", self._on_device_event) self._device_manager.disconnect("device-grab", self._on_device_grab) self._device_manager.disconnect("devices-updated", self._on_devices_updated) self._unselect_xinput_devices() self._device_manager = None return success
def _register_xinput_events(self, register): """ Setup XInput event handling """ success = True if register: self._device_manager = XIDeviceManager() if self._device_manager.is_valid(): self._device_manager.connect("device-event", self._on_device_event) self._device_manager.connect("device-grab", self._on_device_grab) self._device_manager.connect("devices-updated", self._on_devices_updated) self.select_xinput_devices() else: success = False self._device_manager = None else: if self._device_manager: self._device_manager.disconnect("device-event", self._on_device_event) self._device_manager.disconnect("device-grab", self._on_device_grab) self._device_manager.disconnect("devices-updated", self._on_devices_updated) if self._master_device: device = self._master_device try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._master_device = None self._master_device_id = None if self._slave_devices: for device in self._slave_devices: try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._slave_devices = None self._slave_device_ids = None return success
def __init__(self, event_handler): logger.debug("ScanDevice.__init__()") """ Selected device tuple (device id, master id) """ self._active_device_ids = None """ Whether the active device is detached """ self._floating = False """ Event handler for device events """ self._event_handler = event_handler """ The manager for osk XInput devices """ self._device_manager = XIDeviceManager() # singleton self._device_manager.connect("device-event", self._device_event_handler) config.scanner.device_name_notify_add(self._device_name_notify) config.scanner.device_detach_notify_add(self._device_detach_notify) self._device_name_notify(config.scanner.device_name)
def __init__(self, keyboard): ClickSimulator.__init__(self) self._keyboard = keyboard self._device_manager = XIDeviceManager() self._grabbed_device_ids = [] self._num_clicks_detected = 0 self._motion_position = None self._button = self.PRIMARY_BUTTON self._click_type = self.CLICK_TYPE_SINGLE self._click_done_notify_callbacks = [] self._exclusion_rects = [] self._osk_cm = osk.ClickMapper()
def _register_xinput_events(self, register): """ Setup XInput event handling """ success = True if register: self._device_manager = XIDeviceManager() if self._device_manager.is_valid(): self._device_manager.connect("device-event", self._on_device_event) self._device_manager.connect("device-grab", self._on_device_grab) self._device_manager.connect("devices-updated", self._on_devices_updated) self.select_xinput_devices() else: success = False self._device_manager = None else: if self._device_manager: self._device_manager.disconnect("device-event", self._on_device_event) self._device_manager.disconnect("device-grab", self._on_device_grab) self._device_manager.disconnect("devices-updated", self._on_devices_updated) if self._master_device: device = self._master_device try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._master_device = None self._master_device_id = None if self._slave_devices: for device in self._slave_devices: try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._slave_devices = None self._slave_device_ids = None return success
class ScanDevice(object): """ Input device manager class. Manages input devices on the system and deals with PnP related event. The actual press/release events are forwarded to a ScanMode instance. """ """ Default device name (virtual core pointer) """ DEFAULT_NAME = "Default" """ Device id's of the primary masters """ DEFAULT_VCP_ID = 2 DEFAULT_VCK_ID = 3 """ Device name blacklist """ blacklist = [ "Virtual core pointer", "Virtual core keyboard", "Virtual core XTEST pointer", "Virtual core XTEST keyboard", "Power Button" ] def __init__(self, event_handler): logger.debug("ScanDevice.__init__()") """ Selected device tuple (device id, master id) """ self._active_device_ids = None """ Whether the active device is detached """ self._floating = False """ Event handler for device events """ self._event_handler = event_handler """ The manager for osk XInput devices """ self._device_manager = XIDeviceManager() # singleton self._device_manager.connect("device-event", self._device_event_handler) config.scanner.device_name_notify_add(self._device_name_notify) config.scanner.device_detach_notify_add(self._device_detach_notify) self._device_name_notify(config.scanner.device_name) def __del__(self): logger.debug("ScanDevice.__del__()") def _device_event_handler(self, event): """ Handler for XI2 events. """ event_type = event.xi_type device_id = event.device_id if event_type == XIEventType.DeviceAdded: device = self._device_manager.lookup_device_id(device_id) show_new_device_dialog(device.name, device.get_config_string(), device.is_pointer(), self._on_new_device_accepted) elif event_type == XIEventType.DeviceRemoved: # If we are currently using this device, # close it and fall back to 'Default' if self._active_device_ids and \ self._active_device_ids[0] == device_id: self._active_device_ids = None self._floating = False config.scanner.device_detach = False config.scanner.device_name = self.DEFAULT_NAME else: # Never handle VCK events. if device_id != self.DEFAULT_VCK_ID: # Forward VCP events only if 'Default' is selected. # Else only handle devices we selected. if (device_id == self.DEFAULT_VCP_ID and \ config.scanner.device_name == self.DEFAULT_NAME) or \ (self._active_device_ids and \ device_id == self._active_device_ids[0]): self._event_handler(event) def _on_new_device_accepted(self, config_string): """ Callback for the 'New device' dialog. Called only if 'Use device' was chosen. """ config.scanner.device_name = config_string config.scanner.device_detach = True def _device_detach_notify(self, detach): """ Callback for the scanner.device_detach configuration changes. """ if self._active_device_ids is None: return if detach: if not self._floating: self.detach(self._active_device_ids[0]) else: if self._floating: self.attach(*self._active_device_ids) def _device_name_notify(self, name): """ Callback for the scanner.device_name configuration changes. """ self.close() if name == self.DEFAULT_NAME: return for device in self._device_manager.get_devices(): if self.is_useable(device) and \ name == device.get_config_string(): self.open(device) break if self._active_device_ids is None: logger.debug("Unknown device-name in configuration.") config.scanner.device_detach = False config.scanner.device_name = self.DEFAULT_NAME def open(self, device): """ Select for events and optionally detach the device. """ if device.is_pointer(): event_mask = XIEventMask.ButtonPressMask | \ XIEventMask.ButtonReleaseMask else: event_mask = XIEventMask.KeyPressMask | \ XIEventMask.KeyReleaseMask try: self._device_manager.select_events(None, device, event_mask) self._active_device_ids = (device.id, device.master) except Exception as ex: logger.warning("Failed to open device {id}: {ex}".format( id=device.id, ex=ex)) if config.scanner.device_detach and not device.is_master(): self.detach(device.id) def close(self): """ Stop using the current device. """ if self._floating: self.attach(*self._active_device_ids) if self._active_device_ids: device = self._device_manager.lookup_device_id( \ self._active_device_ids[0]) try: self._device_manager.unselect_events(None, device) self._active_device_ids = None except Exception as ex: logger.warning("Failed to close device {id}: {ex}".format( id=self._active_device_ids[0], ex=ex)) def attach(self, dev_id, master): """ Attach the device to a master. """ try: self._device_manager.attach_device_id(dev_id, master) self._floating = False except: logger.warning("Failed to attach device {id} to {master}".format( id=dev_id, master=master)) def detach(self, dev_id): """ Detach the device from its master. """ try: self._device_manager.detach_device_id(dev_id) self._floating = True except: logger.warning("Failed to detach device {id}".format(id=dev_id)) def finalize(self): """ Clean up the ScanDevice instance. """ self._device_manager.disconnect("device-event", self._device_event_handler) config.scanner.device_name_notify_remove(self._device_name_notify) config.scanner.device_detach_notify_remove(self._device_detach_notify) self.close() self._event_handler = None self.devices = None @staticmethod def is_useable(device): """ Check whether this device is useable for scanning. """ return device.name not in ScanDevice.blacklist \ and device.enabled \ and not device.is_floating()
class InputEventSource(EventSource, XIDeviceEventLogger): """ Setup and handle GTK or XInput device events. """ def __init__(self): # There is only button-release to subscribe to currently, # as this is all CSButtonRemapper needs to detect the end of a click. EventSource.__init__(self, ["button-release"]) XIDeviceEventLogger.__init__(self) self._gtk_handler_ids = None self._device_manager = None self._master_device = None # receives enter/leave events self._master_device_id = None # for convenience/performance self._slave_devices = None # receive pointer and touch events self._slave_device_ids = None # for convenience/performance self._xi_grab_active = False self._xi_grab_events_selected = False self._xi_event_handled = False self._touch_active = set() # set of id(XIDevice/GdkX11DeviceXI2) # For devices not contained here only # pointer events are considered. # Wacom devices with enabled gestures never # become touch-active, i.e. they don't # generate touch events. self.connect("realize", self._on_realize_event) self.connect("unrealize", self._on_unrealize_event) def cleanup(self): self._register_gtk_events(False) self._register_xinput_events(False) def _clear_touch_active(self): self._touch_active = set() def set_device_touch_active(self, device): """ Mark source device as actively receiving touch events """ self._touch_active.add(id(device)) def is_device_touch_active(self, device): """ Mark source device as actively receiving touch events """ return id(device) in self._touch_active def _on_realize_event(self, user_data): self.handle_realize_event() def _on_unrealize_event(self, user_data): self.handle_unrealize_event() def handle_realize_event(self): # register events in derived class pass def handle_unrealize_event(self): self.register_input_events(False) def grab_xi_pointer(self, active): """ Tell the xi event source a drag operation has started (ended) and we want to receive events of the whole screen. """ self._xi_grab_active = active # release simulated grab of slave device when the drag operation ends if not active and \ self._xi_grab_events_selected and \ self._device_manager: self._select_xi_grab_events(False) def set_xi_event_handled(self, handled): """ Tell the xi event source to stop/continue processing of handlers for the current event. """ self._xi_event_handled = handled def register_input_events(self, register, use_gtk = False): self._register_gtk_events(False) self._register_xinput_events(False) self._clear_touch_active() if register: if use_gtk: self._register_gtk_events(True) else: if not self._register_xinput_events(True): _logger.warning("XInput event source failed to initialize, " "falling back to GTK.") self._register_gtk_events(True) def _register_gtk_events(self, register): """ Setup GTK event handling """ if register: 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._gtk_handler_ids = [ 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("enter-notify-event", self._on_enter_notify), self.connect("leave-notify-event", self._on_leave_notify), self.connect("touch-event", self._on_touch_event), ] else: if self._gtk_handler_ids: for id in self._gtk_handler_ids: self.disconnect(id) self._gtk_handler_ids = None def _register_xinput_events(self, register): """ Setup XInput event handling """ success = True if register: self._device_manager = XIDeviceManager() if self._device_manager.is_valid(): self._device_manager.connect("device-event", self._on_device_event) self._device_manager.connect("device-grab", self._on_device_grab) self._device_manager.connect("devices-updated", self._on_devices_updated) self.select_xinput_devices() else: success = False self._device_manager = None else: if self._device_manager: self._device_manager.disconnect("device-event", self._on_device_event) self._device_manager.disconnect("device-grab", self._on_device_grab) self._device_manager.disconnect("devices-updated", self._on_devices_updated) if self._master_device: device = self._master_device try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._master_device = None self._master_device_id = None if self._slave_devices: for device in self._slave_devices: try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._slave_devices = None self._slave_device_ids = None return success def select_xinput_devices(self): """ Select pointer devices and their events we want to listen to. """ # Select events of the master pointer. # Enter/leave events aren't supported by the slaves. event_mask = XIEventMask.EnterMask | \ XIEventMask.LeaveMask device = self._device_manager.get_client_pointer() _logger.info("listening to XInput master: {}" \ .format((device.name, device.id, device.get_config_string()))) try: self._device_manager.select_events(self, device, event_mask) except Exception as ex: _logger.warning("Failed to select events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._master_device = device self._master_device_id = device.id # Select events of all attached (non-floating) slave pointers. event_mask = XIEventMask.ButtonPressMask | \ XIEventMask.ButtonReleaseMask | \ XIEventMask.EnterMask | \ XIEventMask.LeaveMask | \ XIEventMask.MotionMask if self._touch_events_enabled: event_mask |= XIEventMask.TouchMask devices = self._device_manager.get_client_pointer_attached_slaves() _logger.info("listening to XInput slaves: {}" \ .format([(d.name, d.id, d.get_config_string()) \ for d in devices])) for device in devices: try: self._device_manager.select_events(self, device, event_mask) except Exception as ex: _logger.warning("Failed to select events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._slave_devices = devices self._slave_device_ids = [d.id for d in devices] def _select_xi_grab_events(self, select): """ Select root window events for simulating a pointer grab. Only called when a drag was initiated, e.g. when moving/resizing the keyboard. """ if select: event_mask = XIEventMask.ButtonReleaseMask | \ XIEventMask.MotionMask for device in self._slave_devices: try: self._device_manager.select_events(None, device, event_mask) except Exception as ex: _logger.warning("Failed to select root events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) else: for device in self._slave_devices: try: self._device_manager.unselect_events(None, device) except Exception as ex: _logger.warning("Failed to unselect root events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._xi_grab_events_selected = select def _on_device_grab(self, device, event): self.select_xinput_devices() def _on_devices_updated(self): self._clear_touch_active() def _on_device_event(self, event): """ Handler for XI2 events. """ event_type = event.xi_type device_id = event.device_id log = self._log_event_stub if _logger.isEnabledFor(logging.DEBUG) and \ event_type != XIEventType.Motion and \ event_type != XIEventType.TouchUpdate: self._log_device_event(event) log = self.log_event # re-select devices on changes to the device hierarchy if event_type in XIEventType.HierarchyEvents or \ event_type == XIEventType.DeviceChanged: self.select_xinput_devices() return log("_on_device_event1") if event_type == XIEventType.KeyPress or \ event_type == XIEventType.KeyRelease: return # check device_id, discard duplicate and unknown events if event_type == XIEventType.Enter or \ event_type == XIEventType.Leave: log("_on_device_event2 {} {}", device_id, self._master_device_id) # enter/leave are only expected from the master device if not device_id == self._master_device_id: log("_on_device_event3") return else: # all other pointer/touch events have to come from slaves log("_on_device_event4 {} {}", event.device_id, self._slave_device_ids) if not event.device_id in self._slave_device_ids: log("_on_device_event5") return # bail if the window isn't realized yet win = self.get_window() if not win: log("_on_device_event6") return # scale coordinates in response to changes to # org.gnome.desktop.interface scaling-factor try: scale = win.get_scale_factor() # from Gdk 3.10 log("_on_device_event7 {}", scale) if scale and scale != 1.0: scale = 1.0 / scale event.x = event.x * scale event.y = event.y * scale event.x_root = event.x_root * scale event.y_root = event.y_root * scale except AttributeError: pass # Slaves aren't grabbed for moving/resizing when simulating a drag # operation (drag click button), or when multiple slave devices are # involved (one for button press, another for motion). # -> Simulate pointer grab, select root events we can track even # outside the keyboard window. # None of these problems are assumed to exist for touch devices. log("_on_device_event8 {}", self._xi_grab_active) if self._xi_grab_active and \ (event_type == XIEventType.Motion or \ event_type == XIEventType.ButtonRelease): if not self._xi_grab_events_selected: self._select_xi_grab_events(True) log("_on_device_event9") # We only get root window coordinates for root window events, # so convert them to our target window's coordinates. rx, ry = win.get_root_coords(0, 0) event.x = event.x_root - rx event.y = event.y_root - ry else: # Is self the hit window? # We need this only for the multi-touch case with open # long press popup, e.g. while shift is held down with # one finger, touching anything in a long press popup must # not also affect the keyboard below. xid_event = event.xid_event xid_win = win.get_xid() log("_on_device_event10 {} {}", xid_event, xid_win) if xid_event != 0 and \ xid_event != xid_win: log("_on_device_event11") return # Dispatch events self._xi_event_handled = False if event_type == XIEventType.Motion: self._on_motion_event(self, event) elif event_type == XIEventType.TouchUpdate or \ event_type == XIEventType.TouchBegin or \ event_type == XIEventType.TouchEnd: self._on_touch_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) # Notify CSButtonMapper, end remapped click. if not self._xi_event_handled: EventSource.emit(self, "button-release", event) elif event_type == XIEventType.Enter: self._on_enter_notify(self, event) elif event_type == XIEventType.Leave: self._on_leave_notify(self, event) def _log_device_event(self, event): win = self.get_window() if not event.xi_type in [ XIEventType.TouchUpdate, XIEventType.Motion]: self.log_event("Device event: dev_id={} src_id={} xi_type={} " "xid_event={}({}) x={} y={} x_root={} y_root={} " "button={} state={} sequence={}" "".format(event.device_id, event.source_id, event.xi_type, event.xid_event, win.get_xid() if win else 0, event.x, event.y, event.x_root, event.y_root, event.button, event.state, event.sequence, ) ) device = event.get_source_device() self.log_event("Source device: " + str(device)) @staticmethod def log_event(msg, *args): _logger.event(msg.format(*args)) @staticmethod def _log_event_stub(msg, *args): pass
class ScanDevice(object): """ Input device manager class. Manages input devices on the system and deals with PnP related event. The actual press/release events are forwarded to a ScanMode instance. """ """ Default device name (virtual core pointer) """ DEFAULT_NAME = "Default" """ Device id's of the primary masters """ DEFAULT_VCP_ID = 2 DEFAULT_VCK_ID = 3 """ Device name blacklist """ blacklist = ["Virtual core pointer", "Virtual core keyboard", "Virtual core XTEST pointer", "Virtual core XTEST keyboard", "Power Button"] def __init__(self, event_handler): logger.debug("ScanDevice.__init__()") """ Selected device tuple (device id, master id) """ self._active_device_ids = None """ Whether the active device is detached """ self._floating = False """ Event handler for device events """ self._event_handler = event_handler """ The manager for osk XInput devices """ self._device_manager = XIDeviceManager() # singleton self._device_manager.connect("device-event", self._device_event_handler) config.scanner.device_name_notify_add(self._device_name_notify) config.scanner.device_detach_notify_add(self._device_detach_notify) self._device_name_notify(config.scanner.device_name) def __del__(self): logger.debug("ScanDevice.__del__()") def _device_event_handler(self, event): """ Handler for XI2 events. """ event_type = event.xi_type device_id = event.device_id if event_type == XIEventType.DeviceAdded: device = self._device_manager.lookup_device_id(device_id) show_new_device_dialog(device.name, device.get_config_string(), device.is_pointer(), self._on_new_device_accepted) elif event_type == XIEventType.DeviceRemoved: # If we are currently using this device, # close it and fall back to 'Default' if self._active_device_ids and \ self._active_device_ids[0] == device_id: self._active_device_ids = None self._floating = False config.scanner.device_detach = False config.scanner.device_name = self.DEFAULT_NAME else: # Never handle VCK events. if device_id != self.DEFAULT_VCK_ID: # Forward VCP events only if 'Default' is selected. # Else only handle devices we selected. if (device_id == self.DEFAULT_VCP_ID and \ config.scanner.device_name == self.DEFAULT_NAME) or \ (self._active_device_ids and \ device_id == self._active_device_ids[0]): self._event_handler(event) def _on_new_device_accepted(self, config_string): """ Callback for the 'New device' dialog. Called only if 'Use device' was chosen. """ config.scanner.device_name = config_string config.scanner.device_detach = True def _device_detach_notify(self, detach): """ Callback for the scanner.device_detach configuration changes. """ if self._active_device_ids is None: return if detach: if not self._floating: self.detach(self._active_device_ids[0]) else: if self._floating: self.attach(*self._active_device_ids) def _device_name_notify(self, name): """ Callback for the scanner.device_name configuration changes. """ self.close() if name == self.DEFAULT_NAME: return for device in self._device_manager.get_devices(): if self.is_useable(device) and \ name == device.get_config_string(): self.open(device) break if self._active_device_ids is None: logger.debug("Unknown device-name in configuration.") config.scanner.device_detach = False config.scanner.device_name = self.DEFAULT_NAME def open(self, device): """ Select for events and optionally detach the device. """ if device.is_pointer(): event_mask = XIEventMask.ButtonPressMask | \ XIEventMask.ButtonReleaseMask else: event_mask = XIEventMask.KeyPressMask | \ XIEventMask.KeyReleaseMask try: self._device_manager.select_events(None, device, event_mask) self._active_device_ids = (device.id, device.master) except Exception as ex: logger.warning("Failed to open device {id}: {ex}" .format(id = device.id, ex = ex)) if config.scanner.device_detach and not device.is_master(): self.detach(device.id) def close(self): """ Stop using the current device. """ if self._floating: self.attach(*self._active_device_ids) if self._active_device_ids: device = self._device_manager.lookup_device_id( \ self._active_device_ids[0]) try: self._device_manager.unselect_events(None, device) self._active_device_ids = None except Exception as ex: logger.warning("Failed to close device {id}: {ex}" .format(id = self._active_device_ids[0], ex = ex)) def attach(self, dev_id, master): """ Attach the device to a master. """ try: self._device_manager.attach_device_id(dev_id, master) self._floating = False except: logger.warning("Failed to attach device {id} to {master}" .format(id = dev_id, master = master)) def detach(self, dev_id): """ Detach the device from its master. """ try: self._device_manager.detach_device_id(dev_id) self._floating = True except: logger.warning("Failed to detach device {id}".format(id = dev_id)) def finalize(self): """ Clean up the ScanDevice instance. """ self._device_manager.disconnect("device-event", self._device_event_handler) config.scanner.device_name_notify_remove(self._device_name_notify) config.scanner.device_detach_notify_remove(self._device_detach_notify) self.close() self._event_handler = None self.devices = None @staticmethod def is_useable(device): """ Check whether this device is useable for scanning. """ return device.name not in ScanDevice.blacklist \ and device.enabled \ and not device.is_floating()
class CSFloatingSlave(ClickSimulator): """ Onboards built-in mouse click mapper. Maps secondary or middle button to the primary button. """ def __init__(self, keyboard): ClickSimulator.__init__(self) self._keyboard = keyboard self._device_manager = XIDeviceManager() self._grabbed_device_ids = [] self._num_clicks_detected = 0 self._motion_position = None self._button = self.PRIMARY_BUTTON self._click_type = self.CLICK_TYPE_SINGLE self._click_done_notify_callbacks = [] self._exclusion_rects = [] self._osk_cm = osk.ClickMapper() def cleanup(self): self.end_mapping() self._click_done_notify_callbacks = [] def is_valid(self): return self._device_manager.is_valid() def supports_click_params(self, button, click_type): return True def map_primary_click(self, event_source, button, click_type): if event_source and (button != self.PRIMARY_BUTTON or \ click_type != self.CLICK_TYPE_SINGLE): self._begin_mapping(event_source, button, click_type) else: self.end_mapping() def _begin_mapping(self, event_source, button, click_type): click_device = self._device_manager.get_last_click_device() motion_device = self._device_manager.get_last_motion_device() if self._register_xinput_events(click_device, motion_device): self._button = button self._click_type = click_type self._num_clicks_detected = 0 self._motion_position = None def end_mapping(self): self._deregister_xinput_events() self._button = self.PRIMARY_BUTTON self._click_type = self.CLICK_TYPE_SINGLE def _register_xinput_events(self, click_device, motion_device): success = self._register_xinput_device(click_device, "primary", XIEventMask.ButtonPressMask | \ XIEventMask.ButtonReleaseMask | \ XIEventMask.MotionMask) if success and not motion_device is click_device: success = self._register_xinput_device(motion_device, "motion", XIEventMask.MotionMask) if success: self._device_manager.connect("device-event", self._on_device_event) else: self._deregister_xinput_events() return success def _register_xinput_device(self, device, description, event_mask): _logger.info("grab {} device {}".format(description, device)) try: self._device_manager.grab_device(device) self._grabbed_device_ids.append(device.id) except osk.error as ex: _logger.error("grab device {id} '{name}': {ex}" .format(id = device.id, name=device.name, ex = ex)) return False try: self._device_manager.select_events(None, device, event_mask) except osk.error as ex: _logger.error("select root events for device " "{id} '{name}': {ex}" .format(id = device.id, name=device.name, ex = ex)) return False return True def _deregister_xinput_events(self): if self._grabbed_device_ids: self._device_manager.disconnect("device-event", self._on_device_event) # unselect and ungrab all devices for device_id in self._grabbed_device_ids: device = self._device_manager.lookup_device_id(device_id) _logger.info("ungrab " + str(device)) try: self._device_manager.unselect_events(None, device) except osk.error as ex: _logger.error("unselect root events for device " "{id} '{name}': {ex}" .format(id = device.id, name=device.name, ex = ex)) try: self._device_manager.ungrab_device(device) except osk.error as ex: _logger.error("ungrab device {id} '{name}': {ex}" .format(id = device.id, name=device.name, ex = ex)) self._grabbed_device_ids = [] def _on_device_event(self, event): event_type = event.xi_type button = self._button click_type = self._click_type generate_button_event = self._osk_cm.generate_button_event if not event.device_id in self._grabbed_device_ids: return #print("device event:", event.device_id, event.xi_type, (event.x, event.y), (event.x_root, event.y_root), event.xid_event) # Get pointer position from motion events only to support clicking # with one device and pointing with another. position = self._motion_position if event_type == XIEventType.Motion: position = (int(event.x_root), int(event.y_root)) self._motion_position = position # tell master pointer about our new position self._osk_cm.generate_motion_event(*position) if position is None and \ (event_type == XIEventType.ButtonPress or \ event_type == XIEventType.ButtonRelease): position = (int(event.x_root), int(event.y_root)) if position is None: return # single click if click_type == self.CLICK_TYPE_SINGLE: if event_type == XIEventType.ButtonPress: self._keyboard.maybe_send_alt_press(None, button, 0) generate_button_event(button, True) elif event_type == XIEventType.ButtonRelease: generate_button_event(button, False) self._keyboard.maybe_send_alt_release(None, button, 0) if self._num_clicks_detected and \ not self._is_point_in_exclusion_rects(position): self.end_mapped_click() # double click elif click_type == self.CLICK_TYPE_DOUBLE: if event_type == XIEventType.ButtonRelease: if self._num_clicks_detected: if not self._is_point_in_exclusion_rects(position): delay = 40 self._keyboard.maybe_send_alt_press(None, button, 0) generate_button_event(button, True) generate_button_event(button, False, delay) generate_button_event(button, True, delay) generate_button_event(button, False, delay) self._keyboard.maybe_send_alt_release(None, button, 0) self.end_mapped_click() # drag click elif click_type == self.CLICK_TYPE_DRAG: if event_type == XIEventType.ButtonRelease: if self._num_clicks_detected == 1: if self._is_point_in_exclusion_rects(position): self.end_mapped_click() else: self._keyboard.maybe_send_alt_press(None, button, 0) generate_button_event(button, True) elif self._num_clicks_detected >= 2: generate_button_event(button, False) self._keyboard.maybe_send_alt_release(None, button, 0) self.end_mapped_click() # count button presses if event_type == XIEventType.ButtonPress: self._num_clicks_detected += 1 def _is_point_in_exclusion_rects(self, point): for rect in self._exclusion_rects: if rect.is_point_within(point): return True return False def is_mapping_active(self): return bool(self._grabbed_device_ids) def get_click_button(self): return self._button def get_click_type(self): return self._click_type def state_notify_add(self, callback): self._click_done_notify_callbacks.append(callback) def end_mapped_click(self): """ osk callback, outside click, xi button release """ if self.is_mapping_active(): self.end_mapping() self._keyboard.release_latched_sticky_keys() # update click type buttons for callback in self._click_done_notify_callbacks: callback(None) def set_exclusion_rects(self, rects): self._exclusion_rects = rects
class GlobalKeyListener(EventSource): """ Singleton class that listens to key presses not bound to a specific window. """ _event_names = ("key-press", "key-release", "devices-updated") def __new__(cls, *args, **kwargs): """ Singleton magic. """ if not hasattr(cls, "self"): cls.self = object.__new__(cls, *args, **kwargs) cls.self.construct() return cls.self def __init__(self): """ Called multiple times, don't use this. """ pass def construct(self): """ Singleton constructor, runs only once. """ EventSource.__init__(self, self._event_names) self._device_manager = None self._keyboard_slave_devices = None def cleanup(self): """ Should not need to be called as long connect and disconnect calls are balanced. """ EventSource.cleanup(self) self._register_input_events_xinput(False) def connect(self, event_name, callback): EventSource.connect(self, event_name, callback) self._update_registered_events() def disconnect(self, event_name, callback): had_listeners = self.has_listeners() EventSource.disconnect(self, event_name, callback) self._update_registered_events() # help debugging disconnecting events on exit if had_listeners and not self.has_listeners(): _logger.info("all listeners disconnected") def _update_registered_events(self): self._register_input_events(self.has_listeners()) def _register_input_events(self, register): """ Only for XInput currently. Extend this if we find out how to do it on other, non-X platforms (or with Gtk). """ if not self._register_input_events_xinput(register): if register: _logger.warning("XInput event source failed to initialize.") def _register_input_events_xinput(self, register): """ Setup XInput event handling """ success = True if register: self._device_manager = XIDeviceManager() if self._device_manager.is_valid(): self._device_manager.connect("device-event", self._on_device_event) self._device_manager.connect("device-grab", self._on_device_grab) self._device_manager.connect("devices-updated", self._on_devices_updated) self._select_xinput_devices() else: success = False self._device_manager = None else: if self._device_manager: self._device_manager.disconnect("device-event", self._on_device_event) self._device_manager.disconnect("device-grab", self._on_device_grab) self._device_manager.disconnect("devices-updated", self._on_devices_updated) self._unselect_xinput_devices() self._device_manager = None return success def _select_xinput_devices(self): """ Select keyboard devices and the events we want to listen to. """ self._unselect_xinput_devices() event_mask = (XIEventMask.KeyPressMask | XIEventMask.KeyReleaseMask) devices = self._device_manager.get_client_keyboard_attached_slaves() _logger.info("listening to keyboard devices: {}".format([ (d.name, d.id, d.get_config_string()) for d in devices ])) for device in devices: try: self._device_manager.select_events(None, device, event_mask) except Exception as ex: _logger.warning("Failed to select events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._keyboard_slave_devices = devices def _unselect_xinput_devices(self): if self._keyboard_slave_devices: for device in self._keyboard_slave_devices: try: self._device_manager.unselect_events(None, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._keyboard_slave_devices = None def _on_device_grab(self, device, event): """ Someone grabbed/relased a device. Update our device list. """ self._select_xinput_devices() def _on_devices_updated(self): # re-select devices on changes to the device hierarchy self._select_xinput_devices() self.emit("devices-updated") def _on_device_event(self, event): """ Handler for XI2 events. """ event_type = event.xi_type if event_type == XIEventType.KeyPress: self.emit("key-press", event) elif event_type == XIEventType.KeyRelease: self.emit("key-release", event) def get_key_event_string(self, event, message=""): device = event.get_source_device() device_name = device.name if device else "None" return ((message + "global key-{}, keycode={}, keyval={} " "from device '{}' ({})").format( "press" if event.xi_type == XIEventType.KeyPress else "release", event.keycode, event.keyval, device_name, event.source_id))
class InputEventSource(EventSource, XIDeviceEventLogger): """ Setup and handle GTK or XInput device events. """ def __init__(self): # There is only button-release to subscribe to currently, # as this is all CSButtonRemapper needs to detect the end of a click. EventSource.__init__(self, ["button-release"]) XIDeviceEventLogger.__init__(self) self._gtk_handler_ids = None self._device_manager = None self._master_device = None # receives enter/leave events self._master_device_id = None # for convenience/performance self._slave_devices = None # receive pointer and touch events self._slave_device_ids = None # for convenience/performance self._xi_grab_active = False self._xi_grab_events_selected = False self._xi_event_handled = False self._touch_active = set() # set of id(XIDevice/GdkX11DeviceXI2) # For devices not contained here only # pointer events are considered. # Wacom devices with enabled gestures never # become touch-active, i.e. they don't # generate touch events. self.connect("realize", self._on_realize_event) self.connect("unrealize", self._on_unrealize_event) def cleanup(self): self._register_gtk_events(False) self._register_xinput_events(False) def _clear_touch_active(self): self._touch_active = set() def set_device_touch_active(self, device): """ Mark source device as actively receiving touch events """ self._touch_active.add(id(device)) def is_device_touch_active(self, device): """ Mark source device as actively receiving touch events """ return id(device) in self._touch_active def _on_realize_event(self, user_data): self.handle_realize_event() def _on_unrealize_event(self, user_data): self.handle_unrealize_event() def handle_realize_event(self): # register events in derived class pass def handle_unrealize_event(self): self.register_input_events(False) def grab_xi_pointer(self, active): """ Tell the xi event source a drag operation has started (ended) and we want to receive events of the whole screen. """ self._xi_grab_active = active # release simulated grab of slave device when the drag operation ends if not active and \ self._xi_grab_events_selected and \ self._device_manager: self._select_xi_grab_events(False) def set_xi_event_handled(self, handled): """ Tell the xi event source to stop/continue processing of handlers for the current event. """ self._xi_event_handled = handled def register_input_events(self, register, use_gtk=False): self._register_gtk_events(False) self._register_xinput_events(False) self._clear_touch_active() if register: if use_gtk: self._register_gtk_events(True) else: if not self._register_xinput_events(True): _logger.warning( "XInput event source failed to initialize, " "falling back to GTK.") self._register_gtk_events(True) def _register_gtk_events(self, register): """ Setup GTK event handling """ if register: 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._gtk_handler_ids = [ 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("enter-notify-event", self._on_enter_notify), self.connect("leave-notify-event", self._on_leave_notify), self.connect("touch-event", self._on_touch_event), ] else: if self._gtk_handler_ids: for id in self._gtk_handler_ids: self.disconnect(id) self._gtk_handler_ids = None def _register_xinput_events(self, register): """ Setup XInput event handling """ success = True if register: self._device_manager = XIDeviceManager() if self._device_manager.is_valid(): self._device_manager.connect("device-event", self._on_device_event) self._device_manager.connect("device-grab", self._on_device_grab) self._device_manager.connect("devices-updated", self._on_devices_updated) self.select_xinput_devices() else: success = False self._device_manager = None else: if self._device_manager: self._device_manager.disconnect("device-event", self._on_device_event) self._device_manager.disconnect("device-grab", self._on_device_grab) self._device_manager.disconnect("devices-updated", self._on_devices_updated) if self._master_device: device = self._master_device try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._master_device = None self._master_device_id = None if self._slave_devices: for device in self._slave_devices: try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._slave_devices = None self._slave_device_ids = None return success def select_xinput_devices(self): """ Select pointer devices and their events we want to listen to. """ # Select events of the master pointer. # Enter/leave events aren't supported by the slaves. event_mask = XIEventMask.EnterMask | \ XIEventMask.LeaveMask device = self._device_manager.get_client_pointer() _logger.info("listening to XInput master: {}" \ .format((device.name, device.id, device.get_config_string()))) try: self._device_manager.select_events(self, device, event_mask) except Exception as ex: _logger.warning("Failed to select events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._master_device = device self._master_device_id = device.id # Select events of all attached (non-floating) slave pointers. event_mask = XIEventMask.ButtonPressMask | \ XIEventMask.ButtonReleaseMask | \ XIEventMask.EnterMask | \ XIEventMask.LeaveMask | \ XIEventMask.MotionMask if self._touch_events_enabled: event_mask |= XIEventMask.TouchMask devices = self._device_manager.get_client_pointer_attached_slaves() _logger.info("listening to XInput slaves: {}" \ .format([(d.name, d.id, d.get_config_string()) \ for d in devices])) for device in devices: try: self._device_manager.select_events(self, device, event_mask) except Exception as ex: _logger.warning("Failed to select events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._slave_devices = devices self._slave_device_ids = [d.id for d in devices] def _select_xi_grab_events(self, select): """ Select root window events for simulating a pointer grab. Only called when a drag was initiated, e.g. when moving/resizing the keyboard. """ if select: event_mask = XIEventMask.ButtonReleaseMask | \ XIEventMask.MotionMask for device in self._slave_devices: try: self._device_manager.select_events(None, device, event_mask) except Exception as ex: _logger.warning("Failed to select root events for device " "{id}: {ex}".format(id=device.id, ex=ex)) else: for device in self._slave_devices: try: self._device_manager.unselect_events(None, device) except Exception as ex: _logger.warning( "Failed to unselect root events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._xi_grab_events_selected = select def _on_device_grab(self, device, event): self.select_xinput_devices() def _on_devices_updated(self): self._clear_touch_active() def _on_device_event(self, event): """ Handler for XI2 events. """ event_type = event.xi_type device_id = event.device_id log = self._log_event_stub if _logger.isEnabledFor(logging.DEBUG) and \ event_type != XIEventType.Motion and \ event_type != XIEventType.TouchUpdate: self._log_device_event(event) log = self.log_event # re-select devices on changes to the device hierarchy if event_type in XIEventType.HierarchyEvents or \ event_type == XIEventType.DeviceChanged: self.select_xinput_devices() return log("_on_device_event1") if event_type == XIEventType.KeyPress or \ event_type == XIEventType.KeyRelease: return # check device_id, discard duplicate and unknown events if event_type == XIEventType.Enter or \ event_type == XIEventType.Leave: log("_on_device_event2 {} {}", device_id, self._master_device_id) # enter/leave are only expected from the master device if not device_id == self._master_device_id: log("_on_device_event3") return else: # all other pointer/touch events have to come from slaves log("_on_device_event4 {} {}", event.device_id, self._slave_device_ids) if not event.device_id in self._slave_device_ids: log("_on_device_event5") return # bail if the window isn't realized yet win = self.get_window() if not win: log("_on_device_event6") return # scale coordinates in response to changes to # org.gnome.desktop.interface scaling-factor try: scale = win.get_scale_factor() # from Gdk 3.10 log("_on_device_event7 {}", scale) if scale and scale != 1.0: scale = 1.0 / scale event.x = event.x * scale event.y = event.y * scale event.x_root = event.x_root * scale event.y_root = event.y_root * scale except AttributeError: pass # Slaves aren't grabbed for moving/resizing when simulating a drag # operation (drag click button), or when multiple slave devices are # involved (one for button press, another for motion). # -> Simulate pointer grab, select root events we can track even # outside the keyboard window. # None of these problems are assumed to exist for touch devices. log("_on_device_event8 {}", self._xi_grab_active) if self._xi_grab_active and \ (event_type == XIEventType.Motion or event_type == XIEventType.ButtonRelease): if not self._xi_grab_events_selected: self._select_xi_grab_events(True) log("_on_device_event9") # We only get root window coordinates for root window events, # so convert them to our target window's coordinates. rx, ry = win.get_root_coords(0, 0) event.x = event.x_root - rx event.y = event.y_root - ry else: # Is self the hit window? # We need this only for the multi-touch case with open # long press popup, e.g. while shift is held down with # one finger, touching anything in a long press popup must # not also affect the keyboard below. xid_event = event.xid_event xid_win = self.get_xid() log("_on_device_event10 {} {}", xid_event, xid_win) if xid_event != 0 and \ xid_event != xid_win: log("_on_device_event11") return # Dispatch events self._xi_event_handled = False if event_type == XIEventType.Motion: self._on_motion_event(self, event) elif event_type == XIEventType.TouchUpdate or \ event_type == XIEventType.TouchBegin or \ event_type == XIEventType.TouchEnd: self._on_touch_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) # Notify CSButtonMapper, end remapped click. if not self._xi_event_handled: EventSource.emit(self, "button-release", event) elif event_type == XIEventType.Enter: self._on_enter_notify(self, event) elif event_type == XIEventType.Leave: self._on_leave_notify(self, event) def _log_device_event(self, event): if not event.xi_type in [XIEventType.TouchUpdate, XIEventType.Motion]: self.log_event("Device event: dev_id={} src_id={} xi_type={} " "xid_event={}({}) x={} y={} x_root={} y_root={} " "button={} state={} sequence={}" "".format( event.device_id, event.source_id, event.xi_type, event.xid_event, self.get_xid(), event.x, event.y, event.x_root, event.y_root, event.button, event.state, event.sequence, )) device = event.get_source_device() self.log_event("Source device: " + str(device)) @staticmethod def log_event(msg, *args): _logger.event(msg.format(*args)) @staticmethod def _log_event_stub(msg, *args): pass
class InputEventSource: """ Setup and handle GTK or XInput device events. """ def __init__(self): self._gtk_handler_ids = None self._device_manager = None self._master_device = None # receives enter/leave events self._master_device_id = None # for convenience/speed only self._slave_devices = None # receive pointer and touch events self._slave_device_ids = None # for convenience/speed only self._xi_drag_active = 0 self._xi_drag_events_selected = False self.connect("realize", self._on_realize_event) self.connect("unrealize", self._on_unrealize_event) def cleanup(self): self._register_gtk_events(False) self._register_xinput_events(False) def _on_realize_event(self, user_data): self.handle_realize_event() def _on_unrealize_event(self, user_data): self.handle_unrealize_event() def handle_realize_event(self): # register events in derived class pass def handle_unrealize_event(self): self.register_input_events(False) def set_xi_drag_active(self, active): """ Tell the xi event source a drag operation has started/ended. """ self._xi_drag_active = active # release slave device grab when the simulated grab ends if not active and \ self._xi_drag_events_selected and \ self._device_manager: self._select_xi_drag_events(False) def register_input_events(self, register, use_gtk = False): self._register_gtk_events(False) self._register_xinput_events(False) if register: if use_gtk: self._register_gtk_events(True) else: if not self._register_xinput_events(True): _logger.warning("XInput event source failed to initialize, " "falling back to GTK.") self._register_gtk_events(True) def _register_gtk_events(self, register): """ Setup GTK event handling """ if register: 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._gtk_handler_ids = [ 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("enter-notify-event", self._on_enter_notify), self.connect("leave-notify-event", self._on_leave_notify), self.connect("touch-event", self._on_touch_event), ] else: if self._gtk_handler_ids: for id in self._gtk_handler_ids: self.disconnect(id) self._gtk_handler_ids = None def _register_xinput_events(self, register): """ Setup XInput event handling """ success = True if register: self._device_manager = XIDeviceManager() if self._device_manager.is_valid(): self._device_manager.connect("device-event", self._device_event_handler) self.select_xinput_devices() else: success = False self._device_manager = None else: if self._device_manager: self._device_manager.disconnect("device-event", self._device_event_handler) if self._master_device: device = self._master_device try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._master_device = None self._master_device_id = None if self._slave_devices: for device in self._slave_devices: try: self._device_manager.unselect_events(self, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._slave_devices = None self._slave_device_ids = None return success def select_xinput_devices(self): """ Select events of all slave pointer devices. """ # select events for the master pointer event_mask = XIEventMask.EnterMask | \ XIEventMask.LeaveMask device = self._device_manager.get_client_pointer() _logger.info("listening to XInput master: {}" \ .format((device.name, device.id, device.get_config_string()))) try: self._device_manager.select_events(self, device, event_mask) except Exception as ex: _logger.warning("Failed to select events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._master_device = device self._master_device_id = device.id # select events for all attached (non-floating) slave pointers event_mask = XIEventMask.ButtonPressMask | \ XIEventMask.ButtonReleaseMask | \ XIEventMask.EnterMask | \ XIEventMask.LeaveMask | \ XIEventMask.MotionMask if self._touch_events_enabled: event_mask |= XIEventMask.TouchMask devices = self._device_manager.get_client_slave_pointer_devices() devices = [d for d in devices if not d.is_floating()] _logger.info("listening to XInput devices: {}" \ .format([(d.name, d.id, d.get_config_string()) \ for d in devices])) for device in devices: try: self._device_manager.select_events(self, device, event_mask) except Exception as ex: _logger.warning("Failed to select events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._slave_devices = devices self._slave_device_ids = [d.id for d in devices] def _select_xi_drag_events(self, select): """ Select events for the root window to simulate a pointer grab. Only relevant when a drag was initiated, i.e. moving/resizing the keyboard. """ if select: event_mask = XIEventMask.ButtonReleaseMask | \ XIEventMask.MotionMask for device in self._slave_devices: try: self._device_manager.select_events(None, device, event_mask) except Exception as ex: _logger.warning("Failed to select root events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) else: for device in self._slave_devices: try: self._device_manager.unselect_events(None, device) except Exception as ex: _logger.warning("Failed to unselect root events for device " "{id}: {ex}" .format(id = device.id, ex = ex)) self._xi_drag_events_selected = select def _device_event_handler(self, event): """ Handler for XI2 events. """ event_type = event.xi_type device_id = event.device_id if _logger.isEnabledFor(logging.DEBUG): self._log_event(event) # re-select devices on changes to the device hierarchy if event_type == XIEventType.DeviceAdded or \ event_type == XIEventType.DeviceRemoved or \ event_type == XIEventType.DeviceChanged: self.select_xinput_devices() return # check device_id, stop duplicate and unknown events if event_type == XIEventType.Enter or \ event_type == XIEventType.Leave: # enter/leave are only expected from the master device if not device_id == self._master_device_id: return else: # all other pointer/touch events have to come from slaves if not event.device_id in self._slave_device_ids: return win = self.get_window() if not win: return # Slaves aren't grabbed for moving/resizing when simulating a drag # operation (drag click button), or multiple slave devices are # involved (one for button press, another for motion). # None of these problems are assumed to exist for touch devices. # -> select root events we can track even outside the keyboard window. if self._xi_drag_active and \ (event_type == XIEventType.Motion or \ event_type == XIEventType.ButtonRelease): if not self._xi_drag_events_selected: self._select_xi_drag_events(True) #print(self._xi_drag_active, event_type, event.state, event.device_id, self._master_device_id, event.xid_event) # We don't get window coordinates for root window events, # so convert them from the root window coordinates. rx, ry = win.get_root_coords(0, 0) event.x = event.x_root - rx event.y = event.y_root - ry else: # Is self the hit window? # We need this only for the multi touch case with open # long press popup, e.g. while shift is held down, touching # anything in a long press popup must not also affect # the keyboard below. xid_event = event.xid_event if xid_event != 0 and \ xid_event != win.get_xid(): return # Dispatch events if event_type == XIEventType.Motion: self._on_motion_event(self, event) elif event_type == XIEventType.TouchUpdate or \ event_type == XIEventType.TouchBegin or \ event_type == XIEventType.TouchEnd: self._on_touch_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.Enter: self._on_enter_notify(self, event) elif event_type == XIEventType.Leave: self._on_leave_notify(self, event) def _log_event(self, event): win = self.get_window() if not event.xi_type in [ XIEventType.TouchUpdate, XIEventType.Motion]: _logger.debug("Device event: dev_id={} src_id={} xi_type={} " "xid_event={}({}) x={} y={} x_root={} y_root={} " "button={} state={} sequence={}" "".format(event.device_id, event.source_id, event.xi_type, event.xid_event, win.get_xid() if win else 0, event.x, event.y, event.x_root, event.y_root, event.button, event.state, event.sequence, ) )
class AutoHide(EventSource): """ Hide Onboard when a physical keyboard is being used. """ def __init__(self, keyboard): # There is only button-release to subscribe to currently, # as this is all CSButtonRemapper needs to detect the end of a click. EventSource.__init__(self, ["button-release"]) self._keyboard = keyboard self._device_manager = None self._keyboard_slave_devices = None def cleanup(self): self._register_xinput_events(False) def is_enabled(self): return self._device_manager is not None def enable(self, enable, use_gtk=False): self.register_input_events(enable, use_gtk) def register_input_events(self, register, use_gtk=False): self._register_xinput_events(False) if register: if not use_gtk: # can't do this with gtk yet if not self._register_xinput_events(True): _logger.warning( "XInput event source failed to initialize, " "falling back to GTK.") def _register_xinput_events(self, register): """ Setup XInput event handling """ success = True if register: self._device_manager = XIDeviceManager() if self._device_manager.is_valid(): self._device_manager.connect("device-event", self._on_device_event) self._device_manager.connect("device-grab", self._on_device_grab) self._select_xinput_devices() else: success = False self._device_manager = None else: if self._device_manager: self._device_manager.disconnect("device-event", self._on_device_event) self._device_manager.disconnect("device-grab", self._on_device_grab) self._unselect_xinput_devices() self._device_manager = None return success def _select_xinput_devices(self): """ Select keyboard devices and the events we want to listen to. """ self._unselect_xinput_devices() event_mask = XIEventMask.KeyPressMask | \ XIEventMask.KeyReleaseMask devices = self._device_manager.get_client_keyboard_attached_slaves() _logger.info("listening to keyboard devices: {}".format([ (d.name, d.id, d.get_config_string()) for d in devices ])) for device in devices: try: self._device_manager.select_events(None, device, event_mask) except Exception as ex: _logger.warning("Failed to select events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._keyboard_slave_devices = devices def _unselect_xinput_devices(self): if self._keyboard_slave_devices: for device in self._keyboard_slave_devices: try: self._device_manager.unselect_events(None, device) except Exception as ex: _logger.warning("Failed to unselect events for device " "{id}: {ex}".format(id=device.id, ex=ex)) self._keyboard_slave_devices = None def _on_device_grab(self, device, event): """ Someone grabbed/relased a device. Update our device list. """ self._select_xinput_devices() def _on_device_event(self, event): """ Handler for XI2 events. """ event_type = event.xi_type # re-select devices on changes to the device hierarchy if event_type in XIEventType.HierarchyEvents or \ event_type == XIEventType.DeviceChanged: self._select_xinput_devices() return if event_type == XIEventType.KeyPress or \ event_type == XIEventType.KeyRelease: if not self._keyboard.is_auto_show_paused(): if _logger.isEnabledFor(logging.INFO): device = event.get_source_device() device_name = device.name if device else "None" _logger.info("Hiding keyboard and pausing " "auto-show due to physical key-{} " "{} from device '{}' ({})".format( "press" if event_type == XIEventType.KeyPress else "release", event.keyval, device_name, event.source_id)) if self._keyboard.is_visible(): if config.are_word_suggestions_enabled(): self._keyboard.discard_changes() self._keyboard.set_visible(False) duration = config.auto_show.hide_on_key_press_pause if duration: if duration < 0.0: # negative means auto-hide is off duration = None self._keyboard.pause_auto_show(duration) return