Skip to content

Commit c90d41e

Browse files
leonardo-lemoszeebokjeremypw
authored
Introduce Monitor Layout Manager (#423)
* Add gschema * Implement MonitorLayoutManager * Use MonitorLayoutManager * Fix build * Notify monitor number change only once * Show display label only once * Rescan Displays only if monitors changed * Add display transformation saving * Removed unused variable * Drop unnecessary check * Reduce scope of some functions * Simplify arrange_monitors() & get_layout_key() * Lose unused object * Inline simplified find_match_layout() * Drop hash for key * Inline build_layout_variant() * Inline add_or_update_layout() * Simplify save layout using VariantDict * Do not arrange unnecessarily in get_monitor_config() * Amend schema variant type to match that generated by simplified save function * Add comment and expanded schema description * Fix saving and restoring layouts * Save and restore which monitor is primary * Remove extraneous code * Remove unused * Arrange monitors after getting config to fix toggling mirror mode * Rescan displays on monitors-changed signal to redraw after "Keep previous" * Save and restore is-active property * Mention "is-active" property in schema description * Update some comments --------- Co-authored-by: Ryan Kornheisl <ryan@skarva.tech> Co-authored-by: Jeremy Wootten <jeremywootten@gmail.com>
1 parent 5bd8f7d commit c90d41e

7 files changed

Lines changed: 163 additions & 15 deletions

File tree

‎data/display.gschema.xml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<schemalist>
2+
<schema id="io.elementary.settings.display" path="/io/elementary/settings/display/">
3+
4+
<key name="preferred-display-layouts" type="a{sv}">
5+
<default>{}</default>
6+
<summary>Preferred display layouts</summary>
7+
<description>
8+
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).
9+
This allows the system to restore or suggest preferred monitor arrangements and settings when displays are connected or configurations change.
10+
The expanded variant type is "a{sa{sa{sv}}}"
11+
Setting: Dictionary of layouts - key is based on a combination of monitor ids
12+
Layout: Dictionary of monitors - key is monitor identifier
13+
Monitor: Dictionary of property values - key is property name
14+
Property keys: "x", "y", "transform", "primary", "is-active"
15+
</description>
16+
</key>
17+
18+
</schema>
19+
</schemalist>

‎data/meson.build‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ gresource = gnome.compile_resources(
1111
'gresource',
1212
'display.gresource.xml'
1313
)
14+
15+
install_data(
16+
'display.gschema.xml',
17+
install_dir: datadir / 'glib-2.0' / 'schemas',
18+
rename: 'io.elementary.settings.display.gschema.xml'
19+
)

‎meson.build‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ config_file = configure_file(
3131
subdir('data')
3232
subdir('src')
3333
subdir('po')
34+
35+
gnome.post_install(glib_compile_schemas: true)
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-2.0-or-later
3+
* SPDX-FileCopyrightText: 2025 elementary, Inc. <https://elementary.io>
4+
*
5+
* Authored by: Leonardo Lemos <leonardolemos@live.com>
6+
*/
7+
8+
public class Display.MonitorLayoutManager : GLib.Object {
9+
private Settings settings;
10+
11+
private const string PREFERRED_MONITOR_LAYOUTS_KEY = "preferred-display-layouts";
12+
13+
public MonitorLayoutManager () {
14+
Object ();
15+
}
16+
17+
construct {
18+
settings = new Settings ("io.elementary.settings.display");
19+
}
20+
21+
public void arrange_monitors (Gee.LinkedList<VirtualMonitor> virtual_monitors) {
22+
if (virtual_monitors.size == 1) {
23+
// If there's only one monitor, no need to arrange
24+
// Cloned monitors only have one virtual monitor so will return here
25+
return;
26+
}
27+
28+
var layout_key = get_layout_key (virtual_monitors);
29+
// Layouts format are 'a{sa{sa{sv}}}'
30+
var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY);
31+
Variant? monitors = null;
32+
if (layouts != null) {
33+
monitors = layouts.lookup_value (layout_key, VariantType.VARDICT);
34+
foreach (var virtual_monitor in virtual_monitors) {
35+
Variant? props = monitors.lookup_value (virtual_monitor.id, VariantType.VARDICT);
36+
if (props != null) {
37+
int32 x = 0, y = 0;
38+
uint32 t = 0;
39+
bool p = false, e = false;
40+
if (props.lookup ("x", "i", out x) &&
41+
props.lookup ("y", "i", out y) &&
42+
props.lookup ("transform", "u", out t) &&
43+
props.lookup ("primary", "b", out p) &&
44+
props.lookup ("enabled", "b", out e)) {
45+
46+
virtual_monitor.x = x;
47+
virtual_monitor.y = y;
48+
virtual_monitor.transform = t;
49+
virtual_monitor.primary = p;
50+
virtual_monitor.is_active = e;
51+
} else {
52+
warning ("property setting missing for monitor %s", virtual_monitor.get_display_name ());
53+
}
54+
} else {
55+
warning ("no property dictionary found for monitor.id %s", virtual_monitor.get_display_name ());
56+
}
57+
}
58+
59+
return;
60+
} else {
61+
warning ("layout key %s not found", layout_key);
62+
}
63+
64+
// If no layout found, we save the current layout to use later
65+
save_layout (virtual_monitors);
66+
}
67+
68+
public void save_layout (Gee.LinkedList<VirtualMonitor> virtual_monitors) {
69+
var save_key = get_layout_key (virtual_monitors);
70+
71+
var monitor_dict = new VariantDict ();
72+
foreach (var monitor in virtual_monitors) {
73+
var props_dict = new VariantDict ();
74+
props_dict.insert_value ("x", new Variant.int32 (monitor.x));
75+
props_dict.insert_value ("y", new Variant.int32 (monitor.y));
76+
props_dict.insert_value ("transform", new Variant.uint32 (monitor.transform));
77+
props_dict.insert_value ("primary", new Variant.boolean (monitor.primary));
78+
props_dict.insert_value ("enabled", new Variant.boolean (monitor.is_active));
79+
monitor_dict.insert_value (monitor.id, props_dict.end ());
80+
}
81+
82+
// Add or update the layouts setting
83+
var layouts = settings.get_value (PREFERRED_MONITOR_LAYOUTS_KEY);
84+
var layouts_dict = new VariantDict (layouts);
85+
layouts_dict.insert_value (save_key, monitor_dict.end ());
86+
87+
// Save to settings
88+
//NOTE The variant yielded by VariantDict.end () always has type "a{sv}"
89+
settings.set_value (PREFERRED_MONITOR_LAYOUTS_KEY, layouts_dict.end ());
90+
}
91+
92+
private string get_layout_key (Gee.LinkedList<VirtualMonitor> virtual_monitors) {
93+
// Generate a unique key based on the virtual monitors' monitors hashes
94+
//NOTE The key depends on the order of the list which will change depending on whether monitors are
95+
// active or not (and possibly on the order they were connected).
96+
//TODO Consider whether a more controlled key is needed
97+
var key = new StringBuilder ();
98+
99+
foreach (var virtual_monitor in virtual_monitors) {
100+
foreach (var monitor in virtual_monitor.monitors) {
101+
key.append (virtual_monitor.id);
102+
}
103+
}
104+
105+
return key.str;
106+
}
107+
}

‎src/Objects/MonitorManager.vala‎

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ public class Display.MonitorManager : GLib.Object {
4646
}
4747
}
4848

49+
public signal void monitors_changed ();
50+
4951
private MutterDisplayConfigInterface iface;
5052
private uint current_serial;
53+
private MonitorLayoutManager layout_manager;
5154

5255
private static MonitorManager monitor_manager;
5356
public static unowned MonitorManager get_default () {
@@ -65,9 +68,16 @@ public class Display.MonitorManager : GLib.Object {
6568
construct {
6669
monitors = new Gee.LinkedList<Display.Monitor> ();
6770
virtual_monitors = new Gee.LinkedList<Display.VirtualMonitor> ();
71+
layout_manager = new MonitorLayoutManager ();
6872
try {
69-
iface = Bus.get_proxy_sync (BusType.SESSION, "org.gnome.Mutter.DisplayConfig", "/org/gnome/Mutter/DisplayConfig");
70-
iface.monitors_changed.connect (get_monitor_config);
73+
iface = Bus.get_proxy_sync (
74+
BusType.SESSION,
75+
"org.gnome.Mutter.DisplayConfig",
76+
"/org/gnome/Mutter/DisplayConfig");
77+
iface.monitors_changed.connect (() => {
78+
get_monitor_config ();
79+
monitors_changed ();
80+
});
7181
} catch (Error e) {
7282
critical (e.message);
7383
}
@@ -78,7 +88,10 @@ public class Display.MonitorManager : GLib.Object {
7888
MutterReadLogicalMonitor[] mutter_logical_monitors;
7989
GLib.HashTable<string, GLib.Variant> properties;
8090
try {
81-
iface.get_current_state (out current_serial, out mutter_monitors, out mutter_logical_monitors, out properties);
91+
iface.get_current_state (out current_serial,
92+
out mutter_monitors,
93+
out mutter_logical_monitors,
94+
out properties);
8295
} catch (Error e) {
8396
critical (e.message);
8497
}
@@ -217,7 +230,7 @@ public class Display.MonitorManager : GLib.Object {
217230
virtual_monitor.scale = mutter_logical_monitor.scale;
218231
virtual_monitor.transform = mutter_logical_monitor.transform;
219232
virtual_monitor.primary = mutter_logical_monitor.primary;
220-
add_virtual_monitor (virtual_monitor);
233+
virtual_monitors.add (virtual_monitor);
221234
}
222235

223236
// Look for any monitors that aren't part of a virtual monitor (hence disabled)
@@ -237,9 +250,15 @@ public class Display.MonitorManager : GLib.Object {
237250
virtual_monitor.primary = false;
238251
virtual_monitor.monitors.add (monitor);
239252
virtual_monitor.scale = virtual_monitors[0].scale;
240-
add_virtual_monitor (virtual_monitor);
253+
virtual_monitors.add (virtual_monitor);
241254
}
242255
}
256+
257+
if (!is_mirrored) {
258+
layout_manager.save_layout (virtual_monitors);
259+
}
260+
261+
layout_manager.arrange_monitors (virtual_monitors);
243262
}
244263

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

405-
notify_property ("virtual-monitor-number");
406-
notify_property ("is-mirrored");
407-
}
424+
layout_manager.arrange_monitors (virtual_monitors);
408425

409-
private void add_virtual_monitor (Display.VirtualMonitor virtual_monitor) {
410-
virtual_monitors.add (virtual_monitor);
411426
notify_property ("virtual-monitor-number");
427+
notify_property ("is-mirrored");
412428
}
413429

414430
private VirtualMonitor? get_virtual_monitor_by_id (string id) {

‎src/Widgets/DisplaysOverlay.vala‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public class Display.DisplaysOverlay : Gtk.Box {
9797
gala_dbus = GLib.Bus.get_proxy.end (res);
9898
monitor_manager = Display.MonitorManager.get_default ();
9999
monitor_manager.notify["virtual-monitor-number"].connect (() => rescan_displays ());
100+
monitor_manager.monitors_changed.connect (() => rescan_displays ());
100101
rescan_displays ();
101102
} catch (GLib.Error e) {
102103
critical (e.message);
@@ -185,7 +186,6 @@ public class Display.DisplaysOverlay : Gtk.Box {
185186

186187
public void rescan_displays () {
187188
scanning = true;
188-
189189
display_widgets.@foreach ((display_widget) => {
190190
overlay.remove_overlay (display_widget);
191191
display_widget.destroy ();
@@ -198,6 +198,7 @@ public class Display.DisplaysOverlay : Gtk.Box {
198198
add_output (virtual_monitor);
199199
}
200200

201+
show_windows ();
201202
change_active_displays_sensitivity ();
202203
calculate_ratio ();
203204
scanning = false;
@@ -350,10 +351,6 @@ public class Display.DisplaysOverlay : Gtk.Box {
350351
check_configuration_change ();
351352
calculate_ratio ();
352353
});
353-
354-
if (!monitor_manager.is_mirrored && virtual_monitor.is_active) {
355-
show_windows ();
356-
}
357354
}
358355

359356
private void set_as_primary (Display.VirtualMonitor new_primary) {

‎src/meson.build‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ plug_files = files(
1010
'Objects/MonitorMode.vala',
1111
'Objects/MonitorManager.vala',
1212
'Objects/Monitor.vala',
13+
'Objects/MonitorLayoutManager.vala',
1314
'Views/NightLightView.vala',
1415
'Views/DisplaysView.vala',
1516
'Views' / 'FiltersView.vala',

0 commit comments

Comments
 (0)