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
48 static const char *widgetElementC = "widget";
49 static const char *nameAttributeC = "name";
50 static const char *uiOpeningTagC = "<ui>";
51 static const char *uiClosingTagC = "</ui>";
52
53 QT_BEGIN_NAMESPACE
54
55 enum { FilterRole = Qt::UserRole + 11 };
56
domToString(const QDomElement & elt)57 static 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
stringToDom(const QString & xml)66 static QDomDocument stringToDom(const QString &xml)
67 {
68 QDomDocument result;
69 result.setContent(xml);
70 return result;
71 }
72
73 namespace qdesigner_internal {
74
75 // Entry of the model list
76
77 struct 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
WidgetBoxCategoryEntry(const QDesignerWidgetBoxInterface::Widget & w,const QString & filterIn,const QIcon & i,bool e)92 WidgetBoxCategoryEntry::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
106 class WidgetBoxCategoryModel : public QAbstractListModel {
107 public:
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
131 private:
132 using WidgetBoxCategoryEntrys = QVector<WidgetBoxCategoryEntry>;
133
134 QDesignerFormEditorInterface *m_core;
135 WidgetBoxCategoryEntrys m_items;
136 QListView::ViewMode m_viewMode;
137 };
138
WidgetBoxCategoryModel(QDesignerFormEditorInterface * core,QObject * parent)139 WidgetBoxCategoryModel::WidgetBoxCategoryModel(QDesignerFormEditorInterface *core, QObject *parent) :
140 QAbstractListModel(parent),
141 m_core(core),
142 m_viewMode(QListView::ListMode)
143 {
144 }
145
viewMode() const146 QListView::ViewMode WidgetBoxCategoryModel::viewMode() const
147 {
148 return m_viewMode;
149 }
150
setViewMode(QListView::ViewMode vm)151 void 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
indexOfWidget(const QString & name)163 int 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
category() const172 QDesignerWidgetBoxInterface::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(it->widget);
178 return rc;
179 }
180
removeCustomWidgets()181 bool 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(it);
191 changed = true;
192 } else {
193 ++it;
194 }
195 if (changed)
196 endResetModel();
197 return changed;
198 }
199
addWidget(const QDesignerWidgetBoxInterface::Widget & widget,const QIcon & icon,bool editable)200 void 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(widget.domXml());
208 if (match.hasMatch()) {
209 const QString className = match.captured(1);
210 if (!filter.contains(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(widget.name());
217 if (dbIndex != -1) {
218 const QDesignerWidgetDataBaseItemInterface *dbItem = db->item(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(QModelIndex(), row, row);
229 m_items.push_back(item);
230 endInsertRows();
231 }
232
data(const QModelIndex & index,int role) const233 QVariant 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(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
setData(const QModelIndex & index,const QVariant & value,int role)268 bool 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(WidgetBoxCategoryListView::widgetDomXml(item.widget));
279 QDomElement widget_elt = doc.firstChildElement(QLatin1String(widgetElementC));
280 if (!widget_elt.isNull()) {
281 widget_elt.setAttribute(QLatin1String(nameAttributeC), newName);
282 item.widget.setDomXml(domToString(widget_elt));
283 }
284 emit dataChanged(index, index);
285 return true;
286 }
287
flags(const QModelIndex & index) const288 Qt::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(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
rowCount(const QModelIndex &) const302 int WidgetBoxCategoryModel::rowCount(const QModelIndex & /*parent*/) const
303 {
304 return m_items.size();
305 }
306
removeRows(int row,int count,const QModelIndex & parent)307 bool 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, row, last);
316 for (int r = last; r >= row; r--)
317 m_items.removeAt(r);
318 endRemoveRows();
319 return true;
320 }
321
widgetAt(const QModelIndex & index) const322 QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryModel::widgetAt(const QModelIndex & index) const
323 {
324 return widgetAt(index.row());
325 }
326
widgetAt(int row) const327 QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryModel::widgetAt(int row) const
328 {
329 if (row < 0 || row >= m_items.size())
330 return QDesignerWidgetBoxInterface::Widget();
331 return m_items.at(row).widget;
332 }
333
334 /* WidgetSubBoxItemDelegate, ensures a valid name using a regexp validator */
335
336 class WidgetBoxCategoryEntryDelegate : public QItemDelegate
337 {
338 public:
WidgetBoxCategoryEntryDelegate(QWidget * parent=nullptr)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
createEditor(QWidget * parent,const QStyleOptionViewItem & option,const QModelIndex & index) const345 QWidget *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*>(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
WidgetBoxCategoryListView(QDesignerFormEditorInterface * core,QWidget * parent)360 WidgetBoxCategoryListView::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(this, &QListView::pressed, this,
378 &WidgetBoxCategoryListView::slotPressed);
379 setEditTriggers(QAbstractItemView::AnyKeyPressed);
380
381 m_proxyModel->setSourceModel(m_model);
382 m_proxyModel->setFilterRole(FilterRole);
383 setModel(m_proxyModel);
384 connect(m_model, &QAbstractItemModel::dataChanged,
385 this, &WidgetBoxCategoryListView::scratchPadChanged);
386 }
387
setViewMode(ViewMode vm)388 void WidgetBoxCategoryListView::setViewMode(ViewMode vm)
389 {
390 QListView::setViewMode(vm);
391 m_model->setViewMode(vm);
392 }
393
setCurrentItem(AccessMode am,int row)394 void WidgetBoxCategoryListView::setCurrentItem(AccessMode am, int row)
395 {
396 const QModelIndex index = am == FilteredAccess ?
397 m_proxyModel->index(row, 0) :
398 m_proxyModel->mapFromSource(m_model->index(row, 0));
399
400 if (index.isValid())
401 setCurrentIndex(index);
402 }
403
slotPressed(const QModelIndex & index)404 void WidgetBoxCategoryListView::slotPressed(const QModelIndex &index)
405 {
406 const QDesignerWidgetBoxInterface::Widget wgt = m_model->widgetAt(m_proxyModel->mapToSource(index));
407 if (wgt.isNull())
408 return;
409 emit pressed(wgt.name(), widgetDomXml(wgt), QCursor::pos());
410 }
411
removeCurrentItem()412 void WidgetBoxCategoryListView::removeCurrentItem()
413 {
414 const QModelIndex index = currentIndex();
415 if (!index.isValid() || !m_proxyModel->removeRow(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
editCurrentItem()427 void WidgetBoxCategoryListView::editCurrentItem()
428 {
429 const QModelIndex index = currentIndex();
430 if (index.isValid())
431 edit(index);
432 }
433
count(AccessMode am) const434 int WidgetBoxCategoryListView::count(AccessMode am) const
435 {
436 return am == FilteredAccess ? m_proxyModel->rowCount() : m_model->rowCount();
437 }
438
mapRowToSource(int filterRow) const439 int WidgetBoxCategoryListView::mapRowToSource(int filterRow) const
440 {
441 const QModelIndex filterIndex = m_proxyModel->index(filterRow, 0);
442 return m_proxyModel->mapToSource(filterIndex).row();
443 }
444
widgetAt(AccessMode am,const QModelIndex & index) const445 QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryListView::widgetAt(AccessMode am, const QModelIndex & index) const
446 {
447 const QModelIndex unfilteredIndex = am == FilteredAccess ? m_proxyModel->mapToSource(index) : index;
448 return m_model->widgetAt(unfilteredIndex);
449 }
450
widgetAt(AccessMode am,int row) const451 QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryListView::widgetAt(AccessMode am, int row) const
452 {
453 return m_model->widgetAt(am == UnfilteredAccess ? row : mapRowToSource(row));
454 }
455
removeRow(AccessMode am,int row)456 void WidgetBoxCategoryListView::removeRow(AccessMode am, int row)
457 {
458 m_model->removeRow(am == UnfilteredAccess ? row : mapRowToSource(row));
459 }
460
containsWidget(const QString & name)461 bool WidgetBoxCategoryListView::containsWidget(const QString &name)
462 {
463 return m_model->indexOfWidget(name) != -1;
464 }
465
addWidget(const QDesignerWidgetBoxInterface::Widget & widget,const QIcon & icon,bool editable)466 void WidgetBoxCategoryListView::addWidget(const QDesignerWidgetBoxInterface::Widget &widget, const QIcon &icon, bool editable)
467 {
468 m_model->addWidget(widget, icon, editable);
469 }
470
widgetDomXml(const QDesignerWidgetBoxInterface::Widget & widget)471 QString 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
filter(const QRegExp & re)485 void WidgetBoxCategoryListView::filter(const QRegExp &re)
486 {
487 m_proxyModel->setFilterRegExp(re);
488 }
489
category() const490 QDesignerWidgetBoxInterface::Category WidgetBoxCategoryListView::category() const
491 {
492 return m_model->category();
493 }
494
removeCustomWidgets()495 bool WidgetBoxCategoryListView::removeCustomWidgets()
496 {
497 return m_model->removeCustomWidgets();
498 }
499 } // namespace qdesigner_internal
500
501 QT_END_NAMESPACE
502