diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2aba05d6b..3c33f8d8c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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 }}' diff --git a/premake5.lua b/premake5.lua index 972867ac9..5903ad1c7 100644 --- a/premake5.lua +++ b/premake5.lua @@ -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", diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index e03d22e8d..667716320 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -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(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( - 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(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(window_.get()); + if (qt_window) { + auto* dialog = new ui::ContentInstallDialogQt(qt_window->qwindow(), + emulator_->content_root(), + content_installation_status); + dialog->show(); + } } void EmulatorWindow::ExtractZarchive() { diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index 4c901ae56..fd8c426c8 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -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> 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> - installation_entries_; - }; - class DisplayConfigDialog final : public ui::ImGuiDialog { public: DisplayConfigDialog(ui::ImGuiDrawer* imgui_drawer, diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 73a42f368..02eb69dfa 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -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(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 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; diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index c98440daf..2b0f8d85b 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -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 icon_; + std::vector icon_data_; // Raw PNG data for Qt dialog + std::atomic cancelled_{false}; // Flag to cancel installation }; // Migrates data from content to content/xuid with respect to common data. diff --git a/src/xenia/ui/content_install_dialog_qt.cc b/src/xenia/ui/content_install_dialog_qt.cc new file mode 100644 index 000000000..0c4be44a4 --- /dev/null +++ b/src/xenia/ui/content_install_dialog_qt.cc @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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> 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(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(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(i), 0, row_widget); + + // Auto-resize row height to fit content + table_widget_->resizeRowToContents(static_cast(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: %1").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( + 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( + (static_cast(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(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 diff --git a/src/xenia/ui/content_install_dialog_qt.h b/src/xenia/ui/content_install_dialog_qt.h new file mode 100644 index 000000000..33effa0df --- /dev/null +++ b/src/xenia/ui/content_install_dialog_qt.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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> entries); + ~ContentInstallDialogQt() override; + + private slots: + void UpdateProgress(); + void OnCloseClicked(); + void OnCancelClicked(); + + private: + void SetupUI(); + bool IsEverythingInstalled(); + + std::filesystem::path content_root_; + std::shared_ptr> + 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 entry_widgets_; +}; + +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_CONTENT_INSTALL_DIALOG_QT_H_ diff --git a/src/xenia/ui/moc_content_install_dialog_qt.cc b/src/xenia/ui/moc_content_install_dialog_qt.cc new file mode 100644 index 000000000..78f4a16e5 --- /dev/null +++ b/src/xenia/ui/moc_content_install_dialog_qt.cc @@ -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 +#include "content_install_dialog_qt.h" + +#include + +#include + +#include +#if !defined(Q_MOC_OUTPUT_REVISION) +#error \ + "The header file 'content_install_dialog_qt.h' doesn't include ." +#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(1, 2, QMC::AccessPrivate, QMetaType::Void), + // Slot 'OnCloseClicked' + QtMocHelpers::SlotData(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(), + qt_staticMetaObjectStaticContent.stringdata, + qt_staticMetaObjectStaticContent.data, + qt_static_metacall, + nullptr, + qt_staticMetaObjectRelocatingContent.metaTypes, + nullptr +} }; + +void xe::ui::ContentInstallDialogQt::qt_static_metacall(QObject* _o, + QMetaObject::Call _c, + int _id, void** _a) { + auto* _t = static_cast(_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.strings)) + return static_cast(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(_a[0]) = QMetaType(); + _id -= 2; + } + return _id; +} +QT_WARNING_POP