Commits

prokhin_alexey committed b71e0b6

Introduced data forms

Comments (0)

Files changed (6)

src/lime/dataforms.cpp

+/*
+	This file is part of Lime.
+	Copyright (C) 2012  Alexey Prokhin <alexey.prokhin@yandex.ru>
+
+	This program is free software: you can redistribute it and/or modify
+	it under the terms of the GNU General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	This program is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU General Public License for more details.
+
+	You should have received a copy of the GNU General Public License
+	along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "dataforms.h"
+
+#include <QTimer>
+#include <QComboBox>
+#include <QLabel>
+#include <QDebug>
+#include <QTextEdit>
+#include <QLineEdit>
+#include <QCheckBox>
+#include <iostream>
+#include <algorithm>
+#include <QSpinBox>
+#include <QDateTimeEdit>
+#include <QBoxLayout>
+#include <QPushButton>
+#include <QFileDialog>
+#include <QDialogButtonBox>
+#include <QStringListModel>
+#include <QListView>
+#include <QStyledItemDelegate>
+#include <QFormLayout>
+
+#include "utils.h"
+
+namespace Lime {
+
+class DataFormLayout : public QFormLayout
+{
+public:
+	DataFormLayout(DataForm *dataForm, QWidget *widget = 0) :
+		QFormLayout(widget), m_dataForm(dataForm)
+	{}
+	virtual ~DataFormLayout();
+	StringChooserField *addStringChooser(const QString &name, const QString &label, const QString &value);
+	StringField *addString(const QString &name, const QString &label, const QString &value);
+	StringListField *addStringList(const QString &name, const QString &label, const QStringList &value);
+	MultiStringField *addMultiString(const QString &name, const QString &label, const QString &value);
+	BoolField *addBool(const QString &name, const QString &label, bool value);
+	IntField *addInt(const QString &name, const QString &label, int value);
+	DoubleField *addDouble(const QString &name, const QString &label, double value);
+	DateField *addDate(const QString &name, const QString &label, const QDate &value);
+	DateTimeField *addDateTime(const QString &name, const QString &label, const QDateTime &value);
+	PixmapChooserField *addPixmap(const QString &name, const QString &label, const QString &value);
+private:
+	template <typename FieldType, typename DataType>
+	FieldType *addField(const QString &name, const QString &label, const DataType &value);
+private:
+	QList<DataField*> m_fields;
+	DataForm *m_dataForm;
+};
+
+class DataWidgetBase
+{
+public:
+	DataForm *dataForm() { return m_dataForm; }
+	void setDataForm(DataForm *dataForm) { m_dataForm = dataForm; }
+protected:
+	void incIncompleteWidgetsCount() { m_dataForm->incIncompleteWidgetsCount(); }
+	void decIncompleteWidgetsCount() { m_dataForm->decIncompleteWidgetsCount(); }
+protected:
+	DataForm *m_dataForm;
+};
+
+template <typename ParentType, typename WidgetType>
+class DataWidget : public ParentType, public DataWidgetBase
+{
+public:
+	DataWidget() : m_visible(true) { m_widget = new WidgetType; }
+	virtual ~DataWidget() { m_widget->deleteLater(); destroyed(); }
+	QString name() { return m_widget->objectName(); }
+	void setName(const QString &name) { m_widget->setObjectName(name); }
+	bool isVisible() { return m_visible; }
+	void setVisible(bool visible)
+	{
+		m_visible = visible;
+		m_widget->setVisible(visible);
+		if (auto label = labelWidget())
+			label->setVisible(visible);
+	}
+	bool isEnabled() { return m_widget->isEnabled(); }
+	void setEnabled(bool enabled) { m_widget->setEnabled(enabled); }
+	QWidget *widget() { return m_widget; }
+	virtual QLabel *labelWidget() { return 0; }
+	Signal<void(QLabel*)> labelAdded;
+	Signal<void()> destroyed;
+protected:
+	WidgetType *m_widget;
+	bool m_visible;
+};
+
+template <typename ParentType, typename WidgetType>
+class TitledDataWidget : public DataWidget<ParentType, WidgetType>
+{
+public:
+	virtual ~TitledDataWidget()
+	{
+		if (m_labelWidget)
+			m_labelWidget->deleteLater();
+	}
+	QString label()
+	{
+		return m_label;
+	}
+	void setLabel(const QString &label)
+	{
+		m_label = label;
+		if (!label.isEmpty()) {
+			if (!m_labelWidget) {
+				typedef DataWidget<ParentType, WidgetType> Parent;
+				m_labelWidget = new QLabel;
+				m_labelWidget->setBuddy(Parent::m_widget);
+				m_labelWidget->setVisible(Parent::m_visible);
+				Parent::labelAdded(m_labelWidget);
+			}
+			m_labelWidget->setText(label + ":");
+		} else if (m_labelWidget) {
+			m_labelWidget->deleteLater();
+		}
+	}
+	QLabel *labelWidget() { return m_labelWidget; }
+protected:
+	Pointer<QLabel> m_labelWidget;
+	QString m_label;
+};
+
+template <typename ParentType, typename WidgetType>
+class ValidatedDataWidget : public TitledDataWidget<ParentType, WidgetType>
+{
+public:
+	ValidatedDataWidget() :
+		m_isComplete(true)
+	{}
+protected:
+	void setComplete(bool complete)
+	{
+		if (m_isComplete != complete) {
+			if (complete)
+				this->decIncompleteWidgetsCount();
+			else
+				this->incIncompleteWidgetsCount();
+			m_isComplete = complete;
+		}
+	}
+	void setMandatory(bool mandatory)
+	{
+		m_isMandatory = mandatory;
+		if (!m_isMandatory)
+			setComplete(true);
+		else
+			checkCompletion();
+	}
+	bool isMandatory() const
+	{
+		return m_isMandatory;
+	}
+	void checkCompletion()
+	{
+		if (!isMandatory())
+			return;
+		setComplete(this->isComplete());
+	}
+	bool checkText(QString text, const QValidator *validator) const
+	{
+		if (validator) {
+			int pos = 0;
+			return validator->validate(text, pos) == QValidator::Acceptable;
+		} else {
+			return !text.isEmpty();
+		}
+	}
+private:
+	bool m_isComplete;
+	bool m_isMandatory;
+};
+
+DataField::DataField()
+{
+}
+
+DataField::~DataField()
+{
+}
+
+DataFormLayout::~DataFormLayout()
+{
+	qDeleteAll(m_fields);
+}
+
+#define RETURN_FIELD \
+	if (m_dataForm->flags() & DataForm::ReadOnly) \
+		return addField<ReadOnlyFieldImpl>(name, label, value); \
+	else \
+		return addField<FieldImpl>(name, label, value)
+
+StringChooserField *DataFormLayout::addStringChooser(const QString &name, const QString &label, const QString &value)
+{
+	class FieldImpl : public ValidatedDataWidget<StringChooserField, QComboBox>
+	{
+	public:
+		FieldImpl() :
+			m_validator(0)
+		{
+			lconnect(m_widget, SIGNAL(currentIndexChanged(int)), m_widget, [=] {
+				if (!m_widget->isEditable()) {
+					onValueChanged();
+					checkCompletion();
+				}
+			});
+		}
+		QVariant value()
+		{
+			return m_widget->currentText();
+		}
+		void setValue(const QVariant &value)
+		{
+			m_current.clear();
+			if (value.type() != QVariant::String) {
+				m_widget->setCurrentIndex(-1);
+				return;
+			}
+			auto text = value.toString();
+			int index = m_widget->findText(text);
+			if (index != -1)
+				m_widget->setCurrentIndex(index);
+			else if (m_widget->isEditable())
+				m_widget->setEditText(text);
+			else
+				m_current = text;
+		}
+		void setValidator(QValidator *validator)
+		{
+			m_validator = validator;
+			m_widget->setValidator(validator);
+			checkCompletion();
+		}
+		const QValidator *validator() const
+		{
+			return m_validator;
+		}
+		void addOption(const QString &option, const QVariant &userData)
+		{
+			m_widget->addItem(option, userData);
+			if (m_current == option) {
+				m_widget->setCurrentIndex(m_widget->count()-1);
+				m_current.clear();
+			}
+		}
+		void addOptions(const QStringList &options, const QVariantList &userData)
+		{
+			Q_ASSERT(userData.isEmpty() || options.size() == userData.size());
+			bool hasUserData = !userData.isEmpty();
+			int currentIndex = -1;
+			m_widget->blockSignals(true);
+			for (int i = 0, n = options.size(); i < n; ++i) {
+				auto &option = options.at(i);
+				m_widget->addItem(option, hasUserData ? userData.at(i) : QVariant());
+				if (m_current == option)
+					currentIndex = i;
+			}
+			m_widget->blockSignals(false);
+			if (currentIndex != -1) {
+				m_widget->setCurrentIndex(m_widget->count()-1);
+				m_current.clear();
+			}
+		}
+		void removeOption(const QString &option)
+		{
+			int index = m_widget->findText(option);
+			if (index != -1)
+				m_widget->removeItem(index);
+		}
+		QStringList options() const
+		{
+			QStringList options;
+			for (int i = 0, n = m_widget->count(); i < n; ++i)
+				options << m_widget->itemText(i);
+			return options;
+		}
+		void setEditable(bool editable)
+		{
+			m_widget->setEditable(editable);
+			if (editable)
+				m_widget->setValidator(m_validator);
+			if (editable && !m_current.isEmpty()) {
+				m_widget->setEditText(m_current);
+				onValueChanged();
+			} else if (m_widget->count() == 0) {
+				onValueChanged();
+			}
+
+			if (editable) {
+				lconnect(m_widget, SIGNAL(editTextChanged(QString)), m_widget,
+						 [=](const QString &text)
+				{
+					m_current = text;
+					onValueChanged();
+					checkCompletion();
+				});
+			} else {
+				ldisconnect(m_widget, SIGNAL(editTextChanged(QString)), m_widget);
+			}
+		}
+		bool isEditable() const
+		{
+			return m_widget->isEditable();
+		}
+		QVariant currentUserData()
+		{
+			return m_widget->itemData(m_widget->currentIndex());
+		}
+		bool isComplete() const
+		{
+			return checkText(m_widget->currentText(), m_validator);
+		}
+	private:
+		QString m_current;
+		QValidator *m_validator;
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<StringChooserField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_widget->text();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::String)
+				m_widget->clear();
+			else
+				m_widget->setText(value.toString());
+		}
+		void setValidator(QValidator *) { }
+		const QValidator *validator() const { return 0; }
+		void setMandatory(bool mandatory) { }
+		bool isMandatory() const { return false; }
+		void addOption(const QString &option, const QVariant &userData)
+		{
+			m_userData.insert(option, userData);
+		}
+		void removeOption(const QString &option)
+		{
+			m_userData.remove(option);
+		}
+		QStringList options() const
+		{
+			return QStringList(m_userData.keys());
+		}
+		void setEditable(bool /*editable*/) { }
+		bool isEditable() const
+		{
+			return false;
+		}
+		QVariant currentUserData()
+		{
+			return m_userData.value(m_widget->text());
+		}
+		bool isComplete() const
+		{
+			return true;
+		}
+	private:
+		QHash<QString, QVariant> m_userData;
+	};
+
+	RETURN_FIELD;
+}
+
+StringField *DataFormLayout::addString(const QString &name, const QString &label, const QString &value)
+{
+	class FieldImpl : public ValidatedDataWidget<StringField, QLineEdit>
+	{
+	public:
+		FieldImpl()
+		{
+			lconnect(m_widget, SIGNAL(textChanged(QString)), m_widget, [=] {
+				onValueChanged();
+				checkCompletion();
+			});
+		}
+		QVariant value()
+		{
+			return m_widget->text();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::String)
+				m_widget->clear();
+			else
+				m_widget->setText(value.toString());
+		}
+		void setValidator(QValidator *validator)
+		{
+			m_widget->setValidator(validator);
+			checkCompletion();
+		}
+		const QValidator *validator() const
+		{
+			return m_widget->validator();
+		}
+		void setPasswordMode(bool passwordMode)
+		{
+			m_widget->setEchoMode(passwordMode ? QLineEdit::Password : QLineEdit::Normal);
+		}
+		bool isPasswordMode() const
+		{
+			return m_widget->echoMode() != QLineEdit::Normal;
+		}
+		bool isComplete() const
+		{
+			return checkText(m_widget->text(), m_widget->validator());
+		}
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<StringField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_widget->text();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::String)
+				m_widget->clear();
+			else
+				m_widget->setText(value.toString());
+		}
+		void setValidator(QValidator */*validator*/) { }
+		const QValidator *validator() const { return 0; }
+		void setMandatory(bool /*mandatory*/) { }
+		bool isMandatory() const { return false; }
+		void setPasswordMode(bool /*passwordMode*/) { }
+		bool isPasswordMode() const
+		{
+			return false;
+		}
+		bool isComplete() const { return true; }
+	};
+
+	RETURN_FIELD;
+}
+
+StringListField *DataFormLayout::addStringList(const QString &name, const QString &label, const QStringList &value)
+{
+	class ItemDelegate : public QStyledItemDelegate
+	{
+	public:
+		ItemDelegate(QValidator* &validator, QObject *parent = 0) :
+			QStyledItemDelegate(parent), m_validator(validator)
+		{
+		}
+		QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
+		{
+			auto w = QStyledItemDelegate::createEditor(parent, option, index);
+			if (auto lineEdit = qobject_cast<QLineEdit*>(w))
+				lineEdit->setValidator(m_validator);
+			return w;
+		}
+		void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
+		{
+			if (auto lineEdit = qobject_cast<QLineEdit*>(editor)) {
+				if (lineEdit->text() == model->data(index).toString())
+					return;
+			}
+			QStyledItemDelegate::setModelData(editor, model, index);
+		}
+	private:
+		QValidator* &m_validator;
+	};
+
+	class FieldImpl : public ValidatedDataWidget<StringListField, QWidget>
+	{
+	public:
+		FieldImpl() :
+			m_validator(0)
+		{
+			m_view = new QListView(m_widget);
+			m_view->setObjectName("listView");
+			auto layout = new QGridLayout(m_widget);
+			layout->setContentsMargins(0, 0, 0, 0);
+			layout->addWidget(m_view, 0, 0, 3, 1);
+
+			m_model = new QStringListModel(m_view);
+			m_view->setModel(m_model);
+			m_view->setItemDelegate(new ItemDelegate(m_validator, m_widget));
+
+			auto addButton = new QPushButton("Add", m_widget);
+			addButton->setObjectName("addButton");
+			layout->addWidget(addButton, 0, 1);
+			lconnect(addButton, SIGNAL(clicked(bool)), m_view, [=] {
+				int row = m_model->rowCount();
+				m_model->insertRows(row, 1);
+				auto index = m_model->index(row);
+				m_view->setCurrentIndex(index);
+				m_view->edit(index);
+				checkCompletion();
+				onValueChanged();
+			});
+
+			auto removeButton = new QPushButton("Remove", m_widget);
+			removeButton->setObjectName("removeButton");
+			layout->addWidget(removeButton, 1, 1);
+			lconnect(removeButton, SIGNAL(clicked(bool)), m_view, [=] {
+				auto selectedRows = m_view->selectionModel()->selectedRows();
+				for (int i = selectedRows.count()-1; i >= 0; --i)
+					m_model->removeRow(selectedRows.at(i).row());
+				checkCompletion();
+				onValueChanged();
+			});
+
+			lconnect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), m_widget, [=] {
+				checkCompletion();
+				onValueChanged();
+			});
+		}
+		QVariant value()
+		{
+			return m_model->stringList();
+		}
+		void setValue(const QVariant &value)
+		{
+			m_model->setStringList(value.value<QStringList>());
+			checkCompletion();
+			onValueChanged();
+		}
+		void setValidator(QValidator *validator)
+		{
+			m_validator = validator;
+			checkCompletion();
+		}
+		QValidator *validator() const
+		{
+			return m_validator;
+		}
+		bool isComplete() const
+		{
+			auto list = m_model->stringList();
+
+			if (list.isEmpty())
+				return false;
+
+			foreach (auto &str, list) {
+				bool isStrComplete = checkText(str, m_validator);
+				if (!isStrComplete)
+					return false;
+			}
+
+			return true;
+		}
+	private:
+		QValidator *m_validator;
+		QStringListModel *m_model;
+		QListView *m_view;
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<StringListField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_value;
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() == QVariant::StringList) {
+				m_value = value.toStringList();
+				m_widget->setText(m_value.join("\n"));
+			} else {
+				m_widget->clear();
+			}
+		}
+		void setValidator(QValidator */*validator*/) { }
+		const QValidator *validator() const { return 0; }
+		void setMandatory(bool /*mandatory*/) { }
+		bool isMandatory() const { return false; }
+		bool isComplete() const { return true; }
+	private:
+		QStringList m_value;
+	};
+
+	RETURN_FIELD;
+}
+
+MultiStringField *DataFormLayout::addMultiString(const QString &name, const QString &label, const QString &value)
+{
+	class FieldImpl : public TitledDataWidget<MultiStringField, QTextEdit>
+	{
+	public:
+		FieldImpl()
+		{
+			lconnect(m_widget, SIGNAL(textChanged()), m_widget, onValueChanged);
+		}
+		QVariant value()
+		{
+			return m_widget->toPlainText();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() == QVariant::String)
+				m_widget->setPlainText(value.toString());
+			else
+				m_widget->clear();
+		}
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<MultiStringField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_widget->text();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() == QVariant::String)
+				m_widget->setText(value.toString());
+			else
+				m_widget->clear();
+		}
+	};
+
+	RETURN_FIELD;
+}
+
+BoolField *DataFormLayout::addBool(const QString &name, const QString &label, bool value)
+{
+	class FieldImpl : public DataWidget<BoolField, QCheckBox>
+	{
+	public:
+		FieldImpl()
+		{
+			lconnect(m_widget, SIGNAL(toggled(bool)), m_widget, onValueChanged);
+		}
+		QString label()
+		{
+			return m_widget->text();
+		}
+		void setLabel(const QString &label)
+		{
+			m_widget->setText(label);
+		}
+		QVariant value()
+		{
+			return m_widget->isChecked();
+		}
+		void setValue(const QVariant &value)
+		{
+			bool v = value.type() == QVariant::Bool ? value.toBool() : false;
+			m_widget->setChecked(v);
+		}
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<BoolField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_value;
+		}
+		void setValue(const QVariant &value)
+		{
+			m_value = value.type() == QVariant::Bool ? value.toBool() : false;
+			m_widget->setText(m_value ? tr("yes") : tr("no"));
+		}
+	private:
+		bool m_value;
+	};
+
+ 	RETURN_FIELD;
+}
+
+IntField *DataFormLayout::addInt(const QString &name, const QString &label, int value)
+{
+	class FieldImpl : public TitledDataWidget<IntField, QSpinBox>
+	{
+	public:
+		FieldImpl()
+		{
+			lconnect(m_widget, SIGNAL(valueChanged(int)), m_widget, onValueChanged);
+		}
+		QVariant value()
+		{
+			return m_widget->value();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::Int)
+				return;
+			auto newData = value.toInt();
+			if (newData > m_widget->maximum())
+				m_widget->setMaximum(newData);
+			else if (newData < m_widget->minimum())
+				m_widget->setMinimum(newData);
+			m_widget->setValue(newData);
+		}
+		int minimum() const
+		{
+			return m_widget->minimum();
+		}
+		virtual int maximum() const
+		{
+			return m_widget->maximum();
+		}
+		virtual void setMaximum(int max)
+		{
+			m_widget->setMaximum(max);
+		}
+		virtual void setMinimum(int min)
+		{
+			m_widget->setMinimum(min);
+		}
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<IntField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_value;
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::Int)
+				return;
+			m_value = value.toInt();
+			m_widget->setText(QString::number(m_value));
+		}
+		int minimum() const
+		{
+			return 0;
+		}
+		virtual int maximum() const
+		{
+			return 99;
+		}
+		virtual void setMaximum(int /*max*/)
+		{
+		}
+		virtual void setMinimum(int /*min*/)
+		{
+		}
+	private:
+		int m_value;
+	};
+
+ 	RETURN_FIELD;
+}
+
+DoubleField *DataFormLayout::addDouble(const QString &name, const QString &label, double value)
+{
+	class FieldImpl : public TitledDataWidget<DoubleField, QDoubleSpinBox>
+	{
+	public:
+		FieldImpl()
+		{
+			lconnect(m_widget, SIGNAL(valueChanged(double)), m_widget, onValueChanged);
+		}
+		QVariant value()
+		{
+			return m_widget->value();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::Double)
+				return;
+			auto newData = value.toDouble();
+			if (newData > m_widget->maximum())
+				m_widget->setMaximum(newData);
+			else if (newData < m_widget->minimum())
+				m_widget->setMinimum(newData);
+			m_widget->setValue(newData);
+		}
+		double minimum() const
+		{
+			return m_widget->minimum();
+		}
+		virtual double maximum() const
+		{
+			return m_widget->maximum();
+		}
+		virtual void setMaximum(double max)
+		{
+			m_widget->setMaximum(max);
+		}
+		virtual void setMinimum(double min)
+		{
+			m_widget->setMinimum(min);
+		}
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<DoubleField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_value;
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::Double)
+				return;
+			m_value = value.toDouble();
+			m_widget->setText(QString::number(m_value));
+		}
+		double minimum() const
+		{
+			return 0;
+		}
+		virtual double maximum() const
+		{
+			return 99.99;
+		}
+		virtual void setMaximum(double /*max*/)
+		{
+		}
+		virtual void setMinimum(double /*min*/)
+		{
+		}
+	private:
+		double m_value;
+	};
+
+ 	RETURN_FIELD;
+}
+
+DateField *DataFormLayout::addDate(const QString &name, const QString &label, const QDate &value)
+{
+	class FieldImpl : public TitledDataWidget<DateField, QDateEdit>
+	{
+	public:
+		FieldImpl()
+		{
+			lconnect(m_widget, SIGNAL(dateChanged(QDate)), m_widget, onValueChanged);
+		}
+		QVariant value()
+		{
+			return m_widget->date();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::Date)
+				return;
+			m_widget->setDate(value.toDate());
+		}
+		void setDisplayFormat(const QString &format)
+		{
+			m_widget->setDisplayFormat(format);
+		}
+		QString displayFormat() const
+		{
+			return m_widget->displayFormat();
+		}
+		QDate minimum() const
+		{
+			return m_widget->minimumDate();
+		}
+		QDate maximum() const
+		{
+			return m_widget->maximumDate();
+		}
+		void setMaximum(const QDate &max)
+		{
+			m_widget->setMaximumDate(max);
+		}
+		void setMinimum(const QDate &min)
+		{
+			m_widget->setMinimumDate(min);
+		}
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<DateField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_value;
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::Date)
+				return;
+			m_value = value.toDate();
+			update();
+		}
+		void setDisplayFormat(const QString &format)
+		{
+			m_format = format;
+			update();
+		}
+		QString displayFormat() const
+		{
+			return m_format;
+		}
+		QDate minimum() const
+		{
+			return QDate(7999, 9, 14);
+		}
+		QDate maximum() const
+		{
+			return QDate(7999, 12, 31);
+		}
+		void setMaximum(const QDate &/*max*/)
+		{
+		}
+		void setMinimum(const QDate &/*min*/)
+		{
+		}
+	private:
+		void update()
+		{
+			auto text = m_format.isEmpty() ?
+							m_value.toString(Qt::DefaultLocaleLongDate) :
+							m_value.toString(m_format);
+			m_widget->setText(text);
+		}
+	private:
+		QDate m_value;
+		QString m_format;
+	};
+
+ 	RETURN_FIELD;
+}
+
+DateTimeField *DataFormLayout::addDateTime(const QString &name, const QString &label, const QDateTime &value)
+{
+	class FieldImpl : public TitledDataWidget<DateTimeField, QDateTimeEdit>
+	{
+	public:
+		FieldImpl()
+		{
+			lconnect(m_widget, SIGNAL(dateTimeChanged(QDateTime)), m_widget, onValueChanged);
+		}
+		QVariant value()
+		{
+			return m_widget->dateTime();
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::DateTime)
+				return;
+			m_widget->setDateTime(value.toDateTime());
+		}
+		void setDisplayFormat(const QString &format)
+		{
+			m_widget->setDisplayFormat(format);
+		}
+		QString displayFormat() const
+		{
+			return m_widget->displayFormat();
+		}
+		QDateTime minimum() const
+		{
+			return m_widget->minimumDateTime();
+		}
+		QDateTime maximum() const
+		{
+			return m_widget->maximumDateTime();
+		}
+		void setMaximum(const QDateTime &max)
+		{
+			m_widget->setMaximumDateTime(max);
+		}
+		void setMinimum(const QDateTime &min)
+		{
+			m_widget->setMinimumDateTime(min);
+		}
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<DateTimeField, QLabel>
+	{
+	public:
+		QVariant value()
+		{
+			return m_value;
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::DateTime)
+				return;
+			m_value = value.toDateTime();
+			update();
+		}
+		void setDisplayFormat(const QString &format)
+		{
+			m_format = format;
+			update();
+		}
+		QString displayFormat() const
+		{
+			return m_format;
+		}
+		QDateTime minimum() const
+		{
+			return QDateTime(QDate(1752, 9, 14), QTime(0, 0));
+		}
+		QDateTime maximum() const
+		{
+			return QDateTime(QDate(7999, 12, 31), QTime(23, 59, 59, 999));
+		}
+		void setMaximum(const QDateTime &max)
+		{
+		}
+		void setMinimum(const QDateTime &min)
+		{
+		}
+	private:
+		void update()
+		{
+			auto text = m_format.isEmpty() ?
+							m_value.toString(Qt::DefaultLocaleLongDate) :
+							m_value.toString(m_format);
+			m_widget->setText(text);
+		}
+	private:
+		QDateTime m_value;
+		QString m_format;
+	};
+
+ 	RETURN_FIELD;
+}
+
+PixmapChooserField *DataFormLayout::addPixmap(const QString &name, const QString &label, const QString &value)
+{
+	class FieldImpl : public TitledDataWidget<PixmapChooserField, QWidget>
+	{
+	public:
+		FieldImpl()
+		{
+			auto layout = new QGridLayout(m_widget);
+			m_pixmapWidget = new QLabel(m_widget);
+			m_pixmapWidget->setObjectName("pixmapLabel");
+			m_pixmapWidget->setFrameShape(QFrame::Panel);
+			m_pixmapWidget->setFrameShadow(QFrame::Sunken);
+			m_pixmapWidget->setAlignment(Qt::AlignCenter);
+
+			auto setButton = new QPushButton(QIcon(), QString(), m_widget);
+			setButton->setObjectName("setButton");
+			setButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+			//setButton->setIcon(Icon("list-add"));
+			auto removeButton = new QPushButton(QIcon(), QString(), m_widget);
+			removeButton->setObjectName("removeButton");
+			//removeButton->setIcon(Icon("list-remove"));
+
+			layout->addWidget(m_pixmapWidget, 0, 0, 3, 1);
+			layout->addWidget(setButton, 0, 1);
+			layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Preferred, QSizePolicy::Expanding), 2, 1);
+			layout->addWidget(removeButton, 2, 1, Qt::AlignBottom);
+
+			lconnect(setButton, SIGNAL(clicked(bool)), m_widget, [this]() {
+				m_path = QFileDialog::getOpenFileName(
+							m_widget,
+							QT_TRANSLATE_NOOP("DataForms", "Open image"),
+							QDir::homePath(),
+							QT_TRANSLATE_NOOP("DataForms",
+											"Images (*.gif *.bmp *.jpg *.jpeg *.png);;"
+											"All files (*.*)"));
+				updatePixmap();
+			});
+			lconnect(removeButton, SIGNAL(clicked(bool)), m_widget, [this]() {
+				m_path.clear();
+				m_pixmapWidget->setPixmap(m_defaultPixmap);
+				onValueChanged();
+			});
+		}
+		QPixmap pixmap() const
+		{
+			return *m_pixmapWidget->pixmap();
+		}
+		QVariant value()
+		{
+			return m_path;
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::String)
+				return;
+			auto newPath = value.toString();
+			if (newPath != m_path) {
+				m_path = newPath;
+				updatePixmap();
+			}
+		}
+		void setDefaultImage(const QPixmap &pixmap)
+		{
+			m_defaultPixmap = pixmap;
+			if (m_path.isEmpty() && pixmap.cacheKey() != m_pixmapWidget->pixmap()->cacheKey()) {
+				m_pixmapWidget->setPixmap(m_defaultPixmap);
+				onValueChanged();
+			}
+		}
+		QPixmap defaultImage() const
+		{
+			return m_defaultPixmap;
+		}
+		void setMaximumSize(const QSize &size)
+		{
+			m_size = size;
+			updatePixmap();
+		}
+		QSize maximumSize() const
+		{
+			return m_size;
+		}
+	private:
+		void updatePixmap()
+		{
+			QPixmap pixmap(m_path);
+			if (!pixmap.isNull() && m_size.isValid() && pixmap.size() != m_size)
+				pixmap = pixmap.scaled(m_size, Qt::KeepAspectRatio);
+			m_pixmapWidget->setPixmap(pixmap.isNull() ? m_defaultPixmap : pixmap);
+			onValueChanged();
+		}
+	private:
+		QString m_path;
+		QPixmap m_defaultPixmap;
+		QLabel *m_pixmapWidget;
+		QSize   m_size;
+	};
+
+	class ReadOnlyFieldImpl : public TitledDataWidget<PixmapChooserField, QLabel>
+	{
+	public:
+		ReadOnlyFieldImpl()
+		{
+			m_widget->setFrameShape(QFrame::Panel);
+			m_widget->setFrameShadow(QFrame::Sunken);
+			m_widget->setAlignment(Qt::AlignCenter);
+		}
+		QPixmap pixmap() const
+		{
+			return *m_widget->pixmap();
+		}
+		QVariant value()
+		{
+			return m_path;
+		}
+		void setValue(const QVariant &value)
+		{
+			if (value.type() != QVariant::String)
+				return;
+			auto newPath = value.toString();
+			if (newPath != m_path) {
+				m_path = newPath;
+				updatePixmap();
+			}
+		}
+		void setDefaultImage(const QPixmap &pixmap)
+		{
+			m_defaultPixmap = pixmap;
+			if (m_path.isEmpty() && pixmap.cacheKey() != m_pixmapWidget->pixmap()->cacheKey()) {
+				m_pixmapWidget->setPixmap(m_defaultPixmap);
+				onValueChanged();
+			}
+		}
+		QPixmap defaultImage() const
+		{
+			return m_defaultPixmap;
+		}
+		void setMaximumSize(const QSize &size)
+		{
+			m_size = size;
+			updatePixmap();
+		}
+		QSize maximumSize() const
+		{
+			return m_size;
+		}
+	private:
+		void updatePixmap()
+		{
+			QPixmap pixmap(m_path);
+			if (!pixmap.isNull() && m_size.isValid() && pixmap.size() != m_size)
+				pixmap = pixmap.scaled(m_size, Qt::KeepAspectRatio);
+			m_widget->setPixmap(pixmap.isNull() ? m_defaultPixmap : pixmap);
+		}
+	private:
+		QString m_path;
+		QPixmap m_defaultPixmap;
+		QLabel *m_pixmapWidget;
+		QSize   m_size;
+	};
+
+	 RETURN_FIELD;
+}
+
+template <typename FieldType, typename DataType>
+FieldType *DataFormLayout::addField(const QString &name, const QString &label, const DataType &value)
+{
+	auto field = new FieldType;
+	field->setDataForm(m_dataForm);
+	if (!name.isEmpty())
+		field->setName(name);
+	if (!label.isEmpty())
+		field->setLabel(label);
+	field->setValue(value);
+
+	auto widget = field->widget();
+	auto labelWidget = field->labelWidget();
+	if (labelWidget)
+		addRow(labelWidget, widget);
+	else
+		addRow(widget);
+
+	m_fields << field;
+	field->destroyed.connect([this, field]() {
+		m_fields.removeOne(field);
+	});
+
+	field->labelAdded.connect([=](QLabel *labelWidget) {
+		int row;
+		getWidgetPosition(widget, &row, 0);
+		Q_ASSERT(row != -1);
+		insertRow(row, labelWidget, widget);
+	});
+
+	return field;
+}
+
+void StringChooserField::addOptions(const QStringList &options, const QVariantList &userData)
+{
+	Q_ASSERT(userData.isEmpty() || options.size() == userData.size());
+	bool hasUserData = !userData.isEmpty();
+	for (int i = 0, c = options.size(); i < c; ++i)
+		addOption(options.at(i), hasUserData ? userData.at(i) : QVariant());
+}
+
+void IntField::setRange(int min, int max)
+{
+	setMinimum(min);
+	setMaximum(max);
+}
+
+void DoubleField::setRange(double min, double max)
+{
+	setMinimum(min);
+	setMaximum(max);
+}
+
+void DateField::setRange(const QDate &min, const QDate &max)
+{
+	setMinimum(min);
+	setMaximum(max);
+}
+
+void DateTimeField::setRange(const QDateTime &min, const QDateTime &max)
+{
+	setMinimum(min);
+	setMaximum(max);
+}
+
+DataForm::DataForm(int flags, QWidget *parent) :
+	QWidget(parent), m_buttonBox(0), m_flags(flags), m_incompleteWidgetsCount(0)
+{
+	m_mainLayout = new QVBoxLayout(this);
+	m_layout = new DataFormLayout(this);
+	m_mainLayout->addLayout(m_layout);
+}
+
+int DataForm::flags()
+{
+	return m_flags;
+}
+
+void DataForm::addDialogButton(const QString &text, ButtonRole role, const function<void()> &clickHandler)
+{
+	ensureButtonBox();
+	auto button = m_buttonBox->addButton(text, static_cast<QDialogButtonBox::ButtonRole>(role));
+	lconnect(button, SIGNAL(clicked(bool)), button, clickHandler);
+}
+
+void DataForm::addDialogButton(StandardButton standartButton, const function<void()> &clickHandler)
+{
+	ensureButtonBox();
+	auto button = m_buttonBox->addButton(static_cast<QDialogButtonBox::StandardButton>(standartButton));
+	lconnect(button, SIGNAL(clicked(bool)), button, clickHandler);
+}
+
+void DataForm::ensureButtonBox()
+{
+	if (!m_buttonBox) {
+		m_buttonBox = new QDialogButtonBox(this);
+		m_mainLayout->addSpacerItem(new QSpacerItem(0, 20, QSizePolicy::Preferred, QSizePolicy::Expanding));
+		m_mainLayout->addWidget(m_buttonBox);
+	}
+}
+
+bool DataForm::isComplete() const
+{
+	return m_incompleteWidgetsCount == 0;
+}
+
+void DataForm::incIncompleteWidgetsCount()
+{
+	if (m_incompleteWidgetsCount == 0) {
+		if (m_buttonBox) {
+			foreach (auto button, m_buttonBox->buttons()) {
+				auto role = static_cast<ButtonRole>(m_buttonBox->buttonRole(button));
+				if (role == ApplyRole || role == AcceptRole || role == YesRole) {
+					button->setEnabled(false);
+				}
+			}
+		}
+		emit completeChanged(false);
+	}
+	++m_incompleteWidgetsCount;
+}
+
+void DataForm::decIncompleteWidgetsCount()
+{
+	--m_incompleteWidgetsCount;
+	Q_ASSERT(m_incompleteWidgetsCount >= 0);
+	if (m_incompleteWidgetsCount == 0) {
+		if (m_buttonBox) {
+			foreach (auto button, m_buttonBox->buttons())
+				button->setEnabled(true);
+		}
+		emit completeChanged(true);
+	}
+}
+
+#define IMPLEMENT_METHOD(Name, Type) \
+	IMPLEMENT_METHOD2(Name, Name, Type)
+
+#define IMPLEMENT_METHOD2(Name, ClassName, Type) \
+	ClassName##Field *DataForm::add##Name(Type value, const QString &label, const QString &name) \
+	{ \
+		return m_layout->add##Name(name, label, value); \
+	}
+
+IMPLEMENT_METHOD(StringChooser, const QString &)
+IMPLEMENT_METHOD(String, const QString &)
+IMPLEMENT_METHOD(StringList, const QStringList &)
+IMPLEMENT_METHOD(MultiString, const QString &)
+IMPLEMENT_METHOD(Bool, bool)
+IMPLEMENT_METHOD(Int, int)
+IMPLEMENT_METHOD(Double, double)
+IMPLEMENT_METHOD(Date, const QDate &)
+IMPLEMENT_METHOD(DateTime, const QDateTime &)
+IMPLEMENT_METHOD2(Pixmap, PixmapChooser, const QString &)
+
+} // Lime

src/lime/dataforms.h

+/*
+	This file is part of Lime.
+	Copyright (C) 2012  Alexey Prokhin <alexey.prokhin@yandex.ru>
+
+	This program is free software: you can redistribute it and/or modify
+	it under the terms of the GNU General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	This program is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU General Public License for more details.
+
+	You should have received a copy of the GNU General Public License
+	along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef LIME_DATAFORMS_H
+#define LIME_DATAFORMS_H
+
+#include "signal.h"
+
+#include <QDateTime>
+#include <QWidget>
+
+class QDialogButtonBox;
+class QValidator;
+class QFormLayout;
+class QVBoxLayout;
+
+#if ENABLE_UNITTESTS
+class QLabel;
+#endif
+
+namespace Lime {
+
+class DataFormLayout;
+
+/** The DataField class is the base of all fields of \ref DataForm "data forms".
+ *
+ * A data field is a base part of data forms. It represents a widget for
+ * displaying and usually editing value(). The value can be changed at any time
+ * by calling setValue(). Whenever the value has been changed, either by setValue()
+ * or as a result of a user action, onValueChanged signal is emitted.
+ *
+ * Each field can have a \ref label() "label" that is usually shown at the right side
+ * of the field widget. setLabel() changes the label. The label is allowed to be an empty
+ * string.
+ *
+ * A data field may have \ref name() "name" for identification purposes, call setName() to
+ * change it.
+ */
+class DataField
+{
+#ifndef Q_MOC_RUN
+	/**  Holds whether the field is enabled.
+	 *
+	 * Unlike an enabled field, a disabled field ignores keyboard, mouse and touch events.
+	 * (i.e. the user will not be able to edit the \ref value() "value" of the field).
+	 *
+	 * Also, most fields are displayed differently when they are disabled. For instance,
+	 * StringField might have a gray background when it is disabled.
+	 *
+	 * By default, the property is true.
+	 */
+	Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
+	/** Holds true if the field is currently visible, otherwise holds false.
+
+	 * If the property is false, the field (including the \ref label() "label")
+	 * is completely invisible for the user.
+	 *
+	 * By default, the property is true.
+	 */
+	Q_PROPERTY(bool visible READ isVisible WRITE setVisible)
+#endif
+public:
+	virtual ~DataField();
+	/** Returns the name of the field.
+	 *  \see setName()
+	 */
+	virtual QString name() = 0;
+	/** Changes the name of the field to be \a name.
+	 * \see name()
+	 */
+	virtual void setName(const QString &name) = 0;
+	/** Return the label of the field
+	 * \see setLabel()
+	 */
+	virtual QString label() = 0;
+	/** Sets the label of the field to be \a label.
+	 *
+	 * The \a label can be an empty string.
+	 * \see label()
+	 */
+	virtual void setLabel(const QString &label) = 0;
+	/** Returns the current value of the field.
+	 *
+	 * The value has different type depending on the type of the field.
+	 * See documentation of classes that inherits the class for more
+	 * information about the type of a value that is returned by the method.
+	 *
+	 * \see setValue()
+	 */
+	virtual QVariant value() = 0;
+	/** Sets the value of the field to be \a value.
+	 *
+	 * The value has different type depending on the type of the field.
+	 * See documentation of classes that inherits the class for more
+	 * information about the type of a value that can be set by the method.
+	 *
+	 * \see value()
+	 */
+	virtual void setValue(const QVariant &value) = 0;
+	virtual bool isVisible() = 0;
+	virtual void setVisible(bool visible) = 0;
+	virtual bool isEnabled() = 0;
+	virtual void setEnabled(bool enabled) = 0;
+	/** The signal is emitted whenever the \ref value() "value" of the field is changed.
+	 *
+	 * \see setValue(), value()
+	 */
+	Signal<void()> onValueChanged;
+protected:
+	DataField();
+#if ENABLE_UNITTESTS
+private:
+	friend class DataFormsTest;
+	virtual QWidget *widget() = 0;
+	virtual QLabel *labelWidget() = 0;
+#endif
+};
+
+/** A base class of data fields the data of which can be validated.
+ *
+ * The class allows to place any arbitrary constraints on the value which
+ * may be entered by user. setValidator() sets a validator that is used
+ * to check whether the value() is invalid or accepted.
+ */
+class ValidatedField : public DataField
+{
+public:
+	/** Sets the input \a validator for the value() of the field.
+	 *
+	 * If \a validator is null, removes the current input validator.
+	 *
+	 * The initial setting is to have no input validator.
+	 *
+	 * \see validator()
+	 */
+	virtual void setValidator(QValidator *validator) = 0;
+	/** Returns validator of the field. Or null, if the field
+	 * does not have a validator.
+	 *
+	 * \see setValidator()
+	 */
+	virtual const QValidator *validator() const = 0;
+
+	/** \property setMandatory isMandatory
+	 *
+	 * \brief Holds whether the field is mandatory.
+	 *
+	 * If at least one mandatory field of a data form is not \ref isComplete() "complete",
+	 * the data form is not complete as well.
+	 *
+	 * \see setMandatory()
+	 */
+	virtual bool isMandatory() const = 0;
+	virtual void setMandatory(bool mandatory) = 0;
+	/** Returns true if the field is complete, if it is incomplete, returns false.
+	 *
+	 * If a field has a validator, it is complete when validate() method of the validator
+	 * returns QValidator::Acceptable, if the field does not have a validator, it is
+	 * complete when value() is not empty().
+	 *
+	 * In all other cases, the field is incomplete.
+	 *
+	 * \see DataForm::isComplete()
+	 */
+	virtual bool isComplete() const = 0;
+};
+
+/** The field provides the user with a list of options.
+ *
+ * The field provides a means of presenting a list of text options to the user. The user
+ * can choose one of the option or, if the field is \ref isEditable() "editable", enter
+ * a new text value.
+ *
+ * value() method returns the currently chosen (or entered) value. The type of value()
+ * is QVariant::String. If you will try to \ref setValue() "set value" that has another type,
+ * no current option will be set.
+ *
+ * If a line of text that passed into setValue() method is one of the options, the option
+ * will be chosen. If there are no such option and the field is \ref isEditable() "editable",
+ * the currently entered text will be replaced with the line. If there are no such option
+ * and the field is not editable, no current option will be set, however, when the option will
+ * be added later the option will be chosen. For example, the code is perfectly correct,
+ * even though the current option is set before it was added via addOption():
+ * \code
+ * 		auto stringChooser = dataForm->addStringChooser(tr("Chooser"));
+ * 		stringChooser->setData("Second");
+ * 		stringChooser->addOption("First");
+ * 		stringChooser->addOption("Second");
+ * \endcode
+ *
+ * Every option can has user data that is usually used to identify the option. To retrieve
+ * user data of the current option, use currentUserData().
+ */
+class StringChooserField : public ValidatedField
+{
+#ifndef Q_MOC_RUN
+	/**  Holds whether the field is editable.
+	 *
+	 * If the field is editable, the user will be able to enter a new value in
+	 * addition to provided \ref options() "options".
+	 *
+	 * By default, that property is false.
+	 */
+	Q_PROPERTY(bool editable READ isEditable WRITE setEditable)
+#endif
+public:
+	/** Adds a new \a option that has \a userData.
+	 *
+	 * \see removeOption(), options()
+	 */
+	virtual void addOption(const QString &option, const QVariant &userData = QVariant()) = 0;
+	/** Removes an \a option.
+	 *
+	 * \see addOption(), options()
+	 */
+	virtual void removeOption(const QString &option) = 0;
+	/** Adds a list of \a options.
+	 *
+	 * Lengths of \a options and \a userData must be same.
+	 *
+	 * \see addOption(), options()
+	 */
+	virtual void addOptions(const QStringList &options, const QVariantList &userData = QVariantList());
+	/** Returns a list of current options.
+	 */
+	virtual QStringList options() const = 0;
+	virtual void setEditable(bool editable) = 0;
+	virtual bool isEditable() const = 0;
+	/** Returns the user data of the current options.
+	 *
+	 * \see addOption(), addOptions()
+	 */
+	virtual QVariant currentUserData() = 0;
+};
+
+/** The field allows the user to edit a single line of plain text.
+ *
+ * value() method returns the currently entered text. The type of value()
+ * is QVariant::String. If you will try to \ref setValue() "set value" that has another type,
+ * the entered text will be clear.
+ */
+class StringField : public ValidatedField
+{
+#ifndef Q_MOC_RUN
+	/** Holds whether the field is in password mode.
+	 *
+	 * If a field is in a password mode, asterisks will be displayed instead of the
+	 * characters that were actually entered.
+	 *
+	 * By default, that property is false.
+	 */
+	Q_PROPERTY(bool passwordMode READ isPasswordMode WRITE setPasswordMode)
+#endif
+public:
+	virtual void setPasswordMode(bool passwordMode) = 0;
+	virtual bool isPasswordMode() const = 0;
+};
+
+/** The field displays multiple lines of plain text, allows the user to edit them,
+ * to add a new line and remove an old one.
+ *
+ * value() method returns QStringList of the lines. If you will try to \ref setValue() "set value"
+ * that has a type that is not QVariant::StringList, the all currently entered lines will be removed.
+ */
+class StringListField : public ValidatedField
+{
+};
+
+/** The field allows the user to edit a plain text that consists of more than one line.
+ *
+ * value() method returns the currently entered text. The type of value()
+ * is QVariant::String. If you will try to \ref setValue() "set value" that has another type,
+ * the entered text will be clear.
+ */
+class MultiStringField : public DataField
+{
+public:
+};
+
+/** The field provides the user with two options.
+ *
+ * The field usually implemented as a check box. If the check box is checked,
+ * value() returns true, otherwise it returns false. setValue() accepts only QVariant::Bool,
+ * if a variant has another type, the check box will be unchecked.
+ */
+class BoolField : public DataField
+{
+public:
+};
+
+/** The field allows the user to enter an integer number.
+ *
+ * value() returns a variant with type QVariant::Int. setValue() ignores all values of other
+ * types.
+ *
+ */
+class IntField : public DataField
+{
+#ifndef Q_MOC_RUN
+	/** Holds minimum value of the field.
+	 *
+	 * The default minimum is 0.
+	 */
+	Q_PROPERTY(int minimum READ minimum WRITE setMinimum)
+	/** Holds maximum value of the field.
+	 *
+	 * The default maximum is 99.
+	 */
+	Q_PROPERTY(int maximum READ maximum WRITE setMaximum)
+#endif
+public:
+	/** Convenience function to set the minimum, and maximum values with a single function call.
+	 * \code
+	 * 		setRange(minimum, maximum);
+	 * \endcode
+	 * 	is equivalent to:
+	 * \code
+	 * 	 setMinimum(minimum);
+	 * 	 setMaximum(maximum);
+	 * \endcode
+	 */
+	void setRange(int minimum, int maximum);
+	virtual int minimum() const = 0;
+	virtual int maximum() const = 0;
+	virtual void setMaximum(int maximum) = 0;
+	virtual void setMinimum(int minimum) = 0;
+};
+
+/** The field allows the user to enter a floating-point number.
+ *
+ * value() returns a variant with type QVariant::Double. setValue() ignores all values of other
+ * types.
+ */
+class DoubleField : public DataField
+{
+#ifndef Q_MOC_RUN
+	/** Holds minimum value of the field.
+	 *
+	 * The default minimum is 0.
+	 */
+	Q_PROPERTY(double minimum READ minimum WRITE setMinimum)
+	/** Holds maximum value of the field.
+	 *
+	 * The default maximum is 99.99.
+	 */
+	Q_PROPERTY(double maximum READ maximum WRITE setMaximum)
+#endif
+public:
+	/** Convenience function to set the minimum, and maximum values with a single function call.
+	 * \code
+	 * 		setRange(minimum, maximum);
+	 * \endcode
+	 * 	is equivalent to:
+	 * \code
+	 * 	 setMinimum(minimum);
+	 * 	 setMaximum(maximum);
+	 * \endcode
+	 */
+	void setRange(double minimum, double maximum);
+	virtual double minimum() const = 0;
+	virtual double maximum() const = 0;
+	virtual void setMaximum(double maximum) = 0;
+	virtual void setMinimum(double minimum) = 0;
+};
+
+/** A base class for fields that provides means to show and edit dates and times.
+ */
+class AbstractDateTimeField : public DataField
+{
+#ifndef Q_MOC_RUN
+	/** This property holds the format used to display the time/date of the date time edit.
+	 *
+	 * This format is the same as the one used described in QDateTime::toString() and QDateTime::fromString()
+	 *
+	 * Example format strings (assuming that the date is 2nd of July 1969):
+	 * <table>
+	 *     <tr>
+	 *          <th>Format</th>
+	 *          <th>Result</th>
+	 *     </tr>
+	 *     <tr>
+	 * 	        <td>dd.MM.yyyy</td>
+	 *          <td>02.07.1969</td>
+	 *     </tr>
+	 *     <tr>
+	 * 	        <td>MMM d yy</td>
+	 *          <td>Jul 2 69</td>
+	 *     </tr>
+	 *     <tr>
+	 * 	        <td>MMMM d yy</td>
+	 *          <td>July 2 69</td>
+	 *     </tr>
+	 * </table>
+	 *
+	 * Note that if you specify a two digit year, it will be interpreted to be in the century in which
+	 * the date time edit was initialized. The default century is the 21 (2000-2099).
+	 *
+	 * If you specify an invalid format the format will not be set.
+	 */
+	Q_PROPERTY(double displayFormat READ displayFormat WRITE displayFormat)
+#endif
+public:
+	virtual void setDisplayFormat(const QString &format) = 0;
+	virtual QString displayFormat() const= 0;
+};
+
+/** The field allows the user to edit a date.
+ *
+ * value() method returns a variant with type QVariant::Date. setValue() ignores all values
+ * of other types.
+ */
+class DateField : public AbstractDateTimeField
+{
+#ifndef Q_MOC_RUN
+	/** Holds minimum value of the field.
+	 *
+	 * By default, this property contains a date that refers to September 14, 1752.
+	 */
+	Q_PROPERTY(double minimum READ minimum WRITE setMinimum)
+	/** Holds maximum value of the field.
+	 *
+	 * By default, this property contains a date that refers to December 31, 7999.
+	 */
+	Q_PROPERTY(double maximum READ maximum WRITE setMaximum)
+#endif
+public:
+	/** Convenience function to set the minimum, and maximum values with a single function call.
+	 * \code
+	 * 		setRange(minimum, maximum);
+	 * \endcode
+	 * 	is equivalent to:
+	 * \code
+	 * 	 setMinimum(minimum);
+	 * 	 setMaximum(maximum);
+	 * \endcode
+	 */
+	void setRange(const QDate &minimum, const QDate &maximum);
+	virtual QDate minimum() const = 0;
+	virtual QDate maximum() const = 0;
+	virtual void setMaximum(const QDate &maximum) = 0;
+	virtual void setMinimum(const QDate &minimum) = 0;
+};
+
+/** The field allows the user to edit dates and times.
+ *
+ * value() method returns a variant with type QVariant::DateTime. setValue() ignores all values
+ * of other types.
+ */
+class DateTimeField : public AbstractDateTimeField
+{
+#ifndef Q_MOC_RUN
+	/** Holds minimum value of the field.
+	 *
+	 * By default, this property contains a date that refers to September 14, 1752
+	 * and a time of 00:00:00 and 0 milliseconds.
+	 */
+	Q_PROPERTY(QDateTime minimum READ minimum WRITE setMinimum)
+	/** Holds maximum value of the field.
+	 *
+	 * By default, this property contains a date that refers to 31 December, 7999
+	 * and a time of 23:59:59 and 999 milliseconds.
+	 */
+	Q_PROPERTY(QDateTime maximum READ maximum WRITE setMaximum)
+#endif
+public:
+	/** Convenience function to set the minimum, and maximum values with a single function call.
+	 * \code
+	 * 		setRange(minimum, maximum);
+	 * \endcode
+	 * 	is equivalent to:
+	 * \code
+	 * 	 setMinimum(minimum);
+	 * 	 setMaximum(maximum);
+	 * \endcode
+	 */
+	void setRange(const QDateTime &minimum, const QDateTime &maximum);
+	virtual QDateTime minimum() const = 0;
+	virtual QDateTime maximum() const = 0;
+	virtual void setMaximum(const QDateTime &maximum) = 0;
+	virtual void setMinimum(const QDateTime &minimum) = 0;
+};
+
+/** The field displays a image and allows the user to choose another one from their
+ * collection.
+ *
+ * value() returns a path to the currently chosen image, or an empty string, if no valid image
+ * has been chosen. The type of \ref value() "value" is QVariant::String. setValue() expects
+ * a path to an image. If the value parameter passed into setValue() has a type other
+ * that QVariant::String or if the value is not a valid path to a valid image,
+ * the current image will be cleared and the \ref defaultImage "default image" will be shown instead.
+ */
+class PixmapChooserField : public DataField
+{
+#ifndef Q_MOC_RUN
+	/** Holds the default image that is shown when value() is not
+	 * a string or it is a string but not a valid path to an actual
+	 * image.
+	 */
+	Q_PROPERTY(QPixmap defaultImage READ defaultImage WRITE setDefaultImage)
+	/** Holds the maximum size of the displayed image.
+	 *
+	 * If the user has chosen an image that is more than the size, the image
+	 * will be scaled to the size.
+	 */
+	Q_PROPERTY(QSize maximumSize READ maximumSize WRITE setMaximumSize)
+#endif
+public:
+	/** Returns currently choosen image or the \ref defaultImage "default image"
+	 * if no valid image has been chosen.
+	 */
+	virtual QPixmap pixmap() const = 0;
+	virtual void setDefaultImage(const QPixmap &pixmap) = 0;
+	virtual QPixmap defaultImage() const = 0;
+	virtual void setMaximumSize(const QSize &size) = 0;
+	virtual QSize maximumSize() const = 0;
+};
+
+/** The DataForm class provides a form to edit and display an uncomplicated structure of data.
+ *
+ * A data form comprised of several fields. Each field usually consists of a
+ * \ref DataField::label() "label" and an editor for the \ref Data::Field::value() "value" of
+ * the field. The class supports a wide range of fields:
+ * -# \ref StringField allows the user to edit a single line of plain text.
+ * -# \ref StringChooser provides the user with a list of options.
+ * -# \ref StringListField displays multiple lines of plain text, allows the user to edit them,
+ * to add a new line and remove an old one.
+ * -# \ref MultiStringField allows the user to edit a plain text that consists of more than one
+ * line.
+ * -# \ref BoolField provides two options to the user.
+ * -# \ref IntField allows the user to enter an integer number.
+ * -# \ref DoubleField allows the user to enter a floating point value.
+ * -# \ref DateField provides a means to choose a date.
+ * -# \ref DataTimeField provides a means to choose a date and time.
+ * -# \ref PixmapChooser displays an image and allows the user to choose another one from their
+ * collection.
+ *
+ * The appearance and behavior of fields may change depending on the platform to be more complaint
+ * with its look&feel. For instance, on mobile phones, the fields will be more compact than on PC
+ * and will be organized into one column.
+ *
+ * If every \ref ValidatedField::mandatory() "mandatory" field is \ref ValidatedField::complete()
+ * "complete", the data form is complete as well, otherwise it is incomplete. Use isComplete() to
+ * retrieve the current state.
+ *
+ * A data form can be used as a dialog. addDialogButton() methods add a dialog button usually at the
+ * bottom of the form. Buttons that have AcceptRole, YesRole or ApplyRole roles are disabled
+ * if the form is \ref isComplete() "incomplete".
+ */
+class DataForm : public QWidget
+{
+	Q_OBJECT
+public:
+	enum ButtonRole {
+		// keep this in sync with QMessageBox::ButtonRole
+		InvalidRole = -1,
+		AcceptRole,
+		RejectRole,
+		DestructiveRole,
+		ActionRole,
+		HelpRole,
+		YesRole,
+		NoRole,
+		ResetRole,
+		ApplyRole,
+
+		NRoles
+	};
+	Q_DECLARE_FLAGS(ButtonRoles, ButtonRole);
+	enum StandardButton {
+		// keep this in sync with QMessageBox::ButtonRole
+		NoButton           = 0x00000000,
+		Ok                 = 0x00000400,
+		Save               = 0x00000800,
+		SaveAll            = 0x00001000,
+		Open               = 0x00002000,
+		Yes                = 0x00004000,
+		YesToAll           = 0x00008000,
+		No                 = 0x00010000,
+		NoToAll            = 0x00020000,
+		Abort              = 0x00040000,
+		Retry              = 0x00080000,
+		Ignore             = 0x00100000,
+		Close              = 0x00200000,
+		Cancel             = 0x00400000,
+		Discard            = 0x00800000,
+		Help               = 0x01000000,
+		Apply              = 0x02000000,
+		Reset              = 0x04000000,
+		RestoreDefaults    = 0x08000000,
+	};
+	Q_DECLARE_FLAGS(StandardButtons, StandardButton);
+
+	enum Flags
+	{
+		ReadOnly
+	};
+
+	DataForm(int flags = 0, QWidget *parent = 0);
+	int flags();
+	/** Creates a new field of type StringChooserField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	StringChooserField *addStringChooser(const QString &value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type StringField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	StringField *addString(const QString &value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type StringListField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	StringListField *addStringList(const QStringList &value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type MultiStringField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	MultiStringField *addMultiString(const QString &value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type BoolField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	BoolField *addBool(bool value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type IntField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	IntField *addInt(int value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type DoubleField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	DoubleField *addDouble(double value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type DateField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	DateField *addDate(const QDate &value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type DateTimeField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	DateTimeField *addDateTime(const QDateTime &value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new field of type PixmapChooserField, initializing its \a value, \a label and \a name.
+	 * Returns pointer to the field.
+	 */
+	PixmapChooserField *addPixmap(const QString &value, const QString &label = QString(), const QString &name = QString());
+	/** Creates a new dialog button with text \a text and role \role.
+	 *
+	 * clickHandler is the functor that is called whenever the button is clicked by the user.
+	 */
+	void addDialogButton(const QString &text, ButtonRole role, const function<void()> &clickHandler);
+	/** Creates a new standard \a button if it is valid to do so. If the \a button is not valid the \a button
+	 * is not added.
+	 *
+	 * clickHandler is the functor that is called whenever the button is clicked by the user.
+	 */
+	void addDialogButton(StandardButton button, const function<void()> &clickHandler);
+	/** Returns true, it the data form is complete.
+	 *
+	 * The data form is complete when every its mandatory field is \ref ValidatedField::complete()
+	 * "complete"
+	 *
+	 * \se completeChanged()
+	 */
+	bool isComplete() const;
+signals:
+	/** The signal is emitted whenever the state of completion of the data form is changed.
+	 *
+	 * \a complete is true, if the data form has become complete, it is false, if the data form
+	 * has become incomplete.
+	 *
+	 * \see isComplete()
+	 */
+	void completeChanged(bool complete);
+private:
+	friend class DataWidgetBase;
+	void ensureButtonBox();
+	void incIncompleteWidgetsCount();
+	void decIncompleteWidgetsCount();
+private:
+	DataFormLayout *m_layout;
+	QVBoxLayout *m_mainLayout;
+	QDialogButtonBox *m_buttonBox;
+	int m_flags;
+    int m_incompleteWidgetsCount;
+};
+
+} // Lime
+
+#endif // LIME_DATAFORMS_H

src/tests/contactmodeltest.h

 };
 
 } // Lime
+
 #endif // LIME_CONTACTMODELTEST_H_

src/tests/testutils.h

 class StaticSignalSpy<void(T *, Args...)>: protected SignalGuard, public SpyBase
 {
 public:
-	StaticSignalSpy<void(T *, Args...)>(T *sender, Signal<void(T *, Args...)> signal) :
+	StaticSignalSpy(T *sender, Signal<void(T *, Args...)> signal) :
 		m_sender(sender), m_signal(signal)
 	{
 		connectSignal();
 	}
-	StaticSignalSpy<void(T *, Args...)>(const StaticSignalSpy<void(T *, Args...)> &other) :
+	StaticSignalSpy(const StaticSignalSpy<void(T *, Args...)> &other) :
 		m_sender(other.m_sender), m_signal(other.m_signal)
 	{
 		connectSignal();
 	}
-	StaticSignalSpy<void(T *, Args...)> &operator=(const StaticSignalSpy<void(T *, Args...)> &other)
+	StaticSignalSpy &operator=(const StaticSignalSpy<void(T *, Args...)> &other)
 	{
 		m_signal.disconnect(m_connId);
 		m_sender = other.m_sender;
 		connectSignal();
 		return *this;
 	}
-	~StaticSignalSpy<void(T *, Args...)>()
+	~StaticSignalSpy()
 	{
 		m_signal.disconnect(m_connId);
 	}
 			}
 		});
 	}
-
 private:
 	T *m_sender;
 	Signal<void(T *, Args...)> m_signal;
 	return StaticSignalSpy<Signature>(sender, signal);
 }
 
+template<typename... Args>
+class StaticSignalSpy<void(Args...)>: protected SignalGuard, public SpyBase
+{
+public:
+	StaticSignalSpy(Signal<void(Args...)> signal) :
+		m_signal(signal)
+	{
+		connectSignal();
+	}
+	StaticSignalSpy(const StaticSignalSpy<void(Args...)> &other) :
+		m_signal(other.m_signal)
+	{
+		connectSignal();
+	}
+	StaticSignalSpy &operator=(const StaticSignalSpy<void(Args...)> &other)
+	{
+		m_signal.disconnect(m_connId);
+		m_signal = other.m_signal;
+		connectSignal();
+		return *this;
+	}
+	~StaticSignalSpy()
+	{
+		m_signal.disconnect(m_connId);
+	}
+private:
+	void connectSignal()
+	{
+		m_connId = m_signal.connect([=](Args... args) {
+			QList<QVariant> values;
+			Private::Appender<sizeof...(Args), Args...>::append(values, args...);
+			*this << values;
+		});
+	}
+private:
+	Signal<void(Args...)> m_signal;
+	int m_connId;
+};
+
+template<typename Signature>
+StaticSignalSpy<Signature> createStaticSignalSpy(Signal<Signature> signal)
+{
+	return StaticSignalSpy<Signature>(signal);
+}
+
 template<typename T, typename Signature>
 class SignalChecker
 {

src/tests/tst_dataforms.cpp

+/*
+	This file is part of Lime.
+	Copyright (C) 2012  Alexey Prokhin <alexey.prokhin@yandex.ru>
+
+	This program is free software: you can redistribute it and/or modify
+	it under the terms of the GNU General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	This program is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU General Public License for more details.
+
+	You should have received a copy of the GNU General Public License
+	along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "tst_dataforms.h"
+#include <QTest>
+#include <QComboBox>
+#include <QLabel>
+#include <QLineEdit>
+#include <QListView>
+#include <QPushButton>
+#include <QTextEdit>
+#include <QCheckBox>
+#include <QSpinBox>
+#include <QDateEdit>
+#include <QValidator>
+#include <QSignalSpy>
+#include <QDialogButtonBox>
+
+namespace QTest {
+
+bool qCompare(const QString &t1, const char *t2, const char *actual,
+			  const char *expected, const char *file, int line)
+{
+	return qCompare<QString>(t1, QString(t2), actual, expected, file, line);
+}
+
+} // QTest
+
+namespace Lime {
+
+#define CHECK_VALUE_HELPER_EX(Type, WidgetMethod, val, widgetVal) \
+	QVERIFY(field->value().type() == QVariant:: Type); \
+	QCOMPARE(field->value().to##Type (), val); \
+	QCOMPARE(widget-> WidgetMethod, widgetVal); \
+
+#define CHECK_VALUE_HELPER(Type, WidgetMethod, val) \
+	CHECK_VALUE_HELPER_EX(Type, WidgetMethod, val, val)
+
+#define CHECK_NEW_VALUE_HELPER_EX(Type, WidgetMethod, newVal, widgetVal) \
+	CHECK_VALUE_HELPER_EX(Type, WidgetMethod, newVal, widgetVal) \
+	QCOMPARE(spy.count(), 1); \
+	spy.removeFirst();
+
+#define CHECK_NEW_VALUE_HELPER(Type, WidgetMethod, newVal) \
+	CHECK_NEW_VALUE_HELPER_EX(Type, WidgetMethod, newVal, newVal)
+
+class TestValidator : public QValidator
+{
+public:
+	TestValidator(int len) :
+		m_len(len)
+	{}
+	State validate(QString &str, int &) const
+	{
+		if (str.length() < m_len)
+			return Intermediate;
+		else if (str.length() == m_len)
+			return Acceptable;
+		return Invalid;
+	}
+private:
+	int m_len;
+};
+
+QString DataFormsTest::getLabelHelper(DataField *field)
+{
+	Q_ASSERT(field->labelWidget());
+	auto str = field->labelWidget()->text();
+	str.resize(str.length() - 1);