1 /**
2 * \file qmlcommandplugin.cpp
3 * Starter for QML scripts.
4 *
5 * \b Project: Kid3
6 * \author Urs Fleisch
7 * \date 15 Feb 2015
8 *
9 * Copyright (C) 2015-2018 Urs Fleisch
10 *
11 * This file is part of Kid3.
12 *
13 * Kid3 is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * Kid3 is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 #include "qmlcommandplugin.h"
28 #include <QDir>
29 #if !defined NDEBUG && !defined QT_QML_DEBUG
30 #define QT_QML_DEBUG
31 #endif
32 #include <QQuickView>
33 #include <QQmlApplicationEngine>
34 #include <QQmlContext>
35 #include <QQmlComponent>
36 #include <QTimer>
37 #include "kid3application.h"
38
39 /**
40 * Constructor.
41 *
42 * @param parent parent object
43 */
QmlCommandPlugin(QObject * parent)44 QmlCommandPlugin::QmlCommandPlugin(QObject* parent) : QObject(parent),
45 m_app(nullptr), m_qmlView(nullptr), m_qmlEngine(nullptr), m_showOutput(false)
46 {
47 setObjectName(QLatin1String("QmlCommand"));
48 }
49
50 /**
51 * Get keys of available user commands.
52 * @return list of keys, ["qml", "qmlview"].
53 */
userCommandKeys() const54 QStringList QmlCommandPlugin::userCommandKeys() const
55 {
56 return {QLatin1String("qml"), QLatin1String("qmlview")};
57 }
58
59 /**
60 * Initialize processor.
61 * This method must be invoked before the first call to startUserCommand()
62 * to set the application context.
63 * @param app application context
64 */
initialize(Kid3Application * app)65 void QmlCommandPlugin::initialize(Kid3Application* app)
66 {
67 m_app = app;
68 }
69
70 /**
71 * Cleanup processor.
72 * This method must be invoked to close and delete the GUI resources.
73 */
cleanup()74 void QmlCommandPlugin::cleanup()
75 {
76 if (m_qmlView) {
77 m_qmlView->close();
78 }
79 delete m_qmlView;
80 m_qmlView = nullptr;
81 delete m_qmlEngine;
82 m_qmlEngine = nullptr;
83 if (s_messageHandlerInstance == this) {
84 s_messageHandlerInstance = nullptr;
85 }
86 }
87
88 /**
89 * Start a QML script.
90 * @param key user command name, "qml" or "qmlview"
91 * @param arguments arguments to pass to script
92 * @param showOutput true to enable output in output viewer, using signal
93 * commandOutput().
94 * @return true if command is started.
95 */
startUserCommand(const QString & key,const QStringList & arguments,bool showOutput)96 bool QmlCommandPlugin::startUserCommand(
97 const QString& key, const QStringList& arguments, bool showOutput)
98 {
99 if (!arguments.isEmpty()) {
100 if (key == QLatin1String("qmlview")) {
101 m_showOutput = showOutput;
102 if (!m_qmlView) {
103 m_qmlView = new QQuickView;
104 m_qmlView->setResizeMode(QQuickView::SizeRootObjectToView);
105 setupQmlEngine(m_qmlView->engine());
106 // New style functor based connection is not possible because
107 // QQuickCloseEvent is not public (QTBUG-36453, QTBUG-55722).
108 connect(m_qmlView, SIGNAL(closing(QQuickCloseEvent*)), // clazy:exclude=old-style-connect
109 this, SLOT(onQmlViewClosing()));
110 connect(m_qmlView->engine(), &QQmlEngine::quit,
111 this, &QmlCommandPlugin::onQmlViewFinished, Qt::QueuedConnection);
112 }
113 m_qmlView->engine()->rootContext()->setContextProperty(
114 QLatin1String("args"), arguments);
115 onEngineReady();
116 m_qmlView->setSource(QUrl::fromLocalFile(arguments.first()));
117 if (m_qmlView->status() == QQuickView::Ready) {
118 m_qmlView->show();
119 } else {
120 // Probably an error.
121 if (m_showOutput && m_qmlView->status() == QQuickView::Error) {
122 const auto errs = m_qmlView->errors();
123 for (const QQmlError& err : errs) {
124 emit commandOutput(err.toString());
125 }
126 }
127 m_qmlView->engine()->clearComponentCache();
128 onEngineFinished();
129 }
130 return true;
131 } else if (key == QLatin1String("qml")) {
132 m_showOutput = showOutput;
133 if (!m_qmlEngine) {
134 m_qmlEngine = new QQmlEngine;
135 connect(m_qmlEngine, &QQmlEngine::quit, this, &QmlCommandPlugin::onQmlEngineQuit);
136 setupQmlEngine(m_qmlEngine);
137 }
138 m_qmlEngine->rootContext()->setContextProperty(QLatin1String("args"),
139 arguments);
140 QQmlComponent component(m_qmlEngine, arguments.first());
141 if (component.status() == QQmlComponent::Ready) {
142 onEngineReady();
143 component.create();
144 } else {
145 // Probably an error.
146 if (m_showOutput && component.isError()) {
147 const auto errs = component.errors();
148 for (const QQmlError& err : errs) {
149 emit commandOutput(err.toString());
150 }
151 }
152 m_qmlEngine->clearComponentCache();
153 }
154 return true;
155 }
156 }
157 return false;
158 }
159
160 /**
161 * Set import path and app property in QML engine.
162 * @param engine QML engine
163 */
setupQmlEngine(QQmlEngine * engine)164 void QmlCommandPlugin::setupQmlEngine(QQmlEngine* engine)
165 {
166 QDir pluginsDir;
167 #ifdef Q_OS_MAC
168 // Folders containing a dot (like QtQuick.2) will cause Apple's code signing
169 // to fail. On macOS, the QML plugins are therefore in Resorces/qml/imports.
170 const QString qmlImportsRelativeToPlugins =
171 QLatin1String("../Resources/qml/imports");
172 #else
173 const QString qmlImportsRelativeToPlugins = QLatin1String("imports");
174 #endif
175 if (Kid3Application::findPluginsDirectory(pluginsDir) &&
176 pluginsDir.cd(qmlImportsRelativeToPlugins)) {
177 engine->addImportPath(pluginsDir.absolutePath());
178 }
179 engine->rootContext()->setContextProperty(QLatin1String("app"), m_app);
180 connect(engine, &QQmlEngine::warnings,
181 this, &QmlCommandPlugin::onEngineError,
182 Qt::UniqueConnection);
183 }
184
185 /**
186 * Return object which emits commandOutput() signal.
187 * @return this.
188 */
qobject()189 QObject* QmlCommandPlugin::qobject()
190 {
191 return this;
192 }
193
194 /**
195 * Called when an error is reported by the QML engine.
196 */
onEngineError(const QList<QQmlError> & errors)197 void QmlCommandPlugin::onEngineError(const QList<QQmlError>& errors)
198 {
199 if (auto engine = qobject_cast<QQmlEngine*>(sender())) {
200 for (const QQmlError& err : errors) {
201 emit commandOutput(err.toString());
202 }
203 engine->clearComponentCache();
204 onEngineFinished();
205 }
206 }
207
208 /**
209 * Called when the QML view is closing.
210 */
onQmlViewClosing()211 void QmlCommandPlugin::onQmlViewClosing()
212 {
213 if (auto view = qobject_cast<QQuickView*>(sender())) {
214 // This will invoke destruction of the currently loaded QML code.
215 view->setSource(QUrl());
216 view->engine()->clearComponentCache();
217 onEngineFinished();
218 }
219 }
220
221 /**
222 * Called when Qt.quit() is called from the QML code in the QQuickView.
223 */
onQmlViewFinished()224 void QmlCommandPlugin::onQmlViewFinished()
225 {
226 if (m_qmlView) {
227 m_qmlView->close();
228 // Unfortunately, calling close() on the QQuickView will not give a
229 // QEvent::Close in an installed event filter, there is no closeEvent(),
230 // closing() is not signalled. What remains is the hard way.
231 // Calling m_qmlView->deleteLater() will cause a crash when the QML console
232 // is started, a command executed (e.g. app.nextFile()), then .quit and
233 // then a qml script is started.
234 m_qmlView = nullptr;
235 QTimer::singleShot(0, this, &QmlCommandPlugin::onEngineFinished);
236 }
237 }
238
239 /**
240 * Called when Qt.quit() is called from the QML code in the core engine.
241 */
onQmlEngineQuit()242 void QmlCommandPlugin::onQmlEngineQuit()
243 {
244 if (m_qmlEngine) {
245 m_qmlEngine->clearComponentCache();
246 }
247 onEngineFinished();
248 }
249
250 /**
251 * Restore default message handler after QML code is terminated.
252 */
onEngineFinished()253 void QmlCommandPlugin::onEngineFinished()
254 {
255 if (m_showOutput) {
256 qInstallMessageHandler(nullptr);
257 s_messageHandlerInstance = nullptr;
258 }
259 }
260
261 /**
262 * Forward console output to output viewer while QML code is executed.
263 */
onEngineReady()264 void QmlCommandPlugin::onEngineReady()
265 {
266 if (m_showOutput) {
267 s_messageHandlerInstance = this;
268 qInstallMessageHandler(messageHandler);
269 }
270 }
271
272 /** Instance of QmlCommandPlugin running and generating messages. */
273 QmlCommandPlugin* QmlCommandPlugin::s_messageHandlerInstance = nullptr;
274
275 /**
276 * Message handler emitting commandOutput().
277 */
messageHandler(QtMsgType,const QMessageLogContext &,const QString & msg)278 void QmlCommandPlugin::messageHandler(QtMsgType, const QMessageLogContext&, const QString& msg)
279 {
280 if (s_messageHandlerInstance) {
281 emit s_messageHandlerInstance->commandOutput(msg);
282 }
283 }
284