# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2023 FreeCAD Project Association * # * * # * This file is part of FreeCAD. * # * * # * FreeCAD is free software: you can redistribute it and/or modify it * # * under the terms of the GNU Lesser General Public License as * # * published by the Free Software Foundation, either version 2.1 of the * # * License, or (at your option) any later version. * # * * # * FreeCAD 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 * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with FreeCAD. If not, see * # * . * # * * # *************************************************************************** """ Contains a parameter observer class and parameter related functions.""" import PySide.QtCore as QtCore import xml.etree.ElementTree as ET import FreeCAD as App import Draft_rc import Arch_rc from draftutils import init_draft_statusbar from draftutils.translate import translate if App.GuiUp: import FreeCADGui as Gui from PySide import QtWidgets class ParamObserverDraft: def slotParamChanged(self, param_grp, typ, entry, value): if entry == "textheight": _param_observer_callback_tray() return if entry in ("gridBorder", "gridShowHuman", "coloredGridAxes", "gridEvery", "gridSpacing", "gridSize", "gridTransparency", "gridColor"): _param_observer_callback_grid() return if entry == "DefaultAnnoScaleMultiplier": _param_observer_callback_scalemultiplier(value) return if entry == "SnapBarShowOnlyDuringCommands": _param_observer_callback_snapbar(value) return if entry == "DisplayStatusbarSnapWidget": _param_observer_callback_snapwidget() return if entry == "DisplayStatusbarScaleWidget": _param_observer_callback_scalewidget() return if entry == "snapStyle": _param_observer_callback_snapstyle() return if entry == "snapcolor": _param_observer_callback_snapcolor() return if entry == "patternFile": _param_observer_callback_svg_pattern() return class ParamObserverView: def slotParamChanged(self, param_grp, typ, entry, value): if entry in ("DefaultShapeColor", "DefaultShapeLineColor", "DefaultShapeLineWidth"): _param_observer_callback_tray() return if entry == "MarkerSize": _param_observer_callback_snaptextsize() return def _param_observer_callback_tray(): if not hasattr(Gui, "draftToolBar"): return tray = Gui.draftToolBar.tray if tray is None: return Gui.draftToolBar.setStyleButton() def _param_observer_callback_scalemultiplier(value): # value is a string. if not value: return value = float(value) if value <= 0: return mw = Gui.getMainWindow() sb = mw.statusBar() scale_widget = sb.findChild(QtWidgets.QToolBar,"draft_scale_widget") if scale_widget is not None: scale_label = init_draft_statusbar.scale_to_label(1 / value) scale_widget.scaleLabel.setText(scale_label) def _param_observer_callback_grid(): if hasattr(App, "draft_working_planes") and hasattr(Gui, "Snapper"): try: trackers = Gui.Snapper.trackers for wp in App.draft_working_planes[1]: view = wp._view if view in trackers[0]: i = trackers[0].index(view) grid = trackers[1][i] grid.pts = [] grid.reset() grid.displayHumanFigure(wp) grid.setAxesColor(wp) except Exception: pass def _param_observer_callback_snapbar(value): if Gui.activeWorkbench().name() not in ("DraftWorkbench", "ArchWorkbench", "BIMWorkbench"): return if hasattr(Gui, "Snapper"): toolbar = Gui.Snapper.get_snap_toolbar() if toolbar is not None: toolbar.setVisible(value == "0") # value is a string: "0" or "1" def _param_observer_callback_snapwidget(): if Gui.activeWorkbench().name() == "DraftWorkbench": init_draft_statusbar.hide_draft_statusbar() init_draft_statusbar.show_draft_statusbar() def _param_observer_callback_scalewidget(): if Gui.activeWorkbench().name() == "DraftWorkbench": init_draft_statusbar.hide_draft_statusbar() init_draft_statusbar.show_draft_statusbar() def _param_observer_callback_snapstyle(): if hasattr(Gui, "Snapper"): Gui.Snapper.set_snap_style() def _param_observer_callback_snapcolor(): if hasattr(Gui, "Snapper"): tracker_list = [2, 5, 6] for each_tracker in tracker_list: for snap_track in Gui.Snapper.trackers[each_tracker]: snap_track.setColor() def _param_observer_callback_snaptextsize(): if hasattr(Gui, "Snapper"): tracker_list = [5, 6] for each_tracker in tracker_list: for snap_track in Gui.Snapper.trackers[each_tracker]: snap_track.setSize() def _param_observer_callback_svg_pattern(): # imports have to happen here to avoid circular imports from draftutils import utils from draftviewproviders import view_base utils.load_svg_patterns() if App.ActiveDocument is None: return pats = list(utils.svg_patterns()) pats.sort() pats = ["None"] + pats data = [] for doc in App.listDocuments().values(): vobjs = [] for obj in doc.Objects: if hasattr(obj, "ViewObject"): vobj = obj.ViewObject if hasattr(vobj, "Pattern") \ and hasattr(vobj, "Proxy") \ and isinstance(vobj.Proxy, view_base.ViewProviderDraft) \ and vobj.getEnumerationsOfProperty("Pattern") != pats: vobjs.append(vobj) if vobjs: data.append([doc, vobjs]) if not data: return msg = translate("draft", """Do you want to update the SVG pattern options of existing objects in all opened documents?""") res = QtWidgets.QMessageBox.question(None, "Update SVG patterns", msg, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if res == QtWidgets.QMessageBox.No: return for doc, vobjs in data: doc.openTransaction("SVG pattern update") for vobj in vobjs: old = vobj.Pattern if old in pats: vobj.Pattern = pats else: tmp_pats = [old] + pats[1:] tmp_pats.sort() vobj.Pattern = ["None"] + tmp_pats vobj.Pattern = old doc.commitTransaction() def _param_observer_start(): if App.GuiUp: _param_observer_start_draft() _param_observer_start_view() def _param_observer_start_draft(param_grp = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")): param_grp.AttachManager(ParamObserverDraft()) def _param_observer_start_view(param_grp = App.ParamGet("User parameter:BaseApp/Preferences/View")): param_grp.AttachManager(ParamObserverView()) def _param_from_PrefCheckBox(widget): value = False for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "checked": # Can be missing. value = elem.find("bool").text == "true" elif att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text return path, entry, value def _param_from_PrefRadioButton(widget): return _param_from_PrefCheckBox(widget) def _param_from_PrefComboBox(widget): value = 0 for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "currentIndex": # Can be missing. value = int(elem.find("number").text) elif att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text return path, entry, value def _param_from_PrefSpinBox(widget): value = 0 for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "value": # Can be missing. value = int(elem.find("number").text) elif att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text return path, entry, value def _param_from_PrefDoubleSpinBox(widget): value = 0.0 for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "value": # Can be missing. value = float(elem.find("double").text) elif att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text return path, entry, value def _param_from_PrefUnitSpinBox(widget): value = 0.0 for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "rawValue": # Can be missing. value = float(elem.find("double").text) elif att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text return path, entry, value def _param_from_PrefQuantitySpinBox(widget): value = "0" for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "rawValue": # Can be missing. value = elem.find("double").text.rstrip(".0") elif att_name == "unit": unit = elem.find("string").text elif att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text value = value + " " + unit return path, entry, value def _param_from_PrefColorButton(widget): for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "color": sub = list(elem)[0] r = int(sub.find("red").text) g = int(sub.find("green").text) b = int(sub.find("blue").text) elif att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text value = (r << 24) + (g << 16) + (b << 8) + 255 return path, entry, value def _param_from_PrefLineEdit(widget): value = None for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "text": # Can be missing. value = elem.find("string").text # If text is missing value will be None here. elif att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text if value is None: value = "" return path, entry, value def _param_from_PrefFileChooser(widget): for elem in list(widget): if "name" in elem.keys(): att_name = elem.attrib["name"] if att_name == "prefEntry": entry = elem.find("cstring").text elif att_name == "prefPath": path = elem.find("cstring").text return path, entry, "" def _get_param_dictionary(): # print("Creating preferences dictionary...") param_dict = {} # Draft parameters that are not in the preferences: param_dict["Mod/Draft"] = { "AnnotationStyleEditorHeight": ("int", 450), "AnnotationStyleEditorWidth": ("int", 450), "CenterPlaneOnView": ("bool", False), "ContinueMode": ("bool", False), "CopyMode": ("bool", False), "DefaultAnnoDisplayMode": ("int", 0), "DefaultDisplayMode": ("int", 0), "DefaultDrawStyle": ("int", 0), "DefaultPrintColor": ("unsigned", 255), "Draft_array_fuse": ("bool", False), "Draft_array_Link": ("bool", True), "fillmode": ("bool", True), "GlobalMode": ("bool", False), "GridHideInOtherWorkbenches": ("bool", True), "HatchPatternResolution": ("int", 128), "HatchPatternRotation": ("float", 0.0), "HatchPatternScale": ("float", 100.0), "labeltype": ("string", "Custom"), "LayersManagerHeight": ("int", 320), "LayersManagerWidth": ("int", 640), "maxSnapEdges": ("int", 0), "OffsetCopyMode": ("bool", False), "Offset_OCC": ("bool", False), "RelativeMode": ("bool", True), "ScaleClone": ("bool", False), "ScaleCopy": ("bool", False), "ScaleRelative": ("bool", False), "ScaleUniform": ("bool", False), "snapModes": ("string", "100000000000000"), "snapRange": ("int", 8), "SubelementMode": ("bool", False), "SvgLinesBlack": ("bool", True), "useSupport": ("bool", False), } # Arch parameters that are not in the preferences: param_dict["Mod/Arch"] = { "applyConstructionStyle": ("bool", True), "ClaimHosted": ("bool", True), "CustomIfcSchema": ("string", ""), # importIFClegacy.py "createIfcGroups": ("bool", False), # importIFClegacy.py "DoorHeight": ("float", 2100.0), "DoorPreset": ("int", 5), "DoorSill": ("float", 0.0), "DoorWidth": ("float", 1000.0), "FreeLinking": ("bool", False), "forceIfcPythonParser": ("bool", False), # importIFClegacy.py "getStandardType": ("bool", False), "ifcAggregateWindows": ("bool", False), # importIFClegacy.py "ifcAsMesh": ("string", ""), # importIFClegacy.py "IfcExportList": ("bool", False), # importIFClegacy.py "ifcImportLayer": ("bool", True), "ifcJoinSolids": ("bool", False), # importIFClegacy.py "ifcMergeProfiles": ("bool", False), "IfcScalingFactor": ("float", 1.0), # importIFClegacy.py "ifcSeparatePlacements": ("bool", False), # importIFClegacy.py "MultiMaterialColumnWidth0": ("int", 60), "MultiMaterialColumnWidth1": ("int", 60), "PanelLength": ("float", 1000.0), "PanelThickness": ("float", 10.0), "PanelWidth": ("float", 1000.0), "PrecastBase": ("float", 0.0), "PrecastChamfer": ("float", 0.0), "PrecastDentHeight": ("float", 0.0), "PrecastDentLength": ("float", 0.0), "PrecastDentWidth": ("float", 0.0), "PrecastDownLength": ("float", 0.0), "PrecastGrooveDepth": ("float", 0.0), "PrecastGrooveHeight": ("float", 0.0), "PrecastGrooveSpacing": ("float", 0.0), "PrecastHoleMajor": ("float", 0.0), "PrecastHoleMinor": ("float", 0.0), "PrecastHoleSpacing": ("float", 0.0), "PrecastRiser": ("float", 0.0), "PrecastTread": ("float", 0.0), "ScheduleColumnWidth0": ("int", 100), "ScheduleColumnWidth1": ("int", 100), "ScheduleColumnWidth2": ("int", 50), "ScheduleColumnWidth3": ("int", 100), "ScheduleDialogHeight": ("int", 200), "ScheduleDialogWidth": ("int", 300), "StructureHeight": ("float", 1000.0), "StructureLength": ("float", 100.0), "StructurePreset": ("string", ""), "StructureWidth": ("float", 100.0), "swallowAdditions": ("bool", True), "swallowSubtractions": ("bool", True), "WallAlignment": ("int", 0), "WallHeight": ("float", 3000.0), "WallWidth": ("float", 200.0), "WindowH1": ("float", 50.0), "WindowH2": ("float", 50.0), "WindowH3": ("float", 50.0), "WindowHeight": ("float", 1000.0), "WindowO1": ("float", 0.0), "WindowO2": ("float", 50.0), "WindowPreset": ("int", 0), "WindowSill": ("float", 0.0), "WindowW1": ("float", 100.0), "WindowW2": ("float", 50.0), "WindowWidth": ("float", 1000.0), } # For the Mod/Mesh parameters we do not check the preferences: param_dict["Mod/Mesh"] = { "MaxDeviationExport": ("float", 0.1), } # For the Mod/TechDraw/PAT parameters we do not check the preferences: param_dict["Mod/TechDraw/PAT"] = { "FilePattern": ("string", ""), "NamePattern": ("string", "Diamant"), } # For the General parameters we do not check the preferences: param_dict["General"] = { "ToolbarIconSize": ("int", 24), } # For the Units parameters we do not check the preferences: param_dict["Units"] = { "Decimals": ("int", 2), "UserSchema": ("int", 0), } # For the View parameters we do not check the preferences: param_dict["View"] = { "BackgroundColor": ("unsigned", 336897023), "BackgroundColor2": ("unsigned", 859006463), "BackgroundColor3": ("unsigned", 2543299327), "DefaultAmbientColor": ("unsigned", 1431655935), "DefaultEmissiveColor": ("unsigned", 255), "DefaultShapeColor": ("unsigned", 3435980543), "DefaultShapeLineColor": ("unsigned", 421075455), "DefaultShapeLineWidth": ("int", 2), "DefaultShapePointSize": ("int", 2), "DefaultShapeShininess": ("int", 90), "DefaultShapeTransparency": ("int", 0), "DefaultShapeVertexColor": ("unsigned", 421075455), "DefaultSpecularColor": ("unsigned", 2290649343), "EnableSelection": ("bool", True), "Gradient": ("bool", True), "MarkerSize": ("int", 9), "NewDocumentCameraScale": ("float", 100.0), } # Preferences ui files are stored in resource files. # For the Draft Workbench: /Mod/Draft/Draft_rc.py # For the Arch Workbench: /Mod/Arch/Arch_rc.py for fnm in (":/ui/preferences-draft.ui", ":/ui/preferences-draftinterface.ui", ":/ui/preferences-draftsnap.ui", ":/ui/preferences-drafttexts.ui", ":/ui/preferences-draftvisual.ui", ":/ui/preferences-dwg.ui", ":/ui/preferences-dxf.ui", ":/ui/preferences-oca.ui", ":/ui/preferences-svg.ui", ":/ui/preferences-arch.ui", ":/ui/preferences-archdefaults.ui", ":/ui/preferences-dae.ui", ":/ui/preferences-ifc.ui", ":/ui/preferences-ifc-export.ui"): # https://stackoverflow.com/questions/14750997/load-txt-file-from-resources-in-python fd = QtCore.QFile(fnm) if fd.open(QtCore.QIODevice.ReadOnly | QtCore.QFile.Text): text = QtCore.QTextStream(fd).readAll() fd.close() else: print("Preferences file " + fnm + " not found") continue # https://docs.python.org/3/library/xml.etree.elementtree.html root = ET.fromstring(text) # Get all preference widgets: # pref_widgets = [wid for wid in root.iter("widget") if "Gui::Pref" in wid.attrib["class"]] for widget in root.iter("widget"): if "class" in widget.keys(): path = None att_class = widget.attrib["class"] if att_class == "Gui::PrefCheckBox": path, entry, value = _param_from_PrefCheckBox(widget) typ = "bool" elif att_class == "Gui::PrefRadioButton": path, entry, value = _param_from_PrefRadioButton(widget) typ = "bool" elif att_class == "Gui::PrefComboBox": path, entry, value = _param_from_PrefComboBox(widget) typ = "int" elif att_class == "Gui::PrefSpinBox": path, entry, value = _param_from_PrefSpinBox(widget) typ = "int" elif att_class == "Gui::PrefDoubleSpinBox": path, entry, value = _param_from_PrefDoubleSpinBox(widget) typ = "float" elif att_class == "Gui::PrefUnitSpinBox": path, entry, value = _param_from_PrefUnitSpinBox(widget) typ = "float" elif att_class == "Gui::PrefQuantitySpinBox": path, entry, value = _param_from_PrefQuantitySpinBox(widget) typ = "string" elif att_class == "Gui::PrefColorButton": path, entry, value = _param_from_PrefColorButton(widget) typ = "unsigned" elif att_class == "Gui::PrefLineEdit": path, entry, value = _param_from_PrefLineEdit(widget) typ = "string" elif att_class == "Gui::PrefFileChooser": path, entry, value = _param_from_PrefFileChooser(widget) typ = "string" if path is not None: if path in param_dict: param_dict[path][entry] = (typ, value) else: param_dict[path] = {entry: (typ, value)} return param_dict PARAM_DICT = _get_param_dictionary() def get_param(entry, path="Mod/Draft", ret_default=False): """Return a stored parameter value or its default. Parameters ---------- entry: str Name of the parameter. path: str, optional Defaults to "Mod/Draft". The path where the parameter can be found. This string is appended to "User parameter:BaseApp/Preferences/". ret_default: bool, optional Defaults to `False`. If `True`, always return the default value even if a stored value is available. Returns ------- bool, float, int or str (if successful) or `None`. """ if path not in PARAM_DICT or entry not in PARAM_DICT[path]: print(f"draftutils.params.get_param: Unable to find '{entry}' in '{path}'") return None param_grp = App.ParamGet("User parameter:BaseApp/Preferences/" + path) typ, default = PARAM_DICT[path][entry] if ret_default: return default if typ == "bool": return param_grp.GetBool(entry, default) if typ == "float": return param_grp.GetFloat(entry, default) if typ == "int": return param_grp.GetInt(entry, default) if typ == "string": return param_grp.GetString(entry, default) if typ == "unsigned": return param_grp.GetUnsigned(entry, default) return None def get_param_arch(entry, ret_default=False): return get_param(entry, path="Mod/Arch", ret_default=ret_default) def get_param_view(entry, ret_default=False): return get_param(entry, path="View", ret_default=ret_default) def set_param(entry, value, path="Mod/Draft"): """Store a parameter value. Parameters ---------- entry: str Name of the parameter. value: bool, float, int or str New value of the correct type. path: str, optional Defaults to "Mod/Draft". The path where the parameter can be found. This string is appended to "User parameter:BaseApp/Preferences/". Returns ------- `True` (if successful) or `False`. """ if path not in PARAM_DICT or entry not in PARAM_DICT[path]: print(f"draftutils.params.set_param: Unable to find '{entry}' in '{path}'") return False param_grp = App.ParamGet("User parameter:BaseApp/Preferences/" + path) typ = PARAM_DICT[path][entry][0] ret = True if typ == "bool": param_grp.SetBool(entry, value) elif typ == "float": param_grp.SetFloat(entry, value) elif typ == "int": param_grp.SetInt(entry, value) elif typ == "string": param_grp.SetString(entry, value) elif typ == "unsigned": param_grp.SetUnsigned(entry, value) else: ret = False return ret def set_param_arch(entry, value): return set_param(entry, value, path="Mod/Arch") def set_param_view(entry, value): return set_param(entry, value, path="View")