1 /*
2 * scriptedfileformat.cpp
3 * Copyright 2019, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
4 *
5 * This file is part of Tiled.
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the Free
9 * Software Foundation; either version 2 of the License, or (at your option)
10 * any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * more details.
16 *
17 * You should have received a copy of the GNU General Public License along with
18 * this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "scriptedfileformat.h"
22
23 #include "editablemap.h"
24 #include "editabletileset.h"
25 #include "savefile.h"
26 #include "scriptmanager.h"
27
28 #include <QCoreApplication>
29 #include <QFile>
30 #include <QJSEngine>
31 #include <QJSValueIterator>
32 #include <QTextStream>
33
34 namespace Tiled {
35
ScriptedFileFormat(const QJSValue & object)36 ScriptedFileFormat::ScriptedFileFormat(const QJSValue &object)
37 : mObject(object)
38 {
39 }
40
capabilities() const41 FileFormat::Capabilities ScriptedFileFormat::capabilities() const
42 {
43 FileFormat::Capabilities capabilities;
44
45 if (mObject.property(QStringLiteral("read")).isCallable())
46 capabilities |= FileFormat::Read;
47
48 if (mObject.property(QStringLiteral("write")).isCallable())
49 capabilities |= FileFormat::Write;
50
51 return capabilities;
52 }
53
nameFilter() const54 QString ScriptedFileFormat::nameFilter() const
55 {
56 QString name = mObject.property(QStringLiteral("name")).toString();
57 QString extension = mObject.property(QStringLiteral("extension")).toString();
58
59 return QStringLiteral("%1 (*.%2)").arg(name, extension);
60 }
61
supportsFile(const QString & fileName) const62 bool ScriptedFileFormat::supportsFile(const QString &fileName) const
63 {
64 QString extension = mObject.property(QStringLiteral("extension")).toString();
65 extension.prepend(QLatin1Char('.'));
66
67 return fileName.endsWith(extension);
68 }
69
read(const QString & fileName)70 QJSValue ScriptedFileFormat::read(const QString &fileName)
71 {
72 QJSValueList arguments;
73 arguments.append(fileName);
74
75 return mObject.property(QStringLiteral("read")).call(arguments);
76 }
77
write(EditableAsset * asset,const QString & fileName,FileFormat::Options options,QString & error)78 bool ScriptedFileFormat::write(EditableAsset *asset,
79 const QString &fileName,
80 FileFormat::Options options,
81 QString &error)
82 {
83 error.clear();
84
85 QJSValueList arguments;
86 arguments.append(ScriptManager::instance().engine()->newQObject(asset));
87 arguments.append(fileName);
88 arguments.append(static_cast<FileFormat::Options::Int>(options));
89
90 QJSValue resultValue = mObject.property(QStringLiteral("write")).call(arguments);
91 if (ScriptManager::instance().checkError(resultValue)) {
92 error = resultValue.toString();
93 return false;
94 }
95
96 if (resultValue.isString()) {
97 error = resultValue.toString();
98 return error.isEmpty();
99 }
100
101 if (!resultValue.isUndefined())
102 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid return value for 'write' (string or undefined expected)"));
103
104 return true;
105 }
106
outputFiles(EditableAsset * asset,const QString & fileName) const107 QStringList ScriptedFileFormat::outputFiles(EditableAsset *asset, const QString &fileName) const
108 {
109 QJSValue outputFiles = mObject.property(QStringLiteral("outputFiles"));
110 if (!outputFiles.isCallable())
111 return QStringList(fileName);
112
113 QJSValueList arguments;
114 arguments.append(ScriptManager::instance().engine()->newQObject(asset));
115 arguments.append(fileName);
116
117 QJSValue resultValue = outputFiles.call(arguments);
118
119 if (resultValue.isString())
120 return QStringList(resultValue.toString());
121
122 if (resultValue.isArray()) {
123 QStringList result;
124 QJSValueIterator iterator(resultValue);
125 while (iterator.next())
126 result.append(iterator.value().toString());
127 return result;
128 }
129
130 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid return value for 'outputFiles' (string or array expected)"));
131 return QStringList(fileName);
132 }
133
validateFileFormatObject(const QJSValue & value)134 bool ScriptedFileFormat::validateFileFormatObject(const QJSValue &value)
135 {
136 const QJSValue nameProperty = value.property(QStringLiteral("name"));
137 const QJSValue extensionProperty = value.property(QStringLiteral("extension"));
138 const QJSValue writeProperty = value.property(QStringLiteral("write"));
139 const QJSValue readProperty = value.property(QStringLiteral("read"));
140
141 if (!nameProperty.isString()) {
142 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid file format object (requires string 'name' property)"));
143 return false;
144 }
145
146 if (!extensionProperty.isString()) {
147 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid file format object (requires string 'extension' property)"));
148 return false;
149 }
150
151 if (!writeProperty.isCallable() && !readProperty.isCallable()) {
152 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid file format object (requires a 'write' and/or 'read' function property)"));
153 return false;
154 }
155
156 return true;
157 }
158
159
ScriptedMapFormat(const QString & shortName,const QJSValue & object,QObject * parent)160 ScriptedMapFormat::ScriptedMapFormat(const QString &shortName,
161 const QJSValue &object,
162 QObject *parent)
163 : MapFormat(parent)
164 , mShortName(shortName)
165 , mFormat(object)
166 {
167 PluginManager::addObject(this);
168 }
169
~ScriptedMapFormat()170 ScriptedMapFormat::~ScriptedMapFormat()
171 {
172 PluginManager::removeObject(this);
173 }
174
outputFiles(const Map * map,const QString & fileName) const175 QStringList ScriptedMapFormat::outputFiles(const Map *map, const QString &fileName) const
176 {
177 EditableMap editable(map);
178 return mFormat.outputFiles(&editable, fileName);
179 }
180
read(const QString & fileName)181 std::unique_ptr<Map> ScriptedMapFormat::read(const QString &fileName)
182 {
183 mError.clear();
184
185 QJSValue resultValue = mFormat.read(fileName);
186
187 if (ScriptManager::instance().checkError(resultValue)) {
188 mError = resultValue.toString();
189 return nullptr;
190 }
191
192 EditableMap *editableMap = qobject_cast<EditableMap*>(resultValue.toQObject());
193 if (editableMap) {
194 // We need to clone the map here, because the returned map will be
195 // wrapped in a MapDocument, which the EditableMap instance will be
196 // unaware of. Further changes to the map through this editable would
197 // otherwise mess up the undo system.
198 return std::unique_ptr<Map>(editableMap->map()->clone());
199 }
200
201 return nullptr;
202 }
203
write(const Map * map,const QString & fileName,Options options)204 bool ScriptedMapFormat::write(const Map *map, const QString &fileName, Options options)
205 {
206 EditableMap editable(map);
207 return mFormat.write(&editable, fileName, options, mError);
208 }
209
210
ScriptedTilesetFormat(const QString & shortName,const QJSValue & object,QObject * parent)211 ScriptedTilesetFormat::ScriptedTilesetFormat(const QString &shortName,
212 const QJSValue &object,
213 QObject *parent)
214 : TilesetFormat(parent)
215 , mShortName(shortName)
216 , mFormat(object)
217 {
218 PluginManager::addObject(this);
219 }
220
~ScriptedTilesetFormat()221 ScriptedTilesetFormat::~ScriptedTilesetFormat()
222 {
223 PluginManager::removeObject(this);
224 }
225
read(const QString & fileName)226 SharedTileset ScriptedTilesetFormat::read(const QString &fileName)
227 {
228 mError.clear();
229
230 QJSValue resultValue = mFormat.read(fileName);
231
232 if (ScriptManager::instance().checkError(resultValue)) {
233 mError = resultValue.toString();
234 return SharedTileset();
235 }
236
237 EditableTileset *editableTileset = qobject_cast<EditableTileset*>(resultValue.toQObject());
238 if (editableTileset) {
239 // We need to clone the tileset here, because the returned tileset will
240 // be wrapped in a TilesetDocument, which the EditableTileset instance
241 // will be unaware of. Further changes to the tileset through this
242 // editable would otherwise mess up the undo system.
243 return editableTileset->tileset()->clone();
244 }
245
246 return SharedTileset();
247 }
248
write(const Tileset & tileset,const QString & fileName,FileFormat::Options options)249 bool ScriptedTilesetFormat::write(const Tileset &tileset, const QString &fileName, FileFormat::Options options)
250 {
251 EditableTileset editable(&tileset);
252 return mFormat.write(&editable, fileName, options, mError);
253 }
254
255 } // namespace Tiled
256
257 #include "moc_scriptedfileformat.cpp"
258