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 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 diff --git a/src/Mod/AddonManager/AddonManager.py b/src/Mod/AddonManager/AddonManager.py index ae8b6e887b..13c2440cf8 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 @@ -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() 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... - - - 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_macro.py b/src/Mod/AddonManager/addonmanager_macro.py index d602dabd15..65a8739126 100644 --- a/src/Mod/AddonManager/addonmanager_macro.py +++ b/src/Mod/AddonManager/addonmanager_macro.py @@ -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("", "", 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. diff --git a/src/Mod/AddonManager/addonmanager_utilities.py b/src/Mod/AddonManager/addonmanager_utilities.py index 532c46ec67..1ab5fafc15 100644 --- a/src/Mod/AddonManager/addonmanager_utilities.py +++ b/src/Mod/AddonManager/addonmanager_utilities.py @@ -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 + + # @} diff --git a/src/Mod/AddonManager/addonmanager_workers.py b/src/Mod/AddonManager/addonmanager_workers.py index 1460301b11..45dd516e12 100644 --- a/src/Mod/AddonManager/addonmanager_workers.py +++ b/src/Mod/AddonManager/addonmanager_workers.py @@ -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 = ( - '' - + translate("AddonsInstaller", "This macro is already installed.") - + "
" - ) - else: - already_installed_msg = "" message = ( - already_installed_msg - + "

" + "

" + self.macro.name + "

" + 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) 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 + + + + + diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 2e06781a3a..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() diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index 17a2a78e08..247f5bac91 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -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 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"), 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 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/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/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) { 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());