18 #include <QTextStream> 
   20 #include <QInputDialog> 
   22 #include <QGraphicsOpacityEffect> 
   23 #include <QPropertyAnimation> 
   24 #include <QMessageBox> 
   25 #include <QVersionNumber> 
   27 #include <QJsonDocument> 
   28 #include <QJsonObject> 
   30 #include <QFileDialog> 
   61     if ( fieldIndex != -1 )
 
   79   connect( btnRun, &QToolButton::pressed, 
this, &QgsExpressionBuilderWidget::btnRun_pressed );
 
   80   connect( btnNewFile, &QPushButton::pressed, 
this, &QgsExpressionBuilderWidget::btnNewFile_pressed );
 
   81   connect( btnRemoveFile, &QPushButton::pressed, 
this, &QgsExpressionBuilderWidget::btnRemoveFile_pressed );
 
   82   connect( cmbFileNames, &QListWidget::currentItemChanged, 
this, &QgsExpressionBuilderWidget::cmbFileNames_currentItemChanged );
 
   83   connect( txtExpressionString, &QgsCodeEditorExpression::textChanged, 
this, &QgsExpressionBuilderWidget::txtExpressionString_textChanged );
 
   84   connect( txtPython, &QgsCodeEditorPython::textChanged, 
this, &QgsExpressionBuilderWidget::txtPython_textChanged );
 
   85   connect( txtSearchEditValues, &QgsFilterLineEdit::textChanged, 
this, &QgsExpressionBuilderWidget::txtSearchEditValues_textChanged );
 
   86   connect( mValuesListView, &QListView::doubleClicked, 
this, &QgsExpressionBuilderWidget::mValuesListView_doubleClicked );
 
   90   connect( btnImportExpressions, &QPushButton::pressed, 
this, &QgsExpressionBuilderWidget::importUserExpressions_pressed );
 
   91   connect( btnExportExpressions, &QPushButton::pressed, 
this, &QgsExpressionBuilderWidget::exportUserExpressions_pressed );
 
   92   connect( btnClearEditor, &QPushButton::pressed, txtExpressionString, &QgsCodeEditorExpression::clear );
 
  105   mExpressionTreeMenuProvider = 
new ExpressionTreeMenuProvider( 
this );
 
  106   mExpressionTreeView->setMenuProvider( mExpressionTreeMenuProvider );
 
  108   txtHelpText->setOpenExternalLinks( 
true );
 
  109   mValueGroupBox->hide();
 
  126   const auto pushButtons { mOperatorsGroupBox->findChildren<QPushButton *>() };
 
  127   for ( QPushButton *button : pushButtons )
 
  129     connect( button, &QAbstractButton::pressed, 
this, &QgsExpressionBuilderWidget::operatorButtonClicked );
 
  132   txtSearchEdit->setShowSearchIcon( 
true );
 
  133   txtSearchEdit->setPlaceholderText( tr( 
"Search…" ) );
 
  135   mValuesModel = qgis::make_unique<QStandardItemModel>();
 
  136   mProxyValues = qgis::make_unique<QSortFilterProxyModel>();
 
  137   mProxyValues->setSourceModel( mValuesModel.get() );
 
  138   mValuesListView->setModel( mProxyValues.get() );
 
  139   txtSearchEditValues->setShowSearchIcon( 
true );
 
  140   txtSearchEditValues->setPlaceholderText( tr( 
"Search…" ) );
 
  142   editorSplit->setSizes( QList<int>( {175, 300} ) );
 
  144   functionsplit->setCollapsible( 0, 
false );
 
  145   connect( mShowHelpButton, &QPushButton::clicked, 
this, [ = ]()
 
  147     functionsplit->setSizes( QList<int>( {mOperationListGroup->width() - mHelpAndValuesWidget->minimumWidth(),
 
  148                                           mHelpAndValuesWidget->minimumWidth()
 
  150     mShowHelpButton->setEnabled( 
false );
 
  152   connect( functionsplit, &QSplitter::splitterMoved, 
this, [ = ]( 
int, 
int )
 
  154     mShowHelpButton->setEnabled( functionsplit->sizes().at( 1 ) == 0 );
 
  158   splitter->restoreState( settings.value( QStringLiteral( 
"Windows/QgsExpressionBuilderWidget/splitter" ) ).toByteArray() );
 
  159   editorSplit->restoreState( settings.value( QStringLiteral( 
"Windows/QgsExpressionBuilderWidget/editorsplitter" ) ).toByteArray() );
 
  160   functionsplit->restoreState( settings.value( QStringLiteral( 
"Windows/QgsExpressionBuilderWidget/functionsplitter" ) ).toByteArray() );
 
  161   mShowHelpButton->setEnabled( functionsplit->sizes().at( 1 ) == 0 );
 
  163   txtExpressionString->setFoldingVisible( 
false );
 
  169     btnRemoveFile->setEnabled( cmbFileNames->count() > 0 );
 
  176   txtExpressionString->setWrapMode( QsciScintilla::WrapWord );
 
  177   lblAutoSave->clear();
 
  184 #if defined(QSCINTILLA_VERSION) && QSCINTILLA_VERSION >= 0x20a00 
  191   txtExpressionString->setIndicatorForegroundColor( QColor( Qt::red ), -1 );
 
  192   txtExpressionString->setIndicatorHoverForegroundColor( QColor( Qt::red ), -1 );
 
  193   txtExpressionString->setIndicatorOutlineColor( QColor( Qt::red ), -1 );
 
  196   txtExpressionString->indicatorDefine( QgsCodeEditor::HiddenIndicator, FUNCTION_MARKER_ID );
 
  197   txtExpressionString->setIndicatorForegroundColor( QColor( Qt::blue ), FUNCTION_MARKER_ID );
 
  198   txtExpressionString->setIndicatorHoverForegroundColor( QColor( Qt::blue ), FUNCTION_MARKER_ID );
 
  199   txtExpressionString->setIndicatorHoverStyle( QgsCodeEditor::DotsIndicator, FUNCTION_MARKER_ID );
 
  201   connect( txtExpressionString, &QgsCodeEditorExpression::indicatorClicked, 
this, &QgsExpressionBuilderWidget::indicatorClicked );
 
  202   txtExpressionString->setAutoCompletionCaseSensitivity( 
false );
 
  203   txtExpressionString->setAutoCompletionSource( QsciScintilla::AcsAPIs );
 
  204   txtExpressionString->setCallTipsVisible( 0 );
 
  207   mFunctionBuilderHelp->setMarginVisible( 
false );
 
  208   mFunctionBuilderHelp->setEdgeMode( QsciScintilla::EdgeNone );
 
  209   mFunctionBuilderHelp->setEdgeColumn( 0 );
 
  210   mFunctionBuilderHelp->setReadOnly( 
true );
 
  211   mFunctionBuilderHelp->setText( tr( 
"\"\"\"Define a new function using the @qgsfunction decorator.\n\ 
  213  The function accepts the following parameters\n\ 
  215  : param [any]: Define any parameters you want to pass to your function before\n\ 
  216  the following arguments.\n\ 
  217  : param feature: The current feature\n\ 
  218  : param parent: The QgsExpression object\n\ 
  219  : param context: If there is an argument called ``context`` found at the last\n\ 
  220                    position, this variable will contain a ``QgsExpressionContext``\n\ 
  221                    object, that gives access to various additional information like\n\ 
  222                    expression variables. E.g. ``context.variable( 'layer_id' )``\n\ 
  223  : returns: The result of the expression.\n\ 
  227  The @qgsfunction decorator accepts the following arguments:\n\ 
  230  : param args: Defines the number of arguments. With ``args = 'auto'`` the number of\n\ 
  231                arguments will automatically be extracted from the signature.\n\ 
  232                With ``args = -1``, any number of arguments are accepted.\n\ 
  233  : param group: The name of the group under which this expression function will\n\ 
  235  : param handlesnull: Set this to True if your function has custom handling for NULL values.\n\ 
  236                      If False, the result will always be NULL as soon as any parameter is NULL.\n\ 
  237                      Defaults to False.\n\ 
  238  : param usesgeometry : Set this to True if your function requires access to\n\ 
  239                         feature.geometry(). Defaults to False.\n\ 
  240  : param referenced_columns: An array of attribute names that are required to run\n\ 
  241                              this function. Defaults to [QgsFeatureRequest.ALL_ATTRIBUTES].\n\ 
  249   settings.
setValue( QStringLiteral( 
"Windows/QgsExpressionBuilderWidget/splitter" ), splitter->saveState() );
 
  250   settings.
setValue( QStringLiteral( 
"Windows/QgsExpressionBuilderWidget/editorsplitter" ), editorSplit->saveState() );
 
  251   settings.
setValue( QStringLiteral( 
"Windows/QgsExpressionBuilderWidget/functionsplitter" ), functionsplit->saveState() );
 
  252   delete mExpressionTreeMenuProvider;
 
  260     mExpressionTreeView->loadRecent( recentCollection );
 
  263     mExpressionTreeView->loadUserExpressions();
 
  268   init( context, recentCollection, flags );
 
  274   init( context, recentCollection, flags );
 
  275   mExpressionTreeView->loadFieldNames( fields );
 
  282   mExpressionTreeView->setLayer( mLayer );
 
  283   mExpressionPreviewWidget->setLayer( mLayer );
 
  291     txtExpressionString->setFields( mLayer->
fields() );
 
  300 void QgsExpressionBuilderWidget::expressionTreeItemChanged( 
QgsExpressionItem *item )
 
  302   txtSearchEditValues->clear();
 
  310     mValuesModel->clear();
 
  313     cbxValuesInUse->setChecked( 
false );
 
  315   mValueGroupBox->setVisible( isField );
 
  317   mShowHelpButton->setText( isField ? tr( 
"Show Values" ) : tr( 
"Show Help" ) );
 
  320   QString help = loadFunctionHelp( item );
 
  321   txtHelpText->setText( help );
 
  323   bool isUserExpression = item->parent() && item->parent()->text() == mUserExpressionsGroupName;
 
  325   btnRemoveExpression->setEnabled( isUserExpression );
 
  326   btnEditExpression->setEnabled( isUserExpression );
 
  329 void QgsExpressionBuilderWidget::btnRun_pressed()
 
  331   if ( !cmbFileNames->currentItem() )
 
  334   QString file = cmbFileNames->currentItem()->text();
 
  336   runPythonCode( txtPython->text() );
 
  339 void QgsExpressionBuilderWidget::runPythonCode( 
const QString &code )
 
  343     QString pythontext = code;
 
  346   mExpressionTreeView->refresh();
 
  351   QDir myDir( mFunctionsPath );
 
  352   if ( !myDir.exists() )
 
  354     myDir.mkpath( mFunctionsPath );
 
  357   if ( !fileName.endsWith( QLatin1String( 
".py" ) ) )
 
  359     fileName.append( 
".py" );
 
  362   fileName = mFunctionsPath + QDir::separator() + fileName;
 
  363   QFile myFile( fileName );
 
  364   if ( myFile.open( QIODevice::WriteOnly | QFile::Truncate ) )
 
  366     QTextStream myFileStream( &myFile );
 
  367     myFileStream << txtPython->text() << endl;
 
  374   mFunctionsPath = path;
 
  376   dir.setNameFilters( QStringList() << QStringLiteral( 
"*.py" ) );
 
  377   QStringList files = dir.entryList( QDir::Files );
 
  378   cmbFileNames->clear();
 
  379   const auto constFiles = files;
 
  380   for ( 
const QString &name : constFiles )
 
  382     QFileInfo info( mFunctionsPath + QDir::separator() + name );
 
  383     if ( info.baseName() == QLatin1String( 
"__init__" ) ) 
continue;
 
  384     QListWidgetItem *item = 
new QListWidgetItem( 
QgsApplication::getThemeIcon( QStringLiteral( 
"console/iconTabEditorConsole.svg" ) ), info.baseName() );
 
  385     cmbFileNames->addItem( item );
 
  387   if ( !cmbFileNames->currentItem() )
 
  389     cmbFileNames->setCurrentRow( 0 );
 
  392   if ( cmbFileNames->count() == 0 )
 
  396     txtPython->setText( QStringLiteral( 
"'''\n#Sample custom function file\n " 
  397                                         "(uncomment to use and customize or Add button to create a new file) \n%1 \n '''" ).arg( txtPython->text() ) );
 
  404   QList<QListWidgetItem *> items = cmbFileNames->findItems( fileName, Qt::MatchExactly );
 
  405   if ( !items.isEmpty() )
 
  408   QListWidgetItem *item = 
new QListWidgetItem( 
QgsApplication::getThemeIcon( QStringLiteral( 
"console/iconTabEditorConsole.svg" ) ), fileName );
 
  409   cmbFileNames->insertItem( 0, item );
 
  410   cmbFileNames->setCurrentRow( 0 );
 
  414   txtPython->setText( templatetxt );
 
  418 void QgsExpressionBuilderWidget::btnNewFile_pressed()
 
  421   QString text = QInputDialog::getText( 
this, tr( 
"New File" ),
 
  422                                         tr( 
"New file name:" ), QLineEdit::Normal,
 
  424   if ( ok && !text.isEmpty() )
 
  430 void QgsExpressionBuilderWidget::btnRemoveFile_pressed()
 
  432   if ( QMessageBox::question( 
this, tr( 
"Remove File" ),
 
  433                               tr( 
"Are you sure you want to remove current functions file?" ),
 
  434                               QMessageBox::Yes | QMessageBox::No, QMessageBox::No ) == QMessageBox::No )
 
  437   int currentRow = cmbFileNames->currentRow();
 
  438   QString fileName = cmbFileNames->currentItem()->text();
 
  439   if ( QFile::remove( mFunctionsPath + QDir::separator() + fileName.append( 
".py" ) ) )
 
  442       QListWidgetItem *itemToRemove = 
whileBlocking( cmbFileNames )->takeItem( currentRow );
 
  446     if ( cmbFileNames->count() > 0 )
 
  448       cmbFileNames->setCurrentRow( currentRow > 0 ? currentRow - 1 : 0 );
 
  449       loadCodeFromFile( mFunctionsPath + QDir::separator() + cmbFileNames->currentItem()->text() );
 
  453       btnRemoveFile->setEnabled( 
false );
 
  459     QMessageBox::warning( 
this, tr( 
"Remove file" ), tr( 
"Failed to remove function file '%1'." ).arg( fileName ) );
 
  463 void QgsExpressionBuilderWidget::cmbFileNames_currentItemChanged( QListWidgetItem *item, QListWidgetItem *lastitem )
 
  467     QString filename = lastitem->text();
 
  470   QString path = mFunctionsPath + QDir::separator() + item->text();
 
  476   if ( !path.endsWith( QLatin1String( 
".py" ) ) )
 
  477     path.append( 
".py" );
 
  479   txtPython->loadScript( path );
 
  484   txtPython->setText( code );
 
  487 void QgsExpressionBuilderWidget::insertExpressionText( 
const QString &text )
 
  490   txtExpressionString->insertText( text );
 
  491   txtExpressionString->setFocus();
 
  496   Q_UNUSED( fieldValues )
 
  500 void QgsExpressionBuilderWidget::fillFieldValues( 
const QString &fieldName, 
int countLimit, 
bool forceUsedValues )
 
  512   if ( fieldIndex < 0 )
 
  519   if ( cbxValuesInUse->isVisible() && !cbxValuesInUse->isChecked() && !forceUsedValues )
 
  523     values = 
formatter->availableValues( setup.
config(), countLimit, fieldFormatterContext );
 
  527     values = qgis::setToList( mLayer->
uniqueValues( fieldIndex, countLimit ) );
 
  529   std::sort( values.begin(), values.end() );
 
  531   mValuesModel->clear();
 
  532   for ( 
const QVariant &value : qgis::as_const( values ) )
 
  535     if ( value.isNull() )
 
  536       strValue = QStringLiteral( 
"NULL" );
 
  537     else if ( value.type() == QVariant::Int || value.type() == QVariant::Double || value.type() == QVariant::LongLong )
 
  538       strValue = value.toString();
 
  540       strValue = 
'\'' + value.toString().replace( 
'\'', QLatin1String( 
"''" ) ) + 
'\'';
 
  542     QString representedValue = 
formatter->representValue( mLayer, fieldIndex, setup.
config(), QVariant(), value );
 
  543     if ( representedValue != value.toString() )
 
  544       representedValue = representedValue + QStringLiteral( 
" [" ) + strValue + 
']';
 
  546     QStandardItem *item = 
new QStandardItem( representedValue );
 
  547     item->setData( strValue );
 
  548     mValuesModel->appendRow( item );
 
  559   return QStringLiteral( 
"<head><style>" ) + helpStylesheet() + QStringLiteral( 
"</style></head><body>" ) + helpContents + QStringLiteral( 
"</body>" );
 
  567   return mExpressionValid;
 
  572   mExpressionTreeView->saveToRecent( 
expressionText(), collection );
 
  577   mExpressionTreeView->loadRecent( collection );
 
  582   return mExpressionTreeView;
 
  588   mExpressionTreeView->loadUserExpressions();
 
  593   mExpressionTreeView->saveToUserExpressions( label, expression, helpText );
 
  598   mExpressionTreeView->removeFromUserExpressions( label );
 
  604   mExpressionPreviewWidget->setGeomCalculator( da );
 
  609   return txtExpressionString->text();
 
  614   txtExpressionString->setText( expression );
 
  619   return lblExpected->text();
 
  624   lblExpected->setText( expected );
 
  625   mExpectedOutputFrame->setVisible( !expected.isNull() );
 
  630   mExpressionContext = context;
 
  631   txtExpressionString->setExpressionContext( mExpressionContext );
 
  632   mExpressionTreeView->setExpressionContext( context );
 
  633   mExpressionPreviewWidget->setExpressionContext( context );
 
  636 void QgsExpressionBuilderWidget::txtExpressionString_textChanged()
 
  640   btnClearEditor->setEnabled( ! txtExpressionString->text().isEmpty() );
 
  641   btnSaveExpression->setEnabled( 
false );
 
  643   mExpressionPreviewWidget->setExpressionText( text );
 
  648   return mExpressionPreviewWidget->parserError();
 
  653   return mExpressionPreviewWidget->evalError();
 
  659   return mExpressionTreeView->model();
 
  671   mExpressionTreeView->setProject( 
project );
 
  676   QWidget::showEvent( e );
 
  677   txtExpressionString->setFocus();
 
  680 void QgsExpressionBuilderWidget::createErrorMarkers( QList<QgsExpression::ParserError> errors )
 
  685     int errorFirstLine = error.firstLine - 1 ;
 
  686     int errorFirstColumn = error.firstColumn - 1;
 
  687     int errorLastColumn = error.lastColumn - 1;
 
  688     int errorLastLine = error.lastLine - 1;
 
  694       errorFirstLine = errorLastLine;
 
  695       errorFirstColumn = errorLastColumn - 1;
 
  697     txtExpressionString->fillIndicatorRange( errorFirstLine,
 
  700         errorLastColumn, error.errorType );
 
  704 void QgsExpressionBuilderWidget::createMarkers( 
const QgsExpressionNode *inNode )
 
  708     case QgsExpressionNode::NodeType::ntFunction:
 
  711       txtExpressionString->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, FUNCTION_MARKER_ID );
 
  712       txtExpressionString->SendScintilla( QsciScintilla::SCI_SETINDICATORVALUE, node->
fnIndex() );
 
  715       int start_pos = txtExpressionString->positionFromLineIndex( inNode->
parserFirstLine - 1, start );
 
  716       txtExpressionString->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, start_pos, end - start );
 
  719         const QList< QgsExpressionNode * > nodeList = node->
args()->
list();
 
  727     case QgsExpressionNode::NodeType::ntLiteral:
 
  731     case QgsExpressionNode::NodeType::ntUnaryOperator:
 
  734       createMarkers( node->
operand() );
 
  737     case QgsExpressionNode::NodeType::ntBinaryOperator:
 
  740       createMarkers( node->
opLeft() );
 
  741       createMarkers( node->
opRight() );
 
  744     case QgsExpressionNode::NodeType::ntColumnRef:
 
  748     case QgsExpressionNode::NodeType::ntInOperator:
 
  753         const QList< QgsExpressionNode * > nodeList = node->
list()->
list();
 
  761     case QgsExpressionNode::NodeType::ntCondition:
 
  766         createMarkers( cond->whenExp() );
 
  767         createMarkers( cond->thenExp() );
 
  771         createMarkers( node->
elseExp() );
 
  775     case QgsExpressionNode::NodeType::ntIndexOperator:
 
  782 void QgsExpressionBuilderWidget::clearFunctionMarkers()
 
  784   int lastLine = txtExpressionString->lines() - 1;
 
  785   txtExpressionString->clearIndicatorRange( 0, 0, lastLine, txtExpressionString->text( lastLine ).length() - 1, FUNCTION_MARKER_ID );
 
  788 void QgsExpressionBuilderWidget::clearErrors()
 
  790   int lastLine = txtExpressionString->lines() - 1;
 
  799 void QgsExpressionBuilderWidget::txtSearchEditValues_textChanged()
 
  801   mProxyValues->setFilterCaseSensitivity( Qt::CaseInsensitive );
 
  802   mProxyValues->setFilterWildcard( txtSearchEditValues->text() );
 
  805 void QgsExpressionBuilderWidget::mValuesListView_doubleClicked( 
const QModelIndex &index )
 
  808   txtExpressionString->insertText( 
' ' + index.data( Qt::UserRole + 1 ).toString() + 
' ' );
 
  809   txtExpressionString->setFocus();
 
  812 void QgsExpressionBuilderWidget::operatorButtonClicked()
 
  814   QPushButton *button = qobject_cast<QPushButton *>( sender() );
 
  817   txtExpressionString->insertText( 
' ' + button->text() + 
' ' );
 
  818   txtExpressionString->setFocus();
 
  826   if ( !mLayer || !item )
 
  829   mValueGroupBox->show();
 
  830   fillFieldValues( item->text(), 10 );
 
  838   if ( !mLayer || !item )
 
  841   mValueGroupBox->show();
 
  842   fillFieldValues( item->text(), -1 );
 
  850   if ( !mLayer || !item )
 
  853   mValueGroupBox->show();
 
  854   fillFieldValues( item->text(), 10, 
true );
 
  862   if ( !mLayer || !item )
 
  865   mValueGroupBox->show();
 
  866   fillFieldValues( item->text(), -1, 
true );
 
  869 void QgsExpressionBuilderWidget::txtPython_textChanged()
 
  871   lblAutoSave->setText( tr( 
"Saving…" ) );
 
  881   if ( tabWidget->currentIndex() != 1 )
 
  884   QListWidgetItem *item = cmbFileNames->currentItem();
 
  888   QString file = item->text();
 
  890   lblAutoSave->setText( QStringLiteral( 
"Saved" ) );
 
  891   QGraphicsOpacityEffect *effect = 
new QGraphicsOpacityEffect();
 
  892   lblAutoSave->setGraphicsEffect( effect );
 
  893   QPropertyAnimation *anim = 
new QPropertyAnimation( effect, 
"opacity" );
 
  894   anim->setDuration( 2000 );
 
  895   anim->setStartValue( 1.0 );
 
  896   anim->setEndValue( 0.0 );
 
  897   anim->setEasingCurve( QEasingCurve::OutQuad );
 
  898   anim->start( QAbstractAnimation::DeleteWhenStopped );
 
  905   if ( dlg.exec() == QDialog::DialogCode::Accepted )
 
  907     mExpressionTreeView->saveToUserExpressions( dlg.label(), dlg.expression(), dlg.helpText() );
 
  921        ( item->parent() && item->parent()->text() != mUserExpressionsGroupName ) )
 
  925   QString helpText = settings.
value( QStringLiteral( 
"user/%1/helpText" ).arg( item->text() ), 
"", QgsSettings::Section::Expressions ).toString();
 
  928   if ( dlg.exec() == QDialog::DialogCode::Accepted )
 
  931     if ( dlg.label() != item->text() )
 
  933       mExpressionTreeView->removeFromUserExpressions( item->text() );
 
  936     mExpressionTreeView->saveToUserExpressions( dlg.label(), dlg.expression(), dlg.helpText() );
 
  951        ( item->parent() && item->parent()->text() != mUserExpressionsGroupName ) )
 
  954   if ( QMessageBox::Yes == QMessageBox::question( 
this, tr( 
"Remove Stored Expression" ),
 
  955        tr( 
"Do you really want to remove stored expressions '%1'?" ).arg( item->text() ),
 
  956        QMessageBox::Yes | QMessageBox::No ) )
 
  958     mExpressionTreeView->removeFromUserExpressions( item->text() );
 
  963 void QgsExpressionBuilderWidget::exportUserExpressions_pressed()
 
  966   QString lastSaveDir = settings.
value( QStringLiteral( 
"lastExportExpressionsDir" ), QDir::homePath(), 
QgsSettings::App ).toString();
 
  967   QString saveFileName = QFileDialog::getSaveFileName(
 
  969                            tr( 
"Export User Expressions" ),
 
  971                            tr( 
"User expressions" ) + 
" (*.json)" );
 
  973   if ( saveFileName.isEmpty() )
 
  976   QFileInfo saveFileInfo( saveFileName );
 
  978   if ( saveFileInfo.suffix().isEmpty() )
 
  980     QString saveFileNameWithSuffix = saveFileName.append( 
".json" );
 
  981     saveFileInfo = QFileInfo( saveFileNameWithSuffix );
 
  986   QJsonDocument exportJson = mExpressionTreeView->exportUserExpressions();
 
  987   QFile jsonFile( saveFileName );
 
  989   if ( !jsonFile.open( QFile::WriteOnly | QIODevice::Truncate ) )
 
  990     QMessageBox::warning( 
this, tr( 
"Export user expressions" ), tr( 
"Error while creating the expressions file." ) );
 
  992   if ( ! jsonFile.write( exportJson.toJson() ) )
 
  993     QMessageBox::warning( 
this, tr( 
"Export user expressions" ), tr( 
"Error while creating the expressions file." ) );
 
  998 void QgsExpressionBuilderWidget::importUserExpressions_pressed()
 
 1001   QString lastImportDir = settings.
value( QStringLiteral( 
"lastImportExpressionsDir" ), QDir::homePath(), 
QgsSettings::App ).toString();
 
 1002   QString loadFileName = QFileDialog::getOpenFileName(
 
 1004                            tr( 
"Import User Expressions" ),
 
 1006                            tr( 
"User expressions" ) + 
" (*.json)" );
 
 1008   if ( loadFileName.isEmpty() )
 
 1011   QFileInfo loadFileInfo( loadFileName );
 
 1015   QFile jsonFile( loadFileName );
 
 1017   if ( !jsonFile.open( QFile::ReadOnly ) )
 
 1018     QMessageBox::warning( 
this, tr( 
"Import User Expressions" ), tr( 
"Error while reading the expressions file." ) );
 
 1020   QTextStream jsonStream( &jsonFile );
 
 1021   QString jsonString = jsonFile.readAll();
 
 1024   QJsonDocument importJson = QJsonDocument::fromJson( jsonString.toUtf8() );
 
 1026   if ( importJson.isNull() )
 
 1028     QMessageBox::warning( 
this, tr( 
"Import User Expressions" ), tr( 
"Error while reading the expressions file." ) );
 
 1032   mExpressionTreeView->loadExpressionsFromJson( importJson );
 
 1038   return mExpressionTreeView->findExpressions( label );
 
 1041 void QgsExpressionBuilderWidget::indicatorClicked( 
int line, 
int index, Qt::KeyboardModifiers state )
 
 1043   if ( state & Qt::ControlModifier )
 
 1045     int position = txtExpressionString->positionFromLineIndex( line, index );
 
 1046     long fncIndex = txtExpressionString->SendScintilla( QsciScintilla::SCI_INDICATORVALUEAT, FUNCTION_MARKER_ID, 
static_cast<long int>( position ) );
 
 1048     QString help = getFunctionHelp( func );
 
 1049     txtHelpText->setText( help );
 
 1053 void QgsExpressionBuilderWidget::onExpressionParsed( 
bool state )
 
 1057   mExpressionValid = state;
 
 1060     createMarkers( mExpressionPreviewWidget->rootNode() );
 
 1064     createErrorMarkers( mExpressionPreviewWidget->parserErrors() );
 
 1068 QString QgsExpressionBuilderWidget::helpStylesheet()
 const 
 1074   style += 
" .functionname {color: #0a6099; font-weight: bold;} " 
 1075            " .argument {font-family: monospace; color: #bf0c0c; font-style: italic; } " 
 1076            " td.argument { padding-right: 10px; }";
 
 1081 QString QgsExpressionBuilderWidget::loadFunctionHelp( 
QgsExpressionItem *expressionItem )
 
 1083   if ( !expressionItem )
 
 1086   QString helpContents = expressionItem->
getHelpText();
 
 1089   if ( helpContents.isEmpty() )
 
 1091     QString name = expressionItem->data( Qt::UserRole ).toString();
 
 1099   return "<head><style>" + helpStylesheet() + 
"</style></head><body>" + helpContents + 
"</body>";
 
 1106 QMenu *QgsExpressionBuilderWidget::ExpressionTreeMenuProvider::createContextMenu( 
QgsExpressionItem *item )
 
 1108   QMenu *menu = 
nullptr;
 
 1112     menu = 
new QMenu( mExpressionBuilderWidget );
 
 1118       menu->addAction( tr( 
"Load First 10 Unique Used Values" ), mExpressionBuilderWidget, SLOT( loadSampleUsedValues() ) );