Skip to content
Snippets Groups Projects
Commit 6852df5e authored by m.puchner's avatar m.puchner
Browse files

Merge branch 'SampleListView' into 'develop'

Introduce sample selection list

See merge request !405
parents 8e04c690 be627f94
No related branches found
No related tags found
1 merge request!405Introduce sample selection list
Pipeline #47245 passed
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
// //
// BornAgain: simulate and fit reflection and scattering // BornAgain: simulate and fit reflection and scattering
// //
//! @file GUI/Views/ImportDataWidgets/SamplesTreeModel.cpp //! @file GUI/Views/ImportDataWidgets/SampleListModel.cpp
//! @brief Implements class SamplesTreeModel //! @brief Implements class SampleListModel
//! //!
//! @homepage http://www.bornagainproject.org //! @homepage http://www.bornagainproject.org
//! @license GNU General Public License v3 or higher (see COPYING) //! @license GNU General Public License v3 or higher (see COPYING)
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
// //
// ************************************************************************************************ // ************************************************************************************************
#include "GUI/Models/SamplesTreeModel.h" #include "GUI/Models/SampleListModel.h"
#include "GUI/Application/Application.h" #include "GUI/Application/Application.h"
#include "GUI/Models/GUIExamplesFactory.h" #include "GUI/Models/GUIExamplesFactory.h"
#include "GUI/Models/ModelUtils.h" #include "GUI/Models/ModelUtils.h"
...@@ -23,130 +23,65 @@ ...@@ -23,130 +23,65 @@
#include <QFontMetrics> #include <QFontMetrics>
#include <QIcon> #include <QIcon>
SamplesTreeModel::SamplesTreeModel(QObject* parent, SampleModel* model) SampleListModel::SampleListModel(QObject* parent, SampleModel* model)
: QAbstractItemModel(parent), m_sampleModel(model) : QAbstractListModel(parent), m_sampleModel(model)
{ {
connect(m_sampleModel, &SampleModel::modelAboutToBeReset, this, &SamplesTreeModel::clear, connect(m_sampleModel, &SampleModel::modelAboutToBeReset, this,
Qt::UniqueConnection); &SampleListModel::beginResetModel, Qt::UniqueConnection);
}
void SamplesTreeModel::clear() connect(m_sampleModel, &SampleModel::modelReset, this, &SampleListModel::endResetModel,
{ Qt::UniqueConnection);
beginResetModel();
endResetModel();
} }
MultiLayerItem* SamplesTreeModel::topMostItem() const MultiLayerItem* SampleListModel::topMostItem() const
{ {
return m_sampleModel->topItem<MultiLayerItem>(); return m_sampleModel->topItem<MultiLayerItem>();
} }
QModelIndex SamplesTreeModel::index(int row, int column, const QModelIndex& parent) const int SampleListModel::rowCount(const QModelIndex& parent) const
{ {
if (!hasIndex(row, column, parent)) if (parent.isValid())
return QModelIndex(); return 0;
if (!parent.isValid())
return createIndex(row, column, nullptr);
return createIndex(row, column, m_sampleModel->multiLayerItems()[row]);
}
QModelIndex SamplesTreeModel::parent(const QModelIndex& index) const
{
if (!index.isValid())
return QModelIndex();
if (index.internalPointer() == nullptr) // index is headline => no parent
return QModelIndex();
return createIndex(0, 0, nullptr); return m_sampleModel->multiLayerItems().size();
} }
int SamplesTreeModel::columnCount(const QModelIndex& /*parent*/) const QVariant SampleListModel::data(const QModelIndex& index, int role) const
{ {
return 1;
}
int SamplesTreeModel::rowCount(const QModelIndex& parent) const
{
if (!parent.isValid())
return 1;
// parent is a headline
if (isHeadline(parent))
return m_sampleModel->multiLayerItems().size();
return 0; // parent is an item
}
QVariant SamplesTreeModel::data(const QModelIndex& index, int role) const
{
if (isHeadline(index)) {
const QString title = "Samples";
switch (role) {
case Qt::DisplayRole:
return title;
case Qt::FontRole: {
QFont f(QApplication::font());
f.setPointSize(f.pointSize() * 1.5);
f.setBold(true);
return f;
}
case Qt::SizeHintRole: {
QFont f(QApplication::font());
f.setPointSize(f.pointSize() * 1.5);
f.setBold(true);
QSize s = QFontMetrics(f).boundingRect(title).size();
return QSize(s.width() * 2, s.height() * 2);
}
case Qt::TextAlignmentRole:
return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
case Qt::BackgroundRole:
return baApp->styleSheetPalette().headlineBackground;
case Qt::ForegroundRole:
return baApp->styleSheetPalette().headlineText;
default:
return QVariant();
}
}
const auto item = itemForIndex(index); const auto item = itemForIndex(index);
if (role == Qt::ToolTipRole) if (role == Qt::ToolTipRole)
return item->description(); return item->description();
if (role == Qt::DisplayRole) if (role == Qt::DisplayRole) {
return item->itemName(); auto descr = item->description();
if (!descr.isEmpty()) {
descr.prepend("<br><br>");
const int maxDescriptionLines = 8;
while (descr.count("\n") >= maxDescriptionLines) {
descr.truncate(descr.lastIndexOf("\n"));
descr += " [...]";
}
descr.replace("\n", "<br>");
}
return "<b>" + item->itemName() + "</b>" + descr;
}
if (role == Qt::EditRole) if (role == Qt::EditRole)
return item->itemName(); return item->itemName();
if (role == Qt::DecorationRole)
return QIcon(":/SampleDesignerToolbox/images/sample_layers2.png");
return QVariant(); return QVariant();
} }
Qt::ItemFlags SamplesTreeModel::flags(const QModelIndex& index) const Qt::ItemFlags SampleListModel::flags(const QModelIndex& index) const
{ {
if (isHeadline(index) || !index.isValid())
return Qt::NoItemFlags;
auto f = QAbstractItemModel::flags(index); auto f = QAbstractItemModel::flags(index);
f |= Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled; f |= Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
return f; return f;
} }
bool SamplesTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) bool SampleListModel::setData(const QModelIndex& index, const QVariant& value, int role)
{ {
if (!index.isValid()) if (!index.isValid())
return false; return false;
...@@ -166,26 +101,23 @@ bool SamplesTreeModel::setData(const QModelIndex& index, const QVariant& value, ...@@ -166,26 +101,23 @@ bool SamplesTreeModel::setData(const QModelIndex& index, const QVariant& value,
return false; return false;
} }
MultiLayerItem* SamplesTreeModel::itemForIndex(const QModelIndex& index) const MultiLayerItem* SampleListModel::itemForIndex(const QModelIndex& index) const
{ {
if (!index.isValid()) if (!index.isValid())
return nullptr; return nullptr;
return reinterpret_cast<MultiLayerItem*>(index.internalPointer()); return m_sampleModel->multiLayerItems()[index.row()];
} }
QModelIndex SamplesTreeModel::indexForItem(MultiLayerItem* item) const QModelIndex SampleListModel::indexForItem(MultiLayerItem* item) const
{ {
if (item == nullptr)
return QModelIndex();
if (auto row = m_sampleModel->multiLayerItems().indexOf(item); row >= 0) if (auto row = m_sampleModel->multiLayerItems().indexOf(item); row >= 0)
return createIndex(row, 0, item); return index(row, 0);
return QModelIndex(); return QModelIndex();
} }
void SamplesTreeModel::removeSample(MultiLayerItem* item) void SampleListModel::removeSample(MultiLayerItem* item)
{ {
QModelIndex index = indexForItem(item); QModelIndex index = indexForItem(item);
if (!index.isValid()) if (!index.isValid())
...@@ -196,32 +128,24 @@ void SamplesTreeModel::removeSample(MultiLayerItem* item) ...@@ -196,32 +128,24 @@ void SamplesTreeModel::removeSample(MultiLayerItem* item)
endRemoveRows(); endRemoveRows();
} }
bool SamplesTreeModel::isHeadline(const QModelIndex& index) const QModelIndex SampleListModel::createSample()
{
if (!index.isValid())
return false;
return index.internalPointer() == nullptr;
}
QModelIndex SamplesTreeModel::createSample()
{ {
using namespace GUI::Model::ItemUtils; using namespace GUI::Model::ItemUtils;
const int row = m_sampleModel->multiLayerItems().size(); const int row = m_sampleModel->multiLayerItems().size();
beginInsertRows(index(0, 0), row, row); beginInsertRows(QModelIndex(), row, row);
auto multilayer_item = m_sampleModel->insertItem<MultiLayerItem>(); auto multilayer_item = m_sampleModel->insertItem<MultiLayerItem>();
multilayer_item->setItemName(suggestName(topItemNames(m_sampleModel), "Sample")); multilayer_item->setItemName(suggestName(topItemNames(m_sampleModel), "Sample"));
endInsertRows(); endInsertRows();
return indexForItem(multilayer_item); return indexForItem(multilayer_item);
} }
QModelIndex SamplesTreeModel::createSampleFromExamples(const QString& className, QModelIndex SampleListModel::createSampleFromExamples(const QString& className,
const QString& title, const QString& title,
const QString& description) const QString& description)
{ {
const int row = m_sampleModel->multiLayerItems().size(); const int row = m_sampleModel->multiLayerItems().size();
beginInsertRows(index(0, 0), row, row); beginInsertRows(QModelIndex(), row, row);
auto* sample = dynamic_cast<MultiLayerItem*>(GUIExamplesFactory::createSampleItems( auto* sample = dynamic_cast<MultiLayerItem*>(GUIExamplesFactory::createSampleItems(
className, m_sampleModel, ProjectManager::instance()->document()->materialModel())); className, m_sampleModel, ProjectManager::instance()->document()->materialModel()));
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
// //
// BornAgain: simulate and fit reflection and scattering // BornAgain: simulate and fit reflection and scattering
// //
//! @file GUI/Views/ImportDataWidgets/SamplesTreeModel.h //! @file GUI/Views/ImportDataWidgets/SampleListModel.h
//! @brief Defines class SamplesTreeModel //! @brief Defines class SampleListModel
//! //!
//! @homepage http://www.bornagainproject.org //! @homepage http://www.bornagainproject.org
//! @license GNU General Public License v3 or higher (see COPYING) //! @license GNU General Public License v3 or higher (see COPYING)
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
// //
// ************************************************************************************************ // ************************************************************************************************
#ifndef BORNAGAIN_GUI_VIEWS_IMPORTDATAWIDGETS_SAMPLESTREEMODEL_H #ifndef BORNAGAIN_GUI_VIEWS_IMPORTDATAWIDGETS_SAMPLELISTMODEL_H
#define BORNAGAIN_GUI_VIEWS_IMPORTDATAWIDGETS_SAMPLESTREEMODEL_H #define BORNAGAIN_GUI_VIEWS_IMPORTDATAWIDGETS_SAMPLELISTMODEL_H
#include <QAbstractItemModel> #include <QAbstractItemModel>
#include <QSet> #include <QSet>
...@@ -21,20 +21,16 @@ ...@@ -21,20 +21,16 @@
class SampleModel; class SampleModel;
class MultiLayerItem; class MultiLayerItem;
//! Tree model for sample selection. //! List model for sample selection (used in the left pane of the layer oriented sample editor)
class SamplesTreeModel : public QAbstractItemModel { class SampleListModel : public QAbstractListModel {
public: public:
SamplesTreeModel(QObject* parent, SampleModel* model); SampleListModel(QObject* parent, SampleModel* model);
virtual QModelIndex index(int row, int column,
const QModelIndex& parent = QModelIndex()) const override;
virtual QModelIndex parent(const QModelIndex& index) const override;
virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override;
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override; virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override;
MultiLayerItem* itemForIndex(const QModelIndex& index) const; MultiLayerItem* itemForIndex(const QModelIndex& index) const;
QModelIndex indexForItem(MultiLayerItem* item) const; QModelIndex indexForItem(MultiLayerItem* item) const;
...@@ -44,19 +40,13 @@ public: ...@@ -44,19 +40,13 @@ public:
//! The topmost visible item. Can be null of course. //! The topmost visible item. Can be null of course.
MultiLayerItem* topMostItem() const; MultiLayerItem* topMostItem() const;
bool isHeadline(const QModelIndex& index) const;
//! Create a new sample (multilayer) and return the index of it. //! Create a new sample (multilayer) and return the index of it.
QModelIndex createSample(); QModelIndex createSample();
QModelIndex createSampleFromExamples(const QString& className, const QString& title, QModelIndex createSampleFromExamples(const QString& className, const QString& title,
const QString& description); const QString& description);
private:
void clear();
private: private:
SampleModel* m_sampleModel = nullptr; SampleModel* m_sampleModel = nullptr;
}; };
#endif // BORNAGAIN_GUI_VIEWS_IMPORTDATAWIDGETS_SAMPLESTREEMODEL_H #endif // BORNAGAIN_GUI_VIEWS_IMPORTDATAWIDGETS_SAMPLELISTMODEL_H
// ************************************************************************************************
//
// BornAgain: simulate and fit reflection and scattering
//
//! @file GUI/Views/SampleDesigner/SampleListView.cpp
//! @brief Implements class SampleListView
//!
//! @homepage http://www.bornagainproject.org
//! @license GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2021
//! @authors Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
// ************************************************************************************************
#include "GUI/Views/SampleDesigner/SampleListView.h"
#include "GUI/Application/Application.h"
#include "GUI/Models/GUIExamplesFactory.h"
#include "GUI/Models/MultiLayerItem.h"
#include "GUI/Models/SampleListModel.h"
#include "GUI/Views/CommonWidgets/ItemViewOverlayButtons.h"
#include "GUI/utils/ItemDelegateForHTML.h"
#include <QAction>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLineEdit>
#include <QMenu>
#include <QPainter>
#include <QPushButton>
#include <QTextEdit>
namespace {
class ItemDelegateForSampleTree : public ItemDelegateForHTML {
public:
ItemDelegateForSampleTree(QObject* parent) : ItemDelegateForHTML(parent) {}
protected:
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
ItemDelegateForHTML::paint(painter, option, index);
QStyleOptionViewItem options = option;
initStyleOption(&options, index);
painter->save();
painter->setPen(QPen(Qt::lightGray, 1));
painter->drawLine(options.rect.left(), options.rect.bottom(), options.rect.right(),
options.rect.bottom());
painter->restore();
}
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
auto s = ItemDelegateForHTML::sizeHint(option, index);
s.setHeight(std::max(s.height(), 32));
return s;
}
};
} // namespace
SampleListView::SampleListView(QWidget* parent, SampleModel* sampleModel) : QListView(parent)
{
m_model = new SampleListModel(this, sampleModel);
setContextMenuPolicy(Qt::CustomContextMenu);
setModel(m_model);
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
ItemViewOverlayButtons::install(
this, [=](const QModelIndex& i, bool h) { return getOverlayActions(i, h); });
setItemDelegate(new ItemDelegateForSampleTree(this));
connect(selectionModel(), &QItemSelectionModel::currentChanged, this,
&SampleListView::onCurrentChanged);
connect(this, &QWidget::customContextMenuRequested, this, &SampleListView::showContextMenu);
m_newSampleAction = new QAction(this);
m_newSampleAction->setText("Create new sample");
m_newSampleAction->setIcon(QIcon(":/images/shape-square-plus.svg"));
m_newSampleAction->setIconText("New");
m_newSampleAction->setToolTip("Create new sample");
connect(m_newSampleAction, &QAction::triggered, this, &SampleListView::createNewSample);
m_chooseFromLibraryAction = new QAction(this);
m_chooseFromLibraryAction->setText("Choose from sample library");
m_chooseFromLibraryAction->setIcon(QIcon(":/images/library.svg"));
m_chooseFromLibraryAction->setIconText("Library");
m_chooseFromLibraryAction->setToolTip("Choose from sample library");
QMenu* menu = new QMenu(this);
m_chooseFromLibraryAction->setMenu(menu);
for (auto exampleName : GUIExamplesFactory::exampleNames()) {
QString title, description;
std::tie(title, description) = GUIExamplesFactory::exampleInfo(exampleName);
auto icon = QIcon(":/SampleDesignerToolbox/images/sample_layers2.png");
auto* action = menu->addAction(icon, title);
action->setToolTip(description);
connect(action, &QAction::triggered,
[=]() { createSampleFromLibrary(exampleName, title, description); });
}
}
void SampleListView::setCurrentSample(MultiLayerItem* multiLayer)
{
setCurrentIndex(m_model->indexForItem(multiLayer));
}
MultiLayerItem* SampleListView::currentSample()
{
return m_model->itemForIndex(currentIndex());
}
QAction* SampleListView::newSampleAction()
{
return m_newSampleAction;
}
QAction* SampleListView::chooseFromLibraryAction()
{
return m_chooseFromLibraryAction;
}
QSize SampleListView::sizeHint() const
{
QSize s = QListView::sizeHint();
s.setWidth(std::max(300, s.width()));
return s;
}
void SampleListView::createNewSample()
{
const QModelIndex newIndex = m_model->createSample();
setCurrentIndex(newIndex);
}
void SampleListView::createSampleFromLibrary(const QString& classname, const QString& title,
const QString& description)
{
const QModelIndex newIndex = m_model->createSampleFromExamples(classname, title, description);
setCurrentIndex(newIndex);
}
QList<QAction*> SampleListView::getOverlayActions(const QModelIndex& index, bool asHover)
{
if (!asHover)
return {};
auto item = m_model->itemForIndex(index);
if (item == nullptr)
return {};
return {createEditAction(this, item), createRemoveAction(this, item)};
}
void SampleListView::editNameAndDescription(MultiLayerItem* item)
{
QDialog dlg(this);
dlg.setObjectName("NameAndDescriptionDialog");
dlg.setWindowTitle("Edit name and description of sample");
dlg.setWindowFlag(Qt::WindowContextHelpButtonHint, false);
dlg.setProperty("stylable", true); // for stylesheet addressing
QFormLayout* layout = new QFormLayout(&dlg);
QLineEdit* nameEdit = new QLineEdit(&dlg);
nameEdit->setText(item->itemName());
nameEdit->selectAll();
QTextEdit* descriptionEdit = new QTextEdit(&dlg);
descriptionEdit->setMinimumWidth(200);
descriptionEdit->setAcceptRichText(false);
descriptionEdit->setTabChangesFocus(true);
descriptionEdit->setPlainText(item->description());
QDialogButtonBox* buttonBox = new QDialogButtonBox(&dlg);
buttonBox->addButton(QDialogButtonBox::Ok);
buttonBox->addButton(QDialogButtonBox::Cancel);
buttonBox->button(QDialogButtonBox::Ok)->setAutoDefault(true);
buttonBox->button(QDialogButtonBox::Ok)->setDefault(true);
layout->addRow("Name:", nameEdit);
layout->addRow("Description:", descriptionEdit);
layout->addWidget(buttonBox);
dlg.setLayout(layout);
connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
baApp->settings().loadWindowSizeAndPos(&dlg);
if (dlg.exec() == QDialog::Accepted) {
QModelIndex index = m_model->indexForItem(item);
m_model->setData(index, nameEdit->text(), Qt::EditRole);
m_model->setData(index, descriptionEdit->toPlainText(), Qt::ToolTipRole);
// if length of description changes height of TreeItem, the position of the overlay buttons
// has to be updated -> schedule a re-layout
scheduleDelayedItemsLayout();
}
baApp->settings().saveWindowSizeAndPos(&dlg);
}
void SampleListView::onCurrentChanged(const QModelIndex& index)
{
emit currentSampleChanged(m_model->itemForIndex(index));
}
QAction* SampleListView::createEditAction(QObject* parent, MultiLayerItem* item)
{
QAction* editAction = new QAction(parent);
editAction->setText("Edit name and description");
editAction->setIcon(QIcon(":/images/edit.svg"));
editAction->setIconText("Edit");
editAction->setToolTip("Edit name and description");
connect(editAction, &QAction::triggered, [=]() { editNameAndDescription(item); });
return editAction;
}
QAction* SampleListView::createRemoveAction(QObject* parent, MultiLayerItem* item)
{
QAction* removeAction = new QAction(parent);
removeAction->setText("Remove");
removeAction->setIcon(QIcon(":/images/delete.svg"));
removeAction->setIconText("Remove");
removeAction->setToolTip("Remove this sample");
connect(removeAction, &QAction::triggered, [=]() { m_model->removeSample(item); });
return removeAction;
}
void SampleListView::showContextMenu(const QPoint& pos)
{
auto sampleAtPoint = m_model->itemForIndex(indexAt(pos));
QMenu menu;
menu.setToolTipsVisible(true);
if (sampleAtPoint != nullptr) {
menu.addAction(createEditAction(&menu, sampleAtPoint));
menu.addAction(createRemoveAction(&menu, sampleAtPoint));
menu.addSeparator();
}
menu.addAction(m_newSampleAction);
menu.addAction(m_chooseFromLibraryAction);
menu.exec(mapToGlobal(pos));
}
// ************************************************************************************************
//
// BornAgain: simulate and fit reflection and scattering
//
//! @file GUI/Views/SampleDesigner/SampleListView.h
//! @brief Defines class SampleListView
//!
//! @homepage http://www.bornagainproject.org
//! @license GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2021
//! @authors Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
// ************************************************************************************************
#ifndef BORNAGAIN_GUI_VIEWS_SAMPLEDESIGNER_SAMPLELISTVIEW_H
#define BORNAGAIN_GUI_VIEWS_SAMPLEDESIGNER_SAMPLELISTVIEW_H
#include <QListView>
class SampleModel;
class SampleListModel;
class MultiLayerItem;
//! List view to select one sample (left side of layer-oriented sample editor)
class SampleListView : public QListView {
Q_OBJECT
public:
SampleListView(QWidget* parent, SampleModel* sampleModel);
void setCurrentSample(MultiLayerItem* multiLayer);
MultiLayerItem* currentSample();
QAction* newSampleAction();
QAction* chooseFromLibraryAction();
virtual QSize sizeHint() const override;
signals:
void currentSampleChanged(MultiLayerItem* current);
private:
void createNewSample();
void createSampleFromLibrary(const QString& classname, const QString& title,
const QString& description);
QList<QAction*> getOverlayActions(const QModelIndex& index, bool asHover);
void editNameAndDescription(MultiLayerItem* item);
void onCurrentChanged(const QModelIndex& index);
QAction* createEditAction(QObject* parent, MultiLayerItem* item);
QAction* createRemoveAction(QObject* parent, MultiLayerItem* item);
void showContextMenu(const QPoint& pos);
private:
SampleListModel* m_model;
QAction* m_newSampleAction;
QAction* m_chooseFromLibraryAction;
};
#endif // BORNAGAIN_GUI_VIEWS_SAMPLEDESIGNER_SAMPLELISTVIEW_H
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment