Addon Manager: Bug fixes for detailed display

This commit is contained in:
Chris Hennes
2021-12-17 21:42:38 -06:00
parent 787b1e0f56
commit acfde1a4b6
6 changed files with 149 additions and 52 deletions

View File

@@ -83,6 +83,7 @@ class CommandAddonManager:
"install_worker",
"update_metadata_cache_worker",
"update_all_worker",
"update_check_single_worker"
]
lock = threading.Lock()
@@ -236,6 +237,7 @@ 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()
@@ -440,14 +442,14 @@ class CommandAddonManager:
def cache_package(self, repo: AddonManagerRepo):
if not hasattr(self, "package_cache"):
self.package_cache = []
self.package_cache.append(repo.to_cache())
self.package_cache = {}
self.package_cache[repo.name] = repo.to_cache()
def write_package_cache(self):
package_cache_path = self.get_cache_file_name("package_cache.json")
with open(package_cache_path, "w") as f:
f.write(json.dumps(self.package_cache))
self.package_cache = []
if hasattr(self, "package_cache"):
package_cache_path = self.get_cache_file_name("package_cache.json")
with open(package_cache_path, "w") as f:
f.write(json.dumps(self.package_cache))
def activate_table_widgets(self) -> None:
self.packageList.setEnabled(True)
@@ -514,9 +516,7 @@ class CommandAddonManager:
"""Called when the named package has either new metadata or a new icon (or both)"""
with self.lock:
cache_path = os.path.join(
FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata", repo.name
)
self.cache_package(repo)
repo.icon = self.get_icon(repo, update=True)
self.item_model.reload_item(repo)
@@ -527,6 +527,10 @@ class CommandAddonManager:
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
autocheck = pref.GetBool("AutoCheck", False)
if not autocheck:
FreeCAD.Console.PrintMessage(translate(
"AddonsInstaller",
"Addon Manager: Skipping update check because AutoCheck user preference is False"
) + "\n")
self.do_next_startup_phase()
return
if not self.packages_with_updates:
@@ -703,6 +707,28 @@ 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.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
else:
repo.update_status = AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
self.item_model.reload_item(repo)
self.packageDetails.show_repo(repo)
def update_all(self) -> None:
"""Asynchronously apply all available updates: individual failures are noted, but do not stop other updates"""

View File

@@ -72,6 +72,7 @@ class AddonManagerRepo:
def __init__(self, name: str, url: str, status: UpdateStatus, branch: str):
self.name = name.strip()
self.display_name = self.name
self.url = url.strip()
self.branch = branch.strip()
self.update_status = status
@@ -122,9 +123,17 @@ class AddonManagerRepo:
else:
status = AddonManagerRepo.UpdateStatus.NOT_INSTALLED
instance = AddonManagerRepo(data["name"], data["url"], status, data["branch"])
instance.display_name = data["display_name"]
instance.repo_type = AddonManagerRepo.RepoType(data["repo_type"])
instance.description = data["description"]
instance.cached_icon_filename = data["cached_icon_filename"]
if instance.repo_type == AddonManagerRepo.RepoType.PACKAGE:
# There must be a cached metadata file, too
cached_package_xml_file = os.path.join(
FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata", instance.name
)
if os.path.isfile(cached_package_xml_file):
instance.load_metadata_file(cached_package_xml_file)
return instance
def to_cache(self) -> Dict:
@@ -132,6 +141,7 @@ class AddonManagerRepo:
return {
"name": self.name,
"display_name": self.display_name,
"url": self.url,
"branch": self.branch,
"repo_type": int(self.repo_type),
@@ -139,6 +149,24 @@ class AddonManagerRepo:
"cached_icon_filename": self.get_cached_icon_filename(),
}
def load_metadata_file (self, file:str) -> None:
if os.path.isfile(file):
metadata = FreeCAD.Metadata(file)
self.set_metadata(metadata)
def set_metadata (self, metadata:FreeCAD.Metadata) -> None:
self.metadata = metadata
self.display_name = metadata.Name
self.repo_type = AddonManagerRepo.RepoType.PACKAGE
self.description = metadata.Description
for url in metadata.Urls:
if "type" in url and url["type"] == "repository":
self.url = url["location"]
if "branch" in url:
self.branch = url["branch"]
else:
self.branch = "master"
def contains_workbench(self) -> bool:
"""Determine if this package contains (or is) a workbench"""

View File

@@ -25,6 +25,7 @@ import FreeCAD
import tempfile
import os
import hashlib
from typing import Dict, List
from PySide2 import QtCore, QtNetwork
from PySide2.QtCore import QObject
@@ -47,8 +48,8 @@ class MetadataDownloadWorker(QObject):
updated = QtCore.Signal(AddonManagerRepo)
def __init__(self, parent, repo, index):
"repo is an AddonManagerRepo object, and index is a dictionary of SHA1 hashes of the package.xml files in the cache"
def __init__(self, parent, repo:AddonManagerRepo, index:Dict[str,str]):
""" repo is an AddonManagerRepo object, and index is a dictionary of SHA1 hashes of the package.xml files in the cache """
super().__init__(parent)
self.repo = repo
@@ -59,8 +60,9 @@ class MetadataDownloadWorker(QObject):
self.last_sha1 = ""
self.url = self.repo.metadata_url
def start_fetch(self, network_manager):
"Asynchronously begin the network access. Intended as a set-and-forget black box for downloading metadata."
def start_fetch(self, network_manager:QtNetwork.QNetworkAccessManager):
"""Asynchronously begin the network access. Intended as a set-and-forget black box for downloading metadata."""
self.request = QtNetwork.QNetworkRequest(QtCore.QUrl(self.url))
self.request.setAttribute(
QtNetwork.QNetworkRequest.RedirectPolicyAttribute,
@@ -76,20 +78,21 @@ class MetadataDownloadWorker(QObject):
if not self.fetch_task.isFinished():
self.fetch_task.abort()
def on_redirect(self, url):
def on_redirect(self, _):
# For now just blindly follow all redirects
self.fetch_task.redirectAllowed.emit()
def on_ssl_error(self, reply, errors):
def on_ssl_error(self, reply:str, errors:List[str]):
FreeCAD.Console.PrintWarning(f"Error with encrypted connection:\n")
FreeCAD.Console.PrintWarning(reply)
for error in errors:
FreeCAD.Console.PrintWarning(error)
def resolve_fetch(self):
"Called when the data fetch completed, either with an error, or if it found the metadata file"
""" Called when the data fetch completed, either with an error, or if it found the metadata file """
if self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.NoError:
FreeCAD.Console.PrintMessage(
FreeCAD.Console.PrintLog(
f"Found a metadata file for {self.repo.name}\n"
)
self.repo.repo_type = AddonManagerRepo.RepoType.PACKAGE
@@ -128,8 +131,8 @@ class MetadataDownloadWorker(QObject):
):
pass
else:
FreeCAD.Console.PrintWarning(
f"Failed to connect to {self.url}:\n {self.fetch_task.error()}\n"
FreeCAD.Console.PrintWarning(
translate("AddonsInstaller", "Failed to connect to") + f" {self.url}:\n {self.fetch_task.error()}\n"
)
def update_local_copy(self, new_xml):
@@ -151,8 +154,8 @@ class MetadataDownloadWorker(QObject):
if not icon:
# If there is no icon set for the entire package, see if there are
# any workbenches, which
# are required to have icons, and grab the first one we find:
# any workbenches, which are required to have icons, and grab the first
# one we find:
content = self.repo.metadata.Content
if "workbench" in content:
wb = content["workbench"][0]

View File

@@ -120,8 +120,8 @@ class UpdateWorker(QtCore.QThread):
if "obsolete" in j and "Mod" in j["obsolete"]:
obsolete = j["obsolete"]["Mod"]
if "reject_listed" in j and "Macro" in j["reject_listed"]:
macros_reject_list = j["reject_listed"]["Macro"]
if "blacklisted" in j and "Macro" in j["blacklisted"]:
macros_reject_list = j["blacklisted"]["Macro"]
if "py2only" in j and "Mod" in j["py2only"]:
py2only = j["py2only"]["Mod"]
@@ -157,16 +157,21 @@ class UpdateWorker(QtCore.QThread):
if name.lower().endswith(".git"):
name = name[:-4]
if name in package_names:
# We already have this one cached since it's a package
# We already have something with this name, skip this one
continue
package_names.append(name)
addondir = moddir + os.sep + name
if os.path.exists(addondir) and os.listdir(addondir):
state = AddonManagerRepo.UpdateStatus.UNCHECKED
else:
state = AddonManagerRepo.UpdateStatus.NOT_INSTALLED
self.addon_repo.emit(
AddonManagerRepo(name, addon["url"], state, addon["branch"])
)
repo = AddonManagerRepo(name, addon["url"], state, addon["branch"])
md_file = os.path.join(addondir,"package.xml")
if os.path.isfile(md_file):
repo.load_metadata_file(md_file)
repo.installed_version = repo.metadata.Version
repo.updated_timestamp = os.path.getmtime(md_file)
self.addon_repo.emit(repo)
# querying official addons
u = utils.urlopen(
@@ -193,8 +198,9 @@ class UpdateWorker(QtCore.QThread):
if self.current_thread.isInterruptionRequested():
return
if name in package_names:
# We've already got this info since it's a package or a custom repo
# We already have something with this name, skip this one
continue
package_names.append(name)
if branch is None or len(branch) == 0:
branch = "master"
url = url.split(".git")[0]
@@ -204,7 +210,13 @@ class UpdateWorker(QtCore.QThread):
state = AddonManagerRepo.UpdateStatus.UNCHECKED
else:
state = AddonManagerRepo.UpdateStatus.NOT_INSTALLED
self.addon_repo.emit(AddonManagerRepo(name, url, state, branch))
repo = AddonManagerRepo(name, url, state, branch)
md_file = os.path.join(addondir,"package.xml")
if os.path.isfile(md_file):
repo.load_metadata_file(md_file)
repo.installed_version = repo.metadata.Version
repo.updated_timestamp = os.path.getmtime(md_file)
self.addon_repo.emit(repo)
self.status_message.emit(
translate("AddonsInstaller", "Workbenches list was updated.")
@@ -231,7 +243,7 @@ class LoadPackagesFromCacheWorker(QtCore.QThread):
data = f.read()
if data:
dict_data = json.loads(data)
for item in dict_data:
for item in dict_data.values():
if QtCore.QThread.currentThread().isInterruptionRequested():
return
repo = AddonManagerRepo.from_cache(item)
@@ -240,8 +252,13 @@ class LoadPackagesFromCacheWorker(QtCore.QThread):
)
if os.path.isfile(repo_metadata_cache_path):
try:
repo.metadata = FreeCAD.Metadata(repo_metadata_cache_path)
repo.load_metadata_file(repo_metadata_cache_path)
repo.installed_version = repo.metadata.Version
repo.updated_timestamp = os.path.getmtime(repo_metadata_cache_path)
except Exception:
FreeCAD.Console.PrintWarning(
translate("AddonsInstaller","Failed loading") + f"{repo_metadata_cache_path}\n"
)
pass
self.addon_repo.emit(repo)
self.done.emit()
@@ -382,6 +399,7 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
package.updated_timestamp = os.path.getmtime(installed_metadata_file)
try:
installed_metadata = FreeCAD.Metadata(installed_metadata_file)
package.set_metadata(installed_metadata)
package.installed_version = installed_metadata.Version
# 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.
@@ -1096,7 +1114,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
"FreeCAD to apply the changes.",
)
else:
self.emit.failure(self.repo, answer)
self.failure.emit(self.repo, answer)
return
if self.repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
@@ -1199,14 +1217,15 @@ class InstallWorkbenchWorker(QtCore.QThread):
)
message += ": " + pl + ", "
if message and (not ok):
message = translate(
final_message = translate(
"AddonsInstaller",
"Some errors were found that prevent installation of this workbench",
)
message += ": <b>" + message + "</b>. "
message += translate(
final_message += ": <b>" + message + "</b>. "
final_message += translate(
"AddonsInstaller", "Please install the missing components first."
)
message = final_message
return ok, message
def check_package_dependencies(self):
@@ -1295,7 +1314,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
basedir = FreeCAD.getUserAppDataDir()
package_xml = os.path.join(basedir, "Mod", self.repo.name, "package.xml")
if os.path.isfile(package_xml):
self.repo.metadata = FreeCAD.Metadata(package_xml)
self.repo.load_metadata_file(package_xml)
self.repo.installed_version = self.repo.metadata.Version
self.repo.updated_timestamp = datetime.now().timestamp()

View File

@@ -46,6 +46,7 @@ class PackageDetails(QWidget):
update = Signal(AddonManagerRepo)
execute = Signal(AddonManagerRepo)
update_status = Signal(AddonManagerRepo)
check_for_update = Signal(AddonManagerRepo)
def __init__(self, parent=None):
super().__init__(parent)
@@ -61,6 +62,7 @@ class PackageDetails(QWidget):
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:
@@ -149,9 +151,18 @@ class PackageDetails(QWidget):
+ "."
)
elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
installed_version_string += (
translate("AddonsInstaller", "Update check in progress") + "."
)
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)
@@ -171,22 +182,27 @@ class PackageDetails(QWidget):
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()
@classmethod
def cache_path(self, repo: AddonManagerRepo) -> str:
@@ -354,6 +370,11 @@ class Ui_PackageDetails(object):
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")
@@ -390,6 +411,9 @@ class Ui_PackageDetails(object):
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)
)

View File

@@ -28,8 +28,6 @@ from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
import datetime
from typing import Dict, Union
from enum import IntEnum
import threading
@@ -130,6 +128,7 @@ class PackageList(QWidget):
self.item_filter.setFilterRegularExpression(text_filter)
def set_view_style(self, style: ListDisplayStyle) -> None:
self.item_model.layoutAboutToBeChanged.emit()
self.item_delegate.set_view(style)
if style == ListDisplayStyle.COMPACT:
self.ui.listPackages.setSpacing(2)
@@ -172,17 +171,17 @@ class PackageListItemModel(QAbstractListModel):
if self.repos[row].repo_type == AddonManagerRepo.RepoType.PACKAGE:
tooltip = (
translate("AddonsInstaller", "Click for details about package")
+ f" '{self.repos[row].name}'"
+ f" '{self.repos[row].display_name}'"
)
elif self.repos[row].repo_type == AddonManagerRepo.RepoType.WORKBENCH:
tooltip = (
translate("AddonsInstaller", "Click for details about workbench")
+ f" '{self.repos[row].name}'"
+ f" '{self.repos[row].display_name}'"
)
elif self.repos[row].repo_type == AddonManagerRepo.RepoType.MACRO:
tooltip = (
translate("AddonsInstaller", "Click for details about macro")
+ f" '{self.repos[row].name}'"
+ f" '{self.repos[row].display_name}'"
)
return tooltip
elif role == PackageListItemModel.DataAccessRole:
@@ -301,11 +300,11 @@ class PackageListItemDelegate(QStyledItemDelegate):
repo = index.data(PackageListItemModel.DataAccessRole)
if self.displayStyle == ListDisplayStyle.EXPANDED:
self.widget = self.expanded
self.widget.ui.labelPackageName.setText(f"<h1>{repo.name}</h1>")
self.widget.ui.labelPackageName.setText(f"<h1>{repo.display_name}</h1>")
self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(48, 48)))
else:
self.widget = self.compact
self.widget.ui.labelPackageName.setText(f"<b>{repo.name}</b>")
self.widget.ui.labelPackageName.setText(f"<b>{repo.display_name}</b>")
self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(16, 16)))
self.widget.ui.labelIcon.setText("")
@@ -409,7 +408,7 @@ class PackageListItemDelegate(QStyledItemDelegate):
return result
def paint(
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
self, painter: QPainter, option: QStyleOptionViewItem, _: QModelIndex
):
painter.save()
self.widget.resize(option.rect.size())
@@ -436,9 +435,7 @@ class PackageListFilter(QSortFilterProxyModel):
l = self.sourceModel().data(left, PackageListItemModel.DataAccessRole)
r = self.sourceModel().data(right, PackageListItemModel.DataAccessRole)
lname = l.name if l.metadata is None else l.metadata.Name
rname = r.name if r.metadata is None else r.metadata.Name
return lname.lower() < rname.lower()
return l.display_name.lower() < r.display_name.lower()
def filterAcceptsRow(self, row, parent=QModelIndex()):
index = self.sourceModel().createIndex(row, 0)
@@ -453,8 +450,8 @@ class PackageListFilter(QSortFilterProxyModel):
if not data.contains_preference_pack():
return False
name = data.name if data.metadata is None else data.metadata.Name
desc = data.description if not data.metadata else data.metadata.Description
name = data.display_name
desc = data.description
re = self.filterRegularExpression()
if re.isValid():
re.setPatternOptions(QRegularExpression.CaseInsensitiveOption)