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 "widgetboxtreewidget.h"
30 #include "widgetboxcategorylistview.h"
31
32 // shared
33 #include <iconloader_p.h>
34 #include <sheet_delegate_p.h>
35 #include <QtDesigner/private/ui4_p.h>
36 #include <qdesigner_utils_p.h>
37 #include <pluginmanager_p.h>
38
39 // sdk
40 #include <QtDesigner/abstractformeditor.h>
41 #include <QtDesigner/abstractdnditem.h>
42 #include <QtDesigner/abstractsettings.h>
43
44 #include <QtUiPlugin/customwidget.h>
45
46 #include <QtWidgets/qheaderview.h>
47 #include <QtWidgets/qapplication.h>
48 #include <QtWidgets/qtreewidget.h>
49 #include <QtGui/qevent.h>
50 #include <QtWidgets/qaction.h>
51 #include <QtWidgets/qactiongroup.h>
52 #include <QtWidgets/qmenu.h>
53
54 #include <QtCore/qfile.h>
55 #include <QtCore/qtimer.h>
56 #include <QtCore/qdebug.h>
57
58 static const char *widgetBoxRootElementC = "widgetbox";
59 static const char *widgetElementC = "widget";
60 static const char *uiElementC = "ui";
61 static const char *categoryElementC = "category";
62 static const char *categoryEntryElementC = "categoryentry";
63 static const char *nameAttributeC = "name";
64 static const char *typeAttributeC = "type";
65 static const char *iconAttributeC = "icon";
66 static const char *defaultTypeValueC = "default";
67 static const char *customValueC = "custom";
68 static const char *iconPrefixC = "__qt_icon__";
69 static const char *scratchPadValueC = "scratchpad";
70 static const char *invisibleNameC = "[invisible]";
71
72 enum TopLevelRole { NORMAL_ITEM, SCRATCHPAD_ITEM, CUSTOM_ITEM };
73
74 QT_BEGIN_NAMESPACE
75
setTopLevelRole(TopLevelRole tlr,QTreeWidgetItem * item)76 static void setTopLevelRole(TopLevelRole tlr, QTreeWidgetItem *item)
77 {
78 item->setData(0, Qt::UserRole, QVariant(tlr));
79 }
80
topLevelRole(const QTreeWidgetItem * item)81 static TopLevelRole topLevelRole(const QTreeWidgetItem *item)
82 {
83 return static_cast<TopLevelRole>(item->data(0, Qt::UserRole).toInt());
84 }
85
86 namespace qdesigner_internal {
87
WidgetBoxTreeWidget(QDesignerFormEditorInterface * core,QWidget * parent)88 WidgetBoxTreeWidget::WidgetBoxTreeWidget(QDesignerFormEditorInterface *core, QWidget *parent) :
89 QTreeWidget(parent),
90 m_core(core),
91 m_iconMode(false),
92 m_scratchPadDeleteTimer(nullptr)
93 {
94 setFocusPolicy(Qt::NoFocus);
95 setIndentation(0);
96 setRootIsDecorated(false);
97 setColumnCount(1);
98 header()->hide();
99 header()->setSectionResizeMode(QHeaderView::Stretch);
100 setTextElideMode(Qt::ElideMiddle);
101 setVerticalScrollMode(ScrollPerPixel);
102
103 setItemDelegate(new SheetDelegate(this, this));
104
105 connect(this, &QTreeWidget::itemPressed,
106 this, &WidgetBoxTreeWidget::handleMousePress);
107 }
108
iconForWidget(const QString & iconName) const109 QIcon WidgetBoxTreeWidget::iconForWidget(const QString &iconName) const
110 {
111 if (iconName.isEmpty())
112 return qdesigner_internal::qtLogoIcon();
113
114 if (iconName.startsWith(QLatin1String(iconPrefixC))) {
115 const IconCache::const_iterator it = m_pluginIcons.constFind(iconName);
116 if (it != m_pluginIcons.constEnd())
117 return it.value();
118 }
119 return createIconSet(iconName);
120 }
121
categoryViewAt(int idx) const122 WidgetBoxCategoryListView *WidgetBoxTreeWidget::categoryViewAt(int idx) const
123 {
124 WidgetBoxCategoryListView *rc = nullptr;
125 if (QTreeWidgetItem *cat_item = topLevelItem(idx))
126 if (QTreeWidgetItem *embedItem = cat_item->child(0))
127 rc = qobject_cast<WidgetBoxCategoryListView*>(itemWidget(embedItem, 0));
128 Q_ASSERT(rc);
129 return rc;
130 }
131
132 static const char widgetBoxSettingsGroupC[] = "WidgetBox";
133 static const char widgetBoxExpandedKeyC[] = "Closed categories";
134 static const char widgetBoxViewModeKeyC[] = "View mode";
135
saveExpandedState() const136 void WidgetBoxTreeWidget::saveExpandedState() const
137 {
138 QStringList closedCategories;
139 if (const int numCategories = categoryCount()) {
140 for (int i = 0; i < numCategories; ++i) {
141 const QTreeWidgetItem *cat_item = topLevelItem(i);
142 if (!cat_item->isExpanded())
143 closedCategories.append(cat_item->text(0));
144 }
145 }
146 QDesignerSettingsInterface *settings = m_core->settingsManager();
147 settings->beginGroup(QLatin1String(widgetBoxSettingsGroupC));
148 settings->setValue(QLatin1String(widgetBoxExpandedKeyC), closedCategories);
149 settings->setValue(QLatin1String(widgetBoxViewModeKeyC), m_iconMode);
150 settings->endGroup();
151 }
152
restoreExpandedState()153 void WidgetBoxTreeWidget::restoreExpandedState()
154 {
155 using StringSet = QSet<QString>;
156 QDesignerSettingsInterface *settings = m_core->settingsManager();
157 const QString groupKey = QLatin1String(widgetBoxSettingsGroupC) + QLatin1Char('/');
158 m_iconMode = settings->value(groupKey + QLatin1String(widgetBoxViewModeKeyC)).toBool();
159 updateViewMode();
160 const auto &closedCategoryList = settings->value(groupKey + QLatin1String(widgetBoxExpandedKeyC), QStringList()).toStringList();
161 const StringSet closedCategories(closedCategoryList.cbegin(), closedCategoryList.cend());
162 expandAll();
163 if (closedCategories.isEmpty())
164 return;
165
166 if (const int numCategories = categoryCount()) {
167 for (int i = 0; i < numCategories; ++i) {
168 QTreeWidgetItem *item = topLevelItem(i);
169 if (closedCategories.contains(item->text(0)))
170 item->setExpanded(false);
171 }
172 }
173 }
174
~WidgetBoxTreeWidget()175 WidgetBoxTreeWidget::~WidgetBoxTreeWidget()
176 {
177 saveExpandedState();
178 }
179
setFileName(const QString & file_name)180 void WidgetBoxTreeWidget::setFileName(const QString &file_name)
181 {
182 m_file_name = file_name;
183 }
184
fileName() const185 QString WidgetBoxTreeWidget::fileName() const
186 {
187 return m_file_name;
188 }
189
save()190 bool WidgetBoxTreeWidget::save()
191 {
192 if (fileName().isEmpty())
193 return false;
194
195 QFile file(fileName());
196 if (!file.open(QIODevice::WriteOnly))
197 return false;
198
199 CategoryList cat_list;
200 const int count = categoryCount();
201 for (int i = 0; i < count; ++i)
202 cat_list.append(category(i));
203
204 QXmlStreamWriter writer(&file);
205 writer.setAutoFormatting(true);
206 writer.setAutoFormattingIndent(1);
207 writer.writeStartDocument();
208 writeCategories(writer, cat_list);
209 writer.writeEndDocument();
210
211 return true;
212 }
213
slotSave()214 void WidgetBoxTreeWidget::slotSave()
215 {
216 save();
217 }
218
handleMousePress(QTreeWidgetItem * item)219 void WidgetBoxTreeWidget::handleMousePress(QTreeWidgetItem *item)
220 {
221 if (item == nullptr)
222 return;
223
224 if (QApplication::mouseButtons() != Qt::LeftButton)
225 return;
226
227 if (item->parent() == nullptr) {
228 item->setExpanded(!item->isExpanded());
229 return;
230 }
231 }
232
ensureScratchpad()233 int WidgetBoxTreeWidget::ensureScratchpad()
234 {
235 const int existingIndex = indexOfScratchpad();
236 if (existingIndex != -1)
237 return existingIndex;
238
239 QTreeWidgetItem *scratch_item = new QTreeWidgetItem(this);
240 scratch_item->setText(0, tr("Scratchpad"));
241 setTopLevelRole(SCRATCHPAD_ITEM, scratch_item);
242 addCategoryView(scratch_item, false); // Scratchpad in list mode.
243 return categoryCount() - 1;
244 }
245
addCategoryView(QTreeWidgetItem * parent,bool iconMode)246 WidgetBoxCategoryListView *WidgetBoxTreeWidget::addCategoryView(QTreeWidgetItem *parent, bool iconMode)
247 {
248 QTreeWidgetItem *embed_item = new QTreeWidgetItem(parent);
249 embed_item->setFlags(Qt::ItemIsEnabled);
250 WidgetBoxCategoryListView *categoryView = new WidgetBoxCategoryListView(m_core, this);
251 categoryView->setViewMode(iconMode ? QListView::IconMode : QListView::ListMode);
252 connect(categoryView, &WidgetBoxCategoryListView::scratchPadChanged,
253 this, &WidgetBoxTreeWidget::slotSave);
254 connect(categoryView, &WidgetBoxCategoryListView::pressed,
255 this, &WidgetBoxTreeWidget::pressed);
256 connect(categoryView, &WidgetBoxCategoryListView::itemRemoved,
257 this, &WidgetBoxTreeWidget::slotScratchPadItemDeleted);
258 connect(categoryView, &WidgetBoxCategoryListView::lastItemRemoved,
259 this, &WidgetBoxTreeWidget::slotLastScratchPadItemDeleted);
260 setItemWidget(embed_item, 0, categoryView);
261 return categoryView;
262 }
263
indexOfScratchpad() const264 int WidgetBoxTreeWidget::indexOfScratchpad() const
265 {
266 if (const int numTopLevels = topLevelItemCount()) {
267 for (int i = numTopLevels - 1; i >= 0; --i) {
268 if (topLevelRole(topLevelItem(i)) == SCRATCHPAD_ITEM)
269 return i;
270 }
271 }
272 return -1;
273 }
274
indexOfCategory(const QString & name) const275 int WidgetBoxTreeWidget::indexOfCategory(const QString &name) const
276 {
277 const int topLevelCount = topLevelItemCount();
278 for (int i = 0; i < topLevelCount; ++i) {
279 if (topLevelItem(i)->text(0) == name)
280 return i;
281 }
282 return -1;
283 }
284
load(QDesignerWidgetBox::LoadMode loadMode)285 bool WidgetBoxTreeWidget::load(QDesignerWidgetBox::LoadMode loadMode)
286 {
287 switch (loadMode) {
288 case QDesignerWidgetBox::LoadReplace:
289 clear();
290 break;
291 case QDesignerWidgetBox::LoadCustomWidgetsOnly:
292 addCustomCategories(true);
293 updateGeometries();
294 return true;
295 default:
296 break;
297 }
298
299 const QString name = fileName();
300
301 QFile f(name);
302 if (!f.open(QIODevice::ReadOnly)) // Might not exist at first startup
303 return false;
304
305 const QString contents = QString::fromUtf8(f.readAll());
306 return loadContents(contents);
307 }
308
loadContents(const QString & contents)309 bool WidgetBoxTreeWidget::loadContents(const QString &contents)
310 {
311 QString errorMessage;
312 CategoryList cat_list;
313 if (!readCategories(m_file_name, contents, &cat_list, &errorMessage)) {
314 qdesigner_internal::designerWarning(errorMessage);
315 return false;
316 }
317
318 for (const Category &cat : qAsConst(cat_list))
319 addCategory(cat);
320
321 addCustomCategories(false);
322 // Restore which items are expanded
323 restoreExpandedState();
324 return true;
325 }
326
addCustomCategories(bool replace)327 void WidgetBoxTreeWidget::addCustomCategories(bool replace)
328 {
329 if (replace) {
330 // clear out all existing custom widgets
331 if (const int numTopLevels = topLevelItemCount()) {
332 for (int t = 0; t < numTopLevels ; ++t)
333 categoryViewAt(t)->removeCustomWidgets();
334 }
335 }
336 // re-add
337 const CategoryList customList = loadCustomCategoryList();
338 const CategoryList::const_iterator cend = customList.constEnd();
339 for (CategoryList::const_iterator it = customList.constBegin(); it != cend; ++it)
340 addCategory(*it);
341 }
342
msgXmlError(const QString & fileName,const QXmlStreamReader & r)343 static inline QString msgXmlError(const QString &fileName, const QXmlStreamReader &r)
344 {
345 return QDesignerWidgetBox::tr("An error has been encountered at line %1 of %2: %3")
346 .arg(r.lineNumber()).arg(fileName, r.errorString());
347 }
348
readCategories(const QString & fileName,const QString & contents,CategoryList * cats,QString * errorMessage)349 bool WidgetBoxTreeWidget::readCategories(const QString &fileName, const QString &contents,
350 CategoryList *cats, QString *errorMessage)
351 {
352 // Read widget box XML:
353 //
354 //<widgetbox version="4.5">
355 // <category name="Layouts">
356 // <categoryentry name="Vertical Layout" icon="win/editvlayout.png" type="default">
357 // <widget class="QListWidget" ...>
358 // ...
359
360 QXmlStreamReader reader(contents);
361
362
363 // Entries of category with name="invisible" should be ignored
364 bool ignoreEntries = false;
365
366 while (!reader.atEnd()) {
367 switch (reader.readNext()) {
368 case QXmlStreamReader::StartElement: {
369 const auto tag = reader.name();
370 if (tag == QLatin1String(widgetBoxRootElementC)) {
371 //<widgetbox version="4.5">
372 continue;
373 }
374 if (tag == QLatin1String(categoryElementC)) {
375 // <category name="Layouts">
376 const QXmlStreamAttributes attributes = reader.attributes();
377 const QString categoryName = attributes.value(QLatin1String(nameAttributeC)).toString();
378 if (categoryName == QLatin1String(invisibleNameC)) {
379 ignoreEntries = true;
380 } else {
381 Category category(categoryName);
382 if (attributes.value(QLatin1String(typeAttributeC)) == QLatin1String(scratchPadValueC))
383 category.setType(Category::Scratchpad);
384 cats->push_back(category);
385 }
386 continue;
387 }
388 if (tag == QLatin1String(categoryEntryElementC)) {
389 // <categoryentry name="Vertical Layout" icon="win/editvlayout.png" type="default">
390 if (!ignoreEntries) {
391 QXmlStreamAttributes attr = reader.attributes();
392 const QString widgetName = attr.value(QLatin1String(nameAttributeC)).toString();
393 const QString widgetIcon = attr.value(QLatin1String(iconAttributeC)).toString();
394 const WidgetBoxTreeWidget::Widget::Type widgetType =
395 attr.value(QLatin1String(typeAttributeC)).toString()
396 == QLatin1String(customValueC) ?
397 WidgetBoxTreeWidget::Widget::Custom :
398 WidgetBoxTreeWidget::Widget::Default;
399
400 Widget w;
401 w.setName(widgetName);
402 w.setIconName(widgetIcon);
403 w.setType(widgetType);
404 if (!readWidget(&w, contents, reader))
405 continue;
406
407 cats->back().addWidget(w);
408 } // ignoreEntries
409 continue;
410 }
411 break;
412 }
413 case QXmlStreamReader::EndElement: {
414 const auto tag = reader.name();
415 if (tag == QLatin1String(widgetBoxRootElementC)) {
416 continue;
417 }
418 if (tag == QLatin1String(categoryElementC)) {
419 ignoreEntries = false;
420 continue;
421 }
422 if (tag == QLatin1String(categoryEntryElementC)) {
423 continue;
424 }
425 break;
426 }
427 default: break;
428 }
429 }
430
431 if (reader.hasError()) {
432 *errorMessage = msgXmlError(fileName, reader);
433 return false;
434 }
435
436 return true;
437 }
438
439 /*!
440 * Read out a widget within a category. This can either be
441 * enclosed in a <ui> element or a (legacy) <widget> element which may
442 * contain nested <widget> elements.
443 *
444 * Examples:
445 *
446 * <ui language="c++">
447 * <widget class="MultiPageWidget" name="multipagewidget"> ... </widget>
448 * <customwidgets>...</customwidgets>
449 * <ui>
450 *
451 * or
452 *
453 * <widget>
454 * <widget> ... </widget>
455 * ...
456 * <widget>
457 *
458 * Returns true on success, false if end was reached or an error has been encountered
459 * in which case the reader has its error flag set. If successful, the current item
460 * of the reader will be the closing element (</ui> or </widget>)
461 */
readWidget(Widget * w,const QString & xml,QXmlStreamReader & r)462 bool WidgetBoxTreeWidget::readWidget(Widget *w, const QString &xml, QXmlStreamReader &r)
463 {
464 qint64 startTagPosition =0, endTagPosition = 0;
465
466 int nesting = 0;
467 bool endEncountered = false;
468 bool parsedWidgetTag = false;
469 while (!endEncountered) {
470 const qint64 currentPosition = r.characterOffset();
471 switch(r.readNext()) {
472 case QXmlStreamReader::StartElement:
473 if (nesting++ == 0) {
474 // First element must be <ui> or (legacy) <widget>
475 const auto name = r.name();
476 if (name == QLatin1String(uiElementC)) {
477 startTagPosition = currentPosition;
478 } else {
479 if (name == QLatin1String(widgetElementC)) {
480 startTagPosition = currentPosition;
481 parsedWidgetTag = true;
482 } else {
483 r.raiseError(QDesignerWidgetBox::tr("Unexpected element <%1> encountered when parsing for <widget> or <ui>").arg(name.toString()));
484 return false;
485 }
486 }
487 } else {
488 // We are within <ui> looking for the first <widget> tag
489 if (!parsedWidgetTag && r.name() == QLatin1String(widgetElementC)) {
490 parsedWidgetTag = true;
491 }
492 }
493 break;
494 case QXmlStreamReader::EndElement:
495 // Reached end of widget?
496 if (--nesting == 0) {
497 endTagPosition = r.characterOffset();
498 endEncountered = true;
499 }
500 break;
501 case QXmlStreamReader::EndDocument:
502 r.raiseError(QDesignerWidgetBox::tr("Unexpected end of file encountered when parsing widgets."));
503 return false;
504 case QXmlStreamReader::Invalid:
505 return false;
506 default:
507 break;
508 }
509 }
510 if (!parsedWidgetTag) {
511 r.raiseError(QDesignerWidgetBox::tr("A widget element could not be found."));
512 return false;
513 }
514 // Oddity: Startposition is 1 off
515 QString widgetXml = xml.mid(startTagPosition, endTagPosition - startTagPosition);
516 const QChar lessThan = QLatin1Char('<');
517 if (!widgetXml.startsWith(lessThan))
518 widgetXml.prepend(lessThan);
519 w->setDomXml(widgetXml);
520 return true;
521 }
522
writeCategories(QXmlStreamWriter & writer,const CategoryList & cat_list) const523 void WidgetBoxTreeWidget::writeCategories(QXmlStreamWriter &writer, const CategoryList &cat_list) const
524 {
525 const QString widgetbox = QLatin1String(widgetBoxRootElementC);
526 const QString name = QLatin1String(nameAttributeC);
527 const QString type = QLatin1String(typeAttributeC);
528 const QString icon = QLatin1String(iconAttributeC);
529 const QString defaultType = QLatin1String(defaultTypeValueC);
530 const QString category = QLatin1String(categoryElementC);
531 const QString categoryEntry = QLatin1String(categoryEntryElementC);
532 const QString iconPrefix = QLatin1String(iconPrefixC);
533
534 //
535 // <widgetbox>
536 // <category name="Layouts">
537 // <categoryEntry name="Vertical Layout" type="default" icon="win/editvlayout.png">
538 // <ui>
539 // ...
540 // </ui>
541 // </categoryEntry>
542 // ...
543 // </category>
544 // ...
545 // </widgetbox>
546 //
547
548 writer.writeStartElement(widgetbox);
549
550 for (const Category &cat : cat_list) {
551 writer.writeStartElement(category);
552 writer.writeAttribute(name, cat.name());
553 if (cat.type() == Category::Scratchpad)
554 writer.writeAttribute(type, QLatin1String(scratchPadValueC));
555
556 const int widgetCount = cat.widgetCount();
557 for (int i = 0; i < widgetCount; ++i) {
558 const Widget wgt = cat.widget(i);
559 if (wgt.type() == Widget::Custom)
560 continue;
561
562 writer.writeStartElement(categoryEntry);
563 writer.writeAttribute(name, wgt.name());
564 if (!wgt.iconName().startsWith(iconPrefix))
565 writer.writeAttribute(icon, wgt.iconName());
566 writer.writeAttribute(type, defaultType);
567
568 const DomUI *domUI = QDesignerWidgetBox::xmlToUi(wgt.name(), WidgetBoxCategoryListView::widgetDomXml(wgt), false);
569 if (domUI) {
570 domUI->write(writer);
571 delete domUI;
572 }
573
574 writer.writeEndElement(); // categoryEntry
575 }
576 writer.writeEndElement(); // categoryEntry
577 }
578
579 writer.writeEndElement(); // widgetBox
580 }
581
findCategory(const QString & name,const WidgetBoxTreeWidget::CategoryList & list)582 static int findCategory(const QString &name, const WidgetBoxTreeWidget::CategoryList &list)
583 {
584 int idx = 0;
585 for (const WidgetBoxTreeWidget::Category &cat : list) {
586 if (cat.name() == name)
587 return idx;
588 ++idx;
589 }
590 return -1;
591 }
592
isValidIcon(const QIcon & icon)593 static inline bool isValidIcon(const QIcon &icon)
594 {
595 if (!icon.isNull()) {
596 const auto availableSizes = icon.availableSizes();
597 return !availableSizes.isEmpty() && !availableSizes.constFirst().isEmpty();
598 }
599 return false;
600 }
601
loadCustomCategoryList() const602 WidgetBoxTreeWidget::CategoryList WidgetBoxTreeWidget::loadCustomCategoryList() const
603 {
604 CategoryList result;
605
606 const QDesignerPluginManager *pm = m_core->pluginManager();
607 const QDesignerPluginManager::CustomWidgetList customWidgets = pm->registeredCustomWidgets();
608 if (customWidgets.isEmpty())
609 return result;
610
611 static const QString customCatName = tr("Custom Widgets");
612
613 const QString invisible = QLatin1String(invisibleNameC);
614 const QString iconPrefix = QLatin1String(iconPrefixC);
615
616 for (QDesignerCustomWidgetInterface *c : customWidgets) {
617 const QString dom_xml = c->domXml();
618 if (dom_xml.isEmpty())
619 continue;
620
621 const QString pluginName = c->name();
622 const QDesignerCustomWidgetData data = pm->customWidgetData(c);
623 QString displayName = data.xmlDisplayName();
624 if (displayName.isEmpty())
625 displayName = pluginName;
626
627 QString cat_name = c->group();
628 if (cat_name.isEmpty())
629 cat_name = customCatName;
630 else if (cat_name == invisible)
631 continue;
632
633 int idx = findCategory(cat_name, result);
634 if (idx == -1) {
635 result.append(Category(cat_name));
636 idx = result.size() - 1;
637 }
638 Category &cat = result[idx];
639
640 const QIcon icon = c->icon();
641
642 QString icon_name;
643 if (isValidIcon(icon)) {
644 icon_name = iconPrefix;
645 icon_name += pluginName;
646 m_pluginIcons.insert(icon_name, icon);
647 }
648
649 cat.addWidget(Widget(displayName, dom_xml, icon_name, Widget::Custom));
650 }
651
652 return result;
653 }
654
adjustSubListSize(QTreeWidgetItem * cat_item)655 void WidgetBoxTreeWidget::adjustSubListSize(QTreeWidgetItem *cat_item)
656 {
657 QTreeWidgetItem *embedItem = cat_item->child(0);
658 if (embedItem == nullptr)
659 return;
660
661 WidgetBoxCategoryListView *list_widget = static_cast<WidgetBoxCategoryListView*>(itemWidget(embedItem, 0));
662 list_widget->setFixedWidth(header()->width());
663 list_widget->doItemsLayout();
664 const int height = qMax(list_widget->contentsSize().height() ,1);
665 list_widget->setFixedHeight(height);
666 embedItem->setSizeHint(0, QSize(-1, height - 1));
667 }
668
categoryCount() const669 int WidgetBoxTreeWidget::categoryCount() const
670 {
671 return topLevelItemCount();
672 }
673
category(int cat_idx) const674 WidgetBoxTreeWidget::Category WidgetBoxTreeWidget::category(int cat_idx) const
675 {
676 if (cat_idx >= topLevelItemCount())
677 return Category();
678
679 QTreeWidgetItem *cat_item = topLevelItem(cat_idx);
680
681 QTreeWidgetItem *embedItem = cat_item->child(0);
682 WidgetBoxCategoryListView *categoryView = static_cast<WidgetBoxCategoryListView*>(itemWidget(embedItem, 0));
683
684 Category result = categoryView->category();
685 result.setName(cat_item->text(0));
686
687 switch (topLevelRole(cat_item)) {
688 case SCRATCHPAD_ITEM:
689 result.setType(Category::Scratchpad);
690 break;
691 default:
692 result.setType(Category::Default);
693 break;
694 }
695 return result;
696 }
697
addCategory(const Category & cat)698 void WidgetBoxTreeWidget::addCategory(const Category &cat)
699 {
700 if (cat.widgetCount() == 0)
701 return;
702
703 const bool isScratchPad = cat.type() == Category::Scratchpad;
704 WidgetBoxCategoryListView *categoryView;
705 QTreeWidgetItem *cat_item;
706
707 if (isScratchPad) {
708 const int idx = ensureScratchpad();
709 categoryView = categoryViewAt(idx);
710 cat_item = topLevelItem(idx);
711 } else {
712 const int existingIndex = indexOfCategory(cat.name());
713 if (existingIndex == -1) {
714 cat_item = new QTreeWidgetItem();
715 cat_item->setText(0, cat.name());
716 setTopLevelRole(NORMAL_ITEM, cat_item);
717 // insert before scratchpad
718 const int scratchPadIndex = indexOfScratchpad();
719 if (scratchPadIndex == -1) {
720 addTopLevelItem(cat_item);
721 } else {
722 insertTopLevelItem(scratchPadIndex, cat_item);
723 }
724 cat_item->setExpanded(true);
725 categoryView = addCategoryView(cat_item, m_iconMode);
726 } else {
727 categoryView = categoryViewAt(existingIndex);
728 cat_item = topLevelItem(existingIndex);
729 }
730 }
731 // The same categories are read from the file $HOME, avoid duplicates
732 const int widgetCount = cat.widgetCount();
733 for (int i = 0; i < widgetCount; ++i) {
734 const Widget w = cat.widget(i);
735 if (!categoryView->containsWidget(w.name()))
736 categoryView->addWidget(w, iconForWidget(w.iconName()), isScratchPad);
737 }
738 adjustSubListSize(cat_item);
739 }
740
removeCategory(int cat_idx)741 void WidgetBoxTreeWidget::removeCategory(int cat_idx)
742 {
743 if (cat_idx >= topLevelItemCount())
744 return;
745 delete takeTopLevelItem(cat_idx);
746 }
747
widgetCount(int cat_idx) const748 int WidgetBoxTreeWidget::widgetCount(int cat_idx) const
749 {
750 if (cat_idx >= topLevelItemCount())
751 return 0;
752 // SDK functions want unfiltered access
753 return categoryViewAt(cat_idx)->count(WidgetBoxCategoryListView::UnfilteredAccess);
754 }
755
widget(int cat_idx,int wgt_idx) const756 WidgetBoxTreeWidget::Widget WidgetBoxTreeWidget::widget(int cat_idx, int wgt_idx) const
757 {
758 if (cat_idx >= topLevelItemCount())
759 return Widget();
760 // SDK functions want unfiltered access
761 WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);
762 return categoryView->widgetAt(WidgetBoxCategoryListView::UnfilteredAccess, wgt_idx);
763 }
764
addWidget(int cat_idx,const Widget & wgt)765 void WidgetBoxTreeWidget::addWidget(int cat_idx, const Widget &wgt)
766 {
767 if (cat_idx >= topLevelItemCount())
768 return;
769
770 QTreeWidgetItem *cat_item = topLevelItem(cat_idx);
771 WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);
772
773 const bool scratch = topLevelRole(cat_item) == SCRATCHPAD_ITEM;
774 categoryView->addWidget(wgt, iconForWidget(wgt.iconName()), scratch);
775 adjustSubListSize(cat_item);
776 }
777
removeWidget(int cat_idx,int wgt_idx)778 void WidgetBoxTreeWidget::removeWidget(int cat_idx, int wgt_idx)
779 {
780 if (cat_idx >= topLevelItemCount())
781 return;
782
783 WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);
784
785 // SDK functions want unfiltered access
786 const WidgetBoxCategoryListView::AccessMode am = WidgetBoxCategoryListView::UnfilteredAccess;
787 if (wgt_idx >= categoryView->count(am))
788 return;
789
790 categoryView->removeRow(am, wgt_idx);
791 }
792
slotScratchPadItemDeleted()793 void WidgetBoxTreeWidget::slotScratchPadItemDeleted()
794 {
795 const int scratch_idx = indexOfScratchpad();
796 QTreeWidgetItem *scratch_item = topLevelItem(scratch_idx);
797 adjustSubListSize(scratch_item);
798 save();
799 }
800
slotLastScratchPadItemDeleted()801 void WidgetBoxTreeWidget::slotLastScratchPadItemDeleted()
802 {
803 // Remove the scratchpad in the next idle loop
804 if (!m_scratchPadDeleteTimer) {
805 m_scratchPadDeleteTimer = new QTimer(this);
806 m_scratchPadDeleteTimer->setSingleShot(true);
807 m_scratchPadDeleteTimer->setInterval(0);
808 connect(m_scratchPadDeleteTimer, &QTimer::timeout,
809 this, &WidgetBoxTreeWidget::deleteScratchpad);
810 }
811 if (!m_scratchPadDeleteTimer->isActive())
812 m_scratchPadDeleteTimer->start();
813 }
814
deleteScratchpad()815 void WidgetBoxTreeWidget::deleteScratchpad()
816 {
817 const int idx = indexOfScratchpad();
818 if (idx == -1)
819 return;
820 delete takeTopLevelItem(idx);
821 save();
822 }
823
824
slotListMode()825 void WidgetBoxTreeWidget::slotListMode()
826 {
827 m_iconMode = false;
828 updateViewMode();
829 }
830
slotIconMode()831 void WidgetBoxTreeWidget::slotIconMode()
832 {
833 m_iconMode = true;
834 updateViewMode();
835 }
836
updateViewMode()837 void WidgetBoxTreeWidget::updateViewMode()
838 {
839 if (const int numTopLevels = topLevelItemCount()) {
840 for (int i = numTopLevels - 1; i >= 0; --i) {
841 QTreeWidgetItem *topLevel = topLevelItem(i);
842 // Scratch pad stays in list mode.
843 const QListView::ViewMode viewMode = m_iconMode && (topLevelRole(topLevel) != SCRATCHPAD_ITEM) ? QListView::IconMode : QListView::ListMode;
844 WidgetBoxCategoryListView *categoryView = categoryViewAt(i);
845 if (viewMode != categoryView->viewMode()) {
846 categoryView->setViewMode(viewMode);
847 adjustSubListSize(topLevelItem(i));
848 }
849 }
850 }
851
852 updateGeometries();
853 }
854
resizeEvent(QResizeEvent * e)855 void WidgetBoxTreeWidget::resizeEvent(QResizeEvent *e)
856 {
857 QTreeWidget::resizeEvent(e);
858 if (const int numTopLevels = topLevelItemCount()) {
859 for (int i = numTopLevels - 1; i >= 0; --i)
860 adjustSubListSize(topLevelItem(i));
861 }
862 }
863
contextMenuEvent(QContextMenuEvent * e)864 void WidgetBoxTreeWidget::contextMenuEvent(QContextMenuEvent *e)
865 {
866 QTreeWidgetItem *item = itemAt(e->pos());
867
868 const bool scratchpad_menu = item != nullptr
869 && item->parent() != nullptr
870 && topLevelRole(item->parent()) == SCRATCHPAD_ITEM;
871
872 QMenu menu;
873 menu.addAction(tr("Expand all"), this, &WidgetBoxTreeWidget::expandAll);
874 menu.addAction(tr("Collapse all"), this, &WidgetBoxTreeWidget::collapseAll);
875 menu.addSeparator();
876
877 QAction *listModeAction = menu.addAction(tr("List View"));
878 QAction *iconModeAction = menu.addAction(tr("Icon View"));
879 listModeAction->setCheckable(true);
880 iconModeAction->setCheckable(true);
881 QActionGroup *viewModeGroup = new QActionGroup(&menu);
882 viewModeGroup->addAction(listModeAction);
883 viewModeGroup->addAction(iconModeAction);
884 if (m_iconMode)
885 iconModeAction->setChecked(true);
886 else
887 listModeAction->setChecked(true);
888 connect(listModeAction, &QAction::triggered, this, &WidgetBoxTreeWidget::slotListMode);
889 connect(iconModeAction, &QAction::triggered, this, &WidgetBoxTreeWidget::slotIconMode);
890
891 if (scratchpad_menu) {
892 menu.addSeparator();
893 WidgetBoxCategoryListView *listView = qobject_cast<WidgetBoxCategoryListView *>(itemWidget(item, 0));
894 Q_ASSERT(listView);
895 menu.addAction(tr("Remove"), listView, &WidgetBoxCategoryListView::removeCurrentItem);
896 if (!m_iconMode)
897 menu.addAction(tr("Edit name"), listView, &WidgetBoxCategoryListView::editCurrentItem);
898 }
899 e->accept();
900 menu.exec(mapToGlobal(e->pos()));
901 }
902
dropWidgets(const QList<QDesignerDnDItemInterface * > & item_list)903 void WidgetBoxTreeWidget::dropWidgets(const QList<QDesignerDnDItemInterface*> &item_list)
904 {
905 QTreeWidgetItem *scratch_item = nullptr;
906 WidgetBoxCategoryListView *categoryView = nullptr;
907 bool added = false;
908
909 for (QDesignerDnDItemInterface *item : item_list) {
910 QWidget *w = item->widget();
911 if (w == nullptr)
912 continue;
913
914 DomUI *dom_ui = item->domUi();
915 if (dom_ui == nullptr)
916 continue;
917
918 const int scratch_idx = ensureScratchpad();
919 scratch_item = topLevelItem(scratch_idx);
920 categoryView = categoryViewAt(scratch_idx);
921
922 // Temporarily remove the fake toplevel in-between
923 DomWidget *fakeTopLevel = dom_ui->takeElementWidget();
924 DomWidget *firstWidget = nullptr;
925 if (fakeTopLevel && !fakeTopLevel->elementWidget().isEmpty()) {
926 firstWidget = fakeTopLevel->elementWidget().constFirst();
927 dom_ui->setElementWidget(firstWidget);
928 } else {
929 dom_ui->setElementWidget(fakeTopLevel);
930 continue;
931 }
932
933 // Serialize to XML
934 QString xml;
935 {
936 QXmlStreamWriter writer(&xml);
937 writer.setAutoFormatting(true);
938 writer.setAutoFormattingIndent(1);
939 writer.writeStartDocument();
940 dom_ui->write(writer);
941 writer.writeEndDocument();
942 }
943
944 // Insert fake toplevel again
945 dom_ui->takeElementWidget();
946 dom_ui->setElementWidget(fakeTopLevel);
947
948 const Widget wgt = Widget(w->objectName(), xml);
949 categoryView->addWidget(wgt, iconForWidget(wgt.iconName()), true);
950 scratch_item->setExpanded(true);
951 added = true;
952 }
953
954 if (added) {
955 save();
956 QApplication::setActiveWindow(this);
957 // Is the new item visible in filtered mode?
958 const WidgetBoxCategoryListView::AccessMode am = WidgetBoxCategoryListView::FilteredAccess;
959 if (const int count = categoryView->count(am))
960 categoryView->setCurrentItem(am, count - 1);
961 categoryView->adjustSize(); // XXX
962 adjustSubListSize(scratch_item);
963 }
964 }
965
filter(const QString & f)966 void WidgetBoxTreeWidget::filter(const QString &f)
967 {
968 const bool empty = f.isEmpty();
969 QRegExp re = empty ? QRegExp() : QRegExp(f, Qt::CaseInsensitive, QRegExp::FixedString);
970 const int numTopLevels = topLevelItemCount();
971 bool changed = false;
972 for (int i = 0; i < numTopLevels; i++) {
973 QTreeWidgetItem *tl = topLevelItem(i);
974 WidgetBoxCategoryListView *categoryView = categoryViewAt(i);
975 // Anything changed? -> Enable the category
976 const int oldCount = categoryView->count(WidgetBoxCategoryListView::FilteredAccess);
977 categoryView->filter(re);
978 const int newCount = categoryView->count(WidgetBoxCategoryListView::FilteredAccess);
979 if (oldCount != newCount) {
980 changed = true;
981 const bool categoryEnabled = newCount > 0 || empty;
982 if (categoryEnabled) {
983 categoryView->adjustSize();
984 adjustSubListSize(tl);
985 }
986 setRowHidden (i, QModelIndex(), !categoryEnabled);
987 }
988 }
989 if (changed)
990 updateGeometries();
991 }
992
993 } // namespace qdesigner_internal
994
995 QT_END_NAMESPACE
996