Project Plugin for Feature Voting

Heya everyone!

I just wanted to show off a cute little project plugin we built for one of our recent projects.

Our goal was to allow users to show their interest in features recorded by other users, essentially allowing them to “vote” for a feature.
We achieved this by adding a button with a heart icon to the feature menu. Clicking the button creates an entry in a hidden layer containing the cloud user name and the feature ID. Clicking the button again removes the vote.

Here is what it looks like:

We can then use database queries or even expressions to analyze these votes.

Unfortunately, the way these menu elements are chained together makes it pretty difficult to make sure the custom button is always correctly injected on their left, not to mention the spacing and centering of the title text.
For future iterations, we will probably have to place the button on top of the feature form or inside the Plugins Toolbar, so it doesn’t potentially cover up existing UI elements.

Despite these challenges, I think our current version looks really neat, so I wanted to share it! :grinning_face_with_smiling_eyes:

5 Likes

This is neat. I’d be interested to see your code for injecting that button into the feature header!

It’s kind of an abomination, honestly :joy:

And I would generally recommend against it, especially in plugins you want to publish, since it can break VERY easily, either by changes made to the QField UI upstream or by other plugins attempting something similar.
It definitely would be fun if we could get more “official“ ways to place our plugin elements (like the Plugins Toolbar) in future updates.

The first thing we need to do is find a reference to the element we want to “inject” into. In my case, that was pretty easy, since the NavigationBar has an ID and is a child of the FeatureForm, which we can get by its name.

let navigationBar = null
let featureForm = iface.findItemByObjectName("featureForm")
for (let i = 0; i < featureForm.children.length; ++i) {
    const child = featureForm.children[i];
    if (child && child.toString && String(child).indexOf("NavigationBar") !== -1) {
        navigationBar = child;
        break;
    }
}

My button is a QfToolButton component in a separate file, which is “instantiated” like this (navigationBar will be our parent, plugin is the ID of my main plugin item, so that I can use variables I define in my main file in the button code as well):

const comp = Qt.createComponent("VoteButton.qml")
var voteButtonRef = comp.createObject(navigationBar, { "plugin": plugin })

The easy way to not cover up system functionality is to anchor our button on the right and use a Binding to artificially increase the right margin. A barebones button could look something like this:

import QtQuick
import QtCore

import org.qfield
import org.qgis
import Theme

QfToolButton {
    id: voteButton
    property var plugin: null

    width: 48
    height: 48
    round: true
    clip: true
    z: 100

    anchors.right: parent.right
    anchors.top: parent.top
    anchors.topMargin: parent.topMargin

    iconSource: "icon.svg"
    iconColor: Theme.mainOverlayColor
    visible: true

    onClicked:
    {}

    Binding {
        target: voteButton.parent
        property: "rightMargin"
        value: voteButton.visible ? (voteButton.width + 4) : 0
    }
}

image

About getting it on the left side…

I didn’t like the menu button not being the right-most icon, so with the help of Copilot, I did some weird calculations to place it on the left of the right-anchored elements (by iterating over all visible elements, checking which side they are on, and adding their width together).
But this breaks the wrapping and centering of the text if there happen to be more buttons on the right than on the left.

image

Haven’t found an elegant solution for this yet, but I didn’t need to for our use case.

There might also be prettier ways to achieve something like this. If you have to use something like this, make sure to test it thoroughly. :slightly_smiling_face:

Ping @Mathieu_Pellerin