QGIS API Documentation 3.41.0-Master (3440c17df1d)
Loading...
Searching...
No Matches
qgscodeeditorwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscodeeditorwidget.cpp
3 --------------------------------------
4 Date : May 2024
5 Copyright : (C) 2024 by Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgscodeeditorwidget.h"
17#include "moc_qgscodeeditorwidget.cpp"
18#include "qgscodeeditor.h"
19#include "qgsfilterlineedit.h"
20#include "qgsapplication.h"
21#include "qgsguiutils.h"
22#include "qgsmessagebar.h"
24#include "qgscodeeditorpython.h"
27#include "qgsjsonutils.h"
28#include "nlohmann/json.hpp"
29#include "qgssettings.h"
30
31#include <QVBoxLayout>
32#include <QToolButton>
33#include <QCheckBox>
34#include <QShortcut>
35#include <QGridLayout>
36#include <QDesktopServices>
37#include <QProcess>
38#include <QFileInfo>
39#include <QDir>
40#include <QNetworkRequest>
41
43 QgsCodeEditor *editor,
44 QgsMessageBar *messageBar,
45 QWidget *parent )
46 : QgsPanelWidget( parent )
47 , mEditor( editor )
48 , mMessageBar( messageBar )
49{
50 Q_ASSERT( mEditor );
51
52 mEditor->installEventFilter( this );
53 installEventFilter( this );
54
55 QVBoxLayout *vl = new QVBoxLayout();
56 vl->setContentsMargins( 0, 0, 0, 0 );
57 vl->setSpacing( 0 );
58 vl->addWidget( editor, 1 );
59
60 if ( !mMessageBar )
61 {
62 QGridLayout *layout = new QGridLayout( mEditor );
63 layout->setContentsMargins( 0, 0, 0, 0 );
64 layout->addItem( new QSpacerItem( 20, 40, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding ), 1, 0, 1, 1 );
65
66 mMessageBar = new QgsMessageBar();
67 QSizePolicy sizePolicy = QSizePolicy( QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Fixed );
68 mMessageBar->setSizePolicy( sizePolicy );
69 layout->addWidget( mMessageBar, 0, 0, 1, 1 );
70 }
71
72 mFindWidget = new QWidget();
73 QGridLayout *layoutFind = new QGridLayout();
74 layoutFind->setContentsMargins( 0, 2, 0, 0 );
75 layoutFind->setSpacing( 1 );
76
77 if ( !mEditor->isReadOnly() )
78 {
79 mShowReplaceBarButton = new QToolButton();
80 mShowReplaceBarButton->setToolTip( tr( "Replace" ) );
81 mShowReplaceBarButton->setCheckable( true );
82 mShowReplaceBarButton->setAutoRaise( true );
83 mShowReplaceBarButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionReplace.svg" ) ) );
84 layoutFind->addWidget( mShowReplaceBarButton, 0, 0 );
85
86 connect( mShowReplaceBarButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::setReplaceBarVisible );
87 }
88
89 mLineEditFind = new QgsFilterLineEdit();
90 mLineEditFind->setShowSearchIcon( true );
91 mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) );
92 layoutFind->addWidget( mLineEditFind, 0, mShowReplaceBarButton ? 1 : 0 );
93
94 mLineEditReplace = new QgsFilterLineEdit();
95 mLineEditReplace->setShowSearchIcon( true );
96 mLineEditReplace->setPlaceholderText( tr( "Replace…" ) );
97 layoutFind->addWidget( mLineEditReplace, 1, mShowReplaceBarButton ? 1 : 0 );
98
99 QHBoxLayout *findButtonLayout = new QHBoxLayout();
100 findButtonLayout->setContentsMargins( 0, 0, 0, 0 );
101 findButtonLayout->setSpacing( 1 );
102 mCaseSensitiveButton = new QToolButton();
103 mCaseSensitiveButton->setToolTip( tr( "Case Sensitive" ) );
104 mCaseSensitiveButton->setCheckable( true );
105 mCaseSensitiveButton->setAutoRaise( true );
106 mCaseSensitiveButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchCaseSensitive.svg" ) ) );
107 findButtonLayout->addWidget( mCaseSensitiveButton );
108
109 mWholeWordButton = new QToolButton( );
110 mWholeWordButton->setToolTip( tr( "Whole Word" ) );
111 mWholeWordButton->setCheckable( true );
112 mWholeWordButton->setAutoRaise( true );
113 mWholeWordButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWholeWord.svg" ) ) );
114 findButtonLayout->addWidget( mWholeWordButton );
115
116 mRegexButton = new QToolButton( );
117 mRegexButton->setToolTip( tr( "Use Regular Expressions" ) );
118 mRegexButton->setCheckable( true );
119 mRegexButton->setAutoRaise( true );
120 mRegexButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchRegex.svg" ) ) );
121 findButtonLayout->addWidget( mRegexButton );
122
123 mWrapAroundButton = new QToolButton();
124 mWrapAroundButton->setToolTip( tr( "Wrap Around" ) );
125 mWrapAroundButton->setCheckable( true );
126 mWrapAroundButton->setAutoRaise( true );
127 mWrapAroundButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWrapAround.svg" ) ) );
128 findButtonLayout->addWidget( mWrapAroundButton );
129
130 mFindPrevButton = new QToolButton();
131 mFindPrevButton->setEnabled( false );
132 mFindPrevButton->setToolTip( tr( "Find Previous" ) );
133 mFindPrevButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchPrevEditorConsole.svg" ) ) );
134 mFindPrevButton->setAutoRaise( true );
135 findButtonLayout->addWidget( mFindPrevButton );
136
137 mFindNextButton = new QToolButton();
138 mFindNextButton->setEnabled( false );
139 mFindNextButton->setToolTip( tr( "Find Next" ) );
140 mFindNextButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchNextEditorConsole.svg" ) ) );
141 mFindNextButton->setAutoRaise( true );
142 findButtonLayout->addWidget( mFindNextButton );
143
144 connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext );
145 connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textSearchChanged );
146 connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext );
147 connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious );
148 connect( mCaseSensitiveButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
149 connect( mWholeWordButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
150 connect( mRegexButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
151 connect( mWrapAroundButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch );
152
153 QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor );
154 findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
155 connect( findShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::triggerFind );
156
157 QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this );
158 findNextShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
159 connect( findNextShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findNext );
160
161 QShortcut *findPreviousShortcut = new QShortcut( QKeySequence::StandardKey::FindPrevious, this );
162 findPreviousShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
163 connect( findPreviousShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findPrevious );
164
165 if ( !mEditor->isReadOnly() )
166 {
167 QShortcut *replaceShortcut = new QShortcut( QKeySequence::StandardKey::Replace, this );
168 replaceShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
169 connect( replaceShortcut, &QShortcut::activated, this, [ = ]
170 {
171 // shortcut toggles bar visibility
172 const bool show = mLineEditReplace->isHidden();
173 setReplaceBarVisible( show );
174
175 // ensure search bar is also visible
176 if ( show )
178 } );
179 }
180
181 // escape on editor hides the find bar
182 QShortcut *closeFindShortcut = new QShortcut( Qt::Key::Key_Escape, this );
183 closeFindShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
184 connect( closeFindShortcut, &QShortcut::activated, this, [this]
185 {
187 mEditor->setFocus();
188 } );
189
190 layoutFind->addLayout( findButtonLayout, 0, mShowReplaceBarButton ? 2 : 1 );
191
192 QHBoxLayout *replaceButtonLayout = new QHBoxLayout();
193 replaceButtonLayout->setContentsMargins( 0, 0, 0, 0 );
194 replaceButtonLayout->setSpacing( 1 );
195
196 mReplaceButton = new QToolButton();
197 mReplaceButton->setText( tr( "Replace" ) );
198 mReplaceButton->setEnabled( false );
199 connect( mReplaceButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::replace );
200 replaceButtonLayout->addWidget( mReplaceButton );
201
202 mReplaceAllButton = new QToolButton();
203 mReplaceAllButton->setText( tr( "Replace All" ) );
204 mReplaceAllButton->setEnabled( false );
205 connect( mReplaceAllButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::replaceAll );
206 replaceButtonLayout->addWidget( mReplaceAllButton );
207
208 layoutFind->addLayout( replaceButtonLayout, 1, mShowReplaceBarButton ? 2 : 1 );
209
210 QToolButton *closeFindButton = new QToolButton( this );
211 closeFindButton->setToolTip( tr( "Close" ) );
212 closeFindButton->setMinimumWidth( QgsGuiUtils::scaleIconSize( 44 ) );
213 closeFindButton->setStyleSheet(
214 "QToolButton { border:none; background-color: rgba(0, 0, 0, 0); }"
215 "QToolButton::menu-button { border:none; background-color: rgba(0, 0, 0, 0); }" );
216 closeFindButton->setCursor( Qt::PointingHandCursor );
217 closeFindButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconClose.svg" ) ) );
218
219 const int iconSize = std::max( 18.0, Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 0.9 );
220 closeFindButton->setIconSize( QSize( iconSize, iconSize ) );
221 closeFindButton->setFixedSize( QSize( iconSize, iconSize ) );
222 connect( closeFindButton, &QAbstractButton::clicked, this, [this]
223 {
225 mEditor->setFocus();
226 } );
227 layoutFind->addWidget( closeFindButton, 0, mShowReplaceBarButton ? 3 : 2 );
228
229 layoutFind->setColumnStretch( mShowReplaceBarButton ? 1 : 0, 1 );
230
231 mFindWidget->setLayout( layoutFind );
232 vl->addWidget( mFindWidget );
233 mFindWidget->hide();
234
235 setReplaceBarVisible( false );
236
237 setLayout( vl );
238
239 mHighlightController = std::make_unique< QgsScrollBarHighlightController >();
240 mHighlightController->setScrollArea( mEditor );
241}
242
243void QgsCodeEditorWidget::resizeEvent( QResizeEvent *event )
244{
245 QgsPanelWidget::resizeEvent( event );
246 updateHighlightController();
247}
248
249void QgsCodeEditorWidget::showEvent( QShowEvent *event )
250{
251 QgsPanelWidget::showEvent( event );
252 updateHighlightController();
253}
254
255bool QgsCodeEditorWidget::eventFilter( QObject *obj, QEvent *event )
256{
257 if ( event->type() == QEvent::FocusIn )
258 {
259 if ( !mFilePath.isEmpty() )
260 {
261 if ( !QFile::exists( mFilePath ) )
262 {
263 // file deleted externally
264 if ( mMessageBar )
265 {
266 mMessageBar->pushCritical( QString(), tr( "The file <b>\"%1\"</b> has been deleted or is not accessible" ).arg( QDir::toNativeSeparators( mFilePath ) ) );
267 }
268 }
269 else
270 {
271 const QFileInfo fi( mFilePath );
272 if ( mLastModified != fi.lastModified() )
273 {
274 // TODO - we should give users a choice of how to react to this, eg "ignore changes"
275 // note -- we intentionally don't call loadFile here -- we want this action to be undo-able
276 QFile file( mFilePath );
277 if ( file.open( QFile::ReadOnly ) )
278 {
279 int currentLine = -1, currentColumn = -1;
280 if ( !mLastModified.isNull() )
281 {
282 mEditor->getCursorPosition( &currentLine, &currentColumn );
283 }
284
285 const QString content = file.readAll();
286
287 // don't clear, instead perform undoable actions:
288 mEditor->beginUndoAction();
289 mEditor->selectAll();
290 mEditor->removeSelectedText();
291 mEditor->insert( content );
292 mEditor->setModified( false );
293 mEditor->recolor();
294 mEditor->endUndoAction();
295
296 mLastModified = fi.lastModified();
297 if ( currentLine >= 0 && currentLine < mEditor->lines() )
298 {
299 mEditor->setCursorPosition( currentLine, currentColumn );
300 }
301
303 }
304 }
305 }
306 }
307 }
308 return QgsPanelWidget::eventFilter( obj, event );
309}
310
312
314{
315 return !mFindWidget->isHidden();
316}
317
319{
320 return mMessageBar;
321}
322
327
328void QgsCodeEditorWidget::addWarning( int lineNumber, const QString &warning )
329{
330 mEditor->addWarning( lineNumber, warning );
331
332 mHighlightController->addHighlight(
334 HighlightCategory::Warning,
335 lineNumber,
336 QColor( 255, 0, 0 ),
338 )
339 );
340}
341
343{
344 mEditor->clearWarnings();
345
346 mHighlightController->removeHighlights(
347 HighlightCategory::Warning
348 );
349}
350
352{
353 addSearchHighlights();
354 mFindWidget->show();
355
356 if ( mEditor->isReadOnly() )
357 {
358 setReplaceBarVisible( false );
359 }
360
361 emit searchBarToggled( true );
362}
363
365{
366 clearSearchHighlights();
367 mFindWidget->hide();
368 emit searchBarToggled( false );
369}
370
372{
373 if ( visible )
375 else
377}
378
380{
381 if ( visible )
382 {
383 mReplaceAllButton->show();
384 mReplaceButton->show();
385 mLineEditReplace->show();
386 }
387 else
388 {
389 mReplaceAllButton->hide();
390 mReplaceButton->hide();
391 mLineEditReplace->hide();
392 }
393 if ( mShowReplaceBarButton )
394 mShowReplaceBarButton->setChecked( visible );
395}
396
398{
399 clearSearchHighlights();
400 mLineEditFind->setFocus();
401 if ( mEditor->hasSelectedText() )
402 {
403 mBlockSearching++;
404 mLineEditFind->setText( mEditor->selectedText().trimmed() );
405 mBlockSearching--;
406 }
407 mLineEditFind->selectAll();
409}
410
411bool QgsCodeEditorWidget::loadFile( const QString &path )
412{
413 if ( !QFile::exists( path ) )
414 return false;
415
416 QFile file( path );
417 if ( file.open( QFile::ReadOnly ) )
418 {
419 const QString content = file.readAll();
420 mEditor->setText( content );
421 setFilePath( path );
422 mEditor->recolor();
423 mEditor->setModified( false );
424 mLastModified = QFileInfo( path ).lastModified();
425 return true;
426 }
427 return false;
428}
429
430void QgsCodeEditorWidget::setFilePath( const QString &path )
431{
432 if ( mFilePath == path )
433 return;
434
435 mFilePath = path;
436 mLastModified = QDateTime();
437
438 emit filePathChanged( mFilePath );
439}
440
441bool QgsCodeEditorWidget::save( const QString &path )
442{
443 const QString filePath = !path.isEmpty() ? path : mFilePath;
444 if ( !filePath.isEmpty() )
445 {
446 QFile file( filePath );
447 if ( file.open( QFile::WriteOnly ) )
448 {
449 file.write( mEditor->text().toUtf8() );
450 file.close();
451
453 mEditor->setModified( false );
454 mLastModified = QFileInfo( filePath ).lastModified();
455
456 return true;
457 }
458 }
459 return false;
460}
461
463{
464 if ( mFilePath.isEmpty() )
465 return false;
466
467 const QDir dir = QFileInfo( mFilePath ).dir();
468
469 bool useFallback = true;
470
471 QString externalEditorCommand;
472 switch ( mEditor->language() )
473 {
475 externalEditorCommand = QgsCodeEditorPython::settingExternalPythonEditorCommand->value();
476 break;
477
488 break;
489 }
490
491 int currentLine, currentColumn;
492 mEditor->getCursorPosition( &currentLine, &currentColumn );
493 if ( line < 0 )
494 line = currentLine;
495 if ( column < 0 )
496 column = currentColumn;
497
498 if ( !externalEditorCommand.isEmpty() )
499 {
500 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<file>" ), mFilePath );
501 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<line>" ), QString::number( line + 1 ) );
502 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<col>" ), QString::number( column + 1 ) );
503
504 const QStringList commandParts = QProcess::splitCommand( externalEditorCommand );
505 if ( QProcess::startDetached( commandParts.at( 0 ), commandParts.mid( 1 ), dir.absolutePath() ) )
506 {
507 return true;
508 }
509 }
510
511 const QString editorCommand = qgetenv( "EDITOR" );
512 if ( !editorCommand.isEmpty() )
513 {
514 const QFileInfo fi( editorCommand );
515 if ( fi.exists( ) )
516 {
517 const QString command = fi.fileName();
518 const bool isTerminalEditor = command.compare( QLatin1String( "nano" ), Qt::CaseInsensitive ) == 0
519 || command.contains( QLatin1String( "vim" ), Qt::CaseInsensitive );
520
521 if ( !isTerminalEditor && QProcess::startDetached( editorCommand, {mFilePath}, dir.absolutePath() ) )
522 {
523 useFallback = false;
524 }
525 }
526 }
527
528 if ( useFallback )
529 {
530 QDesktopServices::openUrl( QUrl::fromLocalFile( mFilePath ) );
531 }
532 return true;
533}
534
536{
537 const QString accessToken = QgsSettings().value( "pythonConsole/accessTokenGithub", QString() ).toString();
538 if ( accessToken.isEmpty() )
539 {
540 if ( mMessageBar )
541 mMessageBar->pushWarning( QString(), tr( "GitHub personal access token must be generated (see IDE Options)" ) );
542 return false;
543 }
544
545 QString defaultFileName;
546 switch ( mEditor->language() )
547 {
549 defaultFileName = QStringLiteral( "pyqgis_snippet.py" );
550 break;
551
553 defaultFileName = QStringLiteral( "qgis_snippet.css" );
554 break;
555
557 defaultFileName = QStringLiteral( "qgis_snippet" );
558 break;
559
561 defaultFileName = QStringLiteral( "qgis_snippet.html" );
562 break;
563
565 defaultFileName = QStringLiteral( "qgis_snippet.js" );
566 break;
567
569 defaultFileName = QStringLiteral( "qgis_snippet.json" );
570 break;
571
573 defaultFileName = QStringLiteral( "qgis_snippet.r" );
574 break;
575
577 defaultFileName = QStringLiteral( "qgis_snippet.sql" );
578 break;
579
581 defaultFileName = QStringLiteral( "qgis_snippet.bat" );
582 break;
583
585 defaultFileName = QStringLiteral( "qgis_snippet.sh" );
586 break;
587
589 defaultFileName = QStringLiteral( "qgis_snippet.txt" );
590 break;
591 }
592 const QString filename = mFilePath.isEmpty() ? defaultFileName : QFileInfo( mFilePath ).fileName();
593
594 const QString contents = mEditor->hasSelectedText() ? mEditor->selectedText() : mEditor->text();
595 const QVariantMap data
596 {
597 { QStringLiteral( "description" ), "Gist created by PyQGIS Console"},
598 { QStringLiteral( "public" ), isPublic },
599 { QStringLiteral( "files" ), QVariantMap{ {filename, QVariantMap{{ QStringLiteral( "content" ), contents }} } } }
600 };
601
602 QNetworkRequest request;
603 request.setUrl( QUrl( QStringLiteral( "https://api.github.com/gists" ) ) );
604 request.setRawHeader( "Authorization", QStringLiteral( "token %1" ).arg( accessToken ).toLocal8Bit() );
605 request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/json" ) );
606 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy );
607 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsCodeEditorWidget" ) );
608
609 QNetworkReply *reply = QgsNetworkAccessManager::instance()->post( request, QgsJsonUtils::jsonFromVariant( data ).dump().c_str() );
610 connect( reply, &QNetworkReply::finished, this, [this, reply]
611 {
612 if ( reply->error() == QNetworkReply::NoError )
613 {
614 const QVariantMap replyJson = QgsJsonUtils::parseJson( reply->readAll() ).toMap();
615 const QString link = replyJson.value( QStringLiteral( "html_url" ) ).toString();
616 QDesktopServices::openUrl( QUrl( link ) );
617 }
618 else
619 {
620 if ( mMessageBar )
621 mMessageBar->pushCritical( QString(), tr( "Connection error: %1" ).arg( reply->errorString() ) );
622 }
623 reply->deleteLater();
624 } );
625 return true;
626}
627
628bool QgsCodeEditorWidget::findNext()
629{
630 return findText( true, false );
631}
632
633void QgsCodeEditorWidget::findPrevious()
634{
635 findText( false, false );
636}
637
638void QgsCodeEditorWidget::textSearchChanged( const QString &text )
639{
640 if ( !text.isEmpty() )
641 {
642 updateSearch();
643 }
644 else
645 {
646 clearSearchHighlights();
647 mLineEditFind->setStyleSheet( QString() );
648 }
649}
650
651void QgsCodeEditorWidget::updateSearch()
652{
653 if ( mBlockSearching )
654 return;
655
656 clearSearchHighlights();
657 addSearchHighlights();
658
659 findText( true, true );
660}
661
662void QgsCodeEditorWidget::replace()
663{
664 if ( mEditor->isReadOnly() )
665 return;
666
667 replaceSelection();
668
669 clearSearchHighlights();
670 addSearchHighlights();
671 findNext();
672}
673
674void QgsCodeEditorWidget::replaceSelection()
675{
676 const long selectionStart = mEditor->SendScintilla( QsciScintilla::SCI_GETSELECTIONSTART );
677 const long selectionEnd = mEditor->SendScintilla( QsciScintilla::SCI_GETSELECTIONEND );
678 if ( selectionEnd - selectionStart <= 0 )
679 return;
680
681 const QString replacement = mLineEditReplace->text();
682
683 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, selectionStart, selectionEnd );
684
685 if ( mRegexButton->isChecked() )
686 mEditor->SendScintilla( QsciScintilla::SCI_REPLACETARGETRE, replacement.size(), replacement.toLocal8Bit().constData() );
687 else
688 mEditor->SendScintilla( QsciScintilla::SCI_REPLACETARGET, replacement.size(), replacement.toLocal8Bit().constData() );
689
690 // set the cursor to the end of the replaced text
691 const long postReplacementEnd = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND );
692 mEditor->SendScintilla( QsciScintilla::SCI_SETCURRENTPOS, postReplacementEnd );
693}
694
695void QgsCodeEditorWidget::replaceAll()
696{
697 if ( mEditor->isReadOnly() )
698 return;
699
700 if ( !findText( true, true ) )
701 {
702 return;
703 }
704
705 mEditor->SendScintilla( QsciScintilla::SCI_BEGINUNDOACTION );
706 replaceSelection();
707
708 while ( findText( true, false ) )
709 {
710 replaceSelection();
711 }
712
713 mEditor->SendScintilla( QsciScintilla::SCI_ENDUNDOACTION );
714 clearSearchHighlights();
715}
716
717void QgsCodeEditorWidget::addSearchHighlights()
718{
719 const QString searchString = mLineEditFind->text();
720 if ( searchString.isEmpty() )
721 return;
722
723 const long originalStartPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETSTART );
724 const long originalEndPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND );
725 long startPos = 0;
726 long docEnd = mEditor->length();
727
728 updateHighlightController();
729
730 int searchFlags = 0;
731 const bool isRegEx = mRegexButton->isChecked();
732 const bool isCaseSensitive = mCaseSensitiveButton->isChecked();
733 const bool isWholeWordOnly = mWholeWordButton->isChecked();
734 if ( isRegEx )
735 searchFlags |= QsciScintilla::SCFIND_REGEXP | QsciScintilla::SCFIND_CXX11REGEX;
736 if ( isCaseSensitive )
737 searchFlags |= QsciScintilla::SCFIND_MATCHCASE;
738 if ( isWholeWordOnly )
739 searchFlags |= QsciScintilla::SCFIND_WHOLEWORD;
740 mEditor->SendScintilla( QsciScintilla::SCI_SETSEARCHFLAGS, searchFlags );
741 int matchCount = 0;
742 while ( true )
743 {
744 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, startPos, docEnd );
745 const int fstart = mEditor->SendScintilla( QsciScintilla::SCI_SEARCHINTARGET, searchString.length(), searchString.toLocal8Bit().constData() );
746 if ( fstart < 0 )
747 break;
748
749 const int matchLength = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETTEXT, 0, static_cast< void * >( nullptr ) );
750
751 if ( matchLength == 0 )
752 {
753 startPos += 1;
754 continue;
755 }
756
757 matchCount++;
758 startPos = fstart + matchLength;
759
760 mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR );
761 mEditor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, fstart, matchLength );
762
763 int thisLine = 0;
764 int thisIndex = 0;
765 mEditor->lineIndexFromPosition( fstart, &thisLine, &thisIndex );
766 mHighlightController->addHighlight( QgsScrollBarHighlight( SearchMatch, thisLine, QColor( 0, 200, 0 ), QgsScrollBarHighlight::Priority::HighPriority ) );
767 }
768
769 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, originalStartPos, originalEndPos );
770
771 searchMatchCountChanged( matchCount );
772}
773
774void QgsCodeEditorWidget::clearSearchHighlights()
775{
776 long docStart = 0;
777 long docEnd = mEditor->length();
778 mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR );
779 mEditor->SendScintilla( QsciScintilla::SCI_INDICATORCLEARRANGE, docStart, docEnd - docStart );
780
781 mHighlightController->removeHighlights( SearchMatch );
782
783 searchMatchCountChanged( 0 );
784}
785
786bool QgsCodeEditorWidget::findText( bool forward, bool findFirst )
787{
788 const QString searchString = mLineEditFind->text();
789 if ( searchString.isEmpty() )
790 return false;
791
792 int lineFrom = 0;
793 int indexFrom = 0;
794 int lineTo = 0;
795 int indexTo = 0;
796 mEditor->getSelection( &lineFrom, &indexFrom, &lineTo, &indexTo );
797
798 int line = 0;
799 int index = 0;
800 if ( !findFirst )
801 {
802 mEditor->getCursorPosition( &line, &index );
803 }
804 if ( !forward )
805 {
806 line = lineFrom;
807 index = indexFrom;
808 }
809
810 const bool isRegEx = mRegexButton->isChecked();
811 const bool wrapAround = mWrapAroundButton->isChecked();
812 const bool isCaseSensitive = mCaseSensitiveButton->isChecked();
813 const bool isWholeWordOnly = mWholeWordButton->isChecked();
814
815 const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward,
816 line, index, true, true, isRegEx );
817
818 if ( !found )
819 {
820 const QString styleError = QStringLiteral( "QLineEdit {background-color: #d65253; color: #ffffff;}" );
821 mLineEditFind->setStyleSheet( styleError );
822 }
823 else
824 {
825 mLineEditFind->setStyleSheet( QString() );
826 }
827 return found;
828}
829
830void QgsCodeEditorWidget::searchMatchCountChanged( int matchCount )
831{
832 mReplaceButton->setEnabled( matchCount > 0 );
833 mReplaceAllButton->setEnabled( matchCount > 0 );
834 mFindNextButton->setEnabled( matchCount > 0 );
835 mFindPrevButton->setEnabled( matchCount > 0 );
836}
837
838void QgsCodeEditorWidget::updateHighlightController()
839{
840 mHighlightController->setLineHeight( QFontMetrics( mEditor->font() ).lineSpacing() );
841 mHighlightController->setVisibleRange( mEditor->viewport()->rect().height() );
842}
843
@ QgisExpression
QGIS expressions.
@ Batch
Windows batch files.
@ JavaScript
JavaScript.
@ Bash
Bash scripts.
@ Unknown
Unknown/other language.
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition qgis.h:5667
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
void setFilePath(const QString &path)
Sets the widget's associated file path.
QgsScrollBarHighlightController * scrollbarHighlightController()
Returns the scrollbar highlight controller, which can be used to add highlights in the code editor sc...
bool isSearchBarVisible() const
Returns true if the search bar is visible.
void showEvent(QShowEvent *event) override
void addWarning(int lineNumber, const QString &warning)
Adds a warning message and indicator to the specified a lineNumber.
void setReplaceBarVisible(bool visible)
Sets whether the replace bar is visible.
void loadedExternalChanges()
Emitted when the widget loads in text from the associated file to bring in changes made externally to...
QgsMessageBar * messageBar()
Returns the message bar associated with the widget, to use for user feedback.
void triggerFind()
Triggers a find operation, using the default behavior.
bool openInExternalEditor(int line=-1, int column=-1)
Attempts to opens the script from the editor in an external text editor.
void hideSearchBar()
Hides the search bar.
void showSearchBar()
Shows the search bar.
void searchBarToggled(bool visible)
Emitted when the visibility of the search bar is changed.
bool save(const QString &path=QString())
Saves the code editor content into the file path.
void setSearchBarVisible(bool visible)
Sets whether the search bar is visible.
void filePathChanged(const QString &path)
Emitted when the widget's associated file path is changed.
bool shareOnGist(bool isPublic)
Shares the contents of the code editor on GitHub Gist.
QgsCodeEditor * editor()
Returns the wrapped code editor.
void clearWarnings()
Clears all warning messages from the editor.
QString filePath() const
Returns the widget's associated file path.
QgsCodeEditorWidget(QgsCodeEditor *editor, QgsMessageBar *messageBar=nullptr, QWidget *parent=nullptr)
Constructor for QgsCodeEditorWidget, wrapping the specified editor widget.
bool eventFilter(QObject *obj, QEvent *event) override
bool loadFile(const QString &path)
Loads the file at the specified path into the widget, replacing the code editor's content with that f...
void resizeEvent(QResizeEvent *event) override
~QgsCodeEditorWidget() override
A text editor based on QScintilla2.
static constexpr int SEARCH_RESULT_INDICATOR
Indicator index for search results.
void clearWarnings()
Clears all warning messages from the editor.
virtual Qgis::ScriptLanguage language() const
Returns the associated scripting language.
void addWarning(int lineNumber, const QString &warning)
Adds a warning message and indicator to the specified a lineNumber.
QLineEdit subclass with built in support for clearing the widget's value and handling custom null val...
void setShowSearchIcon(bool visible)
Define if a search icon shall be shown on the left of the image when no text is entered.
static json jsonFromVariant(const QVariant &v)
Converts a QVariant v to a json object.
A bar for displaying non-blocking messages to the user.
void pushCritical(const QString &title, const QString &message)
Pushes a critical warning message that must be manually dismissed by the user.
void pushWarning(const QString &title, const QString &message)
Pushes a warning message that must be manually dismissed by the user.
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
Base class for any widget that can be shown as a inline panel.
Adds highlights (colored markers) to a scrollbar.
Encapsulates the details of a highlight in a scrollbar, used alongside QgsScrollBarHighlightControlle...
@ HighestPriority
Highest priority, rendered above all other highlights.
This class is a composition of two QSettings instances:
Definition qgssettings.h:64
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...
#define QgsSetRequestInitiatorClass(request, _class)