[UI] Add controller support to Qt achievement dialog

This commit is contained in:
Herman S.
2025-11-24 17:58:08 +09:00
parent dbbaf2232b
commit 94f8a287d8
5 changed files with 521 additions and 3 deletions
+1 -1
View File
@@ -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),
+3 -2
View File
@@ -11,7 +11,6 @@
#define XENIA_UI_ACHIEVEMENTS_DIALOG_QT_H_
#include <QCheckBox>
#include <QDialog>
#include <QLabel>
#include <QPixmap>
#include <QProgressBar>
@@ -23,6 +22,8 @@
#include <memory>
#include <vector>
#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:
+332
View File
@@ -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 <cstdlib>
#include <QAbstractButton>
#include <QAbstractScrollArea>
#include <QApplication>
#include <QComboBox>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QScrollArea>
#include <QScrollBar>
#include <QSpinBox>
#include <QTableWidget>
#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<int>(focusable_widgets_.size())) {
auto* widget = focusable_widgets_[current_focus_index_];
// Find the scrollable area
QAbstractScrollArea* scroll_area =
qobject_cast<QAbstractScrollArea*>(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<QAbstractScrollArea*>(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<float>(right_y) / 50000.0f;
int scroll_pixels = static_cast<int>(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<QWidget*>();
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<QPushButton*>(widget) ||
qobject_cast<QAbstractButton*>(widget) ||
qobject_cast<QLineEdit*>(widget) || qobject_cast<QComboBox*>(widget) ||
qobject_cast<QSpinBox*>(widget) ||
qobject_cast<QListWidget*>(widget) ||
qobject_cast<QTableWidget*>(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<int>(focusable_widgets_.size())) {
auto* widget = focusable_widgets_[current_focus_index_];
// Handle QListWidget - navigate items
if (auto* list = qobject_cast<QListWidget*>(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<QTableWidget*>(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<int>(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<int>(focusable_widgets_.size()) - 1;
} else if (current_focus_index_ >=
static_cast<int>(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<QScrollArea*>(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<int>(focusable_widgets_.size())) {
return;
}
auto* widget = focusable_widgets_[current_focus_index_];
if (auto* button = qobject_cast<QAbstractButton*>(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<QListWidget*>(widget) ||
qobject_cast<QTableWidget*>(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
+80
View File
@@ -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 <QDialog>
#include <QTimer>
#include <QWidget>
#include <vector>
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<QWidget*> 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_
+105
View File
@@ -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 <QtCore/qmetatype.h>
#include "gamepad_dialog_qt.h"
#include <QtCore/qtmochelpers.h>
#include <memory>
#include <QtCore/qxptype_traits.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'gamepad_dialog_qt.h' doesn't include <QObject>."
#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<void()>(1, 2, QMC::AccessPrivate, QMetaType::Void),
};
QtMocHelpers::UintData qt_properties{};
QtMocHelpers::UintData qt_enums{};
return QtMocHelpers::metaObjectData<GamepadDialog,
qt_meta_tag_ZN2xe2ui13GamepadDialogE_t>(
QMC::MetaObjectFlag{}, qt_stringData, qt_methods, qt_properties,
qt_enums);
}
Q_CONSTINIT const QMetaObject xe::ui::GamepadDialog::staticMetaObject = { {
QMetaObject::SuperData::link<QDialog::staticMetaObject>(),
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe2ui13GamepadDialogE_t>.stringdata,
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe2ui13GamepadDialogE_t>.data,
qt_static_metacall,
nullptr,
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN2xe2ui13GamepadDialogE_t>.metaTypes,
nullptr
} };
void xe::ui::GamepadDialog::qt_static_metacall(QObject* _o,
QMetaObject::Call _c, int _id,
void** _a) {
auto* _t = static_cast<GamepadDialog*>(_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<qt_meta_tag_ZN2xe2ui13GamepadDialogE_t>.strings))
return static_cast<void*>(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<QMetaType*>(_a[0]) = QMetaType();
_id -= 1;
}
return _id;
}
QT_WARNING_POP