Commit 7c639729 authored by Emmet O'Neill's avatar Emmet O'Neill Committed by Dmitry Kazakov

Animation Timeline Docker: Insert Keyframes with Timing.

Summary:
This patch adds timing functionality to the Animation Timeline Docker's "Insert N Keyframes" menu actions. Out of necessity, it also replaces the create-on-demand QInputDialog with a new TimelineInsertKeyframesDialog that was designed as a drop-in replacement that allows for getting more than a single variable of user input (i.e. frame count and timing) and which could easy be given more functionality in the future.

{F5845888}

The motivation behind this patch was to improve the "Insert N Keyframes Right/Left" action workflow by giving the animator control of frame timing. Previously, those actions worked by adding a number of immediately adjacent frames based on the user's desired number of frames to insert.

In animation, keyframes that are immediately adjacent to each other are described as being "on 1s", but other timings are also common - "on 2s", "on 3s", etc. - in which drawings are held for different amounts of time. Animations typically maintain a particular rhythm for a span of frames, but it's also very common for different parts of a single animation to switch timings.

This patch improves upon existing functionality to facilitate animating on 1s, 2s, 3s, and others. Now, a Krita user can specify a timing of 2 to quickly and easily insert keyframes "on 2s", a timing of 3 to insert "on 3s", and so on, allowing even longer timings if desired.

( Pair-programmed on a sunny Portland Saturday with @eoinoneill, of course! =] )

Test Plan:
1. Right-click a position on the animation Timeline docker.

2. Select "Keyframes > Insert N Keyframes Right" or "Keyframes > Insert N Keyframes Left".

3. A new dialog windows should pop up, asking for a number of frames and a timing.

4. Test different values as well as both the "Cancel" and "OK" buttons and check for predictable behavior in a variety of cases.

Reviewers: #krita, #krita_abyss, dkazakov, rempt, scottpetrovic, Bollebib

Subscribers: #krita_abyss, #krita, eoinoneill

Tags: #krita_abyss

Differential Revision: https://phabricator.kde.org/D12843
parent 6509644e
...@@ -20,6 +20,7 @@ set(KRITA_ANIMATIONDOCKER_SOURCES ...@@ -20,6 +20,7 @@ set(KRITA_ANIMATIONDOCKER_SOURCES
timeline_node_list_keeper.cpp timeline_node_list_keeper.cpp
timeline_color_scheme.cpp timeline_color_scheme.cpp
timeline_insert_keyframe_dialog.cpp
kis_draggable_tool_button.cpp kis_draggable_tool_button.cpp
kis_zoom_button.cpp kis_zoom_button.cpp
......
...@@ -794,9 +794,10 @@ bool TimelineFramesModel::copyFrame(const QModelIndex &dstIndex) ...@@ -794,9 +794,10 @@ bool TimelineFramesModel::copyFrame(const QModelIndex &dstIndex)
return m_d->addKeyframe(dstIndex.row(), dstIndex.column(), true); return m_d->addKeyframe(dstIndex.row(), dstIndex.column(), true);
} }
bool TimelineFramesModel::insertFrames(int dstColumn, const QList<int> &dstRows, int count) bool TimelineFramesModel::insertFrames(int dstColumn, const QList<int> &dstRows, int count, int timing)
{ {
if (dstRows.isEmpty() || count <= 0) return true; if (dstRows.isEmpty() || count <= 0) return true;
timing = qMax(timing, 1);
KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Insert frame", "Insert %1 frames", count)); KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Insert frame", "Insert %1 frames", count));
...@@ -811,10 +812,9 @@ bool TimelineFramesModel::insertFrames(int dstColumn, const QList<int> &dstRows, ...@@ -811,10 +812,9 @@ bool TimelineFramesModel::insertFrames(int dstColumn, const QList<int> &dstRows,
} }
} }
setLastVisibleFrame(columnCount() + count - 1); setLastVisibleFrame(columnCount() + (count * timing) - 1);
createOffsetFramesCommand(indexes, QPoint((count * timing), 0), false, parentCommand);
createOffsetFramesCommand(indexes, QPoint(count, 0), false, parentCommand);
Q_FOREACH (int row, dstRows) { Q_FOREACH (int row, dstRows) {
KisNodeDummy *dummy = m_d->converter->dummyFromRow(row); KisNodeDummy *dummy = m_d->converter->dummyFromRow(row);
...@@ -823,13 +823,13 @@ bool TimelineFramesModel::insertFrames(int dstColumn, const QList<int> &dstRows, ...@@ -823,13 +823,13 @@ bool TimelineFramesModel::insertFrames(int dstColumn, const QList<int> &dstRows,
KisNodeSP node = dummy->node(); KisNodeSP node = dummy->node();
if (!KisAnimationUtils::supportsContentFrames(node)) continue; if (!KisAnimationUtils::supportsContentFrames(node)) continue;
for (int column = dstColumn; column < dstColumn + count; column++) { for (int column = dstColumn; column < dstColumn + (count * timing); column += timing) {
KisAnimationUtils::createKeyframeCommand(m_d->image, node, KisKeyframeChannel::Content.id(), column, false, parentCommand); KisAnimationUtils::createKeyframeCommand(m_d->image, node, KisKeyframeChannel::Content.id(), column, false, parentCommand);
} }
} }
const int oldTime = m_d->image->animationInterface()->currentUITime(); const int oldTime = m_d->image->animationInterface()->currentUITime();
const int newTime = dstColumn > oldTime ? dstColumn : dstColumn + count - 1; const int newTime = dstColumn > oldTime ? dstColumn : dstColumn + (count * timing) - 1;
new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(), new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(),
oldTime, oldTime,
...@@ -934,7 +934,6 @@ bool TimelineFramesModel::insertHoldFrames(QModelIndexList selectedIndexes, int ...@@ -934,7 +934,6 @@ bool TimelineFramesModel::insertHoldFrames(QModelIndexList selectedIndexes, int
return true; return true;
} }
QString TimelineFramesModel::audioChannelFileName() const QString TimelineFramesModel::audioChannelFileName() const
{ {
return m_d->image ? m_d->image->animationInterface()->audioChannelFileName() : QString(); return m_d->image ? m_d->image->animationInterface()->audioChannelFileName() : QString();
......
...@@ -59,7 +59,7 @@ public: ...@@ -59,7 +59,7 @@ public:
bool createFrame(const QModelIndex &dstIndex); bool createFrame(const QModelIndex &dstIndex);
bool copyFrame(const QModelIndex &dstIndex); bool copyFrame(const QModelIndex &dstIndex);
bool insertFrames(int dstColumn, const QList<int> &dstRows, int count); bool insertFrames(int dstColumn, const QList<int> &dstRows, int count, int timing = 1);
bool insertHoldFrames(QModelIndexList selectedIndexes, int count); bool insertHoldFrames(QModelIndexList selectedIndexes, int count);
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
#include "timeline_ruler_header.h" #include "timeline_ruler_header.h"
#include "timeline_layers_header.h" #include "timeline_layers_header.h"
#include "timeline_insert_keyframe_dialog.h"
#include <cmath> #include <cmath>
#include <limits> #include <limits>
...@@ -112,10 +113,11 @@ struct TimelineFramesView::Private ...@@ -112,10 +113,11 @@ struct TimelineFramesView::Private
QAction *audioMuteAction; QAction *audioMuteAction;
KisSliderSpinBox *volumeSlider; KisSliderSpinBox *volumeSlider;
QMenu *layerEditingMenu; QMenu *layerEditingMenu;
QMenu *existingLayersMenu; QMenu *existingLayersMenu;
TimelineInsertKeyframeDialog *insertKeyframeDialog;
KisZoomButton *zoomDragButton; KisZoomButton *zoomDragButton;
bool dragInProgress; bool dragInProgress;
...@@ -192,7 +194,6 @@ TimelineFramesView::TimelineFramesView(QWidget *parent) ...@@ -192,7 +194,6 @@ TimelineFramesView::TimelineFramesView(QWidget *parent)
connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), SLOT(slotUpdateInfiniteFramesCount())); connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), SLOT(slotUpdateInfiniteFramesCount()));
connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount())); connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(slotUpdateInfiniteFramesCount()));
/********** New Layer Menu ***********************************************************/ /********** New Layer Menu ***********************************************************/
m_d->addLayersButton = new QToolButton(this); m_d->addLayersButton = new QToolButton(this);
...@@ -270,6 +271,10 @@ TimelineFramesView::TimelineFramesView(QWidget *parent) ...@@ -270,6 +271,10 @@ TimelineFramesView::TimelineFramesView(QWidget *parent)
m_d->multiframeColorSelectorAction->setDefaultWidget(m_d->multiframeColorSelector); m_d->multiframeColorSelectorAction->setDefaultWidget(m_d->multiframeColorSelector);
connect(m_d->multiframeColorSelector, &KisColorLabelSelectorWidget::currentIndexChanged, this, &TimelineFramesView::slotColorLabelChanged); connect(m_d->multiframeColorSelector, &KisColorLabelSelectorWidget::currentIndexChanged, this, &TimelineFramesView::slotColorLabelChanged);
/********** Insert Keyframes Dialog **************************************************/
m_d->insertKeyframeDialog = new TimelineInsertKeyframeDialog(this);
/********** Zoom Button **************************************************************/ /********** Zoom Button **************************************************************/
m_d->zoomDragButton = new KisZoomButton(this); m_d->zoomDragButton = new KisZoomButton(this);
...@@ -371,9 +376,6 @@ void TimelineFramesView::setActionManager( KisActionManager * actionManager) ...@@ -371,9 +376,6 @@ void TimelineFramesView::setActionManager( KisActionManager * actionManager)
action = m_d->actionMan->createAction("update_playback_range"); action = m_d->actionMan->createAction("update_playback_range");
connect(action, SIGNAL(triggered()), SLOT(slotUpdatePlackbackRange())); connect(action, SIGNAL(triggered()), SLOT(slotUpdatePlackbackRange()));
} }
} }
...@@ -563,9 +565,6 @@ void TimelineFramesView::slotAudioVolumeChanged(int value) ...@@ -563,9 +565,6 @@ void TimelineFramesView::slotAudioVolumeChanged(int value)
m_d->model->setAudioVolume(qreal(value) / 100.0); m_d->model->setAudioVolume(qreal(value) / 100.0);
} }
void TimelineFramesView::slotUpdateInfiniteFramesCount() void TimelineFramesView::slotUpdateInfiniteFramesCount()
{ {
if (horizontalScrollBar()->isSliderDown()) return; if (horizontalScrollBar()->isSliderDown()) return;
...@@ -988,7 +987,6 @@ void TimelineFramesView::createFrameEditingMenuActions(QMenu *menu, bool addFram ...@@ -988,7 +987,6 @@ void TimelineFramesView::createFrameEditingMenuActions(QMenu *menu, bool addFram
KisActionManager::safePopulateMenu(menu, "add_duplicate_frame", m_d->actionMan); KisActionManager::safePopulateMenu(menu, "add_duplicate_frame", m_d->actionMan);
menu->addSeparator(); menu->addSeparator();
} }
} }
void TimelineFramesView::mousePressEvent(QMouseEvent *event) void TimelineFramesView::mousePressEvent(QMouseEvent *event)
...@@ -1324,7 +1322,7 @@ void TimelineFramesView::calculateSelectionMetrics(int &minColumn, int &maxColum ...@@ -1324,7 +1322,7 @@ void TimelineFramesView::calculateSelectionMetrics(int &minColumn, int &maxColum
} }
} }
void TimelineFramesView::insertFramesImpl(int insertAtColumn, int count, QSet<int> rows, bool forceEntireColumn) void TimelineFramesView::insertFramesImpl(int insertAtColumn, int count, int timing, QSet<int> rows, bool forceEntireColumn)
{ {
if (forceEntireColumn) { if (forceEntireColumn) {
rows.clear(); rows.clear();
...@@ -1335,11 +1333,11 @@ void TimelineFramesView::insertFramesImpl(int insertAtColumn, int count, QSet<in ...@@ -1335,11 +1333,11 @@ void TimelineFramesView::insertFramesImpl(int insertAtColumn, int count, QSet<in
} }
if (!rows.isEmpty()) { if (!rows.isEmpty()) {
m_d->model->insertFrames(insertAtColumn, rows.toList(), count); m_d->model->insertFrames(insertAtColumn, rows.toList(), count, timing);
} }
} }
void TimelineFramesView::slotInsertKeyframesLeft(int count, bool forceEntireColumn) void TimelineFramesView::slotInsertKeyframesLeft(int count, int timing, bool forceEntireColumn)
{ {
QSet<int> rows; QSet<int> rows;
int minColumn = 0; int minColumn = 0;
...@@ -1351,10 +1349,10 @@ void TimelineFramesView::slotInsertKeyframesLeft(int count, bool forceEntireColu ...@@ -1351,10 +1349,10 @@ void TimelineFramesView::slotInsertKeyframesLeft(int count, bool forceEntireColu
count = qMax(1, maxColumn - minColumn + 1); count = qMax(1, maxColumn - minColumn + 1);
} }
insertFramesImpl(minColumn, count, rows, forceEntireColumn); insertFramesImpl(minColumn, count, timing, rows, forceEntireColumn);
} }
void TimelineFramesView::slotInsertKeyframesRight(int count, bool forceEntireColumn) void TimelineFramesView::slotInsertKeyframesRight(int count, int timing, bool forceEntireColumn)
{ {
QSet<int> rows; QSet<int> rows;
int minColumn = 0; int minColumn = 0;
...@@ -1366,78 +1364,52 @@ void TimelineFramesView::slotInsertKeyframesRight(int count, bool forceEntireCol ...@@ -1366,78 +1364,52 @@ void TimelineFramesView::slotInsertKeyframesRight(int count, bool forceEntireCol
count = qMax(1, maxColumn - minColumn + 1); count = qMax(1, maxColumn - minColumn + 1);
} }
insertFramesImpl(maxColumn + 1, count, rows, forceEntireColumn); insertFramesImpl(maxColumn + 1, count, timing, rows, forceEntireColumn);
} }
void TimelineFramesView::slotInsertColumnsLeft(int count) void TimelineFramesView::slotInsertColumnsLeft(int count, int timing)
{ {
slotInsertKeyframesLeft(count, true); slotInsertKeyframesLeft(count, timing, true);
} }
void TimelineFramesView::slotInsertColumnsRight(int count) void TimelineFramesView::slotInsertColumnsRight(int count, int timing)
{ {
slotInsertKeyframesRight(count, true); slotInsertKeyframesRight(count, timing, true);
} }
void TimelineFramesView::slotInsertKeyframesLeftCustom() void TimelineFramesView::slotInsertKeyframesLeftCustom()
{ {
bool ok = false; int count, timing;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert left"),
i18nc("@label:spinbox", "Enter number of frames"),
defaultNumberOfFramesToAdd(),
1, 10000, 1, &ok);
if (ok) { if (m_d->insertKeyframeDialog->promptUserSettings(count, timing)) {
setDefaultNumberOfFramesToAdd(count); slotInsertKeyframesLeft(count, timing, false);
slotInsertKeyframesLeft(count);
} }
} }
void TimelineFramesView::slotInsertKeyframesRightCustom() void TimelineFramesView::slotInsertKeyframesRightCustom()
{ {
bool ok = false; int count, timing;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert right"),
i18nc("@label:spinbox", "Enter number of frames"),
defaultNumberOfFramesToAdd(),
1, 10000, 1, &ok);
if (ok) { if (m_d->insertKeyframeDialog->promptUserSettings(count, timing)) {
setDefaultNumberOfFramesToAdd(count); slotInsertKeyframesRight(count, timing, false);
slotInsertKeyframesRight(count);
} }
} }
void TimelineFramesView::slotInsertColumnsLeftCustom() void TimelineFramesView::slotInsertColumnsLeftCustom()
{ {
bool ok = false; int count, timing;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert left"),
i18nc("@label:spinbox", "Enter number of columns"),
defaultNumberOfColumnsToAdd(),
1, 10000, 1, &ok);
if (ok) { if (m_d->insertKeyframeDialog->promptUserSettings(count, timing)) {
setDefaultNumberOfColumnsToAdd(count); slotInsertColumnsLeft(count, timing);
slotInsertColumnsLeft(count);
} }
} }
void TimelineFramesView::slotInsertColumnsRightCustom() void TimelineFramesView::slotInsertColumnsRightCustom()
{ {
bool ok = false; int count, timing;
const int count = QInputDialog::getInt(this,
i18nc("@title:window", "Insert right"),
i18nc("@label:spinbox", "Enter number of columns"),
defaultNumberOfColumnsToAdd(),
1, 10000, 1, &ok);
if (ok) { if (m_d->insertKeyframeDialog->promptUserSettings(count, timing)) {
setDefaultNumberOfColumnsToAdd(count); slotInsertColumnsRight(count, timing);
slotInsertColumnsRight(count);
} }
} }
......
...@@ -27,7 +27,6 @@ ...@@ -27,7 +27,6 @@
class KisAction; class KisAction;
class TimelineWidget; class TimelineWidget;
class KRITAANIMATIONDOCKER_EXPORT TimelineFramesView : public QTableView class KRITAANIMATIONDOCKER_EXPORT TimelineFramesView : public QTableView
{ {
Q_OBJECT Q_OBJECT
...@@ -55,20 +54,19 @@ private Q_SLOTS: ...@@ -55,20 +54,19 @@ private Q_SLOTS:
void slotSetEndTimeToCurrentPosition(); void slotSetEndTimeToCurrentPosition();
void slotUpdatePlackbackRange(); void slotUpdatePlackbackRange();
// Layer
void slotAddNewLayer(); void slotAddNewLayer();
void slotAddExistingLayer(QAction *action); void slotAddExistingLayer(QAction *action);
void slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
void slotRemoveLayer(); void slotRemoveLayer();
void slotLayerContextMenuRequested(const QPoint &globalPos); void slotLayerContextMenuRequested(const QPoint &globalPos);
// New, Copy, Insert and Remove Frames
void slotNewFrame(); void slotNewFrame();
void slotCopyFrame(); void slotCopyFrame();
void slotInsertKeyframesLeft(int count = 1, int timing = 1, bool forceEntireColumn = false);
void slotInsertKeyframesLeft(int count = -1, bool forceEntireColumn = false); void slotInsertKeyframesRight(int count = 1, int timing = 1, bool forceEntireColumn = false);
void slotInsertKeyframesRight(int count = -1, bool forceEntireColumn = false);
void slotInsertKeyframesLeftCustom(); void slotInsertKeyframesLeftCustom();
void slotInsertKeyframesRightCustom(); void slotInsertKeyframesRightCustom();
...@@ -76,10 +74,10 @@ private Q_SLOTS: ...@@ -76,10 +74,10 @@ private Q_SLOTS:
void slotRemoveFrame(bool forceEntireColumn = false, bool needsOffset = false); void slotRemoveFrame(bool forceEntireColumn = false, bool needsOffset = false);
void slotRemoveFramesAndShift(bool forceEntireColumn = false); void slotRemoveFramesAndShift(bool forceEntireColumn = false);
void slotInsertColumnsLeft(int count = -1); void slotInsertColumnsLeft(int count = 1, int timing = 1);
void slotInsertColumnsLeftCustom(); void slotInsertColumnsLeftCustom();
void slotInsertColumnsRight(int count = -1); void slotInsertColumnsRight(int count = 1, int timing = 1);
void slotInsertColumnsRightCustom(); void slotInsertColumnsRightCustom();
void slotRemoveColumns(); void slotRemoveColumns();
...@@ -100,7 +98,7 @@ private Q_SLOTS: ...@@ -100,7 +98,7 @@ private Q_SLOTS:
void slotMirrorFrames(bool forceEntireColumn = false); void slotMirrorFrames(bool forceEntireColumn = false);
void slotMirrorColumns(); void slotMirrorColumns();
// Copy-paste
void slotCopyFrames(bool forceEntireColumn = false); void slotCopyFrames(bool forceEntireColumn = false);
void slotCutFrames(bool forceEntireColumn = false); void slotCutFrames(bool forceEntireColumn = false);
void slotPasteFrames(bool forceEntireColumn = false); void slotPasteFrames(bool forceEntireColumn = false);
...@@ -121,7 +119,7 @@ private Q_SLOTS: ...@@ -121,7 +119,7 @@ private Q_SLOTS:
void slotColorLabelChanged(int); void slotColorLabelChanged(int);
void slotEnsureRowVisible(int row); void slotEnsureRowVisible(int row);
// Audio
void slotSelectAudioChannelFile(); void slotSelectAudioChannelFile();
void slotAudioChannelMute(bool value); void slotAudioChannelMute(bool value);
void slotAudioChannelRemove(); void slotAudioChannelRemove();
...@@ -132,7 +130,7 @@ private: ...@@ -132,7 +130,7 @@ private:
void setFramesPerSecond(int fps); void setFramesPerSecond(int fps);
void calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet<int> &rows) const; void calculateSelectionMetrics(int &minColumn, int &maxColumn, QSet<int> &rows) const;
void insertFramesImpl(int insertAtColumn, int count, QSet<int> rows, bool forceEntireColumn); void insertFramesImpl(int insertAtColumn, int count, int timing, QSet<int> rows, bool forceEntireColumn);
void createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions); void createFrameEditingMenuActions(QMenu *menu, bool addFrameCreationActions);
......
#include "timeline_insert_keyframe_dialog.h"
#include <QLabel>
#include <QSpinBox>
#include <QDialogButtonBox>
#include <QVBoxLayout>
#include <QFormLayout>
#include <klocalizedstring.h>
TimelineInsertKeyframeDialog::TimelineInsertKeyframeDialog(QWidget *parent) :
QDialog(parent)
{
setWindowTitle(i18nc("@title:window","Insert Keyframes"));
setModal(true);
setLayout(new QVBoxLayout(this));
frameCountSpinbox.setMinimum(1);
frameCountSpinbox.setValue(1);
frameTimingSpinbox.setMinimum(1);
frameTimingSpinbox.setValue(1);
QWidget *formsWidget = new QWidget();
QFormLayout *formLayout = new QFormLayout();
formsWidget->setLayout( formLayout );
layout()->addWidget(formsWidget);
QDialogButtonBox *buttonbox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
layout()->addWidget(buttonbox);
formLayout->addRow(QString(i18nc("@label:spinbox", "Number of frames:")), &frameCountSpinbox);
formLayout->addRow(QString(i18nc("@label:spinbox", "Frame timing:")), &frameTimingSpinbox);
connect(buttonbox, SIGNAL(accepted()), this, SLOT(accept()));
connect(buttonbox, SIGNAL(rejected()), this, SLOT(reject()));
}
bool TimelineInsertKeyframeDialog::promptUserSettings(int &out_count, int &out_timing){
if(exec() == QDialog::Accepted){
out_count = frameCountSpinbox.value();
out_timing = frameTimingSpinbox.value();
return true;
}
return false;
}
#ifndef __TIMELINE_INSERT_KEYFRAME_DIALOG_H
#define __TIMELINE_INSERT_KEYFRAME_DIALOG_H
#include "kritaanimationdocker_export.h"
#include <QDialog>
#include <QSpinBox>
class KRITAANIMATIONDOCKER_EXPORT TimelineInsertKeyframeDialog : QDialog {
Q_OBJECT
private:
QSpinBox frameCountSpinbox;
QSpinBox frameTimingSpinbox;
public:
TimelineInsertKeyframeDialog(QWidget *parent = 0);
bool promptUserSettings(int &count, int &timing);
};
#endif // __TIMELINE_INSERT_KEYFRAME_DIALOG_H
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