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 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