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