Skip to content
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

Re-open windows for a monitor when it's re-connected #64

Open
benthejack-vuw opened this issue Aug 25, 2023 · 9 comments
Open

Re-open windows for a monitor when it's re-connected #64

benthejack-vuw opened this issue Aug 25, 2023 · 9 comments

Comments

@benthejack-vuw
Copy link
Contributor

if a monitor is turned off ags currently has to be killed and re-started, it would be great if it could re-launch any windows for that monitor.

@Aylur
Copy link
Owner

Aylur commented Sep 8, 2023

I can't figure out how to show the window on the correct monitor, no matter how I call GtkLayerShell.set_monitor the window stays where it is which is the monitor that has focus when the monitor is reconnected

@Greccl
Copy link

Greccl commented Jan 17, 2024

Hi, I have a workaround for that. I noticed that when a monitor is powered off the windows are destroyed, so connect to the destroyed signal of that window and rebuild it.

if you build your window with a function like in the example bar make this:

win.connect('destroy', () => {
	app.removeWindow(win);
	app.addWindow(Bar(monitor));
	});

Be careful because the old window is fully destroyed and a new one is created, it means that if you have functions that make use of widget instances of the old window, it will throws error each time its executed. For example:

if you added:

Hyprland.connect('workspace-added', (_, id) => {
		// ...
		someWorkspaceButtonContainer.add( build_a_button_for_workspace(id) );
		// ...
});

the handler must be removed because someWorkspaceButtonContainer will be disposed on your window destruction, so get the handler id and remove it like this:

let signal_id = Hyprland.connect('workspace-added', (_, id) => {
		// ...
		someWorkspaceButtonContainer.add( build_a_button_for_workspace(id) );
		// ...
});

// ...

win.connect('destroy', () => {
	if (signal_id) {
		Hyprland.disconnect(signal_id);
		signal_id = null;
		}
	app.removeWindow(win);
	app.addWindow(Bar(monitor));
	});

Same happens with interval, timeouts, and so on. if you use .bind(), make use of the last parameter so the callback can be removed on widget or service destruction.

Hope be helpful.

@Aylur
Copy link
Owner

Aylur commented Jan 17, 2024

Hi, I have a workaround for that. I noticed that when a monitor is powered off the windows are destroyed, so connect to the destroyed signal of that window and rebuild it.

Thanks for the input, it didn't even occur to me that it emitted destroy, since App should have a reference of it, so I wonder why it does. Anyway will put it on the common issues page as a plausible solution. The disconnection issue shouldn't be a problem as long as you only use .bind() and .hook()

const Bar = (...params) => Window({
  setup: win => win.on('destroy', () => {
    App.removeWindow(win)
    App.addWindow(Bar(...params))
  })
})

@Greccl
Copy link

Greccl commented Jan 17, 2024

i haven't tried to reproduce in sway, but i remember that when i came from sway to hyprland one thing that surprised me was that hyprland kill outputs when the monitor is powered off, but in sway i had not that issue. I suspect that hyprland send a destroy event on the output interface and then gdk issue a window destroy, so nothing to do for ags, its an external problem.

the only thing that comes to mind is bind() to hyprland monitors and map between monitors and windows, then have a program logic to rebuild the windows when the monitor is reconnected. i think that the Hyprland service could fetch name and id from monitors for the sake of really binding windows to the correct screen.

maybe a new feature could be adding a "monitor" parameter to the Widget.Window class and then monitoring monitor disconnections but that is tricky, that shouldn't be necessary if the compositor works properly. I just want to say that this problem comes from outside, your work is awesome and it works, thank you for that.

@Greccl
Copy link

Greccl commented Feb 1, 2024

i think that the Hyprland service could fetch name and id from monitors

My bad, it's implemented, sorry.

I can't figure out how to show the window on the correct monitor, no matter how I call GtkLayerShell.set_monitor the window stays where it is which is the monitor that has focus when the monitor is reconnected

I found a way to do that:

// This function returns the monitor number in the Gdk.Display monitor list
// given it's name in your wayland compositor
function getMonitorIndexFromName(name) {
	let x = -1;
	let y = -1;
	// Suppose you are in Hyprland, find your way if you are in Sway
	for (let m of Hyprland.monitors) if (m.name == name) {
		x = m.x + (m.width / 2);
		y = m.y + (m.height / 2);
		break;
	}
	if (x == -1) return 0; // Monitor 0 if errors
	let monitor = Gdk.Display.get_default()?.get_monitor_at_point(x, y);
	for (let i=0; i<Gdk.Display.get_default()?.get_n_monitors(); i++) {
		const m = Gdk.Display.get_default()?.get_monitor(i);
		if (m === monitor) return i;
	}
	return 0; // Monitor 0 if errors
};

The Gdk docs says that they represents the display (compositor in our case) as a big and unique screen where monitors are just portions. Gdk.Display.get_monitor_at_point(x, y) returns the Gdk.Monitor that contains the (x,y) point, or the nearest, so i calculate the center of the monitor just for fun, any point should work, for example (monitor.x + 1, monitor.y + 1).

I tested and found that Window.monitor can set the correct monitor, even dynamically, but Window.gdkmonitor don't.

if a monitor is turned off ags currently has to be killed and re-started, it would be great if it could re-launch any windows for that monitor.

@benthejack-vuw try this:

// Find the imports
/*
	Windows creation and bind to specific monitor
*/
const windowList = [
	{ monitor: "HDMI-A-1", callback: Bar,  data: "extra parameter if needed"},
	{ monitor: "eDP-1", callback: OtherBar,  data: "optional"},
	{ monitor: "DP-2", callback: FancyWidget,  data: "don't add if not used"},
];

function getMonitorIndex (name) {
	let x = -1;
	let y = -1;
	for (let m of Hyprland.monitors) if (m.name == name) {
		x = m.x + 1;
		y = m.y + 1;
		break;
	}
	if (x == -1) return 0;
	let monitor = Gdk.Display.get_default()?.get_monitor_at_point(x, y);
	for (let i=0; i<Gdk.Display.get_default()?.get_n_monitors(); i++) {
		const m = Gdk.Display.get_default()?.get_monitor(i);
		if (m === monitor) return i;
	}
	return 0;
};

function initWindows() {
	if (Hyprland.monitors.length == 0) {
		timeout(100, initWindows);
		return;
	}
	for (let entry of windowList) {
		const win = entry.callback(getMonitorIndex(entry.monitor), entry.data);
		app.addWindow(win);
		win.on("destroy", () => app.removeWindow(win.name) );
	}
};
initWindows();

Hyprland.connect('monitor-added', (_hypr, monitor) => {
	if (!monitor) return;
	for (let entry of windowList) if (monitor == entry.monitor) {
		const win = entry.callback(getMonitorIndex(entry.monitor), entry.data);
		app.addWindow(win);
		win.on("destroy", () => app.removeWindow(win.name) );
	}
});

@The-Lost-Light
Copy link
Contributor

The-Lost-Light commented Jun 4, 2024

Share my simply method

const createWindows = () =>
	[
		...Hyprland.monitors.map(m => Bar(m.id)),
		Powermenu(),
	].map(w => w.on("destroy", self => App.removeWindow(self)));

const recreateWindows = () => {
	for (const win of App.windows) {
		App.removeWindow(win);
	}
	App.config({ windows: createWindows() });
};

export default App.config({
	style: css,
	windows: createWindows(),
	onConfigParsed: () => {
		hyprland.connect("monitor-removed", recreateWindows);
		hyprland.connect("monitor-added", recreateWindows);
	},
});

use .on("destroy", self => App.removeWindow(self)) deal with disposed windows
and when the monitor connect or disconnect rebuild all windows

@Greccl
Copy link

Greccl commented Jun 6, 2024

Beware, windows should not be created on monitor-removed event. Removing on window's destroy signal should be enough, and recreatewindows() needs to be called on monitor-added event only, and just for creation, removal is performed on destroy signal. A little improvement.

Another problem is multimonitor setups, how to bind the correct window to the correct monitor as aylur mentioned?

@The-Lost-Light
Copy link
Contributor

For my instance, currently, My windows have bar-0, bar-1, power_menu

  1. If use addWindow(self) in destroy signal, when unplug monitor1, bar-1 would recreate at monitor0
  2. If not use recreateWindows in monitor-removed event, if i open power_menu(which not specify monitor) at monitor1 and unplug it at the same time, then power_menu would be destroy and not recreate (since 1.)
  3. monitor-added actually can only for creation, but I don't know how to determine if each window already exists. So it’s better to recreate everything

Finally, my approach is actually like restart ags when monitor changed, so it should correct the window's position as its initial.
I think its concise way without too many side effects (There aren’t that many opportunities to dis/connect monitor)

@Greccl
Copy link

Greccl commented Jun 10, 2024

I see, it's enough for some monitor-agnostic implementación, but note that issue is about binding a specific window to a specific monitor, it make sense what monitor is the correct, since some user, like the OP, maybe wants display their windows in that specific monitor and not in other one.

Suppose someone have two monitors with a bar in each one, if one is disconnected, both bars will appear in the only available monitor, it may be annoying.

Note that some HDMI monitors seem to go out when you turn it off, but some VGA monitors just forward an dmps state change. It's a hardware issue but in the former some window managers cant distinguish between a monitor being disconnected and just powering off.

The OP wonders about reopen window without restarting ags. In your second case, it's a problem with GDK, it actually destroy the window on monitor disconnection without giving you the chance to reuse it, and the window's state is lost. Unfortunately, in your example, powermenu-like windows needs a more sofisticated mechanism for recreation.

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

When branches are created from issues, their pull requests are automatically linked.

4 participants