1 /*
2 * command.cpp
3 * Copyright 2011, Jeff Bland <jksb@member.fsf.org>
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 "command.h"
22
23 #include "actionmanager.h"
24 #include "commandmanager.h"
25 #include "documentmanager.h"
26 #include "logginginterface.h"
27 #include "mapdocument.h"
28 #include "mapobject.h"
29 #include "projectmanager.h"
30 #include "worlddocument.h"
31 #include "worldmanager.h"
32
33 #include <QAction>
34 #include <QDir>
35 #include <QMessageBox>
36 #include <QProcess>
37 #include <QStandardPaths>
38 #include <QUndoStack>
39
40 using namespace Tiled;
41
42 namespace Tiled {
43
44 class CommandProcess : public QProcess
45 {
46 Q_OBJECT
47
48 public:
49 CommandProcess(const Command &command, bool inTerminal = false, bool showOutput = true);
50
51 private:
52 void consoleOutput();
53 void consoleError();
54 void handleProcessError(QProcess::ProcessError);
55
56 void reportErrorAndDelete(const QString &);
57
58 QString mName;
59 QString mFinalCommand;
60
61 #ifdef Q_OS_MAC
62 QTemporaryFile mFile;
63 #endif
64 };
65
66
replaceVariables(const QString & string,bool quoteValues=true)67 static QString replaceVariables(const QString &string, bool quoteValues = true)
68 {
69 QString finalString = string;
70 QString replaceString = quoteValues ? QStringLiteral("\"%1\"") :
71 QStringLiteral("%1");
72
73 // Perform variable replacement
74 if (Document *document = DocumentManager::instance()->currentDocument()) {
75 const QString fileName = document->fileName();
76 QFileInfo fileInfo(fileName);
77 const QString mapPath = fileInfo.absolutePath();
78 const QString projectPath = QFileInfo(ProjectManager::instance()->project().fileName()).absolutePath();
79
80 finalString.replace(QLatin1String("%mapfile"), replaceString.arg(fileName));
81 finalString.replace(QLatin1String("%mappath"), replaceString.arg(mapPath));
82 finalString.replace(QLatin1String("%projectpath"), replaceString.arg(projectPath));
83
84 if (MapDocument *mapDocument = qobject_cast<MapDocument*>(document)) {
85 if (const Layer *layer = mapDocument->currentLayer()) {
86 finalString.replace(QLatin1String("%layername"),
87 replaceString.arg(layer->name()));
88 }
89 } else if (TilesetDocument *tilesetDocument = qobject_cast<TilesetDocument*>(document)) {
90 QStringList selectedTileIds;
91 for (Tile *tile : tilesetDocument->selectedTiles())
92 selectedTileIds.append(QString::number(tile->id()));
93
94 finalString.replace(QLatin1String("%tileid"),
95 replaceString.arg(selectedTileIds.join(QLatin1Char(','))));
96 }
97
98 if (MapObject *currentObject = dynamic_cast<MapObject *>(document->currentObject())) {
99 finalString.replace(QLatin1String("%objecttype"),
100 replaceString.arg(currentObject->type()));
101 finalString.replace(QLatin1String("%objectid"),
102 replaceString.arg(currentObject->id()));
103 }
104 }
105
106 return finalString;
107 }
108
109 } // namespace Tiled
110
111
finalWorkingDirectory() const112 QString Command::finalWorkingDirectory() const
113 {
114 QString finalWorkingDirectory = replaceVariables(workingDirectory, false);
115 QString finalExecutable = replaceVariables(executable);
116 QFileInfo mFile(finalExecutable);
117
118 if (!mFile.exists())
119 mFile = QFileInfo(QStandardPaths::findExecutable(finalExecutable));
120
121 finalWorkingDirectory.replace(QLatin1String("%executablepath"),
122 mFile.absolutePath());
123
124 return finalWorkingDirectory;
125 }
126
127 /**
128 * Returns the final command with replaced tokens.
129 */
finalCommand() const130 QString Command::finalCommand() const
131 {
132 QString exe = executable.trimmed();
133
134 // Quote the executable when not already done, to make it work even when
135 // the path contains spaces.
136 if (!exe.startsWith(QLatin1Char('"')) && !exe.startsWith(QLatin1Char('\'')))
137 exe.prepend(QLatin1Char('"')).append(QLatin1Char('"'));
138
139 QString finalCommand = QStringLiteral("%1 %2").arg(exe, arguments);
140 return replaceVariables(finalCommand);
141 }
142
143 /**
144 * Executes the command in the operating system shell or terminal
145 * application.
146 */
execute(bool inTerminal) const147 void Command::execute(bool inTerminal) const
148 {
149 if (saveBeforeExecute) {
150 ActionManager::instance()->action("Save")->trigger();
151
152 if (Document *document = DocumentManager::instance()->currentDocument()) {
153 const World *world = WorldManager::instance().worldForMap(document->fileName());
154 if (world && WorldManager::instance().saveWorld(world->fileName))
155 DocumentManager::instance()->ensureWorldDocument(world->fileName)->undoStack()->setClean();
156 }
157 }
158
159 // Start the process
160 new CommandProcess(*this, inTerminal, showOutput);
161 }
162
163 /**
164 * Stores this command in a QVariant.
165 */
toVariant() const166 QVariantHash Command::toVariant() const
167 {
168 return QVariantHash {
169 { QStringLiteral("arguments"), arguments },
170 { QStringLiteral("command"), executable },
171 { QStringLiteral("enabled"), isEnabled },
172 { QStringLiteral("name"), name },
173 { QStringLiteral("saveBeforeExecute"), saveBeforeExecute },
174 { QStringLiteral("shortcut"), shortcut },
175 { QStringLiteral("showOutput"), showOutput },
176 { QStringLiteral("workingDirectory"), workingDirectory },
177 };
178 }
179
180 /**
181 * Generates a command from a QVariant.
182 */
fromVariant(const QVariant & variant)183 Command Command::fromVariant(const QVariant &variant)
184 {
185 const auto hash = variant.toHash();
186
187 auto read = [&] (const QString &prop) {
188 if (hash.contains(prop))
189 return hash.value(prop);
190
191 QString oldProp = prop.at(0).toUpper() + prop.mid(1);
192 return hash.value(oldProp);
193 };
194
195 const QVariant arguments = read(QStringLiteral("arguments"));
196 const QVariant enable = read(QStringLiteral("enabled"));
197 const QVariant executable = read(QStringLiteral("command"));
198 const QVariant name = read(QStringLiteral("name"));
199 const QVariant saveBeforeExecute = read(QStringLiteral("saveBeforeExecute"));
200 const QVariant shortcut = read(QStringLiteral("shortcut"));
201 const QVariant showOutput = read(QStringLiteral("showOutput"));
202 const QVariant workingDirectory = read(QStringLiteral("workingDirectory"));
203
204 Command command;
205
206 command.arguments = arguments.toString();
207 command.isEnabled = enable.toBool();
208 command.executable = executable.toString();
209 command.name = name.toString();
210 command.saveBeforeExecute = saveBeforeExecute.toBool();
211 command.shortcut = shortcut.value<QKeySequence>();
212 command.showOutput = showOutput.toBool();
213 command.workingDirectory = workingDirectory.toString();
214
215 return command;
216 }
217
CommandProcess(const Command & command,bool inTerminal,bool showOutput)218 CommandProcess::CommandProcess(const Command &command, bool inTerminal, bool showOutput)
219 : QProcess(DocumentManager::instance())
220 , mName(command.name)
221 , mFinalCommand(command.finalCommand())
222 #ifdef Q_OS_MAC
223 , mFile(QDir::tempPath() + QLatin1String("/tiledXXXXXX.command"))
224 #endif
225 {
226 // Give an error if the command is empty or just whitespace
227 if (mFinalCommand.trimmed().isEmpty()) {
228 handleProcessError(QProcess::FailedToStart);
229 return;
230 }
231
232 // Modify the command to run in a terminal
233 if (inTerminal) {
234 #ifdef Q_OS_LINUX
235 static bool hasGnomeTerminal = QProcess::execute(QLatin1String("which"),
236 QStringList(QLatin1String("gnome-terminal"))) == 0;
237
238 if (hasGnomeTerminal)
239 mFinalCommand = QLatin1String("gnome-terminal -x ") + mFinalCommand;
240 else
241 mFinalCommand = QLatin1String("xterm -e ") + mFinalCommand;
242 #elif defined(Q_OS_MAC)
243 // The only way I know to launch a Terminal with a command on mac is
244 // to make a .command file and open it. The client command invoke the
245 // executable directly (rather than using open) in order to get std
246 // output in the terminal. Otherwise, you can use the Console
247 // application to see the output.
248
249 // Create and write the command to a .command file
250
251 if (!mFile.open()) {
252 reportErrorAndDelete(tr("Unable to create/open %1").arg(mFile.fileName()));
253 return;
254 }
255 mFile.write(mFinalCommand.toLocal8Bit());
256 mFile.close();
257
258 // Add execute permission to the file
259 int chmodRet = QProcess::execute(QStringLiteral("chmod"),
260 { QStringLiteral("+x"), mFile.fileName() });
261 if (chmodRet != 0) {
262 reportErrorAndDelete(tr("Unable to add executable permissions to %1")
263 .arg(mFile.fileName()));
264 return;
265 }
266
267 // Use open command to launch the command in the terminal
268 // -W makes it not return immediately
269 // -n makes it open a new instance of terminal if it is open already
270 mFinalCommand = QStringLiteral("open -W -n \"%1\"")
271 .arg(mFile.fileName());
272 #endif
273 }
274
275 connect(this, &QProcess::errorOccurred,
276 this, &CommandProcess::handleProcessError);
277
278 connect(this, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
279 this, &QObject::deleteLater);
280
281 if (showOutput) {
282 Tiled::INFO(tr("Executing: %1").arg(mFinalCommand));
283
284 connect(this, &QProcess::readyReadStandardError, this, &CommandProcess::consoleError);
285 connect(this, &QProcess::readyReadStandardOutput, this, &CommandProcess::consoleOutput);
286 }
287
288 const QString finalWorkingDirectory = command.finalWorkingDirectory();
289 if (!finalWorkingDirectory.trimmed().isEmpty())
290 setWorkingDirectory(finalWorkingDirectory);
291
292 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
293 start(mFinalCommand);
294 #else
295 QStringList args = QProcess::splitCommand(mFinalCommand);
296 const QString executable = args.takeFirst();
297 start(executable, args);
298 #endif
299 }
300
consoleOutput()301 void CommandProcess::consoleOutput()
302 {
303 Tiled::INFO(QString::fromLocal8Bit(readAllStandardOutput()));
304 }
305
consoleError()306 void CommandProcess::consoleError()
307 {
308 Tiled::ERROR(QString::fromLocal8Bit(readAllStandardError()));
309 }
310
handleProcessError(QProcess::ProcessError error)311 void CommandProcess::handleProcessError(QProcess::ProcessError error)
312 {
313 QString errorStr;
314 switch (error) {
315 case QProcess::FailedToStart:
316 errorStr = tr("The command failed to start.");
317 break;
318 case QProcess::Crashed:
319 errorStr = tr("The command crashed.");
320 break;
321 case QProcess::Timedout:
322 errorStr = tr("The command timed out.");
323 break;
324 default:
325 errorStr = tr("An unknown error occurred.");
326 }
327
328 reportErrorAndDelete(errorStr);
329 }
330
reportErrorAndDelete(const QString & error)331 void CommandProcess::reportErrorAndDelete(const QString &error)
332 {
333 const QString title = tr("Error Executing %1").arg(mName);
334 const QString message = error + QLatin1String("\n\n") + mFinalCommand;
335
336 QWidget *parent = DocumentManager::instance()->widget();
337 QMessageBox::warning(parent, title, message);
338
339 // Make sure this object gets deleted if the process failed to start
340 deleteLater();
341 }
342
343 #include "command.moc"
344