From 51f489083bbbb569c040a82709bf7811b0bd5577 Mon Sep 17 00:00:00 2001 From: tarman3 Date: Mon, 26 May 2025 13:13:00 +0300 Subject: [PATCH 01/30] CAM: Changes in warnings of old Array --- src/Mod/CAM/Path/Op/Gui/Array.py | 106 +++++++++++++++++-------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/src/Mod/CAM/Path/Op/Gui/Array.py b/src/Mod/CAM/Path/Op/Gui/Array.py index 8a33d58280..956bb88042 100644 --- a/src/Mod/CAM/Path/Op/Gui/Array.py +++ b/src/Mod/CAM/Path/Op/Gui/Array.py @@ -24,11 +24,10 @@ import FreeCAD import FreeCADGui import Path import Path.Op.Base as PathOp -import PathScripts import PathScripts.PathUtils as PathUtils +import Path.Base.Util as PathUtil from Path.Dressup.Utils import toolController from PySide import QtCore -from PySide import QtGui import random @@ -154,8 +153,6 @@ class ObjectArray: self.setEditorModes(obj) obj.Proxy = self - self.FirstRun = True - def dumps(self): return None @@ -223,42 +220,21 @@ class ObjectArray: self.setEditorModes(obj) - self.FirstRun = True - def execute(self, obj): - if FreeCAD.GuiUp and self.FirstRun: - self.FirstRun = False - QtGui.QMessageBox.warning( - None, - QT_TRANSLATE_NOOP("CAM_ArrayOp", "Operation is deprecated"), - QT_TRANSLATE_NOOP( - "CAM_ArrayOp", - ( - "CAM -> Path Modification -> Array operation is deprecated " - "and will be removed in future FreeCAD versions.\n\n" - "Please use CAM -> Path Dressup -> Array instead.\n\n" - "DO NOT USE CURRENT ARRAY OPERATION WHEN MACHINING WITH COOLANT!\n" - "Due to a bug - coolant will not be enabled for array paths." - ), - ), - ) # backwards compatibility for PathArrays created before support for multiple bases if isinstance(obj.Base, list): base = obj.Base else: base = [obj.Base] - if len(base) == 0: + # Do not generate paths and clear current Path data + # if operation not Active or no base operations or operations not compatible + if not obj.Active or len(base) == 0 or not self.isBaseCompatible(obj): + obj.Path = Path.Path() return obj.ToolController = toolController(base[0]) - # Do not generate paths and clear current Path data if operation not - if not obj.Active: - if obj.Path: - obj.Path = Path.Path() - return - # use seed if specified, otherwise default to object name for consistency during recomputes seed = obj.JitterSeed or obj.Name @@ -280,6 +256,37 @@ class ObjectArray: obj.Path = pa.getPath() obj.CycleTime = PathOp.getCycleTimeEstimate(obj) + def isBaseCompatible(self, obj): + if not obj.Base: + return False + tcs = [] + cms = [] + for sel in obj.Base: + if not sel.isDerivedFrom("Path::Feature"): + return False + tcs.append(toolController(sel)) + cms.append(PathUtil.coolantModeForOp(sel)) + + if tcs == {None} or len(set(tcs)) > 1: + Path.Log.warning( + translate( + "PathArray", + "Arrays of toolpaths having different tool controllers or tool controller not selected.", + ) + ) + return False + + if set(cms) != {"None"}: + Path.Log.warning( + translate( + "PathArray", + "Arrays not compatible with coolant modes.", + ) + ) + return False + + return True + class PathArray: """class PathArray ... @@ -337,10 +344,6 @@ class PathArray: """getPath() ... Call this method on an instance of the class to generate and return path data for the requested path array.""" - if len(self.baseList) == 0: - Path.Log.error(translate("PathArray", "No base objects for PathArray.")) - return None - base = self.baseList for b in base: if not b.isDerivedFrom("Path::Feature"): @@ -352,15 +355,6 @@ class PathArray: if not b_tool_controller: return - if b_tool_controller != toolController(base[0]): - # this may be important if Job output is split by tool controller - Path.Log.warning( - translate( - "PathArray", - "Arrays of toolpaths having different tool controllers are handled according to the tool controller of the first path.", - ) - ) - # build copies output = "" random.seed(self.seed) @@ -486,14 +480,32 @@ class CommandPathArray: return { "Pixmap": "CAM_Array", "MenuText": QT_TRANSLATE_NOOP("CAM_Array", "Array"), - "ToolTip": QT_TRANSLATE_NOOP("CAM_Array", "Creates an array from selected toolpath(s)"), + "ToolTip": QT_TRANSLATE_NOOP( + "CAM_Array", + "Creates an array from selected toolpath(s)\nwith identical tool controllers and without coolant", + ), } def IsActive(self): - selections = [ - sel.isDerivedFrom("Path::Feature") for sel in FreeCADGui.Selection.getSelection() - ] - return selections and all(selections) + selection = FreeCADGui.Selection.getSelection() + if not selection: + return False + tcs = [] + for sel in selection: + if not sel.isDerivedFrom("Path::Feature"): + return False + tc = toolController(sel) + if tc: + # Active only for operations with identical tool controller + tcs.append(tc) + if len(set(tcs)) != 1: + return False + else: + return False + if PathUtil.coolantModeForOp(sel) != "None": + # Active only for operations without cooling + return False + return True def Activated(self): From a32594faeafd989a77869af1ebd934010cdef92d Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 6 Apr 2025 22:36:06 +0200 Subject: [PATCH 02/30] Gui: Add ThemeTokenManager class to contain theme parameters This class aims to implement Design Token idea into FreeCAD themes. It allows themes to use generic variables with generic values so we could use one qss theme and change the style based on values from preference packs. --- src/Base/Tools.h | 27 + src/Gui/Application.cpp | 81 +- src/Gui/Application.h | 9 +- src/Gui/CMakeLists.txt | 8 + src/Gui/CommandStd.cpp | 7 +- src/Gui/Dialogs/DlgThemeEditor.cpp | 764 ++++++++++++++++++ src/Gui/Dialogs/DlgThemeEditor.h | 152 ++++ src/Gui/Dialogs/DlgThemeEditor.ui | 156 ++++ src/Gui/OverlayManager.cpp | 4 +- src/Gui/PreCompiled.h | 2 + src/Gui/PreferencePackManager.cpp | 8 +- src/Gui/PreferencePages/DlgSettingsUI.cpp | 29 +- src/Gui/PreferencePages/DlgSettingsUI.h | 1 + src/Gui/PreferencePages/DlgSettingsUI.ui | 61 +- src/Gui/StyleParameters/ParameterManager.cpp | 353 ++++++++ src/Gui/StyleParameters/ParameterManager.h | 457 +++++++++++ src/Gui/StyleParameters/Parser.cpp | 403 +++++++++ src/Gui/StyleParameters/Parser.h | 170 ++++ src/Gui/Tools.h | 110 ++- tests/src/Gui/CMakeLists.txt | 3 + .../StyleParameters/ParameterManagerTest.cpp | 326 ++++++++ tests/src/Gui/StyleParameters/ParserTest.cpp | 617 ++++++++++++++ .../StyleParametersApplicationTest.cpp | 94 +++ 23 files changed, 3749 insertions(+), 93 deletions(-) create mode 100644 src/Gui/Dialogs/DlgThemeEditor.cpp create mode 100644 src/Gui/Dialogs/DlgThemeEditor.h create mode 100644 src/Gui/Dialogs/DlgThemeEditor.ui create mode 100644 src/Gui/StyleParameters/ParameterManager.cpp create mode 100644 src/Gui/StyleParameters/ParameterManager.h create mode 100644 src/Gui/StyleParameters/Parser.cpp create mode 100644 src/Gui/StyleParameters/Parser.h create mode 100644 tests/src/Gui/StyleParameters/ParameterManagerTest.cpp create mode 100644 tests/src/Gui/StyleParameters/ParserTest.cpp create mode 100644 tests/src/Gui/StyleParameters/StyleParametersApplicationTest.cpp diff --git a/src/Base/Tools.h b/src/Base/Tools.h index 88982c0ae6..404cdae6ba 100644 --- a/src/Base/Tools.h +++ b/src/Base/Tools.h @@ -356,6 +356,33 @@ struct BaseExport ZipTools }; +/** + * Helper struct to define inline overloads for the visitor pattern in std::visit. + * + * It uses type deduction to infer the type from the expression and creates a dedicated type that + * essentially is callable using any overload supplied. + * + * @code + * using Base::Overloads; + * + * const auto visitor = Overloads + * { + * [](int i){ std::print("int = {}\n", i); }, + * [](std::string_view s){ std::println("string = “{}”", s); }, + * [](const Base&){ std::println("base"); }, + * }; + * @endcode + * + * @see https://en.cppreference.com/w/cpp/utility/variant/visit + * + * @tparam Ts Types for functions that will be used for overloads + */ +template +struct Overloads: Ts... +{ + using Ts::operator()...; +}; + } // namespace Base #endif // SRC_BASE_TOOLS_H_ diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index 2f7466b13f..2cd30d8b8a 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -137,9 +137,11 @@ #include "WorkbenchManipulator.h" #include "WidgetFactory.h" #include "3Dconnexion/navlib/NavlibInterface.h" +#include "Inventor/SoFCPlacementIndicatorKit.h" #include "QtWidgets.h" -#include +#include +#include #ifdef BUILD_TRACY_FRAME_PROFILER #include @@ -208,6 +210,8 @@ struct ApplicationP // Create the Theme Manager prefPackManager = new PreferencePackManager(); + // Create the Style Parameter Manager + styleParameterManager = new StyleParameters::ParameterManager(); } ~ApplicationP() @@ -221,8 +225,11 @@ struct ApplicationP /// Active document Gui::Document* activeDocument {nullptr}; Gui::Document* editDocument {nullptr}; + MacroManager* macroMngr; PreferencePackManager* prefPackManager; + StyleParameters::ParameterManager* styleParameterManager; + /// List of all registered views std::list passive; bool isClosing {false}; @@ -372,6 +379,31 @@ struct PyMethodDef FreeCADGui_methods[] = { } // namespace Gui +void Application::initStyleParameterManager() +{ + Base::registerServiceImplementation( + new StyleParameters::BuiltInParameterSource({.name = QT_TR_NOOP("Built-in Parameters")})); + + Base::registerServiceImplementation( + new StyleParameters::UserParameterSource( + App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Themes/Tokens"), + {.name = QT_TR_NOOP("Theme Parameters"), + .options = StyleParameters::ParameterSourceOption::UserEditable})); + + Base::registerServiceImplementation( + new StyleParameters::UserParameterSource( + App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Themes/UserTokens"), + {.name = QT_TR_NOOP("User Parameters"), + .options = StyleParameters::ParameterSource::UserEditable})); + + for (auto* source : Base::provideServiceImplementations()) { + d->styleParameterManager->addSource(source); + } + + Base::registerServiceImplementation(d->styleParameterManager); +} // clang-format off Application::Application(bool GUIenabled) { @@ -576,6 +608,8 @@ Application::Application(bool GUIenabled) d = new ApplicationP(GUIenabled); + initStyleParameterManager(); + // global access Instance = this; @@ -1955,6 +1989,11 @@ Gui::PreferencePackManager* Application::prefPackManager() return d->prefPackManager; } +Gui::StyleParameters::ParameterManager* Application::styleParameterManager() +{ + return d->styleParameterManager; +} + //************************************************************************** // Init, Destruct and singleton @@ -2314,7 +2353,7 @@ void Application::runApplication() setenv("COIN_EGL", "1", 1); } #endif - + // Make sure that we use '.' as decimal point. See also // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=559846 // and issue #0002891 @@ -2498,36 +2537,22 @@ void Application::setStyleSheet(const QString& qssFile, bool tiledBackground) } } -QString Application::replaceVariablesInQss(QString qssText) +void Application::reloadStyleSheet() { - // First we fetch the colors from preferences, - ParameterGrp::handle hGrp = - App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Themes"); - unsigned long longAccentColor1 = hGrp->GetUnsigned("ThemeAccentColor1", 0); - unsigned long longAccentColor2 = hGrp->GetUnsigned("ThemeAccentColor2", 0); - unsigned long longAccentColor3 = hGrp->GetUnsigned("ThemeAccentColor3", 0); + const MainWindow* mw = getMainWindow(); - // convert them to hex. - // Note: the ulong contains alpha channels so 8 hex characters when we need 6 here. - QString accentColor1 = QStringLiteral("#%1") - .arg(longAccentColor1, 8, 16, QLatin1Char('0')) - .toUpper() - .mid(0, 7); - QString accentColor2 = QStringLiteral("#%1") - .arg(longAccentColor2, 8, 16, QLatin1Char('0')) - .toUpper() - .mid(0, 7); - QString accentColor3 = QStringLiteral("#%1") - .arg(longAccentColor3, 8, 16, QLatin1Char('0')) - .toUpper() - .mid(0, 7); + const QString qssFile = mw->property("fc_currentStyleSheet").toString(); + const bool tiledBackground = mw->property("fc_tiledBackground").toBool(); - qssText = qssText.replace(QStringLiteral("@ThemeAccentColor1"), accentColor1); - qssText = qssText.replace(QStringLiteral("@ThemeAccentColor2"), accentColor2); - qssText = qssText.replace(QStringLiteral("@ThemeAccentColor3"), accentColor3); + d->styleParameterManager->reload(); - // Base::Console().warning("%s\n", qssText.toStdString()); - return qssText; + setStyleSheet(qssFile, tiledBackground); + OverlayManager::instance()->refresh(nullptr, true); +} + +QString Application::replaceVariablesInQss(const QString& qssText) +{ + return QString::fromStdString(d->styleParameterManager->replacePlaceholders(qssText.toStdString())); } void Application::checkForDeprecatedSettings() diff --git a/src/Gui/Application.h b/src/Gui/Application.h index 596fc32dae..45af443579 100644 --- a/src/Gui/Application.h +++ b/src/Gui/Application.h @@ -30,6 +30,8 @@ #include +#include "StyleParameters/ParameterManager.h" + class QCloseEvent; class SoNode; class NavlibInterface; @@ -63,6 +65,9 @@ public: /// destruction ~Application(); + /// Initializes default configuration for Style Parameter Manager + void initStyleParameterManager(); + /** @name methods for support of files */ //@{ /// open a file @@ -221,7 +226,8 @@ public: //@{ /// Activate a stylesheet void setStyleSheet(const QString& qssFile, bool tiledBackground); - QString replaceVariablesInQss(QString qssText); + void reloadStyleSheet(); + QString replaceVariablesInQss(const QString& qssText); //@} /** @name User Commands */ @@ -235,6 +241,7 @@ public: //@} Gui::PreferencePackManager* prefPackManager(); + Gui::StyleParameters::ParameterManager* styleParameterManager(); /** @name Init, Destruct an Access methods */ //@{ diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index ab93d16cac..e4979d9d51 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -55,6 +55,7 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/Quarter ${CMAKE_CURRENT_SOURCE_DIR}/PreferencePages ${CMAKE_CURRENT_SOURCE_DIR}/Selection + ${CMAKE_CURRENT_SOURCE_DIR}/StyleParameters ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/.. @@ -401,6 +402,7 @@ SET(Gui_UIC_SRCS Dialogs/DlgProjectUtility.ui Dialogs/DlgPropertyLink.ui Dialogs/DlgRevertToBackupConfig.ui + Dialogs/DlgThemeEditor.ui PreferencePages/DlgSettings3DView.ui PreferencePages/DlgSettingsCacheDirectory.ui Dialogs/DlgSettingsColorGradient.ui @@ -516,6 +518,7 @@ SET(Dialog_CPP_SRCS Dialogs/DlgPropertyLink.cpp Dialogs/DlgRevertToBackupConfigImp.cpp Dialogs/DlgExpressionInput.cpp + Dialogs/DlgThemeEditor.cpp TaskDlgRelocation.cpp Dialogs/DlgCheckableMessageBox.cpp TaskTransform.cpp @@ -558,6 +561,7 @@ SET(Dialog_HPP_SRCS Dialogs/DlgRevertToBackupConfigImp.h Dialogs/DlgCheckableMessageBox.h Dialogs/DlgExpressionInput.h + Dialogs/DlgThemeEditor.h TaskDlgRelocation.h TaskTransform.h Dialogs/DlgUndoRedo.h @@ -1326,6 +1330,8 @@ SET(FreeCADGui_CPP_SRCS StartupProcess.cpp TransactionObject.cpp ToolHandler.cpp + StyleParameters/Parser.cpp + StyleParameters/ParameterManager.cpp ) SET(FreeCADGui_SRCS Application.h @@ -1368,6 +1374,8 @@ SET(FreeCADGui_SRCS StartupProcess.h TransactionObject.h ToolHandler.h + StyleParameters/Parser.h + StyleParameters/ParameterManager.h ) SET(FreeCADGui_SRCS diff --git a/src/Gui/CommandStd.cpp b/src/Gui/CommandStd.cpp index 2837ccd190..d6e0087af7 100644 --- a/src/Gui/CommandStd.cpp +++ b/src/Gui/CommandStd.cpp @@ -964,12 +964,7 @@ StdCmdReloadStyleSheet::StdCmdReloadStyleSheet() void StdCmdReloadStyleSheet::activated(int ) { - auto mw = getMainWindow(); - - auto qssFile = mw->property("fc_currentStyleSheet").toString(); - auto tiledBackground = mw->property("fc_tiledBackground").toBool(); - - Gui::Application::Instance->setStyleSheet(qssFile, tiledBackground); + Application::Instance->reloadStyleSheet(); } namespace Gui { diff --git a/src/Gui/Dialogs/DlgThemeEditor.cpp b/src/Gui/Dialogs/DlgThemeEditor.cpp new file mode 100644 index 0000000000..eeabf70581 --- /dev/null +++ b/src/Gui/Dialogs/DlgThemeEditor.cpp @@ -0,0 +1,764 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "Tools.h" +#include "Application.h" +#include "OverlayManager.h" +#include "DlgThemeEditor.h" +#include "ui_DlgThemeEditor.h" +#include "BitmapFactory.h" + +#include +#include + +#ifndef _PreComp_ +# include +# include +# include +# include +# include +#endif + +QPixmap colorPreview(const QColor& color) +{ + constexpr qsizetype size = 16; + + QPixmap preview = Gui::BitmapFactory().empty({ size, size }); + + QPainter painter(&preview); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawEllipse(QRect { 0, 0, size, size }); + + return preview; +} + +QString typeOfTokenValue(const Gui::StyleParameters::Value& value) +{ + // clang-format off + return std::visit( + Base::Overloads { + [](const std::string&) { + return QWidget::tr("Generic"); + }, + [](const Gui::StyleParameters::Length&) { + return QWidget::tr("Length"); + }, + [](const QColor&) { + return QWidget::tr("Color"); + } + }, + value + ); + // clang-format on +} + +namespace Gui +{ +struct StyleParametersModel::Item +{ + Item() = default; + virtual ~Item() = default; + + FC_DEFAULT_COPY_MOVE(Item); + + virtual bool isHeader() const = 0; +}; + +struct StyleParametersModel::GroupItem: Item +{ + explicit GroupItem(QString title, ParameterSource* source) + : title(std::move(title)) + , canAddNewParameters(source && source->metadata.options.testFlag(UserEditable)) + , source(source) + {} + + bool isHeader() const override + { + return true; + } + + QString title; + bool canAddNewParameters {false}; + ParameterSource* source; + std::set deleted {}; +}; + +struct StyleParametersModel::ParameterItem: Item +{ + ParameterItem(QString name, StyleParameters::Parameter token) + : name(std::move(name)) + , token(std::move(token)) + {} + + bool isHeader() const override + { + return false; + } + + QString name; + StyleParameters::Parameter token; + QFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; +}; + +class StyleParametersModel::Node +{ +public: + explicit Node(std::unique_ptr data, Node* parent = nullptr) + : _parent(parent) + , _data(std::move(data)) + {} + + void appendChild(std::unique_ptr child) + { + child->_parent = this; + _children.push_back(std::move(child)); + } + + void removeChild(const int row) + { + if (row >= 0 && row < static_cast(_children.size())) { + _children.erase(_children.begin() + row); + } + } + + Node* child(const int row) const + { + if (row < 0 || row >= static_cast(_children.size())) { + if (!_empty) { + _empty = std::make_unique(nullptr, const_cast(this)); + } + + return _empty.get(); + } + + return _children[row].get(); + } + + int childCount() const + { + return static_cast(_children.size()); + } + + int row() const + { + if (!_parent) { + return 0; + } + + const auto& siblings = _parent->_children; + for (size_t i = 0; i < siblings.size(); ++i) { + if (siblings[i].get() == this) { + return static_cast(i); + } + } + + return -1; + } + + Item* data() const + { + return _data.get(); + } + + template + T* data() const + { + return dynamic_cast(_data.get()); + } + + Node* parent() const + { + return _parent; + } + +private: + Node* _parent; + std::vector> _children {}; + + mutable std::unique_ptr _empty {}; + std::unique_ptr _data {}; +}; + +class DlgThemeEditor::Delegate: public QStyledItemDelegate +{ + Q_OBJECT + + QRegularExpression validNameRegExp { QStringLiteral("^[A-Z][a-zA-Z0-9]*$") }; + QRegularExpressionValidator* nameValidator; + +public: + explicit Delegate(QObject* parent = nullptr) + : QStyledItemDelegate(parent) + , nameValidator(new QRegularExpressionValidator(validNameRegExp, this)) + {} + + QWidget* createEditor(QWidget* parent, + [[maybe_unused]] const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + auto model = dynamic_cast(index.model()); + if (!model) { + return nullptr; + } + + if (model->item(index) + && index.column() == StyleParametersModel::ParameterExpression) { + return new QLineEdit(parent); + } + + if (index.column() == StyleParametersModel::ParameterName) { + auto editor = new QLineEdit(parent); + editor->setValidator(nameValidator); + return editor; + } + + return nullptr; + } + + void setEditorData(QWidget* editor, const QModelIndex& index) const override + { + if (auto* lineEdit = qobject_cast(editor)) { + lineEdit->setText(index.data(Qt::DisplayRole).toString()); + } + } + + void setModelData(QWidget* editor, + QAbstractItemModel* model, + const QModelIndex& index) const override + { + if (auto* lineEdit = qobject_cast(editor)) { + model->setData(index, lineEdit->text(), Qt::EditRole); + } + } + + void updateEditorGeometry(QWidget* editor, + const QStyleOptionViewItem& option, + [[maybe_unused]] const QModelIndex& index) const override + { + editor->setGeometry(option.rect); + } + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + constexpr int height = 36; + + QSize base = QStyledItemDelegate::sizeHint(option, index); + return {base.width(), std::max(base.height(), height)}; + } + + void paintAddPlaceholder(QPainter* painter, const QStyleOptionViewItem& option) const + { + QStyle* style = option.widget ? option.widget->style() : QApplication::style(); + QRect rect = style->subElementRect(QStyle::SE_ItemViewItemText, &option, option.widget); + + QFont font = option.font; + font.setItalic(true); + + painter->setFont(font); + painter->drawText(rect, Qt::AlignLeft | Qt::AlignVCenter, tr("New parameter...")); + } + + void paint(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + auto model = dynamic_cast(index.model()); + + painter->save(); + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + if (model->isAddPlaceholder(index)) { + if (index.column() == StyleParametersModel::ParameterName) { + paintAddPlaceholder(painter, opt); + } + } + else if (model->item(index)) { + constexpr int headerContrast = 120; + + const bool isLightTheme = option.palette.color(QPalette::Text).lightness() < 128; + + const QColor headerBackgroundColor = QtTools::valueOr( + option.widget->property("headerBackgroundColor"), + isLightTheme + ? option.palette.color(QPalette::AlternateBase).darker(headerContrast) + : option.palette.color(QPalette::AlternateBase).lighter(headerContrast) + ); + + painter->fillRect(option.rect, headerBackgroundColor); + QStyledItemDelegate::paint(painter, option, index); + } + else { + QStyledItemDelegate::paint(painter, option, index); + } + + painter->restore(); + } +}; + +void TokenTreeView::keyPressEvent(QKeyEvent* event) +{ + static constexpr auto expressionEditKeys = { Qt::Key_Return, Qt::Key_Enter, Qt::Key_Space }; + static constexpr auto nameEditKeys = { Qt::Key_F2 }; + static constexpr auto deleteKeys = { Qt::Key_Delete }; + + const auto isCorrectKey = [&event](auto key) { return event->key() == key; }; + + if (QModelIndex index = currentIndex(); index.isValid()) { + if (std::ranges::any_of(expressionEditKeys, isCorrectKey)) { + edit(index.siblingAtColumn(StyleParametersModel::ParameterExpression)); + return; + } + + if (std::ranges::any_of(nameEditKeys, isCorrectKey)) { + edit(index.siblingAtColumn(StyleParametersModel::ParameterName)); + return; + } + + if (std::ranges::any_of(deleteKeys, isCorrectKey)) { + requestRemove(currentIndex()); + return; + } + } + + QTreeView::keyPressEvent(event); +} + +StyleParametersModel::StyleParametersModel( + const std::list& sources, + QObject* parent) + : QAbstractItemModel(parent) + , ParameterSource({ .name = QT_TR_NOOP("All Theme Editor Parameters") }) + , sources(sources) + , manager(new StyleParameters::ParameterManager()) +{ + // The parameter model serves as the source, so the manager can compute all necessary things + manager->addSource(this); + + reset(); +} + +StyleParametersModel::~StyleParametersModel() = default; + +std::list StyleParametersModel::all() const +{ + std::map result; + + QtTools::walkTreeModel(this, [this, &result](const QModelIndex& index) { + if (auto parameterItem = item(index)) { + if (result.contains(parameterItem->token.name)) { + return; + } + + result[parameterItem->token.name] = parameterItem->token; + } + }); + + const auto values = result | std::ranges::views::values; + return std::list(values.begin(), values.end()); +} + +std::optional StyleParametersModel::get(const std::string& name) const +{ + std::optional result = std::nullopt; + + QtTools::walkTreeModel(this, [this, &name, &result](const QModelIndex& index) { + if (auto parameterItem = item(index)) { + if (parameterItem->token.name == name) { + result = parameterItem->token; + return true; + } + } + + return false; + }); + + return result; +} + +void StyleParametersModel::removeItem(const QModelIndex& index) +{ + if (auto parameterItem = item(index)) { + auto groupItem = item(index.parent()); + + if (!groupItem->source->metadata.options.testFlag(UserEditable)) { + return; + } + + groupItem->deleted.insert(parameterItem->token.name); + + beginRemoveRows(index.parent(), index.row(), index.row()); + node(index.parent())->removeChild(index.row()); + endRemoveRows(); + } +} + +void StyleParametersModel::reset() +{ + using enum StyleParameters::ParameterSourceOption; + + beginResetModel(); + root = std::make_unique(std::make_unique(tr("Root"), nullptr)); + + for (auto* source : sources) { + auto groupNode = std::make_unique( + std::make_unique(tr(source->metadata.name.c_str()), source)); + + for (const auto& parameter : source->all()) { + auto item = std::make_unique( + std::make_unique(QString::fromStdString(parameter.name), parameter)); + + if (source->metadata.options.testFlag(ReadOnly)) { + item->data()->flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + } + + groupNode->appendChild(std::move(item)); + } + + root->appendChild(std::move(groupNode)); + } + + endResetModel(); +} + +void StyleParametersModel::flush() +{ + QtTools::walkTreeModel(this, [this](const QModelIndex& index) { + if (const auto& groupItem = item(index)) { + for (const auto& parameter : groupItem->deleted) { + groupItem->source->remove(parameter); + } + + groupItem->deleted.clear(); + } + + if (const auto& parameterItem = item(index)) { + const auto& groupItem = item(index.parent()); + + groupItem->source->define(parameterItem->token); + } + }); + + reset(); +} + +int StyleParametersModel::rowCount(const QModelIndex& index) const +{ + if (index.column() > 0) { + return 0; + } + + int childCount = node(index)->childCount(); + + if (const auto& groupItem = item(index)) { + return childCount + (groupItem->canAddNewParameters ? 1 : 0); + } + + return childCount; +} + +int StyleParametersModel::columnCount([[maybe_unused]] const QModelIndex& index) const +{ + return ColumnCount; +} + +QVariant StyleParametersModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + switch (section) { + case ParameterName: + return tr("Name"); + case ParameterExpression: + return tr("Expression"); + case ParameterPreview: + return tr("Preview"); + case ParameterType: + return tr("Type"); + default: + return {}; + } + } + + return {}; +} + +QVariant StyleParametersModel::data(const QModelIndex& index, int role) const +{ + if (auto parameterItem = item(index)) { + const auto& [name, token, _] = *parameterItem; + const auto& value = manager->resolve(name.toStdString()); + + if (role == Qt::DisplayRole) { + if (index.column() == ParameterName) { + return name; + } + if (index.column() == ParameterExpression) { + return QString::fromStdString(token.value); + } + if (index.column() == ParameterType) { + return typeOfTokenValue(value); + } + if (index.column() == ParameterPreview) { + return QString::fromStdString(value.toString()); + } + } + + if (role == Qt::DecorationRole) { + if (index.column() == ParameterPreview && std::holds_alternative(value)) { + return colorPreview(std::get(value)); + } + } + } + + if (auto groupItem = item(index)) { + if (role == Qt::DisplayRole && index.column() == ParameterName) { + return groupItem->title; + } + } + + return {}; +} + +bool StyleParametersModel::setData(const QModelIndex& index, + const QVariant& value, + [[maybe_unused]] int role) +{ + if (auto parameterItem = item(index)) { + auto groupItem = item(index.parent()); + + if (index.column() == ParameterName) { + QString newName = value.toString(); + + StyleParameters::Parameter newToken = parameterItem->token; + newToken.name = newName.toStdString(); + + // there is no rename operation, so we need to mark the previous token as deleted + groupItem->deleted.insert(parameterItem->token.name); + + parameterItem->name = newName; + parameterItem->token = newToken; + } + + if (index.column() == ParameterExpression) { + QString newValue = value.toString(); + + StyleParameters::Parameter newToken = parameterItem->token; + newToken.value = newValue.toStdString(); + + parameterItem->token = newToken; + } + } + + if (isAddPlaceholder(index)) { + if (index.column() == ParameterName) { + QString newName = value.toString(); + + if (newName.isEmpty()) { + return false; + } + + StyleParameters::Parameter token { .name = newName.toStdString(), .value = "" }; + + int start = rowCount(index.parent()); + + beginInsertRows(index.parent(), start, start + 1); + auto item = std::make_unique( + std::make_unique(newName, token)); + node(index.parent())->appendChild(std::move(item)); + endInsertRows(); + + // this must be queued to basically next frame so widget has a chance to update + QTimer::singleShot(0, [this, index]() { + this->newParameterAdded(index); + }); + } + } + + this->manager->reload(); + + QtTools::walkTreeModel(this, [this](const QModelIndex& index) { + const QModelIndex previewColumnIndex = index.siblingAtColumn(ParameterPreview); + + Q_EMIT dataChanged(previewColumnIndex, previewColumnIndex); + }); + + return true; +} + +Qt::ItemFlags StyleParametersModel::flags(const QModelIndex& index) const +{ + if (auto parameterItem = item(index)) { + if (index.column() == ParameterName || index.column() == ParameterExpression) { + return parameterItem->flags | QAbstractItemModel::flags(index); + } + } + + if (isAddPlaceholder(index)) { + if (index.column() == ParameterName) { + return Qt::ItemIsEnabled | Qt::ItemIsEditable | QAbstractItemModel::flags(index); + } + } + + return QAbstractItemModel::flags(index); +} + +QModelIndex StyleParametersModel::index(int row, int col, const QModelIndex& parent) const +{ + if (!hasIndex(row, col, parent)) { + return {}; + } + + if (auto child = node(parent)->child(row)) { + return createIndex(row, col, child); + } + + return {}; +} + +QModelIndex StyleParametersModel::parent(const QModelIndex& index) const +{ + if (!index.isValid()) { + return {}; + } + + auto node = static_cast(index.internalPointer()); + auto parent = node->parent(); + + if (!parent || parent == root.get()) { + return {}; + } + + return createIndex(parent->row(), 0, parent); +} + +bool StyleParametersModel::isAddPlaceholder(const QModelIndex& index) const +{ + return item(index) == nullptr; +} + +StyleParametersModel::Node* StyleParametersModel::node(const QModelIndex& index) const +{ + return index.isValid() ? static_cast(index.internalPointer()) : root.get(); +} + +StyleParametersModel::Item* StyleParametersModel::item(const QModelIndex& index) const +{ + return node(index)->data(); +} + +DlgThemeEditor::DlgThemeEditor(QWidget* parent) + : QDialog(parent) + , ui(new Ui::DlgThemeEditor) + , model(std::make_unique( + Base::provideServiceImplementations(), + this)) +{ + ui->setupUi(this); + + ui->tokensTreeView->setMouseTracking(true); + ui->tokensTreeView->setItemDelegate(new Delegate(ui->tokensTreeView)); + ui->tokensTreeView->setModel(model.get()); + + constexpr int typeColumnWidth = 80; + constexpr int nameColumnWidth = 200; + + struct ColumnDefinition // NOLINT(*-pro-type-member-init) + { + StyleParametersModel::Column column; + QHeaderView::ResizeMode mode; + qsizetype defaultWidth = 0; + }; + + static constexpr std::initializer_list columnSizingDefinitions = { + {StyleParametersModel::ParameterName, QHeaderView::ResizeMode::ResizeToContents}, + {StyleParametersModel::ParameterExpression, QHeaderView::ResizeMode::Stretch}, + {StyleParametersModel::ParameterPreview, QHeaderView::ResizeMode::Stretch}, + {StyleParametersModel::ParameterType, QHeaderView::ResizeMode::Fixed, typeColumnWidth}, + }; + + for (const auto& [column, mode, defaultWidth] : columnSizingDefinitions) { + ui->tokensTreeView->header()->setSectionResizeMode(column, mode); + + if (defaultWidth > 0) { + ui->tokensTreeView->header()->setDefaultSectionSize(defaultWidth); + } + } + + ui->tokensTreeView->setColumnWidth(StyleParametersModel::ParameterName, nameColumnWidth); + ui->tokensTreeView->expandAll(); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &DlgThemeEditor::handleButtonClick); + + connect(ui->tokensTreeView, + &TokenTreeView::requestRemove, + model.get(), + qOverload(&StyleParametersModel::removeItem)); + + connect(model.get(), &StyleParametersModel::modelReset, ui->tokensTreeView, [this] { + ui->tokensTreeView->expandAll(); + }); + connect(model.get(), + &StyleParametersModel::newParameterAdded, + this, + [this](const QModelIndex& index) { + const auto newParameterExpressionIndex = + index.siblingAtColumn(StyleParametersModel::ParameterExpression); + + ui->tokensTreeView->scrollTo(newParameterExpressionIndex); + ui->tokensTreeView->setCurrentIndex(newParameterExpressionIndex); + ui->tokensTreeView->edit(newParameterExpressionIndex); + }); +} + +DlgThemeEditor::~DlgThemeEditor() = default; + +void DlgThemeEditor::handleButtonClick(QAbstractButton* button) +{ + auto role = ui->buttonBox->buttonRole(button); + + switch (role) { + case QDialogButtonBox::ApplyRole: + case QDialogButtonBox::AcceptRole: + model->flush(); + Application::Instance->reloadStyleSheet(); + break; + case QDialogButtonBox::ResetRole: + model->reset(); + break; + default: + // no-op + break; + } +} + +} // namespace Gui + +#include "DlgThemeEditor.moc" diff --git a/src/Gui/Dialogs/DlgThemeEditor.h b/src/Gui/Dialogs/DlgThemeEditor.h new file mode 100644 index 0000000000..6964742c6f --- /dev/null +++ b/src/Gui/Dialogs/DlgThemeEditor.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef DLGTHEMEEDITOR_H +#define DLGTHEMEEDITOR_H + + +#include "StyleParameters/ParameterManager.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QAbstractButton; +QT_END_NAMESPACE + +namespace Gui { + +namespace StyleParameters +{ +class ParameterManager; +} + +QT_BEGIN_NAMESPACE +namespace Ui { class DlgThemeEditor; } +QT_END_NAMESPACE + +class GuiExport TokenTreeView : public QTreeView +{ + Q_OBJECT +public: + using QTreeView::QTreeView; + +protected: + void keyPressEvent(QKeyEvent* event) override; + +Q_SIGNALS: + void requestRemove(const QModelIndex& index); +}; + +class GuiExport StyleParametersModel: public QAbstractItemModel, public StyleParameters::ParameterSource +{ + Q_OBJECT + + class Node; + +public: + struct Item; + struct GroupItem; + struct ParameterItem; + + enum Column : std::uint8_t + { + ParameterName, + ParameterExpression, + ParameterType, + ParameterPreview, + ColumnCount + }; + + FC_DISABLE_COPY_MOVE(StyleParametersModel); + + explicit StyleParametersModel(const std::list& sources, + QObject* parent = nullptr); + + ~StyleParametersModel() override; + + std::list all() const override; + std::optional get(const std::string& name) const override; + + void reset(); + void flush(); + + int rowCount(const QModelIndex& index) const override; + int columnCount(const QModelIndex& index) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + + Qt::ItemFlags flags(const QModelIndex& index) const override; + + QModelIndex index(int row, int col, const QModelIndex& parent) const override; + QModelIndex parent(const QModelIndex& index) const override; + + Node* node(const QModelIndex& index) const; + Item* item(const QModelIndex& index) const; + + template + T* item(const QModelIndex& index) const + { + return dynamic_cast(item(index)); + } + + bool isAddPlaceholder(const QModelIndex& index) const; + +public Q_SLOTS: + void removeItem(const QModelIndex& index); + +Q_SIGNALS: + void newParameterAdded(const QModelIndex& index); + +private: + std::list sources; + std::unique_ptr manager; + std::unique_ptr root; +}; + +class GuiExport DlgThemeEditor : public QDialog { + Q_OBJECT + + class Delegate; + +public: + FC_DISABLE_COPY_MOVE(DlgThemeEditor); + + explicit DlgThemeEditor(QWidget *parent = nullptr); + + ~DlgThemeEditor() override; + +public Q_SLOTS: + void handleButtonClick(QAbstractButton* button); + +private: + std::unique_ptr ui; + std::unique_ptr manager; + std::unique_ptr model; +}; +} // Gui + +#endif //DLGTHEMEEDITOR_H diff --git a/src/Gui/Dialogs/DlgThemeEditor.ui b/src/Gui/Dialogs/DlgThemeEditor.ui new file mode 100644 index 0000000000..dfc7802cf0 --- /dev/null +++ b/src/Gui/Dialogs/DlgThemeEditor.ui @@ -0,0 +1,156 @@ + + + Gui::DlgThemeEditor + + + + 0 + 0 + 1169 + 562 + + + + Theme Editor + + + + + + Preview + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + CheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + RadioButton + + + + + + + + Item 1 + + + + + Item 2 + + + + + + + + PushButton + + + + + + + + Tab 1 + + + + + Tab 2 + + + + + + + + + + + + + + + + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset + + + + + + + + + true + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + QAbstractItemView::EditTrigger::DoubleClicked + + + true + + + QAbstractItemView::SelectionBehavior::SelectRows + + + false + + + true + + + + + + + + Gui::ColorButton + QPushButton +
Gui/Widgets.h
+
+ + Gui::TokenTreeView + QTreeView +
Gui/Dialogs/DlgThemeEditor.h
+
+
+ + +
diff --git a/src/Gui/OverlayManager.cpp b/src/Gui/OverlayManager.cpp index 8c1cb144e7..15fdc211c1 100644 --- a/src/Gui/OverlayManager.cpp +++ b/src/Gui/OverlayManager.cpp @@ -133,6 +133,8 @@ public: if (activeStyleSheet.isEmpty()) { activeStyleSheet = _default; } + + activeStyleSheet = Application::Instance->replaceVariablesInQss(activeStyleSheet); } ParameterGrp::handle handle; @@ -157,7 +159,7 @@ private: } else if (!overlayStyleSheet.isEmpty() && !QFile::exists(overlayStyleSheet)) { // User did choose one of predefined stylesheets, we need to qualify it with namespace - overlayStyleSheet = QStringLiteral("overlay:%1").arg(overlayStyleSheet); + overlayStyleSheet = QStringLiteral("overlay:%1").arg(overlayStyleSheet); } return overlayStyleSheet; diff --git a/src/Gui/PreCompiled.h b/src/Gui/PreCompiled.h index 718190076b..4da9bd1cda 100644 --- a/src/Gui/PreCompiled.h +++ b/src/Gui/PreCompiled.h @@ -69,8 +69,10 @@ #include #include #include +#include #include #include +#include #include #include #include diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp index 2bac18b0e2..d573a06eeb 100644 --- a/src/Gui/PreferencePackManager.cpp +++ b/src/Gui/PreferencePackManager.cpp @@ -41,9 +41,12 @@ #include "DockWindowManager.h" #include "ToolBarManager.h" +#include #include -#include // For generating a timestamped filename +#include // For generating a timestamped filename +#include +#include using namespace Gui; @@ -359,6 +362,9 @@ bool PreferencePackManager::apply(const std::string& preferencePackName) const Gui::ToolBarManager* pToolbarMgr = Gui::ToolBarManager::getInstance(); pToolbarMgr->restoreState(); + // We need to reload stylesheet to apply any changed style parameters + Gui::Application::Instance->reloadStyleSheet(); + // TODO: Are there other things that have to be manually triggered? } return wasApplied; diff --git a/src/Gui/PreferencePages/DlgSettingsUI.cpp b/src/Gui/PreferencePages/DlgSettingsUI.cpp index 7774259798..e5d7974621 100644 --- a/src/Gui/PreferencePages/DlgSettingsUI.cpp +++ b/src/Gui/PreferencePages/DlgSettingsUI.cpp @@ -31,6 +31,10 @@ #include "DlgSettingsUI.h" #include "ui_DlgSettingsUI.h" +#include "Dialogs/DlgThemeEditor.h" + +#include + using namespace Gui::Dialog; @@ -45,6 +49,10 @@ DlgSettingsUI::DlgSettingsUI(QWidget* parent) , ui(new Ui_DlgSettingsUI) { ui->setupUi(this); + + connect(ui->themeEditorButton, &QPushButton::clicked, [this]() { + openThemeEditor(); + }); } /** @@ -114,13 +122,14 @@ void DlgSettingsUI::loadStyleSheet() populateStylesheets("OverlayActiveStyleSheet", "overlay", ui->OverlayStyleSheets, "Auto"); } -void DlgSettingsUI::populateStylesheets(const char *key, - const char *path, - PrefComboBox *combo, - const char *def, +void DlgSettingsUI::populateStylesheets(const char* key, + const char* path, + PrefComboBox* combo, + const char* def, QStringList filter) { - auto hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/MainWindow"); + auto hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/MainWindow"); // List all .qss/.css files QMap cssFiles; QDir dir; @@ -172,6 +181,12 @@ void DlgSettingsUI::populateStylesheets(const char *key, combo->onRestore(); } +void DlgSettingsUI::openThemeEditor() +{ + Gui::DlgThemeEditor editor; + editor.exec(); +} + /** * Sets the strings of the subwidgets using the current language. */ @@ -190,6 +205,10 @@ namespace { void applyStyleSheet(ParameterGrp *hGrp) { + if (auto parameterManager = Base::provideService()) { + parameterManager->reload(); + } + auto sheet = hGrp->GetASCII("StyleSheet"); bool tiledBG = hGrp->GetBool("TiledBackground", false); Gui::Application::Instance->setStyleSheet(QString::fromUtf8(sheet.c_str()), tiledBG); diff --git a/src/Gui/PreferencePages/DlgSettingsUI.h b/src/Gui/PreferencePages/DlgSettingsUI.h index 4dcfbea680..37030b5c4b 100644 --- a/src/Gui/PreferencePages/DlgSettingsUI.h +++ b/src/Gui/PreferencePages/DlgSettingsUI.h @@ -62,6 +62,7 @@ protected: const char *def, QStringList filter = QStringList()); + void openThemeEditor(); private: std::unique_ptr ui; }; diff --git a/src/Gui/PreferencePages/DlgSettingsUI.ui b/src/Gui/PreferencePages/DlgSettingsUI.ui index 5bf0459cd2..2f6f72d89e 100644 --- a/src/Gui/PreferencePages/DlgSettingsUI.ui +++ b/src/Gui/PreferencePages/DlgSettingsUI.ui @@ -31,7 +31,7 @@
- + @@ -53,7 +53,7 @@ This color might be used by your theme to let you customize it. - + 85 123 @@ -119,7 +119,7 @@ This color might be used by your theme to let you customize it. - + 85 123 @@ -145,7 +145,7 @@ This color might be used by your theme to let you customize it. - + 85 123 @@ -165,12 +165,12 @@ Style sheet how user interface will look like - - MainWindow - StyleSheet + + MainWindow + @@ -178,12 +178,12 @@ - - MainWindow - OverlayActiveStyleSheet + + MainWindow + @@ -191,6 +191,13 @@ + + + + Open Theme Editor + + +
@@ -230,12 +237,12 @@ 16 - - TreeView - IconSize + + TreeView + @@ -285,12 +292,12 @@ 0 - - TreeView - ItemSpacing + + TreeView + @@ -492,7 +499,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -510,26 +517,26 @@ QPushButton
Gui/Widgets.h
- - Gui::PrefColorButton - Gui::ColorButton -
Gui/PrefWidgets.h
-
- - Gui::PrefComboBox - QComboBox -
Gui/PrefWidgets.h
-
Gui::PrefSpinBox QSpinBox
Gui/PrefWidgets.h
+ + Gui::PrefColorButton + Gui::ColorButton +
Gui/PrefWidgets.h
+
Gui::PrefCheckBox QCheckBox
Gui/PrefWidgets.h
+ + Gui::PrefComboBox + QComboBox +
Gui/PrefWidgets.h
+
ThemeAccentColor1 diff --git a/src/Gui/StyleParameters/ParameterManager.cpp b/src/Gui/StyleParameters/ParameterManager.cpp new file mode 100644 index 0000000000..24c685b096 --- /dev/null +++ b/src/Gui/StyleParameters/ParameterManager.cpp @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "ParameterManager.h" +#include "Parser.h" + +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#endif + +namespace Gui::StyleParameters +{ + +Length Length::operator+(const Length& rhs) const +{ + ensureEqualUnits(rhs); + return {value + rhs.value, unit}; +} + +Length Length::operator-(const Length& rhs) const +{ + ensureEqualUnits(rhs); + return {value - rhs.value, unit}; +} + +Length Length::operator-() const +{ + return {-value, unit}; +} + +Length Length::operator/(const Length& rhs) const +{ + if (rhs.value == 0) { + THROWM(Base::RuntimeError, "Division by zero"); + } + + if (rhs.unit.empty() || unit.empty()) { + return {value / rhs.value, unit}; + } + + ensureEqualUnits(rhs); + return {value / rhs.value, unit}; +} + +Length Length::operator*(const Length& rhs) const +{ + if (rhs.unit.empty() || unit.empty()) { + return {value * rhs.value, unit}; + } + + ensureEqualUnits(rhs); + return {value * rhs.value, unit}; +} + +void Length::ensureEqualUnits(const Length& rhs) const +{ + if (unit != rhs.unit) { + THROWM(Base::RuntimeError, + fmt::format("Units mismatch left expression is '{}', right expression is '{}'", + unit, + rhs.unit)); + } +} + +std::string Value::toString() const +{ + if (std::holds_alternative(*this)) { + auto [value, unit] = std::get(*this); + return fmt::format("{}{}", value, unit); + } + + if (std::holds_alternative(*this)) { + auto color = std::get(*this); + return fmt::format("#{:0>6x}", 0xFFFFFF & color.rgb()); // NOLINT(*-magic-numbers) + } + + return std::get(*this); +} + +ParameterSource::ParameterSource(const Metadata& metadata) + : metadata(metadata) +{} + +InMemoryParameterSource::InMemoryParameterSource(const std::list& parameters, + const Metadata& metadata) + : ParameterSource(metadata) +{ + for (const auto& parameter : parameters) { + InMemoryParameterSource::define(parameter); + } +} + +std::list InMemoryParameterSource::all() const +{ + auto values = parameters | std::ranges::views::values; + + return std::list(values.begin(), values.end()); +} + +std::optional InMemoryParameterSource::get(const std::string& name) const +{ + if (parameters.contains(name)) { + return parameters.at(name); + } + + return std::nullopt; +} + +void InMemoryParameterSource::define(const Parameter& parameter) +{ + parameters[parameter.name] = parameter; +} + +void InMemoryParameterSource::remove(const std::string& name) +{ + parameters.erase(name); +} + +BuiltInParameterSource::BuiltInParameterSource(const Metadata& metadata) + : ParameterSource(metadata) +{ + this->metadata.options |= ReadOnly; +} + +std::list BuiltInParameterSource::all() const +{ + std::list result; + + for (const auto& name : params | std::views::keys) { + result.push_back(*get(name)); + } + + return result; +} + +std::optional BuiltInParameterSource::get(const std::string& name) const +{ + if (params.contains(name)) { + unsigned long color = params.at(name)->GetUnsigned(name.c_str(), 0); + + return Parameter { + .name = name, + .value = fmt::format("#{:0>6x}", 0x00FFFFFF & (color >> 8)), // NOLINT(*-magic-numbers) + }; + } + + return std::nullopt; +} + +UserParameterSource::UserParameterSource(ParameterGrp::handle hGrp, const Metadata& metadata) + : ParameterSource(metadata) + , hGrp(hGrp) +{} + +std::list UserParameterSource::all() const +{ + std::list result; + + for (const auto& [token, value] : hGrp->GetASCIIMap()) { + result.push_back({ + .name = token, + .value = value, + }); + } + + return result; +} + +std::optional UserParameterSource::get(const std::string& name) const +{ + if (auto value = hGrp->GetASCII(name.c_str(), ""); !value.empty()) { + return Parameter { + .name = name, + .value = value, + }; + } + + return {}; +} + +void UserParameterSource::define(const Parameter& parameter) +{ + hGrp->SetASCII(parameter.name.c_str(), parameter.value); +} + +void UserParameterSource::remove(const std::string& name) +{ + hGrp->RemoveASCII(name.c_str()); +} + +ParameterManager::ParameterManager() = default; + +void ParameterManager::reload() +{ + _resolved.clear(); +} + +std::string ParameterManager::replacePlaceholders(const std::string& expression, + ResolveContext context) const +{ + static const QRegularExpression regex(QStringLiteral("@(\\w+)")); + + auto substituteWithCallback = + [](const QRegularExpression& regex, + const QString& input, + const std::function& callback) { + QRegularExpressionMatchIterator it = regex.globalMatch(input); + + QString result; + qsizetype lastIndex = 0; + + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + + qsizetype start = match.capturedStart(); + qsizetype end = match.capturedEnd(); + + result += input.mid(lastIndex, start - lastIndex); + result += callback(match); + + lastIndex = end; + } + + // Append any remaining text after the last match + result += input.mid(lastIndex); + + return result; + }; + + // clang-format off + return substituteWithCallback( + regex, + QString::fromStdString(expression), + [&](const QRegularExpressionMatch& match) { + auto tokenName = match.captured(1).toStdString(); + + auto tokenValue = resolve(tokenName, context); + context.visited.erase(tokenName); + + return QString::fromStdString(tokenValue.toString()); + } + ).toStdString(); + // clang-format on +} + +std::list ParameterManager::parameters() const +{ + std::set result; + + // we need to traverse it in reverse order so more important tokens will take precedence + for (const ParameterSource* source : _sources | std::views::reverse) { + for (const Parameter& parameter : source->all()) { + result.insert(parameter); + } + } + + return std::list(result.begin(), result.end()); +} + +std::optional ParameterManager::expression(const std::string& name) const +{ + if (auto param = parameter(name)) { + return param->value; + } + + return {}; +} + +Value ParameterManager::resolve(const std::string& name, ResolveContext context) const +{ + std::optional maybeParameter = this->parameter(name); + + if (!maybeParameter) { + Base::Console().warning("Requested non-existent design token '%s'.", name); + return std::string {}; + } + + if (context.visited.contains(name)) { + Base::Console().warning("The design token '%s' contains circular-reference.", name); + return expression(name).value_or(std::string {}); + } + + const Parameter& token = *maybeParameter; + + if (!_resolved.contains(token.name)) { + context.visited.insert(token.name); + try { + _resolved[token.name] = evaluate(token.value, context); + } + catch (Base::Exception&) { + // in case of being unable to parse it, we need to treat it as a generic value + _resolved[token.name] = replacePlaceholders(token.value, context); + } + context.visited.erase(token.name); + } + + return _resolved[token.name]; +} + +Value ParameterManager::evaluate(const std::string& expression, ResolveContext context) const +{ + Parser parser(expression); + return parser.parse()->evaluate({.manager = this, .context = std::move(context)}); +} + +std::optional ParameterManager::parameter(const std::string& name) const +{ + for (const ParameterSource* source : _sources) { + if (const auto& parameter = source->get(name)) { + return parameter; + } + } + + return {}; +} + +void ParameterManager::addSource(ParameterSource* source) +{ + _sources.push_front(source); +} + +std::list ParameterManager::sources() const +{ + return _sources; +} + +} // namespace Gui::StyleParameters \ No newline at end of file diff --git a/src/Gui/StyleParameters/ParameterManager.h b/src/Gui/StyleParameters/ParameterManager.h new file mode 100644 index 0000000000..8736755c99 --- /dev/null +++ b/src/Gui/StyleParameters/ParameterManager.h @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef STYLEPARAMETERS_PARAMETERMANAGER_H +#define STYLEPARAMETERS_PARAMETERMANAGER_H + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace Gui::StyleParameters +{ + +// Forward declaration for Parser +class Parser; + +/** + * @brief Represents a length in a specified unit. + * + * This struct is a very simplified representation of lengths that can be used as parameters for + * styling purposes. The length basically consists of value and unit. Unit is optional, empty unit + * represents a dimensionless length that can be used as a scalar. This struct does not care about + * unit conversions as its uses do not require it. + */ +struct Length +{ + /// Numeric value of the length. + double value; + /// Unit of the length, empty if the value is dimensionless. + std::string unit; + + /** + * @name Operators + * + * This struct supports basic operations on Length. Each operation requires for operands to be + * the same unit. Multiplication and division additionally allow one operand to be dimensionless + * and hence act as a scalar. + * + * @code{c++} + * Length a { 10, "px" }; + * Length b { 5, "px" }; + * + * Length differentUnit { 3, "rem" } + * Length scalar { 2, "" }; + * + * // basic operations with the same unit are allowed + * auto sum = a + b; // 15 px + * auto difference = a - 5; // 10 px + * + * // basic operations with mixed units are NOT allowed + * auto sumOfIncompatibleUnits = a + differentUnit; // will throw + * auto productOfIncompatibleUnits = a * differentUnit; // will throw + * + * // exception is that for multiplication and division dimensionless units are allowed + * auto productWithScalar = a * scalar; // 20 px + * @endcode + * @{ + */ + Length operator+(const Length& rhs) const; + Length operator-(const Length& rhs) const; + Length operator-() const; + + Length operator/(const Length& rhs) const; + Length operator*(const Length& rhs) const; + /// @} + +private: + void ensureEqualUnits(const Length& rhs) const; +}; + +/** + * @brief This struct represents any valid value that can be used as the parameter value. + * + * The value can be one of three basic types: + * - Numbers / Lengths (so any length with optional unit) (Length) + * - Colors (QColor) + * - Any other generic expression. (std::string) + * + * As a rule, operations can be only performed over values of the same type. + */ +struct Value : std::variant +{ + using std::variant::variant; + + /** + * Converts the object into its string representation. + * + * @return A string representation of the object that can later be used in QSS. + */ + std::string toString() const; +}; + +/** + * @struct Parameter + * + * @brief Represents a named, dynamic expression-based parameter. + * + * The Parameter structure is used to define reusable named variables in styling or layout systems. + * Each parameter consists of a `name` and a `value` string, where the value is a CSS-like expression + * that supports numbers, units, arithmetic, colors, functions, and parameter references. + * + * ### Naming Convention + * Parameter names must be unique and follow **CamelCase**. + */ +struct Parameter +{ + /// Comparator that assumes that parameters are equal as long as name is the same + struct NameComparator + { + bool operator()(const Parameter& lhs, const Parameter& rhs) const + { + return lhs.name < rhs.name; + } + }; + + /// Name of the parameter, name should follow CamelCase + std::string name; + /// Expression associated with the parameter + std::string value; +}; + +enum class ParameterSourceOption +{ + // clang-format off + /// Parameters are read-only and the source does not allow editing + ReadOnly = 1 << 0, + /// Parameters are expected to be edited by the user, not only theme developers + UserEditable = 1 << 1, + // clang-format on +}; + +using ParameterSourceOptions = Base::Flags; + +/** + * @brief Abstract base class representing a source of style parameters. + * + * A `ParameterSource` is responsible for managing a collection of named parameters. Each source + * has metadata describing its type, characteristics, and behavior. + * + * ### Key Responsibilities + * - Define, update, and remove parameters within the source. + * - Provide access to all parameters or specific ones by name. + * - Act as a backend for parameter management, feeding the `ParameterManager` with available + * parameter data. + * + * ### Metadata + * Each parameter source includes metadata consisting of: + * - `name`: Name of the source, for identification purposes. + * - `options`: Flags specifying optional behavior (e.g., `ReadOnly`, `UserEditable`). + * + * ### Notes on Usage + * - Subclasses of `ParameterSource` (e.g., `BuiltInParameterSource`, `UserParameterSource`) + * implement different storage mechanisms and behaviors based on whether parameters are + * pre-defined, user-defined, or loaded in memory. + * - Parameters can be retrieved and manipulated globally through the `ParameterManager`, which + * aggregates multiple `ParameterSource` instances. + * + * #### Example + * @code{.cpp} + * // Create an in-memory parameter source + * InMemoryParameterSource source({ + * Parameter{ "BasePadding", "16px" }, + * Parameter{ "DefaultColor", "#ff00ff" }, + * }); + * + * source.define(Parameter{ "Margin", "4px" }); // Adds a new parameter + * + * auto padding = source.get("BasePadding"); // Retrieves parameter named "BasePadding" + * auto parametersList = source.all(); // Retrieve all parameters + * @endcode + * + * ### Subclass Requirements + * Derived classes must implement: + * - `all()` - Retrieve all parameters in the source. + * - `get()` - Retrieve a specific parameter. + * - `define()` - Add or update a parameter, can be left empty for readonly sources. + * - `remove()` - Remove a parameter by name, can be left empty for readonly sources. + */ +class GuiExport ParameterSource +{ +public: + using enum ParameterSourceOption; + + /** + * @brief Contains metadata information about a `ParameterSource`. + * + * The `Metadata` struct provides a way to describe the characteristics and identity + * of a `ParameterSource`. It includes a name for identification and a set of options + * that define the source's behavior and restrictions. + */ + struct Metadata + { + /// The name of the parameter source. Should be marked for translation using QT_TR_NOOP + std::string name; + /// Flags defining the behavior and properties of the parameter source. + ParameterSourceOptions options {}; + }; + + /// Metadata of the parameter source + Metadata metadata; + + FC_DEFAULT_MOVE(ParameterSource); + FC_DISABLE_COPY(ParameterSource); + + explicit ParameterSource(const Metadata& metadata); + virtual ~ParameterSource() = default; + + /** + * @brief Retrieves a list of all parameters available in the source. + * + * This method returns every parameter defined within this source, enabling iteration and bulk + * access to all stored values. + * + * @return A list containing all `Parameter` objects stored in the source. + */ + virtual std::list all() const = 0; + + /** + * @brief Retrieves a specific parameter by its name. + * + * @param[in] name The name of the parameter to retrieve. + * @return An optional containing the requested parameter if it exists, or empty if not. + */ + virtual std::optional get(const std::string& name) const = 0; + + /** + * @brief Defines or updates a parameter in the source. + * + * Adds a new parameter to the source if it doesn't already exist or updates the value of an + * existing parameter with the same name. + * + * @param[in] parameter The `Parameter` object to define or update in the source. + */ + virtual void define([[maybe_unused]] const Parameter& parameter) {} + + /** + * @brief Removes a parameter from the source by its name. + * + * Deletes the specific parameter from the source if it exists. If no parameter with the given + * name is found, the method does nothing. + * + * @param[in] name The name of the parameter to remove. + */ + virtual void remove([[maybe_unused]] const std::string& name) {} +}; + +/** + * @brief In-memory parameter source that stores parameters in a map. + * + * This source is useful for temporary parameter storage or when you need to + * define parameters programmatically without persisting them to disk. + */ +class GuiExport InMemoryParameterSource : public ParameterSource +{ + std::map parameters; + +public: + InMemoryParameterSource(const std::list& parameters, const Metadata& metadata); + + std::list all() const override; + std::optional get(const std::string& name) const override; + void define(const Parameter& parameter) override; + void remove(const std::string& name) override; +}; + +/** + * @brief Built-in parameter source that reads from FreeCAD's parameter system. + * + * This source provides access to predefined parameters that are stored in + * FreeCAD's global parameter system. These parameters are typically defined + * by the application and are read-only. + */ +class GuiExport BuiltInParameterSource : public ParameterSource +{ +public: + explicit BuiltInParameterSource(const Metadata& metadata = {}); + + std::list all() const override; + std::optional get(const std::string& name) const override; + +private: + ParameterGrp::handle hGrpThemes = + App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Themes"); + ParameterGrp::handle hGrpView = + App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/View"); + + std::map params = { + {"ThemeAccentColor1", hGrpThemes}, + {"ThemeAccentColor2", hGrpThemes}, + {"ThemeAccentColor3", hGrpThemes}, + {"BackgroundColor", hGrpView}, + }; +}; + +/** + * @brief User-defined parameter source that reads from user preferences. + * + * This source provides access to user-defined parameters that are stored + * in the user's preference file. These parameters can be modified by the + * user and persist across application sessions. + */ +class GuiExport UserParameterSource : public ParameterSource +{ + ParameterGrp::handle hGrp; + +public: + UserParameterSource(ParameterGrp::handle hGrp, const Metadata& metadata); + + std::list all() const override; + std::optional get(const std::string& name) const override; + void define(const Parameter& parameter) override; + void remove(const std::string& name) override; +}; + +/** + * @brief Central manager for style parameters that aggregates multiple sources. + * + * The ParameterManager is responsible for: + * - Managing multiple parameter sources + * - Resolving parameter references and expressions + * - Caching resolved values for performance + * - Handling circular references + */ +class GuiExport ParameterManager +{ + std::list _sources; + mutable std::map _resolved; + +public: + struct ResolveContext + { + /// Names of parameters currently being resolved. + std::set visited; + }; + + ParameterManager(); + + /** + * @brief Clears the internal cache of resolved values. + * + * Call this method when parameters have been modified to ensure + * that the changes are reflected in subsequent resolutions. + */ + void reload(); + + /** + * @brief Replaces parameter placeholders in a string with their resolved values. + * + * This method performs simple string substitution of @parameter references + * with their actual values. It does not evaluate expressions, only performs + * direct substitution. + * + * @param expression The string containing parameter placeholders + * @param context Resolution context for handling circular references + * @return The string with all placeholders replaced + */ + std::string replacePlaceholders(const std::string& expression, ResolveContext context = {}) const; + + /** + * @brief Returns all available parameters from all sources. + * + * Parameters are returned in order of source priority, with later sources + * taking precedence over earlier ones. + * + * @return List of all available parameters + */ + std::list parameters() const; + + /** + * @brief Gets the raw expression string for a parameter. + * + * @param name The name of the parameter + * @return The expression string if the parameter exists, empty otherwise + */ + std::optional expression(const std::string& name) const; + + /** + * @brief Resolves a parameter to its final value. + * + * This method evaluates the parameter's expression and returns the computed + * value. The result is cached for subsequent calls. + * + * @param name The name of the parameter to resolve + * @param context Resolution context for handling circular references + * @return The resolved value + */ + Value resolve(const std::string& name, ResolveContext context = {}) const; + + /** + * @brief Evaluates an expression string and returns the result. + * + * @param expression The expression to evaluate + * @param context Resolution context for handling circular references + * @return The evaluated value + */ + Value evaluate(const std::string& expression, ResolveContext context = {}) const; + + /** + * @brief Gets a parameter by name from any source. + * + * @param name The name of the parameter + * @return The parameter if found, empty otherwise + */ + std::optional parameter(const std::string& name) const; + + /** + * @brief Adds a parameter source to the manager. + * + * Sources are evaluated in the order they are added, with later sources + * taking precedence over earlier ones. + * + * @param source The parameter source to add + */ + void addSource(ParameterSource* source); + + /** + * @brief Returns all registered parameter sources. + * + * @return List of parameter sources in order of registration + */ + std::list sources() const; +}; + +} // namespace Gui::StyleParameters + +ENABLE_BITMASK_OPERATORS(Gui::StyleParameters::ParameterSourceOption); + +#endif // STYLEPARAMETERS_PARAMETERMANAGER_H \ No newline at end of file diff --git a/src/Gui/StyleParameters/Parser.cpp b/src/Gui/StyleParameters/Parser.cpp new file mode 100644 index 0000000000..dbcb52f27a --- /dev/null +++ b/src/Gui/StyleParameters/Parser.cpp @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "Parser.h" +#include "ParameterManager.h" + +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#endif + +namespace Gui::StyleParameters +{ + +Value ParameterReference::evaluate(const EvaluationContext& context) const +{ + return context.manager->resolve(name, context.context); +} + +Value Number::evaluate([[maybe_unused]] const EvaluationContext& context) const +{ + return value; +} + +Value Color::evaluate([[maybe_unused]] const EvaluationContext& context) const +{ + return color; +} + +Value FunctionCall::evaluate(const EvaluationContext& context) const +{ + if (arguments.size() != 2) { + THROWM(Base::ExpressionError, + fmt::format("Function '{}' expects 2 arguments, got {}", + functionName, + arguments.size())); + } + + auto colorArg = arguments[0]->evaluate(context); + auto amountArg = arguments[1]->evaluate(context); + + if (!std::holds_alternative(colorArg)) { + THROWM(Base::ExpressionError, + fmt::format("'{}' is not supported for colors", functionName)); + } + + auto color = std::get(colorArg); + + // In Qt if you want to make color 20% darker or lighter, you need to pass 120 as the value + // we, however, want users to pass only the relative difference, hence we need to add the + // 100 required by Qt. + // + // NOLINTNEXTLINE(*-magic-numbers) + auto amount = 100 + static_cast(std::get(amountArg).value); + + if (functionName == "lighten") { + return color.lighter(amount); + } + + if (functionName == "darken") { + return color.darker(amount); + } + + THROWM(Base::ExpressionError, fmt::format("Unknown function '{}'", functionName)); +} + +Value BinaryOp::evaluate(const EvaluationContext& context) const +{ + Value lval = left->evaluate(context); + Value rval = right->evaluate(context); + + if (!std::holds_alternative(lval) || !std::holds_alternative(rval)) { + THROWM(Base::ExpressionError, "Math operations are supported only on lengths"); + } + + auto lhs = std::get(lval); + auto rhs = std::get(rval); + + switch (op) { + case Operator::Add: + return lhs + rhs; + case Operator::Subtract: + return lhs - rhs; + case Operator::Multiply: + return lhs * rhs; + case Operator::Divide: + return lhs / rhs; + default: + THROWM(Base::ExpressionError, "Unknown operator"); + } +} + +Value UnaryOp::evaluate(const EvaluationContext& context) const +{ + Value val = operand->evaluate(context); + if (std::holds_alternative(val)) { + THROWM(Base::ExpressionError, "Unary operations on colors are not supported"); + } + + auto v = std::get(val); + switch (op) { + case Operator::Add: + return v; + case Operator::Subtract: + return -v; + default: + THROWM(Base::ExpressionError, "Unknown unary operator"); + } +} + +std::unique_ptr Parser::parse() +{ + auto expr = parseExpression(); + skipWhitespace(); + if (pos != input.size()) { + THROWM(Base::ParserError, + fmt::format("Unexpected characters at end of input: {}", input.substr(pos))); + } + return expr; +} + +bool Parser::peekString(const char* function) const +{ + return input.compare(pos, strlen(function), function) == 0; +} + +std::unique_ptr Parser::parseExpression() +{ + auto expr = parseTerm(); + while (true) { + skipWhitespace(); + if (match('+')) { + expr = std::make_unique(std::move(expr), Operator::Add, parseTerm()); + } + else if (match('-')) { + expr = std::make_unique(std::move(expr), Operator::Subtract, parseTerm()); + } + else { + break; + } + } + return expr; +} + +std::unique_ptr Parser::parseTerm() +{ + auto expr = parseFactor(); + while (true) { + skipWhitespace(); + if (match('*')) { + expr = std::make_unique(std::move(expr), Operator::Multiply, parseFactor()); + } + else if (match('/')) { + expr = std::make_unique(std::move(expr), Operator::Divide, parseFactor()); + } + else { + break; + } + } + return expr; +} + +std::unique_ptr Parser::parseFactor() +{ + skipWhitespace(); + if (match('+') || match('-')) { + Operator op = (input[pos - 1] == '+') ? Operator::Add : Operator::Subtract; + return std::make_unique(op, parseFactor()); + } + if (match('(')) { + auto expr = parseExpression(); + if (!match(')')) { + THROWM(Base::ParserError, fmt::format("Expected ')', got '{}'", input[pos])); + } + return expr; + } + if (peekColor()) { + return parseColor(); + } + if (peekParameter()) { + return parseParameter(); + } + if (peekFunction()) { + return parseFunctionCall(); + } + return parseNumber(); +} + +bool Parser::peekColor() +{ + skipWhitespace(); + // clang-format off + return input[pos] == '#' + || peekString(rgbFunction) + || peekString(rgbaFunction); + // clang-format on +} + +std::unique_ptr Parser::parseColor() +{ + const auto parseHexadecimalColor = [&]() { + constexpr int hexadecimalBase = 16; + + // Format is #RRGGBB + pos++; + int r = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); + pos += 2; + int g = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); + pos += 2; + int b = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); + pos += 2; + + return std::make_unique(QColor(r, g, b)); + }; + + const auto parseFunctionStyleColor = [&]() { + bool hasAlpha = peekString(rgbaFunction); + + pos += hasAlpha ? strlen(rgbaFunction) : strlen(rgbFunction); + + int r = parseInt(); + if (!match(',')) { + THROWM(Base::ParserError, fmt::format("Expected ',' after red, got '{}'", input[pos])); + } + int g = parseInt(); + if (!match(',')) { + THROWM(Base::ParserError, fmt::format("Expected ',' after green, got '{}'", input[pos])); + } + int b = parseInt(); + int a = 255; // NOLINT(*-magic-numbers) + if (hasAlpha) { + if (!match(',')) { + THROWM(Base::ParserError, fmt::format("Expected ',' after blue, got '{}'", input[pos])); + } + a = parseInt(); + } + if (!match(')')) { + THROWM(Base::ParserError, fmt::format("Expected ')' after color arguments, got '{}'", input[pos])); + } + return std::make_unique(QColor(r, g, b, a)); + }; + + skipWhitespace(); + + try { + if (input[pos] == '#') { + return parseHexadecimalColor(); + } + + if (peekString(rgbFunction) || peekString(rgbaFunction)) { + return parseFunctionStyleColor(); + } + } catch (std::invalid_argument&) { + THROWM(Base::ParserError, "Invalid color format, expected #RRGGBB or rgb(r,g,b) or rgba(r,g,b,a)"); + } + + THROWM(Base::ParserError, "Unknown color format"); +} + +bool Parser::peekParameter() +{ + skipWhitespace(); + return pos < input.size() && input[pos] == '@'; +} + +std::unique_ptr Parser::parseParameter() +{ + skipWhitespace(); + if (!match('@')) { + THROWM(Base::ParserError, fmt::format("Expected '@' for parameter, got '{}'", input[pos])); + } + size_t start = pos; + while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_')) { + ++pos; + } + if (start == pos) { + THROWM(Base::ParserError, + fmt::format("Expected parameter name after '@', got '{}'", input[pos])); + } + return std::make_unique(input.substr(start, pos - start)); +} + +bool Parser::peekFunction() +{ + skipWhitespace(); + return pos < input.size() && isalpha(input[pos]); +} + +std::unique_ptr Parser::parseFunctionCall() +{ + skipWhitespace(); + size_t start = pos; + while (pos < input.size() && isalnum(input[pos])) { + ++pos; + } + std::string functionName = input.substr(start, pos - start); + + if (!match('(')) { + THROWM(Base::ParserError, + fmt::format("Expected '(' after function name, got '{}'", input[pos])); + } + + std::vector> arguments; + if (!match(')')) { + do { // NOLINT(*-avoid-do-while) + arguments.push_back(parseExpression()); + } while (match(',')); + + if (!match(')')) { + THROWM(Base::ParserError, + fmt::format("Expected ')' after function arguments, got '{}'", input[pos])); + } + } + + return std::make_unique(functionName, std::move(arguments)); +} + +int Parser::parseInt() +{ + skipWhitespace(); + size_t start = pos; + while (pos < input.size() && (isdigit(input[pos]) || input[pos] == '.')) { + ++pos; + } + return std::stoi(input.substr(start, pos - start)); +} + +std::unique_ptr Parser::parseNumber() +{ + skipWhitespace(); + size_t start = pos; + while (pos < input.size() && (isdigit(input[pos]) || input[pos] == '.')) { + ++pos; + } + + std::string number = input.substr(start, pos - start); + + try { + double value = std::stod(number); + std::string unit = parseUnit(); + return std::make_unique(value, unit); + } + catch (std::invalid_argument& e) { + THROWM(Base::ParserError, fmt::format("Invalid number: {}", number)); + } +} + +std::string Parser::parseUnit() +{ + skipWhitespace(); + size_t start = pos; + while (pos < input.size() && (isalpha(input[pos]) || input[pos] == '%')) { + ++pos; + } + if (start == pos) { + return ""; + } + return input.substr(start, pos - start); +} + +bool Parser::match(char expected) +{ + skipWhitespace(); + if (pos < input.size() && input[pos] == expected) { + ++pos; + return true; + } + return false; +} + +void Parser::skipWhitespace() +{ + while (pos < input.size() && isspace(input[pos])) { + ++pos; + } +} + +} // namespace Gui::StyleParameters \ No newline at end of file diff --git a/src/Gui/StyleParameters/Parser.h b/src/Gui/StyleParameters/Parser.h new file mode 100644 index 0000000000..3e7867cae9 --- /dev/null +++ b/src/Gui/StyleParameters/Parser.h @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef STYLEPARAMETERS_PARSER_H +#define STYLEPARAMETERS_PARSER_H + +#include +#include +#include + +#include "ParameterManager.h" + +namespace Gui::StyleParameters +{ + +enum class Operator : std::uint8_t +{ + Add, + Subtract, + Multiply, + Divide +}; + +struct EvaluationContext +{ + const ParameterManager* manager {}; + ParameterManager::ResolveContext context; +}; + +// Abstract Syntax Tree (AST) Base +struct GuiExport Expr +{ + Expr() = default; + + FC_DEFAULT_MOVE(Expr); + FC_DISABLE_COPY(Expr); + + virtual Value evaluate(const EvaluationContext& context) const = 0; + virtual ~Expr() = default; +}; + +struct GuiExport ParameterReference: public Expr +{ + std::string name; + + explicit ParameterReference(std::string name) + : name(std::move(name)) + {} + + Value evaluate(const EvaluationContext& context) const override; +}; + +struct GuiExport Number: public Expr +{ + Length value; + + Number(double value, std::string unit) + : value({value, std::move(unit)}) + {} + + Value evaluate([[maybe_unused]] const EvaluationContext& context) const override; +}; + +struct GuiExport Color: public Expr +{ + QColor color; + + explicit Color(QColor color) + : color(std::move(color)) + {} + + Value evaluate([[maybe_unused]] const EvaluationContext& context) const override; +}; + +struct GuiExport FunctionCall: public Expr +{ + std::string functionName; + std::vector> arguments; + + FunctionCall(std::string functionName, std::vector> arguments) + : functionName(std::move(functionName)) + , arguments(std::move(arguments)) + {} + + Value evaluate(const EvaluationContext& context) const override; +}; + +struct GuiExport BinaryOp: public Expr +{ + std::unique_ptr left, right; + Operator op; + + BinaryOp(std::unique_ptr left, Operator op, std::unique_ptr right) + : left(std::move(left)) + , right(std::move(right)) + , op(op) + {} + + Value evaluate(const EvaluationContext& context) const override; +}; + +struct GuiExport UnaryOp: public Expr +{ + Operator op; + std::unique_ptr operand; + + UnaryOp(Operator op, std::unique_ptr operand) + : op(op) + , operand(std::move(operand)) + {} + + Value evaluate(const EvaluationContext& context) const override; +}; + +class GuiExport Parser +{ + static constexpr auto rgbFunction = "rgb("; + static constexpr auto rgbaFunction = "rgba("; + + std::string input; + size_t pos = 0; + +public: + explicit Parser(std::string input) + : input(std::move(input)) + {} + + std::unique_ptr parse(); + +private: + bool peekString(const char* function) const; + std::unique_ptr parseExpression(); + std::unique_ptr parseTerm(); + std::unique_ptr parseFactor(); + bool peekColor(); + std::unique_ptr parseColor(); + bool peekParameter(); + std::unique_ptr parseParameter(); + bool peekFunction(); + std::unique_ptr parseFunctionCall(); + int parseInt(); + std::unique_ptr parseNumber(); + std::string parseUnit(); + bool match(char expected); + void skipWhitespace(); +}; + +} // namespace Gui::StyleParameters + +#endif // STYLEPARAMETERS_PARSER_H \ No newline at end of file diff --git a/src/Gui/Tools.h b/src/Gui/Tools.h index 29f5741a68..364da69ff1 100644 --- a/src/Gui/Tools.h +++ b/src/Gui/Tools.h @@ -1,5 +1,6 @@ /*************************************************************************** * Copyright (c) 2020 Werner Mayer * + * Copyright (c) 2025 Kacper Donat * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,39 +24,100 @@ #ifndef GUI_TOOLS_H #define GUI_TOOLS_H +#include + #include #include #include -#include +#include -namespace Gui { - -/*! - * \brief The QtTools class - * Helper class to reduce adding a lot of extra QT_VERSION checks to client code. +/** + * @brief The QtTools namespace + * + * Helper namespace to provide utilities to ease work with Qt. */ -class GuiExport QtTools { -public: - static int horizontalAdvance(const QFontMetrics& fm, QChar ch) { +namespace Gui::QtTools +{ +inline int horizontalAdvance(const QFontMetrics& fm, QChar ch) +{ + return fm.horizontalAdvance(ch); +} - return fm.horizontalAdvance(ch); - } - static int horizontalAdvance(const QFontMetrics& fm, const QString& text, int len = -1) { - return fm.horizontalAdvance(text, len); - } - static bool matches(QKeyEvent* ke, const QKeySequence& ks) { - uint searchkey = (ke->modifiers() | ke->key()) & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier); - return ks == QKeySequence(searchkey); - } - static QKeySequence::StandardKey deleteKeySequence() { +inline int horizontalAdvance(const QFontMetrics& fm, const QString& text, int len = -1) +{ + return fm.horizontalAdvance(text, len); +} + +inline bool matches(QKeyEvent* ke, const QKeySequence& ks) +{ + uint searchkey = + (ke->modifiers() | ke->key()) & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier); + return ks == QKeySequence(searchkey); +} + +inline QKeySequence::StandardKey deleteKeySequence() { #ifdef FC_OS_MACOSX - return QKeySequence::Backspace; + return QKeySequence::Backspace; #else - return QKeySequence::Delete; + return QKeySequence::Delete; #endif +} + +// clang-format off +/** + * TreeWalkCallable is a function that takes const QModelIndex& and: + * - returns void, if there is no stopping logic; + * - returns boolean, if there is logic that should stop tree traversal. + */ +template +concept TreeWalkCallable = + std::is_invocable_r_v || + std::is_invocable_r_v; +// clang-format on + +/** + * @brief Recursively traverses a QAbstractItemModel tree structure. + * + * The function traverses a tree model starting from a given index, or the root + * if no index is provided. For each node, it invokes the provided callable `func`. + * + * The callable can: + * - Return `void`, in which case the traversal continues through all nodes. + * - Return `bool`, in which case returning `true` stops further traversal. + * + * @param[in] model The tree model to traverse. + * @param[in] func A callable object applied to each QModelIndex. It can either + * return `void` or `bool` (for stopping logic). + * @param[in] index The starting index for traversal. If omitted, defaults to the root. + */ +void walkTreeModel(const QAbstractItemModel* model, + TreeWalkCallable auto&& func, + const QModelIndex& index = {}) +{ + using ReturnType = std::invoke_result_t; + + if (index.isValid()) { + if constexpr (std::is_same_v) { + func(index); + } + else if constexpr (std::is_same_v) { + if (func(index)) { + return; + } + } } -}; -} // namespace Gui + for (int i = 0; i < model->rowCount(index); ++i) { + walkTreeModel(model, func, model->index(i, 0, index)); + } +} -#endif // GUI_TOOLS_H +template +T valueOr(const QVariant& variant, const T& defaultValue) +{ + return variant.canConvert() ? variant.value() : defaultValue; +} + +} // namespace Gui::QtTools + +#endif // GUI_TOOLS_H diff --git a/tests/src/Gui/CMakeLists.txt b/tests/src/Gui/CMakeLists.txt index 774cf35f31..2c1e3f8ded 100644 --- a/tests/src/Gui/CMakeLists.txt +++ b/tests/src/Gui/CMakeLists.txt @@ -2,6 +2,9 @@ target_sources(Tests_run PRIVATE Assistant.cpp Camera.cpp + StyleParameters/StyleParametersApplicationTest.cpp + StyleParameters/ParserTest.cpp + StyleParameters/ParameterManagerTest.cpp ) # Qt tests diff --git a/tests/src/Gui/StyleParameters/ParameterManagerTest.cpp b/tests/src/Gui/StyleParameters/ParameterManagerTest.cpp new file mode 100644 index 0000000000..09c40cf13b --- /dev/null +++ b/tests/src/Gui/StyleParameters/ParameterManagerTest.cpp @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#include + +#include +#include + +using namespace Gui::StyleParameters; + +class ParameterManagerTest: public ::testing::Test +{ +protected: + void SetUp() override + { + // Create test sources + auto source1 = std::make_unique( + std::list { + {"BaseSize", "8px"}, + {"PrimaryColor", "#ff0000"}, + {"SecondaryColor", "#00ff00"}, + }, + ParameterSource::Metadata {"Source 1"}); + + auto source2 = std::make_unique( + std::list { + {"BaseSize", "16px"}, // Override from source1 + {"Margin", "@BaseSize * 2"}, + {"Padding", "@BaseSize / 2"}, + }, + ParameterSource::Metadata {"Source 2"}); + + manager.addSource(source1.get()); + manager.addSource(source2.get()); + sources.push_back(std::move(source1)); + sources.push_back(std::move(source2)); + } + + Gui::StyleParameters::ParameterManager manager; + std::vector> sources; +}; + +// Test basic parameter resolution +TEST_F(ParameterManagerTest, BasicParameterResolution) +{ + { + auto result = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 16.0); // Should get value from source2 (later source) + EXPECT_EQ(length.unit, "px"); + } + + { + auto result = manager.resolve("PrimaryColor"); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + { + auto result = manager.resolve("SecondaryColor"); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 0); + EXPECT_EQ(color.green(), 255); + EXPECT_EQ(color.blue(), 0); + } +} + +// Test parameter references +TEST_F(ParameterManagerTest, ParameterReferences) +{ + { + auto result = manager.resolve("Margin"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 32.0); // @BaseSize * 2 = 16 * 2 = 32 + EXPECT_EQ(length.unit, "px"); + } + + { + auto result = manager.resolve("Padding"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 8.0); // @BaseSize / 2 = 16 / 2 = 8 + EXPECT_EQ(length.unit, "px"); + } +} + +// Test caching +TEST_F(ParameterManagerTest, Caching) +{ + // First resolution should cache the result + auto result1 = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result1)); + + // Second resolution should use cached value + auto result2 = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result2)); + + // Results should be identical + auto length1 = std::get(result1); + auto length2 = std::get(result2); + EXPECT_DOUBLE_EQ(length1.value, length2.value); + EXPECT_EQ(length1.unit, length2.unit); +} + +// Test cache invalidation +TEST_F(ParameterManagerTest, CacheInvalidation) +{ + // Initial resolution + auto result1 = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result1)); + auto length1 = std::get(result1); + EXPECT_DOUBLE_EQ(length1.value, 16.0); + + // Reload should clear cache + manager.reload(); + + // Resolution after reload should work the same + auto result2 = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result2)); + auto length2 = std::get(result2); + EXPECT_DOUBLE_EQ(length2.value, 16.0); + EXPECT_EQ(length1.unit, length2.unit); +} + +// Test source priority +TEST_F(ParameterManagerTest, SourcePriority) +{ + // Create a third source with higher priority + auto source3 = std::make_unique( + std::list { + {"BaseSize", "24px"}, // Should override both previous sources + }, + ParameterSource::Metadata {"Source 3"}); + + manager.addSource(source3.get()); + sources.push_back(std::move(source3)); + + // Should get value from the latest source (highest priority) + auto result = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 24.0); + EXPECT_EQ(length.unit, "px"); +} + +// Test parameter listing +TEST_F(ParameterManagerTest, ParameterListing) +{ + auto params = manager.parameters(); + + // Should contain all parameters from all sources + std::set paramNames; + for (const auto& param : params) { + paramNames.insert(param.name); + } + + EXPECT_TRUE(paramNames.contains("BaseSize")); + EXPECT_TRUE(paramNames.contains("PrimaryColor")); + EXPECT_TRUE(paramNames.contains("SecondaryColor")); + EXPECT_TRUE(paramNames.contains("Margin")); + EXPECT_TRUE(paramNames.contains("Padding")); + + // Should not contain duplicates (BaseSize should appear only once) + EXPECT_EQ(paramNames.count("BaseSize"), 1); +} + +// Test expression retrieval +TEST_F(ParameterManagerTest, ExpressionRetrieval) +{ + { + auto expr = manager.expression("BaseSize"); + EXPECT_TRUE(expr.has_value()); + EXPECT_EQ(*expr, "16px"); + } + + { + auto expr = manager.expression("Margin"); + EXPECT_TRUE(expr.has_value()); + EXPECT_EQ(*expr, "@BaseSize * 2"); + } + + { + auto expr = manager.expression("NonExistent"); + EXPECT_FALSE(expr.has_value()); + } +} + +// Test parameter retrieval +TEST_F(ParameterManagerTest, ParameterRetrieval) +{ + { + auto param = manager.parameter("BaseSize"); + EXPECT_TRUE(param.has_value()); + EXPECT_EQ(param->name, "BaseSize"); + EXPECT_EQ(param->value, "16px"); + } + + { + auto param = manager.parameter("NonExistent"); + EXPECT_FALSE(param.has_value()); + } +} + +// Test source management +TEST_F(ParameterManagerTest, SourceManagement) +{ + auto sources = manager.sources(); + EXPECT_EQ(sources.size(), 2); // We added 2 sources in SetUp + + // Test that we can access the sources + for (auto source : sources) { + EXPECT_NE(source, nullptr); + auto params = source->all(); + EXPECT_FALSE(params.empty()); + } +} + +// Test circular reference detection +TEST_F(ParameterManagerTest, CircularReferenceDetection) +{ + // Create a source with circular reference + auto circularSource = std::make_unique( + std::list { + {"A", "@B"}, + {"B", "@A"}, + }, + ParameterSource::Metadata {"Circular Source"}); + + manager.addSource(circularSource.get()); + sources.push_back(std::move(circularSource)); + + // Should handle circular reference gracefully + auto result = manager.resolve("A"); + // Should return the expression string as fallback + EXPECT_TRUE(std::holds_alternative(result)); +} + +// Test complex expressions +TEST_F(ParameterManagerTest, ComplexExpressions) +{ + // Create a source with complex expressions + auto complexSource = std::make_unique( + std::list { + {"ComplexMargin", "(@BaseSize + 4px) * 2"}, + {"ComplexPadding", "(@BaseSize - 2px) / 2"}, + {"ColorWithFunction", "lighten(@PrimaryColor, 20)"}, + }, + ParameterSource::Metadata {"Complex Source"}); + + manager.addSource(complexSource.get()); + sources.push_back(std::move(complexSource)); + + { + auto result = manager.resolve("ComplexMargin"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 40.0); // (16 + 4) * 2 = 20 * 2 = 40 + EXPECT_EQ(length.unit, "px"); + } + + { + auto result = manager.resolve("ComplexPadding"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 7.0); // (16 - 2) / 2 = 14 / 2 = 7 + EXPECT_EQ(length.unit, "px"); + } + + { + auto result = manager.resolve("ColorWithFunction"); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + // Should be lighter than the original red + EXPECT_GT(color.lightness(), QColor("#ff0000").lightness()); + } +} + +// Test error handling +TEST_F(ParameterManagerTest, ErrorHandling) +{ + // Test non-existent parameter + auto result = manager.resolve("NonExistent"); + EXPECT_TRUE(std::holds_alternative(result)); + EXPECT_EQ(std::get(result), ""); + + // Test invalid expression + auto invalidSource = std::make_unique( + std::list { + {"Invalid", "invalid expression that will fail"}, + }, + ParameterSource::Metadata {"Invalid Source"}); + + manager.addSource(invalidSource.get()); + sources.push_back(std::move(invalidSource)); + + // Should handle invalid expression gracefully + auto invalidResult = manager.resolve("Invalid"); + // Should return the expression string as fallback + EXPECT_TRUE(std::holds_alternative(invalidResult)); +} diff --git a/tests/src/Gui/StyleParameters/ParserTest.cpp b/tests/src/Gui/StyleParameters/ParserTest.cpp new file mode 100644 index 0000000000..a7e1b50734 --- /dev/null +++ b/tests/src/Gui/StyleParameters/ParserTest.cpp @@ -0,0 +1,617 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#include + +#include + +#include +#include + +using namespace Gui::StyleParameters; + +class ParserTest: public ::testing::Test +{ +protected: + void SetUp() override + { + // Create a simple parameter manager for testing + auto source = std::make_unique( + std::list { + {"TestParam", "10px"}, + {"TestColor", "#ff0000"}, + {"TestNumber", "5"}, + }, + ParameterSource::Metadata {"Test Source"}); + + manager.addSource(source.get()); + sources.push_back(std::move(source)); + } + + Gui::StyleParameters::ParameterManager manager; + std::vector> sources; +}; + +// Test number parsing +TEST_F(ParserTest, ParseNumbers) +{ + { + Parser parser("42"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 42.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10.5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.5); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("2.5em"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 2.5); + EXPECT_EQ(length.unit, "em"); + } + + { + Parser parser("100%"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 100.0); + EXPECT_EQ(length.unit, "%"); + } +} + +// Test color parsing +TEST_F(ParserTest, ParseColors) +{ + { + Parser parser("#ff0000"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + { + Parser parser("#00ff00"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 0); + EXPECT_EQ(color.green(), 255); + EXPECT_EQ(color.blue(), 0); + } + + { + Parser parser("#0000ff"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 0); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 255); + } + + { + Parser parser("rgb(255, 0, 0)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + { + Parser parser("rgba(255, 0, 0, 128)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + EXPECT_EQ(color.alpha(), 128); + } +} + +// Test parameter reference parsing +TEST_F(ParserTest, ParseParameterReferences) +{ + { + Parser parser("@TestParam"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("@TestColor"); + auto expr = parser.parse(); + auto result = expr->evaluate({.manager = &manager, .context = {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + { + Parser parser("@TestNumber"); + auto expr = parser.parse(); + auto result = expr->evaluate({.manager = &manager, .context = {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, ""); + } +} + +// Test arithmetic operations +TEST_F(ParserTest, ParseArithmeticOperations) +{ + { + Parser parser("10 + 5"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px + 5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("10 - 5"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px - 5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("10 * 5"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 50.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 20.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("10 / 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px / 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, "px"); + } +} + +// Test complex expressions +TEST_F(ParserTest, ParseComplexExpressions) +{ + { + Parser parser("(10 + 5) * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 30.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("(10px + 5px) * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 30.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("@TestParam + 5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("@TestParam * @TestNumber"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 50.0); + EXPECT_EQ(length.unit, "px"); + } +} + +// Test unary operations +TEST_F(ParserTest, ParseUnaryOperations) +{ + { + Parser parser("+10"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("-10"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, -10.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("-10px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, -10.0); + EXPECT_EQ(length.unit, "px"); + } +} + +// Test function calls +TEST_F(ParserTest, ParseFunctionCalls) +{ + { + Parser parser("lighten(#ff0000, 20)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + // The result should be lighter than the original red + EXPECT_GT(color.lightness(), QColor("#ff0000").lightness()); + } + + { + Parser parser("darken(#ff0000, 20)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + // The result should be darker than the original red + EXPECT_LT(color.lightness(), QColor("#ff0000").lightness()); + } + + { + Parser parser("lighten(@TestColor, 20)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + // The result should be lighter than the original red + EXPECT_GT(color.lightness(), QColor("#ff0000").lightness()); + } +} + +// Test error cases +TEST_F(ParserTest, ParseErrors) +{ + // Invalid color format + EXPECT_THROW( + { + Parser parser("#invalid"); + parser.parse(); + }, + Base::ParserError); + + // Invalid RGB format + EXPECT_THROW( + { + Parser parser("rgb(invalid)"); + parser.parse(); + }, + Base::ParserError); + + // Missing closing parenthesis + EXPECT_THROW( + { + Parser parser("(10 + 5"); + parser.parse(); + }, + Base::ParserError); + + // Invalid function + EXPECT_THROW( + { + Parser parser("invalid()"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::ExpressionError); + + // Division by zero + EXPECT_THROW( + { + Parser parser("10 / 0"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::RuntimeError); + + // Unit mismatch + EXPECT_THROW( + { + Parser parser("10px + 5em"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::RuntimeError); + + // Unary operation on color + EXPECT_THROW( + { + Parser parser("-@TestColor"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::ExpressionError); + + // Function with wrong number of arguments + EXPECT_THROW( + { + Parser parser("lighten(#ff0000)"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::ExpressionError); + + // Function with wrong argument type + EXPECT_THROW( + { + Parser parser("lighten(10px, 20)"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::ExpressionError); +} + +// Test whitespace handling +TEST_F(ParserTest, ParseWhitespace) +{ + { + Parser parser(" 10 + 5 "); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px+5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("rgb(255,0,0)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } +} + +// Test edge cases +TEST_F(ParserTest, ParseEdgeCases) +{ + // Empty input + EXPECT_THROW( + { + Parser parser(""); + parser.parse(); + }, + Base::ParserError); + + // Just whitespace + EXPECT_THROW( + { + Parser parser(" "); + parser.parse(); + }, + Base::ParserError); + + // Single number + { + Parser parser("42"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 42.0); + EXPECT_EQ(length.unit, ""); + } + + // Single color + { + Parser parser("#ff0000"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + // Single parameter reference + { + Parser parser("@TestParam"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.0); + EXPECT_EQ(length.unit, "px"); + } +} + +// Test operator precedence +TEST_F(ParserTest, ParseOperatorPrecedence) +{ + { + Parser parser("2 + 3 * 4"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 14.0); // 2 + (3 * 4) = 2 + 12 = 14 + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10 - 3 * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 4.0); // 10 - (3 * 2) = 10 - 6 = 4 + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("20 / 4 + 3"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 8.0); // (20 / 4) + 3 = 5 + 3 = 8 + EXPECT_EQ(length.unit, ""); + } +} + +// Test nested parentheses +TEST_F(ParserTest, ParseNestedParentheses) +{ + { + Parser parser("((2 + 3) * 4)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 20.0); // (5) * 4 = 20 + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("(10 - (3 + 2)) * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.0); // (10 - 5) * 2 = 5 * 2 = 10 + EXPECT_EQ(length.unit, ""); + } +} diff --git a/tests/src/Gui/StyleParameters/StyleParametersApplicationTest.cpp b/tests/src/Gui/StyleParameters/StyleParametersApplicationTest.cpp new file mode 100644 index 0000000000..cacdfeea91 --- /dev/null +++ b/tests/src/Gui/StyleParameters/StyleParametersApplicationTest.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#include + +#include "src/App/InitApplication.h" + +#include +#include + +using namespace Gui; + +class StyleParametersApplicationTest: public ::testing::Test +{ +protected: + static Application* app; + + static void SetUpTestSuite() + { + tests::initApplication(); + app = new Application(true); + } + + void SetUp() override + { + auto styleParamManager = app->styleParameterManager(); + + styleParamManager->addSource(new StyleParameters::InMemoryParameterSource( + { + {.name = "ColorPrimary", .value = "#ff0000"}, + {.name = "FontSize", .value = "12px"}, + {.name = "BoxWidth", .value = "100px"}, + }, + {.name = "Fixture Source"})); + } +}; + +Application* StyleParametersApplicationTest::app = {}; + +// Test for replacing variables in QSS string +TEST_F(StyleParametersApplicationTest, ReplaceVariablesInQss) +{ + QString qss = "QWidget { color: @ColorPrimary; font-size: @FontSize; width: @BoxWidth; }"; + QString result = app->replaceVariablesInQss(qss); + + EXPECT_EQ(result.toStdString(), "QWidget { color: #ff0000; font-size: 12px; width: 100px; }"); +} + +// Test if unknown variables remain unchanged +TEST_F(StyleParametersApplicationTest, ReplaceVariablesInQssWithUnknownVariable) +{ + QString qss = "QWidget { color: @UnknownColor; margin: 10px; }"; + QString result = app->replaceVariablesInQss(qss); + + EXPECT_EQ(result.toStdString(), "QWidget { color: ; margin: 10px; }"); +} + +// Test with an empty QSS string +TEST_F(StyleParametersApplicationTest, ReplaceVariablesInQssWithEmptyString) +{ + QString qss = ""; + QString result = app->replaceVariablesInQss(qss); + + EXPECT_EQ(result.toStdString(), ""); +} + +// Test replacing multiple occurrences of the same variable +TEST_F(StyleParametersApplicationTest, ReplaceVariablesInQssWithMultipleOccurrences) +{ + QString qss = "QWidget { color: @ColorPrimary; background: @ColorPrimary; }"; + QString result = app->replaceVariablesInQss(qss); + + EXPECT_EQ(result.toStdString(), "QWidget { color: #ff0000; background: #ff0000; }"); +} From dffdfb1a3b64de291d1d4da2c619ded5c6dad298 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Mon, 7 Jul 2025 00:37:34 +0200 Subject: [PATCH 03/30] Gui: Fix too small link icon on high dpi (#22359) * Gui: Fix too small link icon on high dpi * Gui: suppress warning in ViewProvideLink.cpp --------- Co-authored-by: Benjamin Nauck --- src/Gui/ViewProviderLink.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Gui/ViewProviderLink.cpp b/src/Gui/ViewProviderLink.cpp index 28f8f71f6c..8073c7b51e 100644 --- a/src/Gui/ViewProviderLink.cpp +++ b/src/Gui/ViewProviderLink.cpp @@ -673,8 +673,11 @@ public: QIcon getIcon(QPixmap px) { static int iconSize = -1; - if(iconSize < 0) - iconSize = QApplication::style()->standardPixmap(QStyle::SP_DirClosedIcon).width(); + if (iconSize < 0) { + auto sampleIcon = QApplication::style()->standardPixmap(QStyle::SP_DirClosedIcon); + double pixelRatio = sampleIcon.devicePixelRatio(); + iconSize = static_cast(sampleIcon.width() / pixelRatio); + } if(!isLinked()) return QIcon(); From 4b84834112c1a630b5c7ffa71e16baa05bd83d14 Mon Sep 17 00:00:00 2001 From: theo-vt Date: Sun, 6 Jul 2025 19:25:24 -0400 Subject: [PATCH 04/30] Sketcher: Autoscale: do not scale dimension's position if it is a radius or diameter (#22308) * Do not scale dimension's position if it is a radius or diameter * Update src/Mod/Sketcher/Gui/EditDatumDialog.cpp Sketcher: spell checking --------- Co-authored-by: Benjamin Nauck --- src/Mod/Sketcher/Gui/EditDatumDialog.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Mod/Sketcher/Gui/EditDatumDialog.cpp b/src/Mod/Sketcher/Gui/EditDatumDialog.cpp index b2278d8530..27e3258ce9 100644 --- a/src/Mod/Sketcher/Gui/EditDatumDialog.cpp +++ b/src/Mod/Sketcher/Gui/EditDatumDialog.cpp @@ -412,12 +412,22 @@ void EditDatumDialog::performAutoScale(double newDatum) && sketch->getExternalGeometryCount() <= 2 && sketch->hasSingleScaleDefiningConstraint()) { try { double oldDatum = sketch->getDatum(ConstrNbr); - double scale_factor = newDatum / oldDatum; + double scaleFactor = newDatum / oldDatum; float initLabelDistance = sketch->Constraints[ConstrNbr]->LabelDistance; float initLabelPosition = sketch->Constraints[ConstrNbr]->LabelPosition; - centerScale(sketch, scale_factor); - sketch->setLabelDistance(ConstrNbr, initLabelDistance * scale_factor); - sketch->setLabelPosition(ConstrNbr, initLabelPosition * scale_factor); + centerScale(sketch, scaleFactor); + sketch->setLabelDistance(ConstrNbr, initLabelDistance * scaleFactor); + + // Label position or radii and diameters represent an angle, so + // they should not be scaled + Sketcher::ConstraintType type = sketch->Constraints[ConstrNbr]->Type; + if (type == Sketcher::ConstraintType::Radius + || type == Sketcher::ConstraintType::Diameter) { + sketch->setLabelPosition(ConstrNbr, initLabelPosition); + } + else { + sketch->setLabelPosition(ConstrNbr, initLabelPosition * scaleFactor); + } } catch (const Base::Exception& e) { Base::Console().error("Exception performing autoscale: %s\n", e.what()); From 3e32ea5dd422eaecd415581d50a16af778334ef6 Mon Sep 17 00:00:00 2001 From: Max Wilfinger <6246609+maxwxyz@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:16:07 +0200 Subject: [PATCH 05/30] Sketcher: Update missed UI strings (#22363) --- .../Sketcher/Gui/CommandSketcherOverlay.cpp | 46 +++++++++---------- src/Mod/Sketcher/Gui/CommandSketcherTools.cpp | 2 +- src/Mod/Sketcher/Gui/Workbench.cpp | 4 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/Mod/Sketcher/Gui/CommandSketcherOverlay.cpp b/src/Mod/Sketcher/Gui/CommandSketcherOverlay.cpp index a9c46f6582..98dc29f1ef 100644 --- a/src/Mod/Sketcher/Gui/CommandSketcherOverlay.cpp +++ b/src/Mod/Sketcher/Gui/CommandSketcherOverlay.cpp @@ -293,50 +293,50 @@ void CmdSketcherCompBSplineShowHideGeometryInformation::languageChange() QAction* c1 = a[0]; c1->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline degree")); - c1->setToolTip(QApplication::translate( - "Sketcher_BSplineDegree", - "Switches between showing and hiding the degree for all B-splines")); - c1->setStatusTip(QApplication::translate( - "Sketcher_BSplineDegree", - "Switches between showing and hiding the degree for all B-splines")); + "Toggle B-Spline Degree")); + c1->setToolTip( + QApplication::translate("Sketcher_BSplineDegree", + "Toggles the visibility of the degree for all B-splines")); + c1->setStatusTip( + QApplication::translate("Sketcher_BSplineDegree", + "Toggles the visibility of the degree for all B-splines")); QAction* c2 = a[1]; c2->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline control polygon")); + "Toggle B-Spline Control Polygon")); c2->setToolTip(QApplication::translate( "Sketcher_BSplinePolygon", - "Switches between showing and hiding the control polygons for all B-splines")); + "Toggles the visibility of the control polygons for all B-splines")); c2->setStatusTip(QApplication::translate( "Sketcher_BSplinePolygon", - "Switches between showing and hiding the control polygons for all B-splines")); + "Toggles the visibility of the control polygons for all B-splines")); QAction* c3 = a[2]; c3->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline curvature comb")); - c3->setToolTip(QApplication::translate( - "Sketcher_BSplineComb", - "Switches between showing and hiding the curvature comb for all B-splines")); - c3->setStatusTip(QApplication::translate( - "Sketcher_BSplineComb", - "Switches between showing and hiding the curvature comb for all B-splines")); + "Toggle B-Spline Curvature Comb")); + c3->setToolTip( + QApplication::translate("Sketcher_BSplineComb", + "Toggles the visibility of the curvature comb for all B-splines")); + c3->setStatusTip( + QApplication::translate("Sketcher_BSplineComb", + "Toggles the visibility of the curvature comb for all B-splines")); QAction* c4 = a[3]; c4->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline knot multiplicity")); + "Toggle B-Spline Knot Multiplicity")); c4->setToolTip(QApplication::translate( "Sketcher_BSplineKnotMultiplicity", - "Switches between showing and hiding the knot multiplicity for all B-splines")); + "Toggles the visibility of the knot multiplicity for all B-splines")); c4->setStatusTip(QApplication::translate( "Sketcher_BSplineKnotMultiplicity", - "Switches between showing and hiding the knot multiplicity for all B-splines")); + "Toggles the visibility of the knot multiplicity for all B-splines")); QAction* c5 = a[4]; c5->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline control point weight")); + "Toggle B-Spline Control Point Weight")); c5->setToolTip(QApplication::translate( "Sketcher_BSplinePoleWeight", - "Switches between showing and hiding the control point weight for all B-splines")); + "Toggles the visibility of the control point weight for all B-splines")); c5->setStatusTip(QApplication::translate( "Sketcher_BSplinePoleWeight", - "Switches between showing and hiding the control point weight for all B-splines")); + "Toggles the visibility of the control point weight for all B-splines")); } void CmdSketcherCompBSplineShowHideGeometryInformation::updateAction(int /*mode*/) diff --git a/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp b/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp index 554c87eb35..72535b7191 100644 --- a/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp +++ b/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp @@ -2406,7 +2406,7 @@ CmdSketcherRotate::CmdSketcherRotate() { sAppModule = "Sketcher"; sGroup = "Sketcher"; - sMenuText = QT_TR_NOOP("Rotate/Polar Transform"); + sMenuText = QT_TR_NOOP("Rotate / Polar Transform"); sToolTipText = QT_TR_NOOP("Rotates the selected geometry by creating 'n' copies, enabling circular pattern creation"); sWhatsThis = "Sketcher_Rotate"; sStatusTip = sToolTipText; diff --git a/src/Mod/Sketcher/Gui/Workbench.cpp b/src/Mod/Sketcher/Gui/Workbench.cpp index 27f4760869..6ee89c8fb1 100644 --- a/src/Mod/Sketcher/Gui/Workbench.cpp +++ b/src/Mod/Sketcher/Gui/Workbench.cpp @@ -37,7 +37,7 @@ using namespace SketcherGui; qApp->translate("Workbench","P&rofiles"); qApp->translate("Workbench","S&ketch"); qApp->translate("Workbench", "Sketcher"); - qApp->translate("Workbench", "Sketcher Edit Mode"); + qApp->translate("Workbench", "Edit Mode"); qApp->translate("Workbench", "Geometries"); qApp->translate("Workbench", "Constraints"); @@ -113,7 +113,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const Gui::ToolBarItem* sketcherEditMode = new Gui::ToolBarItem(root, Gui::ToolBarItem::DefaultVisibility::Unavailable); - sketcherEditMode->setCommand("Sketcher Edit Mode"); + sketcherEditMode->setCommand("Edit Mode"); addSketcherWorkbenchSketchEditModeActions(*sketcherEditMode); Gui::ToolBarItem* geom = From 56024f12ad9815b35e7f441385b1d58e4dcace93 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sun, 6 Jul 2025 21:24:42 +0200 Subject: [PATCH 06/30] BIM: fix default radius for rectangular pipe connector Fixes #22364. The default radius of a connector between rectangular pipes should depend on the Height or Width of the pipe (the max. of the two is used), not on the hidden (and unused) Diameter property. --- src/Mod/BIM/Arch.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Mod/BIM/Arch.py b/src/Mod/BIM/Arch.py index 979ff4c888..a017c96181 100644 --- a/src/Mod/BIM/Arch.py +++ b/src/Mod/BIM/Arch.py @@ -794,7 +794,12 @@ def makePipeConnector(pipes, radius=0, name=None): # Initialize all relevant properties pipeConnector.Pipes = pipes - pipeConnector.Radius = radius if radius else pipes[0].Diameter + if radius: + pipeConnector.Radius = radius + elif pipes[0].ProfileType == "Circle": + pipeConnector.Radius = pipes[0].Diameter + else: + pipeConnector.Radius = max(pipes[0].Height, pipes[0].Width) return pipeConnector @@ -1933,4 +1938,4 @@ def _initializeArchObject( FreeCAD.Console.PrintError(f"Failed to import module '{moduleName}': {e}\n") return None - return obj \ No newline at end of file + return obj From cf3de7b730c5222327a717ee4226f91ecc6399c7 Mon Sep 17 00:00:00 2001 From: Ryan Kembrey Date: Mon, 7 Jul 2025 01:14:38 +1000 Subject: [PATCH 07/30] Sandbox: Update UI strings for consistency --- src/Mod/Sandbox/App/AppSandbox.cpp | 2 +- src/Mod/Sandbox/App/DocumentProtectorPy.cpp | 2 +- src/Mod/Sandbox/App/DocumentThread.cpp | 2 +- src/Mod/Sandbox/Gui/AppSandboxGui.cpp | 2 +- src/Mod/Sandbox/Gui/Command.cpp | 150 ++++++++++---------- 5 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/Mod/Sandbox/App/AppSandbox.cpp b/src/Mod/Sandbox/App/AppSandbox.cpp index 5a6adf4c7a..f74381bb60 100644 --- a/src/Mod/Sandbox/App/AppSandbox.cpp +++ b/src/Mod/Sandbox/App/AppSandbox.cpp @@ -253,6 +253,6 @@ PyMOD_INIT_FUNC(Sandbox) // the following constructor call registers our extension module // with the Python runtime system PyObject* mod = Sandbox::initModule(); - Base::Console().log("Loading Sandbox module... done\n"); + Base::Console().log("Loading Sandbox module… done\n"); PyMOD_Return(mod); } diff --git a/src/Mod/Sandbox/App/DocumentProtectorPy.cpp b/src/Mod/Sandbox/App/DocumentProtectorPy.cpp index c74d0977e2..af672fdac5 100644 --- a/src/Mod/Sandbox/App/DocumentProtectorPy.cpp +++ b/src/Mod/Sandbox/App/DocumentProtectorPy.cpp @@ -136,7 +136,7 @@ Py::Object DocumentProtectorPy::addObject(const Py::Tuple& args) if (!obj) { std::string s; std::ostringstream s_out; - s_out << "Couldn't create an object of type '" << type << "'"; + s_out << "Could not create an object of type '" << type << "'"; throw Py::RuntimeError(s_out.str()); } //return Py::asObject(obj->getPyObject()); diff --git a/src/Mod/Sandbox/App/DocumentThread.cpp b/src/Mod/Sandbox/App/DocumentThread.cpp index b8df4ce511..f3756d8df5 100644 --- a/src/Mod/Sandbox/App/DocumentThread.cpp +++ b/src/Mod/Sandbox/App/DocumentThread.cpp @@ -76,7 +76,7 @@ void WorkerThread::run() #else int max = 100000000; #endif - Base::SequencerLauncher seq("Do something meaningful...", max); + Base::SequencerLauncher seq("Do something meaningful…", max); double val=0; for (int i=0; i mesh_future = QtConcurrent::mapped (mesh_groups, boost::bind(&MeshTestJob::run, &meshJob, bp::_1)); @@ -877,10 +877,10 @@ CmdSandboxMeshTestRef::CmdSandboxMeshTestRef() { sAppModule = "Sandbox"; sGroup = QT_TR_NOOP("Sandbox"); - sMenuText = QT_TR_NOOP("Test mesh reference"); - sToolTipText = QT_TR_NOOP("Sandbox Test function"); + sMenuText = QT_TR_NOOP("Test Mesh Reference"); + sToolTipText = QT_TR_NOOP("Runs a sandbox test function"); sWhatsThis = "Sandbox_MeshTestRef"; - sStatusTip = QT_TR_NOOP("Sandbox Test function"); + sStatusTip = sToolTipText; } void CmdSandboxMeshTestRef::activated(int) @@ -924,8 +924,8 @@ CmdTestGrabWidget::CmdTestGrabWidget() : Command("Std_GrabWidget") { sGroup = "Standard-Test"; - sMenuText = "Grab widget"; - sToolTipText = "Grab widget"; + sMenuText = "Grab Widget"; + sToolTipText = "Grabs a widget"; sWhatsThis = "Std_GrabWidget"; sStatusTip = sToolTipText; } @@ -1045,7 +1045,7 @@ CmdTestImageNode::CmdTestImageNode() : Command("Std_ImageNode") { sGroup = "Standard-Test"; - sMenuText = "SoImage node"; + sMenuText = "SoImage Node"; sToolTipText = "SoImage node"; sWhatsThis = "Std_ImageNode"; sStatusTip = sToolTipText; @@ -1109,7 +1109,7 @@ CmdTestGDIWidget::CmdTestGDIWidget() : Command("Sandbox_GDIWidget") { sGroup = "Standard-Test"; - sMenuText = "GDI widget"; + sMenuText = "GDI Widget"; sToolTipText = "GDI widget"; sWhatsThis = "Sandbox_GDIWidget"; sStatusTip = sToolTipText; @@ -1135,7 +1135,7 @@ CmdTestRedirectPaint::CmdTestRedirectPaint() : Command("Sandbox_RedirectPaint") { sGroup = "Standard-Test"; - sMenuText = "Redirect paint"; + sMenuText = "Redirect Paint"; sToolTipText = "Redirect paint"; sWhatsThis = "Sandbox_RedirectPaint"; sStatusTip = sToolTipText; @@ -1165,7 +1165,7 @@ CmdTestCryptographicHash::CmdTestCryptographicHash() { sGroup = "Standard-Test"; sMenuText = "Cryptographic Hash"; - sToolTipText = "Cryptographic Hash"; + sToolTipText = "Cryptographic hash"; sWhatsThis = "Sandbox_CryptographicHash"; sStatusTip = sToolTipText; } @@ -1186,7 +1186,7 @@ CmdTestWidgetShape::CmdTestWidgetShape() : Command("Sandbox_WidgetShape") { sGroup = "Standard-Test"; - sMenuText = "Widget shape"; + sMenuText = "Widget Shape"; sToolTipText = "Widget shape"; sWhatsThis = "Sandbox_WidgetShape"; sStatusTip = sToolTipText; @@ -1210,10 +1210,10 @@ CmdMengerSponge::CmdMengerSponge() { sAppModule = "Sandbox"; sGroup = QT_TR_NOOP("Sandbox"); - sMenuText = QT_TR_NOOP("Menger sponge"); + sMenuText = QT_TR_NOOP("Menger Sponge"); sToolTipText = QT_TR_NOOP("Menger sponge"); sWhatsThis = "Sandbox_MengerSponge"; - sStatusTip = QT_TR_NOOP("Menger sponge"); + sStatusTip = sToolTipText; } struct Param { @@ -1341,7 +1341,7 @@ void CmdMengerSponge::activated(int) return; int ret = QMessageBox::question(Gui::getMainWindow(), QStringLiteral("Parallel"), - QStringLiteral("Do you want to run this in a thread pool?"), + QStringLiteral("Run this in a thread pool?"), QMessageBox::Yes|QMessageBox::No); bool parallel=(ret == QMessageBox::Yes); float x0=0,y0=0,z0=0; @@ -1379,9 +1379,9 @@ CmdTestGraphicsView::CmdTestGraphicsView() : Command("Std_TestGraphicsView") { sGroup = QT_TR_NOOP("Standard-Test"); - sMenuText = QT_TR_NOOP("Create new graphics view"); + sMenuText = QT_TR_NOOP("New Graphics View"); sToolTipText= QT_TR_NOOP("Creates a new view window for the active document"); - sStatusTip = QT_TR_NOOP("Creates a new view window for the active document"); + sStatusTip = sToolTipText; } void CmdTestGraphicsView::activated(int) @@ -1407,7 +1407,7 @@ CmdTestTaskBox::CmdTestTaskBox() : Command("Std_TestTaskBox") { sGroup = "Standard-Test"; - sMenuText = "Task box"; + sMenuText = "Task Box"; sToolTipText = "Task box"; sWhatsThis = "Std_TestTaskBox"; sStatusTip = sToolTipText; From 152f4989c62f3dd8e413b6bec7c025ff5fb570bd Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sat, 5 Jul 2025 16:21:01 +0200 Subject: [PATCH 08/30] BIM: fix Arch_SectionPlane 'Toggle Cutview' issue --- src/Mod/BIM/ArchSectionPlane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/BIM/ArchSectionPlane.py b/src/Mod/BIM/ArchSectionPlane.py index 84444756c0..a001e44a30 100644 --- a/src/Mod/BIM/ArchSectionPlane.py +++ b/src/Mod/BIM/ArchSectionPlane.py @@ -1173,7 +1173,7 @@ class _ViewProviderSectionPlane: actionToggleCutview = QtGui.QAction(QtGui.QIcon(":/icons/Draft_Edit.svg"), translate("Arch", "Toggle Cutview"), menu) - actionToggleCutview.triggered.connect(lambda f=self.toggleCutview, arg=vobj: f(arg)) + actionToggleCutview.triggered.connect(lambda: self.toggleCutview(vobj)) menu.addAction(actionToggleCutview) def edit(self): From 680be1548c64f82c2b9aef933cce6f2c042bc96d Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:39:55 +0200 Subject: [PATCH 09/30] BIM: fix profile selection (#22223) * Update params.py * Update ArchStructure.py * Update BimProfile.py --- src/Mod/BIM/ArchStructure.py | 7 ++- src/Mod/BIM/bimcommands/BimProfile.py | 71 ++++++++++++++------------- src/Mod/Draft/draftutils/params.py | 1 + 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/Mod/BIM/ArchStructure.py b/src/Mod/BIM/ArchStructure.py index 14e9985359..72865ccbaa 100644 --- a/src/Mod/BIM/ArchStructure.py +++ b/src/Mod/BIM/ArchStructure.py @@ -627,10 +627,9 @@ class _CommandStructure: self.dents.form.hide() params.set_param_arch("StructurePreset",self.Profile) else: - p=elt[0]-1 # Presets indexes are 1-based - self.vLength.setText(FreeCAD.Units.Quantity(float(Presets[p][4]),FreeCAD.Units.Length).UserString) - self.vWidth.setText(FreeCAD.Units.Quantity(float(Presets[p][5]),FreeCAD.Units.Length).UserString) - self.Profile = Presets[p] + self.vLength.setText(FreeCAD.Units.Quantity(float(elt[4]),FreeCAD.Units.Length).UserString) + self.vWidth.setText(FreeCAD.Units.Quantity(float(elt[5]),FreeCAD.Units.Length).UserString) + self.Profile = elt params.set_param_arch("StructurePreset",";".join([str(i) for i in self.Profile])) def switchLH(self,bmode): diff --git a/src/Mod/BIM/bimcommands/BimProfile.py b/src/Mod/BIM/bimcommands/BimProfile.py index 82434cbeeb..bf6f4ac618 100644 --- a/src/Mod/BIM/bimcommands/BimProfile.py +++ b/src/Mod/BIM/bimcommands/BimProfile.py @@ -77,7 +77,7 @@ class Arch_Profile: # categories box labelc = QtGui.QLabel(translate("Arch","Category")) self.vCategory = QtGui.QComboBox() - self.vCategory.addItems([" "] + self.Categories) + self.vCategory.addItems(self.Categories) grid.addWidget(labelc,1,0,1,1) grid.addWidget(self.vCategory,1,1,1,1) @@ -90,22 +90,32 @@ class Arch_Profile: grid.addWidget(labelp,2,0,1,1) grid.addWidget(self.vPresets,2,1,1,1) + # restore preset + categoryIdx = -1 + presetIdx = -1 + stored = params.get_param_arch("ProfilePreset") + if stored and ";" in stored and len(stored.split(";")) >= 3: + stored = stored.split(";") + if stored[1] in self.Categories: + categoryIdx = self.Categories.index(stored[1]) + self.setCategory(categoryIdx) + self.vCategory.setCurrentIndex(categoryIdx) + ps = [p[2] for p in self.pSelect] + if stored[2] in ps: + presetIdx = ps.index(stored[2]) + self.setPreset(presetIdx) + self.vPresets.setCurrentIndex(presetIdx) + if categoryIdx == -1: + self.setCategory(0) + self.vCategory.setCurrentIndex(0) + if presetIdx == -1: + self.setPreset(0) + self.vPresets.setCurrentIndex(0) + # connect slots self.vCategory.currentIndexChanged.connect(self.setCategory) self.vPresets.currentIndexChanged.connect(self.setPreset) - # restore preset - stored = params.get_param_arch("StructurePreset") - if stored: - if ";" in stored: - stored = stored.split(";") - if len(stored) >= 3: - if stored[1] in self.Categories: - self.vCategory.setCurrentIndex(1+self.Categories.index(stored[1])) - self.setCategory(1+self.Categories.index(stored[1])) - ps = [p[2] for p in self.pSelect] - if stored[2] in ps: - self.vPresets.setCurrentIndex(ps.index(stored[2])) return w def getPoint(self,point=None,obj=None): @@ -134,32 +144,23 @@ class Arch_Profile: from draftutils import params self.vPresets.clear() - if i == 0: - self.pSelect = [None] - self.vPresets.addItems([" "]) - params.set_param_arch("StructurePreset","") - else: - self.pSelect = [p for p in self.Presets if p[1] == self.Categories[i-1]] - fpresets = [] - for p in self.pSelect: - f = FreeCAD.Units.Quantity(p[4],FreeCAD.Units.Length).getUserPreferred() - d = params.get_param("Decimals",path="Units") - s1 = str(round(p[4]/f[1],d)) - s2 = str(round(p[5]/f[1],d)) - s3 = str(f[2]) - fpresets.append(p[2]+" ("+s1+"x"+s2+s3+")") - self.vPresets.addItems(fpresets) - self.setPreset(0) + self.pSelect = [p for p in self.Presets if p[1] == self.Categories[i]] + fpresets = [] + for p in self.pSelect: + f = FreeCAD.Units.Quantity(p[4],FreeCAD.Units.Length).getUserPreferred() + d = params.get_param("Decimals",path="Units") + s1 = str(round(p[4]/f[1],d)) + s2 = str(round(p[5]/f[1],d)) + s3 = str(f[2]) + fpresets.append(p[2]+" ("+s1+"x"+s2+s3+")") + self.vPresets.addItems(fpresets) + self.setPreset(0) def setPreset(self,i): from draftutils import params - self.Profile = None - elt = self.pSelect[i] - if elt: - p=elt[0]-1 # Presets indexes are 1-based - self.Profile = self.Presets[p] - params.set_param_arch("StructurePreset",";".join([str(i) for i in self.Profile])) + self.Profile = self.pSelect[i] + params.set_param_arch("ProfilePreset",";".join([str(i) for i in self.Profile])) FreeCADGui.addCommand('Arch_Profile',Arch_Profile()) diff --git a/src/Mod/Draft/draftutils/params.py b/src/Mod/Draft/draftutils/params.py index ad49b2b7bf..f2046e5c52 100644 --- a/src/Mod/Draft/draftutils/params.py +++ b/src/Mod/Draft/draftutils/params.py @@ -558,6 +558,7 @@ def _get_param_dictionary(): "PrecastHoleSpacing": ("float", 0.0), "PrecastRiser": ("float", 0.0), "PrecastTread": ("float", 0.0), + "ProfilePreset": ("string", ""), "ScheduleColumnWidth0": ("int", 100), "ScheduleColumnWidth1": ("int", 100), "ScheduleColumnWidth2": ("int", 50), From addac93afa4fc254369d32e18a5eae7baf511608 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Sun, 29 Jun 2025 17:41:04 +0100 Subject: [PATCH 10/30] [BIM] Stop combobox sizeAdjustPolicy warning --- src/Mod/BIM/Resources/ui/dialogProjectManager.ui | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Mod/BIM/Resources/ui/dialogProjectManager.ui b/src/Mod/BIM/Resources/ui/dialogProjectManager.ui index ffffe2703f..4a280670be 100644 --- a/src/Mod/BIM/Resources/ui/dialogProjectManager.ui +++ b/src/Mod/BIM/Resources/ui/dialogProjectManager.ui @@ -406,9 +406,6 @@ The main use of this building - - QComboBox::AdjustToMinimumContentsLength - 0 From 3e9089bd47cb027c614b5968eb1c3745ddc7268d Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:45:07 +0200 Subject: [PATCH 11/30] BIM: fix handling of Project coin nodes (#22244) * Update ArchProject.py * Update ArchSite.py --- src/Mod/BIM/ArchProject.py | 7 +++++-- src/Mod/BIM/ArchSite.py | 15 ++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Mod/BIM/ArchProject.py b/src/Mod/BIM/ArchProject.py index 7374824f0f..6d16ad1c00 100644 --- a/src/Mod/BIM/ArchProject.py +++ b/src/Mod/BIM/ArchProject.py @@ -137,9 +137,12 @@ class _ViewProviderProject(ArchIFCView.IfcContextView): https://forum.freecad.org/viewtopic.php?f=10&t=74731 """ + from pivy import coin + from draftutils import gui_utils + if not hasattr(self, "displaymodes_cleaned"): - if vobj.RootNode.getNumChildren() > 2: - main_switch = vobj.RootNode.getChild(2) # The display mode switch. + if vobj.RootNode.getNumChildren(): + main_switch = gui_utils.find_coin_node(vobj.RootNode, coin.SoSwitch) # The display mode switch. if main_switch is not None and main_switch.getNumChildren() == 4: # Check if all display modes are available. for node in tuple(main_switch.getChildren()): node.removeAllChildren() diff --git a/src/Mod/BIM/ArchSite.py b/src/Mod/BIM/ArchSite.py index 2d80e8f825..13693bb6ee 100644 --- a/src/Mod/BIM/ArchSite.py +++ b/src/Mod/BIM/ArchSite.py @@ -978,19 +978,12 @@ class _ViewProviderSite: """ from pivy import coin - - def find_node(parent, nodetype): - for i in range(parent.getNumChildren()): - if isinstance(parent.getChild(i), nodetype): - return parent.getChild(i) - return None + from draftutils import gui_utils if not hasattr(self, "terrain_switches"): - if vobj.RootNode.getNumChildren() > 2: - main_switch = find_node(vobj.RootNode, coin.SoSwitch) - if not main_switch: - return - if main_switch.getNumChildren() == 4: # Check if all display modes are available. + if vobj.RootNode.getNumChildren(): + main_switch = gui_utils.find_coin_node(vobj.RootNode, coin.SoSwitch) # The display mode switch. + if main_switch is not None and main_switch.getNumChildren() == 4: # Check if all display modes are available. self.terrain_switches = [] for node in tuple(main_switch.getChildren()): new_switch = coin.SoSwitch() From e6e1d6c54e1bda5f17b49ca93e925b8a160df2ab Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:07:51 +0200 Subject: [PATCH 12/30] BIM: fix visibility handling of objects hosted by additions --- src/Mod/BIM/ArchComponent.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Mod/BIM/ArchComponent.py b/src/Mod/BIM/ArchComponent.py index 7ca305eb66..ad04c2ce6a 100644 --- a/src/Mod/BIM/ArchComponent.py +++ b/src/Mod/BIM/ArchComponent.py @@ -1413,18 +1413,13 @@ class ViewProviderComponent: The name of the property that has changed. """ - #print(vobj.Object.Name, " : changing ",prop) - #if prop == "Visibility": - #for obj in vobj.Object.Additions+vobj.Object.Subtractions: - # if (Draft.getType(obj) == "Window") or (Draft.isClone(obj,"Window",True)): - # obj.ViewObject.Visibility = vobj.Visibility - # this would now hide all previous windows... Not the desired behaviour anymore. + obj = vobj.Object if prop == "DiffuseColor": - if hasattr(vobj.Object,"CloneOf"): - if vobj.Object.CloneOf and hasattr(vobj.Object.CloneOf,"DiffuseColor"): - if len(vobj.Object.CloneOf.ViewObject.DiffuseColor) > 1: - if vobj.DiffuseColor != vobj.Object.CloneOf.ViewObject.DiffuseColor: - vobj.DiffuseColor = vobj.Object.CloneOf.ViewObject.DiffuseColor + if hasattr(obj,"CloneOf"): + if obj.CloneOf and hasattr(obj.CloneOf,"DiffuseColor"): + if len(obj.CloneOf.ViewObject.DiffuseColor) > 1: + if vobj.DiffuseColor != obj.CloneOf.ViewObject.DiffuseColor: + vobj.DiffuseColor = obj.CloneOf.ViewObject.DiffuseColor vobj.update() elif prop == "ShapeColor": # restore DiffuseColor after overridden by ShapeColor @@ -1433,10 +1428,16 @@ class ViewProviderComponent: d = vobj.DiffuseColor vobj.DiffuseColor = d elif prop == "Visibility": - for host in vobj.Object.Proxy.getHosts(vobj.Object): - if hasattr(host, 'ViewObject'): - host.ViewObject.Visibility = vobj.Visibility - + # do nothing if object is an addition + if not [parent for parent in obj.InList if obj in getattr(parent, "Additions", [])]: + hostedObjs = obj.Proxy.getHosts(obj) + # add objects hosted by additions + for addition in getattr(obj, "Additions", []): + if hasattr(addition, "Proxy") and hasattr(addition.Proxy, "getHosts"): + hostedObjs.extend(addition.Proxy.getHosts(addition)) + for hostedObj in hostedObjs: + if hasattr(hostedObj, "ViewObject"): + hostedObj.ViewObject.Visibility = vobj.Visibility return def attach(self,vobj): From 236bec9aa9200b04a14bd8b2fc073257c5b0a070 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:51:27 +0200 Subject: [PATCH 13/30] BIM: improve Arch_MergeWalls (#22262) * BIM: improve Arch_MergeWalls * Improve delete behavior Keep hosted objects*, additions and subtractions if delete is False. * For hosted objects with a Host property this is not possible. * Make deletion of base objects optional --- src/Mod/BIM/Arch.py | 46 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/Mod/BIM/Arch.py b/src/Mod/BIM/Arch.py index a017c96181..614ca38cb5 100644 --- a/src/Mod/BIM/Arch.py +++ b/src/Mod/BIM/Arch.py @@ -1691,7 +1691,7 @@ def makeWall( return wall -def joinWalls(walls, delete=False): +def joinWalls(walls, delete=False, deletebase=False): """Join the given list of walls into one sketch-based wall. Take the first wall in the list, and adds on the other walls in the list. @@ -1707,6 +1707,9 @@ def joinWalls(walls, delete=False): be based off a base object. delete : bool, optional If True, deletes the other walls in the list. Defaults to False. + deletebase : bool, optional + If True, and delete is True, the base of the other walls is also deleted + Defaults to False. Returns ------- @@ -1745,14 +1748,39 @@ def joinWalls(walls, delete=False): else: sk = base.Base for w in walls: - if w.Base: - if not w.Base.Shape.Faces: - for e in w.Base.Shape.Edges: - l = e.Curve - if isinstance(l, Part.Line): - l = Part.LineSegment(e.Vertexes[0].Point, e.Vertexes[-1].Point) - sk.addGeometry(l) - deleteList.append(w.Name) + if w.Base and not w.Base.Shape.Faces: + for hostedObj in w.Proxy.getHosts(w): + if hasattr(hostedObj, "Host"): + hostedObj.Host = base + else: + tmp = hostedObj.Hosts + if delete: + tmp.remove(w) + if not base in tmp: + tmp.append(base) + hostedObj.Hosts = tmp + tmp = [] + for add in w.Additions: + if not add in base.Additions: + tmp.append(add) + if delete: + w.Additions = None + base.Additions += tmp + tmp = [] + for sub in w.Subtractions: + if not sub in base.Subtractions: + tmp.append(sub) + if delete: + w.Subtractions = None + base.Subtractions += tmp + for e in w.Base.Shape.Edges: + l = e.Curve + if isinstance(l, Part.Line): + l = Part.LineSegment(e.Vertexes[0].Point, e.Vertexes[-1].Point) + sk.addGeometry(l) + deleteList.append(w.Name) + if deletebase: + deleteList.append(w.Base.Name) if delete: for n in deleteList: FreeCAD.ActiveDocument.removeObject(n) From 985e42b61a1523de0e765c4f5092785680856941 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:44:53 +0200 Subject: [PATCH 14/30] BIM: check OutListRecursive in addComponents --- src/Mod/BIM/ArchCommands.py | 70 ++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/src/Mod/BIM/ArchCommands.py b/src/Mod/BIM/ArchCommands.py index 9595af1a1a..57420587bf 100644 --- a/src/Mod/BIM/ArchCommands.py +++ b/src/Mod/BIM/ArchCommands.py @@ -93,6 +93,14 @@ def getDefaultColor(objectType): r, g, b, _ = Draft.get_rgba_tuple(c) return (r, g, b, alpha) +def _usedForAttachment(host,obj): + if not getattr(obj,"AttachmentSupport",[]): + return False + for sub in obj.AttachmentSupport: + if sub[0] == host: + return True + return False + def addComponents(objectsList,host): '''addComponents(objectsList,hostObject): adds the given object or the objects from the given list as components to the given host Object. Use this for @@ -105,24 +113,32 @@ def addComponents(objectsList,host): host.addObject(o) elif hostType in ["Wall","CurtainWall","Structure","Precast","Window","Roof","Stairs","StructuralSystem","Panel","Component","Pipe"]: import DraftGeomUtils + outList = host.OutListRecursive a = host.Additions - if hasattr(host,"Axes"): - x = host.Axes + x = getattr(host,"Axes",[]) for o in objectsList: - if hasattr(o,'Shape'): + if hasattr(o,"Shape"): if Draft.getType(o) == "Window": if hasattr(o,"Hosts"): if not host in o.Hosts: g = o.Hosts g.append(host) o.Hosts = g - elif DraftGeomUtils.isValidPath(o.Shape) and (hostType in ["Structure","Precast"]): - if o.AttachmentSupport == host: - o.AttachmentSupport = None - host.Tool = o + elif o in outList: + FreeCAD.Console.PrintWarning( + translate( + "Arch", + "Cannot add {0} as it is already referenced by {1}." + ).format(o.Label, host.Label) + "\n" + ) elif Draft.getType(o) == "Axis": if not o in x: x.append(o) + elif DraftGeomUtils.isValidPath(o.Shape) and (hostType in ["Structure","Precast"]): + if _usedForAttachment(host,o): + o.AttachmentSupport = None + o.MapMode = "Deactivated" + host.Tool = o elif not o in a: if hasattr(o,"Shape"): a.append(o) @@ -148,15 +164,15 @@ def removeComponents(objectsList,host=None): objectsList = [objectsList] if host: if Draft.getType(host) in ["Wall","CurtainWall","Structure","Precast","Window","Roof","Stairs","StructuralSystem","Panel","Component","Pipe"]: - if hasattr(host,"Tool"): - if objectsList[0] == host.Tool: - host.Tool = None + if getattr(host,"Tool",None) in objectsList: + host.Tool = None if hasattr(host,"Axes"): a = host.Axes for o in objectsList[:]: if o in a: a.remove(o) objectsList.remove(o) + host.Axes = a s = host.Subtractions for o in objectsList: if Draft.getType(o) == "Window": @@ -168,34 +184,24 @@ def removeComponents(objectsList,host=None): elif not o in s: s.append(o) if FreeCAD.GuiUp: - if not Draft.getType(o) in ["Window","Roof"]: + if Draft.getType(o) != "Roof": setAsSubcomponent(o) - # Making reference to BimWindow.Arch_Window: - # Check if o and o.Base has Attachment Support, and - # if the support is the host object itself - thus a cyclic - # dependency and probably creating TNP. - # If above is positive, remove its AttachmentSupport: + # Avoid cyclic dependency via Attachment Support: if hasattr(o,"Base") and o.Base: objList = [o, o.Base] else: objList = [o] for i in objList: - objHost = None - if hasattr(i,"AttachmentSupport"): - if i.AttachmentSupport: - if isinstance(i.AttachmentSupport,tuple): - objHost = i.AttachmentSupport[0] - elif isinstance(i.AttachmentSupport,list): - objHost = i.AttachmentSupport[0][0] - else: - objHost = i.AttachmentSupport - if objHost == host: - msg = FreeCAD.Console.PrintMessage - msg(i.Label + " is mapped to " + host.Label + - ", removing the former's Attachment " + - "Support to avoid cyclic dependency and " + - "TNP." + "\n") - i.AttachmentSupport = None # remove + if _usedForAttachment(host,i): + FreeCAD.Console.PrintMessage( + translate( + "Arch", + "{0} is mapped to {1}, removing the former's " + + "Attachment Support to avoid cyclic dependency." + ).format(o.Label, host.Label) + "\n" + ) + i.AttachmentSupport = None + i.MapMode = "Deactivated" host.Subtractions = s elif Draft.getType(host) in ["SectionPlane"]: a = host.Objects From 4d7f03bf633d8d41b0828c8b949832bee0dff76d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbyn=C4=9Bk=20Winkler?= Date: Wed, 25 Jun 2025 15:31:36 +0200 Subject: [PATCH 15/30] Copy subvolume before changing its Placement Placement property of a cached object was modified each time component is recalculated. Fixes #22162. --- src/Mod/BIM/ArchComponent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/BIM/ArchComponent.py b/src/Mod/BIM/ArchComponent.py index ad04c2ce6a..836ff2c01b 100644 --- a/src/Mod/BIM/ArchComponent.py +++ b/src/Mod/BIM/ArchComponent.py @@ -782,7 +782,7 @@ class Component(ArchIFC.IfcProduct): subvolume = o.getLinkedObject().Proxy.getSubVolume(o,host=obj) # pass host obj (mostly Wall) elif (Draft.getType(o) == "Roof") or (Draft.isClone(o,"Roof")): # roofs define their own special subtraction volume - subvolume = o.Proxy.getSubVolume(o) + subvolume = o.Proxy.getSubVolume(o).copy() elif hasattr(o,"Subvolume") and hasattr(o.Subvolume,"Shape"): # Any other object with a Subvolume property ## TODO - Part.Shape() instead? From 2c82bfa150c97dfd87e4b3073b03cfd55b80292f Mon Sep 17 00:00:00 2001 From: tetektoza Date: Mon, 7 Jul 2025 16:06:43 +0200 Subject: [PATCH 16/30] BIM: Add an option to preload IFC types during document opening (#21450) * BIM: Add an option to preload IFC types during document opening Currently, IFC types are only possible to be loaded if user double clicks an IFC object, and this has be done for optimization reasons. But user can also want to preload IFC types, so this patch adds an option to the dialog and Properties dialog to do just that. * BIM: Remove cyclic import --------- Co-authored-by: Yorik van Havre --- src/Mod/BIM/Resources/ui/dialogImport.ui | 10 ++++++ .../BIM/Resources/ui/preferencesNativeIFC.ui | 16 ++++++++++ src/Mod/BIM/nativeifc/ifc_import.py | 7 +++++ src/Mod/BIM/nativeifc/ifc_tools.py | 1 + src/Mod/BIM/nativeifc/ifc_types.py | 31 +++++++++++++++++-- 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/Mod/BIM/Resources/ui/dialogImport.ui b/src/Mod/BIM/Resources/ui/dialogImport.ui index d94cf3ac3f..e4609879d8 100644 --- a/src/Mod/BIM/Resources/ui/dialogImport.ui +++ b/src/Mod/BIM/Resources/ui/dialogImport.ui @@ -113,6 +113,16 @@
+ + + + Preload IFC types that are connected to the objects. It is also possible to leave this setting disabled and double click later on the object to load the types + + + Preload types + + + diff --git a/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui b/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui index 136249f741..43e9771878 100644 --- a/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui +++ b/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui @@ -129,6 +129,22 @@ + + + + Load all types automatically when opening an IFC file + + + Preload types + + + LoadTypes + + + Mod/NativeIFC + + + diff --git a/src/Mod/BIM/nativeifc/ifc_import.py b/src/Mod/BIM/nativeifc/ifc_import.py index 5209401baa..5f539c1cf9 100644 --- a/src/Mod/BIM/nativeifc/ifc_import.py +++ b/src/Mod/BIM/nativeifc/ifc_import.py @@ -32,6 +32,7 @@ from . import ifc_psets from . import ifc_materials from . import ifc_layers from . import ifc_status +from . import ifc_types if FreeCAD.GuiUp: import FreeCADGui @@ -101,6 +102,8 @@ def insert( ifc_layers.load_layers(prj_obj) if PARAMS.GetBool("LoadPsets", False): ifc_psets.load_psets(prj_obj) + if PARAMS.GetBool("LoadTypes", False): + ifc_types.load_types(prj_obj) document.recompute() # print a reference to the IFC file on the console if FreeCAD.GuiUp and PARAMS.GetBool("IfcFileToConsole", False): @@ -133,6 +136,7 @@ def get_options(strategy=None, shapemode=None, switchwb=None, silent=False): """ psets = PARAMS.GetBool("LoadPsets", False) + types = PARAMS.GetBool("LoadTypes", False) materials = PARAMS.GetBool("LoadMaterials", False) layers = PARAMS.GetBool("LoadLayers", False) singledoc = PARAMS.GetBool("SingleDoc", False) @@ -155,6 +159,7 @@ def get_options(strategy=None, shapemode=None, switchwb=None, silent=False): dlg.checkSwitchWB.setChecked(switchwb) dlg.checkAskAgain.setChecked(ask) dlg.checkLoadPsets.setChecked(psets) + dlg.checkLoadTypes.setChecked(types) dlg.checkLoadMaterials.setChecked(materials) dlg.checkLoadLayers.setChecked(layers) dlg.comboSingleDoc.setCurrentIndex(1 - int(singledoc)) @@ -166,6 +171,7 @@ def get_options(strategy=None, shapemode=None, switchwb=None, silent=False): switchwb = dlg.checkSwitchWB.isChecked() ask = dlg.checkAskAgain.isChecked() psets = dlg.checkLoadPsets.isChecked() + types = dlg.checkLoadTypes.isChecked() materials = dlg.checkLoadMaterials.isChecked() layers = dlg.checkLoadLayers.isChecked() singledoc = dlg.comboSingleDoc.currentIndex() @@ -174,6 +180,7 @@ def get_options(strategy=None, shapemode=None, switchwb=None, silent=False): PARAMS.SetBool("SwitchWB", switchwb) PARAMS.SetBool("AskAgain", ask) PARAMS.SetBool("LoadPsets", psets) + PARAMS.SetBool("LoadTypes", types) PARAMS.SetBool("LoadMaterials", materials) PARAMS.SetBool("LoadLayers", layers) PARAMS.SetBool("SingleDoc", bool(1 - singledoc)) diff --git a/src/Mod/BIM/nativeifc/ifc_tools.py b/src/Mod/BIM/nativeifc/ifc_tools.py index 8bc16126b4..619c58419f 100644 --- a/src/Mod/BIM/nativeifc/ifc_tools.py +++ b/src/Mod/BIM/nativeifc/ifc_tools.py @@ -350,6 +350,7 @@ def create_children( ] for window in windows: subresult.extend(create_child(child, window)) + if recursive: subresult.extend( create_children( diff --git a/src/Mod/BIM/nativeifc/ifc_types.py b/src/Mod/BIM/nativeifc/ifc_types.py index 7967c471a9..ce95c8222d 100644 --- a/src/Mod/BIM/nativeifc/ifc_types.py +++ b/src/Mod/BIM/nativeifc/ifc_types.py @@ -51,6 +51,34 @@ def show_type(obj): obj.Type = typeobj +def load_types(prj_obj): + """ + Loads IFC types for all objects in the project, used during + import of IFC files. + prj_obj is the project object, either a document or a document object. + """ + + def process_object(obj): + """Recursively process an object and its children""" + # Check if this object has IFC data and can have types + if hasattr(obj, 'StepId') and obj.StepId: + show_type(obj) + + # Process children recursively + if hasattr(obj, 'Group'): + for child in obj.Group: + process_object(child) + + if isinstance(prj_obj, FreeCAD.DocumentObject): + # Handle document object case + process_object(prj_obj) + else: + # Handle document case - process all IFC objects in the document + for obj in prj_obj.Objects: + if hasattr(obj, 'StepId') and obj.StepId: + show_type(obj) + + def is_typable(obj): """Checks if an object can become a type""" @@ -87,10 +115,9 @@ def convert_to_type(obj, keep_object=False): original_text = dlg.label.text() dlg.label.setText(original_text.replace("%1", obj.Class+"Type")) - + # Set the initial state of the checkbox from the "always keep" preference dlg.checkKeepObject.setChecked(always_keep) - result = dlg.exec_() if not result: return From 59088a036519be08fd471920e9fc3cb068d881cb Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:02:14 +0200 Subject: [PATCH 17/30] Draft: make_sketch.py should not use view direction (#22249) --- src/Mod/Draft/draftmake/make_sketch.py | 30 +++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Mod/Draft/draftmake/make_sketch.py b/src/Mod/Draft/draftmake/make_sketch.py index ea055e9924..3f958bbccc 100644 --- a/src/Mod/Draft/draftmake/make_sketch.py +++ b/src/Mod/Draft/draftmake/make_sketch.py @@ -78,11 +78,6 @@ def make_sketch(objects_list, autoconstraints=False, addTo=None, import Part from Sketcher import Constraint - if App.GuiUp: - v_dir = gui_utils.get_3d_view().getViewDirection() - else: - v_dir = App.Vector(0, 0, -1) - # lists to accumulate shapes with defined normal and undefined normal shape_norm_yes = list() shape_norm_no = list() @@ -127,21 +122,20 @@ def make_sketch(objects_list, autoconstraints=False, addTo=None, # suppose all geometries are straight lines or points points = [vertex.Point for shape in shapes_list for vertex in shape.Vertexes] if len(points) >= 2: - poly = Part.makePolygon(points) - if not DraftGeomUtils.is_planar(poly, tol): - App.Console.PrintError(translate("draft", - "All Shapes must be coplanar") + "\n") - return None - normal = DraftGeomUtils.get_normal(poly, tol) - if not normal: - # all points aligned - poly_dir = poly.Edges[0].Curve.Direction - normal = (v_dir - v_dir.dot(poly_dir)*poly_dir).normalize() - normal = normal.negative() + try: + poly = Part.makePolygon(points) + except Part.OCCError: + # all points coincide + normal = App.Vector(0, 0, 1) + else: + if not DraftGeomUtils.is_planar(poly, tol): + App.Console.PrintError(translate("draft", + "All Shapes must be coplanar") + "\n") + return None + normal = DraftGeomUtils.get_shape_normal(poly) else: # only one point - normal = v_dir.negative() - + normal = App.Vector(0, 0, 1) if addTo: nobj = addTo From 50faa723783bde22924d92cc7cf6101d1ea6147b Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:45:46 +0200 Subject: [PATCH 18/30] Draft: gui_utils.py minor improvement for autogroup --- src/Mod/Draft/draftutils/gui_utils.py | 85 +++++++++++++-------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/src/Mod/Draft/draftutils/gui_utils.py b/src/Mod/Draft/draftutils/gui_utils.py index 698f5e9481..b6ab35aafc 100644 --- a/src/Mod/Draft/draftutils/gui_utils.py +++ b/src/Mod/Draft/draftutils/gui_utils.py @@ -120,71 +120,66 @@ def autogroup(obj): return # autogroup code - active_group = None if Gui.draftToolBar.autogroup is not None: active_group = App.ActiveDocument.getObject(Gui.draftToolBar.autogroup) - if active_group: - gr = active_group.Group - if not obj in gr: - gr.append(obj) - active_group.Group = gr + if obj in active_group.InListRecursive: + return + if not obj in active_group.Group: + active_group.Group += [obj] - if Gui.ActiveDocument.ActiveView.getActiveObject("NativeIFC") is not None: + elif Gui.ActiveDocument.ActiveView.getActiveObject("NativeIFC") is not None: # NativeIFC handling try: from nativeifc import ifc_tools parent = Gui.ActiveDocument.ActiveView.getActiveObject("NativeIFC") - if parent != active_group: - ifc_tools.aggregate(obj, parent) + ifc_tools.aggregate(obj, parent) except: pass elif Gui.ActiveDocument.ActiveView.getActiveObject("Arch") is not None: # add object to active Arch Container active_arch_obj = Gui.ActiveDocument.ActiveView.getActiveObject("Arch") - if active_arch_obj != active_group: - if obj in active_arch_obj.InListRecursive: - # do not autogroup if obj points to active_arch_obj to prevent cyclic references - return - active_arch_obj.addObject(obj) + if obj in active_arch_obj.InListRecursive: + # do not autogroup if obj points to active_arch_obj to prevent cyclic references + return + active_arch_obj.addObject(obj) elif Gui.ActiveDocument.ActiveView.getActiveObject("part") is not None: # add object to active part and change it's placement accordingly # so object does not jump to different position, works with App::Link # if not scaled. Modified accordingly to realthunder suggestions active_part, parent, sub = Gui.ActiveDocument.ActiveView.getActiveObject("part", False) - if active_part != active_group: - if obj in active_part.InListRecursive: - # do not autogroup if obj points to active_part to prevent cyclic references - return - matrix = parent.getSubObject(sub, retType=4) - if matrix.hasScale() == App.ScaleType.Uniform: - err = translate("draft", - "Unable to insert new object into " - "a scaled part") - App.Console.PrintMessage(err) - return - inverse_placement = App.Placement(matrix.inverse()) - if utils.get_type(obj) == 'Point': - point_vector = App.Vector(obj.X, obj.Y, obj.Z) - real_point = inverse_placement.multVec(point_vector) - obj.X = real_point.x - obj.Y = real_point.y - obj.Z = real_point.z - elif utils.get_type(obj) in ["Dimension", "LinearDimension"]: - obj.Start = inverse_placement.multVec(obj.Start) - obj.End = inverse_placement.multVec(obj.End) - obj.Dimline = inverse_placement.multVec(obj.Dimline) - obj.Normal = inverse_placement.Rotation.multVec(obj.Normal) - obj.Direction = inverse_placement.Rotation.multVec(obj.Direction) - elif utils.get_type(obj) in ["Label"]: - obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) - obj.TargetPoint = inverse_placement.multVec(obj.TargetPoint) - elif hasattr(obj,"Placement"): - # every object that have a placement is processed here - obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) + if obj in active_part.InListRecursive: + # do not autogroup if obj points to active_part to prevent cyclic references + return + matrix = parent.getSubObject(sub, retType=4) + if matrix.hasScale() == App.ScaleType.Uniform: + err = translate("draft", + "Unable to insert new object into " + "a scaled part") + App.Console.PrintMessage(err) + return + inverse_placement = App.Placement(matrix.inverse()) + if utils.get_type(obj) == 'Point': + point_vector = App.Vector(obj.X, obj.Y, obj.Z) + real_point = inverse_placement.multVec(point_vector) + obj.X = real_point.x + obj.Y = real_point.y + obj.Z = real_point.z + elif utils.get_type(obj) in ["Dimension", "LinearDimension"]: + obj.Start = inverse_placement.multVec(obj.Start) + obj.End = inverse_placement.multVec(obj.End) + obj.Dimline = inverse_placement.multVec(obj.Dimline) + obj.Normal = inverse_placement.Rotation.multVec(obj.Normal) + obj.Direction = inverse_placement.Rotation.multVec(obj.Direction) + elif utils.get_type(obj) in ["Label"]: + obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) + obj.TargetPoint = inverse_placement.multVec(obj.TargetPoint) + elif hasattr(obj,"Placement"): + # every object that have a placement is processed here + obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) - active_part.addObject(obj) + active_part.addObject(obj) def dim_symbol(symbol=None, invert=False): From 45be617bf331091e220802a00b3a72ed96f522eb Mon Sep 17 00:00:00 2001 From: FEA-eng <59876896+FEA-eng@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:46:19 +0200 Subject: [PATCH 19/30] Part: Enable solid creation by default for Loft and Sweep (#22098) * Part: Update PartFeatures.cpp * Part: Update DlgRevolution.ui * Part: Update TaskLoft.ui * Part: Update TaskSweep.ui * Part: Update PartFeatures.cpp * Update src/Mod/Part/Gui/DlgRevolution.ui --------- Co-authored-by: Max Wilfinger <6246609+maxwxyz@users.noreply.github.com> --- src/Mod/Part/App/PartFeatures.cpp | 4 ++-- src/Mod/Part/Gui/TaskLoft.ui | 3 +++ src/Mod/Part/Gui/TaskSweep.ui | 3 +++ tests/src/Mod/Part/App/PartFeatures.cpp | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Mod/Part/App/PartFeatures.cpp b/src/Mod/Part/App/PartFeatures.cpp index dab9d9219d..be5de959f7 100644 --- a/src/Mod/Part/App/PartFeatures.cpp +++ b/src/Mod/Part/App/PartFeatures.cpp @@ -177,7 +177,7 @@ Loft::Loft() { ADD_PROPERTY_TYPE(Sections, (nullptr), "Loft", App::Prop_None, "List of sections"); Sections.setSize(0); - ADD_PROPERTY_TYPE(Solid, (false), "Loft", App::Prop_None, "Create solid"); + ADD_PROPERTY_TYPE(Solid, (true), "Loft", App::Prop_None, "Create solid"); ADD_PROPERTY_TYPE(Ruled, (false), "Loft", App::Prop_None, "Ruled surface"); ADD_PROPERTY_TYPE(Closed, (false), "Loft", App::Prop_None, "Close Last to First Profile"); ADD_PROPERTY_TYPE(MaxDegree, (5), "Loft", App::Prop_None, "Maximum Degree"); @@ -257,7 +257,7 @@ Sweep::Sweep() ADD_PROPERTY_TYPE(Sections, (nullptr), "Sweep", App::Prop_None, "List of sections"); Sections.setSize(0); ADD_PROPERTY_TYPE(Spine, (nullptr), "Sweep", App::Prop_None, "Path to sweep along"); - ADD_PROPERTY_TYPE(Solid, (false), "Sweep", App::Prop_None, "Create solid"); + ADD_PROPERTY_TYPE(Solid, (true), "Sweep", App::Prop_None, "Create solid"); ADD_PROPERTY_TYPE(Frenet, (true), "Sweep", App::Prop_None, "Frenet"); ADD_PROPERTY_TYPE(Transition, (long(1)), "Sweep", App::Prop_None, "Transition mode"); ADD_PROPERTY_TYPE(Linearize,(false), "Sweep", App::Prop_None, diff --git a/src/Mod/Part/Gui/TaskLoft.ui b/src/Mod/Part/Gui/TaskLoft.ui index d47c1ebff1..2b9178aa95 100644 --- a/src/Mod/Part/Gui/TaskLoft.ui +++ b/src/Mod/Part/Gui/TaskLoft.ui @@ -22,6 +22,9 @@ Create solid + + true + diff --git a/src/Mod/Part/Gui/TaskSweep.ui b/src/Mod/Part/Gui/TaskSweep.ui index 3b2ee55c37..cc563b59d9 100644 --- a/src/Mod/Part/Gui/TaskSweep.ui +++ b/src/Mod/Part/Gui/TaskSweep.ui @@ -52,6 +52,9 @@ Create solid + + true + diff --git a/tests/src/Mod/Part/App/PartFeatures.cpp b/tests/src/Mod/Part/App/PartFeatures.cpp index 550a478356..d5f07f8e6c 100644 --- a/tests/src/Mod/Part/App/PartFeatures.cpp +++ b/tests/src/Mod/Part/App/PartFeatures.cpp @@ -106,6 +106,7 @@ TEST_F(PartFeaturesTest, testSweep) auto _sweep = _doc->addObject(); _sweep->Sections.setValues({_plane1}); _sweep->Spine.setValue(_edge1); + _sweep->Solid.setValue((false)); // Act _sweep->execute(); TopoShape ts = _sweep->Shape.getShape(); From 1598d565605e706671c059666aa82723cdbe8960 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:07:27 +0000 Subject: [PATCH 20/30] Bump step-security/harden-runner from 2.12.1 to 2.12.2 Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.12.1 to 2.12.2. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/002fdce3c6a235733a90a27c80493a3241e56863...6c439dc8bdf85cadbbce9ed30d1c7b959517bc49) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.12.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/CI_cleanup.yml | 2 +- .../workflows/auto-close_stale_issues_and_pull-requests.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/codeql_cpp.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/issue-metrics.yml | 2 +- .github/workflows/labeler.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/sub_buildPixi.yml | 2 +- .github/workflows/sub_buildUbuntu.yml | 2 +- .github/workflows/sub_buildWindows.yml | 2 +- .github/workflows/sub_lint.yml | 2 +- .github/workflows/sub_prepare.yml | 2 +- .github/workflows/sub_weeklyBuild.yml | 4 ++-- .github/workflows/sub_wrapup.yml | 2 +- 15 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/CI_cleanup.yml b/.github/workflows/CI_cleanup.yml index a17ac554ef..bc81055b25 100644 --- a/.github/workflows/CI_cleanup.yml +++ b/.github/workflows/CI_cleanup.yml @@ -58,7 +58,7 @@ jobs: logdir: /tmp/log/ steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/auto-close_stale_issues_and_pull-requests.yml b/.github/workflows/auto-close_stale_issues_and_pull-requests.yml index 3d187e2ad2..d5607b9f73 100644 --- a/.github/workflows/auto-close_stale_issues_and_pull-requests.yml +++ b/.github/workflows/auto-close_stale_issues_and_pull-requests.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 38795742c1..6c34139654 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -64,7 +64,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/codeql_cpp.yml b/.github/workflows/codeql_cpp.yml index 366565db4b..e8b528771a 100644 --- a/.github/workflows/codeql_cpp.yml +++ b/.github/workflows/codeql_cpp.yml @@ -68,7 +68,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index bb23bfeb91..b25b05b707 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/issue-metrics.yml b/.github/workflows/issue-metrics.yml index 42e7d8d49a..574624919e 100644 --- a/.github/workflows/issue-metrics.yml +++ b/.github/workflows/issue-metrics.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c5673d83a1..ca6703f629 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index fe8f207d82..2f3995ae6e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_buildPixi.yml b/.github/workflows/sub_buildPixi.yml index 00d9458bd3..9d600149aa 100644 --- a/.github/workflows/sub_buildPixi.yml +++ b/.github/workflows/sub_buildPixi.yml @@ -70,7 +70,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_buildUbuntu.yml b/.github/workflows/sub_buildUbuntu.yml index 191311d30c..78c4132e3f 100644 --- a/.github/workflows/sub_buildUbuntu.yml +++ b/.github/workflows/sub_buildUbuntu.yml @@ -72,7 +72,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_buildWindows.yml b/.github/workflows/sub_buildWindows.yml index 1e1fa0680f..3fe50fa1e9 100644 --- a/.github/workflows/sub_buildWindows.yml +++ b/.github/workflows/sub_buildWindows.yml @@ -63,7 +63,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_lint.yml b/.github/workflows/sub_lint.yml index ac31ef17e1..1815f6be95 100644 --- a/.github/workflows/sub_lint.yml +++ b/.github/workflows/sub_lint.yml @@ -198,7 +198,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_prepare.yml b/.github/workflows/sub_prepare.yml index 45c97a16b8..7c82b5f851 100644 --- a/.github/workflows/sub_prepare.yml +++ b/.github/workflows/sub_prepare.yml @@ -81,7 +81,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_weeklyBuild.yml b/.github/workflows/sub_weeklyBuild.yml index 6cd2e7e5f5..ca4df3871d 100644 --- a/.github/workflows/sub_weeklyBuild.yml +++ b/.github/workflows/sub_weeklyBuild.yml @@ -14,7 +14,7 @@ jobs: build_tag: ${{ steps.tag_build.outputs.build_tag }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit @@ -72,7 +72,7 @@ jobs: environment: weekly-build steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_wrapup.yml b/.github/workflows/sub_wrapup.yml index 62ab068e5a..3dbf23fbc2 100644 --- a/.github/workflows/sub_wrapup.yml +++ b/.github/workflows/sub_wrapup.yml @@ -54,7 +54,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit From 5f610d32ab19b4e1d517ce46e93db4033b66ee4b Mon Sep 17 00:00:00 2001 From: FEA-eng <59876896+FEA-eng@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:16:22 +0200 Subject: [PATCH 21/30] FEM: Update ElementGeometry1D.ui (#22134) --- src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui b/src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui index b28f2951ff..1bc29e2a2d 100644 --- a/src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui +++ b/src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui @@ -274,14 +274,14 @@ QFormLayout::AllNonFixedFieldsGrow - + - Height + Width - + Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter @@ -306,14 +306,14 @@ - + - Width + Height - + Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter From ff0b4b6325b5da3e8ed3f734978107bc88c5f135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbyn=C4=9Bk=20Winkler?= Date: Wed, 25 Jun 2025 11:17:42 +0200 Subject: [PATCH 22/30] remove conda dir since we have pixi --- CMakePresets.json | 3 - conda/cmake.cmd | 1 - conda/cmake.sh | 3 - conda/conda-env-qt6.yaml | 9 --- conda/conda-env.yaml | 9 --- conda/environment-qt6.devenv.yml | 101 ------------------------------- conda/environment.devenv.yml | 101 ------------------------------- conda/setup-environment-qt6.sh | 12 ---- conda/setup-environment.cmd | 10 --- conda/setup-environment.sh | 12 ---- 10 files changed, 261 deletions(-) delete mode 100644 conda/cmake.cmd delete mode 100755 conda/cmake.sh delete mode 100644 conda/conda-env-qt6.yaml delete mode 100644 conda/conda-env.yaml delete mode 100644 conda/environment-qt6.devenv.yml delete mode 100644 conda/environment.devenv.yml delete mode 100755 conda/setup-environment-qt6.sh delete mode 100644 conda/setup-environment.cmd delete mode 100755 conda/setup-environment.sh diff --git a/CMakePresets.json b/CMakePresets.json index 2ab2f39048..b6d9d067c2 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -121,7 +121,6 @@ "lhs": "${hostSystemName}", "rhs": "Linux" }, - "cmakeExecutable": "${sourceDir}/conda/cmake.sh", "cacheVariables": { "CMAKE_C_COMPILER": { "type": "STRING", @@ -161,7 +160,6 @@ "lhs": "${hostSystemName}", "rhs": "Darwin" }, - "cmakeExecutable": "${sourceDir}/conda/cmake.sh", "cacheVariables": { "CMAKE_IGNORE_PREFIX_PATH": { "type": "STRING", @@ -189,7 +187,6 @@ "lhs": "${hostSystemName}", "rhs": "Windows" }, - "cmakeExecutable": "${sourceDir}/conda/cmake.cmd", "cacheVariables": { "CMAKE_INSTALL_PREFIX": { "type": "FILEPATH", diff --git a/conda/cmake.cmd b/conda/cmake.cmd deleted file mode 100644 index 3f63072703..0000000000 --- a/conda/cmake.cmd +++ /dev/null @@ -1 +0,0 @@ -mamba run --live-stream -n freecad cmake %* diff --git a/conda/cmake.sh b/conda/cmake.sh deleted file mode 100755 index 958e1a8da4..0000000000 --- a/conda/cmake.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -mamba run --live-stream -n freecad cmake $@ diff --git a/conda/conda-env-qt6.yaml b/conda/conda-env-qt6.yaml deleted file mode 100644 index 77d325217e..0000000000 --- a/conda/conda-env-qt6.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: freecad -channels: -- conda-forge -dependencies: -- conda-forge/noarch::conda-libmamba-solver==24.11.1 -- conda-devenv -- mamba -- python==3.12.* -- zstd diff --git a/conda/conda-env.yaml b/conda/conda-env.yaml deleted file mode 100644 index 495ce08296..0000000000 --- a/conda/conda-env.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: freecad -channels: -- conda-forge -dependencies: -- conda-forge/noarch::conda-libmamba-solver==24.11.1 -- conda-devenv -- mamba -- python==3.11.* -- zstd==1.5.6 diff --git a/conda/environment-qt6.devenv.yml b/conda/environment-qt6.devenv.yml deleted file mode 100644 index 7a8c8dd7c8..0000000000 --- a/conda/environment-qt6.devenv.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: freecad -channels: -- conda-forge -- conda-forge/label/pivy_rc -dependencies: -- conda-forge/noarch::conda-libmamba-solver==24.11.1 -- libspnav # [linux] -- kernel-headers_linux-64 # [linux and x86_64] -- libdrm-cos7-x86_64 # [linux and x86_64] -- libselinux-cos7-x86_64 # [linux and x86_64] -- libsepol-cos7-x86_64 # [linux and x86_64] -- libx11-common-cos7-x86_64 # [linux and x86_64] -- libx11-cos7-x86_64 # [linux and x86_64] -- libxau-cos7-x86_64 # [linux and x86_64] -- libxcb-cos7-x86_64 # [linux and x86_64] -- libxdamage-cos7-x86_64 # [linux and x86_64] -- libxext-cos7-x86_64 # [linux and x86_64] -- libxfixes-cos7-x86_64 # [linux and x86_64] -- libxi-cos7-x86_64 # [linux and x86_64] -- libxi-devel-cos7-x86_64 # [linux and x86_64] -- libxxf86vm-cos7-x86_64 # [linux and x86_64] -- mesa-dri-drivers-cos7-x86_64 # [linux and x86_64] -- mesa-libegl-cos7-x86_64 # [linux and x86_64] -- mesa-libegl-devel-cos7-x86_64 # [linux and x86_64] -- mesa-libgl-cos7-x86_64 # [linux and x86_64] -- mesa-libgl-devel-cos7-x86_64 # [linux and x86_64] -- pixman-cos7-x86_64 # [linux and x86_64] -- sysroot_linux-64 # [linux and x86_64] -- xorg-x11-server-common-cos7-x86_64 # [linux and x86_64] -- xorg-x11-server-xvfb-cos7-x86_64 # [linux and x86_64] -- kernel-headers_linux-aarch64 # [linux and aarch64] -- libdrm-cos7-aarch64 # [linux and aarch64] -- libglvnd-cos7-aarch64 # [linux and aarch64] -- libglvnd-glx-cos7-aarch64 # [linux and aarch64] -- libselinux-cos7-aarch64 # [linux and aarch64] -- libsepol-cos7-aarch64 # [linux and aarch64] -- libx11-common-cos7-aarch64 # [linux and aarch64] -- libx11-cos7-aarch64 # [linux and aarch64] -- libxau-cos7-aarch64 # [linux and aarch64] -- libxcb-cos7-aarch64 # [linux and aarch64] -- libxdamage-cos7-aarch64 # [linux and aarch64] -- libxext-cos7-aarch64 # [linux and aarch64] -- libxfixes-cos7-aarch64 # [linux and aarch64] -- libxi-cos7-aarch64 # [linux and aarch64] -- libxi-devel-cos7-aarch64 # [linux and aarch64] -- libxxf86vm-cos7-aarch64 # [linux and aarch64] -- mesa-dri-drivers-cos7-aarch64 # [linux and aarch64] -- mesa-khr-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libegl-cos7-aarch64 # [linux and aarch64] -- mesa-libegl-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libgbm-cos7-aarch64 # [linux and aarch64] -- mesa-libgl-cos7-aarch64 # [linux and aarch64] -- mesa-libgl-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libglapi-cos7-aarch64 # [linux and aarch64] -- pixman-cos7-aarch64 # [linux and aarch64] -- sysroot_linux-aarch64 # [linux and aarch64] -- xorg-x11-server-common-cos7-aarch64 # [linux and aarch64] -- xorg-x11-server-xvfb-cos7-aarch64 # [linux and aarch64] -- sed # [unix] -- ccache -- clang-format -- cmake -- coin3d -- compilers -- conda -- conda-devenv -- debugpy -- doxygen -- eigen -- fmt -- freetype -- git -- gmsh -- graphviz -- hdf5 -- libboost-devel -- libcxx -- mamba -- matplotlib -- ninja -- numpy -- occt -- openssl -- pcl -- pip -- conda-forge/label/pivy_rc::pivy -- pkg-config -- ply -- pre-commit -- pybind11 -- pyside6 -- python==3.12.* -- pyyaml -- qt6-main -- smesh -- swig -- vtk==9.2.6 -- xerces-c -- yaml-cpp -- zlib -- zstd diff --git a/conda/environment.devenv.yml b/conda/environment.devenv.yml deleted file mode 100644 index 022a353490..0000000000 --- a/conda/environment.devenv.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: freecad -channels: -- conda-forge -dependencies: -- conda-forge/noarch::conda-libmamba-solver==24.11.1 -- libspnav # [linux] -- kernel-headers_linux-64 # [linux and x86_64] -- libdrm-cos6-x86_64 # [linux and x86_64] -- libselinux-cos6-x86_64 # [linux and x86_64] -- libsepol-cos6-x86_64 # [linux and x86_64] -- libx11-common-cos6-x86_64 # [linux and x86_64] -- libx11-cos6-x86_64 # [linux and x86_64] -- libxau-cos6-x86_64 # [linux and x86_64] -- libxcb-cos6-x86_64 # [linux and x86_64] -- libxdamage-cos6-x86_64 # [linux and x86_64] -- libxext-cos6-x86_64 # [linux and x86_64] -- libxfixes-cos6-x86_64 # [linux and x86_64] -- libxi-cos6-x86_64 # [linux and x86_64] -- libxi-devel-cos6-x86_64 # [linux and x86_64] -- libxxf86vm-cos6-x86_64 # [linux and x86_64] -- mesa-dri-drivers-cos6-x86_64 # [linux and x86_64] -- mesa-dri1-drivers-cos6-x86_64 # [linux and x86_64] -- mesa-libegl-cos6-x86_64 # [linux and x86_64] -- mesa-libegl-devel-cos6-x86_64 # [linux and x86_64] -- mesa-libgl-cos6-x86_64 # [linux and x86_64] -- mesa-libgl-devel-cos6-x86_64 # [linux and x86_64] -- pixman-cos6-x86_64 # [linux and x86_64] -- sysroot_linux-64 # [linux and x86_64] -- xorg-x11-server-common-cos6-x86_64 # [linux and x86_64] -- xorg-x11-server-xvfb-cos6-x86_64 # [linux and x86_64] -- kernel-headers_linux-aarch64 # [linux and aarch64] -- libdrm-cos7-aarch64 # [linux and aarch64] -- libglvnd-cos7-aarch64 # [linux and aarch64] -- libglvnd-glx-cos7-aarch64 # [linux and aarch64] -- libselinux-cos7-aarch64 # [linux and aarch64] -- libsepol-cos7-aarch64 # [linux and aarch64] -- libx11-common-cos7-aarch64 # [linux and aarch64] -- libx11-cos7-aarch64 # [linux and aarch64] -- libxau-cos7-aarch64 # [linux and aarch64] -- libxcb-cos7-aarch64 # [linux and aarch64] -- libxdamage-cos7-aarch64 # [linux and aarch64] -- libxext-cos7-aarch64 # [linux and aarch64] -- libxfixes-cos7-aarch64 # [linux and aarch64] -- libxi-cos7-aarch64 # [linux and aarch64] -- libxi-devel-cos7-aarch64 # [linux and aarch64] -- libxxf86vm-cos7-aarch64 # [linux and aarch64] -- mesa-dri-drivers-cos7-aarch64 # [linux and aarch64] -- mesa-khr-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libegl-cos7-aarch64 # [linux and aarch64] -- mesa-libegl-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libgbm-cos7-aarch64 # [linux and aarch64] -- mesa-libgl-cos7-aarch64 # [linux and aarch64] -- mesa-libgl-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libglapi-cos7-aarch64 # [linux and aarch64] -- pixman-cos7-aarch64 # [linux and aarch64] -- sysroot_linux-aarch64 # [linux and aarch64] -- xorg-x11-server-common-cos7-aarch64 # [linux and aarch64] -- xorg-x11-server-xvfb-cos7-aarch64 # [linux and aarch64] -- sed # [unix] -- ccache -- clang-format -- cmake -- coin3d -- compilers -- conda -- conda-devenv -- debugpy -- doxygen -- eigen -- fmt -- freetype -- git -- gmsh -- graphviz -- hdf5 -- libboost-devel -- libcxx -- mamba -- matplotlib -- ninja -- numpy -- occt -- openssl -- pcl -- pip -- pivy -- pkg-config -- ply -- pre-commit -- pybind11 -- pyside2 -- python==3.11.* -- pyyaml -- qt-main -- smesh -- swig -- vtk==9.2.6 -- xerces-c -- yaml-cpp -- zlib -- zstd==1.5.6 diff --git a/conda/setup-environment-qt6.sh b/conda/setup-environment-qt6.sh deleted file mode 100755 index d656491fb3..0000000000 --- a/conda/setup-environment-qt6.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# create the conda environment as a subdirectory -mamba env create -p .conda/freecad -f conda/conda-env.yaml - -# add the environment subdirectory to the conda configuration -conda config --add envs_dirs $CONDA_PREFIX/envs -conda config --add envs_dirs $(pwd)/.conda -conda config --set env_prompt "({name})" - -# install the FreeCAD dependencies into the environment -mamba run --live-stream -n freecad mamba-devenv --no-prune -f conda/environment-qt6.devenv.yml diff --git a/conda/setup-environment.cmd b/conda/setup-environment.cmd deleted file mode 100644 index dd51d7c739..0000000000 --- a/conda/setup-environment.cmd +++ /dev/null @@ -1,10 +0,0 @@ -:: create the conda environment as a subdirectory -call mamba env create -p .conda/freecad -f conda/conda-env.yaml - -:: add the environment subdirectory to the conda configuration -call conda config --add envs_dirs %CONDA_PREFIX%/envs -call conda config --add envs_dirs %CD%/.conda -call conda config --set env_prompt "({name})" - -:: install the FreeCAD dependencies into the environment -call mamba run --live-stream -n freecad mamba-devenv --no-prune -f conda/environment.devenv.yml diff --git a/conda/setup-environment.sh b/conda/setup-environment.sh deleted file mode 100755 index bd75d76602..0000000000 --- a/conda/setup-environment.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# create the conda environment as a subdirectory -mamba env create -p .conda/freecad -f conda/conda-env.yaml - -# add the environment subdirectory to the conda configuration -conda config --add envs_dirs $CONDA_PREFIX/envs -conda config --add envs_dirs $(pwd)/.conda -conda config --set env_prompt "({name})" - -# install the FreeCAD dependencies into the environment -mamba run --live-stream -n freecad mamba-devenv --no-prune -f conda/environment.devenv.yml From 1b7c12629c11bf56a5769d090d63c1d534d79079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbyn=C4=9Bk=20Winkler?= Date: Wed, 25 Jun 2025 12:32:56 +0200 Subject: [PATCH 23/30] Improve compatibility with VSCode - remove build directory override, use build directory from CMakePresets.json without this VSCode cannot use builddir created by pixi - add *-debug and *-release commands everywhere --- pixi.toml | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/pixi.toml b/pixi.toml index f36510890c..a2f3377459 100644 --- a/pixi.toml +++ b/pixi.toml @@ -150,35 +150,41 @@ freecad-stubs = "*" ## Qt 6 Configuration Presets [target.linux-64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-debug = { cmd = [ "cmake", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-linux-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} [target.linux-aarch64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-debug = { cmd = [ "cmake", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-linux-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} [target.osx-64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-debug = { cmd = [ "cmake", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-macos-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} [target.osx-arm64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-debug = { cmd = [ "cmake", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-macos-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} [target.win-64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-windows-debug", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-windows-debug", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-windows-release", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -freecad = { cmd = [ ".pixi/envs/default/Library/bin/FreeCAD.exe" ], depends-on = ["install"]} +configure-debug = { cmd = [ "cmake", "--preset", "conda-windows-debug", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-windows-release", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +freecad-debug = { cmd = [ ".pixi/envs/default/Library/bin/FreeCAD.exe" ], depends-on = ["install-debug"]} +freecad-release = { cmd = [ ".pixi/envs/default/Library/bin/FreeCAD.exe" ], depends-on = ["install-release"]} -## Tasks [tasks] initialize = { cmd = ["git", "submodule", "update", "--init", "--recursive"]} -build = { cmd = [ "cmake", "--build", "build" ] } -install = { cmd = [ "cmake", "--install", "build" ] } -test = { cmd = [ "ctest", "--test-dir", "build" ] } -freecad = "build/bin/FreeCAD" +# redirect to debug by default +configure = [{ task = "configure-debug" }] +build = [{ task = "build-debug" }] +install = [{ task = "install-debug" }] +test = [{ task = "test-debug" }] +freecad = [{ task = "freecad-debug" }] + +build-debug = { cmd = ["cmake", "--build", "build/debug"]} +build-release = { cmd = ["cmake", "--build", "build/release"]} +install-debug = { cmd = ["cmake", "--install", "build/debug"]} +install-release = { cmd = ["cmake", "--install", "build/release"]} +test-debug = { cmd = ["ctest", "--test-dir", "build/debug"]} +test-release = { cmd = ["ctest", "--test-dir", "build/release"]} +freecad-debug = "build/debug/bin/FreeCAD" +freecad-release = "build/release/bin/FreeCAD" From 91bfde1fd2dbc29bc65fd4e12e759fb0edd5bda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbyn=C4=9Bk=20Winkler?= Date: Wed, 25 Jun 2025 13:09:19 +0200 Subject: [PATCH 24/30] update python debugger type to debugpy vscode says that "python" is deprecated --- contrib/.vscode/launch.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/.vscode/launch.json b/contrib/.vscode/launch.json index c54dd2c8a0..b8c63d1c84 100644 --- a/contrib/.vscode/launch.json +++ b/contrib/.vscode/launch.json @@ -100,7 +100,7 @@ }, { "name": "Python debugger", - "type": "python", + "type": "debugpy", "request": "attach", "preLaunchTask": "WaitForDebugpy", "redirectOutput": true, @@ -131,4 +131,4 @@ } } ] -} \ No newline at end of file +} From 9c91b959a60b88a9d029bf195f1df3b7c6b287b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbyn=C4=9Bk=20Winkler?= Date: Wed, 25 Jun 2025 13:52:47 +0200 Subject: [PATCH 25/30] build and install debug build explicitly --- .github/workflows/sub_buildPixi.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sub_buildPixi.yml b/.github/workflows/sub_buildPixi.yml index 9d600149aa..759a4e3cab 100644 --- a/.github/workflows/sub_buildPixi.yml +++ b/.github/workflows/sub_buildPixi.yml @@ -54,7 +54,8 @@ jobs: CCACHE_MAXSIZE: 1G CCACHE_COMPRESS: true CCACHE_COMPRESSLEVEL: 5 - builddir: ${{ github.workspace }}/build/ + config: debug + builddir: ${{ github.workspace }}/build/debug/ cacheKey: pixi-${{ matrix.os }} logdir: ${{ github.workspace }}/logs/ reportdir: ${{ github.workspace }}/report/ @@ -133,11 +134,11 @@ jobs: - name: CMake Configure run: | - pixi run configure-release + pixi run configure-${{ env.config }} - name: CMake Build run: | - pixi run build + pixi run build-${{ env.config }} - name: Print ccache statistics after Build run: | @@ -174,7 +175,7 @@ jobs: - name: CMake Install run: | - pixi run install + pixi run install-${{ env.config }} - name: FreeCAD CLI tests on install if: runner.os != 'Windows' From 0e8a7b6319f4ab54b3c3d21c062c123ee35a72d9 Mon Sep 17 00:00:00 2001 From: FEA-eng <59876896+FEA-eng@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:31:55 +0200 Subject: [PATCH 26/30] Sketcher: Add tooltip for autoscale feature (#22252) * Sketcher: Update SketcherSettings.ui * Sketcher: Update SketcherSettings.ui --- src/Mod/Sketcher/Gui/SketcherSettings.ui | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Mod/Sketcher/Gui/SketcherSettings.ui b/src/Mod/Sketcher/Gui/SketcherSettings.ui index ca3ef8bc83..37d3e1c9db 100644 --- a/src/Mod/Sketcher/Gui/SketcherSettings.ui +++ b/src/Mod/Sketcher/Gui/SketcherSettings.ui @@ -293,21 +293,12 @@ This setting is only for the toolbar. Whichever you choose, all tools are always - - - Always - - - - - Never - - - - - Only if there is no visual scale indicator - - + + Select the mode of automatic geometry scaling upon first dimension: +'Always': Automatic scaling upon first dimension is always performed. +'Never': Automatic scaling upon first dimension is never performed. +'When no scale feature is visible': Automatic scaling upon first dimension is only performed if there are no visible objects in the 3D view. + From d2a4f8994fba9e3e62098f1ff9a1af67e0ecf6e1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 19:19:07 +0000 Subject: [PATCH 27/30] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-clang-format: 64827eb3528d4dc019b01153e9fb79107241405f → 6b9072cd80691b1b48d80046d884409fb1d962d1](https://github.com/pre-commit/mirrors-clang-format/compare/64827eb3528d4dc019b01153e9fb79107241405f...6b9072cd80691b1b48d80046d884409fb1d962d1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1b60a15e7..bf054f58f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,6 +66,6 @@ repos: - id: black args: ['--line-length', '100'] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: 64827eb3528d4dc019b01153e9fb79107241405f # frozen: v20.1.6 + rev: 6b9072cd80691b1b48d80046d884409fb1d962d1 # frozen: v20.1.7 hooks: - id: clang-format From ff5a25784c1c0955bf1efafea72e7e19916c83f0 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:19:19 +0200 Subject: [PATCH 28/30] BIM: fix autojoin behavior (#22303) * BIM: fix autojoin behavior * Use new deletebase argument of joinWalls * Remove colon from task bar title --- src/Mod/BIM/bimcommands/BimWall.py | 100 +++++++++++++++-------------- 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/src/Mod/BIM/bimcommands/BimWall.py b/src/Mod/BIM/bimcommands/BimWall.py index 35c91d5938..bfa3293d48 100644 --- a/src/Mod/BIM/bimcommands/BimWall.py +++ b/src/Mod/BIM/bimcommands/BimWall.py @@ -114,7 +114,7 @@ class Arch_Wall: self.tracker = DraftTrackers.boxTracker() FreeCADGui.Snapper.getPoint(callback=self.getPoint, extradlg=self.taskbox(), - title=translate("Arch","First point of wall")+":") + title=translate("Arch", "First point of wall")) FreeCADGui.draftToolBar.continueCmd.show() def getPoint(self,point=None,obj=None): @@ -132,8 +132,6 @@ class Arch_Wall: """ import Draft - import Part - import Arch import ArchWall from draftutils import gui_utils if obj: @@ -154,40 +152,36 @@ class Arch_Wall: callback=self.getPoint, movecallback=self.update, extradlg=self.taskbox(), - title=translate("Arch","Next point")+":",mode="line") + title=translate("Arch", "Next point"), + mode="line") elif len(self.points) == 2: FreeCAD.activeDraftCommand = None FreeCADGui.Snapper.off() self.tracker.off() - l = Part.LineSegment(self.wp.get_local_coords(self.points[0]), - self.wp.get_local_coords(self.points[1])) - self.doc.openTransaction(translate("Arch","Create Wall")) - FreeCADGui.addModule("Arch") - FreeCADGui.doCommand('import Part') - FreeCADGui.doCommand('trace=Part.LineSegment(FreeCAD.'+str(l.StartPoint)+',FreeCAD.'+str(l.EndPoint)+')') - if not self.existing: - # no existing wall snapped, just add a default wall - self.addDefault() - else: - if self.JOIN_WALLS_SKETCHES: - # join existing subwalls first if possible, then add the new one - w = Arch.joinWalls(self.existing) - if w: - if ArchWall.areSameWallTypes([w,self]): - FreeCADGui.doCommand('FreeCAD.ActiveDocument.'+w.Name+'.Base.addGeometry(trace)') - else: - # if not possible, add new wall as addition to the existing one - self.addDefault() - if self.AUTOJOIN: - FreeCADGui.doCommand('Arch.addComponents(FreeCAD.ActiveDocument.'+self.doc.Objects[-1].Name+',FreeCAD.ActiveDocument.'+w.Name+')') - else: - self.addDefault() - else: - # add new wall as addition to the first existing one - self.addDefault() - if self.AUTOJOIN: - FreeCADGui.doCommand('Arch.addComponents(FreeCAD.ActiveDocument.'+self.doc.Objects[-1].Name+',FreeCAD.ActiveDocument.'+self.existing[0].Name+')') + + self.doc.openTransaction(translate("Arch", "Create Wall")) + + # Some code in gui_utils.autogroup requires a wall shape to determine + # the target group. We therefore need to create a wall first. + self.addDefault() + wall = self.doc.Objects[-1] + wallGrp = wall.getParentGroup() + + if (self.JOIN_WALLS_SKETCHES or self.AUTOJOIN) \ + and self.existing \ + and self.existing[-1].getParentGroup() == wallGrp: + oldWall = self.existing[-1] + if self.JOIN_WALLS_SKETCHES and ArchWall.areSameWallTypes([wall, oldWall]): + FreeCADGui.doCommand( + "Arch.joinWalls([wall, doc." + oldWall.Name + "], " + + "delete=True, deletebase=True)" + ) + elif self.AUTOJOIN: + if wallGrp is not None: + FreeCADGui.doCommand("wall.getParentGroup().removeObject(wall)") + FreeCADGui.doCommand("Arch.addComponents(wall, doc." + oldWall.Name + ")") + self.doc.commitTransaction() self.doc.recompute() # gui_utils.end_all_events() # Causes a crash on Linux. @@ -200,36 +194,48 @@ class Arch_Wall: Used solely by _CommandWall.getPoint() when the interactive mode has selected two points. - - Relies on the assumption that FreeCADGui.doCommand() has already - created a Part.LineSegment assigned as the variable "trace" """ - from draftutils import params + + sta = self.wp.get_local_coords(self.points[0]) + end = self.wp.get_local_coords(self.points[1]) + + FreeCADGui.doCommand("import Part") FreeCADGui.addModule("Draft") + FreeCADGui.addModule("Arch") FreeCADGui.addModule("WorkingPlane") + FreeCADGui.doCommand("doc = FreeCAD.ActiveDocument") FreeCADGui.doCommand("wp = WorkingPlane.get_working_plane()") + FreeCADGui.doCommand( + "trace = Part.LineSegment(FreeCAD." + str(sta) + ", FreeCAD." + str(end) + ")" + ) if params.get_param_arch("WallSketches"): # Use ArchSketch if SketchArch add-on is present try: import ArchSketchObject - FreeCADGui.doCommand('import ArchSketchObject') - FreeCADGui.doCommand('base=ArchSketchObject.makeArchSketch()') + FreeCADGui.doCommand("import ArchSketchObject") + FreeCADGui.doCommand("base = ArchSketchObject.makeArchSketch()") except: - FreeCADGui.doCommand('base=FreeCAD.ActiveDocument.addObject("Sketcher::SketchObject","WallTrace")') - FreeCADGui.doCommand('base.Placement = wp.get_placement()') - FreeCADGui.doCommand('base.addGeometry(trace)') + FreeCADGui.doCommand( + "base = doc.addObject(\"Sketcher::SketchObject\", \"WallTrace\")" + ) + FreeCADGui.doCommand("base.Placement = wp.get_placement()") + FreeCADGui.doCommand("base.addGeometry(trace)") else: - FreeCADGui.doCommand('base=Draft.make_line(trace)') + FreeCADGui.doCommand("base = Draft.make_line(trace)") # The created line should not stay selected as this causes an issue in continue mode. # Two walls would then be created based on the same line. FreeCADGui.Selection.clearSelection() - FreeCADGui.doCommand('base.Placement = wp.get_placement()') - FreeCADGui.doCommand('FreeCAD.ActiveDocument.recompute()') - FreeCADGui.doCommand('wall = Arch.makeWall(base,width='+str(self.Width)+',height='+str(self.Height)+',align="'+str(self.Align)+'")') - FreeCADGui.doCommand('wall.Normal = wp.axis') + FreeCADGui.doCommand("base.Placement = wp.get_placement()") + FreeCADGui.doCommand("doc.recompute()") + FreeCADGui.doCommand( + "wall = Arch.makeWall(base, width=" + str(self.Width) + + ", height=" + str(self.Height) + ", align=\"" + str(self.Align) + "\")" + ) + FreeCADGui.doCommand("wall.Normal = wp.axis") if self.MultiMat: - FreeCADGui.doCommand("wall.Material = FreeCAD.ActiveDocument."+self.MultiMat.Name) + FreeCADGui.doCommand("wall.Material = doc." + self.MultiMat.Name) + FreeCADGui.doCommand("doc.recompute()") # required as some autogroup code requires the wall shape FreeCADGui.doCommand("Draft.autogroup(wall)") def update(self,point,info): From 82698073f176d889578d61c1c58d2270f5da3a40 Mon Sep 17 00:00:00 2001 From: Max Wilfinger <6246609+maxwxyz@users.noreply.github.com> Date: Tue, 8 Jul 2025 23:23:01 +0200 Subject: [PATCH 29/30] Update Help menu. Remove outdated links; add Developers handbook (#22283) * Update Help menu. Remove outdated links; add Developers handbook * Apply suggestions from code review * Update src/Gui/CommandStd.cpp * Update Shortcuts.cfg --- src/Gui/CommandStd.cpp | 125 ++++++------------ src/Gui/PreferencePackTemplates/Shortcuts.cfg | 4 +- src/Gui/Workbench.cpp | 6 +- 3 files changed, 42 insertions(+), 93 deletions(-) diff --git a/src/Gui/CommandStd.cpp b/src/Gui/CommandStd.cpp index d6e0087af7..cc93595e84 100644 --- a/src/Gui/CommandStd.cpp +++ b/src/Gui/CommandStd.cpp @@ -574,8 +574,8 @@ StdCmdFreeCADDonation::StdCmdFreeCADDonation() :Command("Std_FreeCADDonation") { sGroup = "Help"; - sMenuText = QT_TR_NOOP("Support FreeCA&D"); - sToolTipText = QT_TR_NOOP("Support FreeCAD development"); + sMenuText = QT_TR_NOOP("Donate to FreeCA&D"); + sToolTipText = QT_TR_NOOP("Support the FreeCAD development"); sWhatsThis = "Std_FreeCADDonation"; sStatusTip = sToolTipText; sPixmap = "internet-web-browser"; @@ -586,11 +586,45 @@ void StdCmdFreeCADDonation::activated(int iMsg) { Q_UNUSED(iMsg); ParameterGrp::handle hURLGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Websites"); - std::string url = hURLGrp->GetASCII("DonatePage", "https://wiki.freecad.org/Donate"); + std::string url = hURLGrp->GetASCII("DonatePage", "https://www.freecad.org/sponsor"); hURLGrp->SetASCII("DonatePage", url.c_str()); OpenURLInBrowser(url.c_str()); } +//=========================================================================== +// Std_FreeDevHandbook + +//=========================================================================== + +DEF_STD_CMD(StdCmdDevHandbook) + +StdCmdDevHandbook::StdCmdDevHandbook() + + : Command("Std_DevHandbook") +{ + sGroup = "Help"; + sMenuText = QT_TR_NOOP("Developers Handbook"); + + sToolTipText = QT_TR_NOOP("Handbook about FreeCAD development"); + + sWhatsThis = "Std_DevHandbook"; + sStatusTip = sToolTipText; + sPixmap = "internet-web-browser"; + eType = 0; +} + +void StdCmdDevHandbook::activated(int iMsg) + +{ + Q_UNUSED(iMsg); + ParameterGrp::handle hURLGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Websites"); + std::string url = hURLGrp->GetASCII("DevHandbook", "https://freecad.github.io/DevelopersHandbook/"); + + hURLGrp->SetASCII("DevHandbook", url.c_str()); + OpenURLInBrowser(url.c_str()); +} + //=========================================================================== // Std_FreeCADWebsite //=========================================================================== @@ -647,34 +681,6 @@ void StdCmdFreeCADUserHub::activated(int iMsg) OpenURLInBrowser(url.c_str()); } -//=========================================================================== -// Std_FreeCADPowerUserHub -//=========================================================================== - -DEF_STD_CMD(StdCmdFreeCADPowerUserHub) - -StdCmdFreeCADPowerUserHub::StdCmdFreeCADPowerUserHub() - :Command("Std_FreeCADPowerUserHub") -{ - sGroup = "Help"; - sMenuText = QT_TR_NOOP("&Python Scripting Documentation"); - sToolTipText = QT_TR_NOOP("Opens the Python Scripting documentation"); - sWhatsThis = "Std_FreeCADPowerUserHub"; - sStatusTip = sToolTipText; - sPixmap = "applications-python"; - eType = 0; -} - -void StdCmdFreeCADPowerUserHub::activated(int iMsg) -{ - Q_UNUSED(iMsg); - std::string defaulturl = QCoreApplication::translate(this->className(),"https://wiki.freecad.org/Power_users_hub").toStdString(); - ParameterGrp::handle hURLGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Websites"); - std::string url = hURLGrp->GetASCII("PowerUsers", defaulturl.c_str()); - hURLGrp->SetASCII("PowerUsers", url.c_str()); - OpenURLInBrowser(url.c_str()); -} - //=========================================================================== // Std_FreeCADForum //=========================================================================== @@ -703,59 +709,6 @@ void StdCmdFreeCADForum::activated(int iMsg) OpenURLInBrowser(url.c_str()); } -//=========================================================================== -// Std_FreeCADFAQ -//=========================================================================== - -DEF_STD_CMD(StdCmdFreeCADFAQ) - -StdCmdFreeCADFAQ::StdCmdFreeCADFAQ() - :Command("Std_FreeCADFAQ") -{ - sGroup = "Help"; - sMenuText = QT_TR_NOOP("FreeCAD FA&Q"); - sToolTipText = QT_TR_NOOP("Opens the Frequently Asked Questions"); - sWhatsThis = "Std_FreeCADFAQ"; - sStatusTip = sToolTipText; - sPixmap = "internet-web-browser"; - eType = 0; -} - -void StdCmdFreeCADFAQ::activated(int iMsg) -{ - Q_UNUSED(iMsg); - std::string defaulturl = QCoreApplication::translate(this->className(),"https://wiki.freecad.org/Frequently_asked_questions").toStdString(); - ParameterGrp::handle hURLGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Websites"); - std::string url = hURLGrp->GetASCII("FAQ", defaulturl.c_str()); - hURLGrp->SetASCII("FAQ", url.c_str()); - OpenURLInBrowser(url.c_str()); -} - -//=========================================================================== -// Std_PythonWebsite -//=========================================================================== - -DEF_STD_CMD(StdCmdPythonWebsite) - -StdCmdPythonWebsite::StdCmdPythonWebsite() - :Command("Std_PythonWebsite") -{ - sGroup = "Help"; - sMenuText = QT_TR_NOOP("Python Website"); - sToolTipText = QT_TR_NOOP("The official Python website"); - sWhatsThis = "Std_PythonWebsite"; - sStatusTip = QT_TR_NOOP("Python Website"); - sPixmap = "applications-python"; - eType = 0; -} - -void StdCmdPythonWebsite::activated(int iMsg) -{ - Q_UNUSED(iMsg); - OpenURLInBrowser("https://www.python.org"); -} - - //=========================================================================== // Std_ReportBug //=========================================================================== @@ -991,15 +944,13 @@ void CreateStdCommands() rcCmdMgr.addCommand(new StdCmdFreeCADWebsite()); rcCmdMgr.addCommand(new StdCmdFreeCADDonation()); rcCmdMgr.addCommand(new StdCmdFreeCADUserHub()); - rcCmdMgr.addCommand(new StdCmdFreeCADPowerUserHub()); rcCmdMgr.addCommand(new StdCmdFreeCADForum()); - rcCmdMgr.addCommand(new StdCmdFreeCADFAQ()); - rcCmdMgr.addCommand(new StdCmdPythonWebsite()); rcCmdMgr.addCommand(new StdCmdReportBug()); rcCmdMgr.addCommand(new StdCmdTextDocument()); rcCmdMgr.addCommand(new StdCmdUnitsCalculator()); rcCmdMgr.addCommand(new StdCmdUserEditMode()); rcCmdMgr.addCommand(new StdCmdReloadStyleSheet()); + rcCmdMgr.addCommand(new StdCmdDevHandbook()); //rcCmdMgr.addCommand(new StdCmdDownloadOnlineHelp()); //rcCmdMgr.addCommand(new StdCmdDescription()); } diff --git a/src/Gui/PreferencePackTemplates/Shortcuts.cfg b/src/Gui/PreferencePackTemplates/Shortcuts.cfg index 63db87461b..1ef9b11b24 100644 --- a/src/Gui/PreferencePackTemplates/Shortcuts.cfg +++ b/src/Gui/PreferencePackTemplates/Shortcuts.cfg @@ -627,6 +627,7 @@ Del + Ctrl+F6 @@ -642,9 +643,7 @@ - - @@ -690,7 +689,6 @@ - Ctrl+Q End diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 5acad2bc8f..5adecc1ce0 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -767,11 +767,11 @@ MenuItem* StdWorkbench::setupMenuBar() const // Help auto help = new MenuItem( menuBar ); help->setCommand("&Help"); - *help << "Std_OnlineHelp" << "Std_WhatsThis" << "Separator" + *help << "Std_WhatsThis" << "Separator" // Start page and additional separator are dynamically inserted here - << "Std_FreeCADUserHub" << "Std_FreeCADForum" << "Std_FreeCADFAQ" << "Std_ReportBug" << "Separator" + << "Std_FreeCADUserHub" << "Std_FreeCADForum" << "Std_ReportBug" << "Separator" << "Std_RestartInSafeMode" << "Separator" - << "Std_FreeCADPowerUserHub" << "Std_PythonHelp" << "Separator" + << "Std_DevHandbook" << "Std_PythonHelp" << "Separator" << "Std_FreeCADWebsite" << "Std_FreeCADDonation" << "Std_About"; return menuBar; From 1881686e7233da28da105885c931d02c30f25593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferm=C3=ADn=20Olaiz?= Date: Wed, 9 Jul 2025 01:16:50 -0300 Subject: [PATCH 30/30] Fix crash on out-of-bound vector access (#22397) --- src/Mod/Sketcher/App/SketchObject.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index b1a7433052..4b5d53e1cd 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -8632,9 +8632,6 @@ void SketchObject::rebuildExternalGeometry(std::optional extToAdd auto SubElements = ExternalGeometry.getSubValues(); assert(externalGeoRef.size() == Objects.size()); auto keys = externalGeoRef; - if (Types.size() != Objects.size()) { - Types.resize(Objects.size(), 0); - } // re-check for any missing geometry element. The code here has a side // effect that the linked external geometry will continue to work even if @@ -8695,6 +8692,10 @@ void SketchObject::rebuildExternalGeometry(std::optional extToAdd BRepBuilderAPI_MakeFace mkFace(sketchPlane); TopoDS_Shape aProjFace = mkFace.Shape(); + if (Types.size() != Objects.size()) { + Types.resize(Objects.size(), 0); + } + std::set refSet; // We use a vector here to keep the order (roughly) the same as ExternalGeometry std::vector > > newGeos;