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 ):