1 /*
2   quickitemmodel.cpp
3 
4   This file is part of GammaRay, the Qt application inspection and
5   manipulation tool.
6 
7   Copyright (C) 2014-2021 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
8   Author: Volker Krause <volker.krause@kdab.com>
9 
10   Licensees holding valid commercial KDAB GammaRay licenses may use this file in
11   accordance with GammaRay Commercial License Agreement provided with the Software.
12 
13   Contact info@kdab.com if any conditions of this licensing are not clear to you.
14 
15   This program is free software; you can redistribute it and/or modify
16   it under the terms of the GNU General Public License as published by
17   the Free Software Foundation, either version 2 of the License, or
18   (at your option) any later version.
19 
20   This program is distributed in the hope that it will be useful,
21   but WITHOUT ANY WARRANTY; without even the implied warranty of
22   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23   GNU General Public License for more details.
24 
25   You should have received a copy of the GNU General Public License
26   along with this program.  If not, see <http://www.gnu.org/licenses/>.
27 */
28 
29 #include "quickitemmodel.h"
30 #include "quickitemmodelroles.h"
31 
32 #include <core/paintanalyzer.h>
33 #include <core/probe.h>
34 
35 #include <QQuickItem>
36 #include <QQuickWindow>
37 #include <QQuickPaintedItem>
38 #include <QThread>
39 #include <QQmlEngine>
40 #include <QQmlContext>
41 #include <QEvent>
42 
43 #include <algorithm>
44 
45 using namespace GammaRay;
46 
QuickItemModel(QObject * parent)47 QuickItemModel::QuickItemModel(QObject *parent)
48     : ObjectModelBase<QAbstractItemModel>(parent)
49     , m_dataChangeTimer(new QTimer(this))
50 {
51     m_clickEventFilter = new QuickEventMonitor(this);
52 
53     m_dataChangeTimer->setSingleShot(true);
54     m_dataChangeTimer->setInterval(500);
55     connect(m_dataChangeTimer, &QTimer::timeout, this, &QuickItemModel::emitPendingDataChanges);
56 }
57 
58 QuickItemModel::~QuickItemModel() = default;
59 
setWindow(QQuickWindow * window)60 void QuickItemModel::setWindow(QQuickWindow *window)
61 {
62     beginResetModel();
63     clear();
64     m_window = window;
65     populateFromItem(window->contentItem());
66     endResetModel();
67 }
68 
data(const QModelIndex & index,int role) const69 QVariant QuickItemModel::data(const QModelIndex &index, int role) const
70 {
71     if (!index.isValid())
72         return QVariant();
73 
74     QQuickItem *item = reinterpret_cast<QQuickItem *>(index.internalPointer());
75 
76     if (role == QuickItemModelRole::ItemFlags)
77         return m_itemFlags[item];
78     if (role == ObjectModel::ObjectIdRole)
79         return QVariant::fromValue(ObjectId(item));
80 
81     return dataForObject(item, index, role);
82 }
83 
rowCount(const QModelIndex & parent) const84 int QuickItemModel::rowCount(const QModelIndex &parent) const
85 {
86     if (parent.column() == 1)
87         return 0;
88 
89     QQuickItem *parentItem = reinterpret_cast<QQuickItem *>(parent.internalPointer());
90 
91     return m_parentChildMap.value(parentItem).size();
92 }
93 
parent(const QModelIndex & child) const94 QModelIndex QuickItemModel::parent(const QModelIndex &child) const
95 {
96     QQuickItem *childItem = reinterpret_cast<QQuickItem *>(child.internalPointer());
97     return indexForItem(m_childParentMap.value(childItem));
98 }
99 
index(int row,int column,const QModelIndex & parent) const100 QModelIndex QuickItemModel::index(int row, int column, const QModelIndex &parent) const
101 {
102     QQuickItem *parentItem = reinterpret_cast<QQuickItem *>(parent.internalPointer());
103     const QVector<QQuickItem *> children = m_parentChildMap.value(parentItem);
104     if (row < 0 || column < 0 || row >= children.size() || column >= columnCount())
105         return {};
106     return createIndex(row, column, children.at(row));
107 }
108 
itemData(const QModelIndex & index) const109 QMap<int, QVariant> QuickItemModel::itemData(const QModelIndex &index) const
110 {
111     QMap<int, QVariant> d = ObjectModelBase<QAbstractItemModel>::itemData(index);
112     d.insert(QuickItemModelRole::ItemFlags, data(index, QuickItemModelRole::ItemFlags));
113     d.insert(QuickItemModelRole::ItemActions, data(index, QuickItemModelRole::ItemActions));
114     return d;
115 }
116 
clear()117 void QuickItemModel::clear()
118 {
119     for (auto it = m_childParentMap.constBegin(); it != m_childParentMap.constEnd(); ++it)
120         disconnect(it.key(), nullptr, this, nullptr);
121     m_childParentMap.clear();
122     m_parentChildMap.clear();
123 }
124 
populateFromItem(QQuickItem * item)125 void QuickItemModel::populateFromItem(QQuickItem *item)
126 {
127     if (!item)
128         return;
129 
130     connectItem(item);
131     updateItemFlags(item);
132     m_childParentMap[item] = item->parentItem();
133     m_parentChildMap[item->parentItem()].push_back(item);
134 
135     foreach (QQuickItem *child, item->childItems())
136         populateFromItem(child);
137 
138     QVector<QQuickItem *> &children = m_parentChildMap[item->parentItem()];
139     std::sort(children.begin(), children.end());
140 
141     if (Probe::instance()) {
142         // Make sure every items are known to the objects model as this is not always
143         // the case when attaching to running process (OSX)
144         Probe::instance()->discoverObject(item);
145     }
146 }
147 
connectItem(QQuickItem * item)148 void QuickItemModel::connectItem(QQuickItem *item)
149 {
150     Q_ASSERT(item);
151     auto itemUpdatedFunc = [this, item]() { itemUpdated(item); };
152     std::array<QMetaObject::Connection, 8> connections = {{
153         connect(item, &QQuickItem::parentChanged, this, [this, item]() { itemReparented(item); }),
154         connect(item, &QQuickItem::visibleChanged, this, itemUpdatedFunc),
155         connect(item, &QQuickItem::focusChanged, this, itemUpdatedFunc),
156         connect(item, &QQuickItem::activeFocusChanged, this, itemUpdatedFunc),
157         connect(item, &QQuickItem::widthChanged, this, itemUpdatedFunc),
158         connect(item, &QQuickItem::heightChanged, this, itemUpdatedFunc),
159         connect(item, &QQuickItem::xChanged, this, itemUpdatedFunc),
160         connect(item, &QQuickItem::yChanged, this, itemUpdatedFunc)
161     }};
162     m_itemConnections.emplace(std::make_pair(item, std::move(connections))); // can't construct in-place, fails to compile under MSVC2010 :(
163 
164     item->installEventFilter(m_clickEventFilter);
165 }
166 
disconnectItem(QQuickItem * item)167 void QuickItemModel::disconnectItem(QQuickItem *item)
168 {
169     Q_ASSERT(item);
170     auto it = m_itemConnections.find(item);
171     if (it != m_itemConnections.end()) {
172         for (const auto &connection : it->second) {
173             disconnect(connection);
174         }
175         m_itemConnections.erase(it);
176     }
177     item->removeEventFilter(m_clickEventFilter);
178 }
179 
indexForItem(QQuickItem * item) const180 QModelIndex QuickItemModel::indexForItem(QQuickItem *item) const
181 {
182     if (!item)
183         return {};
184 
185     QQuickItem *parent = m_childParentMap.value(item);
186     const QVector<QQuickItem *> &siblings = m_parentChildMap[parent];
187     const auto it = std::lower_bound(siblings.constBegin(), siblings.constEnd(), item);
188     if (it == siblings.constEnd() || *it != item)
189         return QModelIndex();
190 
191     const int row = std::distance(siblings.constBegin(), it);
192     return createIndex(row, 0, item);
193 }
194 
objectAdded(QObject * obj)195 void QuickItemModel::objectAdded(QObject *obj)
196 {
197     Q_ASSERT(thread() == QThread::currentThread());
198     QQuickItem *item = qobject_cast<QQuickItem *>(obj);
199     if (!item)
200         return;
201 
202     // detect if item is added to scene later
203     connect(item, &QQuickItem::windowChanged, this, [this, item]() { itemWindowChanged(item); });
204 
205     addItem(item);
206 }
207 
addItem(QQuickItem * item)208 void QuickItemModel::addItem(QQuickItem *item)
209 {
210     Q_ASSERT(item);
211 
212     if (!item->window())
213         return; // item not (yet) added to a scene
214 
215     if (item->window() != m_window)
216         return; // item for a different scene
217 
218     if (m_childParentMap.contains(item))
219         return; // already known
220 
221     QQuickItem *parentItem = item->parentItem();
222     if (parentItem) {
223         // add parent first, if we don't know that yet
224         if (!m_childParentMap.contains(parentItem))
225             objectAdded(parentItem);
226     }
227 
228     connectItem(item);
229 
230     const QModelIndex index = indexForItem(parentItem);
231     if (!index.isValid() && parentItem)
232         return;
233 
234     QVector<QQuickItem *> &children = m_parentChildMap[parentItem];
235     auto it = std::lower_bound(children.begin(), children.end(), item);
236     const int row = std::distance(children.begin(), it);
237 
238     beginInsertRows(index, row, row);
239     children.insert(it, item);
240     m_childParentMap.insert(item, parentItem);
241     endInsertRows();
242 }
243 
objectRemoved(QObject * obj)244 void QuickItemModel::objectRemoved(QObject *obj)
245 {
246     Q_ASSERT(thread() == QThread::currentThread());
247     QQuickItem *item = static_cast<QQuickItem *>(obj); // this is fine, we must not deref
248                                                        // obj/item at this point anyway
249     removeItem(item, true);
250 }
251 
removeItem(QQuickItem * item,bool danglingPointer)252 void QuickItemModel::removeItem(QQuickItem *item, bool danglingPointer)
253 {
254     if (!m_childParentMap.contains(item)) { // not an item of our current scene
255         Q_ASSERT(!m_parentChildMap.contains(item));
256         return;
257     }
258 
259     if (item && !danglingPointer)
260         disconnectItem(item);
261 
262     QQuickItem *parentItem = m_childParentMap.value(item);
263     const QModelIndex parentIndex = indexForItem(parentItem);
264     if (parentItem && !parentIndex.isValid())
265         return;
266 
267     QVector<QQuickItem *> &siblings = m_parentChildMap[parentItem];
268     auto it = std::lower_bound(siblings.begin(), siblings.end(), item);
269     if (it == siblings.end() || *it != item)
270         return;
271     const int row = std::distance(siblings.begin(), it);
272 
273     beginRemoveRows(parentIndex, row, row);
274 
275     siblings.erase(it);
276     doRemoveSubtree(item, danglingPointer);
277 
278     endRemoveRows();
279 }
280 
doRemoveSubtree(QQuickItem * item,bool danglingPointer)281 void QuickItemModel::doRemoveSubtree(QQuickItem *item, bool danglingPointer)
282 {
283     m_childParentMap.remove(item);
284     m_parentChildMap.remove(item);
285     if (!danglingPointer) {
286         foreach (QQuickItem *child, item->childItems())
287             doRemoveSubtree(child, false);
288     }
289 }
290 
itemReparented(QQuickItem * item)291 void QuickItemModel::itemReparented(QQuickItem *item)
292 {
293     Q_ASSERT(item);
294     if (!item->parentItem()) { // Item was not deleted, but removed from the scene.
295         removeItem(item, false);
296         return;
297     }
298 
299     Q_ASSERT(item && item->window() == m_window);
300 
301     QQuickItem *sourceParent = m_childParentMap.value(item);
302     Q_ASSERT(sourceParent);
303     if (sourceParent == item->parentItem())
304         return;
305 
306     const QModelIndex sourceParentIndex = indexForItem(sourceParent);
307 
308     auto &sourceSiblings = m_parentChildMap[sourceParent];
309     auto sit = std::lower_bound(sourceSiblings.begin(), sourceSiblings.end(), item);
310     Q_ASSERT(sit != sourceSiblings.end() && *sit == item);
311     const int sourceRow = std::distance(sourceSiblings.begin(), sit);
312 
313     QQuickItem *destParent = item->parentItem();
314     Q_ASSERT(destParent);
315     const QModelIndex destParentIndex = indexForItem(destParent);
316     if (!destParentIndex.isValid()) { // item was moved to a parent we don't know (yet?)
317         removeItem(item, false);
318         return;
319     }
320 
321     QVector<QQuickItem *> &destSiblings = m_parentChildMap[destParent];
322     auto dit = std::lower_bound(destSiblings.begin(), destSiblings.end(), item);
323     const int destRow = std::distance(destSiblings.begin(), dit);
324 
325     // as of Qt 5.10, QSFPM translates moves into layout changes, which is way worse for us than remove/insert
326 #if 0
327     beginMoveRows(sourceParentIndex, sourceRow, sourceRow, destParentIndex, destRow);
328     destSiblings.insert(dit, item);
329     sourceSiblings.erase(sit);
330     m_childParentMap.insert(item, destParent);
331     endMoveRows();
332 #else
333     beginRemoveRows(sourceParentIndex, sourceRow, sourceRow);
334     sourceSiblings.erase(sit);
335     m_childParentMap.remove(item);
336     endRemoveRows();
337     beginInsertRows(destParentIndex, destRow, destRow);
338     destSiblings.insert(dit, item);
339     m_childParentMap.insert(item, destParent);
340     endInsertRows();
341 #endif
342 }
343 
itemWindowChanged(QQuickItem * item)344 void QuickItemModel::itemWindowChanged(QQuickItem *item)
345 {
346     Q_ASSERT(item);
347     if (!item->window() || item->window() != m_window)
348         removeItem(item);
349     else if (m_window && item->window() == m_window)
350         addItem(item);
351 }
352 
itemUpdated(QQuickItem * item)353 void QuickItemModel::itemUpdated(QQuickItem *item)
354 {
355     Q_ASSERT(item);
356     recursivelyUpdateItem(item);
357 }
358 
recursivelyUpdateItem(QQuickItem * item)359 void QuickItemModel::recursivelyUpdateItem(QQuickItem *item)
360 {
361     Q_ASSERT(item);
362     if (item->parent() == QObject::parent()) // skip items injected by ourselves
363         return;
364 
365     int oldFlags = m_itemFlags.value(item);
366     updateItemFlags(item);
367 
368     if (oldFlags != m_itemFlags.value(item))
369         updateItem(item, QuickItemModelRole::ItemFlags);
370 
371     foreach (QQuickItem *child, item->childItems())
372         recursivelyUpdateItem(child);
373 }
374 
updateItem(QQuickItem * item,int role)375 void QuickItemModel::updateItem(QQuickItem *item, int role)
376 {
377     if (!item || item->window() != m_window)
378         return;
379 
380     auto it = std::lower_bound(m_pendingDataChanges.begin(), m_pendingDataChanges.end(), item);
381     if (it == m_pendingDataChanges.end() || (*it).item != item) {
382         PendingDataChange c;
383         c.item = item;
384         it = m_pendingDataChanges.insert(it, c);
385     }
386 
387     if (role == QuickItemModelRole::ItemEvent)
388         (*it).eventChange = true;
389     if (role == QuickItemModelRole::ItemFlags)
390         (*it).flagChange = true;
391 
392     if (!m_dataChangeTimer->isActive())
393         m_dataChangeTimer->start();
394 }
395 
updateItemFlags(QQuickItem * item)396 void QuickItemModel::updateItemFlags(QQuickItem *item)
397 {
398     QQuickItem *ancestor = item->parentItem();
399     bool outOfView = false;
400     bool partiallyOutOfView = false;
401 
402     auto rect = item->mapRectToScene(QRectF(0, 0, item->width(), item->height()));
403 
404     if (item->isVisible()) {
405         while (ancestor && ancestor != m_window->contentItem()) {
406             if (ancestor->parentItem() == m_window->contentItem() || ancestor->clip()) {
407                 auto ancestorRect = ancestor->mapRectToScene(QRectF(0, 0, ancestor->width(), ancestor->height()));
408 
409                 partiallyOutOfView |= !ancestorRect.contains(rect);
410                 outOfView = partiallyOutOfView && !rect.intersects(ancestorRect);
411 
412                 if (outOfView) {
413                     break;
414                 }
415             }
416             ancestor = ancestor->parentItem();
417         }
418     }
419 
420     m_itemFlags[item] = (!item->isVisible() || item->opacity() == 0
421                          ? QuickItemModelRole::Invisible : QuickItemModelRole::None)
422                         |(item->width() == 0 || item->height() == 0
423                           ? QuickItemModelRole::ZeroSize : QuickItemModelRole::None)
424                         |(partiallyOutOfView
425                           ? QuickItemModelRole::PartiallyOutOfView : QuickItemModelRole::None)
426                         |(outOfView
427                           ? QuickItemModelRole::OutOfView : QuickItemModelRole::None)
428                         |(item->hasFocus()
429                           ? QuickItemModelRole::HasFocus : QuickItemModelRole::None)
430                         |(item->hasActiveFocus()
431                           ? QuickItemModelRole::HasActiveFocus : QuickItemModelRole::None);
432 }
433 
QuickEventMonitor(QuickItemModel * parent)434 QuickEventMonitor::QuickEventMonitor(QuickItemModel *parent)
435     : QObject(parent)
436     , m_model(parent)
437 {
438 }
439 
eventFilter(QObject * obj,QEvent * event)440 bool QuickEventMonitor::eventFilter(QObject *obj, QEvent *event)
441 {
442     switch (event->type()) {
443         // exclude some unsafe event types
444         case QEvent::DeferredDelete:
445         case QEvent::Destroy:
446 
447         // exclude some event types which occur far too often and thus cost us bandwidth
448         case QEvent::HoverMove:
449         case QEvent::MouseMove:
450         case QEvent::TouchUpdate:
451         case QEvent::Wheel: // due to high frequency creation from touch events
452 
453         // exclude event types that are unrelated to user interaction
454         case QEvent::MetaCall:
455         case QEvent::ChildAdded:
456         case QEvent::ChildPolished:
457         case QEvent::ChildRemoved:
458         case QEvent::Timer:
459 
460             return false;
461 
462         default:
463             break;
464     }
465 
466     m_model->updateItem(qobject_cast<QQuickItem *>(obj), QuickItemModelRole::ItemEvent);
467     return false;
468 }
469 
emitPendingDataChanges()470 void QuickItemModel::emitPendingDataChanges()
471 {
472     QVector<int> roles;
473     roles.reserve(2);
474 
475     for (const auto &change : m_pendingDataChanges) {
476         const auto left = indexForItem(change.item);
477         if (!left.isValid()) {
478             continue;
479         }
480         const auto right = left.sibling(left.row(), columnCount() - 1);
481         Q_ASSERT(left.isValid());
482         Q_ASSERT(right.isValid());
483 
484         roles.clear();
485         if (change.eventChange) {
486             roles.push_back(QuickItemModelRole::ItemEvent);
487         }
488         if (change.flagChange) {
489             roles.push_back(QuickItemModelRole::ItemFlags);
490         }
491         emit dataChanged(left, right, roles);
492     }
493 
494     m_pendingDataChanges.clear();
495 }
496