From 30177e2cf2721095c8a2dc85d283268b6adb4faa Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Thu, 5 Dec 2024 22:08:47 -0600 Subject: [PATCH] Addon Manager: Refactor progress reporting --- src/Mod/AddonManager/AddonManager.py | 49 +++--- .../gui/test_widget_progress_bar.py | 145 ++++++++++++++++++ .../addonmanager_widget_progress_bar.py | 108 +++++++++++-- .../addonmanager_workers_installation.py | 29 ++-- .../addonmanager_workers_startup.py | 31 ++-- src/Mod/AddonManager/package_list.py | 24 ++- 6 files changed, 293 insertions(+), 93 deletions(-) create mode 100644 src/Mod/AddonManager/AddonManagerTest/gui/test_widget_progress_bar.py diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index d55d7b7834..7d8b30cf8a 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -56,6 +56,7 @@ import addonmanager_freecad_interface as fci import AddonManager_rc # pylint: disable=unused-import from composite_view import CompositeView from Widgets.addonmanager_widget_global_buttons import WidgetGlobalButtonBar +from Widgets.addonmanager_widget_progress_bar import Progress from package_list import PackageListItemModel from Addon import Addon from manage_python_dependencies import ( @@ -310,7 +311,7 @@ class CommandAddonManager(QtCore.QObject): ) self.button_bar.python_dependencies.clicked.connect(self.show_python_updates_dialog) self.button_bar.developer_tools.clicked.connect(self.show_developer_tools) - self.composite_view.package_list.ui.progressBar.stop_clicked.connect(self.stop_update) + self.composite_view.package_list.stop_loading.connect(self.stop_update) self.composite_view.package_list.setEnabled(False) self.composite_view.execute.connect(self.execute_macro) self.composite_view.install.connect(self.launch_installer_gui) @@ -328,9 +329,6 @@ class CommandAddonManager(QtCore.QObject): # begin populating the table in a set of sub-threads self.startup() - # set the label text to start with - self.show_information(translate("AddonsInstaller", "Loading addon information")) - # rock 'n roll!!! self.dialog.exec() @@ -522,9 +520,8 @@ class CommandAddonManager(QtCore.QObject): self.update_cache = True # Make sure to trigger the other cache updates, if the json # file was missing self.create_addon_list_worker = CreateAddonListWorker() - self.create_addon_list_worker.status_message.connect(self.show_information) self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo) - self.update_progress_bar(10, 100) + self.update_progress_bar(translate("AddonsInstaller", "Creating addon list"), 10, 100) self.create_addon_list_worker.finished.connect( self.do_next_startup_phase ) # Link to step 2 @@ -534,7 +531,7 @@ class CommandAddonManager(QtCore.QObject): utils.get_cache_file_name("package_cache.json") ) self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo) - self.update_progress_bar(10, 100) + self.update_progress_bar(translate("AddonsInstaller", "Loading addon list"), 10, 100) self.create_addon_list_worker.finished.connect( self.do_next_startup_phase ) # Link to step 2 @@ -568,9 +565,10 @@ class CommandAddonManager(QtCore.QObject): self.update_cache = True # Make sure to trigger the other cache updates, if the # json file was missing self.create_addon_list_worker = CreateAddonListWorker() - self.create_addon_list_worker.status_message.connect(self.show_information) self.create_addon_list_worker.addon_repo.connect(self.add_addon_repo) - self.update_progress_bar(10, 100) + self.update_progress_bar( + translate("AddonsInstaller", "Creating macro list"), 10, 100 + ) self.create_addon_list_worker.finished.connect( self.do_next_startup_phase ) # Link to step 2 @@ -608,7 +606,6 @@ class CommandAddonManager(QtCore.QObject): def update_metadata_cache(self) -> None: if self.update_cache: self.update_metadata_cache_worker = UpdateMetadataCacheWorker(self.item_model.repos) - self.update_metadata_cache_worker.status_message.connect(self.show_information) self.update_metadata_cache_worker.finished.connect( self.do_next_startup_phase ) # Link to step 4 @@ -644,7 +641,6 @@ class CommandAddonManager(QtCore.QObject): def load_macro_metadata(self) -> None: if self.update_cache: self.load_macro_metadata_worker = CacheMacroCodeWorker(self.item_model.repos) - self.load_macro_metadata_worker.status_message.connect(self.show_information) self.load_macro_metadata_worker.update_macro.connect(self.on_package_updated) self.load_macro_metadata_worker.progress_made.connect(self.update_progress_bar) self.load_macro_metadata_worker.finished.connect(self.do_next_startup_phase) @@ -798,12 +794,6 @@ class CommandAddonManager(QtCore.QObject): return self.item_model.append_item(addon_repo) - def show_information(self, message: str) -> None: - """shows generic text in the information pane""" - - self.composite_view.package_list.ui.progressBar.set_status(message) - self.composite_view.package_list.ui.progressBar.repaint() - def append_to_repos_list(self, repo: Addon) -> None: """this function allows threads to update the main list of workbenches""" self.item_model.append_item(repo) @@ -862,15 +852,12 @@ class CommandAddonManager(QtCore.QObject): def hide_progress_widgets(self) -> None: """hides the progress bar and related widgets""" - - self.composite_view.package_list.ui.progressBar.hide() - self.composite_view.package_list.ui.view_bar.search.setFocus() + self.composite_view.package_list.set_loading(False) def show_progress_widgets(self) -> None: - if self.composite_view.package_list.ui.progressBar.isHidden(): - self.composite_view.package_list.ui.progressBar.show() + self.composite_view.package_list.set_loading(True) - def update_progress_bar(self, current_value: int, max_value: int) -> None: + def update_progress_bar(self, message: str, current_value: int, max_value: int) -> None: """Update the progress bar, showing it if it's hidden""" max_value = max_value if max_value > 0 else 1 @@ -881,14 +868,14 @@ class CommandAddonManager(QtCore.QObject): current_value = max_value self.show_progress_widgets() - region_size = 100.0 / self.number_of_progress_regions - completed_region_portion = (self.current_progress_region - 1) * region_size - current_region_portion = (float(current_value) / float(max_value)) * region_size - value = completed_region_portion + current_region_portion - self.composite_view.package_list.ui.progressBar.set_value( - value * 10 - ) # Out of 1000 segments, so it moves sort of smoothly - self.composite_view.package_list.ui.progressBar.repaint() + + progress = Progress( + status_text=message, + number_of_tasks=self.number_of_progress_regions, + current_task=self.current_progress_region - 1, + current_task_progress=current_value / max_value, + ) + self.composite_view.package_list.update_loading_progress(progress) def stop_update(self) -> None: self.cleanup_workers() diff --git a/src/Mod/AddonManager/AddonManagerTest/gui/test_widget_progress_bar.py b/src/Mod/AddonManager/AddonManagerTest/gui/test_widget_progress_bar.py new file mode 100644 index 0000000000..1e6dca06a6 --- /dev/null +++ b/src/Mod/AddonManager/AddonManagerTest/gui/test_widget_progress_bar.py @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# *************************************************************************** +# * * +# * Copyright (c) 2022 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +import sys +import unittest + +sys.path.append("../..") + +from Widgets.addonmanager_widget_progress_bar import Progress + + +class TestProgress(unittest.TestCase): + + def test_default_construction(self): + """Given no parameters, a single-task Progress object is initialized with zero progress""" + progress = Progress() + self.assertEqual(progress.status_text, "") + self.assertEqual(progress.number_of_tasks, 1) + self.assertEqual(progress.current_task, 0) + self.assertEqual(progress.current_task_progress, 0.0) + + def test_good_parameters(self): + """Given good parameters, no exception is raised""" + _ = Progress( + status_text="Some text", number_of_tasks=1, current_task=0, current_task_progress=0.0 + ) + + def test_zero_task_count(self): + with self.assertRaises(ValueError): + _ = Progress(number_of_tasks=0) + + def test_negative_task_count(self): + with self.assertRaises(ValueError): + _ = Progress(number_of_tasks=-1) + + def test_setting_status_post_creation(self): + progress = Progress() + self.assertEqual(progress.status_text, "") + progress.status_text = "Some status" + self.assertEqual(progress.status_text, "Some status") + + def test_setting_task_count(self): + progress = Progress() + progress.number_of_tasks = 10 + self.assertEqual(progress.number_of_tasks, 10) + + def test_setting_negative_task_count(self): + progress = Progress() + with self.assertRaises(ValueError): + progress.number_of_tasks = -1 + + def test_setting_invalid_task_count(self): + progress = Progress() + with self.assertRaises(TypeError): + progress.number_of_tasks = 3.14159 + + def test_setting_current_task(self): + progress = Progress(number_of_tasks=10) + progress.number_of_tasks = 5 + self.assertEqual(progress.number_of_tasks, 5) + + def test_setting_current_task_greater_than_task_count(self): + progress = Progress() + progress.number_of_tasks = 10 + with self.assertRaises(ValueError): + progress.current_task = 11 + + def test_setting_current_task_equal_to_task_count(self): + """current_task is zero-indexed, so this is too high""" + progress = Progress() + progress.number_of_tasks = 10 + with self.assertRaises(ValueError): + progress.current_task = 10 + + def test_setting_current_task_negative(self): + progress = Progress() + with self.assertRaises(ValueError): + progress.current_task = -1 + + def test_setting_current_task_invalid(self): + progress = Progress() + with self.assertRaises(TypeError): + progress.current_task = 2.718281 + + def test_setting_current_task_progress(self): + progress = Progress() + progress.current_task_progress = 50.0 + self.assertEqual(progress.current_task_progress, 50.0) + + def test_setting_current_task_progress_too_low(self): + progress = Progress() + progress.current_task_progress = -0.01 + self.assertEqual(progress.current_task_progress, 0.0) + + def test_setting_current_task_progress_too_high(self): + progress = Progress() + progress.current_task_progress = 100.001 + self.assertEqual(progress.current_task_progress, 100.0) + + def test_incrementing_task(self): + progress = Progress(number_of_tasks=10, current_task_progress=100.0) + progress.next_task() + self.assertEqual(progress.current_task, 1) + self.assertEqual(progress.current_task_progress, 0.0) + + def test_incrementing_task_too_high(self): + progress = Progress(number_of_tasks=10, current_task=9, current_task_progress=100.0) + with self.assertRaises(ValueError): + progress.next_task() + + def test_overall_progress_simple(self): + progress = Progress() + self.assertEqual(progress.overall_progress(), 0.0) + + def test_overall_progress_with_ranges(self): + progress = Progress(number_of_tasks=2, current_task=1, current_task_progress=0.0) + self.assertAlmostEqual(progress.overall_progress(), 0.5) + + def test_overall_progress_with_ranges_and_progress(self): + progress = Progress(number_of_tasks=10, current_task=5, current_task_progress=50.0) + self.assertAlmostEqual(progress.overall_progress(), 0.55) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/Mod/AddonManager/Widgets/addonmanager_widget_progress_bar.py b/src/Mod/AddonManager/Widgets/addonmanager_widget_progress_bar.py index b9d51a6750..2e069a5baf 100644 --- a/src/Mod/AddonManager/Widgets/addonmanager_widget_progress_bar.py +++ b/src/Mod/AddonManager/Widgets/addonmanager_widget_progress_bar.py @@ -36,27 +36,103 @@ except ImportError: # Get whatever version of PySide we can try: - import PySide # Use the FreeCAD wrapper + from PySide import QtCore, QtGui, QtWidgets # Use the FreeCAD wrapper except ImportError: try: - import PySide6 # Outside FreeCAD, try Qt6 first - - PySide = PySide6 + from PySide6 import QtCore, QtGui, QtWidgets # Outside FreeCAD, try Qt6 first except ImportError: - import PySide2 # Fall back to Qt5 (if this fails, Python will kill this module's import) + from PySide2 import QtCore, QtGui, QtWidgets # Fall back to Qt5 - PySide = PySide2 - -from PySide import QtCore, QtGui, QtWidgets +from dataclasses import dataclass _TOTAL_INCREMENTS = 1000 +class Progress: + """Represents progress through a process composed of multiple sub-tasks.""" + + def __init__( + self, + *, + status_text: str = "", + number_of_tasks: int = 1, + current_task: int = 0, + current_task_progress: float = 0.0, + ): + if number_of_tasks < 1: + raise ValueError(f"Number of tasks must be at least one, not {number_of_tasks}") + if current_task < 0 or current_task >= number_of_tasks: + raise ValueError( + "Current task must be between 0 and the number of tasks " + f"({number_of_tasks}), not {current_task}" + ) + if current_task_progress < 0.0: + current_task_progress = 0.0 + elif current_task_progress > 100.0: + current_task_progress = 100.0 + self.status_text: str = status_text + self._number_of_tasks: int = number_of_tasks + self._current_task: int = current_task + self._current_task_progress: float = current_task_progress + + @property + def number_of_tasks(self): + return self._number_of_tasks + + @number_of_tasks.setter + def number_of_tasks(self, value: int): + if not isinstance(value, int): + raise TypeError("Number of tasks must be an integer") + if value < 1: + raise ValueError("Number of tasks must be at least one") + self._number_of_tasks = value + + @property + def current_task(self): + """The current task (zero-indexed, always less than the number of tasks)""" + return self._current_task + + @current_task.setter + def current_task(self, value: int): + if not isinstance(value, int): + raise TypeError("Current task must be an integer") + if value < 0: + raise ValueError("Current task must be at least zero") + if value >= self._number_of_tasks: + raise ValueError("Current task must be less than the total number of tasks") + self._current_task = value + + @property + def current_task_progress(self): + """Current task progress, guaranteed to be in the range [0.0, 100.0]. Attempts to set a + value outside that range are clamped to the range.""" + return self._current_task_progress + + @current_task_progress.setter + def current_task_progress(self, value: float): + """Set the current task's progress. Rather than raising an exception when the value is + outside the expected range of [0,100], clamp the task progress to allow for some + floating point imprecision in its calculation.""" + if value < 0.0: + value = 0.0 + elif value > 100.0: + value = 100.0 + self._current_task_progress = value + + def next_task(self) -> None: + """Increment the task counter and reset the progress""" + self.current_task += 1 + self.current_task_progress = 0.0 + + def overall_progress(self) -> float: + """Gets the overall progress as a fractional value in the range [0, 1]""" + base = self._current_task / self._number_of_tasks + fraction = self._current_task_progress / (100.0 * self._number_of_tasks) + return base + fraction + + class WidgetProgressBar(QtWidgets.QWidget): - """A multipart progress bar widget, including a stop button and a status label. Defaults to a - single range with 100 increments, but can be configured with any number of major and minor - ranges. Clicking the stop button will emit a signal, but does not otherwise affect the - widget.""" + """A multipart progress bar widget, including a stop button and a status label.""" stop_clicked = QtCore.Signal() @@ -87,8 +163,6 @@ class WidgetProgressBar(QtWidgets.QWidget): self.vertical_layout.setContentsMargins(0, 0, 0, 0) self.horizontal_layout.setContentsMargins(0, 0, 0, 0) - def set_status(self, status: str): - self.status_label.setText(status) - - def set_value(self, value: int): - self.progress_bar.setValue(value) + def set_progress(self, progress: Progress) -> None: + self.status_label.setText(progress.status_text) + self.progress_bar.setValue(progress.overall_progress() * _TOTAL_INCREMENTS) diff --git a/src/Mod/AddonManager/addonmanager_workers_installation.py b/src/Mod/AddonManager/addonmanager_workers_installation.py index bec69d02bb..ff3e063ece 100644 --- a/src/Mod/AddonManager/addonmanager_workers_installation.py +++ b/src/Mod/AddonManager/addonmanager_workers_installation.py @@ -53,8 +53,7 @@ class UpdateMetadataCacheWorker(QtCore.QThread): """Scan through all available packages and see if our local copy of package.xml needs to be updated""" - status_message = QtCore.Signal(str) - progress_made = QtCore.Signal(int, int) + progress_made = QtCore.Signal(str, int, int) package_updated = QtCore.Signal(Addon) class RequestType(Enum): @@ -166,18 +165,26 @@ class UpdateMetadataCacheWorker(QtCore.QThread): """Callback for handling a completed metadata file download.""" if index in self.requests: self.requests_completed += 1 - self.progress_made.emit(self.requests_completed, self.total_requests) request = self.requests.pop(index) if code == 200: # HTTP success self.updated_repos.add(request[0]) # mark this repo as updated + file = "unknown" if request[1] == UpdateMetadataCacheWorker.RequestType.PACKAGE_XML: self.process_package_xml(request[0], data) + file = "package.xml" elif request[1] == UpdateMetadataCacheWorker.RequestType.METADATA_TXT: self.process_metadata_txt(request[0], data) + file = "metadata.txt" elif request[1] == UpdateMetadataCacheWorker.RequestType.REQUIREMENTS_TXT: self.process_requirements_txt(request[0], data) + file = "requirements.txt" elif request[1] == UpdateMetadataCacheWorker.RequestType.ICON: self.process_icon(request[0], data) + file = "icon" + message = translate("AddonsInstaller", "Downloaded {} for {}").format( + file, request[0].display_name + ) + self.progress_made.emit(message, self.requests_completed, self.total_requests) def process_package_xml(self, repo: Addon, data: QtCore.QByteArray): """Process the package.xml metadata file""" @@ -197,9 +204,6 @@ class UpdateMetadataCacheWorker(QtCore.QThread): return repo.set_metadata(metadata) FreeCAD.Console.PrintLog(f"Downloaded package.xml for {repo.name}\n") - self.status_message.emit( - translate("AddonsInstaller", "Downloaded package.xml for {}").format(repo.name) - ) # Grab a new copy of the icon as well: we couldn't enqueue this earlier because # we didn't know the path to it, which is stored in the package.xml file. @@ -251,10 +255,6 @@ class UpdateMetadataCacheWorker(QtCore.QThread): def process_metadata_txt(self, repo: Addon, data: QtCore.QByteArray): """Process the metadata.txt metadata file""" - self.status_message.emit( - translate("AddonsInstaller", "Downloaded metadata.txt for {}").format(repo.display_name) - ) - f = self._ensure_string(data, repo.name, "metadata.txt") lines = f.splitlines() for line in lines: @@ -291,12 +291,6 @@ class UpdateMetadataCacheWorker(QtCore.QThread): def process_requirements_txt(self, repo: Addon, data: QtCore.QByteArray): """Process the requirements.txt metadata file""" - self.status_message.emit( - translate( - "AddonsInstaller", - "Downloaded requirements.txt for {}", - ).format(repo.display_name) - ) f = self._ensure_string(data, repo.name, "requirements.txt") lines = f.splitlines() @@ -312,9 +306,6 @@ class UpdateMetadataCacheWorker(QtCore.QThread): def process_icon(self, repo: Addon, data: QtCore.QByteArray): """Convert icon data into a valid icon file and store it""" - self.status_message.emit( - translate("AddonsInstaller", "Downloaded icon for {}").format(repo.display_name) - ) cache_file = repo.get_cached_icon_filename() with open(cache_file, "wb") as icon_file: icon_file.write(data.data()) diff --git a/src/Mod/AddonManager/addonmanager_workers_startup.py b/src/Mod/AddonManager/addonmanager_workers_startup.py index 3681cbc9de..32f590467b 100644 --- a/src/Mod/AddonManager/addonmanager_workers_startup.py +++ b/src/Mod/AddonManager/addonmanager_workers_startup.py @@ -57,8 +57,8 @@ class CreateAddonListWorker(QtCore.QThread): """This worker updates the list of available workbenches, emitting an "addon_repo" signal for each Addon as they are processed.""" - status_message = QtCore.Signal(str) addon_repo = QtCore.Signal(object) + progress_made = QtCore.Signal(str, int, int) def __init__(self): QtCore.QThread.__init__(self) @@ -121,7 +121,6 @@ class CreateAddonListWorker(QtCore.QThread): "Failed to connect to GitHub. Check your connection and proxy settings.", ) FreeCAD.Console.PrintError(message + "\n") - self.status_message.emit(message) raise ConnectionError def _process_deprecated(self, deprecated_addons): @@ -265,8 +264,6 @@ class CreateAddonListWorker(QtCore.QThread): repo.obsolete = True self.addon_repo.emit(repo) - self.status_message.emit(translate("AddonsInstaller", "Workbenches list was updated.")) - def _retrieve_macros_from_git(self): """Retrieve macros from FreeCAD-macros.git @@ -281,7 +278,6 @@ class CreateAddonListWorker(QtCore.QThread): "AddonsInstaller", "Git is disabled, skipping Git macros", ) - self.status_message.emit(message) FreeCAD.Console.PrintWarning(message + "\n") return @@ -529,7 +525,7 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): """This worker checks for available updates for all workbenches""" update_status = QtCore.Signal(Addon) - progress_made = QtCore.Signal(int, int) + progress_made = QtCore.Signal(str, int, int) def __init__(self, repos: List[Addon]): @@ -549,7 +545,10 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): for repo in self.repos: if self.current_thread.isInterruptionRequested(): return - self.progress_made.emit(count, len(self.repos)) + message = translate("AddonsInstaller", "Checking {} for update").format( + repo.display_name + ) + self.progress_made.emit(message, count, len(self.repos)) count += 1 if repo.status() == Addon.Status.UNCHECKED: if repo.repo_type == Addon.Kind.WORKBENCH: @@ -743,9 +742,8 @@ class UpdateChecker: class CacheMacroCodeWorker(QtCore.QThread): """Download and cache the macro code, and parse its internal metadata""" - status_message = QtCore.Signal(str) update_macro = QtCore.Signal(Addon) - progress_made = QtCore.Signal(int, int) + progress_made = QtCore.Signal(str, int, int) def __init__(self, repos: List[Addon]) -> None: QtCore.QThread.__init__(self) @@ -761,8 +759,6 @@ class CacheMacroCodeWorker(QtCore.QThread): """Rarely called directly: create an instance and call start() on it instead to launch in a new thread""" - self.status_message.emit(translate("AddonsInstaller", "Caching macro code...")) - self.repo_queue = queue.Queue() num_macros = 0 for repo in self.repos: @@ -846,7 +842,8 @@ class CacheMacroCodeWorker(QtCore.QThread): if QtCore.QThread.currentThread().isInterruptionRequested(): return - self.progress_made.emit(len(self.repos) - self.repo_queue.qsize(), len(self.repos)) + message = translate("AddonsInstaller", "Caching {} macro").format(repo.display_name) + self.progress_made.emit(message, len(self.repos) - self.repo_queue.qsize(), len(self.repos)) try: next_repo = self.repo_queue.get_nowait() @@ -857,12 +854,6 @@ class CacheMacroCodeWorker(QtCore.QThread): self.terminators.append( QtCore.QTimer.singleShot(10000, lambda: self.terminate(worker)) ) - self.status_message.emit( - translate( - "AddonsInstaller", - "Getting metadata from macro {}", - ).format(next_repo.macro.name) - ) worker.start() except queue.Empty: pass @@ -895,7 +886,6 @@ class CacheMacroCodeWorker(QtCore.QThread): class GetMacroDetailsWorker(QtCore.QThread): """Retrieve the macro details for a macro""" - status_message = QtCore.Signal(str) readme_updated = QtCore.Signal(str) def __init__(self, repo): @@ -907,12 +897,9 @@ class GetMacroDetailsWorker(QtCore.QThread): """Rarely called directly: create an instance and call start() on it instead to launch in a new thread""" - self.status_message.emit(translate("AddonsInstaller", "Retrieving macro description...")) if not self.macro.parsed and self.macro.on_git: - self.status_message.emit(translate("AddonsInstaller", "Retrieving info from Git")) self.macro.fill_details_from_file(self.macro.src_filename) if not self.macro.parsed and self.macro.on_wiki: - self.status_message.emit(translate("AddonsInstaller", "Retrieving info from wiki")) mac = self.macro.name.replace(" ", "_") mac = mac.replace("&", "%26") mac = mac.replace("+", "%2B") diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index c9c2a92558..921337af50 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -39,7 +39,7 @@ from addonmanager_metadata import get_first_supported_freecad_version, Version from Widgets.addonmanager_widget_view_control_bar import WidgetViewControlBar, SortOptions from Widgets.addonmanager_widget_view_selector import AddonManagerDisplayStyle from Widgets.addonmanager_widget_filter_selector import StatusFilter, Filter -from Widgets.addonmanager_widget_progress_bar import WidgetProgressBar +from Widgets.addonmanager_widget_progress_bar import Progress, WidgetProgressBar from addonmanager_licenses import get_license_manager translate = FreeCAD.Qt.translate @@ -50,9 +50,11 @@ translate = FreeCAD.Qt.translate class PackageList(QtWidgets.QWidget): """A widget that shows a list of packages and various widgets to control the - display of the list""" + display of the list, including a progress bar that can display and interrupt the load + process.""" itemSelected = QtCore.Signal(Addon) + stop_loading = QtCore.Signal() def __init__(self, parent=None): super().__init__(parent) @@ -70,6 +72,7 @@ class PackageList(QtWidgets.QWidget): self.ui.view_bar.sort_changed.connect(self.item_filter.setSortRole) self.ui.view_bar.sort_changed.connect(self.item_delegate.set_sort) self.ui.view_bar.sort_order_changed.connect(lambda order: self.item_filter.sort(0, order)) + self.ui.progress_bar.stop_clicked.connect(self.stop_loading) # Set up the view the same as the last time: pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") @@ -157,6 +160,19 @@ class PackageList(QtWidgets.QWidget): pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") pref.SetInt("ViewStyle", style) + def set_loading(self, is_loading: bool) -> None: + """Set the loading status of this package list: when a package list is loading, it shows + a progress bar. When it is no longer loading, the bar is hidden and the search bar gets + the focus.""" + if is_loading: + self.ui.progress_bar.show() + else: + self.ui.progress_bar.hide() + self.ui.view_bar.search.setFocus() + + def update_loading_progress(self, progress: Progress) -> None: + self.ui.progress_bar.set_progress(progress) + class PackageListItemModel(QtCore.QAbstractListModel): """The model for use with the PackageList class.""" @@ -764,7 +780,7 @@ class Ui_PackageList: self.verticalLayout.addWidget(self.listPackages) - self.progressBar = WidgetProgressBar() - self.verticalLayout.addWidget(self.progressBar) + self.progress_bar = WidgetProgressBar() + self.verticalLayout.addWidget(self.progress_bar) QtCore.QMetaObject.connectSlotsByName(form)