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 5711df2d..a9e8839c 100644
--- a/meson.build
+++ b/meson.build
@@ -30,3 +30,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..547c986b
--- /dev/null
+++ b/src/Objects/MonitorLayoutManager.vala
@@ -0,0 +1,192 @@
+/*
+ * 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
+ return;
+ }
+
+ var layout_key = get_layout_key (virtual_monitors);
+ var layout = find_match_layout (layout_key);
+ var is_cloned = is_virtual_monitors_cloned (virtual_monitors);
+ var has_update = false;
+
+ if (layout != null) {
+ foreach (var virtual_monitor in virtual_monitors) {
+ var monitor_position = layout
+ .find_position_by_id (virtual_monitor.monitors[0].hash.to_string ());
+
+ if (monitor_position != null) {
+ if ((virtual_monitor.x != monitor_position.x
+ || virtual_monitor.y != monitor_position.y
+ || virtual_monitor.transform != monitor_position.transform)
+ && !is_cloned) {
+ has_update = true;
+ break;
+ }
+
+ virtual_monitor.x = monitor_position.x;
+ virtual_monitor.y = monitor_position.y;
+ virtual_monitor.transform = monitor_position.transform;
+ }
+ }
+ } else {
+ // If no layout found, we save the current layout to use later
+ save_layout (virtual_monitors);
+ }
+
+ if (has_update) {
+ // If the layout has been updated, save the new layout
+ save_layout (virtual_monitors);
+ }
+ }
+
+ public void save_layout (Gee.LinkedList virtual_monitors) {
+ var key = get_layout_key (virtual_monitors);
+ var layout_variant = build_layout_variant (virtual_monitors);
+
+ var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY);
+
+ add_or_update_layout (layouts, key, layout_variant);
+ }
+
+ public MonitorLayoutProfile? find_match_layout (string key) {
+ // Layouts format are 'a{sa{sa{sv}}}'
+ var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY);
+
+ if (layouts == null || layouts.n_children () == 0) {
+ return null; // No layouts saved
+ }
+
+ for (var i = 0; i < layouts.n_children (); i++) {
+ var layout = layouts.get_child_value (i);
+ var layout_key = layout.get_child_value (0).get_string ();
+ var monitors = layout.get_child_value (1);
+
+ if (layout_key != key) {
+ continue;
+ }
+
+ var virtual_monitor_position = new MonitorLayoutProfile (layout_key);
+
+ // Process the monitors in the layout
+ for (var j = 0; j < monitors.n_children (); j++) {
+ var monitor = monitors.get_child_value (j);
+ var monitor_props = monitor.get_child_value (1);
+
+ virtual_monitor_position.add_position (
+ monitor.get_child_value (0)
+ .get_string (), // id
+ monitor_props.get_child_value (0)
+ .get_child_value (1)
+ .get_child_value (0)
+ .get_int32 (), // x position
+ monitor_props.get_child_value (1)
+ .get_child_value (1)
+ .get_child_value (0)
+ .get_int32 (), // y position
+ monitor_props.get_child_value (2)
+ .get_child_value (1)
+ .get_child_value (0)
+ .get_int32 () // transform
+ );
+ }
+
+ return virtual_monitor_position;
+ }
+
+ return null;
+ }
+
+ 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 (monitor.hash.to_string ());
+ }
+ }
+
+ return key.str.hash ().to_string ();
+ }
+
+ private GLib.Variant build_layout_variant (Gee.LinkedList virtual_monitors) {
+ var dict_builder = new VariantBuilder (VariantType.DICTIONARY);
+
+ foreach (var monitor in virtual_monitors) {
+ var props_builder = new VariantBuilder (VariantType.DICTIONARY);
+ var key = monitor.monitors.get (0).hash.to_string ();
+
+ var coordinate_x_variant = new Variant.variant (new Variant.int32 (monitor.x));
+ var coordinate_y_variant = new Variant.variant (new Variant.int32 (monitor.y));
+ var transform_variant = new Variant.variant (new Variant.int32 (monitor.transform));
+
+ props_builder.add_value (new Variant.dict_entry ("x", coordinate_x_variant));
+ props_builder.add_value (new Variant.dict_entry ("y", coordinate_y_variant));
+ props_builder.add_value (new Variant.dict_entry ("transform", transform_variant));
+
+ var props_variant = props_builder.end ();
+
+ warning (props_variant.print (true));
+
+ dict_builder.add_value (new Variant.dict_entry (key, props_variant));
+ }
+
+ return dict_builder.end ();
+ }
+
+ private void add_or_update_layout (GLib.Variant layouts, string key, GLib.Variant layout_variant) {
+ var layout_builder = new VariantBuilder (VariantType.DICTIONARY);
+ bool found = false;
+
+ for (var i = 0; i < layouts.n_children (); i++) {
+ var layout = layouts.get_child_value (i);
+ var layout_key = layout.get_child_value (0).get_string ();
+
+ if (layout_key == key) {
+ // Update existing layout
+ layout_builder.add_value (new Variant.dict_entry (key, layout_variant));
+ found = true;
+ } else {
+ // Keep existing layout
+ layout_builder.add_value (new Variant.dict_entry (layout_key, layout));
+ }
+ }
+
+ if (!found) {
+ // Add new layout
+ layout_builder.add_value (new Variant.dict_entry (key, layout_variant));
+ }
+
+ settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, layout_builder.end ());
+ }
+
+ 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/MonitorLayoutProfile.vala b/src/Objects/MonitorLayoutProfile.vala
new file mode 100644
index 00000000..1c8822c4
--- /dev/null
+++ b/src/Objects/MonitorLayoutProfile.vala
@@ -0,0 +1,49 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * SPDX-FileCopyrightText: 2025 elementary, Inc.
+ *
+ * Authored by: Leonardo Lemos
+ */
+
+public class Display.MonitorLayoutProfile : GLib.Object {
+ public class MonitorPosition : GLib.Object {
+ public string id { get; set; }
+ public int x { get; set; }
+ public int y { get; set; }
+ public DisplayTransform transform { get; set; }
+
+ public MonitorPosition (string id, int x, int y, DisplayTransform transform) {
+ Object (id: id, x: x, y: y, transform: transform);
+ }
+ }
+
+ public string id { get; set; }
+ private List _positions;
+
+ public unowned List positions {
+ get {
+ return _positions;
+ }
+ }
+
+ public MonitorLayoutProfile (string id) {
+ Object (id: id);
+ }
+
+ construct {
+ _positions = new List ();
+ }
+
+ public void add_position (string id, int x, int y, DisplayTransform transform) {
+ _positions.append (new MonitorPosition (id, x, y, transform));
+ }
+
+ public MonitorPosition? find_position_by_id (string id) {
+ foreach (var position in _positions) {
+ if (position.id == id) {
+ return position;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala
index 7e04b0e4..de91eac5 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,11 @@ 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);
}
}
+
+ layout_manager.arrange_monitors (virtual_monitors);
}
public void set_monitor_config () throws Error {
@@ -402,13 +417,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 d76bb41c..f1657964 100644
--- a/src/Widgets/DisplaysOverlay.vala
+++ b/src/Widgets/DisplaysOverlay.vala
@@ -87,7 +87,7 @@ public class Display.DisplaysOverlay : Gtk.Box {
add_controller (drag_gesture);
monitor_manager = Display.MonitorManager.get_default ();
- monitor_manager.notify["virtual-monitor-number"].connect (() => rescan_displays ());
+ monitor_manager.monitors_changed.connect (() => rescan_displays ());
rescan_displays ();
overlay.get_child_position.connect (get_child_position);
@@ -199,6 +199,7 @@ public class Display.DisplaysOverlay : Gtk.Box {
add_output (virtual_monitor);
}
+ show_windows ();
change_active_displays_sensitivity ();
calculate_ratio ();
scanning = false;
@@ -351,10 +352,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 94aa9310..6de3dd7f 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -10,6 +10,8 @@ plug_files = files(
'Objects/MonitorMode.vala',
'Objects/MonitorManager.vala',
'Objects/Monitor.vala',
+ 'Objects/MonitorLayoutManager.vala',
+ 'Objects/MonitorLayoutProfile.vala',
'Views/NightLightView.vala',
'Views/DisplaysView.vala',
'Views' / 'FiltersView.vala',