diff --git a/images/images.qrc b/images/images.qrc
index bdf25e13710..8eabb4d080d 100644
--- a/images/images.qrc
+++ b/images/images.qrc
@@ -314,6 +314,7 @@
themes/default/mIconClear.svg
themes/default/mIconClose.png
themes/default/mIconCollapse.png
+ themes/default/mIconCollapseSmall.svg
themes/default/mIconColorBox.svg
themes/default/mIconColorPicker.svg
themes/default/mIconColorSwatches.svg
@@ -333,6 +334,7 @@
themes/default/mIconEditable.png
themes/default/mIconEditableEdits.png
themes/default/mIconExpand.png
+ themes/default/mIconExpandSmall.svg
themes/default/mIconExpression.svg
themes/default/mIconExpressionEditorOpen.svg
themes/default/mIconExpressionFilter.svg
diff --git a/images/themes/default/mIconCollapseSmall.svg b/images/themes/default/mIconCollapseSmall.svg
new file mode 100644
index 00000000000..ae958529d7d
--- /dev/null
+++ b/images/themes/default/mIconCollapseSmall.svg
@@ -0,0 +1,78 @@
+
+
+
+
diff --git a/images/themes/default/mIconExpandSmall.svg b/images/themes/default/mIconExpandSmall.svg
new file mode 100644
index 00000000000..20282d7c96e
--- /dev/null
+++ b/images/themes/default/mIconExpandSmall.svg
@@ -0,0 +1,78 @@
+
+
+
+
diff --git a/python/gui/gui.sip b/python/gui/gui.sip
index 6f4e8361f0a..b83f5ba09b1 100644
--- a/python/gui/gui.sip
+++ b/python/gui/gui.sip
@@ -118,6 +118,7 @@
%Include qgssvgannotationitem.sip
%Include qgstextannotationitem.sip
%Include qgsuserinputdockwidget.sip
+%Include qgsvariableeditorwidget.sip
%Include qgsvectorlayertools.sip
%Include qgsvertexmarker.sip
diff --git a/python/gui/qgsvariableeditorwidget.sip b/python/gui/qgsvariableeditorwidget.sip
new file mode 100644
index 00000000000..e7959fe649d
--- /dev/null
+++ b/python/gui/qgsvariableeditorwidget.sip
@@ -0,0 +1,90 @@
+/** \ingroup gui
+ * \class QgsVariableEditorWidget
+ * A tree based widget for editing expression context scope variables. The widget allows editing
+ * variables from a QgsExpressionContextScope, and can optionally also show inherited
+ * variables from a QgsExpressionContext.
+ * \note added in QGIS 2.12
+ */
+
+class QgsVariableEditorWidget : QWidget
+{
+%TypeHeaderCode
+#include
+%End
+
+ public:
+
+ /** Constructor for QgsVariableEditorWidget.
+ * @param parent parent widget
+ */
+ QgsVariableEditorWidget( QWidget *parent /TransferThis/ = 0 );
+
+ ~QgsVariableEditorWidget();
+
+ /** Overwrites the QgsExpressionContext for the widget. Setting a context
+ * allows the widget to show all inherited variables for the context,
+ * and highlight any overriden variables within scopes.
+ * @param context expression context
+ * @see context()
+ */
+ void setContext( QgsExpressionContext* context );
+
+ /** Returns the current expression context for the widget. QgsVariableEditorWidget widgets
+ * are created with an empty context by default.
+ * @see setContext()
+ */
+ QgsExpressionContext* context() const;
+
+ /** Reloads all scopes from the editor's current context. This method should be called
+ * after adding or removing scopes from the attached context.
+ * @see context()
+ */
+ void reloadContext();
+
+ /** Sets the editable scope for the widget. Only variables from the editable scope can
+ * be modified by users.
+ * @param scopeIndex index of current editable scope. Set to -1 to disable
+ * editing and make the widget read-only.
+ * @see editableScope()
+ */
+ void setEditableScopeIndex( int scopeIndex );
+
+ /** Returns the current editable scope for the widget.
+ * @returns editable scope, or 0 if no editable scope is set
+ * @see setEditableScopeIndex()
+ */
+ QgsExpressionContextScope* editableScope() const;
+
+ /** Sets the setting group for the widget. QgsVariableEditorWidget widgets with
+ * the same setting group will synchronise their settings, eg the size
+ * of columns in the tree widget.
+ * @param group setting group
+ * @see settingGroup()
+ */
+ void setSettingGroup( const QString &group );
+
+ /** Returns the setting group for the widget. QgsVariableEditorWidget widgets with
+ * the same setting group will synchronise their settings, eg the size
+ * of columns in the tree widget.
+ * @returns setting group name
+ * @see setSettingGroup()
+ */
+ QString settingGroup() const;
+
+ /** Returns a map variables set within the editable scope. Read only variables are not
+ * returned. This method can be used to retrieve the variables edited an added by
+ * users via the widget.
+ */
+ QgsStringMap variablesInActiveScope() const;
+
+ signals:
+
+ /** Emitted when the user has modified a scope using the widget.
+ */
+ void scopeChanged();
+
+ protected:
+
+ void showEvent( QShowEvent *event );
+
+};
diff --git a/src/app/qgsoptions.cpp b/src/app/qgsoptions.cpp
index 230601750c6..01347142ca5 100644
--- a/src/app/qgsoptions.cpp
+++ b/src/app/qgsoptions.cpp
@@ -39,6 +39,7 @@
#include "qgscolorschemeregistry.h"
#include "qgssymbollayerv2utils.h"
#include "qgscolordialog.h"
+#include "qgsexpressioncontext.h"
#include
#include
@@ -838,6 +839,10 @@ QgsOptions::QgsOptions( QWidget *parent, Qt::WindowFlags fl ) :
// load gdal driver list only when gdal tab is first opened
mLoadedGdalDriverList = false;
+ mVariableEditor->context()->appendScope( QgsExpressionContextUtils::globalScope() );
+ mVariableEditor->reloadContext();
+ mVariableEditor->setEditableScopeIndex( 0 );
+
// restore window and widget geometry/state
restoreOptionsBaseUi();
}
@@ -1333,6 +1338,9 @@ void QgsOptions::saveOptions()
// TODO[MD] QgisApp::instance()->legend()->updateLegendItemSymbologies();
}
+ //save variables
+ QgsExpressionContextUtils::setGlobalVariables( mVariableEditor->variablesInActiveScope() );
+
// save app stylesheet last (in case reset becomes necessary)
if ( mStyleSheetNewOpts != mStyleSheetOldOpts )
{
diff --git a/src/app/qgsoptions.h b/src/app/qgsoptions.h
index b6b0e779f4c..f78caab6669 100644
--- a/src/app/qgsoptions.h
+++ b/src/app/qgsoptions.h
@@ -29,6 +29,8 @@
#include
+class QgsExpressionContext;
+
/**
* \class QgsOptions
* \brief Set user options and preferences
diff --git a/src/app/qgsprojectproperties.cpp b/src/app/qgsprojectproperties.cpp
index 4ef20232722..ea2398cdbd4 100644
--- a/src/app/qgsprojectproperties.cpp
+++ b/src/app/qgsprojectproperties.cpp
@@ -48,6 +48,7 @@
#include "qgscolorschemeregistry.h"
#include "qgssymbollayerv2utils.h"
#include "qgscolordialog.h"
+#include "qgsexpressioncontext.h"
//qt includes
#include
@@ -524,6 +525,11 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas* mapCanvas, QWidget *pa
on_cbxProjectionEnabled_toggled( myProjectionEnabled );
}
+ mVariableEditor->context()->appendScope( QgsExpressionContextUtils::globalScope() );
+ mVariableEditor->context()->appendScope( QgsExpressionContextUtils::projectScope() );
+ mVariableEditor->reloadContext();
+ mVariableEditor->setEditableScopeIndex( 1 );
+
projectionSelectorInitialized();
restoreOptionsBaseUi();
restoreState();
@@ -919,6 +925,9 @@ void QgsProjectProperties::apply()
QgsProject::instance()->relationManager()->setRelations( mRelationManagerDlg->relations() );
+ //save variables
+ QgsExpressionContextUtils::setProjectVariables( mVariableEditor->variablesInActiveScope() );
+
emit refresh();
}
diff --git a/src/app/qgsprojectproperties.h b/src/app/qgsprojectproperties.h
index b9b0cba6386..5718d1cb052 100644
--- a/src/app/qgsprojectproperties.h
+++ b/src/app/qgsprojectproperties.h
@@ -26,6 +26,7 @@
class QgsMapCanvas;
class QgsRelationManagerDialog;
class QgsStyleV2;
+class QgsExpressionContext;
/** Dialog to set project level properties
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 395c730be24..e8c81e1b511 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -234,6 +234,7 @@ SET(QGIS_GUI_SRCS
qgssvgannotationitem.cpp
qgstextannotationitem.cpp
qgsuserinputdockwidget.cpp
+ qgsvariableeditorwidget.cpp
qgsvertexmarker.cpp
qgsunitselectionwidget.cpp
)
@@ -351,6 +352,7 @@ SET(QGIS_GUI_MOC_HDRS
qgssearchquerybuilder.h
qgsslider.h
qgssublayersdialog.h
+ qgsvariableeditorwidget.h
qgsunitselectionwidget.h
qgsuserinputdockwidget.h
diff --git a/src/gui/qgsvariableeditorwidget.cpp b/src/gui/qgsvariableeditorwidget.cpp
new file mode 100644
index 00000000000..4628c04ae85
--- /dev/null
+++ b/src/gui/qgsvariableeditorwidget.cpp
@@ -0,0 +1,753 @@
+/***************************************************************************
+ qgsvariableeditorwidget.cpp
+ ---------------------------
+ Date : April 2015
+ Copyright : (C) 2015 by Nyall Dawson
+ Email : nyall dot dawson at gmail dot com
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#include "qgsvariableeditorwidget.h"
+#include "qgsexpressioncontext.h"
+#include "qgsfeature.h"
+#include "qgsapplication.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/// @cond
+
+class VariableEditorDelegate : public QItemDelegate
+{
+
+ public:
+ VariableEditorDelegate( QObject *parent = 0, QgsVariableEditorTree *tree = 0 )
+ : QItemDelegate( parent )
+ , mParentTree( tree )
+ {}
+
+ QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem &option,
+ const QModelIndex &index ) const override;
+ void updateEditorGeometry( QWidget *editor, const QStyleOptionViewItem &option,
+ const QModelIndex &index ) const override;
+ QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const override;
+ void setModelData( QWidget* widget, QAbstractItemModel* model,
+ const QModelIndex & index ) const override;
+ void setEditorData( QWidget *, const QModelIndex & ) const override {}
+
+ private:
+ QgsVariableEditorTree *mParentTree;
+};
+
+QIcon QgsVariableEditorTree::mExpandIcon;
+
+/// @endcond
+
+
+//
+// QgsVariableEditorWidget
+//
+
+QgsVariableEditorWidget::QgsVariableEditorWidget( QWidget *parent )
+ : QWidget( parent )
+ , mContext( 0 )
+ , mEditableScopeIndex( -1 )
+ , mShown( false )
+{
+ QVBoxLayout* verticalLayout = new QVBoxLayout( this );
+ verticalLayout->setSpacing( 3 );
+ verticalLayout->setContentsMargins( 3, 3, 3, 3 );
+ mTreeWidget = new QgsVariableEditorTree( this );
+ mTreeWidget->setSelectionMode( QAbstractItemView::SingleSelection );
+ verticalLayout->addWidget( mTreeWidget );
+ QHBoxLayout* horizontalLayout = new QHBoxLayout();
+ horizontalLayout->setSpacing( 6 );
+ QSpacerItem* horizontalSpacer = new QSpacerItem( 40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum );
+ horizontalLayout->addItem( horizontalSpacer );
+ mAddButton = new QPushButton();
+ mAddButton->setIcon( QgsApplication::getThemeIcon( "/mActionSignPlus.png" ) );
+ mAddButton->setEnabled( false );
+ horizontalLayout->addWidget( mAddButton );
+ mRemoveButton = new QPushButton();
+ mRemoveButton->setIcon( QgsApplication::getThemeIcon( "/symbologyRemove.png" ) );
+ mRemoveButton->setEnabled( false );
+ horizontalLayout->addWidget( mRemoveButton );
+ verticalLayout->addLayout( horizontalLayout );
+ connect( mRemoveButton, SIGNAL( clicked() ), this, SLOT( on_mRemoveButton_clicked() ) );
+ connect( mAddButton, SIGNAL( clicked() ), this, SLOT( on_mAddButton_clicked() ) );
+ connect( mTreeWidget, SIGNAL( itemSelectionChanged() ), this, SLOT( selectionChanged() ) );
+ connect( mTreeWidget, SIGNAL( scopeChanged() ), this, SIGNAL( scopeChanged() ) );
+
+ setContext( new QgsExpressionContext() );
+}
+
+QgsVariableEditorWidget::~QgsVariableEditorWidget()
+{
+ QSettings settings;
+ settings.setValue( saveKey() + "column0width", mTreeWidget->header()->sectionSize( 0 ) );
+}
+
+void QgsVariableEditorWidget::showEvent( QShowEvent * event )
+{
+ // initialise widget on first show event only
+ if ( mShown )
+ {
+ event->accept();
+ return;
+ }
+
+ //restore split size
+ QSettings settings;
+ QVariant val;
+ val = settings.value( saveKey() + "column0width" );
+ bool ok;
+ int sectionSize = val.toInt( &ok );
+ if ( ok )
+ {
+ mTreeWidget->header()->resizeSection( 0, sectionSize );
+ }
+ mShown = true;
+
+ QWidget::showEvent( event );
+}
+
+void QgsVariableEditorWidget::setContext( QgsExpressionContext* context )
+{
+ mContext.reset( new QgsExpressionContext( *context ) );
+ reloadContext();
+}
+
+void QgsVariableEditorWidget::reloadContext()
+{
+ mTreeWidget->resetTree();
+ mTreeWidget->setContext( mContext.data() );
+ mTreeWidget->refreshTree();
+}
+
+void QgsVariableEditorWidget::setEditableScopeIndex( int scopeIndex )
+{
+ mEditableScopeIndex = scopeIndex;
+ if ( mEditableScopeIndex >= 0 )
+ {
+ mAddButton->setEnabled( true );
+ }
+ mTreeWidget->setEditableScopeIndex( scopeIndex );
+ mTreeWidget->refreshTree();
+}
+
+QgsExpressionContextScope* QgsVariableEditorWidget::editableScope() const
+{
+ if ( !mContext || mEditableScopeIndex < 0 || mEditableScopeIndex >= mContext->scopeCount() )
+ {
+ return 0;
+ }
+ return mContext->scope( mEditableScopeIndex );
+}
+
+QgsStringMap QgsVariableEditorWidget::variablesInActiveScope() const
+{
+ QgsStringMap variables;
+ if ( !mContext || mEditableScopeIndex < 0 || mEditableScopeIndex >= mContext->scopeCount() )
+ {
+ return variables;
+ }
+
+ QgsExpressionContextScope* scope = mContext->scope( mEditableScopeIndex );
+ Q_FOREACH ( QString variable, scope->variableNames() )
+ {
+ if ( scope->isReadOnly( variable ) )
+ continue;
+
+ variables.insert( variable, scope->variable( variable ).toString() );
+ }
+
+ return variables;
+}
+
+QString QgsVariableEditorWidget::saveKey() const
+{
+ // save key for load/save state
+ // currently QgsVariableEditorTree/window()/object
+ QString setGroup = mSettingGroup.isEmpty() ? objectName() : mSettingGroup;
+ QString saveKey = "/QgsVariableEditorTree/" + setGroup + "/";
+ return saveKey;
+}
+
+void QgsVariableEditorWidget::on_mAddButton_clicked()
+{
+ if ( mEditableScopeIndex < 0 || mEditableScopeIndex >= mContext->scopeCount() )
+ return;
+
+ QgsExpressionContextScope* scope = mContext->scope( mEditableScopeIndex );
+ scope->setVariable( "new_variable", QVariant() );
+ mTreeWidget->refreshTree();
+ QTreeWidgetItem* item = mTreeWidget->itemFromVariable( scope, "new_variable" );
+ QModelIndex index = mTreeWidget->itemToIndex( item );
+ mTreeWidget->selectionModel()->select( index, QItemSelectionModel::ClearAndSelect );
+ mTreeWidget->editItem( item, 0 );
+
+ emit scopeChanged();
+}
+
+void QgsVariableEditorWidget::on_mRemoveButton_clicked()
+{
+ if ( mEditableScopeIndex < 0 || mEditableScopeIndex >= mContext->scopeCount() )
+ return;
+
+ QgsExpressionContextScope* editableScope = mContext->scope( mEditableScopeIndex );
+ QList selectedItems = mTreeWidget->selectedItems();
+
+ foreach ( QTreeWidgetItem* item, selectedItems )
+ {
+ if ( !( item->flags() & Qt::ItemIsEditable ) )
+ continue;
+
+ QString name = item->text( 0 );
+ QgsExpressionContextScope* itemScope = mTreeWidget->scopeFromItem( item );
+ if ( itemScope != editableScope )
+ continue;
+
+ if ( itemScope->isReadOnly( name ) )
+ continue;
+
+ itemScope->removeVariable( name );
+ mTreeWidget->removeItem( item );
+ }
+ mTreeWidget->refreshTree();
+}
+
+void QgsVariableEditorWidget::selectionChanged()
+{
+ if ( mEditableScopeIndex < 0 || mEditableScopeIndex >= mContext->scopeCount() )
+ {
+ mRemoveButton->setEnabled( false );
+ return;
+ }
+
+ QgsExpressionContextScope* editableScope = mContext->scope( mEditableScopeIndex );
+ QList selectedItems = mTreeWidget->selectedItems();
+
+ bool removeEnabled = true;
+ foreach ( QTreeWidgetItem* item, selectedItems )
+ {
+ if ( !( item->flags() & Qt::ItemIsEditable ) )
+ {
+ removeEnabled = false;
+ break;
+ }
+
+ QString name = item->text( 0 );
+ QgsExpressionContextScope* itemScope = mTreeWidget->scopeFromItem( item );
+ if ( itemScope != editableScope )
+ {
+ removeEnabled = false;
+ break;
+ }
+
+ if ( editableScope->isReadOnly( name ) )
+ {
+ removeEnabled = false;
+ break;
+ }
+ }
+ mRemoveButton->setEnabled( removeEnabled );
+}
+
+
+//
+// VariableEditorTree
+//
+
+QgsVariableEditorTree::QgsVariableEditorTree( QWidget *parent )
+ : QTreeWidget( parent )
+ , mEditorDelegate( 0 )
+ , mEditableScopeIndex( -1 )
+ , mContext( 0 )
+{
+ // init icons
+ if ( mExpandIcon.isNull() )
+ {
+ QPixmap pix( 14, 14 );
+ pix.fill( Qt::transparent );
+ mExpandIcon.addPixmap( QgsApplication::getThemeIcon( "/mIconExpandSmall.svg" ).pixmap( 14, 14 ), QIcon::Normal, QIcon::Off );
+ mExpandIcon.addPixmap( QgsApplication::getThemeIcon( "/mIconExpandSmall.svg" ).pixmap( 14, 14 ), QIcon::Selected, QIcon::Off );
+ mExpandIcon.addPixmap( QgsApplication::getThemeIcon( "/mIconCollapseSmall.svg" ).pixmap( 14, 14 ), QIcon::Normal, QIcon::On );
+ mExpandIcon.addPixmap( QgsApplication::getThemeIcon( "/mIconCollapseSmall.svg" ).pixmap( 14, 14 ), QIcon::Selected, QIcon::On );
+ }
+
+ setIconSize( QSize( 18, 18 ) );
+ setColumnCount( 2 );
+ setHeaderLabels( QStringList() << tr( "Variable" ) << tr( "Value" ) );
+ setAlternatingRowColors( true );
+ setEditTriggers( QAbstractItemView::AllEditTriggers );
+ setRootIsDecorated( false );
+ header()->setMovable( false );
+ header()->setResizeMode( QHeaderView::Interactive );
+
+ mEditorDelegate = new VariableEditorDelegate( this, this );
+ setItemDelegate( mEditorDelegate );
+}
+
+QgsExpressionContextScope* QgsVariableEditorTree::scopeFromItem( QTreeWidgetItem *item ) const
+{
+ if ( !item )
+ return 0;
+
+ bool ok;
+ int contextIndex = item->data( 0, ContextIndex ).toInt( &ok );
+ if ( !ok )
+ return 0;
+
+ if ( !mContext )
+ {
+ return 0;
+ }
+ else if ( mContext->scopeCount() > contextIndex )
+ {
+ return mContext->scope( contextIndex );
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+QTreeWidgetItem* QgsVariableEditorTree::itemFromVariable( QgsExpressionContextScope *scope, const QString &name ) const
+{
+ int contextIndex = mContext ? mContext->indexOfScope( scope ) : 0;
+ if ( contextIndex < 0 )
+ return 0;
+ return mVariableToItem.value( qMakePair( contextIndex, name ) );
+}
+
+QgsExpressionContextScope* QgsVariableEditorTree::editableScope()
+{
+ if ( !mContext || mEditableScopeIndex < 0 || mEditableScopeIndex >= mContext->scopeCount() )
+ {
+ return 0;
+ }
+
+ return mContext->scope( mEditableScopeIndex );
+}
+
+void QgsVariableEditorTree::refreshTree()
+{
+ if ( !mContext || mEditableScopeIndex < 0 )
+ {
+ clear();
+ return;
+ }
+
+ //add all scopes from the context
+ int scopeIndex = 0;
+ Q_FOREACH ( QgsExpressionContextScope* scope, mContext->scopes() )
+ {
+ refreshScopeItems( scope, scopeIndex );
+ scopeIndex++;
+ }
+}
+
+void QgsVariableEditorTree::refreshScopeVariables( QgsExpressionContextScope* scope, int scopeIndex )
+{
+ QColor baseColor = rowColor( scopeIndex );
+ bool isCurrent = scopeIndex == mEditableScopeIndex;
+ QTreeWidgetItem* scopeItem = mScopeToItem.value( scopeIndex );
+
+ foreach ( QString name, scope->variableNames() )
+ {
+ QTreeWidgetItem* item;
+ if ( mVariableToItem.contains( qMakePair( scopeIndex, name ) ) )
+ {
+ item = mVariableToItem.value( qMakePair( scopeIndex, name ) );
+ }
+ else
+ {
+ item = new QTreeWidgetItem( scopeItem );
+ mVariableToItem.insert( qMakePair( scopeIndex, name ), item );
+ }
+
+ bool readOnly = scope->isReadOnly( name );
+ bool isActive = true;
+ QgsExpressionContextScope* activeScope = 0;
+ if ( mContext )
+ {
+ activeScope = mContext->activeScopeForVariable( name );
+ isActive = activeScope == scope;
+ }
+
+ item->setFlags( item->flags() | Qt::ItemIsEnabled );
+ item->setText( 0, name );
+ QString value = scope->variable( name ).toString();
+ item->setText( 1, value );
+ QFont font = item->font( 0 );
+ if ( readOnly || !isCurrent )
+ {
+ font.setItalic( true );
+ item->setFlags( item->flags() ^ Qt::ItemIsEditable );
+ }
+ else
+ {
+ font.setItalic( false );
+ item->setFlags( item->flags() | Qt::ItemIsEditable );
+ }
+ if ( !isActive )
+ {
+ //overridden
+ font.setStrikeOut( true );
+ QString toolTip = tr( "Overriden by value from %1" ).arg( activeScope->name() );
+ item->setToolTip( 0, toolTip );
+ item->setToolTip( 1, toolTip );
+ }
+ else
+ {
+ font.setStrikeOut( false );
+ item->setToolTip( 0, name );
+ item->setToolTip( 1, value );
+ }
+ item->setFont( 0, font );
+ item->setFont( 1, font );
+ item->setData( 0, RowBaseColor, baseColor );
+ item->setData( 0, ContextIndex, scopeIndex );
+ item->setFirstColumnSpanned( false );
+ }
+}
+
+void QgsVariableEditorTree::refreshScopeItems( QgsExpressionContextScope* scope, int scopeIndex )
+{
+ QSettings settings;
+
+ //add top level item
+ bool isCurrent = scopeIndex == mEditableScopeIndex;
+
+ QTreeWidgetItem* scopeItem;
+ if ( mScopeToItem.contains( scopeIndex ) )
+ {
+ //retrieve existing item
+ scopeItem = mScopeToItem.value( scopeIndex );
+ }
+ else
+ {
+ //create new top-level item
+ scopeItem = new QTreeWidgetItem();
+ mScopeToItem.insert( scopeIndex, scopeItem );
+ scopeItem->setFlags( scopeItem->flags() | Qt::ItemIsEnabled );
+ scopeItem->setText( 0, scope->name() );
+ scopeItem->setFlags( scopeItem->flags() ^ Qt::ItemIsEditable );
+ scopeItem->setFirstColumnSpanned( true );
+ QFont scopeFont = scopeItem->font( 0 );
+ scopeFont .setBold( true );
+ scopeItem->setFont( 0, scopeFont );
+ scopeItem->setFirstColumnSpanned( true );
+
+ addTopLevelItem( scopeItem );
+
+ //expand by default if current context or context was previously expanded
+ if ( isCurrent || settings.value( "QgsVariableEditor/" + scopeItem->text( 0 ) + "/expanded" ).toBool() )
+ scopeItem->setExpanded( true );
+
+ scopeItem->setIcon( 0, mExpandIcon );
+ }
+
+ refreshScopeVariables( scope, scopeIndex );
+}
+
+void QgsVariableEditorTree::removeItem( QTreeWidgetItem *item )
+{
+ if ( !item )
+ return;
+
+ mVariableToItem.remove( mVariableToItem.key( item ) );
+ item->parent()->takeChild( item->parent()->indexOfChild( item ) );
+
+ emit scopeChanged();
+}
+
+void QgsVariableEditorTree::renameItem( QTreeWidgetItem *item, const QString& name )
+{
+ if ( !item )
+ return;
+
+ int contextIndex = mVariableToItem.key( item ).first;
+ mVariableToItem.remove( mVariableToItem.key( item ) );
+ mVariableToItem.insert( qMakePair( contextIndex, name ), item );
+ item->setText( 0, name );
+
+ emit scopeChanged();
+}
+
+void QgsVariableEditorTree::resetTree()
+{
+ mVariableToItem.clear();
+ mScopeToItem.clear();
+ clear();
+}
+
+void QgsVariableEditorTree::emitChanged()
+{
+ emit scopeChanged();
+}
+
+void QgsVariableEditorTree::drawRow( QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index ) const
+{
+ QStyleOptionViewItemV3 opt = option;
+ QTreeWidgetItem* item = itemFromIndex( index );
+ if ( index.parent().isValid() )
+ {
+ //not a top-level item, so shade row background by context
+ const QColor baseColor = item->data( 0, RowBaseColor ).value();
+ painter->fillRect( option.rect, baseColor );
+ opt.palette.setColor( QPalette::AlternateBase, baseColor.lighter( 110 ) );
+ }
+ QTreeWidget::drawRow( painter, opt, index );
+ QColor color = static_cast( QApplication::style()->styleHint( QStyle::SH_Table_GridLineColor, &opt ) );
+ painter->save();
+ painter->setPen( QPen( color ) );
+ painter->drawLine( opt.rect.x(), opt.rect.bottom(), opt.rect.right(), opt.rect.bottom() );
+ painter->restore();
+}
+
+QColor QgsVariableEditorTree::rowColor( int index ) const
+{
+ //return some nice (inspired by Qt Designer) background row colors
+ int colorIdx = index % 6;
+ switch ( colorIdx )
+ {
+ case 0:
+ return QColor( 255, 220, 167 );
+ case 1:
+ return QColor( 255, 255, 191 );
+ case 2:
+ return QColor( 191, 255, 191 );
+ case 3:
+ return QColor( 199, 255, 255 );
+ case 4:
+ return QColor( 234, 191, 255 );
+ case 5:
+ default:
+ return QColor( 255, 191, 239 );
+ }
+}
+
+void QgsVariableEditorTree::toggleContextExpanded( QTreeWidgetItem* item )
+{
+ if ( !item )
+ return;
+
+ item->setExpanded( !item->isExpanded() );
+
+ //save expanded state
+ QSettings settings;
+ settings.setValue( "QgsVariableEditor/" + item->text( 0 ) + "/expanded", item->isExpanded() );
+}
+
+void QgsVariableEditorTree::editNext( const QModelIndex& index )
+{
+ if ( !index.isValid() )
+ return;
+
+ if ( index.column() == 0 )
+ {
+ //switch to next column
+ QModelIndex nextIndex = index.sibling( index.row(), 1 );
+ if ( nextIndex.isValid() )
+ {
+ setCurrentIndex( nextIndex );
+ edit( nextIndex );
+ }
+ }
+ else
+ {
+ QModelIndex nextIndex = model()->index( index.row() + 1, 0, index.parent() );
+ if ( nextIndex.isValid() )
+ {
+ //start editing next row
+ setCurrentIndex( nextIndex );
+ edit( nextIndex );
+ }
+ else
+ {
+ edit( index );
+ }
+ }
+}
+
+void QgsVariableEditorTree::keyPressEvent( QKeyEvent *event )
+{
+ switch ( event->key() )
+ {
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+ case Qt::Key_Space:
+ {
+ QTreeWidgetItem *item = currentItem();
+ if ( item && !item->parent() )
+ {
+ event->accept();
+ toggleContextExpanded( item );
+ return;
+ }
+ else if ( item && ( item->flags() & Qt::ItemIsEditable ) )
+ {
+ event->accept();
+ editNext( currentIndex() );
+ return;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ QTreeWidget::keyPressEvent( event );
+}
+
+void QgsVariableEditorTree::mousePressEvent( QMouseEvent *event )
+{
+ QTreeWidget::mousePressEvent( event );
+ QTreeWidgetItem* item = itemAt( event->pos() );
+ if ( !item )
+ return;
+
+ if ( item->parent() )
+ {
+ //not a top-level item
+ return;
+ }
+
+ if ( event->pos().x() + header()->offset() > 20 )
+ {
+ //not clicking on expand icon
+ return;
+ }
+
+ if ( event->modifiers() & Qt::ShiftModifier )
+ {
+ //shift modifier expands all
+ if ( !item->isExpanded() )
+ {
+ expandAll();
+ }
+ else
+ {
+ collapseAll();
+ }
+ }
+ else
+ {
+ toggleContextExpanded( item );
+ }
+}
+
+
+//
+// VariableEditorDelegate
+//
+
+QWidget* VariableEditorDelegate::createEditor( QWidget *parent,
+ const QStyleOptionViewItem&,
+ const QModelIndex &index ) const
+{
+ if ( !mParentTree )
+ return 0;
+
+ //no editing for top level items
+ if ( !index.parent().isValid() )
+ return 0;
+
+ QTreeWidgetItem *item = mParentTree->indexToItem( index );
+ QgsExpressionContextScope* scope = mParentTree->scopeFromItem( item );
+ if ( !item || !scope )
+ return 0;
+
+ QString variableName = mParentTree->variableNameFromIndex( index );
+
+ //no editing inherited or read-only variables
+ if ( scope != mParentTree->editableScope() || scope->isReadOnly( variableName ) )
+ return 0;
+
+ QLineEdit *lineEdit = new QLineEdit( parent );
+ lineEdit->setText( index.column() == 0 ? variableName : mParentTree->editableScope()->variable( variableName ).toString() );
+ lineEdit->setAutoFillBackground( true );
+ return lineEdit;
+}
+
+void VariableEditorDelegate::updateEditorGeometry( QWidget *editor,
+ const QStyleOptionViewItem &option,
+ const QModelIndex & ) const
+{
+ editor->setGeometry( option.rect.adjusted( 0, 0, 0, -1 ) );
+}
+
+QSize VariableEditorDelegate::sizeHint( const QStyleOptionViewItem &option,
+ const QModelIndex &index ) const
+{
+ return QItemDelegate::sizeHint( option, index ) + QSize( 3, 4 );
+}
+
+void VariableEditorDelegate::setModelData( QWidget* widget, QAbstractItemModel *model,
+ const QModelIndex& index ) const
+{
+ Q_UNUSED( model );
+
+ if ( !mParentTree )
+ return;
+
+ QTreeWidgetItem *item = mParentTree->indexToItem( index );
+ QgsExpressionContextScope *scope = mParentTree->scopeFromItem( item );
+ if ( !item || !scope )
+ return;
+
+ QLineEdit* lineEdit = qobject_cast< QLineEdit* >( widget );
+ if ( !lineEdit )
+ return;
+
+ QString variableName = mParentTree->variableNameFromIndex( index );
+ if ( index.column() == 0 )
+ {
+ //edited variable name
+ QString newName = lineEdit->text();
+
+ //test for validity
+ if ( newName == variableName )
+ {
+ return;
+ }
+ if ( scope->hasVariable( newName ) )
+ {
+ //existing name
+ QMessageBox::warning( mParentTree, tr( "Rename variable" ), tr( "A variable with the name \"%1\" already exists in this context." ).arg( newName ) );
+ newName.append( "_1" );
+ }
+
+ QString value = scope->variable( variableName ).toString();
+ mParentTree->renameItem( item, newName );
+ scope->removeVariable( variableName );
+ scope->setVariable( newName, value );
+ mParentTree->emitChanged();
+ }
+ else if ( index.column() == 1 )
+ {
+ //edited variable value
+ QString value = lineEdit->text();
+ if ( scope->variable( variableName ).toString() == value )
+ {
+ return;
+ }
+ scope->setVariable( variableName, value );
+ mParentTree->emitChanged();
+ }
+ mParentTree->refreshTree();
+}
diff --git a/src/gui/qgsvariableeditorwidget.h b/src/gui/qgsvariableeditorwidget.h
new file mode 100644
index 00000000000..2afed827fcc
--- /dev/null
+++ b/src/gui/qgsvariableeditorwidget.h
@@ -0,0 +1,205 @@
+/***************************************************************************
+ qgsvariableeditorwidget.h
+ -------------------------
+ Date : April 2015
+ Copyright : (C) 2015 by Nyall Dawson
+ Email : nyall dot dawson at gmail dot com
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef QGSVARIABLEEDITORWIDGET_H
+#define QGSVARIABLEEDITORWIDGET_H
+
+#include "qgis.h"
+#include
+#include
+#include
+
+class QTableWidget;
+class QgsVariableEditorTree;
+class VariableEditorDelegate;
+class QgsExpressionContextScope;
+class QPushButton;
+class QgsExpressionContext;
+
+
+/** \ingroup gui
+ * \class QgsVariableEditorWidget
+ * A tree based widget for editing expression context scope variables. The widget allows editing
+ * variables from a QgsExpressionContextScope, and can optionally also show inherited
+ * variables from a QgsExpressionContext.
+ * \note added in QGIS 2.12
+ */
+
+class GUI_EXPORT QgsVariableEditorWidget : public QWidget
+{
+ Q_OBJECT
+
+ Q_PROPERTY( QString settingGroup READ settingGroup WRITE setSettingGroup )
+
+ public:
+
+ /** Constructor for QgsVariableEditorWidget.
+ * @param parent parent widget
+ */
+ QgsVariableEditorWidget( QWidget *parent = 0 );
+
+ ~QgsVariableEditorWidget();
+
+ /** Overwrites the QgsExpressionContext for the widget. Setting a context
+ * allows the widget to show all inherited variables for the context,
+ * and highlight any overriden variables within scopes.
+ * @param context expression context
+ * @see context()
+ */
+ void setContext( QgsExpressionContext* context );
+
+ /** Returns the current expression context for the widget. QgsVariableEditorWidget widgets
+ * are created with an empty context by default.
+ * @see setContext()
+ */
+ QgsExpressionContext* context() const { return mContext.data(); }
+
+ /** Reloads all scopes from the editor's current context. This method should be called
+ * after adding or removing scopes from the attached context.
+ * @see context()
+ */
+ void reloadContext();
+
+ /** Sets the editable scope for the widget. Only variables from the editable scope can
+ * be modified by users.
+ * @param scopeIndex index of current editable scope. Set to -1 to disable
+ * editing and make the widget read-only.
+ * @see editableScope()
+ */
+ void setEditableScopeIndex( int scopeIndex );
+
+ /** Returns the current editable scope for the widget.
+ * @returns editable scope, or 0 if no editable scope is set
+ * @see setEditableScopeIndex()
+ */
+ QgsExpressionContextScope* editableScope() const;
+
+ /** Sets the setting group for the widget. QgsVariableEditorWidget widgets with
+ * the same setting group will synchronise their settings, eg the size
+ * of columns in the tree widget.
+ * @param group setting group
+ * @see settingGroup()
+ */
+ void setSettingGroup( const QString &group ) { mSettingGroup = group; }
+
+ /** Returns the setting group for the widget. QgsVariableEditorWidget widgets with
+ * the same setting group will synchronise their settings, eg the size
+ * of columns in the tree widget.
+ * @returns setting group name
+ * @see setSettingGroup()
+ */
+ QString settingGroup() const { return mSettingGroup; }
+
+ /** Returns a map variables set within the editable scope. Read only variables are not
+ * returned. This method can be used to retrieve the variables edited an added by
+ * users via the widget.
+ */
+ QgsStringMap variablesInActiveScope() const;
+
+ signals:
+
+ /** Emitted when the user has modified a scope using the widget.
+ */
+ void scopeChanged();
+
+ protected:
+
+ void showEvent( QShowEvent *event ) override;
+
+ private:
+
+ QScopedPointer mContext;
+ int mEditableScopeIndex;
+ QgsVariableEditorTree* mTreeWidget;
+ QPushButton* mAddButton;
+ QPushButton* mRemoveButton;
+ QString mSettingGroup;
+ bool mShown;
+
+ QString saveKey() const;
+
+ private slots:
+
+ void on_mAddButton_clicked();
+ void on_mRemoveButton_clicked();
+ void selectionChanged();
+
+};
+
+
+/// @cond
+
+/* QgsVariableEditorTree is NOT part of the public QGIS api. It's only
+ * public here as Qt meta objects can't be nested classes
+ */
+
+class QgsVariableEditorTree : public QTreeWidget
+{
+ Q_OBJECT
+
+ public:
+
+ enum VaribleRoles
+ {
+ ContextIndex = Qt::UserRole,
+ RowBaseColor
+ };
+
+ QgsVariableEditorTree( QWidget *parent = 0 );
+
+ QTreeWidgetItem *indexToItem( const QModelIndex &index ) const { return itemFromIndex( index ); }
+ QModelIndex itemToIndex( QTreeWidgetItem* item ) const { return indexFromItem( item ); }
+ QString variableNameFromItem( QTreeWidgetItem* item ) const { return item ? item->text( 0 ) : QString(); }
+ QString variableNameFromIndex( const QModelIndex& index ) const { return variableNameFromItem( itemFromIndex( index ) ); }
+ QgsExpressionContextScope* scopeFromItem( QTreeWidgetItem* item ) const;
+ QTreeWidgetItem* itemFromVariable( QgsExpressionContextScope* scope, const QString& name ) const;
+ void setEditableScopeIndex( int scopeIndex ) { mEditableScopeIndex = scopeIndex; }
+ QgsExpressionContextScope* editableScope();
+ void setContext( QgsExpressionContext* context ) { mContext = context; }
+ void refreshTree();
+ void removeItem( QTreeWidgetItem* item );
+ void renameItem( QTreeWidgetItem *item, const QString &name );
+ void resetTree();
+ void emitChanged();
+
+ signals:
+
+ void scopeChanged();
+
+ protected:
+ void keyPressEvent( QKeyEvent *event ) override;
+ void mousePressEvent( QMouseEvent *event ) override;
+ void drawRow( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const override;
+ QColor rowColor( int index ) const;
+ void toggleContextExpanded( QTreeWidgetItem *item );
+ void editNext( const QModelIndex &index );
+
+ static QIcon mExpandIcon;
+
+ private:
+
+ VariableEditorDelegate* mEditorDelegate;
+ int mEditableScopeIndex;
+ QgsExpressionContext* mContext;
+ QMap< QPair, QTreeWidgetItem* > mVariableToItem;
+ QMap< int, QTreeWidgetItem* > mScopeToItem;
+
+ void refreshScopeItems( QgsExpressionContextScope* scope, int scopeIndex );
+ void refreshScopeVariables( QgsExpressionContextScope* scope, int scopeIndex );
+};
+
+/// @endcond
+
+#endif //QGSVARIABLEEDITORWIDGET_H
diff --git a/src/ui/qgsoptionsbase.ui b/src/ui/qgsoptionsbase.ui
index 3de997ceba9..555a705443d 100644
--- a/src/ui/qgsoptionsbase.ui
+++ b/src/ui/qgsoptionsbase.ui
@@ -248,6 +248,15 @@
:/images/themes/default/propertyicons/network_and_proxy.png:/images/themes/default/propertyicons/network_and_proxy.png
+ -
+
+ Variables
+
+
+
+ :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg
+
+
@@ -4826,6 +4835,26 @@
+
+
+ -
+
+
+ Variables
+
+
+
-
+
+
+ globalOptions
+
+
+
+
+
+
+
+
@@ -4911,6 +4940,12 @@
qgsprojectionselectionwidget.h
1
+
+ QgsVariableEditorWidget
+ QWidget
+ qgsvariableeditorwidget.h
+ 1
+
mOptionsListWidget
diff --git a/src/ui/qgsprojectpropertiesbase.ui b/src/ui/qgsprojectpropertiesbase.ui
index a49843c3c22..6a80d13ae67 100644
--- a/src/ui/qgsprojectpropertiesbase.ui
+++ b/src/ui/qgsprojectpropertiesbase.ui
@@ -42,7 +42,16 @@
QFrame::Raised
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -161,6 +170,15 @@
:/images/themes/default/relation.svg:/images/themes/default/relation.svg
+ -
+
+ Variables
+
+
+
+ :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg
+
+
@@ -179,7 +197,16 @@
QFrame::Raised
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -191,11 +218,20 @@
- 4
+ 7
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -211,8 +247,8 @@
0
0
- 683
- 780
+ 656
+ 657
@@ -227,7 +263,7 @@
General settings
-
+
projgeneral
@@ -427,7 +463,7 @@
Measure tool
-
+
projgeneral
@@ -479,7 +515,7 @@
Canvas units
-
+
projgeneral
@@ -596,7 +632,7 @@
Precision
-
+
projgeneral
@@ -609,7 +645,16 @@
QFrame::Raised
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -690,7 +735,7 @@
false
-
+
projgeneral
@@ -782,7 +827,16 @@
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -798,8 +852,8 @@
0
0
- 683
- 780
+ 326
+ 46
@@ -832,7 +886,16 @@
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -848,8 +911,8 @@
0
0
- 683
- 780
+ 133
+ 100
@@ -904,7 +967,16 @@
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -920,8 +992,8 @@
0
0
- 683
- 780
+ 379
+ 582
@@ -930,7 +1002,7 @@
Default symbols
-
+
projstyles
@@ -1145,7 +1217,7 @@
Options
-
+
projstyles
@@ -1293,7 +1365,7 @@
-
-
+
-
@@ -1320,7 +1392,16 @@
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -1336,8 +1417,8 @@
0
0
- 667
- 1562
+ 542
+ 1633
@@ -1358,13 +1439,13 @@
false
-
+
false
-
+
true
-
+
projowsserver
@@ -1491,7 +1572,7 @@
WMS capabilities
-
+
projowsserver
@@ -1506,10 +1587,10 @@
false
-
+
false
-
+
true
@@ -1565,10 +1646,10 @@
false
-
+
false
-
+
true
@@ -1624,10 +1705,10 @@
false
-
+
false
-
+
true
@@ -1733,10 +1814,10 @@
false
-
+
false
-
+
true
@@ -1909,7 +1990,7 @@
WFS capabilities (also influences DXF export)
-
+
projowsserver
@@ -1995,7 +2076,7 @@
WCS capabilities
-
+
projowsserver
@@ -2071,7 +2152,16 @@
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -2087,8 +2177,8 @@
0
0
- 683
- 780
+ 168
+ 46
@@ -2124,9 +2214,38 @@
-
+
0
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+ -
+
+
+ Variables
+
+
+
-
+
+
+ projectProperties
+
+
+
+
+
+
@@ -2150,7 +2269,16 @@
QFrame::Raised
-
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
0
-
@@ -2179,6 +2307,19 @@
QgsColorButtonV2
QToolButton
+ 1
+
+
+ QgsColorSchemeList
+ QWidget
+
+ 1
+
+
+ QgsVariableEditorWidget
+ QWidget
+ qgsvariableeditorwidget.h
+ 1
QgsProjectionSelector
@@ -2192,11 +2333,6 @@
1
-
- QgsColorSchemeList
- QTreeView
-
-
scrollArea_2