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(text: 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(awidget: 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(pos: 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(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 | |
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(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 | |
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(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 | |
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(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 | |
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, first: row, last); |
316 | for (int r = last; r >= row; r--) |
317 | m_items.removeAt(i: r); |
318 | endRemoveRows(); |
319 | return true; |
320 | } |
321 | |
322 | QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryModel::widgetAt(const QModelIndex & index) const |
323 | { |
324 | return widgetAt(row: 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(i: 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*>(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 | |
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(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 | |
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, column: 0) : |
398 | m_proxyModel->mapFromSource(sourceIndex: m_model->index(row, column: 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(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 | |
412 | void 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 | |
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(row: filterRow, column: 0); |
442 | return m_proxyModel->mapToSource(proxyIndex: 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(proxyIndex: index) : index; |
448 | return m_model->widgetAt(index: unfilteredIndex); |
449 | } |
450 | |
451 | QDesignerWidgetBoxInterface::Widget WidgetBoxCategoryListView::widgetAt(AccessMode am, int row) const |
452 | { |
453 | return m_model->widgetAt(row: am == UnfilteredAccess ? row : mapRowToSource(filterRow: row)); |
454 | } |
455 | |
456 | void WidgetBoxCategoryListView::removeRow(AccessMode am, int row) |
457 | { |
458 | m_model->removeRow(arow: am == UnfilteredAccess ? row : mapRowToSource(filterRow: 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 | |