Commit 682f8126 authored by Yann Boucher's avatar Yann Boucher
Browse files

Implementé #3 et #22; mise en place d'une structure du projet (src/ d'une...

Implementé #3 et #22; mise en place d'une structure du projet (src/ d'une part, tests/ d'autre part)
parent 7ddee743
Pipeline #76524 passed with stages
in 16 seconds
TEMPLATE = subdirs
CONFIG += ordered
SUBDIRS += \
src \
tests
/**
\file property.hpp
\date 17/04/2021
\author Yann Boucher
\version 1
\brief Property
Fichier définissant la classe Property représentant une propriété chargable, sauvegardable et affichable d'un objet.
**/
#ifndef PROPERTY_HPP
#define PROPERTY_HPP
#include <string>
#include <vector>
#include <memory>
#include <limits>
#include <exception>
#include <QWidget>
#include <QLayout>
#include <QLabel>
#include <QLineEdit>
#include <QSpinBox>
#include <QMessageBox>
#include <QFormLayout>
#include <QJsonDocument>
#include <QJsonObject>
#include <QFile>
struct StringProperty;
struct IntegerProperty;
struct CoordinateProperty;
struct PropertyList;
/**
\class PropertyVisitor
\brief Représente un visiteur de propriété
Visiteur des différentes classes filles de Property. Destinée à être héritée et concrétisée pour par exemple sauvegarder, charger ou afficher une propriété.
**/
class PropertyVisitor
{
public:
virtual ~PropertyVisitor() = default;
virtual void visit(StringProperty& str) = 0;
virtual void visit(IntegerProperty& str) = 0;
virtual void visit(CoordinateProperty& coord) = 0;
virtual void visit(PropertyList& str) = 0;
};
/**
\class Property
\brief Représente une propriété
Une Property est un attribut membre d'une classe, pouvant être facilement visité par des PropertyVisitor afin de permettre facilement par exemple un affichage à l'aide de widgets des propriétés d'un objet, et facilitant ainsi le paramètrage par l'utilisateur d'un tel objet.
**/
class Property
{
friend class HasUserProperties;
public:
//! \brief Constructeur virtuel par défaut.
virtual ~Property() = default;
//! \brief Accepte un visiteur. Cette fonction est automatiquement redéfinie correctement en héritant de PropertyImpl<Classe>?
virtual void accept(PropertyVisitor& v) = 0;
//! \brief Retourne le nom de la propriété.
std::string name() const
{ return prop_name; }
private:
std::string prop_name;
};
/**
\class PropertyImpl
Classe utilitaire permettant de définir automatiquement 'accept' parmi les classes implémentant un type de Property.
*/
template <typename Child>
class PropertyImpl : public Property
{
public:
/**
\internal
\brief Accepte le vitieur passé en argument afin de faire fonctionner le double-dispatch.
**/
virtual void accept(PropertyVisitor& v)
{
v.visit(static_cast<Child&>(*this));
}
//! \endinternal
};
/**
\class HasUserProperties
Permet à une classe héritant de HasUSerProperties d'avoir des objets Property fonctionnels.
**/
class HasUserProperties
{
public:
//! \brief Retourne l'ensemble des Property associées à l'objet.
const std::vector<std::unique_ptr<Property>>& get_properties() const
{
return m_properties;
}
protected:
template <typename PropertyType, typename... Args>
PropertyType& register_property(const std::string& name, Args&&... args)
{
m_properties.emplace_back(std::make_unique<PropertyType>(std::forward<Args>(args)...));
m_properties.back()->prop_name = name;
return static_cast<PropertyType&>(*m_properties.back());
}
private:
std::vector<std::unique_ptr<Property>> m_properties;
};
/**
\class StringProperty
Représente une propriété textuelle.
**/
struct StringProperty : public PropertyImpl<StringProperty>
{
public:
//! \brief Constructeur par défaut.
StringProperty() = default;
/** \brief Constructeur avec valeur initiale.
\package initval Valeur initiale.
**/
StringProperty(const std::string& initval)
: str(initval)
{}
public:
std::string str;
};
/**
\class IntegerProperty
Représente une propriété entière bornée.
**/
struct IntegerProperty : public PropertyImpl<IntegerProperty>
{
public:
//! \brief Construit un objet IntegerProperty avec une valeur min et une valeur max, non borné par défaut.
IntegerProperty(int min_value = std::numeric_limits<int>::min(), int max_value = std::numeric_limits<int>::max())
: prop_min(min_value), prop_max(max_value)
{}
public:
int val = 0;
const int prop_min, prop_max;
};
/**
\class CoordinateProperty
Représente une coordonnée relative 2D.
**/
struct CoordinateProperty : public PropertyImpl<CoordinateProperty>
{
public:
int x = 0, y = 0;
};
/**
\class PropertyList
Représente une liste de propriétés.
\todo Documenter l'ensemble des fonctions membres.
**/
struct PropertyList : public PropertyImpl<PropertyList>
{
public:
PropertyList(std::unique_ptr<Property>(*create_function)(void), bool is_dynamic = false, int in_min_size = 0, int in_max_size = std::numeric_limits<int>::max())
: dynamic(is_dynamic), min_size(in_min_size), max_size(in_max_size), m_create_function(create_function)
{
assert(m_create_function);
for (int i = 0; i < min_size; ++i)
push_back();
}
bool empty() const
{ return contents.empty(); }
void clear()
{ contents.clear(); }
size_t size() const
{ return contents.size(); }
Property& push_back()
{
if ((int)size() < max_size)
contents.emplace_back(m_create_function());
return back();
}
void pop_back()
{
if (dynamic && !contents.empty() && (int)size() > min_size)
contents.pop_back();
}
Property& back()
{ return *contents.back(); }
Property& at(size_t i)
{
if (i < size())
return *contents[i];
else
throw std::out_of_range("Invalid index value");
}
public:
const bool dynamic;
const int min_size;
const int max_size;
std::vector<std::unique_ptr<Property>> contents;
private:
std::unique_ptr<Property>(*m_create_function)(void);
};
/** Permet de déclarer une variable propriété 'identifier' de type 'prop_type', avec comme nom affichable 'name'.
\param prop_type Type de la propriété.
\param identifier Nom de la variable dans le code.
\param name Nom affichable de la propriété.
\param ... Arguments supplémentaires à passer au constructeur de 'prop_type'
**/
#define DEFINE_CONFIGURABLE_PROPERTY(prop_type, identifier, name, ...) \
prop_type& identifier = register_property<prop_type>(name, ##__VA_ARGS__)
/** Permet de déclarer une liste fixée de propriétés 'identifier' de type 'prop_type', avec comme nom affichable 'name'.
\param prop_type Type des propriétés.
\param identifier Nom de la variable dans le code.
\param name Nom affichable de la liste de propriétés.
\param ... Arguments supplémentaires à passer au constructeur de 'prop_type' lors de la créatin d'une propriété dans la liste
**/
#define DEFINE_CONFIGURABLE_LIST(prop_type, identifier, name, ...) \
PropertyList& identifier = register_property<PropertyList>(name, []()->std::unique_ptr<Property>{ return std::make_unique<prop_type>(__VA_ARGS__); false, ##__VA_ARGS__ })
/** Permet de déclarer une liste dynamique de propriétés 'identifier' de type 'prop_type', avec comme nom affichable 'name'.
\param prop_type Type des propriétés.
\param identifier Nom de la variable dans le code.
\param name Nom affichable de la liste de propriétés.
\param inner_args Arguments supplémentaires à passer au constructeur de 'prop_type' lors de la créatin d'une propriété dans la liste
\param ... Arguments supplémentaires à passer au constructeur de PropertyList.
**/
#define DEFINE_CONFIGURABLE_DYNLIST(prop_type, identifier, name, inner_args, ...) \
PropertyList& identifier = register_property<PropertyList>(name, []()->std::unique_ptr<Property>{ return std::make_unique<prop_type> inner_args; }, true, ##__VA_ARGS__ )
#endif // PROPERTY_HPP
/**
\file propertyvisitors.hpp
\date 17/04/2021
\author Yann Boucher
\version 1
\brief Classes filles de PropertyVisitor
Fichier contenant diverses classes filles de PropertyVisitor permettant l'affichage sur l'interface, le chargement et la sauvegarder en JSON d'objets Property.
\todo Documenter les méthodes
**/
#ifndef VISITORS_HPP
#define VISITORS_HPP
#include <stack>
#include <QVariant>
#include "property.hpp"
class UIBuilderVisitor : public PropertyVisitor
{
public:
// destrucive = true if we want to rebuild the layout of the base_widget passed in parameter
UIBuilderVisitor(QWidget* base_widget, bool destructive = true);
private:
void add_widget(const std::string& prop_name, QWidget* ptr);
void clear_layout(QLayout *layout);
QWidget* current_widget();
void push_array_widget(const Property& prop);
QWidget *pop_widget();
private:
void visit(StringProperty& str);
void visit(IntegerProperty& prop);
void visit(CoordinateProperty& prop);
void visit(PropertyList& list);
private:
std::stack<QWidget*> m_widget_hierarchy;
};
class PropertySaverVisitor : public PropertyVisitor
{
public:
PropertySaverVisitor();
void save(const std::string& filename);
private:
QVariant& current();
void save_value(Property& prop, const QJsonValue& val);
void push_object();
void push_array();
QJsonValue pop_value();
void visit(StringProperty& prop);
void visit(IntegerProperty& prop);
void visit(CoordinateProperty& prop);
void visit(PropertyList& list);
private:
std::stack<QVariant> m_current_hierarchy;
};
class PropertyLoaderVisitor : public PropertyVisitor
{
public:
PropertyLoaderVisitor(const std::string& filename);
private:
QJsonValue& current();
void enter_value(const QJsonValue& value);
QJsonValue exit_value();
QJsonValue fetch_value(Property& prop);
void visit(StringProperty& prop);
void visit(IntegerProperty& prop);
void visit(CoordinateProperty& prop);
void visit(PropertyList& prop);
private:
std::stack<QJsonValue> m_current_hierarchy;
};
#endif // VISITORS_HPP
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
return a.exec();
}
#include "propertyvisitors.hpp"
#include <QJsonArray>
#include <QGroupBox>
#include <QPushButton>
UIBuilderVisitor::UIBuilderVisitor(QWidget *base_widget, bool destructive)
{
assert(base_widget != nullptr);
if (destructive)
{
// use a form layout on the parent widget
if (base_widget->layout())
{
clear_layout(base_widget->layout());
delete base_widget->layout();
}
base_widget->setLayout(new QFormLayout);
}
else
assert(base_widget->layout() != nullptr);
m_widget_hierarchy.push(base_widget);
}
void UIBuilderVisitor::add_widget(const std::string &prop_name, QWidget *ptr)
{
// handle whether the current widget is for JSON objects or JSON arrays
QFormLayout* form_layout = qobject_cast<QFormLayout*>(current_widget()->layout());
QVBoxLayout* vbox_layout = qobject_cast<QVBoxLayout*>(current_widget()->layout());
if (form_layout)
{
if (prop_name.empty())
form_layout->addRow(ptr);
else
form_layout->addRow(QString::fromStdString(prop_name) + " : ", ptr);
}
else if (vbox_layout)
{
vbox_layout->addWidget(ptr);
}
}
void UIBuilderVisitor::clear_layout(QLayout *layout) {
QLayoutItem *item;
while(layout->count() && (item = layout->takeAt(0))) {
if (item->layout()) {
clear_layout(item->layout());
item->layout()->deleteLater();
}
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
}
QWidget *UIBuilderVisitor::current_widget()
{
assert(!m_widget_hierarchy.empty());
return m_widget_hierarchy.top();
}
void UIBuilderVisitor::push_array_widget(const Property &prop)
{
QGroupBox* box = new QGroupBox(QString::fromStdString(prop.name()), current_widget());
box->setLayout(new QVBoxLayout);
add_widget("", box);
m_widget_hierarchy.push(box);
}
QWidget* UIBuilderVisitor::pop_widget()
{
assert(!m_widget_hierarchy.empty());
QWidget* ptr = m_widget_hierarchy.top();
m_widget_hierarchy.pop();
return ptr;
}
void UIBuilderVisitor::visit(StringProperty &str)
{
QLineEdit* line = new QLineEdit(QString::fromStdString(str.str), current_widget());
add_widget(str.name(), line);
QObject::connect(line, &QLineEdit::textEdited,
[&str](const QString& qstr) { str.str = qstr.toStdString(); });
}
void UIBuilderVisitor::visit(IntegerProperty &prop)
{
QSpinBox* spin = new QSpinBox(current_widget());
add_widget(prop.name(), spin);
spin->setValue(prop.val);
spin->setMinimum(prop.prop_min);
spin->setMaximum(prop.prop_max);
QObject::connect(spin, QOverload<int>::of(&QSpinBox::valueChanged),
[&prop](int i) { prop.val = i; });
}
void UIBuilderVisitor::visit(CoordinateProperty &prop)
{
QWidget* frame = new QWidget(current_widget());
frame->setLayout(new QHBoxLayout);
frame->layout()->setContentsMargins(0,0,0,0);
QSpinBox* spin_x = new QSpinBox(frame);
QSpinBox* spin_y = new QSpinBox(frame);
frame->layout()->addWidget(new QLabel("x :", frame));
frame->layout()->addWidget(spin_x);
frame->layout()->addWidget(new QLabel("y :", frame));
frame->layout()->addWidget(spin_y);
frame->layout()->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum));
add_widget(prop.name(), frame);
spin_x->setValue(prop.x); spin_y->setValue(prop.y);
QObject::connect(spin_x, QOverload<int>::of(&QSpinBox::valueChanged),
[&prop](int i) { prop.x = i; });
QObject::connect(spin_y, QOverload<int>::of(&QSpinBox::valueChanged),
[&prop](int i) { prop.y = i; });
}
void UIBuilderVisitor::visit(PropertyList &list)
{
push_array_widget(list);
for (const auto& ptr : list.contents)
{
ptr->accept(*this);
}
QWidget* list_widget = pop_widget();
if (list.dynamic)
{
QWidget* frame = new QWidget(current_widget());
frame->setLayout(new QHBoxLayout);
frame->layout()->setContentsMargins(0,0,0,0);
QPushButton* add_button = new QPushButton("Ajouter...", current_widget());
QPushButton* del_button = new QPushButton("Supprimer...", current_widget());
frame->layout()->addWidget(add_button);
frame->layout()->addWidget(del_button);
frame->layout()->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum));
add_widget("", frame);
QObject::connect(add_button, &QPushButton::pressed,
[&list, add_button, del_button, list_widget]() {
list.push_back();
UIBuilderVisitor visit(list_widget, false); // keep the layout of the previous
list.back().accept(visit);
if (list_widget->layout()->count() >= (int)list.max_size)
add_button->setEnabled(false);
if (list_widget->layout()->count() > (int)list.min_size)
del_button->setEnabled(true);
});
QObject::connect(del_button, &QPushButton::pressed,
[&list, add_button, list_widget, del_button]() {
if (!list.empty())
{
list.pop_back();
assert(list_widget->layout()->count() > 0);
QLayoutItem* child = list_widget->layout()->takeAt(list_widget->layout()->count()-1);
delete child->widget();
delete child;
if (list_widget->layout()->count() <= (int)list.min_size) // no more items to delete, grey the button out
del_button->setEnabled(false);
if (list_widget->layout()->count() < (int)list.max_size)
add_button->setEnabled(true);
}
});
}
}
PropertySaverVisitor::PropertySaverVisitor()
{
// create the top level json object
push_object();
}
void PropertySaverVisitor::save(const std::string &filename)
{
//6. Create a QByteArray and fill it with QJsonDocument (json formatted)
QByteArray byteArray;
byteArray = QJsonDocument(current().toJsonObject()).toJson();
//7. Open a QFile and write the byteArray filled with json formatted data
//thanks to the QJsonDocument() Class to the file
QFile file;
file.setFileName(filename.c_str());
if(!file.open(QIODevice::WriteOnly)){
qDebug() << "No write access for json file";
return;
}
//8. finally write the file and close it
file.write(byteArray);
file.close();
}
QVariant &PropertySaverVisitor::current()
{
assert(!m_current_hierarchy.empty());
return m_current_hierarchy.top();
}
void PropertySaverVisitor::save_value(Property &prop, const QJsonValue &val)
{
if (current().canConvert<QJsonArray>())
{
QJsonArray arr = current().toJsonArray();
arr.append(val);
current() = arr;
}
else
{
QJsonObject obj = current().toJsonObject();
obj[QString::fromStdString(prop.name())] = val;
current() = obj;
}
}
void PropertySaverVisitor::push_object()
{
m_current_hierarchy.push(QJsonObject{});
}
void PropertySaverVisitor::push_array()
{
m_current_hierarchy.push(QJsonArray{});
}
QJsonValue PropertySaverVisitor::pop_value()
{
QJsonValue val;
if (current().canConvert<QJsonArray>())
val = QJsonValue(current().toJsonArray());
else
val = QJsonValue(current().toJsonObject());
m_current_hierarchy.pop();
return val;