QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
qgsjsoneditwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsjsoneditwidget.cpp
3 --------------------------------------
4 Date : 3.5.2021
5 Copyright : (C) 2021 Damiano Lombardi
6 Email : damiano at opengis dot ch
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 "qgsjsoneditwidget.h"
17
18#include <QAction>
19#include <QClipboard>
20#include <QDesktopServices>
21#include <QJsonArray>
22#include <QLabel>
23#include <QPushButton>
24#include <QString>
25#include <QToolTip>
26#include <QUrl>
27
28#include "moc_qgsjsoneditwidget.cpp"
29
30using namespace Qt::StringLiterals;
31
33 : QWidget( parent )
34 , mCopyValueAction( new QAction( tr( "Copy Value" ), this ) )
35 , mCopyKeyAction( new QAction( tr( "Copy Key" ), this ) )
36{
37 setupUi( this );
38
40
41 mCodeEditorJson->setReadOnly( true );
42 mCodeEditorJson->setHorizontalScrollBarPolicy( Qt::ScrollBarAsNeeded );
43 mCodeEditorJson->setVerticalScrollBarPolicy( Qt::ScrollBarAsNeeded );
44 mCodeEditorJson->indicatorDefine( QsciScintilla::PlainIndicator, SCINTILLA_UNDERLINE_INDICATOR_INDEX );
45 mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_SETINDICATORCURRENT, SCINTILLA_UNDERLINE_INDICATOR_INDEX );
46 mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_SETMOUSEDWELLTIME, 400 );
47
48 mTreeWidget->setContextMenuPolicy( Qt::ActionsContextMenu );
49 mTreeWidget->addAction( mCopyValueAction );
50 mTreeWidget->addAction( mCopyKeyAction );
51
52 connect( mTextToolButton, &QToolButton::clicked, this, &QgsJsonEditWidget::textToolButtonClicked );
53 connect( mTreeToolButton, &QToolButton::clicked, this, &QgsJsonEditWidget::treeToolButtonClicked );
54
55 connect( mCopyValueAction, &QAction::triggered, this, &QgsJsonEditWidget::copyValueActionTriggered );
56 connect( mCopyKeyAction, &QAction::triggered, this, &QgsJsonEditWidget::copyKeyActionTriggered );
57
58 connect( mCodeEditorJson, &QgsCodeEditorJson::textChanged, this, &QgsJsonEditWidget::codeEditorJsonTextChanged );
59 // Signal indicatorClicked is used because indicatorReleased has a bug in Scintilla and the keyboard modifier state
60 // parameter is not correct. Merge request was submittet to fix it: https://sourceforge.net/p/scintilla/code/merge-requests/26/
61 connect( mCodeEditorJson, &QsciScintilla::indicatorClicked, this, &QgsJsonEditWidget::codeEditorJsonIndicatorClicked );
62 connect( mCodeEditorJson, &QsciScintillaBase::SCN_DWELLSTART, this, &QgsJsonEditWidget::codeEditorJsonDwellStart );
63 connect( mCodeEditorJson, &QsciScintillaBase::SCN_DWELLEND, this, &QgsJsonEditWidget::codeEditorJsonDwellEnd );
64}
65
67{
68 return mCodeEditorJson;
69}
70
72{
73 mJsonText = jsonText;
74 mClickableLinkList.clear();
75
76 const QJsonDocument jsonDocument = QJsonDocument::fromJson( mJsonText.toUtf8() );
77
78 mCodeEditorJson->blockSignals( true );
79 if ( jsonDocument.isNull() )
80 {
81 mCodeEditorJson->setText( mJsonText );
82 }
83 else
84 {
85 switch ( mFormatJsonMode )
86 {
88 mCodeEditorJson->setText( jsonDocument.toJson( QJsonDocument::Indented ) );
89 break;
91 mCodeEditorJson->setText( jsonDocument.toJson( QJsonDocument::Compact ) );
92 break;
94 mCodeEditorJson->setText( mJsonText );
95 break;
96 }
97 }
98 mCodeEditorJson->blockSignals( false );
99
100 refreshTreeView( jsonDocument );
101}
102
104{
105 return mJsonText;
106}
107
109{
110 switch ( view )
111 {
112 case View::Text:
113 {
114 mStackedWidget->setCurrentWidget( mStackedWidgetPageText );
115 mTextToolButton->setChecked( true );
116 mTreeToolButton->setChecked( false );
117 }
118 break;
119 case View::Tree:
120 {
121 mStackedWidget->setCurrentWidget( mStackedWidgetPageTree );
122 mTreeToolButton->setChecked( true );
123 mTextToolButton->setChecked( false );
124 }
125 break;
126 }
127}
128
130{
131 mFormatJsonMode = formatJson;
132}
133
135{
136 mControlsWidget->setVisible( visible );
137}
138
139void QgsJsonEditWidget::textToolButtonClicked( bool checked )
140{
141 if ( checked )
143 else
145}
146
147void QgsJsonEditWidget::treeToolButtonClicked( bool checked )
148{
149 if ( checked )
151 else
153}
154
155void QgsJsonEditWidget::copyValueActionTriggered()
156{
157 if ( !mTreeWidget->currentItem() )
158 return;
159
160 const QJsonValue jsonValue = QJsonValue::fromVariant( mTreeWidget->currentItem()->data( static_cast<int>( TreeWidgetColumn::Value ), Qt::UserRole ) );
161
162 switch ( jsonValue.type() )
163 {
164 case QJsonValue::Null:
165 case QJsonValue::Bool:
166 case QJsonValue::Double:
167 case QJsonValue::Undefined:
168 QApplication::clipboard()->setText( mTreeWidget->currentItem()->text( static_cast<int>( TreeWidgetColumn::Value ) ) );
169 break;
170 case QJsonValue::String:
171 QApplication::clipboard()->setText( jsonValue.toString() );
172 break;
173 case QJsonValue::Array:
174 {
175 const QJsonDocument jsonDocument( jsonValue.toArray() );
176 QApplication::clipboard()->setText( jsonDocument.toJson() );
177 }
178 break;
179 case QJsonValue::Object:
180 {
181 const QJsonDocument jsonDocument( jsonValue.toObject() );
182 QApplication::clipboard()->setText( jsonDocument.toJson() );
183 }
184 break;
185 }
186}
187
188void QgsJsonEditWidget::copyKeyActionTriggered()
189{
190 if ( !mTreeWidget->currentItem() )
191 return;
192
193 QApplication::clipboard()->setText( mTreeWidget->currentItem()->text( static_cast<int>( TreeWidgetColumn::Key ) ) );
194}
195
196void QgsJsonEditWidget::codeEditorJsonTextChanged()
197{
198 mJsonText = mCodeEditorJson->text();
199 const QJsonDocument jsonDocument = QJsonDocument::fromJson( mJsonText.toUtf8() );
200 refreshTreeView( jsonDocument );
201}
202
203void QgsJsonEditWidget::codeEditorJsonIndicatorClicked( int line, int index, Qt::KeyboardModifiers state )
204{
205 if ( !state.testFlag( Qt::ControlModifier ) )
206 return;
207
208 const int position = mCodeEditorJson->positionFromLineIndex( line, index );
209 const int clickableLinkListIndex = mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_INDICATORVALUEAT, SCINTILLA_UNDERLINE_INDICATOR_INDEX, position );
210 if ( clickableLinkListIndex <= 0 )
211 return;
212
213 QDesktopServices::openUrl( mClickableLinkList.at( clickableLinkListIndex - 1 ) );
214}
215
216void QgsJsonEditWidget::codeEditorJsonDwellStart( int position, int x, int y )
217{
218 Q_UNUSED( x )
219 Q_UNUSED( y )
220
221 const int clickableLinkListIndex = mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_INDICATORVALUEAT, SCINTILLA_UNDERLINE_INDICATOR_INDEX, position );
222 if ( clickableLinkListIndex <= 0 )
223 return;
224
225 QToolTip::showText( QCursor::pos(), tr( "%1\nCTRL + click to follow link" ).arg( mClickableLinkList.at( clickableLinkListIndex - 1 ) ) );
226}
227
228void QgsJsonEditWidget::codeEditorJsonDwellEnd( int position, int x, int y )
229{
230 Q_UNUSED( position )
231 Q_UNUSED( x )
232 Q_UNUSED( y )
233 QToolTip::hideText();
234}
235
236void QgsJsonEditWidget::refreshTreeView( const QJsonDocument &jsonDocument )
237{
238 mTreeWidget->clear();
239
240 if ( jsonDocument.isNull() )
241 {
243 mTextToolButton->setDisabled( true );
244 mTreeToolButton->setDisabled( true );
245 mTreeToolButton->setToolTip( tr( "Invalid JSON, tree view not available" ) );
246 return;
247 }
248 else
249 {
250 mTextToolButton->setEnabled( true );
251 mTreeToolButton->setEnabled( true );
252 mTreeToolButton->setToolTip( tr( "Tree view" ) );
253 }
254
255 if ( jsonDocument.isObject() )
256 {
257 const QStringList keys = jsonDocument.object().keys();
258 for ( const QString &key : keys )
259 {
260 const QJsonValue jsonValue = jsonDocument.object().value( key );
261 QTreeWidgetItem *treeWidgetItem = new QTreeWidgetItem( mTreeWidget, QStringList() << key );
262 treeWidgetItem->setFont( 0, monospaceFont() );
263 refreshTreeViewItem( treeWidgetItem, jsonValue );
264 mTreeWidget->addTopLevelItem( treeWidgetItem );
265 mTreeWidget->expandItem( treeWidgetItem );
266 }
267 }
268 else if ( jsonDocument.isArray() )
269 {
270 const QJsonArray array = jsonDocument.array();
271 const auto arraySize = array.size();
272 // Limit the number of rows we display, otherwise for pathological cases
273 // like https://github.com/qgis/QGIS/pull/55847#issuecomment-1902077683
274 // a unbounded number of elements will just stall the GUI forever.
275 constexpr decltype( arraySize ) MAX_ELTS = 200;
276 // If there are too many elements, disable URL highighting as it
277 // performs very poorly.
278 if ( arraySize > MAX_ELTS )
279 mEnableUrlHighlighting = false;
280 for ( auto index = decltype( arraySize ) { 0 }; index < arraySize; index++ )
281 {
282 QTreeWidgetItem *treeWidgetItem = new QTreeWidgetItem( mTreeWidget, QStringList() << QString::number( index ) );
283 treeWidgetItem->setFont( 0, monospaceFont() );
284 if ( arraySize <= MAX_ELTS || ( index < MAX_ELTS / 2 || index + MAX_ELTS / 2 > arraySize ) )
285 {
286 refreshTreeViewItem( treeWidgetItem, array.at( index ) );
287 mTreeWidget->addTopLevelItem( treeWidgetItem );
288 mTreeWidget->expandItem( treeWidgetItem );
289 }
290 else if ( index == MAX_ELTS / 2 )
291 {
292 index = arraySize - MAX_ELTS / 2;
293 refreshTreeViewItem( treeWidgetItem, tr( "... truncated ..." ) );
294 mTreeWidget->addTopLevelItem( treeWidgetItem );
295 mTreeWidget->expandItem( treeWidgetItem );
296 }
297 }
298 }
299
300 mTreeWidget->resizeColumnToContents( static_cast<int>( TreeWidgetColumn::Key ) );
301}
302
303void QgsJsonEditWidget::refreshTreeViewItem( QTreeWidgetItem *treeWidgetItem, const QJsonValue &jsonValue )
304{
305 treeWidgetItem->setData( static_cast<int>( TreeWidgetColumn::Value ), Qt::UserRole, jsonValue.toVariant() );
306
307 switch ( jsonValue.type() )
308 {
309 case QJsonValue::Null:
310 refreshTreeViewItemValue( treeWidgetItem, u"null"_s, QgsCodeEditor::color( QgsCodeEditorColorScheme::ColorRole::Keyword ) );
311 break;
312 case QJsonValue::Bool:
313 refreshTreeViewItemValue( treeWidgetItem, jsonValue.toBool() ? u"true"_s : u"false"_s, QgsCodeEditor::color( QgsCodeEditorColorScheme::ColorRole::Keyword ) );
314 break;
315 case QJsonValue::Double:
316 refreshTreeViewItemValue( treeWidgetItem, QString::number( jsonValue.toDouble() ), QgsCodeEditor::color( QgsCodeEditorColorScheme::ColorRole::Number ) );
317 break;
318 case QJsonValue::String:
319 {
320 const QString jsonValueString = jsonValue.toString();
321 if ( !mEnableUrlHighlighting || QUrl( jsonValueString ).scheme().isEmpty() )
322 {
323 refreshTreeViewItemValue( treeWidgetItem, jsonValueString, QgsCodeEditor::color( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ) );
324 }
325 else
326 {
327 QLabel *label = new QLabel( QString( "<a href='%1'>%1</a>" ).arg( jsonValueString ) );
328 label->setOpenExternalLinks( true );
329 label->setFont( monospaceFont() );
330 mTreeWidget->setItemWidget( treeWidgetItem, static_cast<int>( TreeWidgetColumn::Value ), label );
331
332 mClickableLinkList.append( jsonValueString );
333 mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_SETINDICATORVALUE, static_cast<int>( mClickableLinkList.size() ) );
334 mCodeEditorJson->SendScintilla( QsciScintillaBase::SCI_INDICATORFILLRANGE, mCodeEditorJson->text().indexOf( jsonValueString ), jsonValueString.size() );
335 }
336 }
337 break;
338 case QJsonValue::Array:
339 {
340 const QJsonArray jsonArray = jsonValue.toArray();
341 const auto arraySize = jsonArray.size();
342 // Limit the number of rows we display, otherwise for pathological cases
343 // like https://github.com/qgis/QGIS/pull/55847#issuecomment-1902077683
344 // a unbounded number of elements will just stall the GUI forever.
345 constexpr decltype( arraySize ) MAX_ELTS = 200;
346 // If there are too many elements, disable URL highighting as it
347 // performs very poorly.
348 if ( arraySize > MAX_ELTS )
349 mEnableUrlHighlighting = false;
350 for ( auto index = decltype( arraySize ) { 0 }; index < arraySize; index++ )
351 {
352 QTreeWidgetItem *treeWidgetItemChild = new QTreeWidgetItem( treeWidgetItem, QStringList() << QString::number( index ) );
353 treeWidgetItemChild->setFont( 0, monospaceFont() );
354 if ( arraySize <= MAX_ELTS || ( index < MAX_ELTS / 2 || index + MAX_ELTS / 2 > arraySize ) )
355 {
356 refreshTreeViewItem( treeWidgetItemChild, jsonArray.at( index ) );
357 treeWidgetItem->addChild( treeWidgetItemChild );
358 treeWidgetItem->setExpanded( true );
359 }
360 else if ( index == MAX_ELTS / 2 )
361 {
362 index = arraySize - MAX_ELTS / 2;
363 refreshTreeViewItem( treeWidgetItemChild, tr( "... truncated ..." ) );
364 treeWidgetItem->addChild( treeWidgetItemChild );
365 treeWidgetItem->setExpanded( true );
366 }
367 }
368 }
369 break;
370 case QJsonValue::Object:
371 {
372 const QJsonObject jsonObject = jsonValue.toObject();
373 const QStringList keys = jsonObject.keys();
374 for ( const QString &key : keys )
375 {
376 QTreeWidgetItem *treeWidgetItemChild = new QTreeWidgetItem( treeWidgetItem, QStringList() << key );
377 treeWidgetItemChild->setFont( 0, monospaceFont() );
378 refreshTreeViewItem( treeWidgetItemChild, jsonObject.value( key ) );
379 treeWidgetItem->addChild( treeWidgetItemChild );
380 treeWidgetItem->setExpanded( true );
381 }
382 }
383 break;
384 case QJsonValue::Undefined:
385 refreshTreeViewItemValue( treeWidgetItem, u"Undefined value"_s, QgsCodeEditor::color( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ) );
386 break;
387 }
388}
389
390void QgsJsonEditWidget::refreshTreeViewItemValue( QTreeWidgetItem *treeWidgetItem, const QString &jsonValueString, const QColor &textColor )
391{
392 QLabel *label = new QLabel( jsonValueString );
393 label->setFont( monospaceFont() );
394
395 if ( textColor.isValid() )
396 label->setStyleSheet( u"color: %1;"_s.arg( textColor.name() ) );
397 mTreeWidget->setItemWidget( treeWidgetItem, static_cast<int>( TreeWidgetColumn::Value ), label );
398}
399
400QFont QgsJsonEditWidget::monospaceFont() const
401{
403 // use standard widget font size, not code editor font size
404 f.setPointSize( font().pointSize() );
405 return f;
406}
A JSON editor based on QScintilla2.
static QFont getMonospaceFont()
Returns the monospaced font to use for code editors.
static QColor color(QgsCodeEditorColorScheme::ColorRole role)
Returns the color to use in the editor for the specified role.
void setJsonText(const QString &jsonText)
Set the JSON text in the widget to jsonText.
QString jsonText() const
Returns the JSON text.
View
View mode, text or tree.
@ Tree
JSON data displayed as tree. Tree view is disabled for invalid JSON data.
@ Text
JSON data displayed as text.
QgsCodeEditorJson * jsonEditor()
Returns a reference to the JSON code editor used in the widget.
QgsJsonEditWidget(QWidget *parent=nullptr)
Constructor for QgsJsonEditWidget.
void setFormatJsonMode(FormatJson formatJson)
Set the formatJson mode.
void setControlsVisible(bool visible)
Set the visibility of controls to visible.
void setView(View view) const
Set the view mode.
FormatJson
Format mode in the text view.
@ Compact
JSON data formatted as a compact one line string.
@ Disabled
JSON data is not formatted.
@ Indented
JSON data formatted with regular indentation.