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',