Best practice for plugin UI

• Feb 24, 2023 - 14:51

I'm trying to put together a small set of simple plugins that will mostly need dialog boxes with one or two controls and OK/Cancel buttons. Looking at the existing plugins, they are kind of all over the map in terms of how they create their UI. Most don't really handle sizing well (the dialog is either too big or more often too small for its contents), and also, keyboard navigation within these seems to be largely non-existent (e.g., click in a spin box and press Up or Down and nothing happens. I'm trying to figure out if there is a good solution to these basic problems, so I can do a good job in my own plugins, and maybe we can collectively aim for a similar approach across the board.

Note: I am an absolute beginner when it comes to QML, all I know is what little I've gleaned from looking at other code and reading a couple of specific help pages.

I gather for plugins that want a dialog, one way is to use pluginType: dialog. It's not clear to me how OK & Cancel buttons are supposed to be handled if so - it looks like we have to build these ourselves, which makes this seem much less desirable than actually creating and showing a QML Dialog, which knows already how to build, position, label, and hook up these buttons. But probably there is some other advantage to using pluginType I am not aware of. Right now I have a plugin working equally well (functionality-wise) and equally badly (UI-wise) either way.

Any suggestions?


Comments

edit: removed : rereading your question makes me realize you may be asking for suggestion of a balanced ui building capability of future plugin engine instead of asking for advice to build a good ui. I'm gonna reply more to the point in a new comment.

In reply to by msfp

Commenting on ms3 as not experienced in ms4

> collectively aim for a similar approach across the board.

For plugin dev, determining good ui includes
1) item/component size
2) item/component pos
3) plugin window size

Difficulties

  • item size is commonly determined by font size, which might vary with different built
  • difficult for dev to determine a suitable bound size for items to reflow: should this be two rows in a narrower window, or one row in a wider window?
  • troublesome for end user to resize when the size is not what he desires, because dev feel the need to suggest a window size and item reflow style.
  • both QMLDialog and Musescore{plugintype=dialog} often hide behind the main app window (is there any workaround?)

Making these accessible 'd be useful or create a guideline in dev handbook if exist already

  • musescore's styling reference and current end user appearance settings eg icon size; current body text size in preference
  • measurements on end user's screen eg current musescore window pos and size (or offer a suitable maximum window size like ms4's preference window size data)
  • OS DPI, and offer guideline because qml use pixel unit (do you know of any DPI independent unit?)

the plugin window size may not be a problem if devs can create dockable plugin

Alternative 1
dockable
pros:

  • plugin is a panel: end user need to do repeatative and undoable actions that require parameter tweaking.
  • plugin is offers infomation responsive to user input such as showing data of selected notes after mouse click.
  • different UI items on a complex panel automatically "reflow" in bound of the dock (no hidden overflow items). Lesser work for devs to figure out because end user can determine the size of a good score working area and dock area himself
  • docks do not overlap on top of useful UI areas like popups do. Upon opneing a dockable will attempt to adapt to the shape of the already opened dockarea, avoiding 1) overlapping on score area 2) overlapping on other dockarea.
  • dockable can be dragged out to switch to dialog-like display.

cons:

  • hard/no way be set up individual keyboard shortcut to different button/part of one plugin

Dockable plugin has the advantage of dialog plugin, the opposite is not true. This makes dockable more useful for ambitious dev but confusing for those aiming for to provide simple function.
Dockable should be picked as the main parent component of a generalize plugin ui framework, dialog as a optional descendent popup component. Considering dialog's nature, complex sizing of it is usually not needed.

Then offer devs some one-click templates/functions that one can setup simple UI and write a simple plugin quickly.

Alternative2

REAPER plugins separate concerns: plugin dev create "actions", users are required to manually create new custom toolbar buttons that have unified style in the app and then attached a action to it .

> keyboard

For your plugin, ms3 plugin and QML tutorial especially the boilerplates gathered clever codes shared selflessly on this forum.

There's no centralized simple keyboard navigation profile to set on plugin scope, which is disappointing, as I assumed a plugin scripting language for hobbyist coder should have. I then searched for the designated qml object to attach keyboard handlers but also found none, so I'm using a basic component with focus:true (it is a property, not the current focus state) to grab the focus. There's a capture property too, to stop propagation but I'm yet find a good use.

Upon curScore.endcmd() , the plugin window's focus and hence the key capture may loss

There's one musescore scope handler but requires caution to use nicely, as matt28 did nicely in his workaround, find that in the link.

> actually creating and showing a QML Dialog,

I didn't realize I can wrap the whole app, I'll try that out. Thanks for the idea.

In reply to by msfp

Thanks for the comments I'm afraid most of it is over my head since I'm a QML newbie. But, the overall impression I am getting is, maybe there actually isn't a good solution already known, and that's why the current state of plugins is so bad. I mean, no way should individual plugin developers have to be worrying about font sizes or element sizes or dialog sizes. In any UI-building system I've ever used, you say what controls you want, and the system chooses appropriate defaults. The buttons are automatically made big enough for the text within them, and the dialog is automatically made big enough for the buttons within them, etc. This just doesn't seem to be the case with QML - or at least, with QML as current plugins seem to be using it. I have to believe QML itself is capable of it and we're all just missing something...

Regarding keyboard focus, I have seen documentation on the focus property, also forceActiveFocus. I have just so far failed in any effort to get it to actually work. If someone knows of a plugin with a working spin box - where you can click in the field and use cursor keys - let me know so I can study the code!

In reply to by Marc Sabatella

> no way should individual plugin developers have to be worrying about font sizes or element sizes or dialog sizes.

Totally agree. Who would expect they need to build app-level UI pixel by pixel when they only want to run some "script" or "macro"? And the language limited to the unpopular QML?
This problem could easily be fixed by harnessing the ready-to-use and most popular css / html by making Webengine + WebChannel module available. I thought I cleared out that hurdle for ms3, only be told later that Webengine only works on win built of ms3.

In reply to by msfp

To be honest, I never worry about font size. The only cases when I worry about font size is when I use exotic fonts such as Fiati or music fonts such as Bravura, MScore.

My base boilerplate for writing dialog plugins

It includes the following feature:

  • automatic window dimension based on UI content
  • use of the SystemPalette.Active and SystemPalette.Disabled for proper color management
  • Rely on DialogButtonBox for correct button placement
  • use as much as possible native MuseScore translations.
    The latter can be tricky because translations structure differs from MS3 and MS4.
    E.g. for a "title" label:
text: mscoreMajorVersion > 3 ? qsTranslate("project", "Title") + ":" : qsTranslate("NewWizard", "Title") + ":"

Remarks

  • Additionally, I'm using a bunch of pre-made components such as a TapTempoBox, TempoUnitBox, ...
  • I don't care for now for keyboard management (maybe I'll add this some day)

My plugin boilerplate

import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.3
import Qt.labs.settings 1.0
import MuseScore 3.0

MuseScore {
    menuPath: "Plugins." + qsTr("Plugin Name")
    version: "1.1.0"
    requiresScore: false
    description: qsTr("Create a new score based on exsiting template")
    pluginType: "dialog"
    id: mainWindow // required for setting MS4 properties and closing Window


    Component.onCompleted: {
        // Specific MS4 features
        if (mscoreMajorVersion >= 4) {
            mainWindow.title = qsTr("Plugin Name");
            mainWindow.thumbnailName = "logo.png";
            // mainWindow.categoryCode = "some category";
        }
    }

    onRun: {
        // check MuseScore version
        if (mscoreMajorVersion < 3) {
            mainWindow.visible = false
                versionError.open()
        }

    }

    // Compute dimension based on content
    width: mainRow.implicitWidth + extraLeft + extraRight
    height: mainRow.implicitHeight + extraTop + extraBottom
    
    property int extraMargin: mainRow.anchors.margins ? mainRow.anchors.margins : 0
    property int extraTop: mainRow.anchors.topMargin ? mainRow.anchors.topMargin : extraMargin
    property int extraBottom: mainRow.anchors.bottomMargin ? mainRow.anchors.bottomMargin : extraMargin
    property int extraLeft: mainRow.anchors.leftMargin ? mainRow.anchors.leftMargin : extraMargin
    property int extraRight: mainRow.anchors.rightMargin ? mainRow.anchors.rightMargin : extraMargin
       
    // Signal onClosing on the main Window. This code is executed when the window closed
    // Rem: this generates some warnings in the plugin editor log, but this is ok
    Connections {
            target: mainWindow.parent.Window.window
            onClosing: {
                // do whatever is required to do when the plugin window is closing such as managing the settings
                console.log("onClosing: Saving some date to stettings");
            }
        }
    
    // UI
    ColumnLayout {
        id: mainRow // needed for reference in size computing
        spacing: 2
        anchors.margins: 0

        // Plugin controls
        GridLayout {
            Layout.margins: 20
            Layout.minimumWidth: 250
            Layout.minimumHeight: 200
            columnSpacing: 5
            rowSpacing: 5

            // debug
            columns: 1
            Rectangle {
                color: "yellow"
                width: 400
                height: 300
            }

        } // GridLayout

        // Buttons
        DialogButtonBox {
            Layout.fillWidth: true
            spacing: 5
            alignment: Qt.AlignRight
            background.opacity: 0 // hide default white background

            standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel

            Button {
                // some non standard button placed on the left side
                id: special
                enabled: true // add real enabling test
                DialogButtonBox.buttonRole: DialogButtonBox.ResetRole
                text: qsTr("Special")
            }

            onAccepted: {
                //... do the stuff ...

                // and close
                mainWindow.parent.Window.window.close();
            }

            onClicked: {
                if (button === special) {
                    //... do some other stuff ...
                }
            }
            onRejected: mainWindow.parent.Window.window.close();

        } // DialogButtonBox

        // Status bar (delete if not needed)
        RowLayout {
            Layout.fillWidth: true
            Layout.preferredHeight: txtStatus.height
            Layout.margins: 5
            spacing: 5

            Text {
                id: txtStatus
                text: "some status"
                wrapMode: Text.NoWrap
                elide: Text.ElideRight
                maximumLineCount: 1
                Layout.fillWidth: true
            }
        } // status bar

    } // ColumnLayout

    // Plugin settings
    Settings {
        id: settings
        category: "MyPlugin"
        // use alias on whatever UI controls
        //property alias subtitle: subtitle.text
        //property alias composer: composer.text
        //property alias lyricist: lyricist.text
        //property alias copyright: copyright.text
    }

    // Palette for nice color management
    SystemPalette {
        id: sysActivePalette;
        colorGroup: SystemPalette.Active
    }
    SystemPalette {
        id: sysDisabledPalette;
        colorGroup: SystemPalette.Disabled
    }

    // Version mismatch dialog
    MessageDialog {
        id: versionError
        visible: false
        title: qsTr("Unsupported MuseScore Version")
        text: qsTr("This plugin requires MuseScore 3.6later.")
        onAccepted: {
            mainWindow.parent.Window.window.close();
        }
    }

} // MuseScore

In reply to by msfp

> Marc Sabatella: ... dialog is automatically made big enough for the buttons within them...

Absolutely right. That simplifies uibuilding and make life easier for almost all of the time, though I recall an old version of adobe reader had a silly bug that the preference window is bigger than my screen so that I cannot scroll to other options, and see and click ok or quit!
My previous post's dockable plugin suggestion is all about this.
What do you think is safe for the window size? 800x600 pixel? is there any guideline?

edit: after several tries, the automatic window dimension based on UI content method by parkingb GridLayout { } definitely works

In reply to by msfp

Nice; right now though I can get even a single spinbox to respond to keyboard input, much less to be able to navigate. So my problem I think is more about getting focus, and finding the magic combination of "focus" and "forceActivefocus" just to get started. But I'm slowly finding my way around thanks to the help here!

In reply to by msfp

> I cannot understand SystemPalette, how do you use it?
I'm using it to ensure the right and colour matching in light and dark mode (and also tweak the dark mode whom RadioButton and CheckBox have an invalid text colour unless you force it SystemPalette.Active.Text).

Some examples

BatchConvert

palette.jpg

Status bar

palette2.jpg
(PS: this example is under "palette-ization" and the I haven't applied the right colour to the text themselves)

In reply to by Marc Sabatella

Hi Marc, my UI boilerplate does not require any dimension !
The only one presents are those 4 ones and are only set for the sake of the demo. You can get rid of these in a working UI.

GridLayout {
            Layout.margins: 20
            Layout.minimumWidth: 250
            Layout.minimumHeight: 200
            columnSpacing: 5
            rowSpacing: 5

            // debug
            columns: 1
            Rectangle {
                color: "yellow"
                width: 400
                height: 300
            }

        }

In reply to by parkingb

Confirmed, thank you so much! I see now two things of interest that presumably are the key - setting fillWidth to true for the components within the dialog, and perhaps also the line where you explicitly set the width of the dialog itself to the implicitWidth of the components. But also some other interesting things like the id: mainWindow that seems intriguing...

In reply to by Marc Sabatella

id: mainWindow (or whatever id name you'd like to give) is used for

  • the window closing command
mainWindow.parent.Window.window.close()
  • and setting all the MuseScore 4 properties in a MuseScore 3+4 compatible properties
    Component.onCompleted: {
        if (mscoreMajorVersion &gt;= 4) {
            mainWindow.title = qsTr("Euclidian Rhythm");
            mainWindow.thumbnailName = "logoTemplater.png";
            // mainWindow.categoryCode = "batch-processing";
        }
    }

In reply to by parkingb

Thanks, I see this. I was previously thinking somehow it allowed to to ac cess the actual main window's properties :-)

The main thing I am still struggling with is getting any form of keyboard focus. Nothing I do seem to allow allow focus to go to the dialog. It always remains in the main application window, so Tab etc simply moves around the controls there. I've tried all sorts of variations on setting focus: true for different elements, calling forceActiveFocus(), calling requestActivate(), setting up a FocusScope - nothing seems to result in keyboard input ever being passed to the plugin.

In reply to by msfp

No, because the plugins wouldn't be needed in MU3 :-). But I just checked, and at least one plugin that doesn't accept keyboard input in MU4 does in MU3. I'm assuming it's still possible, just perhaps needs to be done differently due to how the whole UI uses QML now.

In reply to by parkingb

Thanks, this looks very helpful! I know this is asking a lot, but - would it be possible to have a more minimal example? A simple dialog with a single control (say, a spinbox), plus and OK and Cancel buttons? If it's possible to get that working without 100+ lines of code, that would be a great starting place.

Do you still have an unanswered question? Please log in first to post your question.