Example #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 = 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
Example #2
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
Example #3
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,
                                   )
                         )