File indexing completed on 2024-05-12 04:55:43

0001 /**
0002  * \file qmlcommandplugin.cpp
0003  * Starter for QML scripts.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 15 Feb 2015
0008  *
0009  * Copyright (C) 2015-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "qmlcommandplugin.h"
0028 #include <QDir>
0029 #if !defined NDEBUG && !defined QT_QML_DEBUG
0030 #define QT_QML_DEBUG
0031 #endif
0032 #include <QQuickView>
0033 #include <QQmlApplicationEngine>
0034 #include <QQmlContext>
0035 #include <QQmlComponent>
0036 #include <QTimer>
0037 #include "kid3application.h"
0038 
0039 /**
0040  * Constructor.
0041  *
0042  * @param parent parent object
0043  */
0044 QmlCommandPlugin::QmlCommandPlugin(QObject* parent) : QObject(parent),
0045   m_app(nullptr), m_qmlView(nullptr), m_qmlEngine(nullptr), m_showOutput(false)
0046 {
0047   setObjectName(QLatin1String("QmlCommand"));
0048 }
0049 
0050 /**
0051  * Get keys of available user commands.
0052  * @return list of keys, ["qml", "qmlview"].
0053  */
0054 QStringList QmlCommandPlugin::userCommandKeys() const
0055 {
0056   return {QLatin1String("qml"), QLatin1String("qmlview")};
0057 }
0058 
0059 /**
0060  * Initialize processor.
0061  * This method must be invoked before the first call to startUserCommand()
0062  * to set the application context.
0063  * @param app application context
0064  */
0065 void QmlCommandPlugin::initialize(Kid3Application* app)
0066 {
0067   m_app = app;
0068 }
0069 
0070 /**
0071  * Cleanup processor.
0072  * This method must be invoked to close and delete the GUI resources.
0073  */
0074 void QmlCommandPlugin::cleanup()
0075 {
0076   if (m_qmlView) {
0077     m_qmlView->close();
0078   }
0079   if (!m_qmlEngine) {
0080     // If both m_qmlEngine and m_qmlView have been constructed, deleting both
0081     // will result in a crash upon termination (Bug 457655). This happens when
0082     // using both a @qml user action (e.g. TitleCase.qml) and a @qmlview
0083     // user action (QmlConsole.qml). Do not delete m_qmlView in such a case.
0084     delete m_qmlView;
0085   }
0086   m_qmlView = nullptr;
0087   delete m_qmlEngine;
0088   m_qmlEngine = nullptr;
0089   if (s_messageHandlerInstance == this) {
0090     s_messageHandlerInstance = nullptr;
0091   }
0092 }
0093 
0094 /**
0095  * Start a QML script.
0096  * @param key user command name, "qml" or "qmlview"
0097  * @param arguments arguments to pass to script
0098  * @param showOutput true to enable output in output viewer, using signal
0099  *                   commandOutput().
0100  * @return true if command is started.
0101  */
0102 bool QmlCommandPlugin::startUserCommand(
0103     const QString& key, const QStringList& arguments, bool showOutput)
0104 {
0105   if (!arguments.isEmpty()) {
0106     if (key == QLatin1String("qmlview")) {
0107       m_showOutput = showOutput;
0108       if (!m_qmlView) {
0109         m_qmlView = new QQuickView;
0110         m_qmlView->setResizeMode(QQuickView::SizeRootObjectToView);
0111         setupQmlEngine(m_qmlView->engine());
0112         // New style functor based connection is not possible because
0113         // QQuickCloseEvent is not public (QTBUG-36453, QTBUG-55722).
0114         connect(m_qmlView, SIGNAL(closing(QQuickCloseEvent*)), // clazy:exclude=old-style-connect
0115                 this, SLOT(onQmlViewClosing()));
0116         connect(m_qmlView->engine(), &QQmlEngine::quit,
0117                 this, &QmlCommandPlugin::onQmlViewFinished, Qt::QueuedConnection);
0118       }
0119       m_qmlView->engine()->rootContext()->setContextProperty(
0120             QLatin1String("args"), arguments);
0121       onEngineReady();
0122       m_qmlView->setSource(QUrl::fromLocalFile(arguments.first()));
0123       if (m_qmlView->status() == QQuickView::Ready) {
0124         m_qmlView->show();
0125       } else {
0126         // Probably an error.
0127         if (m_showOutput && m_qmlView->status() == QQuickView::Error) {
0128           const auto errs = m_qmlView->errors();
0129           for (const QQmlError& err : errs) {
0130             emit commandOutput(err.toString());
0131           }
0132         }
0133         m_qmlView->engine()->clearComponentCache();
0134         onEngineFinished();
0135       }
0136       return true;
0137     }
0138     if (key == QLatin1String("qml")) {
0139       m_showOutput = showOutput;
0140       if (!m_qmlEngine) {
0141         m_qmlEngine = new QQmlEngine;
0142         connect(m_qmlEngine, &QQmlEngine::quit, this, &QmlCommandPlugin::onQmlEngineQuit);
0143         setupQmlEngine(m_qmlEngine);
0144       }
0145       m_qmlEngine->rootContext()->setContextProperty(QLatin1String("args"),
0146                                                      arguments);
0147       if (QQmlComponent component(m_qmlEngine, arguments.first());
0148         component.status() == QQmlComponent::Ready) {
0149         onEngineReady();
0150         component.create();
0151       } else {
0152         // Probably an error.
0153         if (m_showOutput && component.isError()) {
0154           const auto errs = component.errors();
0155           for (const QQmlError& err : errs) {
0156             emit commandOutput(err.toString());
0157           }
0158         }
0159         m_qmlEngine->clearComponentCache();
0160         onEngineFinished();
0161       }
0162       return true;
0163     }
0164   }
0165   return false;
0166 }
0167 
0168 /**
0169  * Set import path and app property in QML engine.
0170  * @param engine QML engine
0171  */
0172 void QmlCommandPlugin::setupQmlEngine(QQmlEngine* engine)
0173 {
0174 #ifdef Q_OS_MAC
0175   // Folders containing a dot (like QtQuick.2) will cause Apple's code signing
0176   // to fail. On macOS, the QML plugins are therefore in Resorces/qml/imports.
0177   const QString qmlImportsRelativeToPlugins =
0178       QLatin1String("../Resources/qml/imports");
0179 #else
0180   const QString qmlImportsRelativeToPlugins = QLatin1String("imports");
0181 #endif
0182   if (QDir pluginsDir;
0183       Kid3Application::findPluginsDirectory(pluginsDir) &&
0184       pluginsDir.cd(qmlImportsRelativeToPlugins)) {
0185     engine->addImportPath(pluginsDir.absolutePath());
0186   }
0187   engine->rootContext()->setContextProperty(QLatin1String("app"), m_app);
0188   connect(engine, &QQmlEngine::warnings,
0189           this, &QmlCommandPlugin::onEngineError,
0190           Qt::UniqueConnection);
0191 }
0192 
0193 /**
0194  * Return object which emits commandOutput() signal.
0195  * @return this.
0196  */
0197 QObject* QmlCommandPlugin::qobject()
0198 {
0199   return this;
0200 }
0201 
0202 /**
0203  * Called when an error is reported by the QML engine.
0204  */
0205 void QmlCommandPlugin::onEngineError(const QList<QQmlError>& errors)
0206 {
0207   if (auto engine = qobject_cast<QQmlEngine*>(sender())) {
0208     for (const QQmlError& err : errors) {
0209       emit commandOutput(err.toString());
0210     }
0211     engine->clearComponentCache();
0212     onEngineFinished();
0213   }
0214 }
0215 
0216 /**
0217  * Called when the QML view is closing.
0218  */
0219 void QmlCommandPlugin::onQmlViewClosing()
0220 {
0221   if (auto view = qobject_cast<QQuickView*>(sender())) {
0222     // This will invoke destruction of the currently loaded QML code.
0223     view->setSource(QUrl());
0224     view->engine()->clearComponentCache();
0225     onEngineFinished();
0226   }
0227 }
0228 
0229 /**
0230  * Called when Qt.quit() is called from the QML code in the QQuickView.
0231  */
0232 void QmlCommandPlugin::onQmlViewFinished()
0233 {
0234   if (m_qmlView) {
0235     m_qmlView->close();
0236     // Unfortunately, calling close() on the QQuickView will not give a
0237     // QEvent::Close in an installed event filter, there is no closeEvent(),
0238     // closing() is not signalled. What remains is the hard way.
0239     // Calling m_qmlView->deleteLater() will cause a crash when the QML console
0240     // is started, a command executed (e.g. app.nextFile()), then .quit and
0241     // then a qml script is started.
0242     m_qmlView = nullptr;
0243     QTimer::singleShot(0, this, &QmlCommandPlugin::onEngineFinished);
0244   }
0245 }
0246 
0247 /**
0248  * Called when Qt.quit() is called from the QML code in the core engine.
0249  */
0250 void QmlCommandPlugin::onQmlEngineQuit()
0251 {
0252   if (m_qmlEngine) {
0253     m_qmlEngine->clearComponentCache();
0254   }
0255   onEngineFinished();
0256 }
0257 
0258 /**
0259  * Restore default message handler after QML code is terminated.
0260  */
0261 void QmlCommandPlugin::onEngineFinished()
0262 {
0263   if (m_showOutput) {
0264     qInstallMessageHandler(nullptr);
0265     s_messageHandlerInstance = nullptr;
0266   }
0267   QTimer::singleShot(0, this, [this] { emit finished(0); });
0268 }
0269 
0270 /**
0271  * Forward console output to output viewer while QML code is executed.
0272  */
0273 void QmlCommandPlugin::onEngineReady()
0274 {
0275   if (m_showOutput) {
0276     s_messageHandlerInstance = this;
0277     qInstallMessageHandler(messageHandler);
0278   }
0279 }
0280 
0281 /** Instance of QmlCommandPlugin running and generating messages. */
0282 QmlCommandPlugin* QmlCommandPlugin::s_messageHandlerInstance = nullptr;
0283 
0284 /**
0285  * Message handler emitting commandOutput().
0286  */
0287 void QmlCommandPlugin::messageHandler(QtMsgType, const QMessageLogContext&, const QString& msg)
0288 {
0289   if (s_messageHandlerInstance) {
0290     emit s_messageHandlerInstance->commandOutput(msg);
0291   }
0292 }