diff --git a/data/display.gschema.xml b/data/display.gschema.xml
new file mode 100644
index 00000000..405b702b
--- /dev/null
+++ b/data/display.gschema.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ {}
+ Preferred display layouts
+
+ Stores user-defined display layout profiles.
+ 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.
+
+
+
+
+
\ No newline at end of file
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..c3ad2c85
--- /dev/null
+++ b/src/Objects/MonitorLayoutManager.vala
@@ -0,0 +1,101 @@
+/*
+ * 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);
+ VariantDict? monitors = null;
+ if (layouts == null &&
+ layouts.lookup (layout_key, "a{sa{sv}}", out monitors) &&
+ monitors != null) {
+
+ foreach (var virtual_monitor in virtual_monitors) {
+ DisplayTransform transform;
+ int x, y;
+ if (monitors.lookup (virtual_monitor.id, "(iiu)", out x, out y, out transform)) {
+ virtual_monitor.x = x;
+ virtual_monitor.y = y;
+ virtual_monitor.transform = transform;
+ }
+ }
+
+ return;
+ }
+
+ // If no layout found, we save the current layout to use later
+ save_layout (virtual_monitors);
+ }
+
+ public void save_layout (Gee.LinkedList virtual_monitors) {
+ //Build the layout variant
+ var dict_builder = new VariantDict ();
+ foreach (var monitor in virtual_monitors) {
+ var props_builder = new VariantDict ();
+ // We save three properties for now, may want to save more later
+ props_builder.insert ("x", "v", new Variant.int32 (monitor.x));
+ props_builder.insert ("y", "v", new Variant.int32 (monitor.y));
+ props_builder.insert ("transform", "v", new Variant.uint32 (monitor.transform));
+ var props_variant = props_builder.end ();
+ debug (props_variant.print (true));
+ dict_builder.insert_value (monitor.id, props_variant);
+ }
+
+ var layout_variant = dict_builder.end ();
+
+ // Add or update the layouts setting
+ var save_key = get_layout_key (virtual_monitors);
+ var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY);
+ dict_builder = new VariantDict (layouts);
+ dict_builder.insert_value (save_key, layout_variant);
+
+ // Save to settings
+ settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, dict_builder.end ());
+ }
+
+ private string get_layout_key (Gee.LinkedList virtual_monitors) {
+ // Generate a unique key based on the virtual monitors' monitors hashes
+ 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;
+ }
+
+ private bool is_virtual_monitors_cloned (Gee.LinkedList virtual_monitors) {
+ foreach (var monitor in virtual_monitors) {
+ if (monitor.x != 0 || monitor.y != 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala
index 7e04b0e4..0c8f7ad1 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,13 @@ 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);
+ }
}
public void set_monitor_config () throws Error {
@@ -402,13 +419,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..6cfa3776 100644
--- a/src/Widgets/DisplaysOverlay.vala
+++ b/src/Widgets/DisplaysOverlay.vala
@@ -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',