mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[UI] Convert to Qt
This commit is contained in:
@@ -51,7 +51,7 @@ jobs:
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
sudo apt-add-repository "deb http://apt.llvm.org/${UBUNTU_BASE}/ llvm-toolchain-${UBUNTU_BASE}-20 main"
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install mesa-vulkan-drivers valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-20 ninja-build spirv-tools
|
||||
sudo apt-get -y install mesa-vulkan-drivers valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-20 ninja-build spirv-tools qt6-base-dev
|
||||
# Verify spirv-opt is available
|
||||
which spirv-opt && spirv-opt --version || echo "Warning: spirv-opt not found in PATH"
|
||||
git submodule update --init --depth=1 -j$(getconf _NPROCESSORS_ONLN) $(grep -oP '(?<=path = )(?!third_party\/(DirectXShaderCompiler|FidelityFX-(CAS|FSR)|premake-(androidndk|export-compile-commands))).+' .gitmodules)
|
||||
@@ -103,6 +103,15 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-vulkan-sdk-
|
||||
|
||||
- name: Cache Qt6
|
||||
id: cache-qt
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: C:\Qt
|
||||
key: ${{ runner.os }}-qt6-6.8.1
|
||||
restore-keys: |
|
||||
${{ runner.os }}-qt6-
|
||||
|
||||
- name: Cache submodules
|
||||
id: cache-submodules
|
||||
uses: actions/cache@v3
|
||||
@@ -133,6 +142,13 @@ jobs:
|
||||
echo "Warning: spirv-opt.exe not found at expected location"
|
||||
}
|
||||
|
||||
# Install Qt6
|
||||
$qtCacheHit = '${{ steps.cache-qt.outputs.cache-hit }}'
|
||||
if ($qtCacheHit -ne 'true') {
|
||||
pip install aqtinstall
|
||||
aqt install-qt windows desktop 6.8.1 win64_msvc2022_64 -O C:\Qt
|
||||
}
|
||||
|
||||
$cacheHit = '${{ steps.cache-submodules.outputs.cache-hit }}'
|
||||
if ($cacheHit -ne 'true') {
|
||||
git submodule update --init --depth=1 -j $env:NUMBER_OF_PROCESSORS (Select-String -CaseSensitive '(?<=path = )(?!third_party\/(FidelityFX-(CAS|FSR)|premake-(androidndk|cmake|export-compile-commands))).+' .gitmodules).Matches.Value
|
||||
@@ -148,8 +164,9 @@ jobs:
|
||||
echo "::error::$file is too small."
|
||||
}
|
||||
}
|
||||
robocopy . build\bin\Windows\Release LICENSE /r:0 /w:0
|
||||
robocopy build\bin\Windows\Release artifacts\xenia_edge xenia_edge.exe xenia_edge.pdb LICENSE /r:0 /w:0
|
||||
robocopy . build\bin\Windows\Release LICENSE /r:0 /w:0
|
||||
# Copy all files and directories (Qt DLLs, plugins, etc.) excluding build artifacts
|
||||
robocopy build\bin\Windows\Release artifacts\xenia_edge /MIR /r:0 /w:0 /XF *.lib *.idb *.exp /XD generic iconengines imageformats networkinformation styles tls
|
||||
If ($LastExitCode -le 7) { echo "LastExitCode = $LastExitCode";$LastExitCode = 0 }
|
||||
- name: Upload xenia edge artifacts
|
||||
if: steps.prepare_artifacts.outcome == 'success'
|
||||
|
||||
@@ -14,6 +14,8 @@ drivers.
|
||||
* Windows 11 SDK version 10.0.22000.0 (for Visual Studio 2022, this or any newer version)
|
||||
* [Python 3.9+ 64-bit](https://www.python.org/downloads/)
|
||||
* Ensure Python is in PATH.
|
||||
* [Vulkan SDK](https://sdk.lunarg.com/sdk/download/latest/windows/vulkan-sdk.exe)
|
||||
* The build script will automatically detect it if installed at `C:\VulkanSDK`
|
||||
|
||||
```
|
||||
git clone https://github.com/has207/xenia-edge.git
|
||||
|
||||
@@ -133,6 +133,7 @@ filter("platforms:Linux")
|
||||
system("linux")
|
||||
toolset("clang")
|
||||
pkg_config.all("gtk+-x11-3.0")
|
||||
pkg_config.all("Qt6Core Qt6Gui Qt6Widgets")
|
||||
links({
|
||||
"stdc++fs",
|
||||
"dl",
|
||||
@@ -225,6 +226,8 @@ filter("platforms:Windows")
|
||||
toolset("msc")
|
||||
buildoptions({
|
||||
"/utf-8", -- 'build correctly on systems with non-Latin codepages'.
|
||||
"/Zc:__cplusplus", -- Enable correct __cplusplus macro value (required for Qt6).
|
||||
"/Zc:preprocessor", -- Enable conformant preprocessor (supports #if in macro args).
|
||||
-- Disable warnings
|
||||
"/wd4201", -- Nameless struct/unions are ok.
|
||||
})
|
||||
@@ -256,6 +259,41 @@ filter("platforms:Windows")
|
||||
"bcrypt",
|
||||
})
|
||||
|
||||
local qt_dir = os.getenv("QT_DIR")
|
||||
if qt_dir then
|
||||
includedirs({
|
||||
path.join(qt_dir, "include"),
|
||||
path.join(qt_dir, "include/QtCore"),
|
||||
path.join(qt_dir, "include/QtGui"),
|
||||
path.join(qt_dir, "include/QtWidgets"),
|
||||
})
|
||||
libdirs({
|
||||
path.join(qt_dir, "lib"),
|
||||
})
|
||||
end
|
||||
|
||||
filter({"platforms:Windows", "configurations:Release"})
|
||||
local qt_dir = os.getenv("QT_DIR")
|
||||
if qt_dir then
|
||||
links({
|
||||
"Qt6Core",
|
||||
"Qt6Gui",
|
||||
"Qt6Widgets",
|
||||
})
|
||||
end
|
||||
|
||||
filter({"platforms:Windows", "configurations:Debug or Checked"})
|
||||
local qt_dir = os.getenv("QT_DIR")
|
||||
if qt_dir then
|
||||
links({
|
||||
"Qt6Cored",
|
||||
"Qt6Guid",
|
||||
"Qt6Widgetsd",
|
||||
})
|
||||
end
|
||||
|
||||
filter("platforms:Windows")
|
||||
|
||||
-- Embed the manifest for things like dependencies and DPI awareness.
|
||||
filter({"platforms:Windows", "kind:ConsoleApp or WindowedApp"})
|
||||
files({
|
||||
|
||||
@@ -43,7 +43,7 @@ project("xenia-app")
|
||||
local_platform_files()
|
||||
files({
|
||||
"../base/main_init_"..platform_suffix..".cc",
|
||||
"../ui/windowed_app_main_"..platform_suffix..".cc",
|
||||
"../ui/windowed_app_main_qt.cc",
|
||||
})
|
||||
|
||||
resincludedirs({
|
||||
@@ -85,6 +85,7 @@ project("xenia-app")
|
||||
files({
|
||||
"main_resources.rc",
|
||||
})
|
||||
linkoptions({"/ENTRY:mainCRTStartup"})
|
||||
|
||||
filter({"architecture:x86_64", "files:../base/main_init_"..platform_suffix..".cc"})
|
||||
vectorextensions("SSE2") -- Disable AVX for main_init_win.cc so our AVX check doesn't use AVX instructions.
|
||||
|
||||
@@ -40,9 +40,11 @@ std::filesystem::path to_path(const std::u16string_view source) {
|
||||
namespace filesystem {
|
||||
|
||||
std::filesystem::path GetExecutablePath() {
|
||||
wchar_t* path;
|
||||
auto error = _get_wpgmptr(&path);
|
||||
return !error ? std::filesystem::path(path) : std::filesystem::path();
|
||||
// Don't use _get_wpgmptr in Qt apps - CRT may not be initialized properly
|
||||
// Use GetModuleFileName directly instead
|
||||
wchar_t buffer[MAX_PATH];
|
||||
DWORD len = GetModuleFileNameW(nullptr, buffer, MAX_PATH);
|
||||
return len > 0 ? std::filesystem::path(buffer) : std::filesystem::path();
|
||||
}
|
||||
|
||||
std::filesystem::path GetExecutableFolder() {
|
||||
|
||||
@@ -51,6 +51,13 @@ project("xenia-gpu-shader-compiler")
|
||||
"../base/console_app_main_"..platform_suffix..".cc",
|
||||
})
|
||||
|
||||
-- Include SPIRV-Tools headers from Vulkan SDK
|
||||
filter("platforms:Windows")
|
||||
includedirs({
|
||||
"$(VULKAN_SDK)/Include",
|
||||
})
|
||||
filter({})
|
||||
|
||||
filter("platforms:Windows")
|
||||
-- Only create the .user file if it doesn't already exist.
|
||||
local user_file = project_root.."/build/xenia-gpu-shader-compiler.vcxproj.user"
|
||||
|
||||
@@ -1068,6 +1068,12 @@ Presenter::PaintResult D3D12Presenter::PaintAndPresentImpl(
|
||||
0, DXGI_PRESENT_RESTART | (paint_context_.swap_chain_allows_tearing
|
||||
? DXGI_PRESENT_ALLOW_TEARING
|
||||
: 0));
|
||||
static bool logged_present = false;
|
||||
if (!logged_present) {
|
||||
XELOGI("D3D12Presenter: First Present() call, result: 0x{:08X}",
|
||||
present_result);
|
||||
logged_present = true;
|
||||
}
|
||||
// Even if presentation has failed, work might have been enqueued anyway
|
||||
// internally before the failure according to Jesse Natalie from the DirectX
|
||||
// Discord server.
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* 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/file_picker.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QString>
|
||||
|
||||
#include "xenia/base/assert.h"
|
||||
#include "xenia/base/filesystem.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/string.h"
|
||||
#include "xenia/ui/window_qt.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
class QtFilePicker : public FilePicker {
|
||||
public:
|
||||
QtFilePicker();
|
||||
~QtFilePicker() override;
|
||||
|
||||
bool Show(Window* parent_window) override;
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
std::unique_ptr<FilePicker> FilePicker::Create() {
|
||||
return std::make_unique<QtFilePicker>();
|
||||
}
|
||||
|
||||
QtFilePicker::QtFilePicker() = default;
|
||||
|
||||
QtFilePicker::~QtFilePicker() = default;
|
||||
|
||||
bool QtFilePicker::Show(Window* parent_window) {
|
||||
QString title;
|
||||
QFileDialog::FileMode file_mode;
|
||||
QFileDialog::AcceptMode accept_mode;
|
||||
|
||||
switch (mode()) {
|
||||
case Mode::kOpen:
|
||||
if (type() == Type::kFile) {
|
||||
title = "Open File";
|
||||
file_mode = QFileDialog::ExistingFile;
|
||||
} else {
|
||||
title = "Open Directory";
|
||||
file_mode = QFileDialog::Directory;
|
||||
}
|
||||
accept_mode = QFileDialog::AcceptOpen;
|
||||
break;
|
||||
case Mode::kSave:
|
||||
title = "Save File";
|
||||
file_mode = QFileDialog::AnyFile;
|
||||
accept_mode = QFileDialog::AcceptSave;
|
||||
break;
|
||||
default:
|
||||
XELOGE("QtFilePicker::Show: Unhandled mode: {}, Type: {}",
|
||||
static_cast<int>(mode()), static_cast<int>(type()));
|
||||
assert_always();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* qt_window =
|
||||
parent_window ? dynamic_cast<QtWindow*>(parent_window) : nullptr;
|
||||
|
||||
// Use static QFileDialog function to avoid Qt container ABI issues on Windows
|
||||
QString initial_dir;
|
||||
if (!initial_directory().empty()) {
|
||||
std::string dir_str = initial_directory().string();
|
||||
initial_dir =
|
||||
QString::fromUtf8(dir_str.c_str(), static_cast<int>(dir_str.size()));
|
||||
}
|
||||
|
||||
QString file_path = QFileDialog::getOpenFileName(
|
||||
qt_window ? qt_window->qwindow() : nullptr, title, initial_dir,
|
||||
QString(), // filter
|
||||
nullptr, // selected filter
|
||||
QFileDialog::DontUseNativeDialog // Use Qt dialog to avoid native API
|
||||
// issues
|
||||
);
|
||||
|
||||
if (!file_path.isEmpty()) {
|
||||
QByteArray utf8_bytes = file_path.toUtf8();
|
||||
std::string file_path_str(utf8_bytes.constData(), utf8_bytes.size());
|
||||
|
||||
std::vector<std::filesystem::path> selected_files;
|
||||
selected_files.push_back(xe::to_path(file_path_str));
|
||||
set_selected_files(selected_files);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
@@ -830,6 +830,9 @@ void ImGuiDrawer::OnKey(KeyEvent& e, bool is_down) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (presenter_) {
|
||||
presenter_->RequestUIPaintFromUIThread();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiDrawer::UpdateMousePosition(float x, float y) {
|
||||
|
||||
@@ -60,7 +60,7 @@ void MenuItem::RemoveChild(MenuItem* child_item) {
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem* MenuItem::child(size_t index) { return children_[index].get(); }
|
||||
MenuItem* MenuItem::child(size_t index) const { return children_[index].get(); }
|
||||
|
||||
void MenuItem::OnSelected() {
|
||||
if (callback_) {
|
||||
|
||||
@@ -57,7 +57,8 @@ class MenuItem {
|
||||
void AddChild(std::unique_ptr<MenuItem> child_item);
|
||||
void AddChild(MenuItemPtr child_item);
|
||||
void RemoveChild(MenuItem* child_item);
|
||||
MenuItem* child(size_t index);
|
||||
MenuItem* child(size_t index) const;
|
||||
size_t child_count() const { return children_.size(); }
|
||||
|
||||
virtual void SetEnabled(bool enabled) {}
|
||||
|
||||
|
||||
@@ -12,7 +12,15 @@ project("xenia-ui")
|
||||
local_platform_files()
|
||||
removefiles({
|
||||
"*_demo.cc",
|
||||
"windowed_app_main_*.cc",
|
||||
-- Exclude old platform-specific implementations in favor of Qt
|
||||
"window_gtk.cc",
|
||||
"window_win.cc",
|
||||
"file_picker_gtk.cc",
|
||||
"file_picker_win.cc",
|
||||
"windowed_app_context_gtk.cc",
|
||||
"windowed_app_context_win.cc",
|
||||
"windowed_app_main_posix.cc",
|
||||
"windowed_app_main_win.cc",
|
||||
})
|
||||
if os.istarget("android") then
|
||||
filter("platforms:Android-*")
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "xenia/ui/window.h"
|
||||
|
||||
#if XE_PLATFORM_WIN32
|
||||
#include "xenia/ui/surface_win.h"
|
||||
#include "xenia/ui/window_win.h"
|
||||
#endif
|
||||
|
||||
@@ -1208,7 +1209,12 @@ void Presenter::UpdateSurfaceMonitorFromUIThread(
|
||||
#if XE_PLATFORM_WIN32
|
||||
HMONITOR surface_new_win32_monitor = nullptr;
|
||||
if (surface_) {
|
||||
HWND hwnd = static_cast<const Win32Window*>(window_)->hwnd();
|
||||
// Get HWND from the surface instead of assuming Win32Window
|
||||
// This supports both Win32Window and QtWindow
|
||||
HWND hwnd = nullptr;
|
||||
if (surface_->GetType() == Surface::kTypeIndex_Win32Hwnd) {
|
||||
hwnd = static_cast<const Win32HwndSurface*>(surface_)->hwnd();
|
||||
}
|
||||
// The HWND may be non-existent if the window has been closed and destroyed
|
||||
// (the HWND, not the xe::ui::Window) already.
|
||||
if (hwnd) {
|
||||
@@ -1382,6 +1388,20 @@ void Presenter::WaitForUITickFromUIThread() {
|
||||
}
|
||||
dxgi_ui_tick_signal_condition_.wait(dxgi_ui_tick_lock);
|
||||
}
|
||||
#elif XE_PLATFORM_LINUX
|
||||
if (!AreUITicksNeededFromUIThread()) {
|
||||
return;
|
||||
}
|
||||
// On Linux, implement timer-based rate limiting at 60 FPS to match game frame
|
||||
// rate.
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
now - linux_ui_tick_last_paint_time_);
|
||||
constexpr auto frame_time = std::chrono::microseconds(16667); // ~60 FPS
|
||||
if (elapsed < frame_time) {
|
||||
std::this_thread::sleep_for(frame_time - elapsed);
|
||||
}
|
||||
linux_ui_tick_last_paint_time_ = std::chrono::steady_clock::now();
|
||||
#endif // XE_PLATFORM
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
@@ -956,6 +957,10 @@ class Presenter {
|
||||
|
||||
std::array<GuestOutputProperties, kGuestOutputMailboxSize>
|
||||
guest_output_properties_;
|
||||
|
||||
#if XE_PLATFORM_LINUX
|
||||
std::chrono::steady_clock::time_point linux_ui_tick_last_paint_time_;
|
||||
#endif
|
||||
// Accessible only by refreshing, whether the last refresh contained an image
|
||||
// rather than being blank.
|
||||
bool guest_output_active_last_refresh_ = false;
|
||||
|
||||
@@ -484,8 +484,6 @@ VirtualKey GTKWindow::TranslateVirtualKey(guint keyval) {
|
||||
return VirtualKey::kSpace;
|
||||
case GDK_KEY_Caps_Lock:
|
||||
return VirtualKey::kCapital;
|
||||
case GDK_KEY_Escape:
|
||||
return VirtualKey::kEscape;
|
||||
case GDK_KEY_F1:
|
||||
return VirtualKey::kF1;
|
||||
case GDK_KEY_F2:
|
||||
@@ -521,6 +519,8 @@ VirtualKey GTKWindow::TranslateVirtualKey(guint keyval) {
|
||||
case GDK_KEY_Pause:
|
||||
case GDK_KEY_Break:
|
||||
return VirtualKey::kPause;
|
||||
case GDK_KEY_Escape:
|
||||
return VirtualKey::kEscape;
|
||||
default:
|
||||
XELOGW("Unhandled key code: {}", keyval);
|
||||
return VirtualKey(keyval);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* 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_WINDOW_QT_H_
|
||||
#define XENIA_UI_WINDOW_QT_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QWidget>
|
||||
|
||||
#include "xenia/ui/menu_item.h"
|
||||
#include "xenia/ui/window.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
class QtWindowedAppContext;
|
||||
|
||||
class QtWindowInternal;
|
||||
|
||||
class QtWindow : public Window {
|
||||
using super = Window;
|
||||
|
||||
public:
|
||||
QtWindow(WindowedAppContext& app_context, const std::string_view title,
|
||||
uint32_t desired_logical_width, uint32_t desired_logical_height);
|
||||
~QtWindow() override;
|
||||
|
||||
QMainWindow* qwindow() const { return qwindow_; }
|
||||
|
||||
void TriggerPaint() { OnPaint(); }
|
||||
|
||||
// Public accessors for event handling from ExternalRenderWidget
|
||||
void OnMouseMoveEvent(QMouseEvent* event);
|
||||
void OnMousePressEvent(QMouseEvent* event);
|
||||
void OnMouseReleaseEvent(QMouseEvent* event);
|
||||
void OnWheelEvent(QWheelEvent* event);
|
||||
|
||||
protected:
|
||||
bool OpenImpl() override;
|
||||
void RequestCloseImpl() override;
|
||||
|
||||
void ApplyNewFullscreen() override;
|
||||
void ApplyNewTitle() override;
|
||||
void ApplyNewMainMenu(MenuItem* old_main_menu) override;
|
||||
void ApplyNewCursorVisibility(
|
||||
CursorVisibility old_cursor_visibility) override;
|
||||
void FocusImpl() override;
|
||||
|
||||
std::unique_ptr<Surface> CreateSurfaceImpl(
|
||||
Surface::TypeFlags allowed_types) override;
|
||||
void RequestPaintImpl() override;
|
||||
|
||||
private:
|
||||
friend class QtWindowInternal;
|
||||
friend class ExternalRenderWidget;
|
||||
|
||||
void HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver);
|
||||
static VirtualKey TranslateVirtualKey(int qt_key);
|
||||
|
||||
void OnCloseEvent(QCloseEvent* event);
|
||||
void OnResizeEvent(QResizeEvent* event);
|
||||
void OnKeyPressEvent(QKeyEvent* event);
|
||||
void OnKeyReleaseEvent(QKeyEvent* event);
|
||||
bool OnEventFilter(QObject* obj, QEvent* event);
|
||||
|
||||
void SetCursorAutoHideTimer();
|
||||
void OnCursorAutoHideTimeout();
|
||||
void UpdateCursorVisibility();
|
||||
|
||||
QMainWindow* qwindow_ = nullptr;
|
||||
QWidget* drawing_widget_ = nullptr;
|
||||
|
||||
bool in_size_update_ = false;
|
||||
|
||||
// Cursor auto-hide support
|
||||
QTimer* cursor_auto_hide_timer_ = nullptr;
|
||||
QPoint cursor_auto_hide_last_pos_;
|
||||
bool cursor_currently_auto_hidden_ = false;
|
||||
};
|
||||
|
||||
class QtMenuItem : public MenuItem {
|
||||
public:
|
||||
QtMenuItem(Type type, const std::string& text, const std::string& hotkey,
|
||||
std::function<void()> callback);
|
||||
~QtMenuItem() override;
|
||||
|
||||
QAction* action() const { return action_; }
|
||||
QMenu* menu() const { return menu_; }
|
||||
|
||||
protected:
|
||||
void OnChildAdded(MenuItem* child_item) override;
|
||||
void OnChildRemoved(MenuItem* child_item) override;
|
||||
|
||||
private:
|
||||
void OnTriggered();
|
||||
|
||||
QAction* action_ = nullptr;
|
||||
QMenu* menu_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_UI_WINDOW_QT_H_
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* 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/windowed_app_context_qt.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMetaObject>
|
||||
#include <QTimer>
|
||||
|
||||
#if XE_PLATFORM_WIN32
|
||||
#include "xenia/base/platform_win.h"
|
||||
#endif
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
QtWindowedAppContext::QtWindowedAppContext(QApplication* app)
|
||||
: app_(app)
|
||||
#if XE_PLATFORM_WIN32
|
||||
,
|
||||
hinstance_(GetModuleHandle(nullptr))
|
||||
#endif
|
||||
{
|
||||
pending_functions_timer_ = new QTimer();
|
||||
pending_functions_timer_->setSingleShot(true);
|
||||
QObject::connect(pending_functions_timer_, &QTimer::timeout,
|
||||
[this]() { ExecutePendingFunctionsFromTimer(); });
|
||||
}
|
||||
|
||||
QtWindowedAppContext::~QtWindowedAppContext() {
|
||||
if (pending_functions_timer_) {
|
||||
pending_functions_timer_->stop();
|
||||
delete pending_functions_timer_;
|
||||
pending_functions_timer_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void QtWindowedAppContext::NotifyUILoopOfPendingFunctions() {
|
||||
// Use QMetaObject::invokeMethod on the timer to safely call from any thread
|
||||
QMetaObject::invokeMethod(
|
||||
pending_functions_timer_, [this]() { StartTimerInternal(); },
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void QtWindowedAppContext::StartTimerInternal() {
|
||||
if (!pending_functions_timer_->isActive()) {
|
||||
pending_functions_timer_->start(0);
|
||||
}
|
||||
}
|
||||
|
||||
void QtWindowedAppContext::PlatformQuitFromUIThread() {
|
||||
if (app_) {
|
||||
app_->quit();
|
||||
}
|
||||
}
|
||||
|
||||
void QtWindowedAppContext::RunMainQtLoop() {
|
||||
if (app_) {
|
||||
app_->exec();
|
||||
}
|
||||
}
|
||||
|
||||
void QtWindowedAppContext::ExecutePendingFunctionsFromTimer() {
|
||||
ExecutePendingFunctionsFromUIThread();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* 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_WINDOWED_APP_CONTEXT_QT_H_
|
||||
#define XENIA_UI_WINDOWED_APP_CONTEXT_QT_H_
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "xenia/base/platform.h"
|
||||
#include "xenia/ui/windowed_app_context.h"
|
||||
|
||||
#if XE_PLATFORM_WIN32
|
||||
#include "xenia/base/platform_win.h"
|
||||
#endif
|
||||
|
||||
class QApplication;
|
||||
class QTimer;
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
class QtWindowedAppContext final : public WindowedAppContext {
|
||||
public:
|
||||
explicit QtWindowedAppContext(QApplication* app);
|
||||
~QtWindowedAppContext();
|
||||
|
||||
#if XE_PLATFORM_WIN32
|
||||
HINSTANCE hinstance() const { return hinstance_; }
|
||||
#endif
|
||||
|
||||
void NotifyUILoopOfPendingFunctions() override;
|
||||
|
||||
void PlatformQuitFromUIThread() override;
|
||||
|
||||
void RunMainQtLoop();
|
||||
|
||||
private:
|
||||
void ExecutePendingFunctionsFromTimer();
|
||||
void StartTimerInternal();
|
||||
|
||||
QApplication* app_;
|
||||
QTimer* pending_functions_timer_ = nullptr;
|
||||
|
||||
#if XE_PLATFORM_WIN32
|
||||
HINSTANCE hinstance_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_UI_WINDOWED_APP_CONTEXT_QT_H_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user