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

source code of qttools/src/designer/src/components/widgetbox/widgetboxtreewidget.cpp