diff --git a/src/citron/bootmanager.cpp b/src/citron/bootmanager.cpp index 71dee56be..c04f143aa 100644 --- a/src/citron/bootmanager.cpp +++ b/src/citron/bootmanager.cpp @@ -284,13 +284,14 @@ struct NullRenderWidget : public RenderWidget { GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_, std::shared_ptr input_subsystem_, - Core::System& system_) - : QWidget(parent), - emu_thread(emu_thread_), input_subsystem{std::move(input_subsystem_)}, system{system_} { + Core::System& system_, HotkeyRegistry& hotkey_registry_) +: QWidget(parent), +emu_thread(emu_thread_), input_subsystem{std::move(input_subsystem_)}, system{system_}, +hotkey_registry{hotkey_registry_} { setWindowTitle(QStringLiteral("citron %1 | %2-%3") - .arg(QString::fromUtf8(Common::g_build_name), - QString::fromUtf8(Common::g_scm_branch), - QString::fromUtf8(Common::g_scm_desc))); + .arg(QString::fromUtf8(Common::g_build_name), + QString::fromUtf8(Common::g_scm_branch), + QString::fromUtf8(Common::g_scm_desc))); setAttribute(Qt::WA_AcceptTouchEvents); auto* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -299,7 +300,7 @@ GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_, this->setMouseTracking(true); strict_context_required = QGuiApplication::platformName() == QStringLiteral("wayland") || - QGuiApplication::platformName() == QStringLiteral("wayland-egl"); + QGuiApplication::platformName() == QStringLiteral("wayland-egl"); connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete); connect(this, &GRenderWindow::ExecuteProgramSignal, parent, &GMainWindow::OnExecuteProgram, @@ -310,11 +311,13 @@ 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); + connect(this, &GRenderWindow::PanningToggleHotkeyPressed, this, [this]() { + SetMousePanningState(!Settings::values.mouse_panning.GetValue()); + }); } void GRenderWindow::ExecuteProgram(std::size_t program_index) { @@ -599,22 +602,31 @@ int GRenderWindow::QtModifierToSwitchModifier(Qt::KeyboardModifiers qt_modifiers } void GRenderWindow::keyPressEvent(QKeyEvent* event) { - /** - * This feature can be enhanced with the following functions, but they do not provide - * cross-platform behavior. - * - * event->nativeVirtualKey() can distinguish between keys on the numpad. - * event->nativeModifiers() can distinguish between left and right keys and numlock, - * capslock, scroll lock. - */ - if (!event->isAutoRepeat()) { - const auto modifier = QtModifierToSwitchModifier(event->modifiers()); - const auto key = QtKeyToSwitchKey(Qt::Key(event->key())); - input_subsystem->GetKeyboard()->SetKeyboardModifiers(modifier); - input_subsystem->GetKeyboard()->PressKeyboardKey(key); - // This is used for gamepads that can have any key mapped - input_subsystem->GetKeyboard()->PressKey(event->key()); + if (event->isAutoRepeat()) { + return; // Ignore auto-repeated key presses } + + const QKeySequence key_sequence(event->modifiers() | event->key()); + static const std::string main_window_id = "Main Window"; + + if (key_sequence == hotkey_registry.GetKeySequence(main_window_id, "Toggle Mouse Panning")) { + emit PanningToggleHotkeyPressed(); // Signal the main window + event->accept(); // Consume the event + return; // Stop processing + } + + if (key_sequence == hotkey_registry.GetKeySequence(main_window_id, "Exit Fullscreen")) { + emit FullscreenExitHotkeyPressed(); // Signal the main window + event->accept(); // Consume the event + return; // Stop processing + } + + // --- If not a critical hotkey, pass to game as normal --- + const auto modifier = QtModifierToSwitchModifier(event->modifiers()); + const auto key = QtKeyToSwitchKey(static_cast(event->key())); + input_subsystem->GetKeyboard()->SetKeyboardModifiers(modifier); + input_subsystem->GetKeyboard()->PressKeyboardKey(key); + input_subsystem->GetKeyboard()->PressKey(event->key()); } void GRenderWindow::keyReleaseEvent(QKeyEvent* event) { @@ -1173,15 +1185,19 @@ void GRenderWindow::showEvent(QShowEvent* event) { } bool GRenderWindow::eventFilter(QObject* object, QEvent* event) { + // Only handle HoverMove for panning. if (event->type() == QEvent::HoverMove) { - if (Settings::values.mouse_panning || Settings::values.mouse_enabled) { + if (Settings::values.mouse_panning.GetValue() || Settings::values.mouse_enabled.GetValue()) { auto* hover_event = static_cast(event); mouseMoveEvent(hover_event); - return false; + // Consume the hover event to prevent it from being processed twice. + return true; } emit MouseActivity(); } - return false; + + // Pass on all other events for default processing. + return QWidget::eventFilter(object, event); } void GRenderWindow::HideMouseCursor() { @@ -1189,3 +1205,15 @@ void GRenderWindow::HideMouseCursor() { QApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); } } + +void GRenderWindow::SetMousePanningState(bool enabled) { + Settings::values.mouse_panning = enabled; + if (enabled) { + installEventFilter(this); + setAttribute(Qt::WA_Hover, true); + } else { + QApplication::restoreOverrideCursor(); + removeEventFilter(this); + setAttribute(Qt::WA_Hover, false); + } +} diff --git a/src/citron/bootmanager.h b/src/citron/bootmanager.h index 78b255e6b..dcdbbfe43 100644 --- a/src/citron/bootmanager.h +++ b/src/citron/bootmanager.h @@ -30,8 +30,10 @@ #include "common/polyfill_thread.h" #include "common/thread.h" #include "core/frontend/emu_window.h" +#include "citron/hotkeys.h" class GMainWindow; +class HotkeyRegistry; class QCamera; class QCameraImageCapture; class QCloseEvent; @@ -150,7 +152,7 @@ class GRenderWindow : public QWidget, public Core::Frontend::EmuWindow { public: explicit GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_, std::shared_ptr input_subsystem_, - Core::System& system_); + Core::System& system_, HotkeyRegistry& hotkey_registry_); ~GRenderWindow() override; // EmuWindow implementation. @@ -197,6 +199,7 @@ public: void focusOutEvent(QFocusEvent* event) override; bool InitRenderTarget(); + void SetMousePanningState(bool enabled); // <<< ADDED /// Destroy the previous run's child_widget which should also destroy the child_window void ReleaseRenderTarget(); @@ -227,6 +230,8 @@ signals: void ExitSignal(); void MouseActivity(); void TasPlaybackStateChanged(); + void PanningToggleHotkeyPressed(); + void FullscreenExitHotkeyPressed(); private slots: void HideMouseCursor(); @@ -265,6 +270,7 @@ private: bool first_frame = false; InputCommon::TasInput::TasState last_tas_state; + bool strict_context_required; #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) && CITRON_USE_QT_MULTIMEDIA bool is_virtual_camera; @@ -279,6 +285,7 @@ private: QTimer mouse_hide_timer; Core::System& system; + HotkeyRegistry& hotkey_registry; protected: void showEvent(QShowEvent* event) override; diff --git a/src/citron/configuration/configure_mouse_panning.cpp b/src/citron/configuration/configure_mouse_panning.cpp index 83636cf4a..0a37ea0e5 100644 --- a/src/citron/configuration/configure_mouse_panning.cpp +++ b/src/citron/configuration/configure_mouse_panning.cpp @@ -1,9 +1,12 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include +#include "citron/main.h" +#include "citron/bootmanager.h" #include "common/settings.h" #include "ui_configure_mouse_panning.h" #include "citron/configuration/configure_mouse_panning.h" @@ -69,7 +72,14 @@ void ConfigureMousePanning::ConnectEvents() { } void ConfigureMousePanning::ApplyConfiguration() { - Settings::values.mouse_panning = ui->enable->isChecked(); + GMainWindow* main_window = qobject_cast(this->parent()); + + if (main_window) { + main_window->GetRenderWindow()->SetMousePanningState(ui->enable->isChecked()); + } else { + Settings::values.mouse_panning = ui->enable->isChecked(); + } + Settings::values.mouse_panning_x_sensitivity = static_cast(ui->x_sensitivity->value()); Settings::values.mouse_panning_y_sensitivity = static_cast(ui->y_sensitivity->value()); Settings::values.mouse_panning_deadzone_counterweight = @@ -78,7 +88,11 @@ void ConfigureMousePanning::ApplyConfiguration() { Settings::values.mouse_panning_min_decay = static_cast(ui->min_decay->value()); if (Settings::values.mouse_enabled && Settings::values.mouse_panning) { - Settings::values.mouse_panning = false; + if (main_window) { + main_window->GetRenderWindow()->SetMousePanningState(false); + } else { + Settings::values.mouse_panning = false; + } QMessageBox::critical( this, tr("Emulated mouse is enabled"), tr("Real mouse input and mouse panning are incompatible. Please disable the " diff --git a/src/citron/main.cpp b/src/citron/main.cpp index ed3f1c53a..66012394c 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -1079,7 +1079,7 @@ void GMainWindow::InitializeWidgets() { #ifdef CITRON_ENABLE_COMPATIBILITY_REPORTING ui->action_Report_Compatibility->setVisible(true); #endif - render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system); + render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system, hotkey_registry); render_window->hide(); game_list = new GameList(vfs, provider.get(), *play_time_manager, *system, this); @@ -1106,7 +1106,6 @@ void GMainWindow::InitializeWidgets() { // Create status bar message_label = new QLabel(); - // Configured separately for left alignment message_label->setFrameStyle(QFrame::NoFrame); message_label->setContentsMargins(4, 0, 4, 0); message_label->setAlignment(Qt::AlignLeft); @@ -1145,7 +1144,6 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); - // Create performance overlay performance_overlay = new PerformanceOverlay(this); performance_overlay->hide(); @@ -1215,7 +1213,6 @@ void GMainWindow::InitializeWidgets() { statusBar()->insertPermanentWidget(0, volume_button); - // setup AA button aa_status_button = new QPushButton(); aa_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); aa_status_button->setFocusPolicy(Qt::NoFocus); @@ -1247,7 +1244,6 @@ void GMainWindow::InitializeWidgets() { }); statusBar()->insertPermanentWidget(0, aa_status_button); - // Setup Filter button filter_status_button = new QPushButton(); filter_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); filter_status_button->setFocusPolicy(Qt::NoFocus); @@ -1271,7 +1267,6 @@ void GMainWindow::InitializeWidgets() { }); statusBar()->insertPermanentWidget(0, filter_status_button); - // Setup Dock button dock_status_button = new QPushButton(); dock_status_button->setObjectName(QStringLiteral("DockingStatusBarButton")); dock_status_button->setFocusPolicy(Qt::NoFocus); @@ -1282,7 +1277,6 @@ void GMainWindow::InitializeWidgets() { connect(dock_status_button, &QPushButton::customContextMenuRequested, [this](const QPoint& menu_location) { QMenu context_menu; - for (auto const& pair : ConfigurationShared::use_docked_mode_texts_map) { context_menu.addAction(pair.second, [this, &pair] { if (pair.first != Settings::values.use_docked_mode.GetValue()) { @@ -1295,7 +1289,6 @@ void GMainWindow::InitializeWidgets() { }); statusBar()->insertPermanentWidget(0, dock_status_button); - // Setup GPU Accuracy button gpu_accuracy_button = new QPushButton(); gpu_accuracy_button->setObjectName(QStringLiteral("GPUStatusBarButton")); gpu_accuracy_button->setCheckable(true); @@ -1306,7 +1299,6 @@ void GMainWindow::InitializeWidgets() { connect(gpu_accuracy_button, &QPushButton::customContextMenuRequested, [this](const QPoint& menu_location) { QMenu context_menu; - for (auto const& gpu_accuracy_pair : ConfigurationShared::gpu_accuracy_texts_map) { if (gpu_accuracy_pair.first == Settings::GpuAccuracy::Extreme) { continue; @@ -1321,7 +1313,6 @@ void GMainWindow::InitializeWidgets() { }); statusBar()->insertPermanentWidget(0, gpu_accuracy_button); - // Setup Renderer API button renderer_status_button = new QPushButton(); renderer_status_button->setObjectName(QStringLiteral("RendererStatusBarButton")); renderer_status_button->setCheckable(true); @@ -1335,7 +1326,6 @@ void GMainWindow::InitializeWidgets() { connect(renderer_status_button, &QPushButton::customContextMenuRequested, [this](const QPoint& menu_location) { QMenu context_menu; - for (auto const& renderer_backend_pair : ConfigurationShared::renderer_backend_texts_map) { if (renderer_backend_pair.first == Settings::RendererBackend::Null) { @@ -1428,6 +1418,7 @@ void GMainWindow::LinkActionShortcut(QAction* action, const QString& action_name void GMainWindow::InitializeHotkeys() { hotkey_registry.LoadHotkeys(); + // Link all standard menu actions LinkActionShortcut(ui->action_Load_File, QStringLiteral("Load File")); LinkActionShortcut(ui->action_Load_Amiibo, QStringLiteral("Load/Remove Amiibo")); LinkActionShortcut(ui->action_Exit, QStringLiteral("Exit citron")); @@ -1446,42 +1437,30 @@ void GMainWindow::InitializeHotkeys() { LinkActionShortcut(ui->action_TAS_Start, QStringLiteral("TAS Start/Stop"), true); LinkActionShortcut(ui->action_TAS_Record, QStringLiteral("TAS Record"), true); LinkActionShortcut(ui->action_TAS_Reset, QStringLiteral("TAS Reset"), true); - LinkActionShortcut(ui->action_View_Lobby, - QStringLiteral("Multiplayer Browse Public Game Lobby")); + LinkActionShortcut(ui->action_View_Lobby, QStringLiteral("Multiplayer Browse Public Game Lobby")); LinkActionShortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); - LinkActionShortcut(ui->action_Connect_To_Room, - QStringLiteral("Multiplayer Direct Connect to Room")); + LinkActionShortcut(ui->action_Connect_To_Room, QStringLiteral("Multiplayer Direct Connect to Room")); LinkActionShortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); LinkActionShortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); - // Create and connect a dedicated, robust QAction for exiting fullscreen. - action_exit_fullscreen = new QAction(this); - connect(action_exit_fullscreen, &QAction::triggered, this, [this] { + connect(render_window, &GRenderWindow::FullscreenExitHotkeyPressed, this, [this]() { if (emulation_running && ui->action_Fullscreen->isChecked()) { - // Un-check the toggle to keep the UI in sync and then exit fullscreen. ui->action_Fullscreen->setChecked(false); ToggleFullscreen(); } }); - // Now bind the "Exit Fullscreen" hotkey (Esc by default) to this new QAction. - LinkActionShortcut(action_exit_fullscreen, QStringLiteral("Exit Fullscreen")); - render_window->addAction(action_exit_fullscreen); - - static const QString main_window = QStringLiteral("Main Window"); const auto connect_shortcut = [&](const QString& action_name, const Fn& function) { - const auto* hotkey = - hotkey_registry.GetHotkey(main_window.toStdString(), action_name.toStdString(), this); + static const std::string main_window = "Main Window"; + const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name.toStdString(), this); auto* controller = system->HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1); - const auto* controller_hotkey = hotkey_registry.GetControllerHotkey( - main_window.toStdString(), action_name.toStdString(), controller); + const auto* controller_hotkey = + hotkey_registry.GetControllerHotkey(main_window, action_name.toStdString(), controller); connect(hotkey, &QShortcut::activated, this, function); - connect(controller_hotkey, &ControllerShortcut::Activated, this, function, - Qt::QueuedConnection); + connect(controller_hotkey, &ControllerShortcut::Activated, this, function, Qt::QueuedConnection); }; - connect_shortcut(QStringLiteral("Change Adapting Filter"), - &GMainWindow::OnToggleAdaptingFilter); + connect_shortcut(QStringLiteral("Change Adapting Filter"), &GMainWindow::OnToggleAdaptingFilter); connect_shortcut(QStringLiteral("Change Docked Mode"), &GMainWindow::OnToggleDockedMode); connect_shortcut(QStringLiteral("Change GPU Accuracy"), &GMainWindow::OnToggleGpuAccuracy); connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); @@ -1495,13 +1474,6 @@ void GMainWindow::InitializeHotkeys() { system->GetRenderdocAPI().ToggleCapture(); } }); - connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] { - Settings::values.mouse_panning = !Settings::values.mouse_panning; - if (Settings::values.mouse_panning) { - render_window->installEventFilter(render_window); - render_window->setAttribute(Qt::WA_Hover, true); - } - }); } void GMainWindow::SetDefaultUIGeometry() { @@ -3771,6 +3743,14 @@ void GMainWindow::OnMenuRecentFile() { void GMainWindow::OnStartGame() { PreventOSSleep(); + if (Settings::values.mouse_panning) { + render_window->installEventFilter(render_window); + render_window->setAttribute(Qt::WA_Hover, true); + } else { + render_window->removeEventFilter(render_window); + render_window->setAttribute(Qt::WA_Hover, false); + } + emu_thread->SetRunning(true); UpdateMenuState(); diff --git a/src/citron/main.h b/src/citron/main.h index a3c4a0b7e..b34954c2b 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -114,6 +114,7 @@ public: const std::shared_ptr& GetVFS() const { return vfs; } bool IsEmulationRunning() const { return emulation_running; } void RefreshGameList(); + GRenderWindow* GetRenderWindow() const { return render_window; } bool ExtractZipToDirectoryPublic(const std::filesystem::path& zip_path, const std::filesystem::path& extract_path); signals: void EmulationStarting(EmuThread* emu_thread);