mirror of
https://github.com/Vita3K/Vita3K.git
synced 2026-07-11 01:34:23 +02:00
2147 lines
78 KiB
C++
2147 lines
78 KiB
C++
// Vita3K emulator project
|
|
// Copyright (C) 2026 Vita3K team
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along
|
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
#include "ui_main_window.h"
|
|
|
|
#include <gui-qt/about_dialog.h>
|
|
#include <gui-qt/apps_list.h>
|
|
#include <gui-qt/apps_list_context_menu.h>
|
|
#include <gui-qt/archive_install_dialog.h>
|
|
#include <gui-qt/controls_dialog.h>
|
|
#include <gui-qt/ctrl_keyboard_filter.h>
|
|
#include <gui-qt/debug_widget.h>
|
|
#include <gui-qt/firmware_install_dialog.h>
|
|
#include <gui-qt/game_compatibility.h>
|
|
#include <gui-qt/game_window.h>
|
|
#include <gui-qt/gui_language.h>
|
|
#include <gui-qt/gui_settings.h>
|
|
#include <gui-qt/ime_keyboard_filter.h>
|
|
#include <gui-qt/license_install_dialog.h>
|
|
#include <gui-qt/live_area_widget.h>
|
|
#include <gui-qt/log_widget.h>
|
|
#include <gui-qt/main_window.h>
|
|
#include <gui-qt/persistent_settings.h>
|
|
#include <gui-qt/pkg_install_dialog.h>
|
|
#include <gui-qt/qt_utils.h>
|
|
#include <gui-qt/settings_dialog.h>
|
|
#include <gui-qt/settings_dialog_tooltips.h>
|
|
#include <gui-qt/theme_manager.h>
|
|
#include <gui-qt/theme_surface.h>
|
|
#include <gui-qt/trophy_collection_dialog.h>
|
|
#include <gui-qt/update_manager.h>
|
|
#include <gui-qt/user_management_dialog.h>
|
|
#include <gui-qt/vita_themes_dialog.h>
|
|
#include <gui-qt/welcome_dialog.h>
|
|
|
|
#include <app/functions.h>
|
|
#include <archive.h>
|
|
#include <audio/state.h>
|
|
#include <config/functions.h>
|
|
#include <config/settings.h>
|
|
#include <config/state.h>
|
|
#include <config/version.h>
|
|
#include <ctrl/functions.h>
|
|
#include <display/state.h>
|
|
#include <gxm/state.h>
|
|
#include <interface.h>
|
|
#include <io/state.h>
|
|
#include <kernel/state.h>
|
|
#include <motion/event_handler.h>
|
|
#include <np/state.h>
|
|
#include <packages/functions.h>
|
|
#include <packages/license.h>
|
|
#include <renderer/functions.h>
|
|
#include <renderer/shaders.h>
|
|
#include <renderer/state.h>
|
|
#include <touch/functions.h>
|
|
#include <util/fs.h>
|
|
#include <util/log.h>
|
|
#include <util/string_utils.h>
|
|
|
|
#if USE_DISCORD
|
|
#include <app/discord.h>
|
|
#endif
|
|
|
|
#include <SDL3/SDL_events.h>
|
|
#include <SDL3/SDL_gamepad.h>
|
|
#include <SDL3/SDL_sensor.h>
|
|
|
|
#include <QApplication>
|
|
#include <QCheckBox>
|
|
#include <QCloseEvent>
|
|
#include <QDragEnterEvent>
|
|
#include <QDropEvent>
|
|
#include <QFileInfo>
|
|
#include <QHBoxLayout>
|
|
#include <QLineEdit>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QMimeData>
|
|
#include <QOpenGLContext>
|
|
#include <QPainter>
|
|
#include <QPushButton>
|
|
#include <QSlider>
|
|
#include <QStandardPaths>
|
|
#include <QStatusBar>
|
|
#include <QStyleFactory>
|
|
#include <QStyleHints>
|
|
#include <QToolBar>
|
|
#include <QVariantAnimation>
|
|
#include <QWidgetAction>
|
|
#include <QtResource>
|
|
|
|
#include <optional>
|
|
|
|
#if defined(HAVE_X11) || defined(HAVE_WAYLAND)
|
|
#include <qpa/qplatformnativeinterface.h>
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
void present_tool_window(QWidget *window) {
|
|
if (!window)
|
|
return;
|
|
|
|
if (window->isMinimized())
|
|
window->showNormal();
|
|
else
|
|
window->show();
|
|
|
|
window->activateWindow();
|
|
}
|
|
|
|
void copy_config_for_edit(Config &dst, const Config &src) {
|
|
dst = src;
|
|
dst.current_config = src.current_config;
|
|
}
|
|
|
|
void update_status_button_accent(QPushButton *button, const QString &accent) {
|
|
if (!button)
|
|
return;
|
|
|
|
if (button->objectName() != QStringLiteral("status_button"))
|
|
button->setObjectName(QStringLiteral("status_button"));
|
|
|
|
if (button->property("accent").toString() != accent) {
|
|
button->setProperty("accent", accent);
|
|
gui::utils::refresh_theme_state(button);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
MainWindow::MainWindow(EmuEnvState &emuenv,
|
|
std::shared_ptr<GuiSettings> gui_settings,
|
|
std::shared_ptr<PersistentSettings> persistent_settings,
|
|
bool admin_privileged)
|
|
: QMainWindow(nullptr)
|
|
, emuenv{ emuenv }
|
|
, m_app_session(emuenv)
|
|
, m_ui(std::make_unique<Ui::MainWindow>())
|
|
, m_gui_settings(std::move(gui_settings))
|
|
, m_persistent_settings(std::move(persistent_settings))
|
|
, m_admin_privileged(admin_privileged) {
|
|
initialize();
|
|
}
|
|
|
|
MainWindow::~MainWindow() {
|
|
shutdown_discord();
|
|
}
|
|
|
|
void MainWindow::changeEvent(QEvent *event) {
|
|
if (event->type() == QEvent::LanguageChange) {
|
|
m_ui->retranslateUi(this);
|
|
setWindowTitle(QString::fromStdString(window_title));
|
|
refresh_emulation_actions();
|
|
refresh_status_bar();
|
|
update_update_available_button();
|
|
if (m_search_bar)
|
|
m_search_bar->setPlaceholderText(tr("Search..."));
|
|
m_ui->toolbar_fullscreen->setText(m_fullscreen ? tr("Exit Fullscreen") : tr("Fullscreen"));
|
|
}
|
|
|
|
QMainWindow::changeEvent(event);
|
|
}
|
|
|
|
void MainWindow::register_auxiliary_window(QWidget *window) {
|
|
if (!window)
|
|
return;
|
|
|
|
m_auxiliary_windows.push_back(window);
|
|
}
|
|
|
|
void MainWindow::prune_auxiliary_windows() {
|
|
m_auxiliary_windows.erase(
|
|
std::remove_if(m_auxiliary_windows.begin(), m_auxiliary_windows.end(),
|
|
[](const QPointer<QWidget> &window) { return window.isNull(); }),
|
|
m_auxiliary_windows.end());
|
|
}
|
|
|
|
bool MainWindow::close_auxiliary_windows() {
|
|
prune_auxiliary_windows();
|
|
|
|
const auto windows = m_auxiliary_windows;
|
|
for (const QPointer<QWidget> &window : windows) {
|
|
if (!window || !window->isVisible())
|
|
continue;
|
|
|
|
window->close();
|
|
if (window && window->isVisible())
|
|
return false;
|
|
}
|
|
|
|
prune_auxiliary_windows();
|
|
return true;
|
|
}
|
|
|
|
void MainWindow::closeEvent(QCloseEvent *event) {
|
|
if (m_is_app_closing) {
|
|
QMainWindow::closeEvent(event);
|
|
return;
|
|
}
|
|
|
|
const bool confirm_exit_app = m_gui_settings
|
|
? m_gui_settings->get_value(gui::mw_confirmExitApp).toBool()
|
|
: true;
|
|
|
|
if (m_game_window && confirm_exit_app) {
|
|
QMessageBox box(this);
|
|
box.setIcon(QMessageBox::Question);
|
|
box.setWindowTitle(tr("Exit App?"));
|
|
box.setText(tr("An app is still running. Do you really want to exit?\n\nAny unsaved progress will be lost!"));
|
|
box.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
|
box.setDefaultButton(QMessageBox::No);
|
|
|
|
auto *dont_show_again = new QCheckBox(tr("Don't show this again"), &box);
|
|
box.setCheckBox(dont_show_again);
|
|
|
|
if (box.exec() != QMessageBox::Yes) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
if (dont_show_again->isChecked() && m_gui_settings)
|
|
m_gui_settings->set_value(gui::mw_confirmExitApp, false);
|
|
}
|
|
|
|
if (!close_auxiliary_windows()) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
m_is_app_closing = true;
|
|
if (m_live_area_widget)
|
|
on_live_area_closed();
|
|
|
|
if (m_game_window)
|
|
on_game_closed();
|
|
|
|
save_window_state();
|
|
QMainWindow::closeEvent(event);
|
|
}
|
|
|
|
void MainWindow::paintEvent(QPaintEvent *event) {
|
|
QMainWindow::paintEvent(event);
|
|
|
|
if (m_theme_background_scaled_pixmaps.empty())
|
|
return;
|
|
|
|
if (m_theme_background_scaled_size != size())
|
|
rescale_theme_background_pixmaps();
|
|
if (m_theme_background_scaled_pixmaps.empty())
|
|
return;
|
|
|
|
const int current_index = std::clamp(m_theme_background_current_index, 0,
|
|
static_cast<int>(m_theme_background_scaled_pixmaps.size()) - 1);
|
|
|
|
QPainter painter(this);
|
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
|
|
const auto draw_index = [this, &painter](const int index, const qreal opacity) {
|
|
if (index < 0 || index >= static_cast<int>(m_theme_background_scaled_pixmaps.size()) || opacity <= 0.0)
|
|
return;
|
|
|
|
painter.setOpacity(opacity);
|
|
painter.drawPixmap(rect(), m_theme_background_scaled_pixmaps[static_cast<std::size_t>(index)]);
|
|
};
|
|
|
|
if (m_theme_background_next_index >= 0 && m_theme_background_next_index < static_cast<int>(m_theme_background_scaled_pixmaps.size())) {
|
|
draw_index(current_index, 1.0 - m_theme_background_fade_progress);
|
|
draw_index(m_theme_background_next_index, m_theme_background_fade_progress);
|
|
} else {
|
|
draw_index(current_index, 1.0);
|
|
}
|
|
}
|
|
|
|
void MainWindow::save_window_state() {
|
|
if (!m_gui_settings)
|
|
return;
|
|
|
|
m_gui_settings->set_value(gui::mw_geometry, saveGeometry(), false);
|
|
m_gui_settings->set_value(gui::mw_windowState, saveState(), false);
|
|
if (m_dock_host)
|
|
m_gui_settings->set_value(gui::mw_mwState, m_dock_host->saveState(), false);
|
|
|
|
if (m_apps_list_widget)
|
|
m_apps_list_widget->save_settings(*m_gui_settings);
|
|
|
|
if (m_icon_size_slider)
|
|
m_gui_settings->set_value(gui::gl_iconSize, m_icon_size_slider->value(), false);
|
|
|
|
if (m_log_widget)
|
|
m_gui_settings->set_value(gui::mw_loggerVisible, m_log_widget->isVisible(), false);
|
|
if (m_apps_list_widget)
|
|
m_gui_settings->set_value(gui::mw_appsListVisible, m_apps_list_widget->isVisible(), false);
|
|
|
|
m_gui_settings->set_value(gui::mw_toolBarVisible, m_ui->show_toolbar_action->isChecked(), false);
|
|
m_gui_settings->set_value(gui::mw_titleBarsVisible, m_ui->show_title_bars_action->isChecked(), false);
|
|
|
|
m_gui_settings->sync();
|
|
}
|
|
|
|
void MainWindow::restore_window_state() {
|
|
if (!m_gui_settings)
|
|
return;
|
|
|
|
if (!restoreGeometry(m_gui_settings->get_value(gui::mw_geometry).toByteArray()))
|
|
resize(1280, 720);
|
|
|
|
restoreState(m_gui_settings->get_value(gui::mw_windowState).toByteArray());
|
|
|
|
if (m_apps_list_widget)
|
|
m_apps_list_widget->load_settings(*m_gui_settings);
|
|
|
|
if (m_icon_size_slider) {
|
|
const int icon_size = m_gui_settings->get_value(gui::gl_iconSize).toInt();
|
|
m_icon_size_slider->setValue(icon_size);
|
|
}
|
|
|
|
bool log_visible = m_gui_settings->get_value(gui::mw_loggerVisible).toBool();
|
|
bool gamelist_visible = m_gui_settings->get_value(gui::mw_appsListVisible).toBool();
|
|
if (!log_visible && !gamelist_visible) {
|
|
log_visible = true;
|
|
gamelist_visible = true;
|
|
m_gui_settings->set_value(gui::mw_loggerVisible, true, false);
|
|
m_gui_settings->set_value(gui::mw_appsListVisible, true, false);
|
|
}
|
|
|
|
if (m_dock_host) {
|
|
const QByteArray dock_state = m_gui_settings->get_value(gui::mw_mwState).toByteArray();
|
|
if (!dock_state.isEmpty() && m_apps_list_widget && m_log_widget) {
|
|
m_dock_host->restoreState(dock_state);
|
|
|
|
if (!m_apps_list_widget->isVisible() && !m_log_widget->isVisible()) {
|
|
m_dock_host->addDockWidget(Qt::LeftDockWidgetArea, m_apps_list_widget);
|
|
m_dock_host->addDockWidget(Qt::LeftDockWidgetArea, m_log_widget);
|
|
m_dock_host->splitDockWidget(m_apps_list_widget, m_log_widget, Qt::Vertical);
|
|
m_dock_host->resizeDocks({ m_log_widget }, { m_dock_host->sizeHint().height() / 5 }, Qt::Vertical);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_log_widget) {
|
|
m_log_widget->setVisible(log_visible);
|
|
m_ui->show_log_action->setChecked(log_visible);
|
|
}
|
|
if (m_apps_list_widget) {
|
|
m_apps_list_widget->setVisible(gamelist_visible);
|
|
m_ui->show_apps_list_action->setChecked(gamelist_visible);
|
|
}
|
|
|
|
const bool toolbar_visible = m_gui_settings->get_value(gui::mw_toolBarVisible).toBool();
|
|
m_ui->show_toolbar_action->setChecked(toolbar_visible);
|
|
m_ui->toolBar->setVisible(toolbar_visible);
|
|
|
|
const bool title_bars_visible = m_gui_settings->get_value(gui::mw_titleBarsVisible).toBool();
|
|
m_ui->show_title_bars_action->setChecked(title_bars_visible);
|
|
show_title_bars(title_bars_visible);
|
|
}
|
|
|
|
void MainWindow::initialize() {
|
|
Q_INIT_RESOURCE(resources);
|
|
m_ui->setupUi(this);
|
|
setObjectName(QStringLiteral("main_window"));
|
|
|
|
this->resize(1280, 720);
|
|
this->setWindowIcon(QIcon(":/Vita3K.png"));
|
|
this->setWindowTitle(QString::fromStdString(window_title));
|
|
|
|
emuenv.compat.log_compat_warn = emuenv.cfg.log_compat_warn;
|
|
m_game_compat = new GameCompatibility(emuenv.compat, emuenv.cache_path.native(), this);
|
|
|
|
emuenv.vulkan_device_info = std::make_unique<renderer::VulkanDeviceInfo>(renderer::enumerate_vulkan_devices());
|
|
|
|
init_current_user();
|
|
|
|
auto *shell_frame = new ThemeSurface(this);
|
|
shell_frame->setObjectName(QStringLiteral("mw_frame"));
|
|
auto *shell_layout = new QVBoxLayout(shell_frame);
|
|
shell_layout->setContentsMargins(0, 0, 0, 0);
|
|
shell_layout->setSpacing(0);
|
|
setCentralWidget(shell_frame);
|
|
|
|
m_dock_host = new QMainWindow(shell_frame);
|
|
m_dock_host->setObjectName(QStringLiteral("mw_dock_host"));
|
|
m_dock_host->setWindowFlags(Qt::Widget);
|
|
m_dock_host->setContextMenuPolicy(Qt::PreventContextMenu);
|
|
m_dock_host->setDockNestingEnabled(true);
|
|
shell_layout->addWidget(m_dock_host);
|
|
|
|
m_apps_list_widget = new AppsList(emuenv, m_dock_host);
|
|
m_apps_list_widget->refresh();
|
|
m_dock_host->addDockWidget(Qt::LeftDockWidgetArea, m_apps_list_widget);
|
|
|
|
m_log_widget = new LogWidget(m_dock_host);
|
|
m_log_widget->attach();
|
|
m_dock_host->addDockWidget(Qt::LeftDockWidgetArea, m_log_widget);
|
|
m_dock_host->splitDockWidget(m_apps_list_widget, m_log_widget, Qt::Vertical);
|
|
m_dock_host->resizeDocks({ m_log_widget }, { m_dock_host->sizeHint().height() / 5 }, Qt::Vertical);
|
|
|
|
connect(m_apps_list_widget, &AppsList::boot_requested,
|
|
this, &MainWindow::on_app_selected);
|
|
|
|
connect(m_apps_list_widget, &AppsList::context_menu_requested,
|
|
this, &MainWindow::on_context_menu_requested);
|
|
|
|
m_sdl_pump_timer = new QTimer(this);
|
|
m_sdl_pump_timer->setInterval(4);
|
|
connect(m_sdl_pump_timer, &QTimer::timeout, this, &MainWindow::pump_sdl_events);
|
|
m_sdl_pump_timer->start();
|
|
|
|
setup_toolbar();
|
|
|
|
connect(m_ui->welcome_dialog_action, &QAction::triggered, this, [this]() {
|
|
auto *welcome = new WelcomeDialog(emuenv, true, this);
|
|
welcome->open();
|
|
});
|
|
|
|
connect(m_ui->trophy_collection_action, &QAction::triggered,
|
|
this, &MainWindow::open_trophy_collection);
|
|
|
|
connect(m_ui->vita_themes_action, &QAction::triggered,
|
|
this, &MainWindow::open_vita_themes);
|
|
|
|
connect(m_ui->user_management_action, &QAction::triggered,
|
|
this, &MainWindow::open_user_management);
|
|
|
|
connect(m_ui->settings_core_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::Core)); });
|
|
connect(m_ui->settings_cpu_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::CPU)); });
|
|
connect(m_ui->settings_gpu_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::Graphics)); });
|
|
connect(m_ui->settings_audio_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::Audio)); });
|
|
connect(m_ui->settings_camera_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::Camera)); });
|
|
connect(m_ui->settings_system_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::System)); });
|
|
connect(m_ui->settings_emulator_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::Emulator)); });
|
|
connect(m_ui->settings_gui_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::Interface)); });
|
|
connect(m_ui->settings_network_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::Network)); });
|
|
connect(m_ui->settings_debug_action, &QAction::triggered, this, [this] { open_settings(static_cast<int>(SettingsTab::Debug)); });
|
|
|
|
connect(m_ui->install_firmware_action, &QAction::triggered,
|
|
this, &MainWindow::on_install_firmware_triggered);
|
|
connect(m_ui->install_pkg_action, &QAction::triggered,
|
|
this, &MainWindow::on_install_pkg_triggered);
|
|
connect(m_ui->install_zip_action, &QAction::triggered,
|
|
this, &MainWindow::on_install_zip_triggered);
|
|
connect(m_ui->install_license_action, &QAction::triggered,
|
|
this, &MainWindow::on_install_license_triggered);
|
|
|
|
connect(m_ui->debug_threads_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::Threads); });
|
|
connect(m_ui->debug_mutexes_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::Mutexes); });
|
|
connect(m_ui->debug_lw_mutexes_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::LwMutexes); });
|
|
connect(m_ui->debug_condvars_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::Condvars); });
|
|
connect(m_ui->debug_lw_condvars_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::LwCondvars); });
|
|
connect(m_ui->debug_semaphores_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::Semaphores); });
|
|
connect(m_ui->debug_event_flags_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::EventFlags); });
|
|
connect(m_ui->debug_allocations_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::Allocations); });
|
|
connect(m_ui->debug_disassembly_action, &QAction::triggered, this, [this] { open_debug_widget(DebugWidget::Disassembly); });
|
|
|
|
connect(m_ui->pause_action, &QAction::triggered,
|
|
this, &MainWindow::on_toolbar_start);
|
|
connect(m_ui->stop_action, &QAction::triggered,
|
|
this, &MainWindow::on_stop_triggered);
|
|
|
|
connect(m_ui->show_apps_list_action, &QAction::triggered, this, [this](bool checked) {
|
|
m_apps_list_widget->setVisible(checked);
|
|
m_gui_settings->set_value(gui::mw_appsListVisible, checked);
|
|
});
|
|
connect(m_apps_list_widget, &QDockWidget::visibilityChanged,
|
|
m_ui->show_apps_list_action, &QAction::setChecked);
|
|
|
|
connect(m_ui->show_log_action, &QAction::triggered, this, [this](bool checked) {
|
|
m_log_widget->setVisible(checked);
|
|
m_gui_settings->set_value(gui::mw_loggerVisible, checked);
|
|
});
|
|
connect(m_log_widget, &QDockWidget::visibilityChanged,
|
|
m_ui->show_log_action, &QAction::setChecked);
|
|
|
|
connect(m_ui->show_toolbar_action, &QAction::triggered, this, [this](bool checked) {
|
|
m_ui->toolBar->setVisible(checked);
|
|
m_gui_settings->set_value(gui::mw_toolBarVisible, checked);
|
|
});
|
|
|
|
connect(m_ui->show_title_bars_action, &QAction::triggered, this, [this](bool checked) {
|
|
show_title_bars(checked);
|
|
m_gui_settings->set_value(gui::mw_titleBarsVisible, checked);
|
|
});
|
|
|
|
setAcceptDrops(true);
|
|
|
|
setup_status_bar();
|
|
m_update_manager = new UpdateManager(this);
|
|
connect(m_update_manager, &UpdateManager::update_available_changed, this, [this](const bool available) {
|
|
m_update_available = available;
|
|
update_update_available_button();
|
|
});
|
|
update_update_available_button();
|
|
|
|
setMinimumSize(350, minimumSizeHint().height());
|
|
restore_window_state();
|
|
|
|
apply_log_gui_settings();
|
|
|
|
m_theme_manager = std::make_unique<ThemeManager>(m_gui_settings);
|
|
m_theme_manager->set_pref_path(emuenv.pref_path);
|
|
m_theme_manager->set_vita_themes_root(emuenv.pref_path / "ux0/theme");
|
|
connect(m_theme_manager.get(), &ThemeManager::theme_state_changed, this, &MainWindow::repaint_gui);
|
|
connect(m_theme_manager.get(), &ThemeManager::theme_state_changed, this, &MainWindow::apply_theme_background_presentation);
|
|
|
|
m_theme_background_cycle_timer = new QTimer(this);
|
|
connect(m_theme_background_cycle_timer, &QTimer::timeout, this, &MainWindow::advance_theme_background_presentation);
|
|
|
|
m_theme_background_fade_animation = new QVariantAnimation(this);
|
|
m_theme_background_fade_animation->setDuration(900);
|
|
m_theme_background_fade_animation->setStartValue(0.0);
|
|
m_theme_background_fade_animation->setEndValue(1.0);
|
|
m_theme_background_fade_animation->setEasingCurve(QEasingCurve::InOutSine);
|
|
connect(m_theme_background_fade_animation, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) {
|
|
m_theme_background_fade_progress = value.toReal();
|
|
update();
|
|
});
|
|
connect(m_theme_background_fade_animation, &QVariantAnimation::finished, this, [this] {
|
|
if (m_theme_background_next_index >= 0)
|
|
m_theme_background_current_index = m_theme_background_next_index;
|
|
|
|
m_theme_background_next_index = -1;
|
|
m_theme_background_fade_progress = 0.0;
|
|
if (m_theme_background_cycle_timer && m_theme_background_scaled_pixmaps.size() > 1)
|
|
m_theme_background_cycle_timer->start(m_theme_background_cycle_interval_ms);
|
|
update();
|
|
});
|
|
|
|
init_first_run_stylesheet();
|
|
apply_stylesheet();
|
|
|
|
if (emuenv.cfg.show_welcome) {
|
|
WelcomeDialog welcome(emuenv, false, nullptr);
|
|
welcome.exec();
|
|
}
|
|
|
|
if (m_update_manager) {
|
|
const auto startup_mode = static_cast<UpdateStartupMode>(emuenv.cfg.check_for_updates_mode);
|
|
if (startup_mode != UPDATE_STARTUP_OFF) {
|
|
QTimer::singleShot(0, this, [this, startup_mode]() {
|
|
if (!m_update_manager)
|
|
return;
|
|
|
|
updater::UpdateCheckMode mode = updater::UpdateCheckMode::StartupPrompt;
|
|
switch (startup_mode) {
|
|
case UPDATE_STARTUP_BACKGROUND:
|
|
mode = updater::UpdateCheckMode::StartupBackground;
|
|
break;
|
|
case UPDATE_STARTUP_AUTO:
|
|
case UPDATE_STARTUP_PROMPT:
|
|
default:
|
|
mode = updater::UpdateCheckMode::StartupPrompt;
|
|
break;
|
|
}
|
|
|
|
m_update_manager->check_for_updates(mode, this);
|
|
});
|
|
}
|
|
}
|
|
|
|
connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this, [this]() {
|
|
apply_stylesheet();
|
|
});
|
|
|
|
init_discord();
|
|
|
|
if (emuenv.cfg.run_app_path.has_value()) {
|
|
QTimer::singleShot(0, this, [this]() {
|
|
const std::string title_id = *emuenv.cfg.run_app_path;
|
|
emuenv.cfg.run_app_path.reset();
|
|
boot_game(title_id);
|
|
});
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_install_firmware_triggered() {
|
|
auto *dlg = new FirmwareInstallDialog(emuenv, this);
|
|
connect(dlg, &FirmwareInstallDialog::install_complete,
|
|
this, &MainWindow::on_install_finished);
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
}
|
|
|
|
void MainWindow::on_install_pkg_triggered() {
|
|
auto *dlg = new PkgInstallDialog(emuenv, this);
|
|
connect(dlg, &PkgInstallDialog::install_complete,
|
|
this, [this](const QString &) { on_install_finished(); });
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
}
|
|
|
|
void MainWindow::on_install_zip_triggered() {
|
|
auto *dlg = new ArchiveInstallDialog(emuenv, this);
|
|
connect(dlg, &ArchiveInstallDialog::install_complete,
|
|
this, [this](const QList<ArchiveInstallResult> &) { on_install_finished(); });
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
}
|
|
|
|
void MainWindow::on_install_license_triggered() {
|
|
auto *dlg = new LicenseInstallDialog(emuenv, this);
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
}
|
|
|
|
void MainWindow::on_install_finished() {
|
|
m_apps_list_widget->refresh(true);
|
|
if (m_theme_manager)
|
|
m_theme_manager->refresh_vita_theme_catalog();
|
|
if (m_vita_themes_dialog)
|
|
m_vita_themes_dialog->reload();
|
|
}
|
|
|
|
bool MainWindow::prompt_startup_warnings() {
|
|
return prompt_admin_privileges_warning_if_needed();
|
|
}
|
|
|
|
void MainWindow::dragEnterEvent(QDragEnterEvent *event) {
|
|
if (event->mimeData()->hasUrls()) {
|
|
for (const QUrl &url : event->mimeData()->urls()) {
|
|
if (!url.isLocalFile())
|
|
continue;
|
|
const QString path = url.toLocalFile();
|
|
const QString ext = QFileInfo(path).suffix().toLower();
|
|
if (ext == QStringLiteral("pup") || ext == QStringLiteral("vpk")
|
|
|| ext == QStringLiteral("zip") || ext == QStringLiteral("pkg")
|
|
|| ext == QStringLiteral("bin") || ext == QStringLiteral("rif") || path.endsWith(QStringLiteral("work.bin"))
|
|
|| QFileInfo(path).isDir() || path.endsWith(QStringLiteral("theme.xml"))) {
|
|
event->acceptProposedAction();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::dropEvent(QDropEvent *event) {
|
|
std::vector<fs::path> drop_paths;
|
|
for (const QUrl &url : event->mimeData()->urls()) {
|
|
if (url.isLocalFile())
|
|
drop_paths.push_back(gui::utils::to_fs_path(url.toLocalFile()));
|
|
}
|
|
|
|
const auto is_license_path = [](const fs::path &path) {
|
|
const std::string ext = string_utils::tolower(path.extension().string());
|
|
return ext == ".bin" || ext == ".rif" || path.filename() == "work.bin";
|
|
};
|
|
|
|
std::vector<bool> handled(drop_paths.size(), false);
|
|
std::vector<fs::path> archive_paths;
|
|
for (size_t i = 0; i < drop_paths.size(); ++i) {
|
|
const std::string ext = string_utils::tolower(drop_paths[i].extension().string());
|
|
if (ext != ".pkg")
|
|
continue;
|
|
|
|
std::optional<fs::path> license_path;
|
|
for (size_t j = 0; j < drop_paths.size(); ++j) {
|
|
if (handled[j] || i == j || !is_license_path(drop_paths[j]))
|
|
continue;
|
|
license_path = drop_paths[j];
|
|
handled[j] = true;
|
|
break;
|
|
}
|
|
|
|
auto *dlg = new PkgInstallDialog(emuenv, drop_paths[i], license_path, this);
|
|
connect(dlg, &PkgInstallDialog::install_complete,
|
|
this, [this](const QString &) { on_install_finished(); });
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
handled[i] = true;
|
|
}
|
|
|
|
for (size_t i = 0; i < drop_paths.size(); ++i) {
|
|
if (handled[i])
|
|
continue;
|
|
|
|
const std::string ext = string_utils::tolower(drop_paths[i].extension().string());
|
|
if (ext == ".vpk" || ext == ".zip") {
|
|
archive_paths.push_back(drop_paths[i]);
|
|
handled[i] = true;
|
|
}
|
|
}
|
|
|
|
if (!archive_paths.empty()) {
|
|
auto *dlg = new ArchiveInstallDialog(emuenv, archive_paths, this);
|
|
connect(dlg, &ArchiveInstallDialog::install_complete,
|
|
this, [this](const QList<ArchiveInstallResult> &) { on_install_finished(); });
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
}
|
|
|
|
for (size_t i = 0; i < drop_paths.size(); ++i) {
|
|
if (!handled[i])
|
|
handle_drop(QString::fromUtf8(fs_utils::path_to_utf8(drop_paths[i]).c_str()));
|
|
}
|
|
}
|
|
|
|
void MainWindow::handle_drop(const QString &path) {
|
|
const fs::path drop_path = gui::utils::to_fs_path(path);
|
|
const std::string ext = string_utils::tolower(drop_path.extension().string());
|
|
|
|
if (ext == ".pup") {
|
|
auto *dlg = new FirmwareInstallDialog(emuenv, path, this);
|
|
connect(dlg, &FirmwareInstallDialog::install_complete,
|
|
this, &MainWindow::on_install_finished);
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
|
|
} else if (ext == ".vpk" || ext == ".zip") {
|
|
auto *dlg = new ArchiveInstallDialog(emuenv, { drop_path }, this);
|
|
connect(dlg, &ArchiveInstallDialog::install_complete,
|
|
this, [this](const QList<ArchiveInstallResult> &) { on_install_finished(); });
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
|
|
} else if (ext == ".pkg") {
|
|
auto *dlg = new PkgInstallDialog(emuenv, drop_path, this);
|
|
connect(dlg, &PkgInstallDialog::install_complete,
|
|
this, [this](const QString &) { on_install_finished(); });
|
|
connect(dlg, &QDialog::finished, dlg, &QObject::deleteLater);
|
|
dlg->exec();
|
|
|
|
} else if (ext == ".bin" || ext == ".rif" || drop_path.filename() == "work.bin") {
|
|
if (copy_license(emuenv, drop_path))
|
|
LOG_INFO("License installed via drop: {}", drop_path);
|
|
else
|
|
QMessageBox::critical(this, tr("Install Failed"),
|
|
tr("Failed to install the dropped license file.\n"
|
|
"The file may be corrupted."));
|
|
|
|
} else if (fs::is_directory(drop_path)) {
|
|
const uint32_t n = install_contents(emuenv, drop_path);
|
|
if (n > 0)
|
|
on_install_finished();
|
|
|
|
} else if (drop_path.filename() == "theme.xml") {
|
|
const uint32_t n = install_contents(emuenv, drop_path.parent_path());
|
|
if (n > 0)
|
|
on_install_finished();
|
|
|
|
} else {
|
|
LOG_ERROR("Dropped file '{}' is not a supported install type.",
|
|
drop_path.filename().string());
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_about_action_triggered() {
|
|
AboutDialog about(emuenv, this);
|
|
about.exec();
|
|
}
|
|
|
|
void MainWindow::on_about_qt_action_triggered() {
|
|
QApplication::aboutQt();
|
|
}
|
|
|
|
void MainWindow::on_controls_action_triggered() {
|
|
if (!m_controls_dialog) {
|
|
m_controls_dialog = new ControlsDialog(emuenv, m_gui_settings);
|
|
register_auxiliary_window(m_controls_dialog);
|
|
}
|
|
present_tool_window(m_controls_dialog);
|
|
}
|
|
|
|
void MainWindow::on_check_for_updates_action_triggered() {
|
|
if (m_update_manager)
|
|
m_update_manager->review_pending_update(this);
|
|
}
|
|
|
|
void MainWindow::open_settings(int tab_index) {
|
|
if (!m_settings_dialog) {
|
|
m_settings_dialog = new SettingsDialog(emuenv, m_gui_settings, *m_theme_manager, {}, tab_index);
|
|
register_auxiliary_window(m_settings_dialog);
|
|
connect(m_settings_dialog, &SettingsDialog::gui_stylesheet_request, this, &MainWindow::apply_stylesheet);
|
|
connect(m_settings_dialog, &SettingsDialog::gui_theme_music_settings_request, this, [this] {
|
|
if (m_theme_manager)
|
|
m_theme_manager->refresh_vita_theme_bgm();
|
|
});
|
|
connect(m_settings_dialog, &SettingsDialog::gui_log_settings_request, this, &MainWindow::apply_log_gui_settings);
|
|
connect(m_settings_dialog, &SettingsDialog::storage_path_changed, this, [this] {
|
|
if (m_apps_list_widget)
|
|
m_apps_list_widget->refresh(true);
|
|
apply_stylesheet();
|
|
});
|
|
connect(m_settings_dialog, &SettingsDialog::ui_language_request, this, &MainWindow::apply_ui_language);
|
|
connect(m_settings_dialog, &SettingsDialog::restart_game_requested, this, &MainWindow::restart_running_app);
|
|
connect(m_settings_dialog, &QDialog::finished, this, [this](int) {
|
|
sync_discord_presence();
|
|
refresh_status_bar();
|
|
apply_log_gui_settings();
|
|
});
|
|
}
|
|
|
|
m_settings_dialog->set_storage_path_locked(m_game_window != nullptr);
|
|
m_settings_dialog->show_tab(tab_index);
|
|
present_tool_window(m_settings_dialog);
|
|
}
|
|
|
|
void MainWindow::apply_log_gui_settings() {
|
|
if (!m_log_widget || !m_gui_settings)
|
|
return;
|
|
|
|
m_log_widget->set_log_buffer_size(m_gui_settings->get_value(gui::l_bufferSize).toInt());
|
|
m_log_widget->set_log_font_family(m_gui_settings->get_value(gui::l_fontFamily).toString());
|
|
}
|
|
|
|
std::optional<AppLaunchRequest> MainWindow::take_pending_app_launch_request() {
|
|
auto request = emuenv.take_app_launch_request();
|
|
if (request)
|
|
LOG_INFO("Handling in-process app relaunch for {} ({})", request->self_path, request->app_path);
|
|
|
|
return request;
|
|
}
|
|
|
|
bool MainWindow::handle_pending_app_launch_request() {
|
|
if (!m_game_window)
|
|
return false;
|
|
|
|
auto request = take_pending_app_launch_request();
|
|
if (!request)
|
|
return false;
|
|
|
|
on_game_closed();
|
|
if (request->reason != AppLaunchReason::ProcessExit)
|
|
boot_game(*request, false);
|
|
return true;
|
|
}
|
|
|
|
void MainWindow::apply_ui_language(const QString &locale_tag) {
|
|
gui::i18n::apply_ui_language(*qApp, locale_tag.toStdString(), emuenv.static_assets_path);
|
|
}
|
|
|
|
void MainWindow::init_current_user() {
|
|
const auto &users = emuenv.app.user_list.users;
|
|
|
|
if (users.empty()) {
|
|
const std::string id = app::create_user(emuenv, "Vita3K");
|
|
app::activate_user(emuenv, id);
|
|
emuenv.cfg.user_id = id;
|
|
config::serialize_config(emuenv.cfg, emuenv.cfg.config_path);
|
|
return;
|
|
}
|
|
|
|
if (!emuenv.cfg.user_id.empty() && users.contains(emuenv.cfg.user_id)) {
|
|
app::activate_user(emuenv, emuenv.cfg.user_id);
|
|
return;
|
|
}
|
|
|
|
const auto &first = users.begin();
|
|
app::activate_user(emuenv, first->first);
|
|
emuenv.cfg.user_id = first->first;
|
|
config::serialize_config(emuenv.cfg, emuenv.cfg.config_path);
|
|
}
|
|
|
|
void MainWindow::on_app_selected(const app::AppEntry &app) {
|
|
if (emuenv.cfg.show_live_area_screen && !m_game_window)
|
|
show_live_area(app.title_id);
|
|
else
|
|
boot_game(app.title_id);
|
|
}
|
|
|
|
void MainWindow::boot_game(const std::string &title_id) {
|
|
boot_game(AppLaunchRequest{
|
|
.app_path = title_id,
|
|
});
|
|
}
|
|
|
|
void MainWindow::boot_game(const AppLaunchRequest &launch_request, const bool prompt_before_closing_existing) {
|
|
if (m_sdl_pump_timer)
|
|
m_sdl_pump_timer->stop();
|
|
|
|
AppLaunchRequest current_request = launch_request;
|
|
bool should_prompt_before_closing = prompt_before_closing_existing;
|
|
|
|
while (true) {
|
|
auto next_request = boot_game_once(current_request, should_prompt_before_closing);
|
|
if (!next_request)
|
|
break;
|
|
|
|
current_request = std::move(*next_request);
|
|
should_prompt_before_closing = false;
|
|
}
|
|
|
|
if (m_sdl_pump_timer)
|
|
m_sdl_pump_timer->start();
|
|
}
|
|
|
|
std::optional<AppLaunchRequest> MainWindow::boot_game_once(const AppLaunchRequest &launch_request, const bool prompt_before_closing_existing) {
|
|
if (m_game_window) {
|
|
if (prompt_before_closing_existing) {
|
|
const int result = QMessageBox::question(this, tr("Switch App?"),
|
|
tr("An app is already running.\n"
|
|
"Do you want to close it and launch another app?\n\n"
|
|
"Any unsaved progress will be lost!"),
|
|
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
|
|
|
if (result != QMessageBox::Yes)
|
|
return std::nullopt;
|
|
}
|
|
|
|
on_game_closed();
|
|
}
|
|
|
|
if (launch_request.reason != AppLaunchReason::LoadExec && !confirm_missing_firmware_warning())
|
|
return std::nullopt;
|
|
|
|
refresh_controllers(emuenv.ctrl, emuenv);
|
|
if (m_controls_dialog)
|
|
m_controls_dialog->sync_controller_state();
|
|
|
|
const bool update_last_time_used = launch_request.reason != AppLaunchReason::LoadExec && !m_live_area_widget;
|
|
if (!m_app_session.begin_launch(launch_request, update_last_time_used)) {
|
|
QMessageBox::critical(this, tr("Error"),
|
|
tr("Could not find app '%1' in apps list.").arg(QString::fromStdString(launch_request.app_path)));
|
|
return std::nullopt;
|
|
}
|
|
if (m_theme_manager)
|
|
m_theme_manager->set_vita_theme_bgm_blocked(true);
|
|
|
|
bool launch_fullscreen = emuenv.cfg.boot_apps_full_screen;
|
|
if (m_game_container) {
|
|
launch_fullscreen = m_game_container->visibility() == QWindow::FullScreen;
|
|
}
|
|
|
|
m_game_window = new GameWindow(emuenv, m_gui_settings, emuenv.backend_renderer, screen());
|
|
m_game_window->setTitle(tr("%1 (%2) | Loading...")
|
|
.arg(QString::fromStdString(emuenv.current_app_title),
|
|
QString::fromStdString(emuenv.io.title_id)));
|
|
m_game_window->setIcon(windowIcon());
|
|
if (m_live_area_widget)
|
|
m_game_window->setGeometry(m_live_area_widget->geometry());
|
|
|
|
if (m_live_area_widget) {
|
|
m_live_area_widget->removeEventFilter(this);
|
|
m_live_area_widget->deleteLater();
|
|
m_live_area_widget = nullptr;
|
|
}
|
|
|
|
m_game_window->create();
|
|
|
|
m_game_container = m_game_window;
|
|
m_game_window->show();
|
|
|
|
if (launch_fullscreen)
|
|
m_game_window->showFullScreen();
|
|
|
|
m_fullscreen = launch_fullscreen;
|
|
m_ui->toolbar_fullscreen->setIcon(m_fullscreen ? m_icon_fullscreen_off : m_icon_fullscreen_on);
|
|
m_ui->toolbar_fullscreen->setText(m_fullscreen ? tr("Exit Fullscreen") : tr("Fullscreen"));
|
|
|
|
connect(m_game_window, &GameWindow::closed,
|
|
this, &MainWindow::on_game_closed);
|
|
|
|
const auto abort_boot = [&](const QString &msg) {
|
|
QMessageBox::critical(this, tr("Error"), msg);
|
|
m_app_session.stop(app::AppSessionStopReason::LaunchFailure);
|
|
if (m_theme_manager)
|
|
m_theme_manager->set_vita_theme_bgm_blocked(false);
|
|
|
|
if (m_game_window) {
|
|
delete m_game_window;
|
|
m_game_window = nullptr;
|
|
}
|
|
m_game_container = nullptr;
|
|
};
|
|
|
|
if (emuenv.backend_renderer == renderer::Backend::OpenGL) {
|
|
if (!m_game_window->create_gl_context()) {
|
|
abort_boot(tr("Could not create OpenGL context.\nDoes your GPU support at least OpenGL 4.4?"));
|
|
return {};
|
|
}
|
|
if (!m_game_window->make_current()) {
|
|
abort_boot(tr("Could not make OpenGL context current."));
|
|
return {};
|
|
}
|
|
}
|
|
|
|
if (!m_app_session.initialize_renderer(*m_game_window)) {
|
|
abort_boot(tr("Failed to initialise the renderer."));
|
|
return {};
|
|
}
|
|
|
|
m_game_window->setTitle(tr("%1 (%2) | Initializing...")
|
|
.arg(QString::fromStdString(emuenv.current_app_title),
|
|
QString::fromStdString(emuenv.io.title_id)));
|
|
|
|
if (!m_app_session.initialize_runtime()) {
|
|
abort_boot(tr("Failed to initialize emulator state."));
|
|
return {};
|
|
}
|
|
|
|
m_game_window->setTitle(tr("%1 (%2) | Loading modules...")
|
|
.arg(QString::fromStdString(emuenv.current_app_title),
|
|
QString::fromStdString(emuenv.io.title_id)));
|
|
|
|
if (!m_app_session.load_and_run()) {
|
|
abort_boot(tr("Failed to start game threads."));
|
|
return {};
|
|
}
|
|
|
|
auto *kb_filter = new CtrlKeyboardFilter(emuenv, m_game_window);
|
|
m_game_window->installEventFilter(kb_filter);
|
|
m_kb_filter = kb_filter;
|
|
|
|
auto *ime_filter = new ImeKeyboardFilter(emuenv, m_game_window);
|
|
m_game_window->installEventFilter(ime_filter);
|
|
|
|
connect(m_kb_filter, &CtrlKeyboardFilter::ps_button_pressed,
|
|
this, &MainWindow::on_ps_button);
|
|
|
|
connect(m_kb_filter, &CtrlKeyboardFilter::fullscreen_toggled,
|
|
this, [this]() { on_toolbar_fullscreen(); });
|
|
|
|
connect(m_kb_filter, &CtrlKeyboardFilter::texture_replacement_toggled,
|
|
this, [this]() { toggle_texture_replacement(emuenv); });
|
|
|
|
connect(m_kb_filter, &CtrlKeyboardFilter::screenshot_requested,
|
|
this, [this]() { take_screenshot(emuenv); });
|
|
|
|
if (auto next_request = take_pending_app_launch_request()) {
|
|
on_game_closed();
|
|
return next_request;
|
|
}
|
|
|
|
m_game_window->start_ui_updates();
|
|
|
|
m_game_window->setTitle(tr("%1 (%2) | Please wait, loading...")
|
|
.arg(QString::fromStdString(emuenv.current_app_title),
|
|
QString::fromStdString(emuenv.io.title_id)));
|
|
|
|
LOG_INFO("Game started: {} ({})", emuenv.current_app_title, launch_request.app_path);
|
|
|
|
if (m_settings_dialog)
|
|
m_settings_dialog->set_storage_path_locked(true);
|
|
refresh_emulation_actions();
|
|
refresh_status_bar();
|
|
return std::nullopt;
|
|
}
|
|
|
|
void MainWindow::on_game_closed() {
|
|
if (!m_game_window)
|
|
return;
|
|
|
|
LOG_INFO("Game closed: {}", emuenv.current_app_title);
|
|
|
|
m_game_window->stop_ui_updates();
|
|
m_app_session.stop(m_is_app_closing
|
|
? app::AppSessionStopReason::FrontendShutdown
|
|
: app::AppSessionStopReason::UserRequest);
|
|
refresh_controllers(emuenv.ctrl, emuenv);
|
|
if (m_controls_dialog)
|
|
m_controls_dialog->sync_controller_state();
|
|
|
|
QWindow *closed_window = m_game_container;
|
|
|
|
m_game_window = nullptr;
|
|
m_game_container = nullptr;
|
|
|
|
if (closed_window)
|
|
delete closed_window;
|
|
|
|
m_kb_filter = nullptr;
|
|
m_fullscreen = false;
|
|
m_ui->toolbar_fullscreen->setIcon(m_icon_fullscreen_on);
|
|
m_ui->toolbar_fullscreen->setText(tr("Fullscreen"));
|
|
|
|
if (m_settings_dialog)
|
|
m_settings_dialog->set_storage_path_locked(false);
|
|
if (m_theme_manager && !m_is_app_closing)
|
|
m_theme_manager->set_vita_theme_bgm_blocked(false);
|
|
refresh_emulation_actions();
|
|
|
|
if (!m_is_app_closing && m_apps_list_widget)
|
|
m_apps_list_widget->refresh();
|
|
refresh_status_bar();
|
|
|
|
if (!m_is_app_closing) {
|
|
raise();
|
|
activateWindow();
|
|
}
|
|
}
|
|
|
|
void MainWindow::restart_running_app() {
|
|
if (!m_game_window)
|
|
return;
|
|
|
|
AppLaunchRequest relaunch_request{
|
|
.app_path = emuenv.io.app_path,
|
|
};
|
|
on_game_closed();
|
|
boot_game(relaunch_request, false);
|
|
}
|
|
|
|
bool MainWindow::confirm_missing_firmware_warning() {
|
|
if (!emuenv.cfg.warn_missing_firmware)
|
|
return true;
|
|
|
|
const app::FirmwareState firmware = app::get_firmware_state(emuenv);
|
|
if (firmware.main_firmware && firmware.font_package)
|
|
return true;
|
|
|
|
QStringList missing_components;
|
|
if (!firmware.main_firmware)
|
|
missing_components << tr("Main firmware");
|
|
if (!firmware.font_package)
|
|
missing_components << tr("Font package");
|
|
|
|
const auto result = gui::utils::show_message_box(
|
|
this,
|
|
QMessageBox::Warning,
|
|
tr("Missing Firmware"),
|
|
tr("Firmware is not fully installed."),
|
|
{
|
|
{ QStringLiteral("launch"), tr("Launch Anyway"), QMessageBox::AcceptRole, false },
|
|
{ QStringLiteral("cancel"), tr("Cancel"), QMessageBox::RejectRole, true },
|
|
},
|
|
tr(
|
|
"The following firmware components are missing:\n- %1\n\n"
|
|
"Games may fail to boot or render correctly until they are installed.")
|
|
.arg(missing_components.join(QStringLiteral("\n- "))),
|
|
tr("Don't show this warning again"));
|
|
|
|
if (result.checkbox_checked) {
|
|
emuenv.cfg.warn_missing_firmware = false;
|
|
config::serialize_config(emuenv.cfg, emuenv.cfg.config_path);
|
|
}
|
|
|
|
return result.clicked_id == QStringLiteral("launch");
|
|
}
|
|
|
|
bool MainWindow::prompt_admin_privileges_warning_if_needed() {
|
|
const bool warn_admin_privileges = m_gui_settings
|
|
? m_gui_settings->get_value(gui::mw_warnAdminPrivileges).toBool()
|
|
: true;
|
|
if (!m_admin_privileged || !warn_admin_privileges)
|
|
return true;
|
|
|
|
const QString title = tr("Running with Administrator Privileges");
|
|
const QString text = tr(
|
|
"Vita3K is running with administrator or root privileges.\n\n"
|
|
"This can create files owned by the wrong user and cause permission problems later.\n"
|
|
"Please close Vita3K and relaunch it without elevated privileges.");
|
|
|
|
LOG_WARN("{}", text.toUtf8().constData());
|
|
|
|
const auto result = gui::utils::show_message_box(
|
|
this,
|
|
QMessageBox::Warning,
|
|
title,
|
|
text,
|
|
{
|
|
{ QStringLiteral("continue"), tr("Continue"), QMessageBox::AcceptRole, false },
|
|
{ QStringLiteral("exit"), tr("Exit"), QMessageBox::DestructiveRole, true },
|
|
},
|
|
{},
|
|
tr("Don't show this warning again"));
|
|
|
|
if (result.checkbox_checked && m_gui_settings)
|
|
m_gui_settings->set_value(gui::mw_warnAdminPrivileges, false);
|
|
|
|
if (result.clicked_id == QStringLiteral("continue"))
|
|
return true;
|
|
|
|
close();
|
|
return false;
|
|
}
|
|
|
|
void MainWindow::show_live_area(const std::string &title_id) {
|
|
if (m_live_area_widget || m_game_window)
|
|
return;
|
|
|
|
if (!app::set_app_info(emuenv, title_id)) {
|
|
QMessageBox::critical(this, tr("Error"),
|
|
tr("Could not find app '%1' in apps list.").arg(QString::fromStdString(title_id)));
|
|
return;
|
|
}
|
|
|
|
app::set_current_config(emuenv, title_id);
|
|
get_license(emuenv, emuenv.io.title_id, emuenv.io.content_id);
|
|
app::update_last_time_app_used(emuenv, title_id);
|
|
m_live_area_title_id = title_id;
|
|
|
|
m_live_area_widget = new LiveAreaWidget(emuenv, emuenv.io.app_path);
|
|
m_game_container = m_live_area_widget;
|
|
|
|
m_live_area_widget->setTitle(tr("%1 | %2 (%3)")
|
|
.arg(QString::fromStdString(window_title),
|
|
QString::fromStdString(emuenv.current_app_title),
|
|
QString::fromStdString(emuenv.io.title_id)));
|
|
m_live_area_widget->setIcon(windowIcon());
|
|
m_live_area_widget->installEventFilter(this);
|
|
m_live_area_widget->show();
|
|
|
|
if (emuenv.cfg.boot_apps_full_screen)
|
|
m_live_area_widget->showFullScreen();
|
|
|
|
m_fullscreen = emuenv.cfg.boot_apps_full_screen;
|
|
m_ui->toolbar_fullscreen->setIcon(m_fullscreen ? m_icon_fullscreen_off : m_icon_fullscreen_on);
|
|
m_ui->toolbar_fullscreen->setText(m_fullscreen ? tr("Exit Fullscreen") : tr("Fullscreen"));
|
|
|
|
connect(m_live_area_widget, &LiveAreaWidget::play_clicked,
|
|
this, &MainWindow::on_live_area_play);
|
|
}
|
|
|
|
void MainWindow::on_live_area_play() {
|
|
const auto title_id = m_live_area_title_id;
|
|
m_live_area_title_id.clear();
|
|
boot_game(title_id);
|
|
}
|
|
|
|
void MainWindow::on_live_area_closed() {
|
|
QWindow *closed_window = m_game_container;
|
|
if (closed_window)
|
|
closed_window->removeEventFilter(this);
|
|
|
|
m_live_area_widget = nullptr;
|
|
m_live_area_title_id.clear();
|
|
m_game_container = nullptr;
|
|
m_fullscreen = false;
|
|
m_ui->toolbar_fullscreen->setIcon(m_icon_fullscreen_on);
|
|
m_ui->toolbar_fullscreen->setText(tr("Fullscreen"));
|
|
|
|
app::set_current_config(emuenv, "");
|
|
|
|
if (closed_window)
|
|
delete closed_window;
|
|
|
|
if (!m_is_app_closing) {
|
|
raise();
|
|
activateWindow();
|
|
}
|
|
}
|
|
|
|
bool MainWindow::eventFilter(QObject *watched, QEvent *event) {
|
|
if (watched == m_live_area_widget) {
|
|
if (event->type() == QEvent::Close) {
|
|
event->ignore();
|
|
|
|
on_live_area_closed();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return QMainWindow::eventFilter(watched, event);
|
|
}
|
|
|
|
void MainWindow::pump_sdl_events() {
|
|
#if USE_DISCORD
|
|
discordrpc::run_callbacks();
|
|
#endif
|
|
|
|
if (handle_pending_app_launch_request())
|
|
return;
|
|
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event)) {
|
|
switch (event.type) {
|
|
case SDL_EVENT_GAMEPAD_ADDED:
|
|
case SDL_EVENT_GAMEPAD_REMOVED:
|
|
refresh_controllers(emuenv.ctrl, emuenv);
|
|
if (m_controls_dialog)
|
|
m_controls_dialog->sync_controller_state();
|
|
break;
|
|
|
|
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
|
if (m_controls_dialog && m_controls_dialog->handle_sdl_event(event))
|
|
break;
|
|
break;
|
|
|
|
case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
|
|
case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
|
|
case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
|
|
handle_touchpad_event(emuenv.touch, event.gtouchpad);
|
|
break;
|
|
|
|
case SDL_EVENT_GAMEPAD_SENSOR_UPDATE:
|
|
handle_motion_event(emuenv, event.gsensor.sensor, event.gsensor);
|
|
break;
|
|
|
|
case SDL_EVENT_SENSOR_UPDATE:
|
|
handle_motion_event(emuenv, SDL_GetSensorTypeForID(event.sensor.which), event.sensor);
|
|
break;
|
|
|
|
case SDL_EVENT_FINGER_DOWN:
|
|
case SDL_EVENT_FINGER_MOTION:
|
|
case SDL_EVENT_FINGER_UP:
|
|
handle_touch_event(emuenv.touch, event.tfinger);
|
|
break;
|
|
|
|
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
|
if (m_controls_dialog && m_controls_dialog->handle_sdl_event(event))
|
|
break;
|
|
if (event.gbutton.button == SDL_GAMEPAD_BUTTON_GUIDE)
|
|
on_ps_button();
|
|
if (m_game_window && !emuenv.kernel.is_threads_paused()
|
|
&& event.gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD)
|
|
toggle_touchscreen(emuenv.touch);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_pause_triggered() {
|
|
if (!m_game_window)
|
|
return;
|
|
|
|
const bool is_paused = m_app_session.is_paused();
|
|
m_app_session.set_pause_reason(app::AppSessionPauseReason::User, !is_paused);
|
|
refresh_emulation_actions();
|
|
}
|
|
|
|
void MainWindow::on_stop_triggered() {
|
|
if (!m_game_window)
|
|
return;
|
|
|
|
m_app_session.set_pause_reason(app::AppSessionPauseReason::User, false);
|
|
|
|
on_game_closed();
|
|
}
|
|
|
|
void MainWindow::on_ps_button() {
|
|
on_pause_triggered();
|
|
}
|
|
|
|
void MainWindow::refresh_emulation_actions() {
|
|
const bool running = m_game_window != nullptr;
|
|
const bool paused = running && m_app_session.is_paused();
|
|
|
|
m_ui->stop_action->setEnabled(running);
|
|
m_ui->stop_action->setIcon(m_icon_stop);
|
|
m_ui->toolbar_stop->setEnabled(running);
|
|
|
|
if (running) {
|
|
if (paused) {
|
|
m_ui->pause_action->setText(tr("Resume"));
|
|
m_ui->pause_action->setIcon(m_icon_play);
|
|
m_ui->pause_action->setEnabled(true);
|
|
m_ui->toolbar_start->setIcon(m_icon_play);
|
|
m_ui->toolbar_start->setText(tr("Resume"));
|
|
m_ui->toolbar_start->setEnabled(true);
|
|
} else {
|
|
m_ui->pause_action->setText(tr("Pause"));
|
|
m_ui->pause_action->setIcon(m_icon_pause);
|
|
m_ui->pause_action->setEnabled(true);
|
|
m_ui->toolbar_start->setIcon(m_icon_pause);
|
|
m_ui->toolbar_start->setText(tr("Pause"));
|
|
m_ui->toolbar_start->setEnabled(true);
|
|
}
|
|
} else {
|
|
m_ui->pause_action->setText(tr("Play"));
|
|
m_ui->pause_action->setIcon(m_icon_play);
|
|
m_ui->pause_action->setEnabled(m_app_selected);
|
|
m_ui->toolbar_start->setIcon(m_icon_play);
|
|
m_ui->toolbar_start->setText(tr("Play"));
|
|
m_ui->toolbar_start->setEnabled(m_app_selected);
|
|
}
|
|
}
|
|
|
|
void MainWindow::open_trophy_collection() {
|
|
if (!m_trophy_dialog) {
|
|
m_trophy_dialog = new TrophyCollectionDialog(emuenv, m_gui_settings);
|
|
register_auxiliary_window(m_trophy_dialog);
|
|
}
|
|
present_tool_window(m_trophy_dialog);
|
|
}
|
|
|
|
void MainWindow::open_vita_themes() {
|
|
if (!m_vita_themes_dialog) {
|
|
m_vita_themes_dialog = new VitaThemesDialog(m_gui_settings, *m_theme_manager, this);
|
|
register_auxiliary_window(m_vita_themes_dialog);
|
|
}
|
|
|
|
present_tool_window(m_vita_themes_dialog);
|
|
}
|
|
|
|
void MainWindow::open_user_management() {
|
|
if (m_game_window) {
|
|
return;
|
|
}
|
|
|
|
if (!m_user_mgmt_dialog) {
|
|
m_user_mgmt_dialog = new UserManagementDialog(emuenv, this);
|
|
connect(m_user_mgmt_dialog, &UserManagementDialog::user_changed,
|
|
m_apps_list_widget, [this] { m_apps_list_widget->refresh(); });
|
|
connect(m_user_mgmt_dialog, &UserManagementDialog::user_changed,
|
|
this, [this] {
|
|
if (m_trophy_dialog)
|
|
m_trophy_dialog->reload();
|
|
});
|
|
}
|
|
present_tool_window(m_user_mgmt_dialog);
|
|
}
|
|
|
|
void MainWindow::open_debug_widget(int tab) {
|
|
if (!m_debug_widget) {
|
|
m_debug_widget = new DebugWidget(emuenv, this);
|
|
m_debug_widget->setFloating(true);
|
|
}
|
|
m_debug_widget->show_tab(tab);
|
|
}
|
|
|
|
void MainWindow::setup_toolbar() {
|
|
auto *tb = m_ui->toolBar;
|
|
tb->setObjectName(QStringLiteral("mw_toolbar"));
|
|
|
|
connect(m_ui->toolbar_open, &QAction::triggered, this, [this] {
|
|
auto *tb = m_ui->toolBar;
|
|
auto *w = tb->widgetForAction(m_ui->toolbar_open);
|
|
const QPoint pos = w ? w->mapToGlobal(QPoint(0, w->height())) : QCursor::pos();
|
|
|
|
QMenu menu(this);
|
|
menu.addAction(tr("Open Emulated Storage Path"), this, [this] {
|
|
gui::utils::open_dir(emuenv.pref_path);
|
|
});
|
|
|
|
const auto patch_path = emuenv.shared_path / "patch";
|
|
menu.addAction(tr("Open Patch Path"), this, [patch_path] {
|
|
gui::utils::open_dir(patch_path);
|
|
});
|
|
|
|
const auto textures_path = emuenv.shared_path / "textures";
|
|
menu.addAction(tr("Open Textures Path"), this, [textures_path] {
|
|
if (!fs::exists(textures_path))
|
|
fs::create_directories(textures_path);
|
|
gui::utils::open_dir(textures_path);
|
|
});
|
|
|
|
menu.exec(pos);
|
|
});
|
|
|
|
connect(m_ui->toolbar_refresh, &QAction::triggered,
|
|
this, &MainWindow::on_toolbar_refresh);
|
|
|
|
connect(m_ui->toolbar_fullscreen, &QAction::triggered,
|
|
this, &MainWindow::on_toolbar_fullscreen);
|
|
|
|
connect(m_ui->toolbar_stop, &QAction::triggered,
|
|
this, &MainWindow::on_stop_triggered);
|
|
|
|
connect(m_ui->toolbar_start, &QAction::triggered,
|
|
this, &MainWindow::on_toolbar_start);
|
|
|
|
connect(m_ui->toolbar_config, &QAction::triggered,
|
|
this, [this] { open_settings(0); });
|
|
|
|
connect(m_ui->toolbar_controls, &QAction::triggered,
|
|
this, &MainWindow::on_controls_action_triggered);
|
|
|
|
m_slider_container = new ThemeSurface(this);
|
|
m_slider_container->setObjectName(QStringLiteral("toolbar_icon_size_container"));
|
|
auto *slider_layout = new QHBoxLayout(m_slider_container);
|
|
slider_layout->setContentsMargins(14, 0, 14, 0);
|
|
slider_layout->setSpacing(0);
|
|
|
|
m_icon_size_slider = new QSlider(Qt::Horizontal, m_slider_container);
|
|
m_icon_size_slider->setObjectName(QStringLiteral("toolbar_icon_size_slider"));
|
|
m_icon_size_slider->setRange(0, 100);
|
|
m_icon_size_slider->setValue(25);
|
|
m_icon_size_slider->setFocusPolicy(Qt::ClickFocus);
|
|
m_slider_container->setToolTip(tr("Icon Size"));
|
|
m_icon_size_slider->setToolTip(tr("Icon Size"));
|
|
slider_layout->addWidget(m_icon_size_slider);
|
|
|
|
tb->addWidget(m_slider_container);
|
|
|
|
connect(m_icon_size_slider, &QSlider::valueChanged,
|
|
this, &MainWindow::resize_icons);
|
|
|
|
m_search_bar = new QLineEdit(this);
|
|
m_search_bar->setObjectName(QStringLiteral("mw_searchbar"));
|
|
m_search_bar->setPlaceholderText(tr("Search..."));
|
|
m_search_bar->setFocusPolicy(Qt::ClickFocus);
|
|
m_search_bar->setClearButtonEnabled(true);
|
|
tb->addWidget(m_search_bar);
|
|
|
|
connect(m_search_bar, &QLineEdit::textChanged,
|
|
m_apps_list_widget, &AppsList::set_search_text);
|
|
|
|
connect(m_apps_list_widget, &AppsList::app_selection_changed,
|
|
this, &MainWindow::on_app_selection_changed);
|
|
|
|
repaint_toolbar_icons();
|
|
|
|
resize_icons(m_icon_size_slider->value());
|
|
refresh_emulation_actions();
|
|
}
|
|
|
|
void MainWindow::show_title_bars(bool show) const {
|
|
if (m_apps_list_widget)
|
|
m_apps_list_widget->set_title_bar_visible(show);
|
|
if (m_log_widget)
|
|
m_log_widget->set_title_bar_visible(show);
|
|
}
|
|
|
|
void MainWindow::repaint_gui() {
|
|
repaint_toolbar_icons();
|
|
if (m_log_widget)
|
|
m_log_widget->repaint_text_colors();
|
|
if (m_apps_list_widget)
|
|
m_apps_list_widget->update();
|
|
update();
|
|
}
|
|
|
|
void MainWindow::init_first_run_stylesheet() {
|
|
if (!m_gui_settings || m_gui_settings->contains_value(gui::m_currentStylesheet))
|
|
return;
|
|
|
|
const QString stylesheet = gui::utils::dark_mode_active()
|
|
? gui::DarkStylesheet
|
|
: gui::LightStylesheet;
|
|
|
|
m_gui_settings->set_value(gui::m_currentStylesheet, stylesheet);
|
|
}
|
|
|
|
void MainWindow::apply_stylesheet() {
|
|
if (!m_theme_manager)
|
|
return;
|
|
m_theme_manager->ensure_generated_theme_ready(emuenv.pref_path / "ux0/theme");
|
|
m_theme_manager->apply_theme({}, true);
|
|
}
|
|
|
|
void MainWindow::apply_theme_background_presentation() {
|
|
if (!m_theme_manager) {
|
|
clear_theme_background_presentation();
|
|
return;
|
|
}
|
|
|
|
const auto state = m_theme_manager->current_applied_theme_state();
|
|
if (!state || state->background.image_paths.isEmpty()) {
|
|
clear_theme_background_presentation();
|
|
return;
|
|
}
|
|
|
|
std::vector<QPixmap> next_pixmaps;
|
|
next_pixmaps.reserve(static_cast<std::size_t>(state->background.image_paths.size()));
|
|
for (const QString &path : state->background.image_paths) {
|
|
QPixmap pixmap(path);
|
|
if (!pixmap.isNull())
|
|
next_pixmaps.push_back(std::move(pixmap));
|
|
}
|
|
|
|
if (next_pixmaps.empty()) {
|
|
clear_theme_background_presentation();
|
|
return;
|
|
}
|
|
|
|
if (m_theme_background_cycle_timer)
|
|
m_theme_background_cycle_timer->stop();
|
|
if (m_theme_background_fade_animation)
|
|
m_theme_background_fade_animation->stop();
|
|
|
|
m_theme_background_original_pixmaps = std::move(next_pixmaps);
|
|
m_theme_background_scaled_pixmaps.clear();
|
|
m_theme_background_scaled_size = {};
|
|
m_theme_background_cycle_interval_ms = state->background.interval_ms;
|
|
m_theme_background_current_index = 0;
|
|
m_theme_background_next_index = -1;
|
|
m_theme_background_fade_progress = 0.0;
|
|
rescale_theme_background_pixmaps();
|
|
|
|
if (m_theme_background_cycle_timer && state->background.animated && m_theme_background_scaled_pixmaps.size() > 1)
|
|
m_theme_background_cycle_timer->start(m_theme_background_cycle_interval_ms);
|
|
update();
|
|
}
|
|
|
|
void MainWindow::clear_theme_background_presentation() {
|
|
if (m_theme_background_cycle_timer)
|
|
m_theme_background_cycle_timer->stop();
|
|
if (m_theme_background_fade_animation)
|
|
m_theme_background_fade_animation->stop();
|
|
|
|
m_theme_background_original_pixmaps.clear();
|
|
m_theme_background_scaled_pixmaps.clear();
|
|
m_theme_background_scaled_size = {};
|
|
m_theme_background_current_index = 0;
|
|
m_theme_background_next_index = -1;
|
|
m_theme_background_fade_progress = 0.0;
|
|
update();
|
|
}
|
|
|
|
void MainWindow::rescale_theme_background_pixmaps() {
|
|
m_theme_background_scaled_pixmaps.clear();
|
|
if (m_theme_background_original_pixmaps.empty() || size().isEmpty()) {
|
|
m_theme_background_scaled_size = {};
|
|
return;
|
|
}
|
|
|
|
m_theme_background_scaled_size = size();
|
|
m_theme_background_scaled_pixmaps.reserve(m_theme_background_original_pixmaps.size());
|
|
for (const QPixmap &pixmap : m_theme_background_original_pixmaps) {
|
|
m_theme_background_scaled_pixmaps.push_back(pixmap.scaled(
|
|
m_theme_background_scaled_size,
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation));
|
|
}
|
|
}
|
|
|
|
void MainWindow::advance_theme_background_presentation() {
|
|
if (!m_theme_background_fade_animation || m_theme_background_scaled_pixmaps.size() < 2)
|
|
return;
|
|
|
|
if (!isVisible() || isMinimized()) {
|
|
if (m_theme_background_cycle_timer)
|
|
m_theme_background_cycle_timer->start(1000);
|
|
return;
|
|
}
|
|
|
|
if (m_theme_background_fade_animation->state() == QAbstractAnimation::Running)
|
|
return;
|
|
|
|
m_theme_background_next_index = (m_theme_background_current_index + 1)
|
|
% static_cast<int>(m_theme_background_scaled_pixmaps.size());
|
|
m_theme_background_fade_progress = 0.0;
|
|
m_theme_background_fade_animation->start();
|
|
}
|
|
|
|
void MainWindow::repaint_toolbar_icons() {
|
|
auto *tb = m_ui->toolBar;
|
|
const QColor fg = gui::utils::toolbar_icon_tint();
|
|
|
|
std::map<QIcon::Mode, QColor> new_colors;
|
|
new_colors[QIcon::Normal] = fg;
|
|
new_colors[QIcon::Disabled] = gui::utils::muted_text_color();
|
|
new_colors[QIcon::Active] = fg;
|
|
new_colors[QIcon::Selected] = fg;
|
|
|
|
const auto icon = [&new_colors](const QString &path) {
|
|
return gui::utils::get_colorized_icon(QIcon(path), Qt::black, new_colors);
|
|
};
|
|
|
|
m_icon_play = icon(QStringLiteral(":/icons/play.png"));
|
|
m_icon_pause = icon(QStringLiteral(":/icons/pause.png"));
|
|
m_icon_stop = icon(QStringLiteral(":/icons/stop.png"));
|
|
m_icon_fullscreen_on = icon(QStringLiteral(":/icons/fullscreen.png"));
|
|
m_icon_fullscreen_off = icon(QStringLiteral(":/icons/exit_fullscreen.png"));
|
|
|
|
m_ui->toolbar_open->setIcon(icon(QStringLiteral(":/icons/open.png")));
|
|
m_ui->toolbar_refresh->setIcon(icon(QStringLiteral(":/icons/refresh.png")));
|
|
m_ui->toolbar_fullscreen->setIcon(m_fullscreen ? m_icon_fullscreen_off : m_icon_fullscreen_on);
|
|
m_ui->toolbar_stop->setIcon(m_icon_stop);
|
|
m_ui->toolbar_config->setIcon(icon(QStringLiteral(":/icons/configure.png")));
|
|
m_ui->toolbar_controls->setIcon(icon(QStringLiteral(":/icons/controllers.png")));
|
|
|
|
const bool running = m_game_window != nullptr;
|
|
const bool paused = running && emuenv.kernel.is_threads_paused();
|
|
if (running && !paused)
|
|
m_ui->toolbar_start->setIcon(m_icon_pause);
|
|
else
|
|
m_ui->toolbar_start->setIcon(m_icon_play);
|
|
|
|
const int tool_bar_height = tb->sizeHint().height();
|
|
|
|
for (const auto &act : tb->actions()) {
|
|
if (act->isSeparator())
|
|
continue;
|
|
if (auto *w = tb->widgetForAction(act))
|
|
w->setMinimumWidth(tool_bar_height);
|
|
}
|
|
|
|
if (m_slider_container)
|
|
m_slider_container->setFixedWidth(tool_bar_height * 4);
|
|
if (m_search_bar)
|
|
m_search_bar->setFixedWidth(tool_bar_height * 5);
|
|
}
|
|
|
|
void MainWindow::on_toolbar_refresh() {
|
|
m_apps_list_widget->refresh(true);
|
|
}
|
|
|
|
void MainWindow::on_toolbar_fullscreen() {
|
|
if (m_fullscreen) {
|
|
if (m_game_container)
|
|
m_game_container->showNormal();
|
|
else
|
|
showNormal();
|
|
m_fullscreen = false;
|
|
m_ui->toolbar_fullscreen->setIcon(m_icon_fullscreen_on);
|
|
m_ui->toolbar_fullscreen->setText(tr("Fullscreen"));
|
|
} else {
|
|
if (m_game_container)
|
|
m_game_container->showFullScreen();
|
|
else
|
|
showFullScreen();
|
|
m_fullscreen = true;
|
|
m_ui->toolbar_fullscreen->setIcon(m_icon_fullscreen_off);
|
|
m_ui->toolbar_fullscreen->setText(tr("Exit Fullscreen"));
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_toolbar_start() {
|
|
if (m_game_window) {
|
|
on_pause_triggered();
|
|
} else {
|
|
const app::AppEntry *app = m_apps_list_widget->selected_app();
|
|
if (app && !app->title_id.empty()) {
|
|
if (emuenv.cfg.show_live_area_screen && !m_live_area_widget)
|
|
show_live_area(app->title_id);
|
|
else
|
|
boot_game(app->title_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::on_app_selection_changed(const app::AppEntry *app) {
|
|
m_app_selected = app && !app->title_id.empty();
|
|
refresh_emulation_actions();
|
|
}
|
|
|
|
void MainWindow::on_context_menu_requested(const QPoint &global_pos, const std::vector<const app::AppEntry *> &apps) {
|
|
AppsListContextMenu menu(emuenv, apps, m_game_compat, m_game_window != nullptr, this);
|
|
|
|
connect(&menu, &AppsListContextMenu::boot_requested,
|
|
this, [this](const app::AppEntry &app) {
|
|
boot_game(app.title_id);
|
|
});
|
|
connect(&menu, &AppsListContextMenu::view_live_area_requested,
|
|
this, [this](const app::AppEntry &app) {
|
|
show_live_area(app.title_id);
|
|
});
|
|
connect(&menu, &AppsListContextMenu::open_settings_requested,
|
|
this, [this](const std::string &app_path, int tab) {
|
|
auto *dlg = new SettingsDialog(emuenv, m_gui_settings, *m_theme_manager, app_path, tab);
|
|
register_auxiliary_window(dlg);
|
|
connect(dlg, &SettingsDialog::gui_stylesheet_request, this, &MainWindow::apply_stylesheet);
|
|
connect(dlg, &SettingsDialog::gui_theme_music_settings_request, this, [this] {
|
|
if (m_theme_manager)
|
|
m_theme_manager->refresh_vita_theme_bgm();
|
|
});
|
|
connect(dlg, &SettingsDialog::gui_log_settings_request, this, &MainWindow::apply_log_gui_settings);
|
|
connect(dlg, &SettingsDialog::ui_language_request, this, &MainWindow::apply_ui_language);
|
|
connect(dlg, &SettingsDialog::restart_game_requested, this, &MainWindow::restart_running_app);
|
|
connect(dlg, &QDialog::finished, this, [this, dlg, app_path](int) {
|
|
if (!app_path.empty() && !m_game_window)
|
|
app::set_current_config(emuenv, "");
|
|
refresh_status_bar();
|
|
m_apps_list_widget->refresh();
|
|
apply_log_gui_settings();
|
|
});
|
|
present_tool_window(dlg);
|
|
});
|
|
connect(&menu, &AppsListContextMenu::refresh_requested,
|
|
this, [this] { m_apps_list_widget->refresh(); });
|
|
|
|
menu.exec(global_pos);
|
|
}
|
|
|
|
void MainWindow::resize_icons(int index) {
|
|
if (m_icon_size_slider->value() != index) {
|
|
m_icon_size_slider->setSliderPosition(index);
|
|
return;
|
|
}
|
|
m_apps_list_widget->resize_icons(index);
|
|
}
|
|
|
|
void MainWindow::setup_status_bar() {
|
|
auto *sb = statusBar();
|
|
sb->setObjectName(QStringLiteral("emulator_status_bar"));
|
|
|
|
m_renderer_button = new QPushButton(this);
|
|
m_renderer_button->setObjectName(QStringLiteral("status_button"));
|
|
m_renderer_button->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_renderer_button, &QPushButton::clicked, this, [this] {
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
auto &cc = desired.current_config;
|
|
cc.backend_renderer = (cc.backend_renderer == "Vulkan") ? "OpenGL" : "Vulkan";
|
|
desired.backend_renderer = cc.backend_renderer;
|
|
save_config(desired);
|
|
update_renderer_button();
|
|
update_accuracy_button();
|
|
update_screen_filter_button();
|
|
});
|
|
connect(m_renderer_button, &QPushButton::customContextMenuRequested, this, [this](const QPoint &pos) {
|
|
QMenu menu(this);
|
|
auto *vulkan_action = menu.addAction(QStringLiteral("Vulkan"));
|
|
auto *opengl_action = menu.addAction(QStringLiteral("OpenGL"));
|
|
QAction *chosen = menu.exec(m_renderer_button->mapToGlobal(pos));
|
|
if (!chosen)
|
|
return;
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
auto &cc = desired.current_config;
|
|
if (chosen == vulkan_action) {
|
|
cc.backend_renderer = "Vulkan";
|
|
} else if (chosen == opengl_action) {
|
|
cc.backend_renderer = "OpenGL";
|
|
}
|
|
desired.backend_renderer = cc.backend_renderer;
|
|
save_config(desired);
|
|
update_renderer_button();
|
|
update_accuracy_button();
|
|
update_screen_filter_button();
|
|
});
|
|
sb->addWidget(m_renderer_button);
|
|
|
|
m_accuracy_button = new QPushButton(this);
|
|
m_accuracy_button->setObjectName(QStringLiteral("status_button"));
|
|
m_accuracy_button->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_accuracy_button, &QPushButton::clicked, this, [this] {
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
auto &cc = desired.current_config;
|
|
cc.high_accuracy = !cc.high_accuracy;
|
|
save_config(desired);
|
|
update_accuracy_button();
|
|
});
|
|
connect(m_accuracy_button, &QPushButton::customContextMenuRequested, this, [this](const QPoint &pos) {
|
|
QMenu menu(this);
|
|
auto *standard_action = menu.addAction(tr("Standard"));
|
|
auto *high_action = menu.addAction(tr("High"));
|
|
QAction *chosen = menu.exec(m_accuracy_button->mapToGlobal(pos));
|
|
if (!chosen)
|
|
return;
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
auto &cc = desired.current_config;
|
|
cc.high_accuracy = (chosen == high_action);
|
|
save_config(desired);
|
|
update_accuracy_button();
|
|
});
|
|
sb->addWidget(m_accuracy_button);
|
|
|
|
auto get_filter_names = [this]() -> QStringList {
|
|
if (emuenv.cfg.current_config.backend_renderer == "Vulkan")
|
|
return { QStringLiteral("Nearest"), QStringLiteral("Bilinear"),
|
|
QStringLiteral("Bicubic"), QStringLiteral("FXAA"), QStringLiteral("FSR") };
|
|
else
|
|
return { QStringLiteral("Bilinear"), QStringLiteral("FXAA") };
|
|
};
|
|
|
|
auto apply_screen_filter = [this](const std::string &filter) {
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
desired.current_config.screen_filter = filter;
|
|
save_config(desired);
|
|
update_screen_filter_button();
|
|
};
|
|
|
|
m_screen_filter_button = new QPushButton(this);
|
|
m_screen_filter_button->setObjectName(QStringLiteral("status_button"));
|
|
m_screen_filter_button->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_screen_filter_button, &QPushButton::clicked, this, [this, get_filter_names, apply_screen_filter] {
|
|
const QStringList filters = get_filter_names();
|
|
const QString current = QString::fromStdString(emuenv.cfg.current_config.screen_filter);
|
|
int idx = filters.indexOf(current);
|
|
idx = (idx + 1) % filters.size();
|
|
apply_screen_filter(filters[idx].toStdString());
|
|
});
|
|
connect(m_screen_filter_button, &QPushButton::customContextMenuRequested, this, [this, get_filter_names, apply_screen_filter](const QPoint &pos) {
|
|
const QStringList filters = get_filter_names();
|
|
QMenu menu(this);
|
|
for (const auto &name : filters)
|
|
menu.addAction(name);
|
|
QAction *chosen = menu.exec(m_screen_filter_button->mapToGlobal(pos));
|
|
if (!chosen)
|
|
return;
|
|
apply_screen_filter(chosen->text().toStdString());
|
|
});
|
|
sb->addWidget(m_screen_filter_button);
|
|
|
|
m_audio_backend_button = new QPushButton(this);
|
|
m_audio_backend_button->setObjectName(QStringLiteral("status_button"));
|
|
m_audio_backend_button->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_audio_backend_button, &QPushButton::clicked, this, [this] {
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
auto &cc = desired.current_config;
|
|
cc.audio_backend = (cc.audio_backend == "SDL") ? "Cubeb" : "SDL";
|
|
save_config(desired);
|
|
update_audio_backend_button();
|
|
});
|
|
connect(m_audio_backend_button, &QPushButton::customContextMenuRequested, this, [this](const QPoint &pos) {
|
|
QMenu menu(this);
|
|
auto *sdl_action = menu.addAction(QStringLiteral("SDL"));
|
|
auto *cubeb_action = menu.addAction(QStringLiteral("Cubeb"));
|
|
QAction *chosen = menu.exec(m_audio_backend_button->mapToGlobal(pos));
|
|
if (!chosen)
|
|
return;
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
auto &cc = desired.current_config;
|
|
cc.audio_backend = (chosen == cubeb_action) ? "Cubeb" : "SDL";
|
|
save_config(desired);
|
|
update_audio_backend_button();
|
|
});
|
|
sb->addWidget(m_audio_backend_button);
|
|
|
|
m_ngs_button = new QPushButton(this);
|
|
m_ngs_button->setObjectName(QStringLiteral("status_button"));
|
|
m_ngs_button->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_ngs_button, &QPushButton::clicked, this, [this] {
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
auto &cc = desired.current_config;
|
|
cc.ngs_enable = !cc.ngs_enable;
|
|
save_config(desired);
|
|
update_ngs_button();
|
|
});
|
|
connect(m_ngs_button, &QPushButton::customContextMenuRequested, this, [this](const QPoint &pos) {
|
|
QMenu menu(this);
|
|
auto *on_action = menu.addAction(tr("NGS: ON"));
|
|
auto *off_action = menu.addAction(tr("NGS: OFF"));
|
|
QAction *chosen = menu.exec(m_ngs_button->mapToGlobal(pos));
|
|
if (!chosen)
|
|
return;
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
desired.current_config.ngs_enable = (chosen == on_action);
|
|
save_config(desired);
|
|
update_ngs_button();
|
|
});
|
|
sb->addWidget(m_ngs_button);
|
|
|
|
m_volume_button = new QPushButton(this);
|
|
m_volume_button->setObjectName(QStringLiteral("status_button"));
|
|
m_volume_button->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_volume_button, &QPushButton::clicked, this, [this] {
|
|
QMenu menu(this);
|
|
auto *container = new QWidget(&menu);
|
|
auto *layout = new QHBoxLayout(container);
|
|
layout->setContentsMargins(12, 8, 12, 8);
|
|
|
|
auto *slider = new QSlider(Qt::Horizontal, container);
|
|
slider->setRange(0, 100);
|
|
slider->setValue(emuenv.cfg.current_config.audio_volume);
|
|
slider->setMinimumWidth(180);
|
|
|
|
layout->addWidget(slider);
|
|
connect(slider, &QSlider::valueChanged, this, [this](int val) {
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
desired.current_config.audio_volume = val;
|
|
save_config(desired);
|
|
update_volume_button();
|
|
});
|
|
auto *action = new QWidgetAction(&menu);
|
|
action->setDefaultWidget(container);
|
|
menu.addAction(action);
|
|
menu.exec(m_volume_button->mapToGlobal(QPoint(0, -menu.sizeHint().height())));
|
|
});
|
|
connect(m_volume_button, &QPushButton::customContextMenuRequested, this, [this](const QPoint &pos) {
|
|
QMenu menu(this);
|
|
auto *mute_action = menu.addAction(tr("Mute"));
|
|
auto *reset_action = menu.addAction(tr("Reset to 100%"));
|
|
QAction *chosen = menu.exec(m_volume_button->mapToGlobal(pos));
|
|
if (!chosen)
|
|
return;
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
if (chosen == mute_action) {
|
|
desired.current_config.audio_volume = 0;
|
|
} else if (chosen == reset_action) {
|
|
desired.current_config.audio_volume = 100;
|
|
}
|
|
save_config(desired);
|
|
update_volume_button();
|
|
});
|
|
sb->addWidget(m_volume_button);
|
|
|
|
m_update_available_button = new QPushButton(this);
|
|
m_update_available_button->setObjectName(QStringLiteral("status_button"));
|
|
m_update_available_button->setVisible(false);
|
|
connect(m_update_available_button, &QPushButton::clicked, this, &MainWindow::on_check_for_updates_action_triggered);
|
|
sb->addPermanentWidget(m_update_available_button);
|
|
|
|
update_renderer_button();
|
|
update_accuracy_button();
|
|
update_screen_filter_button();
|
|
update_audio_backend_button();
|
|
update_ngs_button();
|
|
update_volume_button();
|
|
update_update_available_button();
|
|
}
|
|
|
|
void MainWindow::update_renderer_button() {
|
|
const auto &renderer = emuenv.cfg.current_config.backend_renderer;
|
|
if (renderer == "Vulkan") {
|
|
m_renderer_button->setText(QStringLiteral("VULKAN"));
|
|
update_status_button_accent(m_renderer_button, QStringLiteral("renderer_vulkan"));
|
|
} else {
|
|
m_renderer_button->setText(QStringLiteral("OPENGL"));
|
|
update_status_button_accent(m_renderer_button, QStringLiteral("renderer_opengl"));
|
|
}
|
|
}
|
|
|
|
void MainWindow::update_accuracy_button() {
|
|
const bool high = emuenv.cfg.current_config.high_accuracy;
|
|
const bool is_vulkan = emuenv.cfg.current_config.backend_renderer == "Vulkan";
|
|
m_accuracy_button->setVisible(is_vulkan);
|
|
if (high) {
|
|
m_accuracy_button->setText(tr("HIGH"));
|
|
update_status_button_accent(m_accuracy_button, QStringLiteral("high"));
|
|
} else {
|
|
m_accuracy_button->setText(tr("STANDARD"));
|
|
update_status_button_accent(m_accuracy_button, QStringLiteral("standard"));
|
|
}
|
|
}
|
|
|
|
void MainWindow::update_screen_filter_button() {
|
|
auto &cc = emuenv.cfg.current_config;
|
|
const QStringList valid = (cc.backend_renderer == "Vulkan")
|
|
? QStringList{ QStringLiteral("Nearest"), QStringLiteral("Bilinear"),
|
|
QStringLiteral("Bicubic"), QStringLiteral("FXAA"), QStringLiteral("FSR") }
|
|
: QStringList{ QStringLiteral("Bilinear"), QStringLiteral("FXAA") };
|
|
|
|
const QString current = QString::fromStdString(cc.screen_filter);
|
|
if (!valid.contains(current)) {
|
|
Config desired;
|
|
copy_config_for_edit(desired, emuenv.cfg);
|
|
desired.current_config.screen_filter = valid.first().toStdString();
|
|
save_config(desired);
|
|
}
|
|
|
|
m_screen_filter_button->setText(QString::fromStdString(cc.screen_filter).toUpper());
|
|
update_status_button_accent(m_screen_filter_button, QStringLiteral("neutral"));
|
|
}
|
|
|
|
void MainWindow::update_audio_backend_button() {
|
|
const auto &backend = emuenv.cfg.current_config.audio_backend;
|
|
if (backend == "SDL") {
|
|
m_audio_backend_button->setText(QStringLiteral("SDL"));
|
|
} else {
|
|
m_audio_backend_button->setText(QStringLiteral("CUBEB"));
|
|
}
|
|
update_status_button_accent(m_audio_backend_button, QStringLiteral("audio"));
|
|
}
|
|
|
|
void MainWindow::update_ngs_button() {
|
|
const bool ngs = emuenv.cfg.current_config.ngs_enable;
|
|
if (ngs) {
|
|
m_ngs_button->setText(tr("NGS: ON"));
|
|
update_status_button_accent(m_ngs_button, QStringLiteral("neutral"));
|
|
} else {
|
|
m_ngs_button->setText(tr("NGS: OFF"));
|
|
update_status_button_accent(m_ngs_button, QStringLiteral("muted"));
|
|
}
|
|
}
|
|
|
|
void MainWindow::update_volume_button() {
|
|
const int vol = emuenv.cfg.current_config.audio_volume;
|
|
|
|
if (vol == 0) {
|
|
m_volume_button->setText(tr("VOLUME: MUTED"));
|
|
update_status_button_accent(m_volume_button, QStringLiteral("muted"));
|
|
} else {
|
|
m_volume_button->setText(tr("VOLUME: %1%").arg(vol));
|
|
update_status_button_accent(m_volume_button, QStringLiteral("neutral"));
|
|
}
|
|
}
|
|
|
|
void MainWindow::update_update_available_button() {
|
|
if (!m_update_available_button)
|
|
return;
|
|
|
|
m_update_available_button->setText(tr("UPDATE AVAILABLE"));
|
|
update_status_button_accent(m_update_available_button, QStringLiteral("update"));
|
|
m_update_available_button->setVisible(m_update_available);
|
|
m_update_available_button->setToolTip(tr("Click to review the latest Vita3K release information."));
|
|
}
|
|
|
|
void MainWindow::refresh_status_bar() {
|
|
const bool running = m_game_window != nullptr;
|
|
|
|
update_renderer_button();
|
|
update_accuracy_button();
|
|
update_screen_filter_button();
|
|
update_audio_backend_button();
|
|
update_ngs_button();
|
|
update_volume_button();
|
|
|
|
m_renderer_button->setEnabled(!running);
|
|
m_accuracy_button->setEnabled(!running);
|
|
m_audio_backend_button->setEnabled(!running);
|
|
m_ngs_button->setEnabled(!running);
|
|
update_update_available_button();
|
|
}
|
|
|
|
void MainWindow::save_config() {
|
|
save_config(emuenv.cfg);
|
|
}
|
|
|
|
void MainWindow::save_config(const Config &desired_cfg) {
|
|
const std::string scope_app_path = (m_game_window && config::has_custom_config(emuenv.config_path, emuenv.io.app_path))
|
|
? emuenv.io.app_path
|
|
: std::string();
|
|
const auto result = app::commit_settings(emuenv, desired_cfg, scope_app_path);
|
|
prompt_restart_if_needed(result);
|
|
}
|
|
|
|
void MainWindow::prompt_restart_if_needed(const app::SettingsCommitResult &result) {
|
|
if (prompt_restart_required_dialog(this, result.restart_required_settings))
|
|
restart_running_app();
|
|
}
|
|
|
|
void MainWindow::init_discord() {
|
|
#if USE_DISCORD
|
|
m_discord_rich_presence_old = emuenv.cfg.discord_rich_presence;
|
|
#endif
|
|
}
|
|
|
|
void MainWindow::sync_discord_presence() {
|
|
#if USE_DISCORD
|
|
if (!emuenv.cfg.discord_rich_presence) {
|
|
if (m_discord_rich_presence_old)
|
|
discordrpc::clear_presence();
|
|
m_discord_rich_presence_old = false;
|
|
return;
|
|
}
|
|
|
|
if (!discordrpc::init()) {
|
|
m_discord_rich_presence_old = false;
|
|
return;
|
|
}
|
|
|
|
if (m_game_window)
|
|
discordrpc::update_presence(emuenv.io.title_id, emuenv.current_app_title);
|
|
else
|
|
discordrpc::update_presence();
|
|
|
|
m_discord_rich_presence_old = true;
|
|
#endif
|
|
}
|
|
|
|
void MainWindow::shutdown_discord() {
|
|
#if USE_DISCORD
|
|
if (discordrpc::is_running())
|
|
discordrpc::shutdown();
|
|
m_discord_rich_presence_old = false;
|
|
#endif
|
|
}
|