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 }