Merge pull request #5312 from chennes/addonManagerCacheMacros

Addon Manager: download and display Macro metadata
This commit is contained in:
Chris Hennes
2022-01-01 15:44:19 -06:00
committed by GitHub
10 changed files with 681 additions and 190 deletions

View File

@@ -29,8 +29,7 @@ import shutil
import stat
import tempfile
from datetime import date, timedelta
from typing import Dict, Union
from enum import Enum
from typing import Dict
from PySide2 import QtGui, QtCore, QtWidgets
import FreeCADGui
@@ -82,6 +81,7 @@ class CommandAddonManager:
"macro_worker",
"install_worker",
"update_metadata_cache_worker",
"load_macro_metadata_worker",
"update_all_worker",
"update_check_single_worker",
]
@@ -109,32 +109,66 @@ class CommandAddonManager:
def Activated(self) -> None:
# display first use dialog if needed
readWarningParameter = FreeCAD.ParamGet(
"User parameter:BaseApp/Preferences/Addons"
)
readWarning = readWarningParameter.GetBool("readWarning", False)
newReadWarningParameter = FreeCAD.ParamGet(
"User parameter:Plugins/addonsRepository"
)
readWarning |= newReadWarningParameter.GetBool("readWarning", False)
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
readWarning = pref.GetBool("readWarning2022", 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
):
readWarningParameter.SetBool("readWarning", True)
warning_dialog = FreeCADGui.PySideUic.loadUi(
os.path.join(os.path.dirname(__file__), "first_run.ui")
)
autocheck = pref.GetBool("AutoCheck", False)
download_macros = pref.GetBool("DownloadMacros", False)
proxy_string = pref.GetString("ProxyUrl", "")
if pref.GetBool("NoProxyCheck", True):
proxy_option = 0
elif pref.GetBool("SystemProxyCheck", False):
proxy_option = 1
elif pref.GetBool("UserProxyCheck", False):
proxy_option = 2
def toggle_proxy_list(option: int):
if option == 2:
warning_dialog.lineEditProxy.show()
else:
warning_dialog.lineEditProxy.hide()
warning_dialog.checkBoxAutoCheck.setChecked(autocheck)
warning_dialog.checkBoxDownloadMacroMetadata.setChecked(download_macros)
warning_dialog.comboBoxProxy.setCurrentIndex(proxy_option)
toggle_proxy_list(proxy_option)
if proxy_option == 2:
warning_dialog.lineEditProxy.setText(proxy_string)
warning_dialog.comboBoxProxy.currentIndexChanged.connect(toggle_proxy_list)
warning_dialog.labelWarning.setStyleSheet(
f"color:{utils.warning_color_string()};font-weight:bold;"
)
if warning_dialog.exec() == QtWidgets.QDialog.Accepted:
readWarning = True
pref.SetBool("readWarning2022", True)
pref.SetBool("AutoCheck", warning_dialog.checkBoxAutoCheck.isChecked())
pref.SetBool(
"DownloadMacros",
warning_dialog.checkBoxDownloadMacroMetadata.isChecked(),
)
if warning_dialog.checkBoxDownloadMacroMetadata.isChecked():
self.trigger_recache = True
selected_proxy_option = warning_dialog.comboBoxProxy.currentIndex()
if selected_proxy_option == 0:
pref.SetBool("NoProxyCheck", True)
pref.SetBool("SystemProxyCheck", False)
pref.SetBool("UserProxyCheck", False)
elif selected_proxy_option == 1:
pref.SetBool("NoProxyCheck", False)
pref.SetBool("SystemProxyCheck", True)
pref.SetBool("UserProxyCheck", False)
else:
pref.SetBool("NoProxyCheck", False)
pref.SetBool("SystemProxyCheck", False)
pref.SetBool("UserProxyCheck", True)
pref.SetString("ProxyUrl", warning_dialog.lineEditProxy.text())
if readWarning:
self.launch()
@@ -168,6 +202,8 @@ class CommandAddonManager:
# 0: Update every launch
# >0: Update every n days
self.update_cache = False
if hasattr(self, "trigger_recache") and self.trigger_recache:
self.update_cache = True
update_frequency = pref.GetInt("UpdateFrequencyComboEntry", 0)
if update_frequency == 0:
days_between_updates = -1
@@ -241,7 +277,6 @@ class CommandAddonManager:
)
self.dialog.buttonClose.clicked.connect(self.dialog.reject)
self.dialog.buttonUpdateCache.clicked.connect(self.on_buttonUpdateCache_clicked)
self.dialog.buttonShowDetails.clicked.connect(self.toggle_details)
self.dialog.buttonPauseUpdate.clicked.connect(self.stop_update)
self.packageList.itemSelected.connect(self.table_row_activated)
self.packageList.setEnabled(False)
@@ -262,7 +297,7 @@ class CommandAddonManager:
)
# set info for the progress bar:
self.dialog.progressBar.setMaximum(100)
self.dialog.progressBar.setMaximum(1000)
# begin populating the table in a set of sub-threads
self.startup()
@@ -385,6 +420,9 @@ class CommandAddonManager:
self.update_metadata_cache,
self.check_updates,
]
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
if pref.GetBool("DownloadMacros", False):
self.startup_sequence.append(self.load_macro_metadata)
self.current_progress_region = 0
self.number_of_progress_regions = len(self.startup_sequence)
self.do_next_startup_phase()
@@ -410,7 +448,6 @@ class CommandAddonManager:
def populate_packages_table(self) -> None:
self.item_model.clear()
self.current_progress_region += 1
use_cache = not self.update_cache
if use_cache:
@@ -456,7 +493,7 @@ class CommandAddonManager:
if hasattr(self, "package_cache"):
package_cache_path = self.get_cache_file_name("package_cache.json")
with open(package_cache_path, "w") as f:
f.write(json.dumps(self.package_cache))
f.write(json.dumps(self.package_cache, indent=" "))
def activate_table_widgets(self) -> None:
self.packageList.setEnabled(True)
@@ -464,42 +501,44 @@ class CommandAddonManager:
self.do_next_startup_phase()
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")
):
macro_cache_file = self.get_cache_file_name("macro_cache.json")
cache_is_bad = True
if os.path.isfile(macro_cache_file):
size = os.path.getsize(macro_cache_file)
if size > 1000: # Make sure there is actually data in there
cache_is_bad = False
if self.update_cache or cache_is_bad:
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.finished.connect(
self.do_next_startup_phase
) # Link to step 3
self.macro_worker.finished.connect(self.do_next_startup_phase)
self.macro_worker.start()
else:
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.finished.connect(
self.do_next_startup_phase
) # Link to step 3
self.macro_worker.finished.connect(self.do_next_startup_phase)
self.macro_worker.start()
def cache_macro(self, macro: AddonManagerRepo):
def cache_macro(self, repo: AddonManagerRepo):
if not hasattr(self, "macro_cache"):
self.macro_cache = []
if macro.macro is not None:
self.macro_cache.append(macro.macro.to_cache())
if repo.macro is not None:
self.macro_cache.append(repo.macro.to_cache())
else:
FreeCAD.Console.PrintError(
f"Addon Manager: Internal error, cache_macro called on non-macro {repo.name}\n"
)
def write_macro_cache(self):
macro_cache_path = self.get_cache_file_name("macro_cache.json")
with open(macro_cache_path, "w") as f:
f.write(json.dumps(self.macro_cache))
f.write(json.dumps(self.macro_cache, indent=" "))
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
@@ -528,14 +567,29 @@ class CommandAddonManager:
"""Called when the named package has either new metadata or a new icon (or both)"""
with self.lock:
self.cache_package(repo)
repo.icon = self.get_icon(repo, update=True)
self.item_model.reload_item(repo)
def load_macro_metadata(self) -> None:
if self.update_cache:
self.load_macro_metadata_worker = CacheMacroCode(self.item_model.repos)
self.load_macro_metadata_worker.status_message.connect(
self.show_information
)
self.load_macro_metadata_worker.update_macro.connect(
self.on_package_updated
)
self.load_macro_metadata_worker.progress_made.connect(
self.update_progress_bar
)
self.load_macro_metadata_worker.finished.connect(self.do_next_startup_phase)
self.load_macro_metadata_worker.start()
else:
self.do_next_startup_phase()
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)
if not autocheck:
@@ -645,6 +699,7 @@ class CommandAddonManager:
"""shows generic text in the information pane (which might be collapsed)"""
self.dialog.labelStatusInfo.setText(message)
self.dialog.labelStatusInfo.repaint()
def show_workbench(self, repo: AddonManagerRepo) -> None:
self.packageList.hide()
@@ -700,6 +755,8 @@ class CommandAddonManager:
real_install_succeeded, errors = macro.install(self.macro_repo_dir)
if not real_install_succeeded:
failed = True
else:
utils.update_macro_installation_details(repo)
if not failed:
message = translate(
@@ -851,36 +908,31 @@ class CommandAddonManager:
self.dialog.labelStatusInfo.hide()
self.dialog.progressBar.hide()
self.dialog.buttonPauseUpdate.hide()
self.dialog.buttonShowDetails.hide()
self.dialog.labelUpdateInProgress.hide()
self.packageList.ui.lineEditFilter.setFocus()
def show_progress_widgets(self) -> None:
if self.dialog.progressBar.isHidden():
self.dialog.progressBar.show()
self.dialog.buttonPauseUpdate.show()
self.dialog.buttonShowDetails.show()
self.dialog.labelStatusInfo.hide()
self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow)
self.dialog.labelUpdateInProgress.show()
self.dialog.labelStatusInfo.show()
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
self.dialog.progressBar.setValue(value)
if current_value < 0:
FreeCAD.Console.PrintWarning(
f"Addon Manager: Internal error, current progress value is negative in region {self.current_progress_region}"
)
def toggle_details(self) -> None:
if self.dialog.labelStatusInfo.isHidden():
self.dialog.labelStatusInfo.show()
self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.DownArrow)
else:
self.dialog.labelStatusInfo.hide()
self.dialog.buttonShowDetails.setArrowType(QtCore.Qt.RightArrow)
self.show_progress_widgets()
region_size = 100.0 / self.number_of_progress_regions
completed_region_portion = (self.current_progress_region - 1) * region_size
current_region_portion = (float(current_value) / float(max_value)) * region_size
value = completed_region_portion + current_region_portion
self.dialog.progressBar.setValue(
value * 10
) # Out of 1000 segments, so it moves sort of smoothly
self.dialog.progressBar.repaint()
def stop_update(self) -> None:
self.cleanup_workers()

View File

@@ -28,26 +28,6 @@
<layout class="QVBoxLayout" name="layoutUpdateInProgress">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QToolButton" name="buttonShowDetails">
<property name="toolTip">
<string>Show details</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::RightArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelUpdateInProgress">
<property name="text">
<string>Loading...</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="sizePolicy">

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>390</width>
<height>628</height>
<width>388</width>
<height>621</height>
</rect>
</property>
<property name="windowTitle">
@@ -35,6 +35,19 @@ installed addons will be checked for available updates
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="guiprefcheckboxdownloadmacros">
<property name="text">
<string>Download Macro metadata (approximately 10MB)</string>
</property>
<property name="prefEntry" stdset="0">
<string>DownloadMacros</string>
</property>
<property name="prefPath" stdset="0">
<string>Addons</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>

View File

@@ -13,6 +13,7 @@ SET(AddonManager_SRCS
addonmanager_workers.py
AddonManager.ui
AddonManagerOptions.ui
first_run.ui
compact_view.py
expanded_view.py
package_list.py

View File

@@ -23,10 +23,11 @@
import os
import re
import sys
import io
import codecs
import shutil
from typing import Dict, Union, List
import time
from typing import Dict, Tuple, List, Union
import FreeCAD
@@ -56,10 +57,13 @@ class Macro(object):
self.on_wiki = False
self.on_git = False
self.desc = ""
self.comment = ""
self.code = ""
self.url = ""
self.version = ""
self.date = ""
self.src_filename = ""
self.author = ""
self.other_files = []
self.parsed = False
@@ -93,37 +97,71 @@ class Macro(object):
os.path.join(FreeCAD.getUserMacroDir(True), "Macro_" + self.filename)
)
def fill_details_from_file(self, filename):
with open(filename) as f:
# Number of parsed fields of metadata. For now, __Comment__,
# __Web__, __Version__, __Files__.
number_of_required_fields = 4
re_desc = re.compile(r"^__Comment__\s*=\s*(['\"])(.*)\1")
re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1")
re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1")
re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1")
for line in f.readlines():
match = re.match(re_desc, line)
if match:
self.desc = match.group(2)
number_of_required_fields -= 1
match = re.match(re_url, line)
if match:
self.url = match.group(2)
number_of_required_fields -= 1
match = re.match(re_version, line)
if match:
self.version = match.group(2)
number_of_required_fields -= 1
match = re.match(re_files, line)
if match:
self.other_files = [of.strip() for of in match.group(2).split(",")]
number_of_required_fields -= 1
if number_of_required_fields <= 0:
break
f.seek(0)
def fill_details_from_file(self, filename: str) -> None:
with open(filename, errors="replace") as f:
self.code = f.read()
self.parsed = True
self.fill_details_from_code(self.code)
def fill_details_from_code(self, code: str) -> None:
# Number of parsed fields of metadata. Overrides anything set previously (the code is considered authoritative).
# For now:
# __Comment__
# __Web__
# __Version__
# __Files__
# __Author__
# __Date__
max_lines_to_search = 50
line_counter = 0
number_of_fields = 5
ic = re.IGNORECASE # Shorten the line for Black
re_comment = re.compile(r"^__Comment__\s*=\s*(['\"])(.*)\1", flags=ic)
re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1", flags=ic)
re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1", flags=ic)
re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1", flags=ic)
re_author = re.compile(r"^__Author__\s*=\s*(['\"])(.*)\1", flags=ic)
re_date = re.compile(r"^__Date__\s*=\s*(['\"])(.*)\1", flags=ic)
f = io.StringIO(code)
while f and line_counter < max_lines_to_search:
line = f.readline()
line_counter += 1
if not line.startswith(
"__"
): # Speed things up a bit... this comparison is very cheap
continue
match = re.match(re_comment, line)
if match:
self.comment = match.group(2)
self.comment = re.sub("<.*?>", "", self.comment) # Strip any HTML tags
number_of_fields -= 1
match = re.match(re_author, line)
if match:
self.author = match.group(2)
number_of_fields -= 1
match = re.match(re_url, line)
if match:
self.url = match.group(2)
number_of_fields -= 1
match = re.match(re_version, line)
if match:
self.version = match.group(2)
number_of_fields -= 1
match = re.match(re_date, line)
if match:
self.date = match.group(2)
number_of_fields -= 1
match = re.match(re_files, line)
if match:
self.other_files = [of.strip() for of in match.group(2).split(",")]
number_of_fields -= 1
if number_of_fields <= 0:
break
# Truncate long comments to speed up searches, and clean up display
if len(self.comment) > 512:
self.comment = self.comment[:511] + ""
self.parsed = True
def fill_details_from_wiki(self, url):
code = ""
@@ -157,13 +195,9 @@ class Macro(object):
+ "\n"
)
return
# code = u2.read()
# github is slow to respond... We need to use this trick below
response = ""
block = 8192
# expected = int(u2.headers["content-length"])
while True:
# print("expected:", expected, "got:", len(response))
data = u2.read(block)
if not data:
break
@@ -200,12 +234,14 @@ class Macro(object):
FreeCAD.Console.PrintWarning(
translate(
"AddonsInstaller",
"Unable to retrieve a description for this macro.",
f"Unable to retrieve a description from the wiki for macro {self.name}",
)
+ "\n"
)
desc = "No description available"
self.desc = desc
self.comment, _, _ = desc.partition("<br") # Up to the first line break
self.comment = re.sub("<.*?>", "", self.comment) # Strip any tags
self.url = url
if isinstance(code, list):
flat_code = ""
@@ -213,9 +249,20 @@ class Macro(object):
flat_code += chunk
code = flat_code
self.code = code
self.parsed = True
self.fill_details_from_code(self.code)
if not self.author:
self.author = self.parse_desc("Author: ")
if not self.date:
self.date = self.parse_desc("Last modified: ")
def install(self, macro_dir: str) -> (bool, List[str]):
def parse_desc(self, line_start: str) -> Union[str, None]:
components = self.desc.split(">")
for component in components:
if component.startswith(line_start):
end = component.find("<")
return component[len(line_start) : end]
def install(self, macro_dir: str) -> Tuple[bool, List[str]]:
"""Install a macro and all its related files
Returns True if the macro was installed correctly.

View File

@@ -21,21 +21,19 @@
# * *
# ***************************************************************************
import codecs
import os
import re
import shutil
import sys
import ctypes
import tempfile
import ssl
from typing import Union
import urllib
from urllib.request import Request
from urllib.error import URLError
from urllib.parse import urlparse
from http.client import HTTPResponse
from PySide2 import QtGui, QtCore, QtWidgets
from PySide2 import QtCore, QtWidgets
import FreeCAD
import FreeCADGui
@@ -94,7 +92,7 @@ def symlink(source, link_name):
raise ctypes.WinError()
def urlopen(url: str):
def urlopen(url: str) -> Union[None, HTTPResponse]:
"""Opens an url with urllib and streams it to a temp file"""
timeout = 5
@@ -125,9 +123,7 @@ def urlopen(url: str):
u = urllib.request.urlopen(req, timeout=timeout)
except URLError as e:
FreeCAD.Console.PrintError(
translate("AddonsInstaller", f"Error loading {url}") + ":\n {e.reason}\n"
)
FreeCAD.Console.PrintLog(f"Error loading {url}:\n {e.reason}\n")
return None
except Exception:
return None
@@ -298,4 +294,76 @@ def fix_relative_links(text, base_url):
return new_text
def warning_color_string() -> str:
"""A shade of red, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
warningColorString = "rgb(255,0,0)"
if hasattr(QtWidgets.QApplication.instance(), "styleSheet"):
# Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info
if "dark" in QtWidgets.QApplication.instance().styleSheet().lower():
warningColorString = "rgb(255,105,97)"
else:
warningColorString = "rgb(215,0,21)"
return warningColorString
def bright_color_string() -> str:
"""A shade of green, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
brightColorString = "rgb(0,255,0)"
if hasattr(QtWidgets.QApplication.instance(), "styleSheet"):
# Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info
if "dark" in QtWidgets.QApplication.instance().styleSheet().lower():
brightColorString = "rgb(48,219,91)"
else:
brightColorString = "rgb(36,138,61)"
return brightColorString
def attention_color_string() -> str:
"""A shade of orange, adapted to darkmode if possible. Targets a minimum 7:1 contrast ratio."""
attentionColorString = "rgb(255,149,0)"
if hasattr(QtWidgets.QApplication.instance(), "styleSheet"):
# Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info
if "dark" in QtWidgets.QApplication.instance().styleSheet().lower():
attentionColorString = "rgb(255,179,64)"
else:
attentionColorString = "rgb(255,149,0)"
return attentionColorString
def get_macro_version_from_file(filename: str) -> str:
re_version = re.compile(r"^__Version__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE)
with open(filename, "r", errors="ignore") as f:
line_counter = 0
max_lines_to_scan = 50
while line_counter < max_lines_to_scan:
line_counter += 1
line = f.readline()
if line.startswith("__"):
match = re.match(re_version, line)
if match:
return match.group(2)
return ""
def update_macro_installation_details(repo) -> None:
if repo is None or not hasattr(repo, "macro") or repo.macro is None:
FreeCAD.Console.PrintLog(f"Requested macro details for non-macro object\n")
return
test_file_one = os.path.join(FreeCAD.getUserMacroDir(True), repo.macro.filename)
test_file_two = os.path.join(
FreeCAD.getUserMacroDir(True), "Macro_" + repo.macro.filename
)
if os.path.exists(test_file_one):
repo.updated_timestamp = os.path.getmtime(test_file_one)
repo.installed_version = get_macro_version_from_file(test_file_one)
elif os.path.exists(test_file_two):
repo.updated_timestamp = os.path.getmtime(test_file_two)
repo.installed_version = get_macro_version_from_file(test_file_two)
else:
return
# @}

View File

@@ -31,10 +31,11 @@ import hashlib
import threading
import queue
import io
import time
from datetime import datetime
from typing import Union, List
from PySide2 import QtCore, QtGui, QtNetwork
from PySide2 import QtCore, QtNetwork
import FreeCAD
@@ -285,7 +286,6 @@ class LoadPackagesFromCacheWorker(QtCore.QThread):
class LoadMacrosFromCacheWorker(QtCore.QThread):
add_macro_signal = QtCore.Signal(object)
done = QtCore.Signal()
def __init__(self, cache_file: str):
QtCore.QThread.__init__(self)
@@ -299,8 +299,9 @@ class LoadMacrosFromCacheWorker(QtCore.QThread):
if QtCore.QThread.currentThread().isInterruptionRequested():
return
new_macro = Macro.from_cache(item)
self.add_macro_signal.emit(AddonManagerRepo.from_macro(new_macro))
self.done.emit()
repo = AddonManagerRepo.from_macro(new_macro)
utils.update_macro_installation_details(repo)
self.add_macro_signal.emit(repo)
class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
@@ -589,6 +590,7 @@ class FillMacroListWorker(QtCore.QThread):
macro.src_filename = os.path.join(dirpath, filename)
repo = AddonManagerRepo.from_macro(macro)
repo.url = "https://github.com/FreeCAD/FreeCAD-macros.git"
utils.update_macro_installation_details(repo)
self.add_macro_signal.emit(repo)
def retrieve_macros_from_wiki(self):
@@ -634,9 +636,137 @@ class FillMacroListWorker(QtCore.QThread):
macro.on_wiki = True
repo = AddonManagerRepo.from_macro(macro)
repo.url = "https://wiki.freecad.org/Macros_recipes"
utils.update_macro_installation_details(repo)
self.add_macro_signal.emit(repo)
class CacheMacroCode(QtCore.QThread):
"""Download and cache the macro code, and parse its internal metadata"""
status_message = QtCore.Signal(str)
update_macro = QtCore.Signal(AddonManagerRepo)
progress_made = QtCore.Signal(int, int)
def __init__(self, repos: List[AddonManagerRepo]) -> None:
QtCore.QThread.__init__(self)
self.repos = repos
self.workers = []
self.terminators = []
self.lock = threading.Lock()
self.failed = []
self.counter = 0
def run(self):
self.status_message.emit(translate("AddonsInstaller", "Caching macro code..."))
self.repo_queue = queue.Queue()
current_thread = QtCore.QThread.currentThread()
num_macros = 0
for repo in self.repos:
if repo.macro is not None:
self.repo_queue.put(repo)
num_macros += 1
# Emulate QNetworkAccessManager and spool up six connections:
for _ in range(6):
self.update_and_advance(None)
while True:
if current_thread.isInterruptionRequested():
for worker in self.workers:
worker.requestInterruption()
worker.wait(100)
if not worker.isFinished():
# Kill it
worker.terminate()
return
# Ensure our signals propagate out by running an internal thread-local event loop
QtCore.QCoreApplication.processEvents()
with self.lock:
if self.counter >= num_macros:
break
time.sleep(0.1)
# Make sure all of our child threads have fully exited:
for i, worker in enumerate(self.workers):
worker.wait(50)
if not worker.isFinished():
FreeCAD.Console.PrintError(
f"Addon Manager: a worker process failed to complete while fetching {worker.macro.name}\n"
)
worker.terminate()
self.repo_queue.join()
for terminator in self.terminators:
if terminator and terminator.isActive():
terminator.stop()
if len(self.failed) > 0:
num_failed = len(self.failed)
FreeCAD.Console.PrintWarning(
translate(
"AddonsInstaller",
f"Out of {num_macros} macros, {num_failed} timed out while processing",
)
)
def update_and_advance(self, repo: AddonManagerRepo) -> None:
if repo is not None:
if repo.macro.name not in self.failed:
self.update_macro.emit(repo)
self.repo_queue.task_done()
with self.lock:
self.counter += 1
if QtCore.QThread.currentThread().isInterruptionRequested():
return
self.progress_made.emit(
len(self.repos) - self.repo_queue.qsize(), len(self.repos)
)
try:
next_repo = self.repo_queue.get_nowait()
worker = GetMacroDetailsWorker(next_repo)
worker.finished.connect(lambda: self.update_and_advance(next_repo))
with self.lock:
self.workers.append(worker)
self.terminators.append(
QtCore.QTimer.singleShot(10000, lambda: self.terminate(worker))
)
self.status_message.emit(
translate(
"AddonsInstaller",
f"Getting metadata from macro {next_repo.macro.name}",
)
)
worker.start()
except queue.Empty:
pass
def terminate(self, worker) -> None:
if not worker.isFinished():
macro_name = worker.macro.name
FreeCAD.Console.PrintWarning(
translate(
"AddonsInstaller",
f"Timeout while fetching metadata for macro {macro_name}",
)
+ "\n"
)
worker.requestInterruption()
worker.wait(100)
if worker.isRunning():
worker.terminate()
worker.wait(50)
if worker.isRunning():
FreeCAD.Console.PrintError(
f"Failed to kill process for macro {macro_name}!\n"
)
with self.lock:
self.failed.append(macro_name)
class ShowWorker(QtCore.QThread):
"""This worker retrieves info of a given workbench"""
@@ -927,17 +1057,8 @@ class GetMacroDetailsWorker(QtCore.QThread):
mac = mac.replace("+", "%2B")
url = "https://wiki.freecad.org/Macro_" + mac
self.macro.fill_details_from_wiki(url)
if self.macro.is_installed():
already_installed_msg = (
'<strong style="background: #00B629;">'
+ translate("AddonsInstaller", "This macro is already installed.")
+ "</strong><br>"
)
else:
already_installed_msg = ""
message = (
already_installed_msg
+ "<h1>"
"<h1>"
+ self.macro.name
+ "</h1>"
+ self.macro.desc
@@ -1545,11 +1666,15 @@ class UpdateAllWorker(QtCore.QThread):
self.done.emit()
def on_success(self, repo: AddonManagerRepo) -> None:
self.progress_made.emit(self.repo_queue.qsize(), len(self.repos))
self.progress_made.emit(
len(self.repos) - self.repo_queue.qsize(), len(self.repos)
)
self.success.emit(repo)
def on_failure(self, repo: AddonManagerRepo) -> None:
self.progress_made.emit(self.repo_queue.qsize(), len(self.repos))
self.progress_made.emit(
len(self.repos) - self.repo_queue.qsize(), len(self.repos)
)
self.failure.emit(repo)
@@ -1589,6 +1714,7 @@ class UpdateSingleWorker(QtCore.QThread):
install_succeeded, errors = repo.macro.install(
FreeCAD.getUserMacroDir(True)
)
utils.update_macro_installation_details(repo)
if install_succeeded:
self.success.emit(repo)

View File

@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>237</height>
</rect>
</property>
<property name="windowTitle">
<string>Welcome to the Addon Manager</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="labelWarning">
<property name="text">
<string>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!</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Download Settings</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxAutoCheck">
<property name="text">
<string>Automatically check installed Addons for updates</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxDownloadMacroMetadata">
<property name="text">
<string>Download Macro metadata (approximately 10MB)</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="comboBoxProxy">
<item>
<property name="text">
<string>No proxy</string>
</property>
</item>
<item>
<property name="text">
<string>System proxy</string>
</property>
</item>
<item>
<property name="text">
<string>User-defined proxy:</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEditProxy"/>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>These and other settings are available in the FreeCAD Preferences window.</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -31,12 +31,14 @@ from datetime import date, timedelta
import FreeCAD
from addonmanager_utilities import translate # this needs to be as is for pylupdate
import addonmanager_utilities as utils
from addonmanager_workers import ShowWorker, GetMacroDetailsWorker
from AddonManagerRepo import AddonManagerRepo
import inspect
translate = FreeCAD.Qt.translate
class PackageDetails(QWidget):
@@ -93,31 +95,34 @@ class PackageDetails(QWidget):
self.ui.buttonExecute.hide()
if repo.update_status != AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
installed_version_string = ""
if repo.installed_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)"
)
+ " "
)
version = repo.installed_version
date = ""
installed_version_string = "<h3>"
if repo.updated_timestamp:
installed_version_string += (
" " + translate("AddonsInstaller", "installed on") + " "
)
installed_version_string += (
date = (
QDateTime.fromTime_t(repo.updated_timestamp)
.date()
.toString(Qt.SystemLocaleShortDate)
)
installed_version_string += ". "
if version and date:
installed_version_string += (
translate(
"AddonsInstaller", f"Version {version} installed on {date}"
)
+ ". "
)
elif version:
installed_version_string += (
translate("AddonsInstaller", f"Version {version} installed") + ". "
)
elif date:
installed_version_string += (
translate("AddonsInstaller", f"Installed on {date}") + ". "
)
else:
installed_version_string += (
translate("AddonsInstaller", "installed") + ". "
translate("AddonsInstaller", "Installed") + ". "
)
if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
@@ -129,12 +134,20 @@ class PackageDetails(QWidget):
)
installed_version_string += repo.metadata.Version
installed_version_string += ".</b>"
elif repo.macro and repo.macro.version:
installed_version_string += (
"<b>"
+ translate("AddonsInstaller", "Update available to version")
+ " "
)
installed_version_string += repo.macro.version
installed_version_string += ".</b>"
else:
installed_version_string += (
"<b>"
+ translate(
"AddonsInstaller",
"Update available to unknown version (no package.xml file found)",
"An update is available",
)
+ ".</b>"
)
@@ -166,19 +179,32 @@ class PackageDetails(QWidget):
+ "."
)
basedir = FreeCAD.getUserAppDataDir()
moddir = os.path.join(basedir, "Mod", repo.name)
installed_version_string += (
"<br/>"
+ translate("AddonsInstaller", "Installation location")
+ ": "
+ moddir
installed_version_string += "</h3>"
self.ui.labelPackageDetails.setText(installed_version_string)
if repo.update_status == AddonManagerRepo.UpdateStatus.UPDATE_AVAILABLE:
self.ui.labelPackageDetails.setStyleSheet(
"color:" + utils.attention_color_string()
)
else:
self.ui.labelPackageDetails.setStyleSheet(
"color:" + utils.bright_color_string()
)
self.ui.labelPackageDetails.show()
if repo.macro is not None:
moddir = FreeCAD.getUserMacroDir(True)
else:
basedir = FreeCAD.getUserAppDataDir()
moddir = os.path.join(basedir, "Mod", repo.name)
installationLocationString = (
translate("AddonsInstaller", "Installation location") + ": " + moddir
)
self.ui.labelPackageDetails.setText(installed_version_string)
self.ui.labelPackageDetails.show()
self.ui.labelInstallationLocation.setText(installationLocationString)
self.ui.labelInstallationLocation.show()
else:
self.ui.labelPackageDetails.hide()
self.ui.labelInstallationLocation.hide()
if repo.update_status == AddonManagerRepo.UpdateStatus.NOT_INSTALLED:
self.ui.buttonInstall.show()
@@ -206,14 +232,6 @@ class PackageDetails(QWidget):
self.ui.buttonUpdate.hide()
self.ui.buttonCheckForUpdate.hide()
warningColorString = "rgb(255,0,0)"
if hasattr(QApplication.instance(),"styleSheet"):
# Qt 5.9 doesn't give a QApplication instance, so can't give the stylesheet info
if "dark" in QApplication.instance().styleSheet().lower():
warningColorString = "rgb(255,50,50)"
else:
warningColorString = "rgb(200,0,0)"
if repo.obsolete:
self.ui.labelWarningInfo.show()
self.ui.labelWarningInfo.setText(
@@ -221,7 +239,9 @@ class PackageDetails(QWidget):
+ translate("AddonsInstaller", "WARNING: This addon is obsolete")
+ "</h1>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + warningColorString)
self.ui.labelWarningInfo.setStyleSheet(
"color:" + utils.warning_color_string()
)
elif repo.python2:
self.ui.labelWarningInfo.show()
self.ui.labelWarningInfo.setText(
@@ -229,7 +249,9 @@ class PackageDetails(QWidget):
+ translate("AddonsInstaller", "WARNING: This addon is Python 2 Only")
+ "</h1>"
)
self.ui.labelWarningInfo.setStyleSheet("color:" + warningColorString)
self.ui.labelWarningInfo.setStyleSheet(
"color:" + utils.warning_color_string()
)
else:
self.ui.labelWarningInfo.hide()
@@ -416,6 +438,12 @@ class Ui_PackageDetails(object):
self.verticalLayout_2.addWidget(self.labelPackageDetails)
self.labelInstallationLocation = QLabel(PackageDetails)
self.labelInstallationLocation.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.labelInstallationLocation.hide()
self.verticalLayout_2.addWidget(self.labelInstallationLocation)
self.labelWarningInfo = QLabel(PackageDetails)
self.labelWarningInfo.hide()

View File

@@ -358,6 +358,32 @@ class PackageListItemDelegate(QStyledItemDelegate):
f"\n{maintainer['name']} <{maintainer['email']}>"
)
self.widget.ui.labelMaintainer.setText(maintainers_string)
elif repo.macro and repo.macro.parsed:
self.widget.ui.labelDescription.setText(repo.macro.comment)
self.widget.ui.labelVersion.setText(repo.macro.version)
if repo.macro.date:
if repo.macro.version:
new_label = (
"v"
+ repo.macro.version
+ ", "
+ translate("AddonsInstaller", "updated")
+ " "
+ repo.macro.date
)
else:
new_label = (
translate("AddonsInstaller", "Updated") + " " + repo.macro.date
)
self.widget.ui.labelVersion.setText(new_label)
if self.displayStyle == ListDisplayStyle.EXPANDED:
if repo.macro.author:
caption = translate("AddonsInstaller", "Author")
self.widget.ui.labelMaintainer.setText(
caption + ": " + repo.macro.author
)
else:
self.widget.ui.labelMaintainer.setText("")
else:
self.widget.ui.labelDescription.setText("")
self.widget.ui.labelVersion.setText("")
@@ -533,6 +559,12 @@ class PackageListFilter(QSortFilterProxyModel):
return True
if re.match(desc).hasMatch():
return True
if (
data.macro
and data.macro.comment
and re.match(data.macro.comment).hasMatch()
):
return True
return False
else:
return False