Addon Manager: Black reformatting
This commit is contained in:
@@ -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.")
|
||||
)
|
||||
|
||||
|
||||
# @}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
import AddonManager
|
||||
|
||||
FreeCADGui.addLanguagePath(":/translations")
|
||||
FreeCADGui.addCommand('Std_AddonMgr', AddonManager.CommandAddonManager())
|
||||
FreeCADGui.addCommand("Std_AddonMgr", AddonManager.CommandAddonManager())
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# @}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user