Addon Manager: Begin UI redesign
Migrate to a UI that looks more like other software package managers, giving more space to each addon by hiding the list when an addon is selected, and providing a "back" button to get back. Implements a two-style view option for the list of addons: expanded and condensed, via a delegate that provides the drawing function for each row in the table based on two different widget designs.
This commit is contained in:
@@ -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"<h1>{repo.metadata.Name}</h1>")
|
||||
|
||||
# Description
|
||||
self.dialog.labelDescription.setText(repo.metadata.Description)
|
||||
|
||||
# Version
|
||||
self.dialog.labelVersion.setText(f"<h3>v{repo.metadata.Version}</h3>")
|
||||
|
||||
# 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"<h2>{content_type}</h2>"
|
||||
content_string += "<ul>"
|
||||
for item in item_list:
|
||||
content_string += f"<li><b>{item.Name}</b> – {item.Description}</li>"
|
||||
content_string += "</ul>"
|
||||
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)
|
||||
# @}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>599</width>
|
||||
<height>480</height>
|
||||
<width>800</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -15,282 +15,98 @@
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<widget class="QWidget" name="contentPlaceholder" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="verticalLayoutWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="layoutUpdateInProgress">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Show packages containing:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboPackageType">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>All</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Workbenches</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Macros</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Preference Packs</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="QToolButton" name="buttonShowDetails">
|
||||
<property name="toolTip">
|
||||
<string>Show details</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::RightArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEditFilter">
|
||||
<property name="placeholderText">
|
||||
<string>Filter</string>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelFilterValidity">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="QLabel" name="labelUpdateInProgress">
|
||||
<property name="text">
|
||||
<string>Loading...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableView" name="tablePackages">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>12</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="textVisible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>16</number>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderMinimumSectionSize">
|
||||
<number>12</number>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderDefaultSectionSize">
|
||||
<number>16</number>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="verticalLayoutWidget_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonConfigure">
|
||||
<property name="toolTip">
|
||||
<string>Sets configuration options for the Addon Manager</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Configure...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelIcon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Icon</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelPackageName">
|
||||
<property name="text">
|
||||
<string><h1>Package Name</h1></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelVersion">
|
||||
<property name="text">
|
||||
<string><em>Version</em></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelMaintainer">
|
||||
<property name="text">
|
||||
<string>Maintainer</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelDescription">
|
||||
<property name="text">
|
||||
<string>Description</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
<property name="format">
|
||||
<string>Downloading info...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="urlGrid">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelUrlType">
|
||||
<property name="text">
|
||||
<string>URL Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="labelUrl">
|
||||
<property name="text">
|
||||
<string>Url</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelContents">
|
||||
<property name="text">
|
||||
<string>Package contents:</string>
|
||||
<widget class="QToolButton" name="buttonPauseUpdate">
|
||||
<property name="toolTip">
|
||||
<string>Pause cache update</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="description"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonExecute">
|
||||
<property name="text">
|
||||
<string>Run Macro</string>
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelStatusInfo">
|
||||
<property name="text">
|
||||
<string>labelStatusInfo</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
<widget class="QLabel" name="labelStatusInfo">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="textVisible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="format">
|
||||
<string>Downloading info...</string>
|
||||
<property name="text">
|
||||
<string>labelStatusInfo</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -302,22 +118,9 @@
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUninstall">
|
||||
<property name="toolTip">
|
||||
<string>Uninstalls a selected macro or workbench</string>
|
||||
</property>
|
||||
<widget class="QPushButton" name="buttonUpdateCache">
|
||||
<property name="text">
|
||||
<string>Uninstall selected</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonInstall">
|
||||
<property name="toolTip">
|
||||
<string>Installs or updates the selected macro or workbench</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Install/update selected</string>
|
||||
<string>Refresh local cache</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -327,10 +130,23 @@
|
||||
<string>Download and apply all available updates</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Update all</string>
|
||||
<string>Update all packages</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonClose">
|
||||
<property name="toolTip">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
<file>icons/WebTools_workbench_icon.svg</file>
|
||||
<file>icons/workfeature_workbench_icon.svg</file>
|
||||
<file>icons/yaml-workspace_workbench_icon.svg</file>
|
||||
<file>icons/expanded_view.svg</file>
|
||||
<file>icons/compact_view.svg</file>
|
||||
<file>translations/AddonManager_af.qm</file>
|
||||
<file>translations/AddonManager_ar.qm</file>
|
||||
<file>translations/AddonManager_ca.qm</file>
|
||||
|
||||
15
src/Mod/AddonManager/Resources/icons/compact_view.svg
Normal file
15
src/Mod/AddonManager/Resources/icons/compact_view.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
|
||||
.st1{fill:none;stroke:#000000;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<rect x="0.5" y="1.5" class="st0" width="3" height="3"/>
|
||||
<rect x="0.5" y="6.5" class="st0" width="3" height="3"/>
|
||||
<rect x="0.5" y="11.5" class="st0" width="3" height="3"/>
|
||||
<line class="st1" x1="6" y1="3.5" x2="16" y2="3.5"/>
|
||||
<line class="st1" x1="6" y1="8.5" x2="16" y2="8.5"/>
|
||||
<line class="st1" x1="6" y1="13.5" x2="16" y2="13.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 829 B |
15
src/Mod/AddonManager/Resources/icons/expanded_view.svg
Normal file
15
src/Mod/AddonManager/Resources/icons/expanded_view.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
|
||||
.st1{fill:none;stroke:#000000;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<rect x="0.5" y="1.5" class="st0" width="5" height="5"/>
|
||||
<line class="st1" x1="7" y1="2.5" x2="16" y2="2.5"/>
|
||||
<line class="st1" x1="7" y1="10.5" x2="16" y2="10.5"/>
|
||||
<line class="st1" x1="7" y1="13.5" x2="16" y2="13.5"/>
|
||||
<rect x="0.5" y="9.5" class="st0" width="5" height="5"/>
|
||||
<line class="st1" x1="7" y1="5.5" x2="16" y2="5.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 826 B |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<name>.*)"\]\s*'
|
||||
r"path\s*=\s*(?P<path>.+)\s*"
|
||||
r"url\s*=\s*(?P<url>https?://.*)\s*"
|
||||
r"(branch\s*=\s*(?P<branch>.*)\s*)?"), p)
|
||||
r"(branch\s*=\s*(?P<branch>[^\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 = """
|
||||
<div style="width: 100%; text-align:center;background: #91bbe0;">
|
||||
<strong style="color: #FFFFFF;">
|
||||
"""
|
||||
message += translate("AddonsInstaller", "Raw markdown displayed")
|
||||
message += "</strong><br/><br/>"
|
||||
message += translate("AddonsInstaller", "Python Markdown library is missing.")
|
||||
message += "<br/></div><hr/><pre>" + desc + "</pre>"
|
||||
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 = """
|
||||
<div style="width: 100%; text-align:center;background: #91bbe0;">
|
||||
<strong style="color: #FFFFFF;">
|
||||
"""
|
||||
message += translate("AddonsInstaller", "Raw markdown displayed")
|
||||
message += "</strong><br/><br/>"
|
||||
message += translate("AddonsInstaller", "Python Markdown library is missing.")
|
||||
message += "<br/></div><hr/><pre>" + desc + "</pre>"
|
||||
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("<img.*?src=\"(.*?)\"", message)
|
||||
if imagepaths:
|
||||
storedimages = []
|
||||
store = os.path.join(FreeCAD.getUserAppDataDir(), "AddonManager", "Images")
|
||||
store = os.path.join(self.cache_path, "Images")
|
||||
if not os.path.exists(store):
|
||||
os.makedirs(store)
|
||||
with open(os.path.join(store,"download_in_progress"),"w") as f:
|
||||
f.write("If this file still exists, it's because a download was interrupted. It can be safely ignored.")
|
||||
for path in imagepaths:
|
||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||
return
|
||||
if QtCore.QThread.currentThread().isInterruptionRequested():
|
||||
return message
|
||||
if not self.mustLoadImages:
|
||||
@@ -759,6 +803,7 @@ class ShowWorker(QtCore.QThread):
|
||||
QtCore.Qt.FastTransformation))
|
||||
pix.save(storename, "jpeg", 100)
|
||||
message = message.replace("src=\"" + origpath, "src=\"file:///" + storename.replace("\\", "/"))
|
||||
os.remove(os.path.join(store,"download_in_progress"))
|
||||
return message
|
||||
return None
|
||||
|
||||
@@ -767,7 +812,7 @@ class GetMacroDetailsWorker(QtCore.QThread):
|
||||
"""Retrieve the macro details for a macro"""
|
||||
|
||||
status_message = QtCore.Signal(str)
|
||||
description_updated = QtCore.Signal(str)
|
||||
readme_updated = QtCore.Signal(str)
|
||||
done = QtCore.Signal()
|
||||
|
||||
def __init__(self, repo):
|
||||
@@ -795,7 +840,7 @@ class GetMacroDetailsWorker(QtCore.QThread):
|
||||
message = (already_installed_msg + "<h1>" + self.macro.name + "</h1>" + self.macro.desc + "<br/><br/>Macro location: <a href=\"" + self.macro.url + "\">" + self.macro.url + "</a>")
|
||||
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)
|
||||
|
||||
84
src/Mod/AddonManager/compact_view.py
Normal file
84
src/Mod/AddonManager/compact_view.py
Normal file
@@ -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"<b>Package Name</b>", 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
|
||||
|
||||
110
src/Mod/AddonManager/compact_view.ui
Normal file
110
src/Mod/AddonManager/compact_view.ui
Normal file
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CompactView</class>
|
||||
<widget class="QWidget" name="CompactView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>489</width>
|
||||
<height>16</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetNoConstraint</enum>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelIcon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Icon</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelPackageName">
|
||||
<property name="text">
|
||||
<string><b>Package Name</b></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelVersion">
|
||||
<property name="text">
|
||||
<string>Version</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Description</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelStatus">
|
||||
<property name="text">
|
||||
<string>UpdateAvailable</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
123
src/Mod/AddonManager/expanded_view.py
Normal file
123
src/Mod/AddonManager/expanded_view.py
Normal file
@@ -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"<h1>Package Name</h1>", 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
|
||||
|
||||
203
src/Mod/AddonManager/expanded_view.ui
Normal file
203
src/Mod/AddonManager/expanded_view.ui
Normal file
@@ -0,0 +1,203 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ExpandedView</class>
|
||||
<widget class="QWidget" name="ExpandedView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>657</width>
|
||||
<height>64</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetNoConstraint</enum>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelIcon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>48</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>48</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>48</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Icon</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>8</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelPackageName">
|
||||
<property name="text">
|
||||
<string><h1>Package Name</h1></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelVersion">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Version</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelDescription">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Description</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelMaintainer">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Maintainer</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelStatus">
|
||||
<property name="text">
|
||||
<string>UpdateAvailable</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
245
src/Mod/AddonManager/package_details.py
Normal file
245
src/Mod/AddonManager/package_details.py
Normal file
@@ -0,0 +1,245 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#***************************************************************************
|
||||
#* *
|
||||
#* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
|
||||
#* *
|
||||
#* This program is free software; you can redistribute it and/or modify *
|
||||
#* it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
#* as published by the Free Software Foundation; either version 2 of *
|
||||
#* the License, or (at your option) any later version. *
|
||||
#* for detail see the LICENCE text file. *
|
||||
#* *
|
||||
#* This program is distributed in the hope that it will be useful, *
|
||||
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
#* GNU Library General Public License for more details. *
|
||||
#* *
|
||||
#* You should have received a copy of the GNU Library General Public *
|
||||
#* License along with this program; if not, write to the Free Software *
|
||||
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
#* USA *
|
||||
#* *
|
||||
#***************************************************************************
|
||||
from PySide2.QtCore import *
|
||||
from PySide2.QtGui import *
|
||||
from PySide2.QtWidgets import *
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from datetime import date, timedelta
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from addonmanager_utilities import translate # this needs to be as is for pylupdate
|
||||
from addonmanager_workers import ShowWorker, GetMacroDetailsWorker
|
||||
from AddonManagerRepo import AddonManagerRepo
|
||||
|
||||
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
|
||||
|
||||
90
src/Mod/AddonManager/package_details.ui
Normal file
90
src/Mod/AddonManager/package_details.ui
Normal file
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PackageDetails</class>
|
||||
<widget class="QWidget" name="PackageDetails">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>755</width>
|
||||
<height>367</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="layoutDetailsBackButton">
|
||||
<item>
|
||||
<widget class="QToolButton" name="buttonBack">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="buttonRefresh">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="layoutDetailsInstallButtons">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonInstall">
|
||||
<property name="toolTip">
|
||||
<string>Uninstalls a selected macro or workbench</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Install</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUninstall">
|
||||
<property name="text">
|
||||
<string>Uninstall</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUpdate">
|
||||
<property name="text">
|
||||
<string>Update</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonExecute">
|
||||
<property name="text">
|
||||
<string>Run Macro</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="textBrowserReadMe"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
439
src/Mod/AddonManager/package_list.py
Normal file
439
src/Mod/AddonManager/package_list.py
Normal file
@@ -0,0 +1,439 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#***************************************************************************
|
||||
#* *
|
||||
#* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
|
||||
#* *
|
||||
#* This program is free software; you can redistribute it and/or modify *
|
||||
#* it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
#* as published by the Free Software Foundation; either version 2 of *
|
||||
#* the License, or (at your option) any later version. *
|
||||
#* for detail see the LICENCE text file. *
|
||||
#* *
|
||||
#* This program is distributed in the hope that it will be useful, *
|
||||
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
#* GNU Library General Public License for more details. *
|
||||
#* *
|
||||
#* You should have received a copy of the GNU Library General Public *
|
||||
#* License along with this program; if not, write to the Free Software *
|
||||
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
#* USA *
|
||||
#* *
|
||||
#***************************************************************************
|
||||
|
||||
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"<h1>{repo.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.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"<i>v{repo.metadata.Version}</i>")
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user