/* PCSX2 - PS2 Emulator for PCs * Copyright (C) 2002-2022 PCSX2 Dev Team * * PCSX2 is free software: you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCSX2. * If not, see . */ #include "PrecompiledHeader.h" #include "common/Assertions.h" #include "DisplayWidget.h" #include "EmuThread.h" #include "MainWindow.h" #include "QtHost.h" #include "QtUtils.h" #include "pcsx2/GS/GSIntrin.h" // _BitScanForward #include #include #include #include #include #include #include #include #if defined(_WIN32) #include "common/RedtapeWindows.h" #elif !defined(APPLE) #include #endif DisplayWidget::DisplayWidget(QWidget* parent) : QWidget(parent) { // We want a native window for both D3D and OpenGL. setAutoFillBackground(false); setAttribute(Qt::WA_NativeWindow, true); setAttribute(Qt::WA_NoSystemBackground, true); setAttribute(Qt::WA_PaintOnScreen, true); setAttribute(Qt::WA_KeyCompression, false); setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); } DisplayWidget::~DisplayWidget() { #ifdef _WIN32 if (m_clip_mouse_enabled) ClipCursor(nullptr); #endif } qreal DisplayWidget::devicePixelRatioFromScreen() const { const QScreen* screen_for_ratio = screen(); if (!screen_for_ratio) screen_for_ratio = QGuiApplication::primaryScreen(); return screen_for_ratio ? screen_for_ratio->devicePixelRatio() : static_cast(1); } int DisplayWidget::scaledWindowWidth() const { return std::max(static_cast(std::ceil(static_cast(width()) * devicePixelRatioFromScreen())), 1); } int DisplayWidget::scaledWindowHeight() const { return std::max(static_cast(std::ceil(static_cast(height()) * devicePixelRatioFromScreen())), 1); } std::optional DisplayWidget::getWindowInfo() { WindowInfo wi; // Windows and Apple are easy here since there's no display connection. #if defined(_WIN32) wi.type = WindowInfo::Type::Win32; wi.window_handle = reinterpret_cast(winId()); #elif defined(__APPLE__) wi.type = WindowInfo::Type::MacOS; wi.window_handle = reinterpret_cast(winId()); #else QPlatformNativeInterface* pni = QGuiApplication::platformNativeInterface(); const QString platform_name = QGuiApplication::platformName(); if (platform_name == QStringLiteral("xcb")) { wi.type = WindowInfo::Type::X11; wi.display_connection = pni->nativeResourceForWindow("display", windowHandle()); wi.window_handle = reinterpret_cast(winId()); } else if (platform_name == QStringLiteral("wayland")) { wi.type = WindowInfo::Type::Wayland; wi.display_connection = pni->nativeResourceForWindow("display", windowHandle()); wi.window_handle = pni->nativeResourceForWindow("surface", windowHandle()); } else { qCritical() << "Unknown PNI platform " << platform_name; return std::nullopt; } #endif m_last_window_width = wi.surface_width = static_cast(scaledWindowWidth()); m_last_window_height = wi.surface_height = static_cast(scaledWindowHeight()); m_last_window_scale = wi.surface_scale = static_cast(devicePixelRatioFromScreen()); return wi; } void DisplayWidget::updateRelativeMode(bool master_enable) { bool relative_mode = master_enable && InputManager::HasPointerAxisBinds(); #ifdef _WIN32 // prefer ClipCursor() over warping movement when we're using raw input bool clip_cursor = relative_mode && false /*InputManager::IsUsingRawInput()*/; if (m_relative_mouse_enabled == relative_mode && m_clip_mouse_enabled == clip_cursor) return; DevCon.WriteLn("updateRelativeMode(): relative=%s, clip=%s", relative_mode ? "yes" : "no", clip_cursor ? "yes" : "no"); if (!clip_cursor && m_clip_mouse_enabled) { m_clip_mouse_enabled = false; ClipCursor(nullptr); } #else if (m_relative_mouse_enabled == relative_mode) return; DevCon.WriteLn("updateRelativeMode(): relative=%s", relative_mode ? "yes" : "no"); #endif if (relative_mode) { #ifdef _WIN32 m_relative_mouse_enabled = !clip_cursor; m_clip_mouse_enabled = clip_cursor; #else m_relative_mouse_enabled = true; #endif m_relative_mouse_start_pos = QCursor::pos(); updateCenterPos(); grabMouse(); } else if (m_relative_mouse_enabled) { m_relative_mouse_enabled = false; QCursor::setPos(m_relative_mouse_start_pos); releaseMouse(); } } void DisplayWidget::updateCursor(bool master_enable) { #ifdef _WIN32 const bool hide = master_enable && (m_should_hide_cursor || m_relative_mouse_enabled || m_clip_mouse_enabled); #else const bool hide = master_enable && (m_should_hide_cursor || m_relative_mouse_enabled); #endif if (m_cursor_hidden == hide) return; m_cursor_hidden = hide; if (hide) setCursor(Qt::BlankCursor); else unsetCursor(); } void DisplayWidget::updateCenterPos() { #ifdef _WIN32 if (m_clip_mouse_enabled) { RECT rc; if (GetWindowRect(reinterpret_cast(winId()), &rc)) ClipCursor(&rc); } else if (m_relative_mouse_enabled) { RECT rc; if (GetWindowRect(reinterpret_cast(winId()), &rc)) { m_relative_mouse_center_pos.setX(((rc.right - rc.left) / 2) + rc.left); m_relative_mouse_center_pos.setY(((rc.bottom - rc.top) / 2) + rc.top); SetCursorPos(m_relative_mouse_center_pos.x(), m_relative_mouse_center_pos.y()); } } #else if (m_relative_mouse_enabled) { // we do a round trip here because these coordinates are dpi-unscaled m_relative_mouse_center_pos = mapToGlobal(QPoint((width() + 1) / 2, (height() + 1) / 2)); QCursor::setPos(m_relative_mouse_center_pos); m_relative_mouse_center_pos = QCursor::pos(); } #endif } QPaintEngine* DisplayWidget::paintEngine() const { return nullptr; } bool DisplayWidget::event(QEvent* event) { switch (event->type()) { case QEvent::KeyPress: case QEvent::KeyRelease: { const QKeyEvent* key_event = static_cast(event); if (key_event->isAutoRepeat()) return true; // For some reason, Windows sends "fake" key events. // Scenario: Press shift, press F1, release shift, release F1. // Events: Shift=Pressed, F1=Pressed, Shift=Released, **F1=Pressed**, F1=Released. // To work around this, we keep track of keys pressed with modifiers in a list, and // discard the press event when it's been previously activated. It's pretty gross, // but I can't think of a better way of handling it, and there doesn't appear to be // any window flag which changes this behavior that I can see. const u32 key = QtUtils::KeyEventToCode(key_event); const Qt::KeyboardModifiers modifiers = key_event->modifiers(); const bool pressed = (key_event->type() == QEvent::KeyPress); const auto it = std::find(m_keys_pressed_with_modifiers.begin(), m_keys_pressed_with_modifiers.end(), key); if (it != m_keys_pressed_with_modifiers.end()) { if (pressed) return true; else m_keys_pressed_with_modifiers.erase(it); } else if (modifiers != Qt::NoModifier && modifiers != Qt::KeypadModifier && pressed) { m_keys_pressed_with_modifiers.push_back(key); } Host::RunOnCPUThread([key, pressed]() { InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key), static_cast(pressed)); }); return true; } case QEvent::MouseMove: { const QMouseEvent* mouse_event = static_cast(event); if (!m_relative_mouse_enabled) { const qreal dpr = devicePixelRatioFromScreen(); const QPoint mouse_pos = mouse_event->pos(); const float scaled_x = static_cast(static_cast(mouse_pos.x()) * dpr); const float scaled_y = static_cast(static_cast(mouse_pos.y()) * dpr); InputManager::UpdatePointerAbsolutePosition(0, scaled_x, scaled_y); } else { // On windows, we use winapi here. The reason being that the coordinates in QCursor // are un-dpi-scaled, so we lose precision at higher desktop scalings. float dx = 0.0f, dy = 0.0f; #ifndef _WIN32 const QPoint mouse_pos = QCursor::pos(); if (mouse_pos != m_relative_mouse_center_pos) { dx = static_cast(mouse_pos.x() - m_relative_mouse_center_pos.x()); dy = static_cast(mouse_pos.y() - m_relative_mouse_center_pos.y()); QCursor::setPos(m_relative_mouse_center_pos); } #else POINT mouse_pos; if (GetCursorPos(&mouse_pos)) { dx = static_cast(mouse_pos.x - m_relative_mouse_center_pos.x()); dy = static_cast(mouse_pos.y - m_relative_mouse_center_pos.y()); SetCursorPos(m_relative_mouse_center_pos.x(), m_relative_mouse_center_pos.y()); } #endif if (dx != 0.0f) InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::X, dx); if (dy != 0.0f) InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::Y, dy); } return true; } case QEvent::MouseButtonPress: case QEvent::MouseButtonDblClick: case QEvent::MouseButtonRelease: { unsigned long button_index; if (_BitScanForward(&button_index, static_cast(static_cast(event)->button()))) { Host::RunOnCPUThread([button_index, pressed = (event->type() != QEvent::MouseButtonRelease)]() { InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button_index), static_cast(pressed)); }); } // don't toggle fullscreen when we're bound.. that wouldn't end well. if (event->type() == QEvent::MouseButtonDblClick && static_cast(event)->button() == Qt::LeftButton && !InputManager::HasAnyBindingsForKey(InputManager::MakePointerButtonKey(0, 0)) && Host::GetBoolSettingValue("UI", "DoubleClickTogglesFullscreen", true)) { g_emu_thread->toggleFullscreen(); } return true; } case QEvent::Wheel: { // wheel delta is 120 as in winapi const QPoint delta_angle(static_cast(event)->angleDelta()); constexpr float DELTA = 120.0f; const float dx = std::clamp(static_cast(delta_angle.x()) / DELTA, -1.0f, 1.0f); if (dx != 0.0f) InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, dx); const float dy = std::clamp(static_cast(delta_angle.y()) / DELTA, -1.0f, 1.0f); if (dy != 0.0f) InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, dy); return true; } // According to https://bugreports.qt.io/browse/QTBUG-95925 the recommended practice for handling DPI change is responding to paint events case QEvent::Paint: case QEvent::Resize: { QWidget::event(event); const float dpr = devicePixelRatioFromScreen(); const u32 scaled_width = static_cast(std::max(static_cast(std::ceil(static_cast(width()) * dpr)), 1)); const u32 scaled_height = static_cast(std::max(static_cast(std::ceil(static_cast(height()) * dpr)), 1)); // avoid spamming resize events for paint events (sent on move on windows) if (m_last_window_width != scaled_width || m_last_window_height != scaled_height || m_last_window_scale != dpr) { m_last_window_width = scaled_width; m_last_window_height = scaled_height; m_last_window_scale = dpr; emit windowResizedEvent(scaled_width, scaled_height, dpr); } updateCenterPos(); return true; } case QEvent::Move: { updateCenterPos(); return true; } case QEvent::Close: { if (!g_main_window->requestShutdown()) { // abort the window close event->ignore(); return true; } QWidget::event(event); return true; } case QEvent::WindowStateChange: { QWidget::event(event); if (static_cast(event)->oldState() & Qt::WindowMinimized) emit windowRestoredEvent(); return true; } case QEvent::FocusIn: { QWidget::event(event); emit windowFocusEvent(); return true; } case QEvent::ActivationChange: { QWidget::event(event); if (isActiveWindow()) emit windowFocusEvent(); return true; } default: return QWidget::event(event); } } DisplayContainer::DisplayContainer() : QStackedWidget(nullptr) { } DisplayContainer::~DisplayContainer() = default; bool DisplayContainer::IsNeeded(bool fullscreen, bool render_to_main) { #if defined(_WIN32) || defined(__APPLE__) return false; #else if (!fullscreen && render_to_main) return false; // We only need this on Wayland because of client-side decorations... const QString platform_name = QGuiApplication::platformName(); return (platform_name == QStringLiteral("wayland")); #endif } void DisplayContainer::setDisplayWidget(DisplayWidget* widget) { pxAssert(!m_display_widget); m_display_widget = widget; addWidget(widget); } DisplayWidget* DisplayContainer::removeDisplayWidget() { DisplayWidget* widget = m_display_widget; pxAssert(widget); m_display_widget = nullptr; removeWidget(widget); return widget; } bool DisplayContainer::event(QEvent* event) { if (event->type() == QEvent::Close && !g_main_window->requestShutdown()) { // abort the window close event->ignore(); return true; } const bool res = QStackedWidget::event(event); if (!m_display_widget) return res; switch (event->type()) { case QEvent::WindowStateChange: { if (static_cast(event)->oldState() & Qt::WindowMinimized) emit m_display_widget->windowRestoredEvent(); } break; case QEvent::FocusIn: { emit m_display_widget->windowFocusEvent(); } break; case QEvent::ActivationChange: { if (isActiveWindow()) emit m_display_widget->windowFocusEvent(); } break; default: break; } return res; }