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