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