diff --git a/data/display.gschema.xml b/data/display.gschema.xml new file mode 100644 index 00000000..dc4c1d95 --- /dev/null +++ b/data/display.gschema.xml @@ -0,0 +1,19 @@ + + + + + {} + Preferred display layouts + + Each profile contains a unique identifier and a list of monitors, with their respective position (x, y) and other properties such as transformation (e.g., rotation). + This allows the system to restore or suggest preferred monitor arrangements and settings when displays are connected or configurations change. + The expanded variant type is "a{sa{sa{sv}}}" + Setting: Dictionary of layouts - key is based on a combination of monitor ids + Layout: Dictionary of monitors - key is monitor identifier + Monitor: Dictionary of property values - key is property name + Property keys: "x", "y", "transform", "primary", "is-active" + + + + + diff --git a/data/meson.build b/data/meson.build index bb32ce31..592d77a7 100644 --- a/data/meson.build +++ b/data/meson.build @@ -11,3 +11,9 @@ gresource = gnome.compile_resources( 'gresource', 'display.gresource.xml' ) + +install_data( + 'display.gschema.xml', + install_dir: datadir / 'glib-2.0' / 'schemas', + rename: 'io.elementary.settings.display.gschema.xml' +) \ No newline at end of file diff --git a/meson.build b/meson.build index 108fe1e8..cc921e71 100644 --- a/meson.build +++ b/meson.build @@ -31,3 +31,5 @@ config_file = configure_file( subdir('data') subdir('src') subdir('po') + +gnome.post_install(glib_compile_schemas: true) \ No newline at end of file diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala new file mode 100644 index 00000000..f7cdb687 --- /dev/null +++ b/src/Objects/MonitorLayoutManager.vala @@ -0,0 +1,107 @@ +/* + * SPDX-License-Identifier: GPL-2.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. + * + * Authored by: Leonardo Lemos + */ + +public class Display.MonitorLayoutManager : GLib.Object { + private Settings settings; + + private const string PREFERRED_MONITOR_LAYOUTS_KEY = "preferred-display-layouts"; + + public MonitorLayoutManager () { + Object (); + } + + construct { + settings = new Settings ("io.elementary.settings.display"); + } + + public void arrange_monitors (Gee.LinkedList virtual_monitors) { + if (virtual_monitors.size == 1) { + // If there's only one monitor, no need to arrange + // Cloned monitors only have one virtual monitor so will return here + return; + } + + var layout_key = get_layout_key (virtual_monitors); + // Layouts format are 'a{sa{sa{sv}}}' + var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); + Variant? monitors = null; + if (layouts != null) { + monitors = layouts.lookup_value (layout_key, VariantType.VARDICT); + foreach (var virtual_monitor in virtual_monitors) { + Variant? props = monitors.lookup_value (virtual_monitor.id, VariantType.VARDICT); + if (props != null) { + int32 x = 0, y = 0; + uint32 t = 0; + bool p = false, e = false; + if (props.lookup ("x", "i", out x) && + props.lookup ("y", "i", out y) && + props.lookup ("transform", "u", out t) && + props.lookup ("primary", "b", out p) && + props.lookup ("enabled", "b", out e)) { + + virtual_monitor.x = x; + virtual_monitor.y = y; + virtual_monitor.transform = t; + virtual_monitor.primary = p; + virtual_monitor.is_active = e; + } else { + warning ("property setting missing for monitor %s", virtual_monitor.get_display_name ()); + } + } else { + warning ("no property dictionary found for monitor.id %s", virtual_monitor.get_display_name ()); + } + } + + return; + } else { + warning ("layout key %s not found", layout_key); + } + + // If no layout found, we save the current layout to use later + save_layout (virtual_monitors); + } + + public void save_layout (Gee.LinkedList virtual_monitors) { + var save_key = get_layout_key (virtual_monitors); + + var monitor_dict = new VariantDict (); + foreach (var monitor in virtual_monitors) { + var props_dict = new VariantDict (); + props_dict.insert_value ("x", new Variant.int32 (monitor.x)); + props_dict.insert_value ("y", new Variant.int32 (monitor.y)); + props_dict.insert_value ("transform", new Variant.uint32 (monitor.transform)); + props_dict.insert_value ("primary", new Variant.boolean (monitor.primary)); + props_dict.insert_value ("enabled", new Variant.boolean (monitor.is_active)); + monitor_dict.insert_value (monitor.id, props_dict.end ()); + } + + // Add or update the layouts setting + var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); + var layouts_dict = new VariantDict (layouts); + layouts_dict.insert_value (save_key, monitor_dict.end ()); + + // Save to settings + //NOTE The variant yielded by VariantDict.end () always has type "a{sv}" + settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, layouts_dict.end ()); + } + + private string get_layout_key (Gee.LinkedList virtual_monitors) { + // Generate a unique key based on the virtual monitors' monitors hashes + //NOTE The key depends on the order of the list which will change depending on whether monitors are + // active or not (and possibly on the order they were connected). + //TODO Consider whether a more controlled key is needed + var key = new StringBuilder (); + + foreach (var virtual_monitor in virtual_monitors) { + foreach (var monitor in virtual_monitor.monitors) { + key.append (virtual_monitor.id); + } + } + + return key.str; + } +} diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala index 7e04b0e4..57a5b2c5 100644 --- a/src/Objects/MonitorManager.vala +++ b/src/Objects/MonitorManager.vala @@ -46,8 +46,11 @@ public class Display.MonitorManager : GLib.Object { } } + public signal void monitors_changed (); + private MutterDisplayConfigInterface iface; private uint current_serial; + private MonitorLayoutManager layout_manager; private static MonitorManager monitor_manager; public static unowned MonitorManager get_default () { @@ -65,9 +68,16 @@ public class Display.MonitorManager : GLib.Object { construct { monitors = new Gee.LinkedList (); virtual_monitors = new Gee.LinkedList (); + layout_manager = new MonitorLayoutManager (); try { - iface = Bus.get_proxy_sync (BusType.SESSION, "org.gnome.Mutter.DisplayConfig", "/org/gnome/Mutter/DisplayConfig"); - iface.monitors_changed.connect (get_monitor_config); + iface = Bus.get_proxy_sync ( + BusType.SESSION, + "org.gnome.Mutter.DisplayConfig", + "/org/gnome/Mutter/DisplayConfig"); + iface.monitors_changed.connect (() => { + get_monitor_config (); + monitors_changed (); + }); } catch (Error e) { critical (e.message); } @@ -78,7 +88,10 @@ public class Display.MonitorManager : GLib.Object { MutterReadLogicalMonitor[] mutter_logical_monitors; GLib.HashTable properties; try { - iface.get_current_state (out current_serial, out mutter_monitors, out mutter_logical_monitors, out properties); + iface.get_current_state (out current_serial, + out mutter_monitors, + out mutter_logical_monitors, + out properties); } catch (Error e) { critical (e.message); } @@ -217,7 +230,7 @@ public class Display.MonitorManager : GLib.Object { virtual_monitor.scale = mutter_logical_monitor.scale; virtual_monitor.transform = mutter_logical_monitor.transform; virtual_monitor.primary = mutter_logical_monitor.primary; - add_virtual_monitor (virtual_monitor); + virtual_monitors.add (virtual_monitor); } // Look for any monitors that aren't part of a virtual monitor (hence disabled) @@ -237,9 +250,15 @@ public class Display.MonitorManager : GLib.Object { virtual_monitor.primary = false; virtual_monitor.monitors.add (monitor); virtual_monitor.scale = virtual_monitors[0].scale; - add_virtual_monitor (virtual_monitor); + virtual_monitors.add (virtual_monitor); } } + + if (!is_mirrored) { + layout_manager.save_layout (virtual_monitors); + } + + layout_manager.arrange_monitors (virtual_monitors); } public void set_monitor_config () throws Error { @@ -402,13 +421,10 @@ public class Display.MonitorManager : GLib.Object { virtual_monitors.clear (); virtual_monitors.add_all (new_virtual_monitors); - notify_property ("virtual-monitor-number"); - notify_property ("is-mirrored"); - } + layout_manager.arrange_monitors (virtual_monitors); - private void add_virtual_monitor (Display.VirtualMonitor virtual_monitor) { - virtual_monitors.add (virtual_monitor); notify_property ("virtual-monitor-number"); + notify_property ("is-mirrored"); } private VirtualMonitor? get_virtual_monitor_by_id (string id) { diff --git a/src/Widgets/DisplaysOverlay.vala b/src/Widgets/DisplaysOverlay.vala index 3874994d..1ca4fc1e 100644 --- a/src/Widgets/DisplaysOverlay.vala +++ b/src/Widgets/DisplaysOverlay.vala @@ -97,6 +97,7 @@ public class Display.DisplaysOverlay : Gtk.Box { gala_dbus = GLib.Bus.get_proxy.end (res); monitor_manager = Display.MonitorManager.get_default (); monitor_manager.notify["virtual-monitor-number"].connect (() => rescan_displays ()); + monitor_manager.monitors_changed.connect (() => rescan_displays ()); rescan_displays (); } catch (GLib.Error e) { critical (e.message); @@ -185,7 +186,6 @@ public class Display.DisplaysOverlay : Gtk.Box { public void rescan_displays () { scanning = true; - display_widgets.@foreach ((display_widget) => { overlay.remove_overlay (display_widget); display_widget.destroy (); @@ -198,6 +198,7 @@ public class Display.DisplaysOverlay : Gtk.Box { add_output (virtual_monitor); } + show_windows (); change_active_displays_sensitivity (); calculate_ratio (); scanning = false; @@ -350,10 +351,6 @@ public class Display.DisplaysOverlay : Gtk.Box { check_configuration_change (); calculate_ratio (); }); - - if (!monitor_manager.is_mirrored && virtual_monitor.is_active) { - show_windows (); - } } private void set_as_primary (Display.VirtualMonitor new_primary) { diff --git a/src/meson.build b/src/meson.build index fc6df44b..6ed4f693 100644 --- a/src/meson.build +++ b/src/meson.build @@ -10,6 +10,7 @@ plug_files = files( 'Objects/MonitorMode.vala', 'Objects/MonitorManager.vala', 'Objects/Monitor.vala', + 'Objects/MonitorLayoutManager.vala', 'Views/NightLightView.vala', 'Views/DisplaysView.vala', 'Views' / 'FiltersView.vala',