mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[UI] Implement Qt based file install dialog
This commit is contained in:
@@ -45,13 +45,40 @@ jobs:
|
||||
with:
|
||||
key: ${{ hashFiles('tools/build/bin/premake5.exe') }}
|
||||
path: tools/build/bin/premake5
|
||||
|
||||
- name: Cache Qt6
|
||||
id: cache-qt-linux
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/Qt
|
||||
key: ${{ runner.os }}-qt6-6.9.2
|
||||
restore-keys: |
|
||||
${{ runner.os }}-qt6-
|
||||
|
||||
- name: Setup
|
||||
run: |
|
||||
UBUNTU_BASE=$(lsb_release -cs)
|
||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
sudo apt-add-repository "deb http://apt.llvm.org/${UBUNTU_BASE}/ llvm-toolchain-${UBUNTU_BASE}-20 main"
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install mesa-vulkan-drivers valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-20 ninja-build spirv-tools qt6-base-dev
|
||||
sudo apt-get -y install mesa-vulkan-drivers valgrind libc++-dev libc++abi-dev libgtk-3-dev libsdl2-dev libvulkan-dev libx11-xcb-dev clang-20 ninja-build spirv-tools libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-cursor0
|
||||
|
||||
# Install Qt 6.9.2 via aqtinstall
|
||||
if [ '${{ steps.cache-qt-linux.outputs.cache-hit }}' != 'true' ]; then
|
||||
pip install aqtinstall
|
||||
echo "Available architectures for Qt 6.9.2:"
|
||||
aqt list-qt linux desktop --arch 6.9.2 || true
|
||||
aqt install-qt linux desktop 6.9.2 linux_gcc_64 -O /opt/Qt
|
||||
fi
|
||||
export PATH="/opt/Qt/6.9.2/gcc_64/bin:$PATH"
|
||||
export CMAKE_PREFIX_PATH="/opt/Qt/6.9.2/gcc_64"
|
||||
export PKG_CONFIG_PATH="/opt/Qt/6.9.2/gcc_64/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export QT_DIR="/opt/Qt/6.9.2/gcc_64"
|
||||
echo "/opt/Qt/6.9.2/gcc_64/bin" >> $GITHUB_PATH
|
||||
echo "CMAKE_PREFIX_PATH=/opt/Qt/6.9.2/gcc_64" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=/opt/Qt/6.9.2/gcc_64/lib/pkgconfig" >> $GITHUB_ENV
|
||||
echo "QT_DIR=/opt/Qt/6.9.2/gcc_64" >> $GITHUB_ENV
|
||||
|
||||
# Verify spirv-opt is available
|
||||
which spirv-opt && spirv-opt --version || echo "Warning: spirv-opt not found in PATH"
|
||||
git submodule update --init --depth=1 -j$(getconf _NPROCESSORS_ONLN) $(grep -oP '(?<=path = )(?!third_party\/(DirectXShaderCompiler|FidelityFX-(CAS|FSR)|premake-(androidndk|export-compile-commands))).+' .gitmodules)
|
||||
@@ -108,7 +135,7 @@ jobs:
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: C:\Qt
|
||||
key: ${{ runner.os }}-qt6-6.8.1
|
||||
key: ${{ runner.os }}-qt6-6.9.2
|
||||
restore-keys: |
|
||||
${{ runner.os }}-qt6-
|
||||
|
||||
@@ -146,7 +173,7 @@ jobs:
|
||||
$qtCacheHit = '${{ steps.cache-qt.outputs.cache-hit }}'
|
||||
if ($qtCacheHit -ne 'true') {
|
||||
pip install aqtinstall
|
||||
aqt install-qt windows desktop 6.8.1 win64_msvc2022_64 -O C:\Qt
|
||||
aqt install-qt windows desktop 6.9.2 win64_msvc2022_64 -O C:\Qt
|
||||
}
|
||||
|
||||
$cacheHit = '${{ steps.cache-submodules.outputs.cache-hit }}'
|
||||
|
||||
+23
-1
@@ -134,7 +134,29 @@ filter("platforms:Linux")
|
||||
system("linux")
|
||||
toolset("clang")
|
||||
pkg_config.all("gtk+-x11-3.0")
|
||||
pkg_config.all("Qt6Core Qt6Gui Qt6Widgets")
|
||||
local qt_dir = os.getenv("QT_DIR")
|
||||
if qt_dir then
|
||||
includedirs({
|
||||
path.join(qt_dir, "include"),
|
||||
path.join(qt_dir, "include/QtCore"),
|
||||
path.join(qt_dir, "include/QtGui"),
|
||||
path.join(qt_dir, "include/QtWidgets"),
|
||||
})
|
||||
libdirs({
|
||||
path.join(qt_dir, "lib"),
|
||||
})
|
||||
runpathdirs({
|
||||
path.join(qt_dir, "lib"),
|
||||
})
|
||||
links({
|
||||
"Qt6Core",
|
||||
"Qt6Gui",
|
||||
"Qt6Widgets",
|
||||
})
|
||||
else
|
||||
pkg_config.all("Qt6Core Qt6Gui Qt6Widgets")
|
||||
end
|
||||
|
||||
links({
|
||||
"stdc++fs",
|
||||
"dl",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "xenia/kernel/xam/profile_manager.h"
|
||||
#include "xenia/kernel/xam/xam_module.h"
|
||||
#include "xenia/kernel/xam/xam_state.h"
|
||||
#include "xenia/ui/content_install_dialog_qt.h"
|
||||
#include "xenia/ui/file_picker.h"
|
||||
#include "xenia/ui/graphics_provider.h"
|
||||
#include "xenia/ui/imgui_dialog.h"
|
||||
@@ -39,6 +40,7 @@
|
||||
#include "xenia/ui/presenter.h"
|
||||
#include "xenia/ui/ui_event.h"
|
||||
#include "xenia/ui/virtual_key.h"
|
||||
#include "xenia/ui/window_qt.h"
|
||||
|
||||
// Autogenerated by `xb premake`.
|
||||
#include "build/version.h"
|
||||
@@ -563,96 +565,6 @@ void EmulatorWindow::DisplayConfigDialog::OnDraw(ImGuiIO& io) {
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::ContentInstallDialog::OnDraw(ImGuiIO& io) {
|
||||
ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(20, 20), ImGuiCond_FirstUseEver);
|
||||
|
||||
bool dialog_open = true;
|
||||
if (!ImGui::Begin(
|
||||
fmt::format("Installation Progress###{}", window_id_).c_str(),
|
||||
&dialog_open,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_HorizontalScrollbar)) {
|
||||
Close();
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_everything_installed = true;
|
||||
for (const auto& entry : *installation_entries_) {
|
||||
ImGui::BeginTable(fmt::format("table_{}", entry.name_).c_str(), 2);
|
||||
ImGui::TableNextRow(0);
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
if (entry.icon_) {
|
||||
ImGui::Image(reinterpret_cast<ImTextureID>(entry.icon_.get()),
|
||||
ui::default_image_icon_size);
|
||||
} else {
|
||||
ImGui::Dummy(ui::default_image_icon_size);
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
|
||||
ImGui::Text("Name: %s", entry.name_.c_str());
|
||||
ImGui::Text("Installation Path:");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::TextLink(
|
||||
xe::path_to_utf8(entry.data_installation_path_).c_str())) {
|
||||
LaunchFileExplorer(emulator_window_.emulator_->content_root() /
|
||||
entry.data_installation_path_);
|
||||
}
|
||||
|
||||
if (entry.content_type_ != xe::XContentType::kInvalid) {
|
||||
ImGui::Text("Content Type: %s",
|
||||
XContentTypeMap.at(entry.content_type_).c_str());
|
||||
}
|
||||
|
||||
std::string result = fmt::format(
|
||||
"Status: {}", xe::Emulator::installStateStringName[static_cast<uint8_t>(
|
||||
entry.installation_state_)]);
|
||||
|
||||
if (entry.installation_state_ == xe::Emulator::InstallState::failed) {
|
||||
result += fmt::format(" - {} ({:08X})",
|
||||
entry.installation_error_message_.c_str(),
|
||||
entry.installation_result_);
|
||||
}
|
||||
|
||||
ImGui::Text("%s", result.c_str());
|
||||
ImGui::EndTable();
|
||||
|
||||
if (entry.content_size_ > 0) {
|
||||
ImGui::ProgressBar(static_cast<float>(entry.currently_installed_size_) /
|
||||
entry.content_size_);
|
||||
|
||||
if (entry.currently_installed_size_ != entry.content_size_ &&
|
||||
entry.installation_result_ == X_ERROR_SUCCESS) {
|
||||
is_everything_installed = false;
|
||||
}
|
||||
} else {
|
||||
ImGui::ProgressBar(0.0f);
|
||||
}
|
||||
|
||||
if (installation_entries_->size() > 1) {
|
||||
ImGui::Separator();
|
||||
}
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::BeginDisabled(!is_everything_installed);
|
||||
if (ImGui::Button("Close")) {
|
||||
ImGui::EndDisabled();
|
||||
Close();
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (!dialog_open && is_everything_installed) {
|
||||
Close();
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
bool EmulatorWindow::Initialize() {
|
||||
window_->AddListener(&window_listener_);
|
||||
window_->AddInputListener(&window_listener_, kZOrderEmulatorWindowInput);
|
||||
@@ -1204,8 +1116,13 @@ void EmulatorWindow::InstallContent() {
|
||||
});
|
||||
installationThread.detach();
|
||||
|
||||
new ContentInstallDialog(imgui_drawer_.get(), *this,
|
||||
content_installation_status);
|
||||
auto* qt_window = dynamic_cast<ui::QtWindow*>(window_.get());
|
||||
if (qt_window) {
|
||||
auto* dialog = new ui::ContentInstallDialogQt(qt_window->qwindow(),
|
||||
emulator_->content_root(),
|
||||
content_installation_status);
|
||||
dialog->show();
|
||||
}
|
||||
}
|
||||
|
||||
void EmulatorWindow::ExtractZarchive() {
|
||||
|
||||
@@ -168,34 +168,6 @@ class EmulatorWindow {
|
||||
EmulatorWindow& emulator_window_;
|
||||
};
|
||||
|
||||
class ContentInstallDialog final : public ui::ImGuiDialog {
|
||||
public:
|
||||
ContentInstallDialog(
|
||||
ui::ImGuiDrawer* imgui_drawer, EmulatorWindow& emulator_window,
|
||||
std::shared_ptr<std::vector<Emulator::ContentInstallEntry>> entries)
|
||||
: ui::ImGuiDialog(imgui_drawer),
|
||||
emulator_window_(emulator_window),
|
||||
installation_entries_(entries) {
|
||||
window_id_ = GetWindowId();
|
||||
}
|
||||
|
||||
~ContentInstallDialog() {
|
||||
for (auto& entry : *installation_entries_) {
|
||||
entry.icon_.release();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
void OnDraw(ImGuiIO& io) override;
|
||||
|
||||
private:
|
||||
uint64_t window_id_;
|
||||
|
||||
EmulatorWindow& emulator_window_;
|
||||
std::shared_ptr<std::vector<Emulator::ContentInstallEntry>>
|
||||
installation_entries_;
|
||||
};
|
||||
|
||||
class DisplayConfigDialog final : public ui::ImGuiDialog {
|
||||
public:
|
||||
DisplayConfigDialog(ui::ImGuiDrawer* imgui_drawer,
|
||||
|
||||
+51
-3
@@ -852,9 +852,12 @@ X_STATUS Emulator::ProcessContentPackageHeader(
|
||||
installation_info.content_size_ = header->content_metadata.content_size;
|
||||
installation_info.installation_state_ = InstallState::pending;
|
||||
|
||||
installation_info.icon_ = imgui_drawer_->LoadImGuiIcon(
|
||||
std::span<const uint8_t>(header->content_metadata.title_thumbnail,
|
||||
header->content_metadata.title_thumbnail_size));
|
||||
// Store raw PNG data for Qt dialog
|
||||
installation_info.icon_data_.assign(
|
||||
header->content_metadata.title_thumbnail,
|
||||
header->content_metadata.title_thumbnail +
|
||||
header->content_metadata.title_thumbnail_size);
|
||||
|
||||
return X_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -862,6 +865,14 @@ X_STATUS Emulator::InstallContentPackage(
|
||||
const std::filesystem::path& path, ContentInstallEntry& installation_info) {
|
||||
installation_info.installation_state_ = InstallState::preparing;
|
||||
|
||||
// Check if installation was cancelled before starting
|
||||
if (installation_info.cancelled_.load()) {
|
||||
installation_info.installation_state_ = InstallState::failed;
|
||||
installation_info.installation_error_message_ = "Installation cancelled";
|
||||
installation_info.installation_result_ = X_ERROR_CANCELLED;
|
||||
return X_ERROR_CANCELLED;
|
||||
}
|
||||
|
||||
std::unique_ptr<vfs::XContentContainerDevice> device =
|
||||
vfs::XContentContainerDevice::CreateContentDevice("", path);
|
||||
|
||||
@@ -914,9 +925,46 @@ X_STATUS Emulator::InstallContentPackage(
|
||||
|
||||
vfs::VirtualFileSystem::ExtractContentHeader(device.get(), header_path);
|
||||
|
||||
// Check for cancellation before extracting files
|
||||
if (installation_info.cancelled_.load()) {
|
||||
installation_info.installation_state_ = InstallState::failed;
|
||||
installation_info.installation_error_message_ = "Installation cancelled";
|
||||
installation_info.installation_result_ = X_ERROR_CANCELLED;
|
||||
|
||||
// Clean up partial installation
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(installation_path)) {
|
||||
std::filesystem::remove_all(installation_path, ec);
|
||||
}
|
||||
if (std::filesystem::exists(header_path)) {
|
||||
std::filesystem::remove(header_path, ec);
|
||||
}
|
||||
|
||||
return X_ERROR_CANCELLED;
|
||||
}
|
||||
|
||||
X_STATUS error_code = vfs::VirtualFileSystem::ExtractContentFiles(
|
||||
device.get(), installation_path,
|
||||
installation_info.currently_installed_size_);
|
||||
|
||||
// Check for cancellation after extraction
|
||||
if (installation_info.cancelled_.load()) {
|
||||
installation_info.installation_state_ = InstallState::failed;
|
||||
installation_info.installation_error_message_ = "Installation cancelled";
|
||||
installation_info.installation_result_ = X_ERROR_CANCELLED;
|
||||
|
||||
// Clean up partial installation
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(installation_path)) {
|
||||
std::filesystem::remove_all(installation_path, ec);
|
||||
}
|
||||
if (std::filesystem::exists(header_path)) {
|
||||
std::filesystem::remove(header_path, ec);
|
||||
}
|
||||
|
||||
return X_ERROR_CANCELLED;
|
||||
}
|
||||
|
||||
if (error_code != X_ERROR_SUCCESS) {
|
||||
installation_info.installation_state_ = InstallState::failed;
|
||||
return error_code;
|
||||
|
||||
+42
-1
@@ -249,6 +249,46 @@ class Emulator {
|
||||
struct ContentInstallEntry {
|
||||
ContentInstallEntry(std::filesystem::path path) : path_(path) {};
|
||||
|
||||
// Move constructor
|
||||
ContentInstallEntry(ContentInstallEntry&& other) noexcept
|
||||
: name_(std::move(other.name_)),
|
||||
path_(std::move(other.path_)),
|
||||
data_installation_path_(std::move(other.data_installation_path_)),
|
||||
header_installation_path_(std::move(other.header_installation_path_)),
|
||||
content_size_(other.content_size_),
|
||||
currently_installed_size_(other.currently_installed_size_),
|
||||
content_type_(other.content_type_),
|
||||
installation_state_(other.installation_state_),
|
||||
installation_result_(other.installation_result_),
|
||||
installation_error_message_(
|
||||
std::move(other.installation_error_message_)),
|
||||
icon_data_(std::move(other.icon_data_)),
|
||||
cancelled_(other.cancelled_.load()) {}
|
||||
|
||||
// Move assignment
|
||||
ContentInstallEntry& operator=(ContentInstallEntry&& other) noexcept {
|
||||
if (this != &other) {
|
||||
name_ = std::move(other.name_);
|
||||
path_ = std::move(other.path_);
|
||||
data_installation_path_ = std::move(other.data_installation_path_);
|
||||
header_installation_path_ = std::move(other.header_installation_path_);
|
||||
content_size_ = other.content_size_;
|
||||
currently_installed_size_ = other.currently_installed_size_;
|
||||
content_type_ = other.content_type_;
|
||||
installation_state_ = other.installation_state_;
|
||||
installation_result_ = other.installation_result_;
|
||||
installation_error_message_ =
|
||||
std::move(other.installation_error_message_);
|
||||
icon_data_ = std::move(other.icon_data_);
|
||||
cancelled_.store(other.cancelled_.load());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Delete copy constructor and copy assignment
|
||||
ContentInstallEntry(const ContentInstallEntry&) = delete;
|
||||
ContentInstallEntry& operator=(const ContentInstallEntry&) = delete;
|
||||
|
||||
std::string name_{};
|
||||
std::filesystem::path path_;
|
||||
std::filesystem::path data_installation_path_;
|
||||
@@ -262,7 +302,8 @@ class Emulator {
|
||||
X_STATUS installation_result_{};
|
||||
std::string installation_error_message_{};
|
||||
|
||||
std::unique_ptr<ui::ImmediateTexture> icon_;
|
||||
std::vector<uint8_t> icon_data_; // Raw PNG data for Qt dialog
|
||||
std::atomic<bool> cancelled_{false}; // Flag to cancel installation
|
||||
};
|
||||
|
||||
// Migrates data from content to content/xuid with respect to common data.
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2025 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/ui/content_install_dialog_qt.h"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QImage>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QScrollArea>
|
||||
#include <QUrl>
|
||||
|
||||
#include "third_party/fmt/include/fmt/format.h"
|
||||
#include "xenia/base/system.h"
|
||||
#include "xenia/xbox.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
ContentInstallDialogQt::ContentInstallDialogQt(
|
||||
QWidget* parent, const std::filesystem::path& content_root,
|
||||
std::shared_ptr<std::vector<Emulator::ContentInstallEntry>> entries)
|
||||
: QDialog(parent),
|
||||
content_root_(content_root),
|
||||
installation_entries_(entries) {
|
||||
SetupUI();
|
||||
|
||||
// Update every 100ms
|
||||
update_timer_ = new QTimer(this);
|
||||
connect(update_timer_, &QTimer::timeout, this,
|
||||
&ContentInstallDialogQt::UpdateProgress);
|
||||
update_timer_->start(100);
|
||||
}
|
||||
|
||||
ContentInstallDialogQt::~ContentInstallDialogQt() {
|
||||
if (update_timer_) {
|
||||
update_timer_->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ContentInstallDialogQt::SetupUI() {
|
||||
setWindowTitle("Installation Progress");
|
||||
setModal(true);
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
auto* main_layout = new QVBoxLayout(this);
|
||||
|
||||
// Create table widget with 1 column per entry
|
||||
table_widget_ = new QTableWidget(this);
|
||||
table_widget_->setColumnCount(1);
|
||||
table_widget_->setRowCount(static_cast<int>(installation_entries_->size()));
|
||||
table_widget_->horizontalHeader()->setVisible(false);
|
||||
table_widget_->verticalHeader()->setVisible(false);
|
||||
table_widget_->setShowGrid(false);
|
||||
table_widget_->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
table_widget_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table_widget_->horizontalHeader()->setStretchLastSection(true);
|
||||
|
||||
// Populate table for each entry
|
||||
for (size_t i = 0; i < installation_entries_->size(); i++) {
|
||||
auto& entry = (*installation_entries_)[i];
|
||||
|
||||
// Create a container widget for the entire row
|
||||
auto* row_widget = new QWidget();
|
||||
auto* row_layout = new QHBoxLayout(row_widget);
|
||||
row_layout->setContentsMargins(10, 10, 10, 10);
|
||||
row_layout->setSpacing(15);
|
||||
|
||||
// Icon label - will be sized dynamically based on row height
|
||||
auto* icon_label = new QLabel();
|
||||
icon_label->setMinimumSize(80, 80);
|
||||
icon_label->setAlignment(Qt::AlignCenter);
|
||||
|
||||
// Load icon from PNG data if available
|
||||
if (!entry.icon_data_.empty()) {
|
||||
QImage image;
|
||||
if (image.loadFromData(entry.icon_data_.data(),
|
||||
static_cast<int>(entry.icon_data_.size()))) {
|
||||
// Scale pixmap to a larger size while preserving aspect ratio
|
||||
QPixmap pixmap = QPixmap::fromImage(image);
|
||||
icon_label->setPixmap(pixmap.scaled(80, 80, Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
} else {
|
||||
// Fallback if PNG loading fails
|
||||
icon_label->setText("📦");
|
||||
QFont icon_font = icon_label->font();
|
||||
icon_font.setPointSize(icon_font.pointSize() * 3);
|
||||
icon_label->setFont(icon_font);
|
||||
}
|
||||
} else {
|
||||
// No icon data, show placeholder
|
||||
icon_label->setText("📦");
|
||||
QFont icon_font = icon_label->font();
|
||||
icon_font.setPointSize(icon_font.pointSize() * 3);
|
||||
icon_label->setFont(icon_font);
|
||||
}
|
||||
row_layout->addWidget(icon_label);
|
||||
|
||||
// Info column
|
||||
auto* info_widget = new QWidget();
|
||||
auto* info_layout = new QVBoxLayout(info_widget);
|
||||
info_layout->setContentsMargins(0, 0, 0, 0);
|
||||
info_layout->setSpacing(2);
|
||||
|
||||
auto* name_label = new QLabel();
|
||||
name_label->setTextFormat(Qt::PlainText);
|
||||
info_layout->addWidget(name_label);
|
||||
|
||||
auto* path_label = new QLabel();
|
||||
path_label->setTextFormat(Qt::RichText);
|
||||
path_label->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||
path_label->setOpenExternalLinks(false);
|
||||
connect(
|
||||
path_label, &QLabel::linkActivated, this, [this, i](const QString&) {
|
||||
auto& entry = (*installation_entries_)[i];
|
||||
// Launch file explorer in a separate thread to avoid blocking the
|
||||
// UI
|
||||
std::thread path_open(LaunchFileExplorer,
|
||||
content_root_ / entry.data_installation_path_);
|
||||
path_open.detach();
|
||||
});
|
||||
info_layout->addWidget(path_label);
|
||||
|
||||
auto* type_label = new QLabel();
|
||||
info_layout->addWidget(type_label);
|
||||
|
||||
auto* status_label = new QLabel();
|
||||
info_layout->addWidget(status_label);
|
||||
|
||||
auto* progress_bar = new QProgressBar();
|
||||
progress_bar->setMinimum(0);
|
||||
progress_bar->setMaximum(100);
|
||||
info_layout->addWidget(progress_bar);
|
||||
|
||||
row_layout->addWidget(info_widget, 1);
|
||||
|
||||
table_widget_->setCellWidget(static_cast<int>(i), 0, row_widget);
|
||||
|
||||
// Auto-resize row height to fit content
|
||||
table_widget_->resizeRowToContents(static_cast<int>(i));
|
||||
|
||||
// Store widgets for later updates
|
||||
entry_widgets_.push_back({icon_label, name_label, path_label, type_label,
|
||||
status_label, progress_bar});
|
||||
}
|
||||
|
||||
main_layout->addWidget(table_widget_);
|
||||
|
||||
// Button layout
|
||||
auto* button_layout = new QHBoxLayout();
|
||||
|
||||
// Cancel button
|
||||
cancel_button_ = new QPushButton("Cancel", this);
|
||||
connect(cancel_button_, &QPushButton::clicked, this,
|
||||
&ContentInstallDialogQt::OnCancelClicked);
|
||||
button_layout->addWidget(cancel_button_);
|
||||
|
||||
button_layout->addStretch();
|
||||
|
||||
// Close button
|
||||
close_button_ = new QPushButton("Close", this);
|
||||
close_button_->setEnabled(false);
|
||||
connect(close_button_, &QPushButton::clicked, this,
|
||||
&ContentInstallDialogQt::OnCloseClicked);
|
||||
button_layout->addWidget(close_button_);
|
||||
|
||||
main_layout->addLayout(button_layout);
|
||||
|
||||
setMinimumSize(600, 400);
|
||||
|
||||
// Initial update
|
||||
UpdateProgress();
|
||||
}
|
||||
|
||||
void ContentInstallDialogQt::UpdateProgress() {
|
||||
bool is_everything_installed = true;
|
||||
bool has_cancelled_entries = false;
|
||||
bool cancellation_complete = false;
|
||||
|
||||
for (size_t i = 0; i < installation_entries_->size(); i++) {
|
||||
auto& entry = (*installation_entries_)[i];
|
||||
auto& widgets = entry_widgets_[i];
|
||||
|
||||
// Check if this entry was cancelled and has finished
|
||||
if (entry.cancelled_.load() &&
|
||||
entry.installation_state_ == xe::Emulator::InstallState::failed &&
|
||||
entry.installation_result_ == X_ERROR_CANCELLED) {
|
||||
has_cancelled_entries = true;
|
||||
}
|
||||
|
||||
// Update name
|
||||
widgets.name_label->setText(
|
||||
QString("Name: %1").arg(QString::fromStdString(entry.name_)));
|
||||
|
||||
// Update path with link
|
||||
QString path_str =
|
||||
QString::fromStdString(xe::path_to_utf8(entry.data_installation_path_));
|
||||
widgets.path_label->setText(
|
||||
QString("Installation Path: <a href=\"%1\">%1</a>").arg(path_str));
|
||||
|
||||
// Update content type
|
||||
if (entry.content_type_ != xe::XContentType::kInvalid) {
|
||||
auto it = XContentTypeMap.find(entry.content_type_);
|
||||
if (it != XContentTypeMap.end()) {
|
||||
widgets.type_label->setText(
|
||||
QString("Content Type: %1")
|
||||
.arg(QString::fromStdString(it->second)));
|
||||
}
|
||||
} else {
|
||||
widgets.type_label->setText("");
|
||||
}
|
||||
|
||||
// Update status
|
||||
std::string result = fmt::format(
|
||||
"Status: {}", xe::Emulator::installStateStringName[static_cast<uint8_t>(
|
||||
entry.installation_state_)]);
|
||||
|
||||
if (entry.installation_state_ == xe::Emulator::InstallState::failed) {
|
||||
result += fmt::format(" - {} ({:08X})",
|
||||
entry.installation_error_message_.c_str(),
|
||||
entry.installation_result_);
|
||||
}
|
||||
|
||||
widgets.status_label->setText(QString::fromStdString(result));
|
||||
|
||||
// Update progress bar
|
||||
if (entry.content_size_ > 0) {
|
||||
int progress = static_cast<int>(
|
||||
(static_cast<double>(entry.currently_installed_size_) /
|
||||
entry.content_size_) *
|
||||
100.0);
|
||||
widgets.progress_bar->setValue(progress);
|
||||
|
||||
if (entry.currently_installed_size_ != entry.content_size_ &&
|
||||
entry.installation_result_ == X_ERROR_SUCCESS) {
|
||||
is_everything_installed = false;
|
||||
}
|
||||
} else {
|
||||
widgets.progress_bar->setValue(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if cancellation is complete (all cancelled entries are now failed)
|
||||
if (has_cancelled_entries && !cancel_button_->isEnabled()) {
|
||||
bool all_cancelled_complete = true;
|
||||
for (const auto& entry : *installation_entries_) {
|
||||
if (entry.cancelled_.load()) {
|
||||
if (entry.installation_state_ != xe::Emulator::InstallState::failed) {
|
||||
all_cancelled_complete = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
cancellation_complete = all_cancelled_complete;
|
||||
}
|
||||
|
||||
// Show cancellation dialog and remove cancelled entries
|
||||
if (cancellation_complete && !cancellation_dialog_shown_) {
|
||||
cancellation_dialog_shown_ = true;
|
||||
|
||||
// Count cancelled entries
|
||||
int cancelled_count = 0;
|
||||
for (const auto& entry : *installation_entries_) {
|
||||
if (entry.cancelled_.load() &&
|
||||
entry.installation_result_ == X_ERROR_CANCELLED) {
|
||||
cancelled_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Show dialog
|
||||
QMessageBox::information(
|
||||
this, "Installation Cancelled",
|
||||
QString("Cancelled %1 installation(s) and cleaned up partial files.")
|
||||
.arg(cancelled_count));
|
||||
|
||||
// Remove cancelled entries from the vector and UI
|
||||
for (int i = static_cast<int>(installation_entries_->size()) - 1; i >= 0;
|
||||
i--) {
|
||||
auto& entry = (*installation_entries_)[i];
|
||||
if (entry.cancelled_.load() &&
|
||||
entry.installation_result_ == X_ERROR_CANCELLED) {
|
||||
// Remove from table
|
||||
table_widget_->removeRow(i);
|
||||
// Remove from entry_widgets_
|
||||
entry_widgets_.erase(entry_widgets_.begin() + i);
|
||||
// Remove from installation_entries_
|
||||
installation_entries_->erase(installation_entries_->begin() + i);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset cancel button
|
||||
cancel_button_->setText("Cancel");
|
||||
cancel_button_->setEnabled(true);
|
||||
cancellation_dialog_shown_ = false; // Reset for future cancellations
|
||||
}
|
||||
|
||||
// Enable close button when everything is done, disable cancel
|
||||
close_button_->setEnabled(is_everything_installed);
|
||||
if (!cancellation_complete) {
|
||||
cancel_button_->setEnabled(!is_everything_installed);
|
||||
}
|
||||
|
||||
// Show success dialog when all installations complete (detect transition)
|
||||
if (is_everything_installed && !was_everything_installed_ &&
|
||||
!success_dialog_shown_ && !installation_entries_->empty()) {
|
||||
success_dialog_shown_ = true;
|
||||
|
||||
// Count successful installations
|
||||
int success_count = 0;
|
||||
for (const auto& entry : *installation_entries_) {
|
||||
if (entry.installation_state_ == Emulator::InstallState::installed) {
|
||||
success_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Show dialog immediately on completion
|
||||
if (success_count > 0) {
|
||||
QMessageBox::information(
|
||||
this, "Installation Complete",
|
||||
QString("Successfully installed %1 package(s)!").arg(success_count));
|
||||
}
|
||||
}
|
||||
|
||||
// Track previous state for next update
|
||||
was_everything_installed_ = is_everything_installed;
|
||||
|
||||
// Auto-close if everything is done and user closed the dialog
|
||||
if (is_everything_installed && !isVisible()) {
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
bool ContentInstallDialogQt::IsEverythingInstalled() {
|
||||
for (const auto& entry : *installation_entries_) {
|
||||
if (entry.content_size_ > 0) {
|
||||
if (entry.currently_installed_size_ != entry.content_size_ &&
|
||||
entry.installation_result_ == X_ERROR_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ContentInstallDialogQt::OnCloseClicked() {
|
||||
if (IsEverythingInstalled()) {
|
||||
accept();
|
||||
}
|
||||
}
|
||||
|
||||
void ContentInstallDialogQt::OnCancelClicked() {
|
||||
// Set the cancelled flag for all entries that are still in progress
|
||||
for (auto& entry : *installation_entries_) {
|
||||
if (entry.installation_state_ == Emulator::InstallState::preparing ||
|
||||
entry.installation_state_ == Emulator::InstallState::pending ||
|
||||
entry.installation_state_ == Emulator::InstallState::installing) {
|
||||
entry.cancelled_.store(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable the cancel button
|
||||
cancel_button_->setEnabled(false);
|
||||
cancel_button_->setText("Cancelling...");
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2025 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_UI_CONTENT_INSTALL_DIALOG_QT_H_
|
||||
#define XENIA_UI_CONTENT_INSTALL_DIALOG_QT_H_
|
||||
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidget>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/emulator.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
class ContentInstallDialogQt : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ContentInstallDialogQt(
|
||||
QWidget* parent, const std::filesystem::path& content_root,
|
||||
std::shared_ptr<std::vector<Emulator::ContentInstallEntry>> entries);
|
||||
~ContentInstallDialogQt() override;
|
||||
|
||||
private slots:
|
||||
void UpdateProgress();
|
||||
void OnCloseClicked();
|
||||
void OnCancelClicked();
|
||||
|
||||
private:
|
||||
void SetupUI();
|
||||
bool IsEverythingInstalled();
|
||||
|
||||
std::filesystem::path content_root_;
|
||||
std::shared_ptr<std::vector<Emulator::ContentInstallEntry>>
|
||||
installation_entries_;
|
||||
|
||||
QTableWidget* table_widget_;
|
||||
QPushButton* close_button_;
|
||||
QPushButton* cancel_button_;
|
||||
QTimer* update_timer_;
|
||||
bool cancellation_dialog_shown_ = false;
|
||||
bool success_dialog_shown_ = false;
|
||||
bool was_everything_installed_ = false;
|
||||
|
||||
struct EntryWidgets {
|
||||
QLabel* icon_label;
|
||||
QLabel* name_label;
|
||||
QLabel* path_label;
|
||||
QLabel* type_label;
|
||||
QLabel* status_label;
|
||||
QProgressBar* progress_bar;
|
||||
};
|
||||
|
||||
std::vector<EntryWidgets> entry_widgets_;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_UI_CONTENT_INSTALL_DIALOG_QT_H_
|
||||
@@ -0,0 +1,111 @@
|
||||
/****************************************************************************
|
||||
** Meta object code from reading C++ file 'content_install_dialog_qt.h'
|
||||
**
|
||||
** Created by: The Qt Meta Object Compiler version 69 (Qt 6.9.2)
|
||||
**
|
||||
** WARNING! All changes made in this file will be lost!
|
||||
*****************************************************************************/
|
||||
|
||||
#include <QtCore/qmetatype.h>
|
||||
#include "content_install_dialog_qt.h"
|
||||
|
||||
#include <QtCore/qtmochelpers.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QtCore/qxptype_traits.h>
|
||||
#if !defined(Q_MOC_OUTPUT_REVISION)
|
||||
#error \
|
||||
"The header file 'content_install_dialog_qt.h' doesn't include <QObject>."
|
||||
#elif Q_MOC_OUTPUT_REVISION != 69
|
||||
#error "This file was generated using the moc from 6.9.2. It"
|
||||
#error "cannot be used with the include files from this version of Qt."
|
||||
#error "(The moc has changed too much.)"
|
||||
#endif
|
||||
|
||||
#ifndef Q_CONSTINIT
|
||||
#define Q_CONSTINIT
|
||||
#endif
|
||||
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
QT_WARNING_DISABLE_GCC("-Wuseless-cast")
|
||||
namespace {
|
||||
struct qt_meta_tag_ZN2xe2ui22ContentInstallDialogQtE_t {};
|
||||
} // unnamed namespace
|
||||
|
||||
template <>
|
||||
constexpr inline auto xe::ui::ContentInstallDialogQt::qt_create_metaobjectdata<
|
||||
qt_meta_tag_ZN2xe2ui22ContentInstallDialogQtE_t>() {
|
||||
namespace QMC = QtMocConstants;
|
||||
QtMocHelpers::StringRefStorage qt_stringData{
|
||||
"xe::ui::ContentInstallDialogQt", "UpdateProgress", "", "OnCloseClicked"};
|
||||
|
||||
QtMocHelpers::UintData qt_methods{
|
||||
// Slot 'UpdateProgress'
|
||||
QtMocHelpers::SlotData<void()>(1, 2, QMC::AccessPrivate, QMetaType::Void),
|
||||
// Slot 'OnCloseClicked'
|
||||
QtMocHelpers::SlotData<void()>(3, 2, QMC::AccessPrivate, QMetaType::Void),
|
||||
};
|
||||
QtMocHelpers::UintData qt_properties{};
|
||||
QtMocHelpers::UintData qt_enums{};
|
||||
return QtMocHelpers::metaObjectData<
|
||||
ContentInstallDialogQt, qt_meta_tag_ZN2xe2ui22ContentInstallDialogQtE_t>(
|
||||
QMC::MetaObjectFlag{}, qt_stringData, qt_methods, qt_properties,
|
||||
qt_enums);
|
||||
}
|
||||
Q_CONSTINIT const QMetaObject xe::ui::ContentInstallDialogQt::staticMetaObject = { {
|
||||
QMetaObject::SuperData::link<QDialog::staticMetaObject>(),
|
||||
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe2ui22ContentInstallDialogQtE_t>.stringdata,
|
||||
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe2ui22ContentInstallDialogQtE_t>.data,
|
||||
qt_static_metacall,
|
||||
nullptr,
|
||||
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN2xe2ui22ContentInstallDialogQtE_t>.metaTypes,
|
||||
nullptr
|
||||
} };
|
||||
|
||||
void xe::ui::ContentInstallDialogQt::qt_static_metacall(QObject* _o,
|
||||
QMetaObject::Call _c,
|
||||
int _id, void** _a) {
|
||||
auto* _t = static_cast<ContentInstallDialogQt*>(_o);
|
||||
if (_c == QMetaObject::InvokeMetaMethod) {
|
||||
switch (_id) {
|
||||
case 0:
|
||||
_t->UpdateProgress();
|
||||
break;
|
||||
case 1:
|
||||
_t->OnCloseClicked();
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
}
|
||||
(void)_a;
|
||||
}
|
||||
|
||||
const QMetaObject* xe::ui::ContentInstallDialogQt::metaObject() const {
|
||||
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject()
|
||||
: &staticMetaObject;
|
||||
}
|
||||
|
||||
void* xe::ui::ContentInstallDialogQt::qt_metacast(const char* _clname) {
|
||||
if (!_clname) return nullptr;
|
||||
if (!strcmp(_clname, qt_staticMetaObjectStaticContent<qt_meta_tag_ZN2xe2ui22ContentInstallDialogQtE_t>.strings))
|
||||
return static_cast<void*>(this);
|
||||
return QDialog::qt_metacast(_clname);
|
||||
}
|
||||
|
||||
int xe::ui::ContentInstallDialogQt::qt_metacall(QMetaObject::Call _c, int _id,
|
||||
void** _a) {
|
||||
_id = QDialog::qt_metacall(_c, _id, _a);
|
||||
if (_id < 0) return _id;
|
||||
if (_c == QMetaObject::InvokeMetaMethod) {
|
||||
if (_id < 2) qt_static_metacall(this, _c, _id, _a);
|
||||
_id -= 2;
|
||||
}
|
||||
if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
|
||||
if (_id < 2) *reinterpret_cast<QMetaType*>(_a[0]) = QMetaType();
|
||||
_id -= 2;
|
||||
}
|
||||
return _id;
|
||||
}
|
||||
QT_WARNING_POP
|
||||
Reference in New Issue
Block a user