QGIS API Documentation 3.99.0-Master (26c88405ac0)
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 <QToolButton>
41#include <QVBoxLayout>
42
43#include "moc_qgscodeeditorwidget.cpp"
44
48 QWidget *parent
49)
50 : QgsPanelWidget( parent )
51 , mEditor( editor )
52 , mMessageBar( messageBar )
53{
54 Q_ASSERT( mEditor );
55
56 mEditor->installEventFilter( this );
57 installEventFilter( this );
58
59 QVBoxLayout *vl = new QVBoxLayout();
60 vl->setContentsMargins( 0, 0, 0, 0 );
61 vl->setSpacing( 0 );
62 vl->addWidget( editor, 1 );
63
64 if ( !mMessageBar )
65 {
66 QGridLayout *layout = new QGridLayout( mEditor );
67 layout->setContentsMargins( 0, 0, 0, 0 );
68 layout->addItem( new QSpacerItem( 20, 40, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding ), 1, 0, 1, 1 );
69
70 mMessageBar = new QgsMessageBar();
71 QSizePolicy sizePolicy = QSizePolicy( QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Fixed );
72 mMessageBar->setSizePolicy( sizePolicy );
73 layout->addWidget( mMessageBar, 0, 0, 1, 1 );
74 }
75
76 mFindWidget = new QWidget();
77 QGridLayout *layoutFind = new QGridLayout();
78 layoutFind->setContentsMargins( 0, 2, 0, 0 );
79 layoutFind->setSpacing( 1 );
80
81 if ( !mEditor->isReadOnly() )
82 {
83 mShowReplaceBarButton = new QToolButton();
84 mShowReplaceBarButton->setToolTip( tr( "Replace" ) );
85 mShowReplaceBarButton->setCheckable( true );
86 mShowReplaceBarButton->setAutoRaise( true );
87 mShowReplaceBarButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionReplace.svg" ) ) );
88 layoutFind->addWidget( mShowReplaceBarButton, 0, 0 );
89
90 connect( mShowReplaceBarButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::setReplaceBarVisible );
91 }
92
93 mLineEditFind = new QgsFilterLineEdit();
94 mLineEditFind->setShowSearchIcon( true );
95 mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) );
96 layoutFind->addWidget( mLineEditFind, 0, mShowReplaceBarButton ? 1 : 0 );
97
98 mLineEditReplace = new QgsFilterLineEdit();
99 mLineEditReplace->setShowSearchIcon( true );
100 mLineEditReplace->setPlaceholderText( tr( "Replace…" ) );
101 layoutFind->addWidget( mLineEditReplace, 1, mShowReplaceBarButton ? 1 : 0 );
102
103 QHBoxLayout *findButtonLayout = new QHBoxLayout();
104 findButtonLayout->setContentsMargins( 0, 0, 0, 0 );
105 findButtonLayout->setSpacing( 1 );
106 mCaseSensitiveButton = new QToolButton();
107 mCaseSensitiveButton->setToolTip( tr( "Case Sensitive" ) );
108 mCaseSensitiveButton->setCheckable( true );
109 mCaseSensitiveButton->setAutoRaise( true );
110 mCaseSensitiveButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchCaseSensitive.svg" ) ) );
111 findButtonLayout->addWidget( mCaseSensitiveButton );
112
113 mWholeWordButton = new QToolButton();
114 mWholeWordButton->setToolTip( tr( "Whole Word" ) );
115 mWholeWordButton->setCheckable( true );
116 mWholeWordButton->setAutoRaise( true );
117 mWholeWordButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWholeWord.svg" ) ) );
118 findButtonLayout->addWidget( mWholeWordButton );
119
120 mRegexButton = new QToolButton();
121 mRegexButton->setToolTip( tr( "Use Regular Expressions" ) );
122 mRegexButton->setCheckable( true );
123 mRegexButton->setAutoRaise( true );
124 mRegexButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchRegex.svg" ) ) );
125 findButtonLayout->addWidget( mRegexButton );
126
127 mWrapAroundButton = new QToolButton();
128 mWrapAroundButton->setToolTip( tr( "Wrap Around" ) );
129 mWrapAroundButton->setCheckable( true );
130 mWrapAroundButton->setAutoRaise( true );
131 mWrapAroundButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWrapAround.svg" ) ) );
132 findButtonLayout->addWidget( mWrapAroundButton );
133
134 mFindPrevButton = new QToolButton();
135 mFindPrevButton->setEnabled( false );
136 mFindPrevButton->setToolTip( tr( "Find Previous" ) );
137 mFindPrevButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchPrevEditorConsole.svg" ) ) );
138 mFindPrevButton->setAutoRaise( true );
139 findButtonLayout->addWidget( mFindPrevButton );
140
141 mFindNextButton = new QToolButton();
142 mFindNextButton->setEnabled( false );
143 mFindNextButton->setToolTip( tr( "Find Next" ) );
144 mFindNextButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchNextEditorConsole.svg" ) ) );
145 mFindNextButton->setAutoRaise( true );
146 findButtonLayout->addWidget( mFindNextButton );
147
148 connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext );
149 connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textSearchChanged );
150 connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext );
151 connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious );
152 connect( mCaseSensitiveButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
153 connect( mWholeWordButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
154 connect( mRegexButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
155 connect( mWrapAroundButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch );
156
157 QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor );
158 findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
159 connect( findShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::triggerFind );
160
161 QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this );
162 findNextShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
163 connect( findNextShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findNext );
164
165 QShortcut *findPreviousShortcut = new QShortcut( QKeySequence::StandardKey::FindPrevious, this );
166 findPreviousShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
167 connect( findPreviousShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findPrevious );
168
169 if ( !mEditor->isReadOnly() )
170 {
171 QShortcut *replaceShortcut = new QShortcut( QKeySequence::StandardKey::Replace, this );
172 replaceShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
173 connect( replaceShortcut, &QShortcut::activated, this, [this] {
174 // shortcut toggles bar visibility
175 const bool show = mLineEditReplace->isHidden();
176 setReplaceBarVisible( show );
177
178 // ensure search bar is also visible
179 if ( show )
181 } );
182 }
183
184 // escape on editor hides the find bar
185 QShortcut *closeFindShortcut = new QShortcut( Qt::Key::Key_Escape, this );
186 closeFindShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
187 connect( closeFindShortcut, &QShortcut::activated, this, [this] {
189 mEditor->setFocus();
190 } );
191
192 layoutFind->addLayout( findButtonLayout, 0, mShowReplaceBarButton ? 2 : 1 );
193
194 QHBoxLayout *replaceButtonLayout = new QHBoxLayout();
195 replaceButtonLayout->setContentsMargins( 0, 0, 0, 0 );
196 replaceButtonLayout->setSpacing( 1 );
197
198 mReplaceButton = new QToolButton();
199 mReplaceButton->setText( tr( "Replace" ) );
200 mReplaceButton->setEnabled( false );
201 connect( mReplaceButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::replace );
202 replaceButtonLayout->addWidget( mReplaceButton );
203
204 mReplaceAllButton = new QToolButton();
205 mReplaceAllButton->setText( tr( "Replace All" ) );
206 mReplaceAllButton->setEnabled( false );
207 connect( mReplaceAllButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::replaceAll );
208 replaceButtonLayout->addWidget( mReplaceAllButton );
209
210 layoutFind->addLayout( replaceButtonLayout, 1, mShowReplaceBarButton ? 2 : 1 );
211
212 QToolButton *closeFindButton = new QToolButton( this );
213 closeFindButton->setToolTip( tr( "Close" ) );
214 closeFindButton->setMinimumWidth( QgsGuiUtils::scaleIconSize( 44 ) );
215 closeFindButton->setStyleSheet(
216 "QToolButton { border:none; background-color: rgba(0, 0, 0, 0); }"
217 "QToolButton::menu-button { border:none; background-color: rgba(0, 0, 0, 0); }"
218 );
219 closeFindButton->setCursor( Qt::PointingHandCursor );
220 closeFindButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconClose.svg" ) ) );
221
222 const int iconSize = std::max( 18.0, Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 0.9 );
223 closeFindButton->setIconSize( QSize( iconSize, iconSize ) );
224 closeFindButton->setFixedSize( QSize( iconSize, iconSize ) );
225 connect( closeFindButton, &QAbstractButton::clicked, this, [this] {
227 mEditor->setFocus();
228 } );
229 layoutFind->addWidget( closeFindButton, 0, mShowReplaceBarButton ? 3 : 2 );
230
231 layoutFind->setColumnStretch( mShowReplaceBarButton ? 1 : 0, 1 );
232
233 mFindWidget->setLayout( layoutFind );
234 vl->addWidget( mFindWidget );
235 mFindWidget->hide();
236
237 setReplaceBarVisible( false );
238
239 setLayout( vl );
240
241 mHighlightController = std::make_unique<QgsScrollBarHighlightController>();
242 mHighlightController->setScrollArea( mEditor );
243}
244
245void QgsCodeEditorWidget::resizeEvent( QResizeEvent *event )
246{
247 QgsPanelWidget::resizeEvent( event );
248 updateHighlightController();
249}
250
251void QgsCodeEditorWidget::showEvent( QShowEvent *event )
252{
253 QgsPanelWidget::showEvent( event );
254 updateHighlightController();
255}
256
257bool QgsCodeEditorWidget::eventFilter( QObject *obj, QEvent *event )
258{
259 if ( event->type() == QEvent::FocusIn )
260 {
261 if ( !mFilePath.isEmpty() )
262 {
263 if ( !QFile::exists( mFilePath ) )
264 {
265 // file deleted externally
266 if ( mMessageBar )
267 {
268 mMessageBar->pushCritical( QString(), tr( "The file <b>\"%1\"</b> has been deleted or is not accessible" ).arg( QDir::toNativeSeparators( mFilePath ) ) );
269 }
270 }
271 else
272 {
273 const QFileInfo fi( mFilePath );
274 if ( mLastModified != fi.lastModified() )
275 {
276 // TODO - we should give users a choice of how to react to this, eg "ignore changes"
277 // note -- we intentionally don't call loadFile here -- we want this action to be undo-able
278 QFile file( mFilePath );
279 if ( file.open( QFile::ReadOnly ) )
280 {
281 int currentLine = -1, currentColumn = -1;
282 if ( !mLastModified.isNull() )
283 {
284 mEditor->getCursorPosition( &currentLine, &currentColumn );
285 }
286
287 const QString content = file.readAll();
288
289 // don't clear, instead perform undoable actions:
290 mEditor->beginUndoAction();
291 mEditor->selectAll();
292 mEditor->removeSelectedText();
293 mEditor->insert( content );
294 mEditor->setModified( false );
295 mEditor->recolor();
296 mEditor->endUndoAction();
297
298 mLastModified = fi.lastModified();
299 if ( currentLine >= 0 && currentLine < mEditor->lines() )
300 {
301 mEditor->setCursorPosition( currentLine, currentColumn );
302 }
303
305 }
306 }
307 }
308 }
309 }
310 return QgsPanelWidget::eventFilter( obj, event );
311}
312
314
316{
317 return !mFindWidget->isHidden();
318}
319
321{
322 return mMessageBar;
323}
324
329
330void QgsCodeEditorWidget::addWarning( int lineNumber, const QString &warning )
331{
332 mEditor->addWarning( lineNumber, warning );
333
334 mHighlightController->addHighlight(
336 HighlightCategory::Warning,
337 lineNumber,
338 QColor( 255, 0, 0 ),
340 )
341 );
342}
343
345{
346 mEditor->clearWarnings();
347
348 mHighlightController->removeHighlights(
349 HighlightCategory::Warning
350 );
351}
352
354{
355 addSearchHighlights();
356 mFindWidget->show();
357
358 if ( mEditor->isReadOnly() )
359 {
360 setReplaceBarVisible( false );
361 }
362
363 emit searchBarToggled( true );
364}
365
367{
368 clearSearchHighlights();
369 mFindWidget->hide();
370 emit searchBarToggled( false );
371}
372
374{
375 if ( visible )
377 else
379}
380
382{
383 if ( visible )
384 {
385 mReplaceAllButton->show();
386 mReplaceButton->show();
387 mLineEditReplace->show();
388 }
389 else
390 {
391 mReplaceAllButton->hide();
392 mReplaceButton->hide();
393 mLineEditReplace->hide();
394 }
395 if ( mShowReplaceBarButton )
396 mShowReplaceBarButton->setChecked( visible );
397}
398
400{
401 clearSearchHighlights();
402 mLineEditFind->setFocus();
403 if ( mEditor->hasSelectedText() )
404 {
405 mBlockSearching++;
406 mLineEditFind->setText( mEditor->selectedText().trimmed() );
407 mBlockSearching--;
408 }
409 mLineEditFind->selectAll();
411}
412
413bool QgsCodeEditorWidget::loadFile( const QString &path )
414{
415 if ( !QFile::exists( path ) )
416 return false;
417
418 QFile file( path );
419 if ( file.open( QFile::ReadOnly ) )
420 {
421 const QString content = file.readAll();
422 mEditor->setText( content );
423 setFilePath( path );
424 mEditor->recolor();
425 mEditor->setModified( false );
426 mLastModified = QFileInfo( path ).lastModified();
427 return true;
428 }
429 return false;
430}
431
432void QgsCodeEditorWidget::setFilePath( const QString &path )
433{
434 if ( mFilePath == path )
435 return;
436
437 mFilePath = path;
438 mLastModified = QDateTime();
439
440 emit filePathChanged( mFilePath );
441}
442
443bool QgsCodeEditorWidget::save( const QString &path )
444{
445 const QString filePath = !path.isEmpty() ? path : mFilePath;
446 if ( !filePath.isEmpty() )
447 {
448 QFile file( filePath );
449 if ( file.open( QFile::WriteOnly ) )
450 {
451 file.write( mEditor->text().toUtf8() );
452 file.close();
453
455 mEditor->setModified( false );
456 mLastModified = QFileInfo( filePath ).lastModified();
457
458 return true;
459 }
460 }
461 return false;
462}
463
465{
466 if ( mFilePath.isEmpty() )
467 return false;
468
469 const QDir dir = QFileInfo( mFilePath ).dir();
470
471 bool useFallback = true;
472
473 QString externalEditorCommand;
474 switch ( mEditor->language() )
475 {
477 externalEditorCommand = QgsCodeEditorPython::settingExternalPythonEditorCommand->value();
478 break;
479
490 break;
491 }
492
493 int currentLine, currentColumn;
494 mEditor->getCursorPosition( &currentLine, &currentColumn );
495 if ( line < 0 )
496 line = currentLine;
497 if ( column < 0 )
498 column = currentColumn;
499
500 if ( !externalEditorCommand.isEmpty() )
501 {
502 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<file>" ), mFilePath );
503 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<line>" ), QString::number( line + 1 ) );
504 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<col>" ), QString::number( column + 1 ) );
505
506 const QStringList commandParts = QProcess::splitCommand( externalEditorCommand );
507 if ( QProcess::startDetached( commandParts.at( 0 ), commandParts.mid( 1 ), dir.absolutePath() ) )
508 {
509 return true;
510 }
511 }
512
513 const QString editorCommand = qgetenv( "EDITOR" );
514 if ( !editorCommand.isEmpty() )
515 {
516 const QFileInfo fi( editorCommand );
517 if ( fi.exists() )
518 {
519 const QString command = fi.fileName();
520 const bool isTerminalEditor = command.compare( QLatin1String( "nano" ), Qt::CaseInsensitive ) == 0
521 || command.contains( QLatin1String( "vim" ), Qt::CaseInsensitive );
522
523 if ( !isTerminalEditor && QProcess::startDetached( editorCommand, { mFilePath }, dir.absolutePath() ) )
524 {
525 useFallback = false;
526 }
527 }
528 }
529
530 if ( useFallback )
531 {
532 QDesktopServices::openUrl( QUrl::fromLocalFile( mFilePath ) );
533 }
534 return true;
535}
536
538{
539 const QString accessToken = QgsSettings().value( "pythonConsole/accessTokenGithub", QString() ).toString();
540 if ( accessToken.isEmpty() )
541 {
542 if ( mMessageBar )
543 mMessageBar->pushWarning( QString(), tr( "GitHub personal access token must be generated (see IDE Options)" ) );
544 return false;
545 }
546
547 QString defaultFileName;
548 switch ( mEditor->language() )
549 {
551 defaultFileName = QStringLiteral( "pyqgis_snippet.py" );
552 break;
553
555 defaultFileName = QStringLiteral( "qgis_snippet.css" );
556 break;
557
559 defaultFileName = QStringLiteral( "qgis_snippet" );
560 break;
561
563 defaultFileName = QStringLiteral( "qgis_snippet.html" );
564 break;
565
567 defaultFileName = QStringLiteral( "qgis_snippet.js" );
568 break;
569
571 defaultFileName = QStringLiteral( "qgis_snippet.json" );
572 break;
573
575 defaultFileName = QStringLiteral( "qgis_snippet.r" );
576 break;
577
579 defaultFileName = QStringLiteral( "qgis_snippet.sql" );
580 break;
581
583 defaultFileName = QStringLiteral( "qgis_snippet.bat" );
584 break;
585
587 defaultFileName = QStringLiteral( "qgis_snippet.sh" );
588 break;
589
591 defaultFileName = QStringLiteral( "qgis_snippet.txt" );
592 break;
593 }
594 const QString filename = mFilePath.isEmpty() ? defaultFileName : QFileInfo( mFilePath ).fileName();
595
596 const QString contents = mEditor->hasSelectedText() ? mEditor->selectedText() : mEditor->text();
597 const QVariantMap data {
598 { QStringLiteral( "description" ), "Gist created by PyQGIS Console" },
599 { QStringLiteral( "public" ), isPublic },
600 { QStringLiteral( "files" ), QVariantMap { { filename, QVariantMap { { QStringLiteral( "content" ), contents } } } } }
601 };
602
603 QNetworkRequest request;
604 request.setUrl( QUrl( QStringLiteral( "https://api.github.com/gists" ) ) );
605 request.setRawHeader( "Authorization", QStringLiteral( "token %1" ).arg( accessToken ).toLocal8Bit() );
606 request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/json" ) );
607 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy );
608 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsCodeEditorWidget" ) );
609
610 QNetworkReply *reply = QgsNetworkAccessManager::instance()->post( request, QgsJsonUtils::jsonFromVariant( data ).dump().c_str() );
611 connect( reply, &QNetworkReply::finished, this, [this, reply] {
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, line, index, true, true, isRegEx );
816
817 if ( !found )
818 {
819 const QString styleError = QStringLiteral( "QLineEdit {background-color: #d65253; color: #ffffff;}" );
820 mLineEditFind->setStyleSheet( styleError );
821 }
822 else
823 {
824 mLineEditFind->setStyleSheet( QString() );
825 }
826 return found;
827}
828
829void QgsCodeEditorWidget::searchMatchCountChanged( int matchCount )
830{
831 mReplaceButton->setEnabled( matchCount > 0 );
832 mReplaceAllButton->setEnabled( matchCount > 0 );
833 mFindNextButton->setEnabled( matchCount > 0 );
834 mFindPrevButton->setEnabled( matchCount > 0 );
835}
836
837void QgsCodeEditorWidget::updateHighlightController()
838{
839 mHighlightController->setLineHeight( QFontMetrics( mEditor->font() ).lineSpacing() );
840 mHighlightController->setVisibleRange( mEditor->viewport()->rect().height() );
841}
@ QgisExpression
QGIS expressions.
Definition qgis.h:4480
@ Batch
Windows batch files.
Definition qgis.h:4487
@ JavaScript
JavaScript.
Definition qgis.h:4482
@ Bash
Bash scripts.
Definition qgis.h:4488
@ Unknown
Unknown/other language.
Definition qgis.h:4489
@ Python
Python.
Definition qgis.h:4484
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition qgis.h:6222
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:65
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)