| |
| |
| #include "toonzqt/expressionfield.h" |
| |
| #include "texpression.h" |
| #include "tparser.h" |
| #include "tconvert.h" |
| #include <QSyntaxHighlighter> |
| #include <QCompleter> |
| #include <QKeyEvent> |
| #include <QAbstractItemView> |
| #include <QScrollBar> |
| #include <QStringListModel> |
| #include <QStandardItemModel> |
| #include <QStandardItem> |
| #include <QToolTip> |
| #include <QListView> |
| #include <QLabel> |
| |
| using namespace DVGui; |
| using namespace TSyntax; |
| |
| |
| |
| class ExpressionField::SyntaxHighlighter final : public QSyntaxHighlighter { |
| const Grammar *m_grammar; |
| |
| public: |
| bool m_open; |
| SyntaxHighlighter(QTextDocument *parent) |
| : QSyntaxHighlighter(parent), m_grammar(0), m_open(true) {} |
| ~SyntaxHighlighter() {} |
| |
| void setGrammar(const Grammar *grammar) { m_grammar = grammar; } |
| |
| void highlightBlock(const QString &text) override { |
| Parser parser(m_grammar); |
| std::vector<SyntaxToken> tokens; |
| Parser::SyntaxStatus status = |
| parser.checkSyntax(tokens, text.toStdString()); |
| |
| int nextPos = 0; |
| for (int i = 0; i < (int)tokens.size(); i++) { |
| QTextCharFormat fmt; |
| int pos = tokens[i].m_pos; |
| int length = tokens[i].m_length; |
| int type = tokens[i].m_type; |
| nextPos = pos + length; |
| switch (type) { |
| case TSyntax::Unknown: |
| fmt.setForeground(Qt::black); |
| break; |
| case TSyntax::Number: |
| fmt.setForeground(QColor(0x50, 0x7d, 0x0)); |
| break; |
| case TSyntax::Constant: |
| fmt.setForeground(QColor(0x50, 0x7d, 0x0)); |
| break; |
| case TSyntax::Variable: |
| fmt.setForeground(QColor(0x0, 0x88, 0xc8)); |
| break; |
| case TSyntax::Operator: |
| fmt.setForeground(QColor(50, 0, 255)); |
| break; |
| case TSyntax::Parenthesis: |
| fmt.setForeground(QColor(50, 50, 255)); |
| break; |
| case TSyntax::Function: |
| fmt.setForeground(QColor(0x0, 0x50, 0x7d)); |
| break; |
| case TSyntax::Comma: |
| fmt.setForeground(QColor(50, 20, 255)); |
| break; |
| |
| case TSyntax::UnexpectedToken: |
| fmt.setForeground(QColor(0xdc, 0x0, 0x0)); |
| break; |
| case TSyntax::Eos: |
| fmt.setForeground(QColor(255, 127, 0)); |
| break; |
| case TSyntax::Mismatch: |
| fmt.setForeground(QColor(255, 0, 0)); |
| break; |
| |
| default: |
| fmt.setForeground(QColor(127, 127, 255)); |
| break; |
| } |
| if (type == 4) fmt.setToolTip("Infix"); |
| if (length == 0) length = 1; |
| setFormat(pos, length, fmt); |
| } |
| } |
| }; |
| |
| |
| |
| class MyListView final : public QListView { |
| QLabel *m_tooltip; |
| |
| public: |
| MyListView() : QListView() { |
| setObjectName("SuggestionPopup"); |
| setStyleSheet( |
| "#SuggestionPopup {background-color:#FFFFFF; border:1px solid black;}"); |
| setWindowFlags(Qt::Popup); |
| setMouseTracking(true); |
| m_tooltip = new QLabel(0, Qt::ToolTip); |
| |
| m_tooltip->hide(); |
| |
| m_tooltip->setObjectName("helpTooltip"); |
| m_tooltip->setAlignment(Qt::AlignLeft); |
| m_tooltip->setIndent(1); |
| m_tooltip->setWordWrap(false); |
| } |
| void showEvent(QShowEvent *) override { showToolTip(currentIndex()); } |
| void hideEvent(QHideEvent *) override { m_tooltip->hide(); } |
| void currentChanged(const QModelIndex ¤t, |
| const QModelIndex &previous) override { |
| showToolTip(current); |
| QListView::currentChanged(current, previous); |
| } |
| void showToolTip(const QModelIndex &index) { |
| if (!index.isValid()) { |
| m_tooltip->hide(); |
| return; |
| } |
| QVariant data = model()->data(index, Qt::ToolTipRole); |
| if (!data.isValid()) { |
| m_tooltip->hide(); |
| return; |
| } |
| QRect rect = visualRect(index); |
| m_tooltip->setText(data.toString()); |
| QPoint pos = viewport()->mapToGlobal( |
| QPoint(-m_tooltip->sizeHint().width(), rect.top())); |
| m_tooltip->setGeometry(QRect(pos, m_tooltip->sizeHint())); |
| m_tooltip->show(); |
| } |
| |
| protected: |
| void resizeEvent(QResizeEvent *e) override { |
| QListView::resizeEvent(e); |
| if (m_tooltip->isVisible()) showToolTip(currentIndex()); |
| } |
| }; |
| |
| |
| |
| ExpressionField::ExpressionField(QWidget *parent) |
| : QTextEdit(parent) |
| , m_editing(false) |
| , m_grammar(0) |
| , m_syntaxHighlighter(0) |
| , m_completerPopup(0) |
| , m_completerStartPos(0) { |
| setFrameStyle(QFrame::StyledPanel); |
| setObjectName("ExpressionField"); |
| setLineWrapMode(NoWrap); |
| setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
| setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
| setTabChangesFocus(true); |
| |
| connect(this, SIGNAL(textChanged()), this, SLOT(onTextChanged())); |
| |
| #ifdef MACOSX |
| setFixedHeight(23); |
| #else |
| setFixedHeight(20); |
| #endif |
| |
| m_completerPopup = new MyListView(); |
| QStandardItemModel *model = new QStandardItemModel(); |
| m_completerPopup->setModel(model); |
| m_completerPopup->setFocusPolicy(Qt::NoFocus); |
| m_completerPopup->setFocusProxy(this); |
| m_completerPopup->installEventFilter(this); |
| connect(m_completerPopup, SIGNAL(clicked(const QModelIndex &)), this, |
| SLOT(insertCompletion(const QModelIndex &))); |
| |
| m_syntaxHighlighter = new SyntaxHighlighter(document()); |
| } |
| |
| ExpressionField::~ExpressionField() { delete m_syntaxHighlighter; } |
| |
| void ExpressionField::showEvent(QShowEvent *e) { QTextEdit::showEvent(e); } |
| |
| void ExpressionField::hideEvent(QHideEvent *e) { QTextEdit::hideEvent(e); } |
| |
| void ExpressionField::setExpression(std::string expression) { |
| setPlainText(QString::fromStdString(expression)); |
| } |
| |
| std::string ExpressionField::getExpression() const { |
| return toPlainText().toStdString(); |
| } |
| |
| bool ExpressionField::event(QEvent *e) { |
| if (e->type() == QEvent::ToolTip) { |
| QHelpEvent *helpEvent = static_cast<QHelpEvent *>(e); |
| |
| |
| } else if (e->type() == QEvent::ShortcutOverride) { |
| e->accept(); |
| return true; |
| } |
| |
| return QTextEdit::event(e); |
| } |
| |
| void ExpressionField::keyPressEvent(QKeyEvent *e) { |
| |
| if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { |
| m_editing = false; |
| emit expressionChanged(); |
| } else if (e->key() == Qt::Key_F10) { |
| setAutoFillBackground(true); |
| QPalette p = palette(); |
| p.setColor(QPalette::Base, Qt::cyan); |
| p.setColor(QPalette::Background, Qt::cyan); |
| setPalette(p); |
| update(); |
| setStyleSheet("#ExpressionField {background-color:cyan;}"); |
| } else if (e->key() == Qt::Key_F11) { |
| m_completerPopup->installEventFilter(this); |
| QRect cr = cursorRect(); |
| QSize size(100, 200); |
| QPoint pos = mapToGlobal(QPoint(cr.left(), cr.top() - size.height())); |
| m_completerPopup->setGeometry(QRect(pos, size)); |
| m_completerPopup->show(); |
| QTextEdit::keyPressEvent(e); |
| } else { |
| QTextEdit::keyPressEvent(e); |
| if (m_completerPopup->isVisible()) { |
| updateCompleterPopup(); |
| } else if (Qt::Key_A <= e->key() && e->key() <= Qt::Key_Z || |
| std::string("+&|!*/=?,:-").find(e->key()) != std::string::npos) { |
| openCompleterPopup(); |
| } |
| setFocus(); |
| } |
| } |
| |
| bool ExpressionField::eventFilter(QObject *obj, QEvent *e) { |
| if (e->type() == QEvent::KeyPress) { |
| QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e); |
| switch (keyEvent->key()) { |
| case Qt::Key_Return: |
| case Qt::Key_Enter: |
| insertCompletion(m_completerPopup->currentIndex()); |
| return true; |
| |
| case Qt::Key_Left: |
| case Qt::Key_Right: |
| event(keyEvent); |
| m_completerPopup->hide(); |
| return true; |
| |
| case Qt::Key_Escape: |
| m_completerPopup->hide(); |
| return true; |
| |
| case Qt::Key_Down: |
| case Qt::Key_Up: |
| return false; |
| |
| default: |
| event(e); |
| return true; |
| } |
| } else if (e->type() == QEvent::MouseButtonPress) { |
| m_completerPopup->hide(); |
| event(e); |
| return true; |
| } else if (e->type() == QEvent::ShortcutOverride) { |
| e->accept(); |
| return true; |
| } |
| return QObject::eventFilter(obj, e); |
| } |
| |
| void ExpressionField::onTextChanged() { |
| if (!m_editing) { |
| m_editing = true; |
| |
| |
| } |
| } |
| |
| void ExpressionField::focusInEvent(QFocusEvent *e) { |
| m_syntaxHighlighter->m_open = true; |
| m_syntaxHighlighter->rehighlight(); |
| QTextEdit::focusInEvent(e); |
| } |
| |
| void ExpressionField::focusOutEvent(QFocusEvent *e) { |
| m_syntaxHighlighter->m_open = false; |
| m_syntaxHighlighter->rehighlight(); |
| QTextEdit::focusOutEvent(e); |
| } |
| |
| void ExpressionField::onCursorPositionChanged() {} |
| |
| void ExpressionField::openCompleterPopup() { |
| int n = computeSuggestions(); |
| if (n < 2) return; |
| if (updateCompleterPopup()) m_completerPopup->show(); |
| } |
| |
| bool ExpressionField::updateCompleterPopup() { |
| int start = m_completerStartPos; |
| int pos = textCursor().position(); |
| std::string text = getExpression(); |
| if (m_suggestions.empty() || start < 0 || start > pos || |
| pos > (int)text.length()) { |
| if (m_completerPopup->isVisible()) m_completerPopup->hide(); |
| return false; |
| } |
| |
| QStandardItemModel *model = new QStandardItemModel(); |
| std::string prefix = toLower(text.substr(start, pos - start)); |
| int prefixLength = prefix.length(); |
| int count = 0; |
| for (int i = 0; i < (int)m_suggestions.size(); i++) { |
| std::string item = m_suggestions[i].first; |
| if ((int)item.length() >= prefixLength && |
| toLower(item.substr(0, prefixLength)) == prefix) { |
| QStandardItem *item = new QStandardItem(); |
| item->setData(QString::fromStdString(m_suggestions[i].first), |
| Qt::EditRole); |
| if (m_suggestions[i].second != "") |
| item->setData(QString::fromStdString(m_suggestions[i].second), |
| Qt::ToolTipRole); |
| model->appendRow(item); |
| count++; |
| } |
| } |
| if (count == 0) { |
| if (m_completerPopup->isVisible()) m_completerPopup->hide(); |
| return false; |
| } |
| m_completerPopup->setModel(model); |
| m_completerPopup->setCurrentIndex(model->index(0, 0)); |
| |
| QTextCursor cursor(textCursor()); |
| cursor.setPosition(m_completerStartPos); |
| QRect cr = cursorRect(cursor); |
| |
| int w = m_completerPopup->sizeHintForColumn(0) + |
| m_completerPopup->verticalScrollBar()->sizeHint().width() + 5; |
| int h = |
| (m_completerPopup->sizeHintForRow(0) * qMin(7, model->rowCount()) + 3) + |
| 3; |
| |
| QSize size(w, h); |
| QPoint popupPos = mapToGlobal(QPoint(cr.left(), cr.bottom() + 3)); |
| m_completerPopup->setGeometry(QRect(popupPos, size)); |
| return true; |
| } |
| |
| int ExpressionField::computeSuggestions() { |
| m_completerStartPos = -1; |
| m_suggestions.clear(); |
| |
| std::string text = getExpression(); |
| int pos = textCursor().position(); |
| int start = pos; |
| if (start > 0) { |
| start--; |
| while (start > 0) { |
| char c = text[start - 1]; |
| if (isascii(c) && isalpha(c) || c == '_' || |
| c == '.' && (start - 2 < 0 || |
| isascii(text[start - 2]) && isalpha(text[start - 2]))) { |
| } else |
| break; |
| start--; |
| } |
| } |
| if (start >= (int)text.length()) return 0; |
| m_completerStartPos = start; |
| text = text.substr(0, start); |
| TSyntax::Parser parser(m_grammar); |
| parser.getSuggestions(m_suggestions, text); |
| return (int)m_suggestions.size(); |
| } |
| |
| void ExpressionField::insertCompletion() { |
| if (!m_completerPopup->isVisible()) return; |
| QModelIndex index = m_completerPopup->currentIndex(); |
| if (!index.isValid()) return; |
| QString item = |
| m_completerPopup->model()->data(index, Qt::EditRole).toString(); |
| QTextCursor tc = textCursor(); |
| int pos = tc.position(); |
| |
| tc.insertText(item); |
| m_completerPopup->hide(); |
| } |
| |
| void ExpressionField::insertCompletion(const QModelIndex &index) { |
| if (index.isValid()) { |
| QString item = |
| m_completerPopup->model()->data(index, Qt::EditRole).toString(); |
| |
| QTextCursor tc = textCursor(); |
| int pos = tc.position(); |
| if (pos - m_completerStartPos >= 1) |
| tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, |
| pos - m_completerStartPos); |
| tc.insertText(item); |
| } |
| m_completerPopup->hide(); |
| } |
| |
| void ExpressionField::setGrammar(const Grammar *grammar) { |
| m_grammar = grammar; |
| m_syntaxHighlighter->setGrammar(grammar); |
| }; |
| |