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
54QT_BEGIN_NAMESPACE
55
56namespace {
57 enum { DataRole = 1000 };
58}
59
60static inline QObject *objectOfItem(const QStandardItem *item) {
61 return qvariant_cast<QObject *>(v: item->data(role: DataRole));
62}
63
64static bool sortEntry(const QObject *a, const QObject *b)
65{
66 return a->objectName() < b->objectName();
67}
68
69static 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
78static inline bool isNameColumnEditable(const QObject *)
79{
80 return true;
81}
82
83static 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
98static inline bool isQLayoutWidget(const QObject *o)
99{
100 return o->metaObject() == &QLayoutWidget::staticMetaObject;
101}
102
103namespace 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 menu = 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 headers;
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
489QT_END_NAMESPACE
490

source code of qttools/src/designer/src/components/objectinspector/objectinspectormodel.cpp