diff --git a/src/xenia/ui/achievements_dialog_qt.cc b/src/xenia/ui/achievements_dialog_qt.cc index 2f3842595..99e1c7388 100644 --- a/src/xenia/ui/achievements_dialog_qt.cc +++ b/src/xenia/ui/achievements_dialog_qt.cc @@ -52,7 +52,7 @@ AchievementsDialogQt::AchievementsDialogQt( QWidget* parent, kernel::KernelState* kernel_state, const kernel::xam::TitleInfo* title_info, const kernel::xam::UserProfile* profile) - : QDialog(parent), + : GamepadDialog(parent, kernel_state->emulator()->input_system()), kernel_state_(kernel_state), title_info_(title_info), profile_(profile), diff --git a/src/xenia/ui/achievements_dialog_qt.h b/src/xenia/ui/achievements_dialog_qt.h index 0d5cd8188..ad265dcac 100644 --- a/src/xenia/ui/achievements_dialog_qt.h +++ b/src/xenia/ui/achievements_dialog_qt.h @@ -11,7 +11,6 @@ #define XENIA_UI_ACHIEVEMENTS_DIALOG_QT_H_ #include -#include #include #include #include @@ -23,6 +22,8 @@ #include #include +#include "xenia/ui/gamepad_dialog_qt.h" + namespace xe { namespace kernel { class KernelState; @@ -37,7 +38,7 @@ class UserProfile; namespace xe { namespace ui { -class AchievementsDialogQt : public QDialog { +class AchievementsDialogQt : public GamepadDialog { Q_OBJECT public: diff --git a/src/xenia/ui/gamepad_dialog_qt.cc b/src/xenia/ui/gamepad_dialog_qt.cc new file mode 100644 index 000000000..045debbbd --- /dev/null +++ b/src/xenia/ui/gamepad_dialog_qt.cc @@ -0,0 +1,332 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/gamepad_dialog_qt.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xenia/base/logging.h" +#include "xenia/hid/input_system.h" + +namespace xe { +namespace ui { + +GamepadDialog::GamepadDialog(QWidget* parent, hid::InputSystem* input_system) + : QDialog(parent), + input_system_(input_system), + poll_timer_(nullptr), + current_focus_index_(-1), + prev_buttons_(0), + repeat_counter_(0), + scroll_accumulator_(0.0f) { + if (input_system_) { + poll_timer_ = new QTimer(this); + connect(poll_timer_, &QTimer::timeout, this, &GamepadDialog::PollGamepad); + poll_timer_->start(16); // Poll at ~60Hz + } +} + +GamepadDialog::~GamepadDialog() { + if (poll_timer_) { + poll_timer_->stop(); + delete poll_timer_; + } +} + +void GamepadDialog::PollGamepad() { + if (!input_system_) { + return; + } + + // Try to get input from any connected controller + hid::X_INPUT_STATE state; + bool got_input = false; + for (uint32_t user_index = 0; user_index < 4; user_index++) { + // Pass InputType::Controller (1) as flags to get controller input + if (input_system_->GetState(user_index, 1, &state) == 0) { + got_input = true; + break; + } + } + + if (!got_input) { + repeat_counter_ = 0; + prev_buttons_ = 0; + return; + } + + uint16_t buttons = state.gamepad.buttons; + uint16_t pressed = buttons & ~prev_buttons_; // Edge detection + + // Button mapping (no repeat for these) + if (pressed & 0x1000) { // A button + OnGamepadButtonA(); + } + if (pressed & 0x2000) { // B button + OnGamepadButtonB(); + } + if (pressed & 0x4000) { // X button + OnGamepadButtonX(); + } + if (pressed & 0x8000) { // Y button + OnGamepadButtonY(); + } + if (pressed & 0x0010) { // Start button + OnGamepadStart(); + } + if (pressed & 0x0020) { // Back button + OnGamepadBack(); + } + + // D-pad navigation with repeat + // First press (edge detection) + bool navigated = false; + if (pressed & 0x0001) { // D-pad Up + NavigateFocusVertical(-1); + navigated = true; + } + if (pressed & 0x0002) { // D-pad Down + NavigateFocusVertical(1); + navigated = true; + } + if (pressed & 0x0004) { // D-pad Left + NavigateFocusHorizontal(-1); + navigated = true; + } + if (pressed & 0x0008) { // D-pad Right + NavigateFocusHorizontal(1); + navigated = true; + } + + // Handle button repeat for held D-pad + const uint16_t dpad_mask = 0x000F; // All D-pad buttons + if (buttons & dpad_mask) { + repeat_counter_++; + // Initial delay: 30 polls (~500ms), then repeat every 4 polls (~67ms) + const int initial_delay = 30; + const int repeat_rate = 4; + + if (repeat_counter_ >= initial_delay && + (repeat_counter_ - initial_delay) % repeat_rate == 0) { + if (buttons & 0x0001) NavigateFocusVertical(-1); + if (buttons & 0x0002) NavigateFocusVertical(1); + if (buttons & 0x0004) NavigateFocusHorizontal(-1); + if (buttons & 0x0008) NavigateFocusHorizontal(1); + navigated = true; + } + } else { + repeat_counter_ = 0; + } + + // Right stick scrolling (moves scrollbar without changing selection) + const int16_t deadzone = 7849; // ~30% deadzone + int16_t right_y = state.gamepad.thumb_ry; + + if (abs(right_y) > deadzone && current_focus_index_ >= 0 && + current_focus_index_ < static_cast(focusable_widgets_.size())) { + auto* widget = focusable_widgets_[current_focus_index_]; + + // Find the scrollable area + QAbstractScrollArea* scroll_area = + qobject_cast(widget); + if (!scroll_area) { + // Check if it's a child of a scroll area + QWidget* parent = widget->parentWidget(); + while (parent && !scroll_area) { + scroll_area = qobject_cast(parent); + parent = parent->parentWidget(); + } + } + + if (scroll_area && scroll_area->verticalScrollBar()) { + auto* scrollbar = scroll_area->verticalScrollBar(); + // Invert Y axis (up is positive in gamepad coords) + // Accumulate fractional scrolling for smooth movement + scroll_accumulator_ += -static_cast(right_y) / 50000.0f; + + int scroll_pixels = static_cast(scroll_accumulator_); + if (scroll_pixels != 0) { + scrollbar->setValue(scrollbar->value() + scroll_pixels); + scroll_accumulator_ -= scroll_pixels; // Keep fractional part + } + } + } else { + scroll_accumulator_ = 0.0f; // Reset when stick released + } + + prev_buttons_ = buttons; +} + +void GamepadDialog::showEvent(QShowEvent* event) { + QDialog::showEvent(event); + UpdateFocusableWidgets(); + + // Auto-focus first widget + if (!focusable_widgets_.empty()) { + current_focus_index_ = 0; + focusable_widgets_[0]->setFocus(); + ApplyFocusStyle(focusable_widgets_[0], true); + } +} + +void GamepadDialog::UpdateFocusableWidgets() { + focusable_widgets_.clear(); + + // Find all focusable widgets in the dialog + auto all_widgets = findChildren(); + for (auto* widget : all_widgets) { + if (IsWidgetGamepadFocusable(widget)) { + focusable_widgets_.push_back(widget); + } + } +} + +bool GamepadDialog::IsWidgetGamepadFocusable(QWidget* widget) const { + if (!widget || !widget->isVisible() || !widget->isEnabled()) { + return false; + } + + // Check if it's a focusable widget type + return qobject_cast(widget) || + qobject_cast(widget) || + qobject_cast(widget) || qobject_cast(widget) || + qobject_cast(widget) || + qobject_cast(widget) || + qobject_cast(widget); +} + +void GamepadDialog::NavigateFocusVertical(int direction) { + if (focusable_widgets_.empty()) { + return; + } + + // Check if current focused widget is a list/table - navigate items within it + bool navigate_items = false; + if (current_focus_index_ >= 0 && + current_focus_index_ < static_cast(focusable_widgets_.size())) { + auto* widget = focusable_widgets_[current_focus_index_]; + + // Handle QListWidget - navigate items + if (auto* list = qobject_cast(widget)) { + int current_row = list->currentRow(); + int new_row = current_row + direction; + if (new_row >= 0 && new_row < list->count()) { + list->setCurrentRow(new_row); + list->scrollToItem(list->currentItem()); + return; // Successfully navigated within list + } + navigate_items = true; + } + + // Handle QTableWidget - navigate rows + if (auto* table = qobject_cast(widget)) { + int current_row = table->currentRow(); + int new_row = current_row + direction; + if (new_row >= 0 && new_row < table->rowCount()) { + table->setCurrentCell(new_row, table->currentColumn()); + table->scrollToItem(table->currentItem()); + return; // Successfully navigated within table + } + navigate_items = true; + } + } + + // If we're at the edge of a list/table, or not in one, navigate between + // widgets Clear old focus style + if (current_focus_index_ >= 0 && + current_focus_index_ < static_cast(focusable_widgets_.size())) { + ApplyFocusStyle(focusable_widgets_[current_focus_index_], false); + } + + // Navigate + current_focus_index_ += direction; + if (current_focus_index_ < 0) { + current_focus_index_ = static_cast(focusable_widgets_.size()) - 1; + } else if (current_focus_index_ >= + static_cast(focusable_widgets_.size())) { + current_focus_index_ = 0; + } + + // Apply new focus + auto* widget = focusable_widgets_[current_focus_index_]; + widget->setFocus(); + ApplyFocusStyle(widget, true); + + // Scroll into view if needed + if (auto* scroll_area = qobject_cast(widget->parentWidget())) { + scroll_area->ensureWidgetVisible(widget); + } +} + +void GamepadDialog::NavigateFocusHorizontal(int direction) { + // For now, treat horizontal navigation the same as vertical + // Could be enhanced to handle horizontal layouts differently + NavigateFocusVertical(direction); +} + +void GamepadDialog::AcceptFocusedButton() { + if (current_focus_index_ < 0 || + current_focus_index_ >= static_cast(focusable_widgets_.size())) { + return; + } + + auto* widget = focusable_widgets_[current_focus_index_]; + if (auto* button = qobject_cast(widget)) { + button->click(); + } +} + +void GamepadDialog::ApplyFocusStyle(QWidget* widget, bool focused) { + if (!widget) { + return; + } + + // Don't apply border to list/table widgets - they have their own item + // selection highlighting + if (qobject_cast(widget) || + qobject_cast(widget)) { + return; + } + + if (focused) { + // Save original stylesheet if not already saved + if (original_stylesheet_.isEmpty()) { + original_stylesheet_ = widget->styleSheet(); + } + + // Apply highlighted border + QString focus_style = original_stylesheet_ + + "\nQWidget { border: 2px solid #0078d7; " + "border-radius: 4px; }"; + widget->setStyleSheet(focus_style); + } else { + // Restore original stylesheet + widget->setStyleSheet(original_stylesheet_); + } +} + +bool GamepadDialog::eventFilter(QObject* obj, QEvent* event) { + // Let the base class handle the event + return QDialog::eventFilter(obj, event); +} + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/gamepad_dialog_qt.h b/src/xenia/ui/gamepad_dialog_qt.h new file mode 100644 index 000000000..ae0ee47ce --- /dev/null +++ b/src/xenia/ui/gamepad_dialog_qt.h @@ -0,0 +1,80 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2025 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_GAMEPAD_DIALOG_QT_H_ +#define XENIA_UI_GAMEPAD_DIALOG_QT_H_ + +#include +#include +#include +#include + +namespace xe { +namespace hid { +class InputSystem; +} // namespace hid +} // namespace xe + +namespace xe { +namespace ui { + +class GamepadDialog : public QDialog { + Q_OBJECT + + public: + explicit GamepadDialog(QWidget* parent, hid::InputSystem* input_system); + ~GamepadDialog() override; + + protected: + // Called when gamepad is connected/disconnected + virtual void OnGamepadConnected() {} + virtual void OnGamepadDisconnected() {} + + // Override to customize button behavior + virtual void OnGamepadButtonA() { AcceptFocusedButton(); } + virtual void OnGamepadButtonB() { reject(); } + virtual void OnGamepadButtonX() {} + virtual void OnGamepadButtonY() {} + virtual void OnGamepadStart() { AcceptFocusedButton(); } + virtual void OnGamepadBack() { reject(); } + + // Override to customize which widgets are focusable + virtual bool IsWidgetGamepadFocusable(QWidget* widget) const; + + bool eventFilter(QObject* obj, QEvent* event) override; + void showEvent(QShowEvent* event) override; + + private slots: + void PollGamepad(); + + private: + void UpdateFocusableWidgets(); + void NavigateFocusVertical(int direction); + void NavigateFocusHorizontal(int direction); + void AcceptFocusedButton(); + void ApplyFocusStyle(QWidget* widget, bool focused); + + hid::InputSystem* input_system_; + QTimer* poll_timer_; + std::vector focusable_widgets_; + int current_focus_index_; + + // Previous button states for edge detection and repeat + uint16_t prev_buttons_; + int repeat_counter_; // Counts polls while button held for repeat + // functionality + float scroll_accumulator_; // Accumulates fractional scrolling + + QString original_stylesheet_; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_GAMEPAD_DIALOG_QT_H_ diff --git a/src/xenia/ui/moc_gamepad_dialog_qt.cc b/src/xenia/ui/moc_gamepad_dialog_qt.cc new file mode 100644 index 000000000..122c52b18 --- /dev/null +++ b/src/xenia/ui/moc_gamepad_dialog_qt.cc @@ -0,0 +1,105 @@ +/**************************************************************************** +** Meta object code from reading C++ file 'gamepad_dialog_qt.h' +** +** Created by: The Qt Meta Object Compiler version 69 (Qt 6.9.2) +** +** WARNING! All changes made in this file will be lost! +*****************************************************************************/ + +#include +#include "gamepad_dialog_qt.h" + +#include + +#include + +#include +#if !defined(Q_MOC_OUTPUT_REVISION) +#error "The header file 'gamepad_dialog_qt.h' doesn't include ." +#elif Q_MOC_OUTPUT_REVISION != 69 +#error "This file was generated using the moc from 6.9.2. It" +#error "cannot be used with the include files from this version of Qt." +#error "(The moc has changed too much.)" +#endif + +#ifndef Q_CONSTINIT +#define Q_CONSTINIT +#endif + +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED +QT_WARNING_DISABLE_GCC("-Wuseless-cast") +namespace { +struct qt_meta_tag_ZN2xe2ui13GamepadDialogE_t {}; +} // unnamed namespace + +template <> +constexpr inline auto xe::ui::GamepadDialog::qt_create_metaobjectdata< + qt_meta_tag_ZN2xe2ui13GamepadDialogE_t>() { + namespace QMC = QtMocConstants; + QtMocHelpers::StringRefStorage qt_stringData{"xe::ui::GamepadDialog", + "PollGamepad", ""}; + + QtMocHelpers::UintData qt_methods{ + // Slot 'PollGamepad' + QtMocHelpers::SlotData(1, 2, QMC::AccessPrivate, QMetaType::Void), + }; + QtMocHelpers::UintData qt_properties{}; + QtMocHelpers::UintData qt_enums{}; + return QtMocHelpers::metaObjectData( + QMC::MetaObjectFlag{}, qt_stringData, qt_methods, qt_properties, + qt_enums); +} +Q_CONSTINIT const QMetaObject xe::ui::GamepadDialog::staticMetaObject = { { + QMetaObject::SuperData::link(), + qt_staticMetaObjectStaticContent.stringdata, + qt_staticMetaObjectStaticContent.data, + qt_static_metacall, + nullptr, + qt_staticMetaObjectRelocatingContent.metaTypes, + nullptr +} }; + +void xe::ui::GamepadDialog::qt_static_metacall(QObject* _o, + QMetaObject::Call _c, int _id, + void** _a) { + auto* _t = static_cast(_o); + if (_c == QMetaObject::InvokeMetaMethod) { + switch (_id) { + case 0: + _t->PollGamepad(); + break; + default:; + } + } + (void)_a; +} + +const QMetaObject* xe::ui::GamepadDialog::metaObject() const { + return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() + : &staticMetaObject; +} + +void* xe::ui::GamepadDialog::qt_metacast(const char* _clname) { + if (!_clname) return nullptr; + if (!strcmp(_clname, qt_staticMetaObjectStaticContent.strings)) + return static_cast(this); + return QDialog::qt_metacast(_clname); +} + +int xe::ui::GamepadDialog::qt_metacall(QMetaObject::Call _c, int _id, + void** _a) { + _id = QDialog::qt_metacall(_c, _id, _a); + if (_id < 0) return _id; + if (_c == QMetaObject::InvokeMetaMethod) { + if (_id < 1) qt_static_metacall(this, _c, _id, _a); + _id -= 1; + } + if (_c == QMetaObject::RegisterMethodArgumentMetaType) { + if (_id < 1) *reinterpret_cast(_a[0]) = QMetaType(); + _id -= 1; + } + return _id; +} +QT_WARNING_POP