Skip to content

Commit

Permalink
Scripting: Added support for custom binary map formats
Browse files Browse the repository at this point in the history
The 'toString' member for scripted map formats has now been renamed to
'write' and may return either a string or an ArrayBuffer object.

When an error happens during export a backtrace is now reported in the
Console and Issues views.

Also, no longer fall back to "Export As" when triggering "Export" runs
into an error. Turns out opening the file dialog after the error dialog
is just annoying.

Issue #949
  • Loading branch information
bjorn committed Oct 1, 2019
1 parent d027f7c commit 8e16bbb
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 29 deletions.
8 changes: 4 additions & 4 deletions docs/reference/scripting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ tiled.registerMapFormat(shortName : string, mapFormat : object) : void

**name** : string, Name of the format as shown in the file dialog.
**extension** : string, The file extension used by the format.
"**toString** : function(map : :ref:`script-map`, fileName : string) : string", "A function
that returns the string representation of the given map, when
saved to the given file name (useful for relative references)."
"**write** : function(map : :ref:`script-map`, fileName : string) : string | ArrayBuffer", "A function
that serializes the map into either a string or binary data (using ArrayBuffer). The result will be
written to the given file (useful for making relative file references)."

Example that produces a simple JSON representation of a map:

Expand All @@ -281,7 +281,7 @@ tiled.registerMapFormat(shortName : string, mapFormat : object) : void
name: "Custom map format",
extension: "custom",
toString: function(map, fileName) {
write: function(map, fileName) {
var m = {
width: map.width,
height: map.height,
Expand Down
12 changes: 11 additions & 1 deletion src/tiled/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -999,11 +999,19 @@ bool MainWindow::confirmAllSave()
void MainWindow::export_()
{
if (!exportDocument(mDocument)) {
// fall back when no successful export happened
// fall back when previous export could not be repeated
exportAs();
}
}

/**
* Exports the given document to the previously used export file name and the
* previously used export format.
*
* @return `false` when no previous file name and export format could be de
* determined. Otherwise, `true` is returned, even if an error
* happened during export.
*/
bool MainWindow::exportDocument(Document *document)
{
const QString exportFileName = document->lastExportFileName();
Expand All @@ -1023,6 +1031,7 @@ bool MainWindow::exportDocument(Document *document)

QMessageBox::critical(this, tr("Error Exporting Map"),
exportFormat->errorString());
return true;
}
} else if (auto tilesetDocument = qobject_cast<TilesetDocument*>(document)) {
if (TilesetFormat *exportFormat = tilesetDocument->exportFormat()) {
Expand All @@ -1036,6 +1045,7 @@ bool MainWindow::exportDocument(Document *document)

QMessageBox::critical(this, tr("Error Exporting Tileset"),
exportFormat->errorString());
return true;
}
}

Expand Down
46 changes: 26 additions & 20 deletions src/tiled/scriptedmapformat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ FileFormat::Capabilities ScriptedMapFormat::capabilities() const
{
Capabilities capabilities;

if (mObject.property(QStringLiteral("fromString")).isCallable())
if (mObject.property(QStringLiteral("read")).isCallable())
capabilities |= Read;

if (mObject.property(QStringLiteral("toString")).isCallable())
if (mObject.property(QStringLiteral("write")).isCallable())
capabilities |= Write;

return capabilities;
Expand All @@ -75,7 +75,7 @@ bool ScriptedMapFormat::supportsFile(const QString &fileName) const
}

#if 0
// TODO: Currently makes no sense, because 'toString' can only return the contents of a single file anyway
// TODO: Currently makes no sense, because 'write' can only return the contents of a single file anyway
QStringList ScriptedMapFormat::outputFiles(const Map *map, const QString &fileName) const
{
QJSValue outputFiles = mObject.property(QStringLiteral("outputFiles"));
Expand Down Expand Up @@ -121,41 +121,47 @@ bool ScriptedMapFormat::write(const Map *map, const QString &fileName, Options o

EditableMap editable(map);

QJSValue toStringProperty = mObject.property(QStringLiteral("toString"));
QJSValue writeProperty = mObject.property(QStringLiteral("write"));

QJSValueList arguments;
arguments.append(ScriptManager::instance().engine()->newQObject(&editable));
arguments.append(fileName);
arguments.append(static_cast<Options::Int>(options));

QJSValue resultValue = toStringProperty.call(arguments);
QJSValue resultValue = writeProperty.call(arguments);

if (resultValue.isError()) {
if (ScriptManager::instance().checkError(resultValue)) {
mError = resultValue.toString();
return false;
}

if (!resultValue.isString()) {
mError = tr("Invalid return value for 'toString' (string expected)");
QByteArray bytes;
bool isString = resultValue.isString();

if (!isString && (bytes = qjsvalue_cast<QByteArray>(resultValue)).isNull()) {
mError = tr("Invalid return value for 'write' (string or ArrayBuffer expected)");
return false;
}

SaveFile file(fileName);

if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QIODevice::OpenMode mode { QIODevice::WriteOnly };
if (isString)
mode |= QIODevice::Text;

if (!file.open(mode)) {
mError = tr("Could not open file for writing.");
return false;
}

QTextStream out(file.device());
out << resultValue.toString();

if (file.error() != QFileDevice::NoError) {
mError = file.errorString();
return false;
if (isString) {
QTextStream out(file.device());
out << resultValue.toString();
} else {
file.device()->write(bytes);
}

if (!file.commit()) {
if (file.error() != QFileDevice::NoError || !file.commit()) {
mError = file.errorString();
return false;
}
Expand All @@ -167,8 +173,8 @@ bool ScriptedMapFormat::validateMapFormatObject(const QJSValue &value)
{
const QJSValue nameProperty = value.property(QStringLiteral("name"));
const QJSValue extensionProperty = value.property(QStringLiteral("extension"));
const QJSValue toStringProperty = value.property(QStringLiteral("toString"));
const QJSValue fromStringProperty = value.property(QStringLiteral("fromString"));
const QJSValue writeProperty = value.property(QStringLiteral("write"));
const QJSValue readProperty = value.property(QStringLiteral("read"));

if (!nameProperty.isString()) {
ScriptManager::instance().throwError(tr("Invalid map format object (requires string 'name' property)"));
Expand All @@ -180,8 +186,8 @@ bool ScriptedMapFormat::validateMapFormatObject(const QJSValue &value)
return false;
}

if (!toStringProperty.isCallable() && !fromStringProperty.isCallable()) {
ScriptManager::instance().throwError(tr("Invalid map format object (requires a 'toString' and/or 'fromString' function property)"));
if (!writeProperty.isCallable() && !readProperty.isCallable()) {
ScriptManager::instance().throwError(tr("Invalid map format object (requires a 'write' and/or 'read' function property)"));
return false;
}

Expand Down
7 changes: 4 additions & 3 deletions src/tiled/scriptmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@ void ScriptManager::loadExtension(const QString &path)
}
}

void ScriptManager::checkError(QJSValue value, const QString &program)
bool ScriptManager::checkError(QJSValue value, const QString &program)
{
if (!value.isError())
return;
return false;

QString errorString = value.toString();
QString stack = value.property(QStringLiteral("stack")).toString();
Expand All @@ -230,7 +230,8 @@ void ScriptManager::checkError(QJSValue value, const QString &program)
.arg(errorString);
}

emit mModule->error(errorString);
mModule->error(errorString);
return true;
}

void ScriptManager::throwError(const QString &message)
Expand Down
2 changes: 1 addition & 1 deletion src/tiled/scriptmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class ScriptManager : public QObject

QJSValue evaluateFile(const QString &fileName);

void checkError(QJSValue value, const QString &program = QString());
bool checkError(QJSValue value, const QString &program = QString());
void throwError(const QString &message);

void reset();
Expand Down

0 comments on commit 8e16bbb

Please sign in to comment.