diff --git a/python/gui/attributetable/qgsdualview.sip b/python/gui/attributetable/qgsdualview.sip index d53eb1d23a9..f58919e6b56 100644 --- a/python/gui/attributetable/qgsdualview.sip +++ b/python/gui/attributetable/qgsdualview.sip @@ -183,6 +183,12 @@ class QgsDualView : QStackedWidget */ void setMultiEditEnabled( bool enabled ); + /** Toggles whether search mode should be enabled in the form. + * @param enabled set to true to switch on search mode + * @note added in QGIS 2.16 + */ + void toggleSearchMode( bool enabled ); + signals: /** * Is emitted, whenever the display expression is successfully changed @@ -194,6 +200,19 @@ class QgsDualView : QStackedWidget * Is emitted, whenever the filter changes */ void filterChanged(); + + /** Is emitted when a filter expression is set using the view. + * @param expression filter expression + * @param type filter type + * @note added in QGIS 2.16 + */ + void filterExpressionSet( const QString& expression, QgsAttributeForm::FilterType type ); + + /** Emitted when the form changes mode. + * @param mode new mode + */ + void formModeChanged( QgsAttributeForm::Mode mode ); + }; class QgsAttributeTableAction : QAction diff --git a/python/gui/editorwidgets/core/qgssearchwidgetwrapper.sip b/python/gui/editorwidgets/core/qgssearchwidgetwrapper.sip index 5bd84485074..a2d8f8685e5 100644 --- a/python/gui/editorwidgets/core/qgssearchwidgetwrapper.sip +++ b/python/gui/editorwidgets/core/qgssearchwidgetwrapper.sip @@ -1,3 +1,50 @@ +%MappedType QList +{ +%TypeHeaderCode +#include +%End + +%ConvertFromTypeCode + // Create the list. + PyObject *l; + + if ((l = PyList_New(sipCpp->size())) == NULL) + return NULL; + + // Set the list elements. + QList::iterator it = sipCpp->begin(); + for (int i = 0; it != sipCpp->end(); ++it, ++i) + { + PyObject *tobj; + + if ((tobj = sipConvertFromEnum(*it, sipType_QgsSearchWidgetWrapper_FilterFlag)) == NULL) + { + Py_DECREF(l); + return NULL; + } + PyList_SET_ITEM(l, i, tobj); + } + + return l; +%End + +%ConvertToTypeCode + // Check the type if that is all that is required. + if (sipIsErr == NULL) + return PyList_Check(sipPy); + + QList *qlist = new QList; + + for (int i = 0; i < PyList_GET_SIZE(sipPy); ++i) + { + *qlist << (QgsSearchWidgetWrapper::FilterFlag)SIPLong_AsLong(PyList_GET_ITEM(sipPy, i)); + } + + *sipCppPtr = qlist; + return sipGetState(sipTransferObj); +%End +}; + /** * Manages an editor widget * Widget and wrapper share the same parent @@ -14,6 +61,43 @@ class QgsSearchWidgetWrapper : QgsWidgetWrapper #include %End public: + + //! Flags which indicate what types of filtering and searching is possible using the widget + //! @note added in QGIS 2.16 + enum FilterFlag + { + EqualTo, /*!< Supports equal to */ + NotEqualTo, /*!< Supports not equal to */ + GreaterThan, /*!< Supports greater than */ + LessThan, /*!< Supports less than */ + GreaterThanOrEqualTo, /*!< Supports >= */ + LessThanOrEqualTo, /*!< Supports <= */ + Between, /*!< Supports searches between two values */ + CaseInsensitive, /*!< Supports case insensitive searching */ + Contains, /*!< Supports value "contains" searching */ + DoesNotContain, /*!< Supports value does not contain searching */ + IsNull, /*!< Supports searching for null values */ + }; + typedef QFlags FilterFlags; + + /** Returns a list of exclusive filter flags, which cannot be combined with other flags (eg EqualTo/NotEqualTo) + * @note added in QGIS 2.16 + * @see nonExclusiveFilterFlags() + */ + static QList< QgsSearchWidgetWrapper::FilterFlag > exclusiveFilterFlags(); + + /** Returns a list of non-exclusive filter flags, which can be combined with other flags (eg CaseInsensitive) + * @note added in QGIS 2.16 + * @see exclusiveFilterFlags() + */ + static QList< QgsSearchWidgetWrapper::FilterFlag > nonExclusiveFilterFlags(); + + /** Returns a translated string representing a filter flag. + * @param flag flag to convert to string + * @note added in QGIS 2.16 + */ + static QString toString( FilterFlag flag ); + /** * Create a new widget wrapper * @@ -23,6 +107,18 @@ class QgsSearchWidgetWrapper : QgsWidgetWrapper */ explicit QgsSearchWidgetWrapper( QgsVectorLayer* vl, int fieldIdx, QWidget* parent /TransferThis/ = nullptr ); + /** Returns filter flags supported by the search widget. + * @note added in QGIS 2.16 + * @see defaultFlags() + */ + virtual FilterFlags supportedFlags() const; + + /** Returns the filter flags which should be set by default for the search widget. + * @note added in QGIS 2.16 + * @see supportedFlags() + */ + virtual FilterFlags defaultFlags() const; + /** * Will be used to access the widget's value. Read the value from the widget and * return it properly formatted to be saved in the attribute. @@ -39,6 +135,28 @@ class QgsSearchWidgetWrapper : QgsWidgetWrapper */ virtual bool applyDirectly() = 0; + /** Creates a filter expression based on the current state of the search widget + * and the specified filter flags. + * @param flags filter flags + * @returns filter expression + * @note added in QGIS 2.16 + */ + // TODO QGIS 3.0 - make pure virtual + virtual QString createExpression( FilterFlags flags ) const; + + public slots: + + /** Clears the widget's current value and resets it back to the default state + * @note added in QGIS 2.16 + */ + virtual void clearWidget(); + + /** Toggles whether the search widget is enabled or disabled. + * @param enabled set to true to enable widget + * @note added in QGIS 2.16 + */ + virtual void setEnabled( bool enabled ); + signals: /** @@ -47,6 +165,17 @@ class QgsSearchWidgetWrapper : QgsWidgetWrapper */ void expressionChanged( const QString& exp ); + /** Emitted when a user changes the value of the search widget. + * @note added in QGIS 2.16 + */ + void valueChanged(); + + /** Emitted when a user changes the value of the search widget back + * to an empty, default state. + * @note added in QGIS 2.16 + */ + void valueCleared(); + protected slots: virtual void setExpression( QString value ) = 0; @@ -57,3 +186,5 @@ class QgsSearchWidgetWrapper : QgsWidgetWrapper void clearExpression(); }; + +QFlags operator|(QgsSearchWidgetWrapper::FilterFlag f1, QFlags f2); diff --git a/python/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.sip b/python/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.sip new file mode 100644 index 00000000000..87d3d3aec1a --- /dev/null +++ b/python/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.sip @@ -0,0 +1,46 @@ +/** + * Wraps a search widget. Default form is just a QgsLineFilterEdit + */ +class QgsDefaultSearchWidgetWrapper : QgsSearchWidgetWrapper +{ +%TypeHeaderCode +#include +%End + public: + + explicit QgsDefaultSearchWidgetWrapper( QgsVectorLayer* vl, int fieldIdx, QWidget* parent /TransferThis/ = nullptr ); + + // QgsSearchWidgetWrapper interface + public: + QString expression(); + bool applyDirectly(); + FilterFlags supportedFlags() const; + FilterFlags defaultFlags() const; + virtual QString createExpression( FilterFlags flags ) const; + + public slots: + + virtual void clearWidget(); + virtual void setEnabled( bool enabled ); + + protected slots: + void setExpression( QString exp ); + + protected: + QWidget* createWidget( QWidget* parent ); + void initWidget( QWidget* editor ); + bool valid() const; + + /** Returns a pointer to the line edit part of the widget. + * @note this method is in place for unit testing only, and is not considered + * stable API + */ + QgsFilterLineEdit* lineEdit(); + + /** Returns a pointer to the case sensitivity check box in the widget. + * @note this method is in place for unit testing only, and is not considered + * stable API + */ + QCheckBox* caseSensitiveCheckBox(); + +}; diff --git a/python/gui/editorwidgets/qgssearchwidgettoolbutton.sip b/python/gui/editorwidgets/qgssearchwidgettoolbutton.sip new file mode 100644 index 00000000000..eb832531ccf --- /dev/null +++ b/python/gui/editorwidgets/qgssearchwidgettoolbutton.sip @@ -0,0 +1,90 @@ +/** \ingroup gui + * \class QgsSearchWidgetToolButton + * A tool button widget which is displayed next to search widgets in forms, and + * allows for controlling how the widget behaves and how the filtering/searching + * operates. + * \note Added in version 2.16 + */ +class QgsSearchWidgetToolButton : QToolButton +{ +%TypeHeaderCode +#include +%End + public: + + /** Constructor for QgsSearchWidgetToolButton. + * @param parent parent object + */ + explicit QgsSearchWidgetToolButton( QWidget *parent /TransferThis/ = nullptr ); + + /** Sets the search widget wrapper associated with this button. + * Calling this will automatically set the available flags to match those + * supported by the wrapper and reset the active flags to match the wrapper's + * default flags. + * @param wrapper search wrapper. Ownership is not transferred. + */ + void setSearchWidgetWrapper( QgsSearchWidgetWrapper* wrapper ); + + /** Sets the available filter flags to show in the widget. Any active flags + * (see activeFlags()) which are not present in the new available filter + * flags will be cleared; + * @param flags available flags to show in widget + * @see availableFlags() + * @see setActiveFlags() + */ + void setAvailableFlags( QgsSearchWidgetWrapper::FilterFlags flags ); + + /** Returns the available filter flags shown in the widget. + * @see setAvailableFlags() + * @see activeFlags() + */ + QgsSearchWidgetWrapper::FilterFlags availableFlags() const; + + /** Sets the current active filter flags for the widget. Any flags + * which are not present in the available filter flags (see availableFlags()) + * will not be set. + * @param flags active flags to show in widget + * @see toggleFlag() + * @see activeFlags() + * @see setAvailableFlags() + */ + void setActiveFlags( QgsSearchWidgetWrapper::FilterFlags flags ); + + /** Toggles an individual active filter flag for the widget. Any flags + * which are not present in the available filter flags (see availableFlags()) + * will be ignore. Other flags may be cleared if they conflict with the newly + * toggled flag. + * @param flag flag to toggle + * @see setActiveFlags() + * @see activeFlags() + */ + void toggleFlag( QgsSearchWidgetWrapper::FilterFlag flag ); + + /** Returns the active filter flags shown in the widget. + * @see setActiveFlags() + * @see toggleFlag() + * @see availableFlags() + */ + QgsSearchWidgetWrapper::FilterFlags activeFlags() const; + + /** Returns true if the widget is set to be included in the search. + * @see setInactive() + * @see setActive() + */ + bool isActive() const; + + public slots: + + /** Sets the search widget as inactive, ie do not search the corresponding field. + * @see isActive() + * @see setActive() + */ + void setInactive(); + + /** Sets the search widget as active by selecting the first available search type. + * @see isActive() + * @see setInactive() + */ + void setActive(); + +}; diff --git a/python/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.sip b/python/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.sip new file mode 100644 index 00000000000..40630574fce --- /dev/null +++ b/python/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.sip @@ -0,0 +1,34 @@ +/** + * Wraps a value map search widget. This widget will offer a combobox with values from another layer + * referenced by a foreign key (a constraint may be set but is not required on data level). + * It will be used as a search widget and produces expression to look for in the layer. + */ + +class QgsValueMapSearchWidgetWrapper : QgsSearchWidgetWrapper +{ +%TypeHeaderCode +#include +%End + public: + + explicit QgsValueMapSearchWidgetWrapper( QgsVectorLayer* vl, int fieldIdx, QWidget* parent /TransferThis/ = nullptr ); + bool applyDirectly(); + QString expression(); + bool valid() const; + FilterFlags supportedFlags() const; + FilterFlags defaultFlags() const; + virtual QString createExpression( FilterFlags flags ) const; + + public slots: + + virtual void clearWidget(); + virtual void setEnabled( bool enabled ); + + protected: + QWidget* createWidget( QWidget* parent ); + void initWidget( QWidget* editor ); + + protected slots: + void setExpression( QString exp ); + +}; diff --git a/python/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.sip b/python/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.sip new file mode 100644 index 00000000000..5d1f0751ec8 --- /dev/null +++ b/python/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.sip @@ -0,0 +1,39 @@ +/** + * Wraps a value relation search widget. This widget will offer a combobox with values from another layer + * referenced by a foreign key (a constraint may be set but is not required on data level). + * It will be used as a search widget and produces expression to look for in the layer. + */ + +class QgsValueRelationSearchWidgetWrapper : QgsSearchWidgetWrapper +{ +%TypeHeaderCode +#include +%End + public: + explicit QgsValueRelationSearchWidgetWrapper( QgsVectorLayer* vl, int fieldIdx, QWidget* parent /TransferThis/ = nullptr ); + bool applyDirectly(); + QString expression(); + bool valid() const; + QVariant value() const; + FilterFlags supportedFlags() const; + FilterFlags defaultFlags() const; + virtual QString createExpression( FilterFlags flags ) const; + + public slots: + + virtual void clearWidget(); + virtual void setEnabled( bool enabled ); + + protected: + QWidget* createWidget( QWidget* parent ); + void initWidget( QWidget* editor ); + + public slots: + + //! Called when current value of search widget changes + void onValueChanged(); + + protected slots: + void setExpression( QString exp ); + +}; diff --git a/python/gui/gui.sip b/python/gui/gui.sip index 83a9d412ade..c90a7a70589 100644 --- a/python/gui/gui.sip +++ b/python/gui/gui.sip @@ -253,11 +253,15 @@ %Include editorwidgets/core/qgssearchwidgetwrapper.sip %Include editorwidgets/core/qgswidgetwrapper.sip %Include editorwidgets/qgsdatetimeedit.sip +%Include editorwidgets/qgsdefaultsearchwidgetwrapper.sip %Include editorwidgets/qgsdoublespinbox.sip %Include editorwidgets/qgsmultiedittoolbutton.sip %Include editorwidgets/qgsrelationreferencewidget.sip %Include editorwidgets/qgsrelationreferencewidgetwrapper.sip %Include editorwidgets/qgsrelationwidgetwrapper.sip +%Include editorwidgets/qgssearchwidgettoolbutton.sip %Include editorwidgets/qgsspinbox.sip +%Include editorwidgets/qgsvaluemapsearchwidgetwrapper.sip +%Include editorwidgets/qgsvaluerelationsearchwidgetwrapper.sip %Include layertree/qgslayertreeview.sip diff --git a/python/gui/qgsattributeform.sip b/python/gui/qgsattributeform.sip index f636de540be..61272409678 100644 --- a/python/gui/qgsattributeform.sip +++ b/python/gui/qgsattributeform.sip @@ -25,8 +25,18 @@ class QgsAttributeForm : QWidget 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 */ + SearchMode, /*!< Form values are used for searching/filtering the layer */ + }; + + //! Filter types + enum FilterType + { + ReplaceFilter, /*!< Filter should replace any existing filter */ + FilterAnd, /*!< Filter should be combined using "AND" */ + FilterOr, /*!< Filter should be combined using "OR" */ }; explicit QgsAttributeForm( QgsVectorLayer* vl, const QgsFeature& feature = QgsFeature(), const QgsAttributeEditorContext& context = QgsAttributeEditorContext(), QWidget *parent /TransferThis/ = 0 ); @@ -139,6 +149,18 @@ class QgsAttributeForm : QWidget */ void featureSaved( const QgsFeature& feature ); + /** Is emitted when a filter expression is set using the form. + * @param expression filter expression + * @param type filter type + * @note added in QGIS 2.16 + */ + void filterExpressionSet( const QString& expression, QgsAttributeForm::FilterType type ); + + /** Emitted when the form changes mode. + * @param mode new mode + */ + void modeChanged( QgsAttributeForm::Mode mode ); + public slots: /** * Call this to change the content of a given attribute. Will update the editor(s) related to this field. @@ -181,6 +203,11 @@ class QgsAttributeForm : QWidget */ void resetValues(); + /** Resets the search/filter form values. + * @note added in QGIS 2.16 + */ + void resetSearch(); + /** * reload current feature */ diff --git a/python/gui/qgsattributeformeditorwidget.sip b/python/gui/qgsattributeformeditorwidget.sip index aa092d5ffbf..c4f42d80b60 100644 --- a/python/gui/qgsattributeformeditorwidget.sip +++ b/python/gui/qgsattributeformeditorwidget.sip @@ -19,17 +19,31 @@ class QgsAttributeFormEditorWidget : QWidget { 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 */ + SearchMode, /*!< Layer search/filter mode */ }; /** Constructor for QgsAttributeFormEditorWidget. - * @param editorWidget associated editor widget wrapper + * @param editorWidget associated editor widget wrapper (for default/edit modes) * @param form parent attribute form */ - explicit QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, QgsAttributeForm* form /TransferThis/ ); - + explicit QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, + QgsAttributeForm* form /TransferThis/ ); ~QgsAttributeFormEditorWidget(); + /** Sets the search widget wrapper for the widget used when the form is in + * search mode. + * @param wrapper search widget wrapper. + * @note the search widget wrapper should be created using searchWidgetFrame() + * as its parent + */ + void setSearchWidgetWrapper( QgsSearchWidgetWrapper* wrapper ); + + /** Returns the widget which should be used as a parent during construction + * of the search widget wrapper. + * @see setSearchWidgetWrapper() + */ + QWidget* searchWidgetFrame(); + /** 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 @@ -57,6 +71,12 @@ class QgsAttributeFormEditorWidget : QWidget */ QVariant currentValue() const; + /** Creates an expression matching the current search filter value and + * search properties represented in the widget. + * @note added in QGIS 2.16 + */ + QString currentFilterExpression() const; + public slots: /** Sets whether the widget should be displayed in a "mixed values" mode. @@ -68,11 +88,22 @@ class QgsAttributeFormEditorWidget : QWidget */ void changesCommitted(); + /** Resets the search/filter value of the widget. + */ + void resetSearch(); + signals: //! Emitted when the widget's value changes //! @param value new widget value void valueChanged( const QVariant& value ); + protected: + + /** Returns a pointer to the search widget tool button in the widget. + * @note this method is in place for unit testing only, and is not considered + * stable API + */ + QgsSearchWidgetToolButton* searchWidgetToolButton(); }; diff --git a/src/app/qgsattributetabledialog.cpp b/src/app/qgsattributetabledialog.cpp index d99bae5183c..a9542001c0e 100644 --- a/src/app/qgsattributetabledialog.cpp +++ b/src/app/qgsattributetabledialog.cpp @@ -166,6 +166,8 @@ QgsAttributeTableDialog::QgsAttributeTableDialog( QgsVectorLayer *theLayer, QWid // connect table info to window connect( mMainView, SIGNAL( filterChanged() ), this, SLOT( updateTitle() ) ); + connect( mMainView, SIGNAL( filterExpressionSet( QString, QgsAttributeForm::FilterType ) ), this, SLOT( setFilterExpression( QString, QgsAttributeForm::FilterType ) ) ); + connect( mMainView, SIGNAL( formModeChanged( QgsAttributeForm::Mode ) ), this, SLOT( viewModeChanged( QgsAttributeForm::Mode ) ) ); // info from table to application connect( this, SIGNAL( saveEdits( QgsMapLayer * ) ), QgisApp::instance(), SLOT( saveEdits( QgsMapLayer * ) ) ); @@ -265,6 +267,7 @@ QgsAttributeTableDialog::QgsAttributeTableDialog( QgsVectorLayer *theLayer, QWid mMainViewButtonGroup->button( initialView )->setChecked( true ); connect( mToggleMultiEditButton, SIGNAL( toggled( bool ) ), mMainView, SLOT( setMultiEditEnabled( bool ) ) ); + connect( mSearchFormButton, SIGNAL( toggled( bool ) ), mMainView, SLOT( toggleSearchMode( bool ) ) ); updateMultiEditButtonState(); editingToggled(); @@ -379,6 +382,12 @@ void QgsAttributeTableDialog::updateFieldFromExpressionSelected() runFieldCalculation( mLayer, mFieldCombo->currentText(), mUpdateExpressionText->asExpression(), filteredIds ); } +void QgsAttributeTableDialog::viewModeChanged( QgsAttributeForm::Mode mode ) +{ + if ( mode != QgsAttributeForm::SearchMode ) + mSearchFormButton->setChecked( false ); +} + void QgsAttributeTableDialog::runFieldCalculation( QgsVectorLayer* layer, const QString& fieldName, const QString& expression, const QgsFeatureIds& filteredIds ) { QApplication::setOverrideCursor( Qt::WaitCursor ); @@ -524,6 +533,7 @@ void QgsAttributeTableDialog::filterShowAll() mFilterButton->setDefaultAction( mActionShowAllFilter ); mFilterButton->setPopupMode( QToolButton::InstantPopup ); mFilterQuery->setVisible( false ); + mFilterQuery->setText( QString() ); if ( mCurrentSearchWidgetWrapper ) { mCurrentSearchWidgetWrapper->widget()->setVisible( false ); @@ -717,6 +727,9 @@ void QgsAttributeTableDialog::on_mMainView_currentChanged( int viewMode ) mMainViewButtonGroup->button( viewMode )->click(); updateMultiEditButtonState(); + if ( viewMode == 0 ) + mSearchFormButton->setChecked( false ); + QSettings s; s.setValue( "/qgis/attributeTableLastView", static_cast< int >( viewMode ) ); } @@ -739,6 +752,10 @@ void QgsAttributeTableDialog::editingToggled() mSaveEditsButton->setEnabled( mLayer->isEditable() ); mReloadButton->setEnabled( ! mLayer->isEditable() ); updateMultiEditButtonState(); + if ( mLayer->isEditable() ) + { + mSearchFormButton->setChecked( false ); + } mToggleEditingButton->blockSignals( false ); bool canChangeAttributes = mLayer->dataProvider()->capabilities() & QgsVectorDataProvider::ChangeAttributeValues; @@ -866,11 +883,39 @@ void QgsAttributeTableDialog::openConditionalStyles() mMainView->openConditionalStyles(); } -void QgsAttributeTableDialog::setFilterExpression( const QString& filterString ) +void QgsAttributeTableDialog::setFilterExpression( const QString& filterString, QgsAttributeForm::FilterType type ) { + QString filter; + if ( !mFilterQuery->text().isEmpty() && !filterString.isEmpty() ) + { + switch ( type ) + { + case QgsAttributeForm::ReplaceFilter: + filter = filterString; + break; + + case QgsAttributeForm::FilterAnd: + filter = QString( "(%1) AND (%2)" ).arg( mFilterQuery->text(), filterString ); + break; + + case QgsAttributeForm::FilterOr: + filter = QString( "(%1) OR (%2)" ).arg( mFilterQuery->text(), filterString ); + break; + } + } + else if ( !filterString.isEmpty() ) + { + filter = filterString; + } + else + { + filterShowAll(); + return; + } + if ( !mCurrentSearchWidgetWrapper || !mCurrentSearchWidgetWrapper->applyDirectly() ) { - mFilterQuery->setText( filterString ); + mFilterQuery->setText( filter ); mFilterButton->setDefaultAction( mActionAdvancedFilter ); mFilterButton->setPopupMode( QToolButton::MenuButtonPopup ); mFilterQuery->setVisible( true ); @@ -890,7 +935,7 @@ void QgsAttributeTableDialog::setFilterExpression( const QString& filterString ) myDa.setEllipsoid( QgsProject::instance()->readEntry( "Measure", "/Ellipsoid", GEO_NONE ) ); // parse search string and build parsed tree - QgsExpression filterExpression( filterString ); + QgsExpression filterExpression( filter ); if ( filterExpression.hasParserError() ) { QgisApp::instance()->messageBar()->pushMessage( tr( "Parsing error" ), filterExpression.parserErrorString(), QgsMessageBar::WARNING, QgisApp::instance()->messageTimeout() ); diff --git a/src/app/qgsattributetabledialog.h b/src/app/qgsattributetabledialog.h index f84d6ebfb5a..633da3d9b83 100644 --- a/src/app/qgsattributetabledialog.h +++ b/src/app/qgsattributetabledialog.h @@ -56,18 +56,18 @@ class APP_EXPORT QgsAttributeTableDialog : public QDialog, private Ui::QgsAttrib QgsAttributeTableDialog( QgsVectorLayer *theLayer, QWidget *parent = nullptr, Qt::WindowFlags flags = Qt::Window ); ~QgsAttributeTableDialog(); - /** - * Sets the filter expression to filter visible features - * @param filterString filter query string. QgsExpression compatible. - */ - void setFilterExpression( const QString& filterString ); - public slots: /** * Toggles editing mode */ void editingToggled(); + /** + * Sets the filter expression to filter visible features + * @param filterString filter query string. QgsExpression compatible. + */ + void setFilterExpression( const QString& filterString, QgsAttributeForm::FilterType type = QgsAttributeForm::ReplaceFilter ); + private slots: /** * Copies selected rows to the clipboard @@ -212,6 +212,7 @@ class APP_EXPORT QgsAttributeTableDialog : public QDialog, private Ui::QgsAttrib void runFieldCalculation( QgsVectorLayer* layer, const QString& fieldName, const QString& expression, const QgsFeatureIds& filteredIds = QgsFeatureIds() ); void updateFieldFromExpression(); void updateFieldFromExpressionSelected(); + void viewModeChanged( QgsAttributeForm::Mode mode ); private: QMenu* mMenuActions; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 5144cb63555..47bc7baae5f 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -123,6 +123,7 @@ SET(QGIS_GUI_SRCS editorwidgets/qgsrangeconfigdlg.cpp editorwidgets/qgsrangewidgetwrapper.cpp editorwidgets/qgsrangewidgetfactory.cpp + editorwidgets/qgssearchwidgettoolbutton.cpp editorwidgets/qgsspinbox.cpp editorwidgets/qgsrelationwidgetwrapper.cpp editorwidgets/qgsrelationreferenceconfigdlg.cpp @@ -545,6 +546,7 @@ SET(QGIS_GUI_MOC_HDRS editorwidgets/qgsrelationreferencewidget.h editorwidgets/qgsrelationreferencewidgetwrapper.h editorwidgets/qgsrelationwidgetwrapper.h + editorwidgets/qgssearchwidgettoolbutton.h editorwidgets/qgsspinbox.h editorwidgets/qgstexteditconfigdlg.h editorwidgets/qgstexteditwrapper.h diff --git a/src/gui/attributetable/qgsdualview.cpp b/src/gui/attributetable/qgsdualview.cpp index 478ed176b4f..f3ee3187c95 100644 --- a/src/gui/attributetable/qgsdualview.cpp +++ b/src/gui/attributetable/qgsdualview.cpp @@ -15,7 +15,6 @@ #include "qgsapplication.h" #include "qgsactionmanager.h" -#include "qgsattributeform.h" #include "qgsattributetablemodel.h" #include "qgsdualview.h" #include "qgsexpressionbuilderdialog.h" @@ -84,8 +83,9 @@ void QgsDualView::init( QgsVectorLayer* layer, QgsMapCanvas* mapCanvas, const Qg mAttributeForm->hideButtonBox(); connect( mAttributeForm, SIGNAL( attributeChanged( QString, QVariant ) ), this, SLOT( featureFormAttributeChanged() ) ); + connect( mAttributeForm, SIGNAL( modeChanged( QgsAttributeForm::Mode ) ), this, SIGNAL( formModeChanged( QgsAttributeForm::Mode ) ) ); connect( mMasterModel, SIGNAL( modelChanged() ), mAttributeForm, SLOT( refreshFeature() ) ); - + connect( mAttributeForm, SIGNAL( filterExpressionSet( QString, QgsAttributeForm::FilterType ) ), this, SIGNAL( filterExpressionSet( QString, QgsAttributeForm::FilterType ) ) ); if ( mFeatureListPreviewButton->defaultAction() ) mFeatureList->setDisplayExpression( mDisplayExpression ); else @@ -308,6 +308,19 @@ void QgsDualView::setMultiEditEnabled( bool enabled ) mAttributeForm->setMode( enabled ? QgsAttributeForm::MultiEditMode : QgsAttributeForm::SingleEditMode ); } +void QgsDualView::toggleSearchMode( bool enabled ) +{ + if ( enabled ) + { + setView( AttributeEditor ); + mAttributeForm->setMode( QgsAttributeForm::SearchMode ); + } + else + { + mAttributeForm->setMode( QgsAttributeForm::SingleEditMode ); + } +} + void QgsDualView::previewExpressionBuilder() { // Show expression builder diff --git a/src/gui/attributetable/qgsdualview.h b/src/gui/attributetable/qgsdualview.h index 9c4cb5e8a27..145aaa28bac 100644 --- a/src/gui/attributetable/qgsdualview.h +++ b/src/gui/attributetable/qgsdualview.h @@ -25,6 +25,7 @@ #include "qgsattributetablefiltermodel.h" #include "qgscachedfeatureiterator.h" #include "qgsdistancearea.h" +#include "qgsattributeform.h" class QgsAttributeForm; class QgsFeatureRequest; @@ -219,6 +220,12 @@ class GUI_EXPORT QgsDualView : public QStackedWidget, private Ui::QgsDualViewBas */ void setMultiEditEnabled( bool enabled ); + /** Toggles whether search mode should be enabled in the form. + * @param enabled set to true to switch on search mode + * @note added in QGIS 2.16 + */ + void toggleSearchMode( bool enabled ); + signals: /** * Is emitted, whenever the display expression is successfully changed @@ -231,6 +238,18 @@ class GUI_EXPORT QgsDualView : public QStackedWidget, private Ui::QgsDualViewBas */ void filterChanged(); + /** Is emitted when a filter expression is set using the view. + * @param expression filter expression + * @param type filter type + * @note added in QGIS 2.16 + */ + void filterExpressionSet( const QString& expression, QgsAttributeForm::FilterType type ); + + /** Emitted when the form changes mode. + * @param mode new mode + */ + void formModeChanged( QgsAttributeForm::Mode mode ); + private slots: void on_mFeatureList_aboutToChangeEditSelection( bool& ok ); diff --git a/src/gui/editorwidgets/core/qgssearchwidgetwrapper.cpp b/src/gui/editorwidgets/core/qgssearchwidgetwrapper.cpp index 9d4a1841ffc..72b40f4c890 100644 --- a/src/gui/editorwidgets/core/qgssearchwidgetwrapper.cpp +++ b/src/gui/editorwidgets/core/qgssearchwidgetwrapper.cpp @@ -20,6 +20,57 @@ #include +QList QgsSearchWidgetWrapper::exclusiveFilterFlags() +{ + return QList() + << EqualTo + << NotEqualTo + << GreaterThan + << LessThan + << GreaterThanOrEqualTo + << LessThanOrEqualTo + << Between + << Contains + << DoesNotContain + << IsNull; +} + +QList QgsSearchWidgetWrapper::nonExclusiveFilterFlags() +{ + return QList() + << CaseInsensitive; +} + +QString QgsSearchWidgetWrapper::toString( QgsSearchWidgetWrapper::FilterFlag flag ) +{ + switch ( flag ) + { + case EqualTo: + return QObject::tr( "Equal to (=)" ); + case NotEqualTo: + return QObject::tr( "Not equal to" ); + case GreaterThan: + return QObject::tr( "Greater than (>)" ); + case LessThan: + return QObject::tr( "Less than (<)" ); + case GreaterThanOrEqualTo: + return QObject::tr( "Greater than or equal to (>=)" ); + case LessThanOrEqualTo: + return QObject::tr( "Less than or equal to (<=)" ); + case Between: + return QObject::tr( "Between (inclusive)" ); + case CaseInsensitive: + return QObject::tr( "Case insensitive" ); + case Contains: + return QObject::tr( "Contains" ); + case DoesNotContain: + return QObject::tr( "Does not contain" ); + case IsNull: + return QObject::tr( "Is missing (null)" ); + } + return QString(); +} + QgsSearchWidgetWrapper::QgsSearchWidgetWrapper( QgsVectorLayer* vl, int fieldIdx, QWidget* parent ) : QgsWidgetWrapper( vl, nullptr, parent ) , mExpression( QString() ) @@ -27,6 +78,16 @@ QgsSearchWidgetWrapper::QgsSearchWidgetWrapper( QgsVectorLayer* vl, int fieldIdx { } +QgsSearchWidgetWrapper::FilterFlags QgsSearchWidgetWrapper::supportedFlags() const +{ + return EqualTo; +} + +QgsSearchWidgetWrapper::FilterFlags QgsSearchWidgetWrapper::defaultFlags() const +{ + return FilterFlags(); +} + void QgsSearchWidgetWrapper::setFeature( const QgsFeature& feature ) { diff --git a/src/gui/editorwidgets/core/qgssearchwidgetwrapper.h b/src/gui/editorwidgets/core/qgssearchwidgetwrapper.h index 667560de519..c5e137aa4ab 100644 --- a/src/gui/editorwidgets/core/qgssearchwidgetwrapper.h +++ b/src/gui/editorwidgets/core/qgssearchwidgetwrapper.h @@ -41,6 +41,43 @@ class GUI_EXPORT QgsSearchWidgetWrapper : public QgsWidgetWrapper { Q_OBJECT public: + + //! Flags which indicate what types of filtering and searching is possible using the widget + //! @note added in QGIS 2.16 + enum FilterFlag + { + EqualTo = 1 << 1, /*!< Supports equal to */ + NotEqualTo = 1 << 2, /*!< Supports not equal to */ + GreaterThan = 1 << 3, /*!< Supports greater than */ + LessThan = 1 << 4, /*!< Supports less than */ + GreaterThanOrEqualTo = 1 << 5, /*!< Supports >= */ + LessThanOrEqualTo = 1 << 6, /*!< Supports <= */ + Between = 1 << 7, /*!< Supports searches between two values */ + CaseInsensitive = 1 << 8, /*!< Supports case insensitive searching */ + Contains = 1 << 9, /*!< Supports value "contains" searching */ + DoesNotContain = 1 << 10, /*!< Supports value does not contain searching */ + IsNull = 1 << 11, /*!< Supports searching for null values */ + }; + Q_DECLARE_FLAGS( FilterFlags, FilterFlag ) + + /** Returns a list of exclusive filter flags, which cannot be combined with other flags (eg EqualTo/NotEqualTo) + * @note added in QGIS 2.16 + * @see nonExclusiveFilterFlags() + */ + static QList< FilterFlag > exclusiveFilterFlags(); + + /** Returns a list of non-exclusive filter flags, which can be combined with other flags (eg CaseInsensitive) + * @note added in QGIS 2.16 + * @see exclusiveFilterFlags() + */ + static QList< FilterFlag > nonExclusiveFilterFlags(); + + /** Returns a translated string representing a filter flag. + * @param flag flag to convert to string + * @note added in QGIS 2.16 + */ + static QString toString( FilterFlag flag ); + /** * Create a new widget wrapper * @@ -50,6 +87,18 @@ class GUI_EXPORT QgsSearchWidgetWrapper : public QgsWidgetWrapper */ explicit QgsSearchWidgetWrapper( QgsVectorLayer* vl, int fieldIdx, QWidget* parent = nullptr ); + /** Returns filter flags supported by the search widget. + * @note added in QGIS 2.16 + * @see defaultFlags() + */ + virtual FilterFlags supportedFlags() const; + + /** Returns the filter flags which should be set by default for the search widget. + * @note added in QGIS 2.16 + * @see supportedFlags() + */ + virtual FilterFlags defaultFlags() const; + /** * Will be used to access the widget's value. Read the value from the widget and * return it properly formatted to be saved in the attribute. @@ -60,12 +109,34 @@ class GUI_EXPORT QgsSearchWidgetWrapper : public QgsWidgetWrapper * @return The current value the widget represents */ virtual QString expression() = 0; + /** * If this is true, then this search widget should take effect directly * when its expression changes */ virtual bool applyDirectly() = 0; + /** Creates a filter expression based on the current state of the search widget + * and the specified filter flags. + * @param flags filter flags + * @returns filter expression + * @note added in QGIS 2.16 + */ + // TODO QGIS 3.0 - make pure virtual + virtual QString createExpression( FilterFlags flags ) const { Q_UNUSED( flags ); return "TRUE"; } + + public slots: + + /** Clears the widget's current value and resets it back to the default state + * @note added in QGIS 2.16 + */ + virtual void clearWidget() {} + + /** Toggles whether the search widget is enabled or disabled. + * @param enabled set to true to enable widget + */ + virtual void setEnabled( bool enabled ) override { Q_UNUSED( enabled ); } + signals: /** @@ -74,6 +145,17 @@ class GUI_EXPORT QgsSearchWidgetWrapper : public QgsWidgetWrapper */ void expressionChanged( const QString& exp ); + /** Emitted when a user changes the value of the search widget. + * @note added in QGIS 2.16 + */ + void valueChanged(); + + /** Emitted when a user changes the value of the search widget back + * to an empty, default state. + * @note added in QGIS 2.16 + */ + void valueCleared(); + protected slots: virtual void setExpression( QString value ) = 0; @@ -90,4 +172,6 @@ class GUI_EXPORT QgsSearchWidgetWrapper : public QgsWidgetWrapper // We'll use this class inside a QVariant in the widgets properties Q_DECLARE_METATYPE( QgsSearchWidgetWrapper* ) +Q_DECLARE_OPERATORS_FOR_FLAGS( QgsSearchWidgetWrapper::FilterFlags ) + #endif // QGSSEARCHWIDGETWRAPPER_H diff --git a/src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.cpp b/src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.cpp index 4a506a21450..17e9318e632 100644 --- a/src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.cpp @@ -17,7 +17,7 @@ #include "qgsfield.h" #include "qgsfieldvalidator.h" - +#include "qgsexpression.h" #include #include @@ -30,7 +30,6 @@ QgsDefaultSearchWidgetWrapper::QgsDefaultSearchWidgetWrapper( QgsVectorLayer* vl { } - QString QgsDefaultSearchWidgetWrapper::expression() { return mExpression; @@ -86,17 +85,178 @@ bool QgsDefaultSearchWidgetWrapper::applyDirectly() return false; } +QgsSearchWidgetWrapper::FilterFlags QgsDefaultSearchWidgetWrapper::supportedFlags() const +{ + FilterFlags flags = EqualTo | NotEqualTo | IsNull; + + QVariant::Type fldType = layer()->fields().at( mFieldIdx ).type(); + switch ( fldType ) + { + case QVariant::Int: + case QVariant::UInt: + case QVariant::Double: + case QVariant::LongLong: + case QVariant::ULongLong: + //numeric + flags |= GreaterThan | LessThan | GreaterThanOrEqualTo | LessThanOrEqualTo; + break; + + case QVariant::Date: + case QVariant::DateTime: + case QVariant::Time: + flags |= GreaterThan | LessThan | GreaterThanOrEqualTo | LessThanOrEqualTo | Between; + break; + + case QVariant::String: + flags |= Contains | DoesNotContain; + break; + + default: + break; + } + return flags; +} + +QgsSearchWidgetWrapper::FilterFlags QgsDefaultSearchWidgetWrapper::defaultFlags() const +{ + QVariant::Type fldType = layer()->fields().at( mFieldIdx ).type(); + switch ( fldType ) + { + case QVariant::Int: + case QVariant::UInt: + case QVariant::Double: + case QVariant::LongLong: + case QVariant::ULongLong: + //numeric + return EqualTo; + + case QVariant::Date: + case QVariant::DateTime: + case QVariant::Time: + return EqualTo; + + case QVariant::String: + return Contains; + + default: + break; + } + return EqualTo; +} + +QString QgsDefaultSearchWidgetWrapper::createExpression( QgsSearchWidgetWrapper::FilterFlags flags ) const +{ + //clear any unsupported flags + flags &= supportedFlags(); + + QVariant::Type fldType = layer()->fields().at( mFieldIdx ).type(); + QString fieldName = QgsExpression::quotedColumnRef( layer()->fields().at( mFieldIdx ).name() ); + + if ( flags & IsNull ) + return fieldName + " IS NULL"; + + switch ( fldType ) + { + case QVariant::Int: + case QVariant::UInt: + case QVariant::Double: + case QVariant::LongLong: + case QVariant::ULongLong: + { + if ( flags & EqualTo ) + return fieldName + '=' + mLineEdit->text(); + else if ( flags & NotEqualTo ) + return fieldName + "<>" + mLineEdit->text(); + else if ( flags & GreaterThan ) + return fieldName + '>' + mLineEdit->text(); + else if ( flags & LessThan ) + return fieldName + '<' + mLineEdit->text(); + else if ( flags & GreaterThanOrEqualTo ) + return fieldName + ">=" + mLineEdit->text(); + else if ( flags & LessThanOrEqualTo ) + return fieldName + "<=" + mLineEdit->text(); + break; + } + + case QVariant::Date: + case QVariant::DateTime: + case QVariant::Time: + { + if ( flags & EqualTo ) + return fieldName + "='" + mLineEdit->text() + '\''; + else if ( flags & NotEqualTo ) + return fieldName + "<>'" + mLineEdit->text() + '\''; + else if ( flags & GreaterThan ) + return fieldName + ">'" + mLineEdit->text() + '\''; + else if ( flags & LessThan ) + return fieldName + "<'" + mLineEdit->text() + '\''; + else if ( flags & GreaterThanOrEqualTo ) + return fieldName + ">='" + mLineEdit->text() + '\''; + else if ( flags & LessThanOrEqualTo ) + return fieldName + "<='" + mLineEdit->text() + '\''; + break; + } + + case QVariant::String: + { + // case insensitive! + if ( flags & EqualTo || flags & NotEqualTo ) + { + if ( mCheckbox->isChecked() ) + return fieldName + ( flags & EqualTo ? "=" : "<>" ) + + QgsExpression::quotedString( mLineEdit->text() ); + else + return QString( "lower(%1)" ).arg( fieldName ) + + ( flags & EqualTo ? "=" : "<>" ) + + QString( "lower(%1)" ).arg( QgsExpression::quotedString( mLineEdit->text() ) ); + } + else if ( flags & Contains || flags & DoesNotContain ) + { + QString exp = fieldName + ( mCheckbox->isChecked() ? " LIKE " : " ILIKE " ); + QString value = QgsExpression::quotedString( mLineEdit->text() ); + value.chop( 1 ); + value = value.remove( 0, 1 ); + exp += "'%" + value + "%'"; + if ( flags & DoesNotContain ) + exp.prepend( "NOT (" ).append( ")" ); + return exp; + } + + break; + } + + default: + break; + } + + return QString(); +} + +void QgsDefaultSearchWidgetWrapper::clearWidget() +{ + mLineEdit->setText( QString() ); +} + +void QgsDefaultSearchWidgetWrapper::setEnabled( bool enabled ) +{ + mLineEdit->setEnabled( enabled ); + mCheckbox->setEnabled( enabled ); +} + void QgsDefaultSearchWidgetWrapper::initWidget( QWidget* widget ) { mContainer = widget; mContainer->setLayout( new QHBoxLayout() ); + mContainer->layout()->setMargin( 0 ); + mContainer->layout()->setContentsMargins( 0, 0, 0, 0 ); mLineEdit = new QgsFilterLineEdit(); mCheckbox = new QCheckBox( "Case sensitive" ); mContainer->layout()->addWidget( mLineEdit ); mContainer->layout()->addWidget( mCheckbox ); - connect( mLineEdit, SIGNAL( textChanged( QString ) ), this, SLOT( setExpression( QString ) ) ); + connect( mLineEdit, SIGNAL( textChanged( QString ) ), this, SLOT( textChanged( QString ) ) ); connect( mLineEdit, SIGNAL( returnPressed() ), this, SLOT( filterChanged() ) ); connect( mCheckbox, SIGNAL( stateChanged( int ) ), this, SLOT( setCaseString( int ) ) ); + connect( mLineEdit, SIGNAL( textEdited( QString ) ), this, SIGNAL( valueChanged() ) ); mCheckbox->setChecked( Qt::Unchecked ); mCaseString = "ILIKE"; } @@ -106,7 +266,25 @@ bool QgsDefaultSearchWidgetWrapper::valid() const return true; } +QgsFilterLineEdit* QgsDefaultSearchWidgetWrapper::lineEdit() +{ + return mLineEdit; +} + +QCheckBox* QgsDefaultSearchWidgetWrapper::caseSensitiveCheckBox() +{ + return mCheckbox; +} + void QgsDefaultSearchWidgetWrapper::filterChanged() { emit expressionChanged( mExpression ); } + +void QgsDefaultSearchWidgetWrapper::textChanged( const QString& text ) +{ + if ( text.isEmpty() ) + emit valueCleared(); + + setExpression( text ); +} diff --git a/src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.h b/src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.h index 0ef800a4501..9b28915cb7a 100644 --- a/src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.h +++ b/src/gui/editorwidgets/qgsdefaultsearchwidgetwrapper.h @@ -23,7 +23,6 @@ /** * Wraps a search widget. Default form is just a QgsLineFilterEdit - * \note not available in Python bindings */ class GUI_EXPORT QgsDefaultSearchWidgetWrapper : public QgsSearchWidgetWrapper @@ -36,6 +35,15 @@ class GUI_EXPORT QgsDefaultSearchWidgetWrapper : public QgsSearchWidgetWrapper public: QString expression() override; bool applyDirectly() override; + FilterFlags supportedFlags() const override; + FilterFlags defaultFlags() const override; + virtual QString createExpression( FilterFlags flags ) const override; + + public slots: + + virtual void clearWidget() override; + + virtual void setEnabled( bool enabled ) override; protected slots: void setExpression( QString exp ) override; @@ -43,12 +51,25 @@ class GUI_EXPORT QgsDefaultSearchWidgetWrapper : public QgsSearchWidgetWrapper private slots: void setCaseString( int caseSensitiveCheckState ); void filterChanged(); + void textChanged( const QString& text ); protected: QWidget* createWidget( QWidget* parent ) override; void initWidget( QWidget* editor ) override; bool valid() const override; + /** Returns a pointer to the line edit part of the widget. + * @note this method is in place for unit testing only, and is not considered + * stable API + */ + QgsFilterLineEdit* lineEdit(); + + /** Returns a pointer to the case sensitivity check box in the widget. + * @note this method is in place for unit testing only, and is not considered + * stable API + */ + QCheckBox* caseSensitiveCheckBox(); + private: QgsFilterLineEdit* mLineEdit; QCheckBox* mCheckbox; diff --git a/src/gui/editorwidgets/qgssearchwidgettoolbutton.cpp b/src/gui/editorwidgets/qgssearchwidgettoolbutton.cpp new file mode 100644 index 00000000000..55af0a7ab04 --- /dev/null +++ b/src/gui/editorwidgets/qgssearchwidgettoolbutton.cpp @@ -0,0 +1,265 @@ +/*************************************************************************** + qgssearchwidgettoolbutton.cpp + ----------------------------- + Date : May 2016 + Copyright : (C) 2016 Nyall Dawson + Email : nyall dot dawson at gmail.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 "qgssearchwidgettoolbutton.h" +#include "qgsapplication.h" +#include + +QgsSearchWidgetToolButton::QgsSearchWidgetToolButton( QWidget* parent ) + : QToolButton( parent ) + , mAvailableFilterFlags( QgsSearchWidgetWrapper::EqualTo | QgsSearchWidgetWrapper::NotEqualTo | QgsSearchWidgetWrapper::CaseInsensitive ) + , mFilterFlags( QgsSearchWidgetWrapper::EqualTo ) + , mSearchWrapper( nullptr ) + , mMenu( nullptr ) +{ + setFocusPolicy( Qt::StrongFocus ); + setPopupMode( QToolButton::InstantPopup ); + + mMenu = new QMenu( this ); + connect( mMenu, SIGNAL( aboutToShow() ), this, SLOT( aboutToShowMenu() ) ); + setMenu( mMenu ); + + // sets initial appearance + updateState(); +} + +void QgsSearchWidgetToolButton::setSearchWidgetWrapper( QgsSearchWidgetWrapper* wrapper ) +{ + mSearchWrapper = wrapper; + setAvailableFlags( mSearchWrapper->supportedFlags() ); + setActiveFlags( QgsSearchWidgetWrapper::FilterFlags() ); + connect( mSearchWrapper, SIGNAL( valueChanged() ), this, SLOT( searchWidgetValueChanged() ) ); + connect( mSearchWrapper, SIGNAL( valueCleared() ), this, SLOT( setInactive() ) ); +} + +void QgsSearchWidgetToolButton::setAvailableFlags( QgsSearchWidgetWrapper::FilterFlags flags ) +{ + mFilterFlags &= flags; + mAvailableFilterFlags = flags; + updateState(); +} + +void QgsSearchWidgetToolButton::setActiveFlags( QgsSearchWidgetWrapper::FilterFlags flags ) +{ + // sanitize list + QgsSearchWidgetWrapper::FilterFlags newFlags; + + // only accept a single exclusive flag + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::exclusiveFilterFlags() ) + { + if ( !( mAvailableFilterFlags & flag ) ) + { + //unsupported + continue; + } + if ( flags & flag ) + { + newFlags |= flag; + break; + } + } + + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::nonExclusiveFilterFlags() ) + { + if ( !( mAvailableFilterFlags & flag ) ) + { + //unsupported + continue; + } + + if ( flags & flag ) + newFlags |= flag; + } + + mFilterFlags = newFlags; + + updateState(); +} + +void QgsSearchWidgetToolButton::toggleFlag( QgsSearchWidgetWrapper::FilterFlag flag ) +{ + if ( !( flag & mAvailableFilterFlags ) ) + return; + + if ( QgsSearchWidgetWrapper::nonExclusiveFilterFlags().contains( flag ) ) + { + if ( flag & mFilterFlags ) + mFilterFlags &= ~flag; + else + mFilterFlags |= flag; + } + else + { + // clear other exclusive flags + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag exclusiveFlag, QgsSearchWidgetWrapper::exclusiveFilterFlags() ) + { + mFilterFlags &= ~exclusiveFlag; + } + // and set new exclusive flag + mFilterFlags |= flag; + } + + updateState(); +} + +bool QgsSearchWidgetToolButton::isActive() const +{ + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::exclusiveFilterFlags() ) + { + if ( mFilterFlags & flag ) + return true; + } + return false; +} + +void QgsSearchWidgetToolButton::aboutToShowMenu() +{ + mMenu->clear(); + bool fieldActive = false; + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::exclusiveFilterFlags() ) + { + if ( !( mAvailableFilterFlags & flag ) ) + { + //unsupported + continue; + } + + QAction* action = mMenu->addAction( QgsSearchWidgetWrapper::toString( flag ) ); + connect( action, SIGNAL( triggered( bool ) ), this, SLOT( actionSelected() ) ); + action->setData( flag ); + action->setCheckable( true ); + if ( mFilterFlags & flag ) + { + fieldActive = true; + action->setChecked( true ); + } + } + + QAction* clearAction = mMenu->addAction( tr( "Exclude field" ) ); + connect( clearAction, SIGNAL( triggered( bool ) ), this, SLOT( setInactive() ) ); + clearAction->setCheckable( true ); + clearAction->setChecked( !fieldActive ); + if ( mMenu->actions().count() > 0 ) + { + mMenu->insertAction( mMenu->actions().at( 0 ), clearAction ); + mMenu->insertSeparator( mMenu->actions().at( 1 ) ); + } + else + mMenu->addAction( clearAction ); + + mMenu->addSeparator(); + + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::nonExclusiveFilterFlags() ) + { + if ( !( mAvailableFilterFlags & flag ) ) + { + //unsupported + continue; + } + + QAction* action = mMenu->addAction( QgsSearchWidgetWrapper::toString( flag ) ); + connect( action, SIGNAL( triggered( bool ) ), this, SLOT( actionSelected() ) ); + action->setData( flag ); + action->setCheckable( true ); + if ( mFilterFlags & flag ) + action->setChecked( true ); + } +} + +void QgsSearchWidgetToolButton::actionSelected() +{ + QgsSearchWidgetWrapper::FilterFlag flag = static_cast< QgsSearchWidgetWrapper::FilterFlag >( qobject_cast< QAction* >( sender() )->data().toInt() ); + toggleFlag( flag ); +} + +void QgsSearchWidgetToolButton::searchWidgetValueChanged() +{ + setActive(); +} + +void QgsSearchWidgetToolButton::setInactive() +{ + if ( !isActive() ) + return; + + if ( mSearchWrapper ) + mSearchWrapper->clearWidget(); + + QgsSearchWidgetWrapper::FilterFlags newFlags; + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::nonExclusiveFilterFlags() ) + { + if ( !( mAvailableFilterFlags & flag ) || !( mFilterFlags & flag ) ) + continue; + newFlags |= flag; + } + mFilterFlags = newFlags; + updateState(); +} + +void QgsSearchWidgetToolButton::setActive() +{ + if ( isActive() ) + return; + + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::exclusiveFilterFlags() ) + { + if ( mSearchWrapper && mSearchWrapper->defaultFlags() & flag ) + { + toggleFlag( flag ); + return; + } + else if ( !mSearchWrapper && mAvailableFilterFlags & flag ) + { + toggleFlag( flag ); + return; + } + } +} + +void QgsSearchWidgetToolButton::updateState() +{ + if ( mSearchWrapper ) + mSearchWrapper->setEnabled( !( mFilterFlags & QgsSearchWidgetWrapper::IsNull ) ); + + bool active = false; + QStringList toolTips; + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::exclusiveFilterFlags() ) + { + if ( mFilterFlags & flag ) + { + toolTips << QgsSearchWidgetWrapper::toString( flag ); + active = true; + } + } + Q_FOREACH ( QgsSearchWidgetWrapper::FilterFlag flag, QgsSearchWidgetWrapper::nonExclusiveFilterFlags() ) + { + if ( mFilterFlags & flag ) + { + toolTips << QgsSearchWidgetWrapper::toString( flag ).toLower(); + } + } + + if ( active ) + { + QString text = toolTips.join( ", " ); + setText( text ); + setToolTip( text ); + } + else + { + setText( tr( "Exclude field" ) ); + setToolTip( QString() ); + } +} diff --git a/src/gui/editorwidgets/qgssearchwidgettoolbutton.h b/src/gui/editorwidgets/qgssearchwidgettoolbutton.h new file mode 100644 index 00000000000..3a2614b7530 --- /dev/null +++ b/src/gui/editorwidgets/qgssearchwidgettoolbutton.h @@ -0,0 +1,129 @@ +/*************************************************************************** + qgssearchwidgettoolbutton.h + --------------------------- + Date : May 2016 + Copyright : (C) 2016 Nyall Dawson + Email : nyall dot dawson at gmail.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 QGSSEARCHWIDGETTOOLBUTTON_H +#define QGSSEARCHWIDGETTOOLBUTTON_H + +#include "editorwidgets/core/qgssearchwidgetwrapper.h" +#include + +/** \ingroup gui + * \class QgsSearchWidgetToolButton + * A tool button widget which is displayed next to search widgets in forms, and + * allows for controlling how the widget behaves and how the filtering/searching + * operates. + * \note Added in version 2.16 + */ +class GUI_EXPORT QgsSearchWidgetToolButton : public QToolButton +{ + Q_OBJECT + + public: + + /** Constructor for QgsSearchWidgetToolButton. + * @param parent parent object + */ + explicit QgsSearchWidgetToolButton( QWidget *parent = nullptr ); + + /** Sets the search widget wrapper associated with this button. + * Calling this will automatically set the available flags to match those + * supported by the wrapper and reset the active flags to match the wrapper's + * default flags. + * @param wrapper search wrapper. Ownership is not transferred. + */ + void setSearchWidgetWrapper( QgsSearchWidgetWrapper* wrapper ); + + /** Sets the available filter flags to show in the widget. Any active flags + * (see activeFlags()) which are not present in the new available filter + * flags will be cleared; + * @param flags available flags to show in widget + * @see availableFlags() + * @see setActiveFlags() + */ + void setAvailableFlags( QgsSearchWidgetWrapper::FilterFlags flags ); + + /** Returns the available filter flags shown in the widget. + * @see setAvailableFlags() + * @see activeFlags() + */ + QgsSearchWidgetWrapper::FilterFlags availableFlags() const { return mAvailableFilterFlags; } + + /** Sets the current active filter flags for the widget. Any flags + * which are not present in the available filter flags (see availableFlags()) + * will not be set. + * @param flags active flags to show in widget + * @see toggleFlag() + * @see activeFlags() + * @see setAvailableFlags() + */ + void setActiveFlags( QgsSearchWidgetWrapper::FilterFlags flags ); + + /** Toggles an individual active filter flag for the widget. Any flags + * which are not present in the available filter flags (see availableFlags()) + * will be ignore. Other flags may be cleared if they conflict with the newly + * toggled flag. + * @param flag flag to toggle + * @see setActiveFlags() + * @see activeFlags() + */ + void toggleFlag( QgsSearchWidgetWrapper::FilterFlag flag ); + + /** Returns the active filter flags shown in the widget. + * @see setActiveFlags() + * @see toggleFlag() + * @see availableFlags() + */ + QgsSearchWidgetWrapper::FilterFlags activeFlags() const { return mFilterFlags; } + + /** Returns true if the widget is set to be included in the search. + * @see setInactive() + * @see setActive() + */ + bool isActive() const; + + public slots: + + /** Sets the search widget as inactive, ie do not search the corresponding field. + * @see isActive() + * @see setActive() + */ + void setInactive(); + + /** Sets the search widget as active by selecting the first available search type. + * @see isActive() + * @see setInactive() + */ + void setActive(); + + private slots: + + void aboutToShowMenu(); + + void actionSelected(); + + void searchWidgetValueChanged(); + + private: + + QgsSearchWidgetWrapper::FilterFlags mAvailableFilterFlags; + QgsSearchWidgetWrapper::FilterFlags mFilterFlags; + QgsSearchWidgetWrapper* mSearchWrapper; + QMenu* mMenu; + + void updateState(); + +}; + +#endif // QGSSEARCHWIDGETTOOLBUTTON_H diff --git a/src/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.cpp b/src/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.cpp index 13546f07855..449932ee330 100644 --- a/src/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.cpp @@ -40,10 +40,12 @@ void QgsValueMapSearchWidgetWrapper::comboBoxIndexChanged( int idx ) if ( idx == 0 ) { clearExpression(); + emit valueCleared(); } else { setExpression( mComboBox->itemData( idx ).toString() ); + emit valueChanged(); } emit expressionChanged( mExpression ); } @@ -64,6 +66,71 @@ bool QgsValueMapSearchWidgetWrapper::valid() const return true; } +QgsSearchWidgetWrapper::FilterFlags QgsValueMapSearchWidgetWrapper::supportedFlags() const +{ + return EqualTo | NotEqualTo | IsNull; +} + +QgsSearchWidgetWrapper::FilterFlags QgsValueMapSearchWidgetWrapper::defaultFlags() const +{ + return EqualTo; +} + +QString QgsValueMapSearchWidgetWrapper::createExpression( QgsSearchWidgetWrapper::FilterFlags flags ) const +{ + //if unselect value, always pass + if ( mComboBox->currentIndex() == 0 ) + return QString(); + + //clear any unsupported flags + flags &= supportedFlags(); + + QVariant::Type fldType = layer()->fields().at( mFieldIdx ).type(); + QString fieldName = QgsExpression::quotedColumnRef( layer()->fields().at( mFieldIdx ).name() ); + + if ( flags & IsNull ) + return fieldName + " IS NULL"; + + QString currentKey = mComboBox->itemData( mComboBox->currentIndex() ).toString(); + + switch ( fldType ) + { + case QVariant::Int: + case QVariant::UInt: + case QVariant::Double: + case QVariant::LongLong: + case QVariant::ULongLong: + { + if ( flags & EqualTo ) + return fieldName + '=' + currentKey; + else if ( flags & NotEqualTo ) + return fieldName + "<>" + currentKey; + break; + } + + default: + { + if ( flags & EqualTo ) + return fieldName + "='" + currentKey + '\''; + else if ( flags & NotEqualTo ) + return fieldName + "<>'" + currentKey + '\''; + break; + } + } + + return QString(); +} + +void QgsValueMapSearchWidgetWrapper::clearWidget() +{ + mComboBox->setCurrentIndex( 0 ); +} + +void QgsValueMapSearchWidgetWrapper::setEnabled( bool enabled ) +{ + mComboBox->setEnabled( enabled ); +} + void QgsValueMapSearchWidgetWrapper::initWidget( QWidget* editor ) { mComboBox = qobject_cast( editor ); diff --git a/src/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.h b/src/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.h index e89782f3fe5..a9267475761 100644 --- a/src/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.h +++ b/src/gui/editorwidgets/qgsvaluemapsearchwidgetwrapper.h @@ -23,7 +23,6 @@ * Wraps a value map search widget. This widget will offer a combobox with values from another layer * referenced by a foreign key (a constraint may be set but is not required on data level). * It will be used as a search widget and produces expression to look for in the layer. - * \note not available in Python bindings */ class GUI_EXPORT QgsValueMapSearchWidgetWrapper : public QgsSearchWidgetWrapper @@ -34,6 +33,14 @@ class GUI_EXPORT QgsValueMapSearchWidgetWrapper : public QgsSearchWidgetWrapper bool applyDirectly() override; QString expression() override; bool valid() const override; + FilterFlags supportedFlags() const override; + FilterFlags defaultFlags() const override; + virtual QString createExpression( FilterFlags flags ) const override; + + public slots: + + virtual void clearWidget() override; + virtual void setEnabled( bool enabled ) override; protected: QWidget* createWidget( QWidget* parent ) override; diff --git a/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp b/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp index 9b488cb6c78..9f41d2eaec8 100644 --- a/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp @@ -89,22 +89,107 @@ QVariant QgsValueRelationSearchWidgetWrapper::value() const return v; } +QgsSearchWidgetWrapper::FilterFlags QgsValueRelationSearchWidgetWrapper::supportedFlags() const +{ + return EqualTo | NotEqualTo | IsNull; +} + +QgsSearchWidgetWrapper::FilterFlags QgsValueRelationSearchWidgetWrapper::defaultFlags() const +{ + return EqualTo; +} + +QString QgsValueRelationSearchWidgetWrapper::createExpression( QgsSearchWidgetWrapper::FilterFlags flags ) const +{ + QString fieldName = QgsExpression::quotedColumnRef( layer()->fields().at( mFieldIdx ).name() ); + + //clear any unsupported flags + flags &= supportedFlags(); + if ( flags & IsNull ) + return fieldName + " IS NULL"; + + QVariant v = value(); + if ( !v.isValid() ) + return QString(); + + switch ( v.type() ) + { + case QVariant::Int: + case QVariant::UInt: + case QVariant::Double: + case QVariant::LongLong: + case QVariant::ULongLong: + { + if ( flags & EqualTo ) + return fieldName + '=' + v.toString(); + else if ( flags & NotEqualTo ) + return fieldName + "<>" + v.toString(); + break; + } + + default: + { + if ( flags & EqualTo ) + return fieldName + "='" + v.toString() + '\''; + else if ( flags & NotEqualTo ) + return fieldName + "<>'" + v.toString() + '\''; + break; + } + } + + return QString(); +} + +void QgsValueRelationSearchWidgetWrapper::clearWidget() +{ + if ( mComboBox ) + { + mComboBox->setCurrentIndex( 0 ); + } + if ( mListWidget ) + { + mListWidget->clearSelection(); + } + if ( mLineEdit ) + { + mLineEdit->setText( QString() ); + } +} + +void QgsValueRelationSearchWidgetWrapper::setEnabled( bool enabled ) +{ + if ( mComboBox ) + { + mComboBox->setEnabled( enabled ); + } + if ( mListWidget ) + { + mListWidget->setEnabled( enabled ); + } + if ( mLineEdit ) + { + mLineEdit->setEnabled( enabled ); + } +} + bool QgsValueRelationSearchWidgetWrapper::valid() const { return true; } -void QgsValueRelationSearchWidgetWrapper::valueChanged() +void QgsValueRelationSearchWidgetWrapper::onValueChanged() { QVariant vl = value(); if ( !vl.isValid() ) { clearExpression(); + emit valueCleared(); } else { QSettings settings; setExpression( vl.isNull() ? settings.value( "qgis/nullValue", "NULL" ).toString() : vl.toString() ); + emit valueChanged(); } emit expressionChanged( mExpression ); } @@ -167,7 +252,7 @@ void QgsValueRelationSearchWidgetWrapper::initWidget( QWidget* editor ) mComboBox->addItem( element.second, element.first ); } - connect( mComboBox, SIGNAL( currentIndexChanged( int ) ), this, SLOT( valueChanged() ) ); + connect( mComboBox, SIGNAL( currentIndexChanged( int ) ), this, SLOT( onValueChanged() ) ); } else if ( mListWidget ) { @@ -179,7 +264,7 @@ void QgsValueRelationSearchWidgetWrapper::initWidget( QWidget* editor ) mListWidget->addItem( item ); } - connect( mListWidget, SIGNAL( itemChanged( QListWidgetItem* ) ), this, SLOT( valueChanged() ) ); + connect( mListWidget, SIGNAL( itemChanged( QListWidgetItem* ) ), this, SLOT( onValueChanged() ) ); } else if ( mLineEdit ) { @@ -193,7 +278,7 @@ void QgsValueRelationSearchWidgetWrapper::initWidget( QWidget* editor ) QCompleter* completer = new QCompleter( m, mLineEdit ); completer->setCaseSensitivity( Qt::CaseInsensitive ); mLineEdit->setCompleter( completer ); - connect( mLineEdit, SIGNAL( textChanged( QListWidgetItem* ) ), this, SLOT( valueChanged() ) ); + connect( mLineEdit, SIGNAL( textChanged( QString ) ), this, SLOT( onValueChanged() ) ); } } diff --git a/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.h b/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.h index 7f9c68dee0f..159268f6d38 100644 --- a/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.h +++ b/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.h @@ -29,7 +29,6 @@ class QgsValueRelationWidgetFactory; * Wraps a value relation search widget. This widget will offer a combobox with values from another layer * referenced by a foreign key (a constraint may be set but is not required on data level). * It will be used as a search widget and produces expression to look for in the layer. - * \note not available in Python bindings */ class GUI_EXPORT QgsValueRelationSearchWidgetWrapper : public QgsSearchWidgetWrapper @@ -46,13 +45,23 @@ class GUI_EXPORT QgsValueRelationSearchWidgetWrapper : public QgsSearchWidgetWra QString expression() override; bool valid() const override; QVariant value() const; + FilterFlags supportedFlags() const override; + FilterFlags defaultFlags() const override; + virtual QString createExpression( FilterFlags flags ) const override; + + public slots: + + virtual void clearWidget() override; + virtual void setEnabled( bool enabled ) override; protected: QWidget* createWidget( QWidget* parent ) override; void initWidget( QWidget* editor ) override; public slots: - void valueChanged(); + + //! Called when current value of search widget changes + void onValueChanged(); protected slots: void setExpression( QString exp ) override; diff --git a/src/gui/qgsattributeeditor.h b/src/gui/qgsattributeeditor.h index 019b006f69a..39f467799cf 100644 --- a/src/gui/qgsattributeeditor.h +++ b/src/gui/qgsattributeeditor.h @@ -35,6 +35,7 @@ class QgsVectorLayer; * * @deprecated */ +// TODO QGIS 3.0 - remove class GUI_EXPORT QgsAttributeEditor : public QObject { Q_OBJECT diff --git a/src/gui/qgsattributeform.cpp b/src/gui/qgsattributeform.cpp index 3bb8c2bb095..c7abc51c60a 100644 --- a/src/gui/qgsattributeform.cpp +++ b/src/gui/qgsattributeform.cpp @@ -42,6 +42,8 @@ #include #include #include +#include +#include int QgsAttributeForm::sFormCounter = 0; @@ -53,6 +55,7 @@ QgsAttributeForm::QgsAttributeForm( QgsVectorLayer* vl, const QgsFeature &featur , mMultiEditMessageBarItem( nullptr ) , mContext( context ) , mButtonBox( nullptr ) + , mSearchButtonBox( nullptr ) , mFormNr( sFormCounter++ ) , mIsSaving( false ) , mPreventFeatureRefresh( false ) @@ -166,11 +169,9 @@ void QgsAttributeForm::setMode( QgsAttributeForm::Mode mode ) w->setMode( QgsAttributeFormEditorWidget::MultiEditMode ); break; -#if 0 case QgsAttributeForm::SearchMode: w->setMode( QgsAttributeFormEditorWidget::SearchMode ); break; -#endif } } @@ -178,18 +179,26 @@ void QgsAttributeForm::setMode( QgsAttributeForm::Mode mode ) { case QgsAttributeForm::SingleEditMode: setFeature( mFeature ); + mSearchButtonBox->setVisible( false ); break; case QgsAttributeForm::AddFeatureMode: synchronizeEnabledState(); + mSearchButtonBox->setVisible( false ); break; case QgsAttributeForm::MultiEditMode: resetMultiEdit( false ); synchronizeEnabledState(); + mSearchButtonBox->setVisible( false ); + break; + + case QgsAttributeForm::SearchMode: + mSearchButtonBox->setVisible( true ); break; } + emit modeChanged( mMode ); } void QgsAttributeForm::setIsAddDialog( bool isAddDialog ) @@ -230,6 +239,7 @@ void QgsAttributeForm::setFeature( const QgsFeature& feature ) break; } case MultiEditMode: + case SearchMode: { //ignore setFeature break; @@ -368,6 +378,84 @@ void QgsAttributeForm::multiEditMessageClicked( const QString& link ) resetMultiEdit( link == "#apply" ); } +void QgsAttributeForm::filterTriggered() +{ + QString filter = createFilterExpression(); + emit filterExpressionSet( filter, ReplaceFilter ); + setMode( SingleEditMode ); +} + +void QgsAttributeForm::filterAndTriggered() +{ + QString filter = createFilterExpression(); + if ( filter.isEmpty() ) + return; + + setMode( SingleEditMode ); + emit filterExpressionSet( filter, FilterAnd ); +} + +void QgsAttributeForm::filterOrTriggered() +{ + QString filter = createFilterExpression(); + if ( filter.isEmpty() ) + return; + + setMode( SingleEditMode ); + emit filterExpressionSet( filter, FilterOr ); +} + +void QgsAttributeForm::pushSelectedFeaturesMessage() +{ + int count = mLayer->selectedFeatureCount(); + if ( count > 0 ) + { + mMessageBar->pushMessage( QString(), + tr( "%1 matching %2 selected" ).arg( count ) + .arg( count == 1 ? tr( "feature" ) : tr( "features" ) ), + QgsMessageBar::INFO, + messageTimeout() ); + } + else + { + mMessageBar->pushMessage( QString(), + tr( "No matching features found" ), + QgsMessageBar::WARNING, + messageTimeout() ); + } +} + +void QgsAttributeForm::runSearchSelect( QgsVectorLayer::SelectBehaviour behaviour ) +{ + QString filter = createFilterExpression(); + if ( filter.isEmpty() ) + return; + + mLayer->selectByExpression( filter, behaviour ); + pushSelectedFeaturesMessage(); + setMode( SingleEditMode ); +} + +void QgsAttributeForm::searchSetSelection() +{ + runSearchSelect( QgsVectorLayer::SetSelection ); +} + +void QgsAttributeForm::searchAddToSelection() +{ + runSearchSelect( QgsVectorLayer::AddToSelection ); +} + +void QgsAttributeForm::searchRemoveFromSelection() +{ + runSearchSelect( QgsVectorLayer::RemoveFromSelection ); +} + +void QgsAttributeForm::searchIntersectSelection() +{ + runSearchSelect( QgsVectorLayer::IntersectSelection ); +} + bool QgsAttributeForm::saveMultiEdits() { //find changed attributes @@ -458,6 +546,7 @@ bool QgsAttributeForm::save() { case SingleEditMode: case AddFeatureMode: + case SearchMode: success = saveEdits(); break; @@ -480,6 +569,14 @@ void QgsAttributeForm::resetValues() } } +void QgsAttributeForm::resetSearch() +{ + Q_FOREACH ( QgsAttributeFormEditorWidget* w, findChildren< QgsAttributeFormEditorWidget* >() ) + { + w->resetSearch(); + } +} + void QgsAttributeForm::clearMultiEditMessages() { if ( mMultiEditUnsavedMessageBarItem ) @@ -496,6 +593,23 @@ void QgsAttributeForm::clearMultiEditMessages() } } +QString QgsAttributeForm::createFilterExpression() const +{ + QStringList filters; + Q_FOREACH ( QgsAttributeFormEditorWidget* w, findChildren< QgsAttributeFormEditorWidget* >() ) + { + QString filter = w->currentFilterExpression(); + if ( !filter.isEmpty() ) + filters << filter; + } + + if ( filters.isEmpty() ) + return QString(); + + QString filter = filters.join( ") AND (" ).prepend( '(' ).append( ')' ); + return filter; +} + void QgsAttributeForm::onAttributeChanged( const QVariant& value ) { QgsEditorWidgetWrapper* eww = qobject_cast( sender() ); @@ -532,6 +646,9 @@ void QgsAttributeForm::onAttributeChanged( const QVariant& value ) } break; } + case SearchMode: + //nothing to do + break; } } @@ -651,6 +768,12 @@ void QgsAttributeForm::init() mButtonBox = nullptr; } + if ( mSearchButtonBox ) + { + delete mSearchButtonBox; + mSearchButtonBox = nullptr; + } + qDeleteAll( mWidgets ); mWidgets.clear(); @@ -660,17 +783,24 @@ void QgsAttributeForm::init() } delete layout(); + QVBoxLayout* vl = new QVBoxLayout(); + vl->setMargin( 0 ); + vl->setContentsMargins( 0, 0, 0, 0 ); + mMessageBar = new QgsMessageBar( this ); + mMessageBar->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed ); + vl->addWidget( mMessageBar ); + setLayout( vl ); + // Get a layout QGridLayout* layout = new QGridLayout(); - setLayout( layout ); + QWidget* container = new QWidget(); + container->setLayout( layout ); + vl->addWidget( container ); mFormEditorWidgets.clear(); // a bar to warn the user with non-blocking messages setContentsMargins( 0, 0, 0, 0 ); - mMessageBar = new QgsMessageBar( this ); - mMessageBar->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed ); - layout->addWidget( mMessageBar, 0, 0, 1, 1 ); // Try to load Ui-File for layout if ( mLayer->editFormConfig()->layout() == QgsEditFormConfig::UiFileLayout && !mLayer->editFormConfig()->uiForm().isEmpty() ) @@ -770,8 +900,12 @@ void QgsAttributeForm::init() QWidget* w = nullptr; if ( eww ) { - w = new QgsAttributeFormEditorWidget( eww, this ); - mFormEditorWidgets.insert( idx, static_cast< QgsAttributeFormEditorWidget* >( w ) ); + QgsAttributeFormEditorWidget* formWidget = new QgsAttributeFormEditorWidget( eww, this ); + w = formWidget; + mFormEditorWidgets.insert( idx, formWidget ); + QgsSearchWidgetWrapper* sww = QgsEditorWidgetRegistry::instance()->createSearchWidget( widgetType, mLayer, idx, widgetConfig, + formWidget->searchWidgetFrame(), mContext ); + formWidget->setSearchWidgetWrapper( sww ); } else { @@ -806,7 +940,9 @@ void QgsAttributeForm::init() if ( QgsProject::instance()->relationManager()->referencedRelations( mLayer ).isEmpty() ) { QSpacerItem *spacerItem = new QSpacerItem( 20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding ); - gridLayout->addItem( spacerItem, row++, 0 ); + gridLayout->addItem( spacerItem, row, 0 ); + gridLayout->setRowStretch( row, 1 ); + row++; } } @@ -816,9 +952,62 @@ void QgsAttributeForm::init() mButtonBox->setObjectName( "buttonBox" ); layout->addWidget( mButtonBox ); } - mButtonBox->setVisible( buttonBoxVisible ); + if ( !mSearchButtonBox ) + { + mSearchButtonBox = new QWidget(); + QHBoxLayout* boxLayout = new QHBoxLayout(); + boxLayout->setMargin( 0 ); + boxLayout->setContentsMargins( 0, 0, 0, 0 ); + mSearchButtonBox->setLayout( boxLayout ); + mSearchButtonBox->setObjectName( "searchButtonBox" ); + + QPushButton* clearButton = new QPushButton( tr( "Reset form" ), mSearchButtonBox ); + connect( clearButton, SIGNAL( clicked( bool ) ), this, SLOT( resetSearch() ) ); + boxLayout->addWidget( clearButton ); + boxLayout->addStretch( 1 ); + + QToolButton* selectButton = new QToolButton(); + selectButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum ); + selectButton->setText( tr( "Select features" ) ); + selectButton->setPopupMode( QToolButton::MenuButtonPopup ); + connect( selectButton, SIGNAL( clicked( bool ) ), this, SLOT( searchSetSelection() ) ); + QMenu* selectMenu = new QMenu( selectButton ); + QAction* selectAction = new QAction( tr( "Select features" ), selectMenu ); + connect( selectAction, SIGNAL( triggered( bool ) ), this, SLOT( searchSetSelection() ) ); + selectMenu->addAction( selectAction ); + QAction* addSelectAction = new QAction( tr( "Add to current selection" ), selectMenu ); + connect( addSelectAction, SIGNAL( triggered( bool ) ), this, SLOT( searchAddToSelection() ) ); + selectMenu->addAction( addSelectAction ); + QAction* filterSelectAction = new QAction( tr( "Filter current selection" ), selectMenu ); + connect( filterSelectAction, SIGNAL( triggered( bool ) ), this, SLOT( searchIntersectSelection() ) ); + selectMenu->addAction( filterSelectAction ); + QAction* deselectAction = new QAction( tr( "Remove from current selection" ), selectMenu ); + connect( deselectAction, SIGNAL( triggered( bool ) ), this, SLOT( searchRemoveFromSelection() ) ); + selectMenu->addAction( deselectAction ); + selectButton->setMenu( selectMenu ); + boxLayout->addWidget( selectButton ); + + QToolButton* filterButton = new QToolButton(); + filterButton->setText( tr( "Filter features" ) ); + filterButton->setPopupMode( QToolButton::MenuButtonPopup ); + filterButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum ); + connect( filterButton, SIGNAL( clicked( bool ) ), this, SLOT( filterTriggered() ) ); + QMenu* filterMenu = new QMenu( filterButton ); + QAction* filterAndAction = new QAction( tr( "Filter within (\"AND\")" ), filterMenu ); + connect( filterAndAction, SIGNAL( triggered( bool ) ), this, SLOT( filterAndTriggered() ) ); + filterMenu->addAction( filterAndAction ); + QAction* filterOrAction = new QAction( tr( "Extend filter (\"OR\")" ), filterMenu ); + connect( filterOrAction, SIGNAL( triggered( bool ) ), this, SLOT( filterOrTriggered() ) ); + filterMenu->addAction( filterOrAction ); + filterButton->setMenu( filterMenu ); + boxLayout->addWidget( filterButton ); + + layout->addWidget( mSearchButtonBox ); + } + mSearchButtonBox->setVisible( mMode == SearchMode ); + connectWrappers(); connect( mButtonBox, SIGNAL( accepted() ), this, SLOT( accept() ) ); @@ -970,9 +1159,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 ); + QgsAttributeFormEditorWidget* w = new QgsAttributeFormEditorWidget( eww, this ); + mFormEditorWidgets.insert( fldIdx, w ); - QWidget* w = new QgsAttributeFormEditorWidget( eww, this ); - mFormEditorWidgets.insert( fldIdx, static_cast< QgsAttributeFormEditorWidget* >( w ) ); + QgsSearchWidgetWrapper* sww = QgsEditorWidgetRegistry::instance()->createSearchWidget( widgetType, mLayer, fldIdx, widgetConfig, w->searchWidgetFrame(), mContext ); + w->setSearchWidgetWrapper( sww ); newWidgetInfo.widget = w; addWidgetWrapper( eww ); @@ -1083,6 +1274,7 @@ QgsAttributeForm::WidgetInfo QgsAttributeForm::createWidgetFromDef( const QgsAtt QWidget* spacer = new QWidget(); spacer->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Preferred ); gbLayout->addWidget( spacer, ++row, 0 ); + gbLayout->setRowStretch( row, 1 ); newWidgetInfo.labelText = QString::null; newWidgetInfo.labelOnTop = true; @@ -1240,6 +1432,7 @@ void QgsAttributeForm::layerSelectionChanged() { case SingleEditMode: case AddFeatureMode: + case SearchMode: break; case MultiEditMode: diff --git a/src/gui/qgsattributeform.h b/src/gui/qgsattributeform.h index c5011b258df..3a140ec93d7 100644 --- a/src/gui/qgsattributeform.h +++ b/src/gui/qgsattributeform.h @@ -42,7 +42,15 @@ class GUI_EXPORT QgsAttributeForm : public QWidget 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 */ + SearchMode, /*!< Form values are used for searching/filtering the layer */ + }; + + //! Filter types + enum FilterType + { + ReplaceFilter, /*!< Filter should replace any existing filter */ + FilterAnd, /*!< Filter should be combined using "AND" */ + FilterOr, /*!< Filter should be combined using "OR" */ }; explicit QgsAttributeForm( QgsVectorLayer* vl, const QgsFeature &feature = QgsFeature(), const QgsAttributeEditorContext& context = QgsAttributeEditorContext(), QWidget *parent = nullptr ); @@ -156,6 +164,18 @@ class GUI_EXPORT QgsAttributeForm : public QWidget */ void featureSaved( const QgsFeature& feature ); + /** Is emitted when a filter expression is set using the form. + * @param expression filter expression + * @param type filter type + * @note added in QGIS 2.16 + */ + void filterExpressionSet( const QString& expression, QgsAttributeForm::FilterType type ); + + /** Emitted when the form changes mode. + * @param mode new mode + */ + void modeChanged( QgsAttributeForm::Mode mode ); + public slots: /** * Call this to change the content of a given attribute. Will update the editor(s) related to this field. @@ -198,6 +218,11 @@ class GUI_EXPORT QgsAttributeForm : public QWidget */ void resetValues(); + /** Resets the search/filter form values. + * @note added in QGIS 2.16 + */ + void resetSearch(); + /** * reload current feature */ @@ -218,6 +243,14 @@ class GUI_EXPORT QgsAttributeForm : public QWidget void resetMultiEdit( bool promptToSave = false ); void multiEditMessageClicked( const QString& link ); + void filterAndTriggered(); + void filterOrTriggered(); + void filterTriggered(); + + void searchSetSelection(); + void searchAddToSelection(); + void searchRemoveFromSelection(); + void searchIntersectSelection(); private: void init(); @@ -258,6 +291,10 @@ class GUI_EXPORT QgsAttributeForm : public QWidget int messageTimeout(); void clearMultiEditMessages(); + void pushSelectedFeaturesMessage(); + void runSearchSelect( QgsVectorLayer::SelectBehaviour behaviour ); + + QString createFilterExpression() const; QgsVectorLayer* mLayer; QgsFeature mFeature; @@ -267,6 +304,7 @@ class GUI_EXPORT QgsAttributeForm : public QWidget QList mWidgets; QgsAttributeEditorContext mContext; QDialogButtonBox* mButtonBox; + QWidget* mSearchButtonBox; QList mInterfaces; QMap< int, QgsAttributeFormEditorWidget* > mFormEditorWidgets; diff --git a/src/gui/qgsattributeformeditorwidget.cpp b/src/gui/qgsattributeformeditorwidget.cpp index ded27e4d9c0..3a21c14d93f 100644 --- a/src/gui/qgsattributeformeditorwidget.cpp +++ b/src/gui/qgsattributeformeditorwidget.cpp @@ -16,13 +16,18 @@ #include "qgsattributeformeditorwidget.h" #include "qgsattributeform.h" #include "qgsmultiedittoolbutton.h" +#include "qgssearchwidgettoolbutton.h" #include "qgseditorwidgetwrapper.h" +#include "qgssearchwidgetwrapper.h" #include #include +#include -QgsAttributeFormEditorWidget::QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, QgsAttributeForm* form ) +QgsAttributeFormEditorWidget::QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, + QgsAttributeForm* form ) : QWidget( form ) , mWidget( editorWidget ) + , mSearchWidget( nullptr ) , mForm( form ) , mMode( DefaultMode ) , mMultiEditButton( new QgsMultiEditToolButton() ) @@ -30,13 +35,42 @@ QgsAttributeFormEditorWidget::QgsAttributeFormEditorWidget( QgsEditorWidgetWrapp , mIsMixed( false ) , mIsChanged( false ) { + mEditPage = new QWidget(); + QHBoxLayout* l = new QHBoxLayout(); + l->setMargin( 0 ); + l->setContentsMargins( 0, 0, 0, 0 ); + mEditPage->setLayout( l ); + + l = new QHBoxLayout(); + l->setMargin( 0 ); + l->setContentsMargins( 0, 0, 0, 0 ); + mSearchFrame = new QWidget(); + mSearchFrame->setLayout( l ); + + mSearchPage = new QWidget(); + l = new QHBoxLayout(); + l->setMargin( 0 ); + l->setContentsMargins( 0, 0, 0, 0 ); + mSearchPage->setLayout( l ); + l->addWidget( mSearchFrame, 1 ); + mSearchWidgetToolButton = new QgsSearchWidgetToolButton(); + l->addWidget( mSearchWidgetToolButton, 0 ); + + + mStack = new QStackedWidget; + mStack->addWidget( mEditPage ); + mStack->addWidget( mSearchPage ); + + l = new QHBoxLayout(); + l->setMargin( 0 ); + l->setContentsMargins( 0, 0, 0, 0 ); + setLayout( l ); + l->addWidget( mStack ); + if ( !mWidget || !mForm ) return; - QLayout* l = new QHBoxLayout(); - l->setMargin( 0 ); - l->setContentsMargins( 0, 0, 0, 0 ); - l->addWidget( mWidget->widget() ); + mEditPage->layout()->addWidget( mWidget->widget() ); if ( mWidget->widget() ) { @@ -48,7 +82,6 @@ QgsAttributeFormEditorWidget::QgsAttributeFormEditorWidget( QgsEditorWidgetWrapp mMultiEditButton->setField( mWidget->field() ); - setLayout( l ); updateWidgets(); } @@ -58,6 +91,13 @@ QgsAttributeFormEditorWidget::~QgsAttributeFormEditorWidget() delete mMultiEditButton; } +void QgsAttributeFormEditorWidget::setSearchWidgetWrapper( QgsSearchWidgetWrapper* wrapper ) +{ + mSearchWidget = wrapper; + mSearchFrame->layout()->addWidget( wrapper->widget() ); + mSearchWidgetToolButton->setSearchWidgetWrapper( wrapper ); +} + void QgsAttributeFormEditorWidget::setMode( QgsAttributeFormEditorWidget::Mode mode ) { mMode = mode; @@ -82,6 +122,11 @@ void QgsAttributeFormEditorWidget::changesCommitted() mIsChanged = false; } +void QgsAttributeFormEditorWidget::resetSearch() +{ + mSearchWidgetToolButton->setInactive(); +} + void QgsAttributeFormEditorWidget::initialize( const QVariant& initialValue, bool mixedValues ) { if ( mWidget ) @@ -101,6 +146,19 @@ QVariant QgsAttributeFormEditorWidget::currentValue() const return mWidget->value(); } +QString QgsAttributeFormEditorWidget::currentFilterExpression() const +{ + if ( !mSearchWidgetToolButton->isActive() ) + return QString(); + + return mSearchWidget->createExpression( mSearchWidgetToolButton->activeFlags() ); +} + +QWidget* QgsAttributeFormEditorWidget::searchWidgetFrame() +{ + return mSearchFrame; +} + void QgsAttributeFormEditorWidget::editorWidgetChanged( const QVariant& value ) { if ( mBlockValueUpdate ) @@ -111,6 +169,7 @@ void QgsAttributeFormEditorWidget::editorWidgetChanged( const QVariant& value ) switch ( mMode ) { case DefaultMode: + case SearchMode: break; case MultiEditMode: mMultiEditButton->setIsChanged( true ); @@ -130,6 +189,7 @@ void QgsAttributeFormEditorWidget::resetValue() switch ( mMode ) { case DefaultMode: + case SearchMode: break; case MultiEditMode: { @@ -146,6 +206,11 @@ void QgsAttributeFormEditorWidget::setFieldTriggered() mIsChanged = true; } +QgsSearchWidgetToolButton* QgsAttributeFormEditorWidget::searchWidgetToolButton() +{ + return mSearchWidgetToolButton; +} + QgsVectorLayer* QgsAttributeFormEditorWidget::layer() { return mForm ? mForm->layer() : nullptr; @@ -153,14 +218,15 @@ QgsVectorLayer* QgsAttributeFormEditorWidget::layer() void QgsAttributeFormEditorWidget::updateWidgets() { - bool hasMultiEditButton = ( layout()->indexOf( mMultiEditButton ) >= 0 ); + //first update the tool buttons + bool hasMultiEditButton = ( mEditPage->layout()->indexOf( mMultiEditButton ) >= 0 ); bool fieldReadOnly = layer()->editFormConfig()->readOnly( mWidget->fieldIdx() ); if ( hasMultiEditButton ) { if ( mMode != MultiEditMode || fieldReadOnly ) { - layout()->removeWidget( mMultiEditButton ); + mEditPage->layout()->removeWidget( mMultiEditButton ); mMultiEditButton->setParent( nullptr ); } } @@ -168,7 +234,23 @@ void QgsAttributeFormEditorWidget::updateWidgets() { if ( mMode == MultiEditMode && !fieldReadOnly ) { - layout()->addWidget( mMultiEditButton ); + mEditPage->layout()->addWidget( mMultiEditButton ); + } + } + + switch ( mMode ) + { + case DefaultMode: + case MultiEditMode: + { + mStack->setCurrentWidget( mEditPage ); + break; + } + + case SearchMode: + { + mStack->setCurrentWidget( mSearchPage ); + break; } } } diff --git a/src/gui/qgsattributeformeditorwidget.h b/src/gui/qgsattributeformeditorwidget.h index eb4b52ed5ef..a9fbda2f2d8 100644 --- a/src/gui/qgsattributeformeditorwidget.h +++ b/src/gui/qgsattributeformeditorwidget.h @@ -22,8 +22,10 @@ class QgsAttributeForm; class QgsEditorWidgetWrapper; class QgsMultiEditToolButton; +class QgsSearchWidgetToolButton; class QgsVectorLayer; - +class QgsSearchWidgetWrapper; +class QStackedWidget; /** \ingroup gui * \class QgsAttributeFormEditorWidget @@ -44,17 +46,32 @@ class GUI_EXPORT QgsAttributeFormEditorWidget : public QWidget { 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 */ + SearchMode, /*!< Layer search/filter mode */ }; /** Constructor for QgsAttributeFormEditorWidget. - * @param editorWidget associated editor widget wrapper + * @param editorWidget associated editor widget wrapper (for default/edit modes) * @param form parent attribute form */ - explicit QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, QgsAttributeForm* form ); + explicit QgsAttributeFormEditorWidget( QgsEditorWidgetWrapper* editorWidget, + QgsAttributeForm* form ); ~QgsAttributeFormEditorWidget(); + /** Sets the search widget wrapper for the widget used when the form is in + * search mode. + * @param wrapper search widget wrapper. + * @note the search widget wrapper should be created using searchWidgetFrame() + * as its parent + */ + void setSearchWidgetWrapper( QgsSearchWidgetWrapper* wrapper ); + + /** Returns the widget which should be used as a parent during construction + * of the search widget wrapper. + * @see setSearchWidgetWrapper() + */ + QWidget* searchWidgetFrame(); + /** 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 @@ -82,6 +99,12 @@ class GUI_EXPORT QgsAttributeFormEditorWidget : public QWidget */ QVariant currentValue() const; + /** Creates an expression matching the current search filter value and + * search properties represented in the widget. + * @note added in QGIS 2.16 + */ + QString currentFilterExpression() const; + public slots: /** Sets whether the widget should be displayed in a "mixed values" mode. @@ -93,6 +116,10 @@ class GUI_EXPORT QgsAttributeFormEditorWidget : public QWidget */ void changesCommitted(); + /** Resets the search/filter value of the widget. + */ + void resetSearch(); + signals: //! Emitted when the widget's value changes @@ -110,13 +137,28 @@ class GUI_EXPORT QgsAttributeFormEditorWidget : public QWidget //! Triggered when the multi edit tool button "set field value" action is selected void setFieldTriggered(); + protected: + + /** Returns a pointer to the search widget tool button in the widget. + * @note this method is in place for unit testing only, and is not considered + * stable API + */ + QgsSearchWidgetToolButton* searchWidgetToolButton(); + private: + QWidget* mEditPage; + QWidget* mSearchPage; + QStackedWidget* mStack; + QWidget* mSearchFrame; + QgsEditorWidgetWrapper* mWidget; + QgsSearchWidgetWrapper* mSearchWidget; QgsAttributeForm* mForm; Mode mMode; QgsMultiEditToolButton* mMultiEditButton; + QgsSearchWidgetToolButton* mSearchWidgetToolButton; QVariant mPreviousValue; bool mBlockValueUpdate; bool mIsMixed; diff --git a/src/ui/qgsattributetabledialog.ui b/src/ui/qgsattributetabledialog.ui index f47b56b5de1..f5c6be481a6 100644 --- a/src/ui/qgsattributetabledialog.ui +++ b/src/ui/qgsattributetabledialog.ui @@ -322,6 +322,32 @@ + + + + Select/filter features using form + + + ... + + + + :/images/themes/default/mActionFilter2.svg:/images/themes/default/mActionFilter2.svg + + + + 18 + 18 + + + + true + + + true + + + diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 282c8a5be5e..379e8a787b4 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -11,6 +11,7 @@ ADD_PYTHON_TEST(PyQgsAggregateCalculator test_qgsaggregatecalculator.py) ADD_PYTHON_TEST(PyQgsAnalysis test_qgsanalysis.py) ADD_PYTHON_TEST(PyQgsApplication test_qgsapplication.py) ADD_PYTHON_TEST(PyQgsAtlasComposition test_qgsatlascomposition.py) +ADD_PYTHON_TEST(PyQgsAttributeFormEditorWidget test_qgsattributeformeditorwidget.py) ADD_PYTHON_TEST(PyQgsAttributeTableModel test_qgsattributetablemodel.py) #ADD_PYTHON_TEST(PyQgsAuthenticationSystem test_qgsauthsystem.py) ADD_PYTHON_TEST(PyQgsBlendModes test_qgsblendmodes.py) @@ -68,6 +69,8 @@ ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) ADD_PYTHON_TEST(PyQgsTabfileProvider test_provider_tabfile.py) ADD_PYTHON_TEST(PyQgsOGRProvider test_provider_ogr.py) +ADD_PYTHON_TEST(PyQgsSearchWidgetToolButton test_qgssearchwidgettoolbutton.py) +ADD_PYTHON_TEST(PyQgsSearchWidgetWrapper test_qgssearchwidgetwrapper.py) ADD_PYTHON_TEST(PyQgsSpatialIndex test_qgsspatialindex.py) ADD_PYTHON_TEST(PyQgsSpatialiteProvider test_provider_spatialite.py) ADD_PYTHON_TEST(PyQgsSQLStatement test_qgssqlstatement.py) diff --git a/tests/src/python/test_qgsattributeformeditorwidget.py b/tests/src/python/test_qgsattributeformeditorwidget.py new file mode 100644 index 00000000000..a18b8b5ff29 --- /dev/null +++ b/tests/src/python/test_qgsattributeformeditorwidget.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsAttributeFormEditorWidget. + +.. 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__ = '2016-05' +__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 # NOQA + +from qgis.gui import (QgsSearchWidgetWrapper, + QgsAttributeFormEditorWidget, + QgsSearchWidgetToolButton, + QgsDefaultSearchWidgetWrapper) +from qgis.core import (QgsVectorLayer) +from qgis.PyQt.QtWidgets import QWidget +from qgis.testing import start_app, unittest + +start_app() + + +class PyQgsAttributeFormEditorWidget(unittest.TestCase): + + def testCurrentFilterExpression(self): + """ Test creating an expression using the widget""" + + layer = QgsVectorLayer("Point?field=fldint:integer", "test", "memory") + parent = QWidget() + w = QgsDefaultSearchWidgetWrapper(layer, 0, parent) + af = QgsAttributeFormEditorWidget(None, None) + af.setSearchWidgetWrapper(w) + + # test that filter combines both current value in search widget wrapper and flags from search tool button + w.lineEdit().setText('5.5') + af.searchWidgetToolButton().setActiveFlags(QgsSearchWidgetWrapper.EqualTo) + self.assertEquals(af.currentFilterExpression(), '"fldint"=5.5') + af.searchWidgetToolButton().setActiveFlags(QgsSearchWidgetWrapper.NotEqualTo) + self.assertEquals(af.currentFilterExpression(), '"fldint"<>5.5') + + def testSetActive(self): + """ Test setting the search as active - should set active flags to match search widget wrapper's defaults """ + + layer = QgsVectorLayer("Point?field=fldtext:string&field=fldint:integer", "test", "memory") + parent = QWidget() + w = QgsDefaultSearchWidgetWrapper(layer, 0, parent) + af = QgsAttributeFormEditorWidget(None, None) + af.setSearchWidgetWrapper(w) + + sb = af.searchWidgetToolButton() + # start with inactive + sb.setActiveFlags(QgsSearchWidgetWrapper.FilterFlags()) + # set to inactive + sb.setActive() + # check that correct default flag was taken from search widget wrapper + self.assertTrue(sb.activeFlags() & QgsSearchWidgetWrapper.Contains) + + # try again with numeric field - default should be "EqualTo" + w = QgsDefaultSearchWidgetWrapper(layer, 1, parent) + af.setSearchWidgetWrapper(w) + # start with inactive + sb.setActiveFlags(QgsSearchWidgetWrapper.FilterFlags()) + # set to inactive + sb.setActive() + # check that correct default flag was taken from search widget wrapper + self.assertTrue(sb.activeFlags() & QgsSearchWidgetWrapper.EqualTo) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgssearchwidgettoolbutton.py b/tests/src/python/test_qgssearchwidgettoolbutton.py new file mode 100644 index 00000000000..3c8ac172c2c --- /dev/null +++ b/tests/src/python/test_qgssearchwidgettoolbutton.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsSearchWidgetToolButton. + +.. 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__ = '18/05/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 QgsSearchWidgetToolButton, QgsSearchWidgetWrapper + +from qgis.testing import (start_app, + unittest + ) + +start_app() + + +class TestQgsSearchWidgetToolButton(unittest.TestCase): + + def testAvailableFlags(self): + """ + Test setting available flags + """ + w = QgsSearchWidgetToolButton() + w.setAvailableFlags(QgsSearchWidgetWrapper.EqualTo | + QgsSearchWidgetWrapper.NotEqualTo | + QgsSearchWidgetWrapper.CaseInsensitive) + + flags = w.availableFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertTrue(flags & QgsSearchWidgetWrapper.NotEqualTo) + self.assertTrue(flags & QgsSearchWidgetWrapper.CaseInsensitive) + self.assertFalse(flags & QgsSearchWidgetWrapper.Between) + + # setting available flags should update active flags + w.setActiveFlags(QgsSearchWidgetWrapper.NotEqualTo | QgsSearchWidgetWrapper.CaseInsensitive) + w.setAvailableFlags(QgsSearchWidgetWrapper.EqualTo | + QgsSearchWidgetWrapper.NotEqualTo) + flags = w.activeFlags() + self.assertFalse(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertTrue(flags & QgsSearchWidgetWrapper.NotEqualTo) + self.assertFalse(flags & QgsSearchWidgetWrapper.CaseInsensitive) + + def testActiveFlags(self): + """ + Test setting/retrieving active flag logic + """ + w = QgsSearchWidgetToolButton() + w.setAvailableFlags(QgsSearchWidgetWrapper.EqualTo | + QgsSearchWidgetWrapper.NotEqualTo | + QgsSearchWidgetWrapper.CaseInsensitive) + + w.setActiveFlags(QgsSearchWidgetWrapper.EqualTo) + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertFalse(flags & QgsSearchWidgetWrapper.NotEqualTo) + + w.setActiveFlags(QgsSearchWidgetWrapper.EqualTo | QgsSearchWidgetWrapper.CaseInsensitive) + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertTrue(flags & QgsSearchWidgetWrapper.CaseInsensitive) + + # setting a non-available flag as active + w.setAvailableFlags(QgsSearchWidgetWrapper.EqualTo | + QgsSearchWidgetWrapper.NotEqualTo) + w.setActiveFlags(QgsSearchWidgetWrapper.EqualTo | QgsSearchWidgetWrapper.CaseInsensitive) + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertFalse(flags & QgsSearchWidgetWrapper.CaseInsensitive) + + # setting conflicting flags + w.setActiveFlags(QgsSearchWidgetWrapper.EqualTo | QgsSearchWidgetWrapper.NotEqualTo) + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertFalse(flags & QgsSearchWidgetWrapper.NotEqualTo) + + def testToggleFlag(self): + """ Test toggling flags """ + w = QgsSearchWidgetToolButton() + w.setAvailableFlags(QgsSearchWidgetWrapper.EqualTo | + QgsSearchWidgetWrapper.NotEqualTo | + QgsSearchWidgetWrapper.CaseInsensitive) + w.setActiveFlags(QgsSearchWidgetWrapper.EqualTo) + # should set flag + w.toggleFlag(QgsSearchWidgetWrapper.CaseInsensitive) + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertFalse(flags & QgsSearchWidgetWrapper.NotEqualTo) + self.assertTrue(flags & QgsSearchWidgetWrapper.CaseInsensitive) + # should clear flag + w.toggleFlag(QgsSearchWidgetWrapper.CaseInsensitive) + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertFalse(flags & QgsSearchWidgetWrapper.NotEqualTo) + self.assertFalse(flags & QgsSearchWidgetWrapper.CaseInsensitive) + + #toggling non-available flag should be ignored + w.setAvailableFlags(QgsSearchWidgetWrapper.Between | + QgsSearchWidgetWrapper.NotEqualTo) + w.setActiveFlags(QgsSearchWidgetWrapper.Between) + # should be ignored + w.toggleFlag(QgsSearchWidgetWrapper.CaseInsensitive) + w.toggleFlag(QgsSearchWidgetWrapper.LessThan) + flags = w.activeFlags() + self.assertFalse(flags & QgsSearchWidgetWrapper.CaseInsensitive) + self.assertFalse(flags & QgsSearchWidgetWrapper.LessThan) + self.assertTrue(flags & QgsSearchWidgetWrapper.Between) + + # toggling exclusive flag should result in other exclusive flags being cleared + w.setAvailableFlags(QgsSearchWidgetWrapper.Between | + QgsSearchWidgetWrapper.NotEqualTo | + QgsSearchWidgetWrapper.CaseInsensitive) + w.setActiveFlags(QgsSearchWidgetWrapper.Between | QgsSearchWidgetWrapper.CaseInsensitive) + w.toggleFlag(QgsSearchWidgetWrapper.Between) + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.CaseInsensitive) + self.assertFalse(flags & QgsSearchWidgetWrapper.NotEqualTo) + self.assertTrue(flags & QgsSearchWidgetWrapper.Between) + w.toggleFlag(QgsSearchWidgetWrapper.NotEqualTo) + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.CaseInsensitive) + self.assertTrue(flags & QgsSearchWidgetWrapper.NotEqualTo) + self.assertFalse(flags & QgsSearchWidgetWrapper.Between) + + def testSetInactive(self): + """ Test setting the search as inactive """ + w = QgsSearchWidgetToolButton() + w.setAvailableFlags(QgsSearchWidgetWrapper.EqualTo | + QgsSearchWidgetWrapper.NotEqualTo | + QgsSearchWidgetWrapper.CaseInsensitive) + w.setActiveFlags(QgsSearchWidgetWrapper.EqualTo | + QgsSearchWidgetWrapper.CaseInsensitive) + self.assertTrue(w.isActive()) + w.setInactive() + flags = w.activeFlags() + self.assertFalse(flags & QgsSearchWidgetWrapper.EqualTo) + self.assertTrue(flags & QgsSearchWidgetWrapper.CaseInsensitive) + self.assertFalse(w.isActive()) + + def testSetActive(self): + """ Test setting the search as active """ + w = QgsSearchWidgetToolButton() + w.setAvailableFlags(QgsSearchWidgetWrapper.Between | + QgsSearchWidgetWrapper.NotEqualTo | + QgsSearchWidgetWrapper.CaseInsensitive) + w.setActiveFlags(QgsSearchWidgetWrapper.CaseInsensitive) + self.assertFalse(w.isActive()) + w.setActive() + flags = w.activeFlags() + self.assertTrue(flags & QgsSearchWidgetWrapper.NotEqualTo) + self.assertTrue(flags & QgsSearchWidgetWrapper.CaseInsensitive) + self.assertTrue(w.isActive()) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgssearchwidgetwrapper.py b/tests/src/python/test_qgssearchwidgetwrapper.py new file mode 100644 index 00000000000..bb7a878546e --- /dev/null +++ b/tests/src/python/test_qgssearchwidgetwrapper.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsSearchWidgetWrapper. + +.. 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__ = '2016-05' +__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 # NOQA + +from qgis.gui import (QgsSearchWidgetWrapper, + QgsDefaultSearchWidgetWrapper, + QgsValueMapSearchWidgetWrapper, + QgsValueRelationSearchWidgetWrapper) +from qgis.core import (QgsVectorLayer, + QgsFeature, + QgsMapLayerRegistry, + ) +from qgis.PyQt.QtWidgets import QWidget + +from qgis.testing import start_app, unittest + +start_app() + + +class PyQgsSearchWidgetWrapper(unittest.TestCase): + + def testFlagToString(self): + # test converting QgsSearchWidgetWrapper.FilterFlag to string + tests = [QgsSearchWidgetWrapper.EqualTo, + QgsSearchWidgetWrapper.NotEqualTo, + QgsSearchWidgetWrapper.GreaterThan, + QgsSearchWidgetWrapper.LessThan, + QgsSearchWidgetWrapper.GreaterThanOrEqualTo, + QgsSearchWidgetWrapper.LessThanOrEqualTo, + QgsSearchWidgetWrapper.Between, + QgsSearchWidgetWrapper.CaseInsensitive, + QgsSearchWidgetWrapper.Contains, + QgsSearchWidgetWrapper.DoesNotContain, + QgsSearchWidgetWrapper.IsNull + ] + for t in tests: + self.assertTrue(len(QgsSearchWidgetWrapper.toString(t)) > 0) + + def testExclusiveFlags(self): + # test flag exclusive/non exclusive + exclusive = QgsSearchWidgetWrapper.exclusiveFilterFlags() + non_exclusive = QgsSearchWidgetWrapper.nonExclusiveFilterFlags() + for e in exclusive: + self.assertFalse(e in non_exclusive) + + +class PyQgsDefaultSearchWidgetWrapper(unittest.TestCase): + + def testCreateExpression(self): + """ Test creating an expression using the widget""" + layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer&field=flddate:datetime", + "test", "memory") + + parent = QWidget() + w = QgsDefaultSearchWidgetWrapper(layer, 0) + w.initWidget(parent) + + line_edit = w.lineEdit() + line_edit.setText('test') + case_sensitive = w.caseSensitiveCheckBox() + + case_sensitive.setChecked(False) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldtxt" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), 'lower("fldtxt")=lower(\'test\')') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), 'lower("fldtxt")<>lower(\'test\')') + case_sensitive.setChecked(True) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldtxt"=\'test\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldtxt"<>\'test\'') + case_sensitive.setChecked(False) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.Contains), '"fldtxt" ILIKE \'%test%\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.DoesNotContain), 'NOT ("fldtxt" ILIKE \'%test%\')') + case_sensitive.setChecked(True) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.Contains), '"fldtxt" LIKE \'%test%\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.DoesNotContain), 'NOT ("fldtxt" LIKE \'%test%\')') + case_sensitive.setChecked(False) + + # numeric field + parent = QWidget() + w = QgsDefaultSearchWidgetWrapper(layer, 1) + w.initWidget(parent) + + # may need updating if widget layout changes: + line_edit = w.lineEdit() + line_edit.setText('5.5') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldint"=5.5') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldint"<>5.5') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.GreaterThan), '"fldint">5.5') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.LessThan), '"fldint"<5.5') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.GreaterThanOrEqualTo), '"fldint">=5.5') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.LessThanOrEqualTo), '"fldint"<=5.5') + + # date/time/datetime + parent = QWidget() + w = QgsDefaultSearchWidgetWrapper(layer, 2) + w.initWidget(parent) + + # may need updating if widget layout changes: + line_edit = w.lineEdit() + line_edit.setText('2015-06-03') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"flddate"=\'2015-06-03\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"flddate"<>\'2015-06-03\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.GreaterThan), '"flddate">\'2015-06-03\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.LessThan), '"flddate"<\'2015-06-03\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.GreaterThanOrEqualTo), '"flddate">=\'2015-06-03\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.LessThanOrEqualTo), '"flddate"<=\'2015-06-03\'') + + +class PyQgsValueMapSearchWidgetWrapper(unittest.TestCase): + + def testCreateExpression(self): + """ Test creating an expression using the widget""" + layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer", "test", "memory") + + w = QgsValueMapSearchWidgetWrapper(layer, 0) + config = {"val1": 1, + "val2": 200} + w.setConfig(config) + c = w.widget() + + # first, set it to the "select value" item + c.setCurrentIndex(0) + + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '') + + c.setCurrentIndex(1) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldtxt" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldtxt"=\'1\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldtxt"<>\'1\'') + c.setCurrentIndex(2) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldtxt" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldtxt"=\'200\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldtxt"<>\'200\'') + + # try with numeric field + w = QgsValueMapSearchWidgetWrapper(layer, 1) + w.setConfig(config) + c = w.widget() + c.setCurrentIndex(1) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldint" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldint"=1') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldint"<>1') + + +class PyQgsValueRelationSearchWidgetWrapper(unittest.TestCase): + + def testCreateExpression(self): + """ Test creating an expression using the widget""" + layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer", "test", "memory") + # setup value relation + parent_layer = QgsVectorLayer("Point?field=stringkey:string&field=intkey:integer&field=display:string", "parent", "memory") + f1 = QgsFeature(parent_layer.fields(), 1) + f1.setAttributes(['a', 1, 'value a']) + f2 = QgsFeature(parent_layer.fields(), 2) + f2.setAttributes(['b', 2, 'value b']) + f3 = QgsFeature(parent_layer.fields(), 3) + f3.setAttributes(['c', 3, 'value c']) + parent_layer.dataProvider().addFeatures([f1, f2, f3]) + QgsMapLayerRegistry.instance().addMapLayers([layer, parent_layer]) + + config = {"Layer": parent_layer.id(), + "Key": 'stringkey', + "Value": 'display'} + + w = QgsValueRelationSearchWidgetWrapper(layer, 0) + w.setConfig(config) + c = w.widget() + + # first, set it to the "select value" item + c.setCurrentIndex(0) + + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldtxt" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '') + + c.setCurrentIndex(1) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldtxt" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldtxt"=\'a\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldtxt"<>\'a\'') + c.setCurrentIndex(2) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldtxt" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldtxt"=\'b\'') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldtxt"<>\'b\'') + + # try with numeric field + w = QgsValueRelationSearchWidgetWrapper(layer, 1) + config['Key'] = 'intkey' + w.setConfig(config) + c = w.widget() + c.setCurrentIndex(c.findText('value c')) + self.assertEqual(c.currentIndex(), 3) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldint" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldint"=3') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldint"<>3') + + # try with allow null set + w = QgsValueRelationSearchWidgetWrapper(layer, 1) + config['AllowNull'] = True + w.setConfig(config) + c = w.widget() + c.setCurrentIndex(c.findText('value c')) + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldint" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldint"=3') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldint"<>3') + + # try with line edit + w = QgsValueRelationSearchWidgetWrapper(layer, 1) + config['UseCompleter'] = True + w.setConfig(config) + l = w.widget() + l.setText('value b') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.IsNull), '"fldint" IS NULL') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.EqualTo), '"fldint"=2') + self.assertEquals(w.createExpression(QgsSearchWidgetWrapper.NotEqualTo), '"fldint"<>2') + +if __name__ == '__main__': + unittest.main()