QGIS API Documentation 3.40.0-Bratislava (b56115d8743)
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 int currentLine = -1, currentColumn = -1;
279 if ( !mLastModified.isNull() )
280 {
281 mEditor->getCursorPosition( &currentLine, &currentColumn );
282 }
283
284 const QString content = file.readAll();
285
286 // don't clear, instead perform undoable actions:
287 mEditor->beginUndoAction();
288 mEditor->selectAll();
289 mEditor->removeSelectedText();
290 mEditor->insert( content );
291 mEditor->setModified( false );
292 mEditor->recolor();
293 mEditor->endUndoAction();
294
295 mLastModified = fi.lastModified();
296 if ( currentLine >= 0 && currentLine < mEditor->lines() )
297 {
298 mEditor->setCursorPosition( currentLine, currentColumn );
299 }
300
302 }
303 }
304 }
305 }
306 }
307 return QgsPanelWidget::eventFilter( obj, event );
308}
309
311
313{
314 return !mFindWidget->isHidden();
315}
316
318{
319 return mMessageBar;
320}
321
326
327void QgsCodeEditorWidget::addWarning( int lineNumber, const QString &warning )
328{
329 mEditor->addWarning( lineNumber, warning );
330
331 mHighlightController->addHighlight(
333 HighlightCategory::Warning,
334 lineNumber,
335 QColor( 255, 0, 0 ),
337 )
338 );
339}
340
342{
343 mEditor->clearWarnings();
344
345 mHighlightController->removeHighlights(
346 HighlightCategory::Warning
347 );
348}
349
351{
352 addSearchHighlights();
353 mFindWidget->show();
354
355 if ( mEditor->isReadOnly() )
356 {
357 setReplaceBarVisible( false );
358 }
359
360 emit searchBarToggled( true );
361}
362
364{
365 clearSearchHighlights();
366 mFindWidget->hide();
367 emit searchBarToggled( false );
368}
369
371{
372 if ( visible )
374 else
376}
377
379{
380 if ( visible )
381 {
382 mReplaceAllButton->show();
383 mReplaceButton->show();
384 mLineEditReplace->show();
385 }
386 else
387 {
388 mReplaceAllButton->hide();
389 mReplaceButton->hide();
390 mLineEditReplace->hide();
391 }
392 if ( mShowReplaceBarButton )
393 mShowReplaceBarButton->setChecked( visible );
394}
395
397{
398 clearSearchHighlights();
399 mLineEditFind->setFocus();
400 if ( mEditor->hasSelectedText() )
401 {
402 mBlockSearching++;
403 mLineEditFind->setText( mEditor->selectedText().trimmed() );
404 mBlockSearching--;
405 }
406 mLineEditFind->selectAll();
408}
409
410bool QgsCodeEditorWidget::loadFile( const QString &path )
411{
412 if ( !QFile::exists( path ) )
413 return false;
414
415 QFile file( path );
416 if ( file.open( QFile::ReadOnly ) )
417 {
418 const QString content = file.readAll();
419 mEditor->setText( content );
420 mEditor->setModified( false );
421 mEditor->recolor();
422 mLastModified = QFileInfo( path ).lastModified();
423 setFilePath( path );
424 return true;
425 }
426 return false;
427}
428
429void QgsCodeEditorWidget::setFilePath( const QString &path )
430{
431 if ( mFilePath == path )
432 return;
433
434 mFilePath = path;
435 mLastModified = QDateTime();
436
437 emit filePathChanged( mFilePath );
438}
439
440bool QgsCodeEditorWidget::save( const QString &path )
441{
442 const QString filePath = !path.isEmpty() ? path : mFilePath;
443 if ( !filePath.isEmpty() )
444 {
445 QFile file( filePath );
446 if ( file.open( QFile::WriteOnly ) )
447 {
448 file.write( mEditor->text().toUtf8() );
449 file.close();
450
451 mEditor->setModified( false );
452 mLastModified = QFileInfo( filePath ).lastModified();
454
455 return true;
456 }
457 }
458 return false;
459}
460
462{
463 if ( mFilePath.isEmpty() )
464 return false;
465
466 const QDir dir = QFileInfo( mFilePath ).dir();
467
468 bool useFallback = true;
469
470 QString externalEditorCommand;
471 switch ( mEditor->language() )
472 {
474 externalEditorCommand = QgsCodeEditorPython::settingExternalPythonEditorCommand->value();
475 break;
476
487 break;
488 }
489
490 int currentLine, currentColumn;
491 mEditor->getCursorPosition( &currentLine, &currentColumn );
492 if ( line < 0 )
493 line = currentLine;
494 if ( column < 0 )
495 column = currentColumn;
496
497 if ( !externalEditorCommand.isEmpty() )
498 {
499 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<file>" ), mFilePath );
500 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<line>" ), QString::number( line + 1 ) );
501 externalEditorCommand = externalEditorCommand.replace( QLatin1String( "<col>" ), QString::number( column + 1 ) );
502
503 const QStringList commandParts = QProcess::splitCommand( externalEditorCommand );
504 if ( QProcess::startDetached( commandParts.at( 0 ), commandParts.mid( 1 ), dir.absolutePath() ) )
505 {
506 return true;
507 }
508 }
509
510 const QString editorCommand = qgetenv( "EDITOR" );
511 if ( !editorCommand.isEmpty() )
512 {
513 const QFileInfo fi( editorCommand );
514 if ( fi.exists( ) )
515 {
516 const QString command = fi.fileName();
517 const bool isTerminalEditor = command.compare( QLatin1String( "nano" ), Qt::CaseInsensitive ) == 0
518 || command.contains( QLatin1String( "vim" ), Qt::CaseInsensitive );
519
520 if ( !isTerminalEditor && QProcess::startDetached( editorCommand, {mFilePath}, dir.absolutePath() ) )
521 {
522 useFallback = false;
523 }
524 }
525 }
526
527 if ( useFallback )
528 {
529 QDesktopServices::openUrl( QUrl::fromLocalFile( mFilePath ) );
530 }
531 return true;
532}
533
535{
536 const QString accessToken = QgsSettings().value( "pythonConsole/accessTokenGithub", QString() ).toString();
537 if ( accessToken.isEmpty() )
538 {
539 if ( mMessageBar )
540 mMessageBar->pushWarning( QString(), tr( "GitHub personal access token must be generated (see IDE Options)" ) );
541 return false;
542 }
543
544 QString defaultFileName;
545 switch ( mEditor->language() )
546 {
548 defaultFileName = QStringLiteral( "pyqgis_snippet.py" );
549 break;
550
552 defaultFileName = QStringLiteral( "qgis_snippet.css" );
553 break;
554
556 defaultFileName = QStringLiteral( "qgis_snippet" );
557 break;
558
560 defaultFileName = QStringLiteral( "qgis_snippet.html" );
561 break;
562
564 defaultFileName = QStringLiteral( "qgis_snippet.js" );
565 break;
566
568 defaultFileName = QStringLiteral( "qgis_snippet.json" );
569 break;
570
572 defaultFileName = QStringLiteral( "qgis_snippet.r" );
573 break;
574
576 defaultFileName = QStringLiteral( "qgis_snippet.sql" );
577 break;
578
580 defaultFileName = QStringLiteral( "qgis_snippet.bat" );
581 break;
582
584 defaultFileName = QStringLiteral( "qgis_snippet.sh" );
585 break;
586
588 defaultFileName = QStringLiteral( "qgis_snippet.txt" );
589 break;
590 }
591 const QString filename = mFilePath.isEmpty() ? defaultFileName : QFileInfo( mFilePath ).fileName();
592
593 const QString contents = mEditor->hasSelectedText() ? mEditor->selectedText() : mEditor->text();
594 const QVariantMap data
595 {
596 { QStringLiteral( "description" ), "Gist created by PyQGIS Console"},
597 { QStringLiteral( "public" ), isPublic },
598 { QStringLiteral( "files" ), QVariantMap{ {filename, QVariantMap{{ QStringLiteral( "content" ), contents }} } } }
599 };
600
601 QNetworkRequest request;
602 request.setUrl( QUrl( QStringLiteral( "https://api.github.com/gists" ) ) );
603 request.setRawHeader( "Authorization", QStringLiteral( "token %1" ).arg( accessToken ).toLocal8Bit() );
604 request.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/json" ) );
605 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy );
606 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsCodeEditorWidget" ) );
607
608 QNetworkReply *reply = QgsNetworkAccessManager::instance()->post( request, QgsJsonUtils::jsonFromVariant( data ).dump().c_str() );
609 connect( reply, &QNetworkReply::finished, this, [this, reply]
610 {
611 if ( reply->error() == QNetworkReply::NoError )
612 {
613 const QVariantMap replyJson = QgsJsonUtils::parseJson( reply->readAll() ).toMap();
614 const QString link = replyJson.value( QStringLiteral( "html_url" ) ).toString();
615 QDesktopServices::openUrl( QUrl( link ) );
616 }
617 else
618 {
619 if ( mMessageBar )
620 mMessageBar->pushCritical( QString(), tr( "Connection error: %1" ).arg( reply->errorString() ) );
621 }
622 reply->deleteLater();
623 } );
624 return true;
625}
626
627bool QgsCodeEditorWidget::findNext()
628{
629 return findText( true, false );
630}
631
632void QgsCodeEditorWidget::findPrevious()
633{
634 findText( false, false );
635}
636
637void QgsCodeEditorWidget::textSearchChanged( const QString &text )
638{
639 if ( !text.isEmpty() )
640 {
641 updateSearch();
642 }
643 else
644 {
645 clearSearchHighlights();
646 mLineEditFind->setStyleSheet( QString() );
647 }
648}
649
650void QgsCodeEditorWidget::updateSearch()
651{
652 if ( mBlockSearching )
653 return;
654
655 clearSearchHighlights();
656 addSearchHighlights();
657
658 findText( true, true );
659}
660
661void QgsCodeEditorWidget::replace()
662{
663 if ( mEditor->isReadOnly() )
664 return;
665
666 replaceSelection();
667
668 clearSearchHighlights();
669 addSearchHighlights();
670 findNext();
671}
672
673void QgsCodeEditorWidget::replaceSelection()
674{
675 const long selectionStart = mEditor->SendScintilla( QsciScintilla::SCI_GETSELECTIONSTART );
676 const long selectionEnd = mEditor->SendScintilla( QsciScintilla::SCI_GETSELECTIONEND );
677 if ( selectionEnd - selectionStart <= 0 )
678 return;
679
680 const QString replacement = mLineEditReplace->text();
681
682 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, selectionStart, selectionEnd );
683
684 if ( mRegexButton->isChecked() )
685 mEditor->SendScintilla( QsciScintilla::SCI_REPLACETARGETRE, replacement.size(), replacement.toLocal8Bit().constData() );
686 else
687 mEditor->SendScintilla( QsciScintilla::SCI_REPLACETARGET, replacement.size(), replacement.toLocal8Bit().constData() );
688
689 // set the cursor to the end of the replaced text
690 const long postReplacementEnd = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND );
691 mEditor->SendScintilla( QsciScintilla::SCI_SETCURRENTPOS, postReplacementEnd );
692}
693
694void QgsCodeEditorWidget::replaceAll()
695{
696 if ( mEditor->isReadOnly() )
697 return;
698
699 if ( !findText( true, true ) )
700 {
701 return;
702 }
703
704 mEditor->SendScintilla( QsciScintilla::SCI_BEGINUNDOACTION );
705 replaceSelection();
706
707 while ( findText( true, false ) )
708 {
709 replaceSelection();
710 }
711
712 mEditor->SendScintilla( QsciScintilla::SCI_ENDUNDOACTION );
713 clearSearchHighlights();
714}
715
716void QgsCodeEditorWidget::addSearchHighlights()
717{
718 const QString searchString = mLineEditFind->text();
719 if ( searchString.isEmpty() )
720 return;
721
722 const long originalStartPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETSTART );
723 const long originalEndPos = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETEND );
724 long startPos = 0;
725 long docEnd = mEditor->length();
726
727 updateHighlightController();
728
729 int searchFlags = 0;
730 const bool isRegEx = mRegexButton->isChecked();
731 const bool isCaseSensitive = mCaseSensitiveButton->isChecked();
732 const bool isWholeWordOnly = mWholeWordButton->isChecked();
733 if ( isRegEx )
734 searchFlags |= QsciScintilla::SCFIND_REGEXP | QsciScintilla::SCFIND_CXX11REGEX;
735 if ( isCaseSensitive )
736 searchFlags |= QsciScintilla::SCFIND_MATCHCASE;
737 if ( isWholeWordOnly )
738 searchFlags |= QsciScintilla::SCFIND_WHOLEWORD;
739 mEditor->SendScintilla( QsciScintilla::SCI_SETSEARCHFLAGS, searchFlags );
740 int matchCount = 0;
741 while ( true )
742 {
743 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, startPos, docEnd );
744 const int fstart = mEditor->SendScintilla( QsciScintilla::SCI_SEARCHINTARGET, searchString.length(), searchString.toLocal8Bit().constData() );
745 if ( fstart < 0 )
746 break;
747
748 const int matchLength = mEditor->SendScintilla( QsciScintilla::SCI_GETTARGETTEXT, 0, static_cast< void * >( nullptr ) );
749
750 if ( matchLength == 0 )
751 {
752 startPos += 1;
753 continue;
754 }
755
756 matchCount++;
757 startPos = fstart + matchLength;
758
759 mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR );
760 mEditor->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, fstart, matchLength );
761
762 int thisLine = 0;
763 int thisIndex = 0;
764 mEditor->lineIndexFromPosition( fstart, &thisLine, &thisIndex );
765 mHighlightController->addHighlight( QgsScrollBarHighlight( SearchMatch, thisLine, QColor( 0, 200, 0 ), QgsScrollBarHighlight::Priority::HighPriority ) );
766 }
767
768 mEditor->SendScintilla( QsciScintilla::SCI_SETTARGETRANGE, originalStartPos, originalEndPos );
769
770 searchMatchCountChanged( matchCount );
771}
772
773void QgsCodeEditorWidget::clearSearchHighlights()
774{
775 long docStart = 0;
776 long docEnd = mEditor->length();
777 mEditor->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, QgsCodeEditor::SEARCH_RESULT_INDICATOR );
778 mEditor->SendScintilla( QsciScintilla::SCI_INDICATORCLEARRANGE, docStart, docEnd - docStart );
779
780 mHighlightController->removeHighlights( SearchMatch );
781
782 searchMatchCountChanged( 0 );
783}
784
785bool QgsCodeEditorWidget::findText( bool forward, bool findFirst )
786{
787 const QString searchString = mLineEditFind->text();
788 if ( searchString.isEmpty() )
789 return false;
790
791 int lineFrom = 0;
792 int indexFrom = 0;
793 int lineTo = 0;
794 int indexTo = 0;
795 mEditor->getSelection( &lineFrom, &indexFrom, &lineTo, &indexTo );
796
797 int line = 0;
798 int index = 0;
799 if ( !findFirst )
800 {
801 mEditor->getCursorPosition( &line, &index );
802 }
803 if ( !forward )
804 {
805 line = lineFrom;
806 index = indexFrom;
807 }
808
809 const bool isRegEx = mRegexButton->isChecked();
810 const bool wrapAround = mWrapAroundButton->isChecked();
811 const bool isCaseSensitive = mCaseSensitiveButton->isChecked();
812 const bool isWholeWordOnly = mWholeWordButton->isChecked();
813
814 const bool found = mEditor->findFirst( searchString, isRegEx, isCaseSensitive, isWholeWordOnly, wrapAround, forward,
815 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}
842
@ 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:5627
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
void setFilePath(const QString &path)
Sets the widget's associated file path.
QgsScrollBarHighlightController * scrollbarHighlightController()
Returns the scrollbar highlight controller, which can be used to add highlights in the code editor sc...
bool isSearchBarVisible() const
Returns true if the search bar is visible.
void showEvent(QShowEvent *event) override
void addWarning(int lineNumber, const QString &warning)
Adds a warning message and indicator to the specified a lineNumber.
void setReplaceBarVisible(bool visible)
Sets whether the replace bar is visible.
void loadedExternalChanges()
Emitted when the widget loads in text from the associated file to bring in changes made externally to...
QgsMessageBar * messageBar()
Returns the message bar associated with the widget, to use for user feedback.
void triggerFind()
Triggers a find operation, using the default behavior.
bool openInExternalEditor(int line=-1, int column=-1)
Attempts to opens the script from the editor in an external text editor.
void hideSearchBar()
Hides the search bar.
void showSearchBar()
Shows the search bar.
void searchBarToggled(bool visible)
Emitted when the visibility of the search bar is changed.
bool save(const QString &path=QString())
Saves the code editor content into the file path.
void setSearchBarVisible(bool visible)
Sets whether the search bar is visible.
void filePathChanged(const QString &path)
Emitted when the widget's associated file path is changed.
bool shareOnGist(bool isPublic)
Shares the contents of the code editor on GitHub Gist.
QgsCodeEditor * editor()
Returns the wrapped code editor.
void clearWarnings()
Clears all warning messages from the editor.
QString filePath() const
Returns the widget's associated file path.
QgsCodeEditorWidget(QgsCodeEditor *editor, QgsMessageBar *messageBar=nullptr, QWidget *parent=nullptr)
Constructor for QgsCodeEditorWidget, wrapping the specified editor widget.
bool eventFilter(QObject *obj, QEvent *event) override
bool loadFile(const QString &path)
Loads the file at the specified path into the widget, replacing the code editor's content with that f...
void resizeEvent(QResizeEvent *event) override
~QgsCodeEditorWidget() override
A text editor based on QScintilla2.
static constexpr int SEARCH_RESULT_INDICATOR
Indicator index for search results.
void clearWarnings()
Clears all warning messages from the editor.
virtual Qgis::ScriptLanguage language() const
Returns the associated scripting language.
void addWarning(int lineNumber, const QString &warning)
Adds a warning message and indicator to the specified a lineNumber.
QLineEdit subclass with built in support for clearing the widget's value and handling custom null val...
void setShowSearchIcon(bool visible)
Define if a search icon shall be shown on the left of the image when no text is entered.
static json jsonFromVariant(const QVariant &v)
Converts a QVariant v to a json object.
A bar for displaying non-blocking messages to the user.
void pushCritical(const QString &title, const QString &message)
Pushes a critical warning message that must be manually dismissed by the user.
void pushWarning(const QString &title, const QString &message)
Pushes a warning message that must be manually dismissed by the user.
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
Base class for any widget that can be shown as a inline panel.
Adds highlights (colored markers) to a scrollbar.
Encapsulates the details of a highlight in a scrollbar, used alongside QgsScrollBarHighlightControlle...
@ HighestPriority
Highest priority, rendered above all other highlights.
This class is a composition of two QSettings instances:
Definition qgssettings.h:64
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...
#define QgsSetRequestInitiatorClass(request, _class)