Reimplement effect presets

Fixes #5
parent 68574e56
......@@ -123,4 +123,26 @@ bool AssetKeyframeCommand::mergeWith(const QUndoCommand *other)
return true;
}
AssetUpdateCommand::AssetUpdateCommand(std::shared_ptr<AssetParameterModel> model, QVector<QPair<QString, QVariant> > parameters, QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_value(parameters)
{
const QString id = model->getAssetId();
if (EffectsRepository::get()->exists(id)) {
setText(i18n("Update %1", EffectsRepository::get()->getName(id)));
} else if (TransitionsRepository::get()->exists(id)) {
setText(i18n("Update %1", TransitionsRepository::get()->getName(id)));
}
m_oldValue = m_model->getAllParameters();
}
void AssetUpdateCommand::undo()
{
m_model->setParameters(m_oldValue);
}
// virtual
void AssetUpdateCommand::redo()
{
m_model->setParameters(m_value);
}
......@@ -65,5 +65,17 @@ private:
QTime m_stamp;
};
class AssetUpdateCommand : public QUndoCommand
{
public:
AssetUpdateCommand(std::shared_ptr<AssetParameterModel> model, QVector<QPair<QString, QVariant> > parameters, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
std::shared_ptr<AssetParameterModel> m_model;
QVector<QPair<QString, QVariant> > m_value;
QVector<QPair<QString, QVariant> > m_oldValue;
bool m_updateView;
};
#endif
......@@ -28,6 +28,8 @@
#include <QDebug>
#include <QLocale>
#include <QString>
#include <QJsonObject>
#include <QJsonArray>
AssetParameterModel::AssetParameterModel(Mlt::Properties *asset, const QDomElement &assetXml, const QString &assetId, ObjectId ownerId, QObject *parent)
: QAbstractListModel(parent)
......@@ -510,16 +512,215 @@ QVector<QPair<QString, QVariant>> AssetParameterModel::getAllParameters() const
return res;
}
QJsonDocument AssetParameterModel::toJson() const
{
QJsonArray list;
QLocale locale;
for (const auto &fixed : m_fixedParams) {
QJsonObject currentParam;
QModelIndex ix = index(m_rows.indexOf(fixed.first), 0);
currentParam.insert(QLatin1String("name"), QJsonValue(fixed.first));
currentParam.insert(QLatin1String("value"), fixed.second.toString());
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
double factor = data(ix, AssetParameterModel::FactorRole).toDouble();
if (factor > 0) {
min /= factor;
max /= factor;
}
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
list.push_back(currentParam);
}
for (const auto &param : m_params) {
QJsonObject currentParam;
QModelIndex ix = index(m_rows.indexOf(param.first), 0);
currentParam.insert(QLatin1String("name"), QJsonValue(param.first));
currentParam.insert(QLatin1String("value"), QJsonValue(param.second.value.toString()));
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
double factor = data(ix, AssetParameterModel::FactorRole).toDouble();
if (factor > 0) {
min /= factor;
max /= factor;
}
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
list.push_back(currentParam);
}
return QJsonDocument(list);
}
void AssetParameterModel::deletePreset(const QString &presetFile, const QString &presetName)
{
QJsonObject object;
QJsonArray array;
QFile loadFile(presetFile);
if (loadFile.exists()) {
if (loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isArray()) {
qDebug()<<" * * ** JSON IS AN ARRAY, DELETING: "<<presetName;
array = loadDoc.array();
QList <int> toDelete;
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().keys().contains(presetName)) {
toDelete << i;
}
}
for (int i : toDelete) {
array.removeAt(i);
}
} else if (loadDoc.isObject()) {
QJsonObject obj = loadDoc.object();
qDebug()<<" * * ** JSON IS AN OBJECT, DELETING: "<<presetName;
if (obj.keys().contains(presetName)) {
obj.remove(presetName);
} else {
qDebug()<<" * * ** JSON DOES NOT CONTAIN: "<<obj.keys();
}
array.append(obj);
}
loadFile.close();
} else if (!loadFile.open(QIODevice::ReadWrite)) {
//TODO: error message
}
}
if (!loadFile.open(QIODevice::WriteOnly)) {
//TODO: error message
}
loadFile.write(QJsonDocument(array).toJson());
}
void AssetParameterModel::savePreset(const QString &presetFile, const QString &presetName)
{
QJsonObject object;
QJsonArray array;
QJsonDocument doc = toJson();
QFile loadFile(presetFile);
if (loadFile.exists()) {
if (loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isArray()) {
array = loadDoc.array();
QList <int> toDelete;
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().keys().contains(presetName)) {
toDelete << i;
}
}
for (int i : toDelete) {
array.removeAt(i);
}
} else if (loadDoc.isObject()) {
QJsonObject obj = loadDoc.object();
if (obj.keys().contains(presetName)) {
obj.remove(presetName);
}
array.append(obj);
}
loadFile.close();
} else if (!loadFile.open(QIODevice::ReadWrite)) {
//TODO: error message
}
}
if (!loadFile.open(QIODevice::WriteOnly)) {
//TODO: error message
}
object[presetName] = doc.array();
array.append(object);
loadFile.write(QJsonDocument(array).toJson());
}
const QStringList AssetParameterModel::getPresetList(const QString &presetFile) const
{
QFile loadFile(presetFile);
if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isObject()) {
qDebug()<<"// PRESET LIST IS AN OBJECT!!!";
return loadDoc.object().keys();
} else if (loadDoc.isArray()) {
qDebug()<<"// PRESET LIST IS AN ARRAY!!!";
QStringList result;
QJsonArray array = loadDoc.array();
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject()) {
result << val.toObject().keys();
}
}
return result;
}
}
return QStringList();
}
const QVector<QPair<QString, QVariant>> AssetParameterModel::loadPreset(const QString &presetFile, const QString &presetName)
{
QFile loadFile(presetFile);
QVector<QPair<QString, QVariant>> params;
if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isObject() && loadDoc.object().contains(presetName)) {
qDebug()<<"..........\n..........\nLOADING OBJECT JSON";
QJsonValue val = loadDoc.object().value(presetName);
if (val.isObject()) {
QVariantMap map = val.toObject().toVariantMap();
QMap<QString, QVariant>::const_iterator i = map.constBegin();
while (i != map.constEnd()) {
params.append({i.key(), i.value()});
++i;
}
}
} else if (loadDoc.isArray()) {
QJsonArray array = loadDoc.array();
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().contains(presetName)) {
QJsonValue preset = val.toObject().value(presetName);
if (preset.isArray()) {
QJsonArray paramArray = preset.toArray();
for (int j = 0; j < paramArray.size(); j++) {
QJsonValue v1 = paramArray.at(j);
if (v1.isObject()) {
QJsonObject ob = v1.toObject();
params.append({ob.value("name").toString(), ob.value("value").toVariant()});
}
}
}
qDebug()<<"// LOADED PRESET: "<<presetName<<"\n"<<params;
break;
}
}
}
}
return params;
}
void AssetParameterModel::setParameters(const QVector<QPair<QString, QVariant>> &params)
{
QLocale locale;
for (const auto &param : params) {
if (param.second.type() == QVariant::Double) {
setParameter(param.first, locale.toString(param.second.toDouble()));
setParameter(param.first, locale.toString(param.second.toDouble()), false);
} else {
setParameter(param.first, param.second.toString());
setParameter(param.first, param.second.toString(), false);
}
}
emit modelChanged();
emit dataChanged(index(0, 0), index(m_rows.count() - 1, 0), {});
}
ObjectId AssetParameterModel::getOwnerId() const
......
......@@ -27,6 +27,7 @@
#include <QAbstractListModel>
#include <QDomElement>
#include <QUndoCommand>
#include <QJsonDocument>
#include <unordered_map>
#include <memory>
......@@ -127,6 +128,12 @@ public:
/* @brief Return all the parameters as pairs (parameter name, parameter value) */
QVector<QPair<QString, QVariant>> getAllParameters() const;
/* @brief Returns a json definition of the effect with all param values */
QJsonDocument toJson() const;
void savePreset(const QString &presetFile, const QString &presetName);
void deletePreset(const QString &presetFile, const QString &presetName);
const QStringList getPresetList(const QString &presetFile) const;
const QVector<QPair<QString, QVariant>> loadPreset(const QString &presetFile, const QString &presetName);
/* @brief Sets the value of a list of parameters
@param params contains the pairs (parameter name, parameter value)
......
......@@ -31,6 +31,11 @@
#include <QFontDatabase>
#include <QLabel>
#include <QVBoxLayout>
#include <QInputDialog>
#include <QDir>
#include <QStandardPaths>
#include <QActionGroup>
#include <QMenu>
#include <utility>
AssetParameterView::AssetParameterView(QWidget *parent)
......@@ -41,6 +46,9 @@ AssetParameterView::AssetParameterView(QWidget *parent)
m_lay->setContentsMargins(0, 0, 0, 2);
m_lay->setSpacing(0);
setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
// Presets Combo
m_presetMenu = new QMenu(this);
m_presetMenu->setToolTip(i18n("Presets"));
}
void AssetParameterView::setModel(const std::shared_ptr<AssetParameterModel> &model, QSize frameSize, bool addSpacer)
......@@ -49,6 +57,30 @@ void AssetParameterView::setModel(const std::shared_ptr<AssetParameterModel> &mo
QMutexLocker lock(&m_lock);
m_model = model;
const QString paramTag = model->getAssetId();
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(paramTag));
connect(this, &AssetParameterView::updatePresets, [this, presetFile] (const QString &presetName) {
m_presetMenu->clear();
m_presetGroup.reset(new QActionGroup(this));
m_presetGroup->setExclusive(true);
m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reset Effect"), this, SLOT(resetValues()));
// Save preset
m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as-template")), i18n("Save preset"), this, SLOT(slotSavePreset()));
m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as-template")), i18n("Update current preset"), this, SLOT(slotUpdatePreset()));
m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete preset"), this, SLOT(slotDeletePreset()));
m_presetMenu->addSeparator();
QStringList presets = m_model->getPresetList(presetFile);
for (const QString &pName : presets) {
QAction *ac = m_presetMenu->addAction(pName, this, SLOT(slotLoadPreset()));
m_presetGroup->addAction(ac);
ac->setData(pName);
ac->setCheckable(true);
if (pName == presetName) {
ac->setChecked(true);
}
}
});
emit updatePresets();
connect(m_model.get(), &AssetParameterModel::dataChanged, this, &AssetParameterView::refresh);
if (paramTag.endsWith(QStringLiteral("lift_gamma_gain"))) {
// Special case, the colorwheel widget manages several parameters
......@@ -86,17 +118,21 @@ void AssetParameterView::setModel(const std::shared_ptr<AssetParameterModel> &mo
void AssetParameterView::resetValues()
{
QVector<QPair<QString, QVariant> > values;
for (int i = 0; i < m_model->rowCount(); ++i) {
QModelIndex index = m_model->index(i, 0);
QString name = m_model->data(index, AssetParameterModel::NameRole).toString();
ParamType type = m_model->data(index, AssetParameterModel::TypeRole).value<ParamType>();
QString defaultValue = m_model->data(index, AssetParameterModel::DefaultRole).toString();
QVariant defaultValue = m_model->data(index, AssetParameterModel::DefaultRole);
if (type == ParamType::KeyframeParam || type == ParamType::AnimatedRect) {
if (!defaultValue.contains(QLatin1Char('='))) {
defaultValue.prepend(QStringLiteral("%1=").arg(m_model->data(index, AssetParameterModel::ParentInRole).toInt()));
QString val = defaultValue.toString();
if (!val.contains(QLatin1Char('='))) {
val.prepend(QStringLiteral("%1=").arg(m_model->data(index, AssetParameterModel::ParentInRole).toInt()));
defaultValue = QVariant(val);
}
}
m_model->setParameter(name, defaultValue);
values.append({name, defaultValue});
/*m_model->setParameter(name, defaultValue);
if (m_mainKeyframeWidget) {
// Handles additional params like rotation so only refresh initial param at the end
} else if (type == ParamType::ColorWheel) {
......@@ -107,8 +143,10 @@ void AssetParameterView::resetValues()
}
} else {
refresh(index, index, QVector<int>());
}*/
}
}
AssetUpdateCommand *command = new AssetUpdateCommand(m_model, values);
pCore->pushUndo(command);
if (m_mainKeyframeWidget) {
m_mainKeyframeWidget->resetKeyframes();
}
......@@ -239,3 +277,66 @@ void AssetParameterView::toggleKeyframes(bool enable)
m_mainKeyframeWidget->showKeyframes(enable);
}
}
void AssetParameterView::slotDeletePreset()
{
QAction *ac = m_presetGroup->checkedAction();
if (!ac) {
return;
}
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
if (!dir.exists()) {
dir.mkpath(QStringLiteral("."));
}
const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId()));
m_model->deletePreset(presetFile, ac->data().toString());
emit updatePresets();
}
void AssetParameterView::slotUpdatePreset()
{
QAction *ac = m_presetGroup->checkedAction();
if (!ac) {
return;
}
slotSavePreset(ac->data().toString());
}
void AssetParameterView::slotSavePreset(QString presetName)
{
if (presetName.isEmpty()) {
bool ok;
presetName = QInputDialog::getText(this, i18n("Enter preset name"),
i18n("Enter the name of this preset"),
QLineEdit::Normal, QString(), &ok);
if (!ok)
return;
}
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
if (!dir.exists()) {
dir.mkpath(QStringLiteral("."));
}
const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId()));
m_model->savePreset(presetFile, presetName);
emit updatePresets(presetName);
}
void AssetParameterView::slotLoadPreset()
{
QAction *action = qobject_cast<QAction *>(sender());
if (!action) {
return;
}
const QString presetName = action->data().toString();
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId()));
const QVector<QPair<QString, QVariant>> params = m_model->loadPreset(presetFile, presetName);
AssetUpdateCommand *command = new AssetUpdateCommand(m_model, params);
pCore->pushUndo(command);
}
QMenu *AssetParameterView::presetMenu()
{
return m_presetMenu;
}
......@@ -34,6 +34,8 @@
*/
class QVBoxLayout;
class QMenu;
class QActionGroup;
class AbstractParamWidget;
class AssetParameterModel;
class KeyframeWidget;
......@@ -54,9 +56,6 @@ public:
/** Returns the preferred widget height */
int contentHeight() const;
/** Reset all parameter values to default */
void resetValues();
/** Returns the type of monitor overlay required by this effect */
MonitorSceneType needsMonitorEffectScene() const;
......@@ -64,10 +63,20 @@ public:
bool keyframesAllowed() const;
/** Returns true is the keyframes should be hidden on first opening*/
bool modelHideKeyframes() const;
/** Returns the preset menu to be embedded in toolbars */
QMenu *presetMenu();
public slots:
void slotRefresh();
void toggleKeyframes(bool enable);
/** Reset all parameter values to default */
void resetValues();
/** Save all parameters to a preset */
void slotSavePreset(QString presetName = QString());
/** Save all parameters to a preset */
void slotLoadPreset();
void slotUpdatePreset();
void slotDeletePreset();
protected:
/** @brief This is a handler for the dataChanged slot of the model.
......@@ -81,6 +90,8 @@ protected:
std::shared_ptr<AssetParameterModel> m_model;
std::vector<AbstractParamWidget *> m_widgets;
KeyframeWidget *m_mainKeyframeWidget;
QMenu *m_presetMenu;
std::shared_ptr<QActionGroup> m_presetGroup;
private slots:
/** @brief Apply a change of parameter sent by the view
......@@ -93,6 +104,8 @@ private slots:
signals:
void seekToPos(int);
void initKeyframeView(bool active);
/** @brief clear and refill the effect presets */
void updatePresets(const QString &presetName = QString());
};
#endif
......@@ -78,7 +78,6 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
m_buttonNext->setAutoRaise(true);
m_buttonNext->setIcon(QIcon::fromTheme(QStringLiteral("media-skip-forward")));
m_buttonNext->setToolTip(i18n("Go to next keyframe"));
// Keyframe type widget
m_selectType = new KSelectAction(QIcon::fromTheme(QStringLiteral("keyframes")), i18n("Keyframe interpolation"), this);
QAction *linear = new QAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Linear"), this);
......@@ -112,8 +111,11 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
connect(copy, &QAction::triggered, this, &KeyframeWidget::slotCopyKeyframes);
QAction *paste = new QAction(i18n("Import keyframes from clipboard"), this);
connect(paste, &QAction::triggered, this, &KeyframeWidget::slotImportKeyframes);
// Remove keyframes
QAction *removeNext = new QAction(i18n("Remove all keyframes after cursor"), this);
connect(removeNext, &QAction::triggered, this, &KeyframeWidget::slotRemoveNextKeyframes);
// Default kf interpolation
KSelectAction *kfType = new KSelectAction(i18n("Default keyframe type"), this);
QAction *discrete2 = new QAction(QIcon::fromTheme(QStringLiteral("discrete")), i18n("Discrete"), this);
discrete2->setData((int)mlt_keyframe_discrete);
......@@ -144,6 +146,7 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
auto *container = new QMenu(this);
container->addAction(copy);
container->addAction(paste);
container->addSeparator();
container->addAction(kfType);
container->addAction(removeNext);
......@@ -451,32 +454,12 @@ void KeyframeWidget::showKeyframes(bool enable)
void KeyframeWidget::slotCopyKeyframes()
{
QJsonArray list;
for (const auto &w : m_parameters) {
int type = m_model->data(w.first, AssetParameterModel::TypeRole).toInt();
QString name = m_model->data(w.first, Qt::DisplayRole).toString();
QString value = m_model->data(w.first, AssetParameterModel::ValueRole).toString();
double min = m_model->data(w.first, AssetParameterModel::MinRole).toDouble();
double max = m_model->data(w.first, AssetParameterModel::MaxRole).toDouble();
double factor = m_model->data(w.first, AssetParameterModel::FactorRole).toDouble();
if (factor > 0) {
min /= factor;
max /= factor;
}
QJsonObject currentParam;
currentParam.insert(QLatin1String("name"), QJsonValue(name));
currentParam.insert(QLatin1String("value"), QJsonValue(value));
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
list.push_back(currentParam);
}
if (list.isEmpty()) {
QJsonDocument effectDoc = m_model->toJson();
if (effectDoc.isEmpty()) {
return;
}
QClipboard *clipboard = QApplication::clipboard();
QJsonDocument json(list);
clipboard->setText(QString(json.toJson()));
clipboard->setText(QString(effectDoc.toJson()));
}
void KeyframeWidget::slotImportKeyframes()
......@@ -524,3 +507,6 @@ void KeyframeWidget::slotRemoveNextKeyframes()
{
m_keyframes->removeNextKeyframes(GenTime(m_time->getValue(), pCore->getCurrentFps()));
}
......@@ -110,6 +110,8 @@ CollapsibleEffectView::CollapsibleEffectView(std::shared_ptr<EffectItemModel> ef
m_keyframesButton->setCheckable(true);
m_keyframesButton->setToolTip(i18n("Enable Keyframes"));
l->insertWidget(3, m_keyframesButton);
// Enable button
m_enabledButton = new KDualAction(i18n("Disable Effect"), i18n("Enable Effect"), this);
m_enabledButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("hint")));
m_enabledButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("visibility")));
......@@ -154,11 +156,13 @@ CollapsibleEffectView::CollapsibleEffectView(std::shared_ptr<EffectItemModel> ef
} else {
m_keyframesButton->setChecked(true);
}
// Presets
presetButton->setIcon(QIcon::fromTheme(QStringLiteral("document-new-from-template")));
presetButton->setMenu(m_view->presetMenu());
// Main menu
m_menu = new QMenu(this);
if (effectModel->rowCount() > 0) {
m_menu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reset Effect"), this, SLOT(slotResetEffect()));
} else {
if (effectModel->rowCount() == 0) {
collapseButton->setEnabled(false);
m_view->setVisible(false);
}
......
......@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>271</width>
<height>48</height>
<width>344</width>
<height>67</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
......@@ -122,6 +122,19 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="presetButton">
<property name="text">
<string>...</string>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="menuButton">
<property name="text">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment