mainwindow.cpp (104069B)
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 "mainwindow.h" 5 #include "aboutdialog.h" 6 #include "achievementlogindialog.h" 7 #include "autoupdaterdialog.h" 8 #include "cheatmanagerwindow.h" 9 #include "coverdownloaddialog.h" 10 #include "debuggerwindow.h" 11 #include "displaywidget.h" 12 #include "gamelistmodel.h" 13 #include "gamelistsettingswidget.h" 14 #include "gamelistwidget.h" 15 #include "interfacesettingswidget.h" 16 #include "logwindow.h" 17 #include "memorycardeditorwindow.h" 18 #include "memoryscannerwindow.h" 19 #include "qthost.h" 20 #include "qtutils.h" 21 #include "selectdiscdialog.h" 22 #include "settingswindow.h" 23 #include "settingwidgetbinder.h" 24 25 #include "core/achievements.h" 26 #include "core/game_list.h" 27 #include "core/host.h" 28 #include "core/memory_card.h" 29 #include "core/settings.h" 30 #include "core/system.h" 31 32 #include "util/cd_image.h" 33 #include "util/gpu_device.h" 34 35 #include "common/assert.h" 36 #include "common/error.h" 37 #include "common/file_system.h" 38 #include "common/log.h" 39 40 #include <QtCore/QDebug> 41 #include <QtCore/QFile> 42 #include <QtCore/QFileInfo> 43 #include <QtCore/QMimeData> 44 #include <QtCore/QUrl> 45 #include <QtGui/QActionGroup> 46 #include <QtGui/QCursor> 47 #include <QtGui/QWindowStateChangeEvent> 48 #include <QtWidgets/QFileDialog> 49 #include <QtWidgets/QInputDialog> 50 #include <QtWidgets/QMessageBox> 51 #include <QtWidgets/QProgressBar> 52 #include <QtWidgets/QStyleFactory> 53 #include <cmath> 54 55 #ifdef _WIN32 56 #include "common/windows_headers.h" 57 #include <Dbt.h> 58 #include <VersionHelpers.h> 59 #endif 60 61 #ifdef __APPLE__ 62 #include "common/cocoa_tools.h" 63 #endif 64 65 Log_SetChannel(MainWindow); 66 67 static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( 68 "MainWindow", 69 "All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.mds *.pbp *.exe *.psexe *.ps-exe *.psf *.minipsf " 70 "*.m3u);;Single-Track " 71 "Raw Images (*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;Error Code Modeler Images " 72 "(*.ecm);;Media Descriptor Sidecar Images (*.mds);;PlayStation EBOOTs (*.pbp *.PBP);;PlayStation Executables (*.exe " 73 "*.psexe *.ps-exe);;Portable Sound Format Files (*.psf *.minipsf);;Playlists (*.m3u)"); 74 75 MainWindow* g_main_window = nullptr; 76 77 #if defined(_WIN32) || defined(__APPLE__) 78 static const bool s_use_central_widget = false; 79 #else 80 // Qt Wayland is broken. Any sort of stacked widget usage fails to update, 81 // leading to broken window resizes, no display rendering, etc. So, we mess 82 // with the central widget instead. Which we can't do on xorg, because it 83 // breaks window resizing there... 84 static bool s_use_central_widget = false; 85 #endif 86 87 // UI thread VM validity. 88 static bool s_system_valid = false; 89 static bool s_system_paused = false; 90 static QString s_current_game_title; 91 static QString s_current_game_serial; 92 static QString s_current_game_path; 93 static QIcon s_current_game_icon; 94 95 bool QtHost::IsSystemPaused() 96 { 97 return s_system_paused; 98 } 99 100 bool QtHost::IsSystemValid() 101 { 102 return s_system_valid; 103 } 104 105 const QString& QtHost::GetCurrentGameTitle() 106 { 107 return s_current_game_title; 108 } 109 110 const QString& QtHost::GetCurrentGameSerial() 111 { 112 return s_current_game_serial; 113 } 114 115 const QString& QtHost::GetCurrentGamePath() 116 { 117 return s_current_game_path; 118 } 119 120 MainWindow::MainWindow() : QMainWindow(nullptr) 121 { 122 Assert(!g_main_window); 123 g_main_window = this; 124 125 #if !defined(_WIN32) && !defined(__APPLE__) 126 s_use_central_widget = DisplayContainer::isRunningOnWayland(); 127 #endif 128 129 initialize(); 130 } 131 132 MainWindow::~MainWindow() 133 { 134 Assert(!m_display_widget); 135 Assert(!m_debugger_window); 136 cancelGameListRefresh(); 137 138 // we compare here, since recreate destroys the window later 139 if (g_main_window == this) 140 g_main_window = nullptr; 141 142 #ifdef _WIN32 143 unregisterForDeviceNotifications(); 144 #endif 145 #ifdef __APPLE__ 146 CocoaTools::RemoveThemeChangeHandler(this); 147 #endif 148 } 149 150 void MainWindow::initialize() 151 { 152 m_ui.setupUi(this); 153 setupAdditionalUi(); 154 connectSignals(); 155 156 restoreStateFromConfig(); 157 switchToGameListView(); 158 updateWindowTitle(); 159 160 #ifdef ENABLE_RAINTEGRATION 161 if (Achievements::IsUsingRAIntegration()) 162 Achievements::RAIntegration::MainWindowChanged((void*)winId()); 163 #endif 164 165 #ifdef _WIN32 166 registerForDeviceNotifications(); 167 #endif 168 169 #ifdef __APPLE__ 170 CocoaTools::AddThemeChangeHandler(this, 171 [](void* ctx) { QtHost::RunOnUIThread([] { g_main_window->updateTheme(); }); }); 172 #endif 173 } 174 175 void MainWindow::reportError(const QString& title, const QString& message) 176 { 177 QMessageBox::critical(this, title, message, QMessageBox::Ok); 178 } 179 180 bool MainWindow::confirmMessage(const QString& title, const QString& message) 181 { 182 SystemLock lock(pauseAndLockSystem()); 183 184 return (QMessageBox::question(this, title, message) == QMessageBox::Yes); 185 } 186 187 void MainWindow::onStatusMessage(const QString& message) 188 { 189 m_ui.statusBar->showMessage(message); 190 } 191 192 void MainWindow::registerForDeviceNotifications() 193 { 194 #ifdef _WIN32 195 // We use these notifications to detect when a controller is connected or disconnected. 196 DEV_BROADCAST_DEVICEINTERFACE_W filter = { 197 sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE, 0u, {}, {}}; 198 m_device_notification_handle = RegisterDeviceNotificationW( 199 (HANDLE)winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES); 200 #endif 201 } 202 203 void MainWindow::unregisterForDeviceNotifications() 204 { 205 #ifdef _WIN32 206 if (!m_device_notification_handle) 207 return; 208 209 UnregisterDeviceNotification(static_cast<HDEVNOTIFY>(m_device_notification_handle)); 210 m_device_notification_handle = nullptr; 211 #endif 212 } 213 214 #ifdef _WIN32 215 216 bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result) 217 { 218 static constexpr const char win_type[] = "windows_generic_MSG"; 219 if (eventType == QByteArray(win_type, sizeof(win_type) - 1)) 220 { 221 const MSG* msg = static_cast<const MSG*>(message); 222 if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED) 223 { 224 g_emu_thread->reloadInputDevices(); 225 *result = 1; 226 return true; 227 } 228 } 229 230 return QMainWindow::nativeEvent(eventType, message, result); 231 } 232 233 #endif 234 235 std::optional<WindowInfo> MainWindow::acquireRenderWindow(bool recreate_window, bool fullscreen, bool render_to_main, 236 bool surfaceless, bool use_main_window_pos) 237 { 238 DEV_LOG("acquireRenderWindow() recreate={} fullscreen={} render_to_main={} surfaceless={} use_main_window_pos={}", 239 recreate_window ? "true" : "false", fullscreen ? "true" : "false", render_to_main ? "true" : "false", 240 surfaceless ? "true" : "false", use_main_window_pos ? "true" : "false"); 241 242 QWidget* container = 243 m_display_container ? static_cast<QWidget*>(m_display_container) : static_cast<QWidget*>(m_display_widget); 244 const bool is_fullscreen = isRenderingFullscreen(); 245 const bool is_rendering_to_main = isRenderingToMain(); 246 const bool changing_surfaceless = (!m_display_widget != surfaceless); 247 if (m_display_created && !recreate_window && fullscreen == is_fullscreen && is_rendering_to_main == render_to_main && 248 !changing_surfaceless) 249 { 250 return m_display_widget ? m_display_widget->getWindowInfo() : WindowInfo(); 251 } 252 253 // Skip recreating the surface if we're just transitioning between fullscreen and windowed with render-to-main off. 254 // .. except on Wayland, where everything tends to break if you don't recreate. 255 const bool has_container = (m_display_container != nullptr); 256 const bool needs_container = DisplayContainer::isNeeded(fullscreen, render_to_main); 257 if (m_display_created && !recreate_window && !is_rendering_to_main && !render_to_main && 258 has_container == needs_container && !needs_container && !changing_surfaceless) 259 { 260 DEV_LOG("Toggling to {} without recreating surface", (fullscreen ? "fullscreen" : "windowed")); 261 262 // since we don't destroy the display widget, we need to save it here 263 if (!is_fullscreen && !is_rendering_to_main) 264 saveDisplayWindowGeometryToConfig(); 265 266 if (fullscreen) 267 { 268 container->showFullScreen(); 269 } 270 else 271 { 272 if (use_main_window_pos) 273 container->setGeometry(geometry()); 274 else 275 restoreDisplayWindowGeometryFromConfig(); 276 277 container->showNormal(); 278 } 279 280 updateDisplayWidgetCursor(); 281 m_display_widget->setFocus(); 282 updateWindowState(); 283 284 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 285 return m_display_widget->getWindowInfo(); 286 } 287 288 destroyDisplayWidget(surfaceless); 289 m_display_created = true; 290 291 // if we're going to surfaceless, we're done here 292 if (surfaceless) 293 return WindowInfo(); 294 295 createDisplayWidget(fullscreen, render_to_main, use_main_window_pos); 296 297 std::optional<WindowInfo> wi = m_display_widget->getWindowInfo(); 298 if (!wi.has_value()) 299 { 300 QMessageBox::critical(this, tr("Error"), tr("Failed to get window info from widget")); 301 destroyDisplayWidget(true); 302 return std::nullopt; 303 } 304 305 g_emu_thread->connectDisplaySignals(m_display_widget); 306 307 updateWindowTitle(); 308 updateWindowState(); 309 310 updateDisplayWidgetCursor(); 311 updateDisplayRelatedActions(true, render_to_main, fullscreen); 312 QtUtils::ShowOrRaiseWindow(QtUtils::GetRootWidget(m_display_widget)); 313 m_display_widget->setFocus(); 314 315 return wi; 316 } 317 318 void MainWindow::createDisplayWidget(bool fullscreen, bool render_to_main, bool use_main_window_pos) 319 { 320 // If we're rendering to main and were hidden (e.g. coming back from fullscreen), 321 // make sure we're visible before trying to add ourselves. Otherwise Wayland breaks. 322 if (!fullscreen && render_to_main && !isVisible()) 323 { 324 setVisible(true); 325 QGuiApplication::sync(); 326 } 327 328 QWidget* container; 329 if (DisplayContainer::isNeeded(fullscreen, render_to_main)) 330 { 331 m_display_container = new DisplayContainer(); 332 m_display_widget = new DisplayWidget(m_display_container); 333 m_display_container->setDisplayWidget(m_display_widget); 334 container = m_display_container; 335 } 336 else 337 { 338 m_display_widget = new DisplayWidget((!fullscreen && render_to_main) ? getContentParent() : nullptr); 339 container = m_display_widget; 340 } 341 342 if (fullscreen || !render_to_main) 343 { 344 container->setWindowTitle(windowTitle()); 345 container->setWindowIcon(windowIcon()); 346 } 347 348 if (fullscreen) 349 { 350 // Don't risk doing this on Wayland, it really doesn't like window state changes, 351 // and positioning has no effect anyway. 352 if (!s_use_central_widget) 353 { 354 if (isVisible() && g_emu_thread->shouldRenderToMain()) 355 container->move(pos()); 356 else 357 restoreDisplayWindowGeometryFromConfig(); 358 } 359 360 container->showFullScreen(); 361 } 362 else if (!render_to_main) 363 { 364 // See lameland comment above. 365 if (use_main_window_pos && !s_use_central_widget) 366 container->setGeometry(geometry()); 367 else 368 restoreDisplayWindowGeometryFromConfig(); 369 container->showNormal(); 370 } 371 else if (s_use_central_widget) 372 { 373 m_game_list_widget->setVisible(false); 374 takeCentralWidget(); 375 m_game_list_widget->setParent(this); // takeCentralWidget() removes parent 376 setCentralWidget(m_display_widget); 377 m_display_widget->setFocus(); 378 update(); 379 } 380 else 381 { 382 AssertMsg(m_ui.mainContainer->count() == 1, "Has no display widget"); 383 m_ui.mainContainer->addWidget(container); 384 m_ui.mainContainer->setCurrentIndex(1); 385 } 386 387 updateDisplayRelatedActions(true, render_to_main, fullscreen); 388 389 // We need the surface visible. 390 QGuiApplication::sync(); 391 } 392 393 void MainWindow::displayResizeRequested(qint32 width, qint32 height) 394 { 395 if (!m_display_widget) 396 return; 397 398 // unapply the pixel scaling factor for hidpi 399 const float dpr = devicePixelRatioF(); 400 width = static_cast<qint32>(std::max(static_cast<int>(std::lroundf(static_cast<float>(width) / dpr)), 1)); 401 height = static_cast<qint32>(std::max(static_cast<int>(std::lroundf(static_cast<float>(height) / dpr)), 1)); 402 403 if (m_display_container || !m_display_widget->parent()) 404 { 405 // no parent - rendering to separate window. easy. 406 QtUtils::ResizePotentiallyFixedSizeWindow(getDisplayContainer(), width, height); 407 return; 408 } 409 410 // we are rendering to the main window. we have to add in the extra height from the toolbar/status bar. 411 const s32 extra_height = this->height() - m_display_widget->height(); 412 QtUtils::ResizePotentiallyFixedSizeWindow(this, width, height + extra_height); 413 } 414 415 void MainWindow::releaseRenderWindow() 416 { 417 // Now we can safely destroy the display window. 418 destroyDisplayWidget(true); 419 m_display_created = false; 420 421 updateDisplayRelatedActions(false, false, false); 422 423 m_ui.actionViewSystemDisplay->setEnabled(false); 424 m_ui.actionFullscreen->setEnabled(false); 425 } 426 427 void MainWindow::destroyDisplayWidget(bool show_game_list) 428 { 429 if (!m_display_widget) 430 return; 431 432 if (!isRenderingFullscreen() && !isRenderingToMain()) 433 saveDisplayWindowGeometryToConfig(); 434 435 if (m_display_container) 436 m_display_container->removeDisplayWidget(); 437 438 if (isRenderingToMain()) 439 { 440 if (s_use_central_widget) 441 { 442 AssertMsg(centralWidget() == m_display_widget, "Display widget is currently central"); 443 takeCentralWidget(); 444 if (show_game_list) 445 { 446 m_game_list_widget->setVisible(true); 447 setCentralWidget(m_game_list_widget); 448 m_game_list_widget->resizeTableViewColumnsToFit(); 449 } 450 } 451 else 452 { 453 AssertMsg(m_ui.mainContainer->indexOf(m_display_widget) == 1, "Display widget in stack"); 454 m_ui.mainContainer->removeWidget(m_display_widget); 455 if (show_game_list) 456 { 457 m_ui.mainContainer->setCurrentIndex(0); 458 m_game_list_widget->resizeTableViewColumnsToFit(); 459 } 460 } 461 } 462 463 if (m_display_widget) 464 { 465 m_display_widget->destroy(); 466 m_display_widget = nullptr; 467 } 468 469 if (m_display_container) 470 { 471 m_display_container->deleteLater(); 472 m_display_container = nullptr; 473 } 474 } 475 476 void MainWindow::updateDisplayWidgetCursor() 477 { 478 m_display_widget->updateRelativeMode(s_system_valid && !s_system_paused && m_relative_mouse_mode); 479 m_display_widget->updateCursor(s_system_valid && !s_system_paused && shouldHideMouseCursor()); 480 } 481 482 void MainWindow::updateDisplayRelatedActions(bool has_surface, bool render_to_main, bool fullscreen) 483 { 484 // rendering to main, or switched to gamelist/grid 485 m_ui.actionViewSystemDisplay->setEnabled((has_surface && render_to_main) || (!has_surface && g_gpu_device)); 486 m_ui.menuWindowSize->setEnabled(has_surface && !fullscreen); 487 m_ui.actionFullscreen->setEnabled(has_surface); 488 489 { 490 QSignalBlocker blocker(m_ui.actionFullscreen); 491 m_ui.actionFullscreen->setChecked(fullscreen); 492 } 493 } 494 495 void MainWindow::focusDisplayWidget() 496 { 497 if (!m_display_widget || centralWidget() != m_display_widget) 498 return; 499 500 m_display_widget->setFocus(); 501 } 502 503 QWidget* MainWindow::getContentParent() 504 { 505 return s_use_central_widget ? static_cast<QWidget*>(this) : static_cast<QWidget*>(m_ui.mainContainer); 506 } 507 508 QWidget* MainWindow::getDisplayContainer() const 509 { 510 return (m_display_container ? static_cast<QWidget*>(m_display_container) : static_cast<QWidget*>(m_display_widget)); 511 } 512 513 void MainWindow::onMouseModeRequested(bool relative_mode, bool hide_cursor) 514 { 515 m_relative_mouse_mode = relative_mode; 516 m_hide_mouse_cursor = hide_cursor; 517 if (m_display_widget) 518 updateDisplayWidgetCursor(); 519 } 520 521 void MainWindow::onSystemStarting() 522 { 523 s_system_valid = false; 524 s_system_paused = false; 525 526 updateEmulationActions(true, false, Achievements::IsHardcoreModeActive()); 527 } 528 529 void MainWindow::onSystemStarted() 530 { 531 m_was_disc_change_request = false; 532 s_system_valid = true; 533 updateEmulationActions(false, true, Achievements::IsHardcoreModeActive()); 534 updateWindowTitle(); 535 updateStatusBarWidgetVisibility(); 536 updateDisplayWidgetCursor(); 537 } 538 539 void MainWindow::onSystemPaused() 540 { 541 // update UI 542 { 543 QSignalBlocker sb(m_ui.actionPause); 544 m_ui.actionPause->setChecked(true); 545 } 546 547 s_system_paused = true; 548 updateStatusBarWidgetVisibility(); 549 m_ui.statusBar->showMessage(tr("Paused")); 550 if (m_display_widget) 551 updateDisplayWidgetCursor(); 552 } 553 554 void MainWindow::onSystemResumed() 555 { 556 // update UI 557 { 558 QSignalBlocker sb(m_ui.actionPause); 559 m_ui.actionPause->setChecked(false); 560 } 561 562 s_system_paused = false; 563 m_was_disc_change_request = false; 564 m_ui.statusBar->clearMessage(); 565 updateStatusBarWidgetVisibility(); 566 if (m_display_widget) 567 { 568 updateDisplayWidgetCursor(); 569 m_display_widget->setFocus(); 570 } 571 } 572 573 void MainWindow::onSystemDestroyed() 574 { 575 // update UI 576 { 577 QSignalBlocker sb(m_ui.actionPause); 578 m_ui.actionPause->setChecked(false); 579 } 580 581 s_system_valid = false; 582 s_system_paused = false; 583 584 // If we're closing or in batch mode, quit the whole application now. 585 if (m_is_closing || QtHost::InBatchMode()) 586 { 587 destroySubWindows(); 588 quit(); 589 return; 590 } 591 592 updateEmulationActions(false, false, Achievements::IsHardcoreModeActive()); 593 if (m_display_widget) 594 updateDisplayWidgetCursor(); 595 else 596 switchToGameListView(); 597 598 // reload played time 599 if (m_game_list_widget->isShowingGameList()) 600 m_game_list_widget->refresh(false); 601 } 602 603 void MainWindow::onRunningGameChanged(const QString& filename, const QString& game_serial, const QString& game_title) 604 { 605 s_current_game_path = filename; 606 s_current_game_title = game_title; 607 s_current_game_serial = game_serial; 608 s_current_game_icon = m_game_list_widget->getModel()->getIconForGame(filename); 609 610 updateWindowTitle(); 611 } 612 613 void MainWindow::onMediaCaptureStarted() 614 { 615 QSignalBlocker sb(m_ui.actionMediaCapture); 616 m_ui.actionMediaCapture->setChecked(true); 617 } 618 619 void MainWindow::onMediaCaptureStopped() 620 { 621 QSignalBlocker sb(m_ui.actionMediaCapture); 622 m_ui.actionMediaCapture->setChecked(false); 623 } 624 625 void MainWindow::onApplicationStateChanged(Qt::ApplicationState state) 626 { 627 if (!s_system_valid) 628 return; 629 630 const bool focus_loss = (state != Qt::ApplicationActive); 631 if (focus_loss) 632 { 633 if (g_settings.pause_on_focus_loss && !m_was_paused_by_focus_loss && !s_system_paused) 634 { 635 g_emu_thread->setSystemPaused(true); 636 m_was_paused_by_focus_loss = true; 637 } 638 639 // Clear the state of all keyboard binds. 640 // That way, if we had a key held down, and lost focus, the bind won't be stuck enabled because we never 641 // got the key release message, because it happened in another window which "stole" the event. 642 g_emu_thread->clearInputBindStateFromSource(InputManager::MakeHostKeyboardKey(0)); 643 } 644 else 645 { 646 if (m_was_paused_by_focus_loss) 647 { 648 if (s_system_paused) 649 g_emu_thread->setSystemPaused(false); 650 m_was_paused_by_focus_loss = false; 651 } 652 } 653 } 654 655 void MainWindow::onStartFileActionTriggered() 656 { 657 QString filename = QDir::toNativeSeparators( 658 QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr)); 659 if (filename.isEmpty()) 660 return; 661 662 startFileOrChangeDisc(filename); 663 } 664 665 std::string MainWindow::getDeviceDiscPath(const QString& title) 666 { 667 std::string ret; 668 669 auto devices = CDImage::GetDeviceList(); 670 if (devices.empty()) 671 { 672 QMessageBox::critical(this, title, 673 tr("Could not find any CD-ROM devices. Please ensure you have a CD-ROM drive connected and " 674 "sufficient permissions to access it.")); 675 return ret; 676 } 677 678 // if there's only one, select it automatically 679 if (devices.size() == 1) 680 { 681 ret = std::move(devices.front().first); 682 return ret; 683 } 684 685 QStringList input_options; 686 for (const auto& [path, name] : devices) 687 input_options.append(tr("%1 (%2)").arg(QString::fromStdString(name)).arg(QString::fromStdString(path))); 688 689 QInputDialog input_dialog(this); 690 input_dialog.setWindowTitle(title); 691 input_dialog.setLabelText(tr("Select disc drive:")); 692 input_dialog.setInputMode(QInputDialog::TextInput); 693 input_dialog.setOptions(QInputDialog::UseListViewForComboBoxItems); 694 input_dialog.setComboBoxEditable(false); 695 input_dialog.setComboBoxItems(std::move(input_options)); 696 if (input_dialog.exec() == 0) 697 return ret; 698 699 const int selected_index = input_dialog.comboBoxItems().indexOf(input_dialog.textValue()); 700 if (selected_index < 0 || static_cast<u32>(selected_index) >= devices.size()) 701 return ret; 702 703 ret = std::move(devices[selected_index].first); 704 return ret; 705 } 706 707 void MainWindow::quit() 708 { 709 // Make sure VM is gone. It really should be if we're here. 710 if (s_system_valid) 711 { 712 g_emu_thread->shutdownSystem(false, true); 713 while (s_system_valid) 714 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); 715 } 716 717 // Big picture might still be active. 718 if (m_display_created) 719 g_emu_thread->stopFullscreenUI(); 720 721 // Ensure subwindows are removed before quitting. That way the log window cancelling 722 // the close event won't cancel the quit process. 723 destroySubWindows(); 724 QGuiApplication::quit(); 725 } 726 727 void MainWindow::recreate() 728 { 729 std::optional<QPoint> settings_window_pos; 730 int settings_window_row = 0; 731 std::optional<QPoint> controller_settings_window_pos; 732 ControllerSettingsWindow::Category controller_settings_window_row = 733 ControllerSettingsWindow::Category::GlobalSettings; 734 if (m_settings_window && m_settings_window->isVisible()) 735 { 736 settings_window_pos = m_settings_window->pos(); 737 settings_window_row = m_settings_window->getCategoryRow(); 738 } 739 if (m_controller_settings_window && m_controller_settings_window->isVisible()) 740 { 741 controller_settings_window_pos = m_controller_settings_window->pos(); 742 controller_settings_window_row = m_controller_settings_window->getCurrentCategory(); 743 } 744 745 // Remove subwindows before switching to surfaceless, because otherwise e.g. the debugger can cause funkyness. 746 destroySubWindows(); 747 748 const bool was_display_created = m_display_created; 749 if (was_display_created) 750 { 751 g_emu_thread->setSurfaceless(true); 752 while (m_display_widget || !g_emu_thread->isSurfaceless()) 753 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); 754 755 m_display_created = false; 756 } 757 758 // We need to close input sources, because e.g. DInput uses our window handle. 759 g_emu_thread->closeInputSources(); 760 761 close(); 762 g_main_window = nullptr; 763 764 MainWindow* new_main_window = new MainWindow(); 765 DebugAssert(g_main_window == new_main_window); 766 new_main_window->show(); 767 deleteLater(); 768 769 // Recreate log window as well. Then make sure we're still on top. 770 LogWindow::updateSettings(); 771 new_main_window->raise(); 772 new_main_window->activateWindow(); 773 774 // Reload the sources we just closed. 775 g_emu_thread->reloadInputSources(); 776 777 if (was_display_created) 778 { 779 g_emu_thread->setSurfaceless(false); 780 g_main_window->updateEmulationActions(false, System::IsValid(), Achievements::IsHardcoreModeActive()); 781 g_main_window->onFullscreenUIStateChange(g_emu_thread->isRunningFullscreenUI()); 782 } 783 784 if (settings_window_pos.has_value()) 785 { 786 SettingsWindow* dlg = g_main_window->getSettingsWindow(); 787 dlg->move(settings_window_pos.value()); 788 dlg->setCategoryRow(settings_window_row); 789 QtUtils::ShowOrRaiseWindow(dlg); 790 } 791 if (controller_settings_window_pos.has_value()) 792 { 793 ControllerSettingsWindow* dlg = g_main_window->getControllerSettingsWindow(); 794 dlg->move(controller_settings_window_pos.value()); 795 dlg->setCategory(controller_settings_window_row); 796 QtUtils::ShowOrRaiseWindow(dlg); 797 } 798 } 799 800 void MainWindow::destroySubWindows() 801 { 802 QtUtils::CloseAndDeleteWindow(m_memory_scanner_window); 803 QtUtils::CloseAndDeleteWindow(m_debugger_window); 804 QtUtils::CloseAndDeleteWindow(m_cheat_manager_window); 805 QtUtils::CloseAndDeleteWindow(m_memory_card_editor_window); 806 QtUtils::CloseAndDeleteWindow(m_controller_settings_window); 807 QtUtils::CloseAndDeleteWindow(m_settings_window); 808 809 SettingsWindow::closeGamePropertiesDialogs(); 810 811 LogWindow::destroy(); 812 } 813 814 void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu) 815 { 816 QAction* resume_action = nullptr; 817 QMenu* load_state_menu = nullptr; 818 819 if (!entry->IsDiscSet()) 820 { 821 resume_action = menu->addAction(tr("Resume")); 822 resume_action->setEnabled(false); 823 824 load_state_menu = menu->addMenu(tr("Load State")); 825 load_state_menu->setEnabled(false); 826 827 if (!entry->serial.empty()) 828 { 829 std::vector<SaveStateInfo> available_states(System::GetAvailableSaveStates(entry->serial.c_str())); 830 const QString timestamp_format = QLocale::system().dateTimeFormat(QLocale::ShortFormat); 831 const bool challenge_mode = Achievements::IsHardcoreModeActive(); 832 for (SaveStateInfo& ssi : available_states) 833 { 834 if (ssi.global) 835 continue; 836 837 const s32 slot = ssi.slot; 838 const QDateTime timestamp(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(ssi.timestamp))); 839 const QString timestamp_str(timestamp.toString(timestamp_format)); 840 841 QAction* action; 842 if (slot < 0) 843 { 844 resume_action->setText(tr("Resume (%1)").arg(timestamp_str)); 845 resume_action->setEnabled(!challenge_mode); 846 action = resume_action; 847 } 848 else 849 { 850 load_state_menu->setEnabled(true); 851 action = load_state_menu->addAction(tr("Game Save %1 (%2)").arg(slot).arg(timestamp_str)); 852 } 853 854 action->setDisabled(challenge_mode); 855 connect(action, &QAction::triggered, 856 [this, entry, path = std::move(ssi.path)]() { startFile(entry->path, std::move(path), std::nullopt); }); 857 } 858 } 859 } 860 861 QAction* open_memory_cards_action = menu->addAction(tr("Edit Memory Cards...")); 862 connect(open_memory_cards_action, &QAction::triggered, [entry]() { 863 QString paths[2]; 864 for (u32 i = 0; i < 2; i++) 865 paths[i] = QString::fromStdString(System::GetGameMemoryCardPath(entry->serial, entry->path, i)); 866 867 g_main_window->openMemoryCardEditor(paths[0], paths[1]); 868 }); 869 870 if (!entry->IsDiscSet()) 871 { 872 const bool has_any_states = resume_action->isEnabled() || load_state_menu->isEnabled(); 873 QAction* delete_save_states_action = menu->addAction(tr("Delete Save States...")); 874 delete_save_states_action->setEnabled(has_any_states); 875 if (has_any_states) 876 { 877 connect(delete_save_states_action, &QAction::triggered, [parent_window, entry] { 878 if (QMessageBox::warning( 879 parent_window, tr("Confirm Save State Deletion"), 880 tr("Are you sure you want to delete all save states for %1?\n\nThe saves will not be recoverable.") 881 .arg(QString::fromStdString(entry->serial)), 882 QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) 883 { 884 return; 885 } 886 887 System::DeleteSaveStates(entry->serial.c_str(), true); 888 }); 889 } 890 } 891 } 892 893 static QString FormatTimestampForSaveStateMenu(u64 timestamp) 894 { 895 const QDateTime qtime(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(timestamp))); 896 return qtime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); 897 } 898 899 void MainWindow::populateLoadStateMenu(const char* game_serial, QMenu* menu) 900 { 901 auto add_slot = [this, game_serial, menu](const QString& title, const QString& empty_title, bool global, s32 slot) { 902 std::optional<SaveStateInfo> ssi = System::GetSaveStateInfo(global ? nullptr : game_serial, slot); 903 904 const QString menu_title = 905 ssi.has_value() ? title.arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot); 906 907 QAction* load_action = menu->addAction(menu_title); 908 load_action->setEnabled(ssi.has_value()); 909 if (ssi.has_value()) 910 { 911 const QString path(QString::fromStdString(ssi->path)); 912 connect(load_action, &QAction::triggered, this, [path]() { g_emu_thread->loadState(path); }); 913 } 914 }; 915 916 menu->clear(); 917 918 connect(menu->addAction(tr("Load From File...")), &QAction::triggered, []() { 919 const QString path = QDir::toNativeSeparators( 920 QFileDialog::getOpenFileName(g_main_window, tr("Select Save State File"), QString(), tr("Save States (*.sav)"))); 921 if (path.isEmpty()) 922 return; 923 924 g_emu_thread->loadState(path); 925 }); 926 QAction* load_from_state = menu->addAction(tr("Undo Load State")); 927 load_from_state->setEnabled(System::CanUndoLoadState()); 928 connect(load_from_state, &QAction::triggered, g_emu_thread, &EmuThread::undoLoadState); 929 menu->addSeparator(); 930 931 if (game_serial && std::strlen(game_serial) > 0) 932 { 933 for (u32 slot = 1; slot <= System::PER_GAME_SAVE_STATE_SLOTS; slot++) 934 add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), false, static_cast<s32>(slot)); 935 936 menu->addSeparator(); 937 } 938 939 for (u32 slot = 1; slot <= System::GLOBAL_SAVE_STATE_SLOTS; slot++) 940 add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), true, static_cast<s32>(slot)); 941 } 942 943 void MainWindow::populateSaveStateMenu(const char* game_serial, QMenu* menu) 944 { 945 auto add_slot = [game_serial, menu](const QString& title, const QString& empty_title, bool global, s32 slot) { 946 std::optional<SaveStateInfo> ssi = System::GetSaveStateInfo(global ? nullptr : game_serial, slot); 947 948 const QString menu_title = 949 ssi.has_value() ? title.arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot); 950 951 QAction* save_action = menu->addAction(menu_title); 952 connect(save_action, &QAction::triggered, [global, slot]() { g_emu_thread->saveState(global, slot); }); 953 }; 954 955 menu->clear(); 956 957 connect(menu->addAction(tr("Save To File...")), &QAction::triggered, []() { 958 if (!System::IsValid()) 959 return; 960 961 const QString path = QDir::toNativeSeparators( 962 QFileDialog::getSaveFileName(g_main_window, tr("Select Save State File"), QString(), tr("Save States (*.sav)"))); 963 if (path.isEmpty()) 964 return; 965 966 g_emu_thread->saveState(QDir::toNativeSeparators(path)); 967 }); 968 menu->addSeparator(); 969 970 if (game_serial && std::strlen(game_serial) > 0) 971 { 972 for (u32 slot = 1; slot <= System::PER_GAME_SAVE_STATE_SLOTS; slot++) 973 add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), false, static_cast<s32>(slot)); 974 975 menu->addSeparator(); 976 } 977 978 for (u32 slot = 1; slot <= System::GLOBAL_SAVE_STATE_SLOTS; slot++) 979 add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), true, static_cast<s32>(slot)); 980 } 981 982 void MainWindow::populateChangeDiscSubImageMenu(QMenu* menu, QActionGroup* action_group) 983 { 984 if (!s_system_valid) 985 return; 986 987 if (System::HasMediaSubImages()) 988 { 989 const u32 count = System::GetMediaSubImageCount(); 990 const u32 current = System::GetMediaSubImageIndex(); 991 for (u32 i = 0; i < count; i++) 992 { 993 QAction* action = action_group->addAction(QString::fromStdString(System::GetMediaSubImageTitle(i))); 994 action->setCheckable(true); 995 action->setChecked(i == current); 996 connect(action, &QAction::triggered, [i]() { g_emu_thread->changeDiscFromPlaylist(i); }); 997 menu->addAction(action); 998 } 999 } 1000 else if (const GameDatabase::Entry* entry = System::GetGameDatabaseEntry(); entry && !entry->disc_set_serials.empty()) 1001 { 1002 auto lock = GameList::GetLock(); 1003 for (const auto& [title, glentry] : GameList::GetMatchingEntriesForSerial(entry->disc_set_serials)) 1004 { 1005 QAction* action = action_group->addAction(QString::fromStdString(title)); 1006 QString path = QString::fromStdString(glentry->path); 1007 action->setCheckable(true); 1008 action->setChecked(path == s_current_game_path); 1009 connect(action, &QAction::triggered, [path = std::move(path)]() { g_emu_thread->changeDisc(path, false, true); }); 1010 menu->addAction(action); 1011 } 1012 } 1013 } 1014 1015 void MainWindow::updateCheatActionsVisibility() 1016 { 1017 // If the cheat system is disabled, put an action to enable it in place of the menu under System. 1018 const bool cheats_enabled = Host::GetBoolSettingValue("Console", "EnableCheats", false); 1019 m_ui.actionCheats->setVisible(!cheats_enabled); 1020 m_ui.menuCheats->menuAction()->setVisible(cheats_enabled); 1021 } 1022 1023 void MainWindow::onCheatsActionTriggered() 1024 { 1025 const bool cheats_enabled = Host::GetBoolSettingValue("Console", "EnableCheats", false); 1026 if (cheats_enabled) 1027 { 1028 m_ui.menuCheats->exec(QCursor::pos()); 1029 return; 1030 } 1031 1032 SystemLock lock(pauseAndLockSystem()); 1033 QMessageBox mb(this); 1034 mb.setWindowTitle(tr("Enable Cheats")); 1035 mb.setText( 1036 tr("Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, and corrupted " 1037 "saves. By using the cheat manager, you agree that it is an unsupported configuration, and we will not " 1038 "provide you with any assistance when games break.\n\nCheats persist through save states even after being " 1039 "disabled, please remember to reset/reboot the game after turning off any codes.\n\nAre you sure you want " 1040 "to continue?")); 1041 mb.setIcon(QMessageBox::Warning); 1042 QPushButton* global = mb.addButton(tr("Enable For All Games"), QMessageBox::DestructiveRole); 1043 QPushButton* game = mb.addButton(tr("Enable For This Game"), QMessageBox::AcceptRole); 1044 game->setEnabled(s_system_valid && !s_current_game_serial.isEmpty()); 1045 QPushButton* cancel = mb.addButton(tr("Cancel"), QMessageBox::RejectRole); 1046 mb.setDefaultButton(cancel); 1047 mb.setEscapeButton(cancel); 1048 mb.exec(); 1049 1050 if (mb.clickedButton() == global) 1051 { 1052 // enable globally 1053 Host::SetBaseBoolSettingValue("Console", "EnableCheats", true); 1054 Host::CommitBaseSettingChanges(); 1055 g_emu_thread->applySettings(false); 1056 } 1057 else if (mb.clickedButton() == game) 1058 { 1059 if (!SettingsWindow::setGameSettingsBoolForSerial(s_current_game_serial.toStdString(), "Console", "EnableCheats", 1060 true)) 1061 { 1062 QMessageBox::critical(this, tr("Error"), tr("Failed to enable cheats for %1.").arg(s_current_game_serial)); 1063 return; 1064 } 1065 1066 g_emu_thread->reloadGameSettings(false); 1067 } 1068 else 1069 { 1070 // do nothing 1071 return; 1072 } 1073 } 1074 1075 void MainWindow::onCheatsMenuAboutToShow() 1076 { 1077 m_ui.menuCheats->clear(); 1078 connect(m_ui.menuCheats->addAction(tr("Cheat Manager")), &QAction::triggered, this, &MainWindow::openCheatManager); 1079 m_ui.menuCheats->addSeparator(); 1080 populateCheatsMenu(m_ui.menuCheats); 1081 } 1082 1083 void MainWindow::populateCheatsMenu(QMenu* menu) 1084 { 1085 const bool has_cheat_list = (s_system_valid && System::HasCheatList()); 1086 1087 QMenu* enabled_menu = menu->addMenu(tr("&Enabled Cheats")); 1088 enabled_menu->setEnabled(s_system_valid); 1089 QMenu* apply_menu = menu->addMenu(tr("&Apply Cheats")); 1090 apply_menu->setEnabled(s_system_valid); 1091 1092 if (has_cheat_list) 1093 { 1094 CheatList* cl = System::GetCheatList(); 1095 for (const std::string& group : cl->GetCodeGroups()) 1096 { 1097 QMenu* enabled_submenu = nullptr; 1098 QMenu* apply_submenu = nullptr; 1099 1100 for (u32 i = 0; i < cl->GetCodeCount(); i++) 1101 { 1102 CheatCode& cc = cl->GetCode(i); 1103 if (cc.group != group) 1104 continue; 1105 1106 QString desc(QString::fromStdString(cc.description)); 1107 if (cc.IsManuallyActivated()) 1108 { 1109 if (!apply_submenu) 1110 { 1111 apply_menu->setEnabled(true); 1112 apply_submenu = apply_menu->addMenu(QString::fromStdString(group)); 1113 } 1114 1115 QAction* action = apply_submenu->addAction(desc); 1116 connect(action, &QAction::triggered, [i]() { g_emu_thread->applyCheat(i); }); 1117 } 1118 else 1119 { 1120 if (!enabled_submenu) 1121 { 1122 enabled_menu->setEnabled(true); 1123 enabled_submenu = enabled_menu->addMenu(QString::fromStdString(group)); 1124 } 1125 1126 QAction* action = enabled_submenu->addAction(desc); 1127 action->setCheckable(true); 1128 action->setChecked(cc.enabled); 1129 connect(action, &QAction::toggled, [i](bool enabled) { g_emu_thread->setCheatEnabled(i, enabled); }); 1130 } 1131 } 1132 } 1133 } 1134 } 1135 1136 const GameList::Entry* MainWindow::resolveDiscSetEntry(const GameList::Entry* entry, 1137 std::unique_lock<std::recursive_mutex>& lock) 1138 { 1139 if (!entry || entry->type != GameList::EntryType::DiscSet) 1140 return entry; 1141 1142 // disc set... need to figure out the disc we want 1143 SelectDiscDialog dlg(entry->path, this); 1144 1145 lock.unlock(); 1146 const int res = dlg.exec(); 1147 lock.lock(); 1148 1149 return res ? GameList::GetEntryForPath(dlg.getSelectedDiscPath()) : nullptr; 1150 } 1151 1152 std::shared_ptr<SystemBootParameters> MainWindow::getSystemBootParameters(std::string file) 1153 { 1154 std::shared_ptr<SystemBootParameters> ret = std::make_shared<SystemBootParameters>(std::move(file)); 1155 ret->start_media_capture = m_ui.actionMediaCapture->isChecked(); 1156 return ret; 1157 } 1158 1159 std::optional<bool> MainWindow::promptForResumeState(const std::string& save_state_path) 1160 { 1161 FILESYSTEM_STAT_DATA sd; 1162 if (save_state_path.empty() || !FileSystem::StatFile(save_state_path.c_str(), &sd)) 1163 return false; 1164 1165 QMessageBox msgbox(this); 1166 msgbox.setIcon(QMessageBox::Question); 1167 msgbox.setWindowTitle(tr("Load Resume State")); 1168 msgbox.setWindowModality(Qt::WindowModal); 1169 msgbox.setText(tr("A resume save state was found for this game, saved at:\n\n%1.\n\nDo you want to load this state, " 1170 "or start from a fresh boot?") 1171 .arg(QDateTime::fromSecsSinceEpoch(sd.ModificationTime, Qt::UTC).toLocalTime().toString())); 1172 1173 QPushButton* load = msgbox.addButton(tr("Load State"), QMessageBox::AcceptRole); 1174 QPushButton* boot = msgbox.addButton(tr("Fresh Boot"), QMessageBox::RejectRole); 1175 QPushButton* delboot = msgbox.addButton(tr("Delete And Boot"), QMessageBox::RejectRole); 1176 msgbox.addButton(QMessageBox::Cancel); 1177 msgbox.setDefaultButton(load); 1178 msgbox.exec(); 1179 1180 QAbstractButton* clicked = msgbox.clickedButton(); 1181 if (load == clicked) 1182 { 1183 return true; 1184 } 1185 else if (boot == clicked) 1186 { 1187 return false; 1188 } 1189 else if (delboot == clicked) 1190 { 1191 if (!FileSystem::DeleteFile(save_state_path.c_str())) 1192 { 1193 QMessageBox::critical(this, tr("Error"), 1194 tr("Failed to delete save state file '%1'.").arg(QString::fromStdString(save_state_path))); 1195 } 1196 1197 return false; 1198 } 1199 1200 return std::nullopt; 1201 } 1202 1203 void MainWindow::startFile(std::string path, std::optional<std::string> save_path, std::optional<bool> fast_boot) 1204 { 1205 std::shared_ptr<SystemBootParameters> params = getSystemBootParameters(std::move(path)); 1206 params->override_fast_boot = fast_boot; 1207 if (save_path.has_value()) 1208 params->save_state = std::move(save_path.value()); 1209 1210 g_emu_thread->bootSystem(std::move(params)); 1211 } 1212 1213 void MainWindow::startFileOrChangeDisc(const QString& path) 1214 { 1215 if (s_system_valid) 1216 { 1217 // this is a disc change 1218 promptForDiscChange(path); 1219 return; 1220 } 1221 1222 // try to find the serial for the game 1223 std::string path_str(path.toStdString()); 1224 std::string serial(GameDatabase::GetSerialForPath(path_str.c_str())); 1225 std::optional<std::string> save_path; 1226 if (!serial.empty()) 1227 { 1228 std::string resume_path(System::GetGameSaveStateFileName(serial.c_str(), -1)); 1229 std::optional<bool> resume = promptForResumeState(resume_path); 1230 if (!resume.has_value()) 1231 { 1232 // cancelled 1233 return; 1234 } 1235 else if (resume.value()) 1236 save_path = std::move(resume_path); 1237 } 1238 1239 // only resume if the option is enabled, and we have one for this game 1240 startFile(std::move(path_str), std::move(save_path), std::nullopt); 1241 } 1242 1243 void MainWindow::promptForDiscChange(const QString& path) 1244 { 1245 SystemLock lock(pauseAndLockSystem()); 1246 1247 bool reset_system = false; 1248 if (!m_was_disc_change_request) 1249 { 1250 QMessageBox mb(QMessageBox::Question, tr("Confirm Disc Change"), 1251 tr("Do you want to swap discs or boot the new image (via system reset)?"), QMessageBox::NoButton, 1252 this); 1253 /*const QAbstractButton* const swap_button = */ mb.addButton(tr("Swap Disc"), QMessageBox::YesRole); 1254 const QAbstractButton* const reset_button = mb.addButton(tr("Reset"), QMessageBox::NoRole); 1255 const QAbstractButton* const cancel_button = mb.addButton(tr("Cancel"), QMessageBox::RejectRole); 1256 mb.exec(); 1257 1258 const QAbstractButton* const clicked_button = mb.clickedButton(); 1259 if (!clicked_button || clicked_button == cancel_button) 1260 return; 1261 1262 reset_system = (clicked_button == reset_button); 1263 } 1264 1265 switchToEmulationView(); 1266 1267 g_emu_thread->changeDisc(path, reset_system, true); 1268 } 1269 1270 void MainWindow::onStartDiscActionTriggered() 1271 { 1272 std::string path(getDeviceDiscPath(tr("Start Disc"))); 1273 if (path.empty()) 1274 return; 1275 1276 g_emu_thread->bootSystem(getSystemBootParameters(std::move(path))); 1277 } 1278 1279 void MainWindow::onStartBIOSActionTriggered() 1280 { 1281 g_emu_thread->bootSystem(getSystemBootParameters(std::string())); 1282 } 1283 1284 void MainWindow::onChangeDiscFromFileActionTriggered() 1285 { 1286 QString filename = QDir::toNativeSeparators( 1287 QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr)); 1288 if (filename.isEmpty()) 1289 return; 1290 1291 g_emu_thread->changeDisc(filename, false, true); 1292 } 1293 1294 void MainWindow::onChangeDiscFromGameListActionTriggered() 1295 { 1296 m_was_disc_change_request = true; 1297 switchToGameListView(); 1298 } 1299 1300 void MainWindow::onChangeDiscFromDeviceActionTriggered() 1301 { 1302 std::string path(getDeviceDiscPath(tr("Change Disc"))); 1303 if (path.empty()) 1304 return; 1305 1306 g_emu_thread->changeDisc(QString::fromStdString(path), false, true); 1307 } 1308 1309 void MainWindow::onChangeDiscMenuAboutToShow() 1310 { 1311 populateChangeDiscSubImageMenu(m_ui.menuChangeDisc, m_ui.actionGroupChangeDiscSubImages); 1312 } 1313 1314 void MainWindow::onChangeDiscMenuAboutToHide() 1315 { 1316 for (QAction* action : m_ui.actionGroupChangeDiscSubImages->actions()) 1317 { 1318 m_ui.actionGroupChangeDiscSubImages->removeAction(action); 1319 m_ui.menuChangeDisc->removeAction(action); 1320 action->deleteLater(); 1321 } 1322 } 1323 1324 void MainWindow::onLoadStateMenuAboutToShow() 1325 { 1326 populateLoadStateMenu(s_current_game_serial.toUtf8().constData(), m_ui.menuLoadState); 1327 } 1328 1329 void MainWindow::onSaveStateMenuAboutToShow() 1330 { 1331 populateSaveStateMenu(s_current_game_serial.toUtf8().constData(), m_ui.menuSaveState); 1332 } 1333 1334 void MainWindow::onStartFullscreenUITriggered() 1335 { 1336 if (m_display_widget) 1337 g_emu_thread->stopFullscreenUI(); 1338 else 1339 g_emu_thread->startFullscreenUI(); 1340 } 1341 1342 void MainWindow::onFullscreenUIStateChange(bool running) 1343 { 1344 m_ui.actionStartFullscreenUI->setText(running ? tr("Stop Big Picture Mode") : tr("Start Big Picture Mode")); 1345 m_ui.actionStartFullscreenUI2->setText(running ? tr("Exit Big Picture") : tr("Big Picture")); 1346 } 1347 1348 void MainWindow::onRemoveDiscActionTriggered() 1349 { 1350 g_emu_thread->changeDisc(QString(), false, true); 1351 } 1352 1353 void MainWindow::onViewToolbarActionToggled(bool checked) 1354 { 1355 Host::SetBaseBoolSettingValue("UI", "ShowToolbar", checked); 1356 Host::CommitBaseSettingChanges(); 1357 m_ui.toolBar->setVisible(checked); 1358 } 1359 1360 void MainWindow::onViewLockToolbarActionToggled(bool checked) 1361 { 1362 Host::SetBaseBoolSettingValue("UI", "LockToolbar", checked); 1363 Host::CommitBaseSettingChanges(); 1364 m_ui.toolBar->setMovable(!checked); 1365 } 1366 1367 void MainWindow::onViewStatusBarActionToggled(bool checked) 1368 { 1369 Host::SetBaseBoolSettingValue("UI", "ShowStatusBar", checked); 1370 Host::CommitBaseSettingChanges(); 1371 m_ui.statusBar->setVisible(checked); 1372 } 1373 1374 void MainWindow::onViewGameListActionTriggered() 1375 { 1376 switchToGameListView(); 1377 m_game_list_widget->showGameList(); 1378 } 1379 1380 void MainWindow::onViewGameGridActionTriggered() 1381 { 1382 switchToGameListView(); 1383 m_game_list_widget->showGameGrid(); 1384 } 1385 1386 void MainWindow::onViewSystemDisplayTriggered() 1387 { 1388 if (m_display_created) 1389 switchToEmulationView(); 1390 } 1391 1392 void MainWindow::onViewGamePropertiesActionTriggered() 1393 { 1394 if (!s_system_valid) 1395 return; 1396 1397 Host::RunOnCPUThread([]() { 1398 const std::string& path = System::GetDiscPath(); 1399 const std::string& serial = System::GetGameSerial(); 1400 if (path.empty() || serial.empty()) 1401 return; 1402 1403 QtHost::RunOnUIThread([path = path, serial = serial]() { 1404 SettingsWindow::openGamePropertiesDialog(path, System::GetGameTitle(), serial, System::GetDiscRegion()); 1405 }); 1406 }); 1407 } 1408 1409 void MainWindow::onGitHubRepositoryActionTriggered() 1410 { 1411 QtUtils::OpenURL(this, "https://github.com/stenzek/duckstation/"); 1412 } 1413 1414 void MainWindow::onIssueTrackerActionTriggered() 1415 { 1416 QtUtils::OpenURL(this, "https://www.duckstation.org/issues.html"); 1417 } 1418 1419 void MainWindow::onDiscordServerActionTriggered() 1420 { 1421 QtUtils::OpenURL(this, "https://www.duckstation.org/discord.html"); 1422 } 1423 1424 void MainWindow::onAboutActionTriggered() 1425 { 1426 AboutDialog about(this); 1427 about.exec(); 1428 } 1429 1430 void MainWindow::onGameListRefreshProgress(const QString& status, int current, int total) 1431 { 1432 m_ui.statusBar->showMessage(status); 1433 setProgressBar(current, total); 1434 } 1435 1436 void MainWindow::onGameListRefreshComplete() 1437 { 1438 m_ui.statusBar->clearMessage(); 1439 clearProgressBar(); 1440 } 1441 1442 void MainWindow::onGameListSelectionChanged() 1443 { 1444 auto lock = GameList::GetLock(); 1445 const GameList::Entry* entry = m_game_list_widget->getSelectedEntry(); 1446 if (!entry) 1447 return; 1448 1449 m_ui.statusBar->showMessage(QString::fromStdString(entry->path)); 1450 } 1451 1452 void MainWindow::onGameListEntryActivated() 1453 { 1454 auto lock = GameList::GetLock(); 1455 const GameList::Entry* entry = resolveDiscSetEntry(m_game_list_widget->getSelectedEntry(), lock); 1456 if (!entry) 1457 return; 1458 1459 if (s_system_valid) 1460 { 1461 // change disc on double click 1462 if (!entry->IsDisc()) 1463 { 1464 QMessageBox::critical(this, tr("Error"), tr("You must select a disc to change discs.")); 1465 return; 1466 } 1467 1468 promptForDiscChange(QString::fromStdString(entry->path)); 1469 return; 1470 } 1471 1472 std::optional<std::string> save_path; 1473 if (!entry->serial.empty()) 1474 { 1475 std::string resume_path(System::GetGameSaveStateFileName(entry->serial.c_str(), -1)); 1476 std::optional<bool> resume = promptForResumeState(resume_path); 1477 if (!resume.has_value()) 1478 { 1479 // cancelled 1480 return; 1481 } 1482 else if (resume.value()) 1483 save_path = std::move(resume_path); 1484 } 1485 1486 // only resume if the option is enabled, and we have one for this game 1487 startFile(entry->path, std::move(save_path), std::nullopt); 1488 } 1489 1490 void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) 1491 { 1492 auto lock = GameList::GetLock(); 1493 const GameList::Entry* entry = m_game_list_widget->getSelectedEntry(); 1494 1495 QMenu menu; 1496 1497 // Hopefully this pointer doesn't disappear... it shouldn't. 1498 if (entry) 1499 { 1500 if (!entry->IsDiscSet()) 1501 { 1502 connect(menu.addAction(tr("Properties...")), &QAction::triggered, [entry]() { 1503 SettingsWindow::openGamePropertiesDialog(entry->path, entry->title, entry->serial, entry->region); 1504 }); 1505 1506 connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() { 1507 const QFileInfo fi(QString::fromStdString(entry->path)); 1508 QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absolutePath())); 1509 }); 1510 1511 connect(menu.addAction(tr("Set Cover Image...")), &QAction::triggered, 1512 [this, entry]() { setGameListEntryCoverImage(entry); }); 1513 1514 menu.addSeparator(); 1515 1516 if (!s_system_valid) 1517 { 1518 populateGameListContextMenu(entry, this, &menu); 1519 menu.addSeparator(); 1520 1521 connect(menu.addAction(tr("Default Boot")), &QAction::triggered, 1522 [this, entry]() { g_emu_thread->bootSystem(getSystemBootParameters(entry->path)); }); 1523 1524 connect(menu.addAction(tr("Fast Boot")), &QAction::triggered, [this, entry]() { 1525 std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path); 1526 boot_params->override_fast_boot = true; 1527 g_emu_thread->bootSystem(std::move(boot_params)); 1528 }); 1529 1530 connect(menu.addAction(tr("Full Boot")), &QAction::triggered, [this, entry]() { 1531 std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path); 1532 boot_params->override_fast_boot = false; 1533 g_emu_thread->bootSystem(std::move(boot_params)); 1534 }); 1535 1536 if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::IsHardcoreModeActive()) 1537 { 1538 connect(menu.addAction(tr("Boot and Debug")), &QAction::triggered, [this, entry]() { 1539 m_open_debugger_on_start = true; 1540 1541 std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path); 1542 boot_params->override_start_paused = true; 1543 g_emu_thread->bootSystem(std::move(boot_params)); 1544 }); 1545 } 1546 } 1547 else 1548 { 1549 connect(menu.addAction(tr("Change Disc")), &QAction::triggered, [this, entry]() { 1550 g_emu_thread->changeDisc(QString::fromStdString(entry->path), false, true); 1551 g_emu_thread->setSystemPaused(false); 1552 switchToEmulationView(); 1553 }); 1554 } 1555 1556 menu.addSeparator(); 1557 1558 connect(menu.addAction(tr("Exclude From List")), &QAction::triggered, 1559 [this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); }); 1560 1561 connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered, 1562 [this, entry]() { clearGameListEntryPlayTime(entry); }); 1563 } 1564 else 1565 { 1566 connect(menu.addAction(tr("Properties...")), &QAction::triggered, [disc_set_name = entry->path]() { 1567 // resolve path first 1568 auto lock = GameList::GetLock(); 1569 const GameList::Entry* first_disc = GameList::GetFirstDiscSetMember(disc_set_name); 1570 if (first_disc) 1571 { 1572 SettingsWindow::openGamePropertiesDialog(first_disc->path, first_disc->title, first_disc->serial, 1573 first_disc->region); 1574 } 1575 }); 1576 1577 connect(menu.addAction(tr("Set Cover Image...")), &QAction::triggered, 1578 [this, entry]() { setGameListEntryCoverImage(entry); }); 1579 1580 menu.addSeparator(); 1581 1582 populateGameListContextMenu(entry, this, &menu); 1583 1584 menu.addSeparator(); 1585 1586 connect(menu.addAction(tr("Select Disc")), &QAction::triggered, this, &MainWindow::onGameListEntryActivated); 1587 1588 menu.addSeparator(); 1589 1590 connect(menu.addAction(tr("Exclude From List")), &QAction::triggered, 1591 [this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); }); 1592 } 1593 } 1594 1595 menu.addSeparator(); 1596 1597 connect(menu.addAction(tr("Add Search Directory...")), &QAction::triggered, 1598 [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); }); 1599 1600 menu.exec(point); 1601 } 1602 1603 void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry) 1604 { 1605 const QString filename = QDir::toNativeSeparators(QFileDialog::getOpenFileName( 1606 this, tr("Select Cover Image"), QString(), tr("All Cover Image Types (*.jpg *.jpeg *.png *.webp)"))); 1607 if (filename.isEmpty()) 1608 return; 1609 1610 const QString old_filename = QString::fromStdString(GameList::GetCoverImagePathForEntry(entry)); 1611 const QString new_filename = 1612 QString::fromStdString(GameList::GetNewCoverImagePathForEntry(entry, filename.toUtf8().constData(), false)); 1613 if (new_filename.isEmpty()) 1614 return; 1615 1616 if (!old_filename.isEmpty()) 1617 { 1618 if (QFileInfo(old_filename) == QFileInfo(filename)) 1619 { 1620 QMessageBox::critical(this, tr("Copy Error"), tr("You must select a different file to the current cover image.")); 1621 return; 1622 } 1623 1624 if (QMessageBox::question(this, tr("Cover Already Exists"), 1625 tr("A cover image for this game already exists, do you wish to replace it?"), 1626 QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) 1627 { 1628 return; 1629 } 1630 } 1631 1632 if (QFile::exists(new_filename) && !QFile::remove(new_filename)) 1633 { 1634 QMessageBox::critical(this, tr("Copy Error"), tr("Failed to remove existing cover '%1'").arg(new_filename)); 1635 return; 1636 } 1637 if (!QFile::copy(filename, new_filename)) 1638 { 1639 QMessageBox::critical(this, tr("Copy Error"), tr("Failed to copy '%1' to '%2'").arg(filename).arg(new_filename)); 1640 return; 1641 } 1642 if (!old_filename.isEmpty() && old_filename != new_filename && !QFile::remove(old_filename)) 1643 { 1644 QMessageBox::critical(this, tr("Copy Error"), tr("Failed to remove '%1'").arg(old_filename)); 1645 return; 1646 } 1647 m_game_list_widget->refreshGridCovers(); 1648 } 1649 1650 void MainWindow::clearGameListEntryPlayTime(const GameList::Entry* entry) 1651 { 1652 if (QMessageBox::question( 1653 this, tr("Confirm Reset"), 1654 tr("Are you sure you want to reset the play time for '%1'?\n\nThis action cannot be undone.") 1655 .arg(QString::fromStdString(entry->title))) != QMessageBox::Yes) 1656 { 1657 return; 1658 } 1659 1660 GameList::ClearPlayedTimeForSerial(entry->serial); 1661 m_game_list_widget->refresh(false); 1662 } 1663 1664 void MainWindow::setupAdditionalUi() 1665 { 1666 const bool status_bar_visible = Host::GetBaseBoolSettingValue("UI", "ShowStatusBar", true); 1667 m_ui.actionViewStatusBar->setChecked(status_bar_visible); 1668 m_ui.statusBar->setVisible(status_bar_visible); 1669 1670 const bool toolbar_visible = Host::GetBaseBoolSettingValue("UI", "ShowToolbar", false); 1671 m_ui.actionViewToolbar->setChecked(toolbar_visible); 1672 m_ui.toolBar->setVisible(toolbar_visible); 1673 1674 const bool toolbars_locked = Host::GetBaseBoolSettingValue("UI", "LockToolbar", false); 1675 m_ui.actionViewLockToolbar->setChecked(toolbars_locked); 1676 m_ui.toolBar->setMovable(!toolbars_locked); 1677 m_ui.toolBar->setContextMenuPolicy(Qt::PreventContextMenu); 1678 1679 m_game_list_widget = new GameListWidget(getContentParent()); 1680 m_game_list_widget->initialize(); 1681 m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->isShowingGridCoverTitles()); 1682 m_ui.actionMergeDiscSets->setChecked(m_game_list_widget->isMergingDiscSets()); 1683 m_ui.actionShowGameIcons->setChecked(m_game_list_widget->isShowingGameIcons()); 1684 if (s_use_central_widget) 1685 { 1686 m_ui.mainContainer = nullptr; // setCentralWidget() will delete this 1687 setCentralWidget(m_game_list_widget); 1688 } 1689 else 1690 { 1691 m_ui.mainContainer->addWidget(m_game_list_widget); 1692 } 1693 1694 m_status_progress_widget = new QProgressBar(m_ui.statusBar); 1695 m_status_progress_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 1696 m_status_progress_widget->setFixedSize(140, 16); 1697 m_status_progress_widget->setMinimum(0); 1698 m_status_progress_widget->setMaximum(100); 1699 m_status_progress_widget->hide(); 1700 1701 m_status_renderer_widget = new QLabel(m_ui.statusBar); 1702 m_status_renderer_widget->setFixedHeight(16); 1703 m_status_renderer_widget->setFixedSize(65, 16); 1704 m_status_renderer_widget->hide(); 1705 1706 m_status_resolution_widget = new QLabel(m_ui.statusBar); 1707 m_status_resolution_widget->setFixedHeight(16); 1708 m_status_resolution_widget->setFixedSize(70, 16); 1709 m_status_resolution_widget->hide(); 1710 1711 m_status_fps_widget = new QLabel(m_ui.statusBar); 1712 m_status_fps_widget->setFixedSize(85, 16); 1713 m_status_fps_widget->hide(); 1714 1715 m_status_vps_widget = new QLabel(m_ui.statusBar); 1716 m_status_vps_widget->setFixedSize(125, 16); 1717 m_status_vps_widget->hide(); 1718 1719 m_settings_toolbar_menu = new QMenu(m_ui.toolBar); 1720 m_settings_toolbar_menu->addAction(m_ui.actionSettings); 1721 m_settings_toolbar_menu->addAction(m_ui.actionViewGameProperties); 1722 1723 m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->isShowingGridCoverTitles()); 1724 1725 updateDebugMenuVisibility(); 1726 updateCheatActionsVisibility(); 1727 1728 for (u32 i = 0; i < static_cast<u32>(CPUExecutionMode::Count); i++) 1729 { 1730 const CPUExecutionMode mode = static_cast<CPUExecutionMode>(i); 1731 QAction* action = 1732 m_ui.menuCPUExecutionMode->addAction(QString::fromUtf8(Settings::GetCPUExecutionModeDisplayName(mode))); 1733 action->setCheckable(true); 1734 connect(action, &QAction::triggered, [this, mode]() { 1735 Host::SetBaseStringSettingValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(mode)); 1736 Host::CommitBaseSettingChanges(); 1737 g_emu_thread->applySettings(); 1738 updateDebugMenuCPUExecutionMode(); 1739 }); 1740 } 1741 updateDebugMenuCPUExecutionMode(); 1742 1743 for (u32 i = 0; i < static_cast<u32>(GPURenderer::Count); i++) 1744 { 1745 const GPURenderer renderer = static_cast<GPURenderer>(i); 1746 QAction* action = m_ui.menuRenderer->addAction(QString::fromUtf8(Settings::GetRendererDisplayName(renderer))); 1747 action->setCheckable(true); 1748 connect(action, &QAction::triggered, [this, renderer]() { 1749 Host::SetBaseStringSettingValue("GPU", "Renderer", Settings::GetRendererName(renderer)); 1750 Host::CommitBaseSettingChanges(); 1751 g_emu_thread->applySettings(); 1752 updateDebugMenuGPURenderer(); 1753 }); 1754 } 1755 updateDebugMenuGPURenderer(); 1756 1757 for (u32 i = 0; i < static_cast<u32>(DisplayCropMode::Count); i++) 1758 { 1759 const DisplayCropMode crop_mode = static_cast<DisplayCropMode>(i); 1760 QAction* action = 1761 m_ui.menuCropMode->addAction(QString::fromUtf8(Settings::GetDisplayCropModeDisplayName(crop_mode))); 1762 action->setCheckable(true); 1763 connect(action, &QAction::triggered, [this, crop_mode]() { 1764 Host::SetBaseStringSettingValue("Display", "CropMode", Settings::GetDisplayCropModeName(crop_mode)); 1765 Host::CommitBaseSettingChanges(); 1766 g_emu_thread->applySettings(); 1767 updateDebugMenuCropMode(); 1768 }); 1769 } 1770 updateDebugMenuCropMode(); 1771 1772 for (u32 scale = 1; scale <= 10; scale++) 1773 { 1774 QAction* action = m_ui.menuWindowSize->addAction(tr("%1x Scale").arg(scale)); 1775 connect(action, &QAction::triggered, [scale]() { g_emu_thread->requestDisplaySize(scale); }); 1776 } 1777 1778 #ifdef ENABLE_RAINTEGRATION 1779 if (Achievements::IsUsingRAIntegration()) 1780 { 1781 QMenu* raMenu = new QMenu(QStringLiteral("RAIntegration"), m_ui.menu_Tools); 1782 connect(raMenu, &QMenu::aboutToShow, this, [this, raMenu]() { 1783 raMenu->clear(); 1784 1785 const auto items = Achievements::RAIntegration::GetMenuItems(); 1786 for (const auto& [id, title, checked] : items) 1787 { 1788 if (id == 0) 1789 { 1790 raMenu->addSeparator(); 1791 continue; 1792 } 1793 1794 QAction* raAction = raMenu->addAction(QString::fromUtf8(title)); 1795 if (checked) 1796 { 1797 raAction->setCheckable(true); 1798 raAction->setChecked(checked); 1799 } 1800 1801 connect(raAction, &QAction::triggered, this, 1802 [id = id]() { Host::RunOnCPUThread([id]() { Achievements::RAIntegration::ActivateMenuItem(id); }); }); 1803 } 1804 }); 1805 m_ui.menu_Tools->insertMenu(m_ui.actionOpenDataDirectory, raMenu); 1806 } 1807 #endif 1808 } 1809 1810 void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode) 1811 { 1812 m_ui.actionStartFile->setDisabled(starting || running); 1813 m_ui.actionStartDisc->setDisabled(starting || running); 1814 m_ui.actionStartBios->setDisabled(starting || running); 1815 m_ui.actionResumeLastState->setDisabled(starting || running || cheevos_challenge_mode); 1816 m_ui.actionStartFullscreenUI->setDisabled(starting || running); 1817 m_ui.actionStartFullscreenUI2->setDisabled(starting || running); 1818 1819 m_ui.actionPowerOff->setDisabled(starting || !running); 1820 m_ui.actionPowerOffWithoutSaving->setDisabled(starting || !running); 1821 m_ui.actionReset->setDisabled(starting || !running); 1822 m_ui.actionPause->setDisabled(starting || !running); 1823 m_ui.actionChangeDisc->setDisabled(starting || !running); 1824 m_ui.actionCheats->setDisabled(cheevos_challenge_mode); 1825 m_ui.actionCheatsToolbar->setDisabled(cheevos_challenge_mode); 1826 m_ui.actionScreenshot->setDisabled(starting || !running); 1827 m_ui.menuChangeDisc->setDisabled(starting || !running); 1828 m_ui.menuCheats->setDisabled(cheevos_challenge_mode); 1829 m_ui.actionCPUDebugger->setDisabled(cheevos_challenge_mode); 1830 m_ui.actionMemoryScanner->setDisabled(cheevos_challenge_mode); 1831 m_ui.actionDumpRAM->setDisabled(starting || !running || cheevos_challenge_mode); 1832 m_ui.actionDumpVRAM->setDisabled(starting || !running || cheevos_challenge_mode); 1833 m_ui.actionDumpSPURAM->setDisabled(starting || !running || cheevos_challenge_mode); 1834 1835 m_ui.actionSaveState->setDisabled(starting || !running); 1836 m_ui.menuSaveState->setDisabled(starting || !running); 1837 m_ui.menuWindowSize->setDisabled(starting || !running); 1838 1839 m_ui.actionViewGameProperties->setDisabled(starting || !running); 1840 1841 if (starting || running) 1842 { 1843 if (!m_ui.toolBar->actions().contains(m_ui.actionPowerOff)) 1844 { 1845 m_ui.toolBar->insertAction(m_ui.actionResumeLastState, m_ui.actionPowerOff); 1846 m_ui.toolBar->removeAction(m_ui.actionResumeLastState); 1847 } 1848 } 1849 else 1850 { 1851 if (!m_ui.toolBar->actions().contains(m_ui.actionResumeLastState)) 1852 { 1853 m_ui.toolBar->insertAction(m_ui.actionPowerOff, m_ui.actionResumeLastState); 1854 m_ui.toolBar->removeAction(m_ui.actionPowerOff); 1855 } 1856 1857 m_ui.actionViewGameProperties->setEnabled(false); 1858 } 1859 1860 if (m_open_debugger_on_start && running) 1861 openCPUDebugger(); 1862 if ((!starting && !running) || running) 1863 m_open_debugger_on_start = false; 1864 1865 m_ui.statusBar->clearMessage(); 1866 } 1867 1868 void MainWindow::updateStatusBarWidgetVisibility() 1869 { 1870 auto Update = [this](QWidget* widget, bool visible, int stretch) { 1871 if (widget->isVisible()) 1872 { 1873 m_ui.statusBar->removeWidget(widget); 1874 widget->hide(); 1875 } 1876 1877 if (visible) 1878 { 1879 m_ui.statusBar->addPermanentWidget(widget, stretch); 1880 widget->show(); 1881 } 1882 }; 1883 1884 Update(m_status_renderer_widget, s_system_valid && !s_system_paused, 0); 1885 Update(m_status_resolution_widget, s_system_valid && !s_system_paused, 0); 1886 Update(m_status_fps_widget, s_system_valid && !s_system_paused, 0); 1887 Update(m_status_vps_widget, s_system_valid && !s_system_paused, 0); 1888 } 1889 1890 void MainWindow::updateWindowTitle() 1891 { 1892 QString suffix(QtHost::GetAppConfigSuffix()); 1893 QString main_title(QtHost::GetAppNameAndVersion() + suffix); 1894 QString display_title(s_current_game_title + suffix); 1895 1896 if (!s_system_valid || s_current_game_title.isEmpty()) 1897 display_title = main_title; 1898 else if (isRenderingToMain()) 1899 main_title = display_title; 1900 1901 if (windowTitle() != main_title) 1902 setWindowTitle(main_title); 1903 setWindowIcon(s_current_game_icon.isNull() ? QtHost::GetAppIcon() : s_current_game_icon); 1904 1905 if (m_display_widget && !isRenderingToMain()) 1906 { 1907 QWidget* container = 1908 m_display_container ? static_cast<QWidget*>(m_display_container) : static_cast<QWidget*>(m_display_widget); 1909 if (container->windowTitle() != display_title) 1910 container->setWindowTitle(display_title); 1911 container->setWindowIcon(s_current_game_icon.isNull() ? QtHost::GetAppIcon() : s_current_game_icon); 1912 } 1913 1914 if (g_log_window) 1915 g_log_window->updateWindowTitle(); 1916 } 1917 1918 void MainWindow::updateWindowState(bool force_visible) 1919 { 1920 // Skip all of this when we're closing, since we don't want to make ourselves visible and cancel it. 1921 if (m_is_closing) 1922 return; 1923 1924 const bool hide_window = !isRenderingToMain() && shouldHideMainWindow(); 1925 const bool disable_resize = Host::GetBoolSettingValue("Main", "DisableWindowResize", false); 1926 const bool has_window = s_system_valid || m_display_widget; 1927 1928 // Need to test both valid and display widget because of startup (vm invalid while window is created). 1929 const bool visible = force_visible || !hide_window || !has_window; 1930 if (isVisible() != visible) 1931 setVisible(visible); 1932 1933 // No point changing realizability if we're not visible. 1934 const bool resizeable = force_visible || !disable_resize || !has_window; 1935 if (visible) 1936 QtUtils::SetWindowResizeable(this, resizeable); 1937 1938 // Update the display widget too if rendering separately. 1939 if (m_display_widget && !isRenderingToMain()) 1940 QtUtils::SetWindowResizeable(getDisplayContainer(), resizeable); 1941 } 1942 1943 void MainWindow::setProgressBar(int current, int total) 1944 { 1945 const int value = (total != 0) ? ((current * 100) / total) : 0; 1946 if (m_status_progress_widget->value() != value) 1947 m_status_progress_widget->setValue(value); 1948 1949 if (m_status_progress_widget->isVisible()) 1950 return; 1951 1952 m_status_progress_widget->show(); 1953 m_ui.statusBar->addPermanentWidget(m_status_progress_widget); 1954 } 1955 1956 void MainWindow::clearProgressBar() 1957 { 1958 if (!m_status_progress_widget->isVisible()) 1959 return; 1960 1961 m_status_progress_widget->hide(); 1962 m_ui.statusBar->removeWidget(m_status_progress_widget); 1963 } 1964 1965 bool MainWindow::isShowingGameList() const 1966 { 1967 if (s_use_central_widget) 1968 return (centralWidget() == m_game_list_widget); 1969 else 1970 return (m_ui.mainContainer->currentIndex() == 0); 1971 } 1972 1973 bool MainWindow::isRenderingFullscreen() const 1974 { 1975 if (!g_gpu_device || !m_display_widget) 1976 return false; 1977 1978 return getDisplayContainer()->isFullScreen(); 1979 } 1980 1981 bool MainWindow::isRenderingToMain() const 1982 { 1983 if (s_use_central_widget) 1984 return (m_display_widget && centralWidget() == m_display_widget); 1985 else 1986 return (m_display_widget && m_ui.mainContainer->indexOf(m_display_widget) == 1); 1987 } 1988 1989 bool MainWindow::shouldHideMouseCursor() const 1990 { 1991 return m_hide_mouse_cursor || 1992 (isRenderingFullscreen() && Host::GetBoolSettingValue("Main", "HideCursorInFullscreen", true)); 1993 } 1994 1995 bool MainWindow::shouldHideMainWindow() const 1996 { 1997 return Host::GetBoolSettingValue("Main", "HideMainWindowWhenRunning", false) || 1998 (g_emu_thread->shouldRenderToMain() && !isRenderingToMain()) || QtHost::InNoGUIMode(); 1999 } 2000 2001 void MainWindow::switchToGameListView() 2002 { 2003 if (!isShowingGameList()) 2004 { 2005 if (m_display_created) 2006 { 2007 m_was_paused_on_surface_loss = s_system_paused; 2008 if (!s_system_paused) 2009 g_emu_thread->setSystemPaused(true); 2010 2011 // switch to surfaceless. we have to wait until the display widget is gone before we swap over. 2012 g_emu_thread->setSurfaceless(true); 2013 while (m_display_widget) 2014 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); 2015 } 2016 } 2017 2018 m_game_list_widget->setFocus(); 2019 } 2020 2021 void MainWindow::switchToEmulationView() 2022 { 2023 if (!m_display_created || !isShowingGameList()) 2024 return; 2025 2026 // we're no longer surfaceless! this will call back to UpdateDisplay(), which will swap the widget out. 2027 g_emu_thread->setSurfaceless(false); 2028 2029 // resume if we weren't paused at switch time 2030 if (s_system_paused && !m_was_paused_on_surface_loss) 2031 g_emu_thread->setSystemPaused(false); 2032 2033 if (m_display_widget) 2034 m_display_widget->setFocus(); 2035 } 2036 2037 void MainWindow::connectSignals() 2038 { 2039 updateEmulationActions(false, false, Achievements::IsHardcoreModeActive()); 2040 2041 connect(qApp, &QGuiApplication::applicationStateChanged, this, &MainWindow::onApplicationStateChanged); 2042 2043 connect(m_ui.actionStartFile, &QAction::triggered, this, &MainWindow::onStartFileActionTriggered); 2044 connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered); 2045 connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBIOSActionTriggered); 2046 connect(m_ui.actionResumeLastState, &QAction::triggered, g_emu_thread, &EmuThread::resumeSystemFromMostRecentState); 2047 connect(m_ui.actionChangeDisc, &QAction::triggered, [this] { m_ui.menuChangeDisc->exec(QCursor::pos()); }); 2048 connect(m_ui.actionChangeDiscFromFile, &QAction::triggered, this, &MainWindow::onChangeDiscFromFileActionTriggered); 2049 connect(m_ui.actionChangeDiscFromDevice, &QAction::triggered, this, 2050 &MainWindow::onChangeDiscFromDeviceActionTriggered); 2051 connect(m_ui.actionChangeDiscFromGameList, &QAction::triggered, this, 2052 &MainWindow::onChangeDiscFromGameListActionTriggered); 2053 connect(m_ui.menuChangeDisc, &QMenu::aboutToShow, this, &MainWindow::onChangeDiscMenuAboutToShow); 2054 connect(m_ui.menuChangeDisc, &QMenu::aboutToHide, this, &MainWindow::onChangeDiscMenuAboutToHide); 2055 connect(m_ui.menuLoadState, &QMenu::aboutToShow, this, &MainWindow::onLoadStateMenuAboutToShow); 2056 connect(m_ui.menuSaveState, &QMenu::aboutToShow, this, &MainWindow::onSaveStateMenuAboutToShow); 2057 connect(m_ui.menuCheats, &QMenu::aboutToShow, this, &MainWindow::onCheatsMenuAboutToShow); 2058 connect(m_ui.actionCheats, &QAction::triggered, this, &MainWindow::onCheatsActionTriggered); 2059 connect(m_ui.actionCheatsToolbar, &QAction::triggered, this, &MainWindow::onCheatsActionTriggered); 2060 connect(m_ui.actionStartFullscreenUI, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered); 2061 connect(m_ui.actionStartFullscreenUI2, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered); 2062 connect(m_ui.actionRemoveDisc, &QAction::triggered, this, &MainWindow::onRemoveDiscActionTriggered); 2063 connect(m_ui.actionAddGameDirectory, &QAction::triggered, 2064 [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); }); 2065 connect(m_ui.actionPowerOff, &QAction::triggered, this, 2066 [this]() { requestShutdown(true, true, g_settings.save_state_on_exit); }); 2067 connect(m_ui.actionPowerOffWithoutSaving, &QAction::triggered, this, 2068 [this]() { requestShutdown(false, false, false); }); 2069 connect(m_ui.actionReset, &QAction::triggered, this, []() { g_emu_thread->resetSystem(true); }); 2070 connect(m_ui.actionPause, &QAction::toggled, this, [](bool active) { g_emu_thread->setSystemPaused(active); }); 2071 connect(m_ui.actionScreenshot, &QAction::triggered, g_emu_thread, &EmuThread::saveScreenshot); 2072 connect(m_ui.actionScanForNewGames, &QAction::triggered, this, [this]() { refreshGameList(false); }); 2073 connect(m_ui.actionRescanAllGames, &QAction::triggered, this, [this]() { refreshGameList(true); }); 2074 connect(m_ui.actionLoadState, &QAction::triggered, this, [this]() { m_ui.menuLoadState->exec(QCursor::pos()); }); 2075 connect(m_ui.actionSaveState, &QAction::triggered, this, [this]() { m_ui.menuSaveState->exec(QCursor::pos()); }); 2076 connect(m_ui.actionExit, &QAction::triggered, this, &MainWindow::close); 2077 connect(m_ui.actionFullscreen, &QAction::triggered, g_emu_thread, &EmuThread::toggleFullscreen); 2078 connect(m_ui.actionSettings, &QAction::triggered, [this]() { doSettings(); }); 2079 connect(m_ui.actionSettings2, &QAction::triggered, this, &MainWindow::onSettingsTriggeredFromToolbar); 2080 connect(m_ui.actionInterfaceSettings, &QAction::triggered, [this]() { doSettings("Interface"); }); 2081 connect(m_ui.actionBIOSSettings, &QAction::triggered, [this]() { doSettings("BIOS"); }); 2082 connect(m_ui.actionConsoleSettings, &QAction::triggered, [this]() { doSettings("Console"); }); 2083 connect(m_ui.actionEmulationSettings, &QAction::triggered, [this]() { doSettings("Emulation"); }); 2084 connect(m_ui.actionGameListSettings, &QAction::triggered, [this]() { doSettings("Game List"); }); 2085 connect(m_ui.actionHotkeySettings, &QAction::triggered, 2086 [this]() { doControllerSettings(ControllerSettingsWindow::Category::HotkeySettings); }); 2087 connect(m_ui.actionControllerSettings, &QAction::triggered, 2088 [this]() { doControllerSettings(ControllerSettingsWindow::Category::GlobalSettings); }); 2089 connect(m_ui.actionMemoryCardSettings, &QAction::triggered, [this]() { doSettings("Memory Cards"); }); 2090 connect(m_ui.actionGraphicsSettings, &QAction::triggered, [this]() { doSettings("Graphics"); }); 2091 connect(m_ui.actionPostProcessingSettings, &QAction::triggered, [this]() { doSettings("Post-Processing"); }); 2092 connect(m_ui.actionAudioSettings, &QAction::triggered, [this]() { doSettings("Audio"); }); 2093 connect(m_ui.actionAchievementSettings, &QAction::triggered, [this]() { doSettings("Achievements"); }); 2094 connect(m_ui.actionFolderSettings, &QAction::triggered, [this]() { doSettings("Folders"); }); 2095 connect(m_ui.actionAdvancedSettings, &QAction::triggered, [this]() { doSettings("Advanced"); }); 2096 connect(m_ui.actionViewToolbar, &QAction::toggled, this, &MainWindow::onViewToolbarActionToggled); 2097 connect(m_ui.actionViewLockToolbar, &QAction::toggled, this, &MainWindow::onViewLockToolbarActionToggled); 2098 connect(m_ui.actionViewStatusBar, &QAction::toggled, this, &MainWindow::onViewStatusBarActionToggled); 2099 connect(m_ui.actionViewGameList, &QAction::triggered, this, &MainWindow::onViewGameListActionTriggered); 2100 connect(m_ui.actionViewGameGrid, &QAction::triggered, this, &MainWindow::onViewGameGridActionTriggered); 2101 connect(m_ui.actionViewSystemDisplay, &QAction::triggered, this, &MainWindow::onViewSystemDisplayTriggered); 2102 connect(m_ui.actionViewGameProperties, &QAction::triggered, this, &MainWindow::onViewGamePropertiesActionTriggered); 2103 connect(m_ui.actionGitHubRepository, &QAction::triggered, this, &MainWindow::onGitHubRepositoryActionTriggered); 2104 connect(m_ui.actionIssueTracker, &QAction::triggered, this, &MainWindow::onIssueTrackerActionTriggered); 2105 connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered); 2106 connect(m_ui.actionViewThirdPartyNotices, &QAction::triggered, this, 2107 [this]() { AboutDialog::showThirdPartyNotices(this); }); 2108 connect(m_ui.actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt); 2109 connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); 2110 connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered); 2111 connect(m_ui.actionMemoryCardEditor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered); 2112 connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered); 2113 connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered); 2114 connect(m_ui.actionMediaCapture, &QAction::toggled, this, &MainWindow::onToolsMediaCaptureToggled); 2115 connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger); 2116 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false); 2117 connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); 2118 connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets); 2119 connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons); 2120 connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles); 2121 connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() { 2122 if (isShowingGameList()) 2123 m_game_list_widget->gridZoomIn(); 2124 }); 2125 connect(m_ui.actionGridViewZoomOut, &QAction::triggered, m_game_list_widget, [this]() { 2126 if (isShowingGameList()) 2127 m_game_list_widget->gridZoomOut(); 2128 }); 2129 connect(m_ui.actionGridViewRefreshCovers, &QAction::triggered, m_game_list_widget, 2130 &GameListWidget::refreshGridCovers); 2131 2132 connect(g_emu_thread, &EmuThread::settingsResetToDefault, this, &MainWindow::onSettingsResetToDefault, 2133 Qt::QueuedConnection); 2134 connect(g_emu_thread, &EmuThread::errorReported, this, &MainWindow::reportError, Qt::BlockingQueuedConnection); 2135 connect(g_emu_thread, &EmuThread::messageConfirmed, this, &MainWindow::confirmMessage, Qt::BlockingQueuedConnection); 2136 connect(g_emu_thread, &EmuThread::statusMessage, this, &MainWindow::onStatusMessage); 2137 connect(g_emu_thread, &EmuThread::onAcquireRenderWindowRequested, this, &MainWindow::acquireRenderWindow, 2138 Qt::BlockingQueuedConnection); 2139 connect(g_emu_thread, &EmuThread::onReleaseRenderWindowRequested, this, &MainWindow::releaseRenderWindow); 2140 connect(g_emu_thread, &EmuThread::onResizeRenderWindowRequested, this, &MainWindow::displayResizeRequested, 2141 Qt::BlockingQueuedConnection); 2142 connect(g_emu_thread, &EmuThread::focusDisplayWidgetRequested, this, &MainWindow::focusDisplayWidget); 2143 connect(g_emu_thread, &EmuThread::systemStarting, this, &MainWindow::onSystemStarting); 2144 connect(g_emu_thread, &EmuThread::systemStarted, this, &MainWindow::onSystemStarted); 2145 connect(g_emu_thread, &EmuThread::systemDestroyed, this, &MainWindow::onSystemDestroyed); 2146 connect(g_emu_thread, &EmuThread::systemPaused, this, &MainWindow::onSystemPaused); 2147 connect(g_emu_thread, &EmuThread::systemResumed, this, &MainWindow::onSystemResumed); 2148 connect(g_emu_thread, &EmuThread::runningGameChanged, this, &MainWindow::onRunningGameChanged); 2149 connect(g_emu_thread, &EmuThread::mediaCaptureStarted, this, &MainWindow::onMediaCaptureStarted); 2150 connect(g_emu_thread, &EmuThread::mediaCaptureStopped, this, &MainWindow::onMediaCaptureStopped); 2151 connect(g_emu_thread, &EmuThread::mouseModeRequested, this, &MainWindow::onMouseModeRequested); 2152 connect(g_emu_thread, &EmuThread::fullscreenUIStateChange, this, &MainWindow::onFullscreenUIStateChange); 2153 connect(g_emu_thread, &EmuThread::achievementsLoginRequested, this, &MainWindow::onAchievementsLoginRequested); 2154 connect(g_emu_thread, &EmuThread::achievementsChallengeModeChanged, this, 2155 &MainWindow::onAchievementsChallengeModeChanged); 2156 connect(g_emu_thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered); 2157 2158 // These need to be queued connections to stop crashing due to menus opening/closing and switching focus. 2159 connect(m_game_list_widget, &GameListWidget::refreshProgress, this, &MainWindow::onGameListRefreshProgress); 2160 connect(m_game_list_widget, &GameListWidget::refreshComplete, this, &MainWindow::onGameListRefreshComplete); 2161 connect(m_game_list_widget, &GameListWidget::selectionChanged, this, &MainWindow::onGameListSelectionChanged, 2162 Qt::QueuedConnection); 2163 connect(m_game_list_widget, &GameListWidget::entryActivated, this, &MainWindow::onGameListEntryActivated, 2164 Qt::QueuedConnection); 2165 connect(m_game_list_widget, &GameListWidget::entryContextMenuRequested, this, 2166 &MainWindow::onGameListEntryContextMenuRequested, Qt::QueuedConnection); 2167 connect(m_game_list_widget, &GameListWidget::addGameDirectoryRequested, this, 2168 [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); }); 2169 2170 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDisableAllEnhancements, "Main", 2171 "DisableAllEnhancements", false); 2172 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugDumpCPUtoVRAMCopies, "Debug", 2173 "DumpCPUToVRAMCopies", false); 2174 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugDumpVRAMtoCPUCopies, "Debug", 2175 "DumpVRAMToCPUCopies", false); 2176 connect(m_ui.actionDumpRAM, &QAction::triggered, [this]() { 2177 const QString filename = QDir::toNativeSeparators( 2178 QFileDialog::getSaveFileName(this, tr("Destination File"), QString(), tr("Binary Files (*.bin)"))); 2179 if (filename.isEmpty()) 2180 return; 2181 2182 g_emu_thread->dumpRAM(filename); 2183 }); 2184 connect(m_ui.actionDumpVRAM, &QAction::triggered, [this]() { 2185 const QString filename = QDir::toNativeSeparators(QFileDialog::getSaveFileName( 2186 this, tr("Destination File"), QString(), tr("Binary Files (*.bin);;PNG Images (*.png)"))); 2187 if (filename.isEmpty()) 2188 return; 2189 2190 g_emu_thread->dumpVRAM(filename); 2191 }); 2192 connect(m_ui.actionDumpSPURAM, &QAction::triggered, [this]() { 2193 const QString filename = QDir::toNativeSeparators( 2194 QFileDialog::getSaveFileName(this, tr("Destination File"), QString(), tr("Binary Files (*.bin)"))); 2195 if (filename.isEmpty()) 2196 return; 2197 2198 g_emu_thread->dumpSPURAM(filename); 2199 }); 2200 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowVRAM, "Debug", "ShowVRAM", false); 2201 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowGPUState, "Debug", "ShowGPUState", false); 2202 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowCDROMState, "Debug", "ShowCDROMState", 2203 false); 2204 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowSPUState, "Debug", "ShowSPUState", false); 2205 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowTimersState, "Debug", "ShowTimersState", 2206 false); 2207 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowMDECState, "Debug", "ShowMDECState", false); 2208 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowDMAState, "Debug", "ShowDMAState", false); 2209 } 2210 2211 void MainWindow::updateTheme() 2212 { 2213 QtHost::UpdateApplicationTheme(); 2214 reloadThemeSpecificImages(); 2215 } 2216 2217 void MainWindow::reloadThemeSpecificImages() 2218 { 2219 m_game_list_widget->reloadThemeSpecificImages(); 2220 } 2221 2222 void MainWindow::onSettingsThemeChanged() 2223 { 2224 #ifdef _WIN32 2225 const QString old_style_name = qApp->style()->name(); 2226 #endif 2227 2228 updateTheme(); 2229 2230 #ifdef _WIN32 2231 // Work around a bug where the background colour of menus is broken when changing to/from the windowsvista theme. 2232 const QString new_style_name = qApp->style()->name(); 2233 if ((old_style_name == QStringLiteral("windowsvista")) != (new_style_name == QStringLiteral("windowsvista"))) 2234 recreate(); 2235 #endif 2236 } 2237 2238 void MainWindow::onSettingsResetToDefault(bool system, bool controller) 2239 { 2240 if (system && m_settings_window) 2241 { 2242 const bool had_settings_window = m_settings_window->isVisible(); 2243 m_settings_window->close(); 2244 m_settings_window->deleteLater(); 2245 m_settings_window = nullptr; 2246 2247 if (had_settings_window) 2248 doSettings(); 2249 } 2250 2251 if (controller && m_controller_settings_window) 2252 { 2253 const bool had_controller_settings_window = m_controller_settings_window->isVisible(); 2254 m_controller_settings_window->close(); 2255 m_controller_settings_window->deleteLater(); 2256 m_controller_settings_window = nullptr; 2257 2258 if (had_controller_settings_window) 2259 doControllerSettings(ControllerSettingsWindow::Category::GlobalSettings); 2260 } 2261 2262 updateDebugMenuCPUExecutionMode(); 2263 updateDebugMenuGPURenderer(); 2264 updateDebugMenuCropMode(); 2265 updateDebugMenuVisibility(); 2266 } 2267 2268 void MainWindow::saveStateToConfig() 2269 { 2270 if (!isVisible() || ((windowState() & Qt::WindowFullScreen) != Qt::WindowNoState)) 2271 return; 2272 2273 bool changed = false; 2274 2275 const QByteArray geometry(saveGeometry()); 2276 const QByteArray geometry_b64(geometry.toBase64()); 2277 const std::string old_geometry_b64(Host::GetBaseStringSettingValue("UI", "MainWindowGeometry")); 2278 if (old_geometry_b64 != geometry_b64.constData()) 2279 { 2280 Host::SetBaseStringSettingValue("UI", "MainWindowGeometry", geometry_b64.constData()); 2281 changed = true; 2282 } 2283 2284 const QByteArray state(saveState()); 2285 const QByteArray state_b64(state.toBase64()); 2286 const std::string old_state_b64(Host::GetBaseStringSettingValue("UI", "MainWindowState")); 2287 if (old_state_b64 != state_b64.constData()) 2288 { 2289 Host::SetBaseStringSettingValue("UI", "MainWindowState", state_b64.constData()); 2290 changed = true; 2291 } 2292 2293 if (changed) 2294 Host::CommitBaseSettingChanges(); 2295 } 2296 2297 void MainWindow::restoreStateFromConfig() 2298 { 2299 { 2300 const std::string geometry_b64 = Host::GetBaseStringSettingValue("UI", "MainWindowGeometry"); 2301 const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64)); 2302 if (!geometry.isEmpty()) 2303 restoreGeometry(geometry); 2304 } 2305 2306 { 2307 const std::string state_b64 = Host::GetBaseStringSettingValue("UI", "MainWindowState"); 2308 const QByteArray state = QByteArray::fromBase64(QByteArray::fromStdString(state_b64)); 2309 if (!state.isEmpty()) 2310 { 2311 restoreState(state); 2312 2313 // make sure we're not loading a dodgy config which had fullscreen set... 2314 setWindowState(windowState() & ~(Qt::WindowFullScreen | Qt::WindowActive)); 2315 } 2316 2317 { 2318 QSignalBlocker sb(m_ui.actionViewToolbar); 2319 m_ui.actionViewToolbar->setChecked(!m_ui.toolBar->isHidden()); 2320 } 2321 { 2322 QSignalBlocker sb(m_ui.actionViewStatusBar); 2323 m_ui.actionViewStatusBar->setChecked(!m_ui.statusBar->isHidden()); 2324 } 2325 } 2326 } 2327 2328 void MainWindow::saveDisplayWindowGeometryToConfig() 2329 { 2330 QWidget* container = getDisplayContainer(); 2331 if (container->windowState() & Qt::WindowFullScreen) 2332 { 2333 // if we somehow ended up here, don't save the fullscreen state to the config 2334 return; 2335 } 2336 2337 const QByteArray geometry = container->saveGeometry(); 2338 const QByteArray geometry_b64 = geometry.toBase64(); 2339 const std::string old_geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry"); 2340 if (old_geometry_b64 != geometry_b64.constData()) 2341 { 2342 Host::SetBaseStringSettingValue("UI", "DisplayWindowGeometry", geometry_b64.constData()); 2343 Host::CommitBaseSettingChanges(); 2344 } 2345 } 2346 2347 void MainWindow::restoreDisplayWindowGeometryFromConfig() 2348 { 2349 const std::string geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry"); 2350 const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64)); 2351 QWidget* container = getDisplayContainer(); 2352 if (!geometry.isEmpty()) 2353 { 2354 container->restoreGeometry(geometry); 2355 2356 // make sure we're not loading a dodgy config which had fullscreen set... 2357 container->setWindowState(container->windowState() & ~(Qt::WindowFullScreen | Qt::WindowActive)); 2358 } 2359 else 2360 { 2361 // default size 2362 container->resize(640, 480); 2363 } 2364 } 2365 2366 SettingsWindow* MainWindow::getSettingsWindow() 2367 { 2368 if (!m_settings_window) 2369 { 2370 m_settings_window = new SettingsWindow(); 2371 connect(m_settings_window->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::themeChanged, this, 2372 &MainWindow::onSettingsThemeChanged); 2373 } 2374 2375 return m_settings_window; 2376 } 2377 2378 void MainWindow::doSettings(const char* category /* = nullptr */) 2379 { 2380 SettingsWindow* dlg = getSettingsWindow(); 2381 QtUtils::ShowOrRaiseWindow(dlg); 2382 if (category) 2383 dlg->setCategory(category); 2384 } 2385 2386 ControllerSettingsWindow* MainWindow::getControllerSettingsWindow() 2387 { 2388 if (!m_controller_settings_window) 2389 m_controller_settings_window = new ControllerSettingsWindow(); 2390 2391 return m_controller_settings_window; 2392 } 2393 2394 void MainWindow::doControllerSettings( 2395 ControllerSettingsWindow::Category category /*= ControllerSettingsDialog::Category::Count*/) 2396 { 2397 ControllerSettingsWindow* dlg = getControllerSettingsWindow(); 2398 QtUtils::ShowOrRaiseWindow(dlg); 2399 if (category != ControllerSettingsWindow::Category::Count) 2400 dlg->setCategory(category); 2401 } 2402 2403 void MainWindow::openInputProfileEditor(const std::string_view name) 2404 { 2405 ControllerSettingsWindow* dlg = getControllerSettingsWindow(); 2406 QtUtils::ShowOrRaiseWindow(dlg); 2407 dlg->switchProfile(name); 2408 } 2409 2410 void MainWindow::updateDebugMenuCPUExecutionMode() 2411 { 2412 std::optional<CPUExecutionMode> current_mode = 2413 Settings::ParseCPUExecutionMode(Host::GetBaseStringSettingValue("CPU", "ExecutionMode").c_str()); 2414 if (!current_mode.has_value()) 2415 return; 2416 2417 const QString current_mode_display_name = 2418 QString::fromUtf8(Settings::GetCPUExecutionModeDisplayName(current_mode.value())); 2419 for (QObject* obj : m_ui.menuCPUExecutionMode->children()) 2420 { 2421 QAction* action = qobject_cast<QAction*>(obj); 2422 if (action) 2423 action->setChecked(action->text() == current_mode_display_name); 2424 } 2425 } 2426 2427 void MainWindow::updateDebugMenuGPURenderer() 2428 { 2429 // update the menu with the new selected renderer 2430 std::optional<GPURenderer> current_renderer = 2431 Settings::ParseRendererName(Host::GetBaseStringSettingValue("GPU", "Renderer").c_str()); 2432 if (!current_renderer.has_value()) 2433 return; 2434 2435 const QString current_renderer_display_name = 2436 QString::fromUtf8(Settings::GetRendererDisplayName(current_renderer.value())); 2437 for (QObject* obj : m_ui.menuRenderer->children()) 2438 { 2439 QAction* action = qobject_cast<QAction*>(obj); 2440 if (action) 2441 action->setChecked(action->text() == current_renderer_display_name); 2442 } 2443 } 2444 2445 void MainWindow::updateDebugMenuCropMode() 2446 { 2447 std::optional<DisplayCropMode> current_crop_mode = 2448 Settings::ParseDisplayCropMode(Host::GetBaseStringSettingValue("Display", "CropMode").c_str()); 2449 if (!current_crop_mode.has_value()) 2450 return; 2451 2452 const QString current_crop_mode_display_name = 2453 QString::fromUtf8(Settings::GetDisplayCropModeDisplayName(current_crop_mode.value())); 2454 for (QObject* obj : m_ui.menuCropMode->children()) 2455 { 2456 QAction* action = qobject_cast<QAction*>(obj); 2457 if (action) 2458 action->setChecked(action->text() == current_crop_mode_display_name); 2459 } 2460 } 2461 2462 void MainWindow::showEvent(QShowEvent* event) 2463 { 2464 QMainWindow::showEvent(event); 2465 2466 // This is a bit silly, but for some reason resizing *before* the window is shown 2467 // gives the incorrect sizes for columns, if you set the style before setting up 2468 // the rest of the window... so, instead, let's just force it to be resized on show. 2469 if (isShowingGameList()) 2470 m_game_list_widget->resizeTableViewColumnsToFit(); 2471 } 2472 2473 void MainWindow::closeEvent(QCloseEvent* event) 2474 { 2475 // If there's no VM, we can just exit as normal. 2476 if (!s_system_valid || !m_display_created) 2477 { 2478 saveStateToConfig(); 2479 if (m_display_created) 2480 g_emu_thread->stopFullscreenUI(); 2481 destroySubWindows(); 2482 QMainWindow::closeEvent(event); 2483 return; 2484 } 2485 2486 // But if there is, we have to cancel the action, regardless of whether we ended exiting 2487 // or not. The window still needs to be visible while GS is shutting down. 2488 event->ignore(); 2489 2490 // Exit cancelled? 2491 if (!requestShutdown(true, true, g_settings.save_state_on_exit)) 2492 return; 2493 2494 // Application will be exited in VM stopped handler. 2495 saveStateToConfig(); 2496 m_is_closing = true; 2497 } 2498 2499 void MainWindow::changeEvent(QEvent* event) 2500 { 2501 if (static_cast<QWindowStateChangeEvent*>(event)->oldState() & Qt::WindowMinimized) 2502 { 2503 // TODO: This should check the render-to-main option. 2504 if (m_display_widget) 2505 g_emu_thread->redrawDisplayWindow(); 2506 } 2507 2508 if (event->type() == QEvent::StyleChange) 2509 { 2510 QtHost::SetIconThemeFromStyle(); 2511 reloadThemeSpecificImages(); 2512 } 2513 2514 QMainWindow::changeEvent(event); 2515 } 2516 2517 static QString getFilenameFromMimeData(const QMimeData* md) 2518 { 2519 QString filename; 2520 if (md->hasUrls()) 2521 { 2522 // only one url accepted 2523 const QList<QUrl> urls(md->urls()); 2524 if (urls.size() == 1) 2525 filename = QDir::toNativeSeparators(urls.front().toLocalFile()); 2526 } 2527 2528 return filename; 2529 } 2530 2531 void MainWindow::dragEnterEvent(QDragEnterEvent* event) 2532 { 2533 const std::string filename(getFilenameFromMimeData(event->mimeData()).toStdString()); 2534 if (!System::IsLoadableFilename(filename) && !System::IsSaveStateFilename(filename)) 2535 return; 2536 2537 event->acceptProposedAction(); 2538 } 2539 2540 void MainWindow::dropEvent(QDropEvent* event) 2541 { 2542 const QString qfilename(getFilenameFromMimeData(event->mimeData())); 2543 const std::string filename(qfilename.toStdString()); 2544 if (!System::IsLoadableFilename(filename) && !System::IsSaveStateFilename(filename)) 2545 return; 2546 2547 event->acceptProposedAction(); 2548 2549 if (System::IsSaveStateFilename(filename)) 2550 { 2551 g_emu_thread->loadState(qfilename); 2552 return; 2553 } 2554 2555 if (s_system_valid) 2556 promptForDiscChange(qfilename); 2557 else 2558 startFileOrChangeDisc(qfilename); 2559 } 2560 2561 void MainWindow::moveEvent(QMoveEvent* event) 2562 { 2563 QMainWindow::moveEvent(event); 2564 2565 if (g_log_window && g_log_window->isAttachedToMainWindow()) 2566 g_log_window->reattachToMainWindow(); 2567 } 2568 2569 void MainWindow::resizeEvent(QResizeEvent* event) 2570 { 2571 QMainWindow::resizeEvent(event); 2572 2573 if (g_log_window && g_log_window->isAttachedToMainWindow()) 2574 g_log_window->reattachToMainWindow(); 2575 } 2576 2577 void MainWindow::startupUpdateCheck() 2578 { 2579 if (!Host::GetBaseBoolSettingValue("AutoUpdater", "CheckAtStartup", true)) 2580 return; 2581 2582 checkForUpdates(false); 2583 } 2584 2585 void MainWindow::updateDebugMenuVisibility() 2586 { 2587 const bool visible = QtHost::ShouldShowDebugOptions(); 2588 m_ui.menuDebug->menuAction()->setVisible(visible); 2589 } 2590 2591 void MainWindow::refreshGameList(bool invalidate_cache) 2592 { 2593 m_game_list_widget->refresh(invalidate_cache); 2594 } 2595 2596 void MainWindow::refreshGameListModel() 2597 { 2598 m_game_list_widget->refreshModel(); 2599 } 2600 2601 void MainWindow::cancelGameListRefresh() 2602 { 2603 m_game_list_widget->cancelRefresh(); 2604 } 2605 2606 void MainWindow::runOnUIThread(const std::function<void()>& func) 2607 { 2608 func(); 2609 } 2610 2611 bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_save_to_state /* = true */, 2612 bool save_state /* = true */) 2613 { 2614 if (!s_system_valid) 2615 return true; 2616 2617 // If we don't have a serial, we can't save state. 2618 allow_save_to_state &= !s_current_game_serial.isEmpty(); 2619 save_state &= allow_save_to_state; 2620 2621 // Only confirm on UI thread because we need to display a msgbox. 2622 if (!m_is_closing && allow_confirm && Host::GetBoolSettingValue("Main", "ConfirmPowerOff", true)) 2623 { 2624 SystemLock lock(pauseAndLockSystem()); 2625 2626 QMessageBox msgbox(lock.getDialogParent()); 2627 msgbox.setIcon(QMessageBox::Question); 2628 msgbox.setWindowTitle(tr("Confirm Shutdown")); 2629 msgbox.setWindowModality(Qt::WindowModal); 2630 msgbox.setText(tr("Are you sure you want to shut down the virtual machine?")); 2631 2632 QCheckBox* save_cb = new QCheckBox(tr("Save State For Resume"), &msgbox); 2633 save_cb->setChecked(allow_save_to_state && save_state); 2634 save_cb->setEnabled(allow_save_to_state); 2635 msgbox.setCheckBox(save_cb); 2636 msgbox.addButton(QMessageBox::Yes); 2637 msgbox.addButton(QMessageBox::No); 2638 msgbox.setDefaultButton(QMessageBox::Yes); 2639 if (msgbox.exec() != QMessageBox::Yes) 2640 return false; 2641 2642 save_state = save_cb->isChecked(); 2643 2644 // Don't switch back to fullscreen when we're shutting down anyway. 2645 lock.cancelResume(); 2646 } 2647 2648 // This is a little bit annoying. Qt will close everything down if we don't have at least one window visible, 2649 // but we might not be visible because the user is using render-to-separate and hide. We don't want to always 2650 // reshow the main window during display updates, because otherwise fullscreen transitions and renderer switches 2651 // would briefly show and then hide the main window. So instead, we do it on shutdown, here. Except if we're in 2652 // batch mode, when we're going to exit anyway. 2653 if (!isRenderingToMain() && isHidden() && !QtHost::InBatchMode() && !g_emu_thread->isRunningFullscreenUI()) 2654 updateWindowState(true); 2655 2656 // Now we can actually shut down the VM. 2657 g_emu_thread->shutdownSystem(save_state, true); 2658 return true; 2659 } 2660 2661 void MainWindow::requestExit(bool allow_confirm /* = true */) 2662 { 2663 // this is block, because otherwise closeEvent() will also prompt 2664 if (!requestShutdown(allow_confirm, true, g_settings.save_state_on_exit)) 2665 return; 2666 2667 // VM stopped signal won't have fired yet, so queue an exit if we still have one. 2668 // Otherwise, immediately exit, because there's no VM to exit us later. 2669 if (s_system_valid) 2670 m_is_closing = true; 2671 else 2672 quit(); 2673 } 2674 2675 void MainWindow::checkForSettingChanges() 2676 { 2677 LogWindow::updateSettings(); 2678 updateWindowState(); 2679 updateCheatActionsVisibility(); 2680 } 2681 2682 std::optional<WindowInfo> MainWindow::getWindowInfo() 2683 { 2684 if (!m_display_widget || isRenderingToMain()) 2685 return QtUtils::GetWindowInfoForWidget(this); 2686 else if (QWidget* widget = getDisplayContainer()) 2687 return QtUtils::GetWindowInfoForWidget(widget); 2688 else 2689 return std::nullopt; 2690 } 2691 2692 void MainWindow::onCheckForUpdatesActionTriggered() 2693 { 2694 // Wipe out the last version, that way it displays the update if we've previously skipped it. 2695 Host::DeleteBaseSettingValue("AutoUpdater", "LastVersion"); 2696 Host::CommitBaseSettingChanges(); 2697 checkForUpdates(true); 2698 } 2699 2700 void MainWindow::openMemoryCardEditor(const QString& card_a_path, const QString& card_b_path) 2701 { 2702 for (const QString& card_path : {card_a_path, card_b_path}) 2703 { 2704 if (!card_path.isEmpty() && !QFile::exists(card_path)) 2705 { 2706 if (QMessageBox::question( 2707 this, tr("Memory Card Not Found"), 2708 tr("Memory card '%1' does not exist. Do you want to create an empty memory card?").arg(card_path), 2709 QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) 2710 { 2711 Error error; 2712 if (!MemoryCardEditorWindow::createMemoryCard(card_path, &error)) 2713 { 2714 QMessageBox::critical(this, tr("Memory Card Not Found"), 2715 tr("Failed to create memory card '%1': %2") 2716 .arg(card_path) 2717 .arg(QString::fromStdString(error.GetDescription()))); 2718 } 2719 } 2720 } 2721 } 2722 2723 if (!m_memory_card_editor_window) 2724 m_memory_card_editor_window = new MemoryCardEditorWindow(); 2725 2726 QtUtils::ShowOrRaiseWindow(m_memory_card_editor_window); 2727 2728 if (!card_a_path.isEmpty()) 2729 { 2730 if (!m_memory_card_editor_window->setCardA(card_a_path)) 2731 { 2732 QMessageBox::critical( 2733 this, tr("Memory Card Not Found"), 2734 tr("Memory card '%1' could not be found. Try starting the game and saving to create it.").arg(card_a_path)); 2735 } 2736 } 2737 if (!card_b_path.isEmpty()) 2738 { 2739 if (!m_memory_card_editor_window->setCardB(card_b_path)) 2740 { 2741 QMessageBox::critical( 2742 this, tr("Memory Card Not Found"), 2743 tr("Memory card '%1' could not be found. Try starting the game and saving to create it.").arg(card_b_path)); 2744 } 2745 } 2746 } 2747 2748 void MainWindow::onAchievementsLoginRequested(Achievements::LoginRequestReason reason) 2749 { 2750 const auto lock = pauseAndLockSystem(); 2751 2752 AchievementLoginDialog dlg(lock.getDialogParent(), reason); 2753 dlg.exec(); 2754 } 2755 2756 void MainWindow::onAchievementsChallengeModeChanged(bool enabled) 2757 { 2758 if (enabled) 2759 { 2760 QtUtils::CloseAndDeleteWindow(m_cheat_manager_window); 2761 QtUtils::CloseAndDeleteWindow(m_debugger_window); 2762 QtUtils::CloseAndDeleteWindow(m_memory_scanner_window); 2763 } 2764 2765 updateEmulationActions(false, System::IsValid(), enabled); 2766 } 2767 2768 void MainWindow::onToolsMemoryCardEditorTriggered() 2769 { 2770 openMemoryCardEditor(QString(), QString()); 2771 } 2772 2773 void MainWindow::onToolsCoverDownloaderTriggered() 2774 { 2775 // This can be invoked via big picture, so exit fullscreen. 2776 SystemLock lock(pauseAndLockSystem()); 2777 CoverDownloadDialog dlg(lock.getDialogParent()); 2778 connect(&dlg, &CoverDownloadDialog::coverRefreshRequested, m_game_list_widget, &GameListWidget::refreshGridCovers); 2779 dlg.exec(); 2780 } 2781 2782 void MainWindow::onToolsMediaCaptureToggled(bool checked) 2783 { 2784 if (!QtHost::IsSystemValid()) 2785 { 2786 // leave it for later, we'll fill in the boot params 2787 return; 2788 } 2789 2790 if (!checked) 2791 { 2792 Host::RunOnCPUThread(&System::StopMediaCapture); 2793 return; 2794 } 2795 2796 const std::string container = 2797 Host::GetStringSettingValue("MediaCapture", "Container", Settings::DEFAULT_MEDIA_CAPTURE_CONTAINER); 2798 const QString qcontainer = QString::fromStdString(container); 2799 const QString filter(tr("%1 Files (*.%2)").arg(qcontainer.toUpper()).arg(qcontainer)); 2800 2801 QString path = 2802 QString::fromStdString(System::GetNewMediaCapturePath(QtHost::GetCurrentGameTitle().toStdString(), container)); 2803 path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Media Capture"), path, filter)); 2804 if (path.isEmpty()) 2805 { 2806 // uncheck it again 2807 const QSignalBlocker sb(m_ui.actionMediaCapture); 2808 m_ui.actionMediaCapture->setChecked(false); 2809 return; 2810 } 2811 2812 Host::RunOnCPUThread([path = path.toStdString()]() { System::StartMediaCapture(path); }); 2813 } 2814 2815 void MainWindow::onToolsMemoryScannerTriggered() 2816 { 2817 if (Achievements::IsHardcoreModeActive()) 2818 return; 2819 2820 if (!m_memory_scanner_window) 2821 { 2822 m_memory_scanner_window = new MemoryScannerWindow(); 2823 connect(m_memory_scanner_window, &MemoryScannerWindow::closed, this, [this]() { 2824 m_memory_scanner_window->deleteLater(); 2825 m_memory_scanner_window = nullptr; 2826 }); 2827 } 2828 2829 QtUtils::ShowOrRaiseWindow(m_memory_scanner_window); 2830 } 2831 2832 void MainWindow::openCheatManager() 2833 { 2834 if (Achievements::IsHardcoreModeActive()) 2835 return; 2836 2837 if (!m_cheat_manager_window) 2838 { 2839 m_cheat_manager_window = new CheatManagerWindow(); 2840 connect(m_cheat_manager_window, &CheatManagerWindow::closed, this, [this]() { 2841 m_cheat_manager_window->deleteLater(); 2842 m_cheat_manager_window = nullptr; 2843 }); 2844 } 2845 2846 QtUtils::ShowOrRaiseWindow(m_cheat_manager_window); 2847 } 2848 2849 void MainWindow::openCPUDebugger() 2850 { 2851 if (!m_debugger_window) 2852 { 2853 m_debugger_window = new DebuggerWindow(); 2854 connect(m_debugger_window, &DebuggerWindow::closed, this, [this]() { 2855 m_debugger_window->deleteLater(); 2856 m_debugger_window = nullptr; 2857 }); 2858 } 2859 2860 QtUtils::ShowOrRaiseWindow(m_debugger_window); 2861 } 2862 2863 void MainWindow::onToolsOpenDataDirectoryTriggered() 2864 { 2865 QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::DataRoot))); 2866 } 2867 2868 void MainWindow::onSettingsTriggeredFromToolbar() 2869 { 2870 if (s_system_valid) 2871 m_settings_toolbar_menu->exec(QCursor::pos()); 2872 else 2873 doSettings(); 2874 } 2875 2876 void MainWindow::checkForUpdates(bool display_message) 2877 { 2878 if (!AutoUpdaterDialog::isSupported()) 2879 { 2880 if (display_message) 2881 { 2882 QMessageBox mbox(this); 2883 mbox.setWindowTitle(tr("Updater Error")); 2884 mbox.setWindowModality(Qt::WindowModal); 2885 mbox.setTextFormat(Qt::RichText); 2886 2887 QString message; 2888 if (!AutoUpdaterDialog::isOfficialBuild()) 2889 { 2890 message = 2891 tr("<p>Sorry, you are trying to update a DuckStation version which is not an official GitHub release. To " 2892 "prevent incompatibilities, the auto-updater is only enabled on official builds.</p>" 2893 "<p>Please download an official release from from <a " 2894 "href=\"https://www.duckstation.org/\">duckstation.org</a>.</p>"); 2895 } 2896 else 2897 { 2898 message = tr("Automatic updating is not supported on the current platform."); 2899 } 2900 2901 mbox.setText(message); 2902 mbox.setIcon(QMessageBox::Critical); 2903 mbox.exec(); 2904 } 2905 2906 return; 2907 } 2908 2909 if (m_auto_updater_dialog) 2910 return; 2911 2912 m_auto_updater_dialog = new AutoUpdaterDialog(this); 2913 connect(m_auto_updater_dialog, &AutoUpdaterDialog::updateCheckCompleted, this, &MainWindow::onUpdateCheckComplete); 2914 m_auto_updater_dialog->queueUpdateCheck(display_message); 2915 } 2916 2917 void* MainWindow::getNativeWindowId() 2918 { 2919 return (void*)winId(); 2920 } 2921 2922 void MainWindow::onUpdateCheckComplete() 2923 { 2924 if (!m_auto_updater_dialog) 2925 return; 2926 2927 m_auto_updater_dialog->deleteLater(); 2928 m_auto_updater_dialog = nullptr; 2929 } 2930 2931 MainWindow::SystemLock MainWindow::pauseAndLockSystem() 2932 { 2933 // To switch out of fullscreen when displaying a popup, or not to? 2934 // For Windows, with driver's direct scanout, what renders behind tends to be hit and miss. 2935 // We can't draw anything over exclusive fullscreen, so get out of it in that case. 2936 // Wayland's a pain as usual, we need to recreate the window, which means there'll be a brief 2937 // period when there's no window, and Qt might shut us down. So avoid it there. 2938 // On MacOS, it forces a workspace switch, which is kinda jarring. 2939 2940 #ifndef __APPLE__ 2941 const bool was_fullscreen = g_emu_thread->isFullscreen() && !s_use_central_widget; 2942 #else 2943 const bool was_fullscreen = false; 2944 #endif 2945 const bool was_paused = !s_system_valid || s_system_paused; 2946 2947 // We need to switch out of exclusive fullscreen before we can display our popup. 2948 // However, we do not want to switch back to render-to-main, the window might have generated this event. 2949 if (was_fullscreen) 2950 { 2951 g_emu_thread->setFullscreen(false, false); 2952 2953 // Container could change... thanks Wayland. 2954 QWidget* container; 2955 while (s_system_valid && 2956 (g_emu_thread->isFullscreen() || !(container = getDisplayContainer()) || container->isFullScreen())) 2957 { 2958 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 2959 } 2960 } 2961 2962 if (!was_paused) 2963 { 2964 g_emu_thread->setSystemPaused(true); 2965 2966 // Need to wait for the pause to go through, and make the main window visible if needed. 2967 while (!s_system_paused) 2968 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); 2969 2970 // Ensure it's visible before we try to create any dialogs parented to us. 2971 QApplication::sync(); 2972 } 2973 2974 // Now we'll either have a borderless window, or a regular window (if we were exclusive fullscreen). 2975 QWidget* dialog_parent = getDisplayContainer(); 2976 2977 return SystemLock(dialog_parent, was_paused, was_fullscreen); 2978 } 2979 2980 MainWindow::SystemLock::SystemLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen) 2981 : m_dialog_parent(dialog_parent), m_was_paused(was_paused), m_was_fullscreen(was_fullscreen) 2982 { 2983 } 2984 2985 MainWindow::SystemLock::SystemLock(SystemLock&& lock) 2986 : m_dialog_parent(lock.m_dialog_parent), m_was_paused(lock.m_was_paused), m_was_fullscreen(lock.m_was_fullscreen) 2987 { 2988 lock.m_dialog_parent = nullptr; 2989 lock.m_was_paused = true; 2990 lock.m_was_fullscreen = false; 2991 } 2992 2993 MainWindow::SystemLock::~SystemLock() 2994 { 2995 if (m_was_fullscreen) 2996 g_emu_thread->setFullscreen(true, true); 2997 if (!m_was_paused) 2998 g_emu_thread->setSystemPaused(false); 2999 } 3000 3001 void MainWindow::SystemLock::cancelResume() 3002 { 3003 m_was_paused = true; 3004 m_was_fullscreen = false; 3005 }