From 994071c43bfe7fe5c635dcece0ad6a97874cb402 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 28 Jan 2022 21:46:36 -0600 Subject: [PATCH] Addon Manager: Switch to QWebEngineView for README Rather than manually parsing the output from the repo's host, the Addon Manager now uses an embedded QWebEngineView to display the README data. This makes the display more repo-location agnostic (rather than trying to parse the specific code from GitHub, Gitlab, etc.). Special handling of known hosts is still provided to improve the display of the page, but it is not required. Clicking a link on the page still loads in a new system browser window, with the exception of links to the FreeCAD wiki, which are loaded in the same browser. This is expected to be used primarily to access traslated pages for Macros, so no advanced web-browsing features are displayed (e.g. back buttons, history access, etc.). --- src/Mod/AddonManager/AddonManager.py | 81 +--- src/Mod/AddonManager/AddonManagerRepo.py | 36 +- src/Mod/AddonManager/CMakeLists.txt | 1 + .../AddonManager/addonmanager_utilities.py | 52 +-- src/Mod/AddonManager/addonmanager_workers.py | 428 ++++++------------ src/Mod/AddonManager/loading.html | 93 ++++ src/Mod/AddonManager/package_details.py | 399 +++++++++------- src/Mod/AddonManager/package_list.py | 30 +- 8 files changed, 533 insertions(+), 587 deletions(-) create mode 100644 src/Mod/AddonManager/loading.html diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 8b88b4eeee..72017d1a52 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -87,7 +87,6 @@ class CommandAddonManager: "update_metadata_cache_worker", "load_macro_metadata_worker", "update_all_worker", - "update_check_single_worker", "dependency_installation_worker", ] @@ -370,9 +369,6 @@ class CommandAddonManager: # connect slots self.dialog.rejected.connect(self.reject) self.dialog.buttonUpdateAll.clicked.connect(self.update_all) - self.dialog.buttonCheckForUpdates.clicked.connect( - self.manually_check_for_updates - ) self.dialog.buttonClose.clicked.connect(self.dialog.reject) self.dialog.buttonUpdateCache.clicked.connect(self.on_buttonUpdateCache_clicked) self.dialog.buttonPauseUpdate.clicked.connect(self.stop_update) @@ -384,7 +380,6 @@ class CommandAddonManager: self.packageDetails.update.connect(self.update) self.packageDetails.back.connect(self.on_buttonBack_clicked) self.packageDetails.update_status.connect(self.status_updated) - self.packageDetails.check_for_update.connect(self.check_for_update) # center the dialog over the FreeCAD window mw = FreeCADGui.getMainWindow() @@ -759,7 +754,7 @@ class CommandAddonManager: def status_updated(self, repo: AddonManagerRepo) -> None: self.item_model.reload_item(repo) - if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: + if repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: self.packages_with_updates.append(repo) self.enable_updates(len(self.packages_with_updates)) @@ -881,7 +876,7 @@ class CommandAddonManager: missing_external_addons = [] for dep in deps.required_external_addons: - if dep.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: + if dep.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: missing_external_addons.append(dep) # Now check the loaded addons to see if we are missing an internal workbench: @@ -1181,62 +1176,16 @@ class CommandAddonManager: def update(self, repo: AddonManagerRepo) -> None: self.install(repo) - def check_for_update(self, repo: AddonManagerRepo) -> None: - """Check a single repo for available updates asynchronously""" - - if ( - hasattr(self, "update_check_single_worker") - and self.update_check_single_worker - ): - if self.update_check_single_worker.isRunning(): - self.update_check_single_worker.blockSignals(True) - self.update_check_single_worker.requestInterrupt() - self.update_check_single_worker.wait() - - self.update_check_single_worker = CheckSingleWorker(repo.name) - self.update_check_single_worker.updateAvailable.connect( - lambda update_available: self.mark_repo_update_available( - repo, update_available - ) - ) - self.update_check_single_worker.start() - def mark_repo_update_available( self, repo: AddonManagerRepo, available: bool ) -> None: if available: - repo.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE + repo.set_status(AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE) else: - repo.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + repo.set_status(AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE) self.item_model.reload_item(repo) self.packageDetails.show_repo(repo) - def manually_check_for_updates(self) -> None: - if hasattr(self, "check_worker"): - thread = self.check_worker - if thread: - if not thread.isFinished(): - self.do_next_startup_phase() - return - self.dialog.buttonCheckForUpdates.setText( - translate("AddonsInstaller", "Checking for updates...") - ) - self.dialog.buttonCheckForUpdates.setEnabled(False) - self.show_progress_widgets() - self.current_progress_region = 1 - self.number_of_progress_regions = 1 - self.check_worker = CheckWorkbenchesForUpdatesWorker(self.item_model.repos) - self.check_worker.finished.connect(self.manual_update_check_complete) - self.check_worker.progress_made.connect(self.update_progress_bar) - self.check_worker.update_status.connect(self.status_updated) - self.check_worker.start() - - def manual_update_check_complete(self) -> None: - self.dialog.buttonUpdateAll.show() - self.dialog.buttonCheckForUpdates.hide() - self.enable_updates(len(self.packages_with_updates)) - self.hide_progress_widgets() - def update_all(self) -> None: """Asynchronously apply all available updates: individual failures are noted, but do not stop other updates""" @@ -1325,11 +1274,9 @@ class CommandAddonManager: for installed_repo in self.subupdates_succeeded: if not installed_repo.repo_type == AddonManagerRepo.RepoType.MACRO: self.restart_required = True - installed_repo.update_status = ( - AddonManagerRepo.UpdateStatus.PENDING_RESTART - ) + installed_repo.set_status(AddonManagerRepo.UpdateStatus.PENDING_RESTART) else: - installed_repo.update_status = ( + installed_repo.set_status( AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE ) self.item_model.reload_item(installed_repo) @@ -1404,10 +1351,10 @@ class CommandAddonManager: QtWidgets.QMessageBox.Close, ) if repo.repo_type != AddonManagerRepo.RepoType.MACRO: - repo.update_status = AddonManagerRepo.UpdateStatus.PENDING_RESTART + repo.set_status(AddonManagerRepo.UpdateStatus.PENDING_RESTART) self.restart_required = True else: - repo.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + repo.set_status(AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE) self.item_model.reload_item(repo) self.packageDetails.show_repo(repo) @@ -1504,13 +1451,19 @@ class CommandAddonManager: ) # Second, run the Addon's "uninstall.py" script, if it exists - uninstall_script = os.path.join(clonedir,"uninstall.py") + uninstall_script = os.path.join(clonedir, "uninstall.py") if os.path.exists(uninstall_script): try: - with open(uninstall_script, 'r') as f: + with open(uninstall_script, "r") as f: exec(f.read()) except Exception: - FreeCAD.Console.PrintError(translate("AddonsInstaller","Execution of Addon's uninstall.py script failed. Proceeding with uninstall...") + "\n") + FreeCAD.Console.PrintError( + translate( + "AddonsInstaller", + "Execution of Addon's uninstall.py script failed. Proceeding with uninstall...", + ) + + "\n" + ) if os.path.exists(clonedir): shutil.rmtree(clonedir, onerror=self.remove_readonly) diff --git a/src/Mod/AddonManager/AddonManagerRepo.py b/src/Mod/AddonManager/AddonManagerRepo.py index a5aeb0ee52..69bcad1862 100644 --- a/src/Mod/AddonManager/AddonManagerRepo.py +++ b/src/Mod/AddonManager/AddonManagerRepo.py @@ -23,9 +23,12 @@ import FreeCAD import os -from typing import Dict, Set, List +from urllib.parse import urlparse +from typing import Dict, Set +from threading import Lock from addonmanager_macro import Macro +import addonmanager_utilities as utils translate = FreeCAD.Qt.translate @@ -54,6 +57,7 @@ class AddonManagerRepo: NO_UPDATE_AVAILABLE = 2 UPDATE_AVAILABLE = 3 PENDING_RESTART = 4 + CANNOT_CHECK = 5 # If we don't have git, etc. def __lt__(self, other): if self.__class__ is other.__class__: @@ -71,6 +75,8 @@ class AddonManagerRepo: return "Update available" elif self.value == 4: return "Restart required" + elif self.value == 5: + return "Can't check" class Dependencies: def __init__(self): @@ -90,16 +96,32 @@ class AddonManagerRepo: self.display_name = self.name self.url = url.strip() self.branch = branch.strip() - self.update_status = status self.python2 = False self.obsolete = False self.rejected = False self.repo_type = AddonManagerRepo.RepoType.WORKBENCH self.description = None self.tags = set() # Just a cache, loaded from Metadata + + # To prevent multiple threads from running git actions on this repo at the same time + self.git_lock = Lock() + + # To prevent multiple threads from accessing the status at the same time + self.status_lock = Lock() + self.set_status(status) + from addonmanager_utilities import construct_git_url - if "github" in self.url or "gitlab" in self.url or "salsa" in self.url: + # The url should never end in ".git", so strip it if it's there + parsed_url = urlparse(self.url) + if parsed_url.path.endswith(".git"): + self.url = parsed_url.scheme + parsed_url.path[:-4] + if parsed_url.query: + self.url += "?" + parsed_url.query + if parsed_url.fragment: + self.url += "#" + parsed_url.fragment + + if utils.recognized_git_location(self): self.metadata_url = construct_git_url(self, "package.xml") else: self.metadata_url = None @@ -334,3 +356,11 @@ class AddonManagerRepo: for dep in self.blocks: if dep in all_repos: deps.blockers[dep] = all_repos[dep] + + def status(self): + with self.status_lock: + return self.update_status + + def set_status(self, status): + with self.status_lock: + self.update_status = status diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index 56a12e7983..5e1d1c7584 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -20,6 +20,7 @@ SET(AddonManager_SRCS NetworkManager.py package_list.py package_details.py + loading.html ) SOURCE_GROUP("" FILES ${AddonManager_SRCS}) diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 395618e718..3d65e336d4 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -146,27 +146,24 @@ def get_zip_url(repo): def recognized_git_location(repo) -> bool: parsed_url = urlparse(repo.url) - if ( - parsed_url.netloc == "github.com" - or parsed_url.netloc == "framagit.com" - or parsed_url.netloc == "gitlab.com" - or parsed_url.netloc == "salsa.debian.org" - ): + if parsed_url.netloc in [ + "github.com", + "framagit.org", + "gitlab.com", + "salsa.debian.org", + ]: return True else: return False def construct_git_url(repo, filename): - "Returns a direct download link to a file in an online Git repo: works with github, gitlab, and framagit" + "Returns a direct download link to a file in an online Git repo: works with github, gitlab, framagit, and salsa.debian.org" parsed_url = urlparse(repo.url) - if parsed_url.netloc == "github.com" or parsed_url.netloc == "framagit.com": + if parsed_url.netloc == "github.com": return f"{repo.url}/raw/{repo.branch}/{filename}" - elif parsed_url.netloc == "gitlab.com": - return f"{repo.url}/-/raw/{repo.branch}/{filename}" - elif parsed_url.netloc == "salsa.debian.org": - # e.g. https://salsa.debian.org/joha2/pyrate/-/raw/master/package.xml + elif parsed_url.netloc in ["gitlab.com", "framagit.org", "salsa.debian.org"]: return f"{repo.url}/-/raw/{repo.branch}/{filename}" else: FreeCAD.Console.PrintLog( @@ -216,39 +213,12 @@ def get_readme_html_url(repo): parsedUrl = urlparse(repo.url) if parsedUrl.netloc == "github.com": return f"{repo.url}/blob/{repo.branch}/README.md" + elif parsedUrl.netloc in ["gitlab.com", "salsa.debian.org", "framagit.org"]: + return f"{repo.url}/-/blob/{repo.branch}/README.md" else: return None -def get_readme_regex(repo): - """Return a regex string that extracts the contents to be displayed in the description - panel of the Addon manager, from raw HTML data (the readme's html rendering usually)""" - - parsedUrl = urlparse(repo.url) - if parsedUrl.netloc == "github.com": - return "(.*?)" - else: - return None - - -def fix_relative_links(text, base_url): - """Replace markdown image relative links with - absolute ones using the base URL""" - - new_text = "" - for line in text.splitlines(): - for link in re.findall(r"!\[.*?\]\((.*?)\)", line) + re.findall( - r"src\s*=\s*[\"'](.+?)[\"']", line - ): - parts = link.split("/") - if len(parts) < 2 or not re.match(r"^http|^www|^.+\.|^/", parts[0]): - newlink = os.path.join(base_url, link.lstrip("./")) - line = line.replace(link, newlink) - FreeCAD.Console.PrintLog("Debug: replaced " + link + " with " + newlink) - new_text = new_text + "\n" + line - return new_text - - def repair_git_repo(repo_url: str, clone_dir: str) -> None: # Repair addon installed with raw download by adding the .git # directory to it diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 901ba3be49..4869168107 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -359,6 +359,29 @@ class LoadMacrosFromCacheWorker(QtCore.QThread): self.add_macro_signal.emit(repo) +class CheckSingleUpdateWorker(QtCore.QObject): + """This worker is a little different from the others: the actual recommended way of + running in a QThread is to make a worker object that gets moved into the thread.""" + + update_status = QtCore.Signal(int) + + def __init__(self, repo: AddonManagerRepo, parent: QtCore.QObject = None): + super().__init__(parent) + self.repo = repo + + def do_work(self): + # Borrow the function from another class: + checker = UpdateChecker() + if self.repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH: + checker.check_workbench(self.repo) + elif self.repo.repo_type == AddonManagerRepo.RepoType.MACRO: + checker.check_macro(self.repo) + elif self.repo.repo_type == AddonManagerRepo.RepoType.PACKAGE: + checker.check_package(self.repo) + + self.update_status.emit(self.repo.update_status) + + class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): """This worker checks for available updates for all workbenches""" @@ -372,61 +395,73 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): def run(self): - if NOGIT or not have_git: - return self.current_thread = QtCore.QThread.currentThread() self.basedir = FreeCAD.getUserAppDataDir() self.moddir = self.basedir + os.sep + "Mod" + checker = UpdateChecker() count = 1 for repo in self.repos: if self.current_thread.isInterruptionRequested(): return self.progress_made.emit(count, len(self.repos)) count += 1 - if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: + if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED: if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH: - self.check_workbench(repo) + checker.check_workbench(repo) + self.update_status.emit(repo) elif repo.repo_type == AddonManagerRepo.RepoType.MACRO: - self.check_macro(repo) + checker.check_macro(repo) + self.update_status.emit(repo) elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE: - self.check_package(repo) + checker.check_package(repo) + self.update_status.emit(repo) + + +class UpdateChecker: + def __init__(self): + self.basedir = FreeCAD.getUserAppDataDir() + self.moddir = self.basedir + os.sep + "Mod" def check_workbench(self, wb): if not have_git or NOGIT: + wb.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK) return clonedir = self.moddir + os.sep + wb.name if os.path.exists(clonedir): # mark as already installed AND already checked for updates if not os.path.exists(clonedir + os.sep + ".git"): - utils.repair_git_repo(wb.url, clonedir) - gitrepo = git.Git(clonedir) - try: - gitrepo.fetch() - except Exception: - FreeCAD.Console.PrintWarning( - "AddonManager: " - + translate( - "AddonsInstaller", - "Unable to fetch git updates for workbench {}", - ).format(wb.name) - ) - else: + with wb.git_lock: + utils.repair_git_repo(wb.url, clonedir) + with wb.git_lock: + gitrepo = git.Git(clonedir) try: - if "git pull" in gitrepo.status(): - wb.update_status = ( - AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE - ) - else: - wb.update_status = ( - AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE - ) - self.update_status.emit(wb) + gitrepo.fetch() except Exception: FreeCAD.Console.PrintWarning( - translate("AddonsInstaller", "git fetch failed for {}").format( - wb.name - ) + "AddonManager: " + + translate( + "AddonsInstaller", + "Unable to fetch git updates for workbench {}", + ).format(wb.name) ) + wb.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK) + else: + try: + if "git pull" in gitrepo.status(): + wb.set_status( + AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE + ) + else: + wb.set_status( + AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE + ) + except Exception: + FreeCAD.Console.PrintWarning( + translate( + "AddonsInstaller", "git fetch failed for {}" + ).format(wb.name) + ) + wb.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK) def check_package(self, package: AddonManagerRepo) -> None: clonedir = self.moddir + os.sep + package.name @@ -436,9 +471,8 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): # If there is no package.xml file, then it's because the package author added it after the last time # the local installation was updated. By definition, then, there is an update available, if only to # download the new XML file. - package.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE + package.set_status(AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE) package.installed_version = None - self.update_status.emit(package) return else: package.updated_timestamp = os.path.getmtime(installed_metadata_file) @@ -448,14 +482,11 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): # Packages are considered up-to-date if the metadata version matches. Authors should update # their version string when they want the addon manager to alert users of a new version. if package.metadata.Version != installed_metadata.Version: - package.update_status = ( - AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE - ) + package.set_status(AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE) else: - package.update_status = ( + package.set_status( AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE ) - self.update_status.emit(package) except Exception: FreeCAD.Console.PrintWarning( translate( @@ -464,6 +495,7 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): ).format(name=installed_metadata_file) + "\n" ) + package.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK) def check_macro(self, macro_wrapper: AddonManagerRepo) -> None: # Make sure this macro has its code downloaded: @@ -486,6 +518,7 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): ).format(name=macro_wrapper.macro.name) + "\n" ) + macro_wrapper.set_status(AddonManagerRepo.UpdateStatus.CANNOT_CHECK) return hasher1 = hashlib.sha1() @@ -511,12 +544,9 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): else: return if new_sha1 == old_sha1: - macro_wrapper.update_status = ( - AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE - ) + macro_wrapper.set_status(AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE) else: - macro_wrapper.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE - self.update_status.emit(macro_wrapper) + macro_wrapper.set_status(AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE) class FillMacroListWorker(QtCore.QThread): @@ -801,189 +831,6 @@ class CacheMacroCode(QtCore.QThread): self.failed.append(macro_name) -class ShowWorker(QtCore.QThread): - """This worker retrieves info of a given workbench""" - - status_message = QtCore.Signal(str) - readme_updated = QtCore.Signal(str) - update_status = QtCore.Signal(AddonManagerRepo) - - def __init__(self, repo, cache_path): - - QtCore.QThread.__init__(self) - self.repo = repo - self.cache_path = cache_path - - def run(self): - self.status_message.emit( - translate("AddonsInstaller", "Retrieving description...") - ) - u = None - url = self.repo.url - self.status_message.emit( - translate("AddonsInstaller", "Retrieving info from {}").format(str(url)) - ) - desc = "" - regex = utils.get_readme_regex(self.repo) - if regex: - # extract readme from html via regex - readmeurl = utils.get_readme_html_url(self.repo) - if not readmeurl: - FreeCAD.Console.PrintLog(f"README not found for {url}\n") - p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(readmeurl) - if not p: - FreeCAD.Console.PrintLog(f"Debug: README not found at {readmeurl}\n") - p = p.data().decode("utf8") - readme = re.findall(regex, p, flags=re.MULTILINE | re.DOTALL) - if readme: - desc = readme[0] - else: - FreeCAD.Console.PrintLog(f"Debug: README not found at {readmeurl}\n") - else: - # convert raw markdown using lib - readmeurl = utils.get_readme_url(self.repo) - if not readmeurl: - FreeCAD.Console.PrintLog(f"Debug: README not found for {url}\n") - p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(readmeurl) - if p: - p = p.data().decode("utf8") - desc = utils.fix_relative_links(p, readmeurl.rsplit("/README.md")[0]) - if not NOMARKDOWN and have_markdown: - desc = markdown.markdown(desc, extensions=["md_in_html"]) - else: - message = """ -
- -""" - message += translate("AddonsInstaller", "Raw markdown displayed") - message += "

" - message += translate( - "AddonsInstaller", "Python Markdown library is missing." - ) - message += "

" + desc + "
" - desc = message - else: - FreeCAD.Console.PrintLog("Debug: README not found at {readmeurl}\n") - if desc == "": - # fall back to the description text - p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url) - if not p: - return - p = p.data().decode("utf8") - descregex = utils.get_desc_regex(self.repo) - if descregex: - desc = re.findall(descregex, p) - if desc: - desc = desc[0] - if not desc: - desc = "Unable to retrieve addon description" - self.repo.description = desc - if QtCore.QThread.currentThread().isInterruptionRequested(): - return - message = desc - if self.repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: - # Addon is installed but we haven't checked it yet, so let's check if it has an update - upd = False - # checking for updates - if not NOGIT and have_git: - repo = self.repo - clonedir = ( - FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + repo.name - ) - if os.path.exists(clonedir): - if not os.path.exists(clonedir + os.sep + ".git"): - utils.repair_git_repo(self.repo.url, clonedir) - gitrepo = git.Git(clonedir) - gitrepo.fetch() - if "git pull" in gitrepo.status(): - upd = True - if upd: - self.repo.update_status = AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE - else: - self.repo.update_status = ( - AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE - ) - self.update_status.emit(self.repo) - - if QtCore.QThread.currentThread().isInterruptionRequested(): - return - self.readme_updated.emit(message) - self.mustLoadImages = True - label = self.loadImages(message, self.repo.url, self.repo.name) - if label: - self.readme_updated.emit(label) - if QtCore.QThread.currentThread().isInterruptionRequested(): - return - - def stopImageLoading(self): - "this stops the image loading process and allow the thread to terminate earlier" - - self.mustLoadImages = False - - def loadImages(self, message, url, wbName): - "checks if the given page contains images and downloads them" - - # QTextBrowser cannot display online images. So we download them - # here, and replace the image link in the html code with the - # downloaded version - - imagepaths = re.findall('= 260: - remainChars = 259 - (len(store) + len(wbName) + 1) - storename = os.path.join(store, wbName + name[-remainChars:]) - if not os.path.exists(storename): - try: - imagedata = NetworkManager.AM_NETWORK_MANAGER.blocking_get( - path - ) - if not imagedata: - raise Exception - except Exception: - FreeCAD.Console.PrintLog( - "AddonManager: Debug: Error retrieving image from " - + path - ) - else: - try: - f = open(storename, "wb") - except OSError: - # ecryptfs (and probably not only ecryptfs) has - # lower length limit for path - storename = storename[-140:] - f = open(storename, "wb") - f.write(imagedata.data()) - f.close() - message = message.replace( - 'src="' + origpath, - 'src="file:///' + storename.replace("\\", "/"), - ) - os.remove(os.path.join(store, "download_in_progress")) - return message - return None - - class GetMacroDetailsWorker(QtCore.QThread): """Retrieve the macro details for a macro""" @@ -1126,33 +973,34 @@ class InstallWorkbenchWorker(QtCore.QThread): + str(self.repo.name) + "\n" ) - if not os.path.exists(clonedir + os.sep + ".git"): - utils.repair_git_repo(self.repo.url, clonedir) - repo = git.Git(clonedir) - try: - repo.pull() # Refuses to take a progress object? - answer = translate( - "AddonsInstaller", - "Workbench successfully updated. Please restart FreeCAD to apply the changes.", - ) - except Exception as e: - answer = ( - translate("AddonsInstaller", "Error updating module ") - + self.repo.name - + " - " - + translate("AddonsInstaller", "Please fix manually") - + " -- \n" - ) - answer += str(e) - self.failure.emit(self.repo, answer) - else: - # Update the submodules for this repository - repo_sms = git.Repo(clonedir) - self.status_message.emit("Updating submodules...") - for submodule in repo_sms.submodules: - submodule.update(init=True, recursive=True) - self.update_metadata() - self.success.emit(self.repo, answer) + with self.repo.git_lock: + if not os.path.exists(clonedir + os.sep + ".git"): + utils.repair_git_repo(self.repo.url, clonedir) + repo = git.Git(clonedir) + try: + repo.pull() # Refuses to take a progress object? + answer = translate( + "AddonsInstaller", + "Workbench successfully updated. Please restart FreeCAD to apply the changes.", + ) + except Exception as e: + answer = ( + translate("AddonsInstaller", "Error updating module ") + + self.repo.name + + " - " + + translate("AddonsInstaller", "Please fix manually") + + " -- \n" + ) + answer += str(e) + self.failure.emit(self.repo, answer) + else: + # Update the submodules for this repository + repo_sms = git.Repo(clonedir) + self.status_message.emit("Updating submodules...") + for submodule in repo_sms.submodules: + submodule.update(init=True, recursive=True) + self.update_metadata() + self.success.emit(self.repo, answer) def run_git_clone(self, clonedir: str) -> None: self.status_message.emit("Checking module dependencies...") @@ -1168,21 +1016,39 @@ class InstallWorkbenchWorker(QtCore.QThread): self.status_message.emit("Cloning module...") current_thread = QtCore.QThread.currentThread() - # NOTE: There is no way to interrupt this process in GitPython: someday we should - # support pygit2/libgit2 so we can actually interrupt this properly. - repo = git.Repo.clone_from(self.repo.url, clonedir, progress=self.git_progress) - if current_thread.isInterruptionRequested(): - return + FreeCAD.Console.PrintMessage("Cloning repo...\n") + if self.repo.git_lock.locked(): + FreeCAD.Console.PrintMessage("Waiting for lock to be released to us...\n") + if not self.repo.git_lock.acquire(timeout=2): + FreeCAD.Console.PrintError("Timeout waiting for a lock on the git process, failed to clone repo\n") + return + else: + self.repo.git_lock.release() - # Make sure to clone all the submodules as well - if repo.submodules: - repo.submodule_update(recursive=True) + with self.repo.git_lock: + FreeCAD.Console.PrintMessage("Lock acquired...\n") + # NOTE: There is no way to interrupt this process in GitPython: someday we should + # support pygit2/libgit2 so we can actually interrupt this properly. + repo = git.Repo.clone_from( + self.repo.url, clonedir, progress=self.git_progress + ) + FreeCAD.Console.PrintMessage("Initial clone complete...\n") + if current_thread.isInterruptionRequested(): + return - if current_thread.isInterruptionRequested(): - return + # Make sure to clone all the submodules as well + if repo.submodules: + FreeCAD.Console.PrintMessage("Updating submodules...\n") + repo.submodule_update(recursive=True) - if self.repo.branch in repo.heads: - repo.heads[self.repo.branch].checkout() + if current_thread.isInterruptionRequested(): + return + + if self.repo.branch in repo.heads: + FreeCAD.Console.PrintMessage("Checking out HEAD...\n") + repo.heads[self.repo.branch].checkout() + + FreeCAD.Console.PrintMessage("Clone complete\n") answer = translate( "AddonsInstaller", @@ -1465,38 +1331,6 @@ class DependencyInstallationWorker(QtCore.QThread): self.success.emit() -class CheckSingleWorker(QtCore.QThread): - """Worker to check for updates for a single addon""" - - updateAvailable = QtCore.Signal(bool) - - def __init__(self, name): - - QtCore.QThread.__init__(self) - self.name = name - - def run(self): - - if not have_git or NOGIT: - return - FreeCAD.Console.PrintLog( - "Checking for available updates of the " + self.name + " addon\n" - ) - addondir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", self.name) - if os.path.exists(addondir): - if os.path.exists(addondir + os.sep + ".git"): - gitrepo = git.Git(addondir) - try: - gitrepo.fetch() - if "git pull" in gitrepo.status(): - self.updateAvailable.emit(True) - return - except Exception: - # can fail for any number of reasons, ex. not being online - pass - self.updateAvailable.emit(False) - - class UpdateMetadataCacheWorker(QtCore.QThread): "Scan through all available packages and see if our local copy of package.xml needs to be updated" diff --git a/src/Mod/AddonManager/loading.html b/src/Mod/AddonManager/loading.html new file mode 100644 index 0000000000..fd445821b2 --- /dev/null +++ b/src/Mod/AddonManager/loading.html @@ -0,0 +1,93 @@ + + + + + + + +
Loading...
+ + \ No newline at end of file diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index e2b315e297..7a7e1d62dc 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -24,21 +24,20 @@ from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import * +from PySide2.QtWebEngineWidgets import * import os -import shutil -from datetime import date, timedelta import FreeCAD import addonmanager_utilities as utils -from addonmanager_workers import ShowWorker, GetMacroDetailsWorker +from addonmanager_workers import GetMacroDetailsWorker, CheckSingleUpdateWorker from AddonManagerRepo import AddonManagerRepo -import inspect - translate = FreeCAD.Qt.translate +show_javascript_console_output = False + class PackageDetails(QWidget): @@ -57,9 +56,10 @@ class PackageDetails(QWidget): self.worker = None self.repo = None + self.status_update_thread = None self.ui.buttonBack.clicked.connect(self.back.emit) - self.ui.buttonRefresh.clicked.connect(self.refresh) + self.ui.buttonBack.clicked.connect(self.clear_web_view) self.ui.buttonExecute.clicked.connect(lambda: self.execute.emit(self.repo)) self.ui.buttonInstall.clicked.connect(lambda: self.install.emit(self.repo)) self.ui.buttonUninstall.clicked.connect(lambda: self.uninstall.emit(self.repo)) @@ -67,34 +67,64 @@ class PackageDetails(QWidget): self.ui.buttonCheckForUpdate.clicked.connect( lambda: self.check_for_update.emit(self.repo) ) + self.ui.webView.loadStarted.connect(self.load_started) + self.ui.webView.loadProgress.connect(self.load_progress) + self.ui.webView.loadFinished.connect(self.load_finished) + + loading_html_file = os.path.join(os.path.dirname(__file__), "loading.html") + with open(loading_html_file, "r", errors="ignore") as f: + html = f.read() + self.ui.loadingLabel.setHtml(html) + self.ui.loadingLabel.show() + self.ui.webView.hide() def show_repo(self, repo: AddonManagerRepo, reload: bool = False) -> None: - self.repo = repo + # If this is the same repo we were already showing, we do not have to do the + # expensive refetch unless reload is true + if self.repo != repo or reload: + self.repo = repo + self.ui.loadingLabel.show() + self.ui.webView.hide() + self.ui.progressBar.show() - if self.worker is not None: - if not self.worker.isFinished(): - self.worker.requestInterruption() - self.worker.wait() + if self.worker is not None: + if not self.worker.isFinished(): + self.worker.requestInterruption() + self.worker.wait() - # Always load bare macros from scratch, we need to grab their code, which isn't cached - force_reload = reload - if repo.repo_type == AddonManagerRepo.RepoType.MACRO: - force_reload = True + if repo.repo_type == AddonManagerRepo.RepoType.MACRO: + self.show_macro(repo) + self.ui.buttonExecute.show() + elif repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH: + self.show_workbench(repo) + self.ui.buttonExecute.hide() + elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE: + self.show_package(repo) + self.ui.buttonExecute.hide() - self.check_and_clean_cache(force_reload) + if self.status_update_thread is not None: + if not self.status_update_thread.isFinished(): + self.status_update_thread.requestInterruption() + self.status_update_thread.wait() - if repo.repo_type == AddonManagerRepo.RepoType.MACRO: - self.show_macro(repo) - self.ui.buttonExecute.show() - elif repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH: - self.show_workbench(repo) - self.ui.buttonExecute.hide() - elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE: - self.show_package(repo) - self.ui.buttonExecute.hide() + if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED: + self.status_update_thread = QThread() + self.status_update_worker = CheckSingleUpdateWorker(repo, self) + self.status_update_worker.moveToThread(self.status_update_thread) + self.status_update_thread.finished.connect( + self.status_update_worker.deleteLater + ) + self.check_for_update.connect(self.status_update_worker.do_work) + self.status_update_worker.update_status.connect(self.display_repo_status) + self.status_update_thread.start() + self.check_for_update.emit(self.repo) - if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: + self.display_repo_status(self.repo.update_status) + + def display_repo_status(self, status): + repo = self.repo + if status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: version = repo.installed_version date = "" @@ -125,7 +155,7 @@ class PackageDetails(QWidget): translate("AddonsInstaller", "Installed") + ". " ) - if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: + if status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: if repo.metadata: installed_version_string += ( "" @@ -151,21 +181,19 @@ class PackageDetails(QWidget): ) + "." ) - elif ( - repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE - ): + elif status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: installed_version_string += ( translate("AddonsInstaller", "This is the latest version available") + "." ) - elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: + elif status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: installed_version_string += ( translate( "AddonsInstaller", "Updated, please restart FreeCAD to use" ) + "." ) - elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: + elif status == AddonManagerRepo.UpdateStatus.UNCHECKED: pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") autocheck = pref.GetBool("AutoCheck", False) @@ -181,7 +209,7 @@ class PackageDetails(QWidget): installed_version_string += "" self.ui.labelPackageDetails.setText(installed_version_string) - if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: + if repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: self.ui.labelPackageDetails.setStyleSheet( "color:" + utils.attention_color_string() ) @@ -206,27 +234,27 @@ class PackageDetails(QWidget): self.ui.labelPackageDetails.hide() self.ui.labelInstallationLocation.hide() - if repo.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: + if status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: self.ui.buttonInstall.show() self.ui.buttonUninstall.hide() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.hide() - elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: + elif status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.hide() - elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: + elif status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.show() self.ui.buttonCheckForUpdate.hide() - elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: + elif status == AddonManagerRepo.UpdateStatus.UNCHECKED: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.show() - elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: + elif status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() @@ -255,127 +283,155 @@ class PackageDetails(QWidget): else: self.ui.labelWarningInfo.hide() - @classmethod - def cache_path(self, repo: AddonManagerRepo) -> str: - cache_path = FreeCAD.getUserCachePath() - full_path = os.path.join(cache_path, "AddonManager", repo.name) - return full_path - - def check_and_clean_cache(self, force: bool = False) -> None: - cache_path = PackageDetails.cache_path(self.repo) - readme_cache_file = os.path.join(cache_path, "README.html") - readme_images_path = os.path.join(cache_path, "Images") - download_interrupted_sentinel = os.path.join( - readme_images_path, "download_in_progress" - ) - download_interrupted = os.path.isfile(download_interrupted_sentinel) - if os.path.isfile(readme_cache_file): - pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") - days_between_updates = pref.GetInt("DaysBetweenUpdates", 2 ^ 32) - timestamp = os.path.getmtime(readme_cache_file) - last_cache_update = date.fromtimestamp(timestamp) - delta_update = timedelta(days=days_between_updates) - if ( - date.today() >= last_cache_update + delta_update - or download_interrupted - or force - ): - if force: - FreeCAD.Console.PrintLog( - f"Forced README cache update for {self.repo.name}\n" - ) - elif download_interrupted: - FreeCAD.Console.PrintLog( - f"Restarting interrupted README download for {self.repo.name}\n" - ) - else: - FreeCAD.Console.PrintLog( - f"Cache expired, downloading README for {self.repo.name} again\n" - ) - os.remove(readme_cache_file) - if os.path.isdir(readme_images_path): - shutil.rmtree(readme_images_path) - - def refresh(self): - self.check_and_clean_cache(force=True) - self.show_repo(self.repo) - - def show_cached_readme(self, repo: AddonManagerRepo) -> bool: - """Attempts to show a cached readme, returns true if there was a cache, or false if not""" - - cache_path = PackageDetails.cache_path(repo) - readme_cache_file = os.path.join(cache_path, "README.html") - if os.path.isfile(readme_cache_file): - with open(readme_cache_file, "rb") as f: - data = f.read() - self.ui.textBrowserReadMe.setText(data.decode()) - return True - return False - def show_workbench(self, repo: AddonManagerRepo) -> None: """loads information of a given workbench""" - - if not self.show_cached_readme(repo): - self.ui.textBrowserReadMe.setText( - translate( - "AddonsInstaller", "Fetching README.md from package repository" - ) - ) - self.worker = ShowWorker(repo, PackageDetails.cache_path(repo)) - self.worker.readme_updated.connect( - lambda desc: self.cache_readme(repo, desc) - ) - self.worker.readme_updated.connect( - lambda desc: self.ui.textBrowserReadMe.setText(desc) - ) - self.worker.update_status.connect(self.update_status.emit) - self.worker.update_status.connect(self.show) - self.worker.start() + url = utils.get_readme_html_url(repo) + self.ui.webView.load(QUrl(url)) + self.ui.urlBar.setText(url) def show_package(self, repo: AddonManagerRepo) -> None: """Show the details for a package (a repo with a package.xml metadata file)""" - if not self.show_cached_readme(repo): - self.ui.textBrowserReadMe.setText( - translate( - "AddonsInstaller", "Fetching README.md from package repository" - ) - ) - self.worker = ShowWorker(repo, PackageDetails.cache_path(repo)) - self.worker.readme_updated.connect( - lambda desc: self.cache_readme(repo, desc) - ) - self.worker.readme_updated.connect( - lambda desc: self.ui.textBrowserReadMe.setText(desc) - ) - self.worker.update_status.connect(self.update_status.emit) - self.worker.update_status.connect(self.show) - self.worker.start() + readme_url = None + if repo.metadata: + urls = repo.metadata.Urls + for url in urls: + if url["type"] == "readme": + readme_url = url["location"] + break + if not readme_url: + readme_url = utils.get_readme_html_url(repo) + self.ui.webView.load(QUrl(readme_url)) + self.ui.urlBar.setText(readme_url) def show_macro(self, repo: AddonManagerRepo) -> None: """loads information of a given macro""" - if not self.show_cached_readme(repo): - self.ui.textBrowserReadMe.setText( - translate( - "AddonsInstaller", "Fetching README.md from package repository" - ) - ) - self.worker = GetMacroDetailsWorker(repo) - self.worker.readme_updated.connect( - lambda desc: self.cache_readme(repo, desc) - ) - self.worker.readme_updated.connect( - lambda desc: self.ui.textBrowserReadMe.setText(desc) - ) - self.worker.start() + self.ui.webView.load(QUrl(repo.macro.url)) + self.ui.urlBar.setText(repo.macro.url) - def cache_readme(self, repo: AddonManagerRepo, readme: str) -> None: - cache_path = PackageDetails.cache_path(repo) - readme_cache_file = os.path.join(cache_path, "README.html") - os.makedirs(cache_path, exist_ok=True) - with open(readme_cache_file, "wb") as f: - f.write(readme.encode()) + # We need to populate the macro information... may as well do it while the user reads the wiki page + self.worker = GetMacroDetailsWorker(repo) + self.worker.start() + + def run_javascript(self): + """Modify the page for a README to optimize for viewing in a smaller window""" + + s = """ +( function() { + const url = new URL (window.location); + const body = document.getElementsByTagName("body")[0]; + if (url.hostname === "github.com") { + const articles = document.getElementsByTagName("article"); + if (articles.length > 0) { + const article = articles[0]; + body.appendChild (article); + body.style.padding = "1em"; + let sibling = article.previousSibling; + while (sibling) { + sibling.remove(); + sibling = article.previousSibling; + } + } + } else if (url.hostname === "gitlab.com" || + url.hostname === "framagit.org" || + url.hostname === "salsa.debian.org") { + // These all use the GitLab page display... + const articles = document.getElementsByTagName("article"); + if (articles.length > 0) { + const article = articles[0]; + body.appendChild (article); + body.style.padding = "1em"; + let sibling = article.previousSibling; + while (sibling) { + sibling.remove(); + sibling = article.previousSibling; + } + } + } else if (url.hostname === "wiki.freecad.org" || + url.hostname === "wiki.freecadweb.org") { + const first_heading = document.getElementById('firstHeading'); + const body_content = document.getElementById('bodyContent'); + const new_node = document.createElement("div"); + new_node.appendChild(first_heading); + new_node.appendChild(body_content); + body.appendChild(new_node); + let sibling = new_node.previousSibling; + while (sibling) { + sibling.remove(); + sibling = new_node.previousSibling; + } + } +} +) () +""" + self.ui.webView.page().runJavaScript(s) + + def clear_web_view(self): + self.ui.webView.setHtml("

Loading...

") + + def load_started(self): + self.ui.progressBar.show() + self.ui.progressBar.setValue(0) + + def load_progress(self, progress: int): + self.ui.progressBar.setValue(progress) + + def load_finished(self, load_succeeded: bool): + self.ui.loadingLabel.hide() + self.ui.webView.show() + self.ui.progressBar.hide() + url = self.ui.webView.url() + if load_succeeded: + # It says it succeeded, but it might have only succeeded in loading a "Page not found" page! + title = self.ui.webView.title() + path_components = url.path().split("/") + expected_content = path_components[-1] + if url.host() == "github.com" and expected_content not in title: + self.show_error_for(url) + elif title == "": + self.show_error_for(url) + else: + self.run_javascript() + else: + self.show_error_for(url) + + def show_error_for(self, url: QUrl) -> None: + m = translate( + "AddonsInstaller", "Could not load README data from URL {}" + ).format(url.toString()) + html = f"

{m}

" + self.ui.webView.setHtml(html) + + +class RestrictedWebPage(QWebEnginePage): + """A class that follows links to FreeCAD wiki pages, but opens all other clicked links in the system web browser""" + + def __init__(self, parent): + super().__init__(parent) + self.settings().setAttribute(QWebEngineSettings.ErrorPageEnabled, False) + + def acceptNavigationRequest(self, url, _type, isMainFrame): + if _type == QWebEnginePage.NavigationTypeLinkClicked: + + # See if the link is to a FreeCAD Wiki page -- if so, follow it, otherwise ask the OS to open it + if url.host() == "wiki.freecad.org" or url.host() == "wiki.freecadweb.org": + return super().acceptNavigationRequest(url, _type, isMainFrame) + else: + QDesktopServices.openUrl(url) + return False + return super().acceptNavigationRequest(url, _type, isMainFrame) + + def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID): + global show_javascript_console_output + if show_javascript_console_output: + tag = translate("AddonsInstaller", "Page JavaScript reported") + if level == QWebEnginePage.InfoMessageLevel: + FreeCAD.Console.PrintMessage(f"{tag} {lineNumber}: {message}\n") + elif level == QWebEnginePage.WarningMessageLevel: + FreeCAD.Console.PrintWarning(f"{tag} {lineNumber}: {message}\n") + elif level == QWebEnginePage.ErrorMessageLevel: + FreeCAD.Console.PrintError(f"{tag} {lineNumber}: {message}\n") class Ui_PackageDetails(object): @@ -391,14 +447,8 @@ class Ui_PackageDetails(object): self.buttonBack.setIcon( QIcon.fromTheme("back", QIcon(":/icons/button_left.svg")) ) - self.buttonRefresh = QToolButton(PackageDetails) - self.buttonRefresh.setObjectName("buttonRefresh") - self.buttonRefresh.setIcon( - QIcon.fromTheme("refresh", QIcon(":/icons/view-refresh.svg")) - ) self.layoutDetailsBackButton.addWidget(self.buttonBack) - self.layoutDetailsBackButton.addWidget(self.buttonRefresh) self.horizontalSpacer = QSpacerItem( 40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum @@ -449,12 +499,34 @@ class Ui_PackageDetails(object): self.verticalLayout_2.addWidget(self.labelWarningInfo) - self.textBrowserReadMe = QTextBrowser(PackageDetails) - self.textBrowserReadMe.setObjectName("textBrowserReadMe") - self.textBrowserReadMe.setOpenExternalLinks(True) - self.textBrowserReadMe.setOpenLinks(True) + sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) - self.verticalLayout_2.addWidget(self.textBrowserReadMe) + self.webView = QWebEngineView(PackageDetails) + self.webView.setObjectName("webView") + self.webView.setSizePolicy(sizePolicy1) + self.webView.setPage(RestrictedWebPage(PackageDetails)) + + self.verticalLayout_2.addWidget(self.webView) + + self.loadingLabel = QWebEngineView(PackageDetails) + self.loadingLabel.setObjectName("loadingLabel") + self.loadingLabel.setSizePolicy(sizePolicy1) + + self.verticalLayout_2.addWidget(self.loadingLabel) + + self.progressBar = QProgressBar(PackageDetails) + self.progressBar.setObjectName("progressBar") + self.progressBar.setTextVisible(False) + + self.verticalLayout_2.addWidget(self.progressBar) + + self.urlBar = QLineEdit(PackageDetails) + self.urlBar.setObjectName("urlBar") + self.urlBar.setReadOnly(True) + + self.verticalLayout_2.addWidget(self.urlBar) self.retranslateUi(PackageDetails) @@ -462,7 +534,7 @@ class Ui_PackageDetails(object): # setupUi - def retranslateUi(self, PackageDetails): + def retranslateUi(self, _): self.buttonBack.setText("") self.buttonInstall.setText( QCoreApplication.translate("AddonsInstaller", "Install", None) @@ -484,12 +556,5 @@ class Ui_PackageDetails(object): "AddonsInstaller", "Return to package list", None ) ) - self.buttonRefresh.setToolTip( - QCoreApplication.translate( - "AddonsInstaller", - "Delete cached version of this README and re-download", - None, - ) - ) # retranslateUi diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index 59a674c1c0..ef181ebad8 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -226,7 +226,7 @@ class PackageListItemModel(QAbstractListModel): row = index.row() self.write_lock.acquire() if role == PackageListItemModel.StatusUpdateRole: - self.repos[row].update_status = value + self.repos[row].set_status(value) self.dataChanged.emit( self.index(row, 2), self.index(row, 2), @@ -408,13 +408,13 @@ class PackageListItemDelegate(QStyledItemDelegate): """Get a single-line string listing details about the installed version and date""" result = "" - if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: + if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED: result = translate("AddonsInstaller", "Installed") - elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: + elif repo.status() == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: result = translate("AddonsInstaller", "Up-to-date") - elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: + elif repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: result = translate("AddonsInstaller", "Update available") - elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: + elif repo.status() == AddonManagerRepo.UpdateStatus.PENDING_RESTART: result = translate("AddonsInstaller", "Pending restart") return result @@ -424,7 +424,7 @@ class PackageListItemDelegate(QStyledItemDelegate): result = "" installed_version_string = "" - if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: + if repo.status() != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: if repo.installed_version: installed_version_string = ( "\n" + translate("AddonsInstaller", "Installed version") + ": " @@ -453,20 +453,20 @@ class PackageListItemDelegate(QStyledItemDelegate): ) available_version_string += repo.metadata.Version - if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED: + if repo.status() == AddonManagerRepo.UpdateStatus.UNCHECKED: result = translate("AddonsInstaller", "Installed") result += installed_version_string result += installed_date_string - elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: + elif repo.status() == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: result = translate("AddonsInstaller", "Up-to-date") result += installed_version_string result += installed_date_string - elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: + elif repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: result = translate("AddonsInstaller", "Update available") result += installed_version_string result += installed_date_string result += available_version_string - elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: + elif repo.status() == AddonManagerRepo.UpdateStatus.PENDING_RESTART: result = translate("AddonsInstaller", "Pending restart") return result @@ -530,18 +530,18 @@ class PackageListFilter(QSortFilterProxyModel): return False if self.status == StatusFilter.INSTALLED: - if data.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: + if data.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: return False elif self.status == StatusFilter.NOT_INSTALLED: - if data.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: + if data.status() != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: return False elif self.status == StatusFilter.UPDATE_AVAILABLE: - if data.update_status != AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: + if data.status() != AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: return False # If it's not installed, check to see if it's Py2 only if ( - data.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED + data.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED and self.hide_py2 and data.python2 ): @@ -549,7 +549,7 @@ class PackageListFilter(QSortFilterProxyModel): # If it's not installed, check to see if it's marked obsolete if ( - data.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED + data.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED and self.hide_obsolete and data.obsolete ):