LCOV - code coverage report
Current view: top level - muse/framework/extensions/qml/Muse/Extensions - extensionbuilder.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 106 0
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2024 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : #include "extensionbuilder.h"
      23              : 
      24              : #include <QPointer>
      25              : #include <QQmlEngine>
      26              : 
      27              : #include "global/types/number.h"
      28              : #include "global/io/file.h"
      29              : 
      30              : #include "api/v1/ipluginapiv1.h"
      31              : 
      32              : #include "log.h"
      33              : 
      34              : using namespace muse::extensions;
      35              : 
      36              : const QString ERROR_MSG_PATH = ":/qml/Muse/Extensions/ExtensionErrorMessage.qml";
      37              : 
      38            0 : ExtensionBuilder::ExtensionBuilder(QObject* parent)
      39            0 :     : QObject(parent), Contextable(muse::iocCtxForQmlObject(this))
      40            0 : {}
      41              : 
      42            0 : QString ExtensionBuilder::validateImports(const io::path_t& qmlFilePath) const
      43              : {
      44            0 :     ByteArray data;
      45            0 :     Ret ret = io::File::readFile(qmlFilePath, data);
      46            0 :     if (!ret) {
      47            0 :         return "failed read file: " + qmlFilePath.toQString() + ", err: " + QString::fromStdString(ret.toString());
      48              :     }
      49              : 
      50            0 :     QByteArray qdata = data.toQByteArrayNoCopy();
      51            0 :     QString content = QString::fromUtf8(qdata);
      52            0 :     QStringList lines = content.split('\n');
      53            0 :     for (const QString& line_ : std::as_const(lines)) {
      54            0 :         QString line = line_.trimmed();
      55            0 :         if (line.startsWith("import") && line.contains("Muse.")) {
      56            0 :             return "Importing 'Muse.' modules is prohibited.";
      57              :         }
      58            0 :     }
      59            0 :     return QString();
      60            0 : }
      61              : 
      62            0 : void ExtensionBuilder::load(const QString& extension, const QString& action, QObject* itemParent)
      63              : {
      64            0 :     unload();
      65            0 :     const ExtensionUri uri = ExtensionUri(extension.toStdString());
      66            0 :     const Manifest& manifest = provider()->manifest(uri);
      67            0 :     IF_ASSERT_FAILED(manifest.isValid()) {
      68            0 :         LOGE() << "Not found manifest, uri: " << uri;
      69            0 :         return;
      70              :     }
      71              : 
      72            0 :     const Action a = manifest.action(action.toStdString());
      73            0 :     IF_ASSERT_FAILED(a.isValid()) {
      74            0 :         LOGE() << "Not found action, uri: " << uri;
      75            0 :         return;
      76              :     }
      77              : 
      78            0 :     setTitle(a.title);
      79              : 
      80            0 :     QQmlEngine* engin = nullptr;
      81            0 :     if (a.apiversion == 1) {
      82            0 :         engin = engine()->qmlEngineApiV1();
      83              :     } else {
      84            0 :         engin = engine()->qmlEngine();
      85              :     }
      86              : 
      87            0 :     QObject* qmlObj = nullptr;
      88              : 
      89            0 :     QString errorString;
      90            0 :     if (manifest.apiversion != 1) {
      91            0 :         errorString = validateImports(a.path);
      92              :     }
      93              : 
      94            0 :     if (errorString.isEmpty()) {
      95              :         //! NOTE We create extension UI using a separate engine to control what we provide,
      96              :         //! making it easier to maintain backward compatibility and stability.
      97            0 :         QQmlComponent component = QQmlComponent(engin, a.path.toQString());
      98            0 :         if (component.isReady()) {
      99            0 :             qmlObj = component.createWithInitialProperties({ { "parent", QVariant::fromValue(itemParent) } });
     100              :         } else {
     101            0 :             errorString = component.errorString();
     102            0 :             LOGE() << "Failed to load QML file: " << a.path << ", from extension: " << uri;
     103              :         }
     104            0 :     }
     105              : 
     106            0 :     if (!errorString.isEmpty()) {
     107            0 :         LOGE() << errorString;
     108              : 
     109            0 :         QQmlComponent component = QQmlComponent(engin, ERROR_MSG_PATH);
     110            0 :         if (component.isReady()) {
     111            0 :             qmlObj = component.createWithInitialProperties({
     112            0 :                 { "parent", QVariant::fromValue(itemParent) },
     113              :                 { "text", errorString }
     114              :             });
     115              :         } else {
     116            0 :             LOGE() << "Failed to load QML file: " << a.path << ", from extension: " << uri;
     117            0 :             LOGE() << component.errorString();
     118            0 :             return;
     119              :         }
     120            0 :     }
     121              : 
     122            0 :     m_contentItem = qobject_cast<QQuickItem*>(qmlObj);
     123            0 :     if (!m_contentItem) {
     124            0 :         LOGE() << "Component not QuickItem, file: " << a.path << ", from extension: " << uri;
     125            0 :         delete qmlObj;
     126            0 :         return;
     127              :     }
     128            0 :     qmlObj->setParent(this);
     129              : 
     130            0 :     if (m_contentItem) {
     131            0 :         if (muse::is_zero(m_contentItem->implicitHeight())) {
     132            0 :             m_contentItem->setImplicitHeight(m_contentItem->height());
     133            0 :             if (muse::is_zero(m_contentItem->implicitHeight())) {
     134            0 :                 m_contentItem->setImplicitHeight(480);
     135              :             }
     136              :         }
     137              : 
     138            0 :         if (muse::is_zero(m_contentItem->implicitWidth())) {
     139            0 :             m_contentItem->setImplicitWidth(m_contentItem->width());
     140            0 :             if (muse::is_zero(m_contentItem->implicitWidth())) {
     141            0 :                 m_contentItem->setImplicitWidth(a.docked ? 360 : 600);
     142              :             }
     143              :         }
     144              :     }
     145              : 
     146            0 :     emit contentItemChanged();
     147              : 
     148            0 :     if (a.apiversion == 1) {
     149            0 :         apiv1::IPluginApiV1* plugin = dynamic_cast<apiv1::IPluginApiV1*>(m_contentItem.data());
     150              : 
     151            0 :         if (!plugin) {
     152            0 :             return;
     153              :         }
     154              : 
     155            0 :         plugin->setup(engin);
     156              : 
     157            0 :         plugin->closeRequest().onNotify(this, [this]() {
     158            0 :             emit closeRequested();
     159            0 :         });
     160              : 
     161              :         //! NOTE For version 1 plugins we need to call run.
     162              :         //! Queued on Qt's event loop rather than async::Async: the viewer is opened
     163              :         //! with openSync from inside a command, that is from inside async message
     164              :         //! processing, and the nested loop of the dialog cannot process async
     165              :         //! messages again, so run would only arrive once the dialog had closed.
     166            0 :         const QPointer<QQuickItem> contentItem = m_contentItem;
     167            0 :         QMetaObject::invokeMethod(this, [this, contentItem, a, uri]() {
     168            0 :             if (!contentItem || contentItem != m_contentItem) {
     169            0 :                 return;
     170              :             }
     171            0 :             auto plugin = dynamic_cast<apiv1::IPluginApiV1*>(contentItem.data());
     172            0 :             if (!plugin) {
     173            0 :                 LOGE() << "Qml Object not MuseScore plugin: " << a.path
     174            0 :                        << ", from extension: " << uri;
     175            0 :                 return;
     176              :             }
     177            0 :             plugin->runPlugin();
     178              :         }, Qt::QueuedConnection);
     179            0 :     }
     180            0 : }
     181              : 
     182              : //! Destroy the plug-in synchronously when its dock closes or the active score changes.
     183            0 : void ExtensionBuilder::unload()
     184              : {
     185            0 :     auto item = m_contentItem.data();
     186            0 :     m_contentItem = nullptr;
     187            0 :     emit contentItemChanged();
     188            0 :     delete item;
     189            0 : }
     190              : 
     191            0 : QQuickItem* ExtensionBuilder::contentItem() const
     192              : {
     193            0 :     return m_contentItem;
     194              : }
     195              : 
     196            0 : void ExtensionBuilder::setTitle(QString title)
     197              : {
     198            0 :     if (m_title == title) {
     199            0 :         return;
     200              :     }
     201            0 :     m_title = title;
     202            0 :     emit titleChanged();
     203              : }
     204              : 
     205            0 : QString ExtensionBuilder::title() const
     206              : {
     207            0 :     return m_title;
     208              : }
        

Generated by: LCOV version 2.0-1