Merge pull request #20668 from kadet1090/stylesheet-params

Gui: Add Style Parameter Manager to contain theme parameters
This commit is contained in:
Benjamin Nauck
2025-07-07 02:58:36 +02:00
committed by GitHub
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; }");
}