diff options
64 files changed, 6851 insertions, 173 deletions
diff --git a/.github/workflows/archlinux.sh b/.github/workflows/archlinux.sh index bb11578..3e38575 100755 --- a/.github/workflows/archlinux.sh +++ b/.github/workflows/archlinux.sh @@ -24,6 +24,8 @@ requires+=( gettext git glib2-devel + gtk-layer-shell + libayatana-appindicator libcanberra libmatekbd libmatemixer @@ -35,6 +37,7 @@ requires+=( polkit python-packaging which + wayland-protocols ) infobegin "Update system" diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 01b3ba8..c6c9691 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - MATE_DESKTOP_VERSION: 1.28.2 + MATE_DESKTOP_VERSION: 1.29.0 CACHE_PATH: /tmp/.cache jobs: @@ -69,7 +69,7 @@ jobs: pacman --noconfirm -Sy git || true echo "::endgroup::" - name: Repository checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install dependency packages run: .github/workflows/${{ env.DISTRO }}.sh - name: Enable ccache to speed up builds @@ -79,7 +79,7 @@ jobs: # INFO: Depends on mate-desktop 1.27.1+, so we should install it from source. - name: Cache mate-desktop binary packages - uses: actions/cache@v5 + uses: actions/cache@v6 id: cache-mate-desktop with: path: ${{ env.CACHE_PATH }} diff --git a/.github/workflows/debian.sh b/.github/workflows/debian.sh index 8750804..0534670 100755 --- a/.github/workflows/debian.sh +++ b/.github/workflows/debian.sh @@ -20,11 +20,13 @@ requires+=( gcc g++ git + libayatana-appindicator3-dev libcanberra-gtk3-dev libdconf-dev libfontconfig1-dev libglib2.0-dev libgtk-3-dev + libgtk-layer-shell-dev libmate-desktop-dev libmatekbd-dev libmatemixer-dev @@ -44,6 +46,7 @@ requires+=( mate-common polkitd pkexec + wayland-protocols x11proto-kb-dev ) diff --git a/.github/workflows/fedora.sh b/.github/workflows/fedora.sh index 407d67d..dc35600 100755 --- a/.github/workflows/fedora.sh +++ b/.github/workflows/fedora.sh @@ -23,7 +23,9 @@ requires+=( gcc-c++ git gtk3-devel + gtk-layer-shell-devel iso-codes-devel + libayatana-appindicator-gtk3-devel libSM-devel libcanberra-devel libmatekbd-devel @@ -36,6 +38,7 @@ requires+=( polkit-devel pulseaudio-libs-devel redhat-rpm-config + wayland-protocols-devel ) infobegin "Update system" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a59377..fde2a49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Repository checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install dependency packages run: sudo .github/workflows/ubuntu.sh diff --git a/.github/workflows/ubuntu.sh b/.github/workflows/ubuntu.sh index 3ab2e73..839e1e5 100755 --- a/.github/workflows/ubuntu.sh +++ b/.github/workflows/ubuntu.sh @@ -20,11 +20,13 @@ requires+=( gcc g++ git + libayatana-appindicator3-dev libcanberra-gtk3-dev libdconf-dev libfontconfig1-dev libglib2.0-dev libgtk-3-dev + libgtk-layer-shell-dev libmate-desktop-dev libmatekbd-dev libmatemixer-dev @@ -42,6 +44,7 @@ requires+=( libxt-dev make mate-common + wayland-protocols x11proto-kb-dev ) @@ -1,3 +1,42 @@ +### mate-settings-daemon 1.29.1 + + * background: don't initialize alpha channel + * wayland/background: respect show-desktop-icons setting + * fix wayland-protocols path + * ci: add wayland related packages + * Remove dead code + * wlrandr: persist the applied configuration instead of a live snapshot + * clipboard: adjust offer timeout + * xsettings: flush Xlib output so settings reach X11/XWayland clients + * Remove dead code + * xsettings: flush Xlib output so settings reach X11/XWayland clients + * wlrandr: expose configuration over D-Bus for the display capplet + * Add wlrandr plugin + * Respect fade effect settings and fix redraw at resolution changes + * background: port to wayland via gtk-layer-shell + * clipboard-manager: add Wayland support + * Initial Wayland support + * Translated using Weblate (Greek) + * Translated using Weblate (Kabyle) + * Translated using Weblate (Russian) + * Translated using Weblate (Georgian) + * Translated using Weblate (Korean) + * Translated using Weblate (Occitan) + * build (deps) bump actions/checkout from 6 to 7 + * build (deps) bump actions/cache from 5 to 76 + * xrandr: Handle hotplug_mode_update for VM guest resizing + * background: simplify desktop detection to use EWMH standard + * correcting po with errors + * remove empty po file + * add translation coverage badge + * retrieve last transifex translations + * Revert "Retrieve last transifex translations" + * Retrieve last transifex translations + * build(deps): bump actions/checkout from 5 to 6 + * ci: migrate from Travis CI to GitHub Actions + * fix memleak + + ### mate-settings-daemon 1.29.0 * xsettings: Remove HiDPI scale change workaround diff --git a/configure.ac b/configure.ac index 48c2865..911a8bd 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ AC_PREREQ([2.60]) m4_define([msd_api_version_major],[1]) m4_define([msd_api_version_minor],[29]) -m4_define([msd_api_version_micro],[0]) +m4_define([msd_api_version_micro],[1]) m4_define([msd_api_version], [msd_api_version_major.msd_api_version_minor.msd_api_version_micro]) @@ -55,7 +55,7 @@ dnl --------------------------------------------------------------------------- GLIB_REQUIRED_VERSION=2.58.0 GIO_REQUIRED_VERSION=2.50.0 GTK_REQUIRED_VERSION=3.22.0 -MATE_DESKTOP_REQUIRED_VERSION=1.27.1 +MATE_DESKTOP_REQUIRED_VERSION=1.29.0 LIBMATEKBD_REQUIRED_VERSION=1.17.0 LIBNOTIFY_REQUIRED_VERSION=0.7.0 LIBATSPI_REQUIRED_VERSION=2.0 @@ -82,6 +82,81 @@ case $host_os in esac AC_SUBST([MSD_PLUGIN_LDFLAGS]) +dnl --------------------------------------------------------------------------- +dnl - Ayatana AppIndicator +dnl --------------------------------------------------------------------------- + +AC_ARG_ENABLE(appindicator, + AS_HELP_STRING([--enable-appindicator], [Enable ayatana appindicator support (default: auto)]), + [case "${enableval}" in + yes|no|auto) enable_appindicator=${enableval} ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-appindicator) ;; + esac], + enable_appindicator=auto) + +have_appindicator=no +if test "x${enable_appindicator}" != "xno"; then + PKG_CHECK_MODULES(APPINDICATOR, ayatana-appindicator3-0.1, + [have_appindicator=yes + AC_DEFINE(HAVE_APP_INDICATOR, 1, [Define if ayatana appindicator support is enabled])], + [if test "x${enable_appindicator}" = "xyes"; then + AC_MSG_ERROR([Ayatana appindicator support requested but ayatana-appindicator3-0.1 not found]) + fi]) +fi + +dnl --------------------------------------------------------------------------- +dnl - Wayland +dnl --------------------------------------------------------------------------- + +AC_ARG_ENABLE(wayland, + AS_HELP_STRING([--enable-wayland], [Enable Wayland support (default: auto)]), + [case "${enableval}" in + yes|no|auto) enable_wayland=${enableval} ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-wayland) ;; + esac], + enable_wayland=auto) + +have_wayland=no +if test "x${enable_wayland}" != "xno"; then + save_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="$CPPFLAGS $SETTINGS_DAEMON_CFLAGS" + AC_CHECK_HEADER([gdk/gdkwayland.h], + [have_wayland=yes], + [have_wayland=no]) + CPPFLAGS="$save_CPPFLAGS" + + if test "x${have_wayland}" = "xyes"; then + PKG_CHECK_MODULES(WAYLAND_CLIENT, wayland-client >= 1.18, + [have_wayland=yes], + [have_wayland=no]) + fi + + if test "x${have_wayland}" = "xyes"; then + WAYLAND_SCANNER=wayland-scanner + AC_SUBST(WAYLAND_SCANNER) + WAYLAND_PROTOCOLS_DATADIR=`$PKG_CONFIG --variable=pkgdatadir wayland-protocols | sed 's|^//*|/|'` + AC_SUBST(WAYLAND_PROTOCOLS_DATADIR) + if test "x${WAYLAND_PROTOCOLS_DATADIR}" = "x"; then + have_wayland=no + fi + fi + + if test "x${have_wayland}" = "xyes"; then + PKG_CHECK_MODULES(GTK_LAYER_SHELL, gtk-layer-shell-0 >= 0.6, + [have_wayland=yes], + [have_wayland=no]) + fi + + if test "x${have_wayland}" = "xyes"; then + AC_DEFINE(HAVE_WAYLAND, 1, [Define if Wayland support is enabled]) + fi + + if test "x${have_wayland}" = "xno" -a "x${enable_wayland}" = "xyes"; then + AC_MSG_ERROR([Wayland support requested but the required Wayland libraries (GTK+ Wayland backend, wayland-client, wayland-protocols or gtk-layer-shell) are missing]) + fi +fi +AM_CONDITIONAL(BUILD_WAYLAND, test "x${have_wayland}" = "xyes") + PKG_CHECK_MODULES(DCONF, dconf >= 0.13.4) AC_PATH_PROG(GLIB_GENMARSHAL, glib-genmarshal) @@ -429,6 +504,7 @@ plugins/xrandr/Makefile plugins/xrdb/Makefile plugins/xrdb/data/Makefile plugins/xsettings/Makefile +plugins/wlrandr/Makefile data/Makefile data/mate-settings-daemon.desktop.in data/mate-settings-daemon.pc @@ -457,6 +533,7 @@ data/org.mate.SettingsDaemon.plugins.typing-break.gschema.xml data/org.mate.SettingsDaemon.plugins.xrandr.gschema.xml data/org.mate.SettingsDaemon.plugins.xrdb.gschema.xml data/org.mate.SettingsDaemon.plugins.xsettings.gschema.xml +data/org.mate.SettingsDaemon.plugins.wlrandr.gschema.xml data/org.mate.SettingsDaemon.plugins.gschema.xml po/Makefile.in ]) @@ -498,6 +575,9 @@ Configure summary: Libmatemixer support ....: ${have_libmatemixer} Smartcard support .......: ${have_smartcard_support} RFKill support ..........: ${enable_rfkill} + Appindicator support ....: ${have_appindicator} + Wayland support .........: ${have_wayland} + Wayland protocols dir ...: ${WAYLAND_PROTOCOLS_DATADIR} ${NSS_DATABASE:+\ System nssdb ............: ${NSS_DATABASE} }\ diff --git a/data/Makefile.am b/data/Makefile.am index 3745513..13442c1 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -25,6 +25,7 @@ msd_gschemas_in = \ org.mate.SettingsDaemon.plugins.xrandr.gschema.xml.in \ org.mate.SettingsDaemon.plugins.xrdb.gschema.xml.in \ org.mate.SettingsDaemon.plugins.xsettings.gschema.xml.in \ + org.mate.SettingsDaemon.plugins.wlrandr.gschema.xml.in \ org.mate.SettingsDaemon.plugins.gschema.xml.in \ $(NULL) diff --git a/data/org.mate.SettingsDaemon.plugins.wlrandr.gschema.xml.in b/data/org.mate.SettingsDaemon.plugins.wlrandr.gschema.xml.in new file mode 100644 index 0000000..40d8e26 --- /dev/null +++ b/data/org.mate.SettingsDaemon.plugins.wlrandr.gschema.xml.in @@ -0,0 +1,29 @@ +<schemalist> + <schema gettext-domain="mate-settings-daemon" id="org.mate.SettingsDaemon.plugins.wlrandr" path="/org/mate/settings-daemon/plugins/wlrandr/"> + <key name="active" type="b"> + <default>true</default> + <summary>Activation of this plugin</summary> + <description>Whether this plugin would be activated by mate-settings-daemon or not</description> + </key> + <key name="priority" type="i"> + <default>1</default> + <summary>Priority to use for this plugin</summary> + <description>Priority to use for this plugin in mate-settings-daemon startup queue</description> + </key> + <key name="show-notification-icon" type="b"> + <default>false</default> + <summary>Show Displays in Notification Area</summary> + <description>Whether a notification icon with display-related things should be shown in the panel.</description> + </key> + <key name="turn-on-external-monitors-at-startup" type="b"> + <default>false</default> + <summary>Turn on external monitor after system boot</summary> + <description>Turn on external monitor after system boot if user plugs in external monitor on system boot.</description> + </key> + <key name="default-configuration-file" type="s"> + <default>'/etc/monitors-wayland.xml'</default> + <summary>File for default configuration for Wayland outputs</summary> + <description>The wlrandr plugin will look for a default configuration in the file specified by this key. This is similar to the ~/.config/monitors-wayland.xml that normally gets stored in users' home directories. If a user does not have such a file, or has one that does not match the user's setup of monitors, then the file specified by this key will be used instead.</description> + </key> + </schema> +</schemalist> diff --git a/data/org.mate.peripherals-mouse.gschema.xml.in b/data/org.mate.peripherals-mouse.gschema.xml.in index 87b4bc2..b36cbd8 100644 --- a/data/org.mate.peripherals-mouse.gschema.xml.in +++ b/data/org.mate.peripherals-mouse.gschema.xml.in @@ -10,6 +10,11 @@ <summary>Mouse button orientation</summary> <description>Swap left and right mouse buttons for left-handed mice.</description> </key> + <key name="natural-scroll" type="b"> + <default>false</default> + <summary>Natural scrolling</summary> + <description>Set this to true to enable natural (reverse) scrolling for mice</description> + </key> <key name="motion-acceleration" type="d"> <default>-1</default> <summary>Motion Acceleration</summary> diff --git a/mate-settings-daemon/main.c b/mate-settings-daemon/main.c index 11d8687..ed9c2b3 100644 --- a/mate-settings-daemon/main.c +++ b/mate-settings-daemon/main.c @@ -39,6 +39,10 @@ #include <libnotify/notify.h> #endif /* HAVE_LIBNOTIFY */ +#ifdef HAVE_WAYLAND +#include <gdk/gdkwayland.h> +#endif /* HAVE_WAYLAND */ + #include "mate-settings-manager.h" #include "mate-settings-profile.h" @@ -438,6 +442,12 @@ main (int argc, char *argv[]) } mate_settings_profile_end ("opening gtk display"); +#ifdef HAVE_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { + g_debug ("Running in a Wayland session; only the xsettings plugin (for XWayland) will be active"); + } +#endif /* HAVE_WAYLAND */ + g_log_set_default_handler (msd_log_default_handler, NULL); bus_register (); diff --git a/mate-settings-daemon/mate-settings-plugin-info.c b/mate-settings-daemon/mate-settings-plugin-info.c index 50508cb..282ffbf 100644 --- a/mate-settings-daemon/mate-settings-plugin-info.c +++ b/mate-settings-daemon/mate-settings-plugin-info.c @@ -27,6 +27,13 @@ #include <glib/gi18n.h> #include <gmodule.h> #include <gio/gio.h> +#include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif /* GDK_WINDOWING_WAYLAND */ #include "mate-settings-plugin-info.h" #include "mate-settings-module.h" @@ -56,6 +63,8 @@ struct MateSettingsPluginInfoPrivate guint enabled : 1; guint active : 1; + guint x11_only : 1; + guint wayland_only : 1; /* A plugin is unavailable if it is not possible to activate it due to an error loading the plugin module */ @@ -248,6 +257,14 @@ mate_settings_plugin_info_fill_from_file (MateSettingsPluginInfo *info, info->priv->priority = PLUGIN_PRIORITY_DEFAULT; } + /* Get X11Only */ + info->priv->x11_only = + g_key_file_get_boolean (plugin_file, PLUGIN_GROUP, "X11Only", NULL); + + /* Get WaylandOnly */ + info->priv->wayland_only = + g_key_file_get_boolean (plugin_file, PLUGIN_GROUP, "WaylandOnly", NULL); + g_key_file_free (plugin_file); debug_info (info); @@ -405,6 +422,32 @@ mate_settings_plugin_info_activate (MateSettingsPluginInfo *info) return TRUE; } + if (info->priv->x11_only) { +#ifdef GDK_WINDOWING_X11 + gboolean x11 = GDK_IS_X11_DISPLAY (gdk_display_get_default ()); +#else + gboolean x11 = FALSE; +#endif + if (!x11) { + g_debug ("Plugin '%s' requires X11; not loading on this display", + info->priv->name); + return FALSE; + } + } + + if (info->priv->wayland_only) { +#ifdef GDK_WINDOWING_WAYLAND + gboolean wayland = GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ()); +#else + gboolean wayland = FALSE; +#endif + if (!wayland) { + g_debug ("Plugin '%s' requires Wayland; not loading on this display", + info->priv->name); + return FALSE; + } + } + if (_activate_plugin (info)) { info->priv->active = TRUE; return TRUE; @@ -430,6 +473,22 @@ mate_settings_plugin_info_get_enabled (MateSettingsPluginInfo *info) } gboolean +mate_settings_plugin_info_get_x11_only (MateSettingsPluginInfo *info) +{ + g_return_val_if_fail (MATE_IS_SETTINGS_PLUGIN_INFO (info), FALSE); + + return (info->priv->x11_only); +} + +gboolean +mate_settings_plugin_info_get_wayland_only (MateSettingsPluginInfo *info) +{ + g_return_val_if_fail (MATE_IS_SETTINGS_PLUGIN_INFO (info), FALSE); + + return (info->priv->wayland_only); +} + +gboolean mate_settings_plugin_info_is_available (MateSettingsPluginInfo *info) { g_return_val_if_fail (MATE_IS_SETTINGS_PLUGIN_INFO (info), FALSE); diff --git a/mate-settings-daemon/mate-settings-plugin-info.h b/mate-settings-daemon/mate-settings-plugin-info.h index d7d2c25..8678c6d 100644 --- a/mate-settings-daemon/mate-settings-plugin-info.h +++ b/mate-settings-daemon/mate-settings-plugin-info.h @@ -61,6 +61,8 @@ gboolean mate_settings_plugin_info_deactivate (MateSettingsPluginIn gboolean mate_settings_plugin_info_is_active (MateSettingsPluginInfo *info); gboolean mate_settings_plugin_info_get_enabled (MateSettingsPluginInfo *info); gboolean mate_settings_plugin_info_is_available (MateSettingsPluginInfo *info); +gboolean mate_settings_plugin_info_get_x11_only (MateSettingsPluginInfo *info); +gboolean mate_settings_plugin_info_get_wayland_only (MateSettingsPluginInfo *info); const char *mate_settings_plugin_info_get_name (MateSettingsPluginInfo *info); const char *mate_settings_plugin_info_get_description (MateSettingsPluginInfo *info); 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/a11y-keyboard/a11y-keyboard.mate-settings-plugin.desktop.in b/plugins/a11y-keyboard/a11y-keyboard.mate-settings-plugin.desktop.in index 51a59a2..9a1793e 100644 --- a/plugins/a11y-keyboard/a11y-keyboard.mate-settings-plugin.desktop.in +++ b/plugins/a11y-keyboard/a11y-keyboard.mate-settings-plugin.desktop.in @@ -1,6 +1,7 @@ [MATE Settings Plugin] Module=a11y-keyboard IAge=0 +X11Only=true Name=Accessibility Keyboard Description=Accessibility keyboard plugin Authors=Jody Goldberg diff --git a/plugins/a11y-keyboard/msd-a11y-keyboard-manager.c b/plugins/a11y-keyboard/msd-a11y-keyboard-manager.c index d354db5..7ffe863 100644 --- a/plugins/a11y-keyboard/msd-a11y-keyboard-manager.c +++ b/plugins/a11y-keyboard/msd-a11y-keyboard-manager.c @@ -35,7 +35,9 @@ #include <glib.h> #include <glib/gi18n.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #include <gtk/gtk.h> #include <gio/gio.h> @@ -1149,6 +1151,20 @@ msd_a11y_keyboard_manager_start (MsdA11yKeyboardManager *manager, { mate_settings_profile_start (NULL); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "X11 display is required for the a11y_keyboard plugin"); + mate_settings_profile_end (NULL); + return FALSE; + } +#else + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The a11y_keyboard plugin was built without X11 support"); + mate_settings_profile_end (NULL); + return FALSE; +#endif /* GDK_WINDOWING_X11 */ + /* give our initialization a lower priority over msd-keyoboard so it * restores the numlock state before we might start monitoring it */ g_idle_add_full (G_PRIORITY_LOW, @@ -1195,6 +1211,11 @@ msd_a11y_keyboard_manager_stop (MsdA11yKeyboardManager *manager) g_debug ("Stopping a11y_keyboard manager"); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return; +#endif /* GDK_WINDOWING_X11 */ + gdk_window_remove_filter (NULL, devicepresence_filter, manager); if (p->status_icon) diff --git a/plugins/background/Makefile.am b/plugins/background/Makefile.am index b2ab685..ce8109f 100644 --- a/plugins/background/Makefile.am +++ b/plugins/background/Makefile.am @@ -1,5 +1,14 @@ NULL = +if BUILD_WAYLAND +WAYLAND_BACKGROUND_SOURCES = \ + msd-background-manager-wayland.c \ + msd-background-manager-wayland.h \ + $(NULL) +else +WAYLAND_BACKGROUND_SOURCES = +endif + noinst_PROGRAMS = \ test-background \ $(NULL) @@ -8,6 +17,7 @@ test_background_SOURCES = \ test-background.c \ msd-background-manager.h \ msd-background-manager.c \ + $(WAYLAND_BACKGROUND_SOURCES) \ $(NULL) test_background_CPPFLAGS = \ @@ -18,6 +28,8 @@ test_background_CPPFLAGS = \ test_background_CFLAGS = \ $(SETTINGS_PLUGIN_CFLAGS) \ $(MATE_DESKTOP_CFLAGS) \ + $(GTK_LAYER_SHELL_CFLAGS) \ + $(WAYLAND_CLIENT_CFLAGS) \ $(AM_CFLAGS) \ $(WARN_CFLAGS) @@ -26,6 +38,8 @@ test_background_LDADD = \ $(SETTINGS_PLUGIN_LIBS) \ $(MATE_DESKTOP_LIBS) \ $(X11_LIBS) \ + $(GTK_LAYER_SHELL_LIBS) \ + $(WAYLAND_CLIENT_LIBS) \ $(NULL) plugin_LTLIBRARIES = \ @@ -37,17 +51,22 @@ libbackground_la_SOURCES = \ msd-background-plugin.c \ msd-background-manager.h \ msd-background-manager.c \ + $(WAYLAND_BACKGROUND_SOURCES) \ $(NULL) libbackground_la_CPPFLAGS = \ -I$(top_srcdir)/mate-settings-daemon \ -I$(top_srcdir)/plugins/background/libbackground \ -DMATE_SETTINGS_LOCALEDIR=\""$(datadir)/locale"\" \ + $(GTK_LAYER_SHELL_CFLAGS) \ + $(WAYLAND_CLIENT_CFLAGS) \ $(AM_CPPFLAGS) libbackground_la_CFLAGS = \ $(SETTINGS_PLUGIN_CFLAGS) \ $(MATE_DESKTOP_CFLAGS) \ + $(GTK_LAYER_SHELL_CFLAGS) \ + $(WAYLAND_CLIENT_CFLAGS) \ $(AM_CFLAGS) \ $(WARN_CFLAGS) @@ -59,6 +78,8 @@ libbackground_la_LIBADD = \ $(SETTINGS_PLUGIN_LIBS) \ $(MATE_DESKTOP_LIBS) \ $(X11_LIBS) \ + $(GTK_LAYER_SHELL_LIBS) \ + $(WAYLAND_CLIENT_LIBS) \ $(NULL) plugin_in_files = \ diff --git a/plugins/background/msd-background-manager-wayland.c b/plugins/background/msd-background-manager-wayland.c new file mode 100644 index 0000000..b4ab88d --- /dev/null +++ b/plugins/background/msd-background-manager-wayland.c @@ -0,0 +1,650 @@ +/* -*- 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 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "config.h" + +#include <glib.h> +#include <glib/gi18n.h> +#include <gdk/gdk.h> +#include <gdk/gdkwayland.h> +#include <gtk/gtk.h> +#include <wayland-client.h> + +#include <gtk-layer-shell.h> + +#define MATE_DESKTOP_USE_UNSTABLE_API +#include <libmate-desktop/mate-bg.h> + +#include "msd-background-manager-wayland.h" + +struct _MsdBackgroundManagerWayland +{ + GdkDisplay *display; + GSettings *settings; + MateBG *bg; + GPtrArray *monitor_windows; + gboolean caja_desktop_active; +}; + +typedef struct +{ + GtkWidget *window; + GdkMonitor *monitor; + MsdBackgroundManagerWayland *manager; + cairo_surface_t *surface; + cairo_surface_t *fading_surface; + cairo_surface_t *end_surface; + guint fade_timeout_id; + gdouble fade_start_time; + gdouble fade_total_duration; + gboolean fade_is_first_frame; + int width; + int height; +} MonitorWindow; + +static void +free_monitor_window_surface (MonitorWindow *mw) +{ + if (mw->surface != NULL) { + cairo_surface_destroy (mw->surface); + mw->surface = NULL; + } +} + +static gdouble +get_current_time (void) +{ + return g_get_monotonic_time () / 1000000.0; +} + +/* Check the GSettings key that indicates whether Caja is configured to + * draw desktop icons. When this is enabled Caja draws the desktop itself + * so we must not create our own layer-shell background windows. + */ +static gboolean +caja_desktop_is_showing (MsdBackgroundManagerWayland *manager) +{ + return g_settings_get_boolean (manager->settings, MATE_BG_KEY_SHOW_DESKTOP); +} + +static gboolean +animations_are_disabled (MonitorWindow *mw G_GNUC_UNUSED) +{ + GtkSettings *settings; + gboolean enabled; + + settings = gtk_settings_get_default (); + if (settings == NULL) + return TRUE; + + g_object_get (settings, "gtk-enable-animations", &enabled, NULL); + + return !enabled; +} + +static void +cancel_fade (MonitorWindow *mw) +{ + if (mw->fade_timeout_id == 0) + return; + + g_source_remove (mw->fade_timeout_id); + mw->fade_timeout_id = 0; +} + +static void +on_fade_finished (MonitorWindow *mw) +{ + cairo_t *cr; + + mw->fade_timeout_id = 0; + + if (mw->fading_surface == NULL || mw->end_surface == NULL) { + if (mw->end_surface != NULL) { + cairo_surface_destroy (mw->end_surface); + mw->end_surface = NULL; + } + return; + } + + cr = cairo_create (mw->fading_surface); + cairo_set_source_surface (cr, mw->end_surface, 0, 0); + cairo_paint (cr); + cairo_destroy (cr); + + cairo_surface_destroy (mw->end_surface); + mw->end_surface = NULL; + + free_monitor_window_surface (mw); + mw->surface = mw->fading_surface; + mw->fading_surface = NULL; + + gtk_widget_queue_draw (mw->window); +} + +static gboolean +on_fade_tick (MonitorWindow *mw) +{ + gdouble now, percent_done; + cairo_t *cr; + cairo_status_t status; + + if (mw->fading_surface == NULL || mw->end_surface == NULL) + return FALSE; + + now = get_current_time (); + percent_done = (now - mw->fade_start_time) / mw->fade_total_duration; + percent_done = CLAMP (percent_done, 0.0, 1.0); + + /* If it's taking a long time to get to the first frame, + * then lengthen the duration, so the user will get to see + * the effect. + */ + if (mw->fade_is_first_frame && percent_done > .33) { + mw->fade_is_first_frame = FALSE; + mw->fade_total_duration *= 1.5; + return TRUE; + } + + if (animations_are_disabled (mw)) + return FALSE; + + /* Accumulate the end surface in place, so the fade is + * exponential rather than linear (looks good, matches X11). + */ + cr = cairo_create (mw->fading_surface); + cairo_set_source_surface (cr, mw->end_surface, 0, 0); + cairo_paint_with_alpha (cr, percent_done); + status = cairo_status (cr); + cairo_destroy (cr); + + if (status == CAIRO_STATUS_SUCCESS) + gtk_widget_queue_draw (mw->window); + + return percent_done <= .99; +} + +static void +set_surface_instantly (MonitorWindow *mw, cairo_surface_t *new_surface) +{ + if (mw->fade_timeout_id != 0) { + if (mw->fading_surface != NULL) { + cairo_surface_destroy (mw->fading_surface); + mw->fading_surface = NULL; + } + if (mw->end_surface != NULL) { + cairo_surface_destroy (mw->end_surface); + mw->end_surface = NULL; + } + g_source_remove (mw->fade_timeout_id); + mw->fade_timeout_id = 0; + } + free_monitor_window_surface (mw); + mw->surface = new_surface; + gtk_widget_queue_draw (mw->window); +} + +static void +start_or_continue_fade (MsdBackgroundManagerWayland *manager, + MonitorWindow *mw, + cairo_surface_t *new_surface) +{ + gboolean fade_enabled; + gboolean animations_enabled; + cairo_t *cr; + GSource *source; + + fade_enabled = g_settings_get_boolean (manager->settings, + MATE_BG_KEY_BACKGROUND_FADE); + animations_enabled = !animations_are_disabled (mw); + + if (!fade_enabled || !animations_enabled || mw->surface == NULL) { + /* No fade: swap instantly. This also covers the first render + * after startup. + */ + set_surface_instantly (mw, new_surface); + return; + } + + /* If a fade is already running, restart from the current blended + * state (its completion commits the blend into mw->surface). + */ + cancel_fade (mw); + + mw->fading_surface = cairo_surface_create_similar (mw->surface, + cairo_surface_get_content (mw->surface), + mw->width, mw->height); + cr = cairo_create (mw->fading_surface); + cairo_set_source_surface (cr, mw->surface, 0, 0); + cairo_paint (cr); + cairo_destroy (cr); + + mw->end_surface = new_surface; + mw->fade_start_time = get_current_time (); + mw->fade_total_duration = 0.75; + mw->fade_is_first_frame = TRUE; + + source = g_timeout_source_new (1000 / 60.0); + g_source_set_callback (source, (GSourceFunc) on_fade_tick, mw, + (GDestroyNotify) on_fade_finished); + mw->fade_timeout_id = g_source_attach (source, NULL); + g_source_unref (source); + + gtk_widget_queue_draw (mw->window); +} + +static void +render_surface_at_size (MsdBackgroundManagerWayland *manager, + MonitorWindow *mw, + int width, + int height, + gboolean animate) +{ + GdkPixbuf *pixbuf; + cairo_surface_t *new_surface; + cairo_t *cr; + + if (width <= 0 || height <= 0) + return; + + pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE, 8, + width, height); + if (pixbuf == NULL) { + g_warning ("msd-background-wayland: unable to create %dx%d pixbuf", + width, height); + return; + } + + mate_bg_draw (manager->bg, pixbuf, NULL, FALSE); + + new_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + width, height); + cr = cairo_create (new_surface); + gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0); + cairo_paint (cr); + cairo_destroy (cr); + + g_object_unref (pixbuf); + + mw->width = width; + mw->height = height; + + if (animate) + start_or_continue_fade (manager, mw, new_surface); + else + set_surface_instantly (mw, new_surface); +} + +static void +render_surface_for_monitor (MsdBackgroundManagerWayland *manager, + MonitorWindow *mw, + gboolean animate) +{ + GdkRectangle rect; + + gdk_monitor_get_geometry (mw->monitor, &rect); + render_surface_at_size (manager, mw, rect.width, rect.height, animate); +} + +static void +on_window_size_allocate (GtkWidget *widget G_GNUC_UNUSED, + GtkAllocation *alloc, + MonitorWindow *mw) +{ + if (alloc->width <= 0 || alloc->height <= 0) + return; + + if (mw->surface == NULL) + return; + + /* The layer surface is anchored to the output edges, so when the + * output resolution or scale changes the compositor resizes the + * window. Re-render at the new size so the wallpaper adapts even + * if no monitors-changed event arrives. + */ + if (alloc->width == mw->width && alloc->height == mw->height) + return; + + g_debug ("msd-background-wayland: window resized to %dx%d, re-rendering", + alloc->width, alloc->height); + render_surface_at_size (mw->manager, mw, + alloc->width, alloc->height, FALSE); +} + +static gboolean +on_window_draw (GtkWidget *widget, + cairo_t *cr, + MonitorWindow *mw) +{ + GtkAllocation alloc; + cairo_surface_t *src; + gdouble sx, sy; + + if (mw->surface == NULL && mw->fading_surface == NULL) + return FALSE; + + src = (mw->fading_surface != NULL) ? mw->fading_surface : mw->surface; + + gtk_widget_get_allocation (widget, &alloc); + if (alloc.width <= 0 || alloc.height <= 0) + return FALSE; + + sx = alloc.width / (gdouble) cairo_image_surface_get_width (src); + sy = alloc.height / (gdouble) cairo_image_surface_get_height (src); + cairo_scale (cr, sx, sy); + cairo_set_source_surface (cr, src, 0, 0); + cairo_paint (cr); + + return FALSE; +} + +static void +redraw_all (MsdBackgroundManagerWayland *manager) +{ + guint i; + + for (i = 0; i < manager->monitor_windows->len; i++) { + MonitorWindow *mw = g_ptr_array_index (manager->monitor_windows, i); + render_surface_for_monitor (manager, mw, TRUE); + gtk_widget_queue_draw (mw->window); + } +} + +static void +on_window_destroy (GtkWidget *widget G_GNUC_UNUSED, + MonitorWindow *mw) +{ + if (mw->fade_timeout_id != 0) { + /* Make on_fade_finished a no-op, then drop the source */ + if (mw->fading_surface != NULL) { + cairo_surface_destroy (mw->fading_surface); + mw->fading_surface = NULL; + } + if (mw->end_surface != NULL) { + cairo_surface_destroy (mw->end_surface); + mw->end_surface = NULL; + } + g_source_remove (mw->fade_timeout_id); + mw->fade_timeout_id = 0; + } + free_monitor_window_surface (mw); + g_free (mw); +} + +static void +set_input_region_empty (GtkWidget *window) +{ + struct wl_surface *wl_surface; + GdkWindow *gdk_window; + + gdk_window = gtk_widget_get_window (window); + if (gdk_window == NULL) + return; + + wl_surface = gdk_wayland_window_get_wl_surface (gdk_window); + if (wl_surface != NULL) + wl_surface_set_input_region (wl_surface, NULL); +} + +static MonitorWindow * +create_monitor_window (MsdBackgroundManagerWayland *manager G_GNUC_UNUSED, + GdkMonitor *monitor) +{ + MonitorWindow *mw; + GtkWidget *window; + GdkRectangle rect; + guint i; + + gdk_monitor_get_geometry (monitor, &rect); + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_widget_set_app_paintable (window, TRUE); + gtk_widget_set_size_request (window, rect.width, rect.height); + gtk_window_set_default_size (GTK_WINDOW (window), rect.width, rect.height); + gtk_window_set_resizable (GTK_WINDOW (window), FALSE); + + g_debug ("msd-background-wayland: monitor %dx%d at %d,%d", rect.width, rect.height, + rect.x, rect.y); + + gtk_layer_init_for_window (GTK_WINDOW (window)); + gtk_layer_set_layer (GTK_WINDOW (window), GTK_LAYER_SHELL_LAYER_BACKGROUND); + gtk_layer_set_namespace (GTK_WINDOW (window), "mate-background"); + gtk_layer_set_monitor (GTK_WINDOW (window), monitor); + for (i = 0; i < GTK_LAYER_SHELL_EDGE_ENTRY_NUMBER; i++) + gtk_layer_set_anchor (GTK_WINDOW (window), i, TRUE); + gtk_layer_set_keyboard_mode (GTK_WINDOW (window), GTK_LAYER_SHELL_KEYBOARD_MODE_NONE); + + mw = g_new0 (MonitorWindow, 1); + mw->window = window; + mw->monitor = monitor; + mw->manager = manager; + mw->width = rect.width; + mw->height = rect.height; + + g_signal_connect (window, "draw", G_CALLBACK (on_window_draw), mw); + g_signal_connect (window, "size-allocate", G_CALLBACK (on_window_size_allocate), mw); + g_signal_connect (window, "destroy", G_CALLBACK (on_window_destroy), mw); + + gtk_widget_show (window); + + set_input_region_empty (window); + + return mw; +} + +static void +destroy_all_monitor_windows (MsdBackgroundManagerWayland *manager) +{ + guint i; + + for (i = 0; i < manager->monitor_windows->len; i++) { + MonitorWindow *mw = g_ptr_array_index (manager->monitor_windows, i); + gtk_widget_destroy (mw->window); + } + g_ptr_array_set_size (manager->monitor_windows, 0); +} + +static void +rebuild_monitor_windows (MsdBackgroundManagerWayland *manager) +{ + GdkDisplay *display = manager->display; + int n_monitors; + int i; + + destroy_all_monitor_windows (manager); + + /* Caja's desktop window (layer "desktop", BACKGROUND) owns the + * wallpaper and icons. When the "show-desktop-icons" setting is + * enabled we must not map our own layer windows, or we would + * cover it. + */ + if (caja_desktop_is_showing (manager)) { + manager->caja_desktop_active = TRUE; + g_debug ("msd-background-wayland: caja desktop detected, background disabled"); + return; + } + manager->caja_desktop_active = FALSE; + + n_monitors = gdk_display_get_n_monitors (display); + for (i = 0; i < n_monitors; i++) { + GdkMonitor *monitor; + MonitorWindow *mw; + + monitor = gdk_display_get_monitor (display, i); + mw = create_monitor_window (manager, monitor); + g_ptr_array_add (manager->monitor_windows, mw); + } +} + +static void +on_show_desktop_icons_changed (GSettings *settings G_GNUC_UNUSED, + const gchar *key G_GNUC_UNUSED, + MsdBackgroundManagerWayland *manager) +{ + gboolean active; + + active = caja_desktop_is_showing (manager); + if (active == manager->caja_desktop_active) + return; + + manager->caja_desktop_active = active; + + if (active) { + g_debug ("msd-background-wayland: caja desktop icons enabled, hiding background"); + destroy_all_monitor_windows (manager); + } else { + g_debug ("msd-background-wayland: caja desktop icons disabled, showing background"); + rebuild_monitor_windows (manager); + redraw_all (manager); + } +} + +static void +on_monitors_changed (GdkScreen *screen G_GNUC_UNUSED, + MsdBackgroundManagerWayland *manager) +{ + guint n_monitors = gdk_display_get_n_monitors (manager->display); + guint i; + + /* A monitor set change (added/removed) requires recreating the + * layer windows. A pure geometry/scale change (e.g. the compositor + * output is being resized) keeps the same window count, so just + * re-render in place at the new size - destroying and recreating + * windows mid-resize races with the compositor and leaves the + * wallpaper missing. + */ + if (n_monitors != manager->monitor_windows->len) { + g_debug ("msd-background-wayland: monitors changed, rebuilding (%u monitors)", + n_monitors); + rebuild_monitor_windows (manager); + redraw_all (manager); + return; + } + + g_debug ("msd-background-wayland: monitors changed, re-rendering in place (%u monitors)", + n_monitors); + + for (i = 0; i < manager->monitor_windows->len; i++) { + MonitorWindow *mw = g_ptr_array_index (manager->monitor_windows, i); + render_surface_for_monitor (manager, mw, FALSE); + gtk_widget_queue_draw (mw->window); + } +} + +static void +on_bg_changed (MateBG *bg G_GNUC_UNUSED, + MsdBackgroundManagerWayland *manager) +{ + if (manager->caja_desktop_active) + return; + + g_debug ("msd-background-wayland: background changed, redrawing"); + redraw_all (manager); +} + +static gboolean +settings_change_event_idle_cb (MsdBackgroundManagerWayland *manager) +{ + g_debug ("msd-background-wayland: applying settings change"); + mate_bg_load_from_preferences (manager->bg); + return FALSE; +} + +static gboolean +settings_change_event_cb (GSettings *settings G_GNUC_UNUSED, + gpointer keys G_GNUC_UNUSED, + gint n_keys G_GNUC_UNUSED, + MsdBackgroundManagerWayland *manager) +{ + g_idle_add ((GSourceFunc) settings_change_event_idle_cb, manager); + return FALSE; +} + +MsdBackgroundManagerWayland * +msd_background_manager_wayland_new (GError **error) +{ + MsdBackgroundManagerWayland *manager; + GdkDisplay *display; + + display = gdk_display_get_default (); + if (display == NULL || !GDK_IS_WAYLAND_DISPLAY (display)) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "A Wayland display is required for the Wayland background backend"); + return NULL; + } + + manager = g_new0 (MsdBackgroundManagerWayland, 1); + manager->display = display; + manager->monitor_windows = g_ptr_array_new (); + + manager->settings = g_settings_new (MATE_BG_SCHEMA); + manager->bg = mate_bg_new (); + + g_signal_connect (manager->bg, "changed", + G_CALLBACK (on_bg_changed), manager); + g_signal_connect (manager->bg, "transitioned", + G_CALLBACK (on_bg_changed), manager); + g_signal_connect (manager->settings, "change-event", + G_CALLBACK (settings_change_event_cb), manager); + g_signal_connect (gdk_screen_get_default (), "monitors-changed", + G_CALLBACK (on_monitors_changed), manager); + + g_debug ("msd-background-wayland: initializing on %d monitors", + gdk_display_get_n_monitors (display)); + + mate_bg_load_from_gsettings (manager->bg, manager->settings); + + rebuild_monitor_windows (manager); + redraw_all (manager); + + g_signal_connect (manager->settings, "changed::show-desktop-icons", + G_CALLBACK (on_show_desktop_icons_changed), manager); + + return manager; +} + +void +msd_background_manager_wayland_destroy (MsdBackgroundManagerWayland *manager) +{ + if (manager == NULL) + return; + + g_signal_handlers_disconnect_by_func (gdk_screen_get_default (), + G_CALLBACK (on_monitors_changed), manager); + + destroy_all_monitor_windows (manager); + g_ptr_array_free (manager->monitor_windows, TRUE); + + if (manager->settings != NULL) { + g_signal_handlers_disconnect_by_func (manager->settings, + G_CALLBACK (settings_change_event_cb), manager); + g_signal_handlers_disconnect_by_func (manager->settings, + G_CALLBACK (on_show_desktop_icons_changed), manager); + g_object_unref (manager->settings); + } + + if (manager->bg != NULL) { + g_signal_handlers_disconnect_by_func (manager->bg, + G_CALLBACK (on_bg_changed), manager); + g_object_unref (manager->bg); + } + + g_free (manager); +} diff --git a/plugins/background/msd-background-manager-wayland.h b/plugins/background/msd-background-manager-wayland.h new file mode 100644 index 0000000..8125f58 --- /dev/null +++ b/plugins/background/msd-background-manager-wayland.h @@ -0,0 +1,38 @@ +/* -*- 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 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef MSD_BACKGROUND_MANAGER_WAYLAND_H +#define MSD_BACKGROUND_MANAGER_WAYLAND_H + +#include <glib-object.h> +#include <glib.h> +#include <gdk/gdk.h> + +G_BEGIN_DECLS + +typedef struct _MsdBackgroundManagerWayland MsdBackgroundManagerWayland; + +MsdBackgroundManagerWayland *msd_background_manager_wayland_new (GError **error); + +void msd_background_manager_wayland_destroy (MsdBackgroundManagerWayland *manager); + +G_END_DECLS + +#endif /* MSD_BACKGROUND_MANAGER_WAYLAND_H */ diff --git a/plugins/background/msd-background-manager.c b/plugins/background/msd-background-manager.c index 5264bfa..514f74c 100644 --- a/plugins/background/msd-background-manager.c +++ b/plugins/background/msd-background-manager.c @@ -37,7 +37,12 @@ #include <glib.h> #include <glib/gi18n.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ +#ifdef HAVE_WAYLAND +#include <gdk/gdkwayland.h> +#endif /* HAVE_WAYLAND */ #include <gio/gio.h> #define MATE_DESKTOP_USE_UNSTABLE_API @@ -47,6 +52,10 @@ #include "mate-settings-profile.h" #include "msd-background-manager.h" +#ifdef HAVE_WAYLAND +#include "msd-background-manager-wayland.h" +#endif /* HAVE_WAYLAND */ + struct _MsdBackgroundManager { GObject parent; @@ -55,6 +64,10 @@ struct _MsdBackgroundManager { cairo_surface_t *surface; gboolean draw_in_progress; + +#ifdef HAVE_WAYLAND + MsdBackgroundManagerWayland *wayland; +#endif /* HAVE_WAYLAND */ }; G_DEFINE_TYPE (MsdBackgroundManager, msd_background_manager, G_TYPE_OBJECT) @@ -241,6 +254,33 @@ msd_background_manager_start (MsdBackgroundManager *manager, g_debug ("Starting background manager"); mate_settings_profile_start (NULL); +#ifdef HAVE_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { + manager->wayland = msd_background_manager_wayland_new (error); + if (manager->wayland == NULL) { + mate_settings_profile_end (NULL); + return FALSE; + } + + mate_settings_profile_end (NULL); + return TRUE; + } +#endif /* HAVE_WAYLAND */ + +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "X11 display is required for the background plugin"); + mate_settings_profile_end (NULL); + return FALSE; + } +#else + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The background plugin was built without X11 support"); + mate_settings_profile_end (NULL); + return FALSE; +#endif /* GDK_WINDOWING_X11 */ + manager->settings = g_settings_new (MATE_BG_SCHEMA); manager->bg = mate_bg_new(); @@ -265,6 +305,19 @@ msd_background_manager_stop (MsdBackgroundManager *manager) { g_debug ("Stopping background manager"); +#ifdef HAVE_WAYLAND + if (manager->wayland != NULL) { + msd_background_manager_wayland_destroy (manager->wayland); + manager->wayland = NULL; + return; + } +#endif /* HAVE_WAYLAND */ + +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return; +#endif /* GDK_WINDOWING_X11 */ + disconnect_screen_signals (manager); if (manager->settings != NULL) { diff --git a/plugins/clipboard/Makefile.am b/plugins/clipboard/Makefile.am index 414fff1..19c9c24 100644 --- a/plugins/clipboard/Makefile.am +++ b/plugins/clipboard/Makefile.am @@ -1,5 +1,25 @@ NULL = +if BUILD_WAYLAND +WAYLAND_PROTOCOL_XML = $(WAYLAND_PROTOCOLS_DATADIR)/staging/ext-data-control/ext-data-control-v1.xml +WAYLAND_PROTOCOL_GENERATED = \ + ext-data-control-v1-client.c \ + ext-data-control-v1-client.h \ + $(NULL) +WAYLAND_CLIPBOARD_SOURCES = \ + msd-clipboard-manager-wayland.c \ + msd-clipboard-manager-wayland.h \ + $(NULL) +else +WAYLAND_PROTOCOL_XML = +WAYLAND_PROTOCOL_GENERATED = +WAYLAND_CLIPBOARD_SOURCES = +endif + +BUILT_SOURCES = \ + $(WAYLAND_PROTOCOL_GENERATED) \ + $(NULL) + plugin_LTLIBRARIES = \ libclipboard.la \ $(NULL) @@ -9,19 +29,23 @@ libclipboard_la_SOURCES = \ msd-clipboard-plugin.c \ msd-clipboard-manager.h \ msd-clipboard-manager.c \ + $(WAYLAND_CLIPBOARD_SOURCES) \ xutils.h \ xutils.c \ list.h \ list.c \ + $(WAYLAND_PROTOCOL_GENERATED) \ $(NULL) libclipboard_la_CPPFLAGS = \ -I$(top_srcdir)/mate-settings-daemon \ -DMATE_SETTINGS_LOCALEDIR=\""$(datadir)/locale"\" \ + $(WAYLAND_CLIENT_CFLAGS) \ $(AM_CPPFLAGS) libclipboard_la_CFLAGS = \ $(SETTINGS_PLUGIN_CFLAGS) \ + $(WAYLAND_CLIENT_CFLAGS) \ $(AM_CFLAGS) \ $(WARN_CFLAGS) @@ -33,8 +57,17 @@ libclipboard_la_LIBADD = \ $(SETTINGS_PLUGIN_LIBS) \ $(X11_LIBS) \ $(XINPUT_LIBS) \ + $(WAYLAND_CLIENT_LIBS) \ $(NULL) +$(WAYLAND_PROTOCOL_GENERATED): $(WAYLAND_PROTOCOL_XML) + +ext-data-control-v1-client.h: $(WAYLAND_PROTOCOL_XML) + $(AM_V_GEN)$(WAYLAND_SCANNER) client-header $(WAYLAND_PROTOCOL_XML) $@ + +ext-data-control-v1-client.c: $(WAYLAND_PROTOCOL_XML) + $(AM_V_GEN)$(WAYLAND_SCANNER) private-code $(WAYLAND_PROTOCOL_XML) $@ + plugin_in_files = \ clipboard.mate-settings-plugin.desktop.in \ $(NULL) @@ -47,10 +80,12 @@ EXTRA_DIST = \ CLEANFILES = \ $(plugin_DATA) \ + $(WAYLAND_PROTOCOL_GENERATED) \ $(NULL) DISTCLEANFILES = \ $(plugin_DATA) \ + $(WAYLAND_PROTOCOL_GENERATED) \ $(NULL) $(plugin_DATA): $(plugin_in_files) diff --git a/plugins/clipboard/msd-clipboard-manager-wayland.c b/plugins/clipboard/msd-clipboard-manager-wayland.c new file mode 100644 index 0000000..a8701c8 --- /dev/null +++ b/plugins/clipboard/msd-clipboard-manager-wayland.c @@ -0,0 +1,769 @@ +/* -*- 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 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "config.h" + +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <string.h> + +#include <glib.h> +#include <glib-unix.h> +#include <gio/gio.h> +#include <wayland-client.h> + +#include "ext-data-control-v1-client.h" +#include "msd-clipboard-manager-wayland.h" + +typedef enum +{ + MSD_WAYLAND_SELECTION_CLIPBOARD, + MSD_WAYLAND_SELECTION_PRIMARY +} SelectionKind; + +typedef struct +{ + gchar *mime; + GByteArray *data; +} ClipEntry; + +typedef struct _MsdClipboardManagerWayland MsdClipboardManagerWayland; + +typedef struct +{ + MsdClipboardManagerWayland *wl; + SelectionKind kind; + gint fd; + guint source_id; + gchar *mime; + GByteArray *data; + gboolean done; +} ClipReceive; + +typedef struct +{ + MsdClipboardManagerWayland *wl; + SelectionKind kind; + struct ext_data_control_offer_v1 *offer; + GPtrArray *cache; + GPtrArray *receives; + struct ext_data_control_source_v1 *source; + gboolean reoffer_pending; + guint reoffer_timeout; + gint64 last_activity_ms; +} SelectionState; + +struct _MsdClipboardManagerWayland +{ + struct wl_display *display; + struct wl_registry *registry; + struct ext_data_control_manager_v1 *dc_manager; + struct wl_seat *seat; + struct ext_data_control_device_v1 *device; + guint display_source; + + struct ext_data_control_offer_v1 *pending_offer; + GPtrArray *pending_mimes; + + SelectionState clipboard; + SelectionState primary; +}; + +static void selection_state_clear (SelectionState *state); + +static void +clip_entry_free (gpointer data) +{ + ClipEntry *entry = data; + + if (entry == NULL) + return; + + g_free (entry->mime); + if (entry->data != NULL) + g_byte_array_free (entry->data, TRUE); + g_free (entry); +} + +static SelectionState * +state_from_kind (MsdClipboardManagerWayland *wl, + SelectionKind kind) +{ + if (kind == MSD_WAYLAND_SELECTION_PRIMARY) + return &wl->primary; + + return &wl->clipboard; +} + +static void +start_reoffer (SelectionState *state); + +#define REOFFER_WINDOW_MS 2000 +#define REOFFER_QUIET_MS 1000 + +static void +cancel_reoffer (SelectionState *state) +{ + if (state->reoffer_timeout != 0) { + g_source_remove (state->reoffer_timeout); + state->reoffer_timeout = 0; + } +} + +static gboolean +reoffer_timeout_cb (gpointer data) +{ + SelectionState *state = data; + + state->reoffer_timeout = 0; + start_reoffer (state); + + return G_SOURCE_REMOVE; +} + +static void +schedule_reoffer (SelectionState *state) +{ + if (state->cache == NULL || state->cache->len == 0) + return; + if (state->wl->dc_manager == NULL || state->wl->device == NULL) + return; + if (state->source != NULL) + return; + + if (state->reoffer_timeout == 0) { + gint64 now; + guint delay; + + now = g_get_monotonic_time () / 1000; + + /* Keep a longer delay right after clipboard activity: an app + * may be about to re-assert its selection (e.g. GTK re-serving + * a torn copy if we steal ownership mid-store). Once the + * selection has been quiet, fall back to a shorter delay so + * pasting works soon after the source app is closed. */ + delay = REOFFER_WINDOW_MS - (now - state->last_activity_ms); + if (delay > REOFFER_WINDOW_MS) + delay = REOFFER_WINDOW_MS; + if (delay < REOFFER_QUIET_MS) + delay = REOFFER_QUIET_MS; + + state->reoffer_timeout = g_timeout_add (delay, + reoffer_timeout_cb, + state); + } +} + +static void +finalize_cache (SelectionState *state) +{ + GPtrArray *new_cache; + guint i; + + if (state->receives == NULL) + return; + + new_cache = g_ptr_array_new_with_free_func (clip_entry_free); + + for (i = 0; i < state->receives->len; i++) { + ClipReceive *recv; + ClipEntry *entry; + + recv = state->receives->pdata[i]; + + entry = g_new0 (ClipEntry, 1); + entry->mime = recv->mime; + entry->data = recv->data; + recv->mime = NULL; + recv->data = NULL; + + g_ptr_array_add (new_cache, entry); + } + + if (state->cache != NULL) + g_ptr_array_free (state->cache, TRUE); + state->cache = new_cache; + + for (i = 0; i < state->receives->len; i++) { + ClipReceive *recv; + + recv = state->receives->pdata[i]; + if (recv->fd != -1) + close (recv->fd); + g_free (recv); + } + g_ptr_array_free (state->receives, FALSE); + state->receives = NULL; +} + +static void +receive_finish (ClipReceive *recv) +{ + SelectionState *state; + guint i; + + if (recv->done) + return; + recv->done = TRUE; + + if (recv->fd != -1) { + close (recv->fd); + recv->fd = -1; + } + + state = state_from_kind (recv->wl, recv->kind); + + if (state->receives == NULL) + return; + + for (i = 0; i < state->receives->len; i++) { + ClipReceive *other; + + other = state->receives->pdata[i]; + if (!other->done) + return; + } + + finalize_cache (state); + + if (state->reoffer_pending) { + state->reoffer_pending = FALSE; + schedule_reoffer (state); + } +} + +static void +receive_cancel (ClipReceive *recv) +{ + if (recv->source_id != 0) + g_source_remove (recv->source_id); + if (recv->fd != -1) + close (recv->fd); + g_free (recv->mime); + if (recv->data != NULL) + g_byte_array_free (recv->data, TRUE); + g_free (recv); +} + +static gboolean +receive_dispatch_cb (gint fd, + GIOCondition condition, + gpointer user_data) +{ + ClipReceive *recv = user_data; + guchar buf[4096]; + gssize n; + + n = read (fd, buf, sizeof (buf)); + if (n > 0) { + g_byte_array_append (recv->data, buf, n); + return G_SOURCE_CONTINUE; + } + if (n == 0) { + receive_finish (recv); + return G_SOURCE_REMOVE; + } + if (n < 0 && errno == EINTR) + return G_SOURCE_CONTINUE; + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + if (condition & (G_IO_HUP | G_IO_ERR)) { + receive_finish (recv); + return G_SOURCE_REMOVE; + } + return G_SOURCE_CONTINUE; + } + + receive_finish (recv); + return G_SOURCE_REMOVE; +} + +static void +start_receives (SelectionState *state, + struct ext_data_control_offer_v1 *offer, + GPtrArray *mimes) +{ + guint i; + + if (state->receives != NULL) { + for (i = 0; i < state->receives->len; i++) + receive_cancel (state->receives->pdata[i]); + g_ptr_array_free (state->receives, FALSE); + state->receives = NULL; + } + + state->receives = g_ptr_array_new (); + + if (mimes == NULL) + return; + + state->last_activity_ms = g_get_monotonic_time () / 1000; + + for (i = 0; i < mimes->len; i++) { + ClipReceive *recv; + int fds[2]; + + if (pipe (fds) < 0) + continue; + + ext_data_control_offer_v1_receive (offer, mimes->pdata[i], fds[1]); + close (fds[1]); + + recv = g_new0 (ClipReceive, 1); + recv->wl = state->wl; + recv->kind = state->kind; + recv->fd = fds[0]; + recv->mime = g_strdup (mimes->pdata[i]); + recv->data = g_byte_array_new (); + fcntl (recv->fd, F_SETFL, O_NONBLOCK); + recv->source_id = g_unix_fd_add (recv->fd, + G_IO_IN | G_IO_HUP | G_IO_ERR, + receive_dispatch_cb, + recv); + g_ptr_array_add (state->receives, recv); + } +} + +static void +source_write_all (gint fd, + const guint8 *data, + gsize len) +{ + gsize total = 0; + gint flags; + + flags = fcntl (fd, F_GETFL, 0); + if (flags != -1) + fcntl (fd, F_SETFL, flags | O_NONBLOCK); + + while (total < len) { + gssize n; + + n = write (fd, data + total, len - total); + if (n > 0) { + total += n; + continue; + } + if (n < 0 && errno == EINTR) + continue; + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + GPollFD pfd; + + pfd.fd = fd; + pfd.events = G_IO_OUT; + if (g_poll (&pfd, 1, 10000) <= 0) + break; + continue; + } + break; + } + + close (fd); +} + +static void +handle_source_send (void *data, + struct ext_data_control_source_v1 *source, + const char *mime_type, + int32_t fd) +{ + SelectionState *state = data; + guint i; + + if (state->cache == NULL) { + close (fd); + return; + } + + for (i = 0; i < state->cache->len; i++) { + ClipEntry *entry; + + entry = state->cache->pdata[i]; + if (g_strcmp0 (entry->mime, mime_type) == 0) { + source_write_all (fd, entry->data->data, entry->data->len); + return; + } + } + + close (fd); +} + +static void +handle_source_cancelled (void *data, + struct ext_data_control_source_v1 *source) +{ + SelectionState *state = data; + + if (state->source == source) { + ext_data_control_source_v1_destroy (source); + state->source = NULL; + } + + state->last_activity_ms = g_get_monotonic_time () / 1000; +} + +static const struct ext_data_control_source_v1_listener source_listener = { + .send = handle_source_send, + .cancelled = handle_source_cancelled, +}; + +static void +start_reoffer (SelectionState *state) +{ + struct ext_data_control_source_v1 *source; + guint i; + + if (state->cache == NULL || state->cache->len == 0) + return; + if (state->wl->dc_manager == NULL || state->wl->device == NULL) + return; + if (state->source != NULL) + return; + + source = ext_data_control_manager_v1_create_data_source (state->wl->dc_manager); + ext_data_control_source_v1_add_listener (source, &source_listener, state); + + for (i = 0; i < state->cache->len; i++) { + ClipEntry *entry; + + entry = state->cache->pdata[i]; + ext_data_control_source_v1_offer (source, entry->mime); + } + + state->source = source; + + if (state->kind == MSD_WAYLAND_SELECTION_PRIMARY) + ext_data_control_device_v1_set_primary_selection (state->wl->device, source); + else + ext_data_control_device_v1_set_selection (state->wl->device, source); + + wl_display_flush (state->wl->display); +} + +static void +handle_offer (void *data, + struct ext_data_control_offer_v1 *offer, + const char *mime_type) +{ + MsdClipboardManagerWayland *wl = data; + + if (wl->pending_offer == offer && wl->pending_mimes != NULL) + g_ptr_array_add (wl->pending_mimes, g_strdup (mime_type)); +} + +static const struct ext_data_control_offer_v1_listener offer_listener = { + .offer = handle_offer, +}; + +static void +handle_selection_for (MsdClipboardManagerWayland *wl, + SelectionState *state, + struct ext_data_control_offer_v1 *offer) +{ + if (offer != NULL && state->source != NULL) { + /* The compositor is echoing our own set_selection back to + * us: state->source is still the current selection (it was + * not cancelled), so this offer describes our own data. + * Keep the source alive and ignore the echoed offer. + */ + if (state->offer != NULL && state->offer != offer) + ext_data_control_offer_v1_destroy (state->offer); + state->offer = offer; + + if (wl->pending_offer != NULL) + wl->pending_offer = NULL; + if (wl->pending_mimes != NULL) { + g_ptr_array_free (wl->pending_mimes, TRUE); + wl->pending_mimes = NULL; + } + return; + } + + if (state->source != NULL) { + ext_data_control_source_v1_destroy (state->source); + state->source = NULL; + } + + if (state->offer != NULL && state->offer != offer) { + ext_data_control_offer_v1_destroy (state->offer); + state->offer = NULL; + } + + if (offer == NULL) { + state->offer = NULL; + + if (state->receives != NULL && state->receives->len > 0) { + state->reoffer_pending = TRUE; + } else { + schedule_reoffer (state); + } + + return; + } + + state->offer = offer; + state->reoffer_pending = FALSE; + cancel_reoffer (state); + + if (wl->pending_offer == offer && wl->pending_mimes != NULL) { + GPtrArray *mimes; + + mimes = wl->pending_mimes; + wl->pending_mimes = NULL; + wl->pending_offer = NULL; + + start_receives (state, offer, mimes); + g_ptr_array_free (mimes, TRUE); + } else { + start_receives (state, offer, NULL); + } +} + +static void +handle_selection (void *data, + struct ext_data_control_device_v1 *device, + struct ext_data_control_offer_v1 *offer) +{ + MsdClipboardManagerWayland *wl = data; + + handle_selection_for (wl, &wl->clipboard, offer); +} + +static void +handle_primary_selection (void *data, + struct ext_data_control_device_v1 *device, + struct ext_data_control_offer_v1 *offer) +{ + MsdClipboardManagerWayland *wl = data; + + handle_selection_for (wl, &wl->primary, offer); +} + +static void +handle_data_offer (void *data, + struct ext_data_control_device_v1 *device, + struct ext_data_control_offer_v1 *offer) +{ + MsdClipboardManagerWayland *wl = data; + + if (wl->pending_offer != NULL) + ext_data_control_offer_v1_destroy (wl->pending_offer); + if (wl->pending_mimes != NULL) + g_ptr_array_free (wl->pending_mimes, TRUE); + + wl->pending_offer = offer; + wl->pending_mimes = g_ptr_array_new_with_free_func (g_free); + ext_data_control_offer_v1_add_listener (offer, &offer_listener, wl); +} + +static void +handle_finished (void *data, + struct ext_data_control_device_v1 *device) +{ + MsdClipboardManagerWayland *wl = data; + + ext_data_control_device_v1_destroy (device); + wl->device = NULL; +} + +static const struct ext_data_control_device_v1_listener device_listener = { + .data_offer = handle_data_offer, + .selection = handle_selection, + .finished = handle_finished, + .primary_selection = handle_primary_selection, +}; + +static void +registry_handle_global (void *data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) +{ + MsdClipboardManagerWayland *wl = data; + + if (g_strcmp0 (interface, "ext_data_control_manager_v1") == 0) { + wl->dc_manager = wl_registry_bind (registry, + name, + &ext_data_control_manager_v1_interface, + MIN (version, 1u)); + } else if (g_strcmp0 (interface, "wl_seat") == 0) { + wl->seat = wl_registry_bind (registry, + name, + &wl_seat_interface, + MIN (version, 5u)); + } +} + +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, +}; + +static gboolean +display_dispatch_cb (gint fd, + GIOCondition condition, + gpointer user_data) +{ + MsdClipboardManagerWayland *wl = user_data; + + if (condition & (G_IO_HUP | G_IO_ERR)) { + g_warning ("Clipboard manager: Wayland connection lost"); + return G_SOURCE_REMOVE; + } + + if (wl_display_dispatch (wl->display) == -1) { + g_warning ("Clipboard manager: Failed to dispatch Wayland events"); + return G_SOURCE_REMOVE; + } + + wl_display_flush (wl->display); + + return G_SOURCE_CONTINUE; +} + +static void +selection_state_clear (SelectionState *state) +{ + guint i; + + cancel_reoffer (state); + + if (state->source != NULL) { + ext_data_control_source_v1_destroy (state->source); + state->source = NULL; + } + + if (state->offer != NULL) { + ext_data_control_offer_v1_destroy (state->offer); + state->offer = NULL; + } + + if (state->receives != NULL) { + for (i = 0; i < state->receives->len; i++) + receive_cancel (state->receives->pdata[i]); + g_ptr_array_free (state->receives, FALSE); + state->receives = NULL; + } + + if (state->cache != NULL) { + g_ptr_array_free (state->cache, TRUE); + state->cache = NULL; + } +} + +MsdClipboardManagerWayland * +msd_clipboard_manager_wayland_new (GError **error) +{ + MsdClipboardManagerWayland *wl; + + wl = g_new0 (MsdClipboardManagerWayland, 1); + + wl->display = wl_display_connect (NULL); + if (wl->display == NULL) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not connect to the Wayland compositor"); + g_free (wl); + return NULL; + } + + wl->clipboard.wl = wl; + wl->clipboard.kind = MSD_WAYLAND_SELECTION_CLIPBOARD; + wl->primary.wl = wl; + wl->primary.kind = MSD_WAYLAND_SELECTION_PRIMARY; + + wl->registry = wl_display_get_registry (wl->display); + wl_registry_add_listener (wl->registry, ®istry_listener, wl); + + wl_display_roundtrip (wl->display); + + if (wl->dc_manager == NULL || wl->seat == NULL) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The compositor does not support the ext-data-control protocol"); + msd_clipboard_manager_wayland_destroy (wl); + return NULL; + } + + wl->device = ext_data_control_manager_v1_get_data_device (wl->dc_manager, wl->seat); + ext_data_control_device_v1_add_listener (wl->device, &device_listener, wl); + + wl->display_source = g_unix_fd_add (wl_display_get_fd (wl->display), + G_IO_IN | G_IO_HUP | G_IO_ERR, + display_dispatch_cb, + wl); + + wl_display_roundtrip (wl->display); + + return wl; +} + +void +msd_clipboard_manager_wayland_destroy (MsdClipboardManagerWayland *wl) +{ + if (wl == NULL) + return; + + if (wl->display_source != 0) { + g_source_remove (wl->display_source); + wl->display_source = 0; + } + + selection_state_clear (&wl->clipboard); + selection_state_clear (&wl->primary); + + if (wl->pending_offer != NULL) { + ext_data_control_offer_v1_destroy (wl->pending_offer); + wl->pending_offer = NULL; + } + if (wl->pending_mimes != NULL) { + g_ptr_array_free (wl->pending_mimes, TRUE); + wl->pending_mimes = NULL; + } + + if (wl->device != NULL) { + ext_data_control_device_v1_destroy (wl->device); + wl->device = NULL; + } + if (wl->dc_manager != NULL) { + ext_data_control_manager_v1_destroy (wl->dc_manager); + wl->dc_manager = NULL; + } + if (wl->seat != NULL) { + wl_seat_destroy (wl->seat); + wl->seat = NULL; + } + if (wl->registry != NULL) { + wl_registry_destroy (wl->registry); + wl->registry = NULL; + } + if (wl->display != NULL) { + wl_display_disconnect (wl->display); + wl->display = NULL; + } + + g_free (wl); +} diff --git a/plugins/clipboard/msd-clipboard-manager-wayland.h b/plugins/clipboard/msd-clipboard-manager-wayland.h new file mode 100644 index 0000000..886ed4d --- /dev/null +++ b/plugins/clipboard/msd-clipboard-manager-wayland.h @@ -0,0 +1,39 @@ +/* -*- 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 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef __MSD_CLIPBOARD_MANAGER_WAYLAND_H +#define __MSD_CLIPBOARD_MANAGER_WAYLAND_H + +#include <glib-object.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct _MsdClipboardManagerWayland MsdClipboardManagerWayland; + +MsdClipboardManagerWayland *msd_clipboard_manager_wayland_new (GError **error); +void msd_clipboard_manager_wayland_destroy (MsdClipboardManagerWayland *manager); + +#ifdef __cplusplus +} +#endif + +#endif /* __MSD_CLIPBOARD_MANAGER_WAYLAND_H */ diff --git a/plugins/clipboard/msd-clipboard-manager.c b/plugins/clipboard/msd-clipboard-manager.c index ef4007a..b885b89 100644 --- a/plugins/clipboard/msd-clipboard-manager.c +++ b/plugins/clipboard/msd-clipboard-manager.c @@ -36,8 +36,14 @@ #include <glib.h> #include <glib/gi18n.h> +#include <gio/gio.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif /* GDK_WINDOWING_WAYLAND */ #include <X11/Xlib.h> #include <X11/Xatom.h> @@ -46,6 +52,9 @@ #include "mate-settings-profile.h" #include "msd-clipboard-manager.h" +#ifdef HAVE_WAYLAND +#include "msd-clipboard-manager-wayland.h" +#endif /* HAVE_WAYLAND */ struct MsdClipboardManagerPrivate { @@ -59,6 +68,10 @@ struct MsdClipboardManagerPrivate Window requestor; Atom property; Time time; + +#ifdef HAVE_WAYLAND + MsdClipboardManagerWayland *wayland; +#endif /* HAVE_WAYLAND */ }; typedef struct @@ -930,16 +943,40 @@ start_clipboard_idle_cb (MsdClipboardManager *manager) } gboolean -msd_clipboard_manager_start (MsdClipboardManager *manager, + msd_clipboard_manager_start (MsdClipboardManager *manager, GError **error) { mate_settings_profile_start (NULL); - g_idle_add ((GSourceFunc) start_clipboard_idle_cb, manager); +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { + g_idle_add ((GSourceFunc) start_clipboard_idle_cb, manager); + + mate_settings_profile_end (NULL); + + return TRUE; + } +#endif /* GDK_WINDOWING_X11 */ + +#if defined (HAVE_WAYLAND) && defined (GDK_WINDOWING_WAYLAND) + if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { + manager->priv->wayland = msd_clipboard_manager_wayland_new (error); + if (manager->priv->wayland == NULL) { + mate_settings_profile_end (NULL); + return FALSE; + } + + mate_settings_profile_end (NULL); + return TRUE; + } +#endif /* HAVE_WAYLAND && GDK_WINDOWING_WAYLAND */ + + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The clipboard plugin requires an X11 or a Wayland display with the ext-data-control protocol"); mate_settings_profile_end (NULL); - return TRUE; + return FALSE; } void @@ -947,18 +984,32 @@ msd_clipboard_manager_stop (MsdClipboardManager *manager) { g_debug ("Stopping clipboard manager"); - clipboard_manager_watch_cb (manager, - manager->priv->window, - FALSE, - 0, - NULL); - XDestroyWindow (manager->priv->display, manager->priv->window); +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { + clipboard_manager_watch_cb (manager, + manager->priv->window, + FALSE, + 0, + NULL); + XDestroyWindow (manager->priv->display, manager->priv->window); + + list_foreach (manager->priv->conversions, (Callback) conversion_free, NULL); + list_free (manager->priv->conversions); - list_foreach (manager->priv->conversions, (Callback) conversion_free, NULL); - list_free (manager->priv->conversions); + list_foreach (manager->priv->contents, (Callback) target_data_unref, NULL); + list_free (manager->priv->contents); - list_foreach (manager->priv->contents, (Callback) target_data_unref, NULL); - list_free (manager->priv->contents); + return; + } +#endif /* GDK_WINDOWING_X11 */ + +#if defined (HAVE_WAYLAND) && defined (GDK_WINDOWING_WAYLAND) + if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { + msd_clipboard_manager_wayland_destroy (manager->priv->wayland); + manager->priv->wayland = NULL; + return; + } +#endif /* HAVE_WAYLAND && GDK_WINDOWING_WAYLAND */ } static void @@ -974,8 +1025,18 @@ msd_clipboard_manager_init (MsdClipboardManager *manager) { manager->priv = msd_clipboard_manager_get_instance_private (manager); - manager->priv->display = GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()); - +#ifdef HAVE_WAYLAND + manager->priv->wayland = NULL; +#endif /* HAVE_WAYLAND */ + +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + manager->priv->display = GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()); + else + manager->priv->display = NULL; +#else + manager->priv->display = NULL; +#endif /* GDK_WINDOWING_X11 */ } static void @@ -990,6 +1051,13 @@ msd_clipboard_manager_finalize (GObject *object) g_return_if_fail (clipboard_manager->priv != NULL); +#if defined (HAVE_WAYLAND) && defined (GDK_WINDOWING_WAYLAND) + if (clipboard_manager->priv->wayland != NULL) { + msd_clipboard_manager_wayland_destroy (clipboard_manager->priv->wayland); + clipboard_manager->priv->wayland = NULL; + } +#endif /* HAVE_WAYLAND && GDK_WINDOWING_WAYLAND */ + G_OBJECT_CLASS (msd_clipboard_manager_parent_class)->finalize (object); } diff --git a/plugins/common/eggaccelerators.c b/plugins/common/eggaccelerators.c index 0f924d3..77f320b 100644 --- a/plugins/common/eggaccelerators.c +++ b/plugins/common/eggaccelerators.c @@ -23,7 +23,9 @@ #include <stdlib.h> #include <string.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #include <gdk/gdkkeysyms.h> #include <gtk/gtk.h> @@ -566,6 +568,7 @@ static void reload_modmap (GdkKeymap *keymap, EggModmap *modmap) { +#ifdef GDK_WINDOWING_X11 XModifierKeymap *xmodmap; int map_size; int i; @@ -629,6 +632,11 @@ reload_modmap (GdkKeymap *keymap, g_free (keys); } + XFreeModifiermap (xmodmap); +#else + memset (modmap->mapping, 0, sizeof (modmap->mapping)); +#endif /* GDK_WINDOWING_X11 */ + /* Add in the not-really-virtual fixed entries */ modmap->mapping[EGG_MODMAP_ENTRY_SHIFT] |= EGG_VIRTUAL_SHIFT_MASK; modmap->mapping[EGG_MODMAP_ENTRY_CONTROL] |= EGG_VIRTUAL_CONTROL_MASK; @@ -638,8 +646,6 @@ reload_modmap (GdkKeymap *keymap, modmap->mapping[EGG_MODMAP_ENTRY_MOD3] |= EGG_VIRTUAL_MOD3_MASK; modmap->mapping[EGG_MODMAP_ENTRY_MOD4] |= EGG_VIRTUAL_MOD4_MASK; modmap->mapping[EGG_MODMAP_ENTRY_MOD5] |= EGG_VIRTUAL_MOD5_MASK; - - XFreeModifiermap (xmodmap); } const EggModmap* diff --git a/plugins/common/msd-input-helper.c b/plugins/common/msd-input-helper.c index 05fad76..b6ca524 100644 --- a/plugins/common/msd-input-helper.c +++ b/plugins/common/msd-input-helper.c @@ -22,7 +22,9 @@ #include "config.h" #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #include <sys/types.h> #include <X11/Xatom.h> @@ -32,13 +34,20 @@ gboolean supports_xinput_devices (void) { +#ifdef GDK_WINDOWING_X11 gint op_code, event, error; + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return FALSE; + return XQueryExtension (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "XInputExtension", &op_code, &event, &error); +#else + return FALSE; +#endif /* GDK_WINDOWING_X11 */ } static gboolean @@ -78,6 +87,9 @@ device_is_touchpad (XDeviceInfo *deviceinfo) display = gdk_display_get_default (); + if (!GDK_IS_X11_DISPLAY (display)) + return NULL; + if (deviceinfo->type != XInternAtom (GDK_DISPLAY_XDISPLAY (display), XI_TOUCHPAD, True)) return NULL; @@ -103,6 +115,9 @@ touchpad_is_present (void) guint i; gboolean retval; + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return FALSE; + if (supports_xinput_devices () == FALSE) return TRUE; diff --git a/plugins/common/msd-keygrab.c b/plugins/common/msd-keygrab.c index 0ea0cc1..184aa21 100644 --- a/plugins/common/msd-keygrab.c +++ b/plugins/common/msd-keygrab.c @@ -24,7 +24,9 @@ #include "config.h" #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #ifdef HAVE_X11_EXTENSIONS_XKB_H #include <X11/XKBlib.h> #include <X11/extensions/XKB.h> @@ -74,6 +76,10 @@ grab_key_real (guint keycode, gboolean grab, int mask) { +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return; + if (grab) { XGrabKey (GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), keycode, @@ -88,6 +94,9 @@ grab_key_real (guint keycode, mask, GDK_WINDOW_XID (root)); } +#else + g_warning ("Key grabbing is not supported without X11"); +#endif /* GDK_WINDOWING_X11 */ } /* Grab the key. In order to ignore MSD_IGNORED_MODS we need to grab @@ -203,6 +212,7 @@ key_uses_keycode (const Key *key, guint keycode) return FALSE; } +#ifdef GDK_WINDOWING_X11 gboolean match_key (Key *key, XEvent *event) { @@ -215,11 +225,9 @@ match_key (Key *key, XEvent *event) setup_modifiers (); -#ifdef HAVE_X11_EXTENSIONS_XKB_H if (have_xkb (event->xkey.display)) group = XkbGroupForCoreState (event->xkey.state); else -#endif group = (event->xkey.state & GDK_KEY_Mode_switch) ? 1 : 0; /* Check if we find a keysym that matches our current state */ @@ -244,3 +252,10 @@ match_key (Key *key, XEvent *event) return (key->state == (event->xkey.state & msd_used_mods) && key_uses_keycode (key, event->xkey.keycode)); } +#else /* !GDK_WINDOWING_X11 */ +gboolean +match_key (Key *key, XEvent *event) +{ + return FALSE; +} +#endif /* GDK_WINDOWING_X11 */ diff --git a/plugins/common/msd-osd-window.c b/plugins/common/msd-osd-window.c index d241cfb..5ac99a4 100644 --- a/plugins/common/msd-osd-window.c +++ b/plugins/common/msd-osd-window.c @@ -36,7 +36,7 @@ #include <glib.h> #include <glib/gi18n.h> #include <gtk/gtk.h> -#include <gdk/gdkx.h> +#include <gdk/gdk.h> #include "msd-osd-window.h" @@ -455,8 +455,8 @@ msd_osd_window_init (MsdOsdWindow *window) gtk_style_context_add_class (style, "window-frame"); /* assume 110x110 on a 640x480 display and scale from there */ - scalew = WidthOfScreen (gdk_x11_screen_get_xscreen (screen)) / (640.0 * window->priv->scale_factor); - scaleh = HeightOfScreen (gdk_x11_screen_get_xscreen (screen)) / (480.0 * window->priv->scale_factor); + scalew = gdk_screen_get_width (screen) / (640.0 * window->priv->scale_factor); + scaleh = gdk_screen_get_height (screen) / (480.0 * window->priv->scale_factor); scale = MIN (scalew, scaleh); size = 110 * MAX (1, scale); diff --git a/plugins/keybindings/keybindings.mate-settings-plugin.desktop.in b/plugins/keybindings/keybindings.mate-settings-plugin.desktop.in index c668e3f..dbed8b9 100644 --- a/plugins/keybindings/keybindings.mate-settings-plugin.desktop.in +++ b/plugins/keybindings/keybindings.mate-settings-plugin.desktop.in @@ -1,6 +1,7 @@ [MATE Settings Plugin] Module=keybindings IAge=0 +X11Only=true Name=Keybindings Description=Keybindings plugin Authors=AUTHOR diff --git a/plugins/keybindings/msd-keybindings-manager.c b/plugins/keybindings/msd-keybindings-manager.c index 5ce7f0d..324a976 100644 --- a/plugins/keybindings/msd-keybindings-manager.c +++ b/plugins/keybindings/msd-keybindings-manager.c @@ -34,7 +34,9 @@ #include <glib.h> #include <glib/gi18n.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #include <gtk/gtk.h> #include <X11/keysym.h> #include <gio/gio.h> @@ -519,6 +521,7 @@ gboolean msd_keybindings_manager_start (MsdKeybindingsManager *manager, GError **error) { +#ifdef GDK_WINDOWING_X11 GdkDisplay *dpy; GdkScreen *screen; GdkWindow *window; @@ -530,6 +533,15 @@ msd_keybindings_manager_start (MsdKeybindingsManager *manager, mate_settings_profile_start (NULL); dpy = gdk_display_get_default (); + + if (!GDK_IS_X11_DISPLAY (dpy)) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Keybindings are only supported on X11"); + return FALSE; + } + xdpy = GDK_DISPLAY_XDISPLAY (dpy); screen = gdk_display_get_default_screen (dpy); @@ -559,6 +571,13 @@ msd_keybindings_manager_start (MsdKeybindingsManager *manager, mate_settings_profile_end (NULL); return TRUE; +#else /* !GDK_WINDOWING_X11 */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Keybindings are only supported on X11"); + return FALSE; +#endif /* GDK_WINDOWING_X11 */ } void @@ -569,6 +588,11 @@ msd_keybindings_manager_stop (MsdKeybindingsManager *manager) g_debug ("Stopping keybindings manager"); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return; +#endif /* GDK_WINDOWING_X11 */ + if (p->client != NULL) { g_object_unref (p->client); p->client = NULL; diff --git a/plugins/keyboard/keyboard.mate-settings-plugin.desktop.in b/plugins/keyboard/keyboard.mate-settings-plugin.desktop.in index 00bd1d9..562f21d 100644 --- a/plugins/keyboard/keyboard.mate-settings-plugin.desktop.in +++ b/plugins/keyboard/keyboard.mate-settings-plugin.desktop.in @@ -1,6 +1,7 @@ [MATE Settings Plugin] Module=keyboard IAge=0 +X11Only=true Name=Keyboard Description=Keyboard plugin Authors= diff --git a/plugins/keyboard/msd-keyboard-manager.c b/plugins/keyboard/msd-keyboard-manager.c index 06bc322..7e7d3ae 100644 --- a/plugins/keyboard/msd-keyboard-manager.c +++ b/plugins/keyboard/msd-keyboard-manager.c @@ -34,8 +34,11 @@ #include <glib.h> #include <glib/gi18n.h> +#include <gio/gio.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #ifdef HAVE_X11_EXTENSIONS_XKB_H #include <X11/XKBlib.h> @@ -310,6 +313,20 @@ msd_keyboard_manager_start (MsdKeyboardManager *manager, { mate_settings_profile_start (NULL); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "X11 display is required for the keyboard plugin"); + mate_settings_profile_end (NULL); + return FALSE; + } +#else + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The keyboard plugin was built without X11 support"); + mate_settings_profile_end (NULL); + return FALSE; +#endif /* GDK_WINDOWING_X11 */ + g_idle_add ((GSourceFunc) start_keyboard_idle_cb, manager); mate_settings_profile_end (NULL); @@ -324,6 +341,11 @@ msd_keyboard_manager_stop (MsdKeyboardManager *manager) g_debug ("Stopping keyboard manager"); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return; +#endif /* GDK_WINDOWING_X11 */ + if (p->settings != NULL) { g_object_unref (p->settings); p->settings = NULL; diff --git a/plugins/media-keys/media-keys.mate-settings-plugin.desktop.in b/plugins/media-keys/media-keys.mate-settings-plugin.desktop.in index 733bd10..05d2456 100644 --- a/plugins/media-keys/media-keys.mate-settings-plugin.desktop.in +++ b/plugins/media-keys/media-keys.mate-settings-plugin.desktop.in @@ -1,6 +1,7 @@ [MATE Settings Plugin] Module=media-keys IAge=0 +X11Only=true Name=Media keys Description=Media keys plugin Authors= diff --git a/plugins/media-keys/msd-media-keys-manager.c b/plugins/media-keys/msd-media-keys-manager.c index 5ab8946..2d5428e 100644 --- a/plugins/media-keys/msd-media-keys-manager.c +++ b/plugins/media-keys/msd-media-keys-manager.c @@ -26,7 +26,9 @@ #include <glib.h> #include <glib/gi18n.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #include <gtk/gtk.h> #include <gio/gio.h> @@ -1605,6 +1607,20 @@ msd_media_keys_manager_start (MsdMediaKeysManager *manager, GError **error) { mate_settings_profile_start (NULL); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "X11 display is required for the media_keys plugin"); + mate_settings_profile_end (NULL); + return FALSE; + } +#else + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The media_keys plugin was built without X11 support"); + mate_settings_profile_end (NULL); + return FALSE; +#endif /* GDK_WINDOWING_X11 */ + #ifdef HAVE_LIBMATEMIXER if (G_LIKELY (mate_mixer_is_initialized ())) { mate_settings_profile_start ("mate_mixer_context_new"); @@ -1653,6 +1669,11 @@ msd_media_keys_manager_stop (MsdMediaKeysManager *manager) g_debug ("Stopping media_keys manager"); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return; +#endif /* GDK_WINDOWING_X11 */ + for (ls = priv->screens; ls != NULL; ls = ls->next) { gdk_window_remove_filter (gdk_screen_get_root_window (ls->data), (GdkFilterFunc) acme_filter_events, diff --git a/plugins/mouse/mouse.mate-settings-plugin.desktop.in b/plugins/mouse/mouse.mate-settings-plugin.desktop.in index ae66d36..8b688f5 100644 --- a/plugins/mouse/mouse.mate-settings-plugin.desktop.in +++ b/plugins/mouse/mouse.mate-settings-plugin.desktop.in @@ -1,6 +1,7 @@ [MATE Settings Plugin] Module=mouse IAge=0 +X11Only=true Name=Mouse Description=Mouse plugin Authors= diff --git a/plugins/mouse/msd-mouse-manager.c b/plugins/mouse/msd-mouse-manager.c index 92a5f0a..a79fd09 100644 --- a/plugins/mouse/msd-mouse-manager.c +++ b/plugins/mouse/msd-mouse-manager.c @@ -36,7 +36,9 @@ #include <glib/gi18n.h> #include <gtk/gtk.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #include <gdk/gdkkeysyms.h> #include <X11/keysym.h> #include <X11/Xatom.h> @@ -58,6 +60,7 @@ /* Mouse settings */ #define MATE_MOUSE_SCHEMA "org.mate.peripherals-mouse" #define KEY_MOUSE_LOCATE_POINTER "locate-pointer" +#define KEY_MOUSE_NATURAL_SCROLL "natural-scroll" #define KEY_MIDDLE_BUTTON_EMULATION "middle-button-enabled" /* Touchpad settings */ @@ -1389,8 +1392,8 @@ set_natural_scroll_libinput (XDeviceInfo *device_info, } static void -set_natural_scroll (XDeviceInfo *device_info, - gboolean natural_scroll) +set_touchpad_natural_scroll (XDeviceInfo *device_info, + gboolean natural_scroll) { if (property_from_name ("Synaptics Scrolling Distance")) set_natural_scroll_synaptics (device_info, natural_scroll); @@ -1400,7 +1403,7 @@ set_natural_scroll (XDeviceInfo *device_info, } static void -set_natural_scroll_all (MsdMouseManager *manager) +set_touchpad_natural_scroll_all (MsdMouseManager *manager) { int numdevices, i; XDeviceInfo *devicelist = XListInputDevices (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), &numdevices); @@ -1411,7 +1414,69 @@ set_natural_scroll_all (MsdMouseManager *manager) gboolean natural_scroll = g_settings_get_boolean (manager->priv->settings_touchpad, KEY_TOUCHPAD_NATURAL_SCROLL); for (i = 0; i < numdevices; i++) { - set_natural_scroll (&devicelist[i], natural_scroll); + set_touchpad_natural_scroll (&devicelist[i], natural_scroll); + } + + XFreeDeviceList (devicelist); +} + +static void +mouse_set_bool (XDeviceInfo *device_info, + const char *property_name, + int property_index, + gboolean enabled) +{ + XDevice *device; + GdkDisplay *display; + + display = gdk_display_get_default (); + + device = device_is_touchpad (device_info); + if (device != NULL) { + XCloseDevice (GDK_DISPLAY_XDISPLAY (display), device); + return; + } + + if ((device_info->use == IsXPointer) || + (device_info->use == IsXKeyboard) || + (!xinput_device_has_buttons (device_info))) + return; + + gdk_x11_display_error_trap_push (display); + device = XOpenDevice (GDK_DISPLAY_XDISPLAY (display), device_info->id); + if ((gdk_x11_display_error_trap_pop (display) != 0) || (device == NULL)) + return; + + property_set_bool (device_info, device, property_name, property_index, enabled); + + gdk_x11_display_error_trap_push (display); + XCloseDevice (GDK_DISPLAY_XDISPLAY (display), device); + gdk_x11_display_error_trap_pop_ignored (display); +} + +static void +set_mouse_natural_scroll (XDeviceInfo *device_info, + gboolean natural_scroll) +{ + g_debug ("Trying to set %s for \"%s\"", natural_scroll ? "natural (reverse) scroll" : "normal scroll", device_info->name); + + mouse_set_bool (device_info, "libinput Natural Scrolling Enabled", 0, natural_scroll); + mouse_set_bool (device_info, "Evdev Wheel Inversion", 0, natural_scroll); +} + +static void +set_mouse_natural_scroll_all (MsdMouseManager *manager) +{ + int numdevices, i; + XDeviceInfo *devicelist = XListInputDevices (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), &numdevices); + + if (devicelist == NULL) + return; + + gboolean natural_scroll = g_settings_get_boolean (manager->priv->settings_mouse, KEY_MOUSE_NATURAL_SCROLL); + + for (i = 0; i < numdevices; i++) { + set_mouse_natural_scroll (&devicelist[i], natural_scroll); } XFreeDeviceList (devicelist); @@ -1677,7 +1742,8 @@ set_mouse_settings (MsdMouseManager *manager) set_tap_to_click_all (manager); set_click_actions_all (manager); set_scrolling_all (manager->priv->settings_touchpad); - set_natural_scroll_all (manager); + set_touchpad_natural_scroll_all (manager); + set_mouse_natural_scroll_all (manager); set_touchpad_enabled_all (g_settings_get_boolean (manager->priv->settings_touchpad, KEY_TOUCHPAD_ENABLED)); set_accel_profile_all (manager); } @@ -1698,6 +1764,8 @@ mouse_callback (GSettings *settings, set_accel_profile_all (manager); } else if (g_strcmp0 (key, KEY_MIDDLE_BUTTON_EMULATION) == 0) { set_middle_button_all (g_settings_get_boolean (settings, key)); + } else if (g_strcmp0 (key, KEY_MOUSE_NATURAL_SCROLL) == 0) { + set_mouse_natural_scroll_all (manager); } else if (g_strcmp0 (key, KEY_MOUSE_LOCATE_POINTER) == 0) { set_locate_pointer (manager, g_settings_get_boolean (settings, key)); #if 0 /* FIXME need to fork (?) mousetweaks for this to work */ @@ -1739,7 +1807,7 @@ touchpad_callback (GSettings *settings, || (g_strcmp0 (key, KEY_HORIZ_TWO_FINGER_SCROLL) == 0)) { set_scrolling_all (manager->priv->settings_touchpad); } else if (g_strcmp0 (key, KEY_TOUCHPAD_NATURAL_SCROLL) == 0) { - set_natural_scroll_all (manager); + set_touchpad_natural_scroll_all (manager); } else if (g_strcmp0 (key, KEY_TOUCHPAD_ENABLED) == 0) { set_touchpad_enabled_all (g_settings_get_boolean (settings, key)); } else if ((g_strcmp0 (key, KEY_MOTION_ACCELERATION) == 0) @@ -1795,6 +1863,20 @@ msd_mouse_manager_start (MsdMouseManager *manager, { mate_settings_profile_start (NULL); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "X11 display is required for the mouse plugin"); + mate_settings_profile_end (NULL); + return FALSE; + } +#else + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The mouse plugin was built without X11 support"); + mate_settings_profile_end (NULL); + return FALSE; +#endif /* GDK_WINDOWING_X11 */ + if (!supports_xinput_devices ()) { g_debug ("XInput is not supported, not applying any settings"); return TRUE; @@ -1814,6 +1896,11 @@ msd_mouse_manager_stop (MsdMouseManager *manager) g_debug ("Stopping mouse manager"); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return; +#endif /* GDK_WINDOWING_X11 */ + if (p->settings_mouse != NULL) { g_object_unref(p->settings_mouse); p->settings_mouse = NULL; diff --git a/plugins/wlrandr/Makefile.am b/plugins/wlrandr/Makefile.am new file mode 100644 index 0000000..cfcabd4 --- /dev/null +++ b/plugins/wlrandr/Makefile.am @@ -0,0 +1,108 @@ +icondir = $(datadir)/icons/hicolor +context = apps + + +ICON_FILES = \ + msd-wlrandr-16.png \ + msd-wlrandr-22.png \ + msd-wlrandr-24.png \ + msd-wlrandr-32.png \ + msd-wlrandr-48.png \ + msd-wlrandr-64.png \ + msd-wlrandr-96.png \ + msd-wlrandr.svg + +install-data-local: + $(mkinstalldirs) $(DESTDIR)$(icondir)/16x16/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/22x22/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/24x24/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/32x32/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/48x48/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/64x64/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/96x96/$(context) + $(mkinstalldirs) $(DESTDIR)$(icondir)/scalable/$(context) + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-16.png $(DESTDIR)$(icondir)/16x16/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-22.png $(DESTDIR)$(icondir)/22x22/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-24.png $(DESTDIR)$(icondir)/24x24/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-32.png $(DESTDIR)$(icondir)/32x32/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-48.png $(DESTDIR)$(icondir)/48x48/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-64.png $(DESTDIR)$(icondir)/64x64/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr-96.png $(DESTDIR)$(icondir)/96x96/$(context)/msd-wlrandr.png + $(INSTALL_DATA) $(srcdir)/msd-wlrandr.svg $(DESTDIR)$(icondir)/scalable/$(context)/msd-wlrandr.svg + +uninstall-local: + rm -f $(DESTDIR)$(icondir)/16x16/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/22x22/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/24x24/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/32x32/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/48x48/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/64x64/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/96x96/$(context)/msd-wlrandr.png + rm -f $(DESTDIR)$(icondir)/scalable/$(context)/msd-wlrandr.svg + +plugin_LTLIBRARIES = \ + libwlrandr.la + +protocol_src_dir = protocol +protocol_src = $(protocol_src_dir)/wlr-output-management-unstable-v1.xml + +generated_protocol_sources = \ + wlr-output-management-unstable-v1-protocol.c \ + wlr-output-management-unstable-v1-client-protocol.h + +libwlrandr_la_SOURCES = \ + msd-wlrandr-plugin.h \ + msd-wlrandr-plugin.c \ + msd-wlrandr-manager.h \ + msd-wlrandr-manager.c \ + $(generated_protocol_sources) + +libwlrandr_la_CPPFLAGS = \ + -I$(top_srcdir)/mate-settings-daemon \ + -I$(builddir) \ + -DBINDIR=\"$(bindir)\" \ + -DMATE_SETTINGS_LOCALEDIR=\""$(datadir)/locale"\" \ + $(AM_CPPFLAGS) + +libwlrandr_la_CFLAGS = \ + $(SETTINGS_PLUGIN_CFLAGS) \ + $(WAYLAND_CLIENT_CFLAGS) \ + $(APPINDICATOR_CFLAGS) \ + $(LIBNOTIFY_CFLAGS) \ + $(MATE_DESKTOP_CFLAGS) \ + $(AM_CFLAGS) \ + $(WARN_CFLAGS) + +libwlrandr_la_LDFLAGS = \ + $(MSD_PLUGIN_LDFLAGS) + +libwlrandr_la_LIBADD = \ + $(SETTINGS_PLUGIN_LIBS) \ + $(WAYLAND_CLIENT_LIBS) \ + $(APPINDICATOR_LIBS) \ + $(LIBNOTIFY_LIBS) \ + $(MATE_DESKTOP_LIBS) \ + $(NULL) + +BUILT_SOURCES = $(generated_protocol_sources) + +CLEANFILES = $(generated_protocol_sources) + +wlr-output-management-unstable-v1-client-protocol.h: $(protocol_src) + $(AM_V_GEN) $(WAYLAND_SCANNER) client-header $< $@ + +wlr-output-management-unstable-v1-protocol.c: $(protocol_src) + $(AM_V_GEN) $(WAYLAND_SCANNER) private-code $< $@ + +plugin_in_files = \ + wlrandr.mate-settings-plugin.desktop.in + +plugin_DATA = $(plugin_in_files:.mate-settings-plugin.desktop.in=.mate-settings-plugin) + +EXTRA_DIST = $(plugin_in_files) $(ICON_FILES) $(protocol_src) +DISTCLEANFILES = $(plugin_DATA) + +$(plugin_DATA): $(plugin_in_files) + $(AM_V_GEN) $(MSGFMT) --desktop --template $< -d $(top_srcdir)/po -o $@ + +-include $(top_srcdir)/git.mk diff --git a/plugins/wlrandr/msd-wlrandr-16.png b/plugins/wlrandr/msd-wlrandr-16.png Binary files differnew file mode 100644 index 0000000..adb9e36 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-16.png diff --git a/plugins/wlrandr/msd-wlrandr-22.png b/plugins/wlrandr/msd-wlrandr-22.png Binary files differnew file mode 100644 index 0000000..a90f71b --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-22.png diff --git a/plugins/wlrandr/msd-wlrandr-24.png b/plugins/wlrandr/msd-wlrandr-24.png Binary files differnew file mode 100644 index 0000000..6d66034 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-24.png diff --git a/plugins/wlrandr/msd-wlrandr-32.png b/plugins/wlrandr/msd-wlrandr-32.png Binary files differnew file mode 100644 index 0000000..d0fd058 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-32.png diff --git a/plugins/wlrandr/msd-wlrandr-48.png b/plugins/wlrandr/msd-wlrandr-48.png Binary files differnew file mode 100644 index 0000000..ff2e1c7 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-48.png diff --git a/plugins/wlrandr/msd-wlrandr-64.png b/plugins/wlrandr/msd-wlrandr-64.png Binary files differnew file mode 100644 index 0000000..ebde0b6 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-64.png diff --git a/plugins/wlrandr/msd-wlrandr-96.png b/plugins/wlrandr/msd-wlrandr-96.png Binary files differnew file mode 100644 index 0000000..fab9dcd --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-96.png diff --git a/plugins/wlrandr/msd-wlrandr-manager.c b/plugins/wlrandr/msd-wlrandr-manager.c new file mode 100644 index 0000000..7240ae8 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-manager.c @@ -0,0 +1,2968 @@ +/* -*- 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> + +#ifdef HAVE_APP_INDICATOR +#include <libayatana-appindicator/app-indicator.h> +#endif + +#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_NAME_2 MSD_WLRANDR_DBUS_NAME "_2" +#define MSD_WLRANDR_DBUS_PATH MSD_DBUS_PATH "/WLRANDR" + +static const gchar introspection_xml[] = +"<node>" +" <interface name='org.mate.SettingsDaemon.WLRANDR'>" +" <method name='GetConfiguration'>" +" <arg name='configuration' type='a(ssbiiidbb(iiii)a(iiii))' direction='out'/>" +" </method>" +" <method name='SetConfiguration'>" +" <arg name='configuration' type='a(sbiiiiidib)' direction='in'/>" +" </method>" +" <method name='ApplyConfiguration'>" +" </method>" +" <signal name='ConfigurationChanged'>" +" </signal>" +" </interface>" +"" +" <interface name='org.mate.SettingsDaemon.WLRANDR_2'>" +" <method name='GetConfiguration'>" +" <arg name='configuration' type='a(ssbiiidbb(iiii)a(iiii))' direction='out'/>" +" </method>" +" <method name='SetConfiguration'>" +" <arg name='configuration' type='a(sbiiiiidib)' direction='in'/>" +" </method>" +" <method name='ApplyConfiguration'>" +" <arg name='parent_window_id' type='x' direction='in'/>" +" <arg name='timestamp' type='x' direction='in'/>" +" </method>" +" <signal name='ConfigurationChanged'>" +" </signal>" +" </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; +#ifdef HAVE_APP_INDICATOR + AppIndicator *indicator; +#endif + 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 +emit_configuration_changed (MsdWlrandrManager *manager); + +static void +save_current_configuration (MsdWlrandrManager *manager, WlrConfig *config); + +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 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); + emit_configuration_changed (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; + WlrConfig *config; + 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, ad->config); + + 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); + wlr_config_free (ad->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; + + /* The configuration was rejected, so nothing was applied and + * the live head state is unchanged; persist it as is. + */ + save_current_configuration (ad->manager, NULL); + 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); + wlr_config_free (ad->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); + wlr_config_free (ad->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; + } + } +} + +/* Deep copy of a configuration. Used to keep a reference to the + * configuration that was actually sent to the compositor, so that the file + * is saved with the intended values rather than whatever the compositor + * last reported (which may not have been updated yet when the + * "succeeded" event arrives). + */ +static WlrConfig * +wlr_config_copy (const WlrConfig *config) +{ + WlrConfig *copy; + GPtrArray *outputs; + int i; + + if (config == NULL) + return NULL; + + outputs = g_ptr_array_new (); + for (i = 0; config->outputs != NULL && config->outputs[i] != NULL; i++) { + WlrOutputInfo *src = config->outputs[i]; + WlrOutputInfo *dst = wlr_output_info_new (src->name); + + dst->vendor = g_strdup (src->vendor); + dst->product = g_strdup (src->product); + dst->serial = g_strdup (src->serial); + dst->enabled = src->enabled; + dst->width = src->width; + dst->height = src->height; + dst->rate = src->rate; + dst->x = src->x; + dst->y = src->y; + dst->scale = src->scale; + dst->transform = src->transform; + dst->adaptive_sync = src->adaptive_sync; + + g_ptr_array_add (outputs, dst); + } + g_ptr_array_add (outputs, NULL); + + copy = g_new0 (WlrConfig, 1); + copy->outputs = (WlrOutputInfo **) g_ptr_array_free (outputs, FALSE); + + return copy; +} + +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; + + ad->config = wlr_config_copy (config); + + /* Enrich the copy with the head identity fields; configurations that + * arrive over D-Bus do not carry them and they are useful when the + * file is inspected by hand. + */ + for (l = priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + WlrOutputInfo *info = find_output_in_config (ad->config, head->name); + + if (info == NULL) + continue; + if (info->vendor == NULL || *info->vendor == '\0') + info->vendor = g_strdup (head->make); + if (info->product == NULL || *info->product == '\0') + info->product = g_strdup (head->model); + if (info->serial == NULL || *info->serial == '\0') + info->serial = g_strdup (head->serial_number); + } + + 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; +} + +/* Read a scale value from the configuration file. g_ascii_strtod() is + * locale-independent and only accepts '.' as the decimal separator, so + * normalize any ',' left in the file by older versions of the plugin that + * wrote it with a locale-dependent "%f". Without this, e.g. "1,500000" + * would silently come back as 1.0. + */ +static double +parse_scale (const char *text) +{ + char *normalized; + char *p; + double value; + + if (text == NULL) + return 1.0; + + normalized = g_strdup (text); + for (p = normalized; *p != '\0'; p++) { + if (*p == ',') + *p = '.'; + } + + value = g_ascii_strtod (normalized, NULL); + g_free (normalized); + + return value; +} + +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 = parse_scale (text); + } 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 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); + + { + /* "%f" is locale-dependent and would write + * e.g. "1,500000" in comma-decimal locales, + * while the parser always reads with the + * locale-independent g_ascii_strtod(). Use + * g_ascii_formatd() so the file is always + * written with a '.' decimal separator. + */ + char scale_buf[G_ASCII_DTOSTR_BUF_SIZE]; + g_ascii_formatd (scale_buf, sizeof (scale_buf), "%f", output->scale); + g_string_append_printf (string, " <scale>%s</scale>\n", scale_buf); + } + 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"); +} + +/* Persist the current output configuration to monitors-wayland.xml. If + * `config` is non-NULL it is used as-is; otherwise the live head state is + * snapshotted (only used when nothing was applied and the state is known to + * be accurate, e.g. after a rejected adaptive sync toggle). + */ +static void +save_current_configuration (MsdWlrandrManager *manager, WlrConfig *config) +{ + struct MsdWlrandrManagerPrivate *priv = manager->priv; + WlrConfig *snapshot; + WlrConfig **existing; + GString *string; + char *intended; + char *backup; + int i; + GError *error = NULL; + + if (config == NULL) + config = snapshot = snapshot_config (manager); + else + snapshot = NULL; + + 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); + if (snapshot != NULL) + wlr_config_free (snapshot); +} + +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 * + * ------------------------------------------------------------------ */ + +#define HEAD_INFO_TYPE "a(ssbiiidbb(iiii)a(iiii))" +#define HEAD_SET_TYPE "a(sbiiiiidib)" +#define HEAD_INFO_FORMAT "(ssbiiidbb(iiii)a(iiii))" +#define HEAD_SET_FORMAT "(sbiiiiidib)" +#define MODE_FORMAT "(iiii)" + +/* GVariant type strings cannot contain whitespace, so the compact forms + * above are required on the wire. These enums document what each field + * means so the code below never has to be decoded from the string. */ + +/* One head as reported by GetConfiguration: + * (name, description, enabled, x, y, transform, scale, + * adaptive_sync_supported, adaptive_sync, + * current_mode (width, height, refresh, preferred), + * modes (width, height, refresh, preferred) ...) */ +typedef enum { + HEAD_INFO_FIELD_NAME, /* s output name */ + HEAD_INFO_FIELD_DESCRIPTION, /* s human-readable description */ + HEAD_INFO_FIELD_ENABLED, /* b output enabled */ + HEAD_INFO_FIELD_X, /* i x position on the logical screen */ + HEAD_INFO_FIELD_Y, /* i y position on the logical screen */ + HEAD_INFO_FIELD_TRANSFORM, /* i rotation (wl_output_transform) */ + HEAD_INFO_FIELD_SCALE, /* d fractional scale factor */ + HEAD_INFO_FIELD_ADAPTIVE_SYNC_SUPPORTED, /* b hardware supports adaptive sync */ + HEAD_INFO_FIELD_ADAPTIVE_SYNC, /* b adaptive sync currently active */ + HEAD_INFO_FIELD_CURRENT_MODE, /* (iiii) the mode currently in use */ + HEAD_INFO_FIELD_MODES, /* a(iiii) all modes the output supports */ + HEAD_INFO_N_FIELDS +} HeadInfoField; + +/* One entry of the modes array. */ +typedef enum { + MODE_FIELD_WIDTH, /* i width in pixels */ + MODE_FIELD_HEIGHT, /* i height in pixels */ + MODE_FIELD_REFRESH, /* i refresh rate in mHz */ + MODE_FIELD_PREFERRED, /* i 1 if this is the preferred mode */ + MODE_N_FIELDS +} ModeField; + +/* One head as sent to SetConfiguration: + * (name, enabled, width, height, refresh, x, y, scale, transform, + * adaptive_sync) */ +typedef enum { + HEAD_SET_FIELD_NAME, /* s output name */ + HEAD_SET_FIELD_ENABLED, /* b output enabled */ + HEAD_SET_FIELD_WIDTH, /* i desired width in pixels */ + HEAD_SET_FIELD_HEIGHT, /* i desired height in pixels */ + HEAD_SET_FIELD_RATE, /* i desired refresh rate in mHz */ + HEAD_SET_FIELD_X, /* i desired x position */ + HEAD_SET_FIELD_Y, /* i desired y position */ + HEAD_SET_FIELD_SCALE, /* d desired fractional scale */ + HEAD_SET_FIELD_TRANSFORM, /* i desired rotation */ + HEAD_SET_FIELD_ADAPTIVE_SYNC, /* b desired adaptive sync state */ + HEAD_SET_N_FIELDS +} HeadSetField; + +/* Notify interested parties (e.g. the display capplet) that the current + * output configuration may have changed. The same object path is + * registered twice, once per interface, so the signal is emitted on both. + */ +static void +emit_configuration_changed (MsdWlrandrManager *manager) +{ + GDBusConnection *connection; + + if (manager->priv->connection == NULL) + return; + + connection = manager->priv->connection; + + g_dbus_connection_emit_signal (connection, + NULL, + MSD_WLRANDR_DBUS_PATH, + MSD_WLRANDR_DBUS_NAME, + "ConfigurationChanged", + NULL, NULL); + g_dbus_connection_emit_signal (connection, + NULL, + MSD_WLRANDR_DBUS_PATH, + MSD_WLRANDR_DBUS_NAME_2, + "ConfigurationChanged", + NULL, NULL); +} + +/* Serialize the current state of the outputs so the display capplet can + * render and edit it. Field order and meaning are documented by the + * HeadInfoField and ModeField enums above. + */ +static GVariant * +build_configuration_variant (MsdWlrandrManager *manager) +{ + GVariantBuilder builder; + GList *l; + + g_variant_builder_init (&builder, G_VARIANT_TYPE (HEAD_INFO_TYPE)); + + for (l = manager->priv->heads; l != NULL; l = l->next) { + WlOutputHead *head = l->data; + GVariantBuilder modes_builder; + GList *m; + int cur_w = 0, cur_h = 0, cur_rate = 0, cur_preferred = 0; + + g_variant_builder_init (&modes_builder, G_VARIANT_TYPE ("a" MODE_FORMAT)); + + for (m = head->modes; m != NULL; m = m->next) { + WlOutputMode *mode = m->data; + + g_variant_builder_add (&modes_builder, MODE_FORMAT, + mode->width, + mode->height, + mode->refresh, + mode->preferred ? 1 : 0); + } + + if (head->enabled && head->current_mode != NULL) { + cur_w = head->current_mode->width; + cur_h = head->current_mode->height; + cur_rate = head->current_mode->refresh; + cur_preferred = head->current_mode->preferred ? 1 : 0; + } + + g_variant_builder_add (&builder, HEAD_INFO_FORMAT, + head->name != NULL ? head->name : "", + head->description != NULL ? head->description : "", + head->enabled, + head->x, + head->y, + head->transform, + head->scale, + head->adaptive_sync_supported == 1, + head->adaptive_sync, + cur_w, cur_h, cur_rate, cur_preferred, + &modes_builder); + } + + return g_variant_builder_end (&builder); +} + +/* Deserialize a configuration sent by the display capplet. Field order + * and meaning are documented by the HeadSetField enum above. + */ +static WlrConfig * +parse_configuration_from_variant (GVariant *configuration) +{ + WlrConfig *config; + GVariantIter iter; + GPtrArray *outputs; + + if (configuration == NULL || + !g_variant_is_of_type (configuration, G_VARIANT_TYPE (HEAD_SET_TYPE))) + return NULL; + + outputs = g_ptr_array_new (); + + g_variant_iter_init (&iter, configuration); + while (TRUE) { + GVariant *head_var; + const char *name; + gboolean enabled; + gboolean adaptive_sync; + int width, height, rate, x, y, transform; + double scale; + WlrOutputInfo *info; + + head_var = g_variant_iter_next_value (&iter); + if (head_var == NULL) + break; + + g_variant_get_child (head_var, HEAD_SET_FIELD_NAME, + "&s", &name); + g_variant_get_child (head_var, HEAD_SET_FIELD_ENABLED, + "b", &enabled); + g_variant_get_child (head_var, HEAD_SET_FIELD_WIDTH, + "i", &width); + g_variant_get_child (head_var, HEAD_SET_FIELD_HEIGHT, + "i", &height); + g_variant_get_child (head_var, HEAD_SET_FIELD_RATE, + "i", &rate); + g_variant_get_child (head_var, HEAD_SET_FIELD_X, + "i", &x); + g_variant_get_child (head_var, HEAD_SET_FIELD_Y, + "i", &y); + g_variant_get_child (head_var, HEAD_SET_FIELD_SCALE, + "d", &scale); + g_variant_get_child (head_var, HEAD_SET_FIELD_TRANSFORM, + "i", &transform); + g_variant_get_child (head_var, HEAD_SET_FIELD_ADAPTIVE_SYNC, + "b", &adaptive_sync); + + /* wlr_output_info_new () duplicates the name, so it is + * safe to release the tuple afterwards. */ + info = wlr_output_info_new (name); + info->enabled = enabled; + info->width = width; + info->height = height; + info->rate = rate; + info->x = x; + info->y = y; + info->scale = scale; + info->transform = transform; + info->adaptive_sync = adaptive_sync; + + g_variant_unref (head_var); + + g_ptr_array_add (outputs, info); + } + + if (outputs->len == 0) { + g_ptr_array_free (outputs, TRUE); + return NULL; + } + + g_ptr_array_add (outputs, NULL); + + config = g_new0 (WlrConfig, 1); + config->outputs = (WlrOutputInfo **) g_ptr_array_free (outputs, FALSE); + + return config; +} + +/* Pick the head whose adaptive sync state the caller wants to change, if + * any. The zwlr-output-management protocol only lets the plugin set + * adaptive sync on the heads it explicitly marks, so only the head whose + * requested value differs from the current one is applied. + */ +static WlOutputHead * +find_adaptive_sync_head_to_set (MsdWlrandrManager *manager, + WlrConfig *config) +{ + GList *l; + + 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->adaptive_sync != head->adaptive_sync) + return head; + } + + return NULL; +} + +static gboolean +msd_wlrandr_manager_set_configuration (MsdWlrandrManager *manager, + GVariant *configuration, + GError **error) +{ + WlrConfig *config; + WlrConfig *previous; + WlOutputHead *adaptive_sync_head; + gboolean res; + + config = parse_configuration_from_variant (configuration); + if (config == NULL) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "The supplied configuration is invalid"); + return FALSE; + } + + if (config_enabled_head_count (config) == 0) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "The last active output cannot be turned off"); + wlr_config_free (config); + return FALSE; + } + + previous = snapshot_config (manager); + + adaptive_sync_head = find_adaptive_sync_head_to_set (manager, config); + + res = apply_configuration (manager, config, TRUE, TRUE, previous, adaptive_sync_head); + if (!res) { + g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Could not apply the display configuration"); + wlr_config_free (config); + wlr_config_free (previous); + return FALSE; + } + + wlr_config_free (config); + + return TRUE; +} + +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, "GetConfiguration") == 0) { + GVariant *configuration; + + configuration = build_configuration_variant (manager); + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(@a(ssbiiidbb(iiii)a(iiii)))", + configuration)); + } else if (g_strcmp0 (method_name, "SetConfiguration") == 0) { + g_autoptr (GVariant) configuration = NULL; + + g_variant_get (parameters, "(@a(sbiiiiidib))", &configuration); + if (!msd_wlrandr_manager_set_configuration (manager, configuration, &error)) + g_dbus_method_invocation_return_gerror (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, NULL); + } else 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, "GetConfiguration") == 0) { + GVariant *configuration; + + configuration = build_configuration_variant (manager); + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(@a(ssbiiidbb(iiii)a(iiii)))", + configuration)); + } else if (g_strcmp0 (method_name, "SetConfiguration") == 0) { + g_autoptr (GVariant) configuration = NULL; + + g_variant_get (parameters, "(@a(sbiiiiidib))", &configuration); + if (!msd_wlrandr_manager_set_configuration (manager, configuration, &error)) + g_dbus_method_invocation_return_gerror (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, NULL); + } else if (g_strcmp0 (method_name, "ApplyConfiguration") == 0) { + gint64 parent_window_id; + gint64 timestamp; + + g_variant_get (parameters, "(xx)", &parent_window_id, ×tamp); + /* Wayland has no X window ids or X timestamps; the arguments + * are accepted only for API compatibility with xrandr. + */ + (void) parent_window_id; + (void) timestamp; + + if (!msd_wlrandr_manager_apply_configuration (manager, &error)) + g_dbus_method_invocation_return_gerror (invocation, error); + else + g_dbus_method_invocation_return_value (invocation, NULL); + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + .method_call = handle_method_call, +}; + +static const GDBusInterfaceVTable interface_vtable2 = +{ + .method_call = handle_method_call2, +}; + +static void +on_bus_gotten (GObject *source_object, + GAsyncResult *res, + MsdWlrandrManager *manager) +{ + GDBusConnection *connection; + GError *error = NULL; + + connection = g_bus_get_finish (res, &error); + if (connection == NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Could not get session bus: %s", error->message); + g_error_free (error); + return; + } + manager->priv->connection = connection; + + if (manager->priv->introspection_data == NULL) { + g_warning ("Could not parse wlrandr D-Bus introspection XML"); + return; + } + + manager->priv->registration_id = + g_dbus_connection_register_object (connection, + MSD_WLRANDR_DBUS_PATH, + manager->priv->introspection_data->interfaces[0], + &interface_vtable, + manager, + NULL, + NULL); + manager->priv->registration_id_2 = + g_dbus_connection_register_object (connection, + MSD_WLRANDR_DBUS_PATH, + manager->priv->introspection_data->interfaces[1], + &interface_vtable2, + manager, + NULL, + NULL); + + manager->priv->owner_id = g_bus_own_name_on_connection (manager->priv->connection, + MSD_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT, + NULL, NULL, NULL, NULL); +} + +static void +register_manager_dbus (MsdWlrandrManager *manager) +{ + manager->priv->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + if (manager->priv->introspection_data == NULL) { + g_warning ("Could not parse wlrandr D-Bus introspection XML"); + return; + } + manager->priv->bus_cancellable = g_cancellable_new (); + g_bus_get (G_BUS_TYPE_SESSION, + manager->priv->bus_cancellable, + (GAsyncReadyCallback) on_bus_gotten, + manager); +} + +/* ------------------------------------------------------------------ * + * Menu actions * + * ------------------------------------------------------------------ */ + +static void +monitor_activate_cb (GtkCheckMenuItem *item, MsdWlrandrManager *manager) +{ + WlOutputHead *head = g_object_get_data (G_OBJECT (item), "head"); + gboolean on = gtk_check_menu_item_get_active (item); + WlrConfig *config; + WlrOutputInfo *info; + + if (head == NULL) + return; + + if (head->enabled == on) + return; + + if (!on && enabled_head_count (manager) <= 1) { + gulong activate_id; + + g_warning ("msd-wlrandr: refusing to disable the last output"); + notify_config_error (_("The last active output cannot be turned off")); + + activate_id = (gulong) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (item), "activate-id")); + if (activate_id > 0) { + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (item, TRUE); + g_signal_handler_unblock (item, activate_id); + } + return; + } + + config = snapshot_config (manager); + info = find_output_in_config (config, head->name); + if (info == NULL) { + wlr_config_free (config); + return; + } + + info->enabled = on; + if (on) { + fill_output_mode (head, info); + if (info->x == 0 && info->y == 0) { + GList *l; + int max_x = 0; + + for (l = manager->priv->heads; l != NULL; l = l->next) { + WlOutputHead *h = l->data; + if (h->enabled && h->current_mode != NULL) { + int right = h->x + h->current_mode->width; + if (right > max_x) + max_x = right; + } + } + info->x = max_x; + info->y = 0; + } + } + + apply_configuration (manager, config, TRUE, FALSE, NULL, NULL); + wlr_config_free (config); +} + +static void +mode_activate_cb (GtkCheckMenuItem *item, MsdWlrandrManager *manager) +{ + WlOutputHead *head = g_object_get_data (G_OBJECT (item), "head"); + WlOutputMode *mode = g_object_get_data (G_OBJECT (item), "mode"); + WlrConfig *config; + WlrOutputInfo *info; + + if (head == NULL || mode == NULL) + return; + + if (!gtk_check_menu_item_get_active (item)) + return; + + config = snapshot_config (manager); + info = find_output_in_config (config, head->name); + if (info == NULL) { + wlr_config_free (config); + return; + } + + info->width = mode->width; + info->height = mode->height; + info->rate = mode->refresh; + + apply_configuration (manager, config, TRUE, FALSE, NULL, NULL); + wlr_config_free (config); +} + +static void +rotation_activate_cb (GtkCheckMenuItem *item, MsdWlrandrManager *manager) +{ + WlOutputHead *head = g_object_get_data (G_OBJECT (item), "head"); + int transform = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "transform")); + WlrConfig *config; + WlrOutputInfo *info; + + if (head == NULL) + return; + + if (!gtk_check_menu_item_get_active (item)) + return; + + config = snapshot_config (manager); + info = find_output_in_config (config, head->name); + if (info == NULL) { + wlr_config_free (config); + return; + } + + info->transform = transform; + + apply_configuration (manager, config, TRUE, FALSE, NULL, NULL); + wlr_config_free (config); +} + +static void +adaptive_sync_activate_cb (GtkCheckMenuItem *item, MsdWlrandrManager *manager) +{ + WlOutputHead *head = g_object_get_data (G_OBJECT (item), "head"); + gboolean on = gtk_check_menu_item_get_active (item); + WlrConfig *config; + WlrOutputInfo *info; + + if (head == NULL) + return; + + if (head->adaptive_sync_supported != 1) { + gulong activate_id; + + g_debug ("msd-wlrandr: adaptive sync support not confirmed on %s", head->name); + + activate_id = (gulong) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (item), "activate-id")); + if (activate_id > 0) { + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (item, head->adaptive_sync); + g_signal_handler_unblock (item, activate_id); + } + return; + } + + config = snapshot_config (manager); + info = find_output_in_config (config, head->name); + if (info == NULL) { + wlr_config_free (config); + return; + } + + info->adaptive_sync = on; + + apply_configuration (manager, config, TRUE, FALSE, NULL, head); + wlr_config_free (config); +} + +static void +run_display_capplet (MsdWlrandrManager *manager) +{ + GError *error = NULL; + + if (!mate_gdk_spawn_command_line_on_screen (gdk_screen_get_default (), + MSD_WLRANDR_DISPLAY_CAPPLET, &error)) { + g_warning ("Could not spawn %s: %s", MSD_WLRANDR_DISPLAY_CAPPLET, error->message); + g_error_free (error); + } +} + +static void +configure_display_cb (GtkMenuItem *item, MsdWlrandrManager *manager) +{ + run_display_capplet (manager); +} + +/* ------------------------------------------------------------------ * + * SNI menu * + * ------------------------------------------------------------------ */ + +static void +append_output_title_item (GtkMenu *menu, WlOutputHead *head) +{ + GtkWidget *item; + GtkWidget *box; + GtkWidget *image; + GtkWidget *label; + char *markup; + char *display_name; + + item = gtk_menu_item_new (); + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + image = gtk_image_new_from_icon_name ("computer", GTK_ICON_SIZE_MENU); + gtk_container_add (GTK_CONTAINER (box), image); + + display_name = head->description != NULL && *head->description != '\0' + ? g_strdup (head->description) : g_strdup (head->name); + + markup = g_markup_printf_escaped ("<b>%s</b>", display_name); + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), markup); + gtk_widget_set_margin_start (label, 6); + gtk_widget_set_margin_end (label, 6); + gtk_container_add (GTK_CONTAINER (box), label); + + gtk_container_add (GTK_CONTAINER (item), box); + gtk_widget_set_sensitive (item, FALSE); + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + + g_free (markup); + g_free (display_name); +} + +static void +append_enable_item (GtkMenu *menu, MsdWlrandrManager *manager, WlOutputHead *head) +{ + GtkWidget *item; + gulong activate_id; + + item = gtk_check_menu_item_new (); + + if (head->enabled) { + gtk_menu_item_set_label (GTK_MENU_ITEM (item), _("ON")); + gtk_widget_set_tooltip_text (item, _("Turn this monitor off")); + } else { + gtk_menu_item_set_label (GTK_MENU_ITEM (item), _("OFF")); + gtk_widget_set_tooltip_text (item, _("Turn this monitor on")); + } + + g_object_set_data (G_OBJECT (item), "head", head); + + activate_id = g_signal_connect (item, "activate", G_CALLBACK (monitor_activate_cb), manager); + g_object_set_data (G_OBJECT (item), "activate-id", GUINT_TO_POINTER ((guint) activate_id)); + + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), head->enabled); + g_signal_handler_unblock (item, activate_id); + + if (head->enabled && enabled_head_count (manager) <= 1) + gtk_widget_set_sensitive (item, FALSE); + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +append_resolution_submenu (GtkMenu *menu, MsdWlrandrManager *manager, WlOutputHead *head) +{ + GtkWidget *submenu; + GtkWidget *item; + GList *l; + GSList *group = NULL; + + submenu = gtk_menu_new (); + + for (l = head->modes; l != NULL; l = l->next) { + WlOutputMode *mode = l->data; + gboolean swapped = head->transform == WL_OUTPUT_TRANSFORM_90 || + head->transform == WL_OUTPUT_TRANSFORM_270; + int width = swapped ? mode->height : mode->width; + int height = swapped ? mode->width : mode->height; + char *label; + gulong activate_id; + + if (mode->refresh > 0) + label = g_strdup_printf ("%dx%d @ %g Hz", width, height, mode->refresh / 1000.0); + else + label = g_strdup_printf ("%dx%d", width, height); + + item = gtk_radio_menu_item_new_with_label (group, label); + group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (item)); + g_free (label); + + g_object_set_data (G_OBJECT (item), "head", head); + g_object_set_data (G_OBJECT (item), "mode", mode); + + if (mode->preferred) + gtk_widget_set_tooltip_text (item, _("Preferred resolution")); + + if (head->current_mode == mode) { + activate_id = g_signal_connect (item, "toggled", G_CALLBACK (mode_activate_cb), manager); + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), TRUE); + g_signal_handler_unblock (item, activate_id); + } else { + g_signal_connect (item, "toggled", G_CALLBACK (mode_activate_cb), manager); + } + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item); + } + + item = gtk_menu_item_new_with_label (_("Resolution")); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +append_rotation_submenu (GtkMenu *menu, MsdWlrandrManager *manager, WlOutputHead *head) +{ + typedef struct { + int transform; + const char *name; + } RotationInfo; + + static const RotationInfo rotations[] = { + { WL_OUTPUT_TRANSFORM_NORMAL, N_("Normal") }, + { WL_OUTPUT_TRANSFORM_90, N_("Left") }, + { WL_OUTPUT_TRANSFORM_270, N_("Right") }, + { WL_OUTPUT_TRANSFORM_180, N_("Upside Down") }, + }; + + GtkWidget *submenu; + GtkWidget *item; + GSList *group = NULL; + int i; + + submenu = gtk_menu_new (); + + for (i = 0; i < G_N_ELEMENTS (rotations); i++) { + gulong activate_id; + + item = gtk_radio_menu_item_new_with_label (group, _(rotations[i].name)); + group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (item)); + + if (!head->enabled) + gtk_widget_set_sensitive (item, FALSE); + + g_object_set_data (G_OBJECT (item), "head", head); + g_object_set_data (G_OBJECT (item), "transform", GINT_TO_POINTER (rotations[i].transform)); + + if (head->transform == rotations[i].transform) { + activate_id = g_signal_connect (item, "toggled", G_CALLBACK (rotation_activate_cb), manager); + g_signal_handler_block (item, activate_id); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), TRUE); + g_signal_handler_unblock (item, activate_id); + } else { + g_signal_connect (item, "toggled", G_CALLBACK (rotation_activate_cb), manager); + } + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item); + } + + item = gtk_menu_item_new_with_label (_("Rotation")); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +append_adaptive_sync_item (GtkMenu *menu, MsdWlrandrManager *manager, WlOutputHead *head) +{ + GtkWidget *item; + gulong activate_id; + gboolean unavailable; + + if (head_is_wayland_backend (head) && head->adaptive_sync_supported == -1) + head->adaptive_sync_supported = 0; + + unavailable = !head->enabled || head_is_wayland_backend (head) || head->adaptive_sync_supported != 1; + + item = gtk_check_menu_item_new_with_label (_("Adaptive Sync (VRR)")); + g_object_set_data (G_OBJECT (item), "head", head); + + activate_id = g_signal_connect (item, "activate", G_CALLBACK (adaptive_sync_activate_cb), manager); + g_object_set_data (G_OBJECT (item), "activate-id", GUINT_TO_POINTER ((guint) activate_id)); + + g_signal_handler_block (item, activate_id); + if (unavailable) + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), FALSE); + else + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), head->adaptive_sync); + g_signal_handler_unblock (item, activate_id); + + if (unavailable) + gtk_widget_set_sensitive (item, FALSE); + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +append_configure_item (GtkMenu *menu, MsdWlrandrManager *manager) +{ + GtkWidget *item; + GtkWidget *box; + GtkWidget *image; + GtkWidget *label; + GSettings *icon_settings; + + item = gtk_menu_item_new (); + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); + image = gtk_image_new_from_icon_name ("preferences-system", GTK_ICON_SIZE_MENU); + label = gtk_label_new_with_mnemonic (_("_Configure Display Settings…")); + + icon_settings = g_settings_new ("org.mate.interface"); + if (g_settings_get_boolean (icon_settings, "menus-have-icons")) + gtk_container_add (GTK_CONTAINER (box), image); + g_object_unref (icon_settings); + + gtk_container_add (GTK_CONTAINER (box), label); + gtk_container_add (GTK_CONTAINER (item), box); + + g_signal_connect (item, "activate", G_CALLBACK (configure_display_cb), manager); + + gtk_widget_show_all (item); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); +} + +static void +rebuild_menu (MsdWlrandrManager *manager) +{ + struct MsdWlrandrManagerPrivate *priv = manager->priv; + GtkWidget *menu; + GList *l; + +#ifndef HAVE_APP_INDICATOR + /* there is no tray icon to attach the menu to */ + return; +#else + if (priv->indicator == NULL) + return; +#endif + + 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; + +#ifdef HAVE_APP_INDICATOR + app_indicator_set_menu (priv->indicator, GTK_MENU (menu)); +#endif +} + +/* ------------------------------------------------------------------ * + * 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) { +#ifdef HAVE_APP_INDICATOR + app_indicator_set_status (manager->priv->indicator, APP_INDICATOR_STATUS_ACTIVE); +#endif + rebuild_menu (manager); + } else { +#ifdef HAVE_APP_INDICATOR + app_indicator_set_status (manager->priv->indicator, APP_INDICATOR_STATUS_PASSIVE); +#endif + } +} + +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); + } + +#ifdef HAVE_APP_INDICATOR + if (priv->indicator != NULL) + g_object_unref (priv->indicator); +#endif + if (priv->menu != NULL) + gtk_widget_destroy (priv->menu); + if (priv->confirm_dialog != NULL) + gtk_widget_destroy (priv->confirm_dialog); + + for (l = priv->heads; l != NULL; l = l->next) + wlr_output_head_free (l->data); + g_list_free (priv->heads); + + if (priv->manager != NULL) + zwlr_output_manager_v1_destroy (priv->manager); + if (priv->registry != NULL) + wl_registry_destroy (priv->registry); + + if (notify_is_initted ()) + notify_uninit (); + + G_OBJECT_CLASS (msd_wlrandr_manager_parent_class)->finalize (object); +} + +static void +msd_wlrandr_manager_init (MsdWlrandrManager *manager) +{ + manager->priv = msd_wlrandr_manager_get_instance_private (manager); +} + +static void +msd_wlrandr_manager_class_init (MsdWlrandrManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = msd_wlrandr_manager_finalize; +} + +MsdWlrandrManager * +msd_wlrandr_manager_new (void) +{ + return g_object_new (MSD_TYPE_WLRANDR_MANAGER, NULL); +} + +gboolean +msd_wlrandr_manager_start (MsdWlrandrManager *manager, GError **error) +{ + struct MsdWlrandrManagerPrivate *priv = manager->priv; + GdkDisplay *gdk_display; + GSettings *icon_settings; + + g_return_val_if_fail (MSD_IS_WLRANDR_MANAGER (manager), FALSE); + + if (priv->started) + return TRUE; + + if (!GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The wlrandr plugin requires a Wayland display"); + return FALSE; + } + + gdk_display = gdk_display_get_default (); + priv->display = gdk_wayland_display_get_wl_display (gdk_display); + if (priv->display == NULL) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Unable to get the Wayland display"); + return FALSE; + } + + g_debug ("Starting wlrandr manager"); + + priv->settings = g_settings_new (CONF_SCHEMA); + + notify_init ("mate-settings-daemon"); + + priv->registry = wl_display_get_registry (priv->display); + wl_registry_add_listener (priv->registry, ®istry_listener, manager); + wl_display_flush (priv->display); + +#ifdef HAVE_APP_INDICATOR + 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")); +#endif + + icon_settings = g_settings_new ("org.mate.interface"); +#ifdef HAVE_APP_INDICATOR + if (g_settings_get_boolean (icon_settings, "menus-have-icons")) + app_indicator_set_icon_theme_path (priv->indicator, NULL); +#endif + 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; + } + +#ifdef HAVE_APP_INDICATOR + if (priv->indicator != NULL) { + app_indicator_set_status (priv->indicator, APP_INDICATOR_STATUS_PASSIVE); + g_object_unref (priv->indicator); + priv->indicator = NULL; + } +#endif + + if (priv->menu != NULL) { + gtk_widget_destroy (priv->menu); + priv->menu = NULL; + } + + if (priv->confirm_dialog != NULL) { + gtk_widget_destroy (priv->confirm_dialog); + priv->confirm_dialog = NULL; + } + + if (priv->manager != NULL) { + zwlr_output_manager_v1_stop (priv->manager); + zwlr_output_manager_v1_destroy (priv->manager); + priv->manager = NULL; + } + + if (priv->registry != NULL) { + wl_registry_destroy (priv->registry); + priv->registry = NULL; + } + + for (l = priv->heads; l != NULL; l = l->next) + wlr_output_head_free (l->data); + g_list_free (priv->heads); + priv->heads = NULL; + + if (notify_is_initted ()) + notify_uninit (); + + priv->started = FALSE; +} diff --git a/plugins/wlrandr/msd-wlrandr-manager.h b/plugins/wlrandr/msd-wlrandr-manager.h new file mode 100644 index 0000000..2763c09 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-manager.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2026 MATE Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef __MSD_WLRANDR_MANAGER_H +#define __MSD_WLRANDR_MANAGER_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define MSD_TYPE_WLRANDR_MANAGER (msd_wlrandr_manager_get_type ()) +#define MSD_WLRANDR_MANAGER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MSD_TYPE_WLRANDR_MANAGER, MsdWlrandrManager)) +#define MSD_WLRANDR_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), MSD_TYPE_WLRANDR_MANAGER, MsdWlrandrManagerClass)) +#define MSD_IS_WLRANDR_MANAGER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MSD_TYPE_WLRANDR_MANAGER)) +#define MSD_IS_WLRANDR_MANAGER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), MSD_TYPE_WLRANDR_MANAGER)) +#define MSD_WLRANDR_MANAGER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), MSD_TYPE_WLRANDR_MANAGER, MsdWlrandrManagerClass)) + +typedef struct MsdWlrandrManagerPrivate MsdWlrandrManagerPrivate; + +typedef struct +{ + GObject parent; + MsdWlrandrManagerPrivate *priv; +} MsdWlrandrManager; + +typedef struct +{ + GObjectClass parent_class; +} MsdWlrandrManagerClass; + +GType msd_wlrandr_manager_get_type (void); + +MsdWlrandrManager * msd_wlrandr_manager_new (void); +gboolean msd_wlrandr_manager_start (MsdWlrandrManager *manager, + GError **error); +void msd_wlrandr_manager_stop (MsdWlrandrManager *manager); + +G_END_DECLS + +#endif /* __MSD_WLRANDR_MANAGER_H */ diff --git a/plugins/wlrandr/msd-wlrandr-plugin.c b/plugins/wlrandr/msd-wlrandr-plugin.c new file mode 100644 index 0000000..522e122 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-plugin.c @@ -0,0 +1,105 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2026 MATE Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "config.h" + +#include <glib/gi18n-lib.h> +#include <gmodule.h> + +#include "mate-settings-plugin.h" +#include "msd-wlrandr-plugin.h" +#include "msd-wlrandr-manager.h" + +struct MsdWlrandrPluginPrivate { + MsdWlrandrManager *manager; +}; + +MATE_SETTINGS_PLUGIN_REGISTER_WITH_PRIVATE (MsdWlrandrPlugin, msd_wlrandr_plugin) + +static void +msd_wlrandr_plugin_init (MsdWlrandrPlugin *plugin) +{ + plugin->priv = msd_wlrandr_plugin_get_instance_private (plugin); + + g_debug ("MsdWlrandrPlugin initializing"); + + plugin->priv->manager = msd_wlrandr_manager_new (); +} + +static void +msd_wlrandr_plugin_finalize (GObject *object) +{ + MsdWlrandrPlugin *plugin; + + g_return_if_fail (object != NULL); + g_return_if_fail (MSD_IS_WLRANDR_PLUGIN (object)); + + g_debug ("MsdWlrandrPlugin finalizing"); + + plugin = MSD_WLRANDR_PLUGIN (object); + + g_return_if_fail (plugin->priv != NULL); + + if (plugin->priv->manager != NULL) { + g_object_unref (plugin->priv->manager); + } + + G_OBJECT_CLASS (msd_wlrandr_plugin_parent_class)->finalize (object); +} + +static void +impl_activate (MateSettingsPlugin *plugin) +{ + gboolean res; + GError *error; + + g_debug ("Activating wlrandr plugin"); + + error = NULL; + res = msd_wlrandr_manager_start (MSD_WLRANDR_PLUGIN (plugin)->priv->manager, &error); + if (! res) { + g_warning ("Unable to start wlrandr manager: %s", error->message); + g_error_free (error); + } +} + +static void +impl_deactivate (MateSettingsPlugin *plugin) +{ + g_debug ("Deactivating wlrandr plugin"); + msd_wlrandr_manager_stop (MSD_WLRANDR_PLUGIN (plugin)->priv->manager); +} + +static void +msd_wlrandr_plugin_class_init (MsdWlrandrPluginClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + MateSettingsPluginClass *plugin_class = MATE_SETTINGS_PLUGIN_CLASS (klass); + + object_class->finalize = msd_wlrandr_plugin_finalize; + + plugin_class->activate = impl_activate; + plugin_class->deactivate = impl_deactivate; +} + +static void +msd_wlrandr_plugin_class_finalize (MsdWlrandrPluginClass *klass) +{ +} diff --git a/plugins/wlrandr/msd-wlrandr-plugin.h b/plugins/wlrandr/msd-wlrandr-plugin.h new file mode 100644 index 0000000..3adda68 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr-plugin.h @@ -0,0 +1,63 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2026 MATE Developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef __MSD_WLRANDR_PLUGIN_H__ +#define __MSD_WLRANDR_PLUGIN_H__ + +#include <glib.h> +#include <glib-object.h> +#include <gmodule.h> + +#include "mate-settings-plugin.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MSD_TYPE_WLRANDR_PLUGIN (msd_wlrandr_plugin_get_type ()) +#define MSD_WLRANDR_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MSD_TYPE_WLRANDR_PLUGIN, MsdWlrandrPlugin)) +#define MSD_WLRANDR_PLUGIN_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), MSD_TYPE_WLRANDR_PLUGIN, MsdWlrandrPluginClass)) +#define MSD_IS_WLRANDR_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MSD_TYPE_WLRANDR_PLUGIN)) +#define MSD_IS_WLRANDR_PLUGIN_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), MSD_TYPE_WLRANDR_PLUGIN)) +#define MSD_WLRANDR_PLUGIN_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), MSD_TYPE_WLRANDR_PLUGIN, MsdWlrandrPluginClass)) + +typedef struct MsdWlrandrPluginPrivate MsdWlrandrPluginPrivate; + +typedef struct +{ + MateSettingsPlugin parent; + MsdWlrandrPluginPrivate *priv; +} MsdWlrandrPlugin; + +typedef struct +{ + MateSettingsPluginClass parent_class; +} MsdWlrandrPluginClass; + +GType msd_wlrandr_plugin_get_type (void) G_GNUC_CONST; + +/* All the plugins must implement this function */ +G_MODULE_EXPORT GType register_mate_settings_plugin (GTypeModule *module); + +#ifdef __cplusplus +} +#endif + +#endif /* __MSD_WLRANDR_PLUGIN_H__ */ diff --git a/plugins/wlrandr/msd-wlrandr.svg b/plugins/wlrandr/msd-wlrandr.svg new file mode 100644 index 0000000..87ce536 --- /dev/null +++ b/plugins/wlrandr/msd-wlrandr.svg @@ -0,0 +1,491 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="128" + height="128" + id="svg3304" + sodipodi:version="0.32" + inkscape:version="0.92.4 (unknown)" + sodipodi:docname="msd-xrandr.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:modified="true" + version="1.1"> + <defs + id="defs3306"> + <linearGradient + id="linearGradient2804"> + <stop + style="stop-color:black;stop-opacity:0;" + offset="0" + id="stop2806" /> + <stop + id="stop2812" + offset="0.5" + style="stop-color:black;stop-opacity:1;" /> + <stop + style="stop-color:black;stop-opacity:0;" + offset="1" + id="stop2808" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient2804" + id="linearGradient2831" + gradientUnits="userSpaceOnUse" + x1="21.875" + y1="48.000977" + x2="21.875" + y2="40" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient2781" + id="radialGradient2829" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(2,0,0,0.8,36,8.8)" + cx="1" + cy="44" + fx="1" + fy="44" + r="5" /> + <linearGradient + inkscape:collect="always" + id="linearGradient2781"> + <stop + style="stop-color:black;stop-opacity:1;" + offset="0" + id="stop2783" /> + <stop + style="stop-color:black;stop-opacity:0;" + offset="1" + id="stop2785" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient2781" + id="radialGradient2827" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(2,0,0,0.8,-13,-79.2)" + cx="1" + cy="44" + fx="1" + fy="44" + r="5" /> + <linearGradient + id="linearGradient5137"> + <stop + style="stop-color:#eeeeec;stop-opacity:1;" + offset="0" + id="stop5139" /> + <stop + style="stop-color:#e6e6e3;stop-opacity:1;" + offset="1" + id="stop5141" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient6240"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop6242" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop6244" /> + </linearGradient> + <linearGradient + id="linearGradient11400"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop11402" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop11404" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient11400" + id="linearGradient11406" + x1="23.154902" + y1="34.572548" + x2="23.529411" + y2="40.219608" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient5137" + id="linearGradient5147" + gradientUnits="userSpaceOnUse" + x1="17.247635" + y1="6.3760414" + x2="39.904388" + y2="38.876041" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient5137" + id="linearGradient5223" + gradientUnits="userSpaceOnUse" + x1="31.743324" + y1="37.842293" + x2="31.86105" + y2="43.82579" + gradientTransform="translate(0,0.7954955)" /> + <linearGradient + id="linearGradient7025" + inkscape:collect="always"> + <stop + id="stop7027" + offset="0" + style="stop-color:#e6ce46;stop-opacity:1" /> + <stop + id="stop7029" + offset="1" + style="stop-color:#d6ba1c;stop-opacity:1" /> + </linearGradient> + <linearGradient + id="linearGradient22122" + inkscape:collect="always"> + <stop + id="stop22124" + offset="0" + style="stop-color:black;stop-opacity:1;" /> + <stop + id="stop22126" + offset="1" + style="stop-color:black;stop-opacity:0;" /> + </linearGradient> + <linearGradient + id="linearGradient22140"> + <stop + id="stop22142" + offset="0" + style="stop-color:black;stop-opacity:0;" /> + <stop + style="stop-color:black;stop-opacity:1;" + offset="0.5" + id="stop22148" /> + <stop + id="stop22144" + offset="1" + style="stop-color:black;stop-opacity:0;" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient22122" + id="radialGradient4770" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.000001,1.142856,0,-41.10259,45.50001)" + cx="7" + cy="39.464806" + fx="7" + fy="39.464806" + r="3.5" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient22122" + id="radialGradient4772" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.000001,1.142856,0,-89.10259,-31.49999)" + cx="7" + cy="39.464806" + fx="7" + fy="39.464806" + r="3.5" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient22140" + id="linearGradient4774" + gradientUnits="userSpaceOnUse" + x1="18.142136" + y1="35" + x2="18.142136" + y2="42.040661" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient7025" + id="linearGradient4776" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(53,1.428571)" + x1="13.630114" + y1="28.5" + x2="25.208096" + y2="41.180992" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient6240" + id="linearGradient4778" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(0.007843,0.7954955)" + x1="20.156862" + y1="5.0996137" + x2="20.156862" + y2="26.039215" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#849193" + borderopacity="1.0000000" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="5.6568542" + inkscape:cx="77.858376" + inkscape:cy="41.598514" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:grid-bbox="true" + inkscape:document-units="px" + inkscape:showpageshadow="false" + showborder="true" + inkscape:window-width="2509" + inkscape:window-height="1738" + inkscape:window-x="556" + inkscape:window-y="140" + showguides="true" + inkscape:guide-bbox="true" + inkscape:grid-points="true" + inkscape:window-maximized="0" /> + <metadata + id="metadata3309"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>Change Resolution</dc:title> + <dc:creator> + <cc:Agent> + <dc:title>Jakub Steiner</dc:title> + </cc:Agent> + </dc:creator> + <cc:license + rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" /> + <dc:subject> + <rdf:Bag> + <rdf:li>display</rdf:li> + <rdf:li>resolution</rdf:li> + <rdf:li>video</rdf:li> + </rdf:Bag> + </dc:subject> + <dc:contributor> + <cc:Agent> + <dc:title>Andreas Nilsson +Luca Ferretti <[email protected]></dc:title> + </cc:Agent> + </dc:contributor> + <dc:date /> + <dc:source>http://www.gnome.org</dc:source> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/GPL/2.0/"> + <cc:permits + rdf:resource="http://web.resource.org/cc/Reproduction" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/Distribution" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/Notice" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/DerivativeWorks" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/ShareAlike" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/SourceCode" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + id="layer1" + inkscape:label="Layer 1" + inkscape:groupmode="layer" + transform="translate(0,80)"> + <g + id="g100" + transform="matrix(2.6666587,0,0,2.6666587,0.12519204,-80.521133)"> + <g + transform="matrix(0.9308511,0,0,1.037397,1.6941489,-1.795056)" + style="opacity:0.3" + id="g2822"> + <rect + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#radialGradient2827);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" + id="rect1892" + width="10" + height="8" + x="-11" + y="-48" + transform="scale(-1)" /> + <rect + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#radialGradient2829);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" + id="rect2789" + width="10" + height="8" + x="38" + y="40" /> + <rect + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#linearGradient2831);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" + id="rect2793" + width="27" + height="8" + x="11" + y="40" /> + </g> + <rect + y="0.79549509" + x="0" + height="48" + width="48" + id="rect4784" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="csccscc" + id="path9222" + d="m 14.375479,36.328843 c 0,0 1.216876,4.9346 -3.856329,4.980924 -2.4302757,0.0219 -1.9324777,4.035151 -1.9324777,4.035151 l 30.8464667,-0.03137 c 0,0 0.418438,-3.895363 -2.022217,-3.941032 -4.987467,-0.09214 -3.810529,-5.106415 -3.810529,-5.106415 z" + style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#linearGradient5223);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccccc" + id="rect5040" + d="M 4.8886262,4.2739318 H 43.01746 c 2.025119,0 3.500978,1.4276545 3.500978,3.6261408 l 0.01095,26.2038354 c 0,1.692067 -0.528347,2.215229 -2.034656,2.215229 L 3.5320635,36.300202 C 2.3527922,36.271409 1.513468,35.805541 1.4976345,34.280899 L 1.5128113,7.7123281 c 0,-1.7738259 1.5394074,-3.4383963 3.3758149,-3.4383963 z" + style="fill:url(#linearGradient5147);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" /> + <rect + y="8.2973185" + x="5.5059438" + height="22.996691" + width="37.00032" + id="rect9208" + style="fill:#555753;fill-opacity:1;fill-rule:evenodd;stroke:#2e3436;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + d="m 4.8886719,5.1484375 c -1.3073224,0 -2.5,1.2966026 -2.5,2.5644531 L 2.3730469,34.271484 c 0.00626,0.602943 0.1382004,0.797516 0.2773437,0.917969 0.1391434,0.120454 0.4228271,0.22462 0.9023438,0.236328 l 40.9414066,0.01758 c 0.651929,0 0.862707,-0.109199 0.943359,-0.195312 0.08065,-0.08611 0.216797,-0.389657 0.216797,-1.144531 L 45.642578,7.9003906 c 0,-1.8071501 -1.011349,-2.7519531 -2.625,-2.7519531 z" + inkscape:href="#rect5040" + id="path5145" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1" + xlink:href="#rect5040" + inkscape:original="M 4.8886719 4.2734375 C 3.0522644 4.2734375 1.5136719 5.9390647 1.5136719 7.7128906 L 1.4980469 34.28125 C 1.5138804 35.805892 2.3519787 36.271988 3.53125 36.300781 L 44.494141 36.318359 C 46.00045 36.318359 46.529297 35.795583 46.529297 34.103516 L 46.517578 7.9003906 C 46.517578 5.7019043 45.042697 4.2734375 43.017578 4.2734375 L 4.8886719 4.2734375 z " + inkscape:radius="-0.875" + sodipodi:type="inkscape:offset" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" + id="path10672" + d="m 13.992156,36.834708 c 0.08366,2.172549 -0.283353,2.911443 -1.633624,3.675231 l 23.615402,1.297318 c -0.949019,-1.168628 -2.329411,-2.933334 -1.952941,-4.988235 z" + style="opacity:0.6;fill:url(#linearGradient11406);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="cc" + id="path6575" + d="m 9.9921262,42.291555 c 6.7066928,0.002 27.7967838,0 27.7967838,0" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#8d8d8f;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:0.43902438" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 9.647928,43.299429 c 6.706693,0.002 28.719861,0 28.719861,0" + id="path8029" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" + id="path4073" + d="M 6.031372,8.8268676 V 26.834708 C 22.475817,25.480459 28.630065,16.722289 41.999999,15.807256 L 42,8.7954956 Z" + style="opacity:0.4;fill:url(#linearGradient4778);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <g + transform="translate(-46.77135,-7.3370294)" + id="g4754"> + <g + id="g22150" + style="opacity:0.3" + transform="matrix(0.916667,0,0,0.714282,49.771334,11.132681)"> + <rect + y="35" + x="0" + height="7" + width="4" + id="rect22120" + style="opacity:1;fill:url(#radialGradient4770);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1" /> + <rect + transform="scale(-1)" + y="-42" + x="-48" + height="7" + width="4" + id="rect22134" + style="opacity:1;fill:url(#radialGradient4772);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1" /> + <rect + y="35" + x="4" + height="7" + width="40" + id="rect22138" + style="opacity:1;fill:url(#linearGradient4774);fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:1.20000052;stroke-opacity:1" /> + </g> + <g + id="g4743"> + <g + transform="translate(-5.228666,-6.29601)" + id="g10824"> + <path + inkscape:connector-curvature="0" + style="fill:url(#linearGradient4776);fill-opacity:1;fill-rule:evenodd;stroke:#a38503;stroke-width:1.00000024px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 57.5,14.928571 v 30 h 39 z m 6,13 15,11 h -15 z" + id="path4319" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 61.5,44.928571 v -3" + id="path4326" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 67.5,44.928571 v -3" + id="path4328" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="M 73.5,44.928571 V 41.964285" + id="path4330" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 79.5,44.928571 v -3" + id="path4332" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#a38503;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="m 85.5,44.928571 v -3" + id="path4334" + sodipodi:nodetypes="cc" /> + <path + inkscape:connector-curvature="0" + style="opacity:0.4;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.0000006px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 58.500002,16.928575 V 43.928586 H 93.500014 Z" + id="path6126" /> + </g> + <path + inkscape:connector-curvature="0" + transform="translate(47.771334,-4.867439)" + sodipodi:nodetypes="cccc" + id="path12221" + d="M 9.5017474,38.542446 9.4575532,24.532894 28.541592,38.563262 Z" + style="opacity:0.4;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/plugins/wlrandr/protocol/wlr-output-management-unstable-v1.xml b/plugins/wlrandr/protocol/wlr-output-management-unstable-v1.xml new file mode 100644 index 0000000..411e2f0 --- /dev/null +++ b/plugins/wlrandr/protocol/wlr-output-management-unstable-v1.xml @@ -0,0 +1,601 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="wlr_output_management_unstable_v1"> + <copyright> + Copyright © 2019 Purism SPC + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + </copyright> + + <description summary="protocol to configure output devices"> + This protocol exposes interfaces to obtain and modify output device + configuration. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + </description> + + <interface name="zwlr_output_manager_v1" version="4"> + <description summary="output device configuration manager"> + This interface is a manager that allows reading and writing the current + output device configuration. + + Output devices that display pixels (e.g. a physical monitor or a virtual + output in a window) are represented as heads. Heads cannot be created nor + destroyed by the client, but they can be enabled or disabled and their + properties can be changed. Each head may have one or more available modes. + + Whenever a head appears (e.g. a monitor is plugged in), it will be + advertised via the head event. Immediately after the output manager is + bound, all current heads are advertised. + + Whenever a head's properties change, the relevant wlr_output_head events + will be sent. Not all head properties will be sent: only properties that + have changed need to. + + Whenever a head disappears (e.g. a monitor is unplugged), a + wlr_output_head.finished event will be sent. + + After one or more heads appear, change or disappear, the done event will + be sent. It carries a serial which can be used in a create_configuration + request to update heads properties. + + The information obtained from this protocol should only be used for output + configuration purposes. This protocol is not designed to be a generic + output property advertisement protocol for regular clients. Instead, + protocols such as xdg-output should be used. + </description> + + <event name="head"> + <description summary="introduce a new head"> + This event introduces a new head. This happens whenever a new head + appears (e.g. a monitor is plugged in) or after the output manager is + bound. + </description> + <arg name="head" type="new_id" interface="zwlr_output_head_v1"/> + </event> + + <event name="done"> + <description summary="sent all information about current configuration"> + This event is sent after all information has been sent after binding to + the output manager object and after any subsequent changes. This applies + to child head and mode objects as well. In other words, this event is + sent whenever a head or mode is created or destroyed and whenever one of + their properties has been changed. Not all state is re-sent each time + the current configuration changes: only the actual changes are sent. + + This allows changes to the output configuration to be seen as atomic, + even if they happen via multiple events. + + A serial is sent to be used in a future create_configuration request. + </description> + <arg name="serial" type="uint" summary="current configuration serial"/> + </event> + + <request name="create_configuration"> + <description summary="create a new output configuration object"> + Create a new output configuration object. This allows to update head + properties. + </description> + <arg name="id" type="new_id" interface="zwlr_output_configuration_v1"/> + <arg name="serial" type="uint"/> + </request> + + <request name="stop"> + <description summary="stop sending events"> + Indicates the client no longer wishes to receive events for output + configuration changes. However the compositor may emit further events, + until the finished event is emitted. + + The client must not send any more requests after this one. + </description> + </request> + + <event name="finished" type="destructor"> + <description summary="the compositor has finished with the manager"> + This event indicates that the compositor is done sending manager events. + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + </description> + </event> + </interface> + + <interface name="zwlr_output_head_v1" version="4"> + <description summary="output device"> + A head is an output device. The difference between a wl_output object and + a head is that heads are advertised even if they are turned off. A head + object only advertises properties and cannot be used directly to change + them. + + A head has some read-only properties: modes, name, description and + physical_size. These cannot be changed by clients. + + Other properties can be updated via a wlr_output_configuration object. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + </description> + + <event name="name"> + <description summary="head name"> + This event describes the head name. + + The naming convention is compositor defined, but limited to alphanumeric + characters and dashes (-). Each name is unique among all wlr_output_head + objects, but if a wlr_output_head object is destroyed the same name may + be reused later. The names will also remain consistent across sessions + with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + If the compositor implements the xdg-output protocol and this head is + enabled, the xdg_output.name event must report the same name. + + The name event is sent after a wlr_output_head object is created. This + event is only sent once per object, and the name does not change over + the lifetime of the wlr_output_head object. + </description> + <arg name="name" type="string"/> + </event> + + <event name="description"> + <description summary="head description"> + This event describes a human-readable description of the head. + + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. However, do not assume that the name is a reflection of + the make, model, serial of the underlying DRM connector or the display + name of the underlying X11 connection, etc. + + If the compositor implements xdg-output and this head is enabled, + the xdg_output.description must report the same description. + + The description event is sent after a wlr_output_head object is created. + This event is only sent once per object, and the description does not + change over the lifetime of the wlr_output_head object. + </description> + <arg name="description" type="string"/> + </event> + + <event name="physical_size"> + <description summary="head physical size"> + This event describes the physical size of the head. This event is only + sent if the head has a physical size (e.g. is not a projector or a + virtual device). + </description> + <arg name="width" type="int" summary="width in millimeters of the output"/> + <arg name="height" type="int" summary="height in millimeters of the output"/> + </event> + + <event name="mode"> + <description summary="introduce a mode"> + This event introduces a mode for this head. It is sent once per + supported mode. + </description> + <arg name="mode" type="new_id" interface="zwlr_output_mode_v1"/> + </event> + + <event name="enabled"> + <description summary="head is enabled or disabled"> + This event describes whether the head is enabled. A disabled head is not + mapped to a region of the global compositor space. + + When a head is disabled, some properties (current_mode, position, + transform and scale) are irrelevant. + </description> + <arg name="enabled" type="int" summary="zero if disabled, non-zero if enabled"/> + </event> + + <event name="current_mode"> + <description summary="current mode"> + This event describes the mode currently in use for this head. It is only + sent if the output is enabled. + </description> + <arg name="mode" type="object" interface="zwlr_output_mode_v1"/> + </event> + + <event name="position"> + <description summary="current position"> + This events describes the position of the head in the global compositor + space. It is only sent if the output is enabled. + </description> + <arg name="x" type="int" + summary="x position within the global compositor space"/> + <arg name="y" type="int" + summary="y position within the global compositor space"/> + </event> + + <event name="transform"> + <description summary="current transformation"> + This event describes the transformation currently applied to the head. + It is only sent if the output is enabled. + </description> + <arg name="transform" type="int" enum="wl_output.transform"/> + </event> + + <event name="scale"> + <description summary="current scale"> + This events describes the scale of the head in the global compositor + space. It is only sent if the output is enabled. + </description> + <arg name="scale" type="fixed"/> + </event> + + <event name="finished"> + <description summary="the head has disappeared"> + This event indicates that the head is no longer available. The head + object becomes inert. Clients should send a destroy request and release + any resources associated with it. + </description> + </event> + + <!-- Version 2 additions --> + + <event name="make" since="2"> + <description summary="head manufacturer"> + This event describes the manufacturer of the head. + + This must report the same make as the wl_output interface does in its + geometry event. + + Together with the model and serial_number events the purpose is to + allow clients to recognize heads from previous sessions and for example + load head-specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the make of + the head or the definition of a make is not sensible in the current + setup, for example in a virtual session. Clients can still try to + identify the head by available information from other events but should + be aware that there is an increased risk of false positives. + + It is not recommended to display the make string in UI to users. For + that the string provided by the description event should be preferred. + </description> + <arg name="make" type="string"/> + </event> + + <event name="model" since="2"> + <description summary="head model"> + This event describes the model of the head. + + This must report the same model as the wl_output interface does in its + geometry event. + + Together with the make and serial_number events the purpose is to + allow clients to recognize heads from previous sessions and for example + load head-specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the model of + the head or the definition of a model is not sensible in the current + setup, for example in a virtual session. Clients can still try to + identify the head by available information from other events but should + be aware that there is an increased risk of false positives. + + It is not recommended to display the model string in UI to users. For + that the string provided by the description event should be preferred. + </description> + <arg name="model" type="string"/> + </event> + + <event name="serial_number" since="2"> + <description summary="head serial number"> + This event describes the serial number of the head. + + Together with the make and model events the purpose is to allow clients + to recognize heads from previous sessions and for example load head- + specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the serial + number of the head or the definition of a serial number is not sensible + in the current setup. Clients can still try to identify the head by + available information from other events but should be aware that there + is an increased risk of false positives. + + It is not recommended to display the serial_number string in UI to + users. For that the string provided by the description event should be + preferred. + </description> + <arg name="serial_number" type="string"/> + </event> + + <!-- Version 3 additions --> + + <request name="release" type="destructor" since="3"> + <description summary="destroy the head object"> + This request indicates that the client will no longer use this head + object. + </description> + </request> + + <!-- Version 4 additions --> + + <enum name="adaptive_sync_state" since="4"> + <entry name="disabled" value="0" summary="adaptive sync is disabled"/> + <entry name="enabled" value="1" summary="adaptive sync is enabled"/> + </enum> + + <event name="adaptive_sync" since="4"> + <description summary="current adaptive sync state"> + This event describes whether adaptive sync is currently enabled for + the head or not. Adaptive sync is also known as Variable Refresh + Rate or VRR. + </description> + <arg name="state" type="uint" enum="adaptive_sync_state"/> + </event> + </interface> + + <interface name="zwlr_output_mode_v1" version="3"> + <description summary="output mode"> + This object describes an output mode. + + Some heads don't support output modes, in which case modes won't be + advertised. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + </description> + + <event name="size"> + <description summary="mode size"> + This event describes the mode size. The size is given in physical + hardware units of the output device. This is not necessarily the same as + the output size in the global compositor space. For instance, the output + may be scaled or transformed. + </description> + <arg name="width" type="int" summary="width of the mode in hardware units"/> + <arg name="height" type="int" summary="height of the mode in hardware units"/> + </event> + + <event name="refresh"> + <description summary="mode refresh rate"> + This event describes the mode's fixed vertical refresh rate. It is only + sent if the mode has a fixed refresh rate. + </description> + <arg name="refresh" type="int" summary="vertical refresh rate in mHz"/> + </event> + + <event name="preferred"> + <description summary="mode is preferred"> + This event advertises this mode as preferred. + </description> + </event> + + <event name="finished"> + <description summary="the mode has disappeared"> + This event indicates that the mode is no longer available. The mode + object becomes inert. Clients should send a destroy request and release + any resources associated with it. + </description> + </event> + + <!-- Version 3 additions --> + + <request name="release" type="destructor" since="3"> + <description summary="destroy the mode object"> + This request indicates that the client will no longer use this mode + object. + </description> + </request> + </interface> + + <interface name="zwlr_output_configuration_v1" version="4"> + <description summary="output configuration"> + This object is used by the client to describe a full output configuration. + + First, the client needs to setup the output configuration. Each head can + be either enabled (and configured) or disabled. It is a protocol error to + send two enable_head or disable_head requests with the same head. It is a + protocol error to omit a head in a configuration. + + Then, the client can apply or test the configuration. The compositor will + then reply with a succeeded, failed or cancelled event. Finally the client + should destroy the configuration object. + </description> + + <enum name="error"> + <entry name="already_configured_head" value="1" + summary="head has been configured twice"/> + <entry name="unconfigured_head" value="2" + summary="head has not been configured"/> + <entry name="already_used" value="3" + summary="request sent after configuration has been applied or tested"/> + </enum> + + <request name="enable_head"> + <description summary="enable and configure a head"> + Enable a head. This request creates a head configuration object that can + be used to change the head's properties. + </description> + <arg name="id" type="new_id" interface="zwlr_output_configuration_head_v1" + summary="a new object to configure the head"/> + <arg name="head" type="object" interface="zwlr_output_head_v1" + summary="the head to be enabled"/> + </request> + + <request name="disable_head"> + <description summary="disable a head"> + Disable a head. + </description> + <arg name="head" type="object" interface="zwlr_output_head_v1" + summary="the head to be disabled"/> + </request> + + <request name="apply"> + <description summary="apply the configuration"> + Apply the new output configuration. + + In case the configuration is successfully applied, there is no guarantee + that the new output state matches completely the requested + configuration. For instance, a compositor might round the scale if it + doesn't support fractional scaling. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + </description> + </request> + + <request name="test"> + <description summary="test the configuration"> + Test the new output configuration. The configuration won't be applied, + but will only be validated. + + Even if the compositor succeeds to test a configuration, applying it may + fail. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + </description> + </request> + + <event name="succeeded"> + <description summary="configuration changes succeeded"> + Sent after the compositor has successfully applied the changes or + tested them. + + Upon receiving this event, the client should destroy this object. + + If the current configuration has changed, events to describe the changes + will be sent followed by a wlr_output_manager.done event. + </description> + </event> + + <event name="failed"> + <description summary="configuration changes failed"> + Sent if the compositor rejects the changes or failed to apply them. The + compositor should revert any changes made by the apply request that + triggered this event. + + Upon receiving this event, the client should destroy this object. + </description> + </event> + + <event name="cancelled"> + <description summary="configuration has been cancelled"> + Sent if the compositor cancels the configuration because the state of an + output changed and the client has outdated information (e.g. after an + output has been hotplugged). + + The client can create a new configuration with a newer serial and try + again. + + Upon receiving this event, the client should destroy this object. + </description> + </event> + + <request name="destroy" type="destructor"> + <description summary="destroy the output configuration"> + Using this request a client can tell the compositor that it is not going + to use the configuration object anymore. Any changes to the outputs + that have not been applied will be discarded. + + This request also destroys wlr_output_configuration_head objects created + via this object. + </description> + </request> + </interface> + + <interface name="zwlr_output_configuration_head_v1" version="4"> + <description summary="head configuration"> + This object is used by the client to update a single head's configuration. + + It is a protocol error to set the same property twice. + </description> + + <enum name="error"> + <entry name="already_set" value="1" summary="property has already been set"/> + <entry name="invalid_mode" value="2" summary="mode doesn't belong to head"/> + <entry name="invalid_custom_mode" value="3" summary="mode is invalid"/> + <entry name="invalid_transform" value="4" summary="transform value outside enum"/> + <entry name="invalid_scale" value="5" summary="scale negative or zero"/> + <entry name="invalid_adaptive_sync_state" value="6" since="4" + summary="invalid enum value used in the set_adaptive_sync request"/> + </enum> + + <request name="set_mode"> + <description summary="set the mode"> + This request sets the head's mode. + </description> + <arg name="mode" type="object" interface="zwlr_output_mode_v1"/> + </request> + + <request name="set_custom_mode"> + <description summary="set a custom mode"> + This request assigns a custom mode to the head. The size is given in + physical hardware units of the output device. If set to zero, the + refresh rate is unspecified. + + It is a protocol error to set both a mode and a custom mode. + </description> + <arg name="width" type="int" summary="width of the mode in hardware units"/> + <arg name="height" type="int" summary="height of the mode in hardware units"/> + <arg name="refresh" type="int" summary="vertical refresh rate in mHz or zero"/> + </request> + + <request name="set_position"> + <description summary="set the position"> + This request sets the head's position in the global compositor space. + </description> + <arg name="x" type="int" summary="x position in the global compositor space"/> + <arg name="y" type="int" summary="y position in the global compositor space"/> + </request> + + <request name="set_transform"> + <description summary="set the transform"> + This request sets the head's transform. + </description> + <arg name="transform" type="int" enum="wl_output.transform"/> + </request> + + <request name="set_scale"> + <description summary="set the scale"> + This request sets the head's scale. + </description> + <arg name="scale" type="fixed"/> + </request> + + <!-- Version 4 additions --> + + <request name="set_adaptive_sync" since="4"> + <description summary="enable/disable adaptive sync"> + This request enables/disables adaptive sync. Adaptive sync is also + known as Variable Refresh Rate or VRR. + </description> + <arg name="state" type="uint" enum="zwlr_output_head_v1.adaptive_sync_state"/> + </request> + </interface> +</protocol> diff --git a/plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in b/plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in new file mode 100644 index 0000000..b44192c --- /dev/null +++ b/plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in @@ -0,0 +1,9 @@ +[MATE Settings Plugin] +Module=wlrandr +IAge=0 +WaylandOnly=true +Name=WLrandr +Description=Configure Wayland outputs +Authors=Various +Copyright=Copyright © 2026 MATE Developers +Website= diff --git a/plugins/xrandr/msd-xrandr-manager.c b/plugins/xrandr/msd-xrandr-manager.c index 78e538f..f49db50 100644 --- a/plugins/xrandr/msd-xrandr-manager.c +++ b/plugins/xrandr/msd-xrandr-manager.c @@ -36,7 +36,9 @@ #include <glib.h> #include <glib/gi18n.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ #include <gtk/gtk.h> #include <gio/gio.h> @@ -1572,6 +1574,22 @@ apply_color_profiles (void) } } +static gboolean +has_hotplug_mode_update (MateRRScreen *screen) +{ + MateRROutput **outputs; + int i; + + outputs = mate_rr_screen_list_outputs (screen); + + for (i = 0; outputs[i] != NULL; i++) { + if (mate_rr_output_get_hotplug_mode_update (outputs[i])) + return TRUE; + } + + return FALSE; +} + static void on_randr_event (MateRRScreen *screen, gpointer data) { @@ -1600,6 +1618,24 @@ on_randr_event (MateRRScreen *screen, gpointer data) */ show_timestamps_dialog (manager, "ignoring since change > config"); log_msg (" Ignoring event since change >= config\n"); + } else if (has_hotplug_mode_update (screen)) { + /* An output has the hotplug_mode_update property, which + * means that the driver is requesting that the desktop + * environment apply a new preferred mode on hotplug events + * to handle dynamic guest resizing (e.g. SPICE/QXL in VMs). + * + * In this case, always auto-configure to use the new + * preferred mode rather than applying stored configurations. + */ + + show_timestamps_dialog (manager, "hotplug_mode_update detected, auto-configuring"); + + if (config_timestamp != priv->last_config_timestamp) { + priv->last_config_timestamp = config_timestamp; + auto_configure_outputs (manager, config_timestamp); + log_msg (" Automatically configured outputs for hotplug_mode_update\n"); + } else + log_msg (" Ignored hotplug_mode_update event as timestamps are the same\n"); } else { /* Here, config_timestamp > change_timestamp. This means that * the screen got reconfigured because of hotplug/unplug; the X @@ -2548,6 +2584,20 @@ msd_xrandr_manager_start (MsdXrandrManager *manager, g_debug ("Starting xrandr manager"); mate_settings_profile_start (NULL); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "X11 display is required for the xrandr plugin"); + mate_settings_profile_end (NULL); + return FALSE; + } +#else + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "The xrandr plugin was built without X11 support"); + mate_settings_profile_end (NULL); + return FALSE; +#endif /* GDK_WINDOWING_X11 */ + log_open (); log_msg ("------------------------------------------------------------\nSTARTING XRANDR PLUGIN\n"); @@ -2629,6 +2679,11 @@ msd_xrandr_manager_stop (MsdXrandrManager *manager) g_debug ("Stopping xrandr manager"); +#ifdef GDK_WINDOWING_X11 + if (!GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + return; +#endif /* GDK_WINDOWING_X11 */ + manager->priv->running = FALSE; display = gdk_display_get_default (); diff --git a/plugins/xrandr/xrandr.mate-settings-plugin.desktop.in b/plugins/xrandr/xrandr.mate-settings-plugin.desktop.in index 1c49e42..411489a 100644 --- a/plugins/xrandr/xrandr.mate-settings-plugin.desktop.in +++ b/plugins/xrandr/xrandr.mate-settings-plugin.desktop.in @@ -1,6 +1,7 @@ [MATE Settings Plugin] Module=xrandr IAge=0 +X11Only=true Name=XRandR Description=Set up screen size and rotation settings Authors=Various diff --git a/plugins/xsettings/msd-xsettings-manager.c b/plugins/xsettings/msd-xsettings-manager.c index 86ff376..879aa81 100644 --- a/plugins/xsettings/msd-xsettings-manager.c +++ b/plugins/xsettings/msd-xsettings-manager.c @@ -36,7 +36,12 @@ #include <glib.h> #include <glib/gi18n.h> #include <gdk/gdk.h> +#ifdef GDK_WINDOWING_X11 #include <gdk/gdkx.h> +#endif /* GDK_WINDOWING_X11 */ +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif /* GDK_WINDOWING_WAYLAND */ #include <gtk/gtk.h> #include <gio/gio.h> @@ -108,6 +113,7 @@ struct _TranslationEntry { struct MateXSettingsManagerPrivate { XSettingsManager **managers; + Display *xdisplay; GHashTable *gsettings; GSettings *gsettings_font; GSettings *plugin_settings; @@ -249,6 +255,10 @@ get_window_scale_auto (void) display = gdk_display_get_default (); monitor = gdk_display_get_primary_monitor (display); + /* If no monitor is available, use the current value as the default */ + if (monitor == NULL) + return 1; + /* Use current value as the default */ window_scale = 1; @@ -322,10 +332,10 @@ get_dpi_from_x_server (void) if (screen != NULL) { double width_dpi, height_dpi; - Screen *xscreen = gdk_x11_screen_get_xscreen (screen); - - width_dpi = dpi_from_pixels_and_mm (WidthOfScreen (xscreen), WidthMMOfScreen (xscreen)); - height_dpi = dpi_from_pixels_and_mm (HeightOfScreen (xscreen), HeightMMOfScreen (xscreen)); + width_dpi = dpi_from_pixels_and_mm (gdk_screen_get_width (screen), + gdk_screen_get_width_mm (screen)); + height_dpi = dpi_from_pixels_and_mm (gdk_screen_get_height (screen), + gdk_screen_get_height_mm (screen)); if (width_dpi < DPI_LOW_REASONABLE_VALUE || width_dpi > DPI_HIGH_REASONABLE_VALUE || height_dpi < DPI_LOW_REASONABLE_VALUE || height_dpi > DPI_HIGH_REASONABLE_VALUE) { @@ -628,7 +638,11 @@ xft_settings_set_xresources (MateXftSettings *settings) /* get existing properties */ dpy = XOpenDisplay (NULL); - g_return_if_fail (dpy != NULL); + if (dpy == NULL) { + g_warning ("Unable to open X display for X resources"); + mate_settings_profile_end (NULL); + return; + } add_string = g_string_new (XResourceManagerString (dpy)); g_debug("xft_settings_set_xresources: orig res '%s'", add_string->str); @@ -868,32 +882,56 @@ terminate_cb (void *data) gtk_main_quit (); } +#ifdef GDK_WINDOWING_WAYLAND +/* No-op Xlib IO error handler used when running on Wayland: the X connection + * only serves XSettings on XWayland, so the daemon should survive an + * XWayland restart instead of exiting. */ +static int +wayland_x_io_error_handler (Display *display) +{ + g_warning ("Connection to the XWayland display lost; XSettings for " + "X11 applications disabled"); + return 0; +} +#endif /* GDK_WINDOWING_WAYLAND */ + static gboolean setup_xsettings_managers (MateXSettingsManager *manager) { - GdkDisplay *display; - gboolean res; - gboolean terminated; + Display *xdisplay; + gboolean res; + gboolean terminated; - display = gdk_display_get_default (); + xdisplay = XOpenDisplay (NULL); + if (xdisplay == NULL) { + g_warning ("Unable to open X display; xsettings manager disabled"); + return FALSE; + } - res = xsettings_manager_check_running (gdk_x11_display_get_xdisplay (display), - gdk_x11_screen_get_screen_number (gdk_screen_get_default ())); +#ifdef GDK_WINDOWING_WAYLAND + /* When running on Wayland the XSETTINGS manager uses its own Xlib + * connection to XWayland, which nothing else watches. Now that we + * explicitly flush it, a disappearing XWayland would otherwise kill + * the whole daemon through the default Xlib IO error handler. */ + if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ())) { + XSetIOErrorHandler (wayland_x_io_error_handler); + } +#endif /* GDK_WINDOWING_WAYLAND */ + + res = xsettings_manager_check_running (xdisplay, DefaultScreen (xdisplay)); if (res) { g_warning ("You can only run one xsettings manager at a time; exiting"); + XCloseDisplay (xdisplay); return FALSE; } + manager->priv->xdisplay = xdisplay; manager->priv->managers = g_new0 (XSettingsManager *, 2); terminated = FALSE; - GdkScreen *screen; - - screen = gdk_display_get_default_screen (display); - - manager->priv->managers [0] = xsettings_manager_new (gdk_x11_display_get_xdisplay (display), - gdk_x11_screen_get_screen_number (screen), + manager->priv->managers [0] = xsettings_manager_new (xdisplay, + DefaultScreen (xdisplay), terminate_cb, &terminated); if (! manager->priv->managers [0]) { @@ -1011,6 +1049,11 @@ mate_xsettings_manager_stop (MateXSettingsManager *manager) p->managers = NULL; } + if (p->xdisplay != NULL) { + XCloseDisplay (p->xdisplay); + p->xdisplay = NULL; + } + if (p->gsettings != NULL) { g_hash_table_destroy (p->gsettings); p->gsettings = NULL; diff --git a/plugins/xsettings/wm-common.c b/plugins/xsettings/wm-common.c index 8b5be13..804a727 100644 --- a/plugins/xsettings/wm-common.c +++ b/plugins/xsettings/wm-common.c @@ -1,11 +1,13 @@ - #include <X11/Xatom.h> -#include <gdk/gdkx.h> -#include <gdk/gdk.h> #include <string.h> #include <glib.h> #include <glib-object.h> #include "wm-common.h" +#ifdef GDK_WINDOWING_X11 +#include <X11/Xatom.h> +#include <gdk/gdkx.h> +#include <gdk/gdk.h> + /* Our WM Window */ static Window wm_window = None; @@ -112,3 +114,18 @@ wm_common_update_window () { update_wm_window(); } + +#else /* !GDK_WINDOWING_X11 */ + +gchar* +wm_common_get_current_window_manager (void) +{ + return g_strdup (WM_COMMON_UNKNOWN); +} + +void +wm_common_update_window () +{ +} + +#endif /* GDK_WINDOWING_X11 */ diff --git a/plugins/xsettings/xsettings-manager.c b/plugins/xsettings/xsettings-manager.c index d8f5dd5..3aeffa6 100644 --- a/plugins/xsettings/xsettings-manager.c +++ b/plugins/xsettings/xsettings-manager.c @@ -188,6 +188,11 @@ xsettings_manager_new (Display *display, manager->terminate (manager->cb_data); } + /* The display may not be watched by the main loop (e.g. when running on + * Wayland the XSettings manager uses its own connection to XWayland), so + * make sure the MANAGER message actually reaches the server. */ + XFlush (manager->display); + return manager; } @@ -428,6 +433,11 @@ xsettings_manager_notify (XSettingsManager *manager) free (buffer.data); + /* See the note in xsettings_manager_new(): on Wayland nothing else + * flushes this connection, so without this clients never see the + * property and none of the settings are applied. */ + XFlush (manager->display); + return XSETTINGS_SUCCESS; } diff --git a/po/POTFILES.in b/po/POTFILES.in index 3a965df..2a1c106 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -25,6 +25,7 @@ data/org.mate.SettingsDaemon.plugins.typing-break.gschema.xml.in data/org.mate.SettingsDaemon.plugins.xrandr.gschema.xml.in data/org.mate.SettingsDaemon.plugins.xrdb.gschema.xml.in data/org.mate.SettingsDaemon.plugins.xsettings.gschema.xml.in +data/org.mate.SettingsDaemon.plugins.wlrandr.gschema.xml.in data/mate-settings-daemon.desktop.in.in mate-settings-daemon/main.c plugins/a11y-keyboard/a11y-keyboard.mate-settings-plugin.desktop.in @@ -62,3 +63,5 @@ plugins/xrdb/msd-xrdb-manager.c plugins/xrdb/xrdb.mate-settings-plugin.desktop.in plugins/xsettings/msd-xsettings-manager.c plugins/xsettings/xsettings.mate-settings-plugin.desktop.in +plugins/wlrandr/msd-wlrandr-manager.c +plugins/wlrandr/wlrandr.mate-settings-plugin.desktop.in @@ -1,9 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR MATE Desktop Environment team # This file is distributed under the same license as the mate-settings-daemon package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -# Translators: # alexandros_ <[email protected]>, 2018 # Petros Freris (Πέτρος Φρέρης) <[email protected]>, 2018 # 437c9d6e19936ed69f57bed9e0fe4716, 2018 @@ -11,24 +8,29 @@ # Alexandros Kapetanios <[email protected]>, 2019 # Stefano Karapetsas <[email protected]>, 2019 # Transifex Bot <>, 2026 -# +# Jim Spentzos <[email protected]>, 2026. +# +# Translators: +# msgid "" msgstr "" "Project-Id-Version: mate-settings-daemon 1.26.0\n" "Report-Msgid-Bugs-To: https://mate-desktop.org/\n" "POT-Creation-Date: 2022-10-29 15:35+0200\n" -"PO-Revision-Date: 2018-03-12 08:16+0000\n" -"Last-Translator: Transifex Bot <>, 2026\n" -"Language-Team: Greek (https://app.transifex.com/mate/teams/13566/el/)\n" +"PO-Revision-Date: 2026-07-29 00:18+0000\n" +"Last-Translator: Jim Spentzos <[email protected]>\n" +"Language-Team: Greek <https://translate.fedoraproject.org/projects/mate-" +"desktop/applications/mate-settings-daemon/el/>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: el\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 2026.7.1\n" #: data/50-accessibility.xml.in:2 msgid "Accessibility" -msgstr "Προσιτότητα" +msgstr "Προσβασιμότητα" #: data/50-accessibility.xml.in:4 #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:155 @@ -1600,7 +1602,7 @@ msgstr "Κανονική" #: plugins/xrandr/msd-xrandr-manager.c:2087 msgid "Left" -msgstr "Αριστερά" +msgstr "Αριστερό βέλος" #: plugins/xrandr/msd-xrandr-manager.c:2088 msgid "Right" @@ -1,28 +1,30 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR MATE Desktop Environment team # This file is distributed under the same license as the mate-settings-daemon package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -# Translators: # Stefano Karapetsas <[email protected]>, 2019 -# +# Temuri Doghonadze <[email protected]>, 2026. +# +# Translators: +# msgid "" msgstr "" "Project-Id-Version: mate-settings-daemon 1.26.0\n" "Report-Msgid-Bugs-To: https://mate-desktop.org/\n" "POT-Creation-Date: 2022-10-29 15:35+0200\n" -"PO-Revision-Date: 2018-03-12 08:16+0000\n" -"Last-Translator: Stefano Karapetsas <[email protected]>, 2019\n" -"Language-Team: Georgian (https://app.transifex.com/mate/teams/13566/ka/)\n" +"PO-Revision-Date: 2026-07-10 05:17+0000\n" +"Last-Translator: Temuri Doghonadze <[email protected]>\n" +"Language-Team: Georgian <https://translate.fedoraproject.org/projects/mate-" +"desktop/applications/mate-settings-daemon/ka/>\n" +"Language: ka\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: ka\n" -"Plural-Forms: nplurals=2; plural=(n!=1);\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 2026.7.1.dev0\n" #: data/50-accessibility.xml.in:2 msgid "Accessibility" -msgstr "_დამხმარე საშუალებები" +msgstr "წვდომადობა" #: data/50-accessibility.xml.in:4 #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:155 @@ -65,7 +67,7 @@ msgstr "" #: data/org.mate.font-rendering.gschema.xml.in:22 msgid "DPI" -msgstr "" +msgstr "DPI" #: data/org.mate.font-rendering.gschema.xml.in:23 msgid "" @@ -75,7 +77,7 @@ msgstr "" #: data/org.mate.font-rendering.gschema.xml.in:27 msgid "Antialiasing" -msgstr "" +msgstr "მოგლუვება" #: data/org.mate.font-rendering.gschema.xml.in:28 msgid "" @@ -86,7 +88,7 @@ msgstr "" #: data/org.mate.font-rendering.gschema.xml.in:32 msgid "Hinting" -msgstr "" +msgstr "ჰინტინგი" #: data/org.mate.font-rendering.gschema.xml.in:33 msgid "" @@ -622,7 +624,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:75 msgid "Eject" -msgstr "მოხსნა" +msgstr "გამოღება" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:76 msgid "Binding to eject an optical disc." @@ -882,12 +884,12 @@ msgstr "" #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:619 #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:755 msgid "Activate" -msgstr "" +msgstr "გააქტიურება" #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:619 #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:755 msgid "Deactivate" -msgstr "" +msgstr "დეაქტივაცია" #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:674 #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:813 @@ -902,7 +904,7 @@ msgstr "ა_რ გამორთო" #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:677 #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:816 msgid "_Activate" -msgstr "გააქ_ტიურება" +msgstr "გა_აქტიურება" #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:677 #: plugins/a11y-keyboard/msd-a11y-keyboard-manager.c:816 @@ -1003,11 +1005,11 @@ msgstr "ფონი" #: plugins/background/background.mate-settings-plugin.desktop.in:6 msgid "Background plugin" -msgstr "" +msgstr "ფონის დამატება" #: plugins/clipboard/clipboard.mate-settings-plugin.desktop.in:5 msgid "Clipboard" -msgstr "" +msgstr "ბუფერი" #: plugins/clipboard/clipboard.mate-settings-plugin.desktop.in:6 msgid "Clipboard plugin" @@ -1039,7 +1041,7 @@ msgstr "" #: plugins/dummy/dummy.mate-settings-plugin.desktop.in:5 msgid "Dummy" -msgstr "" +msgstr "საცობი" #: plugins/dummy/dummy.mate-settings-plugin.desktop.in:6 msgid "Dummy plugin" @@ -1047,7 +1049,7 @@ msgstr "" #: plugins/housekeeping/housekeeping.mate-settings-plugin.desktop.in:5 msgid "Housekeeping" -msgstr "" +msgstr "სახლის დალაგება" #: plugins/housekeeping/housekeeping.mate-settings-plugin.desktop.in:6 msgid "" @@ -1108,11 +1110,11 @@ msgstr "სანაგვე ყუთის დაცარიელება" #: plugins/housekeeping/msd-ldsm-dialog.c:444 msgid "Examine…" -msgstr "" +msgstr "გამოცდა…" #: plugins/housekeeping/msd-ldsm-dialog.c:451 msgid "Ignore" -msgstr "" +msgstr "იგნორი" #: plugins/housekeeping/msd-ldsm-trash-empty.c:98 #, c-format @@ -1136,7 +1138,7 @@ msgstr "" #. Translators: "Emptying trash from <device>" #: plugins/housekeeping/msd-ldsm-trash-empty.c:304 msgid "From: " -msgstr "" +msgstr "მოწყობილობიდან: " #: plugins/housekeeping/msd-ldsm-trash-empty.c:356 msgid "Empty all of the items from the trash?" @@ -1156,7 +1158,7 @@ msgstr "_სანაგვე ყუთის დაცარიელება #: plugins/keybindings/keybindings.mate-settings-plugin.desktop.in:5 msgid "Keybindings" -msgstr "" +msgstr "კლავიატურის მალსახმობები" #: plugins/keybindings/keybindings.mate-settings-plugin.desktop.in:6 msgid "Keybindings plugin" @@ -1208,7 +1210,7 @@ msgstr "" #: plugins/keyboard/msd-keyboard-xkb.c:285 msgid "_Layouts" -msgstr "" +msgstr "გან_ლაგებები" #: plugins/keyboard/msd-keyboard-xkb.c:292 msgid "Keyboard _Preferences" @@ -1289,7 +1291,7 @@ msgstr "" #: plugins/mouse/mouse.mate-settings-plugin.desktop.in:5 msgid "Mouse" -msgstr "თაგვი" +msgstr "თაგუნა" #: plugins/mouse/mouse.mate-settings-plugin.desktop.in:6 msgid "Mouse plugin" @@ -1310,7 +1312,7 @@ msgstr "თაგვის პარამეტრები" #: plugins/mpris/mpris.mate-settings-plugin.desktop.in:5 msgid "Mpris" -msgstr "" +msgstr "Mpris" #: plugins/mpris/mpris.mate-settings-plugin.desktop.in:6 msgid "Mpris plugin" @@ -1320,7 +1322,7 @@ msgstr "" #. Priority=100 #: plugins/rfkill/rfkill.mate-settings-plugin.desktop.in:7 msgid "Rfkill" -msgstr "" +msgstr "Rfkill" #: plugins/rfkill/rfkill.mate-settings-plugin.desktop.in:8 msgid "Rfkill plugin" @@ -1378,11 +1380,11 @@ msgstr "" #: plugins/smartcard/msd-smartcard.c:163 plugins/smartcard/msd-smartcard.c:164 msgid "name" -msgstr "" +msgstr "სახელი" #: plugins/smartcard/msd-smartcard.c:168 msgid "Module" -msgstr "" +msgstr "მოდული" #: plugins/smartcard/msd-smartcard.c:169 msgid "smartcard driver" @@ -1390,7 +1392,7 @@ msgstr "" #: plugins/smartcard/smartcard.mate-settings-plugin.desktop.in:5 msgid "Smartcard" -msgstr "" +msgstr "ჭკვიანი ბარათი" #: plugins/smartcard/smartcard.mate-settings-plugin.desktop.in:6 msgid "Smartcard plugin" @@ -1472,15 +1474,15 @@ msgstr "" #: plugins/xrandr/msd-xrandr-manager.c:2086 msgid "Normal" -msgstr "ჩვეულებრივი" +msgstr "ნორმალური" #: plugins/xrandr/msd-xrandr-manager.c:2087 msgid "Left" -msgstr "მარცხნივ" +msgstr "მარცხენა" #: plugins/xrandr/msd-xrandr-manager.c:2088 msgid "Right" -msgstr "მარჯვნივ" +msgstr "მარჯვენა" #: plugins/xrandr/msd-xrandr-manager.c:2089 msgid "Upside Down" @@ -1488,7 +1490,7 @@ msgstr "ქვემოდან ზემოთ" #: plugins/xrandr/msd-xrandr-manager.c:2178 msgid "ON" -msgstr "" +msgstr "ჩართ" #: plugins/xrandr/msd-xrandr-manager.c:2179 msgid "Turn this monitor off" @@ -1496,7 +1498,7 @@ msgstr "" #: plugins/xrandr/msd-xrandr-manager.c:2182 msgid "OFF" -msgstr "" +msgstr "გამორთ" #: plugins/xrandr/msd-xrandr-manager.c:2183 msgid "Turn this monitor on" @@ -1528,7 +1530,7 @@ msgstr "" #: plugins/xrandr/xrandr.mate-settings-plugin.desktop.in:5 msgid "XRandR" -msgstr "" +msgstr "XRandR" #: plugins/xrandr/xrandr.mate-settings-plugin.desktop.in:6 msgid "Set up screen size and rotation settings" @@ -1,30 +1,33 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR MATE Desktop Environment team # This file is distributed under the same license as the mate-settings-daemon package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -# Translators: # Azwaw <[email protected]>, 2020 # ButterflyOfFire, 2025 # Transifex Bot <>, 2026 -# +# ButterflyOfFire <[email protected]>, 2026. +# +# Translators: +# msgid "" msgstr "" "Project-Id-Version: mate-settings-daemon 1.26.0\n" "Report-Msgid-Bugs-To: https://mate-desktop.org/\n" "POT-Creation-Date: 2022-10-29 15:35+0200\n" -"PO-Revision-Date: 2018-03-12 08:16+0000\n" -"Last-Translator: Transifex Bot <>, 2026\n" -"Language-Team: Kabyle (https://app.transifex.com/mate/teams/13566/kab/)\n" +"PO-Revision-Date: 2026-06-24 19:18+0000\n" +"Last-Translator: ButterflyOfFire " +"<[email protected]>\n" +"Language-Team: Kabyle <https://translate.fedoraproject.org/projects/mate-" +"desktop/applications/mate-settings-daemon/kab/>\n" +"Language: kab\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: kab\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Weblate 2026.6.1\n" #: data/50-accessibility.xml.in:2 msgid "Accessibility" -msgstr "" +msgstr "Tuffart" #: data/50-accessibility.xml.in:4 #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:155 @@ -88,7 +91,7 @@ msgstr "" #: data/org.mate.font-rendering.gschema.xml.in:32 msgid "Hinting" -msgstr "" +msgstr "Areqqeɛ" #: data/org.mate.font-rendering.gschema.xml.in:33 msgid "" @@ -548,7 +551,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:30 msgid "Volume mute" -msgstr "" +msgstr "Gzem imesli" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:31 msgid "Binding to mute the system volume." @@ -556,7 +559,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:35 msgid "Volume down" -msgstr "" +msgstr "Ssider-as i ubleɣ" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:36 msgid "Binding to lower the system volume." @@ -564,7 +567,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:40 msgid "Volume up" -msgstr "" +msgstr "Rnu-yas i ubleɣ" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:41 msgid "Binding to raise the system volume." @@ -604,7 +607,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:65 msgid "Shut down" -msgstr "" +msgstr "Sexsi" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:66 msgid "Binding to shut down." @@ -612,7 +615,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:70 msgid "Log out" -msgstr "" +msgstr "Senser tuqqna" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:71 msgid "Binding to log out." @@ -620,7 +623,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:75 msgid "Eject" -msgstr "" +msgstr "Ḍeqqer" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:76 msgid "Binding to eject an optical disc." @@ -628,7 +631,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:80 msgid "Home folder" -msgstr "" +msgstr "Akaram umager" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:81 msgid "Binding to open the Home folder." @@ -636,7 +639,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:85 msgid "Search" -msgstr "Rechercher" +msgstr "Nadi" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:86 msgid "Binding to launch the search tool." @@ -644,7 +647,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:90 msgid "Launch email client" -msgstr "" +msgstr "Sekker amsaɣ n tirawt" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:91 msgid "Binding to launch the email client." @@ -660,7 +663,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:100 msgid "Lock screen" -msgstr "" +msgstr "Lock screen" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:101 msgid "Binding to lock the screen." @@ -668,7 +671,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:105 msgid "Launch help browser" -msgstr "" +msgstr "Serreḥ i iminig n uɛiwen" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:106 msgid "Binding to launch the help browser." @@ -676,7 +679,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:110 msgid "Launch calculator" -msgstr "" +msgstr "Sekker taselkimt" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:111 msgid "Binding to launch the calculator." @@ -692,7 +695,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:120 msgid "Launch web browser" -msgstr "" +msgstr "Sekker iminig n web" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:121 msgid "Binding to launch the web browser." @@ -700,7 +703,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:125 msgid "Launch media player" -msgstr "" +msgstr "Sekker ameɣri n umidya" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:126 msgid "Binding to launch the media player." @@ -716,7 +719,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:135 msgid "Pause playback" -msgstr "" +msgstr "Sesganfi taɣuri" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:136 msgid "Binding to pause playback." @@ -724,7 +727,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:140 msgid "Stop playback" -msgstr "" +msgstr "Ḥbes taɣuri" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:141 msgid "Binding to stop playback." @@ -732,7 +735,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:145 msgid "Previous track" -msgstr "" +msgstr "Tafuɣalt tuzwirt" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:146 msgid "Binding to skip to previous track." @@ -740,7 +743,7 @@ msgstr "" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:150 msgid "Next track" -msgstr "" +msgstr "Tafuɣalt tuḍfirt" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:151 msgid "Binding to skip to next track." @@ -821,7 +824,7 @@ msgstr "" #: mate-settings-daemon/main.c:66 msgid "Enable debugging code" -msgstr "" +msgstr "Ulamek aseɣti n tengalt" #: mate-settings-daemon/main.c:67 msgid "Replace the current daemon" @@ -989,7 +992,7 @@ msgstr "" #: plugins/background/background.mate-settings-plugin.desktop.in:5 msgid "Background" -msgstr "Arrière-plan" +msgstr "Agilal" #: plugins/background/background.mate-settings-plugin.desktop.in:6 msgid "Background plugin" @@ -1360,7 +1363,7 @@ msgstr "" #: plugins/smartcard/msd-smartcard.c:163 plugins/smartcard/msd-smartcard.c:164 msgid "name" -msgstr "" +msgstr "isem" #: plugins/smartcard/msd-smartcard.c:168 msgid "Module" @@ -1454,15 +1457,15 @@ msgstr "" #: plugins/xrandr/msd-xrandr-manager.c:2086 msgid "Normal" -msgstr "" +msgstr "Amagnu" #: plugins/xrandr/msd-xrandr-manager.c:2087 msgid "Left" -msgstr "Aẓelmaḍ" +msgstr "Ẓelmaḍ" #: plugins/xrandr/msd-xrandr-manager.c:2088 msgid "Right" -msgstr "Ayfus" +msgstr "Yeffus" #: plugins/xrandr/msd-xrandr-manager.c:2089 msgid "Upside Down" @@ -1,29 +1,31 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR MATE Desktop Environment team # This file is distributed under the same license as the mate-settings-daemon package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -# Translators: # Alan Lee <[email protected]>, 2018 # D.K Kim <[email protected]>, 2018 # Seong-ho Cho <[email protected]>, 2019 # 1763f4a4329a2376c933c5e919a36cbc_341ca53 <1f851310383599d03339229d772e1290_119292>, 2019 # Stefano Karapetsas <[email protected]>, 2019 # Junghee Lee <[email protected]>, 2021 -# +# 김인수 <[email protected]>, 2026. +# +# Translators: +# msgid "" msgstr "" "Project-Id-Version: mate-settings-daemon 1.26.0\n" "Report-Msgid-Bugs-To: https://mate-desktop.org/\n" "POT-Creation-Date: 2022-10-29 15:35+0200\n" -"PO-Revision-Date: 2018-03-12 08:16+0000\n" -"Last-Translator: Junghee Lee <[email protected]>, 2021\n" -"Language-Team: Korean (https://app.transifex.com/mate/teams/13566/ko/)\n" +"PO-Revision-Date: 2026-06-16 10:10+0000\n" +"Last-Translator: 김인수 <[email protected]>\n" +"Language-Team: Korean <https://translate.fedoraproject.org/projects/mate-" +"desktop/mate-settings-daemon/ko/>\n" +"Language: ko\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: ko\n" "Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 2026.6.1\n" #: data/50-accessibility.xml.in:2 msgid "Accessibility" @@ -1429,7 +1431,7 @@ msgstr "" #: plugins/sound/sound.mate-settings-plugin.desktop.in:5 msgid "Sound" -msgstr "사운드" +msgstr "음향" #: plugins/sound/sound.mate-settings-plugin.desktop.in:6 msgid "Sound Sample Cache plugin" @@ -1,28 +1,30 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR MATE Desktop Environment team # This file is distributed under the same license as the mate-settings-daemon package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -# Translators: -# 48c2de07903ce5a77a8e90265f95c4c2_8909374 <837c8d0ab97b27e737893c19f79bd4de_348476>, 2018 # Stefano Karapetsas <[email protected]>, 2019 # Cédric Valmary <[email protected]>, 2019 # Quentin PAGÈS, 2025 # Transifex Bot <>, 2026 -# +# Quentin PAGÈS <[email protected]>, 2026. +# +# Translators: +# 48c2de07903ce5a77a8e90265f95c4c2_8909374 <837c8d0ab97b27e737893c19f79bd4de_348476>, 2018 +# msgid "" msgstr "" "Project-Id-Version: mate-settings-daemon 1.26.0\n" "Report-Msgid-Bugs-To: https://mate-desktop.org/\n" "POT-Creation-Date: 2022-10-29 15:35+0200\n" -"PO-Revision-Date: 2018-03-12 08:16+0000\n" -"Last-Translator: Transifex Bot <>, 2026\n" -"Language-Team: Occitan (post 1500) (https://app.transifex.com/mate/teams/13566/oc/)\n" +"PO-Revision-Date: 2026-06-18 15:10+0000\n" +"Last-Translator: Quentin PAGÈS <[email protected]>\n" +"Language-Team: Occitan <https://translate.fedoraproject.org/projects/mate-" +"desktop/mate-settings-daemon/oc/>\n" +"Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: oc\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 2026.6.1\n" #: data/50-accessibility.xml.in:2 msgid "Accessibility" @@ -1101,8 +1103,8 @@ msgid "" "Automatically prunes thumbnail caches and other transient files, and warns " "about low disk space" msgstr "" -"Purga automaticament los caches de las vinhetas e autres fichièrs temporaris" -" e avertís en cas d'espaci de disc bas" +"Purga automaticament los caches de las miniaturas e autres fichièrs " +"temporaris e avertís en cas d'espaci de disc bas" #: plugins/housekeeping/msd-ldsm-dialog.c:61 msgid "Don't show any warnings again for this file system" @@ -1447,7 +1449,7 @@ msgstr "pilòt smartcard" #: plugins/smartcard/smartcard.mate-settings-plugin.desktop.in:5 msgid "Smartcard" -msgstr "Smartcard" +msgstr "Carta de piuse" #: plugins/smartcard/smartcard.mate-settings-plugin.desktop.in:6 msgid "Smartcard plugin" @@ -1,10 +1,6 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR MATE Desktop Environment team # This file is distributed under the same license as the mate-settings-daemon package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -# Translators: -# XRevan86, 2018 # Dmitriy Kulikov <[email protected]>, 2018 # Alex Puts, 2018 # Дмитрий Михирев, 2019 @@ -16,20 +12,28 @@ # Norbert X, 2020 # Павел Коваленко, 2021 # Olesya Gerasimenko <[email protected]>, 2021 -# +# "Sergey A." <[email protected]>, 2026. +# +# Translators: +# XRevan86, 2018 +# msgid "" msgstr "" "Project-Id-Version: mate-settings-daemon 1.26.0\n" "Report-Msgid-Bugs-To: https://mate-desktop.org/\n" "POT-Creation-Date: 2022-10-29 15:35+0200\n" -"PO-Revision-Date: 2018-03-12 08:16+0000\n" -"Last-Translator: Olesya Gerasimenko <[email protected]>, 2021\n" -"Language-Team: Russian (https://app.transifex.com/mate/teams/13566/ru/)\n" +"PO-Revision-Date: 2026-06-20 17:10+0000\n" +"Last-Translator: \"Sergey A.\" <[email protected]>\n" +"Language-Team: Russian <https://translate.fedoraproject.org/projects/mate-" +"desktop/applications/mate-settings-daemon/ru/>\n" +"Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: ru\n" -"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || " +"(n%100>=11 && n%100<=14)? 2 : 3);\n" +"X-Generator: Weblate 2026.6.1\n" #: data/50-accessibility.xml.in:2 msgid "Accessibility" @@ -501,7 +505,7 @@ msgstr "Активация этого модуля" #: data/org.mate.SettingsDaemon.plugins.xrdb.gschema.xml.in:6 #: data/org.mate.SettingsDaemon.plugins.xsettings.gschema.xml.in:6 msgid "Whether this plugin would be activated by mate-settings-daemon or not" -msgstr "Будет ли активирован этот модуль mate-settings-daemon или нет." +msgstr "Будет ли активирован этот модуль mate-settings-daemon или нет" #: data/org.mate.SettingsDaemon.plugins.a11y-keyboard.gschema.xml.in:10 #: data/org.mate.SettingsDaemon.plugins.a11y-settings.gschema.xml.in:10 @@ -570,7 +574,7 @@ msgid "" "issuing a subsequent warning." msgstr "" "Укажите процент свободного места, при достижении ниже которого нужно " -"отобразить повторное предупреждение" +"отобразить повторное предупреждение." #: data/org.mate.SettingsDaemon.plugins.housekeeping.gschema.xml.in:25 msgid "Free space no notify threshold" @@ -660,7 +664,7 @@ msgstr "Бесшумно отключить звук" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:46 msgid "Binding to mute the system volume quietly." -msgstr "Комбинация клавиш для бесшумного приглушения системной громкости. " +msgstr "Комбинация клавиш для бесшумного приглушения системной громкости." #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:50 msgid "Turn the volume down quietly" @@ -692,7 +696,7 @@ msgstr "Выключить" #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:66 msgid "Binding to shut down." -msgstr "Комбинация клавиш для выключения" +msgstr "Комбинация клавиш для выключения." #: data/org.mate.SettingsDaemon.plugins.media-keys.gschema.xml.in:70 msgid "Log out" @@ -1647,7 +1651,7 @@ msgstr "Открыть диалог конфигурации монитора (� #: plugins/xrandr/msd-xrandr-manager.c:2358 msgid "Configure display settings" -msgstr "Настроить параметры " +msgstr "Настроить параметры" #: plugins/xrandr/msd-xrandr-manager.c:2418 msgid "Could not apply the stored configuration for monitors" |
