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 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | struct 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 | |
38 | QQmlPreviewHandler::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 | |
58 | QQmlPreviewHandler::~QQmlPreviewHandler() |
59 | { |
60 | clear(); |
61 | } |
62 | |
63 | static void closeAllWindows() |
64 | { |
65 | const QWindowList windows = QGuiApplication::allWindows(); |
66 | for (QWindow *window : windows) |
67 | window->close(); |
68 | } |
69 | |
70 | bool 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 | |
80 | QQuickItem *QQmlPreviewHandler::currentRootItem() |
81 | { |
82 | return m_currentRootItem; |
83 | } |
84 | |
85 | void QQmlPreviewHandler::addEngine(QQmlEngine *qmlEngine) |
86 | { |
87 | m_engines.append(t: qmlEngine); |
88 | } |
89 | |
90 | void 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 | |
100 | void 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 | |
147 | void 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 | |
158 | void QQmlPreviewHandler::zoom(qreal newFactor) |
159 | { |
160 | m_zoomFactor = newFactor; |
161 | QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlPreviewHandler::doZoom); |
162 | } |
163 | |
164 | void 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 | |
194 | void QQmlPreviewHandler::clear() |
195 | { |
196 | qDeleteAll(c: m_createdObjects); |
197 | m_createdObjects.clear(); |
198 | setCurrentWindow(nullptr); |
199 | } |
200 | |
201 | Qt::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 | |
217 | void 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 | |
274 | void 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 | |
308 | void QQmlPreviewHandler::beforeSynchronizing() |
309 | { |
310 | m_synchronizing.beginFrame(); |
311 | } |
312 | |
313 | void QQmlPreviewHandler::afterSynchronizing() |
314 | { |
315 | |
316 | if (m_rendering.elapsed >= 0) |
317 | m_rendering.endFrame(); |
318 | m_synchronizing.recordFrame(); |
319 | m_synchronizing.endFrame(); |
320 | } |
321 | |
322 | void QQmlPreviewHandler::beforeRendering() |
323 | { |
324 | m_rendering.beginFrame(); |
325 | } |
326 | |
327 | void QQmlPreviewHandler::frameSwapped() |
328 | { |
329 | m_rendering.recordFrame(); |
330 | } |
331 | |
332 | void QQmlPreviewHandler::FrameTime::beginFrame() |
333 | { |
334 | timer.start(); |
335 | } |
336 | |
337 | void QQmlPreviewHandler::FrameTime::recordFrame() |
338 | { |
339 | elapsed = timer.elapsed(); |
340 | } |
341 | |
342 | void 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 | |
354 | void QQmlPreviewHandler::FrameTime::reset() |
355 | { |
356 | min = std::numeric_limits<quint16>::max(); |
357 | max = 0; |
358 | total = 0; |
359 | number = 0; |
360 | } |
361 | |
362 | void 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 | |
382 | void 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 | |
391 | QT_END_NAMESPACE |
392 | |
393 | #include "moc_qqmlpreviewhandler.cpp" |
394 | |