Addon Manager: Improve cache behavior

Also includes a number of bug fixes.
This commit is contained in:
Chris Hennes
2021-12-05 18:46:21 -06:00
parent c6383e77f8
commit 52da213a3c
10 changed files with 157 additions and 196 deletions

View File

@@ -196,11 +196,11 @@ class CommandAddonManager:
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)
self.packageDetails.execute.connect(self.executemacro)
self.packageDetails.install.connect(self.install)
self.packageDetails.uninstall.connect(self.remove)
self.packageDetails.update.connect(self.update)
self.packageDetails.back.connect(self.on_buttonBack_clicked)
# center the dialog over the FreeCAD window
mw = FreeCADGui.getMainWindow()
@@ -254,13 +254,25 @@ class CommandAddonManager:
if not thread.isFinished():
thread.requestInterruption()
oktoclose = False
if not oktoclose:
while not oktoclose:
oktoclose = True
for worker in self.workers:
if hasattr(self, worker):
thread = getattr(self, worker)
if thread:
thread.wait()
thread.wait(25)
if not thread.isFinished():
oktoclose = False
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
# Write the cache data
for repo in self.item_model.repos:
if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
self.cache_macro(repo)
else:
self.cache_package(repo)
self.write_package_cache()
self.write_macro_cache()
# all threads have finished
if oktoclose:
@@ -345,15 +357,28 @@ class CommandAddonManager:
def populate_packages_table(self) -> None:
self.item_model.clear()
self.current_progress_region += 1
if self.update_cache or not os.path.isfile(self.get_cache_file_name("package_cache.json")):
use_cache = not self.update_cache
if use_cache:
if os.path.isfile(self.get_cache_file_name("package_cache.json")):
with open(self.get_cache_file_name("package_cache.json"),"r") as f:
data = f.read()
try:
from_json = json.loads(data)
if len(from_json) == 0:
use_cache = False
except Exception as e:
use_cache = False
else:
use_cache = False
if not use_cache:
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"))
@@ -371,6 +396,7 @@ class CommandAddonManager:
package_cache_path = self.get_cache_file_name("package_cache.json")
with open(package_cache_path,"w") as f:
f.write(json.dumps(self.package_cache))
self.package_cache = []
def activate_table_widgets(self) -> None:
self.packageList.setEnabled(True)
@@ -384,9 +410,7 @@ class CommandAddonManager:
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"))
@@ -403,6 +427,7 @@ class CommandAddonManager:
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))
self.macro_cache = []
def update_metadata_cache(self) -> None:
self.current_progress_region += 1
@@ -414,10 +439,7 @@ class CommandAddonManager:
self.update_metadata_cache_worker.package_updated.connect(self.on_package_updated)
self.update_metadata_cache_worker.start()
else:
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()
self.do_next_startup_phase()
def on_buttonUpdateCache_clicked(self) -> None:
self.update_cache = True
@@ -545,19 +567,13 @@ class CommandAddonManager:
self.item_model.append_item(repo)
def install(self) -> None:
def install(self, repo:AddonManagerRepo) -> None:
"""installs or updates a workbench, macro, or package"""
if hasattr(self, "install_worker") and self.install_worker:
if self.install_worker.isRunning():
return
if not hasattr(self, "selected_repo"):
FreeCAD.Console.PrintWarning ("Internal error: no selected repo\n")
return
repo = self.selected_repo
if not repo:
return
@@ -576,13 +592,17 @@ class CommandAddonManager:
# To try to ensure atomicity, test the installation into a temp directory first,
# and assume if that worked we have good odds of the real installation working
failed = False
errors = []
with tempfile.TemporaryDirectory() as dir:
temp_install_succeeded = macro.install(dir)
temp_install_succeeded, error_list = macro.install(dir)
if not temp_install_succeeded:
failed = True
errors = error_list
if not failed:
failed = macro.install(self.macro_repo_dir)
real_install_succeeded, errors = macro.install(self.macro_repo_dir)
if not real_install_succeeded:
failed = True
if not failed:
message = translate("AddonsInstaller",
@@ -590,9 +610,15 @@ class CommandAddonManager:
"now available from the Macros dialog.")
self.on_package_installed (repo, message)
else:
message = translate("AddonsInstaller", "Installation of macro failed. See console for failure details.")
message = translate("AddonsInstaller", "Installation of macro failed" + ":")
for error in errors:
message += "\n * "
message += error
self.on_installation_failed (repo, message)
def update(self, repo:AddonManagerRepo) -> None:
self.install(repo)
def update_all(self) -> None:
""" Asynchronously apply all available updates: individual failures are noted, but do not stop other updates """
@@ -682,12 +708,11 @@ class CommandAddonManager:
translate("AddonsInstaller", "Installation succeeded"),
message,
QtWidgets.QMessageBox.Close)
self.dialog.progressBar.hide()
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:
self.item_model.update_item_status(repo.name, AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE)
self.packageDetails.show_repo(repo, reload=True)
def on_installation_failed(self, _:AddonManagerRepo, message:str) -> None:
QtWidgets.QMessageBox.warning(None,
@@ -696,10 +721,10 @@ class CommandAddonManager:
QtWidgets.QMessageBox.Close)
self.dialog.progressBar.hide()
def executemacro(self) -> None:
def executemacro(self, repo:AddonManagerRepo) -> None:
"""executes a selected macro"""
macro = self.selected_repo.macro
macro = repo.macro
if not macro or not macro.code:
return
@@ -713,7 +738,7 @@ class CommandAddonManager:
temp_install_succeeded = macro.install(dir)
if not temp_install_succeeded:
message = translate("AddonsInstaller", "Execution of macro failed. See console for failure details.")
self.on_installation_failed (self.selected_repo, message)
self.on_installation_failed (repo, message)
return
else:
macro_path = os.path.join(dir,macro.filename)
@@ -727,59 +752,28 @@ class CommandAddonManager:
os.chmod(path, stat.S_IWRITE)
func(path)
def remove(self) -> None:
def remove(self, repo:AddonManagerRepo) -> None:
"""uninstalls a macro or workbench"""
if self.selected_repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH or \
self.selected_repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH or \
repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
basedir = FreeCAD.getUserAppDataDir()
moddir = basedir + os.sep + "Mod"
clonedir = moddir + os.sep + self.selected_repo.name
clonedir = moddir + os.sep + repo.name
if os.path.exists(clonedir):
shutil.rmtree(clonedir, onerror=self.remove_readonly)
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.item_model.update_item_status(repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED)
self.addon_removed = True # A value to trigger the restart message on dialog close
self.packageDetails.show_repo(repo, reload=True)
else:
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
elif repo.repo_type == AddonManagerRepo.RepoType.MACRO:
macro = repo.macro
if macro.remove():
self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller", "Macro successfully removed."))
self.item_model.update_item_status(self.selected_repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED)
self.item_model.update_item_status(repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED)
self.packageDetails.show_repo(repo, reload=True)
else:
self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller", "Macro could not be removed."))
def show_config(self) -> None:
"""shows the configuration dialog"""
self.config = FreeCADGui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__), "AddonManagerOptions.ui"))
# restore stored values
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
self.config.checkUpdates.setChecked(pref.GetBool("AutoCheck", False))
self.config.customRepositories.setPlainText(pref.GetString("CustomRepositories", ""))
self.config.radioButtonNoProxy.setChecked(pref.GetBool("NoProxyCheck", True))
self.config.radioButtonSystemProxy.setChecked(pref.GetBool("SystemProxyCheck", False))
self.config.radioButtonUserProxy.setChecked(pref.GetBool("UserProxyCheck", False))
self.config.userProxy.setPlainText(pref.GetString("ProxyUrl", ""))
# center the dialog over the Addon Manager
self.config.move(self.dialog.frameGeometry().topLeft() +
self.dialog.rect().center() -
self.config.rect().center())
ret = self.config.exec_()
if ret:
# OK button has been pressed
pref.SetBool("AutoCheck", self.config.checkUpdates.isChecked())
pref.SetString("CustomRepositories", self.config.customRepositories.toPlainText())
pref.SetBool("NoProxyCheck", self.config.radioButtonNoProxy.isChecked())
pref.SetBool("SystemProxyCheck", self.config.radioButtonSystemProxy.isChecked())
pref.SetBool("UserProxyCheck", self.config.radioButtonUserProxy.isChecked())
pref.SetString("ProxyUrl", self.config.userProxy.toPlainText())
# @}

View File

@@ -127,7 +127,7 @@ class AddonManagerRepo:
"branch":self.branch,
"repo_type":int(self.repo_type),
"description":self.description,
"cached_icon_filename":self.cached_icon_filename}
"cached_icon_filename":self.get_cached_icon_filename()}
def contains_workbench(self) -> bool:
""" Determine if this package contains (or is) a workbench """
@@ -166,6 +166,9 @@ class AddonManagerRepo:
if self.cached_icon_filename:
return self.cached_icon_filename
if not self.metadata:
return ""
real_icon = self.metadata.Icon
if not real_icon:
# If there is no icon set for the entire package, see if there are any workbenches, which

View File

@@ -26,7 +26,7 @@ import re
import sys
import codecs
import shutil
from typing import Dict, Union
from typing import Dict, Union, List
import FreeCAD
@@ -184,7 +184,7 @@ class Macro(object):
self.code = code
self.parsed = True
def install(self, macro_dir:str) -> bool:
def install(self, macro_dir:str) -> (bool, List[str]):
"""Install a macro and all its related files
Returns True if the macro was installed correctly.
@@ -195,41 +195,40 @@ class Macro(object):
"""
if not self.code:
return False
return False,["No code"]
if not os.path.isdir(macro_dir):
try:
os.makedirs(macro_dir)
except OSError:
FreeCAD.Console.PrintError(f"Failed to create {macro_dir}\n")
return False
return False, [f"Failed to create {macro_dir}"]
macro_path = os.path.join(macro_dir, self.filename)
try:
with codecs.open(macro_path, 'w', 'utf-8') as macrofile:
macrofile.write(self.code)
except IOError:
FreeCAD.Console.PrintError(f"Failed to write {macro_path}\n")
return False
return False, [f"Failed to write {macro_path}"]
# Copy related files, which are supposed to be given relative to
# self.src_filename.
base_dir = os.path.dirname(self.src_filename)
warnings = []
for other_file in self.other_files:
dst_dir = os.path.join(macro_dir, os.path.dirname(other_file))
if not os.path.isdir(dst_dir):
try:
os.makedirs(dst_dir)
except OSError:
FreeCAD.Console.PrintError(f"Failed to create {dst_dir}\n")
return False
return False, [f"Failed to create {dst_dir}"]
src_file = os.path.join(base_dir, other_file)
dst_file = os.path.join(macro_dir, other_file)
try:
shutil.copy(src_file, dst_file)
except IOError:
FreeCAD.Console.PrintError(f"Failed to copy {src_file} to {dst_file}\n")
return False
warnings.append(f"Failed to copy {src_file} to {dst_file}")
if len(warnings) > 0:
return False, warnings
FreeCAD.Console.PrintMessage(f"Macro {self.name} was installed successfully.\n")
return True
return True, []
def remove(self) -> bool:

View File

@@ -68,7 +68,8 @@ class MetadataDownloadWorker(QObject):
self.fetch_task.sslErrors.connect(self.on_ssl_error)
def abort(self):
self.fetch_task.abort()
if not self.fetch_task.isFinished():
self.fetch_task.abort()
def on_redirect(self, url):
# For now just blindly follow all redirects
@@ -111,6 +112,8 @@ class MetadataDownloadWorker(QObject):
self.update_local_copy(new_xml)
elif self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.ContentNotFoundError:
pass
elif self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.OperationCanceledError:
pass
else:
FreeCAD.Console.PrintWarning(f"Failed to connect to {self.url}:\n {self.fetch_task.error()}\n")

View File

@@ -25,15 +25,14 @@
import os
import re
import shutil
import sys
import json
import tempfile
import hashlib
import threading
import queue
from typing import Union
from typing import Union, List
from PySide2 import QtCore, QtGui, QtWidgets, QtNetwork
from PySide2 import QtCore, QtGui, QtNetwork
import FreeCAD
if FreeCAD.GuiUp:
@@ -69,8 +68,6 @@ try:
except ImportError:
pass
from io import BytesIO
# @package AddonManager_workers
# \ingroup ADDONMANAGER
# \brief Multithread workers for the addon manager
@@ -203,13 +200,22 @@ class LoadPackagesFromCacheWorker(QtCore.QThread):
self.cache_file = cache_file
def run(self):
metadata_cache_path = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
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))
if data:
dict_data = json.loads(data)
for item in dict_data:
if QtCore.QThread.currentThread().isInterruptionRequested():
return
repo = AddonManagerRepo.from_cache(item)
repo_metadata_cache_path = os.path.join(metadata_cache_path, repo.name, "package.xml")
if os.path.isfile(repo_metadata_cache_path):
try:
repo.metadata = FreeCAD.Metadata(repo_metadata_cache_path)
except Exception:
pass
self.addon_repo.emit(repo)
self.done.emit()
class LoadMacrosFromCacheWorker(QtCore.QThread):
@@ -231,50 +237,6 @@ class LoadMacrosFromCacheWorker(QtCore.QThread):
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"""
@@ -284,7 +246,7 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
progress_made = QtCore.Signal(int, int)
done = QtCore.Signal()
def __init__(self, repos:[AddonManagerRepo]):
def __init__(self, repos:List[AddonManagerRepo]):
QtCore.QThread.__init__(self)
self.repos = repos
@@ -986,7 +948,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
answer += ":\n<b>" + f + "</b>"
self.success.emit(self.repo, answer)
def check_python_dependencies(self, baseurl:str) -> [bool,str]:
def check_python_dependencies(self, baseurl:str) -> Union[bool,str]:
"""checks if the repo contains a metadata.txt and check its contents"""
ok = True
@@ -995,7 +957,10 @@ class InstallWorkbenchWorker(QtCore.QThread):
if not depsurl.endswith("/"):
depsurl += "/"
depsurl += "master/metadata.txt"
mu = utils.urlopen(depsurl)
try:
mu = utils.urlopen(depsurl)
except Exception:
return True,"No metadata.txt found"
if mu:
# metadata.txt found
depsfile = mu.read()
@@ -1189,22 +1154,24 @@ class UpdateMetadataCacheWorker(QtCore.QThread):
self.downloaders.append(downloader)
# Run a local event loop until we've processed all of the downloads:
# this is local
# to this thread, and does not affect the main event loop
# this is local to this thread, and does not affect the main event loop
ui_updater = QtCore.QTimer()
ui_updater.timeout.connect(self.send_ui_update)
ui_updater.start(100) # Send an update back to the main thread every 100ms
self.num_downloads_required = len(self.downloaders)
self.num_downloads_completed = UpdateMetadataCacheWorker.AtomicCounter()
aborted = False
while True:
if current_thread.isInterruptionRequested():
if current_thread.isInterruptionRequested() and not aborted:
for downloader in self.downloaders:
downloader.abort()
QtCore.QCoreApplication.processEvents()
if self.num_downloads_completed.get() == self.num_downloads_required:
aborted = True
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
if self.num_downloads_completed.get() >= self.num_downloads_required:
break
if current_thread.isInterruptionRequested():
if aborted:
FreeCAD.Console.PrintMessage("Metadata update cancelled\n")
return
# Update and serialize the updated index, overwriting whatever was
@@ -1224,6 +1191,7 @@ class UpdateMetadataCacheWorker(QtCore.QThread):
# Called by the QNetworkAccessManager's sub-threads when a fetch
# process completed (in any state)
self.num_downloads_completed.increment()
reply.deleteLater()
def on_updated(self, repo):
# Called if this repo got new metadata and/or a new icon

View File

@@ -18,7 +18,7 @@ class Ui_ExpandedView(object):
if not ExpandedView.objectName():
ExpandedView.setObjectName(u"ExpandedView")
ExpandedView.resize(657, 64)
sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(ExpandedView.sizePolicy().hasHeightForWidth())
@@ -36,8 +36,6 @@ class Ui_ExpandedView(object):
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)
@@ -79,7 +77,7 @@ class Ui_ExpandedView(object):
self.labelDescription.setSizePolicy(sizePolicy)
self.labelDescription.setTextFormat(Qt.PlainText)
self.labelDescription.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop)
self.labelDescription.setWordWrap(False)
self.labelDescription.setWordWrap(True)
self.verticalLayout.addWidget(self.labelDescription)
@@ -95,9 +93,6 @@ class Ui_ExpandedView(object):
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")

View File

@@ -148,7 +148,7 @@
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>false</bool>
<bool>true</bool>
</property>
</widget>
</item>
@@ -173,19 +173,6 @@
</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">

View File

@@ -35,13 +35,15 @@ from addonmanager_utilities import translate # this needs to be as is for pylup
from addonmanager_workers import ShowWorker, GetMacroDetailsWorker
from AddonManagerRepo import AddonManagerRepo
import inspect
class PackageDetails(QWidget):
backClicked = Signal()
installClicked = Signal()
uninstallClicked = Signal()
updateClicked = Signal()
executeClicked = Signal()
back = Signal()
install = Signal(AddonManagerRepo)
uninstall = Signal(AddonManagerRepo)
update = Signal(AddonManagerRepo)
execute = Signal(AddonManagerRepo)
def __init__(self, parent=None):
super().__init__(parent)
@@ -51,14 +53,14 @@ class PackageDetails(QWidget):
self.worker = None
self.repo = None
self.ui.buttonBack.clicked.connect(self.backClicked.emit)
self.ui.buttonBack.clicked.connect(self.back.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)
self.ui.buttonExecute.clicked.connect(lambda: self.execute.emit(self.repo))
self.ui.buttonInstall.clicked.connect(lambda: self.install.emit(self.repo))
self.ui.buttonUninstall.clicked.connect(lambda: self.uninstall.emit(self.repo))
self.ui.buttonUpdate.clicked.connect(lambda: self.update.emit(self.repo))
def show_repo(self, repo:AddonManagerRepo) -> None:
def show_repo(self, repo:AddonManagerRepo, reload:bool = False) -> None:
self.repo = repo
@@ -67,7 +69,12 @@ class PackageDetails(QWidget):
self.worker.requestInterruption()
self.worker.wait()
self.check_and_clean_cache(repo)
# Always load bare macros from scratch, we need to grab their code, which isn't cached
force_reload = reload
if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
force_reload = True
self.check_and_clean_cache(force_reload)
if repo.repo_type == AddonManagerRepo.RepoType.MACRO:
self.show_macro(repo)
@@ -95,6 +102,10 @@ class PackageDetails(QWidget):
self.ui.buttonInstall.hide()
self.ui.buttonUninstall.show()
self.ui.buttonUpdate.hide()
elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
self.ui.buttonInstall.hide()
self.ui.buttonUninstall.show()
self.ui.buttonUpdate.hide()
@classmethod
def cache_path(self, repo:AddonManagerRepo) -> str:
@@ -115,6 +126,12 @@ class PackageDetails(QWidget):
last_cache_update = date.fromtimestamp(timestamp)
delta_update = timedelta(days=days_between_updates)
if date.today() >= last_cache_update + delta_update or download_interrupted or force:
if force:
FreeCAD.Console.PrintMessage(f"Forced README cache update for {self.repo.name}\n")
elif download_interrupted:
FreeCAD.Console.PrintMessage(f"Restarting interrupted README download for {self.repo.name}\n")
else:
FreeCAD.Console.PrintMessage(f"Cache expired, downloading README for {self.repo.name} again\n")
os.remove(readme_cache_file)
if os.path.isdir(readme_images_path):
shutil.rmtree(readme_images_path)
@@ -195,35 +212,33 @@ class Ui_PackageDetails(object):
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.layoutDetailsBackButton.addWidget(self.buttonInstall)
self.buttonUninstall = QPushButton(PackageDetails)
self.buttonUninstall.setObjectName(u"buttonUninstall")
self.layoutDetailsInstallButtons.addWidget(self.buttonUninstall)
self.layoutDetailsBackButton.addWidget(self.buttonUninstall)
self.buttonUpdate = QPushButton(PackageDetails)
self.buttonUpdate.setObjectName(u"buttonUpdate")
self.layoutDetailsInstallButtons.addWidget(self.buttonUpdate)
self.layoutDetailsBackButton.addWidget(self.buttonUpdate)
self.buttonExecute = QPushButton(PackageDetails)
self.buttonExecute.setObjectName(u"buttonExecute")
self.layoutDetailsInstallButtons.addWidget(self.buttonExecute)
self.layoutDetailsBackButton.addWidget(self.buttonExecute)
self.verticalLayout_2.addLayout(self.layoutDetailsInstallButtons)
self.verticalLayout_2.addLayout(self.layoutDetailsBackButton)
self.textBrowserReadMe = QTextBrowser(PackageDetails)
self.textBrowserReadMe.setObjectName(u"textBrowserReadMe")
self.textBrowserReadMe.setOpenExternalLinks(True)
self.textBrowserReadMe.setOpenLinks(True)
self.verticalLayout_2.addWidget(self.textBrowserReadMe)

View File

@@ -43,10 +43,6 @@
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="layoutDetailsInstallButtons">
<item>
<widget class="QPushButton" name="buttonInstall">
<property name="toolTip">

View File

@@ -54,7 +54,7 @@ class PackageList(QWidget):
self.item_filter = PackageListFilter()
self.ui.listPackages.setModel (self.item_filter)
self.item_delegate = PackageListItemDelegate()
self.item_delegate = PackageListItemDelegate(self.ui.listPackages)
self.ui.listPackages.setItemDelegate(self.item_delegate)
self.ui.listPackages.clicked.connect(self.on_listPackages_clicked)
@@ -420,6 +420,7 @@ class Ui_PackageList(object):
self.listPackages.setResizeMode(QListView.Adjust)
self.listPackages.setUniformItemSizes(False)
self.listPackages.setAlternatingRowColors(True)
self.listPackages.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.verticalLayout.addWidget(self.listPackages)