addon/client: Add modeless dialog for displaying add-on install progress

This implements an add-on extraction progress dialog that does not
actually run its own event loop, allowing the caller to take ownership
of it (display() static method) and update its state using a callback
function object. The object in question is passed into the add-ons
management API and used to update the dialog status each time an add-on
file is written to disk as part of the pack extraction process.

In order to avoid stalling the extraction process in UI code, the
callback is invoked for every single file, but the dialog's progress
update method places a time restriction on GUI2 API calls of 120
milliseconds -- this is a good throttle interval that allows add-ons to
be extracted in about the same amount of time as before while still
updating the progress bar smoothly enough for add-ons that take longer
than that.

(This is not the most trivial code to test, so it is suggested to add a
sleep/delay API call in unarchive_file() in src/addon/manager.cpp to
introduce artificial delays.)

One issue with this code, however, is that because modeless_dialog
doesn't execute its own event loop, the only way to get the dialog to be
updated is to force a draw event in ourselves via the new
gui2::dialogs::modeless_dialog::force_redraw() method. This is really a
side-effect of my design choice here to run the dialog in the middle of
a blocking operation instead of somewhere where events are being
processed normally. I'm not entirely sure if the draw events would be
pushed even in that case, however.

Closes #1101.
This commit is contained in:
Iris Morelle 2021-02-18 22:39:15 -03:00 committed by Pentarctagon
parent ad8f6a6f4c
commit 532ec4e06f
9 changed files with 282 additions and 7 deletions

View file

@ -0,0 +1,105 @@
#textdomain wesnoth-lib
###
### Modeless dialog that tracks progress of a file operation
###
### NOTE: The dialog layout is intended to match network_transmission's since
### they are both used during the add-on download/install flow.
###
[window]
id = "file_progress"
description = "Modeless dialog that tracks progress of a file operation"
[resolution]
definition = "default"
maximum_width = 800
[tooltip]
id = "tooltip"
[/tooltip]
[helptip]
id = "tooltip"
[/helptip]
[grid]
[row]
[column]
border = "all"
border_size = 5
horizontal_alignment = "left"
[label]
id = "title"
definition = "title"
[/label]
[/column]
[/row]
[row]
[column]
border = "all"
border_size = 5
horizontal_alignment = "left"
[label]
id = "message"
definition = "default"
[/label]
[/column]
[/row]
[row]
[column]
grow_factor = 1
horizontal_grow = true
border = "all"
border_size = 5
[progress_bar]
id = "progress"
definition = "default"
[/progress_bar]
[/column]
[/row]
[row]
[column]
horizontal_alignment = "right"
grow_factor = 0
border = "all"
border_size = 5
[button]
# This button is only used for decoration and doesn't
# actually do anything, hence the nonstandard id.
id = "placeholder"
definition = "default"
label = _ "Cancel"
[/button]
[/column]
[/row]
[/grid]
[/resolution]
[/window]

View file

@ -2,6 +2,9 @@
###
### Dialog that tracks progress of a network transmission
###
### NOTE: The dialog layout is intended to match file_progress' since they are
### both used during the add-on download/install flow.
###
[window]
id = "network_transmission"

View file

@ -323,6 +323,8 @@
46F92DBA2174F6A300602C1C /* help_browser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 46F92C692174F6A300602C1C /* help_browser.cpp */; };
46F92DBB2174F6A300602C1C /* file_dialog.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 46F92C6B2174F6A300602C1C /* file_dialog.cpp */; };
46F92DBC2174F6A300602C1C /* file_dialog.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 46F92C6B2174F6A300602C1C /* file_dialog.cpp */; };
1234567890ABCDEF12345678 /* file_progress.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEF12345680 /* file_progress.cpp */; };
1234567890ABCDEF12345679 /* file_progress.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEF12345680 /* file_progress.cpp */; };
46F92DBD2174F6A300602C1C /* folder_create.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 46F92C6E2174F6A300602C1C /* folder_create.cpp */; };
46F92DBE2174F6A300602C1C /* folder_create.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 46F92C6E2174F6A300602C1C /* folder_create.cpp */; };
46F92DBF2174F6A300602C1C /* language_selection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 46F92C6F2174F6A300602C1C /* language_selection.cpp */; };
@ -1731,6 +1733,7 @@
46F92C692174F6A300602C1C /* help_browser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = help_browser.cpp; sourceTree = "<group>"; };
46F92C6A2174F6A300602C1C /* title_screen.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = title_screen.hpp; sourceTree = "<group>"; };
46F92C6B2174F6A300602C1C /* file_dialog.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = file_dialog.cpp; sourceTree = "<group>"; };
1234567890ABCDEF12345680 /* file_progress.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = file_progress.cpp; sourceTree = "<group>"; };
46F92C6C2174F6A300602C1C /* attack_predictions.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = attack_predictions.hpp; sourceTree = "<group>"; };
46F92C6D2174F6A300602C1C /* tooltip.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = tooltip.hpp; sourceTree = "<group>"; };
46F92C6E2174F6A300602C1C /* folder_create.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = folder_create.cpp; sourceTree = "<group>"; };
@ -1756,6 +1759,7 @@
46F92C822174F6A300602C1C /* unit_recruit.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = unit_recruit.cpp; sourceTree = "<group>"; };
46F92C832174F6A300602C1C /* edit_text.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = edit_text.cpp; sourceTree = "<group>"; };
46F92C842174F6A300602C1C /* file_dialog.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = file_dialog.hpp; sourceTree = "<group>"; };
1234567890ABCDEF12345681 /* file_progress.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = file_progress.hpp; sourceTree = "<group>"; };
46F92C852174F6A300602C1C /* attack_predictions.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = attack_predictions.cpp; sourceTree = "<group>"; };
46F92C862174F6A300602C1C /* tooltip.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = tooltip.cpp; sourceTree = "<group>"; };
46F92C872174F6A300602C1C /* title_screen.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = title_screen.cpp; sourceTree = "<group>"; };
@ -3623,6 +3627,8 @@
46F92C8D2174F6A300602C1C /* end_credits.hpp */,
46F92C6B2174F6A300602C1C /* file_dialog.cpp */,
46F92C842174F6A300602C1C /* file_dialog.hpp */,
1234567890ABCDEF12345680 /* file_progress.cpp */,
1234567890ABCDEF12345681 /* file_progress.hpp */,
46F92C6E2174F6A300602C1C /* folder_create.cpp */,
46F92C8A2174F6A300602C1C /* folder_create.hpp */,
46F92CBB2174F6A300602C1C /* formula_debugger.cpp */,
@ -5497,6 +5503,7 @@
B52EE8D6121359A600CFBDAB /* persist_context.cpp in Sources */,
B52EE8D7121359A600CFBDAB /* persist_manager.cpp in Sources */,
46F92DBB2174F6A300602C1C /* file_dialog.cpp in Sources */,
1234567890ABCDEF12345678 /* file_progress.cpp in Sources */,
46F92F0F2174FEC000602C1C /* standard_colors.cpp in Sources */,
46F92E852174F6A400602C1C /* debug.cpp in Sources */,
B52EE8D8121359A600CFBDAB /* persist_var.cpp in Sources */,
@ -6116,6 +6123,7 @@
46F92E142174F6A400602C1C /* player_list_helper.cpp in Sources */,
91E3570A1CACC9B200774252 /* playcampaign.cpp in Sources */,
46F92DBC2174F6A300602C1C /* file_dialog.cpp in Sources */,
1234567890ABCDEF12345679 /* file_progress.cpp in Sources */,
91E3570B1CACC9B200774252 /* singleplayer.cpp in Sources */,
4649B88B20288EEF00827CFB /* surface.cpp in Sources */,
46F92DB02174F6A300602C1C /* campaign_selection.cpp in Sources */,

View file

@ -193,6 +193,7 @@ gui/dialogs/editor/new_map.cpp
gui/dialogs/editor/resize_map.cpp
gui/dialogs/end_credits.cpp
gui/dialogs/file_dialog.cpp
gui/dialogs/file_progress.cpp
gui/dialogs/folder_create.cpp
gui/dialogs/formula_debugger.cpp
gui/dialogs/game_cache_options.cpp

View file

@ -25,6 +25,7 @@
#include "gettext.hpp"
#include "gui/dialogs/addon/addon_auth.hpp"
#include "gui/dialogs/addon/install_dependencies.hpp"
#include "gui/dialogs/file_progress.hpp"
#include "gui/dialogs/message.hpp"
#include "gui/widgets/retval.hpp"
#include "log.hpp"
@ -344,6 +345,11 @@ bool addons_client::install_addon(config& archive_cfg, const addon_info& info)
utils::string_map i18n_symbols;
i18n_symbols["addon_title"] = font::escape_text(info.title);
auto progress_dlg = gui2::dialogs::file_progress::display(_("Add-ons Manager"), VGETTEXT("Installing add-on <i>$addon_title</i>...", i18n_symbols));
auto progress_cb = [&progress_dlg](unsigned value) {
progress_dlg->update_progress(value);
};
if(archive_cfg.has_child("removelist") || archive_cfg.has_child("addlist")) {
LOG_ADDONS << "Received an updatepack for the addon '" << info.id << "'";
@ -366,7 +372,7 @@ bool addons_client::install_addon(config& archive_cfg, const addon_info& info)
if(entry.key == "removelist") {
purge_addon(entry.cfg);
} else if(entry.key == "addlist") {
unarchive_addon(entry.cfg);
unarchive_addon(entry.cfg, progress_cb);
}
}
@ -393,7 +399,7 @@ bool addons_client::install_addon(config& archive_cfg, const addon_info& info)
WRN_ADDONS << "failed to uninstall previous version of " << info.id << "; the add-on may not work properly!";
}
unarchive_addon(archive_cfg);
unarchive_addon(archive_cfg, progress_cb);
LOG_ADDONS << "unpacking finished";
}

View file

@ -293,7 +293,7 @@ static void unarchive_file(const std::string& path, const config& cfg)
filesystem::write_file(path + '/' + cfg["name"].str(), unencode_binary(cfg["contents"]));
}
static void unarchive_dir(const std::string& path, const config& cfg)
static void unarchive_dir(const std::string& path, const config& cfg, std::function<void()> file_callback = {})
{
std::string dir;
if (cfg["name"].empty())
@ -304,18 +304,36 @@ static void unarchive_dir(const std::string& path, const config& cfg)
filesystem::make_directory(dir);
for(const config &d : cfg.child_range("dir")) {
unarchive_dir(dir, d);
unarchive_dir(dir, d, file_callback);
}
for(const config &f : cfg.child_range("file")) {
unarchive_file(dir, f);
if(file_callback) {
file_callback();
}
}
}
void unarchive_addon(const config& cfg)
static unsigned count_pack_files(const config& cfg)
{
unsigned count = 0;
for(const config& d : cfg.child_range("dir")) {
count += count_pack_files(d);
}
return count + cfg.child_count("file");
}
void unarchive_addon(const config& cfg, std::function<void(unsigned)> progress_callback)
{
const std::string parentd = filesystem::get_addons_dir();
unarchive_dir(parentd, cfg);
unsigned file_count = progress_callback ? count_pack_files(cfg) : 0, done = 0;
auto file_callback = progress_callback
? [&]() { progress_callback(++done * 100.0 / file_count); }
: std::function<void()>{};
unarchive_dir(parentd, cfg, file_callback);
}
static void purge_dir(const std::string& path, const config& removelist)

View file

@ -33,6 +33,7 @@ class version_info;
#include "addon/validation.hpp"
#include <functional>
#include <string>
#include <vector>
#include <utility>
@ -153,7 +154,7 @@ bool is_addon_installed(const std::string& addon_name);
void archive_addon(const std::string& addon_name, class config& cfg);
/** Unarchives an add-on from campaignd's retrieved config object. */
void unarchive_addon(const class config& cfg);
void unarchive_addon(const class config& cfg, std::function<void(unsigned)> progress_callback = {});
/** Removes the listed files from the addon. */
void purge_addon(const config& removelist);

View file

@ -0,0 +1,80 @@
/*
Copyright (C) 2021 by Iris Morelle <shadowm@wesnoth.org>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "gui/dialogs/file_progress.hpp"
#include "gui/auxiliary/find_widget.hpp"
#include "gui/widgets/button.hpp"
#include "gui/dialogs/modal_dialog.hpp"
#include "gui/widgets/label.hpp"
#include "gui/widgets/progress_bar.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/window.hpp"
namespace gui2::dialogs {
REGISTER_WINDOW(file_progress)
const std::string& file_progress::window_id() const
{
static std::string wid = "file_progress";
return wid;
}
file_progress::file_progress(const std::string& title, const std::string& message)
: title_(title)
, message_(message)
, update_time_()
{
}
void file_progress::pre_show(window& window)
{
find_widget<label>(&window, "title", false).set_label(title_);
auto& message = find_widget<label>(&window, "message", false);
message.set_use_markup(true);
message.set_label(message_);
find_widget<button>(&window, "placeholder", false).set_active(false);
update_time_ = clock::now();
}
void file_progress::update_progress(unsigned value)
{
auto* window = get_window();
if(!window) {
return;
}
using std::chrono::duration_cast;
using std::chrono::milliseconds;
using namespace std::chrono_literals;
auto now = clock::now();
if(duration_cast<milliseconds>(now - update_time_) < 120ms) {
return;
}
find_widget<progress_bar>(window, "progress", false).set_percentage(value);
force_redraw();
update_time_ = now;
}
}

View file

@ -0,0 +1,53 @@
/*
Copyright (C) 2021 by Iris Morelle <shadowm@wesnoth.org>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "gui/dialogs/modeless_dialog.hpp"
#include <chrono>
namespace gui2::dialogs {
class file_progress : public modeless_dialog
{
public:
file_progress(const std::string& title, const std::string& message);
template<typename... T>
static auto display(T&&... args)
{
auto instance = std::make_unique<file_progress>(std::forward<T>(args)...);
instance->show(true);
return instance;
}
void update_progress(unsigned value);
private:
/** Inherited from modeless_dialog. */
virtual const std::string& window_id() const override;
/** Inherited from modeless_dialog. */
virtual void pre_show(window& window) override;
std::string title_;
std::string message_;
using clock = std::chrono::steady_clock;
std::chrono::time_point<clock> update_time_;
};
}