From e450e50bb276b1d52d39b35241e8680f16e4596f Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 15:10:57 -0600 Subject: [PATCH 01/16] Addon Manager: Implement simple macro metadata cache --- src/Mod/AddonManager/AddonManager.py | 43 ++++-- src/Mod/AddonManager/addonmanager_macro.py | 98 +++++++------ .../AddonManager/addonmanager_utilities.py | 4 +- src/Mod/AddonManager/addonmanager_workers.py | 131 +++++++++++++++++- src/Mod/AddonManager/package_list.py | 9 ++ 5 files changed, 228 insertions(+), 57 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index ae8b6e887b..0eb185a123 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -24,6 +24,7 @@ # * * # *************************************************************************** +from inspect import indentsize import os import shutil import stat @@ -78,6 +79,7 @@ class CommandAddonManager: "update_worker", "check_worker", "show_worker", + "cache_macros_worker", "showmacro_worker", "macro_worker", "install_worker", @@ -262,7 +264,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() @@ -383,6 +385,7 @@ class CommandAddonManager: self.activate_table_widgets, self.populate_macros, self.update_metadata_cache, + self.cache_macros, self.check_updates, ] self.current_progress_region = 0 @@ -410,7 +413,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 +458,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,7 +466,6 @@ 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") ): @@ -495,11 +496,10 @@ class CommandAddonManager: 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 @@ -532,10 +532,20 @@ class CommandAddonManager: repo.icon = self.get_icon(repo, update=True) self.item_model.reload_item(repo) + def cache_macros(self) -> None: + if self.update_cache: + self.cache_macros_worker = CacheMacroCode(self.item_model.repos) + self.cache_macros_worker.status_message.connect(self.show_information) + self.cache_macros_worker.update_macro.connect(self.on_package_updated) + self.cache_macros_worker.progress_made.connect(self.update_progress_bar) + self.cache_macros_worker.finished.connect(self.do_next_startup_phase) + self.cache_macros_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 +655,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() @@ -867,12 +878,20 @@ class CommandAddonManager: def update_progress_bar(self, current_value: int, max_value: int) -> None: """Update the progress bar, showing it if it's hidden""" + if current_value < 0: + FreeCAD.Console.PrintWarning( + f"Addon Manager: Internal error, current progress value is negative in region {self.current_progress_region}" + ) + 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) + 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 toggle_details(self) -> None: if self.dialog.labelStatusInfo.isHidden(): diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index d602dabd15..4fbcf96067 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -23,10 +23,10 @@ import os import re -import sys +import io import codecs import shutil -from typing import Dict, Union, List +from typing import Dict, Tuple, List import FreeCAD @@ -56,10 +56,12 @@ class Macro(object): self.on_wiki = False self.on_git = False self.desc = "" + self.comment = "" self.code = "" self.url = "" self.version = "" self.src_filename = "" + self.author = "" self.other_files = [] self.parsed = False @@ -93,37 +95,56 @@ 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__ + number_of_fields = 5 + re_comment = re.compile( + r"^__Comment__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE + ) + re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + re_version = re.compile( + r"^__Version__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE + ) + re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + re_author = re.compile(r"^__Author__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + + f = io.StringIO(code) + while f: + line = f.readline() + match = re.match(re_comment, line) + if match: + self.comment = match.group(2) + 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_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 + self.parsed = True def fill_details_from_wiki(self, url): code = "" @@ -157,16 +178,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 + while data := u2.read(block): if isinstance(data, bytes): data = data.decode("utf-8") response += data @@ -213,9 +227,9 @@ class Macro(object): flat_code += chunk code = flat_code self.code = code - self.parsed = True + self.fill_details_from_code(self.code) - def install(self, macro_dir: str) -> (bool, List[str]): + 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. diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 532c46ec67..c7f516b9ef 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -29,11 +29,13 @@ 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 @@ -94,7 +96,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 diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 1460301b11..8a65498ae3 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -31,6 +31,7 @@ import hashlib import threading import queue import io +import time from datetime import datetime from typing import Union, List @@ -637,6 +638,128 @@ class FillMacroListWorker(QtCore.QThread): 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() + + FreeCAD.Console.PrintMessage( + f"Out of {num_macros} macros, {len(self.failed)} failed" + ) + + 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""" @@ -1545,11 +1668,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) diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index 17a2a78e08..2e7e677dee 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -358,6 +358,15 @@ class PackageListItemDelegate(QStyledItemDelegate): f"\n{maintainer['name']} <{maintainer['email']}>" ) self.widget.ui.labelMaintainer.setText(maintainers_string) + elif repo.macro and repo.macro.parsed: + if repo.macro.comment: + self.widget.ui.labelDescription.setText(repo.macro.comment) + elif repo.macro.desc: + comment, _, _ = repo.desc.partition(" Date: Fri, 31 Dec 2021 16:02:15 -0600 Subject: [PATCH 02/16] Addon Manager: Improve macro scanning performance --- src/Mod/AddonManager/addonmanager_macro.py | 36 ++++++++++++++++------ src/Mod/AddonManager/package_details.py | 2 +- src/Mod/AddonManager/package_list.py | 18 +++++++---- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index 4fbcf96067..ffd619cab7 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -26,6 +26,7 @@ import re import io import codecs import shutil +import time from typing import Dict, Tuple, List import FreeCAD @@ -64,6 +65,7 @@ class Macro(object): self.author = "" self.other_files = [] self.parsed = False + self.parse_time = 0.0 def __eq__(self, other): return self.filename == other.filename @@ -108,23 +110,29 @@ class Macro(object): # __Version__ # __Files__ # __Author__ + start = time.perf_counter() + max_lines_to_search = 50 + line_counter = 0 number_of_fields = 5 - re_comment = re.compile( - r"^__Comment__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE - ) - re_url = re.compile(r"^__Web__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) - re_version = re.compile( - r"^__Version__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE - ) - re_files = re.compile(r"^__Files__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) - re_author = re.compile(r"^__Author__\s*=\s*(['\"])(.*)\1", flags=re.IGNORECASE) + 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) f = io.StringIO(code) - while f: + 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: @@ -144,7 +152,13 @@ class Macro(object): 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 + end = time.perf_counter() + self.parse_time = end - start def fill_details_from_wiki(self, url): code = "" @@ -220,6 +234,8 @@ class Macro(object): ) desc = "No description available" self.desc = desc + self.comment, _, _ = desc.partition("", "", self.comment) # Strip any tags self.url = url if isinstance(code, list): flat_code = "" diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 2e06781a3a..2e4a115def 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -207,7 +207,7 @@ class PackageDetails(QWidget): self.ui.buttonCheckForUpdate.hide() warningColorString = "rgb(255,0,0)" - if hasattr(QApplication.instance(),"styleSheet"): + 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)" diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index 2e7e677dee..f0e30a83a4 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -359,14 +359,14 @@ class PackageListItemDelegate(QStyledItemDelegate): ) self.widget.ui.labelMaintainer.setText(maintainers_string) elif repo.macro and repo.macro.parsed: - if repo.macro.comment: - self.widget.ui.labelDescription.setText(repo.macro.comment) - elif repo.macro.desc: - comment, _, _ = repo.desc.partition(" Date: Fri, 31 Dec 2021 17:11:23 -0600 Subject: [PATCH 03/16] Addon Manager: Fix macro cache loading --- src/Mod/AddonManager/AddonManager.py | 53 ++++++++++++-------- src/Mod/AddonManager/addonmanager_workers.py | 2 - src/Mod/AddonManager/package_list.py | 2 + 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 0eb185a123..e30c0d6924 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -79,11 +79,11 @@ class CommandAddonManager: "update_worker", "check_worker", "show_worker", - "cache_macros_worker", "showmacro_worker", "macro_worker", "install_worker", "update_metadata_cache_worker", + "load_macro_metadata_worker", "update_all_worker", "update_check_single_worker", ] @@ -385,7 +385,7 @@ class CommandAddonManager: self.activate_table_widgets, self.populate_macros, self.update_metadata_cache, - self.cache_macros, + self.load_macro_metadata, self.check_updates, ] self.current_progress_region = 0 @@ -466,32 +466,36 @@ class CommandAddonManager: self.do_next_startup_phase() def populate_macros(self) -> None: - 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") @@ -528,18 +532,23 @@ 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 cache_macros(self) -> None: + def load_macro_metadata(self) -> None: if self.update_cache: - self.cache_macros_worker = CacheMacroCode(self.item_model.repos) - self.cache_macros_worker.status_message.connect(self.show_information) - self.cache_macros_worker.update_macro.connect(self.on_package_updated) - self.cache_macros_worker.progress_made.connect(self.update_progress_bar) - self.cache_macros_worker.finished.connect(self.do_next_startup_phase) - self.cache_macros_worker.start() + 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() diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 8a65498ae3..e57e842641 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -286,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) @@ -301,7 +300,6 @@ class LoadMacrosFromCacheWorker(QtCore.QThread): return new_macro = Macro.from_cache(item) self.add_macro_signal.emit(AddonManagerRepo.from_macro(new_macro)) - self.done.emit() class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index f0e30a83a4..fd3a35cc02 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -367,6 +367,8 @@ class PackageListItemDelegate(QStyledItemDelegate): 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("") From 382eabbb622c2926af6ccf0ca6201910d5a8bed8 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 17:46:13 -0600 Subject: [PATCH 04/16] Addon Manager: Improve wiki page data extraction --- src/Mod/AddonManager/addonmanager_macro.py | 23 ++++++++++++++++---- src/Mod/AddonManager/addonmanager_workers.py | 11 +++++++--- src/Mod/AddonManager/package_list.py | 15 +++++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index ffd619cab7..d2ee65a41a 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -27,7 +27,7 @@ import io import codecs import shutil import time -from typing import Dict, Tuple, List +from typing import Dict, Tuple, List, Union import FreeCAD @@ -61,6 +61,7 @@ class Macro(object): self.code = "" self.url = "" self.version = "" + self.date = "" self.src_filename = "" self.author = "" self.other_files = [] @@ -110,7 +111,7 @@ class Macro(object): # __Version__ # __Files__ # __Author__ - start = time.perf_counter() + # __Date__ max_lines_to_search = 50 line_counter = 0 number_of_fields = 5 @@ -120,6 +121,7 @@ class Macro(object): 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: @@ -146,6 +148,10 @@ class Macro(object): 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(",")] @@ -157,8 +163,6 @@ class Macro(object): if len(self.comment) > 512: self.comment = self.comment[:511] + "…" self.parsed = True - end = time.perf_counter() - self.parse_time = end - start def fill_details_from_wiki(self, url): code = "" @@ -244,6 +248,17 @@ class Macro(object): code = flat_code self.code = code 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 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 diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index e57e842641..00f4762adf 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -697,9 +697,14 @@ class CacheMacroCode(QtCore.QThread): if terminator and terminator.isActive(): terminator.stop() - FreeCAD.Console.PrintMessage( - f"Out of {num_macros} macros, {len(self.failed)} failed" - ) + 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: diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index fd3a35cc02..247f5bac91 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -361,6 +361,21 @@ class PackageListItemDelegate(QStyledItemDelegate): 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") From cd51ec53c033295ef7adb79b7ddc08ce46ef5ffe Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 17:55:23 -0600 Subject: [PATCH 05/16] Addon Manager: Remove extraneous UI elements --- src/Mod/AddonManager/AddonManager.py | 14 +------------- src/Mod/AddonManager/AddonManager.ui | 20 -------------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index e30c0d6924..fac36dc128 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -243,7 +243,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) @@ -871,7 +870,6 @@ 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() @@ -879,9 +877,7 @@ class CommandAddonManager: 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.labelStatusInfo.show() self.dialog.labelUpdateInProgress.show() def update_progress_bar(self, current_value: int, max_value: int) -> None: @@ -902,14 +898,6 @@ class CommandAddonManager: ) # Out of 1000 segments, so it moves sort of smoothly self.dialog.progressBar.repaint() - 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) - def stop_update(self) -> None: self.cleanup_workers() self.hide_progress_widgets() diff --git a/src/Mod/AddonManager/AddonManager.ui b/src/Mod/AddonManager/AddonManager.ui index ac9269af0a..0b1be55d46 100644 --- a/src/Mod/AddonManager/AddonManager.ui +++ b/src/Mod/AddonManager/AddonManager.ui @@ -28,26 +28,6 @@ - - - - Show details - - - ... - - - Qt::RightArrow - - - - - - - Loading... - - - From f5f78670dd6b3a079d4f0201f6a6896b83ba38f8 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 20:02:03 -0600 Subject: [PATCH 06/16] Addon Manager: Add preference to control macro download --- src/Mod/AddonManager/AddonManager.py | 86 +++++++---- src/Mod/AddonManager/AddonManagerOptions.ui | 17 ++- src/Mod/AddonManager/CMakeLists.txt | 1 + .../AddonManager/addonmanager_utilities.py | 11 ++ src/Mod/AddonManager/first_run.ui | 144 ++++++++++++++++++ 5 files changed, 230 insertions(+), 29 deletions(-) create mode 100644 src/Mod/AddonManager/first_run.ui diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index fac36dc128..5073d1bd7f 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -111,32 +111,64 @@ 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(), + ) + 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() @@ -384,9 +416,11 @@ class CommandAddonManager: self.activate_table_widgets, self.populate_macros, self.update_metadata_cache, - self.load_macro_metadata, 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() @@ -870,7 +904,6 @@ class CommandAddonManager: self.dialog.labelStatusInfo.hide() self.dialog.progressBar.hide() self.dialog.buttonPauseUpdate.hide() - self.dialog.labelUpdateInProgress.hide() self.packageList.ui.lineEditFilter.setFocus() def show_progress_widgets(self) -> None: @@ -878,7 +911,6 @@ class CommandAddonManager: self.dialog.progressBar.show() self.dialog.buttonPauseUpdate.show() self.dialog.labelStatusInfo.show() - 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""" diff --git a/src/Mod/AddonManager/AddonManagerOptions.ui b/src/Mod/AddonManager/AddonManagerOptions.ui index e022702023..33970bb76c 100644 --- a/src/Mod/AddonManager/AddonManagerOptions.ui +++ b/src/Mod/AddonManager/AddonManagerOptions.ui @@ -6,8 +6,8 @@ 0 0 - 390 - 628 + 388 + 621 @@ -35,6 +35,19 @@ installed addons will be checked for available updates + + + + Download Macro metadata (approximately 10MB) + + + DownloadMacros + + + Addons + + + diff --git a/src/Mod/AddonManager/CMakeLists.txt b/src/Mod/AddonManager/CMakeLists.txt index de711853f1..9ef2819ea1 100644 --- a/src/Mod/AddonManager/CMakeLists.txt +++ b/src/Mod/AddonManager/CMakeLists.txt @@ -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 diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index c7f516b9ef..a0cd0236fe 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -300,4 +300,15 @@ def fix_relative_links(text, base_url): return new_text +def warning_color_string() -> str: + 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,50,50)" + else: + warningColorString = "rgb(200,0,0)" + return warningColorString + + # @} diff --git a/src/Mod/AddonManager/first_run.ui b/src/Mod/AddonManager/first_run.ui new file mode 100644 index 0000000000..4daf95476c --- /dev/null +++ b/src/Mod/AddonManager/first_run.ui @@ -0,0 +1,144 @@ + + + Dialog + + + Qt::WindowModal + + + + 0 + 0 + 398 + 237 + + + + Welcome to the Addon Manager + + + + + + 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! + + + true + + + + + + + Qt::Horizontal + + + + + + + + 75 + true + + + + Download Settings + + + + + + + Automatically check installed Addons for updates + + + + + + + Download Macro metadata (approximately 10MB) + + + + + + + + + + No proxy + + + + + System proxy + + + + + User-defined proxy: + + + + + + + + + + + + + These and other settings are available in the FreeCAD Preferences window. + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From e954f677fbc52526d1aa47a01fde6198578773e1 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 20:10:44 -0600 Subject: [PATCH 07/16] Addon Manager: Downgrade failed low-level urlopen to log --- src/Mod/AddonManager/addonmanager_utilities.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index a0cd0236fe..8ad9df5528 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -127,9 +127,7 @@ def urlopen(url: str) -> Union[None, HTTPResponse]: 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 From a5e89982f499edc5d96795d797e14d39d113b230 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 31 Dec 2021 20:19:52 -0600 Subject: [PATCH 08/16] Addon Manager: Ensure cache is rebuilt after firstrun dialog --- src/Mod/AddonManager/AddonManager.py | 5 ++++- src/Mod/AddonManager/addonmanager_macro.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 5073d1bd7f..d336e516ad 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -24,7 +24,6 @@ # * * # *************************************************************************** -from inspect import indentsize import os import shutil import stat @@ -155,6 +154,8 @@ class CommandAddonManager: "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) @@ -202,6 +203,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 diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index d2ee65a41a..6e65685de8 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -198,7 +198,10 @@ class Macro(object): return response = "" block = 8192 - while data := u2.read(block): + while True: + data = u2.read(block) + if not data: + break if isinstance(data, bytes): data = data.decode("utf-8") response += data @@ -232,7 +235,7 @@ 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" ) From 6f1c2627b235c3fa37613e76596c46881b8810ad Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 1 Jan 2022 09:43:06 -0600 Subject: [PATCH 09/16] Addon Manager: Remove unused imports --- src/Mod/AddonManager/AddonManager.py | 3 +-- src/Mod/AddonManager/addonmanager_macro.py | 1 - src/Mod/AddonManager/addonmanager_utilities.py | 7 +------ src/Mod/AddonManager/addonmanager_workers.py | 2 +- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index d336e516ad..7253877b29 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -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 diff --git a/src/Mod/AddonManager/addonmanager_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index 6e65685de8..65a8739126 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -66,7 +66,6 @@ class Macro(object): self.author = "" self.other_files = [] self.parsed = False - self.parse_time = 0.0 def __eq__(self, other): return self.filename == other.filename diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 8ad9df5528..5cc3ca5950 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -21,23 +21,18 @@ # * * # *************************************************************************** -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 diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 00f4762adf..a75d43b355 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -35,7 +35,7 @@ import time from datetime import datetime from typing import Union, List -from PySide2 import QtCore, QtGui, QtNetwork +from PySide2 import QtCore, QtNetwork import FreeCAD From 172243138a2c759881f6604bc37b3e49ea8bdb3b Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 1 Jan 2022 11:41:23 -0600 Subject: [PATCH 10/16] Addon Manager: Improve display of installation details --- src/Mod/AddonManager/AddonManager.py | 2 + .../AddonManager/addonmanager_utilities.py | 66 ++++++++++- src/Mod/AddonManager/addonmanager_workers.py | 18 ++- src/Mod/AddonManager/package_details.py | 104 +++++++++++------- 4 files changed, 139 insertions(+), 51 deletions(-) diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index 7253877b29..13c2440cf8 100644 --- a/src/Mod/AddonManager/AddonManager.py +++ b/src/Mod/AddonManager/AddonManager.py @@ -755,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( diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 5cc3ca5950..1ab5fafc15 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -28,6 +28,7 @@ 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 @@ -294,14 +295,75 @@ def fix_relative_links(text, base_url): 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,50,50)" + warningColorString = "rgb(255,105,97)" else: - warningColorString = "rgb(200,0,0)" + 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 + + # @} diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index a75d43b355..45dd516e12 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -299,7 +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)) + repo = AddonManagerRepo.from_macro(new_macro) + utils.update_macro_installation_details(repo) + self.add_macro_signal.emit(repo) class CheckWorkbenchesForUpdatesWorker(QtCore.QThread): @@ -588,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): @@ -633,6 +636,7 @@ 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) @@ -1053,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 = ( - '' - + translate("AddonsInstaller", "This macro is already installed.") - + "
" - ) - else: - already_installed_msg = "" message = ( - already_installed_msg - + "

" + "

" + self.macro.name + "

" + self.macro.desc @@ -1719,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) diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 2e4a115def..508073d7d4 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -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 = "

" 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 += "." + elif repo.macro and repo.macro.version: + installed_version_string += ( + "" + + translate("AddonsInstaller", "Update available to version") + + " " + ) + installed_version_string += repo.macro.version + installed_version_string += "." else: installed_version_string += ( "" + translate( "AddonsInstaller", - "Update available to unknown version (no package.xml file found)", + "An update is available", ) + "." ) @@ -166,19 +179,32 @@ class PackageDetails(QWidget): + "." ) - basedir = FreeCAD.getUserAppDataDir() - moddir = os.path.join(basedir, "Mod", repo.name) - installed_version_string += ( - "
" - + translate("AddonsInstaller", "Installation location") - + ": " - + moddir + installed_version_string += "

" + 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") + "" ) - 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") + "" ) - 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() From 911fd3bf179b5cc5657f886eba38f7930b82da6e Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sat, 1 Jan 2022 19:04:21 +0100 Subject: [PATCH 11/16] Draft: fix several issues with importAirfoilDAT.py The importAirfoilDAT.py code could not handle several *.dat files posted in a forum topic. --- src/Mod/Draft/importAirfoilDAT.py | 49 ++++++++++++++++++------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/Mod/Draft/importAirfoilDAT.py b/src/Mod/Draft/importAirfoilDAT.py index 3e3e13d147..e6886f94d1 100644 --- a/src/Mod/Draft/importAirfoilDAT.py +++ b/src/Mod/Draft/importAirfoilDAT.py @@ -9,7 +9,6 @@ \brief Airfoil (.dat) file importer This module provides support for importing airfoil .dat files. -Note (2019): this module has been unmaintained for a long time. ''' # Check code with # flake8 --ignore=E226,E266,E401,W503 @@ -103,8 +102,9 @@ def open(filename): """ docname = os.path.splitext(os.path.basename(filename))[0] doc = FreeCAD.newDocument(docname) - doc.Label = decodeName(docname[:-4]) - process(doc, filename) + doc.Label = decodeName(docname) + process(filename) + doc.recompute() def insert(filename, docname): @@ -131,13 +131,16 @@ def insert(filename, docname): doc = FreeCAD.getDocument(docname) except NameError: doc = FreeCAD.newDocument(docname) - importgroup = doc.addObject("App::DocumentObjectGroup", groupname) - importgroup.Label = decodeName(groupname) - process(doc, filename) + obj = process(filename) + if obj is not None: + importgroup = doc.addObject("App::DocumentObjectGroup", groupname) + importgroup.Label = decodeName(groupname) + importgroup.Group = [obj] + doc.recompute() -def process(doc, filename): - """Process the filename and provide the document with the information. +def process(filename): + """Process the filename and create a Draft Wire from the data. The common airfoil dat format has many flavors. This code should work with almost every dialect. @@ -146,18 +149,15 @@ def process(doc, filename): ---------- filename : str The path to the filename to be opened. - docname : str - The name of the active App::Document if one exists, or - of the new one created. Returns ------- - App::Document - The active FreeCAD document, or the document created if none exists, - with the parsed information. + Part::Part2DObject or None. + The created Draft Wire object or None if the file contains less + than 3 points. """ # Regex to identify data rows and throw away unused metadata - xval = r'(?P(\-|\d*)\.*\d*([Ee]\-?\d+)?)' + xval = r'(?P\-?\s*\d*\.*\d*([Ee]\-?\d+)?)' yval = r'(?P\-?\s*\d*\.*\d*([Ee]\-?\d+)?)' _regex = r'^\s*' + xval + r'\,?\s*' + yval + r'\s*$' @@ -170,21 +170,28 @@ def process(doc, filename): # upside = True # last_x = None - # Collect the data for the upper and the lower side separately if possible + # Collect the data for lin in afile: curdat = regex.match(lin) - if curdat is not None: + if (curdat is not None + and curdat.group("xval") + and curdat.group("yval")): x = float(curdat.group("xval")) y = float(curdat.group("yval")) - # the normal processing - coords.append(Vector(x, y, 0)) + # Some files specify the number of upper and lower points on the 2nd line: + # " 67. 72." + # See: http://airfoiltools.com/airfoil + # This line must be skipped: + if x < 2 and y < 2: + # the normal processing + coords.append(Vector(x, y, 0)) afile.close() if len(coords) < 3: FCC.PrintError(translate("ImportAirfoilDAT", "Did not find enough coordinates") + "\n") - return + return None # sometimes coords are divided in upper an lower side # so that x-coordinate begin new from leading or trailing edge @@ -227,4 +234,4 @@ def process(doc, filename): obj = FreeCAD.ActiveDocument.addObject('Part::Feature', airfoilname) obj.Shape = face - doc.recompute() + return obj From 3c1531b39ac8bc6b970068923fdef352c7b18412 Mon Sep 17 00:00:00 2001 From: Roy Date: Thu, 23 Dec 2021 11:50:12 +0100 Subject: [PATCH 12/16] Draft: Commands acting on subelements did not load the Draft module --- src/Mod/Draft/draftguitools/gui_move.py | 1 + src/Mod/Draft/draftguitools/gui_rotate.py | 1 + src/Mod/Draft/draftguitools/gui_scale.py | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Mod/Draft/draftguitools/gui_move.py b/src/Mod/Draft/draftguitools/gui_move.py index 6c2196cb6c..803d95b55e 100644 --- a/src/Mod/Draft/draftguitools/gui_move.py +++ b/src/Mod/Draft/draftguitools/gui_move.py @@ -197,6 +197,7 @@ class Move(gui_base_original.Modifier): def move_subelements(self, is_copy): """Move the subelements.""" + Gui.addModule("Draft") try: if is_copy: self.commit(translate("draft", "Copy"), diff --git a/src/Mod/Draft/draftguitools/gui_rotate.py b/src/Mod/Draft/draftguitools/gui_rotate.py index d382e8723e..be7a5888b0 100644 --- a/src/Mod/Draft/draftguitools/gui_rotate.py +++ b/src/Mod/Draft/draftguitools/gui_rotate.py @@ -271,6 +271,7 @@ class Rotate(gui_base_original.Modifier): def rotate_subelements(self, is_copy): """Rotate the subelements.""" + Gui.addModule("Draft") try: if is_copy: self.commit(translate("draft", "Copy"), diff --git a/src/Mod/Draft/draftguitools/gui_scale.py b/src/Mod/Draft/draftguitools/gui_scale.py index 0e8e77a42c..0f1e164639 100644 --- a/src/Mod/Draft/draftguitools/gui_scale.py +++ b/src/Mod/Draft/draftguitools/gui_scale.py @@ -192,6 +192,7 @@ class Scale(gui_base_original.Modifier): the selected object is not a rectangle or another object that can't be used with `scaleVertex` and `scaleEdge`. """ + Gui.addModule("Draft") try: if self.task.isCopy.isChecked(): self.commit(translate("draft", "Copy"), From 5ade10d049e68724f6b852434d1586be5f9b4527 Mon Sep 17 00:00:00 2001 From: Jean-Marie Verdun Date: Sat, 1 Jan 2022 12:11:29 -0500 Subject: [PATCH 13/16] Fix MacBundle relocation --- src/MacAppBundle/CMakeLists.txt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/MacAppBundle/CMakeLists.txt b/src/MacAppBundle/CMakeLists.txt index c0b6ccf09f..14d547e33e 100644 --- a/src/MacAppBundle/CMakeLists.txt +++ b/src/MacAppBundle/CMakeLists.txt @@ -32,10 +32,14 @@ if(HOMEBREW_PREFIX) file(READ ${PTH_FILE} ADDITIONAL_DIR) string(STRIP "${ADDITIONAL_DIR}" ADDITIONAL_DIR) - string(REGEX REPLACE "^${HOMEBREW_PREFIX}/Cellar/([A-Za-z0-9_]+).*$" "\\1" LIB_NAME ${ADDITIONAL_DIR}) - string(REGEX REPLACE ".*libexec(.*)/site-packages" "libexec/${LIB_NAME}\\1" NEW_SITE_DIR ${ADDITIONAL_DIR}) + string(FIND "${ADDITIONAL_DIR}" "${HOMEBREW_PREFIX}/Cellar" POSITION) + string(LENGTH "${ADDITIONAL_DIR}" DIR_LENGTH) + string(SUBSTRING "${ADDITIONAL_DIR}" ${POSITION} ${DIR_LENGTH}-${POSITION} DIR_TAIL) + string(REGEX MATCHALL "^([/A-Za-z0-9_.@-]+)" CLEAR_TAIL ${DIR_TAIL}) + string(REGEX REPLACE "^${HOMEBREW_PREFIX}/Cellar/([A-Za-z0-9_]+).*$" "\\1" LIB_NAME ${CLEAR_TAIL}) + string(REGEX REPLACE ".*libexec(.*)/site-packages" "libexec/${LIB_NAME}\\1" NEW_SITE_DIR ${CLEAR_TAIL}) - install(DIRECTORY ${ADDITIONAL_DIR} DESTINATION ${CMAKE_INSTALL_PREFIX}/${NEW_SITE_DIR}) + install(DIRECTORY ${CLEAR_TAIL} DESTINATION ${CMAKE_INSTALL_PREFIX}/${NEW_SITE_DIR}) #update the paths of the .pth files copied into the bundle get_filename_component(PTH_FILENAME ${PTH_FILE} NAME) @@ -120,7 +124,7 @@ find_package(PkgConfig) pkg_check_modules(ICU icu-uc) execute_process( - COMMAND find /usr/local/Cellar/nglib -name MacOS + COMMAND find -L /usr/local/Cellar/nglib -name MacOS OUTPUT_VARIABLE CONFIG_NGLIB) install(CODE From 74dc5344d4a4e0d638fd35ae112c5bb6f68b58ad Mon Sep 17 00:00:00 2001 From: Wanderer Fan Date: Thu, 23 Dec 2021 15:29:04 -0500 Subject: [PATCH 14/16] [TD]prevent touched but must be execute on DrawPage --- src/Mod/TechDraw/App/DrawPage.cpp | 2 +- src/Mod/TechDraw/Gui/Command.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mod/TechDraw/App/DrawPage.cpp b/src/Mod/TechDraw/App/DrawPage.cpp index f35835c0f6..75bd59e160 100644 --- a/src/Mod/TechDraw/App/DrawPage.cpp +++ b/src/Mod/TechDraw/App/DrawPage.cpp @@ -162,7 +162,7 @@ void DrawPage::onChanged(const App::Property* prop) //Page is just a container. It doesn't "do" anything. App::DocumentObjectExecReturn *DrawPage::execute(void) { - return App::DocumentObject::StdReturn; + return App::DocumentObject::execute(); } // this is now irrelevant, b/c DP::execute doesn't do anything. diff --git a/src/Mod/TechDraw/Gui/Command.cpp b/src/Mod/TechDraw/Gui/Command.cpp index da4565bfdd..d540d9f0a4 100644 --- a/src/Mod/TechDraw/Gui/Command.cpp +++ b/src/Mod/TechDraw/Gui/Command.cpp @@ -141,6 +141,7 @@ void CmdTechDrawPageDefault::activated(int iMsg) doCommand(Doc,"App.activeDocument().%s.Template = '%s'",TemplateName.c_str(), templateFileName.toStdString().c_str()); doCommand(Doc,"App.activeDocument().%s.Template = App.activeDocument().%s",PageName.c_str(),TemplateName.c_str()); + updateActive(); commitCommand(); TechDraw::DrawPage* fp = dynamic_cast(getDocument()->getObject(PageName.c_str())); if (!fp) { @@ -220,6 +221,7 @@ void CmdTechDrawPageTemplate::activated(int iMsg) doCommand(Doc,"App.activeDocument().%s.Template = App.activeDocument().%s",PageName.c_str(),TemplateName.c_str()); // consider renaming DrawSVGTemplate.Template property? + updateActive(); commitCommand(); TechDraw::DrawPage* fp = dynamic_cast(getDocument()->getObject(PageName.c_str())); if (!fp) { From 5f109b5ee32f0bf20ef9e6ac0b60fa24b6871747 Mon Sep 17 00:00:00 2001 From: Wanderer Fan Date: Thu, 30 Dec 2021 12:25:24 -0500 Subject: [PATCH 15/16] [TD]Allow selection of Page when adding View --- src/Mod/TechDraw/Gui/CMakeLists.txt | 4 + src/Mod/TechDraw/Gui/DlgPageChooser.cpp | 103 ++++++++++++++++++++++++ src/Mod/TechDraw/Gui/DlgPageChooser.h | 57 +++++++++++++ src/Mod/TechDraw/Gui/DlgPageChooser.ui | 93 +++++++++++++++++++++ src/Mod/TechDraw/Gui/DrawGuiUtil.cpp | 31 ++++++- 5 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 src/Mod/TechDraw/Gui/DlgPageChooser.cpp create mode 100644 src/Mod/TechDraw/Gui/DlgPageChooser.h create mode 100644 src/Mod/TechDraw/Gui/DlgPageChooser.ui diff --git a/src/Mod/TechDraw/Gui/CMakeLists.txt b/src/Mod/TechDraw/Gui/CMakeLists.txt index 1cffb31d8f..f0c7d73fba 100644 --- a/src/Mod/TechDraw/Gui/CMakeLists.txt +++ b/src/Mod/TechDraw/Gui/CMakeLists.txt @@ -54,6 +54,7 @@ else() endif() set(TechDrawGui_UIC_SRCS + DlgPageChooser.ui DlgPrefsTechDrawAdvanced.ui DlgPrefsTechDrawAnnotation.ui DlgPrefsTechDrawColors.ui @@ -117,6 +118,9 @@ SET(TechDrawGui_SRCS TaskProjGroup.ui TaskProjGroup.cpp TaskProjGroup.h + DlgPageChooser.ui + DlgPageChooser.cpp + DlgPageChooser.h DlgPrefsTechDrawGeneral.ui DlgPrefsTechDrawGeneralImp.cpp DlgPrefsTechDrawGeneralImp.h diff --git a/src/Mod/TechDraw/Gui/DlgPageChooser.cpp b/src/Mod/TechDraw/Gui/DlgPageChooser.cpp new file mode 100644 index 0000000000..b8373e1f86 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPageChooser.cpp @@ -0,0 +1,103 @@ +/**************************************************************************** + * Copyright (c) 2021 Wanderer Fan * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ****************************************************************************/ +#include "PreCompiled.h" +#ifndef _PreComp_ +# include +# include +# include +# include +# include +#endif + +#include +#include +#include "DlgPageChooser.h" +#include "ui_DlgPageChooser.h" + +FC_LOG_LEVEL_INIT("Gui",true,true) + +using namespace TechDrawGui; + +/* TRANSLATOR Gui::DlgPageChooser */ + +DlgPageChooser::DlgPageChooser( + const std::vector labels, + const std::vector names, + QWidget* parent, Qt::WindowFlags fl) + : QDialog(parent, fl), ui(new Ui_DlgPageChooser) +{ + ui->setupUi(this); + ui->lwPages->setSortingEnabled(true); + + fillList(labels, names); + + connect(ui->bbButtons, SIGNAL(accepted()), this, SLOT(accept())); + connect(ui->bbButtons, SIGNAL(rejected()), this, SLOT(reject())); +} + +/** + * Destroys the object and frees any allocated resources + */ +DlgPageChooser::~DlgPageChooser() +{ + // no need to delete child widgets, Qt does it all for us + delete ui; +} + +void DlgPageChooser::fillList(std::vector labels, std::vector names) +{ + QListWidgetItem* item; + QString qLabel; + QString qName; + QString qText; + int labelCount = labels.size(); + int i = 0; + for(; i < labelCount; i++) { + qLabel = Base::Tools::fromStdString(labels[i]); + qName = Base::Tools::fromStdString(names[i]); + qText = QString::fromUtf8("%1 (%2)").arg(qLabel).arg(qName); + item = new QListWidgetItem(qText, ui->lwPages); + item->setData(Qt::UserRole, qName); + } +} + +std::string DlgPageChooser::getSelection() const +{ + std::string result; + QList sels = ui->lwPages->selectedItems(); + if (!sels.empty()) { + QListWidgetItem* item = sels.front(); + result = item->data(Qt::UserRole).toByteArray().constData(); + } + return result; +} + + +void DlgPageChooser::accept() { + QDialog::accept(); +} + +void DlgPageChooser::reject() { + QDialog::reject(); +} + +#include "moc_DlgPageChooser.cpp" diff --git a/src/Mod/TechDraw/Gui/DlgPageChooser.h b/src/Mod/TechDraw/Gui/DlgPageChooser.h new file mode 100644 index 0000000000..d786276bd8 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPageChooser.h @@ -0,0 +1,57 @@ +/**************************************************************************** + * Copyright (c) 2021 Wanderer Fan * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ****************************************************************************/ +#ifndef GUI_DLGPAGECHOOSER_H +#define GUI_DLGPAGECHOOSER_H + +#include + +namespace TechDrawGui { + +class Ui_DlgPageChooser; +class TechDrawGuiExport DlgPageChooser : public QDialog +{ + Q_OBJECT + +public: + DlgPageChooser(const std::vector labels, + const std::vector names, + QWidget* parent = 0, Qt::WindowFlags fl = Qt::WindowFlags()); + ~DlgPageChooser(); + + std::string getSelection() const; + void accept(); + void reject(); + +private Q_SLOTS: + +private: + void fillList(std::vector labels, std::vector names); + +private: + Ui_DlgPageChooser* ui; +}; + +} // namespace Gui + + +#endif // GUI_DLGPAGECHOOSER_H + diff --git a/src/Mod/TechDraw/Gui/DlgPageChooser.ui b/src/Mod/TechDraw/Gui/DlgPageChooser.ui new file mode 100644 index 0000000000..c10bdf4304 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPageChooser.ui @@ -0,0 +1,93 @@ + + + TechDrawGui::DlgPageChooser + + + Qt::WindowModal + + + + 0 + 0 + 360 + 280 + + + + Page Chooser + + + + + + true + + + + + + FreeCAD could not determine which Page to use. Please select a Page. + + + true + + + + + + + Select a Page that should be used + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + + + + + + + + bbButtons + accepted() + TechDrawGui::DlgPageChooser + accept() + + + 179 + 228 + + + 179 + 139 + + + + + bbButtons + rejected() + TechDrawGui::DlgPageChooser + reject() + + + 179 + 228 + + + 179 + 139 + + + + + diff --git a/src/Mod/TechDraw/Gui/DrawGuiUtil.cpp b/src/Mod/TechDraw/Gui/DrawGuiUtil.cpp index 1fa0ce681e..3ff5097b44 100644 --- a/src/Mod/TechDraw/Gui/DrawGuiUtil.cpp +++ b/src/Mod/TechDraw/Gui/DrawGuiUtil.cpp @@ -80,6 +80,7 @@ #include "QGVPage.h" #include "MDIViewPage.h" #include "ViewProviderPage.h" +#include "DlgPageChooser.h" #include "DrawGuiUtil.h" using namespace TechDrawGui; @@ -104,6 +105,8 @@ void DrawGuiUtil::loadArrowBox(QComboBox* qcb) TechDraw::DrawPage* DrawGuiUtil::findPage(Gui::Command* cmd) { TechDraw::DrawPage* page = nullptr; + std::vector names; + std::vector labels; //check Selection for a page std::vector selPages = cmd->getSelection(). @@ -127,8 +130,18 @@ TechDraw::DrawPage* DrawGuiUtil::findPage(Gui::Command* cmd) page = qp->getDrawPage(); } else { // no active page - QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Which page?"), - QObject::tr("Can not determine correct page.")); + for(auto obj: selPages) { + std::string name = obj->getNameInDocument(); + names.push_back(name); + std::string label = obj->Label.getValue(); + labels.push_back(label); + } + DlgPageChooser dlg(labels, names, Gui::getMainWindow()); + if(dlg.exec()==QDialog::Accepted) { + std::string selName = dlg.getSelection(); + App::Document* doc = cmd->getDocument(); + page = static_cast(doc->getObject(selName.c_str())); + } } } else { //only 1 page in document - use it @@ -136,8 +149,18 @@ TechDraw::DrawPage* DrawGuiUtil::findPage(Gui::Command* cmd) } } else if (selPages.size() > 1) { //multiple pages in selection - QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Too many pages"), - QObject::tr("Select only 1 page.")); + for(auto obj: selPages) { + std::string name = obj->getNameInDocument(); + names.push_back(name); + std::string label = obj->Label.getValue(); + labels.push_back(label); + } + DlgPageChooser dlg(labels, names, Gui::getMainWindow()); + if(dlg.exec()==QDialog::Accepted) { + std::string selName = dlg.getSelection(); + App::Document* doc = cmd->getDocument(); + page = static_cast(doc->getObject(selName.c_str())); + } } else { //exactly 1 page in selection, use it page = static_cast(selPages.front()); From c5affe7ea9c146a0356dc67dc63e7dd9dedc3702 Mon Sep 17 00:00:00 2001 From: Florian Simmer Date: Sat, 25 Dec 2021 21:42:37 +0100 Subject: [PATCH 16/16] Github: Create codespell workflow and codespellignore --- .github/codespellignore | 90 +++++++++++++++++++++++++++++++++ .github/workflows/codespell.yml | 31 ++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 .github/codespellignore create mode 100644 .github/workflows/codespell.yml diff --git a/.github/codespellignore b/.github/codespellignore new file mode 100644 index 0000000000..54bd1fa58a --- /dev/null +++ b/.github/codespellignore @@ -0,0 +1,90 @@ +aci +ake +aline +alle +alledges +alocation +als +ang +anid +apoints +ba +beginn +behaviour +bloaded +bottome +byteorder +calculater +cancelled +cancelling +cas +cascade +centimetre +childrens +childs +colour +colours +commen +connexion +currenty +dof +doubleclick +dum +eiter +elemente +ende +feld +finde +findf +freez +hist +iff +indicies +initialisation +initialise +initialised +initialises +initialisiert +inout +ist +kilometre +lod +mantatory +methode +metres +millimetre +modell +nd +noe +normale +normaly +nto +numer +oder +ontop +orgin +orginx +orginy +ot +pard +parm +parms +pres +programm +que +recurrance +rougly +seperator +serie +sinc +strack +substraction +te +thist +thru +tread +uint +unter +vertexes +wallthickness +whitespaces \ No newline at end of file diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000000..068a2a6221 --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,31 @@ +# GitHub Action to automate the identification of common misspellings in text files. +# https://github.com/codespell-project/actions-codespell +# https://github.com/codespell-project/codespell + +name: Codespell +on: + pull_request: + +jobs: + codespell: + name: Check for spelling errors + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v12.2 + + - name: List all changed files + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + echo "$file was changed" + done + + - uses: codespell-project/actions-codespell@master + with: + check_filenames: true + ignore_words_file: .github/codespellignore + skip: ./.git,*.po,*.ts,./ChangeLog.txt,./src/3rdParty,./src/Mod/Assembly/App/opendcm,./src/CXX,./src/zipios++,./src/Base/swig*,./src/Mod/Robot/App/kdl_cp,./src/Mod/Import/App/SCL,./src/WindowsInstaller,./src/Doc/FreeCAD.uml,./build/ + path: ${{ steps.changed-files.outputs.all_changed_files }} \ No newline at end of file