RenderWidget.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. // Copyright 2015 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include "DolphinQt/RenderWidget.h"
  4. #include <array>
  5. #include <QApplication>
  6. #include <QDragEnterEvent>
  7. #include <QDropEvent>
  8. #include <QFileInfo>
  9. #include <QGuiApplication>
  10. #include <QIcon>
  11. #include <QKeyEvent>
  12. #include <QMimeData>
  13. #include <QMouseEvent>
  14. #include <QPalette>
  15. #include <QScreen>
  16. #include <QTimer>
  17. #include <QWindow>
  18. #include "Core/Config/MainSettings.h"
  19. #include "Core/Core.h"
  20. #include "Core/State.h"
  21. #include "Core/System.h"
  22. #include "DolphinQt/Host.h"
  23. #include "DolphinQt/QtUtils/ModalMessageBox.h"
  24. #include "DolphinQt/Resources.h"
  25. #include "DolphinQt/Settings.h"
  26. #include "InputCommon/ControllerInterface/ControllerInterface.h"
  27. #include "VideoCommon/OnScreenUI.h"
  28. #include "VideoCommon/Present.h"
  29. #include "VideoCommon/VideoConfig.h"
  30. #ifdef _WIN32
  31. #include <Windows.h>
  32. #include <dwmapi.h>
  33. #endif
  34. RenderWidget::RenderWidget(QWidget* parent) : QWidget(parent)
  35. {
  36. setWindowTitle(QStringLiteral("Dolphin"));
  37. setWindowIcon(Resources::GetAppIcon());
  38. setWindowRole(QStringLiteral("renderer"));
  39. setAcceptDrops(true);
  40. QPalette p;
  41. p.setColor(QPalette::Window, Qt::black);
  42. setPalette(p);
  43. connect(Host::GetInstance(), &Host::RequestTitle, this, &RenderWidget::setWindowTitle);
  44. connect(Host::GetInstance(), &Host::RequestRenderSize, this, [this](int w, int h) {
  45. if (!Config::Get(Config::MAIN_RENDER_WINDOW_AUTOSIZE) || isFullScreen() || isMaximized())
  46. return;
  47. const auto dpr = window()->windowHandle()->screen()->devicePixelRatio();
  48. resize(w / dpr, h / dpr);
  49. });
  50. connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [this](Core::State state) {
  51. if (state == Core::State::Running)
  52. SetPresenterKeyMap();
  53. });
  54. // We have to use Qt::DirectConnection here because we don't want those signals to get queued
  55. // (which results in them not getting called)
  56. connect(this, &RenderWidget::StateChanged, Host::GetInstance(), &Host::SetRenderFullscreen,
  57. Qt::DirectConnection);
  58. connect(this, &RenderWidget::HandleChanged, this, &RenderWidget::OnHandleChanged,
  59. Qt::DirectConnection);
  60. connect(this, &RenderWidget::SizeChanged, Host::GetInstance(), &Host::ResizeSurface,
  61. Qt::DirectConnection);
  62. connect(this, &RenderWidget::FocusChanged, Host::GetInstance(), &Host::SetRenderFocus,
  63. Qt::DirectConnection);
  64. m_mouse_timer = new QTimer(this);
  65. connect(m_mouse_timer, &QTimer::timeout, this, &RenderWidget::HandleCursorTimer);
  66. m_mouse_timer->setSingleShot(true);
  67. setMouseTracking(true);
  68. connect(&Settings::Instance(), &Settings::CursorVisibilityChanged, this,
  69. &RenderWidget::OnHideCursorChanged);
  70. connect(&Settings::Instance(), &Settings::LockCursorChanged, this,
  71. &RenderWidget::OnLockCursorChanged);
  72. OnHideCursorChanged();
  73. OnLockCursorChanged();
  74. connect(&Settings::Instance(), &Settings::KeepWindowOnTopChanged, this,
  75. &RenderWidget::OnKeepOnTopChanged);
  76. OnKeepOnTopChanged(Settings::Instance().IsKeepWindowOnTopEnabled());
  77. m_mouse_timer->start(MOUSE_HIDE_DELAY);
  78. // We need a native window to render into.
  79. setAttribute(Qt::WA_NativeWindow);
  80. setAttribute(Qt::WA_PaintOnScreen);
  81. }
  82. QPaintEngine* RenderWidget::paintEngine() const
  83. {
  84. return nullptr;
  85. }
  86. void RenderWidget::dragEnterEvent(QDragEnterEvent* event)
  87. {
  88. if (event->mimeData()->hasUrls() && event->mimeData()->urls().size() == 1)
  89. event->acceptProposedAction();
  90. }
  91. void RenderWidget::dropEvent(QDropEvent* event)
  92. {
  93. const auto& urls = event->mimeData()->urls();
  94. if (urls.empty())
  95. return;
  96. const auto& url = urls[0];
  97. QFileInfo file_info(url.toLocalFile());
  98. auto path = file_info.filePath();
  99. if (!file_info.exists() || !file_info.isReadable())
  100. {
  101. ModalMessageBox::critical(this, tr("Error"), tr("Failed to open '%1'").arg(path));
  102. return;
  103. }
  104. if (!file_info.isFile())
  105. {
  106. return;
  107. }
  108. State::LoadAs(Core::System::GetInstance(), path.toStdString());
  109. }
  110. void RenderWidget::OnHandleChanged(void* handle)
  111. {
  112. if (handle)
  113. {
  114. #ifdef _WIN32
  115. // Remove rounded corners from the render window on Windows 11
  116. const DWM_WINDOW_CORNER_PREFERENCE corner_preference = DWMWCP_DONOTROUND;
  117. DwmSetWindowAttribute(reinterpret_cast<HWND>(handle), DWMWA_WINDOW_CORNER_PREFERENCE,
  118. &corner_preference, sizeof(corner_preference));
  119. #endif
  120. }
  121. Host::GetInstance()->SetRenderHandle(handle);
  122. }
  123. void RenderWidget::OnHideCursorChanged()
  124. {
  125. UpdateCursor();
  126. }
  127. void RenderWidget::OnLockCursorChanged()
  128. {
  129. SetCursorLocked(false);
  130. UpdateCursor();
  131. }
  132. // Calling this at any time will set the cursor (image) to the correct state
  133. void RenderWidget::UpdateCursor()
  134. {
  135. if (!Settings::Instance().GetLockCursor())
  136. {
  137. // Only hide if the cursor is automatically locking (it will hide on lock).
  138. // "Unhide" the cursor if we lost focus, otherwise it will disappear when hovering
  139. // on top of the game window in the background
  140. const bool keep_on_top = (windowFlags() & Qt::WindowStaysOnTopHint) != 0;
  141. const bool should_hide =
  142. (Settings::Instance().GetCursorVisibility() == Config::ShowCursor::Never) &&
  143. (keep_on_top || Config::Get(Config::MAIN_INPUT_BACKGROUND_INPUT) || isActiveWindow());
  144. setCursor(should_hide ? Qt::BlankCursor : Qt::ArrowCursor);
  145. }
  146. else
  147. {
  148. setCursor((m_cursor_locked &&
  149. Settings::Instance().GetCursorVisibility() == Config::ShowCursor::Never) ?
  150. Qt::BlankCursor :
  151. Qt::ArrowCursor);
  152. }
  153. }
  154. void RenderWidget::OnKeepOnTopChanged(bool top)
  155. {
  156. const bool was_visible = isVisible();
  157. setWindowFlags(top ? windowFlags() | Qt::WindowStaysOnTopHint :
  158. windowFlags() & ~Qt::WindowStaysOnTopHint);
  159. m_dont_lock_cursor_on_show = true;
  160. if (was_visible)
  161. show();
  162. m_dont_lock_cursor_on_show = false;
  163. UpdateCursor();
  164. }
  165. void RenderWidget::HandleCursorTimer()
  166. {
  167. if (!isActiveWindow())
  168. return;
  169. if ((!Settings::Instance().GetLockCursor() || m_cursor_locked) &&
  170. Settings::Instance().GetCursorVisibility() == Config::ShowCursor::OnMovement)
  171. {
  172. setCursor(Qt::BlankCursor);
  173. }
  174. }
  175. void RenderWidget::showFullScreen()
  176. {
  177. QWidget::showFullScreen();
  178. QScreen* screen = window()->windowHandle()->screen();
  179. const auto dpr = screen->devicePixelRatio();
  180. emit SizeChanged(width() * dpr, height() * dpr);
  181. }
  182. // Lock the cursor within the window/widget internal borders, including the aspect ratio if wanted
  183. void RenderWidget::SetCursorLocked(bool locked, bool follow_aspect_ratio)
  184. {
  185. // It seems like QT doesn't scale the window frame correctly with some DPIs
  186. // so it might happen that the locked cursor can be on the frame of the window,
  187. // being able to resize it, but that is a minor problem.
  188. // As a hack, if necessary, we could always scale down the size by 2 pixel, to a min of 1 given
  189. // that the size can be 0 already. We probably shouldn't scale axes already scaled by aspect ratio
  190. QRect render_rect = geometry();
  191. if (parentWidget())
  192. {
  193. render_rect.moveTopLeft(parentWidget()->mapToGlobal(render_rect.topLeft()));
  194. }
  195. auto scale = devicePixelRatioF(); // Seems to always be rounded on Win. Should we round results?
  196. QPoint screen_offset = QPoint(0, 0);
  197. if (window()->windowHandle() && window()->windowHandle()->screen())
  198. {
  199. screen_offset = window()->windowHandle()->screen()->geometry().topLeft();
  200. }
  201. render_rect.moveTopLeft(((render_rect.topLeft() - screen_offset) * scale) + screen_offset);
  202. render_rect.setSize(render_rect.size() * scale);
  203. if (follow_aspect_ratio)
  204. {
  205. // TODO: SetCursorLocked() should be re-called every time this value is changed?
  206. // This might cause imprecisions of one pixel (but it won't cause the cursor to go over borders)
  207. Common::Vec2 aspect_ratio = g_controller_interface.GetWindowInputScale();
  208. if (aspect_ratio.x > 1.f)
  209. {
  210. const float new_half_width = float(render_rect.width()) / (aspect_ratio.x * 2.f);
  211. // Only ceil if it was >= 0.25
  212. const float ceiled_new_half_width = std::ceil(std::round(new_half_width * 2.f) / 2.f);
  213. const int x_center = render_rect.center().x();
  214. // Make a guess on which one to floor and ceil.
  215. // For more precision, we should have kept the rounding point scale from above as well.
  216. render_rect.setLeft(x_center - std::floor(new_half_width));
  217. render_rect.setRight(x_center + ceiled_new_half_width);
  218. }
  219. if (aspect_ratio.y > 1.f)
  220. {
  221. const float new_half_height = render_rect.height() / (aspect_ratio.y * 2.f);
  222. const float ceiled_new_half_height = std::ceil(std::round(new_half_height * 2.f) / 2.f);
  223. const int y_center = render_rect.center().y();
  224. render_rect.setTop(y_center - std::floor(new_half_height));
  225. render_rect.setBottom(y_center + ceiled_new_half_height);
  226. }
  227. }
  228. if (locked)
  229. {
  230. #ifdef _WIN32
  231. RECT rect;
  232. rect.left = render_rect.left();
  233. rect.right = render_rect.right();
  234. rect.top = render_rect.top();
  235. rect.bottom = render_rect.bottom();
  236. if (ClipCursor(&rect))
  237. #else
  238. // TODO: Implement on other platforms. XGrabPointer on Linux X11 should be equivalent to
  239. // ClipCursor on Windows, though XFixesCreatePointerBarrier and XFixesDestroyPointerBarrier
  240. // may also work. On Wayland zwp_pointer_constraints_v1::confine_pointer and
  241. // zwp_pointer_constraints_v1::destroy provide this functionality.
  242. // More info:
  243. // https://stackoverflow.com/a/36269507
  244. // https://tronche.com/gui/x/xlib/input/XGrabPointer.html
  245. // https://www.x.org/releases/X11R7.7/doc/fixesproto/fixesproto.txt
  246. // https://wayland.app/protocols/pointer-constraints-unstable-v1
  247. // The setting is hidden in the UI if not implemented
  248. if (false)
  249. #endif
  250. {
  251. m_cursor_locked = true;
  252. if (Settings::Instance().GetCursorVisibility() != Config::ShowCursor::Constantly)
  253. {
  254. setCursor(Qt::BlankCursor);
  255. }
  256. Host::GetInstance()->SetRenderFullFocus(true);
  257. }
  258. }
  259. else
  260. {
  261. #ifdef _WIN32
  262. ClipCursor(nullptr);
  263. #endif
  264. if (m_cursor_locked)
  265. {
  266. m_cursor_locked = false;
  267. if (!Settings::Instance().GetLockCursor())
  268. {
  269. return;
  270. }
  271. // Center the mouse in the window if it's still active
  272. // Leave it where it was otherwise, e.g. a prompt has opened or we alt tabbed.
  273. if (isActiveWindow())
  274. {
  275. cursor().setPos(render_rect.left() + render_rect.width() / 2,
  276. render_rect.top() + render_rect.height() / 2);
  277. }
  278. // Show the cursor or the user won't know the mouse is now unlocked
  279. setCursor(Qt::ArrowCursor);
  280. Host::GetInstance()->SetRenderFullFocus(false);
  281. }
  282. }
  283. }
  284. void RenderWidget::SetCursorLockedOnNextActivation(bool locked)
  285. {
  286. if (Settings::Instance().GetLockCursor())
  287. {
  288. m_lock_cursor_on_next_activation = locked;
  289. return;
  290. }
  291. m_lock_cursor_on_next_activation = false;
  292. }
  293. void RenderWidget::SetWaitingForMessageBox(bool waiting_for_message_box)
  294. {
  295. if (m_waiting_for_message_box == waiting_for_message_box)
  296. {
  297. return;
  298. }
  299. m_waiting_for_message_box = waiting_for_message_box;
  300. if (!m_waiting_for_message_box && m_lock_cursor_on_next_activation && isActiveWindow())
  301. {
  302. if (Settings::Instance().GetLockCursor())
  303. {
  304. SetCursorLocked(true);
  305. }
  306. m_lock_cursor_on_next_activation = false;
  307. }
  308. }
  309. bool RenderWidget::event(QEvent* event)
  310. {
  311. PassEventToPresenter(event);
  312. switch (event->type())
  313. {
  314. case QEvent::KeyPress:
  315. {
  316. QKeyEvent* ke = static_cast<QKeyEvent*>(event);
  317. if (ke->key() == Qt::Key_Escape)
  318. emit EscapePressed();
  319. // The render window might flicker on some platforms because Qt tries to change focus to a new
  320. // element when there is none (?) Handling this event before it reaches QWidget fixes the issue.
  321. if (ke->key() == Qt::Key_Tab)
  322. return true;
  323. break;
  324. }
  325. // Needed in case a new window open and it moves the mouse
  326. case QEvent::WindowBlocked:
  327. SetCursorLocked(false);
  328. break;
  329. case QEvent::MouseButtonPress:
  330. if (isActiveWindow())
  331. {
  332. // Lock the cursor with any mouse button click (behave the same as window focus change).
  333. // This event is occasionally missed because isActiveWindow is laggy
  334. if (Settings::Instance().GetLockCursor())
  335. {
  336. SetCursorLocked(true);
  337. }
  338. }
  339. break;
  340. case QEvent::MouseMove:
  341. // Unhide on movement
  342. if (Settings::Instance().GetCursorVisibility() == Config::ShowCursor::OnMovement)
  343. {
  344. setCursor(Qt::ArrowCursor);
  345. m_mouse_timer->start(MOUSE_HIDE_DELAY);
  346. }
  347. break;
  348. case QEvent::WinIdChange:
  349. emit HandleChanged(reinterpret_cast<void*>(winId()));
  350. break;
  351. case QEvent::Show:
  352. // Don't do if "stay on top" changed (or was true)
  353. if (Settings::Instance().GetLockCursor() &&
  354. Settings::Instance().GetCursorVisibility() != Config::ShowCursor::Constantly &&
  355. !m_dont_lock_cursor_on_show)
  356. {
  357. // Auto lock when this window is shown (it was hidden)
  358. if (isActiveWindow())
  359. SetCursorLocked(true);
  360. else
  361. SetCursorLockedOnNextActivation();
  362. }
  363. break;
  364. // Note that this event in Windows is not always aligned to the window that is highlighted,
  365. // it's the window that has keyboard and mouse focus
  366. case QEvent::WindowActivate:
  367. if (m_should_unpause_on_focus &&
  368. Core::GetState(Core::System::GetInstance()) == Core::State::Paused)
  369. {
  370. Core::SetState(Core::System::GetInstance(), Core::State::Running);
  371. }
  372. m_should_unpause_on_focus = false;
  373. UpdateCursor();
  374. // Avoid "race conditions" with message boxes
  375. if (m_lock_cursor_on_next_activation && !m_waiting_for_message_box)
  376. {
  377. if (Settings::Instance().GetLockCursor())
  378. {
  379. SetCursorLocked(true);
  380. }
  381. m_lock_cursor_on_next_activation = false;
  382. }
  383. emit FocusChanged(true);
  384. break;
  385. case QEvent::WindowDeactivate:
  386. SetCursorLocked(false);
  387. UpdateCursor();
  388. if (Config::Get(Config::MAIN_PAUSE_ON_FOCUS_LOST) &&
  389. Core::GetState(Core::System::GetInstance()) == Core::State::Running)
  390. {
  391. // If we are declared as the CPU or GPU thread, it means that the real CPU or GPU thread
  392. // is waiting for us to finish showing a panic alert (with that panic alert likely being
  393. // the cause of this event), so trying to pause the core would cause a deadlock
  394. if (!Core::IsCPUThread() && !Core::IsGPUThread())
  395. {
  396. m_should_unpause_on_focus = true;
  397. Core::SetState(Core::System::GetInstance(), Core::State::Paused);
  398. }
  399. }
  400. emit FocusChanged(false);
  401. break;
  402. case QEvent::Move:
  403. SetCursorLocked(m_cursor_locked);
  404. break;
  405. // According to https://bugreports.qt.io/browse/QTBUG-95925 the recommended practice for
  406. // handling DPI change is responding to paint events
  407. case QEvent::Paint:
  408. case QEvent::Resize:
  409. {
  410. SetCursorLocked(m_cursor_locked);
  411. const QResizeEvent* se = static_cast<QResizeEvent*>(event);
  412. QSize new_size = se->size();
  413. QScreen* screen = window()->windowHandle()->screen();
  414. const float dpr = screen->devicePixelRatio();
  415. const int width = new_size.width() * dpr;
  416. const int height = new_size.height() * dpr;
  417. if (m_last_window_width != width || m_last_window_height != height ||
  418. m_last_window_scale != dpr)
  419. {
  420. m_last_window_width = width;
  421. m_last_window_height = height;
  422. m_last_window_scale = dpr;
  423. emit SizeChanged(width, height);
  424. }
  425. break;
  426. }
  427. // Happens when we add/remove the widget from the main window instead of the dedicated one
  428. case QEvent::ParentChange:
  429. SetCursorLocked(false);
  430. break;
  431. case QEvent::WindowStateChange:
  432. // Lock the mouse again when fullscreen changes (we might have missed some events)
  433. SetCursorLocked(m_cursor_locked || (isFullScreen() && Settings::Instance().GetLockCursor()));
  434. emit StateChanged(isFullScreen());
  435. break;
  436. case QEvent::Close:
  437. emit Closed();
  438. break;
  439. default:
  440. break;
  441. }
  442. return QWidget::event(event);
  443. }
  444. void RenderWidget::PassEventToPresenter(const QEvent* event)
  445. {
  446. if (!Core::IsRunning(Core::System::GetInstance()))
  447. return;
  448. switch (event->type())
  449. {
  450. case QEvent::KeyPress:
  451. case QEvent::KeyRelease:
  452. {
  453. // As the imgui KeysDown array is only 512 elements wide, and some Qt keys which
  454. // we need to track (e.g. alt) are above this value, we mask the lower 9 bits.
  455. // Even masked, the key codes are still unique, so conflicts aren't an issue.
  456. // The actual text input goes through AddInputCharactersUTF8().
  457. const QKeyEvent* key_event = static_cast<const QKeyEvent*>(event);
  458. const bool is_down = event->type() == QEvent::KeyPress;
  459. const u32 key = static_cast<u32>(key_event->key() & 0x1FF);
  460. const char* chars = nullptr;
  461. QByteArray utf8;
  462. if (is_down)
  463. {
  464. utf8 = key_event->text().toUtf8();
  465. if (utf8.size())
  466. chars = utf8.constData();
  467. }
  468. // Pass the key onto Presenter (for the imgui UI)
  469. g_presenter->SetKey(key, is_down, chars);
  470. }
  471. break;
  472. case QEvent::MouseMove:
  473. {
  474. // Qt multiplies all coordinates by the scaling factor in highdpi mode, giving us "scaled" mouse
  475. // coordinates (as if the screen was standard dpi). We need to update the mouse position in
  476. // native coordinates, as the UI (and game) is rendered at native resolution.
  477. const float scale = devicePixelRatio();
  478. float x = static_cast<const QMouseEvent*>(event)->pos().x() * scale;
  479. float y = static_cast<const QMouseEvent*>(event)->pos().y() * scale;
  480. g_presenter->SetMousePos(x, y);
  481. }
  482. break;
  483. case QEvent::MouseButtonPress:
  484. case QEvent::MouseButtonRelease:
  485. {
  486. const u32 button_mask = static_cast<u32>(static_cast<const QMouseEvent*>(event)->buttons());
  487. g_presenter->SetMousePress(button_mask);
  488. }
  489. break;
  490. default:
  491. break;
  492. }
  493. }
  494. void RenderWidget::SetPresenterKeyMap()
  495. {
  496. static constexpr DolphinKeyMap key_map = {
  497. Qt::Key_Tab, Qt::Key_Left, Qt::Key_Right, Qt::Key_Up, Qt::Key_Down,
  498. Qt::Key_PageUp, Qt::Key_PageDown, Qt::Key_Home, Qt::Key_End, Qt::Key_Insert,
  499. Qt::Key_Delete, Qt::Key_Backspace, Qt::Key_Space, Qt::Key_Return, Qt::Key_Escape,
  500. Qt::Key_Enter, // Keypad enter
  501. Qt::Key_A, Qt::Key_C, Qt::Key_V, Qt::Key_X, Qt::Key_Y,
  502. Qt::Key_Z,
  503. };
  504. g_presenter->SetKeyMap(key_map);
  505. }