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.
This commit is contained in:
Kacper Donat
2025-04-06 22:36:06 +02:00
parent 8d1f65e992
commit a32594faea
23 changed files with 3749 additions and 93 deletions

View File

@@ -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<class... Ts>
struct Overloads: Ts...
{
using Ts::operator()...;
};
} // namespace Base
#endif // SRC_BASE_TOOLS_H_

View File

@@ -137,9 +137,11 @@
#include "WorkbenchManipulator.h"
#include "WidgetFactory.h"
#include "3Dconnexion/navlib/NavlibInterface.h"
#include "Inventor/SoFCPlacementIndicatorKit.h"
#include "QtWidgets.h"
#include <Inventor/SoFCPlacementIndicatorKit.h>
#include <OverlayManager.h>
#include <Base/ServiceProvider.h>
#ifdef BUILD_TRACY_FRAME_PROFILER
#include <tracy/Tracy.hpp>
@@ -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<Gui::BaseView*> passive;
bool isClosing {false};
@@ -372,6 +379,31 @@ struct PyMethodDef FreeCADGui_methods[] = {
} // namespace Gui
void Application::initStyleParameterManager()
{
Base::registerServiceImplementation<StyleParameters::ParameterSource>(
new StyleParameters::BuiltInParameterSource({.name = QT_TR_NOOP("Built-in Parameters")}));
Base::registerServiceImplementation<StyleParameters::ParameterSource>(
new StyleParameters::UserParameterSource(
App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Themes/Tokens"),
{.name = QT_TR_NOOP("Theme Parameters"),
.options = StyleParameters::ParameterSourceOption::UserEditable}));
Base::registerServiceImplementation<StyleParameters::ParameterSource>(
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<StyleParameters::ParameterSource>()) {
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()

View File

@@ -30,6 +30,8 @@
#include <App/Application.h>
#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 */
//@{

View File

@@ -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

View File

@@ -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 {

View File

@@ -0,0 +1,764 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#include "Tools.h"
#include "Application.h"
#include "OverlayManager.h"
#include "DlgThemeEditor.h"
#include "ui_DlgThemeEditor.h"
#include "BitmapFactory.h"
#include <Base/ServiceProvider.h>
#include <Base/Tools.h>
#ifndef _PreComp_
# include <ranges>
# include <QImageReader>
# include <QPainter>
# include <QStyledItemDelegate>
# include <QTimer>
#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<std::string> 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<Qt::ItemFlag> flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
};
class StyleParametersModel::Node
{
public:
explicit Node(std::unique_ptr<Item> data, Node* parent = nullptr)
: _parent(parent)
, _data(std::move(data))
{}
void appendChild(std::unique_ptr<Node> child)
{
child->_parent = this;
_children.push_back(std::move(child));
}
void removeChild(const int row)
{
if (row >= 0 && row < static_cast<int>(_children.size())) {
_children.erase(_children.begin() + row);
}
}
Node* child(const int row) const
{
if (row < 0 || row >= static_cast<int>(_children.size())) {
if (!_empty) {
_empty = std::make_unique<Node>(nullptr, const_cast<Node*>(this));
}
return _empty.get();
}
return _children[row].get();
}
int childCount() const
{
return static_cast<int>(_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<int>(i);
}
}
return -1;
}
Item* data() const
{
return _data.get();
}
template<class T>
T* data() const
{
return dynamic_cast<T*>(_data.get());
}
Node* parent() const
{
return _parent;
}
private:
Node* _parent;
std::vector<std::unique_ptr<Node>> _children {};
mutable std::unique_ptr<Node> _empty {};
std::unique_ptr<Item> _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<const StyleParametersModel*>(index.model());
if (!model) {
return nullptr;
}
if (model->item<StyleParametersModel::ParameterItem>(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<QLineEdit*>(editor)) {
lineEdit->setText(index.data(Qt::DisplayRole).toString());
}
}
void setModelData(QWidget* editor,
QAbstractItemModel* model,
const QModelIndex& index) const override
{
if (auto* lineEdit = qobject_cast<QLineEdit*>(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<const StyleParametersModel*>(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<StyleParametersModel::GroupItem>(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<StyleParameters::ParameterSource*>& 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<StyleParameters::Parameter> StyleParametersModel::all() const
{
std::map<std::string, StyleParameters::Parameter> result;
QtTools::walkTreeModel(this, [this, &result](const QModelIndex& index) {
if (auto parameterItem = item<ParameterItem>(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<StyleParameters::Parameter>(values.begin(), values.end());
}
std::optional<StyleParameters::Parameter> StyleParametersModel::get(const std::string& name) const
{
std::optional<StyleParameters::Parameter> result = std::nullopt;
QtTools::walkTreeModel(this, [this, &name, &result](const QModelIndex& index) {
if (auto parameterItem = item<ParameterItem>(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<ParameterItem>(index)) {
auto groupItem = item<GroupItem>(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<Node>(std::make_unique<GroupItem>(tr("Root"), nullptr));
for (auto* source : sources) {
auto groupNode = std::make_unique<Node>(
std::make_unique<GroupItem>(tr(source->metadata.name.c_str()), source));
for (const auto& parameter : source->all()) {
auto item = std::make_unique<Node>(
std::make_unique<ParameterItem>(QString::fromStdString(parameter.name), parameter));
if (source->metadata.options.testFlag(ReadOnly)) {
item->data<ParameterItem>()->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<GroupItem>(index)) {
for (const auto& parameter : groupItem->deleted) {
groupItem->source->remove(parameter);
}
groupItem->deleted.clear();
}
if (const auto& parameterItem = item<ParameterItem>(index)) {
const auto& groupItem = item<GroupItem>(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<GroupItem>(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<ParameterItem>(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<QColor>(value)) {
return colorPreview(std::get<QColor>(value));
}
}
}
if (auto groupItem = item<GroupItem>(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<ParameterItem>(index)) {
auto groupItem = item<GroupItem>(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<Node>(
std::make_unique<ParameterItem>(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<ParameterItem>(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<Node*>(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<Node*>(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<StyleParametersModel>(
Base::provideServiceImplementations<StyleParameters::ParameterSource>(),
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<ColumnDefinition> 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<const QModelIndex&>(&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"

View File

@@ -0,0 +1,152 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef DLGTHEMEEDITOR_H
#define DLGTHEMEEDITOR_H
#include "StyleParameters/ParameterManager.h"
#include <QDialog>
#include <QTreeView>
#include <optional>
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<ParameterSource*>& sources,
QObject* parent = nullptr);
~StyleParametersModel() override;
std::list<StyleParameters::Parameter> all() const override;
std::optional<StyleParameters::Parameter> 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 <typename T>
T* item(const QModelIndex& index) const
{
return dynamic_cast<T*>(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<ParameterSource*> sources;
std::unique_ptr<StyleParameters::ParameterManager> manager;
std::unique_ptr<Node> 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::DlgThemeEditor> ui;
std::unique_ptr<StyleParameters::ParameterManager> manager;
std::unique_ptr<StyleParametersModel> model;
};
} // Gui
#endif //DLGTHEMEEDITOR_H

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Gui::DlgThemeEditor</class>
<widget class="QDialog" name="Gui::DlgThemeEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1169</width>
<height>562</height>
</rect>
</property>
<property name="windowTitle">
<string>Theme Editor</string>
</property>
<layout class="QGridLayout" name="gridLayout" columnstretch="3,1">
<item row="1" column="1">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Preview</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="1">
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>CheckBox</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QScrollBar" name="horizontalScrollBar">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="spinBox"/>
</item>
<item row="8" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<widget class="QRadioButton" name="radioButton">
<property name="text">
<string>RadioButton</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="comboBox">
<item>
<property name="text">
<string>Item 1</string>
</property>
</item>
<item>
<property name="text">
<string>Item 2</string>
</property>
</item>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QTabWidget" name="tabWidget">
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Tab 1</string>
</attribute>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Tab 2</string>
</attribute>
</widget>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::ColorButton" name="colorButton"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="Gui::TokenTreeView" name="tokensTreeView">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::DoubleClicked</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::ColorButton</class>
<extends>QPushButton</extends>
<header>Gui/Widgets.h</header>
</customwidget>
<customwidget>
<class>Gui::TokenTreeView</class>
<extends>QTreeView</extends>
<header location="global">Gui/Dialogs/DlgThemeEditor.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -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;

View File

@@ -69,8 +69,10 @@
#include <list>
#include <map>
#include <numbers>
#include <optional>
#include <queue>
#include <random>
#include <ranges>
#include <set>
#include <sstream>
#include <stack>

View File

@@ -41,9 +41,12 @@
#include "DockWindowManager.h"
#include "ToolBarManager.h"
#include <Application.h>
#include <App/Application.h>
#include <ctime> // For generating a timestamped filename
#include <ctime> // For generating a timestamped filename
#include <Base/ServiceProvider.h>
#include <Dialogs/DlgThemeEditor.h>
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;

View File

@@ -31,6 +31,10 @@
#include "DlgSettingsUI.h"
#include "ui_DlgSettingsUI.h"
#include "Dialogs/DlgThemeEditor.h"
#include <Base/ServiceProvider.h>
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<QString, QString> 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<Gui::StyleParameters::ParameterManager>()) {
parameterManager->reload();
}
auto sheet = hGrp->GetASCII("StyleSheet");
bool tiledBG = hGrp->GetBool("TiledBackground", false);
Gui::Application::Instance->setStyleSheet(QString::fromUtf8(sheet.c_str()), tiledBG);

View File

@@ -62,6 +62,7 @@ protected:
const char *def,
QStringList filter = QStringList());
void openThemeEditor();
private:
std::unique_ptr<Ui_DlgSettingsUI> ui;
};

View File

@@ -31,7 +31,7 @@
</widget>
</item>
<item>
<layout class="QGridLayout" columnstretch="2,1,0">
<layout class="QGridLayout" columnstretch="2,1">
<item row="0" column="0">
<widget class="QLabel" name="label1">
<property name="text">
@@ -53,7 +53,7 @@
<property name="toolTip">
<string>This color might be used by your theme to let you customize it.</string>
</property>
<property name="color" stdset="0">
<property name="color">
<color>
<red>85</red>
<green>123</green>
@@ -119,7 +119,7 @@
<property name="toolTip">
<string>This color might be used by your theme to let you customize it.</string>
</property>
<property name="color" stdset="0">
<property name="color">
<color>
<red>85</red>
<green>123</green>
@@ -145,7 +145,7 @@
<property name="toolTip">
<string>This color might be used by your theme to let you customize it.</string>
</property>
<property name="color" stdset="0">
<property name="color">
<color>
<red>85</red>
<green>123</green>
@@ -165,12 +165,12 @@
<property name="toolTip">
<string>Style sheet how user interface will look like</string>
</property>
<property name="prefPath" stdset="0">
<cstring>MainWindow</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>StyleSheet</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>MainWindow</cstring>
</property>
<property name="prefType" stdset="0">
<cstring></cstring>
</property>
@@ -178,12 +178,12 @@
</item>
<item row="4" column="1">
<widget class="Gui::PrefComboBox" name="OverlayStyleSheets">
<property name="prefPath" stdset="0">
<cstring>MainWindow</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>OverlayActiveStyleSheet</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>MainWindow</cstring>
</property>
<property name="prefType" stdset="0">
<cstring></cstring>
</property>
@@ -191,6 +191,13 @@
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="themeEditorButton">
<property name="text">
<string>Open Theme Editor</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -230,12 +237,12 @@
<property name="value">
<number>16</number>
</property>
<property name="prefPath" stdset="0">
<cstring>TreeView</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>IconSize</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>TreeView</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
@@ -285,12 +292,12 @@
<property name="value">
<number>0</number>
</property>
<property name="prefPath" stdset="0">
<cstring>TreeView</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>ItemSpacing</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>TreeView</cstring>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
@@ -492,7 +499,7 @@
<item>
<spacer name="spacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@@ -510,26 +517,26 @@
<extends>QPushButton</extends>
<header>Gui/Widgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefColorButton</class>
<extends>Gui::ColorButton</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefComboBox</class>
<extends>QComboBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefSpinBox</class>
<extends>QSpinBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefColorButton</class>
<extends>Gui::ColorButton</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefCheckBox</class>
<extends>QCheckBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefComboBox</class>
<extends>QComboBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>ThemeAccentColor1</tabstop>

View File

@@ -0,0 +1,353 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#include "ParameterManager.h"
#include "Parser.h"
#ifndef _PreComp_
#include <QColor>
#include <QRegularExpression>
#include <QString>
#include <ranges>
#include <variant>
#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<Length>(*this)) {
auto [value, unit] = std::get<Length>(*this);
return fmt::format("{}{}", value, unit);
}
if (std::holds_alternative<QColor>(*this)) {
auto color = std::get<QColor>(*this);
return fmt::format("#{:0>6x}", 0xFFFFFF & color.rgb()); // NOLINT(*-magic-numbers)
}
return std::get<std::string>(*this);
}
ParameterSource::ParameterSource(const Metadata& metadata)
: metadata(metadata)
{}
InMemoryParameterSource::InMemoryParameterSource(const std::list<Parameter>& parameters,
const Metadata& metadata)
: ParameterSource(metadata)
{
for (const auto& parameter : parameters) {
InMemoryParameterSource::define(parameter);
}
}
std::list<Parameter> InMemoryParameterSource::all() const
{
auto values = parameters | std::ranges::views::values;
return std::list<Parameter>(values.begin(), values.end());
}
std::optional<Parameter> 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<Parameter> BuiltInParameterSource::all() const
{
std::list<Parameter> result;
for (const auto& name : params | std::views::keys) {
result.push_back(*get(name));
}
return result;
}
std::optional<Parameter> 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<Parameter> UserParameterSource::all() const
{
std::list<Parameter> result;
for (const auto& [token, value] : hGrp->GetASCIIMap()) {
result.push_back({
.name = token,
.value = value,
});
}
return result;
}
std::optional<Parameter> 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<QString(const QRegularExpressionMatch&)>& 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<Parameter> ParameterManager::parameters() const
{
std::set<Parameter, Parameter::NameComparator> 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<std::string> 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<Parameter> 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<Parameter> 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<ParameterSource*> ParameterManager::sources() const
{
return _sources;
}
} // namespace Gui::StyleParameters

View File

@@ -0,0 +1,457 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef STYLEPARAMETERS_PARAMETERMANAGER_H
#define STYLEPARAMETERS_PARAMETERMANAGER_H
#include <list>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include <QColor>
#include <App/Application.h>
#include <Base/Bitmask.h>
#include <Base/Parameter.h>
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<Length, QColor, std::string>
{
using std::variant<Length, QColor, std::string>::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<ParameterSourceOption>;
/**
* @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<Parameter> 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<Parameter> 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<std::string, Parameter> parameters;
public:
InMemoryParameterSource(const std::list<Parameter>& parameters, const Metadata& metadata);
std::list<Parameter> all() const override;
std::optional<Parameter> 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<Parameter> all() const override;
std::optional<Parameter> 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<std::string, ParameterGrp::handle> 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<Parameter> all() const override;
std::optional<Parameter> 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<ParameterSource*> _sources;
mutable std::map<std::string, Value> _resolved;
public:
struct ResolveContext
{
/// Names of parameters currently being resolved.
std::set<std::string> 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<Parameter> 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<std::string> 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> 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<ParameterSource*> sources() const;
};
} // namespace Gui::StyleParameters
ENABLE_BITMASK_OPERATORS(Gui::StyleParameters::ParameterSourceOption);
#endif // STYLEPARAMETERS_PARAMETERMANAGER_H

View File

@@ -0,0 +1,403 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#include "Parser.h"
#include "ParameterManager.h"
#ifndef _PreComp_
#include <QColor>
#include <QRegularExpression>
#include <QString>
#include <ranges>
#include <variant>
#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<QColor>(colorArg)) {
THROWM(Base::ExpressionError,
fmt::format("'{}' is not supported for colors", functionName));
}
auto color = std::get<QColor>(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<int>(std::get<Length>(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<Length>(lval) || !std::holds_alternative<Length>(rval)) {
THROWM(Base::ExpressionError, "Math operations are supported only on lengths");
}
auto lhs = std::get<Length>(lval);
auto rhs = std::get<Length>(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<QColor>(val)) {
THROWM(Base::ExpressionError, "Unary operations on colors are not supported");
}
auto v = std::get<Length>(val);
switch (op) {
case Operator::Add:
return v;
case Operator::Subtract:
return -v;
default:
THROWM(Base::ExpressionError, "Unknown unary operator");
}
}
std::unique_ptr<Expr> 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<Expr> Parser::parseExpression()
{
auto expr = parseTerm();
while (true) {
skipWhitespace();
if (match('+')) {
expr = std::make_unique<BinaryOp>(std::move(expr), Operator::Add, parseTerm());
}
else if (match('-')) {
expr = std::make_unique<BinaryOp>(std::move(expr), Operator::Subtract, parseTerm());
}
else {
break;
}
}
return expr;
}
std::unique_ptr<Expr> Parser::parseTerm()
{
auto expr = parseFactor();
while (true) {
skipWhitespace();
if (match('*')) {
expr = std::make_unique<BinaryOp>(std::move(expr), Operator::Multiply, parseFactor());
}
else if (match('/')) {
expr = std::make_unique<BinaryOp>(std::move(expr), Operator::Divide, parseFactor());
}
else {
break;
}
}
return expr;
}
std::unique_ptr<Expr> Parser::parseFactor()
{
skipWhitespace();
if (match('+') || match('-')) {
Operator op = (input[pos - 1] == '+') ? Operator::Add : Operator::Subtract;
return std::make_unique<UnaryOp>(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<Expr> 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<Color>(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<Color>(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<Expr> 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<ParameterReference>(input.substr(start, pos - start));
}
bool Parser::peekFunction()
{
skipWhitespace();
return pos < input.size() && isalpha(input[pos]);
}
std::unique_ptr<Expr> 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<std::unique_ptr<Expr>> 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<FunctionCall>(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<Expr> 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<Number>(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

View File

@@ -0,0 +1,170 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef STYLEPARAMETERS_PARSER_H
#define STYLEPARAMETERS_PARSER_H
#include <memory>
#include <string>
#include <vector>
#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<std::unique_ptr<Expr>> arguments;
FunctionCall(std::string functionName, std::vector<std::unique_ptr<Expr>> arguments)
: functionName(std::move(functionName))
, arguments(std::move(arguments))
{}
Value evaluate(const EvaluationContext& context) const override;
};
struct GuiExport BinaryOp: public Expr
{
std::unique_ptr<Expr> left, right;
Operator op;
BinaryOp(std::unique_ptr<Expr> left, Operator op, std::unique_ptr<Expr> 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<Expr> operand;
UnaryOp(Operator op, std::unique_ptr<Expr> 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<Expr> parse();
private:
bool peekString(const char* function) const;
std::unique_ptr<Expr> parseExpression();
std::unique_ptr<Expr> parseTerm();
std::unique_ptr<Expr> parseFactor();
bool peekColor();
std::unique_ptr<Expr> parseColor();
bool peekParameter();
std::unique_ptr<Expr> parseParameter();
bool peekFunction();
std::unique_ptr<Expr> parseFunctionCall();
int parseInt();
std::unique_ptr<Expr> parseNumber();
std::string parseUnit();
bool match(char expected);
void skipWhitespace();
};
} // namespace Gui::StyleParameters
#endif // STYLEPARAMETERS_PARSER_H

View File

@@ -1,5 +1,6 @@
/***************************************************************************
* Copyright (c) 2020 Werner Mayer <wmayer[at]users.sourceforge.net> *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
@@ -23,39 +24,100 @@
#ifndef GUI_TOOLS_H
#define GUI_TOOLS_H
#include <FCGlobal.h>
#include <QFontMetrics>
#include <QKeyEvent>
#include <QKeySequence>
#include <FCGlobal.h>
#include <QModelIndex>
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<typename Func>
concept TreeWalkCallable =
std::is_invocable_r_v<void, Func, const QModelIndex&> ||
std::is_invocable_r_v<bool, Func, const QModelIndex&>;
// 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<decltype(func), const QModelIndex&>;
if (index.isValid()) {
if constexpr (std::is_same_v<ReturnType, void>) {
func(index);
}
else if constexpr (std::is_same_v<ReturnType, bool>) {
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<typename T>
T valueOr(const QVariant& variant, const T& defaultValue)
{
return variant.canConvert<T>() ? variant.value<T>() : defaultValue;
}
} // namespace Gui::QtTools
#endif // GUI_TOOLS_H

View File

@@ -2,6 +2,9 @@
target_sources(Tests_run PRIVATE
Assistant.cpp
Camera.cpp
StyleParameters/StyleParametersApplicationTest.cpp
StyleParameters/ParserTest.cpp
StyleParameters/ParameterManagerTest.cpp
)
# Qt tests

View File

@@ -0,0 +1,326 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include <gtest/gtest.h>
#include <Gui/Application.h>
#include <Gui/StyleParameters/ParameterManager.h>
using namespace Gui::StyleParameters;
class ParameterManagerTest: public ::testing::Test
{
protected:
void SetUp() override
{
// Create test sources
auto source1 = std::make_unique<InMemoryParameterSource>(
std::list<Parameter> {
{"BaseSize", "8px"},
{"PrimaryColor", "#ff0000"},
{"SecondaryColor", "#00ff00"},
},
ParameterSource::Metadata {"Source 1"});
auto source2 = std::make_unique<InMemoryParameterSource>(
std::list<Parameter> {
{"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<std::unique_ptr<ParameterSource>> sources;
};
// Test basic parameter resolution
TEST_F(ParameterManagerTest, BasicParameterResolution)
{
{
auto result = manager.resolve("BaseSize");
EXPECT_TRUE(std::holds_alternative<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result1));
// Second resolution should use cached value
auto result2 = manager.resolve("BaseSize");
EXPECT_TRUE(std::holds_alternative<Length>(result2));
// Results should be identical
auto length1 = std::get<Length>(result1);
auto length2 = std::get<Length>(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<Length>(result1));
auto length1 = std::get<Length>(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<Length>(result2));
auto length2 = std::get<Length>(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<InMemoryParameterSource>(
std::list<Parameter> {
{"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<Length>(result));
auto length = std::get<Length>(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<std::string> 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<InMemoryParameterSource>(
std::list<Parameter> {
{"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<std::string>(result));
}
// Test complex expressions
TEST_F(ParameterManagerTest, ComplexExpressions)
{
// Create a source with complex expressions
auto complexSource = std::make_unique<InMemoryParameterSource>(
std::list<Parameter> {
{"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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<std::string>(result));
EXPECT_EQ(std::get<std::string>(result), "");
// Test invalid expression
auto invalidSource = std::make_unique<InMemoryParameterSource>(
std::list<Parameter> {
{"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<std::string>(invalidResult));
}

View File

@@ -0,0 +1,617 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include <gtest/gtest.h>
#include <QColor>
#include <Gui/StyleParameters/Parser.h>
#include <Gui/StyleParameters/ParameterManager.h>
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<InMemoryParameterSource>(
std::list<Parameter> {
{"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<std::unique_ptr<ParameterSource>> 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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(result);
EXPECT_DOUBLE_EQ(length.value, 10.0); // (10 - 5) * 2 = 5 * 2 = 10
EXPECT_EQ(length.unit, "");
}
}

View File

@@ -0,0 +1,94 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include <gtest/gtest.h>
#include "src/App/InitApplication.h"
#include <Gui/Application.h>
#include <Gui/StyleParameters/ParameterManager.h>
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; }");
}