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