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 }