Skip to content

Introduce Monitor Layout Manager #423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

leonardo-lemos
Copy link
Collaborator

@leonardo-lemos leonardo-lemos commented May 30, 2025

Fixes #418
Closes #412
Resolves #424

This PR introduces the MonitorLayoutManager class to persist and restore monitor arrangements when changes occur.

We rely on Mutter's DisplayConfig D-Bus interface to detect monitor changes and retrieve the current monitor layout configuration (i.e., virtual monitors or CRTCs). However, there are some edge cases that cause layout issues:

  • Clone Mode (Mirror Mode): When clone mode is enabled, Settings creates a single virtual monitor encompassing all physical monitors with identical coordinates (x=0, y=0). When clone mode is disabled, Settings has no memory of the previous layout and falls back to overlapping display widgets, since all monitors share the same coordinates.

  • Disabling Monitors: When a monitor is disabled, Mutter still reports its existence but omits its coordinates, as it’s no longer part of the active layout. This results in the disabled monitor’s widget overlapping another widget in the UI.

To address these issues, this PR introduces a GSetting to persist monitor layout data, including each monitor's position and transformation. This allows us to restore previous layouts accurately, even after mode toggles or monitor disable events.

Additionally, this PR modifies when the display overlay updates its widgets — now triggered only when monitor changes are detected, ensuring a more stable and consistent display configuration.

@leonardo-lemos leonardo-lemos marked this pull request as ready for review May 31, 2025 23:12
@danirabbit
Copy link
Member

I think @jeremypw has some experience in this codebase?

@jeremypw
Copy link
Collaborator

jeremypw commented Jun 6, 2025

Its been quite a while since I looked at this plug but I'll try to test this out this weekend.

Copy link

@Marukesu Marukesu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some simplifications suggestions.

@@ -30,3 +30,5 @@ config_file = configure_file(
subdir('data')
subdir('src')
subdir('po')

gnome.post_install(glib_compile_schemas: true)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meson_version should be set to '>=0.57' so that it doesn't try to run the build script in a older version.

Comment on lines +62 to +69
public void save_layout (Gee.LinkedList<VirtualMonitor> 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);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_layout_variant and add_or_update_layout should be inlined here.

}

public MonitorLayoutProfile? find_match_layout (string key) {
// Layouts format are 'a{sa{sa{sv}}}'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"a{sa{s(iiu)}}" should be simpler, we don't need the monitor properties be extensible, nor the extra boxing of using variant values.

Comment on lines +133 to +154
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 ();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using VariantDict here make the code simple. Also, you can use monitor.id instead of getting the id of the first monitor only.

Suggested change
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 ();
var dict_builder = new VariantDict ();
foreach (var monitor in virtual_monitors) {
var props_builder = new VariantDict ();
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));
}
return dict_builder.end ();

or using "{s(iiu)}" instead of "{sv}"

Suggested change
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 ();
var dict_builder = new VariantDict ();
foreach (var monitor in virtual_monitors) {
dict_builder.insert (monitor.id, "(iiu)", monitor.x, monitor.y, monitor.transform);
}
return dict_builder.end ();

Comment on lines +158 to +178
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));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Suggested change
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));
}
var layout_builder = new VariantDict (layouts);
layout_builder.insert_value (key, layout_variant);

}

public void arrange_monitors (Gee.LinkedList<VirtualMonitor> virtual_monitors) {
if (virtual_monitors.size == 1) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloned monitors have just one virtual monitor, so the checks bellow aren't necessary.

}

var layout_key = get_layout_key (virtual_monitors);
var layout = find_match_layout (layout_key);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find_match_layout should be inlined.

Comment on lines +30 to +59
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);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With MonitorManager.get_monitor_config() calling save_layout, and MonitorManager.disable_clone_mode() calling arrange_monitors, there's no need for handling updating the layout here.

The suggestion is assuming the above and that we are receiving the monitors dictionary from find_match_layout.

Suggested change
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);
}
if (layout != null) {
foreach (var virtual_monitor in virtual_monitors) {
VariantDict monitor_position;
if (layout.lookup (virtual_monitor.id, "a{sv}", out monitor_position)) {
virtual_monitor.x = monitor_position.lookup_value ("x").get_int ();
virtual_monitor.y = monitor_position.lookup_value ("y").get_int ();
virtual_monitor.transform = monitor_position.lookup_value ("transform").get_uint ();
}
}
} else {
// If no layout found, we save the current layout to use later
save_layout (virtual_monitors);
}

or with "{s(iiu)}" instead of "{sv}"

Suggested change
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);
}
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)) {
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);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this class isn't necessary at all. There's just the settings object, and with the code cleaned, what rest is arrange_windows, save_layout, and get_layout_key.

all would be better residing in the MonitorManager class.

}
}

layout_manager.arrange_monitors (virtual_monitors);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disable_clone_mode is already re-arranging the monitors, so we only need to save it here.

Suggested change
layout_manager.arrange_monitors (virtual_monitors);
if (!is_mirrored) {
layout_manager.save_layouts (virtual_monitors);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants