1 /*
2 * scriptmodule.cpp
3 * Copyright 2018, 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 "scriptmodule.h"
22
23 #include "actionmanager.h"
24 #include "commandmanager.h"
25 #include "editabletileset.h"
26 #include "issuesmodel.h"
27 #include "logginginterface.h"
28 #include "mainwindow.h"
29 #include "mapeditor.h"
30 #include "scriptedaction.h"
31 #include "scriptedfileformat.h"
32 #include "scriptedtool.h"
33 #include "scriptfileformatwrappers.h"
34 #include "scriptmanager.h"
35 #include "tilesetdocument.h"
36 #include "tileseteditor.h"
37
38 #include <QAction>
39 #include <QCoreApplication>
40 #include <QInputDialog>
41 #include <QMenu>
42 #include <QMessageBox>
43 #include <QQmlEngine>
44
45 namespace Tiled {
46
ScriptModule(QObject * parent)47 ScriptModule::ScriptModule(QObject *parent)
48 : QObject(parent)
49 {
50 // If the script module is only created for command-line use, there will
51 // not be a DocumentManager instance.
52 if (auto documentManager = DocumentManager::maybeInstance()) {
53 connect(documentManager, &DocumentManager::documentCreated, this, &ScriptModule::documentCreated);
54 connect(documentManager, &DocumentManager::documentOpened, this, &ScriptModule::documentOpened);
55 connect(documentManager, &DocumentManager::documentAboutToBeSaved, this, &ScriptModule::documentAboutToBeSaved);
56 connect(documentManager, &DocumentManager::documentSaved, this, &ScriptModule::documentSaved);
57 connect(documentManager, &DocumentManager::documentAboutToClose, this, &ScriptModule::documentAboutToClose);
58 connect(documentManager, &DocumentManager::currentDocumentChanged, this, &ScriptModule::currentDocumentChanged);
59 }
60 }
61
~ScriptModule()62 ScriptModule::~ScriptModule()
63 {
64 for (const auto &pair : mRegisteredActions)
65 ActionManager::unregisterAction(pair.second.get(), pair.first);
66
67 ActionManager::clearMenuExtensions();
68
69 IssuesModel::instance().removeIssuesWithContext(this);
70 }
71
version() const72 QString ScriptModule::version() const
73 {
74 return QCoreApplication::applicationVersion();
75 }
76
platform() const77 QString ScriptModule::platform() const
78 {
79 #if defined(Q_OS_WIN)
80 return QStringLiteral("windows");
81 #elif defined(Q_OS_MAC)
82 return QStringLiteral("macos");
83 #elif defined(Q_OS_LINUX)
84 return QStringLiteral("linux");
85 #else
86 return QStringLiteral("unix");
87 #endif
88 }
89
arch() const90 QString ScriptModule::arch() const
91 {
92 #if defined(Q_PROCESSOR_X86_64)
93 return QStringLiteral("x64");
94 #elif defined(Q_PROCESSOR_X86)
95 return QStringLiteral("x86");
96 #else
97 return QStringLiteral("unknown");
98 #endif
99 }
100
idsToNames(const QList<Id> & ids)101 static QStringList idsToNames(const QList<Id> &ids)
102 {
103 QStringList names;
104 for (const Id &id : ids)
105 names.append(QLatin1String(id.name()));
106
107 names.sort();
108
109 return names;
110 }
111
actions() const112 QStringList ScriptModule::actions() const
113 {
114 return idsToNames(ActionManager::actions());
115 }
116
menus() const117 QStringList ScriptModule::menus() const
118 {
119 return idsToNames(ActionManager::menus());
120 }
121
mapFormats() const122 QStringList ScriptModule::mapFormats() const
123 {
124 const auto formats = PluginManager::objects<MapFormat>();
125 QStringList ret;
126 ret.reserve(formats.length());
127 for (auto format : formats)
128 ret.append(format->shortName());
129
130 return ret;
131 }
132
tilesetFormats() const133 QStringList ScriptModule::tilesetFormats() const
134 {
135 const auto formats = PluginManager::objects<TilesetFormat>();
136 QStringList ret;
137 ret.reserve(formats.length());
138 for (auto format : formats)
139 ret.append(format->shortName());
140
141 return ret;
142 }
143
activeAsset() const144 EditableAsset *ScriptModule::activeAsset() const
145 {
146 if (auto documentManager = DocumentManager::maybeInstance())
147 if (Document *document = documentManager->currentDocument())
148 return document->editable();
149
150 return nullptr;
151 }
152
setActiveAsset(EditableAsset * asset) const153 bool ScriptModule::setActiveAsset(EditableAsset *asset) const
154 {
155 if (!asset) {
156 ScriptManager::instance().throwNullArgError(0);
157 return false;
158 }
159
160 auto documentManager = DocumentManager::maybeInstance();
161 if (!documentManager) {
162 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Editor not available"));
163 return false;
164 }
165
166 for (const DocumentPtr &document : documentManager->documents())
167 if (document->editable() == asset)
168 return documentManager->switchToDocument(document.data());
169
170 return false;
171 }
172
openAssets() const173 QList<QObject *> ScriptModule::openAssets() const
174 {
175 QList<QObject *> assets;
176 if (auto documentManager = DocumentManager::maybeInstance())
177 for (const DocumentPtr &document : documentManager->documents())
178 assets.append(document->editable());
179 return assets;
180 }
181
tilesetEditor() const182 TilesetEditor *ScriptModule::tilesetEditor() const
183 {
184 if (auto documentManager = DocumentManager::maybeInstance())
185 return static_cast<TilesetEditor*>(documentManager->editor(Document::TilesetDocumentType));
186 return nullptr;
187 }
188
mapEditor() const189 MapEditor *ScriptModule::mapEditor() const
190 {
191 if (auto documentManager = DocumentManager::maybeInstance())
192 return static_cast<MapEditor*>(documentManager->editor(Document::MapDocumentType));
193 return nullptr;
194 }
195
filePath(const QUrl & path) const196 FilePath ScriptModule::filePath(const QUrl &path) const
197 {
198 return { path };
199 }
200
objectRef(int id) const201 ObjectRef ScriptModule::objectRef(int id) const
202 {
203 return { id };
204 }
205
open(const QString & fileName) const206 EditableAsset *ScriptModule::open(const QString &fileName) const
207 {
208 auto documentManager = DocumentManager::maybeInstance();
209 if (!documentManager) {
210 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Editor not available"));
211 return nullptr;
212 }
213
214 documentManager->openFile(fileName);
215
216 // If opening succeeded, it is the current document
217 int index = documentManager->findDocument(fileName);
218 if (index != -1)
219 if (auto document = documentManager->currentDocument())
220 return document->editable();
221
222 return nullptr;
223 }
224
close(EditableAsset * asset) const225 bool ScriptModule::close(EditableAsset *asset) const
226 {
227 if (!asset) {
228 ScriptManager::instance().throwNullArgError(0);
229 return false;
230 }
231
232 auto documentManager = DocumentManager::maybeInstance();
233 int index = -1;
234
235 if (documentManager)
236 index = documentManager->findDocument(asset->document());
237
238 if (index == -1) {
239 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Not an open asset"));
240 return false;
241 }
242
243 documentManager->closeDocumentAt(index);
244 return true;
245 }
246
reload(EditableAsset * asset) const247 EditableAsset *ScriptModule::reload(EditableAsset *asset) const
248 {
249 if (!asset) {
250 ScriptManager::instance().throwNullArgError(0);
251 return nullptr;
252 }
253
254 auto documentManager = DocumentManager::maybeInstance();
255 int index = -1;
256
257 if (documentManager)
258 index = documentManager->findDocument(asset->document());
259
260 if (index == -1) {
261 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Not an open asset"));
262 return nullptr;
263 }
264
265 if (auto editableTileset = qobject_cast<EditableTileset*>(asset)) {
266 if (editableTileset->tilesetDocument()->isEmbedded()) {
267 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Can't reload an embedded tileset"));
268 return nullptr;
269 }
270 }
271
272 // The reload is going to invalidate the EditableAsset instance and
273 // possibly also its document. We'll try to find it by its file name.
274 const auto fileName = asset->fileName();
275
276 if (documentManager->reloadDocumentAt(index)) {
277 int newIndex = documentManager->findDocument(fileName);
278 if (newIndex != -1)
279 return documentManager->documents().at(newIndex)->editable();
280 }
281
282 return nullptr;
283 }
284
registerAction(const QByteArray & idName,QJSValue callback)285 ScriptedAction *ScriptModule::registerAction(const QByteArray &idName, QJSValue callback)
286 {
287 if (idName.isEmpty()) {
288 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid ID"));
289 return nullptr;
290 }
291
292 if (!callback.isCallable()) {
293 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid callback function"));
294 return nullptr;
295 }
296
297 Id id { idName };
298 auto &action = mRegisteredActions[id];
299
300 // Remove any previously registered action with the same name
301 if (action) {
302 ActionManager::unregisterAction(action.get(), id);
303 } else if (ActionManager::findAction(id)) {
304 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Reserved ID"));
305 return nullptr;
306 }
307
308 action = std::make_unique<ScriptedAction>(id, callback, this);
309 ActionManager::registerAction(action.get(), id);
310 return action.get();
311 }
312
registerMapFormat(const QString & shortName,QJSValue mapFormatObject)313 void ScriptModule::registerMapFormat(const QString &shortName, QJSValue mapFormatObject)
314 {
315 if (shortName.isEmpty()) {
316 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid shortName"));
317 return;
318 }
319
320 if (!ScriptedFileFormat::validateFileFormatObject(mapFormatObject))
321 return;
322
323 auto &format = mRegisteredMapFormats[shortName];
324 format = std::make_unique<ScriptedMapFormat>(shortName, mapFormatObject, this);
325 }
326
registerTilesetFormat(const QString & shortName,QJSValue tilesetFormatObject)327 void ScriptModule::registerTilesetFormat(const QString &shortName, QJSValue tilesetFormatObject)
328 {
329 if (shortName.isEmpty()) {
330 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid shortName"));
331 return;
332 }
333
334 if (!ScriptedFileFormat::validateFileFormatObject(tilesetFormatObject))
335 return;
336
337 auto &format = mRegisteredTilesetFormats[shortName];
338 format = std::make_unique<ScriptedTilesetFormat>(shortName, tilesetFormatObject, this);
339 }
340
registerTool(const QString & shortName,QJSValue toolObject)341 QJSValue ScriptModule::registerTool(const QString &shortName, QJSValue toolObject)
342 {
343 if (shortName.isEmpty()) {
344 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid shortName"));
345 return QJSValue();
346 }
347
348 if (!ScriptedTool::validateToolObject(toolObject))
349 return QJSValue();
350
351 Id id { shortName.toUtf8() };
352 auto &tool = mRegisteredTools[id];
353
354 tool = std::make_unique<ScriptedTool>(id, toolObject, this);
355 return toolObject;
356 }
357
mapFormat(const QString & shortName) const358 ScriptMapFormatWrapper *ScriptModule::mapFormat(const QString &shortName) const
359 {
360 const auto formats = PluginManager::objects<MapFormat>();
361 for (auto format : formats) {
362 if (format->shortName() == shortName)
363 return new ScriptMapFormatWrapper(format);
364 }
365
366 return nullptr;
367 }
368
mapFormatForFile(const QString & fileName) const369 ScriptMapFormatWrapper *ScriptModule::mapFormatForFile(const QString &fileName) const
370 {
371 const auto formats = PluginManager::objects<MapFormat>();
372 for (auto format : formats) {
373 if (format->supportsFile(fileName))
374 return new ScriptMapFormatWrapper(format);
375 }
376
377 return nullptr;
378 }
379
tilesetFormat(const QString & shortName) const380 ScriptTilesetFormatWrapper *ScriptModule::tilesetFormat(const QString &shortName) const
381 {
382 const auto formats = PluginManager::objects<TilesetFormat>();
383 for (auto format : formats) {
384 if (format->shortName() == shortName)
385 return new ScriptTilesetFormatWrapper(format);
386 }
387
388 return nullptr;
389 }
390
tilesetFormatForFile(const QString & fileName) const391 ScriptTilesetFormatWrapper *ScriptModule::tilesetFormatForFile(const QString &fileName) const
392 {
393 const auto formats = PluginManager::objects<TilesetFormat>();
394 for (auto format : formats) {
395 if (format->supportsFile(fileName))
396 return new ScriptTilesetFormatWrapper(format);
397 }
398
399 return nullptr;
400 }
401
402
toString(QJSValue value)403 static QString toString(QJSValue value)
404 {
405 if (value.isString())
406 return value.toString();
407 return QString();
408 }
409
toId(QJSValue value)410 static Id toId(QJSValue value)
411 {
412 return Id(toString(value).toUtf8());
413 }
414
extendMenu(const QByteArray & idName,QJSValue items)415 void ScriptModule::extendMenu(const QByteArray &idName, QJSValue items)
416 {
417 ActionManager::MenuExtension extension;
418 Id menuId(idName);
419
420 if (!ActionManager::hasMenu(menuId)) {
421 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Unknown menu"));
422 return;
423 }
424
425 auto addItem = [&] (QJSValue item) -> bool {
426 ActionManager::MenuItem menuItem;
427
428 const QJSValue action = item.property(QStringLiteral("action"));
429
430 menuItem.action = toId(action);
431 menuItem.beforeAction = toId(item.property(QStringLiteral("before")));
432 menuItem.isSeparator = item.property(QStringLiteral("separator")).toBool();
433
434 if (!menuItem.action.isNull()) {
435 if (menuItem.isSeparator) {
436 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Separators can't have actions"));
437 return false;
438 }
439
440 if (!ActionManager::findAction(menuItem.action)) {
441 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Unknown action: '%1'").arg(
442 QString::fromUtf8(menuItem.action.name())));
443 return false;
444 }
445 } else if (!menuItem.isSeparator) {
446 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Non-separator item without action"));
447 return false;
448 }
449
450 extension.items.append(menuItem);
451 return true;
452 };
453
454 // Support either a single menu item or an array of items
455 if (items.isArray()) {
456 const quint32 length = items.property(QStringLiteral("length")).toUInt();
457 for (quint32 i = 0; i < length; ++i)
458 if (!addItem(items.property(i)))
459 return;
460 } else if (!addItem(items)) {
461 return;
462 }
463
464 ActionManager::registerMenuExtension(menuId, extension);
465 }
466
trigger(const QByteArray & actionName) const467 void ScriptModule::trigger(const QByteArray &actionName) const
468 {
469 if (QAction *action = ActionManager::findAction(actionName))
470 action->trigger();
471 else
472 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Unknown action"));
473 }
474
executeCommand(const QString & name,bool inTerminal) const475 void ScriptModule::executeCommand(const QString &name, bool inTerminal) const
476 {
477 const auto commands = CommandManager::instance()->allCommands();
478
479 for (const Command &command : commands) {
480 if (command.name == name) {
481 command.execute(inTerminal);
482 return;
483 }
484 }
485
486 ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Unknown command"));
487 }
488
alert(const QString & text,const QString & title) const489 void ScriptModule::alert(const QString &text, const QString &title) const
490 {
491 QMessageBox::warning(MainWindow::instance(), title, text);
492 }
493
confirm(const QString & text,const QString & title) const494 bool ScriptModule::confirm(const QString &text, const QString &title) const
495 {
496 return QMessageBox::question(MainWindow::instance(), title, text) == QMessageBox::Yes;
497 }
498
prompt(const QString & label,const QString & text,const QString & title) const499 QString ScriptModule::prompt(const QString &label, const QString &text, const QString &title) const
500 {
501 return QInputDialog::getText(MainWindow::instance(), title, label, QLineEdit::Normal, text);
502 }
503
log(const QString & text) const504 void ScriptModule::log(const QString &text) const
505 {
506 Tiled::INFO(text);
507 }
508
warn(const QString & text,QJSValue activated)509 void ScriptModule::warn(const QString &text, QJSValue activated)
510 {
511 Issue issue { Issue::Warning, text };
512 setCallback(issue, activated);
513 LoggingInterface::instance().report(issue);
514 }
515
error(const QString & text,QJSValue activated)516 void ScriptModule::error(const QString &text, QJSValue activated)
517 {
518 Issue issue { Issue::Error, text };
519 setCallback(issue, activated);
520 LoggingInterface::instance().report(issue);
521 }
522
setCallback(Issue & issue,QJSValue activated)523 void ScriptModule::setCallback(Issue &issue, QJSValue activated)
524 {
525 if (activated.isCallable()) {
526 issue.setCallback([activated] () mutable { // 'mutable' needed because of non-const QJSValue::call
527 QJSValue result = activated.call();
528 ScriptManager::instance().checkError(result);
529 });
530 issue.setContext(this);
531 }
532 }
533
documentCreated(Document * document)534 void ScriptModule::documentCreated(Document *document)
535 {
536 emit assetCreated(document->editable());
537 }
538
documentOpened(Document * document)539 void ScriptModule::documentOpened(Document *document)
540 {
541 emit assetOpened(document->editable());
542 }
543
documentAboutToBeSaved(Document * document)544 void ScriptModule::documentAboutToBeSaved(Document *document)
545 {
546 emit assetAboutToBeSaved(document->editable());
547 }
548
documentSaved(Document * document)549 void ScriptModule::documentSaved(Document *document)
550 {
551 emit assetSaved(document->editable());
552 }
553
documentAboutToClose(Document * document)554 void ScriptModule::documentAboutToClose(Document *document)
555 {
556 emit assetAboutToBeClosed(document->editable());
557 }
558
currentDocumentChanged(Document * document)559 void ScriptModule::currentDocumentChanged(Document *document)
560 {
561 emit activeAssetChanged(document ? document->editable() : nullptr);
562 }
563
564 } // namespace Tiled
565
566 #include "moc_scriptmodule.cpp"
567