QGIS API Documentation 3.39.0-Master (47f7b3a4989)
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 "qgscodeeditor.h"
18#include "qgsfilterlineedit.h"
19#include "qgsapplication.h"
20#include "qgsguiutils.h"
21#include "qgsmessagebar.h"
23#include "qgscodeeditorpython.h"
26#include "qgsjsonutils.h"
27#include "nlohmann/json.hpp"
28#include "qgssettings.h"
29
30#include <QVBoxLayout>
31#include <QToolButton>
32#include <QCheckBox>
33#include <QShortcut>
34#include <QGridLayout>
35#include <QDesktopServices>
36#include <QProcess>
37#include <QFileInfo>
38#include <QDir>
39#include <QNetworkRequest>
40
42 QgsCodeEditor *editor,
43 QgsMessageBar *messageBar,
44 QWidget *parent )
45 : QgsPanelWidget( parent )
46 , mEditor( editor )
47 , mMessageBar( messageBar )
48{
49 Q_ASSERT( mEditor );
50
51 mEditor->installEventFilter( this );
52 installEventFilter( this );
53
54 QVBoxLayout *vl = new QVBoxLayout();
55 vl->setContentsMargins( 0, 0, 0, 0 );
56 vl->setSpacing( 0 );
57 vl->addWidget( editor, 1 );
58
59 if ( !mMessageBar )
60 {
61 QGridLayout *layout = new QGridLayout( mEditor );
62 layout->setContentsMargins( 0, 0, 0, 0 );
63 layout->addItem( new QSpacerItem( 20, 40, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding ), 1, 0, 1, 1 );
64
65 mMessageBar = new QgsMessageBar();
66 QSizePolicy sizePolicy = QSizePolicy( QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Fixed );
67 mMessageBar->setSizePolicy( sizePolicy );
68 layout->addWidget( mMessageBar, 0, 0, 1, 1 );
69 }
70
71 mFindWidget = new QWidget();
72 QGridLayout *layoutFind = new QGridLayout();
73 layoutFind->setContentsMargins( 0, 2, 0, 0 );
74 layoutFind->setSpacing( 1 );
75
76 if ( !mEditor->isReadOnly() )
77 {
78 mShowReplaceBarButton = new QToolButton();
79 mShowReplaceBarButton->setToolTip( tr( "Replace" ) );
80 mShowReplaceBarButton->setCheckable( true );
81 mShowReplaceBarButton->setAutoRaise( true );
82 mShowReplaceBarButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionReplace.svg" ) ) );
83 layoutFind->addWidget( mShowReplaceBarButton, 0, 0 );
84
85 connect( mShowReplaceBarButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::setReplaceBarVisible );
86 }
87
88 mLineEditFind = new QgsFilterLineEdit();
89 mLineEditFind->setShowSearchIcon( true );
90 mLineEditFind->setPlaceholderText( tr( "Enter text to find…" ) );
91 layoutFind->addWidget( mLineEditFind, 0, mShowReplaceBarButton ? 1 : 0 );
92
93 mLineEditReplace = new QgsFilterLineEdit();
94 mLineEditReplace->setShowSearchIcon( true );
95 mLineEditReplace->setPlaceholderText( tr( "Replace…" ) );
96 layoutFind->addWidget( mLineEditReplace, 1, mShowReplaceBarButton ? 1 : 0 );
97
98 QHBoxLayout *findButtonLayout = new QHBoxLayout();
99 findButtonLayout->setContentsMargins( 0, 0, 0, 0 );
100 findButtonLayout->setSpacing( 1 );
101 mCaseSensitiveButton = new QToolButton();
102 mCaseSensitiveButton->setToolTip( tr( "Case Sensitive" ) );
103 mCaseSensitiveButton->setCheckable( true );
104 mCaseSensitiveButton->setAutoRaise( true );
105 mCaseSensitiveButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchCaseSensitive.svg" ) ) );
106 findButtonLayout->addWidget( mCaseSensitiveButton );
107
108 mWholeWordButton = new QToolButton( );
109 mWholeWordButton->setToolTip( tr( "Whole Word" ) );
110 mWholeWordButton->setCheckable( true );
111 mWholeWordButton->setAutoRaise( true );
112 mWholeWordButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWholeWord.svg" ) ) );
113 findButtonLayout->addWidget( mWholeWordButton );
114
115 mRegexButton = new QToolButton( );
116 mRegexButton->setToolTip( tr( "Use Regular Expressions" ) );
117 mRegexButton->setCheckable( true );
118 mRegexButton->setAutoRaise( true );
119 mRegexButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchRegex.svg" ) ) );
120 findButtonLayout->addWidget( mRegexButton );
121
122 mWrapAroundButton = new QToolButton();
123 mWrapAroundButton->setToolTip( tr( "Wrap Around" ) );
124 mWrapAroundButton->setCheckable( true );
125 mWrapAroundButton->setAutoRaise( true );
126 mWrapAroundButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconSearchWrapAround.svg" ) ) );
127 findButtonLayout->addWidget( mWrapAroundButton );
128
129 mFindPrevButton = new QToolButton();
130 mFindPrevButton->setEnabled( false );
131 mFindPrevButton->setToolTip( tr( "Find Previous" ) );
132 mFindPrevButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchPrevEditorConsole.svg" ) ) );
133 mFindPrevButton->setAutoRaise( true );
134 findButtonLayout->addWidget( mFindPrevButton );
135
136 mFindNextButton = new QToolButton();
137 mFindNextButton->setEnabled( false );
138 mFindNextButton->setToolTip( tr( "Find Next" ) );
139 mFindNextButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSearchNextEditorConsole.svg" ) ) );
140 mFindNextButton->setAutoRaise( true );
141 findButtonLayout->addWidget( mFindNextButton );
142
143 connect( mLineEditFind, &QLineEdit::returnPressed, this, &QgsCodeEditorWidget::findNext );
144 connect( mLineEditFind, &QLineEdit::textChanged, this, &QgsCodeEditorWidget::textSearchChanged );
145 connect( mFindNextButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findNext );
146 connect( mFindPrevButton, &QToolButton::clicked, this, &QgsCodeEditorWidget::findPrevious );
147 connect( mCaseSensitiveButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
148 connect( mWholeWordButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
149 connect( mRegexButton, &QToolButton::toggled, this, &QgsCodeEditorWidget::updateSearch );
150 connect( mWrapAroundButton, &QCheckBox::toggled, this, &QgsCodeEditorWidget::updateSearch );
151
152 QShortcut *findShortcut = new QShortcut( QKeySequence::StandardKey::Find, mEditor );
153 findShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
154 connect( findShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::triggerFind );
155
156 QShortcut *findNextShortcut = new QShortcut( QKeySequence::StandardKey::FindNext, this );
157 findNextShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
158 connect( findNextShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findNext );
159
160 QShortcut *findPreviousShortcut = new QShortcut( QKeySequence::StandardKey::FindPrevious, this );
161 findPreviousShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
162 connect( findPreviousShortcut, &QShortcut::activated, this, &QgsCodeEditorWidget::findPrevious );
163
164 if ( !mEditor->isReadOnly() )
165 {
166 QShortcut *replaceShortcut = new QShortcut( QKeySequence::StandardKey::Replace, this );
167 replaceShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
168 connect( replaceShortcut, &QShortcut::activated, this, [ = ]
169 {
170 // shortcut toggles bar visibility
171 const bool show = mLineEditReplace->isHidden();
172 setReplaceBarVisible( show );
173
174 // ensure search bar is also visible
175 if ( show )
177 } );
178 }
179
180 // escape on editor hides the find bar
181 QShortcut *closeFindShortcut = new QShortcut( Qt::Key::Key_Escape, this );
182 closeFindShortcut->setContext( Qt::ShortcutContext::WidgetWithChildrenShortcut );
183 connect( closeFindShortcut, &QShortcut::activated, this, [this]
184 {
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 closeFindButton->setCursor( Qt::PointingHandCursor );
216 closeFindButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconClose.svg" ) ) );
217
218 const int iconSize = std::max( 18.0, Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 0.9 );
219 closeFindButton->setIconSize( QSize( iconSize, iconSize ) );
220 closeFindButton->setFixedSize( QSize( iconSize, iconSize ) );
221 connect( closeFindButton, &QAbstractButton::clicked, this, [this]
222 {
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 const QString content = file.readAll();
279
280 // don't clear, instead perform undoable actions:
281 mEditor->beginUndoAction();
282 mEditor->selectAll();
283 mEditor->removeSelectedText();
284 mEditor->insert( content );
285 mEditor->setModified( false );
286 mEditor->recolor();
287 mEditor->endUndoAction();
288
289 mLastModified = fi.lastModified();
291 }
292 }
293 }
294 }
295 }
296 return QgsPanelWidget::eventFilter( obj, event );
297}
298
300
302{
303 return !mFindWidget->isHidden();
304}
305
307{
308 return mMessageBar;
309}
310
315
316void QgsCodeEditorWidget::addWarning( int lineNumber, const QString &warning )
317{
318 mEditor->addWarning( lineNumber, warning );
319
320 mHighlightController->addHighlight(
322 HighlightCategory::Warning,
323 lineNumber,
324 QColor( 255, 0, 0 ),
326 )
327 );
328}
329
331{
332 mEditor->clearWarnings();
333
334 mHighlightController->removeHighlights(
335 HighlightCategory::Warning
336 );
337}
338
340{
341 addSearchHighlights();
342 mFindWidget->show();
343
344 if ( mEditor->isReadOnly() )
345 {
346 setReplaceBarVisible( false );
347 }
348
349 emit searchBarToggled( true );
350}
351
353{
354 clearSearchHighlights();
355 mFindWidget->hide();
356 emit searchBarToggled( false );
357}
358
360{
361 if ( visible )
363 else
365}
366
368{
369 if ( visible )
370 {
371 mReplaceAllButton->show();
372 mReplaceButton->show();
373 mLineEditReplace->show();
374 }
375 else
376 {
377 mReplaceAllButton->hide();
378 mReplaceButton->hide();
379 mLineEditReplace->hide();
380 }
381 if ( mShowReplaceBarButton )
382 mShowReplaceBarButton->setChecked( visible );
383}
384
386{
387 clearSearchHighlights();
388 mLineEditFind->setFocus();
389 if ( mEditor->hasSelectedText() )
390 {
391 mBlockSearching++;
392 mLineEditFind->setText( mEditor->selectedText().trimmed() );
393 mBlockSearching--;
394 }
395 mLineEditFind->selectAll();
397}
398
399bool QgsCodeEditorWidget::loadFile( const QString &path )
400{
401 if ( !QFile::exists( path ) )
402 return false;
403
404 QFile file( path );
405 if ( file.open( QFile::ReadOnly ) )
406 {
407 const QString content = file.readAll();
408 mEditor->setText( content );
409 mEditor->setModified( false );
410 mEditor->recolor();
411 mLastModified = QFileInfo( path ).lastModified();
412 setFilePath( path );
413 return true;
414 }
415 return false;
416}
417
418void QgsCodeEditorWidget::setFilePath( const QString &path )
419{
420 if ( mFilePath == path )
421 return;
422
423 mFilePath = path;
424 emit filePathChanged( mFilePath );
425}
426
428{
429 if ( mFilePath.isEmpty() )
430 return false;
431
432 const QDir dir = QFileInfo( mFilePath ).dir();
433
434 bool useFallback = true;
435
436 QString externalEditorCommand;
437 switch ( mEditor->language() )
438 {
440 externalEditorCommand = QgsCodeEditorPython::settingExternalPythonEditorCommand->value();
441 break;
442
453 break;
454 }
455
456 int currentLine, currentColumn;
457 mEditor->getCursorPosition( &currentLine, &currentColumn );
458 if ( line < 0 )
459 line = currentLine;
460 if ( column < 0 )
461 column = currentColumn;
462
463 if ( !externalEditorCommand.isEmpty() )
464 {
465 externalEditorCommand = externalEditorCommand.replace( QStringLiteral( "<file>" ), mFilePath );
466 externalEditorCommand = externalEditorCommand.replace( QStringLiteral( "<line>" ), QString::number( line + 1 ) );
467 externalEditorCommand = externalEditorCommand.replace( QStringLiteral( "<col>" ), QString::number( column + 1 ) );
468
469 const QStringList commandParts = QProcess::splitCommand( externalEditorCommand );
470 if ( QProcess::startDetached( commandParts.at( 0 ), commandParts.mid( 1 ), dir.absolutePath() ) )
471 {
472 return true;
473 }
474 }
475
476 const QString editorCommand = qgetenv( "EDITOR" );
477 if ( !editorCommand.isEmpty() )
478 {
479 const QFileInfo fi( editorCommand );
480 if ( fi.exists( ) )
481 {
482 const QString command = fi.fileName();
483 const bool isTerminalEditor = command.compare( QLatin1String( "nano" ), Qt::CaseInsensitive ) == 0
484 || command.contains( QLatin1String( "vim" ), Qt::CaseInsensitive );
485
486 if ( !isTerminalEditor && QProcess::startDetached( editorCommand, {mFilePath}, dir.absolutePath() ) )
487 {
488 useFallback = false;
489 }
490 }
491 }
492
493 if ( useFallback )
494 {
495 QDesktopServices::openUrl( QUrl::fromLocalFile( mFilePath ) );
496 }
497 return true;
498}
499
501{
502 const QString accessToken = QgsSettings().value( "pythonConsole/accessTokenGithub", QString() ).toString();
503 if ( accessToken.isEmpty() )
504 {
505 if ( mMessageBar )
506 mMessageBar->pushWarning( QString(), tr( "GitHub personal access token must be generated (see IDE Options)" ) );
507 return false;
508 }
509
510 QString defaultFileName;
511 switch ( mEditor->language() )
512 {
514 defaultFileName = QStringLiteral( "pyqgis_snippet.py" );
515 break;
516
518 defaultFileName = QStringLiteral( "qgis_snippet.css" );
519 break;
520
522 defaultFileName = QStringLiteral( "qgis_snippet" );
523 break;
524
526 defaultFileName = QStringLiteral( "qgis_snippet.html" );
527 break;
528
530 defaultFileName = QStringLiteral( "qgis_snippet.js" );
531 break;
532
534 defaultFileName = QStringLiteral( "qgis_snippet.json" );
535 break;
536
538 defaultFileName = QStringLiteral( "qgis_snippet.r" );
539 break;
540
542 defaultFileName = QStringLiteral( "qgis_snippet.sql" );
543 break;
544
546 defaultFileName = QStringLiteral( "qgis_snippet.bat" );
547 break;
548
550 defaultFileName = QStringLiteral( "qgis_snippet.sh" );
551 break;
552
554 defaultFileName = QStringLiteral( "qgis_snippet.txt" );
555 break;
556 }
557 const QString filename = mFilePath.isEmpty() ? defaultFileName : QFileInfo( mFilePath ).fileName();
558
559 const QString contents = mEditor->hasSelectedText() ? mEditor->selectedText() : mEditor->text();
560 const QVariantMap data
561 {
562 { QStringLiteral( "description" ), "Gist created by PyQGIS Console"},
563 { QStringLiteral( "public" ), isPublic },
564 { QStringLiteral( "files" ), QVariantMap{ {filename, QVariantMap{{ QStringLiteral( "content" ), contents }} } } }
565 };
566
567 QNetworkRequest request;
568 request.setUrl( QUrl( QStringLiteral( "https://api.github.com/gists" ) ) );
569 request.setRawHeader( "Authorization", QStringLiteral( "token %1" ).arg( accessToken ).toLocal8Bit() );
570 request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/json" ) );
571 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy );
572 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsCodeEditorWidget" ) );
573
574 QNetworkReply *reply = QgsNetworkAccessManager::instance()->post( request, QgsJsonUtils::jsonFromVariant( data ).dump().c_str() );
575 connect( reply, &QNetworkReply::finished, this, [this, reply]
576 {
577 if ( reply->error() == QNetworkReply::NoError )
578 {
579 const QVariantMap replyJson = QgsJsonUtils::parseJson( reply->readAll() ).toMap();
580 const QString link = replyJson.value( QStringLiteral( "html_url" ) ).toString();
581 QDesktopServices::openUrl( QUrl( link ) );
582 }
583 else
584 {
585 if ( mMessageBar )
586 mMessageBar->pushCritical( QString(), tr( "Connection error: %1" ).arg( reply->errorString() ) );
587 }
588 reply->deleteLater();
589 } );
590 return true;
591}
592
593bool QgsCodeEditorWidget::findNext()
594{
595 return findText( true, false );
596}
597
598void QgsCodeEditorWidget::findPrevious()
599{
600 findText( false, false );
601}
602
603void QgsCodeEditorWidget::textSearchChanged( const QString &text )
604{
605 if ( !text.isEmpty() )
606 {
607 updateSearch();
608 }
609 else
610 {
611 clearSearchHighlights();
612 mLineEditFind->setStyleSheet( QString() );
613 }
614}
615
616void QgsCodeEditorWidget::updateSearch()
617{
618 if ( mBlockSearching )
619 return;
620
621 clearSearchHighlights();
622 addSearchHighlights();
623
624 findText( true, true );
625}
626
627void QgsCodeEditorWidget::replace()
628{
629 if ( mEditor->isReadOnly() )
630 return;
631
632 replaceSelection();
633
634 clearSearchHighlights();
635 addSearchHighlights();
636 findNext();
637}
638
639void QgsCodeEditorWidget::replaceSelection()
640{
641 const long selectionStart = mEditor->SendScintilla( QsciScintilla::SCI_GETSELECTIONSTART );
642 const long selectionEnd = mEditor->SendScintilla( QsciScintilla::SCI_GETSELECTIONEND );
643 if ( selectionEnd - selectionStart <= 0 )
644 return;
645
646 const QString replacement = mLineEditReplace->text();
647
648 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, selectionStart, selectionEnd );
649
650 if ( mRegexButton->isChecked() )
651 mEditor->SendScintilla( QsciScintilla::SCI_REPLACETARGETRE, replacement.size(), replacement.toLocal8Bit().constData() );
652 else
653 mEditor->SendScintilla( QsciScintilla::SCI_REPLACETARGET, replacement.size(), replacement.toLocal8Bit().constData() );
654
655 // set the cursor to the end of the replaced text
656 const long postReplacementEnd = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND );
657 mEditor->SendScintilla( QsciScintilla::SCI_SETCURRENTPOS, postReplacementEnd );
658}
659
660void QgsCodeEditorWidget::replaceAll()
661{
662 if ( mEditor->isReadOnly() )
663 return;
664
665 if ( !findText( true, true ) )
666 {
667 return;
668 }
669
670 mEditor->SendScintilla( QsciScintilla::SCI_BEGINUNDOACTION );
671 replaceSelection();
672
673 while ( findText( true, false ) )
674 {
675 replaceSelection();
676 }
677
678 mEditor->SendScintilla( QsciScintilla::SCI_ENDUNDOACTION );
679 clearSearchHighlights();
680}
681
682void QgsCodeEditorWidget::addSearchHighlights()
683{
684 const QString searchString = mLineEditFind->text();
685 if ( searchString.isEmpty() )
686 return;
687
688 const long originalStartPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETSTART );
689 const long originalEndPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND );
690 long startPos = 0;
691 long docEnd = mEditor->length();
692
693 updateHighlightController();
694
695 int searchFlags = 0;
696 const bool isRegEx = mRegexButton->isChecked();
697 const bool isCaseSensitive = mCaseSensitiveButton->isChecked();
698 const bool isWholeWordOnly = mWholeWordButton->isChecked();
699 if ( isRegEx )
700 searchFlags |= QsciScintilla::SCFIND_REGEXP | QsciScintilla::SCFIND_CXX11REGEX;
701 if ( isCaseSensitive )
702 searchFlags |= QsciScintilla::SCFIND_MATCHCASE;
703 if ( isWholeWordOnly )
704 searchFlags |= QsciScintilla::SCFIND_WHOLEWORD;
705 mEditor->SendScintilla( QsciScintilla::SCI_SETSEARCHFLAGS, searchFlags );
706 int matchCount = 0;
707 while ( true )
708 {
709 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, startPos, docEnd );
710 const int fstart = mEditor->SendScintilla( QsciScintilla::SCI_SEARCHINTARGET, searchString.length(), searchString.toLocal8Bit().constData() );
711 if ( fstart < 0 )
712 break;
713
714 matchCount++;
715 const int matchLength = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETTEXT, 0, static_cast< void * >( nullptr ) );
716
717 startPos = fstart + matchLength;
718
719 mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR );
720 mEditor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, fstart, matchLength );
721
722 int thisLine = 0;
723 int thisIndex = 0;
724 mEditor->lineIndexFromPosition( fstart, &thisLine, &thisIndex );
725 mHighlightController->addHighlight( QgsScrollBarHighlight( SearchMatch, thisLine, QColor( 0, 200, 0 ), QgsScrollBarHighlight::Priority::HighPriority ) );
726 }
727
728 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, originalStartPos, originalEndPos );
729
730 searchMatchCountChanged( matchCount );
731}
732
733void QgsCodeEditorWidget::clearSearchHighlights()
734{
735 long docStart = 0;
736 long docEnd = mEditor->length();
737 mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR );
738 mEditor->SendScintilla( QsciScintilla::SCI_INDICATORCLEARRANGE, docStart, docEnd - docStart );
739
740 mHighlightController->removeHighlights( SearchMatch );
741
742 searchMatchCountChanged( 0 );
743}
744
745bool QgsCodeEditorWidget::findText( bool forward, bool findFirst )
746{
747 const QString searchString = mLineEditFind->text();
748 if ( searchString.isEmpty() )
749 return false;
750
751 int lineFrom = 0;
752 int indexFrom = 0;
753 int lineTo = 0;
754 int indexTo = 0;
755 mEditor->getSelection( &lineFrom, &indexFrom, &lineTo, &indexTo );
756
757 int line = 0;
758 int index = 0;
759 if ( !findFirst )
760 {
761 mEditor->getCursorPosition( &line, &index );
762 }
763 if ( !forward )
764 {
765 line = lineFrom;
766 index = indexFrom;
767 }
768
769 const bool isRegEx = mRegexButton->isChecked();
770 const bool wrapAround = mWrapAroundButton->isChecked();
771 const bool isCaseSensitive = mCaseSensitiveButton->isChecked();
772 const bool isWholeWordOnly = mWholeWordButton->isChecked();
773
774 const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward,
775 line, index, true, true, isRegEx );
776
777 if ( !found )
778 {
779 const QString styleError = QStringLiteral( "QLineEdit {background-color: #d65253; color: #ffffff;}" );
780 mLineEditFind->setStyleSheet( styleError );
781 }
782 else
783 {
784 mLineEditFind->setStyleSheet( QString() );
785 }
786 return found;
787}
788
789void QgsCodeEditorWidget::searchMatchCountChanged( int matchCount )
790{
791 mReplaceButton->setEnabled( matchCount > 0 );
792 mReplaceAllButton->setEnabled( matchCount > 0 );
793 mFindNextButton->setEnabled( matchCount > 0 );
794 mFindPrevButton->setEnabled( matchCount > 0 );
795}
796
797void QgsCodeEditorWidget::updateHighlightController()
798{
799 mHighlightController->setLineHeight( QFontMetrics( mEditor->font() ).lineSpacing() );
800 mHighlightController->setVisibleRange( mEditor->viewport()->rect().height() );
801}
802
@ 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:5182
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.
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.
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)