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 "itemlisteditor.h" |
30 | #include <abstractformbuilder.h> |
31 | #include <iconloader_p.h> |
32 | #include <formwindowbase_p.h> |
33 | #include <designerpropertymanager.h> |
34 | |
35 | #include <QtDesigner/abstractformwindow.h> |
36 | |
37 | #include <qttreepropertybrowser.h> |
38 | |
39 | #include <QtWidgets/qsplitter.h> |
40 | #include <QtCore/qcoreapplication.h> |
41 | |
42 | QT_BEGIN_NAMESPACE |
43 | |
44 | namespace qdesigner_internal { |
45 | |
46 | class ItemPropertyBrowser : public QtTreePropertyBrowser |
47 | { |
48 | public: |
49 | ItemPropertyBrowser() |
50 | { |
51 | setResizeMode(Interactive); |
52 | //: Sample string to determinate the width for the first column of the list item property browser |
53 | const QString widthSampleString = QCoreApplication::translate(context: "ItemPropertyBrowser" , key: "XX Icon Selected off" ); |
54 | m_width = fontMetrics().horizontalAdvance(widthSampleString); |
55 | setSplitterPosition(m_width); |
56 | m_width += fontMetrics().horizontalAdvance(QStringLiteral("/this/is/some/random/path" )); |
57 | } |
58 | |
59 | QSize sizeHint() const override |
60 | { |
61 | return QSize(m_width, 1); |
62 | } |
63 | |
64 | private: |
65 | int m_width; |
66 | }; |
67 | |
68 | ////////////////// Item editor /////////////// |
69 | AbstractItemEditor::AbstractItemEditor(QDesignerFormWindowInterface *form, QWidget *parent) |
70 | : QWidget(parent), |
71 | m_iconCache(qobject_cast<FormWindowBase *>(object: form)->iconCache()) |
72 | { |
73 | setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); |
74 | m_propertyManager = new DesignerPropertyManager(form->core(), this); |
75 | m_editorFactory = new DesignerEditorFactory(form->core(), this); |
76 | m_editorFactory->setSpacing(0); |
77 | m_propertyBrowser = new ItemPropertyBrowser; |
78 | m_propertyBrowser->setFactoryForManager(manager: static_cast<QtVariantPropertyManager *>(m_propertyManager), |
79 | factory: m_editorFactory); |
80 | |
81 | connect(sender: m_editorFactory, signal: &DesignerEditorFactory::resetProperty, |
82 | receiver: this, slot: &AbstractItemEditor::resetProperty); |
83 | connect(sender: m_propertyManager, signal: &DesignerPropertyManager::valueChanged, |
84 | receiver: this, slot: &AbstractItemEditor::propertyChanged); |
85 | connect(sender: iconCache(), signal: &DesignerIconCache::reloaded, receiver: this, slot: &AbstractItemEditor::cacheReloaded); |
86 | } |
87 | |
88 | AbstractItemEditor::~AbstractItemEditor() |
89 | { |
90 | m_propertyBrowser->unsetFactoryForManager(manager: m_propertyManager); |
91 | } |
92 | |
93 | static const char * const itemFlagNames[] = { |
94 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "Selectable" ), |
95 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "Editable" ), |
96 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "DragEnabled" ), |
97 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "DropEnabled" ), |
98 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "UserCheckable" ), |
99 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "Enabled" ), |
100 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "Tristate" ), |
101 | nullptr |
102 | }; |
103 | |
104 | static const char * const checkStateNames[] = { |
105 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "Unchecked" ), |
106 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "PartiallyChecked" ), |
107 | QT_TRANSLATE_NOOP("AbstractItemEditor" , "Checked" ), |
108 | nullptr |
109 | }; |
110 | |
111 | static QStringList c2qStringList(const char * const in[]) |
112 | { |
113 | QStringList out; |
114 | for (int i = 0; in[i]; i++) |
115 | out << AbstractItemEditor::tr(s: in[i]); |
116 | return out; |
117 | } |
118 | |
119 | void AbstractItemEditor::setupProperties(const PropertyDefinition *propList, |
120 | Qt::Alignment alignDefault) |
121 | { |
122 | for (int i = 0; propList[i].name; i++) { |
123 | int type = propList[i].typeFunc ? propList[i].typeFunc() : propList[i].type; |
124 | int role = propList[i].role; |
125 | QtVariantProperty *prop = m_propertyManager->addProperty(propertyType: type, name: QLatin1String(propList[i].name)); |
126 | if (role == Qt::TextAlignmentRole) { |
127 | prop->setAttribute(attribute: DesignerPropertyManager::alignDefaultAttribute(), |
128 | value: QVariant(uint(alignDefault))); |
129 | } |
130 | Q_ASSERT(prop); |
131 | if (role == Qt::ToolTipPropertyRole || role == Qt::WhatsThisPropertyRole) |
132 | prop->setAttribute(QStringLiteral("validationMode" ), value: ValidationRichText); |
133 | else if (role == Qt::DisplayPropertyRole) |
134 | prop->setAttribute(QStringLiteral("validationMode" ), value: ValidationMultiLine); |
135 | else if (role == Qt::StatusTipPropertyRole) |
136 | prop->setAttribute(QStringLiteral("validationMode" ), value: ValidationSingleLine); |
137 | else if (role == ItemFlagsShadowRole) |
138 | prop->setAttribute(QStringLiteral("flagNames" ), value: c2qStringList(in: itemFlagNames)); |
139 | else if (role == Qt::CheckStateRole) |
140 | prop->setAttribute(QStringLiteral("enumNames" ), value: c2qStringList(in: checkStateNames)); |
141 | prop->setAttribute(QStringLiteral("resettable" ), value: true); |
142 | m_properties.append(t: prop); |
143 | m_rootProperties.append(t: prop); |
144 | m_propertyToRole.insert(akey: prop, avalue: role); |
145 | } |
146 | } |
147 | |
148 | void AbstractItemEditor::setupObject(QWidget *object) |
149 | { |
150 | m_propertyManager->setObject(object); |
151 | QDesignerFormWindowInterface *formWindow = QDesignerFormWindowInterface::findFormWindow(w: object); |
152 | FormWindowBase *fwb = qobject_cast<FormWindowBase *>(object: formWindow); |
153 | m_editorFactory->setFormWindowBase(fwb); |
154 | } |
155 | |
156 | void AbstractItemEditor::setupEditor(QWidget *object, |
157 | const PropertyDefinition *propList, |
158 | Qt::Alignment alignDefault) |
159 | { |
160 | setupProperties(propList, alignDefault); |
161 | setupObject(object); |
162 | } |
163 | |
164 | void AbstractItemEditor::propertyChanged(QtProperty *property) |
165 | { |
166 | if (m_updatingBrowser) |
167 | return; |
168 | |
169 | |
170 | BoolBlocker block(m_updatingBrowser); |
171 | QtVariantProperty *prop = m_propertyManager->variantProperty(property); |
172 | int role; |
173 | if ((role = m_propertyToRole.value(akey: prop, adefaultValue: -1)) == -1) |
174 | // Subproperty |
175 | return; |
176 | |
177 | if ((role == ItemFlagsShadowRole && prop->value().toInt() == defaultItemFlags()) |
178 | || (role == Qt::DecorationPropertyRole && !qvariant_cast<PropertySheetIconValue>(v: prop->value()).mask()) |
179 | || (role == Qt::FontRole && !qvariant_cast<QFont>(v: prop->value()).resolve())) { |
180 | prop->setModified(false); |
181 | setItemData(role, v: QVariant()); |
182 | } else { |
183 | prop->setModified(true); |
184 | setItemData(role, v: prop->value()); |
185 | } |
186 | |
187 | switch (role) { |
188 | case Qt::DecorationPropertyRole: |
189 | setItemData(role: Qt::DecorationRole, v: QVariant::fromValue(value: iconCache()->icon(value: qvariant_cast<PropertySheetIconValue>(v: prop->value())))); |
190 | break; |
191 | case Qt::DisplayPropertyRole: |
192 | setItemData(role: Qt::EditRole, v: QVariant::fromValue(value: qvariant_cast<PropertySheetStringValue>(v: prop->value()).value())); |
193 | break; |
194 | case Qt::ToolTipPropertyRole: |
195 | setItemData(role: Qt::ToolTipRole, v: QVariant::fromValue(value: qvariant_cast<PropertySheetStringValue>(v: prop->value()).value())); |
196 | break; |
197 | case Qt::StatusTipPropertyRole: |
198 | setItemData(role: Qt::StatusTipRole, v: QVariant::fromValue(value: qvariant_cast<PropertySheetStringValue>(v: prop->value()).value())); |
199 | break; |
200 | case Qt::WhatsThisPropertyRole: |
201 | setItemData(role: Qt::WhatsThisRole, v: QVariant::fromValue(value: qvariant_cast<PropertySheetStringValue>(v: prop->value()).value())); |
202 | break; |
203 | default: |
204 | break; |
205 | } |
206 | |
207 | prop->setValue(getItemData(role)); |
208 | } |
209 | |
210 | void AbstractItemEditor::resetProperty(QtProperty *property) |
211 | { |
212 | if (m_propertyManager->resetFontSubProperty(property)) |
213 | return; |
214 | |
215 | if (m_propertyManager->resetIconSubProperty(subProperty: property)) |
216 | return; |
217 | |
218 | if (m_propertyManager->resetTextAlignmentProperty(property)) |
219 | return; |
220 | |
221 | BoolBlocker block(m_updatingBrowser); |
222 | |
223 | QtVariantProperty *prop = m_propertyManager->variantProperty(property); |
224 | int role = m_propertyToRole.value(akey: prop); |
225 | if (role == ItemFlagsShadowRole) |
226 | prop->setValue(QVariant::fromValue(value: defaultItemFlags())); |
227 | else |
228 | prop->setValue(QVariant(prop->valueType(), nullptr)); |
229 | prop->setModified(false); |
230 | |
231 | setItemData(role, v: QVariant()); |
232 | if (role == Qt::DecorationPropertyRole) |
233 | setItemData(role: Qt::DecorationRole, v: QVariant::fromValue(value: QIcon())); |
234 | if (role == Qt::DisplayPropertyRole) |
235 | setItemData(role: Qt::EditRole, v: QVariant::fromValue(value: QString())); |
236 | if (role == Qt::ToolTipPropertyRole) |
237 | setItemData(role: Qt::ToolTipRole, v: QVariant::fromValue(value: QString())); |
238 | if (role == Qt::StatusTipPropertyRole) |
239 | setItemData(role: Qt::StatusTipRole, v: QVariant::fromValue(value: QString())); |
240 | if (role == Qt::WhatsThisPropertyRole) |
241 | setItemData(role: Qt::WhatsThisRole, v: QVariant::fromValue(value: QString())); |
242 | } |
243 | |
244 | void AbstractItemEditor::cacheReloaded() |
245 | { |
246 | BoolBlocker block(m_updatingBrowser); |
247 | m_propertyManager->reloadResourceProperties(); |
248 | } |
249 | |
250 | void AbstractItemEditor::updateBrowser() |
251 | { |
252 | BoolBlocker block(m_updatingBrowser); |
253 | for (QtVariantProperty *prop : qAsConst(t&: m_properties)) { |
254 | int role = m_propertyToRole.value(akey: prop); |
255 | QVariant val = getItemData(role); |
256 | |
257 | bool modified = false; |
258 | if (!val.isValid()) { |
259 | if (role == ItemFlagsShadowRole) |
260 | val = QVariant::fromValue(value: defaultItemFlags()); |
261 | else |
262 | val = QVariant(int(prop->value().userType()), nullptr); |
263 | } else { |
264 | modified = role != Qt::TextAlignmentRole |
265 | || val.toUInt() != DesignerPropertyManager::alignDefault(prop); |
266 | } |
267 | prop->setModified(modified); |
268 | prop->setValue(val); |
269 | } |
270 | |
271 | if (m_propertyBrowser->topLevelItems().isEmpty()) { |
272 | for (QtVariantProperty *prop : qAsConst(t&: m_rootProperties)) |
273 | m_propertyBrowser->addProperty(property: prop); |
274 | } |
275 | } |
276 | |
277 | void AbstractItemEditor::injectPropertyBrowser(QWidget *parent, QWidget *widget) |
278 | { |
279 | // It is impossible to design a splitter with just one widget, so we do it by hand. |
280 | m_propertySplitter = new QSplitter; |
281 | m_propertySplitter->addWidget(widget); |
282 | m_propertySplitter->addWidget(widget: m_propertyBrowser); |
283 | m_propertySplitter->setStretchFactor(index: 0, stretch: 1); |
284 | m_propertySplitter->setStretchFactor(index: 1, stretch: 0); |
285 | parent->layout()->addWidget(w: m_propertySplitter); |
286 | } |
287 | |
288 | ////////////////// List editor /////////////// |
289 | ItemListEditor::ItemListEditor(QDesignerFormWindowInterface *form, QWidget *parent) |
290 | : AbstractItemEditor(form, parent), |
291 | m_updating(false) |
292 | { |
293 | ui.setupUi(this); |
294 | |
295 | injectPropertyBrowser(parent: this, widget: ui.widget); |
296 | connect(sender: ui.showPropertiesButton, signal: &QAbstractButton::clicked, |
297 | receiver: this, slot: &ItemListEditor::togglePropertyBrowser); |
298 | setPropertyBrowserVisible(false); |
299 | |
300 | QIcon upIcon = createIconSet(name: QString::fromUtf8(str: "up.png" )); |
301 | QIcon downIcon = createIconSet(name: QString::fromUtf8(str: "down.png" )); |
302 | QIcon minusIcon = createIconSet(name: QString::fromUtf8(str: "minus.png" )); |
303 | QIcon plusIcon = createIconSet(name: QString::fromUtf8(str: "plus.png" )); |
304 | ui.moveListItemUpButton->setIcon(upIcon); |
305 | ui.moveListItemDownButton->setIcon(downIcon); |
306 | ui.newListItemButton->setIcon(plusIcon); |
307 | ui.deleteListItemButton->setIcon(minusIcon); |
308 | |
309 | connect(sender: iconCache(), signal: &DesignerIconCache::reloaded, receiver: this, slot: &AbstractItemEditor::cacheReloaded); |
310 | } |
311 | |
312 | void ItemListEditor::setupEditor(QWidget *object, |
313 | const PropertyDefinition *propList, |
314 | Qt::Alignment alignDefault) |
315 | { |
316 | AbstractItemEditor::setupEditor(object, propList, alignDefault); |
317 | |
318 | if (ui.listWidget->count() > 0) |
319 | ui.listWidget->setCurrentRow(0); |
320 | else |
321 | updateEditor(); |
322 | } |
323 | |
324 | void ItemListEditor::setCurrentIndex(int idx) |
325 | { |
326 | m_updating = true; |
327 | ui.listWidget->setCurrentRow(idx); |
328 | m_updating = false; |
329 | } |
330 | |
331 | void ItemListEditor::on_newListItemButton_clicked() |
332 | { |
333 | int row = ui.listWidget->currentRow() + 1; |
334 | |
335 | QListWidgetItem *item = new QListWidgetItem(m_newItemText); |
336 | item->setData(role: Qt::DisplayPropertyRole, value: QVariant::fromValue(value: PropertySheetStringValue(m_newItemText))); |
337 | if (m_alignDefault != 0) |
338 | item->setTextAlignment(Qt::Alignment(m_alignDefault)); |
339 | item->setFlags(item->flags() | Qt::ItemIsEditable); |
340 | if (row < ui.listWidget->count()) |
341 | ui.listWidget->insertItem(row, item); |
342 | else |
343 | ui.listWidget->addItem(aitem: item); |
344 | emit itemInserted(idx: row); |
345 | |
346 | ui.listWidget->setCurrentItem(item); |
347 | ui.listWidget->editItem(item); |
348 | } |
349 | |
350 | void ItemListEditor::on_deleteListItemButton_clicked() |
351 | { |
352 | int row = ui.listWidget->currentRow(); |
353 | |
354 | if (row != -1) { |
355 | delete ui.listWidget->takeItem(row); |
356 | emit itemDeleted(idx: row); |
357 | } |
358 | |
359 | if (row == ui.listWidget->count()) |
360 | row--; |
361 | if (row < 0) |
362 | updateEditor(); |
363 | else |
364 | ui.listWidget->setCurrentRow(row); |
365 | } |
366 | |
367 | void ItemListEditor::on_moveListItemUpButton_clicked() |
368 | { |
369 | int row = ui.listWidget->currentRow(); |
370 | if (row <= 0) |
371 | return; // nothing to do |
372 | |
373 | ui.listWidget->insertItem(row: row - 1, item: ui.listWidget->takeItem(row)); |
374 | ui.listWidget->setCurrentRow(row - 1); |
375 | emit itemMovedUp(idx: row); |
376 | } |
377 | |
378 | void ItemListEditor::on_moveListItemDownButton_clicked() |
379 | { |
380 | int row = ui.listWidget->currentRow(); |
381 | if (row == -1 || row == ui.listWidget->count() - 1) |
382 | return; // nothing to do |
383 | |
384 | ui.listWidget->insertItem(row: row + 1, item: ui.listWidget->takeItem(row)); |
385 | ui.listWidget->setCurrentRow(row + 1); |
386 | emit itemMovedDown(idx: row); |
387 | } |
388 | |
389 | void ItemListEditor::on_listWidget_currentRowChanged() |
390 | { |
391 | updateEditor(); |
392 | if (!m_updating) |
393 | emit indexChanged(idx: ui.listWidget->currentRow()); |
394 | } |
395 | |
396 | void ItemListEditor::on_listWidget_itemChanged(QListWidgetItem *item) |
397 | { |
398 | if (m_updatingBrowser) |
399 | return; |
400 | |
401 | PropertySheetStringValue val = qvariant_cast<PropertySheetStringValue>(v: item->data(role: Qt::DisplayPropertyRole)); |
402 | val.setValue(item->text()); |
403 | BoolBlocker block(m_updatingBrowser); |
404 | item->setData(role: Qt::DisplayPropertyRole, value: QVariant::fromValue(value: val)); |
405 | |
406 | // The checkState could change, too, but if this signal is connected, |
407 | // checkState is not in the list anyway, as we are editing a header item. |
408 | emit itemChanged(idx: ui.listWidget->currentRow(), role: Qt::DisplayPropertyRole, |
409 | v: QVariant::fromValue(value: val)); |
410 | updateBrowser(); |
411 | } |
412 | |
413 | void ItemListEditor::togglePropertyBrowser() |
414 | { |
415 | setPropertyBrowserVisible(!m_propertyBrowser->isVisible()); |
416 | } |
417 | |
418 | void ItemListEditor::setPropertyBrowserVisible(bool v) |
419 | { |
420 | ui.showPropertiesButton->setText(v ? tr(s: "Properties &>>" ) : tr(s: "Properties &<<" )); |
421 | m_propertyBrowser->setVisible(v); |
422 | } |
423 | |
424 | void ItemListEditor::setItemData(int role, const QVariant &v) |
425 | { |
426 | QListWidgetItem *item = ui.listWidget->currentItem(); |
427 | bool reLayout = false; |
428 | if ((role == Qt::EditRole && (v.toString().count(c: QLatin1Char('\n')) != item->data(role).toString().count(c: QLatin1Char('\n')))) |
429 | || role == Qt::FontRole) |
430 | reLayout = true; |
431 | QVariant newValue = v; |
432 | if (role == Qt::FontRole && newValue.type() == QVariant::Font) { |
433 | QFont oldFont = ui.listWidget->font(); |
434 | QFont newFont = qvariant_cast<QFont>(v: newValue).resolve(oldFont); |
435 | newValue = QVariant::fromValue(value: newFont); |
436 | item->setData(role, value: QVariant()); // force the right font with the current resolve mask is set (item view bug) |
437 | } |
438 | item->setData(role, value: newValue); |
439 | if (reLayout) |
440 | ui.listWidget->doItemsLayout(); |
441 | emit itemChanged(idx: ui.listWidget->currentRow(), role, v: newValue); |
442 | } |
443 | |
444 | QVariant ItemListEditor::getItemData(int role) const |
445 | { |
446 | return ui.listWidget->currentItem()->data(role); |
447 | } |
448 | |
449 | int ItemListEditor::defaultItemFlags() const |
450 | { |
451 | static const int flags = QListWidgetItem().flags(); |
452 | return flags; |
453 | } |
454 | |
455 | void ItemListEditor::cacheReloaded() |
456 | { |
457 | reloadIconResources(iconCache: iconCache(), object: ui.listWidget); |
458 | } |
459 | |
460 | void ItemListEditor::updateEditor() |
461 | { |
462 | bool currentItemEnabled = false; |
463 | |
464 | bool moveRowUpEnabled = false; |
465 | bool moveRowDownEnabled = false; |
466 | |
467 | QListWidgetItem *item = ui.listWidget->currentItem(); |
468 | if (item) { |
469 | currentItemEnabled = true; |
470 | int currentRow = ui.listWidget->currentRow(); |
471 | if (currentRow > 0) |
472 | moveRowUpEnabled = true; |
473 | if (currentRow < ui.listWidget->count() - 1) |
474 | moveRowDownEnabled = true; |
475 | } |
476 | |
477 | ui.moveListItemUpButton->setEnabled(moveRowUpEnabled); |
478 | ui.moveListItemDownButton->setEnabled(moveRowDownEnabled); |
479 | ui.deleteListItemButton->setEnabled(currentItemEnabled); |
480 | |
481 | if (item) |
482 | updateBrowser(); |
483 | else |
484 | m_propertyBrowser->clear(); |
485 | } |
486 | |
487 | uint ItemListEditor::alignDefault() const |
488 | { |
489 | return m_alignDefault; |
490 | } |
491 | |
492 | void ItemListEditor::setAlignDefault(uint newAlignDefault) |
493 | { |
494 | m_alignDefault = newAlignDefault; |
495 | } |
496 | } // namespace qdesigner_internal |
497 | |
498 | QT_END_NAMESPACE |
499 | |