diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp index dab6876d5a..4660daafff 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "Core/Core.h" @@ -26,11 +27,115 @@ #include "DolphinQt/QtUtils/BlockUserInputFilter.h" #include "InputCommon/ControlReference/ControlReference.h" +#include "InputCommon/ControlReference/ExpressionParser.h" #include "InputCommon/ControllerEmu/ControllerEmu.h" #include "InputCommon/ControllerInterface/ControllerInterface.h" constexpr int SLIDER_TICK_COUNT = 100; +namespace +{ +QTextCharFormat GetSpecialCharFormat() +{ + QTextCharFormat format; + format.setFontWeight(QFont::Weight::Bold); + return format; +} + +QTextCharFormat GetOperatorCharFormat() +{ + QTextCharFormat format; + format.setFontWeight(QFont::Weight::Bold); + format.setForeground(QBrush{Qt::darkBlue}); + return format; +} + +QTextCharFormat GetLiteralCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::darkMagenta}); + return format; +} + +QTextCharFormat GetInvalidCharFormat() +{ + QTextCharFormat format; + format.setUnderlineStyle(QTextCharFormat::WaveUnderline); + format.setUnderlineColor(Qt::darkRed); + return format; +} + +QTextCharFormat GetControlCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::darkGreen}); + return format; +} + +QTextCharFormat GetVariableCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::magenta}); + return format; +} + +QTextCharFormat GetFunctionCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::darkCyan}); + return format; +} + +class SyntaxHighlighter : public QSyntaxHighlighter +{ +public: + SyntaxHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) {} + + void highlightBlock(const QString& text) final override + { + // TODO: This is going to result in improper highlighting with non-ascii characters: + ciface::ExpressionParser::Lexer lexer(text.toStdString()); + + std::vector tokens; + lexer.Tokenize(tokens); + + using ciface::ExpressionParser::TokenType; + + for (auto& token : tokens) + { + switch (token.type) + { + case TokenType::TOK_INVALID: + setFormat(token.string_position, token.string_length, GetInvalidCharFormat()); + break; + case TokenType::TOK_LPAREN: + case TokenType::TOK_RPAREN: + case TokenType::TOK_COMMA: + setFormat(token.string_position, token.string_length, GetSpecialCharFormat()); + break; + case TokenType::TOK_LITERAL: + setFormat(token.string_position, token.string_length, GetLiteralCharFormat()); + break; + case TokenType::TOK_CONTROL: + setFormat(token.string_position, token.string_length, GetControlCharFormat()); + break; + case TokenType::TOK_FUNCTION: + setFormat(token.string_position, token.string_length, GetFunctionCharFormat()); + break; + case TokenType::TOK_VARIABLE: + setFormat(token.string_position, token.string_length, GetVariableCharFormat()); + break; + default: + if (token.IsBinaryOperator()) + setFormat(token.string_position, token.string_length, GetOperatorCharFormat()); + break; + } + } + } +}; + +} // namespace + IOWindow::IOWindow(QWidget* parent, ControllerEmu::EmulatedController* controller, ControlReference* ref, IOWindow::Type type) : QDialog(parent), m_reference(ref), m_controller(controller), m_type(type) @@ -54,13 +159,16 @@ void IOWindow::CreateMainLayout() m_select_button = new QPushButton(tr("Select")); m_detect_button = new QPushButton(tr("Detect")); m_test_button = new QPushButton(tr("Test")); - m_expression_text = new QPlainTextEdit(); m_button_box = new QDialogButtonBox(); m_clear_button = new QPushButton(tr("Clear")); m_apply_button = new QPushButton(tr("Apply")); m_range_slider = new QSlider(Qt::Horizontal); m_range_spinbox = new QSpinBox(); + m_expression_text = new QPlainTextEdit(); + m_expression_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + new SyntaxHighlighter(m_expression_text->document()); + m_operators_combo = new QComboBox(); m_operators_combo->addItem(tr("Operators")); m_operators_combo->insertSeparator(1); diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index b0704d3d8d..08278154f4 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -20,33 +20,6 @@ namespace ciface::ExpressionParser { using namespace ciface::Core; -enum TokenType -{ - TOK_DISCARD, - TOK_INVALID, - TOK_EOF, - TOK_LPAREN, - TOK_RPAREN, - TOK_FUNCTION, - TOK_CONTROL, - TOK_LITERAL, - TOK_VARIABLE, - // Binary Ops: - TOK_BINARY_OPS_BEGIN, - TOK_AND = TOK_BINARY_OPS_BEGIN, - TOK_OR, - TOK_ADD, - TOK_SUB, - TOK_MUL, - TOK_DIV, - TOK_MOD, - TOK_ASSIGN, - TOK_LTHAN, - TOK_GTHAN, - TOK_COMMA, - TOK_BINARY_OPS_END, -}; - inline std::string OpName(TokenType op) { switch (op) @@ -83,213 +56,213 @@ inline std::string OpName(TokenType op) } } -class Token +Token::Token(TokenType type_) : type(type_) { -public: - TokenType type; - std::string data; +} - Token(TokenType type_) : type(type_) {} - Token(TokenType type_, std::string data_) : type(type_), data(std::move(data_)) {} - operator std::string() const +Token::Token(TokenType type_, std::string data_) : type(type_), data(std::move(data_)) +{ +} + +bool Token::IsBinaryOperator() const +{ + return type >= TOK_BINARY_OPS_BEGIN && type < TOK_BINARY_OPS_END; +} + +Token::operator std::string() const +{ + switch (type) { - switch (type) - { - case TOK_DISCARD: - return "Discard"; - case TOK_EOF: - return "EOF"; - case TOK_LPAREN: - return "("; - case TOK_RPAREN: - return ")"; - case TOK_AND: - return "&"; - case TOK_OR: - return "|"; - case TOK_FUNCTION: - return '!' + data; - case TOK_ADD: - return "+"; - case TOK_SUB: - return "-"; - case TOK_MUL: - return "*"; - case TOK_DIV: - return "/"; - case TOK_MOD: - return "%"; - case TOK_ASSIGN: - return "="; - case TOK_LTHAN: - return "<"; - case TOK_GTHAN: - return ">"; - case TOK_COMMA: - return ","; - case TOK_CONTROL: - return "Device(" + data + ')'; - case TOK_LITERAL: - return '\'' + data + '\''; - case TOK_VARIABLE: - return '$' + data; - default: + case TOK_DISCARD: + return "Discard"; + case TOK_EOF: + return "EOF"; + case TOK_LPAREN: + return "("; + case TOK_RPAREN: + return ")"; + case TOK_AND: + return "&"; + case TOK_OR: + return "|"; + case TOK_FUNCTION: + return '!' + data; + case TOK_ADD: + return "+"; + case TOK_SUB: + return "-"; + case TOK_MUL: + return "*"; + case TOK_DIV: + return "/"; + case TOK_MOD: + return "%"; + case TOK_ASSIGN: + return "="; + case TOK_LTHAN: + return "<"; + case TOK_GTHAN: + return ">"; + case TOK_COMMA: + return ","; + case TOK_CONTROL: + return "Device(" + data + ')'; + case TOK_LITERAL: + return '\'' + data + '\''; + case TOK_VARIABLE: + return '$' + data; + default: + break; + } + + return "Invalid"; +} + +Lexer::Lexer(const std::string& expr_) : expr(expr_) +{ + it = expr.begin(); +} + +std::string Lexer::FetchDelimString(char delim) +{ + const std::string result = FetchCharsWhile([delim](char c) { return c != delim; }); + if (it != expr.end()) + ++it; + return result; +} + +std::string Lexer::FetchWordChars() +{ + // Words must start with a letter or underscore. + if (expr.end() == it || (!std::isalpha(*it, std::locale::classic()) && ('_' != *it))) + return ""; + + // Valid word characters: + std::regex rx("[a-z0-9_]", std::regex_constants::icase); + + return FetchCharsWhile([&rx](char c) { return std::regex_match(std::string(1, c), rx); }); +} + +Token Lexer::GetFunction() +{ + return Token(TOK_FUNCTION, FetchWordChars()); +} + +Token Lexer::GetDelimitedLiteral() +{ + return Token(TOK_LITERAL, FetchDelimString('\'')); +} + +Token Lexer::GetVariable() +{ + return Token(TOK_VARIABLE, FetchWordChars()); +} + +Token Lexer::GetFullyQualifiedControl() +{ + return Token(TOK_CONTROL, FetchDelimString('`')); +} + +Token Lexer::GetBarewordsControl(char c) +{ + std::string name; + name += c; + name += FetchCharsWhile([](char c) { return std::isalpha(c, std::locale::classic()); }); + + ControlQualifier qualifier; + qualifier.control_name = name; + return Token(TOK_CONTROL, qualifier); +} + +Token Lexer::GetRealLiteral(char c) +{ + std::string value; + value += c; + value += FetchCharsWhile([](char c) { return isdigit(c, std::locale::classic()) || ('.' == c); }); + + return Token(TOK_LITERAL, value); +} + +Token Lexer::NextToken() +{ + if (it == expr.end()) + return Token(TOK_EOF); + + char c = *it++; + switch (c) + { + case ' ': + case '\t': + case '\n': + case '\r': + return Token(TOK_DISCARD); + case '(': + return Token(TOK_LPAREN); + case ')': + return Token(TOK_RPAREN); + case '&': + return Token(TOK_AND); + case '|': + return Token(TOK_OR); + case '!': + return GetFunction(); + case '+': + return Token(TOK_ADD); + case '-': + return Token(TOK_SUB); + case '*': + return Token(TOK_MUL); + case '/': + return Token(TOK_DIV); + case '%': + return Token(TOK_MOD); + case '=': + return Token(TOK_ASSIGN); + case '<': + return Token(TOK_LTHAN); + case '>': + return Token(TOK_GTHAN); + case ',': + return Token(TOK_COMMA); + case '\'': + return GetDelimitedLiteral(); + case '$': + return GetVariable(); + case '`': + return GetFullyQualifiedControl(); + default: + if (isalpha(c, std::locale::classic())) + return GetBarewordsControl(c); + else if (isdigit(c, std::locale::classic())) + return GetRealLiteral(c); + else + return Token(TOK_INVALID); + } +} + +ParseStatus Lexer::Tokenize(std::vector& tokens) +{ + while (true) + { + const std::size_t string_position = it - expr.begin(); + Token tok = NextToken(); + + tok.string_position = string_position; + tok.string_length = it - expr.begin(); + + if (tok.type == TOK_DISCARD) + continue; + + tokens.push_back(tok); + + if (tok.type == TOK_INVALID) + return ParseStatus::SyntaxError; + + if (tok.type == TOK_EOF) break; - } - - return "Invalid"; } -}; - -class Lexer -{ -public: - std::string expr; - std::string::iterator it; - - Lexer(const std::string& expr_) : expr(expr_) { it = expr.begin(); } - - template - std::string FetchCharsWhile(F&& func) - { - std::string value; - while (it != expr.end() && func(*it)) - { - value += *it; - ++it; - } - return value; - } - - std::string FetchDelimString(char delim) - { - const std::string result = FetchCharsWhile([delim](char c) { return c != delim; }); - if (it != expr.end()) - ++it; - return result; - } - - std::string FetchWordChars() - { - // Words must start with a letter or underscore. - if (expr.end() == it || (!std::isalpha(*it, std::locale::classic()) && ('_' != *it))) - return ""; - - // Valid word characters: - std::regex rx("[a-z0-9_]", std::regex_constants::icase); - - return FetchCharsWhile([&rx](char c) { return std::regex_match(std::string(1, c), rx); }); - } - - Token GetFunction() { return Token(TOK_FUNCTION, FetchWordChars()); } - - Token GetDelimitedLiteral() { return Token(TOK_LITERAL, FetchDelimString('\'')); } - - Token GetVariable() { return Token(TOK_VARIABLE, FetchWordChars()); } - - Token GetFullyQualifiedControl() { return Token(TOK_CONTROL, FetchDelimString('`')); } - - Token GetBarewordsControl(char c) - { - std::string name; - name += c; - name += FetchCharsWhile([](char c) { return std::isalpha(c, std::locale::classic()); }); - - ControlQualifier qualifier; - qualifier.control_name = name; - return Token(TOK_CONTROL, qualifier); - } - - Token GetRealLiteral(char c) - { - std::string value; - value += c; - value += - FetchCharsWhile([](char c) { return isdigit(c, std::locale::classic()) || ('.' == c); }); - - return Token(TOK_LITERAL, value); - } - - Token NextToken() - { - if (it == expr.end()) - return Token(TOK_EOF); - - char c = *it++; - switch (c) - { - case ' ': - case '\t': - case '\n': - case '\r': - return Token(TOK_DISCARD); - case '(': - return Token(TOK_LPAREN); - case ')': - return Token(TOK_RPAREN); - case '&': - return Token(TOK_AND); - case '|': - return Token(TOK_OR); - case '!': - return GetFunction(); - case '+': - return Token(TOK_ADD); - case '-': - return Token(TOK_SUB); - case '*': - return Token(TOK_MUL); - case '/': - return Token(TOK_DIV); - case '%': - return Token(TOK_MOD); - case '=': - return Token(TOK_ASSIGN); - case '<': - return Token(TOK_LTHAN); - case '>': - return Token(TOK_GTHAN); - case ',': - return Token(TOK_COMMA); - case '\'': - return GetDelimitedLiteral(); - case '$': - return GetVariable(); - case '`': - return GetFullyQualifiedControl(); - default: - if (isalpha(c, std::locale::classic())) - return GetBarewordsControl(c); - else if (isdigit(c, std::locale::classic())) - return GetRealLiteral(c); - else - return Token(TOK_INVALID); - } - } - - ParseStatus Tokenize(std::vector& tokens) - { - while (true) - { - Token tok = NextToken(); - - if (tok.type == TOK_DISCARD) - continue; - - if (tok.type == TOK_INVALID) - { - tokens.clear(); - return ParseStatus::SyntaxError; - } - - tokens.push_back(tok); - - if (tok.type == TOK_EOF) - break; - } - return ParseStatus::Successful; - } -}; + return ParseStatus::Successful; +} class ControlExpression : public Expression { @@ -418,7 +391,7 @@ public: class LiteralExpression : public Expression { public: - void SetValue(ControlState value) override + void SetValue(ControlState) override { // Do nothing. } @@ -695,11 +668,6 @@ private: } } - static bool IsBinaryToken(TokenType type) - { - return type >= TOK_BINARY_OPS_BEGIN && type < TOK_BINARY_OPS_END; - } - static int BinaryOperatorPrecedence(TokenType type) { switch (type) @@ -738,7 +706,7 @@ private: std::unique_ptr expr = std::move(lhs.expr); // TODO: handle LTR/RTL associativity? - while (IsBinaryToken(Peek().type) && BinaryOperatorPrecedence(Peek().type) < precedence) + while (Peek().IsBinaryOperator() && BinaryOperatorPrecedence(Peek().type) < precedence) { const Token tok = Chew(); ParseResult rhs = ParseBinary(BinaryOperatorPrecedence(tok.type)); diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.h b/Source/Core/InputCommon/ControlReference/ExpressionParser.h index 0cb84af5cd..8e3e6cc1a9 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.h +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.h @@ -12,6 +12,92 @@ namespace ciface::ExpressionParser { +enum TokenType +{ + TOK_DISCARD, + TOK_INVALID, + TOK_EOF, + TOK_LPAREN, + TOK_RPAREN, + TOK_FUNCTION, + TOK_CONTROL, + TOK_LITERAL, + TOK_VARIABLE, + // Binary Ops: + TOK_BINARY_OPS_BEGIN, + TOK_AND = TOK_BINARY_OPS_BEGIN, + TOK_OR, + TOK_ADD, + TOK_SUB, + TOK_MUL, + TOK_DIV, + TOK_MOD, + TOK_ASSIGN, + TOK_LTHAN, + TOK_GTHAN, + TOK_COMMA, + TOK_BINARY_OPS_END, +}; + +class Token +{ +public: + TokenType type; + std::string data; + + // Position in the input string: + std::size_t string_position = 0; + std::size_t string_length = 0; + + Token(TokenType type_); + Token(TokenType type_, std::string data_); + + bool IsBinaryOperator() const; + operator std::string() const; +}; + +enum class ParseStatus +{ + Successful, + SyntaxError, + EmptyExpression, +}; + +class Lexer +{ +public: + std::string expr; + std::string::iterator it; + + Lexer(const std::string& expr_); + + ParseStatus Tokenize(std::vector& tokens); + +private: + template + std::string FetchCharsWhile(F&& func) + { + std::string value; + while (it != expr.end() && func(*it)) + { + value += *it; + ++it; + } + return value; + } + + std::string FetchDelimString(char delim); + std::string FetchWordChars(); + Token GetFunction(); + Token GetDelimitedLiteral(); + Token GetVariable(); + Token GetFullyQualifiedControl(); + Token GetBarewordsControl(char c); + Token GetRealLiteral(char c); + + Token NextToken(); +}; + class ControlQualifier { public: @@ -80,12 +166,5 @@ public: virtual operator std::string() const = 0; }; -enum class ParseStatus -{ - Successful, - SyntaxError, - EmptyExpression, -}; - std::pair> ParseExpression(const std::string& expr); } // namespace ciface::ExpressionParser