Qt: Add display monitor selector - #14196
Conversation
|
Fill out our PR template. |
e40df2d to
771078a
Compare
|
Please rebase instead of merging |
| m_display_surface = new DisplaySurface(); | ||
| const int monitor_index = Host::GetBaseIntSettingValue("UI", "DisplayMonitor", 0); | ||
| const QList<QScreen*> screens = QGuiApplication::screens(); | ||
| QScreen* target_screen = (monitor_index > 0 && monitor_index <= screens.size()) ? screens[monitor_index - 1] : QGuiApplication::primaryScreen(); |
There was a problem hiding this comment.
why -1 on the index? isn't the combobox index (which gets saved to the settings) zero-based?
There was a problem hiding this comment.
Because the default monitor takes up two slots in the combo box (default and monitor 1, in this case monitor 1 is my default). For example, monitor 2 is index 2 in the combo box but monitor 2 is in index 1 in what Qt returns in screens. That's why I subtract 1.
In my setup, my combobox looks like: Default, Monitor 1, Monitor 2. Default is always at index 0.
There was a problem hiding this comment.
ah, I missed the first addItem() on line 126. That explains it, thanks.
There was a problem hiding this comment.
I have not tested, and have only skim read the pr code, so bear with if I've misunderstood something.
Strictly speaking, PCSX2 should fullscreen on the same monitor that the window currently exists on. Nevertheless, having explicit control could be useful.
This PR, however, seems to always override that logic, as it forces the primary screen when no screen is specified.
Your handling of render to separate (where render_to_main is false) appears incorrect.
You aren’t adjusting the separate window position in the fullscreen case, instead seemingly adjusting it only when fullscreen is false.
You do, however, have added additional code to restore the window position. this is probably plastering over the above issue.
What platform have you tested your changes on?
| if (monitor_index > 0) | ||
| { | ||
| const QSize windowSize = m_display_surface->size(); | ||
| const QRect screenGeo = target_screen->availableGeometry(); | ||
| const QPoint center(screenGeo.x() + (screenGeo.width() - windowSize.width()) / 2, | ||
| screenGeo.y() + (screenGeo.height() - windowSize.height()) / 2); | ||
| m_display_surface->setGeometry(QRect(center, windowSize)); | ||
| } |
There was a problem hiding this comment.
Why are these changes made?
This section of code looks like it would only run when fullscreen is false
There was a problem hiding this comment.
When I render to a separate window, I want to make sure the window is at good position on the screen. I've had cases where the window would open on a different monitor and the title bar is off screen and I couldn't move it around. Originally, I had it open on the top left, that's when I discovered the issue.
Putting the separate window on the center of the screen makes sure this does not happen. I don't do this with Fullscreen because accidentally making the title bar off screen is not really an issue.
What I can add, once the window is open at a safe position, the user can move the window around and it can save that position for the session so every time a separate windowed window is launched it will be in that position.
I can also fix it so if it is windowed, using the main window, and already at the desired monitor, I can skip re-centering. I know it's at a safe location already.
There was a problem hiding this comment.
So restoreDisplayWindowGeometryFromConfig() ends up restoring the window offscreen?
Ideally restoreDisplayWindowGeometryFromConfig() or the associated Qt functions it calls would handle this.
What situations did you have the window appear offscreen?
If correcting the position does prove to be necessary, we should verify that the window is offscreen before repositioning it.
Code for saving and loading the window position already exists and is in use.
You, however, are currently overriding the restored position when you are centring the window.
There was a problem hiding this comment.
restoreDisplayWindowGeometryFromConfig() returns the saved screen coordinates on whatever monitor it was last on, right? What happens if my display set up changes? My ini files are on the cloud so I'm not always launching pcsx2 on the same device.
We can check if a window is off screen and recenter it when it is not. I think we can use Qt's contains function.
There was a problem hiding this comment.
restoreDisplayWindowGeometryFromConfig() will use Qt's restoreGeomatry() function.
Qt documents restoreGeometry() to adjust the window position if the saved geometry was offscreen.
See Qt's documentation.
As such, I would assume the window position would be corrected, but I must admit I've never tested this.
There was a problem hiding this comment.
I updated restoreDisplayWindowGeometryFromConfig() and tested restoreGeometry(). When I load from the config and the geometry is not within the target screen, I have to get the geometry from the target screen to pass it to restoreGeometry(), but if I do that, I have to saveGeometry() to get the target screen's geometry which requires me choosing a position anyways, and the center of the screen is still the safest position to save.
Anyways, I still do use restoreDisplayWindowGeometryFromConfig() and cleaned it up a bit. This way it also saves the position of the separate window when it matches the target screen within and across different sessions.
| if (monitor_index > 0) | ||
| { | ||
| const QSize windowSize = m_display_container->size(); | ||
| const QRect screenGeo = target_screen->availableGeometry(); | ||
| const QPoint center(screenGeo.x() + (screenGeo.width() - windowSize.width()) / 2, | ||
| screenGeo.y() + (screenGeo.height() - windowSize.height()) / 2); | ||
| m_display_container->setGeometry(QRect(center, windowSize)); | ||
| } |
| if (monitor_index > 0) | ||
| { | ||
| m_pre_game_main_window_geometry = saveGeometry(); | ||
| const QRect screenGeo = target_screen->availableGeometry(); | ||
| const QPoint center(screenGeo.x() + (screenGeo.width() - width()) / 2, | ||
| screenGeo.y() + (screenGeo.height() - height()) / 2); | ||
| move(center); | ||
| } |
| const int monitor_index = Host::GetBaseIntSettingValue("UI", "DisplayMonitor", 0); | ||
| const QList<QScreen*> screens = QGuiApplication::screens(); | ||
| QScreen* target_screen = (monitor_index > 0 && monitor_index <= screens.size()) ? screens[monitor_index - 1] : QGuiApplication::primaryScreen(); | ||
| m_display_surface->setScreen(target_screen); | ||
|
|
There was a problem hiding this comment.
Probably should only perform this when fullscreen is true.
I also found that setScreen() may also be ineffective.
positioning the window (for which you've adjusted/added code for) proved more reliable in my testing.
There was a problem hiding this comment.
You are right. I can move setScreen() in the fullscreen path.
I've only tested on windows.
|
Adding that, this doesn't work on Wayland. |
I don't have wayland to test it out right now, but what doesn't work specifically? |
The entire PR, in which to be fair is caused by limiations of Wayland itself (It doesn't allow the application itself to set their own window position). I haven't checked X11 but it theoretically can work there. |
Wayland seems to allow apps to pick the screen they show up on. (at least that's how it seems to behave under weston) |
|
So I should make sure to setScreen whenever possible for Wayland before any repositioning? Wayland will just ignore all the other repositioning logic but setScreen should, at least, put the window on the right screen. |
|
I created a Wayland VM to test this. When starting in fullscreen, Wayland respects the selected monitor. However, all windowed mode options are controlled by the compositor, which ignores the selected monitor. Even setScreen appears to be ignored. I tried a few workarounds, such as starting in fullscreen and then switching to windowed mode, but that did not work. It seems like this would require rendering at least one frame first before switching, which adds a fair amount of complexity just to support monitor selection in windowed mode on Wayland. |
Testing this myself, this seems to work for me under both WSL, and a manjaro VM |
|
can this be rebased? |
ddacf90 to
ef12fbb
Compare
Rebased and added a commit that fixed a bug when windowed, rendering to main, and changing the target screen. |
F0bes
left a comment
There was a problem hiding this comment.
So I'm testing on macOS.
It seems to work fine, but it's a little confusing.
Primary Monitor -> Goes to the primary monitor only when start fullscreen is enabled. When it's disabled, the window will not move positions.
Setting it to another monitor (not primary) will make the PCSX2 window move to that monitor when a game starts with start fullscreen enabled or disabled.
This also breaks the default behaviour of PCSX2. I think you might want to make another option. Something like Active Monitor or Current Monitor? I'm not good at names. This should make it so PCSX2 goes fullscreen on whatever monitor it is currently on. This is the current behaviour of master.
That's a bug I need to fix... I like your idea about the active/current monitor though. I feel like the primary monitor option is a bit redundant since the primary monitor technically has two entries on the monitor list. Would it be alright if I remove primary and add current? or should I have both? |
|
Just me two cents: |
Followed this logic. The refactoring I did for the target screen should fix any primary monitor bugs from before. I also changed it so the settings window opens on the same screen as the main window. The settings window still retains its previous location, but if the main window has moved to a different screen since the last launch, the settings window will recenter on the new screen. |
|
When does the monitor override actually gets enforced? during game startup? only when startin fullscreen? I tried every option but none of them works (but this could be because I'm on Linux using Wayland) |
game startup. It was a while ago when I tested using a Linux VM using Wayland. It was working when starting fullscreen, but not windowed. Did that stop working? |
Neither are working for me, it used to work the last few times i tested the PR |
Adds a "Display Monitor" dropdown to Interface settings, allowing the user to choose which monitor a game launches on. Options include the current monitor, the primary monitor, or any connected display by name. The selected monitor is respected when launching in fullscreen, in a separate window, or when rendering to the main window. In the render-to-main case, the main window moves to the target screen before the game starts and restores to its previous position afterward. Window geometry is saved and restored independently for each screen.
The settings menu retains its location on screen even after closing. If the main window has moved to a different screen since the settings window was last opened, it recenters on that screen instead of appearing on the previous one.
f17766a to
86422df
Compare
Starting in fullscreen should be working now with Wayland rendering to main or a separate window. The problem was I removed the setScreen() calls. I didn't need them for Windows / display surfaces anymore but display containers / Wayland still needed them. Windowed screens still won't work since Wayland doesn't give control over repositioning those. I squashed most of my commits since most were bug fixes. |
Probably worth adding a warning notice about those wayland issues on the setting's description box. |
Added a description for the display monitor setting and included a warning that Start Fullscreen needs to be enabled for Linux Wayland.
I have added a description to the setting and added a warning for wayland. |
| const QRect screenGeo = target_screen->availableGeometry(); | ||
| move(screenGeo.x() + (screenGeo.width() - width()) / 2, | ||
| screenGeo.y() + (screenGeo.height() - height()) / 2); |
There was a problem hiding this comment.
Is it possible that the target screen geometry is smaller than the current window geometry? e.g. moving to a smaller screen. In that case, should we clamp width and height to the available screen geometry?
I suggest writing a helper function for this, since the same logic is used two more times in restoreDisplayWindowGeometryFromConfig().
There was a problem hiding this comment.
Good catch. We have to clamp slightly a little bit more than the available screen geometry though. availableGeometry() doesn't take to account the title bar, at least on windows, so it's possible it gets cut off. I need to use frameGeometry() to subtract the difference.
| if (dlg->screen() != screen()) | ||
| { | ||
| const QRect screenGeo = screen()->availableGeometry(); | ||
| dlg->move(screenGeo.center() - QPoint(dlg->frameGeometry().width() / 2, dlg->frameGeometry().height() / 2)); |
There was a problem hiding this comment.
maybe slightly easier to use: dlg->frameGeometry().moveCenter(screenGeo.center())
Also it's probably(?) more efficient to do this before calling show(), unless the dialog is required to be visible to get the correct frame geometry.
There was a problem hiding this comment.
I'll try it and see it if it works.
There was a problem hiding this comment.
dlg->frameGeometry().moveCenter(screenGeo.center())
dlg->frameGeometry() returns a copy of Qrect of the settings window so calling moveCenter() on it won't move the settings window directly.
unless the dialog is required to be visible to get the correct frame geometry
This part is correct, but it's more about the correct frame geometry than visibility. I looked into this a bit more. Before show() is called, frameGeometry() pretty much equals geometry() because the windows manager hasn't decorated the final window so the final size hasn't been determined yet. So if show() is called after frameGeometry(), frameGeometry() will be missing the title bar and borders and our calculations will be wrong. That's why I had to move some showNormals() to get clamping to work correctly.
There was a problem hiding this comment.
dlg->frameGeometry().moveCenter(screenGeo.center())
dlg->frameGeometry() returns a copy of Qrect of the settings window so calling moveCenter() on it won't move the settings window directly.
The line I wrote was just meant as a hint to convey my idea, it's not the full code. Of course you still need to call move() or setGeometry().
| m_ui.displayMonitor->addItem(tr("Monitor %1: %2").arg(i + 1).arg(screens[i]->name())); | ||
| m_ui.displayMonitor->setCurrentIndex(Host::GetBaseIntSettingValue("UI", "DisplayMonitor", 0)); | ||
| connect(m_ui.displayMonitor, &QComboBox::currentIndexChanged, this, [](int index) { | ||
| Host::SetBaseIntSettingValue("UI", "DisplayMonitor", index); |
There was a problem hiding this comment.
Using the combobox index as persistent screen identifier seems unreliable to me. The documentation of QGuiApplication::screens() doesn't guarantee a consistent ordering of the returned list. Even if the order was stable, what happens if I have three screens connected, select the second screen, then unplug it? Won't the wrong screen be used then?
There was a problem hiding this comment.
Currently if an invalid index is used, the current monitor will be used so we don't have to worry about crashes. There are QT signals for display add and remove so we can have the interface settings update in real time whenever a monitor is connected or disconnected. I haven't pushed that yet, but it seems to be working fine so far.
But it is true, there is no guarantee the ordering will stable. We can use the names of the displays... but that won't work in case someone has multiple monitors of the same name. The monitors have to be sorted in a consistent, deterministic order. My idea right now is geometry position, but I haven't tested yet.
There was a problem hiding this comment.
Currently if an invalid index is used, the current monitor will be used so we don't have to worry about crashes.
I wasn't worried about crashing (the code is already robust against that) but about the wrong monitor being used, e.g., when the index is still valid but refers to a different monitor because the screens() list changed.
We can use the names of the displays... but that won't work in case someone has multiple monitors of the same name. The monitors have to be sorted in a consistent, deterministic order. My idea right now is geometry position, but I haven't tested yet.
There is QScreen::serialNumber but I haven't checked what it returns and whether it's reliable.
There was a problem hiding this comment.
QScreen::serialNumber doesn't work consistently so I went with sorting displays by geometry positions, so the order is always consistent. If a user rearranges their monitors in their OS, a game may launch on an unexpected display, but the interface settings will still reflect the correct index, and they can correct it from there.
…geometry positions Qt signals have been added so the interface settings are now updated in real time when a display is removed or added. If an index is no longer valid, "Current Monitor" is set. Screens are now sorted by their geometry positions. QGuiApplication::screens() can't guarantee screen order so there's always a possibility the indexes on the interface setting does not match the indexes of the screen when a game launches. Sorting helps keep the order consistent.
When a window's saved geometry was recorded on a larger monitor, restoring it on a smaller screen could place it partially or fully off-screen. A centeredGeometry() helper now centers the window on the target screen and shrinks it to fit within the available area, accounting for frame offsets so the title bar is never clipped.
Description of Changes
Adds a display monitor selector to the Qt frontend, allowing users to choose which monitor PCSX2 opens on. Also restores the main window position when a game shuts down.
Rationale Behind Changes
Users with multi-monitor setups previously had no control over which display PCSX2 opened on. This change provides explicit control and ensures the window position is remembered per monitor.
Suggested Testing Steps
Did you use AI to help find, test, or implement this issue or feature?
Yes. I am unfamiliar with Qt, so I used AI to help learn it during implementation.