diff --git a/src/core/expressionvariablemodel.cpp b/src/core/expressionvariablemodel.cpp index 065d1f64f9..a303997747 100644 --- a/src/core/expressionvariablemodel.cpp +++ b/src/core/expressionvariablemodel.cpp @@ -32,30 +32,78 @@ ExpressionVariableModel::ExpressionVariableModel( QObject *parent ) bool ExpressionVariableModel::setData( const QModelIndex &index, const QVariant &value, int role ) { - return QStandardItemModel::setData( index, value, role ); + QStandardItem *rowItem = item( index.row() ); + if ( !rowItem || !rowItem->data( VariableEditableRole ).toBool() ) + { + return false; + } + + switch ( role ) + { + case VariableNameRole: + if ( rowItem->data( VariableNameRole ) == value ) + { + return false; + } + + rowItem->setData( value, VariableNameRole ); + return true; + + case VariableValueRole: + + if ( rowItem->data( VariableValueRole ) == value ) + { + return false; + } + + rowItem->setData( value, VariableValueRole ); + return true; + + case VariableEditableRole: + case VariableScopeRole: + default: + break; + } + + return false; } -int ExpressionVariableModel::addVariable( VariableScope scope, const QString &name, const QString &value, bool editable ) +void ExpressionVariableModel::appendVariable( VariableScope scope, const QString &name, const QString &value, bool editable ) { - int lastVariableInScope = rowCount(); - for ( int i = 0; i < rowCount(); ++i ) + QStandardItem *nameItem = new QStandardItem( name ); + nameItem->setData( name, VariableNameRole ); + nameItem->setData( value, VariableValueRole ); + nameItem->setData( QVariant::fromValue( scope ), VariableScopeRole ); + nameItem->setData( editable, VariableEditableRole ); + nameItem->setData( name, VariableOriginalNameRole ); + nameItem->setEditable( editable ); + + appendRow( QList() << nameItem ); +} + +int ExpressionVariableModel::addVariable( VariableScope scope, const QString &name, const QString &value ) +{ + int lastEditableVariable = 0; + while ( lastEditableVariable < rowCount() ) { - if ( item( i )->data( VariableScopeRole ).value() == scope ) + if ( !item( lastEditableVariable )->data( VariableEditableRole ).toBool() ) { - lastVariableInScope = i + 1; + break; } + lastEditableVariable++; } QStandardItem *nameItem = new QStandardItem( name ); - nameItem->setData( name, VariableName ); - nameItem->setData( value, VariableValue ); + nameItem->setData( name, VariableNameRole ); + nameItem->setData( value, VariableValueRole ); nameItem->setData( QVariant::fromValue( scope ), VariableScopeRole ); - nameItem->setData( editable, VariableEditable ); - nameItem->setEditable( editable ); + nameItem->setData( true, VariableEditableRole ); + nameItem->setData( QString(), VariableOriginalNameRole ); + nameItem->setEditable( true ); - insertRow( lastVariableInScope, QList() << nameItem ); + insertRow( lastEditableVariable, QList() << nameItem ); - return lastVariableInScope; + return lastEditableVariable; } void ExpressionVariableModel::removeVariable( VariableScope scope, const QString &name ) @@ -63,11 +111,12 @@ void ExpressionVariableModel::removeVariable( VariableScope scope, const QString for ( int i = 0; i < rowCount(); ++i ) { const QStandardItem *rowItem = item( i ); - const QString variableName = rowItem->data( VariableName ).toString(); + const QString variableName = rowItem->data( VariableNameRole ).toString(); const VariableScope variableScope = rowItem->data( VariableScopeRole ).value(); if ( variableName == name && variableScope == scope ) { + mRemovedVariables << QPair( variableScope, variableName ); removeRow( i ); return; } @@ -76,15 +125,29 @@ void ExpressionVariableModel::removeVariable( VariableScope scope, const QString void ExpressionVariableModel::save() { + for ( const QPair &variable : mRemovedVariables ) + { + if ( variable.first == VariableScope::GlobalScope ) + { + QgsExpressionContextUtils::removeGlobalVariable( variable.second ); + } + } + for ( int i = 0; i < rowCount(); ++i ) { const QStandardItem *currentItem = item( i ); const VariableScope itemScope = currentItem->data( VariableScopeRole ).value(); - const QString itemName = currentItem->data( VariableName ).toString(); - const QString itemValue = currentItem->data( VariableValue ).toString(); + const QString itemName = currentItem->data( VariableNameRole ).toString(); + const QString itemOriginalName = currentItem->data( VariableOriginalNameRole ).toString(); + const QString itemValue = currentItem->data( VariableValueRole ).toString(); if ( currentItem->isEditable() && itemScope == VariableScope::GlobalScope ) { + if ( !itemOriginalName.isEmpty() && itemName != itemOriginalName ) + { + // Remove renamed variable + QgsExpressionContextUtils::removeGlobalVariable( itemOriginalName ); + } QgsExpressionContextUtils::setGlobalVariable( itemName, itemValue ); } else if ( itemScope == VariableScope::ProjectScope ) @@ -98,75 +161,54 @@ void ExpressionVariableModel::reloadVariables() { clear(); - std::unique_ptr scope( QgsExpressionContextUtils::globalScope() ); - - QStringList variableNames = scope->variableNames(); - variableNames.sort(); - - // First add readonly app variables - for ( const QString &varName : variableNames ) - { - if ( scope->isReadOnly( varName ) ) - { - QVariant varValue = scope->variable( varName ); - if ( QString::compare( varValue.toString(), QStringLiteral( "Not available" ) ) == 0 ) - varValue = QVariant( QT_TR_NOOP( "Not Available" ) ); + mRemovedVariables.clear(); - addVariable( VariableScope::GlobalScope, varName, varValue.toString(), false ); - } - } - // Second add custom variables - for ( const QString &varName : variableNames ) - { - if ( !scope->isReadOnly( varName ) ) - { - addVariable( VariableScope::GlobalScope, varName, scope->variable( varName ).toString() ); - } - } - // Finally add readonly project variables + // First, add project variables QVariantMap projectVariables = ExpressionContextUtils::projectVariables( mCurrentProject ); const QStringList projectVariableKeys = projectVariables.keys(); for ( const QString &varName : projectVariableKeys ) { QVariant varValue = projectVariables.value( varName ).toString(); - addVariable( VariableScope::ProjectScope, varName, varValue.toString() ); + appendVariable( VariableScope::ProjectScope, varName, varValue.toString(), true ); } -} -void ExpressionVariableModel::setName( int row, const QString &name ) -{ - QStandardItem *rowItem = item( row ); + std::unique_ptr scope( QgsExpressionContextUtils::globalScope() ); + QStringList variableNames = scope->variableNames(); + variableNames.sort(); - if ( !rowItem ) - return; + // Second add user-provided global variables + for ( const QString &name : variableNames ) + { + if ( !scope->isReadOnly( name ) ) + { + appendVariable( VariableScope::GlobalScope, name, scope->variable( name ).toString(), true ); + } + } - if ( rowItem->data( VariableName ).toString() == name ) - return; + // Finally, add read-only global variables + for ( const QString &name : variableNames ) + { + if ( scope->isReadOnly( name ) ) + { + QVariant varValue = scope->variable( name ); + if ( QString::compare( varValue.toString(), QStringLiteral( "Not available" ) ) == 0 ) + varValue = QVariant( QT_TR_NOOP( "Not Available" ) ); - rowItem->setData( name, VariableName ); + appendVariable( VariableScope::GlobalScope, name, varValue.toString(), false ); + } + } } -void ExpressionVariableModel::setValue( int row, const QString &value ) -{ - QStandardItem *rowItem = item( row ); - - if ( !rowItem ) - return; - - if ( rowItem->data( VariableValue ).toString() == value ) - return; - - rowItem->setData( value, VariableValue ); -} QHash ExpressionVariableModel::roleNames() const { QHash names = QStandardItemModel::roleNames(); - names[VariableName] = "VariableName"; - names[VariableValue] = "VariableValue"; + names[VariableNameRole] = "VariableName"; + names[VariableValueRole] = "VariableValue"; names[VariableScopeRole] = "VariableScope"; - names[VariableEditable] = "VariableEditable"; + names[VariableEditableRole] = "VariableEditable"; + names[VariableOriginalNameRole] = "VariableOriginalName"; return names; } diff --git a/src/core/expressionvariablemodel.h b/src/core/expressionvariablemodel.h index b827a47d6e..4d84b7ddc6 100644 --- a/src/core/expressionvariablemodel.h +++ b/src/core/expressionvariablemodel.h @@ -28,10 +28,11 @@ class ExpressionVariableModel : public QStandardItemModel public: enum Roles { - VariableName = Qt::UserRole, - VariableValue, - VariableScopeRole, - VariableEditable = Qt::EditRole + VariableEditableRole = Qt::EditRole, + VariableNameRole = Qt::UserRole, + VariableValueRole = Qt::UserRole + 1, + VariableScopeRole = Qt::UserRole + 2, + VariableOriginalNameRole = Qt::UserRole + 3, }; enum class VariableScope @@ -46,7 +47,7 @@ class ExpressionVariableModel : public QStandardItemModel bool setData( const QModelIndex &index, const QVariant &value, int role ) override; - Q_INVOKABLE int addVariable( VariableScope scope, const QString &name, const QString &value, bool editable = true ); + Q_INVOKABLE int addVariable( VariableScope scope, const QString &name, const QString &value ); Q_INVOKABLE void removeVariable( VariableScope scope, const QString &name ); @@ -54,10 +55,6 @@ class ExpressionVariableModel : public QStandardItemModel Q_INVOKABLE void reloadVariables(); - Q_INVOKABLE void setName( int row, const QString &name ); - - Q_INVOKABLE void setValue( int row, const QString &value ); - QHash roleNames() const override; /** @@ -77,7 +74,11 @@ class ExpressionVariableModel : public QStandardItemModel void onDataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles ); private: + void appendVariable( VariableScope scope, const QString &name, const QString &value, bool editable ); + QgsProject *mCurrentProject = nullptr; + + QList> mRemovedVariables; }; #endif // EXPRESSIONVARIABLEMODEL_H diff --git a/src/core/projectinfo.cpp b/src/core/projectinfo.cpp index e313103af1..346760e4ed 100644 --- a/src/core/projectinfo.cpp +++ b/src/core/projectinfo.cpp @@ -15,6 +15,7 @@ ***************************************************************************/ +#include "expressioncontextutils.h" #include "projectinfo.h" #include @@ -445,6 +446,14 @@ void ProjectInfo::mapThemeChanged() mSettings.endGroup(); } +void ProjectInfo::saveVariable( const QString &name, const QString &value ) +{ + if ( mFilePath.isEmpty() ) + return; + + mSettings.setValue( QStringLiteral( "/qgis/projectInfo/%1/variables/%2" ).arg( mFilePath, name ), value ); +} + void ProjectInfo::restoreSettings( QString &projectFilePath, QgsProject *project, QgsQuickMapCanvasMap *mapCanvas, FlatLayerTreeModel *layerTree ) { QSettings settings; @@ -625,6 +634,13 @@ void ProjectInfo::restoreSettings( QString &projectFilePath, QgsProject *project project->setSnappingConfig( config ); } settings.endGroup(); + + settings.beginGroup( QStringLiteral( "/qgis/projectInfo/%1/variables" ).arg( projectFilePath ) ); + const QStringList variableNames = settings.allKeys(); + for ( const QString &name : variableNames ) + { + ExpressionContextUtils::setProjectVariable( project, name, settings.value( name ).toString() ); + } } QVariantMap ProjectInfo::getTitleDecorationConfiguration() diff --git a/src/core/projectinfo.h b/src/core/projectinfo.h index ab07b4af70..f50dfd5f7f 100644 --- a/src/core/projectinfo.h +++ b/src/core/projectinfo.h @@ -180,6 +180,8 @@ class ProjectInfo : public QObject //! Restore the last tracking session that occured within a vector \a layer. Q_INVOKABLE QModelIndex restoreTracker( QgsVectorLayer *layer ); + Q_INVOKABLE void saveVariable( const QString &name, const QString &value ); + //! Restore various project settings static void restoreSettings( QString &projectFilePath, QgsProject *project, QgsQuickMapCanvasMap *mapCanvas, FlatLayerTreeModel *layerTree ); diff --git a/src/core/utils/expressioncontextutils.cpp b/src/core/utils/expressioncontextutils.cpp index c02fa3864d..c7fb6514d2 100644 --- a/src/core/utils/expressioncontextutils.cpp +++ b/src/core/utils/expressioncontextutils.cpp @@ -211,6 +211,7 @@ void ExpressionContextUtils::setProjectVariable( QgsProject *project, const QStr variables.insert( name, value ); project->setCustomVariables( variables ); } + void ExpressionContextUtils::setProjectVariables( QgsProject *project, const QVariantMap &variables ) { if ( !project ) diff --git a/src/qml/VariableEditor.qml b/src/qml/VariableEditor.qml index 05d3433bbf..fefa4501fd 100644 --- a/src/qml/VariableEditor.qml +++ b/src/qml/VariableEditor.qml @@ -28,21 +28,44 @@ ColumnLayout { ListView { id: table - model: ExpressionVariableModel { - currentProject: qgisProject - } flickableDirection: Flickable.VerticalFlick boundsBehavior: Flickable.StopAtBounds clip: true spacing: 1 anchors.fill: parent - anchors.margins: 3 - anchors.leftMargin: 9 - anchors.rightMargin: 5 + + ScrollBar.vertical: QfScrollBar { + } + + model: ExpressionVariableModel { + currentProject: qgisProject + } + + section.property: "VariableScope" + section.labelPositioning: ViewSection.CurrentLabelAtStart | ViewSection.InlineLabels + section.delegate: Component { + Rectangle { + width: parent.width + height: 30 + color: Theme.controlBorderColor + + Text { + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + font.bold: true + font.pointSize: Theme.resultFont.pointSize + color: Theme.mainTextColor + text: section == "GlobalScope" ? qsTr("Global variables") : qsTr("Project variables") + } + } + } + delegate: Rectangle { id: rectangle width: parent ? parent.width : 0 - height: line.height + height: line.height + 10 color: "transparent" property var itemRow: index @@ -52,14 +75,20 @@ ColumnLayout { variableNameText.forceActiveFocus(); } - Row { + Column { id: line - spacing: 5 + anchors { + left: parent.left + leftMargin: 10 + right: parent.right + rightMargin: 10 + } + spacing: 0 QfSwipeAnimator { id: variableNameTextAnimator - width: 0.35 * table.width - 10 - height: 40 + width: table.width - line.anchors.leftMargin * 2 + height: 24 shouldAutoFlick: (width < variableNameText.implicitWidth) && !dragging && !variableNameText.activeFocus contentImplicitWidth: variableNameText.implicitWidth contentWidth: variableNameText.implicitWidth @@ -67,24 +96,29 @@ ColumnLayout { TextField { id: variableNameText - topPadding: 10 - bottomPadding: 10 + topPadding: 5 + bottomPadding: 0 leftPadding: 1 rightPadding: 1 + width: Math.max(table.width - line.anchors.leftMargin * 2, implicitWidth) + height: variableNameTextAnimator.height text: VariableName - enabled: VariableEditable - font: Theme.tipFont + enabled: VariableScope == ExpressionVariableModel.GlobalScope && VariableEditable + opacity: enabled ? 1 : 0.45 + font.bold: true + font.pointSize: Theme.tinyFont.pointSize + color: variableNameText.activeFocus ? Theme.mainColor : Theme.mainTextColor horizontalAlignment: TextInput.AlignLeft - placeholderText: displayText === '' ? qsTr("Enter name") : '' + verticalAlignment: TextInput.AlignVCenter + background: Rectangle { - y: variableNameText.height - height - variableNameText.bottomPadding / 2 - height: variableNameText.activeFocus ? 2 : variableNameText.enabled ? 1 : 0 - width: Math.max(variableNameTextAnimator.width, variableNameText.implicitWidth) - color: variableNameText.activeFocus ? Theme.accentColor : "transparent" + color: "transparent" } onTextChanged: { - table.model.setName(index, text); + if (enabled && VariableName != text) { + VariableName = text; + } } onCursorRectangleChanged: { @@ -93,56 +127,60 @@ ColumnLayout { } } - QfSwipeAnimator { - id: variableValueTextAnimator - width: 0.65 * table.width - 10 - (canDelete ? deleteVariableButton.width : 0) - height: 40 - shouldAutoFlick: (width < variableValueText.implicitWidth) && !dragging && !variableValueText.activeFocus - contentImplicitWidth: variableValueText.implicitWidth - contentWidth: variableValueText.implicitWidth - duration: shouldAutoFlick ? Math.abs(variableValueText.width - width) * 100 + 10 : 10000 - - TextField { - id: variableValueText - topPadding: 10 - bottomPadding: 10 - leftPadding: 1 - rightPadding: 1 - text: VariableValue - enabled: VariableEditable - font: Theme.tipFont - horizontalAlignment: TextInput.AlignLeft - placeholderText: displayText === '' ? qsTr("Enter value") : '' - background: Rectangle { - y: variableValueText.height - height - variableValueText.bottomPadding / 2 - height: variableValueText.activeFocus ? 2 : variableNameText.enabled ? 1 : 0 - width: Math.max(variableValueTextAnimator.width, variableValueText.implicitWidth) - color: variableValueText.activeFocus ? Theme.accentColor : "transparent" - } - - onTextChanged: { - table.model.setValue(index, text); - } - - onCursorRectangleChanged: { - variableValueTextAnimator.ensureCursorVisible(cursorRectangle); + Row { + spacing: 5 + + QfSwipeAnimator { + id: variableValueTextAnimator + width: table.width - line.anchors.leftMargin * 2 - (canDelete ? deleteVariableButton.width : 0) + height: 40 + shouldAutoFlick: (width < variableValueText.implicitWidth) && !dragging && !variableValueText.activeFocus + contentImplicitWidth: variableValueText.implicitWidth + contentWidth: variableValueText.implicitWidth + duration: shouldAutoFlick ? Math.abs(variableValueText.width - width) * 100 + 10 : 10000 + + QfTextField { + id: variableValueText + topPadding: 10 + bottomPadding: 10 + leftPadding: 1 + rightPadding: 1 + width: Math.max(table.width - line.anchors.leftMargin * 2 - (canDelete ? deleteVariableButton.width : 0), implicitWidth) + text: VariableValue + enabled: VariableEditable + font: Theme.defaultFont + horizontalAlignment: TextInput.AlignLeft + placeholderText: !variableValueText.activeFocus && displayText === '' ? qsTr("Enter value") : '' + + onTextChanged: { + if (enabled && VariableValue != text) { + VariableValue = text; + if (VariableScope == ExpressionVariableModel.ProjectScope) { + projectInfo.saveVariable(VariableName, text); + } + } + } + + onCursorRectangleChanged: { + variableValueTextAnimator.ensureCursorVisible(cursorRectangle); + } } } - } - QfToolButton { - id: deleteVariableButton - width: 36 - height: 36 - anchors.verticalCenter: parent.verticalCenter - visible: canDelete + QfToolButton { + id: deleteVariableButton + width: 36 + height: 36 + anchors.verticalCenter: parent.verticalCenter + visible: canDelete - iconSource: Theme.getThemeIcon('ic_delete_forever_white_24dp') - iconColor: Theme.mainTextColor - bgcolor: "transparent" + iconSource: Theme.getThemeIcon('ic_delete_forever_white_24dp') + iconColor: Theme.mainTextColor + bgcolor: "transparent" - onClicked: { - table.model.removeVariable(VariableScope, variableNameText.text); + onClicked: { + table.model.removeVariable(VariableScope, variableNameText.text); + } } } }