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 | |
59 | static const char *widgetBoxRootElementC = "widgetbox" ; |
60 | static const char *widgetElementC = "widget" ; |
61 | static const char *uiElementC = "ui" ; |
62 | static const char *categoryElementC = "category" ; |
63 | static const char *categoryEntryElementC = "categoryentry" ; |
64 | static const char *nameAttributeC = "name" ; |
65 | static const char *typeAttributeC = "type" ; |
66 | static const char *iconAttributeC = "icon" ; |
67 | static const char *defaultTypeValueC = "default" ; |
68 | static const char *customValueC = "custom" ; |
69 | static const char *iconPrefixC = "__qt_icon__" ; |
70 | static const char *scratchPadValueC = "scratchpad" ; |
71 | static const char *invisibleNameC = "[invisible]" ; |
72 | |
73 | enum TopLevelRole { NORMAL_ITEM, SCRATCHPAD_ITEM, CUSTOM_ITEM }; |
74 | |
75 | QT_BEGIN_NAMESPACE |
76 | |
77 | static void setTopLevelRole(TopLevelRole tlr, QTreeWidgetItem *item) |
78 | { |
79 | item->setData(column: 0, role: Qt::UserRole, value: QVariant(tlr)); |
80 | } |
81 | |
82 | static TopLevelRole topLevelRole(const QTreeWidgetItem *item) |
83 | { |
84 | return static_cast<TopLevelRole>(item->data(column: 0, role: Qt::UserRole).toInt()); |
85 | } |
86 | |
87 | namespace qdesigner_internal { |
88 | |
89 | WidgetBoxTreeWidget::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 | |
110 | QIcon 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 | |
123 | WidgetBoxCategoryListView *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 | |
133 | static const char widgetBoxSettingsGroupC[] = "WidgetBox" ; |
134 | static const char widgetBoxExpandedKeyC[] = "Closed categories" ; |
135 | static const char widgetBoxViewModeKeyC[] = "View mode" ; |
136 | |
137 | void 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 | |
154 | void 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 | |
176 | WidgetBoxTreeWidget::~WidgetBoxTreeWidget() |
177 | { |
178 | saveExpandedState(); |
179 | } |
180 | |
181 | void WidgetBoxTreeWidget::setFileName(const QString &file_name) |
182 | { |
183 | m_file_name = file_name; |
184 | } |
185 | |
186 | QString WidgetBoxTreeWidget::fileName() const |
187 | { |
188 | return m_file_name; |
189 | } |
190 | |
191 | bool 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 | |
215 | void WidgetBoxTreeWidget::slotSave() |
216 | { |
217 | save(); |
218 | } |
219 | |
220 | void 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 | |
234 | int 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 | |
247 | WidgetBoxCategoryListView *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 | |
265 | int 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 | |
276 | int 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 | |
286 | bool 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 | |
318 | bool 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 | |
336 | void 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 | |
352 | static 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 | |
358 | bool 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 | */ |
471 | bool 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 | |
532 | void 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 | |
591 | static 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 | |
602 | static 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 | |
611 | WidgetBoxTreeWidget::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 | |
664 | void 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 | |
678 | int WidgetBoxTreeWidget::categoryCount() const |
679 | { |
680 | return topLevelItemCount(); |
681 | } |
682 | |
683 | WidgetBoxTreeWidget::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 | |
707 | void 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 | |
750 | void WidgetBoxTreeWidget::removeCategory(int cat_idx) |
751 | { |
752 | if (cat_idx >= topLevelItemCount()) |
753 | return; |
754 | delete takeTopLevelItem(index: cat_idx); |
755 | } |
756 | |
757 | int 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 | |
765 | WidgetBoxTreeWidget::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 | |
774 | void 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 | |
787 | void 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 | |
802 | void 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 | |
810 | void 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 | |
824 | void WidgetBoxTreeWidget::deleteScratchpad() |
825 | { |
826 | const int idx = indexOfScratchpad(); |
827 | if (idx == -1) |
828 | return; |
829 | delete takeTopLevelItem(index: idx); |
830 | save(); |
831 | } |
832 | |
833 | |
834 | void WidgetBoxTreeWidget::slotListMode() |
835 | { |
836 | m_iconMode = false; |
837 | updateViewMode(); |
838 | } |
839 | |
840 | void WidgetBoxTreeWidget::slotIconMode() |
841 | { |
842 | m_iconMode = true; |
843 | updateViewMode(); |
844 | } |
845 | |
846 | void 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 | |
864 | void 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 | |
873 | void WidgetBoxTreeWidget::(QContextMenuEvent *e) |
874 | { |
875 | QTreeWidgetItem *item = itemAt(p: e->pos()); |
876 | |
877 | const bool = item != nullptr |
878 | && item->parent() != nullptr |
879 | && topLevelRole(item: item->parent()) == SCRATCHPAD_ITEM; |
880 | |
881 | QMenu ; |
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 | |
912 | void 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 | |
975 | void 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 | |
1004 | QT_END_NAMESPACE |
1005 | |