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 }