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 : }
|