Merge pull request #20668 from kadet1090/stylesheet-params
Gui: Add Style Parameter Manager to contain theme parameters
This commit is contained in:
@@ -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_
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 */
|
||||
//@{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
764
src/Gui/Dialogs/DlgThemeEditor.cpp
Normal file
764
src/Gui/Dialogs/DlgThemeEditor.cpp
Normal 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"
|
||||
152
src/Gui/Dialogs/DlgThemeEditor.h
Normal file
152
src/Gui/Dialogs/DlgThemeEditor.h
Normal 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
|
||||
156
src/Gui/Dialogs/DlgThemeEditor.ui
Normal file
156
src/Gui/Dialogs/DlgThemeEditor.ui
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -69,8 +69,10 @@
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <numbers>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <random>
|
||||
#include <ranges>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stack>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -62,6 +62,7 @@ protected:
|
||||
const char *def,
|
||||
QStringList filter = QStringList());
|
||||
|
||||
void openThemeEditor();
|
||||
private:
|
||||
std::unique_ptr<Ui_DlgSettingsUI> ui;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
353
src/Gui/StyleParameters/ParameterManager.cpp
Normal file
353
src/Gui/StyleParameters/ParameterManager.cpp
Normal 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
|
||||
457
src/Gui/StyleParameters/ParameterManager.h
Normal file
457
src/Gui/StyleParameters/ParameterManager.h
Normal 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
|
||||
403
src/Gui/StyleParameters/Parser.cpp
Normal file
403
src/Gui/StyleParameters/Parser.cpp
Normal 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
|
||||
170
src/Gui/StyleParameters/Parser.h
Normal file
170
src/Gui/StyleParameters/Parser.h
Normal 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
|
||||
110
src/Gui/Tools.h
110
src/Gui/Tools.h
@@ -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
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
target_sources(Tests_run PRIVATE
|
||||
Assistant.cpp
|
||||
Camera.cpp
|
||||
StyleParameters/StyleParametersApplicationTest.cpp
|
||||
StyleParameters/ParserTest.cpp
|
||||
StyleParameters/ParameterManagerTest.cpp
|
||||
)
|
||||
|
||||
# Qt tests
|
||||
|
||||
326
tests/src/Gui/StyleParameters/ParameterManagerTest.cpp
Normal file
326
tests/src/Gui/StyleParameters/ParameterManagerTest.cpp
Normal 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));
|
||||
}
|
||||
617
tests/src/Gui/StyleParameters/ParserTest.cpp
Normal file
617
tests/src/Gui/StyleParameters/ParserTest.cpp
Normal 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, "");
|
||||
}
|
||||
}
|
||||
@@ -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; }");
|
||||
}
|
||||
Reference in New Issue
Block a user