[UI] Add Quit Game option to the context menu

This commit is contained in:
Herman S.
2026-01-03 00:53:44 +09:00
parent 81af207bad
commit d7ca76fbb7
4 changed files with 443 additions and 0 deletions
+16
View File
@@ -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) {
+257
View File
@@ -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 <QHBoxLayout>
#include <QKeyEvent>
#include <QVBoxLayout>
#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<void(bool)> 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<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
QWidget* widget = qobject_cast<QWidget*>(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
+65
View File
@@ -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 <QLabel>
#include <QPushButton>
#include <QTimer>
#include <QWidget>
#include <functional>
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<void(bool)> 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<void(bool)> 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_
@@ -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 <QtCore/qmetatype.h>
#include "confirm_dialog_widget_qt.h"
#include <QtCore/qtmochelpers.h>
#include <memory>
#include <QtCore/qxptype_traits.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'confirm_dialog_widget_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_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<void()>(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<QWidget::staticMetaObject>(),
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe3app21ConfirmDialogWidgetQtE_t>.stringdata,
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe3app21ConfirmDialogWidgetQtE_t>.data,
qt_static_metacall,
nullptr,
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN2xe3app21ConfirmDialogWidgetQtE_t>.metaTypes,
nullptr
} };
void xe::app::ConfirmDialogWidgetQt::qt_static_metacall(QObject* _o,
QMetaObject::Call _c,
int _id, void** _a) {
auto* _t = static_cast<ConfirmDialogWidgetQt*>(_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<qt_meta_tag_ZN2xe3app21ConfirmDialogWidgetQtE_t>.strings))
return static_cast<void*>(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<QMetaType*>(_a[0]) = QMetaType();
_id -= 1;
}
return _id;
}
QT_WARNING_POP