QGIS API Documentation 3.43.0-Master (32433f7016e)
qgscodeeditor.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscodeeditor.cpp - A base code editor for QGIS and plugins. Provides
3 a base editor using QScintilla for editors
4 --------------------------------------
5 Date : 06-Oct-2013
6 Copyright : (C) 2013 by Salvatore Larosa
7 Email : lrssvtml (at) gmail (dot) com
8 ***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
17#include "qgsapplication.h"
18#include "qgscodeeditor.h"
19#include "moc_qgscodeeditor.cpp"
20#include "qgssettings.h"
21#include "qgssymbollayerutils.h"
22#include "qgsgui.h"
25#include "qgsstringutils.h"
26#include "qgsfontutils.h"
28
29#include <QLabel>
30#include <QWidget>
31#include <QFont>
32#include <QFontDatabase>
33#include <QDebug>
34#include <QFocusEvent>
35#include <Qsci/qscistyle.h>
36#include <QMenu>
37#include <QClipboard>
38#include <QScrollBar>
39#include <QMessageBox>
40#include "Qsci/qscilexer.h"
41
43const QgsSettingsEntryBool *QgsCodeEditor::settingContextHelpHover = new QgsSettingsEntryBool( QStringLiteral( "context-help-hover" ), sTreeCodeEditor, false, QStringLiteral( "Whether the context help should works on hovered words" ) );
45
46
47QMap<QgsCodeEditorColorScheme::ColorRole, QString> QgsCodeEditor::sColorRoleToSettingsKey {
48 { QgsCodeEditorColorScheme::ColorRole::Default, QStringLiteral( "defaultFontColor" ) },
49 { QgsCodeEditorColorScheme::ColorRole::Keyword, QStringLiteral( "keywordFontColor" ) },
50 { QgsCodeEditorColorScheme::ColorRole::Class, QStringLiteral( "classFontColor" ) },
51 { QgsCodeEditorColorScheme::ColorRole::Method, QStringLiteral( "methodFontColor" ) },
52 { QgsCodeEditorColorScheme::ColorRole::Decoration, QStringLiteral( "decoratorFontColor" ) },
53 { QgsCodeEditorColorScheme::ColorRole::Number, QStringLiteral( "numberFontColor" ) },
54 { QgsCodeEditorColorScheme::ColorRole::Comment, QStringLiteral( "commentFontColor" ) },
55 { QgsCodeEditorColorScheme::ColorRole::CommentLine, QStringLiteral( "commentLineFontColor" ) },
56 { QgsCodeEditorColorScheme::ColorRole::CommentBlock, QStringLiteral( "commentBlockFontColor" ) },
57 { QgsCodeEditorColorScheme::ColorRole::Background, QStringLiteral( "paperBackgroundColor" ) },
58 { QgsCodeEditorColorScheme::ColorRole::Cursor, QStringLiteral( "cursorColor" ) },
59 { QgsCodeEditorColorScheme::ColorRole::CaretLine, QStringLiteral( "caretLineColor" ) },
60 { QgsCodeEditorColorScheme::ColorRole::Operator, QStringLiteral( "operatorFontColor" ) },
61 { QgsCodeEditorColorScheme::ColorRole::QuotedOperator, QStringLiteral( "quotedOperatorFontColor" ) },
62 { QgsCodeEditorColorScheme::ColorRole::Identifier, QStringLiteral( "identifierFontColor" ) },
63 { QgsCodeEditorColorScheme::ColorRole::QuotedIdentifier, QStringLiteral( "quotedIdentifierFontColor" ) },
64 { QgsCodeEditorColorScheme::ColorRole::Tag, QStringLiteral( "tagFontColor" ) },
65 { QgsCodeEditorColorScheme::ColorRole::UnknownTag, QStringLiteral( "unknownTagFontColor" ) },
66 { QgsCodeEditorColorScheme::ColorRole::SingleQuote, QStringLiteral( "singleQuoteFontColor" ) },
67 { QgsCodeEditorColorScheme::ColorRole::DoubleQuote, QStringLiteral( "doubleQuoteFontColor" ) },
68 { QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote, QStringLiteral( "tripleSingleQuoteFontColor" ) },
69 { QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote, QStringLiteral( "tripleDoubleQuoteFontColor" ) },
70 { QgsCodeEditorColorScheme::ColorRole::MarginBackground, QStringLiteral( "marginBackgroundColor" ) },
71 { QgsCodeEditorColorScheme::ColorRole::MarginForeground, QStringLiteral( "marginForegroundColor" ) },
72 { QgsCodeEditorColorScheme::ColorRole::SelectionBackground, QStringLiteral( "selectionBackgroundColor" ) },
73 { QgsCodeEditorColorScheme::ColorRole::SelectionForeground, QStringLiteral( "selectionForegroundColor" ) },
74 { QgsCodeEditorColorScheme::ColorRole::MatchedBraceBackground, QStringLiteral( "matchedBraceBackground" ) },
75 { QgsCodeEditorColorScheme::ColorRole::MatchedBraceForeground, QStringLiteral( "matchedBraceColor" ) },
76 { QgsCodeEditorColorScheme::ColorRole::Edge, QStringLiteral( "edgeColor" ) },
77 { QgsCodeEditorColorScheme::ColorRole::Fold, QStringLiteral( "foldColor" ) },
78 { QgsCodeEditorColorScheme::ColorRole::Error, QStringLiteral( "stderrFontColor" ) },
79 { QgsCodeEditorColorScheme::ColorRole::ErrorBackground, QStringLiteral( "stderrBackgroundColor" ) },
80 { QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QStringLiteral( "foldIconForeground" ) },
81 { QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QStringLiteral( "foldIconHalo" ) },
82 { QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QStringLiteral( "indentationGuide" ) },
83 { QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QStringLiteral( "searchMatchBackground" ) }
84};
85
86QgsCodeEditor::QgsCodeEditor( QWidget *parent, const QString &title, bool folding, bool margin, QgsCodeEditor::Flags flags, QgsCodeEditor::Mode mode )
87 : QsciScintilla( parent )
88 , mWidgetTitle( title )
89 , mMargin( margin )
90 , mFlags( flags )
91 , mMode( mode )
92{
93 if ( !parent && mWidgetTitle.isEmpty() )
94 {
95 setWindowTitle( QStringLiteral( "Text Editor" ) );
96 }
97 else
98 {
99 setWindowTitle( mWidgetTitle );
100 }
101
102 if ( folding )
104
105 mSoftHistory.append( QString() );
106
107 setSciWidget();
108 setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded );
109
110 SendScintilla( SCI_SETADDITIONALSELECTIONTYPING, 1 );
111 SendScintilla( SCI_SETMULTIPASTE, 1 );
112 SendScintilla( SCI_SETVIRTUALSPACEOPTIONS, SCVS_RECTANGULARSELECTION );
113
114 SendScintilla( SCI_SETMARGINTYPEN, static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), SC_MARGIN_SYMBOL );
115 SendScintilla( SCI_SETMARGINMASKN, static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 1 << MARKER_NUMBER );
116 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
117 setAnnotationDisplay( QsciScintilla::AnnotationBoxed );
118
119 connect( QgsGui::instance(), &QgsGui::optionsChanged, this, [=] {
120 setSciWidget();
122 } );
123
124 switch ( mMode )
125 {
127 break;
128
130 {
131 // Don't want to see the horizontal scrollbar at all
132 SendScintilla( QsciScintilla::SCI_SETHSCROLLBAR, 0 );
133
134 setWrapMode( QsciScintilla::WrapCharacter );
135 break;
136 }
137
139 {
140 // Don't want to see the horizontal scrollbar at all
141 SendScintilla( QsciScintilla::SCI_SETHSCROLLBAR, 0 );
142
143 setWrapMode( QsciScintilla::WrapCharacter );
144 SendScintilla( QsciScintilla::SCI_EMPTYUNDOBUFFER );
145 break;
146 }
147 }
148
149#if QSCINTILLA_VERSION < 0x020d03
150 installEventFilter( this );
151#endif
152
153 mLastEditTimer = new QTimer( this );
154 mLastEditTimer->setSingleShot( true );
155 mLastEditTimer->setInterval( 1000 );
156 connect( mLastEditTimer, &QTimer::timeout, this, &QgsCodeEditor::onLastEditTimeout );
157 connect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
158}
159
160// Workaround a bug in QScintilla 2.8.X
161void QgsCodeEditor::focusOutEvent( QFocusEvent *event )
162{
163#if QSCINTILLA_VERSION >= 0x020800 && QSCINTILLA_VERSION < 0x020900
164 if ( event->reason() != Qt::ActiveWindowFocusReason )
165 {
166 /* There's a bug in all QScintilla 2.8.X, where
167 a focus out event that is not due to ActiveWindowFocusReason doesn't
168 lead to the bliking caret being disabled. The hack consists in making
169 QsciScintilla::focusOutEvent believe that the event is a ActiveWindowFocusReason
170 The bug was fixed in 2.9 per:
171 2015-04-14 Phil Thompson <phil@riverbankcomputing.com>
172
173 * qt/qsciscintillabase.cpp:
174 Fixed a problem notifying when focus is lost to another application
175 widget.
176 [41734678234e]
177 */
178 QFocusEvent newFocusEvent( QEvent::FocusOut, Qt::ActiveWindowFocusReason );
179 QsciScintilla::focusOutEvent( &newFocusEvent );
180 }
181 else
182#endif
183 {
184 QsciScintilla::focusOutEvent( event );
185 }
186 onLastEditTimeout();
187}
188
189// This workaround a likely bug in QScintilla. The ESC key should not be consumned
190// by the main entry, so that the default behavior (Dialog closing) can trigger,
191// but only is the auto-completion suggestion list isn't displayed
192void QgsCodeEditor::keyPressEvent( QKeyEvent *event )
193{
194 if ( isListActive() )
195 {
196 QsciScintilla::keyPressEvent( event );
197 return;
198 }
199
200 if ( event->key() == Qt::Key_Escape )
201 {
202 // Shortcut QScintilla and redirect the event to the QWidget handler
203 QWidget::keyPressEvent( event ); // NOLINT(bugprone-parent-virtual-call) clazy:exclude=skipped-base-method
204 return;
205 }
206
207 if ( event->key() == Qt::Key_F1 )
208 {
209 // Check if some text is selected
210 QString text = selectedText();
211
212 // Check if mouse is hovering over a word
213 if ( text.isEmpty() && settingContextHelpHover->value() )
214 {
215 text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
216 }
217
218 // Otherwise, check if there is a word at the current text cursor position
219 if ( text.isEmpty() )
220 {
221 int line, index;
222 getCursorPosition( &line, &index );
223 text = wordAtLineIndex( line, index );
224 }
225 emit helpRequested( text );
226 return;
227 }
228
229
231 {
232 switch ( event->key() )
233 {
234 case Qt::Key_Return:
235 case Qt::Key_Enter:
236 runCommand( text() );
237 updatePrompt();
238 return;
239
240 case Qt::Key_Down:
242 updatePrompt();
243 return;
244
245 case Qt::Key_Up:
247 updatePrompt();
248 return;
249
250 default:
251 break;
252 }
253 }
254
255 const bool ctrlModifier = event->modifiers() & Qt::ControlModifier;
256 const bool altModifier = event->modifiers() & Qt::AltModifier;
257
258 // Ctrl+Alt+F: reformat code
260 if ( !isReadOnly() && canReformat && ctrlModifier && altModifier && event->key() == Qt::Key_F )
261 {
262 event->accept();
263 reformatCode();
264 return;
265 }
266
267 // Toggle comment when user presses Ctrl+:
269 if ( !isReadOnly() && canToggle && ctrlModifier && event->key() == Qt::Key_Colon )
270 {
271 event->accept();
273 return;
274 }
275
276 QsciScintilla::keyPressEvent( event );
277
278 // Update calltips unless event is autorepeat
279 if ( !event->isAutoRepeat() )
280 {
281 callTip();
282 }
283}
284
285void QgsCodeEditor::contextMenuEvent( QContextMenuEvent *event )
286{
287 switch ( mMode )
288 {
290 {
291 QMenu *menu = createStandardContextMenu();
292 menu->setAttribute( Qt::WA_DeleteOnClose );
293
295 {
296 menu->addSeparator();
297 }
298
300 {
301 QAction *reformatAction = new QAction( tr( "Reformat Code" ), menu );
302 reformatAction->setShortcut( QStringLiteral( "Ctrl+Alt+F" ) );
303 reformatAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconFormatCode.svg" ) ) );
304 reformatAction->setEnabled( !isReadOnly() );
305 connect( reformatAction, &QAction::triggered, this, &QgsCodeEditor::reformatCode );
306 menu->addAction( reformatAction );
307 }
308
310 {
311 QAction *syntaxCheckAction = new QAction( tr( "Check Syntax" ), menu );
312 syntaxCheckAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSyntaxErrorConsole.svg" ) ) );
313 connect( syntaxCheckAction, &QAction::triggered, this, &QgsCodeEditor::checkSyntax );
314 menu->addAction( syntaxCheckAction );
315 }
316
318 {
319 QAction *toggleCommentAction = new QAction( tr( "Toggle Comment" ), menu );
320 toggleCommentAction->setShortcut( QStringLiteral( "Ctrl+:" ) );
321 toggleCommentAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconCommentEditorConsole.svg" ), palette().color( QPalette::ColorRole::WindowText ) ) );
322 toggleCommentAction->setEnabled( !isReadOnly() );
323 connect( toggleCommentAction, &QAction::triggered, this, &QgsCodeEditor::toggleComment );
324 menu->addAction( toggleCommentAction );
325 }
326
327 populateContextMenu( menu );
328
329 menu->exec( mapToGlobal( event->pos() ) );
330 break;
331 }
332
334 {
335 QMenu *menu = new QMenu( this );
336 QMenu *historySubMenu = new QMenu( tr( "Command History" ), menu );
337
338 historySubMenu->addAction( tr( "Show" ), this, &QgsCodeEditor::showHistory, QStringLiteral( "Ctrl+Shift+SPACE" ) );
339 historySubMenu->addAction( tr( "Clear File" ), this, &QgsCodeEditor::clearPersistentHistory );
340 historySubMenu->addAction( tr( "Clear Session" ), this, &QgsCodeEditor::clearSessionHistory );
341
342 menu->addMenu( historySubMenu );
343 menu->addSeparator();
344
345 QAction *copyAction = menu->addAction( QgsApplication::getThemeIcon( "mActionEditCopy.svg" ), tr( "Copy" ), this, &QgsCodeEditor::copy, QKeySequence::Copy );
346 QAction *pasteAction = menu->addAction( QgsApplication::getThemeIcon( "mActionEditPaste.svg" ), tr( "Paste" ), this, &QgsCodeEditor::paste, QKeySequence::Paste );
347 copyAction->setEnabled( hasSelectedText() );
348 pasteAction->setEnabled( !QApplication::clipboard()->text().isEmpty() );
349
350 populateContextMenu( menu );
351
352 menu->exec( mapToGlobal( event->pos() ) );
353 break;
354 }
355
357 QsciScintilla::contextMenuEvent( event );
358 break;
359 }
360}
361
362
363bool QgsCodeEditor::eventFilter( QObject *watched, QEvent *event )
364{
365#if QSCINTILLA_VERSION < 0x020d03
366 if ( watched == this && event->type() == QEvent::InputMethod )
367 {
368 // swallow input method events, which cause loss of selected text.
369 // See https://sourceforge.net/p/scintilla/bugs/1913/ , which was ported to QScintilla
370 // in version 2.13.3
371 return true;
372 }
373#endif
374
375 return QsciScintilla::eventFilter( watched, event );
376}
377
381
383{
384 if ( mUseDefaultSettings )
385 return color( role );
386
387 if ( !mOverrideColors )
388 {
389 return defaultColor( role, mColorScheme );
390 }
391 else
392 {
393 const QColor color = mCustomColors.value( role );
394 return !color.isValid() ? defaultColor( role ) : color;
395 }
396}
397
399{
400 if ( mUseDefaultSettings )
401 return getMonospaceFont();
402
403 QFont font = QFontDatabase::systemFont( QFontDatabase::FixedFont );
404
405 const QgsSettings settings;
406 if ( !mFontFamily.isEmpty() )
407 QgsFontUtils::setFontFamily( font, mFontFamily );
408
409#ifdef Q_OS_MAC
410 if ( mFontSize > 0 )
411 font.setPointSize( mFontSize );
412 else
413 {
414 // The font size gotten from getMonospaceFont() is too small on Mac
415 font.setPointSize( QLabel().font().pointSize() );
416 }
417#else
418 if ( mFontSize > 0 )
419 font.setPointSize( mFontSize );
420 else
421 {
422 const int fontSize = settings.value( QStringLiteral( "qgis/stylesheet/fontPointSize" ), 10 ).toInt();
423 font.setPointSize( fontSize );
424 }
425#endif
426 font.setBold( false );
427
428 return font;
429}
430
432{
433 updateFolding();
434
437
438 SendScintilla( SCI_MARKERSETFORE, SC_MARKNUM_FOLDEROPEN, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconHalo ) );
439 SendScintilla( SCI_MARKERSETBACK, SC_MARKNUM_FOLDEROPEN, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconForeground ) );
440 SendScintilla( SCI_MARKERSETFORE, SC_MARKNUM_FOLDER, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconHalo ) );
441 SendScintilla( SCI_MARKERSETBACK, SC_MARKNUM_FOLDER, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconForeground ) );
442 SendScintilla( SCI_STYLESETFORE, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) );
443 SendScintilla( SCI_STYLESETBACK, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) );
444
445 SendScintilla( QsciScintilla::SCI_INDICSETSTYLE, SEARCH_RESULT_INDICATOR, QsciScintilla::INDIC_STRAIGHTBOX );
446 SendScintilla( QsciScintilla::SCI_INDICSETFORE, SEARCH_RESULT_INDICATOR, lexerColor( QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground ) );
447 SendScintilla( QsciScintilla::SCI_INDICSETALPHA, SEARCH_RESULT_INDICATOR, 100 );
448 SendScintilla( QsciScintilla::SCI_INDICSETUNDER, SEARCH_RESULT_INDICATOR, true );
449 SendScintilla( QsciScintilla::SCI_INDICGETOUTLINEALPHA, SEARCH_RESULT_INDICATOR, 255 );
450
452 {
453 setCaretLineVisible( false );
454 setLineNumbersVisible( false ); // NO linenumbers for the input line
455 // Margin 1 is used for the '>' prompt (console input)
456 setMarginLineNumbers( 1, true );
457 setMarginWidth( 1, "00000" );
458 setMarginType( 1, QsciScintilla::MarginType::TextMarginRightJustified );
459 setMarginsBackgroundColor( color( QgsCodeEditorColorScheme::ColorRole::Background ) );
460 setEdgeMode( QsciScintilla::EdgeNone );
461 }
462}
463
464void QgsCodeEditor::onLastEditTimeout()
465{
466 mLastEditTimer->stop();
467 emit editingTimeout();
468}
469
470void QgsCodeEditor::setSciWidget()
471{
472 const QFont font = lexerFont();
473 setFont( font );
474
475 setUtf8( true );
476 setCaretLineVisible( true );
477 setCaretLineBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CaretLine ) );
478 setCaretForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Cursor ) );
481
482 setBraceMatching( QsciScintilla::SloppyBraceMatch );
485
486 setLineNumbersVisible( false );
487
488 // temporarily disable folding, will be enabled later if required by updateFolding()
489 setFolding( QsciScintilla::NoFoldStyle );
490 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
491
492 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
493
496 setIndentationGuidesForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginForeground ) );
497 setIndentationGuidesBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginBackground ) );
498 // whether margin will be shown
499 updateFolding();
500 const QColor foldColor = lexerColor( QgsCodeEditorColorScheme::ColorRole::Fold );
501 setFoldMarginColors( foldColor, foldColor );
502 // indentation
503 setAutoIndent( true );
504 setIndentationWidth( 4 );
505 setTabIndents( true );
506 setBackspaceUnindents( true );
507 setTabWidth( 4 );
508 // autocomplete
509 setAutoCompletionThreshold( 2 );
510 setAutoCompletionSource( QsciScintilla::AcsAPIs );
511
512 markerDefine( QgsApplication::getThemePixmap( "console/iconSyntaxErrorConsoleParams.svg", lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), lexerColor( QgsCodeEditorColorScheme::ColorRole::ErrorBackground ), 16 ), MARKER_NUMBER );
513}
514
515void QgsCodeEditor::setTitle( const QString &title )
516{
517 setWindowTitle( title );
518}
519
524
529
531{
532 switch ( language )
533 {
535 return tr( "CSS" );
537 return tr( "Expression" );
539 return tr( "HTML" );
541 return tr( "JavaScript" );
543 return tr( "JSON" );
545 return tr( "Python" );
547 return tr( "R" );
549 return tr( "SQL" );
551 return tr( "Batch" );
553 return tr( "Bash" );
555 return QString();
556 }
558}
559
561{
562 mMargin = margin;
563 if ( margin )
564 {
565 QFont marginFont = lexerFont();
566 marginFont.setPointSize( 10 );
567 setMarginLineNumbers( 0, true );
568 setMarginsFont( marginFont );
569 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), QStringLiteral( "00000" ) );
572 }
573 else
574 {
575 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), 0 );
576 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
577 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
578 }
579}
580
582{
583 if ( visible )
584 {
585 QFont marginFont = lexerFont();
586 marginFont.setPointSize( 10 );
587 setMarginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), true );
588 setMarginsFont( marginFont );
589 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), QStringLiteral( "00000" ) );
592 }
593 else
594 {
595 setMarginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), false );
596 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), 0 );
597 }
598}
599
601{
602 return marginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ) );
603}
604
606{
607 if ( folding )
608 {
610 }
611 else
612 {
613 mFlags &= ~( static_cast<int>( QgsCodeEditor::Flag::CodeFolding ) );
614 }
615 updateFolding();
616}
617
622
623void QgsCodeEditor::updateFolding()
624{
626 {
627 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), "0" );
630 setFolding( QsciScintilla::PlainFoldStyle );
631 }
632 else
633 {
634 setFolding( QsciScintilla::NoFoldStyle );
635 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
636 }
637}
638
639bool QgsCodeEditor::readHistoryFile()
640{
641 if ( mHistoryFilePath.isEmpty() || !QFile::exists( mHistoryFilePath ) )
642 return false;
643
644 QFile file( mHistoryFilePath );
645 if ( file.open( QIODevice::ReadOnly ) )
646 {
647 QTextStream stream( &file );
648#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
649 // Always use UTF-8
650 stream.setCodec( "UTF-8" );
651#endif
652 QString line;
653 while ( !stream.atEnd() )
654 {
655 line = stream.readLine(); // line of text excluding '\n'
656 mHistory.append( line );
657 }
658 syncSoftHistory();
659 return true;
660 }
661
662 return false;
663}
664
665void QgsCodeEditor::syncSoftHistory()
666{
667 mSoftHistory = mHistory;
668 mSoftHistory.append( QString() );
669 mSoftHistoryIndex = mSoftHistory.length() - 1;
670}
671
673{
674 mSoftHistory[mSoftHistoryIndex] = text();
675}
676
677void QgsCodeEditor::updateHistory( const QStringList &commands, bool skipSoftHistory )
678{
679 if ( commands.size() > 1 )
680 {
681 mHistory.append( commands );
682 }
683 else if ( !commands.value( 0 ).isEmpty() )
684 {
685 const QString command = commands.value( 0 );
686 if ( mHistory.empty() || command != mHistory.constLast() )
687 mHistory.append( command );
688 }
689
690 if ( !skipSoftHistory )
691 syncSoftHistory();
692}
693
695{
696}
697
698QString QgsCodeEditor::reformatCodeString( const QString &string )
699{
700 return string;
701}
702
703void QgsCodeEditor::showMessage( const QString &title, const QString &message, Qgis::MessageLevel level )
704{
705 switch ( level )
706 {
707 case Qgis::Info:
708 case Qgis::Success:
709 case Qgis::NoLevel:
710 QMessageBox::information( this, title, message );
711 break;
712
713 case Qgis::Warning:
714 QMessageBox::warning( this, title, message );
715 break;
716
717 case Qgis::Critical:
718 QMessageBox::critical( this, title, message );
719 break;
720 }
721}
722
724{
725 if ( mInterpreter )
726 {
727 const QString prompt = mInterpreter->promptForState( mInterpreter->currentState() );
728 SendScintilla( QsciScintilla::SCI_MARGINSETTEXT, static_cast<uintptr_t>( 0 ), prompt.toUtf8().constData() );
729 }
730}
731
733{
734 return mInterpreter;
735}
736
738{
739 mInterpreter = newInterpreter;
740 updatePrompt();
741}
742
743// Find the source substring index that most closely matches the target string
744int findMinimalDistanceIndex( const QString &source, const QString &target )
745{
746 const int index = std::min( source.length(), target.length() );
747
748 const int d0 = QgsStringUtils::levenshteinDistance( source.left( index ), target );
749 if ( d0 == 0 )
750 return index;
751
752 int refDistanceMore = d0;
753 int refIndexMore = index;
754 if ( index < source.length() - 1 )
755 {
756 while ( true )
757 {
758 const int newDistance = QgsStringUtils::levenshteinDistance( source.left( refIndexMore + 1 ), target );
759 if ( newDistance <= refDistanceMore )
760 {
761 refDistanceMore = newDistance;
762 refIndexMore++;
763 if ( refIndexMore == source.length() - 1 )
764 break;
765 }
766 else
767 {
768 break;
769 }
770 }
771 }
772
773 int refDistanceLess = d0;
774 int refIndexLess = index;
775 if ( index > 0 )
776 {
777 while ( true )
778 {
779 const int newDistance = QgsStringUtils::levenshteinDistance( source.left( refIndexLess - 1 ), target );
780 if ( newDistance <= refDistanceLess )
781 {
782 refDistanceLess = newDistance;
783 refIndexLess--;
784 if ( refIndexLess == 0 )
785 break;
786 }
787 else
788 {
789 break;
790 }
791 }
792 }
793
794 if ( refDistanceMore < refDistanceLess )
795 return refIndexMore;
796 else
797 return refIndexLess;
798}
799
801{
803 return;
804
805 const QString textBeforeCursor = text( 0, linearPosition() );
806 const QString originalText = text();
807 const QString newText = reformatCodeString( originalText );
808
809 if ( originalText == newText )
810 return;
811
812 // try to preserve the cursor position and scroll position
813 const int oldScrollValue = verticalScrollBar()->value();
814 const int linearIndex = findMinimalDistanceIndex( newText, textBeforeCursor );
815
816 beginUndoAction();
817 selectAll();
818 removeSelectedText();
819 insert( newText );
820 setLinearPosition( linearIndex );
821 verticalScrollBar()->setValue( oldScrollValue );
822 endUndoAction();
823}
824
826{
827 return true;
828}
829
833
834void QgsCodeEditor::toggleLineComments( const QString &commentPrefix )
835{
836 if ( isReadOnly() )
837 {
838 return;
839 }
840
841 beginUndoAction();
842 int startLine, startPos, endLine, endPos;
843 if ( hasSelectedText() )
844 {
845 getSelection( &startLine, &startPos, &endLine, &endPos );
846 }
847 else
848 {
849 getCursorPosition( &startLine, &startPos );
850 endLine = startLine;
851 endPos = startPos;
852 }
853
854 // Check comment state and minimum indentation for each selected line
855 bool allEmpty = true;
856 bool allCommented = true;
857 int minIndentation = -1;
858 for ( int line = startLine; line <= endLine; line++ )
859 {
860 const QString stripped = text( line ).trimmed();
861 if ( !stripped.isEmpty() )
862 {
863 allEmpty = false;
864 if ( !stripped.startsWith( commentPrefix ) )
865 {
866 allCommented = false;
867 }
868 if ( minIndentation == -1 || minIndentation > indentation( line ) )
869 {
870 minIndentation = indentation( line );
871 }
872 }
873 }
874
875 // Special case, only empty lines
876 if ( allEmpty )
877 {
878 return;
879 }
880
881 // Selection shift to keep the same selected text after the prefix is added/removed
882 int delta = 0;
883
884 const int prefixLength = static_cast<int>( commentPrefix.length() );
885
886 const bool startLineEmpty = ( text( startLine ).trimmed().isEmpty() );
887 const bool endLineEmpty = ( text( endLine ).trimmed().isEmpty() );
888
889 for ( int line = startLine; line <= endLine; line++ )
890 {
891 const QString stripped = text( line ).trimmed();
892
893 // Empty line
894 if ( stripped.isEmpty() )
895 {
896 continue;
897 }
898
899 if ( !allCommented )
900 {
901 insertAt( commentPrefix + ' ', line, minIndentation );
902 delta = -( prefixLength + 1 );
903 }
904 else
905 {
906 if ( !stripped.startsWith( commentPrefix ) )
907 {
908 continue;
909 }
910 if ( stripped.startsWith( commentPrefix + ' ' ) )
911 {
912 delta = prefixLength + 1;
913 }
914 else
915 {
916 delta = prefixLength;
917 }
918 setSelection( line, indentation( line ), line, indentation( line ) + delta );
919 removeSelectedText();
920 }
921 }
922
923 endUndoAction();
924 setSelection( startLine, startPos - ( startLineEmpty ? 0 : delta ), endLine, endPos - ( endLineEmpty ? 0 : delta ) );
925}
926
928{
929 // A zero width would make setScrollWidth crash
930 long maxWidth = 10;
931
932 // Get the number of lines
933 int lineCount = lines();
934
935 // Loop through all the lines to get the longest one
936 for ( int line = 0; line < lineCount; line++ )
937 {
938 // Get the linear position at the end of the current line
939 const long endLine = SendScintilla( SCI_GETLINEENDPOSITION, line );
940 // Get the x coordinates of the end of the line
941 const long x = SendScintilla( SCI_POINTXFROMPOSITION, 0, endLine );
942 maxWidth = std::max( maxWidth, x );
943 }
944
945 // Use the longest line width as the new scroll width
946 setScrollWidth( static_cast<int>( maxWidth ) );
947}
948
949void QgsCodeEditor::setText( const QString &text )
950{
951 disconnect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
952 QsciScintilla::setText( text );
953 connect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
954 onLastEditTimeout();
956}
957
959{
960 return mLastEditTimer->interval();
961}
962
964{
965 mLastEditTimer->setInterval( timeout );
966}
967
968
969QStringList QgsCodeEditor::history() const
970{
971 return mHistory;
972}
973
974void QgsCodeEditor::runCommand( const QString &command, bool skipHistory )
975{
976 if ( !skipHistory )
977 {
978 updateHistory( { command } );
981 }
982
983 if ( mInterpreter )
984 mInterpreter->exec( command );
985
986 clear();
988}
989
991{
992 mHistory.clear();
993 readHistoryFile();
994 syncSoftHistory();
995
997}
998
1000{
1001 mHistory.clear();
1002
1003 if ( !mHistoryFilePath.isEmpty() && QFile::exists( mHistoryFilePath ) )
1004 {
1005 QFile file( mHistoryFilePath );
1006 file.open( QFile::WriteOnly | QFile::Truncate );
1007 }
1008
1010}
1011
1013{
1014 if ( mHistoryFilePath.isEmpty() )
1015 return false;
1016
1017 QFile f( mHistoryFilePath );
1018 if ( !f.open( QFile::WriteOnly | QIODevice::Truncate ) )
1019 {
1020 return false;
1021 }
1022
1023 QTextStream ts( &f );
1024#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
1025 ts.setCodec( "UTF-8" );
1026#endif
1027 for ( const QString &command : std::as_const( mHistory ) )
1028 {
1029 ts << command + '\n';
1030 }
1031 return true;
1032}
1033
1035{
1036 if ( mSoftHistoryIndex < mSoftHistory.length() - 1 && !mSoftHistory.isEmpty() )
1037 {
1038 mSoftHistoryIndex += 1;
1039 setText( mSoftHistory[mSoftHistoryIndex] );
1041 }
1042}
1043
1045{
1046 if ( mSoftHistoryIndex > 0 && !mSoftHistory.empty() )
1047 {
1048 mSoftHistoryIndex -= 1;
1049 setText( mSoftHistory[mSoftHistoryIndex] );
1051 }
1052}
1053
1055{
1056 QgsCodeEditorHistoryDialog *dialog = new QgsCodeEditorHistoryDialog( this, this );
1057 dialog->setAttribute( Qt::WA_DeleteOnClose );
1058
1059 dialog->show();
1060 dialog->activateWindow();
1061}
1062
1064{
1065 // remove item from the command history (just for the current session)
1066 mHistory.removeAt( index );
1067 mSoftHistory.removeAt( index );
1068 if ( index < mSoftHistoryIndex )
1069 {
1070 mSoftHistoryIndex -= 1;
1071 if ( mSoftHistoryIndex < 0 )
1072 mSoftHistoryIndex = mSoftHistory.length() - 1;
1073 }
1074}
1075
1076void QgsCodeEditor::insertText( const QString &text )
1077{
1078 // Insert the text or replace selected text
1079 if ( hasSelectedText() )
1080 {
1081 replaceSelectedText( text );
1082 }
1083 else
1084 {
1085 int line, index;
1086 getCursorPosition( &line, &index );
1087 insertAt( text, line, index );
1088 setCursorPosition( line, index + text.length() );
1089 }
1090}
1091
1093{
1094 if ( theme.isEmpty() && QgsApplication::themeName() == QLatin1String( "default" ) )
1095 {
1096 // if using default theme, take certain colors from the palette
1097 const QPalette pal = qApp->palette();
1098
1099 switch ( role )
1100 {
1102 return pal.color( QPalette::Highlight );
1104 return pal.color( QPalette::HighlightedText );
1105 default:
1106 break;
1107 }
1108 }
1109 else if ( theme.isEmpty() )
1110 {
1111 // non default theme (e.g. Blend of Gray). Take colors from theme ini file...
1112 const QSettings ini( QgsApplication::uiThemes().value( QgsApplication::themeName() ) + "/qscintilla.ini", QSettings::IniFormat );
1113
1114 static const QMap<QgsCodeEditorColorScheme::ColorRole, QString> sColorRoleToIniKey {
1115 { QgsCodeEditorColorScheme::ColorRole::Default, QStringLiteral( "python/defaultFontColor" ) },
1116 { QgsCodeEditorColorScheme::ColorRole::Keyword, QStringLiteral( "python/keywordFontColor" ) },
1117 { QgsCodeEditorColorScheme::ColorRole::Class, QStringLiteral( "python/classFontColor" ) },
1118 { QgsCodeEditorColorScheme::ColorRole::Method, QStringLiteral( "python/methodFontColor" ) },
1119 { QgsCodeEditorColorScheme::ColorRole::Decoration, QStringLiteral( "python/decoratorFontColor" ) },
1120 { QgsCodeEditorColorScheme::ColorRole::Number, QStringLiteral( "python/numberFontColor" ) },
1121 { QgsCodeEditorColorScheme::ColorRole::Comment, QStringLiteral( "python/commentFontColor" ) },
1122 { QgsCodeEditorColorScheme::ColorRole::CommentLine, QStringLiteral( "sql/commentLineFontColor" ) },
1123 { QgsCodeEditorColorScheme::ColorRole::CommentBlock, QStringLiteral( "python/commentBlockFontColor" ) },
1124 { QgsCodeEditorColorScheme::ColorRole::Background, QStringLiteral( "python/paperBackgroundColor" ) },
1125 { QgsCodeEditorColorScheme::ColorRole::Cursor, QStringLiteral( "cursorColor" ) },
1126 { QgsCodeEditorColorScheme::ColorRole::CaretLine, QStringLiteral( "caretLineColor" ) },
1127 { QgsCodeEditorColorScheme::ColorRole::Operator, QStringLiteral( "sql/operatorFontColor" ) },
1128 { QgsCodeEditorColorScheme::ColorRole::QuotedOperator, QStringLiteral( "sql/QuotedOperatorFontColor" ) },
1129 { QgsCodeEditorColorScheme::ColorRole::Identifier, QStringLiteral( "sql/identifierFontColor" ) },
1130 { QgsCodeEditorColorScheme::ColorRole::QuotedIdentifier, QStringLiteral( "sql/QuotedIdentifierFontColor" ) },
1131 { QgsCodeEditorColorScheme::ColorRole::Tag, QStringLiteral( "html/tagFontColor" ) },
1132 { QgsCodeEditorColorScheme::ColorRole::UnknownTag, QStringLiteral( "html/unknownTagFontColor" ) },
1133 { QgsCodeEditorColorScheme::ColorRole::SingleQuote, QStringLiteral( "sql/singleQuoteFontColor" ) },
1134 { QgsCodeEditorColorScheme::ColorRole::DoubleQuote, QStringLiteral( "sql/doubleQuoteFontColor" ) },
1135 { QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote, QStringLiteral( "python/tripleSingleQuoteFontColor" ) },
1136 { QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote, QStringLiteral( "python/tripleDoubleQuoteFontColor" ) },
1137 { QgsCodeEditorColorScheme::ColorRole::MarginBackground, QStringLiteral( "marginBackgroundColor" ) },
1138 { QgsCodeEditorColorScheme::ColorRole::MarginForeground, QStringLiteral( "marginForegroundColor" ) },
1139 { QgsCodeEditorColorScheme::ColorRole::SelectionBackground, QStringLiteral( "selectionBackgroundColor" ) },
1140 { QgsCodeEditorColorScheme::ColorRole::SelectionForeground, QStringLiteral( "selectionForegroundColor" ) },
1141 { QgsCodeEditorColorScheme::ColorRole::MatchedBraceBackground, QStringLiteral( "matchedBraceBackground" ) },
1142 { QgsCodeEditorColorScheme::ColorRole::MatchedBraceForeground, QStringLiteral( "matchedBraceColor" ) },
1143 { QgsCodeEditorColorScheme::ColorRole::Edge, QStringLiteral( "edgeColor" ) },
1144 { QgsCodeEditorColorScheme::ColorRole::Fold, QStringLiteral( "foldColor" ) },
1145 { QgsCodeEditorColorScheme::ColorRole::Error, QStringLiteral( "stderrFontColor" ) },
1146 { QgsCodeEditorColorScheme::ColorRole::ErrorBackground, QStringLiteral( "stderrBackground" ) },
1147 { QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QStringLiteral( "foldIconForeground" ) },
1148 { QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QStringLiteral( "foldIconHalo" ) },
1149 { QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QStringLiteral( "indentationGuide" ) },
1150 { QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QStringLiteral( "searchMatchBackground" ) },
1151 };
1152
1153 const QgsCodeEditorColorScheme defaultScheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( QStringLiteral( "default" ) );
1154 return QgsSymbolLayerUtils::decodeColor( ini.value( sColorRoleToIniKey.value( role ), defaultScheme.color( role ).name() ).toString() );
1155 }
1156
1157 const QgsCodeEditorColorScheme scheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( theme.isEmpty() ? QStringLiteral( "default" ) : theme );
1158 return scheme.color( role );
1159}
1160
1162{
1163 const QgsSettings settings;
1164 if ( !settings.value( QStringLiteral( "codeEditor/overrideColors" ), false, QgsSettings::Gui ).toBool() )
1165 {
1166 const QString theme = settings.value( QStringLiteral( "codeEditor/colorScheme" ), QString(), QgsSettings::Gui ).toString();
1167 return defaultColor( role, theme );
1168 }
1169 else
1170 {
1171 const QString color = settings.value( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), QString(), QgsSettings::Gui ).toString();
1172 return color.isEmpty() ? defaultColor( role ) : QgsSymbolLayerUtils::decodeColor( color );
1173 }
1174}
1175
1177{
1178 QgsSettings settings;
1179 if ( color.isValid() )
1180 {
1181 settings.setValue( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), color.name(), QgsSettings::Gui );
1182 }
1183 else
1184 {
1185 settings.remove( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), QgsSettings::Gui );
1186 }
1187}
1188
1189// Settings for font and fontsize
1190bool QgsCodeEditor::isFixedPitch( const QFont &font )
1191{
1192 return font.fixedPitch();
1193}
1194
1196{
1197 QFont font = QFontDatabase::systemFont( QFontDatabase::FixedFont );
1198
1199 const QgsSettings settings;
1200 if ( !settings.value( QStringLiteral( "codeEditor/fontfamily" ), QString(), QgsSettings::Gui ).toString().isEmpty() )
1201 QgsFontUtils::setFontFamily( font, settings.value( QStringLiteral( "codeEditor/fontfamily" ), QString(), QgsSettings::Gui ).toString() );
1202
1203 const int fontSize = settings.value( QStringLiteral( "codeEditor/fontsize" ), 0, QgsSettings::Gui ).toInt();
1204
1205#ifdef Q_OS_MAC
1206 if ( fontSize > 0 )
1207 font.setPointSize( fontSize );
1208 else
1209 {
1210 // The font size gotten from getMonospaceFont() is too small on Mac
1211 font.setPointSize( QLabel().font().pointSize() );
1212 }
1213#else
1214 if ( fontSize > 0 )
1215 font.setPointSize( fontSize );
1216 else
1217 {
1218 const int fontSize = settings.value( QStringLiteral( "qgis/stylesheet/fontPointSize" ), 10 ).toInt();
1219 font.setPointSize( fontSize );
1220 }
1221#endif
1222 font.setBold( false );
1223
1224 return font;
1225}
1226
1227void QgsCodeEditor::setCustomAppearance( const QString &scheme, const QMap<QgsCodeEditorColorScheme::ColorRole, QColor> &customColors, const QString &fontFamily, int fontSize )
1228{
1229 mUseDefaultSettings = false;
1230 mOverrideColors = !customColors.isEmpty();
1231 mColorScheme = scheme;
1232 mCustomColors = customColors;
1233 mFontFamily = fontFamily;
1234 mFontSize = fontSize;
1235
1236 setSciWidget();
1238}
1239
1240void QgsCodeEditor::addWarning( const int lineNumber, const QString &warning )
1241{
1242 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), "000" );
1243 markerAdd( lineNumber, MARKER_NUMBER );
1244 QFont font = lexerFont();
1245 font.setItalic( true );
1246 const QsciStyle styleAnn = QsciStyle( -1, QStringLiteral( "Annotation" ), lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), lexerColor( QgsCodeEditorColorScheme::ColorRole::ErrorBackground ), font, true );
1247 annotate( lineNumber, warning, styleAnn );
1248 mWarningLines.push_back( lineNumber );
1249}
1250
1252{
1253 for ( const int line : mWarningLines )
1254 {
1255 markerDelete( line );
1256 clearAnnotations( line );
1257 }
1258 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
1259 mWarningLines.clear();
1260}
1261
1263{
1264 int line = 0;
1265 int index = 0;
1266 getCursorPosition( &line, &index );
1267 return line == lines() - 1;
1268}
1269
1270void QgsCodeEditor::setHistoryFilePath( const QString &path )
1271{
1272 mHistoryFilePath = path;
1273 readHistoryFile();
1274}
1275
1277{
1278 setCursorPosition( 0, 0 );
1279 ensureCursorVisible();
1280 ensureLineVisible( 0 );
1281
1282 if ( mMode == QgsCodeEditor::Mode::CommandInput )
1283 updatePrompt();
1284}
1285
1287{
1288 const int endLine = lines() - 1;
1289 const int endLineLength = lineLength( endLine );
1290 setCursorPosition( endLine, endLineLength );
1291 ensureCursorVisible();
1292 ensureLineVisible( endLine );
1293
1294 if ( mMode == QgsCodeEditor::Mode::CommandInput )
1295 updatePrompt();
1296}
1297
1299{
1300 return static_cast<int>( SendScintilla( SCI_GETCURRENTPOS ) );
1301}
1302
1304{
1305 int line, index;
1306 lineIndexFromPosition( linearIndex, &line, &index );
1307 setCursorPosition( line, index );
1308}
1309
1311{
1312 int startLine, startIndex, _;
1313 getSelection( &startLine, &startIndex, &_, &_ );
1314 if ( startLine == -1 )
1315 {
1316 return linearPosition();
1317 }
1318 return positionFromLineIndex( startLine, startIndex );
1319}
1320
1322{
1323 int endLine, endIndex, _;
1324 getSelection( &_, &_, &endLine, &endIndex );
1325 if ( endLine == -1 )
1326 {
1327 return linearPosition();
1328 }
1329 return positionFromLineIndex( endLine, endIndex );
1330}
1331
1332void QgsCodeEditor::setLinearSelection( int start, int end )
1333{
1334 int startLine, startIndex, endLine, endIndex;
1335 lineIndexFromPosition( start, &startLine, &startIndex );
1336 lineIndexFromPosition( end, &endLine, &endIndex );
1337 setSelection( startLine, startIndex, endLine, endIndex );
1338}
1339
1341
1342int QgsCodeInterpreter::exec( const QString &command )
1343{
1344 mState = execCommandImpl( command );
1345 return mState;
1346}
1347
1348
1350{
1351 // If wrapping is disabled, return -1
1352 if ( wrapMode() == WrapNone )
1353 {
1354 return -1;
1355 }
1356 // Get the current line
1357 if ( line == -1 )
1358 {
1359 int _index;
1360 lineIndexFromPosition( linearPosition(), &line, &_index );
1361 }
1362
1363 // If line isn't wrapped, return -1
1364 if ( SendScintilla( SCI_WRAPCOUNT, line ) <= 1 )
1365 {
1366 return -1;
1367 }
1368
1369 // Get the linear position at the end of the current line
1370 const long endLine = SendScintilla( SCI_GETLINEENDPOSITION, line );
1371 // Get the y coordinates of the start of the last wrapped line
1372 const long y = SendScintilla( SCI_POINTYFROMPOSITION, 0, endLine );
1373 // Return the linear position of the start of the last wrapped line
1374 return static_cast<int>( SendScintilla( SCI_POSITIONFROMPOINT, 0, y ) );
1375}
1376
1377
1378// Adapted from QsciScintilla source code (qsciscintilla.cpp) to handle line wrap
1380{
1381 if ( callTipsStyle() == CallTipsNone || lexer() == nullptr )
1382 {
1383 return;
1384 }
1385
1386 QsciAbstractAPIs *apis = lexer()->apis();
1387
1388 if ( !apis )
1389 return;
1390
1391 int pos, commas = 0;
1392 bool found = false;
1393 char ch;
1394
1395 pos = linearPosition();
1396
1397 // Move backwards through the line looking for the start of the current
1398 // call tip and working out which argument it is.
1399 while ( ( ch = getCharacter( pos ) ) != '\0' )
1400 {
1401 if ( ch == ',' )
1402 ++commas;
1403 else if ( ch == ')' )
1404 {
1405 int depth = 1;
1406
1407 // Ignore everything back to the start of the corresponding
1408 // parenthesis.
1409 while ( ( ch = getCharacter( pos ) ) != '\0' )
1410 {
1411 if ( ch == ')' )
1412 ++depth;
1413 else if ( ch == '(' && --depth == 0 )
1414 break;
1415 }
1416 }
1417 else if ( ch == '(' )
1418 {
1419 found = true;
1420 break;
1421 }
1422 }
1423
1424 // Cancel any existing call tip.
1425 SendScintilla( SCI_CALLTIPCANCEL );
1426
1427 // Done if there is no new call tip to set.
1428 if ( !found )
1429 return;
1430
1431 int contextStart, lastWordStart;
1432 QStringList context = apiContext( pos, contextStart, lastWordStart );
1433
1434 if ( context.isEmpty() )
1435 return;
1436
1437 // The last word is complete, not partial.
1438 context << QString();
1439
1440 QList<int> ctShifts;
1441 QStringList ctEntries = apis->callTips( context, commas, callTipsStyle(), ctShifts );
1442
1443 int nbEntries = ctEntries.count();
1444
1445 if ( nbEntries == 0 )
1446 return;
1447
1448 const int maxNumberOfCallTips = callTipsVisible();
1449
1450 // Clip to at most maxNumberOfCallTips entries.
1451 if ( maxNumberOfCallTips > 0 && maxNumberOfCallTips < nbEntries )
1452 {
1453 ctEntries = ctEntries.mid( 0, maxNumberOfCallTips );
1454 nbEntries = maxNumberOfCallTips;
1455 }
1456
1457 int shift;
1458 QString ct;
1459
1460 int nbShifts = ctShifts.count();
1461
1462 if ( maxNumberOfCallTips < 0 && nbEntries > 1 )
1463 {
1464 shift = ( nbShifts > 0 ? ctShifts.first() : 0 );
1465 ct = ctEntries[0];
1466 ct.prepend( '\002' );
1467 }
1468 else
1469 {
1470 if ( nbShifts > nbEntries )
1471 nbShifts = nbEntries;
1472
1473 // Find the biggest shift.
1474 shift = 0;
1475
1476 for ( int i = 0; i < nbShifts; ++i )
1477 {
1478 int sh = ctShifts[i];
1479
1480 if ( shift < sh )
1481 shift = sh;
1482 }
1483
1484 ct = ctEntries.join( "\n" );
1485 }
1486
1487 QByteArray ctBa = ct.toLatin1();
1488 const char *cts = ctBa.data();
1489
1490 const int currentWrapPosition = wrapPosition();
1491
1492 if ( currentWrapPosition != -1 )
1493 {
1494 SendScintilla( SCI_CALLTIPSHOW, currentWrapPosition, cts );
1495 }
1496 else
1497 {
1498 // Shift the position of the call tip (to take any context into account) but
1499 // don't go before the start of the line.
1500 if ( shift )
1501 {
1502 int ctmin = static_cast<int>( SendScintilla( SCI_POSITIONFROMLINE, SendScintilla( SCI_LINEFROMPOSITION, ct ) ) );
1503 if ( lastWordStart - shift < ctmin )
1504 lastWordStart = ctmin;
1505 }
1506
1507 int line, index;
1508 lineIndexFromPosition( lastWordStart, &line, &index );
1509 SendScintilla( SCI_CALLTIPSHOW, positionFromLineIndex( line, index ), cts );
1510 }
1511
1512 // Done if there is more than one call tip.
1513 if ( nbEntries > 1 )
1514 return;
1515
1516 // Highlight the current argument.
1517 const char *astart;
1518
1519 if ( commas == 0 )
1520 astart = strchr( cts, '(' );
1521 else
1522 for ( astart = strchr( cts, ',' ); astart && --commas > 0; astart = strchr( astart + 1, ',' ) )
1523 ;
1524
1525 if ( !astart )
1526 return;
1527
1528 astart++;
1529 if ( !*astart )
1530 return;
1531
1532 // The end is at the next comma or unmatched closing parenthesis.
1533 const char *aend;
1534 int depth = 0;
1535
1536 for ( aend = astart; *aend; ++aend )
1537 {
1538 char ch = *aend;
1539
1540 if ( ch == ',' && depth == 0 )
1541 break;
1542 else if ( ch == '(' )
1543 ++depth;
1544 else if ( ch == ')' )
1545 {
1546 if ( depth == 0 )
1547 break;
1548
1549 --depth;
1550 }
1551 }
1552
1553 if ( astart != aend )
1554 SendScintilla( SCI_CALLTIPSETHLT, astart - cts, aend - cts );
1555}
1556
1557
1558// Duplicated from QsciScintilla source code (qsciscintilla.cpp)
1559// Get the "next" character (ie. the one before the current position) in the
1560// current line. The character will be '\0' if there are no more.
1561char QgsCodeEditor::getCharacter( int &pos ) const
1562{
1563 if ( pos <= 0 )
1564 return '\0';
1565
1566 char ch = static_cast<char>( SendScintilla( SCI_GETCHARAT, --pos ) );
1567
1568 // Don't go past the end of the previous line.
1569 if ( ch == '\n' || ch == '\r' )
1570 {
1571 ++pos;
1572 return '\0';
1573 }
1574
1575 return ch;
1576}
MessageLevel
Level for messages This will be used both for message log and message bar in application.
Definition qgis.h:154
@ NoLevel
No level.
Definition qgis.h:159
@ Warning
Warning message.
Definition qgis.h:156
@ Critical
Critical/error message.
Definition qgis.h:157
@ Info
Information message.
Definition qgis.h:155
@ Success
Used for reporting a successful operation.
Definition qgis.h:158
@ CheckSyntax
Language supports syntax checking.
@ Reformat
Language supports automatic code reformatting.
@ ToggleComment
Language supports comment toggling.
ScriptLanguage
Scripting languages.
Definition qgis.h:4328
@ QgisExpression
QGIS expressions.
@ Batch
Windows batch files.
@ JavaScript
JavaScript.
@ Bash
Bash scripts.
@ Unknown
Unknown/other language.
QFlags< ScriptLanguageCapability > ScriptLanguageCapabilities
Script language capabilities.
Definition qgis.h:4363
static QPixmap getThemePixmap(const QString &name, const QColor &foreColor=QColor(), const QColor &backColor=QColor(), int size=16)
Helper to get a theme icon as a pixmap.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QHash< QString, QString > uiThemes()
All themes found in ~/.qgis3/themes folder.
static QString themeName()
Set the active theme to the specified theme.
QgsCodeEditorColorScheme scheme(const QString &id) const
Returns the color scheme with matching id.
Defines a color scheme for use in QgsCodeEditor widgets.
@ TripleSingleQuote
Triple single quote color.
@ CommentBlock
Comment block color.
@ QuotedOperator
Quoted operator color.
@ DoubleQuote
Double quote color.
@ QuotedIdentifier
Quoted identifier color.
@ SelectionForeground
Selection foreground color.
@ CommentLine
Line comment color.
@ FoldIconForeground
Fold icon foreground color.
@ MarginForeground
Margin foreground color.
@ ErrorBackground
Error background color.
@ MatchedBraceBackground
Matched brace background color.
@ SearchMatchBackground
Background color for search matches.
@ IndentationGuide
Indentation guide line.
@ SingleQuote
Single quote color.
@ MarginBackground
Margin background color.
@ SelectionBackground
Selection background color.
@ MatchedBraceForeground
Matched brace foreground color.
@ TripleDoubleQuote
Triple double quote color.
@ FoldIconHalo
Fold icon halo color.
QColor color(ColorRole role) const
Returns the color to use in the editor for the specified role.
A dialog for displaying and managing command history for a QgsCodeEditor widget.
bool eventFilter(QObject *watched, QEvent *event) override
void sessionHistoryCleared()
Emitted when the history of commands run in the current session is cleared.
static const QgsSettingsEntryBool * settingContextHelpHover
void showHistory()
Shows the command history dialog.
int wrapPosition(int line=-1)
Returns the linear position of the start of the last wrapped part for the specified line,...
void setCustomAppearance(const QString &scheme=QString(), const QMap< QgsCodeEditorColorScheme::ColorRole, QColor > &customColors=QMap< QgsCodeEditorColorScheme::ColorRole, QColor >(), const QString &fontFamily=QString(), int fontSize=0)
Sets a custom appearance for the widget, disconnecting it from using the standard appearance taken fr...
int editingTimeoutInterval() const
Returns the timeout (in milliseconds) threshold for the editingTimeout() signal to be emitted after a...
Mode
Code editor modes.
@ OutputDisplay
Read only mode for display of command outputs.
@ ScriptEditor
Standard mode, allows for display and edit of entire scripts.
@ CommandInput
Command input mode.
void reformatCode()
Applies code reformatting to the editor.
virtual void toggleComment()
Toggle comment for the selected text.
void contextMenuEvent(QContextMenuEvent *event) override
void clearPersistentHistory()
Clears the entire persistent history of commands run in the editor.
void removeHistoryCommand(int index)
Removes the command at the specified index from the history of the code editor.
static void setColor(QgsCodeEditorColorScheme::ColorRole role, const QColor &color)
Sets the color to use in the editor for the specified role.
void setHistoryFilePath(const QString &path)
Sets the file path to use for recording and retrieving previously executed commands.
void setLinearSelection(int start, int end)
Convenience function to set the selection using linear indexes.
QStringList history() const
Returns the list of commands previously executed in the editor.
static constexpr int SEARCH_RESULT_INDICATOR
Indicator index for search results.
void keyPressEvent(QKeyEvent *event) override
virtual void moveCursorToStart()
Moves the cursor to the start of the document and scrolls to ensure it is visible.
virtual void populateContextMenu(QMenu *menu)
Called when the context menu for the widget is about to be shown, after it has been fully populated w...
QFlags< Flag > Flags
Flags controlling behavior of code editor.
void persistentHistoryCleared()
Emitted when the persistent history of commands run in the editor is cleared.
virtual void callTip() override
void runCommand(const QString &command, bool skipHistory=false)
Runs a command in the editor.
void setText(const QString &text) override
void setFoldingVisible(bool folding)
Set whether the folding controls are visible in the editor.
virtual Qgis::ScriptLanguageCapabilities languageCapabilities() const
Returns the associated scripting language capabilities.
void setEditingTimeoutInterval(int timeout)
Sets the timeout (in milliseconds) threshold for the editingTimeout() signal to be emitted after an e...
void setInterpreter(QgsCodeInterpreter *newInterpreter)
Sets an attached code interpreter for executing commands when the editor is in the QgsCodeEditor::Mod...
@ FoldingControls
Folding controls.
@ ErrorIndicators
Error indicators.
@ LineNumbers
Line numbers.
void runPostLexerConfigurationTasks()
Performs tasks which must be run after a lexer has been set for the widget.
virtual void showMessage(const QString &title, const QString &message, Qgis::MessageLevel level)
Shows a user facing message (eg a warning message).
int selectionEnd() const
Convenience function to return the end of the selection as a linear index Contrary to the getSelectio...
virtual void initializeLexer()
Called when the dialect specific code lexer needs to be initialized (or reinitialized).
int linearPosition() const
Convenience function to return the cursor position as a linear index.
void setTitle(const QString &title)
Set the widget title.
void setLinearPosition(int position)
Convenience function to set the cursor position as a linear index.
QgsCodeEditor(QWidget *parent=nullptr, const QString &title=QString(), bool folding=false, bool margin=false, QgsCodeEditor::Flags flags=QgsCodeEditor::Flags(), QgsCodeEditor::Mode mode=QgsCodeEditor::Mode::ScriptEditor)
Construct a new code editor.
void clearWarnings()
Clears all warning messages from the editor.
static QFont getMonospaceFont()
Returns the monospaced font to use for code editors.
void showNextCommand()
Shows the next command from the session in the editor.
void focusOutEvent(QFocusEvent *event) override
@ CodeFolding
Indicates that code folding should be enabled for the editor.
@ ImmediatelyUpdateHistory
Indicates that the history file should be immediately updated whenever a command is executed,...
void helpRequested(const QString &word)
Emitted when documentation was requested for the specified word.
bool isCursorOnLastLine() const
Returns true if the cursor is on the last line of the document.
static bool isFixedPitch(const QFont &font)
Returns true if a font is a fixed pitch font.
void updateSoftHistory()
Updates the soft history by storing the current editor text in the history.
void clearSessionHistory()
Clears the history of commands run in the current session.
void insertText(const QString &text)
Insert text at cursor position, or replace any selected text if user has made a selection.
bool writeHistoryFile()
Stores the commands executed in the editor to the persistent history file.
virtual void moveCursorToEnd()
Moves the cursor to the end of the document and scrolls to ensure it is visible.
static QString languageToString(Qgis::ScriptLanguage language)
Returns a user-friendly, translated name of the specified script language.
void setLineNumbersVisible(bool visible)
Sets whether line numbers should be visible in the editor.
void adjustScrollWidth()
Adjust the width of the scroll bar to fit the content.
virtual Qgis::ScriptLanguage language() const
Returns the associated scripting language.
QFont lexerFont() const
Returns the font to use in the lexer.
void toggleLineComments(const QString &commentPrefix)
Toggles comment for selected lines with the given comment prefix.
virtual QString reformatCodeString(const QString &string)
Applies code reformatting to a string and returns the result.
QgsCodeInterpreter * interpreter() const
Returns the attached code interpreter, or nullptr if not set.
bool lineNumbersVisible() const
Returns whether line numbers are visible in the editor.
QColor lexerColor(QgsCodeEditorColorScheme::ColorRole role) const
Returns the color to use in the lexer for the specified role.
bool foldingVisible()
Returns true if the folding controls are visible in the editor.
void showPreviousCommand()
Shows the previous command from the session in the editor.
Q_DECL_DEPRECATED void setMarginVisible(bool margin)
Set margin visible state.
void updatePrompt()
Triggers an update of the interactive prompt part of the editor.
void editingTimeout()
Emitted when either:
static QColor defaultColor(QgsCodeEditorColorScheme::ColorRole role, const QString &theme=QString())
Returns the default color for the specified role.
int selectionStart() const
Convenience function to return the start of the selection as a linear index Contrary to the getSelect...
void addWarning(int lineNumber, const QString &warning)
Adds a warning message and indicator to the specified a lineNumber.
virtual bool checkSyntax()
Applies syntax checking to the editor.
static QColor color(QgsCodeEditorColorScheme::ColorRole role)
Returns the color to use in the editor for the specified role.
An interface for code interpreters.
virtual int execCommandImpl(const QString &command)=0
Pure virtual method for executing commands in the interpreter.
virtual int currentState() const
Returns the current interpreter state.
virtual QString promptForState(int state) const =0
Returns the interactive prompt string to use for the interpreter, given a state.
int exec(const QString &command)
Executes a command in the interpreter.
virtual ~QgsCodeInterpreter()
static void setFontFamily(QFont &font, const QString &family)
Sets the family for a font object.
void optionsChanged()
This signal is emitted whenever the application options have been changed.
static QgsGui * instance()
Returns a pointer to the singleton instance.
Definition qgsgui.cpp:79
static QgsCodeEditorColorSchemeRegistry * codeEditorColorSchemeRegistry()
Returns the global code editor color scheme registry, used for registering the color schemes for QgsC...
Definition qgsgui.cpp:165
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
A boolean settings entry.
Stores settings for use within QGIS.
Definition qgssettings.h:66
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void remove(const QString &key, QgsSettings::Section section=QgsSettings::NoSection)
Removes the setting key and any sub-settings of key in a section.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
static int levenshteinDistance(const QString &string1, const QString &string2, bool caseSensitive=false)
Returns the Levenshtein edit distance between two strings.
static QColor decodeColor(const QString &str)
#define BUILTIN_UNREACHABLE
Definition qgis.h:6896
int findMinimalDistanceIndex(const QString &source, const QString &target)