/*************************************************************************** qgscodeeditor.cpp - A base code editor for QGIS and plugins. Provides a base editor using QScintilla for editors -------------------------------------- Date : 06-Oct-2013 Copyright : (C) 2013 by Salvatore Larosa Email : lrssvtml (at) gmail (dot) com *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "qgsapplication.h" #include "qgscodeeditor.h" #include "qgssettings.h" #include "qgssymbollayerutils.h" #include "qgsgui.h" #include "qgscodeeditorcolorschemeregistry.h" #include "qgscodeeditorhistorydialog.h" #include "qgsstringutils.h" #include #include #include #include #include #include #include #include #include #include QMap< QgsCodeEditorColorScheme::ColorRole, QString > QgsCodeEditor::sColorRoleToSettingsKey { {QgsCodeEditorColorScheme::ColorRole::Default, QStringLiteral( "defaultFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Keyword, QStringLiteral( "keywordFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Class, QStringLiteral( "classFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Method, QStringLiteral( "methodFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Decoration, QStringLiteral( "decoratorFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Number, QStringLiteral( "numberFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Comment, QStringLiteral( "commentFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::CommentLine, QStringLiteral( "commentLineFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::CommentBlock, QStringLiteral( "commentBlockFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Background, QStringLiteral( "paperBackgroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Cursor, QStringLiteral( "cursorColor" ) }, {QgsCodeEditorColorScheme::ColorRole::CaretLine, QStringLiteral( "caretLineColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Operator, QStringLiteral( "operatorFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::QuotedOperator, QStringLiteral( "quotedOperatorFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Identifier, QStringLiteral( "identifierFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::QuotedIdentifier, QStringLiteral( "quotedIdentifierFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Tag, QStringLiteral( "tagFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::UnknownTag, QStringLiteral( "unknownTagFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::SingleQuote, QStringLiteral( "singleQuoteFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::DoubleQuote, QStringLiteral( "doubleQuoteFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote, QStringLiteral( "tripleSingleQuoteFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote, QStringLiteral( "tripleDoubleQuoteFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::MarginBackground, QStringLiteral( "marginBackgroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::MarginForeground, QStringLiteral( "marginForegroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::SelectionBackground, QStringLiteral( "selectionBackgroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::SelectionForeground, QStringLiteral( "selectionForegroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::MatchedBraceBackground, QStringLiteral( "matchedBraceBackground" ) }, {QgsCodeEditorColorScheme::ColorRole::MatchedBraceForeground, QStringLiteral( "matchedBraceColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Edge, QStringLiteral( "edgeColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Fold, QStringLiteral( "foldColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Error, QStringLiteral( "stderrFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::ErrorBackground, QStringLiteral( "stderrBackgroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QStringLiteral( "foldIconForeground" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QStringLiteral( "foldIconHalo" ) }, {QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QStringLiteral( "indentationGuide" ) }, }; QgsCodeEditor::QgsCodeEditor( QWidget *parent, const QString &title, bool folding, bool margin, QgsCodeEditor::Flags flags, QgsCodeEditor::Mode mode ) : QsciScintilla( parent ) , mWidgetTitle( title ) , mMargin( margin ) , mFlags( flags ) , mMode( mode ) { if ( !parent && mWidgetTitle.isEmpty() ) { setWindowTitle( QStringLiteral( "Text Editor" ) ); } else { setWindowTitle( mWidgetTitle ); } if ( folding ) mFlags |= QgsCodeEditor::Flag::CodeFolding; mSoftHistory.append( QString() ); setSciWidget(); setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded ); SendScintilla( SCI_SETADDITIONALSELECTIONTYPING, 1 ); SendScintilla( SCI_SETMULTIPASTE, 1 ); SendScintilla( SCI_SETVIRTUALSPACEOPTIONS, SCVS_RECTANGULARSELECTION ); SendScintilla( SCI_SETMARGINTYPEN, static_cast< int >( QgsCodeEditor::MarginRole::ErrorIndicators ), SC_MARGIN_SYMBOL ); SendScintilla( SCI_SETMARGINMASKN, static_cast< int >( QgsCodeEditor::MarginRole::ErrorIndicators ), 1 << MARKER_NUMBER ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 ); setAnnotationDisplay( QsciScintilla::AnnotationBoxed ); connect( QgsGui::instance(), &QgsGui::optionsChanged, this, [ = ] { setSciWidget(); initializeLexer(); } ); switch ( mMode ) { case QgsCodeEditor::Mode::ScriptEditor: break; case QgsCodeEditor::Mode::OutputDisplay: { // Don't want to see the horizontal scrollbar at all SendScintilla( QsciScintilla::SCI_SETHSCROLLBAR, 0 ); setWrapMode( QsciScintilla::WrapCharacter ); break; } case QgsCodeEditor::Mode::CommandInput: { // Don't want to see the horizontal scrollbar at all SendScintilla( QsciScintilla::SCI_SETHSCROLLBAR, 0 ); setWrapMode( QsciScintilla::WrapCharacter ); SendScintilla( QsciScintilla::SCI_EMPTYUNDOBUFFER ); break; } } } // Workaround a bug in QScintilla 2.8.X void QgsCodeEditor::focusOutEvent( QFocusEvent *event ) { #if QSCINTILLA_VERSION >= 0x020800 && QSCINTILLA_VERSION < 0x020900 if ( event->reason() != Qt::ActiveWindowFocusReason ) { /* There's a bug in all QScintilla 2.8.X, where a focus out event that is not due to ActiveWindowFocusReason doesn't lead to the bliking caret being disabled. The hack consists in making QsciScintilla::focusOutEvent believe that the event is a ActiveWindowFocusReason The bug was fixed in 2.9 per: 2015-04-14 Phil Thompson * qt/qsciscintillabase.cpp: Fixed a problem notifying when focus is lost to another application widget. [41734678234e] */ QFocusEvent newFocusEvent( QEvent::FocusOut, Qt::ActiveWindowFocusReason ); QsciScintilla::focusOutEvent( &newFocusEvent ); } else #endif { QsciScintilla::focusOutEvent( event ); } } // This workaround a likely bug in QScintilla. The ESC key should not be consumned // by the main entry, so that the default behavior (Dialog closing) can trigger, // but only is the auto-completion suggestion list isn't displayed void QgsCodeEditor::keyPressEvent( QKeyEvent *event ) { if ( isListActive() ) { QsciScintilla::keyPressEvent( event ); return; } if ( event->key() == Qt::Key_Escape ) { // Shortcut QScintilla and redirect the event to the QWidget handler QWidget::keyPressEvent( event ); // NOLINT(bugprone-parent-virtual-call) clazy:exclude=skipped-base-method return; } if ( mMode == QgsCodeEditor::Mode::CommandInput ) { switch ( event->key() ) { case Qt::Key_Return: case Qt::Key_Enter: runCommand( text() ); updatePrompt(); return; case Qt::Key_Down: showPreviousCommand(); updatePrompt(); return; case Qt::Key_Up: showNextCommand(); updatePrompt(); return; default: break; } } QsciScintilla::keyPressEvent( event ); } void QgsCodeEditor::contextMenuEvent( QContextMenuEvent *event ) { switch ( mMode ) { case Mode::ScriptEditor: { QMenu *menu = createStandardContextMenu(); menu->setAttribute( Qt::WA_DeleteOnClose ); if ( languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat ) { menu->addSeparator(); QAction *reformatAction = new QAction( tr( "Reformat Code" ), menu ); reformatAction->setEnabled( !isReadOnly() ); connect( reformatAction, &QAction::triggered, this, &QgsCodeEditor::reformatCode ); menu->addAction( reformatAction ); } populateContextMenu( menu ); menu->exec( mapToGlobal( event->pos() ) ); break; } case Mode::CommandInput: { QMenu *menu = new QMenu( this ); QMenu *historySubMenu = new QMenu( tr( "Command History" ), menu ); historySubMenu->addAction( tr( "Show" ), this, &QgsCodeEditor::showHistory, QStringLiteral( "Ctrl+Shift+SPACE" ) ); historySubMenu->addAction( tr( "Clear File" ), this, &QgsCodeEditor::clearPersistentHistory ); historySubMenu->addAction( tr( "Clear Session" ), this, &QgsCodeEditor::clearSessionHistory ); menu->addMenu( historySubMenu ); menu->addSeparator(); QAction *copyAction = menu->addAction( QgsApplication::getThemeIcon( "mActionEditCopy.svg" ), tr( "Copy" ), this, &QgsCodeEditor::copy, QKeySequence::Copy ); QAction *pasteAction = menu->addAction( QgsApplication::getThemeIcon( "mActionEditPaste.svg" ), tr( "Paste" ), this, &QgsCodeEditor::paste, QKeySequence::Paste ); copyAction->setEnabled( hasSelectedText() ); pasteAction->setEnabled( !QApplication::clipboard()->text().isEmpty() ); populateContextMenu( menu ); menu->exec( mapToGlobal( event->pos() ) ); break; } case Mode::OutputDisplay: QsciScintilla::contextMenuEvent( event ); break; } } void QgsCodeEditor::initializeLexer() { } QColor QgsCodeEditor::lexerColor( QgsCodeEditorColorScheme::ColorRole role ) const { if ( mUseDefaultSettings ) return color( role ); if ( !mOverrideColors ) { return defaultColor( role, mColorScheme ); } else { const QColor color = mCustomColors.value( role ); return !color.isValid() ? defaultColor( role ) : color; } } QFont QgsCodeEditor::lexerFont() const { if ( mUseDefaultSettings ) return getMonospaceFont(); QFont font = QFontDatabase::systemFont( QFontDatabase::FixedFont ); const QgsSettings settings; if ( !mFontFamily.isEmpty() ) font.setFamily( mFontFamily ); #ifdef Q_OS_MAC if ( mFontSize > 0 ) font.setPointSize( mFontSize ); else { // The font size gotten from getMonospaceFont() is too small on Mac font.setPointSize( QLabel().font().pointSize() ); } #else if ( mFontSize > 0 ) font.setPointSize( mFontSize ); else { const int fontSize = settings.value( QStringLiteral( "qgis/stylesheet/fontPointSize" ), 10 ).toInt(); font.setPointSize( fontSize ); } #endif font.setBold( false ); return font; } void QgsCodeEditor::runPostLexerConfigurationTasks() { updateFolding(); setMatchedBraceForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MatchedBraceForeground ) ); setMatchedBraceBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MatchedBraceBackground ) ); SendScintilla( SCI_MARKERSETFORE, SC_MARKNUM_FOLDEROPEN, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconHalo ) ); SendScintilla( SCI_MARKERSETBACK, SC_MARKNUM_FOLDEROPEN, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconForeground ) ); SendScintilla( SCI_MARKERSETFORE, SC_MARKNUM_FOLDER, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconHalo ) ); SendScintilla( SCI_MARKERSETBACK, SC_MARKNUM_FOLDER, lexerColor( QgsCodeEditorColorScheme::ColorRole::FoldIconForeground ) ); SendScintilla( SCI_STYLESETFORE, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) ); SendScintilla( SCI_STYLESETBACK, STYLE_INDENTGUIDE, lexerColor( QgsCodeEditorColorScheme::ColorRole::IndentationGuide ) ); if ( mMode == QgsCodeEditor::Mode::CommandInput ) { setCaretLineVisible( false ); setLineNumbersVisible( false ); // NO linenumbers for the input line // Margin 1 is used for the '>' prompt (console input) setMarginLineNumbers( 1, true ); setMarginWidth( 1, "00000" ); setMarginType( 1, QsciScintilla::MarginType::TextMarginRightJustified ); setMarginsBackgroundColor( color( QgsCodeEditorColorScheme::ColorRole::Background ) ); setEdgeMode( QsciScintilla::EdgeNone ); } } void QgsCodeEditor::setSciWidget() { const QFont font = lexerFont(); setFont( font ); setUtf8( true ); setCaretLineVisible( true ); setCaretLineBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CaretLine ) ); setCaretForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Cursor ) ); setSelectionForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SelectionForeground ) ); setSelectionBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SelectionBackground ) ); setBraceMatching( QsciScintilla::SloppyBraceMatch ); setMatchedBraceForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MatchedBraceForeground ) ); setMatchedBraceBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MatchedBraceBackground ) ); setLineNumbersVisible( false ); // temporarily disable folding, will be enabled later if required by updateFolding() setFolding( QsciScintilla::NoFoldStyle ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::FoldingControls ), 0 ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 ); setMarginsForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginForeground ) ); setMarginsBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginBackground ) ); setIndentationGuidesForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginForeground ) ); setIndentationGuidesBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginBackground ) ); // whether margin will be shown updateFolding(); const QColor foldColor = lexerColor( QgsCodeEditorColorScheme::ColorRole::Fold ); setFoldMarginColors( foldColor, foldColor ); // indentation setAutoIndent( true ); setIndentationWidth( 4 ); setTabIndents( true ); setBackspaceUnindents( true ); setTabWidth( 4 ); // autocomplete setAutoCompletionThreshold( 2 ); setAutoCompletionSource( QsciScintilla::AcsAPIs ); markerDefine( QgsApplication::getThemePixmap( "console/iconSyntaxErrorConsoleParams.svg", lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), lexerColor( QgsCodeEditorColorScheme::ColorRole::ErrorBackground ), 16 ), MARKER_NUMBER ); } void QgsCodeEditor::setTitle( const QString &title ) { setWindowTitle( title ); } Qgis::ScriptLanguage QgsCodeEditor::language() const { return Qgis::ScriptLanguage::Unknown; } Qgis::ScriptLanguageCapabilities QgsCodeEditor::languageCapabilities() const { return Qgis::ScriptLanguageCapabilities(); } QString QgsCodeEditor::languageToString( Qgis::ScriptLanguage language ) { switch ( language ) { case Qgis::ScriptLanguage::Css: return tr( "CSS" ); case Qgis::ScriptLanguage::QgisExpression: return tr( "Expression" ); case Qgis::ScriptLanguage::Html: return tr( "HTML" ); case Qgis::ScriptLanguage::JavaScript: return tr( "JavaScript" ); case Qgis::ScriptLanguage::Json: return tr( "JSON" ); case Qgis::ScriptLanguage::Python: return tr( "Python" ); case Qgis::ScriptLanguage::R: return tr( "R" ); case Qgis::ScriptLanguage::Sql: return tr( "SQL" ); case Qgis::ScriptLanguage::Unknown: return QString(); } BUILTIN_UNREACHABLE } void QgsCodeEditor::setMarginVisible( bool margin ) { mMargin = margin; if ( margin ) { QFont marginFont = lexerFont(); marginFont.setPointSize( 10 ); setMarginLineNumbers( 0, true ); setMarginsFont( marginFont ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::LineNumbers ), QStringLiteral( "00000" ) ); setMarginsForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginForeground ) ); setMarginsBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginBackground ) ); } else { setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::LineNumbers ), 0 ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::FoldingControls ), 0 ); } } void QgsCodeEditor::setLineNumbersVisible( bool visible ) { if ( visible ) { QFont marginFont = lexerFont(); marginFont.setPointSize( 10 ); setMarginLineNumbers( static_cast< int >( QgsCodeEditor::MarginRole::LineNumbers ), true ); setMarginsFont( marginFont ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::LineNumbers ), QStringLiteral( "00000" ) ); setMarginsForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginForeground ) ); setMarginsBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginBackground ) ); } else { setMarginLineNumbers( static_cast< int >( QgsCodeEditor::MarginRole::LineNumbers ), false ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::LineNumbers ), 0 ); } } bool QgsCodeEditor::lineNumbersVisible() const { return marginLineNumbers( static_cast< int >( QgsCodeEditor::MarginRole::LineNumbers ) ); } void QgsCodeEditor::setFoldingVisible( bool folding ) { if ( folding ) { mFlags |= QgsCodeEditor::Flag::CodeFolding; } else { mFlags &= ~( static_cast< int >( QgsCodeEditor::Flag::CodeFolding ) ); } updateFolding(); } bool QgsCodeEditor::foldingVisible() { return mFlags & QgsCodeEditor::Flag::CodeFolding; } void QgsCodeEditor::updateFolding() { if ( ( mFlags & QgsCodeEditor::Flag::CodeFolding ) && mMode == QgsCodeEditor::Mode::ScriptEditor ) { setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::FoldingControls ), "0" ); setMarginsForegroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginForeground ) ); setMarginsBackgroundColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::MarginBackground ) ); setFolding( QsciScintilla::PlainFoldStyle ); } else { setFolding( QsciScintilla::NoFoldStyle ); setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::FoldingControls ), 0 ); } } bool QgsCodeEditor::readHistoryFile() { if ( mHistoryFilePath.isEmpty() || !QFile::exists( mHistoryFilePath ) ) return false; QFile file( mHistoryFilePath ); if ( file.open( QIODevice::ReadOnly ) ) { QTextStream stream( &file ); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // Always use UTF-8 stream.setCodec( "UTF-8" ); #endif QString line; while ( !stream.atEnd() ) { line = stream.readLine(); // line of text excluding '\n' mHistory.append( line ); } syncSoftHistory(); return true; } return false; } void QgsCodeEditor::syncSoftHistory() { mSoftHistory = mHistory; mSoftHistory.append( QString() ); mSoftHistoryIndex = mSoftHistory.length() - 1; } void QgsCodeEditor::updateSoftHistory() { mSoftHistory[mSoftHistoryIndex] = text(); } void QgsCodeEditor::updateHistory( const QStringList &commands, bool skipSoftHistory ) { if ( commands.size() > 1 ) { mHistory.append( commands ); } else if ( !commands.value( 0 ).isEmpty() ) { const QString command = commands.value( 0 ); if ( mHistory.empty() || command != mHistory.constLast() ) mHistory.append( command ); } if ( !skipSoftHistory ) syncSoftHistory(); } void QgsCodeEditor::populateContextMenu( QMenu * ) { } QString QgsCodeEditor::reformatCodeString( const QString &string, bool &ok, QString & ) const { ok = false; return string; } void QgsCodeEditor::updatePrompt() { if ( mInterpreter ) { const QString prompt = mInterpreter->promptForState( mInterpreter->currentState() ); SendScintilla( QsciScintilla::SCI_MARGINSETTEXT, static_cast< uintptr_t >( 0 ), prompt.toUtf8().constData() ); } } QgsCodeInterpreter *QgsCodeEditor::interpreter() const { return mInterpreter; } void QgsCodeEditor::setInterpreter( QgsCodeInterpreter *newInterpreter ) { mInterpreter = newInterpreter; updatePrompt(); } // Find the source substring index that most closely matches the target string int findMinimalDistanceIndex( const QString &source, const QString &target ) { const int index = std::min( source.length(), target.length() ); const int d0 = QgsStringUtils::levenshteinDistance( source.left( index ), target ); if ( d0 == 0 ) return index; int refDistanceMore = d0; int refIndexMore = index; if ( index < source.length() - 1 ) { while ( true ) { const int newDistance = QgsStringUtils::levenshteinDistance( source.left( refIndexMore + 1 ), target ); if ( newDistance <= refDistanceMore ) { refDistanceMore = newDistance; refIndexMore++; if ( refIndexMore == source.length() - 1 ) break; } else { break; } } } int refDistanceLess = d0; int refIndexLess = index; if ( index > 0 ) { while ( true ) { const int newDistance = QgsStringUtils::levenshteinDistance( source.left( refIndexLess - 1 ), target ); if ( newDistance <= refDistanceLess ) { refDistanceLess = newDistance; refIndexLess--; if ( refIndexLess == 0 ) break; } else { break; } } } if ( refDistanceMore < refDistanceLess ) return refIndexMore; else return refIndexLess; } void QgsCodeEditor::reformatCode() { if ( !( languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat ) ) return; int line = 0; int index = 0; getCursorPosition( &line, &index ); const QString textBeforeCursor = text( 0, positionFromLineIndex( line, index ) ); const QString originalText = text(); bool ok = false; QString error; const QString newText = reformatCodeString( originalText, ok, error ); if ( !ok ) { if ( !error.isEmpty() ) { // TODO raise } } if ( originalText == newText ) return; // try to preserve the cursor position and scroll position const int oldScrollValue = verticalScrollBar()->value(); const int linearPosition = findMinimalDistanceIndex( newText, textBeforeCursor ); beginUndoAction(); selectAll(); removeSelectedText(); insert( newText ); lineIndexFromPosition( linearPosition, &line, &index ); setCursorPosition( line, index ); verticalScrollBar()->setValue( oldScrollValue ); endUndoAction(); } QStringList QgsCodeEditor::history() const { return mHistory; } void QgsCodeEditor::runCommand( const QString &command ) { updateHistory( { command } ); if ( mInterpreter ) mInterpreter->exec( command ); clear(); moveCursorToEnd(); } void QgsCodeEditor::clearSessionHistory() { mHistory.clear(); readHistoryFile(); syncSoftHistory(); emit sessionHistoryCleared(); } void QgsCodeEditor::clearPersistentHistory() { mHistory.clear(); if ( !mHistoryFilePath.isEmpty() && QFile::exists( mHistoryFilePath ) ) { QFile file( mHistoryFilePath ); file.open( QFile::WriteOnly | QFile::Truncate ); } emit persistentHistoryCleared(); } bool QgsCodeEditor::writeHistoryFile() { if ( mHistoryFilePath.isEmpty() ) return false; QFile f( mHistoryFilePath ); if ( !f.open( QFile::WriteOnly | QIODevice::Truncate ) ) { return false; } QTextStream ts( &f ); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) ts.setCodec( "UTF-8" ); #endif for ( const QString &command : std::as_const( mHistory ) ) { ts << command + '\n'; } return true; } void QgsCodeEditor::showPreviousCommand() { if ( mSoftHistoryIndex < mSoftHistory.length() - 1 && !mSoftHistory.isEmpty() ) { mSoftHistoryIndex += 1; setText( mSoftHistory[mSoftHistoryIndex] ); moveCursorToEnd(); } } void QgsCodeEditor::showNextCommand() { if ( mSoftHistoryIndex > 0 && !mSoftHistory.empty() ) { mSoftHistoryIndex -= 1; setText( mSoftHistory[mSoftHistoryIndex] ); moveCursorToEnd(); } } void QgsCodeEditor::showHistory() { QgsCodeEditorHistoryDialog *dialog = new QgsCodeEditorHistoryDialog( this, this ); dialog->setAttribute( Qt::WA_DeleteOnClose ); dialog->show(); dialog->activateWindow(); } void QgsCodeEditor::removeHistoryCommand( int index ) { // remove item from the command history (just for the current session) mHistory.removeAt( index ); mSoftHistory.removeAt( index ); if ( index < mSoftHistoryIndex ) { mSoftHistoryIndex -= 1; if ( mSoftHistoryIndex < 0 ) mSoftHistoryIndex = mSoftHistory.length() - 1; } } void QgsCodeEditor::insertText( const QString &text ) { // Insert the text or replace selected text if ( hasSelectedText() ) { replaceSelectedText( text ); } else { int line, index; getCursorPosition( &line, &index ); insertAt( text, line, index ); setCursorPosition( line, index + text.length() ); } } QColor QgsCodeEditor::defaultColor( QgsCodeEditorColorScheme::ColorRole role, const QString &theme ) { if ( theme.isEmpty() && QgsApplication::themeName() == QLatin1String( "default" ) ) { // if using default theme, take certain colors from the palette const QPalette pal = qApp->palette(); switch ( role ) { case QgsCodeEditorColorScheme::ColorRole::SelectionBackground: return pal.color( QPalette::Highlight ); case QgsCodeEditorColorScheme::ColorRole::SelectionForeground: return pal.color( QPalette::HighlightedText ); default: break; } } else if ( theme.isEmpty() ) { // non default theme (e.g. Blend of Gray). Take colors from theme ini file... const QSettings ini( QgsApplication::uiThemes().value( QgsApplication::themeName() ) + "/qscintilla.ini", QSettings::IniFormat ); static const QMap< QgsCodeEditorColorScheme::ColorRole, QString > sColorRoleToIniKey { {QgsCodeEditorColorScheme::ColorRole::Default, QStringLiteral( "python/defaultFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Keyword, QStringLiteral( "python/keywordFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Class, QStringLiteral( "python/classFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Method, QStringLiteral( "python/methodFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Decoration, QStringLiteral( "python/decoratorFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Number, QStringLiteral( "python/numberFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Comment, QStringLiteral( "python/commentFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::CommentLine, QStringLiteral( "sql/commentLineFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::CommentBlock, QStringLiteral( "python/commentBlockFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Background, QStringLiteral( "python/paperBackgroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Cursor, QStringLiteral( "cursorColor" ) }, {QgsCodeEditorColorScheme::ColorRole::CaretLine, QStringLiteral( "caretLineColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Operator, QStringLiteral( "sql/operatorFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::QuotedOperator, QStringLiteral( "sql/QuotedOperatorFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Identifier, QStringLiteral( "sql/identifierFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::QuotedIdentifier, QStringLiteral( "sql/QuotedIdentifierFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Tag, QStringLiteral( "html/tagFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::UnknownTag, QStringLiteral( "html/unknownTagFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::SingleQuote, QStringLiteral( "sql/singleQuoteFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::DoubleQuote, QStringLiteral( "sql/doubleQuoteFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote, QStringLiteral( "python/tripleSingleQuoteFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote, QStringLiteral( "python/tripleDoubleQuoteFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::MarginBackground, QStringLiteral( "marginBackgroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::MarginForeground, QStringLiteral( "marginForegroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::SelectionBackground, QStringLiteral( "selectionBackgroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::SelectionForeground, QStringLiteral( "selectionForegroundColor" ) }, {QgsCodeEditorColorScheme::ColorRole::MatchedBraceBackground, QStringLiteral( "matchedBraceBackground" ) }, {QgsCodeEditorColorScheme::ColorRole::MatchedBraceForeground, QStringLiteral( "matchedBraceColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Edge, QStringLiteral( "edgeColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Fold, QStringLiteral( "foldColor" ) }, {QgsCodeEditorColorScheme::ColorRole::Error, QStringLiteral( "stderrFontColor" ) }, {QgsCodeEditorColorScheme::ColorRole::ErrorBackground, QStringLiteral( "stderrBackground" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconForeground, QStringLiteral( "foldIconForeground" ) }, {QgsCodeEditorColorScheme::ColorRole::FoldIconHalo, QStringLiteral( "foldIconHalo" ) }, {QgsCodeEditorColorScheme::ColorRole::IndentationGuide, QStringLiteral( "indentationGuide" ) }, }; const QgsCodeEditorColorScheme defaultScheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( QStringLiteral( "default" ) ); return QgsSymbolLayerUtils::decodeColor( ini.value( sColorRoleToIniKey.value( role ), defaultScheme.color( role ).name() ).toString() ); } const QgsCodeEditorColorScheme scheme = QgsGui::codeEditorColorSchemeRegistry()->scheme( theme.isEmpty() ? QStringLiteral( "default" ) : theme ); return scheme.color( role ); } QColor QgsCodeEditor::color( QgsCodeEditorColorScheme::ColorRole role ) { const QgsSettings settings; if ( !settings.value( QStringLiteral( "codeEditor/overrideColors" ), false, QgsSettings::Gui ).toBool() ) { const QString theme = settings.value( QStringLiteral( "codeEditor/colorScheme" ), QString(), QgsSettings::Gui ).toString(); return defaultColor( role, theme ); } else { const QString color = settings.value( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), QString(), QgsSettings::Gui ).toString(); return color.isEmpty() ? defaultColor( role ) : QgsSymbolLayerUtils::decodeColor( color ); } } void QgsCodeEditor::setColor( QgsCodeEditorColorScheme::ColorRole role, const QColor &color ) { QgsSettings settings; if ( color.isValid() ) { settings.setValue( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), color.name(), QgsSettings::Gui ); } else { settings.remove( QStringLiteral( "codeEditor/%1" ).arg( sColorRoleToSettingsKey.value( role ) ), QgsSettings::Gui ); } } // Settings for font and fontsize bool QgsCodeEditor::isFixedPitch( const QFont &font ) { return font.fixedPitch(); } QFont QgsCodeEditor::getMonospaceFont() { QFont font = QFontDatabase::systemFont( QFontDatabase::FixedFont ); const QgsSettings settings; if ( !settings.value( QStringLiteral( "codeEditor/fontfamily" ), QString(), QgsSettings::Gui ).toString().isEmpty() ) font.setFamily( settings.value( QStringLiteral( "codeEditor/fontfamily" ), QString(), QgsSettings::Gui ).toString() ); const int fontSize = settings.value( QStringLiteral( "codeEditor/fontsize" ), 0, QgsSettings::Gui ).toInt(); #ifdef Q_OS_MAC if ( fontSize > 0 ) font.setPointSize( fontSize ); else { // The font size gotten from getMonospaceFont() is too small on Mac font.setPointSize( QLabel().font().pointSize() ); } #else if ( fontSize > 0 ) font.setPointSize( fontSize ); else { const int fontSize = settings.value( QStringLiteral( "qgis/stylesheet/fontPointSize" ), 10 ).toInt(); font.setPointSize( fontSize ); } #endif font.setBold( false ); return font; } void QgsCodeEditor::setCustomAppearance( const QString &scheme, const QMap &customColors, const QString &fontFamily, int fontSize ) { mUseDefaultSettings = false; mOverrideColors = !customColors.isEmpty(); mColorScheme = scheme; mCustomColors = customColors; mFontFamily = fontFamily; mFontSize = fontSize; setSciWidget(); initializeLexer(); } void QgsCodeEditor::addWarning( const int lineNumber, const QString &warning ) { setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::ErrorIndicators ), "000" ); markerAdd( lineNumber, MARKER_NUMBER ); QFont font = lexerFont(); font.setItalic( true ); const QsciStyle styleAnn = QsciStyle( -1, QStringLiteral( "Annotation" ), lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), lexerColor( QgsCodeEditorColorScheme::ColorRole::ErrorBackground ), font, true ); annotate( lineNumber, warning, styleAnn ); mWarningLines.push_back( lineNumber ); } void QgsCodeEditor::clearWarnings() { for ( const int line : mWarningLines ) { markerDelete( line ); clearAnnotations( line ); } setMarginWidth( static_cast< int >( QgsCodeEditor::MarginRole::ErrorIndicators ), 0 ); mWarningLines.clear(); } bool QgsCodeEditor::isCursorOnLastLine() const { int line = 0; int index = 0; getCursorPosition( &line, &index ); return line == lines() - 1; } void QgsCodeEditor::setHistoryFilePath( const QString &path ) { mHistoryFilePath = path; readHistoryFile(); } void QgsCodeEditor::moveCursorToStart() { setCursorPosition( 0, 0 ); ensureCursorVisible(); ensureLineVisible( 0 ); if ( mMode == QgsCodeEditor::Mode::CommandInput ) updatePrompt(); } void QgsCodeEditor::moveCursorToEnd() { const int endLine = lines() - 1; const int endLineLength = lineLength( endLine ); setCursorPosition( endLine, endLineLength ); ensureCursorVisible(); ensureLineVisible( endLine ); if ( mMode == QgsCodeEditor::Mode::CommandInput ) updatePrompt(); } QgsCodeInterpreter::~QgsCodeInterpreter() = default; int QgsCodeInterpreter::exec( const QString &command ) { mState = execCommandImpl( command ); return mState; }