From 4c5b8f368c1951107cde743936d26d6695ae7eb9 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 15:10:57 -0600 Subject: [PATCH 01/10] Addon Manager: Implement simple macro metadata cache --- src/Mod/AddonManager/AddonManager.py | 43 ++++-- src/Mod/AddonManager/addonmanager_macro.py | 98 +++++++------ .../AddonManager/addonmanager_utilities.py | 4 +- src/Mod/AddonManager/addonmanager_workers.py | 131 +++++++++++++++++- src/Mod/AddonManager/package_list.py | 9 ++ 5 files changed, 228 insertions(+), 57 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index ae8b6e887b..0eb185a123 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -24,6 +24,7 @@ # * * # *************************************************************************** +from inspect import indentsize import os import shutil import stat @@ -78,6 +79,7 @@ class CommandAddonManager: "update_worker", "check_worker", "show_worker", + "cache_macros_worker", "showmacro_worker", "macro_worker", "install_worker", @@ -262,7 +264,7 @@ class CommandAddonManager: ) # set info for the progress bar: - self.dialog.progressBar.setMaximum(100) + self.dialog.progressBar.setMaximum(1000) # begin populating the table in a set of sub-threads self.startup() @@ -383,6 +385,7 @@ class CommandAddonManager: self.activate_table_widgets, self.populate_macros, self.update_metadata_cache, + self.cache_macros, self.check_updates, ] self.current_progress_region = 0 @@ -410,7 +413,6 @@ class CommandAddonManager: def populate_packages_table(self) -> None: self.item_model.clear() - self.current_progress_region += 1 use_cache = not self.update_cache if use_cache: @@ -456,7 +458,7 @@ class CommandAddonManager: if hasattr(self, "package_cache"): package_cache_path = self.get_cache_file_name("package_cache.json") with open(package_cache_path, "w") as f: - f.write(json.dumps(self.package_cache)) + f.write(json.dumps(self.package_cache, indent=" ")) def activate_table_widgets(self) -> None: self.packageList.setEnabled(True) @@ -464,7 +466,6 @@ class CommandAddonManager: self.do_next_startup_phase() def populate_macros(self) -> None: - self.current_progress_region += 1 if self.update_cache or not os.path.isfile( self.get_cache_file_name("macro_cache.json") ): @@ -495,11 +496,10 @@ class CommandAddonManager: def write_macro_cache(self): macro_cache_path = self.get_cache_file_name("macro_cache.json") with open(macro_cache_path, "w") as f: - f.write(json.dumps(self.macro_cache)) + f.write(json.dumps(self.macro_cache, indent=" ")) self.macro_cache = [] def update_metadata_cache(self) -> None: - self.current_progress_region += 1 if self.update_cache: self.update_metadata_cache_worker = UpdateMetadataCacheWorker( self.item_model.repos @@ -532,10 +532,20 @@ class CommandAddonManager: repo.icon = self.get_icon(repo, update=True) self.item_model.reload_item(repo) + def cache_macros(self) -> None: + if self.update_cache: + self.cache_macros_worker = CacheMacroCode(self.item_model.repos) + self.cache_macros_worker.status_message.connect(self.show_information) + self.cache_macros_worker.update_macro.connect(self.on_package_updated) + self.cache_macros_worker.progress_made.connect(self.update_progress_bar) + self.cache_macros_worker.finished.connect(self.do_next_startup_phase) + self.cache_macros_worker.start() + else: + self.do_next_startup_phase() + def check_updates(self) -> None: "checks every installed addon for available updates" - self.current_progress_region += 1 pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") autocheck = pref.GetBool("AutoCheck", False) if not autocheck: @@ -645,6 +655,7 @@ class CommandAddonManager: """shows generic text in the information pane (which might be collapsed)""" self.dialog.labelStatusInfo.setText(message) + self.dialog.labelStatusInfo.repaint() def show_workbench(self, repo: AddonManagerRepo) -> None: self.packageList.hide() @@ -867,12 +878,20 @@ class CommandAddonManager: def update_progress_bar(self, current_value: int, max_value: int) -> None: """Update the progress bar, showing it if it's hidden""" + if current_value < 0: + FreeCAD.Console.PrintWarning( + f"Addon Manager: Internal error, current progress value is negative in region {self.current_progress_region}" + ) + self.show_progress_widgets() - region_size = 100 / self.number_of_progress_regions - value = (self.current_progress_region - 1) * region_size + ( - current_value / max_value / self.number_of_progress_regions - ) * region_size - self.dialog.progressBar.setValue(value) + 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.dialog.progressBar.setValue( + value * 10 + ) # Out of 1000 segments, so it moves sort of smoothly + self.dialog.progressBar.repaint() def toggle_details(self) -> None: if self.dialog.labelStatusInfo.isHidden(): diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index d602dabd15..4fbcf96067 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -23,10 +23,10 @@ import os import re -import sys +import io import codecs import shutil -from typing import Dict, Union, List +from typing import Dict, Tuple, List import FreeCAD @@ -56,10 +56,12 @@ class Macro(object): self.on_wiki = False self.on_git = False self.desc = "" + self.comment = "" self.code = "" self.url = "" self.version = "" self.src_filename = "" + self.author = "" self.other_files = [] self.parsed = False @@ -93,37 +95,56 @@ class Macro(object): os.path.join(FreeCAD.getUserMacroDir(True), "Macro_" + self.filename) ) - def fill_details_from_file(self, filename): - with open(filename) as f: - # Number of parsed fields of metadata. For now, __Comment__, - # __Web__, __Version__, __Files__. - number_of_required_fields = 4 - re_desc = re.compile(r"^__Comment__\s*=\s*(['\"])(.*)\1") - re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1") - re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1") - re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1") - for line in f.readlines(): - match = re.match(re_desc, line) - if match: - self.desc = match.group(2) - number_of_required_fields -= 1 - match = re.match(re_url, line) - if match: - self.url = match.group(2) - number_of_required_fields -= 1 - match = re.match(re_version, line) - if match: - self.version = match.group(2) - number_of_required_fields -= 1 - match = re.match(re_files, line) - if match: - self.other_files = [of.strip() for of in match.group(2).split(",")] - number_of_required_fields -= 1 - if number_of_required_fields <= 0: - break - f.seek(0) + def fill_details_from_file(self, filename: str) -> None: + with open(filename, errors="replace") as f: self.code = f.read() - self.parsed = True + self.fill_details_from_code(self.code) + + def fill_details_from_code(self, code: str) -> None: + # Number of parsed fields of metadata. Overrides anything set previously (the code is considered authoritative). + # For now: + # __Comment__ + # __Web__ + # __Version__ + # __Files__ + # __Author__ + number_of_fields = 5 + re_comment = re.compile( + r"^__Comment__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE + ) + re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + re_version = re.compile( + r"^__Version__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE + ) + re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + re_author = re.compile(r"^__Author__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + + f = io.StringIO(code) + while f: + line = f.readline() + match = re.match(re_comment, line) + if match: + self.comment = match.group(2) + number_of_fields -= 1 + match = re.match(re_author, line) + if match: + self.author = match.group(2) + number_of_fields -= 1 + match = re.match(re_url, line) + if match: + self.url = match.group(2) + number_of_fields -= 1 + match = re.match(re_version, line) + if match: + self.version = match.group(2) + number_of_fields -= 1 + match = re.match(re_files, line) + if match: + self.other_files = [of.strip() for of in match.group(2).split(",")] + number_of_fields -= 1 + if number_of_fields <= 0: + break + self.parsed = True def fill_details_from_wiki(self, url): code = "" @@ -157,16 +178,9 @@ class Macro(object): + "\n" ) return - # code = u2.read() - # github is slow to respond... We need to use this trick below response = "" block = 8192 - # expected = int(u2.headers["content-length"]) - while True: - # print("expected:", expected, "got:", len(response)) - data = u2.read(block) - if not data: - break + while data := u2.read(block): if isinstance(data, bytes): data = data.decode("utf-8") response += data @@ -213,9 +227,9 @@ class Macro(object): flat_code += chunk code = flat_code self.code = code - self.parsed = True + self.fill_details_from_code(self.code) - def install(self, macro_dir: str) -> (bool, List[str]): + def install(self, macro_dir: str) -> Tuple[bool, List[str]]: """Install a macro and all its related files Returns True if the macro was installed correctly. diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 532c46ec67..c7f516b9ef 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -29,11 +29,13 @@ import sys import ctypes import tempfile import ssl +from typing import Union import urllib from urllib.request import Request from urllib.error import URLError from urllib.parse import urlparse +from http.client import HTTPResponse from PySide2 import QtGui, QtCore, QtWidgets @@ -94,7 +96,7 @@ def symlink(source, link_name): raise ctypes.WinError() -def urlopen(url: str): +def urlopen(url: str) -> Union[None, HTTPResponse]: """Opens an url with urllib and streams it to a temp file""" timeout = 5 diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 1460301b11..8a65498ae3 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -31,6 +31,7 @@ import hashlib import threading import queue import io +import time from datetime import datetime from typing import Union, List @@ -637,6 +638,128 @@ class FillMacroListWorker(QtCore.QThread): self.add_macro_signal.emit(repo) +class CacheMacroCode(QtCore.QThread): + """Download and cache the macro code, and parse its internal metadata""" + + status_message = QtCore.Signal(str) + update_macro = QtCore.Signal(AddonManagerRepo) + progress_made = QtCore.Signal(int, int) + + def __init__(self, repos: List[AddonManagerRepo]) -> None: + QtCore.QThread.__init__(self) + self.repos = repos + self.workers = [] + self.terminators = [] + self.lock = threading.Lock() + self.failed = [] + self.counter = 0 + + def run(self): + self.status_message.emit(translate("AddonsInstaller", "Caching macro code...")) + + self.repo_queue = queue.Queue() + current_thread = QtCore.QThread.currentThread() + num_macros = 0 + for repo in self.repos: + if repo.macro is not None: + self.repo_queue.put(repo) + num_macros += 1 + + # Emulate QNetworkAccessManager and spool up six connections: + for _ in range(6): + self.update_and_advance(None) + + while True: + if current_thread.isInterruptionRequested(): + for worker in self.workers: + worker.requestInterruption() + worker.wait(100) + if not worker.isFinished(): + # Kill it + worker.terminate() + return + # Ensure our signals propagate out by running an internal thread-local event loop + QtCore.QCoreApplication.processEvents() + with self.lock: + if self.counter >= num_macros: + break + time.sleep(0.1) + + # Make sure all of our child threads have fully exited: + for i, worker in enumerate(self.workers): + worker.wait(50) + if not worker.isFinished(): + FreeCAD.Console.PrintError( + f"Addon Manager: a worker process failed to complete while fetching {worker.macro.name}\n" + ) + worker.terminate() + + self.repo_queue.join() + for terminator in self.terminators: + if terminator and terminator.isActive(): + terminator.stop() + + FreeCAD.Console.PrintMessage( + f"Out of {num_macros} macros, {len(self.failed)} failed" + ) + + def update_and_advance(self, repo: AddonManagerRepo) -> None: + if repo is not None: + if repo.macro.name not in self.failed: + self.update_macro.emit(repo) + self.repo_queue.task_done() + with self.lock: + self.counter += 1 + + if QtCore.QThread.currentThread().isInterruptionRequested(): + return + + self.progress_made.emit( + len(self.repos) - self.repo_queue.qsize(), len(self.repos) + ) + + try: + next_repo = self.repo_queue.get_nowait() + worker = GetMacroDetailsWorker(next_repo) + worker.finished.connect(lambda: self.update_and_advance(next_repo)) + with self.lock: + self.workers.append(worker) + self.terminators.append( + QtCore.QTimer.singleShot(10000, lambda: self.terminate(worker)) + ) + self.status_message.emit( + translate( + "AddonsInstaller", + f"Getting metadata from macro {next_repo.macro.name}", + ) + ) + worker.start() + except queue.Empty: + pass + + def terminate(self, worker) -> None: + if not worker.isFinished(): + macro_name = worker.macro.name + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + f"Timeout while fetching metadata for macro {macro_name}", + ) + + "\n" + ) + worker.requestInterruption() + worker.wait(100) + if worker.isRunning(): + worker.terminate() + worker.wait(50) + if worker.isRunning(): + FreeCAD.Console.PrintError( + f"Failed to kill process for macro {macro_name}!\n" + ) + with self.lock: + self.failed.append(macro_name) + + class ShowWorker(QtCore.QThread): """This worker retrieves info of a given workbench""" @@ -1545,11 +1668,15 @@ class UpdateAllWorker(QtCore.QThread): self.done.emit() def on_success(self, repo: AddonManagerRepo) -> None: - self.progress_made.emit(self.repo_queue.qsize(), len(self.repos)) + self.progress_made.emit( + len(self.repos) - self.repo_queue.qsize(), len(self.repos) + ) self.success.emit(repo) def on_failure(self, repo: AddonManagerRepo) -> None: - self.progress_made.emit(self.repo_queue.qsize(), len(self.repos)) + self.progress_made.emit( + len(self.repos) - self.repo_queue.qsize(), len(self.repos) + ) self.failure.emit(repo) diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index 17a2a78e08..2e7e677dee 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -358,6 +358,15 @@ class PackageListItemDelegate(QStyledItemDelegate): f"\n{maintainer['name']} <{maintainer['email']}>" ) self.widget.ui.labelMaintainer.setText(maintainers_string) + elif repo.macro and repo.macro.parsed: + if repo.macro.comment: + self.widget.ui.labelDescription.setText(repo.macro.comment) + elif repo.macro.desc: + comment, _, _ = repo.desc.partition(" Date: Fri, 31 Dec 2021 16:02:15 -0600 Subject: [PATCH 02/10] Addon Manager: Improve macro scanning performance --- src/Mod/AddonManager/addonmanager_macro.py | 36 ++++++++++++++++------ src/Mod/AddonManager/package_details.py | 2 +- src/Mod/AddonManager/package_list.py | 18 +++++++---- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index 4fbcf96067..ffd619cab7 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -26,6 +26,7 @@ import re import io import codecs import shutil +import time from typing import Dict, Tuple, List import FreeCAD @@ -64,6 +65,7 @@ class Macro(object): self.author = "" self.other_files = [] self.parsed = False + self.parse_time = 0.0 def __eq__(self, other): return self.filename == other.filename @@ -108,23 +110,29 @@ class Macro(object): # __Version__ # __Files__ # __Author__ + start = time.perf_counter() + max_lines_to_search = 50 + line_counter = 0 number_of_fields = 5 - re_comment = re.compile( - r"^__Comment__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE - ) - re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) - re_version = re.compile( - r"^__Version__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE - ) - re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) - re_author = re.compile(r"^__Author__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + ic = re.IGNORECASE # Shorten the line for Black + re_comment = re.compile(r"^__Comment__\s*=\s*(['\"])(.*)\1", flags=ic) + re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1", flags=ic) + re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1", flags=ic) + re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1", flags=ic) + re_author = re.compile(r"^__Author__\s*=\s*(['\"])(.*)\1", flags=ic) f = io.StringIO(code) - while f: + while f and line_counter < max_lines_to_search: line = f.readline() + line_counter += 1 + if not line.startswith( + "__" + ): # Speed things up a bit... this comparison is very cheap + continue match = re.match(re_comment, line) if match: self.comment = match.group(2) + self.comment = re.sub("<.*?>", "", self.comment) # Strip any HTML tags number_of_fields -= 1 match = re.match(re_author, line) if match: @@ -144,7 +152,13 @@ class Macro(object): number_of_fields -= 1 if number_of_fields <= 0: break + + # Truncate long comments to speed up searches, and clean up display + if len(self.comment) > 512: + self.comment = self.comment[:511] + "…" self.parsed = True + end = time.perf_counter() + self.parse_time = end - start def fill_details_from_wiki(self, url): code = "" @@ -220,6 +234,8 @@ class Macro(object): ) desc = "No description available" self.desc = desc + self.comment, _, _ = desc.partition("", "", self.comment) # Strip any tags self.url = url if isinstance(code, list): flat_code = "" diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 2e06781a3a..2e4a115def 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -207,7 +207,7 @@ class PackageDetails(QWidget): self.ui.buttonCheckForUpdate.hide() warningColorString = "rgb(255,0,0)" - if hasattr(QApplication.instance(),"styleSheet"): + if hasattr(QApplication.instance(), "styleSheet"): # Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info if "dark" in QApplication.instance().styleSheet().lower(): warningColorString = "rgb(255,50,50)" diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index 2e7e677dee..f0e30a83a4 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -359,14 +359,14 @@ class PackageListItemDelegate(QStyledItemDelegate): ) self.widget.ui.labelMaintainer.setText(maintainers_string) elif repo.macro and repo.macro.parsed: - if repo.macro.comment: - self.widget.ui.labelDescription.setText(repo.macro.comment) - elif repo.macro.desc: - comment, _, _ = repo.desc.partition(" Date: Fri, 31 Dec 2021 17:11:23 -0600 Subject: [PATCH 03/10] Addon Manager: Fix macro cache loading --- src/Mod/AddonManager/AddonManager.py | 53 ++++++++++++-------- src/Mod/AddonManager/addonmanager_workers.py | 2 - src/Mod/AddonManager/package_list.py | 2 + 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 0eb185a123..e30c0d6924 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -79,11 +79,11 @@ class CommandAddonManager: "update_worker", "check_worker", "show_worker", - "cache_macros_worker", "showmacro_worker", "macro_worker", "install_worker", "update_metadata_cache_worker", + "load_macro_metadata_worker", "update_all_worker", "update_check_single_worker", ] @@ -385,7 +385,7 @@ class CommandAddonManager: self.activate_table_widgets, self.populate_macros, self.update_metadata_cache, - self.cache_macros, + self.load_macro_metadata, self.check_updates, ] self.current_progress_region = 0 @@ -466,32 +466,36 @@ class CommandAddonManager: self.do_next_startup_phase() def populate_macros(self) -> None: - if self.update_cache or not os.path.isfile( - self.get_cache_file_name("macro_cache.json") - ): + macro_cache_file = self.get_cache_file_name("macro_cache.json") + cache_is_bad = True + if os.path.isfile(macro_cache_file): + size = os.path.getsize(macro_cache_file) + if size > 1000: # Make sure there is actually data in there + cache_is_bad = False + if self.update_cache or cache_is_bad: self.macro_worker = FillMacroListWorker(self.get_cache_file_name("Macros")) self.macro_worker.status_message_signal.connect(self.show_information) self.macro_worker.progress_made.connect(self.update_progress_bar) self.macro_worker.add_macro_signal.connect(self.add_addon_repo) - self.macro_worker.finished.connect( - self.do_next_startup_phase - ) # Link to step 3 + self.macro_worker.finished.connect(self.do_next_startup_phase) self.macro_worker.start() else: self.macro_worker = LoadMacrosFromCacheWorker( self.get_cache_file_name("macro_cache.json") ) self.macro_worker.add_macro_signal.connect(self.add_addon_repo) - self.macro_worker.finished.connect( - self.do_next_startup_phase - ) # Link to step 3 + self.macro_worker.finished.connect(self.do_next_startup_phase) self.macro_worker.start() - def cache_macro(self, macro: AddonManagerRepo): + def cache_macro(self, repo: AddonManagerRepo): if not hasattr(self, "macro_cache"): self.macro_cache = [] - if macro.macro is not None: - self.macro_cache.append(macro.macro.to_cache()) + if repo.macro is not None: + self.macro_cache.append(repo.macro.to_cache()) + else: + FreeCAD.Console.PrintError( + f"Addon Manager: Internal error, cache_macro called on non-macro {repo.name}\n" + ) def write_macro_cache(self): macro_cache_path = self.get_cache_file_name("macro_cache.json") @@ -528,18 +532,23 @@ class CommandAddonManager: """Called when the named package has either new metadata or a new icon (or both)""" with self.lock: - self.cache_package(repo) repo.icon = self.get_icon(repo, update=True) self.item_model.reload_item(repo) - def cache_macros(self) -> None: + def load_macro_metadata(self) -> None: if self.update_cache: - self.cache_macros_worker = CacheMacroCode(self.item_model.repos) - self.cache_macros_worker.status_message.connect(self.show_information) - self.cache_macros_worker.update_macro.connect(self.on_package_updated) - self.cache_macros_worker.progress_made.connect(self.update_progress_bar) - self.cache_macros_worker.finished.connect(self.do_next_startup_phase) - self.cache_macros_worker.start() + self.load_macro_metadata_worker = CacheMacroCode(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) + self.load_macro_metadata_worker.start() else: self.do_next_startup_phase() diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 8a65498ae3..e57e842641 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -286,7 +286,6 @@ class LoadPackagesFromCacheWorker(QtCore.QThread): class LoadMacrosFromCacheWorker(QtCore.QThread): add_macro_signal = QtCore.Signal(object) - done = QtCore.Signal() def __init__(self, cache_file: str): QtCore.QThread.__init__(self) @@ -301,7 +300,6 @@ class LoadMacrosFromCacheWorker(QtCore.QThread): return new_macro = Macro.from_cache(item) self.add_macro_signal.emit(AddonManagerRepo.from_macro(new_macro)) - self.done.emit() class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index f0e30a83a4..fd3a35cc02 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -367,6 +367,8 @@ class PackageListItemDelegate(QStyledItemDelegate): self.widget.ui.labelMaintainer.setText( caption + ": " + repo.macro.author ) + else: + self.widget.ui.labelMaintainer.setText("") else: self.widget.ui.labelDescription.setText("") self.widget.ui.labelVersion.setText("") From b370d3dae47d2f2de70ac883de1672abcff0c1ad Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 17:46:13 -0600 Subject: [PATCH 04/10] Addon Manager: Improve wiki page data extraction --- src/Mod/AddonManager/addonmanager_macro.py | 23 ++++++++++++++++---- src/Mod/AddonManager/addonmanager_workers.py | 11 +++++++--- src/Mod/AddonManager/package_list.py | 15 +++++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index ffd619cab7..d2ee65a41a 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -27,7 +27,7 @@ import io import codecs import shutil import time -from typing import Dict, Tuple, List +from typing import Dict, Tuple, List, Union import FreeCAD @@ -61,6 +61,7 @@ class Macro(object): self.code = "" self.url = "" self.version = "" + self.date = "" self.src_filename = "" self.author = "" self.other_files = [] @@ -110,7 +111,7 @@ class Macro(object): # __Version__ # __Files__ # __Author__ - start = time.perf_counter() + # __Date__ max_lines_to_search = 50 line_counter = 0 number_of_fields = 5 @@ -120,6 +121,7 @@ class Macro(object): re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1", flags=ic) re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1", flags=ic) re_author = re.compile(r"^__Author__\s*=\s*(['\"])(.*)\1", flags=ic) + re_date = re.compile(r"^__Date__\s*=\s*(['\"])(.*)\1", flags=ic) f = io.StringIO(code) while f and line_counter < max_lines_to_search: @@ -146,6 +148,10 @@ class Macro(object): if match: self.version = match.group(2) number_of_fields -= 1 + match = re.match(re_date, line) + if match: + self.date = match.group(2) + number_of_fields -= 1 match = re.match(re_files, line) if match: self.other_files = [of.strip() for of in match.group(2).split(",")] @@ -157,8 +163,6 @@ class Macro(object): if len(self.comment) > 512: self.comment = self.comment[:511] + "…" self.parsed = True - end = time.perf_counter() - self.parse_time = end - start def fill_details_from_wiki(self, url): code = "" @@ -244,6 +248,17 @@ class Macro(object): code = flat_code self.code = code self.fill_details_from_code(self.code) + if not self.author: + self.author = self.parse_desc("Author: ") + if not self.date: + self.date = self.parse_desc("Last modified: ") + + def parse_desc(self, line_start: str) -> Union[str, None]: + components = self.desc.split(">") + for component in components: + if component.startswith(line_start): + end = component.find("<") + return component[len(line_start) : end] def install(self, macro_dir: str) -> Tuple[bool, List[str]]: """Install a macro and all its related files diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index e57e842641..00f4762adf 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -697,9 +697,14 @@ class CacheMacroCode(QtCore.QThread): if terminator and terminator.isActive(): terminator.stop() - FreeCAD.Console.PrintMessage( - f"Out of {num_macros} macros, {len(self.failed)} failed" - ) + if len(self.failed) > 0: + num_failed = len(self.failed) + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", + f"Out of {num_macros} macros, {num_failed} timed out while processing", + ) + ) def update_and_advance(self, repo: AddonManagerRepo) -> None: if repo is not None: diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index fd3a35cc02..247f5bac91 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -361,6 +361,21 @@ class PackageListItemDelegate(QStyledItemDelegate): elif repo.macro and repo.macro.parsed: self.widget.ui.labelDescription.setText(repo.macro.comment) self.widget.ui.labelVersion.setText(repo.macro.version) + if repo.macro.date: + if repo.macro.version: + new_label = ( + "v" + + repo.macro.version + + ", " + + translate("AddonsInstaller", "updated") + + " " + + repo.macro.date + ) + else: + new_label = ( + translate("AddonsInstaller", "Updated") + " " + repo.macro.date + ) + self.widget.ui.labelVersion.setText(new_label) if self.displayStyle == ListDisplayStyle.EXPANDED: if repo.macro.author: caption = translate("AddonsInstaller", "Author") From 8ef9f257be08a7306368d9ded73106105b9c5ab4 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 17:55:23 -0600 Subject: [PATCH 05/10] Addon Manager: Remove extraneous UI elements --- src/Mod/AddonManager/AddonManager.py | 14 +------------- src/Mod/AddonManager/AddonManager.ui | 20 -------------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index e30c0d6924..fac36dc128 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -243,7 +243,6 @@ class CommandAddonManager: ) self.dialog.buttonClose.clicked.connect(self.dialog.reject) self.dialog.buttonUpdateCache.clicked.connect(self.on_buttonUpdateCache_clicked) - self.dialog.buttonShowDetails.clicked.connect(self.toggle_details) self.dialog.buttonPauseUpdate.clicked.connect(self.stop_update) self.packageList.itemSelected.connect(self.table_row_activated) self.packageList.setEnabled(False) @@ -871,7 +870,6 @@ class CommandAddonManager: self.dialog.labelStatusInfo.hide() self.dialog.progressBar.hide() self.dialog.buttonPauseUpdate.hide() - self.dialog.buttonShowDetails.hide() self.dialog.labelUpdateInProgress.hide() self.packageList.ui.lineEditFilter.setFocus() @@ -879,9 +877,7 @@ class CommandAddonManager: if self.dialog.progressBar.isHidden(): self.dialog.progressBar.show() self.dialog.buttonPauseUpdate.show() - self.dialog.buttonShowDetails.show() - self.dialog.labelStatusInfo.hide() - self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow) + self.dialog.labelStatusInfo.show() self.dialog.labelUpdateInProgress.show() def update_progress_bar(self, current_value: int, max_value: int) -> None: @@ -902,14 +898,6 @@ class CommandAddonManager: ) # Out of 1000 segments, so it moves sort of smoothly self.dialog.progressBar.repaint() - def toggle_details(self) -> None: - if self.dialog.labelStatusInfo.isHidden(): - self.dialog.labelStatusInfo.show() - self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.DownArrow) - else: - self.dialog.labelStatusInfo.hide() - self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow) - def stop_update(self) -> None: self.cleanup_workers() self.hide_progress_widgets() diff --git a/src/Mod/AddonManager/AddonManager.ui b/src/Mod/AddonManager/AddonManager.ui index ac9269af0a..0b1be55d46 100644 --- a/src/Mod/AddonManager/AddonManager.ui +++ b/src/Mod/AddonManager/AddonManager.ui @@ -28,26 +28,6 @@ - - - - Show details - - - ... - - - Qt::RightArrow - - - - - - - Loading... - - - From 50336440dc685b90cfaee6ec4ba375be64788396 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 20:02:03 -0600 Subject: [PATCH 06/10] Addon Manager: Add preference to control macro download --- src/Mod/AddonManager/AddonManager.py | 86 +++++++---- src/Mod/AddonManager/AddonManagerOptions.ui | 17 ++- src/Mod/AddonManager/CMakeLists.txt | 1 + .../AddonManager/addonmanager_utilities.py | 11 ++ src/Mod/AddonManager/first_run.ui | 144 ++++++++++++++++++ 5 files changed, 230 insertions(+), 29 deletions(-) create mode 100644 src/Mod/AddonManager/first_run.ui diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index fac36dc128..5073d1bd7f 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -111,32 +111,64 @@ class CommandAddonManager: def Activated(self) -> None: # display first use dialog if needed - readWarningParameter = FreeCAD.ParamGet( - "User parameter:BaseApp/Preferences/Addons" - ) - readWarning = readWarningParameter.GetBool("readWarning", False) - newReadWarningParameter = FreeCAD.ParamGet( - "User parameter:Plugins/addonsRepository" - ) - readWarning |= newReadWarningParameter.GetBool("readWarning", False) + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + readWarning = pref.GetBool("readWarning2022", False) + if not readWarning: - if ( - QtWidgets.QMessageBox.warning( - None, - "FreeCAD", - translate( - "AddonsInstaller", - "The addons that can be installed here are not " - "officially part of FreeCAD, and are not reviewed " - "by the FreeCAD team. Make sure you know what you " - "are installing!", - ), - QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Ok, - ) - != QtWidgets.QMessageBox.StandardButton.Cancel - ): - readWarningParameter.SetBool("readWarning", True) + warning_dialog = FreeCADGui.PySideUic.loadUi( + os.path.join(os.path.dirname(__file__), "first_run.ui") + ) + autocheck = pref.GetBool("AutoCheck", False) + download_macros = pref.GetBool("DownloadMacros", False) + proxy_string = pref.GetString("ProxyUrl", "") + if pref.GetBool("NoProxyCheck", True): + proxy_option = 0 + elif pref.GetBool("SystemProxyCheck", False): + proxy_option = 1 + elif pref.GetBool("UserProxyCheck", False): + proxy_option = 2 + + def toggle_proxy_list(option: int): + if option == 2: + warning_dialog.lineEditProxy.show() + else: + warning_dialog.lineEditProxy.hide() + + warning_dialog.checkBoxAutoCheck.setChecked(autocheck) + warning_dialog.checkBoxDownloadMacroMetadata.setChecked(download_macros) + warning_dialog.comboBoxProxy.setCurrentIndex(proxy_option) + toggle_proxy_list(proxy_option) + if proxy_option == 2: + warning_dialog.lineEditProxy.setText(proxy_string) + + warning_dialog.comboBoxProxy.currentIndexChanged.connect(toggle_proxy_list) + + warning_dialog.labelWarning.setStyleSheet( + f"color:{utils.warning_color_string()};font-weight:bold;" + ) + + if warning_dialog.exec() == QtWidgets.QDialog.Accepted: readWarning = True + pref.SetBool("readWarning2022", True) + pref.SetBool("AutoCheck", warning_dialog.checkBoxAutoCheck.isChecked()) + pref.SetBool( + "DownloadMacros", + warning_dialog.checkBoxDownloadMacroMetadata.isChecked(), + ) + selected_proxy_option = warning_dialog.comboBoxProxy.currentIndex() + if selected_proxy_option == 0: + pref.SetBool("NoProxyCheck", True) + pref.SetBool("SystemProxyCheck", False) + pref.SetBool("UserProxyCheck", False) + elif selected_proxy_option == 1: + pref.SetBool("NoProxyCheck", False) + pref.SetBool("SystemProxyCheck", True) + pref.SetBool("UserProxyCheck", False) + else: + pref.SetBool("NoProxyCheck", False) + pref.SetBool("SystemProxyCheck", False) + pref.SetBool("UserProxyCheck", True) + pref.SetString("ProxyUrl", warning_dialog.lineEditProxy.text()) if readWarning: self.launch() @@ -384,9 +416,11 @@ class CommandAddonManager: self.activate_table_widgets, self.populate_macros, self.update_metadata_cache, - self.load_macro_metadata, self.check_updates, ] + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + if pref.GetBool("DownloadMacros", False): + self.startup_sequence.append(self.load_macro_metadata) self.current_progress_region = 0 self.number_of_progress_regions = len(self.startup_sequence) self.do_next_startup_phase() @@ -870,7 +904,6 @@ class CommandAddonManager: self.dialog.labelStatusInfo.hide() self.dialog.progressBar.hide() self.dialog.buttonPauseUpdate.hide() - self.dialog.labelUpdateInProgress.hide() self.packageList.ui.lineEditFilter.setFocus() def show_progress_widgets(self) -> None: @@ -878,7 +911,6 @@ class CommandAddonManager: self.dialog.progressBar.show() self.dialog.buttonPauseUpdate.show() self.dialog.labelStatusInfo.show() - self.dialog.labelUpdateInProgress.show() def update_progress_bar(self, current_value: int, max_value: int) -> None: """Update the progress bar, showing it if it's hidden""" diff --git a/src/Mod/AddonManager/AddonManagerOptions.ui b/src/Mod/AddonManager/AddonManagerOptions.ui index e022702023..33970bb76c 100644 --- a/src/Mod/AddonManager/AddonManagerOptions.ui +++ b/src/Mod/AddonManager/AddonManagerOptions.ui @@ -6,8 +6,8 @@ 0 0 - 390 - 628 + 388 + 621 @@ -35,6 +35,19 @@ installed addons will be checked for available updates + + + + Download Macro metadata (approximately 10MB) + + + DownloadMacros + + + Addons + + + diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index de711853f1..9ef2819ea1 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -13,6 +13,7 @@ SET(AddonManager_SRCS addonmanager_workers.py AddonManager.ui AddonManagerOptions.ui + first_run.ui compact_view.py expanded_view.py package_list.py diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index c7f516b9ef..a0cd0236fe 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -300,4 +300,15 @@ def fix_relative_links(text, base_url): return new_text +def warning_color_string() -> str: + warningColorString = "rgb(255,0,0)" + if hasattr(QtWidgets.QApplication.instance(), "styleSheet"): + # Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info + if "dark" in QtWidgets.QApplication.instance().styleSheet().lower(): + warningColorString = "rgb(255,50,50)" + else: + warningColorString = "rgb(200,0,0)" + return warningColorString + + # @} diff --git a/src/Mod/AddonManager/first_run.ui b/src/Mod/AddonManager/first_run.ui new file mode 100644 index 0000000000..4daf95476c --- /dev/null +++ b/src/Mod/AddonManager/first_run.ui @@ -0,0 +1,144 @@ + + + Dialog + + + Qt::WindowModal + + + + 0 + 0 + 398 + 237 + + + + Welcome to the Addon Manager + + + + + + The addons that can be installed here are not officially part of FreeCAD, and are not reviewed by the FreeCAD team. Make sure you know what you are installing! + + + true + + + + + + + Qt::Horizontal + + + + + + + + 75 + true + + + + Download Settings + + + + + + + Automatically check installed Addons for updates + + + + + + + Download Macro metadata (approximately 10MB) + + + + + + + + + + No proxy + + + + + System proxy + + + + + User-defined proxy: + + + + + + + + + + + + + These and other settings are available in the FreeCAD Preferences window. + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 024e404a0e22b9f6557d4ec44800296eec4c6059 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 20:10:44 -0600 Subject: [PATCH 07/10] Addon Manager: Downgrade failed low-level urlopen to log --- src/Mod/AddonManager/addonmanager_utilities.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index a0cd0236fe..8ad9df5528 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -127,9 +127,7 @@ def urlopen(url: str) -> Union[None, HTTPResponse]: u = urllib.request.urlopen(req, timeout=timeout) except URLError as e: - FreeCAD.Console.PrintError( - translate("AddonsInstaller", f"Error loading {url}") + ":\n {e.reason}\n" - ) + FreeCAD.Console.PrintLog(f"Error loading {url}:\n {e.reason}\n") return None except Exception: return None From 70c9804fc02570ba3d71bc3a253c42597f0ddc86 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 20:19:52 -0600 Subject: [PATCH 08/10] Addon Manager: Ensure cache is rebuilt after firstrun dialog --- src/Mod/AddonManager/AddonManager.py | 5 ++++- src/Mod/AddonManager/addonmanager_macro.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 5073d1bd7f..d336e516ad 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -24,7 +24,6 @@ # * * # *************************************************************************** -from inspect import indentsize import os import shutil import stat @@ -155,6 +154,8 @@ class CommandAddonManager: "DownloadMacros", warning_dialog.checkBoxDownloadMacroMetadata.isChecked(), ) + if warning_dialog.checkBoxDownloadMacroMetadata.isChecked(): + self.trigger_recache = True selected_proxy_option = warning_dialog.comboBoxProxy.currentIndex() if selected_proxy_option == 0: pref.SetBool("NoProxyCheck", True) @@ -202,6 +203,8 @@ class CommandAddonManager: # 0: Update every launch # >0: Update every n days self.update_cache = False + if hasattr(self, "trigger_recache") and self.trigger_recache: + self.update_cache = True update_frequency = pref.GetInt("UpdateFrequencyComboEntry", 0) if update_frequency == 0: days_between_updates = -1 diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index d2ee65a41a..6e65685de8 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -198,7 +198,10 @@ class Macro(object): return response = "" block = 8192 - while data := u2.read(block): + while True: + data = u2.read(block) + if not data: + break if isinstance(data, bytes): data = data.decode("utf-8") response += data @@ -232,7 +235,7 @@ class Macro(object): FreeCAD.Console.PrintWarning( translate( "AddonsInstaller", - "Unable to retrieve a description for this macro.", + f"Unable to retrieve a description from the wiki for macro {self.name}", ) + "\n" ) From 64cf6a68d85d218558adc6435cae97f11d135c7a Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 1 Jan 2022 09:43:06 -0600 Subject: [PATCH 09/10] Addon Manager: Remove unused imports --- src/Mod/AddonManager/AddonManager.py | 3 +-- src/Mod/AddonManager/addonmanager_macro.py | 1 - src/Mod/AddonManager/addonmanager_utilities.py | 7 +------ src/Mod/AddonManager/addonmanager_workers.py | 2 +- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index d336e516ad..7253877b29 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -29,8 +29,7 @@ import shutil import stat import tempfile from datetime import date, timedelta -from typing import Dict, Union -from enum import Enum +from typing import Dict from PySide2 import QtGui, QtCore, QtWidgets import FreeCADGui diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index 6e65685de8..65a8739126 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -66,7 +66,6 @@ class Macro(object): self.author = "" self.other_files = [] self.parsed = False - self.parse_time = 0.0 def __eq__(self, other): return self.filename == other.filename diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 8ad9df5528..5cc3ca5950 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -21,23 +21,18 @@ # * * # *************************************************************************** -import codecs import os import re -import shutil -import sys import ctypes -import tempfile import ssl from typing import Union import urllib -from urllib.request import Request from urllib.error import URLError from urllib.parse import urlparse from http.client import HTTPResponse -from PySide2 import QtGui, QtCore, QtWidgets +from PySide2 import QtCore, QtWidgets import FreeCAD import FreeCADGui diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 00f4762adf..a75d43b355 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -35,7 +35,7 @@ import time from datetime import datetime from typing import Union, List -from PySide2 import QtCore, QtGui, QtNetwork +from PySide2 import QtCore, QtNetwork import FreeCAD From f48d79a2271391b52b142a5fa0f5ebc3f60a631a Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 1 Jan 2022 11:41:23 -0600 Subject: [PATCH 10/10] Addon Manager: Improve display of installation details --- src/Mod/AddonManager/AddonManager.py | 2 + .../AddonManager/addonmanager_utilities.py | 66 ++++++++++- src/Mod/AddonManager/addonmanager_workers.py | 18 ++- src/Mod/AddonManager/package_details.py | 104 +++++++++++------- 4 files changed, 139 insertions(+), 51 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 7253877b29..13c2440cf8 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -755,6 +755,8 @@ class CommandAddonManager: real_install_succeeded, errors = macro.install(self.macro_repo_dir) if not real_install_succeeded: failed = True + else: + utils.update_macro_installation_details(repo) if not failed: message = translate( diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 5cc3ca5950..1ab5fafc15 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -28,6 +28,7 @@ import ssl from typing import Union import urllib +from urllib.request import Request from urllib.error import URLError from urllib.parse import urlparse from http.client import HTTPResponse @@ -294,14 +295,75 @@ def fix_relative_links(text, base_url): def warning_color_string() -> str: + """A shade of red, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio.""" + warningColorString = "rgb(255,0,0)" if hasattr(QtWidgets.QApplication.instance(), "styleSheet"): # Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info if "dark" in QtWidgets.QApplication.instance().styleSheet().lower(): - warningColorString = "rgb(255,50,50)" + warningColorString = "rgb(255,105,97)" else: - warningColorString = "rgb(200,0,0)" + warningColorString = "rgb(215,0,21)" return warningColorString +def bright_color_string() -> str: + """A shade of green, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio.""" + + brightColorString = "rgb(0,255,0)" + if hasattr(QtWidgets.QApplication.instance(), "styleSheet"): + # Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info + if "dark" in QtWidgets.QApplication.instance().styleSheet().lower(): + brightColorString = "rgb(48,219,91)" + else: + brightColorString = "rgb(36,138,61)" + return brightColorString + + +def attention_color_string() -> str: + """A shade of orange, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio.""" + + attentionColorString = "rgb(255,149,0)" + if hasattr(QtWidgets.QApplication.instance(), "styleSheet"): + # Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info + if "dark" in QtWidgets.QApplication.instance().styleSheet().lower(): + attentionColorString = "rgb(255,179,64)" + else: + attentionColorString = "rgb(255,149,0)" + return attentionColorString + + +def get_macro_version_from_file(filename: str) -> str: + re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + with open(filename, "r", errors="ignore") as f: + line_counter = 0 + max_lines_to_scan = 50 + while line_counter < max_lines_to_scan: + line_counter += 1 + line = f.readline() + if line.startswith("__"): + match = re.match(re_version, line) + if match: + return match.group(2) + return "" + + +def update_macro_installation_details(repo) -> None: + if repo is None or not hasattr(repo, "macro") or repo.macro is None: + FreeCAD.Console.PrintLog(f"Requested macro details for non-macro object\n") + return + test_file_one = os.path.join(FreeCAD.getUserMacroDir(True), repo.macro.filename) + test_file_two = os.path.join( + FreeCAD.getUserMacroDir(True), "Macro_" + repo.macro.filename + ) + if os.path.exists(test_file_one): + repo.updated_timestamp = os.path.getmtime(test_file_one) + repo.installed_version = get_macro_version_from_file(test_file_one) + elif os.path.exists(test_file_two): + repo.updated_timestamp = os.path.getmtime(test_file_two) + repo.installed_version = get_macro_version_from_file(test_file_two) + else: + return + + # @} diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index a75d43b355..45dd516e12 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -299,7 +299,9 @@ class LoadMacrosFromCacheWorker(QtCore.QThread): if QtCore.QThread.currentThread().isInterruptionRequested(): return new_macro = Macro.from_cache(item) - self.add_macro_signal.emit(AddonManagerRepo.from_macro(new_macro)) + repo = AddonManagerRepo.from_macro(new_macro) + utils.update_macro_installation_details(repo) + self.add_macro_signal.emit(repo) class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): @@ -588,6 +590,7 @@ class FillMacroListWorker(QtCore.QThread): macro.src_filename = os.path.join(dirpath, filename) repo = AddonManagerRepo.from_macro(macro) repo.url = "https://github.com/FreeCAD/FreeCAD-macros.git" + utils.update_macro_installation_details(repo) self.add_macro_signal.emit(repo) def retrieve_macros_from_wiki(self): @@ -633,6 +636,7 @@ class FillMacroListWorker(QtCore.QThread): macro.on_wiki = True repo = AddonManagerRepo.from_macro(macro) repo.url = "https://wiki.freecad.org/Macros_recipes" + utils.update_macro_installation_details(repo) self.add_macro_signal.emit(repo) @@ -1053,17 +1057,8 @@ class GetMacroDetailsWorker(QtCore.QThread): mac = mac.replace("+", "%2B") url = "https://wiki.freecad.org/Macro_" + mac self.macro.fill_details_from_wiki(url) - if self.macro.is_installed(): - already_installed_msg = ( - '' - + translate("AddonsInstaller", "This macro is already installed.") - + "
" - ) - else: - already_installed_msg = "" message = ( - already_installed_msg - + "

" + "

" + self.macro.name + "

" + self.macro.desc @@ -1719,6 +1714,7 @@ class UpdateSingleWorker(QtCore.QThread): install_succeeded, errors = repo.macro.install( FreeCAD.getUserMacroDir(True) ) + utils.update_macro_installation_details(repo) if install_succeeded: self.success.emit(repo) diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 2e4a115def..508073d7d4 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -31,12 +31,14 @@ from datetime import date, timedelta import FreeCAD -from addonmanager_utilities import translate # this needs to be as is for pylupdate +import addonmanager_utilities as utils from addonmanager_workers import ShowWorker, GetMacroDetailsWorker from AddonManagerRepo import AddonManagerRepo import inspect +translate = FreeCAD.Qt.translate + class PackageDetails(QWidget): @@ -93,31 +95,34 @@ class PackageDetails(QWidget): self.ui.buttonExecute.hide() if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: - installed_version_string = "" - if repo.installed_version: - installed_version_string = translate("AddonsInstaller", "Version") + " " - installed_version_string += repo.installed_version - else: - installed_version_string = ( - translate( - "AddonsInstaller", "Unknown version (no package.xml file found)" - ) - + " " - ) + version = repo.installed_version + date = "" + installed_version_string = "

" if repo.updated_timestamp: - installed_version_string += ( - " " + translate("AddonsInstaller", "installed on") + " " - ) - installed_version_string += ( + date = ( QDateTime.fromTime_t(repo.updated_timestamp) .date() .toString(Qt.SystemLocaleShortDate) ) - installed_version_string += ". " + if version and date: + installed_version_string += ( + translate( + "AddonsInstaller", f"Version {version} installed on {date}" + ) + + ". " + ) + elif version: + installed_version_string += ( + translate("AddonsInstaller", f"Version {version} installed") + ". " + ) + elif date: + installed_version_string += ( + translate("AddonsInstaller", f"Installed on {date}") + ". " + ) else: installed_version_string += ( - translate("AddonsInstaller", "installed") + ". " + translate("AddonsInstaller", "Installed") + ". " ) if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: @@ -129,12 +134,20 @@ class PackageDetails(QWidget): ) installed_version_string += repo.metadata.Version installed_version_string += "." + elif repo.macro and repo.macro.version: + installed_version_string += ( + "" + + translate("AddonsInstaller", "Update available to version") + + " " + ) + installed_version_string += repo.macro.version + installed_version_string += "." else: installed_version_string += ( "" + translate( "AddonsInstaller", - "Update available to unknown version (no package.xml file found)", + "An update is available", ) + "." ) @@ -166,19 +179,32 @@ class PackageDetails(QWidget): + "." ) - basedir = FreeCAD.getUserAppDataDir() - moddir = os.path.join(basedir, "Mod", repo.name) - installed_version_string += ( - "
" - + translate("AddonsInstaller", "Installation location") - + ": " - + moddir + installed_version_string += "

" + self.ui.labelPackageDetails.setText(installed_version_string) + if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: + self.ui.labelPackageDetails.setStyleSheet( + "color:" + utils.attention_color_string() + ) + else: + self.ui.labelPackageDetails.setStyleSheet( + "color:" + utils.bright_color_string() + ) + self.ui.labelPackageDetails.show() + + if repo.macro is not None: + moddir = FreeCAD.getUserMacroDir(True) + else: + basedir = FreeCAD.getUserAppDataDir() + moddir = os.path.join(basedir, "Mod", repo.name) + installationLocationString = ( + translate("AddonsInstaller", "Installation location") + ": " + moddir ) - self.ui.labelPackageDetails.setText(installed_version_string) - self.ui.labelPackageDetails.show() + self.ui.labelInstallationLocation.setText(installationLocationString) + self.ui.labelInstallationLocation.show() else: self.ui.labelPackageDetails.hide() + self.ui.labelInstallationLocation.hide() if repo.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: self.ui.buttonInstall.show() @@ -206,14 +232,6 @@ class PackageDetails(QWidget): self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.hide() - warningColorString = "rgb(255,0,0)" - if hasattr(QApplication.instance(), "styleSheet"): - # Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info - if "dark" in QApplication.instance().styleSheet().lower(): - warningColorString = "rgb(255,50,50)" - else: - warningColorString = "rgb(200,0,0)" - if repo.obsolete: self.ui.labelWarningInfo.show() self.ui.labelWarningInfo.setText( @@ -221,7 +239,9 @@ class PackageDetails(QWidget): + translate("AddonsInstaller", "WARNING: This addon is obsolete") + "" ) - self.ui.labelWarningInfo.setStyleSheet("color:" + warningColorString) + self.ui.labelWarningInfo.setStyleSheet( + "color:" + utils.warning_color_string() + ) elif repo.python2: self.ui.labelWarningInfo.show() self.ui.labelWarningInfo.setText( @@ -229,7 +249,9 @@ class PackageDetails(QWidget): + translate("AddonsInstaller", "WARNING: This addon is Python 2 Only") + "" ) - self.ui.labelWarningInfo.setStyleSheet("color:" + warningColorString) + self.ui.labelWarningInfo.setStyleSheet( + "color:" + utils.warning_color_string() + ) else: self.ui.labelWarningInfo.hide() @@ -416,6 +438,12 @@ class Ui_PackageDetails(object): self.verticalLayout_2.addWidget(self.labelPackageDetails) + self.labelInstallationLocation = QLabel(PackageDetails) + self.labelInstallationLocation.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.labelInstallationLocation.hide() + + self.verticalLayout_2.addWidget(self.labelInstallationLocation) + self.labelWarningInfo = QLabel(PackageDetails) self.labelWarningInfo.hide()