Add a scroll button lock feature
authorPeter Hutterer <peter.hutterer@who-t.net>
Wed, 20 Mar 2019 00:56:51 +0000 (10:56 +1000)
committerPeter Hutterer <peter.hutterer@who-t.net>
Thu, 17 Oct 2019 02:21:41 +0000 (12:21 +1000)
Scroll button locking is an accessibility feature. When enabled, the scroll
button does not need to be held down, the first click holds it logically down,
to be released on the second click of that same button.

This is implemented as simple event filter, so we still get the same behavior
from the emulated logical button, i.e. a physical double click results in a
single logical click of that button provided no scrolling was triggered.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
14 files changed:
doc/user/scrolling.rst
src/evdev-fallback.c
src/evdev.c
src/evdev.h
src/libinput-private.h
src/libinput.c
src/libinput.h
src/libinput.sym
test/litest.c
test/litest.h
test/test-pointer.c
tools/libinput-debug-events.man
tools/shared.c
tools/shared.h

index c828a3d6fa4af2d16bb1960308436386b0c3e940..f5cd454c329038783d980fbc62356257b01c81c9 100644 (file)
@@ -117,6 +117,13 @@ the motion events. Cross-device scrolling is not supported but
 for one exception: libinput's :ref:`t440_support` enables the use of the middle
 button for button scrolling (even when the touchpad is disabled).
 
+If the scroll button lock is enabled (see
+**libinput_device_config_scroll_set_button_lock()**), the button does not
+need to be held down. Pressing and releasing the button once enables the
+button lock, the button is now considered logically held down. Pressing and
+releasing the button a second time logically releases the button. While the
+button is logically held down, motion events are converted to scroll events.
+
 .. _scroll_sources:
 
 ------------------------------------------------------------------------------
index 1a9113bc7bcbacd435efa3df99a78250f9c9e212..a40421f07e9c0c48859ce42acf585a8e47c53fb2 100644 (file)
@@ -1499,7 +1499,8 @@ fallback_change_scroll_method(struct evdev_device *device)
        struct fallback_dispatch *dispatch = fallback_dispatch(device->dispatch);
 
        if (device->scroll.want_method == device->scroll.method &&
-           device->scroll.want_button == device->scroll.button)
+           device->scroll.want_button == device->scroll.button &&
+           device->scroll.want_lock_enabled == device->scroll.lock_enabled)
                return;
 
        if (fallback_any_button_down(dispatch, device))
@@ -1507,6 +1508,8 @@ fallback_change_scroll_method(struct evdev_device *device)
 
        device->scroll.method = device->scroll.want_method;
        device->scroll.button = device->scroll.want_button;
+       device->scroll.lock_enabled = device->scroll.want_lock_enabled;
+       evdev_set_button_scroll_lock_enabled(device, device->scroll.lock_enabled);
 }
 
 static int
index 60a599e72fd9b6c6ac9fae4ccc3d83d6edf8ffb2..20b8dac90754487b3630a80ad768c8e2b3d1e08a 100644 (file)
@@ -198,6 +198,34 @@ static void
 evdev_button_scroll_button(struct evdev_device *device,
                           uint64_t time, int is_press)
 {
+       /* Where the button lock is enabled, we wrap the buttons into
+          their own little state machine and filter out the events.
+        */
+       switch (device->scroll.lock_state) {
+       case BUTTONSCROLL_LOCK_DISABLED:
+               break;
+       case BUTTONSCROLL_LOCK_IDLE:
+               assert(is_press);
+               device->scroll.lock_state = BUTTONSCROLL_LOCK_FIRSTDOWN;
+               evdev_log_debug(device, "scroll lock: first down\n");
+               break; /* handle event */
+       case BUTTONSCROLL_LOCK_FIRSTDOWN:
+               assert(!is_press);
+               device->scroll.lock_state = BUTTONSCROLL_LOCK_FIRSTUP;
+               evdev_log_debug(device, "scroll lock: first up\n");
+               return; /* filter release event */
+       case BUTTONSCROLL_LOCK_FIRSTUP:
+               assert(is_press);
+               device->scroll.lock_state = BUTTONSCROLL_LOCK_SECONDDOWN;
+               evdev_log_debug(device, "scroll lock: second down\n");
+               return; /* filter press event */
+       case BUTTONSCROLL_LOCK_SECONDDOWN:
+               assert(!is_press);
+               device->scroll.lock_state = BUTTONSCROLL_LOCK_IDLE;
+               evdev_log_debug(device, "scroll lock: idle\n");
+               break; /* handle event */
+       }
+
        if (is_press) {
                enum timer_flags flags = TIMER_FLAG_NONE;
 
@@ -705,6 +733,56 @@ evdev_scroll_get_default_button(struct libinput_device *device)
        return 0;
 }
 
+static enum libinput_config_status
+evdev_scroll_set_button_lock(struct libinput_device *device,
+                            enum libinput_config_scroll_button_lock_state state)
+{
+       struct evdev_device *evdev = evdev_device(device);
+
+       switch (state) {
+       case LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED:
+               evdev->scroll.want_lock_enabled = false;
+               break;
+       case LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED:
+               evdev->scroll.want_lock_enabled = true;
+               break;
+       default:
+               return LIBINPUT_CONFIG_STATUS_INVALID;
+       }
+
+       evdev->scroll.change_scroll_method(evdev);
+
+       return LIBINPUT_CONFIG_STATUS_SUCCESS;
+}
+
+static enum libinput_config_scroll_button_lock_state
+evdev_scroll_get_button_lock(struct libinput_device *device)
+{
+       struct evdev_device *evdev = evdev_device(device);
+
+       if (evdev->scroll.lock_state == BUTTONSCROLL_LOCK_DISABLED)
+               return LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED;
+       else
+               return LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED;
+}
+
+static enum libinput_config_scroll_button_lock_state
+evdev_scroll_get_default_button_lock(struct libinput_device *device)
+{
+       return LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED;
+}
+
+
+void
+evdev_set_button_scroll_lock_enabled(struct evdev_device *device,
+                                    bool enabled)
+{
+       if (enabled)
+               device->scroll.lock_state = BUTTONSCROLL_LOCK_IDLE;
+       else
+               device->scroll.lock_state = BUTTONSCROLL_LOCK_DISABLED;
+}
+
 void
 evdev_init_button_scroll(struct evdev_device *device,
                         void (*change_scroll_method)(struct evdev_device *))
@@ -726,6 +804,9 @@ evdev_init_button_scroll(struct evdev_device *device,
        device->scroll.config.set_button = evdev_scroll_set_button;
        device->scroll.config.get_button = evdev_scroll_get_button;
        device->scroll.config.get_default_button = evdev_scroll_get_default_button;
+       device->scroll.config.set_button_lock = evdev_scroll_set_button_lock;
+       device->scroll.config.get_button_lock = evdev_scroll_get_button_lock;
+       device->scroll.config.get_default_button_lock = evdev_scroll_get_default_button_lock;
        device->base.config.scroll_method = &device->scroll.config;
        device->scroll.method = evdev_scroll_get_default_method((struct libinput_device *)device);
        device->scroll.want_method = device->scroll.method;
index 7de1fea95d209ee2bfc2ed4cdf0b89d91bf10e56..0af0ba36b61dd844877095cfc13a459e92e37f44 100644 (file)
@@ -125,6 +125,14 @@ enum evdev_button_scroll_state {
        BUTTONSCROLL_SCROLLING,         /* have sent scroll events */
 };
 
+enum evdev_button_scroll_lock_state {
+       BUTTONSCROLL_LOCK_DISABLED,
+       BUTTONSCROLL_LOCK_IDLE,
+       BUTTONSCROLL_LOCK_FIRSTDOWN,
+       BUTTONSCROLL_LOCK_FIRSTUP,
+       BUTTONSCROLL_LOCK_SECONDDOWN,
+};
+
 enum evdev_debounce_state {
        /**
         * Initial state, no debounce but monitoring events
@@ -224,6 +232,10 @@ struct evdev_device {
                struct wheel_angle wheel_click_angle;
 
                struct wheel_tilt_flags is_tilt;
+
+               enum evdev_button_scroll_lock_state lock_state;
+               bool want_lock_enabled;
+               bool lock_enabled;
        } scroll;
 
        struct {
@@ -557,6 +569,10 @@ void
 evdev_init_button_scroll(struct evdev_device *device,
                         void (*change_scroll_method)(struct evdev_device *));
 
+void
+evdev_set_button_scroll_lock_enabled(struct evdev_device *device,
+                                    bool enabled);
+
 int
 evdev_update_key_down_count(struct evdev_device *device,
                            int code,
index 1950e6637d220549337bf4c1091ffc1f41eb8241..abb478944e732cf6bba6b773707b398039b1c075 100644 (file)
@@ -272,6 +272,10 @@ struct libinput_device_config_scroll_method {
                                                  uint32_t button);
        uint32_t (*get_button)(struct libinput_device *device);
        uint32_t (*get_default_button)(struct libinput_device *device);
+       enum libinput_config_status (*set_button_lock)(struct libinput_device *device,
+                                                      enum libinput_config_scroll_button_lock_state);
+       enum libinput_config_scroll_button_lock_state (*get_button_lock)(struct libinput_device *device);
+       enum libinput_config_scroll_button_lock_state (*get_default_button_lock)(struct libinput_device *device);
 };
 
 struct libinput_device_config_click_method {
index 6d00a006624782461a185fd2ac8cb4481cef57f1..9570dc684899e199ea862360d4e3392f1c714eb6 100644 (file)
@@ -4149,6 +4149,45 @@ libinput_device_config_scroll_get_default_button(struct libinput_device *device)
        return device->config.scroll_method->get_default_button(device);
 }
 
+LIBINPUT_EXPORT enum libinput_config_status
+libinput_device_config_scroll_set_button_lock(struct libinput_device *device,
+                                             enum libinput_config_scroll_button_lock_state state)
+{
+       if ((libinput_device_config_scroll_get_methods(device) &
+            LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN) == 0)
+               return LIBINPUT_CONFIG_STATUS_UNSUPPORTED;
+
+       switch (state) {
+       case LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED:
+       case LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED:
+               break;
+       default:
+               return LIBINPUT_CONFIG_STATUS_INVALID;
+       }
+
+       return device->config.scroll_method->set_button_lock(device, state);
+}
+
+LIBINPUT_EXPORT enum libinput_config_scroll_button_lock_state
+libinput_device_config_scroll_get_button_lock(struct libinput_device *device)
+{
+       if ((libinput_device_config_scroll_get_methods(device) &
+            LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN) == 0)
+               return LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED;
+
+       return device->config.scroll_method->get_button_lock(device);
+}
+
+LIBINPUT_EXPORT enum libinput_config_scroll_button_lock_state
+libinput_device_config_scroll_get_default_button_lock(struct libinput_device *device)
+{
+       if ((libinput_device_config_scroll_get_methods(device) &
+            LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN) == 0)
+               return LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED;
+
+       return device->config.scroll_method->get_default_button_lock(device);
+}
+
 LIBINPUT_EXPORT int
 libinput_device_config_dwt_is_available(struct libinput_device *device)
 {
index 59463f43314925bfb0717e2c83ab88a80df69a42..54d40de641d166a5c0ee83bbdbbe6a43abb46a97 100644 (file)
@@ -5594,6 +5594,81 @@ libinput_device_config_scroll_get_button(struct libinput_device *device);
 uint32_t
 libinput_device_config_scroll_get_default_button(struct libinput_device *device);
 
+enum libinput_config_scroll_button_lock_state {
+       LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED,
+       LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED,
+};
+
+/**
+ * @ingroup config
+ *
+ * Set the scroll button lock. If the state is
+ * @ref LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED, the button must
+ * physically be held down for button scrolling to work.
+ * If the state is
+ * @ref LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED, the button is considered
+ * logically down after the first press and release sequence, and logically
+ * up after the second press and release sequence.
+ *
+ * @param device The device to configure
+ * @param state The state to set the scroll button lock to
+ *
+ * @return A config status code. Disabling the scroll button lock on
+ * device that does not support button scrolling always succeeds.
+ *
+ * @see libinput_device_config_scroll_set_button
+ * @see libinput_device_config_scroll_get_button
+ * @see libinput_device_config_scroll_get_default_button
+ */
+enum libinput_config_status
+libinput_device_config_scroll_set_button_lock(struct libinput_device *device,
+                                             enum libinput_config_scroll_button_lock_state state);
+
+/**
+ * @ingroup config
+ *
+ * Get the current scroll button lock state.
+ *
+ * If @ref LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN scroll method is not
+ * supported, or no button is set, this function returns @ref
+ * LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED.
+ *
+ * @note The return value is independent of the currently selected
+ * scroll-method. For the scroll button lock to activate, a device must have
+ * the @ref LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN method enabled, and a
+ * non-zero button set as scroll button.
+ *
+ * @param device The device to configure
+ * @return The scroll button lock state
+ *
+ * @see libinput_device_config_scroll_set_button
+ * @see libinput_device_config_scroll_set_button_lock
+ * @see libinput_device_config_scroll_get_button_lock
+ * @see libinput_device_config_scroll_get_default_button_lock
+ */
+enum libinput_config_scroll_button_lock_state
+libinput_device_config_scroll_get_button_lock(struct libinput_device *device);
+
+/**
+ * @ingroup config
+ *
+ * Get the default scroll button lock state.
+ *
+ * If @ref LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN scroll method is not
+ * supported, or no button is set, this function returns @ref
+ * LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED.
+ *
+ * @param device The device to configure
+ * @return The default scroll button lock state
+ *
+ * @see libinput_device_config_scroll_set_button
+ * @see libinput_device_config_scroll_set_button_lock
+ * @see libinput_device_config_scroll_get_button_lock
+ * @see libinput_device_config_scroll_get_default_button_lock
+ */
+enum libinput_config_scroll_button_lock_state
+libinput_device_config_scroll_get_default_button_lock(struct libinput_device *device);
+
 /**
  * @ingroup config
  *
index ef9d91780b91b49dd69fc7c02d19ecaf709bd872..1698a066d90aa71d490e89e18d18b8eb6e719724 100644 (file)
@@ -305,3 +305,9 @@ LIBINPUT_1.14 {
        libinput_event_tablet_tool_size_major_has_changed;
        libinput_event_tablet_tool_size_minor_has_changed;
 } LIBINPUT_1.11;
+
+LIBINPUT_1.15 {
+       libinput_device_config_scroll_set_button_lock;
+       libinput_device_config_scroll_get_button_lock;
+       libinput_device_config_scroll_get_default_button_lock;
+} LIBINPUT_1.14;
index 5b09ec4e4e3f1e07b7549a1cf8ac4f45371b4929..2e17009e7290bc63c9f350a1ba6ce8fea78944d4 100644 (file)
@@ -2495,6 +2495,27 @@ litest_button_scroll(struct litest_device *dev,
        libinput_dispatch(li);
 }
 
+void
+litest_button_scroll_locked(struct litest_device *dev,
+                           unsigned int button,
+                           double dx, double dy)
+{
+       struct libinput *li = dev->libinput;
+
+       litest_button_click_debounced(dev, li, button, 1);
+       litest_button_click_debounced(dev, li, button, 0);
+
+       libinput_dispatch(li);
+       litest_timeout_buttonscroll();
+       libinput_dispatch(li);
+
+       litest_event(dev, EV_REL, REL_X, dx);
+       litest_event(dev, EV_REL, REL_Y, dy);
+       litest_event(dev, EV_SYN, SYN_REPORT, 0);
+
+       libinput_dispatch(li);
+}
+
 void
 litest_keyboard_key(struct litest_device *d, unsigned int key, bool is_press)
 {
index 85a0a1f2ee54476a40e94be17cb3aa8107740c7e..fea10f7ec6619a4fbeb4bbacbf28bf9d6a3b339e 100644 (file)
@@ -663,6 +663,10 @@ void
 litest_button_scroll(struct litest_device *d,
                     unsigned int button,
                     double dx, double dy);
+void
+litest_button_scroll_locked(struct litest_device *d,
+                           unsigned int button,
+                           double dx, double dy);
 
 void
 litest_keyboard_key(struct litest_device *d,
index 4ec32f894ab74e0386eecad5c8c0a4ca74c09583..f148264224542397e01f0ca147f456e491231e3c 100644 (file)
@@ -1227,6 +1227,503 @@ START_TEST(pointer_scroll_button_device_remove_while_down)
 }
 END_TEST
 
+static void
+litest_enable_scroll_button_lock(struct litest_device *dev,
+                                unsigned int button)
+{
+       struct libinput_device *device = dev->libinput_device;
+       enum libinput_config_status status;
+
+       status = libinput_device_config_scroll_set_method(device,
+                                                         LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN);
+       ck_assert_int_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
+
+       status = libinput_device_config_scroll_set_button(device, button);
+       ck_assert_int_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
+
+       status = libinput_device_config_scroll_set_button_lock(device,
+                                                              LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED);
+       ck_assert_int_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
+}
+
+START_TEST(pointer_scroll_button_lock)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+
+       litest_enable_scroll_button_lock(dev, BTN_LEFT);
+       litest_disable_middleemu(dev);
+
+       litest_drain_events(li);
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+
+       litest_assert_empty_queue(li);
+
+       litest_timeout_buttonscroll();
+       libinput_dispatch(li);
+
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+
+       libinput_dispatch(li);
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       libinput_dispatch(li);
+
+       litest_assert_scroll(li, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL, 6);
+
+       litest_assert_empty_queue(li);
+
+       /* back to motion */
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+       litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+}
+END_TEST
+
+START_TEST(pointer_scroll_button_lock_defaults)
+{
+       struct litest_device *dev = litest_current_device();
+       enum libinput_config_scroll_button_lock_state state;
+
+       state = libinput_device_config_scroll_get_button_lock(dev->libinput_device);
+       ck_assert_int_eq(state, LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED);
+       state = libinput_device_config_scroll_get_default_button_lock(dev->libinput_device);
+       ck_assert_int_eq(state, LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED);
+}
+END_TEST
+
+START_TEST(pointer_scroll_button_lock_config)
+{
+       struct litest_device *dev = litest_current_device();
+       enum libinput_config_status status;
+       enum libinput_config_scroll_button_lock_state state;
+
+       state = libinput_device_config_scroll_get_button_lock(dev->libinput_device);
+       ck_assert_int_eq(state, LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED);
+       state = libinput_device_config_scroll_get_default_button_lock(dev->libinput_device);
+       ck_assert_int_eq(state, LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED);
+
+       status = libinput_device_config_scroll_set_button_lock(dev->libinput_device,
+                                                              LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED);
+       ck_assert_int_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
+       state = libinput_device_config_scroll_get_button_lock(dev->libinput_device);
+       ck_assert_int_eq(state, LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED);
+
+
+       status = libinput_device_config_scroll_set_button_lock(dev->libinput_device,
+                                                              LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED);
+       ck_assert_int_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
+       state = libinput_device_config_scroll_get_button_lock(dev->libinput_device);
+       ck_assert_int_eq(state, LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED);
+
+       status = libinput_device_config_scroll_set_button_lock(dev->libinput_device,
+                                                              LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED + 1);
+       ck_assert_int_eq(status, LIBINPUT_CONFIG_STATUS_INVALID);
+}
+END_TEST
+
+START_TEST(pointer_scroll_button_lock_enable_while_down)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+
+       litest_disable_middleemu(dev);
+       litest_drain_events(li);
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+
+       /* Enable lock while button is down */
+       litest_enable_scroll_button_lock(dev, BTN_LEFT);
+
+       litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
+       litest_assert_empty_queue(li);
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED);
+       litest_assert_empty_queue(li);
+
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+
+       /* no scrolling yet */
+       litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+
+       /* but on the next button press we scroll lock */
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       libinput_dispatch(li);
+       litest_timeout_buttonscroll();
+       libinput_dispatch(li);
+
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+
+       litest_assert_scroll(li, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL, 6);
+
+       litest_assert_empty_queue(li);
+
+       /* back to motion */
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+       litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+}
+END_TEST
+
+START_TEST(pointer_scroll_button_lock_enable_while_down_just_lock)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+
+       litest_disable_middleemu(dev);
+       litest_drain_events(li);
+
+       /* switch method first, but enable lock when we already have a
+        * button down */
+       libinput_device_config_scroll_set_method(dev->libinput_device,
+                                       LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN);
+       libinput_device_config_scroll_set_button(dev->libinput_device,
+                                       BTN_LEFT);
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       libinput_device_config_scroll_set_button_lock(dev->libinput_device,
+                                       LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED);
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
+       litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED);
+       litest_assert_empty_queue(li);
+
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+
+       /* no scrolling yet */
+       litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+
+       /* but on the next button press we scroll lock */
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       libinput_dispatch(li);
+       litest_timeout_buttonscroll();
+       libinput_dispatch(li);
+
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+
+       litest_assert_scroll(li, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL, 6);
+
+       litest_assert_empty_queue(li);
+
+       /* back to motion */
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+       litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+}
+END_TEST
+
+START_TEST(pointer_scroll_button_lock_otherbutton)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+
+       litest_disable_middleemu(dev);
+       litest_drain_events(li);
+
+       litest_enable_scroll_button_lock(dev, BTN_LEFT);
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       litest_assert_empty_queue(li);
+       litest_timeout_buttonscroll();
+       libinput_dispatch(li);
+
+       /* other button passes on normally */
+       litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+       litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_PRESSED);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_RELEASED);
+       litest_assert_empty_queue(li);
+
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+       litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_AXIS);
+
+       /* other button passes on normally */
+       litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+       litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_PRESSED);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_RELEASED);
+
+       /* stop scroll lock */
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_AXIS);
+
+       /* other button passes on normally */
+       litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+       litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_PRESSED);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_RELEASED);
+
+       litest_assert_empty_queue(li);
+}
+END_TEST
+
+START_TEST(pointer_scroll_button_lock_enable_while_otherbutton_down)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+
+       litest_disable_middleemu(dev);
+       litest_drain_events(li);
+
+       litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+       litest_timeout_middlebutton();
+       litest_drain_events(li);
+
+       /* Enable lock while button is down */
+       litest_enable_scroll_button_lock(dev, BTN_LEFT);
+
+       /* We only enable once we go to a neutral state so this still counts
+        * as normal button event */
+       for (int twice = 0; twice < 2; twice++) {
+               litest_button_click_debounced(dev, li, BTN_LEFT, true);
+               litest_button_click_debounced(dev, li, BTN_LEFT, false);
+               litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
+               litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED);
+
+               for (int i = 0; i < 10; i++) {
+                       litest_event(dev, EV_REL, REL_X, 1);
+                       litest_event(dev, EV_REL, REL_Y, 6);
+                       litest_event(dev, EV_SYN, SYN_REPORT, 0);
+               }
+               litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+       }
+
+       litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_RELEASED);
+       litest_assert_empty_queue(li);
+
+       /* now we should trigger it */
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       litest_timeout_buttonscroll();
+       litest_assert_empty_queue(li);
+
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       litest_assert_scroll(li, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL, 6);
+       litest_assert_empty_queue(li);
+
+       /* back to motion */
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+       litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION);
+}
+END_TEST
+
+enum mb_buttonorder {
+       LLRR, /* left down, left up, r down, r up */
+       LRLR, /* left down, right down, left up, right up */
+       LRRL,
+       RRLL,
+       RLRL,
+       RLLR,
+       _MB_BUTTONORDER_COUNT
+};
+
+START_TEST(pointer_scroll_button_lock_middlebutton)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+       enum mb_buttonorder buttonorder = _i; /* ranged test */
+
+       if (!libinput_device_config_middle_emulation_is_available(dev->libinput_device))
+               return;
+
+       litest_enable_middleemu(dev);
+
+       litest_enable_scroll_button_lock(dev, BTN_LEFT);
+       litest_drain_events(li);
+
+       /* We expect scroll lock to work only where left and right are never
+        * held down simultaneously. Everywhere else we expect middle button
+        * instead.
+        */
+       switch (buttonorder) {
+       case LLRR:
+               litest_button_click_debounced(dev, li, BTN_LEFT, true);
+               litest_button_click_debounced(dev, li, BTN_LEFT, false);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+               break;
+       case LRLR:
+               litest_button_click_debounced(dev, li, BTN_LEFT, true);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+               litest_button_click_debounced(dev, li, BTN_LEFT, false);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+               break;
+       case LRRL:
+               litest_button_click_debounced(dev, li, BTN_LEFT, true);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+               litest_button_click_debounced(dev, li, BTN_LEFT, false);
+               break;
+       case RRLL:
+               litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+               litest_button_click_debounced(dev, li, BTN_LEFT, true);
+               litest_button_click_debounced(dev, li, BTN_LEFT, false);
+               break;
+       case RLRL:
+               litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+               litest_button_click_debounced(dev, li, BTN_LEFT, true);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+               litest_button_click_debounced(dev, li, BTN_LEFT, false);
+               break;
+       case RLLR:
+               litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+               litest_button_click_debounced(dev, li, BTN_LEFT, true);
+               litest_button_click_debounced(dev, li, BTN_LEFT, false);
+               litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+               break;
+       default:
+               abort();
+       }
+
+       libinput_dispatch(li);
+       litest_timeout_middlebutton();
+       litest_timeout_buttonscroll();
+       libinput_dispatch(li);
+
+       /* motion events are the same for all of them */
+       for (int i = 0; i < 10; i++) {
+               litest_event(dev, EV_REL, REL_X, 1);
+               litest_event(dev, EV_REL, REL_Y, 6);
+               litest_event(dev, EV_SYN, SYN_REPORT, 0);
+       }
+
+       libinput_dispatch(li);
+
+       switch (buttonorder) {
+       case LLRR:
+       case RRLL:
+               litest_button_click_debounced(dev, li, BTN_LEFT, true);
+               litest_button_click_debounced(dev, li, BTN_LEFT, false);
+               break;
+       default:
+               break;
+       }
+
+       libinput_dispatch(li);
+
+       switch (buttonorder) {
+       case LLRR:
+       case RRLL:
+               litest_assert_button_event(li, BTN_RIGHT,
+                                          LIBINPUT_BUTTON_STATE_PRESSED);
+               litest_assert_button_event(li, BTN_RIGHT,
+                                          LIBINPUT_BUTTON_STATE_RELEASED);
+               litest_assert_scroll(li, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL, 6);
+               litest_assert_empty_queue(li);
+               break;
+       case LRLR:
+       case LRRL:
+       case RLRL:
+       case RLLR:
+               litest_assert_button_event(li, BTN_MIDDLE,
+                                          LIBINPUT_BUTTON_STATE_PRESSED);
+               litest_assert_button_event(li, BTN_MIDDLE,
+                                          LIBINPUT_BUTTON_STATE_RELEASED);
+               litest_assert_only_typed_events(li,
+                                               LIBINPUT_EVENT_POINTER_MOTION);
+               break;
+       default:
+               abort();
+       }
+
+}
+END_TEST
+
+START_TEST(pointer_scroll_button_lock_doubleclick_nomove)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+
+       litest_disable_middleemu(dev);
+       litest_enable_scroll_button_lock(dev, BTN_LEFT);
+       litest_drain_events(li);
+
+       /* double click without move in between counts as single click */
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+       litest_assert_empty_queue(li);
+       litest_button_click_debounced(dev, li, BTN_LEFT, true);
+       litest_button_click_debounced(dev, li, BTN_LEFT, false);
+
+       litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED);
+       litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_RELEASED);
+       litest_assert_empty_queue(li);
+
+       /* But a non-scroll button it should work normally */
+       litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+       litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+       litest_button_click_debounced(dev, li, BTN_RIGHT, true);
+       litest_button_click_debounced(dev, li, BTN_RIGHT, false);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_PRESSED);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_RELEASED);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_PRESSED);
+       litest_assert_button_event(li, BTN_RIGHT, LIBINPUT_BUTTON_STATE_RELEASED);
+       litest_assert_empty_queue(li);
+
+}
+END_TEST
+
 START_TEST(pointer_scroll_nowheel_defaults)
 {
        struct litest_device *dev = litest_current_device();
@@ -2691,6 +3188,7 @@ TEST_COLLECTION(pointer)
        struct range axis_range = {ABS_X, ABS_Y + 1};
        struct range compass = {0, 7}; /* cardinal directions */
        struct range buttons = {BTN_LEFT, BTN_TASK + 1};
+       struct range buttonorder = {0, _MB_BUTTONORDER_COUNT};
 
        litest_add("pointer:motion", pointer_motion_relative, LITEST_RELATIVE, LITEST_POINTINGSTICK);
        litest_add_for_device("pointer:motion", pointer_motion_relative_zero, LITEST_MOUSE);
@@ -2709,6 +3207,17 @@ TEST_COLLECTION(pointer)
        litest_add("pointer:scroll", pointer_scroll_button_no_event_before_timeout, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
        litest_add("pointer:scroll", pointer_scroll_button_middle_emulation, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
        litest_add("pointer:scroll", pointer_scroll_button_device_remove_while_down, LITEST_ANY, LITEST_RELATIVE|LITEST_BUTTON);
+
+       litest_add("pointer:scroll", pointer_scroll_button_lock, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
+       litest_add("pointer:scroll", pointer_scroll_button_lock_defaults, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
+       litest_add("pointer:scroll", pointer_scroll_button_lock_config, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
+       litest_add("pointer:scroll", pointer_scroll_button_lock_enable_while_down, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
+       litest_add("pointer:scroll", pointer_scroll_button_lock_enable_while_down_just_lock, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
+       litest_add("pointer:scroll", pointer_scroll_button_lock_otherbutton, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
+       litest_add("pointer:scroll", pointer_scroll_button_lock_enable_while_otherbutton_down, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
+       litest_add_ranged("pointer:scroll", pointer_scroll_button_lock_middlebutton, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY, &buttonorder);
+       litest_add("pointer:scroll", pointer_scroll_button_lock_doubleclick_nomove, LITEST_RELATIVE|LITEST_BUTTON, LITEST_ANY);
+
        litest_add("pointer:scroll", pointer_scroll_nowheel_defaults, LITEST_RELATIVE|LITEST_BUTTON, LITEST_WHEEL);
        litest_add_for_device("pointer:scroll", pointer_scroll_defaults_logitech_marble , LITEST_LOGITECH_TRACKBALL);
        litest_add("pointer:scroll", pointer_scroll_natural_defaults, LITEST_WHEEL, LITEST_TABLET);
index 29a03c5a3fdb846d2c334b6524e377f39abf6914..0fc7300947e95f5b3e4e41beb024004be6fa3349 100644 (file)
@@ -79,6 +79,9 @@ Enable or disable middle button emulation
 .B \-\-enable\-dwt|\-\-disable\-dwt
 Enable or disable disable-while-typing
 .TP 8
+.B \-\-enable\scroll-button-lock|\-\-disable\-scroll-button-lock
+Enable or disable the scroll button lock
+.TP 8
 .B \-\-set\-click\-method=[none|clickfinger|buttonareas]
 Set the desired click method
 .TP 8
index 8541c595d6c4187edccbe3ba94d726f8508e82dc..7dba96dc5c6cdda4dc4fdde751781f7249b88a71 100644 (file)
@@ -83,6 +83,7 @@ tools_init_options(struct tools_options *options)
        options->click_method = -1;
        options->scroll_method = -1;
        options->scroll_button = -1;
+       options->scroll_button_lock = -1;
        options->speed = 0.0;
        options->profile = LIBINPUT_CONFIG_ACCEL_PROFILE_NONE;
 }
@@ -198,6 +199,12 @@ tools_parse_option(int option,
                        return 1;
                }
                break;
+       case OPT_SCROLL_BUTTON_LOCK_ENABLE:
+               options->scroll_button_lock = true;
+               break;
+       case OPT_SCROLL_BUTTON_LOCK_DISABLE:
+               options->scroll_button_lock = false;
+               break;
        case OPT_SPEED:
                if (!optarg)
                        return 1;
@@ -407,6 +414,10 @@ tools_device_apply_config(struct libinput_device *device,
        if (options->scroll_button != -1)
                libinput_device_config_scroll_set_button(device,
                                                         options->scroll_button);
+       if (options->scroll_button_lock != -1)
+               libinput_device_config_scroll_set_button_lock(device,
+                                                             options->scroll_button_lock);
+
 
        if (libinput_device_config_accel_is_available(device)) {
                libinput_device_config_accel_set_speed(device,
index e2a6d662fd5e009cff0fbb02e20db0a194a29222..e36bfd6e5720f34fc8d6799c9a4fea51e05cf6ee 100644 (file)
@@ -51,6 +51,8 @@ enum configuration_options {
        OPT_CLICK_METHOD,
        OPT_SCROLL_METHOD,
        OPT_SCROLL_BUTTON,
+       OPT_SCROLL_BUTTON_LOCK_ENABLE,
+       OPT_SCROLL_BUTTON_LOCK_DISABLE,
        OPT_SPEED,
        OPT_PROFILE,
        OPT_DISABLE_SENDEVENTS,
@@ -73,6 +75,8 @@ enum configuration_options {
        { "disable-middlebutton",      no_argument,       0, OPT_MIDDLEBUTTON_DISABLE }, \
        { "enable-dwt",                no_argument,       0, OPT_DWT_ENABLE }, \
        { "disable-dwt",               no_argument,       0, OPT_DWT_DISABLE }, \
+       { "enable-scroll-button-lock", no_argument,       0, OPT_SCROLL_BUTTON_LOCK_ENABLE }, \
+       { "disable-scroll-button-lock",no_argument,       0, OPT_SCROLL_BUTTON_LOCK_DISABLE }, \
        { "set-click-method",          required_argument, 0, OPT_CLICK_METHOD }, \
        { "set-scroll-method",         required_argument, 0, OPT_SCROLL_METHOD }, \
        { "set-scroll-button",         required_argument, 0, OPT_SCROLL_BUTTON }, \
@@ -100,6 +104,7 @@ struct tools_options {
        enum libinput_config_scroll_method scroll_method;
        enum libinput_config_tap_button_map tap_map;
        int scroll_button;
+       int scroll_button_lock;
        double speed;
        int dwt;
        enum libinput_config_accel_profile profile;