1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmlpreviewhandler.h"
5
6#include <QtCore/qtimer.h>
7#include <QtCore/qsettings.h>
8#include <QtCore/qlibraryinfo.h>
9
10#include <QtGui/qwindow.h>
11#include <QtGui/qguiapplication.h>
12#include <QtQuick/qquickwindow.h>
13#include <QtQuick/qquickitem.h>
14#include <QtQml/qqmlcomponent.h>
15
16#include <private/qquickpixmapcache_p.h>
17#include <private/qquickview_p.h>
18#include <private/qhighdpiscaling_p.h>
19
20QT_BEGIN_NAMESPACE
21
22struct QuitLockDisabler
23{
24 const bool quitLockEnabled;
25
26 Q_NODISCARD_CTOR QuitLockDisabler()
27 : quitLockEnabled(QCoreApplication::isQuitLockEnabled())
28 {
29 QCoreApplication::setQuitLockEnabled(false);
30 }
31
32 ~QuitLockDisabler()
33 {
34 QCoreApplication::setQuitLockEnabled(quitLockEnabled);
35 }
36};
37
38QQmlPreviewHandler::QQmlPreviewHandler(QObject *parent) : QObject(parent)
39{
40 m_dummyItem.reset(other: new QQuickItem);
41
42 // TODO: Is there a better way to determine this? We want to keep the window alive when possible
43 // as otherwise it will reappear in a different place when (re)loading a file. However,
44 // the file we load might create another window, in which case the eglfs plugin (and
45 // others?) will do a qFatal as it only supports a single window.
46 const QString platformName = QGuiApplication::platformName();
47 m_supportsMultipleWindows = (platformName == QStringLiteral("windows")
48 || platformName == QStringLiteral("cocoa")
49 || platformName == QStringLiteral("xcb")
50 || platformName == QStringLiteral("wayland"));
51
52 QCoreApplication::instance()->installEventFilter(filterObj: this);
53
54 m_fpsTimer.setInterval(1000);
55 connect(sender: &m_fpsTimer, signal: &QTimer::timeout, context: this, slot: &QQmlPreviewHandler::fpsTimerHit);
56}
57
58QQmlPreviewHandler::~QQmlPreviewHandler()
59{
60 clear();
61}
62
63static void closeAllWindows()
64{
65 const QWindowList windows = QGuiApplication::allWindows();
66 for (QWindow *window : windows)
67 window->close();
68}
69
70bool QQmlPreviewHandler::eventFilter(QObject *obj, QEvent *event)
71{
72 if (m_currentWindow && (event->type() == QEvent::Move) &&
73 qobject_cast<QQuickWindow*>(object: obj) == m_currentWindow) {
74 m_lastPosition.takePosition(window: m_currentWindow);
75 }
76
77 return QObject::eventFilter(watched: obj, event);
78}
79
80QQuickItem *QQmlPreviewHandler::currentRootItem()
81{
82 return m_currentRootItem;
83}
84
85void QQmlPreviewHandler::addEngine(QQmlEngine *qmlEngine)
86{
87 m_engines.append(t: qmlEngine);
88}
89
90void QQmlPreviewHandler::removeEngine(QQmlEngine *qmlEngine)
91{
92 const bool found = m_engines.removeOne(t: qmlEngine);
93 Q_ASSERT(found);
94 for (QObject *obj : m_createdObjects)
95 if (obj && ::qmlEngine(obj) == qmlEngine)
96 delete obj;
97 m_createdObjects.removeAll(t: nullptr);
98}
99
100void QQmlPreviewHandler::loadUrl(const QUrl &url)
101{
102 QSharedPointer<QuitLockDisabler> disabler(new QuitLockDisabler);
103
104 clear();
105 m_component.reset(other: nullptr);
106 QQuickPixmap::purgeCache();
107
108 const int numEngines = m_engines.size();
109 if (numEngines > 1) {
110 emit error(message: QString::fromLatin1(ba: "%1 QML engines available. We cannot decide which one "
111 "should load the component.").arg(a: numEngines));
112 return;
113 } else if (numEngines == 0) {
114 emit error(message: QLatin1String("No QML engines found."));
115 return;
116 }
117 m_lastPosition.loadWindowPositionSettings(url);
118
119 QQmlEngine *engine = m_engines.front();
120 engine->clearComponentCache();
121 m_component.reset(other: new QQmlComponent(engine, url, this));
122
123 auto onStatusChanged = [disabler, this](QQmlComponent::Status status) {
124 switch (status) {
125 case QQmlComponent::Null:
126 case QQmlComponent::Loading:
127 return true; // try again later
128 case QQmlComponent::Ready:
129 tryCreateObject();
130 break;
131 case QQmlComponent::Error:
132 emit error(message: m_component->errorString());
133 break;
134 default:
135 Q_UNREACHABLE();
136 break;
137 }
138
139 disconnect(sender: m_component.data(), signal: &QQmlComponent::statusChanged, receiver: this, zero: nullptr);
140 return false; // we're done
141 };
142
143 if (onStatusChanged(m_component->status()))
144 connect(sender: m_component.data(), signal: &QQmlComponent::statusChanged, context: this, slot&: onStatusChanged);
145}
146
147void QQmlPreviewHandler::rerun()
148{
149 if (m_component.isNull() || !m_component->isReady())
150 emit error(message: QLatin1String("Component is not ready."));
151
152 QuitLockDisabler disabler;
153 Q_UNUSED(disabler);
154 clear();
155 tryCreateObject();
156}
157
158void QQmlPreviewHandler::zoom(qreal newFactor)
159{
160 m_zoomFactor = newFactor;
161 QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlPreviewHandler::doZoom);
162}
163
164void QQmlPreviewHandler::doZoom()
165{
166 if (!m_currentWindow)
167 return;
168 if (qFuzzyIsNull(d: m_zoomFactor)) {
169 emit error(message: QString::fromLatin1(ba: "Zooming with factor: %1 will result in nothing " \
170 "so it will be ignored.").arg(a: m_zoomFactor));
171 return;
172 }
173
174 bool resetZoom = false;
175 if (m_zoomFactor < 0) {
176 resetZoom = true;
177 m_zoomFactor = 1.0;
178 }
179
180 m_currentWindow->setGeometry(m_currentWindow->geometry());
181
182 m_lastPosition.takePosition(window: m_currentWindow, state: QQmlPreviewPosition::InitializePosition);
183 m_currentWindow->destroy();
184
185 for (QScreen *screen : QGuiApplication::screens())
186 QHighDpiScaling::setScreenFactor(screen, factor: m_zoomFactor);
187 if (resetZoom)
188 QHighDpiScaling::updateHighDpiScaling();
189
190 m_currentWindow->show();
191 m_lastPosition.initLastSavedWindowPosition(window: m_currentWindow);
192}
193
194void QQmlPreviewHandler::clear()
195{
196 qDeleteAll(c: m_createdObjects);
197 m_createdObjects.clear();
198 setCurrentWindow(nullptr);
199}
200
201Qt::WindowFlags fixFlags(Qt::WindowFlags flags)
202{
203 // If only the type flag is given, some other window flags are automatically assumed. When we
204 // add a flag, we need to make those explicit.
205 switch (flags) {
206 case Qt::Window:
207 return flags | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint
208 | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint;
209 case Qt::Dialog:
210 case Qt::Tool:
211 return flags | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint;
212 default:
213 return flags;
214 }
215}
216
217void QQmlPreviewHandler::showObject(QObject *object)
218{
219 if (QWindow *window = qobject_cast<QWindow *>(o: object)) {
220 setCurrentWindow(qobject_cast<QQuickWindow *>(object: window));
221 for (QWindow *otherWindow : QGuiApplication::allWindows()) {
222 if (QQuickWindow *quickWindow = qobject_cast<QQuickWindow *>(object: otherWindow)) {
223 if (quickWindow == m_currentWindow)
224 continue;
225 quickWindow->setVisible(false);
226 quickWindow->setFlags(quickWindow->flags() & ~Qt::WindowStaysOnTopHint);
227 }
228 }
229 } else if (QQuickItem *item = qobject_cast<QQuickItem *>(o: object)) {
230 setCurrentWindow(nullptr);
231 for (QWindow *window : QGuiApplication::allWindows()) {
232 if (QQuickWindow *quickWindow = qobject_cast<QQuickWindow *>(object: window)) {
233 if (m_currentWindow != nullptr) {
234 emit error(message: QLatin1String("Multiple QQuickWindows available. We cannot "
235 "decide which one to use."));
236 return;
237 }
238 setCurrentWindow(quickWindow);
239 } else {
240 window->setVisible(false);
241 window->setFlag(Qt::WindowStaysOnTopHint, on: false);
242 }
243 }
244
245 if (m_currentWindow == nullptr) {
246 setCurrentWindow(new QQuickWindow);
247 m_createdObjects.append(t: m_currentWindow.data());
248 }
249
250 for (QQuickItem *oldItem : m_currentWindow->contentItem()->childItems())
251 oldItem->setParentItem(m_dummyItem.data());
252
253 // Special case for QQuickView, as that keeps a "root" pointer around, and uses it to
254 // automatically resize the window or the item.
255 if (QQuickView *view = qobject_cast<QQuickView *>(object: m_currentWindow))
256 QQuickViewPrivate::get(view)->setRootObject(item);
257 else
258 item->setParentItem(m_currentWindow->contentItem());
259
260 m_currentWindow->resize(newSize: item->size().toSize());
261 // used by debug translation service to get the states
262 m_currentRootItem = item;
263 } else {
264 emit error(message: QLatin1String("Created object is neither a QWindow nor a QQuickItem."));
265 }
266
267 if (m_currentWindow) {
268 m_lastPosition.initLastSavedWindowPosition(window: m_currentWindow);
269 m_currentWindow->setFlags(fixFlags(flags: m_currentWindow->flags()) | Qt::WindowStaysOnTopHint);
270 m_currentWindow->setVisible(true);
271 }
272}
273
274void QQmlPreviewHandler::setCurrentWindow(QQuickWindow *window)
275{
276 if (window == m_currentWindow.data())
277 return;
278
279 if (m_currentWindow) {
280 disconnect(sender: m_currentWindow.data(), signal: &QQuickWindow::beforeSynchronizing,
281 receiver: this, slot: &QQmlPreviewHandler::beforeSynchronizing);
282 disconnect(sender: m_currentWindow.data(), signal: &QQuickWindow::afterSynchronizing,
283 receiver: this, slot: &QQmlPreviewHandler::afterSynchronizing);
284 disconnect(sender: m_currentWindow.data(), signal: &QQuickWindow::beforeRendering,
285 receiver: this, slot: &QQmlPreviewHandler::beforeRendering);
286 disconnect(sender: m_currentWindow.data(), signal: &QQuickWindow::frameSwapped,
287 receiver: this, slot: &QQmlPreviewHandler::frameSwapped);
288 m_fpsTimer.stop();
289 m_rendering = FrameTime();
290 m_synchronizing = FrameTime();
291 }
292
293 m_currentWindow = window;
294
295 if (m_currentWindow) {
296 connect(sender: m_currentWindow.data(), signal: &QQuickWindow::beforeSynchronizing,
297 context: this, slot: &QQmlPreviewHandler::beforeSynchronizing, type: Qt::DirectConnection);
298 connect(sender: m_currentWindow.data(), signal: &QQuickWindow::afterSynchronizing,
299 context: this, slot: &QQmlPreviewHandler::afterSynchronizing, type: Qt::DirectConnection);
300 connect(sender: m_currentWindow.data(), signal: &QQuickWindow::beforeRendering,
301 context: this, slot: &QQmlPreviewHandler::beforeRendering, type: Qt::DirectConnection);
302 connect(sender: m_currentWindow.data(), signal: &QQuickWindow::frameSwapped,
303 context: this, slot: &QQmlPreviewHandler::frameSwapped, type: Qt::DirectConnection);
304 m_fpsTimer.start();
305 }
306}
307
308void QQmlPreviewHandler::beforeSynchronizing()
309{
310 m_synchronizing.beginFrame();
311}
312
313void QQmlPreviewHandler::afterSynchronizing()
314{
315
316 if (m_rendering.elapsed >= 0)
317 m_rendering.endFrame();
318 m_synchronizing.recordFrame();
319 m_synchronizing.endFrame();
320}
321
322void QQmlPreviewHandler::beforeRendering()
323{
324 m_rendering.beginFrame();
325}
326
327void QQmlPreviewHandler::frameSwapped()
328{
329 m_rendering.recordFrame();
330}
331
332void QQmlPreviewHandler::FrameTime::beginFrame()
333{
334 timer.start();
335}
336
337void QQmlPreviewHandler::FrameTime::recordFrame()
338{
339 elapsed = timer.elapsed();
340}
341
342void QQmlPreviewHandler::FrameTime::endFrame()
343{
344 if (elapsed < min)
345 min = static_cast<quint16>(qMax(a: 0ll, b: elapsed));
346 if (elapsed > max)
347 max = static_cast<quint16>(qMin(a: qint64(std::numeric_limits<quint16>::max()), b: elapsed));
348 total = static_cast<quint16>(qBound(min: 0ll, val: qint64(std::numeric_limits<quint16>::max()),
349 max: elapsed + total));
350 ++number;
351 elapsed = -1;
352}
353
354void QQmlPreviewHandler::FrameTime::reset()
355{
356 min = std::numeric_limits<quint16>::max();
357 max = 0;
358 total = 0;
359 number = 0;
360}
361
362void QQmlPreviewHandler::fpsTimerHit()
363{
364 const FpsInfo info = {
365 .numSyncs: m_synchronizing.number,
366 .minSync: m_synchronizing.min,
367 .maxSync: m_synchronizing.max,
368 .totalSync: m_synchronizing.total,
369
370 .numRenders: m_rendering.number,
371 .minRender: m_rendering.min,
372 .maxRender: m_rendering.max,
373 .totalRender: m_rendering.total
374 };
375
376 emit fps(info);
377
378 m_rendering.reset();
379 m_synchronizing.reset();
380}
381
382void QQmlPreviewHandler::tryCreateObject()
383{
384 if (!m_supportsMultipleWindows)
385 closeAllWindows();
386 QObject *object = m_component->create();
387 m_createdObjects.append(t: object);
388 showObject(object);
389}
390
391QT_END_NAMESPACE
392
393#include "moc_qqmlpreviewhandler.cpp"
394

source code of qtdeclarative/src/plugins/qmltooling/qmldbg_preview/qqmlpreviewhandler.cpp