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 "widgetboxcategorylistview.h"
30
31#include <QtDesigner/abstractformeditor.h>
32#include <QtDesigner/abstractwidgetdatabase.h>
33
34#include <QtXml/qdom.h>
35
36#include <QtGui/qicon.h>
37#include <QtGui/qvalidator.h>
38#include <QtWidgets/qlistview.h>
39#include <QtWidgets/qlineedit.h>
40#include <QtWidgets/qitemdelegate.h>
41#include <QtCore/qsortfilterproxymodel.h>
42
43#include <QtCore/qabstractitemmodel.h>
44#include <QtCore/qlist.h>
45#include <QtCore/qtextstream.h>
46#include <QtCore/qregularexpression.h>
47
48static const char *widgetElementC = "widget";
49static const char *nameAttributeC = "name";
50static const char *uiOpeningTagC = "<ui>";
51static const char *uiClosingTagC = "</ui>";
52
53QT_BEGIN_NAMESPACE
54
55enum { FilterRole = Qt::UserRole + 11 };
56
57static QString domToString(const QDomElement &elt)
58{
59 QString result;
60 QTextStream stream(&result, QIODevice::WriteOnly);
61 elt.save(stream, 2);
62 stream.flush();
63 return result;
64}
65
66static QDomDocument stringToDom(const QString &xml)
67{
68 QDomDocument result;
69 result.setContent(text: xml);
70 return result;
71}
72
73namespace qdesigner_internal {
74
75// Entry of the model list
76
77struct WidgetBoxCategoryEntry {
78 WidgetBoxCategoryEntry() = default;
79 explicit WidgetBoxCategoryEntry(const QDesignerWidgetBoxInterface::Widget &widget,
80 const QString &filter,
81 const QIcon &icon,
82 bool editable);
83
84 QDesignerWidgetBoxInterface::Widget widget;
85 QString toolTip;
86 QString whatsThis;
87 QString filter;
88 QIcon icon;
89 bool editable{false};
90};
91
92WidgetBoxCategoryEntry::WidgetBoxCategoryEntry(const QDesignerWidgetBoxInterface::Widget &w,
93 const QString &filterIn,
94 const QIcon &i, bool e) :
95 widget(w),
96 filter(filterIn),
97 icon(i),
98 editable(e)
99{
100}
101
102/* WidgetBoxCategoryModel, representing a list of category entries. Uses a
103 * QAbstractListModel since the behaviour depends on the view mode of the list
104 * view, it does not return text in the case of IconMode. */
105
106class WidgetBoxCategoryModel : public QAbstractListModel {
107public:
108 explicit WidgetBoxCategoryModel(QDesignerFormEditorInterface *core, QObject *parent = nullptr);
109
110 // QAbstractListModel
111 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
112 int rowCount(const QModelIndex &parent = QModelIndex()) const override;
113 bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole) override;
114 Qt::ItemFlags flags (const QModelIndex & index ) const override;
115 bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
116
117 // The model returns no text in icon mode, so, it also needs to know it
118 QListView::ViewMode viewMode() const;
119 void setViewMode(QListView::ViewMode vm);
120
121 void addWidget(const QDesignerWidgetBoxInterface::Widget &widget, const QIcon &icon, bool editable);
122
123 QDesignerWidgetBoxInterface::Widget widgetAt(const QModelIndex & index) const;
124 QDesignerWidgetBoxInterface::Widget widgetAt(int row) const;
125
126 int indexOfWidget(const QString &name);
127
128 QDesignerWidgetBoxInterface::Category category() const;
129 bool removeCustomWidgets();
130
131private:
132 using WidgetBoxCategoryEntrys = QVector<WidgetBoxCategoryEntry>;
133
134 QDesignerFormEditorInterface *m_core;
135 WidgetBoxCategoryEntrys m_items;
136 QListView::ViewMode m_viewMode;
137};
138
139WidgetBoxCategoryModel::WidgetBoxCategoryModel(QDesignerFormEditorInterface *core, QObject *parent) :
140 QAbstractListModel(parent),
141 m_core(core),
142 m_viewMode(QListView::ListMode)
143{
144}
145
146QListView::ViewMode WidgetBoxCategoryModel::viewMode() const
147{
148 return m_viewMode;
149}
150
151void WidgetBoxCategoryModel::setViewMode(QListView::ViewMode vm)
152{
153 if (m_viewMode == vm)
154 return;
155 const bool empty = m_items.isEmpty();
156 if (!empty)
157 beginResetModel();
158 m_viewMode = vm;
159 if (!empty)
160 endResetModel();
161}
162
163int WidgetBoxCategoryModel::indexOfWidget(const QString &name)
164{
165 const int count = m_items.size();
166 for (int i = 0; i < count; i++)
167 if (m_items.at(i).widget.name() == name)
168 return i;
169 return -1;
170}
171
172QDesignerWidgetBoxInterface::Category WidgetBoxCategoryModel::category() const
173{
174 QDesignerWidgetBoxInterface::Category rc;
175 const WidgetBoxCategoryEntrys::const_iterator cend = m_items.constEnd();
176 for (WidgetBoxCategoryEntrys::const_iterator it = m_items.constBegin(); it != cend; ++it)
177 rc.addWidget(awidget: it->widget);
178 return rc;
179}
180
181bool WidgetBoxCategoryModel::removeCustomWidgets()
182{
183 // Typically, we are a whole category of custom widgets, so, remove all
184 // and do reset.
185 bool changed = false;
186 for (WidgetBoxCategoryEntrys::iterator it = m_items.begin(); it != m_items.end(); )
187 if (it->widget.type() == QDesignerWidgetBoxInterface::Widget::Custom) {
188 if (!changed)
189 beginResetModel();
190 it = m_items.erase(pos: it);
191 changed = true;
192 } else {
193 ++it;
194 }
195 if (changed)
196 endResetModel();
197 return changed;
198}
199
200void WidgetBoxCategoryModel::addWidget(const QDesignerWidgetBoxInterface::Widget &widget, const QIcon &icon,bool editable)
201{
202 // build item. Filter on name + class name if it is different and not a layout.
203 QString filter = widget.name();
204 if (!filter.contains(QStringLiteral("Layout"))) {
205 static const QRegularExpression classNameRegExp(QStringLiteral("<widget +class *= *\"([^\"]+)\""));
206 Q_ASSERT(classNameRegExp.isValid());
207 const QRegularExpressionMatch match = classNameRegExp.match(subject: widget.domXml());
208 if (match.hasMatch()) {
209 const QString className = match.captured(nth: 1);
210 if (!filter.contains(s: className))
211 filter += className;
212 }
213 }
214 WidgetBoxCategoryEntry item(widget, filter, icon, editable);
215 const QDesignerWidgetDataBaseInterface *db = m_core->widgetDataBase();
216 const int dbIndex = db->indexOfClassName(className: widget.name());
217 if (dbIndex != -1) {
218 const QDesignerWidgetDataBaseItemInterface *dbItem = db->item(index: dbIndex);
219 const QString toolTip = dbItem->toolTip();
220 if (!toolTip.isEmpty())
221 item.toolTip = toolTip;
222 const QString whatsThis = dbItem->whatsThis();
223 if (!whatsThis.isEmpty())
224 item.whatsThis = whatsThis;
225 }
226 // insert
227 const int row = m_items.size();
228 beginInsertRows(parent: QModelIndex(), first: row, last: row);
229 m_items.push_back(t: item);
230 endInsertRows();
231}
232
233QVariant WidgetBoxCategoryModel::data(const QModelIndex &index, int role) const
234{
235 const int row = index.row();
236 if (row < 0 || row >= m_items.size())
237 return QVariant();
238
239 const WidgetBoxCategoryEntry &item = m_items.at(i: row);
240 switch (role) {
241 case Qt::DisplayRole:
242 // No text in icon mode
243 return QVariant(m_viewMode == QListView::ListMode ? item.widget.name() : QString());
244 case Qt::DecorationRole:
245 return QVariant(item.icon);
246 case Qt::EditRole:
247 return QVariant(item.widget.name());
248 case Qt::ToolTipRole: {
249 if (m_viewMode == QListView::ListMode)
250 return QVariant(item.toolTip);
251 // Icon mode tooltip should contain the class name
252 QString tt = item.widget.name();
253 if (!item.toolTip.isEmpty()) {
254 tt += QLatin1Char('\n');
255 tt += item.toolTip;
256 }
257 return QVariant(tt);
258
259 }
260 case Qt::WhatsThisRole:
261 return QVariant(item.whatsThis);
262 case FilterRole:
263 return item.filter;
264 }
265 return QVariant();
266}
267
268bool WidgetBoxCategoryModel::setData(const QModelIndex &index, const QVariant &value, int role)
269{
270 const int row = index.row();
271 if (role != Qt::EditRole || row < 0 || row >= m_items.size() || value.type() != QVariant::String)
272 return false;
273 // Set name and adapt Xml
274 WidgetBoxCategoryEntry &item = m_items[row];
275 const QString newName = value.toString();
276 item.widget.setName(newName);
277
278 const QDomDocument doc = stringToDom(xml: WidgetBoxCategoryListView::widgetDomXml(widget: item.widget));
279 QDomElement widget_elt = doc.firstChildElement(tagName: QLatin1String(widgetElementC));
280 if (!widget_elt.isNull()) {
281 widget_elt.setAttribute(name: QLatin1String(nameAttributeC), value: newName);
282 item.widget.setDomXml(domToString(elt: widget_elt));
283 }
284 emit dataChanged(topLeft: index, bottomRight: index);
285 return true;
286}
287
288Qt::ItemFlags WidgetBoxCategoryModel::flags(const QModelIndex &index) const
289{
290 Qt::ItemFlags rc = Qt::ItemIsEnabled;
291 const int row = index.row();
292 if (row >= 0 && row < m_items.size())
293 if (m_items.at(i: row).editable) {
294 rc |= Qt::ItemIsSelectable;
295 // Can change name in list mode only
296 if (m_viewMode == QListView::ListMode)
297 rc |= Qt::ItemIsEditable;
298 }
299 return rc;
300}
301
302int WidgetBoxCategoryModel::rowCount(const QModelIndex & /*parent*/) const
303{
304 return m_items.size();
305}
306
307bool WidgetBoxCategoryModel::removeRows(int row, int count, const QModelIndex & parent)
308{
309 if (row < 0 || count < 1)
310 return false;
311 const int size = m_items.size();
312 const int last = row + count - 1;
313 if (row >= size || last >= size)
314 return false;
315 beginRemoveRows(parent, first: row, last);
316 for (int r = last; r >= row; r--)
317 m_items.removeAt(i: r);
318 endRemoveRows();
319 return true;
320}
321
322QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryModel::widgetAt(const QModelIndex & index) const
323{
324 return widgetAt(row: index.row());
325}
326
327QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryModel::widgetAt(int row) const
328{
329 if (row < 0 || row >= m_items.size())
330 return QDesignerWidgetBoxInterface::Widget();
331 return m_items.at(i: row).widget;
332}
333
334/* WidgetSubBoxItemDelegate, ensures a valid name using a regexp validator */
335
336class WidgetBoxCategoryEntryDelegate : public QItemDelegate
337{
338public:
339 explicit WidgetBoxCategoryEntryDelegate(QWidget *parent = nullptr) : QItemDelegate(parent) {}
340 QWidget *createEditor(QWidget *parent,
341 const QStyleOptionViewItem &option,
342 const QModelIndex &index) const override;
343};
344
345QWidget *WidgetBoxCategoryEntryDelegate::createEditor(QWidget *parent,
346 const QStyleOptionViewItem &option,
347 const QModelIndex &index) const
348{
349 QWidget *result = QItemDelegate::createEditor(parent, option, index);
350 if (QLineEdit *line_edit = qobject_cast<QLineEdit*>(object: result)) {
351 static const QRegularExpression re(QStringLiteral("^[_a-zA-Z][_a-zA-Z0-9]*$"));
352 Q_ASSERT(re.isValid());
353 line_edit->setValidator(new QRegularExpressionValidator(re, line_edit));
354 }
355 return result;
356}
357
358// ---------------------- WidgetBoxCategoryListView
359
360WidgetBoxCategoryListView::WidgetBoxCategoryListView(QDesignerFormEditorInterface *core, QWidget *parent) :
361 QListView(parent),
362 m_proxyModel(new QSortFilterProxyModel(this)),
363 m_model(new WidgetBoxCategoryModel(core, this))
364{
365 setFocusPolicy(Qt::NoFocus);
366 setFrameShape(QFrame::NoFrame);
367 setIconSize(QSize(22, 22));
368 setSpacing(1);
369 setTextElideMode(Qt::ElideMiddle);
370 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
371 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
372 setResizeMode(QListView::Adjust);
373 setUniformItemSizes(true);
374
375 setItemDelegate(new WidgetBoxCategoryEntryDelegate(this));
376
377 connect(sender: this, signal: &QListView::pressed, receiver: this,
378 slot: &WidgetBoxCategoryListView::slotPressed);
379 setEditTriggers(QAbstractItemView::AnyKeyPressed);
380
381 m_proxyModel->setSourceModel(m_model);
382 m_proxyModel->setFilterRole(FilterRole);
383 setModel(m_proxyModel);
384 connect(sender: m_model, signal: &QAbstractItemModel::dataChanged,
385 receiver: this, slot: &WidgetBoxCategoryListView::scratchPadChanged);
386}
387
388void WidgetBoxCategoryListView::setViewMode(ViewMode vm)
389{
390 QListView::setViewMode(vm);
391 m_model->setViewMode(vm);
392}
393
394void WidgetBoxCategoryListView::setCurrentItem(AccessMode am, int row)
395{
396 const QModelIndex index = am == FilteredAccess ?
397 m_proxyModel->index(row, column: 0) :
398 m_proxyModel->mapFromSource(sourceIndex: m_model->index(row, column: 0));
399
400 if (index.isValid())
401 setCurrentIndex(index);
402}
403
404void WidgetBoxCategoryListView::slotPressed(const QModelIndex &index)
405{
406 const QDesignerWidgetBoxInterface::Widget wgt = m_model->widgetAt(index: m_proxyModel->mapToSource(proxyIndex: index));
407 if (wgt.isNull())
408 return;
409 emit pressed(name: wgt.name(), xml: widgetDomXml(widget: wgt), globalPos: QCursor::pos());
410}
411
412void WidgetBoxCategoryListView::removeCurrentItem()
413{
414 const QModelIndex index = currentIndex();
415 if (!index.isValid() || !m_proxyModel->removeRow(arow: index.row()))
416 return;
417
418 // We check the unfiltered item count here, we don't want to get removed if the
419 // filtered view is empty
420 if (m_model->rowCount()) {
421 emit itemRemoved();
422 } else {
423 emit lastItemRemoved();
424 }
425}
426
427void WidgetBoxCategoryListView::editCurrentItem()
428{
429 const QModelIndex index = currentIndex();
430 if (index.isValid())
431 edit(index);
432}
433
434int WidgetBoxCategoryListView::count(AccessMode am) const
435{
436 return am == FilteredAccess ? m_proxyModel->rowCount() : m_model->rowCount();
437}
438
439int WidgetBoxCategoryListView::mapRowToSource(int filterRow) const
440{
441 const QModelIndex filterIndex = m_proxyModel->index(row: filterRow, column: 0);
442 return m_proxyModel->mapToSource(proxyIndex: filterIndex).row();
443}
444
445QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryListView::widgetAt(AccessMode am, const QModelIndex & index) const
446{
447 const QModelIndex unfilteredIndex = am == FilteredAccess ? m_proxyModel->mapToSource(proxyIndex: index) : index;
448 return m_model->widgetAt(index: unfilteredIndex);
449}
450
451QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryListView::widgetAt(AccessMode am, int row) const
452{
453 return m_model->widgetAt(row: am == UnfilteredAccess ? row : mapRowToSource(filterRow: row));
454}
455
456void WidgetBoxCategoryListView::removeRow(AccessMode am, int row)
457{
458 m_model->removeRow(arow: am == UnfilteredAccess ? row : mapRowToSource(filterRow: row));
459}
460
461bool WidgetBoxCategoryListView::containsWidget(const QString &name)
462{
463 return m_model->indexOfWidget(name) != -1;
464}
465
466void WidgetBoxCategoryListView::addWidget(const QDesignerWidgetBoxInterface::Widget &widget, const QIcon &icon, bool editable)
467{
468 m_model->addWidget(widget, icon, editable);
469}
470
471QString WidgetBoxCategoryListView::widgetDomXml(const QDesignerWidgetBoxInterface::Widget &widget)
472{
473 QString domXml = widget.domXml();
474
475 if (domXml.isEmpty()) {
476 domXml = QLatin1String(uiOpeningTagC);
477 domXml += QStringLiteral("<widget class=\"");
478 domXml += widget.name();
479 domXml += QStringLiteral("\"/>");
480 domXml += QLatin1String(uiClosingTagC);
481 }
482 return domXml;
483}
484
485void WidgetBoxCategoryListView::filter(const QRegExp &re)
486{
487 m_proxyModel->setFilterRegExp(re);
488}
489
490QDesignerWidgetBoxInterface::Category WidgetBoxCategoryListView::category() const
491{
492 return m_model->category();
493}
494
495bool WidgetBoxCategoryListView::removeCustomWidgets()
496{
497 return m_model->removeCustomWidgets();
498}
499} // namespace qdesigner_internal
500
501QT_END_NAMESPACE
502

source code of qttools/src/designer/src/components/widgetbox/widgetboxcategorylistview.cpp