1
/***************************************************************************
2
* Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net> *
4
* This file is part of the FreeCAD CAx development system. *
6
* This library is free software; you can redistribute it and/or *
7
* modify it under the terms of the GNU Library General Public *
8
* License as published by the Free Software Foundation; either *
9
* version 2 of the License, or (at your option) any later version. *
11
* This library is distributed in the hope that it will be useful, *
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
* GNU Library General Public License for more details. *
16
* You should have received a copy of the GNU Library General Public *
17
* License along with this library; see the file COPYING.LIB. If not, *
18
* write to the Free Software Foundation, Inc., 59 Temple Place, *
19
* Suite 330, Boston, MA 02111-1307, USA *
21
***************************************************************************/
23
#include "PreCompiled.h"
26
# include <QApplication>
29
# include <QRegularExpression>
30
# include <QRegularExpressionMatch>
32
# include <QTextCursor>
36
#include "SyntaxHighlighter.h"
44
* Constructs a TextEdit which is a child of 'parent'.
46
TextEdit::TextEdit(QWidget* parent)
47
: QPlainTextEdit(parent), cursorPosition(0), listBox(nullptr)
49
//Note: Set the correct context to this shortcut as we may use several instances of this
51
auto shortcut = new QShortcut(this);
52
shortcut->setKey(QKeySequence(QString::fromLatin1("CTRL+Space")));
53
shortcut->setContext(Qt::WidgetShortcut);
54
connect(shortcut, &QShortcut::activated, this, &TextEdit::complete);
56
auto shortcutFind = new QShortcut(this);
57
shortcutFind->setKey(QKeySequence::Find);
58
shortcutFind->setContext(Qt::WidgetShortcut);
59
connect(shortcutFind, &QShortcut::activated, this, &TextEdit::showSearchBar);
61
auto shortcutNext = new QShortcut(this);
62
shortcutNext->setKey(QKeySequence::FindNext);
63
shortcutNext->setContext(Qt::WidgetShortcut);
64
connect(shortcutNext, &QShortcut::activated, this, &TextEdit::findNext);
66
auto shortcutPrev = new QShortcut(this);
67
shortcutPrev->setKey(QKeySequence::FindPrevious);
68
shortcutPrev->setContext(Qt::WidgetShortcut);
69
connect(shortcutPrev, &QShortcut::activated, this, &TextEdit::findPrevious);
72
/** Destroys the object and frees any allocated resources */
73
TextEdit::~TextEdit() = default;
76
* Set the approproriate item of the completion box or hide it, if needed.
78
void TextEdit::keyPressEvent(QKeyEvent* e)
80
QPlainTextEdit::keyPressEvent(e);
81
// This can't be done in CompletionList::eventFilter() because we must first perform
82
// the event and afterwards update the list widget
83
if (listBox && listBox->isVisible()) {
84
// Get the word under the cursor
85
QTextCursor cursor = textCursor();
86
cursor.movePosition(QTextCursor::StartOfWord);
87
// the cursor has moved to outside the word prefix
88
if (cursor.position() < cursorPosition-wordPrefix.length() ||
89
cursor.position() > cursorPosition) {
93
cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
94
listBox->keyboardSearch(cursor.selectedText());
95
cursor.clearSelection();
100
* Completes the word.
102
void TextEdit::complete()
104
QTextBlock block = textCursor().block();
105
if (!block.isValid())
107
int cursorPos = textCursor().position()-block.position();
108
QString para = block.text();
109
int wordStart = cursorPos;
110
while (wordStart > 0 && para[wordStart - 1].isLetterOrNumber())
112
wordPrefix = para.mid(wordStart, cursorPos - wordStart);
113
if (wordPrefix.isEmpty())
116
QStringList list = toPlainText().split(QRegularExpression(QLatin1String("\\W+")));
117
QMap<QString, QString> map;
118
QStringList::Iterator it = list.begin();
119
while (it != list.end()) {
120
if ((*it).startsWith(wordPrefix) && (*it).length() > wordPrefix.length())
121
map[(*it).toLower()] = *it;
125
if (map.count() == 1) {
126
insertPlainText((*map.begin()).mid(wordPrefix.length()));
127
} else if (map.count() > 1) {
131
listBox->addItems(map.values());
132
listBox->setFont(QFont(font().family(), 8));
134
this->cursorPosition = textCursor().position();
136
// get the minimum width and height of the box
139
for (int i = 0; i < listBox->count(); ++i) {
140
QRect r = listBox->visualItemRect(listBox->item(i));
141
w = qMax(w, r.width());
146
w += 2*listBox->frameWidth();
147
h += 2*listBox->frameWidth();
149
// get the start position of the word prefix
150
QTextCursor cursor = textCursor();
151
cursor.movePosition(QTextCursor::StartOfWord);
152
QRect rect = cursorRect(cursor);
157
// Decide whether to show downstairs or upstairs
158
if (posY > viewport()->height()/2) {
159
h = qMin(qMin(h,posY), 250);
161
w += style()->pixelMetric(QStyle::PM_ScrollBarExtent);
162
listBox->setGeometry(posX,posY-h, w, h);
164
h = qMin(qMin(h,viewport()->height()-fontMetrics().height()-posY), 250);
166
w += style()->pixelMetric(QStyle::PM_ScrollBarExtent);
167
listBox->setGeometry(posX, posY+fontMetrics().height(), w, h);
170
listBox->setCurrentRow(0);
176
* Creates the listbox containing all possibilities for the completion.
177
* The listbox is closed when ESC is pressed, the text edit field loses focus or a
178
* mouse button was pressed.
180
void TextEdit::createListBox()
182
listBox = new CompletionList(this);
183
listBox->setFrameStyle(QFrame::Box);
184
listBox->setFrameShadow(QFrame::Raised);
185
listBox->setLineWidth(2);
186
installEventFilter(listBox);
187
viewport()->installEventFilter(listBox);
188
listBox->setSelectionMode( QAbstractItemView::SingleSelection );
192
// ------------------------------------------------------------------------------
197
QMap<QString, QColor> colormap; // Color map
200
colormap[QLatin1String("Text")] = qApp->palette().windowText().color();
201
colormap[QLatin1String("Bookmark")] = Qt::cyan;
202
colormap[QLatin1String("Breakpoint")] = Qt::red;
203
colormap[QLatin1String("Keyword")] = Qt::blue;
204
colormap[QLatin1String("Comment")] = QColor(0, 170, 0);
205
colormap[QLatin1String("Block comment")] = QColor(160, 160, 164);
206
colormap[QLatin1String("Number")] = Qt::blue;
207
colormap[QLatin1String("String")] = Qt::red;
208
colormap[QLatin1String("Character")] = Qt::red;
209
colormap[QLatin1String("Class name")] = QColor(255, 170, 0);
210
colormap[QLatin1String("Define name")] = QColor(255, 170, 0);
211
colormap[QLatin1String("Operator")] = QColor(160, 160, 164);
212
colormap[QLatin1String("Python output")] = QColor(170, 170, 127);
213
colormap[QLatin1String("Python error")] = Qt::red;
214
colormap[QLatin1String("Current line highlight")] = QColor(224,224,224);
220
* Constructs a TextEditor which is a child of 'parent' and does the
221
* syntax highlighting for the Python language.
223
TextEditor::TextEditor(QWidget* parent)
224
: TextEdit(parent), WindowParameter("Editor"), highlighter(nullptr)
226
d = new TextEditorP();
227
lineNumberArea = new LineMarker(this);
229
QFont serifFont(QLatin1String("Courier"), 10, QFont::Normal);
232
ParameterGrp::handle hPrefGrp = getWindowParameter();
233
hPrefGrp->Attach( this );
235
// set colors and font
236
hPrefGrp->NotifyAll();
238
connect(this, &QPlainTextEdit::cursorPositionChanged,
239
this, &TextEditor::highlightCurrentLine);
240
connect(this, &QPlainTextEdit::blockCountChanged,
241
this, &TextEditor::updateLineNumberAreaWidth);
242
connect(this, &QPlainTextEdit::updateRequest,
243
this, &TextEditor::updateLineNumberArea);
245
updateLineNumberAreaWidth(0);
246
highlightCurrentLine();
249
/** Destroys the object and frees any allocated resources */
250
TextEditor::~TextEditor()
252
getWindowParameter()->Detach(this);
257
int TextEditor::lineNumberAreaWidth()
259
return QtTools::horizontalAdvance(fontMetrics(), QLatin1String("0000")) + 10;
262
void TextEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
264
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
267
void TextEditor::updateLineNumberArea(const QRect &rect, int dy)
270
lineNumberArea->scroll(0, dy);
272
lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
274
if (rect.contains(viewport()->rect()))
275
updateLineNumberAreaWidth(0);
278
void TextEditor::resizeEvent(QResizeEvent *e)
280
QPlainTextEdit::resizeEvent(e);
282
QRect cr = contentsRect();
283
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
286
void TextEditor::highlightCurrentLine()
288
QList<QTextEdit::ExtraSelection> extraSelections;
291
QTextEdit::ExtraSelection selection;
292
QColor lineColor = d->colormap[QLatin1String("Current line highlight")];
293
unsigned int col = App::Color::asPackedRGB<QColor>(lineColor);
294
ParameterGrp::handle hPrefGrp = getWindowParameter();
295
auto value = static_cast<unsigned long>(col);
296
value = hPrefGrp->GetUnsigned( "Current line highlight", value);
297
col = static_cast<unsigned int>(value);
298
lineColor.setRgb((col>>24)&0xff, (col>>16)&0xff, (col>>8)&0xff);
299
selection.format.setBackground(lineColor);
300
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
301
selection.cursor = textCursor();
302
selection.cursor.clearSelection();
303
extraSelections.append(selection);
306
setExtraSelections(extraSelections);
309
void TextEditor::drawMarker(int line, int x, int y, QPainter* p)
317
void TextEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
319
QPainter painter(lineNumberArea);
320
//painter.fillRect(event->rect(), Qt::lightGray);
322
QTextBlock block = firstVisibleBlock();
323
int blockNumber = block.blockNumber();
324
int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
325
int bottom = top + (int) blockBoundingRect(block).height();
327
while (block.isValid() && top <= event->rect().bottom()) {
328
if (block.isVisible() && bottom >= event->rect().top()) {
329
QString number = QString::number(blockNumber + 1);
330
QPalette pal = palette();
331
QColor color = pal.windowText().color();
332
painter.setPen(color);
333
painter.drawText(0, top, lineNumberArea->width(), fontMetrics().height(),
334
Qt::AlignRight, number);
335
drawMarker(blockNumber + 1, 1, top, &painter);
338
block = block.next();
340
bottom = top + (int) blockBoundingRect(block).height();
345
void TextEditor::setSyntaxHighlighter(SyntaxHighlighter* sh)
347
sh->setDocument(this->document());
348
this->highlighter = sh;
351
void TextEditor::keyPressEvent (QKeyEvent * e)
353
if ( e->key() == Qt::Key_Tab ) {
354
ParameterGrp::handle hPrefGrp = getWindowParameter();
355
int indent = hPrefGrp->GetInt( "IndentSize", 4 );
356
bool space = hPrefGrp->GetBool( "Spaces", false );
357
QString ch = space ? QString(indent, QLatin1Char(' '))
358
: QString::fromLatin1("\t");
360
QTextCursor cursor = textCursor();
361
if (!cursor.hasSelection()) {
362
// insert a single tab or several spaces
363
cursor.beginEditBlock();
364
cursor.insertText(ch);
365
cursor.endEditBlock();
367
// for each selected block insert a tab or spaces
368
int selStart = cursor.selectionStart();
369
int selEnd = cursor.selectionEnd();
371
cursor.beginEditBlock();
372
for (block = document()->begin(); block.isValid(); block = block.next()) {
373
int pos = block.position();
374
int off = block.length()-1;
375
// at least one char of the block is part of the selection
376
if ( pos >= selStart || pos+off >= selStart) {
377
if ( pos+1 > selEnd )
378
break; // end of selection reached
379
cursor.setPosition(block.position());
380
cursor.insertText(ch);
381
selEnd += ch.length();
385
cursor.endEditBlock();
390
else if (e->key() == Qt::Key_Backtab) {
391
QTextCursor cursor = textCursor();
392
if (!cursor.hasSelection())
393
return; // Shift+Tab should not do anything
394
// If some text is selected we remove a leading tab or
395
// spaces from each selected block
396
ParameterGrp::handle hPrefGrp = getWindowParameter();
397
int indent = hPrefGrp->GetInt( "IndentSize", 4 );
399
int selStart = cursor.selectionStart();
400
int selEnd = cursor.selectionEnd();
402
cursor.beginEditBlock();
403
for (block = document()->begin(); block.isValid(); block = block.next()) {
404
int pos = block.position();
405
int off = block.length()-1;
406
// at least one char of the block is part of the selection
407
if ( pos >= selStart || pos+off >= selStart) {
408
if ( pos+1 > selEnd )
409
break; // end of selection reached
410
// if possible remove one tab or several spaces
411
QString text = block.text();
412
if (text.startsWith(QLatin1String("\t"))) {
413
cursor.setPosition(block.position());
418
cursor.setPosition(block.position());
419
for (int i=0; i<indent; i++) {
420
if (!text.startsWith(QLatin1String(" ")))
430
cursor.endEditBlock();
434
TextEdit::keyPressEvent( e );
437
/** Sets the font, font size and tab size of the editor. */
438
void TextEditor::OnChange(Base::Subject<const char*> &rCaller,const char* sReason)
441
ParameterGrp::handle hPrefGrp = getWindowParameter();
442
if (strcmp(sReason, "FontSize") == 0 || strcmp(sReason, "Font") == 0) {
444
int fontSize = hPrefGrp->GetInt("FontSize", 15);
446
int fontSize = hPrefGrp->GetInt("FontSize", 10);
448
QString fontFamily = QString::fromLatin1(hPrefGrp->GetASCII( "Font", "Courier" ).c_str());
450
QFont font(fontFamily, fontSize);
452
lineNumberArea->setFont(font);
455
QMap<QString, QColor>::Iterator it = d->colormap.find(QString::fromLatin1(sReason));
456
if (it != d->colormap.end()) {
457
QColor color = it.value();
458
unsigned int col = App::Color::asPackedRGB<QColor>(color);
459
auto value = static_cast<unsigned long>(col);
460
value = hPrefGrp->GetUnsigned(sReason, value);
461
col = static_cast<unsigned int>(value);
462
color.setRgb((col>>24)&0xff, (col>>16)&0xff, (col>>8)&0xff);
463
if (this->highlighter)
464
this->highlighter->setColor(QLatin1String(sReason), color);
468
if (strcmp(sReason, "TabSize") == 0 || strcmp(sReason, "FontSize") == 0) {
469
int tabWidth = hPrefGrp->GetInt("TabSize", 4);
470
QFontMetrics metric(font());
471
int fontSize = QtTools::horizontalAdvance(metric, QLatin1Char('0'));
472
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
473
setTabStopWidth(tabWidth * fontSize);
475
setTabStopDistance(tabWidth * fontSize);
479
// Enables/Disables Line number in the Macro Editor from Edit->Preferences->Editor menu.
480
if (strcmp(sReason, "EnableLineNumber") == 0) {
481
QRect cr = contentsRect();
482
bool show = hPrefGrp->GetBool("EnableLineNumber", true);
484
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
486
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), 0, cr.height()));
489
if (strcmp(sReason, "EnableBlockCursor") == 0 ||
490
strcmp(sReason, "FontSize") == 0 ||
491
strcmp(sReason, "Font") == 0) {
492
bool block = hPrefGrp->GetBool("EnableBlockCursor", false);
494
setCursorWidth(QFontMetrics(font()).averageCharWidth());
500
void TextEditor::paintEvent (QPaintEvent * e)
502
TextEdit::paintEvent( e );
505
// ------------------------------------------------------------------------------
507
LineMarker::LineMarker(TextEditor* editor)
508
: QWidget(editor), textEditor(editor)
512
LineMarker::~LineMarker() = default;
514
QSize LineMarker::sizeHint() const
516
return {textEditor->lineNumberAreaWidth(), 0};
519
void LineMarker::paintEvent(QPaintEvent* e)
521
textEditor->lineNumberAreaPaintEvent(e);
524
// ------------------------------------------------------------------------------
526
CompletionList::CompletionList(QPlainTextEdit* parent)
527
: QListWidget(parent), textEdit(parent)
529
// make the user assume that the widget is active
530
QPalette pal = parent->palette();
531
pal.setColor(QPalette::Inactive, QPalette::Highlight, pal.color(QPalette::Active, QPalette::Highlight));
532
pal.setColor(QPalette::Inactive, QPalette::HighlightedText, pal.color(QPalette::Active, QPalette::HighlightedText));
533
parent->setPalette( pal );
535
connect(this, &CompletionList::itemActivated,
536
this, &CompletionList::completionItem);
539
CompletionList::~CompletionList() = default;
541
void CompletionList::findCurrentWord(const QString& wordPrefix)
543
for (int i=0; i<count(); ++i) {
544
QString text = item(i)->text();
545
if (text.startsWith(wordPrefix)) {
552
currentItem()->setSelected(false);
556
* Get all incoming events of the text edit and redirect some of them, like key up and
557
* down, mouse press events, ... to the widget itself.
559
bool CompletionList::eventFilter(QObject * watched, QEvent * event)
561
if (isVisible() && watched == textEdit->viewport()) {
562
if (event->type() == QEvent::MouseButtonPress)
564
} else if (isVisible() && watched == textEdit) {
565
if (event->type() == QEvent::KeyPress) {
566
auto ke = static_cast<QKeyEvent*>(event);
567
if (ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down) {
570
} else if (ke->key() == Qt::Key_PageUp || ke->key() == Qt::Key_PageDown) {
573
} else if (ke->key() == Qt::Key_Escape) {
576
} else if (ke->key() == Qt::Key_Space) {
579
} else if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
580
Q_EMIT itemActivated(currentItem());
583
} else if (event->type() == QEvent::FocusOut) {
589
return QListWidget::eventFilter(watched, event);
593
* If an item was chosen (either by clicking or pressing enter) the rest of the word is completed.
594
* The listbox is closed without destroying it.
596
void CompletionList::completionItem(QListWidgetItem *item)
599
QString text = item->text();
600
QTextCursor cursor = textEdit->textCursor();
601
cursor.movePosition(QTextCursor::StartOfWord);
602
cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
603
cursor.insertText( text );
604
textEdit->ensureCursorVisible();
607
#include "moc_TextEdit.cpp"