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