# -*- coding: utf-8 -*- # *************************************************************************** # * * # * Copyright (c) 2021 Chris Hennes * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import * import os import shutil from datetime import date, timedelta import FreeCAD from addonmanager_utilities import translate # this needs to be as is for pylupdate from addonmanager_workers import ShowWorker, GetMacroDetailsWorker from AddonManagerRepo import AddonManagerRepo import inspect 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.ui.buttonBack.clicked.connect(self.back.emit) self.ui.buttonRefresh.clicked.connect(self.refresh) 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) ) def show_repo(self, repo: AddonManagerRepo, reload: bool = False) -> None: self.repo = repo 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 self.check_and_clean_cache(force_reload) 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.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED: installed_version_string = "" if repo.installed_version: installed_version_string = translate("AddonsInstaller", "Version") + " " installed_version_string += repo.installed_version else: installed_version_string = ( translate( "AddonsInstaller", "Unknown version (no package.xml file found)" ) + " " ) if repo.updated_timestamp: installed_version_string += ( " " + translate("AddonsInstaller", "installed on") + " " ) installed_version_string += ( QDateTime.fromTime_t(repo.updated_timestamp) .date() .toString(Qt.SystemLocaleShortDate) ) installed_version_string += ". " else: installed_version_string += ( translate("AddonsInstaller", "installed") + ". " ) if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE: if repo.metadata: installed_version_string += ( "" + translate("AddonsInstaller", "Update available to version") + " " ) installed_version_string += repo.metadata.Version installed_version_string += "." else: installed_version_string += ( "" + translate( "AddonsInstaller", "Update available to unknown version (no package.xml file found)", ) + "." ) elif ( repo.update_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: installed_version_string += ( translate( "AddonsInstaller", "Updated, please restart FreeCAD to use" ) + "." ) elif repo.update_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") + "." ) basedir = FreeCAD.getUserAppDataDir() moddir = os.path.join(basedir, "Mod", repo.name) installed_version_string += ( "
" + translate("AddonsInstaller", "Installation location") + ": " + moddir ) self.ui.labelPackageDetails.setText(installed_version_string) self.ui.labelPackageDetails.show() else: self.ui.labelPackageDetails.hide() if repo.update_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: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.hide() elif repo.update_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: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.show() elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART: self.ui.buttonInstall.hide() self.ui.buttonUninstall.show() self.ui.buttonUpdate.hide() self.ui.buttonCheckForUpdate.hide() warningColorString = "rgb(255,0,0)" if hasattr(QApplication.instance(), "styleSheet"): # Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info if "dark" in QApplication.instance().styleSheet().lower(): warningColorString = "rgb(255,50,50)" else: warningColorString = "rgb(200,0,0)" if repo.obsolete: self.ui.labelWarningInfo.show() self.ui.labelWarningInfo.setText( "

" + translate("AddonsInstaller", "WARNING: This addon is obsolete") + "

" ) self.ui.labelWarningInfo.setStyleSheet("color:" + warningColorString) elif repo.python2: self.ui.labelWarningInfo.show() self.ui.labelWarningInfo.setText( "

" + translate("AddonsInstaller", "WARNING: This addon is Python 2 Only") + "

" ) self.ui.labelWarningInfo.setStyleSheet("color:" + warningColorString) 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() 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() 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() 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()) class Ui_PackageDetails(object): def setupUi(self, PackageDetails): if not PackageDetails.objectName(): PackageDetails.setObjectName("PackageDetails") self.verticalLayout_2 = QVBoxLayout(PackageDetails) self.verticalLayout_2.setObjectName("verticalLayout_2") self.layoutDetailsBackButton = QHBoxLayout() self.layoutDetailsBackButton.setObjectName("layoutDetailsBackButton") self.buttonBack = QToolButton(PackageDetails) self.buttonBack.setObjectName("buttonBack") 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 ) self.layoutDetailsBackButton.addItem(self.horizontalSpacer) self.buttonInstall = QPushButton(PackageDetails) self.buttonInstall.setObjectName("buttonInstall") self.layoutDetailsBackButton.addWidget(self.buttonInstall) self.buttonUninstall = QPushButton(PackageDetails) self.buttonUninstall.setObjectName("buttonUninstall") self.layoutDetailsBackButton.addWidget(self.buttonUninstall) self.buttonUpdate = QPushButton(PackageDetails) self.buttonUpdate.setObjectName("buttonUpdate") self.layoutDetailsBackButton.addWidget(self.buttonUpdate) self.buttonCheckForUpdate = QPushButton(PackageDetails) self.buttonCheckForUpdate.setObjectName("buttonCheckForUpdate") self.layoutDetailsBackButton.addWidget(self.buttonCheckForUpdate) self.buttonExecute = QPushButton(PackageDetails) self.buttonExecute.setObjectName("buttonExecute") self.layoutDetailsBackButton.addWidget(self.buttonExecute) self.verticalLayout_2.addLayout(self.layoutDetailsBackButton) self.labelPackageDetails = QLabel(PackageDetails) self.labelPackageDetails.hide() self.verticalLayout_2.addWidget(self.labelPackageDetails) self.labelWarningInfo = QLabel(PackageDetails) self.labelWarningInfo.hide() self.verticalLayout_2.addWidget(self.labelWarningInfo) self.textBrowserReadMe = QTextBrowser(PackageDetails) self.textBrowserReadMe.setObjectName("textBrowserReadMe") self.textBrowserReadMe.setOpenExternalLinks(True) self.textBrowserReadMe.setOpenLinks(True) self.verticalLayout_2.addWidget(self.textBrowserReadMe) self.retranslateUi(PackageDetails) QMetaObject.connectSlotsByName(PackageDetails) # setupUi def retranslateUi(self, PackageDetails): self.buttonBack.setText("") self.buttonInstall.setText( QCoreApplication.translate("AddonsInstaller", "Install", None) ) self.buttonUninstall.setText( QCoreApplication.translate("AddonsInstaller", "Uninstall", None) ) self.buttonUpdate.setText( QCoreApplication.translate("AddonsInstaller", "Update", None) ) self.buttonCheckForUpdate.setText( QCoreApplication.translate("AddonsInstaller", "Check for Update", None) ) self.buttonExecute.setText( QCoreApplication.translate("AddonsInstaller", "Run Macro", None) ) self.buttonBack.setToolTip( QCoreApplication.translate( "AddonsInstaller", "Return to package list", None ) ) self.buttonRefresh.setToolTip( QCoreApplication.translate( "AddonsInstaller", "Delete cached version of this README and re-download", None, ) ) # retranslateUi