summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorSunderland93 <[email protected]>2026-08-04 18:50:57 +0400
committerLuke from DC <[email protected]>2026-08-19 19:15:44 +0000
commit49bca7b6e53d0464e7e4f455ec8cd33aaaf2ca26 (patch)
tree6f0a3d916cb54d789b9a62e80ae917d1221da70a /plugins
parentae5e2c5e11cf741162c34c67d87cb43ebe4abb60 (diff)
downloadmate-settings-daemon-49bca7b6e53d0464e7e4f455ec8cd33aaaf2ca26.tar.bz2
mate-settings-daemon-49bca7b6e53d0464e7e4f455ec8cd33aaaf2ca26.tar.xz
Add wlrandr plugin
Diffstat (limited to 'plugins')
-rw-r--r--plugins/Makefile.am6
-rw-r--r--plugins/wlrandr/Makefile.am108
-rw-r--r--plugins/wlrandr/msd-wlrandr-16.pngbin0 -> 875 bytes
-rw-r--r--plugins/wlrandr/msd-wlrandr-22.pngbin0 -> 1242 bytes
-rw-r--r--plugins/wlrandr/msd-wlrandr-24.pngbin0 -> 1354 bytes
-rw-r--r--plugins/wlrandr/msd-wlrandr-32.pngbin0 -> 1887 bytes
-rw-r--r--plugins/wlrandr/msd-wlrandr-48.pngbin0 -> 3049 bytes
-rw-r--r--plugins/wlrandr/msd-wlrandr-64.pngbin0 -> 4273 bytes
-rw-r--r--plugins/wlrandr/msd-wlrandr-96.pngbin0 -> 6937 bytes
-rw-r--r--plugins/wlrandr/msd-wlrandr-manager.c2527
-rw-r--r--plugins/wlrandr/msd-wlrandr-manager.h57
-rw-r--r--plugins/wlrandr/msd-wlrandr-plugin.c105
-rw-r--r--plugins/wlrandr/msd-wlrandr-plugin.h63
-rw-r--r--plugins/wlrandr/msd-wlrandr.svg491
-rw-r--r--plugins/wlrandr/protocol/wlr-output-management-unstable-v1.xml601
-rw-r--r--plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in9
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
new file mode 100644
index 0000000..adb9e36
--- /dev/null
+++ b/plugins/wlrandr/msd-wlrandr-16.png
Binary files differ
diff --git a/plugins/wlrandr/msd-wlrandr-22.png b/plugins/wlrandr/msd-wlrandr-22.png
new file mode 100644
index 0000000..a90f71b
--- /dev/null
+++ b/plugins/wlrandr/msd-wlrandr-22.png
Binary files differ
diff --git a/plugins/wlrandr/msd-wlrandr-24.png b/plugins/wlrandr/msd-wlrandr-24.png
new file mode 100644
index 0000000..6d66034
--- /dev/null
+++ b/plugins/wlrandr/msd-wlrandr-24.png
Binary files differ
diff --git a/plugins/wlrandr/msd-wlrandr-32.png b/plugins/wlrandr/msd-wlrandr-32.png
new file mode 100644
index 0000000..d0fd058
--- /dev/null
+++ b/plugins/wlrandr/msd-wlrandr-32.png
Binary files differ
diff --git a/plugins/wlrandr/msd-wlrandr-48.png b/plugins/wlrandr/msd-wlrandr-48.png
new file mode 100644
index 0000000..ff2e1c7
--- /dev/null
+++ b/plugins/wlrandr/msd-wlrandr-48.png
Binary files differ
diff --git a/plugins/wlrandr/msd-wlrandr-64.png b/plugins/wlrandr/msd-wlrandr-64.png
new file mode 100644
index 0000000..ebde0b6
--- /dev/null
+++ b/plugins/wlrandr/msd-wlrandr-64.png
Binary files differ
diff --git a/plugins/wlrandr/msd-wlrandr-96.png b/plugins/wlrandr/msd-wlrandr-96.png
new file mode 100644
index 0000000..fab9dcd
--- /dev/null
+++ b/plugins/wlrandr/msd-wlrandr-96.png
Binary files differ
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, &timestamp);
+ /* 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, &registry_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 &lt;[email protected]&gt;</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=