diff --git a/doc/dox/jkqtcommon.dox b/doc/dox/jkqtcommon.dox
index d02f89931f..661ccdbf64 100644
--- a/doc/dox/jkqtcommon.dox
+++ b/doc/dox/jkqtcommon.dox
@@ -20,6 +20,11 @@ This group assembles a variety of mathematical tool functions that are used in d
Offers diverse function to convert different datatypes (e.g. double, int, diverse enums) to and from strings and for string manipulation.
+\defgroup jkqtptools_css CSS Parsing Tools
+\ingroup jkqtptools_jkqtcommon
+
+Offers parser(s) for certain subsets of css, that are used e.g. in JKQTPlotter's styling INI files (e.g. for gradients)
+
\defgroup jkqtptools_qt Tools around Qt's clasess
\ingroup jkqtptools_jkqtcommon
diff --git a/doc/dox/todo.dox b/doc/dox/todo.dox
index 76afc74618..fcb914dab6 100644
--- a/doc/dox/todo.dox
+++ b/doc/dox/todo.dox
@@ -30,7 +30,6 @@ This page lists several todos and wishes for future version of JKQTPlotter
plot: elongated grid to left of tick labels
plot: refactor print preview/export preview code
sryling: better styling/more styling options for data-tooltips
- styling: color gradients as fill-styles in style-INIs
user interactions: dialog(s) to edit x/y-range
diff --git a/doc/dox/whatsnew.dox b/doc/dox/whatsnew.dox
index 27c4bd1b43..0ca4319279 100644
--- a/doc/dox/whatsnew.dox
+++ b/doc/dox/whatsnew.dox
@@ -28,8 +28,8 @@ Changes, compared to \ref page_whatsnew_V4_0_0 "v4.0.0" include:
NEW/BREAKING: refactor CMake-Code, so static/dynamic switch is done via BUILD_SHARED_LIBS
, which retires JKQtPlotter_BUILD_STATIC_LIBS
, JKQtPlotter_BUILD_SHARED_LIBS
and removes the capability to build static and shared libraries in one location (fixes issue #104)
NEW: prepareed library for CMake's FetchContent-API
NEW: the different sub-libraries JKQTPlotter, JKQTFastPlotter (DEPRECATED), JKQTMath, JKQTMathText can be activated/deactivated with CMake options JKQtPlotter_BUILD_LIB_JKQTPLOTTER, JKQtPlotter_BUILD_LIB_JKQTFASTPLOTTER, JKQtPlotter_BUILD_LIB_JKQTMATHTEXT, JKQtPlotter_BUILD_LIB_JKQTMATH
- NEW add JKQTPExpected datatype
- NEW add jkqtp_roundToDigits()
+ NEW add JKQTPExpected datatype, jkqtp_roundToDigits(), generic RegularExpression functions (jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt() )
+ NEW CSS-parser JKQTPCSSParser in order to parse e.g. \c linear-gradient() specifications in jkqtp_String2QBrushStyleExt()
JKQTPlotter:
diff --git a/lib/jkqtcommon.pri b/lib/jkqtcommon.pri
index b89bb32123..6fcbc7bc71 100644
--- a/lib/jkqtcommon.pri
+++ b/lib/jkqtcommon.pri
@@ -28,6 +28,7 @@ isEmpty(JKQTP_COMMON_PRI_INCLUDED) {
$$PWD/jkqtcommon/jkqtphighrestimer.h \
$$PWD/jkqtcommon/jkqttools.h \
$$PWD/jkqtcommon/jkqtpicons.h \
+ $$PWD/jkqtcommon/jkqtpcsstools.h \
$$PWD/jkqtcommon/jkqtpcachingtools.h \
$$PWD/jkqtcommon/jkqtpconcurrencytools.h \
$$PWD/jkqtcommon/jkqtpexpected.h
@@ -45,6 +46,7 @@ isEmpty(JKQTP_COMMON_PRI_INCLUDED) {
$$PWD/jkqtcommon/jkqtphighrestimer.cpp \
$$PWD/jkqtcommon/jkqttools.cpp \
$$PWD/jkqtcommon/jkqtpicons.cpp \
+ $$PWD/jkqtcommon/jkqtpcsstools.cpp \
$$PWD/jkqtcommon/jkqtpcachingtools.cpp \
$$PWD/jkqtcommon/jkqtpconcurrencytools.cpp
diff --git a/lib/jkqtcommon/CMakeLists.txt b/lib/jkqtcommon/CMakeLists.txt
index ca7b2e3342..5a0d0098df 100644
--- a/lib/jkqtcommon/CMakeLists.txt
+++ b/lib/jkqtcommon/CMakeLists.txt
@@ -29,6 +29,7 @@ target_sources(${lib_name} PRIVATE
jkqtphighrestimer.cpp
jkqttools.cpp
jkqtpicons.cpp
+ jkqtpcsstools.cpp
)
# ... and add headers
target_sources(${lib_name} PUBLIC FILE_SET HEADERS TYPE HEADERS
@@ -47,6 +48,7 @@ target_sources(${lib_name} PUBLIC FILE_SET HEADERS TYPE HEADERS
jkqtpgeometrytools.h
jkqtpconcurrencytools.h
jkqtpcachingtools.h
+ jkqtpcsstools.h
jkqtpexpected.h
)
diff --git a/lib/jkqtcommon/jkqtpcsstools.cpp b/lib/jkqtcommon/jkqtpcsstools.cpp
new file mode 100644
index 0000000000..83a59edc0b
--- /dev/null
+++ b/lib/jkqtcommon/jkqtpcsstools.cpp
@@ -0,0 +1,417 @@
+/*
+ Copyright (c) 2008-2024 Jan W. Krieger ()
+
+
+ This software is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License (LGPL) as published by
+ the Free Software Foundation, either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program 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 (LGPL) for more details.
+
+ You should have received a copy of the GNU Lesser General Public License (LGPL)
+ along with this program. If not, see .
+*/
+
+
+
+#include "jkqtcommon/jkqtpcsstools.h"
+#include "jkqtcommon/jkqtpstringtools.h"
+#include
+#include
+
+
+JKQTPCSSParser::RawErrorTag_t JKQTPCSSParser::RawErrorTag={};
+
+
+
+JKQTPCSSParser::Token JKQTPCSSParser::peekNextToken(int *endPos)
+{
+ // on "RETURN" fromm this function: reset to current status and store the new pos in oldPos (if !=nullptr)
+ auto _final=JKQTPFinally(std::bind([&](JKQTPCSSParser* p, const Token oldToken, const int oldPos){
+ if (endPos) *endPos=p->pos;
+ p->CurrentToken=oldToken;
+ p->pos=oldPos;
+ }, this, CurrentToken, pos));
+
+ QChar ch('\0');
+ while(getCh(ch) && ch.isSpace()) {
+
+ }
+
+ static QString hexDigits="0123456789abcdefABCDEF";
+
+ if (ch==QChar('\0')) return Token(Token::END);
+ else if (ch==QChar(',')) return Token(Token::COMMA);
+ else if (ch==QChar('(')) return Token(Token::LBRACKET);
+ else if (ch==QChar(')')) return Token(Token::RBRACKET);
+ else if (ch==QChar('/')) return Token(Token::SLASH);
+ else if (ch==QChar('#')) {
+ QString hex;
+ while (peekCh(ch) && hexDigits.contains(ch)) {
+ getCh(ch);
+ hex+=ch;
+ }
+ if (hex.size()==0) throw std::runtime_error(QString("did not find HEX digits after a HASH '#' at pos.%1!").arg(pos).toStdString());
+ return Token(hex,Token::HEXSTRING);
+ } else if (ch.isDigit() || ch=='+' || ch=='-' || ch=='.') {
+ QString num=ch;
+ QString unit;
+ while (peekCh(ch) && (ch.isDigit() || ch=='.')) {
+ getCh(ch);
+ num+=ch;
+ }
+ if (ch.isLetter() || ch=='\xB0' || ch=='%') {
+ unit=ch;
+ getCh(ch);
+ while (peekCh(ch) && (ch.isLetter() || ch=='\xB0' || ch=='%')) {
+ getCh(ch);
+ unit+=ch;
+ }
+ }
+ return Token(num.toDouble(), unit);
+ } else if (ch.isLetter()) {
+ QString name=ch;
+ while (peekCh(ch) && (ch.isLetterOrNumber() ||ch=='-' || ch=='_'|| ch=='.')) {
+ getCh(ch);
+ name+=ch;
+ }
+ return Token(name);
+ } else {
+ throw std::runtime_error(QString("found unexpected character '%1' at pos.%2").arg(ch).arg(pos).toStdString());
+ }
+ return Token::END;
+}
+
+JKQTPCSSParser::Token JKQTPCSSParser::getToken()
+{
+ int newpos=pos;
+ const auto newT=peekNextToken(&newpos);
+ pos=newpos;
+ return CurrentToken=newT;
+}
+
+JKQTPExpected JKQTPCSSParser::parseNumberWithUnit(bool get)
+{
+ NumberWithUnit num = NumberWithUnit(0.0,"");
+
+ if (get) getToken();
+ if (CurrentToken.is(Token::TokenType::NUMBER)) {
+ num=NumberWithUnit(CurrentToken.NumberValue, CurrentToken.StringValue);
+ } else {
+ return {JKQTPUnexpected, UnexpectedTokenError(Token::NUMBER, CurrentToken, pos) };
+ }
+ return { num };
+}
+
+JKQTPExpected JKQTPCSSParser::parseColor(bool get)
+{
+ QColor col=Qt::black;
+
+ if (get) getToken();
+
+ if (CurrentToken.is(Token::TokenType::HEXSTRING)) {
+ const QString hex=CurrentToken.StringValue;
+ if (hex.size()==3) {
+ const int r=QString(hex[0]).toInt(nullptr, 16);
+ const int g=QString(hex[1]).toInt(nullptr, 16);
+ const int b=QString(hex[2]).toInt(nullptr, 16);
+ col=QColor::fromRgb(r*16+r,g*16+g,b*16+b);
+ } else if (hex.size()==4) {
+ const int r=QString(hex[0]).toInt(nullptr, 16);
+ const int g=QString(hex[1]).toInt(nullptr, 16);
+ const int b=QString(hex[2]).toInt(nullptr, 16);
+ const int a=QString(hex[3]).toInt(nullptr, 16);
+ col=QColor::fromRgb(r*16+r,g*16+g,b*16+b,a*16+a);
+ } else if (hex.size()==6) {
+ col=QColor::fromRgb(hex.mid(0,2).toInt(nullptr, 16),hex.mid(2,2).toInt(nullptr, 16),hex.mid(4,2).toInt(nullptr, 16));
+ } else if (hex.size()==8) {
+ col=QColor::fromRgb(hex.mid(0,2).toInt(nullptr, 16),hex.mid(2,2).toInt(nullptr, 16),hex.mid(4,2).toInt(nullptr, 16),hex.mid(6,2).toInt(nullptr, 16));
+ } else {
+ return {JKQTPUnexpected, UnconvertobleError("#"+hex, "HEX-RGB value", pos) };
+ }
+ } else if (CurrentToken.is(Token::TokenType::NAME)) {
+ static QMap> rgbFuncs={
+ {"rgb", QPair(3,4)},
+ {"rgba",QPair(4,4)},
+ {"hsl",QPair(3,4)},
+ {"hsv",QPair(3,4)},
+ {"gray",QPair(1,2)},
+ {"grey",QPair(1,2)},
+ {"red",QPair(1,2)},
+ {"green",QPair(1,2)},
+ {"blue",QPair(1,2)}
+ };
+ const QString func=CurrentToken.getNormString();
+ if (rgbFuncs.contains(func)) {
+ int nextPos=pos;
+ const auto nextT=peekNextToken(&nextPos);
+ if (nextT.is(Token::END)) {
+ getToken();
+ return jkqtp_lookupQColorName(func);
+ } else if (!nextT.is(Token::LBRACKET)) {
+ bool found=false;
+ col=jkqtp_lookupQColorName(func, false, &found);
+ if (!found) {
+ getToken();
+ return {JKQTPUnexpected, UnexpectedTokenError(func, Token::LBRACKET, CurrentToken, pos) };
+ } else return { col };
+ }
+ getToken();
+ const int minParams=rgbFuncs[func].first;
+ const int maxParams=rgbFuncs[func].second;
+ QList params;
+ getToken();
+ while (CurrentToken.is(Token::NUMBER)) {
+ const auto num=parseNumberWithUnit(false);
+ if (!num.has_value()) return {JKQTPUnexpected, UnexpectedTermError(func, "number with unit", CurrentToken, pos) };
+ if (params.size()==0 && (func=="hsl" || func=="hsv")) params.append(num.value().normHueNumber());
+ else {
+ if ((minParams!=maxParams && params.size()==maxParams-1) || minParams==4) params.append(jkqtp_bounded(0.0,num.value().normNumber(),1.0));
+ else params.append(num.value().normRGBNumber());
+ }
+ getToken();
+ if (CurrentToken.is(Token::COMMA) || CurrentToken.is(Token::SLASH)) getToken();
+ }
+ if (!CurrentToken.is(Token::RBRACKET) && !CurrentToken.is(Token::END)) return {JKQTPUnexpected, UnexpectedTokenError(func, Token::RBRACKET, CurrentToken, pos) };
+ if (params.size()>=minParams && params.size()<=maxParams) {
+ if (func=="rgb" || func=="rgba") {
+ col=QColor::fromRgbF(params[0],params[1],params[2],params.value(3,1));
+ } else if (func=="hsl") {
+ col=QColor::fromHslF(params[0],params[1],params[2],params.value(3,1));
+ } else if (func=="hsv") {
+ col=QColor::fromHsvF(params[0],params[1],params[2],params.value(3,1));
+ } else if (func=="gray" || func=="grey") {
+ col=QColor::fromRgbF(params[0],params[0],params[0],params.value(1,1));
+ } else if (func=="red") {
+ col=QColor::fromRgbF(params[0],0.0,0.0,params.value(1,1));
+ } else if (func=="green") {
+ col=QColor::fromRgbF(0.0,params[0],0.0,params.value(1,1));
+ } else if (func=="blue") {
+ col=QColor::fromRgbF(0.0,0.0,params[0],params.value(1,1));
+ }
+ } else return {JKQTPUnexpected, WrongNumberOfArgumentError(func, params.size(), minParams, maxParams, pos) };
+ } else {
+ col=jkqtp_lookupQColorName(CurrentToken.StringValue);
+ }
+ } else {
+ return {JKQTPUnexpected, UnexpectedTokenError(Token::NUMBER, CurrentToken, pos) };
+ }
+ return { col };
+}
+
+JKQTPExpected JKQTPCSSParser::parseGradient(bool get)
+{
+ QGradient grad=QGradient();
+
+ if (get) getToken();
+
+ static QMap s_GradientPresets = []() {
+ QMap m;
+ for (int i=1; i().valueToKey(static_cast(i))).toLower().trimmed().simplified();
+ if (id.size()>0) m[id]=static_cast(i);
+ }
+ return m;
+ }();
+
+ const QString func=CurrentToken.StringValue.trimmed().simplified().toLower();
+ if (CurrentToken.is(Token::TokenType::NAME) && func=="linear-gradient") {
+ QGradientStops colorStops;
+ double alphaDeg=180; // 0 = bottom->top angle like clock-pointer
+ // 90 = left->right
+ // 180 = top->bottom
+ // 270 = right->left
+
+ getToken();
+
+ if (!CurrentToken.is(Token::LBRACKET)) return {JKQTPUnexpected, UnexpectedTokenError(func, Token::LBRACKET, CurrentToken, pos) };
+ getToken();
+
+ if (CurrentToken.isNormString("to")) {
+ QStringList capDir;
+ static QSet allowedDirs={"left","right","top","bottom"};
+ while (getToken().is(Token::NAME)) {
+ const QString ns=CurrentToken.getNormString();
+ if (allowedDirs.contains(ns)) {
+ capDir.append(ns);
+ } else {
+ return {JKQTPUnexpected, UnexpectedTermError(func, "direction specifier [left|right|top|bottom] after 'to'", CurrentToken, pos) };
+ }
+ }
+ if (capDir.size()==1 || (capDir.size()==2 && capDir[1].isEmpty())) {
+ if (capDir[0]=="left") alphaDeg=270;
+ else if (capDir[0]=="right") alphaDeg=90;
+ else if (capDir[0]=="top") alphaDeg=0;
+ else if (capDir[0]=="bottom") alphaDeg=180;
+ } else if (capDir.size()==2) {
+ if (capDir[0]=="top"||capDir[0]=="bottom") qSwap(capDir[0],capDir[1]);
+ if (capDir[0]=="left" && capDir[1]=="top") alphaDeg=270+45;
+ else if (capDir[0]=="left" && capDir[1]=="bottom") alphaDeg=270-45;
+ else if (capDir[0]=="right" && capDir[1]=="top") alphaDeg=90-45;
+ else if (capDir[0]=="right" && capDir[1]=="bottom") alphaDeg=90+45;
+ } else {
+ return {JKQTPUnexpected, GeneralError(func, "at most two directions allowed after 'to', but found "+QString::number(capDir.size()), pos) };
+ }
+ if (!CurrentToken.is(Token::COMMA)) return {JKQTPUnexpected, UnexpectedTokenError(func, Token::COMMA, CurrentToken, pos) };
+ //getToken();
+ } if (CurrentToken.is(Token::NUMBER)) {
+ auto angle=parseNumberWithUnit(false);
+ alphaDeg=angle.value().normNumber();
+ getToken();
+ }
+
+ // alphaDeg 0 90 180 270
+ // sin 0 1 0 -1
+ // cos 1 0 -1 0
+ // up right down left
+ const double dx=jkqtp_roundToDigits(0.5*sin(alphaDeg/180.0*M_PI),10);
+ const double dy=jkqtp_roundToDigits(-0.5*cos(alphaDeg/180.0*M_PI),10);
+
+ QLinearGradient lgrad(0.5-dx,0.5-dy,0.5+dx,0.5+dy);
+ lgrad.setCoordinateMode(QGradient::ObjectBoundingMode);
+
+ bool done=false;
+ while (!done) {
+ QGradientStop stop;
+ const auto col=parseColor(true);
+ if (!col.has_value()) return {JKQTPUnexpected, UnexpectedTermError(func, "color specifier", CurrentToken, pos) };
+ stop.second=col.value();
+ stop.first=-1;
+ getToken();
+
+ if (CurrentToken.is(Token::NUMBER)) {
+ if (CurrentToken.StringValue!="%") return {JKQTPUnexpected, UnexpectedTermError(func, "percentage value", CurrentToken, pos) };
+ stop.first=CurrentToken.NumberValue/100.0;
+ getToken();
+ }
+ colorStops<0) {
+ // normalize colorStop positions
+ if (colorStops.first().first<0) colorStops.first().first=0;
+ if (colorStops.size()>1) {
+ if (colorStops.last().first<0) colorStops.last().first=1.0;
+ }
+ int i=1;
+ while (i(cnt+1);
+ for (int j=lastI+1; j(j-lastI)*delta;
+ }
+ }
+ i++;
+ }
+
+ lgrad.setStops(colorStops);
+ grad=lgrad;
+ }
+ } else if (CurrentToken.isNormStringAnyOf(s_GradientPresets.keys())) {
+ grad=QGradient(s_GradientPresets[CurrentToken.getNormString()]);
+ grad.setCoordinateMode(QGradient::ObjectBoundingMode);
+
+ } else {
+ return {JKQTPUnexpected, UnexpectedTermError("supported gradient-function [linear-gradient|] or predefined gradient name", CurrentToken, pos) };
+ }
+ return { grad };
+}
+
+
+
+JKQTPCSSParser::JKQTPCSSParser(const QString &text_):
+ CurrentToken(Token::TokenType::END), text(text_), pos(0)
+{
+
+}
+
+
+QString JKQTPCSSParser::Token::toString(TokenType type)
+{
+ switch(type) {
+ case END: return "END";
+ case NAME: return "NAME";
+ case NUMBER: return "NUMBER";
+ case HEXSTRING: return "HEXSTRING";
+ case LBRACKET: return "LBRACKET";
+ case RBRACKET: return "RBRACKET";
+ case COMMA: return "COMMA";
+ case SLASH: return "SLASH";
+ }
+ return "???";
+}
+
+JKQTPCSSParser::Token::Token():
+ type(TokenType::END), StringValue(""), NumberValue(0.0)
+{
+
+}
+
+JKQTPCSSParser::Token::Token(TokenType type):
+ type(type), StringValue(""), NumberValue(0.0)
+{
+ switch(type) {
+ case LBRACKET: StringValue="("; break;
+ case RBRACKET: StringValue=")"; break;
+ case COMMA: StringValue=","; break;
+ default: break;
+ }
+
+}
+
+JKQTPCSSParser::Token::Token(double num, const QString &unit_):
+ NumberValue(num), StringValue(unit_), type(NUMBER)
+{
+
+}
+
+JKQTPCSSParser::Token::Token(const QString &str, TokenType type_):
+ StringValue(str), type(type_)
+{
+ if (type_==NUMBER) NumberValue=str.toDouble();
+ if (type_==HEXSTRING) NumberValue=str.toInt(nullptr,16);
+}
+
+QString JKQTPCSSParser::Token::toString() const
+{
+ QString s=toString(type);
+ if (type==NUMBER) s+= " ["+QString::number(NumberValue, 'f')+"]";
+ else if (StringValue.size()>0) s+= " ["+StringValue+"]";
+ return s;
+}
+
+
+JKQTPCSSParser::NumberWithUnit JKQTPCSSParser::readNumberWithUnit(const QString &prog)
+{
+ JKQTPCSSParser parser(prog);
+ const auto res= parser.parseNumberWithUnit(true);
+ if (res.has_value()) return res.value();
+ else throw std::runtime_error(res.error().error.toStdString());
+}
+
+QColor JKQTPCSSParser::readColor(const QString &prog)
+{
+ JKQTPCSSParser parser(prog);
+ const auto res= parser.parseColor(true);
+ if (res.has_value()) return res.value();
+ else throw std::runtime_error(res.error().error.toStdString());
+}
+
+QGradient JKQTPCSSParser::readGradient(const QString &prog)
+{
+ JKQTPCSSParser parser(prog);
+ const auto res= parser.parseGradient(true);
+ if (res.has_value()) return res.value();
+ else throw std::runtime_error(res.error().error.toStdString());
+}
diff --git a/lib/jkqtcommon/jkqtpcsstools.h b/lib/jkqtcommon/jkqtpcsstools.h
new file mode 100644
index 0000000000..574ec0f308
--- /dev/null
+++ b/lib/jkqtcommon/jkqtpcsstools.h
@@ -0,0 +1,299 @@
+
+/*
+ Copyright (c) 2008-2024 Jan W. Krieger ()
+
+
+
+ This software 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 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 this program. If not, see .
+*/
+
+#ifndef JKQTPCSSTOOLS_H_INCLUDED
+#define JKQTPCSSTOOLS_H_INCLUDED
+#include "jkqtcommon/jkqtcommon_imexport.h"
+#include
+#include
+#include
+#include
+#include "jkqtcommon/jkqtpmathtools.h"
+#include "jkqtcommon/jkqtpexpected.h"
+
+
+/*! \brief A simple parser for certain CSS subsets
+ \ingroup jkqtptools_math_parser
+
+ \section jkqtpcss_ebnf EBNF definition of the parsed expressions
+
+
+ -> linear-gradient( ? , )
+ ->
+ | to (left|right|bottom|top) (left|right|bottom|top)?
+ -> [A-Za-z%]*
+ -> | ,
+ -> (%)?
+ -> NAME
+ | #
+ | (rgb | rgba | hsl | hsv | gray | grey | red | green | blue) ( ( [,/]? ){1-4} )
+
+ -> floating-point-number, i.e. "[+-]?\d+\.?\d*"
+ -> RGB | RRGGBB | RGBA | RRGGBBAA
+
+
+
+ */
+class JKQTCOMMON_LIB_EXPORT JKQTPCSSParser
+{
+public:
+
+ /** \brief encodes a number with its unit, e.g. \c 100% or \c 45deg or ... */
+ struct NumberWithUnit {
+ inline NumberWithUnit(double val=0.0, const QString& unit_=QString()): unit(unit_), number(val) {};
+ /** \bref unit as string */
+ QString unit;
+ /** \brief the number value ittself */
+ double number;
+ /** \brief partly evaluates the unit and transforms the number accordingly.
+ *
+ * This will do these transformations:
+ * - unit == \c % --> returns number/100.0;
+ * - unit == angle unit : returns number in degrees:
+ * - unit == \c rad --> returns number/M_PI*180.0
+ * - unit == \c turn --> returns number*360.0
+ * - unit == \c grad --> returns number/400.0*360.0
+ * .
+ * .
+ */
+ inline double normNumber() const {
+ const QString nu=normUnit();
+ if (nu=="%") return number/100.0;
+ if (nu=="rad") return number/JKQTPSTATISTICS_PI*180.0;
+ if (nu=="turn") return number*360.0;
+ if (nu=="grad") return number/400.0*360.0;
+ return number;
+ }
+ /** \brief returns number as a normalized number for RGB, output is in the range 0..1
+ *
+ * This interprets %-values and all other values as from the range 0..255 and noralizes both to 0..1
+ */
+ inline double normRGBNumber() const {
+ const QString nu=normUnit();
+ if (nu=="%") return jkqtp_bounded(0.0,number/100.0, 1.0);
+ return jkqtp_bounded(0.0,number/255.0, 1.0);
+ }
+ /** \brief returns number as a normalized number for Hue, output is in the range 0..1
+ *
+ * This interprets %-values and all other values as from the range 0..360 and noralizes both to 0..1
+ */
+ inline double normHueNumber() const {
+ const QString nu=normUnit();
+ if (nu=="%") return jkqtp_bounded(0.0,number/100.0, 1.0);
+ if (nu=="rad") return fmod(number/(JKQTPSTATISTICS_PI*2.0),1.0);
+ if (nu=="turn") return fmod(number,1.0);
+ if (nu=="grad") return fmod(number/400.0,1.0);
+ return fmod(number/360.0, 1.0);
+ }
+ /** \brief returns a normalized version of the unit, i.e. all lower-case, timmed and simplified */
+ inline QString normUnit() const { return unit.simplified().trimmed().toLower(); }
+
+ inline bool operator==(const NumberWithUnit& other) const {
+ return number==other.number && unit==other.unit;
+ }
+ };
+
+
+
+protected:
+ /** @name Tokenizer */
+ /**@{*/
+
+ struct Token {
+ /** \brief the possible Token that can be recognized by the tokenizer in JKQTPCSSParser::getToken() */
+ enum TokenType {
+ END, /*!< \brief end token */
+ NAME, /*!< \brief a name (consisting of characters) of a variable or function */
+ NUMBER, /*!< \brief a number, possibly with a unit(string) */
+ HEXSTRING, /*!< \brief a string in HEX notation (i.e. 0-9a-fA-F) */
+ LBRACKET, /*!< \brief left brackets '(' */
+ RBRACKET, /*!< \brief right brackets ')' */
+ COMMA, /*!< \brief a comma ',' */
+ SLASH, /*!< \brief a slash '/' */
+ };
+
+ static QString toString(TokenType type);
+
+ Token();
+ Token(TokenType type);
+ Token(double num, const QString& unit_=QString());
+ Token(const QString& str, TokenType type=NAME);
+ TokenType type;
+ /** \brief the string value of the current token (when applicable) during the parsing step */
+ QString StringValue;
+
+ /** \brief the string value of the current token (when applicable) during the parsing step */
+ double NumberValue;
+
+ /** \brief checks whether the current token is of the given type (does not compare any other field) */
+ inline bool is(TokenType othertype) const {
+ return (type==othertype);
+ }
+ /** \brief checks whether the current token is of type TokenType::NAME and the String value equals \a name (case-insensitive commparison, trimmer, simplified) */
+ inline bool isNormString(const QString& name) const {
+ return (type==TokenType::NAME) && (getNormString()==name.toLower().simplified().trimmed());
+ }
+ /** \brief checks whether the current token is of type TokenType::NAME and the String value equals any of the entries in \a names (case-insensitive commparison, trimmer, simplified) */
+ inline bool isNormStringAnyOf(const QStringList& names) const {
+ if (type==TokenType::NAME) {
+ const QString ns=getNormString();
+ for (const auto&name: names) {
+ if (ns==name.toLower().simplified().trimmed()) return true;
+ }
+ }
+ return false;
+ }
+ /** \brief normlizes the StringValue (i.e. lower-case, trimmer, simmplified) */
+ inline QString getNormString() const {
+ return StringValue.toLower().simplified().trimmed();
+ }
+ /** \brief converts the TOken information to a string */
+ QString toString() const;
+ };
+
+
+
+ /** \brief Tokenizer: look at the next token from the input, but don't set it as CurrentToken and don't move the read pointer pos */
+ Token peekNextToken(int* endPos=nullptr);
+ /** \brief Tokenizer: extract the next token from the input */
+ Token getToken();
+
+ /** \brief the current token while parsing a string */
+ Token CurrentToken;
+
+
+ /** \brief this stream is used to read in the program. An object is created and assigned
+ * (and destroyed) by the parse()-function */
+ QString text;
+ /** \brief current reading position in text */
+ int pos;
+
+ /** \brief indicates whether pos points to the end of text */
+ inline bool textAtEnd() const {
+ return pos<0 || pos>=text.size()-1;
+ }
+
+ inline bool getCh(QChar& ch) {
+ if (pos<0 || pos>=text.size()) return false;
+ ch=text[pos];
+ pos++;
+ return true;
+ }
+
+ inline bool peekCh(QChar& ch) {
+ if (pos<0 || pos>=text.size()) return false;
+ ch=text[pos];
+ return true;
+ }
+
+ inline void putBackCh() {
+ pos--;
+ }
+
+
+ /**@}*/
+
+ /** @name Error Handling */
+ /**@{*/
+
+ struct RawErrorTag_t {};
+ static RawErrorTag_t RawErrorTag;
+ struct GeneralError {
+ inline GeneralError(const QString& err=QString("unspecified error"), int p=-1): error(QString("%1 at pos.%2").arg(err).arg(p)), pos(p) {}
+ inline GeneralError(RawErrorTag_t, const QString& err, int p=-1): error(err), pos(p) { }
+ inline GeneralError(const QString& context, const QString& err, int p=-1): error(QString("%1 in context %3 at pos.%2").arg(err).arg(p).arg(context)), pos(p) {}
+ QString error;
+ int pos;
+ };
+ /** \brief Exception for unexpected Token */
+ struct UnexpectedTokenError: public GeneralError {
+ inline UnexpectedTokenError(Token::TokenType expectedToken, Token::TokenType foundToken, int pos):
+ GeneralError(RawErrorTag, QString("unexpected token at pos.%1 (found: %2, expected: %3)").arg(pos).arg(Token::toString(foundToken)).arg(Token::toString(expectedToken)), pos)
+ {}
+ inline UnexpectedTokenError(Token::TokenType expectedToken, const Token& foundToken, int pos):
+ GeneralError(RawErrorTag, QString("unexpected token at pos.%1 (found: %2, expected: %3)").arg(pos).arg(foundToken.toString()).arg(Token::toString(expectedToken)), pos)
+ {}
+ inline UnexpectedTokenError(const QString& context, Token::TokenType expectedToken, const Token& foundToken, int pos):
+ GeneralError(RawErrorTag, QString("unexpected token at pos.%1 in context %4 (found: %2, expected: %3)").arg(pos).arg(foundToken.toString()).arg(Token::toString(expectedToken)).arg(context), pos)
+ {}
+ };
+ /** \brief Exception for unexpected Term */
+ struct UnexpectedTermError: public GeneralError {
+ inline UnexpectedTermError(const QString& expectedToken, const Token& foundToken, int pos):
+ GeneralError(RawErrorTag, QString("unexpected term at pos.%1 (found: %2, expected: %3)").arg(pos).arg(foundToken.toString()).arg(expectedToken), pos)
+ {}
+ inline UnexpectedTermError(const QString& context, const QString& expectedToken, const Token& foundToken, int pos):
+ GeneralError(RawErrorTag, QString("unexpected term at pos.%1 in context %4 (found: %2, expected: %3)").arg(pos).arg(foundToken.toString()).arg(expectedToken).arg(context), pos)
+ {}
+ };
+ /** \brief Exception for wrong number of function arguments */
+ struct WrongNumberOfArgumentError: public GeneralError {
+ inline WrongNumberOfArgumentError(const QString& func, int numArgs, int minArgs, int maxArgs, int pos):
+ GeneralError(RawErrorTag, QString("wrong number of function arguments for %2() found at pos.%1 (found: %3, expected: %4...%5)").arg(pos).arg(func).arg(numArgs).arg(minArgs).arg(maxArgs), pos)
+ {}
+ };
+ /** \brief Exception when a string cannot be converted properly */
+ struct UnconvertobleError: public GeneralError {
+ inline UnconvertobleError(const QString& str, const QString& target, int pos):
+ GeneralError(RawErrorTag, QString("Could not convert '%2' to %3 at pos.%1").arg(pos).arg(str).arg(target), pos)
+ {}
+ };
+ /**@}*/
+
+
+ /** @name Parser */
+ /**@{*/
+
+ /** \brief parses a number with unit */
+ JKQTPExpected parseNumberWithUnit(bool get);
+ /** \brief parses a color definition */
+ JKQTPExpected parseColor(bool get);
+ /** \brief parses a color definition */
+ JKQTPExpected parseGradient(bool get);
+
+ /**@}*/
+
+
+
+
+ /** \brief class constructor
+ *
+ * \note This also registers all standatd functions and constants by calling addStandardFunctions() and addStandardVariables()
+ */
+ JKQTPCSSParser(const QString& text);
+
+public:
+
+
+
+
+
+
+ /** \brief parses the given expression*/
+ static NumberWithUnit readNumberWithUnit(const QString& prog);
+ /** \brief parses the given expression, which should represent a color (incl. color-functions like \c rgb() ) */
+ static QColor readColor(const QString& prog);
+ /** \brief parses the given expression, which should represent a QGradient */
+ static QGradient readGradient(const QString& prog);
+
+};
+
+
+#endif // JKQTPCSSTOOLS_H_INCLUDED
diff --git a/lib/jkqtcommon/jkqtpstringtools.cpp b/lib/jkqtcommon/jkqtpstringtools.cpp
index e4ed74735c..29355c389f 100644
--- a/lib/jkqtcommon/jkqtpstringtools.cpp
+++ b/lib/jkqtcommon/jkqtpstringtools.cpp
@@ -20,6 +20,7 @@
#include "jkqtcommon/jkqtpstringtools.h"
#include "jkqtcommon/jkqtpmathtools.h"
+#include "jkqtcommon/jkqtpcsstools.h"
#include
#include
#include
@@ -34,6 +35,11 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
#include
#include
@@ -330,26 +336,73 @@ QString jkqtp_QBrushStyle2String(Qt::BrushStyle style) {
}
}
+namespace {
+ static QMap s_String2QBrushStyleMap = []() {
+ QMap m;
+ m["none"]=Qt::NoBrush;
+ m["d1"]=Qt::Dense1Pattern;
+ m["d2"]=Qt::Dense2Pattern;
+ m["d3"]=Qt::Dense3Pattern;
+ m["d4"]=Qt::Dense4Pattern;
+ m["d5"]=Qt::Dense5Pattern;
+ m["d6"]=Qt::Dense6Pattern;
+ m["d7"]=Qt::Dense7Pattern;
+ m["hor"]=Qt::HorPattern;
+ m["ver"]=Qt::VerPattern;
+ m["cross"]=Qt::CrossPattern;
+ m["bdiag"]=Qt::BDiagPattern;
+ m["vdiag"]=Qt::FDiagPattern;
+ m["diagcross"]=Qt::DiagCrossPattern;
+ return m;
+ }();
+
+ static QMap s_GradientPresets = []() {
+ QMap m;
+ for (int i=1; i().valueToKey(static_cast(i))).toLower().trimmed().simplified();
+ if (id.size()>0) m[id]=static_cast(i);
+ }
+ return m;
+ }();
+
+}
+
Qt::BrushStyle jkqtp_String2QBrushStyle(const QString& style) {
QString s=style.toLower().trimmed();
- if (s=="none") return Qt::NoBrush;
- if (s=="d1") return Qt::Dense1Pattern;
- if (s=="d2") return Qt::Dense2Pattern;
- if (s=="d3") return Qt::Dense3Pattern;
- if (s=="d4") return Qt::Dense4Pattern;
- if (s=="d5") return Qt::Dense5Pattern;
- if (s=="d6") return Qt::Dense6Pattern;
- if (s=="d7") return Qt::Dense7Pattern;
- if (s=="hor") return Qt::HorPattern;
- if (s=="ver") return Qt::VerPattern;
- if (s=="cross") return Qt::CrossPattern;
- if (s=="bdiag") return Qt::BDiagPattern;
- if (s=="vdiag") return Qt::FDiagPattern;
- if (s=="diagcross") return Qt::DiagCrossPattern;
- /*if (s=="lingrad") return Qt::LinearGradientPattern;
- if (s=="radgrad") return Qt::RadialGradientPattern;
- if (s=="congrad") return Qt::ConicalGradientPattern;*/
- return Qt::SolidPattern;
+ return s_String2QBrushStyleMap.value(s, Qt::SolidPattern);
+}
+
+
+Qt::BrushStyle jkqtp_String2QBrushStyleExt(const QString &style, QColor *color, QGradient *gradient, QPixmap *image, double* rotationAngleDeg)
+{
+ const QString s=style.toLower().trimmed().simplified();
+ QStringList caps;
+
+ if (s.startsWith("linear-gradient(")) {
+ try {
+ const auto grad=JKQTPCSSParser::readGradient(s);
+ if (gradient) *gradient=grad;
+ return Qt::LinearGradientPattern;
+ } catch(std::exception& E) {
+ qWarning()<<"error converting '"<