mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-23 20:33:41 +00:00
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:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user