use speed value instead of media out value
This functions more or less identically to using a media out value, but the desired speed is preserved through block length changes, even if the block's length is reduced to zero (i.e. no rounding errors).
This commit is contained in:
@@ -41,7 +41,7 @@ SpeedDurationDialog::SpeedDurationDialog(const rational& timebase, const QList<C
|
||||
|
||||
// Check if the speeds are different
|
||||
if (same_speed
|
||||
&& !qFuzzyCompare(prev_clip->speed(), this_clip->speed())) {
|
||||
&& prev_clip->speed() == this_clip->speed()) {
|
||||
same_speed = false;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ SpeedDurationDialog::SpeedDurationDialog(const rational& timebase, const QList<C
|
||||
|
||||
if (same_speed) {
|
||||
// All clips share the same speed so we can show the value
|
||||
speed_slider_->SetValue(clips_.first()->speed());
|
||||
speed_slider_->SetValue(clips_.first()->speed().toDouble());
|
||||
} else {
|
||||
// Else, we show an invalid initial state
|
||||
speed_slider_->SetTristate();
|
||||
@@ -208,7 +208,7 @@ void SpeedDurationDialog::accept()
|
||||
} else {
|
||||
// Otherwise we have to create a new gap
|
||||
GapBlock* gap = new GapBlock();
|
||||
gap->set_length(gap_length);
|
||||
gap->set_length_and_media_out(gap_length);
|
||||
new NodeAddCommand(static_cast<NodeGraph*>(clip->parent()), gap, command);
|
||||
new TrackInsertBlockBetweenBlocksCommand(TrackOutput::TrackFromBlock(clip), gap, clip, next_block, command);
|
||||
}
|
||||
@@ -222,18 +222,15 @@ void SpeedDurationDialog::accept()
|
||||
}
|
||||
|
||||
if (change_speed) {
|
||||
int64_t new_clip_duration = Timecode::time_to_timestamp(new_clip_length, timebase_);
|
||||
int64_t new_media_duration = qRound(static_cast<double>(new_clip_duration) * new_speed);
|
||||
rational new_media_length = Timecode::timestamp_to_time(new_media_duration, timebase_);
|
||||
rational new_block_speed = rational::fromDouble(new_speed);
|
||||
|
||||
if (clip->is_reversed()) {
|
||||
new_media_length = -new_media_length;
|
||||
new_block_speed = -new_block_speed;
|
||||
}
|
||||
|
||||
rational new_media_out = clip->media_in() + new_media_length;
|
||||
|
||||
// Change the speed by calculating the appropriate media out point for this clip
|
||||
new BlockSetMediaOutCommand(clip, new_media_out, command);
|
||||
|
||||
new BlockSetSpeedCommand(clip, new_block_speed, command);
|
||||
}
|
||||
|
||||
if (!reverse_speed_checkbox_->isTristate() && clip->is_reversed() != reverse_speed_checkbox_->isChecked()) {
|
||||
@@ -251,7 +248,7 @@ double SpeedDurationDialog::GetUnadjustedLengthTimestamp(ClipBlock *clip) const
|
||||
double duration = static_cast<double>(Timecode::time_to_timestamp(clip->length(), timebase_));
|
||||
|
||||
// Convert duration to non-speed adjusted duration
|
||||
duration *= clip->speed();
|
||||
duration *= clip->speed().toDouble();
|
||||
|
||||
return duration;
|
||||
}
|
||||
@@ -343,13 +340,11 @@ BlockReverseCommand::BlockReverseCommand(Block *block, QUndoCommand *parent) :
|
||||
|
||||
void BlockReverseCommand::redo()
|
||||
{
|
||||
rational temp = block_->media_in();
|
||||
block_->set_media_in(block_->media_out());
|
||||
block_->set_media_out(temp);
|
||||
block_->set_speed(-block_->speed());
|
||||
}
|
||||
|
||||
void BlockReverseCommand::undo()
|
||||
{
|
||||
// Since it's a simple swap, we can just run redo() again
|
||||
redo();
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
|
||||
private:
|
||||
Block* block_;
|
||||
|
||||
};
|
||||
|
||||
#endif // SPEEDDURATIONDIALOG_H
|
||||
|
||||
+19
-59
@@ -39,10 +39,11 @@ Block::Block() :
|
||||
media_in_input_->set_is_keyframable(false);
|
||||
AddInput(media_in_input_);
|
||||
|
||||
media_out_input_ = new NodeInput("media_out_in", NodeParam::kRational);
|
||||
media_out_input_->SetConnectable(false);
|
||||
media_out_input_->set_is_keyframable(false);
|
||||
AddInput(media_out_input_);
|
||||
speed_input_ = new NodeInput("speed_in", NodeParam::kRational);
|
||||
speed_input_->set_standard_value(QVariant::fromValue(rational(1)));
|
||||
speed_input_->SetConnectable(false);
|
||||
speed_input_->set_is_keyframable(false);
|
||||
AddInput(speed_input_);
|
||||
|
||||
// A block's length must be greater than 0
|
||||
set_length_and_media_out(1);
|
||||
@@ -78,7 +79,7 @@ rational Block::length() const
|
||||
return length_input_->get_standard_value().value<rational>();
|
||||
}
|
||||
|
||||
void Block::set_length(const rational &length)
|
||||
void Block::set_length_and_media_out(const rational &length)
|
||||
{
|
||||
Q_ASSERT(length > 0);
|
||||
|
||||
@@ -89,26 +90,6 @@ void Block::set_length(const rational &length)
|
||||
length_input_->set_standard_value(QVariant::fromValue(length));
|
||||
}
|
||||
|
||||
void Block::set_length_and_media_out(const rational &length)
|
||||
{
|
||||
Q_ASSERT(length > 0);
|
||||
|
||||
if (length == this->length()) {
|
||||
return;
|
||||
}
|
||||
|
||||
rational media_out_diff = length - this->length();
|
||||
|
||||
// Try to maintain the same speed (which is determined by the media in to out points)
|
||||
if (media_length() != this->length()) {
|
||||
media_out_diff = media_out_diff / this->length() * media_length();
|
||||
}
|
||||
|
||||
set_media_out(media_out() + media_out_diff);
|
||||
|
||||
set_length(length);
|
||||
}
|
||||
|
||||
void Block::set_length_and_media_in(const rational &length)
|
||||
{
|
||||
Q_ASSERT(length > 0);
|
||||
@@ -117,18 +98,11 @@ void Block::set_length_and_media_in(const rational &length)
|
||||
return;
|
||||
}
|
||||
|
||||
rational media_in_diff = this->length() - length;
|
||||
|
||||
// Try to maintain the same speed (which is determined by the media in to out points)
|
||||
if (media_length() != this->length()) {
|
||||
media_in_diff = media_in_diff / this->length() * media_length();
|
||||
}
|
||||
|
||||
// Calculate media_in adjustment
|
||||
set_media_in(media_in() + media_in_diff);
|
||||
set_media_in(media_in() + (this->length() - length) * speed());
|
||||
|
||||
// Set the length without setting media out
|
||||
set_length(length);
|
||||
set_length_and_media_out(length);
|
||||
}
|
||||
|
||||
Block *Block::previous()
|
||||
@@ -163,32 +137,27 @@ void Block::set_media_in(const rational &media_in)
|
||||
|
||||
rational Block::media_out() const
|
||||
{
|
||||
return media_out_input_->get_standard_value().value<rational>();
|
||||
return media_in() + length() * speed();
|
||||
}
|
||||
|
||||
void Block::set_media_out(const rational &media_out)
|
||||
rational Block::speed() const
|
||||
{
|
||||
media_out_input_->set_standard_value(QVariant::fromValue(media_out));
|
||||
return speed_input_->get_standard_value().value<rational>();
|
||||
}
|
||||
|
||||
rational Block::media_length() const
|
||||
void Block::set_speed(const rational &speed)
|
||||
{
|
||||
return media_out() - media_in();
|
||||
}
|
||||
|
||||
double Block::speed() const
|
||||
{
|
||||
return qAbs(media_length().toDouble() / length().toDouble());
|
||||
speed_input_->set_standard_value(QVariant::fromValue(speed));
|
||||
}
|
||||
|
||||
bool Block::is_still() const
|
||||
{
|
||||
return (media_in() == media_out());
|
||||
return speed() == 0;
|
||||
}
|
||||
|
||||
bool Block::is_reversed() const
|
||||
{
|
||||
return (media_out() < media_in());
|
||||
return speed() < 0;
|
||||
}
|
||||
|
||||
const QString &Block::block_name() const
|
||||
@@ -208,7 +177,7 @@ rational Block::SequenceToMediaTime(const rational &sequence_time) const
|
||||
return sequence_time;
|
||||
}
|
||||
|
||||
return ((sequence_time - in()) * media_length() / length()) + media_in();
|
||||
return (sequence_time - in()) * speed() + media_in();
|
||||
}
|
||||
|
||||
rational Block::MediaToSequenceTime(const rational &media_time) const
|
||||
@@ -218,21 +187,12 @@ rational Block::MediaToSequenceTime(const rational &media_time) const
|
||||
return media_time;
|
||||
}
|
||||
|
||||
return (media_time - media_in()) * length() / media_length() + in();
|
||||
return (media_time - media_in()) / speed() + in();
|
||||
}
|
||||
|
||||
void Block::CopyParameters(const Block *source, Block *dest)
|
||||
{
|
||||
dest->set_block_name(source->block_name());
|
||||
|
||||
if (source->type() == kTransition && dest->type() == kTransition) {
|
||||
const TransitionBlock* src_t = static_cast<const TransitionBlock*>(source);
|
||||
TransitionBlock* dst_t = static_cast<TransitionBlock*>(dest);
|
||||
|
||||
dst_t->set_in_and_out_offset(src_t->in_offset(), src_t->out_offset());
|
||||
} else {
|
||||
dest->set_length_and_media_out(source->length());
|
||||
}
|
||||
}
|
||||
|
||||
void Block::LengthInputChanged()
|
||||
@@ -308,8 +268,8 @@ NodeInput *Block::media_in_input() const
|
||||
return media_in_input_;
|
||||
}
|
||||
|
||||
NodeInput *Block::media_out_input() const
|
||||
NodeInput *Block::speed_input() const
|
||||
{
|
||||
return media_out_input_;
|
||||
return speed_input_;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,8 @@ public:
|
||||
void set_out(const rational& out);
|
||||
|
||||
rational length() const;
|
||||
virtual void set_length(const rational &length);
|
||||
virtual void set_length_and_media_out(const rational &length);
|
||||
virtual void set_length_and_media_in(const rational &length);
|
||||
void set_length_and_media_out(const rational &length);
|
||||
void set_length_and_media_in(const rational &length);
|
||||
|
||||
Block* previous();
|
||||
Block* next();
|
||||
@@ -69,10 +68,9 @@ public:
|
||||
void set_media_in(const rational& media_in);
|
||||
|
||||
rational media_out() const;
|
||||
void set_media_out(const rational& media_out);
|
||||
|
||||
rational media_length() const;
|
||||
double speed() const;
|
||||
rational speed() const;
|
||||
void set_speed(const rational& speed);
|
||||
bool is_still() const;
|
||||
bool is_reversed() const;
|
||||
|
||||
@@ -92,7 +90,7 @@ public:
|
||||
|
||||
NodeInput* length_input() const;
|
||||
NodeInput* media_in_input() const;
|
||||
NodeInput* media_out_input() const;
|
||||
NodeInput* speed_input() const;
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -119,7 +117,7 @@ protected:
|
||||
private:
|
||||
NodeInput* length_input_;
|
||||
NodeInput* media_in_input_;
|
||||
NodeInput* media_out_input_;
|
||||
NodeInput* speed_input_;
|
||||
|
||||
rational in_point_;
|
||||
rational out_point_;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "transition.h"
|
||||
|
||||
TransitionBlock::TransitionBlock()
|
||||
TransitionBlock::TransitionBlock() :
|
||||
connected_out_block_(nullptr),
|
||||
connected_in_block_(nullptr)
|
||||
{
|
||||
out_block_input_ = new NodeInput("out_block_in", NodeParam::kBuffer);
|
||||
out_block_input_->set_is_keyframable(false);
|
||||
@@ -9,9 +11,6 @@ TransitionBlock::TransitionBlock()
|
||||
in_block_input_ = new NodeInput("in_block_in", NodeParam::kBuffer);
|
||||
in_block_input_->set_is_keyframable(false);
|
||||
AddInput(in_block_input_);
|
||||
|
||||
// A block's length must be greater than 0
|
||||
set_in_and_out_offset(1, 1);
|
||||
}
|
||||
|
||||
Block::Type TransitionBlock::type() const
|
||||
@@ -35,38 +34,12 @@ void TransitionBlock::Retranslate()
|
||||
in_block_input_->set_name(tr("To"));
|
||||
}
|
||||
|
||||
void TransitionBlock::set_length_and_media_out(const rational &length)
|
||||
rational TransitionBlock::in_offset() const
|
||||
{
|
||||
Q_UNUSED(length)
|
||||
qCritical() << "Set length is not permitted on a transition";
|
||||
abort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TransitionBlock::set_length_and_media_in(const rational &length)
|
||||
rational TransitionBlock::out_offset() const
|
||||
{
|
||||
Q_UNUSED(length)
|
||||
qCritical() << "Set length and media in is not permitted on a transition";
|
||||
abort();
|
||||
}
|
||||
|
||||
const rational &TransitionBlock::in_offset() const
|
||||
{
|
||||
return in_offset_;
|
||||
}
|
||||
|
||||
const rational &TransitionBlock::out_offset() const
|
||||
{
|
||||
return out_offset_;
|
||||
}
|
||||
|
||||
void TransitionBlock::set_in_and_out_offset(const rational &in_offset, const rational &out_offset)
|
||||
{
|
||||
in_offset_ = in_offset;
|
||||
out_offset_ = out_offset;
|
||||
RecalculateLength();
|
||||
}
|
||||
|
||||
void TransitionBlock::RecalculateLength()
|
||||
{
|
||||
Block::set_length_and_media_out(in_offset_ + out_offset_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -15,23 +15,17 @@ public:
|
||||
|
||||
virtual void Retranslate() override;
|
||||
|
||||
virtual void set_length_and_media_out(const rational &length) override;
|
||||
virtual void set_length_and_media_in(const rational &length) override;
|
||||
|
||||
const rational& in_offset() const;
|
||||
const rational& out_offset() const;
|
||||
void set_in_and_out_offset(const rational& in_offset, const rational& out_offset);
|
||||
rational in_offset() const;
|
||||
rational out_offset() const;
|
||||
|
||||
private:
|
||||
void RecalculateLength();
|
||||
|
||||
NodeInput* out_block_input_;
|
||||
|
||||
NodeInput* in_block_input_;
|
||||
|
||||
rational in_offset_;
|
||||
Block* connected_out_block_;
|
||||
|
||||
rational out_offset_;
|
||||
Block* connected_in_block_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -52,10 +52,10 @@ NodeValueTable AudioRenderWorker::RenderBlock(const TrackOutput *track, const Ti
|
||||
|
||||
if (!samples_from_this_block.isEmpty()) {
|
||||
// Stretch samples here
|
||||
if (b->media_length() != b->length()) {
|
||||
if (b->speed() != 1) {
|
||||
QByteArray speed_adjusted_samples;
|
||||
|
||||
double clip_speed = b->speed();
|
||||
double clip_speed = b->speed().toDouble();
|
||||
|
||||
int sample_count = audio_params_.bytes_to_samples(samples_from_this_block.size());
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ void VideoRenderWorker::HashNodeRecursively(QCryptographicHash *hash, const Node
|
||||
|
||||
// Ignore some Block attributes when hashing
|
||||
if (input == b->media_in_input()
|
||||
|| input == b->media_out_input()
|
||||
|| input == b->speed_input()
|
||||
|| input == b->length_input()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@ void TimelineWidget::SlipTool::MouseReleaseInternal(TimelineViewMouseEvent *even
|
||||
Block* b = Node::ValueToPtr<Block>(ghost->data(TimelineViewGhostItem::kAttachedBlock));
|
||||
|
||||
new BlockSetMediaInCommand(b, ghost->GetAdjustedMediaIn(), command);
|
||||
new BlockSetMediaOutCommand(b, ghost->GetAdjustedMediaIn() + b->media_length(), command);
|
||||
}
|
||||
|
||||
Core::instance()->undo_stack()->pushIfHasChildren(command);
|
||||
|
||||
@@ -104,7 +104,8 @@ void TimelineWidget::TransitionTool::MouseRelease(TimelineViewMouseEvent *event)
|
||||
command);
|
||||
|
||||
if (dual_transition_) {
|
||||
transition->set_in_and_out_offset(ghost_->AdjustedLength()/2, ghost_->AdjustedLength()/2);
|
||||
//transition->set_in_and_out_offset(ghost_->AdjustedLength()/2, ghost_->AdjustedLength()/2);
|
||||
transition->set_length_and_media_out(ghost_->AdjustedLength());
|
||||
|
||||
// Block mouse is hovering over
|
||||
Block* active_block = Node::ValueToPtr<Block>(ghost_->data(TimelineViewGhostItem::kAttachedBlock));
|
||||
@@ -129,10 +130,10 @@ void TimelineWidget::TransitionTool::MouseRelease(TimelineViewMouseEvent *event)
|
||||
NodeInput* transition_input_to_connect;
|
||||
|
||||
if (ghost_->mode() == Timeline::kTrimIn) {
|
||||
transition->set_in_and_out_offset(ghost_->AdjustedLength(), 0);
|
||||
transition->set_length_and_media_out(ghost_->AdjustedLength());
|
||||
transition_input_to_connect = transition->in_block_input();
|
||||
} else {
|
||||
transition->set_in_and_out_offset(0, ghost_->AdjustedLength());
|
||||
transition->set_length_and_media_out(ghost_->AdjustedLength());
|
||||
transition_input_to_connect = transition->out_block_input();
|
||||
}
|
||||
|
||||
|
||||
@@ -48,24 +48,6 @@ void BlockResizeCommand::undo()
|
||||
block_->set_length_and_media_out(old_length_);
|
||||
}
|
||||
|
||||
BlockResizeWithoutMediaOutCommand::BlockResizeWithoutMediaOutCommand(Block *block, rational new_length, QUndoCommand *parent) :
|
||||
QUndoCommand(parent),
|
||||
block_(block),
|
||||
old_length_(block->length()),
|
||||
new_length_(new_length)
|
||||
{
|
||||
}
|
||||
|
||||
void BlockResizeWithoutMediaOutCommand::redo()
|
||||
{
|
||||
block_->set_length(new_length_);
|
||||
}
|
||||
|
||||
void BlockResizeWithoutMediaOutCommand::undo()
|
||||
{
|
||||
block_->set_length(old_length_);
|
||||
}
|
||||
|
||||
BlockResizeWithMediaInCommand::BlockResizeWithMediaInCommand(Block *block, rational new_length, QUndoCommand *parent) :
|
||||
QUndoCommand(parent),
|
||||
block_(block),
|
||||
@@ -102,24 +84,6 @@ void BlockSetMediaInCommand::undo()
|
||||
block_->set_media_in(old_media_in_);
|
||||
}
|
||||
|
||||
BlockSetMediaOutCommand::BlockSetMediaOutCommand(Block *block, rational new_media_out, QUndoCommand *parent) :
|
||||
QUndoCommand(parent),
|
||||
block_(block),
|
||||
old_media_out_(block->media_out()),
|
||||
new_media_out_(new_media_out)
|
||||
{
|
||||
}
|
||||
|
||||
void BlockSetMediaOutCommand::redo()
|
||||
{
|
||||
block_->set_media_out(new_media_out_);
|
||||
}
|
||||
|
||||
void BlockSetMediaOutCommand::undo()
|
||||
{
|
||||
block_->set_media_out(old_media_out_);
|
||||
}
|
||||
|
||||
TrackRippleRemoveBlockCommand::TrackRippleRemoveBlockCommand(TrackOutput *track, Block *block, QUndoCommand *parent) :
|
||||
QUndoCommand(parent),
|
||||
track_(track),
|
||||
@@ -411,13 +375,10 @@ void BlockSplitCommand::redo()
|
||||
Node::CopyInputs(block_, new_block_);
|
||||
|
||||
rational new_part_length = block_->length() - (point_ - block_->in());
|
||||
rational original_media_out = block_->media_out();
|
||||
|
||||
block_->set_length_and_media_out(new_length_);
|
||||
|
||||
new_block_->set_length(new_part_length);
|
||||
new_block_->set_media_in(block_->media_out());
|
||||
new_block_->set_media_out(original_media_out);
|
||||
new_block_->set_length_and_media_in(new_part_length);
|
||||
|
||||
track_->InsertBlockAfter(new_block_, block_);
|
||||
|
||||
@@ -588,7 +549,7 @@ void TrackCleanGapsCommand::redo()
|
||||
|
||||
new_gap_length += gap->length();
|
||||
}
|
||||
on_gap->set_length(new_gap_length);
|
||||
on_gap->set_length_and_media_out(new_gap_length);
|
||||
track->UnblockInvalidateCache();
|
||||
|
||||
// Reset state
|
||||
@@ -624,7 +585,7 @@ void TrackCleanGapsCommand::undo()
|
||||
for (int i=merged_gaps_.size()-1;i>=0;i--) {
|
||||
const MergedGap& merge_info = merged_gaps_.at(i);
|
||||
|
||||
merge_info.merged->set_length(merge_info.original_length);
|
||||
merge_info.merged->set_length_and_media_out(merge_info.original_length);
|
||||
|
||||
GapBlock* last_gap_added = merge_info.merged;
|
||||
|
||||
@@ -638,3 +599,21 @@ void TrackCleanGapsCommand::undo()
|
||||
|
||||
merged_gaps_.clear();
|
||||
}
|
||||
|
||||
BlockSetSpeedCommand::BlockSetSpeedCommand(Block *block, const rational &new_speed, QUndoCommand *parent) :
|
||||
QUndoCommand(parent),
|
||||
block_(block),
|
||||
old_speed_(block->speed()),
|
||||
new_speed_(new_speed)
|
||||
{
|
||||
}
|
||||
|
||||
void BlockSetSpeedCommand::redo()
|
||||
{
|
||||
block_->set_speed(new_speed_);
|
||||
}
|
||||
|
||||
void BlockSetSpeedCommand::undo()
|
||||
{
|
||||
block_->set_speed(old_speed_);
|
||||
}
|
||||
|
||||
@@ -41,19 +41,6 @@ private:
|
||||
rational new_length_;
|
||||
};
|
||||
|
||||
class BlockResizeWithoutMediaOutCommand : public QUndoCommand {
|
||||
public:
|
||||
BlockResizeWithoutMediaOutCommand(Block* block, rational new_length, QUndoCommand* parent = nullptr);
|
||||
|
||||
virtual void redo() override;
|
||||
virtual void undo() override;
|
||||
|
||||
private:
|
||||
Block* block_;
|
||||
rational old_length_;
|
||||
rational new_length_;
|
||||
};
|
||||
|
||||
class BlockResizeWithMediaInCommand : public QUndoCommand {
|
||||
public:
|
||||
BlockResizeWithMediaInCommand(Block* block, rational new_length, QUndoCommand* parent = nullptr);
|
||||
@@ -80,17 +67,18 @@ private:
|
||||
rational new_media_in_;
|
||||
};
|
||||
|
||||
class BlockSetMediaOutCommand : public QUndoCommand {
|
||||
class BlockSetSpeedCommand : public QUndoCommand {
|
||||
public:
|
||||
BlockSetMediaOutCommand(Block* block, rational new_media_out, QUndoCommand* parent = nullptr);
|
||||
BlockSetSpeedCommand(Block* block, const rational& new_speed, QUndoCommand* parent = nullptr);
|
||||
|
||||
virtual void redo() override;
|
||||
virtual void undo() override;
|
||||
|
||||
private:
|
||||
Block* block_;
|
||||
rational old_media_out_;
|
||||
rational new_media_out_;
|
||||
|
||||
rational old_speed_;
|
||||
rational new_speed_;
|
||||
};
|
||||
|
||||
class TrackRippleRemoveBlockCommand : public QUndoCommand {
|
||||
|
||||
Reference in New Issue
Block a user