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