File indexing completed on 2024-05-12 15:28:17

0001 /***************************************************************************
0002     File             : ExpressionTextEdit.cpp
0003     Project          : LabPlot
0004     --------------------------------------------------------------------
0005     Copyright        : (C) 2014-2017 Alexander Semke (alexander.semke@web.de)
0006     Description      : widget for defining mathematical expressions
0007     modified version of https://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
0008  ***************************************************************************/
0009 
0010 /***************************************************************************
0011  *                                                                         *
0012  *  This program is free software; you can redistribute it and/or modify   *
0013  *  it under the terms of the GNU General Public License as published by   *
0014  *  the Free Software Foundation; either version 2 of the License, or      *
0015  *  (at your option) any later version.                                    *
0016  *                                                                         *
0017  *  This program is distributed in the hope that it will be useful,        *
0018  *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
0019  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
0020  *  GNU General Public License for more details.                           *
0021  *                                                                         *
0022  *   You should have received a copy of the GNU General Public License     *
0023  *   along with this program; if not, write to the Free Software           *
0024  *   Foundation, Inc., 51 Franklin Street, Fifth Floor,                    *
0025  *   Boston, MA  02110-1301  USA                                           *
0026  *                                                                         *
0027  ***************************************************************************/
0028 
0029 /****************************************************************************
0030  **
0031  ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
0032  ** Contact: http://www.qt-project.org/legal
0033  **
0034  ** This file is part of the examples of the Qt Toolkit.
0035  **
0036  ** $QT_BEGIN_LICENSE:BSD$
0037  ** You may use this file under the terms of the BSD license as follows:
0038  **
0039  ** "Redistribution and use in source and binary forms, with or without
0040  ** modification, are permitted provided that the following conditions are
0041  ** met:
0042  **   * Redistributions of source code must retain the above copyright
0043  **     notice, this list of conditions and the following disclaimer.
0044  **   * Redistributions in binary form must reproduce the above copyright
0045  **     notice, this list of conditions and the following disclaimer in
0046  **     the documentation and/or other materials provided with the
0047  **     distribution.
0048  **   * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
0049  **     of its contributors may be used to endorse or promote products derived
0050  **     from this software without specific prior written permission.
0051  **
0052  **
0053  ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
0054  ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
0055  ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
0056  ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
0057  ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
0058  ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
0059  ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
0060  ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
0061  ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
0062  ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
0063  ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
0064  **
0065  ** $QT_END_LICENSE$
0066  **
0067  ****************************************************************************/
0068 
0069 #include "ExpressionTextEdit.h"
0070 #include "backend/gsl/ExpressionParser.h"
0071 #include "tools/EquationHighlighter.h"
0072 
0073 #include <QCompleter>
0074 #include <QKeyEvent>
0075 #include <QAbstractItemView>
0076 #include <QScrollBar>
0077 
0078 /*!
0079   \class ExpressionTextEdit
0080   \brief  Provides a widget for defining mathematical expressions
0081           Supports syntax-highlighting and completion.
0082 
0083           Modified version of https://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
0084 
0085   \ingroup kdefrontend
0086 */
0087 ExpressionTextEdit::ExpressionTextEdit(QWidget* parent) : KTextEdit(parent),
0088     m_highlighter(new EquationHighlighter(this)) {
0089 
0090     QStringList list = ExpressionParser::getInstance()->functions();
0091     list.append(ExpressionParser::getInstance()->constants());
0092 
0093     setTabChangesFocus(true);
0094 
0095     m_completer = new QCompleter(list, this);
0096     m_completer->setWidget(this);
0097     m_completer->setCompletionMode(QCompleter::PopupCompletion);
0098     m_completer->setCaseSensitivity(Qt::CaseInsensitive);
0099 
0100     connect(m_completer, QOverload<const QString&>::of(&QCompleter::activated), this, &ExpressionTextEdit::insertCompletion);
0101     connect(this, &ExpressionTextEdit::textChanged, this, [=](){ validateExpression();});
0102     connect(this, &ExpressionTextEdit::cursorPositionChanged, m_highlighter, &EquationHighlighter::rehighlight);
0103 }
0104 
0105 EquationHighlighter* ExpressionTextEdit::highlighter() {
0106     return m_highlighter;
0107 }
0108 
0109 bool ExpressionTextEdit::isValid() const {
0110     return (!document()->toPlainText().simplified().isEmpty() && m_isValid);
0111 }
0112 
0113 void ExpressionTextEdit::setExpressionType(XYEquationCurve::EquationType type) {
0114     m_expressionType = type;
0115     m_variables.clear();
0116     if (type == XYEquationCurve::EquationType::Cartesian)
0117         m_variables << "x";
0118     else if (type == XYEquationCurve::EquationType::Polar)
0119         m_variables << "phi";
0120     else if (type == XYEquationCurve::EquationType::Parametric)
0121         m_variables << "t";
0122     else if (type == XYEquationCurve::EquationType::Implicit)
0123         m_variables << "x" << "y";
0124 
0125     m_highlighter->setVariables(m_variables);
0126 }
0127 
0128 void ExpressionTextEdit::setVariables(const QStringList& vars) {
0129     m_variables = vars;
0130     m_highlighter->setVariables(m_variables);
0131     validateExpression(true);
0132 }
0133 
0134 void ExpressionTextEdit::insertCompletion(const QString& completion) {
0135     QTextCursor tc{ textCursor() };
0136     int extra{ completion.length() - m_completer->completionPrefix().length() };
0137     tc.movePosition(QTextCursor::Left);
0138     tc.movePosition(QTextCursor::EndOfWord);
0139     tc.insertText(completion.right(extra));
0140     setTextCursor(tc);
0141 }
0142 
0143 /*!
0144  * \brief Validates the current expression if the text was changed and highlights the text field red if the expression is invalid.
0145  * \param force forces the validation and highlighting when no text changes were made, used when new parameters/variables were provided
0146  */
0147 void ExpressionTextEdit::validateExpression(bool force) {
0148     //check whether the expression was changed or only the formatting
0149     QString text = toPlainText().simplified();
0150     bool textChanged{ (text != m_currentExpression) ? true : false };
0151 
0152     if (textChanged || force) {
0153         m_isValid = ExpressionParser::getInstance()->isValid(text, m_variables);
0154         if (!m_isValid)
0155             setStyleSheet("QTextEdit{background: red;}");
0156         else
0157             setStyleSheet(QString());
0158 
0159         m_currentExpression = text;
0160     }
0161     if (textChanged)
0162         emit expressionChanged();
0163 }
0164 
0165 //##############################################################################
0166 //####################################  Events   ###############################
0167 //##############################################################################
0168 void ExpressionTextEdit::focusInEvent(QFocusEvent* e) {
0169     m_completer->setWidget(this);
0170     QTextEdit::focusInEvent(e);
0171 }
0172 
0173 void ExpressionTextEdit::focusOutEvent(QFocusEvent* e) {
0174     //when loosing focus, rehighlight to remove potential highlighting of opening and closing brackets
0175     m_highlighter->rehighlight();
0176     QTextEdit::focusOutEvent(e);
0177 }
0178 
0179 void ExpressionTextEdit::keyPressEvent(QKeyEvent* e) {
0180     switch (e->key()) {
0181         case Qt::Key_Enter:
0182         case Qt::Key_Return:
0183             e->ignore();
0184             return;
0185         default:
0186             break;
0187     }
0188 
0189     const bool isShortcut = ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_E); // CTRL+E
0190     if (!isShortcut) // do not process the shortcut when we have a completer
0191         QTextEdit::keyPressEvent(e);
0192 
0193     const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier);
0194     if ((ctrlOrShift && e->text().isEmpty()))
0195         return;
0196 
0197     static QString eow("~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="); // end of word
0198     const bool hasModifier = (e->modifiers() != Qt::NoModifier) && !ctrlOrShift;
0199     QTextCursor tc = textCursor();
0200     tc.select(QTextCursor::WordUnderCursor);
0201     const QString& completionPrefix = tc.selectedText();
0202 
0203     if (!isShortcut && (hasModifier || e->text().isEmpty()|| completionPrefix.length() < 1
0204                     || eow.contains(e->text().right(1)))) {
0205         m_completer->popup()->hide();
0206         return;
0207     }
0208 
0209     if (completionPrefix != m_completer->completionPrefix()) {
0210         m_completer->setCompletionPrefix(completionPrefix);
0211         m_completer->popup()->setCurrentIndex(m_completer->completionModel()->index(0, 0));
0212     }
0213     QRect cr{ cursorRect() };
0214     cr.setWidth(m_completer->popup()->sizeHintForColumn(0)
0215                 + m_completer->popup()->verticalScrollBar()->sizeHint().width());
0216     m_completer->complete(cr); // popup it up!
0217 }
0218 
0219 void ExpressionTextEdit::mouseMoveEvent(QMouseEvent* e) {
0220     QTextCursor tc = cursorForPosition(e->pos());
0221     tc.select(QTextCursor::WordUnderCursor);
0222 
0223     const QString& token = tc.selectedText();
0224     if (token.isEmpty()) {
0225         setToolTip(QString());
0226         return;
0227     }
0228 
0229     //try to find the token under the mouse cursor in the list of constants first
0230     static const QStringList& constants = ExpressionParser::getInstance()->constants();
0231     int index = constants.indexOf(token);
0232 
0233     if (index != -1) {
0234         static const QStringList& names = ExpressionParser::getInstance()->constantsNames();
0235         static const QStringList& values = ExpressionParser::getInstance()->constantsValues();
0236         static const QStringList& units = ExpressionParser::getInstance()->constantsUnits();
0237         setToolTip(names.at(index) + ": " + constants.at(index) + " = " + values.at(index) + ' ' + units.at(index));
0238     } else {
0239         //text token was not found in the list of constants -> check functions as next
0240         static const QStringList& functions = ExpressionParser::getInstance()->functions();
0241         index = functions.indexOf(token);
0242         if (index != -1) {
0243             static const QStringList& names = ExpressionParser::getInstance()->functionsNames();
0244             setToolTip(functions.at(index) + " - " + names.at(index));
0245         } else
0246             setToolTip(QString());
0247     }
0248 
0249     KTextEdit::mouseMoveEvent(e);
0250 }