From aaed4a4983696557fc82a5b0ea55d35ed73accc0 Mon Sep 17 00:00:00 2001 From: TheLastRar Date: Fri, 21 Nov 2025 22:35:19 +0000 Subject: [PATCH] Qt: Don't contain display surface when entering fullscreen/separate on Windows --- pcsx2-qt/DisplayWidget.cpp | 60 ++++++++++-- pcsx2-qt/DisplayWidget.h | 7 +- pcsx2-qt/MainWindow.cpp | 184 ++++++++++++++++++++++++++++--------- pcsx2-qt/MainWindow.h | 4 +- pcsx2-qt/QtUtils.cpp | 35 +++++++ pcsx2-qt/QtUtils.h | 2 + 6 files changed, 239 insertions(+), 53 deletions(-) diff --git a/pcsx2-qt/DisplayWidget.cpp b/pcsx2-qt/DisplayWidget.cpp index 7cffdffcd8..62f798bcf4 100644 --- a/pcsx2-qt/DisplayWidget.cpp +++ b/pcsx2-qt/DisplayWidget.cpp @@ -133,7 +133,7 @@ void DisplaySurface::handleCloseEvent(QCloseEvent* event) // In the latter case, it's going to destroy us, so don't let Qt do it first. // Treat a close event while fullscreen as an exit, that way ALT+F4 closes PCSX2, // rather than just the game. - if (QtHost::IsVMValid() && !isActuallyFullscreen()) + if (QtHost::IsVMValid() && !isFullScreen()) { QMetaObject::invokeMethod(g_main_window, "requestShutdown", Q_ARG(bool, true), Q_ARG(bool, true), Q_ARG(bool, false)); @@ -147,10 +147,44 @@ void DisplaySurface::handleCloseEvent(QCloseEvent* event) event->ignore(); } -bool DisplaySurface::isActuallyFullscreen() const +bool DisplaySurface::isFullScreen() const { - // DisplaySurface is always in a container, so we need to check parent window - return parent()->windowState() & Qt::WindowFullScreen; + // DisplaySurface may be in a container + return (parent() ? parent()->windowState() : windowState()) & Qt::WindowFullScreen; +} + +void DisplaySurface::setFocus() +{ + if (m_container) + m_container->setFocus(); + else + requestActivate(); +} + +QByteArray DisplaySurface::saveGeometry() const +{ + if (m_container) + return m_container->saveGeometry(); + else + { + // QWindow lacks saveGeometry, so create a dummy widget and copy geometry across. + QWidget dummy = QWidget(); + dummy.setGeometry(geometry()); + return dummy.saveGeometry(); + } +} + +void DisplaySurface::restoreGeometry(const QByteArray& geometry) +{ + if (m_container) + m_container->restoreGeometry(geometry); + else + { + // QWindow lacks restoreGeometry, so create a dummy widget and copy geometry across. + QWidget dummy = QWidget(); + dummy.restoreGeometry(geometry); + setGeometry(dummy.geometry()); + } } void DisplaySurface::updateCenterPos() @@ -393,10 +427,18 @@ bool DisplaySurface::event(QEvent* event) return event->isAccepted(); case QEvent::Move: - { updateCenterPos(); return true; - } + + // These events only work on the top level control. + // Which is this container when render to seperate or fullscreen is active (Windows). + case QEvent::Close: + handleCloseEvent(static_cast(event)); + return true; + case QEvent::WindowStateChange: + if (static_cast(event)->oldState() & Qt::WindowMinimized) + emit windowRestoredEvent(); + return false; default: return QWindow::event(event); @@ -418,7 +460,7 @@ bool DisplaySurface::eventFilter(QObject* object, QEvent* event) return true; // These events only work on the top level control. - // Which is this container when render to seperate or fullscreen is active. + // Which is this container when render to seperate or fullscreen is active (Non-Windows). case QEvent::Close: handleCloseEvent(static_cast(event)); return true; @@ -427,8 +469,8 @@ bool DisplaySurface::eventFilter(QObject* object, QEvent* event) emit windowRestoredEvent(); return false; - case QEvent::ChildRemoved: - if (static_cast(event)->child() == m_container) + case QEvent::ChildWindowRemoved: + if (static_cast(event)->child() == this) { object->removeEventFilter(this); m_container = nullptr; diff --git a/pcsx2-qt/DisplayWidget.h b/pcsx2-qt/DisplayWidget.h index 00ec2789b6..9c147097ae 100644 --- a/pcsx2-qt/DisplayWidget.h +++ b/pcsx2-qt/DisplayWidget.h @@ -30,6 +30,12 @@ public: void updateRelativeMode(bool enabled); void updateCursor(bool hidden); + bool isFullScreen() const; + void setFocus(); + + QByteArray saveGeometry() const; + void restoreGeometry(const QByteArray& geometry); + Q_SIGNALS: void windowResizedEvent(int width, int height, float scale); void windowRestoredEvent(); @@ -47,7 +53,6 @@ private Q_SLOTS: void onResizeDebounceTimer(); private: - bool isActuallyFullscreen() const; void updateCenterPos(); QPoint m_relative_mouse_start_pos{}; diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 13d55c3cd0..52d5352f33 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -100,6 +100,13 @@ static quint32 s_current_running_crc; static bool s_record_on_start = false; static QString s_path_to_recording_for_record_on_start; +// DX cannot fullscreen when the display surface is in a container. +// QWindow, however, seems to lack CSD under wayland, so needs the container. +// MAC is unknown +#ifdef _WIN32 +#define DISPLAY_SURFACE_WINDOW +#endif + MainWindow::MainWindow() { pxAssert(!g_main_window); @@ -1008,10 +1015,15 @@ void MainWindow::updateWindowTitle() if (windowTitle() != main_title) setWindowTitle(main_title); - if (m_display_container && !isRenderingToMain()) + if (m_display_surface && !isRenderingToMain()) { +#ifdef DISPLAY_SURFACE_WINDOW + if (m_display_surface->title() != display_title) + m_display_surface->setTitle(display_title); +#else if (m_display_container->windowTitle() != display_title) m_display_container->setWindowTitle(display_title); +#endif } if (g_log_window) @@ -1040,7 +1052,13 @@ void MainWindow::updateWindowState(bool force_visible) // Update the display widget too if rendering separately. if (m_display_surface && !isRenderingToMain()) + { +#ifdef DISPLAY_SURFACE_WINDOW + QtUtils::SetWindowResizeable(m_display_surface, resizeable); +#else QtUtils::SetWindowResizeable(m_display_container, resizeable); +#endif + } } void MainWindow::setProgressBar(int current, int total) @@ -1075,12 +1093,12 @@ bool MainWindow::isRenderingFullscreen() const if (!MTGS::IsOpen() || !m_display_surface) return false; - return m_display_container->isFullScreen(); + return m_display_surface->isFullScreen(); } bool MainWindow::isRenderingToMain() const { - return (m_display_surface && m_ui.mainContainer->indexOf(m_display_container) == 1); + return (m_display_container && m_ui.mainContainer->indexOf(m_display_container) == 1); } bool MainWindow::shouldHideMouseCursor() const @@ -1108,16 +1126,25 @@ bool MainWindow::shouldMouseLock() const if (m_display_created == false || m_display_surface == nullptr) return false; - bool windowsHidden = (!g_debugger_window || g_debugger_window->isHidden()) && - (!m_controller_settings_window || m_controller_settings_window->isHidden()) && - (!m_settings_window || m_settings_window->isHidden()); + const bool windowsHidden = (!g_debugger_window || g_debugger_window->isHidden()) && + (!m_controller_settings_window || m_controller_settings_window->isHidden()) && + (!m_settings_window || m_settings_window->isHidden()); - auto* displayWindow = isRenderingToMain() ? window() : m_display_container->window(); - - if (displayWindow == nullptr) + if (!windowsHidden) return false; - return windowsHidden && (displayWindow->isActiveWindow() || displayWindow->isFullScreen()); +#ifdef DISPLAY_SURFACE_WINDOW + if (isRenderingToMain()) + { + const auto* displayWindow = window(); + return displayWindow ? (displayWindow->isActiveWindow() || displayWindow->isFullScreen()) : false; + } + else + return m_display_surface->isActive() || m_display_surface->isFullScreen(); +#else + const auto* displayWindow = isRenderingToMain() ? window() : m_display_container->window(); + return displayWindow ? (displayWindow->isActiveWindow() || displayWindow->isFullScreen()) : false; +#endif } bool MainWindow::shouldAbortForMemcardBusy(const VMLock& lock) @@ -1180,7 +1207,7 @@ void MainWindow::switchToEmulationView() g_emu_thread->setVMPaused(false); if (m_display_surface) - m_display_container->setFocus(); + m_display_surface->setFocus(); } void MainWindow::refreshGameList(bool invalidate_cache, bool popup_on_error) @@ -2129,7 +2156,7 @@ void MainWindow::onVMResumed() if (m_display_surface) { updateDisplayWidgetCursor(); - m_display_container->setFocus(); + m_display_surface->setFocus(); } } @@ -2449,21 +2476,25 @@ std::optional MainWindow::acquireRenderWindow(bool recreate_window, if (!is_fullscreen && !is_rendering_to_main) saveDisplayWindowGeometryToConfig(); +#ifdef DISPLAY_SURFACE_WINDOW + auto* displayWindow = m_display_surface; +#else + auto* displayWindow = m_display_container; +#endif if (fullscreen) - { - m_display_container->showFullScreen(); - } + displayWindow->showFullScreen(); else { + // Needs to exit fullscreen before resizing + displayWindow->showNormal(); if (m_is_temporarily_windowed && g_emu_thread->shouldRenderToMain()) - m_display_container->setGeometry(geometry()); + displayWindow->setGeometry(geometry()); else restoreDisplayWindowGeometryFromConfig(); - m_display_container->showNormal(); } updateDisplayWidgetCursor(); - m_display_container->setFocus(); + m_display_surface->setFocus(); updateWindowState(); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); @@ -2500,7 +2531,7 @@ std::optional MainWindow::acquireRenderWindow(bool recreate_window, updateWindowState(); updateDisplayWidgetCursor(); - m_display_container->setFocus(); + m_display_surface->setFocus(); return wi; } @@ -2517,9 +2548,14 @@ void MainWindow::createDisplayWidget(bool fullscreen, bool render_to_main) m_display_surface = new DisplaySurface(); if (fullscreen || !render_to_main) { +#ifdef DISPLAY_SURFACE_WINDOW + m_display_surface->setTitle(windowTitle()); + m_display_surface->setIcon(windowIcon()); +#else m_display_container = m_display_surface->createWindowContainer(); m_display_container->setWindowTitle(windowTitle()); m_display_container->setWindowIcon(windowIcon()); +#endif } else { @@ -2528,20 +2564,38 @@ void MainWindow::createDisplayWidget(bool fullscreen, bool render_to_main) if (fullscreen) { - // On Wayland, while move/restoreGeometry can't position the window, it can influence which screen they show up on + // On Wayland, while move/restoreGeometry can't position the window, it can influence which screen they show up on. + // Other platforms can position windows fine, but the only thing that matters here is the screen. + +#ifdef DISPLAY_SURFACE_WINDOW + if (isVisible() && g_emu_thread->shouldRenderToMain()) + m_display_surface->setFramePosition(pos()); + else + restoreDisplayWindowGeometryFromConfig(); + m_display_surface->showFullScreen(); +#else if (isVisible() && g_emu_thread->shouldRenderToMain()) m_display_container->move(pos()); else restoreDisplayWindowGeometryFromConfig(); m_display_container->showFullScreen(); +#endif } else if (!render_to_main) { +#ifdef DISPLAY_SURFACE_WINDOW + if (m_is_temporarily_windowed && g_emu_thread->shouldRenderToMain()) + m_display_surface->setGeometry(geometry()); + else + restoreDisplayWindowGeometryFromConfig(); + m_display_surface->showNormal(); +#else if (m_is_temporarily_windowed && g_emu_thread->shouldRenderToMain()) m_display_container->setGeometry(geometry()); else restoreDisplayWindowGeometryFromConfig(); m_display_container->showNormal(); +#endif } else { @@ -2570,12 +2624,21 @@ void MainWindow::displayResizeRequested(qint32 width, qint32 height) width = static_cast(std::max(static_cast(std::lroundf(static_cast(width) / dpr)), 1)); height = static_cast(std::max(static_cast(std::lroundf(static_cast(height) / dpr)), 1)); +#ifdef DISPLAY_SURFACE_WINDOW + if (!m_display_container) + { + // no parent - rendering to separate window. easy. + QtUtils::ResizePotentiallyFixedSizeWindow(m_display_surface, width, height); + return; + } +#else if (!m_display_container->parent()) { // no parent - rendering to separate window. easy. QtUtils::ResizePotentiallyFixedSizeWindow(m_display_container, width, height); return; } +#endif // we are rendering to the main window. we have to add in the extra height from the toolbar/status bar. const s32 extra_height = this->height() - m_display_container->height(); @@ -2608,7 +2671,7 @@ void MainWindow::destroyDisplayWidget(bool show_game_list) if (!m_display_surface) return; - if (!isRenderingFullscreen() && !isRenderingToMain()) + if (!m_display_surface->isFullScreen() && !isRenderingToMain()) saveDisplayWindowGeometryToConfig(); if (isRenderingToMain()) @@ -2622,12 +2685,18 @@ void MainWindow::destroyDisplayWidget(bool show_game_list) } } - // displau surface is always in a container - pxAssert(m_display_container != nullptr); - m_display_container->deleteLater(); - m_display_container = nullptr; - // m_display_surface will be destroyed by the container's dtor - m_display_surface = nullptr; + if (m_display_container) + { + m_display_container->deleteLater(); + m_display_container = nullptr; + // m_display_surface will be destroyed by the container's dtor + m_display_surface = nullptr; + } + else + { + m_display_surface->deleteLater(); + m_display_surface = nullptr; + } updateDisplayRelatedActions(false, false, false); } @@ -2639,14 +2708,6 @@ void MainWindow::updateDisplayWidgetCursor() m_display_surface->updateCursor(s_vm_valid && !s_vm_paused && shouldHideMouseCursor()); } -void MainWindow::focusDisplayWidget() -{ - if (!m_display_surface || centralWidget() != m_display_container) - return; - - m_display_container->setFocus(); -} - void MainWindow::setupMouseMoveHandler() { auto mouse_cb_fn = [](int x, int y) { @@ -2677,7 +2738,11 @@ void MainWindow::checkMousePosition(int x, int y) // logical (DIP) frame rect const QSize logicalSize = displayWindow->size(); +#ifdef DISPLAY_SURFACE_WINDOW + const QPoint logicalPosition = isRenderingToMain() ? (displayWindow->position() + displayWindow->parent()->position()) : displayWindow->position(); +#else const QPoint logicalPosition = displayWindow->position() + displayWindow->parent()->position(); +#endif // The offset to the origin of the current screen is in device-independent pixels while the origin itself is native! // The logicalPosition is the sum of these two values, so we need to separate them and only scale the offset @@ -2708,13 +2773,13 @@ void MainWindow::checkMousePosition(int x, int y) void MainWindow::saveDisplayWindowGeometryToConfig() { - if (m_display_container->windowState() & Qt::WindowFullScreen) + if (m_display_surface->isFullScreen()) { // if we somehow ended up here, don't save the fullscreen state to the config return; } - const QByteArray geometry = m_display_container->saveGeometry(); + const QByteArray geometry = m_display_surface->saveGeometry(); const QByteArray geometry_b64 = geometry.toBase64(); const std::string old_geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry"); if (old_geometry_b64 != geometry_b64.constData()) @@ -2730,15 +2795,23 @@ void MainWindow::restoreDisplayWindowGeometryFromConfig() const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64)); if (!geometry.isEmpty()) { - m_display_container->restoreGeometry(geometry); + m_display_surface->restoreGeometry(geometry); // make sure we're not loading a dodgy config which had fullscreen set... +#ifdef DISPLAY_SURFACE_WINDOW + m_display_surface->setWindowStates(m_display_surface->windowStates() & ~(Qt::WindowFullScreen | Qt::WindowActive)); +#else m_display_container->setWindowState(m_display_container->windowState() & ~(Qt::WindowFullScreen | Qt::WindowActive)); +#endif } else { // default size +#ifdef DISPLAY_SURFACE_WINDOW + m_display_surface->resize(640, 480); +#else m_display_container->resize(640, 480); +#endif } } @@ -3308,7 +3381,7 @@ MainWindow::VMLock MainWindow::pauseAndLockVM() g_emu_thread->setFullscreen(false, false); // Process events untill both EmuThread and Qt have finished exiting fullscreen - while (QtHost::IsVMValid() && (g_emu_thread->isFullscreen() || m_display_container->isFullScreen())) + while (QtHost::IsVMValid() && (g_emu_thread->isFullscreen() || m_display_surface->isFullScreen())) { QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } @@ -3320,7 +3393,27 @@ MainWindow::VMLock MainWindow::pauseAndLockVM() g_main_window->raise(); g_main_window->activateWindow(); - return VMLock(m_display_container, was_paused, was_fullscreen); +#ifdef DISPLAY_SURFACE_WINDOW + if (!m_display_container) + { + // Create a temporary parent for the dialog. + QWidget* dialog_parent = new QWidget(); + dialog_parent->setAttribute(Qt::WA_NativeWindow); + QWindow* window_handle = dialog_parent->windowHandle(); + + // Set the transient parent to the display surface. + // This will position the dialog_parent over the display surface (and thus so will any dialogs) + // and also enforces the focus lock of modal dialogs against the display surface. + // This works even without showing the dialog_parent window. + window_handle->setTransientParent(m_display_surface); + + return VMLock(dialog_parent, was_paused, was_fullscreen, true); + } + else + return VMLock(m_display_container, was_paused, was_fullscreen, false); +#else + return VMLock(m_display_container, was_paused, was_fullscreen, false); +#endif } void MainWindow::rescanFile(const std::string& path) @@ -3328,11 +3421,12 @@ void MainWindow::rescanFile(const std::string& path) m_game_list_widget->rescanFile(path); } -MainWindow::VMLock::VMLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen) +MainWindow::VMLock::VMLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen, bool owns_parent) : m_dialog_parent(dialog_parent) , m_has_lock(true) , m_was_paused(was_paused) , m_was_fullscreen(was_fullscreen) + , m_owns_dialog_parent(owns_parent) { QtHost::LockVMWithDialog(); } @@ -3342,11 +3436,13 @@ MainWindow::VMLock::VMLock(VMLock&& lock) , m_has_lock(lock.m_has_lock) , m_was_paused(lock.m_was_paused) , m_was_fullscreen(lock.m_was_fullscreen) + , m_owns_dialog_parent(lock.m_owns_dialog_parent) { lock.m_dialog_parent = nullptr; lock.m_has_lock = false; lock.m_was_paused = true; lock.m_was_fullscreen = false; + lock.m_owns_dialog_parent = false; } MainWindow::VMLock::~VMLock() @@ -3354,6 +3450,12 @@ MainWindow::VMLock::~VMLock() if (m_has_lock) QtHost::UnlockVMWithDialog(); + if (m_owns_dialog_parent && m_dialog_parent) + { + m_dialog_parent->deleteLater(); + m_dialog_parent = nullptr; + } + if (m_was_fullscreen) { g_main_window->m_is_temporarily_windowed = false; diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 2f5bbff616..49c240b6f3 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -65,13 +65,14 @@ public: void cancelResume(); private: - VMLock(QWidget* dialog_parent, bool was_paused, bool was_exclusive_fullscreen); + VMLock(QWidget* dialog_parent, bool was_paused, bool was_exclusive_fullscreen, bool owns_parent); friend MainWindow; QWidget* m_dialog_parent; bool m_has_lock; bool m_was_paused; bool m_was_fullscreen; + bool m_owns_dialog_parent; }; /// Default filter for opening a file. @@ -138,7 +139,6 @@ private Q_SLOTS: void displayResizeRequested(qint32 width, qint32 height); void mouseModeRequested(bool relative_mode, bool hide_cursor); void releaseRenderWindow(); - void focusDisplayWidget(); void setupMouseMoveHandler(); void onGameListRefreshComplete(); void onGameListRefreshProgress(const QString& status, int current, int total); diff --git a/pcsx2-qt/QtUtils.cpp b/pcsx2-qt/QtUtils.cpp index 4f41681351..e6b900ec33 100644 --- a/pcsx2-qt/QtUtils.cpp +++ b/pcsx2-qt/QtUtils.cpp @@ -366,6 +366,25 @@ namespace QtUtils } } + void SetWindowResizeable(QWindow* window, bool resizeable) + { + if (resizeable) + { + // Min/max numbers come from uic. + window->setMinimumWidth(1); + window->setMinimumHeight(1); + window->setMaximumWidth(16777215); + window->setMaximumHeight(16777215); + } + else + { + window->setMinimumWidth(window->width()); + window->setMinimumHeight(window->height()); + window->setMaximumWidth(window->width()); + window->setMaximumHeight(window->height()); + } + } + void ResizePotentiallyFixedSizeWindow(QWidget* widget, int width, int height) { width = std::max(width, 1); @@ -376,6 +395,22 @@ namespace QtUtils widget->resize(width, height); } + void ResizePotentiallyFixedSizeWindow(QWindow* window, int width, int height) + { + width = std::max(width, 1); + height = std::max(height, 1); + + if (window->minimumHeight() == window->maximumHeight()) + { + window->setMinimumWidth(width); + window->setMinimumHeight(height); + window->setMaximumWidth(width); + window->setMaximumHeight(height); + } + + window->resize(width, height); + } + QString AbstractItemModelToCSV(QAbstractItemModel* model, int role, bool useQuotes) { QString csv; diff --git a/pcsx2-qt/QtUtils.h b/pcsx2-qt/QtUtils.h index f901bc840b..a1af6c4bf0 100644 --- a/pcsx2-qt/QtUtils.h +++ b/pcsx2-qt/QtUtils.h @@ -102,9 +102,11 @@ namespace QtUtils /// Changes whether a window is resizable. void SetWindowResizeable(QWidget* widget, bool resizeable); + void SetWindowResizeable(QWindow* window, bool resizeable); /// Adjusts the fixed size for a window if it's not resizeable. void ResizePotentiallyFixedSizeWindow(QWidget* widget, int width, int height); + void ResizePotentiallyFixedSizeWindow(QWindow* window, int width, int height); /// Returns the common window info structure for a Qt Window/Widget. template