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