mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 00:28:42 +00:00
Formula Editor Improvements 1 of 2
.. update the editor to do some basic syntax highlighting; literals in red, comments in blue and so on. .. next commit will focus on highlighting errors with a wavy line and some form of error list.
This commit is contained in:
@@ -155,6 +155,144 @@ Leaf::isDynamic(Leaf *leaf)
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
DataFilter::colorSyntax(QTextDocument *document)
|
||||
{
|
||||
// for looking for comments
|
||||
QString string = document->toPlainText();
|
||||
|
||||
// clear formatting and color comments
|
||||
QTextCharFormat normal;
|
||||
normal.setUnderlineStyle(QTextCharFormat::NoUnderline);
|
||||
normal.setForeground(Qt::black);
|
||||
|
||||
QTextCharFormat comment;
|
||||
comment.setUnderlineStyle(QTextCharFormat::NoUnderline);
|
||||
comment.setForeground(Qt::blue);
|
||||
|
||||
QTextCursor cursor(document);
|
||||
|
||||
// set all black
|
||||
cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
|
||||
cursor.selectionStart();
|
||||
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||
cursor.selectionEnd();
|
||||
cursor.setCharFormat(normal);
|
||||
|
||||
// color comments
|
||||
bool instring=false;
|
||||
bool incomment=false;
|
||||
int commentstart=0;
|
||||
|
||||
for(int i=0; i<string.length(); i++) {
|
||||
|
||||
// watch for being in a string
|
||||
if (string[i] == '"' && !incomment) instring=!instring;
|
||||
|
||||
// watch for being in a comment
|
||||
if (string[i] == '#' && !instring && !incomment) {
|
||||
incomment = true;
|
||||
commentstart=i;
|
||||
}
|
||||
|
||||
if ((string[i] == '\r' || string[i] == '\n')) {
|
||||
|
||||
// mark if we have a comment
|
||||
if (incomment) {
|
||||
|
||||
// we have a line of comments to here from commentstart
|
||||
incomment = false;
|
||||
|
||||
// comments are blue
|
||||
cursor.setPosition(commentstart, QTextCursor::MoveAnchor);
|
||||
cursor.selectionStart();
|
||||
cursor.setPosition(i, QTextCursor::KeepAnchor);
|
||||
cursor.selectionEnd();
|
||||
cursor.setCharFormat(comment);
|
||||
} else {
|
||||
instring = false; // always quit on end of line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply selective coloring to the symbols and expressions
|
||||
if(treeRoot) treeRoot->color(treeRoot, document);
|
||||
}
|
||||
|
||||
void Leaf::color(Leaf *leaf, QTextDocument *document)
|
||||
{
|
||||
// nope
|
||||
if (leaf == NULL) return;
|
||||
|
||||
QTextCharFormat apply;
|
||||
apply.setForeground(Qt::red); // default to yucky
|
||||
|
||||
switch(leaf->type) {
|
||||
case Leaf::Float :
|
||||
case Leaf::Integer :
|
||||
case Leaf::String :
|
||||
apply.setForeground(Qt::red);
|
||||
break;
|
||||
|
||||
case Leaf::Symbol :
|
||||
apply.setForeground(Qt::green);
|
||||
break;
|
||||
|
||||
case Leaf::Logical :
|
||||
leaf->color(leaf->lvalue.l, document);
|
||||
if (leaf->op) leaf->color(leaf->rvalue.l, document);
|
||||
return;
|
||||
break;
|
||||
|
||||
case Leaf::Operation :
|
||||
leaf->color(leaf->lvalue.l, document);
|
||||
leaf->color(leaf->rvalue.l, document);
|
||||
return;
|
||||
break;
|
||||
|
||||
case Leaf::BinaryOperation :
|
||||
leaf->color(leaf->lvalue.l, document);
|
||||
leaf->color(leaf->rvalue.l, document);
|
||||
return;
|
||||
break;
|
||||
|
||||
case Leaf::Function :
|
||||
if (leaf->series) {
|
||||
if (leaf->lvalue.l) leaf->color(leaf->lvalue.l, document);
|
||||
return;
|
||||
} else {
|
||||
foreach(Leaf*l, leaf->fparms) leaf->color(l, document);
|
||||
apply.setForeground(Qt::green);
|
||||
}
|
||||
break;
|
||||
|
||||
case Leaf::Conditional :
|
||||
{
|
||||
leaf->color(leaf->cond.l, document);
|
||||
leaf->color(leaf->lvalue.l, document);
|
||||
leaf->color(leaf->rvalue.l, document);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
// lets apply that then
|
||||
QTextCursor cursor(document);
|
||||
|
||||
// highlight this token and apply
|
||||
cursor.setPosition(leaf->loc, QTextCursor::MoveAnchor);
|
||||
cursor.selectionStart();
|
||||
cursor.setPosition(leaf->leng+1, QTextCursor::KeepAnchor);
|
||||
cursor.selectionEnd();
|
||||
|
||||
cursor.setCharFormat(apply);
|
||||
}
|
||||
|
||||
void Leaf::print(Leaf *leaf, int level)
|
||||
{
|
||||
qDebug()<<"LEVEL"<<level;
|
||||
@@ -340,7 +478,7 @@ void Leaf::validateFilter(DataFilter *df, Leaf *leaf)
|
||||
DataFiltererrors << QString(QObject::tr("invalid data series for tiz(): %1")).arg(symbol);
|
||||
|
||||
if (leaf->function == "config" && !configValidSymbols.exactMatch(symbol))
|
||||
DataFiltererrors << QString(QObject::tr("invalid data series for config(): %1")).arg(symbol);
|
||||
DataFiltererrors << QString(QObject::tr("invalid literal for config(): %1")).arg(symbol);
|
||||
|
||||
if (leaf->function == "const") {
|
||||
if (!constValidSymbols.exactMatch(symbol))
|
||||
@@ -473,6 +611,35 @@ Result DataFilter::evaluate(RideItem *item)
|
||||
return res;
|
||||
}
|
||||
|
||||
QStringList DataFilter::check(QString query)
|
||||
{
|
||||
// remember where we apply
|
||||
isdynamic=false;
|
||||
|
||||
// Parse from string
|
||||
DataFiltererrors.clear(); // clear out old errors
|
||||
DataFilter_setString(query);
|
||||
DataFilterparse();
|
||||
DataFilter_clearString();
|
||||
|
||||
// save away the results
|
||||
treeRoot = root;
|
||||
|
||||
// if it passed syntax lets check semantics
|
||||
if (treeRoot && DataFiltererrors.count() == 0) treeRoot->validateFilter(this, treeRoot);
|
||||
|
||||
// ok, did it pass all tests?
|
||||
if (!treeRoot || DataFiltererrors.count() > 0) { // nope
|
||||
|
||||
// no errors just failed to finish
|
||||
if (!treeRoot) DataFiltererrors << tr("malformed expression.");
|
||||
|
||||
}
|
||||
|
||||
errors = DataFiltererrors;
|
||||
return errors;
|
||||
}
|
||||
|
||||
QStringList DataFilter::parseFilter(QString query, QStringList *list)
|
||||
{
|
||||
// remember where we apply
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <QDebug>
|
||||
#include <QList>
|
||||
#include <QStringList>
|
||||
#include <QTextDocument>
|
||||
#include "RideCache.h"
|
||||
#include "RideFile.h" //for SeriesType
|
||||
|
||||
@@ -50,13 +51,14 @@ class Leaf {
|
||||
|
||||
public:
|
||||
|
||||
Leaf() : type(none),op(0),series(NULL),dynamic(false) { }
|
||||
Leaf(int loc, int leng) : type(none),op(0),series(NULL),dynamic(false),loc(loc),leng(leng) { }
|
||||
|
||||
// evaluate against a RideItem
|
||||
Result eval(Context *context, DataFilter *df, Leaf *, RideItem *m);
|
||||
|
||||
// tree traversal etc
|
||||
void print(Leaf *, int level); // print leaf and all children
|
||||
void color(Leaf *, QTextDocument *); // update the document to match
|
||||
bool isDynamic(Leaf *);
|
||||
void validateFilter(DataFilter *, Leaf*); // validate
|
||||
bool isNumber(DataFilter *df, Leaf *leaf);
|
||||
@@ -78,6 +80,7 @@ class Leaf {
|
||||
Leaf *series; // is a symbol
|
||||
bool dynamic;
|
||||
RideFile::SeriesType seriesType; // for ridefilecache
|
||||
int loc, leng;
|
||||
};
|
||||
|
||||
class DataFilter : public QObject
|
||||
@@ -100,11 +103,14 @@ class DataFilter : public QObject
|
||||
|
||||
// when used for formulas
|
||||
Result evaluate(RideItem *rideItem);
|
||||
QStringList getErrors() { return errors; };
|
||||
void colorSyntax(QTextDocument *content);
|
||||
|
||||
static QStringList functions(); // return list of functions supported
|
||||
|
||||
public slots:
|
||||
QStringList parseFilter(QString query, QStringList *list=0);
|
||||
QStringList check(QString query);
|
||||
void clearFilter();
|
||||
void configChanged(qint32);
|
||||
void dynamicParse();
|
||||
|
||||
@@ -25,11 +25,18 @@
|
||||
// tokens
|
||||
#include "DataFilter_yacc.h"/* generated by the scanner */
|
||||
|
||||
int DataFiltercolumn = 1;
|
||||
|
||||
#define YY_USER_ACTION DataFilterlloc.first_line = DataFilterlloc.last_line = DataFilterlineno; \
|
||||
DataFilterlloc.first_column = DataFiltercolumn; DataFilterlloc.last_column = DataFiltercolumn + DataFilterleng - 1; \
|
||||
DataFiltercolumn += DataFilterleng;
|
||||
|
||||
%}
|
||||
%option noyywrap
|
||||
%option nounput
|
||||
%option noinput
|
||||
%option 8bit
|
||||
%option yylineno
|
||||
%%
|
||||
|
||||
"=" DataFilterlval.op = EQ; return EQ;
|
||||
@@ -104,6 +111,7 @@ int DataFilterlex_destroy(void) { return 0; }
|
||||
void DataFilter_setString(QString p)
|
||||
{
|
||||
BEGIN(0);
|
||||
DataFiltercolumn = 0;
|
||||
DataFilter_scan_string(p.toLatin1().data());
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ extern Leaf *root; // root node for parsed statement
|
||||
char function[32];
|
||||
}
|
||||
|
||||
%locations
|
||||
|
||||
%type <leaf> symbol value lexpr expr parms;
|
||||
%type <op> lop cop bop;
|
||||
|
||||
@@ -84,7 +86,7 @@ extern Leaf *root; // root node for parsed statement
|
||||
filter: lexpr { root = $1; }
|
||||
;
|
||||
|
||||
parms: lexpr { $$ = new Leaf();
|
||||
parms: lexpr { $$ = new Leaf(@1.first_column, @1.last_column);
|
||||
$$->type = Leaf::Parameters;
|
||||
$$->fparms << $1;
|
||||
}
|
||||
@@ -92,42 +94,42 @@ parms: lexpr { $$ = new Leaf();
|
||||
| parms ',' lexpr { $1->fparms << $3; }
|
||||
;
|
||||
|
||||
lexpr : expr lop expr { $$ = new Leaf();
|
||||
lexpr : expr lop expr { $$ = new Leaf(@1.first_column, @3.last_column);
|
||||
$$->type = Leaf::Logical;
|
||||
$$->lvalue.l = $1;
|
||||
$$->op = $2;
|
||||
$$->rvalue.l = $3; }
|
||||
| lexpr lop lexpr { $$ = new Leaf();
|
||||
| lexpr lop lexpr { $$ = new Leaf(@1.first_column, @3.last_column);
|
||||
$$->type = Leaf::Logical;
|
||||
$$->lvalue.l = $1;
|
||||
$$->op = $2;
|
||||
$$->rvalue.l = $3; }
|
||||
| '(' expr ')' { $$ = new Leaf();
|
||||
| '(' expr ')' { $$ = new Leaf(@2.first_column, @2.last_column);
|
||||
$$->type = Leaf::Logical;
|
||||
$$->lvalue.l = $2;
|
||||
$$->op = 0; }
|
||||
| expr
|
||||
| expr { $$ = $1; }
|
||||
;
|
||||
|
||||
|
||||
expr : '(' expr ')' { $$ = new Leaf();
|
||||
expr : '(' expr ')' { $$ = new Leaf(@2.first_column, @2.last_column);
|
||||
$$->type = Leaf::Logical;
|
||||
$$->lvalue.l = $2;
|
||||
$$->op = 0; }
|
||||
|
||||
| expr cop expr { $$ = new Leaf();
|
||||
| expr cop expr { $$ = new Leaf(@1.first_column, @3.last_column);
|
||||
$$->type = Leaf::Operation;
|
||||
$$->lvalue.l = $1;
|
||||
$$->op = $2;
|
||||
$$->rvalue.l = $3; }
|
||||
|
||||
| expr bop expr { $$ = new Leaf();
|
||||
| expr bop expr { $$ = new Leaf(@1.first_column, @3.last_column);
|
||||
$$->type = Leaf::BinaryOperation;
|
||||
$$->lvalue.l = $1;
|
||||
$$->op = $2;
|
||||
$$->rvalue.l = $3; }
|
||||
|
||||
| expr Q expr COL expr { $$ = new Leaf();
|
||||
| expr Q expr COL expr { $$ = new Leaf(@1.first_column, @5.last_column);
|
||||
$$->type = Leaf::Conditional;
|
||||
$$->op = 0; // unused
|
||||
$$->lvalue.l = $3;
|
||||
@@ -179,63 +181,64 @@ bop : ADD
|
||||
| POW
|
||||
;
|
||||
|
||||
symbol : SYMBOL { $$ = new Leaf(); $$->type = Leaf::Symbol;
|
||||
if (QString(DataFiltertext) == "BikeScore")
|
||||
$$->lvalue.n = new QString("BikeScore™");
|
||||
else
|
||||
$$->lvalue.n = new QString(DataFiltertext);
|
||||
}
|
||||
symbol : SYMBOL { $$ = new Leaf(@1.first_column, @1.last_column);
|
||||
$$->type = Leaf::Symbol;
|
||||
if (QString(DataFiltertext) == "BikeScore")
|
||||
$$->lvalue.n = new QString("BikeScore™");
|
||||
else
|
||||
$$->lvalue.n = new QString(DataFiltertext);
|
||||
}
|
||||
;
|
||||
|
||||
value : symbol { $$ = $1; }
|
||||
|
||||
| DF_STRING { $$ = new Leaf(); $$->type = Leaf::String;
|
||||
| DF_STRING { $$ = new Leaf(@1.first_column, @1.last_column); $$->type = Leaf::String;
|
||||
QString s2(DataFiltertext);
|
||||
$$->lvalue.s = new QString(s2.mid(1,s2.length()-2)); }
|
||||
| DF_FLOAT { $$ = new Leaf(); $$->type = Leaf::Float;
|
||||
| DF_FLOAT { $$ = new Leaf(@1.first_column, @1.last_column); $$->type = Leaf::Float;
|
||||
$$->lvalue.f = QString(DataFiltertext).toFloat(); }
|
||||
| DF_INTEGER { $$ = new Leaf(); $$->type = Leaf::Integer;
|
||||
| DF_INTEGER { $$ = new Leaf(@1.first_column, @1.last_column); $$->type = Leaf::Integer;
|
||||
$$->lvalue.i = QString(DataFiltertext).toInt(); }
|
||||
|
||||
| BEST '(' symbol ',' lexpr ')' { $$ = new Leaf(); $$->type = Leaf::Function;
|
||||
| BEST '(' symbol ',' lexpr ')' { $$ = new Leaf(@1.first_column, @6.last_column); $$->type = Leaf::Function;
|
||||
$$->function = QString($1);
|
||||
$$->series = $3;
|
||||
$$->lvalue.l = $5;
|
||||
}
|
||||
|
||||
|
||||
| TIZ '(' symbol ',' lexpr ')' { $$ = new Leaf(); $$->type = Leaf::Function;
|
||||
| TIZ '(' symbol ',' lexpr ')' { $$ = new Leaf(@1.first_column, @6.last_column); $$->type = Leaf::Function;
|
||||
$$->function = QString($1);
|
||||
$$->series = $3;
|
||||
$$->lvalue.l = $5;
|
||||
}
|
||||
|
||||
| LTS '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function;
|
||||
| LTS '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function;
|
||||
$$->function = QString($1);
|
||||
$$->series = $3;
|
||||
$$->lvalue.l = NULL;
|
||||
}
|
||||
| STS '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function;
|
||||
| STS '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function;
|
||||
$$->function = QString($1);
|
||||
$$->series = $3;
|
||||
$$->lvalue.l = NULL;
|
||||
}
|
||||
| RR '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function;
|
||||
| RR '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function;
|
||||
$$->function = QString($1);
|
||||
$$->series = $3;
|
||||
$$->lvalue.l = NULL;
|
||||
}
|
||||
| SB '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function;
|
||||
| SB '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function;
|
||||
$$->function = QString($1);
|
||||
$$->series = $3;
|
||||
$$->lvalue.l = NULL;
|
||||
}
|
||||
| CONFIG '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function;
|
||||
| CONFIG '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function;
|
||||
$$->function = QString($1);
|
||||
$$->series = $3;
|
||||
$$->lvalue.l = NULL;
|
||||
}
|
||||
| CONST_ '(' symbol ')' { $$ = new Leaf(); $$->type = Leaf::Function;
|
||||
| CONST_ '(' symbol ')' { $$ = new Leaf(@1.first_column, @4.last_column); $$->type = Leaf::Function;
|
||||
$$->function = QString($1);
|
||||
$$->series = $3;
|
||||
$$->lvalue.l = NULL;
|
||||
|
||||
@@ -1703,7 +1703,7 @@ EditMetricDetailDialog::EditMetricDetailDialog(Context *context, LTMTool *ltmToo
|
||||
//formulaWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
QVBoxLayout *formulaLayout = new QVBoxLayout(formulaWidget);
|
||||
formulaLayout->addStretch();
|
||||
formulaEdit = new DataFilterEdit(this);
|
||||
formulaEdit = new DataFilterEdit(this, context);
|
||||
//formulaEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
formulaType = new QComboBox(this);
|
||||
formulaType->addItem(tr("Total"), static_cast<int>(RideMetric::Total));
|
||||
@@ -2370,8 +2370,8 @@ LTMTool::setFilter(QStringList files)
|
||||
emit filterChanged();
|
||||
}
|
||||
|
||||
DataFilterEdit::DataFilterEdit(QWidget *parent)
|
||||
: QTextEdit(parent), c(0)
|
||||
DataFilterEdit::DataFilterEdit(QWidget *parent, Context *context)
|
||||
: QTextEdit(parent), context(context), c(0)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -2379,6 +2379,18 @@ DataFilterEdit::~DataFilterEdit()
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
DataFilterEdit::checkErrors()
|
||||
{
|
||||
// parse and present errors to user
|
||||
DataFilter checker(this, context);
|
||||
QStringList errors = checker.check(toPlainText());
|
||||
checker.colorSyntax(document());
|
||||
|
||||
// need to fixup for errors!
|
||||
// XXX next commit
|
||||
}
|
||||
|
||||
void DataFilterEdit::setCompleter(QCompleter *completer)
|
||||
{
|
||||
if (c)
|
||||
@@ -2411,6 +2423,16 @@ void DataFilterEdit::insertCompletion(const QString& completion)
|
||||
tc.movePosition(QTextCursor::EndOfWord);
|
||||
tc.insertText(completion.right(extra));
|
||||
setTextCursor(tc);
|
||||
|
||||
checkErrors();
|
||||
}
|
||||
|
||||
void
|
||||
DataFilterEdit::setText(const QString &text)
|
||||
{
|
||||
// set text..
|
||||
QTextEdit::setText(text);
|
||||
checkErrors();
|
||||
}
|
||||
|
||||
QString DataFilterEdit::textUnderCursor() const
|
||||
@@ -2428,6 +2450,7 @@ void DataFilterEdit::focusInEvent(QFocusEvent *e)
|
||||
|
||||
void DataFilterEdit::keyPressEvent(QKeyEvent *e)
|
||||
{
|
||||
// wait a couple of seconds before checking the changes....
|
||||
if (c && c->popup()->isVisible()) {
|
||||
// The following keys are forwarded by the completer to the widget
|
||||
switch (e->key()) {
|
||||
@@ -2447,6 +2470,9 @@ void DataFilterEdit::keyPressEvent(QKeyEvent *e)
|
||||
if (!c || !isShortcut) // do not process the shortcut when we have a completer
|
||||
QTextEdit::keyPressEvent(e);
|
||||
|
||||
// check
|
||||
checkErrors();
|
||||
|
||||
const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier);
|
||||
if (!c || (ctrlOrShift && e->text().isEmpty()))
|
||||
return;
|
||||
|
||||
@@ -237,7 +237,7 @@ class DataFilterEdit : public QTextEdit
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DataFilterEdit(QWidget *parent = 0);
|
||||
DataFilterEdit(QWidget *parent, Context *context);
|
||||
~DataFilterEdit();
|
||||
|
||||
void setCompleter(QCompleter *c);
|
||||
@@ -247,14 +247,17 @@ protected:
|
||||
void keyPressEvent(QKeyEvent *e);
|
||||
void focusInEvent(QFocusEvent *e);
|
||||
|
||||
private slots:
|
||||
public slots:
|
||||
void setText(const QString&);
|
||||
void insertCompletion(const QString &completion);
|
||||
void checkErrors();
|
||||
|
||||
private:
|
||||
QString textUnderCursor() const;
|
||||
|
||||
private:
|
||||
QCompleter *c;
|
||||
Context *context;
|
||||
};
|
||||
|
||||
#endif // _GC_LTMTool_h
|
||||
|
||||
Reference in New Issue
Block a user