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

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