diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py
index 0420e6413c..07a655ec53 100644
--- a/src/Mod/AddonManager/AddonManager.py
+++ b/src/Mod/AddonManager/AddonManager.py
@@ -28,7 +28,9 @@ import os
import shutil
import stat
import tempfile
+from datetime import date, timedelta
from typing import Dict, Union
+from enum import Enum
from PySide2 import QtGui, QtCore, QtWidgets
import FreeCADGui
@@ -37,6 +39,8 @@ from addonmanager_utilities import translate # this needs to be as is for pylup
from addonmanager_workers import *
import addonmanager_utilities as utils
import AddonManager_rc
+from package_list import PackageList, PackageListItemModel
+from package_details import PackageDetails
from AddonManagerRepo import AddonManagerRepo
__title__ = "FreeCAD Addon Manager Module"
@@ -113,79 +117,77 @@ class CommandAddonManager:
"AddonManager.ui"))
# cleanup the leftovers from previous runs
- self.macro_repo_dir = tempfile.mkdtemp()
+ self.macro_repo_dir = FreeCAD.getUserMacroDir()
self.packages_with_updates = []
self.startup_sequence = []
self.addon_removed = False
self.cleanup_workers()
- # restore window geometry and splitter state from stored state
+ # restore window geometry from stored state
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
- w = pref.GetInt("WindowWidth", 600)
- h = pref.GetInt("WindowHeight", 480)
+ w = pref.GetInt("WindowWidth", 800)
+ h = pref.GetInt("WindowHeight", 600)
self.dialog.resize(w, h)
- sl = pref.GetInt("SplitterLeft", 298)
- sr = pref.GetInt("SplitterRight", 274)
- self.dialog.splitter.setSizes([sl, sr])
+
+ # figure out our cache update frequency:
+ # -1: Only manual updates (default)
+ # 0: Update every launch
+ # >0: Update every n days
+ self.update_cache = False
+ days_between_updates = pref.GetInt("DaysBetweenUpdates", -1)
+ last_cache_update_string = pref.GetString("LastCacheUpdate", "never")
+ cache_path = FreeCAD.getUserCachePath()
+ am_path = os.path.join(cache_path,"AddonManager")
+ if last_cache_update_string == "never":
+ self.update_cache = True
+ elif days_between_updates > 0:
+ last_cache_update = date.fromisoformat(last_cache_update_string)
+ delta_update = timedelta(days=days_between_updates)
+ if date.today() >= last_cache_update + delta_update:
+ self.update_cache = True
+ elif days_between_updates == 0:
+ self.update_cache = True
+ elif not os.path.isdir(am_path):
+ self.update_cache = True
# Set up the listing of packages using the model-view-controller architecture
+ self.packageList = PackageList(self.dialog)
self.item_model = PackageListItemModel()
- self.item_delegate = PackageListIconDelegate()
- self.item_filter = PackageListFilter()
- self.item_filter.setSourceModel(self.item_model)
- self.dialog.tablePackages.setModel (self.item_filter)
- self.dialog.tablePackages.setItemDelegate(self.item_delegate)
- header = self.dialog.tablePackages.horizontalHeader()
- header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
- header.resizeSection(0,20)
- header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
- header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
- self.dialog.tablePackages.sortByColumn(1, QtCore.Qt.AscendingOrder) # Default to sorting alphabetically by name
+ self.packageList.setModel(self.item_model)
+ self.dialog.contentPlaceholder.hide()
+ self.dialog.layout().replaceWidget(self.dialog.contentPlaceholder, self.packageList)
+ self.packageList.show()
+
+ # Package details start out hidden
+ self.packageDetails = PackageDetails(self.dialog)
+ self.packageDetails.hide()
+ index = self.dialog.layout().indexOf(self.packageList)
+ self.dialog.layout().insertWidget(index, self.packageDetails)
# set nice icons to everything, by theme with fallback to FreeCAD icons
self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
- self.dialog.buttonUninstall.setIcon(QtGui.QIcon.fromTheme("cancel", QtGui.QIcon(":/icons/edit_Cancel.svg")))
- self.dialog.buttonInstall.setIcon(QtGui.QIcon.fromTheme("download", QtGui.QIcon(":/icons/edit_OK.svg")))
self.dialog.buttonUpdateAll.setIcon(QtGui.QIcon(":/icons/button_valid.svg"))
- self.dialog.buttonConfigure.setIcon(QtGui.QIcon(":/icons/preferences-system.svg"))
self.dialog.buttonClose.setIcon(QtGui.QIcon.fromTheme("close", QtGui.QIcon(":/icons/process-stop.svg")))
+ self.dialog.buttonPauseUpdate.setIcon(QtGui.QIcon.fromTheme("pause", QtGui.QIcon(":/icons/media-playback-stop.svg")))
# enable/disable stuff
- self.dialog.buttonUninstall.setEnabled(False)
- self.dialog.buttonInstall.setEnabled(False)
self.dialog.buttonUpdateAll.setEnabled(False)
- self.dialog.buttonExecute.hide()
- self.dialog.labelFilterValidity.hide()
-
- # Hide package-related interface elements until needed
- self.dialog.labelPackageName.hide()
- self.dialog.labelVersion.hide()
- self.dialog.labelMaintainer.hide()
- self.dialog.labelIcon.hide()
- self.dialog.labelDescription.hide()
- self.dialog.labelUrl.hide()
- self.dialog.labelUrlType.hide()
- self.dialog.labelContents.hide()
+ self.hide_progress_widgets()
# connect slots
self.dialog.rejected.connect(self.reject)
- self.dialog.buttonInstall.clicked.connect(self.install)
- self.dialog.buttonUninstall.clicked.connect(self.remove)
self.dialog.buttonUpdateAll.clicked.connect(self.update_all)
- self.dialog.comboPackageType.currentIndexChanged.connect(self.update_type_filter)
- self.dialog.lineEditFilter.textChanged.connect(self.update_text_filter)
- self.dialog.buttonConfigure.clicked.connect(self.show_config)
self.dialog.buttonClose.clicked.connect(self.dialog.reject)
- self.dialog.buttonExecute.clicked.connect(self.executemacro)
- self.dialog.tablePackages.selectionModel().currentRowChanged.connect(self.table_row_selected)
- self.dialog.tablePackages.setEnabled(False)
-
- # Show "Workbenches" to start with
- self.dialog.comboPackageType.setCurrentIndex(1)
-
- # allow links to open in browser
- self.dialog.description.setOpenLinks(True)
- self.dialog.description.setOpenExternalLinks(True)
+ self.dialog.buttonUpdateCache.clicked.connect(self.on_buttonUpdateCache_clicked)
+ self.dialog.buttonShowDetails.clicked.connect(self.toggle_details)
+ self.dialog.buttonPauseUpdate.clicked.connect(self.stop_update)
+ self.packageList.itemSelected.connect(self.table_row_activated)
+ self.packageList.setEnabled(False)
+ self.packageDetails.executeClicked.connect(self.executemacro)
+ self.packageDetails.installClicked.connect(self.install)
+ self.packageDetails.uninstallClicked.connect(self.remove)
+ self.packageDetails.updateClicked.connect(self.remove)
+ self.packageDetails.backClicked.connect(self.on_buttonBack_clicked)
# center the dialog over the FreeCAD window
mw = FreeCADGui.getMainWindow()
@@ -224,12 +226,10 @@ class CommandAddonManager:
def reject(self) -> None:
"""called when the window has been closed"""
- # save window geometry and splitter state for next use
+ # save window geometry for next use
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetInt("WindowWidth", self.dialog.width())
pref.SetInt("WindowHeight", self.dialog.height())
- pref.SetInt("SplitterLeft", self.dialog.splitter.sizes()[0])
- pref.SetInt("SplitterRight", self.dialog.splitter.sizes()[1])
# ensure all threads are finished before closing
oktoclose = True
@@ -269,13 +269,8 @@ class CommandAddonManager:
cancelBtn.setText(translate("AddonsInstaller","Restart later"))
ret = m.exec_()
if ret == m.Ok:
- shutil.rmtree(self.macro_repo_dir, onerror=self.remove_readonly)
# restart FreeCAD after a delay to give time to this dialog to close
QtCore.QTimer.singleShot(1000, utils.restart_freecad)
- try:
- shutil.rmtree(self.macro_repo_dir, onerror=self.remove_readonly)
- except Exception:
- pass
else:
FreeCAD.Console.PrintWarning("Could not terminate sub-threads in Addon Manager.\n")
self.cleanup_workers()
@@ -299,11 +294,15 @@ class CommandAddonManager:
Each of these stages is launched in a separate thread to ensure that the UI remains responsive, and
the operation can be cancelled.
+ Each stage is also subject to caching, so may return immediately, if no cache update has been requested.
+
"""
# Each function in this list is expected to launch a thread and connect its completion signal
- # to self.do_next_startup_phase
+ # to self.do_next_startup_phase, or to shortcut to calling self.do_next_startup_phase if it
+ # is not launching a worker
self.startup_sequence = [self.populate_packages_table,
+ self.activate_table_widgets,
self.populate_macros,
self.update_metadata_cache,
self.check_updates]
@@ -320,32 +319,81 @@ class CommandAddonManager:
phase_runner()
else:
self.hide_progress_widgets()
- self.dialog.tablePackages.setEnabled(True)
- self.dialog.lineEditFilter.setFocus()
+ self.update_cache = False
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ pref.SetString("LastCacheUpdate", date.today().isoformat())
+
+ def get_cache_file_name(self, file:str) -> str:
+ cache_path = FreeCAD.getUserCachePath()
+ am_path = os.path.join(cache_path,"AddonManager")
+ os.makedirs(am_path,exist_ok=True)
+ return os.path.join(am_path,file)
def populate_packages_table(self) -> None:
self.item_model.clear()
self.current_progress_region += 1
- self.update_worker = UpdateWorker()
- self.update_worker.status_message.connect(self.show_information)
- self.update_worker.addon_repo.connect(self.add_addon_repo)
- self.update_progress_bar(10,100)
- self.update_worker.done.connect(self.do_next_startup_phase) # Link to step 2
- self.update_worker.start()
+ if self.update_cache or not os.path.isfile(self.get_cache_file_name("package_cache.json")):
+ self.update_cache = True # Make sure to trigger the other cache updates, if the json file was missing
+ self.update_worker = UpdateWorker()
+ self.update_worker.status_message.connect(self.show_information)
+ self.update_worker.addon_repo.connect(self.add_addon_repo)
+ self.update_worker.addon_repo.connect(self.cache_package)
+ self.update_progress_bar(10,100)
+ self.update_worker.done.connect(self.do_next_startup_phase) # Link to step 2
+ self.update_worker.done.connect(self.write_package_cache)
+ self.update_worker.start()
+ else:
+ self.update_worker = LoadPackagesFromCacheWorker(self.get_cache_file_name("package_cache.json"))
+ self.update_worker.addon_repo.connect(self.add_addon_repo)
+ self.update_progress_bar(10,100)
+ self.update_worker.done.connect(self.do_next_startup_phase) # Link to step 2
+ self.update_worker.start()
+
+ def cache_package(self, repo:AddonManagerRepo):
+ if not hasattr(self, "package_cache"):
+ self.package_cache = []
+ self.package_cache.append(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))
+
+ def activate_table_widgets(self) -> None:
+ self.packageList.setEnabled(True)
+ self.packageList.ui.lineEditFilter.setFocus()
+ self.do_next_startup_phase()
def populate_macros(self) -> None:
self.current_progress_region += 1
- self.macro_worker = FillMacroListWorker(self.macro_repo_dir)
- self.macro_worker.status_message_signal.connect(self.show_information)
- self.macro_worker.progress_made.connect(self.update_progress_bar)
- self.macro_worker.add_macro_signal.connect(self.add_addon_repo)
- self.macro_worker.done.connect(self.do_next_startup_phase) # Link to step 3
- self.macro_worker.start()
+ if self.update_cache or not os.path.isfile(self.get_cache_file_name("macro_cache.json")):
+ self.macro_worker = FillMacroListWorker(self.get_cache_file_name("Macros"))
+ self.macro_worker.status_message_signal.connect(self.show_information)
+ self.macro_worker.progress_made.connect(self.update_progress_bar)
+ self.macro_worker.add_macro_signal.connect(self.add_addon_repo)
+ self.macro_worker.add_macro_signal.connect(self.cache_macro)
+ self.macro_worker.done.connect(self.do_next_startup_phase) # Link to step 3
+ self.macro_worker.done.connect(self.write_macro_cache)
+ self.macro_worker.start()
+ else:
+ self.macro_worker = LoadMacrosFromCacheWorker(self.get_cache_file_name("macro_cache.json"))
+ self.macro_worker.add_macro_signal.connect(self.add_addon_repo)
+ self.macro_worker.done.connect(self.do_next_startup_phase) # Link to step 3
+ self.macro_worker.start()
+
+ def cache_macro(self, macro:AddonManagerRepo):
+ if not hasattr(self, "macro_cache"):
+ self.macro_cache = []
+ self.macro_cache.append(macro.macro.to_cache())
+
+ def write_macro_cache(self):
+ macro_cache_path = self.get_cache_file_name("macro_cache.json")
+ with open(macro_cache_path,"w") as f:
+ f.write(json.dumps(self.macro_cache))
def update_metadata_cache(self) -> None:
self.current_progress_region += 1
- pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
- if pref.GetBool("AutoFetchMetadata", True):
+ if self.update_cache:
self.update_metadata_cache_worker = UpdateMetadataCacheWorker(self.item_model.repos)
self.update_metadata_cache_worker.status_message.connect(self.show_information)
self.update_metadata_cache_worker.done.connect(self.do_next_startup_phase) # Link to step 4
@@ -353,18 +401,21 @@ class CommandAddonManager:
self.update_metadata_cache_worker.package_updated.connect(self.on_package_updated)
self.update_metadata_cache_worker.start()
else:
- self.do_next_startup_phase()
+ self.update_metadata_cache_worker = LoadMetadataFromCacheWorker()
+ self.update_metadata_cache_worker.done.connect(self.do_next_startup_phase) # Link to step 4
+ self.update_metadata_cache_worker.package_updated.connect(self.on_package_updated)
+ self.update_metadata_cache_worker.start()
+
+ def on_buttonUpdateCache_clicked(self) -> None:
+ self.update_cache = True
+ self.startup()
def on_package_updated(self, repo:AddonManagerRepo) -> None:
"""Called when the named package has either new metadata or a new icon (or both)"""
-
+
with self.lock:
- cache_path = os.path.join(FreeCAD.getUserAppDataDir(), "AddonManager", "PackageMetadata", repo.name)
- icon_filename = repo.metadata.Icon
- icon_path = os.path.join(cache_path, icon_filename)
- if os.path.isfile(icon_path):
- addonicon = QtGui.QIcon(icon_path)
- repo.icon = addonicon
+ cache_path = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata", repo.name)
+ repo.icon = self.get_icon(repo, update=True)
self.item_model.reload_item(repo)
@@ -424,7 +475,7 @@ class CommandAddonManager:
def get_icon(self, repo:AddonManagerRepo, update:bool=False) -> QtGui.QIcon:
"""returns an icon for a repo"""
- if not update and repo.icon and not repo.icon.isNull():
+ if not update and repo.icon and not repo.icon.isNull() and repo.icon.isValid():
return repo.icon
path = ":/icons/" + repo.name.replace(" ", "_")
@@ -436,8 +487,8 @@ class CommandAddonManager:
default_icon = QtGui.QIcon(":/icons/document-python.svg")
elif repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
# The cache might not have been downloaded yet, check to see if it's there...
- if repo.cached_icon_filename and os.path.isfile(repo.cached_icon_filename):
- path = repo.cached_icon_filename
+ if os.path.isfile(repo.get_cached_icon_filename()):
+ path = repo.get_cached_icon_filename()
elif repo.contains_workbench():
path += "_workbench_icon.svg"
default_icon = QtGui.QIcon(":/icons/document-package.svg")
@@ -455,168 +506,26 @@ class CommandAddonManager:
return addonicon
- def table_row_selected(self, current:QtCore.QModelIndex, previous:QtCore.QModelIndex) -> None:
- """a new row was selected, show the relevant data"""
-
- if not current.isValid():
- self.selected_repo = None
- return
- source_selection = self.item_filter.mapToSource (current)
- self.selected_repo = self.item_model.repos[source_selection.row()]
- self.dialog.description.clear()
- if self.selected_repo.repo_type == AddonManagerRepo.RepoType.MACRO:
- self.show_macro(self.selected_repo)
- self.dialog.buttonExecute.show()
- elif self.selected_repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
- self.show_workbench(self.selected_repo)
- self.dialog.buttonExecute.hide()
- elif self.selected_repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
- self.show_package(self.selected_repo)
- self.dialog.buttonExecute.hide()
- if self.selected_repo.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
- self.dialog.buttonInstall.setEnabled(True)
- self.dialog.buttonUninstall.setEnabled(False)
- self.dialog.buttonInstall.setText(translate("AddonsInstaller", "Install selected"))
- elif self.selected_repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
- self.dialog.buttonInstall.setEnabled(False)
- self.dialog.buttonUninstall.setEnabled(True)
- self.dialog.buttonInstall.setText(translate("AddonsInstaller", "Already updated"))
- elif self.selected_repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
- self.dialog.buttonInstall.setEnabled(True)
- self.dialog.buttonUninstall.setEnabled(True)
- self.dialog.buttonInstall.setText(translate("AddonsInstaller", "Update selected"))
- elif self.selected_repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
- self.dialog.buttonInstall.setEnabled(False)
- self.dialog.buttonUninstall.setEnabled(True)
- self.dialog.buttonInstall.setText(translate("AddonsInstaller", "Checking status..."))
+ def table_row_activated(self, selected_repo:AddonManagerRepo) -> None:
+ """a row was activated, show the relevant data"""
- def show_package_widgets(self, show:bool) -> None:
- """ Show or hide the widgets related to packages with a package.xml metadata file """
-
- # Always rebuild the urlGrid, remove all previous items
- for i in reversed(range(self.dialog.urlGrid.rowCount())):
- if self.dialog.urlGrid.itemAtPosition(i,0):
- self.dialog.urlGrid.itemAtPosition(i,0).widget().setParent(None)
- if self.dialog.urlGrid.itemAtPosition(i,1):
- self.dialog.urlGrid.itemAtPosition(i,1).widget().setParent(None)
-
- if show:
- # Show all the package-related widgets:
- self.dialog.labelPackageName.show()
- self.dialog.labelVersion.show()
- self.dialog.labelMaintainer.show()
- self.dialog.labelIcon.show()
- self.dialog.labelDescription.show()
- self.dialog.labelContents.show()
- else:
- # Hide all the package-related widgets:
- self.dialog.labelPackageName.hide()
- self.dialog.labelVersion.hide()
- self.dialog.labelMaintainer.hide()
- self.dialog.labelIcon.hide()
- self.dialog.labelDescription.hide()
- self.dialog.labelContents.hide()
+ self.packageList.hide()
+ self.packageDetails.show()
+ self.packageDetails.show_repo(selected_repo)
def show_information(self, message:str) -> None:
- """shows generic text in the information pane"""
+ """shows generic text in the information pane (which might be collapsed)"""
- self.dialog.labelStatusInfo.show()
self.dialog.labelStatusInfo.setText(message)
def show_workbench(self, repo:AddonManagerRepo) -> None:
- """loads information of a given workbench"""
+ self.packageList.hide()
+ self.packageDetails.show()
+ self.packageDetails.show_repo(repo)
- self.cleanup_workers()
- self.show_package_widgets(False)
- self.show_worker = ShowWorker(repo)
- self.show_worker.status_message.connect(self.show_information)
- self.show_worker.description_updated.connect(lambda desc: self.dialog.description.setText(desc))
- self.show_worker.addon_repos.connect(self.append_to_repos_list)
- self.show_worker.done.connect(lambda : self.dialog.labelStatusInfo.hide())
- self.show_worker.start()
-
- def show_package(self, repo:AddonManagerRepo) -> None:
- """ Show the details for a package (a repo with a package.xml metadata file) """
-
- self.cleanup_workers()
- self.show_package_widgets(True)
-
- # Name
- self.dialog.labelPackageName.setText(f"
{repo.metadata.Name} ")
-
- # Description
- self.dialog.labelDescription.setText(repo.metadata.Description)
-
- # Version
- self.dialog.labelVersion.setText(f"v{repo.metadata.Version} ")
-
- # Maintainers and authors
- maintainers = ""
- for maintainer in repo.metadata.Maintainer:
- maintainers += translate("AddonsInstaller","Maintainer") + f": {maintainer['name']} <{maintainer['email']}>\n"
- if len(repo.metadata.Author) > 0:
- for author in repo.metadata.Author:
- maintainers += translate("AddonsInstaller","Author") + f": {author['name']} <{author['email']}>\n"
- self.dialog.labelMaintainer.setText(maintainers)
-
- # Main package icon
- if not repo.icon or repo.icon.isNull():
- icon = self.get_icon(repo, update=True)
- self.item_model.update_item_icon(repo.name, icon)
- self.dialog.labelIcon.setPixmap(repo.icon.pixmap(QtCore.QSize(64,64)))
-
- # Urls
- urls = repo.metadata.Urls
- ui = FreeCADGui.UiLoader()
- for row, url in enumerate(urls):
- location = url["location"]
- url_type = url["type"]
- url_type_string = translate("AddonsInstaller","Other URL")
- if url_type == "website":
- url_type_string = translate("AddonsInstaller", "Website")
- elif url_type == "repository":
- url_type_string = translate("AddonsInstaller", "Repository")
- elif url_type == "bugtracker":
- url_type_string = translate("AddonsInstaller", "Bug tracker")
- elif url_type == "readme":
- url_type_string = translate("AddonsInstaller", "Readme")
- elif url_type == "documentation":
- url_type_string = translate("AddonsInstaller", "Documentation")
- self.dialog.urlGrid.addWidget(QtWidgets.QLabel(url_type_string), row, 0)
- ui=FreeCADGui.UiLoader()
- url_label=ui.createWidget("Gui::UrlLabel")
- url_label.setText(location)
- url_label.setUrl(location)
- self.dialog.urlGrid.addWidget(url_label, row, 1)
-
- # Package contents:
- content_string = ""
- for name,item_list in repo.metadata.Content.items():
- if name == "preferencepack":
- content_type = translate("AddonsInstaller","Preference Packs")
- elif name == "workbench":
- content_type = translate("AddonsInstaller","Workbenches")
- elif name == "macro":
- content_type = translate("AddonsInstaller","Macros")
- else:
- content_type = translate("AddonsInstaller","Other content") + ": " + name
- content_string += f"{content_type} "
- content_string += ""
- for item in item_list:
- content_string += f"{item.Name} – {item.Description} "
- content_string += " "
- self.dialog.description.setText(content_string)
-
- def show_macro(self, repo:AddonManagerRepo) -> None:
- """loads information of a given macro"""
-
- self.cleanup_workers()
- self.show_package_widgets(False)
- self.showmacro_worker = GetMacroDetailsWorker(repo)
- self.showmacro_worker.status_message.connect(self.show_information)
- self.showmacro_worker.description_updated.connect(lambda desc: self.dialog.description.setText(desc))
- self.showmacro_worker.done.connect(lambda : self.dialog.labelStatusInfo.hide())
- self.showmacro_worker.start()
+ def on_buttonBack_clicked(self) -> None:
+ self.packageDetails.hide()
+ self.packageList.show()
def append_to_repos_list(self, repo:AddonManagerRepo) -> None:
"""this function allows threads to update the main list of workbenches"""
@@ -721,23 +630,47 @@ class CommandAddonManager:
self.dialog.labelStatusInfo.hide()
self.dialog.progressBar.hide()
- self.dialog.lineEditFilter.setFocus()
+ self.dialog.buttonPauseUpdate.hide()
+ self.dialog.buttonShowDetails.hide()
+ self.dialog.labelUpdateInProgress.hide()
+ self.packageList.ui.lineEditFilter.setFocus()
+
+ def show_progress_widgets(self) -> None:
+ if self.dialog.progressBar.isHidden():
+ self.dialog.progressBar.show()
+ self.dialog.buttonPauseUpdate.show()
+ self.dialog.buttonShowDetails.show()
+ self.dialog.labelStatusInfo.hide()
+ self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow)
+ self.dialog.labelUpdateInProgress.show()
def update_progress_bar(self, current_value:int, max_value:int) -> None:
""" Update the progress bar, showing it if it's hidden """
- self.dialog.progressBar.show()
+ self.show_progress_widgets()
region_size = 100 / self.number_of_progress_regions
value = (self.current_progress_region-1)*region_size + (current_value / max_value / self.number_of_progress_regions)*region_size
self.dialog.progressBar.setValue(value)
+ def toggle_details(self) -> None:
+ if self.dialog.labelStatusInfo.isHidden():
+ self.dialog.labelStatusInfo.show()
+ self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.DownArrow)
+ else:
+ self.dialog.labelStatusInfo.hide()
+ self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow)
+
+ def stop_update(self)-> None:
+ self.cleanup_workers()
+ self.hide_progress_widgets()
+
def on_package_installed(self, repo:AddonManagerRepo, message:str) -> None:
QtWidgets.QMessageBox.information(None,
translate("AddonsInstaller", "Installation succeeded"),
message,
QtWidgets.QMessageBox.Close)
self.dialog.progressBar.hide()
- self.table_row_selected(self.dialog.tablePackages.selectionModel().selectedIndexes()[0], QtCore.QModelIndex())
+ self.table_row_selected(self.dialog.listPackages.selectionModel().selectedIndexes()[0], QtCore.QModelIndex())
if repo.contains_workbench():
self.item_model.update_item_status(repo.name, AddonManagerRepo.UpdateStatus.PENDING_RESTART)
else:
@@ -791,20 +724,20 @@ class CommandAddonManager:
clonedir = moddir + os.sep + self.selected_repo.name
if os.path.exists(clonedir):
shutil.rmtree(clonedir, onerror=self.remove_readonly)
- self.dialog.description.setText(translate("AddonsInstaller",
+ self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller",
"Addon successfully removed. Please restart FreeCAD."))
self.item_model.update_item_status(self.selected_repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED)
self.addon_removed = True # A value to trigger the restart message on dialog close
else:
- self.dialog.description.setText(translate("AddonsInstaller", "Unable to remove this addon with the Addon Manager."))
+ self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller", "Unable to remove this addon with the Addon Manager."))
elif self.selected_repo.repo_type == AddonManagerRepo.RepoType.MACRO:
macro = self.selected_repo.macro
if macro.remove():
- self.dialog.description.setText(translate("AddonsInstaller", "Macro successfully removed."))
+ self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller", "Macro successfully removed."))
self.item_model.update_item_status(self.selected_repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED)
else:
- self.dialog.description.setText(translate("AddonsInstaller", "Macro could not be removed."))
+ self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller", "Macro could not be removed."))
def show_config(self) -> None:
"""shows the configuration dialog"""
@@ -836,226 +769,4 @@ class CommandAddonManager:
pref.SetBool("UserProxyCheck", self.config.radioButtonUserProxy.isChecked())
pref.SetString("ProxyUrl", self.config.userProxy.toPlainText())
-
-
- def update_type_filter(self, type_filter:int) -> None:
- """hide/show rows corresponding to the type filter
-
- type_filter is an integer: 0 for all, 1 for workbenches, 2 for macros, and 3 for preference packs
-
- """
- self.item_filter.setPackageFilter(type_filter)
-
-
- def update_text_filter(self, text_filter:str) -> None:
- """filter name and description by the regex specified by text_filter"""
-
- if text_filter:
- test_regex = QtCore.QRegularExpression(text_filter)
- if test_regex.isValid():
- self.dialog.labelFilterValidity.setToolTip(translate("AddonsInstaller","Filter is valid"))
- icon = QtGui.QIcon.fromTheme("ok", QtGui.QIcon(":/icons/edit_OK.svg"))
- self.dialog.labelFilterValidity.setPixmap(icon.pixmap(16,16))
- else:
- self.dialog.labelFilterValidity.setToolTip(translate("AddonsInstaller","Filter regular expression is invalid"))
- icon = QtGui.QIcon.fromTheme("cancel", QtGui.QIcon(":/icons/edit_Cancel.svg"))
- self.dialog.labelFilterValidity.setPixmap(icon.pixmap(16,16))
- self.dialog.labelFilterValidity.show()
- else:
- self.dialog.labelFilterValidity.hide()
- self.item_filter.setFilterRegularExpression(text_filter)
-
-
-class PackageListItemModel(QtCore.QAbstractTableModel):
-
- repos = []
- write_lock = threading.Lock()
-
- DataAccessRole = QtCore.Qt.UserRole
- StatusUpdateRole = QtCore.Qt.UserRole + 1
- IconUpdateRole = QtCore.Qt.UserRole + 2
-
- def __init__(self) -> None:
- QtCore.QAbstractTableModel.__init__(self)
-
- def rowCount(self, parent:QtCore.QModelIndex=QtCore.QModelIndex()) -> int:
- if parent.isValid():
- return 0
- return len(self.repos)
-
- def columnCount(self, parent:QtCore.QModelIndex=QtCore.QModelIndex()) -> int:
- if parent.isValid():
- return 0
- return 3 # Icon, Name, Status
-
- def data(self, index:QtCore.QModelIndex, role:int=QtCore.Qt.DisplayRole) -> Union[QtGui.QIcon,str]:
- if not index.isValid():
- return None
- row = index.row()
- column = index.column()
- if role == QtCore.Qt.DisplayRole:
- if row >= len(self.repos):
- return None
- if column == 1:
- return self.repos[row].name if self.repos[row].metadata is None else self.repos[row].metadata.Name
- elif column == 2:
- if self.repos[row].update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
- return translate("AddonsInstaller","Installed")
- elif self.repos[row].update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
- return translate("AddonsInstaller","Up-to-date")
- elif self.repos[row].update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
- return translate("AddonsInstaller","Update available")
- elif self.repos[row].update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
- return translate("AddonsInstaller","Restart required")
- else:
- return None
- else:
- return None
- elif role == QtCore.Qt.DecorationRole:
- if column == 0:
- return self.repos[row].icon
- elif role == QtCore.Qt.ToolTipRole:
- tooltip = ""
- if self.repos[row].repo_type == AddonManagerRepo.RepoType.PACKAGE:
- tooltip = f"Package '{self.repos[row].name}'"
- # TODO add more info from Metadata
- elif self.repos[row].repo_type == AddonManagerRepo.RepoType.WORKBENCH:
- tooltip = f"Workbench '{self.repos[row].name}'"
- elif self.repos[row].repo_type == AddonManagerRepo.RepoType.MACRO:
- tooltip = f"Macro '{self.repos[row].name}'"
- return tooltip
- elif role == QtCore.Qt.TextAlignmentRole:
- return QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
- elif role == PackageListItemModel.DataAccessRole:
- return self.repos[row]
-
- def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
- if role == QtCore.Qt.DisplayRole:
- if orientation == QtCore.Qt.Horizontal:
- if section == 0:
- return None
- elif section == 1:
- return translate("AddonsInstaller", "Name")
- elif section == 2:
- return translate("AddonsInstaller", "Status")
- else:
- return None
-
- def setData(self, index:QtCore.QModelIndex, value, role=QtCore.Qt.EditRole) -> None:
- """ Set the data for this row. The column of the index is ignored. """
-
- row = index.row()
- self.write_lock.acquire()
- if role == PackageListItemModel.StatusUpdateRole:
- self.repos[row].update_status = value
- self.dataChanged.emit(self.index(row,2), self.index(row,2), [PackageListItemModel.StatusUpdateRole])
- elif role == PackageListItemModel.IconUpdateRole:
- self.repos[row].icon = value
- self.dataChanged.emit(self.index(row,0), self.index(row,0), [PackageListItemModel.IconUpdateRole])
- self.write_lock.release()
-
- def append_item(self, repo:AddonManagerRepo) -> None:
- if repo in self.repos:
- # Cowardly refuse to insert the same repo a second time
- return
- self.write_lock.acquire()
- self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
- self.repos.append(repo)
- self.endInsertRows()
- self.write_lock.release()
-
- def clear(self) -> None:
- if self.rowCount() > 0:
- self.write_lock.acquire()
- self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount()-1)
- self.repos = []
- self.endRemoveRows()
- self.write_lock.release()
-
- def update_item_status(self, name:str, status:AddonManagerRepo.UpdateStatus) -> None:
- for row,item in enumerate(self.repos):
- if item.name == name:
- self.setData(self.index(row,0), status, PackageListItemModel.StatusUpdateRole)
- return
-
- def update_item_icon(self, name:str, icon:QtGui.QIcon) -> None:
- for row,item in enumerate(self.repos):
- if item.name == name:
- self.setData(self.index(row,0), icon, PackageListItemModel.IconUpdateRole)
- return
-
- def reload_item(self,repo:AddonManagerRepo) -> None:
- for index,item in enumerate(self.repos):
- if item.name == repo.name:
- self.write_lock.acquire()
- self.repos[index] = repo
- self.write_lock.release()
- return
-
-
-class PackageListIconDelegate(QtWidgets.QStyledItemDelegate):
- """ A delegate to ensure proper alignment of the icon in the table cells """
-
- def paint(self, painter, option, index):
- if (index.column() == 0):
- option.decorationAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter
- super().paint(painter, option, index)
-
-
-class PackageListFilter(QtCore.QSortFilterProxyModel):
- """ Handle filtering the item list on various criteria """
-
- def __init__(self):
- super().__init__()
- self.package_type = 0 # Default to showing everything
- self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
-
- def setPackageFilter(self, type:int) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs
- self.package_type = type
- self.invalidateFilter()
-
- def lessThan(self, left, right) -> bool:
- l = self.sourceModel().data(left,PackageListItemModel.DataAccessRole)
- r = self.sourceModel().data(right,PackageListItemModel.DataAccessRole)
-
- if left.column() == 0: # Icon
- return False
- elif left.column() == 1: # Name
- 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()
- elif left.column() == 2: # Status
- return l.update_status < r.update_status
-
- def filterAcceptsRow(self, row, parent=QtCore.QModelIndex()):
- index = self.sourceModel().createIndex(row, 0)
- data = self.sourceModel().data(index,PackageListItemModel.DataAccessRole)
- if self.package_type == 1:
- if not data.contains_workbench():
- return False
- elif self.package_type == 2:
- if not data.contains_macro():
- return False
- elif self.package_type == 3:
- 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
- re = self.filterRegularExpression()
- if re.isValid():
- re.setPatternOptions(QtCore.QRegularExpression.CaseInsensitiveOption)
- if re.match(name).hasMatch():
- return True
- if re.match(desc).hasMatch():
- return True
- return False
- else:
- return False
-
- def sort(self, column, order):
- if column == 0: # Icons
- return
- else:
- super().sort(column, order)
# @}
diff --git a/src/Mod/AddonManager/AddonManager.ui b/src/Mod/AddonManager/AddonManager.ui
index 5c3e08a1f2..13fff38d6f 100644
--- a/src/Mod/AddonManager/AddonManager.ui
+++ b/src/Mod/AddonManager/AddonManager.ui
@@ -6,8 +6,8 @@
0
0
- 599
- 480
+ 800
+ 600
@@ -15,282 +15,98 @@
-
-
+
0
0
-
- Qt::Horizontal
-
-
-
+
+
+ -
+
+ -
+
-
-
- -
-
-
- Show packages containing:
-
-
-
- -
-
- -
-
- All
-
-
- -
-
- Workbenches
-
-
- -
-
- Macros
-
-
- -
-
- Preference Packs
-
-
-
-
-
+
+
+ Show details
+
+
+ ...
+
+
+ Qt::RightArrow
+
+
-
-
- -
-
-
- Filter
-
-
- true
-
-
-
- -
-
-
- OK
-
-
-
-
+
+
+ Loading...
+
+
-
-
-
- QAbstractItemView::NoEditTriggers
+
+
+
+ 0
+ 0
+
-
- false
-
-
- QAbstractItemView::SingleSelection
-
-
- QAbstractItemView::SelectRows
-
-
+
- 16
- 16
+ 0
+ 0
-
+
+
+ 16777215
+ 12
+
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
false
-
- true
-
-
- false
-
-
- 16
-
-
- false
-
-
- 12
-
-
- 16
-
-
- false
-
-
-
-
-
-
-
- -
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Sets configuration options for the Addon Manager
-
-
- Configure...
-
-
-
-
-
- -
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 64
- 64
-
-
-
-
- 64
- 64
-
-
-
- Icon
-
-
-
- -
-
-
- 0
-
-
- QLayout::SetDefaultConstraint
-
- -
-
-
- <h1>Package Name</h1>
-
-
-
- -
-
-
- <em>Version</em>
-
-
-
- -
-
-
- Maintainer
-
-
-
-
-
-
-
- -
-
-
- Description
-
-
- Qt::PlainText
-
-
- true
+
+ Downloading info...
-
-
- -
-
-
- URL Type
-
-
-
- -
-
-
- Url
-
-
-
-
-
- -
-
-
- Package contents:
+
+
+ Pause cache update
-
-
- -
-
-
- -
-
- Run Macro
+
-
-
-
- -
-
-
- labelStatusInfo
-
-
-
- -
-
+
-
-
-
- 24
+
+
+
+ 0
+ 0
+
-
- false
-
-
- Downloading info...
+
+ labelStatusInfo
@@ -302,22 +118,9 @@
QLayout::SetDefaultConstraint
-
-
-
- Uninstalls a selected macro or workbench
-
+
- Uninstall selected
-
-
-
- -
-
-
- Installs or updates the selected macro or workbench
-
-
- Install/update selected
+ Refresh local cache
@@ -327,10 +130,23 @@
Download and apply all available updates
- Update all
+ Update all packages
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
-
diff --git a/src/Mod/AddonManager/AddonManagerRepo.py b/src/Mod/AddonManager/AddonManagerRepo.py
index 79a22e130c..399cb09093 100644
--- a/src/Mod/AddonManager/AddonManagerRepo.py
+++ b/src/Mod/AddonManager/AddonManagerRepo.py
@@ -23,15 +23,16 @@
import FreeCAD
import os
+from typing import Dict
from addonmanager_macro import Macro
class AddonManagerRepo:
"Encapsulate information about a FreeCAD addon"
- from enum import Enum
+ from enum import IntEnum
- class RepoType(Enum):
+ class RepoType(IntEnum):
WORKBENCH = 1
MACRO = 2
PACKAGE = 3
@@ -44,10 +45,7 @@ class AddonManagerRepo:
elif self.value == 3:
return "Package"
- def __int__(self) -> int :
- return self.value
-
- class UpdateStatus(Enum):
+ class UpdateStatus(IntEnum):
NOT_INSTALLED = 0
UNCHECKED = 1
NO_UPDATE_AVAILABLE = 2
@@ -72,9 +70,9 @@ class AddonManagerRepo:
return "Restart required"
def __init__ (self, name:str, url:str, status:UpdateStatus, branch:str):
- self.name = name
- self.url = url
- self.branch = branch
+ self.name = name.strip()
+ self.url = url.strip()
+ self.branch = branch.strip()
self.update_status = status
self.repo_type = AddonManagerRepo.RepoType.WORKBENCH
self.description = None
@@ -105,6 +103,31 @@ class AddonManagerRepo:
instance.repo_type = AddonManagerRepo.RepoType.MACRO
instance.description = macro.desc
return instance
+
+ @classmethod
+ def from_cache (self, data:Dict):
+ """ Load basic data from cached dict data. Does not include Macro or Metadata information, which must be populated separately. """
+
+ mod_dir = os.path.join(FreeCAD.getUserAppDataDir(),"Mod",data["name"])
+ if os.path.isdir(mod_dir):
+ status = AddonManagerRepo.UpdateStatus.UNCHECKED
+ else:
+ status = AddonManagerRepo.UpdateStatus.NOT_INSTALLED
+ instance = AddonManagerRepo(data["name"], data["url"], status, data["branch"])
+ instance.repo_type = AddonManagerRepo.RepoType(data["repo_type"])
+ instance.description = data["description"]
+ instance.cached_icon_filename = data["cached_icon_filename"]
+ return instance
+
+ def to_cache (self) -> Dict:
+ """ Returns a dictionary with cache information that can be used later with from_cache to recreate this object. """
+
+ return {"name":self.name,
+ "url":self.url,
+ "branch":self.branch,
+ "repo_type":int(self.repo_type),
+ "description":self.description,
+ "cached_icon_filename":self.cached_icon_filename}
def contains_workbench(self) -> bool:
""" Determine if this package contains (or is) a workbench """
@@ -160,7 +183,7 @@ class AddonManagerRepo:
real_icon = real_icon.replace("/", os.path.sep) # Required path separator in the metadata.xml file to local separator
_, file_extension = os.path.splitext(real_icon)
- store = os.path.join(FreeCAD.getUserAppDataDir(), "AddonManager", "PackageMetadata")
+ store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
self.cached_icon_filename = os.path.join(store, self.name, "cached_icon"+file_extension)
return self.cached_icon_filename
diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt
index fa84f324fb..de711853f1 100644
--- a/src/Mod/AddonManager/CMakeLists.txt
+++ b/src/Mod/AddonManager/CMakeLists.txt
@@ -13,6 +13,10 @@ SET(AddonManager_SRCS
addonmanager_workers.py
AddonManager.ui
AddonManagerOptions.ui
+ compact_view.py
+ expanded_view.py
+ package_list.py
+ package_details.py
)
SOURCE_GROUP("" FILES ${AddonManager_SRCS})
diff --git a/src/Mod/AddonManager/Resources/AddonManager.qrc b/src/Mod/AddonManager/Resources/AddonManager.qrc
index 312e646a45..be3f70b57d 100644
--- a/src/Mod/AddonManager/Resources/AddonManager.qrc
+++ b/src/Mod/AddonManager/Resources/AddonManager.qrc
@@ -61,6 +61,8 @@
icons/WebTools_workbench_icon.svg
icons/workfeature_workbench_icon.svg
icons/yaml-workspace_workbench_icon.svg
+ icons/expanded_view.svg
+ icons/compact_view.svg
translations/AddonManager_af.qm
translations/AddonManager_ar.qm
translations/AddonManager_ca.qm
diff --git a/src/Mod/AddonManager/Resources/icons/compact_view.svg b/src/Mod/AddonManager/Resources/icons/compact_view.svg
new file mode 100644
index 0000000000..9923da9443
--- /dev/null
+++ b/src/Mod/AddonManager/Resources/icons/compact_view.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/Resources/icons/expanded_view.svg b/src/Mod/AddonManager/Resources/icons/expanded_view.svg
new file mode 100644
index 0000000000..ad0464a876
--- /dev/null
+++ b/src/Mod/AddonManager/Resources/icons/expanded_view.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py
index 88481da068..e2e46da249 100644
--- a/src/Mod/AddonManager/addonmanager_macro.py
+++ b/src/Mod/AddonManager/addonmanager_macro.py
@@ -26,6 +26,7 @@ import re
import sys
import codecs
import shutil
+from typing import Dict, Union
import FreeCAD
@@ -63,6 +64,18 @@ class Macro(object):
def __eq__(self, other):
return self.filename == other.filename
+ @classmethod
+ def from_cache (self, cache_dict:Dict):
+ instance = Macro(cache_dict["name"])
+ for key,value in cache_dict.items():
+ instance.__dict__[key] = value
+ return instance
+
+ def to_cache (self) -> Dict:
+ """ For cache purposes this entire class is dumped directly """
+
+ return self.__dict__
+
@property
def filename(self):
if self.on_git:
diff --git a/src/Mod/AddonManager/addonmanager_metadata.py b/src/Mod/AddonManager/addonmanager_metadata.py
index f72414866a..2084fe5166 100644
--- a/src/Mod/AddonManager/addonmanager_metadata.py
+++ b/src/Mod/AddonManager/addonmanager_metadata.py
@@ -52,7 +52,7 @@ class MetadataDownloadWorker(QObject):
super().__init__(parent)
self.repo = repo
self.index = index
- self.store = os.path.join(FreeCAD.getUserAppDataDir(), "AddonManager", "PackageMetadata")
+ self.store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
self.last_sha1 = ""
self.url = self.repo.metadata_url
diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py
index 4712e3c94d..91661ff736 100644
--- a/src/Mod/AddonManager/addonmanager_workers.py
+++ b/src/Mod/AddonManager/addonmanager_workers.py
@@ -124,46 +124,11 @@ class UpdateWorker(QtCore.QThread):
else:
FreeCAD.Console.PrintWarning("Debug: addon_flags.json not found\n")
- # Check the local package cache:
basedir = FreeCAD.getUserAppDataDir()
moddir = basedir + os.sep + "Mod"
- cache_path = os.path.join(FreeCAD.getUserAppDataDir(), "AddonManager", "PackageMetadata")
package_names = []
- if os.path.isdir(cache_path):
- for dir in os.listdir(cache_path):
- if self.current_thread.isInterruptionRequested():
- return
- dir_path = os.path.join(cache_path, dir)
- if not os.path.isdir(dir_path):
- continue
- xml_cache = os.path.join(dir_path, "package.xml")
- try:
- meta = FreeCAD.Metadata(xml_cache)
- except Exception:
- FreeCAD.Console.PrintWarning(f"Failed to create Metadata from {xml_cache}\n")
- continue
- name = dir # Do not use metadata name here, we want to match the legacy fetch code below
- url = None
- branch = None
- for meta_url in meta.Urls:
- if meta_url["type"] == "repository":
- url = meta_url["location"]
- branch = meta_url["branch"]
- break
- addondir = moddir + os.sep + name
- if os.path.exists(addondir) and os.listdir(addondir):
- state = AddonManagerRepo.UpdateStatus.UNCHECKED
- else:
- state = AddonManagerRepo.UpdateStatus.NOT_INSTALLED
- cached_package = AddonManagerRepo(name, url, state, branch)
- cached_package.metadata = meta
- cached_package.icon = QtGui.QIcon(cached_package.get_cached_icon_filename())
- cached_package.repo_type = AddonManagerRepo.RepoType.PACKAGE
- cached_package.description = meta.Description
- self.addon_repo.emit(cached_package)
- package_names.append(name)
- # querying custom addons
+ # querying custom addons first
addon_list = (FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
.GetString("CustomRepositories", "").split("\n"))
custom_addons = []
@@ -205,7 +170,7 @@ class UpdateWorker(QtCore.QThread):
p = re.findall((r'(?m)\[submodule\s*"(?P.*)"\]\s*'
r"path\s*=\s*(?P.+)\s*"
r"url\s*=\s*(?Phttps?://.*)\s*"
- r"(branch\s*=\s*(?P.*)\s*)?"), p)
+ r"(branch\s*=\s*(?P[^\s]*)\s*)?"), p)
for name, path, url, _, branch in p:
if self.current_thread.isInterruptionRequested():
return
@@ -229,6 +194,87 @@ class UpdateWorker(QtCore.QThread):
self.done.emit()
self.stop = True
+class LoadPackagesFromCacheWorker(QtCore.QThread):
+ addon_repo = QtCore.Signal(object)
+ done = QtCore.Signal()
+
+ def __init__(self, cache_file:str):
+ QtCore.QThread.__init__(self)
+ self.cache_file = cache_file
+
+ def run(self):
+ with open(self.cache_file,"r") as f:
+ data = f.read()
+ dict_data = json.loads(data)
+ for item in dict_data:
+ if QtCore.QThread.currentThread().isInterruptionRequested():
+ return
+ self.addon_repo.emit(AddonManagerRepo.from_cache(item))
+ self.done.emit()
+
+class LoadMacrosFromCacheWorker(QtCore.QThread):
+ add_macro_signal = QtCore.Signal(object)
+ done = QtCore.Signal()
+
+ def __init__(self, cache_file:str):
+ QtCore.QThread.__init__(self)
+ self.cache_file = cache_file
+
+ def run(self):
+ with open(self.cache_file,"r") as f:
+ data = f.read()
+ dict_data = json.loads(data)
+ for item in dict_data:
+ if QtCore.QThread.currentThread().isInterruptionRequested():
+ return
+ new_macro = Macro.from_cache(item)
+ self.add_macro_signal.emit(AddonManagerRepo.from_macro(new_macro))
+ self.done.emit()
+
+class LoadMetadataFromCacheWorker(QtCore.QThread):
+
+ done = QtCore.Signal()
+ package_updated = QtCore.Signal(AddonManagerRepo)
+
+ def __init__(self):
+ QtCore.QThread.__init__(self)
+
+ def run(self):
+ cache_path = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
+ package_names = []
+ if os.path.isdir(cache_path):
+ for dir in os.listdir(cache_path):
+ if QtCore.QThread.currentThread().isInterruptionRequested():
+ return
+ dir_path = os.path.join(cache_path, dir)
+ if not os.path.isdir(dir_path):
+ continue
+ xml_cache = os.path.join(dir_path, "package.xml")
+ try:
+ meta = FreeCAD.Metadata(xml_cache)
+ except Exception:
+ FreeCAD.Console.PrintWarning(f"Failed to create Metadata from {xml_cache}\n")
+ continue
+ name = dir # Do not use metadata name here, we want to match the legacy fetch code
+ url = None
+ branch = None
+ for meta_url in meta.Urls:
+ if meta_url["type"] == "repository":
+ url = meta_url["location"]
+ branch = meta_url["branch"]
+ break
+ addondir = os.path.join(FreeCAD.getUserAppDataDir(),"Mod",name)
+ if os.path.exists(addondir) and os.listdir(addondir):
+ state = AddonManagerRepo.UpdateStatus.UNCHECKED
+ else:
+ state = AddonManagerRepo.UpdateStatus.NOT_INSTALLED
+ cached_package = AddonManagerRepo(name, url, state, branch)
+ cached_package.metadata = meta
+ cached_package.repo_type = AddonManagerRepo.RepoType.PACKAGE
+ cached_package.description = meta.Description
+ self.package_updated.emit(cached_package)
+ self.done.emit()
+
class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
"""This worker checks for available updates for all workbenches"""
@@ -480,70 +526,68 @@ class ShowWorker(QtCore.QThread):
"""This worker retrieves info of a given workbench"""
status_message = QtCore.Signal(str)
- description_updated = QtCore.Signal(str)
+ readme_updated = QtCore.Signal(str)
addon_repos = QtCore.Signal(object)
done = QtCore.Signal()
- def __init__(self, repo):
+ 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..."))
- if self.repo.description is not None:
- desc = self.repo.description
- else:
- u = None
- url = self.repo.url
- self.status_message.emit(translate("AddonsInstaller", "Retrieving info from") + " " + 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.PrintWarning(f"Debug: README not found for {url}\n")
- u = utils.urlopen(readmeurl)
- if not u:
- FreeCAD.Console.PrintWarning(f"Debug: README not found at {readmeurl}\n")
- u = utils.urlopen(readmeurl)
- if u:
- p = u.read()
- if isinstance(p, bytes):
- p = p.decode("utf-8")
- u.close()
- readme = re.findall(regex, p, flags=re.MULTILINE | re.DOTALL)
- if readme:
- desc = readme[0]
- else:
- FreeCAD.Console.PrintWarning(f"Debug: README not found at {readmeurl}\n")
+ u = None
+ url = self.repo.url
+ self.status_message.emit(translate("AddonsInstaller", "Retrieving info from") + " " + 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.PrintWarning(f"Debug: README not found for {url}\n")
+ u = utils.urlopen(readmeurl)
+ if not u:
+ FreeCAD.Console.PrintWarning(f"Debug: README not found at {readmeurl}\n")
+ u = utils.urlopen(readmeurl)
+ if u:
+ p = u.read()
+ if isinstance(p, bytes):
+ p = p.decode("utf-8")
+ u.close()
+ readme = re.findall(regex, p, flags=re.MULTILINE | re.DOTALL)
+ if readme:
+ desc = readme[0]
else:
- # convert raw markdown using lib
- readmeurl = utils.get_readme_url(self.repo)
- if not readmeurl:
- FreeCAD.Console.PrintWarning(f"Debug: README not found for {url}\n")
- u = utils.urlopen(readmeurl)
- if u:
- p = u.read()
- if isinstance(p, bytes):
- p = p.decode("utf-8")
- u.close()
- 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
+ FreeCAD.Console.PrintWarning(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.PrintWarning(f"Debug: README not found for {url}\n")
+ u = utils.urlopen(readmeurl)
+ if u:
+ p = u.read()
+ if isinstance(p, bytes):
+ p = p.decode("utf-8")
+ u.close()
+ 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:
- FreeCAD.Console.PrintWarning("Debug: README not found at {readmeurl}\n")
+ message = """
+
+
+"""
+ message += translate("AddonsInstaller", "Raw markdown displayed")
+ message += " "
+ message += translate("AddonsInstaller", "Python Markdown library is missing.")
+ message += "
" + desc + " "
+ desc = message
+ else:
+ FreeCAD.Console.PrintWarning("Debug: README not found at {readmeurl}\n")
if desc == "":
# fall back to the description text
u = utils.urlopen(url)
@@ -686,11 +730,11 @@ class ShowWorker(QtCore.QThread):
if QtCore.QThread.currentThread().isInterruptionRequested():
return
- self.description_updated.emit(message)
+ self.readme_updated.emit(message)
self.mustLoadImages = True
label = self.loadImages(message, self.repo.url, self.repo.name)
if label:
- self.description_updated.emit(label)
+ self.readme_updated.emit(label)
if QtCore.QThread.currentThread().isInterruptionRequested():
return
self.done.emit()
@@ -711,12 +755,12 @@ class ShowWorker(QtCore.QThread):
imagepaths = re.findall("" + self.macro.name + "" + self.macro.desc + " Macro location: " + self.macro.url + " ")
if QtCore.QThread.currentThread().isInterruptionRequested():
return
- self.description_updated.emit(message)
+ self.readme_updated.emit(message)
self.done.emit()
self.stop = True
@@ -1124,7 +1169,7 @@ class UpdateMetadataCacheWorker(QtCore.QThread):
self.num_downloads_required = len(self.repos)
self.progress_made.emit(0, self.num_downloads_required)
self.status_message.emit(translate("AddonsInstaller", "Retrieving package metadata..."))
- store = os.path.join(FreeCAD.getUserAppDataDir(), "AddonManager", "PackageMetadata")
+ store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
index_file = os.path.join(store,"index.json")
self.index = {}
if os.path.isfile(index_file):
@@ -1286,10 +1331,11 @@ class UpdateSingleWorker(QtCore.QThread):
def update_macro(self, repo:AddonManagerRepo):
""" Updating a macro happens in this function, in the current thread """
- with tempfile.TemporaryDirectory() as dir:
- temp_install_succeeded = macro.install(dir)
- if not temp_install_succeeded:
- failed = True
+ cache_path = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "MacroCache")
+ os.makedirs(cache_path, exist_ok=True)
+ temp_install_succeeded = macro.install(cache_path)
+ if not temp_install_succeeded:
+ failed = True
if not failed:
failed = macro.install(self.macro_repo_dir)
diff --git a/src/Mod/AddonManager/compact_view.py b/src/Mod/AddonManager/compact_view.py
new file mode 100644
index 0000000000..f5b6bc3962
--- /dev/null
+++ b/src/Mod/AddonManager/compact_view.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+
+################################################################################
+## Form generated from reading UI file 'compact_view.ui'
+##
+## Created by: Qt User Interface Compiler version 5.15.1
+##
+## WARNING! All changes made in this file will be lost when recompiling UI file!
+################################################################################
+
+from PySide2.QtCore import *
+from PySide2.QtGui import *
+from PySide2.QtWidgets import *
+
+
+class Ui_CompactView(object):
+ def setupUi(self, CompactView):
+ if not CompactView.objectName():
+ CompactView.setObjectName(u"CompactView")
+ CompactView.resize(489, 16)
+ sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(CompactView.sizePolicy().hasHeightForWidth())
+ CompactView.setSizePolicy(sizePolicy)
+ self.horizontalLayout_2 = QHBoxLayout(CompactView)
+ self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
+ self.horizontalLayout_2.setSizeConstraint(QLayout.SetNoConstraint)
+ self.horizontalLayout_2.setContentsMargins(3, 0, 9, 0)
+ self.labelIcon = QLabel(CompactView)
+ self.labelIcon.setObjectName(u"labelIcon")
+ sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ sizePolicy1.setHorizontalStretch(0)
+ sizePolicy1.setVerticalStretch(0)
+ sizePolicy1.setHeightForWidth(self.labelIcon.sizePolicy().hasHeightForWidth())
+ self.labelIcon.setSizePolicy(sizePolicy1)
+ self.labelIcon.setMinimumSize(QSize(16, 16))
+ self.labelIcon.setBaseSize(QSize(16, 16))
+
+ self.horizontalLayout_2.addWidget(self.labelIcon)
+
+ self.labelPackageName = QLabel(CompactView)
+ self.labelPackageName.setObjectName(u"labelPackageName")
+
+ self.horizontalLayout_2.addWidget(self.labelPackageName)
+
+ self.labelVersion = QLabel(CompactView)
+ self.labelVersion.setObjectName(u"labelVersion")
+
+ self.horizontalLayout_2.addWidget(self.labelVersion)
+
+ self.labelDescription = QLabel(CompactView)
+ self.labelDescription.setObjectName(u"labelDescription")
+ sizePolicy2 = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
+ sizePolicy2.setHorizontalStretch(0)
+ sizePolicy2.setVerticalStretch(0)
+ sizePolicy2.setHeightForWidth(self.labelDescription.sizePolicy().hasHeightForWidth())
+ self.labelDescription.setSizePolicy(sizePolicy2)
+ self.labelDescription.setTextFormat(Qt.PlainText)
+ self.labelDescription.setWordWrap(False)
+ self.labelDescription.setTextInteractionFlags(Qt.TextSelectableByMouse)
+
+ self.horizontalLayout_2.addWidget(self.labelDescription)
+
+ self.labelStatus = QLabel(CompactView)
+ self.labelStatus.setObjectName(u"labelStatus")
+
+ self.horizontalLayout_2.addWidget(self.labelStatus)
+
+
+ self.retranslateUi(CompactView)
+
+ QMetaObject.connectSlotsByName(CompactView)
+ # setupUi
+
+ def retranslateUi(self, CompactView):
+ CompactView.setWindowTitle(QCoreApplication.translate("CompactView", u"Form", None))
+ self.labelIcon.setText(QCoreApplication.translate("CompactView", u"Icon", None))
+ self.labelPackageName.setText(QCoreApplication.translate("CompactView", u"Package Name ", None))
+ self.labelVersion.setText(QCoreApplication.translate("CompactView", u"Version", None))
+ self.labelDescription.setText(QCoreApplication.translate("CompactView", u"Description", None))
+ self.labelStatus.setText(QCoreApplication.translate("CompactView", u"UpdateAvailable", None))
+ # retranslateUi
+
diff --git a/src/Mod/AddonManager/compact_view.ui b/src/Mod/AddonManager/compact_view.ui
new file mode 100644
index 0000000000..d0b08c9c7c
--- /dev/null
+++ b/src/Mod/AddonManager/compact_view.ui
@@ -0,0 +1,110 @@
+
+
+ CompactView
+
+
+
+ 0
+ 0
+ 489
+ 16
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ QLayout::SetNoConstraint
+
+
+ 3
+
+
+ 0
+
+
+ 9
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+ 16
+
+
+
+
+ 16
+ 16
+
+
+
+ Icon
+
+
+
+ -
+
+
+ <b>Package Name</b>
+
+
+
+ -
+
+
+ Version
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Description
+
+
+ Qt::PlainText
+
+
+ false
+
+
+ Qt::TextSelectableByMouse
+
+
+
+ -
+
+
+ UpdateAvailable
+
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/expanded_view.py b/src/Mod/AddonManager/expanded_view.py
new file mode 100644
index 0000000000..12e4a56548
--- /dev/null
+++ b/src/Mod/AddonManager/expanded_view.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+
+################################################################################
+## Form generated from reading UI file 'expanded_view.ui'
+##
+## Created by: Qt User Interface Compiler version 5.15.1
+##
+## WARNING! All changes made in this file will be lost when recompiling UI file!
+################################################################################
+
+from PySide2.QtCore import *
+from PySide2.QtGui import *
+from PySide2.QtWidgets import *
+
+
+class Ui_ExpandedView(object):
+ def setupUi(self, ExpandedView):
+ if not ExpandedView.objectName():
+ ExpandedView.setObjectName(u"ExpandedView")
+ ExpandedView.resize(657, 64)
+ sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(ExpandedView.sizePolicy().hasHeightForWidth())
+ ExpandedView.setSizePolicy(sizePolicy)
+ self.horizontalLayout_2 = QHBoxLayout(ExpandedView)
+ self.horizontalLayout_2.setSpacing(2)
+ self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
+ self.horizontalLayout_2.setSizeConstraint(QLayout.SetNoConstraint)
+ self.horizontalLayout_2.setContentsMargins(2, 0, 2, 0)
+ self.labelIcon = QLabel(ExpandedView)
+ self.labelIcon.setObjectName(u"labelIcon")
+ sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ sizePolicy1.setHorizontalStretch(0)
+ sizePolicy1.setVerticalStretch(0)
+ sizePolicy1.setHeightForWidth(self.labelIcon.sizePolicy().hasHeightForWidth())
+ self.labelIcon.setSizePolicy(sizePolicy1)
+ self.labelIcon.setMinimumSize(QSize(48, 48))
+ self.labelIcon.setMaximumSize(QSize(48, 48))
+ self.labelIcon.setBaseSize(QSize(48, 48))
+
+ self.horizontalLayout_2.addWidget(self.labelIcon)
+
+ self.horizontalSpacer = QSpacerItem(8, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)
+
+ self.horizontalLayout_2.addItem(self.horizontalSpacer)
+
+ self.verticalLayout = QVBoxLayout()
+ self.verticalLayout.setSpacing(3)
+ self.verticalLayout.setObjectName(u"verticalLayout")
+ self.horizontalLayout = QHBoxLayout()
+ self.horizontalLayout.setSpacing(10)
+ self.horizontalLayout.setObjectName(u"horizontalLayout")
+ self.labelPackageName = QLabel(ExpandedView)
+ self.labelPackageName.setObjectName(u"labelPackageName")
+
+ self.horizontalLayout.addWidget(self.labelPackageName)
+
+ self.labelVersion = QLabel(ExpandedView)
+ self.labelVersion.setObjectName(u"labelVersion")
+ sizePolicy2 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
+ sizePolicy2.setHorizontalStretch(0)
+ sizePolicy2.setVerticalStretch(0)
+ sizePolicy2.setHeightForWidth(self.labelVersion.sizePolicy().hasHeightForWidth())
+ self.labelVersion.setSizePolicy(sizePolicy2)
+
+ self.horizontalLayout.addWidget(self.labelVersion)
+
+ self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
+
+ self.horizontalLayout.addItem(self.horizontalSpacer_2)
+
+
+ self.verticalLayout.addLayout(self.horizontalLayout)
+
+ self.labelDescription = QLabel(ExpandedView)
+ self.labelDescription.setObjectName(u"labelDescription")
+ sizePolicy.setHeightForWidth(self.labelDescription.sizePolicy().hasHeightForWidth())
+ self.labelDescription.setSizePolicy(sizePolicy)
+ self.labelDescription.setTextFormat(Qt.PlainText)
+ self.labelDescription.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop)
+ self.labelDescription.setWordWrap(False)
+
+ self.verticalLayout.addWidget(self.labelDescription)
+
+ self.labelMaintainer = QLabel(ExpandedView)
+ self.labelMaintainer.setObjectName(u"labelMaintainer")
+ sizePolicy2.setHeightForWidth(self.labelMaintainer.sizePolicy().hasHeightForWidth())
+ self.labelMaintainer.setSizePolicy(sizePolicy2)
+ self.labelMaintainer.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop)
+ self.labelMaintainer.setWordWrap(False)
+
+ self.verticalLayout.addWidget(self.labelMaintainer)
+
+
+ self.horizontalLayout_2.addLayout(self.verticalLayout)
+
+ self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
+
+ self.horizontalLayout_2.addItem(self.horizontalSpacer_3)
+
+ self.labelStatus = QLabel(ExpandedView)
+ self.labelStatus.setObjectName(u"labelStatus")
+ self.labelStatus.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
+
+ self.horizontalLayout_2.addWidget(self.labelStatus)
+
+
+ self.retranslateUi(ExpandedView)
+
+ QMetaObject.connectSlotsByName(ExpandedView)
+ # setupUi
+
+ def retranslateUi(self, ExpandedView):
+ ExpandedView.setWindowTitle(QCoreApplication.translate("ExpandedView", u"Form", None))
+ self.labelIcon.setText(QCoreApplication.translate("ExpandedView", u"Icon", None))
+ self.labelPackageName.setText(QCoreApplication.translate("ExpandedView", u"Package Name ", None))
+ self.labelVersion.setText(QCoreApplication.translate("ExpandedView", u"Version", None))
+ self.labelDescription.setText(QCoreApplication.translate("ExpandedView", u"Description", None))
+ self.labelMaintainer.setText(QCoreApplication.translate("ExpandedView", u"Maintainer", None))
+ self.labelStatus.setText(QCoreApplication.translate("ExpandedView", u"UpdateAvailable", None))
+ # retranslateUi
+
diff --git a/src/Mod/AddonManager/expanded_view.ui b/src/Mod/AddonManager/expanded_view.ui
new file mode 100644
index 0000000000..78934b525d
--- /dev/null
+++ b/src/Mod/AddonManager/expanded_view.ui
@@ -0,0 +1,203 @@
+
+
+ ExpandedView
+
+
+
+ 0
+ 0
+ 657
+ 64
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 2
+
+
+ QLayout::SetNoConstraint
+
+
+ 2
+
+
+ 0
+
+
+ 2
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 48
+ 48
+
+
+
+
+ 48
+ 48
+
+
+
+
+ 48
+ 48
+
+
+
+ Icon
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 8
+ 20
+
+
+
+
+ -
+
+
+ 3
+
+ -
+
+
+ 10
+
+ -
+
+
+ <h1>Package Name</h1>
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Version
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Description
+
+
+ Qt::PlainText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ false
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Maintainer
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ false
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ UpdateAvailable
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py
new file mode 100644
index 0000000000..6828dad284
--- /dev/null
+++ b/src/Mod/AddonManager/package_details.py
@@ -0,0 +1,245 @@
+# -*- 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
+
+class PackageDetails(QWidget):
+
+ backClicked = Signal()
+ installClicked = Signal()
+ uninstallClicked = Signal()
+ updateClicked = Signal()
+ executeClicked = Signal()
+
+ 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.backClicked.emit)
+ self.ui.buttonRefresh.clicked.connect(self.refresh)
+ self.ui.buttonExecute.clicked.connect(self.executeClicked.emit)
+ self.ui.buttonInstall.clicked.connect(self.installClicked.emit)
+ self.ui.buttonUninstall.clicked.connect(self.uninstallClicked.emit)
+ self.ui.buttonUpdate.clicked.connect(self.updateClicked.emit)
+
+ def show_repo(self, repo:AddonManagerRepo) -> None:
+
+ self.repo = repo
+
+ if self.worker is not None:
+ if not self.worker.isFinished():
+ self.worker.requestInterruption()
+ self.worker.wait()
+
+ self.check_and_clean_cache(repo)
+
+ 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:
+ self.ui.buttonInstall.show()
+ self.ui.buttonUninstall.hide()
+ self.ui.buttonUpdate.hide()
+ elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
+ self.ui.buttonInstall.hide()
+ self.ui.buttonUninstall.show()
+ self.ui.buttonUpdate.hide()
+ elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
+ self.ui.buttonInstall.hide()
+ self.ui.buttonUninstall.show()
+ self.ui.buttonUpdate.show()
+ elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
+ self.ui.buttonInstall.hide()
+ self.ui.buttonUninstall.show()
+ self.ui.buttonUpdate.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:
+ 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.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.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(u"PackageDetails")
+ self.verticalLayout_2 = QVBoxLayout(PackageDetails)
+ self.verticalLayout_2.setObjectName(u"verticalLayout_2")
+ self.layoutDetailsBackButton = QHBoxLayout()
+ self.layoutDetailsBackButton.setObjectName(u"layoutDetailsBackButton")
+ self.buttonBack = QToolButton(PackageDetails)
+ self.buttonBack.setObjectName(u"buttonBack")
+ self.buttonBack.setIcon(QIcon.fromTheme("back", QIcon(":/icons/button_left.svg")))
+ self.buttonRefresh = QToolButton(PackageDetails)
+ self.buttonRefresh.setObjectName(u"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.verticalLayout_2.addLayout(self.layoutDetailsBackButton)
+
+ self.layoutDetailsInstallButtons = QHBoxLayout()
+ self.layoutDetailsInstallButtons.setObjectName(u"layoutDetailsInstallButtons")
+ self.buttonInstall = QPushButton(PackageDetails)
+ self.buttonInstall.setObjectName(u"buttonInstall")
+
+ self.layoutDetailsInstallButtons.addWidget(self.buttonInstall)
+
+ self.buttonUninstall = QPushButton(PackageDetails)
+ self.buttonUninstall.setObjectName(u"buttonUninstall")
+
+ self.layoutDetailsInstallButtons.addWidget(self.buttonUninstall)
+
+ self.buttonUpdate = QPushButton(PackageDetails)
+ self.buttonUpdate.setObjectName(u"buttonUpdate")
+
+ self.layoutDetailsInstallButtons.addWidget(self.buttonUpdate)
+
+ self.buttonExecute = QPushButton(PackageDetails)
+ self.buttonExecute.setObjectName(u"buttonExecute")
+
+ self.layoutDetailsInstallButtons.addWidget(self.buttonExecute)
+
+
+ self.verticalLayout_2.addLayout(self.layoutDetailsInstallButtons)
+
+ self.textBrowserReadMe = QTextBrowser(PackageDetails)
+ self.textBrowserReadMe.setObjectName(u"textBrowserReadMe")
+
+ 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", u"Install", None))
+ self.buttonUninstall.setText(QCoreApplication.translate("AddonsInstaller", u"Uninstall", None))
+ self.buttonUpdate.setText(QCoreApplication.translate("AddonsInstaller", u"Update", None))
+ self.buttonExecute.setText(QCoreApplication.translate("AddonsInstaller", u"Run Macro", None))
+ self.buttonBack.setToolTip(QCoreApplication.translate("AddonsInstaller", u"Return to package list", None))
+ self.buttonRefresh.setToolTip(QCoreApplication.translate("AddonsInstaller", u"Delete cached version of this README and re-download", None))
+ # retranslateUi
+
diff --git a/src/Mod/AddonManager/package_details.ui b/src/Mod/AddonManager/package_details.ui
new file mode 100644
index 0000000000..a63345505c
--- /dev/null
+++ b/src/Mod/AddonManager/package_details.ui
@@ -0,0 +1,90 @@
+
+
+ PackageDetails
+
+
+
+ 0
+ 0
+ 755
+ 367
+
+
+
+ Form
+
+
+ -
+
+ -
+
+
+
+
+
+
+ -
+
+
+ ...
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+ -
+
+
+ Uninstalls a selected macro or workbench
+
+
+ Install
+
+
+
+ -
+
+
+ Uninstall
+
+
+
+ -
+
+
+ Update
+
+
+
+ -
+
+
+ Run Macro
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py
new file mode 100644
index 0000000000..ce116be7cb
--- /dev/null
+++ b/src/Mod/AddonManager/package_list.py
@@ -0,0 +1,439 @@
+# -*- 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 *
+#* *
+#***************************************************************************
+
+import FreeCAD
+
+from PySide2.QtCore import *
+from PySide2.QtGui import *
+from PySide2.QtWidgets import *
+
+from typing import Dict, Union
+from enum import IntEnum
+import threading
+
+from addonmanager_utilities import translate # this needs to be as is for pylupdate
+from AddonManagerRepo import AddonManagerRepo
+
+from compact_view import Ui_CompactView
+from expanded_view import Ui_ExpandedView
+
+class ListDisplayStyle(IntEnum):
+ COMPACT = 0
+ EXPANDED = 1
+
+class PackageList(QWidget):
+ """ A widget that shows a list of packages and various widgets to control the display of the list """
+
+ itemSelected = Signal(AddonManagerRepo)
+
+ def __init__(self,parent=None):
+ super().__init__(parent)
+ self.ui = Ui_PackageList()
+ self.ui.setupUi(self)
+
+ self.item_filter = PackageListFilter()
+ self.ui.listPackages.setModel (self.item_filter)
+ self.item_delegate = PackageListItemDelegate()
+ self.ui.listPackages.setItemDelegate(self.item_delegate)
+
+ self.ui.listPackages.clicked.connect(self.on_listPackages_clicked)
+ self.ui.comboPackageType.currentIndexChanged.connect(self.update_type_filter)
+ self.ui.lineEditFilter.textChanged.connect(self.update_text_filter)
+ self.ui.buttonCompactLayout.clicked.connect(lambda: self.set_view_style(ListDisplayStyle.COMPACT))
+ self.ui.buttonExpandedLayout.clicked.connect(lambda: self.set_view_style(ListDisplayStyle.EXPANDED))
+
+ # Only shows when the user types in a filter
+ self.ui.labelFilterValidity.hide()
+
+ # Set up the view the same as the last time:
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ package_type = pref.GetInt("PackageTypeSelection", 1)
+ self.ui.comboPackageType.setCurrentIndex(package_type)
+
+ def setModel(self, model):
+ self.item_model = model
+ self.item_filter.setSourceModel(self.item_model)
+ self.item_filter.sort(0)
+
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ style = pref.GetInt("ViewStyle", ListDisplayStyle.EXPANDED)
+ self.set_view_style(style)
+ if style == ListDisplayStyle.EXPANDED:
+ self.ui.buttonExpandedLayout.setChecked(True)
+ else:
+ self.ui.buttonCompactLayout.setChecked(True)
+
+ def on_listPackages_clicked(self, index:QModelIndex):
+ source_selection = self.item_filter.mapToSource (index)
+ selected_repo = self.item_model.repos[source_selection.row()]
+ self.itemSelected.emit(selected_repo)
+
+ def update_type_filter(self, type_filter:int) -> None:
+ """hide/show rows corresponding to the type filter
+
+ type_filter is an integer: 0 for all, 1 for workbenches, 2 for macros, and 3 for preference packs
+
+ """
+
+ self.item_filter.setPackageFilter(type_filter)
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ pref.SetInt("PackageTypeSelection", type_filter)
+
+ def update_text_filter(self, text_filter:str) -> None:
+ """filter name and description by the regex specified by text_filter"""
+
+ if text_filter:
+ test_regex = QRegularExpression(text_filter)
+ if test_regex.isValid():
+ self.ui.labelFilterValidity.setToolTip(translate("AddonsInstaller","Filter is valid"))
+ icon = QIcon.fromTheme("ok", QIcon(":/icons/edit_OK.svg"))
+ self.ui.labelFilterValidity.setPixmap(icon.pixmap(16,16))
+ else:
+ self.ui.labelFilterValidity.setToolTip(translate("AddonsInstaller","Filter regular expression is invalid"))
+ icon = QIcon.fromTheme("cancel", QIcon(":/icons/edit_Cancel.svg"))
+ self.ui.labelFilterValidity.setPixmap(icon.pixmap(16,16))
+ self.ui.labelFilterValidity.show()
+ else:
+ self.ui.labelFilterValidity.hide()
+ self.item_filter.setFilterRegularExpression(text_filter)
+
+ def set_view_style(self, style:ListDisplayStyle) -> None:
+ self.item_delegate.set_view(style)
+ if style == ListDisplayStyle.COMPACT:
+ self.ui.listPackages.setSpacing(2)
+ else:
+ self.ui.listPackages.setSpacing(5)
+ self.item_model.layoutChanged.emit()
+
+ pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
+ pref.SetInt("ViewStyle", style)
+
+class PackageListItemModel(QAbstractListModel):
+
+ repos = []
+ write_lock = threading.Lock()
+
+ DataAccessRole = Qt.UserRole
+ StatusUpdateRole = Qt.UserRole + 1
+ IconUpdateRole = Qt.UserRole + 2
+
+ def __init__(self, parent=None) -> None:
+ super().__init__(parent)
+
+ def rowCount(self, parent:QModelIndex=QModelIndex()) -> int:
+ if parent.isValid():
+ return 0
+ return len(self.repos)
+
+ def columnCount(self, parent:QModelIndex=QModelIndex()) -> int:
+ if parent.isValid():
+ return 0
+ return 1
+
+ def data(self, index:QModelIndex, role:int=Qt.DisplayRole):
+ if not index.isValid():
+ return None
+ row = index.row()
+ if role == Qt.ToolTipRole:
+ tooltip = ""
+ if self.repos[row].repo_type == AddonManagerRepo.RepoType.PACKAGE:
+ tooltip = translate("AddonsInstaller","Click for details about package") + f" '{self.repos[row].name}'"
+ elif self.repos[row].repo_type == AddonManagerRepo.RepoType.WORKBENCH:
+ tooltip = translate("AddonsInstaller","Click for details about workbench") + f" '{self.repos[row].name}'"
+ elif self.repos[row].repo_type == AddonManagerRepo.RepoType.MACRO:
+ tooltip = translate("AddonsInstaller","Click for details about macro") + f" '{self.repos[row].name}'"
+ return tooltip
+ elif role == PackageListItemModel.DataAccessRole:
+ return self.repos[row]
+
+ def headerData(self, section, orientation, role=Qt.DisplayRole):
+ return None
+
+ def setData(self, index:QModelIndex, value, role=Qt.EditRole) -> None:
+ """ Set the data for this row. The column of the index is ignored. """
+
+ row = index.row()
+ self.write_lock.acquire()
+ if role == PackageListItemModel.StatusUpdateRole:
+ self.repos[row].update_status = value
+ self.dataChanged.emit(self.index(row,2), self.index(row,2), [PackageListItemModel.StatusUpdateRole])
+ elif role == PackageListItemModel.IconUpdateRole:
+ self.repos[row].icon = value
+ self.dataChanged.emit(self.index(row,0), self.index(row,0), [PackageListItemModel.IconUpdateRole])
+ self.write_lock.release()
+
+ def append_item(self, repo:AddonManagerRepo) -> None:
+ if repo in self.repos:
+ # Cowardly refuse to insert the same repo a second time
+ return
+ self.write_lock.acquire()
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
+ self.repos.append(repo)
+ self.endInsertRows()
+ self.write_lock.release()
+
+ def clear(self) -> None:
+ if self.rowCount() > 0:
+ self.write_lock.acquire()
+ self.beginRemoveRows(QModelIndex(), 0, self.rowCount()-1)
+ self.repos = []
+ self.endRemoveRows()
+ self.write_lock.release()
+
+ def update_item_status(self, name:str, status:AddonManagerRepo.UpdateStatus) -> None:
+ for row,item in enumerate(self.repos):
+ if item.name == name:
+ self.setData(self.index(row,0), status, PackageListItemModel.StatusUpdateRole)
+ return
+
+ def update_item_icon(self, name:str, icon:QIcon) -> None:
+ for row,item in enumerate(self.repos):
+ if item.name == name:
+ self.setData(self.index(row,0), icon, PackageListItemModel.IconUpdateRole)
+ return
+
+ def reload_item(self,repo:AddonManagerRepo) -> None:
+ for index,item in enumerate(self.repos):
+ if item.name == repo.name:
+ self.write_lock.acquire()
+ self.repos[index] = repo
+ self.write_lock.release()
+ return
+
+class CompactView(QWidget):
+ """ A single-line view of the package information """
+
+ from compact_view import Ui_CompactView
+
+ def __init__(self,parent=None):
+ super().__init__(parent)
+ self.ui = Ui_CompactView()
+ self.ui.setupUi(self)
+
+class ExpandedView(QWidget):
+ """ A multi-line view of the package information """
+
+ from expanded_view import Ui_ExpandedView
+
+ def __init__(self,parent=None):
+ super().__init__(parent)
+ self.ui = Ui_ExpandedView()
+ self.ui.setupUi(self)
+
+class PackageListItemDelegate(QStyledItemDelegate):
+ """ Render the repo data as a formatted region """
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.displayStyle = ListDisplayStyle.EXPANDED
+ self.expanded = ExpandedView()
+ self.compact = CompactView()
+ self.widget = self.expanded
+
+ def set_view (self, style:ListDisplayStyle) -> None:
+ if not self.displayStyle == style:
+ self.displayStyle = style
+
+ def sizeHint(self, option, index):
+ self.update_content(index)
+ return self.widget.sizeHint()
+
+ def update_content(self, index):
+ repo = index.data(PackageListItemModel.DataAccessRole)
+ if self.displayStyle == ListDisplayStyle.EXPANDED:
+ self.widget = self.expanded
+ self.widget.ui.labelPackageName.setText(f"{repo.name} ")
+ self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(48,48)))
+ else:
+ self.widget = self.compact
+ self.widget.ui.labelPackageName.setText(f"{repo.name} ")
+ self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(16,16)))
+
+ self.widget.ui.labelIcon.setText("")
+ if repo.metadata:
+ self.widget.ui.labelDescription.setText(repo.metadata.Description)
+ self.widget.ui.labelVersion.setText(f"v{repo.metadata.Version} ")
+ if self.displayStyle == ListDisplayStyle.EXPANDED:
+ maintainers = repo.metadata.Maintainer
+ maintainers_string = ""
+ if len(maintainers) == 1:
+ maintainers_string = translate("AddonsInstaller","Maintainer") + f": {maintainers[0]['name']} <{maintainers[0]['email']}>"
+ elif len(maintainers) > 1:
+ maintainers_string = translate("AddonsInstaller","Maintainers:")
+ for maintainer in maintainers:
+ maintainers_string += f"\n{maintainer['name']} <{maintainer['email']}>"
+ self.widget.ui.labelMaintainer.setText(maintainers_string)
+ else:
+ self.widget.ui.labelDescription.setText("")
+ self.widget.ui.labelVersion.setText("")
+ if self.displayStyle == ListDisplayStyle.EXPANDED:
+ self.widget.ui.labelMaintainer.setText("")
+
+ # Update status
+ if repo.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
+ self.widget.ui.labelStatus.setText("")
+ elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
+ self.widget.ui.labelStatus.setText(translate("AddonsInstaller","Installed"))
+ elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
+ self.widget.ui.labelStatus.setText(translate("AddonsInstaller","Up-to-date"))
+ elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
+ self.widget.ui.labelStatus.setText(translate("AddonsInstaller","Update available"))
+ elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
+ self.widget.ui.labelStatus.setText(translate("AddonsInstaller","Pending restart"))
+ self.widget.adjustSize()
+
+ def paint(self, painter:QPainter, option:QStyleOptionViewItem, index:QModelIndex):
+ painter.save()
+ self.widget.resize(option.rect.size())
+ painter.translate(option.rect.topLeft())
+ self.widget.render(painter, QPoint(), QRegion(), QWidget.DrawChildren)
+ painter.restore()
+
+class PackageListFilter(QSortFilterProxyModel):
+ """ Handle filtering the item list on various criteria """
+
+ def __init__(self):
+ super().__init__()
+ self.package_type = 0 # Default to showing everything
+ self.setSortCaseSensitivity(Qt.CaseInsensitive)
+
+ def setPackageFilter(self, type:int) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs
+ self.package_type = type
+ self.invalidateFilter()
+
+ def lessThan(self, left, right) -> bool:
+ 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()
+
+ def filterAcceptsRow(self, row, parent=QModelIndex()):
+ index = self.sourceModel().createIndex(row, 0)
+ data = self.sourceModel().data(index,PackageListItemModel.DataAccessRole)
+ if self.package_type == 1:
+ if not data.contains_workbench():
+ return False
+ elif self.package_type == 2:
+ if not data.contains_macro():
+ return False
+ elif self.package_type == 3:
+ 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
+ re = self.filterRegularExpression()
+ if re.isValid():
+ re.setPatternOptions(QRegularExpression.CaseInsensitiveOption)
+ if re.match(name).hasMatch():
+ return True
+ if re.match(desc).hasMatch():
+ return True
+ return False
+ else:
+ return False
+
+class Ui_PackageList(object):
+ """ The contents of the PackageList widget """
+
+ def setupUi(self, Form):
+ if not Form.objectName():
+ Form.setObjectName(u"PackageList")
+ self.verticalLayout = QVBoxLayout(Form)
+ self.verticalLayout.setObjectName(u"verticalLayout")
+ self.horizontalLayout_6 = QHBoxLayout()
+ self.horizontalLayout_6.setObjectName(u"horizontalLayout_6")
+ self.buttonCompactLayout = QToolButton(Form)
+ self.buttonCompactLayout.setObjectName(u"buttonCompactLayout")
+ self.buttonCompactLayout.setCheckable(True)
+ self.buttonCompactLayout.setAutoExclusive(True)
+ self.buttonCompactLayout.setIcon(QIcon.fromTheme("expanded_view", QIcon(":/icons/compact_view.svg")))
+
+ self.horizontalLayout_6.addWidget(self.buttonCompactLayout)
+
+ self.buttonExpandedLayout = QToolButton(Form)
+ self.buttonExpandedLayout.setObjectName(u"buttonExpandedLayout")
+ self.buttonExpandedLayout.setCheckable(True)
+ self.buttonExpandedLayout.setChecked(True)
+ self.buttonExpandedLayout.setAutoExclusive(True)
+ self.buttonExpandedLayout.setIcon(QIcon.fromTheme("expanded_view", QIcon(":/icons/expanded_view.svg")))
+
+ self.horizontalLayout_6.addWidget(self.buttonExpandedLayout)
+
+ self.labelPackagesContaining = QLabel(Form)
+ self.labelPackagesContaining.setObjectName(u"labelPackagesContaining")
+
+ self.horizontalLayout_6.addWidget(self.labelPackagesContaining)
+
+ self.comboPackageType = QComboBox(Form)
+ self.comboPackageType.addItem("")
+ self.comboPackageType.addItem("")
+ self.comboPackageType.addItem("")
+ self.comboPackageType.addItem("")
+ self.comboPackageType.setObjectName(u"comboPackageType")
+
+ self.horizontalLayout_6.addWidget(self.comboPackageType)
+
+ self.lineEditFilter = QLineEdit(Form)
+ self.lineEditFilter.setObjectName(u"lineEditFilter")
+ self.lineEditFilter.setClearButtonEnabled(True)
+
+ self.horizontalLayout_6.addWidget(self.lineEditFilter)
+
+ self.labelFilterValidity = QLabel(Form)
+ self.labelFilterValidity.setObjectName(u"labelFilterValidity")
+
+ self.horizontalLayout_6.addWidget(self.labelFilterValidity)
+
+ self.verticalLayout.addLayout(self.horizontalLayout_6)
+
+ self.listPackages = QListView(Form)
+ self.listPackages.setObjectName(u"listPackages")
+ self.listPackages.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.listPackages.setProperty("showDropIndicator", False)
+ self.listPackages.setSelectionMode(QAbstractItemView.NoSelection)
+ self.listPackages.setLayoutMode(QListView.Batched)
+ self.listPackages.setBatchSize(15)
+ self.listPackages.setResizeMode(QListView.Adjust)
+ self.listPackages.setUniformItemSizes(False)
+ self.listPackages.setAlternatingRowColors(True)
+
+ self.verticalLayout.addWidget(self.listPackages)
+
+ self.retranslateUi(Form)
+
+ QMetaObject.connectSlotsByName(Form)
+
+ def retranslateUi(self, Form):
+ self.labelPackagesContaining.setText(QCoreApplication.translate("AddonsInstaller", u"Show packages containing:", None))
+ self.comboPackageType.setItemText(0, QCoreApplication.translate("AddonsInstaller", u"All", None))
+ self.comboPackageType.setItemText(1, QCoreApplication.translate("AddonsInstaller", u"Workbenches", None))
+ self.comboPackageType.setItemText(2, QCoreApplication.translate("AddonsInstaller", u"Macros", None))
+ self.comboPackageType.setItemText(3, QCoreApplication.translate("AddonsInstaller", u"Preference Packs", None))
+
+ self.lineEditFilter.setPlaceholderText(QCoreApplication.translate("AddonsInstaller", u"Filter", None))
+ self.labelFilterValidity.setText(QCoreApplication.translate("AddonsInstaller", u"OK", None))
+