diff --git a/meson.build b/meson.build index abc8d765..0a8d6a26 100644 --- a/meson.build +++ b/meson.build @@ -431,6 +431,14 @@ configure_file(input : 'tools/libinput-measure-touchpad-pressure.man', install : true, install_dir : join_paths(get_option('mandir'), 'man1') ) +install_data('tools/libinput-measure-touch-size', + install_dir : libinput_tool_path) +configure_file(input : 'tools/libinput-measure-touch-size.man', + output : 'libinput-measure-touch-size.1', + configuration : man_config, + install : true, + install_dir : join_paths(get_option('mandir'), 'man1') + ) if get_option('debug-gui') dep_gtk = dependency('gtk+-3.0') diff --git a/src/evdev-mt-touchpad.c b/src/evdev-mt-touchpad.c index 7f568d91..f2dd7b57 100644 --- a/src/evdev-mt-touchpad.c +++ b/src/evdev-mt-touchpad.c @@ -348,6 +348,16 @@ tp_process_absolute(struct tp_dispatch *tp, t->dirty = true; tp->queued |= TOUCHPAD_EVENT_OTHERAXIS; break; + case ABS_MT_TOUCH_MAJOR: + t->major = e->value; + t->dirty = true; + tp->queued |= TOUCHPAD_EVENT_OTHERAXIS; + break; + case ABS_MT_TOUCH_MINOR: + t->minor = e->value; + t->dirty = true; + tp->queued |= TOUCHPAD_EVENT_OTHERAXIS; + break; } } @@ -735,6 +745,30 @@ tp_palm_detect_multifinger(struct tp_dispatch *tp, struct tp_touch *t, uint64_t return false; } +static inline bool +tp_palm_detect_touch_size_triggered(struct tp_dispatch *tp, + struct tp_touch *t, + uint64_t time) +{ + if (!tp->palm.use_size) + return false; + + /* If a finger size is large enough for palm, we stick with that and + * force the user to release and reset the finger */ + if (t->palm.state != PALM_NONE && t->palm.state != PALM_TOUCH_SIZE) + return false; + + if (t->major > tp->palm.size_threshold || + t->minor > tp->palm.size_threshold) { + evdev_log_debug(tp->device, + "palm: touch size exceeded\n"); + t->palm.state = PALM_TOUCH_SIZE; + return true; + } + + return false; +} + static inline bool tp_palm_detect_edge(struct tp_dispatch *tp, struct tp_touch *t, @@ -818,6 +852,9 @@ tp_palm_detect(struct tp_dispatch *tp, struct tp_touch *t, uint64_t time) if (tp_palm_detect_tool_triggered(tp, t, time)) goto out; + if (tp_palm_detect_touch_size_triggered(tp, t, time)) + goto out; + if (tp_palm_detect_edge(tp, t, time)) goto out; @@ -852,6 +889,9 @@ out: case PALM_PRESSURE: palm_state = "pressure"; break; + case PALM_TOUCH_SIZE: + palm_state = "touch size"; + break; case PALM_NONE: default: abort(); @@ -1015,6 +1055,45 @@ tp_unhover_pressure(struct tp_dispatch *tp, uint64_t time) } } +static void +tp_unhover_size(struct tp_dispatch *tp, uint64_t time) +{ + struct tp_touch *t; + int low = tp->touch_size.low, + high = tp->touch_size.high; + int i; + + /* We require 5 slots for size handling, so we don't need to care + * about fake touches here */ + + for (i = 0; i < (int)tp->num_slots; i++) { + t = tp_get_touch(tp, i); + + if (t->state == TOUCH_NONE) + continue; + + if (!t->dirty) + continue; + + if (t->state == TOUCH_HOVERING) { + if ((t->major > high && t->minor > low) || + (t->major > low && t->minor > high)) { + evdev_log_debug(tp->device, + "touch-size: begin touch\n"); + /* avoid jumps when landing a finger */ + tp_motion_history_reset(t); + tp_begin_touch(tp, t, time); + } + } else { + if (t->major < low || t->minor < low) { + evdev_log_debug(tp->device, + "touch-size: end touch\n"); + tp_end_touch(tp, t, time); + } + } + } +} + static void tp_unhover_fake_touches(struct tp_dispatch *tp, uint64_t time) { @@ -1077,6 +1156,8 @@ tp_unhover_touches(struct tp_dispatch *tp, uint64_t time) { if (tp->pressure.use_pressure) tp_unhover_pressure(tp, time); + else if (tp->touch_size.use_touch_size) + tp_unhover_size(tp, time); else tp_unhover_fake_touches(tp, time); @@ -1512,6 +1593,15 @@ tp_sync_touch(struct tp_dispatch *tp, t->pressure = libevdev_get_event_value(evdev, EV_ABS, ABS_PRESSURE); + + libevdev_fetch_slot_value(evdev, + slot, + ABS_MT_TOUCH_MAJOR, + &t->major); + libevdev_fetch_slot_value(evdev, + slot, + ABS_MT_TOUCH_MINOR, + &t->minor); } static void @@ -2353,10 +2443,14 @@ tp_init_palmdetect_edge(struct tp_dispatch *tp, struct phys_coords mm = { 0.0, 0.0 }; struct device_coords edges; + if (device->tags & EVDEV_TAG_EXTERNAL_TOUCHPAD && + !tp_is_tpkb_combo_below(device)) + return; + evdev_device_get_size(device, &width, &height); - /* Enable palm detection on touchpads >= 70 mm. Anything smaller - probably won't need it, until we find out it does */ + /* Enable edge palm detection on touchpads >= 70 mm. Anything + smaller probably won't need it, until we find out it does */ if (width < 70.0) return; @@ -2413,6 +2507,33 @@ tp_init_palmdetect_pressure(struct tp_dispatch *tp, tp->palm.pressure_threshold); } +static inline void +tp_init_palmdetect_size(struct tp_dispatch *tp, + struct evdev_device *device) +{ + const char *prop; + int threshold; + + if (!tp->touch_size.use_touch_size) + return; + + prop = udev_device_get_property_value(device->udev_device, + "LIBINPUT_ATTR_PALM_SIZE_THRESHOLD"); + if (!prop) + return; + + threshold = parse_palm_size_property(prop); + if (threshold == 0) { + evdev_log_bug_client(device, + "palm: ignoring invalid threshold %s\n", + prop); + return; + } + + tp->palm.use_size = true; + tp->palm.size_threshold = threshold; +} + static void tp_init_palmdetect(struct tp_dispatch *tp, struct evdev_device *device) @@ -2435,6 +2556,7 @@ tp_init_palmdetect(struct tp_dispatch *tp, tp_init_palmdetect_edge(tp, device); tp_init_palmdetect_pressure(tp, device); + tp_init_palmdetect_size(tp, device); } static void @@ -2612,7 +2734,7 @@ tp_init_pressure(struct tp_dispatch *tp, prop = udev_device_get_property_value(device->udev_device, "LIBINPUT_ATTR_PRESSURE_RANGE"); if (prop) { - if (!parse_pressure_range_property(prop, &hi, &lo)) { + if (!parse_range_property(prop, &hi, &lo)) { evdev_log_bug_client(device, "discarding invalid pressure range '%s'\n", prop); @@ -2648,10 +2770,59 @@ tp_init_pressure(struct tp_dispatch *tp, "using pressure-based touch detection\n"); } +static bool +tp_init_touch_size(struct tp_dispatch *tp, + struct evdev_device *device) +{ + const char *prop; + int lo, hi; + + if (!libevdev_has_event_code(device->evdev, + EV_ABS, + ABS_MT_TOUCH_MAJOR)) { + return false; + } + + if (libevdev_get_num_slots(device->evdev) < 5) { + evdev_log_bug_libinput(device, + "Expected 5+ slots for touch size detection\n"); + return false; + } + + prop = udev_device_get_property_value(device->udev_device, + "LIBINPUT_ATTR_TOUCH_SIZE_RANGE"); + if (!prop) + return false; + + if (!parse_range_property(prop, &hi, &lo)) { + evdev_log_bug_client(device, + "discarding invalid touch size range '%s'\n", + prop); + return false; + } + + if (hi == 0 && lo == 0) { + evdev_log_info(device, + "touch size based touch detection disabled\n"); + return false; + } + + /* Thresholds apply for both major or minor */ + tp->touch_size.low = lo; + tp->touch_size.high = hi; + tp->touch_size.use_touch_size = true; + + evdev_log_debug(device, "using size-based touch detection\n"); + + return true; +} + static int tp_init(struct tp_dispatch *tp, struct evdev_device *device) { + bool use_touch_size = false; + tp->base.dispatch_type = DISPATCH_TOUCHPAD; tp->base.interface = &tp_interface; tp->device = device; @@ -2666,7 +2837,11 @@ tp_init(struct tp_dispatch *tp, evdev_device_init_abs_range_warnings(device); - tp_init_pressure(tp, device); + if (device->model_flags & EVDEV_MODEL_APPLE_TOUCHPAD) + use_touch_size = tp_init_touch_size(tp, device); + + if (!use_touch_size) + tp_init_pressure(tp, device); /* Set the dpi to that of the x axis, because that's what we normalize to when needed*/ diff --git a/src/evdev-mt-touchpad.h b/src/evdev-mt-touchpad.h index d601f7e5..664514de 100644 --- a/src/evdev-mt-touchpad.h +++ b/src/evdev-mt-touchpad.h @@ -59,6 +59,7 @@ enum touch_palm_state { PALM_TRACKPOINT, PALM_TOOL_PALM, PALM_PRESSURE, + PALM_TOUCH_SIZE, }; enum button_event { @@ -148,6 +149,7 @@ struct tp_touch { uint64_t time; int pressure; bool is_tool_palm; /* MT_TOOL_PALM */ + int major, minor; bool was_down; /* if distance == 0, false for pure hovering touches */ @@ -248,6 +250,17 @@ struct tp_dispatch { int low; } pressure; + /* If touch size (either axis) goes above high -> touch down, + if touch size (either axis) goes below low -> touch up */ + struct { + bool use_touch_size; + int high; + int low; + + /* convert device units to angle */ + double orientation_to_angle; + } touch_size; + struct device_coords hysteresis_margin; struct { @@ -348,6 +361,9 @@ struct tp_dispatch { bool use_pressure; int pressure_threshold; + + bool use_size; + int size_threshold; } palm; struct { diff --git a/src/evdev.c b/src/evdev.c index 0d861190..24bfad07 100644 --- a/src/evdev.c +++ b/src/evdev.c @@ -1892,7 +1892,7 @@ evdev_process_event(struct evdev_device *device, struct input_event *e) struct evdev_dispatch *dispatch = device->dispatch; uint64_t time = tv2us(&e->time); -#if 0 +#if 1 if (libevdev_event_is_code(e, EV_SYN, SYN_REPORT)) evdev_log_debug(device, "-------------- EV_SYN ------------\n"); diff --git a/src/libinput-util.c b/src/libinput-util.c index ac7a5d54..c7cb2b48 100644 --- a/src/libinput-util.c +++ b/src/libinput-util.c @@ -379,7 +379,7 @@ parse_tpkbcombo_layout_poperty(const char *prop, * @return true on success, false otherwise */ bool -parse_pressure_range_property(const char *prop, int *hi, int *lo) +parse_range_property(const char *prop, int *hi, int *lo) { int first, second; @@ -431,6 +431,31 @@ parse_palm_pressure_property(const char *prop) return threshold; } +/** + * Helper function to parse the LIBINPUT_ATTR_PALM_SIZE_THRESHOLD property + * from udev. Property is of the form: + * LIBINPUT_ATTR_PALM_SIZE_THRESHOLD= + * Where the number indicates the minimum threshold to consider a touch to + * be a palm. + * + * @param prop The value of the udev property (without the + * LIBINPUT_ATTR_PALM_SIZE_THRESHOLD=) + * @return The pressure threshold or 0 on error + */ +int +parse_palm_size_property(const char *prop) +{ + int thr = 0; + + if (!prop) + return 0; + + if (!safe_atoi(prop, &thr) || thr < 0 || thr > 1024) + return 0; + + return thr; +} + /** * Return the next word in a string pointed to by state before the first * separator character. Call repeatedly to tokenize a whole string. diff --git a/src/libinput-util.h b/src/libinput-util.h index 26f0b632..d4d68abe 100644 --- a/src/libinput-util.h +++ b/src/libinput-util.h @@ -413,8 +413,9 @@ int parse_mouse_wheel_click_count_property(const char *prop); double parse_trackpoint_accel_property(const char *prop); bool parse_dimension_property(const char *prop, size_t *width, size_t *height); bool parse_calibration_property(const char *prop, float calibration[6]); -bool parse_pressure_range_property(const char *prop, int *hi, int *lo); +bool parse_range_property(const char *prop, int *hi, int *lo); int parse_palm_pressure_property(const char *prop); +int parse_palm_size_property(const char *prop); enum tpkbcombo_layout { TPKBCOMBO_LAYOUT_UNKNOWN, diff --git a/test/litest-device-bcm5974.c b/test/litest-device-bcm5974.c index 7d872853..1e6a002f 100644 --- a/test/litest-device-bcm5974.c +++ b/test/litest-device-bcm5974.c @@ -40,6 +40,9 @@ static struct input_event down[] = { { .type = EV_ABS, .code = ABS_MT_TRACKING_ID, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_POSITION_X, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_POSITION_Y, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_ORIENTATION, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MAJOR, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MINOR, .value = LITEST_AUTO_ASSIGN }, { .type = EV_SYN, .code = SYN_REPORT, .value = 0 }, { .type = -1, .code = -1 }, }; @@ -51,6 +54,9 @@ static struct input_event move[] = { { .type = EV_ABS, .code = ABS_PRESSURE, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_POSITION_X, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_POSITION_Y, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_ORIENTATION, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MAJOR, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MINOR, .value = LITEST_AUTO_ASSIGN }, { .type = EV_SYN, .code = SYN_REPORT, .value = 0 }, { .type = -1, .code = -1 }, }; @@ -63,6 +69,12 @@ get_axis_default(struct litest_device *d, unsigned int evcode, int32_t *value) case ABS_MT_PRESSURE: *value = 30; return 0; + case ABS_MT_TOUCH_MAJOR: + case ABS_MT_TOUCH_MINOR: + *value = 200; + return 0; + case ABS_MT_ORIENTATION: + return 0; } return 1; } diff --git a/test/litest-device-magic-trackpad.c b/test/litest-device-magic-trackpad.c index cc371633..56d42344 100644 --- a/test/litest-device-magic-trackpad.c +++ b/test/litest-device-magic-trackpad.c @@ -37,10 +37,11 @@ static struct input_event down[] = { { .type = EV_ABS, .code = ABS_Y, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_SLOT, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_TRACKING_ID, .value = LITEST_AUTO_ASSIGN }, - { .type = EV_ABS, .code = ABS_MT_TOUCH_MAJOR, .value = 272 }, - { .type = EV_ABS, .code = ABS_MT_TOUCH_MINOR, .value = 400 }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MAJOR, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MINOR, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_POSITION_X, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_POSITION_Y, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_ORIENTATION, .value = LITEST_AUTO_ASSIGN }, { .type = EV_SYN, .code = SYN_REPORT, .value = 0 }, { .type = -1, .code = -1 }, }; @@ -49,17 +50,34 @@ static struct input_event move[] = { { .type = EV_ABS, .code = ABS_MT_SLOT, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_X, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_Y, .value = LITEST_AUTO_ASSIGN }, - { .type = EV_ABS, .code = ABS_MT_TOUCH_MAJOR, .value = 272 }, - { .type = EV_ABS, .code = ABS_MT_TOUCH_MINOR, .value = 400 }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MAJOR, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MINOR, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_POSITION_X, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_POSITION_Y, .value = LITEST_AUTO_ASSIGN }, + { .type = EV_ABS, .code = ABS_MT_ORIENTATION, .value = LITEST_AUTO_ASSIGN }, { .type = EV_SYN, .code = SYN_REPORT, .value = 0 }, { .type = -1, .code = -1 }, }; +static int +get_axis_default(struct litest_device *d, unsigned int evcode, int32_t *value) +{ + switch (evcode) { + case ABS_MT_TOUCH_MAJOR: + case ABS_MT_TOUCH_MINOR: + *value = 200; + return 0; + case ABS_MT_ORIENTATION: + return 0; + } + return 1; +} + static struct litest_device_interface interface = { .touch_down_events = down, .touch_move_events = move, + + .get_axis_default = get_axis_default, }; static struct input_absinfo absinfo[] = { diff --git a/test/litest.c b/test/litest.c index 82fbf693..f3f71448 100644 --- a/test/litest.c +++ b/test/litest.c @@ -1642,6 +1642,8 @@ touch_up(struct litest_device *d, unsigned int slot) { .type = EV_ABS, .code = ABS_MT_SLOT, .value = LITEST_AUTO_ASSIGN }, { .type = EV_ABS, .code = ABS_MT_TRACKING_ID, .value = -1 }, { .type = EV_ABS, .code = ABS_MT_PRESSURE, .value = 0 }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MAJOR, .value = 0 }, + { .type = EV_ABS, .code = ABS_MT_TOUCH_MINOR, .value = 0 }, { .type = EV_SYN, .code = SYN_REPORT, .value = 0 }, { .type = -1, .code = -1 } }; @@ -2171,7 +2173,11 @@ litest_scale_axis(const struct litest_device *d, const struct input_absinfo *abs; litest_assert_double_ge(val, 0.0); - litest_assert_double_le(val, 100.0); + /* major/minor must be able to beyond 100% for large fingers */ + if (axis != ABS_MT_TOUCH_MAJOR && + axis != ABS_MT_TOUCH_MINOR) { + litest_assert_double_le(val, 100.0); + } abs = libevdev_get_abs_info(d->evdev, axis); litest_assert_notnull(abs); @@ -2192,8 +2198,12 @@ int litest_scale(const struct litest_device *d, unsigned int axis, double val) { int min, max; + litest_assert_double_ge(val, 0.0); - litest_assert_double_le(val, 100.0); + /* major/minor must be able to beyond 100% for large fingers */ + if (axis != ABS_MT_TOUCH_MAJOR && + axis != ABS_MT_TOUCH_MINOR) + litest_assert_double_le(val, 100.0); if (axis <= ABS_Y) { min = d->interface->min[axis]; diff --git a/test/litest.h b/test/litest.h index a2cb6ee3..7086804e 100644 --- a/test/litest.h +++ b/test/litest.h @@ -33,6 +33,8 @@ #include #include +#include "libinput-util.h" + extern void litest_setup_tests_udev(void); extern void litest_setup_tests_path(void); extern void litest_setup_tests_pointer(void); @@ -303,12 +305,12 @@ struct axis_replacement { double value; }; +/** + * Same as litest_axis_set_value but allows for ranges outside 0..100% + */ static inline void -litest_axis_set_value(struct axis_replacement *axes, int code, double value) +litest_axis_set_value_unchecked(struct axis_replacement *axes, int code, double value) { - litest_assert_double_ge(value, 0.0); - litest_assert_double_le(value, 100.0); - while (axes->evcode != -1) { if (axes->evcode == code) { axes->value = value; @@ -320,6 +322,18 @@ litest_axis_set_value(struct axis_replacement *axes, int code, double value) litest_abort_msg("Missing axis code %d\n", code); } +/** + * Takes a value in percent and sets the given axis to that code. + */ +static inline void +litest_axis_set_value(struct axis_replacement *axes, int code, double value) +{ + litest_assert_double_ge(value, 0.0); + litest_assert_double_le(value, 100.0); + + litest_axis_set_value_unchecked(axes, code, value); +} + /* A loop range, resolves to: for (i = lower; i < upper; i++) */ @@ -903,6 +917,22 @@ litest_disable_middleemu(struct litest_device *dev) litest_assert_int_eq(status, expected); } +static inline bool +litest_touchpad_is_external(struct litest_device *dev) +{ + struct udev_device *udev_device; + const char *prop; + bool is_external; + + udev_device = libinput_device_get_udev_device(dev->libinput_device); + prop = udev_device_get_property_value(udev_device, + "ID_INPUT_TOUCHPAD_INTEGRATION"); + is_external = prop && streq(prop, "external"); + udev_device_unref(udev_device); + + return is_external; +} + #undef ck_assert_double_eq #undef ck_assert_double_ne #undef ck_assert_double_lt diff --git a/test/test-device.c b/test/test-device.c index 018b194f..c4dfdd47 100644 --- a/test/test-device.c +++ b/test/test-device.c @@ -61,22 +61,6 @@ START_TEST(device_sendevents_config_invalid) } END_TEST -static inline bool -touchpad_is_external(struct litest_device *dev) -{ - struct udev_device *udev_device; - const char *prop; - bool is_external; - - udev_device = libinput_device_get_udev_device(dev->libinput_device); - prop = udev_device_get_property_value(udev_device, - "ID_INPUT_TOUCHPAD_INTEGRATION"); - is_external = prop && streq(prop, "external"); - udev_device_unref(udev_device); - - return is_external; -} - START_TEST(device_sendevents_config_touchpad) { struct litest_device *dev = litest_current_device(); @@ -87,7 +71,7 @@ START_TEST(device_sendevents_config_touchpad) /* The wacom devices in the test suite are external */ if (libevdev_get_id_vendor(dev->evdev) != VENDOR_ID_WACOM && - !touchpad_is_external(dev)) + !litest_touchpad_is_external(dev)) expected |= LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE; @@ -107,7 +91,7 @@ START_TEST(device_sendevents_config_touchpad_superset) /* The wacom devices in the test suite are external */ if (libevdev_get_id_vendor(dev->evdev) == VENDOR_ID_WACOM || - touchpad_is_external(dev)) + litest_touchpad_is_external(dev)) return; device = dev->libinput_device; diff --git a/test/test-misc.c b/test/test-misc.c index 6a9760f0..2173910b 100644 --- a/test/test-misc.c +++ b/test/test-misc.c @@ -1002,15 +1002,15 @@ START_TEST(calibration_prop_parser) } END_TEST -struct parser_test_pressure_range { +struct parser_test_range { char *tag; bool success; int hi, lo; }; -START_TEST(pressure_range_prop_parser) +START_TEST(range_prop_parser) { - struct parser_test_pressure_range tests[] = { + struct parser_test_range tests[] = { { "10:8", true, 10, 8 }, { "100:-1", true, 100, -1 }, { "-203813:-502023", true, -203813, -502023 }, @@ -1028,7 +1028,7 @@ START_TEST(pressure_range_prop_parser) for (i = 0; tests[i].tag != NULL; i++) { hi = lo = 0xad; - success = parse_pressure_range_property(tests[i].tag, &hi, &lo); + success = parse_range_property(tests[i].tag, &hi, &lo); ck_assert(success == tests[i].success); if (success) { ck_assert_int_eq(hi, tests[i].hi); @@ -1039,7 +1039,7 @@ START_TEST(pressure_range_prop_parser) } } - success = parse_pressure_range_property(NULL, NULL, NULL); + success = parse_range_property(NULL, NULL, NULL); ck_assert(success == false); } END_TEST @@ -1373,7 +1373,7 @@ litest_setup_tests_misc(void) litest_add_no_device("misc:parser", dimension_prop_parser); litest_add_no_device("misc:parser", reliability_prop_parser); litest_add_no_device("misc:parser", calibration_prop_parser); - litest_add_no_device("misc:parser", pressure_range_prop_parser); + litest_add_no_device("misc:parser", range_prop_parser); litest_add_no_device("misc:parser", palm_pressure_parser); litest_add_no_device("misc:parser", safe_atoi_test); litest_add_no_device("misc:parser", safe_atod_test); diff --git a/test/test-touchpad.c b/test/test-touchpad.c index ffb214ec..5e029a45 100644 --- a/test/test-touchpad.c +++ b/test/test-touchpad.c @@ -5273,6 +5273,144 @@ START_TEST(touchpad_pressure_tap_2fg_1fg_light) } END_TEST +static inline bool +touchpad_has_touch_size(struct litest_device *dev) +{ + struct libevdev *evdev = dev->evdev; + + if (!libevdev_has_event_code(evdev, EV_ABS, ABS_MT_TOUCH_MAJOR)) + return false; + + if (libevdev_get_id_vendor(evdev) == VENDOR_ID_APPLE) + return true; + + return false; +} + +START_TEST(touchpad_touch_size) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_MT_TOUCH_MAJOR, 0 }, + { ABS_MT_TOUCH_MINOR, 0 }, + { ABS_MT_ORIENTATION, 0 }, + { -1, 0 } + }; + + if (!touchpad_has_touch_size(dev)) + return; + + litest_drain_events(li); + + litest_axis_set_value(axes, ABS_MT_TOUCH_MAJOR, 1); + litest_axis_set_value(axes, ABS_MT_TOUCH_MINOR, 1); + litest_touch_down_extended(dev, 0, 50, 50, axes); + litest_touch_move_to_extended(dev, 0, + 50, 50, + 80, 80, + axes, 10, 1); + litest_touch_up(dev, 0); + litest_assert_empty_queue(li); + + litest_axis_set_value(axes, ABS_MT_TOUCH_MAJOR, 15); + litest_axis_set_value(axes, ABS_MT_TOUCH_MINOR, 15); + litest_touch_down_extended(dev, 0, 50, 50, axes); + litest_touch_move_to_extended(dev, 0, + 50, 50, + 80, 80, + axes, 10, 1); + litest_touch_up(dev, 0); + litest_assert_only_typed_events(li, + LIBINPUT_EVENT_POINTER_MOTION); +} +END_TEST + +START_TEST(touchpad_touch_size_2fg) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_MT_TOUCH_MAJOR, 0 }, + { ABS_MT_TOUCH_MINOR, 0 }, + { ABS_MT_ORIENTATION, 0 }, + { -1, 0 } + }; + + if (!touchpad_has_touch_size(dev)) + return; + + litest_drain_events(li); + litest_axis_set_value(axes, ABS_MT_TOUCH_MAJOR, 15); + litest_axis_set_value(axes, ABS_MT_TOUCH_MINOR, 15); + litest_touch_down_extended(dev, 0, 50, 50, axes); + litest_touch_move_to_extended(dev, 0, + 50, 50, + 80, 80, + axes, 10, 1); + + litest_assert_only_typed_events(li, + LIBINPUT_EVENT_POINTER_MOTION); + + litest_axis_set_value(axes, ABS_MT_TOUCH_MAJOR, 1); + litest_axis_set_value(axes, ABS_MT_TOUCH_MINOR, 1); + litest_touch_down_extended(dev, 1, 70, 70, axes); + litest_touch_move_to_extended(dev, 1, + 70, 70, + 80, 90, + axes, 10, 1); + litest_assert_empty_queue(li); + + litest_axis_set_value(axes, ABS_MT_TOUCH_MAJOR, 15); + litest_axis_set_value(axes, ABS_MT_TOUCH_MINOR, 15); + litest_touch_move_to_extended(dev, 0, + 80, 80, + 50, 50, + axes, 10, 1); + + litest_assert_only_typed_events(li, + LIBINPUT_EVENT_POINTER_MOTION); + + litest_touch_up(dev, 1); + litest_touch_up(dev, 0); +} +END_TEST + +START_TEST(touchpad_palm_detect_touch_size) +{ + struct litest_device *dev = litest_current_device(); + struct libinput *li = dev->libinput; + struct axis_replacement axes[] = { + { ABS_MT_TOUCH_MAJOR, 0 }, + { ABS_MT_TOUCH_MINOR, 0 }, + { -1, 0 } + }; + + if (!touchpad_has_touch_size(dev) || + litest_touchpad_is_external(dev)) + return; + + litest_drain_events(li); + litest_axis_set_value(axes, ABS_MT_TOUCH_MAJOR, 30); + litest_axis_set_value(axes, ABS_MT_TOUCH_MINOR, 30); + litest_touch_down_extended(dev, 0, 50, 50, axes); + litest_touch_move_to_extended(dev, 0, + 50, 50, + 80, 80, + axes, 10, 1); + litest_assert_only_typed_events(li, + LIBINPUT_EVENT_POINTER_MOTION); + + litest_axis_set_value_unchecked(axes, ABS_MT_TOUCH_MAJOR, 90); + litest_axis_set_value(axes, ABS_MT_TOUCH_MINOR, 90); + litest_touch_move_to_extended(dev, 0, + 80, 80, + 50, 50, + axes, 10, 1); + litest_assert_empty_queue(li); +} +END_TEST + void litest_setup_tests_touchpad(void) { @@ -5327,6 +5465,7 @@ litest_setup_tests_touchpad(void) litest_add("touchpad:palm", touchpad_palm_detect_tool_palm, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); litest_add("touchpad:palm", touchpad_palm_detect_tool_palm_on_off, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); litest_add("touchpad:palm", touchpad_palm_detect_tool_palm_tap, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); + litest_add("touchpad:palm", touchpad_palm_detect_touch_size, LITEST_APPLE_CLICKPAD, LITEST_ANY); litest_add("touchpad:palm", touchpad_palm_detect_pressure, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); litest_add("touchpad:palm", touchpad_palm_detect_pressure_late, LITEST_TOUCHPAD, LITEST_SINGLE_TOUCH); @@ -5434,4 +5573,7 @@ litest_setup_tests_touchpad(void) litest_add("touchpad:pressure", touchpad_pressure_tap, LITEST_TOUCHPAD, LITEST_ANY); litest_add("touchpad:pressure", touchpad_pressure_tap_2fg, LITEST_TOUCHPAD, LITEST_ANY); litest_add("touchpad:pressure", touchpad_pressure_tap_2fg_1fg_light, LITEST_TOUCHPAD, LITEST_ANY); + + litest_add("touchpad:touch-size", touchpad_touch_size, LITEST_APPLE_CLICKPAD, LITEST_ANY); + litest_add("touchpad:touch-size", touchpad_touch_size_2fg, LITEST_APPLE_CLICKPAD, LITEST_ANY); } diff --git a/tools/libinput-measure-touch-size b/tools/libinput-measure-touch-size new file mode 100755 index 00000000..66c7571e --- /dev/null +++ b/tools/libinput-measure-touch-size @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +# vim: set expandtab shiftwidth=4: +# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */ +# +# Copyright © 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import sys +import argparse +import evdev +import evdev.ecodes +import pyudev + + +class Range(object): + """Class to keep a min/max of a value around""" + def __init__(self): + self.min = float('inf') + self.max = float('-inf') + + def update(self, value): + self.min = min(self.min, value) + self.max = max(self.max, value) + + +class Touch(object): + """A single data point of a sequence (i.e. one event frame)""" + + def __init__(self, major=None, minor=None, orientation=None): + self._major = major + self._minor = minor + self._orientation = orientation + self.dirty = False + + @property + def major(self): + return self._major + + @major.setter + def major(self, major): + self._major = major + self.dirty = True + + @property + def minor(self): + return self._minor + + @minor.setter + def minor(self, minor): + self._minor = minor + self.dirty = True + + @property + def orientation(self): + return self._orientation + + @orientation.setter + def orientation(self, orientation): + self._orientation = orientation + self.dirty = True + + def __str__(self): + s = "Touch: major {:3d}".format(self.major) + if self.minor is not None: + s += ", minor {:3d}".format(self.minor) + if self.orientation is not None: + s += ", orientation {:+3d}".format(self.orientation) + return s + + +class TouchSequence(object): + """A touch sequence from beginning to end""" + + def __init__(self, device, tracking_id): + self.device = device + self.tracking_id = tracking_id + self.points = [] + + self.is_active = True + + self.is_down = False + self.was_down = False + self.is_palm = False + self.was_palm = False + + self.major_range = Range() + self.minor_range = Range() + + def append(self, touch): + """Add a Touch to the sequence""" + self.points.append(touch) + self.major_range.update(touch.major) + self.minor_range.update(touch.minor) + + if touch.major < self.device.up or \ + touch.minor < self.device.up: + self.is_down = False + elif touch.major > self.device.down or \ + touch.minor > self.device.down: + self.is_down = True + self.was_down = True + + self.is_palm = touch.major > self.device.palm + if self.is_palm: + self.was_palm = True + + def finalize(self): + """Mark the TouchSequence as complete (finger is up)""" + self.is_active = False + + def avg(self): + """Average pressure value of this sequence""" + return int(sum([p.pressure for p in self.points])/len(self.points)) + + def median(self): + """Median pressure value of this sequence""" + ps = sorted([p.pressure for p in self.points]) + idx = int(len(self.points)/2) + return ps[idx] + + def __str__(self): + return self._str_state() if self.is_active else self._str_summary() + + def _str_summary(self): + if not self.points: + return "Sequence: no major/minor values recorded" + + s = "Sequence: major: [{:3d}..{:3d}] ".format( + self.major_range.min, self.major_range.max + ) + if self.device.has_minor: + s += "minor: [{:3d}..{:3d}] ".format( + self.minor_range.min, self.minor_range.max + ) + if self.was_down: + s += " down" + if self.was_palm: + s += " palm" + + return s + + def _str_state(self): + touch = self.points[-1] + s = "{}, tags: {} {}".format( + touch, + "down" if self.is_down else " ", + "palm" if self.is_palm else " " + ) + return s + + +class InvalidDeviceError(Exception): + pass + + +class Device(object): + def __init__(self, path): + if path is None: + self.path = self.find_touch_device() + else: + self.path = path + + self.device = evdev.InputDevice(self.path) + # capabilities returns a dict with the EV_* codes as key, + # each of which is a list of tuples of (code, AbsInfo) + # + # Get the abs list first (or empty list if missing), + # then extract the pressure absinfo from that + caps = self.device.capabilities(absinfo=True).get( + evdev.ecodes.EV_ABS, [] + ) + codes = [cap[0] for cap in caps] + + if evdev.ecodes.ABS_MT_TOUCH_MAJOR not in codes: + raise InvalidDeviceError("device does not have ABS_MT_TOUCH_MAJOR") + + self.has_minor = evdev.ecodes.ABS_MT_TOUCH_MINOR in codes + self.has_orientation = evdev.ecodes.ABS_MT_ORIENTATION in codes + + self.up = 0 + self.down = 0 + self.palm = 0 + + self._init_thresholds_from_udev() + self.sequences = [] + self.touch = Touch(0, 0) + + def find_touch_device(self): + context = pyudev.Context() + for device in context.list_devices(subsystem='input'): + if not device.get('ID_INPUT_TOUCHPAD', 0) and \ + not device.get('ID_INPUT_TOUCHSCREEN', 0): + continue + + if not device.device_node or \ + not device.device_node.startswith('/dev/input/event'): + continue + + return device.device_node + + print("Unable to find a touch device.", file=sys.stderr) + sys.exit(1) + + def _init_thresholds_from_udev(self): + context = pyudev.Context() + ud = pyudev.Devices.from_device_file(context, self.path) + v = ud.get('LIBINPUT_ATTR_TOUCH_SIZE_RANGE') + if v: + self.up, self.down = colon_tuple(v) + + v = ud.get('LIBINPUT_ATTR_PALM_SIZE_THRESHOLD') + if v: + self.palm = int(v) + + def start_new_sequence(self, tracking_id): + self.sequences.append(TouchSequence(self, tracking_id)) + + def current_sequence(self): + return self.sequences[-1] + + def handle_key(self, event): + tapcodes = [evdev.ecodes.BTN_TOOL_DOUBLETAP, + evdev.ecodes.BTN_TOOL_TRIPLETAP, + evdev.ecodes.BTN_TOOL_QUADTAP, + evdev.ecodes.BTN_TOOL_QUINTTAP] + if event.code in tapcodes and event.value > 0: + print("\rThis tool cannot handle multiple fingers, " + "output will be invalid", file=sys.stderr) + + def handle_abs(self, event): + if event.code == evdev.ecodes.ABS_MT_TRACKING_ID: + if event.value > -1: + self.start_new_sequence(event.value) + else: + s = self.current_sequence() + s.finalize() + print("\r{}".format(s)) + elif event.code == evdev.ecodes.ABS_MT_TOUCH_MAJOR: + self.touch.major = event.value + elif event.code == evdev.ecodes.ABS_MT_TOUCH_MINOR: + self.touch.minor = event.value + elif event.code == evdev.ecodes.ABS_MT_ORIENTATION: + self.touch.orientation = event.value + + def handle_syn(self, event): + if self.touch.dirty: + self.current_sequence().append(self.touch) + print("\r{}".format(self.current_sequence()), end="") + self.touch = Touch(major=self.touch.major, + minor=self.touch.minor, + orientation=self.touch.orientation) + + def handle_event(self, event): + if event.type == evdev.ecodes.EV_ABS: + self.handle_abs(event) + elif event.type == evdev.ecodes.EV_KEY: + self.handle_key(event) + elif event.type == evdev.ecodes.EV_SYN: + self.handle_syn(event) + + def read_events(self): + print("Ready for recording data.") + print("Touch sizes used: {}:{}".format(self.down, self.up)) + print("Palm size used: {}".format(self.palm)) + print("Place a single finger on the device to measure touch size.\n" + "Ctrl+C to exit\n") + + for event in self.device.read_loop(): + self.handle_event(event) + + +def colon_tuple(string): + try: + ts = string.split(':') + t = tuple([int(x) for x in ts]) + if len(t) == 2 and t[0] >= t[1]: + return t + except: + pass + + msg = "{} is not in format N:M (N >= M)".format(string) + raise argparse.ArgumentTypeError(msg) + + +def main(args): + parser = argparse.ArgumentParser( + description="Measure touch size and orientation" + ) + parser.add_argument('path', metavar='/dev/input/event0', + nargs='?', type=str, help='Path to device (optional)') + parser.add_argument('--touch-thresholds', metavar='down:up', + type=colon_tuple, + help='Thresholds when a touch is logically down or up') + parser.add_argument('--palm-threshold', metavar='t', + type=int, help='Threshold when a touch is a palm') + args = parser.parse_args() + + try: + device = Device(args.path) + + if args.touch_thresholds is not None: + device.down, device.up = args.touch_thresholds + + if args.palm_threshold is not None: + device.palm = args.palm_threshold + + device.read_events() + except KeyboardInterrupt: + pass + except (PermissionError, OSError): + print("Error: failed to open device") + except InvalidDeviceError as e: + print("Error: {}".format(e)) + +if __name__ == "__main__": + main(sys.argv) diff --git a/tools/libinput-measure-touch-size.man b/tools/libinput-measure-touch-size.man new file mode 100644 index 00000000..f33f13c1 --- /dev/null +++ b/tools/libinput-measure-touch-size.man @@ -0,0 +1,57 @@ +.TH libinput-measure-touch-size "1" +.SH NAME +libinput\-measure\-touch-size \- measure touch size and orientation of devices +.SH SYNOPSIS +.B libinput measure touch\-size [\-\-help] [options] +[\fI/dev/input/event0\fI] +.SH DESCRIPTION +.PP +The +.B "libinput measure touch\-size" +tool measures the size and orientation of a touch as provided by the kernel. +an interactive tool. When executed, the tool will prompt the user to +interact with the touch device. On termination, the tool prints a summary of the +values seen. This data should be attached to any +touch\-size\-related bug report. +.PP +This is a debugging tool only, its output may change at any time. Do not +rely on the output. +.PP +This tool usually needs to be run as root to have access to the +/dev/input/eventX nodes. +.SH OPTIONS +If a device node is given, this tool opens that device node. Otherwise, this +tool searches for the first node that looks like a touch-capable device and +uses that node. +.TP 8 +.B \-\-help +Print help +.TP 8 +.B \-\-touch\-thresholds=\fI"down:up"\fR +Set the logical touch size thresholds to +.I down +and +.I up, +respectively. When a touch exceeds the size in +.I down +it is considered logically down. If a touch is logically down and goes below +the size in +.I up, +it is considered logically up. The thresholds have to be in +device-specific pressure values and it is required that +.I down +>= +.I up. +.TP 8 +.B \-\-palm\-threshold=\fIN\fR +Assume a palm threshold of +.I N. +The threshold has to be in device-specific pressure values. +.PP +If the touch-thresholds or the palm-threshold are not provided, +this tool uses the thresholds provided by the udev hwdb (if any) or the +built-in defaults. +.SH LIBINPUT +Part of the +.B libinput(1) +suite diff --git a/tools/libinput-measure.man b/tools/libinput-measure.man index d91afdd0..703900b7 100644 --- a/tools/libinput-measure.man +++ b/tools/libinput-measure.man @@ -22,6 +22,9 @@ Print help .SH FEATURES Features that can be measured include .TP 8 +.B libinput\-measure\-touch\-size(1) +Measure touch size and orientation +.TP 8 .B libinput\-measure\-touchpad\-tap\-time(1) Measure tap-to-click time .TP 8 diff --git a/tools/libinput.man b/tools/libinput.man index fef7cdca..16cf7bd7 100644 --- a/tools/libinput.man +++ b/tools/libinput.man @@ -45,6 +45,9 @@ List all devices recognized by libinput .B libinput\-measure(1) Measure various properties of devices .TP 8 +.B libinput\-measure\-touch\-size(1) +Measure touch size and orientation +.TP 8 .B libinput\-measure\-touchpad\-tap(1) Measure tap-to-click time .TP 8 diff --git a/udev/90-libinput-model-quirks.hwdb b/udev/90-libinput-model-quirks.hwdb index 3ec1ac1f..2754c5d5 100644 --- a/udev/90-libinput-model-quirks.hwdb +++ b/udev/90-libinput-model-quirks.hwdb @@ -50,6 +50,8 @@ libinput:touchpad:input:b0003v05ACp* libinput:touchpad:input:b0005v05ACp* LIBINPUT_MODEL_APPLE_TOUCHPAD=1 LIBINPUT_ATTR_SIZE_HINT=104x75 + LIBINPUT_ATTR_TOUCH_SIZE_RANGE=150:130 + LIBINPUT_ATTR_PALM_SIZE_THRESHOLD=800 libinput:name:*Apple Inc. Apple Internal Keyboard*:dmi:* LIBINPUT_ATTR_KEYBOARD_INTEGRATION=internal diff --git a/udev/parse_hwdb.py b/udev/parse_hwdb.py index 5de7eb48..f6b9ec0a 100755 --- a/udev/parse_hwdb.py +++ b/udev/parse_hwdb.py @@ -108,6 +108,7 @@ def property_grammar(): ('LIBINPUT_ATTR_SIZE_HINT', Group(dimension('SETTINGS*'))), ('LIBINPUT_ATTR_RESOLUTION_HINT', Group(dimension('SETTINGS*'))), ('LIBINPUT_ATTR_PRESSURE_RANGE', Group(crange('SETTINGS*'))), + ('LIBINPUT_ATTR_TOUCH_SIZE_RANGE', Group(crange('SETTINGS*'))), ('LIBINPUT_ATTR_TPKBCOMBO_LAYOUT', Or(('below'))), ('LIBINPUT_ATTR_LID_SWITCH_RELIABILITY', Or(('reliable', 'write_open'))), @@ -118,6 +119,7 @@ def property_grammar(): tprops = ( ('LIBINPUT_ATTR_PALM_PRESSURE_THRESHOLD', INTEGER('X')), + ('LIBINPUT_ATTR_PALM_SIZE_THRESHOLD', INTEGER('X')), ) typed_props = [Literal(name)('NAME') - Suppress('=') - val for name, val in tprops]