duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

displaywidget.cpp (13683B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "displaywidget.h"
      5 #include "mainwindow.h"
      6 #include "qthost.h"
      7 #include "qtutils.h"
      8 
      9 #include "core/fullscreen_ui.h"
     10 
     11 #include "util/imgui_manager.h"
     12 
     13 #include "common/assert.h"
     14 #include "common/bitutils.h"
     15 #include "common/log.h"
     16 
     17 #include <QtCore/QDebug>
     18 #include <QtGui/QGuiApplication>
     19 #include <QtGui/QKeyEvent>
     20 #include <QtGui/QScreen>
     21 #include <QtGui/QWindow>
     22 #include <QtGui/QWindowStateChangeEvent>
     23 #include <cmath>
     24 
     25 #if !defined(_WIN32) && !defined(APPLE)
     26 #include <qpa/qplatformnativeinterface.h>
     27 #endif
     28 
     29 #ifdef _WIN32
     30 #include "common/windows_headers.h"
     31 #endif
     32 
     33 Log_SetChannel(DisplayWidget);
     34 
     35 DisplayWidget::DisplayWidget(QWidget* parent) : QWidget(parent)
     36 {
     37   // We want a native window for both D3D and OpenGL.
     38   setAutoFillBackground(false);
     39   setAttribute(Qt::WA_NativeWindow, true);
     40   setAttribute(Qt::WA_NoSystemBackground, true);
     41   setAttribute(Qt::WA_PaintOnScreen, true);
     42   setAttribute(Qt::WA_KeyCompression, false);
     43   setFocusPolicy(Qt::StrongFocus);
     44   setMouseTracking(true);
     45 }
     46 
     47 DisplayWidget::~DisplayWidget() = default;
     48 
     49 int DisplayWidget::scaledWindowWidth() const
     50 {
     51   return std::max(
     52     static_cast<int>(std::ceil(static_cast<qreal>(width()) * QtUtils::GetDevicePixelRatioForWidget(this))), 1);
     53 }
     54 
     55 int DisplayWidget::scaledWindowHeight() const
     56 {
     57   return std::max(
     58     static_cast<int>(std::ceil(static_cast<qreal>(height()) * QtUtils::GetDevicePixelRatioForWidget(this))), 1);
     59 }
     60 
     61 std::optional<WindowInfo> DisplayWidget::getWindowInfo()
     62 {
     63   std::optional<WindowInfo> ret(QtUtils::GetWindowInfoForWidget(this));
     64   if (ret.has_value())
     65   {
     66     m_last_window_width = ret->surface_width;
     67     m_last_window_height = ret->surface_height;
     68     m_last_window_scale = ret->surface_scale;
     69   }
     70   return ret;
     71 }
     72 
     73 void DisplayWidget::updateRelativeMode(bool enabled)
     74 {
     75 #ifdef _WIN32
     76   // prefer ClipCursor() over warping movement when we're using raw input
     77   bool clip_cursor = enabled && InputManager::IsUsingRawInput();
     78   if (m_relative_mouse_enabled == enabled && m_clip_mouse_enabled == clip_cursor)
     79     return;
     80 
     81   INFO_LOG("updateRelativeMode(): relative={}, clip={}", enabled ? "yes" : "no", clip_cursor ? "yes" : "no");
     82 
     83   if (!clip_cursor && m_clip_mouse_enabled)
     84   {
     85     m_clip_mouse_enabled = false;
     86     ClipCursor(nullptr);
     87   }
     88 #else
     89   if (m_relative_mouse_enabled == enabled)
     90     return;
     91 
     92   INFO_LOG("updateRelativeMode(): relative={}", enabled ? "yes" : "no");
     93 #endif
     94 
     95   if (enabled)
     96   {
     97     m_relative_mouse_enabled = true;
     98 #ifdef _WIN32
     99     m_clip_mouse_enabled = clip_cursor;
    100 #endif
    101     m_relative_mouse_start_pos = QCursor::pos();
    102     updateCenterPos();
    103     grabMouse();
    104   }
    105   else if (m_relative_mouse_enabled)
    106   {
    107     m_relative_mouse_enabled = false;
    108     QCursor::setPos(m_relative_mouse_start_pos);
    109     releaseMouse();
    110   }
    111 }
    112 
    113 void DisplayWidget::updateCursor(bool hidden)
    114 {
    115   if (m_cursor_hidden == hidden)
    116     return;
    117 
    118   m_cursor_hidden = hidden;
    119   if (hidden)
    120   {
    121     DEV_LOG("updateCursor(): Cursor is now hidden");
    122     setCursor(Qt::BlankCursor);
    123   }
    124   else
    125   {
    126     DEV_LOG("updateCursor(): Cursor is now shown");
    127     unsetCursor();
    128   }
    129 }
    130 
    131 void DisplayWidget::handleCloseEvent(QCloseEvent* event)
    132 {
    133   event->ignore();
    134 
    135   // Closing the separate widget will either cancel the close, or trigger shutdown.
    136   // In the latter case, it's going to destroy us, so don't let Qt do it first.
    137   // Treat a close event while fullscreen as an exit, that way ALT+F4 closes DuckStation,
    138   // rather than just the game.
    139   if (QtHost::IsSystemValid() && !isActuallyFullscreen())
    140   {
    141     QMetaObject::invokeMethod(g_main_window, "requestShutdown", Qt::QueuedConnection, Q_ARG(bool, true),
    142                               Q_ARG(bool, true), Q_ARG(bool, false));
    143   }
    144   else
    145   {
    146     QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection);
    147   }
    148 }
    149 
    150 void DisplayWidget::destroy()
    151 {
    152   m_destroying = true;
    153 
    154 #ifdef _WIN32
    155   if (m_clip_mouse_enabled)
    156     ClipCursor(nullptr);
    157 #endif
    158 
    159 #ifdef __APPLE__
    160   // See Qt documentation, entire application is in full screen state, and the main
    161   // window will get reopened fullscreen instead of windowed if we don't close the
    162   // fullscreen window first.
    163   if (isActuallyFullscreen())
    164     close();
    165 #endif
    166   deleteLater();
    167 }
    168 
    169 bool DisplayWidget::isActuallyFullscreen() const
    170 {
    171   // I hate you QtWayland... have to check the parent, not ourselves.
    172   QWidget* container = qobject_cast<QWidget*>(parent());
    173   return container ? container->isFullScreen() : isFullScreen();
    174 }
    175 
    176 void DisplayWidget::updateCenterPos()
    177 {
    178 #ifdef _WIN32
    179   if (m_clip_mouse_enabled)
    180   {
    181     RECT rc;
    182     if (GetWindowRect(reinterpret_cast<HWND>(winId()), &rc))
    183       ClipCursor(&rc);
    184   }
    185   else if (m_relative_mouse_enabled)
    186   {
    187     RECT rc;
    188     if (GetWindowRect(reinterpret_cast<HWND>(winId()), &rc))
    189     {
    190       m_relative_mouse_center_pos.setX(((rc.right - rc.left) / 2) + rc.left);
    191       m_relative_mouse_center_pos.setY(((rc.bottom - rc.top) / 2) + rc.top);
    192       SetCursorPos(m_relative_mouse_center_pos.x(), m_relative_mouse_center_pos.y());
    193     }
    194   }
    195 #else
    196   if (m_relative_mouse_enabled)
    197   {
    198     // we do a round trip here because these coordinates are dpi-unscaled
    199     m_relative_mouse_center_pos = mapToGlobal(QPoint((width() + 1) / 2, (height() + 1) / 2));
    200     QCursor::setPos(m_relative_mouse_center_pos);
    201     m_relative_mouse_center_pos = QCursor::pos();
    202   }
    203 #endif
    204 }
    205 
    206 QPaintEngine* DisplayWidget::paintEngine() const
    207 {
    208   return nullptr;
    209 }
    210 
    211 bool DisplayWidget::event(QEvent* event)
    212 {
    213   switch (event->type())
    214   {
    215     case QEvent::KeyPress:
    216     case QEvent::KeyRelease:
    217     {
    218       const QKeyEvent* key_event = static_cast<QKeyEvent*>(event);
    219 
    220       if (ImGuiManager::WantsTextInput() && key_event->type() == QEvent::KeyPress)
    221       {
    222         // Don't forward backspace characters. We send the backspace as a normal key event,
    223         // so if we send the character too, it double-deletes.
    224         QString text(key_event->text());
    225         text.remove(QChar('\b'));
    226         if (!text.isEmpty())
    227           emit windowTextEntered(text);
    228       }
    229 
    230       if (key_event->isAutoRepeat())
    231         return true;
    232 
    233       // For some reason, Windows sends "fake" key events.
    234       // Scenario: Press shift, press F1, release shift, release F1.
    235       // Events: Shift=Pressed, F1=Pressed, Shift=Released, **F1=Pressed**, F1=Released.
    236       // To work around this, we keep track of keys pressed with modifiers in a list, and
    237       // discard the press event when it's been previously activated. It's pretty gross,
    238       // but I can't think of a better way of handling it, and there doesn't appear to be
    239       // any window flag which changes this behavior that I can see.
    240 
    241       const u32 key = QtUtils::KeyEventToCode(key_event);
    242       const Qt::KeyboardModifiers modifiers = key_event->modifiers();
    243       const bool pressed = (key_event->type() == QEvent::KeyPress);
    244       const auto it = std::find(m_keys_pressed_with_modifiers.begin(), m_keys_pressed_with_modifiers.end(), key);
    245       if (it != m_keys_pressed_with_modifiers.end())
    246       {
    247         if (pressed)
    248           return true;
    249         else
    250           m_keys_pressed_with_modifiers.erase(it);
    251       }
    252       else if (modifiers != Qt::NoModifier && modifiers != Qt::KeypadModifier && pressed)
    253       {
    254         m_keys_pressed_with_modifiers.push_back(key);
    255       }
    256 
    257       emit windowKeyEvent(key, pressed);
    258       return true;
    259     }
    260 
    261     case QEvent::MouseMove:
    262     {
    263       if (!m_relative_mouse_enabled)
    264       {
    265         const qreal dpr = QtUtils::GetDevicePixelRatioForWidget(this);
    266         const QPoint mouse_pos = static_cast<QMouseEvent*>(event)->pos();
    267 
    268         const float scaled_x = static_cast<float>(static_cast<qreal>(mouse_pos.x()) * dpr);
    269         const float scaled_y = static_cast<float>(static_cast<qreal>(mouse_pos.y()) * dpr);
    270         InputManager::UpdatePointerAbsolutePosition(0, scaled_x, scaled_y);
    271       }
    272       else
    273       {
    274         // On windows, we use winapi here. The reason being that the coordinates in QCursor
    275         // are un-dpi-scaled, so we lose precision at higher desktop scalings.
    276         float dx = 0.0f, dy = 0.0f;
    277 
    278 #ifndef _WIN32
    279         const QPoint mouse_pos = QCursor::pos();
    280         if (mouse_pos != m_relative_mouse_center_pos)
    281         {
    282           dx = static_cast<float>(mouse_pos.x() - m_relative_mouse_center_pos.x());
    283           dy = static_cast<float>(mouse_pos.y() - m_relative_mouse_center_pos.y());
    284           QCursor::setPos(m_relative_mouse_center_pos);
    285         }
    286 #else
    287         POINT mouse_pos;
    288         if (GetCursorPos(&mouse_pos))
    289         {
    290           dx = static_cast<float>(mouse_pos.x - m_relative_mouse_center_pos.x());
    291           dy = static_cast<float>(mouse_pos.y - m_relative_mouse_center_pos.y());
    292           SetCursorPos(m_relative_mouse_center_pos.x(), m_relative_mouse_center_pos.y());
    293         }
    294 #endif
    295 
    296         if (!InputManager::IsUsingRawInput())
    297         {
    298           if (dx != 0.0f)
    299             InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::X, dx);
    300           if (dy != 0.0f)
    301             InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::Y, dy);
    302         }
    303       }
    304 
    305       return true;
    306     }
    307 
    308     case QEvent::MouseButtonPress:
    309     case QEvent::MouseButtonDblClick:
    310     case QEvent::MouseButtonRelease:
    311     {
    312       if (!m_relative_mouse_enabled || !InputManager::IsUsingRawInput())
    313       {
    314         const u32 button_index = CountTrailingZeros(static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()));
    315         emit windowMouseButtonEvent(static_cast<int>(button_index), event->type() != QEvent::MouseButtonRelease);
    316       }
    317 
    318       // don't toggle fullscreen when we're bound.. that wouldn't end well.
    319       if (event->type() == QEvent::MouseButtonDblClick &&
    320           static_cast<const QMouseEvent*>(event)->button() == Qt::LeftButton && QtHost::IsSystemValid() &&
    321           !FullscreenUI::HasActiveWindow() &&
    322           ((!QtHost::IsSystemPaused() &&
    323             !InputManager::HasAnyBindingsForKey(InputManager::MakePointerButtonKey(0, 0))) ||
    324            (QtHost::IsSystemPaused() && !ImGuiManager::WantsMouseInput())) &&
    325           Host::GetBoolSettingValue("Main", "DoubleClickTogglesFullscreen", true))
    326       {
    327         g_emu_thread->toggleFullscreen();
    328       }
    329 
    330       return true;
    331     }
    332 
    333     case QEvent::Wheel:
    334     {
    335       const QWheelEvent* wheel_event = static_cast<QWheelEvent*>(event);
    336       emit windowMouseWheelEvent(wheel_event->angleDelta());
    337       return true;
    338     }
    339 
    340       // According to https://bugreports.qt.io/browse/QTBUG-95925 the recommended practice for handling DPI change is
    341       // responding to paint events
    342     case QEvent::Paint:
    343     case QEvent::Resize:
    344     {
    345       QWidget::event(event);
    346 
    347       const float dpr = QtUtils::GetDevicePixelRatioForWidget(this);
    348       const u32 scaled_width =
    349         static_cast<u32>(std::max(static_cast<int>(std::ceil(static_cast<qreal>(width()) * dpr)), 1));
    350       const u32 scaled_height =
    351         static_cast<u32>(std::max(static_cast<int>(std::ceil(static_cast<qreal>(height()) * dpr)), 1));
    352 
    353       // avoid spamming resize events for paint events (sent on move on windows)
    354       if (m_last_window_width != scaled_width || m_last_window_height != scaled_height || m_last_window_scale != dpr)
    355       {
    356         m_last_window_width = scaled_width;
    357         m_last_window_height = scaled_height;
    358         m_last_window_scale = dpr;
    359         emit windowResizedEvent(scaled_width, scaled_height, dpr);
    360       }
    361 
    362       updateCenterPos();
    363       return true;
    364     }
    365 
    366     case QEvent::Move:
    367     {
    368       updateCenterPos();
    369       return true;
    370     }
    371 
    372     case QEvent::Close:
    373     {
    374       if (m_destroying)
    375         return QWidget::event(event);
    376 
    377       handleCloseEvent(static_cast<QCloseEvent*>(event));
    378       return true;
    379     }
    380 
    381     case QEvent::WindowStateChange:
    382     {
    383       QWidget::event(event);
    384 
    385       if (static_cast<QWindowStateChangeEvent*>(event)->oldState() & Qt::WindowMinimized)
    386         emit windowRestoredEvent();
    387 
    388       return true;
    389     }
    390 
    391     default:
    392       return QWidget::event(event);
    393   }
    394 }
    395 
    396 DisplayContainer::DisplayContainer() : QStackedWidget(nullptr)
    397 {
    398 }
    399 
    400 DisplayContainer::~DisplayContainer() = default;
    401 
    402 bool DisplayContainer::isNeeded(bool fullscreen, bool render_to_main)
    403 {
    404 #if defined(_WIN32) || defined(__APPLE__)
    405   return false;
    406 #else
    407   if (!isRunningOnWayland())
    408     return false;
    409 
    410   // We only need this on Wayland because of client-side decorations...
    411   return (fullscreen || !render_to_main);
    412 #endif
    413 }
    414 
    415 bool DisplayContainer::isRunningOnWayland()
    416 {
    417 #if defined(_WIN32) || defined(__APPLE__)
    418   return false;
    419 #else
    420   const QString platform_name = QGuiApplication::platformName();
    421   return (platform_name == QStringLiteral("wayland"));
    422 #endif
    423 }
    424 
    425 void DisplayContainer::setDisplayWidget(DisplayWidget* widget)
    426 {
    427   Assert(!m_display_widget);
    428   m_display_widget = widget;
    429   addWidget(widget);
    430 }
    431 
    432 DisplayWidget* DisplayContainer::removeDisplayWidget()
    433 {
    434   DisplayWidget* widget = m_display_widget;
    435   Assert(widget);
    436   m_display_widget = nullptr;
    437   removeWidget(widget);
    438   return widget;
    439 }
    440 
    441 bool DisplayContainer::event(QEvent* event)
    442 {
    443   if (event->type() == QEvent::Close && m_display_widget)
    444   {
    445     m_display_widget->handleCloseEvent(static_cast<QCloseEvent*>(event));
    446     return true;
    447   }
    448 
    449   const bool res = QStackedWidget::event(event);
    450   if (!m_display_widget)
    451     return res;
    452 
    453   switch (event->type())
    454   {
    455     case QEvent::WindowStateChange:
    456     {
    457       if (static_cast<QWindowStateChangeEvent*>(event)->oldState() & Qt::WindowMinimized)
    458         emit m_display_widget->windowRestoredEvent();
    459     }
    460     break;
    461 
    462     default:
    463       break;
    464   }
    465 
    466   return res;
    467 }