/*************************************************************************** * Copyright (c) 2004 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ # include # include # include # include # include # include # include #include #include "CallTips.h" #include "TextEdit.h" #include "SyntaxHighlighter.h" #include "Tools.h" #include using namespace Gui; /** * Constructs a TextEdit which is a child of 'parent'. */ TextEdit::TextEdit(QWidget* parent) : QPlainTextEdit(parent), cursorPosition(0), listBox(nullptr) { // create the window for call tips callTipsList = new CallTipsList(this); callTipsList->setFrameStyle(QFrame::Box); callTipsList->setFrameShadow(QFrame::Raised); callTipsList->setLineWidth(2); installEventFilter(callTipsList); viewport()->installEventFilter(callTipsList); callTipsList->setSelectionMode( QAbstractItemView::SingleSelection ); callTipsList->hide(); //Note: Set the correct context to this shortcut as we may use several instances of this //class at a time auto shortcut = new QShortcut(this); shortcut->setKey(QKeySequence(QStringLiteral("CTRL+Space"))); shortcut->setContext(Qt::WidgetShortcut); connect(shortcut, &QShortcut::activated, this, &TextEdit::complete); auto shortcutFind = new QShortcut(this); shortcutFind->setKey(QKeySequence::Find); shortcutFind->setContext(Qt::WidgetShortcut); connect(shortcutFind, &QShortcut::activated, this, &TextEdit::showSearchBar); auto shortcutNext = new QShortcut(this); shortcutNext->setKey(QKeySequence::FindNext); shortcutNext->setContext(Qt::WidgetShortcut); connect(shortcutNext, &QShortcut::activated, this, &TextEdit::findNext); auto shortcutPrev = new QShortcut(this); shortcutPrev->setKey(QKeySequence::FindPrevious); shortcutPrev->setContext(Qt::WidgetShortcut); connect(shortcutPrev, &QShortcut::activated, this, &TextEdit::findPrevious); } /** Destroys the object and frees any allocated resources */ TextEdit::~TextEdit() = default; /** * Set the approproriate item of the completion box or hide it, if needed. */ void TextEdit::keyPressEvent(QKeyEvent* e) { // We want input to be appended to document before doing calltips QPlainTextEdit::keyPressEvent(e); // This can't be done in CompletionList::eventFilter() because we must first perform // the event and afterwards update the list widget if (listBox && listBox->isVisible()) { // Get the word under the cursor QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::StartOfWord); // the cursor has moved to outside the word prefix if (cursor.position() < cursorPosition-wordPrefix.length() || cursor.position() > cursorPosition) { listBox->hide(); return; } cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); listBox->keyboardSearch(cursor.selectedText()); cursor.clearSelection(); } if (e->key() == Qt::Key_Period) { // QTextCursor cursor = this->textCursor(); // In Qt 4.8 there is a strange behaviour because when pressing ":" // then key is also set to 'Period' instead of 'Colon'. So we have // to make sure we only handle the period. if (e->text() == QLatin1String(".")) { // analyse context and show available call tips // TODO: idk why we need to remove the . from the input string (- 1). This shouldn't be needed QString textToBeCompleted = getInputString().left(getInputStringPosition() - 1); callTipsList->showTips( textToBeCompleted ); } } // This can't be done in CallTipsList::eventFilter() because we must first perform // the event and afterwards update the list widget if (callTipsList->isVisible()) { callTipsList->validateCursor(); } } int TextEdit::getInputStringPosition() { return textCursor().positionInBlock(); } QString TextEdit::getInputString() { return textCursor().block().text(); } void TextEdit::wheelEvent(QWheelEvent* e) { // Reimplement from QPlainText::wheelEvent as zoom is only allowed natively if !isReadOnly if (e->modifiers() & Qt::ControlModifier) { float delta = e->angleDelta().y() / 120.f; zoomInF(delta); return; } QPlainTextEdit::wheelEvent(e); } /** * Completes the word. */ void TextEdit::complete() { QTextBlock block = textCursor().block(); if (!block.isValid()) return; int cursorPos = textCursor().position()-block.position(); QString para = block.text(); int wordStart = cursorPos; while (wordStart > 0 && para[wordStart - 1].isLetterOrNumber()) --wordStart; wordPrefix = para.mid(wordStart, cursorPos - wordStart); if (wordPrefix.isEmpty()) return; QStringList list = toPlainText().split(QRegularExpression(QLatin1String("\\W+"))); QMap map; QStringList::Iterator it = list.begin(); while (it != list.end()) { if ((*it).startsWith(wordPrefix) && (*it).length() > wordPrefix.length()) map[(*it).toLower()] = *it; ++it; } if (map.count() == 1) { insertPlainText((*map.begin()).mid(wordPrefix.length())); } else if (map.count() > 1) { if (!listBox) createListBox(); listBox->clear(); listBox->addItems(map.values()); listBox->setFont(QFont(font().family(), 8)); this->cursorPosition = textCursor().position(); // get the minimum width and height of the box int h = 0; int w = 0; for (int i = 0; i < listBox->count(); ++i) { QRect r = listBox->visualItemRect(listBox->item(i)); w = qMax(w, r.width()); h += r.height(); } // Add an offset w += 2*listBox->frameWidth(); h += 2*listBox->frameWidth(); // get the start position of the word prefix QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::StartOfWord); QRect rect = cursorRect(cursor); int posX = rect.x(); int posY = rect.y(); int boxH = h; // Decide whether to show downstairs or upstairs if (posY > viewport()->height()/2) { h = qMin(qMin(h,posY), 250); if (h < boxH) w += style()->pixelMetric(QStyle::PM_ScrollBarExtent); listBox->setGeometry(posX,posY-h, w, h); } else { h = qMin(qMin(h,viewport()->height()-fontMetrics().height()-posY), 250); if (h < boxH) w += style()->pixelMetric(QStyle::PM_ScrollBarExtent); listBox->setGeometry(posX, posY+fontMetrics().height(), w, h); } listBox->setCurrentRow(0); listBox->show(); } } /** * Creates the listbox containing all possibilities for the completion. * The listbox is closed when ESC is pressed, the text edit field loses focus or a * mouse button was pressed. */ void TextEdit::createListBox() { listBox = new CompletionList(this); listBox->setFrameStyle(QFrame::Box); listBox->setFrameShadow(QFrame::Raised); listBox->setLineWidth(2); installEventFilter(listBox); viewport()->installEventFilter(listBox); listBox->setSelectionMode( QAbstractItemView::SingleSelection ); listBox->hide(); } // ------------------------------------------------------------------------------ namespace Gui { struct TextEditorP { bool highlightLine = true; bool visibleMarker = true; QMap colormap; // Color map TextEditorP() { colormap[QLatin1String("Text")] = qApp->palette().windowText().color(); colormap[QLatin1String("Bookmark")] = Qt::cyan; colormap[QLatin1String("Breakpoint")] = Qt::red; colormap[QLatin1String("Keyword")] = Qt::blue; colormap[QLatin1String("Comment")] = QColor(0, 170, 0); colormap[QLatin1String("Block comment")] = QColor(160, 160, 164); colormap[QLatin1String("Number")] = Qt::blue; colormap[QLatin1String("String")] = Qt::red; colormap[QLatin1String("Character")] = Qt::red; colormap[QLatin1String("Class name")] = QColor(255, 170, 0); colormap[QLatin1String("Define name")] = QColor(255, 170, 0); colormap[QLatin1String("Operator")] = QColor(160, 160, 164); colormap[QLatin1String("Python output")] = QColor(170, 170, 127); colormap[QLatin1String("Python error")] = Qt::red; colormap[QLatin1String("Current line highlight")] = QColor(224,224,224); } }; } // namespace Gui /** * Constructs a TextEditor which is a child of 'parent' and does the * syntax highlighting for the Python language. */ TextEditor::TextEditor(QWidget* parent) : TextEdit(parent), WindowParameter("Editor"), highlighter(nullptr) { d = new TextEditorP(); lineNumberArea = new LineMarker(this); QFont serifFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); serifFont.setPointSize(10); setFont(serifFont); ParameterGrp::handle hPrefGrp = getWindowParameter(); hPrefGrp->Attach( this ); // set colors and font hPrefGrp->NotifyAll(); connect(this, &QPlainTextEdit::cursorPositionChanged, this, &TextEditor::highlightCurrentLine); connect(this, &QPlainTextEdit::blockCountChanged, this, &TextEditor::updateLineNumberAreaWidth); connect(this, &QPlainTextEdit::updateRequest, this, &TextEditor::updateLineNumberArea); updateLineNumberAreaWidth(0); highlightCurrentLine(); } /** Destroys the object and frees any allocated resources */ TextEditor::~TextEditor() { getWindowParameter()->Detach(this); delete highlighter; delete d; } void TextEditor::setVisibleLineNumbers(bool value) { lineNumberArea->setVisible(value); d->visibleMarker = value; } bool TextEditor::isVisibleLineNumbers() const { return d->visibleMarker; } void TextEditor::setEnabledHighlightCurrentLine(bool value) { d->highlightLine = value; } bool TextEditor::isEnabledHighlightCurrentLine() const { return d->highlightLine; } int TextEditor::lineNumberAreaWidth() { return QtTools::horizontalAdvance(fontMetrics(), QLatin1String("0000")) + 10; } void TextEditor::updateLineNumberAreaWidth(int /* newBlockCount */) { int left = isVisibleLineNumbers() ? lineNumberAreaWidth() : 0; setViewportMargins(left, 0, 0, 0); } void TextEditor::updateLineNumberArea(const QRect &rect, int dy) { if (isVisibleLineNumbers()) { if (dy) { lineNumberArea->scroll(0, dy); } else { lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height()); } if (rect.contains(viewport()->rect())) { updateLineNumberAreaWidth(0); } } } void TextEditor::resizeEvent(QResizeEvent *e) { QPlainTextEdit::resizeEvent(e); if (isVisibleLineNumbers()) { QRect cr = contentsRect(); int width = lineNumberAreaWidth(); lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), width, cr.height())); } } void TextEditor::highlightCurrentLine() { QList extraSelections; if (!isReadOnly() && isEnabledHighlightCurrentLine()) { QTextEdit::ExtraSelection selection; QColor lineColor = d->colormap[QLatin1String("Current line highlight")]; unsigned int col = Base::Color::asPackedRGB(lineColor); ParameterGrp::handle hPrefGrp = getWindowParameter(); auto value = static_cast(col); value = hPrefGrp->GetUnsigned( "Current line highlight", value); col = static_cast(value); lineColor.setRgb((col>>24)&0xff, (col>>16)&0xff, (col>>8)&0xff); selection.format.setBackground(lineColor); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); extraSelections.append(selection); } setExtraSelections(extraSelections); } void TextEditor::drawMarker(int line, int x, int y, QPainter* p) { Q_UNUSED(line); Q_UNUSED(x); Q_UNUSED(y); Q_UNUSED(p); } void TextEditor::lineNumberAreaPaintEvent(QPaintEvent *event) { if (!isVisibleLineNumbers()) { return; } QPainter painter(lineNumberArea); //painter.fillRect(event->rect(), Qt::lightGray); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + (int) blockBoundingRect(block).height(); while (block.isValid() && top <= event->rect().bottom()) { if (block.isVisible() && bottom >= event->rect().top()) { QString number = QString::number(blockNumber + 1); QPalette pal = palette(); QColor color = pal.windowText().color(); painter.setPen(color); painter.drawText(0, top, lineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number); drawMarker(blockNumber + 1, 1, top, &painter); } block = block.next(); top = bottom; bottom = top + (int) blockBoundingRect(block).height(); ++blockNumber; } } void TextEditor::setSyntaxHighlighter(SyntaxHighlighter* sh) { sh->setDocument(this->document()); this->highlighter = sh; } /** Sets the font, font size and tab size of the editor. */ void TextEditor::OnChange(Base::Subject &rCaller,const char* sReason) { Q_UNUSED(rCaller); ParameterGrp::handle hPrefGrp = getWindowParameter(); if (strcmp(sReason, "FontSize") == 0 || strcmp(sReason, "Font") == 0) { #ifdef FC_OS_LINUX int fontSize = hPrefGrp->GetInt("FontSize", 15); #else int fontSize = hPrefGrp->GetInt("FontSize", 10); #endif QFont font; auto fontName = hPrefGrp->GetASCII("Font"); if (fontName.empty()) { font = QFontDatabase::systemFont(QFontDatabase::FixedFont); font.setPointSize(fontSize); } else { font = QFont (QString::fromStdString(fontName), fontSize); } setFont(font); lineNumberArea->setFont(font); } else { QMap::Iterator it = d->colormap.find(QString::fromLatin1(sReason)); if (it != d->colormap.end()) { QColor color = it.value(); unsigned int col = Base::Color::asPackedRGB(color); auto value = static_cast(col); value = hPrefGrp->GetUnsigned(sReason, value); col = static_cast(value); color.setRgb((col>>24)&0xff, (col>>16)&0xff, (col>>8)&0xff); if (this->highlighter) this->highlighter->setColor(QLatin1String(sReason), color); } } if (strcmp(sReason, "TabSize") == 0 || strcmp(sReason, "FontSize") == 0) { int tabWidth = hPrefGrp->GetInt("TabSize", 4); QFontMetrics metric(font()); int fontSize = QtTools::horizontalAdvance(metric, QLatin1Char('0')); setTabStopDistance(tabWidth * fontSize); } // Enables/Disables Line number in the Macro Editor from Edit->Preferences->Editor menu. if (strcmp(sReason, "EnableLineNumber") == 0) { int width = 0; QRect cr = contentsRect(); if (hPrefGrp->GetBool("EnableLineNumber", true)) { width = lineNumberAreaWidth(); } lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), width, cr.height())); } } // ------------------------------------------------------------------------------ PythonTextEditor::PythonTextEditor(QWidget *parent) : TextEditor(parent) { } PythonTextEditor::~PythonTextEditor() = default; void PythonTextEditor::prepend(const QString& str) { QTextCursor cursor = textCursor(); // for each selected block insert a tab or spaces int selStart = cursor.selectionStart(); int selEnd = cursor.selectionEnd(); QTextBlock block; cursor.beginEditBlock(); for (block = document()->begin(); block.isValid(); block = block.next()) { int pos = block.position(); int off = block.length()-1; // at least one char of the block is part of the selection if ( pos >= selStart || pos+off >= selStart) { if ( pos+1 > selEnd ) break; // end of selection reached cursor.setPosition(block.position()); cursor.insertText(str); selEnd += str.length(); } } cursor.endEditBlock(); } void PythonTextEditor::remove(const QString& str) { QTextCursor cursor = textCursor(); int selStart = cursor.selectionStart(); int selEnd = cursor.selectionEnd(); QTextBlock block; cursor.beginEditBlock(); for (block = document()->begin(); block.isValid(); block = block.next()) { int pos = block.position(); int off = block.length()-1; // at least one char of the block is part of the selection if ( pos >= selStart || pos+off >= selStart) { if ( pos+1 > selEnd ) break; // end of selection reached QString text = block.text(); if (text.startsWith(str)) { cursor.setPosition(block.position()); for (int i = 0; i < str.length(); i++) { cursor.deleteChar(); selEnd--; } } } } cursor.endEditBlock(); } void PythonTextEditor::keyPressEvent (QKeyEvent * e) { if ( e->key() == Qt::Key_Tab ) { ParameterGrp::handle hPrefGrp = getWindowParameter(); bool space = hPrefGrp->GetBool("Spaces", true); int indent = hPrefGrp->GetInt( "IndentSize", 4 ); QString ch = space ? QString(indent, QLatin1Char(' ')) : QStringLiteral("\t"); QTextCursor cursor = textCursor(); if (!cursor.hasSelection()) { // insert a single tab or several spaces cursor.beginEditBlock(); cursor.insertText(ch); cursor.endEditBlock(); } else { prepend(ch); } return; } else if (e->key() == Qt::Key_Backtab) { QTextCursor cursor = textCursor(); if (!cursor.hasSelection()) return; // Shift+Tab should not do anything // If some text is selected we remove a leading tab or // spaces from each selected block ParameterGrp::handle hPrefGrp = getWindowParameter(); bool space = hPrefGrp->GetBool("Spaces", true); int indent = hPrefGrp->GetInt( "IndentSize", 4 ); QString ch = space ? QString(indent, QLatin1Char(' ')) : QStringLiteral("\t"); // if possible remove one tab or several spaces remove(ch); return; } TextEditor::keyPressEvent( e ); } LineMarker::LineMarker(TextEditor* editor) : QWidget(editor), textEditor(editor) { } LineMarker::~LineMarker() = default; QSize LineMarker::sizeHint() const { return {textEditor->lineNumberAreaWidth(), 0}; } void LineMarker::paintEvent(QPaintEvent* e) { textEditor->lineNumberAreaPaintEvent(e); } // ------------------------------------------------------------------------------ CompletionList::CompletionList(QPlainTextEdit* parent) : QListWidget(parent), textEdit(parent) { // make the user assume that the widget is active QPalette pal = parent->palette(); pal.setColor(QPalette::Inactive, QPalette::Highlight, pal.color(QPalette::Active, QPalette::Highlight)); pal.setColor(QPalette::Inactive, QPalette::HighlightedText, pal.color(QPalette::Active, QPalette::HighlightedText)); parent->setPalette( pal ); connect(this, &CompletionList::itemActivated, this, &CompletionList::completionItem); } CompletionList::~CompletionList() = default; void CompletionList::findCurrentWord(const QString& wordPrefix) { for (int i=0; itext(); if (text.startsWith(wordPrefix)) { setCurrentRow(i); return; } } if (currentItem()) currentItem()->setSelected(false); } /** * Get all incoming events of the text edit and redirect some of them, like key up and * down, mouse press events, ... to the widget itself. */ bool CompletionList::eventFilter(QObject * watched, QEvent * event) { if (isVisible() && watched == textEdit->viewport()) { if (event->type() == QEvent::MouseButtonPress) hide(); } else if (isVisible() && watched == textEdit) { if (event->type() == QEvent::KeyPress) { auto ke = static_cast(event); if (ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down) { keyPressEvent(ke); return true; } else if (ke->key() == Qt::Key_PageUp || ke->key() == Qt::Key_PageDown) { keyPressEvent(ke); return true; } else if (ke->key() == Qt::Key_Escape) { hide(); return true; } else if (ke->key() == Qt::Key_Space) { hide(); return false; } else if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { Q_EMIT itemActivated(currentItem()); return true; } } else if (event->type() == QEvent::FocusOut) { if (!hasFocus()) hide(); } } return QListWidget::eventFilter(watched, event); } /** * If an item was chosen (either by clicking or pressing enter) the rest of the word is completed. * The listbox is closed without destroying it. */ void CompletionList::completionItem(QListWidgetItem *item) { hide(); QString text = item->text(); QTextCursor cursor = textEdit->textCursor(); cursor.movePosition(QTextCursor::StartOfWord); cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); cursor.insertText( text ); textEdit->ensureCursorVisible(); } #include "moc_TextEdit.cpp"