diff options
| author | Sunderland93 <[email protected]> | 2026-08-04 18:50:57 +0400 |
|---|---|---|
| committer | Luke from DC <[email protected]> | 2026-08-19 19:15:44 +0000 |
| commit | 49bca7b6e53d0464e7e4f455ec8cd33aaaf2ca26 (patch) | |
| tree | 6f0a3d916cb54d789b9a62e80ae917d1221da70a /plugins | |
| parent | ae5e2c5e11cf741162c34c67d87cb43ebe4abb60 (diff) | |
| download | mate-settings-daemon-49bca7b6e53d0464e7e4f455ec8cd33aaaf2ca26.tar.bz2 mate-settings-daemon-49bca7b6e53d0464e7e4f455ec8cd33aaaf2ca26.tar.xz | |
Add wlrandr plugin
Diffstat (limited to 'plugins')
| -rw-r--r-- | plugins/Makefile.am | 6 | ||||
| -rw-r--r-- | plugins/wlrandr/Makefile.am | 108 | ||||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-16.png | bin | 0 -> 875 bytes | |||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-22.png | bin | 0 -> 1242 bytes | |||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-24.png | bin | 0 -> 1354 bytes | |||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-32.png | bin | 0 -> 1887 bytes | |||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-48.png | bin | 0 -> 3049 bytes | |||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-64.png | bin | 0 -> 4273 bytes | |||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-96.png | bin | 0 -> 6937 bytes | |||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-manager.c | 2527 | ||||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-manager.h | 57 | ||||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-plugin.c | 105 | ||||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr-plugin.h | 63 | ||||
| -rw-r--r-- | plugins/wlrandr/msd-wlrandr.svg | 491 | ||||
| -rw-r--r-- | plugins/wlrandr/protocol/wlr-output-management-unstable-v1.xml | 601 | ||||
| -rw-r--r-- | plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in | 9 |
16 files changed, 3967 insertions, 0 deletions
diff --git a/plugins/Makefile.am b/plugins/Makefile.am index fe8bf9a..0118876 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -34,6 +34,12 @@ else disabled_plugins += rfkill endif +if BUILD_WAYLAND +enabled_plugins += wlrandr +else +disabled_plugins += wlrandr +endif + SUBDIRS = common $(enabled_plugins) DIST_SUBDIRS = $(SUBDIRS) $(disabled_plugins) diff --git a/plugins/wlrandr/Makefile.am b/plugins/wlrandr/Makefile.am new file mode 100644 index 0000000..cfcabd4 --- /dev/null +++ b/plugins/wlrandr/Makefile.am @@ -0,0 +1,108 @@ +icondir = $(datadir)/icons/hicolor +context = apps + + +ICON_FILES = \ + msd-wlrandr-16.png \ + msd-wlrandr-22.png \ + msd-wlrandr-24.png \ + msd-wlrandr-32.png \ + msd-wlrandr-48.png \ + msd-wlrandr-64.png \ + msd-wlrandr-96.png \ + msd-wlrandr.svg + +install-data-local: + $(mkinstalldirs) $(DESTDIR)$(icondir)/16x16/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/22x22/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/24x24/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/32x32/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/48x48/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/64x64/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/96x96/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/scalable/$(context) + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-16.png $(DESTDIR)$(icondir)/16x16/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-22.png $(DESTDIR)$(icondir)/22x22/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-24.png $(DESTDIR)$(icondir)/24x24/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-32.png $(DESTDIR)$(icondir)/32x32/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-48.png $(DESTDIR)$(icondir)/48x48/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-64.png $(DESTDIR)$(icondir)/64x64/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-96.png $(DESTDIR)$(icondir)/96x96/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr.svg $(DESTDIR)$(icondir)/scalable/$(context)/msd-wlrandr.svg + +uninstall-local: + rm -f $(DESTDIR)$(icondir)/16x16/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/22x22/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/24x24/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/32x32/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/48x48/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/64x64/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/96x96/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/scalable/$(context)/msd-wlrandr.svg + +plugin_LTLIBRARIES = \ + libwlrandr.la + +protocol_src_dir = protocol +protocol_src = $(protocol_src_dir)/wlr-output-management-unstable-v1.xml + +generated_protocol_sources = \ + wlr-output-management-unstable-v1-protocol.c \ + wlr-output-management-unstable-v1-client-protocol.h + +libwlrandr_la_SOURCES = \ + msd-wlrandr-plugin.h \ + msd-wlrandr-plugin.c \ + msd-wlrandr-manager.h \ + msd-wlrandr-manager.c \ + $(generated_protocol_sources) + +libwlrandr_la_CPPFLAGS = \ + -I$(top_srcdir)/mate-settings-daemon \ + -I$(builddir) \ + -DBINDIR=\"$(bindir)\" \ + -DMATE_SETTINGS_LOCALEDIR=\""$(datadir)/locale"\" \ + $(AM_CPPFLAGS) + +libwlrandr_la_CFLAGS = \ + $(SETTINGS_PLUGIN_CFLAGS) \ + $(WAYLAND_CLIENT_CFLAGS) \ + $(APPINDICATOR_CFLAGS) \ + $(LIBNOTIFY_CFLAGS) \ + $(MATE_DESKTOP_CFLAGS) \ + $(AM_CFLAGS) \ + $(WARN_CFLAGS) + +libwlrandr_la_LDFLAGS = \ + $(MSD_PLUGIN_LDFLAGS) + +libwlrandr_la_LIBADD = \ + $(SETTINGS_PLUGIN_LIBS) \ + $(WAYLAND_CLIENT_LIBS) \ + $(APPINDICATOR_LIBS) \ + $(LIBNOTIFY_LIBS) \ + $(MATE_DESKTOP_LIBS) \ + $(NULL) + +BUILT_SOURCES = $(generated_protocol_sources) + +CLEANFILES = $(generated_protocol_sources) + +wlr-output-management-unstable-v1-client-protocol.h: $(protocol_src) + $(AM_V_GEN) $(WAYLAND_SCANNER) client-header $< $@ + +wlr-output-management-unstable-v1-protocol.c: $(protocol_src) + $(AM_V_GEN) $(WAYLAND_SCANNER) private-code $< $@ + +plugin_in_files = \ + wlrandr.mate-settings-plugin.desktop.in + +plugin_DATA = $(plugin_in_files:.mate-settings-plugin.desktop.in=.mate-settings-plugin) + +EXTRA_DIST = $(plugin_in_files) $(ICON_FILES) $(protocol_src) +DISTCLEANFILES = $(plugin_DATA) + +$(plugin_DATA): $(plugin_in_files) + $(AM_V_GEN) $(MSGFMT) --desktop --template $< -d $(top_srcdir)/po -o $@ + +-include $(top_srcdir)/git.mk diff --git a/plugins/wlrandr/msd-wlrandr-16.png b/plugins/wlrandr/msd-wlrandr-16.png Binary files differnew file mode 100644 index 0000000..adb9e36 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-16.png diff --git a/plugins/wlrandr/msd-wlrandr-22.png b/plugins/wlrandr/msd-wlrandr-22.png Binary files differnew file mode 100644 index 0000000..a90f71b --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-22.png diff --git a/plugins/wlrandr/msd-wlrandr-24.png b/plugins/wlrandr/msd-wlrandr-24.png Binary files differnew file mode 100644 index 0000000..6d66034 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-24.png diff --git a/plugins/wlrandr/msd-wlrandr-32.png b/plugins/wlrandr/msd-wlrandr-32.png Binary files differnew file mode 100644 index 0000000..d0fd058 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-32.png diff --git a/plugins/wlrandr/msd-wlrandr-48.png b/plugins/wlrandr/msd-wlrandr-48.png Binary files differnew file mode 100644 index 0000000..ff2e1c7 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-48.png diff --git a/plugins/wlrandr/msd-wlrandr-64.png b/plugins/wlrandr/msd-wlrandr-64.png Binary files differnew file mode 100644 index 0000000..ebde0b6 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-64.png diff --git a/plugins/wlrandr/msd-wlrandr-96.png b/plugins/wlrandr/msd-wlrandr-96.png Binary files differnew file mode 100644 index 0000000..fab9dcd --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-96.png diff --git a/plugins/wlrandr/msd-wlrandr-manager.c b/plugins/wlrandr/msd-wlrandr-manager.c new file mode 100644 index 0000000..d5cc741 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-manager.c @@ -0,0 +1,2527 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2026 MATE Developers + * + * 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, 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <string.h> + +#include <glib/gi18n-lib.h> +#include <glib/gstdio.h> +#include <gtk/gtk.h> +#include <gdk/gdkwayland.h> +#include <libnotify/notify.h> + +#define MATE_DESKTOP_USE_UNSTABLE_API +#include <libmate-desktop/mate-desktop-utils.h> + +#include <libayatana-appindicator/app-indicator.h> + +#include <wayland-client.h> +#include "wlr-output-management-unstable-v1-client-protocol.h" + +#include "msd-wlrandr-manager.h" + +#define CONF_SCHEMA "org.mate.SettingsDaemon.plugins.wlrandr" +#define CONF_KEY_SHOW_NOTIFICATION_ICON "show-notification-icon" +#define CONF_KEY_TURN_ON_EXTERNAL_MONITORS_AT_STARTUP "turn-on-external-monitors-at-startup" +#define CONF_KEY_DEFAULT_CONFIGURATION_FILE "default-configuration-file" + +/* Number of seconds that the confirmation dialog will last before it resets the + * RANDR configuration to its old state. + */ +#define CONFIRMATION_DIALOG_SECONDS 30 + +/* name of the icon files (msd-wlrandr.svg, etc.) */ +#define MSD_WLRANDR_ICON_NAME "msd-wlrandr" + +/* executable of the control center's display configuration capplet */ +#define MSD_WLRANDR_DISPLAY_CAPPLET "mate-display-properties" + +#define CONFIG_INTENDED_BASENAME "monitors-wayland.xml" +#define CONFIG_BACKUP_BASENAME "monitors-wayland.xml.backup" + +#define WLR_OUTPUT_MANAGER_VERSION 4 + +#define MSD_DBUS_PATH "/org/mate/SettingsDaemon" +#define MSD_DBUS_NAME "org.mate.SettingsDaemon" + +#define MSD_WLRANDR_DBUS_NAME MSD_DBUS_NAME ".WLRANDR" +#define MSD_WLRANDR_DBUS_PATH MSD_DBUS_PATH "/WLRANDR" + +static const gchar introspection_xml[] = +"<node>" +" <interface name='org.mate.SettingsDaemon.WLRANDR'>" +" <method name='ApplyConfiguration'>" +" </method>" +" </interface>" +"" +" <interface name='org.mate.SettingsDaemon.WLRANDR_2'>" +" <method name='ApplyConfiguration'>" +" <arg name='parent_window_id' type='x' direction='in'/>" +" <arg name='timestamp' type='x' direction='in'/>" +" </method>" +" </interface>" +"</node>"; + +typedef struct { + char *name; + char *vendor; + char *product; + char *serial; + gboolean enabled; + int width; + int height; + int rate; + int x; + int y; + double scale; + int transform; + gboolean adaptive_sync; +} WlrOutputInfo; + +typedef struct { + WlrOutputInfo **outputs; +} WlrConfig; + +typedef struct _WlOutputHead WlOutputHead; +typedef struct _WlOutputMode WlOutputMode; + +typedef struct _WlOutputMode { + int width; + int height; + int refresh; + gboolean preferred; + struct zwlr_output_mode_v1 *proxy; + WlOutputHead *head; +} WlOutputMode; + +typedef struct _WlOutputHead { + char *name; + char *description; + char *make; + char *model; + char *serial_number; + gboolean enabled; + int x; + int y; + double scale; + int transform; + gboolean adaptive_sync; + int adaptive_sync_supported; /* -1 unknown, 0 no, 1 yes */ + GList *modes; + WlOutputMode *current_mode; + struct zwlr_output_head_v1 *proxy; + MsdWlrandrManager *manager; +} WlOutputHead; + +struct MsdWlrandrManagerPrivate { + GSettings *settings; + struct wl_display *display; + struct wl_registry *registry; + struct zwlr_output_manager_v1 *manager; + uint32_t manager_version; + uint32_t last_serial; + GList *heads; + gboolean started; + gboolean initial_config_applied; + struct zwlr_output_configuration_v1 *pending_config; + gboolean probing; + AppIndicator *indicator; + GtkWidget *menu; + GtkWidget *confirm_dialog; + guint icon_changed_id; + + GDBusConnection *connection; + GDBusNodeInfo *introspection_data; + GCancellable *bus_cancellable; + guint owner_id; + guint registration_id; + guint registration_id_2; +}; + +static void +rebuild_menu (MsdWlrandrManager *manager); + +static void +save_current_configuration (MsdWlrandrManager *manager); + +static void +queue_rollback_confirmation (MsdWlrandrManager *manager, WlrConfig *previous); + +static void +probe_adaptive_sync_support (MsdWlrandrManager *manager); + +static gboolean +probe_idle_cb (gpointer data) +{ + probe_adaptive_sync_support (data); + return G_SOURCE_REMOVE; +} + +static WlrConfig * +snapshot_config (MsdWlrandrManager *manager); + +static WlrOutputInfo * +find_output_in_config (WlrConfig *config, const char *name); + +static struct zwlr_output_mode_v1 * +find_mode_for_output (WlOutputHead *head, WlrOutputInfo *info); + +static WlrOutputInfo * +wlr_output_info_new (const char *name) +{ + WlrOutputInfo *info = g_new0 (WlrOutputInfo, 1); + info->name = g_strdup (name); + info->scale = 1.0; + return info; +} + +static void +wlr_output_info_free (WlrOutputInfo *info) +{ + if (info == NULL) + return; + g_free (info->name); + g_free (info->vendor); + g_free (info->product); + g_free (info->serial); + g_free (info); +} + +static void +wlr_config_free (WlrConfig *config) +{ + int i; + + if (config == NULL) + return; + + for (i = 0; config->outputs != NULL && config->outputs[i] != NULL; i++) + wlr_output_info_free (config->outputs[i]); + g_free (config->outputs); + g_free (config); +} + +static WlOutputMode * +wlr_output_mode_new (struct zwlr_output_mode_v1 *proxy) +{ + WlOutputMode *mode = g_new0 (WlOutputMode, 1); + mode->proxy = proxy; + return mode; +} + +static void +wlr_output_mode_free (WlOutputMode *mode) +{ + g_free (mode); +} + +static WlOutputHead * +wlr_output_head_new (struct zwlr_output_head_v1 *proxy) +{ + WlOutputHead *head = g_new0 (WlOutputHead, 1); + head->proxy = proxy; + head->scale = 1.0; + head->adaptive_sync_supported = -1; + return head; +} + +static void +wlr_output_head_free (WlOutputHead *head) +{ + GList *l; + + if (head == NULL) + return; + + for (l = head->modes; l != NULL; l = l->next) + wlr_output_mode_free (l->data); + g_list_free (head->modes); + + g_free (head->name); + g_free (head->description); + g_free (head->make); + g_free (head->model); + g_free (head->serial_number); + g_free (head); +} + +static WlOutputHead * +find_head_by_proxy (MsdWlrandrManager *manager, struct zwlr_output_head_v1 *proxy) +{ + GList *l; + + for (l = manager->priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + if (head->proxy == proxy) + return head; + } + + return NULL; +} + +static WlOutputMode * +find_mode_by_proxy (WlOutputHead *head, struct zwlr_output_mode_v1 *proxy) +{ + GList *l; + + for (l = head->modes; l != NULL; l = l->next) { + WlOutputMode *mode = l->data; + if (mode->proxy == proxy) + return mode; + } + + return NULL; +} + +/* ------------------------------------------------------------------ * + * wlr-output-management protocol listeners * + * ------------------------------------------------------------------ */ + +static void +head_name_handler (void *data, struct zwlr_output_head_v1 *proxy, const char *name) +{ + WlOutputHead *head = data; + g_free (head->name); + head->name = g_strdup (name); +} + +static void +head_description_handler (void *data, struct zwlr_output_head_v1 *proxy, const char *description) +{ + WlOutputHead *head = data; + g_free (head->description); + head->description = g_strdup (description); +} + +static void +head_make_handler (void *data, struct zwlr_output_head_v1 *proxy, const char *make) +{ + WlOutputHead *head = data; + g_free (head->make); + head->make = g_strdup (make); +} + +static void +head_model_handler (void *data, struct zwlr_output_head_v1 *proxy, const char *model) +{ + WlOutputHead *head = data; + g_free (head->model); + head->model = g_strdup (model); +} + +static void +head_serial_number_handler (void *data, struct zwlr_output_head_v1 *proxy, const char *serial_number) +{ + WlOutputHead *head = data; + g_free (head->serial_number); + head->serial_number = g_strdup (serial_number); +} + +static void +head_physical_size_handler (void *data, struct zwlr_output_head_v1 *proxy, int32_t width, int32_t height) +{ +} + +static void +mode_size_handler (void *data, struct zwlr_output_mode_v1 *mode, int32_t width, int32_t height) +{ + WlOutputMode *wm = data; + wm->width = width; + wm->height = height; +} + +static void +mode_refresh_handler (void *data, struct zwlr_output_mode_v1 *mode, int32_t refresh) +{ + WlOutputMode *wm = data; + wm->refresh = refresh; +} + +static void +mode_preferred_handler (void *data, struct zwlr_output_mode_v1 *mode) +{ + WlOutputMode *wm = data; + wm->preferred = TRUE; +} + +static void +mode_finished_handler (void *data, struct zwlr_output_mode_v1 *mode) +{ + WlOutputMode *wm = data; + WlOutputHead *head = wm->head; + GList *l; + + for (l = head->modes; l != NULL; l = l->next) { + if (l->data == wm) { + head->modes = g_list_delete_link (head->modes, l); + break; + } + } + + if (head->current_mode == wm) + head->current_mode = NULL; + + wlr_output_mode_free (wm); +} + +static const struct zwlr_output_mode_v1_listener mode_listener = { + .size = mode_size_handler, + .refresh = mode_refresh_handler, + .preferred = mode_preferred_handler, + .finished = mode_finished_handler, +}; + +static void +head_mode_handler (void *data, struct zwlr_output_head_v1 *proxy, struct zwlr_output_mode_v1 *mode) +{ + WlOutputHead *head = data; + WlOutputMode *wm; + + wm = wlr_output_mode_new (mode); + wm->head = head; + head->modes = g_list_append (head->modes, wm); + zwlr_output_mode_v1_add_listener (mode, &mode_listener, wm); +} + +static void +head_enabled_handler (void *data, struct zwlr_output_head_v1 *proxy, int32_t enabled) +{ + WlOutputHead *head = data; + head->enabled = enabled != 0; +} + +static void +head_current_mode_handler (void *data, struct zwlr_output_head_v1 *proxy, struct zwlr_output_mode_v1 *mode) +{ + WlOutputHead *head = data; + head->current_mode = find_mode_by_proxy (head, mode); +} + +static void +head_position_handler (void *data, struct zwlr_output_head_v1 *proxy, int32_t x, int32_t y) +{ + WlOutputHead *head = data; + head->x = x; + head->y = y; +} + +static void +head_transform_handler (void *data, struct zwlr_output_head_v1 *proxy, int32_t transform) +{ + WlOutputHead *head = data; + head->transform = transform; +} + +static void +head_scale_handler (void *data, struct zwlr_output_head_v1 *proxy, wl_fixed_t scale) +{ + WlOutputHead *head = data; + head->scale = wl_fixed_to_double (scale); +} + +static void +head_adaptive_sync_handler (void *data, struct zwlr_output_head_v1 *proxy, uint32_t state) +{ + WlOutputHead *head = data; + head->adaptive_sync = state != 0; +} + +static void +head_finished_handler (void *data, struct zwlr_output_head_v1 *proxy) +{ + MsdWlrandrManager *manager = ((WlOutputHead *) data)->manager; + WlOutputHead *head = data; + + manager->priv->heads = g_list_remove (manager->priv->heads, head); + wlr_output_head_free (head); + rebuild_menu (manager); +} + +static const struct zwlr_output_head_v1_listener head_listener = { + .name = head_name_handler, + .description = head_description_handler, + .physical_size = head_physical_size_handler, + .mode = head_mode_handler, + .enabled = head_enabled_handler, + .current_mode = head_current_mode_handler, + .position = head_position_handler, + .transform = head_transform_handler, + .scale = head_scale_handler, + .finished = head_finished_handler, + .make = head_make_handler, + .model = head_model_handler, + .serial_number = head_serial_number_handler, + .adaptive_sync = head_adaptive_sync_handler, +}; + +static void +manager_head_handler (void *data, struct zwlr_output_manager_v1 *manager_obj, struct zwlr_output_head_v1 *proxy) +{ + MsdWlrandrManager *manager = data; + WlOutputHead *head; + + head = wlr_output_head_new (proxy); + head->manager = manager; + manager->priv->heads = g_list_append (manager->priv->heads, head); + + zwlr_output_head_v1_add_listener (proxy, &head_listener, head); +} + +static void +apply_initial_config (MsdWlrandrManager *manager); + +static void +manager_done_handler (void *data, struct zwlr_output_manager_v1 *manager_obj, uint32_t serial) +{ + MsdWlrandrManager *manager = data; + + manager->priv->last_serial = serial; + g_debug ("msd-wlrandr: output manager done, serial %u", serial); + + rebuild_menu (manager); + + if (!manager->priv->initial_config_applied) { + manager->priv->initial_config_applied = TRUE; + apply_initial_config (manager); + } + + probe_adaptive_sync_support (manager); +} + +static void +manager_finished_handler (void *data, struct zwlr_output_manager_v1 *manager_obj) +{ + MsdWlrandrManager *manager = data; + + g_debug ("msd-wlrandr: output manager finished"); + + if (manager->priv->manager != NULL) { + zwlr_output_manager_v1_destroy (manager->priv->manager); + manager->priv->manager = NULL; + } +} + +static const struct zwlr_output_manager_v1_listener manager_listener = { + .head = manager_head_handler, + .done = manager_done_handler, + .finished = manager_finished_handler, +}; + +static void +registry_handle_global (void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) +{ + MsdWlrandrManager *manager = data; + + if (strcmp (interface, zwlr_output_manager_v1_interface.name) == 0) { + manager->priv->manager_version = MIN (version, WLR_OUTPUT_MANAGER_VERSION); + manager->priv->manager = wl_registry_bind (registry, name, + &zwlr_output_manager_v1_interface, + manager->priv->manager_version); + zwlr_output_manager_v1_add_listener (manager->priv->manager, &manager_listener, manager); + g_debug ("msd-wlrandr: bound zwlr_output_manager_v1 version %u", manager->priv->manager_version); + } +} + +static void +registry_handle_global_remove (void *data, struct wl_registry *registry, uint32_t name) +{ +} + +static const struct wl_registry_listener registry_listener = { + .global = registry_handle_global, + .global_remove = registry_handle_global_remove, +}; + +/* ------------------------------------------------------------------ * + * Configuration application * + * ------------------------------------------------------------------ */ + +typedef struct { + MsdWlrandrManager *manager; + gboolean save; + gboolean confirm_rollback; + WlrConfig *previous; + WlOutputHead *adaptive_sync_head; +} ApplyData; + +static void +notify_config_error (const char *message) +{ + NotifyNotification *n; + + g_warning ("msd-wlrandr: %s", message); + + if (notify_is_initted ()) { + n = notify_notification_new (_("Display Settings"), message, MSD_WLRANDR_ICON_NAME); + notify_notification_set_timeout (n, 3000); + notify_notification_show (n, NULL); + g_object_unref (n); + } +} + +static void +config_succeeded_handler (void *data, struct zwlr_output_configuration_v1 *config) +{ + ApplyData *ad = data; + + g_debug ("msd-wlrandr: output configuration applied"); + + if (ad->save) + save_current_configuration (ad->manager); + + if (ad->confirm_rollback) + queue_rollback_confirmation (ad->manager, ad->previous); + else + wlr_config_free (ad->previous); + + ad->manager->priv->pending_config = NULL; + g_idle_add (probe_idle_cb, ad->manager); + zwlr_output_configuration_v1_destroy (config); + g_free (ad); +} + +static void +config_failed_handler (void *data, struct zwlr_output_configuration_v1 *config) +{ + ApplyData *ad = data; + + if (ad->adaptive_sync_head != NULL && + g_list_find (ad->manager->priv->heads, ad->adaptive_sync_head) != NULL) { + WlOutputHead *head = ad->adaptive_sync_head; + + g_debug ("msd-wlrandr: adaptive sync rejected by the compositor on %s", head->name); + head->adaptive_sync_supported = 0; + head->adaptive_sync = 0; + save_current_configuration (ad->manager); + rebuild_menu (ad->manager); + } else { + notify_config_error (_("Could not apply the display configuration")); + } + + wlr_config_free (ad->previous); + ad->manager->priv->pending_config = NULL; + g_idle_add (probe_idle_cb, ad->manager); + zwlr_output_configuration_v1_destroy (config); + g_free (ad); +} + +static void +config_cancelled_handler (void *data, struct zwlr_output_configuration_v1 *config) +{ + ApplyData *ad = data; + + g_debug ("msd-wlrandr: output configuration cancelled"); + + wlr_config_free (ad->previous); + ad->manager->priv->pending_config = NULL; + g_idle_add (probe_idle_cb, ad->manager); + zwlr_output_configuration_v1_destroy (config); + g_free (ad); +} + +static const struct zwlr_output_configuration_v1_listener config_listener = { + .succeeded = config_succeeded_handler, + .failed = config_failed_handler, + .cancelled = config_cancelled_handler, +}; + +typedef struct { + MsdWlrandrManager *manager; + WlOutputHead *head; + guint stage; + gboolean original; +} ProbeData; + +static const struct zwlr_output_configuration_v1_listener probe_listener; + +static void +probe_confirm (ProbeData *pd, gboolean adaptive_sync) +{ + struct MsdWlrandrManagerPrivate *priv = pd->manager->priv; + struct zwlr_output_configuration_v1 *config_obj; + struct zwlr_output_configuration_head_v1 *head_obj; + WlrConfig *config; + GList *l; + + if (priv->manager == NULL || g_list_find (priv->heads, pd->head) == NULL) { + g_free (pd); + return; + } + + config = snapshot_config (pd->manager); + config_obj = zwlr_output_manager_v1_create_configuration (priv->manager, priv->last_serial); + + for (l = priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + WlrOutputInfo *info = find_output_in_config (config, head->name); + + if (info != NULL && info->enabled) { + struct zwlr_output_mode_v1 *mode = find_mode_for_output (head, info); + + head_obj = zwlr_output_configuration_v1_enable_head (config_obj, head->proxy); + + if (mode != NULL) + zwlr_output_configuration_head_v1_set_mode (head_obj, mode); + else + zwlr_output_configuration_head_v1_set_custom_mode (head_obj, + info->width, + info->height, + info->rate); + + zwlr_output_configuration_head_v1_set_position (head_obj, info->x, info->y); + zwlr_output_configuration_head_v1_set_transform (head_obj, info->transform); + zwlr_output_configuration_head_v1_set_scale (head_obj, wl_fixed_from_double (info->scale)); + + if (priv->manager_version >= 4 && head == pd->head) + zwlr_output_configuration_head_v1_set_adaptive_sync (head_obj, + adaptive_sync ? 1 : 0); + } else { + zwlr_output_configuration_v1_disable_head (config_obj, head->proxy); + } + } + + wlr_config_free (config); + + zwlr_output_configuration_v1_add_listener (config_obj, &probe_listener, pd); + zwlr_output_configuration_v1_apply (config_obj); + wl_display_flush (priv->display); +} + +static void +probe_succeeded_handler (void *data, struct zwlr_output_configuration_v1 *config) +{ + ProbeData *pd = data; + struct MsdWlrandrManagerPrivate *priv = pd->manager->priv; + + if (pd->stage == 0) { + if (g_list_find (priv->heads, pd->head) != NULL) { + g_debug ("msd-wlrandr: adaptive sync toggle accepted on %s", pd->head->name); + pd->stage = 1; + probe_confirm (pd, pd->original); + zwlr_output_configuration_v1_destroy (config); + return; + } + } else if (g_list_find (priv->heads, pd->head) != NULL) { + g_debug ("msd-wlrandr: adaptive sync confirmed on %s", pd->head->name); + pd->head->adaptive_sync_supported = 1; + pd->head->adaptive_sync = pd->original; + rebuild_menu (pd->manager); + } + + priv->probing = FALSE; + probe_adaptive_sync_support (pd->manager); + zwlr_output_configuration_v1_destroy (config); + g_free (pd); +} + +static void +probe_failed_handler (void *data, struct zwlr_output_configuration_v1 *config) +{ + ProbeData *pd = data; + struct MsdWlrandrManagerPrivate *priv = pd->manager->priv; + + if (pd->stage == 0 && g_list_find (priv->heads, pd->head) != NULL) { + g_debug ("msd-wlrandr: adaptive sync not supported on %s", pd->head->name); + pd->head->adaptive_sync_supported = 0; + rebuild_menu (pd->manager); + } else if (g_list_find (priv->heads, pd->head) != NULL) { + g_debug ("msd-wlrandr: adaptive sync restore rejected on %s", pd->head->name); + pd->head->adaptive_sync_supported = 0; + pd->head->adaptive_sync = pd->original; + rebuild_menu (pd->manager); + } + + priv->probing = FALSE; + probe_adaptive_sync_support (pd->manager); + zwlr_output_configuration_v1_destroy (config); + g_free (pd); +} + +static void +probe_cancelled_handler (void *data, struct zwlr_output_configuration_v1 *config) +{ + ProbeData *pd = data; + + g_debug ("msd-wlrandr: adaptive sync probe cancelled on %s", pd->head->name); + + pd->manager->priv->probing = FALSE; + probe_adaptive_sync_support (pd->manager); + zwlr_output_configuration_v1_destroy (config); + g_free (pd); +} + +static const struct zwlr_output_configuration_v1_listener probe_listener = { + .succeeded = probe_succeeded_handler, + .failed = probe_failed_handler, + .cancelled = probe_cancelled_handler, +}; + +static gboolean +head_is_wayland_backend (WlOutputHead *head) +{ + return g_strcmp0 (head->make, "wayland") == 0 && + g_strcmp0 (head->model, "wayland") == 0; +} + +static void +probe_adaptive_sync_support (MsdWlrandrManager *manager) +{ + struct MsdWlrandrManagerPrivate *priv = manager->priv; + ProbeData *pd; + GList *l; + + if (priv->manager == NULL || priv->manager_version < 4) + return; + + if (priv->pending_config != NULL || priv->probing) + return; + + for (l = priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + + if (!head->enabled || head->adaptive_sync_supported != -1) + continue; + + /* The wlroots Wayland backend cannot enable VRR on its + * nested output, and the compositor's reply is not reliable + * for it. + */ + if (head_is_wayland_backend (head)) { + g_debug ("msd-wlrandr: adaptive sync not supported on nested output %s", head->name); + head->adaptive_sync_supported = 0; + continue; + } + + if (head->current_mode == NULL) + continue; + + pd = g_new0 (ProbeData, 1); + pd->manager = manager; + pd->head = head; + pd->stage = 0; + pd->original = head->adaptive_sync; + priv->probing = TRUE; + + probe_confirm (pd, !head->adaptive_sync); + + return; + } +} + +static WlrOutputInfo * +find_output_in_config (WlrConfig *config, const char *name) +{ + int i; + + if (config == NULL || config->outputs == NULL) + return NULL; + + for (i = 0; config->outputs[i] != NULL; i++) { + if (g_strcmp0 (config->outputs[i]->name, name) == 0) + return config->outputs[i]; + } + + return NULL; +} + +static struct zwlr_output_mode_v1 * +find_mode_for_output (WlOutputHead *head, WlrOutputInfo *info) +{ + GList *l; + struct zwlr_output_mode_v1 *fallback = NULL; + + for (l = head->modes; l != NULL; l = l->next) { + WlOutputMode *wm = l->data; + + if (wm->width == info->width && wm->height == info->height) { + if (fallback == NULL) + fallback = wm->proxy; + if (info->rate <= 0 || wm->refresh == info->rate) + return wm->proxy; + } + } + + return fallback; +} + +static void +fill_output_mode (WlOutputHead *head, WlrOutputInfo *info) +{ + GList *l; + WlOutputMode *wm; + + if (info->width > 0 && info->height > 0) + return; + + if (head->current_mode != NULL) { + info->width = head->current_mode->width; + info->height = head->current_mode->height; + info->rate = head->current_mode->refresh; + return; + } + + for (l = head->modes; l != NULL; l = l->next) { + wm = l->data; + if (wm->preferred || info->width <= 0) { + info->width = wm->width; + info->height = wm->height; + info->rate = wm->refresh; + if (wm->preferred) + return; + } + } +} + +static gboolean +apply_configuration (MsdWlrandrManager *manager, + WlrConfig *config, + gboolean save, + gboolean confirm_rollback, + WlrConfig *previous, + WlOutputHead *adaptive_sync_head) +{ + struct MsdWlrandrManagerPrivate *priv = manager->priv; + struct zwlr_output_configuration_v1 *config_obj; + GList *l; + ApplyData *ad; + + if (priv->manager == NULL || priv->pending_config != NULL) + return FALSE; + + g_debug ("msd-wlrandr: applying output configuration"); + + config_obj = zwlr_output_manager_v1_create_configuration (priv->manager, priv->last_serial); + + for (l = priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + WlrOutputInfo *info = find_output_in_config (config, head->name); + struct zwlr_output_configuration_head_v1 *head_obj; + + if (info != NULL && info->enabled) { + fill_output_mode (head, info); + + head_obj = zwlr_output_configuration_v1_enable_head (config_obj, head->proxy); + + { + struct zwlr_output_mode_v1 *mode = find_mode_for_output (head, info); + + if (mode != NULL) + zwlr_output_configuration_head_v1_set_mode (head_obj, mode); + else + zwlr_output_configuration_head_v1_set_custom_mode (head_obj, + info->width, + info->height, + info->rate); + } + + zwlr_output_configuration_head_v1_set_position (head_obj, info->x, info->y); + zwlr_output_configuration_head_v1_set_transform (head_obj, info->transform); + zwlr_output_configuration_head_v1_set_scale (head_obj, wl_fixed_from_double (info->scale)); + + if (priv->manager_version >= 4 && head == adaptive_sync_head) + zwlr_output_configuration_head_v1_set_adaptive_sync (head_obj, + info->adaptive_sync ? 1 : 0); + } else { + zwlr_output_configuration_v1_disable_head (config_obj, head->proxy); + } + } + + ad = g_new0 (ApplyData, 1); + ad->manager = manager; + ad->save = save; + ad->confirm_rollback = confirm_rollback; + ad->previous = previous; + ad->adaptive_sync_head = adaptive_sync_head; + + zwlr_output_configuration_v1_add_listener (config_obj, &config_listener, ad); + priv->pending_config = config_obj; + zwlr_output_configuration_v1_apply (config_obj); + wl_display_flush (priv->display); + + return TRUE; +} + +static int +enabled_head_count (MsdWlrandrManager *manager) +{ + GList *l; + int count = 0; + + for (l = manager->priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + if (head->enabled) + count++; + } + + return count; +} + +/* ------------------------------------------------------------------ * + * Confirmation / rollback of risky changes * + * ------------------------------------------------------------------ */ + +typedef struct { + MsdWlrandrManager *manager; + WlrConfig *previous; + GtkWidget *dialog; + GtkLabel *countdown_label; + guint countdown; + guint timeout_id; +} ConfirmationData; + +static void +restore_previous_configuration (MsdWlrandrManager *manager, WlrConfig *previous) +{ + g_debug ("msd-wlrandr: restoring previous configuration"); + + if (!apply_configuration (manager, previous, TRUE, FALSE, NULL, NULL)) + g_warning ("msd-wlrandr: could not restore previous configuration"); +} + +static gboolean +confirmation_timeout_cb (gpointer data) +{ + ConfirmationData *cd = data; + char *markup; + + if (cd->countdown > 1) { + cd->countdown--; + markup = g_markup_printf_escaped (_("Reverting in %u second%s…"), + cd->countdown, + cd->countdown == 1 ? "" : "s"); + gtk_label_set_markup (cd->countdown_label, markup); + g_free (markup); + return G_SOURCE_CONTINUE; + } + + cd->timeout_id = 0; + gtk_dialog_response (GTK_DIALOG (cd->dialog), GTK_RESPONSE_CANCEL); + return G_SOURCE_REMOVE; +} + +static void +confirmation_dialog_response_cb (GtkDialog *dialog, gint response_id, gpointer data) +{ + ConfirmationData *cd = data; + + if (response_id != GTK_RESPONSE_ACCEPT) + restore_previous_configuration (cd->manager, cd->previous); + + gtk_widget_destroy (cd->dialog); +} + +static void +confirmation_dialog_destroy_cb (GtkWidget *widget, gpointer data) +{ + ConfirmationData *cd = data; + + cd->manager->priv->confirm_dialog = NULL; + + if (cd->timeout_id > 0) { + g_source_remove (cd->timeout_id); + cd->timeout_id = 0; + } + + wlr_config_free (cd->previous); + g_free (cd); +} + +static void +queue_rollback_confirmation (MsdWlrandrManager *manager, WlrConfig *previous) +{ + ConfirmationData *cd; + GtkWidget *dialog; + GtkWidget *content; + GtkWidget *countdown_label; + char *markup; + + if (manager->priv->confirm_dialog != NULL) { + wlr_config_free (previous); + return; + } + + cd = g_new0 (ConfirmationData, 1); + cd->manager = manager; + cd->previous = previous; + cd->countdown = CONFIRMATION_DIALOG_SECONDS; + + dialog = gtk_message_dialog_new (NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + _("Does the display look OK?")); + gtk_window_set_icon_name (GTK_WINDOW (dialog), "preferences-desktop-display"); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Restore Previous Configuration"), GTK_RESPONSE_CANCEL); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Keep This Configuration"), GTK_RESPONSE_ACCEPT); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + + countdown_label = gtk_label_new (NULL); + markup = g_markup_printf_escaped (_("Reverting in %u seconds…"), cd->countdown); + gtk_label_set_markup (GTK_LABEL (countdown_label), markup); + g_free (markup); + gtk_widget_show (countdown_label); + + content = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_container_add (GTK_CONTAINER (content), countdown_label); + + cd->dialog = dialog; + cd->countdown_label = GTK_LABEL (countdown_label); + + g_signal_connect (dialog, "response", G_CALLBACK (confirmation_dialog_response_cb), cd); + g_signal_connect (dialog, "destroy", G_CALLBACK (confirmation_dialog_destroy_cb), cd); + + manager->priv->confirm_dialog = dialog; + gtk_widget_show_all (dialog); + + cd->timeout_id = g_timeout_add (1000, confirmation_timeout_cb, cd); +} + +/* ------------------------------------------------------------------ * + * Configuration snapshots * + * ------------------------------------------------------------------ */ + +static WlrOutputInfo * +snapshot_head (WlOutputHead *head) +{ + WlrOutputInfo *info = wlr_output_info_new (head->name); + + info->vendor = g_strdup (head->make); + info->product = g_strdup (head->model); + info->serial = g_strdup (head->serial_number); + info->enabled = head->enabled; + info->x = head->x; + info->y = head->y; + info->scale = head->scale; + info->transform = head->transform; + info->adaptive_sync = head->adaptive_sync; + + if (head->enabled && head->current_mode != NULL) { + info->width = head->current_mode->width; + info->height = head->current_mode->height; + info->rate = head->current_mode->refresh; + } else if (head->enabled) { + fill_output_mode (head, info); + } + + return info; +} + +static WlrConfig * +snapshot_config (MsdWlrandrManager *manager) +{ + WlrConfig *config; + GList *l; + GPtrArray *outputs; + + outputs = g_ptr_array_new (); + for (l = manager->priv->heads; l != NULL; l = l->next) + g_ptr_array_add (outputs, snapshot_head (l->data)); + g_ptr_array_add (outputs, NULL); + + config = g_new0 (WlrConfig, 1); + config->outputs = (WlrOutputInfo **) g_ptr_array_free (outputs, FALSE); + + return config; +} + +/* ------------------------------------------------------------------ * + * Config file storage (mirrors monitors.xml) * + * ------------------------------------------------------------------ */ + +typedef struct { + WlrOutputInfo *output; + GPtrArray *outputs; + GPtrArray *configs; + GString *text; + char *current_element; +} WlrConfigParser; + +static gboolean +yes_no_to_boolean (const char *text) +{ + if (text == NULL) + return FALSE; + + if (g_ascii_strcasecmp (text, "yes") == 0 || + g_ascii_strcasecmp (text, "true") == 0 || + strcmp (text, "1") == 0) + return TRUE; + + return FALSE; +} + +static const char * +transform_to_name (int transform) +{ + switch (transform) { + case WL_OUTPUT_TRANSFORM_NORMAL: + return "normal"; + case WL_OUTPUT_TRANSFORM_90: + return "90"; + case WL_OUTPUT_TRANSFORM_180: + return "180"; + case WL_OUTPUT_TRANSFORM_270: + return "270"; + case WL_OUTPUT_TRANSFORM_FLIPPED: + return "flipped"; + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + return "flipped-90"; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + return "flipped-180"; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + return "flipped-270"; + default: + return "normal"; + } +} + +static int +transform_from_name (const char *name) +{ + if (name == NULL) + return WL_OUTPUT_TRANSFORM_NORMAL; + + if (strcmp (name, "normal") == 0) + return WL_OUTPUT_TRANSFORM_NORMAL; + if (strcmp (name, "90") == 0) + return WL_OUTPUT_TRANSFORM_90; + if (strcmp (name, "180") == 0) + return WL_OUTPUT_TRANSFORM_180; + if (strcmp (name, "270") == 0) + return WL_OUTPUT_TRANSFORM_270; + if (strcmp (name, "flipped") == 0) + return WL_OUTPUT_TRANSFORM_FLIPPED; + if (strcmp (name, "flipped-90") == 0) + return WL_OUTPUT_TRANSFORM_FLIPPED_90; + if (strcmp (name, "flipped-180") == 0) + return WL_OUTPUT_TRANSFORM_FLIPPED_180; + if (strcmp (name, "flipped-270") == 0) + return WL_OUTPUT_TRANSFORM_FLIPPED_270; + + return WL_OUTPUT_TRANSFORM_NORMAL; +} + +static void +parser_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + WlrConfigParser *parser = user_data; + const char *name = NULL; + int i; + + if (strcmp (element_name, "configuration") == 0) { + parser->outputs = g_ptr_array_new (); + return; + } + + if (strcmp (element_name, "head") == 0) { + for (i = 0; attribute_names[i] != NULL; i++) { + if (strcmp (attribute_names[i], "name") == 0) { + name = attribute_values[i]; + break; + } + } + parser->output = wlr_output_info_new (name); + if (parser->outputs != NULL) + g_ptr_array_add (parser->outputs, parser->output); + return; + } + + g_free (parser->current_element); + parser->current_element = g_ascii_strdown (element_name, -1); + if (parser->text != NULL) + g_string_truncate (parser->text, 0); + else + parser->text = g_string_new (NULL); +} + +static void +parser_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + WlrConfigParser *parser = user_data; + WlrOutputInfo *output = parser->output; + const char *text = parser->text != NULL ? parser->text->str : ""; + + if (strcmp (element_name, "configuration") == 0) { + WlrConfig *config; + + if (parser->outputs != NULL) { + g_ptr_array_add (parser->outputs, NULL); + config = g_new0 (WlrConfig, 1); + config->outputs = (WlrOutputInfo **) g_ptr_array_free (parser->outputs, FALSE); + parser->outputs = NULL; + g_ptr_array_add (parser->configs, config); + } + return; + } + + if (strcmp (element_name, "head") == 0) { + parser->output = NULL; + return; + } + + if (parser->current_element == NULL || output == NULL) + return; + + if (strcmp (parser->current_element, "vendor") == 0) { + g_free (output->vendor); + output->vendor = g_strdup (text); + } else if (strcmp (parser->current_element, "product") == 0) { + g_free (output->product); + output->product = g_strdup (text); + } else if (strcmp (parser->current_element, "serial") == 0) { + g_free (output->serial); + output->serial = g_strdup (text); + } else if (strcmp (parser->current_element, "enabled") == 0) { + output->enabled = yes_no_to_boolean (text); + } else if (strcmp (parser->current_element, "width") == 0) { + output->width = atoi (text); + } else if (strcmp (parser->current_element, "height") == 0) { + output->height = atoi (text); + } else if (strcmp (parser->current_element, "rate") == 0) { + output->rate = atoi (text); + } else if (strcmp (parser->current_element, "x") == 0) { + output->x = atoi (text); + } else if (strcmp (parser->current_element, "y") == 0) { + output->y = atoi (text); + } else if (strcmp (parser->current_element, "scale") == 0) { + output->scale = g_ascii_strtod (text, NULL); + } else if (strcmp (parser->current_element, "transform") == 0) { + output->transform = transform_from_name (text); + } else if (strcmp (parser->current_element, "adaptive_sync") == 0) { + output->adaptive_sync = yes_no_to_boolean (text); + } +} + +static void +parser_text (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + WlrConfigParser *parser = user_data; + + if (parser->text != NULL) + g_string_append_len (parser->text, text, text_len); +} + +static WlrConfigParser * +parser_new (void) +{ + WlrConfigParser *parser = g_new0 (WlrConfigParser, 1); + parser->configs = g_ptr_array_new (); + return parser; +} + +static void +parser_free (WlrConfigParser *parser) +{ + g_ptr_array_free (parser->configs, FALSE); + if (parser->outputs != NULL) + g_ptr_array_free (parser->outputs, FALSE); + if (parser->text != NULL) + g_string_free (parser->text, TRUE); + g_free (parser->current_element); + g_free (parser); +} + +static WlrConfig ** +configs_load_from_file (const char *filename) +{ + GMarkupParseContext *context; + WlrConfigParser *parser; + gchar *contents = NULL; + GError *error = NULL; + WlrConfig **result = NULL; + + if (!g_file_get_contents (filename, &contents, NULL, &error)) { + g_debug ("msd-wlrandr: cannot read config file %s: %s", filename, error->message); + g_error_free (error); + return NULL; + } + + parser = parser_new (); + + context = g_markup_parse_context_new (&(GMarkupParser) { + .start_element = parser_start_element, + .end_element = parser_end_element, + .text = parser_text, + }, 0, parser, NULL); + + if (g_markup_parse_context_parse (context, contents, -1, &error) && + g_markup_parse_context_end_parse (context, &error)) { + g_ptr_array_add (parser->configs, NULL); + result = (WlrConfig **) g_ptr_array_free (parser->configs, FALSE); + } else { + g_warning ("msd-wlrandr: failed to parse config file %s: %s", filename, error->message); + g_error_free (error); + } + + g_markup_parse_context_free (context); + g_free (contents); + g_free (parser); + + return result; +} + +static void +free_config_list (WlrConfig **configs) +{ + int i; + + if (configs == NULL) + return; + + for (i = 0; configs[i] != NULL; i++) + wlr_config_free (configs[i]); + g_free (configs); +} + +static gboolean +config_matches_heads (WlrConfig *config, GList *heads) +{ + GPtrArray *config_names; + GPtrArray *head_names; + GList *l; + int i, j; + gboolean match; + + config_names = g_ptr_array_new (); + for (i = 0; config->outputs != NULL && config->outputs[i] != NULL; i++) + g_ptr_array_add (config_names, config->outputs[i]->name); + + head_names = g_ptr_array_new (); + for (l = heads; l != NULL; l = l->next) + g_ptr_array_add (head_names, ((WlOutputHead *) l->data)->name); + + match = config_names->len == head_names->len; + if (match) { + for (i = 0; i < config_names->len && match; i++) { + gboolean found = FALSE; + for (j = 0; j < head_names->len; j++) { + if (g_strcmp0 (g_ptr_array_index (config_names, i), g_ptr_array_index (head_names, j)) == 0) { + found = TRUE; + break; + } + } + match = found; + } + } + + g_ptr_array_free (config_names, FALSE); + g_ptr_array_free (head_names, FALSE); + + return match; +} + +static int +config_enabled_head_count (WlrConfig *config) +{ + int i; + int count = 0; + + for (i = 0; config->outputs != NULL && config->outputs[i] != NULL; i++) { + if (config->outputs[i]->enabled) + count++; + } + + return count; +} + +static void +emit_configuration (WlrConfig *config, GString *string) +{ + int i; + + g_string_append (string, " <configuration>\n"); + + for (i = 0; config->outputs != NULL && config->outputs[i] != NULL; i++) { + WlrOutputInfo *output = config->outputs[i]; + char *escaped; + + g_string_append_printf (string, " <head name=\"%s\">\n", output->name); + + if (output->vendor != NULL && *output->vendor != '\0') { + escaped = g_markup_escape_text (output->vendor, -1); + g_string_append_printf (string, " <vendor>%s</vendor>\n", escaped); + g_free (escaped); + } + if (output->product != NULL && *output->product != '\0') { + escaped = g_markup_escape_text (output->product, -1); + g_string_append_printf (string, " <product>%s</product>\n", escaped); + g_free (escaped); + } + if (output->serial != NULL && *output->serial != '\0') { + escaped = g_markup_escape_text (output->serial, -1); + g_string_append_printf (string, " <serial>%s</serial>\n", escaped); + g_free (escaped); + } + + if (output->enabled && output->width > 0) { + g_string_append (string, " <enabled>yes</enabled>\n"); + g_string_append_printf (string, " <width>%d</width>\n", output->width); + g_string_append_printf (string, " <height>%d</height>\n", output->height); + g_string_append_printf (string, " <rate>%d</rate>\n", output->rate); + g_string_append_printf (string, " <x>%d</x>\n", output->x); + g_string_append_printf (string, " <y>%d</y>\n", output->y); + g_string_append_printf (string, " <scale>%.6f</scale>\n", output->scale); + g_string_append_printf (string, " <transform>%s</transform>\n", transform_to_name (output->transform)); + } else { + g_string_append (string, " <enabled>no</enabled>\n"); + } + + g_string_append_printf (string, " <adaptive_sync>%s</adaptive_sync>\n", + output->adaptive_sync ? "yes" : "no"); + + g_string_append (string, " </head>\n"); + } + + g_string_append (string, " </configuration>\n"); +} + +static gboolean +configurations_match_current (WlrConfig **configs, GList *heads) +{ + int i; + + for (i = 0; configs[i] != NULL; i++) { + if (config_matches_heads (configs[i], heads)) + return TRUE; + } + + return FALSE; +} + +static void +save_current_configuration (MsdWlrandrManager *manager) +{ + struct MsdWlrandrManagerPrivate *priv = manager->priv; + WlrConfig *config; + WlrConfig **existing; + GString *string; + char *intended; + char *backup; + int i; + GError *error = NULL; + + config = snapshot_config (manager); + + intended = g_build_filename (g_get_user_config_dir (), CONFIG_INTENDED_BASENAME, NULL); + backup = g_build_filename (g_get_user_config_dir (), CONFIG_BACKUP_BASENAME, NULL); + + string = g_string_new ("<monitors version=\"1\">\n"); + + existing = configs_load_from_file (intended); + if (existing != NULL) { + for (i = 0; existing[i] != NULL; i++) { + if (config_matches_heads (existing[i], priv->heads)) + continue; + emit_configuration (existing[i], string); + } + free_config_list (existing); + } + + emit_configuration (config, string); + g_string_append (string, "</monitors>\n"); + + g_rename (intended, backup); + + if (!g_file_set_contents (intended, string->str, -1, &error)) { + g_warning ("msd-wlrandr: could not save config to %s: %s", intended, error->message); + g_error_free (error); + } else { + g_debug ("msd-wlrandr: saved configuration to %s", intended); + } + + g_string_free (string, TRUE); + g_free (intended); + g_free (backup); + wlr_config_free (config); +} + +static gboolean +apply_configurations_from_list (MsdWlrandrManager *manager, WlrConfig **configs) +{ + int i; + + if (configs == NULL) + return FALSE; + + for (i = 0; configs[i] != NULL; i++) { + if (config_matches_heads (configs[i], manager->priv->heads)) { + if (config_enabled_head_count (configs[i]) == 0) { + g_debug ("msd-wlrandr: skipping configuration that would disable all heads"); + return FALSE; + } + g_debug ("msd-wlrandr: found matching configuration, applying"); + apply_configuration (manager, configs[i], FALSE, FALSE, NULL, NULL); + return TRUE; + } + } + + return FALSE; +} + +static gboolean +apply_stored_configuration_at_startup (MsdWlrandrManager *manager) +{ + gboolean res; + char *backup; + char *intended; + WlrConfig **configs; + + backup = g_build_filename (g_get_user_config_dir (), CONFIG_BACKUP_BASENAME, NULL); + intended = g_build_filename (g_get_user_config_dir (), CONFIG_INTENDED_BASENAME, NULL); + + res = FALSE; + + configs = configs_load_from_file (intended); + if (apply_configurations_from_list (manager, configs)) + res = TRUE; + free_config_list (configs); + + if (!res) { + configs = configs_load_from_file (backup); + if (apply_configurations_from_list (manager, configs)) + res = TRUE; + free_config_list (configs); + } + + g_free (backup); + g_free (intended); + + return res; +} + +static gboolean +apply_default_configuration_from_file (MsdWlrandrManager *manager) +{ + char *filename; + WlrConfig **configs; + gboolean res; + + filename = g_settings_get_string (manager->priv->settings, CONF_KEY_DEFAULT_CONFIGURATION_FILE); + configs = configs_load_from_file (filename); + res = apply_configurations_from_list (manager, configs); + free_config_list (configs); + g_free (filename); + + return res; +} + +static void +auto_enable_external_monitors (MsdWlrandrManager *manager) +{ + WlrConfig *config; + GList *l; + gboolean changed = FALSE; + + if (!g_settings_get_boolean (manager->priv->settings, CONF_KEY_TURN_ON_EXTERNAL_MONITORS_AT_STARTUP)) + return; + + config = snapshot_config (manager); + + for (l = manager->priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + WlrOutputInfo *info = find_output_in_config (config, head->name); + + if (info != NULL && !info->enabled) { + info->enabled = TRUE; + fill_output_mode (head, info); + changed = TRUE; + } + } + + if (changed) + apply_configuration (manager, config, FALSE, FALSE, NULL, NULL); + + wlr_config_free (config); +} + +static void +apply_initial_config (MsdWlrandrManager *manager) +{ + g_debug ("msd-wlrandr: applying initial configuration"); + + if (apply_stored_configuration_at_startup (manager)) + return; + + if (apply_default_configuration_from_file (manager)) + return; + + auto_enable_external_monitors (manager); +} + +/* ------------------------------------------------------------------ * + * DBus interface * + * ------------------------------------------------------------------ */ + +static gboolean +msd_wlrandr_manager_apply_configuration (MsdWlrandrManager *manager, + GError **error) +{ + if (!apply_stored_configuration_at_startup (manager)) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "No stored configuration applies to the current outputs"); + return FALSE; + } + + return TRUE; +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + MsdWlrandrManager *manager = (MsdWlrandrManager *) user_data; + g_autoptr (GError) error = NULL; + + g_debug ("Calling method '%s' for wlrandr", method_name); + + if (g_strcmp0 (method_name, "ApplyConfiguration") == 0) { + if (!msd_wlrandr_manager_apply_configuration (manager, &error)) + g_dbus_method_invocation_return_gerror (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, NULL); + } +} + +static void +handle_method_call2 (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + MsdWlrandrManager *manager = (MsdWlrandrManager *) user_data; + g_autoptr (GError) error = NULL; + + g_debug ("Calling method '%s' for wlrandr", method_name); + + if (g_strcmp0 (method_name, "ApplyConfiguration") == 0) { + gint64 parent_window_id; + gint64 timestamp; + + g_variant_get (parameters, "(xx)", &parent_window_id, ×tamp); + /* Wayland has no X window ids or X timestamps; the arguments + * are accepted only for API compatibility with xrandr. + */ + (void) parent_window_id; + (void) timestamp; + + if (!msd_wlrandr_manager_apply_configuration (manager, &error)) + g_dbus_method_invocation_return_gerror (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, NULL); + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + .method_call = handle_method_call, +}; + +static const GDBusInterfaceVTable interface_vtable2 = +{ + .method_call = handle_method_call2, +}; + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + MsdWlrandrManager *manager) +{ + GDBusConnection *connection; + GError *error = NULL; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + manager->priv->connection = connection; + + if (manager->priv->introspection_data == NULL) { + g_warning ("Could not parse wlrandr D-Bus introspection XML"); + return; + } + + manager->priv->registration_id = + g_dbus_connection_register_object (connection, + MSD_WLRANDR_DBUS_PATH, + manager->priv->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + NULL); + manager->priv->registration_id_2 = + g_dbus_connection_register_object (connection, + MSD_WLRANDR_DBUS_PATH, + manager->priv->introspection_data->interfaces[1], + &interface_vtable2, + manager, + NULL, + NULL); + + manager->priv->owner_id = g_bus_own_name_on_connection (manager->priv->connection, + MSD_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT, + NULL, NULL, NULL, NULL); +} + +static void +register_manager_dbus (MsdWlrandrManager *manager) +{ + manager->priv->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + if (manager->priv->introspection_data == NULL) { + g_warning ("Could not parse wlrandr D-Bus introspection XML"); + return; + } + manager->priv->bus_cancellable = g_cancellable_new (); + g_bus_get (G_BUS_TYPE_SESSION, + manager->priv->bus_cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); +} + +/* ------------------------------------------------------------------ * + * Menu actions * + * ------------------------------------------------------------------ */ + +static void +monitor_activate_cb (GtkCheckMenuItem *item, MsdWlrandrManager *manager) +{ + WlOutputHead *head = g_object_get_data (G_OBJECT (item), "head"); + gboolean on = gtk_check_menu_item_get_active (item); + WlrConfig *config; + WlrOutputInfo *info; + + if (head == NULL) + return; + + if (head->enabled == on) + return; + + if (!on && enabled_head_count (manager) <= 1) { + gulong activate_id; + + g_warning ("msd-wlrandr: refusing to disable the last output"); + notify_config_error (_("The last active output cannot be turned off")); + + activate_id = (gulong) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (item), "activate-id")); + if (activate_id > 0) { + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (item, TRUE); + g_signal_handler_unblock (item, activate_id); + } + return; + } + + config = snapshot_config (manager); + info = find_output_in_config (config, head->name); + if (info == NULL) { + wlr_config_free (config); + return; + } + + info->enabled = on; + if (on) { + fill_output_mode (head, info); + if (info->x == 0 && info->y == 0) { + GList *l; + int max_x = 0; + + for (l = manager->priv->heads; l != NULL; l = l->next) { + WlOutputHead *h = l->data; + if (h->enabled && h->current_mode != NULL) { + int right = h->x + h->current_mode->width; + if (right > max_x) + max_x = right; + } + } + info->x = max_x; + info->y = 0; + } + } + + apply_configuration (manager, config, TRUE, FALSE, NULL, NULL); + wlr_config_free (config); +} + +static void +mode_activate_cb (GtkCheckMenuItem *item, MsdWlrandrManager *manager) +{ + WlOutputHead *head = g_object_get_data (G_OBJECT (item), "head"); + WlOutputMode *mode = g_object_get_data (G_OBJECT (item), "mode"); + WlrConfig *config; + WlrOutputInfo *info; + + if (head == NULL || mode == NULL) + return; + + if (!gtk_check_menu_item_get_active (item)) + return; + + config = snapshot_config (manager); + info = find_output_in_config (config, head->name); + if (info == NULL) { + wlr_config_free (config); + return; + } + + info->width = mode->width; + info->height = mode->height; + info->rate = mode->refresh; + + apply_configuration (manager, config, TRUE, FALSE, NULL, NULL); + wlr_config_free (config); +} + +static void +rotation_activate_cb (GtkCheckMenuItem *item, MsdWlrandrManager *manager) +{ + WlOutputHead *head = g_object_get_data (G_OBJECT (item), "head"); + int transform = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "transform")); + WlrConfig *config; + WlrOutputInfo *info; + + if (head == NULL) + return; + + if (!gtk_check_menu_item_get_active (item)) + return; + + config = snapshot_config (manager); + info = find_output_in_config (config, head->name); + if (info == NULL) { + wlr_config_free (config); + return; + } + + info->transform = transform; + + apply_configuration (manager, config, TRUE, FALSE, NULL, NULL); + wlr_config_free (config); +} + +static void +adaptive_sync_activate_cb (GtkCheckMenuItem *item, MsdWlrandrManager *manager) +{ + WlOutputHead *head = g_object_get_data (G_OBJECT (item), "head"); + gboolean on = gtk_check_menu_item_get_active (item); + WlrConfig *config; + WlrOutputInfo *info; + + if (head == NULL) + return; + + if (head->adaptive_sync_supported != 1) { + gulong activate_id; + + g_debug ("msd-wlrandr: adaptive sync support not confirmed on %s", head->name); + + activate_id = (gulong) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (item), "activate-id")); + if (activate_id > 0) { + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (item, head->adaptive_sync); + g_signal_handler_unblock (item, activate_id); + } + return; + } + + config = snapshot_config (manager); + info = find_output_in_config (config, head->name); + if (info == NULL) { + wlr_config_free (config); + return; + } + + info->adaptive_sync = on; + + apply_configuration (manager, config, TRUE, FALSE, NULL, head); + wlr_config_free (config); +} + +static void +run_display_capplet (MsdWlrandrManager *manager) +{ + GError *error = NULL; + + if (!mate_gdk_spawn_command_line_on_screen (gdk_screen_get_default (), + MSD_WLRANDR_DISPLAY_CAPPLET, &error)) { + g_warning ("Could not spawn %s: %s", MSD_WLRANDR_DISPLAY_CAPPLET, error->message); + g_error_free (error); + } +} + +static void +configure_display_cb (GtkMenuItem *item, MsdWlrandrManager *manager) +{ + run_display_capplet (manager); +} + +/* ------------------------------------------------------------------ * + * SNI menu * + * ------------------------------------------------------------------ */ + +static void +append_output_title_item (GtkMenu *menu, WlOutputHead *head) +{ + GtkWidget *item; + GtkWidget *box; + GtkWidget *image; + GtkWidget *label; + char *markup; + char *display_name; + + item = gtk_menu_item_new (); + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + image = gtk_image_new_from_icon_name ("computer", GTK_ICON_SIZE_MENU); + gtk_container_add (GTK_CONTAINER (box), image); + + display_name = head->description != NULL && *head->description != '\0' + ? g_strdup (head->description) : g_strdup (head->name); + + markup = g_markup_printf_escaped ("<b>%s</b>", display_name); + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), markup); + gtk_widget_set_margin_start (label, 6); + gtk_widget_set_margin_end (label, 6); + gtk_container_add (GTK_CONTAINER (box), label); + + gtk_container_add (GTK_CONTAINER (item), box); + gtk_widget_set_sensitive (item, FALSE); + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + + g_free (markup); + g_free (display_name); +} + +static void +append_enable_item (GtkMenu *menu, MsdWlrandrManager *manager, WlOutputHead *head) +{ + GtkWidget *item; + gulong activate_id; + + item = gtk_check_menu_item_new (); + + if (head->enabled) { + gtk_menu_item_set_label (GTK_MENU_ITEM (item), _("ON")); + gtk_widget_set_tooltip_text (item, _("Turn this monitor off")); + } else { + gtk_menu_item_set_label (GTK_MENU_ITEM (item), _("OFF")); + gtk_widget_set_tooltip_text (item, _("Turn this monitor on")); + } + + g_object_set_data (G_OBJECT (item), "head", head); + + activate_id = g_signal_connect (item, "activate", G_CALLBACK (monitor_activate_cb), manager); + g_object_set_data (G_OBJECT (item), "activate-id", GUINT_TO_POINTER ((guint) activate_id)); + + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), head->enabled); + g_signal_handler_unblock (item, activate_id); + + if (head->enabled && enabled_head_count (manager) <= 1) + gtk_widget_set_sensitive (item, FALSE); + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +append_resolution_submenu (GtkMenu *menu, MsdWlrandrManager *manager, WlOutputHead *head) +{ + GtkWidget *submenu; + GtkWidget *item; + GList *l; + GSList *group = NULL; + + submenu = gtk_menu_new (); + + for (l = head->modes; l != NULL; l = l->next) { + WlOutputMode *mode = l->data; + gboolean swapped = head->transform == WL_OUTPUT_TRANSFORM_90 || + head->transform == WL_OUTPUT_TRANSFORM_270; + int width = swapped ? mode->height : mode->width; + int height = swapped ? mode->width : mode->height; + char *label; + gulong activate_id; + + if (mode->refresh > 0) + label = g_strdup_printf ("%dx%d @ %g Hz", width, height, mode->refresh / 1000.0); + else + label = g_strdup_printf ("%dx%d", width, height); + + item = gtk_radio_menu_item_new_with_label (group, label); + group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (item)); + g_free (label); + + g_object_set_data (G_OBJECT (item), "head", head); + g_object_set_data (G_OBJECT (item), "mode", mode); + + if (mode->preferred) + gtk_widget_set_tooltip_text (item, _("Preferred resolution")); + + if (head->current_mode == mode) { + activate_id = g_signal_connect (item, "toggled", G_CALLBACK (mode_activate_cb), manager); + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), TRUE); + g_signal_handler_unblock (item, activate_id); + } else { + g_signal_connect (item, "toggled", G_CALLBACK (mode_activate_cb), manager); + } + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item); + } + + item = gtk_menu_item_new_with_label (_("Resolution")); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +append_rotation_submenu (GtkMenu *menu, MsdWlrandrManager *manager, WlOutputHead *head) +{ + typedef struct { + int transform; + const char *name; + } RotationInfo; + + static const RotationInfo rotations[] = { + { WL_OUTPUT_TRANSFORM_NORMAL, N_("Normal") }, + { WL_OUTPUT_TRANSFORM_90, N_("Left") }, + { WL_OUTPUT_TRANSFORM_270, N_("Right") }, + { WL_OUTPUT_TRANSFORM_180, N_("Upside Down") }, + }; + + GtkWidget *submenu; + GtkWidget *item; + GSList *group = NULL; + int i; + + submenu = gtk_menu_new (); + + for (i = 0; i < G_N_ELEMENTS (rotations); i++) { + gulong activate_id; + + item = gtk_radio_menu_item_new_with_label (group, _(rotations[i].name)); + group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (item)); + + if (!head->enabled) + gtk_widget_set_sensitive (item, FALSE); + + g_object_set_data (G_OBJECT (item), "head", head); + g_object_set_data (G_OBJECT (item), "transform", GINT_TO_POINTER (rotations[i].transform)); + + if (head->transform == rotations[i].transform) { + activate_id = g_signal_connect (item, "toggled", G_CALLBACK (rotation_activate_cb), manager); + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), TRUE); + g_signal_handler_unblock (item, activate_id); + } else { + g_signal_connect (item, "toggled", G_CALLBACK (rotation_activate_cb), manager); + } + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item); + } + + item = gtk_menu_item_new_with_label (_("Rotation")); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +append_adaptive_sync_item (GtkMenu *menu, MsdWlrandrManager *manager, WlOutputHead *head) +{ + GtkWidget *item; + gulong activate_id; + gboolean unavailable; + + if (head_is_wayland_backend (head) && head->adaptive_sync_supported == -1) + head->adaptive_sync_supported = 0; + + unavailable = !head->enabled || head_is_wayland_backend (head) || head->adaptive_sync_supported != 1; + + item = gtk_check_menu_item_new_with_label (_("Adaptive Sync (VRR)")); + g_object_set_data (G_OBJECT (item), "head", head); + + activate_id = g_signal_connect (item, "activate", G_CALLBACK (adaptive_sync_activate_cb), manager); + g_object_set_data (G_OBJECT (item), "activate-id", GUINT_TO_POINTER ((guint) activate_id)); + + g_signal_handler_block (item, activate_id); + if (unavailable) + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), FALSE); + else + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), head->adaptive_sync); + g_signal_handler_unblock (item, activate_id); + + if (unavailable) + gtk_widget_set_sensitive (item, FALSE); + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +append_configure_item (GtkMenu *menu, MsdWlrandrManager *manager) +{ + GtkWidget *item; + GtkWidget *box; + GtkWidget *image; + GtkWidget *label; + GSettings *icon_settings; + + item = gtk_menu_item_new (); + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); + image = gtk_image_new_from_icon_name ("preferences-system", GTK_ICON_SIZE_MENU); + label = gtk_label_new_with_mnemonic (_("_Configure Display Settings…")); + + icon_settings = g_settings_new ("org.mate.interface"); + if (g_settings_get_boolean (icon_settings, "menus-have-icons")) + gtk_container_add (GTK_CONTAINER (box), image); + g_object_unref (icon_settings); + + gtk_container_add (GTK_CONTAINER (box), label); + gtk_container_add (GTK_CONTAINER (item), box); + + g_signal_connect (item, "activate", G_CALLBACK (configure_display_cb), manager); + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +rebuild_menu (MsdWlrandrManager *manager) +{ + struct MsdWlrandrManagerPrivate *priv = manager->priv; + GtkWidget *menu; + GList *l; + + if (priv->indicator == NULL) + return; + + menu = gtk_menu_new (); + + for (l = priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + + append_output_title_item (GTK_MENU (menu), head); + append_enable_item (GTK_MENU (menu), manager, head); + append_resolution_submenu (GTK_MENU (menu), manager, head); + append_rotation_submenu (GTK_MENU (menu), manager, head); + append_adaptive_sync_item (GTK_MENU (menu), manager, head); + } + + gtk_menu_shell_append (GTK_MENU_SHELL (menu), gtk_separator_menu_item_new ()); + + append_configure_item (GTK_MENU (menu), manager); + + gtk_widget_show_all (menu); + + if (priv->menu != NULL) + gtk_widget_destroy (priv->menu); + priv->menu = menu; + + app_indicator_set_menu (priv->indicator, GTK_MENU (menu)); +} + +/* ------------------------------------------------------------------ * + * GSettings callbacks * + * ------------------------------------------------------------------ */ + +static void +start_or_stop_icon (MsdWlrandrManager *manager) +{ + gboolean show = g_settings_get_boolean (manager->priv->settings, CONF_KEY_SHOW_NOTIFICATION_ICON); + + if (show) { + app_indicator_set_status (manager->priv->indicator, APP_INDICATOR_STATUS_ACTIVE); + rebuild_menu (manager); + } else { + app_indicator_set_status (manager->priv->indicator, APP_INDICATOR_STATUS_PASSIVE); + } +} + +static void +on_config_changed (GSettings *settings, gchar *key, MsdWlrandrManager *manager) +{ + if (strcmp (key, CONF_KEY_SHOW_NOTIFICATION_ICON) == 0) + start_or_stop_icon (manager); +} + +/* ------------------------------------------------------------------ * + * Object lifecycle * + * ------------------------------------------------------------------ */ + +G_DEFINE_TYPE_WITH_PRIVATE (MsdWlrandrManager, msd_wlrandr_manager, G_TYPE_OBJECT) + +static void +msd_wlrandr_manager_finalize (GObject *object) +{ + MsdWlrandrManager *manager = MSD_WLRANDR_MANAGER (object); + struct MsdWlrandrManagerPrivate *priv = manager->priv; + GList *l; + + if (priv->owner_id > 0) { + g_bus_unown_name (priv->owner_id); + priv->owner_id = 0; + } + + if (priv->connection != NULL) { + if (priv->registration_id > 0) + g_dbus_connection_unregister_object (priv->connection, priv->registration_id); + if (priv->registration_id_2 > 0) + g_dbus_connection_unregister_object (priv->connection, priv->registration_id_2); + g_object_unref (priv->connection); + priv->connection = NULL; + } + + if (priv->introspection_data != NULL) { + g_dbus_node_info_unref (priv->introspection_data); + priv->introspection_data = NULL; + } + + if (priv->bus_cancellable != NULL) { + g_cancellable_cancel (priv->bus_cancellable); + g_object_unref (priv->bus_cancellable); + priv->bus_cancellable = NULL; + } + + if (priv->settings != NULL) { + if (priv->icon_changed_id > 0) + g_signal_handler_disconnect (priv->settings, priv->icon_changed_id); + g_object_unref (priv->settings); + } + + if (priv->indicator != NULL) + g_object_unref (priv->indicator); + if (priv->menu != NULL) + gtk_widget_destroy (priv->menu); + if (priv->confirm_dialog != NULL) + gtk_widget_destroy (priv->confirm_dialog); + + for (l = priv->heads; l != NULL; l = l->next) + wlr_output_head_free (l->data); + g_list_free (priv->heads); + + if (priv->manager != NULL) + zwlr_output_manager_v1_destroy (priv->manager); + if (priv->registry != NULL) + wl_registry_destroy (priv->registry); + + if (notify_is_initted ()) + notify_uninit (); + + G_OBJECT_CLASS (msd_wlrandr_manager_parent_class)->finalize (object); +} + +static void +msd_wlrandr_manager_init (MsdWlrandrManager *manager) +{ + manager->priv = msd_wlrandr_manager_get_instance_private (manager); +} + +static void +msd_wlrandr_manager_class_init (MsdWlrandrManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = msd_wlrandr_manager_finalize; +} + +MsdWlrandrManager * +msd_wlrandr_manager_new (void) +{ + return g_object_new (MSD_TYPE_WLRANDR_MANAGER, NULL); +} + +gboolean +msd_wlrandr_manager_start (MsdWlrandrManager *manager, GError **error) +{ + struct MsdWlrandrManagerPrivate *priv = manager->priv; + GdkDisplay *gdk_display; + GSettings *icon_settings; + + g_return_val_if_fail (MSD_IS_WLRANDR_MANAGER (manager), FALSE); + + if (priv->started) + return TRUE; + + if (!GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The wlrandr plugin requires a Wayland display"); + return FALSE; + } + + gdk_display = gdk_display_get_default (); + priv->display = gdk_wayland_display_get_wl_display (gdk_display); + if (priv->display == NULL) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to get the Wayland display"); + return FALSE; + } + + g_debug ("Starting wlrandr manager"); + + priv->settings = g_settings_new (CONF_SCHEMA); + + notify_init ("mate-settings-daemon"); + + priv->registry = wl_display_get_registry (priv->display); + wl_registry_add_listener (priv->registry, ®istry_listener, manager); + wl_display_flush (priv->display); + + priv->indicator = app_indicator_new ("org.mate.settingsdaemon.wlrandr", + MSD_WLRANDR_ICON_NAME, + APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + app_indicator_set_title (priv->indicator, _("Configure display settings")); + + icon_settings = g_settings_new ("org.mate.interface"); + if (g_settings_get_boolean (icon_settings, "menus-have-icons")) + app_indicator_set_icon_theme_path (priv->indicator, NULL); + g_object_unref (icon_settings); + + priv->icon_changed_id = g_signal_connect (priv->settings, "changed::" CONF_KEY_SHOW_NOTIFICATION_ICON, + G_CALLBACK (on_config_changed), manager); + + start_or_stop_icon (manager); + + register_manager_dbus (manager); + + priv->started = TRUE; + + return TRUE; +} + +void +msd_wlrandr_manager_stop (MsdWlrandrManager *manager) +{ + struct MsdWlrandrManagerPrivate *priv; + GList *l; + + g_return_if_fail (MSD_IS_WLRANDR_MANAGER (manager)); + + if (!manager->priv->started) + return; + + g_debug ("Stopping wlrandr manager"); + + priv = manager->priv; + + if (priv->owner_id > 0) { + g_bus_unown_name (priv->owner_id); + priv->owner_id = 0; + } + + if (priv->connection != NULL) { + if (priv->registration_id > 0) { + g_dbus_connection_unregister_object (priv->connection, priv->registration_id); + priv->registration_id = 0; + } + if (priv->registration_id_2 > 0) { + g_dbus_connection_unregister_object (priv->connection, priv->registration_id_2); + priv->registration_id_2 = 0; + } + g_object_unref (priv->connection); + priv->connection = NULL; + } + + if (priv->introspection_data != NULL) { + g_dbus_node_info_unref (priv->introspection_data); + priv->introspection_data = NULL; + } + + if (priv->bus_cancellable != NULL) { + g_cancellable_cancel (priv->bus_cancellable); + g_object_unref (priv->bus_cancellable); + priv->bus_cancellable = NULL; + } + + if (priv->settings != NULL) { + if (priv->icon_changed_id > 0) + g_signal_handler_disconnect (priv->settings, priv->icon_changed_id); + g_object_unref (priv->settings); + priv->settings = NULL; + } + + if (priv->indicator != NULL) { + app_indicator_set_status (priv->indicator, APP_INDICATOR_STATUS_PASSIVE); + g_object_unref (priv->indicator); + priv->indicator = NULL; + } + + if (priv->menu != NULL) { + gtk_widget_destroy (priv->menu); + priv->menu = NULL; + } + + if (priv->confirm_dialog != NULL) { + gtk_widget_destroy (priv->confirm_dialog); + priv->confirm_dialog = NULL; + } + + if (priv->manager != NULL) { + zwlr_output_manager_v1_stop (priv->manager); + zwlr_output_manager_v1_destroy (priv->manager); + priv->manager = NULL; + } + + if (priv->registry != NULL) { + wl_registry_destroy (priv->registry); + priv->registry = NULL; + } + + for (l = priv->heads; l != NULL; l = l->next) + wlr_output_head_free (l->data); + g_list_free (priv->heads); + priv->heads = NULL; + + if (notify_is_initted ()) + notify_uninit (); + + priv->started = FALSE; +} diff --git a/plugins/wlrandr/msd-wlrandr-manager.h b/plugins/wlrandr/msd-wlrandr-manager.h new file mode 100644 index 0000000..2763c09 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-manager.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2026 MATE Developers + * + * 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, 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef __MSD_WLRANDR_MANAGER_H +#define __MSD_WLRANDR_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define MSD_TYPE_WLRANDR_MANAGER (msd_wlrandr_manager_get_type ()) +#define MSD_WLRANDR_MANAGER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MSD_TYPE_WLRANDR_MANAGER, MsdWlrandrManager)) +#define MSD_WLRANDR_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), MSD_TYPE_WLRANDR_MANAGER, MsdWlrandrManagerClass)) +#define MSD_IS_WLRANDR_MANAGER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MSD_TYPE_WLRANDR_MANAGER)) +#define MSD_IS_WLRANDR_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), MSD_TYPE_WLRANDR_MANAGER)) +#define MSD_WLRANDR_MANAGER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), MSD_TYPE_WLRANDR_MANAGER, MsdWlrandrManagerClass)) + +typedef struct MsdWlrandrManagerPrivate MsdWlrandrManagerPrivate; + +typedef struct +{ + GObject parent; + MsdWlrandrManagerPrivate *priv; +} MsdWlrandrManager; + +typedef struct +{ + GObjectClass parent_class; +} MsdWlrandrManagerClass; + +GType msd_wlrandr_manager_get_type (void); + +MsdWlrandrManager * msd_wlrandr_manager_new (void); +gboolean msd_wlrandr_manager_start (MsdWlrandrManager *manager, + GError **error); +void msd_wlrandr_manager_stop (MsdWlrandrManager *manager); + +G_END_DECLS + +#endif /* __MSD_WLRANDR_MANAGER_H */ diff --git a/plugins/wlrandr/msd-wlrandr-plugin.c b/plugins/wlrandr/msd-wlrandr-plugin.c new file mode 100644 index 0000000..522e122 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-plugin.c @@ -0,0 +1,105 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2026 MATE Developers + * + * 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, 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "config.h" + +#include <glib/gi18n-lib.h> +#include <gmodule.h> + +#include "mate-settings-plugin.h" +#include "msd-wlrandr-plugin.h" +#include "msd-wlrandr-manager.h" + +struct MsdWlrandrPluginPrivate { + MsdWlrandrManager *manager; +}; + +MATE_SETTINGS_PLUGIN_REGISTER_WITH_PRIVATE (MsdWlrandrPlugin, msd_wlrandr_plugin) + +static void +msd_wlrandr_plugin_init (MsdWlrandrPlugin *plugin) +{ + plugin->priv = msd_wlrandr_plugin_get_instance_private (plugin); + + g_debug ("MsdWlrandrPlugin initializing"); + + plugin->priv->manager = msd_wlrandr_manager_new (); +} + +static void +msd_wlrandr_plugin_finalize (GObject *object) +{ + MsdWlrandrPlugin *plugin; + + g_return_if_fail (object != NULL); + g_return_if_fail (MSD_IS_WLRANDR_PLUGIN (object)); + + g_debug ("MsdWlrandrPlugin finalizing"); + + plugin = MSD_WLRANDR_PLUGIN (object); + + g_return_if_fail (plugin->priv != NULL); + + if (plugin->priv->manager != NULL) { + g_object_unref (plugin->priv->manager); + } + + G_OBJECT_CLASS (msd_wlrandr_plugin_parent_class)->finalize (object); +} + +static void +impl_activate (MateSettingsPlugin *plugin) +{ + gboolean res; + GError *error; + + g_debug ("Activating wlrandr plugin"); + + error = NULL; + res = msd_wlrandr_manager_start (MSD_WLRANDR_PLUGIN (plugin)->priv->manager, &error); + if (! res) { + g_warning ("Unable to start wlrandr manager: %s", error->message); + g_error_free (error); + } +} + +static void +impl_deactivate (MateSettingsPlugin *plugin) +{ + g_debug ("Deactivating wlrandr plugin"); + msd_wlrandr_manager_stop (MSD_WLRANDR_PLUGIN (plugin)->priv->manager); +} + +static void +msd_wlrandr_plugin_class_init (MsdWlrandrPluginClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + MateSettingsPluginClass *plugin_class = MATE_SETTINGS_PLUGIN_CLASS (klass); + + object_class->finalize = msd_wlrandr_plugin_finalize; + + plugin_class->activate = impl_activate; + plugin_class->deactivate = impl_deactivate; +} + +static void +msd_wlrandr_plugin_class_finalize (MsdWlrandrPluginClass *klass) +{ +} diff --git a/plugins/wlrandr/msd-wlrandr-plugin.h b/plugins/wlrandr/msd-wlrandr-plugin.h new file mode 100644 index 0000000..3adda68 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-plugin.h @@ -0,0 +1,63 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2026 MATE Developers + * + * 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, 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef __MSD_WLRANDR_PLUGIN_H__ +#define __MSD_WLRANDR_PLUGIN_H__ + +#include <glib.h> +#include <glib-object.h> +#include <gmodule.h> + +#include "mate-settings-plugin.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MSD_TYPE_WLRANDR_PLUGIN (msd_wlrandr_plugin_get_type ()) +#define MSD_WLRANDR_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MSD_TYPE_WLRANDR_PLUGIN, MsdWlrandrPlugin)) +#define MSD_WLRANDR_PLUGIN_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), MSD_TYPE_WLRANDR_PLUGIN, MsdWlrandrPluginClass)) +#define MSD_IS_WLRANDR_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MSD_TYPE_WLRANDR_PLUGIN)) +#define MSD_IS_WLRANDR_PLUGIN_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), MSD_TYPE_WLRANDR_PLUGIN)) +#define MSD_WLRANDR_PLUGIN_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), MSD_TYPE_WLRANDR_PLUGIN, MsdWlrandrPluginClass)) + +typedef struct MsdWlrandrPluginPrivate MsdWlrandrPluginPrivate; + +typedef struct +{ + MateSettingsPlugin parent; + MsdWlrandrPluginPrivate *priv; +} MsdWlrandrPlugin; + +typedef struct +{ + MateSettingsPluginClass parent_class; +} MsdWlrandrPluginClass; + +GType msd_wlrandr_plugin_get_type (void) G_GNUC_CONST; + +/* All the plugins must implement this function */ +G_MODULE_EXPORT GType register_mate_settings_plugin (GTypeModule *module); + +#ifdef __cplusplus +} +#endif + +#endif /* __MSD_WLRANDR_PLUGIN_H__ */ diff --git a/plugins/wlrandr/msd-wlrandr.svg b/plugins/wlrandr/msd-wlrandr.svg new file mode 100644 index 0000000..87ce536 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr.svg @@ -0,0 +1,491 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="128" + height="128" + id="svg3304" + sodipodi:version="0.32" + inkscape:version="0.92.4 (unknown)" + sodipodi:docname="msd-xrandr.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:modified="true" + version="1.1"> + <defs + id="defs3306"> + <linearGradient + id="linearGradient2804"> + <stop + style="stop-color:black;stop-opacity:0;" + offset="0" + id="stop2806" /> + <stop + id="stop2812" + offset="0.5" + style="stop-color:black;stop-opacity:1;" /> + <stop + style="stop-color:black;stop-opacity:0;" + offset="1" + id="stop2808" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient2804" + id="linearGradient2831" + gradientUnits="userSpaceOnUse" + x1="21.875" + y1="48.000977" + x2="21.875" + y2="40" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient2781" + id="radialGradient2829" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(2,0,0,0.8,36,8.8)" + cx="1" + cy="44" + fx="1" + fy="44" + r="5" /> + <linearGradient + inkscape:collect="always" + id="linearGradient2781"> + <stop + style="stop-color:black;stop-opacity:1;" + offset="0" + id="stop2783" /> + <stop + style="stop-color:black;stop-opacity:0;" + offset="1" + id="stop2785" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient2781" + id="radialGradient2827" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(2,0,0,0.8,-13,-79.2)" + cx="1" + cy="44" + fx="1" + fy="44" + r="5" /> + <linearGradient + id="linearGradient5137"> + <stop + style="stop-color:#eeeeec;stop-opacity:1;" + offset="0" + id="stop5139" /> + <stop + style="stop-color:#e6e6e3;stop-opacity:1;" + offset="1" + id="stop5141" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient6240"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop6242" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop6244" /> + </linearGradient> + <linearGradient + id="linearGradient11400"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop11402" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop11404" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient11400" + id="linearGradient11406" + x1="23.154902" + y1="34.572548" + x2="23.529411" + y2="40.219608" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient5137" + id="linearGradient5147" + gradientUnits="userSpaceOnUse" + x1="17.247635" + y1="6.3760414" + x2="39.904388" + y2="38.876041" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient5137" + id="linearGradient5223" + gradientUnits="userSpaceOnUse" + x1="31.743324" + y1="37.842293" + x2="31.86105" + y2="43.82579" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + id="linearGradient7025" + inkscape:collect="always"> + <stop + id="stop7027" + offset="0" + style="stop-color:#e6ce46;stop-opacity:1" /> + <stop + id="stop7029" + offset="1" + style="stop-color:#d6ba1c;stop-opacity:1" /> + </linearGradient> + <linearGradient + id="linearGradient22122" + inkscape:collect="always"> + <stop + id="stop22124" + offset="0" + style="stop-color:black;stop-opacity:1;" /> + <stop + id="stop22126" + offset="1" + style="stop-color:black;stop-opacity:0;" /> + </linearGradient> + <linearGradient + id="linearGradient22140"> + <stop + id="stop22142" + offset="0" + style="stop-color:black;stop-opacity:0;" /> + <stop + style="stop-color:black;stop-opacity:1;" + offset="0.5" + id="stop22148" /> + <stop + id="stop22144" + offset="1" + style="stop-color:black;stop-opacity:0;" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient22122" + id="radialGradient4770" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.000001,1.142856,0,-41.10259,45.50001)" + cx="7" + cy="39.464806" + fx="7" + fy="39.464806" + r="3.5" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient22122" + id="radialGradient4772" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.000001,1.142856,0,-89.10259,-31.49999)" + cx="7" + cy="39.464806" + fx="7" + fy="39.464806" + r="3.5" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient22140" + id="linearGradient4774" + gradientUnits="userSpaceOnUse" + x1="18.142136" + y1="35" + x2="18.142136" + y2="42.040661" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7025" + id="linearGradient4776" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(53,1.428571)" + x1="13.630114" + y1="28.5" + x2="25.208096" + y2="41.180992" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient6240" + id="linearGradient4778" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0.007843,0.7954955)" + x1="20.156862" + y1="5.0996137" + x2="20.156862" + y2="26.039215" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#849193" + borderopacity="1.0000000" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="5.6568542" + inkscape:cx="77.858376" + inkscape:cy="41.598514" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:grid-bbox="true" + inkscape:document-units="px" + inkscape:showpageshadow="false" + showborder="true" + inkscape:window-width="2509" + inkscape:window-height="1738" + inkscape:window-x="556" + inkscape:window-y="140" + showguides="true" + inkscape:guide-bbox="true" + inkscape:grid-points="true" + inkscape:window-maximized="0" /> + <metadata + id="metadata3309"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>Change Resolution</dc:title> + <dc:creator> + <cc:Agent> + <dc:title>Jakub Steiner</dc:title> + </cc:Agent> + </dc:creator> + <cc:license + rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" /> + <dc:subject> + <rdf:Bag> + <rdf:li>display</rdf:li> + <rdf:li>resolution</rdf:li> + <rdf:li>video</rdf:li> + </rdf:Bag> + </dc:subject> + <dc:contributor> + <cc:Agent> + <dc:title>Andreas Nilsson +Luca Ferretti <[email protected]></dc:title> + </cc:Agent> + </dc:contributor> + <dc:date /> + <dc:source>http://www.gnome.org</dc:source> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/GPL/2.0/"> + <cc:permits + rdf:resource="http://web.resource.org/cc/Reproduction" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/Distribution" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/Notice" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/DerivativeWorks" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/ShareAlike" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/SourceCode" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + id="layer1" + inkscape:label="Layer 1" + inkscape:groupmode="layer" + transform="translate(0,80)"> + <g + id="g100" + transform="matrix(2.6666587,0,0,2.6666587,0.12519204,-80.521133)"> + <g + transform="matrix(0.9308511,0,0,1.037397,1.6941489,-1.795056)" + style="opacity:0.3" + id="g2822"> + <rect + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#radialGradient2827);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" + id="rect1892" + width="10" + height="8" + x="-11" + y="-48" + transform="scale(-1)" /> + <rect + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#radialGradient2829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" + id="rect2789" + width="10" + height="8" + x="38" + y="40" /> + <rect + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#linearGradient2831);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" + id="rect2793" + width="27" + height="8" + x="11" + y="40" /> + </g> + <rect + y="0.79549509" + x="0" + height="48" + width="48" + id="rect4784" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="csccscc" + id="path9222" + d="m 14.375479,36.328843 c 0,0 1.216876,4.9346 -3.856329,4.980924 -2.4302757,0.0219 -1.9324777,4.035151 -1.9324777,4.035151 l 30.8464667,-0.03137 c 0,0 0.418438,-3.895363 -2.022217,-3.941032 -4.987467,-0.09214 -3.810529,-5.106415 -3.810529,-5.106415 z" + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#linearGradient5223);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccc" + id="rect5040" + d="M 4.8886262,4.2739318 H 43.01746 c 2.025119,0 3.500978,1.4276545 3.500978,3.6261408 l 0.01095,26.2038354 c 0,1.692067 -0.528347,2.215229 -2.034656,2.215229 L 3.5320635,36.300202 C 2.3527922,36.271409 1.513468,35.805541 1.4976345,34.280899 L 1.5128113,7.7123281 c 0,-1.7738259 1.5394074,-3.4383963 3.3758149,-3.4383963 z" + style="fill:url(#linearGradient5147);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" /> + <rect + y="8.2973185" + x="5.5059438" + height="22.996691" + width="37.00032" + id="rect9208" + style="fill:#555753;fill-opacity:1;fill-rule:evenodd;stroke:#2e3436;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + d="m 4.8886719,5.1484375 c -1.3073224,0 -2.5,1.2966026 -2.5,2.5644531 L 2.3730469,34.271484 c 0.00626,0.602943 0.1382004,0.797516 0.2773437,0.917969 0.1391434,0.120454 0.4228271,0.22462 0.9023438,0.236328 l 40.9414066,0.01758 c 0.651929,0 0.862707,-0.109199 0.943359,-0.195312 0.08065,-0.08611 0.216797,-0.389657 0.216797,-1.144531 L 45.642578,7.9003906 c 0,-1.8071501 -1.011349,-2.7519531 -2.625,-2.7519531 z" + inkscape:href="#rect5040" + id="path5145" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" + xlink:href="#rect5040" + inkscape:original="M 4.8886719 4.2734375 C 3.0522644 4.2734375 1.5136719 5.9390647 1.5136719 7.7128906 L 1.4980469 34.28125 C 1.5138804 35.805892 2.3519787 36.271988 3.53125 36.300781 L 44.494141 36.318359 C 46.00045 36.318359 46.529297 35.795583 46.529297 34.103516 L 46.517578 7.9003906 C 46.517578 5.7019043 45.042697 4.2734375 43.017578 4.2734375 L 4.8886719 4.2734375 z " + inkscape:radius="-0.875" + sodipodi:type="inkscape:offset" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" + id="path10672" + d="m 13.992156,36.834708 c 0.08366,2.172549 -0.283353,2.911443 -1.633624,3.675231 l 23.615402,1.297318 c -0.949019,-1.168628 -2.329411,-2.933334 -1.952941,-4.988235 z" + style="opacity:0.6;fill:url(#linearGradient11406);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="cc" + id="path6575" + d="m 9.9921262,42.291555 c 6.7066928,0.002 27.7967838,0 27.7967838,0" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#8d8d8f;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:0.43902438" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 9.647928,43.299429 c 6.706693,0.002 28.719861,0 28.719861,0" + id="path8029" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" + id="path4073" + d="M 6.031372,8.8268676 V 26.834708 C 22.475817,25.480459 28.630065,16.722289 41.999999,15.807256 L 42,8.7954956 Z" + style="opacity:0.4;fill:url(#linearGradient4778);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <g + transform="translate(-46.77135,-7.3370294)" + id="g4754"> + <g + id="g22150" + style="opacity:0.3" + transform="matrix(0.916667,0,0,0.714282,49.771334,11.132681)"> + <rect + y="35" + x="0" + height="7" + width="4" + id="rect22120" + style="opacity:1;fill:url(#radialGradient4770);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1" /> + <rect + transform="scale(-1)" + y="-42" + x="-48" + height="7" + width="4" + id="rect22134" + style="opacity:1;fill:url(#radialGradient4772);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1" /> + <rect + y="35" + x="4" + height="7" + width="40" + id="rect22138" + style="opacity:1;fill:url(#linearGradient4774);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1" /> + </g> + <g + id="g4743"> + <g + transform="translate(-5.228666,-6.29601)" + id="g10824"> + <path + inkscape:connector-curvature="0" + style="fill:url(#linearGradient4776);fill-opacity:1;fill-rule:evenodd;stroke:#a38503;stroke-width:1.00000024px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 57.5,14.928571 v 30 h 39 z m 6,13 15,11 h -15 z" + id="path4319" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 61.5,44.928571 v -3" + id="path4326" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 67.5,44.928571 v -3" + id="path4328" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="M 73.5,44.928571 V 41.964285" + id="path4330" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 79.5,44.928571 v -3" + id="path4332" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 85.5,44.928571 v -3" + id="path4334" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="opacity:0.4;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.0000006px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 58.500002,16.928575 V 43.928586 H 93.500014 Z" + id="path6126" /> + </g> + <path + inkscape:connector-curvature="0" + transform="translate(47.771334,-4.867439)" + sodipodi:nodetypes="cccc" + id="path12221" + d="M 9.5017474,38.542446 9.4575532,24.532894 28.541592,38.563262 Z" + style="opacity:0.4;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/plugins/wlrandr/protocol/wlr-output-management-unstable-v1.xml b/plugins/wlrandr/protocol/wlr-output-management-unstable-v1.xml new file mode 100644 index 0000000..411e2f0 --- /dev/null +++ b/plugins/wlrandr/protocol/wlr-output-management-unstable-v1.xml @@ -0,0 +1,601 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="wlr_output_management_unstable_v1"> + <copyright> + Copyright © 2019 Purism SPC + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + </copyright> + + <description summary="protocol to configure output devices"> + This protocol exposes interfaces to obtain and modify output device + configuration. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + </description> + + <interface name="zwlr_output_manager_v1" version="4"> + <description summary="output device configuration manager"> + This interface is a manager that allows reading and writing the current + output device configuration. + + Output devices that display pixels (e.g. a physical monitor or a virtual + output in a window) are represented as heads. Heads cannot be created nor + destroyed by the client, but they can be enabled or disabled and their + properties can be changed. Each head may have one or more available modes. + + Whenever a head appears (e.g. a monitor is plugged in), it will be + advertised via the head event. Immediately after the output manager is + bound, all current heads are advertised. + + Whenever a head's properties change, the relevant wlr_output_head events + will be sent. Not all head properties will be sent: only properties that + have changed need to. + + Whenever a head disappears (e.g. a monitor is unplugged), a + wlr_output_head.finished event will be sent. + + After one or more heads appear, change or disappear, the done event will + be sent. It carries a serial which can be used in a create_configuration + request to update heads properties. + + The information obtained from this protocol should only be used for output + configuration purposes. This protocol is not designed to be a generic + output property advertisement protocol for regular clients. Instead, + protocols such as xdg-output should be used. + </description> + + <event name="head"> + <description summary="introduce a new head"> + This event introduces a new head. This happens whenever a new head + appears (e.g. a monitor is plugged in) or after the output manager is + bound. + </description> + <arg name="head" type="new_id" interface="zwlr_output_head_v1"/> + </event> + + <event name="done"> + <description summary="sent all information about current configuration"> + This event is sent after all information has been sent after binding to + the output manager object and after any subsequent changes. This applies + to child head and mode objects as well. In other words, this event is + sent whenever a head or mode is created or destroyed and whenever one of + their properties has been changed. Not all state is re-sent each time + the current configuration changes: only the actual changes are sent. + + This allows changes to the output configuration to be seen as atomic, + even if they happen via multiple events. + + A serial is sent to be used in a future create_configuration request. + </description> + <arg name="serial" type="uint" summary="current configuration serial"/> + </event> + + <request name="create_configuration"> + <description summary="create a new output configuration object"> + Create a new output configuration object. This allows to update head + properties. + </description> + <arg name="id" type="new_id" interface="zwlr_output_configuration_v1"/> + <arg name="serial" type="uint"/> + </request> + + <request name="stop"> + <description summary="stop sending events"> + Indicates the client no longer wishes to receive events for output + configuration changes. However the compositor may emit further events, + until the finished event is emitted. + + The client must not send any more requests after this one. + </description> + </request> + + <event name="finished" type="destructor"> + <description summary="the compositor has finished with the manager"> + This event indicates that the compositor is done sending manager events. + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + </description> + </event> + </interface> + + <interface name="zwlr_output_head_v1" version="4"> + <description summary="output device"> + A head is an output device. The difference between a wl_output object and + a head is that heads are advertised even if they are turned off. A head + object only advertises properties and cannot be used directly to change + them. + + A head has some read-only properties: modes, name, description and + physical_size. These cannot be changed by clients. + + Other properties can be updated via a wlr_output_configuration object. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + </description> + + <event name="name"> + <description summary="head name"> + This event describes the head name. + + The naming convention is compositor defined, but limited to alphanumeric + characters and dashes (-). Each name is unique among all wlr_output_head + objects, but if a wlr_output_head object is destroyed the same name may + be reused later. The names will also remain consistent across sessions + with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + If the compositor implements the xdg-output protocol and this head is + enabled, the xdg_output.name event must report the same name. + + The name event is sent after a wlr_output_head object is created. This + event is only sent once per object, and the name does not change over + the lifetime of the wlr_output_head object. + </description> + <arg name="name" type="string"/> + </event> + + <event name="description"> + <description summary="head description"> + This event describes a human-readable description of the head. + + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. However, do not assume that the name is a reflection of + the make, model, serial of the underlying DRM connector or the display + name of the underlying X11 connection, etc. + + If the compositor implements xdg-output and this head is enabled, + the xdg_output.description must report the same description. + + The description event is sent after a wlr_output_head object is created. + This event is only sent once per object, and the description does not + change over the lifetime of the wlr_output_head object. + </description> + <arg name="description" type="string"/> + </event> + + <event name="physical_size"> + <description summary="head physical size"> + This event describes the physical size of the head. This event is only + sent if the head has a physical size (e.g. is not a projector or a + virtual device). + </description> + <arg name="width" type="int" summary="width in millimeters of the output"/> + <arg name="height" type="int" summary="height in millimeters of the output"/> + </event> + + <event name="mode"> + <description summary="introduce a mode"> + This event introduces a mode for this head. It is sent once per + supported mode. + </description> + <arg name="mode" type="new_id" interface="zwlr_output_mode_v1"/> + </event> + + <event name="enabled"> + <description summary="head is enabled or disabled"> + This event describes whether the head is enabled. A disabled head is not + mapped to a region of the global compositor space. + + When a head is disabled, some properties (current_mode, position, + transform and scale) are irrelevant. + </description> + <arg name="enabled" type="int" summary="zero if disabled, non-zero if enabled"/> + </event> + + <event name="current_mode"> + <description summary="current mode"> + This event describes the mode currently in use for this head. It is only + sent if the output is enabled. + </description> + <arg name="mode" type="object" interface="zwlr_output_mode_v1"/> + </event> + + <event name="position"> + <description summary="current position"> + This events describes the position of the head in the global compositor + space. It is only sent if the output is enabled. + </description> + <arg name="x" type="int" + summary="x position within the global compositor space"/> + <arg name="y" type="int" + summary="y position within the global compositor space"/> + </event> + + <event name="transform"> + <description summary="current transformation"> + This event describes the transformation currently applied to the head. + It is only sent if the output is enabled. + </description> + <arg name="transform" type="int" enum="wl_output.transform"/> + </event> + + <event name="scale"> + <description summary="current scale"> + This events describes the scale of the head in the global compositor + space. It is only sent if the output is enabled. + </description> + <arg name="scale" type="fixed"/> + </event> + + <event name="finished"> + <description summary="the head has disappeared"> + This event indicates that the head is no longer available. The head + object becomes inert. Clients should send a destroy request and release + any resources associated with it. + </description> + </event> + + <!-- Version 2 additions --> + + <event name="make" since="2"> + <description summary="head manufacturer"> + This event describes the manufacturer of the head. + + This must report the same make as the wl_output interface does in its + geometry event. + + Together with the model and serial_number events the purpose is to + allow clients to recognize heads from previous sessions and for example + load head-specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the make of + the head or the definition of a make is not sensible in the current + setup, for example in a virtual session. Clients can still try to + identify the head by available information from other events but should + be aware that there is an increased risk of false positives. + + It is not recommended to display the make string in UI to users. For + that the string provided by the description event should be preferred. + </description> + <arg name="make" type="string"/> + </event> + + <event name="model" since="2"> + <description summary="head model"> + This event describes the model of the head. + + This must report the same model as the wl_output interface does in its + geometry event. + + Together with the make and serial_number events the purpose is to + allow clients to recognize heads from previous sessions and for example + load head-specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the model of + the head or the definition of a model is not sensible in the current + setup, for example in a virtual session. Clients can still try to + identify the head by available information from other events but should + be aware that there is an increased risk of false positives. + + It is not recommended to display the model string in UI to users. For + that the string provided by the description event should be preferred. + </description> + <arg name="model" type="string"/> + </event> + + <event name="serial_number" since="2"> + <description summary="head serial number"> + This event describes the serial number of the head. + + Together with the make and model events the purpose is to allow clients + to recognize heads from previous sessions and for example load head- + specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the serial + number of the head or the definition of a serial number is not sensible + in the current setup. Clients can still try to identify the head by + available information from other events but should be aware that there + is an increased risk of false positives. + + It is not recommended to display the serial_number string in UI to + users. For that the string provided by the description event should be + preferred. + </description> + <arg name="serial_number" type="string"/> + </event> + + <!-- Version 3 additions --> + + <request name="release" type="destructor" since="3"> + <description summary="destroy the head object"> + This request indicates that the client will no longer use this head + object. + </description> + </request> + + <!-- Version 4 additions --> + + <enum name="adaptive_sync_state" since="4"> + <entry name="disabled" value="0" summary="adaptive sync is disabled"/> + <entry name="enabled" value="1" summary="adaptive sync is enabled"/> + </enum> + + <event name="adaptive_sync" since="4"> + <description summary="current adaptive sync state"> + This event describes whether adaptive sync is currently enabled for + the head or not. Adaptive sync is also known as Variable Refresh + Rate or VRR. + </description> + <arg name="state" type="uint" enum="adaptive_sync_state"/> + </event> + </interface> + + <interface name="zwlr_output_mode_v1" version="3"> + <description summary="output mode"> + This object describes an output mode. + + Some heads don't support output modes, in which case modes won't be + advertised. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + </description> + + <event name="size"> + <description summary="mode size"> + This event describes the mode size. The size is given in physical + hardware units of the output device. This is not necessarily the same as + the output size in the global compositor space. For instance, the output + may be scaled or transformed. + </description> + <arg name="width" type="int" summary="width of the mode in hardware units"/> + <arg name="height" type="int" summary="height of the mode in hardware units"/> + </event> + + <event name="refresh"> + <description summary="mode refresh rate"> + This event describes the mode's fixed vertical refresh rate. It is only + sent if the mode has a fixed refresh rate. + </description> + <arg name="refresh" type="int" summary="vertical refresh rate in mHz"/> + </event> + + <event name="preferred"> + <description summary="mode is preferred"> + This event advertises this mode as preferred. + </description> + </event> + + <event name="finished"> + <description summary="the mode has disappeared"> + This event indicates that the mode is no longer available. The mode + object becomes inert. Clients should send a destroy request and release + any resources associated with it. + </description> + </event> + + <!-- Version 3 additions --> + + <request name="release" type="destructor" since="3"> + <description summary="destroy the mode object"> + This request indicates that the client will no longer use this mode + object. + </description> + </request> + </interface> + + <interface name="zwlr_output_configuration_v1" version="4"> + <description summary="output configuration"> + This object is used by the client to describe a full output configuration. + + First, the client needs to setup the output configuration. Each head can + be either enabled (and configured) or disabled. It is a protocol error to + send two enable_head or disable_head requests with the same head. It is a + protocol error to omit a head in a configuration. + + Then, the client can apply or test the configuration. The compositor will + then reply with a succeeded, failed or cancelled event. Finally the client + should destroy the configuration object. + </description> + + <enum name="error"> + <entry name="already_configured_head" value="1" + summary="head has been configured twice"/> + <entry name="unconfigured_head" value="2" + summary="head has not been configured"/> + <entry name="already_used" value="3" + summary="request sent after configuration has been applied or tested"/> + </enum> + + <request name="enable_head"> + <description summary="enable and configure a head"> + Enable a head. This request creates a head configuration object that can + be used to change the head's properties. + </description> + <arg name="id" type="new_id" interface="zwlr_output_configuration_head_v1" + summary="a new object to configure the head"/> + <arg name="head" type="object" interface="zwlr_output_head_v1" + summary="the head to be enabled"/> + </request> + + <request name="disable_head"> + <description summary="disable a head"> + Disable a head. + </description> + <arg name="head" type="object" interface="zwlr_output_head_v1" + summary="the head to be disabled"/> + </request> + + <request name="apply"> + <description summary="apply the configuration"> + Apply the new output configuration. + + In case the configuration is successfully applied, there is no guarantee + that the new output state matches completely the requested + configuration. For instance, a compositor might round the scale if it + doesn't support fractional scaling. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + </description> + </request> + + <request name="test"> + <description summary="test the configuration"> + Test the new output configuration. The configuration won't be applied, + but will only be validated. + + Even if the compositor succeeds to test a configuration, applying it may + fail. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + </description> + </request> + + <event name="succeeded"> + <description summary="configuration changes succeeded"> + Sent after the compositor has successfully applied the changes or + tested them. + + Upon receiving this event, the client should destroy this object. + + If the current configuration has changed, events to describe the changes + will be sent followed by a wlr_output_manager.done event. + </description> + </event> + + <event name="failed"> + <description summary="configuration changes failed"> + Sent if the compositor rejects the changes or failed to apply them. The + compositor should revert any changes made by the apply request that + triggered this event. + + Upon receiving this event, the client should destroy this object. + </description> + </event> + + <event name="cancelled"> + <description summary="configuration has been cancelled"> + Sent if the compositor cancels the configuration because the state of an + output changed and the client has outdated information (e.g. after an + output has been hotplugged). + + The client can create a new configuration with a newer serial and try + again. + + Upon receiving this event, the client should destroy this object. + </description> + </event> + + <request name="destroy" type="destructor"> + <description summary="destroy the output configuration"> + Using this request a client can tell the compositor that it is not going + to use the configuration object anymore. Any changes to the outputs + that have not been applied will be discarded. + + This request also destroys wlr_output_configuration_head objects created + via this object. + </description> + </request> + </interface> + + <interface name="zwlr_output_configuration_head_v1" version="4"> + <description summary="head configuration"> + This object is used by the client to update a single head's configuration. + + It is a protocol error to set the same property twice. + </description> + + <enum name="error"> + <entry name="already_set" value="1" summary="property has already been set"/> + <entry name="invalid_mode" value="2" summary="mode doesn't belong to head"/> + <entry name="invalid_custom_mode" value="3" summary="mode is invalid"/> + <entry name="invalid_transform" value="4" summary="transform value outside enum"/> + <entry name="invalid_scale" value="5" summary="scale negative or zero"/> + <entry name="invalid_adaptive_sync_state" value="6" since="4" + summary="invalid enum value used in the set_adaptive_sync request"/> + </enum> + + <request name="set_mode"> + <description summary="set the mode"> + This request sets the head's mode. + </description> + <arg name="mode" type="object" interface="zwlr_output_mode_v1"/> + </request> + + <request name="set_custom_mode"> + <description summary="set a custom mode"> + This request assigns a custom mode to the head. The size is given in + physical hardware units of the output device. If set to zero, the + refresh rate is unspecified. + + It is a protocol error to set both a mode and a custom mode. + </description> + <arg name="width" type="int" summary="width of the mode in hardware units"/> + <arg name="height" type="int" summary="height of the mode in hardware units"/> + <arg name="refresh" type="int" summary="vertical refresh rate in mHz or zero"/> + </request> + + <request name="set_position"> + <description summary="set the position"> + This request sets the head's position in the global compositor space. + </description> + <arg name="x" type="int" summary="x position in the global compositor space"/> + <arg name="y" type="int" summary="y position in the global compositor space"/> + </request> + + <request name="set_transform"> + <description summary="set the transform"> + This request sets the head's transform. + </description> + <arg name="transform" type="int" enum="wl_output.transform"/> + </request> + + <request name="set_scale"> + <description summary="set the scale"> + This request sets the head's scale. + </description> + <arg name="scale" type="fixed"/> + </request> + + <!-- Version 4 additions --> + + <request name="set_adaptive_sync" since="4"> + <description summary="enable/disable adaptive sync"> + This request enables/disables adaptive sync. Adaptive sync is also + known as Variable Refresh Rate or VRR. + </description> + <arg name="state" type="uint" enum="zwlr_output_head_v1.adaptive_sync_state"/> + </request> + </interface> +</protocol> diff --git a/plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in b/plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in new file mode 100644 index 0000000..b44192c --- /dev/null +++ b/plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in @@ -0,0 +1,9 @@ +[MATE Settings Plugin] +Module=wlrandr +IAge=0 +WaylandOnly=true +Name=WLrandr +Description=Configure Wayland outputs +Authors=Various +Copyright=Copyright © 2026 MATE Developers +Website= |
