Skip to content

Commit

Permalink
multivalue selection for value relation
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasMizera authored and PeterPetrik committed Nov 17, 2020
1 parent 182a388 commit c4be1bd
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 45 deletions.
1 change: 1 addition & 0 deletions src/quickgui/images/ic_angle_right.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/quickgui/images/images.qrc
Expand Up @@ -13,5 +13,6 @@
<file>ic_gallery.svg</file>
<file>ic_today.svg</file>
<file>ic_back.svg</file>
<file>ic_angle_right.svg</file>
</qresource>
</RCC>
219 changes: 186 additions & 33 deletions src/quickgui/plugin/editor/qgsquickvaluerelation.qml
Expand Up @@ -24,23 +24,25 @@ import QgsQuick 0.1 as QgsQuick
* Do not use directly from Application QML
*/
Item {
signal valueChanged(var value, bool isNull)
signal valueChanged( var value, bool isNull )

property var fieldName: field.name
property bool allowMultipleValues: config['AllowMulti']
property var widgetValue: value
property var currentFeatureLayerPair: featurePair

function itemSelected( index ) {
combobox.itemClicked( index )
}
property var model: QgsQuick.FeaturesListModel {
id: vrModel

function openCombobox() {
combobox.popup.open()
}

// Called when data in different fields are changed.
function dataUpdated(feature) {
vrModel.currentFeature = feature
// recalculate index when model changes
onModelReset: {
combobox.currentIndex = vrModel.rowFromAttribute( QgsQuick.FeaturesListModel.KeyColumn, value )
updateField()
}
}

id: fieldItem

enabled: !readOnly
height: customStyle.fields.height
anchors {
Expand All @@ -49,46 +51,197 @@ Item {
rightMargin: 10 * QgsQuick.Utils.dp
}

states: [
State {
name: "textfield"
when: allowMultipleValues || vrModel.featuresCount > customWidget.valueRelationLimit
PropertyChanges {
target: textField
visible: true
}
PropertyChanges {
target: combobox
visible: false
}
},
State {
name: "combobox"
when: state !== "textfield"
PropertyChanges {
target: textField
visible: false
}
PropertyChanges {
target: combobox
visible: true
}
}
]

Component.onCompleted: vrModel.setupValueRelation( config )

/**
* setValue sets value for value relation field
* function accepts feature id as either a single value or an array of values
* if array is passed, ids are converted to Key model column and used as multivalues
*/
function setValue( featureIds ) {

if ( Array.isArray(featureIds) && allowMultipleValues )
{
// construct JSON-like value list of key column
// { val1, val2, val3, ... }

let keys = featureIds.map( id => vrModel.attributeFromValue( QgsQuick.FeaturesListModel.FeatureId, id, QgsQuick.FeaturesListModel.KeyColumn ) )
let valueList = '{' + keys.join(',') + '}'

valueChanged(valueList, false)
}

else {
valueChanged(
vrModel.attributeFromValue(
QgsQuick.FeaturesListModel.FeatureId,
featureIds,
QgsQuick.FeaturesListModel.KeyColumn
),
false)
}
}

function valueRelationClicked() {
if ( state === "combobox" ) openCombobox()
else customWidget.valueRelationOpened( fieldItem, vrModel )
}

function openCombobox() {
combobox.popup.open()
}

// Called when data in different fields are changed.
function dataUpdated( feature ) {
vrModel.currentFeature = feature
combobox.popup.close()
}

/**
* in order to get values as
*/
function getCurrentValueAsFeatureId() {
if ( allowMultipleValues && widgetValue != null && widgetValue.startsWith('{') )
{
let arr = vrModel.convertMultivalueFormat( widgetValue, QgsQuick.FeaturesListModel.FeatureId )
return Object.values(arr)
}

return undefined
}

function updateField() {

if ( vrModel.currentFeature !== currentFeatureLayerPair.feature )
vrModel.currentFeature = currentFeatureLayerPair.feature

if ( widgetValue == null || widgetValue === "" ) {
textField.clear()
combobox.currentIndex = -1
return
}

if ( state == "textfield" ) {
if ( allowMultipleValues && widgetValue.startsWith('{') )
{
let strings = vrModel.convertMultivalueFormat( widgetValue )
textField.text = strings.join(", ")
}
else
textField.text = vrModel.attributeFromValue( QgsQuick.FeaturesListModel.KeyColumn, widgetValue, QgsQuick.FeaturesListModel.FeatureTitle )
}
else {
combobox.currentIndex = vrModel.rowFromAttribute( QgsQuick.FeaturesListModel.KeyColumn, widgetValue )
}
}

/**
* onWidgetValueChanged signal updates value of either custom valueRelation widget or combobox widget
*/
onWidgetValueChanged: updateField()

Item {
id: textFieldContainer
anchors.fill: parent

TextField {
id: textField
anchors.fill: parent
readOnly: true
placeholderText: "Choose a " + fieldName
font.pixelSize: customStyle.fields.fontPixelSize
color: customStyle.fields.fontColor
topPadding: 10 * QgsQuick.Utils.dp
bottomPadding: 10 * QgsQuick.Utils.dp

MouseArea {
anchors.fill: parent
propagateComposedEvents: false
onClicked: {
valueRelationClicked()
mouse.accepted = true
}
}

Image {
id: rightArrow
source: QgsQuick.Utils.getThemeIcon("ic_angle_right")
width: 15
anchors.right: parent.right
anchors.rightMargin: 10
height: 30
smooth: true
visible: false
}
ColorOverlay {
anchors.fill: rightArrow
source: rightArrow
color: customStyle.toolbutton.backgroundColorInvalid
}

background: Rectangle {
anchors.fill: parent
border.color: textField.activeFocus ? customStyle.fields.activeColor : customStyle.fields.normalColor
border.width: textField.activeFocus ? 2 : 1
color: customStyle.fields.backgroundColor
radius: customStyle.fields.cornerRadius
}
}
}

QgsQuick.EditorWidgetComboBox {
id: combobox
property var currentEditorValue: value

comboStyle: customStyle.fields
textRole: 'FeatureTitle'
height: parent.height

model: QgsQuick.FeaturesListModel {
id: vrModel

// recalculate index when model changes
onModelReset: {
combobox.currentIndex = vrModel.rowFromAttribute( QgsQuick.FeaturesListModel.KeyColumn, value )
}
}
model: vrModel

Component.onCompleted: {
vrModel.setupValueRelation( config )
currentIndex = vrModel.rowFromAttribute( QgsQuick.FeaturesListModel.KeyColumn, value )
}

onPressedChanged: {
if( pressed )
if( pressed && state !== "combobox" )
{
pressed = false // we close combobox and let custom handler react, it can open combobox via openCombobox()
customWidget.valueRelationOpened( fieldItem, vrModel )
valueRelationClicked()
}
}

// Called when user makes selection in the combo box
/**
* Called when user makes selection in the combo box.
* No need to set currentIndex manually since it is done in onWidgetValueChanged update function
*/
onItemClicked: {
currentIndex = vrModel.rowFromAttribute( QgsQuick.FeaturesListModel.FeatureId, index )
valueChanged( vrModel.keyFromAttribute( QgsQuick.FeaturesListModel.FeatureId, index ), false )
}

// Called when the same form is used for a different feature
onCurrentEditorValueChanged: {
currentIndex = vrModel.rowFromAttribute( QgsQuick.FeaturesListModel.KeyColumn, value )
vrModel.currentFeature = featurePair.feature
valueChanged( vrModel.attributeFromValue( QgsQuick.FeaturesListModel.FeatureId, index, QgsQuick.FeaturesListModel.KeyColumn ), false )
}
}
}
8 changes: 5 additions & 3 deletions src/quickgui/plugin/qgsquickfeatureform.qml
Expand Up @@ -92,9 +92,9 @@ Item {
* \param widget valuerelation widget for specific field to send valueChanged signal.
* \param valueRelationModel model of type FeaturesListModel bears features of related layer.
*/
property var valueRelationOpened: function valueRelationOpened( widget, valueRelationModel ) {
widget.openCombobox() // by default just open combobox
}
property var valueRelationOpened: function valueRelationOpened( widget, valueRelationModel ) {}

property int valueRelationLimit: 4
}

/**
Expand Down Expand Up @@ -410,6 +410,8 @@ Item {
height: childrenRect.height
anchors { left: parent.left; right: parent.right }

signal dataHasChanged() // to propagate signal to valuerelation model from model

property var value: AttributeValue
property var config: EditorWidgetConfig
property var widget: EditorWidget
Expand Down
22 changes: 18 additions & 4 deletions src/quickgui/qgsquickfeatureslistmodel.cpp
Expand Up @@ -197,8 +197,6 @@ void QgsQuickFeaturesListModel::setupValueRelation( const QVariantMap &config )
// store value relation filter expression
setFilterExpression( config.value( QStringLiteral("FilterExpression") ).toString() );

// config.value( QStringLiteral("AllowMulti") );

loadFeaturesFromLayer( layer );
}

Expand Down Expand Up @@ -227,6 +225,7 @@ void QgsQuickFeaturesListModel::emptyData()
mFeatureTitleField.clear();
mFilterExpression.clear();
mSearchExpression.clear();
mCurrentFeature = QgsFeature();
}

QHash<int, QByteArray> QgsQuickFeaturesListModel::roleNames() const
Expand Down Expand Up @@ -304,20 +303,35 @@ int QgsQuickFeaturesListModel::rowFromAttribute( const int role, const QVariant
return -1;
}

QVariant QgsQuickFeaturesListModel::keyFromAttribute( const int role, const QVariant &value ) const
QVariant QgsQuickFeaturesListModel::attributeFromValue( const int role, const QVariant &value, const int requestedRole ) const
{
for ( int i = 0; i < mFeatures.count(); ++i )
{
QVariant d = data( index( i, 0 ), role );
if ( d == value )
{
QVariant key = data( index( i, 0 ), KeyColumn );
QVariant key = data( index( i, 0 ), requestedRole );
return key;
}
}
return QVariant();
}

QVariant QgsQuickFeaturesListModel::convertMultivalueFormat( const QVariant &multivalue, const int role )
{
QStringList list = QgsValueRelationFieldFormatter::valueToStringList( multivalue );
QList<QVariant> retList;

for ( const QVariant &i : list )
{
QVariant var = attributeFromValue( KeyColumn, i, role );
if ( !var.isNull() )
retList.append(var);
}

return retList;
}

QgsQuickFeatureLayerPair QgsQuickFeaturesListModel::featureLayerPair( const int &featureId )
{
for ( const QgsQuickFeatureLayerPair &i : mFeatures )
Expand Down
20 changes: 15 additions & 5 deletions src/quickgui/qgsquickfeatureslistmodel.h
Expand Up @@ -58,7 +58,7 @@ class QUICK_EXPORT QgsQuickFeaturesListModel : public QAbstractListModel
* Feature that has opened feature form.
* This property needs to be set before opening feature form to be able to evaulate filter expressions that contain form scope.
*/
Q_PROPERTY( QgsFeature currentFeature WRITE setCurrentFeature NOTIFY currentFeatureChanged)
Q_PROPERTY( QgsFeature currentFeature WRITE setCurrentFeature NOTIFY currentFeatureChanged )

public:

Expand Down Expand Up @@ -112,13 +112,23 @@ class QUICK_EXPORT QgsQuickFeaturesListModel : public QAbstractListModel
Q_INVOKABLE int rowFromAttribute( const int role, const QVariant &value ) const;

/**
* \brief keyFromAttribute finds feature with requested role and value, returns keycolumn
* \brief attributeFromValue finds feature with role and value, returns value for requested role
* \param role role to find from modelRoles
* \param value value to find
* \return KeyColumn role for found feature, returns -1 if no feature is found. If more features
* match requested role and value, KeyColumn for first is returned.
* \param requestedRole role thats value is being requested
* \return If feature is found by role and value, method returns value for requested role. Returns empty QVariant if no feature is found. If more features
* match requested role and value, value for first is returned.
*/
Q_INVOKABLE QVariant keyFromAttribute( const int role, const QVariant &value ) const;
Q_INVOKABLE QVariant attributeFromValue( const int role, const QVariant &value, const int requestedRole ) const;

/**
* @brief convertMultivalueFormat converts postgres string like string to an array of variants with requested role.
* Array {1,2,3} with requested role FeatureId results in list of QVariant ints [1, 2, 3]
* @param multivalue string to convert
* @param requestedRole role to convert keys from string, default value is Qt::DisplayRole
* @return array of QVariants with values for requested role. If model can not find value for requested role, this key is omitted.
*/
Q_INVOKABLE QVariant convertMultivalueFormat( const QVariant &multivalue, const int requestedRole = Qt::DisplayRole );

//! Returns maximum amount of features that can be queried from layer
int featuresLimit() const;
Expand Down

0 comments on commit c4be1bd

Please sign in to comment.