Exemplo n.º 1
0
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
Exemplo n.º 2
0
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()
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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()
Exemplo n.º 5
0
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))
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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,
                                   )
                         )
Exemplo n.º 9
0
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