Files
create/src/Mod/AddonManager/package_details.py
2022-01-01 09:16:32 -06:00

468 lines
19 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
# * *
# * 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 += (
"<b>"
+ translate("AddonsInstaller", "Update available to version")
+ " "
)
installed_version_string += repo.metadata.Version
installed_version_string += ".</b>"
else:
installed_version_string += (
"<b>"
+ translate(
"AddonsInstaller",
"Update available to unknown version (no package.xml file found)",
)
+ ".</b>"
)
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 += (
"<br/>"
+ 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(
"<h1>"
+ translate("AddonsInstaller", "WARNING: This addon is obsolete")
+ "</h1>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + warningColorString)
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:" + 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