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) 2025 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 "vstview.h"
23 :
24 : #include <QQuickWindow>
25 :
26 : #include "ivstplugininstance.h"
27 :
28 : #ifdef Q_OS_WIN
29 : #include <windows.h>
30 : #endif
31 :
32 : #ifdef Q_OS_LINUX
33 : #define USE_LINUX_RUNLOOP
34 : #endif
35 :
36 : #ifdef USE_LINUX_RUNLOOP
37 : #include "platform/linux/runloop.h"
38 : #endif
39 :
40 : #include "global/types/number.h"
41 : #include "log.h"
42 :
43 : using namespace muse::vst;
44 :
45 : #ifdef Q_OS_WIN
46 : static void removeMinMaxButtons(HWND window)
47 : {
48 : IF_ASSERT_FAILED(window && IsWindow(window)) {
49 : return;
50 : }
51 :
52 : // Remove the minimize & maximize buttons
53 : LONG style = GetWindowLong(window, GWL_STYLE);
54 : style &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
55 : SetWindowLong(window, GWL_STYLE, style);
56 :
57 : // Force redraw of non-client area
58 : SetWindowPos(window, nullptr, 0, 0, 0, 0,
59 : SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
60 : }
61 :
62 : #endif
63 :
64 0 : ::Steinberg::uint32 PLUGIN_API VstView::addRef()
65 : {
66 0 : return ::Steinberg::FUnknownPrivate::atomicAdd(__funknownRefCount, 1);
67 : }
68 :
69 0 : ::Steinberg::uint32 PLUGIN_API VstView::release()
70 : {
71 0 : if (::Steinberg::FUnknownPrivate::atomicAdd(__funknownRefCount, -1) == 0) {
72 0 : return 0;
73 : }
74 0 : return __funknownRefCount;
75 : }
76 :
77 0 : Steinberg::tresult VstView::queryInterface(const ::Steinberg::TUID _iid, void** obj)
78 : {
79 0 : QUERY_INTERFACE(_iid, obj, Steinberg::FUnknown::iid, Steinberg::IPlugFrame);
80 0 : QUERY_INTERFACE(_iid, obj, Steinberg::IPlugFrame::iid, Steinberg::IPlugFrame);
81 : //As VST3 documentation states, IPlugFrame also has to provide
82 : //reference to the Steinberg::Linux::IRunLoop implementation.
83 : #ifdef USE_LINUX_RUNLOOP
84 0 : if (m_runLoop && Steinberg::FUnknownPrivate::iidEqual(_iid, Steinberg::Linux::IRunLoop::iid)) {
85 0 : m_runLoop->addRef();
86 0 : *obj = static_cast<Steinberg::Linux::IRunLoop*>(m_runLoop);
87 0 : return ::Steinberg::kResultOk;
88 : }
89 : #endif
90 0 : *obj = nullptr;
91 0 : return ::Steinberg::kNoInterface;
92 : }
93 :
94 0 : static FIDString currentPlatformUiType()
95 : {
96 : #ifdef Q_OS_MAC
97 : return Steinberg::kPlatformTypeNSView;
98 : #elif defined(Q_OS_IOS)
99 : return Steinberg::kPlatformTypeUIView;
100 : #elif defined(Q_OS_WIN)
101 : return Steinberg::kPlatformTypeHWND;
102 : #else
103 0 : return Steinberg::kPlatformTypeX11EmbedWindowID;
104 : #endif
105 : }
106 :
107 0 : VstView::VstView(QQuickItem* parent)
108 0 : : QQuickItem(parent)
109 : {
110 0 : FUNKNOWN_CTOR; // IPlugFrame
111 :
112 : #ifdef USE_LINUX_RUNLOOP
113 0 : m_runLoop = new RunLoop();
114 : #endif
115 0 : }
116 :
117 0 : VstView::~VstView()
118 : {
119 : FUNKNOWN_DTOR; // IPlugFrame
120 :
121 0 : deinit();
122 0 : }
123 :
124 0 : void VstView::init()
125 : {
126 0 : m_instance = instancesRegister()->instanceById(m_instanceId);
127 0 : IF_ASSERT_FAILED(m_instance) {
128 0 : return;
129 : }
130 :
131 0 : m_title = QString::fromStdString(m_instance->name());
132 0 : emit titleChanged();
133 :
134 0 : m_view = m_instance->createView();
135 0 : if (!m_view) {
136 0 : return;
137 : }
138 :
139 0 : if (m_view->isPlatformTypeSupported(currentPlatformUiType()) != Steinberg::kResultTrue) {
140 0 : return;
141 : }
142 :
143 0 : updateScreenMetrics(window()->screen());
144 :
145 0 : m_view->setFrame(this);
146 :
147 0 : m_vstWindow = new QWindow(window());
148 :
149 : Steinberg::tresult attached;
150 0 : attached = m_view->attached(reinterpret_cast<void*>(m_vstWindow->winId()), currentPlatformUiType());
151 0 : if (attached != Steinberg::kResultOk) {
152 0 : LOGE() << "Unable to attach vst plugin view to window"
153 0 : << ", instance name: " << m_instance->name();
154 0 : return;
155 : }
156 :
157 0 : connect(window(), &QWindow::screenChanged, this, [this](QScreen* screen) {
158 0 : updateScreenMetrics(screen);
159 :
160 0 : QMetaObject::invokeMethod(this, [this] {
161 0 : updateViewGeometry();
162 0 : }, Qt::QueuedConnection);
163 0 : });
164 :
165 : // Do not only rely on `QWindow::screenChanged` signal, which often does not get emitted though it should.
166 : // Proactively check for screen resolution changes.
167 0 : connect(&m_screenMetricsTimer, &QTimer::timeout, this, [this]() {
168 0 : QWindow* win = window();
169 0 : if (!win) {
170 0 : return;
171 : }
172 0 : QScreen* screen = win->screen();
173 0 : if (m_currentScreen != screen || m_screenMetrics.availableSize != screen->availableSize()
174 0 : || !is_equal(m_screenMetrics.devicePixelRatio, screen->devicePixelRatio())) {
175 0 : updateScreenMetrics(screen);
176 0 : updateViewGeometry();
177 : }
178 : });
179 0 : m_screenMetricsTimer.start(std::chrono::milliseconds { 100 });
180 :
181 0 : updateViewGeometry();
182 :
183 0 : m_vstWindow->show();
184 :
185 : #ifdef Q_OS_WIN
186 : HWND winHwnd = reinterpret_cast<HWND>(m_vstWindow->winId());
187 : HWND viewHwnd = GetWindow(winHwnd, GW_CHILD);
188 : HWND rootHandle = GetAncestor(winHwnd, GA_ROOT);
189 : m_rootHandle = rootHandle;
190 :
191 : if (viewHwnd && IsWindow(viewHwnd)) {
192 : m_plugViewHandle = viewHwnd;
193 : qApp->installNativeEventFilter(this);
194 : }
195 :
196 : removeMinMaxButtons(rootHandle);
197 : #endif
198 :
199 : #ifdef Q_OS_MAC
200 : window()->installEventFilter(this);
201 : #endif
202 : }
203 :
204 0 : void VstView::deinit()
205 : {
206 : #ifdef Q_OS_WIN
207 : qApp->removeNativeEventFilter(this);
208 : #endif
209 :
210 0 : m_screenMetricsTimer.stop();
211 :
212 0 : deinitPluginView();
213 :
214 0 : m_plugViewHandle = nullptr;
215 0 : m_rootHandle = nullptr;
216 :
217 0 : if (m_vstWindow) {
218 0 : m_vstWindow->hide();
219 0 : delete m_vstWindow;
220 0 : m_vstWindow = nullptr;
221 : }
222 :
223 : #ifdef USE_LINUX_RUNLOOP
224 0 : if (m_runLoop) {
225 0 : m_runLoop->stop();
226 0 : delete m_runLoop;
227 : }
228 : #endif
229 :
230 0 : if (m_instance) {
231 0 : m_instance->refreshConfig();
232 0 : m_instance = nullptr;
233 : }
234 0 : }
235 :
236 0 : void VstView::deinitPluginView()
237 : {
238 0 : if (m_view) {
239 0 : m_view->setFrame(nullptr);
240 0 : m_view->removed();
241 0 : m_view = nullptr;
242 : }
243 0 : }
244 :
245 0 : Steinberg::tresult VstView::resizeView(Steinberg::IPlugView* view, Steinberg::ViewRect* requiredSize)
246 : {
247 0 : IF_ASSERT_FAILED(m_vstWindow) {
248 0 : return Steinberg::kResultFalse;
249 : }
250 :
251 : // anti recursion
252 0 : if (m_resizeViewCalled) {
253 0 : return Steinberg::kResultTrue;
254 : }
255 :
256 0 : m_resizeViewCalled = true;
257 :
258 0 : view->checkSizeConstraint(requiredSize);
259 :
260 0 : const int titleBarHeight = window()->frameGeometry().height() - window()->geometry().height();
261 :
262 0 : const int requiredWidth = requiredSize->getWidth() / m_screenMetrics.devicePixelRatio;
263 0 : const int requiredHeight = requiredSize->getHeight() / m_screenMetrics.devicePixelRatio;
264 :
265 0 : const int availableWidth = m_screenMetrics.availableSize.width() - 2 * m_sidePadding;
266 0 : const int availableHeight = m_screenMetrics.availableSize.height() - titleBarHeight - m_topPadding - m_bottomPadding;
267 :
268 0 : const int newWidth = std::min(requiredWidth, availableWidth);
269 0 : const int newHeight = std::min(requiredHeight, availableHeight);
270 :
271 0 : const int implicitWidth = std::max(m_minimumWidth, newWidth);
272 0 : setImplicitWidth(implicitWidth);
273 0 : setImplicitHeight(newHeight);
274 :
275 0 : const int sidePadding = std::max(m_sidePadding, (implicitWidth - newWidth) / 2);
276 0 : m_vstWindow->setGeometry(sidePadding, m_topPadding, newWidth, newHeight);
277 :
278 0 : Steinberg::ViewRect vstSize;
279 0 : vstSize.right = newWidth * m_screenMetrics.devicePixelRatio;
280 0 : vstSize.bottom = newHeight * m_screenMetrics.devicePixelRatio;
281 : // Some VST plugins won't cooperate and size down, in which case their UI will be clipped
282 : // by `m_vstWindow`. This is better than an overflow, because the host might want to place
283 : // control buttons below the UI, and these ought not to be hidden.
284 0 : view->onSize(&vstSize);
285 :
286 0 : m_resizeViewCalled = false;
287 :
288 0 : return Steinberg::kResultTrue;
289 : }
290 :
291 0 : bool VstView::eventFilter(QObject* watched, QEvent* event)
292 : {
293 0 : if (event->type() == QEvent::Close) {
294 0 : deinitPluginView();
295 : }
296 :
297 0 : return QQuickItem::eventFilter(watched, event);
298 : }
299 :
300 0 : bool VstView::nativeEventFilter(const QByteArray& eventType, void* message, qintptr* result)
301 : {
302 : #ifdef Q_OS_WIN
303 : if (!m_vstWindow || !m_vstWindow->isVisible() || !m_plugViewHandle || !m_rootHandle) {
304 : return false;
305 : }
306 :
307 : if (eventType != QByteArrayLiteral("windows_generic_MSG")) {
308 : return false;
309 : }
310 :
311 : const MSG* msg = static_cast<MSG*>(message);
312 : if (!msg->hwnd) {
313 : return false;
314 : }
315 : if (msg->message != WM_ERASEBKGND && msg->message != WM_WINDOWPOSCHANGED) {
316 : return false;
317 : }
318 :
319 : if (msg->hwnd != m_rootHandle) {
320 : const HWND root = GetAncestor(msg->hwnd, GA_ROOT);
321 : if (root != m_rootHandle) {
322 : return false;
323 : }
324 : }
325 :
326 : if (msg->message == WM_ERASEBKGND) {
327 : *result = 1;
328 : return true; // tell Windows: "already erased"
329 : }
330 :
331 : if (msg->message == WM_WINDOWPOSCHANGED && !m_updatePending) {
332 : m_updatePending = true;
333 : QTimer::singleShot(16, this, [this]() { // ~60 FPS
334 : m_updatePending = false;
335 :
336 : HWND hwnd = reinterpret_cast<HWND>(m_plugViewHandle);
337 : if (!hwnd || !IsWindow(hwnd)) {
338 : m_plugViewHandle = nullptr;
339 : return;
340 : }
341 :
342 : RedrawWindow(hwnd, nullptr, nullptr,
343 : RDW_INVALIDATE | RDW_UPDATENOW | RDW_NOERASE);
344 : });
345 : }
346 : #else
347 : Q_UNUSED(eventType);
348 : Q_UNUSED(message);
349 : Q_UNUSED(result);
350 : #endif
351 :
352 0 : return false;
353 : }
354 :
355 0 : void VstView::updateScreenMetrics(QScreen* screen)
356 : {
357 0 : m_currentScreen = screen;
358 0 : m_screenMetrics.availableSize = m_currentScreen->availableSize();
359 : #ifdef Q_OS_MAC
360 : constexpr auto devicePixelRatio = 1.0;
361 : #else
362 0 : const auto devicePixelRatio = m_currentScreen->devicePixelRatio();
363 : #endif
364 0 : m_screenMetrics.devicePixelRatio = devicePixelRatio;
365 :
366 : #ifdef Q_OS_WIN
367 : Steinberg::FUnknownPtr<IPluginContentScaleHandler> scalingHandler(m_view);
368 : if (scalingHandler) {
369 : scalingHandler->setContentScaleFactor(m_screenMetrics.devicePixelRatio);
370 : }
371 : #endif
372 0 : }
373 :
374 0 : void VstView::updateViewGeometry()
375 : {
376 0 : if (!m_view) {
377 0 : return;
378 : }
379 :
380 0 : Steinberg::ViewRect size;
381 0 : m_view->getSize(&size);
382 :
383 0 : resizeView(m_view, &size);
384 : }
385 :
386 0 : int VstView::instanceId() const
387 : {
388 0 : return m_instanceId;
389 : }
390 :
391 0 : void VstView::setInstanceId(int newInstanceId)
392 : {
393 0 : if (m_instanceId == newInstanceId) {
394 0 : return;
395 : }
396 0 : m_instanceId = newInstanceId;
397 0 : emit instanceIdChanged();
398 : }
399 :
400 0 : QString VstView::title() const
401 : {
402 0 : return m_title;
403 : }
404 :
405 0 : int VstView::sidePadding() const
406 : {
407 0 : return m_sidePadding;
408 : }
409 :
410 0 : void VstView::setsidePadding(int sidePadding)
411 : {
412 0 : if (m_sidePadding == sidePadding) {
413 0 : return;
414 : }
415 0 : m_sidePadding = sidePadding;
416 0 : emit sidePaddingChanged();
417 0 : updateViewGeometry();
418 : }
419 :
420 0 : int VstView::topPadding() const
421 : {
422 0 : return m_topPadding;
423 : }
424 :
425 0 : void VstView::setTopPadding(int topPadding)
426 : {
427 0 : if (m_topPadding == topPadding) {
428 0 : return;
429 : }
430 0 : m_topPadding = topPadding;
431 0 : emit topPaddingChanged();
432 0 : updateViewGeometry();
433 : }
434 :
435 0 : int VstView::bottomPadding() const
436 : {
437 0 : return m_bottomPadding;
438 : }
439 :
440 0 : void VstView::setBottomPadding(int bottomPadding)
441 : {
442 0 : if (m_bottomPadding == bottomPadding) {
443 0 : return;
444 : }
445 0 : m_bottomPadding = bottomPadding;
446 0 : emit bottomPaddingChanged();
447 0 : updateViewGeometry();
448 : }
449 :
450 0 : int VstView::minimumWidth() const
451 : {
452 0 : return m_minimumWidth;
453 : }
454 :
455 0 : void VstView::setMinimumWidth(int minimumWidth)
456 : {
457 0 : if (m_minimumWidth == minimumWidth) {
458 0 : return;
459 : }
460 0 : m_minimumWidth = minimumWidth;
461 0 : emit minimumWidthChanged();
462 0 : updateViewGeometry();
463 : }
|