QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
qgscodeeditorpython.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscodeeditorpython.cpp - A Python editor based on QScintilla
3 --------------------------------------
4 Date : 06-Oct-2013
5 Copyright : (C) 2013 by Salvatore Larosa
6 Email : lrssvtml (at) gmail (dot) com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgsapplication.h"
17#include "qgscodeeditorpython.h"
18#include "qgslogger.h"
19#include "qgssymbollayerutils.h"
20#include "qgssettings.h"
21#include "qgis.h"
22
23#include <QWidget>
24#include <QString>
25#include <QFont>
26#include <QUrl>
27#include <QFileInfo>
28#include <QMessageBox>
29#include <QTextStream>
30#include <Qsci/qscilexerpython.h>
31#include <QDesktopServices>
32#include <QKeyEvent>
33
34const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs
35{
36 {"(", ")"},
37 {"[", "]"},
38 {"{", "}"},
39 {"'", "'"},
40 {"\"", "\""}
41};
42const QStringList QgsCodeEditorPython::sCompletionSingleCharacters{"`", "*"};
43
44QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList<QString> &filenames, Mode mode )
45 : QgsCodeEditor( parent,
46 QString(),
47 false,
48 false,
49 QgsCodeEditor::Flag::CodeFolding, mode )
50 , mAPISFilesList( filenames )
51{
52 if ( !parent )
53 {
54 setTitle( tr( "Python Editor" ) );
55 }
56
57 setCaretWidth( 2 );
58
60}
61
63{
65}
66
68{
69 // current line
70 setEdgeMode( QsciScintilla::EdgeLine );
71 setEdgeColumn( 80 );
73
74 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
75
76 QFont font = lexerFont();
78
79 QsciLexerPython *pyLexer = new QgsQsciLexerPython( this );
80
81 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
82 pyLexer->setFoldComments( true );
83 pyLexer->setFoldQuotes( true );
84
85 pyLexer->setDefaultFont( font );
86 pyLexer->setDefaultColor( defaultColor );
88 pyLexer->setFont( font, -1 );
89
90 font.setItalic( true );
91 pyLexer->setFont( font, QsciLexerPython::Comment );
92 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
93
94 font.setItalic( false );
95 font.setBold( true );
96 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
97 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
98
99 pyLexer->setColor( defaultColor, QsciLexerPython::Default );
100 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Class ), QsciLexerPython::ClassName );
101 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Method ), QsciLexerPython::FunctionMethodName );
102 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Number ), QsciLexerPython::Number );
103 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Operator ), QsciLexerPython::Operator );
104 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Identifier ), QsciLexerPython::Identifier );
105 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Comment ), QsciLexerPython::Comment );
106 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CommentBlock ), QsciLexerPython::CommentBlock );
107 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Keyword ), QsciLexerPython::Keyword );
108 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Decoration ), QsciLexerPython::Decorator );
109 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedString );
110 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedString );
111 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote ), QsciLexerPython::TripleSingleQuotedString );
112 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote ), QsciLexerPython::TripleDoubleQuotedString );
113
114 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
115
116 const QgsSettings settings;
117
118 if ( mAPISFilesList.isEmpty() )
119 {
120 if ( settings.value( QStringLiteral( "pythonConsole/preloadAPI" ), true ).toBool() )
121 {
122 mPapFile = QgsApplication::pkgDataPath() + QStringLiteral( "/python/qsci_apis/PyQGIS.pap" );
123 apis->loadPrepared( mPapFile );
124 }
125 else if ( settings.value( QStringLiteral( "pythonConsole/usePreparedAPIFile" ), false ).toBool() )
126 {
127 apis->loadPrepared( settings.value( QStringLiteral( "pythonConsole/preparedAPIFile" ) ).toString() );
128 }
129 else
130 {
131 const QStringList apiPaths = settings.value( QStringLiteral( "pythonConsole/userAPI" ) ).toStringList();
132 for ( const QString &path : apiPaths )
133 {
134 if ( !QFileInfo::exists( path ) )
135 {
136 QgsDebugMsg( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
137 }
138 else
139 {
140 apis->load( path );
141 }
142 }
143 apis->prepare();
144 }
145 }
146 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String( "pap" ) )
147 {
148 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
149 {
150 QgsDebugMsg( QStringLiteral( "The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
151 return;
152 }
153 mPapFile = mAPISFilesList[0];
154 apis->loadPrepared( mPapFile );
155 }
156 else
157 {
158 for ( const QString &path : std::as_const( mAPISFilesList ) )
159 {
160 if ( !QFileInfo::exists( path ) )
161 {
162 QgsDebugMsg( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
163 }
164 else
165 {
166 apis->load( path );
167 }
168 }
169 apis->prepare();
170 }
171 if ( apis )
172 pyLexer->setAPIs( apis.release() );
173
174 setLexer( pyLexer );
175
176 const int threshold = settings.value( QStringLiteral( "pythonConsole/autoCompThreshold" ), 2 ).toInt();
177 setAutoCompletionThreshold( threshold );
178 if ( !settings.value( "pythonConsole/autoCompleteEnabled", true ).toBool() )
179 {
180 setAutoCompletionSource( AcsNone );
181 }
182 else
183 {
184 const QString autoCompleteSource = settings.value( QStringLiteral( "pythonConsole/autoCompleteSource" ), QStringLiteral( "fromAPI" ) ).toString();
185 if ( autoCompleteSource == QLatin1String( "fromDoc" ) )
186 setAutoCompletionSource( AcsDocument );
187 else if ( autoCompleteSource == QLatin1String( "fromDocAPI" ) )
188 setAutoCompletionSource( AcsAll );
189 else
190 setAutoCompletionSource( AcsAPIs );
191 }
192
193 setLineNumbersVisible( true );
194 setIndentationsUseTabs( false );
195 setIndentationGuides( true );
196
198}
199
201{
202 // If editor is readOnly, use the default implementation
203 if ( isReadOnly() )
204 {
205 return QgsCodeEditor::keyPressEvent( event );
206 }
207 const bool ctrlModifier = event->modifiers() & Qt::ControlModifier;
208
209 // Toggle comment when user presses Ctrl+:
210 if ( ctrlModifier && event->key() == Qt::Key_Colon )
211 {
212 event->accept();
214 return;
215 }
216
217 const QgsSettings settings;
218
219 bool autoCloseBracket = settings.value( QStringLiteral( "/pythonConsole/autoCloseBracket" ), true ).toBool();
220 bool autoSurround = settings.value( QStringLiteral( "/pythonConsole/autoSurround" ), true ).toBool();
221 bool autoInsertImport = settings.value( QStringLiteral( "/pythonConsole/autoInsertImport" ), false ).toBool();
222
223 // Update calltips when cursor position changes with left and right keys
224 if ( event->key() == Qt::Key_Left ||
225 event->key() == Qt::Key_Right ||
226 event->key() == Qt::Key_Up ||
227 event->key() == Qt::Key_Down )
228 {
230 callTip();
231 return;
232 }
233
234 // Get entered text and cursor position
235 const QString eText = event->text();
236 int line, column;
237 getCursorPosition( &line, &column );
238
239 // If some text is selected and user presses an opening character
240 // surround the selection with the opening-closing pair
241 if ( hasSelectedText() && autoSurround )
242 {
243 if ( sCompletionPairs.contains( eText ) )
244 {
245 int startLine, startPos, endLine, endPos;
246 getSelection( &startLine, &startPos, &endLine, &endPos );
247
248 // Special case for Multi line quotes (insert triple quotes)
249 if ( startLine != endLine && ( eText == "\"" || eText == "'" ) )
250 {
251 replaceSelectedText(
252 QString( "%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
253 );
254 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
255 }
256 else
257 {
258 replaceSelectedText(
259 QString( "%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
260 );
261 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
262 }
263 event->accept();
264 return;
265 }
266 else if ( sCompletionSingleCharacters.contains( eText ) )
267 {
268 int startLine, startPos, endLine, endPos;
269 getSelection( &startLine, &startPos, &endLine, &endPos );
270 replaceSelectedText(
271 QString( "%1%2%1" ).arg( eText, selectedText() )
272 );
273 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
274 event->accept();
275 return;
276 }
277 }
278
279 // No selected text
280 else
281 {
282 // Automatically insert "import" after "from xxx " if option is enabled
283 if ( autoInsertImport && eText == " " )
284 {
285 const QString lineText = text( line );
286 const thread_local QRegularExpression re( QStringLiteral( "^from [\\w.]+$" ) );
287 if ( re.match( lineText.trimmed() ).hasMatch() )
288 {
289 insert( QStringLiteral( " import" ) );
290 setCursorPosition( line, column + 7 );
291 return QgsCodeEditor::keyPressEvent( event );
292 }
293 }
294
295 // Handle automatic bracket insertion/deletion if option is enabled
296 else if ( autoCloseBracket )
297 {
298 const QString prevChar = characterBeforeCursor();
299 const QString nextChar = characterAfterCursor();
300
301 // When backspace is pressed inside an opening/closing pair, remove both characters
302 if ( event->key() == Qt::Key_Backspace )
303 {
304 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
305 {
306 setSelection( line, column - 1, line, column + 1 );
307 removeSelectedText();
308 event->accept();
309 }
310 else
311 {
313 }
314
315 // Update calltips (cursor position has changed)
316 callTip();
317 return;
318 }
319
320 // When closing character is entered inside an opening/closing pair, shift the cursor
321 else if ( sCompletionPairs.key( eText ) != "" && nextChar == eText )
322 {
323 setCursorPosition( line, column + 1 );
324 event->accept();
325
326 // Will hide calltips when a closing parenthesis is entered
327 callTip();
328 return;
329 }
330
331 // Else, if not inside a string or comment and an opening character
332 // is entered, also insert the closing character
333 else if ( !isCursorInsideStringLiteralOrComment() && sCompletionPairs.contains( eText ) )
334 {
335 // Check if user is not entering triple quotes
336 if ( !( ( eText == "\"" || eText == "'" ) && prevChar == eText ) )
337 {
339 insert( sCompletionPairs[eText] );
340 event->accept();
341 return;
342 }
343 }
344 }
345 }
346
347 // Let QgsCodeEditor handle the keyboard event
348 return QgsCodeEditor::keyPressEvent( event );
349}
350
352{
353 switch ( autoCompletionSource() )
354 {
355 case AcsDocument:
356 autoCompleteFromDocument();
357 break;
358
359 case AcsAPIs:
360 autoCompleteFromAPIs();
361 break;
362
363 case AcsAll:
364 autoCompleteFromAll();
365 break;
366
367 case AcsNone:
368 break;
369 }
370}
371
372void QgsCodeEditorPython::loadAPIs( const QList<QString> &filenames )
373{
374 mAPISFilesList = filenames;
375 //QgsDebugMsg( QStringLiteral( "The apis files: %1" ).arg( mAPISFilesList[0] ) );
377}
378
379bool QgsCodeEditorPython::loadScript( const QString &script )
380{
381 QgsDebugMsgLevel( QStringLiteral( "The script file: %1" ).arg( script ), 2 );
382 QFile file( script );
383 if ( !file.open( QIODevice::ReadOnly ) )
384 {
385 return false;
386 }
387
388 QTextStream in( &file );
389
390 setText( in.readAll().trimmed() );
391 file.close();
392
394 return true;
395}
396
398{
399 int line, index;
400 getCursorPosition( &line, &index );
401 int position = positionFromLineIndex( line, index );
402
403 // Special case: cursor at the end of the document. Style will always be Default,
404 // so we have to check the style of the previous character.
405 // It it is an unclosed string (triple string, unclosed, or comment),
406 // consider cursor is inside a string.
407 if ( position >= length() && position > 0 )
408 {
409 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
410 return style == QsciLexerPython::Comment
411 || style == QsciLexerPython::TripleSingleQuotedString
412 || style == QsciLexerPython::TripleDoubleQuotedString
413 || style == QsciLexerPython::TripleSingleQuotedFString
414 || style == QsciLexerPython::TripleDoubleQuotedFString
415 || style == QsciLexerPython::UnclosedString;
416 }
417 else
418 {
419 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
420 return style == QsciLexerPython::Comment
421 || style == QsciLexerPython::DoubleQuotedString
422 || style == QsciLexerPython::SingleQuotedString
423 || style == QsciLexerPython::TripleSingleQuotedString
424 || style == QsciLexerPython::TripleDoubleQuotedString
425 || style == QsciLexerPython::CommentBlock
426 || style == QsciLexerPython::UnclosedString
427 || style == QsciLexerPython::DoubleQuotedFString
428 || style == QsciLexerPython::SingleQuotedFString
429 || style == QsciLexerPython::TripleSingleQuotedFString
430 || style == QsciLexerPython::TripleDoubleQuotedFString;
431 }
432}
433
435{
436 int line, index;
437 getCursorPosition( &line, &index );
438 int position = positionFromLineIndex( line, index );
439 if ( position <= 0 )
440 {
441 return QString();
442 }
443 return text( position - 1, position );
444}
445
447{
448 int line, index;
449 getCursorPosition( &line, &index );
450 int position = positionFromLineIndex( line, index );
451 if ( position >= length() )
452 {
453 return QString();
454 }
455 return text( position, position + 1 );
456}
457
459{
460 if ( !hasSelectedText() )
461 return;
462
463 QString text = selectedText();
464 text = text.replace( QLatin1String( ">>> " ), QString() ).replace( QLatin1String( "... " ), QString() ).trimmed(); // removing prompts
465 const QString version = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' );
466 QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
467}
468
470{
471 if ( isReadOnly() )
472 {
473 return;
474 }
475
476 beginUndoAction();
477 int startLine, startPos, endLine, endPos;
478 if ( hasSelectedText() )
479 {
480 getSelection( &startLine, &startPos, &endLine, &endPos );
481 }
482 else
483 {
484 getCursorPosition( &startLine, &startPos );
485 endLine = startLine;
486 endPos = startPos;
487 }
488
489 // Check comment state and minimum indentation for each selected line
490 bool allEmpty = true;
491 bool allCommented = true;
492 int minIndentation = -1;
493 for ( int line = startLine; line <= endLine; line++ )
494 {
495 const QString stripped = text( line ).trimmed();
496 if ( !stripped.isEmpty() )
497 {
498 allEmpty = false;
499 if ( !stripped.startsWith( '#' ) )
500 {
501 allCommented = false;
502 }
503 if ( minIndentation == -1 || minIndentation > indentation( line ) )
504 {
505 minIndentation = indentation( line );
506 }
507 }
508 }
509
510 // Special case, only empty lines
511 if ( allEmpty )
512 {
513 return;
514 }
515
516 // Selection shift to keep the same selected text after a # is added/removed
517 int delta = 0;
518
519 for ( int line = startLine; line <= endLine; line++ )
520 {
521 const QString stripped = text( line ).trimmed();
522
523 // Empty line
524 if ( stripped.isEmpty() )
525 {
526 continue;
527 }
528
529 if ( !allCommented )
530 {
531 insertAt( QStringLiteral( "# " ), line, minIndentation );
532 delta = -2;
533 }
534 else
535 {
536 if ( !stripped.startsWith( '#' ) )
537 {
538 continue;
539 }
540 if ( stripped.startsWith( QLatin1String( "# " ) ) )
541 {
542 delta = 2;
543 }
544 else
545 {
546 delta = 1;
547 }
548 setSelection( line, indentation( line ), line, indentation( line ) + delta );
549 removeSelectedText();
550 }
551 }
552
553 endUndoAction();
554 setSelection( startLine, startPos - delta, endLine, endPos - delta );
555}
556
558//
559// QgsQsciLexerPython
560//
561QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
562 : QsciLexerPython( parent )
563{
564
565}
566
567const char *QgsQsciLexerPython::keywords( int set ) const
568{
569 if ( set == 1 )
570 {
571 return "True False and as assert break class continue def del elif else except "
572 "finally for from global if import in is lambda None not or pass "
573 "raise return try while with yield async await nonlocal";
574 }
575
576 return QsciLexerPython::keywords( set );
577}
static QString version()
Version string.
Definition: qgis.cpp:277
ScriptLanguage
Scripting languages.
Definition: qgis.h:2715
static QString pkgDataPath()
Returns the common root path of all application data directories.
@ TripleSingleQuote
Triple single quote color.
@ CommentBlock
Comment block color.
@ DoubleQuote
Double quote color.
@ SingleQuote
Single quote color.
@ TripleDoubleQuote
Triple double quote color.
void autoComplete()
Triggers the autocompletion popup.
QgsCodeEditorPython(QWidget *parent=nullptr, const QList< QString > &filenames=QList< QString >(), QgsCodeEditor::Mode mode=QgsCodeEditor::Mode::ScriptEditor)
Construct a new Python editor.
QString characterAfterCursor() const
Returns the character after the cursor, or an empty string if the cursot is set at end.
bool isCursorInsideStringLiteralOrComment() const
Check whether the current cursor position is inside a string literal or a comment.
void searchSelectedTextInPyQGISDocs()
Searches the selected text in the official PyQGIS online documentation.
void toggleComment()
Toggle comment for the selected text.
Qgis::ScriptLanguage language() const override
Returns the associated scripting language.
void loadAPIs(const QList< QString > &filenames)
Load APIs from one or more files.
void initializeLexer() override
Called when the dialect specific code lexer needs to be initialized (or reinitialized).
virtual void keyPressEvent(QKeyEvent *event) override
bool loadScript(const QString &script)
Loads a script file.
QString characterBeforeCursor() const
Returns the character before the cursor, or an empty string if cursor is set at start.
A text editor based on QScintilla2.
Definition: qgscodeeditor.h:93
Mode
Code editor modes.
void keyPressEvent(QKeyEvent *event) override
void runPostLexerConfigurationTasks()
Performs tasks which must be run after a lexer has been set for the widget.
void setTitle(const QString &title)
Set the widget title.
Flag
Flags controlling behavior of code editor.
void setLineNumbersVisible(bool visible)
Sets whether line numbers should be visible in the editor.
QFont lexerFont() const
Returns the font to use in the lexer.
QColor lexerColor(QgsCodeEditorColorScheme::ColorRole role) const
Returns the color to use in the lexer for the specified role.
static QColor defaultColor(QgsCodeEditorColorScheme::ColorRole role, const QString &theme=QString())
Returns the default color for the specified role.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:63
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38