diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 416569c1c1..78a564991f 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -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()) - # @} diff --git a/src/Mod/AddonManager/AddonManagerRepo.py b/src/Mod/AddonManager/AddonManagerRepo.py index 399cb09093..14bf76c1a9 100644 --- a/src/Mod/AddonManager/AddonManagerRepo.py +++ b/src/Mod/AddonManager/AddonManagerRepo.py @@ -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 diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index e2e46da249..6743a8d9d8 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -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: diff --git a/src/Mod/AddonManager/addonmanager_metadata.py b/src/Mod/AddonManager/addonmanager_metadata.py index 2084fe5166..c6f40a8194 100644 --- a/src/Mod/AddonManager/addonmanager_metadata.py +++ b/src/Mod/AddonManager/addonmanager_metadata.py @@ -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") diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 91661ff736..ef983cbbf6 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -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" + f + "" 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 diff --git a/src/Mod/AddonManager/expanded_view.py b/src/Mod/AddonManager/expanded_view.py index 12e4a56548..1dfd7767b8 100644 --- a/src/Mod/AddonManager/expanded_view.py +++ b/src/Mod/AddonManager/expanded_view.py @@ -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") diff --git a/src/Mod/AddonManager/expanded_view.ui b/src/Mod/AddonManager/expanded_view.ui index 78934b525d..0ba3e6dcb9 100644 --- a/src/Mod/AddonManager/expanded_view.ui +++ b/src/Mod/AddonManager/expanded_view.ui @@ -148,7 +148,7 @@ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - false + true @@ -173,19 +173,6 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 6828dad284..5ec2afb3c7 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -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) diff --git a/src/Mod/AddonManager/package_details.ui b/src/Mod/AddonManager/package_details.ui index a63345505c..d8e49c6cfb 100644 --- a/src/Mod/AddonManager/package_details.ui +++ b/src/Mod/AddonManager/package_details.ui @@ -43,10 +43,6 @@ - - - - diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index ce116be7cb..1b75d6fb7d 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -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)