mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[UI] Add controller support to Qt achievement dialog
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user