QGIS API Documentation 3.99.0-Master (f78f5286a64)
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, [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->matches( QKeySequence::StandardKey::HelpContents ) )
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
362bool QgsCodeEditor::event( QEvent *event )
363{
364 if ( event->type() == QEvent::ShortcutOverride )
365 {
366 if ( QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>( event ) )
367 {
368 if ( keyEvent->matches( QKeySequence::StandardKey::HelpContents ) )
369 {
370 // If the user pressed F1, we want to prevent the main help dialog to show
371 // and handle the event in QgsCodeEditor::keyPressEvent
372 keyEvent->accept();
373 return true;
374 }
375 }
376 }
377 return QsciScintilla::event( event );
378}
379
380bool QgsCodeEditor::eventFilter( QObject *watched, QEvent *event )
381{
382#if QSCINTILLA_VERSION < 0x020d03
383 if ( watched == this && event->type() == QEvent::InputMethod )
384 {
385 // swallow input method events, which cause loss of selected text.
386 // See https://sourceforge.net/p/scintilla/bugs/1913/ , which was ported to QScintilla
387 // in version 2.13.3
388 return true;
389 }
390#endif
391
392 return QsciScintilla::eventFilter( watched, event );
393}
394
398
400{
401 if ( mUseDefaultSettings )
402 return color( role );
403
404 if ( !mOverrideColors )
405 {
406 return defaultColor( role, mColorScheme );
407 }
408 else
409 {
410 const QColor color = mCustomColors.value( role );
411 return !color.isValid() ? defaultColor( role ) : color;
412 }
413}
414
416{
417 if ( mUseDefaultSettings )
418 return getMonospaceFont();
419
420 QFont font = QFontDatabase::systemFont( QFontDatabase::FixedFont );
421
422 const QgsSettings settings;
423 if ( !mFontFamily.isEmpty() )
424 QgsFontUtils::setFontFamily( font, mFontFamily );
425
426#ifdef Q_OS_MAC
427 if ( mFontSize > 0 )
428 font.setPointSize( mFontSize );
429 else
430 {
431 // The font size gotten from getMonospaceFont() is too small on Mac
432 font.setPointSize( QLabel().font().pointSize() );
433 }
434#else
435 if ( mFontSize > 0 )
436 font.setPointSize( mFontSize );
437 else
438 {
439 const int fontSize = settings.value( QStringLiteral( "qgis/stylesheet/fontPointSize" ), 10 ).toInt();
440 font.setPointSize( fontSize );
441 }
442#endif
443 font.setBold( false );
444
445 return font;
446}
447
449{
450 updateFolding();
451
454
455 SendScintilla( SCI_MARKERSETFORE, SC_MARKNUM_FOLDEROPEN, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconHalo ) );
456 SendScintilla( SCI_MARKERSETBACK, SC_MARKNUM_FOLDEROPEN, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconForeground ) );
457 SendScintilla( SCI_MARKERSETFORE, SC_MARKNUM_FOLDER, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconHalo ) );
458 SendScintilla( SCI_MARKERSETBACK, SC_MARKNUM_FOLDER, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconForeground ) );
459 SendScintilla( SCI_STYLESETFORE, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) );
460 SendScintilla( SCI_STYLESETBACK, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) );
461
462 SendScintilla( QsciScintilla::SCI_INDICSETSTYLE, SEARCH_RESULT_INDICATOR, QsciScintilla::INDIC_STRAIGHTBOX );
463 SendScintilla( QsciScintilla::SCI_INDICSETFORE, SEARCH_RESULT_INDICATOR, lexerColor( QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground ) );
464 SendScintilla( QsciScintilla::SCI_INDICSETALPHA, SEARCH_RESULT_INDICATOR, 100 );
465 SendScintilla( QsciScintilla::SCI_INDICSETUNDER, SEARCH_RESULT_INDICATOR, true );
466 SendScintilla( QsciScintilla::SCI_INDICGETOUTLINEALPHA, SEARCH_RESULT_INDICATOR, 255 );
467
469 {
470 setCaretLineVisible( false );
471 setLineNumbersVisible( false ); // NO linenumbers for the input line
472 // Margin 1 is used for the '>' prompt (console input)
473 setMarginLineNumbers( 1, true );
474 setMarginWidth( 1, "00000" );
475 setMarginType( 1, QsciScintilla::MarginType::TextMarginRightJustified );
476 setMarginsBackgroundColor( color( QgsCodeEditorColorScheme::ColorRole::Background ) );
477 setEdgeMode( QsciScintilla::EdgeNone );
478 }
479}
480
481void QgsCodeEditor::onLastEditTimeout()
482{
483 mLastEditTimer->stop();
484 emit editingTimeout();
485}
486
487void QgsCodeEditor::setSciWidget()
488{
489 const QFont font = lexerFont();
490 setFont( font );
491
492 setUtf8( true );
493 setCaretLineVisible( true );
494 setCaretLineBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CaretLine ) );
495 setCaretForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Cursor ) );
498
499 setBraceMatching( QsciScintilla::SloppyBraceMatch );
502
503 setLineNumbersVisible( false );
504
505 // temporarily disable folding, will be enabled later if required by updateFolding()
506 setFolding( QsciScintilla::NoFoldStyle );
507 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
508
509 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
510
513 setIndentationGuidesForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginForeground ) );
514 setIndentationGuidesBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginBackground ) );
515 // whether margin will be shown
516 updateFolding();
517 const QColor foldColor = lexerColor( QgsCodeEditorColorScheme::ColorRole::Fold );
518 setFoldMarginColors( foldColor, foldColor );
519 // indentation
520 setAutoIndent( true );
521 setIndentationWidth( 4 );
522 setTabIndents( true );
523 setBackspaceUnindents( true );
524 setTabWidth( 4 );
525 // autocomplete
526 setAutoCompletionThreshold( 2 );
527 setAutoCompletionSource( QsciScintilla::AcsAPIs );
528
529 markerDefine( QgsApplication::getThemePixmap( "console/iconSyntaxErrorConsoleParams.svg", lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), lexerColor( QgsCodeEditorColorScheme::ColorRole::ErrorBackground ), 16 ), MARKER_NUMBER );
530}
531
532void QgsCodeEditor::setTitle( const QString &title )
533{
534 setWindowTitle( title );
535}
536
541
546
548{
549 switch ( language )
550 {
552 return tr( "CSS" );
554 return tr( "Expression" );
556 return tr( "HTML" );
558 return tr( "JavaScript" );
560 return tr( "JSON" );
562 return tr( "Python" );
564 return tr( "R" );
566 return tr( "SQL" );
568 return tr( "Batch" );
570 return tr( "Bash" );
572 return QString();
573 }
575}
576
578{
579 mMargin = margin;
580 if ( margin )
581 {
582 QFont marginFont = lexerFont();
583 marginFont.setPointSize( 10 );
584 setMarginLineNumbers( 0, true );
585 setMarginsFont( marginFont );
586 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), QStringLiteral( "00000" ) );
589 }
590 else
591 {
592 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), 0 );
593 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
594 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
595 }
596}
597
599{
600 if ( visible )
601 {
602 QFont marginFont = lexerFont();
603 marginFont.setPointSize( 10 );
604 setMarginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), true );
605 setMarginsFont( marginFont );
606 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), QStringLiteral( "00000" ) );
609 }
610 else
611 {
612 setMarginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), false );
613 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), 0 );
614 }
615}
616
618{
619 return marginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ) );
620}
621
623{
624 if ( folding )
625 {
627 }
628 else
629 {
630 mFlags &= ~( static_cast<int>( QgsCodeEditor::Flag::CodeFolding ) );
631 }
632 updateFolding();
633}
634
639
640void QgsCodeEditor::updateFolding()
641{
643 {
644 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), "0" );
647 setFolding( QsciScintilla::PlainFoldStyle );
648 }
649 else
650 {
651 setFolding( QsciScintilla::NoFoldStyle );
652 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
653 }
654}
655
656bool QgsCodeEditor::readHistoryFile()
657{
658 if ( mHistoryFilePath.isEmpty() || !QFile::exists( mHistoryFilePath ) )
659 return false;
660
661 QFile file( mHistoryFilePath );
662 if ( file.open( QIODevice::ReadOnly ) )
663 {
664 QTextStream stream( &file );
665#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
666 // Always use UTF-8
667 stream.setCodec( "UTF-8" );
668#endif
669 QString line;
670 while ( !stream.atEnd() )
671 {
672 line = stream.readLine(); // line of text excluding '\n'
673 mHistory.append( line );
674 }
675 syncSoftHistory();
676 return true;
677 }
678
679 return false;
680}
681
682void QgsCodeEditor::syncSoftHistory()
683{
684 mSoftHistory = mHistory;
685 mSoftHistory.append( QString() );
686 mSoftHistoryIndex = mSoftHistory.length() - 1;
687}
688
690{
691 mSoftHistory[mSoftHistoryIndex] = text();
692}
693
694void QgsCodeEditor::updateHistory( const QStringList &commands, bool skipSoftHistory )
695{
696 if ( commands.size() > 1 )
697 {
698 mHistory.append( commands );
699 }
700 else if ( !commands.value( 0 ).isEmpty() )
701 {
702 const QString command = commands.value( 0 );
703 if ( mHistory.empty() || command != mHistory.constLast() )
704 mHistory.append( command );
705 }
706
707 if ( !skipSoftHistory )
708 syncSoftHistory();
709}
710
712{
713}
714
715QString QgsCodeEditor::reformatCodeString( const QString &string )
716{
717 return string;
718}
719
720void QgsCodeEditor::showMessage( const QString &title, const QString &message, Qgis::MessageLevel level )
721{
722 switch ( level )
723 {
724 case Qgis::Info:
725 case Qgis::Success:
726 case Qgis::NoLevel:
727 QMessageBox::information( this, title, message );
728 break;
729
730 case Qgis::Warning:
731 QMessageBox::warning( this, title, message );
732 break;
733
734 case Qgis::Critical:
735 QMessageBox::critical( this, title, message );
736 break;
737 }
738}
739
741{
742 if ( mInterpreter )
743 {
744 const QString prompt = mInterpreter->promptForState( mInterpreter->currentState() );
745 SendScintilla( QsciScintilla::SCI_MARGINSETTEXT, static_cast<uintptr_t>( 0 ), prompt.toUtf8().constData() );
746 }
747}
748
750{
751 return mInterpreter;
752}
753
755{
756 mInterpreter = newInterpreter;
757 updatePrompt();
758}
759
760// Find the source substring index that most closely matches the target string
761int findMinimalDistanceIndex( const QString &source, const QString &target )
762{
763 const int index = std::min( source.length(), target.length() );
764
765 const int d0 = QgsStringUtils::levenshteinDistance( source.left( index ), target );
766 if ( d0 == 0 )
767 return index;
768
769 int refDistanceMore = d0;
770 int refIndexMore = index;
771 if ( index < source.length() - 1 )
772 {
773 while ( true )
774 {
775 const int newDistance = QgsStringUtils::levenshteinDistance( source.left( refIndexMore + 1 ), target );
776 if ( newDistance <= refDistanceMore )
777 {
778 refDistanceMore = newDistance;
779 refIndexMore++;
780 if ( refIndexMore == source.length() - 1 )
781 break;
782 }
783 else
784 {
785 break;
786 }
787 }
788 }
789
790 int refDistanceLess = d0;
791 int refIndexLess = index;
792 if ( index > 0 )
793 {
794 while ( true )
795 {
796 const int newDistance = QgsStringUtils::levenshteinDistance( source.left( refIndexLess - 1 ), target );
797 if ( newDistance <= refDistanceLess )
798 {
799 refDistanceLess = newDistance;
800 refIndexLess--;
801 if ( refIndexLess == 0 )
802 break;
803 }
804 else
805 {
806 break;
807 }
808 }
809 }
810
811 if ( refDistanceMore < refDistanceLess )
812 return refIndexMore;
813 else
814 return refIndexLess;
815}
816
818{
820 return;
821
822 const QString textBeforeCursor = text( 0, linearPosition() );
823 const QString originalText = text();
824 const QString newText = reformatCodeString( originalText );
825
826 if ( originalText == newText )
827 return;
828
829 // try to preserve the cursor position and scroll position
830 const int oldScrollValue = verticalScrollBar()->value();
831 const int linearIndex = findMinimalDistanceIndex( newText, textBeforeCursor );
832
833 beginUndoAction();
834 selectAll();
835 removeSelectedText();
836 insert( newText );
837 setLinearPosition( linearIndex );
838 verticalScrollBar()->setValue( oldScrollValue );
839 endUndoAction();
840}
841
843{
844 return true;
845}
846
850
851void QgsCodeEditor::toggleLineComments( const QString &commentPrefix )
852{
853 if ( isReadOnly() )
854 {
855 return;
856 }
857
858 beginUndoAction();
859 int startLine, startPos, endLine, endPos;
860 if ( hasSelectedText() )
861 {
862 getSelection( &startLine, &startPos, &endLine, &endPos );
863 }
864 else
865 {
866 getCursorPosition( &startLine, &startPos );
867 endLine = startLine;
868 endPos = startPos;
869 }
870
871 // Check comment state and minimum indentation for each selected line
872 bool allEmpty = true;
873 bool allCommented = true;
874 int minIndentation = -1;
875 for ( int line = startLine; line <= endLine; line++ )
876 {
877 const QString stripped = text( line ).trimmed();
878 if ( !stripped.isEmpty() )
879 {
880 allEmpty = false;
881 if ( !stripped.startsWith( commentPrefix ) )
882 {
883 allCommented = false;
884 }
885 if ( minIndentation == -1 || minIndentation > indentation( line ) )
886 {
887 minIndentation = indentation( line );
888 }
889 }
890 }
891
892 // Special case, only empty lines
893 if ( allEmpty )
894 {
895 return;
896 }
897
898 // Selection shift to keep the same selected text after the prefix is added/removed
899 int delta = 0;
900
901 const int prefixLength = static_cast<int>( commentPrefix.length() );
902
903 const bool startLineEmpty = ( text( startLine ).trimmed().isEmpty() );
904 const bool endLineEmpty = ( text( endLine ).trimmed().isEmpty() );
905
906 for ( int line = startLine; line <= endLine; line++ )
907 {
908 const QString stripped = text( line ).trimmed();
909
910 // Empty line
911 if ( stripped.isEmpty() )
912 {
913 continue;
914 }
915
916 if ( !allCommented )
917 {
918 insertAt( commentPrefix + ' ', line, minIndentation );
919 delta = -( prefixLength + 1 );
920 }
921 else
922 {
923 if ( !stripped.startsWith( commentPrefix ) )
924 {
925 continue;
926 }
927 if ( stripped.startsWith( commentPrefix + ' ' ) )
928 {
929 delta = prefixLength + 1;
930 }
931 else
932 {
933 delta = prefixLength;
934 }
935 setSelection( line, indentation( line ), line, indentation( line ) + delta );
936 removeSelectedText();
937 }
938 }
939
940 endUndoAction();
941 setSelection( startLine, startPos - ( startLineEmpty ? 0 : delta ), endLine, endPos - ( endLineEmpty ? 0 : delta ) );
942}
943
945{
946 // A zero width would make setScrollWidth crash
947 long maxWidth = 10;
948
949 // Get the number of lines
950 int lineCount = lines();
951
952 // Loop through all the lines to get the longest one
953 for ( int line = 0; line < lineCount; line++ )
954 {
955 // Get the linear position at the end of the current line
956 const long endLine = SendScintilla( SCI_GETLINEENDPOSITION, line );
957 // Get the x coordinates of the end of the line
958 const long x = SendScintilla( SCI_POINTXFROMPOSITION, 0, endLine );
959 maxWidth = std::max( maxWidth, x );
960 }
961
962 // Use the longest line width as the new scroll width
963 setScrollWidth( static_cast<int>( maxWidth ) );
964}
965
966void QgsCodeEditor::setText( const QString &text )
967{
968 disconnect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
969 QsciScintilla::setText( text );
970 connect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
971 onLastEditTimeout();
973}
974
976{
977 return mLastEditTimer->interval();
978}
979
981{
982 mLastEditTimer->setInterval( timeout );
983}
984
985
986QStringList QgsCodeEditor::history() const
987{
988 return mHistory;
989}
990
991void QgsCodeEditor::runCommand( const QString &command, bool skipHistory )
992{
993 if ( !skipHistory )
994 {
995 updateHistory( { command } );
998 }
999
1000 if ( mInterpreter )
1001 mInterpreter->exec( command );
1002
1003 clear();
1005}
1006
1008{
1009 mHistory.clear();
1010 readHistoryFile();
1011 syncSoftHistory();
1012
1013 emit sessionHistoryCleared();
1014}
1015
1017{
1018 mHistory.clear();
1019
1020 if ( !mHistoryFilePath.isEmpty() && QFile::exists( mHistoryFilePath ) )
1021 {
1022 QFile file( mHistoryFilePath );
1023 file.open( QFile::WriteOnly | QFile::Truncate );
1024 }
1025
1027}
1028
1030{
1031 if ( mHistoryFilePath.isEmpty() )
1032 return false;
1033
1034 QFile f( mHistoryFilePath );
1035 if ( !f.open( QFile::WriteOnly | QIODevice::Truncate ) )
1036 {
1037 return false;
1038 }
1039
1040 QTextStream ts( &f );
1041#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
1042 ts.setCodec( "UTF-8" );
1043#endif
1044 for ( const QString &command : std::as_const( mHistory ) )
1045 {
1046 ts << command + '\n';
1047 }
1048 return true;
1049}
1050
1052{
1053 if ( mSoftHistoryIndex < mSoftHistory.length() - 1 && !mSoftHistory.isEmpty() )
1054 {
1055 mSoftHistoryIndex += 1;
1056 setText( mSoftHistory[mSoftHistoryIndex] );
1058 }
1059}
1060
1062{
1063 if ( mSoftHistoryIndex > 0 && !mSoftHistory.empty() )
1064 {
1065 mSoftHistoryIndex -= 1;
1066 setText( mSoftHistory[mSoftHistoryIndex] );
1068 }
1069}
1070
1072{
1073 QgsCodeEditorHistoryDialog *dialog = new QgsCodeEditorHistoryDialog( this, this );
1074 dialog->setAttribute( Qt::WA_DeleteOnClose );
1075
1076 dialog->show();
1077 dialog->activateWindow();
1078}
1079
1081{
1082 // remove item from the command history (just for the current session)
1083 mHistory.removeAt( index );
1084 mSoftHistory.removeAt( index );
1085 if ( index < mSoftHistoryIndex )
1086 {
1087 mSoftHistoryIndex -= 1;
1088 if ( mSoftHistoryIndex < 0 )
1089 mSoftHistoryIndex = mSoftHistory.length() - 1;
1090 }
1091}
1092
1093void QgsCodeEditor::insertText( const QString &text )
1094{
1095 // Insert the text or replace selected text
1096 if ( hasSelectedText() )
1097 {
1098 replaceSelectedText( text );
1099 }
1100 else
1101 {
1102 int line, index;
1103 getCursorPosition( &line, &index );
1104 insertAt( text, line, index );
1105 setCursorPosition( line, index + text.length() );
1106 }
1107}
1108
1110{
1111 if ( theme.isEmpty() && QgsApplication::themeName() == QLatin1String( "default" ) )
1112 {
1113 // if using default theme, take certain colors from the palette
1114 const QPalette pal = qApp->palette();
1115
1116 switch ( role )
1117 {
1119 return pal.color( QPalette::Highlight );
1121 return pal.color( QPalette::HighlightedText );
1122 default:
1123 break;
1124 }
1125 }
1126 else if ( theme.isEmpty() )
1127 {
1128 // non default theme (e.g. Blend of Gray). Take colors from theme ini file...
1129 const QSettings ini( QgsApplication::uiThemes().value( QgsApplication::themeName() ) + "/qscintilla.ini", QSettings::IniFormat );
1130
1131 static const QMap<QgsCodeEditorColorScheme::ColorRole, QString> sColorRoleToIniKey {
1132 { QgsCodeEditorColorScheme::ColorRole::Default, QStringLiteral( "python/defaultFontColor" ) },
1133 { QgsCodeEditorColorScheme::ColorRole::Keyword, QStringLiteral( "python/keywordFontColor" ) },
1134 { QgsCodeEditorColorScheme::ColorRole::Class, QStringLiteral( "python/classFontColor" ) },
1135 { QgsCodeEditorColorScheme::ColorRole::Method, QStringLiteral( "python/methodFontColor" ) },
1136 { QgsCodeEditorColorScheme::ColorRole::Decoration, QStringLiteral( "python/decoratorFontColor" ) },
1137 { QgsCodeEditorColorScheme::ColorRole::Number, QStringLiteral( "python/numberFontColor" ) },
1138 { QgsCodeEditorColorScheme::ColorRole::Comment, QStringLiteral( "python/commentFontColor" ) },
1139 { QgsCodeEditorColorScheme::ColorRole::CommentLine, QStringLiteral( "sql/commentLineFontColor" ) },
1140 { QgsCodeEditorColorScheme::ColorRole::CommentBlock, QStringLiteral( "python/commentBlockFontColor" ) },
1141 { QgsCodeEditorColorScheme::ColorRole::Background, QStringLiteral( "python/paperBackgroundColor" ) },
1142 { QgsCodeEditorColorScheme::ColorRole::Cursor, QStringLiteral( "cursorColor" ) },
1143 { QgsCodeEditorColorScheme::ColorRole::CaretLine, QStringLiteral( "caretLineColor" ) },
1144 { QgsCodeEditorColorScheme::ColorRole::Operator, QStringLiteral( "sql/operatorFontColor" ) },
1145 { QgsCodeEditorColorScheme::ColorRole::QuotedOperator, QStringLiteral( "sql/QuotedOperatorFontColor" ) },
1146 { QgsCodeEditorColorScheme::ColorRole::Identifier, QStringLiteral( "sql/identifierFontColor" ) },
1147 { QgsCodeEditorColorScheme::ColorRole::QuotedIdentifier, QStringLiteral( "sql/QuotedIdentifierFontColor" ) },
1148 { QgsCodeEditorColorScheme::ColorRole::Tag, QStringLiteral( "html/tagFontColor" ) },
1149 { QgsCodeEditorColorScheme::ColorRole::UnknownTag, QStringLiteral( "html/unknownTagFontColor" ) },
1150 { QgsCodeEditorColorScheme::ColorRole::SingleQuote, QStringLiteral( "sql/singleQuoteFontColor" ) },
1151 { QgsCodeEditorColorScheme::ColorRole::DoubleQuote, QStringLiteral( "sql/doubleQuoteFontColor" ) },
1152 { QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote, QStringLiteral( "python/tripleSingleQuoteFontColor" ) },
1153 { QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote, QStringLiteral( "python/tripleDoubleQuoteFontColor" ) },
1154 { QgsCodeEditorColorScheme::ColorRole::MarginBackground, QStringLiteral( "marginBackgroundColor" ) },
1155 { QgsCodeEditorColorScheme::ColorRole::MarginForeground, QStringLiteral( "marginForegroundColor" ) },
1156 { QgsCodeEditorColorScheme::ColorRole::SelectionBackground, QStringLiteral( "selectionBackgroundColor" ) },
1157 { QgsCodeEditorColorScheme::ColorRole::SelectionForeground, QStringLiteral( "selectionForegroundColor" ) },
1158 { QgsCodeEditorColorScheme::ColorRole::MatchedBraceBackground, QStringLiteral( "matchedBraceBackground" ) },
1159 { QgsCodeEditorColorScheme::ColorRole::MatchedBraceForeground, QStringLiteral( "matchedBraceColor" ) },
1160 { QgsCodeEditorColorScheme::ColorRole::Edge, QStringLiteral( "edgeColor" ) },
1161 { QgsCodeEditorColorScheme::ColorRole::Fold, QStringLiteral( "foldColor" ) },
1162 { QgsCodeEditorColorScheme::ColorRole::Error, QStringLiteral( "stderrFontColor" ) },
1163 { QgsCodeEditorColorScheme::ColorRole::ErrorBackground, QStringLiteral( "stderrBackground" ) },
1164 { QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QStringLiteral( "foldIconForeground" ) },
1165 { QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QStringLiteral( "foldIconHalo" ) },
1166 { QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QStringLiteral( "indentationGuide" ) },
1167 { QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QStringLiteral( "searchMatchBackground" ) },
1168 };
1169
1170 const QgsCodeEditorColorScheme defaultScheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( QStringLiteral( "default" ) );
1171 return QgsSymbolLayerUtils::decodeColor( ini.value( sColorRoleToIniKey.value( role ), defaultScheme.color( role ).name() ).toString() );
1172 }
1173
1174 const QgsCodeEditorColorScheme scheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( theme.isEmpty() ? QStringLiteral( "default" ) : theme );
1175 return scheme.color( role );
1176}
1177
1179{
1180 const QgsSettings settings;
1181 if ( !settings.value( QStringLiteral( "codeEditor/overrideColors" ), false, QgsSettings::Gui ).toBool() )
1182 {
1183 const QString theme = settings.value( QStringLiteral( "codeEditor/colorScheme" ), QString(), QgsSettings::Gui ).toString();
1184 return defaultColor( role, theme );
1185 }
1186 else
1187 {
1188 const QString color = settings.value( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), QString(), QgsSettings::Gui ).toString();
1189 return color.isEmpty() ? defaultColor( role ) : QgsSymbolLayerUtils::decodeColor( color );
1190 }
1191}
1192
1194{
1195 QgsSettings settings;
1196 if ( color.isValid() )
1197 {
1198 settings.setValue( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), color.name(), QgsSettings::Gui );
1199 }
1200 else
1201 {
1202 settings.remove( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), QgsSettings::Gui );
1203 }
1204}
1205
1206// Settings for font and fontsize
1207bool QgsCodeEditor::isFixedPitch( const QFont &font )
1208{
1209 return font.fixedPitch();
1210}
1211
1213{
1214 QFont font = QFontDatabase::systemFont( QFontDatabase::FixedFont );
1215
1216 const QgsSettings settings;
1217 if ( !settings.value( QStringLiteral( "codeEditor/fontfamily" ), QString(), QgsSettings::Gui ).toString().isEmpty() )
1218 QgsFontUtils::setFontFamily( font, settings.value( QStringLiteral( "codeEditor/fontfamily" ), QString(), QgsSettings::Gui ).toString() );
1219
1220 const int fontSize = settings.value( QStringLiteral( "codeEditor/fontsize" ), 0, QgsSettings::Gui ).toInt();
1221
1222#ifdef Q_OS_MAC
1223 if ( fontSize > 0 )
1224 font.setPointSize( fontSize );
1225 else
1226 {
1227 // The font size gotten from getMonospaceFont() is too small on Mac
1228 font.setPointSize( QLabel().font().pointSize() );
1229 }
1230#else
1231 if ( fontSize > 0 )
1232 font.setPointSize( fontSize );
1233 else
1234 {
1235 const int fontSize = settings.value( QStringLiteral( "qgis/stylesheet/fontPointSize" ), 10 ).toInt();
1236 font.setPointSize( fontSize );
1237 }
1238#endif
1239 font.setBold( false );
1240
1241 return font;
1242}
1243
1244void QgsCodeEditor::setCustomAppearance( const QString &scheme, const QMap<QgsCodeEditorColorScheme::ColorRole, QColor> &customColors, const QString &fontFamily, int fontSize )
1245{
1246 mUseDefaultSettings = false;
1247 mOverrideColors = !customColors.isEmpty();
1248 mColorScheme = scheme;
1249 mCustomColors = customColors;
1250 mFontFamily = fontFamily;
1251 mFontSize = fontSize;
1252
1253 setSciWidget();
1255}
1256
1257void QgsCodeEditor::addWarning( const int lineNumber, const QString &warning )
1258{
1259 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), "000" );
1260 markerAdd( lineNumber, MARKER_NUMBER );
1261 QFont font = lexerFont();
1262 font.setItalic( true );
1263 const QsciStyle styleAnn = QsciStyle( -1, QStringLiteral( "Annotation" ), lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), lexerColor( QgsCodeEditorColorScheme::ColorRole::ErrorBackground ), font, true );
1264 annotate( lineNumber, warning, styleAnn );
1265 mWarningLines.push_back( lineNumber );
1266}
1267
1269{
1270 for ( const int line : mWarningLines )
1271 {
1272 markerDelete( line );
1273 clearAnnotations( line );
1274 }
1275 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
1276 mWarningLines.clear();
1277}
1278
1280{
1281 int line = 0;
1282 int index = 0;
1283 getCursorPosition( &line, &index );
1284 return line == lines() - 1;
1285}
1286
1287void QgsCodeEditor::setHistoryFilePath( const QString &path )
1288{
1289 mHistoryFilePath = path;
1290 readHistoryFile();
1291}
1292
1294{
1295 setCursorPosition( 0, 0 );
1296 ensureCursorVisible();
1297 ensureLineVisible( 0 );
1298
1299 if ( mMode == QgsCodeEditor::Mode::CommandInput )
1300 updatePrompt();
1301}
1302
1304{
1305 const int endLine = lines() - 1;
1306 const int endLineLength = lineLength( endLine );
1307 setCursorPosition( endLine, endLineLength );
1308 ensureCursorVisible();
1309 ensureLineVisible( endLine );
1310
1311 if ( mMode == QgsCodeEditor::Mode::CommandInput )
1312 updatePrompt();
1313}
1314
1316{
1317 return static_cast<int>( SendScintilla( SCI_GETCURRENTPOS ) );
1318}
1319
1321{
1322 int line, index;
1323 lineIndexFromPosition( linearIndex, &line, &index );
1324 setCursorPosition( line, index );
1325}
1326
1328{
1329 int startLine, startIndex, _;
1330 getSelection( &startLine, &startIndex, &_, &_ );
1331 if ( startLine == -1 )
1332 {
1333 return linearPosition();
1334 }
1335 return positionFromLineIndex( startLine, startIndex );
1336}
1337
1339{
1340 int endLine, endIndex, _;
1341 getSelection( &_, &_, &endLine, &endIndex );
1342 if ( endLine == -1 )
1343 {
1344 return linearPosition();
1345 }
1346 return positionFromLineIndex( endLine, endIndex );
1347}
1348
1349void QgsCodeEditor::setLinearSelection( int start, int end )
1350{
1351 int startLine, startIndex, endLine, endIndex;
1352 lineIndexFromPosition( start, &startLine, &startIndex );
1353 lineIndexFromPosition( end, &endLine, &endIndex );
1354 setSelection( startLine, startIndex, endLine, endIndex );
1355}
1356
1358
1359int QgsCodeInterpreter::exec( const QString &command )
1360{
1361 mState = execCommandImpl( command );
1362 return mState;
1363}
1364
1365
1367{
1368 // If wrapping is disabled, return -1
1369 if ( wrapMode() == WrapNone )
1370 {
1371 return -1;
1372 }
1373 // Get the current line
1374 if ( line == -1 )
1375 {
1376 int _index;
1377 lineIndexFromPosition( linearPosition(), &line, &_index );
1378 }
1379
1380 // If line isn't wrapped, return -1
1381 if ( SendScintilla( SCI_WRAPCOUNT, line ) <= 1 )
1382 {
1383 return -1;
1384 }
1385
1386 // Get the linear position at the end of the current line
1387 const long endLine = SendScintilla( SCI_GETLINEENDPOSITION, line );
1388 // Get the y coordinates of the start of the last wrapped line
1389 const long y = SendScintilla( SCI_POINTYFROMPOSITION, 0, endLine );
1390 // Return the linear position of the start of the last wrapped line
1391 return static_cast<int>( SendScintilla( SCI_POSITIONFROMPOINT, 0, y ) );
1392}
1393
1394
1395// Adapted from QsciScintilla source code (qsciscintilla.cpp) to handle line wrap
1397{
1398 if ( callTipsStyle() == CallTipsNone || lexer() == nullptr )
1399 {
1400 return;
1401 }
1402
1403 QsciAbstractAPIs *apis = lexer()->apis();
1404
1405 if ( !apis )
1406 return;
1407
1408 int pos, commas = 0;
1409 bool found = false;
1410 char ch;
1411
1412 pos = linearPosition();
1413
1414 // Move backwards through the line looking for the start of the current
1415 // call tip and working out which argument it is.
1416 while ( ( ch = getCharacter( pos ) ) != '\0' )
1417 {
1418 if ( ch == ',' )
1419 ++commas;
1420 else if ( ch == ')' )
1421 {
1422 int depth = 1;
1423
1424 // Ignore everything back to the start of the corresponding
1425 // parenthesis.
1426 while ( ( ch = getCharacter( pos ) ) != '\0' )
1427 {
1428 if ( ch == ')' )
1429 ++depth;
1430 else if ( ch == '(' && --depth == 0 )
1431 break;
1432 }
1433 }
1434 else if ( ch == '(' )
1435 {
1436 found = true;
1437 break;
1438 }
1439 }
1440
1441 // Cancel any existing call tip.
1442 SendScintilla( SCI_CALLTIPCANCEL );
1443
1444 // Done if there is no new call tip to set.
1445 if ( !found )
1446 return;
1447
1448 int contextStart, lastWordStart;
1449 QStringList context = apiContext( pos, contextStart, lastWordStart );
1450
1451 if ( context.isEmpty() )
1452 return;
1453
1454 // The last word is complete, not partial.
1455 context << QString();
1456
1457 QList<int> ctShifts;
1458 QStringList ctEntries = apis->callTips( context, commas, callTipsStyle(), ctShifts );
1459
1460 int nbEntries = ctEntries.count();
1461
1462 if ( nbEntries == 0 )
1463 return;
1464
1465 const int maxNumberOfCallTips = callTipsVisible();
1466
1467 // Clip to at most maxNumberOfCallTips entries.
1468 if ( maxNumberOfCallTips > 0 && maxNumberOfCallTips < nbEntries )
1469 {
1470 ctEntries = ctEntries.mid( 0, maxNumberOfCallTips );
1471 nbEntries = maxNumberOfCallTips;
1472 }
1473
1474 int shift;
1475 QString ct;
1476
1477 int nbShifts = ctShifts.count();
1478
1479 if ( maxNumberOfCallTips < 0 && nbEntries > 1 )
1480 {
1481 shift = ( nbShifts > 0 ? ctShifts.first() : 0 );
1482 ct = ctEntries[0];
1483 ct.prepend( '\002' );
1484 }
1485 else
1486 {
1487 if ( nbShifts > nbEntries )
1488 nbShifts = nbEntries;
1489
1490 // Find the biggest shift.
1491 shift = 0;
1492
1493 for ( int i = 0; i < nbShifts; ++i )
1494 {
1495 int sh = ctShifts[i];
1496
1497 if ( shift < sh )
1498 shift = sh;
1499 }
1500
1501 ct = ctEntries.join( "\n" );
1502 }
1503
1504 QByteArray ctBa = ct.toLatin1();
1505 const char *cts = ctBa.data();
1506
1507 const int currentWrapPosition = wrapPosition();
1508
1509 if ( currentWrapPosition != -1 )
1510 {
1511 SendScintilla( SCI_CALLTIPSHOW, currentWrapPosition, cts );
1512 }
1513 else
1514 {
1515 // Shift the position of the call tip (to take any context into account) but
1516 // don't go before the start of the line.
1517 if ( shift )
1518 {
1519 int ctmin = static_cast<int>( SendScintilla( SCI_POSITIONFROMLINE, SendScintilla( SCI_LINEFROMPOSITION, ct ) ) );
1520 if ( lastWordStart - shift < ctmin )
1521 lastWordStart = ctmin;
1522 }
1523
1524 int line, index;
1525 lineIndexFromPosition( lastWordStart, &line, &index );
1526 SendScintilla( SCI_CALLTIPSHOW, positionFromLineIndex( line, index ), cts );
1527 }
1528
1529 // Done if there is more than one call tip.
1530 if ( nbEntries > 1 )
1531 return;
1532
1533 // Highlight the current argument.
1534 const char *astart;
1535
1536 if ( commas == 0 )
1537 astart = strchr( cts, '(' );
1538 else
1539 for ( astart = strchr( cts, ',' ); astart && --commas > 0; astart = strchr( astart + 1, ',' ) )
1540 ;
1541
1542 if ( !astart )
1543 return;
1544
1545 astart++;
1546 if ( !*astart )
1547 return;
1548
1549 // The end is at the next comma or unmatched closing parenthesis.
1550 const char *aend;
1551 int depth = 0;
1552
1553 for ( aend = astart; *aend; ++aend )
1554 {
1555 char ch = *aend;
1556
1557 if ( ch == ',' && depth == 0 )
1558 break;
1559 else if ( ch == '(' )
1560 ++depth;
1561 else if ( ch == ')' )
1562 {
1563 if ( depth == 0 )
1564 break;
1565
1566 --depth;
1567 }
1568 }
1569
1570 if ( astart != aend )
1571 SendScintilla( SCI_CALLTIPSETHLT, astart - cts, aend - cts );
1572}
1573
1574
1575// Duplicated from QsciScintilla source code (qsciscintilla.cpp)
1576// Get the "next" character (ie. the one before the current position) in the
1577// current line. The character will be '\0' if there are no more.
1578char QgsCodeEditor::getCharacter( int &pos ) const
1579{
1580 if ( pos <= 0 )
1581 return '\0';
1582
1583 char ch = static_cast<char>( SendScintilla( SCI_GETCHARAT, --pos ) );
1584
1585 // Don't go past the end of the previous line.
1586 if ( ch == '\n' || ch == '\r' )
1587 {
1588 ++pos;
1589 return '\0';
1590 }
1591
1592 return ch;
1593}
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:4378
@ 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:4413
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.
bool event(QEvent *event) override
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:65
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:7022
int findMinimalDistanceIndex(const QString &source, const QString &target)