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 | |
35 | static const char widgetBoxRootElementC[] = "widgetbox" ; |
36 | static const char wbWidgetElementC[] = "widget" ; |
37 | static const char uiElementC[] = "ui" ; |
38 | static const char categoryElementC[] = "category" ; |
39 | static const char categoryEntryElementC[] = "categoryentry" ; |
40 | static const char wbNameAttributeC[] = "name" ; |
41 | static const char typeAttributeC[] = "type" ; |
42 | static const char iconAttributeC[] = "icon" ; |
43 | static const char defaultTypeValueC[] = "default" ; |
44 | static const char customValueC[] = "custom" ; |
45 | static const char iconPrefixC[] = "__qt_icon__" ; |
46 | static const char scratchPadValueC[] = "scratchpad" ; |
47 | static const char invisibleNameC[] = "[invisible]" ; |
48 | |
49 | enum TopLevelRole { NORMAL_ITEM, SCRATCHPAD_ITEM, CUSTOM_ITEM }; |
50 | |
51 | QT_BEGIN_NAMESPACE |
52 | |
53 | using namespace Qt::StringLiterals; |
54 | |
55 | static void setTopLevelRole(TopLevelRole tlr, QTreeWidgetItem *item) |
56 | { |
57 | item->setData(column: 0, role: Qt::UserRole, value: QVariant(tlr)); |
58 | } |
59 | |
60 | static TopLevelRole topLevelRole(const QTreeWidgetItem *item) |
61 | { |
62 | return static_cast<TopLevelRole>(item->data(column: 0, role: Qt::UserRole).toInt()); |
63 | } |
64 | |
65 | namespace qdesigner_internal { |
66 | |
67 | WidgetBoxTreeWidget::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 | |
88 | QIcon 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 | |
101 | WidgetBoxCategoryListView *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 | |
111 | static const char widgetBoxSettingsGroupC[] = "WidgetBox" ; |
112 | static const char widgetBoxExpandedKeyC[] = "Closed categories" ; |
113 | static const char widgetBoxViewModeKeyC[] = "View mode" ; |
114 | |
115 | void 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 | |
132 | void 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 | |
154 | WidgetBoxTreeWidget::~WidgetBoxTreeWidget() |
155 | { |
156 | saveExpandedState(); |
157 | } |
158 | |
159 | void WidgetBoxTreeWidget::setFileName(const QString &file_name) |
160 | { |
161 | m_file_name = file_name; |
162 | } |
163 | |
164 | QString WidgetBoxTreeWidget::fileName() const |
165 | { |
166 | return m_file_name; |
167 | } |
168 | |
169 | bool 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 | |
193 | void WidgetBoxTreeWidget::slotSave() |
194 | { |
195 | save(); |
196 | } |
197 | |
198 | void 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 | |
212 | int 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 | |
225 | WidgetBoxCategoryListView *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 | |
243 | int 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 | |
254 | int 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 | |
264 | bool 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 | |
296 | bool 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 | |
314 | void 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 | |
329 | static 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 | |
335 | bool 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 | */ |
448 | bool 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 | |
508 | void 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 | |
567 | static 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 | |
578 | static 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 | |
587 | WidgetBoxTreeWidget::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 | |
640 | void 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 | |
654 | int WidgetBoxTreeWidget::categoryCount() const |
655 | { |
656 | return topLevelItemCount(); |
657 | } |
658 | |
659 | WidgetBoxTreeWidget::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 | |
683 | void 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 | |
726 | void WidgetBoxTreeWidget::removeCategory(int cat_idx) |
727 | { |
728 | if (cat_idx >= topLevelItemCount()) |
729 | return; |
730 | delete takeTopLevelItem(index: cat_idx); |
731 | } |
732 | |
733 | int 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 | |
741 | WidgetBoxTreeWidget::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 | |
750 | void 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 | |
763 | void 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 | |
778 | void 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 | |
786 | void 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 | |
800 | void WidgetBoxTreeWidget::deleteScratchpad() |
801 | { |
802 | const int idx = indexOfScratchpad(); |
803 | if (idx == -1) |
804 | return; |
805 | delete takeTopLevelItem(index: idx); |
806 | save(); |
807 | } |
808 | |
809 | |
810 | void WidgetBoxTreeWidget::slotListMode() |
811 | { |
812 | m_iconMode = false; |
813 | updateViewMode(); |
814 | } |
815 | |
816 | void WidgetBoxTreeWidget::slotIconMode() |
817 | { |
818 | m_iconMode = true; |
819 | updateViewMode(); |
820 | } |
821 | |
822 | void 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 | |
840 | void 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 | |
849 | void WidgetBoxTreeWidget::(QContextMenuEvent *e) |
850 | { |
851 | QTreeWidgetItem *item = itemAt(p: e->pos()); |
852 | |
853 | const bool = item != nullptr |
854 | && item->parent() != nullptr |
855 | && topLevelRole(item: item->parent()) == SCRATCHPAD_ITEM; |
856 | |
857 | QMenu ; |
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 | |
888 | void 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 | |
953 | void 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 | |
981 | QT_END_NAMESPACE |
982 | |