();
@@ -437,7 +677,7 @@ void QgsAttributeForm::init()
if ( !formWidget && mLayer->editFormConfig()->layout() == QgsEditFormConfig::TabLayout )
{
QTabWidget* tabWidget = new QTabWidget();
- layout()->addWidget( tabWidget );
+ layout->addWidget( tabWidget );
Q_FOREACH ( QgsAttributeEditorElement* widgDef, mLayer->editFormConfig()->tabs() )
{
@@ -481,7 +721,7 @@ void QgsAttributeForm::init()
scrollArea->setFrameShape( QFrame::NoFrame );
scrollArea->setFrameShadow( QFrame::Plain );
scrollArea->setFocusProxy( this );
- layout()->addWidget( scrollArea );
+ layout->addWidget( scrollArea );
int row = 0;
Q_FOREACH ( const QgsField& field, mLayer->fields().toList() )
@@ -504,10 +744,17 @@ void QgsAttributeForm::init()
// This will also create the widget
QWidget *l = new QLabel( fieldName );
QgsEditorWidgetWrapper* eww = QgsEditorWidgetRegistry::instance()->create( widgetType, mLayer, idx, widgetConfig, nullptr, this, mContext );
- QWidget *w = eww ? eww->widget() : new QLabel( QString( "Failed to create widget with type '%1'
" ).arg( widgetType ) );
- if ( w )
- w->setObjectName( field.name() );
+ QWidget* w = nullptr;
+ if ( eww )
+ {
+ w = new QgsAttributeFormEditorWidget( eww, this );
+ mFormEditorWidgets.insert( idx, static_cast< QgsAttributeFormEditorWidget* >( w ) );
+ }
+ else
+ {
+ w = new QLabel( QString( "Failed to create widget with type '%1'
" ).arg( widgetType ) );
+ }
if ( eww )
addWidgetWrapper( eww );
@@ -545,7 +792,7 @@ void QgsAttributeForm::init()
{
mButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
mButtonBox->setObjectName( "buttonBox" );
- layout()->addWidget( mButtonBox );
+ layout->addWidget( mButtonBox );
}
mButtonBox->setVisible( buttonBoxVisible );
@@ -701,7 +948,11 @@ QgsAttributeForm::WidgetInfo QgsAttributeForm::createWidgetFromDef( const QgsAtt
const QgsEditorWidgetConfig widgetConfig = mLayer->editFormConfig()->widgetConfig( fldIdx );
QgsEditorWidgetWrapper* eww = QgsEditorWidgetRegistry::instance()->create( widgetType, mLayer, fldIdx, widgetConfig, nullptr, this, mContext );
- newWidgetInfo.widget = eww->widget();
+
+ QWidget* w = new QgsAttributeFormEditorWidget( eww, this );
+ mFormEditorWidgets.insert( fldIdx, static_cast< QgsAttributeFormEditorWidget* >( w ) );
+
+ newWidgetInfo.widget = w;
addWidgetWrapper( eww );
newWidgetInfo.widget->setObjectName( mLayer->fields().at( fldIdx ).name() );
@@ -922,3 +1173,108 @@ bool QgsAttributeForm::eventFilter( QObject* object, QEvent* e )
return false;
}
+
+void QgsAttributeForm::scanForEqualAttributes( QgsFeatureIterator& fit, QSet< int >& mixedValueFields, QHash< int, QVariant >& fieldSharedValues ) const
+{
+ mixedValueFields.clear();
+ fieldSharedValues.clear();
+
+ QgsFeature f;
+ bool first = true;
+ while ( fit.nextFeature( f ) )
+ {
+ for ( int i = 0; i < mLayer->fields().count(); ++i )
+ {
+ if ( mixedValueFields.contains( i ) )
+ continue;
+
+ if ( first )
+ {
+ fieldSharedValues[i] = f.attribute( i );
+ }
+ else
+ {
+ if ( fieldSharedValues.value( i ) != f.attribute( i ) )
+ {
+ fieldSharedValues.remove( i );
+ mixedValueFields.insert( i );
+ }
+ }
+ }
+ first = false;
+
+ if ( mixedValueFields.count() == mLayer->fields().count() )
+ {
+ // all attributes are mixed, no need to keep scanning
+ break;
+ }
+ }
+}
+
+
+void QgsAttributeForm::layerSelectionChanged()
+{
+ switch ( mMode )
+ {
+ case SingleEditMode:
+ case AddFeatureMode:
+ break;
+
+ case MultiEditMode:
+ resetMultiEdit( true );
+ break;
+ }
+}
+
+void QgsAttributeForm::setMultiEditFeatureIds( const QgsFeatureIds& fids )
+{
+ mIsSettingMultiEditFeatures = true;
+ mMultiEditFeatureIds = fids;
+
+ if ( fids.isEmpty() )
+ {
+ // no selected features
+ QMap< int, QgsAttributeFormEditorWidget* >::const_iterator wIt = mFormEditorWidgets.constBegin();
+ for ( ; wIt != mFormEditorWidgets.constEnd(); ++ wIt )
+ {
+ wIt.value()->initialize( QVariant() );
+ }
+ mIsSettingMultiEditFeatures = false;
+ return;
+ }
+
+ QgsFeatureIterator fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFids( fids ) );
+
+ // Scan through all features to determine which attributes are initially the same
+ QSet< int > mixedValueFields;
+ QHash< int, QVariant > fieldSharedValues;
+ scanForEqualAttributes( fit, mixedValueFields, fieldSharedValues );
+
+ // also fetch just first feature
+ fit = mLayer->getFeatures( QgsFeatureRequest().setFilterFid( *fids.constBegin() ) );
+ QgsFeature firstFeature;
+ fit.nextFeature( firstFeature );
+
+ Q_FOREACH ( int field, mixedValueFields )
+ {
+ if ( QgsAttributeFormEditorWidget* w = mFormEditorWidgets.value( field, nullptr ) )
+ {
+ w->initialize( firstFeature.attribute( field ), true );
+ }
+ }
+ QHash< int, QVariant >::const_iterator sharedValueIt = fieldSharedValues.constBegin();
+ for ( ; sharedValueIt != fieldSharedValues.constEnd(); ++sharedValueIt )
+ {
+ if ( QgsAttributeFormEditorWidget* w = mFormEditorWidgets.value( sharedValueIt.key(), nullptr ) )
+ {
+ w->initialize( sharedValueIt.value(), false );
+ }
+ }
+ mIsSettingMultiEditFeatures = false;
+}
+
+int QgsAttributeForm::messageTimeout()
+{
+ QSettings settings;
+ return settings.value( "/qgis/messageTimeout", 5 ).toInt();
+}
diff --git a/src/gui/qgsattributeform.h b/src/gui/qgsattributeform.h
index d806f1f343b..e04a386d2d0 100644
--- a/src/gui/qgsattributeform.h
+++ b/src/gui/qgsattributeform.h
@@ -25,12 +25,26 @@
#include
class QgsAttributeFormInterface;
+class QgsAttributeFormEditorWidget;
+class QgsMessageBar;
+class QgsMessageBarItem;
class GUI_EXPORT QgsAttributeForm : public QWidget
{
Q_OBJECT
public:
+
+ //! Form modes
+ enum Mode
+ {
+ SingleEditMode, /*!< Single edit mode, for editing a single feature */
+ AddFeatureMode, /*!< Add feature mode, for setting attributes for a new feature. In this mode the dialog will be editable even with an invalid feature and
+ will add a new feature when the form is accepted. */
+ MultiEditMode, /*!< Multi edit mode, for editing fields of multiple features at once */
+ // TODO: SearchMode, /*!< Form values are used for searching/filtering the layer */
+ };
+
explicit QgsAttributeForm( QgsVectorLayer* vl, const QgsFeature &feature = QgsFeature(), const QgsAttributeEditorContext& context = QgsAttributeEditorContext(), QWidget *parent = nullptr );
~QgsAttributeForm();
@@ -72,14 +86,28 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
*/
bool editable();
+ /** Returns the current mode of the form.
+ * @note added in QGIS 2.16
+ * @see setMode()
+ */
+ Mode mode() const { return mMode; }
+
+ /** Sets the current mode of the form.
+ * @param mode form mode
+ * @note added in QGIS 2.16
+ * @see mode()
+ */
+ void setMode( Mode mode );
+
/**
* Toggles the form mode between edit feature and add feature.
* If set to true, the dialog will be editable even with an invalid feature.
* If set to true, the dialog will add a new feature when the form is accepted.
*
* @param isAddDialog If set to true, turn this dialog into an add feature dialog.
+ * @deprecated use setMode() instead
*/
- void setIsAddDialog( bool isAddDialog );
+ Q_DECL_DEPRECATED void setIsAddDialog( bool isAddDialog );
/**
* Sets the edit command message (Undo) that will be used when the dialog is accepted
@@ -98,6 +126,12 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
*/
bool eventFilter( QObject* object, QEvent* event ) override;
+ /** Sets all feature IDs which are to be edited if the form is in multiedit mode
+ * @param fids feature ID list
+ * @note added in QGIS 2.16
+ */
+ void setMultiEditFeatureIds( const QgsFeatureIds& fids );
+
signals:
/**
* Notifies about changes of attributes
@@ -177,6 +211,13 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
void preventFeatureRefresh();
void synchronizeEnabledState();
+ void layerSelectionChanged();
+
+ //! Save multi edit changes
+ bool saveMultiEdits();
+ void resetMultiEdit( bool promptToSave = false );
+ void multiEditMessageClicked( const QString& link );
+
private:
void init();
@@ -210,12 +251,24 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
void createWrappers();
void connectWrappers();
+ void scanForEqualAttributes( QgsFeatureIterator& fit, QSet< int >& mixedValueFields, QHash< int, QVariant >& fieldSharedValues ) const;
+
+ //! Save single feature or add feature edits
+ bool saveEdits();
+
+ int messageTimeout();
+ void clearMultiEditMessages();
+
QgsVectorLayer* mLayer;
QgsFeature mFeature;
+ QgsMessageBar* mMessageBar;
+ QgsMessageBarItem* mMultiEditUnsavedMessageBarItem;
+ QgsMessageBarItem* mMultiEditMessageBarItem;
QList mWidgets;
QgsAttributeEditorContext mContext;
QDialogButtonBox* mButtonBox;
QList mInterfaces;
+ QMap< int, QgsAttributeFormEditorWidget* > mFormEditorWidgets;
// Variables below are used for python
static int sFormCounter;
@@ -224,12 +277,22 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
//! Set to true while saving to prevent recursive saves
bool mIsSaving;
- bool mIsAddDialog;
//! Flag to prevent refreshFeature() to change mFeature
bool mPreventFeatureRefresh;
+ //! Set to true while setting feature to prevent attributeChanged signal
+ bool mIsSettingFeature;
+ bool mIsSettingMultiEditFeatures;
+
+ QgsFeatureIds mMultiEditFeatureIds;
+
QString mEditCommandMessage;
+
+ Mode mMode;
+
+ friend class TestQgsDualView;
};
#endif // QGSATTRIBUTEFORM_H
+
diff --git a/src/gui/qgsattributeformeditorwidget.cpp b/src/gui/qgsattributeformeditorwidget.cpp
new file mode 100644
index 00000000000..6b202be3a4d
--- /dev/null
+++ b/src/gui/qgsattributeformeditorwidget.cpp
@@ -0,0 +1,173 @@
+/***************************************************************************
+ qgsattributeformeditorwidget.cpp
+ -------------------------------
+ Date : March 2016
+ Copyright : (C) 2016 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 "qgsattributeformeditorwidget.h"
+#include "qgsattributeform.h"
+#include "qgsmultiedittoolbutton.h"
+#include "qgseditorwidgetwrapper.h"
+#include
+#include
+
+QgsAttributeFormEditorWidget::QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, QgsAttributeForm* form )
+ : QWidget( form )
+ , mWidget( editorWidget )
+ , mForm( form )
+ , mMode( DefaultMode )
+ , mMultiEditButton( new QgsMultiEditToolButton() )
+ , mBlockValueUpdate( false )
+ , mIsMixed( false )
+ , mIsChanged( false )
+{
+ if ( !mWidget || !mForm )
+ return;
+
+ QLayout* l = new QHBoxLayout();
+ l->setMargin( 0 );
+ l->setContentsMargins( 0, 0, 0, 0 );
+ l->addWidget( mWidget->widget() );
+
+ if ( mWidget->widget() )
+ {
+ mWidget->widget()->setObjectName( mWidget->field().name() );
+ }
+ connect( mWidget, SIGNAL( valueChanged( const QVariant& ) ), this, SLOT( editorWidgetChanged( const QVariant & ) ) );
+ connect( mMultiEditButton, SIGNAL( resetFieldValueTriggered() ), this, SLOT( resetValue() ) );
+ connect( mMultiEditButton, SIGNAL( setFieldValueTriggered() ), this, SLOT( setFieldTriggered() ) );
+
+ mMultiEditButton->setField( mWidget->field() );
+
+ setLayout( l );
+ updateWidgets();
+}
+
+QgsAttributeFormEditorWidget::~QgsAttributeFormEditorWidget()
+{
+ //there's a chance these widgets are not currently added to the layout, so have no parent set
+ delete mMultiEditButton;
+}
+
+void QgsAttributeFormEditorWidget::setMode( QgsAttributeFormEditorWidget::Mode mode )
+{
+ mMode = mode;
+ updateWidgets();
+}
+
+void QgsAttributeFormEditorWidget::setIsMixed( bool mixed )
+{
+ if ( mixed )
+ mWidget->showIndeterminateState( );
+ mMultiEditButton->setIsMixed( mixed );
+ mIsMixed = mixed;
+}
+
+void QgsAttributeFormEditorWidget::changesCommitted()
+{
+ if ( mWidget )
+ mPreviousValue = mWidget->value();
+
+ setIsMixed( false );
+ mMultiEditButton->changesCommitted();
+ mIsChanged = false;
+}
+
+void QgsAttributeFormEditorWidget::initialize( const QVariant& initialValue, bool mixedValues )
+{
+ if ( mWidget )
+ {
+ mBlockValueUpdate = true;
+ mWidget->setValue( initialValue );
+ mBlockValueUpdate = false;
+ }
+ mPreviousValue = initialValue;
+ setIsMixed( mixedValues );
+ mMultiEditButton->setIsChanged( false );
+ mIsChanged = false;
+}
+
+QVariant QgsAttributeFormEditorWidget::currentValue() const
+{
+ return mWidget->value();
+}
+
+void QgsAttributeFormEditorWidget::editorWidgetChanged( const QVariant& value )
+{
+ if ( mBlockValueUpdate )
+ return;
+
+ mIsChanged = true;
+
+ switch ( mMode )
+ {
+ case DefaultMode:
+ break;
+ case MultiEditMode:
+ mMultiEditButton->setIsChanged( true );
+ }
+
+ emit valueChanged( value );
+}
+
+void QgsAttributeFormEditorWidget::resetValue()
+{
+ mIsChanged = false;
+ mBlockValueUpdate = true;
+ mWidget->setValue( mPreviousValue );
+ mBlockValueUpdate = false;
+
+ switch ( mMode )
+ {
+ case DefaultMode:
+ break;
+ case MultiEditMode:
+ {
+ mMultiEditButton->setIsChanged( false );
+ if ( mWidget && mIsMixed )
+ mWidget->showIndeterminateState();
+ break;
+ }
+ }
+}
+
+void QgsAttributeFormEditorWidget::setFieldTriggered()
+{
+ mIsChanged = true;
+}
+
+QgsVectorLayer* QgsAttributeFormEditorWidget::layer()
+{
+ return mForm ? mForm->layer() : nullptr;
+}
+
+void QgsAttributeFormEditorWidget::updateWidgets()
+{
+ bool hasMultiEditButton = ( layout()->indexOf( mMultiEditButton ) >= 0 );
+ bool fieldReadOnly = layer()->editFormConfig()->readOnly( mWidget->fieldIdx() );
+
+ if ( hasMultiEditButton )
+ {
+ if ( mMode != MultiEditMode || fieldReadOnly )
+ {
+ layout()->removeWidget( mMultiEditButton );
+ mMultiEditButton->setParent( nullptr );
+ }
+ }
+ else
+ {
+ if ( mMode == MultiEditMode && !fieldReadOnly )
+ {
+ layout()->addWidget( mMultiEditButton );
+ }
+ }
+}
diff --git a/src/gui/qgsattributeformeditorwidget.h b/src/gui/qgsattributeformeditorwidget.h
new file mode 100644
index 00000000000..917f8f0e91c
--- /dev/null
+++ b/src/gui/qgsattributeformeditorwidget.h
@@ -0,0 +1,130 @@
+/***************************************************************************
+ qgsattributeformeditorwidget.h
+ -----------------------------
+ Date : March 2016
+ Copyright : (C) 2016 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 QGSATTRIBUTEFORMEDITORWIDGET_H
+#define QGSATTRIBUTEFORMEDITORWIDGET_H
+
+#include
+#include
+
+class QgsAttributeForm;
+class QgsEditorWidgetWrapper;
+class QgsMultiEditToolButton;
+class QgsVectorLayer;
+
+
+/** \ingroup gui
+ * \class QgsAttributeFormEditorWidget
+ * A widget consisting of both an editor widget and additional widgets for controlling the behaviour
+ * of the editor widget depending on a number of possible modes. For instance, if the parent attribute
+ * form is in the multi edit mode, this widget will show both the editor widget and a tool button for
+ * controlling the multi edit results.
+ * \note Added in version 2.16
+ */
+class GUI_EXPORT QgsAttributeFormEditorWidget : public QWidget
+{
+ Q_OBJECT
+
+ public:
+
+ //! Widget modes
+ enum Mode
+ {
+ DefaultMode, /*!< Default mode, only the editor widget is shown */
+ MultiEditMode, /*!< Multi edit mode, both the editor widget and a QgsMultiEditToolButton is shown */
+ // TODO: SearchMode, /*!< Layer search/filter mode */
+ };
+
+ /** Constructor for QgsAttributeFormEditorWidget.
+ * @param editorWidget associated editor widget wrapper
+ * @param form parent attribute form
+ */
+ explicit QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, QgsAttributeForm* form );
+
+ ~QgsAttributeFormEditorWidget();
+
+ /** Sets the current mode for the widget. The widget will adapt its state and visible widgets to
+ * reflect the updated mode. Eg, showing multi edit tool buttons if the mode is set to MultiEditMode.
+ * @param mode widget mode
+ * @see mode()
+ */
+ void setMode( Mode mode );
+
+ /** Returns the current mode for the widget.
+ * @see setMode()
+ */
+ Mode mode() const { return mMode; }
+
+ /** Resets the widget to an initial value.
+ * @param initialValue initial value to show in widget
+ * @param mixedValue set to true to initially show the mixed values state
+ */
+ void initialize( const QVariant& initialValue, bool mixedValues = false );
+
+ /** Returns true if the widget's value has been changed since it was initialized.
+ * @see initialize()
+ */
+ bool hasChanged() const { return mIsChanged; }
+
+ /** Returns the current value of the attached editor widget.
+ */
+ QVariant currentValue() const;
+
+ public slots:
+
+ /** Sets whether the widget should be displayed in a "mixed values" mode.
+ * @param mixed set to true to show in a mixed values state
+ */
+ void setIsMixed( bool mixed );
+
+ /** Called when field values have been committed;
+ */
+ void changesCommitted();
+
+ signals:
+
+ //! Emitted when the widget's value changes
+ //! @param value new widget value
+ void valueChanged( const QVariant& value );
+
+ private slots:
+
+ //! Triggered when editor widget's value changes
+ void editorWidgetChanged( const QVariant& value );
+
+ //! Triggered when multi edit tool button requests value reset
+ void resetValue();
+
+ //! Triggered when the multi edit tool button "set field value" action is selected
+ void setFieldTriggered();
+
+ private:
+
+ QgsEditorWidgetWrapper* mWidget;
+ QgsAttributeForm* mForm;
+ Mode mMode;
+
+ QgsMultiEditToolButton* mMultiEditButton;
+ QVariant mPreviousValue;
+ bool mBlockValueUpdate;
+ bool mIsMixed;
+ bool mIsChanged;
+
+
+ QgsVectorLayer* layer();
+ void updateWidgets();
+};
+
+#endif // QGSATTRIBUTEFORMEDITORWIDGET_H
diff --git a/src/ui/qgsattributetabledialog.ui b/src/ui/qgsattributetabledialog.ui
index 48989388fbe..024ad31a5e8 100644
--- a/src/ui/qgsattributetabledialog.ui
+++ b/src/ui/qgsattributetabledialog.ui
@@ -75,6 +75,35 @@
+ -
+
+
+ Toggle multi edit mode
+
+
+
+
+
+
+
+
+
+ :/images/themes/default/mActionAllEdits.svg:/images/themes/default/mActionAllEdits.svg
+
+
+
+ 18
+ 18
+
+
+
+ true
+
+
+ true
+
+
+
-
@@ -858,6 +887,7 @@
mToggleEditingButton
+ mToggleMultiEditButton
mSaveEditsButton
mReloadButton
mAddFeature
diff --git a/tests/src/gui/CMakeLists.txt b/tests/src/gui/CMakeLists.txt
index 4da57a55bb9..b44a0b51a30 100644
--- a/tests/src/gui/CMakeLists.txt
+++ b/tests/src/gui/CMakeLists.txt
@@ -10,6 +10,8 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}/../../../src/ui
${CMAKE_CURRENT_SOURCE_DIR}/../core #for render checker class
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/editorwidgets
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/editorwidgets/core
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/symbology-ng
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/raster
${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core
diff --git a/tests/src/gui/testqgsdualview.cpp b/tests/src/gui/testqgsdualview.cpp
index 7cfb9df1726..723f12ee5e2 100644
--- a/tests/src/gui/testqgsdualview.cpp
+++ b/tests/src/gui/testqgsdualview.cpp
@@ -19,8 +19,10 @@
#include
#include
#include
+#include "qgsattributeform.h"
#include
#include
+#include "qgsvectordataprovider.h"
#include
#include
@@ -42,6 +44,8 @@ class TestQgsDualView : public QObject
void testSelectAll();
+ void testAttributeFormSharedValueScanning();
+
private:
QgsMapCanvas* mCanvas;
QgsVectorLayer* mPointsLayer;
@@ -103,6 +107,72 @@ void TestQgsDualView::testSelectAll()
QVERIFY( mPointsLayer->selectedFeatureCount() == 1 );
}
+void TestQgsDualView::testAttributeFormSharedValueScanning()
+{
+ // test QgsAttributeForm::scanForEqualAttributes
+
+ QSet< int > mixedValueFields;
+ QHash< int, QVariant > fieldSharedValues;
+
+ // make a temporary layer to check through
+ QgsVectorLayer* layer = new QgsVectorLayer( "Point?field=col1:integer&field=col2:integer&field=col3:integer&field=col4:integer", "test", "memory" );
+ QVERIFY( layer->isValid() );
+ QgsFeature f1( layer->dataProvider()->fields(), 1 );
+ f1.setAttribute( "col1", 1 );
+ f1.setAttribute( "col2", 1 );
+ f1.setAttribute( "col3", 3 );
+ f1.setAttribute( "col4", 1 );
+ QgsFeature f2( layer->dataProvider()->fields(), 2 );
+ f2.setAttribute( "col1", 1 );
+ f2.setAttribute( "col2", 2 );
+ f2.setAttribute( "col3", 3 );
+ f2.setAttribute( "col4", 2 );
+ QgsFeature f3( layer->dataProvider()->fields(), 3 );
+ f3.setAttribute( "col1", 1 );
+ f3.setAttribute( "col2", 2 );
+ f3.setAttribute( "col3", 3 );
+ f3.setAttribute( "col4", 2 );
+ QgsFeature f4( layer->dataProvider()->fields(), 4 );
+ f4.setAttribute( "col1", 1 );
+ f4.setAttribute( "col2", 1 );
+ f4.setAttribute( "col3", 3 );
+ f4.setAttribute( "col4", 2 );
+ layer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 << f4 );
+
+ QgsAttributeForm form( layer );
+
+ QgsFeatureIterator it = layer->getFeatures();
+
+ form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
+
+ QCOMPARE( mixedValueFields, QSet< int >() << 1 << 3 );
+ QCOMPARE( fieldSharedValues.value( 0 ).toInt(), 1 );
+ QCOMPARE( fieldSharedValues.value( 2 ).toInt(), 3 );
+
+ // add another feature so all attributes are different
+ QgsFeature f5( layer->dataProvider()->fields(), 5 );
+ f5.setAttribute( "col1", 11 );
+ f5.setAttribute( "col2", 11 );
+ f5.setAttribute( "col3", 13 );
+ f5.setAttribute( "col4", 12 );
+ layer->dataProvider()->addFeatures( QgsFeatureList() << f5 );
+
+ it = layer->getFeatures();
+
+ form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
+ QCOMPARE( mixedValueFields, QSet< int >() << 0 << 1 << 2 << 3 );
+ QVERIFY( fieldSharedValues.isEmpty() );
+
+ // single feature, all attributes should be shared
+ it = layer->getFeatures( QgsFeatureRequest().setFilterFid( 4 ) );
+ form.scanForEqualAttributes( it, mixedValueFields, fieldSharedValues );
+ QCOMPARE( fieldSharedValues.value( 0 ).toInt(), 1 );
+ QCOMPARE( fieldSharedValues.value( 1 ).toInt(), 1 );
+ QCOMPARE( fieldSharedValues.value( 2 ).toInt(), 3 );
+ QCOMPARE( fieldSharedValues.value( 3 ).toInt(), 2 );
+ QVERIFY( mixedValueFields.isEmpty() );
+}
+
QTEST_MAIN( TestQgsDualView )
#include "testqgsdualview.moc"
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt
index b1623e0f035..dd909b03844 100644
--- a/tests/src/python/CMakeLists.txt
+++ b/tests/src/python/CMakeLists.txt
@@ -43,6 +43,7 @@ ADD_PYTHON_TEST(PyQgsGeometryTest test_qgsgeometry.py)
ADD_PYTHON_TEST(PyQgsGraduatedSymbolRendererV2 test_qgsgraduatedsymbolrendererv2.py)
ADD_PYTHON_TEST(PyQgsMapUnitScale test_qgsmapunitscale.py)
ADD_PYTHON_TEST(PyQgsMemoryProvider test_provider_memory.py)
+ADD_PYTHON_TEST(PyQgsMultiEditToolButton test_qgsmultiedittoolbutton.py)
ADD_PYTHON_TEST(PyQgsNetworkContentFetcher test_qgsnetworkcontentfetcher.py)
ADD_PYTHON_TEST(PyQgsNullSymbolRenderer test_qgsnullsymbolrenderer.py)
ADD_PYTHON_TEST(PyQgsPalLabelingBase test_qgspallabeling_base.py)
diff --git a/tests/src/python/test_qgsmultiedittoolbutton.py b/tests/src/python/test_qgsmultiedittoolbutton.py
new file mode 100644
index 00000000000..da057d2431c
--- /dev/null
+++ b/tests/src/python/test_qgsmultiedittoolbutton.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+"""QGIS Unit tests for QgsMultiEditToolButton.
+
+.. note:: 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.
+"""
+__author__ = 'Nyall Dawson'
+__date__ = '16/03/2016'
+__copyright__ = 'Copyright 2016, The QGIS Project'
+# This will get replaced with a git SHA1 when you do a git archive
+__revision__ = '$Format:%H$'
+
+import qgis # switch sip api
+
+from qgis.gui import QgsMultiEditToolButton
+
+from qgis.testing import (start_app,
+ unittest
+ )
+
+start_app()
+
+
+class TestQgsMultiEditToolButton(unittest.TestCase):
+
+ def test_state_logic(self):
+ """
+ Test that the logic involving button states is correct
+ """
+ w = QgsMultiEditToolButton()
+ self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
+
+ # set is changed should update state to changed
+ w.setIsChanged(True)
+ self.assertEqual(w.state(), QgsMultiEditToolButton.Changed)
+ w.setIsChanged(False)
+ self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
+ #resetting changes should fall back to default state
+ w.setIsChanged(True)
+ w.resetChanges()
+ self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
+ #setting changes committed should result in default state
+ w.setIsChanged(True)
+ w.changesCommitted()
+ self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
+
+ #Test with mixed values
+ w.setIsMixed(True)
+ self.assertEqual(w.state(), QgsMultiEditToolButton.MixedValues)
+ #changed state takes priority over mixed state
+ w.setIsChanged(True)
+ self.assertEqual(w.state(), QgsMultiEditToolButton.Changed)
+ w.setIsChanged(False)
+ #should reset to mixed state
+ self.assertEqual(w.state(), QgsMultiEditToolButton.MixedValues)
+ #resetting changes should fall back to mixed state
+ w.setIsChanged(True)
+ w.resetChanges()
+ self.assertEqual(w.state(), QgsMultiEditToolButton.MixedValues)
+ #setting changes committed should result in default state
+ w.setIsChanged(True)
+ w.changesCommitted()
+ self.assertEqual(w.state(), QgsMultiEditToolButton.Default)
+
+if __name__ == '__main__':
+ unittest.main()