11#include <QDoubleSpinBox>
33class QxPropertyColorButton :
public QPushButton
37 explicit QxPropertyColorButton(QWidget *parent =
Q_NULLPTR)
40 QObject::connect(
this, &QAbstractButton::clicked,
this, &QxPropertyColorButton::chooseColor);
47 void setColor(
const QColor &color)
49 if (m_color == color) {
54 Q_EMIT colorChanged(m_color);
58 void colorChanged(
const QColor &color);
62 const QColor picked = QColorDialog::getColor(m_color,
this, tr(
"Choose a colour"));
63 if (picked.isValid()) {
70 QPixmap swatch(20, 12);
71 swatch.fill(m_color.isValid() ? m_color : Qt::transparent);
73 setText(m_color.isValid() ? m_color.name() : tr(
"(none)"));
80class QxPropertyFontButton :
public QPushButton
84 explicit QxPropertyFontButton(QWidget *parent =
Q_NULLPTR)
87 QObject::connect(
this, &QAbstractButton::clicked,
this, &QxPropertyFontButton::chooseFont);
90 QFont currentFont()
const
94 void setCurrentFont(
const QFont &font)
101 Q_EMIT fontChanged(m_font);
105 void fontChanged(
const QFont &font);
109 bool accepted =
false;
110 const QFont picked = QFontDialog::getFont(&accepted, m_font,
this, tr(
"Choose a font"));
112 setCurrentFont(picked);
118 setText(QStringLiteral(
"%1, %2").arg(m_font.family()).arg(m_font.pointSize()));
129const int kIntMinimum = -1000000000;
130const int kIntMaximum = 1000000000;
131const double kDoubleMinimum = -1.0e12;
132const double kDoubleMaximum = 1.0e12;
134const int kDoubleDecimals = 3;
137QString labelFor(
const QxProperty &property)
139 return property.label.isEmpty() ?
property.key :
property.label;
143QString browsePath(
const QString &start, QxProperty::PathMode mode, QWidget *parent)
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:
153 return QFileDialog::getOpenFileName(parent, QObject::tr(
"Choose a file"), start);
164QVariant normalize(
const QVariant &value, QxProperty::Type type)
167 case QxProperty::Bool:
168 return value.isValid() ? QVariant(value.toBool()) : QVariant();
169 case QxProperty::Int: {
171 const int number = value.toInt(&ok);
172 return ok ? QVariant(number) : QVariant();
174 case QxProperty::Double: {
176 const double number = value.toDouble(&ok);
177 return ok ? QVariant(number) : QVariant();
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: {
187 QColor color = value.canConvert<QColor>() ? qvariant_cast<QColor>(value) : QColor();
188 if (!color.isValid()) {
189 color = QColor(value.toString());
191 return color.isValid() ? QVariant::fromValue(color) : QVariant();
193 case QxProperty::Font: {
194 if (value.canConvert<QFont>()) {
195 return QVariant::fromValue(qvariant_cast<QFont>(value));
198 if (value.isValid() && font.fromString(value.toString())) {
199 return QVariant::fromValue(font);
209QxPropertyEditorPrivate::QxPropertyEditorPrivate() =
default;
211void QxPropertyEditorPrivate::init()
215 m_layout =
new QVBoxLayout(q);
216 m_layout->setContentsMargins(0, 0, 0, 0);
218 m_form =
new QFormLayout();
219 m_layout->addLayout(m_form);
222 m_layout->addStretch(1);
225int QxPropertyEditorPrivate::indexOf(
const QString &key)
const
227 for (
int i = 0; i < m_rows.count(); ++i) {
228 if (m_rows.at(i).property.key == key) {
235QWidget *QxPropertyEditorPrivate::createField(
const QxProperty &property, QWidget **input)
239 switch (property.
type) {
240 case QxProperty::Bool: {
243 QCheckBox *check =
new QCheckBox(labelFor(property), q);
247 case QxProperty::Int: {
248 QSpinBox *spin =
new QSpinBox(q);
249 spin->setRange(kIntMinimum, kIntMaximum);
253 case QxProperty::Double: {
254 QDoubleSpinBox *spin =
new QDoubleSpinBox(q);
255 spin->setRange(kDoubleMinimum, kDoubleMaximum);
256 spin->setDecimals(kDoubleDecimals);
260 case QxProperty::String: {
261 QLineEdit *line =
new QLineEdit(q);
263 line->setReadOnly(
true);
268 case QxProperty::Text: {
269 QTextEdit *edit =
new QTextEdit(q);
271 edit->setReadOnly(
true);
276 case QxProperty::Enum: {
277 QComboBox *combo =
new QComboBox(q);
278 combo->addItems(property.
choices);
282 case QxProperty::Color: {
283 QxPropertyColorButton *
button =
new QxPropertyColorButton(q);
287 case QxProperty::Font: {
288 QxPropertyFontButton *
button =
new QxPropertyFontButton(q);
292 case QxProperty::Path: {
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()) {
316void QxPropertyEditorPrivate::insertRow(QFormLayout *form,
const QxProperty &property)
323 QWidget *field = createField(property, &input);
325 qWarning(
"QxPropertyEditor: '%s' has a type there is no control for", qPrintable(property.
key));
329 if (property.
type == QxProperty::Bool) {
332 form->addRow(labelFor(property), field);
337 row.
property.
value = normalize(property.
value.isValid() ? property.
value :
property.defaultValue,
property.type);
342 field->setEnabled(!property.
readOnly);
343 field->setToolTip(property.
tooltip);
351 if (!m_initial.contains(property.
key)) {
354 connectField(m_rows.count() - 1);
357void QxPropertyEditorPrivate::connectField(
int index)
361 const Row row = m_rows.at(index);
368 auto commit = [
this, index]() {
372 setRowValue(index, readField(m_rows.at(index)));
376 case QxProperty::Bool:
377 QObject::connect(qobject_cast<QCheckBox *>(row.
input), &QCheckBox::toggled, q, commit);
379 case QxProperty::Int:
380 QObject::connect(qobject_cast<QSpinBox *>(row.
input), QOverload<int>::of(&QSpinBox::valueChanged), q, commit);
382 case QxProperty::Double:
383 QObject::connect(qobject_cast<QDoubleSpinBox *>(row.
input),
384 QOverload<double>::of(&QDoubleSpinBox::valueChanged), q, commit);
386 case QxProperty::String:
387 case QxProperty::Path:
388 QObject::connect(qobject_cast<QLineEdit *>(row.
input), &QLineEdit::textChanged, q, commit);
390 case QxProperty::Text:
391 QObject::connect(qobject_cast<QTextEdit *>(row.
input), &QTextEdit::textChanged, q, commit);
393 case QxProperty::Enum:
394 QObject::connect(qobject_cast<QComboBox *>(row.
input), &QComboBox::currentTextChanged, q, commit);
396 case QxProperty::Color:
399 QObject::connect(qobject_cast<QxPropertyColorButton *>(row.
input), &QxPropertyColorButton::colorChanged, q,
402 case QxProperty::Font:
403 QObject::connect(qobject_cast<QxPropertyFontButton *>(row.
input), &QxPropertyFontButton::fontChanged, q,
409QVariant QxPropertyEditorPrivate::readField(
const Row &row)
const
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());
437void QxPropertyEditorPrivate::writeField(
const Row &row,
const QVariant &value)
444 case QxProperty::Bool:
445 qobject_cast<QCheckBox *>(row.
input)->setChecked(value.toBool());
447 case QxProperty::Int:
448 qobject_cast<QSpinBox *>(row.
input)->setValue(value.toInt());
450 case QxProperty::Double:
451 qobject_cast<QDoubleSpinBox *>(row.
input)->setValue(value.toDouble());
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());
461 case QxProperty::Text: {
462 QTextEdit *edit = qobject_cast<QTextEdit *>(row.
input);
463 if (edit->toPlainText() != value.toString()) {
464 edit->setPlainText(value.toString());
468 case QxProperty::Enum: {
469 QComboBox *combo = qobject_cast<QComboBox *>(row.
input);
470 const int index = combo->findText(value.toString());
472 combo->setCurrentIndex(index);
478 if (!value.toString().isEmpty()) {
479 combo->addItem(value.toString());
480 combo->setCurrentIndex(combo->count() - 1);
484 case QxProperty::Color:
485 qobject_cast<QxPropertyColorButton *>(row.
input)->setColor(qvariant_cast<QColor>(value));
487 case QxProperty::Font:
488 qobject_cast<QxPropertyFontButton *>(row.
input)->setCurrentFont(qvariant_cast<QFont>(value));
493void QxPropertyEditorPrivate::setRowValue(
int index,
const QVariant &value)
497 if (index < 0 || index >= m_rows.count()) {
501 Row &row = m_rows[index];
508 writeField(row, value);
515void QxPropertyEditorPrivate::updateModified()
519 bool modified =
false;
520 for (
const Row &row : m_rows) {
521 if (m_initial.value(row.property.key) != row.property.value) {
527 if (modified == m_modified) {
530 m_modified = modified;
531 Q_EMIT q->modifiedChanged(m_modified);
534QxPropertyEditor::QxPropertyEditor(QWidget *parent)
551 return d->m_rows.count();
559 keys.append(row.property.key);
567 return d->indexOf(key) >= 0;
574 if (property.
key.isEmpty()) {
575 qWarning(
"QxPropertyEditor: a property needs a non-empty key");
578 if (d->indexOf(property.
key) >= 0) {
579 qWarning(
"QxPropertyEditor: a property named '%s' already exists", qPrintable(property.
key));
583 d->insertRow(d->m_form, property);
590 QGroupBox *group =
new QGroupBox(title,
this);
591 QFormLayout *form =
new QFormLayout(group);
593 d->m_layout->insertWidget(d->m_layout->count() - 1, group);
597 for (
const QxProperty &property : properties) {
605 const int index = d->indexOf(key);
609 return d->m_rows.at(index).property.value;
616 const int index = d->indexOf(key);
618 qWarning(
"QxPropertyEditor: there is no property named '%s'", qPrintable(key));
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));
627 d->setRowValue(index, normalized);
636 for (
auto it = values.constBegin(); it != values.constEnd(); ++it) {
637 const int index = d->indexOf(it.key());
639 qWarning(
"QxPropertyEditor: '%s' is not a property of this editor", qPrintable(it.key()));
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()));
653 QHash<QString, QVariant> values;
655 values.insert(row.property.key, row.property.value);
663 return d->m_modified;
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));
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));
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,...
~QxPropertyEditor() override
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()
One setting, described well enough to be edited.
QStringList choices
The choices an Enum offers; unused by every other type.
bool readOnly
Whether the control is shown disabled.
QString tooltip
Text shown when the pointer rests on the control.
QString key
Stable name the value is stored under.
PathMode pathMode
What the browse button picks, for Path.
QVariant value
The current value.