Plugin access to clipboard

• May 4, 2020 - 06:22

Hi! How is the clipboard accessed from QML?


Comments

It is not. We don't provide a clipboard wrapper class.

What is the goal you're trying to achieve? Perhaps there is another way.

In reply to by jeetee

I'm trying to write a plugin for working with isorhythms. What I want to do is copy a bar or two, and then "paste" either the rhythm or the pitch values. So I figured the easiest way was accessing the clipboard contents, and then using that as a template for the other bars.

The other way that I would do it is insert the rhythm or melody preceding the bars, and then apply it. That's what I did with my vowel-to-pitch plugin, but it becomes a bit tedious for isorhythm. I suppose that another way would be to use text marks in the score, and then scan the score and apply melody and rhythm based on text marks.

In reply to by jeetee

Let me try to make my question a little more precise: How does the plugin get its shortcut information? Your solution seems sound, but I need to know how to ascertain what key invoked the script.

Yes, I do understand that the plugin needs to do the "copy" and "paste" itself. (Please give me some credit, I've been writing code for 40 years, and I've debugged memory overwrites in a multithreaded network processes.) And yes, I've been reading the code for the various plugins and the plugin documentation.

The MuseScore plugin interface starts with onRun: { }, and then the documentation goes into the score elements, etc. There doesn't appear to be an obvious way to get information about how the script was invoked.

In reply to by bmiller793

It doesn't get shortcut/caller information.

So if a shortcut is adamant for this, I'd split the plugin into three separate plugins where the copy plugin would write its information into some kind of temporary "clipboard" file which is then read back in by the paste plugins.
You can then use the Plugin Manager to assign a specific shortcut command to each of the plugins.

Another approach would be to make a dockable plugin with three buttons, avoiding the need for a temporary file. But you'd then loose the possibility of assigning a shortcut to each function.

In reply to by BSG

that is because plugins should not call Qt.quit(), but simply return. Docked plugins still disappear when they are closed. If people have been using Qt.quit() all along, it's a bad thing and it should be stated in the docs to avoid it, but there's nothing that could be done except rewriting a good part of the plugin engine.

How about you let things in memory, and simply handle how docks show up again when onRun is emitted? That way you never have to use Qt.quit(). You simply need to write a little function to hide your stuff and one to show it again when needed.

In reply to by Marr11317

That's great for a plugin with no UI, but any dialog-type plugin will not close its dialog unless Qt.Quit() is called. I tried many devious techniques to try to make a dialog-type plugin hide its face from me, but succeeded not. When you've solved that, call the New York Times. Or at least post here.

There needs to be a QML API that means "shut down this plugin, esp its UI, not the QML world."

In reply to by BSG

Challenge accepted. Here it is:

1) make a first dock-type plugin: here's the code. It never calls Qt.Quit()
run it, see that it works.

2) save it as test.qml under the plugins directory
3) plugin manager, activate your plugin
4) plugin creator, new plugin, exactly the same, but a green rectangle this time.
5) launch both plugins at the same time, they should never conflict one another.

import QtQuick 2.0
import MuseScore 3.0

MuseScore {
      menuPath: "Plugins.pluginName"
      description: "Description goes here"
      version: "1.0"
      pluginType: "dock"
      onRun: {
            console.log("hello world")
            rect.visible = true
            }

      Rectangle {
            id: rect
            visible: false
            height: 200
            width: 200
            color: "red"
            }
      }

Of course in this simple case, you can delete all the call to the visible property of the rectangle without changing the behaviour, but I put it there to show that more complex situations can be handled with the visible property.

Of course again, it is more tedious than using Qt.quit(), and there might be some situations where it is not possible (if you find one, call the New York Times. Or at least post here.)

Now, you say the problem is with dialog-type plugins, how are they different from dock-type plugins?

In reply to by Marr11317

Here's the challenge. Make a dialog-type, not dock-type, plugin with a button "quit" that causes it to dismiss without calling Qt.quit(). I have been in the past unable to coerce dialog-type plugins to disappear by manipulating the "visible" property. The problem is that dialog plugins kill dock plugins.

In reply to by BSG

That is because dialog-type plugins are virtually useless. Just use the QtQuick.dialogs thing instead.

import QtQuick 2.0
import QtQuick.Dialogs 1.2
import MuseScore 3.0

MuseScore {
      menuPath: "Plugins.CallIt"
      description: "Call the New York Times. Or at least post here"
      version: "1.0"
      requiresScore: false

      onRun: dial.visible = true

      Dialog {
            id: dial
            visible: false

            title: "Blue sky dialog"
            contentItem: Rectangle {
                  color: "skyblue"
                  implicitWidth: 100
                  implicitHeight: 100
                  Text {
                        anchors.centerIn: parent
                        text: "click to close"
                        }
                  MouseArea {
                        anchors.fill: parent
                        onClicked: {
                              dial.visible = false
                              }
                        }
                  }
            }
      }

Anyway, I get what you mean, you cannot programmatically close the internal Docks or Dialog. The solution for dialog type plugins is use QtQuick.dialogs, but there is no workaround (at least none I am aware of) for dock plugins. However, I can suggest to keep in mind that docks should not have to be closed programmatically, they are docked to be non annoying to the user.

In reply to by BSG

Wait, what do you mean out of the window? I first read it like a metaphor (saying that dialog-type plugins were useless) and then read it litterally (the dialog shows up outside of the window, thus useless).

In reply to by Marr11317

Oh, I'm sorry. "Window" is a term of art here, my fault! :) What I meant was, "Can any or all of the dialog layout tools, such as GridLayout, and named buttons with dynamic text, be used in these QuickDialogs, and if not, how do I layout a nontrivial and dynamic little UI such as the triller plugin's dialog?"

In reply to by Marr11317

Actually, it doesn't work at all. It fails to create a trill. I suppose we have not accounted for the Undo Stack mechanism. I suppose that the system undoes everything it does because it's not a dialog-type plugin. I have to call the New York Times now.

In reply to by Marr11317

The whole interaction with the Undo system is changed; maybe it needs one less or one more StartCmd() or whatever it is, but it thinks it has the Undo Stack under its own control, and now obviously it does not. The RedNote plugin knows how to make a change that persists, though, without being a dialog plugin. So some serious redesign is needed here. See if you can reproduce my failure.

In reply to by Marr11317

(This is the answer to the question in this comment)

Actually the answer is, it existed there historically at least in MuseScore 2.X times, therefore it was added to version 3 API as well. The practical difference with a self-crafted dialog is, with "dialog" type plugins the framework calls startCmd()/endCmd() for you while with your own dialog implementation you have to take care of this yourself (much like "dock" type plugins).

As for the Qt.quit() problem, I see at least three ways to resolve it:
1) Disable Qt.quit() at all, add a new API method which plugins would be required to call to terminate. This wouldn't work well with existing plugins though.
2) Disable Qt.quit(), add a new API method and add some heuristic to clean up plugins which probably have terminated their execution: for example, delete plugin objects after onRun() call for non-"dock" type plugins and require plugins to notify the framework if they require somewhat different lifecycle (like in case of plugins with their custom dialogs, like what is discussed here). This sounds like one of the best options as it would correctly dispose of unused plugins objects even if plugin author didn't specially handle that, but, again, this would perform not so well with existing plugins.
3) Use a separate QML engine instance for each plugin instance. This would naturally resolve the issue with overlapping Qt.quit() calls but will still make some plugin termination heuristics desirable: now plugin engines number will grow in time instead of just plugin instances. The advatage of this method is that we would probably need to do something like this anyway if we ever decide to implement something like permissions model for plugins.

In reply to by BSG

I've been looking into this today; prepare for a slightly lengthy reply :)

Why the dialog plugin type exists

Short answer

Historic raisins

Long answer

Dialog plugins were the original (and back then only) plugins. All actions performed by such a plugin were automatically wrapped into a single undo-stack option. A bit later the dockable type was added, and had to be specified; the default was still the dialog type plugin.
Still a bit later, the option was added to run plugins without a GUI. That was the time at which the dialog type became something you had to specify.
The main thing it still provides is the undo-stack wrapping (for all non-dockable plugins) and the inheritance of the application window color

What's the Qt.quit() issue?

Qt.quit() forces the underlying QmlEngine to emit its quit signal. In itself this isn't really an issue as it doesn't terminate the engine itself as far as I can tell.
But the intended use of that signal is to inform anyone that wants/needs to know about it that the engine is very likely being shut down.

To that end, it is imho correct that MuseScore connects that single quit signal from the engine to closing the dock and dialog plugins. They would otherwise become invalid and unhinged things in case the engine actually was quitting (such as at program shutdown).
On an additional note: I believe we should also bind the exit signal from the engine to closing all plugins as well.

side note

This means that if you create a dockable plugin that calls Qt.quit() (or likely even a non-gui one) all plugins will be closed as well. I've indeed succeeded in creating a dockable plugin with a button that kills off any other opened plugin as well.
The reason dockable plugins aren't affecting dialog plugins so visibly is two-fold:
(1) The chance that a dockable plugin calls quit while a dialog plugin is open is quite slim
(2) The chance that a dockable plugin calls quit at all is quite slim, as it is intended to stay open

Then what does happen when closing a dockable plugin?

The close button acts on the wrapping dockwidget object, not on the plugin itself; therefor Qt.quit isn't called.

Can we catch/disable Qt.quit from MuseScore?

Not really.
As the method isn't part of the QMLEngine itself (we already have our own subclass of that, so that could've been doable), catching it becomes quite hard. It lives in the core Qt QML Type itself. We could roll our own version of that, but it would require extra maintenance work. And even if we can subclass/create a wrapper class; it would very likely lead to a QML namespace conflict.
This would likely lead to plugins having to call a different function; and if that is the end result, then we could provide a different function with a lot less work on the MuseScore end.

What about rolling your own Dialog?

It's a decent workaround, but has some drawbacks:
(1) Additional wrapping code
(2) You must handle the undo-stack yourself as your now a non-GUI plugin
(3) When the engine quits, your plugin isn't closed as it should be. I'm unsure about the implications of this.

What's your proposal then, mr. smartypants?

Currently when MuseScore receives the quit signal, it calls close on the plugin (or its dock). So perhaps that is what our dialog plugin should do as well?

Dialog plugins

  1. Give the plugin itself an id
  2. Add import QtQuick.Window 2.2
  3. When the plugin needs to close, use pluginId.parent.Window.window.close();

The clue for this approach was discovered by reading this stackoverflow answer
Theoretically, you should be able to call the Window.window attached property on any item within the window scene. By calling it on the pluginId.parent which is a QQuickRootItem (the contentItem of the QQuickView that is the plugin) the traversed path for the attached property reach the correct object should be minimized.

Dockable plugins

In my opinion, these simply shouldn't attempt to close themselves at all.
Moreover, the used workaround from above doesn't work for these. You can reach the QQuickView from the plugin, but calling close on it has no effect. Instead it should be called on the wrapping QDockWidget which is unreachable from within the QML itself.

So what now?

I propose we create an issue to update the default plugin template in MuseScore as well as any default plugins it delivers. It can't hurt to mention this in the documentation somewhere as well.
TempoChanges for v3 has been updated (reference commit), my other plugins naturally will follow in the coming days/weeks

In reply to by jeetee

There is still the case of plugins of the "default" type: right now the only way for them to be terminated is calling Qt.quit(). A natural way to resolve this issue seems to be destroying the plugin object after the onRun() code ends its execution but it will break existing plugins which use their own implementation of dialog interface.

In reply to by dmitrio95

Indeed, those should technically be killed of if their onRun returns.
The main question then would be why those need to resort to a plugin rolled by itself.

Counter question though: they do call Qt.quit, but as the engine isn't really quit at that point, are they terminated currently?

In reply to by jeetee

Well, after checking, they don't seem to be terminated. For some reason I thought they are. Then it seems Qt.quit() doesn't really do anything useful for plugins right now indeed. Some Plugin Creator logic depends on Qt.quit() but it can be adjusted without any harm to compatibility with old plugins.

May it make sense to have an API method which would allow to actually terminate a plugin then (as seems to be also proposed in the Telegram chat)? It would suite all plugin types (being not very useful for "dock" plugins though) and would be easier to use than the parent.Window.window.close() approach.

In reply to by dmitrio95

> May it make sense to have an API method which would allow to actually terminate a plugin then…? It would suite all plugin types (being not very useful for "dock" plugins though) and would be easier to use than the parent.Window.window.close() approach.

That's a great idea. It will keep plugins cleaner-looking and also more future-proof in case the preferred method of terminating needs to change in the future.

In reply to by jeetee

Hmm, the "Batch Convert" plugin is using Qt.Quit() only in one case, a case that shouldn't happen anyway (trying to run it on MuseScore 2), else it is just 'falling off the edge' of onRun.
The "ColorVoices" and "Notenames" plugins are not a "dialog" type. so not affected by this problem, or are they?

MuseScore's "ABC import", "Scorelist", "Scoreview" and HelloQML plugins would be affected though, being "dialog" type plugins.
Also the "Panel" and "Random2" plugin, being "dock" type plugins.

In reply to by Jojo-Schmitz

Is the upshot of this that "one-shot, no UI" plugins like RedNote (blushing child of ColorNotes), now modified not to call Qt.quit(), accumulate non-garbage-collectable detritus every time they are called? What actually happens when they are invoked a second time (today, other than visibly functioning correctly)?

My dialog plugins currently used Marr11317's "Dialog wrapper/visible=false" method. I gather this leaves garbage, while dialogID.Window.window.close() does not? It'd be best if I can continue operability prior to better API's..

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