QGIS API Documentation 3.41.0-Master (cea29feecf2)
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)
47 : QgsPanelWidget( parent )
48 , mEditor( editor )
49 , mMessageBar( messageBar )
50{
51 Q_ASSERT( mEditor );
52
53 mEditor->installEventFilter( this );
54 installEventFilter( this );
55
56 QVBoxLayout *vl = new QVBoxLayout();
57 vl->setContentsMargins( 0, 0, 0, 0 );
58 vl->setSpacing( 0 );
59 vl->addWidget( editor, 1 );
60
61 if ( !mMessageBar )
62 {
63 QGridLayout *layout = new QGridLayout( mEditor );
64 layout->setContentsMargins( 0, 0, 0, 0 );
65 layout->addItem( new QSpacerItem( 20, 40, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding ), 1, 0, 1, 1 );
66
67 mMessageBar = new QgsMessageBar();
68 QSizePolicy sizePolicy = QSizePolicy( QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Fixed );
69 mMessageBar->setSizePolicy( sizePolicy );
70 layout->addWidget( mMessageBar, 0, 0, 1, 1 );
71 }
72
73 mFindWidget = new QWidget();
74 QGridLayout *layoutFind = new QGridLayout();
75 layoutFind->setContentsMargins( 0, 2, 0, 0 );
76 layoutFind->setSpacing( 1 );
77
78 if ( !mEditor->isReadOnly() )
79 {
80 mShowReplaceBarButton = new QToolButton();
81 mShowReplaceBarButton->setToolTip( tr( "Replace" ) );
82 mShowReplaceBarButton->setCheckable( true );
83 mShowReplaceBarButton->setAutoRaise( true );
84 mShowReplaceBarButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionReplace.svg" ) ) );
85 layoutFind->addWidget( mShowReplaceBarButton, 0, 0 );
86
87 connect( mShowReplaceBarButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::setReplaceBarVisible );
88 }
89
90 mLineEditFind = new QgsFilterLineEdit();
91 mLineEditFind->setShowSearchIcon( true );
92 mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) );
93 layoutFind->addWidget( mLineEditFind, 0, mShowReplaceBarButton ? 1 : 0 );
94
95 mLineEditReplace = new QgsFilterLineEdit();
96 mLineEditReplace->setShowSearchIcon( true );
97 mLineEditReplace->setPlaceholderText( tr( "Replace…" ) );
98 layoutFind->addWidget( mLineEditReplace, 1, mShowReplaceBarButton ? 1 : 0 );
99
100 QHBoxLayout *findButtonLayout = new QHBoxLayout();
101 findButtonLayout->setContentsMargins( 0, 0, 0, 0 );
102 findButtonLayout->setSpacing( 1 );
103 mCaseSensitiveButton = new QToolButton();
104 mCaseSensitiveButton->setToolTip( tr( "Case Sensitive" ) );
105 mCaseSensitiveButton->setCheckable( true );
106 mCaseSensitiveButton->setAutoRaise( true );
107 mCaseSensitiveButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchCaseSensitive.svg" ) ) );
108 findButtonLayout->addWidget( mCaseSensitiveButton );
109
110 mWholeWordButton = new QToolButton();
111 mWholeWordButton->setToolTip( tr( "Whole Word" ) );
112 mWholeWordButton->setCheckable( true );
113 mWholeWordButton->setAutoRaise( true );
114 mWholeWordButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWholeWord.svg" ) ) );
115 findButtonLayout->addWidget( mWholeWordButton );
116
117 mRegexButton = new QToolButton();
118 mRegexButton->setToolTip( tr( "Use Regular Expressions" ) );
119 mRegexButton->setCheckable( true );
120 mRegexButton->setAutoRaise( true );
121 mRegexButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchRegex.svg" ) ) );
122 findButtonLayout->addWidget( mRegexButton );
123
124 mWrapAroundButton = new QToolButton();
125 mWrapAroundButton->setToolTip( tr( "Wrap Around" ) );
126 mWrapAroundButton->setCheckable( true );
127 mWrapAroundButton->setAutoRaise( true );
128 mWrapAroundButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWrapAround.svg" ) ) );
129 findButtonLayout->addWidget( mWrapAroundButton );
130
131 mFindPrevButton = new QToolButton();
132 mFindPrevButton->setEnabled( false );
133 mFindPrevButton->setToolTip( tr( "Find Previous" ) );
134 mFindPrevButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchPrevEditorConsole.svg" ) ) );
135 mFindPrevButton->setAutoRaise( true );
136 findButtonLayout->addWidget( mFindPrevButton );
137
138 mFindNextButton = new QToolButton();
139 mFindNextButton->setEnabled( false );
140 mFindNextButton->setToolTip( tr( "Find Next" ) );
141 mFindNextButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchNextEditorConsole.svg" ) ) );
142 mFindNextButton->setAutoRaise( true );
143 findButtonLayout->addWidget( mFindNextButton );
144
145 connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext );
146 connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textSearchChanged );
147 connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext );
148 connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious );
149 connect( mCaseSensitiveButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
150 connect( mWholeWordButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
151 connect( mRegexButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
152 connect( mWrapAroundButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch );
153
154 QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor );
155 findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
156 connect( findShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::triggerFind );
157
158 QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this );
159 findNextShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
160 connect( findNextShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findNext );
161
162 QShortcut *findPreviousShortcut = new QShortcut( QKeySequence::StandardKey::FindPrevious, this );
163 findPreviousShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
164 connect( findPreviousShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findPrevious );
165
166 if ( !mEditor->isReadOnly() )
167 {
168 QShortcut *replaceShortcut = new QShortcut( QKeySequence::StandardKey::Replace, this );
169 replaceShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
170 connect( replaceShortcut, &QShortcut::activated, this, [=] {
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] {
186 mEditor->setFocus();
187 } );
188
189 layoutFind->addLayout( findButtonLayout, 0, mShowReplaceBarButton ? 2 : 1 );
190
191 QHBoxLayout *replaceButtonLayout = new QHBoxLayout();
192 replaceButtonLayout->setContentsMargins( 0, 0, 0, 0 );
193 replaceButtonLayout->setSpacing( 1 );
194
195 mReplaceButton = new QToolButton();
196 mReplaceButton->setText( tr( "Replace" ) );
197 mReplaceButton->setEnabled( false );
198 connect( mReplaceButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::replace );
199 replaceButtonLayout->addWidget( mReplaceButton );
200
201 mReplaceAllButton = new QToolButton();
202 mReplaceAllButton->setText( tr( "Replace All" ) );
203 mReplaceAllButton->setEnabled( false );
204 connect( mReplaceAllButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::replaceAll );
205 replaceButtonLayout->addWidget( mReplaceAllButton );
206
207 layoutFind->addLayout( replaceButtonLayout, 1, mShowReplaceBarButton ? 2 : 1 );
208
209 QToolButton *closeFindButton = new QToolButton( this );
210 closeFindButton->setToolTip( tr( "Close" ) );
211 closeFindButton->setMinimumWidth( QgsGuiUtils::scaleIconSize( 44 ) );
212 closeFindButton->setStyleSheet(
213 "QToolButton { border:none; background-color: rgba(0, 0, 0, 0); }"
214 "QToolButton::menu-button { border:none; background-color: rgba(0, 0, 0, 0); }"
215 );
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] {
224 mEditor->setFocus();
225 } );
226 layoutFind->addWidget( closeFindButton, 0, mShowReplaceBarButton ? 3 : 2 );
227
228 layoutFind->setColumnStretch( mShowReplaceBarButton ? 1 : 0, 1 );
229
230 mFindWidget->setLayout( layoutFind );
231 vl->addWidget( mFindWidget );
232 mFindWidget->hide();
233
234 setReplaceBarVisible( false );
235
236 setLayout( vl );
237
238 mHighlightController = std::make_unique<QgsScrollBarHighlightController>();
239 mHighlightController->setScrollArea( mEditor );
240}
241
242void QgsCodeEditorWidget::resizeEvent( QResizeEvent *event )
243{
244 QgsPanelWidget::resizeEvent( event );
245 updateHighlightController();
246}
247
248void QgsCodeEditorWidget::showEvent( QShowEvent *event )
249{
250 QgsPanelWidget::showEvent( event );
251 updateHighlightController();
252}
253
254bool QgsCodeEditorWidget::eventFilter( QObject *obj, QEvent *event )
255{
256 if ( event->type() == QEvent::FocusIn )
257 {
258 if ( !mFilePath.isEmpty() )
259 {
260 if ( !QFile::exists( mFilePath ) )
261 {
262 // file deleted externally
263 if ( mMessageBar )
264 {
265 mMessageBar->pushCritical( QString(), tr( "The file <b>\"%1\"</b> has been deleted or is not accessible" ).arg( QDir::toNativeSeparators( mFilePath ) ) );
266 }
267 }
268 else
269 {
270 const QFileInfo fi( mFilePath );
271 if ( mLastModified != fi.lastModified() )
272 {
273 // TODO - we should give users a choice of how to react to this, eg "ignore changes"
274 // note -- we intentionally don't call loadFile here -- we want this action to be undo-able
275 QFile file( mFilePath );
276 if ( file.open( QFile::ReadOnly ) )
277 {
278 int currentLine = -1, currentColumn = -1;
279 if ( !mLastModified.isNull() )
280 {
281 mEditor->getCursorPosition( &currentLine, &currentColumn );
282 }
283
284 const QString content = file.readAll();
285
286 // don't clear, instead perform undoable actions:
287 mEditor->beginUndoAction();
288 mEditor->selectAll();
289 mEditor->removeSelectedText();
290 mEditor->insert( content );
291 mEditor->setModified( false );
292 mEditor->recolor();
293 mEditor->endUndoAction();
294
295 mLastModified = fi.lastModified();
296 if ( currentLine >= 0 && currentLine < mEditor->lines() )
297 {
298 mEditor->setCursorPosition( currentLine, currentColumn );
299 }
300
302 }
303 }
304 }
305 }
306 }
307 return QgsPanelWidget::eventFilter( obj, event );
308}
309
311
313{
314 return !mFindWidget->isHidden();
315}
316
318{
319 return mMessageBar;
320}
321
326
327void QgsCodeEditorWidget::addWarning( int lineNumber, const QString &warning )
328{
329 mEditor->addWarning( lineNumber, warning );
330
331 mHighlightController->addHighlight(
333 HighlightCategory::Warning,
334 lineNumber,
335 QColor( 255, 0, 0 ),
337 )
338 );
339}
340
342{
343 mEditor->clearWarnings();
344
345 mHighlightController->removeHighlights(
346 HighlightCategory::Warning
347 );
348}
349
351{
352 addSearchHighlights();
353 mFindWidget->show();
354
355 if ( mEditor->isReadOnly() )
356 {
357 setReplaceBarVisible( false );
358 }
359
360 emit searchBarToggled( true );
361}
362
364{
365 clearSearchHighlights();
366 mFindWidget->hide();
367 emit searchBarToggled( false );
368}
369
371{
372 if ( visible )
374 else
376}
377
379{
380 if ( visible )
381 {
382 mReplaceAllButton->show();
383 mReplaceButton->show();
384 mLineEditReplace->show();
385 }
386 else
387 {
388 mReplaceAllButton->hide();
389 mReplaceButton->hide();
390 mLineEditReplace->hide();
391 }
392 if ( mShowReplaceBarButton )
393 mShowReplaceBarButton->setChecked( visible );
394}
395
397{
398 clearSearchHighlights();
399 mLineEditFind->setFocus();
400 if ( mEditor->hasSelectedText() )
401 {
402 mBlockSearching++;
403 mLineEditFind->setText( mEditor->selectedText().trimmed() );
404 mBlockSearching--;
405 }
406 mLineEditFind->selectAll();
408}
409
410bool QgsCodeEditorWidget::loadFile( const QString &path )
411{
412 if ( !QFile::exists( path ) )
413 return false;
414
415 QFile file( path );
416 if ( file.open( QFile::ReadOnly ) )
417 {
418 const QString content = file.readAll();
419 mEditor->setText( content );
420 setFilePath( path );
421 mEditor->recolor();
422 mEditor->setModified( false );
423 mLastModified = QFileInfo( path ).lastModified();
424 return true;
425 }
426 return false;
427}
428
429void QgsCodeEditorWidget::setFilePath( const QString &path )
430{
431 if ( mFilePath == path )
432 return;
433
434 mFilePath = path;
435 mLastModified = QDateTime();
436
437 emit filePathChanged( mFilePath );
438}
439
440bool QgsCodeEditorWidget::save( const QString &path )
441{
442 const QString filePath = !path.isEmpty() ? path : mFilePath;
443 if ( !filePath.isEmpty() )
444 {
445 QFile file( filePath );
446 if ( file.open( QFile::WriteOnly ) )
447 {
448 file.write( mEditor->text().toUtf8() );
449 file.close();
450
452 mEditor->setModified( false );
453 mLastModified = QFileInfo( filePath ).lastModified();
454
455 return true;
456 }
457 }
458 return false;
459}
460
462{
463 if ( mFilePath.isEmpty() )
464 return false;
465
466 const QDir dir = QFileInfo( mFilePath ).dir();
467
468 bool useFallback = true;
469
470 QString externalEditorCommand;
471 switch ( mEditor->language() )
472 {
474 externalEditorCommand = QgsCodeEditorPython::settingExternalPythonEditorCommand->value();
475 break;
476
487 break;
488 }
489
490 int currentLine, currentColumn;
491 mEditor->getCursorPosition( &currentLine, &currentColumn );
492 if ( line < 0 )
493 line = currentLine;
494 if ( column < 0 )
495 column = currentColumn;
496
497 if ( !externalEditorCommand.isEmpty() )
498 {
499 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<file>" ), mFilePath );
500 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<line>" ), QString::number( line + 1 ) );
501 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<col>" ), QString::number( column + 1 ) );
502
503 const QStringList commandParts = QProcess::splitCommand( externalEditorCommand );
504 if ( QProcess::startDetached( commandParts.at( 0 ), commandParts.mid( 1 ), dir.absolutePath() ) )
505 {
506 return true;
507 }
508 }
509
510 const QString editorCommand = qgetenv( "EDITOR" );
511 if ( !editorCommand.isEmpty() )
512 {
513 const QFileInfo fi( editorCommand );
514 if ( fi.exists() )
515 {
516 const QString command = fi.fileName();
517 const bool isTerminalEditor = command.compare( QLatin1String( "nano" ), Qt::CaseInsensitive ) == 0
518 || command.contains( QLatin1String( "vim" ), Qt::CaseInsensitive );
519
520 if ( !isTerminalEditor && QProcess::startDetached( editorCommand, { mFilePath }, dir.absolutePath() ) )
521 {
522 useFallback = false;
523 }
524 }
525 }
526
527 if ( useFallback )
528 {
529 QDesktopServices::openUrl( QUrl::fromLocalFile( mFilePath ) );
530 }
531 return true;
532}
533
535{
536 const QString accessToken = QgsSettings().value( "pythonConsole/accessTokenGithub", QString() ).toString();
537 if ( accessToken.isEmpty() )
538 {
539 if ( mMessageBar )
540 mMessageBar->pushWarning( QString(), tr( "GitHub personal access token must be generated (see IDE Options)" ) );
541 return false;
542 }
543
544 QString defaultFileName;
545 switch ( mEditor->language() )
546 {
548 defaultFileName = QStringLiteral( "pyqgis_snippet.py" );
549 break;
550
552 defaultFileName = QStringLiteral( "qgis_snippet.css" );
553 break;
554
556 defaultFileName = QStringLiteral( "qgis_snippet" );
557 break;
558
560 defaultFileName = QStringLiteral( "qgis_snippet.html" );
561 break;
562
564 defaultFileName = QStringLiteral( "qgis_snippet.js" );
565 break;
566
568 defaultFileName = QStringLiteral( "qgis_snippet.json" );
569 break;
570
572 defaultFileName = QStringLiteral( "qgis_snippet.r" );
573 break;
574
576 defaultFileName = QStringLiteral( "qgis_snippet.sql" );
577 break;
578
580 defaultFileName = QStringLiteral( "qgis_snippet.bat" );
581 break;
582
584 defaultFileName = QStringLiteral( "qgis_snippet.sh" );
585 break;
586
588 defaultFileName = QStringLiteral( "qgis_snippet.txt" );
589 break;
590 }
591 const QString filename = mFilePath.isEmpty() ? defaultFileName : QFileInfo( mFilePath ).fileName();
592
593 const QString contents = mEditor->hasSelectedText() ? mEditor->selectedText() : mEditor->text();
594 const QVariantMap data {
595 { QStringLiteral( "description" ), "Gist created by PyQGIS Console" },
596 { QStringLiteral( "public" ), isPublic },
597 { QStringLiteral( "files" ), QVariantMap { { filename, QVariantMap { { QStringLiteral( "content" ), contents } } } } }
598 };
599
600 QNetworkRequest request;
601 request.setUrl( QUrl( QStringLiteral( "https://api.github.com/gists" ) ) );
602 request.setRawHeader( "Authorization", QStringLiteral( "token %1" ).arg( accessToken ).toLocal8Bit() );
603 request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/json" ) );
604 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy );
605 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsCodeEditorWidget" ) );
606
607 QNetworkReply *reply = QgsNetworkAccessManager::instance()->post( request, QgsJsonUtils::jsonFromVariant( data ).dump().c_str() );
608 connect( reply, &QNetworkReply::finished, this, [this, reply] {
609 if ( reply->error() == QNetworkReply::NoError )
610 {
611 const QVariantMap replyJson = QgsJsonUtils::parseJson( reply->readAll() ).toMap();
612 const QString link = replyJson.value( QStringLiteral( "html_url" ) ).toString();
613 QDesktopServices::openUrl( QUrl( link ) );
614 }
615 else
616 {
617 if ( mMessageBar )
618 mMessageBar->pushCritical( QString(), tr( "Connection error: %1" ).arg( reply->errorString() ) );
619 }
620 reply->deleteLater();
621 } );
622 return true;
623}
624
625bool QgsCodeEditorWidget::findNext()
626{
627 return findText( true, false );
628}
629
630void QgsCodeEditorWidget::findPrevious()
631{
632 findText( false, false );
633}
634
635void QgsCodeEditorWidget::textSearchChanged( const QString &text )
636{
637 if ( !text.isEmpty() )
638 {
639 updateSearch();
640 }
641 else
642 {
643 clearSearchHighlights();
644 mLineEditFind->setStyleSheet( QString() );
645 }
646}
647
648void QgsCodeEditorWidget::updateSearch()
649{
650 if ( mBlockSearching )
651 return;
652
653 clearSearchHighlights();
654 addSearchHighlights();
655
656 findText( true, true );
657}
658
659void QgsCodeEditorWidget::replace()
660{
661 if ( mEditor->isReadOnly() )
662 return;
663
664 replaceSelection();
665
666 clearSearchHighlights();
667 addSearchHighlights();
668 findNext();
669}
670
671void QgsCodeEditorWidget::replaceSelection()
672{
673 const long selectionStart = mEditor->SendScintilla( QsciScintilla::SCI_GETSELECTIONSTART );
674 const long selectionEnd = mEditor->SendScintilla( QsciScintilla::SCI_GETSELECTIONEND );
675 if ( selectionEnd - selectionStart <= 0 )
676 return;
677
678 const QString replacement = mLineEditReplace->text();
679
680 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, selectionStart, selectionEnd );
681
682 if ( mRegexButton->isChecked() )
683 mEditor->SendScintilla( QsciScintilla::SCI_REPLACETARGETRE, replacement.size(), replacement.toLocal8Bit().constData() );
684 else
685 mEditor->SendScintilla( QsciScintilla::SCI_REPLACETARGET, replacement.size(), replacement.toLocal8Bit().constData() );
686
687 // set the cursor to the end of the replaced text
688 const long postReplacementEnd = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND );
689 mEditor->SendScintilla( QsciScintilla::SCI_SETCURRENTPOS, postReplacementEnd );
690}
691
692void QgsCodeEditorWidget::replaceAll()
693{
694 if ( mEditor->isReadOnly() )
695 return;
696
697 if ( !findText( true, true ) )
698 {
699 return;
700 }
701
702 mEditor->SendScintilla( QsciScintilla::SCI_BEGINUNDOACTION );
703 replaceSelection();
704
705 while ( findText( true, false ) )
706 {
707 replaceSelection();
708 }
709
710 mEditor->SendScintilla( QsciScintilla::SCI_ENDUNDOACTION );
711 clearSearchHighlights();
712}
713
714void QgsCodeEditorWidget::addSearchHighlights()
715{
716 const QString searchString = mLineEditFind->text();
717 if ( searchString.isEmpty() )
718 return;
719
720 const long originalStartPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETSTART );
721 const long originalEndPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND );
722 long startPos = 0;
723 long docEnd = mEditor->length();
724
725 updateHighlightController();
726
727 int searchFlags = 0;
728 const bool isRegEx = mRegexButton->isChecked();
729 const bool isCaseSensitive = mCaseSensitiveButton->isChecked();
730 const bool isWholeWordOnly = mWholeWordButton->isChecked();
731 if ( isRegEx )
732 searchFlags |= QsciScintilla::SCFIND_REGEXP | QsciScintilla::SCFIND_CXX11REGEX;
733 if ( isCaseSensitive )
734 searchFlags |= QsciScintilla::SCFIND_MATCHCASE;
735 if ( isWholeWordOnly )
736 searchFlags |= QsciScintilla::SCFIND_WHOLEWORD;
737 mEditor->SendScintilla( QsciScintilla::SCI_SETSEARCHFLAGS, searchFlags );
738 int matchCount = 0;
739 while ( true )
740 {
741 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, startPos, docEnd );
742 const int fstart = mEditor->SendScintilla( QsciScintilla::SCI_SEARCHINTARGET, searchString.length(), searchString.toLocal8Bit().constData() );
743 if ( fstart < 0 )
744 break;
745
746 const int matchLength = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETTEXT, 0, static_cast<void *>( nullptr ) );
747
748 if ( matchLength == 0 )
749 {
750 startPos += 1;
751 continue;
752 }
753
754 matchCount++;
755 startPos = fstart + matchLength;
756
757 mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR );
758 mEditor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, fstart, matchLength );
759
760 int thisLine = 0;
761 int thisIndex = 0;
762 mEditor->lineIndexFromPosition( fstart, &thisLine, &thisIndex );
763 mHighlightController->addHighlight( QgsScrollBarHighlight( SearchMatch, thisLine, QColor( 0, 200, 0 ), QgsScrollBarHighlight::Priority::HighPriority ) );
764 }
765
766 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, originalStartPos, originalEndPos );
767
768 searchMatchCountChanged( matchCount );
769}
770
771void QgsCodeEditorWidget::clearSearchHighlights()
772{
773 long docStart = 0;
774 long docEnd = mEditor->length();
775 mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR );
776 mEditor->SendScintilla( QsciScintilla::SCI_INDICATORCLEARRANGE, docStart, docEnd - docStart );
777
778 mHighlightController->removeHighlights( SearchMatch );
779
780 searchMatchCountChanged( 0 );
781}
782
783bool QgsCodeEditorWidget::findText( bool forward, bool findFirst )
784{
785 const QString searchString = mLineEditFind->text();
786 if ( searchString.isEmpty() )
787 return false;
788
789 int lineFrom = 0;
790 int indexFrom = 0;
791 int lineTo = 0;
792 int indexTo = 0;
793 mEditor->getSelection( &lineFrom, &indexFrom, &lineTo, &indexTo );
794
795 int line = 0;
796 int index = 0;
797 if ( !findFirst )
798 {
799 mEditor->getCursorPosition( &line, &index );
800 }
801 if ( !forward )
802 {
803 line = lineFrom;
804 index = indexFrom;
805 }
806
807 const bool isRegEx = mRegexButton->isChecked();
808 const bool wrapAround = mWrapAroundButton->isChecked();
809 const bool isCaseSensitive = mCaseSensitiveButton->isChecked();
810 const bool isWholeWordOnly = mWholeWordButton->isChecked();
811
812 const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward, line, index, true, true, isRegEx );
813
814 if ( !found )
815 {
816 const QString styleError = QStringLiteral( "QLineEdit {background-color: #d65253; color: #ffffff;}" );
817 mLineEditFind->setStyleSheet( styleError );
818 }
819 else
820 {
821 mLineEditFind->setStyleSheet( QString() );
822 }
823 return found;
824}
825
826void QgsCodeEditorWidget::searchMatchCountChanged( int matchCount )
827{
828 mReplaceButton->setEnabled( matchCount > 0 );
829 mReplaceAllButton->setEnabled( matchCount > 0 );
830 mFindNextButton->setEnabled( matchCount > 0 );
831 mFindPrevButton->setEnabled( matchCount > 0 );
832}
833
834void QgsCodeEditorWidget::updateHighlightController()
835{
836 mHighlightController->setLineHeight( QFontMetrics( mEditor->font() ).lineSpacing() );
837 mHighlightController->setVisibleRange( mEditor->viewport()->rect().height() );
838}
@ 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:5733
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)