diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 356a91385..fffdb17e1 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -34,6 +34,7 @@ #include "xenia/base/threading.h" #include "xenia/config.h" #include "xenia/ui/config_dialog_qt.h" +#include "xenia/ui/confirm_dialog_widget_qt.h" #include "xenia/ui/context_menu_widget_qt.h" #include "xenia/ui/controller_hotkeys_dialog_qt.h" #include "xenia/ui/game_list_dialog_qt.h" @@ -1180,6 +1181,21 @@ void EmulatorWindow::ToggleContextMenu(bool use_cursor_position) { context_menu->AddAction("XMP Audio Player", [this]() { ToggleXMPConfigDialog(); }); + context_menu->AddSeparator(); + + context_menu->AddAction("Quit Game", [this, qt_window, input_sys]() { + auto* confirm = new ConfirmDialogWidgetQt( + qt_window->qwindow(), input_sys, "Quit Game", + "Are you sure you want to quit?\n\nAny unsaved progress will be " + "lost.", + [this](bool confirmed) { + if (confirmed) { + window_->RequestClose(); + } + }); + confirm->ShowCentered(); + }); + // Show menu at cursor position or center of window QPoint global_pos; if (use_cursor_position) { diff --git a/src/xenia/ui/confirm_dialog_widget_qt.cc b/src/xenia/ui/confirm_dialog_widget_qt.cc new file mode 100644 index 000000000..e88432489 --- /dev/null +++ b/src/xenia/ui/confirm_dialog_widget_qt.cc @@ -0,0 +1,257 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/confirm_dialog_widget_qt.h" + +#include +#include +#include + +#include "xenia/hid/input_system.h" + +namespace xe { +namespace app { + +ConfirmDialogWidgetQt::ConfirmDialogWidgetQt(QWidget* parent, + hid::InputSystem* input_system, + const QString& title, + const QString& message, + std::function callback) + : QWidget(parent), + callback_(callback), + input_system_(input_system), + poll_timer_(nullptr), + focused_index_(0), // Start with "No" focused (safer default) + prev_buttons_(0) { + setAttribute(Qt::WA_DeleteOnClose); + setAutoFillBackground(true); + setAttribute(Qt::WA_StyledBackground, true); + + // Match theme from postprocessing dialog + setStyleSheet( + "background-color: rgb(30, 30, 30); " + "border: 1px solid rgba(100, 100, 100, 180);"); + + auto* main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(24, 20, 24, 20); + main_layout->setSpacing(16); + + // Title + auto* title_label = new QLabel(title, this); + title_label->setStyleSheet( + "color: #f0f0f0; font-size: 15px; font-weight: bold; " + "background: transparent; border: none;"); + title_label->setAlignment(Qt::AlignCenter); + main_layout->addWidget(title_label); + + // Message + auto* message_label = new QLabel(message, this); + message_label->setStyleSheet( + "color: #d0d0d0; font-size: 12px; " + "background: transparent; border: none;"); + message_label->setAlignment(Qt::AlignCenter); + message_label->setWordWrap(true); + main_layout->addWidget(message_label); + + main_layout->addSpacing(8); + + // Buttons + auto* button_layout = new QHBoxLayout(); + button_layout->setSpacing(12); + + no_button_ = new QPushButton("No", this); + no_button_->setMinimumSize(90, 32); + no_button_->setCursor(Qt::PointingHandCursor); + connect(no_button_, &QPushButton::clicked, this, [this]() { + close(); + if (callback_) callback_(false); + }); + + yes_button_ = new QPushButton("Yes", this); + yes_button_->setMinimumSize(90, 32); + yes_button_->setCursor(Qt::PointingHandCursor); + connect(yes_button_, &QPushButton::clicked, this, [this]() { + close(); + if (callback_) callback_(true); + }); + + button_layout->addStretch(); + button_layout->addWidget(no_button_); + button_layout->addWidget(yes_button_); + button_layout->addStretch(); + + main_layout->addLayout(button_layout); + + // Install global event filter to close on clicks outside + qApp->installEventFilter(this); + + // Start gamepad polling if input system is available + if (input_system_) { + input_system_->AddUIInputBlocker(); + + // Initialize prev_buttons_ to current state + hid::X_INPUT_STATE state; + for (uint32_t user_index = 0; user_index < 4; user_index++) { + if (input_system_->GetStateForUI(user_index, 1, &state) == 0) { + prev_buttons_ = state.gamepad.buttons; + break; + } + } + + poll_timer_ = new QTimer(this); + connect(poll_timer_, &QTimer::timeout, this, + &ConfirmDialogWidgetQt::PollGamepad); + poll_timer_->start(16); // ~60fps polling + } +} + +ConfirmDialogWidgetQt::~ConfirmDialogWidgetQt() { + if (poll_timer_) { + poll_timer_->stop(); + } + if (input_system_) { + input_system_->RemoveUIInputBlocker(); + } +} + +void ConfirmDialogWidgetQt::ShowCentered() { + if (!parentWidget()) { + return; + } + + adjustSize(); + + // Center in parent + QWidget* parent = parentWidget(); + int x = (parent->width() - width()) / 2; + int y = (parent->height() - height()) / 2; + + move(x, y); + + show(); + raise(); + setFocus(Qt::PopupFocusReason); + + // Set initial button focus + UpdateFocusedButton(focused_index_); +} + +void ConfirmDialogWidgetQt::keyPressEvent(QKeyEvent* event) { + switch (event->key()) { + case Qt::Key_Escape: + close(); + if (callback_) callback_(false); + break; + case Qt::Key_Left: + case Qt::Key_Right: + UpdateFocusedButton(focused_index_ == 0 ? 1 : 0); + break; + case Qt::Key_Return: + case Qt::Key_Enter: + ActivateFocusedButton(); + break; + default: + QWidget::keyPressEvent(event); + } +} + +bool ConfirmDialogWidgetQt::eventFilter(QObject* obj, QEvent* event) { + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent* mouseEvent = static_cast(event); + + if (mouseEvent->button() == Qt::LeftButton) { + QWidget* widget = qobject_cast(obj); + if (widget) { + QPoint globalPos = widget->mapToGlobal(mouseEvent->pos()); + QPoint localPos = mapFromGlobal(globalPos); + + if (!rect().contains(localPos)) { + close(); + if (callback_) callback_(false); + return true; + } + } + } + } + return false; +} + +void ConfirmDialogWidgetQt::PollGamepad() { + if (!input_system_) { + return; + } + + for (uint32_t i = 0; i < 4; ++i) { + hid::X_INPUT_STATE state; + if (input_system_->GetStateForUI(i, 1, &state) == 0) { + uint16_t buttons = state.gamepad.buttons; + uint16_t pressed = buttons & ~prev_buttons_; + + // D-pad left/right to switch buttons + if (pressed & 0x0004) { // D-pad left + UpdateFocusedButton(0); // No + } + if (pressed & 0x0008) { // D-pad right + UpdateFocusedButton(1); // Yes + } + + // A button to confirm selection + if (pressed & 0x1000) { + ActivateFocusedButton(); + } + + // B button to cancel (same as No) + if (pressed & 0x2000) { + close(); + if (callback_) callback_(false); + } + + prev_buttons_ = buttons; + break; + } + } +} + +void ConfirmDialogWidgetQt::UpdateFocusedButton(int index) { + focused_index_ = index; + + // Theme-consistent button styles + QString unfocused_style = + "QPushButton { background-color: rgba(50, 50, 50, 200); color: #d0d0d0; " + "border: 2px solid #909090; border-radius: 4px; " + "font-size: 12px; padding: 6px 16px; }" + "QPushButton:hover { background-color: rgba(70, 70, 70, 200); " + "border-color: #b0b0b0; }"; + + QString focused_style = + "QPushButton { background-color: rgba(70, 70, 70, 200); color: #f0f0f0; " + "border: 2px solid #107c10; border-radius: 4px; " + "font-size: 12px; padding: 6px 16px; }"; + + if (focused_index_ == 0) { + no_button_->setStyleSheet(focused_style); + yes_button_->setStyleSheet(unfocused_style); + } else { + no_button_->setStyleSheet(unfocused_style); + yes_button_->setStyleSheet(focused_style); + } +} + +void ConfirmDialogWidgetQt::ActivateFocusedButton() { + bool result = (focused_index_ == 1); // Yes = 1 + close(); + if (callback_) { + QTimer::singleShot(0, [this, result]() { + if (callback_) callback_(result); + }); + } +} + +} // namespace app +} // namespace xe diff --git a/src/xenia/ui/confirm_dialog_widget_qt.h b/src/xenia/ui/confirm_dialog_widget_qt.h new file mode 100644 index 000000000..6a62646d7 --- /dev/null +++ b/src/xenia/ui/confirm_dialog_widget_qt.h @@ -0,0 +1,65 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_CONFIRM_DIALOG_WIDGET_QT_H_ +#define XENIA_UI_CONFIRM_DIALOG_WIDGET_QT_H_ + +#include +#include +#include +#include +#include + +namespace xe { +namespace hid { +class InputSystem; +} // namespace hid +} // namespace xe + +namespace xe { +namespace app { + +// Confirmation dialog widget with gamepad support +class ConfirmDialogWidgetQt : public QWidget { + Q_OBJECT + + public: + ConfirmDialogWidgetQt(QWidget* parent, hid::InputSystem* input_system, + const QString& title, const QString& message, + std::function callback); + ~ConfirmDialogWidgetQt() override; + + void ShowCentered(); + + protected: + void keyPressEvent(QKeyEvent* event) override; + bool eventFilter(QObject* obj, QEvent* event) override; + + private slots: + void PollGamepad(); + + private: + void UpdateFocusedButton(int index); + void ActivateFocusedButton(); + + std::function callback_; + QPushButton* yes_button_; + QPushButton* no_button_; + + // Gamepad support + hid::InputSystem* input_system_; + QTimer* poll_timer_; + int focused_index_; // 0 = No, 1 = Yes + uint16_t prev_buttons_; +}; + +} // namespace app +} // namespace xe + +#endif // XENIA_UI_CONFIRM_DIALOG_WIDGET_QT_H_ diff --git a/src/xenia/ui/moc_confirm_dialog_widget_qt.cc b/src/xenia/ui/moc_confirm_dialog_widget_qt.cc new file mode 100644 index 000000000..d7e191e1a --- /dev/null +++ b/src/xenia/ui/moc_confirm_dialog_widget_qt.cc @@ -0,0 +1,105 @@ +/**************************************************************************** +** Meta object code from reading C++ file 'confirm_dialog_widget_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 "confirm_dialog_widget_qt.h" + +#include + +#include + +#include +#if !defined(Q_MOC_OUTPUT_REVISION) +#error "The header file 'confirm_dialog_widget_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_ZN2xe3app21ConfirmDialogWidgetQtE_t {}; +} // unnamed namespace + +template <> +constexpr inline auto xe::app::ConfirmDialogWidgetQt::qt_create_metaobjectdata< + qt_meta_tag_ZN2xe3app21ConfirmDialogWidgetQtE_t>() { + namespace QMC = QtMocConstants; + QtMocHelpers::StringRefStorage qt_stringData{"xe::app::ConfirmDialogWidgetQt", + "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< + ConfirmDialogWidgetQt, qt_meta_tag_ZN2xe3app21ConfirmDialogWidgetQtE_t>( + QMC::MetaObjectFlag{}, qt_stringData, qt_methods, qt_properties, + qt_enums); +} +Q_CONSTINIT const QMetaObject xe::app::ConfirmDialogWidgetQt::staticMetaObject = { { + QMetaObject::SuperData::link(), + qt_staticMetaObjectStaticContent.stringdata, + qt_staticMetaObjectStaticContent.data, + qt_static_metacall, + nullptr, + qt_staticMetaObjectRelocatingContent.metaTypes, + nullptr +} }; + +void xe::app::ConfirmDialogWidgetQt::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::app::ConfirmDialogWidgetQt::metaObject() const { + return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() + : &staticMetaObject; +} + +void* xe::app::ConfirmDialogWidgetQt::qt_metacast(const char* _clname) { + if (!_clname) return nullptr; + if (!strcmp(_clname, qt_staticMetaObjectStaticContent.strings)) + return static_cast(this); + return QWidget::qt_metacast(_clname); +} + +int xe::app::ConfirmDialogWidgetQt::qt_metacall(QMetaObject::Call _c, int _id, + void** _a) { + _id = QWidget::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