From c2e47a296cae5d402da222db3100ba6a8a50f698 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Mon, 1 Jun 2020 04:42:59 +1000 Subject: [PATCH] timeline: heavily optimized and improved nearly all timeline optimizations Features a lot of timeline-related cache optimizations as well as general optimizations and improvements in timeline behavior. Should improve usability significantly. --- app/audio/CMakeLists.txt | 4 +- app/audio/audiovisualwaveform.cpp | 300 +++++++++++ app/audio/audiovisualwaveform.h | 110 ++++ app/audio/sumsamples.cpp | 110 ---- app/audio/sumsamples.h | 66 --- app/codec/samplebuffer.cpp | 8 +- app/codec/samplebuffer.h | 4 +- app/common/CMakeLists.txt | 1 - app/node/block/clip/clip.cpp | 2 +- app/node/block/clip/clip.h | 15 + app/node/math/math/math.cpp | 4 +- app/node/output/track/track.h | 2 +- app/node/output/track/tracklist.h | 2 +- app/node/output/viewer/viewer.h | 2 +- app/render/backend/renderworker.cpp | 61 +-- app/timeline/CMakeLists.txt | 1 + app/{common => timeline}/timelinecommon.h | 11 + app/timeline/trackreference.h | 2 +- app/widget/timebased/timebased.h | 2 +- app/widget/timelinewidget/timelinewidget.cpp | 126 ++--- app/widget/timelinewidget/timelinewidget.h | 88 ++-- app/widget/timelinewidget/tool/add.cpp | 2 +- app/widget/timelinewidget/tool/import.cpp | 14 +- app/widget/timelinewidget/tool/pointer.cpp | 498 ++++++++---------- app/widget/timelinewidget/tool/ripple.cpp | 118 ++--- app/widget/timelinewidget/tool/rolling.cpp | 86 +-- app/widget/timelinewidget/tool/slide.cpp | 134 +++-- app/widget/timelinewidget/tool/slip.cpp | 18 +- app/widget/timelinewidget/tool/tool.cpp | 2 +- app/widget/timelinewidget/undo/undo.cpp | 436 ++++++++++++++- app/widget/timelinewidget/undo/undo.h | 83 +++ .../view/timelineviewblockitem.cpp | 28 +- .../view/timelineviewghostitem.h | 2 +- app/widget/viewer/audiowaveformview.cpp | 89 +--- app/widget/viewer/audiowaveformview.h | 4 +- 35 files changed, 1496 insertions(+), 939 deletions(-) create mode 100644 app/audio/audiovisualwaveform.cpp create mode 100644 app/audio/audiovisualwaveform.h delete mode 100644 app/audio/sumsamples.cpp delete mode 100644 app/audio/sumsamples.h rename app/{common => timeline}/timelinecommon.h (87%) diff --git a/app/audio/CMakeLists.txt b/app/audio/CMakeLists.txt index 0e86913ad..b8b8e49a2 100644 --- a/app/audio/CMakeLists.txt +++ b/app/audio/CMakeLists.txt @@ -18,14 +18,14 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} audio/audiomanager.h audio/audiomanager.cpp + audio/audiovisualwaveform.h + audio/audiovisualwaveform.cpp audio/outputdeviceproxy.h audio/outputdeviceproxy.cpp audio/outputmanager.h audio/outputmanager.cpp audio/sampleformat.h audio/sampleformat.cpp - audio/sumsamples.h - audio/sumsamples.cpp audio/tempoprocessor.h audio/tempoprocessor.cpp PARENT_SCOPE diff --git a/app/audio/audiovisualwaveform.cpp b/app/audio/audiovisualwaveform.cpp new file mode 100644 index 000000000..c9ed53f97 --- /dev/null +++ b/app/audio/audiovisualwaveform.cpp @@ -0,0 +1,300 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "audiovisualwaveform.h" + +#include + +#include "config/config.h" + +OLIVE_NAMESPACE_ENTER + +const int AudioVisualWaveform::kSumSampleRate = 200; + +void AudioVisualWaveform::AddSum(const float *samples, int nb_samples, int nb_channels) +{ + data_.append(SumSamples(samples, nb_samples, nb_channels)); +} + +/* +void AudioVisualWaveform::AddSamples(SampleBufferPtr samples) +{ + if (!params_.is_valid()) { + + } + + int chunk_size = (audio_params_.sample_rate() / waveform_params.sample_rate()); + + qint64 start_offset = sizeof(SampleSummer::Info) + waveform_params.time_to_bytes(range_for_block.in() - b->in()); + qint64 length_offset = waveform_params.time_to_bytes(range_for_block.length()); + qint64 end_offset = start_offset + length_offset; + + if (wave_file.size() < end_offset) { + wave_file.resize(end_offset); + } + + wave_file.seek(start_offset); + + for (int i=0;isample_count();i+=chunk_size) { + QVector summary = SumSamples(samples, + i, + qMin(chunk_size, samples->sample_count_per_channel() - i)); + + wave_file.write(reinterpret_cast(summary.constData()), + summary.size() * sizeof(SampleSummer::Sum)); + } +} +*/ + +void AudioVisualWaveform::OverwriteSamples(SampleBufferPtr samples, int sample_rate, const rational &start) +{ + if (!channels_) { + qWarning() << "Failed to write samples - channel count is zero"; + } + + int start_index = channels_ * qFloor(kSumSampleRate * start.toDouble()); + int samples_length = channels_ * qFloor(kSumSampleRate * (static_cast(samples->sample_count()) / static_cast(sample_rate))); + + int end_index = start_index + samples_length; + if (data_.size() < end_index) { + data_.resize(end_index); + } + + int chunk_size = sample_rate / kSumSampleRate; + + for (int i=0; i summary = SumSamples(samples, + src_index, + qMin(chunk_size, samples->sample_count() - src_index)); + + memcpy(&data_.data()[i + start_index], + summary.constData(), + summary.size() * sizeof(SamplePerChannel)); + } +} + +AudioVisualWaveform AudioVisualWaveform::Cut(const rational &time) +{ + int sample_index = time_to_samples(time); + + // Create a copy of this waveform chop the early section off + AudioVisualWaveform copy = *this; + copy.data_ = data_.mid(sample_index); + + // Chop the latter section off too + data_.resize(sample_index); + + return copy; +} + +void AudioVisualWaveform::Append(const AudioVisualWaveform &waveform) +{ + data_.append(waveform.data_); +} + +void AudioVisualWaveform::TrimIn(const rational &time) +{ + data_ = data_.mid(time_to_samples(time)); +} + +void AudioVisualWaveform::TrimOut(const rational &time) +{ + data_.resize(data_.size() - time_to_samples(time)); +} + +void AudioVisualWaveform::PrependSilence(const rational &time) +{ + int added_samples = time_to_samples(time); + + // Resize buffer for extra space + data_.resize(data_.size() + added_samples); + + // Shift all data forward + for (int i=data_.size()-1; i>=added_samples; i--) { + data_[i] = data_[i - added_samples]; + } + + // Fill remainder with silence + for (int i=0;i AudioVisualWaveform::SumSamples(const float *samples, int nb_samples, int nb_channels) +{ + return SumSamplesInternal(samples, nb_samples, nb_channels); +} + +QVector AudioVisualWaveform::SumSamples(const qfloat16 *samples, int nb_samples, int nb_channels) +{ + return SumSamplesInternal(samples, nb_samples, nb_channels); +} + +QVector AudioVisualWaveform::SumSamples(SampleBufferPtr samples, int start_index, int length) +{ + QVector summed_samples(samples->audio_params().channel_count()); + + int end_index = start_index + length; + + for (int i=start_index;iaudio_params().channel_count();channel++) { + ClampMinMax(summed_samples[channel], samples->data()[channel][i]); + } + } + + return summed_samples; +} + +QVector AudioVisualWaveform::ReSumSamples(const SamplePerChannel* samples, + int nb_samples, + int nb_channels) +{ + QVector summed_samples(nb_channels); + + for (int i=0;i summed_samples[channel].max) { + summed_samples[channel].max = sample.max; + } + } + + return summed_samples; +} + +void AudioVisualWaveform::DrawSample(QPainter *painter, const QVector& sample, int x, int y, int height) +{ + int channel_height = height / sample.size(); + int channel_half_height = channel_height / 2; + + for (int i=0;idrawLine(x, + channel_bottom - diff, + x, + channel_bottom); + } else { + int channel_mid = y + channel_height * i + channel_half_height; + + painter->drawLine(x, + channel_mid + qRound(sample.at(i).min * static_cast(channel_half_height)), + x, + channel_mid + qRound(sample.at(i).max * static_cast(channel_half_height))); + } + } +} + +void AudioVisualWaveform::DrawWaveform(QPainter *painter, const QRect& rect, const double& scale, const AudioVisualWaveform &samples) +{ + int sample_index, next_sample_index = 0; + + QVector summary; + int summary_index = -1; + + const QRect& viewport = painter->viewport(); + QPoint top_left = painter->transform().map(viewport.topLeft()); + + int start = qMax(rect.x(), -top_left.x()); + int end = qMin(rect.right(), -top_left.x() + viewport.width()); + + QVector lines; + + for (int i=start;i(kSumSampleRate) * static_cast(i - rect.x() + 1) / scale) * samples.channel_count()); + + if (summary_index != sample_index) { + summary = AudioVisualWaveform::ReSumSamples(&samples.data_.at(sample_index), + qMax(samples.channel_count(), next_sample_index - sample_index), + samples.channel_count()); + summary_index = sample_index; + } + + DrawSample(painter, summary, i, rect.y(), rect.height()); + } + + painter->drawLines(lines); +} + +int AudioVisualWaveform::time_to_samples(const rational &time) const +{ + return qFloor(time.toDouble() * kSumSampleRate) * channels_; +} + +template +QVector AudioVisualWaveform::SumSamplesInternal(const T *samples, int nb_samples, int nb_channels) +{ + QVector summed_samples(nb_channels); + + for (int i=0;i(summed_samples[i%nb_channels], samples[i]); + } + + return summed_samples; +} + +template +void AudioVisualWaveform::ClampMinMax(AudioVisualWaveform::SamplePerChannel &sum, T value) +{ + if (value < sum.min) { + sum.min = value; + } + + if (value > sum.max) { + sum.max = value; + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/audio/audiovisualwaveform.h b/app/audio/audiovisualwaveform.h new file mode 100644 index 000000000..35df5a054 --- /dev/null +++ b/app/audio/audiovisualwaveform.h @@ -0,0 +1,110 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef SUMSAMPLES_H +#define SUMSAMPLES_H + +#include +#include +#include + +#include "codec/samplebuffer.h" + +OLIVE_NAMESPACE_ENTER + +/** + * @brief A buffer of data used to store a visual representation of audio + * + * This differs from a SampleBuffer as the data in an AudioVisualWaveform has been reduced + * significantly and optimized for visual display. + */ +class AudioVisualWaveform { +public: + AudioVisualWaveform() = default; + + struct SamplePerChannel { + qfloat16 min = 0; + qfloat16 max = 0; + }; + + using Sample = QVector; + + int channel_count() const + { + return channels_; + } + + void set_channel_count(int channels) + { + channels_ = channels; + } + + int nb_samples() const + { + return data_.size(); + } + + const SamplePerChannel* const_data() const + { + return data_.constData(); + } + + void AddSum(const float* samples, int nb_samples, int nb_channels); + + void OverwriteSamples(SampleBufferPtr samples, int sample_rate, const rational& start); + + AudioVisualWaveform Cut(const rational& time); + void Append(const AudioVisualWaveform& waveform); + void TrimIn(const rational& time); + void TrimOut(const rational& time); + void PrependSilence(const rational& time); + void AppendSilence(const rational& time); + + // FIXME: Move to dynamic + static const int kSumSampleRate; + + static QVector SumSamples(const float* samples, int nb_samples, int nb_channels); + static QVector SumSamples(const qfloat16* samples, int nb_samples, int nb_channels); + static QVector SumSamples(SampleBufferPtr samples, int start_index, int length); + + static QVector ReSumSamples(const SamplePerChannel *samples, int nb_samples, int nb_channels); + + static void DrawSample(QPainter* painter, const QVector &sample, int x, int y, int height); + + static void DrawWaveform(QPainter* painter, const QRect &rect, const double &scale, const AudioVisualWaveform& samples); + +private: + template + static QVector SumSamplesInternal(const T* samples, int nb_samples, int nb_channels); + + template + static void ClampMinMax(SamplePerChannel &sum, T value); + + int time_to_samples(const rational& time) const; + + int channels_ = 0; + + QVector data_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // SUMSAMPLES_H diff --git a/app/audio/sumsamples.cpp b/app/audio/sumsamples.cpp deleted file mode 100644 index 3aa5db702..000000000 --- a/app/audio/sumsamples.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "sumsamples.h" - -#include - -OLIVE_NAMESPACE_ENTER - -const int SampleSummer::kSumSampleRate = 200; - -QVector SampleSummer::SumSamples(const float *samples, int nb_samples, int nb_channels) -{ - return SumSamplesInternal(samples, nb_samples, nb_channels); -} - -QVector SampleSummer::SumSamples(const qfloat16 *samples, int nb_samples, int nb_channels) -{ - return SumSamplesInternal(samples, nb_samples, nb_channels); -} - -QVector SampleSummer::SumSamples(SampleBufferPtr samples, int start_index, int length) -{ - QVector summed_samples(samples->audio_params().channel_count()); - - int end_index = start_index + length; - - for (int i=start_index;iaudio_params().channel_count();channel++) { - ClampMinMax(summed_samples[channel], samples->data()[channel][i]); - } - } - - return summed_samples; -} - -QVector SampleSummer::ReSumSamples(const SampleSummer::Sum *samples, int nb_samples, int nb_channels) -{ - QVector summed_samples(nb_channels); - - for (int i=0;i summed_samples[channel].max) { - summed_samples[channel].max = sample.max; - } - } - - return summed_samples; -} - -SampleSummer::Sum::Sum() -{ - min = 0; - max = 0; -} - -template -QVector SampleSummer::SumSamplesInternal(const T *samples, int nb_samples, int nb_channels) -{ - QVector summed_samples(nb_channels); - - for (int i=0;i(summed_samples[i%nb_channels], samples[i]); - } - - return summed_samples; -} - -template -void SampleSummer::ClampMinMax(SampleSummer::Sum &sum, T value) -{ - if (value < sum.min) { - sum.min = value; - } - - if (value > sum.max) { - sum.max = value; - } -} - -SampleSummer::Info::Info() -{ - channels = 0; -} - -OLIVE_NAMESPACE_EXIT diff --git a/app/audio/sumsamples.h b/app/audio/sumsamples.h deleted file mode 100644 index 00d8b4e92..000000000 --- a/app/audio/sumsamples.h +++ /dev/null @@ -1,66 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2019 Olive Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef SUMSAMPLES_H -#define SUMSAMPLES_H - -#include -#include - -#include "codec/samplebuffer.h" - -OLIVE_NAMESPACE_ENTER - -class SampleSummer { -public: - struct Sum { - Sum(); - - qfloat16 min; - qfloat16 max; - }; - - // FIXME: Move to config - static const int kSumSampleRate; - - static QVector SumSamples(const float* samples, int nb_samples, int nb_channels); - static QVector SumSamples(const qfloat16* samples, int nb_samples, int nb_channels); - static QVector SumSamples(SampleBufferPtr samples, int start_index, int length); - - static QVector ReSumSamples(const SampleSummer::Sum* samples, int nb_samples, int nb_channels); - - struct Info { - Info(); - - int channels; - }; - -private: - template - static QVector SumSamplesInternal(const T* samples, int nb_samples, int nb_channels); - - template - static void ClampMinMax(Sum &sum, T value); - -}; - -OLIVE_NAMESPACE_EXIT - -#endif // SUMSAMPLES_H diff --git a/app/codec/samplebuffer.cpp b/app/codec/samplebuffer.cpp index 1dde4fbae..0cc6c540c 100644 --- a/app/codec/samplebuffer.cpp +++ b/app/codec/samplebuffer.cpp @@ -1,4 +1,4 @@ -/*** +/*** Olive - Non-Linear Video Editor Copyright (C) 2019 Olive Team @@ -43,7 +43,7 @@ SampleBufferPtr SampleBuffer::CreateAllocated(const AudioRenderingParams &audio_ SampleBufferPtr buffer = Create(); buffer->set_audio_params(audio_params); - buffer->set_sample_count_per_channel(samples_per_channel); + buffer->set_sample_count(samples_per_channel); buffer->allocate(); return buffer; @@ -88,12 +88,12 @@ void SampleBuffer::set_audio_params(const AudioRenderingParams ¶ms) audio_params_ = params; } -const int &SampleBuffer::sample_count_per_channel() const +const int &SampleBuffer::sample_count() const { return sample_count_per_channel_; } -void SampleBuffer::set_sample_count_per_channel(const int &sample_count) +void SampleBuffer::set_sample_count(const int &sample_count) { if (data_) { qWarning() << "Tried to set sample count on allocated sample buffer"; diff --git a/app/codec/samplebuffer.h b/app/codec/samplebuffer.h index 83a2e4d9c..33bcd07f4 100644 --- a/app/codec/samplebuffer.h +++ b/app/codec/samplebuffer.h @@ -54,8 +54,8 @@ public: const AudioRenderingParams& audio_params() const; void set_audio_params(const AudioRenderingParams& params); - const int &sample_count_per_channel() const; - void set_sample_count_per_channel(const int &sample_count_per_channel); + const int &sample_count() const; + void set_sample_count(const int &sample_count); float** data(); const float** const_data() const; diff --git a/app/common/CMakeLists.txt b/app/common/CMakeLists.txt index be51fa1e8..9b6a1e6f5 100644 --- a/app/common/CMakeLists.txt +++ b/app/common/CMakeLists.txt @@ -42,7 +42,6 @@ set(OLIVE_SOURCES common/threadedobject.cpp common/timecodefunctions.h common/timecodefunctions.cpp - common/timelinecommon.h common/timerange.h common/timerange.cpp common/tohex.h diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index 413050d98..288cb53c6 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -62,7 +62,7 @@ NodeInput *ClipBlock::texture_input() const void ClipBlock::InvalidateCache(const TimeRange &range, NodeInput *from, NodeInput *source) { // If signal is from texture input, transform all times from media time to sequence time - if (from == texture_input_) { + if (from == texture_input_ || from == media_in_input()) { rational start = MediaToSequenceTime(range.in()); rational end = MediaToSequenceTime(range.out()); diff --git a/app/node/block/clip/clip.h b/app/node/block/clip/clip.h index 375f319ce..3ce6a7b39 100644 --- a/app/node/block/clip/clip.h +++ b/app/node/block/clip/clip.h @@ -21,6 +21,7 @@ #ifndef CLIPBLOCK_H #define CLIPBLOCK_H +#include "audio/audiovisualwaveform.h" #include "node/block/block.h" OLIVE_NAMESPACE_ENTER @@ -56,12 +57,26 @@ public: virtual void Hash(QCryptographicHash &hash, const rational &time) const override; + AudioVisualWaveform& waveform() + { + return waveform_; + } + + void set_waveform(const AudioVisualWaveform& wave) + { + waveform_ = wave; + + emit PreviewUpdated(); + } + signals: void PreviewUpdated(); private: NodeInput* texture_input_; + AudioVisualWaveform waveform_; + }; OLIVE_NAMESPACE_EXIT diff --git a/app/node/math/math/math.cpp b/app/node/math/math/math.cpp index 149bcb08a..f8d90e0db 100644 --- a/app/node/math/math/math.cpp +++ b/app/node/math/math/math.cpp @@ -318,8 +318,8 @@ NodeValueTable MathNode::Value(NodeValueDatabase &value) const SampleBufferPtr samples_a = val_a.data().value(); SampleBufferPtr samples_b = val_b.data().value(); - int max_samples = qMax(samples_a->sample_count_per_channel(), samples_b->sample_count_per_channel()); - int min_samples = qMin(samples_a->sample_count_per_channel(), samples_b->sample_count_per_channel()); + int max_samples = qMax(samples_a->sample_count(), samples_b->sample_count()); + int min_samples = qMin(samples_a->sample_count(), samples_b->sample_count()); SampleBufferPtr mixed_samples = SampleBuffer::CreateAllocated(samples_a->audio_params(), max_samples); diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 1cb04f451..90d716b56 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -21,8 +21,8 @@ #ifndef TRACKOUTPUT_H #define TRACKOUTPUT_H -#include "common/timelinecommon.h" #include "node/block/block.h" +#include "timeline/timelinecommon.h" OLIVE_NAMESPACE_ENTER diff --git a/app/node/output/track/tracklist.h b/app/node/output/track/tracklist.h index 44c380cc9..d0923f577 100644 --- a/app/node/output/track/tracklist.h +++ b/app/node/output/track/tracklist.h @@ -23,9 +23,9 @@ #include -#include "common/timelinecommon.h" #include "node/graph.h" #include "node/output/track/track.h" +#include "timeline/timelinecommon.h" OLIVE_NAMESPACE_ENTER diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 2b188e5da..31c9dab8d 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -23,7 +23,6 @@ #include -#include "common/timelinecommon.h" #include "node/block/block.h" #include "node/output/track/track.h" #include "node/output/track/tracklist.h" @@ -32,6 +31,7 @@ #include "render/audioplaybackcache.h" #include "render/framehashcache.h" #include "render/videoparams.h" +#include "timeline/timelinecommon.h" #include "timeline/trackreference.h" OLIVE_NAMESPACE_ENTER diff --git a/app/render/backend/renderworker.cpp b/app/render/backend/renderworker.cpp index a022a1f37..5e465af50 100644 --- a/app/render/backend/renderworker.cpp +++ b/app/render/backend/renderworker.cpp @@ -22,7 +22,7 @@ #include -#include "audio/sumsamples.h" +#include "audio/audiovisualwaveform.h" #include "common/functiontimer.h" #include "config/config.h" #include "node/block/clip/clip.h" @@ -118,61 +118,24 @@ NodeValueTable RenderWorker::GenerateBlockTable(const TrackOutput *track, const samples_from_this_block->reverse(); } - int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count_per_channel()); + int copy_length = qMin(max_dest_sz, samples_from_this_block->sample_count()); // Copy samples into destination buffer block_range_buffer->set(samples_from_this_block->const_data(), destination_offset, copy_length); - { + if (b->type() == Block::kClip) { // Save waveform to file - Block* src_block = static_cast(copy_map_->key(b)); - QDir local_appdata_dir(Config::Current()["DiskCachePath"].toString()); - QDir waveform_loc = local_appdata_dir.filePath(QStringLiteral("waveform")); - waveform_loc.mkpath("."); - QString wave_fn(waveform_loc.filePath(QString::number(reinterpret_cast(src_block)))); - QFile wave_file(wave_fn); + ClipBlock* src_block = static_cast(copy_map_->key(b)); - if (wave_file.open(QFile::ReadWrite)) { - // We use S32 as a size-compatible substitute for SampleSummer::Sum which is 4 bytes in - // size - AudioRenderingParams waveform_params(SampleSummer::kSumSampleRate, - audio_params_.channel_layout(), - SampleFormat::SAMPLE_FMT_S32); + AudioVisualWaveform& clip_waveform = src_block->waveform(); - int chunk_size = (audio_params_.sample_rate() / waveform_params.sample_rate()); + clip_waveform.set_channel_count(audio_params_.channel_count()); - { - // Write metadata header - SampleSummer::Info info; - info.channels = audio_params_.channel_count(); - wave_file.write(reinterpret_cast(&info), sizeof(SampleSummer::Info)); - } + clip_waveform.OverwriteSamples(samples_from_this_block, + audio_params_.sample_rate(), + range_for_block.in() - b->in()); - qint64 start_offset = sizeof(SampleSummer::Info) + waveform_params.time_to_bytes(range_for_block.in() - b->in()); - qint64 length_offset = waveform_params.time_to_bytes(range_for_block.length()); - qint64 end_offset = start_offset + length_offset; - - if (wave_file.size() < end_offset) { - wave_file.resize(end_offset); - } - - wave_file.seek(start_offset); - - for (int i=0;isample_count_per_channel();i+=chunk_size) { - QVector summary = SampleSummer::SumSamples(samples_from_this_block, - i, - qMin(chunk_size, samples_from_this_block->sample_count_per_channel() - i)); - - wave_file.write(reinterpret_cast(summary.constData()), - summary.size() * sizeof(SampleSummer::Sum)); - } - - wave_file.close(); - - if (src_block->type() == Block::kClip) { - emit static_cast(src_block)->PreviewUpdated(); - } - } + emit static_cast(src_block)->PreviewUpdated(); } NodeValueTable::Merge({merged_table, table}); @@ -212,9 +175,9 @@ void RenderWorker::ProcessNodeEvent(const Node *node, const TimeRange &range, No return; } - SampleBufferPtr output_buffer = SampleBuffer::CreateAllocated(input_buffer->audio_params(), input_buffer->sample_count_per_channel()); + SampleBufferPtr output_buffer = SampleBuffer::CreateAllocated(input_buffer->audio_params(), input_buffer->sample_count()); - int sample_count = input_buffer->sample_count_per_channel(); + int sample_count = input_buffer->sample_count(); // FIXME: Hardcoded float sample format for (int i=0;i -#include "common/timelinecommon.h" #include "node/output/viewer/viewer.h" +#include "timeline/timelinecommon.h" #include "widget/resizablescrollbar/resizablescrollbar.h" #include "widget/timelinewidget/timelinescaledobject.h" #include "widget/timelinewidget/view/timelineview.h" diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index f79cf65c5..48fd1ba61 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -473,7 +473,7 @@ void TimelineWidget::DeleteSelectedInternal(const QList &blocks, foreach (Block* b, blocks) { TrackOutput* original_track = TrackOutput::TrackFromBlock(b); - if (transition_aware && b->type() == Block::kTransition) { + /*if (transition_aware && b->type() == Block::kTransition) { // Deleting transitions restores their in/out offsets to their attached blocks TransitionBlock* transition = static_cast(b); @@ -494,20 +494,39 @@ void TimelineWidget::DeleteSelectedInternal(const QList &blocks, transition->connected_out_block()->length() + transition->out_offset(), command); } - } else { - // Make new gap and replace old Block with it for now - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(b->length()); + } else */ - new NodeAddCommand(static_cast(b->parent()), - gap, - command); - new TrackReplaceBlockCommand(original_track, - b, - gap, - command); + /* + if (b->next()) { + + new TrackRippleRemoveBlockCommand(original_track, b, command); + + if (b->previous() && b->previous()->type() == Block::kGap + && b->next() && b->next()->type() == Block::kGap) { + + // Both previous AND next are blocks. We'll want to merge them together. + new TrackRippleRemoveBlockCommand(original_track, b->next(), command); + + } else { + + // Make new gap and replace old Block with it for now + GapBlock* gap = new GapBlock(); + gap->set_length_and_media_out(b->length()); + + new NodeAddCommand(static_cast(b->parent()), + gap, + command); + + new TrackReplaceBlockCommand(original_track, + b, + gap, + command); + } } + */ + + new TrackReplaceBlockWithGapCommand(original_track, b, command); if (remove_from_graph) { new BlockUnlinkAllCommand(b, command); @@ -543,12 +562,14 @@ void TimelineWidget::DeleteSelected(bool ripple) // Replace blocks with gaps (effectively deleting them) DeleteSelectedInternal(blocks_to_delete, true, true, command); + /* // Clean each track foreach (const TrackReference& track, tracks_affected) { new TrackCleanGapsCommand(GetConnectedNode()->track_list(track.type()), track.index(), command); } + */ // Insert ripple command now that it's all cleaned up gaps if (ripple) { @@ -1225,17 +1246,17 @@ void TimelineWidget::SetBlockLinksSelected(Block* block, bool selected) } } -QVector TimelineWidget::GetEditToInfo(const rational& playhead_time, +QVector TimelineWidget::GetEditToInfo(const rational& playhead_time, Timeline::MovementMode mode) { // Get list of unlocked tracks QVector tracks = GetConnectedNode()->GetUnlockedTracks(); // Create list to cache nearest times and the blocks at this point - QVector info_list(tracks.size()); + QVector info_list(tracks.size()); for (int i=0;i tracks = GetEditToInfo(playhead_time, mode); + QVector tracks = GetEditToInfo(playhead_time, mode); // Find each track's nearest point and determine the overall timeline's nearest point rational closest_point_to_playhead = (mode == Timeline::kTrimIn) ? rational() : RATIONAL_MAX; - foreach (const EditToInfo& info, tracks) { + foreach (const Timeline::EditToInfo& info, tracks) { if (info.nearest_block) { if (mode == Timeline::kTrimIn) { closest_point_to_playhead = qMax(info.nearest_time, closest_point_to_playhead); @@ -1306,7 +1327,7 @@ void TimelineWidget::RippleTo(Timeline::MovementMode mode) QUndoCommand* command = new QUndoCommand(); - foreach (const EditToInfo& info, tracks) { + foreach (const Timeline::EditToInfo& info, tracks) { TrackOutput* track = info.track; // Simply remove this region @@ -1335,63 +1356,32 @@ void TimelineWidget::EditTo(Timeline::MovementMode mode) const rational playhead_time = GetTime(); // Get list of unlocked tracks - QVector tracks = GetEditToInfo(playhead_time, mode); + QVector tracks = GetEditToInfo(playhead_time, mode); + + if (tracks.isEmpty()) { + return; + } QUndoCommand* command = new QUndoCommand(); - foreach (const EditToInfo& info, tracks) { - TrackOutput* track = info.track; + foreach (const Timeline::EditToInfo& info, tracks) { + if (info.nearest_block + && info.nearest_block->type() != Block::kGap + && info.nearest_time != playhead_time) { + rational new_len; - Block* block_here = info.nearest_block; - - // Check if this track's nearest time was the playhead or if there's no block here, in which - // case this is a no-op - if (!block_here || info.nearest_time == playhead_time) { - continue; - } - - GapBlock* gap = nullptr; - - // Resize the block at this time - if (mode == Timeline::kTrimIn) { - rational trim_length = playhead_time - block_here->in(); - - gap = new GapBlock(); - gap->set_length_and_media_out(trim_length); - new NodeAddCommand(static_cast(track->parent()), gap, command); - - new BlockResizeWithMediaInCommand(block_here, - block_here->length() - trim_length, - command); - - if (block_here->previous()) { - new TrackInsertBlockAfterCommand(track, gap, block_here->previous(), command); + if (mode == Timeline::kTrimIn) { + new_len = playhead_time - info.nearest_time; } else { - new TrackPrependBlockCommand(track, gap, command); + new_len = info.nearest_time - playhead_time; } - } else { - rational trim_length = block_here->out() - playhead_time; + new_len = info.nearest_block->length() - new_len; - new BlockResizeCommand(block_here, - block_here->length() - trim_length, - command); - - // We only need to add a gap if there's actually a block after this one, otherwise it - // doesn't matter - if (block_here->next()) { - gap = new GapBlock(); - gap->set_length_and_media_out(trim_length); - new NodeAddCommand(static_cast(track->parent()), gap, command); - - new TrackInsertBlockAfterCommand(track, gap, block_here, command); - } - } - - // If a gap was added, clean the gaps on this track too - if (gap) { - new TrackCleanGapsCommand(GetConnectedNode()->track_list(track->track_type()), - track->Index(), - command); + new BlockTrimCommand(info.track, + info.nearest_block, + new_len, + mode, + command); } } diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 89094d80f..4c9015503 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -26,8 +26,9 @@ #include #include "core.h" -#include "timelineandtrackview.h" #include "node/output/viewer/viewer.h" +#include "timeline/timelinecommon.h" +#include "timelineandtrackview.h" #include "widget/nodecopypaste/nodecopypaste.h" #include "widget/slider/timeslider.h" #include "widget/timebased/timebased.h" @@ -179,7 +180,7 @@ private: * Validation is the process of ensuring that whatever movements the user is making are "valid" and "legal". This * function's validation ensures that no Ghost's in point ends up in a negative timecode. */ - rational ValidateFrameMovement(rational movement, const QVector ghosts); + rational ValidateTimeMovement(rational movement, const QVector ghosts); /** * @brief Validates Ghosts that are moving vertically (track-based) @@ -226,15 +227,11 @@ private: virtual void HoverMove(TimelineViewMouseEvent *event) override; protected: - void SetMovementAllowed(bool allowed); - void SetTrackMovementAllowed(bool allowed); - void SetTrimmingAllowed(bool allowed); - virtual void MouseReleaseInternal(TimelineViewMouseEvent *event); - virtual rational FrameValidateInternal(rational time_movement, const QVector &ghosts); + virtual void FinishDrag(TimelineViewMouseEvent *event); - virtual void InitiateGhosts(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode, - bool allow_gap_trimming); + virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode, + bool allow_gap_trimming); TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode); @@ -258,24 +255,51 @@ private: virtual void ProcessDrag(const TimelineCoordinate &mouse_pos); + const Timeline::MovementMode& drag_movement_mode() const + { + return drag_movement_mode_; + } + + void SetMovementAllowed(bool e) + { + movement_allowed_ = e; + } + + void SetTrimmingAllowed(bool e) + { + trimming_allowed_ = e; + } + + void SetTrackMovementAllowed(bool e) + { + track_movement_allowed_ = e; + } + + void SetTrimOverwriteAllowed(bool e) + { + trim_overwrite_allowed_ = e; + } + private: Timeline::MovementMode IsCursorInTrimHandle(TimelineViewBlockItem* block, qreal cursor_x); - void InitiateDrag(TimelineViewMouseEvent *mouse_pos); - void AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode); bool IsClipTrimmable(TimelineViewBlockItem* clip, const QList& items, const Timeline::MovementMode& mode); - TrackReference track_start_; bool movement_allowed_; bool trimming_allowed_; bool track_movement_allowed_; + bool trim_overwrite_allowed_; bool rubberband_selecting_; Timeline::TrackType drag_track_type_; + Timeline::MovementMode drag_movement_mode_; + + TimelineViewBlockItem* clicked_item_; + }; class ImportTool : public Tool @@ -332,12 +356,11 @@ private: public: RippleTool(TimelineWidget* parent); protected: - virtual void MouseReleaseInternal(TimelineViewMouseEvent *event) override; - virtual rational FrameValidateInternal(rational time_movement, const QVector& ghosts) override; + virtual void FinishDrag(TimelineViewMouseEvent *event) override; - virtual void InitiateGhosts(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode, - bool allow_gap_trimming) override; + virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode, + bool allow_gap_trimming) override; }; class RollingTool : public PointerTool @@ -346,12 +369,11 @@ private: RollingTool(TimelineWidget* parent); protected: - virtual void MouseReleaseInternal(TimelineViewMouseEvent *event) override; - virtual rational FrameValidateInternal(rational time_movement, const QVector& ghosts) override; + virtual void FinishDrag(TimelineViewMouseEvent *event) override; - virtual void InitiateGhosts(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode, - bool allow_gap_trimming) override; + virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode, + bool allow_gap_trimming) override; }; class SlideTool : public PointerTool @@ -360,11 +382,11 @@ private: SlideTool(TimelineWidget* parent); protected: - virtual void MouseReleaseInternal(TimelineViewMouseEvent *event) override; - virtual rational FrameValidateInternal(rational time_movement, const QVector& ghosts) override; - virtual void InitiateGhosts(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode, - bool allow_gap_trimming) override; + virtual void FinishDrag(TimelineViewMouseEvent *event) override; + virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode, + bool allow_gap_trimming) override; + }; class SlipTool : public PointerTool @@ -374,7 +396,7 @@ private: protected: virtual void ProcessDrag(const TimelineCoordinate &mouse_pos) override; - virtual void MouseReleaseInternal(TimelineViewMouseEvent *event) override; + virtual void FinishDrag(TimelineViewMouseEvent *event) override; }; class ZoomTool : public Tool @@ -425,13 +447,7 @@ private: void SetBlockLinksSelected(Block *block, bool selected); - struct EditToInfo { - TrackOutput* track; - rational nearest_time; - Block* nearest_block; - }; - - QVector GetEditToInfo(const rational &playhead_time, Timeline::MovementMode mode); + QVector GetEditToInfo(const rational &playhead_time, Timeline::MovementMode mode); void RippleTo(Timeline::MovementMode mode); diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index ce47e2888..4d16ea080 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -37,7 +37,7 @@ void TimelineWidget::AddTool::MousePress(TimelineViewMouseEvent *event) const TrackReference& track = event->GetTrack(); TrackOutput* t = parent()->GetTrackFromReference(track); - if (t && t->IsLocked()) { + if (!t || t->IsLocked()) { return; } diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 1709059d9..c48593326 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -127,7 +127,7 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) SnapPoint(snap_points_, &time_movement); } - time_movement = ValidateFrameMovement(time_movement, parent()->ghost_items_); + time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); rational earliest_ghost = RATIONAL_MAX; @@ -236,11 +236,14 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi footage_duration = Config::Current()["DefaultStillLength"].value(); } else { // Rescale stream duration to timeline timebase - int64_t stream_duration = Timecode::rescale_timestamp_ceil(stream->duration(), stream->timebase(), dest_tb); - // Convert to rational time - footage_duration = rational(dest_tb.numerator() * stream_duration, - dest_tb.denominator()); + if (footage.footage()->workarea()->enabled()) { + footage_duration = footage.footage()->workarea()->range().length(); + ghost->SetMediaIn(footage.footage()->workarea()->in()); + } else { + int64_t stream_duration = Timecode::rescale_timestamp_ceil(stream->duration(), stream->timebase(), dest_tb); + footage_duration = Timecode::timestamp_to_time(stream_duration, dest_tb); + } } ghost->SetIn(ghost_start); @@ -387,6 +390,7 @@ void TimelineWidget::ImportTool::DropGhosts(bool insert) StreamPtr footage_stream = ghost->data(TimelineViewGhostItem::kAttachedFootage).value(); ClipBlock* clip = new ClipBlock(); + clip->set_media_in(ghost->MediaIn()); clip->set_length_and_media_out(ghost->Length()); clip->set_block_name(footage_stream->footage()->name()); new NodeAddCommand(dst_graph, clip, command); diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 31d17bb65..39c20e006 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -41,41 +41,39 @@ TimelineWidget::PointerTool::PointerTool(TimelineWidget *parent) : movement_allowed_(true), trimming_allowed_(true), track_movement_allowed_(true), + trim_overwrite_allowed_(false), rubberband_selecting_(false) { } void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) { - // Main selection code + // Determine if item clicked on is selectable + clicked_item_ = GetItemAtScenePos(event->GetCoordinates()); - TimelineViewBlockItem* item = GetItemAtScenePos(event->GetCoordinates()); - - bool selectable_item = (item != nullptr - && item->flags() & QGraphicsItem::ItemIsSelectable - && !parent()->GetTrackFromReference(item->Track())->IsLocked()); + bool selectable_item = (clicked_item_ + && clicked_item_->flags() & QGraphicsItem::ItemIsSelectable + && !parent()->GetTrackFromReference(clicked_item_->Track())->IsLocked()); if (selectable_item) { // Cache the clip's type for use later - drag_track_type_ = item->Track().type(); - } + drag_track_type_ = clicked_item_->Track().type(); - // If this item is already selected - if (selectable_item - && item->isSelected()) { + // If this item is already selected, no further selection needs to be made + if (clicked_item_->isSelected()) { - // If shift is held, deselect it - if (event->GetModifiers() & Qt::ShiftModifier) { - item->setSelected(false); + // If shift is held, deselect it + if (event->GetModifiers() & Qt::ShiftModifier) { + clicked_item_->setSelected(false); - // If not holding alt, deselect all links as well - if (!(event->GetModifiers() & Qt::AltModifier)) { - parent()->SetBlockLinksSelected(item->block(), false); + // If not holding alt, deselect all links as well + if (!(event->GetModifiers() & Qt::AltModifier)) { + parent()->SetBlockLinksSelected(clicked_item_->block(), false); + } } - } - // Otherwise do nothing - return; + return; + } } // If not holding shift, deselect all clips @@ -85,11 +83,11 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) if (selectable_item) { // Select this item - item->setSelected(true); + clicked_item_->setSelected(true); // If not holding alt, select all links as well if (!(event->GetModifiers() & Qt::AltModifier)) { - parent()->SetBlockLinksSelected(item->block(), true); + parent()->SetBlockLinksSelected(clicked_item_->block(), true); } } else if (event->GetButton() == Qt::LeftButton) { // Start rubberband drag @@ -110,7 +108,28 @@ void TimelineWidget::PointerTool::MouseMove(TimelineViewMouseEvent *event) // Now that the cursor has moved, we will assume the intention is to drag // If we haven't started dragging yet, we'll initiate a drag here - InitiateDrag(event); + // Record where the drag started in timeline coordinates + drag_start_ = event->GetCoordinates(); + + // Clear snap points + snap_points_.clear(); + + // Determine whether we're trimming or moving based on the position of the cursor + drag_movement_mode_ = IsCursorInTrimHandle(clicked_item_, + event->GetSceneX()); + + // If we're not in a trim mode, we must be in a move mode (provided the tool allows movement and + // the block is not a gap) + if (drag_movement_mode_ == Timeline::kNone + && movement_allowed_ + && clicked_item_->block()->type() != Block::kGap) { + drag_movement_mode_ = Timeline::kMove; + } + + // If we're performing an action, we can initiate ghosts + if (drag_movement_mode_ != Timeline::kNone) { + InitiateDrag(clicked_item_, drag_movement_mode_, false); + } // Set dragging to true here so no matter what, the drag isn't re-initiated until it's completed dragging_ = true; @@ -132,11 +151,11 @@ void TimelineWidget::PointerTool::MouseRelease(TimelineViewMouseEvent *event) return; } - if (!parent()->ghost_items_.isEmpty()) { - MouseReleaseInternal(event); - } - if (dragging_) { + if (!parent()->ghost_items_.isEmpty()) { + FinishDrag(event); + } + parent()->ClearGhosts(); snap_points_.clear(); } @@ -146,18 +165,22 @@ void TimelineWidget::PointerTool::MouseRelease(TimelineViewMouseEvent *event) void TimelineWidget::PointerTool::HoverMove(TimelineViewMouseEvent *event) { - // No dragging, but we still want to process cursors - TimelineViewBlockItem* block_at_cursor = GetItemAtScenePos(event->GetCoordinates()); + if (trimming_allowed_) { + // No dragging, but we still want to process cursors + TimelineViewBlockItem* block_at_cursor = GetItemAtScenePos(event->GetCoordinates()); - if (block_at_cursor) { - switch (IsCursorInTrimHandle(block_at_cursor, event->GetSceneX())) { - case Timeline::kTrimIn: - parent()->setCursor(Qt::SizeHorCursor); - break; - case Timeline::kTrimOut: - parent()->setCursor(Qt::SizeHorCursor); - break; - default: + if (block_at_cursor) { + switch (IsCursorInTrimHandle(block_at_cursor, event->GetSceneX())) { + case Timeline::kTrimIn: + parent()->setCursor(Qt::SizeHorCursor); + break; + case Timeline::kTrimOut: + parent()->setCursor(Qt::SizeHorCursor); + break; + default: + parent()->unsetCursor(); + } + } else { parent()->unsetCursor(); } } else { @@ -165,200 +188,123 @@ void TimelineWidget::PointerTool::HoverMove(TimelineViewMouseEvent *event) } } -void TimelineWidget::PointerTool::SetMovementAllowed(bool allowed) +void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) { - movement_allowed_ = allowed; -} + QList ghosts_moving; + QList blocks_moving; + QList ghosts_trimming; + QList blocks_trimming; -void TimelineWidget::PointerTool::SetTrackMovementAllowed(bool allowed) -{ - track_movement_allowed_ = allowed; -} + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + if (!ghost->HasBeenAdjusted()) { + continue; + } -void TimelineWidget::PointerTool::SetTrimmingAllowed(bool allowed) -{ - trimming_allowed_ = allowed; -} + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + if (ghost->mode() == Timeline::kMove) { + ghosts_moving.append(ghost); + blocks_moving.append(b); + } else if (Timeline::IsATrimMode(ghost->mode())) { + ghosts_trimming.append(ghost); + blocks_trimming.append(b); + } + } + + if (blocks_moving.isEmpty() && blocks_trimming.isEmpty()) { + // Likely means no block was adjusted, so we can skip the rest of the processing + return; + } + + // See if we're duplicated because ALT is held (only moved blocks can duplicate) + bool duplicate_clips = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::AltModifier); + bool inserting = (!blocks_moving.isEmpty() && event->GetModifiers() & Qt::ControlModifier); -void TimelineWidget::PointerTool::MouseReleaseInternal(TimelineViewMouseEvent *event) -{ QUndoCommand* command = new QUndoCommand(); - QList blocks_to_temp_remove; - QList tracks_affected; - QList ignore_ghosts; + for (int i=0;iGetModifiers() & Qt::AltModifier); - - // Since all the ghosts will be leaving their old position in some way, we replace all of them with gaps here so the - // entire timeline isn't disrupted in the process - for (int i=0;ighost_items_.size();i++) { - TimelineViewGhostItem* ghost = parent()->ghost_items_.at(i); - - // If the ghost has not been adjusted nothing needs to be done - if (!ghost->HasBeenAdjusted()) { - ignore_ghosts.append(ghost); - continue; - } - - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (!duplicate_clips || ghost->mode() != Timeline::kMove || b->type() == Block::kTransition) { - // If we're duplicating (user is holding ALT), no need to remove the original clip. However if the ghost was - // trimmed, it can't be duplicated. - blocks_to_temp_remove.append(b); - } - - if (!tracks_affected.contains(ghost->Track())) { - tracks_affected.append(ghost->Track()); - } - - if (!tracks_affected.contains(ghost->GetAdjustedTrack())) { - tracks_affected.append(ghost->GetAdjustedTrack()); - } - } - - bool inserting = (event->GetModifiers() & Qt::ControlModifier); - - // If there are any blocks to remove, remove them - parent()->DeleteSelectedInternal(blocks_to_temp_remove, false, false, command); - - if (inserting) { - // Make room to insert clips to - InsertGapsAtGhostDestination(parent()->ghost_items_, command); - } - - // Now we place the clips back in the timeline where the user moved them. It's legal for them to overwrite parts or - // all of the gaps we inserted earlier - for (int i=0;ighost_items_.size();i++) { - TimelineViewGhostItem* ghost = parent()->ghost_items_.at(i); - - // If the ghost has not been adjusted nothing needs to be done - if (ignore_ghosts.contains(ghost)) { - continue; - } - - const TrackReference& track_ref = ghost->GetAdjustedTrack(); - //TrackOutput* track = parent()->GetTrackFromReference(track_ref); - - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - // Normal blocks work in conjunction with the gap made above - if (Timeline::IsATrimMode(ghost->mode())) { - // If we were trimming, we'll need to change the length - - // If we were trimming the in point, we'll need to adjust the media in too - if (ghost->mode() == Timeline::kTrimIn) { - new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); - } else { - new BlockResizeCommand(b, ghost->AdjustedLength(), command); - } - } else if (duplicate_clips && ghost->mode() == Timeline::kMove && b->type() != Block::kTransition) { - // Duplicate rather than move - Node* copy = b->copy(); - - new NodeAddCommand(static_cast(b->parent()), - copy, + new BlockTrimCommand(parent()->GetTrackFromReference(ghost->GetAdjustedTrack()), + blocks_trimming.at(i), + ghost->AdjustedLength(), + ghost->mode(), command); + } - new NodeCopyInputsCommand(b, copy, true, command); - - // Place the copy instead of the original block - b = static_cast(copy); - } else if (b->type() == Block::kTransition) { - // If the block is a dual transition and we're moving it, the mid point should be moved - TransitionBlock* transition = static_cast(b); - - if (transition->connected_in_block() && transition->connected_out_block()) { - new BlockSetMediaInCommand(transition, - transition->media_in() + ghost->InAdjustment(), - command); - } + if (!blocks_moving.isEmpty()) { + // If we're not duplicating, "remove" the clips and replace them with gaps + if (!duplicate_clips) { + parent()->DeleteSelectedInternal(blocks_moving, false, false, command); } - if (b->type() == Block::kTransition && ghost->AdjustedLength() == 0) { - // Remove transitions that have been reduced to zero length - new NodeRemoveCommand(static_cast(b->parent()), - {b}, - command); - } else { - // Normal block placement + if (inserting) { + // If we're inserting, ripple everything at the destination with gaps + InsertGapsAtGhostDestination(parent()->ghost_items_, command); + } + + /* + QList tracks_affected; + */ + + // Now we can re-add each clip + for (int i=0;iGetAdjustedTrack())) { + tracks_affected.append(ghost->GetAdjustedTrack()); + } + */ + + if (duplicate_clips) { + // Duplicate rather than move + Node* copy = block->copy(); + + new NodeAddCommand(static_cast(block->parent()), + copy, + command); + + new NodeCopyInputsCommand(block, copy, true, command); + + // Place the copy instead of the original block + block = static_cast(copy); + /* + } else if (!tracks_affected.contains(ghost->Track())) { + // Block moved from its original position. Mark its track as affected. + tracks_affected.append(ghost->Track()); + */ + } + + const TrackReference& track_ref = ghost->GetAdjustedTrack(); new TrackPlaceBlockCommand(parent()->GetConnectedNode()->track_list(track_ref.type()), track_ref.index(), - b, + block, ghost->GetAdjustedIn(), command); } - } - if (command->childCount() > 0) { + /* foreach (const TrackReference& t, tracks_affected) { new TrackCleanGapsCommand(parent()->GetConnectedNode()->track_list(t.type()), t.index(), command); } + */ + + // FIXME: Heavy optimization since MOST of the timeline does NOT change in this time } Core::instance()->undo_stack()->pushIfHasChildren(command); } -rational TimelineWidget::PointerTool::FrameValidateInternal(rational time_movement, const QVector& ghosts) -{ - // Default behavior is to validate all movement and trimming - time_movement = ValidateFrameMovement(time_movement, ghosts); - time_movement = ValidateInTrimming(time_movement, ghosts, true); - time_movement = ValidateOutTrimming(time_movement, ghosts, true); - - return time_movement; -} - -void TimelineWidget::PointerTool::InitiateDrag(TimelineViewMouseEvent *mouse_pos) -{ - // Record where the drag started in timeline coordinates - drag_start_ = mouse_pos->GetCoordinates(); - - // Get the item that was clicked - TimelineViewBlockItem* clicked_item = GetItemAtScenePos(drag_start_); - - // We only initiate a pointer drag if the user actually dragged an item, otherwise if they dragged on empty space - // QGraphicsView default behavior would initiate a rubberband drag - if (clicked_item != nullptr) { - - // Clear snap points - snap_points_.clear(); - - // Record where the drag started in timeline coordinates - track_start_ = mouse_pos->GetTrack(); - - // Determine whether we're trimming or moving based on the position of the cursor - Timeline::MovementMode trim_mode = IsCursorInTrimHandle(clicked_item, mouse_pos->GetSceneX()); - - // Some derived classes don't allow movement - if (trim_mode == Timeline::kNone && movement_allowed_) { - trim_mode = Timeline::kMove; - } - - // Gaps can't be moved, only trimmed - if (clicked_item->block()->type() == Block::kGap && trim_mode == Timeline::kMove) { - trim_mode = Timeline::kNone; - } - - // Make sure we can actually perform an action here - if (trim_mode != Timeline::kNone) { - InitiateGhosts(clicked_item, trim_mode, false); - } - } -} - void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_pos) { - // Determine track movement - const TrackReference& cursor_track = mouse_pos.GetTrack(); - int track_movement = 0; - - if (track_movement_allowed_) { - track_movement = cursor_track.index() - track_start_.index(); - } + // Calculate track movement + int track_movement = track_movement_allowed_ + ? mouse_pos.GetTrack().index() - drag_start_.GetTrack().index() + : 0; // Determine frame movement rational time_movement = mouse_pos.GetFrame() - drag_start_.GetFrame(); @@ -369,17 +315,22 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po } // Validate movement (enforce all ghosts moving in legal ways) - time_movement = FrameValidateInternal(time_movement, parent()->ghost_items_); + // NOTE: Always do this after snapping to ensure the snap hasn't made an illegal movement. + time_movement = ValidateTimeMovement(time_movement, parent()->ghost_items_); + time_movement = ValidateInTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); + time_movement = ValidateOutTrimming(time_movement, parent()->ghost_items_, !trim_overwrite_allowed_); - // Validate ghosts that are being moved (clips from other track types do NOT get validated or moved) - QVector validate_track_ghosts = parent()->ghost_items_; - for (int i=0;iTrack().type() != drag_track_type_) { - validate_track_ghosts.removeAt(i); - i--; + // Validate ghosts that are being moved (clips from other track types do NOT get moved) + { + QVector validate_track_ghosts = parent()->ghost_items_; + for (int i=0;iTrack().type() != drag_track_type_) { + validate_track_ghosts.removeAt(i); + i--; + } } + track_movement = ValidateTrackMovement(track_movement, validate_track_ghosts); } - track_movement = ValidateTrackMovement(track_movement, validate_track_ghosts); // Perform movement foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { @@ -401,28 +352,23 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po // Also, we only move the clips on the same track type that the drag started from if (ghost->Track().type() == drag_track_type_) { ghost->SetTrackAdjustment(track_movement); - } - const TrackReference& track = ghost->GetAdjustedTrack(); - ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track)); + const TrackReference& track = ghost->GetAdjustedTrack(); + ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track)); + } break; } } } - // Show tooltip - // Generate tooltip (showing earliest in point of imported clip) - int64_t earliest_timestamp = Timecode::time_to_timestamp(time_movement, parent()->GetToolTipTimebase()); - QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, - parent()->GetToolTipTimebase(), - Core::instance()->GetTimecodeDisplay(), - true); - - // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way - // of the cursor) + // Regenerate tooltip and force it to update (otherwise the tooltip won't move as written in the + // documentation, and could get in the way of the cursor) QToolTip::hideText(); QToolTip::showText(QCursor::pos(), - tooltip_text, + Timecode::timestamp_to_timecode(Timecode::time_to_timestamp(time_movement, parent()->GetToolTipTimebase()), + parent()->GetToolTipTimebase(), + Core::instance()->GetTimecodeDisplay(), + true), parent()); } @@ -444,89 +390,61 @@ Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(Timelin } } -void TimelineWidget::PointerTool::InitiateGhosts(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode, - bool allow_gap_trimming) +void TimelineWidget::PointerTool::InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode, + bool allow_gap_trimming) { - // Convert selected items list to clips list + // Get list of selected blocks QList clips = parent()->GetSelectedBlocks(); - // If trimming multiple clips, we only trim the earliest in each track (trimming in) or the latest in each track - // (trimming out). If the current clip is NOT one of these, we only trim it. - bool multitrim_enabled = true; + if (trim_mode == Timeline::kMove) { - // Determine if the clicked item is the earliest/latest in the track for in/out trimming respectively - if (Timeline::IsATrimMode(trim_mode)) { - multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); - } + // Create ghosts for moving + foreach (TimelineViewBlockItem* clip_item, clips) { - // For each selected item, create a "ghost", a visual representation of the action before it gets performed - foreach (TimelineViewBlockItem* clip_item, clips) { + // Gaps are not allowed to move, so we ignore those here + if (clip_item->block()->type() == Block::kGap) { + continue; + } - // Determine correct mode for ghost - // - // Movement is indiscriminate, all the ghosts can be set to do this, however trimming is limited to one block - // PER TRACK - - bool include_this_clip = true; - - if (clip_item->block()->type() == Block::kGap - && !Timeline::IsATrimMode(trim_mode)) { - continue; + AddGhostFromBlock(clip_item->block(), clip_item->Track(), trim_mode); } - if (clip_item != clicked_item - && (Timeline::IsATrimMode(trim_mode))) { - include_this_clip = multitrim_enabled ? IsClipTrimmable(clip_item, clips, trim_mode) : false; - } + } else { + + // "Multi-trim" is trimming a clip on more than one track. Only the earliest (for in trimming) + // or latest (for out trimming) clip on each track can be trimmed. Therefore, it's only enabled + // if the clicked item is the earliest/latest on its track. + bool multitrim_enabled = IsClipTrimmable(clicked_item, clips, trim_mode); + + // Create ghosts for trimming + foreach (TimelineViewBlockItem* clip_item, clips) { + if (clip_item != clicked_item + && (!multitrim_enabled || !IsClipTrimmable(clip_item, clips, trim_mode))) { + // Either multitrim is disabled or this clip is NOT the earliest/latest in its track. We + // won't include it. + continue; + } - if (include_this_clip) { Block* block = clip_item->block(); Timeline::MovementMode block_mode = trim_mode; - // If we don't allow gap trimming, we automatically switch to the next/previous block to trim + // Some tools interpret "gap trimming" as equivalent to resizing the adjacent block. In that + // scenario, we include the adjacent block instead. if (block->type() == Block::kGap && !allow_gap_trimming) { - if (trim_mode == Timeline::kTrimIn) { - // Trim the previous clip's out point instead - block = block->previous(); - } else { - // Assume kTrimOut - block = block->next(); - } + block = (trim_mode == Timeline::kTrimIn) ? block->previous() : block->next(); block_mode = FlipTrimMode(trim_mode); - } - if (block) { - TimelineViewGhostItem* ghost = AddGhostFromBlock(block, clip_item->Track(), block_mode); - - if (block->type() == Block::kTransition) { - TransitionBlock* transition = static_cast(block); - - bool transition_can_move_tracks = false; - - // Create a rolling effect with the attached block - if (transition->connected_in_block()) { - if (parent()->block_items_.value(transition->connected_in_block())->isSelected()) { - // We'll be moving this item too, no need to create a ghost for it here - transition_can_move_tracks = true; - } else if (block_mode == Timeline::kTrimOut || block_mode == Timeline::kMove) { - AddGhostFromBlock(transition->connected_in_block(), clip_item->Track(), Timeline::kTrimIn); - } - } - - if (transition->connected_out_block()) { - if (parent()->block_items_.value(transition->connected_in_block())->isSelected()) { - // We'll be moving this item too, no need to create a ghost for it here - transition_can_move_tracks = true; - } else if (block_mode == Timeline::kTrimIn || block_mode == Timeline::kMove) { - AddGhostFromBlock(transition->connected_out_block(), clip_item->Track(), Timeline::kTrimOut); - } - } - - ghost->SetCanMoveTracks(transition_can_move_tracks); + // If there's no adjacent block, do nothing here + if (!block) { + continue; } } + + // Create ghost for this block + AddGhostFromBlock(block, clip_item->Track(), block_mode); } + } } diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index d6c3ff800..5c3efca98 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -29,74 +29,16 @@ TimelineWidget::RippleTool::RippleTool(TimelineWidget* parent) : PointerTool(parent) { SetMovementAllowed(false); + SetTrimOverwriteAllowed(true); } -void TimelineWidget::RippleTool::MouseReleaseInternal(TimelineViewMouseEvent *event) -{ - Q_UNUSED(event) - - // For ripple operations, all ghosts will be moving the same way - Timeline::MovementMode movement_mode = parent()->ghost_items_.first()->mode(); - - QUndoCommand* command = new QUndoCommand(); - - // Find earliest point to ripple around - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (b == nullptr) { - // This is a gap we are creating - - // Make sure there's actually a gap being created - if (ghost->AdjustedLength() > 0) { - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(ghost->AdjustedLength()); - new NodeAddCommand(static_cast(parent()->GetConnectedNode()->parent()), gap, command); - - Block* block_to_append_gap_to = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kReferenceBlock)); - - new TrackInsertBlockAfterCommand(parent()->GetTrackFromReference(ghost->Track()), - gap, - block_to_append_gap_to, - command); - } - } else { - // This was a Block that already existed - if (ghost->AdjustedLength() > 0) { - if (movement_mode == Timeline::kTrimIn) { - // We'll need to shift the media in point too - new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); - } else { - new BlockResizeCommand(b, ghost->AdjustedLength(), command); - } - } else { - // Assumed the Block was a Gap and it was reduced to zero length, remove it here - new TrackRippleRemoveBlockCommand(parent()->GetTrackFromReference(ghost->Track()), b, command); - - new NodeRemoveWithExclusiveDeps(static_cast(b->parent()), b, command); - } - } - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); -} - -rational TimelineWidget::RippleTool::FrameValidateInternal(rational time_movement, const QVector &ghosts) -{ - // Only validate trimming, and we don't care about "overwriting" since the ripple tool is nondestructive - time_movement = ValidateInTrimming(time_movement, ghosts, false); - time_movement = ValidateOutTrimming(time_movement, ghosts, false); - - return time_movement; -} - -void TimelineWidget::RippleTool::InitiateGhosts(TimelineViewBlockItem *clicked_item, - Timeline::MovementMode trim_mode, - bool allow_gap_trimming) +void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_item, + Timeline::MovementMode trim_mode, + bool allow_gap_trimming) { Q_UNUSED(allow_gap_trimming) - PointerTool::InitiateGhosts(clicked_item, trim_mode, true); + PointerTool::InitiateDrag(clicked_item, trim_mode, true); if (parent()->ghost_items_.isEmpty()) { return; @@ -155,4 +97,54 @@ void TimelineWidget::RippleTool::InitiateGhosts(TimelineViewBlockItem *clicked_i } } +void TimelineWidget::RippleTool::FinishDrag(TimelineViewMouseEvent *event) +{ + Q_UNUSED(event) + + // For ripple operations, all ghosts will be moving the same way + Timeline::MovementMode movement_mode = parent()->ghost_items_.first()->mode(); + + QUndoCommand* command = new QUndoCommand(); + + // Find earliest point to ripple around + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + if (b == nullptr) { + // This is a gap we are creating + + // Make sure there's actually a gap being created + if (ghost->AdjustedLength() > 0) { + GapBlock* gap = new GapBlock(); + gap->set_length_and_media_out(ghost->AdjustedLength()); + new NodeAddCommand(static_cast(parent()->GetConnectedNode()->parent()), gap, command); + + Block* block_to_append_gap_to = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kReferenceBlock)); + + new TrackInsertBlockAfterCommand(parent()->GetTrackFromReference(ghost->Track()), + gap, + block_to_append_gap_to, + command); + } + } else { + // This was a Block that already existed + if (ghost->AdjustedLength() > 0) { + if (movement_mode == Timeline::kTrimIn) { + // We'll need to shift the media in point too + new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); + } else { + new BlockResizeCommand(b, ghost->AdjustedLength(), command); + } + } else { + // Assumed the Block was a Gap and it was reduced to zero length, remove it here + new TrackRippleRemoveBlockCommand(parent()->GetTrackFromReference(ghost->Track()), b, command); + + new NodeRemoveWithExclusiveDeps(static_cast(b->parent()), b, command); + } + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/rolling.cpp b/app/widget/timelinewidget/tool/rolling.cpp index 38babb4e8..f70ba9f7d 100644 --- a/app/widget/timelinewidget/tool/rolling.cpp +++ b/app/widget/timelinewidget/tool/rolling.cpp @@ -29,77 +29,47 @@ TimelineWidget::RollingTool::RollingTool(TimelineWidget* parent) : PointerTool(parent) { SetMovementAllowed(false); + SetTrimOverwriteAllowed(true); } -void TimelineWidget::RollingTool::MouseReleaseInternal(TimelineViewMouseEvent *event) +void TimelineWidget::RollingTool::InitiateDrag(TimelineViewBlockItem *clicked_item, + Timeline::MovementMode trim_mode, + bool allow_gap_trimming) { - Q_UNUSED(event) + PointerTool::InitiateDrag(clicked_item, trim_mode, true); + // For each ghost, we make an equivalent Ghost on the next/previous block + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + Block* ghost_block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + if (ghost->mode() == Timeline::kTrimIn && ghost_block->previous()) { + // Add an extra Ghost for the previous block + AddGhostFromBlock(ghost_block->previous(), ghost->Track(), Timeline::kTrimOut); + } else if (ghost->mode() == Timeline::kTrimOut && ghost_block->next()) { + AddGhostFromBlock(ghost_block->next(), ghost->Track(), Timeline::kTrimIn); + } + } +} + +void TimelineWidget::RollingTool::FinishDrag(TimelineViewMouseEvent *event) +{ QUndoCommand* command = new QUndoCommand(); // Find earliest point to ripple around foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + if (ghost->mode() == drag_movement_mode()) { + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - if (ghost->mode() == Timeline::kTrimIn) { - if (b->previous() == nullptr) { - // We'll need to insert a gap here, so we'll do a Place command instead - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(ghost->Length()); - new NodeAddCommand(static_cast(b->parent()), - gap, - command); - - new TrackReplaceBlockCommand(parent()->GetTrackFromReference(ghost->Track()), b, gap, command); - } - - new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); - - if (b->previous() == nullptr) { - const TrackReference& track_ref = ghost->Track(); - - new TrackPlaceBlockCommand(parent()->GetConnectedNode()->track_list(track_ref.type()), - track_ref.index(), - b, - ghost->GetAdjustedIn(), - command); - } - } else if (ghost->mode() == Timeline::kTrimOut) { - new BlockResizeCommand(b, ghost->AdjustedLength(), command); + BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->Track()), + b, + ghost->AdjustedLength(), + drag_movement_mode(), + command); + c->SetAllowNonGapTrimming(true); } } Core::instance()->undo_stack()->pushIfHasChildren(command); } -rational TimelineWidget::RollingTool::FrameValidateInternal(rational time_movement, const QVector &ghosts) -{ - // Only validate trimming, and we don't care about "overwriting" since the rolling tool is designed to trim at collisions - time_movement = ValidateInTrimming(time_movement, ghosts, false); - time_movement = ValidateOutTrimming(time_movement, ghosts, false); - - return time_movement; -} - -void TimelineWidget::RollingTool::InitiateGhosts(TimelineViewBlockItem *clicked_item, - Timeline::MovementMode trim_mode, - bool allow_gap_trimming) -{ - Q_UNUSED(allow_gap_trimming) - - PointerTool::InitiateGhosts(clicked_item, trim_mode, true); - - // For each ghost, we make an equivalent Ghost on the next/previous block - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* ghost_block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (ghost->mode() == Timeline::kTrimIn && ghost_block->previous() != nullptr) { - // Add an extra Ghost for the previous block - AddGhostFromBlock(ghost_block->previous(), ghost->Track(), Timeline::kTrimOut); - } else if (ghost->mode() == Timeline::kTrimOut && ghost_block->next() != nullptr) { - AddGhostFromBlock(ghost_block->next(), ghost->Track(), Timeline::kTrimIn); - } - } -} - OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/slide.cpp b/app/widget/timelinewidget/tool/slide.cpp index beb60a4cf..3d53e3078 100644 --- a/app/widget/timelinewidget/tool/slide.cpp +++ b/app/widget/timelinewidget/tool/slide.cpp @@ -30,65 +30,107 @@ TimelineWidget::SlideTool::SlideTool(TimelineWidget* parent) : { SetTrimmingAllowed(false); SetTrackMovementAllowed(false); + SetTrimOverwriteAllowed(true); } -void TimelineWidget::SlideTool::MouseReleaseInternal(TimelineViewMouseEvent *event) -{ - Q_UNUSED(event) +struct TrackBlockListPair { + TrackReference track; + QList blocks; +}; - QUndoCommand* command = new QUndoCommand(); - - // Find earliest point to ripple around - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); - - if (ghost->mode() == Timeline::kTrimIn) { - new BlockResizeWithMediaInCommand(b, ghost->AdjustedLength(), command); - } else if (ghost->mode() == Timeline::kTrimOut) { - new BlockResizeCommand(b, ghost->AdjustedLength(), command); - } else if (ghost->mode() == Timeline::kMove && b->previous() == nullptr) { - GapBlock* gap = new GapBlock(); - gap->set_length_and_media_out(ghost->InAdjustment()); - new NodeAddCommand(static_cast(b->parent()), gap, command); - new TrackPrependBlockCommand(parent()->GetTrackFromReference(ghost->Track()), gap, command); - } - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); -} - -rational TimelineWidget::SlideTool::FrameValidateInternal(rational time_movement, const QVector &ghosts) -{ - // Only validate trimming, and we don't care about "overwriting" since the rolling tool is designed to trim at collisions - time_movement = ValidateInTrimming(time_movement, ghosts, false); - time_movement = ValidateOutTrimming(time_movement, ghosts, false); - - return time_movement; -} - -void TimelineWidget::SlideTool::InitiateGhosts(TimelineViewBlockItem *clicked_item, - Timeline::MovementMode trim_mode, - bool allow_gap_trimming) +void TimelineWidget::SlideTool::InitiateDrag(TimelineViewBlockItem *clicked_item, + Timeline::MovementMode trim_mode, + bool allow_gap_trimming) { Q_UNUSED(allow_gap_trimming) - PointerTool::InitiateGhosts(clicked_item, trim_mode, true); + PointerTool::InitiateDrag(clicked_item, trim_mode, true); - // For each ghost, we make an equivalent Ghost on the next/previous block + // Sort blocks into tracks + QList blocks_per_track; foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - Block* ghost_block = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + bool found = false; - // Add trimming ghosts for each side of the Block - - if (ghost_block->previous() != nullptr) { - // Add an extra Ghost for the previous block - AddGhostFromBlock(ghost_block->previous(), ghost->Track(), Timeline::kTrimOut); + for (int i=0;iTrack()) { + blocks_per_track[i].blocks.append(b); + found = true; + break; + } } - if (ghost_block->next() != nullptr) { - AddGhostFromBlock(ghost_block->next(), ghost->Track(), Timeline::kTrimIn); + if (!found) { + blocks_per_track.append({ghost->Track(), {b}}); } } + + // Make contiguous runs of blocks per each track + foreach (const TrackBlockListPair& p, blocks_per_track) { + // Blocks must be merged if any are non-adjacent + const TrackReference& track = p.track; + const QList& blocks = p.blocks; + + Block* earliest_block = blocks.first(); + Block* latest_block = blocks.first(); + + // Find the earliest and latest selected blocks + for (int j=1;jin() < earliest_block->in()) { + earliest_block = compare; + } + + if (compare->in() > latest_block->in()) { + latest_block = compare; + } + } + + // Add any blocks between these blocks that aren't already in the list + if (earliest_block != latest_block) { + Block* b = earliest_block; + while ((b = b->next()) != latest_block) { + if (!blocks.contains(b)) { + AddGhostFromBlock(b, track, Timeline::kMove); + } + } + } + + // Add surrounding blocks that will be trimming instead of moving + if (earliest_block->previous()) { + AddGhostFromBlock(earliest_block->previous(), track, Timeline::kTrimOut); + } + + if (latest_block->next()) { + AddGhostFromBlock(latest_block->next(), track, Timeline::kTrimIn); + } + } +} + +void TimelineWidget::SlideTool::FinishDrag(TimelineViewMouseEvent *event) +{ + Q_UNUSED(event) + + QVector info; + + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + if (!ghost->HasBeenAdjusted()) { + continue; + } + + Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + + info.append({parent()->GetTrackFromReference(ghost->Track()), + b, + ghost->mode(), + ghost->mode() == Timeline::kMove ? ghost->GetAdjustedIn() : ghost->AdjustedLength(), + ghost->mode() == Timeline::kMove ? ghost->In() : ghost->Length()}); + } + + if (!info.isEmpty()) { + Core::instance()->undo_stack()->push(new TrackSlideCommand(info)); + } } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/slip.cpp b/app/widget/timelinewidget/tool/slip.cpp index a548993e4..0208743be 100644 --- a/app/widget/timelinewidget/tool/slip.cpp +++ b/app/widget/timelinewidget/tool/slip.cpp @@ -51,22 +51,18 @@ void TimelineWidget::SlipTool::ProcessDrag(const TimelineCoordinate &mouse_pos) ghost->SetMediaInAdjustment(time_movement); } - // Show tooltip - // Generate tooltip (showing earliest in point of imported clip) - int64_t earliest_timestamp = Timecode::time_to_timestamp(time_movement, parent()->GetToolTipTimebase()); - QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, - parent()->GetToolTipTimebase(), - Core::instance()->GetTimecodeDisplay(), - true); - // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way - // of the cursor) + // Generate tooltip and force it to to update (otherwise the tooltip won't move as written in the + // documentation, and could get in the way of the cursor) QToolTip::hideText(); QToolTip::showText(QCursor::pos(), - tooltip_text, + Timecode::timestamp_to_timecode(Timecode::time_to_timestamp(time_movement, parent()->GetToolTipTimebase()), + parent()->GetToolTipTimebase(), + Core::instance()->GetTimecodeDisplay(), + true), parent()); } -void TimelineWidget::SlipTool::MouseReleaseInternal(TimelineViewMouseEvent *event) +void TimelineWidget::SlipTool::FinishDrag(TimelineViewMouseEvent *event) { Q_UNUSED(event) diff --git a/app/widget/timelinewidget/tool/tool.cpp b/app/widget/timelinewidget/tool/tool.cpp index 957b2dca9..e12b41139 100644 --- a/app/widget/timelinewidget/tool/tool.cpp +++ b/app/widget/timelinewidget/tool/tool.cpp @@ -98,7 +98,7 @@ void AttemptSnap(const QList& proposed_pts, } } -rational TimelineWidget::Tool::ValidateFrameMovement(rational movement, const QVector ghosts) +rational TimelineWidget::Tool::ValidateTimeMovement(rational movement, const QVector ghosts) { foreach (TimelineViewGhostItem* ghost, ghosts) { if (ghost->mode() != Timeline::kMove) { diff --git a/app/widget/timelinewidget/undo/undo.cpp b/app/widget/timelinewidget/undo/undo.cpp index 3aee9318c..45ba69b20 100644 --- a/app/widget/timelinewidget/undo/undo.cpp +++ b/app/widget/timelinewidget/undo/undo.cpp @@ -21,8 +21,9 @@ #include "undo.h" #include "core.h" -#include "node/graph.h" +#include "node/block/clip/clip.h" #include "node/block/transition/transition.h" +#include "node/graph.h" #include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER @@ -342,6 +343,11 @@ TrackPlaceBlockCommand::TrackPlaceBlockCommand(TrackList *timeline, int track, B insert_ = block; } +Project *TrackPlaceBlockCommand::GetRelevantProject() const +{ + return static_cast(static_cast(timeline_->parent())->parent())->project(); +} + void TrackPlaceBlockCommand::redo_internal() { added_track_count_ = 0; @@ -425,8 +431,7 @@ Project *BlockSplitCommand::GetRelevantProject() const void BlockSplitCommand::redo_internal() { - // FIXME: Reintroduce this optimization when block waveforms update automatically - // track_->BlockInvalidateCache(); + track_->BlockInvalidateCache(); static_cast(block_->parent())->AddNode(new_block_); Node::CopyInputs(block_, new_block_); @@ -444,12 +449,16 @@ void BlockSplitCommand::redo_internal() NodeParam::ConnectEdge(new_block_->output(), transition); } - // track_->UnblockInvalidateCache(); + if (block_->type() == Block::kClip) { + static_cast(new_block_)->set_waveform(static_cast(block_)->waveform().Cut(new_length_)); + } + + track_->UnblockInvalidateCache(); } void BlockSplitCommand::undo_internal() { - // track_->BlockInvalidateCache(); + track_->BlockInvalidateCache(); block_->set_length_and_media_out(old_length_); track_->RippleRemoveBlock(new_block_); @@ -461,7 +470,11 @@ void BlockSplitCommand::undo_internal() NodeParam::ConnectEdge(block_->output(), transition); } - // track_->UnblockInvalidateCache(); + if (block_->type() == Block::kClip) { + static_cast(block_)->waveform().Append(static_cast(new_block_)->waveform()); + } + + track_->UnblockInvalidateCache(); } Block *BlockSplitCommand::new_block() @@ -593,6 +606,7 @@ Project *BlockSplitPreservingLinksCommand::GetRelevantProject() const return static_cast(blocks_.first()->parent())->project(); } +/* TrackCleanGapsCommand::TrackCleanGapsCommand(TrackList *track_list, int index, QUndoCommand *parent) : UndoCommand(parent), track_list_(track_list), @@ -686,6 +700,7 @@ void TrackCleanGapsCommand::undo_internal() merged_gaps_.clear(); } +*/ BlockSetSpeedCommand::BlockSetSpeedCommand(Block *block, const rational &new_speed, QUndoCommand *parent) : UndoCommand(parent), @@ -923,4 +938,413 @@ void BlockEnableDisableCommand::undo_internal() block_->set_enabled(old_enabled_); } +BlockTrimCommand::BlockTrimCommand(TrackOutput* track, Block *block, rational new_length, Timeline::MovementMode mode, QUndoCommand *command) : + UndoCommand(command), + track_(track), + block_(block), + old_length_(block->length()), + new_length_(new_length), + mode_(mode), + adjacent_(nullptr), + we_created_adjacent_(false), + allow_nongap_trimming_(false) +{ +} + +Project *BlockTrimCommand::GetRelevantProject() const +{ + return static_cast(block_->parent())->project(); +} + +void BlockTrimCommand::redo_internal() +{ + track_->BlockInvalidateCache(); + + // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer + rational trim_diff = old_length_ - new_length_; + + TimeRange invalidate_range; + + if (mode_ == Timeline::kTrimIn) { + invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff); + block_->set_length_and_media_in(new_length_); + adjacent_ = block_->previous(); + } else { + invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff); + block_->set_length_and_media_out(new_length_); + adjacent_ = block_->next(); + } + + if (trim_diff > rational()) { + // If trimming SHORTER, we'll need to create/modify a gap + if (adjacent_ && (adjacent_->type() == Block::kGap || allow_nongap_trimming_)) { + + // A gap (or equivalent) exists, simply increase the size of it + if (mode_ == Timeline::kTrimIn) { + adjacent_->set_length_and_media_out(adjacent_->length() + trim_diff); + } else { + adjacent_->set_length_and_media_in(adjacent_->length() + trim_diff); + } + + } else { + + // Don't create a gap if the trim was at the end of the sequence (which would be indicated by + // the mode being "trim out" and "block_->next()" being null. + if (mode_ == Timeline::kTrimIn || block_->next()) { + // We must create a gap + we_created_adjacent_ = true; + + adjacent_ = new GapBlock(); + adjacent_->set_length_and_media_out(trim_diff); + static_cast(track_->parent())->AddNode(adjacent_); + + if (mode_ == Timeline::kTrimIn) { + track_->InsertBlockBefore(adjacent_, block_); + } else { + track_->InsertBlockAfter(adjacent_, block_); + } + } + + } + + if (block_->type() == Block::kClip) { + if (mode_ == Timeline::kTrimIn) { + static_cast(block_)->waveform().TrimIn(trim_diff); + } else { + static_cast(block_)->waveform().TrimOut(trim_diff); + } + } + } else { + if (adjacent_) { + // If trimming LONGER, we'll need to trim the adjacent + // (assume if there's no adjacent, we're at the end of the timeline and do nothing) + if (mode_ == Timeline::kTrimIn) { + adjacent_->set_length_and_media_out(adjacent_->length() + trim_diff); + } else { + adjacent_->set_length_and_media_in(adjacent_->length() + trim_diff); + } + } + + if (block_->type() == Block::kClip) { + if (mode_ == Timeline::kTrimIn) { + static_cast(block_)->waveform().PrependSilence(-trim_diff); + } else { + static_cast(block_)->waveform().AppendSilence(-trim_diff); + } + } + } + + track_->UnblockInvalidateCache(); + + track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); +} + +void BlockTrimCommand::undo_internal() +{ + track_->BlockInvalidateCache(); + + // Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer + rational trim_diff = old_length_ - new_length_; + + if (trim_diff > rational()) { + // If trimmed SHORTER, we need to unadjust the gap + if (we_created_adjacent_) { + // If we created a gap, just remove it straight up + track_->RippleRemoveBlock(adjacent_); + TakeNodeFromParentGraph(adjacent_); + delete adjacent_; + adjacent_ = nullptr; + we_created_adjacent_ = false; + } else if (adjacent_) { + // If we adjusted an existing gap, unadjust here + adjacent_->set_length_and_media_out(adjacent_->length() - trim_diff); + } + + if (block_->type() == Block::kClip) { + if (mode_ == Timeline::kTrimIn) { + static_cast(block_)->waveform().PrependSilence(trim_diff); + } else { + static_cast(block_)->waveform().AppendSilence(trim_diff); + } + } + } else { + if (adjacent_) { + // If trimmed LONGER, we adjusted an existing block + // (assume if there's no adjacent, we're at the end of the timeline and do nothing) + if (mode_ == Timeline::kTrimIn) { + adjacent_->set_length_and_media_out(adjacent_->length() - trim_diff); + } + } + + if (block_->type() == Block::kClip) { + if (mode_ == Timeline::kTrimIn) { + static_cast(block_)->waveform().TrimIn(-trim_diff); + } else { + static_cast(block_)->waveform().TrimOut(-trim_diff); + } + } + } + + TimeRange invalidate_range; + + if (mode_ == Timeline::kTrimIn) { + block_->set_length_and_media_in(old_length_); + invalidate_range = TimeRange(block_->in(), block_->in() + trim_diff); + } else { + block_->set_length_and_media_out(old_length_); + invalidate_range = TimeRange(block_->out(), block_->out() - trim_diff); + } + + track_->UnblockInvalidateCache(); + + track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); +} + +TrackReplaceBlockWithGapCommand::TrackReplaceBlockWithGapCommand(TrackOutput *track, Block *block, QUndoCommand *command) : + UndoCommand(command), + track_(track), + block_(block), + we_created_gap_(false), + gap_(nullptr), + merged_gap_(nullptr) +{ +} + +Project *TrackReplaceBlockWithGapCommand::GetRelevantProject() const +{ + return static_cast(block_->parent())->project(); +} + +void TrackReplaceBlockWithGapCommand::redo_internal() +{ + TimeRange invalidate_range; + + track_->BlockInvalidateCache(); + + // If the block has no next, it's at the end of the track and there's no need to create a gap + if (block_->next()) { + invalidate_range = TimeRange(block_->in(), block_->out()); + + rational new_gap_length = block_->length(); + + bool previous_is_a_gap = (block_->previous() && block_->previous()->type() == Block::kGap); + bool next_is_a_gap = (block_->next() && block_->next()->type() == Block::kGap); + + if (previous_is_a_gap) { + // Extend gap before this block + gap_ = static_cast(block_->previous()); + + // If the next is also a gap, we'll merge the two + if (next_is_a_gap) { + merged_gap_ = static_cast(block_->next()); + + new_gap_length += merged_gap_->length(); + track_->RippleRemoveBlock(merged_gap_); + TakeNodeFromParentGraph(merged_gap_, &memory_manager_); + } + } else if (next_is_a_gap) { + // Extend gap after this block + gap_ = static_cast(block_->next()); + } + + if (gap_) { + // Extend an existing gap + new_gap_length += gap_->length(); + gap_->set_length_and_media_out(new_gap_length); + track_->RippleRemoveBlock(block_); + } else { + // No gap exists, create one + gap_ = new GapBlock(); + gap_->set_length_and_media_out(new_gap_length); + static_cast(track_->parent())->AddNode(gap_); + track_->ReplaceBlock(block_, gap_); + we_created_gap_ = true; + } + + } else { + rational earliest_change = block_->in(); + + // Handle the gap being at the end where no gap is necessary + track_->RippleRemoveBlock(block_); + + // If there were also gaps leading up to this block, clean them up here + if (!track_->Blocks().isEmpty() && track_->Blocks().last()->type() == Block::kGap) { + merged_gap_ = static_cast(track_->Blocks().last()); + earliest_change = merged_gap_->in(); + track_->RippleRemoveBlock(merged_gap_); + TakeNodeFromParentGraph(merged_gap_, &memory_manager_); + } + + invalidate_range = TimeRange(earliest_change, RATIONAL_MAX); + } + + track_->UnblockInvalidateCache(); + + track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); +} + +void TrackReplaceBlockWithGapCommand::undo_internal() +{ + TimeRange invalidate_range; + + track_->BlockInvalidateCache(); + + if (gap_) { + + if (we_created_gap_) { + // We made this gap, simply swap our gap back + track_->ReplaceBlock(gap_, block_); + TakeNodeFromParentGraph(gap_); + delete gap_; + gap_ = nullptr; + } else { + // We must have extended an existing gap + rational original_gap_length = gap_->length() - block_->length(); + + // If we merged two gaps together, restore it now + if (merged_gap_) { + original_gap_length -= merged_gap_->length(); + static_cast(track_->parent())->AddNode(merged_gap_); + track_->InsertBlockAfter(merged_gap_, gap_); + } + + // Restore original block + track_->InsertBlockAfter(block_, gap_); + + // Restore gap's original length + gap_->set_length_and_media_out(original_gap_length); + } + + invalidate_range = TimeRange(block_->in(), block_->out()); + + } else { + + // If there's no `gap_`, we must have removed the block at the end + invalidate_range = TimeRange(track_->length(), RATIONAL_MAX); + + if (merged_gap_) { + static_cast(track_->parent())->AddNode(merged_gap_); + track_->AppendBlock(merged_gap_); + } + + track_->AppendBlock(block_); + + } + + merged_gap_ = nullptr; + + track_->UnblockInvalidateCache(); + + track_->InvalidateCache(invalidate_range, track_->block_input(), track_->block_input()); +} + +TrackSlideCommand::TrackSlideCommand(const QVector &blocks, QUndoCommand *parent) : + UndoCommand(parent), + blocks_(blocks) +{ +} + +Project *TrackSlideCommand::GetRelevantProject() const +{ + return static_cast(blocks_.first().track->parent())->project(); +} + +void TrackSlideCommand::redo_internal() +{ + slide_internal(false); +} + +void TrackSlideCommand::undo_internal() +{ + slide_internal(true); +} + +void TrackSlideCommand::slide_internal(bool undo) +{ + QMap invalidate_ranges; + + // Make sure all movement blocks' old positions are invalidated + foreach (const BlockSlideInfo& info, blocks_) { + if (info.mode == Timeline::kMove) { + invalidate_ranges[info.track].InsertTimeRange(TimeRange(info.block->in(), + info.block->out())); + } + } + + // Perform trims + foreach (const BlockSlideInfo& info, blocks_) { + info.track->BlockInvalidateCache(); + + if (info.mode == Timeline::kTrimIn || info.mode == Timeline::kTrimOut) { + rational new_len = undo ? info.old_time : info.new_time; + + if (info.block->type() == Block::kClip) { + AudioVisualWaveform& waveform = static_cast(info.block)->waveform(); + + if (new_len < info.block->length()) { + if (info.mode == Timeline::kTrimIn) { + waveform.TrimIn(info.block->length() - new_len); + } else { + waveform.TrimOut(info.block->length() - new_len); + } + } else { + if (info.mode == Timeline::kTrimIn) { + waveform.PrependSilence(new_len - info.block->length()); + } else { + waveform.AppendSilence(new_len - info.block->length()); + } + } + } + + if (info.mode == Timeline::kTrimIn) { + info.block->set_length_and_media_in(new_len); + } else { + info.block->set_length_and_media_out(new_len); + } + } else if (!undo && info.mode == Timeline::kMove && !info.block->previous()) { + // If this is a moving block and there was nothing before it to offset its time correctly, + // insert a gap here + GapBlock* gap = new GapBlock(); + gap->set_length_and_media_out(info.new_time); + static_cast(info.block->parent())->AddNode(gap); + info.track->PrependBlock(gap); + added_gaps_.append(gap); + } + + info.track->UnblockInvalidateCache(); + } + + if (undo) { + // If undoing, remove added gaps + foreach (GapBlock* gap, added_gaps_) { + TrackOutput* track = TrackOutput::TrackFromBlock(gap); + + track->BlockInvalidateCache(); + + track->RippleRemoveBlock(gap); + TakeNodeFromParentGraph(gap); + delete gap; + + track->UnblockInvalidateCache(); + } + + added_gaps_.clear(); + } + + // Make sure all movement blocks' new positions are invalidated + foreach (const BlockSlideInfo& info, blocks_) { + if (info.mode == Timeline::kMove) { + invalidate_ranges[info.track].InsertTimeRange(TimeRange(info.block->in(), + info.block->out())); + } + } + + QMap::const_iterator i; + for (i=invalidate_ranges.constBegin(); i!=invalidate_ranges.constEnd(); i++) { + foreach (const TimeRange& r, i.value()) { + i.key()->InvalidateCache(r, i.key()->block_input(), i.key()->block_input()); + } + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/undo/undo.h b/app/widget/timelinewidget/undo/undo.h index d763d007c..8d6957ca5 100644 --- a/app/widget/timelinewidget/undo/undo.h +++ b/app/widget/timelinewidget/undo/undo.h @@ -64,6 +64,35 @@ private: rational new_length_; }; +class BlockTrimCommand : public UndoCommand { +public: + BlockTrimCommand(TrackOutput *track, Block* block, rational new_length, Timeline::MovementMode mode, QUndoCommand* command = nullptr); + + virtual Project* GetRelevantProject() const override; + + void SetAllowNonGapTrimming(bool e) + { + allow_nongap_trimming_ = e; + } + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + TrackOutput* track_; + Block* block_; + rational old_length_; + rational new_length_; + Timeline::MovementMode mode_; + + Block* adjacent_; + bool we_created_adjacent_; + + bool allow_nongap_trimming_; + +}; + class BlockSetMediaInCommand : public UndoCommand { public: BlockSetMediaInCommand(Block* block, rational new_media_in, QUndoCommand* parent = nullptr); @@ -168,6 +197,8 @@ protected: virtual void undo_internal() override; protected: + Project* project_; + TrackOutput* track_; rational in_; rational out_; @@ -203,6 +234,8 @@ class TrackPlaceBlockCommand : public TrackRippleRemoveAreaCommand { public: TrackPlaceBlockCommand(TrackList *timeline, int track, Block* block, rational in, QUndoCommand* parent = nullptr); + virtual Project* GetRelevantProject() const override; + protected: virtual void redo_internal() override; virtual void undo_internal() override; @@ -287,6 +320,29 @@ private: Block* replace_; }; +class TrackReplaceBlockWithGapCommand : public UndoCommand { +public: + TrackReplaceBlockWithGapCommand(TrackOutput* track, Block* block, QUndoCommand* command = nullptr); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + TrackOutput* track_; + Block* block_; + + bool we_created_gap_; + GapBlock* gap_; + GapBlock* merged_gap_; + + QObject memory_manager_; + +}; + +/* class TrackCleanGapsCommand : public UndoCommand { public: TrackCleanGapsCommand(TrackList* track_list, int index, QUndoCommand* parent = nullptr); @@ -315,6 +371,7 @@ private: QList removed_end_gaps_; }; +*/ class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand { public: @@ -444,6 +501,32 @@ private: }; +class TrackSlideCommand : public UndoCommand { +public: + struct BlockSlideInfo { + TrackOutput* track; + Block* block; + Timeline::MovementMode mode; + rational new_time; + rational old_time; + }; + + TrackSlideCommand(const QVector& blocks, QUndoCommand* parent = nullptr); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo_internal() override; + virtual void undo_internal() override; + +private: + void slide_internal(bool undo); + + QVector blocks_; + QList added_gaps_; + +}; + OLIVE_NAMESPACE_EXIT #endif // TIMELINEUNDOABLE_H diff --git a/app/widget/timelinewidget/view/timelineviewblockitem.cpp b/app/widget/timelinewidget/view/timelineviewblockitem.cpp index 1b63d0d3e..48df43b20 100644 --- a/app/widget/timelinewidget/view/timelineviewblockitem.cpp +++ b/app/widget/timelinewidget/view/timelineviewblockitem.cpp @@ -96,29 +96,11 @@ void TimelineViewBlockItem::paint(QPainter *painter, const QStyleOptionGraphicsI } // Draw waveform if one is available - QString wave_fn = QDir(QDir(Config::Current()["DiskCachePath"].toString()).filePath("waveform")).filePath(QString::number(reinterpret_cast(block_))); - QFile wave_file(wave_fn); - if (wave_file.open(QFile::ReadOnly)){ - painter->setPen(QColor(64, 64, 64)); - - QByteArray w = wave_file.readAll(); - - wave_file.close(); - - // Read metadata - SampleSummer::Info info; - memcpy(&info, w.data(), sizeof(SampleSummer::Info)); - - // Prevent divide by zero - if (info.channels) { - AudioWaveformView::DrawWaveform(painter, - rect().toRect(), - this->GetScale(), - reinterpret_cast(w.constData() + sizeof(SampleSummer::Info)), - (w.size() - sizeof(SampleSummer::Info)) / sizeof(SampleSummer::Sum), - info.channels); - } - } + painter->setPen(QColor(64, 64, 64)); + AudioVisualWaveform::DrawWaveform(painter, + rect().toRect(), + this->GetScale(), + static_cast(block_)->waveform()); painter->setPen(Qt::white); painter->drawLine(rect().topLeft(), QPointF(rect().right(), rect().top())); diff --git a/app/widget/timelinewidget/view/timelineviewghostitem.h b/app/widget/timelinewidget/view/timelineviewghostitem.h index 06f8018b7..373562900 100644 --- a/app/widget/timelinewidget/view/timelineviewghostitem.h +++ b/app/widget/timelinewidget/view/timelineviewghostitem.h @@ -23,8 +23,8 @@ #include -#include "common/timelinecommon.h" #include "project/item/footage/footage.h" +#include "timeline/timelinecommon.h" #include "timelineviewblockitem.h" #include "timelineviewrect.h" diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 3e139189e..21dbb9f70 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -58,67 +58,6 @@ void AudioWaveformView::SetViewer(AudioPlaybackCache *playback) ForceUpdate(); } -void AudioWaveformView::DrawWaveform(QPainter *painter, const QRect& rect, const double& scale, const SampleSummer::Sum* samples, int nb_samples, int channels) -{ - int sample_index, next_sample_index = 0; - - QVector summary; - int summary_index = -1; - - int channel_height = rect.height() / channels; - int channel_half_height = channel_height / 2; - - const QRect& viewport = painter->viewport(); - QPoint top_left = painter->transform().map(viewport.topLeft()); - - int start = qMax(rect.x(), -top_left.x()); - int end = qMin(rect.right(), -top_left.x() + viewport.width()); - - QVector lines; - - for (int i=start;i(SampleSummer::kSumSampleRate) * static_cast(i - rect.x() + 1) / scale) * channels); - - if (summary_index != sample_index) { - summary = SampleSummer::ReSumSamples(&samples[sample_index], - qMax(channels, next_sample_index - sample_index), - channels); - summary_index = sample_index; - } - - int line_x = i; - - for (int j=0;jdrawLines(lines); -} - void AudioWaveformView::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); @@ -147,15 +86,11 @@ void AudioWaveformView::paintEvent(QPaintEvent *event) // FIXME: Hardcoded color wave_painter.setPen(QColor(64, 255, 160)); - int channel_height = height() / params.channel_count(); - int channel_half_height = channel_height / 2; - int drew = 0; fs.seek(params.samples_to_bytes(ScreenToUnitRounded(0))); for (int x=0; x samples = SampleSummer::SumSamples(reinterpret_cast(read_buffer.constData()), - samples_len, - params.channel_count()); + QVector samples = AudioVisualWaveform::SumSamples(reinterpret_cast(read_buffer.constData()), + samples_len, + params.channel_count()); for (int i=0;i(channel_half_height)), - x, - channel_mid + qRound(samples.at(i).max * static_cast(channel_half_height))); - } + AudioVisualWaveform::DrawSample(&wave_painter, samples, x, 0, height()); drew++; } diff --git a/app/widget/viewer/audiowaveformview.h b/app/widget/viewer/audiowaveformview.h index e43999817..13540d394 100644 --- a/app/widget/viewer/audiowaveformview.h +++ b/app/widget/viewer/audiowaveformview.h @@ -23,7 +23,7 @@ #include -#include "audio/sumsamples.h" +#include "audio/audiovisualwaveform.h" #include "render/audioparams.h" #include "render/audioplaybackcache.h" #include "widget/timeruler/seekablewidget.h" @@ -40,8 +40,6 @@ public: void SetViewer(AudioPlaybackCache *playback); - static void DrawWaveform(QPainter* painter, const QRect &rect, const double &scale, const SampleSummer::Sum *samples, int nb_samples, int channels); - protected: virtual void paintEvent(QPaintEvent* event) override;