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

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