From 1ea364c83051e9453c6f0eca47f6f1e3b1a38933 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Fri, 30 May 2025 20:15:52 -0300 Subject: [PATCH 01/30] Add gschema --- data/display.gschema.xml | 15 +++++++++++++++ data/meson.build | 6 ++++++ meson.build | 2 ++ 3 files changed, 23 insertions(+) create mode 100644 data/display.gschema.xml 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 cd420ed8..352622fe 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 From 762da1f86f7f5d8c53cf6a141dddcda2688578f0 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Fri, 30 May 2025 20:16:32 -0300 Subject: [PATCH 02/30] Implement MonitorLayoutManager --- src/Objects/MonitorLayoutManager.vala | 172 ++++++++++++++++++++++++++ src/Objects/MonitorLayoutProfile.vala | 48 +++++++ src/meson.build | 2 + 3 files changed, 222 insertions(+) create mode 100644 src/Objects/MonitorLayoutManager.vala create mode 100644 src/Objects/MonitorLayoutProfile.vala diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala new file mode 100644 index 00000000..a701a7f7 --- /dev/null +++ b/src/Objects/MonitorLayoutManager.vala @@ -0,0 +1,172 @@ +/* + * 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_layout = layout.find_position_by_id (virtual_monitor.monitors[0].hash.to_string ()); + + if (monitor_layout != null) { + if ((virtual_monitor.x != monitor_layout.x || virtual_monitor.y != monitor_layout.y) && !is_cloned) { + has_update = true; + break; + } + + virtual_monitor.x = monitor_layout.x; + virtual_monitor.y = monitor_layout.y; + } + } + } else { + // If no layout found, we save the current layout to use later + save_layout (virtual_monitors); + } + + if (has_update) { + 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 is 'a{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_layout = 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); + + warning (monitor_props.get_child_value (0).get_type ().dup_string ()); + warning (monitor_props.get_child_value (0).get_child_value (1).get_child_value (0).get_type ().dup_string ()); + + + virtual_monitor_layout.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 + ); + } + + return virtual_monitor_layout; + } + + 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(); + + props_builder.add_value (new Variant.dict_entry ("x", new Variant.variant(new Variant.int32 (monitor.x)))); + props_builder.add_value (new Variant.dict_entry ("y", new Variant.variant(new Variant.int32 (monitor.y)))); + + dict_builder.add_value (new Variant.dict_entry (key, props_builder)); + } + + 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; + } +} \ No newline at end of file diff --git a/src/Objects/MonitorLayoutProfile.vala b/src/Objects/MonitorLayoutProfile.vala new file mode 100644 index 00000000..543f0f8b --- /dev/null +++ b/src/Objects/MonitorLayoutProfile.vala @@ -0,0 +1,48 @@ +/* + * 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 MonitorPosition (string id, int x, int y) { + Object (id: id, x: x, y: y); + } + } + + 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) { + _positions.append (new MonitorPosition (id, x, y)); + } + + public MonitorPosition? find_position_by_id (string id) { + foreach (var position in _positions) { + if (position.id == id) { + return position; + } + } + return null; + } +} \ No newline at end of file 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', From dd6c098b31b28c48c47e0d67779a0ad238405d05 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Fri, 30 May 2025 20:16:52 -0300 Subject: [PATCH 03/30] Use MonitorLayoutManager --- src/Objects/MonitorManager.vala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala index 7e04b0e4..98f9e21d 100644 --- a/src/Objects/MonitorManager.vala +++ b/src/Objects/MonitorManager.vala @@ -48,6 +48,7 @@ public class Display.MonitorManager : GLib.Object { private MutterDisplayConfigInterface iface; private uint current_serial; + private MonitorLayoutManager layout_manager; private static MonitorManager monitor_manager; public static unowned MonitorManager get_default () { @@ -65,6 +66,7 @@ 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); @@ -83,6 +85,8 @@ public class Display.MonitorManager : GLib.Object { critical (e.message); } + var monitor_number = virtual_monitors.size; + // Clear all monitors and virtual monitors before re-adding them if needed monitors.clear (); virtual_monitors.clear (); @@ -239,6 +243,16 @@ public class Display.MonitorManager : GLib.Object { virtual_monitor.scale = virtual_monitors[0].scale; add_virtual_monitor (virtual_monitor); } + + + } + + if (monitor_number != virtual_monitors.size) { + notify_property ("virtual-monitor-number"); + } + + if (virtual_monitors.size >= 2) { + layout_manager.arrange_monitors (virtual_monitors); } } @@ -402,6 +416,8 @@ public class Display.MonitorManager : GLib.Object { virtual_monitors.clear (); virtual_monitors.add_all (new_virtual_monitors); + layout_manager.arrange_monitors (virtual_monitors); + notify_property ("virtual-monitor-number"); notify_property ("is-mirrored"); } From b2c0ff18210ff07e041cc844d6ae9812a990d92f Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Fri, 30 May 2025 20:27:00 -0300 Subject: [PATCH 04/30] Fix build --- src/Objects/MonitorLayoutManager.vala | 4 ++-- src/Objects/MonitorLayoutProfile.vala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index a701a7f7..93834265 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -2,7 +2,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later * SPDX-FileCopyrightText: 2025 elementary, Inc. * - * Authored by: Leonardo Lemos + * Authored by: Leonardo Lemos */ public class Display.MonitorLayoutManager : GLib.Object { @@ -128,7 +128,7 @@ public class Display.MonitorLayoutManager : GLib.Object { props_builder.add_value (new Variant.dict_entry ("x", new Variant.variant(new Variant.int32 (monitor.x)))); props_builder.add_value (new Variant.dict_entry ("y", new Variant.variant(new Variant.int32 (monitor.y)))); - dict_builder.add_value (new Variant.dict_entry (key, props_builder)); + dict_builder.add_value (new Variant.dict_entry (key, props_builder.end ())); } return dict_builder.end (); diff --git a/src/Objects/MonitorLayoutProfile.vala b/src/Objects/MonitorLayoutProfile.vala index 543f0f8b..7791ac22 100644 --- a/src/Objects/MonitorLayoutProfile.vala +++ b/src/Objects/MonitorLayoutProfile.vala @@ -2,7 +2,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later * SPDX-FileCopyrightText: 2025 elementary, Inc. * - * Authored by: Leonardo Lemos + * Authored by: Leonardo Lemos */ public class Display.MonitorLayoutProfile : GLib.Object { From 441b2800fcb9bad09eeba90c8f9d02161c6d217e Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Sat, 31 May 2025 18:28:45 -0300 Subject: [PATCH 05/30] Notify monitor number change only once --- src/Objects/MonitorManager.vala | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala index 98f9e21d..1708e980 100644 --- a/src/Objects/MonitorManager.vala +++ b/src/Objects/MonitorManager.vala @@ -221,7 +221,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) @@ -241,19 +241,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 (monitor_number != virtual_monitors.size) { notify_property ("virtual-monitor-number"); } - if (virtual_monitors.size >= 2) { - layout_manager.arrange_monitors (virtual_monitors); - } + layout_manager.arrange_monitors (virtual_monitors); } public void set_monitor_config () throws Error { @@ -422,11 +418,6 @@ public class Display.MonitorManager : GLib.Object { notify_property ("is-mirrored"); } - private void add_virtual_monitor (Display.VirtualMonitor virtual_monitor) { - virtual_monitors.add (virtual_monitor); - notify_property ("virtual-monitor-number"); - } - private VirtualMonitor? get_virtual_monitor_by_id (string id) { foreach (var vm in virtual_monitors) { if (vm.id == id) { From 12a1a02dba3365eb968f19a42d99190f6408cf85 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Sat, 31 May 2025 18:29:04 -0300 Subject: [PATCH 06/30] Show display label only once --- src/Widgets/DisplaysOverlay.vala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Widgets/DisplaysOverlay.vala b/src/Widgets/DisplaysOverlay.vala index d76bb41c..b429c468 100644 --- a/src/Widgets/DisplaysOverlay.vala +++ b/src/Widgets/DisplaysOverlay.vala @@ -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) { From f8605150ac38418143789b08041eb3708094fe34 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Sat, 31 May 2025 19:18:13 -0300 Subject: [PATCH 07/30] Rescan Displays only if monitors changed --- src/Objects/MonitorManager.vala | 21 ++++++++++++++------- src/Widgets/DisplaysOverlay.vala | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala index 1708e980..7947f1bd 100644 --- a/src/Objects/MonitorManager.vala +++ b/src/Objects/MonitorManager.vala @@ -46,6 +46,8 @@ public class Display.MonitorManager : GLib.Object { } } + public signal void monitors_changed (); + private MutterDisplayConfigInterface iface; private uint current_serial; private MonitorLayoutManager layout_manager; @@ -68,8 +70,14 @@ public class Display.MonitorManager : GLib.Object { 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); } @@ -80,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); } @@ -245,10 +256,6 @@ public class Display.MonitorManager : GLib.Object { } } - if (monitor_number != virtual_monitors.size) { - notify_property ("virtual-monitor-number"); - } - layout_manager.arrange_monitors (virtual_monitors); } diff --git a/src/Widgets/DisplaysOverlay.vala b/src/Widgets/DisplaysOverlay.vala index b429c468..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); From 671246144b7be3c346f41ad98e7d55e80d95b15c Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Sat, 31 May 2025 19:18:40 -0300 Subject: [PATCH 08/30] Add display transformation saving --- src/Objects/MonitorLayoutManager.vala | 86 +++++++++++++++++---------- src/Objects/MonitorLayoutProfile.vala | 11 ++-- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 93834265..547c986b 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -9,7 +9,7 @@ public class Display.MonitorLayoutManager : GLib.Object { private Settings settings; private const string PREFERRED_MONITOR_LAYOUTS_KEY = "preferred-display-layouts"; - + public MonitorLayoutManager () { Object (); } @@ -23,25 +23,29 @@ public class Display.MonitorLayoutManager : GLib.Object { // 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_layout = layout.find_position_by_id (virtual_monitor.monitors[0].hash.to_string ()); - - if (monitor_layout != null) { - if ((virtual_monitor.x != monitor_layout.x || virtual_monitor.y != monitor_layout.y) && !is_cloned) { + 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_layout.x; - virtual_monitor.y = monitor_layout.y; + virtual_monitor.x = monitor_position.x; + virtual_monitor.y = monitor_position.y; + virtual_monitor.transform = monitor_position.transform; } } } else { @@ -50,6 +54,7 @@ public class Display.MonitorLayoutManager : GLib.Object { } if (has_update) { + // If the layout has been updated, save the new layout save_layout (virtual_monitors); } } @@ -61,11 +66,10 @@ public class Display.MonitorLayoutManager : GLib.Object { 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 is 'a{sa{sv}}' + // Layouts format are 'a{sa{sa{sv}}}' var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); if (layouts == null || layouts.n_children () == 0) { @@ -81,25 +85,32 @@ public class Display.MonitorLayoutManager : GLib.Object { continue; } - var virtual_monitor_layout = new MonitorLayoutProfile (layout_key); + 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); - warning (monitor_props.get_child_value (0).get_type ().dup_string ()); - warning (monitor_props.get_child_value (0).get_child_value (1).get_child_value (0).get_type ().dup_string ()); - - - virtual_monitor_layout.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 - ); + 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_layout; + return virtual_monitor_position; } return null; @@ -119,23 +130,32 @@ public class Display.MonitorLayoutManager : GLib.Object { } private GLib.Variant build_layout_variant (Gee.LinkedList virtual_monitors) { - var dict_builder = new VariantBuilder(VariantType.DICTIONARY); + 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(); - - props_builder.add_value (new Variant.dict_entry ("x", new Variant.variant(new Variant.int32 (monitor.x)))); - props_builder.add_value (new Variant.dict_entry ("y", new Variant.variant(new Variant.int32 (monitor.y)))); - - dict_builder.add_value (new Variant.dict_entry (key, props_builder.end ())); + 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); + var layout_builder = new VariantBuilder (VariantType.DICTIONARY); bool found = false; for (var i = 0; i < layouts.n_children (); i++) { @@ -157,7 +177,7 @@ public class Display.MonitorLayoutManager : GLib.Object { layout_builder.add_value (new Variant.dict_entry (key, layout_variant)); } - settings.set_value(PREFERRED_MONITOR_LAYOUTS_KEY, layout_builder.end ()); + settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, layout_builder.end ()); } private bool is_virtual_monitors_cloned (Gee.LinkedList virtual_monitors) { @@ -169,4 +189,4 @@ public class Display.MonitorLayoutManager : GLib.Object { return true; } -} \ No newline at end of file +} diff --git a/src/Objects/MonitorLayoutProfile.vala b/src/Objects/MonitorLayoutProfile.vala index 7791ac22..1c8822c4 100644 --- a/src/Objects/MonitorLayoutProfile.vala +++ b/src/Objects/MonitorLayoutProfile.vala @@ -10,9 +10,10 @@ public class Display.MonitorLayoutProfile : 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) { - Object (id: id, x: x, y: y); + public MonitorPosition (string id, int x, int y, DisplayTransform transform) { + Object (id: id, x: x, y: y, transform: transform); } } @@ -33,8 +34,8 @@ public class Display.MonitorLayoutProfile : GLib.Object { _positions = new List (); } - public void add_position (string id, int x, int y) { - _positions.append (new MonitorPosition (id, x, y)); + 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) { @@ -45,4 +46,4 @@ public class Display.MonitorLayoutProfile : GLib.Object { } return null; } -} \ No newline at end of file +} From 756c165047d587d79e1a05f12f9642dc86872820 Mon Sep 17 00:00:00 2001 From: Leonardo Lemos Date: Sun, 1 Jun 2025 19:11:36 -0300 Subject: [PATCH 09/30] Removed unused variable --- src/Objects/MonitorManager.vala | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala index 7947f1bd..de91eac5 100644 --- a/src/Objects/MonitorManager.vala +++ b/src/Objects/MonitorManager.vala @@ -96,8 +96,6 @@ public class Display.MonitorManager : GLib.Object { critical (e.message); } - var monitor_number = virtual_monitors.size; - // Clear all monitors and virtual monitors before re-adding them if needed monitors.clear (); virtual_monitors.clear (); From 3690b20a4c10774c387eb5bd39bacc6e707ccfea Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 11:05:21 +0100 Subject: [PATCH 10/30] Drop unnecessary check --- src/Objects/MonitorLayoutManager.vala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 547c986b..8243d93e 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -21,24 +21,26 @@ public class Display.MonitorLayoutManager : GLib.Object { 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); var layout = find_match_layout (layout_key); - var is_cloned = is_virtual_monitors_cloned (virtual_monitors); + // 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 ()); + 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) { + || virtual_monitor.transform != monitor_position.transform)) { + has_update = true; break; } From e43a21c2580393b6c88b04bde5ec8b108890a9f9 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 11:12:28 +0100 Subject: [PATCH 11/30] Reduce scope of some functions --- src/Objects/MonitorLayoutManager.vala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 8243d93e..ff93c783 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -27,7 +27,6 @@ public class Display.MonitorLayoutManager : GLib.Object { 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) { @@ -61,7 +60,7 @@ public class Display.MonitorLayoutManager : GLib.Object { } } - public void save_layout (Gee.LinkedList virtual_monitors) { + private void save_layout (Gee.LinkedList virtual_monitors) { var key = get_layout_key (virtual_monitors); var layout_variant = build_layout_variant (virtual_monitors); @@ -70,7 +69,7 @@ public class Display.MonitorLayoutManager : GLib.Object { add_or_update_layout (layouts, key, layout_variant); } - public MonitorLayoutProfile? find_match_layout (string key) { + private MonitorLayoutProfile? find_match_layout (string key) { // Layouts format are 'a{sa{sa{sv}}}' var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); From a9f5d711c58c3473d937bb565bf067f0c1ebb4f8 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 11:56:56 +0100 Subject: [PATCH 12/30] Simplify arrange_monitors() & get_layout_key() --- src/Objects/MonitorLayoutManager.vala | 71 ++++----------------------- 1 file changed, 10 insertions(+), 61 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index ff93c783..60b07eb0 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -27,37 +27,21 @@ public class Display.MonitorLayoutManager : GLib.Object { var layout_key = get_layout_key (virtual_monitors); var layout = find_match_layout (layout_key); - 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)) { - - has_update = true; - break; - } - - virtual_monitor.x = monitor_position.x; - virtual_monitor.y = monitor_position.y; - virtual_monitor.transform = monitor_position.transform; + DisplayTransform transform; + int x, y; + if (layout.lookup (virtual_monitor.id, "(iiu)", out x, out y, out transform)) { + virtual_monitor.x = x; + virtual_monitor.y = y; + virtual_monitor.transform = 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); - } } private void save_layout (Gee.LinkedList virtual_monitors) { @@ -69,7 +53,7 @@ public class Display.MonitorLayoutManager : GLib.Object { add_or_update_layout (layouts, key, layout_variant); } - private MonitorLayoutProfile? find_match_layout (string key) { + private VariantDict? find_match_layout (string key) { // Layouts format are 'a{sa{sa{sv}}}' var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); @@ -77,44 +61,9 @@ public class Display.MonitorLayoutManager : GLib.Object { 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; + VariantDict? monitors = null; + layouts.lookup (key, "a{sa{sv}}", out monitors); + return monitors; } private string get_layout_key (Gee.LinkedList virtual_monitors) { From c5071c62e0258feea88a0e6bbfe2eff9ca51cc56 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 11:59:33 +0100 Subject: [PATCH 13/30] Lose unused object --- src/Objects/MonitorLayoutProfile.vala | 49 --------------------------- src/meson.build | 1 - 2 files changed, 50 deletions(-) delete mode 100644 src/Objects/MonitorLayoutProfile.vala diff --git a/src/Objects/MonitorLayoutProfile.vala b/src/Objects/MonitorLayoutProfile.vala deleted file mode 100644 index 1c8822c4..00000000 --- a/src/Objects/MonitorLayoutProfile.vala +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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/meson.build b/src/meson.build index 8ed4bba3..6ed4f693 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,7 +11,6 @@ plug_files = files( 'Objects/MonitorManager.vala', 'Objects/Monitor.vala', 'Objects/MonitorLayoutManager.vala', - 'Objects/MonitorLayoutProfile.vala', 'Views/NightLightView.vala', 'Views/DisplaysView.vala', 'Views' / 'FiltersView.vala', From 0e85221e618a0d0de1f1bd92df5078b6317f9625 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 12:12:58 +0100 Subject: [PATCH 14/30] Inline simplified find_match_layout() --- src/Objects/MonitorLayoutManager.vala | 31 +++++++++++---------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 60b07eb0..bcafc6f3 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -26,22 +26,28 @@ public class Display.MonitorLayoutManager : GLib.Object { } var layout_key = get_layout_key (virtual_monitors); - var layout = find_match_layout (layout_key); + // 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) { - if (layout != null) { foreach (var virtual_monitor in virtual_monitors) { DisplayTransform transform; int x, y; - if (layout.lookup (virtual_monitor.id, "(iiu)", out x, out y, out transform)) { + 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; } } - } else { - // If no layout found, we save the current layout to use later - save_layout (virtual_monitors); + + return; } + + // If no layout found, we save the current layout to use later + save_layout (virtual_monitors); } private void save_layout (Gee.LinkedList virtual_monitors) { @@ -53,19 +59,6 @@ public class Display.MonitorLayoutManager : GLib.Object { add_or_update_layout (layouts, key, layout_variant); } - private VariantDict? 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 - } - - VariantDict? monitors = null; - layouts.lookup (key, "a{sa{sv}}", out monitors); - return monitors; - } - private string get_layout_key (Gee.LinkedList virtual_monitors) { // Generate a unique key based on the virtual monitors' monitors hashes var key = new StringBuilder (); From a90c35983a20bc37725f01665b563084d93f6288 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 12:22:15 +0100 Subject: [PATCH 15/30] Drop hash for key --- src/Objects/MonitorLayoutManager.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index bcafc6f3..f4e92d42 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -65,11 +65,11 @@ public class Display.MonitorLayoutManager : GLib.Object { foreach (var virtual_monitor in virtual_monitors) { foreach (var monitor in virtual_monitor.monitors) { - key.append (monitor.hash.to_string ()); + key.append (virtual_monitor.id); } } - return key.str.hash ().to_string (); + return key.str; } private GLib.Variant build_layout_variant (Gee.LinkedList virtual_monitors) { From 4442fa32ecbf57a6797edf34b90420da3c7b6293 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 12:37:05 +0100 Subject: [PATCH 16/30] Inline build_layout_variant() --- src/Objects/MonitorLayoutManager.vala | 45 +++++++++++++-------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index f4e92d42..f6d67b0d 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -51,33 +51,13 @@ public class Display.MonitorLayoutManager : GLib.Object { } private 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); - } - - 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 GLib.Variant build_layout_variant (Gee.LinkedList virtual_monitors) { + //Build the layout variant 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 monitor_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)); @@ -91,10 +71,27 @@ public class Display.MonitorLayoutManager : GLib.Object { warning (props_variant.print (true)); - dict_builder.add_value (new Variant.dict_entry (key, props_variant)); + dict_builder.add_value (new Variant.dict_entry (monitor_key, props_variant)); + } + + var layout_variant = dict_builder.end (); + // Add or update the layouts setting + var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); + var layout_key = get_layout_key (virtual_monitors); + add_or_update_layout (layouts, layout_key, layout_variant); + } + + 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 dict_builder.end (); + return key.str; } private void add_or_update_layout (GLib.Variant layouts, string key, GLib.Variant layout_variant) { From 844dc0453585dcffe90e968b1b4b0a99e8e4f36b Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 12:51:35 +0100 Subject: [PATCH 17/30] Inline add_or_update_layout() --- src/Objects/MonitorLayoutManager.vala | 76 +++++++++++++++++---------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index f6d67b0d..51137c77 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -51,8 +51,6 @@ public class Display.MonitorLayoutManager : GLib.Object { } private void save_layout (Gee.LinkedList virtual_monitors) { - - //Build the layout variant var dict_builder = new VariantBuilder (VariantType.DICTIONARY); foreach (var monitor in virtual_monitors) { @@ -75,51 +73,73 @@ public class Display.MonitorLayoutManager : GLib.Object { } var layout_variant = dict_builder.end (); - // Add or update the layouts setting - var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); - var layout_key = get_layout_key (virtual_monitors); - add_or_update_layout (layouts, layout_key, layout_variant); - } - - 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 void add_or_update_layout (GLib.Variant layouts, string key, GLib.Variant layout_variant) { - var layout_builder = new VariantBuilder (VariantType.DICTIONARY); + // Add or update the layouts setting + var save_key = get_layout_key (virtual_monitors); + dict_builder = new VariantBuilder (VariantType.DICTIONARY); bool found = false; - + var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); 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) { + if (layout_key == save_key) { // Update existing layout - layout_builder.add_value (new Variant.dict_entry (key, layout_variant)); + dict_builder.add_value (new Variant.dict_entry (save_key, layout_variant)); found = true; } else { // Keep existing layout - layout_builder.add_value (new Variant.dict_entry (layout_key, layout)); + dict_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)); + dict_builder.add_value (new Variant.dict_entry (save_key, layout_variant)); + } + + 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); + } } - settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, layout_builder.end ()); + return key.str; } + // 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) { From 11ebe2cc1354cd515c45cd60909814e4b1bb2ad6 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 13:18:08 +0100 Subject: [PATCH 18/30] Simplify save layout using VariantDict --- src/Objects/MonitorLayoutManager.vala | 73 ++++----------------------- 1 file changed, 11 insertions(+), 62 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 51137c77..efa3831b 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -52,52 +52,27 @@ public class Display.MonitorLayoutManager : GLib.Object { private void save_layout (Gee.LinkedList virtual_monitors) { //Build the layout variant - var dict_builder = new VariantBuilder (VariantType.DICTIONARY); + var dict_builder = new VariantDict (); foreach (var monitor in virtual_monitors) { - var props_builder = new VariantBuilder (VariantType.DICTIONARY); - var monitor_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_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 (); - - warning (props_variant.print (true)); - - dict_builder.add_value (new Variant.dict_entry (monitor_key, props_variant)); + 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); - dict_builder = new VariantBuilder (VariantType.DICTIONARY); - bool found = false; var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); - 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 == save_key) { - // Update existing layout - dict_builder.add_value (new Variant.dict_entry (save_key, layout_variant)); - found = true; - } else { - // Keep existing layout - dict_builder.add_value (new Variant.dict_entry (layout_key, layout)); - } - } - - if (!found) { - // Add new layout - dict_builder.add_value (new Variant.dict_entry (save_key, layout_variant)); - } + 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 ()); } @@ -114,32 +89,6 @@ public class Display.MonitorLayoutManager : GLib.Object { return key.str; } - // 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) { From 5ee9993cacaf75fe46d321dcff954f3b465da7f0 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 27 May 2026 13:27:49 +0100 Subject: [PATCH 19/30] Do not arrange unnecessarily in get_monitor_config() --- src/Objects/MonitorLayoutManager.vala | 2 +- src/Objects/MonitorManager.vala | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index efa3831b..c3ad2c85 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -50,7 +50,7 @@ public class Display.MonitorLayoutManager : GLib.Object { save_layout (virtual_monitors); } - private void save_layout (Gee.LinkedList 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) { diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala index de91eac5..0c8f7ad1 100644 --- a/src/Objects/MonitorManager.vala +++ b/src/Objects/MonitorManager.vala @@ -254,7 +254,9 @@ public class Display.MonitorManager : GLib.Object { } } - layout_manager.arrange_monitors (virtual_monitors); + if (!is_mirrored) { + layout_manager.save_layout (virtual_monitors); + } } public void set_monitor_config () throws Error { From c07e6cbf23d51c0495c27b77fb70099f6ca652ba Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sun, 31 May 2026 19:26:49 +0100 Subject: [PATCH 20/30] Amend schema variant type to match that generated by simplified save function --- data/display.gschema.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/display.gschema.xml b/data/display.gschema.xml index 405b702b..166aba72 100644 --- a/data/display.gschema.xml +++ b/data/display.gschema.xml @@ -1,15 +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). + 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 + From 2b27cd676b87701d1a0737baa34233fa7a109cee Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 1 Jun 2026 10:35:28 +0100 Subject: [PATCH 21/30] Add comment and expanded schema description --- data/display.gschema.xml | 5 ++++- src/Objects/MonitorLayoutManager.vala | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/data/display.gschema.xml b/data/display.gschema.xml index 166aba72..3a94a6b5 100644 --- a/data/display.gschema.xml +++ b/data/display.gschema.xml @@ -5,9 +5,12 @@ {} 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. + 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 diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index c3ad2c85..cead9ffe 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -52,6 +52,7 @@ public class Display.MonitorLayoutManager : GLib.Object { public void save_layout (Gee.LinkedList virtual_monitors) { //Build the layout variant + //NOTE The variant yielded by VariantDict.end () always has type "a{sv}" var dict_builder = new VariantDict (); foreach (var monitor in virtual_monitors) { var props_builder = new VariantDict (); From 4d4b5b7f8e24f9c39835bebd1af88622c7895d92 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 1 Jun 2026 17:56:34 +0100 Subject: [PATCH 22/30] Fix saving and restoring layouts --- src/Objects/MonitorLayoutManager.vala | 65 ++++++++++++++++----------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index cead9ffe..9803b522 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -28,22 +28,32 @@ public class Display.MonitorLayoutManager : GLib.Object { 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) { - + Variant? monitors = null; + if (layouts != null) { + monitors = layouts.lookup_value (layout_key, VariantType.VARDICT); 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; + Variant? props = monitors.lookup_value (virtual_monitor.id, VariantType.VARDICT); + if (props != null) { + int32 x = 0, y = 0; + uint32 t = 0; + if (props.lookup ("x", "i", out x) && + props.lookup ("y", "i", out y) && + props.lookup ("transform", "u", out t)) { + + virtual_monitor.x = x; + virtual_monitor.y = y; + virtual_monitor.transform = t; + } 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 @@ -51,30 +61,31 @@ public class Display.MonitorLayoutManager : GLib.Object { } public void save_layout (Gee.LinkedList virtual_monitors) { - //Build the layout variant - //NOTE The variant yielded by VariantDict.end () always has type "a{sv}" - var dict_builder = new VariantDict (); + var save_key = get_layout_key (virtual_monitors); + + var monitor_dict = new VariantDict (); foreach (var monitor in virtual_monitors) { - var props_builder = new VariantDict (); + var props_dict = 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); + 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)); + monitor_dict.insert_value (monitor.id, props_dict.end ()); } - 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); + var layouts_dict = new VariantDict (layouts); + layouts_dict.insert_value (save_key, monitor_dict.end ()); // Save to settings - settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, dict_builder.end ()); + //NOTE The variant yielded by VariantDict.end () always has type "a{sv}" + settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, layouts_dict.end ()); + } + + private void save_previous_layout () { + var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); + settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); } private string get_layout_key (Gee.LinkedList virtual_monitors) { From 7c7b111efb4e27a5bc316a65f78598b00668335e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 1 Jun 2026 18:24:09 +0100 Subject: [PATCH 23/30] Save and restore which monitor is primary --- data/display.gschema.xml | 1 + src/Objects/MonitorLayoutManager.vala | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/data/display.gschema.xml b/data/display.gschema.xml index 3a94a6b5..a874d0c3 100644 --- a/data/display.gschema.xml +++ b/data/display.gschema.xml @@ -11,6 +11,7 @@ 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" diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 9803b522..b2997dc1 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -36,13 +36,16 @@ public class Display.MonitorLayoutManager : GLib.Object { if (props != null) { int32 x = 0, y = 0; uint32 t = 0; + bool p = false; if (props.lookup ("x", "i", out x) && props.lookup ("y", "i", out y) && - props.lookup ("transform", "u", out t)) { + props.lookup ("transform", "u", out t) && + props.lookup ("primary", "b", out p)) { virtual_monitor.x = x; virtual_monitor.y = y; virtual_monitor.transform = t; + virtual_monitor.primary = p; } else { warning ("property setting missing for monitor %s", virtual_monitor.get_display_name ()); } @@ -70,6 +73,7 @@ public class Display.MonitorLayoutManager : GLib.Object { 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)); monitor_dict.insert_value (monitor.id, props_dict.end ()); } From f8633d0a5c035af955bfbc62b5afb9633aa6923a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 1 Jun 2026 18:28:59 +0100 Subject: [PATCH 24/30] Remove extraneous code --- src/Objects/MonitorLayoutManager.vala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index b2997dc1..0e649a73 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -87,11 +87,6 @@ public class Display.MonitorLayoutManager : GLib.Object { settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, layouts_dict.end ()); } - private void save_previous_layout () { - var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); - settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY); - } - private string get_layout_key (Gee.LinkedList virtual_monitors) { // Generate a unique key based on the virtual monitors' monitors hashes var key = new StringBuilder (); From c741f5d8b256cce12d83024a099478a2c82e1f54 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 1 Jun 2026 18:30:03 +0100 Subject: [PATCH 25/30] Remove unused --- src/Objects/MonitorLayoutManager.vala | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 0e649a73..5c86a473 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -99,14 +99,4 @@ public class Display.MonitorLayoutManager : GLib.Object { 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; - } } From e0f09123bf66262da6904e766482791223d08d1e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 1 Jun 2026 18:58:16 +0100 Subject: [PATCH 26/30] Arrange monitors after getting config to fix toggling mirror mode --- src/Objects/MonitorManager.vala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Objects/MonitorManager.vala b/src/Objects/MonitorManager.vala index 0c8f7ad1..57a5b2c5 100644 --- a/src/Objects/MonitorManager.vala +++ b/src/Objects/MonitorManager.vala @@ -257,6 +257,8 @@ public class Display.MonitorManager : GLib.Object { if (!is_mirrored) { layout_manager.save_layout (virtual_monitors); } + + layout_manager.arrange_monitors (virtual_monitors); } public void set_monitor_config () throws Error { From 6aaaf5716a50bcd1c007edcab5f2a11ec4139de9 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 1 Jun 2026 18:59:00 +0100 Subject: [PATCH 27/30] Rescan displays on monitors-changed signal to redraw after "Keep previous" --- src/Widgets/DisplaysOverlay.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Widgets/DisplaysOverlay.vala b/src/Widgets/DisplaysOverlay.vala index 6cfa3776..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 (); From a76d5b9db797fedb4af8fbe0c6f781fb3d1554c5 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 2 Jun 2026 09:58:27 +0100 Subject: [PATCH 28/30] Save and restore is-active property --- src/Objects/MonitorLayoutManager.vala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 5c86a473..0d597f6e 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -36,16 +36,18 @@ public class Display.MonitorLayoutManager : GLib.Object { if (props != null) { int32 x = 0, y = 0; uint32 t = 0; - bool p = false; + 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 ("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 ()); } @@ -74,6 +76,7 @@ public class Display.MonitorLayoutManager : GLib.Object { 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 ()); } From fb242bc037dd6641143a8f16dcb63b3bddabe168 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 2 Jun 2026 10:02:16 +0100 Subject: [PATCH 29/30] Mention "is-active" property in schema description --- data/display.gschema.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/display.gschema.xml b/data/display.gschema.xml index a874d0c3..dc4c1d95 100644 --- a/data/display.gschema.xml +++ b/data/display.gschema.xml @@ -11,7 +11,7 @@ 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" + Property keys: "x", "y", "transform", "primary", "is-active" From 2af8f91ad39b9d744f32a10937c6dd9a72b7d282 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Tue, 2 Jun 2026 10:27:09 +0100 Subject: [PATCH 30/30] Update some comments --- src/Objects/MonitorLayoutManager.vala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Objects/MonitorLayoutManager.vala b/src/Objects/MonitorLayoutManager.vala index 0d597f6e..f7cdb687 100644 --- a/src/Objects/MonitorLayoutManager.vala +++ b/src/Objects/MonitorLayoutManager.vala @@ -71,7 +71,6 @@ public class Display.MonitorLayoutManager : GLib.Object { var monitor_dict = new VariantDict (); foreach (var monitor in virtual_monitors) { var props_dict = new VariantDict (); - // We save three properties for now, may want to save more later 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)); @@ -92,6 +91,9 @@ public class Display.MonitorLayoutManager : GLib.Object { 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) {