It would be handy if we could spawn a CLI program, for really simple plugins that just wrap some other existing tool. This would allow people to mostly write plugins in whatever language they like, or whatever is already made, as most languages have a way to make a CLI program (stdio/stdout, file reading/writing, etc.)
This is related to #2780 and I'd like to add it in a similar fashion to ScriptFileInfo.
Some examples of plugin systems that use CLI programs to do fancy stuff, that we might get inspiration from:
I imagine in it's simplest form, it would just be a thin wrapper for QProcess. So you could something like this (without any async stuff):
/* global tiled, TextFile, Process */
tiled.registerMapFormat('Subprocess Demo', {
name: 'Subprocess Demo format',
extension: 'txt',
write: function (map, fileName) {
const n = new Process()
n.start(`ls ${fileName}`)
n.waitForFinished()
// some plugins might just write the file directly, themselves
const out = n.readAllStandardOutput()
const file = new TextFile(fileName, TextFile.WriteOnly)
file.write(out)
file.commit()
}
})
In this form, if the user liked go, ruby, python, nodejs, C#, C++, etc, better than Qt-JS, they could use it, as long as their users can run their thing. They still have to write a little wrapper in Qt-JS, but I think that's ok (and sort of takes the place of XML meta-data file in inkscape plugins.) Plugin-makers that wish to maintain maximum compatibility for end-users (no installing nodejs, python, being a windows/posix user, etc) should opt for all Qt-JS plugins, but if the plugin needs to do stuff in some other language, it's possible.
What do you think?
I think one other thing that would be handy is a way to get the location of the current script file (like node's __dirname), so you could use the path:
/* global tiled, TextFile, Process */
tiled.registerMapFormat('Subprocess Demo', {
name: 'Subprocess Demo format',
extension: 'txt',
write: function (map, fileName) {
const n = new Process()
// is there a __dirname work-alike for Qt?
n.start(`${__dirname}/myCoolPlugin ${fileName}`)
// need a way to pump JSON map into stdin, here...
n.waitForFinished()
// the plugin will write fileName, on it's own
}
})
You can do cross-platform support by making a file myCoolPlugin.bat in plugin-dir for windows people, and a myCoolPlugin with a posix-shebang and +x permission. This is sort of how node's bin works. You could also add code to check tiled.platform and tiled.arch to work out how to run the right thing. This would be great for including a go/c++/etc pre-built binary in the plugin dir, for easier installs.
/* global tiled, TextFile, Process */
tiled.registerMapFormat('Subprocess Demo', {
name: 'Subprocess Demo format',
extension: 'txt',
write: function (map, fileName) {
const n = new Process()
n.start(`${__dirname}/${tiled.platform}/${tiled.arch}/myCoolPlugin ${fileName}`)
n.waitForFinished()
}
})
I think adding a thin QProcess wrapper would suffice for now. I would take the Process service in Qbs as an example in terms of API.
Regarding defining UI in XML or integrating protobuf, I think that goes a little too far right now. Though eventually, I would like to enable building UIs in extensions. I'm not sure if it's worth implementing this with QWidget-based API or whether we could rather make this possible by using QML / Qt Quick.
Regarding defining UI in XML or integrating protobuf, I think that goes a little too far right now.
No, I just meant that for example protoc uses a known protocol on stdin/out (like in our case that could be JSON) and inkscape implements their plugins primarily in scripts, with a little meta-data. I meant more to think about these as inspiration. I think with a Process object we'd be able to do all of the same stuff with a pretty small wrapper script.
I would like to enable building UIs in extensions. I'm not sure if it's worth implementing this with QWidget-based API or whether we could rather make this possible by using QML / Qt Quick.
Yeh, I don't know the details around this, but it seems like it should be pretty built-in to the scripting environment to spawn a config dialog or whatever. I think we should go as thin as possible, and rather than parsing some data-format (like XML) let the user make the widget, however that works easiest in the Qt javascript env.
Would this kind of thing already work in the plugin API?:
const dialog = Qt.createComponent("config.qml")
Again, maybe __dirname would be handy here.
Would this kind of thing already work in the plugin API?:
const dialog = Qt.createComponent("config.qml")
Hmm, it actually works, but you need to create a window and actually instantiate the component. I created a file called window.qml in my extensions folder with the following contents:
import QtQuick.Window 2.12
Window {
x: 100; y: 100; width: 100; height: 100
}
Then, in the Tiled console, I typed:
windowComponent = Qt.createComponent("ext:window.qml")
window = windowComponent.createObject()
window.visible = true
But, while this will work with a local build of Tiled, it does not work with the currently released builds because the relevant Qt Quick libraries are not shipped.
Again, maybe
__dirnamewould be handy here.
Maybe, but I'm not sure how this would work. It is easy to set it globally before evaluating a script file, but it will not work when it is used in a function when that function is called later.
It is easy to set it globally before evaluating a script file, but it will not work when it is used in a function when that function is called later.
I tried this (adding __filename to globals:
QJSValue ScriptManager::evaluate(const QString &program,
const QString &fileName, int lineNumber)
{
QJSValue globalObject = mEngine->globalObject();
globalObject.setProperty(QStringLiteral("__filename"), fileName);
QJSValue result = mEngine->evaluate(program, fileName, lineNumber);
checkError(result, program);
return result;
}
This might be the wrong way, but it outputted another plugin's file:
tiled.registerMapFormat('Subprocess Demo', {
name: 'Subprocess Demo format',
extension: 'txt',
write: function (map, fileName) {
console.log('__filename', __filename)
}
})
qml: __filename /Users/konsumer/Library/Preferences/Tiled/extensions/tiled-to-godot-export/utils.js
@konsumer Yes, this does not work for the reason I described. :-)
this works, in the script, which might be a decent solution:
const f = __filename
tiled.registerMapFormat('Subprocess Demo', {
name: 'Subprocess Demo format',
extension: 'txt',
write: function (map, fileName) {
console.log(f)
}
})
To avoid confusion, it could be called something other than __filename, like __plugin.
More on the other idea, I put the qml file in the plugin dir, and used it with the FileInfo branch, and it worked perfectly (on Mac Qt 5.14.1):
/* global tiled, Qt, FileInfo */
const f = __filename
tiled.registerMapFormat('Subprocess Demo', {
name: 'Subprocess Demo format',
extension: 'txt',
write: function (map, fileName) {
console.log(FileInfo.path(f))
const windowComponent = Qt.createComponent(`${FileInfo.path(f)}/window.qml`)
const window = windowComponent.createObject()
window.visible = true
}
})
It even worked with an image in the same dir, and more complex dialog:

I mean, I'm no Qt dialog designer, so it's ugly, but it works great.
@konsumer That's great! So, the main challenge with supporting this will be to adjust the various release packages to include the necessary Qt Quick plugins.
Regarding __filename, I don't think __plugin helps avoiding the confusion which will surely come from the value only being available when the script is evaluated. But, I've just added it and noted this property in the documentation: 0b08577546dc15a0c91eeb1ac115282b2fa6d93a
So, the main challenge with supporting this will be to adjust the various release packages to include the necessary Qt Quick plugins.
Yeh, for sure. It's cool it works locally, though. I am imagining some really sweet plugin config UIs that are pretty easy to put together.
I don't think __plugin helps avoiding the confusion which will surely come from the value only being available when the script is evaluated
I just meant because node has the__filename and __dirnameglobal names, but they work differently (it's always the current file, no scope trickery.) Maybe it's ok if they don't work exactly the same.
I just meant because node has the
__filenameand__dirnameglobal names, but they work differently (it's always the current file, no scope trickery.) Maybe it's ok if they don't work exactly the same.
Yeah. And, in my patch I delete the property after evaluation, which makes it throw an error when you try to use it when you can't. I think that'll be enough.
Yeah. And, in my patch I delete the property after evaluation, which makes it throw an error when you try to use it when you can't. I think that'll be enough.
Yep, just saw that. I think you're right.
I'm also wondering if any interop can be built using javascript extensions.
Want to expose some javascript extensions functionality to my program and maybe update my program on some tiled events (mouse move, asset save, etc.). So both programs are opened at the same time and somehow exchange information, execute commands.
Was thinking about:
What I tried and thought of so far:
@bjorn Is it possible to schedule code in javascript extensions in Tiled? Create thread? Work with sockets? Sorry, I'm new to javascript. Need help understanding which way to go.
@konsumer Tell me please, what's your progress? Looks, like you are trying to achieve some form of interop too. But I don't quite understand what you were able to achieve.
Context: I wrote a program (libgdx, java) which works with tmx (postprocessing, updating file and special printing). It launches Tiled to edit tmx. Currently one launches another through CLI (java - Process; tiled - using command which launch mine program through CLI)
Tiled v. 1.3.5
It's not really interop, just running a sub-process. Maybe it would work better if you made your program just watch for file changes? Then you could reload on change, and I think it would acheive what you are going for. It would be a totally separate program, but update on save.