diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a1f1f0ee..f68c7c77a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,17 +12,16 @@ on: - 'README.md' env: - DOWNLOAD_TOOL: curl -fLOSs --retry 3 + DOWNLOAD_TOOL: curl -fLOSs --retry 2 --retry-delay 60 + UPLOAD_TOOL: curl -X POST --retry 2 --retry-delay 60 jobs: linux: strategy: fail-fast: false matrix: - build: [1, 2] include: - - build: 1 - build-type: RelWithDebInfo + - build-type: RelWithDebInfo cc-compiler: gcc cxx-compiler: g++ compiler-name: GCC 9.3.1 @@ -30,8 +29,7 @@ jobs: os-name: Linux (CentOS 7) vfx-cy: 2021 ci-common-version: 2 - - build: 2 - build-type: RelWithDebInfo + - build-type: RelWithDebInfo cc-compiler: clang cxx-compiler: clang++ compiler-name: Clang 10.0.0 @@ -49,6 +47,8 @@ jobs: runs-on: ubuntu-latest container: image: olivevideoeditor/ci-olive:${{ matrix.vfx-cy }}.${{ matrix.ci-common-version }} + if: >- + !contains(github.event.head_commit.message, '[skip ci]') steps: - name: Checkout Source Code @@ -105,7 +105,7 @@ jobs: PKGNAME: ${{ steps.package.outputs.pkgname }} if: github.event_name == 'push' && matrix.cc-compiler == 'gcc' run: | - curl -X POST \ + $UPLOAD_TOOL \ -F file="@$PKGNAME" \ -F key="$GH_AUTH_KEY" \ -F hash="$GITHUB_SHA" \ @@ -116,10 +116,8 @@ jobs: windows: strategy: matrix: - build: [1] include: - - build: 1 - build-type: RelWithDebInfo + - build-type: RelWithDebInfo compiler-name: MSVC 16.x os-name: Windows os-arch: x86_64 @@ -133,6 +131,8 @@ jobs: ${{ matrix.build-type }}, ${{ matrix.cmake-gen }}> runs-on: ${{ matrix.os }} + if: >- + !contains(github.event.head_commit.message, '[skip ci]') steps: - name: Checkout Source Code @@ -144,17 +144,17 @@ jobs: PLATFORM: ${{ matrix.os-name }} ARCH: ${{ matrix.os-arch }} run: | - echo "::set-env name=PKGNAME::$(echo Olive-${GITHUB_SHA::8}-${PLATFORM}-${ARCH})" + echo "PKGNAME=$(echo Olive-${GITHUB_SHA::8}-${PLATFORM}-${ARCH})" >> $GITHUB_ENV - name: Create Build Folder run: | cmake -E make_directory ${{ runner.workspace }}/build - name: Enable Developer Command Prompt (Windows) - uses: ilammy/msvc-dev-cmd@v1.3.0 + uses: ilammy/msvc-dev-cmd@v1 - name: Acquire Qt - uses: jurplel/install-qt-action@v2.8.0 + uses: jurplel/install-qt-action@v2 with: version: 5.15.1 @@ -254,7 +254,7 @@ jobs: GH_AUTH_KEY: ${{ secrets.GH_AUTH_KEY }} if: github.event_name == 'push' run: | - curl -X POST \ + $UPLOAD_TOOL \ -F file=@$PKGNAME.exe \ -F key="$GH_AUTH_KEY" \ -F hash="$GITHUB_SHA" \ @@ -262,7 +262,7 @@ jobs: -F comment="$(git -C $GITHUB_WORKSPACE show -s --format=%B)" \ https://ci.olivevideoeditor.org/push.php - curl -X POST \ + $UPLOAD_TOOL \ -F file=@$PKGNAME.zip \ -F key="$GH_AUTH_KEY" \ -F hash="$GITHUB_SHA" \ @@ -276,7 +276,8 @@ jobs: env: GH_AUTH_KEY: ${{ secrets.GH_AUTH_KEY }} run: | - curl -fLSs --retry 3 https://github.com/google/breakpad/blob/master/src/tools/windows/binaries/dump_syms.exe?raw=true > dump_syms.exe + curl -fLSs --retry 2 --retry-delay 60 \ + https://github.com/google/breakpad/blob/master/src/tools/windows/binaries/dump_syms.exe?raw=true > dump_syms.exe ./dump_syms app/olive-editor.pdb > olive-editor.sym curl -X POST \ -F symfile=@olive-editor.sym \ @@ -286,10 +287,8 @@ jobs: macos: strategy: matrix: - build: [1] include: - - build: 1 - build-type: RelWithDebInfo + - build-type: RelWithDebInfo compiler-name: Clang LLVM os-name: macOS os-arch: x86_64 @@ -303,6 +302,8 @@ jobs: ${{ matrix.build-type }}, ${{ matrix.cmake-gen }}> runs-on: ${{ matrix.os }} + if: >- + !contains(github.event.head_commit.message, '[skip ci]') steps: - name: Checkout Source Code @@ -314,14 +315,14 @@ jobs: PLATFORM: ${{ matrix.os-name }} ARCH: ${{ matrix.os-arch }} run: | - echo "::set-env name=PKGNAME::$(echo Olive-${GITHUB_SHA::8}-${PLATFORM}-${ARCH})" + echo "PKGNAME=$(echo Olive-${GITHUB_SHA::8}-${PLATFORM}-${ARCH})" >> $GITHUB_ENV - name: Create Build Folder run: | cmake -E make_directory ${{ runner.workspace }}/build - name: Acquire Qt - uses: jurplel/install-qt-action@v2.8.0 + uses: jurplel/install-qt-action@v2 with: version: 5.15.1 @@ -407,7 +408,7 @@ jobs: GH_AUTH_KEY: ${{ secrets.GH_AUTH_KEY }} if: github.event_name == 'push' run: | - curl -X POST \ + $UPLOAD_TOOL \ -F file=@$PKGNAME.zip \ -F key="$GH_AUTH_KEY" \ -F hash="$GITHUB_SHA" \ diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index e0e98d7f9..a5177e705 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -185,6 +185,10 @@ if (OpenTimelineIO_FOUND) ) endif() +if(UNIX AND NOT APPLE) + install(TARGETS ${OLIVE_TARGET} RUNTIME DESTINATION bin) +endif() + # Enable Crashpad if found if (GoogleCrashpad_FOUND) set(OLIVE_DEFINITIONS ${OLIVE_DEFINITIONS} USE_CRASHPAD) @@ -242,7 +246,7 @@ if (GoogleCrashpad_FOUND) set(MINIDUMP_STACKWALK "minidump_stackwalk${CMAKE_EXECUTABLE_SUFFIX}") if(UNIX AND NOT APPLE) - install(TARGETS ${OLIVE_TARGET} ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin) + install(TARGETS ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin) install(PROGRAMS ${CRASHPAD_LIBRARY_DIRS}/${CRASHPAD_HANDLER} DESTINATION bin) install(PROGRAMS ${BREAKPAD_BIN_DIR}/${MINIDUMP_STACKWALK} DESTINATION bin) endif() diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 832ad2032..a0afa7cce 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -77,26 +77,33 @@ bool AudioManager::IsRefreshingInputs() void AudioManager::PushToOutput(const QByteArray &samples) { - output_manager_.Push(samples); + output_manager_->Push(samples); emit OutputPushed(samples); } -void AudioManager::StartOutput(const QString &filename, qint64 offset, int playback_speed) +void AudioManager::StartOutput(AudioPlaybackCache *cache, qint64 offset, int playback_speed) { - QMetaObject::invokeMethod(&output_manager_, + // Create device + QIODevice* device = cache->CreatePlaybackDevice(); + + // Move to output manager's thread + device->moveToThread(&output_thread_); + + // Queue to output manger in other thread + QMetaObject::invokeMethod(output_manager_, "PullFromDevice", Qt::QueuedConnection, - Q_ARG(const QString&, filename), + Q_ARG(QIODevice*, device), Q_ARG(qint64, offset), Q_ARG(int, playback_speed)); - emit OutputDeviceStarted(filename, offset, playback_speed); + emit OutputDeviceStarted(cache, offset, playback_speed); } void AudioManager::StopOutput() { - QMetaObject::invokeMethod(&output_manager_, + QMetaObject::invokeMethod(output_manager_, "ResetToPushMode", Qt::QueuedConnection); @@ -149,7 +156,7 @@ void AudioManager::SetOutputDevice(const QAudioDeviceInfo &info) } if (info.isFormatSupported(format)) { - QMetaObject::invokeMethod(&output_manager_, + QMetaObject::invokeMethod(output_manager_, "SetOutputDevice", Qt::QueuedConnection, Q_ARG(const QAudioDeviceInfo&, info), @@ -166,7 +173,7 @@ void AudioManager::SetOutputParams(const AudioParams ¶ms) if (output_params_ != params) { output_params_ = params; - QMetaObject::invokeMethod(&output_manager_, + QMetaObject::invokeMethod(output_manager_, "SetParameters", Qt::QueuedConnection, OLIVE_NS_ARG(AudioParams, params)); @@ -222,14 +229,15 @@ AudioManager::AudioManager() : RefreshDevices(); output_thread_.start(QThread::TimeCriticalPriority); - output_manager_.moveToThread(&output_thread_); + output_manager_ = new AudioOutputManager(); + output_manager_->moveToThread(&output_thread_); - connect(&output_manager_, &AudioOutputManager::OutputNotified, this, &AudioManager::OutputNotified); + connect(output_manager_, &AudioOutputManager::OutputNotified, this, &AudioManager::OutputNotified); } AudioManager::~AudioManager() { - QMetaObject::invokeMethod(&output_manager_, "Close", Qt::QueuedConnection); + QMetaObject::invokeMethod(output_manager_, "deleteLater", Qt::BlockingQueuedConnection); output_thread_.quit(); output_thread_.wait(); } diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index f0c6c7db0..d351e6852 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -30,6 +30,7 @@ #include "common/define.h" #include "outputmanager.h" #include "render/audioparams.h" +#include "render/audioplaybackcache.h" OLIVE_NAMESPACE_ENTER @@ -57,11 +58,9 @@ public: void PushToOutput(const QByteArray& samples); /** - * @brief Start playing audio from QIODevice - * - * This takes ownership of the QIODevice and will delete it when StopOutput() is called + * @brief Start playing audio from AudioPlaybackCache */ - void StartOutput(const QString& filename, qint64 offset, int playback_speed); + void StartOutput(AudioPlaybackCache* cache, qint64 offset, int playback_speed); /** * @brief Stop audio output immediately @@ -86,7 +85,7 @@ signals: void OutputNotified(); - void OutputDeviceStarted(const QString& filename, qint64 offset, int playback_speed); + void OutputDeviceStarted(AudioPlaybackCache* cache, qint64 offset, int playback_speed); void AudioParamsChanged(const AudioParams& params); @@ -108,7 +107,7 @@ private: static AudioManager* instance_; QThread output_thread_; - AudioOutputManager output_manager_; + AudioOutputManager* output_manager_; bool output_is_set_; QAudioDeviceInfo output_device_info_; diff --git a/app/audio/outputdeviceproxy.cpp b/app/audio/outputdeviceproxy.cpp index e9732e93d..112ee5c53 100644 --- a/app/audio/outputdeviceproxy.cpp +++ b/app/audio/outputdeviceproxy.cpp @@ -24,11 +24,10 @@ OLIVE_NAMESPACE_ENTER -AudioOutputDeviceProxy::~AudioOutputDeviceProxy() +AudioOutputDeviceProxy::AudioOutputDeviceProxy(QObject *parent) : + QIODevice(parent), + device_(nullptr) { - if (file_.isOpen()) { - file_.close(); - } } void AudioOutputDeviceProxy::SetParameters(const AudioParams ¶ms) @@ -36,20 +35,23 @@ void AudioOutputDeviceProxy::SetParameters(const AudioParams ¶ms) params_ = params; } -void AudioOutputDeviceProxy::SetDevice(const QString &filename, qint64 offset, int playback_speed) +void AudioOutputDeviceProxy::SetDevice(QIODevice* device, qint64 offset, int playback_speed) { - if (file_.isOpen()) { - file_.close(); + if (device_) { + delete device_; } - file_.setFileName(filename); + device_ = device; + device_->setParent(this); - if (!file_.open(QFile::ReadOnly)) { - qCritical() << "Failed to open" << filename << "for audio playback"; + if (!device_->open(QFile::ReadOnly)) { + qCritical() << "Failed to open IO device for audio playback"; + delete device_; + device_ = nullptr; return; } - file_.seek(offset); + device_->seek(offset); playback_speed_ = playback_speed; @@ -62,7 +64,8 @@ void AudioOutputDeviceProxy::close() { QIODevice::close(); - file_.close(); + delete device_; + device_ = nullptr; if (tempo_processor_.IsOpen()) { tempo_processor_.Close(); @@ -71,7 +74,7 @@ void AudioOutputDeviceProxy::close() qint64 AudioOutputDeviceProxy::readData(char *data, qint64 maxlen) { - if (!file_.isOpen()) { + if (!device_) { return 0; } @@ -111,21 +114,21 @@ qint64 AudioOutputDeviceProxy::ReverseAwareRead(char *data, qint64 maxlen) if (playback_speed_ < 0) { // If we're reversing, we'll seek back by maxlen bytes before we read - new_pos = file_.pos() - maxlen; + new_pos = device_->pos() - maxlen; if (new_pos < 0) { - maxlen = file_.pos(); + maxlen = device_->pos(); new_pos = 0; } - file_.seek(new_pos); + device_->seek(new_pos); } - qint64 read_count = file_.read(data, maxlen); + qint64 read_count = device_->read(data, maxlen); if (playback_speed_ < 0) { - file_.seek(new_pos); + device_->seek(new_pos); // Reverse the samples here AudioManager::ReverseBuffer(data, static_cast(read_count), params_.samples_to_bytes(1)); diff --git a/app/audio/outputdeviceproxy.h b/app/audio/outputdeviceproxy.h index 44dc50562..60ef8cf3c 100644 --- a/app/audio/outputdeviceproxy.h +++ b/app/audio/outputdeviceproxy.h @@ -35,13 +35,11 @@ class AudioOutputDeviceProxy : public QIODevice { Q_OBJECT public: - AudioOutputDeviceProxy() = default; - - virtual ~AudioOutputDeviceProxy() override; + AudioOutputDeviceProxy(QObject* parent = nullptr); void SetParameters(const AudioParams& params); - void SetDevice(const QString &filename, qint64 offset, int playback_speed); + void SetDevice(QIODevice *device, qint64 offset, int playback_speed); virtual void close() override; @@ -53,7 +51,7 @@ protected: private: qint64 ReverseAwareRead(char* data, qint64 maxlen); - QFile file_; + QIODevice* device_; TempoProcessor tempo_processor_; diff --git a/app/audio/outputmanager.cpp b/app/audio/outputmanager.cpp index 2248ccd26..8c4a059fc 100644 --- a/app/audio/outputmanager.cpp +++ b/app/audio/outputmanager.cpp @@ -30,7 +30,8 @@ OLIVE_NAMESPACE_ENTER AudioOutputManager::AudioOutputManager(QObject *parent) : QObject(parent), output_(nullptr), - push_device_(nullptr) + push_device_(nullptr), + device_proxy_(this) { } @@ -82,12 +83,12 @@ void AudioOutputManager::Close() device_proxy_.close(); } - output_->deleteLater(); + delete output_; output_ = nullptr; } } -void AudioOutputManager::PullFromDevice(const QString &filename, qint64 offset, int playback_speed) +void AudioOutputManager::PullFromDevice(QIODevice *device, qint64 offset, int playback_speed) { if (!output_) { return; @@ -99,7 +100,7 @@ void AudioOutputManager::PullFromDevice(const QString &filename, qint64 offset, push_samples_.clear(); // Pull from the device - device_proxy_.SetDevice(filename, offset, playback_speed); + device_proxy_.SetDevice(device, offset, playback_speed); device_proxy_.open(QIODevice::ReadOnly); output_->start(&device_proxy_); } diff --git a/app/audio/outputmanager.h b/app/audio/outputmanager.h index 896cb2e74..994b4aff7 100644 --- a/app/audio/outputmanager.h +++ b/app/audio/outputmanager.h @@ -53,7 +53,7 @@ public slots: * This will clear any pushed samples or QIODevices currently being read and will start reading from this next time * the audio output requests data. */ - void PullFromDevice(const QString &filename, qint64 offset, int playback_speed); + void PullFromDevice(QIODevice* device, qint64 offset, int playback_speed); // Queued void ResetToPushMode(); diff --git a/app/codec/encoder.cpp b/app/codec/encoder.cpp index ea3750720..0f648884e 100644 --- a/app/codec/encoder.cpp +++ b/app/codec/encoder.cpp @@ -20,6 +20,8 @@ #include "encoder.h" +#include + #include "ffmpeg/ffmpegencoder.h" OLIVE_NAMESPACE_ENTER @@ -34,6 +36,12 @@ const EncodingParams &Encoder::params() const return params_; } +void Encoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename) +{ + QFile f(pcm_filename); + WriteAudio(pcm_info, &f); +} + EncodingParams::EncodingParams() : video_enabled_(false), video_bit_rate_(0), diff --git a/app/codec/encoder.h b/app/codec/encoder.h index 412306388..4acf38bde 100644 --- a/app/codec/encoder.h +++ b/app/codec/encoder.h @@ -116,7 +116,9 @@ public: virtual bool WriteFrame(OLIVE_NAMESPACE::FramePtr frame, OLIVE_NAMESPACE::rational time) = 0; virtual void WriteAudio(OLIVE_NAMESPACE::AudioParams pcm_info, - const QString& pcm_filename) = 0; + QIODevice *file) = 0; + void WriteAudio(OLIVE_NAMESPACE::AudioParams pcm_info, + const QString& pcm_filename); virtual void Close() = 0; diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 7a388cd8e..3da0a700b 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -107,9 +107,11 @@ bool FFmpegDecoder::Open() FFmpegFramePoolValue& frame_pool = frame_pool_map_[key]; if (!frame_pool.pool) { - // FIXME: Hardcoded value. It seems to work fine, but is there a possibility we should make - // this a dynamic value somehow or a configurable value? - frame_pool.pool = new FFmpegFramePool(32); + // Frames are allocated as threads * threads, to scale from each thread sharing one set + // to all of them working individually + int thread_count = QThread::idealThreadCount(); + int max_memory_frame_count = thread_count * thread_count; + frame_pool.pool = new FFmpegFramePool(max_memory_frame_count); } frame_pool.handles++; @@ -574,6 +576,25 @@ ItemPtr FFmpegDecoder::Probe(const QString& filename, const QAtomicInt* cancelle audio_stream->set_channels(avstream->codecpar->channels); audio_stream->set_sample_rate(avstream->codecpar->sample_rate); + if (avstream->duration == AV_NOPTS_VALUE) { + // Loop through stream until we get the whole duration + FFmpegDecoderInstance instance(filename, i); + + AVPacket* pkt = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + + int64_t new_dur; + + do { + new_dur = frame->pts; + } while (instance.GetFrame(pkt, frame) >= 0); + + avstream->duration = new_dur; + + av_frame_free(&frame); + av_packet_free(&pkt); + } + str = audio_stream; } else { @@ -1087,6 +1108,9 @@ FFmpegFramePool::ElementPtr FFmpegDecoderInstance::RetrieveFrame(const int64_t& break; } + // Cut down to thread count - 1 before we acquire a new frame + TruncateCacheRangeToFrames(QThread::idealThreadCount() -1); + FFmpegFramePool::ElementPtr cached = frame_pool_->Get(); if (!cached) { @@ -1127,14 +1151,6 @@ FFmpegFramePool::ElementPtr FFmpegDecoderInstance::RetrieveFrame(const int64_t& previous = cached_frames_.last(); } - // Clear early frames - int removed = TruncateCacheRangeTo(second_ts_); - - if (removed == 0 && MemoryPoolLimitReached()) { - // Relinquish a frame if we have to conserve memory - RemoveFirstFrame(); - } - // Append this frame and signal to other threads that a new frame has arrived cached_frames_.append(cached); @@ -1322,7 +1338,7 @@ void FFmpegDecoderInstance::RemoveFramesBefore(const qint64 &t) } } -int FFmpegDecoderInstance::TruncateCacheRangeTo(const qint64 &t) +int FFmpegDecoderInstance::TruncateCacheRangeToTime(const qint64 &t) { int counter = 0; @@ -1335,6 +1351,19 @@ int FFmpegDecoderInstance::TruncateCacheRangeTo(const qint64 &t) return counter; } +int FFmpegDecoderInstance::TruncateCacheRangeToFrames(int nb_frames) +{ + int counter = 0; + + // We keep one frame in memory as an identifier for what pts the decoder is up to + while (cached_frames_.size() > nb_frames) { + RemoveFirstFrame(); + counter++; + } + + return counter; +} + void FFmpegDecoderInstance::RemoveFirstFrame() { cached_frames_.removeFirst(); diff --git a/app/codec/ffmpeg/ffmpegdecoder.h b/app/codec/ffmpeg/ffmpegdecoder.h index 983b686c0..05cbee75e 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.h +++ b/app/codec/ffmpeg/ffmpegdecoder.h @@ -62,7 +62,8 @@ public: FFmpegFramePool::ElementPtr GetFrameFromCache(const int64_t& t) const; void RemoveFramesBefore(const qint64& t); - int TruncateCacheRangeTo(const qint64& t); + int TruncateCacheRangeToTime(const qint64& t); + int TruncateCacheRangeToFrames(int nb_frames); void RemoveFirstFrame(); AVFormatContext* fmt_ctx() const diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index 562d4f82e..c31ac6b5a 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -185,10 +185,9 @@ fail: return success; } -void FFmpegEncoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename) +void FFmpegEncoder::WriteAudio(AudioParams pcm_info, QIODevice* file) { - QFile pcm(pcm_filename); - if (pcm.open(QFile::ReadOnly)) { + if (file->open(QFile::ReadOnly)) { // Divide PCM stream into AVFrames // See if the codec defines a number of samples per frame @@ -239,7 +238,7 @@ void FFmpegEncoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename int max_read = pcm_info.samples_to_bytes(samples_needed); // Read bytes from PCM - QByteArray input_data = pcm.read(max_read); + QByteArray input_data = file->read(max_read); // Use swresample to convert the data into the correct format const char* input_data_array = input_data.constData(); @@ -273,7 +272,7 @@ void FFmpegEncoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename } // Break if we've reached the end point - if (pcm.atEnd()) { + if (file->atEnd()) { break; } } @@ -282,7 +281,9 @@ void FFmpegEncoder::WriteAudio(AudioParams pcm_info, const QString &pcm_filename swr_free(&swr_ctx); - pcm.close(); + file->close(); + } else { + qWarning() << "Failed to open audio IO device for encoding"; } } diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index a3ef13482..4efc76754 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -43,7 +43,7 @@ public: virtual bool WriteFrame(OLIVE_NAMESPACE::FramePtr frame, OLIVE_NAMESPACE::rational time) override; virtual void WriteAudio(OLIVE_NAMESPACE::AudioParams pcm_info, - const QString& pcm_filename) override; + QIODevice *file) override; virtual void Close() override; diff --git a/app/common/crashpadinterface.cpp b/app/common/crashpadinterface.cpp index c65db16d0..adc954e49 100644 --- a/app/common/crashpadinterface.cpp +++ b/app/common/crashpadinterface.cpp @@ -86,7 +86,7 @@ bool InitializeCrashpad() bool status = false; if (QFileInfo::exists(handler_abs_path)) { - base::FilePath handler(QSTRING_TO_BASE_STRING(handler_fn)); + base::FilePath handler(QSTRING_TO_BASE_STRING(handler_abs_path)); base::FilePath reports_dir = GenerateReportPathForCrashpad(); diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index 6656facbe..d8b2a5b6b 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -160,20 +160,25 @@ void TimeRange::normalize() length_ = out_ - in_; } -void TimeRangeList::InsertTimeRange(const TimeRange &range) +void TimeRangeList::InsertTimeRange(TimeRange range_to_add) { + // See if list contains this range + if (ContainsTimeRange(range_to_add)) { + return; + } + + // Does not contain range, so we'll almost certainly be adding it in some way for (int i=0;iremoveAt(i); - i--; - sz--; - - InsertTimeRange(before); - InsertTimeRange(after); + this->append(TimeRange(remove.out(), compare.out())); + compare.set_out(remove.in()); } else if (compare.in() < remove.in() && compare.out() > remove.in()) { // This element's out point overlaps the range's in, we'll trim it compare.set_out(remove.in()); diff --git a/app/common/timerange.h b/app/common/timerange.h index 22b8317fb..de5785b5f 100644 --- a/app/common/timerange.h +++ b/app/common/timerange.h @@ -74,7 +74,7 @@ public: { } - void InsertTimeRange(const TimeRange& range); + void InsertTimeRange(TimeRange range_to_add); void RemoveTimeRange(const TimeRange& remove); diff --git a/app/config/config.cpp b/app/config/config.cpp index f1dbe18fe..f2db463d7 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -87,7 +87,7 @@ void Config::SetDefaults() SetEntryInternal(QStringLiteral("AutoSelectDivider"), NodeParam::kBoolean, true); SetEntryInternal(QStringLiteral("SetNameWithMarker"), NodeParam::kBoolean, false); SetEntryInternal(QStringLiteral("RectifiedWaveforms"), NodeParam::kBoolean, false); - SetEntryInternal(QStringLiteral("DropWithoutSequenceBehavior"), NodeParam::kInt, TimelineWidget::kDWSAsk); + SetEntryInternal(QStringLiteral("DropWithoutSequenceBehavior"), NodeParam::kInt, ImportTool::kDWSAsk); SetEntryInternal(QStringLiteral("Loop"), NodeParam::kBoolean, false); SetEntryInternal(QStringLiteral("AutoCacheInterval"), NodeParam::kInt, 250); diff --git a/app/core.cpp b/app/core.cpp index 1fa22e666..f7e9faec8 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -70,6 +70,7 @@ OLIVE_NAMESPACE_ENTER Core* Core::instance_ = nullptr; +const uint Core::kProjectVersion = 201003; Core::Core(const CoreParams& params) : main_window_(nullptr), diff --git a/app/core.h b/app/core.h index edf176a5d..47d60b6c1 100644 --- a/app/core.h +++ b/app/core.h @@ -273,6 +273,8 @@ public: */ bool ValidateFootageInLoadedProject(ProjectPtr project, const QString &project_saved_url); + static const uint kProjectVersion; + public slots: /** * @brief Starts an open file dialog to load a project from file diff --git a/app/dialog/crashhandler/crashhandler.cpp b/app/dialog/crashhandler/crashhandler.cpp index 586684c03..2b21293f9 100644 --- a/app/dialog/crashhandler/crashhandler.cpp +++ b/app/dialog/crashhandler/crashhandler.cpp @@ -53,7 +53,7 @@ CrashHandlerDialog::CrashHandlerDialog(const char *report_dir, const char* crash summary_edit_ = new QTextEdit(); summary_edit_->setPlaceholderText(tr("Describe what you were doing in as much detail as " - "possible. If you can, provide steps to reproduce this crash.")); + "possible. If you can, provide steps to reproduce this crash.")); layout->addWidget(summary_edit_); @@ -166,6 +166,15 @@ void CrashHandlerDialog::ReadProcessFinished() void CrashHandlerDialog::SendErrorReport() { + if (summary_edit_->document()->isEmpty()) { + if (QMessageBox::question(this, + tr("No Crash Summary"), + tr("Are you sure you want to send an error report with no crash summary?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + return; + } + } + SetGUIObjectsEnabled(false); QNetworkAccessManager* manager = new QNetworkAccessManager(); diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 9e59c94d6..2d3250be9 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -421,15 +421,13 @@ int ExportDialog::AlignEvenNumber(double d) ExportParams ExportDialog::GenerateParams() const { - RenderMode::Mode render_mode = RenderMode::kOnline; - VideoParams video_render_params(static_cast(video_tab_->width_slider()->GetValue()), static_cast(video_tab_->height_slider()->GetValue()), video_tab_->frame_rate_combobox()->GetFrameRate().flipped(), - PixelFormat::instance()->GetConfiguredFormatForMode(render_mode), + PixelFormat::instance()->GetConfiguredFormatForMode(RenderMode::kOnline), video_tab_->pixel_aspect_combobox()->GetPixelAspectRatio(), video_tab_->interlaced_combobox()->GetInterlaceMode(), - render_mode); + 1); AudioParams audio_render_params(audio_tab_->sample_rate_combobox()->currentData().toInt(), audio_tab_->channel_layout_combobox()->GetChannelLayout(), diff --git a/app/node/audio/volume/volume.cpp b/app/node/audio/volume/volume.cpp index 38a6cc7da..4b403118b 100644 --- a/app/node/audio/volume/volume.cpp +++ b/app/node/audio/volume/volume.cpp @@ -25,6 +25,7 @@ OLIVE_NAMESPACE_ENTER VolumeNode::VolumeNode() { samples_input_ = new NodeInput("samples_in", NodeParam::kSamples); + samples_input_->set_is_keyframable(false); AddInput(samples_input_); volume_input_ = new NodeInput("volume_in", NodeParam::kFloat, 1.0); diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 68dac495a..68b9779bd 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -122,6 +122,11 @@ void Block::set_length_and_media_in(const rational &length) LengthChangedEvent(old_length, length, Timeline::kTrimIn); } +TimeRange Block::range() const +{ + return TimeRange(in(), out()); +} + Block *Block::previous() { return previous_; diff --git a/app/node/block/block.h b/app/node/block/block.h index 7fa036302..ca5c700ee 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -54,6 +54,8 @@ public: void set_length_and_media_out(const rational &length); void set_length_and_media_in(const rational &length); + TimeRange range() const; + Block* previous(); Block* next(); void set_previous(Block* previous); diff --git a/app/node/edge.cpp b/app/node/edge.cpp index 1765d6c1b..5f009b8f3 100644 --- a/app/node/edge.cpp +++ b/app/node/edge.cpp @@ -20,22 +20,31 @@ #include "edge.h" +#include "input.h" +#include "node.h" +#include "output.h" + OLIVE_NAMESPACE_ENTER -NodeEdge::NodeEdge(NodeOutput *output, NodeInput *input) : - output_(output), - input_(input) +NodeEdge::NodeEdge(NodeOutput *output, NodeInput *input) { + output_ = ParamToConnection(output); + input_ = ParamToConnection(input); } -NodeOutput *NodeEdge::output() +NodeOutput *NodeEdge::output() const { - return output_; + return output_.node->GetOutputWithID(output_.id); } -NodeInput *NodeEdge::input() +NodeInput *NodeEdge::input() const { - return input_; + return input_.node->GetInputWithID(input_.id); +} + +NodeEdge::Connection NodeEdge::ParamToConnection(NodeParam *param) +{ + return {param->parentNode(), param->id()}; } OLIVE_NAMESPACE_EXIT diff --git a/app/node/edge.h b/app/node/edge.h index 990fdc626..a79b8aec7 100644 --- a/app/node/edge.h +++ b/app/node/edge.h @@ -22,13 +22,16 @@ #define EDGE_H #include +#include #include "common/define.h" OLIVE_NAMESPACE_ENTER -class NodeOutput; +class Node; class NodeInput; +class NodeOutput; +class NodeParam; /** * @brief A connection between two node parameters (a NodeOutput and a NodeInput) @@ -44,19 +47,37 @@ public: */ NodeEdge(NodeOutput* output, NodeInput* input); + Node* output_node() const + { + return output_.node; + } + + Node* input_node() const + { + return input_.node; + } + /** * @brief Return the output parameter this edge is connected to */ - NodeOutput* output(); + NodeOutput* output() const; /** * @brief Return the input parameter this edge is connected to */ - NodeInput* input(); + NodeInput* input() const; private: - NodeOutput* output_; - NodeInput* input_; + struct Connection { + Node* node; + QString id; + }; + + static Connection ParamToConnection(NodeParam* param); + + Connection output_; + Connection input_; + }; using NodeEdgePtr = std::shared_ptr; diff --git a/app/node/graph.cpp b/app/node/graph.cpp index b17132fb3..aa7515f9a 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -22,6 +22,11 @@ OLIVE_NAMESPACE_ENTER +NodeGraph::NodeGraph() : + operation_stack_(0) +{ +} + void NodeGraph::Clear() { foreach (Node* node, node_children_) { @@ -38,14 +43,94 @@ void NodeGraph::AddNode(Node *node) node->setParent(this); - connect(node, &Node::EdgeAdded, this, &NodeGraph::EdgeAdded); - connect(node, &Node::EdgeRemoved, this, &NodeGraph::EdgeRemoved); + connect(node, &Node::EdgeAdded, this, &NodeGraph::SignalEdgeAdded); + connect(node, &Node::EdgeRemoved, this, &NodeGraph::SignalEdgeRemoved); node_children_.append(node); emit NodeAdded(node); } +void NodeGraph::BeginOperation() +{ + operation_stack_++; +} + +void NodeGraph::EndOperation() +{ + operation_stack_--; + + if (!operation_stack_) { + // Signal everything that we cached during the operation + + // First, signal the removed edges + foreach (NodeEdgePtr e, cached_removed_edges_) { + emit EdgeRemoved(e); + } + cached_removed_edges_.clear(); + + // Next, signal the removed nodes + foreach (Node* n, cached_removed_nodes_) { + emit NodeRemoved(n); + } + cached_removed_nodes_.clear(); + + // Next, signal the added nodes + foreach (Node* n, cached_added_nodes_) { + emit NodeAdded(n); + } + cached_added_nodes_.clear(); + + // Finally, signal the added edges + foreach (NodeEdgePtr e, cached_added_edges_) { + emit EdgeAdded(e); + } + cached_added_edges_.clear(); + } +} + +void NodeGraph::SignalNodeAdded(Node* node) +{ + if (!operation_stack_) { + emit NodeAdded(node); + } else if (!cached_removed_nodes_.removeOne(node)) { + // If we already removed this node during the operation (appending a signal to + // cached_removed_nodes_), we just remove that instead of appending a new signal. However if we + // didn't (removeOne returning false), only then do we append an add signal + cached_added_nodes_.append(node); + } +} + +void NodeGraph::SignalNodeRemoved(Node *node) +{ + if (!operation_stack_) { + emit NodeRemoved(node); + } else if (!cached_added_nodes_.removeOne(node)) { + // See SignalNodeAdded() for explanation of this + cached_removed_nodes_.append(node); + } +} + +void NodeGraph::SignalEdgeAdded(NodeEdgePtr edge) +{ + if (!operation_stack_) { + emit EdgeAdded(edge); + } else if (!cached_removed_edges_.removeOne(edge)) { + // See SignalNodeAdded() for explanation of this + cached_added_edges_.append(edge); + } +} + +void NodeGraph::SignalEdgeRemoved(NodeEdgePtr edge) +{ + if (!operation_stack_) { + emit EdgeRemoved(edge); + } else if (!cached_added_edges_.removeOne(edge)) { + // See SignalNodeAdded() for explanation of this + cached_removed_edges_.append(edge); + } +} + void NodeGraph::TakeNode(Node *node, QObject* new_parent) { if (!ContainsNode(node)) { diff --git a/app/node/graph.h b/app/node/graph.h index 8ba686886..68324eb7a 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -37,7 +37,7 @@ public: /** * @brief NodeGraph Constructor */ - NodeGraph() = default; + NodeGraph(); /** * @brief Destructively destroys all nodes in the graph @@ -67,6 +67,10 @@ public: */ bool ContainsNode(Node* n) const; + void BeginOperation(); + + void EndOperation(); + signals: /** * @brief Signal emitted when a Node is added to the graph @@ -90,6 +94,20 @@ signals: private: QList node_children_; + + int operation_stack_; + + QList cached_added_nodes_; + QList cached_removed_nodes_; + QList cached_added_edges_; + QList cached_removed_edges_; + +private slots: + void SignalNodeAdded(Node *node); + void SignalNodeRemoved(Node* node); + void SignalEdgeAdded(NodeEdgePtr edge); + void SignalEdgeRemoved(NodeEdgePtr edge); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/node/input/media/audio/audio.cpp b/app/node/input/media/audio/audio.cpp index 5f34543ec..49463aa22 100644 --- a/app/node/input/media/audio/audio.cpp +++ b/app/node/input/media/audio/audio.cpp @@ -27,6 +27,11 @@ Node *AudioInput::copy() const return new AudioInput(); } +Stream::Type AudioInput::type() const +{ + return Stream::kAudio; +} + QString AudioInput::Name() const { return tr("Audio Input"); diff --git a/app/node/input/media/audio/audio.h b/app/node/input/media/audio/audio.h index 62914b693..5cebdcc3c 100644 --- a/app/node/input/media/audio/audio.h +++ b/app/node/input/media/audio/audio.h @@ -32,6 +32,8 @@ public: virtual Node* copy() const override; + virtual Stream::Type type() const override; + virtual QString Name() const override; virtual QString ShortName() const override; virtual QString id() const override; diff --git a/app/node/input/media/media.cpp b/app/node/input/media/media.cpp index c0e1b3feb..8bfe11bb9 100644 --- a/app/node/input/media/media.cpp +++ b/app/node/input/media/media.cpp @@ -50,6 +50,11 @@ void MediaInput::SetFootage(StreamPtr f) footage_input_->set_standard_value(QVariant::fromValue(f)); } +bool MediaInput::IsMedia() const +{ + return true; +} + void MediaInput::Retranslate() { footage_input_->set_name(tr("Footage")); diff --git a/app/node/input/media/media.h b/app/node/input/media/media.h index ba89f3168..8934e321e 100644 --- a/app/node/input/media/media.h +++ b/app/node/input/media/media.h @@ -23,6 +23,7 @@ #include "codec/decoder.h" #include "node/node.h" +#include "project/item/footage/stream.h" OLIVE_NAMESPACE_ENTER @@ -35,11 +36,16 @@ class MediaInput : public Node public: MediaInput(); + virtual Stream::Type type() const = 0; + virtual QList Category() const override; StreamPtr footage(); void SetFootage(StreamPtr f); + virtual bool IsMedia() const override; + + virtual void Retranslate() override; virtual NodeValueTable Value(NodeValueDatabase& value) const override; diff --git a/app/node/input/media/video/video.cpp b/app/node/input/media/video/video.cpp index d451daeaf..c02aeaf39 100644 --- a/app/node/input/media/video/video.cpp +++ b/app/node/input/media/video/video.cpp @@ -36,6 +36,11 @@ Node *VideoInput::copy() const return new VideoInput(); } +Stream::Type VideoInput::type() const +{ + return Stream::kVideo; +} + QString VideoInput::Name() const { return tr("Video Input"); diff --git a/app/node/input/media/video/video.h b/app/node/input/media/video/video.h index 3ef9f54e8..7000ea82b 100644 --- a/app/node/input/media/video/video.h +++ b/app/node/input/media/video/video.h @@ -35,6 +35,8 @@ public: virtual Node* copy() const override; + virtual Stream::Type type() const override; + virtual QString Name() const override; virtual QString ShortName() const override; virtual QString id() const override; diff --git a/app/node/inputarray.cpp b/app/node/inputarray.cpp index d2faf3485..94d5b4080 100644 --- a/app/node/inputarray.cpp +++ b/app/node/inputarray.cpp @@ -33,6 +33,12 @@ NodeInputArray::NodeInputArray(const QString &id, const DataType &type, const QV { } +NodeInputArray::~NodeInputArray() +{ + // Clear all connected edges (make sure our override is called) + DisconnectAll(); +} + bool NodeInputArray::IsArray() const { return true; @@ -58,6 +64,10 @@ void NodeInputArray::SetSize(int size) if (size < old_size) { // If the new size is less, delete all extraneous parameters + for (int i=size;iDisconnectAll(); + } + for (int i=size;i &NodeInputArray::sub_params() return sub_params_; } +void NodeInputArray::DisconnectAll() +{ + NodeParam::DisconnectAll(); + + foreach (NodeInput* input, sub_params_) { + input->DisconnectAll(); + } +} + void NodeInputArray::InsertAt(int index) { // Add another input at the end diff --git a/app/node/inputarray.h b/app/node/inputarray.h index 8a935f73e..5aac757ac 100644 --- a/app/node/inputarray.h +++ b/app/node/inputarray.h @@ -31,6 +31,8 @@ class NodeInputArray : public NodeInput public: NodeInputArray(const QString &id, const DataType& type, const QVariant& default_value = 0); + virtual ~NodeInputArray() override; + virtual bool IsArray() const override; int GetSize() const; @@ -51,6 +53,8 @@ public: const QVector& sub_params(); + virtual void DisconnectAll() override; + signals: void SizeChanged(int size); diff --git a/app/node/node.cpp b/app/node/node.cpp index 741ca111e..148c41ee5 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -467,6 +467,11 @@ bool Node::IsTrack() const return false; } +bool Node::IsMedia() const +{ + return false; +} + const QList& Node::parameters() const { return params_; diff --git a/app/node/node.h b/app/node/node.h index ef907da37..5ed6826b7 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -369,6 +369,15 @@ public: */ virtual bool IsTrack() const; + + /** + * @brief Returns whether this Node is a "Media" type or not + * + * You shouldn't ever need to override this since all derivatives of Media will automatically have this set to true. + * It's just a more convenient way of checking than dynamic_casting. + */ + virtual bool IsMedia() const; + /** * @brief The main processing function * @@ -568,6 +577,8 @@ QList Node::FindOutputNode() return list; } +using NodePtr = std::shared_ptr; + OLIVE_NAMESPACE_EXIT #endif // NODE_H diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index e98049e85..1cd8de5f9 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -55,6 +55,11 @@ TrackOutput::TrackOutput() : track_height_ = kTrackHeightDefault; } +TrackOutput::~TrackOutput() +{ + DisconnectAll(); +} + void TrackOutput::set_track_type(const Timeline::TrackType &track_type) { track_type_ = track_type; @@ -91,15 +96,6 @@ QString TrackOutput::Description() const "a Sequence."); } -QString TrackOutput::GetTrackName() -{ - if (track_name_.isEmpty()) { - return GetDefaultTrackName(track_type_, index_); - } - - return track_name_; -} - const double &TrackOutput::GetTrackHeight() const { return track_height_; @@ -435,11 +431,6 @@ void TrackOutput::Hash(QCryptographicHash &hash, const rational &time) const } } -void TrackOutput::SetTrackName(const QString &name) -{ - track_name_ = name; -} - void TrackOutput::SetMuted(bool e) { muted_input_->set_standard_value(e); @@ -555,7 +546,7 @@ void TrackOutput::BlockConnected(NodeEdgePtr edge) void TrackOutput::BlockDisconnected(NodeEdgePtr edge) { - Block* b = static_cast(edge->output()->parentNode()); + Block* b = static_cast(edge->output_node()); if (block_cache_.contains(b)) { block_cache_.removeOne(b); diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 954b9b45f..6e32f1c9c 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -36,6 +36,8 @@ class TrackOutput : public Node public: TrackOutput(); + virtual ~TrackOutput() override; + const Timeline::TrackType& track_type() const; void set_track_type(const Timeline::TrackType& track_type); @@ -46,8 +48,6 @@ public: virtual QList Category() const override; virtual QString Description() const override; - QString GetTrackName(); - const double& GetTrackHeight() const; void SetTrackHeight(const double& height); @@ -227,8 +227,6 @@ public: static const double kTrackHeightInterval; public slots: - void SetTrackName(const QString& name); - void SetMuted(bool e); void SetLocked(bool e); @@ -294,8 +292,6 @@ private: double track_height_; - QString track_name_; - int index_; bool locked_; diff --git a/app/node/output/track/tracklist.cpp b/app/node/output/track/tracklist.cpp index 9871baabb..e18332040 100644 --- a/app/node/output/track/tracklist.cpp +++ b/app/node/output/track/tracklist.cpp @@ -134,7 +134,7 @@ TrackOutput* TrackList::AddTrack() return track; } -void TrackList::RemoveTrack() +void TrackList::RemoveTrack(QObject* new_parent) { if (track_cache_.isEmpty()) { return; @@ -144,7 +144,11 @@ void TrackList::RemoveTrack() GetParentGraph()->TakeNode(track); - delete track; + if (!new_parent) { + delete track; + } else { + track->setParent(new_parent); + } track_input_->RemoveLast(); } @@ -184,7 +188,8 @@ void TrackList::TrackConnected(NodeEdgePtr edge) track_cache_.append(connected_track); } - connected_track->SetIndex(track_index); + // Update track indexes in the list (including this track) + UpdateTrackIndexesFrom(track_index); } connect(connected_track, &TrackOutput::BlockAdded, this, &TrackList::TrackAddedBlock); @@ -220,9 +225,7 @@ void TrackList::TrackDisconnected(NodeEdgePtr edge) track_cache_.removeAt(index_of_track); // Update indices for all subsequent tracks - for (int i=index_of_track; iSetIndex(i); - } + UpdateTrackIndexesFrom(index_of_track); // Traverse through Tracks uncaching and disconnecting them emit TrackRemoved(track); @@ -241,6 +244,13 @@ void TrackList::TrackDisconnected(NodeEdgePtr edge) } } +void TrackList::UpdateTrackIndexesFrom(int index) +{ + for (int i=index; iSetIndex(i); + } +} + NodeGraph *TrackList::GetParentGraph() const { return static_cast(parent()->parent()); diff --git a/app/node/output/track/tracklist.h b/app/node/output/track/tracklist.h index d0923f577..2589247e5 100644 --- a/app/node/output/track/tracklist.h +++ b/app/node/output/track/tracklist.h @@ -44,7 +44,7 @@ public: TrackOutput *AddTrack(); - void RemoveTrack(); + void RemoveTrack(QObject *new_parent); const rational& GetTotalLength() const; @@ -68,6 +68,8 @@ signals: void TrackHeightChanged(int index, int height); private: + void UpdateTrackIndexesFrom(int index); + /** * @brief A cache of connected Tracks */ diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 169de22cc..ca99b0837 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -52,7 +52,7 @@ ViewerOutput::ViewerOutput() : connect(list, &TrackList::TrackListChanged, this, &ViewerOutput::UpdateTrackCache); connect(list, &TrackList::LengthChanged, this, &ViewerOutput::VerifyLength); connect(list, &TrackList::BlockAdded, this, &ViewerOutput::TrackListAddedBlock); - connect(list, &TrackList::BlockRemoved, this, &ViewerOutput::BlockRemoved); + connect(list, &TrackList::BlockRemoved, this, &ViewerOutput::SignalBlockRemoved); connect(list, &TrackList::TrackAdded, this, &ViewerOutput::TrackListAddedTrack); connect(list, &TrackList::TrackRemoved, this, &ViewerOutput::TrackRemoved); connect(list, &TrackList::TrackHeightChanged, this, &ViewerOutput::TrackHeightChangedSlot); @@ -62,6 +62,11 @@ ViewerOutput::ViewerOutput() : uuid_ = QUuid::createUuid(); } +ViewerOutput::~ViewerOutput() +{ + DisconnectAll(); +} + Node *ViewerOutput::copy() const { return new ViewerOutput(); @@ -274,6 +279,27 @@ void ViewerOutput::set_media_name(const QString &name) emit MediaNameChanged(media_name_); } +void ViewerOutput::SignalBlockAdded(Block *block, const TrackReference& track) +{ + if (!operation_stack_) { + emit BlockAdded(block, track); + } else { + cached_block_removed_.removeOne(block); + cached_block_added_.insert(block, track); + } +} + +void ViewerOutput::SignalBlockRemoved(Block *block) +{ + if (!operation_stack_) { + emit BlockRemoved({block}); + } else { + // We keep track of all blocks that are removed, even if we don't end up signalling them + cached_block_added_.remove(block); + cached_block_removed_.append(block); + } +} + void ViewerOutput::BeginOperation() { operation_stack_++; @@ -285,13 +311,23 @@ void ViewerOutput::EndOperation() { operation_stack_--; + if (!operation_stack_) { + for (auto it=cached_block_added_.cbegin(); it!=cached_block_added_.cend(); it++) { + emit BlockAdded(it.key(), it.value()); + } + cached_block_added_.clear(); + + emit BlockRemoved(cached_block_removed_); + cached_block_removed_.clear(); + } + Node::EndOperation(); } void ViewerOutput::TrackListAddedBlock(Block *block, int index) { Timeline::TrackType type = static_cast(sender())->type(); - emit BlockAdded(block, TrackReference(type, index)); + SignalBlockAdded(block, TrackReference(type, index)); } void ViewerOutput::TrackListAddedTrack(TrackOutput *track) diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 96c30cdf9..94185f2ac 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -47,6 +47,8 @@ class ViewerOutput : public Node public: ViewerOutput(); + virtual ~ViewerOutput() override; + virtual Node* copy() const override; virtual QString Name() const override; @@ -139,7 +141,7 @@ signals: void AudioParamsChanged(); void BlockAdded(Block* block, TrackReference track); - void BlockRemoved(Block* block); + void BlockRemoved(const QList& blocks); void TrackAdded(TrackOutput* track, Timeline::TrackType type); void TrackRemoved(TrackOutput* track); @@ -149,6 +151,9 @@ signals: void MediaNameChanged(const QString& name); private: + QMap cached_block_added_; + QList cached_block_removed_; + QUuid uuid_; NodeInput* texture_input_; @@ -186,6 +191,9 @@ private slots: void TrackHeightChangedSlot(int index, int height); + void SignalBlockAdded(Block *block, const TrackReference &track); + void SignalBlockRemoved(Block *block); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/node/param.cpp b/app/node/param.cpp index 601b393da..de6453a7d 100644 --- a/app/node/param.cpp +++ b/app/node/param.cpp @@ -43,9 +43,7 @@ NodeParam::NodeParam(const QString &id) : NodeParam::~NodeParam() { // Clear all connected edges - while (!edges_.isEmpty()) { - DisconnectEdge(edges_.last()); - } + DisconnectAll(); } const QString NodeParam::id() const @@ -111,7 +109,7 @@ const QVector &NodeParam::edges() void NodeParam::DisconnectAll() { while (!edges_.isEmpty()) { - DisconnectEdge(edges_.first()); + DisconnectEdge(edges_.last()); } } diff --git a/app/node/param.h b/app/node/param.h index 0157d7bdd..553dbe248 100644 --- a/app/node/param.h +++ b/app/node/param.h @@ -319,7 +319,7 @@ public: /** * @brief Disconnect any edges connecting this parameter to other parameters */ - void DisconnectAll(); + virtual void DisconnectAll(); /** * @brief Connect an output parameter to an input parameter diff --git a/app/panel/curve/curve.cpp b/app/panel/curve/curve.cpp index b6179d258..fd6e418ae 100644 --- a/app/panel/curve/curve.cpp +++ b/app/panel/curve/curve.cpp @@ -32,21 +32,14 @@ CurvePanel::CurvePanel(QWidget *parent) : Retranslate(); } -NodeInput *CurvePanel::GetInput() const -{ - return static_cast(GetTimeBasedWidget())->GetInput(); -} - void CurvePanel::DeleteSelected() { static_cast(GetTimeBasedWidget())->DeleteSelected(); } -void CurvePanel::SetInput(NodeInput *input) +void CurvePanel::SetNodes(const QList &nodes) { - static_cast(GetTimeBasedWidget())->SetInput(input); - - Retranslate(); + static_cast(GetTimeBasedWidget())->SetNodes(nodes); } void CurvePanel::IncreaseTrackHeight() @@ -66,13 +59,6 @@ void CurvePanel::Retranslate() TimeBasedPanel::Retranslate(); SetTitle(tr("Curve Editor")); - - NodeInput* connected_input = static_cast(GetTimeBasedWidget())->GetInput(); - if (connected_input) { - SetSubtitle(connected_input->name()); - } else { - SetSubtitle(QString()); - } } OLIVE_NAMESPACE_EXIT diff --git a/app/panel/curve/curve.h b/app/panel/curve/curve.h index 982a4d0b4..375c42822 100644 --- a/app/panel/curve/curve.h +++ b/app/panel/curve/curve.h @@ -32,12 +32,10 @@ class CurvePanel : public TimeBasedPanel public: CurvePanel(QWidget* parent); - NodeInput* GetInput() const; - virtual void DeleteSelected() override; public slots: - void SetInput(NodeInput* input); + void SetNodes(const QList& nodes); virtual void IncreaseTrackHeight() override; diff --git a/app/panel/param/param.cpp b/app/panel/param/param.cpp index 519250837..2b9df7f4c 100644 --- a/app/panel/param/param.cpp +++ b/app/panel/param/param.cpp @@ -28,9 +28,9 @@ ParamPanel::ParamPanel(QWidget* parent) : TimeBasedPanel(QStringLiteral("ParamPanel"), parent) { NodeParamView* view = new NodeParamView(); - connect(view, &NodeParamView::InputDoubleClicked, this, &ParamPanel::CreateCurvePanel); connect(view, &NodeParamView::RequestSelectNode, this, &ParamPanel::RequestSelectNode); - //connect(view, &NodeParamView::FoundGizmos, this, &ParamPanel::FoundGizmos); + connect(view, &NodeParamView::NodeOrderChanged, this, &ParamPanel::NodeOrderChanged); + connect(view, &NodeParamView::FocusedNodeChanged, this, &ParamPanel::FocusedNodeChanged); SetTimeBasedWidget(view); Retranslate(); @@ -50,14 +50,6 @@ void ParamPanel::DeselectNodes(const QList &nodes) Retranslate(); } -void ParamPanel::SetTimestamp(const int64_t ×tamp) -{ - TimeBasedPanel::SetTimestamp(timestamp); - - // Ensure all CurvePanels are updated with this time too - ParamViewTimeChanged(timestamp); -} - void ParamPanel::DeleteSelected() { static_cast(GetTimeBasedWidget())->DeleteSelected(); @@ -78,69 +70,4 @@ void ParamPanel::Retranslate() } } -void ParamPanel::CreateCurvePanel(NodeInput *input) -{ - if (!input->is_keyframable()) { - return; - } - - CurvePanel* panel = open_curve_panels_.value(input); - - if (panel) { - panel->raise(); - return; - } - - NodeParamView* view = static_cast(GetTimeBasedWidget()); - - panel = Core::instance()->main_window()->AppendCurvePanel(); - - panel->ConnectViewerNode(view->GetConnectedNode()); - panel->SetTimestamp(view->GetTimestamp()); - panel->SetInput(input); - - connect(view, &NodeParamView::TimeChanged, this, &ParamPanel::ParamViewTimeChanged); - connect(panel, &CurvePanel::TimeChanged, this, &ParamPanel::CurvePanelTimeChanged); - connect(panel, &CurvePanel::CloseRequested, this, &ParamPanel::ClosingCurvePanel); - - open_curve_panels_.insert(input, panel); -} - -void ParamPanel::ClosingCurvePanel() -{ - CurvePanel* panel = static_cast(sender()); - open_curve_panels_.remove(panel->GetInput()); -} - -void ParamPanel::ParamViewTimeChanged(const int64_t &time) -{ - // Ensure all CurvePanels are updated with this time too - QHash::const_iterator i; - - for (i=open_curve_panels_.begin(); i!=open_curve_panels_.end(); i++) { - // If connected viewers are the same, set the timestamp - if (i.value()->GetConnectedViewer() == GetConnectedViewer()) { - i.value()->SetTimestamp(time); - } - } -} - -void ParamPanel::CurvePanelTimeChanged(const int64_t &time) -{ - GetTimeBasedWidget()->SetTimestamp(time); - emit GetTimeBasedWidget()->TimeChanged(time); - - CurvePanel* src = static_cast(sender()); - - // Ensure all CurvePanels are updated with this time too - QHash::const_iterator i; - - for (i=open_curve_panels_.begin(); i!=open_curve_panels_.end(); i++) { - // If connected viewers are the same and the panel isn't the source, set the timestamp - if (i.value() != src && i.value()->GetConnectedViewer() == src->GetConnectedViewer()) { - i.value()->SetTimestamp(time); - } - } -} - OLIVE_NAMESPACE_EXIT diff --git a/app/panel/param/param.h b/app/panel/param/param.h index 442c5e921..3c4f15354 100644 --- a/app/panel/param/param.h +++ b/app/panel/param/param.h @@ -37,31 +37,18 @@ public slots: void SelectNodes(const QList& nodes); void DeselectNodes(const QList& nodes); - virtual void SetTimestamp(const int64_t& timestamp) override; - virtual void DeleteSelected() override; signals: void RequestSelectNode(const QList& target); - void FoundGizmos(Node* node); + void NodeOrderChanged(const QList& nodes); + + void FocusedNodeChanged(Node* n); protected: virtual void Retranslate() override; -private slots: - void CreateCurvePanel(NodeInput* input); - - void ClosingCurvePanel(); - -private: - QHash open_curve_panels_; - -private slots: - void ParamViewTimeChanged(const int64_t& time); - - void CurvePanelTimeChanged(const int64_t& time); - }; OLIVE_NAMESPACE_EXIT diff --git a/app/project/projectviewmodel.cpp b/app/project/projectviewmodel.cpp index 7dfaf453a..a1ba9b481 100644 --- a/app/project/projectviewmodel.cpp +++ b/app/project/projectviewmodel.cpp @@ -25,6 +25,7 @@ #include #include "core.h" +#include "node/input/media/media.h" OLIVE_NAMESPACE_ENTER diff --git a/app/project/projectviewmodel.h b/app/project/projectviewmodel.h index 964378316..e3de6e33a 100644 --- a/app/project/projectviewmodel.h +++ b/app/project/projectviewmodel.h @@ -25,6 +25,7 @@ #include "project.h" #include "undo/undocommand.h" +#include "node/block/block.h" OLIVE_NAMESPACE_ENTER diff --git a/app/render/audioplaybackcache.cpp b/app/render/audioplaybackcache.cpp index 9b9e8897b..77f7ab537 100644 --- a/app/render/audioplaybackcache.cpp +++ b/app/render/audioplaybackcache.cpp @@ -28,11 +28,20 @@ OLIVE_NAMESPACE_ENTER +const rational AudioPlaybackCache::kDefaultSegmentSize = 5; + AudioPlaybackCache::AudioPlaybackCache(QObject* parent) : PlaybackCache(parent) { - quint32 r = std::rand(); - UpdateFilename(QString::number(r)); +} + +AudioPlaybackCache::~AudioPlaybackCache() +{ + // Segments are volatile, so delete them here + foreach (const Segment& s, segments_) { + QFile::remove(s.filename()); + } + segments_.clear(); } void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) @@ -44,16 +53,10 @@ void AudioPlaybackCache::SetParameters(const AudioParams ¶ms) params_ = params; // Restart empty file so there's always "something" to play - QFile f(filename_); - if (f.open(QFile::WriteOnly)) { - f.close(); - } + segments_.clear(); // Our current audio cache is unusable, so we truncate it automatically - TimeRange invalidate_range(0, GetLength()); - if (invalidate_range.in() != invalidate_range.out()) { - Invalidate(invalidate_range); - } + InvalidateAll(); emit ParametersChanged(); } @@ -65,148 +68,215 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample return; } - QFile f(filename_); - if (f.open(QFile::ReadWrite)) { - QByteArray a = samples->toPackedData(); + // Determine if we have enough segments to pull this off + while (segment_length_ < range.out()) { + rational seg_sz = qMin(kDefaultSegmentSize, range.out() - segment_length_); + segments_.push_back(CreateSegment(seg_sz)); + segment_length_ += seg_sz; + } - foreach (const TimeRange& r, valid_ranges) { - // Calculate destination offsets - qint64 start_offset = params_.time_to_bytes(r.in()); - qint64 max_len = params_.time_to_bytes(r.out()); + QByteArray a; + if (samples) { + // Convert to packed data, which is what we store on disk + a = samples->toPackedData(); + } - if (f.size() < max_len) { - f.resize(max_len); + // Keep track of validated ranges so we can signal them all at once at the end + TimeRangeList ranges_we_validated; + + // Write each valid range to the segments + foreach (const TimeRange& r, valid_ranges) { + rational this_segment_in = 0; + + for (auto it=segments_.begin(); it!=segments_.end(); it++) { + rational this_segment_out = this_segment_in + (*it).length(); + + if (r.in() < this_segment_out) { + // We'll write at least something to this segment + QFile seg_file((*it).filename()); + + if (seg_file.open(QFile::ReadWrite)) { + // Calculate how much to write + rational this_write_in_point = qMax(r.in(), this_segment_in); + rational this_write_out_point = qMin(r.out(), this_segment_out); + + // Calculate what the byte offsets are going to be in this segment file + rational in_point_relative = this_write_in_point - this_segment_in; + qint64 dst_offset = params_.time_to_bytes(in_point_relative); + + // Calculate where to retrieve data from in the source buffer + qint64 src_offset = params_.time_to_bytes(this_write_in_point - range.in()); + + // Determine how many bytes need to be written + qint64 total_write_length = params_.time_to_bytes(this_write_out_point - this_write_in_point); + + // Determine how many bytes we actually have in the source buffer + qint64 possible_write_length = qMin(qMax(qint64(0), a.size() - src_offset), total_write_length); + + // Seek to our start offset + seg_file.seek(dst_offset); + + // If we have source bytes to write, write them here + if (possible_write_length > 0) { + seg_file.write(a.data() + src_offset, possible_write_length); + } + + if (possible_write_length < total_write_length) { + // Fill remaining space with silence + QByteArray s(total_write_length - possible_write_length, 0x00); + seg_file.write(s); + } + + seg_file.close(); + + ranges_we_validated.InsertTimeRange(TimeRange(this_write_in_point, this_write_out_point)); + } else { + qWarning() << "Failed to write PCM data to" << seg_file.fileName(); + } } - // Calculate source offsets - qint64 sample_start = params_.time_to_bytes(r.in() - range.in()); - qint64 sample_len = params_.time_to_bytes(r.length()); - qint64 actual_write = qMin(sample_len, a.size() - sample_start); - - f.seek(start_offset); - f.write(a.data() + sample_start, actual_write); - - if (actual_write < sample_len) { - // Fill remaining space with silence - QByteArray s(sample_len - actual_write, 0x00); - f.write(s); + if (r.out() <= this_segment_out) { + // We've reached the end of this range, we can break out of the loop here + break; } + + // Each segment is contiguous, so this out will be the next segment's in + this_segment_in = this_segment_out; } + } - f.close(); - - Validate(range); - } else { - qWarning() << "Failed to write PCM data to" << filename_; + foreach (const TimeRange& v, ranges_we_validated) { + Validate(v); } } -void AudioPlaybackCache::WriteSilence(const TimeRange &range) +void AudioPlaybackCache::WriteSilence(const TimeRange &range, qint64 job_time) { - QFile f(filename_); - if (f.open(QFile::ReadWrite)) { - qint64 start_offset = params_.time_to_bytes(range.in()); - qint64 max_len = params_.time_to_bytes(range.out()); - qint64 write_len = max_len - start_offset; - - if (f.size() < max_len) { - f.resize(max_len); - } - - f.seek(start_offset); - - QByteArray a(write_len, 0x00); - f.write(a); - - f.close(); - - Validate(range); - } else { - qWarning() << "Failed to write PCM data to" << filename_; - } + // WritePCM will automatically fill non-existent bytes with silence, so we just have to send + // it an empty sample buffer + WritePCM(range, nullptr, job_time); } -/* -void AudioPlaybackCache::SetUuid(const QUuid &id) -{ - UpdateFilename(id.toByteArray()); -} -*/ - void AudioPlaybackCache::ShiftEvent(const rational &from, const rational &to) { - qint64 from_offset = params_.time_to_bytes(from); - qint64 to_offset = params_.time_to_bytes(to); - - if (from_offset == to_offset) { + if (from == to) { return; } - QFile f(filename_); - if (f.open(QFile::ReadWrite)) { - if (!f.size()) { - return; - } + int to_index = -1; + int from_index = -1; + rational to_start, from_start; - qint64 chunk = qAbs(to_offset - from_offset); + { + rational seg_start; - QByteArray buf(chunk, Qt::Uninitialized); + // Find which segments intersect with this shift event + for (int i=0; i from) { - // Shifting forwards, we must insert a new region and shift all the bytes there - - // For shifting forwards, we copy bytes starting at the back so that bytes we need don't - // get overwritten. - qint64 read_offset = f.size(); - - f.resize(f.size() + chunk); - - qint64 write_offset = f.size(); - - while (read_offset != from_offset) { - - // Calculate how much will be read this time - qint64 chunk_sz = qMin(chunk, read_offset - from_offset); - read_offset -= chunk_sz; - - // Read that chunk - f.seek(read_offset); - f.read(buf.data(), chunk_sz); - - // Write it at the destination - write_offset -= chunk_sz; - f.seek(write_offset); - f.write(buf.data(), chunk_sz); + rational seg_end = seg_start + seg.length(); + if (to_index == -1 && seg_end > to) { + to_index = i; + to_start = seg_start; } - // Replace remainder with silence - f.seek(from_offset); - buf.fill(0); - f.write(buf); + if (from_index == -1 && seg_end >= from) { + from_index = i; + from_start = seg_start; + } + if (to_index != -1 && from_index != -1) { + break; + } + + seg_start = seg_end; + } + } + + const rational& from_length = segments_[from_index].length(); + rational from_end = from_start + from_length; + + if (from < to) { + // Shifting forwards, we must insert a new region and split a segment in half if necessary + int insert_index; + + // Determine at what part of the array we'll be insert into + if (from == from_start) { + insert_index = from_index; } else { - // Shifting backwards, we will shift bytes and truncate - - while (from_offset != f.size()) { - // Read region to be shifted - f.seek(from_offset); - qint64 read_sz = f.read(buf.data(), buf.size()); - from_offset += read_sz; - - // Write it at the destination - f.seek(to_offset); - to_offset += f.write(buf, read_sz); - } - - // Truncate - f.resize(f.size() - chunk); - + insert_index = from_index + 1; } - f.close(); + if (from < from_end) { + // Split from segment into two + Segment second = CloneSegment(segments_.at(from_index)); + + TrimSegmentOut(&segments_[from_index], from - from_start); + TrimSegmentIn(&second, from_end - from); + + segments_.insert(insert_index, second); + } + + // Insert silent segments + rational time_to_insert = to - from; + rational inserted_time; + + while (inserted_time < time_to_insert) { + rational new_seg_sz = qMin(kDefaultSegmentSize, time_to_insert - inserted_time); + + segments_.insert(insert_index, CreateSegment(new_seg_sz)); + + inserted_time += new_seg_sz; + } + + segment_length_ += time_to_insert; + } else { - qWarning() << "Failed to write PCM data to" << filename_; + // Shifting backwards, we'll be removing segments and truncating them if necessary + const rational& to_length = segments_.at(to_index).length(); + rational to_end = to_start + to_length; + + if (from_index == to_index) { + // Shift occurs in the same segment + if (to > to_start && from < to_end) { + // Split into two and process as normal + Segment second = CloneSegment(segments_.at(to_index)); + from_index++; + segments_.insert(from_index, second); + } else if (to == to_start && from == to_end) { + RemoveSegmentFromArray(to_index); + } else if (to == to_start) { + TrimSegmentIn(&segments_[to_index], to_end - from); + } else { + TrimSegmentOut(&segments_[from_index], to - to_start); + } + } else { + // Remove all central segments (if there are any) + while (from_index > to_index + 1) { + RemoveSegmentFromArray(to_index + 1); + from_index--; + } + } + + if (from_index != to_index) { + // Remove or trim "to" segment + if (to == to_start) { + RemoveSegmentFromArray(to_index); + from_index--; + } else if (to < to_end) { + TrimSegmentOut(&segments_[to_index], to - to_start); + } + + // Remove or trim "from" segment + if (from == from_end) { + RemoveSegmentFromArray(from_index); + } else if (from > from_start) { + TrimSegmentIn(&segments_[from_index], from_end - from); + } + } + + segment_length_ -= (from - to); } } @@ -216,11 +286,96 @@ void AudioPlaybackCache::LengthChangedEvent(const rational& old, const rational& return; } - if (newlen < old) { - QFile(filename_).resize(params_.time_to_bytes(newlen)); + while (newlen < segment_length_) { + Segment& last_seg = segments_.back(); + + if (segment_length_ - last_seg.length() < newlen) { + // Truncate this segment rather than removing it + rational diff = segment_length_ - newlen; + + TrimSegmentOut(&last_seg, last_seg.length() - diff); + segment_length_ -= diff; + } else { + // Remove last segment + segment_length_ -= last_seg.length(); + RemoveSegmentFromArray(segments_.size() - 1); + } } } +AudioPlaybackCache::Segment AudioPlaybackCache::CloneSegment(const AudioPlaybackCache::Segment &s) const +{ + Segment new_seg = s; + + // Copy data to a new file + QString new_filename = GenerateSegmentFilename(); + QFile::copy(s.filename(), new_filename); + + new_seg.set_filename(new_filename); + + return new_seg; +} + +AudioPlaybackCache::Segment AudioPlaybackCache::CreateSegment(const rational &length) const +{ + Segment s(length, GenerateSegmentFilename()); + + // Create empty file + QFile f(s.filename()); + if (f.open(QFile::WriteOnly)) { + f.close(); + } + + return s; +} + +QString AudioPlaybackCache::GenerateSegmentFilename() const +{ + QString new_seg_filename; + + do { + uint32_t r = std::rand(); + new_seg_filename = QDir(GetCacheDirectory()).filePath(QStringLiteral("%1.pcm").arg(r)); + } while (QFileInfo::exists(new_seg_filename)); + + return new_seg_filename; +} + +void AudioPlaybackCache::TrimSegmentIn(AudioPlaybackCache::Segment *s, const rational &new_length) +{ + // Read filename + QFile f(s->filename()); + if (f.open(QFile::ReadWrite)) { + // Read whole segment into memory + QByteArray data = f.readAll(); + + // Trim to new length + data = data.right(params_.time_to_bytes(new_length)); + + // Clear existing file + f.resize(0); + + // Write trimmed data + f.write(data); + + f.close(); + } + + s->set_length(new_length); +} + +void AudioPlaybackCache::TrimSegmentOut(AudioPlaybackCache::Segment *s, const rational &new_length) +{ + QFile(s->filename()).resize(params_.time_to_bytes(new_length)); + s->set_length(new_length); +} + +void AudioPlaybackCache::RemoveSegmentFromArray(int index) +{ + QFile::remove(segments_.at(index).filename()); + segments_.removeAt(index); +} + QList AudioPlaybackCache::GetValidRanges(const TimeRange& range, const qint64& job_time) { QList valid_ranges; @@ -236,15 +391,110 @@ QList AudioPlaybackCache::GetValidRanges(const TimeRange& range, cons return valid_ranges; } -void AudioPlaybackCache::UpdateFilename(const QString &s) +AudioPlaybackCache::PlaybackDevice *AudioPlaybackCache::CreatePlaybackDevice(QObject* parent) const { - filename_ = QDir(GetCacheDirectory()).filePath(s); - filename_.append(QStringLiteral(".pcm")); + return new PlaybackDevice(segments_, parent); } -const QString &AudioPlaybackCache::GetPCMFilename() const +AudioPlaybackCache::Segment::Segment(const rational &length, const QString &s) { - return filename_; + length_ = length; + filename_ = s; +} + +AudioPlaybackCache::PlaybackDevice::PlaybackDevice(const AudioPlaybackCache::Playlist &playlist, QObject *parent) : + QIODevice(parent), + playlist_(playlist), + current_segment_(0), + segment_read_index_(0) +{ +} + +AudioPlaybackCache::PlaybackDevice::~PlaybackDevice() +{ + close(); +} + +bool AudioPlaybackCache::PlaybackDevice::seek(qint64 pos) +{ + // Default behavior + QIODevice::seek(pos); + + // Find which segment we're in + // FIXME: Inefficient + qint64 seg_start_bytes = 0; + for (int i=0; i GetValidRanges(const TimeRange &range, const qint64 &job_time); + class Segment + { + public: + Segment() = default; + Segment(const rational& length, const QString& s); + + const rational& length() const + { + return length_; + } + + void set_length(const rational& length) + { + length_ = length; + } + + const QString& filename() const + { + return filename_; + } + + void set_filename(const QString& filename) + { + filename_ = filename; + } + + private: + QString filename_; + + rational length_; + + }; + + using Playlist = QVector; + + class PlaybackDevice : public QIODevice + { + public: + PlaybackDevice(const Playlist& playlist, QObject* parent = nullptr); + + virtual ~PlaybackDevice() override; + + virtual bool isSequential() const override + { + return false; + } + + virtual bool seek(qint64 pos) override; + + virtual qint64 size() const override; + + virtual qint64 readData(char *data, qint64 maxSize) override; + + virtual qint64 writeData(const char *data, qint64 maxSize) override + { + Q_UNUSED(data) + Q_UNUSED(maxSize) + + return -1; + } + + private: + Playlist playlist_; + + int current_segment_; + + qint64 segment_read_index_; + + }; + + PlaybackDevice* CreatePlaybackDevice(QObject *parent = nullptr) const; + signals: void ParametersChanged(); @@ -59,9 +129,23 @@ protected: virtual void LengthChangedEvent(const rational& old, const rational& newlen) override; private: - void UpdateFilename(const QString& s); + static const rational kDefaultSegmentSize; - QString filename_; + Segment CloneSegment(const Segment& s) const; + + Segment CreateSegment(const rational& length) const; + + QString GenerateSegmentFilename() const; + + void TrimSegmentIn(Segment* s, const rational& new_length); + + void TrimSegmentOut(Segment* s, const rational& new_length); + + void RemoveSegmentFromArray(int index); + + Playlist segments_; + + rational segment_length_; AudioParams params_; diff --git a/app/render/colormanager.cpp b/app/render/colormanager.cpp index 7c713edd6..2e4f496dd 100644 --- a/app/render/colormanager.cpp +++ b/app/render/colormanager.cpp @@ -22,6 +22,7 @@ #include #include +#include #include "common/define.h" #include "common/filefunctions.h" @@ -82,7 +83,7 @@ void ColorManager::SetUpDefaultConfig() } // Extract OCIO config - kind of hacky, but it'll work - QString dir = QDir(FileFunctions::GetTempFilePath()).filePath(QStringLiteral("ocioconf")); + QString dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)).filePath(QStringLiteral("ocioconf")); FileFunctions::CopyDirectory(QStringLiteral(":/ocioconf"), dir, diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 6b1739622..28215e856 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -108,7 +108,7 @@ bool ExportTask::Run() if (params_.audio_enabled()) { // Write audio data now - encoder_->WriteAudio(audio_params(), audio_data_.GetPCMFilename()); + encoder_->WriteAudio(audio_params(), audio_data_.CreatePlaybackDevice(encoder_)); } encoder_->Close(); diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 8e3bf3241..979884928 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -25,6 +25,7 @@ #include #include "common/xmlutils.h" +#include "core.h" OLIVE_NAMESPACE_ENTER @@ -45,7 +46,17 @@ bool ProjectLoadTask::Run() if (reader.name() == QStringLiteral("olive")) { while(XMLReadNextStartElement(&reader)) { if (reader.name() == QStringLiteral("version")) { - qDebug() << "Project version:" << reader.readElementText(); + uint project_version = reader.readElementText().toUInt(); + + if (project_version > Core::kProjectVersion) { + // Project is newer than we support + SetError(tr("This project is newer than this version of Olive and cannot be opened.")); + return false; + } else if (project_version < 201003) { // Change this if we drop support for a project version + // Project is older than we support + SetError(tr("This project is from a version of Olive that is no longer supported in this version.")); + return false; + } } else if (reader.name() == QStringLiteral("url")) { project_saved_url_ = reader.readElementText(); } else if (reader.name() == QStringLiteral("project")) { @@ -62,6 +73,11 @@ bool ProjectLoadTask::Run() reader.skipCurrentElement(); } } + } else if (reader.name() == QStringLiteral("project")) { + // 0.1 projects use "project" as the root instead of Olive. We don't currently support + // these projects + SetError(tr("This project is from a version of Olive that is no longer supported in this version.")); + return false; } else { reader.skipCurrentElement(); } diff --git a/app/task/project/save/save.cpp b/app/task/project/save/save.cpp index 22cca573a..4ee1b1fc0 100644 --- a/app/task/project/save/save.cpp +++ b/app/task/project/save/save.cpp @@ -25,6 +25,7 @@ #include #include "common/filefunctions.h" +#include "core.h" OLIVE_NAMESPACE_ENTER @@ -49,7 +50,9 @@ bool ProjectSaveTask::Run() writer.writeStartElement("olive"); - writer.writeTextElement("version", "0.2.0"); + // Version is stored in YYMMDD from whenever the project format was last changed + // Allows easy integer math for checking project versions. + writer.writeTextElement("version", QString::number(Core::kProjectVersion)); writer.writeTextElement("url", project_->filename()); diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index abeff4120..bd813ac19 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -122,7 +122,7 @@ void RenderTask::Render(const TimeRangeList& video_range, || !download_futures.empty() || !audio_lookup_table.empty())) { - if (!IsCancelled() && !frame_queue.empty()) { + while (!IsCancelled() && !frame_queue.empty()) { // Pop another frame off the frame queue const HashTimePair& p = frame_queue.front(); @@ -168,7 +168,7 @@ void RenderTask::Render(const TimeRangeList& video_range, frame_queue.pop_front(); } - if (!IsCancelled() && !audio_queue.empty()) { + while (!IsCancelled() && !audio_queue.empty()) { audio_lookup_table.push_back({audio_queue.front(), backend_->RenderAudio(audio_queue.front())}); audio_queue.pop_front(); } diff --git a/app/timeline/trackreference.cpp b/app/timeline/trackreference.cpp index 6c046df7a..7080ea186 100644 --- a/app/timeline/trackreference.cpp +++ b/app/timeline/trackreference.cpp @@ -49,6 +49,11 @@ bool TrackReference::operator==(const TrackReference &ref) const return type_ == ref.type_ && index_ == ref.index_; } +bool TrackReference::operator!=(const TrackReference &ref) const +{ + return !(*this == ref); +} + uint qHash(const TrackReference &r, uint seed) { // Not super efficient, but couldn't think of any better way to ensure a different hash each time diff --git a/app/timeline/trackreference.h b/app/timeline/trackreference.h index 842985168..7ae13d444 100644 --- a/app/timeline/trackreference.h +++ b/app/timeline/trackreference.h @@ -40,10 +40,13 @@ public: bool operator==(const TrackReference& ref) const; + bool operator!=(const TrackReference& ref) const; + private: Timeline::TrackType type_; int index_; + }; uint qHash(const TrackReference& r, uint seed); diff --git a/app/widget/CMakeLists.txt b/app/widget/CMakeLists.txt index 1a05cbe55..17b954970 100644 --- a/app/widget/CMakeLists.txt +++ b/app/widget/CMakeLists.txt @@ -29,9 +29,10 @@ add_subdirectory(manageddisplay) add_subdirectory(menu) add_subdirectory(nodecombobox) add_subdirectory(nodecopypaste) -add_subdirectory(nodeview) -add_subdirectory(nodeparamview) add_subdirectory(nodetableview) +add_subdirectory(nodetreeview) +add_subdirectory(nodeparamview) +add_subdirectory(nodeview) add_subdirectory(panel) add_subdirectory(path) add_subdirectory(pixelsampler) diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp index a4cbd853d..2ce1f50a5 100644 --- a/app/widget/audiomonitor/audiomonitor.cpp +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -35,6 +35,7 @@ const int kMaximumSmoothness = 8; AudioMonitor::AudioMonitor(QWidget *parent) : QOpenGLWidget(parent), + file_(nullptr), cached_channels_(0) { values_.resize(kMaximumSmoothness); @@ -45,11 +46,6 @@ AudioMonitor::AudioMonitor(QWidget *parent) : connect(AudioManager::instance(), &AudioManager::Stopped, this, &AudioMonitor::Stop); } -AudioMonitor::~AudioMonitor() -{ - Stop(); -} - void AudioMonitor::SetParams(const AudioParams ¶ms) { params_ = params; @@ -63,18 +59,19 @@ void AudioMonitor::SetParams(const AudioParams ¶ms) peaked_.fill(false); } -void AudioMonitor::OutputDeviceSet(const QString &filename, qint64 offset, int playback_speed) +void AudioMonitor::OutputDeviceSet(AudioPlaybackCache *cache, qint64 offset, int playback_speed) { Stop(); - file_.setFileName(filename); + file_ = cache->CreatePlaybackDevice(this); - if (!file_.open(QFile::ReadOnly)) { - qWarning() << "Failed to open" << filename; + if (!file_->open(QFile::ReadOnly)) { + qWarning() << "Failed to open IO device for AudioMonitor display"; + Stop(); return; } - file_.seek(offset); + file_->seek(offset); playback_speed_ = playback_speed; @@ -85,9 +82,8 @@ void AudioMonitor::OutputDeviceSet(const QString &filename, qint64 offset, int p void AudioMonitor::Stop() { - if (file_.isOpen()) { - file_.close(); - } + delete file_; + file_ = nullptr; } void AudioMonitor::OutputPushed(const QByteArray &d) @@ -215,7 +211,7 @@ void AudioMonitor::paintGL() QVector v(params_.channel_count(), 0); - if (file_.isOpen()) { + if (file_) { UpdateValuesFromFile(v); } @@ -258,7 +254,7 @@ void AudioMonitor::paintGL() } } - if (all_zeroes && !file_.isOpen()) { + if (all_zeroes && !file_) { // Optimize by disabling the update loop SetUpdateLoop(false); } @@ -287,18 +283,18 @@ void AudioMonitor::UpdateValuesFromFile(QVector& v) if (playback_speed_ < 0) { // If reversing, jump back by the amount of bytes we're going to read - bytes_to_read = qMin(bytes_to_read, file_.pos()); + bytes_to_read = qMin(bytes_to_read, file_->pos()); - file_.seek(file_.pos() - bytes_to_read); + file_->seek(file_->pos() - bytes_to_read); } // Read bytes in from file - QByteArray b = file_.read(bytes_to_read); + QByteArray b = file_->read(bytes_to_read); if (playback_speed_ < 0) { // If reversing, head back to where we were before the read so that the next read starts // from where we left off - file_.seek(file_.pos() - bytes_to_read); + file_->seek(file_->pos() - bytes_to_read); } // If speed is not 1, transform it here diff --git a/app/widget/audiomonitor/audiomonitor.h b/app/widget/audiomonitor/audiomonitor.h index 2b290ff14..9a0331301 100644 --- a/app/widget/audiomonitor/audiomonitor.h +++ b/app/widget/audiomonitor/audiomonitor.h @@ -27,6 +27,7 @@ #include "common/define.h" #include "render/audioparams.h" +#include "render/audioplaybackcache.h" OLIVE_NAMESPACE_ENTER @@ -36,12 +37,10 @@ class AudioMonitor : public QOpenGLWidget public: AudioMonitor(QWidget* parent = nullptr); - virtual ~AudioMonitor() override; - public slots: void SetParams(const AudioParams& params); - void OutputDeviceSet(const QString& filename, qint64 offset, int playback_speed); + void OutputDeviceSet(AudioPlaybackCache* cache, qint64 offset, int playback_speed); void Stop(); @@ -66,7 +65,7 @@ private: AudioParams params_; - QFile file_; + QIODevice* file_; qint64 last_time_; int playback_speed_; diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index e8b6884ef..a9b43d4a9 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -60,19 +60,58 @@ void CurveView::Clear() lines_.clear(); } -void CurveView::SetTrackCount(int count) +void CurveView::ConnectInput(NodeInput *input) { - track_count_ = count; + if (connected_inputs_.contains(input)) { + // Input wasn't connected, do nothing + return; + } - track_visible_.resize(track_count_); - track_visible_.fill(true); + // Add keyframes from this input + foreach (const NodeInput::KeyframeTrack& track, input->keyframe_tracks()) { + foreach (NodeKeyframePtr key, track) { + this->AddKeyframe(key); + } + + if (!keyframe_colors_.contains(&track)) { + // Generate a random color for this input + keyframe_colors_.insert(&track, QColor::fromHsv(std::rand()%360, std::rand()%255, 255)); + } + } + + // Append to the list + connected_inputs_.append(input); + + // Connect add/remove signals + connect(input, &NodeInput::KeyframeAdded, this, &CurveView::AddKeyframe); + connect(input, &NodeInput::KeyframeRemoved, this, &CurveView::RemoveKeyframe); } -void CurveView::SetTrackVisible(int track, bool visible) +void CurveView::DisconnectNode(Node *node) { - track_visible_[track] = visible; + QList inputs = node->GetInputsIncludingArrays(); - SetKeyframeTrackVisible(track, visible); + foreach (NodeInput* i, inputs) { + DisconnectInput(i); + } +} + +void CurveView::DisconnectInput(NodeInput *input) +{ + if (!connected_inputs_.contains(input)) { + // Input wasn't connected, do nothing + return; + } + + // Remove keyframes belonging to this input + RemoveKeyframesOfInput(input); + + // Remove from the list + connected_inputs_.removeOne(input); + + // Disconnect add/remove signals + disconnect(input, &NodeInput::KeyframeAdded, this, &CurveView::AddKeyframe); + disconnect(input, &NodeInput::KeyframeRemoved, this, &CurveView::RemoveKeyframe); } void CurveView::drawBackground(QPainter *painter, const QRectF &rect) @@ -126,87 +165,85 @@ void CurveView::drawBackground(QPainter *painter, const QRectF &rect) painter->drawLines(lines); // Draw keyframe lines + foreach (NodeInput* input, connected_inputs_) { + if (input->is_keyframing()) { + foreach (const NodeInput::KeyframeTrack& track, input->keyframe_tracks()) { + if (!track.isEmpty()) { + painter->setPen(QPen(keyframe_colors_.value(&track), qMax(1, fontMetrics().height() / 4))); - for (int j=0;j keyframe_lines; - painter->setPen(QPen(GetKeyframeColor(j), qMax(1, fontMetrics().height() / 4))); - QList keys = GetKeyframesSortedByTime(j); + // Draw straight line leading to first keyframe + QPointF first_key_pos = item_map().value(track.first().get())->pos(); + keyframe_lines.append(QLineF(QPointF(scene_bottom_left.x(), first_key_pos.y()), first_key_pos)); - if (!keys.isEmpty()) { - QVector keyframe_lines; + // Draw lines between each keyframe + for (int i=1;ipos(); - keyframe_lines.append(QLineF(QPointF(scene_bottom_left.x(), first_key_pos.y()), first_key_pos)); + KeyframeViewItem* before_item = item_map().value(before.get()); + KeyframeViewItem* after_item = item_map().value(after.get()); - // Draw lines between each keyframe - for (int i=1;itype() == NodeKeyframe::kHold) { + // Draw a hold keyframe (basically a right angle) + keyframe_lines.append(QLineF(before_item->pos().x(), + before_item->pos().y(), + after_item->pos().x(), + before_item->pos().y())); + keyframe_lines.append(QLineF(after_item->pos().x(), + before_item->pos().y(), + after_item->pos().x(), + after_item->pos().y())); + } else if (before->type() == NodeKeyframe::kBezier && after->type() == NodeKeyframe::kBezier) { + // Draw a cubic bezier - KeyframeViewItem* before_item = item_map().value(before); - KeyframeViewItem* after_item = item_map().value(after); + // Cubic beziers have two control points, so we can just use both + QPointF before_control_point = before_item->pos() + ScalePoint(before->bezier_control_out()); + QPointF after_control_point = after_item->pos() + ScalePoint(after->bezier_control_in()); - if (before->type() == NodeKeyframe::kHold) { - // Draw a hold keyframe (basically a right angle) - keyframe_lines.append(QLineF(before_item->pos().x(), - before_item->pos().y(), - after_item->pos().x(), - before_item->pos().y())); - keyframe_lines.append(QLineF(after_item->pos().x(), - before_item->pos().y(), - after_item->pos().x(), - after_item->pos().y())); - } else if (before->type() == NodeKeyframe::kBezier && after->type() == NodeKeyframe::kBezier) { - // Draw a cubic bezier + QPainterPath path; + path.moveTo(before_item->pos()); + path.cubicTo(before_control_point, after_control_point, after_item->pos()); + painter->drawPath(path); - // Cubic beziers have two control points, so we can just use both - QPointF before_control_point = before_item->pos() + ScalePoint(before->bezier_control_out()); - QPointF after_control_point = after_item->pos() + ScalePoint(after->bezier_control_in()); + } else if (before->type() == NodeKeyframe::kBezier || after->type() == NodeKeyframe::kBezier) { + // Draw a quadratic bezier - QPainterPath path; - path.moveTo(before_item->pos()); - path.cubicTo(before_control_point, after_control_point, after_item->pos()); - painter->drawPath(path); + // Quadratic beziers have a single control point, we just have to determine which it is + QPointF key_anchor; + QPointF control_point; - } else if (before->type() == NodeKeyframe::kBezier || after->type() == NodeKeyframe::kBezier) { - // Draw a quadratic bezier + if (before->type() == NodeKeyframe::kBezier) { + key_anchor = before_item->pos(); + control_point = before->bezier_control_out(); + } else { + key_anchor = after_item->pos(); + control_point = after->bezier_control_in(); + } - // Quadratic beziers have a single control point, we just have to determine which it is - QPointF key_anchor; - QPointF control_point; + // Scale control point + control_point = key_anchor + ScalePoint(control_point); - if (before->type() == NodeKeyframe::kBezier) { - key_anchor = before_item->pos(); - control_point = before->bezier_control_out(); - } else { - key_anchor = after_item->pos(); - control_point = after->bezier_control_in(); + // Create the path from both keyframes + QPainterPath path; + path.moveTo(before_item->pos()); + path.quadTo(control_point, after_item->pos()); + painter->drawPath(path); + + } else { + // Linear to linear + keyframe_lines.append(QLineF(before_item->pos(), after_item->pos())); + } } - // Scale control point - control_point = key_anchor + ScalePoint(control_point); + // Draw straight line leading from end keyframe + QPointF last_key_pos = item_map().value(track.last().get())->pos(); + keyframe_lines.append(QLineF(last_key_pos, QPointF(scene_top_right.x(), last_key_pos.y()))); - // Create the path from both keyframes - QPainterPath path; - path.moveTo(before_item->pos()); - path.quadTo(control_point, after_item->pos()); - painter->drawPath(path); - - } else { - // Linear to linear - keyframe_lines.append(QLineF(before_item->pos(), after_item->pos())); + painter->drawLines(keyframe_lines); } } - - // Draw straight line leading from end keyframe - QPointF last_key_pos = item_map().value(keys.last())->pos(); - keyframe_lines.append(QLineF(last_key_pos, QPointF(scene_top_right.x(), last_key_pos.y()))); - - painter->drawLines(keyframe_lines); } } @@ -269,37 +306,6 @@ void CurveView::ContextMenuEvent(Menu &m) //QAction* reset_zoom_action = m.addAction(tr("Reset Zoom")); } -QList CurveView::GetKeyframesSortedByTime(int track) -{ - QList sorted; - - QMap::const_iterator iterator; - - for (iterator=item_map().begin();iterator!=item_map().end();iterator++) { - NodeKeyframe* key = iterator.key(); - - if (key->track() != track) { - continue; - } - - bool inserted = false; - - for (int i=0;itime() > key->time()) { - sorted.insert(i, key); - inserted = true; - break; - } - } - - if (!inserted) { - sorted.append(key); - } - } - - return sorted; -} - qreal CurveView::GetItemYFromKeyframeValue(NodeKeyframe *key) { return GetItemYFromKeyframeValue(key->value().toDouble()); @@ -334,17 +340,6 @@ void CurveView::CreateBezierControlPoints(KeyframeViewItem* item) connect(bezier_out_pt, &QObject::destroyed, this, &CurveView::BezierControlPointDestroyed, Qt::DirectConnection); } -QColor CurveView::GetKeyframeColor(int track) const -{ - if (track_count_) { - QColor c; - c.setHsvF(static_cast(track) / static_cast(track_count_), 0.5, 1.0); - return c; - } - - return palette().text().color(); -} - void CurveView::KeyframeValueChanged() { NodeKeyframe* key = static_cast(sender()); @@ -396,15 +391,13 @@ void CurveView::ZoomToFit() return; } - QMap::const_iterator i; - rational min_time = RATIONAL_MAX; rational max_time = RATIONAL_MIN; double min_val = DBL_MAX; double max_val = DBL_MIN; - for (i=item_map().constBegin(); i!=item_map().constEnd(); i++) { + for (auto i=item_map().constBegin(); i!=item_map().constEnd(); i++) { rational transformed_time = GetAdjustedTime(i.key()->parent()->parentNode(), GetTimeTarget(), i.key()->time(), @@ -432,7 +425,7 @@ void CurveView::AddKeyframe(NodeKeyframePtr key) { KeyframeViewItem* item = AddKeyframeInternal(key); SetItemYFromKeyframeValue(key.get(), item); - item->SetOverrideBrush(GetKeyframeColor(key->track())); + item->SetOverrideBrush(keyframe_colors_.value(&key->parent()->keyframe_tracks().at(key->track()))); connect(key.get(), &NodeKeyframe::ValueChanged, this, &CurveView::KeyframeValueChanged); connect(key.get(), &NodeKeyframe::TypeChanged, this, &CurveView::KeyframeTypeChanged); diff --git a/app/widget/curvewidget/curveview.h b/app/widget/curvewidget/curveview.h index e051f4f04..762a015c5 100644 --- a/app/widget/curvewidget/curveview.h +++ b/app/widget/curvewidget/curveview.h @@ -38,9 +38,11 @@ public: virtual void Clear() override; - void SetTrackCount(int count); + void ConnectInput(NodeInput* input); - void SetTrackVisible(int track, bool visible); + void DisconnectNode(Node* node); + + void DisconnectInput(NodeInput* input); public slots: void AddKeyframe(NodeKeyframePtr key); @@ -61,8 +63,6 @@ protected: virtual void ContextMenuEvent(Menu &m) override; private: - QList GetKeyframesSortedByTime(int track); - qreal GetItemYFromKeyframeValue(NodeKeyframe* key); qreal GetItemYFromKeyframeValue(double value); @@ -74,7 +74,7 @@ private: void CreateBezierControlPoints(KeyframeViewItem *item); - QColor GetKeyframeColor(int track) const; + QMap keyframe_colors_; int text_padding_; @@ -84,9 +84,7 @@ private: QList bezier_control_points_; - QVector track_visible_; - - int track_count_; + QList connected_inputs_; private slots: void KeyframeValueChanged(); diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index da32088ec..41d2a88ef 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "core.h" @@ -34,12 +35,23 @@ OLIVE_NAMESPACE_ENTER CurveWidget::CurveWidget(QWidget *parent) : - TimeBasedWidget(parent), - input_(nullptr), - bridge_(nullptr) + TimeBasedWidget(parent) { - QVBoxLayout* layout = new QVBoxLayout(this); + QHBoxLayout* outer_layout = new QHBoxLayout(this); + + QSplitter* splitter = new QSplitter(); + outer_layout->addWidget(splitter); + + tree_view_ = new NodeTreeView(); + tree_view_->SetOnlyShowKeyframable(true); + connect(tree_view_, &NodeTreeView::NodeEnableChanged, this, &CurveWidget::NodeEnabledChanged); + connect(tree_view_, &NodeTreeView::InputEnableChanged, this, &CurveWidget::InputEnabledChanged); + splitter->addWidget(tree_view_); + + QWidget* workarea = new QWidget(); + QVBoxLayout* layout = new QVBoxLayout(workarea); layout->setMargin(0); + splitter->addWidget(workarea); QHBoxLayout* top_controls = new QHBoxLayout(); @@ -92,12 +104,8 @@ CurveWidget::CurveWidget(QWidget *parent) : view_->setHorizontalScrollBar(scrollbar()); connect(view_->horizontalScrollBar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); - widget_bridge_layout_ = new QHBoxLayout(); - widget_bridge_layout_->addStretch(); - input_label_ = new QLabel(); - widget_bridge_layout_->addWidget(input_label_); - widget_bridge_layout_->addStretch(); - layout->addLayout(widget_bridge_layout_); + // Disable collapsing the main curve view (but allow collapsing the tree) + splitter->setCollapsible(1, false); SetScale(120.0); } @@ -108,71 +116,6 @@ CurveWidget::~CurveWidget() view_->Clear(); } -NodeInput *CurveWidget::GetInput() const -{ - return input_; -} - -void CurveWidget::SetInput(NodeInput *input) -{ - if (bridge_) { - foreach (QWidget* bridge_widget, bridge_->widgets()) { - bridge_widget->deleteLater(); - } - bridge_->deleteLater(); - bridge_ = nullptr; - } - - foreach (QCheckBox* box, checkboxes_) { - box->deleteLater(); - } - checkboxes_.clear(); - - if (input_) { - disconnect(input_, &NodeInput::KeyframeAdded, view_, &CurveView::AddKeyframe); - disconnect(input_, &NodeInput::KeyframeRemoved, view_, &CurveView::RemoveKeyframe); - } - - view_->Clear(); - - input_ = input; - key_control_->SetInput(input_); - - if (input_) { - view_->SetTrackCount(input_->get_number_of_keyframe_tracks()); - - bridge_ = new NodeParamViewWidgetBridge(input_, this); - - bridge_->SetTimeTarget(GetTimeTarget()); - - for (int i=0;iwidgets().size();i++) { - // Insert between two stretches to center the widget - QCheckBox* checkbox = new QCheckBox(); - checkbox->setChecked(true); - widget_bridge_layout_->insertWidget(2 + i*2, checkbox); - checkboxes_.append(checkbox); - connect(checkbox, &QCheckBox::clicked, this, [this](bool e){ - view_->SetTrackVisible(checkboxes_.indexOf(static_cast(sender())), e); - }); - - widget_bridge_layout_->insertWidget(2 + i*2 + 1, bridge_->widgets().at(i)); - } - - connect(input_, &NodeInput::KeyframeAdded, view_, &CurveView::AddKeyframe); - connect(input_, &NodeInput::KeyframeRemoved, view_, &CurveView::RemoveKeyframe); - - foreach (const NodeInput::KeyframeTrack& track, input_->keyframe_tracks()) { - foreach (NodeKeyframePtr key, track) { - view_->AddKeyframe(key); - } - } - } - - UpdateInputLabel(); - - QMetaObject::invokeMethod(view_, "ZoomToFit", Qt::QueuedConnection); -} - const double &CurveWidget::GetVerticalScale() { return view_->GetYScale(); @@ -188,12 +131,26 @@ void CurveWidget::DeleteSelected() view_->DeleteSelected(); } -void CurveWidget::changeEvent(QEvent *e) +void CurveWidget::SetNodes(const QList &nodes) { - if (e->type() == QEvent::LanguageChange) { - UpdateInputLabel(); + tree_view_->SetNodes(nodes); + + // Detect removed nodes + foreach (Node* n, nodes_) { + if (!nodes.contains(n)) { + view_->DisconnectNode(n); + } } - QWidget::changeEvent(e); + + // Detect added nodes + foreach (Node* n, nodes) { + if (tree_view_->IsNodeEnabled(n) && !nodes_.contains(n)) { + ConnectNode(n); + } + } + + // Save new node list + nodes_ = nodes; } void CurveWidget::TimeChangedEvent(const int64_t ×tamp) @@ -223,10 +180,6 @@ void CurveWidget::TimeTargetChangedEvent(Node *target) key_control_->SetTimeTarget(target); view_->SetTimeTarget(target); - - if (bridge_) { - bridge_->SetTimeTarget(target); - } } void CurveWidget::ConnectedNodeChanged(ViewerOutput *n) @@ -234,15 +187,6 @@ void CurveWidget::ConnectedNodeChanged(ViewerOutput *n) SetTimeTarget(n); } -void CurveWidget::UpdateInputLabel() -{ - if (input_) { - input_label_->setText(QStringLiteral("%1 :: %2:").arg(input_->parentNode()->Name(), input_->name())); - } else { - input_label_->clear(); - } -} - void CurveWidget::SetKeyframeButtonEnabled(bool enable) { linear_button_->setEnabled(enable); @@ -266,15 +210,26 @@ void CurveWidget::SetKeyframeButtonCheckedFromType(NodeKeyframe::Type type) void CurveWidget::UpdateBridgeTime(const int64_t ×tamp) { - if (!input_) { - return; - } - rational time = Timecode::timestamp_to_time(timestamp, view_->timebase()); - bridge_->SetTime(time); key_control_->SetTime(time); } +void CurveWidget::ConnectNode(Node *n) +{ + QList inputs = n->GetInputsIncludingArrays(); + + foreach (NodeInput* i, inputs) { + if (tree_view_->IsInputEnabled(i)) { + view_->ConnectInput(i); + } + } +} + +void CurveWidget::DisconnectNode(Node *n) +{ + view_->DisconnectNode(n); +} + void CurveWidget::SelectionChanged() { QList selected = view_->scene()->selectedItems(); @@ -349,4 +304,22 @@ void CurveWidget::KeyControlRequestedTimeChanged(const rational &time) SetTimeAndSignal(Timecode::time_to_timestamp(time, view_->timebase())); } +void CurveWidget::NodeEnabledChanged(Node* n, bool e) +{ + if (e) { + ConnectNode(n); + } else { + DisconnectNode(n); + } +} + +void CurveWidget::InputEnabledChanged(NodeInput *i, bool e) +{ + if (e) { + view_->ConnectInput(i); + } else { + view_->DisconnectInput(i); + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index 7c33b49c7..253ca6a71 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -30,6 +30,7 @@ #include "node/input.h" #include "widget/nodeparamview/nodeparamviewkeyframecontrol.h" #include "widget/nodeparamview/nodeparamviewwidgetbridge.h" +#include "widget/nodetreeview/nodetreeview.h" #include "widget/timebased/timebased.h" OLIVE_NAMESPACE_ENTER @@ -42,17 +43,15 @@ public: virtual ~CurveWidget() override; - NodeInput* GetInput() const; - void SetInput(NodeInput* input); - const double& GetVerticalScale(); void SetVerticalScale(const double& vscale); void DeleteSelected(); -protected: - virtual void changeEvent(QEvent *) override; +public slots: + void SetNodes(const QList& nodes); +protected: virtual void TimeChangedEvent(const int64_t &) override; virtual void TimebaseChangedEvent(const rational &) override; virtual void ScaleChangedEvent(const double &) override; @@ -62,8 +61,6 @@ protected: virtual void ConnectedNodeChanged(ViewerOutput* n) override; private: - void UpdateInputLabel(); - void SetKeyframeButtonEnabled(bool enable); void SetKeyframeButtonChecked(bool checked); @@ -72,6 +69,12 @@ private: void UpdateBridgeTime(const int64_t& timestamp); + void ConnectNode(Node* n); + + void DisconnectNode(Node* n); + + NodeTreeView* tree_view_; + QPushButton* linear_button_; QPushButton* bezier_button_; @@ -80,18 +83,12 @@ private: CurveView* view_; - NodeInput* input_; - - QLabel* input_label_; - - QHBoxLayout* widget_bridge_layout_; - - NodeParamViewWidgetBridge* bridge_; - NodeParamViewKeyframeControl* key_control_; QList checkboxes_; + QList nodes_; + private slots: void SelectionChanged(); @@ -99,6 +96,10 @@ private slots: void KeyControlRequestedTimeChanged(const rational& time); + void NodeEnabledChanged(Node* n, bool e); + + void InputEnabledChanged(NodeInput* i, bool e); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/keyframeview/keyframeviewbase.cpp b/app/widget/keyframeview/keyframeviewbase.cpp index 56ba3078d..b53a0c236 100644 --- a/app/widget/keyframeview/keyframeviewbase.cpp +++ b/app/widget/keyframeview/keyframeviewbase.cpp @@ -79,10 +79,15 @@ void KeyframeViewBase::RemoveKeyframesOfNode(Node *n) QList inputs = n->GetInputsIncludingArrays(); foreach (NodeInput* i, inputs) { - foreach (const NodeInput::KeyframeTrack& track, i->keyframe_tracks()) { - foreach (NodeKeyframePtr key, track) { - RemoveKeyframe(key); - } + RemoveKeyframesOfInput(i); + } +} + +void KeyframeViewBase::RemoveKeyframesOfInput(NodeInput *i) +{ + foreach (const NodeInput::KeyframeTrack& track, i->keyframe_tracks()) { + foreach (NodeKeyframePtr key, track) { + RemoveKeyframe(key); } } } @@ -104,10 +109,6 @@ KeyframeViewItem *KeyframeViewBase::AddKeyframeInternal(NodeKeyframePtr key) item->SetScale(GetScale()); item_map_.insert(key.get(), item); scene()->addItem(item); - - if (hidden_tracks_.contains(key->track())) { - item->setVisible(false); - } } return item; @@ -301,27 +302,6 @@ void KeyframeViewBase::TimeTargetChangedEvent(Node *target) } } -void KeyframeViewBase::SetKeyframeTrackVisible(int track, bool visible) -{ - if (!visible == hidden_tracks_.contains(track)) { - return; - } - - QMap::const_iterator i; - - for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { - if (i.key()->track() == track) { - i.value()->setVisible(visible); - } - } - - if (visible) { - hidden_tracks_.removeOne(track); - } else { - hidden_tracks_.append(track); - } -} - void KeyframeViewBase::ContextMenuEvent(Menu& m) { Q_UNUSED(m) diff --git a/app/widget/keyframeview/keyframeviewbase.h b/app/widget/keyframeview/keyframeviewbase.h index 9cbe0b01f..f25a66937 100644 --- a/app/widget/keyframeview/keyframeviewbase.h +++ b/app/widget/keyframeview/keyframeviewbase.h @@ -42,6 +42,8 @@ public: void RemoveKeyframesOfNode(Node* n); + void RemoveKeyframesOfInput(NodeInput* i); + public slots: void RemoveKeyframe(NodeKeyframePtr key); @@ -60,8 +62,6 @@ protected: virtual void TimeTargetChangedEvent(Node*) override; - void SetKeyframeTrackVisible(int track, bool visible); - virtual void ContextMenuEvent(Menu &m); private: @@ -96,8 +96,6 @@ private: bool currently_autoselecting_; - QList hidden_tracks_; - private slots: void ShowContextMenu(); diff --git a/app/widget/menu/menu.cpp b/app/widget/menu/menu.cpp index 5d1f7321c..9de1136f5 100644 --- a/app/widget/menu/menu.cpp +++ b/app/widget/menu/menu.cpp @@ -100,16 +100,14 @@ void Menu::ConformItem(QAction *a, const QString &id, const QString &key) } } -void Menu::SetBooleanAction(QAction *a, bool* boolean) -{ - // FIXME: Connect to some boolean function - a->setCheckable(true); - a->setChecked(*boolean); - a->setProperty("boolptr", reinterpret_cast(boolean)); -} - void Menu::Init() { + // HACK: Disables embossing on disabled text for a slightly nicer UI + QPalette p = palette(); + p.setColor(QPalette::Disabled, QPalette::Light, QColor(0, 0, 0, 0)); + setPalette(p); + + // If a native palette is available, it should override the one above StyleManager::UseOSNativeStyling(this); } diff --git a/app/widget/menu/menu.h b/app/widget/menu/menu.h index 777143efc..142753402 100644 --- a/app/widget/menu/menu.h +++ b/app/widget/menu/menu.h @@ -226,8 +226,6 @@ public: const QString& id, const QString& key = QString()); - static void SetBooleanAction(QAction* a, bool *boolean); - private: void Init(); diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 4e6a377d7..a63e22fb3 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -58,6 +58,34 @@ MenuShared::MenuShared() clip_enable_disable_item_ = Menu::CreateItem(this, "enabledisable", this, &MenuShared::EnableDisableTriggered, "Shift+E"); clip_nest_item_ = Menu::CreateItem(this, "nest", this, &MenuShared::NestTriggered); + // TimeRuler menu shared items + frame_view_mode_group_ = new QActionGroup(this); + + view_timecode_view_dropframe_item_ = Menu::CreateItem(this, "modedropframe", this, &MenuShared::TimecodeDisplayTriggered); + view_timecode_view_dropframe_item_->setData(Timecode::kTimecodeDropFrame); + view_timecode_view_dropframe_item_->setCheckable(true); + frame_view_mode_group_->addAction(view_timecode_view_dropframe_item_); + + view_timecode_view_nondropframe_item_ = Menu::CreateItem(this, "modenondropframe", this, &MenuShared::TimecodeDisplayTriggered); + view_timecode_view_nondropframe_item_->setData(Timecode::kTimecodeNonDropFrame); + view_timecode_view_nondropframe_item_->setCheckable(true); + frame_view_mode_group_->addAction(view_timecode_view_nondropframe_item_); + + view_timecode_view_seconds_item_ = Menu::CreateItem(this, "modeseconds", this, &MenuShared::TimecodeDisplayTriggered); + view_timecode_view_seconds_item_->setData(Timecode::kTimecodeSeconds); + view_timecode_view_seconds_item_->setCheckable(true); + frame_view_mode_group_->addAction(view_timecode_view_seconds_item_); + + view_timecode_view_frames_item_ = Menu::CreateItem(this, "modeframes", this, &MenuShared::TimecodeDisplayTriggered); + view_timecode_view_frames_item_->setData(Timecode::kFrames); + view_timecode_view_frames_item_->setCheckable(true); + frame_view_mode_group_->addAction(view_timecode_view_frames_item_); + + view_timecode_view_milliseconds_item_ = Menu::CreateItem(this, "milliseconds", this, &MenuShared::TimecodeDisplayTriggered); + view_timecode_view_milliseconds_item_->setData(Timecode::kMilliseconds); + view_timecode_view_milliseconds_item_->setCheckable(true); + frame_view_mode_group_->addAction(view_timecode_view_milliseconds_item_); + Retranslate(); } @@ -117,6 +145,26 @@ void MenuShared::AddItemsForClipEditMenu(Menu *m) m->addAction(clip_nest_item_); } +void MenuShared::AddItemsForTimeRulerMenu(Menu *m) +{ + m->addAction(view_timecode_view_dropframe_item_); + m->addAction(view_timecode_view_nondropframe_item_); + m->addAction(view_timecode_view_seconds_item_); + m->addAction(view_timecode_view_frames_item_); + m->addAction(view_timecode_view_milliseconds_item_); +} + +void MenuShared::AboutToShowTimeRulerActions() +{ + QList timecode_display_actions = frame_view_mode_group_->actions(); + foreach (QAction* a, timecode_display_actions) { + if (a->data() == Core::instance()->GetTimecodeDisplay()) { + a->setChecked(true); + break; + } + } +} + MenuShared *MenuShared::instance() { return instance_; @@ -211,6 +259,18 @@ void MenuShared::DefaultTransitionTriggered() qDebug() << "FIXME: Stub"; } +void MenuShared::TimecodeDisplayTriggered() +{ + // Assume the sender is a QAction + QAction* action = static_cast(sender()); + + // Assume its data() is a member of Timecode::Display + Timecode::Display display = static_cast(action->data().toInt()); + + // Set the current display mode + Core::instance()->SetTimecodeDisplay(display); +} + void MenuShared::Retranslate() { // "New" menu shared items @@ -240,6 +300,13 @@ void MenuShared::Retranslate() clip_link_unlink_item_->setText(tr("Link/Unlink")); clip_enable_disable_item_->setText(tr("Enable/Disable")); clip_nest_item_->setText(tr("Nest")); + + // TimeRuler menu shared items + view_timecode_view_frames_item_->setText(tr("Frames")); + view_timecode_view_dropframe_item_->setText(tr("Drop Frame")); + view_timecode_view_nondropframe_item_->setText(tr("Non-Drop Frame")); + view_timecode_view_milliseconds_item_->setText(tr("Milliseconds")); + view_timecode_view_seconds_item_->setText(tr("Seconds")); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/menu/menushared.h b/app/widget/menu/menushared.h index 684616610..414be8985 100644 --- a/app/widget/menu/menushared.h +++ b/app/widget/menu/menushared.h @@ -42,6 +42,9 @@ public: void AddItemsForEditMenu(Menu* m, bool for_clips); void AddItemsForInOutMenu(Menu* m); void AddItemsForClipEditMenu(Menu* m); + void AddItemsForTimeRulerMenu(Menu* m); + + void AboutToShowTimeRulerActions(); static MenuShared* instance(); @@ -74,6 +77,14 @@ private: QAction* clip_enable_disable_item_; QAction* clip_nest_item_; + // TimeRuler menu shared items + QActionGroup* frame_view_mode_group_; + QAction* view_timecode_view_dropframe_item_; + QAction* view_timecode_view_nondropframe_item_; + QAction* view_timecode_view_seconds_item_; + QAction* view_timecode_view_frames_item_; + QAction* view_timecode_view_milliseconds_item_; + static MenuShared* instance_; private slots: @@ -111,6 +122,14 @@ private slots: void DefaultTransitionTriggered(); + /** + * @brief A slot for the timecode display menu items + * + * Assumes a QAction* sender() and its data() is a member of enum Timecode::Display. Uses the data() to signal a + * timecode change throughout the rest of the application. + */ + void TimecodeDisplayTriggered(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 34fea246d..b07031ea6 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -20,6 +20,7 @@ #include "nodeparamview.h" +#include #include #include #include @@ -31,7 +32,8 @@ OLIVE_NAMESPACE_ENTER NodeParamView::NodeParamView(QWidget *parent) : TimeBasedWidget(true, false, parent), - last_scroll_val_(0) + last_scroll_val_(0), + focused_node_(nullptr) { // Create horizontal layout to place scroll area in (and keyframe editing eventually) QHBoxLayout* layout = new QHBoxLayout(this); @@ -49,18 +51,26 @@ NodeParamView::NodeParamView(QWidget *parent) : splitter->addWidget(scroll_area); // Param widget - param_widget_area_ = new QWidget(); - scroll_area->setWidget(param_widget_area_); + param_widget_container_ = new NodeParamViewParamContainer(); + connect(param_widget_container_, &NodeParamViewParamContainer::Resized, this, &NodeParamView::UpdateGlobalScrollBar); + scroll_area->setWidget(param_widget_container_); - // Set up scroll area layout - param_layout_ = new QVBoxLayout(param_widget_area_); - param_layout_->setSpacing(0); + param_widget_area_ = new QMainWindow(); - // KeyframeView is offset by a ruler, so to stay synchronized with it, we should be too - param_layout_->setContentsMargins(0, ruler()->height(), 0, 0); + // Disable dock widgets from tabbing and disable glitchy animations + param_widget_area_->setDockOptions(static_cast(0)); - // Add a stretch to allow empty space at the bottom of the layout - param_layout_->addStretch(); + // HACK: Hide the main window separators (unfortunately the cursors still appear) + param_widget_area_->setStyleSheet(QStringLiteral("QMainWindow::separator {background: rgba(0, 0, 0, 0)}")); + + QVBoxLayout* param_widget_container_layout = new QVBoxLayout(param_widget_container_); + QMargins param_widget_margin = param_widget_container_layout->contentsMargins(); + param_widget_margin.setTop(ruler()->height()); + param_widget_container_layout->setContentsMargins(param_widget_margin); + param_widget_container_layout->setSpacing(0); + param_widget_container_layout->addWidget(param_widget_area_); + + param_widget_container_layout->addStretch(INT_MAX); // Set up keyframe view QWidget* keyframe_area = new QWidget(); @@ -100,9 +110,6 @@ NodeParamView::NodeParamView(QWidget *parent) : layout->addWidget(vertical_scrollbar_); // Connect scrollbars together - connect(scroll_area->verticalScrollBar(), &QScrollBar::rangeChanged, vertical_scrollbar_, &QScrollBar::setRange); - connect(scroll_area->verticalScrollBar(), &QScrollBar::rangeChanged, this, &NodeParamView::ForceKeyframeViewToScroll); - connect(keyframe_view_->verticalScrollBar(), &QScrollBar::valueChanged, vertical_scrollbar_, &QScrollBar::setValue); connect(keyframe_view_->verticalScrollBar(), &QScrollBar::valueChanged, scroll_area->verticalScrollBar(), &QScrollBar::setValue); connect(scroll_area->verticalScrollBar(), &QScrollBar::valueChanged, vertical_scrollbar_, &QScrollBar::setValue); @@ -120,46 +127,89 @@ NodeParamView::NodeParamView(QWidget *parent) : SetScale(120); SetMaximumScale(TimelineViewBase::kMaximumScale); + + // Pickup on widget focus changes + connect(qApp, + &QApplication::focusChanged, + this, + &NodeParamView::FocusChanged); } void NodeParamView::SelectNodes(const QList &nodes) { + active_nodes_.append(nodes); + + bool changes_made = false; + foreach (Node* n, nodes) { - NodeParamViewItem* item = new NodeParamViewItem(n); + if (!pinned_nodes_.contains(n)) { + NodeParamViewItem* item = new NodeParamViewItem(n, param_widget_area_); - // Insert the widget before the stretch - param_layout_->insertWidget(param_layout_->count() - 1, item); + item->setAllowedAreas(Qt::LeftDockWidgetArea); + item->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable); + item->SetExpanded(node_expanded_state_.value(n, true)); - connect(item, &NodeParamViewItem::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); - connect(item, &NodeParamViewItem::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); - connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::ItemRequestedTimeChanged); - connect(item, &NodeParamViewItem::InputDoubleClicked, this, &NodeParamView::InputDoubleClicked); - connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode); + connect(item, &NodeParamViewItem::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); + connect(item, &NodeParamViewItem::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); + connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::ItemRequestedTimeChanged); + connect(item, &NodeParamViewItem::InputDoubleClicked, this, &NodeParamView::InputDoubleClicked); + connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode); + connect(item, &NodeParamViewItem::dockLocationChanged, this, &NodeParamView::QueueKeyframePositionUpdate); + connect(item, &NodeParamViewItem::dockLocationChanged, this, &NodeParamView::SignalNodeOrder); + connect(item, &NodeParamViewItem::PinToggled, this, &NodeParamView::PinNode); - // Set time target - item->SetTimeTarget(GetTimeTarget()); + // Set time target + item->SetTimeTarget(GetTimeTarget()); - items_.insert(n, item); + items_.insert(n, item); + param_widget_area_->addDockWidget(Qt::LeftDockWidgetArea, item); + + changes_made = true; + + if (!focused_node_ && n->HasGizmos()) { + // We'll focus this node now + item->SetHighlighted(true); + focused_node_ = n; + emit FocusedNodeChanged(focused_node_); + } + } } - UpdateItemTime(GetTimestamp()); + if (changes_made) { + UpdateItemTime(GetTimestamp()); - // Re-arrange keyframes - QMetaObject::invokeMethod(this, "PlaceKeyframesOnView", Qt::QueuedConnection); + // Re-arrange keyframes + QueueKeyframePositionUpdate(); + + SignalNodeOrder(); + } } void NodeParamView::DeselectNodes(const QList &nodes) { // Remove item from map and delete the widget - foreach (Node* n, nodes) { - // Remove all keyframes from this node - keyframe_view_->RemoveKeyframesOfNode(n); + bool changes_made = false; - delete items_.take(n); + foreach (Node* n, nodes) { + if (!pinned_nodes_.contains(n)) { + // Store expanded state + node_expanded_state_.insert(n, items_.value(n)->IsExpanded()); + + // Remove all keyframes from this node + RemoveNode(n); + + changes_made = true; + } + + active_nodes_.removeOne(n); } - // Re-arrange keyframes - QMetaObject::invokeMethod(this, "PlaceKeyframesOnView", Qt::QueuedConnection); + if (changes_made) { + // Re-arrange keyframes + QueueKeyframePositionUpdate(); + + SignalNodeOrder(); + } } void NodeParamView::resizeEvent(QResizeEvent *event) @@ -167,6 +217,8 @@ void NodeParamView::resizeEvent(QResizeEvent *event) QWidget::resizeEvent(event); vertical_scrollbar_->setPageStep(vertical_scrollbar_->height()); + + UpdateGlobalScrollBar(); } void NodeParamView::ScaleChangedEvent(const double &scale) @@ -223,14 +275,63 @@ void NodeParamView::UpdateItemTime(const int64_t ×tamp) } } +void NodeParamView::QueueKeyframePositionUpdate() +{ + QMetaObject::invokeMethod(this, "PlaceKeyframesOnView", Qt::QueuedConnection); +} + +void NodeParamView::SignalNodeOrder() +{ + // Sort by item Y (apparently there's no way in Qt to get the order of dock widgets) + QList nodes; + QList item_ys; + + for (auto it=items_.cbegin(); it!=items_.cend(); it++) { + int item_y = it.value()->pos().y(); + + bool inserted = false; + + for (int i=0; i item_y) { + item_ys.insert(i, item_y); + nodes.insert(i, it.key()); + inserted = true; + break; + } + } + + if (!inserted) { + item_ys.append(item_y); + nodes.append(it.key()); + } + } + + emit NodeOrderChanged(nodes); +} + +void NodeParamView::RemoveNode(Node *n) +{ + keyframe_view_->RemoveKeyframesOfNode(n); + + delete items_.take(n); + + if (focused_node_ == n) { + focused_node_ = nullptr; + emit FocusedNodeChanged(nullptr); + } +} + void NodeParamView::ItemRequestedTimeChanged(const rational &time) { SetTimeAndSignal(Timecode::time_to_timestamp(time, keyframe_view_->timebase())); } -void NodeParamView::ForceKeyframeViewToScroll() +void NodeParamView::UpdateGlobalScrollBar() { - keyframe_view_->SetMaxScroll(param_widget_area_->height() - ruler()->height()); + int height_offscreen = param_widget_container_->height() - ruler()->height() + scrollbar()->height(); + + keyframe_view_->SetMaxScroll(height_offscreen); + vertical_scrollbar_->setRange(0, height_offscreen - keyframe_view_->height()); } void NodeParamView::PlaceKeyframesOnView() @@ -240,4 +341,52 @@ void NodeParamView::PlaceKeyframesOnView() } } +void NodeParamView::PinNode(bool pin) +{ + NodeParamViewItem* item = static_cast(sender()); + Node* node = item->GetNode(); + + if (pin) { + pinned_nodes_.append(node); + } else { + pinned_nodes_.removeOne(node); + + if (!active_nodes_.contains(node)) { + RemoveNode(node); + } + } +} + +void NodeParamView::FocusChanged(QWidget* old, QWidget* now) +{ + Q_UNUSED(old) + + QObject* parent = now; + NodeParamViewItem* item; + + while (parent) { + item = dynamic_cast(parent); + + if (item) { + // Found it! + if (item->GetNode() != focused_node_) { + if (focused_node_) { + // De-focus current node + items_.value(focused_node_)->SetHighlighted(false); + } + + focused_node_ = item->GetNode(); + + item->SetHighlighted(true); + + emit FocusedNodeChanged(focused_node_); + } + + break; + } + + parent = parent->parent(); + } +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index 1e3889b0c..f9fee45db 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -21,6 +21,7 @@ #ifndef NODEPARAMVIEW_H #define NODEPARAMVIEW_H +#include #include #include @@ -31,6 +32,28 @@ OLIVE_NAMESPACE_ENTER +class NodeParamViewParamContainer : public QWidget +{ + Q_OBJECT +public: + NodeParamViewParamContainer(QWidget* parent = nullptr) : + QWidget(parent) + { + } + +protected: + virtual void resizeEvent(QResizeEvent *event) override + { + QWidget::resizeEvent(event); + + emit Resized(event->size().height()); + } + +signals: + void Resized(int new_height); + +}; + class NodeParamView : public TimeBasedWidget { Q_OBJECT @@ -54,6 +77,10 @@ signals: void RequestSelectNode(const QList& target); + void NodeOrderChanged(const QList& nodes); + + void FocusedNodeChanged(Node* n); + protected: virtual void resizeEvent(QResizeEvent *event) override; @@ -66,7 +93,11 @@ protected: private: void UpdateItemTime(const int64_t ×tamp); - QVBoxLayout* param_layout_; + void QueueKeyframePositionUpdate(); + + void SignalNodeOrder(); + + void RemoveNode(Node* n); KeyframeView* keyframe_view_; @@ -76,15 +107,31 @@ private: int last_scroll_val_; - QWidget* param_widget_area_; + NodeParamViewParamContainer* param_widget_container_; + + // This may look weird, but QMainWindow is just a QWidget with a fancy layout that allows + // docking windows + QMainWindow* param_widget_area_; + + QList pinned_nodes_; + + QList active_nodes_; + + QMap node_expanded_state_; + + Node* focused_node_; private slots: void ItemRequestedTimeChanged(const rational& time); - void ForceKeyframeViewToScroll(); + void UpdateGlobalScrollBar(); void PlaceKeyframesOnView(); + void PinNode(bool pin); + + void FocusChanged(QWidget *old, QWidget *now); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index d9093079e..0f5a88e67 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -32,27 +32,15 @@ OLIVE_NAMESPACE_ENTER NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : - QWidget(parent), - node_(node) + QDockWidget(parent), + node_(node), + highlighted_(false) { - QVBoxLayout* main_layout = new QVBoxLayout(this); - main_layout->setSpacing(0); - main_layout->setMargin(0); - // Create title bar widget title_bar_ = new NodeParamViewItemTitleBar(this); - title_bar_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); - - QHBoxLayout* title_bar_layout = new QHBoxLayout(title_bar_); - - title_bar_collapse_btn_ = new CollapseButton(); - title_bar_layout->addWidget(title_bar_collapse_btn_); - - title_bar_lbl_ = new QLabel(title_bar_); - title_bar_layout->addWidget(title_bar_lbl_); // Add title bar to widget - main_layout->addWidget(title_bar_); + this->setTitleBarWidget(title_bar_); // Create and add contents widget QVector inputs; @@ -70,11 +58,26 @@ NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : connect(body_, &NodeParamViewItemBody::RequestSetTime, this, &NodeParamViewItem::RequestSetTime); connect(body_, &NodeParamViewItemBody::KeyframeAdded, this, &NodeParamViewItem::KeyframeAdded); connect(body_, &NodeParamViewItemBody::KeyframeRemoved, this, &NodeParamViewItem::KeyframeRemoved); - connect(title_bar_collapse_btn_, &QPushButton::toggled, body_, &NodeParamViewItemBody::setVisible); - main_layout->addWidget(body_); + connect(title_bar_, &NodeParamViewItemTitleBar::ExpandedStateChanged, this, &NodeParamViewItem::SetExpanded); + connect(title_bar_, &NodeParamViewItemTitleBar::PinToggled, this, &NodeParamViewItem::PinToggled); + + QWidget* body_container = new QWidget(); + body_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + QHBoxLayout* body_container_layout = new QHBoxLayout(body_container); + body_container_layout->setSpacing(0); + body_container_layout->setMargin(0); + body_container_layout->addWidget(body_); + this->setWidget(body_container); connect(node_, &Node::LabelChanged, this, &NodeParamViewItem::Retranslate); + setBackgroundRole(QPalette::Base); + setAutoFillBackground(true); + + setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + + setFocusPolicy(Qt::ClickFocus); + Retranslate(); } @@ -109,34 +112,98 @@ void NodeParamViewItem::changeEvent(QEvent *e) QWidget::changeEvent(e); } +void NodeParamViewItem::paintEvent(QPaintEvent *event) +{ + QDockWidget::paintEvent(event); + + // Draw border if focused + if (highlighted_) { + QPainter p(this); + p.setBrush(Qt::NoBrush); + p.setPen(palette().highlight().color()); + p.drawRect(rect().adjusted(0, 0, -1, -1)); + } +} + void NodeParamViewItem::Retranslate() { node_->Retranslate(); if (node_->GetLabel().isEmpty()) { - title_bar_lbl_->setText(node_->Name()); + title_bar_->SetText(node_->Name()); } else { - title_bar_lbl_->setText(tr("%1 (%2)").arg(node_->GetLabel(), node_->Name())); + title_bar_->SetText(tr("%1 (%2)").arg(node_->GetLabel(), node_->Name())); } body_->Retranslate(); } -NodeParamViewItemTitleBar::NodeParamViewItemTitleBar(QWidget *parent) : - QWidget(parent) +void NodeParamViewItem::SetExpanded(bool e) { + body_->setVisible(e); + title_bar_->SetExpanded(e); +} + +bool NodeParamViewItem::IsExpanded() const +{ + return body_->isVisible(); +} + +void NodeParamViewItem::ToggleExpanded() +{ + SetExpanded(!IsExpanded()); +} + +NodeParamViewItemTitleBar::NodeParamViewItemTitleBar(QWidget *parent) : + QWidget(parent), + draw_border_(true) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + + collapse_btn_ = new CollapseButton(); + connect(collapse_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::ExpandedStateChanged); + layout->addWidget(collapse_btn_); + + lbl_ = new QLabel(); + layout->addWidget(lbl_); + + // Place next buttons on the far side + layout->addStretch(); + + QPushButton* pin_btn = new QPushButton(QStringLiteral("P")); + pin_btn->setCheckable(true); + pin_btn->setFixedSize(pin_btn->sizeHint().height(), pin_btn->sizeHint().height()); + layout->addWidget(pin_btn); + connect(pin_btn, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::PinToggled); +} + +void NodeParamViewItemTitleBar::SetExpanded(bool e) +{ + draw_border_ = e; + collapse_btn_->setChecked(e); + + update(); } void NodeParamViewItemTitleBar::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); - QPainter p(this); + if (draw_border_) { + QPainter p(this); - // Draw bottom border using text color - int bottom = height() - 1; - p.setPen(palette().text().color()); - p.drawLine(0, bottom, width(), bottom); + // Draw bottom border using text color + int bottom = height() - 1; + p.setPen(palette().text().color()); + p.drawLine(0, bottom, width(), bottom); + } +} + +void NodeParamViewItemTitleBar::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + collapse_btn_->click(); } NodeParamViewItemBody::NodeParamViewItemBody(const QVector &inputs, QWidget *parent) : diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index 6d6996219..2bc8972fa 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -21,6 +21,7 @@ #ifndef NODEPARAMVIEWITEM_H #define NODEPARAMVIEWITEM_H +#include #include #include #include @@ -36,12 +37,36 @@ OLIVE_NAMESPACE_ENTER -class NodeParamViewItemTitleBar : public QWidget { +class NodeParamViewItemTitleBar : public QWidget +{ + Q_OBJECT public: NodeParamViewItemTitleBar(QWidget* parent = nullptr); + void SetExpanded(bool e); + + void SetText(const QString& s) + { + lbl_->setText(s); + } + +signals: + void ExpandedStateChanged(bool e); + + void PinToggled(bool e); + protected: virtual void paintEvent(QPaintEvent *event) override; + + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + +private: + bool draw_border_; + + QLabel* lbl_; + + CollapseButton* collapse_btn_; + }; class NodeParamViewItemBody : public QWidget { @@ -99,7 +124,7 @@ private slots: }; -class NodeParamViewItem : public QWidget +class NodeParamViewItem : public QDockWidget { Q_OBJECT public: @@ -111,9 +136,22 @@ public: Node* GetNode() const; + bool IsExpanded() const; + + void SetHighlighted(bool e) + { + highlighted_ = e; + + update(); + } + public slots: void SignalAllKeyframes(); + void SetExpanded(bool e); + + void ToggleExpanded(); + signals: void KeyframeAdded(NodeKeyframePtr key, int y); @@ -125,22 +163,24 @@ signals: void RequestSelectNode(const QList& node); + void PinToggled(bool e); + protected: virtual void changeEvent(QEvent *e) override; + virtual void paintEvent(QPaintEvent *event) override; + private: NodeParamViewItemTitleBar* title_bar_; - QLabel* title_bar_lbl_; - - CollapseButton* title_bar_collapse_btn_; - NodeParamViewItemBody* body_; Node* node_; rational time_; + bool highlighted_; + private slots: void Retranslate(); diff --git a/app/widget/nodetreeview/CMakeLists.txt b/app/widget/nodetreeview/CMakeLists.txt new file mode 100644 index 000000000..486f03d0a --- /dev/null +++ b/app/widget/nodetreeview/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/nodetreeview/nodetreeview.h + widget/nodetreeview/nodetreeview.cpp + PARENT_SCOPE +) diff --git a/app/widget/nodetreeview/nodetreeview.cpp b/app/widget/nodetreeview/nodetreeview.cpp new file mode 100644 index 000000000..b02a707d9 --- /dev/null +++ b/app/widget/nodetreeview/nodetreeview.cpp @@ -0,0 +1,111 @@ +#include "nodetreeview.h" + +OLIVE_NAMESPACE_ENTER + +NodeTreeView::NodeTreeView(QWidget *parent) : + QTreeWidget(parent), + only_show_keyframable_(false) +{ + connect(this, &NodeTreeView::itemChanged, this, &NodeTreeView::ItemCheckStateChanged); + + Retranslate(); +} + +bool NodeTreeView::IsNodeEnabled(Node *n) const +{ + return !disabled_nodes_.contains(n); +} + +bool NodeTreeView::IsInputEnabled(NodeInput *i) const +{ + return !disabled_inputs_.contains(i); +} + +void NodeTreeView::SetNodes(const QList &nodes) +{ + nodes_ = nodes; + + this->clear(); + + foreach (Node* n, nodes_) { + QTreeWidgetItem* node_item = new QTreeWidgetItem(); + node_item->setText(0, n->Name()); + node_item->setCheckState(0, disabled_nodes_.contains(n) ? Qt::Unchecked : Qt::Checked); + node_item->setData(0, kItemType, kItemTypeNode); + node_item->setData(0, kItemPointer, reinterpret_cast(n)); + + QList inputs = n->GetInputsIncludingArrays(); + foreach (NodeInput* i, inputs) { + if (only_show_keyframable_ && !i->is_keyframable()) { + continue; + } + + QTreeWidgetItem* input_item = new QTreeWidgetItem(node_item); + input_item->setText(0, i->name()); + input_item->setCheckState(0, disabled_inputs_.contains(i) ? Qt::Unchecked : Qt::Checked); + input_item->setData(0, kItemType, kItemTypeInput); + input_item->setData(0, kItemPointer, reinterpret_cast(i)); + } + + // Add at the end to prevent unnecessary signalling while we're setting these objects up + if (node_item->childCount() > 0) { + this->addTopLevelItem(node_item); + } else { + delete node_item; + } + } +} + +void NodeTreeView::changeEvent(QEvent *e) +{ + QTreeWidget::changeEvent(e); + + if (e->type() == QEvent::LanguageChange) { + Retranslate(); + } +} + +void NodeTreeView::Retranslate() +{ + setHeaderLabel(tr("Nodes")); +} + +void NodeTreeView::ItemCheckStateChanged(QTreeWidgetItem *item, int column) +{ + Q_UNUSED(column) + + switch (item->data(0, kItemType).toInt()) { + case kItemTypeNode: + { + Node* n = reinterpret_cast(item->data(0, kItemPointer).value()); + + if (item->checkState(0) == Qt::Checked) { + if (disabled_nodes_.contains(n)) { + disabled_nodes_.removeOne(n); + emit NodeEnableChanged(n, true); + } + } else if (!disabled_nodes_.contains(n)) { + disabled_nodes_.append(n); + emit NodeEnableChanged(n, false); + } + break; + } + case kItemTypeInput: + { + NodeInput* i = reinterpret_cast(item->data(0, kItemPointer).value()); + + if (item->checkState(0) == Qt::Checked) { + if (disabled_inputs_.contains(i)) { + disabled_inputs_.removeOne(i); + emit InputEnableChanged(i, true); + } + } else if (!disabled_inputs_.contains(i)) { + disabled_inputs_.append(i); + emit InputEnableChanged(i, false); + } + break; + } + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodetreeview/nodetreeview.h b/app/widget/nodetreeview/nodetreeview.h new file mode 100644 index 000000000..2b221a68c --- /dev/null +++ b/app/widget/nodetreeview/nodetreeview.h @@ -0,0 +1,62 @@ +#ifndef NODETREEVIEW_H +#define NODETREEVIEW_H + +#include + +#include "node/node.h" + +OLIVE_NAMESPACE_ENTER + +class NodeTreeView : public QTreeWidget +{ + Q_OBJECT +public: + NodeTreeView(QWidget *parent = nullptr); + + bool IsNodeEnabled(Node* n) const; + + bool IsInputEnabled(NodeInput* i) const; + + void SetOnlyShowKeyframable(bool e) + { + only_show_keyframable_ = e; + } + +public slots: + void SetNodes(const QList& nodes); + +signals: + void NodeEnableChanged(Node* n, bool e); + + void InputEnableChanged(NodeInput* i, bool e); + +protected: + virtual void changeEvent(QEvent* e) override; + +private: + void Retranslate(); + + enum ItemType { + kItemTypeNode, + kItemTypeInput + }; + + static const int kItemType = Qt::UserRole; + static const int kItemPointer = Qt::UserRole + 1; + + QList nodes_; + + QList disabled_nodes_; + + QList disabled_inputs_; + + bool only_show_keyframable_; + +private slots: + void ItemCheckStateChanged(QTreeWidgetItem* item, int column); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // NODETREEVIEW_H diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index b8d7e526f..d3cfdd3b9 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -57,6 +57,10 @@ NodeView::NodeView(QWidget *parent) : scene_.setSceneRect(-1000000, -1000000, 2000000, 2000000); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + select_blocks_internal_timer_.setSingleShot(true); + select_blocks_internal_timer_.setInterval(100); + connect(&select_blocks_internal_timer_, &QTimer::timeout, this, &NodeView::SelectBlocksInternal); } NodeView::~NodeView() @@ -200,13 +204,21 @@ void NodeView::SelectWithDependencies(QList nodes) void NodeView::SelectBlocks(const QList &blocks) { + if (!graph_) { + return; + } + selected_blocks_.append(blocks); - SelectBlocksInternal(); + QueueSelectBlocksInternal(); } void NodeView::DeselectBlocks(const QList &blocks) { + if (!graph_) { + return; + } + // Remove temporary associations foreach (Block* b, selected_blocks_) { if (!blocks.contains(b)) { @@ -219,7 +231,7 @@ void NodeView::DeselectBlocks(const QList &blocks) selected_blocks_.removeOne(b); } - SelectBlocksInternal(); + QueueSelectBlocksInternal(); } void NodeView::CopySelected(bool cut) @@ -372,8 +384,8 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) QRect edge_detect_rect(event->pos(), event->pos()); - // FIXME: Hardcoded numbers - edge_detect_rect.adjust(-20, -20, 20, 20); + int edge_detect_radius = fontMetrics().height(); + edge_detect_rect.adjust(-edge_detect_radius, -edge_detect_radius, edge_detect_radius, edge_detect_radius); QList items = this->items(edge_detect_rect); @@ -653,140 +665,6 @@ void NodeView::ContextMenuFilterChanged(QAction *action) } } -void NodeView::PlaceNode(NodeViewItem *n, const QPointF &pos) -{ - QRectF destination_rect = n->rect(); - destination_rect.translate(n->pos()); - - double x_movement = destination_rect.width() * 1.5; - double y_movement = destination_rect.height() * 1.5; - - QList items = scene()->items(destination_rect); - - n->setPos(pos); - - foreach (QGraphicsItem* item, items) { - if (item == n) { - continue; - } - - NodeViewItem* node_item = dynamic_cast(item); - - if (!node_item) { - continue; - } - - qDebug() << "Moving" << node_item->GetNode() << "for" << n->GetNode(); - - QPointF new_pos; - - if (item->pos() == pos) { - qDebug() << "Same pos, need more info"; - - // Item positions are exact, we'll need more information to determine where this item should go - Node* ours = n->GetNode(); - Node* theirs = node_item->GetNode(); - - bool moved = false; - - new_pos = item->pos(); - - // Heuristic to determine whether to move the other item above or below - foreach (NodeEdgePtr our_edge, ours->output()->edges()) { - foreach (NodeEdgePtr their_edge, theirs->output()->edges()) { - if (our_edge->output()->parentNode() == their_edge->output()->parentNode()) { - qDebug() << " They share a node that they output to"; - if (our_edge->input()->index() > their_edge->input()->index()) { - // Their edge should go above ours - qDebug() << " Our edge goes BELOW theirs"; - new_pos.setY(new_pos.y() - y_movement); - } else { - // Our edge should go below ours - qDebug() << " Our edge goes ABOVE theirs"; - new_pos.setY(new_pos.y() + y_movement); - } - - moved = true; - - break; - } - } - } - - // If we find anything, just move at random - if (!moved) { - new_pos.setY(new_pos.y() - y_movement); - } - - } else if (item->pos().x() == pos.x()) { - qDebug() << "Same X, moving vertically"; - - // Move strictly up or down - new_pos = item->pos(); - - if (item->pos().y() < pos.y()) { - // Move further up - new_pos.setY(pos.y() - y_movement); - } else { - // Move further down - new_pos.setY(pos.y() + y_movement); - } - } else if (item->pos().y() == pos.y()) { - qDebug() << "Same Y, moving horizontally"; - - // Move strictly left or right - new_pos = item->pos(); - - if (item->pos().x() < pos.x()) { - // Move further up - new_pos.setX(pos.x() - x_movement); - } else { - // Move further down - new_pos.setX(pos.x() + x_movement); - } - } else { - qDebug() << "Diff pos, pushing in angle"; - - // The item does not have equal X or Y, attempt to push it away from `pos` in the direction it's in - double x_diff = item->pos().x() - pos.x(); - double y_diff = item->pos().y() - pos.y(); - - double slope = y_diff / x_diff; - double y_int = item->pos().y() - slope * item->pos().x(); - - if (qAbs(slope) > 1.0) { - // Vertical difference is greater than horizontal difference, prioritize vertical movement - double desired_y = pos.y(); - - if (item->pos().y() > pos.y()) { - desired_y += y_movement; - } else { - desired_y -= y_movement; - } - - double x = (desired_y - y_int) / slope; - - new_pos = QPointF(x, desired_y); - } else { - // Horizontal difference is greater than vertical difference, prioritize horizontal movement - double desired_x = pos.x(); - - if (item->pos().x() > pos.x()) { - desired_x += x_movement; - } else { - desired_x -= x_movement; - } - - double y = slope * desired_x + y_int; - - new_pos = QPointF(desired_x, y); - } - } - - PlaceNode(node_item, new_pos); - } -} - void NodeView::AttachNodesToCursor(const QList &nodes) { QList items; @@ -851,7 +729,7 @@ void NodeView::UpdateBlockFilter() } bool first = true; - QRectF last_rect; + QPointF last_bottom_right; QList currently_visible; @@ -883,10 +761,13 @@ void NodeView::UpdateBlockFilter() if (first) { first = false; } else { - QPointF desired_anchor_pos = last_rect.bottomRight() + QPointF(0, 2); + QPointF desired_anchor_pos = last_bottom_right + QPointF(0, 2); QPointF necessary_movement = anchor.topRight() - desired_anchor_pos; + // Calculate the bottom right from which we'll anchor the next rect + last_bottom_right = anchor.bottomRight() - necessary_movement; + b->SetPosition(b->GetPosition() - necessary_movement); foreach (Node* d, deps) { d->SetPosition(d->GetPosition() - necessary_movement); @@ -915,15 +796,12 @@ void NodeView::UpdateBlockFilter() // And lastly, add them all to our currently visible list currently_visible.append(deps); - - // Cache this rect so we can calculate other rects - last_rect = anchor; } // Show only edges between those dependencies foreach (NodeViewEdge* edge, scene_.edge_map()) { - edge->setVisible((currently_visible.contains(edge->edge()->input()->parentNode()) - && currently_visible.contains(edge->edge()->output()->parentNode()))); + edge->setVisible((currently_visible.contains(edge->edge()->input_node()) + && currently_visible.contains(edge->edge()->output_node()))); } } @@ -942,10 +820,16 @@ void NodeView::DisassociateNode(Node *n, bool remove_from_map) disconnect(n, &Node::destroyed, this, &NodeView::AssociatedNodeDestroyed); } +void NodeView::QueueSelectBlocksInternal() +{ + select_blocks_internal_timer_.stop(); + select_blocks_internal_timer_.start(); +} + void NodeView::SelectBlocksInternal() { // Block scene signals while our selection is changing a lot - scene_.blockSignals(true); + DisconnectSelectionChangedSignal(); if (filter_mode_ == kFilterShowSelectedBlocks) { UpdateBlockFilter(); @@ -962,7 +846,7 @@ void NodeView::SelectBlocksInternal() SelectWithDependencies(nodes); // Stop blocking signals and send a change signal now that all of our processing is done - scene_.blockSignals(false); + ConnectSelectionChangedSignal(); SceneSelectionChangedSlot(); if (!selected_blocks_.isEmpty()) { @@ -1024,7 +908,7 @@ void NodeView::GraphEdgeRemoved(NodeEdgePtr edge) { scene_.RemoveEdge(edge); - Node* output_node = edge->output()->parentNode(); + Node* output_node = edge->output_node(); // Check if this disconnected node still connects to a selected block, in which case do nothing foreach (Block* b, selected_blocks_) { diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 573aaa79d..5bcf7799e 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -84,11 +84,7 @@ protected: virtual void wheelEvent(QWheelEvent* event) override; - //virtual void scrollContentsBy(int dx, int dy) override; - private: - void PlaceNode(NodeViewItem* n, const QPointF& pos); - void AttachNodesToCursor(const QList& nodes); void AttachItemsToCursor(const QList& items); @@ -107,7 +103,9 @@ private: void AssociateNodeWithSelectedBlocks(Node* n); void DisassociateNode(Node* n, bool remove_from_map); - void SelectBlocksInternal(); + void QueueSelectBlocksInternal(); + + QTimer select_blocks_internal_timer_; NodeGraph* graph_; @@ -188,6 +186,8 @@ private slots: */ void ContextMenuFilterChanged(QAction* action); + void SelectBlocksInternal(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 5348efd9b..8f637a212 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -261,8 +261,25 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti Color node_color = Config::Current()[QStringLiteral("NodeCatColor%1") .arg(node_->Category().first())].value(); + QLinearGradient grad; + + grad.setStart(0, rect().top()); + grad.setFinalStop(0, rect().bottom()); + + QColor node_color_as_qcolor = node_color.toQColor(); + + { + // Generate lighter color for gradient + qreal hue, sat, lightness; + node_color_as_qcolor.getHslF(&hue, &sat, &lightness); + lightness = qMin(1.0, lightness + 0.2); + grad.setColorAt(0.0, QColor::fromHslF(hue, sat, lightness)); + } + + grad.setColorAt(1.0, node_color_as_qcolor); + painter->setPen(Qt::black); - painter->setBrush(node_color.toQColor()); + painter->setBrush(grad); painter->drawRect(title_bar_rect_); @@ -270,10 +287,17 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti QString node_label; - if (node_->GetLabel().isEmpty()) { - node_label = node_->ShortName(); - } else { + if (!node_->GetLabel().isEmpty()) { + // Use label directly if node has one node_label = node_->GetLabel(); + } else if (node_->IsTrack()) { + // If node is a track, use a special track name to help users identify better + // Exception for tracks + TrackOutput* track = static_cast(node_); + node_label = TrackOutput::GetDefaultTrackName(track->track_type(), track->Index()); + } else { + // Otherwise, just use the node's short name + node_label = node_->ShortName(); } QFont f; @@ -308,7 +332,7 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti } // Determine the text color (automatically calculate from node background color) - if (node_color.GetRoughLuminance() > 0.66) { + if (node_color.GetRoughLuminance() > 0.5) { painter->setPen(Qt::black); } else { painter->setPen(Qt::white); diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index b3eef2300..032cd0bcd 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -154,6 +154,15 @@ void NodeRemoveCommand::redo_internal() // Take nodes from graph (TakeNode() will automatically disconnect edges) foreach (Node* n, nodes_) { + // If the node is a block, unlink any linked blocks before removing + if (n->IsBlock()) { + Block *b = static_cast(n); + if (b->HasLinks()) { + BlockUnlinkAllCommand *unlink_command = new BlockUnlinkAllCommand(b); + unlink_command->redo(); + block_unlink_commands_.append(unlink_command); + } + } graph_->TakeNode(n, &memory_manager_); } } @@ -165,12 +174,19 @@ void NodeRemoveCommand::undo_internal() graph_->AddNode(n); } + // Relink any blocks that were unlinked + foreach(BlockUnlinkAllCommand* command, block_unlink_commands_) { + command->undo(); + delete command; + } + // Re-connect edges foreach (NodeEdgePtr edge, edges_) { NodeParam::ConnectEdge(edge->output(), edge->input()); } edges_.clear(); + block_unlink_commands_.clear(); } Project *NodeRemoveCommand::GetRelevantProject() const diff --git a/app/widget/nodeview/nodeviewundo.h b/app/widget/nodeview/nodeviewundo.h index 4a9faefd4..6b2854e38 100644 --- a/app/widget/nodeview/nodeviewundo.h +++ b/app/widget/nodeview/nodeviewundo.h @@ -27,6 +27,7 @@ #include "node/node.h" #include "nodeviewitem.h" #include "undo/undocommand.h" +#include "widget/timelinewidget/undo/undo.h" OLIVE_NAMESPACE_ENTER @@ -112,6 +113,7 @@ private: NodeGraph* graph_; QList nodes_; QList edges_; + QList block_unlink_commands_; }; class NodeRemoveWithExclusiveDeps : public UndoCommand { @@ -149,6 +151,64 @@ private: }; +class NodeGraphBeginOperationCommand : public UndoCommand { +public: + NodeGraphBeginOperationCommand(NodeGraph* graph, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + graph_(graph) + { + } + + virtual Project* GetRelevantProject() const override + { + return static_cast(graph_)->project(); + } + +protected: + virtual void redo_internal() override + { + graph_->BeginOperation(); + } + + virtual void undo_internal() override + { + graph_->EndOperation(); + } + +private: + NodeGraph* graph_; + +}; + +class NodeGraphEndOperationCommand : public UndoCommand { +public: + NodeGraphEndOperationCommand(NodeGraph* graph, QUndoCommand* parent = nullptr) : + UndoCommand(parent), + graph_(graph) + { + } + + virtual Project* GetRelevantProject() const override + { + return static_cast(graph_)->project(); + } + +protected: + virtual void redo_internal() override + { + graph_->EndOperation(); + } + + virtual void undo_internal() override + { + graph_->BeginOperation(); + } + +private: + NodeGraph* graph_; + +}; + OLIVE_NAMESPACE_EXIT #endif // NODEVIEWUNDO_H diff --git a/app/widget/playbackcontrols/playbackcontrols.cpp b/app/widget/playbackcontrols/playbackcontrols.cpp index 8a397786a..3acb51bdc 100644 --- a/app/widget/playbackcontrols/playbackcontrols.cpp +++ b/app/widget/playbackcontrols/playbackcontrols.cpp @@ -161,7 +161,10 @@ void PlaybackControls::SetTimebase(const rational &r) time_base_ = r; cur_tc_lbl_->SetTimebase(r); - cur_tc_lbl_->setEnabled(!r.isNull()); + cur_tc_lbl_->setVisible(!r.isNull()); + end_tc_lbl_->setVisible(!r.isNull()); + + setEnabled(!r.isNull()); } void PlaybackControls::SetAudioVideoDragButtonsVisible(bool e) diff --git a/app/widget/projectexplorer/CMakeLists.txt b/app/widget/projectexplorer/CMakeLists.txt index e999f1d1d..cc8d45b76 100644 --- a/app/widget/projectexplorer/CMakeLists.txt +++ b/app/widget/projectexplorer/CMakeLists.txt @@ -32,5 +32,7 @@ set(OLIVE_SOURCES widget/projectexplorer/projectexplorericonviewitemdelegate.cpp widget/projectexplorer/projectexplorernavigation.h widget/projectexplorer/projectexplorernavigation.cpp + widget/projectexplorer/projectexplorerundo.h + widget/projectexplorer/projectexplorerundo.cpp PARENT_SCOPE ) diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 0a1dd0ab7..9041e9f38 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -31,11 +32,14 @@ #include "core.h" #include "dialog/footageproperties/footageproperties.h" #include "dialog/sequence/sequence.h" +#include "projectexplorerundo.h" #include "task/precache/precachetask.h" #include "task/taskmanager.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "window/mainwindow/mainwindow.h" +#include "widget/timelinewidget/timelinewidget.h" +#include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER @@ -536,6 +540,30 @@ void ProjectExplorer::DeselectAll() CurrentView()->selectionModel()->clearSelection(); } +QList ProjectExplorer::GetMediaNodesUsingFootage(Footage *item) +{ + QList list; + + // Get all sequences. + QList sequences = model_.project()->get_items_of_type(Item::kSequence); + + // Footage can contain multiple streams, all of which need to be dealt with + foreach (ItemPtr s, sequences) { + const QList& nodes = static_cast(s.get())->nodes(); + foreach (Node* n, nodes) { + if (n->IsMedia()) { + MediaInput* media_node = static_cast(n); + + if (media_node->footage()->footage() == item) { + list.append(media_node); + } + } + } + } + + return list; +} + void ProjectExplorer::DeleteSelected() { QList selected = SelectedItems(); @@ -547,18 +575,109 @@ void ProjectExplorer::DeleteSelected() QUndoCommand* command = new QUndoCommand(); foreach (Item* item, selected) { - ItemPtr item_ptr = item->get_shared_ptr(); - - // If this is a sequence, close it - if (item_ptr->type() == Item::kSequence) { - Sequence* s = static_cast(item_ptr.get()); + // Verify whether this item is in use anywhere + switch (item->type()) { + case Item::kSequence: + { + // If this is a sequence, check if it's open and close it if necessary + Sequence* s = static_cast(item); if (Core::instance()->main_window()->IsSequenceOpen(s)) { Core::instance()->main_window()->CloseSequence(s); } + break; + } + case Item::kFootage: + { + // If this is footage, check if it's used anywhere in any sequence + Footage* footage = static_cast(item); + + QList footage_nodes = GetMediaNodesUsingFootage(footage); + + if (!footage_nodes.isEmpty()) { + // Footage is in use, show messagebox asking what to do about it + QList used_in_sequences; + + // Compile list of sequences to assist the user in making this decision + foreach (MediaInput* i, footage_nodes) { + Sequence* media_parent = static_cast(i->parent()); + + if (!used_in_sequences.contains(media_parent)) { + used_in_sequences.append(media_parent); + } + } + + QString sequence_list_str; + foreach (Sequence* s, used_in_sequences) { + sequence_list_str.append(QStringLiteral("%1\n").arg(s->name())); + } + + QMessageBox msgbox(this); + msgbox.setWindowTitle(tr("Confirm Footage Deletion")); + msgbox.setText(tr("The footage \"%1\" is currently used in the following sequence(s):\n\n" + "%2\nWhat would you like to do with these clips?") + .arg(footage->filename(), sequence_list_str)); + msgbox.setIcon(QMessageBox::Warning); + + // Set up buttons + QPushButton* offline_btn = msgbox.addButton(tr("Offline Footage"), QMessageBox::YesRole); + QPushButton* delete_clip_btn = msgbox.addButton(tr("Delete Clips"), QMessageBox::NoRole); + msgbox.addButton(QMessageBox::Cancel); + + // Run messagebox + msgbox.exec(); + + if (msgbox.clickedButton() == offline_btn || msgbox.clickedButton() == delete_clip_btn) { + + // For safety, even if we're deleting clips, we'll offline the footage nodes too + new OfflineFootageCommand(footage_nodes, command); + + } + + if (msgbox.clickedButton() == delete_clip_btn) { + + // Delete any blocks that use this footage + QList blocks_to_remove; + + foreach (Sequence* s, used_in_sequences) { + foreach (TrackOutput* track, s->viewer_output()->GetTracks()) { + foreach (Block* b, track->Blocks()) { + QList deps = b->GetDependencies(); + + foreach (MediaInput* i, footage_nodes) { + if (deps.contains(i)) { + blocks_to_remove.append(b); + break; + } + } + } + } + } + + TimelineWidget::ReplaceBlocksWithGaps(blocks_to_remove, true, command); + + } else if (msgbox.clickedButton() != offline_btn) { + + // Must have cancelled + delete command; + return; + + } + } + + // Close footage if currently open in footage panel + FootageViewerPanel* footage_panel = PanelManager::instance()->GetPanelsOfType().first(); + if (footage_panel->GetSelectedFootage().contains(footage)) { + footage_panel->SetFootage(nullptr); + } + break; + } + case Item::kFolder: + // Do nothing + break; } - new ProjectViewModel::RemoveItemCommand(&model_, item_ptr, command); + new ProjectViewModel::RemoveItemCommand(&model_, item->get_shared_ptr(), command); } Core::instance()->undo_stack()->pushIfHasChildren(command); diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 2fe097bbd..90dd10841 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -25,6 +25,7 @@ #include #include +#include "node/input/media/media.h" #include "project/project.h" #include "project/projectviewmodel.h" #include "widget/projectexplorer/projectexplorericonview.h" @@ -101,6 +102,18 @@ signals: void DoubleClickedItem(Item* item); private: + /** + * @brief Check if an item is in use anywhere and return any relevant input nodes + */ + QList GetMediaNodesUsingFootage(Footage* item); + + /** + * @brief Get all the blocks that solely rely on an input node + * + * Ignores blocks that depend on multiple inputs + */ + QList GetFootageBlocks(QList nodes); + /** * @brief Simple convenience function for adding a view to this stacked widget * diff --git a/app/widget/projectexplorer/projectexplorerundo.cpp b/app/widget/projectexplorer/projectexplorerundo.cpp new file mode 100644 index 000000000..584d4a56c --- /dev/null +++ b/app/widget/projectexplorer/projectexplorerundo.cpp @@ -0,0 +1,54 @@ +/*** + + 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 "projectexplorerundo.h" + +OLIVE_NAMESPACE_ENTER + +OfflineFootageCommand::OfflineFootageCommand(const QList &media, QUndoCommand* parent) : + UndoCommand(parent) +{ + foreach (MediaInput* i, media) { + stream_data_.insert(i, i->footage()); + } + + project_ = static_cast(media.first()->parent())->project(); +} + +Project *OfflineFootageCommand::GetRelevantProject() const +{ + return project_; +} + +void OfflineFootageCommand::redo_internal() +{ + for (auto it=stream_data_.cbegin(); it!=stream_data_.cend(); it++) { + it.key()->SetFootage(nullptr); + } +} + +void OfflineFootageCommand::undo_internal() +{ + for (auto it=stream_data_.cbegin(); it!=stream_data_.cend(); it++) { + it.key()->SetFootage(it.value()); + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/projectexplorer/projectexplorerundo.h b/app/widget/projectexplorer/projectexplorerundo.h new file mode 100644 index 000000000..8aaabdc0e --- /dev/null +++ b/app/widget/projectexplorer/projectexplorerundo.h @@ -0,0 +1,52 @@ +/*** + + 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 PROJECTEXPLORERUNDO_H +#define PROJECTEXPLORERUNDO_H + +#include "node/input/media/media.h" +#include "undo/undocommand.h" + +OLIVE_NAMESPACE_ENTER + +/** + * @brief An undo command for offlining footage when it is deleted from the project explorer + */ +class OfflineFootageCommand : public UndoCommand { +public: + OfflineFootageCommand(const QList& media, QUndoCommand* parent = nullptr); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo_internal() override; + + virtual void undo_internal() override; + +private: + QMap stream_data_; + + Project* project_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // PROJECTEXPLORERUNDO_H diff --git a/app/widget/timelinewidget/CMakeLists.txt b/app/widget/timelinewidget/CMakeLists.txt index 293c4c972..9672f1a58 100644 --- a/app/widget/timelinewidget/CMakeLists.txt +++ b/app/widget/timelinewidget/CMakeLists.txt @@ -29,5 +29,7 @@ set(OLIVE_SOURCES widget/timelinewidget/timelinescaledobject.cpp widget/timelinewidget/timelinewidget.h widget/timelinewidget/timelinewidget.cpp + widget/timelinewidget/timelinewidgetselections.h + widget/timelinewidget/timelinewidgetselections.cpp PARENT_SCOPE ) diff --git a/app/widget/timelinewidget/timelinescaledobject.h b/app/widget/timelinewidget/timelinescaledobject.h index c4aec301c..3b05f8e26 100644 --- a/app/widget/timelinewidget/timelinescaledobject.h +++ b/app/widget/timelinewidget/timelinescaledobject.h @@ -48,10 +48,10 @@ public: static double CalculateScaleFromDimensions(double viewport_sz, double content_sz); static double CalculatePaddingFromDimensionScale(double viewport_sz); -protected: double TimeToScene(const rational& time); rational SceneToTime(const double &x, bool round = false); +protected: virtual void TimebaseChangedEvent(const rational&){} virtual void ScaleChangedEvent(const double&){} diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index acfb8f18e..4a3a6b759 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -30,6 +30,17 @@ #include "common/timecodefunctions.h" #include "dialog/sequence/sequence.h" #include "node/block/transition/transition.h" +#include "tool/add.h" +#include "tool/beam.h" +#include "tool/edit.h" +#include "tool/pointer.h" +#include "tool/razor.h" +#include "tool/ripple.h" +#include "tool/rolling.h" +#include "tool/slide.h" +#include "tool/slip.h" +#include "tool/transition.h" +#include "tool/zoom.h" #include "tool/tool.h" #include "trackview/trackview.h" #include "widget/menu/menu.h" @@ -107,6 +118,8 @@ TimelineWidget::TimelineWidget(QWidget *parent) : view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); view->SetSnapService(this); + view->SetSelectionList(&selections_); + view->SetGhostList(&ghost_items_); view_splitter_->addWidget(tview); @@ -149,6 +162,8 @@ TimelineWidget::TimelineWidget(QWidget *parent) : SetMaximumScale(TimelineViewBase::kMaximumScale); SetAutoSetTimebase(false); + + connect(Core::instance(), &Core::ToolChanged, this, &TimelineWidget::ToolChanged); } TimelineWidget::~TimelineWidget() @@ -161,20 +176,16 @@ TimelineWidget::~TimelineWidget() void TimelineWidget::Clear() { - QList deselected_blocks; - - QMap::const_iterator iterator; - for (iterator=block_items_.begin(); iterator!=block_items_.end(); iterator++) { - if (iterator.value()->isSelected()) { - deselected_blocks.append(iterator.key()); - } - + // Delete all items + for (auto iterator=block_items_.begin(); iterator!=block_items_.end(); iterator++) { delete iterator.value(); } block_items_.clear(); - emit BlocksDeselected(deselected_blocks); + // Emit that we've deselected any selected blocks + SignalDeselectedAllBlocks(); + // Set null timebase SetTimebase(0); } @@ -224,10 +235,6 @@ void TimelineWidget::ScaleChangedEvent(const double &scale) } } - foreach (TimelineViewGhostItem* ghost, ghost_items_) { - ghost->SetScale(scale); - } - foreach (TimelineAndTrackView* view, views_) { view->view()->SetScale(scale); } @@ -271,6 +278,10 @@ void TimelineWidget::DisconnectNodeInternal(ViewerOutput *n) disconnect(n, &ViewerOutput::TimebaseChanged, this, &TimelineWidget::SetTimebase); disconnect(n, &ViewerOutput::TrackHeightChanged, this, &TimelineWidget::TrackHeightChanged); + foreach (TrackOutput* track, n->GetTracks()) { + RemoveTrack(track); + } + ruler()->SetPlaybackCache(nullptr); SetTimebase(0); @@ -341,22 +352,6 @@ void TimelineWidget::PasteNodesFromClipboardInternal(QXmlStreamReader *reader, X } } -TimelineWidget::DraggedFootage TimelineWidget::FootageToDraggedFootage(Footage *f) -{ - return DraggedFootage(f, f->get_enabled_stream_flags()); -} - -QList TimelineWidget::FootageToDraggedFootage(QList footage) -{ - QList df; - - foreach (Footage* f, footage) { - df.append(FootageToDraggedFootage(f)); - } - - return df; -} - rational TimelineWidget::GetToolTipTimebase() const { if (GetConnectedNode() && use_audio_time_units_) { @@ -367,34 +362,28 @@ rational TimelineWidget::GetToolTipTimebase() const void TimelineWidget::SelectAll() { - QList blocks_selected; + QList newly_selected_blocks; - QMap::const_iterator i; - - for (i=block_items_.constBegin(); i!=block_items_.end(); i++) { - if (!i.value()->isSelected()) { - i.value()->setSelected(true); - blocks_selected.append(i.key()); + for (auto it=block_items_.cbegin(); it!=block_items_.cend(); it++) { + if (!selected_blocks_.contains(it.key())) { + newly_selected_blocks.append(it.key()); + AddSelection(it.key()->range(), it.value()->Track()); } } - emit BlocksSelected(blocks_selected); + SignalSelectedBlocks(newly_selected_blocks, false); } void TimelineWidget::DeselectAll() { - QList blocks_deselected; + // Clear selections + selections_.clear(); - QMap::const_iterator i; + // Update all viewports + UpdateViewports(); - for (i=block_items_.constBegin(); i!=block_items_.end(); i++) { - if (i.value()->isSelected()) { - i.value()->setSelected(false); - blocks_deselected.append(i.key()); - } - } - - emit BlocksDeselected(blocks_deselected); + // Clear list and emit signal + SignalDeselectedAllBlocks(); } void TimelineWidget::RippleToIn() @@ -486,8 +475,6 @@ void TimelineWidget::ReplaceBlocksWithGaps(const QList &blocks, new TrackReplaceBlockWithGapCommand(original_track, b, command); if (remove_from_graph) { - new BlockUnlinkAllCommand(b, command); - new NodeRemoveWithExclusiveDeps(static_cast(b->parent()), b, command); } } @@ -497,16 +484,11 @@ void TimelineWidget::DeleteSelected(bool ripple) { QList selected_list = GetSelectedBlocks(); QList blocks_to_delete; - QList tracks_affected; foreach (TimelineViewBlockItem* item, selected_list) { Block* b = item->block(); blocks_to_delete.append(b); - - if (!tracks_affected.contains(item->Track())) { - tracks_affected.append(item->Track()); - } } // No-op if nothing is selected @@ -541,6 +523,9 @@ void TimelineWidget::DeleteSelected(bool ripple) // Replace clips with gaps (effectively deleting them) ReplaceBlocksWithGaps(clips_to_delete, true, command); + // Remove all selections + new TimelineSetSelectionsCommand(this, TimelineWidgetSelections(), GetSelections(), command); + // Insert ripple command now that it's all cleaned up gaps if (ripple) { TimeRangeList range_list; @@ -767,16 +752,8 @@ QList TimelineWidget::GetSelectedBlocks() { QList list; - QMapIterator iterator(block_items_); - - while (iterator.hasNext()) { - iterator.next(); - - TimelineViewBlockItem* item = iterator.value(); - - if (item && item->isSelected()) { - list.append(item); - } + foreach (Block* b, selected_blocks_) { + list.append(block_items_.value(b)); } return list; @@ -825,12 +802,7 @@ void TimelineWidget::ClearGhosts() HideSnaps(); } -bool TimelineWidget::HasGhosts() -{ - return !ghost_items_.isEmpty(); -} - -TimelineWidget::Tool *TimelineWidget::GetActiveTool() +TimelineTool *TimelineWidget::GetActiveTool() { return tools_.at(Core::instance()->tool()); } @@ -857,7 +829,7 @@ void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event) active_tool_->MouseMove(event); } else { // Mouse is not down, attempt a hover event - Tool* hover_tool = GetActiveTool(); + TimelineTool* hover_tool = GetActiveTool(); if (hover_tool) { hover_tool->HoverMove(event); @@ -876,9 +848,8 @@ void TimelineWidget::ViewMouseReleased(TimelineViewMouseEvent *event) void TimelineWidget::ViewMouseDoubleClicked(TimelineViewMouseEvent *event) { - if (GetConnectedNode() && active_tool_ != nullptr) { - active_tool_->MouseDoubleClick(event); - active_tool_ = nullptr; + if (GetConnectedNode()) { + GetActiveTool()->MouseDoubleClick(event); } } @@ -905,40 +876,72 @@ void TimelineWidget::ViewDragDropped(TimelineViewMouseEvent *event) void TimelineWidget::AddBlock(Block *block, TrackReference track) { // Set up clip with view parameters (clip item will automatically size its rect accordingly) - TimelineViewBlockItem* item = new TimelineViewBlockItem(block); + TimelineViewBlockItem* item = block_items_.value(block); - item->SetYCoords(GetTrackY(track), GetTrackHeight(track)); - item->SetScale(GetScale()); - item->SetTrack(track); - item->SetTimebase(timebase()); + if (!item) { - // Add to list of clip items that can be iterated through - block_items_.insert(block, item); + // Add to list of clip items that can be iterated through + item = new TimelineViewBlockItem(block); + block_items_.insert(block, item); - // Add item to graphics scene - views_.at(track.type())->view()->scene()->addItem(item); + // Set scale parameters + item->SetScale(GetScale()); + item->SetTimebase(timebase()); + item->SetYCoords(GetTrackY(track), GetTrackHeight(track)); + item->SetTrack(track); - connect(block, &Block::Refreshed, this, &TimelineWidget::BlockRefreshed); - connect(block, &Block::LinksChanged, this, &TimelineWidget::BlockUpdated); - connect(block, &Block::LabelChanged, this, &TimelineWidget::BlockUpdated); - connect(block, &Block::EnabledChanged, this, &TimelineWidget::BlockUpdated); + // Add item to graphics scene + views_.at(track.type())->view()->scene()->addItem(item); + + connect(block, &Block::Refreshed, this, &TimelineWidget::BlockRefreshed); + connect(block, &Block::LinksChanged, this, &TimelineWidget::BlockUpdated); + connect(block, &Block::LabelChanged, this, &TimelineWidget::BlockUpdated); + connect(block, &Block::EnabledChanged, this, &TimelineWidget::BlockUpdated); + + } else if (item->Track() != track) { + + item->SetYCoords(GetTrackY(track), GetTrackHeight(track)); + item->SetTrack(track); + + } } -void TimelineWidget::RemoveBlock(Block *block) +void TimelineWidget::RemoveBlock(const QList &blocks) { - disconnect(block, &Block::Refreshed, this, &TimelineWidget::BlockRefreshed); - disconnect(block, &Block::LinksChanged, this, &TimelineWidget::BlockUpdated); - disconnect(block, &Block::LabelChanged, this, &TimelineWidget::BlockUpdated); - disconnect(block, &Block::EnabledChanged, this, &TimelineWidget::BlockUpdated); + QList delete_items; + delete_items.reserve(blocks.size()); - TimelineViewBlockItem* item = block_items_.take(block); + QList deselect_blocks; - if (item->isSelected()) { - // Sending a list of one item all the time is not very efficient - emit BlocksDeselected({block}); + foreach (Block* b, blocks) { + // Disconnect all signals + disconnect(b, &Block::Refreshed, this, &TimelineWidget::BlockRefreshed); + disconnect(b, &Block::LinksChanged, this, &TimelineWidget::BlockUpdated); + disconnect(b, &Block::LabelChanged, this, &TimelineWidget::BlockUpdated); + disconnect(b, &Block::EnabledChanged, this, &TimelineWidget::BlockUpdated); + + // Take item from map + TimelineViewBlockItem* item = block_items_.take(b); + + // If selected, deselect it + int select_index = selected_blocks_.indexOf(b); + if (select_index > -1) { + selected_blocks_.removeAt(select_index); + deselect_blocks.append(b); + RemoveSelection(item); + } + + // Finally, delete item + delete item; } - delete item; + if (!deselect_blocks.isEmpty()) { + // We already removed the blocks from selected_blocks_, so we can signal directly rather than + // through + emit BlocksDeselected(deselect_blocks); + } + + qDeleteAll(delete_items); } void TimelineWidget::AddTrack(TrackOutput *track, Timeline::TrackType type) @@ -956,9 +959,7 @@ void TimelineWidget::RemoveTrack(TrackOutput *track) disconnect(track, &TrackOutput::IndexChanged, this, &TimelineWidget::TrackIndexChanged); disconnect(track, &TrackOutput::PreviewChanged, this, &TimelineWidget::TrackPreviewUpdated); - foreach (Block* b, track->Blocks()) { - RemoveBlock(b); - } + RemoveBlock(track->Blocks()); } void TimelineWidget::TrackIndexChanged() @@ -1142,11 +1143,16 @@ void TimelineWidget::ViewTimestampChanged(int64_t ts) emit TimeChanged(ts); } +void TimelineWidget::ToolChanged() +{ + HideSnaps(); +} + void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost) { - ghost->SetScale(GetScale()); ghost_items_.append(ghost); - views_.at(ghost->Track().type())->view()->scene()->addItem(ghost); + + UpdateViewports(ghost->GetTrack().type()); } void TimelineWidget::UpdateViewTimebases() @@ -1174,12 +1180,80 @@ void TimelineWidget::SetBlockLinksSelected(Block* block, bool selected) TimelineViewBlockItem* link_item; foreach (Block* link, block->linked_clips()) { - if ((link_item = block_items_[link]) != nullptr) { - link_item->setSelected(selected); + link_item = block_items_.value(link); + + if (link_item) { + if (selected) { + AddSelection(link_item); + } else { + RemoveSelection(link_item); + } } } } +void TimelineWidget::QueueScroll(int value) +{ + // (using a hacky singleShot so the scroll occurs after the scene and its scrollbars have updated) + deferred_scroll_value_ = value; + + QTimer::singleShot(0, this, &TimelineWidget::DeferredScrollAction); +} + +TimelineView *TimelineWidget::GetFirstTimelineView() +{ + return views_.first()->view(); +} + +const QRect& TimelineWidget::GetRubberBandGeometry() const +{ + return rubberband_.geometry(); +} + +void TimelineWidget::SignalSelectedBlocks(QList input, bool filter) +{ + if (input.isEmpty()) { + return; + } + + if (filter) { + // If filtering, remove all the blocks that are already selected + for (int i=0; i &deselected_blocks) +{ + if (deselected_blocks.isEmpty()) { + return; + } + + foreach (Block* b, deselected_blocks) { + selected_blocks_.removeOne(b); + } + + emit BlocksDeselected(deselected_blocks); +} + +void TimelineWidget::SignalDeselectedAllBlocks() +{ + if (!selected_blocks_.isEmpty()) { + emit BlocksDeselected(selected_blocks_); + selected_blocks_.clear(); + } +} + QVector TimelineWidget::GetEditToInfo(const rational& playhead_time, Timeline::MovementMode mode) { @@ -1321,6 +1395,17 @@ void TimelineWidget::ShowSnap(const QList ×) } } +void TimelineWidget::UpdateViewports(const Timeline::TrackType &type) +{ + if (type == Timeline::kTrackTypeNone) { + foreach (TimelineAndTrackView* tview, views_) { + tview->view()->viewport()->update(); + } + } else { + views_.at(type)->view()->viewport()->update(); + } +} + void TimelineWidget::HideSnaps() { foreach (TimelineAndTrackView* tview, views_) { @@ -1344,12 +1429,8 @@ void TimelineWidget::StartRubberBandSelect(bool enable_selecting, bool select_li rubberband_.show(); // We don't touch any blocks that are already selected. If you want these to be deselected by - // default, call DeselectAll() befoer calling StartRubberBandSelect() - foreach (TimelineViewBlockItem* block, block_items_) { - if (block->isSelected()) { - rubberband_already_selected_.append(block); - } - } + // default, call DeselectAll() before calling StartRubberBandSelect() + rubberband_old_selections_ = selections_; MoveRubberBandSelect(enable_selecting, select_links); } @@ -1364,7 +1445,7 @@ void TimelineWidget::MoveRubberBandSelect(bool enable_selecting, bool select_lin return; } - QList new_selected_list; + QList items_in_rubberband; // Determine all items in the rubberband foreach (TimelineAndTrackView* tview, views_) { @@ -1377,60 +1458,45 @@ void TimelineWidget::MoveRubberBandSelect(bool enable_selecting, bool select_lin // Normalize and get items in rect QList rubberband_items = view->items(mapped_rect.normalized()); - new_selected_list.append(rubberband_items); + items_in_rubberband.append(rubberband_items); } - // Filter out any items that were already selected - if (!rubberband_already_selected_.isEmpty()) { - for (int i=0; isetSelected(false); - } + // Add any blocks in rubberband + rubberband_now_selected_.clear(); - // Cache limit because we append to this array in this loop and don't need to process those - int lim = new_selected_list.size(); - for (int i=0;i(new_selected_list.at(i)); - if (block_item->block()->type() == Block::kGap) { - continue; - } + foreach (QGraphicsItem* item, items_in_rubberband) { + TimelineViewBlockItem* block_item = dynamic_cast(item); - TrackOutput* t = GetTrackFromReference(block_item->Track()); - if (t && t->IsLocked()) { - continue; - } - - // Since new_selected_list is filtered by rubberband_already_selected_, this should certainly - // be deselected by now - block_item->setSelected(true); - - if (select_links) { - // Select the block's links + if (block_item) { Block* b = block_item->block(); - // Add its links to the list - TimelineViewBlockItem* link_item; - foreach (Block* link, b->linked_clips()) { - if ((link_item = block_items_[link]) != nullptr) { - link_item->setSelected(true); + if (b->type() == Block::kGap) { + continue; + } - if (!new_selected_list.contains(link_item) - && !rubberband_already_selected_.contains(link_item)) { - new_selected_list.append(link_item); + TrackOutput* t = GetTrackFromReference(block_item->Track()); + if (t && t->IsLocked()) { + continue; + } + + if (!rubberband_now_selected_.contains(b)) { + AddSelection(block_item); + rubberband_now_selected_.append(b); + } + + if (select_links) { + foreach (Block* link, b->linked_clips()) { + if (!rubberband_now_selected_.contains(link)) { + AddSelection(block_items_.value(link)); + rubberband_now_selected_.append(link); } } } } } - - rubberband_now_selected_ = new_selected_list; } void TimelineWidget::EndRubberBandSelect() @@ -1438,14 +1504,57 @@ void TimelineWidget::EndRubberBandSelect() rubberband_.hide(); // Emit any blocks that were newly selected - QList selected_blocks; - foreach (QGraphicsItem* item, rubberband_now_selected_) { - selected_blocks.append(static_cast(item)->block()); - } - emit BlocksSelected(selected_blocks); + SignalSelectedBlocks(rubberband_now_selected_); rubberband_now_selected_.clear(); - rubberband_already_selected_.clear(); + rubberband_old_selections_.clear(); +} + +void TimelineWidget::AddSelection(const TimeRange &time, const TrackReference &track) +{ + selections_[track].InsertTimeRange(time); + + UpdateViewports(track.type()); +} + +void TimelineWidget::AddSelection(TimelineViewBlockItem *item) +{ + AddSelection(item->block()->range(), item->Track()); +} + +void TimelineWidget::RemoveSelection(const TimeRange &time, const TrackReference &track) +{ + selections_[track].RemoveTimeRange(time); + + UpdateViewports(track.type()); +} + +void TimelineWidget::RemoveSelection(TimelineViewBlockItem *item) +{ + RemoveSelection(item->block()->range(), item->Track()); +} + +void TimelineWidget::SetSelections(const TimelineWidgetSelections &s) +{ + selections_ = s; + + UpdateViewports(); +} + +TimelineViewBlockItem *TimelineWidget::GetItemAtScenePos(const TimelineCoordinate& coord) +{ + for (auto it=block_items_.cbegin(); it!=block_items_.cend(); it++) { + Block* b = it.key(); + TimelineViewBlockItem* item = it.value(); + + if (b->in() <= coord.GetFrame() + && b->out() > coord.GetFrame() + && item->Track() == coord.GetTrack()) { + return item; + } + } + + return nullptr; } struct SnapData { diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 70cccebad..c1be47a75 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -34,6 +34,9 @@ #include "widget/nodecopypaste/nodecopypaste.h" #include "widget/slider/timeslider.h" #include "widget/timebased/timebased.h" +#include "widget/timelinewidget/timelinewidgetselections.h" +#include "widget/timelinewidget/tool/import.h" +#include "widget/timelinewidget/tool/tool.h" OLIVE_NAMESPACE_ENTER @@ -46,13 +49,6 @@ class TimelineWidget : public TimeBasedWidget, public NodeCopyPasteWidget, publi { Q_OBJECT public: - enum DropWithoutSequenceBehavior { - kDWSAsk, - kDWSAuto, - kDWSManual, - kDWSDisable - }; - TimelineWidget(QWidget* parent = nullptr); virtual ~TimelineWidget() override; @@ -103,6 +99,104 @@ public: void RestoreSplitterState(const QByteArray& state); + static void ReplaceBlocksWithGaps(const QList& blocks, bool remove_from_graph, QUndoCommand* command); + + /** + * @brief Retrieve the QGraphicsItem at a particular scene position + * + * Requires a float-based scene position. If you have a screen position, use GetScenePos() first to convert it to a + * scene position + */ + TimelineViewBlockItem* GetItemAtScenePos(const TimelineCoordinate &coord); + + const QMap& GetBlockItems() const + { + return block_items_; + } + + void AddSelection(const TimeRange& time, const TrackReference& track); + void AddSelection(TimelineViewBlockItem* item); + + void RemoveSelection(const TimeRange& time, const TrackReference& track); + void RemoveSelection(TimelineViewBlockItem* item); + + const TimelineWidgetSelections& GetSelections() const + { + return selections_; + } + + void SetSelections(const TimelineWidgetSelections &s); + + TrackOutput* GetTrackFromReference(const TrackReference& ref); + + void SetViewBeamCursor(const TimelineCoordinate& coord); + + const QVector& GetGhostItems() const + { + return ghost_items_; + } + + void InsertGapsAt(const rational& time, const rational& length, QUndoCommand* command); + + void StartRubberBandSelect(bool enable_selecting, bool select_links); + void MoveRubberBandSelect(bool enable_selecting, bool select_links); + void EndRubberBandSelect(); + + int GetTrackY(const TrackReference& ref); + int GetTrackHeight(const TrackReference& ref); + + void AddGhost(TimelineViewGhostItem* ghost); + + void ClearGhosts(); + + bool HasGhosts() const + { + return !ghost_items_.isEmpty(); + } + + rational GetToolTipTimebase() const; + + bool IsBlockSelected(Block* b) const + { + return selected_blocks_.contains(b); + } + + void SetBlockLinksSelected(Block *block, bool selected); + + void QueueScroll(int value); + + TimelineView* GetFirstTimelineView(); + + const QRect &GetRubberBandGeometry() const; + + /** + * @brief Track blocks that have newly been selected (this is preferred over emitting BlocksSelected directly) + * + * TimelineWidget keeps track of which blocks are selected internally. Calling this function will + * add to that list and emit a signal to other widgets that said blocks have been selected. + * + * @param selected_blocks + * + * The list of blocks to add to the internal selection list and signal. + * + * @param filter + * + * TRUE to automatically filter blocks that are already selected from the list. In most cases, + * this is preferable and should only be set to FALSE if the list is guaranteed not to contain + * already selected blocks (and therefore filtering can be skipped to save time). + */ + void SignalSelectedBlocks(QList selected_blocks, bool filter = true); + + /** + * @brief Track blocks that have been newly deselected + */ + void SignalDeselectedBlocks(const QList& deselected_blocks); + + /** + * @brief Convenience function to deselect all blocks and signal them + */ + void SignalDeselectedAllBlocks(); + signals: void BlocksSelected(const QList& selected_blocks); @@ -129,341 +223,6 @@ protected: }; private: - class DraggedFootage { - public: - DraggedFootage(Footage* f, quint64 streams) : - footage_(f), - streams_(streams) - { - } - - Footage* footage() const { - return footage_; - } - - const quint64& streams() const { - return streams_; - } - - private: - Footage* footage_; - - quint64 streams_; - - }; - - static DraggedFootage FootageToDraggedFootage(Footage* f); - static QList FootageToDraggedFootage(QList footage); - - class Tool - { - public: - Tool(TimelineWidget* parent); - virtual ~Tool(); - - virtual void MousePress(TimelineViewMouseEvent *){} - virtual void MouseMove(TimelineViewMouseEvent *){} - virtual void MouseRelease(TimelineViewMouseEvent *){} - virtual void MouseDoubleClick(TimelineViewMouseEvent *){} - - virtual void HoverMove(TimelineViewMouseEvent *){} - - virtual void DragEnter(TimelineViewMouseEvent *){} - virtual void DragMove(TimelineViewMouseEvent *){} - virtual void DragLeave(QDragLeaveEvent *){} - virtual void DragDrop(TimelineViewMouseEvent *){} - - TimelineWidget* parent(); - - static Timeline::MovementMode FlipTrimMode(const Timeline::MovementMode& trim_mode); - - protected: - /** - * @brief Retrieve the QGraphicsItem at a particular scene position - * - * Requires a float-based scene position. If you have a screen position, use GetScenePos() first to convert it to a - * scene position - */ - TimelineViewBlockItem* GetItemAtScenePos(const TimelineCoordinate &coord); - - /** - * @brief Validates Ghosts that are moving horizontally (time-based) - * - * 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 ValidateTimeMovement(rational movement); - - /** - * @brief Validates Ghosts that are moving vertically (track-based) - * - * This function's validation ensures that no Ghost's track ends up in a negative (non-existent) track. - */ - int ValidateTrackMovement(int movement, const QVector &ghosts); - - void GetGhostData(rational *earliest_point, rational *latest_point); - - void InsertGapsAtGhostDestination(QUndoCommand* command); - - QList snap_points_; - - bool dragging_; - - TimelineCoordinate drag_start_; - - private: - TimelineWidget* parent_; - - }; - - class BeamTool : public Tool - { - public: - BeamTool(TimelineWidget *parent); - - virtual void HoverMove(TimelineViewMouseEvent *event) override; - - protected: - TimelineCoordinate ValidatedCoordinate(TimelineCoordinate coord); - - }; - - class PointerTool : public Tool - { - public: - PointerTool(TimelineWidget* parent); - - virtual void MousePress(TimelineViewMouseEvent *event) override; - virtual void MouseMove(TimelineViewMouseEvent *event) override; - virtual void MouseRelease(TimelineViewMouseEvent *event) override; - - virtual void HoverMove(TimelineViewMouseEvent *event) override; - - protected: - virtual void FinishDrag(TimelineViewMouseEvent *event); - - virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode); - - TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode, bool check_if_exists = false); - - TimelineViewGhostItem* AddGhostFromNull(const rational& in, const rational& out, const TrackReference& track, Timeline::MovementMode mode); - - /** - * @brief Validates Ghosts that are getting their in points trimmed - * - * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no - * Ghost's length becomes 0 or negative. - */ - rational ValidateInTrimming(rational movement); - - /** - * @brief Validates Ghosts that are getting their out points trimmed - * - * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no - * Ghost's length becomes 0 or negative. - */ - rational ValidateOutTrimming(rational movement); - - virtual void ProcessDrag(const TimelineCoordinate &mouse_pos); - - void InitiateDragInternal(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode, - bool dont_roll_trims, - bool allow_nongap_rolling, bool slide_instead_of_moving); - - 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 SetGapTrimmingAllowed(bool e) - { - gap_trimming_allowed_ = e; - } - - private: - Timeline::MovementMode IsCursorInTrimHandle(TimelineViewBlockItem* block, qreal cursor_x); - - void AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode); - - bool IsClipTrimmable(TimelineViewBlockItem* clip, - const QList& items, - const Timeline::MovementMode& mode); - - void ProcessGhostsForSliding(); - - void ProcessGhostsForRolling(); - - bool AddMovingTransitionsToClipGhost(Block *block, const TrackReference &track, Timeline::MovementMode movement, const QList &selected_items); - - bool movement_allowed_; - bool trimming_allowed_; - bool track_movement_allowed_; - bool gap_trimming_allowed_; - bool rubberband_selecting_; - - Timeline::TrackType drag_track_type_; - Timeline::MovementMode drag_movement_mode_; - - TimelineViewBlockItem* clicked_item_; - - }; - - class ImportTool : public Tool - { - public: - ImportTool(TimelineWidget* parent); - - virtual void DragEnter(TimelineViewMouseEvent *event) override; - virtual void DragMove(TimelineViewMouseEvent *event) override; - virtual void DragLeave(QDragLeaveEvent *event) override; - virtual void DragDrop(TimelineViewMouseEvent *event) override; - - void PlaceAt(const QList &footage, const rational& start, bool insert); - void PlaceAt(const QList &footage, const rational& start, bool insert); - - private: - void FootageToGhosts(rational ghost_start, const QList& footage, const rational &dest_tb, const int &track_start); - - void PrepGhosts(const rational &frame, const int &track_index); - - void DropGhosts(bool insert); - - QList dragged_footage_; - - int import_pre_buffer_; - - }; - - class EditTool : public BeamTool - { - public: - EditTool(TimelineWidget* parent); - - virtual void MousePress(TimelineViewMouseEvent *event) override; - virtual void MouseMove(TimelineViewMouseEvent *event) override; - virtual void MouseRelease(TimelineViewMouseEvent *event) override; - }; - - class RazorTool : public BeamTool - { - public: - RazorTool(TimelineWidget* parent); - - virtual void MousePress(TimelineViewMouseEvent *event) override; - virtual void MouseMove(TimelineViewMouseEvent *event) override; - virtual void MouseRelease(TimelineViewMouseEvent *event) override; - - private: - QVector split_tracks_; - }; - - class RippleTool : public PointerTool - { - public: - RippleTool(TimelineWidget* parent); - protected: - virtual void FinishDrag(TimelineViewMouseEvent *event) override; - - virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode) override; - }; - - class RollingTool : public PointerTool - { - public: - RollingTool(TimelineWidget* parent); - - protected: - virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode) override; - }; - - class SlideTool : public PointerTool - { - public: - SlideTool(TimelineWidget* parent); - - protected: - virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, - Timeline::MovementMode trim_mode) override; - - }; - - class SlipTool : public PointerTool - { - public: - SlipTool(TimelineWidget* parent); - - protected: - virtual void ProcessDrag(const TimelineCoordinate &mouse_pos) override; - virtual void FinishDrag(TimelineViewMouseEvent *event) override; - }; - - class ZoomTool : public Tool - { - public: - ZoomTool(TimelineWidget* parent); - - virtual void MousePress(TimelineViewMouseEvent *event) override; - virtual void MouseMove(TimelineViewMouseEvent *event) override; - virtual void MouseRelease(TimelineViewMouseEvent *event) override; - - }; - - class AddTool : public BeamTool - { - public: - AddTool(TimelineWidget* parent); - - virtual void MousePress(TimelineViewMouseEvent *event) override; - virtual void MouseMove(TimelineViewMouseEvent *event) override; - virtual void MouseRelease(TimelineViewMouseEvent *event) override; - - protected: - void MouseMoveInternal(const rational& cursor_frame, bool outwards); - - TimelineViewGhostItem* ghost_; - - rational drag_start_point_; - }; - - class TransitionTool : public AddTool - { - public: - TransitionTool(TimelineWidget* parent); - - virtual void MousePress(TimelineViewMouseEvent *event) override; - virtual void MouseMove(TimelineViewMouseEvent *event) override; - virtual void MouseRelease(TimelineViewMouseEvent *event) override; - private: - bool dual_transition_; - }; - - rational GetToolTipTimebase() const; - - void InsertGapsAt(const rational& time, const rational& length, QUndoCommand* command); - - void ReplaceBlocksWithGaps(const QList& blocks, bool remove_from_graph, QUndoCommand* command); - - void SetBlockLinksSelected(Block *block, bool selected); - QVector GetEditToInfo(const rational &playhead_time, Timeline::MovementMode mode); void RippleTo(Timeline::MovementMode mode); @@ -472,54 +231,44 @@ private: void ShowSnap(const QList& times); + void UpdateViewports(const Timeline::TrackType& type = Timeline::kTrackTypeNone); + QPoint drag_origin_; - void StartRubberBandSelect(bool enable_selecting, bool select_links); - void MoveRubberBandSelect(bool enable_selecting, bool select_links); - void EndRubberBandSelect(); QRubberBand rubberband_; - QList rubberband_already_selected_; - QList rubberband_now_selected_; + TimelineWidgetSelections rubberband_old_selections_; + QList rubberband_now_selected_; - Tool* GetActiveTool(); + TimelineWidgetSelections selections_; - QVector tools_; + TimelineTool* GetActiveTool(); + + QVector tools_; ImportTool* import_tool_; - Tool* active_tool_; - - void ClearGhosts(); - - bool HasGhosts(); + TimelineTool* active_tool_; QVector ghost_items_; QMap block_items_; - TrackOutput* GetTrackFromReference(const TrackReference& ref); - QList views_; TimeSlider* timecode_label_; + QList selected_blocks_; + int deferred_scroll_value_; bool use_audio_time_units_; QSplitter* view_splitter_; - int GetTrackY(const TrackReference& ref); - int GetTrackHeight(const TrackReference& ref); - void CenterOn(qreal scene_pos); - void AddGhost(TimelineViewGhostItem* ghost); - void UpdateViewTimebases(); - void SetViewBeamCursor(const TimelineCoordinate& coord); - private slots: void ViewMousePressed(TimelineViewMouseEvent* event); void ViewMouseMoved(TimelineViewMouseEvent* event); @@ -532,7 +281,7 @@ private slots: void ViewDragDropped(TimelineViewMouseEvent* event); void AddBlock(Block* block, TrackReference track); - void RemoveBlock(Block* block); + void RemoveBlock(const QList& blocks); void AddTrack(TrackOutput* track, Timeline::TrackType type); void RemoveTrack(TrackOutput* track); @@ -568,6 +317,8 @@ private slots: void ViewTimestampChanged(int64_t ts); + void ToolChanged(); + }; OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/timelinewidgetselections.cpp b/app/widget/timelinewidget/timelinewidgetselections.cpp new file mode 100644 index 000000000..951b80946 --- /dev/null +++ b/app/widget/timelinewidget/timelinewidgetselections.cpp @@ -0,0 +1,77 @@ +/*** + + 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 "timelinewidgetselections.h" + +OLIVE_NAMESPACE_ENTER + +void TimelineWidgetSelections::ShiftTime(const rational &diff) +{ + for (auto it=this->begin(); it!=this->end(); it++) { + for (auto it2=it.value().begin(); it2!=it.value().end(); it2++) { + (*it2) += diff; + } + } +} + +void TimelineWidgetSelections::ShiftTracks(Timeline::TrackType type, int diff) +{ + TimelineWidgetSelections cached_selections; + + { + // Take all selections of this track type + auto it = this->begin(); + while (it != this->end()) { + if (it.key().type() == type) { + cached_selections.insert(it.key(), it.value()); + it = this->erase(it); + } else { + it++; + } + } + } + + // Then re-insert them with the diff applied + for (auto it=cached_selections.cbegin(); it!=cached_selections.cend(); it++) { + TrackReference ref(it.key().type(), it.key().index() + diff); + + this->insert(ref, it.value()); + } +} + +void TimelineWidgetSelections::TrimIn(const rational &diff) +{ + for (auto it=this->begin(); it!=this->end(); it++) { + for (auto it2=it.value().begin(); it2!=it.value().end(); it2++) { + (*it2).set_in((*it2).in() + diff); + } + } +} + +void TimelineWidgetSelections::TrimOut(const rational &diff) +{ + for (auto it=this->begin(); it!=this->end(); it++) { + for (auto it2=it.value().begin(); it2!=it.value().end(); it2++) { + (*it2).set_out((*it2).out() + diff); + } + } +} + +OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/timelinewidgetselections.h b/app/widget/timelinewidget/timelinewidgetselections.h new file mode 100644 index 000000000..1413bbac0 --- /dev/null +++ b/app/widget/timelinewidget/timelinewidgetselections.h @@ -0,0 +1,48 @@ +/*** + + 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 TIMELINEWIDGETSELECTIONS_H +#define TIMELINEWIDGETSELECTIONS_H + +#include + +#include "common/timerange.h" +#include "timeline/trackreference.h" + +OLIVE_NAMESPACE_ENTER + +class TimelineWidgetSelections : public QHash +{ +public: + TimelineWidgetSelections() = default; + + void ShiftTime(const rational& diff); + + void ShiftTracks(Timeline::TrackType type, int diff); + + void TrimIn(const rational& diff); + + void TrimOut(const rational& diff); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // TIMELINEWIDGETSELECTIONS_H diff --git a/app/widget/timelinewidget/tool/CMakeLists.txt b/app/widget/timelinewidget/tool/CMakeLists.txt index f14d4a8de..4e9850fc0 100644 --- a/app/widget/timelinewidget/tool/CMakeLists.txt +++ b/app/widget/timelinewidget/tool/CMakeLists.txt @@ -17,17 +17,30 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/timelinewidget/tool/add.cpp + widget/timelinewidget/tool/add.h widget/timelinewidget/tool/beam.cpp + widget/timelinewidget/tool/beam.h widget/timelinewidget/tool/edit.cpp + widget/timelinewidget/tool/edit.h widget/timelinewidget/tool/import.cpp + widget/timelinewidget/tool/import.h widget/timelinewidget/tool/pointer.cpp + widget/timelinewidget/tool/pointer.h widget/timelinewidget/tool/razor.cpp + widget/timelinewidget/tool/razor.h widget/timelinewidget/tool/ripple.cpp + widget/timelinewidget/tool/ripple.h widget/timelinewidget/tool/rolling.cpp + widget/timelinewidget/tool/rolling.h widget/timelinewidget/tool/slide.cpp + widget/timelinewidget/tool/slide.h widget/timelinewidget/tool/slip.cpp + widget/timelinewidget/tool/slip.h widget/timelinewidget/tool/transition.cpp + widget/timelinewidget/tool/transition.h widget/timelinewidget/tool/tool.cpp + widget/timelinewidget/tool/tool.h widget/timelinewidget/tool/zoom.cpp + widget/timelinewidget/tool/zoom.h PARENT_SCOPE ) diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index f488bbfca..4ea4831f9 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -20,6 +20,7 @@ #include "widget/timelinewidget/timelinewidget.h" +#include "add.h" #include "core.h" #include "node/factory.h" #include "node/generator/solid/solid.h" @@ -28,13 +29,13 @@ OLIVE_NAMESPACE_ENTER -TimelineWidget::AddTool::AddTool(TimelineWidget *parent) : +AddTool::AddTool(TimelineWidget *parent) : BeamTool(parent), ghost_(nullptr) { } -void TimelineWidget::AddTool::MousePress(TimelineViewMouseEvent *event) +void AddTool::MousePress(TimelineViewMouseEvent *event) { const TrackReference& track = event->GetTrack(); @@ -71,14 +72,13 @@ void TimelineWidget::AddTool::MousePress(TimelineViewMouseEvent *event) ghost_->SetIn(drag_start_point_); ghost_->SetOut(drag_start_point_); ghost_->SetTrack(track); - ghost_->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track)); parent()->AddGhost(ghost_); snap_points_.append(drag_start_point_); } } -void TimelineWidget::AddTool::MouseMove(TimelineViewMouseEvent *event) +void AddTool::MouseMove(TimelineViewMouseEvent *event) { if (!ghost_) { return; @@ -87,16 +87,16 @@ void TimelineWidget::AddTool::MouseMove(TimelineViewMouseEvent *event) MouseMoveInternal(event->GetFrame(), event->GetModifiers() & Qt::AltModifier); } -void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) +void AddTool::MouseRelease(TimelineViewMouseEvent *event) { - const TrackReference& track = ghost_->Track(); + const TrackReference& track = ghost_->GetTrack(); if (ghost_) { - if (!ghost_->AdjustedLength().isNull()) { + if (!ghost_->GetAdjustedLength().isNull()) { QUndoCommand* command = new QUndoCommand(); ClipBlock* clip = new ClipBlock(); - clip->set_length_and_media_out(ghost_->AdjustedLength()); + clip->set_length_and_media_out(ghost_->GetAdjustedLength()); clip->SetLabel(OLIVE_NAMESPACE::Tool::GetAddableObjectName(Core::instance()->GetSelectedAddableObject())); NodeGraph* graph = static_cast(parent()->GetConnectedNode()->parent()); @@ -156,14 +156,14 @@ void TimelineWidget::AddTool::MouseRelease(TimelineViewMouseEvent *event) } } -void TimelineWidget::AddTool::MouseMoveInternal(const rational &cursor_frame, bool outwards) +void AddTool::MouseMoveInternal(const rational &cursor_frame, bool outwards) { // Calculate movement rational movement = cursor_frame - drag_start_point_; // Validation: Ensure in point never goes below 0 - if (movement < -ghost_->In() || (outwards && -movement < -ghost_->In())) { - movement = -ghost_->In(); + if (movement < -ghost_->GetIn() || (outwards && -movement < -ghost_->GetIn())) { + movement = -ghost_->GetIn(); } // Snap movement diff --git a/app/widget/timelinewidget/tool/add.h b/app/widget/timelinewidget/tool/add.h new file mode 100644 index 000000000..eabbdf3c9 --- /dev/null +++ b/app/widget/timelinewidget/tool/add.h @@ -0,0 +1,47 @@ +/*** + + 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 ADDTIMELINETOOL_H +#define ADDTIMELINETOOL_H + +#include "beam.h" + +OLIVE_NAMESPACE_ENTER + +class AddTool : public BeamTool +{ +public: + AddTool(TimelineWidget* parent); + + virtual void MousePress(TimelineViewMouseEvent *event) override; + virtual void MouseMove(TimelineViewMouseEvent *event) override; + virtual void MouseRelease(TimelineViewMouseEvent *event) override; + +protected: + void MouseMoveInternal(const rational& cursor_frame, bool outwards); + + TimelineViewGhostItem* ghost_; + + rational drag_start_point_; +}; + +OLIVE_NAMESPACE_EXIT + +#endif // ADDTIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/beam.cpp b/app/widget/timelinewidget/tool/beam.cpp index e2fe1120e..574f2077e 100644 --- a/app/widget/timelinewidget/tool/beam.cpp +++ b/app/widget/timelinewidget/tool/beam.cpp @@ -18,21 +18,22 @@ ***/ +#include "beam.h" #include "widget/timelinewidget/timelinewidget.h" OLIVE_NAMESPACE_ENTER -TimelineWidget::BeamTool::BeamTool(TimelineWidget *parent) : - Tool(parent) +BeamTool::BeamTool(TimelineWidget *parent) : + TimelineTool(parent) { } -void TimelineWidget::BeamTool::HoverMove(TimelineViewMouseEvent *event) +void BeamTool::HoverMove(TimelineViewMouseEvent *event) { parent()->SetViewBeamCursor(ValidatedCoordinate(event->GetCoordinates(true))); } -TimelineCoordinate TimelineWidget::BeamTool::ValidatedCoordinate(TimelineCoordinate coord) +TimelineCoordinate BeamTool::ValidatedCoordinate(TimelineCoordinate coord) { if (Core::instance()->snapping()) { rational movement; diff --git a/app/widget/timelinewidget/tool/beam.h b/app/widget/timelinewidget/tool/beam.h new file mode 100644 index 000000000..7949474c2 --- /dev/null +++ b/app/widget/timelinewidget/tool/beam.h @@ -0,0 +1,42 @@ +/*** + + 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 BEAMTIMELINETOOL_H +#define BEAMTIMELINETOOL_H + +#include "tool.h" + +OLIVE_NAMESPACE_ENTER + +class BeamTool : public TimelineTool +{ +public: + BeamTool(TimelineWidget *parent); + + virtual void HoverMove(TimelineViewMouseEvent *event) override; + +protected: + TimelineCoordinate ValidatedCoordinate(TimelineCoordinate coord); + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // BEAMTIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/edit.cpp b/app/widget/timelinewidget/tool/edit.cpp index cb804bebc..142aca321 100644 --- a/app/widget/timelinewidget/tool/edit.cpp +++ b/app/widget/timelinewidget/tool/edit.cpp @@ -18,28 +18,71 @@ ***/ +#include "edit.h" #include "widget/timelinewidget/timelinewidget.h" OLIVE_NAMESPACE_ENTER -TimelineWidget::EditTool::EditTool(TimelineWidget* parent) : +EditTool::EditTool(TimelineWidget* parent) : BeamTool(parent) { } -void TimelineWidget::EditTool::MousePress(TimelineViewMouseEvent *event) +void EditTool::MousePress(TimelineViewMouseEvent *event) { - Q_UNUSED(event) + if (!(event->GetModifiers() & Qt::ShiftModifier)) { + parent()->DeselectAll(); + } } -void TimelineWidget::EditTool::MouseMove(TimelineViewMouseEvent *event) +void EditTool::MouseMove(TimelineViewMouseEvent *event) { - Q_UNUSED(event) + if (dragging_) { + rational end_frame = event->GetFrame(true); + + if (Core::instance()->snapping()) { + rational movement; + parent()->SnapPoint({end_frame}, &movement); + if (!movement.isNull()) { + end_frame += movement; + } + } + + parent()->SetSelections(start_selections_); + parent()->AddSelection(TimeRange(start_coord_.GetFrame(), end_frame), + start_coord_.GetTrack()); + } else { + start_selections_ = parent()->GetSelections(); + + dragging_ = true; + + start_coord_ = event->GetCoordinates(true); + + // Snap if we're snapping + if (Core::instance()->snapping()) { + rational movement; + parent()->SnapPoint({start_coord_.GetFrame()}, &movement); + if (!movement.isNull()) { + start_coord_.SetFrame(start_coord_.GetFrame() + movement); + } + } + + dragging_ = true; + } } -void TimelineWidget::EditTool::MouseRelease(TimelineViewMouseEvent *event) +void EditTool::MouseRelease(TimelineViewMouseEvent *event) { - Q_UNUSED(event) + dragging_ = false; +} + +void EditTool::MouseDoubleClick(TimelineViewMouseEvent *event) +{ + TimelineViewBlockItem* item = parent()->GetItemAtScenePos(event->GetCoordinates()); + + if (item && !parent()->GetTrackFromReference(item->Track())->IsLocked()) { + parent()->AddSelection(item); + } } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/edit.h b/app/widget/timelinewidget/tool/edit.h new file mode 100644 index 000000000..9f1e40be3 --- /dev/null +++ b/app/widget/timelinewidget/tool/edit.h @@ -0,0 +1,49 @@ +/*** + + 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 EDITTIMELINETOOL_H +#define EDITTIMELINETOOL_H + +#include "beam.h" +#include "tool.h" +#include "widget/timelinewidget/timelinewidgetselections.h" + +OLIVE_NAMESPACE_ENTER + +class EditTool : public BeamTool +{ +public: + EditTool(TimelineWidget* parent); + + virtual void MousePress(TimelineViewMouseEvent *event) override; + virtual void MouseMove(TimelineViewMouseEvent *event) override; + virtual void MouseRelease(TimelineViewMouseEvent *event) override; + virtual void MouseDoubleClick(TimelineViewMouseEvent *event) override; + +private: + TimelineWidgetSelections start_selections_; + + TimelineCoordinate start_coord_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // EDITTIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 78e4542e2..f5ae5ec83 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -59,14 +59,14 @@ Timeline::TrackType TrackTypeFromStreamType(Stream::Type stream_type) return Timeline::kTrackTypeNone; } -TimelineWidget::ImportTool::ImportTool(TimelineWidget *parent) : - Tool(parent) +ImportTool::ImportTool(TimelineWidget *parent) : + TimelineTool(parent) { // Calculate width used for importing to give ghosts a slight lead-in so the ghosts aren't right on the cursor import_pre_buffer_ = QFontMetricsWidth(parent->fontMetrics(), "HHHHHHHH"); } -void TimelineWidget::ImportTool::DragEnter(TimelineViewMouseEvent *event) +void ImportTool::DragEnter(TimelineViewMouseEvent *event) { QStringList mime_formats = event->GetMimeData()->formats(); @@ -118,7 +118,7 @@ void TimelineWidget::ImportTool::DragEnter(TimelineViewMouseEvent *event) } } -void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) +void ImportTool::DragMove(TimelineViewMouseEvent *event) { if (!dragged_footage_.isEmpty()) { @@ -127,27 +127,24 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) int track_movement = event->GetTrack().index() - drag_start_.GetTrack().index(); time_movement = ValidateTimeMovement(time_movement); - track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); + track_movement = ValidateTrackMovement(track_movement, parent()->GetGhostItems()); // If snapping is enabled, check for snap points if (Core::instance()->snapping()) { parent()->SnapPoint(snap_points_, &time_movement); time_movement = ValidateTimeMovement(time_movement); - track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); + track_movement = ValidateTrackMovement(track_movement, parent()->GetGhostItems()); } rational earliest_ghost = RATIONAL_MAX; // Move ghosts to the mouse cursor - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { ghost->SetInAdjustment(time_movement); ghost->SetOutAdjustment(time_movement); ghost->SetTrackAdjustment(track_movement); - TrackReference adjusted_track = ghost->GetAdjustedTrack(); - ghost->SetYCoords(parent()->GetTrackY(adjusted_track), parent()->GetTrackHeight(adjusted_track)); - earliest_ghost = qMin(earliest_ghost, ghost->GetAdjustedIn()); } @@ -171,7 +168,7 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) } } -void TimelineWidget::ImportTool::DragLeave(QDragLeaveEvent* event) +void ImportTool::DragLeave(QDragLeaveEvent* event) { if (!dragged_footage_.isEmpty()) { parent()->ClearGhosts(); @@ -183,7 +180,7 @@ void TimelineWidget::ImportTool::DragLeave(QDragLeaveEvent* event) } } -void TimelineWidget::ImportTool::DragDrop(TimelineViewMouseEvent *event) +void ImportTool::DragDrop(TimelineViewMouseEvent *event) { if (!dragged_footage_.isEmpty()) { DropGhosts(event->GetModifiers() & Qt::ControlModifier); @@ -194,12 +191,12 @@ void TimelineWidget::ImportTool::DragDrop(TimelineViewMouseEvent *event) } } -void TimelineWidget::ImportTool::PlaceAt(const QList &footage, const rational &start, bool insert) +void ImportTool::PlaceAt(const QList &footage, const rational &start, bool insert) { PlaceAt(FootageToDraggedFootage(footage), start, insert); } -void TimelineWidget::ImportTool::PlaceAt(const QList &footage, const rational &start, bool insert) +void ImportTool::PlaceAt(const QList &footage, const rational &start, bool insert) { dragged_footage_ = footage; @@ -211,7 +208,7 @@ void TimelineWidget::ImportTool::PlaceAt(const QList &footage, c DropGhosts(insert); } -void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QList &footage_list, const rational& dest_tb, const int& track_start) +void ImportTool::FootageToGhosts(rational ghost_start, const QList &footage_list, const rational& dest_tb, const int& track_start) { foreach (const DraggedFootage& footage, footage_list) { @@ -262,7 +259,7 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi // Increment track count for this track type track_offsets[track_type]++; - ghost->setData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(stream)); + ghost->SetData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(stream)); ghost->SetMode(Timeline::kMove); footage_ghosts.append(ghost); @@ -278,8 +275,8 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi ghost->SetIn(ghost_start); ghost->SetOut(ghost_start + footage_duration); - snap_points_.append(ghost->In()); - snap_points_.append(ghost->Out()); + snap_points_.append(ghost->GetIn()); + snap_points_.append(ghost->GetOut()); parent()->AddGhost(ghost); } @@ -290,7 +287,7 @@ void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QLi } } -void TimelineWidget::ImportTool::PrepGhosts(const rational& frame, const int& track_index) +void ImportTool::PrepGhosts(const rational& frame, const int& track_index) { if (parent()->GetConnectedNode()) { FootageToGhosts(frame, @@ -300,7 +297,7 @@ void TimelineWidget::ImportTool::PrepGhosts(const rational& frame, const int& tr } } -void TimelineWidget::ImportTool::DropGhosts(bool insert) +void ImportTool::DropGhosts(bool insert) { QUndoCommand* command = new QUndoCommand(); @@ -398,21 +395,21 @@ void TimelineWidget::ImportTool::DropGhosts(bool insert) if (dst_graph) { - QVector block_items(parent()->ghost_items_.size()); + QVector block_items(parent()->GetGhostItems().size()); // Check if we're inserting if (insert) { InsertGapsAtGhostDestination(command); } - for (int i=0;ighost_items_.size();i++) { - TimelineViewGhostItem* ghost = parent()->ghost_items_.at(i); + for (int i=0;iGetGhostItems().size();i++) { + TimelineViewGhostItem* ghost = parent()->GetGhostItems().at(i); - StreamPtr footage_stream = ghost->data(TimelineViewGhostItem::kAttachedFootage).value(); + StreamPtr footage_stream = ghost->GetData(TimelineViewGhostItem::kAttachedFootage).value(); ClipBlock* clip = new ClipBlock(); - clip->set_media_in(ghost->MediaIn()); - clip->set_length_and_media_out(ghost->Length()); + clip->set_media_in(ghost->GetMediaIn()); + clip->set_length_and_media_out(ghost->GetLength()); clip->SetLabel(footage_stream->footage()->name()); new NodeAddCommand(dst_graph, clip, command); @@ -466,7 +463,7 @@ void TimelineWidget::ImportTool::DropGhosts(bool insert) // Link any clips so far that share the same Footage with this one for (int j=0;jghost_items_.at(j)->data(TimelineViewGhostItem::kAttachedFootage).value(); + StreamPtr footage_compare = parent()->GetGhostItems().at(j)->GetData(TimelineViewGhostItem::kAttachedFootage).value(); if (footage_compare->footage() == footage_stream->footage()) { Block::Link(block_items.at(j), clip); @@ -485,4 +482,25 @@ void TimelineWidget::ImportTool::DropGhosts(bool insert) } } +ImportTool::DraggedFootage ImportTool::FootageToDraggedFootage(Footage *f) +{ + return DraggedFootage(f, f->get_enabled_stream_flags()); +} + +QList ImportTool::FootageToDraggedFootage(QList footage) +{ + QList df; + + foreach (Footage* f, footage) { + df.append(FootageToDraggedFootage(f)); + } + + return df; +} + +QString ImportTool::tr(const char *s) +{ + return QCoreApplication::translate("ImportTool", s); +} + OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/import.h b/app/widget/timelinewidget/tool/import.h new file mode 100644 index 000000000..a90c82c49 --- /dev/null +++ b/app/widget/timelinewidget/tool/import.h @@ -0,0 +1,91 @@ +/*** + + 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 IMPORTTIMELINETOOL_H +#define IMPORTTIMELINETOOL_H + +#include "tool.h" + +OLIVE_NAMESPACE_ENTER + +class ImportTool : public TimelineTool +{ +public: + ImportTool(TimelineWidget* parent); + + virtual void DragEnter(TimelineViewMouseEvent *event) override; + virtual void DragMove(TimelineViewMouseEvent *event) override; + virtual void DragLeave(QDragLeaveEvent *event) override; + virtual void DragDrop(TimelineViewMouseEvent *event) override; + + class DraggedFootage { + public: + DraggedFootage(Footage* f, quint64 streams) : + footage_(f), + streams_(streams) + { + } + + Footage* footage() const { + return footage_; + } + + const quint64& streams() const { + return streams_; + } + + private: + Footage* footage_; + + quint64 streams_; + + }; + + void PlaceAt(const QList &footage, const rational& start, bool insert); + void PlaceAt(const QList &footage, const rational& start, bool insert); + + enum DropWithoutSequenceBehavior { + kDWSAsk, + kDWSAuto, + kDWSManual, + kDWSDisable + }; + +private: + static DraggedFootage FootageToDraggedFootage(Footage* f); + static QList FootageToDraggedFootage(QList footage); + + QString tr(const char* s); + + void FootageToGhosts(rational ghost_start, const QList& footage, const rational &dest_tb, const int &track_start); + + void PrepGhosts(const rational &frame, const int &track_index); + + void DropGhosts(bool insert); + + QList dragged_footage_; + + int import_pre_buffer_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // IMPORTTIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 9889a30a3..7f4f8e772 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -32,12 +32,13 @@ #include "core.h" #include "node/block/gap/gap.h" #include "node/block/transition/transition.h" +#include "pointer.h" #include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER -TimelineWidget::PointerTool::PointerTool(TimelineWidget *parent) : - Tool(parent), +PointerTool::PointerTool(TimelineWidget *parent) : + TimelineTool(parent), movement_allowed_(true), trimming_allowed_(true), track_movement_allowed_(true), @@ -46,13 +47,12 @@ TimelineWidget::PointerTool::PointerTool(TimelineWidget *parent) : { } -void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) +void PointerTool::MousePress(TimelineViewMouseEvent *event) { // Determine if item clicked on is selectable - clicked_item_ = GetItemAtScenePos(event->GetCoordinates()); + clicked_item_ = parent()->GetItemAtScenePos(event->GetCoordinates()); bool selectable_item = (clicked_item_ - && clicked_item_->flags() & QGraphicsItem::ItemIsSelectable && !parent()->GetTrackFromReference(clicked_item_->Track())->IsLocked()); if (selectable_item) { @@ -76,14 +76,14 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) } // If this item is already selected, no further selection needs to be made - if (clicked_item_->isSelected()) { + if (parent()->IsBlockSelected(clicked_item_->block())) { // Collect item deselections QList deselected_blocks; // If shift is held, deselect it if (event->GetModifiers() & Qt::ShiftModifier) { - clicked_item_->setSelected(false); + parent()->RemoveSelection(clicked_item_); deselected_blocks.append(clicked_item_->block()); // If not holding alt, deselect all links as well @@ -93,7 +93,7 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) } } - emit parent()->BlocksDeselected(deselected_blocks); + parent()->SignalDeselectedBlocks(deselected_blocks); return; } @@ -110,7 +110,7 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) QList selected_blocks; // Select this item - clicked_item_->setSelected(true); + parent()->AddSelection(clicked_item_); selected_blocks.append(clicked_item_->block()); // If not holding alt, select all links as well @@ -119,7 +119,7 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) selected_blocks.append(clicked_item_->block()->linked_clips().toList()); } - emit parent()->BlocksSelected(selected_blocks); + parent()->SignalSelectedBlocks(selected_blocks); } else if (event->GetButton() == Qt::LeftButton) { @@ -131,7 +131,7 @@ void TimelineWidget::PointerTool::MousePress(TimelineViewMouseEvent *event) } } -void TimelineWidget::PointerTool::MouseMove(TimelineViewMouseEvent *event) +void PointerTool::MouseMove(TimelineViewMouseEvent *event) { if (rubberband_selecting_) { // Process rubberband select @@ -155,7 +155,7 @@ void TimelineWidget::PointerTool::MouseMove(TimelineViewMouseEvent *event) } - if (dragging_ && !parent()->ghost_items_.isEmpty()) { + if (dragging_ && !parent()->GetGhostItems().isEmpty()) { // We're already dragging AND we have ghosts to work with ProcessDrag(event->GetCoordinates()); @@ -164,7 +164,7 @@ void TimelineWidget::PointerTool::MouseMove(TimelineViewMouseEvent *event) } } -void TimelineWidget::PointerTool::MouseRelease(TimelineViewMouseEvent *event) +void PointerTool::MouseRelease(TimelineViewMouseEvent *event) { if (rubberband_selecting_) { // Finish rubberband select @@ -175,7 +175,7 @@ void TimelineWidget::PointerTool::MouseRelease(TimelineViewMouseEvent *event) if (dragging_) { // If we were dragging, process the end of the drag - if (!parent()->ghost_items_.isEmpty()) { + if (!parent()->GetGhostItems().isEmpty()) { FinishDrag(event); } @@ -187,11 +187,11 @@ void TimelineWidget::PointerTool::MouseRelease(TimelineViewMouseEvent *event) } } -void TimelineWidget::PointerTool::HoverMove(TimelineViewMouseEvent *event) +void PointerTool::HoverMove(TimelineViewMouseEvent *event) { if (trimming_allowed_) { // No dragging, but we still want to process cursors - TimelineViewBlockItem* block_at_cursor = GetItemAtScenePos(event->GetCoordinates()); + TimelineViewBlockItem* block_at_cursor = parent()->GetItemAtScenePos(event->GetCoordinates()); if (block_at_cursor) { switch (IsCursorInTrimHandle(block_at_cursor, event->GetSceneX())) { @@ -215,10 +215,10 @@ void TimelineWidget::PointerTool::HoverMove(TimelineViewMouseEvent *event) void SetGhostToSlideMode(TimelineViewGhostItem* g) { g->SetCanMoveTracks(false); - g->setData(TimelineViewGhostItem::kGhostIsSliding, true); + g->SetData(TimelineViewGhostItem::kGhostIsSliding, true); } -void TimelineWidget::PointerTool::InitiateDragInternal(TimelineViewBlockItem *clicked_item, +void PointerTool::InitiateDragInternal(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode, bool dont_roll_trims, bool allow_nongap_rolling, @@ -427,16 +427,16 @@ void TimelineWidget::PointerTool::InitiateDragInternal(TimelineViewBlockItem *cl // If we have an adjacent block (for any reason), this is a roll edit and the adjacent is // expected to fill the remaining space (no gap needs to be created) - ghost->setData(TimelineViewGhostItem::kTrimIsARollEdit, static_cast(adjacent)); + ghost->SetData(TimelineViewGhostItem::kTrimIsARollEdit, static_cast(adjacent)); if (adjacent_ghost) { if (treat_trim_as_slide) { // We're sliding a transition rather than a pure trim/roll SetGhostToSlideMode(adjacent_ghost); } else if (block->type() == Block::kGap) { - ghost->setData(TimelineViewGhostItem::kTrimShouldBeIgnored, true); + ghost->SetData(TimelineViewGhostItem::kTrimShouldBeIgnored, true); } else { - adjacent_ghost->setData(TimelineViewGhostItem::kTrimShouldBeIgnored, true); + adjacent_ghost->SetData(TimelineViewGhostItem::kTrimShouldBeIgnored, true); } } } @@ -444,7 +444,7 @@ void TimelineWidget::PointerTool::InitiateDragInternal(TimelineViewBlockItem *cl } } -void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_pos) +void PointerTool::ProcessDrag(const TimelineCoordinate &mouse_pos) { // Calculate track movement int track_movement = track_movement_allowed_ @@ -470,9 +470,9 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po // Validate ghosts that are being moved (clips from other track types do NOT get moved) { - QVector validate_track_ghosts = parent()->ghost_items_; + QVector validate_track_ghosts = parent()->GetGhostItems(); for (int i=0;iTrack().type() != drag_track_type_) { + if (validate_track_ghosts.at(i)->GetTrack().type() != drag_track_type_) { validate_track_ghosts.removeAt(i); i--; } @@ -481,8 +481,8 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po } // Perform movement - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - switch (ghost->mode()) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + switch (ghost->GetMode()) { case Timeline::kNone: break; case Timeline::kTrimIn: @@ -498,11 +498,8 @@ void TimelineWidget::PointerTool::ProcessDrag(const TimelineCoordinate &mouse_po // Track movement is only legal for moving, not for trimming // Also, we only move the clips on the same track type that the drag started from - if (ghost->Track().type() == drag_track_type_) { + if (ghost->GetTrack().type() == drag_track_type_) { ghost->SetTrackAdjustment(track_movement); - - const TrackReference& track = ghost->GetAdjustedTrack(); - ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track)); } break; } @@ -525,22 +522,22 @@ struct GhostBlockPair { Block* block; }; -void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) +void PointerTool::FinishDrag(TimelineViewMouseEvent *event) { QList blocks_moving; QList blocks_sliding; QList blocks_trimming; // Sort ghosts depending on which ones are trimming, which are moving, and which are sliding - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { if (ghost->HasBeenAdjusted()) { - Block* b = Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)); + Block* b = Node::ValueToPtr(ghost->GetData(TimelineViewGhostItem::kAttachedBlock)); - if (ghost->data(TimelineViewGhostItem::kGhostIsSliding).toBool()) { + if (ghost->GetData(TimelineViewGhostItem::kGhostIsSliding).toBool()) { blocks_sliding.append({ghost, b}); - } else if (ghost->mode() == Timeline::kMove) { + } else if (ghost->GetMode() == Timeline::kMove) { blocks_moving.append({ghost, b}); - } else if (Timeline::IsATrimMode(ghost->mode())) { + } else if (Timeline::IsATrimMode(ghost->GetMode())) { blocks_trimming.append({ghost, b}); } } @@ -555,18 +552,34 @@ void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) QUndoCommand* command = new QUndoCommand(); - foreach (const GhostBlockPair& p, blocks_trimming) { - TimelineViewGhostItem* ghost = p.ghost; + new NodeGraphBeginOperationCommand(static_cast(parent()->GetConnectedNode()->parent()), command); - if (!ghost->data(TimelineViewGhostItem::kTrimShouldBeIgnored).toBool()) { - // Must be an ordinary trim/roll - BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->GetAdjustedTrack()), - p.block, - ghost->AdjustedLength(), - ghost->mode(), - command); + if (!blocks_trimming.isEmpty()) { + foreach (const GhostBlockPair& p, blocks_trimming) { + TimelineViewGhostItem* ghost = p.ghost; - c->SetTrimIsARollEdit(ghost->data(TimelineViewGhostItem::kTrimIsARollEdit).toBool()); + if (!ghost->GetData(TimelineViewGhostItem::kTrimShouldBeIgnored).toBool()) { + // Must be an ordinary trim/roll + BlockTrimCommand* c = new BlockTrimCommand(parent()->GetTrackFromReference(ghost->GetAdjustedTrack()), + p.block, + ghost->GetAdjustedLength(), + ghost->GetMode(), + command); + + c->SetTrimIsARollEdit(ghost->GetData(TimelineViewGhostItem::kTrimIsARollEdit).toBool()); + } + } + + if (blocks_moving.isEmpty() && blocks_sliding.isEmpty()) { + // Trim selections (deferring to moving/sliding blocks when necessary) + TimelineWidgetSelections new_sel = parent()->GetSelections(); + TimelineViewGhostItem* reference_ghost = blocks_trimming.first().ghost; + if (reference_ghost->GetMode() == Timeline::kTrimIn) { + new_sel.TrimIn(reference_ghost->GetInAdjustment()); + } else { + new_sel.TrimOut(reference_ghost->GetOutAdjustment()); + } + new TimelineSetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); } } @@ -616,6 +629,12 @@ void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) p.ghost->GetAdjustedIn(), command); } + + // Adjust selections + TimelineWidgetSelections new_sel = parent()->GetSelections(); + new_sel.ShiftTime(blocks_moving.first().ghost->GetInAdjustment()); + new_sel.ShiftTracks(drag_track_type_, blocks_moving.first().ghost->GetTrackAdjustment()); + new TimelineSetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); } if (!blocks_sliding.isEmpty()) { @@ -628,15 +647,15 @@ void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) rational movement; foreach (const GhostBlockPair& p, blocks_sliding) { - const TrackReference& track = p.ghost->Track(); + const TrackReference& track = p.ghost->GetTrack(); - switch (p.ghost->mode()) { + switch (p.ghost->GetMode()) { case Timeline::kNone: break; case Timeline::kMove: { // These all should have moved uniformly, so as long as this is set, it should be fine - movement = p.ghost->InAdjustment(); + movement = p.ghost->GetInAdjustment(); QList& blocks_on_this_track = slide_info[track]; bool inserted = false; @@ -673,13 +692,20 @@ void TimelineWidget::PointerTool::FinishDrag(TimelineViewMouseEvent *event) movement, command); } + + // Adjust selections + TimelineWidgetSelections new_sel = parent()->GetSelections(); + new_sel.ShiftTime(movement); + new TimelineSetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); } } + new NodeGraphEndOperationCommand(static_cast(parent()->GetConnectedNode()->parent()), command); + Core::instance()->undo_stack()->pushIfHasChildren(command); } -Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(TimelineViewBlockItem *block, qreal cursor_x) +Timeline::MovementMode PointerTool::IsCursorInTrimHandle(TimelineViewBlockItem *block, qreal cursor_x) { double kTrimHandle = QFontMetricsWidth(parent()->fontMetrics(), "H"); @@ -697,7 +723,7 @@ Timeline::MovementMode TimelineWidget::PointerTool::IsCursorInTrimHandle(Timelin } } -void TimelineWidget::PointerTool::InitiateDrag(TimelineViewBlockItem* clicked_item, +void PointerTool::InitiateDrag(TimelineViewBlockItem* clicked_item, Timeline::MovementMode trim_mode) { InitiateDragInternal(clicked_item, trim_mode, false, false, false); @@ -705,20 +731,17 @@ void TimelineWidget::PointerTool::InitiateDrag(TimelineViewBlockItem* clicked_it //#define HIDE_GAP_GHOSTS -TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode, bool check_if_exists) +TimelineViewGhostItem* PointerTool::AddGhostFromBlock(Block* block, const TrackReference& track, Timeline::MovementMode mode, bool check_if_exists) { if (check_if_exists) { - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)) == block) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + if (Node::ValueToPtr(ghost->GetData(TimelineViewGhostItem::kAttachedBlock)) == block) { return ghost; } } } - TimelineViewGhostItem* ghost = TimelineViewGhostItem::FromBlock(block, - track, - parent()->GetTrackY(track), - parent()->GetTrackHeight(track)); + TimelineViewGhostItem* ghost = TimelineViewGhostItem::FromBlock(block, track); #ifdef HIDE_GAP_GHOSTS if (block->type() == Block::kGap) { @@ -731,14 +754,13 @@ TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromBlock(Block* blo return ghost; } -TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const rational &in, const rational &out, const TrackReference& track, Timeline::MovementMode mode) +TimelineViewGhostItem* PointerTool::AddGhostFromNull(const rational &in, const rational &out, const TrackReference& track, Timeline::MovementMode mode) { TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); ghost->SetIn(in); ghost->SetOut(out); ghost->SetTrack(track); - ghost->SetYCoords(parent()->GetTrackY(track), parent()->GetTrackHeight(track)); #ifdef HIDE_GAP_GHOSTS ghost->SetInvisible(true); @@ -749,21 +771,21 @@ TimelineViewGhostItem* TimelineWidget::PointerTool::AddGhostFromNull(const ratio return ghost; } -void TimelineWidget::PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode) +void PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode) { ghost->SetMode(mode); // Prepare snap points (optimizes snapping for later) switch (mode) { case Timeline::kMove: - snap_points_.append(ghost->In()); - snap_points_.append(ghost->Out()); + snap_points_.append(ghost->GetIn()); + snap_points_.append(ghost->GetOut()); break; case Timeline::kTrimIn: - snap_points_.append(ghost->In()); + snap_points_.append(ghost->GetIn()); break; case Timeline::kTrimOut: - snap_points_.append(ghost->Out()); + snap_points_.append(ghost->GetOut()); break; default: break; @@ -772,7 +794,7 @@ void TimelineWidget::PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, parent()->AddGhost(ghost); } -bool TimelineWidget::PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip, +bool PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip, const QList& items, const Timeline::MovementMode& mode) { @@ -788,7 +810,7 @@ bool TimelineWidget::PointerTool::IsClipTrimmable(TimelineViewBlockItem* clip, return true; } -bool TimelineWidget::PointerTool::AddMovingTransitionsToClipGhost(Block* block, +bool PointerTool::AddMovingTransitionsToClipGhost(Block* block, const TrackReference& track, Timeline::MovementMode movement, const QList& selected_items) @@ -838,41 +860,41 @@ bool TimelineWidget::PointerTool::AddMovingTransitionsToClipGhost(Block* block, return ret; } -rational TimelineWidget::PointerTool::ValidateInTrimming(rational movement) +rational PointerTool::ValidateInTrimming(rational movement) { - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (ghost->mode() != Timeline::kTrimIn) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + if (ghost->GetMode() != Timeline::kTrimIn) { continue; } rational earliest_in = RATIONAL_MIN; - rational latest_in = ghost->Out(); + rational latest_in = ghost->GetOut(); if (!ghost->CanHaveZeroLength()) { latest_in -= parent()->timebase(); } // Clamp adjusted value between the earliest and latest values - rational adjusted = ghost->In() + movement; + rational adjusted = ghost->GetIn() + movement; rational clamped = clamp(adjusted, earliest_in, latest_in); if (clamped != adjusted) { - movement = clamped - ghost->In(); + movement = clamped - ghost->GetIn(); } } return movement; } -rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement) +rational PointerTool::ValidateOutTrimming(rational movement) { - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (ghost->mode() != Timeline::kTrimOut) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + if (ghost->GetMode() != Timeline::kTrimOut) { continue; } // Determine earliest and latest out points - rational earliest_out = ghost->In(); + rational earliest_out = ghost->GetIn(); if (!ghost->CanHaveZeroLength()) { earliest_out += parent()->timebase(); @@ -881,11 +903,11 @@ rational TimelineWidget::PointerTool::ValidateOutTrimming(rational movement) rational latest_out = RATIONAL_MAX; // Clamp adjusted value between the earliest and latest values - rational adjusted = ghost->Out() + movement; + rational adjusted = ghost->GetOut() + movement; rational clamped = clamp(adjusted, earliest_out, latest_out); if (clamped != adjusted) { - movement = clamped - ghost->Out(); + movement = clamped - ghost->GetOut(); } } diff --git a/app/widget/timelinewidget/tool/pointer.h b/app/widget/timelinewidget/tool/pointer.h new file mode 100644 index 000000000..31ec8b455 --- /dev/null +++ b/app/widget/timelinewidget/tool/pointer.h @@ -0,0 +1,127 @@ +/*** + + 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 POINTERTIMELINETOOL_H +#define POINTERTIMELINETOOL_H + +#include "tool.h" + +OLIVE_NAMESPACE_ENTER + +class PointerTool : public TimelineTool +{ +public: + PointerTool(TimelineWidget* parent); + + virtual void MousePress(TimelineViewMouseEvent *event) override; + virtual void MouseMove(TimelineViewMouseEvent *event) override; + virtual void MouseRelease(TimelineViewMouseEvent *event) override; + + virtual void HoverMove(TimelineViewMouseEvent *event) override; + +protected: + virtual void FinishDrag(TimelineViewMouseEvent *event); + + virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode); + + TimelineViewGhostItem* AddGhostFromBlock(Block *block, const TrackReference& track, Timeline::MovementMode mode, bool check_if_exists = false); + + TimelineViewGhostItem* AddGhostFromNull(const rational& in, const rational& out, const TrackReference& track, Timeline::MovementMode mode); + + /** + * @brief Validates Ghosts that are getting their in points trimmed + * + * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no + * Ghost's length becomes 0 or negative. + */ + rational ValidateInTrimming(rational movement); + + /** + * @brief Validates Ghosts that are getting their out points trimmed + * + * Assumes ghost->data() is a Block. Ensures no Ghost's in point becomes a negative timecode. Also ensures no + * Ghost's length becomes 0 or negative. + */ + rational ValidateOutTrimming(rational movement); + + virtual void ProcessDrag(const TimelineCoordinate &mouse_pos); + + void InitiateDragInternal(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode, + bool dont_roll_trims, + bool allow_nongap_rolling, bool slide_instead_of_moving); + + 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 SetGapTrimmingAllowed(bool e) + { + gap_trimming_allowed_ = e; + } + +private: + Timeline::MovementMode IsCursorInTrimHandle(TimelineViewBlockItem* block, qreal cursor_x); + + void AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::MovementMode mode); + + bool IsClipTrimmable(TimelineViewBlockItem* clip, + const QList& items, + const Timeline::MovementMode& mode); + + void ProcessGhostsForSliding(); + + void ProcessGhostsForRolling(); + + bool AddMovingTransitionsToClipGhost(Block *block, const TrackReference &track, Timeline::MovementMode movement, const QList &selected_items); + + bool movement_allowed_; + bool trimming_allowed_; + bool track_movement_allowed_; + bool gap_trimming_allowed_; + bool rubberband_selecting_; + + Timeline::TrackType drag_track_type_; + Timeline::MovementMode drag_movement_mode_; + + TimelineViewBlockItem* clicked_item_; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // POINTERTIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/razor.cpp b/app/widget/timelinewidget/tool/razor.cpp index 49177ce8a..fb6244ee8 100644 --- a/app/widget/timelinewidget/tool/razor.cpp +++ b/app/widget/timelinewidget/tool/razor.cpp @@ -18,23 +18,24 @@ ***/ +#include "razor.h" #include "widget/timelinewidget/timelinewidget.h" OLIVE_NAMESPACE_ENTER -TimelineWidget::RazorTool::RazorTool(TimelineWidget* parent) : +RazorTool::RazorTool(TimelineWidget* parent) : BeamTool(parent) { } -void TimelineWidget::RazorTool::MousePress(TimelineViewMouseEvent *event) +void RazorTool::MousePress(TimelineViewMouseEvent *event) { split_tracks_.clear(); MouseMove(event); } -void TimelineWidget::RazorTool::MouseMove(TimelineViewMouseEvent *event) +void RazorTool::MouseMove(TimelineViewMouseEvent *event) { if (!dragging_) { drag_start_ = ValidatedCoordinate(event->GetCoordinates(true)); @@ -49,7 +50,7 @@ void TimelineWidget::RazorTool::MouseMove(TimelineViewMouseEvent *event) } } -void TimelineWidget::RazorTool::MouseRelease(TimelineViewMouseEvent *event) +void RazorTool::MouseRelease(TimelineViewMouseEvent *event) { Q_UNUSED(event) diff --git a/app/widget/timelinewidget/tool/razor.h b/app/widget/timelinewidget/tool/razor.h new file mode 100644 index 000000000..d2da08ac3 --- /dev/null +++ b/app/widget/timelinewidget/tool/razor.h @@ -0,0 +1,43 @@ +/*** + + 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 RAZORTIMELINETOOL_H +#define RAZORTIMELINETOOL_H + +#include "beam.h" + +OLIVE_NAMESPACE_ENTER + +class RazorTool : public BeamTool +{ +public: + RazorTool(TimelineWidget* parent); + + virtual void MousePress(TimelineViewMouseEvent *event) override; + virtual void MouseMove(TimelineViewMouseEvent *event) override; + virtual void MouseRelease(TimelineViewMouseEvent *event) override; + +private: + QVector split_tracks_; +}; + +OLIVE_NAMESPACE_EXIT + +#endif // RAZORTIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index 919c0d908..f9455483e 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -21,36 +21,37 @@ #include "widget/timelinewidget/timelinewidget.h" #include "node/block/gap/gap.h" +#include "ripple.h" #include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER -TimelineWidget::RippleTool::RippleTool(TimelineWidget* parent) : +RippleTool::RippleTool(TimelineWidget* parent) : PointerTool(parent) { SetMovementAllowed(false); SetGapTrimmingAllowed(true); } -void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_item, +void RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { InitiateDragInternal(clicked_item, trim_mode, true, true, false); - if (parent()->ghost_items_.isEmpty()) { + if (!parent()->HasGhosts()) { return; } // Find the earliest ripple rational earliest_ripple = RATIONAL_MAX; - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { rational ghost_ripple_point; if (trim_mode == Timeline::kTrimIn) { - ghost_ripple_point = ghost->In(); + ghost_ripple_point = ghost->GetIn(); } else { - ghost_ripple_point = ghost->Out(); + ghost_ripple_point = ghost->GetOut(); } earliest_ripple = qMin(earliest_ripple, ghost_ripple_point); @@ -65,8 +66,8 @@ void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_ite // Determine if we've already created a ghost on this track bool ghost_on_this_track_exists = false; - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (parent()->GetTrackFromReference(ghost->Track()) == track) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + if (parent()->GetTrackFromReference(ghost->GetTrack()) == track) { ghost_on_this_track_exists = true; break; } @@ -95,7 +96,7 @@ void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_ite } else { // If next is NOT a gap, we'll need to create one, for which we'll use a null ghost ghost = AddGhostFromNull(block_before_ripple->out(), block_before_ripple->out(), track_ref, trim_mode); - ghost->setData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); + ghost->SetData(TimelineViewGhostItem::kReferenceBlock, Node::PtrToValue(block_before_ripple)); } } } @@ -103,34 +104,47 @@ void TimelineWidget::RippleTool::InitiateDrag(TimelineViewBlockItem *clicked_ite } } -void TimelineWidget::RippleTool::FinishDrag(TimelineViewMouseEvent *event) +void RippleTool::FinishDrag(TimelineViewMouseEvent *event) { Q_UNUSED(event) - QVector< QList > info_list(Timeline::kTrackTypeCount); + if (parent()->HasGhosts()) { + QVector< QList > info_list(Timeline::kTrackTypeCount); - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - TrackOutput* track = parent()->GetTrackFromReference(ghost->Track()); + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + TrackOutput* track = parent()->GetTrackFromReference(ghost->GetTrack()); - TrackListRippleToolCommand::RippleInfo i = {Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kAttachedBlock)), - Node::ValueToPtr(ghost->data(TimelineViewGhostItem::kReferenceBlock)), - track, - ghost->AdjustedLength(), - ghost->Length()}; + TrackListRippleToolCommand::RippleInfo i = {Node::ValueToPtr(ghost->GetData(TimelineViewGhostItem::kAttachedBlock)), + Node::ValueToPtr(ghost->GetData(TimelineViewGhostItem::kReferenceBlock)), + track, + ghost->GetAdjustedLength(), + ghost->GetLength()}; - info_list[track->track_type()].append(i); + info_list[track->track_type()].append(i); + } + + QUndoCommand* command = new QUndoCommand(); + + if (!info_list.isEmpty()) { + for (int i=0;iGetConnectedNode()->track_list(static_cast(i)), + info_list.at(i), + drag_movement_mode(), + command); + } + + TimelineWidgetSelections new_sel = parent()->GetSelections(); + TimelineViewGhostItem* reference_ghost = parent()->GetGhostItems().first(); + if (drag_movement_mode() == Timeline::kTrimIn) { + new_sel.TrimOut(-reference_ghost->GetInAdjustment()); + } else { + new_sel.TrimOut(reference_ghost->GetOutAdjustment()); + } + new TimelineSetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), command); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); } - - QUndoCommand* command = new QUndoCommand(); - - for (int i=0;iGetConnectedNode()->track_list(static_cast(i)), - info_list.at(i), - drag_movement_mode(), - command); - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); } OLIVE_NAMESPACE_EXIT diff --git a/app/widget/timelinewidget/tool/ripple.h b/app/widget/timelinewidget/tool/ripple.h new file mode 100644 index 000000000..bb946eaa3 --- /dev/null +++ b/app/widget/timelinewidget/tool/ripple.h @@ -0,0 +1,41 @@ +/*** + + 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 RIPPLETIMELINETOOL_H +#define RIPPLETIMELINETOOL_H + +#include "pointer.h" + +OLIVE_NAMESPACE_ENTER + +class RippleTool : public PointerTool +{ +public: + RippleTool(TimelineWidget* parent); +protected: + virtual void FinishDrag(TimelineViewMouseEvent *event) override; + + virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode) override; +}; + +OLIVE_NAMESPACE_EXIT + +#endif // RIPPLETIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/rolling.cpp b/app/widget/timelinewidget/tool/rolling.cpp index 2aa55e43e..4f340991a 100644 --- a/app/widget/timelinewidget/tool/rolling.cpp +++ b/app/widget/timelinewidget/tool/rolling.cpp @@ -21,18 +21,19 @@ #include "widget/timelinewidget/timelinewidget.h" #include "node/block/gap/gap.h" +#include "rolling.h" #include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER -TimelineWidget::RollingTool::RollingTool(TimelineWidget* parent) : +RollingTool::RollingTool(TimelineWidget* parent) : PointerTool(parent) { SetMovementAllowed(false); SetGapTrimmingAllowed(true); } -void TimelineWidget::RollingTool::InitiateDrag(TimelineViewBlockItem *clicked_item, +void RollingTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { InitiateDragInternal(clicked_item, trim_mode, false, true, false); diff --git a/app/widget/timelinewidget/tool/rolling.h b/app/widget/timelinewidget/tool/rolling.h new file mode 100644 index 000000000..f804b9118 --- /dev/null +++ b/app/widget/timelinewidget/tool/rolling.h @@ -0,0 +1,40 @@ +/*** + + 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 ROLLINGTIMELINETOOL_H +#define ROLLINGTIMELINETOOL_H + +#include "pointer.h" + +OLIVE_NAMESPACE_ENTER + +class RollingTool : public PointerTool +{ +public: + RollingTool(TimelineWidget* parent); + +protected: + virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode) override; +}; + +OLIVE_NAMESPACE_EXIT + +#endif // ROLLINGTIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/slide.cpp b/app/widget/timelinewidget/tool/slide.cpp index c64d9ec43..0dc7d1482 100644 --- a/app/widget/timelinewidget/tool/slide.cpp +++ b/app/widget/timelinewidget/tool/slide.cpp @@ -21,11 +21,12 @@ #include "widget/timelinewidget/timelinewidget.h" #include "node/block/gap/gap.h" +#include "slide.h" #include "widget/nodeview/nodeviewundo.h" OLIVE_NAMESPACE_ENTER -TimelineWidget::SlideTool::SlideTool(TimelineWidget* parent) : +SlideTool::SlideTool(TimelineWidget* parent) : PointerTool(parent) { SetTrimmingAllowed(false); @@ -33,7 +34,7 @@ TimelineWidget::SlideTool::SlideTool(TimelineWidget* parent) : SetGapTrimmingAllowed(true); } -void TimelineWidget::SlideTool::InitiateDrag(TimelineViewBlockItem *clicked_item, +void SlideTool::InitiateDrag(TimelineViewBlockItem *clicked_item, Timeline::MovementMode trim_mode) { InitiateDragInternal(clicked_item, trim_mode, false, true, true); diff --git a/app/widget/timelinewidget/tool/slide.h b/app/widget/timelinewidget/tool/slide.h new file mode 100644 index 000000000..326f78d8b --- /dev/null +++ b/app/widget/timelinewidget/tool/slide.h @@ -0,0 +1,41 @@ +/*** + + 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 SLIDETIMELINETOOL_H +#define SLIDETIMELINETOOL_H + +#include "pointer.h" + +OLIVE_NAMESPACE_ENTER + +class SlideTool : public PointerTool +{ +public: + SlideTool(TimelineWidget* parent); + +protected: + virtual void InitiateDrag(TimelineViewBlockItem* clicked_item, + Timeline::MovementMode trim_mode) override; + +}; + +OLIVE_NAMESPACE_EXIT + +#endif // SLIDETIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/slip.cpp b/app/widget/timelinewidget/tool/slip.cpp index 0208743be..e7636048f 100644 --- a/app/widget/timelinewidget/tool/slip.cpp +++ b/app/widget/timelinewidget/tool/slip.cpp @@ -24,30 +24,31 @@ #include "common/timecodefunctions.h" #include "config/config.h" +#include "slip.h" OLIVE_NAMESPACE_ENTER -TimelineWidget::SlipTool::SlipTool(TimelineWidget *parent) : +SlipTool::SlipTool(TimelineWidget *parent) : PointerTool(parent) { SetTrimmingAllowed(false); SetTrackMovementAllowed(false); } -void TimelineWidget::SlipTool::ProcessDrag(const TimelineCoordinate &mouse_pos) +void SlipTool::ProcessDrag(const TimelineCoordinate &mouse_pos) { // Determine frame movement rational time_movement = drag_start_.GetFrame() - mouse_pos.GetFrame(); // Validate slip (enforce all ghosts moving in legal ways) - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (ghost->MediaIn() + time_movement < 0) { - time_movement = -ghost->MediaIn(); + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + if (ghost->GetMediaIn() + time_movement < 0) { + time_movement = -ghost->GetMediaIn(); } } // Perform slip - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { ghost->SetMediaInAdjustment(time_movement); } @@ -62,15 +63,15 @@ void TimelineWidget::SlipTool::ProcessDrag(const TimelineCoordinate &mouse_pos) parent()); } -void TimelineWidget::SlipTool::FinishDrag(TimelineViewMouseEvent *event) +void SlipTool::FinishDrag(TimelineViewMouseEvent *event) { Q_UNUSED(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)); + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + Block* b = Node::ValueToPtr(ghost->GetData(TimelineViewGhostItem::kAttachedBlock)); new BlockSetMediaInCommand(b, ghost->GetAdjustedMediaIn(), command); } diff --git a/app/widget/timelinewidget/tool/slip.h b/app/widget/timelinewidget/tool/slip.h new file mode 100644 index 000000000..bf358e5fc --- /dev/null +++ b/app/widget/timelinewidget/tool/slip.h @@ -0,0 +1,40 @@ +/*** + + 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 SLIPTIMELINETOOL_H +#define SLIPTIMELINETOOL_H + +#include "pointer.h" + +OLIVE_NAMESPACE_ENTER + +class SlipTool : public PointerTool +{ +public: + SlipTool(TimelineWidget* parent); + +protected: + virtual void ProcessDrag(const TimelineCoordinate &mouse_pos) override; + virtual void FinishDrag(TimelineViewMouseEvent *event) override; +}; + +OLIVE_NAMESPACE_EXIT + +#endif // SLIPTIMELINETOOL_H diff --git a/app/widget/timelinewidget/tool/tool.cpp b/app/widget/timelinewidget/tool/tool.cpp index 04ce25b32..388b4edaf 100644 --- a/app/widget/timelinewidget/tool/tool.cpp +++ b/app/widget/timelinewidget/tool/tool.cpp @@ -25,22 +25,22 @@ OLIVE_NAMESPACE_ENTER -TimelineWidget::Tool::Tool(TimelineWidget *parent) : +TimelineTool::TimelineTool(TimelineWidget *parent) : dragging_(false), parent_(parent) { } -TimelineWidget::Tool::~Tool() +TimelineTool::~TimelineTool() { } -TimelineWidget *TimelineWidget::Tool::parent() +TimelineWidget *TimelineTool::parent() { return parent_; } -Timeline::MovementMode TimelineWidget::Tool::FlipTrimMode(const Timeline::MovementMode &trim_mode) +Timeline::MovementMode TimelineTool::FlipTrimMode(const Timeline::MovementMode &trim_mode) { if (trim_mode == Timeline::kTrimIn) { return Timeline::kTrimOut; @@ -53,57 +53,37 @@ Timeline::MovementMode TimelineWidget::Tool::FlipTrimMode(const Timeline::Moveme return trim_mode; } -TimelineViewBlockItem *TimelineWidget::Tool::GetItemAtScenePos(const TimelineCoordinate& coord) +rational TimelineTool::ValidateTimeMovement(rational movement) { - QMapIterator iterator(parent()->block_items_); - - while (iterator.hasNext()) { - iterator.next(); - - Block* b = iterator.key(); - TimelineViewBlockItem* item = iterator.value(); - - if (b->in() <= coord.GetFrame() - && b->out() > coord.GetFrame() - && item->Track() == coord.GetTrack()) { - return item; - } - } - - return nullptr; -} - -rational TimelineWidget::Tool::ValidateTimeMovement(rational movement) -{ - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - if (ghost->mode() != Timeline::kMove) { + foreach (TimelineViewGhostItem* ghost, parent()->GetGhostItems()) { + if (ghost->GetMode() != Timeline::kMove) { continue; } // Prevents any ghosts from going below 0:00:00 time - if (ghost->In() + movement < 0) { - movement = -ghost->In(); + if (ghost->GetIn() + movement < 0) { + movement = -ghost->GetIn(); } } return movement; } -int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector& ghosts) +int TimelineTool::ValidateTrackMovement(int movement, const QVector& ghosts) { foreach (TimelineViewGhostItem* ghost, ghosts) { - if (ghost->mode() != Timeline::kMove) { + if (ghost->GetMode() != Timeline::kMove) { continue; } - if (!ghost->CanMoveTracks()) { + if (!ghost->GetCanMoveTracks()) { return 0; - } else if (ghost->Track().index() + movement < 0) { + } else if (ghost->GetTrack().index() + movement < 0) { // Prevents any ghosts from going to a non-existent negative track - movement = -ghost->Track().index(); + movement = -ghost->GetTrack().index(); } } @@ -111,12 +91,12 @@ int TimelineWidget::Tool::ValidateTrackMovement(int movement, const QVector