Files
create/src/Gui/TextEdit.cpp
Markus Reitböck 6ef07bb358 Gui: use CMake to generate precompiled headers on all platforms
"Professional CMake" book suggest the following:

"Targets should build successfully with or without compiler support for precompiled headers. It
should be considered an optimization, not a requirement. In particular, do not explicitly include a
precompile header (e.g. stdafx.h) in the source code, let CMake force-include an automatically
generated precompile header on the compiler command line instead. This is more portable across
the major compilers and is likely to be easier to maintain. It will also avoid warnings being
generated from certain code checking tools like iwyu (include what you use)."

Therefore, removed the "#include <PreCompiled.h>" from sources, also
there is no need for the "#ifdef _PreComp_" anymore
2025-09-14 09:47:03 +02:00

694 lines
24 KiB
C++

/***************************************************************************
* Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net> *
* *
* 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 <QApplication>
# include <QKeyEvent>
# include <QPainter>
# include <QRegularExpression>
# include <QRegularExpressionMatch>
# include <QShortcut>
# include <QTextCursor>
#include <FCConfig.h>
#include "CallTips.h"
#include "TextEdit.h"
#include "SyntaxHighlighter.h"
#include "Tools.h"
#include <Base/Color.h>
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<QString, QString> 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<QString, QColor> 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<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly() && isEnabledHighlightCurrentLine()) {
QTextEdit::ExtraSelection selection;
QColor lineColor = d->colormap[QLatin1String("Current line highlight")];
unsigned int col = Base::Color::asPackedRGB<QColor>(lineColor);
ParameterGrp::handle hPrefGrp = getWindowParameter();
auto value = static_cast<unsigned long>(col);
value = hPrefGrp->GetUnsigned( "Current line highlight", value);
col = static_cast<unsigned int>(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<const char*> &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 = QFontDatabase::systemFont(QFontDatabase::FixedFont);
font.setPointSize(fontSize);
setFont(font);
lineNumberArea->setFont(font);
}
else {
QMap<QString, QColor>::Iterator it = d->colormap.find(QString::fromLatin1(sReason));
if (it != d->colormap.end()) {
QColor color = it.value();
unsigned int col = Base::Color::asPackedRGB<QColor>(color);
auto value = static_cast<unsigned long>(col);
value = hPrefGrp->GetUnsigned(sReason, value);
col = static_cast<unsigned int>(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; i<count(); ++i) {
QString text = item(i)->text();
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<QKeyEvent*>(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"