diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp index 1648175360..887063e8aa 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp @@ -93,9 +93,8 @@ QTextCharFormat GetCommentCharFormat() } } // namespace -ControlExpressionSyntaxHighlighter::ControlExpressionSyntaxHighlighter(QTextDocument* parent, - QLineEdit* result) - : QSyntaxHighlighter(parent), m_result_text(result) +ControlExpressionSyntaxHighlighter::ControlExpressionSyntaxHighlighter(QTextDocument* parent) + : QSyntaxHighlighter(parent) { } @@ -168,18 +167,11 @@ void ControlExpressionSyntaxHighlighter::highlightBlock(const QString&) } // This doesn't need to be run for every "block", but it works. - if (ciface::ExpressionParser::ParseStatus::Successful != tokenize_status) - { - m_result_text->setText(tr("Invalid Token.")); - } - else + if (ciface::ExpressionParser::ParseStatus::Successful == tokenize_status) { ciface::ExpressionParser::RemoveInertTokens(&tokens); const auto parse_status = ciface::ExpressionParser::ParseTokens(tokens); - m_result_text->setText( - QString::fromStdString(parse_status.description.value_or(_trans("Success.")))); - if (ciface::ExpressionParser::ParseStatus::Successful != parse_status.status) { const auto token = *parse_status.token; @@ -192,18 +184,33 @@ void ControlExpressionSyntaxHighlighter::highlightBlock(const QString&) class InputStateDelegate : public QItemDelegate { public: - explicit InputStateDelegate(IOWindow* parent); + explicit InputStateDelegate(IOWindow* parent, int column, + std::function state_evaluator); void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; private: - IOWindow* m_parent; + std::function m_state_evaluator; + int m_column; +}; + +class InputStateLineEdit : public QLineEdit +{ +public: + explicit InputStateLineEdit(std::function state_evaluator); + void SetShouldPaintStateIndicator(bool value); + void paintEvent(QPaintEvent* event) override; + +private: + std::function m_state_evaluator; + bool m_should_paint_state_indicator; }; IOWindow::IOWindow(MappingWidget* parent, ControllerEmu::EmulatedController* controller, ControlReference* ref, IOWindow::Type type) - : QDialog(parent), m_reference(ref), m_controller(controller), m_type(type) + : QDialog(parent), m_reference(ref), m_original_expression(ref->GetExpression()), + m_controller(controller), m_type(type) { CreateMainLayout(); @@ -233,16 +240,18 @@ void IOWindow::CreateMainLayout() m_test_button = new QPushButton(tr("Test"), this); 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_parse_text = new QLineEdit(); + m_parse_text = new InputStateLineEdit([this] { + const auto lock = m_controller->GetStateLock(); + return m_reference->GetState(); + }); m_parse_text->setReadOnly(true); m_expression_text = new QPlainTextEdit(); m_expression_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); - new ControlExpressionSyntaxHighlighter(m_expression_text->document(), m_parse_text); + new ControlExpressionSyntaxHighlighter(m_expression_text->document()); m_operators_combo = new QComboBox(); m_operators_combo->addItem(tr("Operators")); @@ -315,7 +324,10 @@ void IOWindow::CreateMainLayout() m_option_list->setColumnWidth(1, 64); m_option_list->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Fixed); - m_option_list->setItemDelegate(new InputStateDelegate(this)); + m_option_list->setItemDelegate(new InputStateDelegate(this, 1, [&](int row) { + // Clamp off negative values but allow greater than one in the text display. + return std::max(GetSelectedDevice()->Inputs()[row]->GetState(), 0.0); + })); } else { @@ -363,7 +375,6 @@ void IOWindow::CreateMainLayout() // Button Box m_main_layout->addWidget(m_button_box); m_button_box->addButton(m_clear_button, QDialogButtonBox::ActionRole); - m_button_box->addButton(m_apply_button, QDialogButtonBox::ActionRole); m_button_box->addButton(QDialogButtonBox::Ok); setLayout(m_main_layout); @@ -372,6 +383,10 @@ void IOWindow::CreateMainLayout() void IOWindow::ConfigChanged() { const QSignalBlocker blocker(this); + const auto lock = m_controller->GetStateLock(); + + // ensure m_parse_text is in the right state + UpdateExpression(m_reference->GetExpression(), UpdateMode::Force); m_expression_text->setPlainText(QString::fromStdString(m_reference->GetExpression())); m_expression_text->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor); @@ -387,6 +402,7 @@ void IOWindow::ConfigChanged() void IOWindow::Update() { m_option_list->viewport()->update(); + m_parse_text->update(); } void IOWindow::ConnectWidgets() @@ -401,10 +417,8 @@ void IOWindow::ConnectWidgets() connect(m_range_spinbox, qOverload(&QSpinBox::valueChanged), this, &IOWindow::OnRangeChanged); - connect(m_expression_text, &QPlainTextEdit::textChanged, [this] { - m_apply_button->setText(m_apply_button->text().remove(QStringLiteral("*"))); - m_apply_button->setText(m_apply_button->text() + QStringLiteral("*")); - }); + connect(m_expression_text, &QPlainTextEdit::textChanged, + [this] { UpdateExpression(m_expression_text->toPlainText().toStdString()); }); connect(m_operators_combo, qOverload(&QComboBox::activated), [this](int index) { if (0 == index) @@ -423,6 +437,9 @@ void IOWindow::ConnectWidgets() m_functions_combo->setCurrentIndex(0); }); + + // revert the expression when the window closes without using the OK button + connect(this, &IOWindow::finished, [this] { UpdateExpression(m_original_expression); }); } void IOWindow::AppendSelectedOption() @@ -448,18 +465,18 @@ void IOWindow::OnDialogButtonPressed(QAbstractButton* button) return; } - m_reference->SetExpression(m_expression_text->toPlainText().toStdString()); - m_controller->UpdateSingleControlReference(g_controller_interface, m_reference); + const auto lock = m_controller->GetStateLock(); - m_apply_button->setText(m_apply_button->text().remove(QStringLiteral("*"))); + UpdateExpression(m_expression_text->toPlainText().toStdString()); + m_original_expression = m_reference->GetExpression(); if (ciface::ExpressionParser::ParseStatus::SyntaxError == m_reference->GetParseStatus()) { ModalMessageBox::warning(this, tr("Error"), tr("The expression contains a syntax error.")); } - if (button != m_apply_button) - accept(); + // must be the OK button + accept(); } void IOWindow::OnDetectButtonPressed() @@ -532,8 +549,71 @@ void IOWindow::UpdateDeviceList() QString::fromStdString(m_controller->GetDefaultDevice().ToString())); } -InputStateDelegate::InputStateDelegate(IOWindow* parent) : QItemDelegate(parent), m_parent(parent) +void IOWindow::UpdateExpression(std::string new_expression, UpdateMode mode) { + const auto lock = m_controller->GetStateLock(); + if (mode != UpdateMode::Force && new_expression == m_reference->GetExpression()) + return; + + const auto error = m_reference->SetExpression(std::move(new_expression)); + const auto status = m_reference->GetParseStatus(); + m_controller->UpdateSingleControlReference(g_controller_interface, m_reference); + + if (error) + { + m_parse_text->SetShouldPaintStateIndicator(false); + m_parse_text->setText(QString::fromStdString(*error)); + } + else if (status == ciface::ExpressionParser::ParseStatus::EmptyExpression) + { + m_parse_text->SetShouldPaintStateIndicator(false); + m_parse_text->setText(QString()); + } + else if (status != ciface::ExpressionParser::ParseStatus::Successful) + { + m_parse_text->SetShouldPaintStateIndicator(false); + m_parse_text->setText(tr("Invalid Expression.")); + } + else + { + m_parse_text->SetShouldPaintStateIndicator(true); + m_parse_text->setText(QString()); + } +} + +InputStateDelegate::InputStateDelegate(IOWindow* parent, int column, + std::function state_evaluator) + : QItemDelegate(parent), m_state_evaluator(std::move(state_evaluator)), m_column(column) +{ +} + +InputStateLineEdit::InputStateLineEdit(std::function state_evaluator) + : m_state_evaluator(std::move(state_evaluator)) +{ +} + +static void PaintStateIndicator(QPainter& painter, const QRect& region, ControlState state) +{ + const QString state_string = QString::number(state, 'g', 4); + + QRect meter_region = region; + meter_region.setWidth(region.width() * std::clamp(state, 0.0, 1.0)); + + // Create a temporary indicator object to retreive color constants. + MappingIndicator indicator; + + // Normal text. + painter.setPen(indicator.GetTextColor()); + painter.drawText(region, Qt::AlignCenter, state_string); + + // Input state meter. + painter.fillRect(meter_region, indicator.GetAdjustedInputColor()); + + // Text on top of meter. + painter.setPen(indicator.GetAltTextColor()); + painter.setClipping(true); + painter.setClipRect(meter_region); + painter.drawText(region, Qt::AlignCenter, state_string); } void InputStateDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, @@ -541,35 +621,26 @@ void InputStateDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op { QItemDelegate::paint(painter, option, index); - // Don't do anything special for the first column. - if (index.column() == 0) + if (index.column() != m_column) return; - // Clamp off negative values but allow greater than one in the text display. - const auto state = - std::max(m_parent->GetSelectedDevice()->Inputs()[index.row()]->GetState(), 0.0); - const auto state_str = QString::number(state, 'g', 4); - - QRect rect = option.rect; - rect.setWidth(rect.width() * std::clamp(state, 0.0, 1.0)); - - // Create a temporary indicator object to retreive color constants. - MappingIndicator indicator; - painter->save(); - - // Normal text. - painter->setPen(indicator.GetTextColor()); - painter->drawText(option.rect, Qt::AlignCenter, state_str); - - // Input state meter. - painter->fillRect(rect, indicator.GetAdjustedInputColor()); - - // Text on top of meter. - painter->setPen(indicator.GetAltTextColor()); - painter->setClipping(true); - painter->setClipRect(rect); - painter->drawText(option.rect, Qt::AlignCenter, state_str); - + PaintStateIndicator(*painter, option.rect, m_state_evaluator(index.row())); painter->restore(); } + +void InputStateLineEdit::SetShouldPaintStateIndicator(bool value) +{ + m_should_paint_state_indicator = value; +} + +void InputStateLineEdit::paintEvent(QPaintEvent* event) +{ + QLineEdit::paintEvent(event); + + if (!m_should_paint_state_indicator) + return; + + QPainter painter(this); + PaintStateIndicator(painter, this->rect(), m_state_evaluator()); +} diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.h b/Source/Core/DolphinQt/Config/Mapping/IOWindow.h index 9870a4c954..28294fc284 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.h +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.h @@ -4,6 +4,9 @@ #pragma once +#include +#include + #include #include #include @@ -30,17 +33,16 @@ namespace ControllerEmu class EmulatedController; } +class InputStateLineEdit; + class ControlExpressionSyntaxHighlighter final : public QSyntaxHighlighter { Q_OBJECT public: - ControlExpressionSyntaxHighlighter(QTextDocument* parent, QLineEdit* result); + explicit ControlExpressionSyntaxHighlighter(QTextDocument* parent); protected: void highlightBlock(const QString& text) final override; - -private: - QLineEdit* const m_result_text; }; class IOWindow final : public QDialog @@ -74,6 +76,14 @@ private: void UpdateOptionList(); void UpdateDeviceList(); + enum class UpdateMode + { + Normal, + Force, + }; + + void UpdateExpression(std::string new_expression, UpdateMode mode = UpdateMode::Normal); + // Main Layout QVBoxLayout* m_main_layout; @@ -100,14 +110,14 @@ private: // Textarea QPlainTextEdit* m_expression_text; - QLineEdit* m_parse_text; + InputStateLineEdit* m_parse_text; // Buttonbox QDialogButtonBox* m_button_box; QPushButton* m_clear_button; - QPushButton* m_apply_button; ControlReference* m_reference; + std::string m_original_expression; ControllerEmu::EmulatedController* m_controller; ciface::Core::DeviceQualifier m_devq; diff --git a/Source/Core/InputCommon/ControlReference/ControlReference.cpp b/Source/Core/InputCommon/ControlReference/ControlReference.cpp index b1dd5deee3..671f2f3379 100644 --- a/Source/Core/InputCommon/ControlReference/ControlReference.cpp +++ b/Source/Core/InputCommon/ControlReference/ControlReference.cpp @@ -50,12 +50,13 @@ std::string ControlReference::GetExpression() const return m_expression; } -void ControlReference::SetExpression(std::string expr) +std::optional ControlReference::SetExpression(std::string expr) { m_expression = std::move(expr); auto parse_result = ParseExpression(m_expression); m_parse_status = parse_result.status; m_parsed_expression = std::move(parse_result.expr); + return parse_result.description; } ControlReference::ControlReference() : range(1), m_parsed_expression(nullptr) diff --git a/Source/Core/InputCommon/ControlReference/ControlReference.h b/Source/Core/InputCommon/ControlReference/ControlReference.h index de289ba265..fb4b5cc1df 100644 --- a/Source/Core/InputCommon/ControlReference/ControlReference.h +++ b/Source/Core/InputCommon/ControlReference/ControlReference.h @@ -38,7 +38,9 @@ public: ciface::ExpressionParser::ParseStatus GetParseStatus() const; void UpdateReference(ciface::ExpressionParser::ControlEnvironment& env); std::string GetExpression() const; - void SetExpression(std::string expr); + + // Returns a human-readable error description when the given expression is invalid. + std::optional SetExpression(std::string expr); ControlState range;