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