QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
qgsexpressiontreeview.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsexpressiontreeview.cpp
3 --------------------------------------
4 Date : march 2020 - quarantine day 9
5 Copyright : (C) 2020 by Denis Rouzaud
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
17
18#include "qgis.h"
19#include "qgsapplication.h"
21#include "qgsiconutils.h"
22#include "qgsrelationmanager.h"
23#include "qgssettings.h"
24#include "qgsvectorlayer.h"
25
26#include <QMenu>
27#include <QMessageBox>
28#include <QString>
29#include <QVersionNumber>
30
31#include "moc_qgsexpressiontreeview.cpp"
32
33using namespace Qt::StringLiterals;
34
36QString formatRelationHelp( const QgsRelation &relation )
37{
38 QString text = u"<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>"_s
39 .arg( QCoreApplication::translate( "relation_help", "relation %1" ).arg( relation.name() ), QObject::tr( "Inserts the relation ID for the relation named '%1'." ).arg( relation.name() ) );
40
41 text += u"<h4>%1</h4><div class=\"description\"><pre>%2</pre></div>"_s
42 .arg( QObject::tr( "Current value" ), relation.id() );
43
44 return text;
45}
46
47
49QString formatLayerHelp( const QgsMapLayer *layer )
50{
51 QString text = u"<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>"_s
52 .arg( QCoreApplication::translate( "layer_help", "map layer %1" ).arg( layer->name() ), QObject::tr( "Inserts the layer ID for the layer named '%1'." ).arg( layer->name() ) );
53
54 text += u"<h4>%1</h4><div class=\"description\"><pre>%2</pre></div>"_s
55 .arg( QObject::tr( "Current value" ), layer->id() );
56
57 return text;
58}
59
61QString formatRecentExpressionHelp( const QString &label, const QString &expression )
62{
63 QString text = u"<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>"_s
64 .arg( QCoreApplication::translate( "recent_expression_help", "expression %1" ).arg( label ), QCoreApplication::translate( "recent_expression_help", "Recently used expression." ) );
65
66 text += u"<h4>%1</h4><div class=\"description\"><pre>%2</pre></div>"_s
67 .arg( QObject::tr( "Expression" ), expression );
68
69 return text;
70}
71
73QString formatUserExpressionHelp( const QString &label, const QString &expression, const QString &description )
74{
75 QString text = u"<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>"_s
76 .arg( QCoreApplication::translate( "user_expression_help", "expression %1" ).arg( label ), description );
77
78 text += u"<h4>%1</h4><div class=\"description\"><pre>%2</pre></div>"_s
79 .arg( QObject::tr( "Expression" ), expression );
80
81 return text;
82}
83
85QString formatVariableHelp( const QString &variable, const QString &description, bool showValue, const QVariant &value )
86{
87 QString text = u"<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>"_s
88 .arg( QCoreApplication::translate( "variable_help", "variable %1" ).arg( variable ), description );
89
90 if ( showValue )
91 {
92 QString valueString = !value.isValid()
93 ? QCoreApplication::translate( "variable_help", "not set" )
94 : u"<pre>%1</pre>"_s.arg( QgsExpression::formatPreviewString( value ) );
95
96 text += u"<h4>%1</h4><div class=\"description\"><p>%2</p></div>"_s
97 .arg( QObject::tr( "Current value" ), valueString );
98 }
99
100 return text;
101}
102
103
104// ****************************
105// ****************************
106// QgsExpressionTreeView
107// ****************************
108
109
111 : QTreeView( parent )
112 , mProject( QgsProject::instance() )
113{
114 connect( this, &QTreeView::doubleClicked, this, &QgsExpressionTreeView::onDoubleClicked );
115
116 mModel = std::make_unique<QStandardItemModel>();
117 mProxyModel = std::make_unique<QgsExpressionItemSearchProxy>();
118 mProxyModel->setDynamicSortFilter( true );
119 mProxyModel->setSourceModel( mModel.get() );
120 setModel( mProxyModel.get() );
121 setSortingEnabled( true );
122 sortByColumn( 0, Qt::AscendingOrder );
123
124 setSelectionMode( QAbstractItemView::SelectionMode::SingleSelection );
125
126 setContextMenuPolicy( Qt::CustomContextMenu );
127 connect( this, &QWidget::customContextMenuRequested, this, &QgsExpressionTreeView::showContextMenu );
128 connect( selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsExpressionTreeView::currentItemChanged );
129
130 updateFunctionTree();
132
133 // select the first item in the function list
134 // in order to avoid a blank help widget
135 QModelIndex firstItem = mProxyModel->index( 0, 0, QModelIndex() );
136 setCurrentIndex( firstItem );
137}
138
140{
141 mLayer = layer;
142
143 //TODO - remove existing layer scope from context
144
145 if ( mLayer )
146 mExpressionContext << QgsExpressionContextUtils::layerScope( mLayer );
147
149}
150
152{
153 mExpressionContext = context;
154 updateFunctionTree();
156 loadRecent( mRecentKey );
158}
159
161{
162 mMenuProvider = provider;
163}
164
166{
167 updateFunctionTree();
169 loadRecent( mRecentKey );
171}
172
174{
175 QModelIndex idx = mProxyModel->mapToSource( currentIndex() );
176 QgsExpressionItem *item = static_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
177 return item;
178}
179
180QStandardItemModel *QgsExpressionTreeView::model()
181{
182 return mModel.get();
183}
184
186{
187 return mProject;
188}
189
191{
192 mProject = project;
193 updateFunctionTree();
194}
195
196
197void QgsExpressionTreeView::setSearchText( const QString &text )
198{
199 mProxyModel->setFilterString( text );
200 if ( text.isEmpty() )
201 {
202 collapseAll();
203 }
204 else
205 {
206 expandAll();
207 QModelIndex index = mProxyModel->index( 0, 0 );
208 if ( mProxyModel->hasChildren( index ) )
209 {
210 QModelIndex child = mProxyModel->index( 0, 0, index );
211 selectionModel()->setCurrentIndex( child, QItemSelectionModel::ClearAndSelect );
212 }
213 }
214}
215
216void QgsExpressionTreeView::onDoubleClicked( const QModelIndex &index )
217{
218 QModelIndex idx = mProxyModel->mapToSource( index );
219 QgsExpressionItem *item = static_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
220 if ( !item )
221 return;
222
223 // Don't handle the double-click if we are on a header node.
224 if ( item->getItemType() == QgsExpressionItem::Header )
225 return;
226
228}
229
230void QgsExpressionTreeView::showContextMenu( QPoint pt )
231{
232 QModelIndex idx = indexAt( pt );
233 idx = mProxyModel->mapToSource( idx );
234 QgsExpressionItem *item = static_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
235 if ( !item )
236 return;
237
238 if ( !mMenuProvider )
239 return;
240
241 QMenu *menu = mMenuProvider->createContextMenu( item );
242
243 if ( menu )
244 menu->popup( mapToGlobal( pt ) );
245}
246
247void QgsExpressionTreeView::currentItemChanged( const QModelIndex &index, const QModelIndex & )
248{
249 // Get the item
250 QModelIndex idx = mProxyModel->mapToSource( index );
251 QgsExpressionItem *item = static_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
252 if ( !item )
253 return;
254
255 emit currentExpressionItemChanged( item );
256}
257
258void QgsExpressionTreeView::updateFunctionTree()
259{
260 mModel->clear();
261 mExpressionGroups.clear();
262
263 //list of pairs where the first is the name and the second is the expression value when adding it
264 static const QList<QPair<QString, QString>> operators = QList<QPair<QString, QString>>()
265 << QPair<QString, QString>( u"+"_s, u" + "_s )
266 << QPair<QString, QString>( u"-"_s, u" - "_s )
267 << QPair<QString, QString>( u"*"_s, u" * "_s )
268 << QPair<QString, QString>( u"/"_s, u" / "_s )
269 << QPair<QString, QString>( u"//"_s, u" // "_s )
270 << QPair<QString, QString>( u"%"_s, u" % "_s )
271 << QPair<QString, QString>( u"^"_s, u" ^ "_s )
272 << QPair<QString, QString>( u"="_s, u" = "_s )
273 << QPair<QString, QString>( u"~"_s, u" ~ "_s )
274 << QPair<QString, QString>( u">"_s, u" > "_s )
275 << QPair<QString, QString>( u"<"_s, u" < "_s )
276 << QPair<QString, QString>( u"<>"_s, u" <> "_s )
277 << QPair<QString, QString>( u"<="_s, u" <= "_s )
278 << QPair<QString, QString>( u">="_s, u" >= "_s )
279 << QPair<QString, QString>( u"[]"_s, u"[]"_s )
280 << QPair<QString, QString>( u"||"_s, u" || "_s )
281 << QPair<QString, QString>( u"BETWEEN"_s, u" BETWEEN "_s )
282 << QPair<QString, QString>( u"NOT BETWEEN"_s, u" NOT BETWEEN "_s )
283 << QPair<QString, QString>( u"IN"_s, u" IN "_s )
284 << QPair<QString, QString>( u"LIKE"_s, u" LIKE "_s )
285 << QPair<QString, QString>( u"ILIKE"_s, u" ILIKE "_s )
286 << QPair<QString, QString>( u"IS"_s, u" IS "_s )
287 << QPair<QString, QString>( u"IS NOT"_s, u" IS NOT "_s )
288 << QPair<QString, QString>( u"OR"_s, u" OR "_s )
289 << QPair<QString, QString>( u"AND"_s, u" AND "_s )
290 << QPair<QString, QString>( u"NOT"_s, u" NOT "_s );
291 for ( const auto &name : operators )
292 {
293 registerItem( u"Operators"_s, name.first, name.second, QString(), QgsExpressionItem::ExpressionNode, false, -1, QIcon(), QgsExpression::tags( name.first ) );
294 }
295
296 QString casestring = u"CASE WHEN condition THEN result END"_s;
297 registerItem( u"Conditionals"_s, u"CASE"_s, casestring, QString(), QgsExpressionItem::ExpressionNode, false, -1, QIcon(), QgsExpression::tags( "CASE" ) );
298
299 // use -1 as sort order here -- NULL should always show before the field list
300 registerItem( u"Fields and Values"_s, u"NULL"_s, u"NULL"_s, QString(), QgsExpressionItem::ExpressionNode, false, -1 );
301
302 // Load the functions from the QgsExpression class
303 int count = QgsExpression::functionCount();
304 for ( int i = 0; i < count; i++ )
305 {
306 QgsExpressionFunction *func = QgsExpression::Functions()[i];
307 QString name = func->name();
308 if ( name.startsWith( '_' ) ) // do not display private functions
309 continue;
310 if ( func->isDeprecated() ) // don't show deprecated functions
311 continue;
312 if ( func->isContextual() )
313 {
314 //don't show contextual functions by default - it's up the the QgsExpressionContext
315 //object to provide them if supported
316 continue;
317 }
318 if ( func->params() != 0 )
319 name += '(';
320 else if ( !name.startsWith( '$' ) )
321 name += "()"_L1;
322 // this is where the functions are being registered, including functions under "Custom"
323 registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) );
324 }
325
326 // load relation names
327 loadRelations();
328
329 // load layer IDs
330 loadLayers();
331
332 loadExpressionContext();
333}
334
335QgsExpressionItem *QgsExpressionTreeView::registerItem( const QString &group, const QString &label, const QString &expressionText, const QString &helpText, QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, const QIcon &icon, const QStringList &tags, const QString &name )
336{
337 QgsExpressionItem *item = new QgsExpressionItem( label, expressionText, helpText, type );
338 item->setData( label, Qt::UserRole );
339 item->setData( sortOrder, QgsExpressionItem::CUSTOM_SORT_ROLE );
340 item->setData( tags, QgsExpressionItem::SEARCH_TAGS_ROLE );
341 item->setData( name, QgsExpressionItem::ITEM_NAME_ROLE );
342 item->setIcon( icon );
343
344 // Look up the group and insert the new function.
345 if ( mExpressionGroups.contains( group ) )
346 {
347 QgsExpressionItem *groupNode = mExpressionGroups.value( group );
348 groupNode->appendRow( item );
349 }
350 else
351 {
352 // If the group doesn't exist yet we make it first.
353 QgsExpressionItem *newgroupNode = new QgsExpressionItem( QgsExpression::group( group ), QString(), QgsExpressionItem::Header );
354 newgroupNode->setData( group, Qt::UserRole );
355 //Recent group should always be last group
356 newgroupNode->setData( group.startsWith( "Recent ("_L1 ) ? 2 : 1, QgsExpressionItem::CUSTOM_SORT_ROLE );
357 newgroupNode->appendRow( item );
358 newgroupNode->setBackground( QBrush( QColor( 150, 150, 150, 150 ) ) );
359 mModel->appendRow( newgroupNode );
360 mExpressionGroups.insert( group, newgroupNode );
361 }
362
363 if ( highlightedItem )
364 {
365 //insert a copy as a top level item
366 QgsExpressionItem *topLevelItem = new QgsExpressionItem( label, expressionText, helpText, type );
367 topLevelItem->setData( label, Qt::UserRole );
368 item->setData( 0, QgsExpressionItem::CUSTOM_SORT_ROLE );
369 QFont font = topLevelItem->font();
370 font.setBold( true );
371 topLevelItem->setFont( font );
372 mModel->appendRow( topLevelItem );
373 }
374 return item;
375}
376
377void QgsExpressionTreeView::registerItemForAllGroups( const QStringList &groups, const QString &label, const QString &expressionText, const QString &helpText, QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, const QStringList &tags )
378{
379 const auto constGroups = groups;
380 for ( const QString &group : constGroups )
381 {
382 registerItem( group, label, expressionText, helpText, type, highlightedItem, sortOrder, QIcon(), tags );
383 }
384}
385
386void QgsExpressionTreeView::loadExpressionContext()
387{
388 QStringList variableNames = mExpressionContext.filteredVariableNames();
389 const auto constVariableNames = variableNames;
390 for ( const QString &variable : constVariableNames )
391 {
392 registerItem( u"Variables"_s, variable, " @" + variable + ' ', formatVariableHelp( variable, mExpressionContext.description( variable ), true, mExpressionContext.variable( variable ) ), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedVariable( variable ) );
393 }
394
395 // Load the functions from the expression context
396 QStringList contextFunctions = mExpressionContext.functionNames();
397 const auto constContextFunctions = contextFunctions;
398 for ( const QString &functionName : constContextFunctions )
399 {
400 QgsExpressionFunction *func = mExpressionContext.function( functionName );
401 QString name = func->name();
402 if ( name.startsWith( '_' ) ) // do not display private functions
403 continue;
404 if ( func->params() != 0 )
405 name += '(';
406 registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) );
407 }
408}
409
410void QgsExpressionTreeView::loadLayers()
411{
412 if ( !mProject )
413 return;
414
415 QMap<QString, QgsMapLayer *> layers = mProject->mapLayers();
416 QMap<QString, QgsMapLayer *>::const_iterator layerIt = layers.constBegin();
417 for ( ; layerIt != layers.constEnd(); ++layerIt )
418 {
419 QIcon icon = QgsIconUtils::iconForLayer( layerIt.value() );
420 QgsExpressionItem *parentItem = registerItem( u"Map Layers"_s, layerIt.value()->name(), u"'%1'"_s.arg( layerIt.key() ), formatLayerHelp( layerIt.value() ), QgsExpressionItem::ExpressionNode, false, 99, icon );
421 loadLayerFields( qobject_cast<QgsVectorLayer *>( layerIt.value() ), parentItem );
422 }
423}
424
425void QgsExpressionTreeView::loadLayerFields( QgsVectorLayer *layer, QgsExpressionItem *parentItem )
426{
427 if ( !layer )
428 return;
429
430 const QgsFields fields = layer->fields();
431 for ( int fieldIdx = 0; fieldIdx < fields.count(); ++fieldIdx )
432 {
433 const QgsField field = fields.at( fieldIdx );
434 QIcon icon = fields.iconForField( fieldIdx );
435 const QString label { field.displayNameWithAlias() };
436 QgsExpressionItem *item = new QgsExpressionItem( label, " '" + field.name() + "' ", QString(), QgsExpressionItem::Field );
437 item->setData( label, Qt::UserRole );
438 item->setData( 99, QgsExpressionItem::CUSTOM_SORT_ROLE );
439 item->setData( QStringList(), QgsExpressionItem::SEARCH_TAGS_ROLE );
440 item->setData( field.name(), QgsExpressionItem::ITEM_NAME_ROLE );
441 item->setData( layer->id(), QgsExpressionItem::LAYER_ID_ROLE );
442 item->setIcon( icon );
443 parentItem->appendRow( item );
444 }
445}
446
448{
449 for ( int i = 0; i < fields.count(); ++i )
450 {
451 const QgsField field = fields.at( i );
452 QIcon icon = fields.iconForField( i );
453 registerItem( u"Fields and Values"_s, field.displayNameWithAlias(), " \"" + field.name() + "\" ", QString(), QgsExpressionItem::Field, false, i, icon, QStringList(), field.name() );
454 }
455}
456
458{
459 // Cleanup
460 if ( mExpressionGroups.contains( u"Fields and Values"_s ) )
461 {
462 QgsExpressionItem *node = mExpressionGroups.value( u"Fields and Values"_s );
463 node->removeRows( 0, node->rowCount() );
464 // Re-add NULL and feature variables
465 // use -1 as sort order here -- NULL and feature variables should always show before the field list
466 registerItem( u"Fields and Values"_s, u"NULL"_s, u"NULL"_s, QString(), QgsExpressionItem::ExpressionNode, false, -1 );
467 }
468
469 if ( mLayer )
470 {
471 // Add feature variables to record and attributes group (and highlighted items)
472
473 const QString currentFeatureHelp = formatVariableHelp( u"feature"_s, QgsExpression::variableHelpText( u"feature"_s ), false, QVariant() );
474 const QString currentFeatureIdHelp = formatVariableHelp( u"id"_s, QgsExpression::variableHelpText( u"id"_s ), false, QVariant() );
475 const QString currentGeometryHelp = formatVariableHelp( u"geometry"_s, QgsExpression::variableHelpText( u"geometry"_s ), false, QVariant() );
476
477 registerItem( u"Fields and Values"_s, u"feature"_s, u"@feature"_s, currentFeatureHelp, QgsExpressionItem::ExpressionNode, false, -1 );
478 registerItem( u"Fields and Values"_s, u"id"_s, u"@id"_s, currentFeatureIdHelp, QgsExpressionItem::ExpressionNode, false, -1 );
479 registerItem( u"Fields and Values"_s, u"geometry"_s, u"@geometry"_s, currentGeometryHelp, QgsExpressionItem::ExpressionNode, false, -1 );
480
481 registerItem( u"Variables"_s, u"feature"_s, u"@feature"_s, currentFeatureHelp, QgsExpressionItem::ExpressionNode );
482 registerItem( u"Variables"_s, u"id"_s, u"@id"_s, currentFeatureIdHelp, QgsExpressionItem::ExpressionNode );
483 registerItem( u"Variables"_s, u"geometry"_s, u"@geometry"_s, currentGeometryHelp, QgsExpressionItem::ExpressionNode, false );
484
485 registerItem( u"Record and Attributes"_s, u"feature"_s, u"@feature"_s, currentFeatureHelp, QgsExpressionItem::ExpressionNode, true, -1 );
486 registerItem( u"Record and Attributes"_s, u"id"_s, u"@id"_s, currentFeatureIdHelp, QgsExpressionItem::ExpressionNode, true, -1 );
487 registerItem( u"Record and Attributes"_s, u"geometry"_s, u"@geometry"_s, currentGeometryHelp, QgsExpressionItem::ExpressionNode, true, -1 );
488 }
489
490 // this can happen if fields are manually set
491 if ( !mLayer )
492 return;
493
494 const QgsFields &fields = mLayer->fields();
495
496 loadFieldNames( fields );
497}
498
499void QgsExpressionTreeView::loadRelations()
500{
501 if ( !mProject )
502 return;
503
504 QMap<QString, QgsRelation> relations = mProject->relationManager()->relations();
505 QMap<QString, QgsRelation>::const_iterator relIt = relations.constBegin();
506 for ( ; relIt != relations.constEnd(); ++relIt )
507 {
508 registerItemForAllGroups( QStringList() << tr( "Relations" ), relIt->name(), u"'%1'"_s.arg( relIt->id() ), formatRelationHelp( relIt.value() ) );
509 }
510}
511
512void QgsExpressionTreeView::loadRecent( const QString &collection )
513{
514 mRecentKey = collection;
515 QString name = tr( "Recent (%1)" ).arg( collection );
516 if ( mExpressionGroups.contains( name ) )
517 {
518 QgsExpressionItem *node = mExpressionGroups.value( name );
519 node->removeRows( 0, node->rowCount() );
520 }
521
522 QgsSettings settings;
523 const QString location = u"/expressions/recent/%1"_s.arg( collection );
524 const QStringList expressions = settings.value( location ).toStringList();
525 int i = 0;
526 for ( const QString &expression : expressions )
527 {
528 QString help = formatRecentExpressionHelp( expression, expression );
529 QString label = expression;
530 label.replace( '\n', ' ' );
531 registerItem( name, label, expression, help, QgsExpressionItem::ExpressionNode, false, i );
532 i++;
533 }
534}
535
536void QgsExpressionTreeView::saveToRecent( const QString &expressionText, const QString &collection )
537{
538 QgsSettings settings;
539 QString location = u"/expressions/recent/%1"_s.arg( collection );
540 QStringList expressions = settings.value( location ).toStringList();
541 expressions.removeAll( expressionText );
542
543 expressions.prepend( expressionText );
544
545 while ( expressions.count() > 20 )
546 {
547 expressions.pop_back();
548 }
549
550 settings.setValue( location, expressions );
551 loadRecent( collection );
552}
553
554void QgsExpressionTreeView::saveToUserExpressions( const QString &label, const QString &expression, const QString &helpText )
555{
556 QgsSettings settings;
557 const QString location = u"user"_s;
558 settings.beginGroup( location, QgsSettings::Section::Expressions );
559 settings.beginGroup( label );
560 settings.setValue( u"expression"_s, expression );
561 settings.setValue( u"helpText"_s, helpText );
563 // Scroll
564 const QModelIndexList idxs { mModel->match( mModel->index( 0, 0 ), Qt::DisplayRole, label, 1, Qt::MatchFlag::MatchRecursive ) };
565 if ( !idxs.isEmpty() )
566 {
567 scrollTo( idxs.first() );
568 }
569}
570
572{
573 QgsSettings settings;
574 settings.remove( u"user/%1"_s.arg( label ), QgsSettings::Section::Expressions );
576}
577
578// this is potentially very slow if there are thousands of user expressions, every time entire cleanup and load
580{
581 // Cleanup
582 if ( mExpressionGroups.contains( u"UserGroup"_s ) )
583 {
584 QgsExpressionItem *node = mExpressionGroups.value( u"UserGroup"_s );
585 node->removeRows( 0, node->rowCount() );
586 }
587
588 QgsSettings settings;
589 const QString location = u"user"_s;
590 settings.beginGroup( location, QgsSettings::Section::Expressions );
591 QString helpText;
592 QString expression;
593 int i = 0;
594 mUserExpressionLabels = settings.childGroups();
595 for ( const auto &label : std::as_const( mUserExpressionLabels ) )
596 {
597 settings.beginGroup( label );
598 expression = settings.value( u"expression"_s ).toString();
599 helpText = formatUserExpressionHelp( label, expression, settings.value( u"helpText"_s ).toString() );
600 registerItem( u"UserGroup"_s, label, expression, helpText, QgsExpressionItem::ExpressionNode, false, i++ );
601 settings.endGroup();
602 }
603}
604
606{
607 return mUserExpressionLabels;
608}
609
611{
612 const QString group = u"user"_s;
613 QgsSettings settings;
614 QJsonArray exportList;
615 QJsonObject exportObject {
616 { "qgis_version", Qgis::version() },
617 { "exported_at", QDateTime::currentDateTime().toString( Qt::ISODate ) },
618 { "author", QgsApplication::userFullName() },
619 { "expressions", exportList }
620 };
621
623
624 mUserExpressionLabels = settings.childGroups();
625
626 for ( const QString &label : std::as_const( mUserExpressionLabels ) )
627 {
628 settings.beginGroup( label );
629
630 const QString expression = settings.value( u"expression"_s ).toString();
631 const QString helpText = settings.value( u"helpText"_s ).toString();
632 const QJsonObject expressionObject {
633 { "name", label },
634 { "type", "expression" },
635 { "expression", expression },
636 { "group", group },
637 { "description", helpText }
638 };
639 exportList.push_back( expressionObject );
640
641 settings.endGroup();
642 }
643
644 exportObject[u"expressions"_s] = exportList;
645 QJsonDocument exportJson = QJsonDocument( exportObject );
646
647 return exportJson;
648}
649
650void QgsExpressionTreeView::loadExpressionsFromJson( const QJsonDocument &expressionsDocument )
651{
652 // if the root of the json document is not an object, it means it's a wrong file
653 if ( !expressionsDocument.isObject() )
654 return;
655
656 QJsonObject expressionsObject = expressionsDocument.object();
657
658 // validate json for manadatory fields
659 if ( !expressionsObject[u"qgis_version"_s].isString()
660 || !expressionsObject[u"exported_at"_s].isString()
661 || !expressionsObject[u"author"_s].isString()
662 || !expressionsObject[u"expressions"_s].isArray() )
663 return;
664
665 // validate versions
666 QVersionNumber qgisJsonVersion = QVersionNumber::fromString( expressionsObject[u"qgis_version"_s].toString() );
667 QVersionNumber qgisVersion = QVersionNumber::fromString( Qgis::version() );
668
669 // if the expressions are from newer version of QGIS, we ask the user to confirm
670 // they want to proceed
671 if ( qgisJsonVersion > qgisVersion )
672 {
673 QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
674 switch ( QMessageBox::question( this, tr( "QGIS Version Mismatch" ), tr( "The imported expressions are from newer version of QGIS (%1) "
675 "and some of the expression might not work the current version (%2). "
676 "Are you sure you want to continue?" )
677 .arg( qgisJsonVersion.toString(), qgisVersion.toString() ),
678 buttons ) )
679 {
680 case QMessageBox::No:
681 return;
682
683 case QMessageBox::Yes:
684 break;
685
686 default:
687 break;
688 }
689 }
690
691 // we store the number of
692 QStringList skippedExpressionLabels;
693 bool isApplyToAll = false;
694 bool isOkToOverwrite = false;
695
696 QgsSettings settings;
697 settings.beginGroup( u"user"_s, QgsSettings::Section::Expressions );
698 mUserExpressionLabels = settings.childGroups();
699
700 const QJsonArray expressions = expressionsObject[u"expressions"_s].toArray();
701 for ( const QJsonValue &&expressionValue : expressions )
702 {
703 // validate the type of the array element, can be anything
704 if ( !expressionValue.isObject() )
705 {
706 // try to stringify and put and indicator what happened
707 skippedExpressionLabels.append( expressionValue.toString() );
708 continue;
709 }
710
711 QJsonObject expressionObj = expressionValue.toObject();
712
713 // make sure the required keys are the correct types
714 if ( !expressionObj[u"name"_s].isString()
715 || !expressionObj[u"type"_s].isString()
716 || !expressionObj[u"expression"_s].isString()
717 || !expressionObj[u"group"_s].isString()
718 || !expressionObj[u"description"_s].isString() )
719 {
720 // try to stringify and put an indicator what happened. Try to stringify the name, if fails, go with the expression.
721 if ( !expressionObj[u"name"_s].toString().isEmpty() )
722 skippedExpressionLabels.append( expressionObj[u"name"_s].toString() );
723 else
724 skippedExpressionLabels.append( expressionObj[u"expression"_s].toString() );
725
726 continue;
727 }
728
729 // we want to import only items of type expression for now
730 if ( expressionObj[u"type"_s].toString() != "expression"_L1 )
731 {
732 skippedExpressionLabels.append( expressionObj[u"name"_s].toString() );
733 continue;
734 }
735
736 // we want to import only items of type expression for now
737 if ( expressionObj[u"group"_s].toString() != "user"_L1 )
738 {
739 skippedExpressionLabels.append( expressionObj[u"name"_s].toString() );
740 continue;
741 }
742
743 const QString label = expressionObj[u"name"_s].toString();
744 const QString expression = expressionObj[u"expression"_s].toString();
745 const QString helpText = expressionObj[u"description"_s].toString();
746
747 // make sure they have valid name
748 if ( label.contains( "\\"_L1 ) || label.contains( '/' ) )
749 {
750 skippedExpressionLabels.append( expressionObj[u"name"_s].toString() );
751 continue;
752 }
753
754 settings.beginGroup( label );
755 const QString oldExpression = settings.value( u"expression"_s ).toString();
756 settings.endGroup();
757
758 // TODO would be nice to skip the cases when labels and expressions match
759 if ( mUserExpressionLabels.contains( label ) && expression != oldExpression )
760 {
761 if ( !isApplyToAll )
762 showMessageBoxConfirmExpressionOverwrite( isApplyToAll, isOkToOverwrite, label, oldExpression, expression );
763
764 if ( isOkToOverwrite )
765 saveToUserExpressions( label, expression, helpText );
766 else
767 {
768 skippedExpressionLabels.append( label );
769 continue;
770 }
771 }
772 else
773 {
774 saveToUserExpressions( label, expression, helpText );
775 }
776 }
777
779
780 if ( !skippedExpressionLabels.isEmpty() )
781 {
782 QStringList skippedExpressionLabelsQuoted;
783 skippedExpressionLabelsQuoted.reserve( skippedExpressionLabels.size() );
784 for ( const QString &skippedExpressionLabel : skippedExpressionLabels )
785 skippedExpressionLabelsQuoted.append( u"'%1'"_s.arg( skippedExpressionLabel ) );
786
787 QMessageBox::information( this, tr( "Skipped Expression Imports" ), u"%1\n%2"_s.arg( tr( "The following expressions have been skipped:" ), skippedExpressionLabelsQuoted.join( ", "_L1 ) ) );
788 }
789}
790
791const QList<QgsExpressionItem *> QgsExpressionTreeView::findExpressions( const QString &label )
792{
793 QList<QgsExpressionItem *> result;
794 const QList<QStandardItem *> found { mModel->findItems( label, Qt::MatchFlag::MatchRecursive ) };
795 result.reserve( found.size() );
796 std::transform( found.begin(), found.end(), std::back_inserter( result ), []( QStandardItem *item ) -> QgsExpressionItem * { return static_cast<QgsExpressionItem *>( item ); } );
797 return result;
798}
799
800void QgsExpressionTreeView::showMessageBoxConfirmExpressionOverwrite(
801 bool &isApplyToAll,
802 bool &isOkToOverwrite,
803 const QString &label,
804 const QString &oldExpression,
805 const QString &newExpression
806)
807{
808 QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll;
809 switch ( QMessageBox::question( this, tr( "Expression Overwrite" ), tr( "The expression with label '%1' was already defined."
810 "The old expression \"%2\" will be overwritten by \"%3\"."
811 "Are you sure you want to overwrite the expression?" )
812 .arg( label, oldExpression, newExpression ),
813 buttons ) )
814 {
815 case QMessageBox::NoToAll:
816 isApplyToAll = true;
817 isOkToOverwrite = false;
818 break;
819
820 case QMessageBox::No:
821 isApplyToAll = false;
822 isOkToOverwrite = false;
823 break;
824
825 case QMessageBox::YesToAll:
826 isApplyToAll = true;
827 isOkToOverwrite = true;
828 break;
829
830 case QMessageBox::Yes:
831 isApplyToAll = false;
832 isOkToOverwrite = true;
833 break;
834
835 default:
836 break;
837 }
838}
839
840
841// ****************************
842// ****************************
843// QgsExpressionItemSearchProxy
844// ****************************
845
846
848{
849 setFilterCaseSensitivity( Qt::CaseInsensitive );
850}
851
852bool QgsExpressionItemSearchProxy::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
853{
854 QModelIndex index = sourceModel()->index( source_row, 0, source_parent );
855 const QgsExpressionItem::ItemType itemType = QgsExpressionItem::ItemType( sourceModel()->data( index, QgsExpressionItem::ITEM_TYPE_ROLE ).toInt() );
856
857 if ( itemType == QgsExpressionItem::Header )
858 {
859 // show header if any child item matches
860 int count = sourceModel()->rowCount( index );
861 bool matchchild = false;
862 for ( int i = 0; i < count; ++i )
863 {
864 if ( filterAcceptsRow( i, index ) )
865 {
866 matchchild = true;
867 break;
868 }
869 }
870 return matchchild;
871 }
872
873 // check match of item label or tags
874 const QString name = sourceModel()->data( index, Qt::DisplayRole ).toString();
875 if ( name.contains( mFilterString, Qt::CaseInsensitive ) )
876 {
877 return true;
878 }
879
880 const QStringList tags = sourceModel()->data( index, QgsExpressionItem::SEARCH_TAGS_ROLE ).toStringList();
881 return std::any_of( tags.begin(), tags.end(), [this]( const QString &tag ) {
882 return tag.contains( mFilterString, Qt::CaseInsensitive );
883 } );
884}
885
887{
888 mFilterString = string;
889 invalidate();
890}
891
892bool QgsExpressionItemSearchProxy::lessThan( const QModelIndex &left, const QModelIndex &right ) const
893{
894 int leftSort = sourceModel()->data( left, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
895 int rightSort = sourceModel()->data( right, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
896 if ( leftSort != rightSort )
897 return leftSort < rightSort;
898
899 QString leftString = sourceModel()->data( left, Qt::DisplayRole ).toString();
900 QString rightString = sourceModel()->data( right, Qt::DisplayRole ).toString();
901
902 //ignore $ prefixes when sorting
903 if ( leftString.startsWith( '$' ) )
904 leftString = leftString.mid( 1 );
905 if ( rightString.startsWith( '$' ) )
906 rightString = rightString.mid( 1 );
907
908 return QString::localeAwareCompare( leftString, rightString ) < 0;
909}
static QString version()
Version string.
Definition qgis.cpp:682
static QString userFullName()
Returns the user's operating system login account full display name.
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
bool isContextual() const
Returns whether the function is only available if provided by a QgsExpressionContext object.
int params() const
The number of parameters this function takes.
QStringList groups() const
Returns a list of the groups the function belongs to.
virtual bool isDeprecated() const
Returns true if the function is deprecated and should not be presented as a valid option to users in ...
QString name() const
The name of the function.
const QString helpText() const
The help text for the function.
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
void setFilterString(const QString &string)
Sets the search filter string.
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
An expression item that can be used in the QgsExpressionBuilderWidget tree.
static const int LAYER_ID_ROLE
Layer ID role.
QString getExpressionText() const
static const int SEARCH_TAGS_ROLE
Search tags role.
static const int ITEM_TYPE_ROLE
Item type role.
static const int CUSTOM_SORT_ROLE
Custom sort order role.
QgsExpressionItem::ItemType getItemType() const
Gets the type of expression item, e.g., header, field, ExpressionNode.
static const int ITEM_NAME_ROLE
Item name role.
Implementation of this interface can be implemented to allow QgsExpressionTreeView instance to provid...
QgsProject * project()
Returns the project currently associated with the widget.
void refresh()
Refreshes the content of the tree.
void setProject(QgsProject *project)
Sets the project currently associated with the widget.
void saveToUserExpressions(const QString &label, const QString &expression, const QString &helpText)
Stores the user expression with given label and helpText.
QgsExpressionItem * currentItem() const
Returns the current item or a nullptr.
void setLayer(QgsVectorLayer *layer)
Sets layer in order to get the fields and values.
void setMenuProvider(MenuProvider *provider)
Sets the menu provider.
QStringList userExpressionLabels() const
Returns the user expression labels.
void expressionItemDoubleClicked(const QString &text)
Emitted when a expression item is double clicked.
void loadRecent(const QString &collection=u"generic"_s)
Loads the recent expressions from the given collection.
void currentExpressionItemChanged(QgsExpressionItem *item)
Emitter when the current expression item changed.
QJsonDocument exportUserExpressions()
Create the expressions JSON document storing all the user expressions to be exported.
QgsExpressionTreeView(QWidget *parent=nullptr)
Constructor.
void loadExpressionsFromJson(const QJsonDocument &expressionsDocument)
Load and permanently store the expressions from the expressions JSON document.
void saveToRecent(const QString &expressionText, const QString &collection="generic")
Adds the current expression to the given collection.
void setSearchText(const QString &text)
Sets the text to filter the expression tree.
void setExpressionContext(const QgsExpressionContext &context)
Sets the expression context for the tree view.
const QList< QgsExpressionItem * > findExpressions(const QString &label)
Returns the list of expression items matching a label.
void removeFromUserExpressions(const QString &label)
Removes the expression label from the user stored expressions.
void loadUserExpressions()
Loads the user expressions.
Q_DECL_DEPRECATED QStandardItemModel * model()
Returns a pointer to the dialog's function item model.
void loadFieldNames(const QgsFields &fields)
This allows loading fields without specifying a layer.
static const QList< QgsExpressionFunction * > & Functions()
static int functionCount()
Returns the number of functions defined in the parser.
static QString variableHelpText(const QString &variableName)
Returns the help text for a specified variable.
static QString formatPreviewString(const QVariant &value, bool htmlOutput=true, int maximumPreviewLength=60)
Formats an expression result for friendly display to the user.
static QStringList tags(const QString &name)
Returns a string list of search tags for a specified function.
static QString group(const QString &group)
Returns the translated name for a function group.
Encapsulate a field in an attribute table or data source.
Definition qgsfield.h:56
QString name
Definition qgsfield.h:65
QString displayNameWithAlias() const
Returns the name to use when displaying this field and adds the alias in parenthesis if it is defined...
Definition qgsfield.cpp:109
Container of fields for a vector layer.
Definition qgsfields.h:46
int count
Definition qgsfields.h:50
QgsField at(int i) const
Returns the field at particular index (must be in range 0..N-1).
QIcon iconForField(int fieldIdx, bool considerOrigin=false) const
Returns an icon corresponding to a field index, based on the field's type and source.
static QIcon iconForLayer(const QgsMapLayer *layer)
Returns the icon corresponding to a specified map layer.
Base class for all map layer types.
Definition qgsmaplayer.h:83
QString name
Definition qgsmaplayer.h:87
QString id
Definition qgsmaplayer.h:86
Encapsulates a QGIS project, including sets of map layers and their styles, layouts,...
Definition qgsproject.h:112
Represents a relationship between two vector layers.
Definition qgsrelation.h:42
QString name
Definition qgsrelation.h:52
QString id
Definition qgsrelation.h:45
Stores settings for use within QGIS.
Definition qgssettings.h:68
QStringList childGroups(Qgis::SettingsOrigin origin=Qgis::SettingsOrigin::Any) const
Returns a list of all key top-level groups that contain keys that can be read using the QSettings obj...
void endGroup()
Resets the group to what it was before the corresponding beginGroup() call.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void beginGroup(const QString &prefix, QgsSettings::Section section=QgsSettings::NoSection)
Appends prefix to the current group.
void remove(const QString &key, QgsSettings::Section section=QgsSettings::NoSection)
Removes the setting key and any sub-settings of key in a section.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
Represents a vector layer which manages a vector based dataset.
QString formatLayerHelp(const QgsMapLayer *layer)
Returns a HTML formatted string for use as a layer item help.
QString formatUserExpressionHelp(const QString &label, const QString &expression, const QString &description)
Returns a HTML formatted string for use as a user expression item help.
QString formatVariableHelp(const QString &variable, const QString &description, bool showValue, const QVariant &value)
Returns a HTML formatted string for use as a variable item help.
QString formatRecentExpressionHelp(const QString &label, const QString &expression)
Returns a HTML formatted string for use as a recent expression item help.
QString formatRelationHelp(const QgsRelation &relation)
Returns a HTML formatted string for use as a relation item help.