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

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