/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ /* * Copyright 2018 Red Hat, Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * Author: Carlos Garnacho */ #include "config.h" #ifdef HAVE_LIBGUDEV #include #endif #include "backends/meta-input-device-private.h" #include "meta-input-mapper-private.h" #include "meta-monitor-manager-private.h" #include "meta-logical-monitor.h" #include "meta-backend-private.h" #define MAX_SIZE_MATCH_DIFF 0.05 typedef struct _MetaMapperInputInfo MetaMapperInputInfo; typedef struct _MetaMapperOutputInfo MetaMapperOutputInfo; typedef struct _MappingHelper MappingHelper; typedef struct _DeviceCandidates DeviceCandidates; typedef struct _DeviceMatch DeviceMatch; struct _MetaInputMapper { GObject parent_instance; MetaMonitorManager *monitor_manager; ClutterSeat *seat; GHashTable *input_devices; /* ClutterInputDevice -> MetaMapperInputInfo */ GHashTable *output_devices; /* MetaLogicalMonitor -> MetaMapperOutputInfo */ #ifdef HAVE_LIBGUDEV GUdevClient *udev_client; #endif }; typedef enum { META_MATCH_EDID_VENDOR, /* EDID vendor match, eg. "WAC" for Wacom */ META_MATCH_EDID_PARTIAL, /* Partial EDID model match, eg. "Cintiq" */ META_MATCH_EDID_FULL, /* Full EDID model match, eg. "Cintiq 12WX" */ META_MATCH_SIZE, /* Size from input device and output match */ META_MATCH_IS_BUILTIN, /* Output is builtin, applies mainly to system-integrated devices */ META_MATCH_CONFIG, /* Specified by config */ N_OUTPUT_MATCHES } MetaOutputMatchType; struct _MetaMapperInputInfo { ClutterInputDevice *device; MetaInputMapper *mapper; MetaMapperOutputInfo *output; GSettings *settings; guint builtin : 1; }; struct _MetaMapperOutputInfo { MetaLogicalMonitor *logical_monitor; GList *input_devices; }; struct _MappingHelper { GArray *device_maps; }; struct _DeviceMatch { MetaMonitor *monitor; uint32_t score; }; struct _DeviceCandidates { MetaMapperInputInfo *input; GArray *matches; /* Array of DeviceMatch */ int best; }; enum { DEVICE_MAPPED, DEVICE_ENABLED, DEVICE_ASPECT_RATIO, N_SIGNALS }; static guint signals[N_SIGNALS] = { 0, }; static void mapper_output_info_remove_input (MetaMapperOutputInfo *output, MetaMapperInputInfo *input); static void mapper_recalculate_input (MetaInputMapper *mapper, MetaMapperInputInfo *input); G_DEFINE_TYPE (MetaInputMapper, meta_input_mapper, G_TYPE_OBJECT) static GSettings * get_device_settings (ClutterInputDevice *device) { const char *group, *schema, *vendor, *product; ClutterInputDeviceType type; GSettings *settings; char *path; type = clutter_input_device_get_device_type (device); if (type == CLUTTER_TOUCHSCREEN_DEVICE) { group = "touchscreens"; schema = "org.gnome.desktop.peripherals.touchscreen"; } else if (type == CLUTTER_TABLET_DEVICE || type == CLUTTER_PEN_DEVICE || type == CLUTTER_ERASER_DEVICE || type == CLUTTER_CURSOR_DEVICE || type == CLUTTER_PAD_DEVICE) { group = "tablets"; schema = "org.gnome.desktop.peripherals.tablet"; } else { return NULL; } vendor = clutter_input_device_get_vendor_id (device); product = clutter_input_device_get_product_id (device); path = g_strdup_printf ("/org/gnome/desktop/peripherals/%s/%s:%s/", group, vendor, product); settings = g_settings_new_with_path (schema, path); g_free (path); return settings; } static void settings_output_changed_cb (GSettings *settings, const char *key, MetaMapperInputInfo *info) { if (info->output != NULL) mapper_output_info_remove_input (info->output, info); mapper_recalculate_input (info->mapper, info); } static MetaMapperInputInfo * mapper_input_info_new (ClutterInputDevice *device, MetaInputMapper *mapper) { MetaMapperInputInfo *info; info = g_new0 (MetaMapperInputInfo, 1); info->mapper = mapper; info->device = device; info->settings = get_device_settings (device); g_signal_connect (info->settings, "changed::output", G_CALLBACK (settings_output_changed_cb), info); return info; } static void mapper_input_info_free (MetaMapperInputInfo *info) { g_signal_handlers_disconnect_by_func (info->settings, settings_output_changed_cb, info); g_object_unref (info->settings); g_free (info); } static MetaMapperOutputInfo * mapper_output_info_new (MetaLogicalMonitor *logical_monitor) { MetaMapperOutputInfo *info; info = g_new0 (MetaMapperOutputInfo, 1); info->logical_monitor = logical_monitor; return info; } static void mapper_output_info_free (MetaMapperOutputInfo *info) { g_free (info); } static void mapper_input_info_set_output (MetaMapperInputInfo *input, MetaMapperOutputInfo *output, MetaMonitor *monitor) { MetaInputMapper *mapper = input->mapper; float matrix[6] = { 1, 0, 0, 0, 1, 0 }; double aspect_ratio; int width, height; if (input->output == output) return; input->output = output; /* These devices don't require emission about mapping/ratio */ if (clutter_input_device_get_device_type (input->device) == CLUTTER_PAD_DEVICE) return; if (output && monitor) { meta_monitor_manager_get_monitor_matrix (mapper->monitor_manager, monitor, output->logical_monitor, matrix); meta_monitor_get_current_resolution (monitor, &width, &height); } else { meta_monitor_manager_get_screen_size (mapper->monitor_manager, &width, &height); } aspect_ratio = (double) width / height; g_signal_emit (input->mapper, signals[DEVICE_MAPPED], 0, input->device, matrix); g_signal_emit (input->mapper, signals[DEVICE_ASPECT_RATIO], 0, input->device, aspect_ratio); } static void mapper_output_info_add_input (MetaMapperOutputInfo *output, MetaMapperInputInfo *input, MetaMonitor *monitor) { g_assert (input->output == NULL); output->input_devices = g_list_prepend (output->input_devices, input); mapper_input_info_set_output (input, output, monitor); } static void mapper_output_info_remove_input (MetaMapperOutputInfo *output, MetaMapperInputInfo *input) { g_assert (input->output == output); output->input_devices = g_list_remove (output->input_devices, input); mapper_input_info_set_output (input, NULL, NULL); } static void mapper_output_info_clear_inputs (MetaMapperOutputInfo *output) { while (output->input_devices) { MetaMapperInputInfo *input = output->input_devices->data; mapper_input_info_set_output (input, NULL, NULL); output->input_devices = g_list_remove (output->input_devices, input); } } static void clear_candidates (DeviceCandidates *candidates) { g_clear_pointer (&candidates->matches, g_array_unref); } static void mapping_helper_init (MappingHelper *helper) { helper->device_maps = g_array_new (FALSE, FALSE, sizeof (DeviceCandidates)); g_array_set_clear_func (helper->device_maps, (GDestroyNotify) clear_candidates); } static void mapping_helper_release (MappingHelper *helper) { g_array_unref (helper->device_maps); } static gboolean match_edid (MetaMapperInputInfo *input, MetaMonitor *monitor, MetaOutputMatchType *match_type) { const gchar *dev_name; dev_name = clutter_input_device_get_device_name (input->device); if (strcasestr (dev_name, meta_monitor_get_vendor (monitor)) == NULL) return FALSE; *match_type = META_MATCH_EDID_VENDOR; if (strcasestr (dev_name, meta_monitor_get_product (monitor)) != NULL) { *match_type = META_MATCH_EDID_FULL; } else { int i; g_auto (GStrv) split = NULL; split = g_strsplit (meta_monitor_get_product (monitor), " ", -1); for (i = 0; split[i]; i++) { if (strcasestr (dev_name, split[i]) != NULL) { *match_type = META_MATCH_EDID_PARTIAL; break; } } } return TRUE; } static gboolean input_device_get_physical_size (MetaInputMapper *mapper, ClutterInputDevice *device, double *width, double *height) { #ifdef HAVE_LIBGUDEV g_autoptr (GUdevDevice) udev_device = NULL; const char *node; node = clutter_input_device_get_device_node (device); if (!node) return FALSE; udev_device = g_udev_client_query_by_device_file (mapper->udev_client, node); if (udev_device && g_udev_device_has_property (udev_device, "ID_INPUT_WIDTH_MM")) { *width = g_udev_device_get_property_as_double (udev_device, "ID_INPUT_WIDTH_MM"); *height = g_udev_device_get_property_as_double (udev_device, "ID_INPUT_HEIGHT_MM"); return TRUE; } #endif return FALSE; } static gboolean match_size (MetaMapperInputInfo *input, MetaMonitor *monitor) { double w_diff, h_diff; int o_width, o_height; double i_width, i_height; if (!input_device_get_physical_size (input->mapper, input->device, &i_width, &i_height)) return FALSE; meta_monitor_get_physical_dimensions (monitor, &o_width, &o_height); w_diff = ABS (1 - ((double) o_width / i_width)); h_diff = ABS (1 - ((double) o_height / i_height)); return w_diff < MAX_SIZE_MATCH_DIFF && h_diff < MAX_SIZE_MATCH_DIFF; } static gboolean match_builtin (MetaInputMapper *mapper, MetaMonitor *monitor) { return monitor == meta_monitor_manager_get_laptop_panel (mapper->monitor_manager); } static gboolean match_config (MetaMapperInputInfo *info, MetaMonitor *monitor) { gboolean match = FALSE; char **edid; guint n_values; edid = g_settings_get_strv (info->settings, "output"); n_values = g_strv_length (edid); if (n_values != 3) { g_warning ("EDID configuration for device '%s' " "is incorrect, must have 3 values", clutter_input_device_get_device_name (info->device)); goto out; } if (!*edid[0] && !*edid[1] && !*edid[2]) goto out; match = (g_strcmp0 (meta_monitor_get_vendor (monitor), edid[0]) == 0 && g_strcmp0 (meta_monitor_get_product (monitor), edid[1]) == 0 && g_strcmp0 (meta_monitor_get_serial (monitor), edid[2]) == 0); out: g_strfreev (edid); return match; } static int sort_by_score (DeviceMatch *match1, DeviceMatch *match2) { return (int) match2->score - match1->score; } static void guess_candidates (MetaInputMapper *mapper, MetaMapperInputInfo *input, DeviceCandidates *info) { GList *monitors, *l; gboolean builtin = FALSE; gboolean integrated = TRUE; #ifdef HAVE_LIBWACOM if (clutter_input_device_get_device_type (input->device) != CLUTTER_TOUCHSCREEN_DEVICE) { WacomDevice *wacom_device; WacomIntegrationFlags flags = 0; wacom_device = meta_input_device_get_wacom_device (META_INPUT_DEVICE (input->device)); if (wacom_device) { flags = libwacom_get_integration_flags (wacom_device); integrated = (flags & (WACOM_DEVICE_INTEGRATED_SYSTEM | WACOM_DEVICE_INTEGRATED_DISPLAY)) != 0; builtin = (flags & WACOM_DEVICE_INTEGRATED_SYSTEM) != 0; } } #endif monitors = meta_monitor_manager_get_monitors (mapper->monitor_manager); for (l = monitors; l; l = l->next) { MetaOutputMatchType edid_match; DeviceMatch match = { l->data, 0 }; g_assert (META_IS_MONITOR (l->data)); if (match_edid (input, l->data, &edid_match)) match.score |= 1 << edid_match; if (integrated && match_size (input, l->data)) match.score |= 1 << META_MATCH_SIZE; if (builtin && match_builtin (mapper, l->data)) match.score |= 1 << META_MATCH_IS_BUILTIN; if (match_config (input, l->data)) match.score |= 1 << META_MATCH_CONFIG; if (match.score > 0) g_array_append_val (info->matches, match); } if (info->matches->len == 0) { if (clutter_input_device_get_device_type (input->device) == CLUTTER_TOUCHSCREEN_DEVICE) { DeviceMatch match = { 0 }; match.monitor = meta_monitor_manager_get_laptop_panel (mapper->monitor_manager); if (match.monitor != NULL) g_array_append_val (info->matches, match); } info->best = 0; } else { DeviceMatch *best; g_array_sort (info->matches, (GCompareFunc) sort_by_score); best = &g_array_index (info->matches, DeviceMatch, 0); info->best = best->score; } } static void mapping_helper_add (MappingHelper *helper, MetaMapperInputInfo *input, MetaInputMapper *mapper) { DeviceCandidates info = { 0, }; guint i, pos = 0; info.input = input; info.matches = g_array_new (FALSE, TRUE, sizeof (DeviceMatch)); guess_candidates (mapper, input, &info); for (i = 0; i < helper->device_maps->len; i++) { DeviceCandidates *elem; elem = &g_array_index (helper->device_maps, DeviceCandidates, i); if (elem->best > info.best) pos = i; } if (pos >= helper->device_maps->len) g_array_append_val (helper->device_maps, info); else g_array_insert_val (helper->device_maps, pos, info); } static void mapping_helper_apply (MappingHelper *helper, MetaInputMapper *mapper) { guint i, j; /* Now, decide which input claims which output */ for (i = 0; i < helper->device_maps->len; i++) { DeviceCandidates *info; info = &g_array_index (helper->device_maps, DeviceCandidates, i); g_debug ("Applying mapping %d to input device '%s', type %d", i, clutter_input_device_get_device_name (info->input->device), clutter_input_device_get_device_type (info->input->device)); for (j = 0; j < info->matches->len; j++) { MetaLogicalMonitor *logical_monitor; MetaMapperOutputInfo *output; MetaMonitor *monitor; DeviceMatch *match; match = &g_array_index (info->matches, DeviceMatch, j); g_debug ("Output candidate '%s', score %x", meta_monitor_get_display_name (match->monitor), match->score); monitor = match->monitor; logical_monitor = meta_monitor_get_logical_monitor (monitor); output = g_hash_table_lookup (mapper->output_devices, logical_monitor); if (!output) continue; g_debug ("Matched input '%s' with output '%s'", clutter_input_device_get_device_name (info->input->device), meta_monitor_get_display_name (match->monitor)); mapper_output_info_add_input (output, info->input, monitor); break; } } } static void mapper_recalculate_candidates (MetaInputMapper *mapper) { MetaMapperInputInfo *input; MappingHelper helper; GHashTableIter iter; mapping_helper_init (&helper); g_hash_table_iter_init (&iter, mapper->input_devices); while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &input)) mapping_helper_add (&helper, input, mapper); mapping_helper_apply (&helper, mapper); mapping_helper_release (&helper); } static void mapper_recalculate_input (MetaInputMapper *mapper, MetaMapperInputInfo *input) { MappingHelper helper; mapping_helper_init (&helper); mapping_helper_add (&helper, input, mapper); mapping_helper_apply (&helper, mapper); mapping_helper_release (&helper); } static void mapper_update_outputs (MetaInputMapper *mapper) { MetaMapperOutputInfo *output; GList *logical_monitors, *l; GHashTableIter iter; g_hash_table_iter_init (&iter, mapper->output_devices); while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &output)) { mapper_output_info_clear_inputs (output); g_hash_table_iter_remove (&iter); } logical_monitors = meta_monitor_manager_get_logical_monitors (mapper->monitor_manager); for (l = logical_monitors; l; l = l->next) { MetaLogicalMonitor *logical_monitor = l->data; MetaMapperOutputInfo *info; info = mapper_output_info_new (logical_monitor); g_hash_table_insert (mapper->output_devices, logical_monitor, info); } mapper_recalculate_candidates (mapper); } static void input_mapper_monitors_changed_cb (MetaMonitorManager *monitor_manager, MetaInputMapper *mapper) { mapper_update_outputs (mapper); } static void input_mapper_power_save_mode_changed_cb (MetaMonitorManager *monitor_manager, MetaInputMapper *mapper) { ClutterInputDevice *device; MetaLogicalMonitor *logical_monitor; MetaMonitor *builtin; MetaPowerSave power_save_mode; gboolean on; power_save_mode = meta_monitor_manager_get_power_save_mode (mapper->monitor_manager); on = power_save_mode == META_POWER_SAVE_ON; builtin = meta_monitor_manager_get_laptop_panel (monitor_manager); if (!builtin) return; logical_monitor = meta_monitor_get_logical_monitor (builtin); if (!logical_monitor) return; device = meta_input_mapper_get_logical_monitor_device (mapper, logical_monitor, CLUTTER_TOUCHSCREEN_DEVICE); if (!device) return; g_signal_emit (mapper, signals[DEVICE_ENABLED], 0, device, on); } static void input_mapper_device_removed_cb (ClutterSeat *seat, ClutterInputDevice *device, MetaInputMapper *mapper) { meta_input_mapper_remove_device (mapper, device); } static void meta_input_mapper_finalize (GObject *object) { MetaInputMapper *mapper = META_INPUT_MAPPER (object); g_signal_handlers_disconnect_by_func (mapper->monitor_manager, input_mapper_monitors_changed_cb, mapper); g_signal_handlers_disconnect_by_func (mapper->seat, input_mapper_device_removed_cb, mapper); g_hash_table_unref (mapper->input_devices); g_hash_table_unref (mapper->output_devices); #ifdef HAVE_LIBGUDEV g_clear_object (&mapper->udev_client); #endif G_OBJECT_CLASS (meta_input_mapper_parent_class)->finalize (object); } static void meta_input_mapper_constructed (GObject *object) { #ifdef HAVE_LIBGUDEV const char *udev_subsystems[] = { "input", NULL }; #endif MetaInputMapper *mapper = META_INPUT_MAPPER (object); MetaBackend *backend; G_OBJECT_CLASS (meta_input_mapper_parent_class)->constructed (object); #ifdef HAVE_LIBGUDEV mapper->udev_client = g_udev_client_new (udev_subsystems); #endif mapper->seat = clutter_backend_get_default_seat (clutter_get_default_backend ()); g_signal_connect (mapper->seat, "device-removed", G_CALLBACK (input_mapper_device_removed_cb), mapper); backend = meta_get_backend (); mapper->monitor_manager = meta_backend_get_monitor_manager (backend); g_signal_connect (mapper->monitor_manager, "monitors-changed-internal", G_CALLBACK (input_mapper_monitors_changed_cb), mapper); g_signal_connect (mapper->monitor_manager, "power-save-mode-changed", G_CALLBACK (input_mapper_power_save_mode_changed_cb), mapper); mapper_update_outputs (mapper); } static void meta_input_mapper_class_init (MetaInputMapperClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = meta_input_mapper_constructed; object_class->finalize = meta_input_mapper_finalize; signals[DEVICE_MAPPED] = g_signal_new ("device-mapped", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2, CLUTTER_TYPE_INPUT_DEVICE, G_TYPE_POINTER); signals[DEVICE_ENABLED] = g_signal_new ("device-enabled", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2, CLUTTER_TYPE_INPUT_DEVICE, G_TYPE_BOOLEAN); signals[DEVICE_ASPECT_RATIO] = g_signal_new ("device-aspect-ratio", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2, CLUTTER_TYPE_INPUT_DEVICE, G_TYPE_DOUBLE); } static void meta_input_mapper_init (MetaInputMapper *mapper) { mapper->input_devices = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) mapper_input_info_free); mapper->output_devices = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) mapper_output_info_free); } MetaInputMapper * meta_input_mapper_new (void) { return g_object_new (META_TYPE_INPUT_MAPPER, NULL); } void meta_input_mapper_add_device (MetaInputMapper *mapper, ClutterInputDevice *device) { MetaMapperInputInfo *info; g_return_if_fail (mapper != NULL); g_return_if_fail (device != NULL); if (g_hash_table_contains (mapper->input_devices, device)) return; info = mapper_input_info_new (device, mapper); g_hash_table_insert (mapper->input_devices, device, info); mapper_recalculate_input (mapper, info); } void meta_input_mapper_remove_device (MetaInputMapper *mapper, ClutterInputDevice *device) { MetaMapperInputInfo *input; g_return_if_fail (mapper != NULL); g_return_if_fail (device != NULL); input = g_hash_table_lookup (mapper->input_devices, device); if (input) { if (input->output) mapper_output_info_remove_input (input->output, input); g_hash_table_remove (mapper->input_devices, device); } } ClutterInputDevice * meta_input_mapper_get_logical_monitor_device (MetaInputMapper *mapper, MetaLogicalMonitor *logical_monitor, ClutterInputDeviceType device_type) { MetaMapperOutputInfo *output; GList *l; output = g_hash_table_lookup (mapper->output_devices, logical_monitor); if (!output) return NULL; for (l = output->input_devices; l; l = l->next) { MetaMapperInputInfo *input = l->data; if (clutter_input_device_get_device_type (input->device) == device_type) return input->device; } return NULL; } static ClutterInputDevice * find_grouped_pen (ClutterInputDevice *device) { GList *l, *devices; ClutterInputDeviceType device_type; ClutterInputDevice *pen = NULL; ClutterSeat *seat; device_type = clutter_input_device_get_device_type (device); if (device_type == CLUTTER_TABLET_DEVICE || device_type == CLUTTER_PEN_DEVICE) return device; seat = clutter_input_device_get_seat (device); devices = clutter_seat_list_devices (seat); for (l = devices; l; l = l->next) { ClutterInputDevice *other_device = l->data; device_type = clutter_input_device_get_device_type (other_device); if ((device_type == CLUTTER_TABLET_DEVICE || device_type == CLUTTER_PEN_DEVICE) && clutter_input_device_is_grouped (device, other_device)) { pen = other_device; break; } } g_list_free (devices); return pen; } MetaLogicalMonitor * meta_input_mapper_get_device_logical_monitor (MetaInputMapper *mapper, ClutterInputDevice *device) { MetaMapperOutputInfo *output; MetaLogicalMonitor *logical_monitor; GHashTableIter iter; GList *l; if (clutter_input_device_get_device_type (device) == CLUTTER_PAD_DEVICE) { device = find_grouped_pen (device); if (!device) return NULL; } g_hash_table_iter_init (&iter, mapper->output_devices); while (g_hash_table_iter_next (&iter, (gpointer *) &logical_monitor, (gpointer *) &output)) { for (l = output->input_devices; l; l = l->next) { MetaMapperInputInfo *input = l->data; if (input->device == device) return logical_monitor; } } return NULL; } GSettings * meta_input_mapper_get_tablet_settings (MetaInputMapper *mapper, ClutterInputDevice *device) { MetaMapperInputInfo *input; g_return_val_if_fail (META_IS_INPUT_MAPPER (mapper), NULL); g_return_val_if_fail (CLUTTER_IS_INPUT_DEVICE (device), NULL); input = g_hash_table_lookup (mapper->input_devices, device); if (!input) return NULL; return input->settings; }