Update 'qt-experimental' to match 'master'
|
@ -28,6 +28,15 @@ os: Visual Studio 2019
|
|||
init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
# build cache
|
||||
cache:
|
||||
- C:\Qt\5.11.2\msvc2017_64 -> .appveyor.yml
|
||||
|
||||
# environment variables
|
||||
environment:
|
||||
QT_DIR: C:\Qt\5.11.2\msvc2017_64
|
||||
QT_STATIC: 1
|
||||
|
||||
install:
|
||||
- cmd: xb setup
|
||||
|
||||
|
|
|
@ -64,3 +64,6 @@
|
|||
[submodule "third_party/DirectXShaderCompiler"]
|
||||
path = third_party/DirectXShaderCompiler
|
||||
url = https://github.com/microsoft/DirectXShaderCompiler.git
|
||||
[submodule "third_party/premake-qt"]
|
||||
path = third_party/premake-qt
|
||||
url = https://github.com/dcourtois/premake-qt.git
|
||||
|
|
|
@ -36,10 +36,16 @@ git:
|
|||
# We handle submodules ourselves in xenia-build setup.
|
||||
submodules: false
|
||||
|
||||
before_install:
|
||||
# Qt
|
||||
- sudo add-apt-repository -y ppa:ubuntu-sdk-team/ppa
|
||||
- sudo apt-get update -yq
|
||||
- sudo apt-get install qt5-default
|
||||
before_script:
|
||||
- export LIBVULKAN_VERSION=1.1.70
|
||||
- export CXX=$CXX_COMPILER
|
||||
- export CC=$C_COMPILER
|
||||
- export QT_DIR=/usr
|
||||
# Dump useful info.
|
||||
- $CXX --version
|
||||
- python3 --version
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
include("tools/build")
|
||||
require("third_party/premake-export-compile-commands/export-compile-commands")
|
||||
require("third_party/premake-qt/qt")
|
||||
|
||||
location(build_root)
|
||||
targetdir(build_bin)
|
||||
|
@ -248,6 +249,7 @@ solution("xenia")
|
|||
include("src/xenia/hid/sdl")
|
||||
include("src/xenia/kernel")
|
||||
include("src/xenia/ui")
|
||||
include("src/xenia/ui/qt")
|
||||
include("src/xenia/ui/spirv")
|
||||
include("src/xenia/ui/vulkan")
|
||||
include("src/xenia/vfs")
|
||||
|
|
|
@ -2,467 +2,204 @@
|
|||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Copyright 2018 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/app/emulator_window.h"
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include <QVulkanWindow>
|
||||
|
||||
#include "third_party/imgui/imgui.h"
|
||||
#include "xenia/apu/xaudio2/xaudio2_audio_system.h"
|
||||
#include "xenia/base/clock.h"
|
||||
#include "xenia/base/cvar.h"
|
||||
#include "xenia/base/debugging.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/platform.h"
|
||||
#include "xenia/base/profiling.h"
|
||||
#include "xenia/base/system.h"
|
||||
#include "xenia/base/threading.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/gpu/graphics_system.h"
|
||||
#include "xenia/ui/file_picker.h"
|
||||
#include "xenia/ui/imgui_dialog.h"
|
||||
#include "xenia/ui/imgui_drawer.h"
|
||||
#include "xenia/gpu/vulkan/vulkan_graphics_system.h"
|
||||
#include "xenia/hid/input_system.h"
|
||||
#include "xenia/hid/xinput/xinput_hid.h"
|
||||
#include "xenia/ui/vulkan/vulkan_instance.h"
|
||||
#include "xenia/ui/vulkan/vulkan_provider.h"
|
||||
|
||||
// Autogenerated by `xb premake`.
|
||||
#include "build/version.h"
|
||||
DEFINE_string(apu, "any", "Audio system. Use: [any, nop, xaudio2]", "General");
|
||||
DEFINE_string(gpu, "any", "Graphics system. Use: [any, vulkan, null]",
|
||||
"General");
|
||||
DEFINE_string(hid, "any", "Input system. Use: [any, nop, winkey, xinput]",
|
||||
"General");
|
||||
|
||||
DECLARE_bool(debug);
|
||||
DEFINE_string(target, "", "Specifies the target .xex or .iso to execute.",
|
||||
"General");
|
||||
DEFINE_bool(fullscreen, false, "Toggles fullscreen", "General");
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
using xe::ui::FileDropEvent;
|
||||
using xe::ui::KeyEvent;
|
||||
using xe::ui::MenuItem;
|
||||
using xe::ui::MouseEvent;
|
||||
using xe::ui::UIEvent;
|
||||
class VulkanWindow : public QVulkanWindow {
|
||||
public:
|
||||
VulkanWindow(gpu::vulkan::VulkanGraphicsSystem* gfx)
|
||||
: graphics_system_(gfx) {}
|
||||
QVulkanWindowRenderer* createRenderer() override;
|
||||
|
||||
const std::string kBaseTitle = "xenia";
|
||||
private:
|
||||
gpu::vulkan::VulkanGraphicsSystem* graphics_system_;
|
||||
};
|
||||
|
||||
EmulatorWindow::EmulatorWindow(Emulator* emulator)
|
||||
: emulator_(emulator),
|
||||
loop_(ui::Loop::Create()),
|
||||
window_(ui::Window::Create(loop_.get(), kBaseTitle)) {
|
||||
base_title_ = kBaseTitle +
|
||||
#ifdef DEBUG
|
||||
#if _NO_DEBUG_HEAP == 1
|
||||
" DEBUG"
|
||||
#else
|
||||
" CHECKED"
|
||||
#endif
|
||||
#endif
|
||||
" (" XE_BUILD_BRANCH "/" XE_BUILD_COMMIT_SHORT "/" XE_BUILD_DATE
|
||||
")";
|
||||
class VulkanRenderer : public QVulkanWindowRenderer {
|
||||
public:
|
||||
VulkanRenderer(VulkanWindow* window,
|
||||
gpu::vulkan::VulkanGraphicsSystem* graphics_system)
|
||||
: window_(window), graphics_system_(graphics_system) {}
|
||||
|
||||
void startNextFrame() override {
|
||||
// Copy the graphics frontbuffer to our backbuffer.
|
||||
//auto swap_state = graphics_system_->swap_state();
|
||||
|
||||
auto cmd = window_->currentCommandBuffer();
|
||||
//auto src = reinterpret_cast<VkImage>(
|
||||
// swap_state->buffer_textures[swap_state->current_buffer]);
|
||||
auto dest = window_->swapChainImage(window_->currentSwapChainImageIndex());
|
||||
auto dest_size = window_->swapChainImageSize();
|
||||
|
||||
VkImageMemoryBarrier barrier;
|
||||
std::memset(&barrier, 0, sizeof(VkImageMemoryBarrier));
|
||||
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
//barrier.image = src;
|
||||
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0,
|
||||
nullptr, 1, &barrier);
|
||||
|
||||
VkImageBlit region;
|
||||
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.srcOffsets[0] = {0, 0, 0};
|
||||
/*region.srcOffsets[1] = {static_cast<int32_t>(swap_state->width),
|
||||
static_cast<int32_t>(swap_state->height), 1};*/
|
||||
|
||||
region.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.dstOffsets[0] = {0, 0, 0};
|
||||
region.dstOffsets[1] = {static_cast<int32_t>(dest_size.width()),
|
||||
static_cast<int32_t>(dest_size.height()), 1};
|
||||
/* vkCmdBlitImage(cmd, src, VK_IMAGE_LAYOUT_GENERAL, dest,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion,
|
||||
VK_FILTER_LINEAR);*/
|
||||
|
||||
//swap_state->pending = false;
|
||||
window_->frameReady();
|
||||
}
|
||||
|
||||
private:
|
||||
gpu::vulkan::VulkanGraphicsSystem* graphics_system_;
|
||||
VulkanWindow* window_;
|
||||
};
|
||||
|
||||
QVulkanWindowRenderer* VulkanWindow::createRenderer() {
|
||||
return new VulkanRenderer(this, graphics_system_);
|
||||
}
|
||||
|
||||
EmulatorWindow::~EmulatorWindow() {
|
||||
loop_->PostSynchronous([this]() { window_.reset(); });
|
||||
}
|
||||
EmulatorWindow::EmulatorWindow(Loop* loop, const std::string& title)
|
||||
: QtWindow(loop, title) {
|
||||
// TODO(DrChat): Pass in command line arguments.
|
||||
emulator_ = std::make_unique<xe::Emulator>("","","");
|
||||
|
||||
std::unique_ptr<EmulatorWindow> EmulatorWindow::Create(Emulator* emulator) {
|
||||
std::unique_ptr<EmulatorWindow> emulator_window(new EmulatorWindow(emulator));
|
||||
|
||||
emulator_window->loop()->PostSynchronous([&emulator_window]() {
|
||||
xe::threading::set_name("Win32 Loop");
|
||||
xe::Profiler::ThreadEnter("Win32 Loop");
|
||||
|
||||
if (!emulator_window->Initialize()) {
|
||||
xe::FatalError("Failed to initialize main window");
|
||||
return;
|
||||
auto audio_factory = [&](cpu::Processor* processor,
|
||||
kernel::KernelState* kernel_state) {
|
||||
auto audio = apu::xaudio2::XAudio2AudioSystem::Create(processor);
|
||||
if (audio->Setup(kernel_state) != X_STATUS_SUCCESS) {
|
||||
audio->Shutdown();
|
||||
return std::unique_ptr<apu::AudioSystem>(nullptr);
|
||||
}
|
||||
});
|
||||
|
||||
return emulator_window;
|
||||
return audio;
|
||||
};
|
||||
|
||||
graphics_provider_ = ui::vulkan::VulkanProvider::Create(nullptr);
|
||||
auto graphics_factory = [&](cpu::Processor* processor,
|
||||
kernel::KernelState* kernel_state) {
|
||||
auto graphics = std::make_unique<gpu::vulkan::VulkanGraphicsSystem>();
|
||||
if (graphics->Setup(processor, kernel_state,
|
||||
graphics_provider_->CreateOffscreenContext()->target_window())) {
|
||||
graphics->Shutdown();
|
||||
return std::unique_ptr<gpu::vulkan::VulkanGraphicsSystem>(nullptr);
|
||||
}
|
||||
|
||||
return graphics;
|
||||
};
|
||||
|
||||
auto input_factory = [&](ui::Window* window) {
|
||||
std::vector<std::unique_ptr<hid::InputDriver>> drivers;
|
||||
auto xinput_driver = hid::xinput::Create(window);
|
||||
xinput_driver->Setup();
|
||||
drivers.push_back(std::move(xinput_driver));
|
||||
|
||||
return drivers;
|
||||
};
|
||||
|
||||
//X_STATUS result = emulator_->Setup(this, audio_factory, graphics_factory, input_factory);
|
||||
//if (result == X_STATUS_SUCCESS) {
|
||||
// // Setup a callback called when the emulator wants to swap.
|
||||
// emulator_->graphics_system()->SetSwapCallback([&]() {
|
||||
// QMetaObject::invokeMethod(this->graphics_window_.get(), "requestUpdate",
|
||||
// Qt::QueuedConnection);
|
||||
// });
|
||||
//}
|
||||
|
||||
//// Initialize our backend display window.
|
||||
//if (!InitializeVulkan()) {
|
||||
// return;
|
||||
//}
|
||||
|
||||
//// Set a callback on launch
|
||||
//emulator_->on_launch.AddListener([this]() {
|
||||
// auto title_db = this->emulator()->game_data();
|
||||
// if (title_db) {
|
||||
// QPixmap p;
|
||||
// auto icon_block = title_db->icon();
|
||||
// if (icon_block.buffer &&
|
||||
// p.loadFromData(icon_block.buffer, uint(icon_block.size), "PNG")) {
|
||||
// this->setWindowIcon(QIcon(p));
|
||||
// }
|
||||
// }
|
||||
//});
|
||||
}
|
||||
|
||||
bool EmulatorWindow::Initialize() {
|
||||
if (!window_->Initialize()) {
|
||||
XELOGE("Failed to initialize platform window");
|
||||
bool EmulatorWindow::InitializeVulkan() {
|
||||
auto provider =
|
||||
reinterpret_cast<ui::vulkan::VulkanProvider*>(graphics_provider_.get());
|
||||
|
||||
// Create a Qt wrapper around our vulkan instance.
|
||||
vulkan_instance_ = std::make_unique<QVulkanInstance>();
|
||||
vulkan_instance_->setVkInstance(*provider->instance());
|
||||
if (!vulkan_instance_->create()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTitle();
|
||||
graphics_window_ = std::make_unique<VulkanWindow>(
|
||||
reinterpret_cast<gpu::vulkan::VulkanGraphicsSystem*>(
|
||||
emulator_->graphics_system()));
|
||||
graphics_window_->setVulkanInstance(vulkan_instance_.get());
|
||||
|
||||
window_->on_closed.AddListener([this](UIEvent* e) { loop_->Quit(); });
|
||||
loop_->on_quit.AddListener([this](UIEvent* e) { window_.reset(); });
|
||||
|
||||
window_->on_file_drop.AddListener(
|
||||
[this](FileDropEvent* e) { FileDrop(e->filename()); });
|
||||
|
||||
window_->on_key_down.AddListener([this](KeyEvent* e) {
|
||||
bool handled = true;
|
||||
switch (e->key_code()) {
|
||||
case 0x4F: { // o
|
||||
if (e->is_ctrl_pressed()) {
|
||||
FileOpen();
|
||||
}
|
||||
} break;
|
||||
case 0x6A: { // numpad *
|
||||
CpuTimeScalarReset();
|
||||
} break;
|
||||
case 0x6D: { // numpad minus
|
||||
CpuTimeScalarSetHalf();
|
||||
} break;
|
||||
case 0x6B: { // numpad plus
|
||||
CpuTimeScalarSetDouble();
|
||||
} break;
|
||||
|
||||
case 0x72: { // F3
|
||||
Profiler::ToggleDisplay();
|
||||
} break;
|
||||
|
||||
case 0x73: { // VK_F4
|
||||
GpuTraceFrame();
|
||||
} break;
|
||||
case 0x74: { // VK_F5
|
||||
GpuClearCaches();
|
||||
} break;
|
||||
case 0x76: { // VK_F7
|
||||
// Save to file
|
||||
// TODO: Choose path based on user input, or from options
|
||||
// TODO: Spawn a new thread to do this.
|
||||
emulator()->SaveToFile("test.sav");
|
||||
} break;
|
||||
case 0x77: { // VK_F8
|
||||
// Restore from file
|
||||
// TODO: Choose path from user
|
||||
// TODO: Spawn a new thread to do this.
|
||||
emulator()->RestoreFromFile("test.sav");
|
||||
} break;
|
||||
case 0x7A: { // VK_F11
|
||||
ToggleFullscreen();
|
||||
} break;
|
||||
case 0x1B: { // VK_ESCAPE
|
||||
// Allow users to escape fullscreen (but not enter it).
|
||||
if (window_->is_fullscreen()) {
|
||||
window_->ToggleFullscreen(false);
|
||||
} else {
|
||||
handled = false;
|
||||
}
|
||||
} break;
|
||||
|
||||
case 0x13: { // VK_PAUSE
|
||||
CpuBreakIntoDebugger();
|
||||
} break;
|
||||
case 0x03: { // VK_CANCEL
|
||||
CpuBreakIntoHostDebugger();
|
||||
} break;
|
||||
|
||||
case 0x70: { // VK_F1
|
||||
ShowHelpWebsite();
|
||||
} break;
|
||||
|
||||
case 0x71: { // VK_F2
|
||||
ShowCommitID();
|
||||
} break;
|
||||
|
||||
default: {
|
||||
handled = false;
|
||||
} break;
|
||||
}
|
||||
e->set_handled(handled);
|
||||
});
|
||||
|
||||
window_->on_mouse_move.AddListener([this](MouseEvent* e) {
|
||||
if (window_->is_fullscreen() && (e->dx() > 2 || e->dy() > 2)) {
|
||||
if (!window_->is_cursor_visible()) {
|
||||
window_->set_cursor_visible(true);
|
||||
}
|
||||
|
||||
cursor_hide_time_ = Clock::QueryHostSystemTime() + 30000000;
|
||||
}
|
||||
|
||||
e->set_handled(false);
|
||||
});
|
||||
|
||||
window_->on_paint.AddListener([this](UIEvent* e) { CheckHideCursor(); });
|
||||
|
||||
// Main menu.
|
||||
// FIXME: This code is really messy.
|
||||
auto main_menu = MenuItem::Create(MenuItem::Type::kNormal);
|
||||
auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File");
|
||||
{
|
||||
file_menu->AddChild(
|
||||
MenuItem::Create(MenuItem::Type::kString, "&Open...", "Ctrl+O",
|
||||
std::bind(&EmulatorWindow::FileOpen, this)));
|
||||
file_menu->AddChild(
|
||||
MenuItem::Create(MenuItem::Type::kString, "Close",
|
||||
std::bind(&EmulatorWindow::FileClose, this)));
|
||||
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||||
file_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "Show content directory...",
|
||||
std::bind(&EmulatorWindow::ShowContentDirectory, this)));
|
||||
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||||
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, "E&xit",
|
||||
"Alt+F4",
|
||||
[this]() { window_->Close(); }));
|
||||
}
|
||||
main_menu->AddChild(std::move(file_menu));
|
||||
|
||||
// CPU menu.
|
||||
auto cpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&CPU");
|
||||
{
|
||||
cpu_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "&Reset Time Scalar", "Numpad *",
|
||||
std::bind(&EmulatorWindow::CpuTimeScalarReset, this)));
|
||||
cpu_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "Time Scalar /= 2", "Numpad -",
|
||||
std::bind(&EmulatorWindow::CpuTimeScalarSetHalf, this)));
|
||||
cpu_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "Time Scalar *= 2", "Numpad +",
|
||||
std::bind(&EmulatorWindow::CpuTimeScalarSetDouble, this)));
|
||||
}
|
||||
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||||
{
|
||||
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString,
|
||||
"Toggle Profiler &Display", "F3",
|
||||
[]() { Profiler::ToggleDisplay(); }));
|
||||
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kString,
|
||||
"&Pause/Resume Profiler", "`",
|
||||
[]() { Profiler::TogglePause(); }));
|
||||
}
|
||||
cpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||||
{
|
||||
cpu_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "&Break and Show Guest Debugger",
|
||||
"Pause/Break", std::bind(&EmulatorWindow::CpuBreakIntoDebugger, this)));
|
||||
cpu_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "&Break into Host Debugger",
|
||||
"Ctrl+Pause/Break",
|
||||
std::bind(&EmulatorWindow::CpuBreakIntoHostDebugger, this)));
|
||||
}
|
||||
main_menu->AddChild(std::move(cpu_menu));
|
||||
|
||||
// GPU menu.
|
||||
auto gpu_menu = MenuItem::Create(MenuItem::Type::kPopup, "&GPU");
|
||||
{
|
||||
gpu_menu->AddChild(
|
||||
MenuItem::Create(MenuItem::Type::kString, "&Trace Frame", "F4",
|
||||
std::bind(&EmulatorWindow::GpuTraceFrame, this)));
|
||||
}
|
||||
gpu_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||||
{
|
||||
gpu_menu->AddChild(
|
||||
MenuItem::Create(MenuItem::Type::kString, "&Clear Runtime Caches", "F5",
|
||||
std::bind(&EmulatorWindow::GpuClearCaches, this)));
|
||||
}
|
||||
main_menu->AddChild(std::move(gpu_menu));
|
||||
|
||||
// Window menu.
|
||||
auto window_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Window");
|
||||
{
|
||||
window_menu->AddChild(
|
||||
MenuItem::Create(MenuItem::Type::kString, "&Fullscreen", "F11",
|
||||
std::bind(&EmulatorWindow::ToggleFullscreen, this)));
|
||||
}
|
||||
main_menu->AddChild(std::move(window_menu));
|
||||
|
||||
// Help menu.
|
||||
auto help_menu = MenuItem::Create(MenuItem::Type::kPopup, "&Help");
|
||||
{
|
||||
help_menu->AddChild(
|
||||
MenuItem::Create(MenuItem::Type::kString, "Build commit on GitHub...",
|
||||
"F2", std::bind(&EmulatorWindow::ShowCommitID, this)));
|
||||
help_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "Recent changes on GitHub...", [this]() {
|
||||
LaunchWebBrowser(
|
||||
"https://github.com/xenia-project/xenia/compare/" XE_BUILD_COMMIT
|
||||
"..." XE_BUILD_BRANCH);
|
||||
}));
|
||||
help_menu->AddChild(MenuItem::Create(MenuItem::Type::kSeparator));
|
||||
help_menu->AddChild(
|
||||
MenuItem::Create(MenuItem::Type::kString, "&Website...", "F1",
|
||||
std::bind(&EmulatorWindow::ShowHelpWebsite, this)));
|
||||
help_menu->AddChild(MenuItem::Create(
|
||||
MenuItem::Type::kString, "&About...",
|
||||
[this]() { LaunchWebBrowser("https://xenia.jp/about/"); }));
|
||||
}
|
||||
main_menu->AddChild(std::move(help_menu));
|
||||
|
||||
window_->set_main_menu(std::move(main_menu));
|
||||
|
||||
window_->Resize(1280, 720);
|
||||
|
||||
window_->DisableMainMenu();
|
||||
// Now set the graphics window as our central widget.
|
||||
QWidget* wrapper = QWidget::createWindowContainer(graphics_window_.get());
|
||||
setCentralWidget(wrapper);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EmulatorWindow::FileDrop(const std::filesystem::path& filename) {
|
||||
auto result = emulator_->LaunchPath(filename);
|
||||
if (XFAILED(result)) {
|
||||
// TODO: Display a message box.
|
||||
XELOGE("Failed to launch target: {:08X}", result);
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::FileOpen() {
|
||||
std::filesystem::path path;
|
||||
|
||||
auto file_picker = xe::ui::FilePicker::Create();
|
||||
file_picker->set_mode(ui::FilePicker::Mode::kOpen);
|
||||
file_picker->set_type(ui::FilePicker::Type::kFile);
|
||||
file_picker->set_multi_selection(false);
|
||||
file_picker->set_title("Select Content Package");
|
||||
file_picker->set_extensions({
|
||||
{"Supported Files", "*.iso;*.xex;*.xcp;*.*"},
|
||||
{"Disc Image (*.iso)", "*.iso"},
|
||||
{"Xbox Executable (*.xex)", "*.xex"},
|
||||
//{"Content Package (*.xcp)", "*.xcp" },
|
||||
{"All Files (*.*)", "*.*"},
|
||||
});
|
||||
if (file_picker->Show(window_->native_handle())) {
|
||||
auto selected_files = file_picker->selected_files();
|
||||
if (!selected_files.empty()) {
|
||||
path = selected_files[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!path.empty()) {
|
||||
// Normalize the path and make absolute.
|
||||
auto abs_path = std::filesystem::absolute(path);
|
||||
auto result = emulator_->LaunchPath(abs_path);
|
||||
if (XFAILED(result)) {
|
||||
// TODO: Display a message box.
|
||||
XELOGE("Failed to launch target: {:08X}", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::FileClose() {
|
||||
if (emulator_->is_title_open()) {
|
||||
emulator_->TerminateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::ShowContentDirectory() {
|
||||
std::filesystem::path target_path;
|
||||
|
||||
auto content_root = emulator_->content_root();
|
||||
if (!emulator_->is_title_open() || !emulator_->kernel_state()) {
|
||||
target_path = content_root;
|
||||
} else {
|
||||
// TODO(gibbed): expose this via ContentManager?
|
||||
auto title_id =
|
||||
fmt::format("{:08X}", emulator_->kernel_state()->title_id());
|
||||
auto package_root = content_root / title_id;
|
||||
target_path = package_root;
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(target_path)) {
|
||||
std::filesystem::create_directories(target_path);
|
||||
}
|
||||
|
||||
LaunchFileExplorer(target_path);
|
||||
}
|
||||
|
||||
void EmulatorWindow::CheckHideCursor() {
|
||||
if (!window_->is_fullscreen()) {
|
||||
// Only hide when fullscreen.
|
||||
return;
|
||||
}
|
||||
|
||||
if (Clock::QueryHostSystemTime() > cursor_hide_time_) {
|
||||
window_->set_cursor_visible(false);
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::CpuTimeScalarReset() {
|
||||
Clock::set_guest_time_scalar(1.0);
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
void EmulatorWindow::CpuTimeScalarSetHalf() {
|
||||
Clock::set_guest_time_scalar(Clock::guest_time_scalar() / 2.0);
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
void EmulatorWindow::CpuTimeScalarSetDouble() {
|
||||
Clock::set_guest_time_scalar(Clock::guest_time_scalar() * 2.0);
|
||||
UpdateTitle();
|
||||
}
|
||||
|
||||
void EmulatorWindow::CpuBreakIntoDebugger() {
|
||||
if (!cvars::debug) {
|
||||
xe::ui::ImGuiDialog::ShowMessageBox(window_.get(), "Xenia Debugger",
|
||||
"Xenia must be launched with the "
|
||||
"--debug flag in order to enable "
|
||||
"debugging.");
|
||||
return;
|
||||
}
|
||||
auto processor = emulator()->processor();
|
||||
if (processor->execution_state() == cpu::ExecutionState::kRunning) {
|
||||
// Currently running, so interrupt (and show the debugger).
|
||||
processor->Pause();
|
||||
} else {
|
||||
// Not running, so just bring the debugger into focus.
|
||||
processor->ShowDebugger();
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::CpuBreakIntoHostDebugger() { xe::debugging::Break(); }
|
||||
|
||||
void EmulatorWindow::GpuTraceFrame() {
|
||||
emulator()->graphics_system()->RequestFrameTrace();
|
||||
}
|
||||
|
||||
void EmulatorWindow::GpuClearCaches() {
|
||||
emulator()->graphics_system()->ClearCaches();
|
||||
}
|
||||
|
||||
void EmulatorWindow::ToggleFullscreen() {
|
||||
window_->ToggleFullscreen(!window_->is_fullscreen());
|
||||
|
||||
// Hide the cursor after a second if we're going fullscreen
|
||||
cursor_hide_time_ = Clock::QueryHostSystemTime() + 30000000;
|
||||
if (!window_->is_fullscreen()) {
|
||||
window_->set_cursor_visible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::ShowHelpWebsite() { LaunchWebBrowser("https://xenia.jp"); }
|
||||
|
||||
void EmulatorWindow::ShowCommitID() {
|
||||
LaunchWebBrowser(
|
||||
"https://github.com/xenia-project/xenia/commit/" XE_BUILD_COMMIT "/");
|
||||
}
|
||||
|
||||
void EmulatorWindow::UpdateTitle() {
|
||||
std::string title(base_title_);
|
||||
|
||||
if (emulator()->is_title_open()) {
|
||||
auto game_title = emulator()->game_title();
|
||||
title += fmt::format(" | [{:08X}] {}", emulator()->title_id(), game_title);
|
||||
}
|
||||
|
||||
auto graphics_system = emulator()->graphics_system();
|
||||
if (graphics_system) {
|
||||
auto graphics_name = graphics_system->name();
|
||||
title += fmt::format(" <{}>", graphics_name);
|
||||
}
|
||||
|
||||
if (Clock::guest_time_scalar() != 1.0) {
|
||||
title += fmt::format(" (@{:.2f}x)", Clock::guest_time_scalar());
|
||||
}
|
||||
|
||||
if (initializing_shader_storage_) {
|
||||
title +=
|
||||
" (Preloading shaders"
|
||||
u8"\u2026"
|
||||
")";
|
||||
}
|
||||
|
||||
window_->set_title(title);
|
||||
}
|
||||
|
||||
void EmulatorWindow::SetInitializingShaderStorage(bool initializing) {
|
||||
if (initializing_shader_storage_ == initializing) {
|
||||
return;
|
||||
}
|
||||
initializing_shader_storage_ = initializing;
|
||||
UpdateTitle();
|
||||
bool EmulatorWindow::Launch(const std::string& path) {
|
||||
return emulator_->LaunchPath(path) == X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
|
|
@ -2,72 +2,63 @@
|
|||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Copyright 2018 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_APP_EMULATOR_WINDOW_H_
|
||||
#define XENIA_APP_EMULATOR_WINDOW_H_
|
||||
#ifndef XENIA_APP_MAIN_WINDOW_H_
|
||||
#define XENIA_APP_MAIN_WINDOW_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <QMainWindow>
|
||||
#include <QVulkanInstance>
|
||||
#include <QWindow>
|
||||
|
||||
#include "xenia/ui/loop.h"
|
||||
#include "xenia/ui/menu_item.h"
|
||||
#include "xenia/ui/window.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
class Emulator;
|
||||
} // namespace xe
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/ui/graphics_context.h"
|
||||
#include "xenia/ui/graphics_provider.h"
|
||||
#include "xenia/ui/qt/window_qt.h"
|
||||
#include "xenia/ui/qt/loop_qt.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
class EmulatorWindow {
|
||||
class VulkanWindow;
|
||||
class VulkanRenderer;
|
||||
|
||||
using ui::qt::QtWindow;
|
||||
using ui::Loop;
|
||||
|
||||
class EmulatorWindow : public ui::qt::QtWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
virtual ~EmulatorWindow();
|
||||
EmulatorWindow(Loop *loop, const std::string& title);
|
||||
|
||||
static std::unique_ptr<EmulatorWindow> Create(Emulator* emulator);
|
||||
bool Launch(const std::string& path);
|
||||
|
||||
Emulator* emulator() const { return emulator_; }
|
||||
ui::Loop* loop() const { return loop_.get(); }
|
||||
ui::Window* window() const { return window_.get(); }
|
||||
xe::Emulator* emulator() { return emulator_.get(); }
|
||||
|
||||
void UpdateTitle();
|
||||
void ToggleFullscreen();
|
||||
void SetInitializingShaderStorage(bool initializing);
|
||||
protected:
|
||||
// Events
|
||||
|
||||
private slots:
|
||||
|
||||
private:
|
||||
explicit EmulatorWindow(Emulator* emulator);
|
||||
void CreateMenuBar();
|
||||
|
||||
bool Initialize();
|
||||
bool InitializeVulkan();
|
||||
|
||||
void FileDrop(const std::filesystem::path& filename);
|
||||
void FileOpen();
|
||||
void FileClose();
|
||||
void ShowContentDirectory();
|
||||
void CheckHideCursor();
|
||||
void CpuTimeScalarReset();
|
||||
void CpuTimeScalarSetHalf();
|
||||
void CpuTimeScalarSetDouble();
|
||||
void CpuBreakIntoDebugger();
|
||||
void CpuBreakIntoHostDebugger();
|
||||
void GpuTraceFrame();
|
||||
void GpuClearCaches();
|
||||
void ShowHelpWebsite();
|
||||
void ShowCommitID();
|
||||
std::unique_ptr<xe::Emulator> emulator_;
|
||||
|
||||
Emulator* emulator_;
|
||||
std::unique_ptr<ui::Loop> loop_;
|
||||
std::unique_ptr<ui::Window> window_;
|
||||
std::string base_title_;
|
||||
uint64_t cursor_hide_time_ = 0;
|
||||
bool initializing_shader_storage_ = false;
|
||||
std::unique_ptr<QWindow> graphics_window_;
|
||||
std::unique_ptr<ui::GraphicsProvider> graphics_provider_;
|
||||
std::unique_ptr<hid::InputSystem> input_system_;
|
||||
|
||||
std::unique_ptr<QVulkanInstance> vulkan_instance_;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_APP_EMULATOR_WINDOW_H_
|
||||
#endif // XENIA_UI_QT_MAIN_WINDOW_H_
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
#include "xenia/app/library/game_entry.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
XGameEntry* XGameEntry::from_game_info(const GameInfo& info) {
|
||||
auto entry = new XGameEntry();
|
||||
auto result = entry->apply_info(info);
|
||||
|
||||
if (!result) return nullptr;
|
||||
return entry;
|
||||
};
|
||||
|
||||
XGameEntry::XGameEntry() {
|
||||
format_ = XGameFormat::kUnknown;
|
||||
ratings_ = xex2_game_ratings_t();
|
||||
version_.value = 0x0;
|
||||
base_version_.value = 0x0;
|
||||
regions_ = XGameRegions::XEX_REGION_ALL;
|
||||
}
|
||||
|
||||
XGameEntry::XGameEntry(const XGameEntry& other) {
|
||||
format_ = other.format_;
|
||||
file_path_ = other.file_path_;
|
||||
file_name_ = other.file_name_;
|
||||
launch_paths_ = other.launch_paths_;
|
||||
default_launch_paths_ = other.default_launch_paths_;
|
||||
title_ = other.title_;
|
||||
icon_ = other.icon_;
|
||||
icon_size_ = other.icon_size_;
|
||||
title_id_ = other.title_id_;
|
||||
media_id_ = other.media_id_;
|
||||
alt_title_ids_ = other.alt_title_ids_;
|
||||
alt_media_ids_ = other.alt_media_ids_;
|
||||
disc_map_ = other.disc_map_;
|
||||
version_ = other.version_;
|
||||
base_version_ = other.base_version_;
|
||||
ratings_ = other.ratings_;
|
||||
regions_ = other.regions_;
|
||||
build_date_ = other.build_date_;
|
||||
genre_ = other.genre_;
|
||||
release_date_ = other.release_date_;
|
||||
player_count_ = other.player_count_;
|
||||
}
|
||||
|
||||
bool XGameEntry::is_valid() {
|
||||
// Minimum requirements
|
||||
return !file_path_.empty() && title_id_ && media_id_;
|
||||
}
|
||||
|
||||
bool XGameEntry::is_missing_data() {
|
||||
return title_.length() == 0 || icon_[0] == 0 || disc_map_.size() == 0;
|
||||
// TODO: Version
|
||||
// TODO: Base Version
|
||||
// TODO: Ratings
|
||||
// TODO: Regions
|
||||
}
|
||||
|
||||
bool XGameEntry::apply_info(const GameInfo& info) {
|
||||
auto xex = &info.xex_info;
|
||||
auto nxe = &info.nxe_info;
|
||||
|
||||
format_ = info.format;
|
||||
file_path_ = info.path;
|
||||
file_name_ = info.filename;
|
||||
|
||||
if (!xex) return false;
|
||||
|
||||
title_id_ = xex->execution_info.title_id;
|
||||
media_id_ = xex->execution_info.media_id;
|
||||
version_ = xex->execution_info.version.value;
|
||||
base_version_ = xex->execution_info.base_version.value;
|
||||
ratings_ = xex->game_ratings;
|
||||
regions_ = (xex2_region_flags)xe::byte_swap<uint32_t>(
|
||||
xex->security_info.region.value);
|
||||
|
||||
// Add to disc map / launch paths
|
||||
auto disc_id = xex->execution_info.disc_number;
|
||||
disc_map_.insert_or_assign(disc_id, media_id_);
|
||||
launch_paths_.insert_or_assign(info.path, media_id_);
|
||||
if (!default_launch_paths_.count(media_id_)) {
|
||||
default_launch_paths_.insert(std::make_pair(media_id_, info.path));
|
||||
}
|
||||
|
||||
if (xex->game_title.length() > 0) {
|
||||
title_ = xex->game_title;
|
||||
} else if (nxe && nxe->game_title.length() > 0) {
|
||||
title_ = nxe->game_title;
|
||||
}
|
||||
|
||||
if (xex->icon) {
|
||||
delete[] icon_;
|
||||
icon_size_ = xex->icon_size;
|
||||
icon_ = (uint8_t*)calloc(1, icon_size_);
|
||||
memcpy(icon_, xex->icon, icon_size_);
|
||||
} else if (nxe && nxe->icon) {
|
||||
delete[] icon_;
|
||||
icon_size_ = nxe->icon_size;
|
||||
icon_ = (uint8_t*)calloc(1, icon_size_);
|
||||
memcpy(icon_, nxe->icon, icon_size_);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
|
@ -0,0 +1,81 @@
|
|||
#ifndef XENIA_APP_GAME_ENTRY_H_
|
||||
#define XENIA_APP_GAME_ENTRY_H_
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "xenia/app/library/scanner_utils.h"
|
||||
#include "xenia/kernel/util/xex2_info.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
class XGameEntry final {
|
||||
public:
|
||||
static XGameEntry* from_game_info(const GameInfo& info);
|
||||
|
||||
explicit XGameEntry();
|
||||
explicit XGameEntry(const XGameEntry& other);
|
||||
|
||||
bool is_valid();
|
||||
bool is_missing_data();
|
||||
bool apply_info(const GameInfo& info);
|
||||
|
||||
const XGameFormat& format() const { return format_; }
|
||||
const std::filesystem::path& file_path() const { return file_path_; }
|
||||
const std::filesystem::path& file_name() const { return file_name_; }
|
||||
const std::map<std::filesystem::path, uint32_t> launch_paths() const {
|
||||
return launch_paths_;
|
||||
}
|
||||
const std::map<uint32_t, std::filesystem::path> default_launch_paths() const {
|
||||
return default_launch_paths_;
|
||||
}
|
||||
|
||||
const std::string& title() const { return title_; }
|
||||
const uint8_t* icon() const { return icon_; }
|
||||
const size_t& icon_size() const { return icon_size_; }
|
||||
const uint32_t title_id() const { return title_id_; }
|
||||
const uint32_t media_id() const { return media_id_; }
|
||||
const std::vector<uint32_t>& alt_title_ids() const { return alt_title_ids_; }
|
||||
const std::vector<uint32_t>& alt_media_ids() const { return alt_media_ids_; }
|
||||
const std::map<uint8_t, uint32_t>& disc_map() const { return disc_map_; }
|
||||
const xex2_version& version() const { return version_; }
|
||||
const xex2_version& base_version() const { return base_version_; }
|
||||
const xex2_game_ratings_t& ratings() const { return ratings_; }
|
||||
const xex2_region_flags& regions() const { return regions_; }
|
||||
const std::string& genre() const { return genre_; }
|
||||
const std::string& build_date() const { return build_date_; }
|
||||
const std::string& release_date() const { return release_date_; }
|
||||
const uint8_t& player_count() const { return player_count_; }
|
||||
|
||||
private:
|
||||
// File Info
|
||||
XGameFormat format_;
|
||||
std::filesystem::path file_path_;
|
||||
std::filesystem::path file_name_;
|
||||
std::map<std::filesystem::path, uint32_t> launch_paths_; // <Path, MediaId>
|
||||
std::map<uint32_t, std::filesystem::path>
|
||||
default_launch_paths_; // <MediaId, Path>
|
||||
|
||||
// Game Metadata
|
||||
std::string title_;
|
||||
uint8_t* icon_ = nullptr;
|
||||
size_t icon_size_ = 0;
|
||||
uint32_t title_id_ = 0;
|
||||
uint32_t media_id_ = 0;
|
||||
std::vector<uint32_t> alt_title_ids_;
|
||||
std::vector<uint32_t> alt_media_ids_;
|
||||
std::map<uint8_t, uint32_t> disc_map_; // <Disc #, MediaID>
|
||||
xex2_version version_;
|
||||
xex2_version base_version_;
|
||||
xex2_game_ratings_t ratings_;
|
||||
xex2_region_flags regions_;
|
||||
std::string build_date_;
|
||||
std::string genre_;
|
||||
std::string release_date_;
|
||||
uint8_t player_count_ = 0;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,107 @@
|
|||
#include "xenia/app/library/game_library.h"
|
||||
#include <algorithm>
|
||||
#include "xenia/app/library/game_scanner.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
using AsyncCallback = XGameLibrary::AsyncCallback;
|
||||
|
||||
XGameLibrary* XGameLibrary::Instance() {
|
||||
static XGameLibrary* instance = new XGameLibrary;
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool XGameLibrary::ContainsPath(const std::filesystem::path& path) const {
|
||||
auto existing = std::find(paths_.begin(), paths_.end(), path);
|
||||
if (existing != paths_.end()) {
|
||||
return false; // Path already exists.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XGameLibrary::ContainsGame(uint32_t title_id) const {
|
||||
return FindGame(title_id) != nullptr;
|
||||
}
|
||||
|
||||
const XGameEntry* XGameLibrary::FindGame(const uint32_t title_id) const {
|
||||
auto result = std::find_if(games_.begin(), games_.end(),
|
||||
[title_id](const XGameEntry& entry) {
|
||||
return entry.title_id() == title_id;
|
||||
});
|
||||
if (result != games_.end()) {
|
||||
return &*result;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool XGameLibrary::RemovePath(const std::filesystem::path& path) {
|
||||
auto existing = std::find(paths_.begin(), paths_.end(), path);
|
||||
if (existing == paths_.end()) {
|
||||
return false; // Path does not exist.
|
||||
}
|
||||
|
||||
paths_.erase(existing);
|
||||
return true;
|
||||
}
|
||||
|
||||
int XGameLibrary::ScanPath(const std::filesystem::path& path) {
|
||||
int count = 0;
|
||||
|
||||
AddPath(path);
|
||||
const auto& results = XGameScanner::ScanPath(path);
|
||||
for (const XGameEntry& result : results) {
|
||||
count++;
|
||||
AddGame(result);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int XGameLibrary::ScanPathAsync(const std::filesystem::path& path,
|
||||
const AsyncCallback& cb) {
|
||||
AddPath(path);
|
||||
|
||||
auto paths = XGameScanner::FindGamesInPath(path);
|
||||
int count = static_cast<int>(paths.size());
|
||||
return XGameScanner::ScanPathsAsync(
|
||||
paths, [=](const XGameEntry& entry, int scanned) {
|
||||
AddGame(entry);
|
||||
if (cb) {
|
||||
cb(((double)scanned / count) * 100.0, entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void XGameLibrary::AddGame(const XGameEntry& game) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
uint32_t title_id = game.title_id();
|
||||
|
||||
const auto& begin = games_.begin();
|
||||
const auto& end = games_.end();
|
||||
|
||||
auto result = end;
|
||||
if (title_id != 0x00000000) {
|
||||
result = std::find_if(begin, end, [title_id](const XGameEntry& entry) {
|
||||
return entry.title_id() == title_id;
|
||||
});
|
||||
}
|
||||
|
||||
// title already exists in library, overwrite existing
|
||||
if (result != games_.end()) {
|
||||
*result = game;
|
||||
} else {
|
||||
games_.push_back(game);
|
||||
}
|
||||
}
|
||||
|
||||
void XGameLibrary::AddPath(const std::filesystem::path& path) {
|
||||
auto result = std::find(paths_.begin(), paths_.end(), path);
|
||||
|
||||
// only save unique paths
|
||||
if (result == paths_.end()) {
|
||||
paths_.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
|
@ -0,0 +1,70 @@
|
|||
#ifndef XENIA_APP_GAME_LIBRARY_H_
|
||||
#define XENIA_APP_GAME_LIBRARY_H_
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "xenia/app/library/game_entry.h"
|
||||
#include "xenia/app/library/game_scanner.h"
|
||||
#include "xenia/app/library/scanner_utils.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
class XGameLibrary {
|
||||
public:
|
||||
using AsyncCallback = std::function<void(double, const XGameEntry&)>;
|
||||
|
||||
XGameLibrary(XGameLibrary const&) = delete;
|
||||
XGameLibrary& operator=(XGameLibrary const&) = delete;
|
||||
|
||||
static XGameLibrary* Instance();
|
||||
|
||||
// Returns whether the library has scanned the provided path.
|
||||
bool ContainsPath(const std::filesystem::path& path) const;
|
||||
|
||||
// Returns whether the library has scanned a game with the provided title id.
|
||||
bool ContainsGame(uint32_t title_id) const;
|
||||
|
||||
// Searches the library for a scanned game entry, and returns it if present.
|
||||
const XGameEntry* FindGame(const uint32_t title_id) const;
|
||||
|
||||
// Scans path for supported games/apps synchronously.
|
||||
// Returns the number of games found.
|
||||
int ScanPath(const std::filesystem::path& path);
|
||||
|
||||
// Scans path for supported games/apps asynchronously,
|
||||
// calling the provided callback for each game found.
|
||||
// Returns the number of games found.
|
||||
int ScanPathAsync(const std::filesystem::path& path, const AsyncCallback& cb);
|
||||
|
||||
// Remove a path from the library.
|
||||
// If path is found, it is removed and the library is rescanned.
|
||||
bool RemovePath(const std::filesystem::path& path);
|
||||
|
||||
// Add a manually crafted game entry to the library.
|
||||
void AddGame(const XGameEntry& game);
|
||||
|
||||
const std::vector<XGameEntry>& games() const { return games_; }
|
||||
const size_t size() const { return games_.size(); }
|
||||
|
||||
void Clear() { games_.clear(); }
|
||||
|
||||
// TODO: Provide functions to load and save the library from a cache on disk.
|
||||
|
||||
private:
|
||||
void AddPath(const std::filesystem::path& path);
|
||||
XGameLibrary() = default;
|
||||
|
||||
std::vector<XGameEntry> games_;
|
||||
std::vector<std::filesystem::path> paths_;
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,254 @@
|
|||
#include "xenia/app/library/game_scanner.h"
|
||||
#include "xenia/app/library/scanner_utils.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/threading.h"
|
||||
|
||||
#include <deque>
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
using filesystem::FileInfo;
|
||||
using AsyncCallback = XGameScanner::AsyncCallback;
|
||||
|
||||
std::vector<wstring> XGameScanner::FindGamesInPath(const wstring& path) {
|
||||
// Path is a directory, scan recursively
|
||||
// TODO: Warn about recursively scanning paths with large hierarchies
|
||||
|
||||
std::deque<wstring> queue;
|
||||
queue.push_front(path);
|
||||
|
||||
std::vector<wstring> paths;
|
||||
int game_count = 0;
|
||||
|
||||
while (!queue.empty()) {
|
||||
wstring current_path = queue.front();
|
||||
FileInfo current_file;
|
||||
filesystem::GetInfo(current_path, ¤t_file);
|
||||
|
||||
queue.pop_front();
|
||||
|
||||
if (current_file.type == FileInfo::Type::kDirectory) {
|
||||
std::vector<FileInfo> directory_files =
|
||||
filesystem::ListFiles(current_path);
|
||||
for (FileInfo file : directory_files) {
|
||||
if (CompareCaseInsensitive(file.name, L"$SystemUpdate")) continue;
|
||||
|
||||
auto next_path = (current_path, file.name);
|
||||
// Skip searching directories with an extracted default.xex file
|
||||
if (std::filesystem::exists(next_path / L"default.xex")) {
|
||||
queue.push_front(next_path / L"default.xex");
|
||||
continue;
|
||||
}
|
||||
queue.push_front(next_path);
|
||||
}
|
||||
} else {
|
||||
// Exclusively scan iso, xex, or files without an extension.
|
||||
auto extension = GetFileExtension(current_path);
|
||||
if (!extension.empty() && extension != L"xex" && extension != L"iso") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not attempt to scan SVOD data files
|
||||
auto filename = GetFileName(current_path);
|
||||
if (memcmp(filename.c_str(), L"Data", 4) == 0) continue;
|
||||
|
||||
// Skip empty files
|
||||
if (current_file.total_size == 0) continue;
|
||||
|
||||
paths.push_back(current_path);
|
||||
game_count++;
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
std::vector<XGameEntry> XGameScanner::ScanPath(const wstring& path) {
|
||||
std::vector<XGameEntry> games;
|
||||
|
||||
// Check if the given path exists
|
||||
if (!std::filesystem::exists(path)) {
|
||||
return games;
|
||||
}
|
||||
|
||||
// Scan if the given path is a file
|
||||
if (!std::filesystem::is_directory(path)) {
|
||||
XGameEntry game_entry;
|
||||
if (XFAILED(ScanGame(path, &game_entry))) {
|
||||
//XELOGE("Failed to scan game at {}", xe::path_to_utf8(path));
|
||||
} else {
|
||||
games.emplace_back(std::move(game_entry));
|
||||
}
|
||||
} else {
|
||||
const std::vector<wstring>& game_paths = FindGamesInPath(path);
|
||||
for (const wstring& game_path : game_paths) {
|
||||
XGameEntry game_entry;
|
||||
if (XFAILED(ScanGame(game_path, &game_entry))) {
|
||||
continue;
|
||||
}
|
||||
games.emplace_back(std::move(game_entry));
|
||||
}
|
||||
}
|
||||
|
||||
//XELOGI("Scanned {} files", games.size());
|
||||
return games;
|
||||
}
|
||||
|
||||
int XGameScanner::ScanPathAsync(const wstring& path, const AsyncCallback& cb) {
|
||||
std::vector<wstring> paths = {path};
|
||||
return ScanPathsAsync(paths, cb);
|
||||
}
|
||||
|
||||
int XGameScanner::ScanPathsAsync(const std::vector<wstring>& paths,
|
||||
const AsyncCallback& cb) {
|
||||
// start scanning in a new thread
|
||||
// TODO: switch to xe::threading::Thread instead of std::thread?
|
||||
std::thread scan_thread = std::thread(
|
||||
[](std::vector<wstring> paths, AsyncCallback cb) {
|
||||
std::atomic<int> scanned = 0;
|
||||
|
||||
auto scan_func = [&](const std::vector<wstring>& paths, size_t start,
|
||||
size_t size) {
|
||||
for (auto it = paths.begin() + start;
|
||||
it != paths.begin() + start + size; ++it) {
|
||||
scanned++;
|
||||
XGameEntry game_info;
|
||||
auto status = ScanGame(*it, &game_info);
|
||||
if (cb && XSUCCEEDED(status)) {
|
||||
cb(game_info, scanned);
|
||||
} else {
|
||||
//XELOGE("Failed to scan game at {}", it);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
uint32_t thread_count = xe::threading::logical_processor_count();
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
|
||||
// scan games on this thread if the user has < 4 cores
|
||||
if (thread_count < 4) {
|
||||
scan_func(paths, 0, paths.size());
|
||||
} else {
|
||||
// split workload into even amounts based on core count
|
||||
size_t total_size = paths.size();
|
||||
size_t work_size = paths.size() / thread_count;
|
||||
size_t leftover = paths.size() % thread_count;
|
||||
|
||||
if (work_size > 0) {
|
||||
for (uint32_t i = 0; i < thread_count; i++) {
|
||||
threads.emplace_back(std::move(
|
||||
std::thread(scan_func, paths, i * work_size, work_size)));
|
||||
}
|
||||
}
|
||||
scan_func(paths, total_size - leftover, leftover);
|
||||
}
|
||||
|
||||
for (auto& thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
},
|
||||
paths, cb);
|
||||
|
||||
scan_thread.detach();
|
||||
|
||||
return (int)paths.size();
|
||||
}
|
||||
|
||||
X_STATUS XGameScanner::ScanGame(const std::filesystem::path& path,
|
||||
XGameEntry* out_info) {
|
||||
if (!out_info) {
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(path)) {
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
XGameFormat format = ResolveFormat(path);
|
||||
const char* format_str;
|
||||
|
||||
switch (format) {
|
||||
case XGameFormat::kIso: {
|
||||
format_str = "ISO";
|
||||
break;
|
||||
}
|
||||
case XGameFormat::kStfs: {
|
||||
format_str = "STFS";
|
||||
break;
|
||||
}
|
||||
case XGameFormat::kXex: {
|
||||
format_str = "XEX";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
format_str = "Unknown";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//XELOGD("Scanning {}", std::filesystem::absolute(path));
|
||||
|
||||
auto device = CreateDevice(path);
|
||||
if (device == nullptr || !device->Initialize()) {
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
XELOGI("Format is {}", format_str);
|
||||
|
||||
GameInfo game_info;
|
||||
game_info.filename = GetFileName(path);
|
||||
game_info.path = path;
|
||||
game_info.format = format;
|
||||
|
||||
// Read XEX
|
||||
auto xex_entry = device->ResolvePath("default.xex");
|
||||
if (xex_entry) {
|
||||
File* xex_file = nullptr;
|
||||
auto status = xex_entry->Open(vfs::FileAccess::kFileReadData, &xex_file);
|
||||
if (XSUCCEEDED(status)) {
|
||||
status = XexScanner::ScanXex(xex_file, &game_info);
|
||||
if (!XSUCCEEDED(status)) {
|
||||
XELOGE("Could not parse xex file: {}",
|
||||
xex_file->entry()->path().c_str());
|
||||
return status;
|
||||
}
|
||||
} else {
|
||||
XELOGE("Could not load default.xex from device: {}", status);
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
xex_file->Destroy();
|
||||
} else {
|
||||
XELOGE("Could not resolve default.xex");
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
// Read NXE
|
||||
auto nxe_entry = device->ResolvePath("nxeart");
|
||||
if (nxe_entry) {
|
||||
File* nxe_file = nullptr;
|
||||
auto status = nxe_entry->Open(vfs::FileAccess::kFileReadData, &nxe_file);
|
||||
if (XSUCCEEDED(status)) {
|
||||
status = NxeScanner::ScanNxe(nxe_file, &game_info);
|
||||
if (!XSUCCEEDED(status)) {
|
||||
XELOGE("Could not parse nxeart file: {}",
|
||||
nxe_file->entry()->path());
|
||||
return status;
|
||||
}
|
||||
} else {
|
||||
XELOGE("Could not load nxeart from device: %x", status);
|
||||
}
|
||||
|
||||
nxe_file->Destroy();
|
||||
} else {
|
||||
XELOGI("Game does not have an nxeart file");
|
||||
}
|
||||
|
||||
out_info->apply_info(game_info);
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
|
@ -0,0 +1,48 @@
|
|||
#ifndef XENIA_APP_GAME_SCANNER_H_
|
||||
#define XENIA_APP_GAME_SCANNER_H_
|
||||
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "xenia/app/library/game_entry.h"
|
||||
#include "xenia/app/library/nxe_scanner.h"
|
||||
#include "xenia/app/library/scanner_utils.h"
|
||||
#include "xenia/app/library/xex_scanner.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
class XGameLibrary;
|
||||
|
||||
class XGameScanner {
|
||||
public:
|
||||
using AsyncCallback = std::function<void(const XGameEntry&, int)>;
|
||||
|
||||
// Returns a vector of all supported games in provided path.
|
||||
static std::vector<wstring> FindGamesInPath(const wstring& path);
|
||||
|
||||
// Scans a provided path and recursively parses the games.
|
||||
// Returns a vector of parsed game entries.
|
||||
static std::vector<XGameEntry> ScanPath(const wstring& path);
|
||||
|
||||
// Scans a provided path and recursively parses the games asynchronously.
|
||||
// The callback provided is called on each successfully parsed game.
|
||||
// Returns the number of games found in the path.
|
||||
static int ScanPathAsync(const wstring& path,
|
||||
const AsyncCallback& cb = nullptr);
|
||||
|
||||
// Scans a list of provided paths and recursively parses the games
|
||||
// asynchronously. The callback provided is called on each successfully parsed
|
||||
// game. Returns the number of games found in all paths provided.
|
||||
static int ScanPathsAsync(const std::vector<wstring>& paths,
|
||||
const AsyncCallback& cb = nullptr);
|
||||
|
||||
// Scans a path for a single game, populating the provided output game entry
|
||||
// if found and parsed successfully.
|
||||
static X_STATUS ScanGame(const std::filesystem::path& path,
|
||||
XGameEntry* out_info);
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,38 @@
|
|||
#include "xenia/app/library/nxe_scanner.h"
|
||||
#include "xenia/app/library/scanner_utils.h"
|
||||
#include "xenia/vfs/devices/stfs_container_entry.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
using vfs::StfsHeader;
|
||||
|
||||
X_STATUS NxeScanner::ScanNxe(File* file, GameInfo* out_info) {
|
||||
NxeInfo* nxe_info = &out_info->nxe_info;
|
||||
// Read Header
|
||||
size_t file_size = file->entry()->size();
|
||||
uint8_t* data = new uint8_t[file_size];
|
||||
|
||||
Read(file, &data[0]);
|
||||
StfsHeader header;
|
||||
header.Read(data);
|
||||
|
||||
// Read Title
|
||||
std::string title(xe::to_utf8(header.title_name));
|
||||
nxe_info->game_title = title;
|
||||
|
||||
// Read Icon
|
||||
nxe_info->icon_size = header.title_thumbnail_image_size;
|
||||
nxe_info->icon = (uint8_t*)calloc(1, nxe_info->icon_size);
|
||||
memcpy(nxe_info->icon, header.title_thumbnail_image, nxe_info->icon_size);
|
||||
|
||||
// TODO: Read nxebg.jpg
|
||||
// TODO: Read nxeslot.jpg
|
||||
// How can we open the file with a StfsContainerDevice?
|
||||
|
||||
delete[] data;
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
|
@ -0,0 +1,20 @@
|
|||
#ifndef XENIA_APP_NXE_SCANNER_H_
|
||||
#define XENIA_APP_NXE_SCANNER_H_
|
||||
|
||||
#include "xenia/app/library/scanner_utils.h"
|
||||
#include "xenia/vfs/file.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
using vfs::File;
|
||||
|
||||
class NxeScanner {
|
||||
public:
|
||||
static X_STATUS ScanNxe(File* file, GameInfo* out_info);
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,205 @@
|
|||
#ifndef XENIA_APP_SCANNER_UTILS_H_
|
||||
#define XENIA_APP_SCANNER_UTILS_H_
|
||||
|
||||
#include "xenia/base/filesystem.h"
|
||||
#include "xenia/base/string_util.h"
|
||||
#include "xenia/kernel/util/xex2_info.h"
|
||||
#include "xenia/vfs/device.h"
|
||||
#include "xenia/vfs/devices/disc_image_device.h"
|
||||
#include "xenia/vfs/devices/host_path_device.h"
|
||||
#include "xenia/vfs/devices/stfs_container_device.h"
|
||||
#include "xenia/vfs/file.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
using std::wstring;
|
||||
using vfs::Device;
|
||||
using vfs::File;
|
||||
|
||||
enum XGameFormat {
|
||||
kUnknown,
|
||||
kIso,
|
||||
kStfs,
|
||||
kXex,
|
||||
};
|
||||
|
||||
typedef xex2_header XexHeader;
|
||||
typedef xex2_opt_header XexOptHeader;
|
||||
typedef xex2_game_ratings_t XGameRatings;
|
||||
typedef xex2_region_flags XGameRegions;
|
||||
typedef xe_xex2_version_t XGameVersion;
|
||||
|
||||
struct XexInfo {
|
||||
std::string game_title;
|
||||
uint8_t* icon = nullptr;
|
||||
size_t icon_size;
|
||||
|
||||
uint32_t* alt_title_ids = nullptr;
|
||||
uint32_t alt_title_ids_count;
|
||||
uint32_t base_address;
|
||||
xex2_opt_execution_info execution_info;
|
||||
xex2_opt_file_format_info* file_format_info = nullptr;
|
||||
xex2_game_ratings_t game_ratings;
|
||||
uint32_t header_count;
|
||||
uint32_t header_size;
|
||||
xex2_module_flags module_flags;
|
||||
xex2_multi_disc_media_id_t* multi_disc_media_ids = nullptr;
|
||||
uint32_t multi_disc_media_ids_count;
|
||||
xex2_opt_original_pe_name* original_pe_name = nullptr;
|
||||
xex2_page_descriptor* page_descriptors = nullptr;
|
||||
uint32_t page_descriptors_count;
|
||||
xex2_resource* resources = nullptr;
|
||||
uint32_t resources_count;
|
||||
xex2_security_info security_info;
|
||||
uint32_t security_offset;
|
||||
uint8_t session_key[0x10];
|
||||
xex2_system_flags system_flags;
|
||||
|
||||
~XexInfo() {
|
||||
delete[] icon;
|
||||
delete[] alt_title_ids;
|
||||
delete file_format_info;
|
||||
delete[] multi_disc_media_ids;
|
||||
delete original_pe_name;
|
||||
delete[] page_descriptors;
|
||||
delete[] resources;
|
||||
}
|
||||
};
|
||||
|
||||
struct NxeInfo {
|
||||
std::string game_title;
|
||||
uint8_t* icon = nullptr;
|
||||
size_t icon_size;
|
||||
uint8_t* nxe_background_image = nullptr; // TODO
|
||||
size_t nxe_background_image_size; // TODO
|
||||
uint8_t* nxe_slot_image = nullptr; // TODO
|
||||
size_t nxe_slot_image_size; // TODO
|
||||
|
||||
NxeInfo() = default;
|
||||
|
||||
~NxeInfo() {
|
||||
delete[] icon;
|
||||
delete[] nxe_background_image;
|
||||
delete[] nxe_slot_image;
|
||||
}
|
||||
};
|
||||
|
||||
struct GameInfo {
|
||||
XGameFormat format;
|
||||
std::filesystem::path path;
|
||||
std::filesystem::path filename;
|
||||
NxeInfo nxe_info;
|
||||
XexInfo xex_info;
|
||||
|
||||
GameInfo() = default;
|
||||
};
|
||||
|
||||
inline const bool CompareCaseInsensitive(const wstring& left,
|
||||
const wstring& right) {
|
||||
// Copy strings for transform
|
||||
wstring a(left);
|
||||
wstring b(right);
|
||||
|
||||
std::transform(a.begin(), a.end(), a.begin(), toupper);
|
||||
std::transform(b.begin(), b.end(), b.begin(), toupper);
|
||||
|
||||
return a == b;
|
||||
}
|
||||
|
||||
inline const std::filesystem::path GetFileExtension(
|
||||
const std::filesystem::path& path) {
|
||||
return path.extension();
|
||||
}
|
||||
|
||||
inline const std::filesystem::path GetFileName(const std::filesystem::path& path) {
|
||||
return path.filename();
|
||||
}
|
||||
|
||||
inline const std::filesystem::path GetParentDirectory(
|
||||
const std::filesystem::path& path) {
|
||||
return path.parent_path();
|
||||
}
|
||||
|
||||
inline X_STATUS Read(File* file, void* buffer, size_t offset = 0,
|
||||
size_t length = 0) {
|
||||
if (!buffer) {
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
if (!length) {
|
||||
length = file->entry()->size();
|
||||
}
|
||||
|
||||
size_t bytes_read;
|
||||
file->ReadSync(buffer, length, offset, &bytes_read);
|
||||
|
||||
if (length == bytes_read) {
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
inline void ReadStfsMagic(const std::filesystem::path& path, char out[4]) {
|
||||
using namespace xe::filesystem;
|
||||
auto file = OpenFile(path, "r");
|
||||
if (file) {
|
||||
size_t read = 0;
|
||||
fread(out, 4, 1, file);
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
|
||||
inline const XGameFormat ResolveFormat(const std::filesystem::path& path) {
|
||||
const std::wstring& extension = GetFileExtension(path);
|
||||
|
||||
if (CompareCaseInsensitive(extension, L"iso")) return XGameFormat::kIso;
|
||||
if (CompareCaseInsensitive(extension, L"xex")) return XGameFormat::kXex;
|
||||
|
||||
// STFS Container
|
||||
char magic[4];
|
||||
if (extension.length() == 0) {
|
||||
ReadStfsMagic(path, magic);
|
||||
|
||||
if (memcmp(&magic, "LIVE", 4) == 0 || memcmp(&magic, "CON ", 4) == 0 ||
|
||||
memcmp(&magic, "PIRS", 4) == 0)
|
||||
return XGameFormat::kStfs;
|
||||
}
|
||||
|
||||
return XGameFormat::kUnknown;
|
||||
}
|
||||
|
||||
inline std::unique_ptr<Device> CreateDevice(const std::filesystem::path& path) {
|
||||
std::string mount_path = "\\SCAN";
|
||||
XGameFormat format = ResolveFormat(path);
|
||||
|
||||
switch (format) {
|
||||
case XGameFormat::kIso:
|
||||
return std::make_unique<vfs::DiscImageDevice>(mount_path, path);
|
||||
case XGameFormat::kXex:
|
||||
return std::make_unique<vfs::HostPathDevice>(
|
||||
mount_path, GetParentDirectory(path), true);
|
||||
case XGameFormat::kStfs:
|
||||
return std::make_unique<vfs::StfsContainerDevice>(mount_path, path);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T Read(File* file, size_t offset) {
|
||||
uint8_t data[sizeof(T)];
|
||||
Read(file, data, offset, sizeof(T));
|
||||
T swapped = xe::load_and_swap<T>(data);
|
||||
|
||||
return swapped;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
||||
#endif // !define XENIA_APP_SCANNER_UTILS_H_
|
|
@ -0,0 +1,555 @@
|
|||
#include "xenia/app/library/xex_scanner.h"
|
||||
#include "third_party/crypto/TinySHA1.hpp"
|
||||
#include "third_party/crypto/rijndael-alg-fst.h"
|
||||
#include "xenia/app/library/scanner_utils.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/cpu/lzx.h"
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
void aes_decrypt_buffer(const uint8_t* session_key, const uint8_t* input_buffer,
|
||||
const size_t input_size, uint8_t* output_buffer,
|
||||
const size_t output_size) {
|
||||
XELOGI("AesDecryptBuffer called on input of size %d", input_size);
|
||||
uint32_t rk[4 * (MAXNR + 1)];
|
||||
uint8_t ivec[16] = {0};
|
||||
int32_t Nr = rijndaelKeySetupDec(rk, session_key, 128);
|
||||
const uint8_t* ct = input_buffer;
|
||||
uint8_t* pt = output_buffer;
|
||||
for (size_t n = 0; n < input_size; n += 16, ct += 16, pt += 16) {
|
||||
// Decrypt 16 uint8_ts from input -> output.
|
||||
rijndaelDecrypt(rk, Nr, ct, pt);
|
||||
for (size_t i = 0; i < 16; i++) {
|
||||
// XOR with previous.
|
||||
pt[i] ^= ivec[i];
|
||||
// Set previous.
|
||||
ivec[i] = ct[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void aes_decrypt_inplace(const uint8_t* session_key, const uint8_t* buffer,
|
||||
const size_t size) {
|
||||
XELOGI("AesDecryptInplace called on input of size %d", size);
|
||||
uint32_t rk[4 * (MAXNR + 1)];
|
||||
uint8_t ivec[0x10] = {0};
|
||||
int32_t Nr = rijndaelKeySetupDec(rk, session_key, 128);
|
||||
for (size_t n = 0; n < size; n += 0x10) {
|
||||
uint8_t* in = (uint8_t*)buffer + n;
|
||||
uint8_t out[0x10] = {0};
|
||||
rijndaelDecrypt(rk, Nr, in, out);
|
||||
for (size_t i = 0; i < 0x10; i++) {
|
||||
// XOR with previous.
|
||||
out[i] ^= ivec[i];
|
||||
// Set previous.
|
||||
ivec[i] = in[i];
|
||||
}
|
||||
|
||||
// Fast copy
|
||||
*(size_t*)in = *(size_t*)out;
|
||||
*(size_t*)(in + 0x8) = *(size_t*)(out + 0x8);
|
||||
}
|
||||
}
|
||||
|
||||
X_STATUS ReadXexImageUncompressed(File* file, XexInfo* info, size_t offset,
|
||||
uint32_t length, uint8_t*& out_data) {
|
||||
XELOGI("Reading uncompressed image");
|
||||
size_t file_size = file->entry()->size();
|
||||
auto format = info->file_format_info;
|
||||
|
||||
const size_t exe_size = file_size - info->header_size;
|
||||
|
||||
X_STATUS status;
|
||||
switch (format->encryption_type) {
|
||||
case XEX_ENCRYPTION_NONE: {
|
||||
status = Read(file, out_data, info->header_size + offset, length);
|
||||
if (status != X_STATUS_SUCCESS) {
|
||||
XELOGE("Could not read from file %ls", file->entry()->path().c_str());
|
||||
return status;
|
||||
}
|
||||
|
||||
XELOGI("Successfully read unencrypted uncompressed image.");
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
case XEX_ENCRYPTION_NORMAL: {
|
||||
status = Read(file, out_data, info->header_size + offset, length);
|
||||
if (status != X_STATUS_SUCCESS) {
|
||||
XELOGE("Could not read from file %ls", file->entry()->path().c_str());
|
||||
return status;
|
||||
}
|
||||
|
||||
aes_decrypt_inplace(info->session_key, out_data, length);
|
||||
XELOGI("Successfully read encrypted uncompressed image.");
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
default:
|
||||
XELOGI("Could not read image. Unknown encryption type.");
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
}
|
||||
|
||||
X_STATUS ReadXexImageBasicCompressed(File* file, XexInfo* info, size_t offset,
|
||||
uint32_t length, uint8_t*& out_data) {
|
||||
XELOGI("Reading basic compressed image.");
|
||||
auto file_size = file->entry()->size();
|
||||
auto format = info->file_format_info;
|
||||
auto compression = &format->compression_info.basic;
|
||||
auto encryption = format->encryption_type;
|
||||
|
||||
// Find proper block
|
||||
uint32_t i;
|
||||
uint32_t compressed_position = 0;
|
||||
uint32_t uncompressed_position = 0;
|
||||
uint32_t block_count = (format->info_size - 8) / 8;
|
||||
for (i = 0; i < block_count; i++) {
|
||||
const uint32_t data_size = compression->blocks[i].data_size;
|
||||
const uint32_t zero_size = compression->blocks[i].zero_size;
|
||||
const uint32_t total_size = data_size + zero_size;
|
||||
if (uncompressed_position + total_size > offset) break;
|
||||
|
||||
compressed_position += data_size;
|
||||
uncompressed_position += total_size;
|
||||
}
|
||||
XELOGI("Found offset %08x at block %d", compressed_position, i);
|
||||
|
||||
// For some reason the AES IV is screwing up the first 0x10 bytes,
|
||||
// so in the meantime, we're just shifting back 0x10 and skipping
|
||||
// the garbage data.
|
||||
auto block = compression->blocks[i];
|
||||
uint32_t block_size = block.data_size + 0x10;
|
||||
uint32_t block_address = info->header_size + compressed_position - 0x10;
|
||||
|
||||
uint8_t* data = new uint8_t[file->entry()->size()];
|
||||
Read(file, data, block_address, block_size);
|
||||
|
||||
if (encryption == XEX_ENCRYPTION_NORMAL) {
|
||||
XELOGI("Decrypting basic compressed image.");
|
||||
aes_decrypt_inplace(info->session_key, data, block_size);
|
||||
}
|
||||
|
||||
// Get the requested data
|
||||
auto remaining_offset = offset - uncompressed_position;
|
||||
out_data = (uint8_t*)malloc(length);
|
||||
memcpy(out_data, data + remaining_offset + 0x10, length);
|
||||
|
||||
XELOGI("Successfully read basic compressed image.");
|
||||
delete[] data;
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
X_STATUS ReadXexImageNormalCompressed(File* file, XexInfo* info, size_t offset,
|
||||
uint32_t length, uint8_t*& out_data) {
|
||||
auto encryption_type = info->file_format_info->encryption_type;
|
||||
auto uncompressed_length = info->security_info.image_size;
|
||||
auto compression_info = info->file_format_info->compression_info.normal;
|
||||
|
||||
sha1::SHA1 s;
|
||||
auto in_length = file->entry()->size() - info->header_size;
|
||||
|
||||
uint8_t* in_buffer = new uint8_t[in_length];
|
||||
Read(file, in_buffer, info->header_size, in_length);
|
||||
|
||||
if (encryption_type == XEX_ENCRYPTION_NORMAL)
|
||||
aes_decrypt_inplace(info->session_key, in_buffer, in_length);
|
||||
|
||||
uint8_t* compressed_buffer = new uint8_t[in_length];
|
||||
|
||||
uint8_t block_calced_digest[0x14];
|
||||
auto current_block = &compression_info.first_block;
|
||||
auto in_cursor = in_buffer;
|
||||
auto out_cursor = compressed_buffer;
|
||||
|
||||
while (current_block->block_size) {
|
||||
auto next_ptr = in_cursor + current_block->block_size;
|
||||
auto next_block = (xex2_compressed_block_info*)in_cursor;
|
||||
|
||||
s.reset();
|
||||
s.processBytes(in_cursor, current_block->block_size);
|
||||
s.finalize(block_calced_digest);
|
||||
if (memcmp(block_calced_digest, current_block->block_hash, 0x14) != 0) {
|
||||
delete[] in_buffer;
|
||||
delete[] compressed_buffer;
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
in_cursor += 0x18;
|
||||
|
||||
while (true) {
|
||||
auto chunk_size = (in_cursor[0] << 8) | in_cursor[1];
|
||||
in_cursor += 2;
|
||||
if (!chunk_size) break;
|
||||
|
||||
memcpy(out_cursor, in_cursor, chunk_size);
|
||||
in_cursor += chunk_size;
|
||||
out_cursor += chunk_size;
|
||||
}
|
||||
|
||||
in_cursor = next_ptr;
|
||||
current_block = next_block;
|
||||
}
|
||||
|
||||
// Decompress
|
||||
auto window_size = compression_info.window_size;
|
||||
auto decompressed_buffer = new uint8_t[uncompressed_length];
|
||||
lzx_decompress(compressed_buffer, out_cursor - compressed_buffer,
|
||||
decompressed_buffer, uncompressed_length, window_size, nullptr,
|
||||
0);
|
||||
|
||||
out_data = (uint8_t*)malloc(length);
|
||||
memcpy(out_data, decompressed_buffer + offset, length);
|
||||
|
||||
delete[] in_buffer;
|
||||
delete[] compressed_buffer;
|
||||
delete[] decompressed_buffer;
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
X_STATUS ReadXexImage(File* file, XexInfo* info, size_t offset, uint32_t length,
|
||||
uint8_t*& out_data) {
|
||||
auto format = info->file_format_info;
|
||||
|
||||
switch (format->compression_type) {
|
||||
case XEX_COMPRESSION_NONE:
|
||||
return ReadXexImageUncompressed(file, info, offset, length, out_data);
|
||||
case XEX_COMPRESSION_BASIC:
|
||||
return ReadXexImageBasicCompressed(file, info, offset, length, out_data);
|
||||
case XEX_COMPRESSION_NORMAL:
|
||||
return ReadXexImageNormalCompressed(file, info, offset, length, out_data);
|
||||
default:
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
}
|
||||
|
||||
inline void ReadXexAltTitleIds(uint8_t* data, XexInfo* info) {
|
||||
uint32_t length = xe::load_and_swap<uint32_t>(data);
|
||||
uint32_t count = (length - 0x04) / 0x04;
|
||||
|
||||
info->alt_title_ids_count = count;
|
||||
info->alt_title_ids = new uint32_t[length];
|
||||
|
||||
uint8_t* cursor = data + 0x04;
|
||||
for (uint32_t i = 0; i < length; i++, cursor += 0x04) {
|
||||
info->alt_title_ids[i] = xe::load_and_swap<uint32_t>(cursor);
|
||||
}
|
||||
|
||||
XELOGI("%d alternate title ids found", count);
|
||||
}
|
||||
|
||||
inline void ReadXexExecutionInfo(uint8_t* data, XexInfo* info) {
|
||||
uint32_t length = sizeof(xex2_opt_execution_info);
|
||||
memcpy(&info->execution_info, data, length);
|
||||
XELOGI("Read ExecutionInfo. TitleID: {}, MediaID: {}",
|
||||
xe::string_util::to_hex_string(info->execution_info.title_id),
|
||||
xe::string_util::to_hex_string(info->execution_info.media_id));
|
||||
}
|
||||
|
||||
inline void ReadXexFileFormatInfo(uint8_t* data, XexInfo* info) {
|
||||
uint32_t length = xe::load_and_swap<uint32_t>(data); // TODO
|
||||
info->file_format_info = (xex2_opt_file_format_info*)calloc(1, length);
|
||||
memcpy(info->file_format_info, data, length);
|
||||
XELOGI("Read FileFormatInfo. Encryption: {}, Compression: {}",
|
||||
info->file_format_info->encryption_type == XEX_ENCRYPTION_NORMAL
|
||||
? "NORMAL"
|
||||
: "NONE",
|
||||
info->file_format_info->compression_type == XEX_COMPRESSION_NORMAL
|
||||
? "NORMAL"
|
||||
: info->file_format_info->compression_type == XEX_COMPRESSION_BASIC
|
||||
? "BASIC"
|
||||
: "NONE");
|
||||
}
|
||||
|
||||
inline void ReadXexGameRatings(uint8_t* data, XexInfo* info) {
|
||||
uint32_t length = 0xC;
|
||||
memcpy(&info->game_ratings, data, 0xC);
|
||||
XELOGI("Read GameRatings.");
|
||||
}
|
||||
|
||||
inline void ReadXexMultiDiscMediaIds(uint8_t* data, XexInfo* info) {
|
||||
uint32_t entry_size = sizeof(xex2_multi_disc_media_id_t);
|
||||
uint32_t length = xe::load_and_swap<uint32_t>(data);
|
||||
uint32_t count = (length - 0x04) / 0x10;
|
||||
|
||||
info->multi_disc_media_ids_count = count;
|
||||
info->multi_disc_media_ids =
|
||||
(xex2_multi_disc_media_id_t*)calloc(count, entry_size);
|
||||
|
||||
uint8_t* cursor = data + 0x04;
|
||||
for (uint32_t i = 0; i < count; i++, cursor += 0x10) {
|
||||
auto id = &info->multi_disc_media_ids[i];
|
||||
memcpy(id->hash, cursor, 0x0C);
|
||||
id->media_id = xe::load_and_swap<uint32_t>(cursor + 0x0C);
|
||||
}
|
||||
|
||||
XELOGI("Read %d MultiDisc Media IDs. Disc is %d of %d", count,
|
||||
info->execution_info.disc_number, info->execution_info.disc_count);
|
||||
}
|
||||
|
||||
inline void ReadXexOriginalPeName(uint8_t* data, XexInfo* info) {
|
||||
uint32_t length = xe::load_and_swap<uint32_t>(data);
|
||||
|
||||
info->original_pe_name = (xex2_opt_original_pe_name*)calloc(1, length);
|
||||
memcpy(info->original_pe_name, data, length);
|
||||
XELOGI("Read OriginalPeName: {}", info->original_pe_name->name);
|
||||
}
|
||||
|
||||
inline void ReadXexResourceInfo(uint8_t* data, XexInfo* info) {
|
||||
uint32_t size = sizeof(xex2_opt_resource_info);
|
||||
uint32_t length = xe::load_and_swap<uint32_t>(data);
|
||||
uint32_t count = (length - 0x04) / 0x10;
|
||||
|
||||
info->resources_count = count;
|
||||
info->resources = (xex2_resource*)calloc(count, size);
|
||||
memcpy(info->resources, data + 0x4, count * size);
|
||||
XELOGI("Read %d resource infos", count);
|
||||
|
||||
/*uint8_t* cursor = data + 0x04;
|
||||
for (uint32_t i = 0; i < count; i++, cursor += 0x10) {
|
||||
auto resource_info = &info->resources[i];
|
||||
memcpy(resource_info->name, cursor, 0x08);
|
||||
resource_info->address = xe::load_and_swap<uint32_t>(cursor + 0x08);
|
||||
resource_info->size = xe::load_and_swap<uint32_t>(cursor + 0x0C);
|
||||
}*/
|
||||
}
|
||||
|
||||
inline void ReadXexOptHeader(xex2_opt_header* entry, uint8_t* data,
|
||||
XexInfo* info) {
|
||||
switch (entry->key) {
|
||||
case XEX_HEADER_ALTERNATE_TITLE_IDS:
|
||||
ReadXexAltTitleIds(data + entry->offset, info);
|
||||
break;
|
||||
case XEX_HEADER_EXECUTION_INFO:
|
||||
ReadXexExecutionInfo(data + entry->offset, info);
|
||||
break;
|
||||
case XEX_HEADER_FILE_FORMAT_INFO:
|
||||
ReadXexFileFormatInfo(data + entry->offset, info);
|
||||
break;
|
||||
case XEX_HEADER_GAME_RATINGS:
|
||||
ReadXexGameRatings(data + entry->offset, info);
|
||||
break;
|
||||
case XEX_HEADER_MULTIDISC_MEDIA_IDS:
|
||||
ReadXexMultiDiscMediaIds(data + entry->offset, info);
|
||||
break;
|
||||
case XEX_HEADER_RESOURCE_INFO:
|
||||
ReadXexResourceInfo(data + entry->offset, info);
|
||||
break;
|
||||
case XEX_HEADER_ORIGINAL_PE_NAME:
|
||||
ReadXexOriginalPeName(data + entry->offset, info);
|
||||
break;
|
||||
case XEX_HEADER_IMAGE_BASE_ADDRESS:
|
||||
info->base_address = entry->value;
|
||||
break;
|
||||
case XEX_HEADER_SYSTEM_FLAGS:
|
||||
info->system_flags =
|
||||
(xex2_system_flags)xe::byte_swap<uint32_t>(entry->value.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
X_STATUS TryKey(const uint8_t* key, uint8_t* magic_block, XexInfo* info,
|
||||
File* file) {
|
||||
uint8_t decrypted_block[0x10];
|
||||
const uint16_t PE_MAGIC = 0x4D5A;
|
||||
auto compression_type = info->file_format_info->compression_type;
|
||||
auto aes_key = reinterpret_cast<uint8_t*>(info->security_info.aes_key);
|
||||
|
||||
aes_decrypt_buffer(key, aes_key, 0x10, info->session_key, 0x10);
|
||||
aes_decrypt_buffer(info->session_key, magic_block, 0x10, decrypted_block,
|
||||
0x10);
|
||||
memcpy(info->session_key, info->session_key, 0x10);
|
||||
|
||||
// Decompress if the XEX is lzx compressed
|
||||
if (compression_type == XEX_COMPRESSION_NORMAL) {
|
||||
uint8_t* data;
|
||||
if (XFAILED(ReadXexImageNormalCompressed(file, info, 0, 0x10, data)))
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
memcpy(decrypted_block, data, 0x10);
|
||||
delete[] data;
|
||||
}
|
||||
|
||||
uint16_t found_magic = xe::load_and_swap<uint16_t>(decrypted_block);
|
||||
if (found_magic == PE_MAGIC) return X_STATUS_SUCCESS;
|
||||
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
X_STATUS ReadSessionKey(File* file, XexInfo* info) {
|
||||
const uint16_t PE_MAGIC = 0x4D5A;
|
||||
uint8_t magic_block[0x10];
|
||||
Read(file, magic_block, info->header_size, 0x10);
|
||||
|
||||
// Check if the XEX is already decrypted.
|
||||
// If decrypted with a third party tool like xextool, the encryption flag
|
||||
// is not changed, but the data is decrypted.
|
||||
uint16_t found_magic = xe::load_and_swap<uint16_t>(magic_block);
|
||||
if (found_magic == PE_MAGIC) {
|
||||
info->file_format_info->encryption_type = XEX_ENCRYPTION_NONE;
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
// XEX is encrypted, derive the session key.
|
||||
if (XFAILED(TryKey(xex2_retail_key, magic_block, info, file)))
|
||||
if (XFAILED(TryKey(xex2_devkit_key, magic_block, info, file))) {
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
X_STATUS ReadXexHeaderSecurityInfo(File* file, XexInfo* info) {
|
||||
const uint32_t length = 0x180;
|
||||
uint8_t data[length];
|
||||
Read(file, data, info->security_offset, length);
|
||||
|
||||
memcpy(&info->security_info, data, sizeof(xex2_security_info));
|
||||
|
||||
// XEX is still encrypted. Derive the session key.
|
||||
if (XFAILED(ReadSessionKey(file, info))) {
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
X_STATUS ReadXexHeaderSectionInfo(File* file, XexInfo* info) {
|
||||
uint32_t offset = info->security_offset;
|
||||
uint32_t count = Read<uint32_t>(file, offset);
|
||||
uint32_t size = sizeof(xex2_page_descriptor);
|
||||
uint32_t length = count * size;
|
||||
uint8_t* data = new uint8_t[length];
|
||||
|
||||
Read(file, data, offset, length);
|
||||
|
||||
info->page_descriptors_count = count;
|
||||
info->page_descriptors = (xex2_page_descriptor*)calloc(count, size);
|
||||
|
||||
if (!info->page_descriptors || !count) {
|
||||
XELOGE("No xex page descriptors are present");
|
||||
|
||||
delete[] data;
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
uint8_t* cursor = data + 0x04;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
auto section = &info->page_descriptors[i];
|
||||
|
||||
section->value = xe::load<uint32_t>(cursor + 0x00);
|
||||
memcpy(section->data_digest, cursor + 0x04, sizeof(section->data_digest));
|
||||
cursor += 0x04 + sizeof(section->data_digest);
|
||||
}
|
||||
|
||||
XELOGI("Section info successfully read. %d page descriptors found.", count);
|
||||
delete[] data;
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
X_STATUS ReadXexHeader(File* file, XexInfo* info) {
|
||||
uint32_t header_size = Read<uint32_t>(file, 0x8);
|
||||
uint8_t* data = new uint8_t[header_size];
|
||||
Read(file, data, 0x0, header_size);
|
||||
|
||||
// Read Main Header Data
|
||||
info->module_flags =
|
||||
(xex2_module_flags)xe::load_and_swap<uint32_t>(data + 0x04);
|
||||
info->header_size = xe::load_and_swap<uint32_t>(data + 0x08);
|
||||
info->security_offset = xe::load_and_swap<uint32_t>(data + 0x10);
|
||||
info->header_count = xe::load_and_swap<uint32_t>(data + 0x14);
|
||||
|
||||
// Read Optional Headers
|
||||
uint8_t* cursor = data + 0x18;
|
||||
for (uint32_t i = 0; i < info->header_count; i++, cursor += 0x8) {
|
||||
auto entry = reinterpret_cast<xex2_opt_header*>(cursor);
|
||||
ReadXexOptHeader(entry, data, info);
|
||||
}
|
||||
|
||||
if (XFAILED(ReadXexHeaderSecurityInfo(file, info))) {
|
||||
XELOGE("Could not read xex security info");
|
||||
delete[] data;
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
if (XFAILED(ReadXexHeaderSectionInfo(file, info))) {
|
||||
XELOGE("Could not read xex section info");
|
||||
delete[] data;
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
delete[] data;
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
X_STATUS ReadXexResources(File* file, XexInfo* info) {
|
||||
auto resources = info->resources;
|
||||
if (resources == nullptr) {
|
||||
XELOGI("XEX has no resources");
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
XELOGI("Reading %d XEX resources.", info->resources_count);
|
||||
|
||||
for (size_t i = 0; i < info->resources_count; i++) {
|
||||
auto resource = &resources[i];
|
||||
|
||||
uint32_t title_id = info->execution_info.title_id;
|
||||
uint32_t name =
|
||||
xe::string_util::from_string<uint32_t>(resource->name, true);
|
||||
XELOGI("Found resource: %X", name);
|
||||
|
||||
// Game resources are listed as the TitleID
|
||||
if (name == title_id) {
|
||||
uint32_t offset = resource->address - info->base_address;
|
||||
XELOGI("Found XBDF resource at %08x with size %08x", offset,
|
||||
resource->size);
|
||||
|
||||
uint8_t* data;
|
||||
if (XFAILED(ReadXexImage(file, info, offset, resource->size, data))) {
|
||||
XELOGE("Could not read XBDF resource.");
|
||||
delete[] data;
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
auto xbdf_data = XdbfGameData(data, resource->size);
|
||||
|
||||
if (!xbdf_data.is_valid()) {
|
||||
delete[] data;
|
||||
XELOGE("XBDF data is invalid.");
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
// Extract Game Title
|
||||
info->game_title = xbdf_data.title();
|
||||
|
||||
// Extract Game Icon
|
||||
auto icon = xbdf_data.icon();
|
||||
info->icon_size = icon.size;
|
||||
info->icon = (uint8_t*)calloc(1, icon.size);
|
||||
memcpy(info->icon, icon.buffer, icon.size);
|
||||
|
||||
// TODO: Extract Achievements
|
||||
|
||||
delete[] data;
|
||||
XELOGI("Successfully read XBDF resource. Game title: {}, icon size: %08x",
|
||||
info->game_title.c_str(), info->icon_size);
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
X_STATUS XexScanner::ScanXex(File* file, GameInfo* out_info) {
|
||||
if (XFAILED(ReadXexHeader(file, &out_info->xex_info))) {
|
||||
XELOGE("ReadXexHeader failed");
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
if (XFAILED(ReadXexResources(file, &out_info->xex_info))) {
|
||||
XELOGE("ReadXexResources failed");
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
XELOGI("ScanXex was successful");
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
|
@ -0,0 +1,33 @@
|
|||
#ifndef XENIA_APP_XEX_SCANNER_H_
|
||||
#define XENIA_APP_XEX_SCANNER_H_
|
||||
|
||||
#include "xenia/app/library/game_entry.h"
|
||||
#include "xenia/app/library/scanner_utils.h"
|
||||
#include "xenia/kernel/util/xdbf_utils.h"
|
||||
#include "xenia/kernel/util/xex2_info.h"
|
||||
#include "xenia/vfs/file.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
using kernel::util::XdbfGameData;
|
||||
using vfs::File;
|
||||
|
||||
static const uint8_t xex2_retail_key[16] = {0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28,
|
||||
0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB,
|
||||
0x08, 0x96, 0xBF, 0x91};
|
||||
static const uint8_t xex2_devkit_key[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
class XexScanner {
|
||||
public:
|
||||
static X_STATUS ScanXex(File* xex_file, GameInfo* out_info);
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -1,5 +1,6 @@
|
|||
project_root = "../../.."
|
||||
include(project_root.."/tools/build")
|
||||
local qt = premake.extensions.qt
|
||||
|
||||
group("src")
|
||||
project("xenia-app")
|
||||
|
@ -39,20 +40,104 @@ project("xenia-app")
|
|||
"xenia-hid-sdl",
|
||||
"xenia-kernel",
|
||||
"xenia-ui",
|
||||
"xenia-ui-qt",
|
||||
"xenia-ui-spirv",
|
||||
"xenia-ui-vulkan",
|
||||
"xenia-vfs",
|
||||
"xxhash",
|
||||
})
|
||||
|
||||
-- Setup Qt libraries
|
||||
qt.enable()
|
||||
qtmodules{"core", "gui", "widgets"}
|
||||
qtprefix "Qt5"
|
||||
configuration {"Checked"}
|
||||
qtsuffix "d"
|
||||
configuration {"Debug"}
|
||||
qtsuffix "d"
|
||||
configuration {}
|
||||
if qt.defaultpath ~= nil then
|
||||
qtpath(qt.defaultpath)
|
||||
end
|
||||
|
||||
-- Qt static configuration (if necessary). Used by AppVeyor.
|
||||
if os.getenv("QT_STATIC") then
|
||||
qt.modules["AccessibilitySupport"] = {
|
||||
name = "AccessibilitySupport",
|
||||
include = "QtAccessibilitySupport",
|
||||
}
|
||||
qt.modules["EventDispatcherSupport"] = {
|
||||
name = "EventDispatcherSupport",
|
||||
include = "QtEventDispatcherSupport",
|
||||
}
|
||||
qt.modules["FontDatabaseSupport"] = {
|
||||
name = "FontDatabaseSupport",
|
||||
include = "QtFontDatabaseSupport",
|
||||
}
|
||||
qt.modules["ThemeSupport"] = {
|
||||
name = "ThemeSupport",
|
||||
include = "QtThemeSupport",
|
||||
}
|
||||
qt.modules["VulkanSupport"] = {
|
||||
name = "VulkanSupport",
|
||||
include = "QtVulkanSupport",
|
||||
}
|
||||
|
||||
defines({"QT_STATIC=1"})
|
||||
|
||||
configuration {"not Checked"}
|
||||
links({
|
||||
"qtmain",
|
||||
"qtfreetype",
|
||||
"qtlibpng",
|
||||
"qtpcre2",
|
||||
"qtharfbuzz",
|
||||
})
|
||||
configuration {"Checked"}
|
||||
links({
|
||||
"qtmaind",
|
||||
"qtfreetyped",
|
||||
"qtlibpngd",
|
||||
"qtpcre2d",
|
||||
"qtharfbuzzd",
|
||||
})
|
||||
configuration {}
|
||||
qtmodules{"AccessibilitySupport", "EventDispatcherSupport", "FontDatabaseSupport", "ThemeSupport", "VulkanSupport"}
|
||||
libdirs("%{cfg.qtpath}/plugins/platforms")
|
||||
|
||||
filter("platforms:Windows")
|
||||
-- Qt dependencies
|
||||
links({
|
||||
"dwmapi",
|
||||
"version",
|
||||
"imm32",
|
||||
"winmm",
|
||||
"netapi32",
|
||||
"userenv",
|
||||
})
|
||||
configuration {"not Checked"}
|
||||
links({"qwindows"})
|
||||
configuration {"Checked"}
|
||||
links({"qwindowsd"})
|
||||
configuration {}
|
||||
filter()
|
||||
end
|
||||
|
||||
filter("platforms:Windows")
|
||||
entrypoint("wWinMainCRTStartup")
|
||||
|
||||
defines({
|
||||
"XBYAK_NO_OP_NAMES",
|
||||
"XBYAK_ENABLE_OMITTED_OPERAND",
|
||||
})
|
||||
local_platform_files()
|
||||
recursive_platform_files()
|
||||
files({
|
||||
"xenia_main.cc",
|
||||
"../base/main_"..platform_suffix..".cc",
|
||||
"../base/main_init_"..platform_suffix..".cc",
|
||||
|
||||
-- Qt files
|
||||
"*.qrc",
|
||||
})
|
||||
|
||||
resincludedirs({
|
||||
|
@ -60,8 +145,8 @@ project("xenia-app")
|
|||
})
|
||||
|
||||
filter("platforms:Windows")
|
||||
files({
|
||||
"main_resources.rc",
|
||||
resincludedirs({
|
||||
project_root,
|
||||
})
|
||||
|
||||
filter("files:../base/main_init_"..platform_suffix..".cc")
|
||||
|
@ -92,4 +177,7 @@ project("xenia-app")
|
|||
debugdir(project_root)
|
||||
debugargs({
|
||||
})
|
||||
debugenvs({
|
||||
"PATH=%{cfg.qtpath}/bin",
|
||||
})
|
||||
end
|
||||
|
|
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,30 @@
|
|||
QWidget#DebugTab QLabel {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
QWidget#DebugTab QScrollArea {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
QWidget#DebugTab QScrollArea > QWidget > QWidget {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
QWidget#sidebarContainer {
|
||||
background: $tertiary;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
QWidget#sidebarTitleLabel {
|
||||
color: $primary;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
QWidget#sidebarTitle #XSeperator {
|
||||
background: $dark1;
|
||||
}
|
||||
|
||||
QWidget#navigationContainer {
|
||||
background: $dark2;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
QWidget#sidebarContainer {
|
||||
background: $tertiary;
|
||||
min-width: 300;
|
||||
max-width: 300;
|
||||
}
|
||||
|
||||
QWidget#sidebarTitle {
|
||||
min-height: 140;
|
||||
max-height: 140;
|
||||
}
|
||||
|
||||
QLabel#sidebarTitleLabel {
|
||||
color: $primary;
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
QLabel#sidebarSubtitleLabel {
|
||||
color: $light2;
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
QWidget#XSeparator {
|
||||
qproperty-thickness: 2;
|
||||
min-width: 250;
|
||||
max-width: 250;
|
||||
background: $dark1;
|
||||
}
|
||||
|
||||
QToolBar#XToolBar {
|
||||
min-height: 46;
|
||||
max-height: 46;
|
||||
}
|
||||
|
||||
QLabel#recentGames {
|
||||
font-family: "Segoe UI";
|
||||
font-size: 18px;
|
||||
color: $primary;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
QMainWindow {
|
||||
background-color: $background;
|
||||
}
|
||||
|
||||
QLabel#buildLabel {
|
||||
color: $light2;
|
||||
padding: 0px 4px 0px 4px;
|
||||
font-size: 10px;
|
||||
qproperty-alignment: AlignTop;
|
||||
}
|
||||
|
||||
QMenu {
|
||||
background-color: $dark1;
|
||||
border: 1px solid $light3;
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
background-color: transparent;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: $secondaryDark;
|
||||
}
|
||||
|
||||
/* Global Scrollbar theming */
|
||||
|
||||
QScrollBar:horizontal {
|
||||
background: $dark2;
|
||||
height: 15px;
|
||||
margin: 0px 20px 0 20px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background: $dark1;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal {
|
||||
background: $dark2;
|
||||
width: 20px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::sub-line:horizontal {
|
||||
background: $dark2;
|
||||
width: 20px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:horizontal,
|
||||
QScrollBar::sub-page:horizontal {
|
||||
background: none;
|
||||
}
|
||||
|
||||
QScrollBar::left-arrow:horizontal {
|
||||
image: url(:/resources/graphics/left-arrow.png);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
QScrollBar::right-arrow:horizontal {
|
||||
image: url(:/resources/graphics/right-arrow.png);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
QScrollBar:vertical {
|
||||
background: $dark2;
|
||||
width: 15px;
|
||||
margin: 20px 0 20px 0;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background: $dark1;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical {
|
||||
background: $dark2;
|
||||
height: 20px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::sub-line:vertical {
|
||||
background: $dark2;
|
||||
height: 20px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:vertical,
|
||||
QScrollBar::sub-page:vertical {
|
||||
background: $dark2;
|
||||
}
|
||||
|
||||
/* up and down are inversed here for some reason */
|
||||
QScrollBar::up-arrow:vertical {
|
||||
image: url(:/resources/graphics/down-arrow.png);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
QScrollBar::down-arrow:vertical {
|
||||
image: url(:/resources/graphics/up-arrow.png);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
QWidget#SettingsTab QLabel {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
QWidget#SettingsTab > QScrollArea {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
QWidget#SettingsTab > QScrollArea > QWidget > QWidget {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
QWidget#sidebarContainer {
|
||||
background: $tertiary;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
QWidget#sidebarTitleLabel {
|
||||
color: $primary;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
QWidget#sidebarTitle #XSeperator {
|
||||
background: $dark1;
|
||||
}
|
||||
|
||||
QWidget#navigationContainer {
|
||||
background: $dark2;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
QWidget#XCard {
|
||||
background: $dark2;
|
||||
border-top-right-radius: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
color: $light1;
|
||||
}
|
||||
|
||||
QListWidget {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
QListWidget::item {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
for some reason QSS property values take priority over calling the setter of the property programatically
|
||||
to work around this users can set the custom property to true before calling any setters
|
||||
*/
|
||||
QCheckBox#XCheckBox[custom="false"] {
|
||||
qproperty-checked_color: $secondary;
|
||||
}
|
||||
|
||||
/*
|
||||
these two properties cannot be changed programatically, to allow this move them into the above style
|
||||
*/
|
||||
QCheckBox#XCheckBox {
|
||||
qproperty-border_color: $light2;
|
||||
qproperty-focus_color: $secondary;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
QComboBox#XComboBox {
|
||||
border: 2px solid $light3;
|
||||
background: $dark2;
|
||||
color: $light2;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
QComboBox#XComboBox:pressed,
|
||||
QComboBox#XComboBox:hover,
|
||||
QComboBox#XComboBox:focus,
|
||||
QComboBox#XComboBox:on {
|
||||
border: 2px solid $secondaryDark;
|
||||
}
|
||||
|
||||
QComboBox#XComboBox::drop-down {
|
||||
border: 0
|
||||
}
|
||||
|
||||
QComboBox#XComboBox::down-arrow {
|
||||
margin-right: 4px;
|
||||
image: url(:/resources/graphics/down-arrow.png);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
QComboBox#XComboBox::item
|
||||
{
|
||||
background: $dark2;
|
||||
}
|
||||
|
||||
QComboBox#XComboBox::item:selected
|
||||
{
|
||||
background: $secondaryDark;
|
||||
}
|
||||
|
||||
QComboBox#XComboBox::item:checked
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QComboBox#XComboBox QAbstractItemView {
|
||||
background: $dark2;
|
||||
color: $light2;
|
||||
selection-background-color: $secondaryDark;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* QComboBox#XComboBox QFrame:focus {
|
||||
background: $secondaryDark;
|
||||
} */
|
|
@ -0,0 +1,51 @@
|
|||
QToolButton#XDropdownButton {
|
||||
background: $dark1;
|
||||
color: $primary;
|
||||
border: 2px solid $light3;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
QToolButton#XDropdownButton:disabled {
|
||||
background: $light3;
|
||||
color: $dark1;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QToolButton#XDropdownButton:hover:!pressed,
|
||||
QToolButton#XDropdownButton:focus {
|
||||
border: 2px solid $secondaryDark;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QToolButton#XDropdownButton:pressed {
|
||||
background: $secondaryDark;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QToolButton#XDropdownButton[popupMode="1"] {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
QToolButton#XDropdownButton::menu-button {
|
||||
width: 15px;
|
||||
margin: 2px;
|
||||
background: $dark2;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QToolButton#XDropdownButton:disabled,
|
||||
QToolButton#XDropdownButton::menu-button {
|
||||
width: 15px;
|
||||
margin: 2px;
|
||||
background: $light3;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QToolButton#XDropdownButton::menu-button:pressed {
|
||||
background: $secondaryDark;
|
||||
}
|
||||
|
||||
QToolButton#XDropdownButton::menu-arrow:open {
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
QGroupBox {
|
||||
background-color: $accent;
|
||||
border: transparent;
|
||||
border-radius: 2px;
|
||||
margin: 20px 4px 4px 4px; /* leave space at the top for the title */
|
||||
color: $primary;
|
||||
font-family: "Segoe UI Semibold";
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top left; /* position at the top left */
|
||||
background: transparent;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
QLineEdit#XLineEdit {
|
||||
background-color: $dark1;
|
||||
color: $primary;
|
||||
border: 2px solid $light3;
|
||||
}
|
||||
|
||||
QLineEdit#XLineEdit:focus {
|
||||
border: 2px solid $secondaryDark;
|
||||
}
|
||||
|
||||
QLineEdit#XLineEdit:disabled {
|
||||
color: $light2;
|
||||
border: 2px solid $dark1;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
QWidget#XNav {
|
||||
background-color: $accent;
|
||||
min-height: 60px;
|
||||
max-height: 60px;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
QPushButton#XPushButton {
|
||||
background: $dark1;
|
||||
color: $primary;
|
||||
border: 2px solid $light3;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
QPushButton#XPushButton:disabled {
|
||||
background: $light3;
|
||||
color: $dark1;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QPushButton#XPushButton:hover:!pressed,
|
||||
QPushButton#XPushButton:focus {
|
||||
border: 2px solid $secondaryDark;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
QPushButton#XPushButton:pressed {
|
||||
background: $secondaryDark;
|
||||
border: none;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
for some reason QSS property values take priority over calling the setter of the property programatically
|
||||
to work around this users can set the custom property to true before calling any setters
|
||||
*/
|
||||
QRadioButton#XRadioButton[custom="false"] {
|
||||
qproperty-checked_color: $secondary;
|
||||
}
|
||||
|
||||
/*
|
||||
these two properties cannot be changed programatically, to allow this move them into the above style
|
||||
*/
|
||||
QRadioButton#XRadioButton {
|
||||
qproperty-border_color: $light2;
|
||||
qproperty-focus_color: $secondary;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
QScrollArea#XScrollArea {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QScrollArea#XScrollArea > QWidget > QWidget {
|
||||
background: transparent;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
QToolBar::separator {
|
||||
background: $dark1;
|
||||
min-width: 300;
|
||||
max-width: 300;
|
||||
height: 2;
|
||||
}
|
||||
|
||||
QPushButton#XSideBarButton {
|
||||
max-height: 50;
|
||||
min-height: 50;
|
||||
max-width: 300;
|
||||
min-width: 300;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
QPushButton {
|
||||
color: $primary;
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: $dark1;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: $dark2;
|
||||
}
|
||||
|
||||
QPushButton:checked {
|
||||
background-color: $dark2;
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
QSlider#XSlider:horizontal {
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::groove:horizontal {
|
||||
background: $secondaryDark;
|
||||
height: 4px;
|
||||
margin: 0px 1px 0px 1px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::handle:horizontal {
|
||||
width: 10px;
|
||||
border-radius: 5px;
|
||||
background: $secondary;
|
||||
margin: -3px -1px -3px -1px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::handle:horizontal:focus {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
border-radius: 7px;
|
||||
background: $secondary;
|
||||
margin: -5px -1px -5px -1px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::groove:vertical {
|
||||
background: $secondaryDark;
|
||||
width: 4px;
|
||||
margin: 1px 0px 1px 0px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::handle:vertical {
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background: $secondary;
|
||||
margin: -1px -3px -1px -3px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::handle:vertical:focus {
|
||||
height: 14px;
|
||||
border-radius: 7px;
|
||||
background: $secondary;
|
||||
margin: -1px -5px -1px -5px;
|
||||
}
|
||||
|
||||
/* Uncomment these to make slider color different before the handle to after */
|
||||
|
||||
QSlider#XSlider::sub-page:horizontal {
|
||||
background: $secondaryDark;
|
||||
margin: 0px 1px 0px 1px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::add-page:horizontal {
|
||||
background: $light3;
|
||||
margin: 0px 1px 0px 1px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::add-page:vertical {
|
||||
background: $secondaryDark;
|
||||
margin: 1px 0px 1px 0px;
|
||||
}
|
||||
|
||||
QSlider#XSlider::sub-page:vertical {
|
||||
background: $light3;
|
||||
margin: 1px 0px 1px 0px;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
QStatusBar#XStatusBar {
|
||||
background-color: rgb(40, 40, 40);
|
||||
color: $light2;
|
||||
}
|
||||
|
||||
QStatusBar#XStatusBar::item {
|
||||
border: 0px;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
QWidget#XTab {
|
||||
}
|
||||
|
||||
QLabel#placeholder {
|
||||
font: ":resources/fonts/segoeui.ttf";
|
||||
color: $primary;
|
||||
font-size: 48px;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
QWidget#XTabSelector {
|
||||
qproperty-bar_color: $secondary;
|
||||
qproperty-font_color: $primary;
|
||||
qproperty-font_size: 24;
|
||||
qproperty-tab_spacing: 20;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
QHeaderView#XTableHeader {
|
||||
background-color: #373737;
|
||||
}
|
||||
|
||||
QHeaderView#XTableHeader::section {
|
||||
border: 0px;
|
||||
border-left: 1px solid #7f7f7f;
|
||||
background-color: #373737;
|
||||
color: $primary;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin: 3px 0;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
QHeaderView#XTableHeader::section:first {
|
||||
border: 0px;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
QTableView#XTableView {
|
||||
color: $primary;
|
||||
background-color: $background;
|
||||
}
|
||||
|
||||
QTableView#XTableView::item {
|
||||
background-color: #232323;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
QTableView#XTableView::item::selected {
|
||||
background-color: #4aa971;
|
||||
}
|
||||
|
||||
QTableView#XTableView::header {
|
||||
background-color: #373737;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
QPlainTextEdit#XTextEdit {
|
||||
background-color: $dark1;
|
||||
color: $primary;
|
||||
border: 2px solid $light3;
|
||||
}
|
||||
|
||||
QPlainTextEdit#XTextEdit:focus {
|
||||
border: 2px solid $secondaryDark;
|
||||
}
|
||||
|
||||
QPlainTextEdit#XTextEdit:disabled {
|
||||
color: $light2;
|
||||
border: 2px solid $dark1;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
QToolBar#XToolBar {
|
||||
background-color: #232323;
|
||||
color: #ffffff;
|
||||
min-height: 44px;
|
||||
qproperty-spacing: 32;
|
||||
}
|
||||
|
||||
QToolBar::separator:horizontal {
|
||||
background: #626262;
|
||||
width: 1px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
QToolButton {
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
QToolButton#XToolBarItem {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "Xenia",
|
||||
"description": "The default xenia theme. This theme is dark and clean",
|
||||
"colors": [
|
||||
{
|
||||
"name": "primary",
|
||||
"value": "#FFFFFF"
|
||||
},
|
||||
{
|
||||
"name": "secondary",
|
||||
"value": "#5CE494"
|
||||
},
|
||||
{
|
||||
"name": "secondaryDark",
|
||||
"value": "#4AA971"
|
||||
},
|
||||
{
|
||||
"name": "tertiary",
|
||||
"value": "#232323"
|
||||
},
|
||||
{
|
||||
"name": "background",
|
||||
"value": "#1F1F1F"
|
||||
},
|
||||
{
|
||||
"name": "accent",
|
||||
"value": "#282828"
|
||||
},
|
||||
{
|
||||
"name": "light1",
|
||||
"value": "#FFFFFF"
|
||||
},
|
||||
{
|
||||
"name": "light2",
|
||||
"value": "#C7C7C7"
|
||||
},
|
||||
{
|
||||
"name": "light3",
|
||||
"value": "#7F7F7F"
|
||||
},
|
||||
{
|
||||
"name": "dark1",
|
||||
"value": "#505050"
|
||||
},
|
||||
{
|
||||
"name": "dark2",
|
||||
"value": "#373737"
|
||||
},
|
||||
{
|
||||
"name": "dark3",
|
||||
"value": "#1E1E1E"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#Sidebar {
|
||||
min-width: 60px;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>resources/graphics/icon.ico</file>
|
||||
<file>resources/graphics/GameIconMask.png</file>
|
||||
<file>resources/graphics/down-arrow.png</file>
|
||||
<file>resources/graphics/left-arrow.png</file>
|
||||
<file>resources/graphics/right-arrow.png</file>
|
||||
<file>resources/graphics/up-arrow.png</file>
|
||||
<file>resources/fonts/SegMDL2.ttf</file>
|
||||
<file>resources/fonts/segoeui.ttf</file>
|
||||
<file>resources/fonts/segoeuisb.ttf</file>
|
||||
<file>resources/themes/base.css</file>
|
||||
<file>resources/themes/Xenia/theme.json</file>
|
||||
<file>resources/themes/Xenia/stylesheets/MainWindow.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XNav.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XTab.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XTabSelector.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XTableView.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XTableHeader.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XSlider.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XToolBar.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XToolBarItem.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/HomeTab.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XSideBarButton.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XSideBar.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XStatusBar.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XCard.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XGroupBox.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XPushButton.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XDropdownButton.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XRadioButton.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XCheckBox.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/DebugTab.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XComboBox.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XTextEdit.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XLineEdit.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/SettingsTab.css</file>
|
||||
<file>resources/themes/Xenia/stylesheets/XScrollArea.css</file>
|
||||
</qresource>
|
||||
</RCC>
|
|
@ -2,219 +2,74 @@
|
|||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Copyright 2013 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/app/discord/discord_presence.h"
|
||||
#include "xenia/app/emulator_window.h"
|
||||
#include "xenia/app/library/game_library.h"
|
||||
//#include "xenia/base/debugging.h"
|
||||
//#include "xenia/base/logging.h"
|
||||
#include "xenia/base/cvar.h"
|
||||
#include "xenia/base/debugging.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/main.h"
|
||||
#include "xenia/base/profiling.h"
|
||||
#include "xenia/base/threading.h"
|
||||
//#include "xenia/base/platform.h"
|
||||
//#include "xenia/base/profiling.h"
|
||||
//#include "xenia/base/threading.h"
|
||||
|
||||
#include "xenia/app/emulator_window.h"
|
||||
#include "xenia/ui/qt/main_window.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QtPlugin>
|
||||
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/config.h"
|
||||
#include "xenia/debug/ui/debug_window.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/ui/file_picker.h"
|
||||
#include "xenia/vfs/devices/host_path_device.h"
|
||||
#include "xenia/ui/qt/loop_qt.h"
|
||||
|
||||
// Available audio systems:
|
||||
#include "xenia/apu/nop/nop_audio_system.h"
|
||||
#include "xenia/apu/sdl/sdl_audio_system.h"
|
||||
#if XE_PLATFORM_WIN32
|
||||
#include "xenia/apu/xaudio2/xaudio2_audio_system.h"
|
||||
#endif // XE_PLATFORM_WIN32
|
||||
#if XE_PLATFORM_WIN32 && QT_STATIC
|
||||
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
|
||||
#endif
|
||||
|
||||
// Available graphics systems:
|
||||
#include "xenia/gpu/null/null_graphics_system.h"
|
||||
#include "xenia/gpu/vulkan/vulkan_graphics_system.h"
|
||||
#if XE_PLATFORM_WIN32
|
||||
#include "xenia/gpu/d3d12/d3d12_graphics_system.h"
|
||||
#endif // XE_PLATFORM_WIN32
|
||||
|
||||
// Available input drivers:
|
||||
#include "xenia/hid/nop/nop_hid.h"
|
||||
#include "xenia/hid/sdl/sdl_hid.h"
|
||||
#if XE_PLATFORM_WIN32
|
||||
#include "xenia/hid/winkey/winkey_hid.h"
|
||||
#include "xenia/hid/xinput/xinput_hid.h"
|
||||
#endif // XE_PLATFORM_WIN32
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "third_party/xbyak/xbyak/xbyak_util.h"
|
||||
|
||||
DEFINE_string(apu, "any", "Audio system. Use: [any, nop, sdl, xaudio2]", "APU");
|
||||
DEFINE_string(gpu, "any", "Graphics system. Use: [any, d3d12, vulkan, null]",
|
||||
"GPU");
|
||||
DEFINE_string(hid, "any", "Input system. Use: [any, nop, sdl, winkey, xinput]",
|
||||
"HID");
|
||||
|
||||
DEFINE_bool(fullscreen, false, "Toggles fullscreen", "GPU");
|
||||
|
||||
DEFINE_path(
|
||||
DEFINE_bool(mount_scratch, false, "Enable scratch mount", "General");
|
||||
DEFINE_bool(mount_cache, false, "Enable cache mount", "General");
|
||||
DEFINE_bool(show_debug_tab, true, "Show the debug tab in the Qt UI", "General");
|
||||
DEFINE_string(
|
||||
storage_root, "",
|
||||
"Root path for persistent internal data storage (config, etc.), or empty "
|
||||
"to use the path preferred for the OS, such as the documents folder, or "
|
||||
"the emulator executable directory if portable.txt is present in it.",
|
||||
"Storage");
|
||||
DEFINE_path(
|
||||
DEFINE_string(
|
||||
content_root, "",
|
||||
"Root path for guest content storage (saves, etc.), or empty to use the "
|
||||
"content folder under the storage root.",
|
||||
"Storage");
|
||||
|
||||
DEFINE_bool(mount_scratch, false, "Enable scratch mount", "Storage");
|
||||
DEFINE_bool(mount_cache, false, "Enable cache mount", "Storage");
|
||||
|
||||
DEFINE_transient_path(target, "",
|
||||
"Specifies the target .xex or .iso to execute.",
|
||||
"General");
|
||||
DEFINE_transient_bool(portable, false,
|
||||
"Specifies if Xenia should run in portable mode.",
|
||||
"General");
|
||||
|
||||
DECLARE_bool(debug);
|
||||
|
||||
DEFINE_bool(discord, true, "Enable Discord rich presence", "General");
|
||||
|
||||
namespace xe {
|
||||
namespace app {
|
||||
|
||||
template <typename T, typename... Args>
|
||||
class Factory {
|
||||
private:
|
||||
struct Creator {
|
||||
std::string name;
|
||||
std::function<bool()> is_available;
|
||||
std::function<std::unique_ptr<T>(Args...)> instantiate;
|
||||
};
|
||||
|
||||
std::vector<Creator> creators_;
|
||||
|
||||
public:
|
||||
void Add(const std::string_view name, std::function<bool()> is_available,
|
||||
std::function<std::unique_ptr<T>(Args...)> instantiate) {
|
||||
creators_.push_back({std::string(name), is_available, instantiate});
|
||||
}
|
||||
|
||||
void Add(const std::string_view name,
|
||||
std::function<std::unique_ptr<T>(Args...)> instantiate) {
|
||||
auto always_available = []() { return true; };
|
||||
Add(name, always_available, instantiate);
|
||||
}
|
||||
|
||||
template <typename DT>
|
||||
void Add(const std::string_view name) {
|
||||
Add(name, DT::IsAvailable, [](Args... args) {
|
||||
return std::make_unique<DT>(std::forward<Args>(args)...);
|
||||
});
|
||||
}
|
||||
|
||||
std::unique_ptr<T> Create(const std::string_view name, Args... args) {
|
||||
if (!name.empty() && name != "any") {
|
||||
auto it = std::find_if(
|
||||
creators_.cbegin(), creators_.cend(),
|
||||
[&name](const auto& f) { return name.compare(f.name) == 0; });
|
||||
if (it != creators_.cend() && (*it).is_available()) {
|
||||
return (*it).instantiate(std::forward<Args>(args)...);
|
||||
}
|
||||
return nullptr;
|
||||
} else {
|
||||
for (const auto& creator : creators_) {
|
||||
if (!creator.is_available()) continue;
|
||||
auto instance = creator.instantiate(std::forward<Args>(args)...);
|
||||
if (!instance) continue;
|
||||
return instance;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<T>> CreateAll(const std::string_view name,
|
||||
Args... args) {
|
||||
std::vector<std::unique_ptr<T>> instances;
|
||||
if (!name.empty() && name != "any") {
|
||||
auto it = std::find_if(
|
||||
creators_.cbegin(), creators_.cend(),
|
||||
[&name](const auto& f) { return name.compare(f.name) == 0; });
|
||||
if (it != creators_.cend() && (*it).is_available()) {
|
||||
auto instance = (*it).instantiate(std::forward<Args>(args)...);
|
||||
if (instance) {
|
||||
instances.emplace_back(std::move(instance));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const auto& creator : creators_) {
|
||||
if (!creator.is_available()) continue;
|
||||
auto instance = creator.instantiate(std::forward<Args>(args)...);
|
||||
if (instance) {
|
||||
instances.emplace_back(std::move(instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<apu::AudioSystem> CreateAudioSystem(cpu::Processor* processor) {
|
||||
Factory<apu::AudioSystem, cpu::Processor*> factory;
|
||||
#if XE_PLATFORM_WIN32
|
||||
factory.Add<apu::xaudio2::XAudio2AudioSystem>("xaudio2");
|
||||
#endif // XE_PLATFORM_WIN32
|
||||
factory.Add<apu::sdl::SDLAudioSystem>("sdl");
|
||||
factory.Add<apu::nop::NopAudioSystem>("nop");
|
||||
return factory.Create(cvars::apu, processor);
|
||||
}
|
||||
|
||||
std::unique_ptr<gpu::GraphicsSystem> CreateGraphicsSystem() {
|
||||
Factory<gpu::GraphicsSystem> factory;
|
||||
#if XE_PLATFORM_WIN32
|
||||
factory.Add<gpu::d3d12::D3D12GraphicsSystem>("d3d12");
|
||||
#endif // XE_PLATFORM_WIN32
|
||||
factory.Add<gpu::vulkan::VulkanGraphicsSystem>("vulkan");
|
||||
factory.Add<gpu::null::NullGraphicsSystem>("null");
|
||||
return factory.Create(cvars::gpu);
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<hid::InputDriver>> CreateInputDrivers(
|
||||
ui::Window* window) {
|
||||
std::vector<std::unique_ptr<hid::InputDriver>> drivers;
|
||||
if (cvars::hid.compare("nop") == 0) {
|
||||
drivers.emplace_back(xe::hid::nop::Create(window));
|
||||
} else {
|
||||
Factory<hid::InputDriver, ui::Window*> factory;
|
||||
#if XE_PLATFORM_WIN32
|
||||
factory.Add("xinput", xe::hid::xinput::Create);
|
||||
// WinKey input driver should always be the last input driver added!
|
||||
factory.Add("winkey", xe::hid::winkey::Create);
|
||||
#endif // XE_PLATFORM_WIN32
|
||||
factory.Add("sdl", xe::hid::sdl::Create);
|
||||
for (auto& driver : factory.CreateAll(cvars::hid, window)) {
|
||||
if (XSUCCEEDED(driver->Setup())) {
|
||||
drivers.emplace_back(std::move(driver));
|
||||
}
|
||||
}
|
||||
if (drivers.empty()) {
|
||||
// Fallback to nop if none created.
|
||||
drivers.emplace_back(xe::hid::nop::Create(window));
|
||||
}
|
||||
}
|
||||
return drivers;
|
||||
}
|
||||
|
||||
int xenia_main(const std::vector<std::string>& args) {
|
||||
Profiler::Initialize();
|
||||
Profiler::ThreadEnter("main");
|
||||
/*Profiler::Initialize();
|
||||
Profiler::ThreadEnter("main");*/
|
||||
|
||||
// auto emulator = std::make_unique<xe::Emulator>(L"");
|
||||
|
||||
|
||||
// Start Qt
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||
QCoreApplication::setApplicationName("Xenia");
|
||||
QCoreApplication::setOrganizationName(
|
||||
"Xenia Xbox 360 Emulator Research Project");
|
||||
QCoreApplication::setOrganizationDomain("https://xenia.jp");
|
||||
//QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
|
||||
//Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
|
||||
// Figure out where internal files and content should go.
|
||||
std::filesystem::path storage_root = cvars::storage_root;
|
||||
if (storage_root.empty()) {
|
||||
storage_root = xe::filesystem::GetExecutableFolder();
|
||||
if (!cvars::portable &&
|
||||
!std::filesystem::exists(storage_root / "portable.txt")) {
|
||||
if (!std::filesystem::exists(storage_root / "portable.txt")) {
|
||||
storage_root = xe::filesystem::GetUserFolder();
|
||||
#if defined(XE_PLATFORM_WIN32) || defined(XE_PLATFORM_LINUX)
|
||||
storage_root = storage_root / "Xenia";
|
||||
|
@ -229,43 +84,28 @@ int xenia_main(const std::vector<std::string>& args) {
|
|||
|
||||
config::SetupConfig(storage_root);
|
||||
|
||||
std::filesystem::path content_root = cvars::content_root;
|
||||
if (content_root.empty()) {
|
||||
content_root = storage_root / "content";
|
||||
} else {
|
||||
// If content root isn't an absolute path, then it should be relative to the
|
||||
// storage root.
|
||||
if (!content_root.is_absolute()) {
|
||||
content_root = storage_root / content_root;
|
||||
}
|
||||
}
|
||||
content_root = std::filesystem::absolute(content_root);
|
||||
XELOGI("Content root: {}", xe::path_to_utf8(content_root));
|
||||
int argc = 1;
|
||||
char* argv[] = {"xenia", nullptr};
|
||||
QApplication app(argc, argv);
|
||||
|
||||
if (cvars::discord) {
|
||||
discord::DiscordPresence::Initialize();
|
||||
discord::DiscordPresence::NotPlaying();
|
||||
}
|
||||
// Load Fonts
|
||||
QFontDatabase fonts;
|
||||
fonts.addApplicationFont(":/resources/fonts/SegMDL2.ttf");
|
||||
fonts.addApplicationFont(":/resources/fonts/segoeui.ttf");
|
||||
fonts.addApplicationFont(":/resources/fonts/segoeuisb.ttf");
|
||||
QApplication::setFont(QFont("Segoe UI"));
|
||||
|
||||
// Create the emulator but don't initialize so we can setup the window.
|
||||
auto emulator = std::make_unique<Emulator>("", storage_root, content_root);
|
||||
// EmulatorWindow main_wnd;
|
||||
ui::qt::QtLoop loop;
|
||||
auto main_wnd = new ui::qt::MainWindow(&loop, "Xenia Qt");
|
||||
main_wnd->Initialize();
|
||||
main_wnd->SetIcon(QIcon(":/resources/graphics/icon.ico"));
|
||||
main_wnd->Resize(1280, 720);
|
||||
|
||||
// Main emulator display window.
|
||||
auto emulator_window = EmulatorWindow::Create(emulator.get());
|
||||
|
||||
// Setup and initialize all subsystems. If we can't do something
|
||||
// (unsupported system, memory issues, etc) this will fail early.
|
||||
X_STATUS result =
|
||||
emulator->Setup(emulator_window->window(), CreateAudioSystem,
|
||||
CreateGraphicsSystem, CreateInputDrivers);
|
||||
if (XFAILED(result)) {
|
||||
XELOGE("Failed to setup emulator: {:08X}", result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (cvars::mount_scratch) {
|
||||
/*
|
||||
if (FLAGS_mount_scratch) {
|
||||
auto scratch_device = std::make_unique<xe::vfs::HostPathDevice>(
|
||||
"\\SCRATCH", "scratch", false);
|
||||
"\\SCRATCH", L"scratch", false);
|
||||
if (!scratch_device->Initialize()) {
|
||||
XELOGE("Unable to scan scratch path");
|
||||
} else {
|
||||
|
@ -277,9 +117,13 @@ int xenia_main(const std::vector<std::string>& args) {
|
|||
}
|
||||
}
|
||||
|
||||
if (cvars::mount_cache) {
|
||||
|
||||
|
||||
|
||||
|
||||
if (FLAGS_mount_cache) {
|
||||
auto cache0_device =
|
||||
std::make_unique<xe::vfs::HostPathDevice>("\\CACHE0", "cache0", false);
|
||||
std::make_unique<xe::vfs::HostPathDevice>("\\CACHE0", L"cache0", false);
|
||||
if (!cache0_device->Initialize()) {
|
||||
XELOGE("Unable to scan cache0 path");
|
||||
} else {
|
||||
|
@ -291,7 +135,7 @@ int xenia_main(const std::vector<std::string>& args) {
|
|||
}
|
||||
|
||||
auto cache1_device =
|
||||
std::make_unique<xe::vfs::HostPathDevice>("\\CACHE1", "cache1", false);
|
||||
std::make_unique<xe::vfs::HostPathDevice>("\\CACHE1", L"cache1", false);
|
||||
if (!cache1_device->Initialize()) {
|
||||
XELOGE("Unable to scan cache1 path");
|
||||
} else {
|
||||
|
@ -306,7 +150,7 @@ int xenia_main(const std::vector<std::string>& args) {
|
|||
// Set a debug handler.
|
||||
// This will respond to debugging requests so we can open the debug UI.
|
||||
std::unique_ptr<xe::debug::ui::DebugWindow> debug_window;
|
||||
if (cvars::debug) {
|
||||
if (FLAGS_debug) {
|
||||
emulator->processor()->set_debug_listener_request_handler(
|
||||
[&](xe::cpu::Processor* processor) {
|
||||
if (debug_window) {
|
||||
|
@ -325,97 +169,19 @@ int xenia_main(const std::vector<std::string>& args) {
|
|||
return debug_window.get();
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
auto evt = xe::threading::Event::CreateAutoResetEvent(false);
|
||||
emulator->on_launch.AddListener([&](auto title_id, const auto& game_title) {
|
||||
if (cvars::discord) {
|
||||
discord::DiscordPresence::PlayingTitle(
|
||||
game_title.empty() ? "Unknown Title" : std::string(game_title));
|
||||
}
|
||||
emulator_window->UpdateTitle();
|
||||
evt->Set();
|
||||
});
|
||||
// if (args.size() >= 2) {
|
||||
// // Launch the path passed in args[1].
|
||||
// main_wnd.Launch(args[1]);
|
||||
//}
|
||||
|
||||
emulator->on_shader_storage_initialization.AddListener(
|
||||
[&](bool initializing) {
|
||||
emulator_window->SetInitializingShaderStorage(initializing);
|
||||
});
|
||||
int rc = app.exec();
|
||||
loop.AwaitQuit();
|
||||
|
||||
emulator->on_terminate.AddListener([&]() {
|
||||
if (cvars::discord) {
|
||||
discord::DiscordPresence::NotPlaying();
|
||||
}
|
||||
});
|
||||
|
||||
emulator_window->window()->on_closing.AddListener([&](ui::UIEvent* e) {
|
||||
// This needs to shut down before the graphics context.
|
||||
Profiler::Shutdown();
|
||||
});
|
||||
|
||||
bool exiting = false;
|
||||
emulator_window->loop()->on_quit.AddListener([&](ui::UIEvent* e) {
|
||||
exiting = true;
|
||||
evt->Set();
|
||||
|
||||
if (cvars::discord) {
|
||||
discord::DiscordPresence::Shutdown();
|
||||
}
|
||||
|
||||
// TODO(DrChat): Remove this code and do a proper exit.
|
||||
XELOGI("Cheap-skate exit!");
|
||||
exit(0);
|
||||
});
|
||||
|
||||
// Enable the main menu now that the emulator is properly loaded
|
||||
emulator_window->window()->EnableMainMenu();
|
||||
|
||||
// Grab path from the flag or unnamed argument.
|
||||
std::filesystem::path path;
|
||||
if (!cvars::target.empty()) {
|
||||
path = cvars::target;
|
||||
}
|
||||
|
||||
// Toggles fullscreen
|
||||
if (cvars::fullscreen) emulator_window->ToggleFullscreen();
|
||||
|
||||
if (!path.empty()) {
|
||||
// Normalize the path and make absolute.
|
||||
auto abs_path = std::filesystem::absolute(path);
|
||||
result = emulator->LaunchPath(abs_path);
|
||||
if (XFAILED(result)) {
|
||||
xe::FatalError(fmt::format("Failed to launch target: {:08X}", result));
|
||||
emulator.reset();
|
||||
emulator_window.reset();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now, we're going to use the main thread to drive events related to
|
||||
// emulation.
|
||||
while (!exiting) {
|
||||
xe::threading::Wait(evt.get(), false);
|
||||
|
||||
while (true) {
|
||||
emulator->WaitUntilExit();
|
||||
if (emulator->TitleRequested()) {
|
||||
emulator->LaunchNextTitle();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug_window.reset();
|
||||
emulator.reset();
|
||||
|
||||
if (cvars::discord) {
|
||||
discord::DiscordPresence::Shutdown();
|
||||
}
|
||||
|
||||
Profiler::Dump();
|
||||
Profiler::Shutdown();
|
||||
emulator_window.reset();
|
||||
return 0;
|
||||
/*Profiler::Dump();
|
||||
Profiler::Shutdown();*/
|
||||
return rc;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
|
|
@ -42,4 +42,4 @@ EntryInfo GetEntryInfo();
|
|||
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_BASE_MAIN_H_
|
||||
#endif // XENIA_BASE_MAIN_H_
|
|
@ -128,7 +128,7 @@ int Main() {
|
|||
// Setup COM on the main thread.
|
||||
// NOTE: this may fail if COM has already been initialized - that's OK.
|
||||
#pragma warning(suppress : 6031)
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
// Initialize logging. Needs parsed FLAGS.
|
||||
xe::InitializeLogging(entry_info.name);
|
||||
|
|
|
@ -90,15 +90,13 @@ bool DebugWindow::Initialize() {
|
|||
loop_->on_quit.AddListener([this](UIEvent* e) { window_.reset(); });
|
||||
|
||||
// Main menu.
|
||||
auto main_menu = MenuItem::Create(MenuItem::Type::kNormal);
|
||||
auto file_menu = MenuItem::Create(MenuItem::Type::kPopup, "&File");
|
||||
auto main_menu = window_->CreateMenu();
|
||||
auto file_menu = main_menu->CreateMenuItem("&File");
|
||||
{
|
||||
file_menu->AddChild(MenuItem::Create(MenuItem::Type::kString, "&Close",
|
||||
"Alt+F4",
|
||||
[this]() { window_->Close(); }));
|
||||
file_menu->CreateChild(MenuItem::Type::kString, "&Close", "Alt+F4",
|
||||
[this]() { window_->Close(); });
|
||||
}
|
||||
main_menu->AddChild(std::move(file_menu));
|
||||
window_->set_main_menu(std::move(main_menu));
|
||||
|
||||
|
||||
window_->Resize(1500, 1000);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/emulator.h"
|
||||
#include "xenia/ui/loop.h"
|
||||
#include "xenia/ui/menu_item.h"
|
||||
#include "xenia/ui/menu.h"
|
||||
#include "xenia/ui/window.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
|
|
|
@ -79,7 +79,8 @@ Emulator::Emulator(const std::filesystem::path& command_line,
|
|||
title_id_(0),
|
||||
paused_(false),
|
||||
restoring_(false),
|
||||
restore_fence_() {}
|
||||
restore_fence_(),
|
||||
title_data_(nullptr, 0) {}
|
||||
|
||||
Emulator::~Emulator() {
|
||||
// Note that we delete things in the reverse order they were initialized.
|
||||
|
@ -106,8 +107,7 @@ Emulator::~Emulator() {
|
|||
ExceptionHandler::Uninstall(Emulator::ExceptionCallbackThunk, this);
|
||||
}
|
||||
|
||||
X_STATUS Emulator::Setup(
|
||||
ui::Window* display_window,
|
||||
X_STATUS Emulator::Setup(ui::Window* display_window,
|
||||
std::function<std::unique_ptr<apu::AudioSystem>(cpu::Processor*)>
|
||||
audio_system_factory,
|
||||
std::function<std::unique_ptr<gpu::GraphicsSystem>()>
|
||||
|
@ -162,6 +162,12 @@ X_STATUS Emulator::Setup(
|
|||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
// Bring up the virtual filesystem used by the kernel.
|
||||
file_system_ = std::make_unique<xe::vfs::VirtualFileSystem>();
|
||||
|
||||
// Shared kernel state.
|
||||
kernel_state_ = std::make_unique<xe::kernel::KernelState>(this);
|
||||
|
||||
// Initialize the APU.
|
||||
if (audio_system_factory) {
|
||||
audio_system_ = audio_system_factory(processor_.get());
|
||||
|
@ -193,45 +199,14 @@ X_STATUS Emulator::Setup(
|
|||
return result;
|
||||
}
|
||||
|
||||
// Bring up the virtual filesystem used by the kernel.
|
||||
file_system_ = std::make_unique<xe::vfs::VirtualFileSystem>();
|
||||
|
||||
// Shared kernel state.
|
||||
kernel_state_ = std::make_unique<xe::kernel::KernelState>(this);
|
||||
|
||||
// Setup the core components.
|
||||
result = graphics_system_->Setup(processor_.get(), kernel_state_.get(),
|
||||
display_window_);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (audio_system_) {
|
||||
result = audio_system_->Setup(kernel_state_.get());
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
#define LOAD_KERNEL_MODULE(t) \
|
||||
static_cast<void>(kernel_state_->LoadKernelModule<kernel::t>())
|
||||
// HLE kernel modules.
|
||||
LOAD_KERNEL_MODULE(xboxkrnl::XboxkrnlModule);
|
||||
LOAD_KERNEL_MODULE(xam::XamModule);
|
||||
LOAD_KERNEL_MODULE(xbdm::XbdmModule);
|
||||
#undef LOAD_KERNEL_MODULE
|
||||
kernel_state_->LoadKernelModule<kernel::xboxkrnl::XboxkrnlModule>();
|
||||
kernel_state_->LoadKernelModule<kernel::xam::XamModule>();
|
||||
kernel_state_->LoadKernelModule<kernel::xbdm::XbdmModule>();
|
||||
|
||||
// Initialize emulator fallback exception handling last.
|
||||
ExceptionHandler::Install(Emulator::ExceptionCallbackThunk, this);
|
||||
|
||||
if (display_window_) {
|
||||
// Finish initializing the display.
|
||||
display_window_->loop()->PostSynchronous([this]() {
|
||||
xe::ui::GraphicsContextLock context_lock(display_window_->context());
|
||||
Profiler::set_window(display_window_);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -481,7 +456,7 @@ bool Emulator::RestoreFromFile(const std::filesystem::path& path) {
|
|||
kernel_state_->object_table()->GetObjectsByType<kernel::XThread>();
|
||||
for (auto thread : threads) {
|
||||
if (thread->main_thread()) {
|
||||
main_thread_ = thread;
|
||||
main_thread_ = thread->thread();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -584,9 +559,7 @@ bool Emulator::ExceptionCallback(Exception* ex) {
|
|||
|
||||
void Emulator::WaitUntilExit() {
|
||||
while (true) {
|
||||
if (main_thread_) {
|
||||
xe::threading::Wait(main_thread_->thread(), false);
|
||||
}
|
||||
xe::threading::Wait(main_thread_, false);
|
||||
|
||||
if (restoring_) {
|
||||
restore_fence_.Wait();
|
||||
|
@ -688,12 +661,12 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path,
|
|||
on_shader_storage_initialization(true);
|
||||
graphics_system_->InitializeShaderStorage(storage_root_, title_id_, true);
|
||||
on_shader_storage_initialization(false);
|
||||
|
||||
auto main_thread = kernel_state_->LaunchModule(module);
|
||||
if (!main_thread) {
|
||||
auto main_xthread = kernel_state_->LaunchModule(module);
|
||||
if (!main_xthread) {
|
||||
return X_STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
main_thread_ = main_thread;
|
||||
|
||||
main_thread_ = main_xthread->thread();
|
||||
on_launch(title_id_, game_title_);
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#include "xenia/base/delegate.h"
|
||||
#include "xenia/base/exception_handler.h"
|
||||
#include "xenia/kernel/kernel_state.h"
|
||||
#include "xenia/kernel/util/xdbf_utils.h"
|
||||
#include "xenia/memory.h"
|
||||
#include "xenia/vfs/virtual_file_system.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
@ -103,13 +104,21 @@ class Emulator {
|
|||
// This is effectively the guest operating system.
|
||||
kernel::KernelState* kernel_state() const { return kernel_state_.get(); }
|
||||
|
||||
// Get the database with information about the running game.
|
||||
const kernel::util::XdbfGameData* game_data() const {
|
||||
if (title_data_.is_valid()) {
|
||||
return &title_data_;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Initializes the emulator and configures all components.
|
||||
// The given window is used for display and the provided functions are used
|
||||
// to create subsystems as required.
|
||||
// Once this function returns a game can be launched using one of the Launch
|
||||
// functions.
|
||||
X_STATUS Setup(
|
||||
ui::Window* display_window,
|
||||
X_STATUS Setup(ui::Window* display_window,
|
||||
std::function<std::unique_ptr<apu::AudioSystem>(cpu::Processor*)>
|
||||
audio_system_factory,
|
||||
std::function<std::unique_ptr<gpu::GraphicsSystem>()>
|
||||
|
@ -182,8 +191,9 @@ class Emulator {
|
|||
std::unique_ptr<vfs::VirtualFileSystem> file_system_;
|
||||
|
||||
std::unique_ptr<kernel::KernelState> kernel_state_;
|
||||
kernel::object_ref<kernel::XThread> main_thread_;
|
||||
uint32_t title_id_; // Currently running title ID
|
||||
threading::Thread* main_thread_ = nullptr;
|
||||
kernel::util::XdbfGameData title_data_; // Currently running title DB
|
||||
uint32_t title_id_ = 0; // Currently running title ID
|
||||
|
||||
bool paused_;
|
||||
bool restoring_;
|
||||
|
|
|
@ -473,6 +473,11 @@ struct xex2_opt_execution_info {
|
|||
};
|
||||
static_assert_size(xex2_opt_execution_info, 0x18);
|
||||
|
||||
struct xex2_multi_disc_media_id_t {
|
||||
char hash[12];
|
||||
uint32_t media_id;
|
||||
};
|
||||
|
||||
struct xex2_opt_import_libraries {
|
||||
xe::be<uint32_t> size; // 0x0
|
||||
struct {
|
||||
|
|
|
@ -287,7 +287,7 @@ void ImGuiDrawer::OnMouseDown(MouseEvent* e) {
|
|||
|
||||
if (button >= 0 && button < std::size(io.MouseDown)) {
|
||||
if (!ImGui::IsAnyMouseDown()) {
|
||||
window_->CaptureMouse();
|
||||
//window_->CaptureMouse();
|
||||
}
|
||||
io.MouseDown[button] = true;
|
||||
}
|
||||
|
@ -320,7 +320,7 @@ void ImGuiDrawer::OnMouseUp(MouseEvent* e) {
|
|||
if (button >= 0 && button < std::size(io.MouseDown)) {
|
||||
io.MouseDown[button] = false;
|
||||
if (!ImGui::IsAnyMouseDown()) {
|
||||
window_->ReleaseMouse();
|
||||
//window_->ReleaseMouse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,27 +7,16 @@
|
|||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/ui/menu_item.h"
|
||||
#include "xenia/ui/menu.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
std::unique_ptr<MenuItem> MenuItem::Create(Type type) {
|
||||
return MenuItem::Create(type, "", "", nullptr);
|
||||
}
|
||||
|
||||
std::unique_ptr<MenuItem> MenuItem::Create(Type type, const std::string& text) {
|
||||
return MenuItem::Create(type, text, "", nullptr);
|
||||
}
|
||||
|
||||
std::unique_ptr<MenuItem> MenuItem::Create(Type type, const std::string& text,
|
||||
std::function<void()> callback) {
|
||||
return MenuItem::Create(type, text, "", std::move(callback));
|
||||
}
|
||||
|
||||
MenuItem::MenuItem(Type type, const std::string& text,
|
||||
MenuItem::MenuItem(Menu* menu, Type type, const std::string& text,
|
||||
const std::string& hotkey, std::function<void()> callback)
|
||||
: type_(type),
|
||||
: menu_(menu),
|
||||
type_(type),
|
||||
parent_item_(nullptr),
|
||||
text_(text),
|
||||
hotkey_(hotkey),
|
||||
|
@ -35,21 +24,6 @@ MenuItem::MenuItem(Type type, const std::string& text,
|
|||
|
||||
MenuItem::~MenuItem() = default;
|
||||
|
||||
void MenuItem::AddChild(MenuItem* child_item) {
|
||||
AddChild(MenuItemPtr(child_item, [](MenuItem* item) {}));
|
||||
}
|
||||
|
||||
void MenuItem::AddChild(std::unique_ptr<MenuItem> child_item) {
|
||||
AddChild(
|
||||
MenuItemPtr(child_item.release(), [](MenuItem* item) { delete item; }));
|
||||
}
|
||||
|
||||
void MenuItem::AddChild(MenuItemPtr child_item) {
|
||||
auto child_item_ptr = child_item.get();
|
||||
children_.emplace_back(std::move(child_item));
|
||||
OnChildAdded(child_item_ptr);
|
||||
}
|
||||
|
||||
void MenuItem::RemoveChild(MenuItem* child_item) {
|
||||
for (auto it = children_.begin(); it != children_.end(); ++it) {
|
||||
if (it->get() == child_item) {
|
|
@ -7,8 +7,8 @@
|
|||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_UI_MENU_ITEM_H_
|
||||
#define XENIA_UI_MENU_ITEM_H_
|
||||
#ifndef XENIA_UI_MENU_H_
|
||||
#define XENIA_UI_MENU_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
@ -23,24 +23,17 @@ namespace ui {
|
|||
class Window;
|
||||
|
||||
class MenuItem {
|
||||
public:
|
||||
typedef std::unique_ptr<MenuItem, void (*)(MenuItem*)> MenuItemPtr;
|
||||
friend class Menu;
|
||||
|
||||
public:
|
||||
using Callback = std::function<void()>;
|
||||
enum class Type {
|
||||
kPopup, // Popup menu (submenu)
|
||||
kSubmenu, // Submenu (Popup menu in win32)
|
||||
kSeparator,
|
||||
kNormal, // Root menu
|
||||
kRoot, // Root menu
|
||||
kString, // Menu is just a string
|
||||
};
|
||||
|
||||
static std::unique_ptr<MenuItem> Create(Type type);
|
||||
static std::unique_ptr<MenuItem> Create(Type type, const std::string& text);
|
||||
static std::unique_ptr<MenuItem> Create(Type type, const std::string& text,
|
||||
std::function<void()> callback);
|
||||
static std::unique_ptr<MenuItem> Create(Type type, const std::string& text,
|
||||
const std::string& hotkey,
|
||||
std::function<void()> callback);
|
||||
|
||||
virtual ~MenuItem();
|
||||
|
||||
MenuItem* parent_item() const { return parent_item_; }
|
||||
|
@ -48,9 +41,9 @@ class MenuItem {
|
|||
const std::string& text() { return text_; }
|
||||
const std::string& hotkey() { return hotkey_; }
|
||||
|
||||
void AddChild(MenuItem* child_item);
|
||||
void AddChild(std::unique_ptr<MenuItem> child_item);
|
||||
void AddChild(MenuItemPtr child_item);
|
||||
virtual MenuItem* CreateChild(Type type, std::string text = "",
|
||||
std::string hotkey = "",
|
||||
Callback callback = nullptr) = 0;
|
||||
void RemoveChild(MenuItem* child_item);
|
||||
MenuItem* child(size_t index);
|
||||
|
||||
|
@ -58,22 +51,57 @@ class MenuItem {
|
|||
virtual void DisableMenuItem(Window& window) = 0;
|
||||
|
||||
protected:
|
||||
MenuItem(Type type, const std::string& text, const std::string& hotkey,
|
||||
std::function<void()> callback);
|
||||
MenuItem(Menu* menu, Type type, const std::string& text,
|
||||
const std::string& hotkey, Callback callback);
|
||||
|
||||
virtual void OnChildAdded(MenuItem* child_item) {}
|
||||
virtual void OnChildRemoved(MenuItem* child_item) {}
|
||||
|
||||
virtual void OnSelected(UIEvent* e);
|
||||
|
||||
Menu* menu_;
|
||||
Type type_;
|
||||
MenuItem* parent_item_;
|
||||
std::vector<MenuItemPtr> children_;
|
||||
std::vector<std::unique_ptr<MenuItem>> children_;
|
||||
std::string text_;
|
||||
std::string hotkey_;
|
||||
std::function<void()> callback_;
|
||||
};
|
||||
|
||||
class Menu {
|
||||
using MenuType = MenuItem::Type;
|
||||
|
||||
public:
|
||||
explicit Menu(Window* window) : window_(window), enabled_(true) {}
|
||||
|
||||
virtual ~Menu() = default;
|
||||
|
||||
virtual MenuItem* CreateMenuItem(const std::string& title) = 0;
|
||||
|
||||
Window* window() const { return window_; }
|
||||
const std::vector<std::unique_ptr<MenuItem>>& menu_items() const {
|
||||
return menu_items_;
|
||||
}
|
||||
|
||||
bool enabled() const { return enabled_; }
|
||||
|
||||
void set_enabled(bool enabled) {
|
||||
enabled_ = enabled;
|
||||
for (const auto& menu_item : menu_items_) {
|
||||
if (enabled) {
|
||||
menu_item->EnableMenuItem(*window());
|
||||
} else {
|
||||
menu_item->DisableMenuItem(*window());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
Window* window_;
|
||||
std::vector<std::unique_ptr<MenuItem>> menu_items_;
|
||||
bool enabled_;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "menu_win.h"
|
||||
|
||||
#include "window.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
MenuItem* Win32MenuItem::CreateChild(Type type, std::string text,
|
||||
std::string hotkey, Callback callback) {
|
||||
auto child = std::unique_ptr<Win32MenuItem>(new Win32MenuItem(
|
||||
static_cast<Win32Menu*>(menu_), type, text, hotkey, callback));
|
||||
|
||||
children_.push_back(std::move(child));
|
||||
auto child_ptr = children_.back().get();
|
||||
OnChildAdded(child_ptr);
|
||||
return child_ptr;
|
||||
}
|
||||
|
||||
Win32MenuItem::Win32MenuItem(Win32Menu* menu, Type type,
|
||||
const std::string& text,
|
||||
const std::string& hotkey,
|
||||
std::function<void()> callback)
|
||||
: MenuItem(menu, type, text, hotkey, std::move(callback)),
|
||||
handle_(nullptr) {
|
||||
switch (type) {
|
||||
case Type::kRoot:
|
||||
case Type::kSubmenu: {
|
||||
handle_ = CreatePopupMenu();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// May just be a placeholder.
|
||||
break;
|
||||
}
|
||||
if (handle_) {
|
||||
MENUINFO menu_info = {0};
|
||||
menu_info.cbSize = sizeof(menu_info);
|
||||
menu_info.fMask = MIM_MENUDATA | MIM_STYLE;
|
||||
menu_info.dwMenuData = ULONG_PTR(this);
|
||||
menu_info.dwStyle = MNS_NOTIFYBYPOS;
|
||||
SetMenuInfo(handle_, &menu_info);
|
||||
}
|
||||
}
|
||||
|
||||
void Win32MenuItem::EnableMenuItem(Window& window) {
|
||||
int i = 0;
|
||||
for (auto iter = children_.begin(); iter != children_.end(); ++iter, i++) {
|
||||
::EnableMenuItem(handle_, i, MF_BYPOSITION | MF_ENABLED);
|
||||
}
|
||||
DrawMenuBar((HWND)window.native_handle());
|
||||
}
|
||||
|
||||
void Win32MenuItem::DisableMenuItem(Window& window) {
|
||||
int i = 0;
|
||||
for (auto iter = children_.begin(); iter != children_.end(); ++iter, i++) {
|
||||
::EnableMenuItem(handle_, i, MF_BYPOSITION | MF_GRAYED);
|
||||
}
|
||||
DrawMenuBar((HWND)window.native_handle());
|
||||
}
|
||||
|
||||
void Win32MenuItem::OnChildAdded(MenuItem* generic_child_item) {
|
||||
auto child_item = static_cast<Win32MenuItem*>(generic_child_item);
|
||||
|
||||
switch (child_item->type()) {
|
||||
case MenuItem::Type::kRoot:
|
||||
// Nothing special.
|
||||
break;
|
||||
case MenuItem::Type::kSubmenu:
|
||||
AppendMenuW(handle_, MF_POPUP,
|
||||
reinterpret_cast<UINT_PTR>(child_item->handle()),
|
||||
reinterpret_cast<LPCWSTR>(xe::to_utf16(child_item->text()).c_str()));
|
||||
break;
|
||||
case MenuItem::Type::kSeparator:
|
||||
AppendMenuW(handle_, MF_SEPARATOR, UINT_PTR(child_item->handle()), 0);
|
||||
break;
|
||||
case MenuItem::Type::kString:
|
||||
auto full_name = child_item->text();
|
||||
if (!child_item->hotkey().empty()) {
|
||||
full_name += "\t" + child_item->hotkey();
|
||||
}
|
||||
AppendMenuW(handle_, MF_STRING, UINT_PTR(child_item->handle_),
|
||||
reinterpret_cast<LPCWSTR>(xe::to_utf16(full_name).c_str()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Win32MenuItem::OnChildRemoved(MenuItem* generic_child_item) {}
|
||||
|
||||
Win32Menu::Win32Menu(Window* window) : Menu(window) {
|
||||
menu_ = ::CreateMenu();
|
||||
|
||||
MENUINFO menu_info = {0};
|
||||
menu_info.cbSize = sizeof(menu_info);
|
||||
menu_info.fMask = MIM_MENUDATA | MIM_STYLE;
|
||||
menu_info.dwMenuData = ULONG_PTR(this);
|
||||
menu_info.dwStyle = MNS_NOTIFYBYPOS;
|
||||
SetMenuInfo(menu_, &menu_info);
|
||||
}
|
||||
|
||||
Win32Menu::~Win32Menu() { DestroyMenu(menu_); }
|
||||
|
||||
|
||||
MenuItem* Win32Menu::CreateMenuItem(const std::string& title) {
|
||||
auto menu_item = std::unique_ptr<Win32MenuItem>(
|
||||
new Win32MenuItem(this, MenuType::kRoot, title, "", nullptr));
|
||||
auto menu_item_ptr = menu_item.get();
|
||||
menu_items_.push_back(std::move(menu_item));
|
||||
AppendMenuW(menu_, MF_POPUP,
|
||||
reinterpret_cast<UINT_PTR>(menu_item_ptr->handle()),
|
||||
reinterpret_cast<LPCWSTR>(menu_item_ptr->text().c_str()));
|
||||
return menu_item_ptr;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_UI_MENU_WIN_H_
|
||||
#define XENIA_UI_MENU_WIN_H_
|
||||
|
||||
#include "menu.h"
|
||||
#include "xenia/base/platform_win.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
class Win32MenuItem : public MenuItem {
|
||||
friend class Win32Menu;
|
||||
public:
|
||||
|
||||
MenuItem* CreateChild(Type type, std::string text, std::string hotkey,
|
||||
Callback callback) override;
|
||||
HMENU handle() const { return handle_; }
|
||||
|
||||
void EnableMenuItem(Window& window) override;
|
||||
void DisableMenuItem(Window& window) override;
|
||||
|
||||
using MenuItem::OnSelected;
|
||||
|
||||
protected:
|
||||
void OnChildAdded(MenuItem* child_item) override;
|
||||
void OnChildRemoved(MenuItem* child_item) override;
|
||||
|
||||
Win32MenuItem(Win32Menu *menu, Type type, const std::string& text, const std::string& hotkey,
|
||||
std::function<void()> callback);
|
||||
|
||||
private:
|
||||
// menu item handle (if submenu)
|
||||
HMENU handle_;
|
||||
};
|
||||
|
||||
class Win32Menu : public Menu {
|
||||
public:
|
||||
explicit Win32Menu(Window* window);
|
||||
~Win32Menu();
|
||||
|
||||
HMENU handle() const { return menu_; }
|
||||
|
||||
MenuItem* CreateMenuItem(const std::string& title) override;
|
||||
private:
|
||||
HMENU menu_;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,65 @@
|
|||
#include "xenia/ui/qt/actions/action.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include "xenia/ui/qt/theme_manager.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
XAction::XAction() {}
|
||||
|
||||
XAction::XAction(const QChar& icon, const QString& text) {
|
||||
setGlyphIcon(QFont("Segoe MDL2 Assets", 64), icon);
|
||||
setIconText(text);
|
||||
}
|
||||
|
||||
void XAction::setGlyphIcon(const QFont& font, const QChar& glyph_char) {
|
||||
glyph_char_ = glyph_char;
|
||||
glyph_font_ = font;
|
||||
|
||||
rebuildGlyphIcons();
|
||||
}
|
||||
|
||||
void XAction::rebuildGlyphIcons() {
|
||||
auto renderPixmap = [=](QColor color) {
|
||||
// Measure the Glyph
|
||||
QFontMetrics measure(glyph_font_);
|
||||
QRect icon_rect = measure.boundingRect(glyph_char_);
|
||||
double max = qMax(icon_rect.width(), icon_rect.height());
|
||||
|
||||
// Create the Pixmap
|
||||
// boundingRect can be inaccurate so add a 4px padding to be safe
|
||||
QPixmap pixmap(max + 4, max + 4);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
// Paint the Glyph
|
||||
QPainter painter(&pixmap);
|
||||
painter.setFont(glyph_font_);
|
||||
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
|
||||
painter.setPen(QPen(color));
|
||||
|
||||
painter.drawText(pixmap.rect(), Qt::AlignVCenter, glyph_char_);
|
||||
|
||||
return pixmap;
|
||||
};
|
||||
|
||||
auto theme_manager = ThemeManager::Instance();
|
||||
const Theme& current_theme = theme_manager.current_theme();
|
||||
|
||||
QColor off_color = current_theme.ColorForKey("light2");
|
||||
QColor on_color = current_theme.ColorForKey("secondary");
|
||||
|
||||
QPixmap OFF = renderPixmap(off_color);
|
||||
QPixmap ON = renderPixmap(on_color);
|
||||
|
||||
QIcon icon;
|
||||
icon.addPixmap(OFF, QIcon::Normal, QIcon::Off);
|
||||
icon.addPixmap(ON, QIcon::Normal, QIcon::On);
|
||||
|
||||
setIcon(icon);
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef XENIA_UI_QT_ACTION_H_
|
||||
#define XENIA_UI_QT_ACTION_H_
|
||||
|
||||
#include "xenia/ui/qt/themeable_widget.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QPixmap>
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class XAction : public QAction {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XAction();
|
||||
explicit XAction(const QChar& icon, const QString& text);
|
||||
|
||||
void setGlyphIcon(const QFont& font, const QChar& icon);
|
||||
|
||||
private:
|
||||
void rebuildGlyphIcons();
|
||||
|
||||
// Glyph Icon
|
||||
QChar glyph_char_;
|
||||
QPalette glyph_palette_;
|
||||
QFont glyph_font_;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,24 @@
|
|||
#ifndef XENIA_UI_QT_ADD_GAME_ACTION_H_
|
||||
#define XENIA_UI_QT_ADD_GAME_ACTION_H_
|
||||
|
||||
#include "xenia/ui/qt/actions/action.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class XAddGameAction : public XAction {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XAddGameAction() : XAction() {
|
||||
setIconText("Add Game");
|
||||
setGlyphIcon(QFont("Segoe MDL2 Assets"), QChar(0xE710));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,91 @@
|
|||
#include "xenia/ui/qt/delegates/game_listview_delegate.h"
|
||||
#include "xenia/ui/qt/models/game_library_model.h"
|
||||
|
||||
#include <QBitmap>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
XGameListViewDelegate::XGameListViewDelegate(QWidget* parent)
|
||||
: QStyledItemDelegate(parent) {
|
||||
QImage mask_image(":resources/graphics/GameIconMask.png");
|
||||
icon_mask_ = QPixmap::fromImage(mask_image.createAlphaMask());
|
||||
}
|
||||
|
||||
void XGameListViewDelegate::paint(QPainter* painter,
|
||||
const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const {
|
||||
painter->save();
|
||||
auto options = QStyleOptionViewItem(option);
|
||||
options.state &= (~QStyle::State_HasFocus);
|
||||
|
||||
initStyleOption(&options, index);
|
||||
GameColumn column = (GameColumn)index.column();
|
||||
switch (column) {
|
||||
case GameColumn::kIconColumn: {
|
||||
if (index.data().canConvert<QImage>()) {
|
||||
QStyledItemDelegate::paint(painter, options, index);
|
||||
QImage image = qvariant_cast<QImage>(index.data());
|
||||
QPixmap pixmap = QPixmap::fromImage(image);
|
||||
pixmap.setDevicePixelRatio(painter->device()->devicePixelRatioF());
|
||||
paintIcon(pixmap, painter, options, index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
QStyledItemDelegate::paint(painter, options, index);
|
||||
}
|
||||
}
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
void XGameListViewDelegate::paintIcon(QPixmap& icon, QPainter* painter,
|
||||
const QStyleOptionViewItem& options,
|
||||
const QModelIndex& index) const {
|
||||
painter->save();
|
||||
|
||||
// Get the column bounds
|
||||
double col_x = options.rect.x();
|
||||
double col_y = options.rect.y();
|
||||
double col_width = options.rect.width();
|
||||
double col_height = options.rect.height();
|
||||
double icon_size = options.rect.height() * 0.8;
|
||||
|
||||
icon.setMask(icon_mask_);
|
||||
|
||||
// Calculate the Icon position
|
||||
QRectF rect = icon.rect();
|
||||
QRectF icon_rect = QRectF(rect.x(), rect.y(), icon_size, icon_size);
|
||||
double shift_x = (col_width - icon_size) / 2 + col_x;
|
||||
double shift_y = (col_height - icon_size) / 2 + col_y;
|
||||
icon_rect.translate(shift_x, shift_y);
|
||||
|
||||
// adding QPainter::Antialiasing here smoothes masked edges
|
||||
// but makes the image look slightly blurry
|
||||
painter->setRenderHints(QPainter::SmoothPixmapTransform);
|
||||
painter->drawPixmap(icon_rect, icon, icon.rect());
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize XGameListViewDelegate::sizeHint(const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const {
|
||||
GameColumn column = (GameColumn)index.column();
|
||||
|
||||
switch (column) {
|
||||
case GameColumn::kIconColumn:
|
||||
return QSize(58, 48);
|
||||
case GameColumn::kPathColumn: {
|
||||
return QSize(500, 48);
|
||||
}
|
||||
default:
|
||||
return QSize(96, 48);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef XENIA_UI_QT_GAME_LISTVIEW_DELEGATE_H_
|
||||
#define XENIA_UI_QT_GAME_LISTVIEW_DELEGATE_H_
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class XGameListViewDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XGameListViewDelegate(QWidget* parent = nullptr);
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem& options,
|
||||
const QModelIndex& index) const override;
|
||||
|
||||
private:
|
||||
void paintIcon(QPixmap& icon, QPainter* painter,
|
||||
const QStyleOptionViewItem& options,
|
||||
const QModelIndex& index) const;
|
||||
|
||||
QPixmap icon_mask_;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "loop_qt.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
#include "xenia/base/assert.h"
|
||||
#include "xenia/base/threading.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
std::unique_ptr<Loop> Loop::Create() { return std::make_unique<qt::QtLoop>(); }
|
||||
|
||||
namespace qt {
|
||||
|
||||
bool QtLoop::is_on_loop_thread() {
|
||||
return QThread::currentThread() == QApplication::instance()->thread();
|
||||
}
|
||||
|
||||
QtLoop::QtLoop() : has_quit_(false) {
|
||||
QObject::connect(qApp, &QCoreApplication::aboutToQuit,
|
||||
[this]() { has_quit_ = true; });
|
||||
}
|
||||
|
||||
void QtLoop::Post(std::function<void()> fn) { PostDelayed(fn, 0); }
|
||||
|
||||
void QtLoop::PostDelayed(std::function<void()> fn, uint64_t delay_millis) {
|
||||
// https://riptutorial.com/qt/example/21783/using-qtimer-to-run-code-on-main-thread
|
||||
const auto& main_thread = QApplication::instance()->thread();
|
||||
QTimer* timer = new QTimer();
|
||||
timer->moveToThread(main_thread);
|
||||
timer->setSingleShot(true);
|
||||
|
||||
QObject::connect(timer, &QTimer::timeout, [=]() {
|
||||
// we are now in the gui thread
|
||||
fn();
|
||||
timer->deleteLater();
|
||||
});
|
||||
QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection,
|
||||
Q_ARG(uint64_t, delay_millis));
|
||||
}
|
||||
|
||||
void QtLoop::Quit() {
|
||||
QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void QtLoop::AwaitQuit() {
|
||||
if (has_quit_) return;
|
||||
|
||||
xe::threading::Fence fence;
|
||||
QObject::connect(qApp, &QCoreApplication::aboutToQuit,
|
||||
[&fence]() { fence.Signal(); });
|
||||
fence.Wait();
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/ui/loop.h"
|
||||
|
||||
#ifndef XENIA_UI_QT_LOOP_QT_H_
|
||||
#define XENIA_UI_QT_LOOP_QT_H_
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class QtLoop final : public Loop {
|
||||
public:
|
||||
QtLoop();
|
||||
bool is_on_loop_thread() override;
|
||||
|
||||
void Post(std::function<void()> fn) override;
|
||||
void PostDelayed(std::function<void()> fn, uint64_t delay_millis) override;
|
||||
|
||||
void Quit() override;
|
||||
void AwaitQuit() override;
|
||||
|
||||
private:
|
||||
bool has_quit_;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,50 @@
|
|||
#include "xenia/ui/qt/main_window.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "build/version.h"
|
||||
#include "xenia/ui/qt/widgets/status_bar.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
bool MainWindow::Initialize() {
|
||||
// Custom Frame Border
|
||||
// Disable for now until windows aero additions are added
|
||||
// setWindowFlags(Qt::Window | Qt::FramelessWindowHint);
|
||||
|
||||
this->setFocusPolicy(Qt::StrongFocus);
|
||||
|
||||
shell_ = new XShell(this);
|
||||
this->setCentralWidget(shell_);
|
||||
|
||||
status_bar_ = new XStatusBar(this);
|
||||
this->setStatusBar(status_bar_);
|
||||
|
||||
QLabel* build_label = new QLabel;
|
||||
build_label->setObjectName("buildLabel");
|
||||
build_label->setText(QStringLiteral("Xenia: %1 / %2 / %3")
|
||||
.arg(XE_BUILD_BRANCH)
|
||||
.arg(XE_BUILD_COMMIT_SHORT)
|
||||
.arg(XE_BUILD_DATE));
|
||||
status_bar_->addPermanentWidget(build_label);
|
||||
|
||||
return QtWindow::Initialize();
|
||||
}
|
||||
|
||||
void MainWindow::AddStatusBarWidget(QWidget* widget, bool permanent) {
|
||||
if (permanent) {
|
||||
status_bar_->addPermanentWidget(widget);
|
||||
} else {
|
||||
status_bar_->addWidget(widget);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::RemoveStatusBarWidget(QWidget* widget) {
|
||||
return status_bar_->removeWidget(widget);
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,37 @@
|
|||
#ifndef XENIA_UI_QT_MAINWINDOW_H_
|
||||
#define XENIA_UI_QT_MAINWINDOW_H_
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
#include "window_qt.h"
|
||||
#include "xenia/ui/qt/themeable_widget.h"
|
||||
#include "xenia/ui/qt/widgets/shell.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
class XStatusBar;
|
||||
|
||||
class MainWindow final : public Themeable<QtWindow> {
|
||||
Q_OBJECT
|
||||
public:
|
||||
MainWindow(Loop* loop, const std::string& title)
|
||||
: Themeable<QtWindow>("MainWindow", loop, title) {}
|
||||
|
||||
bool Initialize() override;
|
||||
|
||||
void AddStatusBarWidget(QWidget* widget, bool permanent = false);
|
||||
void RemoveStatusBarWidget(QWidget* widget);
|
||||
|
||||
const XStatusBar* status_bar() const { return status_bar_; }
|
||||
|
||||
private:
|
||||
XShell* shell_ = nullptr;
|
||||
XStatusBar* status_bar_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "menu_qt.h"
|
||||
|
||||
#include "window_qt.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
MenuItem* QtMenuItem::CreateChild(Type type, std::string text,
|
||||
std::string hotkey, Callback callback) {
|
||||
auto child = std::unique_ptr<QtMenuItem>(new QtMenuItem(
|
||||
static_cast<QtMenu*>(menu_), type, text, hotkey, callback));
|
||||
children_.push_back(std::move(child));
|
||||
auto child_ptr = children_.back().get();
|
||||
OnChildAdded(child_ptr);
|
||||
return child_ptr;
|
||||
}
|
||||
|
||||
void QtMenuItem::EnableMenuItem(Window& window) {
|
||||
qt_menu_->setEnabled(true);
|
||||
for (const auto& child : children_) {
|
||||
child->EnableMenuItem(window);
|
||||
}
|
||||
}
|
||||
|
||||
void QtMenuItem::DisableMenuItem(Window& window) {
|
||||
qt_menu_->setDisabled(true);
|
||||
for (const auto& child : children_) {
|
||||
child->DisableMenuItem(window);
|
||||
}
|
||||
}
|
||||
|
||||
QtMenuItem::QtMenuItem(QtMenu* menu, Type type, const std::string& text,
|
||||
const std::string& hotkey,
|
||||
std::function<void()> callback)
|
||||
: MenuItem(menu, type, text, hotkey, callback), qt_menu_(nullptr) {
|
||||
}
|
||||
|
||||
void QtMenuItem::OnChildAdded(MenuItem* generic_child_item) {
|
||||
assert(qt_menu_ != nullptr);
|
||||
|
||||
auto child_item = static_cast<QtMenuItem*>(generic_child_item);
|
||||
|
||||
switch (child_item->type()) {
|
||||
case Type::kSubmenu: {
|
||||
child_item->qt_menu_ =
|
||||
new QMenu(QString::fromStdString(child_item->text()));
|
||||
qt_menu_->addMenu(child_item->qt_menu_);
|
||||
} break;
|
||||
|
||||
case Type::kSeparator: {
|
||||
qt_menu_->addSeparator();
|
||||
} break;
|
||||
|
||||
case Type::kString: {
|
||||
auto title = QString::fromStdString(child_item->text());
|
||||
auto hotkey = child_item->hotkey();
|
||||
auto shortcut =
|
||||
hotkey.empty()
|
||||
? 0
|
||||
: QKeySequence(QString::fromStdString(hotkey));
|
||||
|
||||
const auto& callback = child_item->callback_;
|
||||
qt_menu_->addAction(
|
||||
title,
|
||||
[&callback]() {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
shortcut);
|
||||
} break;
|
||||
|
||||
case Type::kRoot:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void QtMenuItem::OnChildRemoved(MenuItem* child_item) {}
|
||||
|
||||
QtMenu::QtMenu(Window* window) : Menu(window) {
|
||||
auto qt_window = static_cast<QtWindow*>(window);
|
||||
menu_bar_ = qt_window->menuBar();
|
||||
}
|
||||
|
||||
MenuItem* QtMenu::CreateMenuItem(const std::string& title) {
|
||||
auto menu_item = std::unique_ptr<QtMenuItem>(
|
||||
new QtMenuItem(this, MenuType::kRoot, title, "", nullptr));
|
||||
|
||||
auto menu = new QMenu(QString::fromStdString(title));
|
||||
menu_item->qt_menu_ = menu;
|
||||
|
||||
menu_items_.push_back(std::move(menu_item));
|
||||
|
||||
menu_bar_->addMenu(menu);
|
||||
|
||||
return menu_items_.back().get();
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_UI_QT_MENU_QT_H_
|
||||
#define XENIA_UI_QT_MENU_QT_H_
|
||||
|
||||
#include <QMenu>
|
||||
#include <QMenuBar>
|
||||
#include "xenia/ui/menu.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class QtMenuItem : public MenuItem {
|
||||
friend class QtMenu;
|
||||
public:
|
||||
MenuItem* CreateChild(Type type, std::string text, std::string hotkey,
|
||||
Callback callback) override;
|
||||
|
||||
void EnableMenuItem(Window& window) override;
|
||||
void DisableMenuItem(Window& window) override;
|
||||
|
||||
protected:
|
||||
QtMenuItem(QtMenu* menu, Type type, const std::string& text,
|
||||
const std::string& hotkey, std::function<void()> callback);
|
||||
|
||||
void OnChildAdded(MenuItem* child_item) override;
|
||||
void OnChildRemoved(MenuItem* child_item) override;
|
||||
|
||||
private:
|
||||
QMenu* qt_menu_;
|
||||
};
|
||||
|
||||
class QtMenu : public Menu {
|
||||
public:
|
||||
explicit QtMenu(Window* window);
|
||||
MenuItem* CreateMenuItem(const std::string& title) override;
|
||||
|
||||
QMenuBar* menu_bar() const { return menu_bar_; }
|
||||
private:
|
||||
QMenuBar* menu_bar_;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,171 @@
|
|||
#include "xenia/ui/qt/models/game_library_model.h"
|
||||
#include "xenia/base/string_util.h"
|
||||
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
XGameLibraryModel::XGameLibraryModel(QObject* parent) {
|
||||
library_ = XGameLibrary::Instance();
|
||||
}
|
||||
|
||||
QVariant XGameLibraryModel::data(const QModelIndex& index, int role) const {
|
||||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const XGameEntry& entry = library_->games()[index.row()];
|
||||
|
||||
GameColumn column = (GameColumn)index.column();
|
||||
switch (column) {
|
||||
case GameColumn::kIconColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
QImage image;
|
||||
image.loadFromData(entry.icon(), (int)entry.icon_size());
|
||||
return image;
|
||||
}
|
||||
break;
|
||||
case GameColumn::kTitleColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
return QString::fromUtf8(entry.title().c_str());
|
||||
}
|
||||
break;
|
||||
case GameColumn::kTitleIdColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
auto title_id = xe::string_util::to_hex_string(entry.title_id());
|
||||
return QString::fromUtf8(title_id.c_str());
|
||||
}
|
||||
break;
|
||||
case GameColumn::kMediaIdColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
auto media_id = xe::string_util::to_hex_string(entry.media_id());
|
||||
return QString::fromUtf8(media_id.c_str());
|
||||
}
|
||||
break;
|
||||
case GameColumn::kPathColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
return QString::fromWCharArray(entry.file_path().c_str());
|
||||
}
|
||||
break;
|
||||
case GameColumn::kVersionColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
auto version = &entry.version();
|
||||
QString version_str;
|
||||
version_str.asprintf("v%u.%u.%u.%u", version->major, version->minor,
|
||||
version->build, version->qfe);
|
||||
return version_str;
|
||||
}
|
||||
case GameColumn::kGenreColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
return QString::fromUtf8(entry.genre().c_str());
|
||||
}
|
||||
break;
|
||||
case GameColumn::kReleaseDateColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
return QString::fromUtf8(entry.release_date().c_str());
|
||||
}
|
||||
break;
|
||||
case GameColumn::kBuildDateColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
return QString::fromUtf8(entry.build_date().c_str());
|
||||
}
|
||||
break;
|
||||
case GameColumn::kLastPlayedColumn:
|
||||
return QVariant(); // TODO
|
||||
case GameColumn::kTimePlayedColumn:
|
||||
return QVariant(); // TODO
|
||||
case GameColumn::kAchievementsUnlockedColumn:
|
||||
return QVariant(); // TODO
|
||||
case GameColumn::kGamerscoreUnlockedColumn:
|
||||
return QVariant(); // TODO
|
||||
case GameColumn::kGameRatingColumn:
|
||||
return QVariant(); // TODO
|
||||
case GameColumn::kGameRegionColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
auto region = RegionStringMap.find(entry.regions());
|
||||
if (region != RegionStringMap.end()) {
|
||||
return region->second;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GameColumn::kCompatabilityColumn:
|
||||
return QVariant(); // TODO
|
||||
case GameColumn::kPlayerCountColumn:
|
||||
if (role == Qt::DisplayRole) {
|
||||
return QString::number(entry.player_count());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant XGameLibraryModel::headerData(int section, Qt::Orientation orientation,
|
||||
int role) const {
|
||||
if (orientation == Qt::Vertical || role != Qt::DisplayRole) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
GameColumn column = (GameColumn)section;
|
||||
switch (column) {
|
||||
case GameColumn::kIconColumn:
|
||||
return tr("");
|
||||
case GameColumn::kTitleColumn:
|
||||
return tr("Title");
|
||||
case GameColumn::kTitleIdColumn:
|
||||
return tr("Title ID");
|
||||
case GameColumn::kMediaIdColumn:
|
||||
return tr("Media ID");
|
||||
case GameColumn::kPathColumn:
|
||||
return tr("Path");
|
||||
case GameColumn::kVersionColumn:
|
||||
return tr("Version");
|
||||
case GameColumn::kGenreColumn:
|
||||
return tr("Genre");
|
||||
case GameColumn::kReleaseDateColumn:
|
||||
return tr("Release Date");
|
||||
case GameColumn::kBuildDateColumn:
|
||||
return tr("Build Date");
|
||||
case GameColumn::kLastPlayedColumn:
|
||||
return tr("Last Played");
|
||||
case GameColumn::kTimePlayedColumn:
|
||||
return tr("Time Played");
|
||||
case GameColumn::kAchievementsUnlockedColumn:
|
||||
return tr("Achievements");
|
||||
case GameColumn::kGamerscoreUnlockedColumn:
|
||||
return tr("Gamerscore");
|
||||
case GameColumn::kGameRatingColumn:
|
||||
return tr("Rating");
|
||||
case GameColumn::kGameRegionColumn:
|
||||
return tr("Region");
|
||||
case GameColumn::kCompatabilityColumn:
|
||||
return tr("Compatibility");
|
||||
case GameColumn::kPlayerCountColumn:
|
||||
return tr("# Players");
|
||||
default:
|
||||
return QVariant(); // Should not be seeing this
|
||||
}
|
||||
}
|
||||
|
||||
int XGameLibraryModel::rowCount(const QModelIndex& parent) const {
|
||||
if (parent.isValid()) { // TODO
|
||||
return 0;
|
||||
}
|
||||
return (int)library_->size();
|
||||
}
|
||||
|
||||
int XGameLibraryModel::columnCount(const QModelIndex& parent) const {
|
||||
return (int)GameColumn::kColumnCount;
|
||||
}
|
||||
|
||||
void XGameLibraryModel::refresh() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,72 @@
|
|||
#ifndef XENIA_UI_QT_GAME_LIBRARY_MODEL_H_
|
||||
#define XENIA_UI_QT_GAME_LIBRARY_MODEL_H_
|
||||
|
||||
#include "xenia/app/library/game_library.h"
|
||||
|
||||
#include <QAbstractTableModel>
|
||||
#include <memory>
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
using app::XGameEntry;
|
||||
using app::XGameLibrary;
|
||||
|
||||
enum class GameColumn {
|
||||
kIconColumn,
|
||||
kTitleColumn,
|
||||
kTitleIdColumn,
|
||||
kMediaIdColumn,
|
||||
kPathColumn,
|
||||
kVersionColumn,
|
||||
kGenreColumn,
|
||||
kReleaseDateColumn,
|
||||
kBuildDateColumn,
|
||||
kLastPlayedColumn,
|
||||
kTimePlayedColumn,
|
||||
kAchievementsUnlockedColumn,
|
||||
kGamerscoreUnlockedColumn,
|
||||
kGameRatingColumn,
|
||||
kGameRegionColumn,
|
||||
kCompatabilityColumn,
|
||||
kPlayerCountColumn,
|
||||
|
||||
kColumnCount, // For column counting, unused, keep as last entry
|
||||
};
|
||||
|
||||
static std::map<app::XGameRegions, QString> RegionStringMap{
|
||||
{XEX_REGION_ALL, "Region Free"},
|
||||
{XEX_REGION_NTSCJ, "NTSC-J"},
|
||||
{XEX_REGION_NTSCJ_CHINA, "NTSC-J (China)"},
|
||||
{XEX_REGION_NTSCJ_JAPAN, "JTSC-J (Japan)"},
|
||||
{XEX_REGION_NTSCU, "NTSC-U"},
|
||||
{XEX_REGION_OTHER, "Other"},
|
||||
{XEX_REGION_PAL, "PAL"},
|
||||
{XEX_REGION_PAL_AU_NZ, "PAL (AU/NZ)"},
|
||||
};
|
||||
|
||||
class XGameLibraryModel final : public QAbstractTableModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit XGameLibraryModel(QObject* parent = nullptr);
|
||||
|
||||
// QAbstractTableModel Implementation
|
||||
QVariant data(const QModelIndex& index,
|
||||
int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation,
|
||||
int role = Qt::DisplayRole) const override;
|
||||
int rowCount(const QModelIndex& parent) const override;
|
||||
int columnCount(const QModelIndex& parent) const override;
|
||||
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
XGameLibrary* library_;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,26 @@
|
|||
project_root = "../../../.."
|
||||
include(project_root.."/tools/build")
|
||||
local qt = premake.extensions.qt
|
||||
|
||||
group("src")
|
||||
project("xenia-ui-qt")
|
||||
uuid("3AB69653-3ACB-4C0B-976C-3B7F044E3E3A")
|
||||
kind("StaticLib")
|
||||
language("C++")
|
||||
|
||||
-- Setup Qt libraries
|
||||
qt.enable()
|
||||
qtmodules{"core", "gui", "widgets"}
|
||||
qtprefix "Qt5"
|
||||
|
||||
configuration {"Debug"}
|
||||
qtsuffix "d"
|
||||
configuration {}
|
||||
|
||||
links({
|
||||
"xenia-base",
|
||||
"xenia-core",
|
||||
})
|
||||
defines({
|
||||
})
|
||||
recursive_platform_files()
|
|
@ -0,0 +1,600 @@
|
|||
#include "xenia/ui/qt/tabs/debug_tab.h"
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QGraphicsEffect>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMenu>
|
||||
|
||||
#include "xenia/ui/qt/widgets/checkbox.h"
|
||||
#include "xenia/ui/qt/widgets/combobox.h"
|
||||
#include "xenia/ui/qt/widgets/dropdown_button.h"
|
||||
#include "xenia/ui/qt/widgets/groupbox.h"
|
||||
#include "xenia/ui/qt/widgets/line_edit.h"
|
||||
#include "xenia/ui/qt/widgets/push_button.h"
|
||||
#include "xenia/ui/qt/widgets/radio_button.h"
|
||||
#include "xenia/ui/qt/widgets/scroll_area.h"
|
||||
#include "xenia/ui/qt/widgets/separator.h"
|
||||
#include "xenia/ui/qt/widgets/slider.h"
|
||||
#include "xenia/ui/qt/widgets/tab_selector.h"
|
||||
#include "xenia/ui/qt/widgets/text_edit.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
// sidebar_->addAction(0xE90F, "Components");
|
||||
// sidebar_->addAction(0xE700, "Navigation");
|
||||
// sidebar_->addAction(0xE790, "Theme");
|
||||
// sidebar_->addAction(0xE8F1, "Library");
|
||||
|
||||
DebugTab::DebugTab() : XTab("Debug", "DebugTab") {
|
||||
sidebar_items_ =
|
||||
QList<SidebarItem>{{0xE90F, "Components", CreateComponentsTab()},
|
||||
{0xE700, "Navigation", CreateNavigationTab()},
|
||||
{0xE790, "Theme", CreateThemeTab()},
|
||||
{0xE8F1, "Library", CreateLibraryTab()}};
|
||||
|
||||
Build();
|
||||
}
|
||||
|
||||
void DebugTab::Build() {
|
||||
layout_ = new QHBoxLayout();
|
||||
layout_->setContentsMargins(0, 0, 0, 0);
|
||||
layout_->setSpacing(0);
|
||||
setLayout(layout_);
|
||||
|
||||
BuildSidebar();
|
||||
|
||||
content_widget_ = new QStackedWidget();
|
||||
|
||||
for (const SidebarItem& item : sidebar_items_) {
|
||||
content_widget_->addWidget(item.widget);
|
||||
}
|
||||
|
||||
layout_->addWidget(content_widget_);
|
||||
}
|
||||
|
||||
void DebugTab::BuildSidebar() {
|
||||
sidebar_container_ = new QWidget(this);
|
||||
sidebar_container_->setObjectName("sidebarContainer");
|
||||
|
||||
QVBoxLayout* sidebar_layout = new QVBoxLayout;
|
||||
sidebar_layout->setMargin(0);
|
||||
sidebar_layout->setSpacing(0);
|
||||
|
||||
sidebar_container_->setLayout(sidebar_layout);
|
||||
|
||||
// Add drop shadow to sidebar widget
|
||||
QGraphicsDropShadowEffect* effect = new QGraphicsDropShadowEffect;
|
||||
effect->setBlurRadius(16);
|
||||
effect->setXOffset(4);
|
||||
effect->setYOffset(0);
|
||||
effect->setColor(QColor(0, 0, 0, 64));
|
||||
|
||||
sidebar_container_->setGraphicsEffect(effect);
|
||||
|
||||
// Create sidebar
|
||||
sidebar_ = new XSideBar;
|
||||
sidebar_->setOrientation(Qt::Vertical);
|
||||
sidebar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
sidebar_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
|
||||
|
||||
// Create sidebar title
|
||||
QWidget* sidebar_title = new QWidget;
|
||||
sidebar_title->setObjectName("sidebarTitle");
|
||||
|
||||
QVBoxLayout* title_layout = new QVBoxLayout;
|
||||
title_layout->setMargin(0);
|
||||
title_layout->setContentsMargins(0, 40, 0, 0);
|
||||
title_layout->setSpacing(0);
|
||||
|
||||
sidebar_title->setLayout(title_layout);
|
||||
|
||||
// Title labels
|
||||
QLabel* xenia_title = new QLabel("Debug");
|
||||
xenia_title->setObjectName("sidebarTitleLabel");
|
||||
xenia_title->setAlignment(Qt::AlignCenter);
|
||||
title_layout->addWidget(xenia_title, 0, Qt::AlignCenter);
|
||||
|
||||
// Title separator
|
||||
auto separator = new XSeparator;
|
||||
title_layout->addSpacing(32);
|
||||
title_layout->addWidget(separator, 0, Qt::AlignCenter);
|
||||
|
||||
// Setup Sidebar toolbar
|
||||
sidebar_->addWidget(sidebar_title);
|
||||
|
||||
sidebar_->addSpacing(20);
|
||||
|
||||
QButtonGroup* bg = new QButtonGroup();
|
||||
|
||||
// loop over sidebar button items and connect them to slots
|
||||
int counter = 0;
|
||||
for (auto it = sidebar_items_.begin(); it != sidebar_items_.end();
|
||||
++it, ++counter) {
|
||||
SidebarItem& item = *it;
|
||||
auto btn = sidebar_->addAction(item.glyph, item.name);
|
||||
btn->setCheckable(true);
|
||||
bg->addButton(btn);
|
||||
|
||||
// set the first item to checked
|
||||
if (counter == 0) {
|
||||
btn->setChecked(true);
|
||||
}
|
||||
|
||||
// link up the clicked signal
|
||||
connect(btn, &XSideBarButton::clicked,
|
||||
[&]() { content_widget_->setCurrentWidget(item.widget); });
|
||||
}
|
||||
|
||||
sidebar_layout->addWidget(sidebar_, 0, Qt::AlignHCenter | Qt::AlignTop);
|
||||
sidebar_layout->addStretch(1);
|
||||
|
||||
// Add sidebar to tab widget
|
||||
layout_->addWidget(sidebar_container_, 0, Qt::AlignLeft);
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateComponentsTab() {
|
||||
QWidget* w = new QWidget();
|
||||
w->setSizePolicy(QSizePolicy::MinimumExpanding,
|
||||
QSizePolicy::MinimumExpanding);
|
||||
|
||||
XScrollArea* scroll_area = new XScrollArea(this);
|
||||
scroll_area->setWidget(w);
|
||||
|
||||
QVBoxLayout* layout = new QVBoxLayout();
|
||||
w->setLayout(layout);
|
||||
|
||||
layout->setSpacing(16);
|
||||
layout->setContentsMargins(0, 16, 0, 0);
|
||||
|
||||
layout->addWidget(CreateButtonGroup());
|
||||
layout->addWidget(CreateSliderGroup());
|
||||
layout->addWidget(CreateCheckboxGroup());
|
||||
layout->addWidget(CreateRadioButtonGroup());
|
||||
layout->addWidget(CreateInputGroup());
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
return scroll_area;
|
||||
}
|
||||
QWidget* DebugTab::CreateNavigationTab() {
|
||||
QWidget* w = new QWidget();
|
||||
QVBoxLayout* layout = new QVBoxLayout(w);
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(64, 64, 64, 64);
|
||||
|
||||
QWidget* container = new QWidget();
|
||||
container->setObjectName("navigationContainer");
|
||||
container->setFixedSize(640, 480);
|
||||
QVBoxLayout* container_layout = new QVBoxLayout(container);
|
||||
container_layout->setSpacing(4);
|
||||
container_layout->setContentsMargins(0, 0, 0, 0);
|
||||
container->setLayout(container_layout);
|
||||
|
||||
XTab *tab1 = new XTab("Tab1"), *tab2 = new XTab("Tab2"),
|
||||
*tab3 = new XTab("Tab3");
|
||||
|
||||
QVBoxLayout *tab1_layout = new QVBoxLayout(tab1),
|
||||
*tab2_layout = new QVBoxLayout(tab2),
|
||||
*tab3_layout = new QVBoxLayout(tab3);
|
||||
|
||||
std::vector<XTab*> tabs{tab1, tab2, tab3};
|
||||
|
||||
XTabSelector* tab_selector = new XTabSelector(tabs);
|
||||
container_layout->addWidget(tab_selector, 0, Qt::AlignCenter);
|
||||
|
||||
QStackedLayout* content_layout = new QStackedLayout();
|
||||
content_layout->setSpacing(0);
|
||||
content_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
container_layout->addLayout(content_layout);
|
||||
|
||||
tab1_layout->addWidget(CreateTab1Widget(tab_selector, content_layout));
|
||||
|
||||
QWidget* test2 = new QWidget();
|
||||
test2->setStyleSheet("background:blue");
|
||||
|
||||
tab2_layout->addWidget(test2);
|
||||
|
||||
QWidget* test3 = new QWidget();
|
||||
test3->setStyleSheet("background:green");
|
||||
|
||||
tab3_layout->addWidget(test3);
|
||||
|
||||
for (XTab* tab : tabs) {
|
||||
QVBoxLayout* tab_layout = qobject_cast<QVBoxLayout*>(tab->layout());
|
||||
tab_layout->setContentsMargins(0, 0, 0, 0);
|
||||
tab_layout->setSpacing(0);
|
||||
content_layout->addWidget(tab);
|
||||
}
|
||||
content_layout->setCurrentWidget(tab1);
|
||||
|
||||
connect(tab_selector, &XTabSelector::TabChanged, [content_layout](XTab* tab) {
|
||||
content_layout->setCurrentWidget(tab);
|
||||
});
|
||||
|
||||
layout->addWidget(container, 0, Qt::AlignHCenter | Qt::AlignTop);
|
||||
return w;
|
||||
}
|
||||
QWidget* DebugTab::CreateThemeTab() {
|
||||
QWidget* w = new QWidget();
|
||||
w->setStyleSheet("background: #505050");
|
||||
QVBoxLayout* layout = new QVBoxLayout(w);
|
||||
layout->setContentsMargins(64, 64, 64, 64);
|
||||
ThemeManager& theme_manager = ThemeManager::Instance();
|
||||
Theme& theme = theme_manager.current_theme();
|
||||
|
||||
QLabel* label = new QLabel(
|
||||
QStringLiteral("Current Theme: %1\n Description: %2\n Path: %3")
|
||||
.arg(theme.config().name(), theme.config().description(),
|
||||
theme.directory()));
|
||||
|
||||
layout->addWidget(label);
|
||||
|
||||
return w;
|
||||
}
|
||||
QWidget* DebugTab::CreateLibraryTab() {
|
||||
QWidget* w = new QWidget();
|
||||
w->setStyleSheet("background: yellow;");
|
||||
return w;
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateButtonGroup() {
|
||||
QWidget* group = new QWidget();
|
||||
|
||||
QVBoxLayout* group_layout = new QVBoxLayout();
|
||||
group_layout->setContentsMargins(32, 0, 32, 0);
|
||||
group_layout->setSpacing(16);
|
||||
group->setLayout(group_layout);
|
||||
|
||||
XGroupBox* groupbox = new XGroupBox("Buttons");
|
||||
|
||||
QVBoxLayout* groupbox_layout = new QVBoxLayout();
|
||||
groupbox_layout->setContentsMargins(16, 16, 16, 16);
|
||||
groupbox->setLayout(groupbox_layout);
|
||||
|
||||
group_layout->addWidget(groupbox);
|
||||
|
||||
QLabel* pushbtn_label = new QLabel("Push Buttons");
|
||||
groupbox_layout->addWidget(pushbtn_label);
|
||||
|
||||
QHBoxLayout* pushbtn_layout = new QHBoxLayout();
|
||||
pushbtn_layout->setSpacing(32);
|
||||
pushbtn_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QSize btn_size(120, 40);
|
||||
|
||||
XPushButton* pushbtn1 = new XPushButton("Push Button");
|
||||
pushbtn1->setMinimumSize(btn_size);
|
||||
XPushButton* pushbtn2 = new XPushButton("Disabled");
|
||||
pushbtn2->setDisabled(true);
|
||||
pushbtn2->setMinimumSize(btn_size);
|
||||
|
||||
pushbtn_layout->addWidget(pushbtn1);
|
||||
pushbtn_layout->addWidget(pushbtn2);
|
||||
|
||||
pushbtn_layout->addStretch();
|
||||
|
||||
groupbox_layout->addLayout(pushbtn_layout);
|
||||
|
||||
QLabel* dropdown_btn_label = new QLabel("Dropdown Buttons");
|
||||
groupbox_layout->addWidget(dropdown_btn_label);
|
||||
|
||||
QHBoxLayout* dropdown_btn_layout = new QHBoxLayout();
|
||||
dropdown_btn_layout->setSpacing(32);
|
||||
dropdown_btn_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->addAction("Test");
|
||||
menu->addSeparator();
|
||||
menu->addAction("Close");
|
||||
|
||||
XDropdownButton* dropdown_btn = new XDropdownButton("Dropdown Button", menu);
|
||||
dropdown_btn->setMinimumSize(btn_size);
|
||||
|
||||
XDropdownButton* dropdown_btn_disabled = new XDropdownButton("Disabled");
|
||||
dropdown_btn_disabled->setDisabled(true);
|
||||
dropdown_btn_disabled->setMinimumSize(btn_size);
|
||||
|
||||
dropdown_btn_layout->addWidget(dropdown_btn);
|
||||
dropdown_btn_layout->addWidget(dropdown_btn_disabled);
|
||||
|
||||
dropdown_btn_layout->addStretch();
|
||||
|
||||
groupbox_layout->addLayout(dropdown_btn_layout);
|
||||
|
||||
QLabel* combobox_label = new QLabel("Combo Boxes");
|
||||
groupbox_layout->addWidget(combobox_label);
|
||||
|
||||
QHBoxLayout* combobox_layout = new QHBoxLayout();
|
||||
combobox_layout->setSpacing(32);
|
||||
combobox_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QSize combobox_size = QSize(120, 32);
|
||||
|
||||
XComboBox* combobox1 = new XComboBox();
|
||||
combobox1->setMinimumSize(combobox_size);
|
||||
|
||||
combobox1->addItem("Simple String 1");
|
||||
combobox1->addItem("Simple String 2");
|
||||
|
||||
combobox_layout->addWidget(combobox1);
|
||||
|
||||
XComboBox* combobox2 = new XComboBox();
|
||||
combobox2->setMinimumSize(combobox_size);
|
||||
|
||||
QAction* action1 = new QAction("Action 1");
|
||||
QAction* action2 = new QAction("Action 2");
|
||||
|
||||
combobox2->addItem(action1->text(), QVariant::fromValue(action1));
|
||||
combobox2->addItem(action2->text(), QVariant::fromValue(action2));
|
||||
|
||||
combobox2->addAction(action2);
|
||||
combobox_layout->addWidget(combobox2);
|
||||
|
||||
combobox_layout->addStretch();
|
||||
|
||||
groupbox_layout->addLayout(combobox_layout);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateSliderGroup() {
|
||||
QWidget* group = new QWidget();
|
||||
QVBoxLayout* group_layout = new QVBoxLayout();
|
||||
group_layout->setContentsMargins(32, 0, 32, 0);
|
||||
group_layout->setSpacing(16);
|
||||
group->setLayout(group_layout);
|
||||
|
||||
XGroupBox* groupbox = new XGroupBox("Sliders");
|
||||
|
||||
QHBoxLayout* groupbox_layout = new QHBoxLayout();
|
||||
groupbox_layout->setContentsMargins(16, 16, 16, 16);
|
||||
groupbox->setLayout(groupbox_layout);
|
||||
|
||||
group_layout->addWidget(groupbox);
|
||||
|
||||
// horizontal slider
|
||||
|
||||
XSlider* horizontal_slider = new XSlider();
|
||||
horizontal_slider->setFixedWidth(120);
|
||||
|
||||
QLabel* horizontal_label = new QLabel();
|
||||
horizontal_label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored);
|
||||
connect(horizontal_slider, &XSlider::valueChanged, [=](int value) {
|
||||
QString text;
|
||||
horizontal_label->setText(text.asprintf("Value: %02d", value));
|
||||
});
|
||||
horizontal_slider->valueChanged(0);
|
||||
|
||||
groupbox_layout->addWidget(horizontal_slider);
|
||||
groupbox_layout->addWidget(horizontal_label);
|
||||
|
||||
groupbox_layout->addSpacing(16);
|
||||
|
||||
// vertical slider
|
||||
|
||||
XSlider* vertical_slider = new XSlider(Qt::Vertical);
|
||||
vertical_slider->setFixedSize(20, 60);
|
||||
// vertical slider causes issues in a vertical orientation right now
|
||||
// TODO: fix this. for now just ignore its vertical size
|
||||
vertical_slider->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored);
|
||||
|
||||
QLabel* vertical_label = new QLabel();
|
||||
connect(vertical_slider, &XSlider::valueChanged, [=](int value) {
|
||||
QString text;
|
||||
vertical_label->setText(text.asprintf("Value: %02d", value));
|
||||
});
|
||||
vertical_slider->valueChanged(0);
|
||||
|
||||
groupbox_layout->addWidget(vertical_slider);
|
||||
groupbox_layout->addWidget(vertical_label);
|
||||
|
||||
groupbox_layout->addStretch();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateCheckboxGroup() {
|
||||
QWidget* group = new QWidget();
|
||||
|
||||
QVBoxLayout* group_layout = new QVBoxLayout();
|
||||
group_layout->setContentsMargins(32, 0, 32, 0);
|
||||
group_layout->setSpacing(16);
|
||||
group->setLayout(group_layout);
|
||||
|
||||
XGroupBox* groupbox = new XGroupBox("Checkboxes");
|
||||
|
||||
QVBoxLayout* groupbox_layout = new QVBoxLayout();
|
||||
groupbox_layout->setContentsMargins(16, 16, 16, 16);
|
||||
groupbox->setLayout(groupbox_layout);
|
||||
|
||||
group_layout->addWidget(groupbox);
|
||||
|
||||
QHBoxLayout* layer_1_layout = new QHBoxLayout();
|
||||
layer_1_layout->setContentsMargins(0, 0, 0, 0);
|
||||
layer_1_layout->setSpacing(20);
|
||||
|
||||
QHBoxLayout* layer_2_layout = new QHBoxLayout();
|
||||
layer_2_layout->setContentsMargins(0, 0, 0, 0);
|
||||
layer_2_layout->setSpacing(20);
|
||||
|
||||
groupbox_layout->addLayout(layer_1_layout);
|
||||
groupbox_layout->addLayout(layer_2_layout);
|
||||
|
||||
group_layout->addLayout(groupbox_layout);
|
||||
|
||||
XCheckBox* checkbox1 = new XCheckBox();
|
||||
checkbox1->setText("Test Checkbox");
|
||||
|
||||
XCheckBox* checkbox2 = new XCheckBox();
|
||||
checkbox2->set_custom(true);
|
||||
checkbox2->set_checked_color(QColor(255, 150, 100));
|
||||
checkbox2->setText("Alternate Color");
|
||||
|
||||
layer_1_layout->addWidget(checkbox1);
|
||||
layer_1_layout->addWidget(checkbox2);
|
||||
|
||||
layer_1_layout->addStretch();
|
||||
|
||||
XCheckBox* checkbox3 = new XCheckBox();
|
||||
checkbox3->setText("Checkbox with really long text to test truncation");
|
||||
|
||||
layer_2_layout->addWidget(checkbox3);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateRadioButtonGroup() {
|
||||
QWidget* group = new QWidget();
|
||||
|
||||
QVBoxLayout* group_layout = new QVBoxLayout();
|
||||
group_layout->setContentsMargins(32, 0, 32, 0);
|
||||
group_layout->setSpacing(0);
|
||||
group->setLayout(group_layout);
|
||||
|
||||
XGroupBox* groupbox = new XGroupBox("Radio Buttons");
|
||||
|
||||
QVBoxLayout* groupbox_layout = new QVBoxLayout();
|
||||
groupbox_layout->setContentsMargins(16, 16, 16, 16);
|
||||
groupbox->setLayout(groupbox_layout);
|
||||
|
||||
group_layout->addWidget(groupbox);
|
||||
|
||||
QHBoxLayout* layer_1_layout = new QHBoxLayout();
|
||||
layer_1_layout->setContentsMargins(0, 0, 0, 0);
|
||||
layer_1_layout->setSpacing(20);
|
||||
|
||||
QHBoxLayout* layer_2_layout = new QHBoxLayout();
|
||||
layer_2_layout->setContentsMargins(0, 0, 0, 0);
|
||||
layer_2_layout->setSpacing(20);
|
||||
|
||||
groupbox_layout->addLayout(layer_1_layout);
|
||||
groupbox_layout->addLayout(layer_2_layout);
|
||||
|
||||
group_layout->addLayout(groupbox_layout);
|
||||
|
||||
XRadioButton* radio1 = new XRadioButton();
|
||||
radio1->setText("Test Radio Button 1");
|
||||
|
||||
XRadioButton* radio2 = new XRadioButton();
|
||||
radio2->setText("Test Radio Button 2");
|
||||
|
||||
layer_1_layout->addWidget(radio1);
|
||||
layer_1_layout->addWidget(radio2);
|
||||
|
||||
layer_1_layout->addStretch();
|
||||
|
||||
XRadioButton* radio3 = new XRadioButton();
|
||||
radio3->setText("Radio Button with really long text to test truncation");
|
||||
radio3->set_custom(true);
|
||||
radio3->set_checked_color(QColor(255, 150, 100));
|
||||
|
||||
XRadioButton* radio4 = new XRadioButton();
|
||||
radio4->setText("Error");
|
||||
radio4->set_custom(true);
|
||||
radio4->set_checked_color(QColor(255, 0, 0));
|
||||
|
||||
layer_2_layout->addWidget(radio3);
|
||||
layer_2_layout->addWidget(radio4);
|
||||
|
||||
layer_2_layout->addStretch();
|
||||
|
||||
// add radio buttons to their respective groups
|
||||
|
||||
QButtonGroup* bg1 = new QButtonGroup();
|
||||
QButtonGroup* bg2 = new QButtonGroup();
|
||||
|
||||
bg1->addButton(radio1);
|
||||
bg1->addButton(radio2);
|
||||
|
||||
bg2->addButton(radio3);
|
||||
bg2->addButton(radio4);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateInputGroup() {
|
||||
QWidget* group = new QWidget();
|
||||
|
||||
QVBoxLayout* group_layout = new QVBoxLayout();
|
||||
group_layout->setContentsMargins(32, 0, 32, 0);
|
||||
group_layout->setSpacing(0);
|
||||
group->setLayout(group_layout);
|
||||
|
||||
XGroupBox* groupbox = new XGroupBox("Input Boxes");
|
||||
|
||||
QVBoxLayout* groupbox_layout = new QVBoxLayout();
|
||||
groupbox_layout->setContentsMargins(16, 16, 16, 16);
|
||||
groupbox->setLayout(groupbox_layout);
|
||||
|
||||
QLabel* lineedit_label = new QLabel("Line Edit");
|
||||
groupbox_layout->addWidget(lineedit_label);
|
||||
|
||||
QHBoxLayout* line_layout = new QHBoxLayout();
|
||||
|
||||
XLineEdit* line_edit = new XLineEdit("Text...");
|
||||
XLineEdit* disabled_line = new XLineEdit("Disabled");
|
||||
disabled_line->setDisabled(true);
|
||||
|
||||
line_layout->addWidget(line_edit);
|
||||
line_layout->addWidget(disabled_line);
|
||||
|
||||
groupbox_layout->addLayout(line_layout);
|
||||
|
||||
QLabel* textedit_label = new QLabel("Text Edit");
|
||||
groupbox_layout->addWidget(textedit_label);
|
||||
|
||||
QHBoxLayout* text_layout = new QHBoxLayout();
|
||||
|
||||
XTextEdit* text_edit = new XTextEdit("Text...");
|
||||
text_edit->setMaximumHeight(80);
|
||||
|
||||
XTextEdit* disabled_edit = new XTextEdit("Disabled");
|
||||
disabled_edit->setDisabled(true);
|
||||
disabled_edit->setMaximumHeight(80);
|
||||
|
||||
text_layout->addWidget(text_edit);
|
||||
text_layout->addWidget(disabled_edit);
|
||||
|
||||
groupbox_layout->addLayout(text_layout);
|
||||
|
||||
group_layout->addWidget(groupbox);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateTab1Widget(XTabSelector* tab_selector,
|
||||
QStackedLayout* tab_stack_layout) {
|
||||
QWidget* widget = new QWidget();
|
||||
QVBoxLayout* layout = new QVBoxLayout(widget);
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(16, 16, 16, 16);
|
||||
|
||||
XPushButton* changeTabButton = new XPushButton("Go to Tab 2");
|
||||
changeTabButton->setMinimumSize(100, 24);
|
||||
connect(changeTabButton, &QPushButton::clicked, [=]() {
|
||||
tab_selector->SetTabIndex(1);
|
||||
tab_stack_layout->setCurrentIndex(1);
|
||||
});
|
||||
|
||||
layout->addWidget(changeTabButton, 0, Qt::AlignCenter);
|
||||
layout->addStretch();
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateTab2Widget(XTabSelector* tab_selector,
|
||||
QStackedLayout* tab_stack_layout) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QWidget* DebugTab::CreateTab3Widget(XTabSelector* tab_selector,
|
||||
QStackedLayout* tab_stack_layout) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,65 @@
|
|||
#ifndef XENIA_UI_QT_TABS_DEBUG_H_
|
||||
#define XENIA_UI_QT_TABS_DEBUG_H_
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QStackedLayout>
|
||||
#include <QStackedWidget>
|
||||
|
||||
#include "xenia/ui/qt/widgets/sidebar.h"
|
||||
#include "xenia/ui/qt/widgets/tab.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class XTabSelector;
|
||||
|
||||
// TODO: should this be in its own file for reusability?
|
||||
// Represents a sidebar item and a widget that is shown when the item is clicked
|
||||
struct SidebarItem {
|
||||
QChar glyph;
|
||||
const char* name;
|
||||
QWidget* widget;
|
||||
};
|
||||
|
||||
class DebugTab : public XTab {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DebugTab();
|
||||
|
||||
private:
|
||||
void Build();
|
||||
void BuildSidebar();
|
||||
|
||||
QWidget* CreateComponentsTab();
|
||||
QWidget* CreateNavigationTab();
|
||||
QWidget* CreateThemeTab();
|
||||
QWidget* CreateLibraryTab();
|
||||
|
||||
// create widgets for "components" tab
|
||||
QWidget* CreateButtonGroup();
|
||||
QWidget* CreateSliderGroup();
|
||||
QWidget* CreateCheckboxGroup();
|
||||
QWidget* CreateRadioButtonGroup();
|
||||
QWidget* CreateInputGroup();
|
||||
|
||||
// create widgets for "navigation" tab
|
||||
QWidget* CreateTab1Widget(XTabSelector* tab_selector,
|
||||
QStackedLayout* tab_stack_layout);
|
||||
QWidget* CreateTab2Widget(XTabSelector* tab_selector,
|
||||
QStackedLayout* tab_stack_layout);
|
||||
QWidget* CreateTab3Widget(XTabSelector* tab_selector,
|
||||
QStackedLayout* tab_stack_layout);
|
||||
|
||||
QHBoxLayout* layout_ = nullptr;
|
||||
QWidget* sidebar_container_ = nullptr;
|
||||
XSideBar* sidebar_ = nullptr;
|
||||
QStackedWidget* content_widget_ = nullptr;
|
||||
QList<SidebarItem> sidebar_items_;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,255 @@
|
|||
#include "xenia/ui/qt/tabs/home_tab.h"
|
||||
|
||||
#include <QFIleDialog>
|
||||
#include <QGraphicsEffect>
|
||||
#include <QProgressBar>
|
||||
|
||||
#include "xenia/app/emulator_window.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/ui/qt/actions/action.h"
|
||||
#include "xenia/ui/qt/main_window.h"
|
||||
#include "xenia/ui/qt/widgets/separator.h"
|
||||
#include "xenia/ui/qt/widgets/slider.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
HomeTab::HomeTab() : XTab("Home", "HomeTab") { Build(); }
|
||||
|
||||
void HomeTab::Build() {
|
||||
layout_ = new QHBoxLayout();
|
||||
layout_->setMargin(0);
|
||||
layout_->setSpacing(0);
|
||||
setLayout(layout_);
|
||||
|
||||
BuildSidebar();
|
||||
BuildRecentView();
|
||||
}
|
||||
|
||||
void HomeTab::BuildSidebar() {
|
||||
// sidebar container widget
|
||||
sidebar_ = new QWidget(this);
|
||||
sidebar_->setObjectName("sidebarContainer");
|
||||
|
||||
QVBoxLayout* sidebar_layout = new QVBoxLayout;
|
||||
sidebar_layout->setMargin(0);
|
||||
sidebar_layout->setSpacing(0);
|
||||
|
||||
sidebar_->setLayout(sidebar_layout);
|
||||
|
||||
// Add drop shadow to sidebar widget
|
||||
QGraphicsDropShadowEffect* effect = new QGraphicsDropShadowEffect;
|
||||
effect->setBlurRadius(16);
|
||||
effect->setXOffset(4);
|
||||
effect->setYOffset(0);
|
||||
effect->setColor(QColor(0, 0, 0, 64));
|
||||
|
||||
sidebar_->setGraphicsEffect(effect);
|
||||
|
||||
// Create sidebar
|
||||
sidebar_toolbar_ = new XSideBar;
|
||||
sidebar_toolbar_->setOrientation(Qt::Vertical);
|
||||
sidebar_toolbar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
sidebar_toolbar_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
|
||||
|
||||
// Create sidebar title
|
||||
QWidget* sidebar_title = new QWidget;
|
||||
sidebar_title->setObjectName("sidebarTitle");
|
||||
|
||||
QVBoxLayout* title_layout = new QVBoxLayout;
|
||||
title_layout->setMargin(0);
|
||||
title_layout->setContentsMargins(0, 40, 0, 0);
|
||||
title_layout->setSpacing(0);
|
||||
|
||||
sidebar_title->setLayout(title_layout);
|
||||
|
||||
// Title labels
|
||||
QLabel* xenia_title = new QLabel("Xenia");
|
||||
xenia_title->setObjectName("sidebarTitleLabel");
|
||||
|
||||
QLabel* xenia_subtitle = new QLabel("Xbox 360 Emulator");
|
||||
xenia_subtitle->setObjectName("sidebarSubtitleLabel");
|
||||
|
||||
title_layout->addWidget(xenia_title, 0, Qt::AlignHCenter | Qt::AlignBottom);
|
||||
title_layout->addWidget(xenia_subtitle, 0, Qt::AlignHCenter | Qt::AlignTop);
|
||||
|
||||
// Title separator
|
||||
auto separator = new XSeparator;
|
||||
title_layout->addWidget(separator, 0, Qt::AlignHCenter);
|
||||
|
||||
// Setup Sidebar toolbar
|
||||
sidebar_toolbar_->addWidget(sidebar_title);
|
||||
|
||||
sidebar_toolbar_->addSpacing(20);
|
||||
|
||||
auto open_file_btn = sidebar_toolbar_->addAction(0xE838, "Open File");
|
||||
connect(open_file_btn, &XSideBarButton::clicked, this,
|
||||
&HomeTab::OpenFileTriggered);
|
||||
|
||||
auto import_folder_btn = sidebar_toolbar_->addAction(0xE8F4, "Import Folder");
|
||||
connect(import_folder_btn, &XSideBarButton::clicked, this,
|
||||
&HomeTab::ImportFolderTriggered);
|
||||
|
||||
sidebar_toolbar_->addSeparator();
|
||||
|
||||
sidebar_layout->addWidget(sidebar_toolbar_, 0,
|
||||
Qt::AlignHCenter | Qt::AlignTop);
|
||||
sidebar_layout->addStretch(1);
|
||||
|
||||
// Add sidebar to tab widget
|
||||
layout_->addWidget(sidebar_, 0, Qt::AlignLeft);
|
||||
}
|
||||
|
||||
void HomeTab::BuildRecentView() {
|
||||
// Create container widget
|
||||
QWidget* recent_container = new QWidget(this);
|
||||
|
||||
QVBoxLayout* recent_layout = new QVBoxLayout(this);
|
||||
recent_layout->setContentsMargins(0, 0, 0, 0);
|
||||
recent_layout->setSpacing(0);
|
||||
|
||||
recent_container->setLayout(recent_layout);
|
||||
|
||||
// Setup toolbar
|
||||
auto toolbar = recent_toolbar_;
|
||||
toolbar = new XToolBar(this);
|
||||
|
||||
QLabel* title = new QLabel("Recent Games");
|
||||
title->setObjectName("recentGames");
|
||||
|
||||
toolbar->addWidget(title);
|
||||
|
||||
toolbar->addSeparator();
|
||||
|
||||
// TODO: handle button clicks
|
||||
auto play_btn = toolbar->addAction(new XAction(QChar(0xEDB5), "Play"));
|
||||
connect(play_btn, &XToolBarItem::clicked, [this]() { PlayTriggered(); });
|
||||
|
||||
toolbar->addAction(new XAction(QChar(0xEBE8), "Debug"));
|
||||
toolbar->addAction(new XAction(QChar(0xE946), "Info"));
|
||||
|
||||
toolbar->addSeparator();
|
||||
|
||||
toolbar->addAction(new XAction(QChar(0xE8FD), "List"));
|
||||
toolbar->addAction(new XAction(QChar(0xF0E2), "Grid"));
|
||||
|
||||
// TODO: hide slider unless "Grid" mode is selected
|
||||
|
||||
auto* slider = new XSlider(Qt::Horizontal, this);
|
||||
slider->setRange(48, 96);
|
||||
slider->setFixedWidth(100);
|
||||
toolbar->addWidget(slider);
|
||||
|
||||
recent_layout->addWidget(toolbar);
|
||||
|
||||
// Create recent games list view
|
||||
// TODO: this should only be shown when "List" mode selected in toolbar
|
||||
// and should also only load games from a "recent" cache
|
||||
|
||||
list_view_ = new XGameListView(this);
|
||||
recent_layout->addWidget(list_view_);
|
||||
|
||||
layout_->addWidget(recent_container);
|
||||
|
||||
// Lower the widget to prevent overlap with sidebar's shadow
|
||||
recent_container->lower();
|
||||
}
|
||||
|
||||
void HomeTab::PlayTriggered() {
|
||||
// Get path from table and launch game in new EmulatorWindow
|
||||
// This is purely a proof of concept
|
||||
auto index = list_view_->selectionModel();
|
||||
if (index->hasSelection()) {
|
||||
QModelIndexList path_row =
|
||||
index->selectedRows(static_cast<int>(GameColumn::kPathColumn));
|
||||
|
||||
const QModelIndex& path_index = path_row.at(0);
|
||||
|
||||
QString path = path_index.data().toString();
|
||||
|
||||
/*wchar_t* title_w = new wchar_t[title.length() + 1];
|
||||
title.toWCharArray(title_w);
|
||||
title_w[title.length()] = '\0';*/
|
||||
|
||||
auto win = qobject_cast<QtWindow*>(window());
|
||||
app::EmulatorWindow* wnd = new app::EmulatorWindow(win->loop(), "");
|
||||
/*wnd->resize(1280, 720);
|
||||
wnd->show();*/
|
||||
win->setCentralWidget(wnd);
|
||||
|
||||
wnd->Launch(path.toStdString());
|
||||
}
|
||||
}
|
||||
|
||||
void HomeTab::OpenFileTriggered() {
|
||||
QString file_name = QFileDialog::getOpenFileName(
|
||||
this, "Open Game", "",
|
||||
tr("Xbox 360 Executable (*.xex);;Disc Image (*.iso);;All Files (*)"));
|
||||
if (!file_name.isEmpty()) {
|
||||
// this manual conversion seems to be required as Qt's std::(w)string impl
|
||||
// and the one i've been linking to seem incompatible
|
||||
wchar_t* path_w = new wchar_t[file_name.length() + 1];
|
||||
file_name.toWCharArray(path_w);
|
||||
path_w[file_name.length()] = '\0';
|
||||
|
||||
XGameLibrary* lib = XGameLibrary::Instance();
|
||||
lib->ScanPath(path_w);
|
||||
|
||||
list_view_->RefreshGameList();
|
||||
}
|
||||
}
|
||||
|
||||
void HomeTab::ImportFolderTriggered() {
|
||||
QString path = QFileDialog::getExistingDirectory(this, "Open Folder", "");
|
||||
if (!path.isEmpty()) {
|
||||
// this manual conversion seems to be required as Qt's std::(w)string impl
|
||||
// and the one i've been linking to seem incompatible
|
||||
wchar_t* path_w = new wchar_t[path.length() + 1];
|
||||
path.toWCharArray(path_w);
|
||||
path_w[path.length()] = '\0';
|
||||
|
||||
QWidget* progress_widget = new QWidget();
|
||||
QHBoxLayout* layout = new QHBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(16);
|
||||
progress_widget->setLayout(layout);
|
||||
|
||||
QLabel* label = new QLabel("Scanning directories...");
|
||||
label->setStyleSheet("color: #c7c7c7");
|
||||
layout->addWidget(label);
|
||||
|
||||
QProgressBar* bar = new QProgressBar();
|
||||
bar->setFixedSize(120, 16);
|
||||
bar->setRange(0, 100);
|
||||
bar->setValue(0);
|
||||
bar->setTextVisible(false);
|
||||
layout->addWidget(bar);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
MainWindow* window = qobject_cast<MainWindow*>(this->window());
|
||||
window->AddStatusBarWidget(progress_widget);
|
||||
|
||||
XGameLibrary* lib = XGameLibrary::Instance();
|
||||
lib->ScanPathAsync(path_w, [=](double progress, const XGameEntry& entry) {
|
||||
// update progress bar on main UI thread
|
||||
QMetaObject::invokeMethod(
|
||||
bar,
|
||||
[=]() {
|
||||
bar->setValue(progress);
|
||||
if (progress == 100.0) {
|
||||
window->RemoveStatusBarWidget(progress_widget);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
// Just a PoC. In future change to refresh list
|
||||
// when all games added.
|
||||
list_view_->RefreshGameList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,41 @@
|
|||
#ifndef XENIA_UI_QT_TABS_HOME_H_
|
||||
#define XENIA_UI_QT_TABS_HOME_H_
|
||||
|
||||
#include <QToolBar>
|
||||
#include "xenia/ui/qt/widgets/game_listview.h"
|
||||
#include "xenia/ui/qt/widgets/sidebar.h"
|
||||
#include "xenia/ui/qt/widgets/sidebar_button.h"
|
||||
#include "xenia/ui/qt/widgets/tab.h"
|
||||
#include "xenia/ui/qt/widgets/toolbar.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class HomeTab : public XTab {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HomeTab();
|
||||
|
||||
public slots:
|
||||
void PlayTriggered();
|
||||
void OpenFileTriggered();
|
||||
void ImportFolderTriggered();
|
||||
|
||||
private:
|
||||
void Build();
|
||||
void BuildSidebar();
|
||||
void BuildRecentView();
|
||||
|
||||
QHBoxLayout* layout_ = nullptr;
|
||||
QWidget* sidebar_ = nullptr;
|
||||
XSideBar* sidebar_toolbar_ = nullptr;
|
||||
XToolBar* recent_toolbar_ = nullptr;
|
||||
XGameListView* list_view_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif XENIA_UI_QT_TABS_HOME_H_
|
|
@ -0,0 +1,58 @@
|
|||
#include "xenia/ui/qt/tabs/library_tab.h"
|
||||
#include "xenia/ui/qt/actions/add_game.h"
|
||||
#include "xenia/ui/qt/widgets/slider.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
LibraryTab::LibraryTab() : XTab("Library") {
|
||||
Build();
|
||||
return;
|
||||
}
|
||||
|
||||
void LibraryTab::Build() {
|
||||
layout_ = new QVBoxLayout();
|
||||
layout_->setMargin(0);
|
||||
layout_->setSpacing(0);
|
||||
setLayout(layout_);
|
||||
|
||||
BuildToolBar();
|
||||
BuildListView();
|
||||
connect(slider_, SIGNAL(valueChanged(int)), list_view_,
|
||||
SLOT(setRowSize(int)));
|
||||
}
|
||||
|
||||
void LibraryTab::BuildToolBar() {
|
||||
toolbar_ = new XToolBar(this);
|
||||
toolbar_->setFixedHeight(46);
|
||||
layout_->addWidget(toolbar_);
|
||||
|
||||
toolbar_->addAction(new XAddGameAction());
|
||||
toolbar_->addAction(new XAction(QChar(0xF12B), "Add Folder"));
|
||||
|
||||
toolbar_->addSeparator();
|
||||
|
||||
toolbar_->addAction(new XAction(QChar(0xEDB5), "Play"));
|
||||
toolbar_->addAction(new XAction(QChar(0xEBE8), "Debug"));
|
||||
toolbar_->addAction(new XAction(QChar(0xE946), "Info"));
|
||||
|
||||
toolbar_->addSeparator();
|
||||
|
||||
toolbar_->addAction(new XAction(QChar(0xE8FD), "List"));
|
||||
toolbar_->addAction(new XAction(QChar(0xF0E2), "Grid"));
|
||||
|
||||
slider_ = new XSlider(Qt::Horizontal, this);
|
||||
slider_->setRange(48,96);
|
||||
slider_->setFixedWidth(100);
|
||||
toolbar_->addWidget(slider_);
|
||||
}
|
||||
|
||||
void LibraryTab::BuildListView() {
|
||||
list_view_ = new XGameListView(this);
|
||||
layout_->addWidget(list_view_);
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,32 @@
|
|||
#ifndef XENIA_UI_QT_TABS_LIBRARY_H_
|
||||
#define XENIA_UI_QT_TABS_LIBRARY_H_
|
||||
|
||||
#include "xenia/ui/qt/widgets/game_listview.h"
|
||||
#include "xenia/ui/qt/widgets/tab.h"
|
||||
#include "xenia/ui/qt/widgets/toolbar.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class LibraryTab : public XTab {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LibraryTab();
|
||||
|
||||
private:
|
||||
void Build();
|
||||
void BuildToolBar();
|
||||
void BuildListView();
|
||||
|
||||
QVBoxLayout* layout_ = nullptr;
|
||||
XToolBar* toolbar_ = nullptr;
|
||||
XGameListView* list_view_ = nullptr;
|
||||
QSlider* slider_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,16 @@
|
|||
#include "advanced_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
void AdvancedPane::Build() {
|
||||
QWidget* widget = new QWidget();
|
||||
widget->setStyleSheet("background: green");
|
||||
|
||||
set_widget(widget);
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef XENIA_UI_QT_ADVANCED_PANE_H_
|
||||
#define XENIA_UI_QT_ADVANCED_PANE_H_
|
||||
|
||||
#include "settings_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class AdvancedPane : public SettingsPane {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AdvancedPane() : SettingsPane(0xE7BA, "Advanced") {}
|
||||
|
||||
void Build() override;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,16 @@
|
|||
#include "controls_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
void ControlsPane::Build() {
|
||||
QWidget* widget = new QWidget();
|
||||
widget->setStyleSheet("background: yellow");
|
||||
|
||||
set_widget(widget);
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef XENIA_UI_QT_CONTROLS_PANE_H_
|
||||
#define XENIA_UI_QT_CONTROLS_PANE_H_
|
||||
|
||||
#include "settings_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class ControlsPane : public SettingsPane {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ControlsPane() : SettingsPane(0xE7FC, "Controls") {}
|
||||
|
||||
void Build() override;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,16 @@
|
|||
#include "cpu_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
void CPUPane::Build() {
|
||||
QWidget* widget = new QWidget();
|
||||
widget->setStyleSheet("background: gray");
|
||||
|
||||
set_widget(widget);
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef XENIA_UI_QT_CPU_PANE_H_
|
||||
#define XENIA_UI_QT_CPU_PANE_H_
|
||||
|
||||
#include "settings_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class CPUPane : public SettingsPane {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CPUPane() : SettingsPane(0xEC4A, "CPU") {}
|
||||
|
||||
void Build() override;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,100 @@
|
|||
#include "general_pane.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "xenia/ui/qt/widgets/checkbox.h"
|
||||
#include "xenia/ui/qt/widgets/combobox.h"
|
||||
#include "xenia/ui/qt/widgets/groupbox.h"
|
||||
#include "xenia/ui/qt/widgets/scroll_area.h"
|
||||
|
||||
DECLARE_bool(show_debug_tab)
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
const QStringList game_languages = {
|
||||
"English", "Japanese", "German", "French", "Spanish",
|
||||
"Italian", "Korean", "Chinese", "Portugese", "Polish",
|
||||
"Russian", "Swedish", "Turkish", "Norwegian", "Dutch"};
|
||||
|
||||
void GeneralPane::Build() {
|
||||
QWidget* base_widget = new QWidget();
|
||||
base_widget->setSizePolicy(QSizePolicy::MinimumExpanding,
|
||||
QSizePolicy::MinimumExpanding);
|
||||
|
||||
// Setup scroll area for settings pane
|
||||
XScrollArea* scroll_area = new XScrollArea(this);
|
||||
scroll_area->setWidget(base_widget);
|
||||
scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scroll_area->setWidgetResizable(true);
|
||||
|
||||
QVBoxLayout* layout = new QVBoxLayout();
|
||||
base_widget->setLayout(layout);
|
||||
|
||||
layout->setSpacing(16);
|
||||
layout->setContentsMargins(32, 16, 32, 16);
|
||||
|
||||
// Add settings groupboxes to layout
|
||||
layout->addWidget(CreateGeneralGroupBox());
|
||||
layout->addWidget(CreateUpdateGroupBox());
|
||||
layout->addWidget(CreateWindowGroupBox());
|
||||
layout->addWidget(CreateLogGroupBox());
|
||||
layout->addStretch();
|
||||
|
||||
set_widget(scroll_area);
|
||||
}
|
||||
|
||||
XGroupBox* GeneralPane::CreateGeneralGroupBox() {
|
||||
XGroupBox* groupbox = new XGroupBox("General Settings");
|
||||
|
||||
QVBoxLayout* groupbox_layout = new QVBoxLayout();
|
||||
groupbox_layout->setContentsMargins(16, 16, 16, 16);
|
||||
groupbox->setLayout(groupbox_layout);
|
||||
|
||||
XCheckBox* discord_presence_checkbox = new XCheckBox();
|
||||
discord_presence_checkbox->setText("Discord Rich Presence");
|
||||
|
||||
connect(discord_presence_checkbox, &XCheckBox::stateChanged,
|
||||
[&](bool value) {
|
||||
//update_config_var(cvars::cv_show_debug_tab, value);
|
||||
});
|
||||
groupbox_layout->addWidget(discord_presence_checkbox);
|
||||
|
||||
XCheckBox* game_icon_checkbox = new XCheckBox();
|
||||
game_icon_checkbox->setText("Show Game Icon in Taskbar");
|
||||
groupbox_layout->addWidget(game_icon_checkbox);
|
||||
|
||||
QHBoxLayout* game_language_layout = new QHBoxLayout();
|
||||
game_language_layout->setContentsMargins(0, 0, 0, 0);
|
||||
game_language_layout->setSpacing(16);
|
||||
|
||||
QLabel* game_language_label = new QLabel("Game Language");
|
||||
XComboBox* game_language_combobox = new XComboBox();
|
||||
game_language_combobox->addItems(game_languages);
|
||||
|
||||
game_language_layout->addWidget(game_language_label);
|
||||
game_language_layout->addWidget(game_language_combobox);
|
||||
game_language_layout->addStretch();
|
||||
|
||||
groupbox_layout->addLayout(game_language_layout);
|
||||
|
||||
return groupbox;
|
||||
}
|
||||
|
||||
XGroupBox* GeneralPane::CreateUpdateGroupBox() {
|
||||
return new XGroupBox("Update Settings");
|
||||
}
|
||||
|
||||
XGroupBox* GeneralPane::CreateWindowGroupBox() {
|
||||
return new XGroupBox("Window Settings");
|
||||
}
|
||||
|
||||
XGroupBox* GeneralPane::CreateLogGroupBox() {
|
||||
return new XGroupBox("Log Settings");
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,30 @@
|
|||
#ifndef XENIA_UI_QT_GENERAL_PANE_H_
|
||||
#define XENIA_UI_QT_GENERAL_PANE_H_
|
||||
|
||||
#include "settings_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class XGroupBox;
|
||||
|
||||
class GeneralPane : public SettingsPane {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GeneralPane() : SettingsPane(0xE713, "General") {}
|
||||
|
||||
void Build() override;
|
||||
|
||||
private:
|
||||
XGroupBox* CreateGeneralGroupBox();
|
||||
XGroupBox* CreateUpdateGroupBox();
|
||||
XGroupBox* CreateWindowGroupBox();
|
||||
XGroupBox* CreateLogGroupBox();
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,16 @@
|
|||
#include "gpu_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
void GPUPane::Build() {
|
||||
QWidget* widget = new QWidget();
|
||||
widget->setStyleSheet("background: orange");
|
||||
|
||||
set_widget(widget);
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef XENIA_UI_QT_GPU_PANE_H_
|
||||
#define XENIA_UI_QT_GPU_PANE_H_
|
||||
|
||||
#include "settings_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
class GPUPane : public SettingsPane {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GPUPane() : SettingsPane(0xE7F4, "GPU") {}
|
||||
|
||||
void Build() override;
|
||||
};
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif
|
|
@ -0,0 +1,16 @@
|
|||
#include "interface_pane.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
namespace qt {
|
||||
|
||||
void InterfacePane::Build() {
|
||||
QWidget* widget = new QWidget();
|
||||
widget->setStyleSheet("background: brown");
|
||||
|
||||
set_widget(widget);
|
||||
}
|
||||
|
||||
} // namespace qt
|
||||
} // namespace ui
|
||||
} // namespace xe
|