def show_repo(self, repo: AddonManagerRepo, reload: bool = False) -> None: # 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 if HAS_QTWEBENGINE: self.ui.loadingLabel.show() self.ui.slowLoadLabel.hide() self.ui.webView.setHtml("<html><body>Loading...</body></html>") self.ui.webView.hide() self.ui.progressBar.show() self.timeout = QTimer.singleShot( 6000, self.long_load_running) # Six seconds else: self.ui.missingWebViewLabel.setStyleSheet( "color:" + utils.warning_color_string()) if self.worker is not None: if not self.worker.isFinished(): self.worker.requestInterruption() self.worker.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 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.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) self.display_repo_status(self.repo.update_status)
class PackageDetails(QWidget): back = Signal() install = Signal(AddonManagerRepo) uninstall = Signal(AddonManagerRepo) update = Signal(AddonManagerRepo) execute = Signal(AddonManagerRepo) update_status = Signal(AddonManagerRepo) check_for_update = Signal(AddonManagerRepo) def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_PackageDetails() self.ui.setupUi(self) self.worker = None self.repo = None self.status_update_thread = None self.ui.buttonBack.clicked.connect(self.back.emit) 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)) self.ui.buttonUpdate.clicked.connect( lambda: self.update.emit(self.repo)) self.ui.buttonCheckForUpdate.clicked.connect( lambda: self.check_for_update.emit(self.repo)) self.ui.buttonChangeBranch.clicked.connect(self.change_branch_clicked) if HAS_QTWEBENGINE: 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: # 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 if HAS_QTWEBENGINE: self.ui.loadingLabel.show() self.ui.slowLoadLabel.hide() self.ui.webView.setHtml("<html><body>Loading...</body></html>") self.ui.webView.hide() self.ui.progressBar.show() self.timeout = QTimer.singleShot( 6000, self.long_load_running) # Six seconds else: self.ui.missingWebViewLabel.setStyleSheet( "color:" + utils.warning_color_string()) if self.worker is not None: if not self.worker.isFinished(): self.worker.requestInterruption() self.worker.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 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.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) self.display_repo_status(self.repo.update_status) def display_repo_status(self, status): repo = self.repo self.set_change_branch_button_state() if status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: version = repo.installed_version date = "" installed_version_string = "<h3>" if repo.updated_timestamp: date = (QDateTime.fromTime_t( repo.updated_timestamp).date().toString( Qt.SystemLocaleShortDate)) if version and date: installed_version_string += ( translate("AddonsInstaller", "Version {version} installed on {date}").format( version=version, date=date) + ". ") elif version: installed_version_string += (translate( "AddonsInstaller", "Version {version} installed") + ". ").format(version=version) elif date: installed_version_string += ( translate("AddonsInstaller", "Installed on {date}") + ". ").format(date=date) else: installed_version_string += ( translate("AddonsInstaller", "Installed") + ". ") if status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: if repo.metadata: installed_version_string += ("<b>" + translate( "AddonsInstaller", "On branch {}, update available to version", ).format(repo.branch) + " ") installed_version_string += repo.metadata.Version installed_version_string += ".</b>" elif repo.macro and repo.macro.version: installed_version_string += ("<b>" + translate( "AddonsInstaller", "Update available to version") + " ") installed_version_string += repo.macro.version installed_version_string += ".</b>" else: installed_version_string += ("<b>" + translate( "AddonsInstaller", "An update is available", ) + ".</b>") elif status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: detached_head = False branch = repo.branch if have_git and repo.repo_type != AddonManagerRepo.RepoType.MACRO: basedir = FreeCAD.getUserAppDataDir() moddir = os.path.join(basedir, "Mod", repo.name) if os.path.exists(os.path.join(moddir, ".git")): gitrepo = git.Repo(moddir) branch = gitrepo.head.ref.name detached_head = gitrepo.head.is_detached if detached_head: installed_version_string += (translate( "AddonsInstaller", "Git tag '{}' checked out, no updates possible", ).format(branch) + ".") else: installed_version_string += (translate( "AddonsInstaller", "This is the latest version available for branch {}", ).format(branch) + ".") elif status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: installed_version_string += ( translate("AddonsInstaller", "Updated, please restart FreeCAD to use") + ".") elif status == AddonManagerRepo.UpdateStatus.UNCHECKED: pref = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Addons") autocheck = pref.GetBool("AutoCheck", False) if autocheck: installed_version_string += (translate( "AddonsInstaller", "Update check in progress") + ".") else: installed_version_string += ( translate("AddonsInstaller", "Automatic update checks disabled") + ".") installed_version_string += "</h3>" self.ui.labelPackageDetails.setText(installed_version_string) if repo.status() == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: self.ui.labelPackageDetails.setStyleSheet( "color:" + utils.attention_color_string()) else: self.ui.labelPackageDetails.setStyleSheet( "color:" + utils.bright_color_string()) self.ui.labelPackageDetails.show() if repo.macro is not None: moddir = FreeCAD.getUserMacroDir(True) else: basedir = FreeCAD.getUserAppDataDir() moddir = os.path.join(basedir, "Mod", repo.name) installationLocationString = ( translate("AddonsInstaller", "Installation location") + ": " + moddir) self.ui.labelInstallationLocation.setText( installationLocationString) self.ui.labelInstallationLocation.show() else: self.ui.labelPackageDetails.hide() self.ui.labelInstallationLocation.hide() if status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: self.ui.buttonInstall.show() self.ui.buttonUninstall.hide() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.hide() elif status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.hide() elif status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.show() self.ui.buttonCheckForUpdate.hide() elif status == AddonManagerRepo.UpdateStatus.UNCHECKED: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.show() elif status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.hide() required_version = self.requires_newer_freecad() if repo.obsolete: self.ui.labelWarningInfo.show() self.ui.labelWarningInfo.setText("<h1>" + translate( "AddonsInstaller", "WARNING: This addon is obsolete") + "</h1>") self.ui.labelWarningInfo.setStyleSheet( "color:" + utils.warning_color_string()) elif repo.python2: self.ui.labelWarningInfo.show() self.ui.labelWarningInfo.setText("<h1>" + translate( "AddonsInstaller", "WARNING: This addon is Python 2 Only") + "</h1>") self.ui.labelWarningInfo.setStyleSheet( "color:" + utils.warning_color_string()) elif required_version: self.ui.labelWarningInfo.show() self.ui.labelWarningInfo.setText("<h1>" + translate( "AddonsInstaller", "WARNING: This addon requires FreeCAD ") + required_version + "</h1>") self.ui.labelWarningInfo.setStyleSheet( "color:" + utils.warning_color_string()) else: self.ui.labelWarningInfo.hide() def requires_newer_freecad(self) -> Optional[str]: # If it's not installed, check to see if it's for a newer version of FreeCAD if (self.repo.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED and self.repo.metadata): # Only hide if ALL content items require a newer version, otherwise # it's possible that this package actually provides versions of itself # for newer and older versions first_supported_version = ( self.repo.metadata.getFirstSupportedFreeCADVersion()) if first_supported_version is not None: required_version = first_supported_version.split(".") fc_major = int(FreeCAD.Version()[0]) fc_minor = int(FreeCAD.Version()[1]) if int(required_version[0]) > fc_major: return first_supported_version elif int(required_version[0] ) == fc_major and len(required_version) > 1: if int(required_version[1]) > fc_minor: return first_supported_version return None def set_change_branch_button_state(self): """The change branch button is only available for installed Addons that have a .git directory and in runs where the GitPython import is available.""" self.ui.buttonChangeBranch.hide() pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") show_switcher = pref.GetBool("ShowBranchSwitcher", False) if not show_switcher: return # Is this repo installed? If not, return. if self.repo.status() == AddonManagerRepo.UpdateStatus.NOT_INSTALLED: return # Is it a Macro? If so, return: if self.repo.repo_type == AddonManagerRepo.RepoType.MACRO: return # Can we actually switch branches? If not, return. if not have_git: return # Is there a .git subdirectory? If not, return. basedir = FreeCAD.getUserAppDataDir() path_to_git = os.path.join(basedir, "Mod", self.repo.name, ".git") if not os.path.isdir(path_to_git): return # If all four above checks passed, then it's possible for us to switch # branches, if there are any besides the one we are on: show the button self.ui.buttonChangeBranch.show() def show_workbench(self, repo: AddonManagerRepo) -> None: """loads information of a given workbench""" url = utils.get_readme_html_url(repo) if HAS_QTWEBENGINE: self.ui.webView.load(QUrl(url)) self.ui.urlBar.setText(url) else: readme_data = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url) text = readme_data.data().decode("utf8") self.ui.textBrowserReadMe.setHtml(text) def show_package(self, repo: AddonManagerRepo) -> None: """Show the details for a package (a repo with a package.xml metadata file)""" 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) if HAS_QTWEBENGINE: self.ui.webView.load(QUrl(readme_url)) self.ui.urlBar.setText(readme_url) else: readme_data = NetworkManager.AM_NETWORK_MANAGER.blocking_get( readme_url) text = readme_data.data().decode("utf8") self.ui.textBrowserReadMe.setHtml(text) def show_macro(self, repo: AddonManagerRepo) -> None: """loads information of a given macro""" if HAS_QTWEBENGINE: self.ui.webView.load(QUrl(repo.macro.url)) self.ui.urlBar.setText(repo.macro.url) else: readme_data = NetworkManager.AM_NETWORK_MANAGER.blocking_get( repo.macro.url) text = readme_data.data().decode("utf8") self.ui.textBrowserReadMe.setHtml(text) # 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 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.slowLoadLabel.hide() self.ui.webView.show() self.ui.progressBar.hide() url = self.ui.webView.url() if (hasattr(self, "timeout") and hasattr(self.timeout, "isActive") and self.timeout.isActive()): self.timeout.stop() 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 long_load_running(self): if hasattr(self.ui, "webView") and self.ui.webView.isHidden(): self.ui.slowLoadLabel.show() self.ui.loadingLabel.hide() self.ui.webView.show() def show_error_for(self, url: QUrl) -> None: m = translate("AddonsInstaller", "Could not load README data from URL {}").format( url.toString()) html = f"<html><body><p>{m}</p></body></html>" self.ui.webView.setHtml(html) def change_branch_clicked(self) -> None: basedir = FreeCAD.getUserAppDataDir() path_to_repo = os.path.join(basedir, "Mod", self.repo.name) change_branch_dialog = ChangeBranchDialog(path_to_repo, self) change_branch_dialog.branch_changed.connect(self.branch_changed) change_branch_dialog.exec() def branch_changed(self, name: str) -> None: QMessageBox.information( self, translate("AddonsInstaller", "Success"), translate( "AddonsInstaller", "Branch change succeeded, please restart to use the new version.", ), ) # See if this branch has a package.xml file: basedir = FreeCAD.getUserAppDataDir() path_to_metadata = os.path.join(basedir, "Mod", self.repo.name, "package.xml") if os.path.isfile(path_to_metadata): self.repo.load_metadata_file(path_to_metadata) self.repo.installed_version = self.repo.metadata.Version else: self.repo.repo_type = AddonManagerRepo.RepoType.WORKBENCH self.repo.metadata = None self.repo.installed_version = None self.repo.updated_timestamp = QDateTime.currentDateTime( ).toSecsSinceEpoch() self.repo.branch = name self.repo.set_status(AddonManagerRepo.UpdateStatus.PENDING_RESTART) installed_version_string = "<h3>" installed_version_string += translate( "AddonsInstaller", "Changed to git ref '{}' -- please restart to use Addon.").format( name) installed_version_string += "</h3>" self.ui.labelPackageDetails.setText(installed_version_string) self.ui.labelPackageDetails.setStyleSheet( "color:" + utils.attention_color_string()) self.update_status.emit(self.repo)