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 
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 
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 
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 
139 WidgetBoxCategoryModel::WidgetBoxCategoryModel(QDesignerFormEditorInterface *core, QObject *parent) :
140     QAbstractListModel(parent),
141     m_core(core),
142     m_viewMode(QListView::ListMode)
143 {
144 }
145 
146 QListView::ViewMode WidgetBoxCategoryModel::viewMode() const
147 {
148     return m_viewMode;
149 }
150 
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 
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 
172 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 
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 
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 
233 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 
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 
288 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 
302 int WidgetBoxCategoryModel::rowCount(const QModelIndex & /*parent*/) const
303 {
304     return m_items.size();
305 }
306 
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 
322 QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryModel::widgetAt(const QModelIndex & index) const
323 {
324     return widgetAt(index.row());
325 }
326 
327 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:
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 
345 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 
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 
388 void WidgetBoxCategoryListView::setViewMode(ViewMode vm)
389 {
390     QListView::setViewMode(vm);
391     m_model->setViewMode(vm);
392 }
393 
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 
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 
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 
427 void WidgetBoxCategoryListView::editCurrentItem()
428 {
429     const QModelIndex index = currentIndex();
430     if (index.isValid())
431         edit(index);
432 }
433 
434 int WidgetBoxCategoryListView::count(AccessMode am) const
435 {
436     return am == FilteredAccess ? m_proxyModel->rowCount() : m_model->rowCount();
437 }
438 
439 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 
445 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 
451 QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryListView::widgetAt(AccessMode am, int row) const
452 {
453     return m_model->widgetAt(am == UnfilteredAccess ? row : mapRowToSource(row));
454 }
455 
456 void WidgetBoxCategoryListView::removeRow(AccessMode am, int row)
457 {
458     m_model->removeRow(am == UnfilteredAccess ? row : mapRowToSource(row));
459 }
460 
461 bool WidgetBoxCategoryListView::containsWidget(const QString &name)
462 {
463     return m_model->indexOfWidget(name) != -1;
464 }
465 
466 void WidgetBoxCategoryListView::addWidget(const QDesignerWidgetBoxInterface::Widget &widget, const QIcon &icon, bool editable)
467 {
468     m_model->addWidget(widget, icon, editable);
469 }
470 
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 
485 void WidgetBoxCategoryListView::filter(const QRegExp &re)
486 {
487     m_proxyModel->setFilterRegExp(re);
488 }
489 
490 QDesignerWidgetBoxInterface::Category WidgetBoxCategoryListView::category() const
491 {
492     return m_model->category();
493 }
494 
495 bool WidgetBoxCategoryListView::removeCustomWidgets()
496 {
497     return m_model->removeCustomWidgets();
498 }
499 }  // namespace qdesigner_internal
500 
501 QT_END_NAMESPACE
502