[UI] Implement Qt based file install dialog

This commit is contained in:
Herman S.
2025-10-08 08:40:42 +09:00
parent 0cd3a758a9
commit e522d05e87
9 changed files with 715 additions and 128 deletions
+30 -3
View File
@@ -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
View File
@@ -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",
+9 -92
View File
@@ -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() {
-28
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+376
View File
@@ -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
+73
View File
@@ -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