Addon Manager: Black reformatting

This commit is contained in:
Chris Hennes
2021-12-17 13:48:50 -06:00
parent 14bcba5c55
commit 3cc2d402cf
11 changed files with 1513 additions and 804 deletions

View File

@@ -1,28 +1,28 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#***************************************************************************
#* *
#* Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
#* 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 *
#* *
#***************************************************************************
# ***************************************************************************
# * *
# * Copyright (c) 2015 Yorik van Havre <yorik@uncreated.net> *
# * 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 os
import shutil
@@ -70,44 +70,68 @@ installed.
def QT_TRANSLATE_NOOP(ctx, txt):
return txt
class CommandAddonManager:
"""The main Addon Manager class and FreeCAD command"""
workers = ["update_worker", "check_worker", "show_worker",
"showmacro_worker", "macro_worker", "install_worker",
"update_metadata_cache_worker", "update_all_worker"]
workers = [
"update_worker",
"check_worker",
"show_worker",
"showmacro_worker",
"macro_worker",
"install_worker",
"update_metadata_cache_worker",
"update_all_worker",
]
lock = threading.Lock()
restart_required = False
def __init__(self):
FreeCADGui.addPreferencePage(os.path.join(os.path.dirname(__file__),
"AddonManagerOptions.ui"),"Addon Manager")
FreeCADGui.addPreferencePage(
os.path.join(os.path.dirname(__file__), "AddonManagerOptions.ui"),
"Addon Manager",
)
def GetResources(self) -> Dict[str,str]:
return {"Pixmap": "AddonManager",
"MenuText": QT_TRANSLATE_NOOP("Std_AddonMgr", "&Addon manager"),
"ToolTip": QT_TRANSLATE_NOOP("Std_AddonMgr", "Manage external workbenches, macros, and preference packs"),
"Group": "Tools"}
def GetResources(self) -> Dict[str, str]:
return {
"Pixmap": "AddonManager",
"MenuText": QT_TRANSLATE_NOOP("Std_AddonMgr", "&Addon manager"),
"ToolTip": QT_TRANSLATE_NOOP(
"Std_AddonMgr",
"Manage external workbenches, macros, and preference packs",
),
"Group": "Tools",
}
def Activated(self) -> None:
# display first use dialog if needed
readWarningParameter = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
readWarningParameter = FreeCAD.ParamGet(
"User parameter:BaseApp/Preferences/Addons"
)
readWarning = readWarningParameter.GetBool("readWarning", False)
newReadWarningParameter = FreeCAD.ParamGet("User parameter:Plugins/addonsRepository")
newReadWarningParameter = FreeCAD.ParamGet(
"User parameter:Plugins/addonsRepository"
)
readWarning |= newReadWarningParameter.GetBool("readWarning", False)
if not readWarning:
if (QtWidgets.QMessageBox.warning(None,
"FreeCAD",
translate("AddonsInstaller",
"The addons that can be installed here are not "
"officially part of FreeCAD, and are not reviewed "
"by the FreeCAD team. Make sure you know what you "
"are installing!"),
QtWidgets.QMessageBox.Cancel |
QtWidgets.QMessageBox.Ok) !=
QtWidgets.QMessageBox.StandardButton.Cancel):
if (
QtWidgets.QMessageBox.warning(
None,
"FreeCAD",
translate(
"AddonsInstaller",
"The addons that can be installed here are not "
"officially part of FreeCAD, and are not reviewed "
"by the FreeCAD team. Make sure you know what you "
"are installing!",
),
QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Ok,
)
!= QtWidgets.QMessageBox.StandardButton.Cancel
):
readWarningParameter.SetBool("readWarning", True)
readWarning = True
@@ -118,8 +142,9 @@ class CommandAddonManager:
"""Shows the Addon Manager UI"""
# create the dialog
self.dialog = FreeCADGui.PySideUic.loadUi(os.path.join(os.path.dirname(__file__),
"AddonManager.ui"))
self.dialog = FreeCADGui.PySideUic.loadUi(
os.path.join(os.path.dirname(__file__), "AddonManager.ui")
)
# cleanup the leftovers from previous runs
self.macro_repo_dir = FreeCAD.getUserMacroDir()
@@ -151,7 +176,7 @@ class CommandAddonManager:
days_between_updates = pref.GetInt("DaysBetweenUpdates", days_between_updates)
last_cache_update_string = pref.GetString("LastCacheUpdate", "never")
cache_path = FreeCAD.getUserCachePath()
am_path = os.path.join(cache_path,"AddonManager")
am_path = os.path.join(cache_path, "AddonManager")
if last_cache_update_string == "never":
self.update_cache = True
elif days_between_updates > 0:
@@ -169,7 +194,9 @@ class CommandAddonManager:
self.item_model = PackageListItemModel()
self.packageList.setModel(self.item_model)
self.dialog.contentPlaceholder.hide()
self.dialog.layout().replaceWidget(self.dialog.contentPlaceholder, self.packageList)
self.dialog.layout().replaceWidget(
self.dialog.contentPlaceholder, self.packageList
)
self.packageList.show()
# Package details start out hidden
@@ -181,8 +208,14 @@ class CommandAddonManager:
# set nice icons to everything, by theme with fallback to FreeCAD icons
self.dialog.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
self.dialog.buttonUpdateAll.setIcon(QtGui.QIcon(":/icons/button_valid.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")))
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.buttonUpdateAll.setEnabled(False)
@@ -206,10 +239,14 @@ class CommandAddonManager:
# center the dialog over the FreeCAD window
mw = FreeCADGui.getMainWindow()
self.dialog.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.dialog.rect().center())
self.dialog.move(
mw.frameGeometry().topLeft()
+ mw.rect().center()
- self.dialog.rect().center()
)
# set info for the progress bar:
self.dialog.progressBar.setMaximum (100)
self.dialog.progressBar.setMaximum(100)
# begin populating the table in a set of sub-threads
self.startup()
@@ -221,7 +258,7 @@ class CommandAddonManager:
self.dialog.exec_()
def cleanup_workers(self, wait=False) -> None:
""" Ensure that no workers are running by explicitly asking them to stop and waiting for them until they do """
"""Ensure that no workers are running by explicitly asking them to stop and waiting for them until they do"""
for worker in self.workers:
if hasattr(self, worker):
thread = getattr(self, worker)
@@ -283,26 +320,31 @@ class CommandAddonManager:
m = QtWidgets.QMessageBox()
m.setWindowTitle(translate("AddonsInstaller", "Addon manager"))
m.setWindowIcon(QtGui.QIcon(":/icons/AddonManager.svg"))
m.setText(translate("AddonsInstaller",
"You must restart FreeCAD for changes to take "
"effect."))
m.setText(
translate(
"AddonsInstaller",
"You must restart FreeCAD for changes to take " "effect.",
)
)
m.setIcon(m.Warning)
m.setStandardButtons(m.Ok | m.Cancel)
m.setDefaultButton(m.Cancel)
okBtn = m.button(QtWidgets.QMessageBox.StandardButton.Ok)
cancelBtn = m.button(QtWidgets.QMessageBox.StandardButton.Cancel)
okBtn.setText(translate("AddonsInstaller","Restart now"))
cancelBtn.setText(translate("AddonsInstaller","Restart later"))
okBtn.setText(translate("AddonsInstaller", "Restart now"))
cancelBtn.setText(translate("AddonsInstaller", "Restart later"))
ret = m.exec_()
if ret == m.Ok:
# restart FreeCAD after a delay to give time to this dialog to close
QtCore.QTimer.singleShot(1000, utils.restart_freecad)
else:
FreeCAD.Console.PrintWarning("Could not terminate sub-threads in Addon Manager.\n")
FreeCAD.Console.PrintWarning(
"Could not terminate sub-threads in Addon Manager.\n"
)
self.cleanup_workers()
def startup(self) -> None:
""" Downloads the available packages listings and populates the table
"""Downloads the available packages listings and populates the table
This proceeds in four stages: first, the main GitHub repository is queried for a list of possible
addons. Each addon is specified as a git submodule with name and branch information. The actual specific
@@ -324,22 +366,24 @@ class CommandAddonManager:
"""
# Each function in this list is expected to launch a thread and connect its completion signal
# Each function in this list is expected to launch a thread and connect its completion signal
# 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]
self.startup_sequence = [
self.populate_packages_table,
self.activate_table_widgets,
self.populate_macros,
self.update_metadata_cache,
self.check_updates,
]
self.current_progress_region = 0
self.number_of_progress_regions = len(self.startup_sequence)
self.do_next_startup_phase()
def do_next_startup_phase(self) -> None:
""" Pop the top item in self.startup_sequence off the list and run it """
"""Pop the top item in self.startup_sequence off the list and run it"""
if (len(self.startup_sequence) > 0):
if len(self.startup_sequence) > 0:
phase_runner = self.startup_sequence.pop(0)
self.current_progress_region += 1
phase_runner()
@@ -349,11 +393,11 @@ class CommandAddonManager:
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetString("LastCacheUpdate", date.today().isoformat())
def get_cache_file_name(self, file:str) -> str:
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)
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()
@@ -362,7 +406,7 @@ class CommandAddonManager:
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:
with open(self.get_cache_file_name("package_cache.json"), "r") as f:
data = f.read()
try:
from_json = json.loads(data)
@@ -374,28 +418,34 @@ class CommandAddonManager:
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_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_progress_bar(10,100)
self.update_worker.done.connect(self.do_next_startup_phase) # Link to step 2
self.update_progress_bar(10, 100)
self.update_worker.done.connect(
self.do_next_startup_phase
) # Link to step 2
self.update_worker.start()
else:
self.update_worker = LoadPackagesFromCacheWorker(self.get_cache_file_name("package_cache.json"))
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_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):
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:
with open(package_cache_path, "w") as f:
f.write(json.dumps(self.package_cache))
self.package_cache = []
@@ -406,38 +456,52 @@ class CommandAddonManager:
def populate_macros(self) -> None:
self.current_progress_region += 1
if self.update_cache or not os.path.isfile(self.get_cache_file_name("macro_cache.json")):
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.done.connect(self.do_next_startup_phase) # Link to step 3
self.macro_worker.done.connect(self.do_next_startup_phase) # Link to step 3
self.macro_worker.start()
else:
self.macro_worker = LoadMacrosFromCacheWorker(self.get_cache_file_name("macro_cache.json"))
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.done.connect(self.do_next_startup_phase) # Link to step 3
self.macro_worker.start()
def cache_macro(self, macro:AddonManagerRepo):
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:
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
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
self.update_metadata_cache_worker.progress_made.connect(self.update_progress_bar)
self.update_metadata_cache_worker.package_updated.connect(self.on_package_updated)
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
self.update_metadata_cache_worker.progress_made.connect(
self.update_progress_bar
)
self.update_metadata_cache_worker.package_updated.connect(
self.on_package_updated
)
self.update_metadata_cache_worker.start()
else:
self.do_next_startup_phase()
@@ -446,18 +510,19 @@ class CommandAddonManager:
self.update_cache = True
self.startup()
def on_package_updated(self, repo:AddonManagerRepo) -> None:
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.getUserCachePath(), "AddonManager", "PackageMetadata", repo.name)
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)
def check_updates(self) -> None:
"checks every installed addon for available updates"
self.current_progress_region += 1
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
autocheck = pref.GetBool("AutoCheck", False)
@@ -471,7 +536,9 @@ class CommandAddonManager:
if not thread.isFinished():
self.do_next_startup_phase()
return
self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller", "Checking for updates..."))
self.dialog.buttonUpdateAll.setText(
translate("AddonsInstaller", "Checking for updates...")
)
self.check_worker = CheckWorkbenchesForUpdatesWorker(self.item_model.repos)
self.check_worker.done.connect(self.do_next_startup_phase)
self.check_worker.progress_made.connect(self.update_progress_bar)
@@ -479,36 +546,44 @@ class CommandAddonManager:
self.check_worker.start()
self.enable_updates(len(self.packages_with_updates))
def status_updated(self, repo:AddonManagerRepo) -> None:
def status_updated(self, repo: AddonManagerRepo) -> None:
self.item_model.reload_item(repo)
if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
self.packages_with_updates.append(repo)
self.enable_updates(len(self.packages_with_updates))
def enable_updates(self, number_of_updates:int) -> None:
def enable_updates(self, number_of_updates: int) -> None:
"""enables the update button"""
if number_of_updates:
self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller", "Apply") +
" " + str(number_of_updates) + " " +
translate("AddonsInstaller", "update(s)"))
self.dialog.buttonUpdateAll.setText(
translate("AddonsInstaller", "Apply")
+ " "
+ str(number_of_updates)
+ " "
+ translate("AddonsInstaller", "update(s)")
)
self.dialog.buttonUpdateAll.setEnabled(True)
else:
self.dialog.buttonUpdateAll.setText(translate("AddonsInstaller", "No updates available"))
self.dialog.buttonUpdateAll.setText(
translate("AddonsInstaller", "No updates available")
)
self.dialog.buttonUpdateAll.setEnabled(False)
def add_addon_repo(self, addon_repo:AddonManagerRepo) -> None:
def add_addon_repo(self, addon_repo: AddonManagerRepo) -> None:
"""adds a workbench to the list"""
if addon_repo.icon is None or addon_repo.icon.isNull():
if addon_repo.icon is None or addon_repo.icon.isNull():
addon_repo.icon = self.get_icon(addon_repo)
for repo in self.item_model.repos:
if repo.name == addon_repo.name:
FreeCAD.Console.PrintLog(f"Possible duplicate addon: ignoring second addition of {addon_repo.name}\n")
FreeCAD.Console.PrintLog(
f"Possible duplicate addon: ignoring second addition of {addon_repo.name}\n"
)
return
self.item_model.append_item(addon_repo)
def get_icon(self, repo:AddonManagerRepo, update:bool=False) -> QtGui.QIcon:
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() and repo.icon.isValid():
@@ -516,8 +591,8 @@ class CommandAddonManager:
path = ":/icons/" + repo.name.replace(" ", "_")
if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
path += "_workbench_icon.svg"
default_icon = QtGui.QIcon(":/icons/document-package.svg")
path += "_workbench_icon.svg"
default_icon = QtGui.QIcon(":/icons/document-package.svg")
elif repo.repo_type == AddonManagerRepo.RepoType.MACRO:
path += "_macro_icon.svg"
default_icon = QtGui.QIcon(":/icons/document-python.svg")
@@ -542,19 +617,19 @@ class CommandAddonManager:
return addonicon
def table_row_activated(self, selected_repo:AddonManagerRepo) -> None:
def table_row_activated(self, selected_repo: AddonManagerRepo) -> None:
"""a row was activated, show the relevant data"""
self.packageList.hide()
self.packageDetails.show()
self.packageDetails.show_repo(selected_repo)
def show_information(self, message:str) -> None:
def show_information(self, message: str) -> None:
"""shows generic text in the information pane (which might be collapsed)"""
self.dialog.labelStatusInfo.setText(message)
def show_workbench(self, repo:AddonManagerRepo) -> None:
def show_workbench(self, repo: AddonManagerRepo) -> None:
self.packageList.hide()
self.packageDetails.show()
self.packageDetails.show_repo(repo)
@@ -563,12 +638,12 @@ class CommandAddonManager:
self.packageDetails.hide()
self.packageList.show()
def append_to_repos_list(self, repo:AddonManagerRepo) -> None:
def append_to_repos_list(self, repo: AddonManagerRepo) -> None:
"""this function allows threads to update the main list of workbenches"""
self.item_model.append_item(repo)
def install(self, repo:AddonManagerRepo) -> None:
def install(self, repo: AddonManagerRepo) -> None:
"""installs or updates a workbench, macro, or package"""
if hasattr(self, "install_worker") and self.install_worker:
@@ -578,7 +653,10 @@ class CommandAddonManager:
if not repo:
return
if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH or repo.repo_type == AddonManagerRepo.RepoType.PACKAGE:
if (
repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH
or repo.repo_type == AddonManagerRepo.RepoType.PACKAGE
):
self.show_progress_widgets()
self.install_worker = InstallWorkbenchWorker(repo)
self.install_worker.status_message.connect(self.show_information)
@@ -607,22 +685,26 @@ class CommandAddonManager:
failed = True
if not failed:
message = translate("AddonsInstaller",
"Macro successfully installed. The macro is "
"now available from the Macros dialog.")
self.on_package_installed (repo, message)
message = translate(
"AddonsInstaller",
"Macro successfully installed. The macro is "
"now available from the Macros dialog.",
)
self.on_package_installed(repo, message)
else:
message = translate("AddonsInstaller", "Installation of macro failed") + ":"
message = (
translate("AddonsInstaller", "Installation of macro failed") + ":"
)
for error in errors:
message += "\n * "
message += error
self.on_installation_failed (repo, message)
self.on_installation_failed(repo, message)
def update(self, repo:AddonManagerRepo) -> None:
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 """
"""Asynchronously apply all available updates: individual failures are noted, but do not stop other updates"""
if hasattr(self, "update_all_worker") and self.update_all_worker:
if self.update_all_worker.isRunning():
@@ -630,31 +712,50 @@ class CommandAddonManager:
self.subupdates_succeeded = []
self.subupdates_failed = []
self.show_progress_widgets()
self.current_progress_region = 1
self.number_of_progress_regions = 1
self.update_all_worker = UpdateAllWorker(self.packages_with_updates)
self.update_all_worker.progress_made.connect(self.update_progress_bar)
self.update_all_worker.status_message.connect(self.show_information)
self.update_all_worker.success.connect(lambda repo : self.subupdates_succeeded.append(repo))
self.update_all_worker.failure.connect(lambda repo : self.subupdates_failed.append(repo))
self.update_all_worker.success.connect(
lambda repo: self.subupdates_succeeded.append(repo)
)
self.update_all_worker.failure.connect(
lambda repo: self.subupdates_failed.append(repo)
)
self.update_all_worker.done.connect(self.on_update_all_completed)
self.update_all_worker.start()
def on_update_all_completed(self) -> None:
self.hide_progress_widgets()
if not self.subupdates_failed:
message = translate ("AddonsInstaller", "All packages were successfully updated. Packages:") + "\n"
message += ''.join([repo.name + "\n" for repo in self.subupdates_succeeded])
message = (
translate(
"AddonsInstaller",
"All packages were successfully updated. Packages:",
)
+ "\n"
)
message += "".join([repo.name + "\n" for repo in self.subupdates_succeeded])
elif not self.subupdates_succeeded:
message = translate ("AddonsInstaller", "All packages updates failed. Packages:") + "\n"
message += ''.join([repo.name + "\n" for repo in self.subupdates_failed])
message = (
translate("AddonsInstaller", "All packages updates failed. Packages:")
+ "\n"
)
message += "".join([repo.name + "\n" for repo in self.subupdates_failed])
else:
message = translate ("AddonsInstaller", "Some packages updates failed. Successful packages:") + "\n"
message += ''.join([repo.name + "\n" for repo in self.subupdates_succeeded])
message += translate ("AddonsInstaller", "Failed packages:") + "\n"
message += ''.join([repo.name + "\n" for repo in self.subupdates_failed])
message = (
translate(
"AddonsInstaller",
"Some packages updates failed. Successful packages:",
)
+ "\n"
)
message += "".join([repo.name + "\n" for repo in self.subupdates_succeeded])
message += translate("AddonsInstaller", "Failed packages:") + "\n"
message += "".join([repo.name + "\n" for repo in self.subupdates_failed])
for installed_repo in self.subupdates_succeeded:
self.restart_required = True
@@ -665,13 +766,15 @@ class CommandAddonManager:
self.packages_with_updates.remove(installed_repo)
break
self.enable_updates(len(self.packages_with_updates))
QtWidgets.QMessageBox.information(None,
translate("AddonsInstaller", "Update report"),
message,
QtWidgets.QMessageBox.Close)
QtWidgets.QMessageBox.information(
None,
translate("AddonsInstaller", "Update report"),
message,
QtWidgets.QMessageBox.Close,
)
def hide_progress_widgets(self) -> None:
""" hides the progress bar and related widgets"""
"""hides the progress bar and related widgets"""
self.dialog.labelStatusInfo.hide()
self.dialog.progressBar.hide()
@@ -689,12 +792,14 @@ class CommandAddonManager:
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 """
def update_progress_bar(self, current_value: int, max_value: int) -> None:
"""Update the progress bar, showing it if it's hidden"""
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
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:
@@ -705,29 +810,33 @@ class CommandAddonManager:
self.dialog.labelStatusInfo.hide()
self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow)
def stop_update(self)-> None:
def stop_update(self) -> None:
self.cleanup_workers()
self.hide_progress_widgets()
def on_package_installed(self, repo:AddonManagerRepo, message:str) -> None:
def on_package_installed(self, repo: AddonManagerRepo, message: str) -> None:
self.hide_progress_widgets()
QtWidgets.QMessageBox.information(None,
translate("AddonsInstaller", "Installation succeeded"),
message,
QtWidgets.QMessageBox.Close)
QtWidgets.QMessageBox.information(
None,
translate("AddonsInstaller", "Installation succeeded"),
message,
QtWidgets.QMessageBox.Close,
)
repo.update_status = AddonManagerRepo.UpdateStatus.PENDING_RESTART
self.item_model.reload_item(repo)
self.packageDetails.show_repo(repo)
self.restart_required = True
def on_installation_failed(self, _:AddonManagerRepo, message:str) -> None:
def on_installation_failed(self, _: AddonManagerRepo, message: str) -> None:
self.hide_progress_widgets()
QtWidgets.QMessageBox.warning(None,
translate("AddonsInstaller", "Installation failed"),
message,
QtWidgets.QMessageBox.Close)
QtWidgets.QMessageBox.warning(
None,
translate("AddonsInstaller", "Installation failed"),
message,
QtWidgets.QMessageBox.Close,
)
def executemacro(self, repo:AddonManagerRepo) -> None:
def executemacro(self, repo: AddonManagerRepo) -> None:
"""executes a selected macro"""
macro = repo.macro
@@ -735,7 +844,7 @@ class CommandAddonManager:
return
if macro.is_installed():
macro_path = os.path.join(self.macro_repo_dir,macro.filename)
macro_path = os.path.join(self.macro_repo_dir, macro.filename)
FreeCADGui.open(str(macro_path))
self.dialog.hide()
FreeCADGui.SendMsgToActiveView("Run")
@@ -743,14 +852,17 @@ class CommandAddonManager:
with tempfile.TemporaryDirectory() as dir:
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 (repo, message)
message = translate(
"AddonsInstaller",
"Execution of macro failed. See console for failure details.",
)
self.on_installation_failed(repo, message)
return
else:
macro_path = os.path.join(dir,macro.filename)
macro_path = os.path.join(dir, macro.filename)
FreeCADGui.open(str(macro_path))
self.dialog.hide()
FreeCADGui.SendMsgToActiveView("Run")
FreeCADGui.SendMsgToActiveView("Run")
def remove_readonly(self, func, path, _) -> None:
"""Remove a read-only file."""
@@ -758,29 +870,45 @@ class CommandAddonManager:
os.chmod(path, stat.S_IWRITE)
func(path)
def remove(self, repo:AddonManagerRepo) -> None:
def remove(self, repo: AddonManagerRepo) -> None:
"""uninstalls a macro or workbench"""
if repo.repo_type == AddonManagerRepo.RepoType.WORKBENCH or \
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 + repo.name
if os.path.exists(clonedir):
shutil.rmtree(clonedir, onerror=self.remove_readonly)
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.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)
self.restart_required = True
else:
self.dialog.textBrowserReadMe.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 repo.repo_type == AddonManagerRepo.RepoType.MACRO:
macro = repo.macro
if macro.remove():
self.item_model.update_item_status(repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED)
self.item_model.update_item_status(
repo.name, AddonManagerRepo.UpdateStatus.NOT_INSTALLED
)
self.packageDetails.show_repo(repo)
else:
self.dialog.textBrowserReadMe.setText(translate("AddonsInstaller", "Macro could not be removed."))
self.dialog.textBrowserReadMe.setText(
translate("AddonsInstaller", "Macro could not be removed.")
)
# @}