mirror of
https://github.com/jkriege2/JKQtPlotter.git
synced 2025-01-24 06:32:12 +08:00
NEW CSS-parser JKQTPCSSParser in order to parse e.g. \c linear-gradient() specifications in jkqtp_String2QBrushStyleExt()
NEW generic RegularExpression functions (jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt() )
This commit is contained in:
parent
11bafa52e4
commit
eb7a068fb7
@ -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
|
||||
|
||||
|
@ -30,7 +30,6 @@ This page lists several todos and wishes for future version of JKQTPlotter
|
||||
<li>plot: elongated grid to left of tick labels</li>
|
||||
<li>plot: refactor print preview/export preview code </li>
|
||||
<li>sryling: better styling/more styling options for data-tooltips</li>
|
||||
<li>styling: color gradients as fill-styles in style-INIs</li>
|
||||
<li>user interactions: dialog(s) to edit x/y-range</li>
|
||||
<li></li>
|
||||
</ul></li>
|
||||
|
@ -28,8 +28,8 @@ Changes, compared to \ref page_whatsnew_V4_0_0 "v4.0.0" include:
|
||||
<li>NEW/BREAKING: refactor CMake-Code, so static/dynamic switch is done via <code>BUILD_SHARED_LIBS</code>, which retires <code>JKQtPlotter_BUILD_STATIC_LIBS</code>, <code>JKQtPlotter_BUILD_SHARED_LIBS</code> and removes the capability to build static and shared libraries in one location (fixes issue #104)</li>
|
||||
<li>NEW: prepareed library for CMake's <a href="https://cmake.org/cmake/help/latest/module/FetchContent.html">FetchContent</a>-API</li>
|
||||
<li>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</li>
|
||||
<li>NEW add JKQTPExpected datatype</li>
|
||||
<li>NEW add jkqtp_roundToDigits()</li>
|
||||
<li>NEW add JKQTPExpected datatype, jkqtp_roundToDigits(), generic RegularExpression functions (jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt() )</li>
|
||||
<li>NEW CSS-parser JKQTPCSSParser in order to parse e.g. \c linear-gradient() specifications in jkqtp_String2QBrushStyleExt()</li>
|
||||
</ul></li>
|
||||
|
||||
<li>JKQTPlotter:<ul>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
417
lib/jkqtcommon/jkqtpcsstools.cpp
Normal file
417
lib/jkqtcommon/jkqtpcsstools.cpp
Normal file
@ -0,0 +1,417 @@
|
||||
/*
|
||||
Copyright (c) 2008-2024 Jan W. Krieger (<jan@jkrieger.de>)
|
||||
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
#include "jkqtcommon/jkqtpcsstools.h"
|
||||
#include "jkqtcommon/jkqtpstringtools.h"
|
||||
#include <exception>
|
||||
#include <QMetaEnum>
|
||||
|
||||
|
||||
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::NumberWithUnit, JKQTPCSSParser::GeneralError> 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<QColor, JKQTPCSSParser::GeneralError> 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<QString,QPair<int,int>> rgbFuncs={
|
||||
{"rgb", QPair<int,int>(3,4)},
|
||||
{"rgba",QPair<int,int>(4,4)},
|
||||
{"hsl",QPair<int,int>(3,4)},
|
||||
{"hsv",QPair<int,int>(3,4)},
|
||||
{"gray",QPair<int,int>(1,2)},
|
||||
{"grey",QPair<int,int>(1,2)},
|
||||
{"red",QPair<int,int>(1,2)},
|
||||
{"green",QPair<int,int>(1,2)},
|
||||
{"blue",QPair<int,int>(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<double> 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<QGradient, JKQTPCSSParser::GeneralError> JKQTPCSSParser::parseGradient(bool get)
|
||||
{
|
||||
QGradient grad=QGradient();
|
||||
|
||||
if (get) getToken();
|
||||
|
||||
static QMap<QString,QGradient::Preset> s_GradientPresets = []() {
|
||||
QMap<QString,QGradient::Preset> m;
|
||||
for (int i=1; i<QGradient::Preset::NumPresets; i++) {
|
||||
const QString id=QString(QMetaEnum::fromType<QGradient::Preset>().valueToKey(static_cast<QGradient::Preset>(i))).toLower().trimmed().simplified();
|
||||
if (id.size()>0) m[id]=static_cast<QGradient::Preset>(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<QString> 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<<stop;
|
||||
if (CurrentToken.is(Token::RBRACKET) || CurrentToken.is(Token::END)) {
|
||||
done=true;
|
||||
}
|
||||
}
|
||||
if (colorStops.size()>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<colorStops.size()) {
|
||||
if(colorStops[i].first<0) {
|
||||
const double lastStop=colorStops[i-1].first;
|
||||
const int lastI=i-1;
|
||||
while (colorStops[i].first<0) i++;
|
||||
const double endStop=colorStops[i].first;
|
||||
const int cnt=i-lastI-1;
|
||||
const double delta=(endStop-lastStop)/static_cast<double>(cnt+1);
|
||||
for (int j=lastI+1; j<i; j++) {
|
||||
colorStops[j].first=lastStop+static_cast<double>(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());
|
||||
}
|
299
lib/jkqtcommon/jkqtpcsstools.h
Normal file
299
lib/jkqtcommon/jkqtpcsstools.h
Normal file
@ -0,0 +1,299 @@
|
||||
|
||||
/*
|
||||
Copyright (c) 2008-2024 Jan W. Krieger (<jan@jkrieger.de>)
|
||||
|
||||
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef JKQTPCSSTOOLS_H_INCLUDED
|
||||
#define JKQTPCSSTOOLS_H_INCLUDED
|
||||
#include "jkqtcommon/jkqtcommon_imexport.h"
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
#include <QGradient>
|
||||
#include <QStack>
|
||||
#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
|
||||
|
||||
<pre>
|
||||
<lingradient> -> <b>linear-gradient(</b> <angle>? <b>,</b> <gradientstops> <b>)</b>
|
||||
<angle> -> <numberwithunit>
|
||||
| <b>to</b> (<b>left</b>|<b>right</b>|<b>bottom</b>|<b>top</b>) (<b>left</b>|<b>right</b>|<b>bottom</b>|<b>top</b>)?
|
||||
<numberwithunit> -> <number> [<b>A</b>-<b>Z</b><b>a</b>-<b>z</b><b>%</b>]*
|
||||
<gradientstops> -> <gradientstop> | <gradientstop> <b>,</b> <gradientstops>
|
||||
<gradientstop> -> <color> (<number><b>%</b>)?
|
||||
<color> -> NAME
|
||||
| <b>#</b><hexnumber>
|
||||
| (<b>rgb</b> | <b>rgba</a> | <b>hsl</a> | <b>hsv</a> | <b>gray</a> | <b>grey</a> | <b>red</a> | <b>green</a> | <b>blue</a>) <b>(</b> (<numberwithunit> [<b>,/</b>]? ){1-4} <b>)</b>
|
||||
|
||||
<number> -> floating-point-number, i.e. "[+-]?\d+\.?\d*"
|
||||
<hexnumber> -> RGB | RRGGBB | RGBA | RRGGBBAA
|
||||
</pre>
|
||||
|
||||
|
||||
*/
|
||||
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<JKQTPCSSParser::NumberWithUnit, JKQTPCSSParser::GeneralError> parseNumberWithUnit(bool get);
|
||||
/** \brief parses a color definition <color> */
|
||||
JKQTPExpected<QColor, JKQTPCSSParser::GeneralError> parseColor(bool get);
|
||||
/** \brief parses a color definition <gradient> */
|
||||
JKQTPExpected<QGradient, JKQTPCSSParser::GeneralError> 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
|
@ -20,6 +20,7 @@
|
||||
|
||||
#include "jkqtcommon/jkqtpstringtools.h"
|
||||
#include "jkqtcommon/jkqtpmathtools.h"
|
||||
#include "jkqtcommon/jkqtpcsstools.h"
|
||||
#include <QDateTime>
|
||||
#include <cmath>
|
||||
#include <QDebug>
|
||||
@ -34,6 +35,11 @@
|
||||
#include <ctype.h>
|
||||
#include <sstream>
|
||||
#include <locale>
|
||||
#include <QCache>
|
||||
#include <QSharedPointer>
|
||||
#include <QMetaEnum>
|
||||
#include <QGradient>
|
||||
#include <QGradientStops>
|
||||
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
|
||||
#include<QRegularExpression>
|
||||
#include<QRegularExpressionMatch>
|
||||
@ -330,27 +336,74 @@ QString jkqtp_QBrushStyle2String(Qt::BrushStyle style) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
static QMap<QString,Qt::BrushStyle> s_String2QBrushStyleMap = []() {
|
||||
QMap<QString,Qt::BrushStyle> 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<QString,QGradient::Preset> s_GradientPresets = []() {
|
||||
QMap<QString,QGradient::Preset> m;
|
||||
for (int i=1; i<QGradient::Preset::NumPresets; i++) {
|
||||
const QString id=QString(QMetaEnum::fromType<QGradient::Preset>().valueToKey(static_cast<QGradient::Preset>(i))).toLower().trimmed().simplified();
|
||||
if (id.size()>0) m[id]=static_cast<QGradient::Preset>(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 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 '"<<style<<"' into a QGradient: "<<E.what();
|
||||
return Qt::SolidPattern;
|
||||
}
|
||||
} else if (s_GradientPresets.contains(s)) {
|
||||
QGradient g(s_GradientPresets[s]);
|
||||
g.setCoordinateMode(QGradient::ObjectBoundingMode);
|
||||
if (gradient) *gradient=g;
|
||||
if (g.type()==QGradient::Type::RadialGradient) return Qt::RadialGradientPattern;
|
||||
else if (g.type()==QGradient::Type::ConicalGradient) return Qt::ConicalGradientPattern;
|
||||
else return Qt::LinearGradientPattern;
|
||||
} else if (jkqtp_rxExactlyMatches(s, "\\s*image\\s*\\(\\s*[\\\"\\\']?(.*)[\\\"\\\']?\\s*\\)\\s*", &caps)) {
|
||||
if (image) *image=QPixmap(caps[1]);
|
||||
return Qt::TexturePattern;
|
||||
} else {
|
||||
// now we parse:
|
||||
// pattern ::= none | d1 | d2 | ... | cross | bdiag | ...
|
||||
return jkqtp_String2QBrushStyle(s);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
std::string jkqtp_booltostr(bool data){
|
||||
@ -577,6 +630,7 @@ QString jkqtp_QColor2String(QColor color, bool useSpecialTransparencySyntax) {
|
||||
QColor jkqtp_lookupQColorName(const QString &color, bool namesOnly, bool *nameFound) {
|
||||
if (nameFound) *nameFound=true;
|
||||
const QString col=color.toLower().trimmed();
|
||||
if (col=="transparent") return QColor::fromRgb(0,0,0,0);
|
||||
if (col=="window") return QGuiApplication::palette().color(QPalette::Window);
|
||||
if (col=="windowtext") return QGuiApplication::palette().color(QPalette::WindowText);
|
||||
if (col=="button") return QGuiApplication::palette().color(QPalette::Button);
|
||||
@ -616,8 +670,7 @@ QColor jkqtp_String2QColor(QString color)
|
||||
if (color.startsWith('"')||color.startsWith('\'')) color=color.mid(1);
|
||||
if (color.endsWith('"')||color.endsWith('\'')) color=color.left(color.size()-1);
|
||||
if (color.isEmpty()) return Qt::black;
|
||||
// fish out all #RGB #RRGGBB, ... sequences, withoud added alpha/transparency-value after comma
|
||||
if (color.startsWith("#") && !color.contains(",")) return QColor(color);
|
||||
|
||||
// check whether we have a names color
|
||||
{
|
||||
bool nameFound=false;
|
||||
@ -652,91 +705,37 @@ QColor jkqtp_String2QColor(QString color)
|
||||
// now we check for diverse special syntaxes
|
||||
// P: "color,NN%" NN=TRANSPARENCY in percent
|
||||
// AP: "color,aNN\%" NN=ALPHA in percent
|
||||
// NP: "color,[a]NN\%" NN=ALPHA 0..255
|
||||
// NP: "color,[a]NN" NN=ALPHA 0..255
|
||||
// Frac: "grey25"
|
||||
// ColMod: "rgb(R,G,B)", "hsl(H,S,V)" ... (CSS-Syntax)
|
||||
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
|
||||
static QRegularExpression rxP("(.+)\\s*,\\s*t?\\s*(\\d+\\.?\\d+)\\%");
|
||||
static QRegularExpression rxP("([^,\\(\\)]+)\\s*,\\s*t?\\s*(\\d+\\.?\\d+)\\%");
|
||||
static QRegularExpression rxFrac("([a-zA-Z]{3,})(\\d{1,3})\\%?");
|
||||
static QRegularExpression rxColMod("\\s*(rgb|hsl|hsv|rgba|gray|grey|red|green|blue)\\(\\s*(\\d+\\.?\\d*)(%|deg)?(?:\\s+|\\s*,\\s*|\\s\\/\\s)?(\\d+\\.?\\d*)?(%)?(?:\\s+|\\s*,\\s*|\\s\\/\\s)?(\\d+\\.?\\d*)?(%)?(?:\\s+|\\s*,\\s*|\\s*\\/\\s*)?(\\d+\\.?\\d*)?(%)?\\s*\\)\\s*");
|
||||
static QRegularExpression rxAP("(.+)\\s*,\\s*a\\s*(\\d+\\.?\\d+)\\%");
|
||||
static QRegularExpression rxNP("(.+)\\s*,\\s*a?\\s*([\\d]+)");
|
||||
static QRegularExpression rxAP("([^,\\(\\)]+)\\s*,\\s*a\\s*(\\d+\\.?\\d+)\\%");
|
||||
static QRegularExpression rxNP("([^,\\(\\)]+)\\s*,\\s*a?\\s*([\\d]+)");
|
||||
const auto mP=rxP.match(color);
|
||||
|
||||
const auto mColMod=rxColMod.match(color);
|
||||
if (mColMod.hasMatch()) {
|
||||
QColor col(Qt::black);
|
||||
const QString name=mColMod.captured(1);
|
||||
const int v1=valUnitToInt(mColMod.captured(2), mColMod.captured(3));
|
||||
const int h1=valUnitToInt(mColMod.captured(2), mColMod.captured(3), INT_MAX);
|
||||
const int v2=valUnitToInt(mColMod.captured(4), mColMod.captured(5));
|
||||
const int a2=valUnitToAlphaInt(mColMod.captured(4), mColMod.captured(5));
|
||||
const int v3=valUnitToInt(mColMod.captured(6), mColMod.captured(7));
|
||||
const int a4=valUnitToAlphaInt(mColMod.captured(8), mColMod.captured(9));
|
||||
if (name=="gray"||name=="grey") {
|
||||
if (v2<0) col.setRgb(v1,v1,v1);
|
||||
else col.setRgb(v1,v1,v1,a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="red") {
|
||||
col.setRed(v1);
|
||||
if (v2>=0) col.setAlpha(a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="green") {
|
||||
col.setGreen(v1);
|
||||
if (v2>=0) col.setAlpha(a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="blue") {
|
||||
col.setBlue(v1);
|
||||
if (v2>=0) col.setAlpha(a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="rgb") {
|
||||
if (a4<0) col.setRgb(v1,v2,v3);
|
||||
else col.setRgb(v1,v2,v3,a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="rgba") {
|
||||
col.setRgb(v1,v2,v3,a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="hsl") {
|
||||
if (a4<0) col.setHsl(h1,v2,v3);
|
||||
else col.setHsl(h1,v2,v3,a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="hsv") {
|
||||
if (a4<0) col.setHsv(h1,v2,v3);
|
||||
else col.setHsv(h1,v2,v3,a2);
|
||||
return col;
|
||||
} else {
|
||||
qDebug()<<"unrecognized CSS-color:'"<<color<<"'";
|
||||
}
|
||||
}
|
||||
if (mP.hasMatch()) {
|
||||
QColor col=jkqtp_lookupQColorName(mP.captured(1));
|
||||
if (mP.hasMatch() && mP.captured(0).size()==color.size()) {
|
||||
QColor col=jkqtp_String2QColor(mP.captured(1));
|
||||
const double a=QLocale::c().toDouble(mP.captured(2));
|
||||
col.setAlphaF(1.0-a/100.0);
|
||||
return col;
|
||||
}
|
||||
const auto mAP=rxAP.match(color);
|
||||
if (mAP.hasMatch()) {
|
||||
QColor col=jkqtp_lookupQColorName(mAP.captured(1));
|
||||
if (mAP.hasMatch() && mAP.captured(0).size()==color.size()) {
|
||||
QColor col=jkqtp_String2QColor(mAP.captured(1));
|
||||
const double a=QLocale::c().toDouble(mAP.captured(2));
|
||||
col.setAlphaF(a/100.0);
|
||||
return col;
|
||||
}
|
||||
const auto mNP=rxNP.match(color);
|
||||
if (mNP.hasMatch()) {
|
||||
QColor col=jkqtp_lookupQColorName(mNP.captured(1));
|
||||
if (mNP.hasMatch() && mNP.captured(0).size()==color.size()) {
|
||||
QColor col=jkqtp_String2QColor(mNP.captured(1));
|
||||
const double a=QLocale::c().toInt(mNP.captured(2));
|
||||
col.setAlphaF(a/255.0);
|
||||
return col;
|
||||
}
|
||||
const auto mFrac=rxFrac.match(color);
|
||||
if (mFrac.hasMatch()) {
|
||||
if (mFrac.hasMatch() && mFrac.captured(0).size()==color.size()) {
|
||||
QColor col=jkqtp_lookupQColorName(mFrac.captured(1));
|
||||
const double a=static_cast<double>(QLocale::c().toInt(mFrac.captured(2)))/100.0;
|
||||
if (mFrac.captured(1)=="grey"||mFrac.captured(1)=="gray") col.setRgbF(a,a,a);
|
||||
@ -744,62 +743,10 @@ QColor jkqtp_String2QColor(QString color)
|
||||
return col;
|
||||
}
|
||||
#else
|
||||
QRegExp rxP("(.+)\\s*,\\s*t?\\s*(\\d+\\.?\\d+)\\%");
|
||||
QRegExp rxAP("(.+)\\s*,\\s*a\\s*(\\d+\\.?\\d+)\\%");
|
||||
QRegExp rxNP("(.+)\\s*,\\s*a?\\s*([\\d]+)");
|
||||
QRegExp rxP("([^,\\(\\)]+)\\s*,\\s*t?\\s*(\\d+\\.?\\d+)\\%");
|
||||
QRegExp rxAP("([^,\\(\\)]+)\\s*,\\s*a\\s*(\\d+\\.?\\d+)\\%");
|
||||
QRegExp rxNP("([^,\\(\\)]+)\\s*,\\s*a?\\s*([\\d]+)");
|
||||
QRegExp rxFrac("([a-zA-Z]{3,})(\\d{1,3})\\%?");
|
||||
QRegExp rxColMod("\\s*(rgb|hsl|hsv|rgba|gray|grey|red|green|blue)\\(\\s*(\\d+\\.?\\d*)(%|deg)?(?:\\s+|\\s*,\\s*|\\s\\/\\s)?(\\d+\\.?\\d*)?(%)?(?:\\s+|\\s*,\\s*|\\s\\/\\s)?(\\d+\\.?\\d*)?(%)?(?:\\s+|\\s*,\\s*|\\s*\\/\\s*)?(\\d+\\.?\\d*)?(%)?\\s*\\)\\s*");
|
||||
if (rxColMod.exactMatch(color)) {
|
||||
QColor col(Qt::black);
|
||||
const QString name=rxColMod.cap(1);
|
||||
const int v1=valUnitToInt(rxColMod.cap(2), rxColMod.cap(3));
|
||||
const int h1=valUnitToInt(rxColMod.cap(2), rxColMod.cap(3), INT_MAX);
|
||||
const int v2=valUnitToInt(rxColMod.cap(4), rxColMod.cap(5));
|
||||
const int a2=valUnitToAlphaInt(rxColMod.cap(4), rxColMod.cap(5));
|
||||
const int v3=valUnitToInt(rxColMod.cap(6), rxColMod.cap(7));
|
||||
const int a4=valUnitToAlphaInt(rxColMod.cap(8), rxColMod.cap(9));
|
||||
if (name=="gray"||name=="grey") {
|
||||
if (v2<0) col.setRgb(v1,v1,v1);
|
||||
else col.setRgb(v1,v1,v1,a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="red") {
|
||||
col.setRed(v1);
|
||||
if (v2>=0) col.setAlpha(a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="green") {
|
||||
col.setGreen(v1);
|
||||
if (v2>=0) col.setAlpha(a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="blue") {
|
||||
col.setBlue(v1);
|
||||
if (v2>=0) col.setAlpha(a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="rgb") {
|
||||
if (a4<0) col.setRgb(v1,v2,v3);
|
||||
else col.setRgb(v1,v2,v3,a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="rgba") {
|
||||
col.setRgb(v1,v2,v3,a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="hsl") {
|
||||
if (a4<0) col.setHsl(h1,v2,v3);
|
||||
else col.setHsl(h1,v2,v3,a2);
|
||||
return col;
|
||||
}
|
||||
else if (name=="hsv") {
|
||||
if (a4<0) col.setHsv(h1,v2,v3);
|
||||
else col.setHsv(h1,v2,v3,a2);
|
||||
return col;
|
||||
} else {
|
||||
qDebug()<<"unrecognized CSS-color:'"<<color<<"'";
|
||||
}
|
||||
}
|
||||
if (rxP.exactMatch(color)) {
|
||||
QColor col=jkqtp_lookupQColorName(rxP.cap(1));
|
||||
double a=QLocale::c().toDouble(rxP.cap(2));
|
||||
@ -826,8 +773,14 @@ QColor jkqtp_String2QColor(QString color)
|
||||
return col;
|
||||
}
|
||||
#endif
|
||||
|
||||
try {
|
||||
return JKQTPCSSParser::readColor(color);
|
||||
} catch(std::exception& E) {
|
||||
qWarning()<<"error converting '"<<color<<"' into a QColor: "<<E.what();
|
||||
return jkqtp_lookupQColorName(color);
|
||||
}
|
||||
}
|
||||
|
||||
std::string jkqtp_to_valid_variable_name(const std::string& input) {
|
||||
std::string out="";
|
||||
@ -1092,3 +1045,125 @@ QString jkqtp_backslashEscape(const QString &txt)
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
namespace JKQTCommon_private {
|
||||
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
|
||||
thread_local QCache<QString, QRegularExpression> rxCache(500);
|
||||
#else
|
||||
thread_local QCache<QString, QRegExp> rxCache(500);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool jkqtp_rxContains(const QString& text, const QString ®ex, qsizetype offset, QStringList* caps)
|
||||
{
|
||||
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
|
||||
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegularExpression(regex));
|
||||
QRegularExpression* rx=JKQTCommon_private::rxCache.object(regex);
|
||||
QSharedPointer<QRegularExpression> tempRX;
|
||||
if (!rx) {
|
||||
tempRX.reset(new QRegularExpression(regex));
|
||||
rx=tempRX.data();
|
||||
}
|
||||
const auto m=rx->match(text, offset);
|
||||
if (caps) *caps=m.capturedTexts();
|
||||
return m.hasMatch();
|
||||
#else
|
||||
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegExp(regex));
|
||||
QRegExp* rx=JKQTCommon_private::rxCache.object(regex);
|
||||
QSharedPointer<QRegExp> tempRX;
|
||||
if (!rx) {
|
||||
tempRX.reset(new QRegExp(regex));
|
||||
rx=tempRX.data();
|
||||
}
|
||||
const bool res= rx->indexIn(text, offset)>=0;
|
||||
if (caps) *caps=rx->capturedTexts();
|
||||
return res;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
qsizetype jkqtp_rxIndexIn(const QString& text, const QString ®ex, qsizetype offset, QStringList* caps)
|
||||
{
|
||||
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
|
||||
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegularExpression(regex));
|
||||
QRegularExpression* rx=JKQTCommon_private::rxCache.object(regex);
|
||||
QSharedPointer<QRegularExpression> tempRX;
|
||||
if (!rx) {
|
||||
tempRX.reset(new QRegularExpression(regex));
|
||||
rx=tempRX.data();
|
||||
}
|
||||
QRegularExpressionMatch rmatch;
|
||||
const qsizetype idx=text.indexOf(*rx, offset, &rmatch);
|
||||
if (caps) *caps=rmatch.capturedTexts();
|
||||
return idx;
|
||||
#else
|
||||
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegExp(regex));
|
||||
QRegExp* rx=JKQTCommon_private::rxCache.object(regex);
|
||||
QSharedPointer<QRegExp> tempRX;
|
||||
if (!rx) {
|
||||
tempRX.reset(new QRegExp(regex));
|
||||
rx=tempRX.data();
|
||||
}
|
||||
const qsizetype idx = rx->indexIn(text, offset);
|
||||
if (caps) *caps=rx->capturedTexts();
|
||||
return idx;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
bool jkqtp_rxPartiallyMatchesAt(const QString& text, const QString ®ex, qsizetype offset, QStringList* caps)
|
||||
{
|
||||
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
|
||||
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegularExpression(regex));
|
||||
QRegularExpression* rx=JKQTCommon_private::rxCache.object(regex);
|
||||
QSharedPointer<QRegularExpression> tempRX;
|
||||
if (!rx) {
|
||||
tempRX.reset(new QRegularExpression(regex));
|
||||
rx=tempRX.data();
|
||||
}
|
||||
const auto m=rx->match(text, offset, QRegularExpression::NormalMatch, QRegularExpression::AnchorAtOffsetMatchOption);
|
||||
if (caps) *caps=m.capturedTexts();
|
||||
return m.hasMatch();
|
||||
#else
|
||||
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegExp(regex));
|
||||
QRegExp* rx=JKQTCommon_private::rxCache.object(regex);
|
||||
QSharedPointer<QRegExp> tempRX;
|
||||
if (!rx) {
|
||||
tempRX.reset(new QRegExp(regex));
|
||||
rx=tempRX.data();
|
||||
}
|
||||
const bool res= (rx->indexIn(text, offset)==offset);
|
||||
if (caps) *caps=rx->capturedTexts();
|
||||
return res;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool jkqtp_rxExactlyMatches(const QString& text, const QString ®ex, QStringList* caps)
|
||||
{
|
||||
#if (QT_VERSION>=QT_VERSION_CHECK(6, 0, 0))
|
||||
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegularExpression(regex));
|
||||
QRegularExpression* rx=JKQTCommon_private::rxCache.object(regex);
|
||||
QSharedPointer<QRegularExpression> tempRX;
|
||||
if (!rx) {
|
||||
tempRX.reset(new QRegularExpression(regex));
|
||||
rx=tempRX.data();
|
||||
}
|
||||
const auto m=rx->match(text);
|
||||
if (caps) *caps=m.capturedTexts();
|
||||
return m.hasMatch() && m.captured(0).size()==text.size();
|
||||
#else
|
||||
if (!JKQTCommon_private::rxCache.contains(regex)) JKQTCommon_private::rxCache.insert(regex, new QRegExp(regex));
|
||||
QRegExp* rx=JKQTCommon_private::rxCache.object(regex);
|
||||
QSharedPointer<QRegExp> tempRX;
|
||||
if (!rx) {
|
||||
tempRX.reset(new QRegExp(regex));
|
||||
rx=tempRX.data();
|
||||
}
|
||||
const bool res= rx->exactMatch(text);
|
||||
if (caps) *caps=rx->capturedTexts();
|
||||
return res;
|
||||
#endif
|
||||
}
|
||||
|
@ -28,6 +28,8 @@
|
||||
#include <QtGlobal>
|
||||
#include <limits>
|
||||
|
||||
class QGradient; // forward
|
||||
|
||||
/** \brief converts a QT::PenStyle into a string
|
||||
* \ingroup jkqtptools_string
|
||||
*/
|
||||
@ -45,9 +47,24 @@ JKQTCOMMON_LIB_EXPORT QString jkqtp_QBrushStyle2String(Qt::BrushStyle style);
|
||||
|
||||
/** \brief converts a QString into a Qt::BrushStyle
|
||||
* \ingroup jkqtptools_string
|
||||
* \see jkqtp_String2QBrushStyleExt() for a more complex parser
|
||||
*/
|
||||
JKQTCOMMON_LIB_EXPORT Qt::BrushStyle jkqtp_String2QBrushStyle(const QString& style);
|
||||
|
||||
/** \brief converts a QString into a Qt::BrushStyle. commpared to jkqtp_String2QBrushStyle(), this method can parse
|
||||
* more complex pattern/brush descriptions, such as for colors, gradients or images, which are output in \a color, \a gradient and \a image
|
||||
* \ingroup jkqtptools_string
|
||||
*
|
||||
* \param style the string to be parsed
|
||||
* \param[out] color output parameter for a parsed color
|
||||
* \param[out] gradient output parameter for a parsed gradient
|
||||
* \param[out] image output parameter for a parsed image
|
||||
* \param[out] rotationAngleDeg output parameter for a parsed rotation angle of the pattern in degrees, where the direction equals the direction of a clock hand, i.e. 0=12o'clock, 180=6o'clock, ...
|
||||
*
|
||||
* \see jkqtp_String2QBrushStyle()
|
||||
*/
|
||||
JKQTCOMMON_LIB_EXPORT Qt::BrushStyle jkqtp_String2QBrushStyleExt(const QString& style, QColor* color=nullptr, QGradient* gradient=nullptr, QPixmap* image=nullptr, double *rotationAngleDeg=nullptr);
|
||||
|
||||
|
||||
/** \brief converts a Unicode codepoint into a UTF8-sequence
|
||||
* \ingroup jkqtptools_string
|
||||
@ -326,5 +343,51 @@ JKQTCOMMON_LIB_EXPORT QString jkqtp_MouseButton2String(Qt::MouseButton button, b
|
||||
*/
|
||||
JKQTCOMMON_LIB_EXPORT Qt::MouseButton jkqtp_String2MouseButton(const QString &button);
|
||||
|
||||
/** \brief returns \c true, if \a text contains a match to the given regular expression \a regex,
|
||||
* starts from \a offset and optionally returns the match in \a caps \c =[fullmatch, cap1,cap2,...]
|
||||
* \ingroup jkqtptools_string
|
||||
*
|
||||
* \note this function uses an internal cache, so the \a regex does not have
|
||||
* to be compiled every time this is called (with the same \a regex ).
|
||||
*
|
||||
* \see jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt()
|
||||
*/
|
||||
JKQTCOMMON_LIB_EXPORT bool jkqtp_rxContains(const QString& text, const QString ®ex, qsizetype offset=0, QStringList* caps=nullptr);
|
||||
|
||||
/** \brief returns the next match (i.e. its index) of the given regular expression \a regex within \a text,
|
||||
* starts from \a offset and optionally returns the match in \a caps \c =[fullmatch, cap1,cap2,...]
|
||||
* \ingroup jkqtptools_string
|
||||
*
|
||||
* \note this function uses an internal cache, so the \a regex does not have
|
||||
* to be compiled every time this is called (with the same \a regex ).
|
||||
*
|
||||
* \see jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt()
|
||||
*/
|
||||
JKQTCOMMON_LIB_EXPORT qsizetype jkqtp_rxIndexIn(const QString& text, const QString ®ex, qsizetype offset=0, QStringList* caps=nullptr);
|
||||
|
||||
/** \brief returns \c true, if \a text exactly matches the given regular expression \a regex,
|
||||
* starts from \a offset and optionally returns the match in \a caps \c =[fullmatch, cap1,cap2,...]
|
||||
* \ingroup jkqtptools_string
|
||||
*
|
||||
* \note this function uses an internal cache, so the \a regex does not have
|
||||
* to be compiled every time this is called (with the same \a regex ).
|
||||
*
|
||||
* \see jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt()
|
||||
*/
|
||||
JKQTCOMMON_LIB_EXPORT bool jkqtp_rxExactlyMatches(const QString& text, const QString ®ex, QStringList* caps=nullptr);
|
||||
|
||||
|
||||
/** \brief returns \c true, if \a text partially matches the given regular expression \a regex,
|
||||
* starting from \a offset (and the match starts at \a offset !!!)
|
||||
* and optionally returns the match in \a caps \c =[fullmatch, cap1,cap2,...]
|
||||
* \ingroup jkqtptools_string
|
||||
*
|
||||
* \note this function uses an internal cache, so the \a regex does not have
|
||||
* to be compiled every time this is called (with the same \a regex ).
|
||||
*
|
||||
* \see jkqtp_rxExactlyMatches(), jkqtp_rxIndexIn(), jkqtp_rxContains(), jkqtp_rxPartiallyMatchesAt()
|
||||
*/
|
||||
JKQTCOMMON_LIB_EXPORT bool jkqtp_rxPartiallyMatchesAt(const QString& text, const QString ®ex, qsizetype offset=0, QStringList* caps=nullptr);
|
||||
|
||||
|
||||
#endif // JKQTPSTRINGTOOLS_H_INCLUDED
|
||||
|
Loading…
Reference in New Issue
Block a user