diff --git a/src/citron/CMakeLists.txt b/src/citron/CMakeLists.txt index 2688ada31..01d59b95a 100644 --- a/src/citron/CMakeLists.txt +++ b/src/citron/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(citron applets/qt_web_browser_scripts.h bootmanager.cpp bootmanager.h + controller_overlay.cpp + controller_overlay.h compatdb.ui compatibility_list.cpp compatibility_list.h @@ -229,7 +231,6 @@ add_executable(citron util/vram_overlay.h util/sequence_dialog/sequence_dialog.cpp util/sequence_dialog/sequence_dialog.h - util/title_ids.h util/url_request_interceptor.cpp util/url_request_interceptor.h util/util.cpp @@ -242,6 +243,8 @@ add_executable(citron citron.rc ) +set_source_files_properties(controller_overlay.cpp PROPERTIES SKIP_AUTOUIC ON) + if (CITRON_CRASH_DUMPS) target_sources(citron PRIVATE breakpad.cpp diff --git a/src/citron/configuration/configure_input_player_widget.cpp b/src/citron/configuration/configure_input_player_widget.cpp index 9875e229e..63bb41a1f 100644 --- a/src/citron/configuration/configure_input_player_widget.cpp +++ b/src/citron/configuration/configure_input_player_widget.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -22,6 +23,10 @@ PlayerControlPreview::~PlayerControlPreview() { UnloadController(); }; +void PlayerControlPreview::SetRawJoystickVisible(bool visible) { + raw_joystick_visible = visible; +} + void PlayerControlPreview::SetController(Core::HID::EmulatedController* controller_) { UnloadController(); is_controller_set = true; @@ -226,29 +231,50 @@ void PlayerControlPreview::paintEvent(QPaintEvent* event) { QFrame::paintEvent(event); QPainter p(this); p.setRenderHint(QPainter::Antialiasing); + + // Define the base size that the original drawing coordinates were designed for. + // A Pro Controller is roughly 420x320 pixels in its drawing function. + constexpr QSizeF base_size(450.0, 350.0); + + // Get the current size of the widget. + const QSize current_size = this->size(); + + // Calculate the scaling factor. We want to maintain aspect ratio, + // so we use the smaller of the width/height scaling factors. + const double scale_x = current_size.width() / base_size.width(); + const double scale_y = current_size.height() / base_size.height(); + const double scale = std::min(scale_x, scale_y); + + // Save the painter's state, apply the scaling, and center the drawing. + p.save(); const QPointF center = rect().center(); + p.translate(center); + p.scale(scale, scale); + p.translate(-center); switch (controller_type) { - case Core::HID::NpadStyleIndex::Handheld: - DrawHandheldController(p, center); - break; - case Core::HID::NpadStyleIndex::JoyconDual: - DrawDualController(p, center); - break; - case Core::HID::NpadStyleIndex::JoyconLeft: - DrawLeftController(p, center); - break; - case Core::HID::NpadStyleIndex::JoyconRight: - DrawRightController(p, center); - break; - case Core::HID::NpadStyleIndex::GameCube: - DrawGCController(p, center); - break; - case Core::HID::NpadStyleIndex::Fullkey: - default: - DrawProController(p, center); - break; + case Core::HID::NpadStyleIndex::Handheld: + DrawHandheldController(p, center); + break; + case Core::HID::NpadStyleIndex::JoyconDual: + DrawDualController(p, center); + break; + case Core::HID::NpadStyleIndex::JoyconLeft: + DrawLeftController(p, center); + break; + case Core::HID::NpadStyleIndex::JoyconRight: + DrawRightController(p, center); + break; + case Core::HID::NpadStyleIndex::GameCube: + DrawGCController(p, center); + break; + case Core::HID::NpadStyleIndex::Fullkey: + default: + DrawProController(p, center); + break; } + + p.restore(); // Restore the painter's original state. } void PlayerControlPreview::DrawLeftController(QPainter& p, const QPointF center) { @@ -314,7 +340,9 @@ void PlayerControlPreview::DrawLeftController(QPainter& p, const QPointF center) center + QPointF(9, -69) + (QPointF(stick_values[LStick].x.value, stick_values[LStick].y.value) * 8), 1.8f, button_values[Settings::NativeButton::LStick]); - DrawRawJoystick(p, center + QPointF(-140, 90), QPointF(0, 0)); + if (raw_joystick_visible) { + DrawRawJoystick(p, center + QPointF(-140, 90), QPointF(0, 0)); + } } { @@ -449,7 +477,9 @@ void PlayerControlPreview::DrawRightController(QPainter& p, const QPointF center center + QPointF(-9, 11) + (QPointF(stick_values[RStick].x.value, stick_values[RStick].y.value) * 8), 1.8f, button_values[Settings::NativeButton::RStick]); - DrawRawJoystick(p, QPointF(0, 0), center + QPointF(140, 90)); + if (raw_joystick_visible) { + DrawRawJoystick(p, QPointF(0, 0), center + QPointF(140, 90)); + } } { @@ -593,7 +623,9 @@ void PlayerControlPreview::DrawDualController(QPainter& p, const QPointF center) DrawJoystick(p, center + QPointF(-65, -65) + (l_stick * 7), 1.62f, l_button); DrawJoystick(p, center + QPointF(65, 12) + (r_stick * 7), 1.62f, r_button); - DrawRawJoystick(p, center + QPointF(-180, 90), center + QPointF(180, 90)); + if (raw_joystick_visible) { + DrawRawJoystick(p, center + QPointF(-180, 90), center + QPointF(180, 90)); + } } { @@ -696,7 +728,9 @@ void PlayerControlPreview::DrawHandheldController(QPainter& p, const QPointF cen DrawJoystick(p, center + QPointF(-171, -41) + (l_stick * 4), 1.0f, l_button); DrawJoystick(p, center + QPointF(171, 8) + (r_stick * 4), 1.0f, r_button); - DrawRawJoystick(p, center + QPointF(-50, 0), center + QPointF(50, 0)); + if (raw_joystick_visible) { + DrawRawJoystick(p, center + QPointF(-50, 0), center + QPointF(50, 0)); + } } { @@ -808,7 +842,9 @@ void PlayerControlPreview::DrawProController(QPainter& p, const QPointF center) button_values[Settings::NativeButton::LStick]); DrawProJoystick(p, center + QPointF(51, 0), r_stick, 11, button_values[Settings::NativeButton::RStick]); - DrawRawJoystick(p, center + QPointF(-50, 105), center + QPointF(50, 105)); + if (raw_joystick_visible) { + DrawRawJoystick(p, center + QPointF(-50, 105), center + QPointF(50, 105)); + } } { @@ -910,7 +946,9 @@ void PlayerControlPreview::DrawGCController(QPainter& p, const QPointF center) { p.setPen(colors.transparent); p.setBrush(colors.font); DrawSymbol(p, center + QPointF(61, 37) + (r_stick * 9.5f), Symbol::C, 1.0f); - DrawRawJoystick(p, center + QPointF(-198, -125), center + QPointF(198, -125)); + if (raw_joystick_visible) { + DrawRawJoystick(p, center + QPointF(-198, -125), center + QPointF(198, -125)); + } } using namespace Settings::NativeButton; diff --git a/src/citron/configuration/configure_input_player_widget.h b/src/citron/configuration/configure_input_player_widget.h index 76340912d..7b4589ef3 100644 --- a/src/citron/configuration/configure_input_player_widget.h +++ b/src/citron/configuration/configure_input_player_widget.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -29,6 +30,8 @@ public: // Sets the emulated controller to be displayed void SetController(Core::HID::EmulatedController* controller); + void SetRawJoystickVisible(bool visible); + // Disables events from the emulated controller void UnloadController(); @@ -207,6 +210,8 @@ private: void SetTextFont(QPainter& p, float text_size, const QString& font_family = QStringLiteral("sans-serif")); + bool raw_joystick_visible = false; + bool is_controller_set{}; bool is_connected{}; bool needs_redraw{}; diff --git a/src/citron/configuration/configure_per_game.cpp b/src/citron/configuration/configure_per_game.cpp index 6ee4fa204..ad1c58a2b 100644 --- a/src/citron/configuration/configure_per_game.cpp +++ b/src/citron/configuration/configure_per_game.cpp @@ -65,7 +65,7 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st std::vector& vk_device_records, Core::System& system_) : QDialog(parent), -ui(std::make_unique()), title_id{title_id_}, file_name{file_name}, system{system_}, +ui(std::make_unique()), title_id{title_id_}, file_name{file_name_}, system{system_}, builder{std::make_unique(this, !system_.IsPoweredOn())}, tab_group{std::make_shared>()} , rainbow_timer{new QTimer(this)} { diff --git a/src/citron/controller_overlay.cpp b/src/citron/controller_overlay.cpp new file mode 100644 index 000000000..c843669bb --- /dev/null +++ b/src/citron/controller_overlay.cpp @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "citron/controller_overlay.h" +#include "citron/configuration/configure_input_player_widget.h" +#include "citron/main.h" +#include "core/core.h" +#include "hid_core/hid_core.h" + +#include +#include +#include +#include +#include +#include // Required for Wayland dragging +#include + +namespace { +// Helper to get the active controller for Player 1 +Core::HID::EmulatedController* GetPlayer1Controller(Core::System* system) { + if (!system) return nullptr; + Core::HID::HIDCore& hid_core = system->HIDCore(); + auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); + if (handheld && handheld->IsConnected()) { + return handheld; + } + return hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); +} +} + +ControllerOverlay::ControllerOverlay(GMainWindow* parent) + : QWidget(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint), + main_window(parent) { + + setAttribute(Qt::WA_TranslucentBackground); + + auto* layout = new QGridLayout(this); + setLayout(layout); + // Set margins to 0 so the controller can go right to the edge of the resizable window + layout->setContentsMargins(0, 0, 0, 0); + + // Create the widget that draws the controller and make it transparent + controller_widget = new PlayerControlPreview(this); + controller_widget->setAttribute(Qt::WA_TranslucentBackground); + + // Disable the raw joystick (deadzone) visualization + controller_widget->SetRawJoystickVisible(false); + + // Allow the widget to expand and shrink with the window + controller_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + layout->addWidget(controller_widget, 0, 0); + + // Add a size grip for resizing + size_grip = new QSizeGrip(this); + layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight); + + // Start the timer for continuous updates + connect(&update_timer, &QTimer::timeout, this, &ControllerOverlay::UpdateControllerState); + update_timer.start(16); // ~60 FPS + + // Set a minimum size and a default starting size + setMinimumSize(225, 175); + resize(450, 350); +} + +ControllerOverlay::~ControllerOverlay() = default; + +void ControllerOverlay::UpdateControllerState() { + Core::System* system = main_window->GetSystem(); + Core::HID::EmulatedController* controller = GetPlayer1Controller(system); + if (controller_widget && controller) { + controller_widget->SetController(controller); + controller_widget->UpdateInput(); + } +} + +// The paint event is now empty, which makes the background fully transparent. +void ControllerOverlay::paintEvent(QPaintEvent* event) { + Q_UNUSED(event); + // Intentionally left blank to achieve a fully transparent window background. +} + +// These functions handle dragging the frameless window +void ControllerOverlay::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) { +#if defined(Q_OS_LINUX) + // Use system move on Wayland/Linux for proper dragging + if (windowHandle()) { + windowHandle()->startSystemMove(); + } +#else + // Original dragging implementation for other platforms (Windows, etc.) + is_dragging = true; + drag_start_pos = event->globalPosition().toPoint() - this->pos(); +#endif + event->accept(); + } +} + +void ControllerOverlay::mouseMoveEvent(QMouseEvent* event) { +#if !defined(Q_OS_LINUX) + if (is_dragging) { + move(event->globalPosition().toPoint() - drag_start_pos); + event->accept(); + } +#else + // On Linux, the window manager handles the move, so we do nothing here. + Q_UNUSED(event); +#endif +} + +void ControllerOverlay::mouseReleaseEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton) { + is_dragging = false; + event->accept(); + } +} + +void ControllerOverlay::resizeEvent(QResizeEvent* event) { + QWidget::resizeEvent(event); + // This ensures the layout and its widgets (like the size grip) are correctly repositioned on resize. + layout()->update(); +} diff --git a/src/citron/controller_overlay.h b/src/citron/controller_overlay.h new file mode 100644 index 000000000..69ab4427a --- /dev/null +++ b/src/citron/controller_overlay.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include + +class GMainWindow; +class PlayerControlPreview; +class QSizeGrip; + +class ControllerOverlay : public QWidget { + Q_OBJECT + +public: + explicit ControllerOverlay(GMainWindow* parent); + ~ControllerOverlay() override; + +protected: + void paintEvent(QPaintEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + +private slots: + void UpdateControllerState(); + +private: + GMainWindow* main_window; + QTimer update_timer; + + PlayerControlPreview* controller_widget; + QSizeGrip* size_grip; + + bool is_dragging = false; + QPoint drag_start_pos; +}; \ No newline at end of file diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 7c895b353..e0a6b5db1 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -156,6 +156,8 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "citron/about_dialog.h" #include "citron/bootmanager.h" #include "citron/compatdb.h" +#include "citron/controller_overlay.h" +#include "core/core.h" #include "citron/compatibility_list.h" #include "citron/configuration/configure_dialog.h" #include "citron/configuration/configure_input_per_game.h" @@ -181,7 +183,6 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "citron/util/clickable_label.h" #include "citron/util/performance_overlay.h" #include "citron/util/multiplayer_room_overlay.h" -#include "citron/util/title_ids.h" #include "citron/util/vram_overlay.h" #include "citron/vk_device_info.h" @@ -1668,6 +1669,9 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys); connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); connect_menu(ui->action_About, &GMainWindow::OnAbout); + + connect(ui->actionControllerOverlay, &QAction::triggered, this, &GMainWindow::OnToggleControllerOverlay); + } void GMainWindow::UpdateMenuState() { @@ -2020,8 +2024,7 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP system->ApplySettings(); // Final Fantasy Tactics requires single-core mode to boot properly - if (title_id == UICommon::TitleID::FinalFantasyTactics) { - LOG_INFO(Frontend, "Applying workaround: forcing single-core mode for Final Fantasy Tactics"); + if (title_id == 0x010038B015560000ULL) { Settings::values.use_multi_core.SetValue(false); } } @@ -4924,6 +4927,16 @@ void GMainWindow::OnAbout() { aboutDialog.exec(); } +void GMainWindow::OnToggleControllerOverlay() { + const bool visible = ui->actionControllerOverlay->isChecked(); + if (visible && !controller_overlay) { + controller_overlay = new ControllerOverlay(this); + } + if (controller_overlay) { + controller_overlay->setVisible(visible); + } +} + void GMainWindow::OnToggleFilterBar() { game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); if (ui->action_Show_Filter_Bar->isChecked()) { diff --git a/src/citron/main.h b/src/citron/main.h index 729bc6aae..84f53db1c 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -39,6 +39,7 @@ class OverlayDialog; class PerformanceOverlay; class MultiplayerRoomOverlay; class VramOverlay; +class ControllerOverlay; class ProfilerWidget; class ControllerDialog; class QLabel; @@ -109,6 +110,7 @@ public: bool DropAction(QDropEvent* event); void AcceptDropEvent(QDropEvent* event); MultiplayerState* GetMultiplayerState() { return multiplayer_state; } + Core::System* GetSystem() { return system.get(); } bool IsEmulationRunning() const { return emulation_running; } signals: void EmulationStarting(EmuThread* emu_thread); @@ -249,6 +251,7 @@ private slots: void OnTogglePerformanceOverlay(); void OnToggleMultiplayerRoomOverlay(); void OnToggleVramOverlay(); + void OnToggleControllerOverlay(); void OnDisplayTitleBars(bool); double GetCurrentFPS() const; double GetCurrentFrameTime() const; @@ -335,6 +338,7 @@ private: PerformanceOverlay* performance_overlay{}; MultiplayerRoomOverlay* multiplayer_room_overlay{}; VramOverlay* vram_overlay{}; + ControllerOverlay* controller_overlay{}; GameListPlaceholder* game_list_placeholder; std::vector vk_device_records; QLabel* message_label = nullptr; diff --git a/src/citron/main.ui b/src/citron/main.ui index de91bf454..680a6de36 100644 --- a/src/citron/main.ui +++ b/src/citron/main.ui @@ -129,6 +129,7 @@ + @@ -347,6 +348,14 @@ Show Multiplayer Room Overlay + + + true + + + Show Controller Overlay + + true