Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1ea364c
Add gschema
leonardo-lemos May 30, 2025
762da1f
Implement MonitorLayoutManager
leonardo-lemos May 30, 2025
dd6c098
Use MonitorLayoutManager
leonardo-lemos May 30, 2025
b2c0ff1
Fix build
leonardo-lemos May 30, 2025
36dd686
Merge branch 'main' into introduce-layout-manager
leonardo-lemos May 30, 2025
441b280
Notify monitor number change only once
leonardo-lemos May 31, 2025
12a1a02
Show display label only once
leonardo-lemos May 31, 2025
f860515
Rescan Displays only if monitors changed
leonardo-lemos May 31, 2025
6712461
Add display transformation saving
leonardo-lemos May 31, 2025
756c165
Removed unused variable
leonardo-lemos Jun 1, 2025
26bcac8
Merge branch 'main' into introduce-layout-manager
zeebok Dec 4, 2025
756431b
Merge branch 'main' into introduce-layout-manager
jeremypw May 22, 2026
3690b20
Drop unnecessary check
jeremypw May 27, 2026
e43a21c
Reduce scope of some functions
jeremypw May 27, 2026
a9f5d71
Simplify arrange_monitors() & get_layout_key()
jeremypw May 27, 2026
c5071c6
Lose unused object
jeremypw May 27, 2026
0e85221
Inline simplified find_match_layout()
jeremypw May 27, 2026
a90c359
Drop hash for key
jeremypw May 27, 2026
4442fa3
Inline build_layout_variant()
jeremypw May 27, 2026
844dc04
Inline add_or_update_layout()
jeremypw May 27, 2026
11ebe2c
Simplify save layout using VariantDict
jeremypw May 27, 2026
5ee9993
Do not arrange unnecessarily in get_monitor_config()
jeremypw May 27, 2026
1b7cdc9
Merge branch 'main' into introduce-layout-manager
jeremypw May 27, 2026
c07e6cb
Amend schema variant type to match that generated by simplified save …
jeremypw May 31, 2026
2b27cd6
Add comment and expanded schema description
jeremypw Jun 1, 2026
4d4b5b7
Fix saving and restoring layouts
jeremypw Jun 1, 2026
7c7b111
Save and restore which monitor is primary
jeremypw Jun 1, 2026
f8633d0
Remove extraneous code
jeremypw Jun 1, 2026
c741f5d
Remove unused
jeremypw Jun 1, 2026
e0f0912
Arrange monitors after getting config to fix toggling mirror mode
jeremypw Jun 1, 2026
6aaaf57
Rescan displays on monitors-changed signal to redraw after "Keep prev…
jeremypw Jun 1, 2026
a76d5b9
Save and restore is-active property
jeremypw Jun 2, 2026
fb242bc
Mention "is-active" property in schema description
jeremypw Jun 2, 2026
2af8f91
Update some comments
jeremypw Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions data/display.gschema.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<schemalist>
<schema id="io.elementary.settings.display" path="/io/elementary/settings/display/">

<key name="preferred-display-layouts" type="a{sv}">
<default>{}</default>
<summary>Preferred display layouts</summary>
<description>
Each profile contains a unique identifier and a list of monitors, with their respective position (x, y) and other properties such as transformation (e.g., rotation).
This allows the system to restore or suggest preferred monitor arrangements and settings when displays are connected or configurations change.
The expanded variant type is "a{sa{sa{sv}}}"
Setting: Dictionary of layouts - key is based on a combination of monitor ids
Layout: Dictionary of monitors - key is monitor identifier
Monitor: Dictionary of property values - key is property name
Property keys: "x", "y", "transform", "primary", "is-active"
</description>
</key>

</schema>
</schemalist>
6 changes: 6 additions & 0 deletions data/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)
2 changes: 2 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ config_file = configure_file(
subdir('data')
subdir('src')
subdir('po')

gnome.post_install(glib_compile_schemas: true)
Comment thread
jeremypw marked this conversation as resolved.
107 changes: 107 additions & 0 deletions src/Objects/MonitorLayoutManager.vala
Comment thread
jeremypw marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* SPDX-License-Identifier: GPL-2.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. <https://elementary.io>
*
* Authored by: Leonardo Lemos <leonardolemos@live.com>
*/

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<VirtualMonitor> virtual_monitors) {
if (virtual_monitors.size == 1) {
Comment thread
jeremypw marked this conversation as resolved.
// If there's only one monitor, no need to arrange
// Cloned monitors only have one virtual monitor so will return here
return;
}

var layout_key = get_layout_key (virtual_monitors);
// Layouts format are 'a{sa{sa{sv}}}'
var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY);
Variant? monitors = null;
if (layouts != null) {
monitors = layouts.lookup_value (layout_key, VariantType.VARDICT);
foreach (var virtual_monitor in virtual_monitors) {
Variant? props = monitors.lookup_value (virtual_monitor.id, VariantType.VARDICT);
if (props != null) {
int32 x = 0, y = 0;
uint32 t = 0;
bool p = false, e = false;
if (props.lookup ("x", "i", out x) &&
props.lookup ("y", "i", out y) &&
props.lookup ("transform", "u", out t) &&
props.lookup ("primary", "b", out p) &&
props.lookup ("enabled", "b", out e)) {

virtual_monitor.x = x;
virtual_monitor.y = y;
virtual_monitor.transform = t;
virtual_monitor.primary = p;
virtual_monitor.is_active = e;
} else {
warning ("property setting missing for monitor %s", virtual_monitor.get_display_name ());
}
} else {
warning ("no property dictionary found for monitor.id %s", virtual_monitor.get_display_name ());
}
}

return;
} else {
warning ("layout key %s not found", layout_key);
}

// If no layout found, we save the current layout to use later
save_layout (virtual_monitors);
}

public void save_layout (Gee.LinkedList<VirtualMonitor> virtual_monitors) {
var save_key = get_layout_key (virtual_monitors);

var monitor_dict = new VariantDict ();
foreach (var monitor in virtual_monitors) {
var props_dict = new VariantDict ();
props_dict.insert_value ("x", new Variant.int32 (monitor.x));
props_dict.insert_value ("y", new Variant.int32 (monitor.y));
props_dict.insert_value ("transform", new Variant.uint32 (monitor.transform));
props_dict.insert_value ("primary", new Variant.boolean (monitor.primary));
props_dict.insert_value ("enabled", new Variant.boolean (monitor.is_active));
monitor_dict.insert_value (monitor.id, props_dict.end ());
}

// Add or update the layouts setting
var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY);
var layouts_dict = new VariantDict (layouts);
layouts_dict.insert_value (save_key, monitor_dict.end ());

// Save to settings
//NOTE The variant yielded by VariantDict.end () always has type "a{sv}"
settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, layouts_dict.end ());
}

private string get_layout_key (Gee.LinkedList<VirtualMonitor> virtual_monitors) {
// Generate a unique key based on the virtual monitors' monitors hashes
//NOTE The key depends on the order of the list which will change depending on whether monitors are
// active or not (and possibly on the order they were connected).
//TODO Consider whether a more controlled key is needed
var key = new StringBuilder ();

foreach (var virtual_monitor in virtual_monitors) {
foreach (var monitor in virtual_monitor.monitors) {
key.append (virtual_monitor.id);
}
}

return key.str;
}
}
36 changes: 26 additions & 10 deletions src/Objects/MonitorManager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -65,9 +68,16 @@ public class Display.MonitorManager : GLib.Object {
construct {
monitors = new Gee.LinkedList<Display.Monitor> ();
virtual_monitors = new Gee.LinkedList<Display.VirtualMonitor> ();
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);
}
Expand All @@ -78,7 +88,10 @@ public class Display.MonitorManager : GLib.Object {
MutterReadLogicalMonitor[] mutter_logical_monitors;
GLib.HashTable<string, GLib.Variant> 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);
}
Expand Down Expand Up @@ -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)
Expand All @@ -237,9 +250,15 @@ public class Display.MonitorManager : GLib.Object {
virtual_monitor.primary = false;
virtual_monitor.monitors.add (monitor);
virtual_monitor.scale = virtual_monitors[0].scale;
add_virtual_monitor (virtual_monitor);
virtual_monitors.add (virtual_monitor);
}
}

if (!is_mirrored) {
layout_manager.save_layout (virtual_monitors);
}

layout_manager.arrange_monitors (virtual_monitors);
}

public void set_monitor_config () throws Error {
Expand Down Expand Up @@ -402,13 +421,10 @@ public class Display.MonitorManager : GLib.Object {
virtual_monitors.clear ();
virtual_monitors.add_all (new_virtual_monitors);

notify_property ("virtual-monitor-number");
notify_property ("is-mirrored");
}
layout_manager.arrange_monitors (virtual_monitors);

private void add_virtual_monitor (Display.VirtualMonitor virtual_monitor) {
virtual_monitors.add (virtual_monitor);
notify_property ("virtual-monitor-number");
notify_property ("is-mirrored");
}

private VirtualMonitor? get_virtual_monitor_by_id (string id) {
Expand Down
7 changes: 2 additions & 5 deletions src/Widgets/DisplaysOverlay.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 ();
Expand All @@ -198,6 +198,7 @@ public class Display.DisplaysOverlay : Gtk.Box {
add_output (virtual_monitor);
}

show_windows ();
change_active_displays_sensitivity ();
calculate_ratio ();
scanning = false;
Expand Down Expand Up @@ -350,10 +351,6 @@ public class Display.DisplaysOverlay : Gtk.Box {
check_configuration_change ();
calculate_ratio ();
});

if (!monitor_manager.is_mirrored && virtual_monitor.is_active) {
show_windows ();
}
}

private void set_as_primary (Display.VirtualMonitor new_primary) {
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plug_files = files(
'Objects/MonitorMode.vala',
'Objects/MonitorManager.vala',
'Objects/Monitor.vala',
'Objects/MonitorLayoutManager.vala',
'Views/NightLightView.vala',
'Views/DisplaysView.vala',
'Views' / 'FiltersView.vala',
Expand Down