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