QGIS API Documentation 4.1.0-Master (376402f9aeb)
Loading...
Searching...
No Matches
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 "qgscodeeditor.h"
18
19#include "qgsapplication.h"
23#include "qgsfontutils.h"
24#include "qgsgui.h"
25#include "qgssettings.h"
27#include "qgsshortcutsmanager.h"
28#include "qgsstringutils.h"
29#include "qgssymbollayerutils.h"
30
31#include <QClipboard>
32#include <QDebug>
33#include <QFileInfo>
34#include <QFocusEvent>
35#include <QFont>
36#include <QFontDatabase>
37#include <QLabel>
38#include <QMenu>
39#include <QMessageBox>
40#include <QScrollBar>
41#include <QString>
42#include <QWidget>
43#include <Qsci/qscilexer.h>
44#include <Qsci/qscistyle.h>
45
46#include "moc_qgscodeeditor.cpp"
47
48using namespace Qt::StringLiterals;
49
51const QgsSettingsEntryBool *QgsCodeEditor::settingContextHelpHover = new QgsSettingsEntryBool( u"context-help-hover"_s, sTreeCodeEditor, false, u"Whether the context help should works on hovered words"_s );
52const QgsSettingsEntryString *QgsCodeEditor::settingFontFamily = new QgsSettingsEntryString( u"font-family"_s, sTreeCodeEditor, QString(), u"Code editor font family override"_s );
53const QgsSettingsEntryInteger *QgsCodeEditor::settingFontSize = new QgsSettingsEntryInteger( u"font-size"_s, sTreeCodeEditor, 0, u"Code editor font size override (0 for default)"_s );
55
56
57QMap<QgsCodeEditorColorScheme::ColorRole, QString> QgsCodeEditor::sColorRoleToSettingsKey {
58 { QgsCodeEditorColorScheme::ColorRole::Default, u"defaultFontColor"_s },
59 { QgsCodeEditorColorScheme::ColorRole::Keyword, u"keywordFontColor"_s },
60 { QgsCodeEditorColorScheme::ColorRole::Class, u"classFontColor"_s },
61 { QgsCodeEditorColorScheme::ColorRole::Method, u"methodFontColor"_s },
62 { QgsCodeEditorColorScheme::ColorRole::Decoration, u"decoratorFontColor"_s },
63 { QgsCodeEditorColorScheme::ColorRole::Number, u"numberFontColor"_s },
64 { QgsCodeEditorColorScheme::ColorRole::Comment, u"commentFontColor"_s },
65 { QgsCodeEditorColorScheme::ColorRole::CommentLine, u"commentLineFontColor"_s },
66 { QgsCodeEditorColorScheme::ColorRole::CommentBlock, u"commentBlockFontColor"_s },
67 { QgsCodeEditorColorScheme::ColorRole::Background, u"paperBackgroundColor"_s },
70 { QgsCodeEditorColorScheme::ColorRole::Operator, u"operatorFontColor"_s },
71 { QgsCodeEditorColorScheme::ColorRole::QuotedOperator, u"quotedOperatorFontColor"_s },
72 { QgsCodeEditorColorScheme::ColorRole::Identifier, u"identifierFontColor"_s },
73 { QgsCodeEditorColorScheme::ColorRole::QuotedIdentifier, u"quotedIdentifierFontColor"_s },
74 { QgsCodeEditorColorScheme::ColorRole::Tag, u"tagFontColor"_s },
75 { QgsCodeEditorColorScheme::ColorRole::UnknownTag, u"unknownTagFontColor"_s },
76 { QgsCodeEditorColorScheme::ColorRole::SingleQuote, u"singleQuoteFontColor"_s },
77 { QgsCodeEditorColorScheme::ColorRole::DoubleQuote, u"doubleQuoteFontColor"_s },
78 { QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote, u"tripleSingleQuoteFontColor"_s },
79 { QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote, u"tripleDoubleQuoteFontColor"_s },
80 { QgsCodeEditorColorScheme::ColorRole::MarginBackground, u"marginBackgroundColor"_s },
81 { QgsCodeEditorColorScheme::ColorRole::MarginForeground, u"marginForegroundColor"_s },
82 { QgsCodeEditorColorScheme::ColorRole::SelectionBackground, u"selectionBackgroundColor"_s },
83 { QgsCodeEditorColorScheme::ColorRole::SelectionForeground, u"selectionForegroundColor"_s },
88 { QgsCodeEditorColorScheme::ColorRole::Error, u"stderrFontColor"_s },
89 { QgsCodeEditorColorScheme::ColorRole::ErrorBackground, u"stderrBackgroundColor"_s },
94};
95
96QgsCodeEditor::QgsCodeEditor( QWidget *parent, const QString &title, bool folding, bool margin, QgsCodeEditor::Flags flags, QgsCodeEditor::Mode mode )
97 : QsciScintilla( parent )
98 , mWidgetTitle( title )
99 , mMargin( margin )
100 , mFlags( flags )
101 , mMode( mode )
102{
103 if ( !parent && mWidgetTitle.isEmpty() )
104 {
105 setWindowTitle( u"Text Editor"_s );
106 }
107 else
108 {
109 setWindowTitle( mWidgetTitle );
110 }
111
112 if ( folding )
114
115 mSoftHistory.append( QString() );
116
117 setSciWidget();
118 setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded );
119
120 SendScintilla( SCI_STYLESETBACK, STYLE_DEFAULT, lexerColor( QgsCodeEditorColorScheme::ColorRole::Background ) );
121 SendScintilla( SCI_STYLESETFORE, STYLE_DEFAULT, lexerColor( QgsCodeEditorColorScheme::ColorRole::Default ) );
122
123 SendScintilla( SCI_SETADDITIONALSELECTIONTYPING, 1 );
124 SendScintilla( SCI_SETMULTIPASTE, 1 );
125 SendScintilla( SCI_SETVIRTUALSPACEOPTIONS, SCVS_RECTANGULARSELECTION );
126
127 SendScintilla( SCI_SETMARGINTYPEN, static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), SC_MARGIN_SYMBOL );
128 SendScintilla( SCI_SETMARGINMASKN, static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 1 << MARKER_NUMBER );
129 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
130
131 setAnnotationDisplay( QsciScintilla::AnnotationBoxed );
132
133 connect( QgsGui::instance(), &QgsGui::optionsChanged, this, [this] {
134 setSciWidget();
136 } );
137
138 switch ( mMode )
139 {
141 break;
142
144 {
145 // Don't want to see the horizontal scrollbar at all
146 SendScintilla( QsciScintilla::SCI_SETHSCROLLBAR, 0 );
147
148 setWrapMode( QsciScintilla::WrapCharacter );
149 break;
150 }
151
153 {
154 // Don't want to see the horizontal scrollbar at all
155 SendScintilla( QsciScintilla::SCI_SETHSCROLLBAR, 0 );
156
157 setWrapMode( QsciScintilla::WrapCharacter );
158 SendScintilla( QsciScintilla::SCI_EMPTYUNDOBUFFER );
159 break;
160 }
161 }
162
163#if QSCINTILLA_VERSION < 0x020d03
164 installEventFilter( this );
165#endif
166
167 mLastEditTimer = new QTimer( this );
168 mLastEditTimer->setSingleShot( true );
169 mLastEditTimer->setInterval( 1000 );
170 connect( mLastEditTimer, &QTimer::timeout, this, &QgsCodeEditor::onLastEditTimeout );
171 connect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
172
173 SendScintilla( SCI_STYLECLEARALL );
174}
175
176// Workaround a bug in QScintilla 2.8.X
178{
179#if QSCINTILLA_VERSION >= 0x020800 && QSCINTILLA_VERSION < 0x020900
180 if ( event->reason() != Qt::ActiveWindowFocusReason )
181 {
182 /* There's a bug in all QScintilla 2.8.X, where
183 a focus out event that is not due to ActiveWindowFocusReason doesn't
184 lead to the bliking caret being disabled. The hack consists in making
185 QsciScintilla::focusOutEvent believe that the event is a ActiveWindowFocusReason
186 The bug was fixed in 2.9 per:
187 2015-04-14 Phil Thompson <[email protected]>
188
189 * qt/qsciscintillabase.cpp:
190 Fixed a problem notifying when focus is lost to another application
191 widget.
192 [41734678234e]
193 */
194 QFocusEvent newFocusEvent( QEvent::FocusOut, Qt::ActiveWindowFocusReason );
195 QsciScintilla::focusOutEvent( &newFocusEvent );
196 }
197 else
198#endif
199 {
200 QsciScintilla::focusOutEvent( event );
201 }
202 onLastEditTimeout();
203}
204
205// This workaround a likely bug in QScintilla. The ESC key should not be consumned
206// by the main entry, so that the default behavior (Dialog closing) can trigger,
207// but only is the auto-completion suggestion list isn't displayed
209{
210 if ( isListActive() )
211 {
212 QsciScintilla::keyPressEvent( event );
213 return;
214 }
215
216 if ( event->key() == Qt::Key_Escape )
217 {
218 // Shortcut QScintilla and redirect the event to the QWidget handler
219 QWidget::keyPressEvent( event ); // NOLINT(bugprone-parent-virtual-call) clazy:exclude=skipped-base-method
220 return;
221 }
222
223 if ( event->matches( QKeySequence::StandardKey::HelpContents ) )
224 {
225 // Check if some text is selected
226 QString text = selectedText();
227
228 // Check if mouse is hovering over a word
229 if ( text.isEmpty() && settingContextHelpHover->value() )
230 {
231 text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
232 }
233
234 // Otherwise, check if there is a word at the current text cursor position
235 if ( text.isEmpty() )
236 {
237 int line, index;
238 getCursorPosition( &line, &index );
239 text = wordAtLineIndex( line, index );
240 }
241 emit helpRequested( text );
242 return;
243 }
244
245
247 {
248 switch ( event->key() )
249 {
250 case Qt::Key_Return:
251 case Qt::Key_Enter:
252 runCommand( text() );
253 updatePrompt();
254 return;
255
256 case Qt::Key_Down:
258 updatePrompt();
259 return;
260
261 case Qt::Key_Up:
263 updatePrompt();
264 return;
265
266 default:
267 break;
268 }
269 }
270
271 // check for reformat code sequence
273 if ( !isReadOnly() && canReformat )
274 {
276 if ( !reformatCodeSequence.isEmpty() && reformatCodeSequence.matches( event->key() | event->modifiers() ) )
277 {
278 event->accept();
279 reformatCode();
280 return;
281 }
282 }
283
284 // Check for toggle comment sequence
286 if ( !isReadOnly() && canToggle )
287 {
289 if ( !toggleCommentCodeSequence.isEmpty() && toggleCommentCodeSequence.matches( event->key() | event->modifiers() ) )
290 {
291 event->accept();
293 return;
294 }
295 }
296
297 QsciScintilla::keyPressEvent( event );
298
299 // Update calltips unless event is autorepeat
300 if ( !event->isAutoRepeat() )
301 {
302 callTip();
303 }
304}
305
306void QgsCodeEditor::contextMenuEvent( QContextMenuEvent *event )
307{
308 switch ( mMode )
309 {
311 {
312 QMenu *menu = createStandardContextMenu();
313 menu->setAttribute( Qt::WA_DeleteOnClose );
314
316 {
317 menu->addSeparator();
318 }
319
321 {
322 QAction *reformatAction = new QAction( tr( "Reformat Code" ), menu );
323 reformatAction->setShortcut( QgsGui::shortcutsManager()->sequenceForCommonAction( QgsShortcutsManager::CommonAction::CodeReformat ) );
324 reformatAction->setIcon( QgsApplication::getThemeIcon( u"console/iconFormatCode.svg"_s ) );
325 reformatAction->setEnabled( !isReadOnly() );
326 connect( reformatAction, &QAction::triggered, this, &QgsCodeEditor::reformatCode );
327 menu->addAction( reformatAction );
328 }
329
331 {
332 QAction *syntaxCheckAction = new QAction( tr( "Check Syntax" ), menu );
333 syntaxCheckAction->setIcon( QgsApplication::getThemeIcon( u"console/iconSyntaxErrorConsole.svg"_s ) );
334 connect( syntaxCheckAction, &QAction::triggered, this, &QgsCodeEditor::checkSyntax );
335 menu->addAction( syntaxCheckAction );
336 }
337
339 {
340 QAction *toggleCommentAction = new QAction( tr( "Toggle Comment" ), menu );
341 toggleCommentAction->setShortcut( QgsGui::shortcutsManager()->sequenceForCommonAction( QgsShortcutsManager::CommonAction::CodeToggleComment ) );
342 toggleCommentAction->setIcon( QgsApplication::getThemeIcon( u"console/iconCommentEditorConsole.svg"_s, palette().color( QPalette::ColorRole::WindowText ) ) );
343 toggleCommentAction->setEnabled( !isReadOnly() );
344 connect( toggleCommentAction, &QAction::triggered, this, &QgsCodeEditor::toggleComment );
345 menu->addAction( toggleCommentAction );
346 }
347
348 populateContextMenu( menu );
349
350 menu->exec( mapToGlobal( event->pos() ) );
351 break;
352 }
353
355 {
356 QMenu *menu = new QMenu( this );
357 QMenu *historySubMenu = new QMenu( tr( "Command History" ), menu );
358
359 historySubMenu->addAction( tr( "Show" ), this, &QgsCodeEditor::showHistory, u"Ctrl+Shift+SPACE"_s );
360 historySubMenu->addAction( tr( "Clear File" ), this, &QgsCodeEditor::clearPersistentHistory );
361 historySubMenu->addAction( tr( "Clear Session" ), this, &QgsCodeEditor::clearSessionHistory );
362
363 menu->addMenu( historySubMenu );
364 menu->addSeparator();
365
366 QAction *copyAction = menu->addAction( QgsApplication::getThemeIcon( "mActionEditCopy.svg" ), tr( "Copy" ), this, &QgsCodeEditor::copy, QKeySequence::Copy );
367 QAction *pasteAction = menu->addAction( QgsApplication::getThemeIcon( "mActionEditPaste.svg" ), tr( "Paste" ), this, &QgsCodeEditor::paste, QKeySequence::Paste );
368 copyAction->setEnabled( hasSelectedText() );
369 pasteAction->setEnabled( !QApplication::clipboard()->text().isEmpty() );
370
371 populateContextMenu( menu );
372
373 menu->exec( mapToGlobal( event->pos() ) );
374 break;
375 }
376
378 QsciScintilla::contextMenuEvent( event );
379 break;
380 }
381}
382
384{
385 if ( event->type() == QEvent::ShortcutOverride )
386 {
387 if ( QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>( event ) )
388 {
389 if ( keyEvent->matches( QKeySequence::StandardKey::HelpContents ) )
390 {
391 // If the user pressed F1, we want to prevent the main help dialog to show
392 // and handle the event in QgsCodeEditor::keyPressEvent
393 keyEvent->accept();
394 return true;
395 }
396 }
397 }
398 return QsciScintilla::event( event );
399}
400
401bool QgsCodeEditor::eventFilter( QObject *watched, QEvent *event )
402{
403#if QSCINTILLA_VERSION < 0x020d03
404 if ( watched == this && event->type() == QEvent::InputMethod )
405 {
406 // swallow input method events, which cause loss of selected text.
407 // See https://sourceforge.net/p/scintilla/bugs/1913/ , which was ported to QScintilla
408 // in version 2.13.3
409 return true;
410 }
411#endif
412
413 return QsciScintilla::eventFilter( watched, event );
414}
415
418
420{
421 if ( mUseDefaultSettings )
422 return color( role );
423
424 if ( !mOverrideColors )
425 {
426 return defaultColor( role, mColorScheme );
427 }
428 else
429 {
430 const QColor color = mCustomColors.value( role );
431 return !color.isValid() ? defaultColor( role ) : color;
432 }
433}
434
436{
437 if ( mUseDefaultSettings )
438 return getMonospaceFont();
439
440 QFont font = QFontDatabase::systemFont( QFontDatabase::FixedFont );
441
442 const QgsSettings settings;
443 if ( !mFontFamily.isEmpty() )
444 QgsFontUtils::setFontFamily( font, mFontFamily );
445
446#ifdef Q_OS_MAC
447 if ( mFontSize > 0 )
448 font.setPointSize( mFontSize );
449 else
450 {
451 // The font size gotten from getMonospaceFont() is too small on Mac
452 font.setPointSize( QLabel().font().pointSize() );
453 }
454#else
455 if ( mFontSize > 0 )
456 font.setPointSize( mFontSize );
457 else
458 {
459 const int fontSize = settings.value( u"qgis/stylesheet/fontPointSize"_s, 10 ).toInt();
460 font.setPointSize( fontSize );
461 }
462#endif
463 font.setBold( false );
464
465 return font;
466}
467
469{
470 updateFolding();
471
474
475 SendScintilla( SCI_MARKERSETFORE, SC_MARKNUM_FOLDEROPEN, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconHalo ) );
476 SendScintilla( SCI_MARKERSETBACK, SC_MARKNUM_FOLDEROPEN, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconForeground ) );
477 SendScintilla( SCI_MARKERSETFORE, SC_MARKNUM_FOLDER, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconHalo ) );
478 SendScintilla( SCI_MARKERSETBACK, SC_MARKNUM_FOLDER, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconForeground ) );
479 SendScintilla( SCI_STYLESETFORE, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) );
480 SendScintilla( SCI_STYLESETBACK, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) );
481
482 SendScintilla( QsciScintilla::SCI_INDICSETSTYLE, SEARCH_RESULT_INDICATOR, QsciScintilla::INDIC_STRAIGHTBOX );
483 SendScintilla( QsciScintilla::SCI_INDICSETFORE, SEARCH_RESULT_INDICATOR, lexerColor( QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground ) );
484 SendScintilla( QsciScintilla::SCI_INDICSETALPHA, SEARCH_RESULT_INDICATOR, 100 );
485 SendScintilla( QsciScintilla::SCI_INDICSETUNDER, SEARCH_RESULT_INDICATOR, true );
486 SendScintilla( QsciScintilla::SCI_INDICGETOUTLINEALPHA, SEARCH_RESULT_INDICATOR, 255 );
487
489 {
490 setCaretLineVisible( false );
491 setLineNumbersVisible( false ); // NO linenumbers for the input line
492 // Margin 1 is used for the '>' prompt (console input)
493 setMarginLineNumbers( 1, true );
494 setMarginWidth( 1, "00000" );
495 setMarginType( 1, QsciScintilla::MarginType::TextMarginRightJustified );
496 setMarginsBackgroundColor( color( QgsCodeEditorColorScheme::ColorRole::Background ) );
497 setEdgeMode( QsciScintilla::EdgeNone );
498 }
499}
500
501void QgsCodeEditor::onLastEditTimeout()
502{
503 mLastEditTimer->stop();
504 emit editingTimeout();
505}
506
507void QgsCodeEditor::setSciWidget()
508{
509 const QFont font = lexerFont();
510 setFont( font );
511
512 setUtf8( true );
513 setCaretLineVisible( true );
514 setCaretLineBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CaretLine ) );
515 setCaretForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Cursor ) );
518
519 setBraceMatching( QsciScintilla::SloppyBraceMatch );
522
523 setLineNumbersVisible( false );
524
525 // temporarily disable folding, will be enabled later if required by updateFolding()
526 setFolding( QsciScintilla::NoFoldStyle );
527 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
528
529 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
530
533 setIndentationGuidesForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginForeground ) );
534 setIndentationGuidesBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginBackground ) );
535 // whether margin will be shown
536 updateFolding();
537 const QColor foldColor = lexerColor( QgsCodeEditorColorScheme::ColorRole::Fold );
538 setFoldMarginColors( foldColor, foldColor );
539 // indentation
540 setAutoIndent( true );
541 setIndentationWidth( 4 );
542 setTabIndents( true );
543 setBackspaceUnindents( true );
544 setTabWidth( 4 );
545 // autocomplete
546 setAutoCompletionThreshold( 2 );
547 setAutoCompletionSource( QsciScintilla::AcsAPIs );
548
549 markerDefine( QgsApplication::getThemePixmap( "console/iconSyntaxErrorConsoleParams.svg", lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), lexerColor( QgsCodeEditorColorScheme::ColorRole::ErrorBackground ), 16 ), MARKER_NUMBER );
550}
551
552void QgsCodeEditor::setTitle( const QString &title )
553{
554 setWindowTitle( title );
555}
556
561
566
568{
569 switch ( language )
570 {
572 return tr( "CSS" );
574 return tr( "Expression" );
576 return tr( "HTML" );
578 return tr( "JavaScript" );
580 return tr( "JSON" );
582 return tr( "Python" );
584 return tr( "R" );
586 return tr( "SQL" );
588 return tr( "Batch" );
590 return tr( "Bash" );
592 return QString();
593 }
595}
596
598{
599 mMargin = margin;
600 if ( margin )
601 {
602 QFont marginFont = lexerFont();
603 marginFont.setPointSize( 10 );
604 setMarginLineNumbers( 0, true );
605 setMarginsFont( marginFont );
606 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), u"00000"_s );
609 }
610 else
611 {
612 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), 0 );
613 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
614 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
615 }
616}
617
619{
620 if ( visible )
621 {
622 QFont marginFont = lexerFont();
623 marginFont.setPointSize( 10 );
624 setMarginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), true );
625 setMarginsFont( marginFont );
626 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), u"00000"_s );
629 }
630 else
631 {
632 setMarginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), false );
633 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ), 0 );
634 }
635}
636
638{
639 return marginLineNumbers( static_cast<int>( QgsCodeEditor::MarginRole::LineNumbers ) );
640}
641
643{
644 if ( folding )
645 {
647 }
648 else
649 {
650 mFlags &= ~( static_cast<int>( QgsCodeEditor::Flag::CodeFolding ) );
651 }
652 updateFolding();
653}
654
659
660void QgsCodeEditor::updateFolding()
661{
663 {
664 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), "0" );
667 setFolding( QsciScintilla::PlainFoldStyle );
668 }
669 else
670 {
671 setFolding( QsciScintilla::NoFoldStyle );
672 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::FoldingControls ), 0 );
673 }
674}
675
676bool QgsCodeEditor::readHistoryFile()
677{
678 if ( mHistoryFilePath.isEmpty() || !QFile::exists( mHistoryFilePath ) )
679 return false;
680
681 QFile file( mHistoryFilePath );
682 if ( file.open( QIODevice::ReadOnly ) )
683 {
684 QTextStream stream( &file );
685 QString line;
686 while ( !stream.atEnd() )
687 {
688 line = stream.readLine(); // line of text excluding '\n'
689 mHistory.append( line );
690 }
691 syncSoftHistory();
692 return true;
693 }
694
695 return false;
696}
697
698void QgsCodeEditor::syncSoftHistory()
699{
700 mSoftHistory = mHistory;
701 mSoftHistory.append( QString() );
702 mSoftHistoryIndex = mSoftHistory.length() - 1;
703}
704
706{
707 mSoftHistory[mSoftHistoryIndex] = text();
708}
709
710void QgsCodeEditor::updateHistory( const QStringList &commands, bool skipSoftHistory )
711{
712 if ( commands.size() > 1 )
713 {
714 mHistory.append( commands );
715 }
716 else if ( !commands.value( 0 ).isEmpty() )
717 {
718 const QString command = commands.value( 0 );
719 if ( mHistory.empty() || command != mHistory.constLast() )
720 mHistory.append( command );
721 }
722
723 if ( !skipSoftHistory )
724 syncSoftHistory();
725}
726
729
730QString QgsCodeEditor::reformatCodeString( const QString &string )
731{
732 return string;
733}
734
735void QgsCodeEditor::showMessage( const QString &title, const QString &message, Qgis::MessageLevel level )
736{
737 switch ( level )
738 {
739 case Qgis::Info:
740 case Qgis::Success:
741 case Qgis::NoLevel:
742 QMessageBox::information( this, title, message );
743 break;
744
745 case Qgis::Warning:
746 QMessageBox::warning( this, title, message );
747 break;
748
749 case Qgis::Critical:
750 QMessageBox::critical( this, title, message );
751 break;
752 }
753}
754
756{
757 if ( mInterpreter )
758 {
759 const QString prompt = mInterpreter->promptForState( mInterpreter->currentState() );
760 SendScintilla( QsciScintilla::SCI_MARGINSETTEXT, static_cast<uintptr_t>( 0 ), prompt.toUtf8().constData() );
761 }
762}
763
765{
766 return mInterpreter;
767}
768
770{
771 mInterpreter = newInterpreter;
772 updatePrompt();
773}
774
775// Find the source substring index that most closely matches the target string
776int findMinimalDistanceIndex( const QString &source, const QString &target )
777{
778 const int index = std::min( source.length(), target.length() );
779
780 const int d0 = QgsStringUtils::levenshteinDistance( source.left( index ), target );
781 if ( d0 == 0 )
782 return index;
783
784 int refDistanceMore = d0;
785 int refIndexMore = index;
786 if ( index < source.length() - 1 )
787 {
788 while ( true )
789 {
790 const int newDistance = QgsStringUtils::levenshteinDistance( source.left( refIndexMore + 1 ), target );
791 if ( newDistance <= refDistanceMore )
792 {
793 refDistanceMore = newDistance;
794 refIndexMore++;
795 if ( refIndexMore == source.length() - 1 )
796 break;
797 }
798 else
799 {
800 break;
801 }
802 }
803 }
804
805 int refDistanceLess = d0;
806 int refIndexLess = index;
807 if ( index > 0 )
808 {
809 while ( true )
810 {
811 const int newDistance = QgsStringUtils::levenshteinDistance( source.left( refIndexLess - 1 ), target );
812 if ( newDistance <= refDistanceLess )
813 {
814 refDistanceLess = newDistance;
815 refIndexLess--;
816 if ( refIndexLess == 0 )
817 break;
818 }
819 else
820 {
821 break;
822 }
823 }
824 }
825
826 if ( refDistanceMore < refDistanceLess )
827 return refIndexMore;
828 else
829 return refIndexLess;
830}
831
833{
835 return;
836
837 const QString textBeforeCursor = text( 0, linearPosition() );
838 const QString originalText = text();
839 const QString newText = reformatCodeString( originalText );
840
841 if ( originalText == newText )
842 return;
843
844 // try to preserve the cursor position and scroll position
845 const int oldScrollValue = verticalScrollBar()->value();
846 const int linearIndex = findMinimalDistanceIndex( newText, textBeforeCursor );
847
848 beginUndoAction();
849 selectAll();
850 removeSelectedText();
851 insert( newText );
852 setLinearPosition( linearIndex );
853 verticalScrollBar()->setValue( oldScrollValue );
854 endUndoAction();
855}
856
858{
859 return true;
860}
861
864
865void QgsCodeEditor::toggleLineComments( const QString &commentPrefix )
866{
867 if ( isReadOnly() )
868 {
869 return;
870 }
871
872 beginUndoAction();
873 int startLine, startPos, endLine, endPos;
874 if ( hasSelectedText() )
875 {
876 getSelection( &startLine, &startPos, &endLine, &endPos );
877 }
878 else
879 {
880 getCursorPosition( &startLine, &startPos );
881 endLine = startLine;
882 endPos = startPos;
883 }
884
885 // Check comment state and minimum indentation for each selected line
886 bool allEmpty = true;
887 bool allCommented = true;
888 int minIndentation = -1;
889 for ( int line = startLine; line <= endLine; line++ )
890 {
891 const QString stripped = text( line ).trimmed();
892 if ( !stripped.isEmpty() )
893 {
894 allEmpty = false;
895 if ( !stripped.startsWith( commentPrefix ) )
896 {
897 allCommented = false;
898 }
899 if ( minIndentation == -1 || minIndentation > indentation( line ) )
900 {
901 minIndentation = indentation( line );
902 }
903 }
904 }
905
906 // Special case, only empty lines
907 if ( allEmpty )
908 {
909 return;
910 }
911
912 // Selection shift to keep the same selected text after the prefix is added/removed
913 int delta = 0;
914
915 const int prefixLength = static_cast<int>( commentPrefix.length() );
916
917 const bool startLineEmpty = ( text( startLine ).trimmed().isEmpty() );
918 const bool endLineEmpty = ( text( endLine ).trimmed().isEmpty() );
919
920 for ( int line = startLine; line <= endLine; line++ )
921 {
922 const QString stripped = text( line ).trimmed();
923
924 // Empty line
925 if ( stripped.isEmpty() )
926 {
927 continue;
928 }
929
930 if ( !allCommented )
931 {
932 insertAt( commentPrefix + ' ', line, minIndentation );
933 delta = -( prefixLength + 1 );
934 }
935 else
936 {
937 if ( !stripped.startsWith( commentPrefix ) )
938 {
939 continue;
940 }
941 if ( stripped.startsWith( commentPrefix + ' ' ) )
942 {
943 delta = prefixLength + 1;
944 }
945 else
946 {
947 delta = prefixLength;
948 }
949 setSelection( line, indentation( line ), line, indentation( line ) + delta );
950 removeSelectedText();
951 }
952 }
953
954 endUndoAction();
955 setSelection( startLine, startPos - ( startLineEmpty ? 0 : delta ), endLine, endPos - ( endLineEmpty ? 0 : delta ) );
956}
957
959{
960 // A zero width would make setScrollWidth crash
961 long maxWidth = 10;
962
963 // Get the number of lines
964 int lineCount = lines();
965
966 // Loop through all the lines to get the longest one
967 for ( int line = 0; line < lineCount; line++ )
968 {
969 // Get the linear position at the end of the current line
970 const long endLine = SendScintilla( SCI_GETLINEENDPOSITION, line );
971 // Get the x coordinates of the end of the line
972 const long x = SendScintilla( SCI_POINTXFROMPOSITION, 0, endLine );
973 maxWidth = std::max( maxWidth, x );
974 }
975
976 // Use the longest line width as the new scroll width
977 setScrollWidth( static_cast<int>( maxWidth ) );
978}
979
980void QgsCodeEditor::setText( const QString &text )
981{
982 disconnect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
983 QsciScintilla::setText( text );
984 connect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
985 onLastEditTimeout();
987}
988
990{
991 return mLastEditTimer->interval();
992}
993
995{
996 mLastEditTimer->setInterval( timeout );
997}
998
999
1000QStringList QgsCodeEditor::history() const
1001{
1002 return mHistory;
1003}
1004
1005void QgsCodeEditor::runCommand( const QString &command, bool skipHistory )
1006{
1007 if ( !skipHistory )
1008 {
1009 updateHistory( { command } );
1012 }
1013
1014 if ( mInterpreter )
1015 mInterpreter->exec( command );
1016
1017 clear();
1019}
1020
1022{
1023 mHistory.clear();
1024 readHistoryFile();
1025 syncSoftHistory();
1026
1027 emit sessionHistoryCleared();
1028}
1029
1031{
1032 mHistory.clear();
1033
1034 if ( !mHistoryFilePath.isEmpty() && QFile::exists( mHistoryFilePath ) )
1035 {
1036 QFile file( mHistoryFilePath );
1037 if ( !file.open( QFile::WriteOnly | QFile::Truncate ) )
1038 {
1039 QgsDebugError( u"Could not truncate %1"_s.arg( mHistoryFilePath ) );
1040 }
1041 }
1042
1044}
1045
1047{
1048 if ( mHistoryFilePath.isEmpty() )
1049 return false;
1050
1051 QFile f( mHistoryFilePath );
1052 if ( !f.open( QFile::WriteOnly | QIODevice::Truncate ) )
1053 {
1054 return false;
1055 }
1056
1057 QTextStream ts( &f );
1058 for ( const QString &command : std::as_const( mHistory ) )
1059 {
1060 ts << command + '\n';
1061 }
1062 return true;
1063}
1064
1066{
1067 if ( mSoftHistoryIndex < mSoftHistory.length() - 1 && !mSoftHistory.isEmpty() )
1068 {
1069 mSoftHistoryIndex += 1;
1070 setText( mSoftHistory[mSoftHistoryIndex] );
1072 }
1073}
1074
1076{
1077 if ( mSoftHistoryIndex > 0 && !mSoftHistory.empty() )
1078 {
1079 mSoftHistoryIndex -= 1;
1080 setText( mSoftHistory[mSoftHistoryIndex] );
1082 }
1083}
1084
1086{
1087 QgsCodeEditorHistoryDialog *dialog = new QgsCodeEditorHistoryDialog( this, this );
1088 dialog->setAttribute( Qt::WA_DeleteOnClose );
1089
1090 dialog->show();
1091 dialog->activateWindow();
1092}
1093
1095{
1096 // remove item from the command history (just for the current session)
1097 mHistory.removeAt( index );
1098 mSoftHistory.removeAt( index );
1099 if ( index < mSoftHistoryIndex )
1100 {
1101 mSoftHistoryIndex -= 1;
1102 if ( mSoftHistoryIndex < 0 )
1103 mSoftHistoryIndex = mSoftHistory.length() - 1;
1104 }
1105}
1106
1107void QgsCodeEditor::insertText( const QString &text )
1108{
1109 // Insert the text or replace selected text
1110 if ( hasSelectedText() )
1111 {
1112 replaceSelectedText( text );
1113 }
1114 else
1115 {
1116 int line, index;
1117 getCursorPosition( &line, &index );
1118 insertAt( text, line, index );
1119 setCursorPosition( line, index + text.length() );
1120 }
1121}
1122
1124{
1125 bool useDefault = QgsApplication::themeName() == "default"_L1;
1126 if ( !useDefault )
1127 {
1128 QFileInfo info( QgsApplication::instance()->applicationThemeRegistry()->themeFolder( QgsApplication::themeName() ) + "/qscintilla.ini" );
1129 useDefault = !info.exists();
1130 }
1131
1132 if ( theme.isEmpty() && useDefault )
1133 {
1134 // if using default theme, take certain colors from the palette
1135 const QPalette pal = qApp->palette();
1136
1137 switch ( role )
1138 {
1140 return pal.color( QPalette::Highlight );
1142 return pal.color( QPalette::HighlightedText );
1143 default:
1144 break;
1145 }
1146 }
1147 else if ( theme.isEmpty() )
1148 {
1149 // non default theme (e.g. Blend of Gray). Take colors from theme ini file...
1150 const QSettings ini( QgsApplication::instance()->applicationThemeRegistry()->themeFolder( QgsApplication::themeName() ) + "/qscintilla.ini", QSettings::IniFormat );
1151
1152 static const QMap<QgsCodeEditorColorScheme::ColorRole, QString> sColorRoleToIniKey {
1153 { QgsCodeEditorColorScheme::ColorRole::Default, u"python/defaultFontColor"_s },
1154 { QgsCodeEditorColorScheme::ColorRole::Keyword, u"python/keywordFontColor"_s },
1155 { QgsCodeEditorColorScheme::ColorRole::Class, u"python/classFontColor"_s },
1156 { QgsCodeEditorColorScheme::ColorRole::Method, u"python/methodFontColor"_s },
1157 { QgsCodeEditorColorScheme::ColorRole::Decoration, u"python/decoratorFontColor"_s },
1158 { QgsCodeEditorColorScheme::ColorRole::Number, u"python/numberFontColor"_s },
1159 { QgsCodeEditorColorScheme::ColorRole::Comment, u"python/commentFontColor"_s },
1160 { QgsCodeEditorColorScheme::ColorRole::CommentLine, u"sql/commentLineFontColor"_s },
1161 { QgsCodeEditorColorScheme::ColorRole::CommentBlock, u"python/commentBlockFontColor"_s },
1162 { QgsCodeEditorColorScheme::ColorRole::Background, u"python/paperBackgroundColor"_s },
1163 { QgsCodeEditorColorScheme::ColorRole::Cursor, u"cursorColor"_s },
1164 { QgsCodeEditorColorScheme::ColorRole::CaretLine, u"caretLineColor"_s },
1165 { QgsCodeEditorColorScheme::ColorRole::Operator, u"sql/operatorFontColor"_s },
1166 { QgsCodeEditorColorScheme::ColorRole::QuotedOperator, u"sql/QuotedOperatorFontColor"_s },
1167 { QgsCodeEditorColorScheme::ColorRole::Identifier, u"sql/identifierFontColor"_s },
1168 { QgsCodeEditorColorScheme::ColorRole::QuotedIdentifier, u"sql/QuotedIdentifierFontColor"_s },
1169 { QgsCodeEditorColorScheme::ColorRole::Tag, u"html/tagFontColor"_s },
1170 { QgsCodeEditorColorScheme::ColorRole::UnknownTag, u"html/unknownTagFontColor"_s },
1171 { QgsCodeEditorColorScheme::ColorRole::SingleQuote, u"sql/singleQuoteFontColor"_s },
1172 { QgsCodeEditorColorScheme::ColorRole::DoubleQuote, u"sql/doubleQuoteFontColor"_s },
1173 { QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote, u"python/tripleSingleQuoteFontColor"_s },
1174 { QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote, u"python/tripleDoubleQuoteFontColor"_s },
1175 { QgsCodeEditorColorScheme::ColorRole::MarginBackground, u"marginBackgroundColor"_s },
1176 { QgsCodeEditorColorScheme::ColorRole::MarginForeground, u"marginForegroundColor"_s },
1177 { QgsCodeEditorColorScheme::ColorRole::SelectionBackground, u"selectionBackgroundColor"_s },
1178 { QgsCodeEditorColorScheme::ColorRole::SelectionForeground, u"selectionForegroundColor"_s },
1179 { QgsCodeEditorColorScheme::ColorRole::MatchedBraceBackground, u"matchedBraceBackground"_s },
1183 { QgsCodeEditorColorScheme::ColorRole::Error, u"stderrFontColor"_s },
1188 { QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, u"searchMatchBackground"_s },
1189 };
1190
1191 const QgsCodeEditorColorScheme defaultScheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( u"default"_s );
1192 return QgsSymbolLayerUtils::decodeColor( ini.value( sColorRoleToIniKey.value( role ), defaultScheme.color( role ).name() ).toString() );
1193 }
1194
1195 const QgsCodeEditorColorScheme scheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( theme.isEmpty() ? u"default"_s : theme );
1196 return scheme.color( role );
1197}
1198
1200{
1201 const QgsSettings settings;
1202 if ( !settings.value( u"codeEditor/overrideColors"_s, false, QgsSettings::Gui ).toBool() )
1203 {
1204 const QString theme = settings.value( u"codeEditor/colorScheme"_s, QString(), QgsSettings::Gui ).toString();
1205 return defaultColor( role, theme );
1206 }
1207 else
1208 {
1209 const QString color = settings.value( u"codeEditor/%1"_s.arg( sColorRoleToSettingsKey.value( role ) ), QString(), QgsSettings::Gui ).toString();
1210 return color.isEmpty() ? defaultColor( role ) : QgsSymbolLayerUtils::decodeColor( color );
1211 }
1212}
1213
1215{
1216 QgsSettings settings;
1217 if ( color.isValid() )
1218 {
1219 settings.setValue( u"codeEditor/%1"_s.arg( sColorRoleToSettingsKey.value( role ) ), color.name(), QgsSettings::Gui );
1220 }
1221 else
1222 {
1223 settings.remove( u"codeEditor/%1"_s.arg( sColorRoleToSettingsKey.value( role ) ), QgsSettings::Gui );
1224 }
1225}
1226
1227// Settings for font and fontsize
1228bool QgsCodeEditor::isFixedPitch( const QFont &font )
1229{
1230 return font.fixedPitch();
1231}
1232
1234{
1235 QFont font = QFontDatabase::systemFont( QFontDatabase::FixedFont );
1236
1237 const QString fontFamily = settingFontFamily->value();
1238 if ( !fontFamily.isEmpty() )
1239 QgsFontUtils::setFontFamily( font, fontFamily );
1240
1241 const int fontSize = settingFontSize->value();
1242
1243#ifdef Q_OS_MAC
1244 if ( fontSize > 0 )
1245 font.setPointSize( fontSize );
1246 else
1247 {
1248 // The font size gotten from getMonospaceFont() is too small on Mac
1249 font.setPointSize( QLabel().font().pointSize() );
1250 }
1251#else
1252 if ( fontSize > 0 )
1253 font.setPointSize( fontSize );
1254 else
1255 {
1256 const QgsSettings settings;
1257 const int fontSize = settings.value( u"qgis/stylesheet/fontPointSize"_s, 10 ).toInt();
1258 font.setPointSize( fontSize );
1259 }
1260#endif
1261 font.setBold( false );
1262
1263 return font;
1264}
1265
1266void QgsCodeEditor::setCustomAppearance( const QString &scheme, const QMap<QgsCodeEditorColorScheme::ColorRole, QColor> &customColors, const QString &fontFamily, int fontSize )
1267{
1268 mUseDefaultSettings = false;
1269 mOverrideColors = !customColors.isEmpty();
1270 mColorScheme = scheme;
1271 mCustomColors = customColors;
1272 mFontFamily = fontFamily;
1273 mFontSize = fontSize;
1274
1275 setSciWidget();
1277}
1278
1279void QgsCodeEditor::addWarning( const int lineNumber, const QString &warning )
1280{
1281 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), "000" );
1282 markerAdd( lineNumber, MARKER_NUMBER );
1283 QFont font = lexerFont();
1284 font.setItalic( true );
1285 const QsciStyle styleAnn = QsciStyle( -1, u"Annotation"_s, lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), lexerColor( QgsCodeEditorColorScheme::ColorRole::ErrorBackground ), font, true );
1286 annotate( lineNumber, warning, styleAnn );
1287 mWarningLines.push_back( lineNumber );
1288}
1289
1291{
1292 for ( const int line : mWarningLines )
1293 {
1294 markerDelete( line );
1295 clearAnnotations( line );
1296 }
1297 setMarginWidth( static_cast<int>( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 );
1298 mWarningLines.clear();
1299}
1300
1302{
1303 int line = 0;
1304 int index = 0;
1305 getCursorPosition( &line, &index );
1306 return line == lines() - 1;
1307}
1308
1309void QgsCodeEditor::setHistoryFilePath( const QString &path )
1310{
1311 mHistoryFilePath = path;
1312 readHistoryFile();
1313}
1314
1316{
1317 setCursorPosition( 0, 0 );
1318 ensureCursorVisible();
1319 ensureLineVisible( 0 );
1320
1321 if ( mMode == QgsCodeEditor::Mode::CommandInput )
1322 updatePrompt();
1323}
1324
1326{
1327 const int endLine = lines() - 1;
1328 const int endLineLength = lineLength( endLine );
1329 setCursorPosition( endLine, endLineLength );
1330 ensureCursorVisible();
1331 ensureLineVisible( endLine );
1332
1333 if ( mMode == QgsCodeEditor::Mode::CommandInput )
1334 updatePrompt();
1335}
1336
1338{
1339 return static_cast<int>( SendScintilla( SCI_GETCURRENTPOS ) );
1340}
1341
1343{
1344 int line, index;
1345 lineIndexFromPosition( linearIndex, &line, &index );
1346 setCursorPosition( line, index );
1347}
1348
1350{
1351 int startLine, startIndex, _;
1352 getSelection( &startLine, &startIndex, &_, &_ );
1353 if ( startLine == -1 )
1354 {
1355 return linearPosition();
1356 }
1357 return positionFromLineIndex( startLine, startIndex );
1358}
1359
1361{
1362 int endLine, endIndex, _;
1363 getSelection( &_, &_, &endLine, &endIndex );
1364 if ( endLine == -1 )
1365 {
1366 return linearPosition();
1367 }
1368 return positionFromLineIndex( endLine, endIndex );
1369}
1370
1371void QgsCodeEditor::setLinearSelection( int start, int end )
1372{
1373 int startLine, startIndex, endLine, endIndex;
1374 lineIndexFromPosition( start, &startLine, &startIndex );
1375 lineIndexFromPosition( end, &endLine, &endIndex );
1376 setSelection( startLine, startIndex, endLine, endIndex );
1377}
1378
1380
1381int QgsCodeInterpreter::exec( const QString &command )
1382{
1383 mState = execCommandImpl( command );
1384 return mState;
1385}
1386
1387
1389{
1390 // If wrapping is disabled, return -1
1391 if ( wrapMode() == WrapNone )
1392 {
1393 return -1;
1394 }
1395 // Get the current line
1396 if ( line == -1 )
1397 {
1398 int _index;
1399 lineIndexFromPosition( linearPosition(), &line, &_index );
1400 }
1401
1402 // If line isn't wrapped, return -1
1403 if ( SendScintilla( SCI_WRAPCOUNT, line ) <= 1 )
1404 {
1405 return -1;
1406 }
1407
1408 // Get the linear position at the end of the current line
1409 const long endLine = SendScintilla( SCI_GETLINEENDPOSITION, line );
1410 // Get the y coordinates of the start of the last wrapped line
1411 const long y = SendScintilla( SCI_POINTYFROMPOSITION, 0, endLine );
1412 // Return the linear position of the start of the last wrapped line
1413 return static_cast<int>( SendScintilla( SCI_POSITIONFROMPOINT, 0, y ) );
1414}
1415
1416
1417// Adapted from QsciScintilla source code (qsciscintilla.cpp) to handle line wrap
1419{
1420 if ( callTipsStyle() == CallTipsNone || lexer() == nullptr )
1421 {
1422 return;
1423 }
1424
1425 QsciAbstractAPIs *apis = lexer()->apis();
1426
1427 if ( !apis )
1428 return;
1429
1430 int pos, commas = 0;
1431 bool found = false;
1432 char ch;
1433
1434 pos = linearPosition();
1435
1436 // Move backwards through the line looking for the start of the current
1437 // call tip and working out which argument it is.
1438 while ( ( ch = getCharacter( pos ) ) != '\0' )
1439 {
1440 if ( ch == ',' )
1441 ++commas;
1442 else if ( ch == ')' )
1443 {
1444 int depth = 1;
1445
1446 // Ignore everything back to the start of the corresponding
1447 // parenthesis.
1448 while ( ( ch = getCharacter( pos ) ) != '\0' )
1449 {
1450 if ( ch == ')' )
1451 ++depth;
1452 else if ( ch == '(' && --depth == 0 )
1453 break;
1454 }
1455 }
1456 else if ( ch == '(' )
1457 {
1458 found = true;
1459 break;
1460 }
1461 }
1462
1463 // Cancel any existing call tip.
1464 SendScintilla( SCI_CALLTIPCANCEL );
1465
1466 // Done if there is no new call tip to set.
1467 if ( !found )
1468 return;
1469
1470 int contextStart, lastWordStart;
1471 QStringList context = apiContext( pos, contextStart, lastWordStart );
1472
1473 if ( context.isEmpty() )
1474 return;
1475
1476 // The last word is complete, not partial.
1477 context << QString();
1478
1479 QList<int> ctShifts;
1480 QStringList ctEntries = apis->callTips( context, commas, callTipsStyle(), ctShifts );
1481
1482 int nbEntries = ctEntries.count();
1483
1484 if ( nbEntries == 0 )
1485 return;
1486
1487 const int maxNumberOfCallTips = callTipsVisible();
1488
1489 // Clip to at most maxNumberOfCallTips entries.
1490 if ( maxNumberOfCallTips > 0 && maxNumberOfCallTips < nbEntries )
1491 {
1492 ctEntries = ctEntries.mid( 0, maxNumberOfCallTips );
1493 nbEntries = maxNumberOfCallTips;
1494 }
1495
1496 int shift;
1497 QString ct;
1498
1499 int nbShifts = ctShifts.count();
1500
1501 if ( maxNumberOfCallTips < 0 && nbEntries > 1 )
1502 {
1503 shift = ( nbShifts > 0 ? ctShifts.first() : 0 );
1504 ct = ctEntries[0];
1505 ct.prepend( '\002' );
1506 }
1507 else
1508 {
1509 if ( nbShifts > nbEntries )
1510 nbShifts = nbEntries;
1511
1512 // Find the biggest shift.
1513 shift = 0;
1514
1515 for ( int i = 0; i < nbShifts; ++i )
1516 {
1517 int sh = ctShifts[i];
1518
1519 if ( shift < sh )
1520 shift = sh;
1521 }
1522
1523 ct = ctEntries.join( "\n" );
1524 }
1525
1526 QByteArray ctBa = ct.toLatin1();
1527 const char *cts = ctBa.data();
1528
1529 const int currentWrapPosition = wrapPosition();
1530
1531 if ( currentWrapPosition != -1 )
1532 {
1533 SendScintilla( SCI_CALLTIPSHOW, currentWrapPosition, cts );
1534 }
1535 else
1536 {
1537 // Shift the position of the call tip (to take any context into account) but
1538 // don't go before the start of the line.
1539 if ( shift )
1540 {
1541 int ctmin = static_cast<int>( SendScintilla( SCI_POSITIONFROMLINE, SendScintilla( SCI_LINEFROMPOSITION, ct ) ) );
1542 if ( lastWordStart - shift < ctmin )
1543 lastWordStart = ctmin;
1544 }
1545
1546 int line, index;
1547 lineIndexFromPosition( lastWordStart, &line, &index );
1548 SendScintilla( SCI_CALLTIPSHOW, positionFromLineIndex( line, index ), cts );
1549 }
1550
1551 // Done if there is more than one call tip.
1552 if ( nbEntries > 1 )
1553 return;
1554
1555 // Highlight the current argument.
1556 const char *astart;
1557
1558 if ( commas == 0 )
1559 astart = strchr( cts, '(' );
1560 else
1561 for ( astart = strchr( cts, ',' ); astart && --commas > 0; astart = strchr( astart + 1, ',' ) )
1562 ;
1563
1564 if ( !astart )
1565 return;
1566
1567 astart++;
1568 if ( !*astart )
1569 return;
1570
1571 // The end is at the next comma or unmatched closing parenthesis.
1572 const char *aend;
1573 int depth = 0;
1574
1575 for ( aend = astart; *aend; ++aend )
1576 {
1577 char ch = *aend;
1578
1579 if ( ch == ',' && depth == 0 )
1580 break;
1581 else if ( ch == '(' )
1582 ++depth;
1583 else if ( ch == ')' )
1584 {
1585 if ( depth == 0 )
1586 break;
1587
1588 --depth;
1589 }
1590 }
1591
1592 if ( astart != aend )
1593 SendScintilla( SCI_CALLTIPSETHLT, astart - cts, aend - cts );
1594}
1595
1596
1597// Duplicated from QsciScintilla source code (qsciscintilla.cpp)
1598// Get the "next" character (ie. the one before the current position) in the
1599// current line. The character will be '\0' if there are no more.
1600char QgsCodeEditor::getCharacter( int &pos ) const
1601{
1602 if ( pos <= 0 )
1603 return '\0';
1604
1605 char ch = static_cast<char>( SendScintilla( SCI_GETCHARAT, --pos ) );
1606
1607 // Don't go past the end of the previous line.
1608 if ( ch == '\n' || ch == '\r' )
1609 {
1610 ++pos;
1611 return '\0';
1612 }
1613
1614 return ch;
1615}
MessageLevel
Level for messages This will be used both for message log and message bar in application.
Definition qgis.h:160
@ NoLevel
No level.
Definition qgis.h:165
@ Warning
Warning message.
Definition qgis.h:162
@ Critical
Critical/error message.
Definition qgis.h:163
@ Info
Information message.
Definition qgis.h:161
@ Success
Used for reporting a successful operation.
Definition qgis.h:164
@ CheckSyntax
Language supports syntax checking.
Definition qgis.h:4803
@ Reformat
Language supports automatic code reformatting.
Definition qgis.h:4802
@ ToggleComment
Language supports comment toggling.
Definition qgis.h:4804
ScriptLanguage
Scripting languages.
Definition qgis.h:4778
@ QgisExpression
QGIS expressions.
Definition qgis.h:4780
@ Batch
Windows batch files.
Definition qgis.h:4787
@ JavaScript
JavaScript.
Definition qgis.h:4782
@ Bash
Bash scripts.
Definition qgis.h:4788
@ Unknown
Unknown/other language.
Definition qgis.h:4789
@ Python
Python.
Definition qgis.h:4784
QFlags< ScriptLanguageCapability > ScriptLanguageCapabilities
Script language capabilities.
Definition qgis.h:4813
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 QgsApplication * instance()
Returns the singleton instance of the QgsApplication.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
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.
@ SelectionForeground
Selection foreground color.
@ FoldIconForeground
Fold icon foreground color.
@ MatchedBraceBackground
Matched brace background color.
@ SearchMatchBackground
Background color for search matches.
@ SelectionBackground
Selection background color.
@ MatchedBraceForeground
Matched brace foreground color.
@ TripleDoubleQuote
Triple double quote 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,...
QgsCodeEditor::Mode mode() const
Returns the code editor mode.
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 const QgsSettingsEntryString * settingFontFamily
Settings entry for code editor font family override.
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.
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.
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.
static const QgsSettingsEntryInteger * settingFontSize
Settings entry for code editor font size override.
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.
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.
static QgsShortcutsManager * shortcutsManager()
Returns the global shortcuts manager, used for managing a QAction and QShortcut sequences.
Definition qgsgui.cpp:139
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:93
static QgsCodeEditorColorSchemeRegistry * codeEditorColorSchemeRegistry()
Returns the global code editor color scheme registry, used for registering the color schemes for QgsC...
Definition qgsgui.cpp:179
A boolean settings entry.
An integer settings entry.
A string settings entry.
Stores settings for use within QGIS.
Definition qgssettings.h:68
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.
@ CodeToggleComment
Toggle code comments.
QKeySequence sequenceForCommonAction(CommonAction action) const
Returns the key sequence which is associated with a common action, or an empty sequence if no shortcu...
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:7714
int findMinimalDistanceIndex(const QString &source, const QString &target)
#define QgsDebugError(str)
Definition qgslogger.h:59