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
58static const char *widgetBoxRootElementC = "widgetbox";
59static const char *widgetElementC = "widget";
60static const char *uiElementC = "ui";
61static const char *categoryElementC = "category";
62static const char *categoryEntryElementC = "categoryentry";
63static const char *nameAttributeC = "name";
64static const char *typeAttributeC = "type";
65static const char *iconAttributeC = "icon";
66static const char *defaultTypeValueC = "default";
67static const char *customValueC = "custom";
68static const char *iconPrefixC = "__qt_icon__";
69static const char *scratchPadValueC = "scratchpad";
70static const char *invisibleNameC = "[invisible]";
71
72enum TopLevelRole { NORMAL_ITEM, SCRATCHPAD_ITEM, CUSTOM_ITEM };
73
74QT_BEGIN_NAMESPACE
75
76static void setTopLevelRole(TopLevelRole tlr, QTreeWidgetItem *item)
77{
78 item->setData(0, Qt::UserRole, QVariant(tlr));
79}
80
81static TopLevelRole topLevelRole(const QTreeWidgetItem *item)
82{
83 return static_cast<TopLevelRole>(item->data(0, Qt::UserRole).toInt());
84}
85
86namespace qdesigner_internal {
87
88WidgetBoxTreeWidget::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
109QIcon 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
122WidgetBoxCategoryListView *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
132static const char widgetBoxSettingsGroupC[] = "WidgetBox";
133static const char widgetBoxExpandedKeyC[] = "Closed categories";
134static const char widgetBoxViewModeKeyC[] = "View mode";
135
136void 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
153void 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.empty())
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
175WidgetBoxTreeWidget::~WidgetBoxTreeWidget()
176{
177 saveExpandedState();
178}
179
180void WidgetBoxTreeWidget::setFileName(const QString &file_name)
181{
182 m_file_name = file_name;
183}
184
185QString WidgetBoxTreeWidget::fileName() const
186{
187 return m_file_name;
188}
189
190bool 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
214void WidgetBoxTreeWidget::slotSave()
215{
216 save();
217}
218
219void 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
233int 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
246WidgetBoxCategoryListView *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
264int 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
275int 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
285bool 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
309bool 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
327void 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
343static 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
349bool 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 QStringRef 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 QStringRef 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 */
462bool 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 QStringRef 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
523void 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
582static 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
593static inline bool isValidIcon(const QIcon &icon)
594{
595 if (!icon.isNull()) {
596 const QList<QSize> availableSizes = icon.availableSizes();
597 if (!availableSizes.empty())
598 return !availableSizes.front().isEmpty();
599 }
600 return false;
601}
602
603WidgetBoxTreeWidget::CategoryList WidgetBoxTreeWidget::loadCustomCategoryList() const
604{
605 CategoryList result;
606
607 const QDesignerPluginManager *pm = m_core->pluginManager();
608 const QDesignerPluginManager::CustomWidgetList customWidgets = pm->registeredCustomWidgets();
609 if (customWidgets.empty())
610 return result;
611
612 static const QString customCatName = tr("Custom Widgets");
613
614 const QString invisible = QLatin1String(invisibleNameC);
615 const QString iconPrefix = QLatin1String(iconPrefixC);
616
617 for (QDesignerCustomWidgetInterface *c : customWidgets) {
618 const QString dom_xml = c->domXml();
619 if (dom_xml.isEmpty())
620 continue;
621
622 const QString pluginName = c->name();
623 const QDesignerCustomWidgetData data = pm->customWidgetData(c);
624 QString displayName = data.xmlDisplayName();
625 if (displayName.isEmpty())
626 displayName = pluginName;
627
628 QString cat_name = c->group();
629 if (cat_name.isEmpty())
630 cat_name = customCatName;
631 else if (cat_name == invisible)
632 continue;
633
634 int idx = findCategory(cat_name, result);
635 if (idx == -1) {
636 result.append(Category(cat_name));
637 idx = result.size() - 1;
638 }
639 Category &cat = result[idx];
640
641 const QIcon icon = c->icon();
642
643 QString icon_name;
644 if (isValidIcon(icon)) {
645 icon_name = iconPrefix;
646 icon_name += pluginName;
647 m_pluginIcons.insert(icon_name, icon);
648 }
649
650 cat.addWidget(Widget(displayName, dom_xml, icon_name, Widget::Custom));
651 }
652
653 return result;
654}
655
656void WidgetBoxTreeWidget::adjustSubListSize(QTreeWidgetItem *cat_item)
657{
658 QTreeWidgetItem *embedItem = cat_item->child(0);
659 if (embedItem == nullptr)
660 return;
661
662 WidgetBoxCategoryListView *list_widget = static_cast<WidgetBoxCategoryListView*>(itemWidget(embedItem, 0));
663 list_widget->setFixedWidth(header()->width());
664 list_widget->doItemsLayout();
665 const int height = qMax(list_widget->contentsSize().height() ,1);
666 list_widget->setFixedHeight(height);
667 embedItem->setSizeHint(0, QSize(-1, height - 1));
668}
669
670int WidgetBoxTreeWidget::categoryCount() const
671{
672 return topLevelItemCount();
673}
674
675WidgetBoxTreeWidget::Category WidgetBoxTreeWidget::category(int cat_idx) const
676{
677 if (cat_idx >= topLevelItemCount())
678 return Category();
679
680 QTreeWidgetItem *cat_item = topLevelItem(cat_idx);
681
682 QTreeWidgetItem *embedItem = cat_item->child(0);
683 WidgetBoxCategoryListView *categoryView = static_cast<WidgetBoxCategoryListView*>(itemWidget(embedItem, 0));
684
685 Category result = categoryView->category();
686 result.setName(cat_item->text(0));
687
688 switch (topLevelRole(cat_item)) {
689 case SCRATCHPAD_ITEM:
690 result.setType(Category::Scratchpad);
691 break;
692 default:
693 result.setType(Category::Default);
694 break;
695 }
696 return result;
697}
698
699void WidgetBoxTreeWidget::addCategory(const Category &cat)
700{
701 if (cat.widgetCount() == 0)
702 return;
703
704 const bool isScratchPad = cat.type() == Category::Scratchpad;
705 WidgetBoxCategoryListView *categoryView;
706 QTreeWidgetItem *cat_item;
707
708 if (isScratchPad) {
709 const int idx = ensureScratchpad();
710 categoryView = categoryViewAt(idx);
711 cat_item = topLevelItem(idx);
712 } else {
713 const int existingIndex = indexOfCategory(cat.name());
714 if (existingIndex == -1) {
715 cat_item = new QTreeWidgetItem();
716 cat_item->setText(0, cat.name());
717 setTopLevelRole(NORMAL_ITEM, cat_item);
718 // insert before scratchpad
719 const int scratchPadIndex = indexOfScratchpad();
720 if (scratchPadIndex == -1) {
721 addTopLevelItem(cat_item);
722 } else {
723 insertTopLevelItem(scratchPadIndex, cat_item);
724 }
725 cat_item->setExpanded(true);
726 categoryView = addCategoryView(cat_item, m_iconMode);
727 } else {
728 categoryView = categoryViewAt(existingIndex);
729 cat_item = topLevelItem(existingIndex);
730 }
731 }
732 // The same categories are read from the file $HOME, avoid duplicates
733 const int widgetCount = cat.widgetCount();
734 for (int i = 0; i < widgetCount; ++i) {
735 const Widget w = cat.widget(i);
736 if (!categoryView->containsWidget(w.name()))
737 categoryView->addWidget(w, iconForWidget(w.iconName()), isScratchPad);
738 }
739 adjustSubListSize(cat_item);
740}
741
742void WidgetBoxTreeWidget::removeCategory(int cat_idx)
743{
744 if (cat_idx >= topLevelItemCount())
745 return;
746 delete takeTopLevelItem(cat_idx);
747}
748
749int WidgetBoxTreeWidget::widgetCount(int cat_idx) const
750{
751 if (cat_idx >= topLevelItemCount())
752 return 0;
753 // SDK functions want unfiltered access
754 return categoryViewAt(cat_idx)->count(WidgetBoxCategoryListView::UnfilteredAccess);
755}
756
757WidgetBoxTreeWidget::Widget WidgetBoxTreeWidget::widget(int cat_idx, int wgt_idx) const
758{
759 if (cat_idx >= topLevelItemCount())
760 return Widget();
761 // SDK functions want unfiltered access
762 WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);
763 return categoryView->widgetAt(WidgetBoxCategoryListView::UnfilteredAccess, wgt_idx);
764}
765
766void WidgetBoxTreeWidget::addWidget(int cat_idx, const Widget &wgt)
767{
768 if (cat_idx >= topLevelItemCount())
769 return;
770
771 QTreeWidgetItem *cat_item = topLevelItem(cat_idx);
772 WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);
773
774 const bool scratch = topLevelRole(cat_item) == SCRATCHPAD_ITEM;
775 categoryView->addWidget(wgt, iconForWidget(wgt.iconName()), scratch);
776 adjustSubListSize(cat_item);
777}
778
779void WidgetBoxTreeWidget::removeWidget(int cat_idx, int wgt_idx)
780{
781 if (cat_idx >= topLevelItemCount())
782 return;
783
784 WidgetBoxCategoryListView *categoryView = categoryViewAt(cat_idx);
785
786 // SDK functions want unfiltered access
787 const WidgetBoxCategoryListView::AccessMode am = WidgetBoxCategoryListView::UnfilteredAccess;
788 if (wgt_idx >= categoryView->count(am))
789 return;
790
791 categoryView->removeRow(am, wgt_idx);
792}
793
794void WidgetBoxTreeWidget::slotScratchPadItemDeleted()
795{
796 const int scratch_idx = indexOfScratchpad();
797 QTreeWidgetItem *scratch_item = topLevelItem(scratch_idx);
798 adjustSubListSize(scratch_item);
799 save();
800}
801
802void WidgetBoxTreeWidget::slotLastScratchPadItemDeleted()
803{
804 // Remove the scratchpad in the next idle loop
805 if (!m_scratchPadDeleteTimer) {
806 m_scratchPadDeleteTimer = new QTimer(this);
807 m_scratchPadDeleteTimer->setSingleShot(true);
808 m_scratchPadDeleteTimer->setInterval(0);
809 connect(m_scratchPadDeleteTimer, &QTimer::timeout,
810 this, &WidgetBoxTreeWidget::deleteScratchpad);
811 }
812 if (!m_scratchPadDeleteTimer->isActive())
813 m_scratchPadDeleteTimer->start();
814}
815
816void WidgetBoxTreeWidget::deleteScratchpad()
817{
818 const int idx = indexOfScratchpad();
819 if (idx == -1)
820 return;
821 delete takeTopLevelItem(idx);
822 save();
823}
824
825
826void WidgetBoxTreeWidget::slotListMode()
827{
828 m_iconMode = false;
829 updateViewMode();
830}
831
832void WidgetBoxTreeWidget::slotIconMode()
833{
834 m_iconMode = true;
835 updateViewMode();
836}
837
838void WidgetBoxTreeWidget::updateViewMode()
839{
840 if (const int numTopLevels = topLevelItemCount()) {
841 for (int i = numTopLevels - 1; i >= 0; --i) {
842 QTreeWidgetItem *topLevel = topLevelItem(i);
843 // Scratch pad stays in list mode.
844 const QListView::ViewMode viewMode = m_iconMode && (topLevelRole(topLevel) != SCRATCHPAD_ITEM) ? QListView::IconMode : QListView::ListMode;
845 WidgetBoxCategoryListView *categoryView = categoryViewAt(i);
846 if (viewMode != categoryView->viewMode()) {
847 categoryView->setViewMode(viewMode);
848 adjustSubListSize(topLevelItem(i));
849 }
850 }
851 }
852
853 updateGeometries();
854}
855
856void WidgetBoxTreeWidget::resizeEvent(QResizeEvent *e)
857{
858 QTreeWidget::resizeEvent(e);
859 if (const int numTopLevels = topLevelItemCount()) {
860 for (int i = numTopLevels - 1; i >= 0; --i)
861 adjustSubListSize(topLevelItem(i));
862 }
863}
864
865void WidgetBoxTreeWidget::contextMenuEvent(QContextMenuEvent *e)
866{
867 QTreeWidgetItem *item = itemAt(e->pos());
868
869 const bool scratchpad_menu = item != nullptr
870 && item->parent() != nullptr
871 && topLevelRole(item->parent()) == SCRATCHPAD_ITEM;
872
873 QMenu menu;
874 menu.addAction(tr("Expand all"), this, &WidgetBoxTreeWidget::expandAll);
875 menu.addAction(tr("Collapse all"), this, &WidgetBoxTreeWidget::collapseAll);
876 menu.addSeparator();
877
878 QAction *listModeAction = menu.addAction(tr("List View"));
879 QAction *iconModeAction = menu.addAction(tr("Icon View"));
880 listModeAction->setCheckable(true);
881 iconModeAction->setCheckable(true);
882 QActionGroup *viewModeGroup = new QActionGroup(&menu);
883 viewModeGroup->addAction(listModeAction);
884 viewModeGroup->addAction(iconModeAction);
885 if (m_iconMode)
886 iconModeAction->setChecked(true);
887 else
888 listModeAction->setChecked(true);
889 connect(listModeAction, &QAction::triggered, this, &WidgetBoxTreeWidget::slotListMode);
890 connect(iconModeAction, &QAction::triggered, this, &WidgetBoxTreeWidget::slotIconMode);
891
892 if (scratchpad_menu) {
893 menu.addSeparator();
894 WidgetBoxCategoryListView *listView = qobject_cast<WidgetBoxCategoryListView *>(itemWidget(item, 0));
895 Q_ASSERT(listView);
896 menu.addAction(tr("Remove"), listView, &WidgetBoxCategoryListView::removeCurrentItem);
897 if (!m_iconMode)
898 menu.addAction(tr("Edit name"), listView, &WidgetBoxCategoryListView::editCurrentItem);
899 }
900 e->accept();
901 menu.exec(mapToGlobal(e->pos()));
902}
903
904void WidgetBoxTreeWidget::dropWidgets(const QList<QDesignerDnDItemInterface*> &item_list)
905{
906 QTreeWidgetItem *scratch_item = nullptr;
907 WidgetBoxCategoryListView *categoryView = nullptr;
908 bool added = false;
909
910 for (QDesignerDnDItemInterface *item : item_list) {
911 QWidget *w = item->widget();
912 if (w == nullptr)
913 continue;
914
915 DomUI *dom_ui = item->domUi();
916 if (dom_ui == nullptr)
917 continue;
918
919 const int scratch_idx = ensureScratchpad();
920 scratch_item = topLevelItem(scratch_idx);
921 categoryView = categoryViewAt(scratch_idx);
922
923 // Temporarily remove the fake toplevel in-between
924 DomWidget *fakeTopLevel = dom_ui->takeElementWidget();
925 DomWidget *firstWidget = nullptr;
926 if (fakeTopLevel && !fakeTopLevel->elementWidget().isEmpty()) {
927 firstWidget = fakeTopLevel->elementWidget().constFirst();
928 dom_ui->setElementWidget(firstWidget);
929 } else {
930 dom_ui->setElementWidget(fakeTopLevel);
931 continue;
932 }
933
934 // Serialize to XML
935 QString xml;
936 {
937 QXmlStreamWriter writer(&xml);
938 writer.setAutoFormatting(true);
939 writer.setAutoFormattingIndent(1);
940 writer.writeStartDocument();
941 dom_ui->write(writer);
942 writer.writeEndDocument();
943 }
944
945 // Insert fake toplevel again
946 dom_ui->takeElementWidget();
947 dom_ui->setElementWidget(fakeTopLevel);
948
949 const Widget wgt = Widget(w->objectName(), xml);
950 categoryView->addWidget(wgt, iconForWidget(wgt.iconName()), true);
951 scratch_item->setExpanded(true);
952 added = true;
953 }
954
955 if (added) {
956 save();
957 QApplication::setActiveWindow(this);
958 // Is the new item visible in filtered mode?
959 const WidgetBoxCategoryListView::AccessMode am = WidgetBoxCategoryListView::FilteredAccess;
960 if (const int count = categoryView->count(am))
961 categoryView->setCurrentItem(am, count - 1);
962 categoryView->adjustSize(); // XXX
963 adjustSubListSize(scratch_item);
964 }
965}
966
967void WidgetBoxTreeWidget::filter(const QString &f)
968{
969 const bool empty = f.isEmpty();
970 QRegExp re = empty ? QRegExp() : QRegExp(f, Qt::CaseInsensitive, QRegExp::FixedString);
971 const int numTopLevels = topLevelItemCount();
972 bool changed = false;
973 for (int i = 0; i < numTopLevels; i++) {
974 QTreeWidgetItem *tl = topLevelItem(i);
975 WidgetBoxCategoryListView *categoryView = categoryViewAt(i);
976 // Anything changed? -> Enable the category
977 const int oldCount = categoryView->count(WidgetBoxCategoryListView::FilteredAccess);
978 categoryView->filter(re);
979 const int newCount = categoryView->count(WidgetBoxCategoryListView::FilteredAccess);
980 if (oldCount != newCount) {
981 changed = true;
982 const bool categoryEnabled = newCount > 0 || empty;
983 if (categoryEnabled) {
984 categoryView->adjustSize();
985 adjustSubListSize(tl);
986 }
987 setRowHidden (i, QModelIndex(), !categoryEnabled);
988 }
989 }
990 if (changed)
991 updateGeometries();
992}
993
994} // namespace qdesigner_internal
995
996QT_END_NAMESPACE
997