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.")
)
# @}

View File

@@ -1,24 +1,24 @@
#***************************************************************************
#* *
#* 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) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
@@ -27,6 +27,7 @@ from typing import Dict
from addonmanager_macro import Macro
class AddonManagerRepo:
"Encapsulate information about a FreeCAD addon"
@@ -37,7 +38,7 @@ class AddonManagerRepo:
MACRO = 2
PACKAGE = 3
def __str__(self) ->str :
def __str__(self) -> str:
if self.value == 1:
return "Workbench"
elif self.value == 2:
@@ -54,10 +55,10 @@ class AddonManagerRepo:
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return self.value < other.value
return NotImplemented
def __str__(self) ->str :
def __str__(self) -> str:
if self.value == 0:
return "Not installed"
elif self.value == 1:
@@ -69,7 +70,7 @@ class AddonManagerRepo:
elif self.value == 4:
return "Restart required"
def __init__ (self, name:str, url:str, status:UpdateStatus, branch:str):
def __init__(self, name: str, url: str, status: UpdateStatus, branch: str):
self.name = name.strip()
self.url = url.strip()
self.branch = branch.strip()
@@ -77,28 +78,33 @@ class AddonManagerRepo:
self.repo_type = AddonManagerRepo.RepoType.WORKBENCH
self.description = None
from addonmanager_utilities import construct_git_url
self.metadata_url = "" if not self.url else construct_git_url(self, "package.xml")
self.metadata_url = (
"" if not self.url else construct_git_url(self, "package.xml")
)
self.metadata = None
self.icon = None
self.cached_icon_filename = ""
self.macro = None # Bridge to Gaël Écorchard's macro management class
self.macro = None # Bridge to Gaël Écorchard's macro management class
self.updated_timestamp = None
self.installed_version = None
def __str__ (self) -> str:
result = f"FreeCAD {self.repo_type}\n"
def __str__(self) -> str:
result = f"FreeCAD {self.repo_type}\n"
result += f"Name: {self.name}\n"
result += f"URL: {self.url}\n"
result += "Has metadata\n" if self.metadata is not None else "No metadata found\n"
result += (
"Has metadata\n" if self.metadata is not None else "No metadata found\n"
)
if self.macro is not None:
result += "Has linked Macro object\n"
return result
@classmethod
def from_macro (self, macro:Macro):
def from_macro(self, macro: Macro):
if macro.is_installed():
status = AddonManagerRepo.UpdateStatus.UNCHECKED
else:
else:
status = AddonManagerRepo.UpdateStatus.NOT_INSTALLED
instance = AddonManagerRepo(macro.name, macro.url, status, "master")
instance.macro = macro
@@ -107,10 +113,10 @@ class AddonManagerRepo:
return instance
@classmethod
def from_cache (self, data:Dict):
""" Load basic data from cached dict data. Does not include Macro or Metadata information, which must be populated separately. """
def from_cache(self, data: Dict):
"""Load basic data from cached dict data. Does not include Macro or Metadata information, which must be populated separately."""
mod_dir = os.path.join(FreeCAD.getUserAppDataDir(),"Mod",data["name"])
mod_dir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", data["name"])
if os.path.isdir(mod_dir):
status = AddonManagerRepo.UpdateStatus.UNCHECKED
else:
@@ -121,18 +127,20 @@ class AddonManagerRepo:
instance.cached_icon_filename = data["cached_icon_filename"]
return instance
def to_cache (self) -> Dict:
""" Returns a dictionary with cache information that can be used later with from_cache to recreate this object. """
def to_cache(self) -> Dict:
"""Returns a dictionary with cache information that can be used later with from_cache to recreate this object."""
return {
"name": self.name,
"url": self.url,
"branch": self.branch,
"repo_type": int(self.repo_type),
"description": self.description,
"cached_icon_filename": self.get_cached_icon_filename(),
}
return {"name":self.name,
"url":self.url,
"branch":self.branch,
"repo_type":int(self.repo_type),
"description":self.description,
"cached_icon_filename":self.get_cached_icon_filename()}
def contains_workbench(self) -> bool:
""" Determine if this package contains (or is) a workbench """
"""Determine if this package contains (or is) a workbench"""
if self.repo_type == AddonManagerRepo.RepoType.WORKBENCH:
return True
@@ -143,7 +151,7 @@ class AddonManagerRepo:
return False
def contains_macro(self) -> bool:
""" Determine if this package contains (or is) a macro """
"""Determine if this package contains (or is) a macro"""
if self.repo_type == AddonManagerRepo.RepoType.MACRO:
return True
@@ -154,7 +162,7 @@ class AddonManagerRepo:
return False
def contains_preference_pack(self) -> bool:
""" Determine if this package contains a preference pack """
"""Determine if this package contains a preference pack"""
if self.repo_type == AddonManagerRepo.RepoType.PACKAGE:
content = self.metadata.Content
@@ -162,8 +170,8 @@ class AddonManagerRepo:
else:
return False
def get_cached_icon_filename(self) ->str:
""" Get the filename for the locally-cached copy of the icon """
def get_cached_icon_filename(self) -> str:
"""Get the filename for the locally-cached copy of the icon"""
if self.cached_icon_filename:
return self.cached_icon_filename
@@ -185,10 +193,16 @@ class AddonManagerRepo:
subdir = wb.Name
real_icon = subdir + wb.Icon
real_icon = real_icon.replace("/", os.path.sep) # Required path separator in the metadata.xml file to local separator
real_icon = real_icon.replace(
"/", os.path.sep
) # Required path separator in the metadata.xml file to local separator
_, file_extension = os.path.splitext(real_icon)
store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
self.cached_icon_filename = os.path.join(store, self.name, "cached_icon"+file_extension)
store = os.path.join(
FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata"
)
self.cached_icon_filename = os.path.join(
store, self.name, "cached_icon" + file_extension
)
return self.cached_icon_filename

View File

@@ -6,4 +6,4 @@
import AddonManager
FreeCADGui.addLanguagePath(":/translations")
FreeCADGui.addCommand('Std_AddonMgr', AddonManager.CommandAddonManager())
FreeCADGui.addCommand("Std_AddonMgr", AddonManager.CommandAddonManager())

View File

@@ -1,25 +1,25 @@
# -*- coding: utf-8 -*-
#***************************************************************************
#* *
#* Copyright (c) 2018 Gaël Écorchard <galou_breizh@yahoo.fr> *
#* *
#* 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) 2018 Gaël Écorchard <galou_breizh@yahoo.fr> *
# * *
# * 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 re
@@ -36,6 +36,7 @@ from addonmanager_utilities import remove_directory_if_empty
try:
from HTMLParser import HTMLParser
unescape = HTMLParser().unescape
except ImportError:
from html import unescape
@@ -46,6 +47,7 @@ except ImportError:
# different sources
# @{
class Macro(object):
"""This class provides a unified way to handle macros coming from different sources"""
@@ -65,14 +67,14 @@ class Macro(object):
return self.filename == other.filename
@classmethod
def from_cache (self, cache_dict:Dict):
def from_cache(self, cache_dict: Dict):
instance = Macro(cache_dict["name"])
for key,value in cache_dict.items():
for key, value in cache_dict.items():
instance.__dict__[key] = value
return instance
def to_cache (self) -> Dict:
""" For cache purposes this entire class is dumped directly """
def to_cache(self) -> Dict:
"""For cache purposes this entire class is dumped directly"""
return self.__dict__
@@ -85,7 +87,11 @@ class Macro(object):
def is_installed(self):
if self.on_git and not self.src_filename:
return False
return (os.path.exists(os.path.join(FreeCAD.getUserMacroDir(True), self.filename)) or os.path.exists(os.path.join(FreeCAD.getUserMacroDir(True), "Macro_" + self.filename)))
return os.path.exists(
os.path.join(FreeCAD.getUserMacroDir(True), self.filename)
) or os.path.exists(
os.path.join(FreeCAD.getUserMacroDir(True), "Macro_" + self.filename)
)
def fill_details_from_file(self, filename):
with open(filename) as f:
@@ -123,7 +129,11 @@ class Macro(object):
code = ""
u = urlopen(url)
if u is None:
FreeCAD.Console.PrintWarning("AddonManager: Debug: connection is lost (proxy setting changed?)", url, "\n")
FreeCAD.Console.PrintWarning(
"AddonManager: Debug: connection is lost (proxy setting changed?)",
url,
"\n",
)
return
p = u.read()
if isinstance(p, bytes):
@@ -132,12 +142,14 @@ class Macro(object):
# check if the macro page has its code hosted elsewhere, download if
# needed
if "rawcodeurl" in p:
rawcodeurl = re.findall("rawcodeurl.*?href=\"(http.*?)\">", p)
rawcodeurl = re.findall('rawcodeurl.*?href="(http.*?)">', p)
if rawcodeurl:
rawcodeurl = rawcodeurl[0]
u2 = urlopen(rawcodeurl)
if u2 is None:
FreeCAD.Console.PrintWarning("AddonManager: Debug: unable to open URL", rawcodeurl, "\n")
FreeCAD.Console.PrintWarning(
"AddonManager: Debug: unable to open URL", rawcodeurl, "\n"
)
return
# code = u2.read()
# github is slow to respond... We need to use this trick below
@@ -165,14 +177,27 @@ class Macro(object):
code = unescape(code)
code = code.replace(b"\xc2\xa0".decode("utf-8"), " ")
else:
FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to fetch the code of this macro.") + "\n")
FreeCAD.Console.PrintWarning(
translate(
"AddonsInstaller", "Unable to fetch the code of this macro."
)
+ "\n"
)
desc = re.findall(r"<td class=\"ctEven left macro-description\">(.*?)</td>", p.replace("\n", " "))
desc = re.findall(
r"<td class=\"ctEven left macro-description\">(.*?)</td>",
p.replace("\n", " "),
)
if desc:
desc = desc[0]
else:
FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
"Unable to retrieve a description for this macro.") + "\n")
FreeCAD.Console.PrintWarning(
translate(
"AddonsInstaller",
"Unable to retrieve a description for this macro.",
)
+ "\n"
)
desc = "No description available"
self.desc = desc
self.url = url
@@ -184,7 +209,7 @@ class Macro(object):
self.code = code
self.parsed = True
def install(self, macro_dir:str) -> (bool, List[str]):
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,7 +220,7 @@ class Macro(object):
"""
if not self.code:
return False,["No code"]
return False, ["No code"]
if not os.path.isdir(macro_dir):
try:
os.makedirs(macro_dir)
@@ -203,7 +228,7 @@ class Macro(object):
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:
with codecs.open(macro_path, "w", "utf-8") as macrofile:
macrofile.write(self.code)
except IOError:
return False, [f"Failed to write {macro_path}"]
@@ -226,11 +251,10 @@ class Macro(object):
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, []
def remove(self) -> bool:
"""Remove a macro and all its related files
@@ -242,7 +266,7 @@ class Macro(object):
return True
macro_dir = FreeCAD.getUserMacroDir(True)
macro_path = os.path.join(macro_dir, self.filename)
macro_path_with_macro_prefix = os.path.join(macro_dir, 'Macro_' + self.filename)
macro_path_with_macro_prefix = os.path.join(macro_dir, "Macro_" + self.filename)
if os.path.exists(macro_path):
os.remove(macro_path)
elif os.path.exists(macro_path_with_macro_prefix):
@@ -255,7 +279,10 @@ class Macro(object):
os.remove(dst_file)
remove_directory_if_empty(os.path.dirname(dst_file))
except Exception:
FreeCAD.Console.PrintWarning(f"Failed to remove macro file '{dst_file}': it might not exist, or its permissions changed\n")
FreeCAD.Console.PrintWarning(
f"Failed to remove macro file '{dst_file}': it might not exist, or its permissions changed\n"
)
return True
# @}

View File

@@ -1,24 +1,24 @@
#***************************************************************************
#* *
#* 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) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
@@ -32,19 +32,20 @@ from PySide2.QtCore import QObject
import addonmanager_utilities as utils
from AddonManagerRepo import AddonManagerRepo
class MetadataDownloadWorker(QObject):
"""A worker for downloading package.xml and associated icon(s)
To use, instantiate an object of this class and call the start_fetch() function
with a QNetworkAccessManager. It is expected that many of these objects will all
be created and associated with the same QNAM, which will then handle the actual
asynchronous downloads in some Qt-defined number of threads. To monitor progress
you should connect to the QNAM's "finished" signal, and ensure it is called the
number of times you expect based on how many workers you have enqueued.
"""
updated = QtCore.Signal(AddonManagerRepo)
updated = QtCore.Signal(AddonManagerRepo)
def __init__(self, parent, repo, index):
"repo is an AddonManagerRepo object, and index is a dictionary of SHA1 hashes of the package.xml files in the cache"
@@ -52,15 +53,19 @@ class MetadataDownloadWorker(QObject):
super().__init__(parent)
self.repo = repo
self.index = index
self.store = os.path.join(FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata")
self.store = os.path.join(
FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata"
)
self.last_sha1 = ""
self.url = self.repo.metadata_url
def start_fetch(self, network_manager):
"Asynchronously begin the network access. Intended as a set-and-forget black box for downloading metadata."
"Asynchronously begin the network access. Intended as a set-and-forget black box for downloading metadata."
self.request = QtNetwork.QNetworkRequest(QtCore.QUrl(self.url))
self.request.setAttribute(QtNetwork.QNetworkRequest.RedirectPolicyAttribute,
QtNetwork.QNetworkRequest.UserVerifiedRedirectPolicy)
self.request.setAttribute(
QtNetwork.QNetworkRequest.RedirectPolicyAttribute,
QtNetwork.QNetworkRequest.UserVerifiedRedirectPolicy,
)
self.fetch_task = network_manager.get(self.request)
self.fetch_task.finished.connect(self.resolve_fetch)
@@ -84,7 +89,9 @@ class MetadataDownloadWorker(QObject):
def resolve_fetch(self):
"Called when the data fetch completed, either with an error, or if it found the metadata file"
if self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.NoError:
FreeCAD.Console.PrintMessage(f"Found a metadata file for {self.repo.name}\n")
FreeCAD.Console.PrintMessage(
f"Found a metadata file for {self.repo.name}\n"
)
self.repo.repo_type = AddonManagerRepo.RepoType.PACKAGE
new_xml = self.fetch_task.readAll()
hasher = hashlib.sha1()
@@ -110,17 +117,25 @@ class MetadataDownloadWorker(QObject):
# There is no local copy yet, so we definitely have to update
# the cache
self.update_local_copy(new_xml)
elif self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.ContentNotFoundError:
elif (
self.fetch_task.error()
== QtNetwork.QNetworkReply.NetworkError.ContentNotFoundError
):
pass
elif self.fetch_task.error() == QtNetwork.QNetworkReply.NetworkError.OperationCanceledError:
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")
FreeCAD.Console.PrintWarning(
f"Failed to connect to {self.url}:\n {self.fetch_task.error()}\n"
)
def update_local_copy(self, new_xml):
# We have to update the local copy of the metadata file and re-download
# the icon file
name = self.repo.name
repo_url = self.repo.url
package_cache_directory = os.path.join(self.store, name)

View File

@@ -1,25 +1,25 @@
# -*- coding: utf-8 -*-
#***************************************************************************
#* *
#* Copyright (c) 2018 Gaël Écorchard <galou_breizh@yahoo.fr> *
#* *
#* 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) 2018 Gaël Écorchard <galou_breizh@yahoo.fr> *
# * *
# * 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 codecs
import os
@@ -49,9 +49,9 @@ except ImportError:
pass
else:
try:
#ssl_ctx = ssl.create_default_context(cafile=certifi.where())
# ssl_ctx = ssl.create_default_context(cafile=certifi.where())
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
#ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
# ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
except AttributeError:
pass
@@ -94,7 +94,7 @@ def symlink(source, link_name):
raise ctypes.WinError()
def urlopen(url:str):
def urlopen(url: str):
"""Opens an url with urllib and streams it to a temp file"""
timeout = 5
@@ -106,7 +106,7 @@ def urlopen(url:str):
else:
if pref.GetBool("SystemProxyCheck", False):
proxy = urllib.request.getproxies()
proxies = {"http": proxy.get('http'), "https": proxy.get('http')}
proxies = {"http": proxy.get("http"), "https": proxy.get("http")}
elif pref.GetBool("UserProxyCheck", False):
proxy = pref.GetString("ProxyUrl", "")
proxies = {"http": proxy, "https": proxy}
@@ -120,8 +120,7 @@ def urlopen(url:str):
urllib.request.install_opener(opener)
# Url opening
req = urllib.request.Request(url,
headers={'User-Agent': "Magic Browser"})
req = urllib.request.Request(url, headers={"User-Agent": "Magic Browser"})
try:
u = urllib.request.urlopen(req, timeout=timeout)
@@ -137,7 +136,7 @@ def urlopen(url:str):
def getserver(url):
"""returns the server part of an url"""
return '{uri.scheme}://{uri.netloc}/'.format(uri=urlparse(url))
return "{uri.scheme}://{uri.netloc}/".format(uri=urlparse(url))
def update_macro_details(old_macro, new_macro):
@@ -149,19 +148,20 @@ def update_macro_details(old_macro, new_macro):
"""
if old_macro.on_git and new_macro.on_git:
FreeCAD.Console.PrintWarning('The macro "{}" is present twice in github, please report'.format(old_macro.name))
FreeCAD.Console.PrintWarning(
'The macro "{}" is present twice in github, please report'.format(
old_macro.name
)
)
# We don't report macros present twice on the wiki because a link to a
# macro is considered as a macro. For example, 'Perpendicular To Wire'
# appears twice, as of 2018-05-05).
old_macro.on_wiki = new_macro.on_wiki
for attr in ['desc', 'url', 'code']:
for attr in ["desc", "url", "code"]:
if not hasattr(old_macro, attr):
setattr(old_macro, attr, getattr(new_macro, attr))
def remove_directory_if_empty(dir):
"""Remove the directory if it is empty
@@ -179,7 +179,9 @@ def restart_freecad():
args = QtWidgets.QApplication.arguments()[1:]
if FreeCADGui.getMainWindow().close():
QtCore.QProcess.startDetached(QtWidgets.QApplication.applicationFilePath(), args)
QtCore.QProcess.startDetached(
QtWidgets.QApplication.applicationFilePath(), args
)
def get_zip_url(repo):
@@ -188,15 +190,23 @@ def get_zip_url(repo):
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
return f"{repo.url}/archive/{repo.branch}.zip"
elif parsedUrl.netloc == "framagit.org" or parsedUrl.netloc == "gitlab.com" or parsedUrl.netloc == "salsa.debian.org":
elif (
parsedUrl.netloc == "framagit.org"
or parsedUrl.netloc == "gitlab.com"
or parsedUrl.netloc == "salsa.debian.org"
):
# https://framagit.org/freecad-france/mooc-workbench/-/archive/master/mooc-workbench-master.zip
# https://salsa.debian.org/mess42/pyrate/-/archive/master/pyrate-master.zip
reponame = baseurl.strip("/").split("/")[-1]
return f"{repo.url}/-/archive/{repo.branch}/{repo.name}-{repo.branch}.zip"
else:
FreeCAD.Console.PrintWarning("Debug: addonmanager_utilities.get_zip_url: Unknown git host:", parsedUrl.netloc)
FreeCAD.Console.PrintWarning(
"Debug: addonmanager_utilities.get_zip_url: Unknown git host:",
parsedUrl.netloc,
)
return None
def construct_git_url(repo, filename):
"Returns a direct download link to a file in an online Git repo: works with github, gitlab, and framagit"
@@ -209,14 +219,19 @@ def construct_git_url(repo, filename):
# e.g. https://salsa.debian.org/joha2/pyrate/-/raw/master/package.xml
return f"{repo.url}/-/raw/{repo.branch}/{filename}"
else:
FreeCAD.Console.PrintLog("Debug: addonmanager_utilities.construct_git_url: Unknown git host:" + parsed_url.netloc)
FreeCAD.Console.PrintLog(
"Debug: addonmanager_utilities.construct_git_url: Unknown git host:"
+ parsed_url.netloc
)
return None
def get_readme_url(repo):
"Returns the location of a readme file"
return construct_git_url(repo, "README.md")
def get_metadata_url(url):
"Returns the location of a package.xml metadata file"
@@ -230,9 +245,15 @@ def get_desc_regex(repo):
parsedUrl = urlparse(repo.url)
if parsedUrl.netloc == "github.com":
return r'<meta property="og:description" content="(.*?)"'
elif parsedUrl.netloc == "framagit.org" or parsedUrl.netloc == "gitlab.com" or parsedUrl.netloc == "salsa.debian.org":
elif (
parsedUrl.netloc == "framagit.org"
or parsedUrl.netloc == "gitlab.com"
or parsedUrl.netloc == "salsa.debian.org"
):
return r'<meta.*?content="(.*?)".*?og:description.*?>'
FreeCAD.Console.PrintWarning("Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", repo.url)
FreeCAD.Console.PrintWarning(
"Debug: addonmanager_utilities.get_desc_regex: Unknown git host:", repo.url
)
return None
@@ -263,14 +284,16 @@ def fix_relative_links(text, base_url):
new_text = ""
for line in text.splitlines():
for link in (re.findall(r"!\[.*?\]\((.*?)\)", line) +
re.findall(r"src\s*=\s*[\"'](.+?)[\"']", line)):
parts = link.split('/')
for link in re.findall(r"!\[.*?\]\((.*?)\)", line) + re.findall(
r"src\s*=\s*[\"'](.+?)[\"']", line
):
parts = link.split("/")
if len(parts) < 2 or not re.match(r"^http|^www|^.+\.|^/", parts[0]):
newlink = os.path.join(base_url, link.lstrip('./'))
newlink = os.path.join(base_url, link.lstrip("./"))
line = line.replace(link, newlink)
FreeCAD.Console.PrintLog("Debug: replaced " + link + " with " + newlink)
new_text = new_text + '\n' + line
new_text = new_text + "\n" + line
return new_text
# @}

File diff suppressed because it is too large Load Diff

View File

@@ -54,7 +54,9 @@ class Ui_CompactView(object):
sizePolicy2 = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.labelDescription.sizePolicy().hasHeightForWidth())
sizePolicy2.setHeightForWidth(
self.labelDescription.sizePolicy().hasHeightForWidth()
)
self.labelDescription.setSizePolicy(sizePolicy2)
self.labelDescription.setTextFormat(Qt.PlainText)
self.labelDescription.setWordWrap(False)
@@ -67,18 +69,28 @@ class Ui_CompactView(object):
self.horizontalLayout_2.addWidget(self.labelStatus)
self.retranslateUi(CompactView)
QMetaObject.connectSlotsByName(CompactView)
# setupUi
def retranslateUi(self, CompactView):
CompactView.setWindowTitle(QCoreApplication.translate("CompactView", u"Form", None))
CompactView.setWindowTitle(
QCoreApplication.translate("CompactView", u"Form", None)
)
self.labelIcon.setText(QCoreApplication.translate("CompactView", u"Icon", None))
self.labelPackageName.setText(QCoreApplication.translate("CompactView", u"<b>Package Name</b>", None))
self.labelVersion.setText(QCoreApplication.translate("CompactView", u"Version", None))
self.labelDescription.setText(QCoreApplication.translate("CompactView", u"Description", None))
self.labelStatus.setText(QCoreApplication.translate("CompactView", u"UpdateAvailable", None))
# retranslateUi
self.labelPackageName.setText(
QCoreApplication.translate("CompactView", u"<b>Package Name</b>", None)
)
self.labelVersion.setText(
QCoreApplication.translate("CompactView", u"Version", None)
)
self.labelDescription.setText(
QCoreApplication.translate("CompactView", u"Description", None)
)
self.labelStatus.setText(
QCoreApplication.translate("CompactView", u"UpdateAvailable", None)
)
# retranslateUi

View File

@@ -39,7 +39,9 @@ class Ui_ExpandedView(object):
self.horizontalLayout_2.addWidget(self.labelIcon)
self.horizontalSpacer = QSpacerItem(8, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)
self.horizontalSpacer = QSpacerItem(
8, 20, QSizePolicy.Fixed, QSizePolicy.Minimum
)
self.horizontalLayout_2.addItem(self.horizontalSpacer)
@@ -59,60 +61,81 @@ class Ui_ExpandedView(object):
sizePolicy2 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.labelVersion.sizePolicy().hasHeightForWidth())
sizePolicy2.setHeightForWidth(
self.labelVersion.sizePolicy().hasHeightForWidth()
)
self.labelVersion.setSizePolicy(sizePolicy2)
self.horizontalLayout.addWidget(self.labelVersion)
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalSpacer_2 = QSpacerItem(
40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum
)
self.horizontalLayout.addItem(self.horizontalSpacer_2)
self.verticalLayout.addLayout(self.horizontalLayout)
self.labelDescription = QLabel(ExpandedView)
self.labelDescription.setObjectName(u"labelDescription")
sizePolicy.setHeightForWidth(self.labelDescription.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.labelDescription.sizePolicy().hasHeightForWidth()
)
self.labelDescription.setSizePolicy(sizePolicy)
self.labelDescription.setTextFormat(Qt.PlainText)
self.labelDescription.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop)
self.labelDescription.setAlignment(Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop)
self.labelDescription.setWordWrap(True)
self.verticalLayout.addWidget(self.labelDescription)
self.labelMaintainer = QLabel(ExpandedView)
self.labelMaintainer.setObjectName(u"labelMaintainer")
sizePolicy2.setHeightForWidth(self.labelMaintainer.sizePolicy().hasHeightForWidth())
sizePolicy2.setHeightForWidth(
self.labelMaintainer.sizePolicy().hasHeightForWidth()
)
self.labelMaintainer.setSizePolicy(sizePolicy2)
self.labelMaintainer.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop)
self.labelMaintainer.setAlignment(Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop)
self.labelMaintainer.setWordWrap(False)
self.verticalLayout.addWidget(self.labelMaintainer)
self.horizontalLayout_2.addLayout(self.verticalLayout)
self.labelStatus = QLabel(ExpandedView)
self.labelStatus.setObjectName(u"labelStatus")
self.labelStatus.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
self.labelStatus.setAlignment(
Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter
)
self.horizontalLayout_2.addWidget(self.labelStatus)
self.retranslateUi(ExpandedView)
QMetaObject.connectSlotsByName(ExpandedView)
# setupUi
def retranslateUi(self, ExpandedView):
ExpandedView.setWindowTitle(QCoreApplication.translate("ExpandedView", u"Form", None))
self.labelIcon.setText(QCoreApplication.translate("ExpandedView", u"Icon", None))
self.labelPackageName.setText(QCoreApplication.translate("ExpandedView", u"<h1>Package Name</h1>", None))
self.labelVersion.setText(QCoreApplication.translate("ExpandedView", u"Version", None))
self.labelDescription.setText(QCoreApplication.translate("ExpandedView", u"Description", None))
self.labelMaintainer.setText(QCoreApplication.translate("ExpandedView", u"Maintainer", None))
self.labelStatus.setText(QCoreApplication.translate("ExpandedView", u"UpdateAvailable", None))
# retranslateUi
ExpandedView.setWindowTitle(
QCoreApplication.translate("ExpandedView", u"Form", None)
)
self.labelIcon.setText(
QCoreApplication.translate("ExpandedView", u"Icon", None)
)
self.labelPackageName.setText(
QCoreApplication.translate("ExpandedView", u"<h1>Package Name</h1>", None)
)
self.labelVersion.setText(
QCoreApplication.translate("ExpandedView", u"Version", None)
)
self.labelDescription.setText(
QCoreApplication.translate("ExpandedView", u"Description", None)
)
self.labelMaintainer.setText(
QCoreApplication.translate("ExpandedView", u"Maintainer", None)
)
self.labelStatus.setText(
QCoreApplication.translate("ExpandedView", u"UpdateAvailable", None)
)
# retranslateUi

View File

@@ -1,26 +1,26 @@
# -*- coding: utf-8 -*-
#***************************************************************************
#* *
#* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
#* *
#* This program is free software; you can redistribute it and/or modify *
#* it under the terms of the GNU Lesser General Public License (LGPL) *
#* as published by the Free Software Foundation; either version 2 of *
#* the License, or (at your option) any later version. *
#* for detail see the LICENCE text file. *
#* *
#* This program is distributed in the hope that it will be useful, *
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
#* GNU Library General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with this program; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************
# ***************************************************************************
# * *
# * Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
@@ -37,6 +37,7 @@ from AddonManagerRepo import AddonManagerRepo
import inspect
class PackageDetails(QWidget):
back = Signal()
@@ -61,7 +62,7 @@ class PackageDetails(QWidget):
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, reload:bool = False) -> None:
def show_repo(self, repo: AddonManagerRepo, reload: bool = False) -> None:
self.repo = repo
@@ -90,36 +91,76 @@ class PackageDetails(QWidget):
if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
installed_version_string = ""
if repo.installed_version:
installed_version_string = translate("AddonsInstaller", "Version") + " "
installed_version_string = translate("AddonsInstaller", "Version") + " "
installed_version_string += repo.installed_version
else:
installed_version_string = translate("AddonsInstaller", "Unknown version (no package.xml file found)") + " "
installed_version_string = (
translate(
"AddonsInstaller", "Unknown version (no package.xml file found)"
)
+ " "
)
if repo.updated_timestamp:
installed_version_string += " " + translate("AddonsInstaller", "installed on") + " "
installed_version_string += QDateTime.fromTime_t(repo.updated_timestamp).date().toString(Qt.SystemLocaleShortDate)
installed_version_string += (
" " + translate("AddonsInstaller", "installed on") + " "
)
installed_version_string += (
QDateTime.fromTime_t(repo.updated_timestamp)
.date()
.toString(Qt.SystemLocaleShortDate)
)
installed_version_string += ". "
else:
installed_version_string += translate("AddonsInstaller", "installed") + ". "
installed_version_string += (
translate("AddonsInstaller", "installed") + ". "
)
if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
if repo.metadata:
installed_version_string += "<b>" + translate("AddonsInstaller", "Update available to version") + " "
installed_version_string += (
"<b>"
+ translate("AddonsInstaller", "Update available to version")
+ " "
)
installed_version_string += repo.metadata.Version
installed_version_string += ".</b>"
else:
installed_version_string += "<b>" + translate("AddonsInstaller", "Update available to unknown version (no package.xml file found)") + ".</b>"
elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
installed_version_string += translate("AddonsInstaller", "This is the latest version available") + "."
installed_version_string += (
"<b>"
+ translate(
"AddonsInstaller",
"Update available to unknown version (no package.xml file found)",
)
+ ".</b>"
)
elif (
repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
):
installed_version_string += (
translate("AddonsInstaller", "This is the latest version available")
+ "."
)
elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
installed_version_string += translate("AddonsInstaller", "Updated, please restart FreeCAD to use") + "."
installed_version_string += (
translate(
"AddonsInstaller", "Updated, please restart FreeCAD to use"
)
+ "."
)
elif repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
installed_version_string += translate("AddonsInstaller", "Update check in progress") + "."
installed_version_string += (
translate("AddonsInstaller", "Update check in progress") + "."
)
basedir = FreeCAD.getUserAppDataDir()
moddir = os.path.join(basedir , "Mod", repo.name)
installed_version_string += "<br/>" + translate("AddonsInstaller", "Installation location") + ": " + moddir
moddir = os.path.join(basedir, "Mod", repo.name)
installed_version_string += (
"<br/>"
+ translate("AddonsInstaller", "Installation location")
+ ": "
+ moddir
)
self.ui.labelPackageDetails.setText(installed_version_string)
self.ui.labelPackageDetails.show()
@@ -148,30 +189,42 @@ class PackageDetails(QWidget):
self.ui.buttonUpdate.hide()
@classmethod
def cache_path(self, repo:AddonManagerRepo) -> str:
def cache_path(self, repo: AddonManagerRepo) -> str:
cache_path = FreeCAD.getUserCachePath()
full_path = os.path.join(cache_path,"AddonManager",repo.name)
full_path = os.path.join(cache_path, "AddonManager", repo.name)
return full_path
def check_and_clean_cache(self, force:bool = False) -> None:
def check_and_clean_cache(self, force: bool = False) -> None:
cache_path = PackageDetails.cache_path(self.repo)
readme_cache_file = os.path.join(cache_path,"README.html")
readme_images_path = os.path.join(cache_path,"Images")
download_interrupted_sentinel = os.path.join(readme_images_path,"download_in_progress")
readme_cache_file = os.path.join(cache_path, "README.html")
readme_images_path = os.path.join(cache_path, "Images")
download_interrupted_sentinel = os.path.join(
readme_images_path, "download_in_progress"
)
download_interrupted = os.path.isfile(download_interrupted_sentinel)
if os.path.isfile(readme_cache_file):
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
days_between_updates = pref.GetInt("DaysBetweenUpdates", 2^32)
days_between_updates = pref.GetInt("DaysBetweenUpdates", 2 ^ 32)
timestamp = os.path.getmtime(readme_cache_file)
last_cache_update = date.fromtimestamp(timestamp)
delta_update = timedelta(days=days_between_updates)
if date.today() >= last_cache_update + delta_update or download_interrupted or force:
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")
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")
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")
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)
@@ -180,99 +233,129 @@ class PackageDetails(QWidget):
self.check_and_clean_cache(force=True)
self.show_repo(self.repo)
def show_cached_readme(self, repo:AddonManagerRepo) -> bool:
""" Attempts to show a cached readme, returns true if there was a cache, or false if not """
def show_cached_readme(self, repo: AddonManagerRepo) -> bool:
"""Attempts to show a cached readme, returns true if there was a cache, or false if not"""
cache_path = PackageDetails.cache_path(repo)
readme_cache_file = os.path.join(cache_path,"README.html")
readme_cache_file = os.path.join(cache_path, "README.html")
if os.path.isfile(readme_cache_file):
with open(readme_cache_file,"rb") as f:
with open(readme_cache_file, "rb") as f:
data = f.read()
self.ui.textBrowserReadMe.setText(data.decode())
return True
return False
def show_workbench(self, repo:AddonManagerRepo) -> None:
def show_workbench(self, repo: AddonManagerRepo) -> None:
"""loads information of a given workbench"""
if not self.show_cached_readme(repo):
self.ui.textBrowserReadMe.setText(translate("AddonsInstaller","Fetching README.md from package repository"))
self.ui.textBrowserReadMe.setText(
translate(
"AddonsInstaller", "Fetching README.md from package repository"
)
)
self.worker = ShowWorker(repo, PackageDetails.cache_path(repo))
self.worker.readme_updated.connect(lambda desc: self.cache_readme(repo, desc))
self.worker.readme_updated.connect(lambda desc: self.ui.textBrowserReadMe.setText(desc))
self.worker.readme_updated.connect(
lambda desc: self.cache_readme(repo, desc)
)
self.worker.readme_updated.connect(
lambda desc: self.ui.textBrowserReadMe.setText(desc)
)
self.worker.update_status.connect(self.update_status.emit)
self.worker.update_status.connect(self.show)
self.worker.start()
def show_package(self, repo:AddonManagerRepo) -> None:
""" Show the details for a package (a repo with a package.xml metadata file) """
def show_package(self, repo: AddonManagerRepo) -> None:
"""Show the details for a package (a repo with a package.xml metadata file)"""
if not self.show_cached_readme(repo):
self.ui.textBrowserReadMe.setText(translate("AddonsInstaller","Fetching README.md from package repository"))
self.worker = ShowWorker(repo,PackageDetails.cache_path(repo))
self.worker.readme_updated.connect(lambda desc: self.cache_readme(repo, desc))
self.worker.readme_updated.connect(lambda desc: self.ui.textBrowserReadMe.setText(desc))
self.ui.textBrowserReadMe.setText(
translate(
"AddonsInstaller", "Fetching README.md from package repository"
)
)
self.worker = ShowWorker(repo, PackageDetails.cache_path(repo))
self.worker.readme_updated.connect(
lambda desc: self.cache_readme(repo, desc)
)
self.worker.readme_updated.connect(
lambda desc: self.ui.textBrowserReadMe.setText(desc)
)
self.worker.update_status.connect(self.update_status.emit)
self.worker.update_status.connect(self.show)
self.worker.start()
def show_macro(self, repo:AddonManagerRepo) -> None:
def show_macro(self, repo: AddonManagerRepo) -> None:
"""loads information of a given macro"""
if not self.show_cached_readme(repo):
self.ui.textBrowserReadMe.setText(translate("AddonsInstaller","Fetching README.md from package repository"))
self.ui.textBrowserReadMe.setText(
translate(
"AddonsInstaller", "Fetching README.md from package repository"
)
)
self.worker = GetMacroDetailsWorker(repo)
self.worker.readme_updated.connect(lambda desc: self.cache_readme(repo, desc))
self.worker.readme_updated.connect(lambda desc: self.ui.textBrowserReadMe.setText(desc))
self.worker.readme_updated.connect(
lambda desc: self.cache_readme(repo, desc)
)
self.worker.readme_updated.connect(
lambda desc: self.ui.textBrowserReadMe.setText(desc)
)
self.worker.start()
def cache_readme(self, repo:AddonManagerRepo, readme:str) -> None:
def cache_readme(self, repo: AddonManagerRepo, readme: str) -> None:
cache_path = PackageDetails.cache_path(repo)
readme_cache_file = os.path.join(cache_path,"README.html")
os.makedirs(cache_path,exist_ok=True)
with open(readme_cache_file,"wb") as f:
readme_cache_file = os.path.join(cache_path, "README.html")
os.makedirs(cache_path, exist_ok=True)
with open(readme_cache_file, "wb") as f:
f.write(readme.encode())
class Ui_PackageDetails(object):
def setupUi(self, PackageDetails):
if not PackageDetails.objectName():
PackageDetails.setObjectName(u"PackageDetails")
PackageDetails.setObjectName("PackageDetails")
self.verticalLayout_2 = QVBoxLayout(PackageDetails)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.layoutDetailsBackButton = QHBoxLayout()
self.layoutDetailsBackButton.setObjectName(u"layoutDetailsBackButton")
self.layoutDetailsBackButton.setObjectName("layoutDetailsBackButton")
self.buttonBack = QToolButton(PackageDetails)
self.buttonBack.setObjectName(u"buttonBack")
self.buttonBack.setIcon(QIcon.fromTheme("back", QIcon(":/icons/button_left.svg")))
self.buttonBack.setObjectName("buttonBack")
self.buttonBack.setIcon(
QIcon.fromTheme("back", QIcon(":/icons/button_left.svg"))
)
self.buttonRefresh = QToolButton(PackageDetails)
self.buttonRefresh.setObjectName(u"buttonRefresh")
self.buttonRefresh.setIcon(QIcon.fromTheme("refresh", QIcon(":/icons/view-refresh.svg")))
self.buttonRefresh.setObjectName("buttonRefresh")
self.buttonRefresh.setIcon(
QIcon.fromTheme("refresh", QIcon(":/icons/view-refresh.svg"))
)
self.layoutDetailsBackButton.addWidget(self.buttonBack)
self.layoutDetailsBackButton.addWidget(self.buttonRefresh)
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalSpacer = QSpacerItem(
40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum
)
self.layoutDetailsBackButton.addItem(self.horizontalSpacer)
self.buttonInstall = QPushButton(PackageDetails)
self.buttonInstall.setObjectName(u"buttonInstall")
self.buttonInstall.setObjectName("buttonInstall")
self.layoutDetailsBackButton.addWidget(self.buttonInstall)
self.buttonUninstall = QPushButton(PackageDetails)
self.buttonUninstall.setObjectName(u"buttonUninstall")
self.buttonUninstall.setObjectName("buttonUninstall")
self.layoutDetailsBackButton.addWidget(self.buttonUninstall)
self.buttonUpdate = QPushButton(PackageDetails)
self.buttonUpdate.setObjectName(u"buttonUpdate")
self.buttonUpdate.setObjectName("buttonUpdate")
self.layoutDetailsBackButton.addWidget(self.buttonUpdate)
self.buttonExecute = QPushButton(PackageDetails)
self.buttonExecute.setObjectName(u"buttonExecute")
self.buttonExecute.setObjectName("buttonExecute")
self.layoutDetailsBackButton.addWidget(self.buttonExecute)
@@ -284,25 +367,43 @@ class Ui_PackageDetails(object):
self.verticalLayout_2.addWidget(self.labelPackageDetails)
self.textBrowserReadMe = QTextBrowser(PackageDetails)
self.textBrowserReadMe.setObjectName(u"textBrowserReadMe")
self.textBrowserReadMe.setObjectName("textBrowserReadMe")
self.textBrowserReadMe.setOpenExternalLinks(True)
self.textBrowserReadMe.setOpenLinks(True)
self.verticalLayout_2.addWidget(self.textBrowserReadMe)
self.retranslateUi(PackageDetails)
QMetaObject.connectSlotsByName(PackageDetails)
# setupUi
def retranslateUi(self, PackageDetails):
self.buttonBack.setText("")
self.buttonInstall.setText(QCoreApplication.translate("AddonsInstaller", u"Install", None))
self.buttonUninstall.setText(QCoreApplication.translate("AddonsInstaller", u"Uninstall", None))
self.buttonUpdate.setText(QCoreApplication.translate("AddonsInstaller", u"Update", None))
self.buttonExecute.setText(QCoreApplication.translate("AddonsInstaller", u"Run Macro", None))
self.buttonBack.setToolTip(QCoreApplication.translate("AddonsInstaller", u"Return to package list", None))
self.buttonRefresh.setToolTip(QCoreApplication.translate("AddonsInstaller", u"Delete cached version of this README and re-download", None))
# retranslateUi
self.buttonInstall.setText(
QCoreApplication.translate("AddonsInstaller", "Install", None)
)
self.buttonUninstall.setText(
QCoreApplication.translate("AddonsInstaller", "Uninstall", None)
)
self.buttonUpdate.setText(
QCoreApplication.translate("AddonsInstaller", "Update", None)
)
self.buttonExecute.setText(
QCoreApplication.translate("AddonsInstaller", "Run Macro", None)
)
self.buttonBack.setToolTip(
QCoreApplication.translate(
"AddonsInstaller", "Return to package list", None
)
)
self.buttonRefresh.setToolTip(
QCoreApplication.translate(
"AddonsInstaller",
"Delete cached version of this README and re-download",
None,
)
)
# retranslateUi

View File

@@ -1,26 +1,26 @@
# -*- coding: utf-8 -*-
#***************************************************************************
#* *
#* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
#* *
#* This program is free software; you can redistribute it and/or modify *
#* it under the terms of the GNU Lesser General Public License (LGPL) *
#* as published by the Free Software Foundation; either version 2 of *
#* the License, or (at your option) any later version. *
#* for detail see the LICENCE text file. *
#* *
#* This program is distributed in the hope that it will be useful, *
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
#* GNU Library General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with this program; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************
# ***************************************************************************
# * *
# * Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
@@ -39,31 +39,37 @@ from AddonManagerRepo import AddonManagerRepo
from compact_view import Ui_CompactView
from expanded_view import Ui_ExpandedView
class ListDisplayStyle(IntEnum):
COMPACT = 0
EXPANDED = 1
class PackageList(QWidget):
""" A widget that shows a list of packages and various widgets to control the display of the list """
"""A widget that shows a list of packages and various widgets to control the display of the list"""
itemSelected = Signal(AddonManagerRepo)
def __init__(self,parent=None):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_PackageList()
self.ui.setupUi(self)
self.item_filter = PackageListFilter()
self.ui.listPackages.setModel (self.item_filter)
self.ui.listPackages.setModel(self.item_filter)
self.item_delegate = PackageListItemDelegate(self.ui.listPackages)
self.ui.listPackages.setItemDelegate(self.item_delegate)
self.ui.listPackages.clicked.connect(self.on_listPackages_clicked)
self.ui.comboPackageType.currentIndexChanged.connect(self.update_type_filter)
self.ui.lineEditFilter.textChanged.connect(self.update_text_filter)
self.ui.buttonCompactLayout.clicked.connect(lambda: self.set_view_style(ListDisplayStyle.COMPACT))
self.ui.buttonExpandedLayout.clicked.connect(lambda: self.set_view_style(ListDisplayStyle.EXPANDED))
self.ui.buttonCompactLayout.clicked.connect(
lambda: self.set_view_style(ListDisplayStyle.COMPACT)
)
self.ui.buttonExpandedLayout.clicked.connect(
lambda: self.set_view_style(ListDisplayStyle.EXPANDED)
)
# Only shows when the user types in a filter
self.ui.labelFilterValidity.hide()
@@ -85,51 +91,56 @@ class PackageList(QWidget):
else:
self.ui.buttonCompactLayout.setChecked(True)
def on_listPackages_clicked(self, index:QModelIndex):
source_selection = self.item_filter.mapToSource (index)
def on_listPackages_clicked(self, index: QModelIndex):
source_selection = self.item_filter.mapToSource(index)
selected_repo = self.item_model.repos[source_selection.row()]
self.itemSelected.emit(selected_repo)
def update_type_filter(self, type_filter:int) -> None:
def update_type_filter(self, type_filter: int) -> None:
"""hide/show rows corresponding to the type filter
type_filter is an integer: 0 for all, 1 for workbenches, 2 for macros, and 3 for preference packs
"""
self.item_filter.setPackageFilter(type_filter)
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetInt("PackageTypeSelection", type_filter)
def update_text_filter(self, text_filter:str) -> None:
def update_text_filter(self, text_filter: str) -> None:
"""filter name and description by the regex specified by text_filter"""
if text_filter:
test_regex = QRegularExpression(text_filter)
if test_regex.isValid():
self.ui.labelFilterValidity.setToolTip(translate("AddonsInstaller","Filter is valid"))
self.ui.labelFilterValidity.setToolTip(
translate("AddonsInstaller", "Filter is valid")
)
icon = QIcon.fromTheme("ok", QIcon(":/icons/edit_OK.svg"))
self.ui.labelFilterValidity.setPixmap(icon.pixmap(16,16))
self.ui.labelFilterValidity.setPixmap(icon.pixmap(16, 16))
else:
self.ui.labelFilterValidity.setToolTip(translate("AddonsInstaller","Filter regular expression is invalid"))
self.ui.labelFilterValidity.setToolTip(
translate("AddonsInstaller", "Filter regular expression is invalid")
)
icon = QIcon.fromTheme("cancel", QIcon(":/icons/edit_Cancel.svg"))
self.ui.labelFilterValidity.setPixmap(icon.pixmap(16,16))
self.ui.labelFilterValidity.setPixmap(icon.pixmap(16, 16))
self.ui.labelFilterValidity.show()
else:
self.ui.labelFilterValidity.hide()
self.item_filter.setFilterRegularExpression(text_filter)
def set_view_style(self, style:ListDisplayStyle) -> None:
def set_view_style(self, style: ListDisplayStyle) -> None:
self.item_delegate.set_view(style)
if style == ListDisplayStyle.COMPACT:
self.ui.listPackages.setSpacing(2)
else:
self.ui.listPackages.setSpacing(5)
self.item_model.layoutChanged.emit()
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetInt("ViewStyle", style)
class PackageListItemModel(QAbstractListModel):
repos = []
@@ -142,28 +153,37 @@ class PackageListItemModel(QAbstractListModel):
def __init__(self, parent=None) -> None:
super().__init__(parent)
def rowCount(self, parent:QModelIndex=QModelIndex()) -> int:
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return len(self.repos)
def columnCount(self, parent:QModelIndex=QModelIndex()) -> int:
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return 1
def data(self, index:QModelIndex, role:int=Qt.DisplayRole):
def data(self, index: QModelIndex, role: int = Qt.DisplayRole):
if not index.isValid():
return None
row = index.row()
if role == Qt.ToolTipRole:
tooltip = ""
if self.repos[row].repo_type == AddonManagerRepo.RepoType.PACKAGE:
tooltip = translate("AddonsInstaller","Click for details about package") + f" '{self.repos[row].name}'"
tooltip = (
translate("AddonsInstaller", "Click for details about package")
+ f" '{self.repos[row].name}'"
)
elif self.repos[row].repo_type == AddonManagerRepo.RepoType.WORKBENCH:
tooltip = translate("AddonsInstaller","Click for details about workbench") + f" '{self.repos[row].name}'"
tooltip = (
translate("AddonsInstaller", "Click for details about workbench")
+ f" '{self.repos[row].name}'"
)
elif self.repos[row].repo_type == AddonManagerRepo.RepoType.MACRO:
tooltip = translate("AddonsInstaller","Click for details about macro") + f" '{self.repos[row].name}'"
tooltip = (
translate("AddonsInstaller", "Click for details about macro")
+ f" '{self.repos[row].name}'"
)
return tooltip
elif role == PackageListItemModel.DataAccessRole:
return self.repos[row]
@@ -171,20 +191,28 @@ class PackageListItemModel(QAbstractListModel):
def headerData(self, section, orientation, role=Qt.DisplayRole):
return None
def setData(self, index:QModelIndex, value, role=Qt.EditRole) -> None:
""" Set the data for this row. The column of the index is ignored. """
def setData(self, index: QModelIndex, value, role=Qt.EditRole) -> None:
"""Set the data for this row. The column of the index is ignored."""
row = index.row()
self.write_lock.acquire()
if role == PackageListItemModel.StatusUpdateRole:
self.repos[row].update_status = value
self.dataChanged.emit(self.index(row,2), self.index(row,2), [PackageListItemModel.StatusUpdateRole])
self.dataChanged.emit(
self.index(row, 2),
self.index(row, 2),
[PackageListItemModel.StatusUpdateRole],
)
elif role == PackageListItemModel.IconUpdateRole:
self.repos[row].icon = value
self.dataChanged.emit(self.index(row,0), self.index(row,0), [PackageListItemModel.IconUpdateRole])
self.dataChanged.emit(
self.index(row, 0),
self.index(row, 0),
[PackageListItemModel.IconUpdateRole],
)
self.write_lock.release()
def append_item(self, repo:AddonManagerRepo) -> None:
def append_item(self, repo: AddonManagerRepo) -> None:
if repo in self.repos:
# Cowardly refuse to insert the same repo a second time
return
@@ -197,53 +225,62 @@ class PackageListItemModel(QAbstractListModel):
def clear(self) -> None:
if self.rowCount() > 0:
self.write_lock.acquire()
self.beginRemoveRows(QModelIndex(), 0, self.rowCount()-1)
self.beginRemoveRows(QModelIndex(), 0, self.rowCount() - 1)
self.repos = []
self.endRemoveRows()
self.write_lock.release()
def update_item_status(self, name:str, status:AddonManagerRepo.UpdateStatus) -> None:
for row,item in enumerate(self.repos):
def update_item_status(
self, name: str, status: AddonManagerRepo.UpdateStatus
) -> None:
for row, item in enumerate(self.repos):
if item.name == name:
self.setData(self.index(row,0), status, PackageListItemModel.StatusUpdateRole)
self.setData(
self.index(row, 0), status, PackageListItemModel.StatusUpdateRole
)
return
def update_item_icon(self, name:str, icon:QIcon) -> None:
for row,item in enumerate(self.repos):
def update_item_icon(self, name: str, icon: QIcon) -> None:
for row, item in enumerate(self.repos):
if item.name == name:
self.setData(self.index(row,0), icon, PackageListItemModel.IconUpdateRole)
self.setData(
self.index(row, 0), icon, PackageListItemModel.IconUpdateRole
)
return
def reload_item(self,repo:AddonManagerRepo) -> None:
for index,item in enumerate(self.repos):
def reload_item(self, repo: AddonManagerRepo) -> None:
for index, item in enumerate(self.repos):
if item.name == repo.name:
self.write_lock.acquire()
self.repos[index] = repo
self.write_lock.release()
return
class CompactView(QWidget):
""" A single-line view of the package information """
"""A single-line view of the package information"""
from compact_view import Ui_CompactView
def __init__(self,parent=None):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_CompactView()
self.ui.setupUi(self)
class ExpandedView(QWidget):
""" A multi-line view of the package information """
"""A multi-line view of the package information"""
from expanded_view import Ui_ExpandedView
def __init__(self,parent=None):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_ExpandedView()
self.ui.setupUi(self)
class PackageListItemDelegate(QStyledItemDelegate):
""" Render the repo data as a formatted region """
"""Render the repo data as a formatted region"""
def __init__(self, parent=None):
super().__init__(parent)
@@ -251,8 +288,8 @@ class PackageListItemDelegate(QStyledItemDelegate):
self.expanded = ExpandedView()
self.compact = CompactView()
self.widget = self.expanded
def set_view (self, style:ListDisplayStyle) -> None:
def set_view(self, style: ListDisplayStyle) -> None:
if not self.displayStyle == style:
self.displayStyle = style
@@ -265,11 +302,11 @@ class PackageListItemDelegate(QStyledItemDelegate):
if self.displayStyle == ListDisplayStyle.EXPANDED:
self.widget = self.expanded
self.widget.ui.labelPackageName.setText(f"<h1>{repo.name}</h1>")
self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(48,48)))
self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(48, 48)))
else:
self.widget = self.compact
self.widget.ui.labelPackageName.setText(f"<b>{repo.name}</b>")
self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(16,16)))
self.widget.ui.labelIcon.setPixmap(repo.icon.pixmap(QSize(16, 16)))
self.widget.ui.labelIcon.setText("")
if repo.metadata:
@@ -279,11 +316,16 @@ class PackageListItemDelegate(QStyledItemDelegate):
maintainers = repo.metadata.Maintainer
maintainers_string = ""
if len(maintainers) == 1:
maintainers_string = translate("AddonsInstaller","Maintainer") + f": {maintainers[0]['name']} <{maintainers[0]['email']}>"
maintainers_string = (
translate("AddonsInstaller", "Maintainer")
+ f": {maintainers[0]['name']} <{maintainers[0]['email']}>"
)
elif len(maintainers) > 1:
maintainers_string = translate("AddonsInstaller","Maintainers:")
maintainers_string = translate("AddonsInstaller", "Maintainers:")
for maintainer in maintainers:
maintainers_string += f"\n{maintainer['name']} <{maintainer['email']}>"
maintainers_string += (
f"\n{maintainer['name']} <{maintainer['email']}>"
)
self.widget.ui.labelMaintainer.setText(maintainers_string)
else:
self.widget.ui.labelDescription.setText("")
@@ -296,86 +338,103 @@ class PackageListItemDelegate(QStyledItemDelegate):
self.widget.ui.labelStatus.setText(self.get_expanded_update_string(repo))
else:
self.widget.ui.labelStatus.setText(self.get_compact_update_string(repo))
self.widget.adjustSize()
def get_compact_update_string(self, repo:AddonManagerRepo) -> str:
""" Get a single-line string listing details about the installed version and date """
def get_compact_update_string(self, repo: AddonManagerRepo) -> str:
"""Get a single-line string listing details about the installed version and date"""
result = ""
if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
result = translate("AddonsInstaller","Installed")
result = translate("AddonsInstaller", "Installed")
elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
result = translate("AddonsInstaller","Up-to-date")
result = translate("AddonsInstaller", "Up-to-date")
elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
result = translate("AddonsInstaller","Update available")
result = translate("AddonsInstaller", "Update available")
elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
result = translate("AddonsInstaller","Pending restart")
result = translate("AddonsInstaller", "Pending restart")
return result
def get_expanded_update_string(self, repo:AddonManagerRepo) -> str:
""" Get a multi-line string listing details about the installed version and date """
def get_expanded_update_string(self, repo: AddonManagerRepo) -> str:
"""Get a multi-line string listing details about the installed version and date"""
result = ""
installed_version_string = ""
if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
if repo.installed_version:
installed_version_string = "\n" + translate("AddonsInstaller", "Installed version") + ": "
installed_version_string = (
"\n" + translate("AddonsInstaller", "Installed version") + ": "
)
installed_version_string += repo.installed_version
else:
installed_version_string = "\n" + translate("AddonsInstaller", "Unknown version")
installed_version_string = "\n" + translate(
"AddonsInstaller", "Unknown version"
)
installed_date_string = ""
if repo.updated_timestamp:
installed_date_string = "\n" + translate("AddonsInstaller", "Installed on") + ": "
installed_date_string += QDateTime.fromTime_t(repo.updated_timestamp).date().toString(Qt.SystemLocaleShortDate)
installed_date_string = (
"\n" + translate("AddonsInstaller", "Installed on") + ": "
)
installed_date_string += (
QDateTime.fromTime_t(repo.updated_timestamp)
.date()
.toString(Qt.SystemLocaleShortDate)
)
available_version_string = ""
if repo.metadata:
available_version_string = "\n" + translate("AddonsInstaller", "Available version") + ": "
available_version_string = (
"\n" + translate("AddonsInstaller", "Available version") + ": "
)
available_version_string += repo.metadata.Version
if repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
result = translate("AddonsInstaller","Installed")
result = translate("AddonsInstaller", "Installed")
result += installed_version_string
result += installed_date_string
elif repo.update_status == AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE:
result = translate("AddonsInstaller","Up-to-date")
result = translate("AddonsInstaller", "Up-to-date")
result += installed_version_string
result += installed_date_string
elif repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
result = translate("AddonsInstaller","Update available")
result = translate("AddonsInstaller", "Update available")
result += installed_version_string
result += installed_date_string
result += available_version_string
elif repo.update_status == AddonManagerRepo.UpdateStatus.PENDING_RESTART:
result = translate("AddonsInstaller","Pending restart")
result = translate("AddonsInstaller", "Pending restart")
return result
def paint(self, painter:QPainter, option:QStyleOptionViewItem, index:QModelIndex):
def paint(
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
):
painter.save()
self.widget.resize(option.rect.size())
painter.translate(option.rect.topLeft())
self.widget.render(painter, QPoint(), QRegion(), QWidget.DrawChildren)
painter.restore()
class PackageListFilter(QSortFilterProxyModel):
""" Handle filtering the item list on various criteria """
"""Handle filtering the item list on various criteria"""
def __init__(self):
super().__init__()
self.package_type = 0 # Default to showing everything
self.package_type = 0 # Default to showing everything
self.setSortCaseSensitivity(Qt.CaseInsensitive)
def setPackageFilter(self, type:int) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs
def setPackageFilter(
self, type: int
) -> None: # 0=All, 1=Workbenches, 2=Macros, 3=Preference Packs
self.package_type = type
self.invalidateFilter()
def lessThan(self, left, right) -> bool:
l = self.sourceModel().data(left,PackageListItemModel.DataAccessRole)
r = self.sourceModel().data(right,PackageListItemModel.DataAccessRole)
l = self.sourceModel().data(left, PackageListItemModel.DataAccessRole)
r = self.sourceModel().data(right, PackageListItemModel.DataAccessRole)
lname = l.name if l.metadata is None else l.metadata.Name
rname = r.name if r.metadata is None else r.metadata.Name
@@ -383,17 +442,17 @@ class PackageListFilter(QSortFilterProxyModel):
def filterAcceptsRow(self, row, parent=QModelIndex()):
index = self.sourceModel().createIndex(row, 0)
data = self.sourceModel().data(index,PackageListItemModel.DataAccessRole)
data = self.sourceModel().data(index, PackageListItemModel.DataAccessRole)
if self.package_type == 1:
if not data.contains_workbench():
return False
elif self.package_type == 2:
if not data.contains_macro():
return False
if not data.contains_macro():
return False
elif self.package_type == 3:
if not data.contains_preference_pack():
return False
if not data.contains_preference_pack():
return False
name = data.name if data.metadata is None else data.metadata.Name
desc = data.description if not data.metadata else data.metadata.Description
re = self.filterRegularExpression()
@@ -407,35 +466,40 @@ class PackageListFilter(QSortFilterProxyModel):
else:
return False
class Ui_PackageList(object):
""" The contents of the PackageList widget """
"""The contents of the PackageList widget"""
def setupUi(self, Form):
if not Form.objectName():
Form.setObjectName(u"PackageList")
Form.setObjectName("PackageList")
self.verticalLayout = QVBoxLayout(Form)
self.verticalLayout.setObjectName(u"verticalLayout")
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout_6 = QHBoxLayout()
self.horizontalLayout_6.setObjectName(u"horizontalLayout_6")
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
self.buttonCompactLayout = QToolButton(Form)
self.buttonCompactLayout.setObjectName(u"buttonCompactLayout")
self.buttonCompactLayout.setObjectName("buttonCompactLayout")
self.buttonCompactLayout.setCheckable(True)
self.buttonCompactLayout.setAutoExclusive(True)
self.buttonCompactLayout.setIcon(QIcon.fromTheme("expanded_view", QIcon(":/icons/compact_view.svg")))
self.buttonCompactLayout.setIcon(
QIcon.fromTheme("expanded_view", QIcon(":/icons/compact_view.svg"))
)
self.horizontalLayout_6.addWidget(self.buttonCompactLayout)
self.buttonExpandedLayout = QToolButton(Form)
self.buttonExpandedLayout.setObjectName(u"buttonExpandedLayout")
self.buttonExpandedLayout.setObjectName("buttonExpandedLayout")
self.buttonExpandedLayout.setCheckable(True)
self.buttonExpandedLayout.setChecked(True)
self.buttonExpandedLayout.setAutoExclusive(True)
self.buttonExpandedLayout.setIcon(QIcon.fromTheme("expanded_view", QIcon(":/icons/expanded_view.svg")))
self.buttonExpandedLayout.setIcon(
QIcon.fromTheme("expanded_view", QIcon(":/icons/expanded_view.svg"))
)
self.horizontalLayout_6.addWidget(self.buttonExpandedLayout)
self.labelPackagesContaining = QLabel(Form)
self.labelPackagesContaining.setObjectName(u"labelPackagesContaining")
self.labelPackagesContaining.setObjectName("labelPackagesContaining")
self.horizontalLayout_6.addWidget(self.labelPackagesContaining)
@@ -444,25 +508,25 @@ class Ui_PackageList(object):
self.comboPackageType.addItem("")
self.comboPackageType.addItem("")
self.comboPackageType.addItem("")
self.comboPackageType.setObjectName(u"comboPackageType")
self.comboPackageType.setObjectName("comboPackageType")
self.horizontalLayout_6.addWidget(self.comboPackageType)
self.lineEditFilter = QLineEdit(Form)
self.lineEditFilter.setObjectName(u"lineEditFilter")
self.lineEditFilter.setObjectName("lineEditFilter")
self.lineEditFilter.setClearButtonEnabled(True)
self.horizontalLayout_6.addWidget(self.lineEditFilter)
self.labelFilterValidity = QLabel(Form)
self.labelFilterValidity.setObjectName(u"labelFilterValidity")
self.labelFilterValidity.setObjectName("labelFilterValidity")
self.horizontalLayout_6.addWidget(self.labelFilterValidity)
self.verticalLayout.addLayout(self.horizontalLayout_6)
self.listPackages = QListView(Form)
self.listPackages.setObjectName(u"listPackages")
self.listPackages.setObjectName("listPackages")
self.listPackages.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.listPackages.setProperty("showDropIndicator", False)
self.listPackages.setSelectionMode(QAbstractItemView.NoSelection)
@@ -480,12 +544,27 @@ class Ui_PackageList(object):
QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
self.labelPackagesContaining.setText(QCoreApplication.translate("AddonsInstaller", u"Show packages containing:", None))
self.comboPackageType.setItemText(0, QCoreApplication.translate("AddonsInstaller", u"All", None))
self.comboPackageType.setItemText(1, QCoreApplication.translate("AddonsInstaller", u"Workbenches", None))
self.comboPackageType.setItemText(2, QCoreApplication.translate("AddonsInstaller", u"Macros", None))
self.comboPackageType.setItemText(3, QCoreApplication.translate("AddonsInstaller", u"Preference Packs", None))
self.lineEditFilter.setPlaceholderText(QCoreApplication.translate("AddonsInstaller", u"Filter", None))
self.labelFilterValidity.setText(QCoreApplication.translate("AddonsInstaller", u"OK", None))
self.labelPackagesContaining.setText(
QCoreApplication.translate(
"AddonsInstaller", "Show packages containing:", None
)
)
self.comboPackageType.setItemText(
0, QCoreApplication.translate("AddonsInstaller", "All", None)
)
self.comboPackageType.setItemText(
1, QCoreApplication.translate("AddonsInstaller", "Workbenches", None)
)
self.comboPackageType.setItemText(
2, QCoreApplication.translate("AddonsInstaller", "Macros", None)
)
self.comboPackageType.setItemText(
3, QCoreApplication.translate("AddonsInstaller", "Preference Packs", None)
)
self.lineEditFilter.setPlaceholderText(
QCoreApplication.translate("AddonsInstaller", "Filter", None)
)
self.labelFilterValidity.setText(
QCoreApplication.translate("AddonsInstaller", "OK", None)
)