Addon Manager: Improve cache behavior
Also includes a number of bug fixes.
This commit is contained in:
@@ -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())
|
||||
|
||||
# @}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -43,10 +43,6 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="layoutDetailsInstallButtons">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonInstall">
|
||||
<property name="toolTip">
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user