chore(ui): improve setup wizard and boot manager

- Change setup wizard to non-modal for better user interaction

Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
Zephyron
2025-11-28 16:12:23 +10:00
parent 90eeee345c
commit 32c4a9060d
5 changed files with 347 additions and 154 deletions

View File

@@ -309,6 +309,12 @@ GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_,
mouse_constrain_timer.setInterval(default_mouse_constrain_timeout);
connect(&mouse_constrain_timer, &QTimer::timeout, this, &GRenderWindow::ConstrainMouse);
// mouse-hiding logic for Wayland
constexpr int default_mouse_hide_timeout = 2500; // 2.5 seconds
mouse_hide_timer.setInterval(default_mouse_hide_timeout);
mouse_hide_timer.setSingleShot(true); // The timer fires only once per start()
connect(&mouse_hide_timer, &QTimer::timeout, this, &GRenderWindow::HideMouseCursor);
}
void GRenderWindow::ExecuteProgram(std::size_t program_index) {
@@ -399,6 +405,12 @@ void GRenderWindow::closeEvent(QCloseEvent* event) {
}
void GRenderWindow::leaveEvent(QEvent* event) {
if (UISettings::values.hide_mouse) {
// When the mouse leaves the window, ALWAYS restore the cursor.
QApplication::restoreOverrideCursor();
mouse_hide_timer.stop();
}
if (Settings::values.mouse_panning) {
const QRect& rect = QWidget::geometry();
QPoint position = QCursor::pos();
@@ -644,6 +656,12 @@ InputCommon::MouseButton GRenderWindow::QtButtonToMouseButton(Qt::MouseButton bu
}
void GRenderWindow::mousePressEvent(QMouseEvent* event) {
// A click is also mouse activity. Restore cursor and restart the timer.
if (UISettings::values.hide_mouse) {
QApplication::restoreOverrideCursor();
mouse_hide_timer.start();
}
// Touch input is handled in TouchBeginEvent
if (event->source() == Qt::MouseEventSynthesizedBySystem) {
return;
@@ -663,6 +681,12 @@ void GRenderWindow::mousePressEvent(QMouseEvent* event) {
}
void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
// Any mouse activity must FIRST restore the cursor before doing anything else.
if (UISettings::values.hide_mouse) {
QApplication::restoreOverrideCursor();
mouse_hide_timer.start();
}
// Touch input is handled in TouchUpdateEvent
if (event->source() == Qt::MouseEventSynthesizedBySystem) {
return;
@@ -889,6 +913,12 @@ bool GRenderWindow::event(QEvent* event) {
}
void GRenderWindow::focusOutEvent(QFocusEvent* event) {
// If the window loses focus, ALWAYS restore the cursor.
if (UISettings::values.hide_mouse) {
QApplication::restoreOverrideCursor();
mouse_hide_timer.stop();
}
QWidget::focusOutEvent(event);
input_subsystem->GetKeyboard()->ReleaseAllKeys();
input_subsystem->GetMouse()->ReleaseAllButtons();
@@ -1143,3 +1173,9 @@ bool GRenderWindow::eventFilter(QObject* object, QEvent* event) {
}
return false;
}
void GRenderWindow::HideMouseCursor() {
if (UISettings::values.hide_mouse && isActiveWindow()) {
QApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
}
}

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -10,6 +11,7 @@
#include <utility>
#include <vector>
#include <QApplication>
#include <QByteArray>
#include <QImage>
#include <QObject>
@@ -226,6 +228,9 @@ signals:
void MouseActivity();
void TasPlaybackStateChanged();
private slots:
void HideMouseCursor();
private:
void TouchBeginEvent(const QTouchEvent* event);
void TouchUpdateEvent(const QTouchEvent* event);
@@ -271,6 +276,7 @@ private:
#endif
QTimer mouse_constrain_timer;
QTimer mouse_hide_timer;
Core::System& system;

View File

@@ -13,8 +13,13 @@
#include <QJsonObject>
#include <QList>
#include <QMenu>
#include <QFileDialog>
#include <QDesktopServices>
#include <QMessageBox>
#include <QDirIterator>
#include <QPainter>
#include <QPainterPath>
#include <QProgressDialog>
#include <QScrollBar>
#include <QStyle>
#include <QThreadPool>
@@ -29,6 +34,8 @@
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/registered_cache.h"
#include "citron/compatibility_list.h"
#include "common/fs/path_util.h"
#include "core/hle/service/acc/profile_manager.h"
#include "citron/game_list.h"
#include "citron/game_list_p.h"
#include "citron/game_list_worker.h"
@@ -265,27 +272,31 @@ void GameList::FilterGridView(const QString& filter_text) {
QStandardItem* item = flat_model->item(i);
if (item) {
QVariant icon_data = item->data(Qt::DecorationRole);
if (icon_data.isValid() && icon_data.type() == QVariant::Pixmap) {
if (icon_data.isValid() && icon_data.canConvert<QPixmap>()) {
QPixmap pixmap = icon_data.value<QPixmap>();
if (!pixmap.isNull()) {
// Always recreate the rounded icon at the exact target size for consistency
#ifdef __linux__
// On Linux, use simple scaling to avoid QPainter bugs
QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
item->setData(scaled, Qt::DecorationRole);
#else
// On other platforms, use the QPainter method for rounded corners
QPixmap rounded(icon_size, icon_size);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
// Create rounded rectangle clipping path
const int radius = icon_size / 8;
QPainterPath path;
path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
painter.setClipPath(path);
// Scale the source pixmap to fill the icon size exactly
QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
painter.drawPixmap(0, 0, scaled);
item->setData(rounded, Qt::DecorationRole);
#endif
}
}
}
@@ -560,89 +571,74 @@ play_time_manager{play_time_manager_}, system{system_} {
"}"
));
connect(slider_title_size, &QSlider::valueChanged, [this](int value) {
// Update game icon size
UISettings::values.game_icon_size.SetValue(static_cast<u32>(value));
// Update grid view if it's active - update icons in place without recreating model
if (list_view->isVisible()) {
QAbstractItemModel* current_model = list_view->model();
if (current_model && current_model != item_model) {
// Update existing filtered model - just update icon sizes and grid size
QStandardItemModel* flat_model = qobject_cast<QStandardItemModel*>(current_model);
if (flat_model) {
const u32 icon_size = static_cast<u32>(value);
list_view->setGridSize(QSize(icon_size + 60, icon_size + 80));
// Update icon sizes in the existing model by getting original icons from hierarchical model
// Store current scroll position to restore it
int scroll_position = list_view->verticalScrollBar()->value();
QModelIndex current_index = list_view->currentIndex();
// Update title font size in tree view
QFont font = tree_view->font();
font.setPointSize(qBound(8, value / 8, 24));
tree_view->setFont(font);
for (int i = 0; i < flat_model->rowCount(); ++i) {
QStandardItem* item = flat_model->item(i);
if (item) {
// Get the original item from hierarchical model to get original icon
u64 program_id = item->data(GameListItemPath::ProgramIdRole).toULongLong();
#ifndef __linux__
// On non-Linux platforms, also update game icon size and repaint grid view
UISettings::values.game_icon_size.SetValue(static_cast<u32>(value));
if (list_view->isVisible()) {
QAbstractItemModel* current_model = list_view->model();
if (current_model && current_model != item_model) {
QStandardItemModel* flat_model = qobject_cast<QStandardItemModel*>(current_model);
if (flat_model) {
const u32 icon_size = static_cast<u32>(value);
list_view->setGridSize(QSize(icon_size + 60, icon_size + 80));
int scroll_position = list_view->verticalScrollBar()->value();
QModelIndex current_index = list_view->currentIndex();
// Find the original item in hierarchical model
QStandardItem* original_item = nullptr;
for (int folder_idx = 0; folder_idx < item_model->rowCount(); ++folder_idx) {
QStandardItem* folder = item_model->item(folder_idx, 0);
if (!folder) continue;
for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) {
QStandardItem* game = folder->child(game_idx, 0);
if (game && game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
original_item = game;
break;
}
for (int i = 0; i < flat_model->rowCount(); ++i) {
QStandardItem* item = flat_model->item(i);
if (item) {
u64 program_id = item->data(GameListItemPath::ProgramIdRole).toULongLong();
QStandardItem* original_item = nullptr;
for (int folder_idx = 0; folder_idx < item_model->rowCount(); ++folder_idx) {
QStandardItem* folder = item_model->item(folder_idx, 0);
if (!folder) continue;
for (int game_idx = 0; game_idx < folder->rowCount(); ++game_idx) {
QStandardItem* game = folder->child(game_idx, 0);
if (game && game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
original_item = game;
break;
}
if (original_item) break;
}
if (original_item) break;
}
if (original_item) {
// Get original icon from hierarchical model
QVariant orig_icon_data = original_item->data(Qt::DecorationRole);
if (orig_icon_data.isValid() && orig_icon_data.type() == QVariant::Pixmap) {
QPixmap orig_pixmap = orig_icon_data.value<QPixmap>();
// Create new rounded icon at new size
// Even though original is rounded, we'll scale it and re-apply rounding
QPixmap rounded(icon_size, icon_size);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
const int radius = icon_size / 8;
QPainterPath path;
path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
painter.setClipPath(path);
// Scale original pixmap to new size (even if it's already rounded, scaling will work)
QPixmap scaled = orig_pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
painter.drawPixmap(0, 0, scaled);
item->setData(rounded, Qt::DecorationRole);
}
if (original_item) {
QVariant orig_icon_data = original_item->data(Qt::DecorationRole);
if (orig_icon_data.isValid() && orig_icon_data.type() == QVariant::Pixmap) {
QPixmap orig_pixmap = orig_icon_data.value<QPixmap>();
QPixmap rounded(icon_size, icon_size);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
const int radius = icon_size / 8;
QPainterPath path;
path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
painter.setClipPath(path);
QPixmap scaled = orig_pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
painter.drawPixmap(0, 0, scaled);
item->setData(rounded, Qt::DecorationRole);
}
}
}
// Restore scroll position and selection
if (scroll_position >= 0) {
list_view->verticalScrollBar()->setValue(scroll_position);
}
if (current_index.isValid() && current_index.row() < flat_model->rowCount()) {
list_view->setCurrentIndex(flat_model->index(current_index.row(), 0));
}
}
} else {
// No filter active, use PopulateGridView
PopulateGridView();
if (scroll_position >= 0) {
list_view->verticalScrollBar()->setValue(scroll_position);
}
if (current_index.isValid() && current_index.row() < flat_model->rowCount()) {
list_view->setCurrentIndex(flat_model->index(current_index.row(), 0));
}
}
} else {
PopulateGridView();
}
// Update title font size in tree view
QFont font = tree_view->font();
font.setPointSize(qBound(8, value / 8, 24));
tree_view->setFont(font);
});
}
#endif
});
// A-Z sort button - positioned after slider
btn_sort_az = new QToolButton(toolbar);
@@ -925,7 +921,12 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* start_game_global = context_menu.addAction(tr("Start Game without Custom Configuration"));
context_menu.addSeparator();
QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
QAction* set_custom_save_path = context_menu.addAction(tr("Set Custom Save Path"));
QAction* remove_custom_save_path = context_menu.addAction(tr("Revert to NAND Save Path"));
QAction* open_mod_location = context_menu.addAction(tr("Open Mod Data Location"));
QMenu* open_sdmc_mod_menu = context_menu.addMenu(tr("Open SDMC Mod Data Location"));
QAction* open_current_game_sdmc = open_sdmc_mod_menu->addAction(tr("Open Current Game Location"));
QAction* open_full_sdmc = open_sdmc_mod_menu->addAction(tr("Open Full Location"));
QAction* open_transferable_shader_cache = context_menu.addAction(tr("Open Transferable Pipeline Cache"));
context_menu.addSeparator();
QMenu* remove_menu = context_menu.addMenu(tr("Remove"));
@@ -953,11 +954,16 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
context_menu.addSeparator();
QAction* properties = context_menu.addAction(tr("Properties"));
const bool has_custom_path = Settings::values.custom_save_paths.count(program_id);
favorite->setVisible(program_id != 0);
favorite->setCheckable(true);
favorite->setChecked(UISettings::values.favorited_ids.contains(program_id));
open_save_location->setVisible(program_id != 0);
set_custom_save_path->setVisible(program_id != 0);
remove_custom_save_path->setVisible(program_id != 0 && has_custom_path);
open_mod_location->setVisible(program_id != 0);
open_sdmc_mod_menu->menuAction()->setVisible(program_id != 0);
open_transferable_shader_cache->setVisible(program_id != 0);
remove_update->setVisible(program_id != 0);
remove_dlc->setVisible(program_id != 0);
@@ -970,6 +976,141 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); });
connect(open_save_location, &QAction::triggered, [this, program_id, path]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path); });
auto calculateTotalSize = [](const QString& dirPath) -> qint64 {
qint64 totalSize = 0;
QDirIterator size_it(dirPath, QDirIterator::Subdirectories);
while (size_it.hasNext()) {
size_it.next();
QFileInfo fileInfo = size_it.fileInfo();
if (fileInfo.isFile()) {
totalSize += fileInfo.size();
}
}
return totalSize;
};
auto copyWithProgress = [calculateTotalSize](const QString& sourceDir, const QString& destDir, QWidget* parent) -> bool {
QProgressDialog progress(tr("Moving Save Data..."), QString(), 0, 100, parent);
progress.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(0);
progress.setValue(0);
qint64 totalSize = calculateTotalSize(sourceDir);
qint64 copiedSize = 0;
QDir dir(sourceDir);
if (!dir.exists()) return false;
QDir dest_dir(destDir);
if (!dest_dir.exists()) dest_dir.mkpath(QStringLiteral("."));
QDirIterator dir_iter(sourceDir, QDirIterator::Subdirectories);
while (dir_iter.hasNext()) {
dir_iter.next();
const QFileInfo file_info = dir_iter.fileInfo();
const QString relative_path = dir.relativeFilePath(file_info.absoluteFilePath());
const QString dest_path = QDir(destDir).filePath(relative_path);
if (file_info.isDir()) {
dest_dir.mkpath(dest_path);
} else if (file_info.isFile()) {
if (QFile::exists(dest_path)) QFile::remove(dest_path);
if (!QFile::copy(file_info.absoluteFilePath(), dest_path)) return false;
copiedSize += file_info.size();
if (totalSize > 0) {
progress.setValue(static_cast<int>((copiedSize * 100) / totalSize));
}
}
QCoreApplication::processEvents();
}
progress.setValue(100);
return true;
};
connect(set_custom_save_path, &QAction::triggered, [this, program_id, copyWithProgress]() {
const QString new_path = QFileDialog::getExistingDirectory(this, tr("Select Custom Save Data Location"));
if (new_path.isEmpty()) return;
const auto nand_dir = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir));
const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128();
const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id);
const QString old_save_path = QDir(nand_dir).filePath(QString::fromStdString(relative_save_path));
QDir old_dir(old_save_path);
if (old_dir.exists() && !old_dir.isEmpty()) {
QMessageBox::StandardButton reply = QMessageBox::question(this, tr("Move Save Data"),
tr("You have existing save data in the NAND. Would you like to move it to the new custom save path?"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (reply == QMessageBox::Cancel) return;
if (reply == QMessageBox::Yes) {
const QString full_dest_path = QDir(new_path).filePath(QString::fromStdString(relative_save_path));
if (copyWithProgress(old_save_path, full_dest_path, this)) {
QDir(old_save_path).removeRecursively();
QMessageBox::information(this, tr("Success"), tr("Successfully moved save data to the new location."));
} else {
QMessageBox::warning(this, tr("Error"), tr("Failed to move save data. Please see the log for more details."));
}
}
}
Settings::values.custom_save_paths.insert_or_assign(program_id, new_path.toStdString());
emit SaveConfig();
});
connect(remove_custom_save_path, &QAction::triggered, [this, program_id, copyWithProgress]() {
const QString custom_path_root = QString::fromStdString(Settings::values.custom_save_paths.at(program_id));
const auto nand_dir = QString::fromStdString(Common::FS::GetCitronPathString(Common::FS::CitronPath::NANDDir));
const auto user_id = system.GetProfileManager().GetLastOpenedUser().AsU128();
const std::string relative_save_path = fmt::format("user/save/{:016X}/{:016X}{:016X}/{:016X}", 0, user_id[1], user_id[0], program_id);
const QString custom_game_save_path = QDir(custom_path_root).filePath(QString::fromStdString(relative_save_path));
const QString nand_save_path = QDir(nand_dir).filePath(QString::fromStdString(relative_save_path));
QMessageBox::StandardButton reply = QMessageBox::question(this, tr("Move Save Data"),
tr("Would you like to move the save data from the custom path back to the NAND?"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (reply == QMessageBox::Cancel) return;
if (reply == QMessageBox::Yes) {
if (copyWithProgress(custom_game_save_path, nand_save_path, this)) {
QDir(custom_game_save_path).removeRecursively();
QMessageBox::information(this, tr("Success"), tr("Successfully moved save data back to the NAND."));
} else {
QMessageBox::warning(this, tr("Error"), tr("Failed to move save data. Please see the log for more details."));
}
}
Settings::values.custom_save_paths.erase(program_id);
emit SaveConfig();
});
connect(open_current_game_sdmc, &QAction::triggered, [program_id]() {
const auto sdmc_path = Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir);
const auto full_path = sdmc_path / "atmosphere" / "contents" / fmt::format("{:016X}", program_id);
const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(full_path));
QDir dir(qpath);
if (!dir.exists()) dir.mkpath(QStringLiteral("."));
QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
});
connect(open_full_sdmc, &QAction::triggered, []() {
const auto sdmc_path = Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir);
const auto full_path = sdmc_path / "atmosphere" / "contents";
const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(full_path));
QDir dir(qpath);
if (!dir.exists()) dir.mkpath(QStringLiteral("."));
QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
});
connect(start_game, &QAction::triggered, [this, path]() { emit BootGame(QString::fromStdString(path), StartGameType::Normal); });
connect(start_game_global, &QAction::triggered, [this, path]() { emit BootGame(QString::fromStdString(path), StartGameType::Global); });
connect(open_mod_location, &QAction::triggered, [this, program_id, path]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path); });
@@ -1323,28 +1464,31 @@ const QStringList GameList::supported_file_extensions = {
QStandardItem* item = flat_model->item(i);
if (item) {
QVariant icon_data = item->data(Qt::DecorationRole);
if (icon_data.isValid() && icon_data.type() == QVariant::Pixmap) {
if (icon_data.isValid() && icon_data.canConvert<QPixmap>()) {
QPixmap pixmap = icon_data.value<QPixmap>();
if (!pixmap.isNull()) {
// Always recreate the rounded icon at the exact target size for consistency
// This ensures all icons are the same size regardless of their original size
#ifdef __linux__
// On Linux, use simple scaling to avoid QPainter bugs
QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
item->setData(scaled, Qt::DecorationRole);
#else
// On other platforms, use the QPainter method for rounded corners
QPixmap rounded(icon_size, icon_size);
rounded.fill(Qt::transparent);
QPainter painter(&rounded);
painter.setRenderHint(QPainter::Antialiasing);
// Create rounded rectangle clipping path
const int radius = icon_size / 8;
QPainterPath path;
path.addRoundedRect(0, 0, icon_size, icon_size, radius, radius);
painter.setClipPath(path);
// Scale the source pixmap to fill the icon size exactly
QPixmap scaled = pixmap.scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
painter.drawPixmap(0, 0, scaled);
item->setData(rounded, Qt::DecorationRole);
#endif
}
}
}

View File

@@ -468,10 +468,6 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
// make sure menubar has the arrow cursor instead of inheriting from this
ui->menubar->setCursor(QCursor());
mouse_hide_timer.setInterval(default_mouse_hide_timeout);
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor);
update_input_timer.setInterval(default_input_update_timeout);
connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers);
update_input_timer.start();
@@ -483,20 +479,48 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
// Process events to ensure main window is fully rendered
QApplication::processEvents();
// Show setup wizard on first run (after main window is shown)
LOG_INFO(Frontend, "Checking first_start: {}", UISettings::values.first_start.GetValue());
// Defer the first-time setup check until after the main window is fully constructed.
if (UISettings::values.first_start.GetValue()) {
LOG_INFO(Frontend, "Showing setup wizard");
SetupWizard setup_wizard(*system, this, this);
setup_wizard.setWindowModality(Qt::WindowModal);
setup_wizard.setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint |
Qt::WindowSystemMenuHint | Qt::WindowStaysOnTopHint);
setup_wizard.show();
setup_wizard.raise();
setup_wizard.activateWindow();
QApplication::processEvents();
setup_wizard.exec();
LOG_INFO(Frontend, "Setup wizard closed");
LOG_INFO(Frontend, "Scheduling first-time setup check.");
QTimer::singleShot(0, this, [this]() {
LOG_INFO(Frontend, "Executing deferred first-time setup check.");
// Create a non-modal QMessageBox instance with a nullptr parent to make it a top-level window.
// This prevents it from blocking the main application window.
auto* confirmation_dialog = new QMessageBox(nullptr);
confirmation_dialog->setAttribute(Qt::WA_DeleteOnClose); // This ensures it is deleted automatically on close.
confirmation_dialog->setWindowModality(Qt::NonModal); // Explicitly set modality.
confirmation_dialog->setWindowTitle(tr("First-Time Setup"));
confirmation_dialog->setText(tr("Would you like to run the first-time setup wizard?"));
confirmation_dialog->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
confirmation_dialog->setDefaultButton(QMessageBox::Yes);
// Connect the finished signal to handle the user's choice.
connect(confirmation_dialog, &QMessageBox::finished, this, [this](int result) {
if (result == QMessageBox::Yes) {
LOG_INFO(Frontend, "User chose to run the setup wizard.");
// Check if a wizard is already open to prevent duplicates.
if (this->findChild<SetupWizard*>()) {
this->findChild<SetupWizard*>()->activateWindow();
return;
}
// Create and show the setup wizard.
auto* setup_wizard = new SetupWizard(*system, this, this);
setup_wizard->setAttribute(Qt::WA_DeleteOnClose);
setup_wizard->show();
LOG_INFO(Frontend, "Setup wizard opened.");
} else {
LOG_INFO(Frontend, "User chose to skip the setup wizard.");
UISettings::values.first_start = false;
OnSaveConfig();
}
});
confirmation_dialog->open();
});
} else {
LOG_INFO(Frontend, "Skipping setup wizard - first_start is false");
}
@@ -2119,15 +2143,6 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP
status_bar_update_timer.start(500);
renderer_status_button->setDisabled(true);
if (UISettings::values.hide_mouse || Settings::values.mouse_panning) {
render_window->installEventFilter(render_window);
render_window->setAttribute(Qt::WA_Hover, true);
}
if (UISettings::values.hide_mouse) {
mouse_hide_timer.start();
}
render_window->InitializeCamera();
std::string title_name;
@@ -2362,31 +2377,43 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target
std::filesystem::path path;
QString open_target;
const auto [user_save_size, device_save_size] = [this, &game_path, &program_id] {
const FileSys::PatchManager pm{program_id, system->GetFileSystemController(),
system->GetContentProvider()};
const auto control = pm.GetControlMetadata().first;
if (control != nullptr) {
return std::make_pair(control->GetDefaultNormalSaveSize(),
control->GetDeviceSaveDataSize());
} else {
const auto file = Core::GetGameFileFromPath(vfs, game_path);
const auto loader = Loader::GetLoader(*system, file);
FileSys::NACP nacp{};
loader->ReadControlData(nacp);
return std::make_pair(nacp.GetDefaultNormalSaveSize(), nacp.GetDeviceSaveDataSize());
}
}();
const bool has_user_save{user_save_size > 0};
const bool has_device_save{device_save_size > 0};
ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?");
switch (target) {
case GameListOpenTarget::SaveData: {
open_target = tr("Save Data");
if (Settings::values.custom_save_paths.count(program_id)) {
const std::string& custom_path_str = Settings::values.custom_save_paths.at(program_id);
const std::filesystem::path custom_path = custom_path_str;
if (!custom_path_str.empty() && Common::FS::IsDir(custom_path)) {
LOG_INFO(Frontend, "Opening custom save data path for program_id={:016x}", program_id);
QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(custom_path_str)));
return;
}
}
const auto [user_save_size, device_save_size] = [this, &game_path, &program_id] {
const FileSys::PatchManager pm{program_id, system->GetFileSystemController(),
system->GetContentProvider()};
const auto control = pm.GetControlMetadata().first;
if (control != nullptr) {
return std::make_pair(control->GetDefaultNormalSaveSize(),
control->GetDeviceSaveDataSize());
} else {
const auto file = Core::GetGameFileFromPath(vfs, game_path);
const auto loader = Loader::GetLoader(*system, file);
FileSys::NACP nacp{};
loader->ReadControlData(nacp);
return std::make_pair(nacp.GetDefaultNormalSaveSize(), nacp.GetDeviceSaveDataSize());
}
}();
const bool has_user_save{user_save_size > 0};
const bool has_device_save{device_save_size > 0};
ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?");
const auto nand_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::NANDDir);
auto vfs_nand_dir =
vfs->OpenDirectory(Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read);
@@ -4123,7 +4150,7 @@ void GMainWindow::OnConfigure() {
config->SaveAllValues();
if ((UISettings::values.hide_mouse || Settings::values.mouse_panning) && emulation_running) {
if (Settings::values.mouse_panning && emulation_running) {
render_window->installEventFilter(render_window);
render_window->setAttribute(Qt::WA_Hover, true);
} else {
@@ -4131,10 +4158,6 @@ void GMainWindow::OnConfigure() {
render_window->setAttribute(Qt::WA_Hover, false);
}
if (UISettings::values.hide_mouse) {
mouse_hide_timer.start();
}
// Restart camera config
if (emulation_running) {
render_window->FinalizeCamera();
@@ -5527,26 +5550,8 @@ void GMainWindow::UpdateInputDrivers() {
input_subsystem->PumpEvents();
}
void GMainWindow::HideMouseCursor() {
if (emu_thread == nullptr && UISettings::values.hide_mouse) {
mouse_hide_timer.stop();
ShowMouseCursor();
return;
}
render_window->setCursor(QCursor(Qt::BlankCursor));
}
void GMainWindow::ShowMouseCursor() {
render_window->unsetCursor();
if (emu_thread != nullptr && UISettings::values.hide_mouse) {
mouse_hide_timer.start();
}
}
void GMainWindow::OnMouseActivity() {
if (!Settings::values.mouse_panning) {
ShowMouseCursor();
}
// This moved to GRenderWindow @ bootmanager
}
void GMainWindow::OnCheckFirmwareDecryption() {

View File

@@ -50,7 +50,9 @@ SetupWizard::SetupWizard(Core::System& system_, GMainWindow* main_window_, QWidg
// Set window flags before setting modality
setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint |
Qt::WindowSystemMenuHint | Qt::WindowStaysOnTopHint);
setWindowModality(Qt::WindowModal);
// Set window modality to NonModal to allow interaction with the main window.
setWindowModality(Qt::NonModal);
// Get UI elements from the .ui file
sidebar_list = ui->sidebarList;