[UI] Switch from QWidget to QWindow and ImGui in the game process

Re-implement all the game overlays in imgui and remove unused Qt ones.
Qt overlays (and even the multimedia playback which is currently still
there) interfere with d3d12 rendering in hard to track down ways, so
moving back to imgui and closer to native render window implementation
without the hacks that were needed previously.
This commit is contained in:
Herman S.
2026-01-09 23:52:44 +09:00
parent 440c037404
commit fbfffd9b84
38 changed files with 2854 additions and 2677 deletions
+153 -160
View File
@@ -34,17 +34,16 @@
#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"
#include "xenia/ui/notification_widget_qt.h"
#include "xenia/ui/performance_tuning_dialog_qt.h"
#include "xenia/ui/postprocessing_dialog_qt.h"
#include "xenia/ui/profile_dialog_qt.h"
#include "xenia/ui/imgui_confirm_dialog.h"
#include "xenia/ui/imgui_context_menu.h"
#include "xenia/ui/imgui_controller_hotkeys_dialog.h"
#include "xenia/ui/imgui_performance_dialog.h"
#include "xenia/ui/imgui_postprocessing_dialog.h"
#include "xenia/ui/imgui_xmp_dialog.h"
#include "xenia/ui/profile_dialogs.h"
#include "xenia/ui/qt_util.h"
#include "xenia/ui/simple_config_dialog_qt.h"
#include "xenia/ui/xmp_dialog_qt.h"
#if XE_PLATFORM_WIN32
#include <windows.h>
@@ -82,6 +81,25 @@
#ifdef Bool
#undef Bool
#endif
// Undefine X11 macros that conflict with Qt's QEvent enum
#ifdef KeyPress
#undef KeyPress
#endif
#ifdef KeyRelease
#undef KeyRelease
#endif
#ifdef FocusIn
#undef FocusIn
#endif
#ifdef FocusOut
#undef FocusOut
#endif
#ifdef FontChange
#undef FontChange
#endif
#ifdef Expose
#undef Expose
#endif
#endif
#include "xenia/cpu/processor.h"
@@ -1111,84 +1129,76 @@ void EmulatorWindow::OnMouseDown(const ui::MouseEvent& e) {
void EmulatorWindow::ToggleContextMenu(bool use_cursor_position) {
// If menu is already open, close it instead of opening a new one
if (context_menu_widget_qt_) {
context_menu_widget_qt_->close();
context_menu_widget_qt_ = nullptr;
if (context_menu_) {
context_menu_->CloseMenu();
context_menu_ = nullptr;
return;
}
// Show context menu
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (qt_window) {
// Get input system for gamepad navigation support
auto input_sys = emulator()->input_system();
// Get input system for gamepad navigation support
auto input_sys = emulator()->input_system();
// Create new menu widget each time - just like notification widget
auto* context_menu =
new ContextMenuWidgetQt(qt_window->qwindow(), input_sys);
context_menu_widget_qt_ = context_menu;
// Create new ImGui context menu
auto* context_menu = new ui::ImGuiContextMenu(imgui_drawer(), input_sys);
context_menu_ = context_menu;
context_menu->AddAction(
window_->IsFullscreen() ? "Exit Fullscreen" : "Fullscreen",
[this]() { ToggleFullscreen(); }, "F11");
// Set callback to clear our pointer when menu closes
context_menu->SetOnCloseCallback([this]() { context_menu_ = nullptr; });
context_menu->AddSeparator();
context_menu->AddAction(
window_->IsFullscreen() ? "Exit Fullscreen" : "Fullscreen",
[this]() { ToggleFullscreen(); }, "F11");
context_menu->AddAction(
"Post-Processing...", [this]() { ToggleDisplayConfigDialog(); }, "F6");
context_menu->AddSeparator();
context_menu->AddAction(
"Performance Settings...",
[this]() { TogglePerformanceTuningDialog(); }, "F7");
context_menu->AddAction(
"Post-Processing...", [this]() { ToggleDisplayConfigDialog(); }, "F6");
// Get current vibration state
bool vibration_enabled = false;
if (input_sys) {
vibration_enabled = input_sys->GetVibrationCvar();
}
context_menu->AddAction(
"Performance Settings...", [this]() { TogglePerformanceTuningDialog(); },
"F7");
QString vibration_text =
QString("Vibration: %1").arg(vibration_enabled ? "On" : "Off");
context_menu->AddAction(vibration_text,
[this]() { ToggleControllerVibration(); });
context_menu->AddAction("Controller Hotkeys...",
[this]() { ToggleControllerHotkeysDialog(); });
context_menu->AddSeparator();
context_menu->AddAction(
"Take Screenshot", [this]() { TakeScreenshot(); }, "F12");
context_menu->AddAction("Profiles Menu",
[this]() { ToggleProfilesConfigDialog(); });
context_menu->AddAction("XMP Audio Player",
[this]() { ToggleXMPConfigDialog(); });
context_menu->AddSeparator();
context_menu->AddAction("Quit Game", [this, qt_window, input_sys]() {
if (ConfirmDialogWidgetQt::Confirm(
qt_window->qwindow(), input_sys, "Quit Game",
"Are you sure you want to quit?\n\nAny unsaved progress will be "
"lost.")) {
window_->RequestClose();
}
});
// Show menu at cursor position or center of window
QPoint global_pos;
if (use_cursor_position) {
global_pos = QCursor::pos();
} else {
// Center of window
QWidget* qwindow = qt_window->qwindow();
global_pos = qwindow->mapToGlobal(
QPoint(qwindow->width() / 2, qwindow->height() / 2));
}
context_menu->ShowAt(global_pos);
// Get current vibration state
bool vibration_enabled = false;
if (input_sys) {
vibration_enabled = input_sys->GetVibrationCvar();
}
std::string vibration_text =
std::string("Vibration: ") + (vibration_enabled ? "On" : "Off");
context_menu->AddAction(vibration_text,
[this]() { ToggleControllerVibration(); });
context_menu->AddAction("Controller Hotkeys...",
[this]() { ToggleControllerHotkeysDialog(); });
context_menu->AddSeparator();
context_menu->AddAction(
"Take Screenshot", [this]() { TakeScreenshot(); }, "F12");
context_menu->AddAction("Profiles Menu",
[this]() { ToggleProfilesConfigDialog(); });
context_menu->AddAction("XMP Audio Player",
[this]() { ToggleXMPConfigDialog(); });
context_menu->AddSeparator();
context_menu->AddAction("Quit Game", [this, input_sys]() {
new ui::ImGuiConfirmDialog(
imgui_drawer(), "Quit Game",
"Are you sure you want to quit?\n\nAny unsaved progress will be lost.",
[this](bool confirmed) {
if (confirmed) {
window_->RequestClose();
}
},
input_sys);
});
// Show menu centered (ImGui handles positioning)
context_menu->Show();
}
void EmulatorWindow::OnMouseUp(const ui::MouseEvent& e) {
@@ -1243,12 +1253,9 @@ void EmulatorWindow::ExportScreenshot(const xe::ui::RawImage& image) {
fmt::format("Screenshot saved: {}", filename);
app_context_.CallInUIThread([this, notification_text]() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (qt_window) {
auto* notification =
new NotificationWidgetQt(qt_window->qwindow(), "Screenshot Created!",
SafeQString(notification_text), 3000);
notification->Show();
if (imgui_drawer()) {
new ui::HostNotificationWindow(imgui_drawer(), "Screenshot Created!",
notification_text, 0);
}
});
}
@@ -1658,56 +1665,46 @@ void EmulatorWindow::ToggleFullscreen() {
}
void EmulatorWindow::ToggleDisplayConfigDialog() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (!qt_window) {
if (postprocessing_dialog_) {
postprocessing_dialog_->CloseDialog();
postprocessing_dialog_ = nullptr;
return;
}
if (postprocessing_dialog_qt_) {
postprocessing_dialog_qt_->close();
return;
}
postprocessing_dialog_qt_ = new PostProcessingDialogQt(
qt_window->qwindow(), this, emulator()->input_system());
postprocessing_dialog_qt_->show();
postprocessing_dialog_qt_->raise();
postprocessing_dialog_qt_->activateWindow();
postprocessing_dialog_ = new ui::ImGuiPostProcessingDialog(
imgui_drawer(), this, emulator()->input_system());
postprocessing_dialog_->SetOnCloseCallback(
[this]() { postprocessing_dialog_ = nullptr; });
}
void EmulatorWindow::TogglePerformanceTuningDialog() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (!qt_window) {
if (performance_dialog_) {
performance_dialog_->CloseDialog();
performance_dialog_ = nullptr;
return;
}
if (performance_tuning_dialog_qt_) {
performance_tuning_dialog_qt_->close();
return;
}
performance_tuning_dialog_qt_ = new PerformanceTuningDialogQt(
qt_window->qwindow(), this, emulator()->input_system());
performance_tuning_dialog_qt_->show();
performance_tuning_dialog_qt_->raise();
performance_tuning_dialog_qt_->activateWindow();
performance_dialog_ = new ui::ImGuiPerformanceDialog(
imgui_drawer(), this, emulator()->input_system());
performance_dialog_->SetOnCloseCallback(
[this]() { performance_dialog_ = nullptr; });
}
void EmulatorWindow::ToggleProfilesConfigDialog() {
if (!profile_dialog_qt_) {
profile_dialog_qt_ =
new ProfileDialogQt(nullptr, this, emulator()->input_system());
// Refresh game list icons when profile dialog closes
QObject::connect(profile_dialog_qt_, &QDialog::finished, [this]() {
if (game_list_dialog_qt_) {
game_list_dialog_qt_->RefreshIcons();
}
});
if (profile_dialog_) {
profile_dialog_->CloseDialog();
return;
}
profile_dialog_qt_->show();
profile_dialog_qt_->raise();
profile_dialog_qt_->activateWindow();
profile_dialog_ =
new ProfileConfigDialog(imgui_drawer(), this, emulator()->input_system());
profile_dialog_->SetOnCloseCallback([this]() {
profile_dialog_ = nullptr;
// Refresh game list icons when profile dialog closes
if (game_list_dialog_qt_) {
game_list_dialog_qt_->RefreshIcons();
}
});
}
void EmulatorWindow::ToggleConfigDialog() {
@@ -1737,21 +1734,15 @@ void EmulatorWindow::OpenConfigDialog(const std::string& category) {
}
void EmulatorWindow::ToggleXMPConfigDialog() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (!qt_window) {
if (xmp_dialog_) {
xmp_dialog_->CloseDialog();
xmp_dialog_ = nullptr;
return;
}
if (xmp_dialog_qt_) {
xmp_dialog_qt_->close();
return;
}
xmp_dialog_qt_ =
new XmpDialogQt(qt_window->qwindow(), this, emulator()->input_system());
xmp_dialog_qt_->show();
xmp_dialog_qt_->raise();
xmp_dialog_qt_->activateWindow();
xmp_dialog_ =
new ui::ImGuiXmpDialog(imgui_drawer(), this, emulator()->input_system());
xmp_dialog_->SetOnCloseCallback([this]() { xmp_dialog_ = nullptr; });
}
void EmulatorWindow::ToggleControllerVibration() {
@@ -1772,15 +1763,13 @@ void EmulatorWindow::ToggleControllerVibration() {
// Show notification
bool vibration_enabled = input_sys->GetVibrationCvar();
QString status = vibration_enabled ? "On" : "Off";
std::string status = vibration_enabled ? "On" : "Off";
app_context_.CallInUIThread([this, status]() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (qt_window) {
auto* notification = new NotificationWidgetQt(
qt_window->qwindow(), "Controller Vibration",
QString("Vibration is now %1").arg(status), 2000);
notification->Show();
if (imgui_drawer()) {
new ui::HostNotificationWindow(
imgui_drawer(), "Controller Vibration",
fmt::format("Vibration is now {}", status), 0);
}
});
}
@@ -2169,12 +2158,9 @@ EmulatorWindow::ControllerHotKey EmulatorWindow::ProcessControllerHotkey(
if (!notificationTitle.empty()) {
app_context_.CallInUIThread([this, notificationTitle, notificationDesc]() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (qt_window) {
auto* notification = new NotificationWidgetQt(
qt_window->qwindow(), SafeQString(notificationTitle),
SafeQString(notificationDesc), 3000);
notification->Show();
if (imgui_drawer()) {
new ui::HostNotificationWindow(imgui_drawer(), notificationTitle,
notificationDesc, 0);
}
});
}
@@ -2313,21 +2299,15 @@ void EmulatorWindow::CycleReadbackResolve() {
}
void EmulatorWindow::ToggleControllerHotkeysDialog() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
if (!qt_window) {
if (controller_hotkeys_dialog_) {
controller_hotkeys_dialog_->CloseDialog();
return;
}
if (controller_hotkeys_dialog_qt_) {
controller_hotkeys_dialog_qt_->close();
return;
}
controller_hotkeys_dialog_qt_ = new ControllerHotkeysDialogQt(
qt_window->qwindow(), this, emulator()->input_system());
controller_hotkeys_dialog_qt_->show();
controller_hotkeys_dialog_qt_->raise();
controller_hotkeys_dialog_qt_->activateWindow();
controller_hotkeys_dialog_ = new ui::ImGuiControllerHotkeysDialog(
imgui_drawer(), this, emulator()->input_system());
controller_hotkeys_dialog_->SetOnCloseCallback(
[this]() { controller_hotkeys_dialog_ = nullptr; });
}
std::vector<std::pair<std::string, bool>>
@@ -2546,10 +2526,6 @@ xe::X_STATUS EmulatorWindow::RunTitle(
disable_hotkeys_ = false;
if (postprocessing_dialog_qt_) {
postprocessing_dialog_qt_->deleteLater();
}
ClearDialogs();
if (result) {
@@ -2698,8 +2674,25 @@ void EmulatorWindow::AddRecentlyLaunchedTitle(
}
void EmulatorWindow::ClearDialogs() {
if (postprocessing_dialog_qt_) {
postprocessing_dialog_qt_->deleteLater();
if (postprocessing_dialog_) {
postprocessing_dialog_->CloseDialog();
postprocessing_dialog_ = nullptr;
}
if (performance_dialog_) {
performance_dialog_->CloseDialog();
performance_dialog_ = nullptr;
}
if (controller_hotkeys_dialog_) {
controller_hotkeys_dialog_->CloseDialog();
controller_hotkeys_dialog_ = nullptr;
}
if (xmp_dialog_) {
xmp_dialog_->CloseDialog();
xmp_dialog_ = nullptr;
}
if (profile_dialog_) {
profile_dialog_->CloseDialog();
profile_dialog_ = nullptr;
}
imgui_drawer_.get()->ClearDialogs();
+12 -6
View File
@@ -19,8 +19,14 @@ class QTimer;
#include "xenia/emulator.h"
#include "xenia/gpu/command_processor.h"
#include "xenia/ui/imgui_confirm_dialog.h"
#include "xenia/ui/imgui_context_menu.h"
#include "xenia/ui/imgui_controller_hotkeys_dialog.h"
#include "xenia/ui/imgui_dialog.h"
#include "xenia/ui/imgui_drawer.h"
#include "xenia/ui/imgui_performance_dialog.h"
#include "xenia/ui/imgui_postprocessing_dialog.h"
#include "xenia/ui/imgui_xmp_dialog.h"
#include "xenia/ui/immediate_drawer.h"
#include "xenia/ui/menu_item.h"
#include "xenia/ui/presenter.h"
@@ -271,15 +277,15 @@ class EmulatorWindow {
// Disc number after disc swap (0 = use XEX header value)
uint8_t swapped_disc_number_ = 0;
QPointer<class PostProcessingDialogQt> postprocessing_dialog_qt_;
QPointer<class PerformanceTuningDialogQt> performance_tuning_dialog_qt_;
QPointer<class ControllerHotkeysDialogQt> controller_hotkeys_dialog_qt_;
ui::ImGuiPostProcessingDialog* postprocessing_dialog_ = nullptr;
ui::ImGuiPerformanceDialog* performance_dialog_ = nullptr;
ui::ImGuiControllerHotkeysDialog* controller_hotkeys_dialog_ = nullptr;
QPointer<class GameListDialogQt> game_list_dialog_qt_;
QPointer<class ProfileDialogQt> profile_dialog_qt_;
ProfileConfigDialog* profile_dialog_ = nullptr;
QPointer<class SimpleConfigDialogQt> simple_config_dialog_qt_;
QPointer<class ConfigDialogQt> config_dialog_qt_;
QPointer<class ContextMenuWidgetQt> context_menu_widget_qt_;
QPointer<class XmpDialogQt> xmp_dialog_qt_;
ui::ImGuiContextMenu* context_menu_ = nullptr;
ui::ImGuiXmpDialog* xmp_dialog_ = nullptr;
std::vector<RecentTitleEntry> recently_launched_titles_;
+11 -11
View File
@@ -14,8 +14,8 @@
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/kernel/xam/achievement_backends/gpd_achievement_backend.h"
#include "xenia/kernel/xam/xdbf/gpd_info.h"
#include "xenia/ui/notification_widget_qt.h"
#include "xenia/ui/window_qt.h"
#include "xenia/ui/audio_helper_qt.h"
#include "xenia/ui/imgui_guest_notification.h"
DEFINE_bool(show_achievement_notification, true,
"Show achievement notification on screen.", "UI");
@@ -149,16 +149,16 @@ void AchievementManager::ShowAchievementEarnedNotification(
const Emulator* emulator = kernel_state()->emulator();
ui::WindowedAppContext& app_context =
emulator->display_window()->app_context();
ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer();
app_context.CallInUIThread([emulator, description]() {
auto* qt_window = dynamic_cast<ui::QtWindow*>(emulator->display_window());
if (qt_window) {
auto* notification = new app::NotificationWidgetQt(
qt_window->qwindow(), QString("Achievement unlocked"),
QString::fromUtf8(description.c_str()), 4500,
true); // is_achievement = true, plays sound if configured
notification->Show();
}
app_context.CallInUIThread([imgui_drawer, description]() {
// Play achievement sound
ui::AudioHelperQt::Instance().PlayAchievementSound();
// Show notification
new ui::AchievementNotificationWindow(
imgui_drawer, "Achievement unlocked", description, 0,
kernel_state()->notification_position_);
});
}
+88 -9
View File
@@ -234,7 +234,26 @@ X_RESULT xeXamDispatchHeadlessAsync(std::function<void()> run_callback) {
return X_ERROR_SUCCESS;
}
void MessageBoxDialog::OnGamepadButtonA() {
chosen_button_ = focused_button_;
Close();
}
void MessageBoxDialog::OnGamepadDPadLeft() {
if (focused_button_ > 0) {
focused_button_--;
}
}
void MessageBoxDialog::OnGamepadDPadRight() {
if (focused_button_ < buttons_.size() - 1) {
focused_button_++;
}
}
void MessageBoxDialog::OnDraw(ImGuiIO& io) {
PollGamepad();
bool first_draw = false;
if (!has_opened_) {
ImGui::OpenPopup(title_.c_str());
@@ -250,11 +269,20 @@ void MessageBoxDialog::OnDraw(ImGuiIO& io) {
ImGui::SetKeyboardFocusHere();
}
for (size_t i = 0; i < buttons_.size(); ++i) {
// Highlight focused button for gamepad navigation
bool is_focused = (i == focused_button_);
if (is_focused) {
ImGui::PushStyleColor(ImGuiCol_Button,
ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
}
if (ImGui::Button(buttons_[i].c_str())) {
chosen_button_ = static_cast<uint32_t>(i);
ImGui::CloseCurrentPopup();
Close();
}
if (is_focused) {
ImGui::PopStyleColor();
}
ImGui::SameLine();
}
ImGui::Spacing();
@@ -265,7 +293,34 @@ void MessageBoxDialog::OnDraw(ImGuiIO& io) {
}
}
void KeyboardInputDialog::OnGamepadButtonA() {
if (focused_button_ == 0) {
// OK
text_ = std::string(text_buffer_.data(), text_buffer_.size());
cancelled_ = false;
} else {
// Cancel
text_ = "";
cancelled_ = true;
}
Close();
}
void KeyboardInputDialog::OnGamepadDPadLeft() {
if (focused_button_ > 0) {
focused_button_--;
}
}
void KeyboardInputDialog::OnGamepadDPadRight() {
if (focused_button_ < 1) {
focused_button_++;
}
}
void KeyboardInputDialog::OnDraw(ImGuiIO& io) {
PollGamepad();
bool first_draw = false;
if (!has_opened_) {
ImGui::OpenPopup(title_.c_str());
@@ -302,19 +357,36 @@ void KeyboardInputDialog::OnDraw(ImGuiIO& io) {
ImGui::CloseCurrentPopup();
Close();
}
// Highlight focused button for gamepad navigation
bool ok_focused = (focused_button_ == 0);
if (ok_focused) {
ImGui::PushStyleColor(ImGuiCol_Button,
ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
}
if (ImGui::Button("OK")) {
text_ = std::string(text_buffer_.data(), text_buffer_.size());
cancelled_ = false;
ImGui::CloseCurrentPopup();
Close();
}
if (ok_focused) {
ImGui::PopStyleColor();
}
ImGui::SameLine();
bool cancel_focused = (focused_button_ == 1);
if (cancel_focused) {
ImGui::PushStyleColor(ImGuiCol_Button,
ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
}
if (ImGui::Button("Cancel")) {
text_ = "";
cancelled_ = true;
ImGui::CloseCurrentPopup();
Close();
}
if (cancel_focused) {
ImGui::PopStyleColor();
}
ImGui::Spacing();
ImGui::EndPopup();
} else {
@@ -361,6 +433,7 @@ static dword_result_t XamShowMessageBoxUi(
const Emulator* emulator = kernel_state()->emulator();
xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer();
xe::hid::InputSystem* input_system = emulator->input_system();
if (flags & XMBox_PASSCODEMODE || flags & XMBox_VERIFYPASSCODEMODE) {
auto close = [result_ptr,
@@ -383,7 +456,7 @@ static dword_result_t XamShowMessageBoxUi(
};
result = xeXamDispatchDialog<MessageBoxDialog>(
new MessageBoxDialog(imgui_drawer, title, text, buttons,
new MessageBoxDialog(imgui_drawer, input_system, title, text, buttons,
static_cast<uint32_t>(active_button)),
close, overlapped);
}
@@ -492,6 +565,7 @@ dword_result_t XamShowKeyboardUI_entry(
};
const Emulator* emulator = kernel_state()->emulator();
xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer();
xe::hid::InputSystem* input_system = emulator->input_system();
std::string title_str = title ? xe::to_utf8(title.value()) : "";
std::string desc_str = description ? xe::to_utf8(description.value()) : "";
@@ -499,8 +573,8 @@ dword_result_t XamShowKeyboardUI_entry(
default_text ? xe::to_utf8(default_text.value()) : "";
result = xeXamDispatchDialogEx<KeyboardInputDialog>(
new KeyboardInputDialog(imgui_drawer, title_str, desc_str, def_text_str,
buffer_length),
new KeyboardInputDialog(imgui_drawer, input_system, title_str, desc_str,
def_text_str, buffer_length),
close, overlapped);
}
return result;
@@ -556,9 +630,10 @@ dword_result_t XamShowDeviceSelectorUI_entry(
const Emulator* emulator = kernel_state()->emulator();
xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer();
xe::hid::InputSystem* input_system = emulator->input_system();
return xeXamDispatchDialog<MessageBoxDialog>(
new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close,
overlapped);
new MessageBoxDialog(imgui_drawer, input_system, title, desc, buttons, 0),
close, overlapped);
}
DECLARE_XAM_EXPORT1(XamShowDeviceSelectorUI, kUI, kImplemented);
@@ -576,8 +651,9 @@ void XamShowDirtyDiscErrorUI_entry(dword_t user_index) {
const Emulator* emulator = kernel_state()->emulator();
xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer();
xe::hid::InputSystem* input_system = emulator->input_system();
xeXamDispatchDialog<MessageBoxDialog>(
new MessageBoxDialog(imgui_drawer, title, desc, {"OK"}, 0),
new MessageBoxDialog(imgui_drawer, input_system, title, desc, {"OK"}, 0),
[](MessageBoxDialog*) -> X_RESULT { return X_ERROR_SUCCESS; }, 0);
// This is death, and should never return.
// TODO(benvanik): cleaner exit.
@@ -745,8 +821,10 @@ dword_result_t XamShowMarketplaceUIEx_entry(dword_t user_index, dword_t ui_type,
const Emulator* emulator = kernel_state()->emulator();
xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer();
xe::hid::InputSystem* input_system = emulator->input_system();
return xeXamDispatchDialogAsync<MessageBoxDialog>(
new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close);
new MessageBoxDialog(imgui_drawer, input_system, title, desc, buttons, 0),
close);
}
DECLARE_XAM_EXPORT1(XamShowMarketplaceUIEx, kUI, kSketchy);
@@ -823,9 +901,10 @@ dword_result_t XamShowMarketplaceDownloadItemsUI_entry(
const Emulator* emulator = kernel_state()->emulator();
xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer();
xe::hid::InputSystem* input_system = emulator->input_system();
return xeXamDispatchDialog<MessageBoxDialog>(
new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close,
overlapped);
new MessageBoxDialog(imgui_drawer, input_system, title, desc, buttons, 0),
close, overlapped);
}
DECLARE_XAM_EXPORT1(XamShowMarketplaceDownloadItemsUI, kUI, kSketchy);
+53 -7
View File
@@ -13,6 +13,13 @@
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/ui/imgui_dialog.h"
#include "xenia/ui/imgui_drawer.h"
#include "xenia/ui/imgui_gamepad_dialog.h"
namespace xe {
namespace hid {
class InputSystem;
} // namespace hid
} // namespace xe
namespace xe {
namespace kernel {
@@ -40,17 +47,43 @@ class XamDialog : public xe::ui::ImGuiDialog {
std::function<void()> close_callback_ = nullptr;
};
class MessageBoxDialog : public XamDialog {
// XamDialog with gamepad support for game-triggered dialogs
class XamGamepadDialog : public xe::ui::ImGuiGamepadDialog {
public:
MessageBoxDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title,
void set_close_callback(std::function<void()> close_callback) {
close_callback_ = close_callback;
}
protected:
XamGamepadDialog(xe::ui::ImGuiDrawer* imgui_drawer,
xe::hid::InputSystem* input_system)
: xe::ui::ImGuiGamepadDialog(imgui_drawer, input_system, true) {}
virtual ~XamGamepadDialog() {};
void OnClose() override {
if (close_callback_) {
close_callback_();
}
}
private:
std::function<void()> close_callback_ = nullptr;
};
class MessageBoxDialog : public XamGamepadDialog {
public:
MessageBoxDialog(xe::ui::ImGuiDrawer* imgui_drawer,
xe::hid::InputSystem* input_system, std::string& title,
std::string& description, std::vector<std::string> buttons,
uint32_t default_button)
: XamDialog(imgui_drawer),
: XamGamepadDialog(imgui_drawer, input_system),
title_(title),
description_(description),
buttons_(std::move(buttons)),
default_button_(default_button),
chosen_button_(default_button) {
chosen_button_(default_button),
focused_button_(default_button) {
if (!title_.size()) {
title_ = "Message Box";
}
@@ -61,6 +94,11 @@ class MessageBoxDialog : public XamDialog {
void OnDraw(ImGuiIO& io) override;
virtual ~MessageBoxDialog() {}
protected:
void OnGamepadButtonA() override;
void OnGamepadDPadLeft() override;
void OnGamepadDPadRight() override;
private:
bool has_opened_ = false;
std::string title_;
@@ -68,14 +106,16 @@ class MessageBoxDialog : public XamDialog {
std::vector<std::string> buttons_;
uint32_t default_button_ = 0;
uint32_t chosen_button_ = 0;
uint32_t focused_button_ = 0;
};
class KeyboardInputDialog : public XamDialog {
class KeyboardInputDialog : public XamGamepadDialog {
public:
KeyboardInputDialog(xe::ui::ImGuiDrawer* imgui_drawer, std::string& title,
KeyboardInputDialog(xe::ui::ImGuiDrawer* imgui_drawer,
xe::hid::InputSystem* input_system, std::string& title,
std::string& description, std::string& default_text,
size_t max_length)
: XamDialog(imgui_drawer),
: XamGamepadDialog(imgui_drawer, input_system),
title_(title),
description_(description),
default_text_(default_text),
@@ -101,6 +141,11 @@ class KeyboardInputDialog : public XamDialog {
void OnDraw(ImGuiIO& io) override;
protected:
void OnGamepadButtonA() override;
void OnGamepadDPadLeft() override;
void OnGamepadDPadRight() override;
private:
bool has_opened_ = false;
std::string title_;
@@ -110,6 +155,7 @@ class KeyboardInputDialog : public XamDialog {
std::vector<char> text_buffer_;
std::string text_ = "";
bool cancelled_ = true;
int focused_button_ = 0; // 0 = OK, 1 = Cancel
};
bool xeDrawProfileContent(xe::ui::ImGuiDrawer* imgui_drawer,
+82
View File
@@ -0,0 +1,82 @@
/**
******************************************************************************
* 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/audio_helper_qt.h"
#include <QUrl>
#include <filesystem>
#include "xenia/base/cvar.h"
#include "xenia/base/string.h"
DECLARE_path(achievement_sound_path);
namespace xe {
namespace ui {
AudioHelperQt& AudioHelperQt::Instance() {
static AudioHelperQt instance;
return instance;
}
AudioHelperQt::AudioHelperQt() : QObject(nullptr) {}
AudioHelperQt::~AudioHelperQt() {
if (media_player_) {
media_player_->stop();
delete media_player_;
media_player_ = nullptr;
}
if (audio_output_) {
delete audio_output_;
audio_output_ = nullptr;
}
}
void AudioHelperQt::InitializeMediaPlayer() {
if (initialized_) {
return;
}
initialized_ = true;
if (cvars::achievement_sound_path.empty()) {
return;
}
std::filesystem::path sound_path = cvars::achievement_sound_path;
if (!std::filesystem::exists(sound_path)) {
return;
}
// Create without parent to avoid interfering with Qt's widget event loop
// This prevents FPS drops when the media player is instantiated
media_player_ = new QMediaPlayer(nullptr);
audio_output_ = new QAudioOutput(nullptr);
media_player_->setAudioOutput(audio_output_);
media_player_->setSource(QUrl::fromLocalFile(
QString::fromStdString(xe::path_to_utf8(sound_path))));
audio_output_->setVolume(1.0);
}
void AudioHelperQt::PlayAchievementSound() {
// Lazy initialization - only create media player when first needed
if (!initialized_) {
InitializeMediaPlayer();
}
if (media_player_) {
// Stop any currently playing sound and restart
media_player_->stop();
media_player_->setPosition(0);
media_player_->play();
}
}
} // namespace ui
} // namespace xe
+50
View File
@@ -0,0 +1,50 @@
/**
******************************************************************************
* 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_AUDIO_HELPER_QT_H_
#define XENIA_UI_AUDIO_HELPER_QT_H_
#include <QAudioOutput>
#include <QMediaPlayer>
#include <QObject>
namespace xe {
namespace ui {
// Singleton audio helper for playing notification sounds.
// Uses QMediaPlayer which doesn't require a QWidget parent,
// so it works with both QWidget and QWindow-based windows.
class AudioHelperQt : public QObject {
Q_OBJECT
public:
static AudioHelperQt& Instance();
// Play the achievement unlock sound if configured
void PlayAchievementSound();
private:
AudioHelperQt();
~AudioHelperQt();
// Non-copyable
AudioHelperQt(const AudioHelperQt&) = delete;
AudioHelperQt& operator=(const AudioHelperQt&) = delete;
void InitializeMediaPlayer();
QMediaPlayer* media_player_ = nullptr;
QAudioOutput* audio_output_ = nullptr;
bool initialized_ = false;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_AUDIO_HELPER_QT_H_
-200
View File
@@ -1,200 +0,0 @@
/**
******************************************************************************
* 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)
: QDialog(parent),
input_system_(input_system),
poll_timer_(nullptr),
focused_index_(0), // Start with "No" focused (safer default)
prev_buttons_(0) {
setWindowTitle(title);
setModal(true);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
// Match theme from postprocessing dialog
setStyleSheet(
"QDialog { background-color: rgb(30, 30, 30); }"
"QLabel { color: #d0d0d0; background: transparent; }");
auto* main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(24, 20, 24, 20);
main_layout->setSpacing(16);
// Message
auto* message_label = new QLabel(message, this);
message_label->setStyleSheet("color: #d0d0d0; font-size: 12px;");
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, &QDialog::reject);
yes_button_ = new QPushButton("Yes", this);
yes_button_->setMinimumSize(90, 32);
yes_button_->setCursor(Qt::PointingHandCursor);
connect(yes_button_, &QPushButton::clicked, this, &QDialog::accept);
button_layout->addStretch();
button_layout->addWidget(no_button_);
button_layout->addWidget(yes_button_);
button_layout->addStretch();
main_layout->addLayout(button_layout);
// 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
}
// Set initial button focus
UpdateFocusedButton(focused_index_);
}
ConfirmDialogWidgetQt::~ConfirmDialogWidgetQt() {
if (poll_timer_) {
poll_timer_->stop();
}
if (input_system_) {
input_system_->RemoveUIInputBlocker();
}
}
bool ConfirmDialogWidgetQt::Confirm(QWidget* parent,
hid::InputSystem* input_system,
const QString& title,
const QString& message) {
ConfirmDialogWidgetQt dialog(parent, input_system, title, message);
return dialog.exec() == QDialog::Accepted;
}
void ConfirmDialogWidgetQt::keyPressEvent(QKeyEvent* event) {
switch (event->key()) {
case Qt::Key_Escape:
reject();
break;
case Qt::Key_Left:
case Qt::Key_Right:
UpdateFocusedButton(focused_index_ == 0 ? 1 : 0);
break;
case Qt::Key_Return:
case Qt::Key_Enter:
if (focused_index_ == 1) {
accept();
} else {
reject();
}
break;
default:
QDialog::keyPressEvent(event);
}
}
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) {
if (focused_index_ == 1) {
accept();
} else {
reject();
}
}
// B button to cancel (same as No)
if (pressed & 0x2000) {
reject();
}
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);
}
}
} // namespace app
} // namespace xe
-63
View File
@@ -1,63 +0,0 @@
/**
******************************************************************************
* 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 <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QTimer>
#include <functional>
namespace xe {
namespace hid {
class InputSystem;
} // namespace hid
} // namespace xe
namespace xe {
namespace app {
// Confirmation dialog with gamepad support
class ConfirmDialogWidgetQt : public QDialog {
Q_OBJECT
public:
ConfirmDialogWidgetQt(QWidget* parent, hid::InputSystem* input_system,
const QString& title, const QString& message);
~ConfirmDialogWidgetQt() override;
// Static blocking method - returns true if confirmed
static bool Confirm(QWidget* parent, hid::InputSystem* input_system,
const QString& title, const QString& message);
protected:
void keyPressEvent(QKeyEvent* event) override;
private slots:
void PollGamepad();
private:
void UpdateFocusedButton(int index);
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_
@@ -1,155 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/ui/controller_hotkeys_dialog_qt.h"
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPushButton>
#include <QStyle>
#include "xenia/app/emulator_window.h"
namespace xe {
namespace app {
ControllerHotkeysDialogQt::ControllerHotkeysDialogQt(
QWidget* parent, EmulatorWindow* emulator_window,
hid::InputSystem* input_system)
: ui::GamepadDialog(parent, input_system),
emulator_window_(emulator_window) {
SetupUI();
// Position near top, centered horizontally
if (parent) {
QPoint parent_pos = parent->mapToGlobal(QPoint(0, 0));
int center_x = parent_pos.x() + (parent->width() - width()) / 2;
move(center_x, parent_pos.y() + 20);
}
}
ControllerHotkeysDialogQt::~ControllerHotkeysDialogQt() = default;
void ControllerHotkeysDialogQt::keyPressEvent(QKeyEvent* event) {
if (event->key() == Qt::Key_Escape) {
close();
return;
}
QDialog::keyPressEvent(event);
}
void ControllerHotkeysDialogQt::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
QWidget* child = childAt(event->position().toPoint());
if (!child || child == this) {
drag_position_ =
event->globalPosition().toPoint() - frameGeometry().topLeft();
dragging_ = true;
event->accept();
return;
}
}
QDialog::mousePressEvent(event);
}
void ControllerHotkeysDialogQt::mouseMoveEvent(QMouseEvent* event) {
if (dragging_ && (event->buttons() & Qt::LeftButton)) {
move(event->globalPosition().toPoint() - drag_position_);
event->accept();
return;
}
QDialog::mouseMoveEvent(event);
}
void ControllerHotkeysDialogQt::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
dragging_ = false;
}
QDialog::mouseReleaseEvent(event);
}
void ControllerHotkeysDialogQt::SetupUI() {
setWindowTitle("Controller Hotkeys");
setModal(false);
setAttribute(Qt::WA_DeleteOnClose);
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint);
setMinimumWidth(400);
setWindowOpacity(0.92);
setStyleSheet(R"(
QDialog {
background-color: rgb(30, 30, 30);
border: 1px solid rgba(100, 100, 100, 180);
padding: 0px;
}
QLabel {
color: #d0d0d0;
background-color: transparent;
}
)");
auto* content_layout = new QVBoxLayout(this);
content_layout->setContentsMargins(16, 16, 16, 16);
content_layout->setSpacing(8);
// Top bar with title and close button
auto* top_bar_layout = new QHBoxLayout();
auto* title_label = new QLabel("Controller Hotkeys", this);
title_label->setStyleSheet(
"color: #f0f0f0; font-weight: bold; font-size: 14px;");
top_bar_layout->addWidget(title_label);
top_bar_layout->addStretch();
auto* close_button = new QPushButton(this);
close_button->setIcon(style()->standardIcon(QStyle::SP_TitleBarCloseButton));
close_button->setIconSize(QSize(16, 16));
close_button->setFlat(true);
close_button->setStyleSheet(R"(
QPushButton {
background-color: transparent;
border: none;
min-width: 24px;
max-width: 24px;
min-height: 24px;
max-height: 24px;
padding: 2px;
}
QPushButton:hover {
background-color: rgba(200, 50, 50, 180);
border-radius: 4px;
}
QPushButton:pressed {
background-color: rgba(150, 30, 30, 220);
border-radius: 4px;
}
)");
close_button->setToolTip("Close");
connect(close_button, &QPushButton::clicked, this, &QDialog::close);
top_bar_layout->addWidget(close_button, 0, Qt::AlignTop);
content_layout->addLayout(top_bar_layout);
content_layout->addSpacing(8);
// Get hotkeys from emulator window
auto hotkeys = emulator_window_->GetControllerHotkeysList();
for (const auto& [text, enabled] : hotkeys) {
auto* label = new QLabel(QString::fromStdString(text), this);
if (!enabled) {
label->setStyleSheet("color: #666666;");
}
content_layout->addWidget(label);
}
content_layout->addStretch();
}
} // namespace app
} // namespace xe
@@ -1,49 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_UI_CONTROLLER_HOTKEYS_DIALOG_QT_H_
#define XENIA_UI_CONTROLLER_HOTKEYS_DIALOG_QT_H_
#include <QHBoxLayout>
#include <QLabel>
#include <QVBoxLayout>
#include "xenia/ui/gamepad_dialog_qt.h"
namespace xe {
namespace app {
class EmulatorWindow;
class ControllerHotkeysDialogQt : public ui::GamepadDialog {
Q_OBJECT
public:
ControllerHotkeysDialogQt(QWidget* parent, EmulatorWindow* emulator_window,
hid::InputSystem* input_system);
~ControllerHotkeysDialogQt() override;
protected:
void keyPressEvent(QKeyEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
private:
void SetupUI();
EmulatorWindow* emulator_window_;
QPoint drag_position_;
bool dragging_ = false;
};
} // namespace app
} // namespace xe
#endif // XENIA_UI_CONTROLLER_HOTKEYS_DIALOG_QT_H_
+35 -11
View File
@@ -1297,18 +1297,25 @@ void GameListDialogQt::OnSettingsClicked() {
}
void GameListDialogQt::OnProfileClicked() {
// If logged in, show context menu instead
if (current_profile_xuid_ != 0) {
// Show context menu at the cursor position
QPoint global_pos = QCursor::pos();
QPoint local_pos = profile_button_->mapFromGlobal(global_pos);
OnProfileContextMenu(local_pos);
// Left click always opens profile dialog (Qt version for UI process)
if (profile_dialog_) {
profile_dialog_->raise();
profile_dialog_->activateWindow();
return;
}
// Not logged in, open profile dialog via emulator window
if (emulator_window_) {
emulator_window_->ToggleProfilesConfigDialog();
if (emulator_window_ && emulator_window_->emulator()) {
profile_dialog_ =
new ProfileDialogQt(nullptr, emulator_window_,
emulator_window_->emulator()->input_system());
connect(profile_dialog_, &QDialog::finished, this, [this]() {
profile_dialog_ = nullptr;
UpdateProfileButtonState();
RefreshIcons();
});
profile_dialog_->show();
profile_dialog_->raise();
profile_dialog_->activateWindow();
}
}
@@ -1515,8 +1522,25 @@ void GameListDialogQt::OnProfileContextMenu(const QPoint& pos) {
// Profiles Menu
QAction* profiles_menu_action = context_menu.addAction("Profiles Menu");
connect(profiles_menu_action, &QAction::triggered, [this]() {
if (emulator_window_) {
emulator_window_->ToggleProfilesConfigDialog();
// Use Qt profile dialog directly for UI process
if (profile_dialog_) {
profile_dialog_->raise();
profile_dialog_->activateWindow();
return;
}
if (emulator_window_ && emulator_window_->emulator()) {
profile_dialog_ =
new ProfileDialogQt(nullptr, emulator_window_,
emulator_window_->emulator()->input_system());
connect(profile_dialog_, &QDialog::finished, this, [this]() {
profile_dialog_ = nullptr;
UpdateProfileButtonState();
RefreshIcons();
});
profile_dialog_->show();
profile_dialog_->raise();
profile_dialog_->activateWindow();
}
});
+213
View File
@@ -0,0 +1,213 @@
/**
******************************************************************************
* 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/imgui_confirm_dialog.h"
#include "third_party/imgui/imgui.h"
#include "xenia/hid/input_system.h"
namespace xe {
namespace ui {
ImGuiConfirmDialog::ImGuiConfirmDialog(ImGuiDrawer* drawer,
const std::string& title,
const std::string& message,
Callback callback,
hid::InputSystem* input_system)
: ImGuiDialog(drawer),
title_(title),
message_(message),
callback_(std::move(callback)),
input_system_(input_system) {
if (input_system_) {
input_system_->AddUIInputBlocker();
// Initialize prev_buttons_ to current state so held buttons aren't
// detected as "just pressed" when the dialog opens
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;
}
}
}
}
ImGuiConfirmDialog::~ImGuiConfirmDialog() {
if (input_system_) {
input_system_->RemoveUIInputBlocker();
}
}
void ImGuiConfirmDialog::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
focused_button_ = 0; // No
}
if (pressed & 0x0008) { // D-pad right
focused_button_ = 1; // Yes
}
// A button to confirm selection
if (pressed & 0x1000) {
Confirm(focused_button_ == 1);
}
// B button to cancel (same as No)
if (pressed & 0x2000) {
Confirm(false);
}
prev_buttons_ = buttons;
break;
}
}
}
void ImGuiConfirmDialog::Confirm(bool result) {
if (!callback_invoked_) {
callback_invoked_ = true;
if (callback_) {
callback_(result);
}
Close();
}
}
void ImGuiConfirmDialog::OnDraw(ImGuiIO& io) {
// Poll gamepad input
PollGamepad();
// Open popup on first draw
if (!has_opened_) {
ImGui::OpenPopup(title_.c_str());
has_opened_ = true;
}
// Style the popup
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0.12f, 0.12f, 0.12f, 0.95f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.33f, 0.33f, 0.33f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(24, 20));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
// Center the popup on screen
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
bool is_open = true;
if (ImGui::BeginPopupModal(title_.c_str(), &is_open,
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoMove)) {
// Handle keyboard input
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
Confirm(false);
}
if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
focused_button_ = 0;
}
if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
focused_button_ = 1;
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
Confirm(focused_button_ == 1);
}
// Message text
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.82f, 0.82f, 0.82f, 1.0f));
ImGui::TextWrapped("%s", message_.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Spacing();
// Buttons - centered
float button_width = 90.0f;
float spacing = 12.0f;
float total_width = button_width * 2 + spacing;
float start_x = (ImGui::GetWindowWidth() - total_width) * 0.5f;
ImGui::SetCursorPosX(start_x);
// No button
ImVec4 no_bg = focused_button_ == 0 ? ImVec4(0.27f, 0.27f, 0.27f, 1.0f)
: ImVec4(0.20f, 0.20f, 0.20f, 1.0f);
ImVec4 no_border = focused_button_ == 0
? ImVec4(0.06f, 0.49f, 0.06f, 1.0f) // Xbox green
: ImVec4(0.56f, 0.56f, 0.56f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Button, no_bg);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.27f, 0.27f, 0.27f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.27f, 0.27f, 0.27f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
ImGui::PushStyleColor(ImGuiCol_Border, no_border);
if (ImGui::Button("No", ImVec2(button_width, 32))) {
Confirm(false);
}
if (ImGui::IsItemHovered()) {
focused_button_ = 0;
}
ImGui::PopStyleColor(4);
ImGui::PopStyleVar();
ImGui::SameLine(0, spacing);
// Yes button
ImVec4 yes_bg = focused_button_ == 1 ? ImVec4(0.27f, 0.27f, 0.27f, 1.0f)
: ImVec4(0.20f, 0.20f, 0.20f, 1.0f);
ImVec4 yes_border = focused_button_ == 1
? ImVec4(0.06f, 0.49f, 0.06f, 1.0f) // Xbox green
: ImVec4(0.56f, 0.56f, 0.56f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Button, yes_bg);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.27f, 0.27f, 0.27f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.27f, 0.27f, 0.27f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
ImGui::PushStyleColor(ImGuiCol_Border, yes_border);
if (ImGui::Button("Yes", ImVec2(button_width, 32))) {
Confirm(true);
}
if (ImGui::IsItemHovered()) {
focused_button_ = 1;
}
ImGui::PopStyleColor(4);
ImGui::PopStyleVar();
ImGui::EndPopup();
} else {
// Popup was closed (clicked outside or X button)
Confirm(false);
}
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(2);
}
} // namespace ui
} // namespace xe
+63
View File
@@ -0,0 +1,63 @@
/**
******************************************************************************
* 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_IMGUI_CONFIRM_DIALOG_H_
#define XENIA_UI_IMGUI_CONFIRM_DIALOG_H_
#include <functional>
#include <string>
#include "xenia/ui/imgui_dialog.h"
namespace xe {
namespace hid {
class InputSystem;
} // namespace hid
} // namespace xe
namespace xe {
namespace ui {
// ImGui-based confirmation dialog with gamepad support.
// Uses async callback pattern - the callback is invoked when the user
// confirms or cancels the dialog.
class ImGuiConfirmDialog : public ImGuiDialog {
public:
using Callback = std::function<void(bool confirmed)>;
// Creates a confirm dialog. The callback will be invoked with true if
// the user confirms, false if they cancel.
// If input_system is provided, gamepad input will be supported.
ImGuiConfirmDialog(ImGuiDrawer* drawer, const std::string& title,
const std::string& message, Callback callback,
hid::InputSystem* input_system = nullptr);
~ImGuiConfirmDialog() override;
protected:
void OnDraw(ImGuiIO& io) override;
private:
void PollGamepad();
void Confirm(bool result);
std::string title_;
std::string message_;
Callback callback_;
hid::InputSystem* input_system_;
int focused_button_ = 0; // 0 = No, 1 = Yes
uint16_t prev_buttons_ = 0;
bool has_opened_ = false;
bool callback_invoked_ = false;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_IMGUI_CONFIRM_DIALOG_H_
+266
View File
@@ -0,0 +1,266 @@
/**
******************************************************************************
* 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/imgui_context_menu.h"
#include <cfloat>
#include "third_party/imgui/imgui.h"
#include "xenia/hid/input_system.h"
namespace xe {
namespace ui {
ImGuiContextMenu::ImGuiContextMenu(ImGuiDrawer* drawer,
hid::InputSystem* input_system)
: ImGuiDialog(drawer), input_system_(input_system) {
if (input_system_) {
input_system_->AddUIInputBlocker();
// Initialize prev_buttons_ to current state so held buttons aren't
// detected as "just pressed" when the menu opens
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;
}
}
}
}
ImGuiContextMenu::~ImGuiContextMenu() {
if (input_system_) {
input_system_->RemoveUIInputBlocker();
}
}
void ImGuiContextMenu::OnClose() {
if (on_close_callback_) {
on_close_callback_();
}
// Defer action callback to execute after the current frame completes
// This prevents issues with operations like fullscreen that need the
// ImGui state to be fully cleaned up first
if (pending_callback_) {
imgui_drawer()->PostDeferredCallback(std::move(pending_callback_));
}
}
void ImGuiContextMenu::AddAction(const std::string& text,
std::function<void()> callback,
const std::string& shortcut) {
items_.push_back({text, shortcut, callback, false});
}
void ImGuiContextMenu::AddSeparator() {
items_.push_back({"", "", nullptr, true});
}
void ImGuiContextMenu::Show() {
center_on_screen_ = true;
// Find first selectable item
focused_index_ = GetNextSelectableItem(-1, 1);
}
void ImGuiContextMenu::ShowAt(float x, float y) {
center_on_screen_ = false;
position_x_ = x;
position_y_ = y;
// Find first selectable item
focused_index_ = GetNextSelectableItem(-1, 1);
}
int ImGuiContextMenu::GetNextSelectableItem(int current, int direction) {
if (items_.empty()) return -1;
int count = static_cast<int>(items_.size());
int next = current;
for (int i = 0; i < count; i++) {
next += direction;
if (next < 0) next = count - 1;
if (next >= count) next = 0;
if (!items_[next].is_separator) {
return next;
}
}
return current; // No selectable items found
}
void ImGuiContextMenu::PollGamepad() {
if (!input_system_ || items_.empty()) {
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 navigation
if (pressed & 0x0001) { // D-pad up
focused_index_ = GetNextSelectableItem(focused_index_, -1);
}
if (pressed & 0x0002) { // D-pad down
focused_index_ = GetNextSelectableItem(focused_index_, 1);
}
// A button is handled by ImGui's nav system via the Selectable
// B button, Back button, or Guide button to close
if (pressed & (0x2000 | 0x0020 | 0x0400)) {
Close();
}
prev_buttons_ = buttons;
break;
}
}
}
void ImGuiContextMenu::ActivateItem(int index) {
if (index >= 0 && index < static_cast<int>(items_.size()) &&
!items_[index].is_separator && items_[index].callback) {
// Store callback to execute after dialog is fully closed
pending_callback_ = items_[index].callback;
Close();
}
}
void ImGuiContextMenu::OnDraw(ImGuiIO& io) {
// Poll gamepad input
PollGamepad();
// Open popup on first draw
if (!has_opened_) {
ImGui::OpenPopup("##ContextMenu");
has_opened_ = true;
}
// Style the popup - white background, black text
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.7f, 0.7f, 0.7f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 4));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
// Position the popup
if (center_on_screen_) {
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
} else {
ImGui::SetNextWindowPos(ImVec2(position_x_, position_y_),
ImGuiCond_Appearing);
}
// Set minimum width for the menu (30% wider than default)
ImGui::SetNextWindowSizeConstraints(ImVec2(280, 0), ImVec2(FLT_MAX, FLT_MAX));
bool is_open = true;
if (ImGui::BeginPopupModal(
"##ContextMenu", &is_open,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
// Handle keyboard input
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
ImGui::CloseCurrentPopup();
Close();
}
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
focused_index_ = GetNextSelectableItem(focused_index_, -1);
}
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
focused_index_ = GetNextSelectableItem(focused_index_, 1);
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
ActivateItem(focused_index_);
}
// Draw menu items
for (int i = 0; i < static_cast<int>(items_.size()); i++) {
const auto& item = items_[i];
if (item.is_separator) {
ImGui::PushStyleColor(ImGuiCol_Separator,
ImVec4(0.8f, 0.8f, 0.8f, 1.0f));
ImGui::Separator();
ImGui::PopStyleColor();
continue;
}
bool is_focused = (i == focused_index_);
// Item background - Xbox green (#107C10) for highlight
const ImVec4 xbox_green(0.063f, 0.486f, 0.063f, 1.0f);
if (is_focused) {
ImGui::PushStyleColor(ImGuiCol_Header, xbox_green);
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, xbox_green);
ImGui::PushStyleColor(ImGuiCol_HeaderActive, xbox_green);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
} else {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, xbox_green);
ImGui::PushStyleColor(ImGuiCol_HeaderActive, xbox_green);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
}
// Create selectable item
ImGui::PushID(i);
if (ImGui::Selectable("##item", is_focused,
ImGuiSelectableFlags_SpanAllColumns,
ImVec2(0, 0))) {
ActivateItem(i);
}
// Update focus on hover
if (ImGui::IsItemHovered()) {
focused_index_ = i;
}
// Draw text on same line
ImGui::SameLine();
ImGui::SetCursorPosX(12);
ImGui::TextUnformatted(item.text.c_str());
// Draw shortcut if present
if (!item.shortcut.empty()) {
float shortcut_width =
ImGui::CalcTextSize(item.shortcut.c_str()).x + 12;
float window_width = ImGui::GetWindowWidth();
ImGui::SameLine(window_width - shortcut_width);
// Lighter text for shortcut, white if focused for contrast on green
ImGui::PushStyleColor(ImGuiCol_Text,
is_focused ? ImVec4(0.85f, 0.85f, 0.85f, 1.0f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
ImGui::TextUnformatted(item.shortcut.c_str());
ImGui::PopStyleColor();
}
ImGui::PopID();
ImGui::PopStyleColor(4);
}
ImGui::EndPopup();
} else {
// Popup was closed (clicked outside)
Close();
}
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(3);
}
} // namespace ui
} // namespace xe
+89
View File
@@ -0,0 +1,89 @@
/**
******************************************************************************
* 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_IMGUI_CONTEXT_MENU_H_
#define XENIA_UI_IMGUI_CONTEXT_MENU_H_
#include <functional>
#include <string>
#include <vector>
#include "xenia/ui/imgui_dialog.h"
namespace xe {
namespace hid {
class InputSystem;
} // namespace hid
} // namespace xe
namespace xe {
namespace ui {
// ImGui-based context menu with gamepad support.
// This is a modal popup that blocks game input while open.
class ImGuiContextMenu : public ImGuiDialog {
public:
struct MenuItem {
std::string text;
std::string shortcut;
std::function<void()> callback;
bool is_separator;
};
// Creates a context menu. If input_system is provided, gamepad input
// will be supported and game input will be blocked while menu is open.
ImGuiContextMenu(ImGuiDrawer* drawer, hid::InputSystem* input_system);
~ImGuiContextMenu() override;
// Add a menu item with optional keyboard shortcut hint
void AddAction(const std::string& text, std::function<void()> callback,
const std::string& shortcut = "");
// Add a visual separator line
void AddSeparator();
// Show the menu centered on screen
void Show();
// Show the menu at a specific position (in screen coordinates)
void ShowAt(float x, float y);
// Close the menu programmatically
void CloseMenu() { Close(); }
// Set a callback to be invoked when the menu closes
void SetOnCloseCallback(std::function<void()> callback) {
on_close_callback_ = std::move(callback);
}
protected:
void OnClose() override;
void OnDraw(ImGuiIO& io) override;
private:
void PollGamepad();
void ActivateItem(int index);
int GetNextSelectableItem(int current, int direction);
std::vector<MenuItem> items_;
hid::InputSystem* input_system_;
std::function<void()> on_close_callback_;
std::function<void()> pending_callback_; // Callback to execute after close
int focused_index_ = 0;
uint16_t prev_buttons_ = 0;
bool has_opened_ = false;
bool center_on_screen_ = true;
float position_x_ = 0;
float position_y_ = 0;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_IMGUI_CONTEXT_MENU_H_
@@ -0,0 +1,96 @@
/**
******************************************************************************
* 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/imgui_controller_hotkeys_dialog.h"
#include "third_party/imgui/imgui.h"
#include "xenia/app/emulator_window.h"
namespace xe {
namespace ui {
ImGuiControllerHotkeysDialog::ImGuiControllerHotkeysDialog(
ImGuiDrawer* drawer, app::EmulatorWindow* emulator_window,
hid::InputSystem* input_system)
: ImGuiGamepadDialog(drawer, input_system),
emulator_window_(emulator_window) {}
void ImGuiControllerHotkeysDialog::OnClose() {
if (on_close_callback_) {
on_close_callback_();
}
}
void ImGuiControllerHotkeysDialog::OnDraw(ImGuiIO& io) {
PollGamepad();
// Style - white background, black text, Xbox green accents
const ImVec4 xbox_green(0.063f, 0.486f, 0.063f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.7f, 0.7f, 0.7f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBg, xbox_green);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, xbox_green);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 0.0f, 0.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.8f, 0.8f, 0.8f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16, 16));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
// Center on screen
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
bool is_open = true;
if (ImGui::Begin("Controller Hotkeys", &is_open,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse)) {
// Handle keyboard
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
Close();
}
// Colors for content
ImVec4 disabled_text = ImVec4(0.6f, 0.6f, 0.6f, 1.0f);
// Title
ImGui::PushStyleColor(ImGuiCol_Text, xbox_green);
ImGui::Text("Available Hotkeys");
ImGui::PopStyleColor();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Get hotkeys from emulator window
auto hotkeys = emulator_window_->GetControllerHotkeysList();
for (const auto& [text, enabled] : hotkeys) {
if (!enabled) {
ImGui::PushStyleColor(ImGuiCol_Text, disabled_text);
}
ImGui::Text("%s", text.c_str());
if (!enabled) {
ImGui::PopStyleColor();
}
}
ImGui::End();
}
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(6);
if (!is_open) {
Close();
}
}
} // namespace ui
} // namespace xe
@@ -0,0 +1,51 @@
/**
******************************************************************************
* 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_IMGUI_CONTROLLER_HOTKEYS_DIALOG_H_
#define XENIA_UI_IMGUI_CONTROLLER_HOTKEYS_DIALOG_H_
#include <functional>
#include "xenia/ui/imgui_gamepad_dialog.h"
namespace xe {
namespace app {
class EmulatorWindow;
} // namespace app
} // namespace xe
namespace xe {
namespace ui {
// ImGui-based controller hotkeys dialog.
class ImGuiControllerHotkeysDialog : public ImGuiGamepadDialog {
public:
ImGuiControllerHotkeysDialog(ImGuiDrawer* drawer,
app::EmulatorWindow* emulator_window,
hid::InputSystem* input_system);
void CloseDialog() { Close(); }
void SetOnCloseCallback(std::function<void()> callback) {
on_close_callback_ = std::move(callback);
}
protected:
void OnClose() override;
void OnDraw(ImGuiIO& io) override;
private:
app::EmulatorWindow* emulator_window_;
std::function<void()> on_close_callback_;
};
} // namespace ui
} // namespace xe
#endif // XENIA_UI_IMGUI_CONTROLLER_HOTKEYS_DIALOG_H_
+12 -1
View File
@@ -126,6 +126,10 @@ void ImGuiDrawer::AddNotification(ImGuiNotification* dialog) {
}
}
notifications_.push_back(dialog);
// Request a paint to start the draw cycle for the new notification
if (presenter_) {
presenter_->RequestUIPaintFromUIThread();
}
}
void ImGuiDrawer::RemoveNotification(ImGuiNotification* dialog) {
@@ -227,6 +231,12 @@ void ImGuiDrawer::SetGuideButtonAction(std::function<void(uint8_t)> func) {
onGuidePressFunction_ = func;
}
void ImGuiDrawer::PostDeferredCallback(std::function<void()> callback) {
if (window_) {
window_->app_context().CallInUIThreadDeferred(std::move(callback));
}
}
std::optional<ImGuiKey> ImGuiDrawer::VirtualKeyToImGuiKey(VirtualKey vkey) {
static const std::map<VirtualKey, ImGuiKey> map = {
{ui::VirtualKey::kTab, ImGuiKey_Tab},
@@ -931,7 +941,8 @@ void ImGuiDrawer::UpdateGamepads() {
uint8_t controller_to_poke = XUserIndexNone;
hid::X_INPUT_STATE gamepad_state;
for (uint8_t i = 0; i < XUserMaxUserCount; i++) {
if (input_system_->GetState(i, 1, &gamepad_state) == X_ERROR_SUCCESS) {
// Use GetStateForUI so ImGui navigation works even when input is blocked
if (input_system_->GetStateForUI(i, 1, &gamepad_state) == X_ERROR_SUCCESS) {
if (gamepad_state.gamepad.buttons != 0) {
controller_to_poke = i;
break;
+3
View File
@@ -94,6 +94,9 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
void LoadInputSystem(hid::InputSystem* input_system);
void SetGuideButtonAction(std::function<void(uint8_t)> func);
// Post a callback to be executed after the current frame completes
void PostDeferredCallback(std::function<void()> callback);
protected:
void OnKeyDown(KeyEvent& e) override;
void OnKeyUp(KeyEvent& e) override;

Some files were not shown because too many files have changed in this diff Show More