1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the Qt Designer of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
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 General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include "objectinspectormodel_p.h" |
30 | |
31 | #include <qlayout_widget_p.h> |
32 | #include <layout_p.h> |
33 | #include <qdesigner_propertycommand_p.h> |
34 | #include <qdesigner_utils_p.h> |
35 | #include <iconloader_p.h> |
36 | |
37 | #include <QtDesigner/abstractformeditor.h> |
38 | #include <QtDesigner/abstractformwindow.h> |
39 | #include <QtDesigner/abstractwidgetdatabase.h> |
40 | #include <QtDesigner/container.h> |
41 | #include <QtDesigner/abstractmetadatabase.h> |
42 | #include <QtDesigner/qextensionmanager.h> |
43 | #include <QtWidgets/qlayout.h> |
44 | #include <QtWidgets/qaction.h> |
45 | #include <QtWidgets/qlayoutitem.h> |
46 | #include <QtWidgets/qmenu.h> |
47 | #include <QtWidgets/qbuttongroup.h> |
48 | #include <QtCore/qset.h> |
49 | #include <QtCore/qdebug.h> |
50 | #include <QtCore/qcoreapplication.h> |
51 | |
52 | #include <algorithm> |
53 | |
54 | QT_BEGIN_NAMESPACE |
55 | |
56 | namespace { |
57 | enum { DataRole = 1000 }; |
58 | } |
59 | |
60 | static inline QObject *objectOfItem(const QStandardItem *item) { |
61 | return qvariant_cast<QObject *>(v: item->data(role: DataRole)); |
62 | } |
63 | |
64 | static bool sortEntry(const QObject *a, const QObject *b) |
65 | { |
66 | return a->objectName() < b->objectName(); |
67 | } |
68 | |
69 | static bool sameIcon(const QIcon &i1, const QIcon &i2) |
70 | { |
71 | if (i1.isNull() && i2.isNull()) |
72 | return true; |
73 | if (i1.isNull() != i2.isNull()) |
74 | return false; |
75 | return i1.cacheKey() == i2.cacheKey(); |
76 | } |
77 | |
78 | static inline bool isNameColumnEditable(const QObject *) |
79 | { |
80 | return true; |
81 | } |
82 | |
83 | static qdesigner_internal::ObjectData::StandardItemList createModelRow(const QObject *o) |
84 | { |
85 | qdesigner_internal::ObjectData::StandardItemList rc; |
86 | const Qt::ItemFlags baseFlags = Qt::ItemIsSelectable|Qt::ItemIsDropEnabled|Qt::ItemIsEnabled; |
87 | for (int i = 0; i < qdesigner_internal::ObjectInspectorModel::NumColumns; i++) { |
88 | QStandardItem *item = new QStandardItem; |
89 | Qt::ItemFlags flags = baseFlags; |
90 | if (i == qdesigner_internal::ObjectInspectorModel::ObjectNameColumn && isNameColumnEditable(o)) |
91 | flags |= Qt::ItemIsEditable; |
92 | item->setFlags(flags); |
93 | rc += item; |
94 | } |
95 | return rc; |
96 | } |
97 | |
98 | static inline bool isQLayoutWidget(const QObject *o) |
99 | { |
100 | return o->metaObject() == &QLayoutWidget::staticMetaObject; |
101 | } |
102 | |
103 | namespace qdesigner_internal { |
104 | |
105 | // context kept while building a model, just there to reduce string allocations |
106 | struct ModelRecursionContext { |
107 | explicit ModelRecursionContext(QDesignerFormEditorInterface *core, const QString &sepName); |
108 | |
109 | const QString designerPrefix; |
110 | const QString separator; |
111 | |
112 | QDesignerFormEditorInterface *core; |
113 | const QDesignerWidgetDataBaseInterface *db; |
114 | const QDesignerMetaDataBaseInterface *mdb; |
115 | }; |
116 | |
117 | ModelRecursionContext::ModelRecursionContext(QDesignerFormEditorInterface *c, const QString &sepName) : |
118 | designerPrefix(QStringLiteral("QDesigner" )), |
119 | separator(sepName), |
120 | core(c), |
121 | db(c->widgetDataBase()), |
122 | mdb(c->metaDataBase()) |
123 | { |
124 | } |
125 | |
126 | // ------------ ObjectData/ ObjectModel: |
127 | // Whenever the selection changes, ObjectInspector::setFormWindow is |
128 | // called. To avoid rebuilding the tree every time (loosing expanded state) |
129 | // a model is first built from the object tree by recursion. |
130 | // As a tree is difficult to represent, a flat list of entries (ObjectData) |
131 | // containing object and parent object is used. |
132 | // ObjectData has an overloaded operator== that compares the object pointers. |
133 | // Structural changes which cause a rebuild can be detected by |
134 | // comparing the lists of ObjectData. If it is the same, only the item data (class name [changed by promotion], |
135 | // object name and icon) are checked and the existing items are updated. |
136 | |
137 | ObjectData::ObjectData() = default; |
138 | |
139 | ObjectData::ObjectData(QObject *parent, QObject *object, const ModelRecursionContext &ctx) : |
140 | m_parent(parent), |
141 | m_object(object), |
142 | m_className(QLatin1String(object->metaObject()->className())), |
143 | m_objectName(object->objectName()) |
144 | { |
145 | |
146 | // 1) set entry |
147 | if (object->isWidgetType()) { |
148 | initWidget(w: static_cast<QWidget*>(object), ctx); |
149 | } else { |
150 | initObject(ctx); |
151 | } |
152 | if (m_className.startsWith(s: ctx.designerPrefix)) |
153 | m_className.remove(i: 1, len: ctx.designerPrefix.size() - 1); |
154 | } |
155 | |
156 | void ObjectData::initObject(const ModelRecursionContext &ctx) |
157 | { |
158 | // Check objects: Action? |
159 | if (const QAction *act = qobject_cast<const QAction*>(object: m_object)) { |
160 | if (act->isSeparator()) { // separator is reserved |
161 | m_objectName = ctx.separator; |
162 | m_type = SeparatorAction; |
163 | } else { |
164 | m_type = Action; |
165 | } |
166 | m_classIcon = act->icon(); |
167 | } else { |
168 | m_type = Object; |
169 | } |
170 | } |
171 | |
172 | void ObjectData::initWidget(QWidget *w, const ModelRecursionContext &ctx) |
173 | { |
174 | // Check for extension container, QLayoutwidget, or normal container |
175 | bool isContainer = false; |
176 | if (const QDesignerWidgetDataBaseItemInterface *widgetItem = ctx.db->item(index: ctx.db->indexOfObject(object: w, resolveName: true))) { |
177 | m_classIcon = widgetItem->icon(); |
178 | m_className = widgetItem->name(); |
179 | isContainer = widgetItem->isContainer(); |
180 | } |
181 | |
182 | // We might encounter temporary states with no layouts when re-layouting. |
183 | // Just default to Widget handling for the moment. |
184 | if (isQLayoutWidget(o: w)) { |
185 | if (const QLayout *layout = w->layout()) { |
186 | m_type = LayoutWidget; |
187 | m_managedLayoutType = LayoutInfo::layoutType(core: ctx.core, layout); |
188 | m_className = QLatin1String(layout->metaObject()->className()); |
189 | m_objectName = layout->objectName(); |
190 | } |
191 | return; |
192 | } |
193 | |
194 | if (qt_extension<QDesignerContainerExtension*>(manager: ctx.core->extensionManager(), object: w)) { |
195 | m_type = ExtensionContainer; |
196 | return; |
197 | } |
198 | if (isContainer) { |
199 | m_type = LayoutableContainer; |
200 | m_managedLayoutType = LayoutInfo::managedLayoutType(core: ctx.core, w); |
201 | return; |
202 | } |
203 | m_type = ChildWidget; |
204 | } |
205 | |
206 | bool ObjectData::equals(const ObjectData & me) const |
207 | { |
208 | return m_parent == me.m_parent && m_object == me.m_object; |
209 | } |
210 | |
211 | unsigned ObjectData::compare(const ObjectData & rhs) const |
212 | { |
213 | unsigned rc = 0; |
214 | if (m_className != rhs.m_className) |
215 | rc |= ClassNameChanged; |
216 | if (m_objectName != rhs.m_objectName) |
217 | rc |= ObjectNameChanged; |
218 | if (!sameIcon(i1: m_classIcon, i2: rhs.m_classIcon)) |
219 | rc |= ClassIconChanged; |
220 | if (m_type != rhs.m_type) |
221 | rc |= TypeChanged; |
222 | if (m_managedLayoutType != rhs.m_managedLayoutType) |
223 | rc |= LayoutTypeChanged; |
224 | return rc; |
225 | } |
226 | |
227 | void ObjectData::setItemsDisplayData(const StandardItemList &row, const ObjectInspectorIcons &icons, unsigned mask) const |
228 | { |
229 | if (mask & ObjectNameChanged) |
230 | row[ObjectInspectorModel::ObjectNameColumn]->setText(m_objectName); |
231 | if (mask & ClassNameChanged) { |
232 | row[ObjectInspectorModel::ClassNameColumn]->setText(m_className); |
233 | row[ObjectInspectorModel::ClassNameColumn]->setToolTip(m_className); |
234 | } |
235 | // Set a layout icon only for containers. Note that QLayoutWidget don't have |
236 | // real class icons |
237 | if (mask & (ClassIconChanged|TypeChanged|LayoutTypeChanged)) { |
238 | switch (m_type) { |
239 | case LayoutWidget: |
240 | row[ObjectInspectorModel::ObjectNameColumn]->setIcon(icons.layoutIcons[m_managedLayoutType]); |
241 | row[ObjectInspectorModel::ClassNameColumn]->setIcon(icons.layoutIcons[m_managedLayoutType]); |
242 | break; |
243 | case LayoutableContainer: |
244 | row[ObjectInspectorModel::ObjectNameColumn]->setIcon(icons.layoutIcons[m_managedLayoutType]); |
245 | row[ObjectInspectorModel::ClassNameColumn]->setIcon(m_classIcon); |
246 | break; |
247 | default: |
248 | row[ObjectInspectorModel::ObjectNameColumn]->setIcon(QIcon()); |
249 | row[ObjectInspectorModel::ClassNameColumn]->setIcon(m_classIcon); |
250 | break; |
251 | } |
252 | } |
253 | } |
254 | |
255 | void ObjectData::setItems(const StandardItemList &row, const ObjectInspectorIcons &icons) const |
256 | { |
257 | const QVariant object = QVariant::fromValue(value: m_object); |
258 | row[ObjectInspectorModel::ObjectNameColumn]->setData(value: object, role: DataRole); |
259 | row[ObjectInspectorModel::ClassNameColumn]->setData(value: object, role: DataRole); |
260 | setItemsDisplayData(row, icons, mask: ClassNameChanged|ObjectNameChanged|ClassIconChanged|TypeChanged|LayoutTypeChanged); |
261 | } |
262 | |
263 | // Recursive routine that creates the model by traversing the form window object tree. |
264 | void createModelRecursion(const QDesignerFormWindowInterface *fwi, |
265 | QObject *parent, |
266 | QObject *object, |
267 | ObjectModel &model, |
268 | const ModelRecursionContext &ctx) |
269 | { |
270 | using ButtonGroupList = QVector<QButtonGroup *>; |
271 | // 1) Create entry |
272 | const ObjectData entry(parent, object, ctx); |
273 | model.push_back(t: entry); |
274 | |
275 | // 2) recurse over widget children via container extension or children list |
276 | const QDesignerContainerExtension *containerExtension = nullptr; |
277 | if (entry.type() == ObjectData::ExtensionContainer) { |
278 | containerExtension = qt_extension<QDesignerContainerExtension*>(manager: fwi->core()->extensionManager(), object); |
279 | Q_ASSERT(containerExtension); |
280 | const int count = containerExtension->count(); |
281 | for (int i=0; i < count; ++i) { |
282 | QObject *page = containerExtension->widget(index: i); |
283 | Q_ASSERT(page != nullptr); |
284 | createModelRecursion(fwi, parent: object, object: page, model, ctx); |
285 | } |
286 | } |
287 | |
288 | if (!object->children().isEmpty()) { |
289 | ButtonGroupList buttonGroups; |
290 | QObjectList children = object->children(); |
291 | std::sort(first: children.begin(), last: children.end(), comp: sortEntry); |
292 | for (QObject *childObject : qAsConst(t&: children)) { |
293 | // Managed child widgets unless we had a container extension |
294 | if (childObject->isWidgetType()) { |
295 | if (!containerExtension) { |
296 | QWidget *widget = qobject_cast<QWidget*>(o: childObject); |
297 | if (fwi->isManaged(widget)) |
298 | createModelRecursion(fwi, parent: object, object: widget, model, ctx); |
299 | } |
300 | } else { |
301 | if (ctx.mdb->item(object: childObject)) { |
302 | if (auto bg = qobject_cast<QButtonGroup*>(object: childObject)) |
303 | buttonGroups.push_back(t: bg); |
304 | } // Has MetaDataBase entry |
305 | } |
306 | } |
307 | // Add button groups |
308 | if (!buttonGroups.isEmpty()) { |
309 | for (QButtonGroup *group : qAsConst(t&: buttonGroups)) |
310 | createModelRecursion(fwi, parent: object, object: group, model, ctx); |
311 | } |
312 | } // has children |
313 | if (object->isWidgetType()) { |
314 | // Add actions |
315 | const auto actions = static_cast<QWidget*>(object)->actions(); |
316 | for (QAction *action : actions) { |
317 | if (ctx.mdb->item(object: action)) { |
318 | QObject *childObject = action; |
319 | if (auto = action->menu()) |
320 | childObject = menu; |
321 | createModelRecursion(fwi, parent: object, object: childObject, model, ctx); |
322 | } |
323 | } |
324 | } |
325 | } |
326 | |
327 | // ------------ ObjectInspectorModel |
328 | ObjectInspectorModel::ObjectInspectorModel(QObject *parent) : |
329 | QStandardItemModel(0, NumColumns, parent) |
330 | { |
331 | QStringList ; |
332 | headers += QCoreApplication::translate(context: "ObjectInspectorModel" , key: "Object" ); |
333 | headers += QCoreApplication::translate(context: "ObjectInspectorModel" , key: "Class" ); |
334 | Q_ASSERT(headers.size() == NumColumns); |
335 | setColumnCount(NumColumns); |
336 | setHorizontalHeaderLabels(headers); |
337 | // Icons |
338 | m_icons.layoutIcons[LayoutInfo::NoLayout] = createIconSet(QStringLiteral("editbreaklayout.png" )); |
339 | m_icons.layoutIcons[LayoutInfo::HSplitter] = createIconSet(QStringLiteral("edithlayoutsplit.png" )); |
340 | m_icons.layoutIcons[LayoutInfo::VSplitter] = createIconSet(QStringLiteral("editvlayoutsplit.png" )); |
341 | m_icons.layoutIcons[LayoutInfo::HBox] = createIconSet(QStringLiteral("edithlayout.png" )); |
342 | m_icons.layoutIcons[LayoutInfo::VBox] = createIconSet(QStringLiteral("editvlayout.png" )); |
343 | m_icons.layoutIcons[LayoutInfo::Grid] = createIconSet(QStringLiteral("editgrid.png" )); |
344 | m_icons.layoutIcons[LayoutInfo::Form] = createIconSet(QStringLiteral("editform.png" )); |
345 | } |
346 | |
347 | void ObjectInspectorModel::clearItems() |
348 | { |
349 | beginResetModel(); |
350 | m_objectIndexMultiMap.clear(); |
351 | m_model.clear(); |
352 | endResetModel(); // force editors to be closed in views |
353 | removeRow(arow: 0); |
354 | } |
355 | |
356 | ObjectInspectorModel::UpdateResult ObjectInspectorModel::update(QDesignerFormWindowInterface *fw) |
357 | { |
358 | QWidget *mainContainer = fw ? fw->mainContainer() : nullptr; |
359 | if (!mainContainer) { |
360 | clearItems(); |
361 | m_formWindow = nullptr; |
362 | return NoForm; |
363 | } |
364 | m_formWindow = fw; |
365 | // Build new model and compare to previous one. If the structure is |
366 | // identical, just update, else rebuild |
367 | ObjectModel newModel; |
368 | |
369 | static const QString separator = QCoreApplication::translate(context: "ObjectInspectorModel" , key: "separator" ); |
370 | const ModelRecursionContext ctx(fw->core(), separator); |
371 | createModelRecursion(fwi: fw, parent: nullptr, object: mainContainer, model&: newModel, ctx); |
372 | |
373 | if (newModel == m_model) { |
374 | updateItemContents(oldModel&: m_model, newModel); |
375 | return Updated; |
376 | } |
377 | |
378 | rebuild(newModel); |
379 | m_model = newModel; |
380 | return Rebuilt; |
381 | } |
382 | |
383 | QObject *ObjectInspectorModel::objectAt(const QModelIndex &index) const |
384 | { |
385 | if (index.isValid()) |
386 | if (const QStandardItem *item = itemFromIndex(index)) |
387 | return objectOfItem(item); |
388 | return nullptr; |
389 | } |
390 | |
391 | // Missing Qt API: get a row |
392 | ObjectInspectorModel::StandardItemList ObjectInspectorModel::rowAt(QModelIndex index) const |
393 | { |
394 | StandardItemList rc; |
395 | while (true) { |
396 | rc += itemFromIndex(index); |
397 | const int nextColumn = index.column() + 1; |
398 | if (nextColumn >= NumColumns) |
399 | break; |
400 | index = index.sibling(arow: index.row(), acolumn: nextColumn); |
401 | } |
402 | return rc; |
403 | } |
404 | |
405 | // Rebuild the tree in case the model has completely changed. |
406 | void ObjectInspectorModel::rebuild(const ObjectModel &newModel) |
407 | { |
408 | clearItems(); |
409 | if (newModel.isEmpty()) |
410 | return; |
411 | |
412 | const ObjectModel::const_iterator mcend = newModel.constEnd(); |
413 | ObjectModel::const_iterator it = newModel.constBegin(); |
414 | // Set up root element |
415 | StandardItemList rootRow = createModelRow(o: it->object()); |
416 | it->setItems(row: rootRow, icons: m_icons); |
417 | appendRow(items: rootRow); |
418 | m_objectIndexMultiMap.insert(akey: it->object(), avalue: indexFromItem(item: rootRow.constFirst())); |
419 | for (++it; it != mcend; ++it) { |
420 | // Add to parent item, found via map |
421 | const QModelIndex parentIndex = m_objectIndexMultiMap.value(akey: it->parent(), adefaultValue: QModelIndex()); |
422 | Q_ASSERT(parentIndex.isValid()); |
423 | QStandardItem *parentItem = itemFromIndex(index: parentIndex); |
424 | StandardItemList row = createModelRow(o: it->object()); |
425 | it->setItems(row, icons: m_icons); |
426 | parentItem->appendRow(aitems: row); |
427 | m_objectIndexMultiMap.insert(akey: it->object(), avalue: indexFromItem(item: row.constFirst())); |
428 | } |
429 | } |
430 | |
431 | // Update item data in case the model has the same structure |
432 | void ObjectInspectorModel::updateItemContents(ObjectModel &oldModel, const ObjectModel &newModel) |
433 | { |
434 | // Change text and icon. Keep a set of changed object |
435 | // as for example actions might occur several times in the tree. |
436 | using QObjectSet = QSet<QObject *>; |
437 | |
438 | QObjectSet changedObjects; |
439 | |
440 | const int size = newModel.size(); |
441 | Q_ASSERT(oldModel.size() == size); |
442 | for (int i = 0; i < size; i++) { |
443 | const ObjectData &newEntry = newModel[i]; |
444 | ObjectData &entry = oldModel[i]; |
445 | // Has some data changed? |
446 | if (const unsigned changedMask = entry.compare(rhs: newEntry)) { |
447 | entry = newEntry; |
448 | QObject * o = entry.object(); |
449 | if (!changedObjects.contains(value: o)) { |
450 | changedObjects.insert(value: o); |
451 | const QModelIndexList indexes = m_objectIndexMultiMap.values(akey: o); |
452 | for (const QModelIndex &index : indexes) |
453 | entry.setItemsDisplayData(row: rowAt(index), icons: m_icons, mask: changedMask); |
454 | } |
455 | } |
456 | } |
457 | } |
458 | |
459 | QVariant ObjectInspectorModel::data(const QModelIndex &index, int role) const |
460 | { |
461 | const QVariant rc = QStandardItemModel::data(index, role); |
462 | // Return <noname> if the string is empty for the display role |
463 | // only (else, editing starts with <noname>). |
464 | if (role == Qt::DisplayRole && rc.type() == QVariant::String) { |
465 | const QString s = rc.toString(); |
466 | if (s.isEmpty()) { |
467 | static const QString noName = QCoreApplication::translate(context: "ObjectInspectorModel" , key: "<noname>" ); |
468 | return QVariant(noName); |
469 | } |
470 | } |
471 | return rc; |
472 | } |
473 | |
474 | bool ObjectInspectorModel::setData(const QModelIndex &index, const QVariant &value, int role) |
475 | { |
476 | if (role != Qt::EditRole || !m_formWindow) |
477 | return false; |
478 | |
479 | QObject *object = objectAt(index); |
480 | if (!object) |
481 | return false; |
482 | // Is this a layout widget? |
483 | const QString nameProperty = isQLayoutWidget(o: object) ? QStringLiteral("layoutName" ) : QStringLiteral("objectName" ); |
484 | m_formWindow->commandHistory()->push(cmd: createTextPropertyCommand(propertyName: nameProperty, text: value.toString(), object, fw: m_formWindow)); |
485 | return true; |
486 | } |
487 | } |
488 | |
489 | QT_END_NAMESPACE |
490 | |