QtCanpool 3.3.0
载入中...
搜索中...
未找到
qxpropertyeditor.cpp
浏览该文件的文档.
1
5#include "qxpropertyeditor.h"
7
8#include <QCheckBox>
9#include <QColorDialog>
10#include <QComboBox>
11#include <QDoubleSpinBox>
12#include <QFileDialog>
13#include <QFontDialog>
14#include <QFormLayout>
15#include <QGroupBox>
16#include <QHBoxLayout>
17#include <QLabel>
18#include <QLineEdit>
19#include <QPushButton>
20#include <QSpinBox>
21#include <QTextEdit>
22#include <QVBoxLayout>
23
25
33class QxPropertyColorButton : public QPushButton
34{
35 Q_OBJECT
36public:
37 explicit QxPropertyColorButton(QWidget *parent = Q_NULLPTR)
38 : QPushButton(parent)
39 {
40 QObject::connect(this, &QAbstractButton::clicked, this, &QxPropertyColorButton::chooseColor);
41 }
42
43 QColor color() const
44 {
45 return m_color;
46 }
47 void setColor(const QColor &color)
48 {
49 if (m_color == color) {
50 return;
51 }
52 m_color = color;
53 apply();
54 Q_EMIT colorChanged(m_color);
55 }
56
57Q_SIGNALS:
58 void colorChanged(const QColor &color);
59private:
60 void chooseColor()
61 {
62 const QColor picked = QColorDialog::getColor(m_color, this, tr("Choose a colour"));
63 if (picked.isValid()) {
64 setColor(picked);
65 }
66 }
67
68 void apply()
69 {
70 QPixmap swatch(20, 12);
71 swatch.fill(m_color.isValid() ? m_color : Qt::transparent);
72 setIcon(swatch);
73 setText(m_color.isValid() ? m_color.name() : tr("(none)"));
74 }
75
76 QColor m_color;
77};
78
80class QxPropertyFontButton : public QPushButton
81{
82 Q_OBJECT
83public:
84 explicit QxPropertyFontButton(QWidget *parent = Q_NULLPTR)
85 : QPushButton(parent)
86 {
87 QObject::connect(this, &QAbstractButton::clicked, this, &QxPropertyFontButton::chooseFont);
88 }
89
90 QFont currentFont() const
91 {
92 return m_font;
93 }
94 void setCurrentFont(const QFont &font)
95 {
96 if (m_font == font) {
97 return;
98 }
99 m_font = font;
100 apply();
101 Q_EMIT fontChanged(m_font);
102 }
103
104Q_SIGNALS:
105 void fontChanged(const QFont &font);
106private:
107 void chooseFont()
108 {
109 bool accepted = false;
110 const QFont picked = QFontDialog::getFont(&accepted, m_font, this, tr("Choose a font"));
111 if (accepted) {
112 setCurrentFont(picked);
113 }
114 }
115
116 void apply()
117 {
118 setText(QStringLiteral("%1, %2").arg(m_font.family()).arg(m_font.pointSize()));
119 setFont(m_font);
120 }
121
122 QFont m_font;
123};
124
125namespace
126{
127
129const int kIntMinimum = -1000000000;
130const int kIntMaximum = 1000000000;
131const double kDoubleMinimum = -1.0e12;
132const double kDoubleMaximum = 1.0e12;
134const int kDoubleDecimals = 3;
135
137QString labelFor(const QxProperty &property)
138{
139 return property.label.isEmpty() ? property.key : property.label;
140}
141
143QString browsePath(const QString &start, QxProperty::PathMode mode, QWidget *parent)
144{
145 switch (mode) {
146 case QxProperty::Directory:
147 return QFileDialog::getExistingDirectory(parent, QObject::tr("Choose a folder"), start);
148 case QxProperty::AnyFile:
149 return QFileDialog::getSaveFileName(parent, QObject::tr("Choose a file"), start);
150 case QxProperty::ExistingFile:
151 break;
152 }
153 return QFileDialog::getOpenFileName(parent, QObject::tr("Choose a file"), start);
154}
155
164QVariant normalize(const QVariant &value, QxProperty::Type type)
165{
166 switch (type) {
167 case QxProperty::Bool:
168 return value.isValid() ? QVariant(value.toBool()) : QVariant();
169 case QxProperty::Int: {
170 bool ok = false;
171 const int number = value.toInt(&ok);
172 return ok ? QVariant(number) : QVariant();
173 }
174 case QxProperty::Double: {
175 bool ok = false;
176 const double number = value.toDouble(&ok);
177 return ok ? QVariant(number) : QVariant();
178 }
179 case QxProperty::String:
180 case QxProperty::Text:
181 case QxProperty::Enum:
182 case QxProperty::Path:
183 return value.isValid() ? QVariant(value.toString()) : QVariant();
184 case QxProperty::Color: {
185 // Either the value already is a colour, or it is a string the way a
186 // configuration file writes one - "#rrggbb", or a name.
187 QColor color = value.canConvert<QColor>() ? qvariant_cast<QColor>(value) : QColor();
188 if (!color.isValid()) {
189 color = QColor(value.toString());
190 }
191 return color.isValid() ? QVariant::fromValue(color) : QVariant();
192 }
193 case QxProperty::Font: {
194 if (value.canConvert<QFont>()) {
195 return QVariant::fromValue(qvariant_cast<QFont>(value));
196 }
197 QFont font;
198 if (value.isValid() && font.fromString(value.toString())) {
199 return QVariant::fromValue(font);
200 }
201 return QVariant();
202 }
203 }
204 return value;
205}
206
207} // namespace
208
209QxPropertyEditorPrivate::QxPropertyEditorPrivate() = default;
210
211void QxPropertyEditorPrivate::init()
212{
213 Q_Q(QxPropertyEditor);
214
215 m_layout = new QVBoxLayout(q);
216 m_layout->setContentsMargins(0, 0, 0, 0);
217
218 m_form = new QFormLayout();
219 m_layout->addLayout(m_form);
220 // Everything is packed at the top: an editor that stretches its rows over
221 // the whole dialog is harder to read than one that leaves room below.
222 m_layout->addStretch(1);
223}
224
225int QxPropertyEditorPrivate::indexOf(const QString &key) const
226{
227 for (int i = 0; i < m_rows.count(); ++i) {
228 if (m_rows.at(i).property.key == key) {
229 return i;
230 }
231 }
232 return -1;
233}
234
235QWidget *QxPropertyEditorPrivate::createField(const QxProperty &property, QWidget **input)
236{
237 Q_Q(QxPropertyEditor);
238
239 switch (property.type) {
240 case QxProperty::Bool: {
241 // A check box carries its own text, so it spans both columns rather than
242 // getting a label it would only repeat.
243 QCheckBox *check = new QCheckBox(labelFor(property), q);
244 *input = check;
245 return check;
246 }
247 case QxProperty::Int: {
248 QSpinBox *spin = new QSpinBox(q);
249 spin->setRange(kIntMinimum, kIntMaximum);
250 *input = spin;
251 return spin;
252 }
253 case QxProperty::Double: {
254 QDoubleSpinBox *spin = new QDoubleSpinBox(q);
255 spin->setRange(kDoubleMinimum, kDoubleMaximum);
256 spin->setDecimals(kDoubleDecimals);
257 *input = spin;
258 return spin;
259 }
260 case QxProperty::String: {
261 QLineEdit *line = new QLineEdit(q);
262 if (property.readOnly) {
263 line->setReadOnly(true);
264 }
265 *input = line;
266 return line;
267 }
268 case QxProperty::Text: {
269 QTextEdit *edit = new QTextEdit(q);
270 if (property.readOnly) {
271 edit->setReadOnly(true);
272 }
273 *input = edit;
274 return edit;
275 }
276 case QxProperty::Enum: {
277 QComboBox *combo = new QComboBox(q);
278 combo->addItems(property.choices);
279 *input = combo;
280 return combo;
281 }
282 case QxProperty::Color: {
283 QxPropertyColorButton *button = new QxPropertyColorButton(q);
284 *input = button;
285 return button;
286 }
287 case QxProperty::Font: {
288 QxPropertyFontButton *button = new QxPropertyFontButton(q);
289 *input = button;
290 return button;
291 }
292 case QxProperty::Path: {
293 // The only type that needs a container: the line edit is the input, the
294 // button beside it only fills it in.
295 QWidget *field = new QWidget(q);
296 QHBoxLayout *layout = new QHBoxLayout(field);
297 layout->setContentsMargins(0, 0, 0, 0);
298 QLineEdit *line = new QLineEdit(field);
299 layout->addWidget(line, 1);
300 QPushButton *browse = new QPushButton(QStringLiteral("..."), field);
301 browse->setFixedWidth(browse->fontMetrics().boundingRect(QStringLiteral("...")).width() + 24);
302 layout->addWidget(browse);
303 QObject::connect(browse, &QAbstractButton::clicked, q, [line, property]() {
304 const QString path = browsePath(line->text(), property.pathMode, line);
305 if (!path.isEmpty()) {
306 line->setText(path);
307 }
308 });
309 *input = line;
310 return field;
311 }
312 }
313 return Q_NULLPTR;
314}
315
316void QxPropertyEditorPrivate::insertRow(QFormLayout *form, const QxProperty &property)
317{
318 if (!form) {
319 return;
320 }
321
322 QWidget *input = Q_NULLPTR;
323 QWidget *field = createField(property, &input);
324 if (!field) {
325 qWarning("QxPropertyEditor: '%s' has a type there is no control for", qPrintable(property.key));
326 return;
327 }
328
329 if (property.type == QxProperty::Bool) {
330 form->addRow(field);
331 } else {
332 form->addRow(labelFor(property), field);
333 }
334
335 Row row;
336 row.property = property;
337 row.property.value = normalize(property.value.isValid() ? property.value : property.defaultValue, property.type);
338 row.field = field;
339 row.input = input;
340 m_rows.append(row);
341
342 field->setEnabled(!property.readOnly);
343 field->setToolTip(property.tooltip);
344
345 // Filled before the control is listened to, so the starting value is not
346 // reported as an edit.
347 m_updating = true;
348 writeField(row, row.property.value);
349 m_updating = false;
350
351 if (!m_initial.contains(property.key)) {
352 m_initial.insert(property.key, row.property.value);
353 }
354 connectField(m_rows.count() - 1);
355}
356
357void QxPropertyEditorPrivate::connectField(int index)
358{
359 Q_Q(QxPropertyEditor);
360
361 const Row row = m_rows.at(index);
362 if (!row.input) {
363 return;
364 }
365
366 // Whatever the control reports, the row is re-read from the control and
367 // stored. The guard keeps the form from hearing the value it just wrote.
368 auto commit = [this, index]() {
369 if (m_updating) {
370 return;
371 }
372 setRowValue(index, readField(m_rows.at(index)));
373 };
374
375 switch (row.property.type) {
376 case QxProperty::Bool:
377 QObject::connect(qobject_cast<QCheckBox *>(row.input), &QCheckBox::toggled, q, commit);
378 break;
379 case QxProperty::Int:
380 QObject::connect(qobject_cast<QSpinBox *>(row.input), QOverload<int>::of(&QSpinBox::valueChanged), q, commit);
381 break;
382 case QxProperty::Double:
383 QObject::connect(qobject_cast<QDoubleSpinBox *>(row.input),
384 QOverload<double>::of(&QDoubleSpinBox::valueChanged), q, commit);
385 break;
386 case QxProperty::String:
387 case QxProperty::Path:
388 QObject::connect(qobject_cast<QLineEdit *>(row.input), &QLineEdit::textChanged, q, commit);
389 break;
390 case QxProperty::Text:
391 QObject::connect(qobject_cast<QTextEdit *>(row.input), &QTextEdit::textChanged, q, commit);
392 break;
393 case QxProperty::Enum:
394 QObject::connect(qobject_cast<QComboBox *>(row.input), &QComboBox::currentTextChanged, q, commit);
395 break;
396 case QxProperty::Color:
397 // Not clicked(): the dialog runs after the click, so the colour only
398 // exists once the button says it changed.
399 QObject::connect(qobject_cast<QxPropertyColorButton *>(row.input), &QxPropertyColorButton::colorChanged, q,
400 commit);
401 break;
402 case QxProperty::Font:
403 QObject::connect(qobject_cast<QxPropertyFontButton *>(row.input), &QxPropertyFontButton::fontChanged, q,
404 commit);
405 break;
406 }
407}
408
409QVariant QxPropertyEditorPrivate::readField(const Row &row) const
410{
411 if (!row.input) {
412 return QVariant();
413 }
414
415 switch (row.property.type) {
416 case QxProperty::Bool:
417 return qobject_cast<QCheckBox *>(row.input)->isChecked();
418 case QxProperty::Int:
419 return qobject_cast<QSpinBox *>(row.input)->value();
420 case QxProperty::Double:
421 return qobject_cast<QDoubleSpinBox *>(row.input)->value();
422 case QxProperty::String:
423 case QxProperty::Path:
424 return qobject_cast<QLineEdit *>(row.input)->text();
425 case QxProperty::Text:
426 return qobject_cast<QTextEdit *>(row.input)->toPlainText();
427 case QxProperty::Enum:
428 return qobject_cast<QComboBox *>(row.input)->currentText();
429 case QxProperty::Color:
430 return QVariant::fromValue(qobject_cast<QxPropertyColorButton *>(row.input)->color());
431 case QxProperty::Font:
432 return QVariant::fromValue(qobject_cast<QxPropertyFontButton *>(row.input)->currentFont());
433 }
434 return QVariant();
435}
436
437void QxPropertyEditorPrivate::writeField(const Row &row, const QVariant &value)
438{
439 if (!row.input) {
440 return;
441 }
442
443 switch (row.property.type) {
444 case QxProperty::Bool:
445 qobject_cast<QCheckBox *>(row.input)->setChecked(value.toBool());
446 break;
447 case QxProperty::Int:
448 qobject_cast<QSpinBox *>(row.input)->setValue(value.toInt());
449 break;
450 case QxProperty::Double:
451 qobject_cast<QDoubleSpinBox *>(row.input)->setValue(value.toDouble());
452 break;
453 case QxProperty::String:
454 case QxProperty::Path: {
455 QLineEdit *line = qobject_cast<QLineEdit *>(row.input);
456 if (line->text() != value.toString()) {
457 line->setText(value.toString());
458 }
459 break;
460 }
461 case QxProperty::Text: {
462 QTextEdit *edit = qobject_cast<QTextEdit *>(row.input);
463 if (edit->toPlainText() != value.toString()) {
464 edit->setPlainText(value.toString());
465 }
466 break;
467 }
468 case QxProperty::Enum: {
469 QComboBox *combo = qobject_cast<QComboBox *>(row.input);
470 const int index = combo->findText(value.toString());
471 if (index >= 0) {
472 combo->setCurrentIndex(index);
473 break;
474 }
475 // A value that came out of a configuration file is shown even when the
476 // list of choices moved on without it: silently dropping it would make
477 // the editor lie about what is stored.
478 if (!value.toString().isEmpty()) {
479 combo->addItem(value.toString());
480 combo->setCurrentIndex(combo->count() - 1);
481 }
482 break;
483 }
484 case QxProperty::Color:
485 qobject_cast<QxPropertyColorButton *>(row.input)->setColor(qvariant_cast<QColor>(value));
486 break;
487 case QxProperty::Font:
488 qobject_cast<QxPropertyFontButton *>(row.input)->setCurrentFont(qvariant_cast<QFont>(value));
489 break;
490 }
491}
492
493void QxPropertyEditorPrivate::setRowValue(int index, const QVariant &value)
494{
495 Q_Q(QxPropertyEditor);
496
497 if (index < 0 || index >= m_rows.count()) {
498 return;
499 }
500
501 Row &row = m_rows[index];
502 if (row.property.value == value) {
503 return;
504 }
505
506 row.property.value = value;
507 m_updating = true;
508 writeField(row, value);
509 m_updating = false;
510
511 Q_EMIT q->valueChanged(row.property.key, value);
512 updateModified();
513}
514
515void QxPropertyEditorPrivate::updateModified()
516{
517 Q_Q(QxPropertyEditor);
518
519 bool modified = false;
520 for (const Row &row : m_rows) {
521 if (m_initial.value(row.property.key) != row.property.value) {
522 modified = true;
523 break;
524 }
525 }
526
527 if (modified == m_modified) {
528 return;
529 }
530 m_modified = modified;
531 Q_EMIT q->modifiedChanged(m_modified);
532}
533
534QxPropertyEditor::QxPropertyEditor(QWidget *parent)
535 : QWidget(parent)
536 , d_ptr(new QxPropertyEditorPrivate())
537{
538 Q_D(QxPropertyEditor);
539 d->setPublic(this);
540 d->init();
541}
542
547
549{
550 Q_D(const QxPropertyEditor);
551 return d->m_rows.count();
552}
553
554QStringList QxPropertyEditor::keys() const
555{
556 Q_D(const QxPropertyEditor);
557 QStringList keys;
558 for (const QxPropertyEditorPrivate::Row &row : d->m_rows) {
559 keys.append(row.property.key);
560 }
561 return keys;
562}
563
564bool QxPropertyEditor::contains(const QString &key) const
565{
566 Q_D(const QxPropertyEditor);
567 return d->indexOf(key) >= 0;
568}
569
571{
572 Q_D(QxPropertyEditor);
573
574 if (property.key.isEmpty()) {
575 qWarning("QxPropertyEditor: a property needs a non-empty key");
576 return;
577 }
578 if (d->indexOf(property.key) >= 0) {
579 qWarning("QxPropertyEditor: a property named '%s' already exists", qPrintable(property.key));
580 return;
581 }
582
583 d->insertRow(d->m_form, property);
584}
585
586void QxPropertyEditor::addGroup(const QString &title, const QList<QxProperty> &properties)
587{
588 Q_D(QxPropertyEditor);
589
590 QGroupBox *group = new QGroupBox(title, this);
591 QFormLayout *form = new QFormLayout(group);
592 // Before the stretch, so the section keeps the room at the bottom free.
593 d->m_layout->insertWidget(d->m_layout->count() - 1, group);
594
595 // Everything added from here lands in this section until the next group.
596 d->m_form = form;
597 for (const QxProperty &property : properties) {
598 addProperty(property);
599 }
600}
601
602QVariant QxPropertyEditor::value(const QString &key) const
603{
604 Q_D(const QxPropertyEditor);
605 const int index = d->indexOf(key);
606 if (index < 0) {
607 return QVariant();
608 }
609 return d->m_rows.at(index).property.value;
610}
611
612void QxPropertyEditor::setValue(const QString &key, const QVariant &value)
613{
614 Q_D(QxPropertyEditor);
615
616 const int index = d->indexOf(key);
617 if (index < 0) {
618 qWarning("QxPropertyEditor: there is no property named '%s'", qPrintable(key));
619 return;
620 }
621
622 const QVariant normalized = normalize(value, d->m_rows.at(index).property.type);
623 if (value.isValid() && !normalized.isValid()) {
624 qWarning("QxPropertyEditor: '%s' cannot hold the value it was given", qPrintable(key));
625 return;
626 }
627 d->setRowValue(index, normalized);
628}
629
630void QxPropertyEditor::setAllValues(const QHash<QString, QVariant> &values)
631{
632 Q_D(QxPropertyEditor);
633
634 // A key that is not a property here is a typo or a stale entry. Reporting it
635 // beats keeping it: an unknown key would be silently dropped from the form.
636 for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
637 const int index = d->indexOf(it.key());
638 if (index < 0) {
639 qWarning("QxPropertyEditor: '%s' is not a property of this editor", qPrintable(it.key()));
640 continue;
641 }
642 // The baseline first: whatever is loaded is what "unchanged" means, so
643 // the value has to be seen as modified-free from the start.
644 d->m_initial.insert(it.key(), normalize(it.value(), d->m_rows.at(index).property.type));
645 d->setRowValue(index, d->m_initial.value(it.key()));
646 }
647 d->updateModified();
648}
649
650QHash<QString, QVariant> QxPropertyEditor::allValues() const
651{
652 Q_D(const QxPropertyEditor);
653 QHash<QString, QVariant> values;
654 for (const QxPropertyEditorPrivate::Row &row : d->m_rows) {
655 values.insert(row.property.key, row.property.value);
656 }
657 return values;
658}
659
661{
662 Q_D(const QxPropertyEditor);
663 return d->m_modified;
664}
665
667{
668 Q_D(QxPropertyEditor);
669 for (int i = 0; i < d->m_rows.count(); ++i) {
670 const QxProperty &property = d->m_rows.at(i).property;
671 d->setRowValue(i, normalize(property.defaultValue, property.type));
672 }
673}
674
676{
677 Q_D(QxPropertyEditor);
678 for (int i = 0; i < d->m_rows.count(); ++i) {
679 const QString key = d->m_rows.at(i).property.key;
680 d->setRowValue(i, d->m_initial.value(key));
681 }
682}
683
685
686#include "qxpropertyeditor.moc"
The state behind QxPropertyEditor: one row per property, the baseline the values are compared against...
A form built from a list of QxProperty descriptors.
bool contains(const QString &key) const
void reset()
Sets every property back to its defaultValue.
void setAllValues(const QHash< QString, QVariant > &values)
Sets every key of values that this editor knows and takes the result as the new baseline,...
QStringList keys() const
The keys of the properties, in the order they were added.
void addGroup(const QString &title, const QList< QxProperty > &properties)
Opens a section titled title and adds properties to it.
void addProperty(const QxProperty &property)
Adds property to the section the last addGroup() opened, or to the plain form when there is none yet.
void setValue(const QString &key, const QVariant &value)
QHash< QString, QVariant > allValues() const
The current value of every property.
void resetToInitial()
Sets every property back to the values setAllValues() was given.
QVariant value(const QString &key) const
The current value of key, or an invalid QVariant when there is no such property.
bool isModified() const
Whether the current values differ from the ones setAllValues() took as a baseline.
#define QX_APP_END_NAMESPACE
#define Q_NULLPTR
Copyleft (C) 2025 maminjie canpool@163.com SPDX-License-Identifier: MulanPSL-2.0
#define QX_APP_BEGIN_NAMESPACE
#define QX_FINI_PRIVATE()
QColor button
One setting, described well enough to be edited.
Definition qxproperty.h:44
QStringList choices
The choices an Enum offers; unused by every other type.
Definition qxproperty.h:79
bool readOnly
Whether the control is shown disabled.
Definition qxproperty.h:85
QString tooltip
Text shown when the pointer rests on the control.
Definition qxproperty.h:81
QString key
Stable name the value is stored under.
Definition qxproperty.h:70
PathMode pathMode
What the browse button picks, for Path.
Definition qxproperty.h:83
QVariant value
The current value.
Definition qxproperty.h:75