diff --git a/SerialPrograms/Source/CommonFramework/ProgramSession.cpp b/SerialPrograms/Source/CommonFramework/ProgramSession.cpp index 1f6d0ad4c0..52ff2861f4 100644 --- a/SerialPrograms/Source/CommonFramework/ProgramSession.cpp +++ b/SerialPrograms/Source/CommonFramework/ProgramSession.cpp @@ -34,6 +34,7 @@ ProgramSession::ProgramSession(const ProgramDescriptor& descriptor) , m_logger(global_logger_raw(), "Program") , m_timestamp(current_time()) , m_state(ProgramState::STOPPED) + , m_download_manager(descriptor.required_resources()) { load_historical_stats(); } diff --git a/SerialPrograms/Source/CommonFramework/ProgramSession.h b/SerialPrograms/Source/CommonFramework/ProgramSession.h index feb0013952..128f1afa2f 100644 --- a/SerialPrograms/Source/CommonFramework/ProgramSession.h +++ b/SerialPrograms/Source/CommonFramework/ProgramSession.h @@ -22,6 +22,7 @@ #include "Common/Cpp/ListenerSet.h" #include "Common/Cpp/Concurrency/Mutex.h" #include "Common/Cpp/Concurrency/AsyncTask.h" +#include "CommonFramework/ResourceDownload/RequiredDownloadManager.h" #include "CommonFramework/Globals.h" //#include "CommonFramework/Logging/Logger.h" #include "Integrations/ProgramTrackerInterfaces.h" @@ -59,6 +60,7 @@ class ProgramSession : public TrackableProgram{ const ProgramDescriptor& descriptor() const{ return m_descriptor; } uint64_t instance_id() const{ return m_instance_id; } Logger& logger(){ return m_logger; } + RequiredDownloadManager& get_download_manager(){ return m_download_manager; } public: @@ -140,6 +142,8 @@ class ProgramSession : public TrackableProgram{ // CancellableScope* m_scope = nullptr; ListenerSet m_listeners; + + RequiredDownloadManager m_download_manager; }; diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.cpp b/SerialPrograms/Source/CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.cpp new file mode 100644 index 0000000000..c911819fdc --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.cpp @@ -0,0 +1,235 @@ +/* Required Download Dialog Widget + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +// #include +// #include +// #include +// #include +// #include +// #include "CommonFramework/Logging/Logger.h" +#include "Common/Cpp/Exceptions.h" + +// #include "CommonFramework/Notifications/ProgramNotifications.h" +#include "RequiredDownloadDialogWidget.h" + +#include +using std::cout; +using std::endl; + +namespace PokemonAutomation{ + + +//////////////////////////////////////////// +// RequiredDownloadWidget +//////////////////////////////////////////// + +RequiredDownloadWidget::~RequiredDownloadWidget(){ + m_value->remove_listener(*this); +} + +RequiredDownloadWidget::RequiredDownloadWidget(QWidget& parent, std::shared_ptr download_ptr) + : QWidget(&parent) + , m_value(download_ptr) +{ + + QHBoxLayout* mainLayout = new QHBoxLayout(this); + // Create a label for the specific task + m_resource_name = new QLabel(QString::fromStdString(download_ptr->get_name()), this); + m_resource_name->setFixedWidth(300); + mainLayout->addWidget(m_resource_name); + + // Create a label for the status + m_status_label = new QLabel("", this); + m_status_label->setFixedWidth(70); + mainLayout->addWidget(m_status_label); + + // Create the progress bar + m_progress_bar = new QProgressBar(this); + m_progress_bar->setRange(0, 100); + m_progress_bar->setValue(0); + m_progress_bar->setFixedWidth(100); + mainLayout->addWidget(m_progress_bar); + + download_ptr->add_listener(*this); +} + + +void RequiredDownloadWidget::update_progress_bar(uint64_t bytes_done, uint64_t total_bytes, const std::string& text){ + double percent = total_bytes > 0 ? (static_cast(bytes_done) / total_bytes) * 100.0 : 0; + int current_percent = static_cast(percent); + int last_percentage = m_progress_bar->value(); + // Only update UI if integer value has changed + if (current_percent == last_percentage){ + return; + } + + // current_percent has changed. update the progress bar + m_status_label->setText(QString::fromStdString(text)); + m_progress_bar->setValue(current_percent); +} + +void RequiredDownloadWidget::on_download_progress(uint64_t bytes_done, uint64_t total_bytes){ + QMetaObject::invokeMethod(this, [this, bytes_done, total_bytes]{ + update_progress_bar(bytes_done, total_bytes, "Downloading"); + }, Qt::QueuedConnection); +} +void RequiredDownloadWidget::on_unzip_progress(uint64_t bytes_done, uint64_t total_bytes){ + QMetaObject::invokeMethod(this, [this, bytes_done, total_bytes]{ + update_progress_bar(bytes_done, total_bytes, "Unzipping"); + }, Qt::QueuedConnection); +} +void RequiredDownloadWidget::on_hash_progress(uint64_t bytes_done, uint64_t total_bytes){ + QMetaObject::invokeMethod(this, [this, bytes_done, total_bytes]{ + update_progress_bar(bytes_done, total_bytes, "Verifying"); + }, Qt::QueuedConnection); +} + +void RequiredDownloadWidget::on_download_failed(){ + std::cerr << "Error: Download failed. Check your internet connection and check you have enough disk space." << std::endl; + QMetaObject::invokeMethod(this, []{ + QMessageBox box; + box.critical(nullptr, "Error", + QString::fromStdString("Error: Download failed. Check your internet connection and check you have enough disk space.")); + }); +} + +//////////////////////////////////////////// +// RequiredDownloadDialogWidget +//////////////////////////////////////////// + +RequiredDownloadDialogWidget::~RequiredDownloadDialogWidget(){ + m_download_manager.remove_download_listener(*this); +} +RequiredDownloadDialogWidget::RequiredDownloadDialogWidget(QWidget& parent, RequiredDownloadManager& download_manager) + : QDialog (&parent) + , m_download_manager(download_manager) +{ + setWindowTitle("Task Progress"); + resize(400, 50 * (int)download_manager.get_required_downloads().size() + 50); // Dynamically scale height + + QVBoxLayout* mainLayout = new QVBoxLayout(this); + + for (std::shared_ptr download_ptr : download_manager.get_required_downloads()){ + // cout << download_ptr->get_name() << endl; + + RequiredDownloadWidget* download_layout = new RequiredDownloadWidget(*this, download_ptr); + download_ptr->start_download(); + + mainLayout->addWidget(download_layout); + + // Add a small spacing gap between tasks + mainLayout->addSpacing(0); + } + + // 2. Create standard Dialog Buttons (OK and Cancel) + QDialogButtonBox* buttonBox = new QDialogButtonBox( + QDialogButtonBox::Cancel, + this + ); + + // 3. Connect the buttons to the default QDialog slots + // Clicking OK calls accept(), clicking Cancel calls reject() + // connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + // 4. Add the buttons to the bottom of the layout + mainLayout->addWidget(buttonBox); + setLayout(mainLayout); + + download_manager.add_download_listener(*this); +} + + +void RequiredDownloadDialogWidget::on_all_downloads_finished(){ + cout << "All downloads finished. Start the program." << endl; + QMetaObject::invokeMethod(this, [this](){ + this->accept(); + }); +} +void RequiredDownloadDialogWidget::on_download_failed(){ + std::cerr << "Error: Download failed. Check your internet connection and check you have enough disk space." << std::endl; + QMetaObject::invokeMethod(this, [this]{ + this->reject(); + QMessageBox box; + box.critical(nullptr, "Error", + QString::fromStdString("Error: Download failed. Check your internet connection and check you have enough disk space.")); + }); + +} +void RequiredDownloadDialogWidget::on_exception_caught(const std::string& function_name){ + std::cerr << "Error: Exception thrown in thread. From " + function_name + ". Report this as a bug." << std::endl; + QMetaObject::invokeMethod(this, [this, function_name]{ + this->reject(); + + QMessageBox box; + box.critical(nullptr, "Error", + QString::fromStdString("Error: Exception thrown in thread. From " + function_name + ". Report this as a bug.")); + }); +} + +///////////////////////// +// Non member functions +///////////////////////// + + +bool show_download_prereqs_popup( + QWidget* parent, + RequiredDownloadManager& download_manager, + std::function error_callback +){ + + try{ + + // RequiredDownloadManager& download_manager = m_session.get_download_manager(); + // re-initialize the required downloads, even if already initialized + // this refreshes the download list, which may have changed since the last run + download_manager.initialize_required_downloads(); + + if (download_manager.get_upgrade_warning()){ + std::string warning_string = + "The program is expecting an older version of a resource than is available. " + "This likely means that your version of Computer Control is out of date. " + "We recommend that you upgrade the Computer Control program."; + error_callback(warning_string); + // cout << warning_string << endl; + } + if (download_manager.get_required_downloads().empty()){ + cout << "required_download_list is empty. Start the program." << endl; + + return true; + } + + RequiredDownloadDialogWidget box{*parent, download_manager}; + + // box.open(); + if (box.exec() == QDialog::Accepted){ + cout << "Pre-req downloads done" << endl; + return true; + }else{ + cout << "Pre-req downloads NOT done." << endl; + download_manager.cancel_downloads(); + return false; + } + + }catch(InternalProgramError& e){ + error_callback(e.message()); + }catch (const std::exception& e) { + std::string message = std::string(e.what()) + "Report this as an error."; + error_callback(message); + }catch(...){ + error_callback("show_download_prereqs_popup: Unknown exception caught. Report this as an error."); + } + + return false; + +} + + + + +} diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.h new file mode 100644 index 0000000000..7ce7290398 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.h @@ -0,0 +1,79 @@ +/* Required Download Dialog Widget + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_RequiredDownloadDialogWidget_H +#define PokemonAutomation_RequiredDownloadDialogWidget_H + +#include +#include +#include +#include "RequiredDownloadManager.h" +#include "CommonFramework/ResourceDownload/RequiredDownload.h" + + +namespace PokemonAutomation{ + + +class RequiredDownloadWidget : public QWidget, public RequiredDownload::Listener { +public: + ~RequiredDownloadWidget(); + RequiredDownloadWidget(QWidget& parent, std::shared_ptr download_ptr); + +public: + virtual void on_download_progress(uint64_t bytes_done, uint64_t total_bytes) override; + virtual void on_unzip_progress(uint64_t bytes_done, uint64_t total_bytes) override; + virtual void on_hash_progress(uint64_t bytes_done, uint64_t total_bytes) override; + + virtual void on_download_failed() override; + + + void update_progress_bar(uint64_t bytes_done, uint64_t total_bytes, const std::string& text); + +private: + std::shared_ptr m_value; + QLabel* m_resource_name; + QLabel* m_status_label; + QProgressBar* m_progress_bar; + +}; + +class RequiredDownloadDialogWidget : public QDialog, public RequiredDownloadManager::DownloadListener { +public: + +public: + ~RequiredDownloadDialogWidget(); + RequiredDownloadDialogWidget(QWidget& parent, RequiredDownloadManager& download_manager); + +public: + virtual void on_all_downloads_finished() override; + virtual void on_download_failed() override; + virtual void on_exception_caught(const std::string& function_name) override; + + // virtual void on_action_state_updated() override; + +private: + // void update_UI_state(); + // void update_progress_bar(int percentage, const std::string& text); + // void update_progress_bar(uint64_t bytes_done, uint64_t total_bytes, const std::string& text); +private: + RequiredDownloadManager& m_download_manager; + +}; + +// Opens a pop-up box that downloads all the pre-requisite resources +// that haven't yet been downloaded. +// return true if all pre-requisite resources are downloaded +bool show_download_prereqs_popup( + QWidget* parent, + RequiredDownloadManager& download_manager, + std::function error_callback +); + + + + +} +#endif diff --git a/SerialPrograms/Source/ComputerPrograms/Framework/ComputerProgramWidget.cpp b/SerialPrograms/Source/ComputerPrograms/Framework/ComputerProgramWidget.cpp index 4622e3f3a4..0eeaac8278 100644 --- a/SerialPrograms/Source/ComputerPrograms/Framework/ComputerProgramWidget.cpp +++ b/SerialPrograms/Source/ComputerPrograms/Framework/ComputerProgramWidget.cpp @@ -13,10 +13,15 @@ #include "CommonFramework/Panels/PanelTools.h" #include "CommonFramework/Panels/UI/PanelElements.h" #include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.h" #include "ComputerPrograms/ComputerProgram.h" #include "ComputerPrograms/Framework/ComputerProgramOption.h" #include "ComputerProgramWidget.h" +// #include +// using std::cout; +// using std::endl; + namespace PokemonAutomation{ @@ -77,9 +82,18 @@ ComputerProgramWidget::ComputerProgramWidget( this, [&](ProgramState state){ std::string error; switch (state){ - case ProgramState::STOPPED: - error = m_session.start_program(); + case ProgramState::STOPPED:{ + bool prereqs_downloaded = show_download_prereqs_popup(this, m_session.get_download_manager(), + [this](const std::string& msg) { + this->error(msg); + } + ); + + if (prereqs_downloaded){ + error = m_session.start_program(); + } break; + } case ProgramState::RUNNING: error = m_session.stop_program(); break; diff --git a/SerialPrograms/Source/NintendoSwitch/Framework/UI/NintendoSwitch_MultiSwitchProgramWidget.cpp b/SerialPrograms/Source/NintendoSwitch/Framework/UI/NintendoSwitch_MultiSwitchProgramWidget.cpp index 9100ac78c3..a8bbc9c71e 100644 --- a/SerialPrograms/Source/NintendoSwitch/Framework/UI/NintendoSwitch_MultiSwitchProgramWidget.cpp +++ b/SerialPrograms/Source/NintendoSwitch/Framework/UI/NintendoSwitch_MultiSwitchProgramWidget.cpp @@ -16,6 +16,7 @@ #include "CommonFramework/Panels/PanelTools.h" #include "CommonFramework/Panels/UI/PanelElements.h" #include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.h" #include "NintendoSwitch/Framework/NintendoSwitch_MultiSwitchProgramOption.h" #include "NintendoSwitch/Framework/NintendoSwitch_MultiSwitchProgramSession.h" #include "NintendoSwitch_MultiSwitchProgramWidget.h" @@ -108,9 +109,18 @@ MultiSwitchProgramWidget2::MultiSwitchProgramWidget2( this, [&](ProgramState state){ std::string error; switch (state){ - case ProgramState::STOPPED: - error = m_session.start_program(); + case ProgramState::STOPPED:{ + bool prereqs_downloaded = show_download_prereqs_popup(this, m_session.get_download_manager(), + [this](const std::string& msg) { + this->error(msg); + } + ); + + if (prereqs_downloaded){ + error = m_session.start_program(); + } break; + } case ProgramState::RUNNING: error = m_session.stop_program(); break; diff --git a/SerialPrograms/Source/NintendoSwitch/Framework/UI/NintendoSwitch_SingleSwitchProgramWidget.cpp b/SerialPrograms/Source/NintendoSwitch/Framework/UI/NintendoSwitch_SingleSwitchProgramWidget.cpp index 3d5dabf931..c5cc37797c 100644 --- a/SerialPrograms/Source/NintendoSwitch/Framework/UI/NintendoSwitch_SingleSwitchProgramWidget.cpp +++ b/SerialPrograms/Source/NintendoSwitch/Framework/UI/NintendoSwitch_SingleSwitchProgramWidget.cpp @@ -15,6 +15,7 @@ #include "CommonFramework/Panels/PanelTools.h" #include "CommonFramework/Panels/UI/PanelElements.h" #include "CommonFramework/ProgramStats/StatsTracking.h" +#include "CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.h" #include "NintendoSwitch/Framework/NintendoSwitch_SingleSwitchProgramOption.h" #include "NintendoSwitch_SingleSwitchProgramWidget.h" @@ -111,9 +112,18 @@ SingleSwitchProgramWidget2::SingleSwitchProgramWidget2( this, [&](ProgramState state){ std::string error; switch (state){ - case ProgramState::STOPPED: - error = m_session.start_program(); + case ProgramState::STOPPED:{ + bool prereqs_downloaded = show_download_prereqs_popup(this, m_session.get_download_manager(), + [this](const std::string& msg) { + this->error(msg); + } + ); + + if (prereqs_downloaded){ + error = m_session.start_program(); + } break; + } case ProgramState::RUNNING: error = m_session.stop_program(); break; diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 2fe9fcb596..35ba9e452a 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -494,6 +494,8 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/ResourceDownload/DownloadThread.h Source/CommonFramework/ResourceDownload/RequiredDownload.cpp Source/CommonFramework/ResourceDownload/RequiredDownload.h + Source/CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.cpp + Source/CommonFramework/ResourceDownload/RequiredDownloadDialogWidget.h Source/CommonFramework/ResourceDownload/RequiredDownloadManager.cpp Source/CommonFramework/ResourceDownload/RequiredDownloadManager.h Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.cpp