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"
25
# include <QContextMenuEvent>
29
# include <QTextCursor>
32
#include <Base/Parameter.h>
34
#include "PythonEditor.h"
35
#include "Application.h"
36
#include "BitmapFactory.h"
38
#include "PythonDebugger.h"
51
PythonDebugger* debugger;
53
: breakpoint(BitmapFactory().iconFromTheme("breakpoint").pixmap(16,16)),
54
debugMarker(BitmapFactory().iconFromTheme("debug-marker").pixmap(16,16))
56
debugger = Application::Instance->macroManager()->debugger();
61
/* TRANSLATOR Gui::PythonEditor */
64
* Constructs a PythonEditor which is a child of 'parent' and does the
65
* syntax highlighting for the Python language.
67
PythonEditor::PythonEditor(QWidget* parent)
70
d = new PythonEditorP();
71
this->setSyntaxHighlighter(new PythonSyntaxHighlighter(this));
74
auto comment = new QShortcut(this);
75
comment->setKey(QKeySequence(QString::fromLatin1("ALT+C")));
77
auto uncomment = new QShortcut(this);
78
uncomment->setKey(QKeySequence(QString::fromLatin1("ALT+U")));
80
connect(comment, &QShortcut::activated, this, &PythonEditor::onComment);
81
connect(uncomment, &QShortcut::activated, this, &PythonEditor::onUncomment);
84
/** Destroys the object and frees any allocated resources */
85
PythonEditor::~PythonEditor()
90
void PythonEditor::setFileName(const QString& fn)
95
void PythonEditor::startDebug()
97
if (d->debugger->start()) {
98
d->debugger->runFile(d->filename);
103
void PythonEditor::toggleBreakpoint()
105
QTextCursor cursor = textCursor();
106
int line = cursor.blockNumber() + 1;
107
d->debugger->toggleBreakpoint(line, d->filename);
108
getMarker()->update();
111
void PythonEditor::showDebugMarker(int line)
114
getMarker()->update();
115
QTextCursor cursor = textCursor();
116
cursor.movePosition(QTextCursor::StartOfBlock);
117
int cur = cursor.blockNumber() + 1;
119
for (int i=line; i<cur; i++)
120
cursor.movePosition(QTextCursor::Up);
122
else if (cur < line) {
123
for (int i=cur; i<line; i++)
124
cursor.movePosition(QTextCursor::Down);
126
setTextCursor(cursor);
129
void PythonEditor::hideDebugMarker()
132
getMarker()->update();
135
void PythonEditor::drawMarker(int line, int x, int y, QPainter* p)
137
Breakpoint bp = d->debugger->getBreakpoint(d->filename);
138
if (bp.checkLine(line)) {
139
p->drawPixmap(x, y, d->breakpoint);
141
if (d->debugLine == line) {
142
p->drawPixmap(x, y+2, d->debugMarker);
143
d->debugRect = QRect(x, y+2, d->debugMarker.width(), d->debugMarker.height());
147
void PythonEditor::contextMenuEvent ( QContextMenuEvent * e )
149
QMenu* menu = createStandardContextMenu();
151
menu->addSeparator();
152
QAction* comment = menu->addAction( tr("Comment"), this, &PythonEditor::onComment);
153
comment->setShortcut(QKeySequence(QString::fromLatin1("ALT+C")));
154
QAction* uncomment = menu->addAction( tr("Uncomment"), this, &PythonEditor::onUncomment);
155
uncomment->setShortcut(QKeySequence(QString::fromLatin1("ALT+U")));
158
menu->exec(e->globalPos());
162
void PythonEditor::keyPressEvent(QKeyEvent* e)
164
/** When the user presses enter the next line should match the current
165
* indentation unless the line ends in a colon, where the next line
166
* should have an additional indentation. Shift+Enter should dedent
167
* the next block 1 indentation from what it would have been, if possible.
169
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
170
bool shiftPressed = e->modifiers() & Qt::ShiftModifier;
171
ParameterGrp::handle hPrefGrp = getWindowParameter();
172
int indent = hPrefGrp->GetInt( "IndentSize", 4 );
173
bool space = hPrefGrp->GetBool( "Spaces", true );
174
QString ch = space ? QString::fromLatin1(" ")
175
: QString::fromLatin1("\t");
177
QTextCursor cursor = textCursor();
178
QString currentLineText = cursor.block().text();
179
bool endsWithColon = currentLineText.endsWith(QLatin1Char(':'));
180
int currentIndentation = 0;
181
//count spaces/tabs at start of current line
182
for (auto c : currentLineText) {
184
currentIndentation++;
189
cursor.insertBlock(); //new line
190
cursor.movePosition(QTextCursor::StartOfBlock); //carriage return
191
//Shift+Enter means dedent, but ensure we are not at column 0
192
if (shiftPressed && currentIndentation >= indent){
193
currentIndentation -= indent;
195
//insert appropriate number of spaces/tabs to match current indentation
196
cursor.insertText(QString(currentIndentation, ch[0]));
197
//if the line ended in a colon, then we need to add another tab or multiple spaces
200
cursor.insertText(QString(indent, ch[0])); //4 more spaces by default
202
cursor.insertText(ch); //1 more tab
205
setTextCursor(cursor);
206
return; //skip default handler
208
TextEditor::keyPressEvent(e); //wasn't enter key, so let base class handle it
211
void PythonEditor::onComment()
213
QTextCursor cursor = textCursor();
214
int selStart = cursor.selectionStart();
215
int selEnd = cursor.selectionEnd();
217
cursor.beginEditBlock();
218
for (block = document()->begin(); block.isValid(); block = block.next()) {
219
int pos = block.position();
220
int off = block.length()-1;
221
// at least one char of the block is part of the selection
222
if ( pos >= selStart || pos+off >= selStart) {
223
if ( pos+1 > selEnd )
224
break; // end of selection reached
225
cursor.setPosition(block.position());
226
cursor.insertText(QLatin1String("#"));
231
cursor.endEditBlock();
234
void PythonEditor::onUncomment()
236
QTextCursor cursor = textCursor();
237
int selStart = cursor.selectionStart();
238
int selEnd = cursor.selectionEnd();
240
cursor.beginEditBlock();
241
for (block = document()->begin(); block.isValid(); block = block.next()) {
242
int pos = block.position();
243
int off = block.length()-1;
244
// at least one char of the block is part of the selection
245
if ( pos >= selStart || pos+off >= selStart) {
246
if ( pos+1 > selEnd )
247
break; // end of selection reached
248
if (block.text().startsWith(QLatin1String("#"))) {
249
cursor.setPosition(block.position());
256
cursor.endEditBlock();
259
// ------------------------------------------------------------------------
262
class PythonSyntaxHighlighterP
265
PythonSyntaxHighlighterP()
267
keywords << QLatin1String("and") << QLatin1String("as")
268
<< QLatin1String("assert") << QLatin1String("break")
269
<< QLatin1String("class") << QLatin1String("continue")
270
<< QLatin1String("def") << QLatin1String("del")
271
<< QLatin1String("elif") << QLatin1String("else")
272
<< QLatin1String("except") << QLatin1String("exec")
273
<< QLatin1String("False") << QLatin1String("finally")
274
<< QLatin1String("for") << QLatin1String("from")
275
<< QLatin1String("global") << QLatin1String("if")
276
<< QLatin1String("import") << QLatin1String("in")
277
<< QLatin1String("is") << QLatin1String("lambda")
278
<< QLatin1String("None") << QLatin1String("nonlocal")
279
<< QLatin1String("not") << QLatin1String("or")
280
<< QLatin1String("pass") << QLatin1String("print")
281
<< QLatin1String("raise") << QLatin1String("return")
282
<< QLatin1String("True") << QLatin1String("try")
283
<< QLatin1String("while") << QLatin1String("with")
284
<< QLatin1String("yield");
287
QStringList keywords;
292
* Constructs a Python syntax highlighter.
294
PythonSyntaxHighlighter::PythonSyntaxHighlighter(QObject* parent)
295
: SyntaxHighlighter(parent)
297
d = new PythonSyntaxHighlighterP;
300
/** Destroys this object. */
301
PythonSyntaxHighlighter::~PythonSyntaxHighlighter()
307
* Detects all kinds of text to highlight them in the correct color.
309
void PythonSyntaxHighlighter::highlightBlock (const QString & text)
314
const int Standard = 0; // Standard text
315
const int Digit = 1; // Digits
316
const int Comment = 2; // Comment begins with #
317
const int Literal1 = 3; // String literal beginning with "
318
const int Literal2 = 4; // Other string literal beginning with '
319
const int Blockcomment1 = 5; // Block comments beginning and ending with """
320
const int Blockcomment2 = 6; // Other block comments beginning and ending with '''
321
const int ClassName = 7; // Text after the keyword class
322
const int DefineName = 8; // Text after the keyword def
324
int endStateOfLastPara = previousBlockState();
325
if (endStateOfLastPara < 0 || endStateOfLastPara > maximumUserState())
326
endStateOfLastPara = Standard;
328
while ( i < text.length() )
332
switch ( endStateOfLastPara )
336
switch ( ch.unicode() )
341
setFormat( i, 1, this->colorByType(SyntaxHighlighter::Comment));
342
endStateOfLastPara=Comment;
346
// Begin either string literal or block comment
347
if ((i>=2) && text.at(i-1) == QLatin1Char('"') &&
348
text.at(i-2) == QLatin1Char('"'))
350
setFormat( i-2, 3, this->colorByType(SyntaxHighlighter::BlockComment));
351
endStateOfLastPara=Blockcomment1;
355
setFormat( i, 1, this->colorByType(SyntaxHighlighter::String));
356
endStateOfLastPara=Literal1;
361
// Begin either string literal or block comment
362
if ((i>=2) && text.at(i-1) == QLatin1Char('\'') &&
363
text.at(i-2) == QLatin1Char('\''))
365
setFormat( i-2, 3, this->colorByType(SyntaxHighlighter::BlockComment));
366
endStateOfLastPara=Blockcomment2;
370
setFormat( i, 1, this->colorByType(SyntaxHighlighter::String));
371
endStateOfLastPara=Literal2;
377
// ignore whitespaces
379
case '(': case ')': case '[': case ']':
380
case '+': case '-': case '*': case '/':
381
case ':': case '%': case '^': case '~':
382
case '!': case '=': case '<': case '>': // possibly two characters
384
setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator));
385
endStateOfLastPara=Standard;
389
// Check for normal text
390
if ( ch.isLetter() || ch == QLatin1Char('_') )
394
while ( ch.isLetterOrNumber() || ch == QLatin1Char('_') ) {
397
if (j >= text.length())
398
break; // end of text
402
if ( d->keywords.contains( buffer ) != 0 ) {
403
if ( buffer == QLatin1String("def"))
404
endStateOfLastPara = DefineName;
405
else if ( buffer == QLatin1String("class"))
406
endStateOfLastPara = ClassName;
408
QTextCharFormat keywordFormat;
409
keywordFormat.setForeground(this->colorByType(SyntaxHighlighter::Keyword));
410
keywordFormat.setFontWeight(QFont::Bold);
411
setFormat( i, buffer.length(), keywordFormat);
414
setFormat( i, buffer.length(),this->colorByType(SyntaxHighlighter::Text));
418
if ( !buffer.isEmpty() )
421
// this is the beginning of a number
422
else if ( ch.isDigit() )
424
setFormat(i, 1, this->colorByType(SyntaxHighlighter::Number));
425
endStateOfLastPara=Digit;
427
// probably an operator
428
else if ( ch.isSymbol() || ch.isPunct() )
430
setFormat( i, 1, this->colorByType(SyntaxHighlighter::Operator));
437
setFormat( i, 1, this->colorByType(SyntaxHighlighter::Comment));
441
setFormat( i, 1, this->colorByType(SyntaxHighlighter::String));
442
if ( ch == QLatin1Char('"') )
443
endStateOfLastPara = Standard;
447
setFormat( i, 1, this->colorByType(SyntaxHighlighter::String));
448
if ( ch == QLatin1Char('\'') )
449
endStateOfLastPara = Standard;
453
setFormat( i, 1, this->colorByType(SyntaxHighlighter::BlockComment));
454
if ( i>=2 && ch == QLatin1Char('"') &&
455
text.at(i-1) == QLatin1Char('"') &&
456
text.at(i-2) == QLatin1Char('"'))
457
endStateOfLastPara = Standard;
461
setFormat( i, 1, this->colorByType(SyntaxHighlighter::BlockComment));
462
if ( i>=2 && ch == QLatin1Char('\'') &&
463
text.at(i-1) == QLatin1Char('\'') &&
464
text.at(i-2) == QLatin1Char('\''))
465
endStateOfLastPara = Standard;
469
if ( ch.isLetterOrNumber() || ch == QLatin1Char(' ') || ch == QLatin1Char('_') )
471
setFormat( i, 1, this->colorByType(SyntaxHighlighter::Defname));
475
if ( ch.isSymbol() || ch.isPunct() )
476
setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator));
477
endStateOfLastPara = Standard;
482
if ( ch.isLetterOrNumber() || ch == QLatin1Char(' ') || ch == QLatin1Char('_') )
484
setFormat( i, 1, this->colorByType(SyntaxHighlighter::Classname));
488
if (ch.isSymbol() || ch.isPunct() )
489
setFormat( i, 1, this->colorByType(SyntaxHighlighter::Operator));
490
endStateOfLastPara = Standard;
495
if (ch.isDigit() || ch == QLatin1Char('.'))
497
setFormat( i, 1, this->colorByType(SyntaxHighlighter::Number));
501
if ( ch.isSymbol() || ch.isPunct() )
502
setFormat( i, 1, this->colorByType(SyntaxHighlighter::Operator));
503
endStateOfLastPara = Standard;
512
// only block comments can have several lines
513
if ( endStateOfLastPara != Blockcomment1 && endStateOfLastPara != Blockcomment2 )
515
endStateOfLastPara = Standard ;
518
setCurrentBlockState(endStateOfLastPara);
521
#include "moc_PythonEditor.cpp"