From 1c7a5b2dba7118b0b636e8299ccd7bd71b257665 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 12 Aug 2015 16:10:32 +1000 Subject: [PATCH] Add editor widget for editing QgsExpressionContextScope variables --- images/images.qrc | 2 + images/themes/default/mIconCollapseSmall.svg | 78 ++ images/themes/default/mIconExpandSmall.svg | 78 ++ python/gui/gui.sip | 1 + python/gui/qgsvariableeditorwidget.sip | 90 +++ src/app/qgsoptions.cpp | 8 + src/app/qgsoptions.h | 2 + src/app/qgsprojectproperties.cpp | 9 + src/app/qgsprojectproperties.h | 1 + src/gui/CMakeLists.txt | 2 + src/gui/qgsvariableeditorwidget.cpp | 753 +++++++++++++++++++ src/gui/qgsvariableeditorwidget.h | 205 +++++ src/ui/qgsoptionsbase.ui | 35 + src/ui/qgsprojectpropertiesbase.ui | 238 ++++-- 14 files changed, 1451 insertions(+), 51 deletions(-) create mode 100644 images/themes/default/mIconCollapseSmall.svg create mode 100644 images/themes/default/mIconExpandSmall.svg create mode 100644 python/gui/qgsvariableeditorwidget.sip create mode 100644 src/gui/qgsvariableeditorwidget.cpp create mode 100644 src/gui/qgsvariableeditorwidget.h 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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + 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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + 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
qgscolorbuttonv2.h
+ 1 + + + QgsColorSchemeList + QWidget +
qgscolorschemelist.h
+ 1 +
+ + QgsVariableEditorWidget + QWidget +
qgsvariableeditorwidget.h
+ 1
QgsProjectionSelector @@ -2192,11 +2333,6 @@
qgscodeeditorpython.h
1
- - QgsColorSchemeList - QTreeView -
qgscolorschemelist.h
-
scrollArea_2