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 | |
57 | QT_BEGIN_NAMESPACE |
58 | |
59 | struct 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 | |
74 | QQmlPreviewHandler::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 | |
94 | QQmlPreviewHandler::~QQmlPreviewHandler() |
95 | { |
96 | #if QT_CONFIG(translation) |
97 | removeTranslators(); |
98 | #endif |
99 | clear(); |
100 | } |
101 | |
102 | static void closeAllWindows() |
103 | { |
104 | const QWindowList windows = QGuiApplication::allWindows(); |
105 | for (QWindow *window : windows) |
106 | window->close(); |
107 | } |
108 | |
109 | bool 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 | |
119 | void QQmlPreviewHandler::addEngine(QQmlEngine *qmlEngine) |
120 | { |
121 | m_engines.append(t: qmlEngine); |
122 | } |
123 | |
124 | void 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 | |
134 | void 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 | |
181 | void 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 | |
192 | void QQmlPreviewHandler::zoom(qreal newFactor) |
193 | { |
194 | m_zoomFactor = newFactor; |
195 | QTimer::singleShot(interval: 0, receiver: this, slot: &QQmlPreviewHandler::doZoom); |
196 | } |
197 | |
198 | void 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) |
229 | void 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 | |
242 | void 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 | |
263 | void QQmlPreviewHandler::clear() |
264 | { |
265 | qDeleteAll(c: m_createdObjects); |
266 | m_createdObjects.clear(); |
267 | setCurrentWindow(nullptr); |
268 | } |
269 | |
270 | Qt::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 | |
286 | void 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 | |
341 | void 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 | |
375 | void QQmlPreviewHandler::beforeSynchronizing() |
376 | { |
377 | m_synchronizing.beginFrame(); |
378 | } |
379 | |
380 | void QQmlPreviewHandler::afterSynchronizing() |
381 | { |
382 | |
383 | if (m_rendering.elapsed >= 0) |
384 | m_rendering.endFrame(); |
385 | m_synchronizing.recordFrame(); |
386 | m_synchronizing.endFrame(); |
387 | } |
388 | |
389 | void QQmlPreviewHandler::beforeRendering() |
390 | { |
391 | m_rendering.beginFrame(); |
392 | } |
393 | |
394 | void QQmlPreviewHandler::frameSwapped() |
395 | { |
396 | m_rendering.recordFrame(); |
397 | } |
398 | |
399 | void QQmlPreviewHandler::FrameTime::beginFrame() |
400 | { |
401 | timer.start(); |
402 | } |
403 | |
404 | void QQmlPreviewHandler::FrameTime::recordFrame() |
405 | { |
406 | elapsed = timer.elapsed(); |
407 | } |
408 | |
409 | void 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 | |
421 | void QQmlPreviewHandler::FrameTime::reset() |
422 | { |
423 | min = std::numeric_limits<quint16>::max(); |
424 | max = 0; |
425 | total = 0; |
426 | number = 0; |
427 | } |
428 | |
429 | void 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 | |
449 | void 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 | |
458 | QT_END_NAMESPACE |
459 | |