mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-03 00:04:47 -04:00
1594 lines
50 KiB
C++
1594 lines
50 KiB
C++
/***************************************************************************
|
|
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 "moc_qgscodeeditor.cpp"
|
|
#include "qgssettings.h"
|
|
#include "qgssymbollayerutils.h"
|
|
#include "qgsgui.h"
|
|
#include "qgscodeeditorcolorschemeregistry.h"
|
|
#include "qgscodeeditorhistorydialog.h"
|
|
#include "qgsstringutils.h"
|
|
#include "qgsfontutils.h"
|
|
#include "qgssettingsentryimpl.h"
|
|
|
|
#include <QLabel>
|
|
#include <QWidget>
|
|
#include <QFont>
|
|
#include <QFontDatabase>
|
|
#include <QDebug>
|
|
#include <QFocusEvent>
|
|
#include <Qsci/qscistyle.h>
|
|
#include <QMenu>
|
|
#include <QClipboard>
|
|
#include <QScrollBar>
|
|
#include <QMessageBox>
|
|
#include "Qsci/qscilexer.h"
|
|
|
|
///@cond PRIVATE
|
|
const QgsSettingsEntryBool *QgsCodeEditor::settingContextHelpHover = new QgsSettingsEntryBool( QStringLiteral( "context-help-hover" ), sTreeCodeEditor, false, QStringLiteral( "Whether the context help should works on hovered words" ) );
|
|
///@endcond PRIVATE
|
|
|
|
|
|
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" ) },
|
|
{ QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QStringLiteral( "searchMatchBackground" ) }
|
|
};
|
|
|
|
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, [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;
|
|
}
|
|
}
|
|
|
|
#if QSCINTILLA_VERSION < 0x020d03
|
|
installEventFilter( this );
|
|
#endif
|
|
|
|
mLastEditTimer = new QTimer( this );
|
|
mLastEditTimer->setSingleShot( true );
|
|
mLastEditTimer->setInterval( 1000 );
|
|
connect( mLastEditTimer, &QTimer::timeout, this, &QgsCodeEditor::onLastEditTimeout );
|
|
connect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
|
|
}
|
|
|
|
// 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 <phil@riverbankcomputing.com>
|
|
|
|
* 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 );
|
|
}
|
|
onLastEditTimeout();
|
|
}
|
|
|
|
// 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 ( event->matches( QKeySequence::StandardKey::HelpContents ) )
|
|
{
|
|
// Check if some text is selected
|
|
QString text = selectedText();
|
|
|
|
// Check if mouse is hovering over a word
|
|
if ( text.isEmpty() && settingContextHelpHover->value() )
|
|
{
|
|
text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
|
|
}
|
|
|
|
// Otherwise, check if there is a word at the current text cursor position
|
|
if ( text.isEmpty() )
|
|
{
|
|
int line, index;
|
|
getCursorPosition( &line, &index );
|
|
text = wordAtLineIndex( line, index );
|
|
}
|
|
emit helpRequested( text );
|
|
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;
|
|
}
|
|
}
|
|
|
|
const bool ctrlModifier = event->modifiers() & Qt::ControlModifier;
|
|
const bool altModifier = event->modifiers() & Qt::AltModifier;
|
|
|
|
// Ctrl+Alt+F: reformat code
|
|
const bool canReformat = languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat;
|
|
if ( !isReadOnly() && canReformat && ctrlModifier && altModifier && event->key() == Qt::Key_F )
|
|
{
|
|
event->accept();
|
|
reformatCode();
|
|
return;
|
|
}
|
|
|
|
// Toggle comment when user presses Ctrl+:
|
|
const bool canToggle = languageCapabilities() & Qgis::ScriptLanguageCapability::ToggleComment;
|
|
if ( !isReadOnly() && canToggle && ctrlModifier && event->key() == Qt::Key_Colon )
|
|
{
|
|
event->accept();
|
|
toggleComment();
|
|
return;
|
|
}
|
|
|
|
QsciScintilla::keyPressEvent( event );
|
|
|
|
// Update calltips unless event is autorepeat
|
|
if ( !event->isAutoRepeat() )
|
|
{
|
|
callTip();
|
|
}
|
|
}
|
|
|
|
void QgsCodeEditor::contextMenuEvent( QContextMenuEvent *event )
|
|
{
|
|
switch ( mMode )
|
|
{
|
|
case Mode::ScriptEditor:
|
|
{
|
|
QMenu *menu = createStandardContextMenu();
|
|
menu->setAttribute( Qt::WA_DeleteOnClose );
|
|
|
|
if ( ( languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat ) || ( languageCapabilities() & Qgis::ScriptLanguageCapability::CheckSyntax ) )
|
|
{
|
|
menu->addSeparator();
|
|
}
|
|
|
|
if ( languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat )
|
|
{
|
|
QAction *reformatAction = new QAction( tr( "Reformat Code" ), menu );
|
|
reformatAction->setShortcut( QStringLiteral( "Ctrl+Alt+F" ) );
|
|
reformatAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconFormatCode.svg" ) ) );
|
|
reformatAction->setEnabled( !isReadOnly() );
|
|
connect( reformatAction, &QAction::triggered, this, &QgsCodeEditor::reformatCode );
|
|
menu->addAction( reformatAction );
|
|
}
|
|
|
|
if ( languageCapabilities() & Qgis::ScriptLanguageCapability::CheckSyntax )
|
|
{
|
|
QAction *syntaxCheckAction = new QAction( tr( "Check Syntax" ), menu );
|
|
syntaxCheckAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSyntaxErrorConsole.svg" ) ) );
|
|
connect( syntaxCheckAction, &QAction::triggered, this, &QgsCodeEditor::checkSyntax );
|
|
menu->addAction( syntaxCheckAction );
|
|
}
|
|
|
|
if ( languageCapabilities() & Qgis::ScriptLanguageCapability::ToggleComment )
|
|
{
|
|
QAction *toggleCommentAction = new QAction( tr( "Toggle Comment" ), menu );
|
|
toggleCommentAction->setShortcut( QStringLiteral( "Ctrl+:" ) );
|
|
toggleCommentAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconCommentEditorConsole.svg" ), palette().color( QPalette::ColorRole::WindowText ) ) );
|
|
toggleCommentAction->setEnabled( !isReadOnly() );
|
|
connect( toggleCommentAction, &QAction::triggered, this, &QgsCodeEditor::toggleComment );
|
|
menu->addAction( toggleCommentAction );
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
bool QgsCodeEditor::event( QEvent *event )
|
|
{
|
|
if ( event->type() == QEvent::ShortcutOverride )
|
|
{
|
|
if ( QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>( event ) )
|
|
{
|
|
if ( keyEvent->matches( QKeySequence::StandardKey::HelpContents ) )
|
|
{
|
|
// If the user pressed F1, we want to prevent the main help dialog to show
|
|
// and handle the event in QgsCodeEditor::keyPressEvent
|
|
keyEvent->accept();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return QsciScintilla::event( event );
|
|
}
|
|
|
|
bool QgsCodeEditor::eventFilter( QObject *watched, QEvent *event )
|
|
{
|
|
#if QSCINTILLA_VERSION < 0x020d03
|
|
if ( watched == this && event->type() == QEvent::InputMethod )
|
|
{
|
|
// swallow input method events, which cause loss of selected text.
|
|
// See https://sourceforge.net/p/scintilla/bugs/1913/ , which was ported to QScintilla
|
|
// in version 2.13.3
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
return QsciScintilla::eventFilter( watched, event );
|
|
}
|
|
|
|
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() )
|
|
QgsFontUtils::setFontFamily( font, 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 ) );
|
|
|
|
SendScintilla( QsciScintilla::SCI_INDICSETSTYLE, SEARCH_RESULT_INDICATOR, QsciScintilla::INDIC_STRAIGHTBOX );
|
|
SendScintilla( QsciScintilla::SCI_INDICSETFORE, SEARCH_RESULT_INDICATOR, lexerColor( QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground ) );
|
|
SendScintilla( QsciScintilla::SCI_INDICSETALPHA, SEARCH_RESULT_INDICATOR, 100 );
|
|
SendScintilla( QsciScintilla::SCI_INDICSETUNDER, SEARCH_RESULT_INDICATOR, true );
|
|
SendScintilla( QsciScintilla::SCI_INDICGETOUTLINEALPHA, SEARCH_RESULT_INDICATOR, 255 );
|
|
|
|
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::onLastEditTimeout()
|
|
{
|
|
mLastEditTimer->stop();
|
|
emit editingTimeout();
|
|
}
|
|
|
|
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::Batch:
|
|
return tr( "Batch" );
|
|
case Qgis::ScriptLanguage::Bash:
|
|
return tr( "Bash" );
|
|
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 )
|
|
{
|
|
return string;
|
|
}
|
|
|
|
void QgsCodeEditor::showMessage( const QString &title, const QString &message, Qgis::MessageLevel level )
|
|
{
|
|
switch ( level )
|
|
{
|
|
case Qgis::Info:
|
|
case Qgis::Success:
|
|
case Qgis::NoLevel:
|
|
QMessageBox::information( this, title, message );
|
|
break;
|
|
|
|
case Qgis::Warning:
|
|
QMessageBox::warning( this, title, message );
|
|
break;
|
|
|
|
case Qgis::Critical:
|
|
QMessageBox::critical( this, title, message );
|
|
break;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
const QString textBeforeCursor = text( 0, linearPosition() );
|
|
const QString originalText = text();
|
|
const QString newText = reformatCodeString( originalText );
|
|
|
|
if ( originalText == newText )
|
|
return;
|
|
|
|
// try to preserve the cursor position and scroll position
|
|
const int oldScrollValue = verticalScrollBar()->value();
|
|
const int linearIndex = findMinimalDistanceIndex( newText, textBeforeCursor );
|
|
|
|
beginUndoAction();
|
|
selectAll();
|
|
removeSelectedText();
|
|
insert( newText );
|
|
setLinearPosition( linearIndex );
|
|
verticalScrollBar()->setValue( oldScrollValue );
|
|
endUndoAction();
|
|
}
|
|
|
|
bool QgsCodeEditor::checkSyntax()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void QgsCodeEditor::toggleComment()
|
|
{
|
|
}
|
|
|
|
void QgsCodeEditor::toggleLineComments( const QString &commentPrefix )
|
|
{
|
|
if ( isReadOnly() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
beginUndoAction();
|
|
int startLine, startPos, endLine, endPos;
|
|
if ( hasSelectedText() )
|
|
{
|
|
getSelection( &startLine, &startPos, &endLine, &endPos );
|
|
}
|
|
else
|
|
{
|
|
getCursorPosition( &startLine, &startPos );
|
|
endLine = startLine;
|
|
endPos = startPos;
|
|
}
|
|
|
|
// Check comment state and minimum indentation for each selected line
|
|
bool allEmpty = true;
|
|
bool allCommented = true;
|
|
int minIndentation = -1;
|
|
for ( int line = startLine; line <= endLine; line++ )
|
|
{
|
|
const QString stripped = text( line ).trimmed();
|
|
if ( !stripped.isEmpty() )
|
|
{
|
|
allEmpty = false;
|
|
if ( !stripped.startsWith( commentPrefix ) )
|
|
{
|
|
allCommented = false;
|
|
}
|
|
if ( minIndentation == -1 || minIndentation > indentation( line ) )
|
|
{
|
|
minIndentation = indentation( line );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special case, only empty lines
|
|
if ( allEmpty )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Selection shift to keep the same selected text after the prefix is added/removed
|
|
int delta = 0;
|
|
|
|
const int prefixLength = static_cast<int>( commentPrefix.length() );
|
|
|
|
const bool startLineEmpty = ( text( startLine ).trimmed().isEmpty() );
|
|
const bool endLineEmpty = ( text( endLine ).trimmed().isEmpty() );
|
|
|
|
for ( int line = startLine; line <= endLine; line++ )
|
|
{
|
|
const QString stripped = text( line ).trimmed();
|
|
|
|
// Empty line
|
|
if ( stripped.isEmpty() )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( !allCommented )
|
|
{
|
|
insertAt( commentPrefix + ' ', line, minIndentation );
|
|
delta = -( prefixLength + 1 );
|
|
}
|
|
else
|
|
{
|
|
if ( !stripped.startsWith( commentPrefix ) )
|
|
{
|
|
continue;
|
|
}
|
|
if ( stripped.startsWith( commentPrefix + ' ' ) )
|
|
{
|
|
delta = prefixLength + 1;
|
|
}
|
|
else
|
|
{
|
|
delta = prefixLength;
|
|
}
|
|
setSelection( line, indentation( line ), line, indentation( line ) + delta );
|
|
removeSelectedText();
|
|
}
|
|
}
|
|
|
|
endUndoAction();
|
|
setSelection( startLine, startPos - ( startLineEmpty ? 0 : delta ), endLine, endPos - ( endLineEmpty ? 0 : delta ) );
|
|
}
|
|
|
|
void QgsCodeEditor::adjustScrollWidth()
|
|
{
|
|
// A zero width would make setScrollWidth crash
|
|
long maxWidth = 10;
|
|
|
|
// Get the number of lines
|
|
int lineCount = lines();
|
|
|
|
// Loop through all the lines to get the longest one
|
|
for ( int line = 0; line < lineCount; line++ )
|
|
{
|
|
// Get the linear position at the end of the current line
|
|
const long endLine = SendScintilla( SCI_GETLINEENDPOSITION, line );
|
|
// Get the x coordinates of the end of the line
|
|
const long x = SendScintilla( SCI_POINTXFROMPOSITION, 0, endLine );
|
|
maxWidth = std::max( maxWidth, x );
|
|
}
|
|
|
|
// Use the longest line width as the new scroll width
|
|
setScrollWidth( static_cast<int>( maxWidth ) );
|
|
}
|
|
|
|
void QgsCodeEditor::setText( const QString &text )
|
|
{
|
|
disconnect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
|
|
QsciScintilla::setText( text );
|
|
connect( this, &QgsCodeEditor::textChanged, mLastEditTimer, qOverload<>( &QTimer::start ) );
|
|
onLastEditTimeout();
|
|
adjustScrollWidth();
|
|
}
|
|
|
|
int QgsCodeEditor::editingTimeoutInterval() const
|
|
{
|
|
return mLastEditTimer->interval();
|
|
}
|
|
|
|
void QgsCodeEditor::setEditingTimeoutInterval( int timeout )
|
|
{
|
|
mLastEditTimer->setInterval( timeout );
|
|
}
|
|
|
|
|
|
QStringList QgsCodeEditor::history() const
|
|
{
|
|
return mHistory;
|
|
}
|
|
|
|
void QgsCodeEditor::runCommand( const QString &command, bool skipHistory )
|
|
{
|
|
if ( !skipHistory )
|
|
{
|
|
updateHistory( { command } );
|
|
if ( mFlags & QgsCodeEditor::Flag::ImmediatelyUpdateHistory )
|
|
writeHistoryFile();
|
|
}
|
|
|
|
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" ) },
|
|
{ QgsCodeEditorColorScheme::ColorRole::SearchMatchBackground, QStringLiteral( "searchMatchBackground" ) },
|
|
};
|
|
|
|
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() )
|
|
QgsFontUtils::setFontFamily( font, 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<QgsCodeEditorColorScheme::ColorRole, QColor> &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();
|
|
}
|
|
|
|
int QgsCodeEditor::linearPosition() const
|
|
{
|
|
return static_cast<int>( SendScintilla( SCI_GETCURRENTPOS ) );
|
|
}
|
|
|
|
void QgsCodeEditor::setLinearPosition( int linearIndex )
|
|
{
|
|
int line, index;
|
|
lineIndexFromPosition( linearIndex, &line, &index );
|
|
setCursorPosition( line, index );
|
|
}
|
|
|
|
int QgsCodeEditor::selectionStart() const
|
|
{
|
|
int startLine, startIndex, _;
|
|
getSelection( &startLine, &startIndex, &_, &_ );
|
|
if ( startLine == -1 )
|
|
{
|
|
return linearPosition();
|
|
}
|
|
return positionFromLineIndex( startLine, startIndex );
|
|
}
|
|
|
|
int QgsCodeEditor::selectionEnd() const
|
|
{
|
|
int endLine, endIndex, _;
|
|
getSelection( &_, &_, &endLine, &endIndex );
|
|
if ( endLine == -1 )
|
|
{
|
|
return linearPosition();
|
|
}
|
|
return positionFromLineIndex( endLine, endIndex );
|
|
}
|
|
|
|
void QgsCodeEditor::setLinearSelection( int start, int end )
|
|
{
|
|
int startLine, startIndex, endLine, endIndex;
|
|
lineIndexFromPosition( start, &startLine, &startIndex );
|
|
lineIndexFromPosition( end, &endLine, &endIndex );
|
|
setSelection( startLine, startIndex, endLine, endIndex );
|
|
}
|
|
|
|
QgsCodeInterpreter::~QgsCodeInterpreter() = default;
|
|
|
|
int QgsCodeInterpreter::exec( const QString &command )
|
|
{
|
|
mState = execCommandImpl( command );
|
|
return mState;
|
|
}
|
|
|
|
|
|
int QgsCodeEditor::wrapPosition( int line )
|
|
{
|
|
// If wrapping is disabled, return -1
|
|
if ( wrapMode() == WrapNone )
|
|
{
|
|
return -1;
|
|
}
|
|
// Get the current line
|
|
if ( line == -1 )
|
|
{
|
|
int _index;
|
|
lineIndexFromPosition( linearPosition(), &line, &_index );
|
|
}
|
|
|
|
// If line isn't wrapped, return -1
|
|
if ( SendScintilla( SCI_WRAPCOUNT, line ) <= 1 )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
// Get the linear position at the end of the current line
|
|
const long endLine = SendScintilla( SCI_GETLINEENDPOSITION, line );
|
|
// Get the y coordinates of the start of the last wrapped line
|
|
const long y = SendScintilla( SCI_POINTYFROMPOSITION, 0, endLine );
|
|
// Return the linear position of the start of the last wrapped line
|
|
return static_cast<int>( SendScintilla( SCI_POSITIONFROMPOINT, 0, y ) );
|
|
}
|
|
|
|
|
|
// Adapted from QsciScintilla source code (qsciscintilla.cpp) to handle line wrap
|
|
void QgsCodeEditor::callTip()
|
|
{
|
|
if ( callTipsStyle() == CallTipsNone || lexer() == nullptr )
|
|
{
|
|
return;
|
|
}
|
|
|
|
QsciAbstractAPIs *apis = lexer()->apis();
|
|
|
|
if ( !apis )
|
|
return;
|
|
|
|
int pos, commas = 0;
|
|
bool found = false;
|
|
char ch;
|
|
|
|
pos = linearPosition();
|
|
|
|
// Move backwards through the line looking for the start of the current
|
|
// call tip and working out which argument it is.
|
|
while ( ( ch = getCharacter( pos ) ) != '\0' )
|
|
{
|
|
if ( ch == ',' )
|
|
++commas;
|
|
else if ( ch == ')' )
|
|
{
|
|
int depth = 1;
|
|
|
|
// Ignore everything back to the start of the corresponding
|
|
// parenthesis.
|
|
while ( ( ch = getCharacter( pos ) ) != '\0' )
|
|
{
|
|
if ( ch == ')' )
|
|
++depth;
|
|
else if ( ch == '(' && --depth == 0 )
|
|
break;
|
|
}
|
|
}
|
|
else if ( ch == '(' )
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Cancel any existing call tip.
|
|
SendScintilla( SCI_CALLTIPCANCEL );
|
|
|
|
// Done if there is no new call tip to set.
|
|
if ( !found )
|
|
return;
|
|
|
|
int contextStart, lastWordStart;
|
|
QStringList context = apiContext( pos, contextStart, lastWordStart );
|
|
|
|
if ( context.isEmpty() )
|
|
return;
|
|
|
|
// The last word is complete, not partial.
|
|
context << QString();
|
|
|
|
QList<int> ctShifts;
|
|
QStringList ctEntries = apis->callTips( context, commas, callTipsStyle(), ctShifts );
|
|
|
|
int nbEntries = ctEntries.count();
|
|
|
|
if ( nbEntries == 0 )
|
|
return;
|
|
|
|
const int maxNumberOfCallTips = callTipsVisible();
|
|
|
|
// Clip to at most maxNumberOfCallTips entries.
|
|
if ( maxNumberOfCallTips > 0 && maxNumberOfCallTips < nbEntries )
|
|
{
|
|
ctEntries = ctEntries.mid( 0, maxNumberOfCallTips );
|
|
nbEntries = maxNumberOfCallTips;
|
|
}
|
|
|
|
int shift;
|
|
QString ct;
|
|
|
|
int nbShifts = ctShifts.count();
|
|
|
|
if ( maxNumberOfCallTips < 0 && nbEntries > 1 )
|
|
{
|
|
shift = ( nbShifts > 0 ? ctShifts.first() : 0 );
|
|
ct = ctEntries[0];
|
|
ct.prepend( '\002' );
|
|
}
|
|
else
|
|
{
|
|
if ( nbShifts > nbEntries )
|
|
nbShifts = nbEntries;
|
|
|
|
// Find the biggest shift.
|
|
shift = 0;
|
|
|
|
for ( int i = 0; i < nbShifts; ++i )
|
|
{
|
|
int sh = ctShifts[i];
|
|
|
|
if ( shift < sh )
|
|
shift = sh;
|
|
}
|
|
|
|
ct = ctEntries.join( "\n" );
|
|
}
|
|
|
|
QByteArray ctBa = ct.toLatin1();
|
|
const char *cts = ctBa.data();
|
|
|
|
const int currentWrapPosition = wrapPosition();
|
|
|
|
if ( currentWrapPosition != -1 )
|
|
{
|
|
SendScintilla( SCI_CALLTIPSHOW, currentWrapPosition, cts );
|
|
}
|
|
else
|
|
{
|
|
// Shift the position of the call tip (to take any context into account) but
|
|
// don't go before the start of the line.
|
|
if ( shift )
|
|
{
|
|
int ctmin = static_cast<int>( SendScintilla( SCI_POSITIONFROMLINE, SendScintilla( SCI_LINEFROMPOSITION, ct ) ) );
|
|
if ( lastWordStart - shift < ctmin )
|
|
lastWordStart = ctmin;
|
|
}
|
|
|
|
int line, index;
|
|
lineIndexFromPosition( lastWordStart, &line, &index );
|
|
SendScintilla( SCI_CALLTIPSHOW, positionFromLineIndex( line, index ), cts );
|
|
}
|
|
|
|
// Done if there is more than one call tip.
|
|
if ( nbEntries > 1 )
|
|
return;
|
|
|
|
// Highlight the current argument.
|
|
const char *astart;
|
|
|
|
if ( commas == 0 )
|
|
astart = strchr( cts, '(' );
|
|
else
|
|
for ( astart = strchr( cts, ',' ); astart && --commas > 0; astart = strchr( astart + 1, ',' ) )
|
|
;
|
|
|
|
if ( !astart )
|
|
return;
|
|
|
|
astart++;
|
|
if ( !*astart )
|
|
return;
|
|
|
|
// The end is at the next comma or unmatched closing parenthesis.
|
|
const char *aend;
|
|
int depth = 0;
|
|
|
|
for ( aend = astart; *aend; ++aend )
|
|
{
|
|
char ch = *aend;
|
|
|
|
if ( ch == ',' && depth == 0 )
|
|
break;
|
|
else if ( ch == '(' )
|
|
++depth;
|
|
else if ( ch == ')' )
|
|
{
|
|
if ( depth == 0 )
|
|
break;
|
|
|
|
--depth;
|
|
}
|
|
}
|
|
|
|
if ( astart != aend )
|
|
SendScintilla( SCI_CALLTIPSETHLT, astart - cts, aend - cts );
|
|
}
|
|
|
|
|
|
// Duplicated from QsciScintilla source code (qsciscintilla.cpp)
|
|
// Get the "next" character (ie. the one before the current position) in the
|
|
// current line. The character will be '\0' if there are no more.
|
|
char QgsCodeEditor::getCharacter( int &pos ) const
|
|
{
|
|
if ( pos <= 0 )
|
|
return '\0';
|
|
|
|
char ch = static_cast<char>( SendScintilla( SCI_GETCHARAT, --pos ) );
|
|
|
|
// Don't go past the end of the previous line.
|
|
if ( ch == '\n' || ch == '\r' )
|
|
{
|
|
++pos;
|
|
return '\0';
|
|
}
|
|
|
|
return ch;
|
|
}
|