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