diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 201b78ce6..9794b77c5 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -26,6 +26,7 @@ #include +#include "audio/packedprocessor.h" #include "config/config.h" namespace olive { @@ -68,6 +69,19 @@ int OutputCallback(const void *input, void *output, unsigned long frameCount, co return paContinue; } +int InputCallback(const void *input, void *output, unsigned long frameCount, const PaStreamCallbackTimeInfo *timeInfo, PaStreamCallbackFlags statusFlags, void *userData) +{ + FFmpegEncoder *f = static_cast(userData); + + SampleBufferPtr s = SampleBuffer::Create(); + s->set_sample_count(frameCount); + s->set_audio_params(f->params().audio_params()); + + f->WriteAudioData(f->params().audio_params(), false, reinterpret_cast(&input), frameCount); + + return paContinue; +} + void AudioManager::PushToOutput(const AudioParams ¶ms, const QByteArray &samples) { if (output_device_ == paNoDevice) { @@ -79,13 +93,7 @@ void AudioManager::PushToOutput(const AudioParams ¶ms, const QByteArray &sam CloseOutputStream(); - PaStreamParameters p; - - p.channelCount = output_params_.channel_count(); - p.device = output_device_; - p.hostApiSpecificStreamInfo = nullptr; - p.sampleFormat = GetPortAudioSampleFormat(output_params_.format()); - p.suggestedLatency = Pa_GetDeviceInfo(output_device_)->defaultLowOutputLatency; + PaStreamParameters p = GetPortAudioParams(params, output_device_); Pa_OpenStream(&output_stream_, nullptr, &p, output_params_.sample_rate(), paFramesPerBufferUnspecified, paNoFlag, OutputCallback, output_buffer_); @@ -176,6 +184,52 @@ void AudioManager::HardReset() Pa_Initialize(); } +bool AudioManager::StartRecording(const QString &filename, const AudioParams ¶ms) +{ + if (input_device_ == paNoDevice) { + return false; + } + + EncodingParams encode_param; + encode_param.EnableAudio(params, ExportCodec::kCodecMP3); + encode_param.SetFilename(filename); + + input_encoder_ = new FFmpegEncoder(encode_param); + if (!input_encoder_->Open()) { + qCritical() << "Failed to open encoder for recording"; + return false; + } + + PaStreamParameters p = GetPortAudioParams(params, input_device_); + + if (Pa_OpenStream(&input_stream_, &p, nullptr, params.sample_rate(), paFramesPerBufferUnspecified, paNoFlag, InputCallback, input_encoder_) == paNoError) { + if (Pa_StartStream(input_stream_) == paNoError) { + return true; + } + } + + StopRecording(); + return false; +} + +void AudioManager::StopRecording() +{ + if (input_stream_) { + if (Pa_IsStreamActive(input_stream_)) { + Pa_StopStream(input_stream_); + } + Pa_CloseStream(input_stream_); + + input_stream_ = nullptr; + } + + if (input_encoder_) { + input_encoder_->Close(); + delete input_encoder_; + input_encoder_ = nullptr; + } +} + PaDeviceIndex AudioManager::FindConfigDeviceByName(bool is_output_device) { QString entry = is_output_device ? QStringLiteral("AudioOutput") : QStringLiteral("AudioInput"); @@ -199,8 +253,23 @@ PaDeviceIndex AudioManager::FindDeviceByName(const QString &s, bool is_output_de return is_output_device ? Pa_GetDefaultOutputDevice() : Pa_GetDefaultInputDevice(); } +PaStreamParameters AudioManager::GetPortAudioParams(const AudioParams ¶ms, PaDeviceIndex device) +{ + PaStreamParameters p; + + p.channelCount = params.channel_count(); + p.device = device; + p.hostApiSpecificStreamInfo = nullptr; + p.sampleFormat = GetPortAudioSampleFormat(params.format()); + p.suggestedLatency = Pa_GetDeviceInfo(device)->defaultLowOutputLatency; + + return p; +} + AudioManager::AudioManager() : - output_stream_(nullptr) + output_stream_(nullptr), + input_stream_(nullptr), + input_encoder_(nullptr) { #ifdef PA_HAS_JACK // PortAudio doesn't do a strcpy, so we need a const char that's readily accessible (i.e. not diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index 0d0329bd6..62fd0e920 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -28,6 +28,7 @@ #include "audiovisualwaveform.h" #include "common/define.h" +#include "codec/ffmpeg/ffmpegencoder.h" #include "render/audioparams.h" #include "render/audioplaybackcache.h" #include "render/previewaudiodevice.h" @@ -73,9 +74,15 @@ public: void HardReset(); + bool StartRecording(const QString &filename, const AudioParams ¶ms); + + void StopRecording(); + static PaDeviceIndex FindConfigDeviceByName(bool is_output_device); static PaDeviceIndex FindDeviceByName(const QString &s, bool is_output_device); + static PaStreamParameters GetPortAudioParams(const AudioParams &p, PaDeviceIndex device); + signals: void OutputNotify(); @@ -96,6 +103,8 @@ private: PreviewAudioDevice *output_buffer_; PaDeviceIndex input_device_; + PaStream *input_stream_; + FFmpegEncoder *input_encoder_; }; diff --git a/app/codec/ffmpeg/ffmpegencoder.cpp b/app/codec/ffmpeg/ffmpegencoder.cpp index c1ea0927a..9b38505b6 100644 --- a/app/codec/ffmpeg/ffmpegencoder.cpp +++ b/app/codec/ffmpeg/ffmpegencoder.cpp @@ -236,11 +236,6 @@ fail: bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio) { - if (!InitializeResampleContext(audio)) { - qCritical() << "Failed to initialize resample context"; - return false; - } - bool result = true; // Create input buffer @@ -258,6 +253,25 @@ bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio) } } + result = WriteAudioData(audio->audio_params(), true, const_cast(input_data), input_sample_count); + + if (input_data) { + av_freep(&input_data[0]); + av_freep(&input_data); + } + + return result; +} + +bool FFmpegEncoder::WriteAudioData(const AudioParams &audio_params, bool planar, const uint8_t **input_data, int input_sample_count) +{ + if (!InitializeResampleContext(audio_params, planar)) { + qCritical() << "Failed to initialize resample context"; + return false; + } + + bool result = true; + // Create output buffer int output_sample_count = input_sample_count ? swr_get_out_samples(audio_resample_ctx_, input_sample_count) : 102400; uint8_t** output_data = nullptr; @@ -308,11 +322,6 @@ bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio) av_freep(&output_data); } - if (input_data) { - av_freep(&input_data[0]); - av_freep(&input_data); - } - return result; } @@ -774,7 +783,7 @@ void FFmpegEncoder::FlushCodecCtx(AVCodecContext *codec_ctx, AVStream* stream) av_packet_free(&pkt); } -bool FFmpegEncoder::InitializeResampleContext(SampleBufferPtr audio) +bool FFmpegEncoder::InitializeResampleContext(const AudioParams &audio, bool planar) { if (audio_resample_ctx_) { return true; @@ -785,9 +794,9 @@ bool FFmpegEncoder::InitializeResampleContext(SampleBufferPtr audio) static_cast(audio_codec_ctx_->channel_layout), audio_codec_ctx_->sample_fmt, audio_codec_ctx_->sample_rate, - static_cast(audio->audio_params().channel_layout()), - FFmpegUtils::GetFFmpegSampleFormat(audio->audio_params().format(), true), - audio->audio_params().sample_rate(), + static_cast(audio.channel_layout()), + FFmpegUtils::GetFFmpegSampleFormat(audio.format(), planar), + audio.sample_rate(), 0, nullptr); if (!audio_resample_ctx_) { diff --git a/app/codec/ffmpeg/ffmpegencoder.h b/app/codec/ffmpeg/ffmpegencoder.h index d75d3f4ea..82e7040da 100644 --- a/app/codec/ffmpeg/ffmpegencoder.h +++ b/app/codec/ffmpeg/ffmpegencoder.h @@ -47,6 +47,8 @@ public: virtual bool WriteAudio(olive::SampleBufferPtr audio) override; + bool WriteAudioData(const AudioParams &audio_params, bool planar, const uint8_t **data, int input_sample_count); + virtual bool WriteSubtitle(const SubtitleBlock *sub_block) override; virtual void Close() override; @@ -76,7 +78,7 @@ private: void FlushEncoders(); void FlushCodecCtx(AVCodecContext* codec_ctx, AVStream *stream); - bool InitializeResampleContext(SampleBufferPtr audio); + bool InitializeResampleContext(const AudioParams &audio, bool planar); static const AVCodec *GetEncoder(ExportCodec::Codec c); diff --git a/app/common/rational.cpp b/app/common/rational.cpp index 9aa20426c..62b93c6cd 100644 --- a/app/common/rational.cpp +++ b/app/common/rational.cpp @@ -1,5 +1,22 @@ -//Copyright 2015 Adam Quintero -//This program is distributed under the terms of the GNU General Public License. +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "rational.h" @@ -39,9 +56,9 @@ rational rational::fromString(const QString &str, bool* ok) switch (elements.size()) { case 1: - return rational(elements.first().toLongLong(ok)); + return rational(elements.first().toInt(ok)); case 2: - return rational(elements.at(0).toLongLong(ok), elements.at(1).toLongLong(ok)); + return rational(elements.at(0).toInt(ok), elements.at(1).toInt(ok)); default: // Returns NaN with ok set to false if (ok) { @@ -51,63 +68,12 @@ rational rational::fromString(const QString &str, bool* ok) } } -//Function: ensures denom >= 0 - -void rational::fix_signs() -{ - // Normalize so that denominator is always positive and only numerator is positive - if (denom_ < 0) { - denom_ = -denom_; - numer_ = -numer_; - } else if (denom_ == intType(0)) { - // Normalize to 0/0 (aka NaN) if denominator is zero - numer_ = intType(0); - } else if (numer_ == intType(0)) { - // Normalize to 0/1 if numerator is zero - denom_ = intType(1); - } -} - -//Function: ensures lowest form - -void rational::reduce() -{ - if (!isNull()) { - // Euclidean often fails if numbers are negative, we abs it and re-neg it later if necessary - bool neg = numer_ < 0; - - numer_ = qAbs(numer_); - - intType d = gcd(numer_, denom_); - - if (d > 1) { - numer_ /= d; - denom_ /= d; - } - - if (neg) { - numer_ = -numer_; - } - } -} - -//Function: finds greatest common denominator - -intType rational::gcd(const intType &x, const intType &y) -{ - if (y == 0) { - return x; - } else { - return gcd(y, x % y); - } -} - //Function: convert to double double rational::toDouble() const { - if (denom_ != 0) { - return static_cast(numer_) / static_cast(denom_); + if (r_.den != 0) { + return av_q2d(r_); } else { return qSNaN(); } @@ -115,12 +81,7 @@ double rational::toDouble() const AVRational rational::toAVRational() const { - AVRational r; - - r.num = static_cast(numer_); - r.den = static_cast(denom_); - - return r; + return r_; } #ifdef USE_OTIO @@ -128,7 +89,7 @@ opentime::RationalTime rational::toRationalTime(double framerate) const { // Is this the best way of doing this? // Olive can store rationals as 0/0 which causes errors in OTIO - opentime::RationalTime time = opentime::RationalTime(numer_, denom_ == 0 ? 1 : denom_); + opentime::RationalTime time = opentime::RationalTime(r_.num, r_.den == 0 ? 1 : r_.den); return time.rescaled_to(framerate); } #endif @@ -143,64 +104,54 @@ rational rational::flipped() const void rational::flip() { if (!isNull()) { - std::swap(denom_, numer_); + std::swap(r_.den, r_.num); + FixSigns(); } } -bool rational::isNull() const -{ - return numerator() == 0; -} - -bool rational::isNaN() const -{ - return denominator() == 0; -} - -const intType &rational::numerator() const -{ - return numer_; -} - -const intType &rational::denominator() const -{ - return denom_; -} - QString rational::toString() const { - return QStringLiteral("%1/%2").arg(QString::number(numer_), QString::number(denom_)); + return QStringLiteral("%1/%2").arg(QString::number(r_.num), QString::number(r_.den)); +} + +void rational::FixSigns() +{ + if (r_.den < 0) { + // Normalize so that denominator is always positive + r_.den = -r_.den; + r_.num = -r_.num; + } else if (r_.den == 0) { + // Normalize to 0/0 (aka NaN) if denominator is zero + r_.num = 0; + } else if (r_.num == 0) { + // Normalize to 0/1 if numerator is zero + r_.den = 1; + } +} + +void rational::Reduce() +{ + av_reduce(&r_.num, &r_.den, r_.num, r_.den, INT_MAX); } //Assignment Operators const rational& rational::operator=(const rational &rhs) { - if (this != &rhs) { - numer_ = rhs.numer_; - denom_ = rhs.denom_; - } - + r_ = rhs.r_; return *this; } const rational& rational::operator+=(const rational &rhs) { + Q_ASSERT(*this != RATIONAL_MIN && *this != RATIONAL_MAX && rhs != RATIONAL_MIN && rhs != RATIONAL_MAX); + if (!isNaN()) { if (rhs.isNaN()) { - // Set to NaN - denom_ = 0; - fix_signs(); - } else if (!rhs.isNull()) { - if (isNull()) { - numer_ = rhs.numer_; - denom_ = rhs.denom_; - } else { - numer_ = (numer_ * rhs.denom_) + (rhs.numer_ * denom_); - denom_ = denom_ * rhs.denom_; - fix_signs(); - reduce(); - } + *this = NaN; + } else { + r_ = av_add_q(r_, rhs.r_); + FixSigns(); } } @@ -209,39 +160,14 @@ const rational& rational::operator+=(const rational &rhs) const rational& rational::operator-=(const rational &rhs) { + Q_ASSERT(*this != RATIONAL_MIN && *this != RATIONAL_MAX && rhs != RATIONAL_MIN && rhs != RATIONAL_MAX); + if (!isNaN()) { if (rhs.isNaN()) { - // Set to NaN - denom_ = 0; - fix_signs(); - } else if (!rhs.isNull()) { - if (isNull()) { - numer_ = -rhs.numer_; - denom_ = rhs.denom_; - } else { - numer_ = (numer_ * rhs.denom_) - (rhs.numer_ * denom_); - denom_ = denom_ * rhs.denom_; - fix_signs(); - reduce(); - } - } - } - - return *this; -} - -const rational& rational::operator/=(const rational &rhs) -{ - if (!isNaN()) { - if (rhs.isNaN()) { - // Set to NaN - denom_ = 0; - fix_signs(); + *this = NaN; } else { - numer_ = numer_ * rhs.denom_; - denom_ = denom_ * rhs.numer_; - fix_signs(); - reduce(); + r_ = av_sub_q(r_, rhs.r_); + FixSigns(); } } @@ -250,15 +176,30 @@ const rational& rational::operator/=(const rational &rhs) const rational& rational::operator*=(const rational &rhs) { + Q_ASSERT(*this != RATIONAL_MIN && *this != RATIONAL_MAX && rhs != RATIONAL_MIN && rhs != RATIONAL_MAX); + if (!isNaN()) { if (rhs.isNaN()) { - denom_ = 0; - fix_signs(); + *this = NaN; } else { - numer_ = numer_ * rhs.numer_; - denom_ = denom_ * rhs.denom_; - fix_signs(); - reduce(); + r_ = av_mul_q(r_, rhs.r_); + FixSigns(); + } + } + + return *this; +} + +const rational& rational::operator/=(const rational &rhs) +{ + Q_ASSERT(*this != RATIONAL_MIN && *this != RATIONAL_MAX && rhs != RATIONAL_MIN && rhs != RATIONAL_MAX); + + if (!isNaN()) { + if (rhs.isNaN()) { + *this = NaN; + } else { + r_ = av_div_q(r_, rhs.r_); + FixSigns(); } } @@ -299,87 +240,29 @@ rational rational::operator*(const rational &rhs) const bool rational::operator<(const rational &rhs) const { - if (isNaN() || rhs.isNaN()) { - return false; - } - - if (isNull() && rhs.isNull()) { - return false; - } - - if (rhs == RATIONAL_MAX - || *this == RATIONAL_MIN) { - // We will always either be LESS THAN (true) or EQUAL (false) - return (*this != rhs); - } - - if (*this == RATIONAL_MAX - || rhs == RATIONAL_MIN) { - // We will always be GREATER THAN (false) or EQUAL (false) - return false; - } - - if (!isNull() && rhs.isNull()) { - return (numer_ * denom_ < intType(0)); - } - - if (isNull() && !rhs.isNull()) { - return !(rhs.numer_ * rhs.denom_ < intType(0)); - } - - return ((numer_ * rhs.denom_) < (denom_ * rhs.numer_)); + return av_cmp_q(r_, rhs.r_) == -1; } bool rational::operator<=(const rational &rhs) const { - if (isNaN() || rhs.isNaN()) { - return false; - } - - if (isNull() && rhs.isNull()) { - return true; - } - - if (rhs == RATIONAL_MAX - || *this == RATIONAL_MIN) { - // We will always either be LESS THAN (true) or EQUAL (true) - return true; - } - - if (*this == RATIONAL_MAX - || rhs == RATIONAL_MIN) { - // We will always be GREATER THAN (false) or EQUAL (true) - return rhs == *this; - } - - if (!isNull() && rhs.isNull()) { - return (numer_ * denom_ < intType(0)); - } - - if (isNull() && !rhs.isNull()) { - return !(rhs.numer_ * rhs.denom_ < intType(0)); - } - - return ((numer_ * rhs.denom_) <= (denom_ * rhs.numer_)); + int cmp = av_cmp_q(r_, rhs.r_); + return cmp == 0 || cmp == -1; } bool rational::operator>(const rational &rhs) const { - return rhs < *this; + return av_cmp_q(r_, rhs.r_) == 1; } bool rational::operator>=(const rational &rhs) const { - return rhs <= *this; + int cmp = av_cmp_q(r_, rhs.r_); + return cmp == 0 || cmp == 1; } bool rational::operator==(const rational &rhs) const { - if (isNaN() || rhs.isNaN()) { - return false; - } - - return (numer_ == rhs.numer_ && denom_ == rhs.denom_); + return av_cmp_q(r_, rhs.r_) == 0; } bool rational::operator!=(const rational &rhs) const @@ -387,55 +270,6 @@ bool rational::operator!=(const rational &rhs) const return !(*this == rhs); } -const rational& rational::operator+() const -{ - return *this; -} - -rational rational::operator-() const -{ - return rational(numer_, -denom_); -} - -bool rational::operator!() const -{ - return !numer_; -} - -//IO - -std::ostream& operator<<(std::ostream &out, const rational &value) -{ - out << value.numer_; - - if (value.denom_ != 1) { - out << '/' << value.denom_; - return out; - } - - return out; -} - -std::istream& operator>>(std::istream &in, rational &value) -{ - in >> value.numer_; - value.denom_ = 1; - - char ch; - in.get(ch); - - if(!in.eof()) { - if(ch == '/') { - in >> value.denom_; - value.fix_signs(); - value.reduce(); - } else { - in.putback(ch); - } - } - return in; -} - uint qHash(const rational &r, uint seed) { return ::qHash(r.toDouble(), seed); diff --git a/app/common/rational.h b/app/common/rational.h index 074f1b864..4701e1e0a 100644 --- a/app/common/rational.h +++ b/app/common/rational.h @@ -1,60 +1,67 @@ -//Copyright 2015 Adam Quintero -//This program is distributed under the terms of the GNU General Public License. +/*** -// Adapted by MattKC for the Olive Video Editor (2019) + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 RATIONAL_H #define RATIONAL_H + +extern "C" { +#include +} + #include +#include +#include #ifdef USE_OTIO #include #endif -#include -#include - -extern "C" { -#include -} - #include "common/define.h" namespace olive { -typedef int64_t intType; -/* - * Zero Handling - * 0/0 = 0 - * 0/non-zero = 0 - * non-zero/0 = 0 -*/ class rational { public: - //constructors - rational(const intType &numerator = 0) : - numer_(numerator), - denom_(1) + rational(const int &numerator = 0) { + r_.num = numerator; + r_.den = 1; } - rational(const intType &numerator, const intType &denominator) : - numer_(numerator), - denom_(denominator) + rational(const int &numerator, const int &denominator) { - fix_signs(); - reduce(); + r_.num = numerator; + r_.den = denominator; + + FixSigns(); + Reduce(); } rational(const rational &rhs) = default; - rational(const AVRational& r) : - numer_(r.num), - denom_(r.den) + rational(const AVRational& r) { - fix_signs(); - reduce(); + r_ = r; + + FixSigns(); } static rational fromDouble(const double& flt, bool *ok = nullptr); @@ -84,9 +91,9 @@ public: bool operator!=(const rational &rhs) const; //Unary operators - const rational& operator+() const; - rational operator-() const; - bool operator!() const; + const rational& operator+() const { return *this; } + rational operator-() const { return rational(r_.num, -r_.den); } + bool operator!() const { return !r_.num; } //Function: convert to double double toDouble() const; @@ -100,7 +107,7 @@ public: return fromDouble(t.to_seconds()); } - // Convert Olive ratioanls to opentime rationals with the given framerate (defaults to 24) + // Convert Olive rationals to opentime rationals with the given framerate (defaults to 24) opentime::RationalTime toRationalTime(double framerate = 24) const; #endif @@ -111,35 +118,33 @@ public: // Returns whether the rational is valid but equal to zero or not // // A NaN is always a null, but a null is not always a NaN - bool isNull() const; + bool isNull() const { return r_.num == 0; } - // Returns whether this rational is not a valid number - bool isNaN() const; + // Returns whether this rational is not a valid number (denominator == 0) + bool isNaN() const { return r_.den == 0; } - //IO - friend std::ostream& operator<<(std::ostream &out, const rational &value); - friend std::istream& operator>>(std::istream &in, rational &value); - - const intType& numerator() const; - const intType& denominator() const; + const int& numerator() const { return r_.num; } + const int& denominator() const { return r_.den; } QString toString() const; + friend std::ostream& operator<<(std::ostream &out, const rational &value) + { + out << value.r_.num << '/' << value.r_.den; + + return out; + } + private: - //numerator and denominator - intType numer_; - intType denom_; + void FixSigns(); + void Reduce(); + + AVRational r_; - //Function: ensures denom >= 0 - void fix_signs(); - //Function: ensures lowest form - void reduce(); - //Function: finds greatest common denominator - static intType gcd(const intType &x, const intType &y); }; -#define RATIONAL_MIN rational(INT64_MIN, 1) -#define RATIONAL_MAX rational(INT64_MAX, 1) +#define RATIONAL_MIN rational(INT_MIN) +#define RATIONAL_MAX rational(INT_MAX) uint qHash(const rational& r, uint seed = 0); diff --git a/app/common/timerange.cpp b/app/common/timerange.cpp index c352e476d..c4337a7aa 100644 --- a/app/common/timerange.cpp +++ b/app/common/timerange.cpp @@ -44,6 +44,7 @@ const rational &TimeRange::out() const const rational &TimeRange::length() const { + Q_ASSERT(!length_.isNaN()); return length_; } @@ -173,7 +174,11 @@ void TimeRange::normalize() } // Calculate length - length_ = out_ - in_; + if (out_ == RATIONAL_MIN || out_ == RATIONAL_MAX || in_ == RATIONAL_MIN || in_ == RATIONAL_MAX) { + length_ = rational::NaN; + } else { + length_ = out_ - in_; + } } void TimeRangeList::insert(const TimeRangeList &list_to_add) diff --git a/app/config/config.cpp b/app/config/config.cpp index 2f88ea1b9..501cc4f35 100644 --- a/app/config/config.cpp +++ b/app/config/config.cpp @@ -135,6 +135,8 @@ void Config::SetDefaults() // Online/offline settings SetEntryInternal(QStringLiteral("OnlinePixelFormat"), NodeValue::kInt, VideoParams::kFormatFloat32); SetEntryInternal(QStringLiteral("OfflinePixelFormat"), NodeValue::kInt, VideoParams::kFormatFloat16); + + SetEntryInternal(QStringLiteral("MarkerColor"), NodeValue::kInt, ColorCoding::kLime); } void Config::Load() diff --git a/app/core.cpp b/app/core.cpp index d4e091959..c86f1a9e5 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -245,14 +245,14 @@ UndoStack *Core::undo_stack() return &undo_stack_; } -void Core::ImportFiles(const QStringList &urls, ProjectViewModel* model, Folder* parent) +void Core::ImportFiles(const QStringList &urls, Folder* parent) { if (urls.isEmpty()) { QMessageBox::critical(main_window_, tr("Import error"), tr("Nothing to import")); return; } - ProjectImportTask* pim = new ProjectImportTask(model, parent, urls); + ProjectImportTask* pim = new ProjectImportTask(parent, urls); if (!pim->GetFileCount()) { // No files to import @@ -364,7 +364,7 @@ void Core::DialogImportShow() // Get the selected folder in this panel Folder* folder = active_project_panel->GetSelectedFolder(); - ImportFiles(files, active_project_panel->model(), folder); + ImportFiles(files, folder); } } diff --git a/app/core.h b/app/core.h index 653769af6..759cdd522 100644 --- a/app/core.h +++ b/app/core.h @@ -182,7 +182,7 @@ public: * * @param urls */ - void ImportFiles(const QStringList& urls, ProjectViewModel *model, Folder *parent); + void ImportFiles(const QStringList& urls, Folder *parent); /** * @brief Get the currently active tool diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index a7ecf0f78..1f9df6e3d 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -24,6 +24,7 @@ add_subdirectory(export) add_subdirectory(footageproperties) add_subdirectory(footagerelink) add_subdirectory(keyframeproperties) +add_subdirectory(markerproperties) if(OpenTimelineIO_FOUND) add_subdirectory(otioproperties) endif() diff --git a/app/dialog/export/CMakeLists.txt b/app/dialog/export/CMakeLists.txt index 845d4ab91..481feeed5 100644 --- a/app/dialog/export/CMakeLists.txt +++ b/app/dialog/export/CMakeLists.txt @@ -24,6 +24,8 @@ set(OLIVE_SOURCES dialog/export/exportadvancedvideodialog.h dialog/export/exportaudiotab.cpp dialog/export/exportaudiotab.h + dialog/export/exportformatcombobox.cpp + dialog/export/exportformatcombobox.h dialog/export/exportsubtitlestab.cpp dialog/export/exportsubtitlestab.h dialog/export/exportvideotab.cpp diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp index 83f8a0d8d..ba5280897 100644 --- a/app/dialog/export/export.cpp +++ b/app/dialog/export/export.cpp @@ -38,6 +38,7 @@ #include "node/project/sequence/sequence.h" #include "task/taskmanager.h" #include "ui/icons/icons.h" +#include "widget/timeruler/timeruler.h" namespace olive { @@ -117,7 +118,7 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : row++; preferences_layout->addWidget(new QLabel(tr("Format:")), row, 0); - format_combobox_ = new QComboBox(); + format_combobox_ = new ExportFormatComboBox(); preferences_layout->addWidget(format_combobox_, row, 1, 1, 3); row++; @@ -192,33 +193,11 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : // Set default filename SetDefaultFilename(); - // Populate combobox formats - for (int i=0; i(i)); - - bool inserted = false; - - for (int j=0; jcount(); j++) { - if (format_combobox_->itemText(j) > format_name) { - format_combobox_->insertItem(j, format_name, i); - inserted = true; - break; - } - } - - if (!inserted) { - format_combobox_->addItem(format_name, i); - } - } - // Set defaults previously_selected_format_ = ExportFormat::kFormatMPEG4; - SetCurrentFormat(ExportFormat::kFormatMPEG4); - connect(format_combobox_, - static_cast(&QComboBox::currentIndexChanged), - this, - &ExportDialog::FormatChanged); - FormatChanged(format_combobox_->currentIndex()); + format_combobox_->SetFormat(ExportFormat::kFormatMPEG4); + connect(format_combobox_, &ExportFormatComboBox::FormatChanged, this, &ExportDialog::FormatChanged); + FormatChanged(format_combobox_->GetFormat()); VideoParams vp = viewer_node_->GetVideoParams(); AudioParams ap = viewer_node_->GetAudioParams(); @@ -272,11 +251,6 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) : preview_viewer_->SetColorTransform(video_tab_->CurrentOCIOColorSpace()); } -ExportFormat::Format ExportDialog::GetSelectedFormat() const -{ - return static_cast(format_combobox_->currentData().toInt()); -} - rational ExportDialog::GetSelectedTimebase() const { return video_tab_->GetSelectedFrameRate().flipped(); @@ -292,7 +266,7 @@ void ExportDialog::StartExport() // Validate if the entered filename contains the correct extension (the extension is necessary // for both FFmpeg and OIIO to determine the output format) - QString necessary_ext = QStringLiteral(".%1").arg(ExportFormat::GetExtension(GetSelectedFormat())); + QString necessary_ext = QStringLiteral(".%1").arg(ExportFormat::GetExtension(format_combobox_->GetFormat())); QString proposed_filename = filename_edit_->text().trimmed(); // If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export. @@ -427,7 +401,7 @@ void ExportDialog::AddPreferencesTab(QWidget *inner_widget, const QString &title void ExportDialog::BrowseFilename() { - ExportFormat::Format f = GetSelectedFormat(); + ExportFormat::Format f = format_combobox_->GetFormat(); QString browsed_fn = QFileDialog::getSaveFileName(this, "", @@ -443,11 +417,10 @@ void ExportDialog::BrowseFilename() } } -void ExportDialog::FormatChanged(int index) +void ExportDialog::FormatChanged(ExportFormat::Format current_format) { QString current_filename = filename_edit_->text().trimmed(); QString previously_selected_ext = ExportFormat::GetExtension(previously_selected_format_); - ExportFormat::Format current_format = static_cast(format_combobox_->itemData(index).toInt()); QString currently_selected_ext = ExportFormat::GetExtension(current_format); // If the previous extension was added, remove it @@ -545,7 +518,7 @@ ExportParams ExportDialog::GenerateParams() const AudioParams::kInternalFormat); ExportParams params; - params.set_encoder(Encoder::GetTypeFromFormat(GetSelectedFormat())); + params.set_encoder(Encoder::GetTypeFromFormat(format_combobox_->GetFormat())); params.SetFilename(filename_edit_->text().trimmed()); params.SetExportLength(viewer_node_->GetLength()); @@ -593,16 +566,6 @@ ExportParams ExportDialog::GenerateParams() const return params; } -void ExportDialog::SetCurrentFormat(ExportFormat::Format format) -{ - for (int i=0; icount(); i++) { - if (format_combobox_->itemData(i).toInt() == format) { - format_combobox_->setCurrentIndex(i); - break; - } - } -} - rational ExportDialog::GetExportLength() const { if (range_combobox_->currentIndex() == kRangeInToOut) { diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h index ad8056fcc..d0938a4b1 100644 --- a/app/dialog/export/export.h +++ b/app/dialog/export/export.h @@ -29,6 +29,7 @@ #include "codec/exportcodec.h" #include "codec/exportformat.h" +#include "dialog/export/exportformatcombobox.h" #include "exportaudiotab.h" #include "exportsubtitlestab.h" #include "exportvideotab.h" @@ -43,8 +44,6 @@ class ExportDialog : public QDialog public: ExportDialog(ViewerOutput* viewer_node, QWidget* parent = nullptr); - ExportFormat::Format GetSelectedFormat() const; - rational GetSelectedTimebase() const; protected: @@ -58,8 +57,6 @@ private: ExportParams GenerateParams() const; - void SetCurrentFormat(ExportFormat::Format format); - ViewerOutput* viewer_node_; ExportFormat::Format previously_selected_format_; @@ -82,7 +79,7 @@ private: ViewerWidget* preview_viewer_; QLineEdit* filename_edit_; - QComboBox* format_combobox_; + ExportFormatComboBox* format_combobox_; ExportVideoTab* video_tab_; ExportAudioTab* audio_tab_; @@ -98,7 +95,7 @@ private: private slots: void BrowseFilename(); - void FormatChanged(int index); + void FormatChanged(ExportFormat::Format current_format); void ResolutionChanged(); diff --git a/app/dialog/export/exportaudiotab.h b/app/dialog/export/exportaudiotab.h index ef5155018..4e29e3d48 100644 --- a/app/dialog/export/exportaudiotab.h +++ b/app/dialog/export/exportaudiotab.h @@ -37,8 +37,6 @@ class ExportAudioTab : public QWidget public: ExportAudioTab(QWidget* parent = nullptr); - int SetFormat(ExportFormat::Format format); - QComboBox* codec_combobox() const { return codec_combobox_; @@ -59,6 +57,9 @@ public: return bit_rate_slider_; } +public slots: + int SetFormat(ExportFormat::Format format); + private: QComboBox* codec_combobox_; SampleRateComboBox* sample_rate_combobox_; diff --git a/app/dialog/export/exportformatcombobox.cpp b/app/dialog/export/exportformatcombobox.cpp new file mode 100644 index 000000000..9f1b1927f --- /dev/null +++ b/app/dialog/export/exportformatcombobox.cpp @@ -0,0 +1,83 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "exportformatcombobox.h" + +namespace olive { + +ExportFormatComboBox::ExportFormatComboBox(Mode mode, QWidget *parent) : + QComboBox(parent) +{ + // Populate combobox formats + for (int i=0; i(i); + + switch (mode) { + case kShowAllFormats: + break; + case kShowAudioOnly: + if (!ExportFormat::GetVideoCodecs(f).isEmpty()) { + continue; + } + break; + case kShowVideoOnly: + if (!ExportFormat::GetAudioCodecs(f).isEmpty()) { + continue; + } + break; + } + + QString format_name = ExportFormat::GetName(f); + + bool inserted = false; + + // Sort formats alphabetically + for (int j=0; j format_name) { + insertItem(j, format_name, i); + inserted = true; + break; + } + } + + if (!inserted) { + addItem(format_name, i); + } + } + + connect(this, static_cast(&QComboBox::currentIndexChanged), this, &ExportFormatComboBox::HandleIndexChange); +} + +void ExportFormatComboBox::SetFormat(ExportFormat::Format fmt) +{ + for (int i=0; i(itemData(index).toInt())); +} + +} diff --git a/app/dialog/export/exportformatcombobox.h b/app/dialog/export/exportformatcombobox.h new file mode 100644 index 000000000..3dd33e848 --- /dev/null +++ b/app/dialog/export/exportformatcombobox.h @@ -0,0 +1,63 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 EXPORTFORMATCOMBOBOX_H +#define EXPORTFORMATCOMBOBOX_H + +#include + +#include "codec/exportformat.h" + +namespace olive { + +class ExportFormatComboBox : public QComboBox +{ + Q_OBJECT +public: + enum Mode { + kShowAllFormats, + kShowAudioOnly, + kShowVideoOnly + }; + + ExportFormatComboBox(Mode mode, QWidget *parent = nullptr); + ExportFormatComboBox(QWidget *parent = nullptr) : + ExportFormatComboBox(kShowAllFormats, parent) + {} + + ExportFormat::Format GetFormat() const + { + return static_cast(currentData().toInt()); + } + +signals: + void FormatChanged(ExportFormat::Format fmt); + +public slots: + void SetFormat(ExportFormat::Format fmt); + +private slots: + void HandleIndexChange(int index); + +}; + +} + +#endif // EXPORTFORMATCOMBOBOX_H diff --git a/app/dialog/keyframeproperties/keyframeproperties.cpp b/app/dialog/keyframeproperties/keyframeproperties.cpp index 9637c486f..65fbcd791 100644 --- a/app/dialog/keyframeproperties/keyframeproperties.cpp +++ b/app/dialog/keyframeproperties/keyframeproperties.cpp @@ -30,7 +30,7 @@ namespace olive { -KeyframePropertiesDialog::KeyframePropertiesDialog(const QVector &keys, const rational &timebase, QWidget *parent) : +KeyframePropertiesDialog::KeyframePropertiesDialog(const std::vector &keys, const rational &timebase, QWidget *parent) : QDialog(parent), keys_(keys), timebase_(timebase) @@ -91,7 +91,7 @@ KeyframePropertiesDialog::KeyframePropertiesDialog(const QVector bool all_same_bezier_out_x = true; bool all_same_bezier_out_y = true; - for (int i=0;i 0) { NodeKeyframe* prev_key = keys_.at(i-1); NodeKeyframe* this_key = keys_.at(i); @@ -126,7 +126,7 @@ KeyframePropertiesDialog::KeyframePropertiesDialog(const QVector // Determine if any keyframes are on the same track (in which case we can't set the time) if (can_set_time) { - for (int j=0;jtrack() == keys_.at(i)->track()) { can_set_time = false; @@ -147,7 +147,7 @@ KeyframePropertiesDialog::KeyframePropertiesDialog(const QVector } if (all_same_time) { - time_slider_->SetValue(keys_.first()->time()); + time_slider_->SetValue(keys_.front()->time()); } else { time_slider_->SetTristate(); } @@ -169,7 +169,7 @@ KeyframePropertiesDialog::KeyframePropertiesDialog(const QVector if (all_same_type) { // If all keyframes are the same type, set it here for (int i=0;icount();i++) { - if (type_select_->itemData(i).toInt() == keys_.first()->type()) { + if (type_select_->itemData(i).toInt() == keys_.front()->type()) { type_select_->setCurrentIndex(i); // Ensure UI updates for this index @@ -179,10 +179,10 @@ KeyframePropertiesDialog::KeyframePropertiesDialog(const QVector } } - SetUpBezierSlider(bezier_in_x_slider_, all_same_bezier_in_x, keys_.first()->bezier_control_in().x()); - SetUpBezierSlider(bezier_in_y_slider_, all_same_bezier_in_y, keys_.first()->bezier_control_in().y()); - SetUpBezierSlider(bezier_out_x_slider_, all_same_bezier_out_x, keys_.first()->bezier_control_out().x()); - SetUpBezierSlider(bezier_out_y_slider_, all_same_bezier_out_y, keys_.first()->bezier_control_out().y()); + SetUpBezierSlider(bezier_in_x_slider_, all_same_bezier_in_x, keys_.front()->bezier_control_in().x()); + SetUpBezierSlider(bezier_in_y_slider_, all_same_bezier_in_y, keys_.front()->bezier_control_in().y()); + SetUpBezierSlider(bezier_out_x_slider_, all_same_bezier_out_x, keys_.front()->bezier_control_out().x()); + SetUpBezierSlider(bezier_out_y_slider_, all_same_bezier_out_y, keys_.front()->bezier_control_out().y()); row++; diff --git a/app/dialog/keyframeproperties/keyframeproperties.h b/app/dialog/keyframeproperties/keyframeproperties.h index 835845943..418c09321 100644 --- a/app/dialog/keyframeproperties/keyframeproperties.h +++ b/app/dialog/keyframeproperties/keyframeproperties.h @@ -35,7 +35,7 @@ class KeyframePropertiesDialog : public QDialog { Q_OBJECT public: - KeyframePropertiesDialog(const QVector& keys, const rational& timebase, QWidget* parent = nullptr); + KeyframePropertiesDialog(const std::vector& keys, const rational& timebase, QWidget* parent = nullptr); public slots: virtual void accept() override; @@ -43,7 +43,7 @@ public slots: private: void SetUpBezierSlider(FloatSlider *slider, bool all_same, double value); - const QVector& keys_; + const std::vector& keys_; rational timebase_; diff --git a/app/widget/snapservice/CMakeLists.txt b/app/dialog/markerproperties/CMakeLists.txt similarity index 87% rename from app/widget/snapservice/CMakeLists.txt rename to app/dialog/markerproperties/CMakeLists.txt index 6224fa753..222c9246e 100644 --- a/app/widget/snapservice/CMakeLists.txt +++ b/app/dialog/markerproperties/CMakeLists.txt @@ -16,7 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - widget/snapservice/snapservice.cpp - widget/snapservice/snapservice.h + dialog/markerproperties/markerpropertiesdialog.h + dialog/markerproperties/markerpropertiesdialog.cpp PARENT_SCOPE ) diff --git a/app/dialog/markerproperties/markerpropertiesdialog.cpp b/app/dialog/markerproperties/markerpropertiesdialog.cpp new file mode 100644 index 000000000..007625f8f --- /dev/null +++ b/app/dialog/markerproperties/markerpropertiesdialog.cpp @@ -0,0 +1,152 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "markerpropertiesdialog.h" + +#include +#include +#include +#include +#include + +#include "core.h" + +namespace olive { + +#define super QDialog + +MarkerPropertiesDialog::MarkerPropertiesDialog(const std::vector &markers, const rational &timebase, QWidget *parent) : + super(parent), + markers_(markers) +{ + QGridLayout *layout = new QGridLayout(this); + + int row = 0; + + QGroupBox *time_group = new QGroupBox(tr("Time")); + QGridLayout *time_layout = new QGridLayout(time_group); + + { + int time_row = 0; + + time_layout->addWidget(new QLabel(tr("In:")), time_row, 0); + + in_slider_ = new RationalSlider(); + time_layout->addWidget(in_slider_, time_row, 1); + + time_row++; + + time_layout->addWidget(new QLabel(tr("Out:")), time_row, 0); + + out_slider_ = new RationalSlider(); + time_layout->addWidget(out_slider_, time_row, 1); + } + + if (markers.size() == 1) { + in_slider_->SetValue(markers.front()->time_range().in()); + in_slider_->SetDisplayType(RationalSlider::kTime); + in_slider_->SetTimebase(timebase); + out_slider_->SetValue(markers.front()->time_range().out()); + out_slider_->SetDisplayType(RationalSlider::kTime); + out_slider_->SetTimebase(timebase); + } else { + // Markers cannot be on the same time, so we disable setting time if multiple markers are selected + in_slider_->setEnabled(false); + in_slider_->SetTristate(); + out_slider_->setEnabled(false); + out_slider_->SetTristate(); + } + + layout->addWidget(time_group, row, 0, 1, 2); + + row++; + + layout->addWidget(new QLabel(tr("Color:")), row, 0); + + color_menu_ = new ColorCodingComboBox(); + layout->addWidget(color_menu_, row, 1); + + color_menu_->SetColor(markers.front()->color()); + for (size_t i=1; icolor() != color_menu_->GetSelectedColor()) { + color_menu_->SetColor(-1); + break; + } + } + + row++; + + layout->addWidget(new QLabel(tr("Name:")), row, 0); + + label_edit_ = new LineEditWithFocusSignal(); + connect(label_edit_, &LineEditWithFocusSignal::Focused, this, [this]{ + label_edit_->setPlaceholderText(QString()); + }); + layout->addWidget(label_edit_, row, 1); + + // Determine what the startup label text should be + label_edit_->setText(markers.front()->name()); + for (size_t i=1; iname() != label_edit_->text()) { + label_edit_->clear(); + label_edit_->setPlaceholderText(tr("(multiple)")); + break; + } + } + + row++; + + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &MarkerPropertiesDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &MarkerPropertiesDialog::reject); + layout->addWidget(buttons, row, 0, 1, 2); +} + +void MarkerPropertiesDialog::accept() +{ + if (in_slider_->isEnabled() && in_slider_->GetValue() > out_slider_->GetValue()) { + QMessageBox::critical(this, tr("Invalid Values"), tr("In point must be less than or equal to out point.")); + return; + } + + MultiUndoCommand *command = new MultiUndoCommand(); + + int color = color_menu_->GetSelectedColor(); + + foreach (TimelineMarker *m, markers_) { + if (color != -1) { + command->add_child(new MarkerChangeColorCommand(m, color)); + } + + if (label_edit_->placeholderText().isEmpty()) { + command->add_child(new MarkerChangeNameCommand(m, label_edit_->text())); + } + } + + if (markers_.size() == 1) { + command->add_child(new MarkerChangeTimeCommand(markers_.front(), TimeRange(in_slider_->GetValue(), out_slider_->GetValue()))); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); + + super::accept(); +} + +} diff --git a/app/dialog/markerproperties/markerpropertiesdialog.h b/app/dialog/markerproperties/markerpropertiesdialog.h new file mode 100644 index 000000000..a358dc9a7 --- /dev/null +++ b/app/dialog/markerproperties/markerpropertiesdialog.h @@ -0,0 +1,78 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 MARKERPROPERTIESDIALOG_H +#define MARKERPROPERTIESDIALOG_H + +#include +#include + +#include "timeline/timelinemarker.h" +#include "widget/colorlabelmenu/colorcodingcombobox.h" +#include "widget/slider/rationalslider.h" + +namespace olive { + +class LineEditWithFocusSignal : public QLineEdit +{ + Q_OBJECT +public: + LineEditWithFocusSignal(QWidget *parent = nullptr) : + QLineEdit(parent) + { + } + +protected: + virtual void focusInEvent(QFocusEvent *e) override + { + QLineEdit::focusInEvent(e); + emit Focused(); + } + +signals: + void Focused(); + +}; + +class MarkerPropertiesDialog : public QDialog +{ + Q_OBJECT +public: + MarkerPropertiesDialog(const std::vector &markers, const rational &timebase, QWidget *parent = nullptr); + +public slots: + virtual void accept() override; + +private: + std::vector markers_; + + LineEditWithFocusSignal *label_edit_; + + ColorCodingComboBox *color_menu_; + + RationalSlider *in_slider_; + + RationalSlider *out_slider_; + +}; + +} + +#endif // MARKERPROPERTIESDIALOG_H diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.cpp b/app/dialog/preferences/tabs/preferencesappearancetab.cpp index bec5431ad..761ea18dd 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.cpp +++ b/app/dialog/preferences/tabs/preferencesappearancetab.cpp @@ -82,6 +82,22 @@ PreferencesAppearanceTab::PreferencesAppearanceTab() appearance_layout->addWidget(color_group, row, 0, 1, 2); } + row++; + { + QGroupBox* marker_group = new QGroupBox(); + marker_group->setTitle(tr("Miscellaneous")); + + QGridLayout* marker_layout = new QGridLayout(marker_group); + + marker_layout->addWidget(new QLabel("Default Marker Color"), 0, 0); + + marker_btn_ = new ColorCodingComboBox(); + marker_btn_->SetColor(Config::Current()[QStringLiteral("MarkerColor")].toInt()); + marker_layout->addWidget(marker_btn_, 0, 1); + + appearance_layout->addWidget(marker_group, row, 0, 1, 2); + } + layout->addStretch(); } @@ -99,6 +115,8 @@ void PreferencesAppearanceTab::Accept(MultiUndoCommand *command) for (int i=0; iGetSelectedColor(); } + + Config::Current()[QStringLiteral("MarkerColor")] = marker_btn_->GetSelectedColor(); } } diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.h b/app/dialog/preferences/tabs/preferencesappearancetab.h index 3ee33a0ad..286c79e03 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.h +++ b/app/dialog/preferences/tabs/preferencesappearancetab.h @@ -47,6 +47,8 @@ private: QVector color_btns_; + ColorCodingComboBox* marker_btn_; + }; } diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.cpp b/app/dialog/preferences/tabs/preferencesaudiotab.cpp index 8b16c350b..e8371cf9f 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.cpp +++ b/app/dialog/preferences/tabs/preferencesaudiotab.cpp @@ -26,6 +26,8 @@ #include "audio/audiomanager.h" #include "config/config.h" +#include "dialog/export/exportaudiotab.h" +#include "dialog/export/exportformatcombobox.h" namespace olive { @@ -87,12 +89,24 @@ PreferencesAudioTab::PreferencesAudioTab() row++; - input_layout->addWidget(new QLabel(tr("Recording Mode:"), this), row, 0); + QGroupBox *recording_group = new QGroupBox(tr("Recording")); + input_layout->addWidget(recording_group, row, 0, 1, 2); - recording_combobox_ = new QComboBox(); - recording_combobox_->addItem(tr("Mono")); - recording_combobox_->addItem(tr("Stereo")); - input_layout->addWidget(recording_combobox_, row, 1); + QVBoxLayout *recording_layout = new QVBoxLayout(recording_group); + + QHBoxLayout *fmt_layout = new QHBoxLayout(); + recording_layout->addLayout(fmt_layout); + + fmt_layout->addWidget(new QLabel(tr("Format:"))); + + ExportFormatComboBox *fmt_combo = new ExportFormatComboBox(ExportFormatComboBox::kShowAudioOnly); + fmt_combo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + fmt_layout->addWidget(fmt_combo); + + ExportAudioTab *audio_recording_options = new ExportAudioTab(); + recording_layout->addWidget(audio_recording_options); + + connect(fmt_combo, &ExportFormatComboBox::FormatChanged, audio_recording_options, &ExportAudioTab::SetFormat); } QHBoxLayout* refresh_layout = new QHBoxLayout(); diff --git a/app/node/CMakeLists.txt b/app/node/CMakeLists.txt index cd510b021..3b66b8129 100644 --- a/app/node/CMakeLists.txt +++ b/app/node/CMakeLists.txt @@ -48,8 +48,6 @@ set(OLIVE_SOURCES node/keyframe.h node/node.cpp node/node.h - node/nodecopypaste.cpp - node/nodecopypaste.h node/param.cpp node/param.h node/splitvalue.h diff --git a/app/node/audio/pan/pan.cpp b/app/node/audio/pan/pan.cpp index 9eb8e0766..5e6d87a39 100644 --- a/app/node/audio/pan/pan.cpp +++ b/app/node/audio/pan/pan.cpp @@ -27,6 +27,8 @@ namespace olive { const QString PanNode::kSamplesInput = QStringLiteral("samples_in"); const QString PanNode::kPanningInput = QStringLiteral("panning_in"); +#define super Node + PanNode::PanNode() { AddInput(kSamplesInput, NodeValue::kSamples, InputFlags(kInputFlagNotKeyframable)); @@ -35,6 +37,9 @@ PanNode::PanNode() SetInputProperty(kPanningInput, QStringLiteral("min"), -1.0); SetInputProperty(kPanningInput, QStringLiteral("max"), 1.0); SetInputProperty(kPanningInput, QStringLiteral("view"), FloatSlider::kPercentage); + + SetFlags(kAudioEffect); + SetEffectInput(kSamplesInput); } Node *PanNode::copy() const @@ -115,6 +120,8 @@ void PanNode::ProcessSamples(const NodeValueRow &values, const SampleBufferPtr i void PanNode::Retranslate() { + super::Retranslate(); + SetInputName(kSamplesInput, tr("Samples")); SetInputName(kPanningInput, tr("Pan")); } diff --git a/app/node/audio/volume/volume.cpp b/app/node/audio/volume/volume.cpp index e6c26b50e..79feef607 100644 --- a/app/node/audio/volume/volume.cpp +++ b/app/node/audio/volume/volume.cpp @@ -27,6 +27,8 @@ namespace olive { const QString VolumeNode::kSamplesInput = QStringLiteral("samples_in"); const QString VolumeNode::kVolumeInput = QStringLiteral("volume_in"); +#define super MathNodeBase + VolumeNode::VolumeNode() { AddInput(kSamplesInput, NodeValue::kSamples, InputFlags(kInputFlagNotKeyframable)); @@ -34,6 +36,9 @@ VolumeNode::VolumeNode() AddInput(kVolumeInput, NodeValue::kFloat, 1.0); SetInputProperty(kVolumeInput, QStringLiteral("min"), 0.0); SetInputProperty(kVolumeInput, QStringLiteral("view"), FloatSlider::kDecibel); + + SetFlags(kAudioEffect); + SetEffectInput(kSamplesInput); } Node *VolumeNode::copy() const @@ -80,6 +85,8 @@ void VolumeNode::ProcessSamples(const NodeValueRow &values, const SampleBufferPt void VolumeNode::Retranslate() { + super::Retranslate(); + SetInputName(kSamplesInput, tr("Samples")); SetInputName(kVolumeInput, tr("Volume")); } diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index a726d6b0f..68ee09863 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -32,7 +32,6 @@ namespace olive { #define super Node const QString Block::kLengthInput = QStringLiteral("length_in"); -const QString Block::kEnabledInput = QStringLiteral("enabled_in"); Block::Block() : previous_(nullptr), @@ -46,8 +45,6 @@ Block::Block() : SetInputProperty(kLengthInput, QStringLiteral("viewlock"), true); IgnoreHashingFrom(kLengthInput); - AddInput(kEnabledInput, NodeValue::kBoolean, true, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); - SetFlags(kDontShowInParamView); } diff --git a/app/node/block/block.h b/app/node/block/block.h index 7f03ec805..a555cb065 100644 --- a/app/node/block/block.h +++ b/app/node/block/block.h @@ -118,7 +118,6 @@ public: virtual void InvalidateCache(const TimeRange& range, const QString& from, int element = -1, InvalidateCacheOptions options = InvalidateCacheOptions()) override; static const QString kLengthInput; - static const QString kEnabledInput; public slots: diff --git a/app/node/block/clip/clip.cpp b/app/node/block/clip/clip.cpp index 4e5cfdb81..bb5d7188d 100644 --- a/app/node/block/clip/clip.cpp +++ b/app/node/block/clip/clip.cpp @@ -57,6 +57,8 @@ ClipBlock::ClipBlock() : PrependInput(kBufferIn, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable)); SetValueHintForInput(kBufferIn, ValueHint(NodeValue::kBuffer)); + + SetEffectInput(kBufferIn); } Node *ClipBlock::copy() const @@ -206,10 +208,22 @@ void ClipBlock::InvalidateCache(const TimeRange& range, const QString& from, int // Find connected viewer node auto viewers = FindInputNodesConnectedToInput(NodeInput(this, kBufferIn)); - if (viewers.isEmpty()) { - connected_viewer_ = nullptr; - } else { - connected_viewer_ = viewers.first(); + ViewerOutput *new_connected_viewer = viewers.isEmpty() ? nullptr : viewers.first(); + + if (new_connected_viewer != connected_viewer_) { + if (connected_viewer_) { + disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerAdded, this, &ClipBlock::PreviewChanged); + disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerRemoved, this, &ClipBlock::PreviewChanged); + disconnect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerModified, this, &ClipBlock::PreviewChanged); + } + + connected_viewer_ = new_connected_viewer; + + if (connected_viewer_) { + connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerAdded, this, &ClipBlock::PreviewChanged); + connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerRemoved, this, &ClipBlock::PreviewChanged); + connect(connected_viewer_->GetTimelinePoints()->markers(), &TimelineMarkerList::MarkerModified, this, &ClipBlock::PreviewChanged); + } } super::InvalidateCache(adj, from, element, options); diff --git a/app/node/color/colormanager/colormanager.cpp b/app/node/color/colormanager/colormanager.cpp index 3a4bea28e..6164592b6 100644 --- a/app/node/color/colormanager/colormanager.cpp +++ b/app/node/color/colormanager/colormanager.cpp @@ -34,6 +34,8 @@ const QString ColorManager::kConfigFilenameIn = QStringLiteral("config"); const QString ColorManager::kDefaultColorspaceIn = QStringLiteral("default_input"); const QString ColorManager::kReferenceSpaceIn = QStringLiteral("reference_space"); +#define super Node + OCIO::ConstConfigRcPtr ColorManager::default_config_ = nullptr; ColorManager::ColorManager() : @@ -253,6 +255,8 @@ void ColorManager::GetDefaultLumaCoefs(double *rgb) const void ColorManager::Retranslate() { + super::Retranslate(); + SetInputName(kConfigFilenameIn, tr("Configuration")); SetInputName(kDefaultColorspaceIn, tr("Default Input")); SetInputName(kReferenceSpaceIn, tr("Reference Space")); diff --git a/app/node/distort/cornerpin/cornerpindistortnode.cpp b/app/node/distort/cornerpin/cornerpindistortnode.cpp index e4429ede7..febb41061 100644 --- a/app/node/distort/cornerpin/cornerpindistortnode.cpp +++ b/app/node/distort/cornerpin/cornerpindistortnode.cpp @@ -33,6 +33,8 @@ const QString CornerPinDistortNode::kBottomRightInput = QStringLiteral("bottom_r const QString CornerPinDistortNode::kBottomLeftInput = QStringLiteral("bottom_left_in"); const QString CornerPinDistortNode::kPerspectiveInput = QStringLiteral("perspective_in"); +#define super Node + CornerPinDistortNode::CornerPinDistortNode() { AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -48,10 +50,15 @@ CornerPinDistortNode::CornerPinDistortNode() gizmo_resize_handle_[1] = AddDraggableGizmo({NodeKeyframeTrackReference(NodeInput(this, kTopRightInput), 0), NodeKeyframeTrackReference(NodeInput(this, kTopRightInput), 1)}); gizmo_resize_handle_[2] = AddDraggableGizmo({NodeKeyframeTrackReference(NodeInput(this, kBottomRightInput), 0), NodeKeyframeTrackReference(NodeInput(this, kBottomRightInput), 1)}); gizmo_resize_handle_[3] = AddDraggableGizmo({NodeKeyframeTrackReference(NodeInput(this, kBottomLeftInput), 0), NodeKeyframeTrackReference(NodeInput(this, kBottomLeftInput), 1)}); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } void CornerPinDistortNode::Retranslate() { + super::Retranslate(); + SetInputName(kTextureInput, tr("Texture")); SetInputName(kPerspectiveInput, tr("Perspective")); SetInputName(kTopLeftInput, tr("Top Left")); diff --git a/app/node/distort/crop/cropdistortnode.cpp b/app/node/distort/crop/cropdistortnode.cpp index b204c7eda..18274109d 100644 --- a/app/node/distort/crop/cropdistortnode.cpp +++ b/app/node/distort/crop/cropdistortnode.cpp @@ -33,6 +33,8 @@ const QString CropDistortNode::kRightInput = QStringLiteral("right_in"); const QString CropDistortNode::kBottomInput = QStringLiteral("bottom_in"); const QString CropDistortNode::kFeatherInput = QStringLiteral("feather_in"); +#define super Node + CropDistortNode::CropDistortNode() { AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -56,10 +58,15 @@ CropDistortNode::CropDistortNode() point_gizmo_[kGizmoScaleBottomRight] = AddDraggableGizmo({kRightInput, kBottomInput}); point_gizmo_[kGizmoScaleCenterLeft] = AddDraggableGizmo({kLeftInput}); point_gizmo_[kGizmoScaleCenterRight] = AddDraggableGizmo({kRightInput}); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } void CropDistortNode::Retranslate() { + super::Retranslate(); + SetInputName(kTextureInput, tr("Texture")); SetInputName(kLeftInput, tr("Left")); SetInputName(kTopInput, tr("Top")); diff --git a/app/node/distort/flip/flipdistortnode.cpp b/app/node/distort/flip/flipdistortnode.cpp index 577437fe5..21a835e0c 100644 --- a/app/node/distort/flip/flipdistortnode.cpp +++ b/app/node/distort/flip/flipdistortnode.cpp @@ -26,6 +26,8 @@ const QString FlipDistortNode::kTextureInput = QStringLiteral("tex_in"); const QString FlipDistortNode::kHorizontalInput = QStringLiteral("horiz_in"); const QString FlipDistortNode::kVerticalInput = QStringLiteral("vert_in"); +#define super Node + FlipDistortNode::FlipDistortNode() { AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -33,6 +35,9 @@ FlipDistortNode::FlipDistortNode() AddInput(kHorizontalInput, NodeValue::kBoolean, false); AddInput(kVerticalInput, NodeValue::kBoolean, false); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } Node* FlipDistortNode::copy() const @@ -62,6 +67,8 @@ QString FlipDistortNode::Description() const void FlipDistortNode::Retranslate() { + super::Retranslate(); + SetInputName(kTextureInput, tr("Input")); SetInputName(kHorizontalInput, tr("Horizontal")); SetInputName(kVerticalInput, tr("Vertical")); diff --git a/app/node/distort/transform/transformdistortnode.cpp b/app/node/distort/transform/transformdistortnode.cpp index 59ec09bc2..e60d901f2 100644 --- a/app/node/distort/transform/transformdistortnode.cpp +++ b/app/node/distort/transform/transformdistortnode.cpp @@ -32,7 +32,7 @@ const QString TransformDistortNode::kTextureInput = QStringLiteral("tex_in"); const QString TransformDistortNode::kAutoscaleInput = QStringLiteral("autoscale_in"); const QString TransformDistortNode::kInterpolationInput = QStringLiteral("interpolation_in"); -#define super Node +#define super MatrixGenerator TransformDistortNode::TransformDistortNode() { @@ -64,11 +64,14 @@ TransformDistortNode::TransformDistortNode() point_gizmo_[i]->AddInput(NodeKeyframeTrackReference(NodeInput(this, kScaleInput), 1)); point_gizmo_[i]->SetDragValueBehavior(PointGizmo::kAbsolute); } + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } void TransformDistortNode::Retranslate() { - MatrixGenerator::Retranslate(); + super::Retranslate(); SetInputName(kAutoscaleInput, tr("Auto-Scale")); SetInputName(kTextureInput, tr("Texture")); @@ -150,7 +153,7 @@ void TransformDistortNode::Hash(QCryptographicHash &hash, const NodeGlobals &glo } } - Node::Hash(out, GetValueHintForInput(kTextureInput), hash, globals, video_params); + super::Hash(out, GetValueHintForInput(kTextureInput), hash, globals, video_params); } void TransformDistortNode::GizmoDragStart(const NodeValueRow &row, double x, double y, const rational &time) diff --git a/app/node/effect/opacity/opacityeffect.cpp b/app/node/effect/opacity/opacityeffect.cpp index b8ac2d017..b4dcba646 100644 --- a/app/node/effect/opacity/opacityeffect.cpp +++ b/app/node/effect/opacity/opacityeffect.cpp @@ -24,6 +24,9 @@ OpacityEffect::OpacityEffect() SetInputProperty(kValueInput, QStringLiteral("view"), FloatSlider::kPercentage); SetInputProperty(kValueInput, QStringLiteral("min"), 0.0); SetInputProperty(kValueInput, QStringLiteral("max"), 1.0); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } void OpacityEffect::Retranslate() diff --git a/app/node/factory.cpp b/app/node/factory.cpp index ab7632d7f..4292d5037 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -89,7 +89,7 @@ void NodeFactory::Destroy() library_.clear(); } -Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item, Node::CategoryID restrict_to) +Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item, Node::CategoryID restrict_to, uint64_t restrict_flags) { Menu* menu = new Menu(parent); menu->setToolTipsVisible(true); @@ -102,6 +102,10 @@ Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item, Node::Cate continue; } + if (restrict_flags && !(n->GetFlags() & restrict_flags)) { + continue; + } + if (hidden_.contains(i)) { // Skip this node continue; diff --git a/app/node/factory.h b/app/node/factory.h index bb5878dab..54e417b3e 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -84,7 +84,7 @@ public: static void Destroy(); - static Menu* CreateMenu(QWidget *parent, bool create_none_item = false, Node::CategoryID restrict_to = Node::kCategoryUnknown); + static Menu* CreateMenu(QWidget *parent, bool create_none_item = false, Node::CategoryID restrict_to = Node::kCategoryUnknown, uint64_t restrict_flags = 0); static Node* CreateFromMenuAction(QAction* action); diff --git a/app/node/filter/blur/blur.cpp b/app/node/filter/blur/blur.cpp index c488b8008..1865afbca 100644 --- a/app/node/filter/blur/blur.cpp +++ b/app/node/filter/blur/blur.cpp @@ -29,6 +29,8 @@ const QString BlurFilterNode::kHorizInput = QStringLiteral("horiz_in"); const QString BlurFilterNode::kVertInput = QStringLiteral("vert_in"); const QString BlurFilterNode::kRepeatEdgePixelsInput = QStringLiteral("repeat_edge_pixels_in"); +#define super Node + BlurFilterNode::BlurFilterNode() { AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -43,6 +45,9 @@ BlurFilterNode::BlurFilterNode() AddInput(kVertInput, NodeValue::kBoolean, true); AddInput(kRepeatEdgePixelsInput, NodeValue::kBoolean, true); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } Node *BlurFilterNode::copy() const @@ -72,6 +77,8 @@ QString BlurFilterNode::Description() const void BlurFilterNode::Retranslate() { + super::Retranslate(); + SetInputName(kTextureInput, tr("Input")); SetInputName(kMethodInput, tr("Method")); SetComboBoxStrings(kMethodInput, { tr("Box"), tr("Gaussian") }); diff --git a/app/node/filter/mosaic/mosaicfilternode.cpp b/app/node/filter/mosaic/mosaicfilternode.cpp index 242d5682f..e6f5f0545 100644 --- a/app/node/filter/mosaic/mosaicfilternode.cpp +++ b/app/node/filter/mosaic/mosaicfilternode.cpp @@ -26,6 +26,8 @@ const QString MosaicFilterNode::kTextureInput = QStringLiteral("tex_in"); const QString MosaicFilterNode::kHorizInput = QStringLiteral("horiz_in"); const QString MosaicFilterNode::kVertInput = QStringLiteral("vert_in"); +#define super Node + MosaicFilterNode::MosaicFilterNode() { AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -35,10 +37,15 @@ MosaicFilterNode::MosaicFilterNode() AddInput(kVertInput, NodeValue::kFloat, 18.0); SetInputProperty(kVertInput, QStringLiteral("min"), 1.0); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } void MosaicFilterNode::Retranslate() { + super::Retranslate(); + SetInputName(kTextureInput, tr("Texture")); SetInputName(kHorizInput, tr("Horizontal")); SetInputName(kVertInput, tr("Vertical")); diff --git a/app/node/filter/stroke/stroke.cpp b/app/node/filter/stroke/stroke.cpp index 7818d7e46..8883a9047 100644 --- a/app/node/filter/stroke/stroke.cpp +++ b/app/node/filter/stroke/stroke.cpp @@ -31,6 +31,8 @@ const QString StrokeFilterNode::kRadiusInput = QStringLiteral("radius_in"); const QString StrokeFilterNode::kOpacityInput = QStringLiteral("opacity_in"); const QString StrokeFilterNode::kInnerInput = QStringLiteral("inner_in"); +#define super Node + StrokeFilterNode::StrokeFilterNode() { AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -46,6 +48,9 @@ StrokeFilterNode::StrokeFilterNode() SetInputProperty(kOpacityInput, QStringLiteral("max"), 1.0f); AddInput(kInnerInput, NodeValue::kBoolean, false); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } Node *StrokeFilterNode::copy() const @@ -75,6 +80,8 @@ QString StrokeFilterNode::Description() const void StrokeFilterNode::Retranslate() { + super::Retranslate(); + SetInputName(kTextureInput, tr("Input")); SetInputName(kColorInput, tr("Color")); SetInputName(kRadiusInput, tr("Radius")); diff --git a/app/node/generator/matrix/matrix.cpp b/app/node/generator/matrix/matrix.cpp index 461d2ca3f..bbd5c9ba6 100644 --- a/app/node/generator/matrix/matrix.cpp +++ b/app/node/generator/matrix/matrix.cpp @@ -33,6 +33,8 @@ const QString MatrixGenerator::kScaleInput = QStringLiteral("scale_in"); const QString MatrixGenerator::kUniformScaleInput = QStringLiteral("uniform_scale_in"); const QString MatrixGenerator::kAnchorInput = QStringLiteral("anchor_in"); +#define super Node + MatrixGenerator::MatrixGenerator() { AddInput(kPositionInput, NodeValue::kVec2, QVector2D(0.0, 0.0)); @@ -81,6 +83,8 @@ QString MatrixGenerator::Description() const void MatrixGenerator::Retranslate() { + super::Retranslate(); + SetInputName(kPositionInput, tr("Position")); SetInputName(kRotationInput, tr("Rotation")); SetInputName(kScaleInput, tr("Scale")); diff --git a/app/node/generator/noise/noise.cpp b/app/node/generator/noise/noise.cpp index 21eaa215e..7ed7eb2b9 100644 --- a/app/node/generator/noise/noise.cpp +++ b/app/node/generator/noise/noise.cpp @@ -25,6 +25,8 @@ namespace olive { const QString NoiseGeneratorNode::kColorInput = QStringLiteral("color_in"); const QString NoiseGeneratorNode::kStrengthInput = QStringLiteral("strength_in"); +#define super Node + NoiseGeneratorNode::NoiseGeneratorNode() { AddInput(kStrengthInput, NodeValue::kFloat, 20); @@ -59,6 +61,8 @@ QString NoiseGeneratorNode::Description() const void NoiseGeneratorNode::Retranslate() { + super::Retranslate(); + SetInputName(kStrengthInput, tr("Strength")); SetInputName(kColorInput, tr("Color")); } diff --git a/app/node/generator/polygon/polygon.cpp b/app/node/generator/polygon/polygon.cpp index bd3ae07cf..c96d5b932 100644 --- a/app/node/generator/polygon/polygon.cpp +++ b/app/node/generator/polygon/polygon.cpp @@ -30,6 +30,8 @@ namespace olive { const QString PolygonGenerator::kPointsInput = QStringLiteral("points_in"); const QString PolygonGenerator::kColorInput = QStringLiteral("color_in"); +#define super Node + PolygonGenerator::PolygonGenerator() { AddInput(kPointsInput, NodeValue::kBezier, QVector2D(0, 0), InputFlags(kInputFlagArray)); @@ -86,6 +88,8 @@ QString PolygonGenerator::Description() const void PolygonGenerator::Retranslate() { + super::Retranslate(); + SetInputName(kPointsInput, tr("Points")); SetInputName(kColorInput, tr("Color")); } diff --git a/app/node/generator/solid/solid.cpp b/app/node/generator/solid/solid.cpp index 76b2340df..0327af098 100644 --- a/app/node/generator/solid/solid.cpp +++ b/app/node/generator/solid/solid.cpp @@ -26,6 +26,8 @@ namespace olive { const QString SolidGenerator::kColorInput = QStringLiteral("color_in"); +#define super Node + SolidGenerator::SolidGenerator() { // Default to a color that isn't black @@ -59,6 +61,8 @@ QString SolidGenerator::Description() const void SolidGenerator::Retranslate() { + super::Retranslate(); + SetInputName(kColorInput, tr("Color")); } diff --git a/app/node/generator/text/textv1.cpp b/app/node/generator/text/textv1.cpp index d6c4c8588..45dd1fad7 100644 --- a/app/node/generator/text/textv1.cpp +++ b/app/node/generator/text/textv1.cpp @@ -38,6 +38,8 @@ const QString TextGeneratorV1::kVAlignInput = QStringLiteral("valign_in"); const QString TextGeneratorV1::kFontInput = QStringLiteral("font_in"); const QString TextGeneratorV1::kFontSizeInput = QStringLiteral("font_size_in"); +#define super Node + TextGeneratorV1::TextGeneratorV1() { AddInput(kTextInput, NodeValue::kText, tr("Sample Text")); @@ -75,6 +77,8 @@ QString TextGeneratorV1::Description() const void TextGeneratorV1::Retranslate() { + super::Retranslate(); + SetInputName(kTextInput, tr("Text")); SetInputName(kHtmlInput, tr("Enable HTML")); SetInputName(kFontInput, tr("Font")); diff --git a/app/node/group/group.cpp b/app/node/group/group.cpp index 793140c8a..a3089478d 100644 --- a/app/node/group/group.cpp +++ b/app/node/group/group.cpp @@ -53,6 +53,8 @@ QString NodeGroup::Description() const void NodeGroup::Retranslate() { + super::Retranslate(); + for (auto it=GetContextPositions().cbegin(); it!=GetContextPositions().cend(); it++) { it.key()->Retranslate(); } diff --git a/app/node/input/value/valuenode.cpp b/app/node/input/value/valuenode.cpp index 215e7a08c..00b3d0c5c 100644 --- a/app/node/input/value/valuenode.cpp +++ b/app/node/input/value/valuenode.cpp @@ -48,6 +48,8 @@ ValueNode::ValueNode() void ValueNode::Retranslate() { + super::Retranslate(); + SetInputName(kTypeInput, QStringLiteral("Type")); SetInputName(kValueInput, QStringLiteral("Value")); diff --git a/app/node/keyframe.cpp b/app/node/keyframe.cpp index 30b6e2508..277a1ed61 100644 --- a/app/node/keyframe.cpp +++ b/app/node/keyframe.cpp @@ -195,4 +195,10 @@ NodeKeyframe::BezierType NodeKeyframe::get_opposing_bezier_type(NodeKeyframe::Be } } +bool NodeKeyframe::has_sibling_at_time(const rational &t) const +{ + NodeKeyframe *k = parent()->GetKeyframeAtTimeOnTrack(input(), t, track(), element()); + return k && k != this; +} + } diff --git a/app/node/keyframe.h b/app/node/keyframe.h index a142c5015..a6163039a 100644 --- a/app/node/keyframe.h +++ b/app/node/keyframe.h @@ -26,6 +26,7 @@ #include #include "common/rational.h" +#include "common/timerange.h" #include "node/param.h" namespace olive { @@ -86,6 +87,14 @@ public: const rational& time() const; void set_time(const rational& time); + /** + * @brief Dummy function for TimeBasedViewSelectionManager compatibility + * + * FIXME: Once we upgrade to C++17, we won't need this because we'll be able to check types in + * TimeBasedViewSelectionManager's template functions + */ + TimeRange time_range() const { return TimeRange(time_, time_); } + /** * @brief The value of this keyframe (i.e. the value to use at this keyframe's time) */ @@ -167,6 +176,8 @@ public: next_ = keyframe; } + bool has_sibling_at_time(const rational &t) const; + signals: /** * @brief Signal emitted when this keyframe's time is changed diff --git a/app/node/keying/colordifferencekey/colordifferencekey.cpp b/app/node/keying/colordifferencekey/colordifferencekey.cpp index 1f2ca4d1c..5770d0f55 100644 --- a/app/node/keying/colordifferencekey/colordifferencekey.cpp +++ b/app/node/keying/colordifferencekey/colordifferencekey.cpp @@ -25,7 +25,10 @@ const QString ColorDifferenceKeyNode::kShadowsInput = QStringLiteral("shadows_in const QString ColorDifferenceKeyNode::kHighlightsInput = QStringLiteral("highlights_in"); const QString ColorDifferenceKeyNode::kMaskOnlyInput = QStringLiteral("mask_only_in"); -ColorDifferenceKeyNode::ColorDifferenceKeyNode() { +#define super Node + +ColorDifferenceKeyNode::ColorDifferenceKeyNode() +{ AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); AddInput(kGarbageMatteInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -41,6 +44,9 @@ ColorDifferenceKeyNode::ColorDifferenceKeyNode() { SetInputProperty(kShadowsInput, QStringLiteral("min"), 0.0); AddInput(kMaskOnlyInput, NodeValue::kBoolean, false); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } Node *ColorDifferenceKeyNode::copy() const @@ -70,6 +76,8 @@ QString ColorDifferenceKeyNode::Description() const void ColorDifferenceKeyNode::Retranslate() { + super::Retranslate(); + SetInputName(kTextureInput, tr("Input")); SetInputName(kGarbageMatteInput, tr("Garbage Matte")); SetInputName(kCoreMatteInput, tr("Core Matte")); diff --git a/app/node/keying/despill/despill.cpp b/app/node/keying/despill/despill.cpp index 147ee60df..344b1f869 100644 --- a/app/node/keying/despill/despill.cpp +++ b/app/node/keying/despill/despill.cpp @@ -24,6 +24,8 @@ const QString DespillNode::kColorInput = QStringLiteral("color_in"); const QString DespillNode::kMethodInput = QStringLiteral("method_in"); const QString DespillNode::kPreserveLuminanceInput = QStringLiteral("preserve_luminance_input"); +#define super Node + DespillNode::DespillNode() { AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -33,6 +35,9 @@ DespillNode::DespillNode() AddInput(kMethodInput, NodeValue::kCombo, 0); AddInput(kPreserveLuminanceInput, NodeValue::kBoolean, false); + + SetFlags(kVideoEffect); + SetEffectInput(kTextureInput); } Node* DespillNode::copy() const @@ -62,12 +67,13 @@ QString DespillNode::Description() const void DespillNode::Retranslate() { + super::Retranslate(); + SetInputName(kTextureInput, tr("Input")); SetInputName(kColorInput, tr("Key Color")); SetComboBoxStrings(kColorInput, {tr("Green"), tr("Blue")}); - SetInputName(kMethodInput, tr("Method")); SetComboBoxStrings(kMethodInput, {tr("Average"), tr("Double Red Average"), tr("Double Average"), tr("Limit")}); diff --git a/app/node/math/math/math.cpp b/app/node/math/math/math.cpp index d601c2a8d..932a963d1 100644 --- a/app/node/math/math/math.cpp +++ b/app/node/math/math/math.cpp @@ -27,6 +27,8 @@ const QString MathNode::kParamAIn = QStringLiteral("param_a_in"); const QString MathNode::kParamBIn = QStringLiteral("param_b_in"); const QString MathNode::kParamCIn = QStringLiteral("param_c_in"); +#define super MathNodeBase + MathNode::MathNode() { AddInput(kMethodIn, NodeValue::kCombo, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); @@ -67,7 +69,7 @@ QString MathNode::Description() const void MathNode::Retranslate() { - Node::Retranslate(); + super::Retranslate(); SetInputName(kMethodIn, tr("Method")); SetInputName(kParamAIn, tr("Value")); diff --git a/app/node/math/merge/merge.cpp b/app/node/math/merge/merge.cpp index 5b1582c84..5940358aa 100644 --- a/app/node/math/merge/merge.cpp +++ b/app/node/math/merge/merge.cpp @@ -27,6 +27,8 @@ namespace olive { const QString MergeNode::kBaseIn = QStringLiteral("base_in"); const QString MergeNode::kBlendIn = QStringLiteral("blend_in"); +#define super Node + MergeNode::MergeNode() { AddInput(kBaseIn, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -63,6 +65,8 @@ QString MergeNode::Description() const void MergeNode::Retranslate() { + super::Retranslate(); + SetInputName(kBaseIn, tr("Base")); SetInputName(kBlendIn, tr("Blend")); diff --git a/app/node/math/trigonometry/trigonometry.cpp b/app/node/math/trigonometry/trigonometry.cpp index 3899cc6e4..5c6e16663 100644 --- a/app/node/math/trigonometry/trigonometry.cpp +++ b/app/node/math/trigonometry/trigonometry.cpp @@ -25,6 +25,8 @@ namespace olive { const QString TrigonometryNode::kMethodIn = QStringLiteral("method_in"); const QString TrigonometryNode::kXIn = QStringLiteral("x_in"); +#define super Node + TrigonometryNode::TrigonometryNode() { AddInput(kMethodIn, NodeValue::kCombo, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); @@ -59,6 +61,8 @@ QString TrigonometryNode::Description() const void TrigonometryNode::Retranslate() { + super::Retranslate(); + QStringList strings = {tr("Sine"), tr("Cosine"), tr("Tangent"), diff --git a/app/node/node.cpp b/app/node/node.cpp index c8afb1f12..4a0c60b03 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -42,14 +42,18 @@ namespace olive { #define super QObject +const QString Node::kEnabledInput = QStringLiteral("enabled_in"); + Node::Node() : can_be_deleted_(true), override_color_(-1), folder_(nullptr), operation_stack_(0), cache_result_(false), - flags_(kNone) + flags_(kNone), + effect_element_(-1) { + AddInput(kEnabledInput, NodeValue::kBoolean, true); } Node::~Node() @@ -76,18 +80,9 @@ NodeGraph *Node::parent() const return static_cast(QObject::parent()); } -Project* Node::project() const +Project *Node::project() const { - QObject *t = this->parent(); - - while (t) { - if (Project *p = dynamic_cast(t)) { - return p; - } - t = t->parent(); - } - - return nullptr; + return Project::GetProjectFromObject(this); } QString Node::ShortName() const @@ -103,6 +98,7 @@ QString Node::Description() const void Node::Retranslate() { + SetInputName(kEnabledInput, tr("Enabled")); } QIcon Node::icon() const @@ -1997,6 +1993,50 @@ void Node::SetValueAtTime(const NodeInput &input, const rational &time, const QV } } +void FindPathInternal(std::list &vec, Node *to, int &path_index) +{ + Node *from = vec.back(); + + for (auto it=from->input_connections().cbegin(); it!=from->input_connections().cend(); it++) { + vec.push_back(it->second); + if (it->second == to) { + // Found a path, determine if it's the one we want + if (path_index == 0) { + // It is! + break; + } else { + path_index--; + } + } + + // Recurse to see if we can find it here + FindPathInternal(vec, to, path_index); + if (vec.back() == to) { + // Found through recursion + break; + } else { + // Must not be available through this path + vec.pop_back(); + } + } +} + +std::list Node::FindPath(Node *from, Node *to, int path_index) +{ + std::list v; + + v.push_back(from); + + FindPathInternal(v, to, path_index); + + if (v.size() == 1) { + // Failed to find path, return empty list + v.pop_back(); + } + + return v; +} + Project *Node::ArrayInsertCommand::GetRelevantProject() const { return node_->project(); diff --git a/app/node/node.h b/app/node/node.h index 6d2c27144..98fcd8e27 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -95,7 +95,9 @@ public: enum Flag { kNone = 0, - kDontShowInParamView = 0x1 + kDontShowInParamView = 0x1, + kVideoEffect = 0x2, + kAudioEffect = 0x4 }; Node(); @@ -539,6 +541,11 @@ public: int InputArraySize(const QString& id) const; + NodeInput GetEffectInput() + { + return effect_input_.isEmpty() ? NodeInput() : NodeInput(this, effect_input_, effect_element_); + } + class ValueHint { public: explicit ValueHint(const QVector &types = QVector(), int index = -1, const QString &tag = QString()) : @@ -949,6 +956,10 @@ public: static void SetValueAtTime(const NodeInput &input, const rational &time, const QVariant &value, int track, MultiUndoCommand *command, bool insert_on_all_tracks_if_no_key); + static std::list FindPath(Node *from, Node *to, int path_index = 0); + + static const QString kEnabledInput; + protected: virtual void Hash(QCryptographicHash& hash, const NodeGlobals &globals, const VideoParams& video_params) const; @@ -1027,6 +1038,12 @@ protected: virtual void childEvent(QChildEvent *event) override; + void SetEffectInput(const QString &input, int element = -1) + { + effect_input_ = input; + effect_element_ = element; + } + void SetToolTip(const QString& s) { tooltip_ = s; @@ -1342,6 +1359,9 @@ private: QVector gizmos_; + QString effect_input_; + int effect_element_; + private slots: /** * @brief Slot when a keyframe's time changes to keep the keyframes correctly sorted by time diff --git a/app/node/nodecopypaste.cpp b/app/node/nodecopypaste.cpp deleted file mode 100644 index 9b4aec472..000000000 --- a/app/node/nodecopypaste.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2021 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 "nodecopypaste.h" - -#include - -#include "core.h" -#include "node/factory.h" -#include "widget/nodeview/nodeviewundo.h" -#include "window/mainwindow/mainwindow.h" - -namespace olive { - -void NodeCopyPasteService::CopyNodesToClipboard(QVector nodes, void *userdata) -{ - QString copy_str; - - QXmlStreamWriter writer(©_str); - - // For any groups, add children - for (int i=0; i(nodes.at(i))) { - for (auto it=g->GetContextPositions().cbegin(); it!=g->GetContextPositions().cend(); it++) { - if (!nodes.contains(it.key())) { - nodes.append(it.key()); - } - } - } - } - - ProjectSerializer::SaveData data(nodes.first()->project(), QString(), nodes); - - CopyNodesToClipboardCallback(nodes, &data, userdata); - - ProjectSerializer::Save(&writer, data); - - Core::CopyStringToClipboard(copy_str); -} - -void NodeCopyPasteService::PasteNodesFromClipboard(void *userdata) -{ - QString clipboard = Core::PasteStringFromClipboard(); - if (clipboard.isEmpty()) { - return; - } - - QXmlStreamReader reader(clipboard); - - Project temp; - ProjectSerializer::Result res = ProjectSerializer::Load(&temp, &reader); - - if (res.code() != ProjectSerializer::kSuccess) { - return; - } - - QVector pasted_nodes; - foreach (Node *n, temp.nodes()) { - if (!temp.default_nodes().contains(n)) { - // Move nodes out of Project - n->setParent(nullptr); - pasted_nodes.append(n); - } - } - - if (pasted_nodes.isEmpty()) { - return; - } - - PasteNodesToClipboardCallback(pasted_nodes, res.GetLoadData(), userdata); -} - -} diff --git a/app/node/nodecopypaste.h b/app/node/nodecopypaste.h deleted file mode 100644 index 28c53bed9..000000000 --- a/app/node/nodecopypaste.h +++ /dev/null @@ -1,52 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2021 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 NODECOPYPASTEWIDGET_H -#define NODECOPYPASTEWIDGET_H - -#include -#include - -#include "node/node.h" -#include "node/project/project.h" -#include "node/project/sequence/sequence.h" -#include "node/project/serializer/serializer.h" - -namespace olive { - -class NodeCopyPasteService -{ -public: - NodeCopyPasteService() = default; - -protected: - void CopyNodesToClipboard(QVector nodes, void* userdata = nullptr); - - void PasteNodesFromClipboard(void* userdata = nullptr); - - virtual void CopyNodesToClipboardCallback(const QVector &nodes, ProjectSerializer::SaveData *data, void *userdata){} - - virtual void PasteNodesToClipboardCallback(const QVector &nodes, const ProjectSerializer::LoadData &load_data, void *userdata){} - -}; - -} - -#endif // NODECOPYPASTEWIDGET_H diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 6325285d0..f4244804b 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -277,7 +277,7 @@ void Track::InputValueChangedEvent(const QString &input, int element) void Track::Retranslate() { - Node::Retranslate(); + super::Retranslate(); SetInputName(kBlockInput, tr("Blocks")); SetInputName(kMutedInput, tr("Muted")); diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 9944a0e49..ee458d7d5 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -68,6 +68,8 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_stream } SetFlags(kDontShowInParamView); + + timeline_points_ = new TimelinePoints(this); } Node *ViewerOutput::copy() const diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index c16b86160..ee207c6ec 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -137,7 +137,7 @@ public: TimelinePoints* GetTimelinePoints() { - return &timeline_points_; + return timeline_points_; } QVector GetEnabledStreamsAsReferences() const; @@ -252,7 +252,7 @@ private: AudioParams cached_audio_params_; - TimelinePoints timeline_points_; + TimelinePoints *timeline_points_; bool video_cache_enabled_; bool audio_cache_enabled_; diff --git a/app/node/project/project.cpp b/app/node/project/project.cpp index d8923c9cd..c4340b177 100644 --- a/app/node/project/project.cpp +++ b/app/node/project/project.cpp @@ -169,6 +169,20 @@ void Project::RegenerateUuid() uuid_ = QUuid::createUuid(); } +Project *Project::GetProjectFromObject(const QObject *o) +{ + QObject *t = o->parent(); + + while (t) { + if (Project *p = dynamic_cast(t)) { + return p; + } + t = t->parent(); + } + + return nullptr; +} + void Project::ColorManagerValueChanged(const NodeInput &input, const TimeRange &range) { Q_UNUSED(input) diff --git a/app/node/project/project.h b/app/node/project/project.h index 2fccff6d3..1119e26fc 100644 --- a/app/node/project/project.h +++ b/app/node/project/project.h @@ -109,6 +109,14 @@ public: saved_url_ = url; } + /** + * @brief Find project parent from object + * + * If an object is expected to be a child of a project, this function will traverse its parent + * tree until it finds it. + */ + static Project *GetProjectFromObject(const QObject *o); + signals: void NameChanged(); diff --git a/app/node/project/projectsettings/projectsettings.cpp b/app/node/project/projectsettings/projectsettings.cpp index 2ed08b05b..59b3bedc0 100644 --- a/app/node/project/projectsettings/projectsettings.cpp +++ b/app/node/project/projectsettings/projectsettings.cpp @@ -25,6 +25,8 @@ namespace olive { const QString ProjectSettingsNode::kCacheSetting = QStringLiteral("cache_setting"); const QString ProjectSettingsNode::kCachePath = QStringLiteral("cache_path"); +#define super Node + ProjectSettingsNode::ProjectSettingsNode() { AddInput(kCacheSetting, NodeValue::kCombo, 0, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); @@ -36,6 +38,8 @@ ProjectSettingsNode::ProjectSettingsNode() void ProjectSettingsNode::Retranslate() { + super::Retranslate(); + SetInputName(kCacheSetting, tr("Disk Cache Location")); SetInputName(kCachePath, tr("Disk Cache Path")); SetComboBoxStrings(kCacheSetting, {tr("Use Default Location"), tr("Store Alongside Project"), tr("Use Custom Location")}); diff --git a/app/node/project/projectviewmodel.cpp b/app/node/project/projectviewmodel.cpp index 1e0c2c30b..e4af49620 100644 --- a/app/node/project/projectviewmodel.cpp +++ b/app/node/project/projectviewmodel.cpp @@ -405,7 +405,7 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action } // Trigger an import - Core::instance()->ImportFiles(urls, this, static_cast(drop_item)); + Core::instance()->ImportFiles(urls, static_cast(drop_item)); return true; } diff --git a/app/node/project/serializer/serializer.cpp b/app/node/project/serializer/serializer.cpp index 91164db0e..67bd879f7 100644 --- a/app/node/project/serializer/serializer.cpp +++ b/app/node/project/serializer/serializer.cpp @@ -55,14 +55,14 @@ void ProjectSerializer::Destroy() instances_.clear(); } -ProjectSerializer::Result ProjectSerializer::Load(Project *project, const QString &filename) +ProjectSerializer::Result ProjectSerializer::Load(Project *project, const QString &filename, const QString &type) { QFile project_file(filename); if (project_file.open(QFile::ReadOnly | QFile::Text)) { QXmlStreamReader reader(&project_file); - Result inner_result = Load(project, &reader); + Result inner_result = Load(project, &reader, type); project_file.close(); @@ -82,25 +82,162 @@ ProjectSerializer::Result ProjectSerializer::Load(Project *project, const QStrin } } -ProjectSerializer::Result ProjectSerializer::Load(Project *project, QXmlStreamReader *reader) +ProjectSerializer::Result ProjectSerializer::Load(Project *project, QXmlStreamReader *reader, const QString &type) { // Determine project version uint version = 0; + Result res = kUnknownVersion; - while (!version && XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("olive") || reader->name() == QStringLiteral("project")) { - while(!version && XMLReadNextStartElement(reader)) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("olive") + || reader->name() == QStringLiteral("project")) { // 0.1 projects only + + while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("version")) { version = reader->readElementText().toUInt(); + } else if (reader->name() == QStringLiteral("url")) { + project->SetSavedURL(reader->readElementText()); + + // HACK for 0.1 projects + if (version == 190219) { + res = LoadWithSerializerVersion(version, project, reader); + } + } else if (reader->name() == type) { + // Found our data + res = LoadWithSerializerVersion(version, project, reader); } else { reader->skipCurrentElement(); } } + } else { reader->skipCurrentElement(); } } + return res; +} + +ProjectSerializer::Result ProjectSerializer::Paste(const QString &type) +{ + QString clipboard = Core::PasteStringFromClipboard(); + if (clipboard.isEmpty()) { + return kNoData; + } + + QXmlStreamReader reader(clipboard); + + Project temp; + ProjectSerializer::Result res = ProjectSerializer::Load(&temp, &reader, type); + + if (res.code() != ProjectSerializer::kSuccess) { + return res; + } + + QVector pasted_nodes; + foreach (Node *n, temp.nodes()) { + if (!temp.default_nodes().contains(n)) { + // Move nodes out of Project + n->setParent(nullptr); + pasted_nodes.append(n); + } + } + + res.SetLoadedNodes(pasted_nodes); + + return res; +} + +ProjectSerializer::Result ProjectSerializer::Save(const SaveData &data, const QString &type) +{ + QString temp_save = FileFunctions::GetSafeTemporaryFilename(data.GetFilename()); + + QFile project_file(temp_save); + + if (project_file.open(QFile::WriteOnly | QFile::Text)) { + QXmlStreamWriter writer(&project_file); + + Result inner_result = Save(&writer, data, type); + + project_file.close(); + + if (inner_result != kSuccess) { + return inner_result; + } + + // Save was successful, we can now rewrite the original file + if (FileFunctions::RenameFileAllowOverwrite(temp_save, data.GetFilename())) { + return kSuccess; + } else { + Result r(kOverwriteError); + r.SetDetails(temp_save); + return r; + } + } else { + Result r(kFileError); + r.SetDetails(temp_save); + return r; + } +} + +ProjectSerializer::Result ProjectSerializer::Save(QXmlStreamWriter *writer, const SaveData &data, const QString &type) +{ + writer->setAutoFormatting(true); + + writer->writeStartDocument(); + + writer->writeStartElement("olive"); + + // By default, save as last serializer which, assuming the instances are ordered correctly, + // will be the newest file format. But we may allow saving as older versions later on. + ProjectSerializer *serializer = instances_.last(); + + // Version is stored in YYMMDD from whenever the project format was last changed + // Allows easy integer math for checking project versions. + writer->writeTextElement(QStringLiteral("version"), QString::number(serializer->Version())); + + if (!data.GetFilename().isEmpty()) { + writer->writeTextElement("url", data.GetFilename()); + } + + writer->writeStartElement(type); + + serializer->Save(writer, data, nullptr); + + writer->writeEndElement(); // [type] + + writer->writeEndElement(); // olive + + writer->writeEndDocument(); + + if (writer->hasError()) { + return kXmlError; + } + + return kSuccess; +} + +ProjectSerializer::Result ProjectSerializer::Copy(const SaveData &data, const QString &type) +{ + QString copy_str; + QXmlStreamWriter writer(©_str); + + ProjectSerializer::Result res = ProjectSerializer::Save(&writer, data, type); + + if (res == kSuccess) { + Core::CopyStringToClipboard(copy_str); + } + + return res; +} + +bool ProjectSerializer::IsCancelled() const +{ + return false; +} + +ProjectSerializer::Result ProjectSerializer::LoadWithSerializerVersion(uint version, Project *project, QXmlStreamReader *reader) +{ // Failed to find version in file if (version == 0) { return kUnknownVersion; @@ -135,78 +272,21 @@ ProjectSerializer::Result ProjectSerializer::Load(Project *project, QXmlStreamRe } } -ProjectSerializer::Result ProjectSerializer::Save(const SaveData &data) +void ProjectSerializer::SaveData::SetOnlySerializeNodesAndResolveGroups(QVector nodes) { - QString temp_save = FileFunctions::GetSafeTemporaryFilename(data.GetFilename()); - - QFile project_file(temp_save); - - if (project_file.open(QFile::WriteOnly | QFile::Text)) { - QXmlStreamWriter writer(&project_file); - - Result inner_result = Save(&writer, data); - - project_file.close(); - - if (inner_result != kSuccess) { - return inner_result; + // For any groups, add children + for (int i=0; i(nodes.at(i))) { + for (auto it=g->GetContextPositions().cbegin(); it!=g->GetContextPositions().cend(); it++) { + if (!nodes.contains(it.key())) { + nodes.append(it.key()); + } + } } - - // Save was successful, we can now rewrite the original file - if (FileFunctions::RenameFileAllowOverwrite(temp_save, data.GetFilename())) { - return kSuccess; - } else { - Result r(kOverwriteError); - r.SetDetails(temp_save); - return r; - } - } else { - Result r(kFileError); - r.SetDetails(temp_save); - return r; - } -} - -ProjectSerializer::Result ProjectSerializer::Save(QXmlStreamWriter *writer, const SaveData &data) -{ - writer->setAutoFormatting(true); - - writer->writeStartDocument(); - - writer->writeStartElement("olive"); - - // By default, save as last serializer which, assuming the instances are ordered correctly, - // will be the newest file format. But we may allow saving as older versions later on. - ProjectSerializer *serializer = instances_.last(); - - // Version is stored in YYMMDD from whenever the project format was last changed - // Allows easy integer math for checking project versions. - writer->writeTextElement(QStringLiteral("version"), QString::number(serializer->Version())); - - if (!data.GetFilename().isEmpty()) { - writer->writeTextElement("url", data.GetFilename()); } - writer->writeStartElement(QStringLiteral("project")); - - serializer->Save(writer, data, nullptr); - - writer->writeEndElement(); // project - - writer->writeEndElement(); // olive - - writer->writeEndDocument(); - - if (writer->hasError()) { - return kXmlError; - } - - return kSuccess; -} - -bool ProjectSerializer::IsCancelled() const -{ - return false; + SetOnlySerializeNodes(nodes); } } diff --git a/app/node/project/serializer/serializer.h b/app/node/project/serializer/serializer.h index 0417f440c..cd6b99082 100644 --- a/app/node/project/serializer/serializer.h +++ b/app/node/project/serializer/serializer.h @@ -50,7 +50,8 @@ public: kUnknownVersion, kFileError, kXmlError, - kOverwriteError + kOverwriteError, + kNoData }; using SerializedProperties = QHash >; @@ -62,6 +63,8 @@ public: SerializedProperties properties; + std::vector markers; + }; class Result @@ -84,6 +87,10 @@ public: void SetLoadData(const LoadData &p) { load_data_ = p; } + const QVector &GetLoadedNodes() const { return loaded_nodes_; } + + void SetLoadedNodes(const QVector &n) { loaded_nodes_ = n; } + private: ResultCode code_; @@ -91,17 +98,17 @@ public: LoadData load_data_; + QVector loaded_nodes_; + }; class SaveData { public: - SaveData(Project *project, const QString &filename, const QVector &only = QVector(), const SerializedProperties &p = SerializedProperties()) + SaveData(Project *project, const QString &filename = QString()) { project_ = project; filename_ = filename; - only_serialize_nodes_ = only; - properties_ = p; } Project *GetProject() const @@ -115,11 +122,13 @@ public: } const QVector &GetOnlySerializeNodes() const { return only_serialize_nodes_; } - void SetOnlySerializeNodes(const QVector &only) { only_serialize_nodes_ = only; } + void SetOnlySerializeNodesAndResolveGroups(QVector only); + + const std::vector &GetOnlySerializeMarkers() const { return only_serialize_markers_; } + void SetOnlySerializeMarkers(const std::vector &only) { only_serialize_markers_ = only; } const SerializedProperties &GetProperties() const { return properties_; } - void SetProperties(const SerializedProperties &p) { properties_ = p; } private: @@ -131,17 +140,21 @@ public: SerializedProperties properties_; + std::vector only_serialize_markers_; + }; static void Initialize(); static void Destroy(); - static Result Load(Project *project, const QString &filename); - static Result Load(Project *project, QXmlStreamReader *read_device); + static Result Load(Project *project, const QString &filename, const QString &type); + static Result Load(Project *project, QXmlStreamReader *read_device, const QString &type); + static Result Paste(const QString &type); - static Result Save(const SaveData &data); - static Result Save(QXmlStreamWriter *write_device, const SaveData &data); + static Result Save(const SaveData &data, const QString &type); + static Result Save(QXmlStreamWriter *write_device, const SaveData &data, const QString &type); + static Result Copy(const SaveData &data, const QString &type); protected: virtual LoadData Load(Project *project, QXmlStreamReader *reader, void *reserved) const = 0; @@ -153,6 +166,8 @@ protected: bool IsCancelled() const; private: + static Result LoadWithSerializerVersion(uint version, Project *project, QXmlStreamReader *reader); + static QVector instances_; }; diff --git a/app/node/project/serializer/serializer210528.cpp b/app/node/project/serializer/serializer210528.cpp index 6c9ec058f..baf53e442 100644 --- a/app/node/project/serializer/serializer210528.cpp +++ b/app/node/project/serializer/serializer210528.cpp @@ -20,6 +20,7 @@ #include "serializer210528.h" +#include "config/config.h" #include "node/factory.h" namespace olive { @@ -29,133 +30,125 @@ ProjectSerializer210528::LoadData ProjectSerializer210528::Load(Project *project XMLNodeData xml_node_data; while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("url")) { - project->SetSavedURL(reader->readElementText()); - } else if (reader->name() == QStringLiteral("project")) { + if (reader->name() == QStringLiteral("layout")) { + + // Since the main window's functions have to occur in the GUI thread (and we're likely + // loading in a secondary thread), we load all necessary data into a separate struct so we + // can continue loading and queue it with the main window so it can handle the data + // appropriately in its own thread. + + project->SetLayoutInfo(MainWindowLayoutInfo::fromXml(reader, xml_node_data.node_ptrs)); + + } else if (reader->name() == QStringLiteral("uuid")) { + + project->SetUuid(QUuid::fromString(reader->readElementText())); + + } else if (reader->name() == QStringLiteral("nodes")) { + while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("layout")) { + if (reader->name() == QStringLiteral("node")) { + bool is_root = false; + bool is_cm = false; + bool is_settings = false; + QString id; - // Since the main window's functions have to occur in the GUI thread (and we're likely - // loading in a secondary thread), we load all necessary data into a separate struct so we - // can continue loading and queue it with the main window so it can handle the data - // appropriately in its own thread. - - project->SetLayoutInfo(MainWindowLayoutInfo::fromXml(reader, xml_node_data.node_ptrs)); - - } else if (reader->name() == QStringLiteral("uuid")) { - - project->SetUuid(QUuid::fromString(reader->readElementText())); - - } else if (reader->name() == QStringLiteral("nodes")) { - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - bool is_root = false; - bool is_cm = false; - bool is_settings = false; - QString id; - - { - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("id")) { - id = attr.value().toString(); - } else if (attr.name() == QStringLiteral("root") && attr.value() == QStringLiteral("1")) { - is_root = true; - } else if (attr.name() == QStringLiteral("cm") && attr.value() == QStringLiteral("1")) { - is_cm = true; - } else if (attr.name() == QStringLiteral("settings") && attr.value() == QStringLiteral("1")) { - is_settings = true; - } - } + { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("id")) { + id = attr.value().toString(); + } else if (attr.name() == QStringLiteral("root") && attr.value() == QStringLiteral("1")) { + is_root = true; + } else if (attr.name() == QStringLiteral("cm") && attr.value() == QStringLiteral("1")) { + is_cm = true; + } else if (attr.name() == QStringLiteral("settings") && attr.value() == QStringLiteral("1")) { + is_settings = true; } - - if (id.isEmpty()) { - qWarning() << "Failed to load node with empty ID"; - reader->skipCurrentElement(); - } else { - Node* node; - - if (is_root) { - node = project->root(); - } else if (is_cm) { - node = project->color_manager(); - } else if (is_settings) { - node = project->settings(); - } else { - node = NodeFactory::CreateFromID(id); - } - - if (!node) { - qWarning() << "Failed to find node with ID" << id; - reader->skipCurrentElement(); - } else { - LoadNode(node, xml_node_data, reader); - node->setParent(project); - } - } - } else { - reader->skipCurrentElement(); } } - } else if (reader->name() == QStringLiteral("positions")) { + if (id.isEmpty()) { + qWarning() << "Failed to load node with empty ID"; + reader->skipCurrentElement(); + } else { + Node* node; - while (XMLReadNextStartElement(reader)) { + if (is_root) { + node = project->root(); + } else if (is_cm) { + node = project->color_manager(); + } else if (is_settings) { + node = project->settings(); + } else { + node = NodeFactory::CreateFromID(id); + } - if (reader->name() == QStringLiteral("context")) { + if (!node) { + qWarning() << "Failed to find node with ID" << id; + reader->skipCurrentElement(); + } else { + LoadNode(node, xml_node_data, reader); + node->setParent(project); + } + } + } else { + reader->skipCurrentElement(); + } + } - quintptr context_ptr = 0; - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("ptr")) { - context_ptr = attr.value().toULongLong(); - break; - } - } + } else if (reader->name() == QStringLiteral("positions")) { - Node *context = xml_node_data.node_ptrs.value(context_ptr); + while (XMLReadNextStartElement(reader)) { - if (!context) { - qWarning() << "Failed to find pointer for context"; - reader->skipCurrentElement(); - } else { - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - quintptr node_ptr; - Node::Position node_pos; + if (reader->name() == QStringLiteral("context")) { - if (LoadPosition(reader, &node_ptr, &node_pos)) { - Node *node = xml_node_data.node_ptrs.value(node_ptr); + quintptr context_ptr = 0; + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + context_ptr = attr.value().toULongLong(); + break; + } + } - if (node) { - context->SetNodePositionInContext(node, node_pos); - } else { - qWarning() << "Failed to find pointer for node position"; - reader->skipCurrentElement(); - } - } + Node *context = xml_node_data.node_ptrs.value(context_ptr); + + if (!context) { + qWarning() << "Failed to find pointer for context"; + reader->skipCurrentElement(); + } else { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + quintptr node_ptr; + Node::Position node_pos; + + if (LoadPosition(reader, &node_ptr, &node_pos)) { + Node *node = xml_node_data.node_ptrs.value(node_ptr); + + if (node) { + context->SetNodePositionInContext(node, node_pos); } else { + qWarning() << "Failed to find pointer for node position"; reader->skipCurrentElement(); } } + } else { + reader->skipCurrentElement(); } - - } else { - - reader->skipCurrentElement(); - } - } } else { - // Skip this reader->skipCurrentElement(); } + } + } else { + + // Skip this reader->skipCurrentElement(); + } } @@ -612,7 +605,7 @@ void ProjectSerializer210528::LoadMarkerList(QXmlStreamReader *reader, TimelineM } } - markers->AddMarker(TimeRange(in, out), name); + new TimelineMarker(Config::Current()[QStringLiteral("MarkerColor")].toInt(), TimeRange(in, out), name, markers); } reader->skipCurrentElement(); diff --git a/app/node/project/serializer/serializer210907.cpp b/app/node/project/serializer/serializer210907.cpp index 870fa0b30..96b69f214 100644 --- a/app/node/project/serializer/serializer210907.cpp +++ b/app/node/project/serializer/serializer210907.cpp @@ -20,6 +20,7 @@ #include "serializer210907.h" +#include "config/config.h" #include "node/factory.h" namespace olive { @@ -29,133 +30,125 @@ ProjectSerializer210907::LoadData ProjectSerializer210907::Load(Project *project XMLNodeData xml_node_data; while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("url")) { - project->SetSavedURL(reader->readElementText()); - } else if (reader->name() == QStringLiteral("project")) { + if (reader->name() == QStringLiteral("layout")) { + + // Since the main window's functions have to occur in the GUI thread (and we're likely + // loading in a secondary thread), we load all necessary data into a separate struct so we + // can continue loading and queue it with the main window so it can handle the data + // appropriately in its own thread. + + project->SetLayoutInfo(MainWindowLayoutInfo::fromXml(reader, xml_node_data.node_ptrs)); + + } else if (reader->name() == QStringLiteral("uuid")) { + + project->SetUuid(QUuid::fromString(reader->readElementText())); + + } else if (reader->name() == QStringLiteral("nodes")) { + while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("layout")) { + if (reader->name() == QStringLiteral("node")) { + bool is_root = false; + bool is_cm = false; + bool is_settings = false; + QString id; - // Since the main window's functions have to occur in the GUI thread (and we're likely - // loading in a secondary thread), we load all necessary data into a separate struct so we - // can continue loading and queue it with the main window so it can handle the data - // appropriately in its own thread. - - project->SetLayoutInfo(MainWindowLayoutInfo::fromXml(reader, xml_node_data.node_ptrs)); - - } else if (reader->name() == QStringLiteral("uuid")) { - - project->SetUuid(QUuid::fromString(reader->readElementText())); - - } else if (reader->name() == QStringLiteral("nodes")) { - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - bool is_root = false; - bool is_cm = false; - bool is_settings = false; - QString id; - - { - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("id")) { - id = attr.value().toString(); - } else if (attr.name() == QStringLiteral("root") && attr.value() == QStringLiteral("1")) { - is_root = true; - } else if (attr.name() == QStringLiteral("cm") && attr.value() == QStringLiteral("1")) { - is_cm = true; - } else if (attr.name() == QStringLiteral("settings") && attr.value() == QStringLiteral("1")) { - is_settings = true; - } - } + { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("id")) { + id = attr.value().toString(); + } else if (attr.name() == QStringLiteral("root") && attr.value() == QStringLiteral("1")) { + is_root = true; + } else if (attr.name() == QStringLiteral("cm") && attr.value() == QStringLiteral("1")) { + is_cm = true; + } else if (attr.name() == QStringLiteral("settings") && attr.value() == QStringLiteral("1")) { + is_settings = true; } - - if (id.isEmpty()) { - qWarning() << "Failed to load node with empty ID"; - reader->skipCurrentElement(); - } else { - Node* node; - - if (is_root) { - node = project->root(); - } else if (is_cm) { - node = project->color_manager(); - } else if (is_settings) { - node = project->settings(); - } else { - node = NodeFactory::CreateFromID(id); - } - - if (!node) { - qWarning() << "Failed to find node with ID" << id; - reader->skipCurrentElement(); - } else { - LoadNode(node, xml_node_data, reader); - node->setParent(project); - } - } - } else { - reader->skipCurrentElement(); } } - } else if (reader->name() == QStringLiteral("positions")) { + if (id.isEmpty()) { + qWarning() << "Failed to load node with empty ID"; + reader->skipCurrentElement(); + } else { + Node* node; - while (XMLReadNextStartElement(reader)) { + if (is_root) { + node = project->root(); + } else if (is_cm) { + node = project->color_manager(); + } else if (is_settings) { + node = project->settings(); + } else { + node = NodeFactory::CreateFromID(id); + } - if (reader->name() == QStringLiteral("context")) { + if (!node) { + qWarning() << "Failed to find node with ID" << id; + reader->skipCurrentElement(); + } else { + LoadNode(node, xml_node_data, reader); + node->setParent(project); + } + } + } else { + reader->skipCurrentElement(); + } + } - quintptr context_ptr = 0; - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("ptr")) { - context_ptr = attr.value().toULongLong(); - break; - } - } + } else if (reader->name() == QStringLiteral("positions")) { - Node *context = xml_node_data.node_ptrs.value(context_ptr); + while (XMLReadNextStartElement(reader)) { - if (!context) { - qWarning() << "Failed to find pointer for context"; - reader->skipCurrentElement(); - } else { - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - quintptr node_ptr; - Node::Position node_pos; + if (reader->name() == QStringLiteral("context")) { - if (LoadPosition(reader, &node_ptr, &node_pos)) { - Node *node = xml_node_data.node_ptrs.value(node_ptr); + quintptr context_ptr = 0; + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + context_ptr = attr.value().toULongLong(); + break; + } + } - if (node) { - context->SetNodePositionInContext(node, node_pos); - } else { - qWarning() << "Failed to find pointer for node position"; - reader->skipCurrentElement(); - } - } + Node *context = xml_node_data.node_ptrs.value(context_ptr); + + if (!context) { + qWarning() << "Failed to find pointer for context"; + reader->skipCurrentElement(); + } else { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + quintptr node_ptr; + Node::Position node_pos; + + if (LoadPosition(reader, &node_ptr, &node_pos)) { + Node *node = xml_node_data.node_ptrs.value(node_ptr); + + if (node) { + context->SetNodePositionInContext(node, node_pos); } else { + qWarning() << "Failed to find pointer for node position"; reader->skipCurrentElement(); } } + } else { + reader->skipCurrentElement(); } - - } else { - - reader->skipCurrentElement(); - } - } } else { - // Skip this reader->skipCurrentElement(); } + } + } else { + + // Skip this reader->skipCurrentElement(); + } } @@ -604,7 +597,7 @@ void ProjectSerializer210907::LoadMarkerList(QXmlStreamReader *reader, TimelineM } } - markers->AddMarker(TimeRange(in, out), name); + new TimelineMarker(Config::Current()[QStringLiteral("MarkerColor")].toInt(), TimeRange(in, out), name, markers); } reader->skipCurrentElement(); diff --git a/app/node/project/serializer/serializer211228.cpp b/app/node/project/serializer/serializer211228.cpp index fb7c507d3..139fd2438 100644 --- a/app/node/project/serializer/serializer211228.cpp +++ b/app/node/project/serializer/serializer211228.cpp @@ -20,6 +20,7 @@ #include "serializer211228.h" +#include "config/config.h" #include "node/factory.h" namespace olive { @@ -31,157 +32,149 @@ ProjectSerializer211228::LoadData ProjectSerializer211228::Load(Project *project XMLNodeData xml_node_data; while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("url")) { - project->SetSavedURL(reader->readElementText()); - } else if (reader->name() == QStringLiteral("project")) { + if (reader->name() == QStringLiteral("layout")) { + + // Since the main window's functions have to occur in the GUI thread (and we're likely + // loading in a secondary thread), we load all necessary data into a separate struct so we + // can continue loading and queue it with the main window so it can handle the data + // appropriately in its own thread. + + project->SetLayoutInfo(MainWindowLayoutInfo::fromXml(reader, xml_node_data.node_ptrs)); + + } else if (reader->name() == QStringLiteral("uuid")) { + + project->SetUuid(QUuid::fromString(reader->readElementText())); + + } else if (reader->name() == QStringLiteral("nodes")) { + while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("layout")) { + if (reader->name() == QStringLiteral("node")) { + bool is_root = false; + bool is_cm = false; + bool is_settings = false; + QString id; - // Since the main window's functions have to occur in the GUI thread (and we're likely - // loading in a secondary thread), we load all necessary data into a separate struct so we - // can continue loading and queue it with the main window so it can handle the data - // appropriately in its own thread. - - project->SetLayoutInfo(MainWindowLayoutInfo::fromXml(reader, xml_node_data.node_ptrs)); - - } else if (reader->name() == QStringLiteral("uuid")) { - - project->SetUuid(QUuid::fromString(reader->readElementText())); - - } else if (reader->name() == QStringLiteral("nodes")) { - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - bool is_root = false; - bool is_cm = false; - bool is_settings = false; - QString id; - - { - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("id")) { - id = attr.value().toString(); - } else if (attr.name() == QStringLiteral("root") && attr.value() == QStringLiteral("1")) { - is_root = true; - } else if (attr.name() == QStringLiteral("cm") && attr.value() == QStringLiteral("1")) { - is_cm = true; - } else if (attr.name() == QStringLiteral("settings") && attr.value() == QStringLiteral("1")) { - is_settings = true; - } - } + { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("id")) { + id = attr.value().toString(); + } else if (attr.name() == QStringLiteral("root") && attr.value() == QStringLiteral("1")) { + is_root = true; + } else if (attr.name() == QStringLiteral("cm") && attr.value() == QStringLiteral("1")) { + is_cm = true; + } else if (attr.name() == QStringLiteral("settings") && attr.value() == QStringLiteral("1")) { + is_settings = true; } - - if (id.isEmpty()) { - qWarning() << "Failed to load node with empty ID"; - reader->skipCurrentElement(); - } else { - Node* node; - - if (is_root) { - node = project->root(); - } else if (is_cm) { - node = project->color_manager(); - } else if (is_settings) { - node = project->settings(); - } else { - node = NodeFactory::CreateFromID(id); - } - - if (!node) { - qWarning() << "Failed to find node with ID" << id; - reader->skipCurrentElement(); - } else { - LoadNode(node, xml_node_data, reader); - node->setParent(project); - } - } - } else { - reader->skipCurrentElement(); } } - } else if (reader->name() == QStringLiteral("positions")) { + if (id.isEmpty()) { + qWarning() << "Failed to load node with empty ID"; + reader->skipCurrentElement(); + } else { + Node* node; - while (XMLReadNextStartElement(reader)) { + if (is_root) { + node = project->root(); + } else if (is_cm) { + node = project->color_manager(); + } else if (is_settings) { + node = project->settings(); + } else { + node = NodeFactory::CreateFromID(id); + } - if (reader->name() == QStringLiteral("context")) { + if (!node) { + qWarning() << "Failed to find node with ID" << id; + reader->skipCurrentElement(); + } else { + LoadNode(node, xml_node_data, reader); + node->setParent(project); + } + } + } else { + reader->skipCurrentElement(); + } + } - quintptr context_ptr = 0; - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("ptr")) { - context_ptr = attr.value().toULongLong(); - break; - } - } + } else if (reader->name() == QStringLiteral("positions")) { - if (context_ptr) { - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - quintptr node_ptr; - Node::Position node_pos; + while (XMLReadNextStartElement(reader)) { - if (LoadPosition(reader, &node_ptr, &node_pos)) { - if (node_ptr) { - positions[context_ptr].insert(node_ptr, node_pos); - } else { - qWarning() << "Failed to find pointer for node position"; - reader->skipCurrentElement(); - } - } + if (reader->name() == QStringLiteral("context")) { + + quintptr context_ptr = 0; + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + context_ptr = attr.value().toULongLong(); + break; + } + } + + if (context_ptr) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + quintptr node_ptr; + Node::Position node_pos; + + if (LoadPosition(reader, &node_ptr, &node_pos)) { + if (node_ptr) { + positions[context_ptr].insert(node_ptr, node_pos); } else { + qWarning() << "Failed to find pointer for node position"; reader->skipCurrentElement(); } } } else { - qWarning() << "Attempted to load context with no pointer"; reader->skipCurrentElement(); } - - } else { - - reader->skipCurrentElement(); - - } - - } - - - } else if (reader->name() == QStringLiteral("properties")) { - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - quintptr ptr = 0; - - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("ptr")) { - ptr = attr.value().toULongLong(); - - // Only attribute we're looking for right now - break; - } - } - - if (ptr) { - QMap properties_for_node; - while (XMLReadNextStartElement(reader)) { - properties_for_node.insert(reader->name().toString(), reader->readElementText()); - } - properties.insert(ptr, properties_for_node); - } - } else { - reader->skipCurrentElement(); } + } else { + qWarning() << "Attempted to load context with no pointer"; + reader->skipCurrentElement(); } } else { - // Skip this reader->skipCurrentElement(); } + } + + + } else if (reader->name() == QStringLiteral("properties")) { + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + quintptr ptr = 0; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + ptr = attr.value().toULongLong(); + + // Only attribute we're looking for right now + break; + } + } + + if (ptr) { + QMap properties_for_node; + while (XMLReadNextStartElement(reader)) { + properties_for_node.insert(reader->name().toString(), reader->readElementText()); + } + properties.insert(ptr, properties_for_node); + } + } else { + reader->skipCurrentElement(); + } + } + } else { + + // Skip this reader->skipCurrentElement(); + } } @@ -654,7 +647,7 @@ void ProjectSerializer211228::LoadMarkerList(QXmlStreamReader *reader, TimelineM } } - markers->AddMarker(TimeRange(in, out), name); + new TimelineMarker(Config::Current()[QStringLiteral("MarkerColor")].toInt(), TimeRange(in, out), name, markers); } reader->skipCurrentElement(); diff --git a/app/node/project/serializer/serializer220403.cpp b/app/node/project/serializer/serializer220403.cpp index 3515e7706..c64e420bf 100644 --- a/app/node/project/serializer/serializer220403.cpp +++ b/app/node/project/serializer/serializer220403.cpp @@ -20,6 +20,7 @@ #include "serializer220403.h" +#include "config/config.h" #include "node/factory.h" namespace olive { @@ -30,158 +31,164 @@ ProjectSerializer220403::LoadData ProjectSerializer220403::Load(Project *project QMap > positions; XMLNodeData xml_node_data; + LoadData load_data; + while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("url")) { - project->SetSavedURL(reader->readElementText()); - } else if (reader->name() == QStringLiteral("project")) { + if (reader->name() == QStringLiteral("layout")) { + + // Since the main window's functions have to occur in the GUI thread (and we're likely + // loading in a secondary thread), we load all necessary data into a separate struct so we + // can continue loading and queue it with the main window so it can handle the data + // appropriately in its own thread. + + project->SetLayoutInfo(MainWindowLayoutInfo::fromXml(reader, xml_node_data.node_ptrs)); + + } else if (reader->name() == QStringLiteral("uuid")) { + + project->SetUuid(QUuid::fromString(reader->readElementText())); + + } else if (reader->name() == QStringLiteral("nodes")) { + while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("layout")) { + if (reader->name() == QStringLiteral("node")) { + bool is_root = false; + bool is_cm = false; + bool is_settings = false; + QString id; - // Since the main window's functions have to occur in the GUI thread (and we're likely - // loading in a secondary thread), we load all necessary data into a separate struct so we - // can continue loading and queue it with the main window so it can handle the data - // appropriately in its own thread. - - project->SetLayoutInfo(MainWindowLayoutInfo::fromXml(reader, xml_node_data.node_ptrs)); - - } else if (reader->name() == QStringLiteral("uuid")) { - - project->SetUuid(QUuid::fromString(reader->readElementText())); - - } else if (reader->name() == QStringLiteral("nodes")) { - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - bool is_root = false; - bool is_cm = false; - bool is_settings = false; - QString id; - - { - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("id")) { - id = attr.value().toString(); - } else if (attr.name() == QStringLiteral("root") && attr.value() == QStringLiteral("1")) { - is_root = true; - } else if (attr.name() == QStringLiteral("cm") && attr.value() == QStringLiteral("1")) { - is_cm = true; - } else if (attr.name() == QStringLiteral("settings") && attr.value() == QStringLiteral("1")) { - is_settings = true; - } - } + { + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("id")) { + id = attr.value().toString(); + } else if (attr.name() == QStringLiteral("root") && attr.value() == QStringLiteral("1")) { + is_root = true; + } else if (attr.name() == QStringLiteral("cm") && attr.value() == QStringLiteral("1")) { + is_cm = true; + } else if (attr.name() == QStringLiteral("settings") && attr.value() == QStringLiteral("1")) { + is_settings = true; } - - if (id.isEmpty()) { - qWarning() << "Failed to load node with empty ID"; - reader->skipCurrentElement(); - } else { - Node* node; - - if (is_root) { - node = project->root(); - } else if (is_cm) { - node = project->color_manager(); - } else if (is_settings) { - node = project->settings(); - } else { - node = NodeFactory::CreateFromID(id); - } - - if (!node) { - qWarning() << "Failed to find node with ID" << id; - reader->skipCurrentElement(); - } else { - LoadNode(node, xml_node_data, reader); - node->setParent(project); - } - } - } else { - reader->skipCurrentElement(); } } - } else if (reader->name() == QStringLiteral("positions")) { + if (id.isEmpty()) { + qWarning() << "Failed to load node with empty ID"; + reader->skipCurrentElement(); + } else { + Node* node; - while (XMLReadNextStartElement(reader)) { + if (is_root) { + node = project->root(); + } else if (is_cm) { + node = project->color_manager(); + } else if (is_settings) { + node = project->settings(); + } else { + node = NodeFactory::CreateFromID(id); + } - if (reader->name() == QStringLiteral("context")) { + if (!node) { + qWarning() << "Failed to find node with ID" << id; + reader->skipCurrentElement(); + } else { + LoadNode(node, xml_node_data, reader); + node->setParent(project); + } + } + } else { + reader->skipCurrentElement(); + } + } - quintptr context_ptr = 0; - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("ptr")) { - context_ptr = attr.value().toULongLong(); - break; - } - } + } else if (reader->name() == QStringLiteral("markers")) { - if (context_ptr) { - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - quintptr node_ptr; - Node::Position node_pos; + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("marker")) { + TimelineMarker *marker = new TimelineMarker(); + LoadMarker(reader, marker); + load_data.markers.push_back(marker); + } else { + reader->skipCurrentElement(); + } + } - if (LoadPosition(reader, &node_ptr, &node_pos)) { - if (node_ptr) { - positions[context_ptr].insert(node_ptr, node_pos); - } else { - qWarning() << "Failed to find pointer for node position"; - reader->skipCurrentElement(); - } - } + } else if (reader->name() == QStringLiteral("positions")) { + + while (XMLReadNextStartElement(reader)) { + + if (reader->name() == QStringLiteral("context")) { + + quintptr context_ptr = 0; + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + context_ptr = attr.value().toULongLong(); + break; + } + } + + if (context_ptr) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + quintptr node_ptr; + Node::Position node_pos; + + if (LoadPosition(reader, &node_ptr, &node_pos)) { + if (node_ptr) { + positions[context_ptr].insert(node_ptr, node_pos); } else { + qWarning() << "Failed to find pointer for node position"; reader->skipCurrentElement(); } } } else { - qWarning() << "Attempted to load context with no pointer"; reader->skipCurrentElement(); } - - } else { - - reader->skipCurrentElement(); - - } - - } - - - } else if (reader->name() == QStringLiteral("properties")) { - - while (XMLReadNextStartElement(reader)) { - if (reader->name() == QStringLiteral("node")) { - quintptr ptr = 0; - - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("ptr")) { - ptr = attr.value().toULongLong(); - - // Only attribute we're looking for right now - break; - } - } - - if (ptr) { - QMap properties_for_node; - while (XMLReadNextStartElement(reader)) { - properties_for_node.insert(reader->name().toString(), reader->readElementText()); - } - properties.insert(ptr, properties_for_node); - } - } else { - reader->skipCurrentElement(); } + } else { + qWarning() << "Attempted to load context with no pointer"; + reader->skipCurrentElement(); } } else { - // Skip this reader->skipCurrentElement(); } + } + + + } else if (reader->name() == QStringLiteral("properties")) { + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + quintptr ptr = 0; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("ptr")) { + ptr = attr.value().toULongLong(); + + // Only attribute we're looking for right now + break; + } + } + + if (ptr) { + QMap properties_for_node; + while (XMLReadNextStartElement(reader)) { + properties_for_node.insert(reader->name().toString(), reader->readElementText()); + } + properties.insert(ptr, properties_for_node); + } + } else { + reader->skipCurrentElement(); + } + } + } else { + + // Skip this reader->skipCurrentElement(); + } } @@ -201,8 +208,6 @@ ProjectSerializer220403::LoadData ProjectSerializer220403::Load(Project *project // Make connections PostConnect(xml_node_data); - LoadData load_data; - // Resolve serialized properties (if any) for (auto it=properties.cbegin(); it!=properties.cend(); it++) { Node *node = xml_node_data.node_ptrs.value(it.key()); @@ -218,74 +223,94 @@ void ProjectSerializer220403::Save(QXmlStreamWriter *writer, const SaveData &dat { Project *project = data.GetProject(); - writer->writeTextElement(QStringLiteral("uuid"), data.GetProject()->GetUuid().toString()); + if (!data.GetOnlySerializeMarkers().empty()) { - writer->writeStartElement(QStringLiteral("nodes")); + writer->writeStartElement(QStringLiteral("markers")); - const QVector &using_node_list = (data.GetOnlySerializeNodes().isEmpty()) ? project->nodes() : data.GetOnlySerializeNodes(); + for (auto it=data.GetOnlySerializeMarkers().cbegin(); it!=data.GetOnlySerializeMarkers().cend(); it++) { + TimelineMarker *marker = *it; - foreach (Node* node, using_node_list) { - writer->writeStartElement(QStringLiteral("node")); + writer->writeStartElement(QStringLiteral("marker")); - if (node == project->root()) { - writer->writeAttribute(QStringLiteral("root"), QStringLiteral("1")); - } else if (node == project->color_manager()) { - writer->writeAttribute(QStringLiteral("cm"), QStringLiteral("1")); - } else if (node == project->settings()) { - writer->writeAttribute(QStringLiteral("settings"), QStringLiteral("1")); + SaveMarker(writer, marker); + + writer->writeEndElement(); // marker } - writer->writeAttribute(QStringLiteral("id"), node->id()); + writer->writeEndElement(); // markers - SaveNode(node, writer); + } else { - writer->writeEndElement(); // node - } + writer->writeTextElement(QStringLiteral("uuid"), data.GetProject()->GetUuid().toString()); - writer->writeEndElement(); // nodes + writer->writeStartElement(QStringLiteral("nodes")); - writer->writeStartElement(QStringLiteral("positions")); + const QVector &using_node_list = (data.GetOnlySerializeNodes().isEmpty()) ? project->nodes() : data.GetOnlySerializeNodes(); - foreach (Node* context, using_node_list) { - const Node::PositionMap &map = context->GetContextPositions(); + foreach (Node* node, using_node_list) { + writer->writeStartElement(QStringLiteral("node")); - if (!map.isEmpty()) { - writer->writeStartElement(QStringLiteral("context")); - - writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(context))); - - for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { - if (data.GetOnlySerializeNodes().isEmpty() || data.GetOnlySerializeNodes().contains(jt.key())) { - writer->writeStartElement(QStringLiteral("node")); - SavePosition(writer, jt.key(), jt.value()); - writer->writeEndElement(); // node - } + if (node == project->root()) { + writer->writeAttribute(QStringLiteral("root"), QStringLiteral("1")); + } else if (node == project->color_manager()) { + writer->writeAttribute(QStringLiteral("cm"), QStringLiteral("1")); + } else if (node == project->settings()) { + writer->writeAttribute(QStringLiteral("settings"), QStringLiteral("1")); } - writer->writeEndElement(); // context - } - } + writer->writeAttribute(QStringLiteral("id"), node->id()); - writer->writeEndElement(); // positions + SaveNode(node, writer); - writer->writeStartElement(QStringLiteral("properties")); - - for (auto it=data.GetProperties().cbegin(); it!=data.GetProperties().cend(); it++) { - writer->writeStartElement(QStringLiteral("node")); - - writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(it.key()))); - - for (auto jt=it.value().cbegin(); jt!=it.value().cend(); jt++) { - writer->writeTextElement(jt.key(), jt.value()); + writer->writeEndElement(); // node } - writer->writeEndElement(); // node + writer->writeEndElement(); // nodes + + writer->writeStartElement(QStringLiteral("positions")); + + foreach (Node* context, using_node_list) { + const Node::PositionMap &map = context->GetContextPositions(); + + if (!map.isEmpty()) { + writer->writeStartElement(QStringLiteral("context")); + + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(context))); + + for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { + if (data.GetOnlySerializeNodes().isEmpty() || data.GetOnlySerializeNodes().contains(jt.key())) { + writer->writeStartElement(QStringLiteral("node")); + SavePosition(writer, jt.key(), jt.value()); + writer->writeEndElement(); // node + } + } + + writer->writeEndElement(); // context + } + } + + writer->writeEndElement(); // positions + + writer->writeStartElement(QStringLiteral("properties")); + + for (auto it=data.GetProperties().cbegin(); it!=data.GetProperties().cend(); it++) { + writer->writeStartElement(QStringLiteral("node")); + + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(it.key()))); + + for (auto jt=it.value().cbegin(); jt!=it.value().cend(); jt++) { + writer->writeTextElement(jt.key(), jt.value()); + } + + writer->writeEndElement(); // node + } + + writer->writeEndElement(); // properties + + // Save main window project layout + project->GetLayoutInfo().toXml(writer); + } - - writer->writeEndElement(); // properties - - // Save main window project layout - project->GetLayoutInfo().toXml(writer); } void ProjectSerializer220403::LoadNode(Node *node, XMLNodeData &xml_node_data, QXmlStreamReader *reader) const @@ -961,6 +986,36 @@ void ProjectSerializer220403::SaveTimelinePoints(QXmlStreamWriter *writer, Timel writer->writeEndElement(); // markers } +void ProjectSerializer220403::LoadMarker(QXmlStreamReader *reader, TimelineMarker *marker) const +{ + rational in, out; + + XMLAttributeLoop(reader, attr) { + if (attr.name() == QStringLiteral("name")) { + marker->set_name(attr.value().toString()); + } else if (attr.name() == QStringLiteral("in")) { + in = rational::fromString(attr.value().toString()); + } else if (attr.name() == QStringLiteral("out")) { + out = rational::fromString(attr.value().toString()); + } else if (attr.name() == QStringLiteral("color")) { + marker->set_color(attr.value().toInt()); + } + } + + marker->set_time(TimeRange(in, out)); + + // This element has no inner text, so just skip it + reader->skipCurrentElement(); +} + +void ProjectSerializer220403::SaveMarker(QXmlStreamWriter *writer, TimelineMarker *marker) const +{ + writer->writeAttribute(QStringLiteral("name"), marker->name()); + writer->writeAttribute(QStringLiteral("in"), marker->time_range().in().toString()); + writer->writeAttribute(QStringLiteral("out"), marker->time_range().out().toString()); + writer->writeAttribute(QStringLiteral("color"), QString::number(marker->color())); +} + void ProjectSerializer220403::LoadWorkArea(QXmlStreamReader *reader, TimelineWorkArea *workarea) const { rational range_in = workarea->in(); @@ -996,35 +1051,22 @@ void ProjectSerializer220403::LoadMarkerList(QXmlStreamReader *reader, TimelineM { while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("marker")) { - QString name; - rational in, out; - - XMLAttributeLoop(reader, attr) { - if (attr.name() == QStringLiteral("name")) { - name = attr.value().toString(); - } else if (attr.name() == QStringLiteral("in")) { - in = rational::fromString(attr.value().toString()); - } else if (attr.name() == QStringLiteral("out")) { - out = rational::fromString(attr.value().toString()); - } - } - - markers->AddMarker(TimeRange(in, out), name); + TimelineMarker *marker = new TimelineMarker(markers); + LoadMarker(reader, marker); + } else { + reader->skipCurrentElement(); } - - reader->skipCurrentElement(); } } void ProjectSerializer220403::SaveMarkerList(QXmlStreamWriter *writer, TimelineMarkerList *markers) const { - foreach (TimelineMarker* marker, markers->list()) { + for (auto it=markers->cbegin(); it!=markers->cend(); it++) { + TimelineMarker* marker = *it; + writer->writeStartElement(QStringLiteral("marker")); - writer->writeAttribute(QStringLiteral("name"), marker->name()); - - writer->writeAttribute(QStringLiteral("in"), marker->time().in().toString()); - writer->writeAttribute(QStringLiteral("out"), marker->time().out().toString()); + SaveMarker(writer, marker); writer->writeEndElement(); // marker } diff --git a/app/node/project/serializer/serializer220403.h b/app/node/project/serializer/serializer220403.h index f6a1b939f..16de30679 100644 --- a/app/node/project/serializer/serializer220403.h +++ b/app/node/project/serializer/serializer220403.h @@ -100,6 +100,10 @@ private: void SaveTimelinePoints(QXmlStreamWriter *writer, TimelinePoints *points) const; + void LoadMarker(QXmlStreamReader *reader, TimelineMarker *marker) const; + + void SaveMarker(QXmlStreamWriter *writer, TimelineMarker *marker) const; + void LoadWorkArea(QXmlStreamReader *reader, TimelineWorkArea *workarea) const; void SaveWorkArea(QXmlStreamWriter *writer, TimelineWorkArea *workarea) const; diff --git a/app/node/time/timeoffset/timeoffsetnode.cpp b/app/node/time/timeoffset/timeoffsetnode.cpp index d137847ee..0ef179393 100644 --- a/app/node/time/timeoffset/timeoffsetnode.cpp +++ b/app/node/time/timeoffset/timeoffsetnode.cpp @@ -41,6 +41,8 @@ TimeOffsetNode::TimeOffsetNode() void TimeOffsetNode::Retranslate() { + super::Retranslate(); + SetInputName(kTimeInput, QStringLiteral("Time")); SetInputName(kInputInput, QStringLiteral("Input")); } diff --git a/app/node/time/timeremap/timeremap.cpp b/app/node/time/timeremap/timeremap.cpp index 1f52a27c4..ff2b1ff77 100644 --- a/app/node/time/timeremap/timeremap.cpp +++ b/app/node/time/timeremap/timeremap.cpp @@ -87,6 +87,8 @@ TimeRange TimeRemapNode::OutputTimeAdjustment(const QString &input, int element, void TimeRemapNode::Retranslate() { + super::Retranslate(); + SetInputName(kTimeInput, QStringLiteral("Time")); SetInputName(kInputInput, QStringLiteral("Input")); } diff --git a/app/node/traverser.cpp b/app/node/traverser.cpp index 677f3ce99..ac77a3ca2 100644 --- a/app/node/traverser.cpp +++ b/app/node/traverser.cpp @@ -246,20 +246,33 @@ NodeValueTable NodeTraverser::GenerateTable(const Node *n, const Node::ValueHint // Generate row for node NodeValueDatabase database = GenerateDatabase(n, range); - NodeValueRow row = GenerateRow(&database, n, range); - //qDebug() << "FIXME: Implement pre-process of row"; + // Check for bypass + bool is_enabled; + if (!database[Node::kEnabledInput].Has(NodeValue::kBoolean)) { + // Fallback if we couldn't find a bool value + is_enabled = true; + } else { + is_enabled = database[Node::kEnabledInput].Get(NodeValue::kBoolean).toBool(); + } - // Generate output table - NodeValueTable table = database.Merge(); + if (is_enabled) { + NodeValueRow row = GenerateRow(&database, n, range); + //qDebug() << "FIXME: Implement pre-process of row"; - // By this point, the node should have all the inputs it needs to render correctly - n->Value(row, GenerateGlobals(video_params_, range), &table); + // Generate output table + NodeValueTable table = database.Merge(); - // Post-process table - PostProcessTable(n, hint, range, table); + // By this point, the node should have all the inputs it needs to render correctly + n->Value(row, GenerateGlobals(video_params_, range), &table); - return table; + // Post-process table + PostProcessTable(n, hint, range, table); + + return table; + } else { + return database.Merge(); + } } NodeValueTable NodeTraverser::GenerateBlockTable(const Track *track, const TimeRange &range) diff --git a/app/panel/sequenceviewer/sequenceviewer.cpp b/app/panel/sequenceviewer/sequenceviewer.cpp index 5cda8dda3..9a76f1a18 100644 --- a/app/panel/sequenceviewer/sequenceviewer.cpp +++ b/app/panel/sequenceviewer/sequenceviewer.cpp @@ -19,6 +19,7 @@ ***/ #include "sequenceviewer.h" +#include "panel/timeline/timeline.h" namespace olive { @@ -29,6 +30,12 @@ SequenceViewerPanel::SequenceViewerPanel(QWidget *parent) : Retranslate(); } +void SequenceViewerPanel::StartCapture(const TimeRange &time, const Track::Reference &track) +{ + TimelinePanel *tp = static_cast(sender()); + static_cast(GetTimeBasedWidget())->StartCapture(tp->timeline_widget(), time, track); +} + void SequenceViewerPanel::Retranslate() { ViewerPanel::Retranslate(); diff --git a/app/panel/sequenceviewer/sequenceviewer.h b/app/panel/sequenceviewer/sequenceviewer.h index 6f5f11e1e..486e762a8 100644 --- a/app/panel/sequenceviewer/sequenceviewer.h +++ b/app/panel/sequenceviewer/sequenceviewer.h @@ -31,6 +31,9 @@ class SequenceViewerPanel : public ViewerPanel public: SequenceViewerPanel(QWidget* parent); +public slots: + void StartCapture(const TimeRange &time, const Track::Reference &track); + protected: virtual void Retranslate() override; diff --git a/app/panel/timebased/timebased.cpp b/app/panel/timebased/timebased.cpp index 44cc51281..b2fabbec9 100644 --- a/app/panel/timebased/timebased.cpp +++ b/app/panel/timebased/timebased.cpp @@ -211,4 +211,9 @@ void TimeBasedPanel::GoToOut() GetTimeBasedWidget()->GoToOut(); } +void TimeBasedPanel::DeleteSelected() +{ + GetTimeBasedWidget()->DeleteSelected(); +} + } diff --git a/app/panel/timebased/timebased.h b/app/panel/timebased/timebased.h index b85657e4d..380420bbc 100644 --- a/app/panel/timebased/timebased.h +++ b/app/panel/timebased/timebased.h @@ -98,6 +98,8 @@ public: virtual void GoToOut() override; + virtual void DeleteSelected() override; + public slots: void SetTimebase(const rational& timebase); diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 461d94f16..e4fd4e274 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -34,6 +34,7 @@ TimelinePanel::TimelinePanel(QWidget *parent) : Retranslate(); connect(tw, &TimelineWidget::BlockSelectionChanged, this, &TimelinePanel::BlockSelectionChanged); + connect(tw, &TimelineWidget::RequestCaptureStart, this, &TimelinePanel::RequestCaptureStart ); } void TimelinePanel::SplitAtPlayhead() diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index 360187b0a..9e40b232d 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -112,6 +112,8 @@ protected: signals: void BlockSelectionChanged(const QVector& selected_blocks); + void RequestCaptureStart(const TimeRange &time, const Track::Reference &track); + }; } diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index f762c480e..9289dcc81 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -20,6 +20,7 @@ #include "openglrenderer.h" +#include #include #include #include diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp index dbdc47e13..032357aea 100644 --- a/app/render/videoparams.cpp +++ b/app/render/videoparams.cpp @@ -20,6 +20,10 @@ #include "videoparams.h" +extern "C" { +#include +} + #include #include "core.h" diff --git a/app/task/project/import/import.cpp b/app/task/project/import/import.cpp index 2e2823b15..352a8f175 100644 --- a/app/task/project/import/import.cpp +++ b/app/task/project/import/import.cpp @@ -30,9 +30,8 @@ namespace olive { -ProjectImportTask::ProjectImportTask(ProjectViewModel *model, Folder *folder, const QStringList &filenames) : +ProjectImportTask::ProjectImportTask(Folder *folder, const QStringList &filenames) : command_(nullptr), - model_(model), folder_(folder) { foreach (const QString& f, filenames) { @@ -117,6 +116,9 @@ void ProjectImportTask::Import(Folder *folder, QFileInfoList import, int &counte // Create undoable command that adds the items to the model AddItemToFolder(folder, footage, parent_command); + + // Add to vector + imported_footage_.push_back(footage); } else { // Add to list so we can tell the user about it later invalid_files_.append(file_info.absoluteFilePath()); @@ -220,7 +222,7 @@ void ProjectImportTask::ValidateImageSequence(Footage *footage, QFileInfoList& i void ProjectImportTask::AddItemToFolder(Folder *folder, Node *item, MultiUndoCommand *command) { // Create undoable command that adds the items to the model - Project* project = model_->project(); + Project* project = folder->project(); NodeAddCommand* nac = new NodeAddCommand(project, item); nac->PushToThread(project->thread()); diff --git a/app/task/project/import/import.h b/app/task/project/import/import.h index 79559b598..3f884478a 100644 --- a/app/task/project/import/import.h +++ b/app/task/project/import/import.h @@ -34,7 +34,7 @@ class ProjectImportTask : public Task { Q_OBJECT public: - ProjectImportTask(ProjectViewModel* model, Folder* folder, const QStringList& filenames); + ProjectImportTask(Folder* folder, const QStringList& filenames); const int& GetFileCount() const; @@ -53,6 +53,8 @@ public: return !invalid_files_.isEmpty(); } + const QVector &GetImportedFootage() const { return imported_footage_; } + protected: virtual bool Run() override; @@ -71,8 +73,6 @@ private: MultiUndoCommand* command_; - ProjectViewModel* model_; - Folder* folder_; QFileInfoList filenames_; @@ -83,6 +83,8 @@ private: QList image_sequence_ignore_files_; + QVector imported_footage_; + }; } diff --git a/app/task/project/load/load.cpp b/app/task/project/load/load.cpp index 606f53e37..17497723c 100644 --- a/app/task/project/load/load.cpp +++ b/app/task/project/load/load.cpp @@ -37,7 +37,7 @@ bool ProjectLoadTask::Run() project_->set_filename(GetFilename()); - ProjectSerializer::Result result = ProjectSerializer::Load(project_, GetFilename()); + ProjectSerializer::Result result = ProjectSerializer::Load(project_, GetFilename(), QStringLiteral("project")); switch (result.code()) { case ProjectSerializer::kSuccess: @@ -57,6 +57,9 @@ bool ProjectLoadTask::Run() case ProjectSerializer::kXmlError: SetError(tr("Failed to read XML document. File may be corrupt. Error was: %1").arg(result.GetDetails())); break; + case ProjectSerializer::kNoData: + SetError(tr("Failed to find any data to parse.")); + break; // Errors that should never be thrown by a load case ProjectSerializer::kOverwriteError: diff --git a/app/task/project/save/save.cpp b/app/task/project/save/save.cpp index 1f5742bcf..24c95b665 100644 --- a/app/task/project/save/save.cpp +++ b/app/task/project/save/save.cpp @@ -42,7 +42,7 @@ bool ProjectSaveTask::Run() ProjectSerializer::SaveData data(project_, using_filename); - ProjectSerializer::Result result = ProjectSerializer::Save(data); + ProjectSerializer::Result result = ProjectSerializer::Save(data, QStringLiteral("project")); bool success = false; @@ -66,6 +66,7 @@ bool ProjectSaveTask::Run() case ProjectSerializer::kProjectTooNew: case ProjectSerializer::kProjectTooOld: case ProjectSerializer::kUnknownVersion: + case ProjectSerializer::kNoData: SetError(tr("Unknown error.")); break; } diff --git a/app/timeline/timelinemarker.cpp b/app/timeline/timelinemarker.cpp index c94e1b6ce..32eb98976 100644 --- a/app/timeline/timelinemarker.cpp +++ b/app/timeline/timelinemarker.cpp @@ -20,20 +20,26 @@ #include "timelinemarker.h" +#include "common/qtutils.h" #include "common/xmlutils.h" +#include "config/config.h" +#include "core.h" +#include "ui/colorcoding.h" namespace olive { -TimelineMarker::TimelineMarker(const TimeRange &time, const QString &name, QObject *parent) : - QObject(parent), - time_(time), - name_(name) +TimelineMarker::TimelineMarker(QObject *parent) : + color_(Config::Current()[QStringLiteral("MarkerColor")].toInt()) { + setParent(parent); } -const TimeRange &TimelineMarker::time() const +TimelineMarker::TimelineMarker(int color, const TimeRange &time, const QString &name, QObject *parent) : + time_(time), + name_(name), + color_(color) { - return time_; + setParent(parent); } void TimelineMarker::set_time(const TimeRange &time) @@ -42,9 +48,15 @@ void TimelineMarker::set_time(const TimeRange &time) emit TimeChanged(time_); } -const QString &TimelineMarker::name() const +void TimelineMarker::set_time(const rational &time) { - return name_; + set_time(TimeRange(time, time + time_.length())); +} + +bool TimelineMarker::has_sibling_at_time(const rational &t) const +{ + TimelineMarker *m = static_cast(parent())->GetMarkerAtTime(t); + return m && m != this; } void TimelineMarker::set_name(const QString &name) @@ -53,36 +65,265 @@ void TimelineMarker::set_name(const QString &name) emit NameChanged(name_); } -TimelineMarkerList::~TimelineMarkerList() +void TimelineMarker::set_color(int c) { - qDeleteAll(markers_); + color_ = c; + emit ColorChanged(color_); } -TimelineMarker* TimelineMarkerList::AddMarker(const TimeRange &time, const QString &name) +int TimelineMarker::GetMarkerHeight(const QFontMetrics &fm) { - TimelineMarker* m = new TimelineMarker(time, name); - markers_.append(m); - emit MarkerAdded(m); - return m; + return fm.height(); } -void TimelineMarkerList::RemoveMarker(TimelineMarker *marker) +QRect TimelineMarker::Draw(QPainter *p, const QPoint &pt, double scale, bool selected) { - for (int i=0;ifontMetrics(); + + int marker_height = GetMarkerHeight(fm); + int marker_width = QtUtils::QFontMetricsWidth(fm, QStringLiteral("H")); + + int half_width = marker_width / 2; + + QColor c = ColorCoding::GetColor(color()).toQColor(); + if (selected) { + p->setPen(Qt::white); + p->setBrush(c.lighter()); + } else { + p->setPen(Qt::black); + p->setBrush(c); + } + + int top = pt.y() - marker_height; + + if (time_.out() != time_.in()) { + QRect marker_rect(pt.x(), top, time_.length().toDouble() * scale, marker_height); + + p->drawRect(marker_rect); + + if (!name_.isEmpty()) { + p->setPen(ColorCoding::GetUISelectorColor(ColorCoding::GetColor(color_))); + p->drawText(marker_rect.adjusted(marker_width/4, 0, 0, 0), name_, Qt::AlignLeft | Qt::AlignVCenter); + } + + return marker_rect; + } else { + int half_marker_height = marker_height / 3; + int left = pt.x() - half_width; + int right = pt.x() + half_width; + int center_y = pt.y() - half_marker_height; + + QPoint points[] = { + pt, + QPoint(left, center_y), + QPoint(left, top), + QPoint(right, top), + QPoint(right, center_y), + pt, + }; + + p->setRenderHint(QPainter::Antialiasing); + p->drawPolygon(points, 6); + + return QRect(left, top, marker_width, marker_height); + } +} + +void TimelineMarkerList::childEvent(QChildEvent *e) +{ + QObject::childEvent(e); + + if (TimelineMarker *marker = dynamic_cast(e->child())) { + if (e->type() == QChildEvent::ChildAdded) { + + connect(marker, &TimelineMarker::TimeChanged, this, &TimelineMarkerList::HandleMarkerTimeChange); + connect(marker, &TimelineMarker::TimeChanged, this, &TimelineMarkerList::HandleMarkerModification); + connect(marker, &TimelineMarker::NameChanged, this, &TimelineMarkerList::HandleMarkerModification); + connect(marker, &TimelineMarker::ColorChanged, this, &TimelineMarkerList::HandleMarkerModification); + + InsertIntoList(marker); + + emit MarkerAdded(marker); + + } else if (e->type() == QChildEvent::ChildRemoved) { + + RemoveFromList(marker); + + disconnect(marker, &TimelineMarker::TimeChanged, this, &TimelineMarkerList::HandleMarkerTimeChange); + disconnect(marker, &TimelineMarker::TimeChanged, this, &TimelineMarkerList::HandleMarkerModification); + disconnect(marker, &TimelineMarker::NameChanged, this, &TimelineMarkerList::HandleMarkerModification); + disconnect(marker, &TimelineMarker::ColorChanged, this, &TimelineMarkerList::HandleMarkerModification); + + emit MarkerRemoved(marker); - if (m == marker) { - markers_.removeAt(i); - emit MarkerRemoved(m); - delete m; - break; } } } -const QList &TimelineMarkerList::list() const +void TimelineMarkerList::InsertIntoList(TimelineMarker *marker) { - return markers_; + // Insertion sort by time to allow some loop optimizations + bool found = false; + for (auto it=markers_.begin(); it!=markers_.end(); it++) { + TimelineMarker *m = *it; + + Q_ASSERT(m->time() != marker->time()); + + if (m->time() > marker->time()) { + markers_.insert(it, marker); + found = true; + break; + } + } + + if (!found) { + markers_.push_back(marker); + } +} + +bool TimelineMarkerList::RemoveFromList(TimelineMarker *marker) +{ + auto it = std::find(markers_.begin(), markers_.end(), marker); + + if (it != markers_.end()) { + markers_.erase(it); + return true; + } + + return false; +} + +void TimelineMarkerList::HandleMarkerModification() +{ + emit MarkerModified(static_cast(sender())); +} + +void TimelineMarkerList::HandleMarkerTimeChange() +{ + TimelineMarker *m = static_cast(sender()); + + auto it = std::find(markers_.begin(), markers_.end(), m); + + if ((it+1 != markers_.end() && (*(it+1))->time() < m->time()) + || (it != markers_.begin() && (*(it-1))->time() > m->time())) { + // Re-sort into list + markers_.erase(it); + InsertIntoList(m); + } +} + +MarkerAddCommand::MarkerAddCommand(TimelineMarkerList *marker_list, const TimeRange &range, const QString &name, int color) : + MarkerAddCommand(marker_list, new TimelineMarker(color, range, name, &memory_manager_)) +{ +} + +MarkerAddCommand::MarkerAddCommand(TimelineMarkerList *marker_list, TimelineMarker *marker) : + marker_list_(marker_list), + added_marker_(marker) +{ + added_marker_->setParent(&memory_manager_); +} + +Project* MarkerAddCommand::GetRelevantProject() const +{ + return Project::GetProjectFromObject(marker_list_); +} + +void MarkerAddCommand::redo() +{ + added_marker_->setParent(marker_list_); +} + +void MarkerAddCommand::undo() +{ + added_marker_->setParent(&memory_manager_); +} + +MarkerRemoveCommand::MarkerRemoveCommand(TimelineMarker *marker) : + marker_(marker) +{ +} + +Project* MarkerRemoveCommand::GetRelevantProject() const +{ + return Project::GetProjectFromObject(marker_); +} + +void MarkerRemoveCommand::redo() +{ + marker_list_ = marker_->parent(); + marker_->setParent(&memory_manager_); +} + +void MarkerRemoveCommand::undo() +{ + marker_->setParent(marker_list_); +} + +MarkerChangeColorCommand::MarkerChangeColorCommand(TimelineMarker *marker, int new_color) : + marker_(marker), + new_color_(new_color) +{ +} + +Project* MarkerChangeColorCommand::GetRelevantProject() const +{ + return Project::GetProjectFromObject(marker_); +} + +void MarkerChangeColorCommand::redo() +{ + old_color_ = marker_->color(); + marker_->set_color(new_color_); +} + +void MarkerChangeColorCommand::undo() +{ + marker_->set_color(old_color_); +} + +MarkerChangeNameCommand::MarkerChangeNameCommand(TimelineMarker *marker, QString new_name) : + marker_(marker), + new_name_(new_name) +{ +} + +Project* MarkerChangeNameCommand::GetRelevantProject() const +{ + return Project::GetProjectFromObject(marker_); +} + +void MarkerChangeNameCommand::redo() +{ + old_name_ = marker_->name(); + marker_->set_name(new_name_); +} + +void MarkerChangeNameCommand::undo() +{ + marker_->set_name(old_name_); +} + +MarkerChangeTimeCommand::MarkerChangeTimeCommand(TimelineMarker* marker, TimeRange time) : + marker_(marker), + new_time_(time) +{ +} + +Project* MarkerChangeTimeCommand::GetRelevantProject() const +{ + return Project::GetProjectFromObject(marker_); +} + +void MarkerChangeTimeCommand::redo() +{ + old_time_ = marker_->time_range(); + marker_->set_time(new_time_); +} + +void MarkerChangeTimeCommand::undo() +{ + marker_->set_time(old_time_); } } diff --git a/app/timeline/timelinemarker.h b/app/timeline/timelinemarker.h index 3c597b47d..492c61b9b 100644 --- a/app/timeline/timelinemarker.h +++ b/app/timeline/timelinemarker.h @@ -21,11 +21,13 @@ #ifndef TIMELINEMARKER_H #define TIMELINEMARKER_H +#include #include #include #include #include "common/timerange.h" +#include "undo/undocommand.h" namespace olive { @@ -33,47 +35,210 @@ class TimelineMarker : public QObject { Q_OBJECT public: - TimelineMarker(const TimeRange& time = TimeRange(), const QString& name = QString(), QObject* parent = nullptr); + TimelineMarker(QObject* parent = nullptr); + TimelineMarker(int color, const TimeRange& time, const QString& name = QString(), QObject* parent = nullptr); - const TimeRange &time() const; + /** + * @brief Dummy function for TimeBasedViewSelectionManager compatibility + * + * FIXME: Once we upgrade to C++17, we won't need this because we'll be able to check types in + * TimeBasedViewSelectionManager's template functions + */ + const rational &time() const { return time_.in(); } + void set_time(const rational& time); + + const TimeRange &time_range() const { return time_; } void set_time(const TimeRange& time); - const QString& name() const; + bool has_sibling_at_time(const rational &t) const; + + const QString& name() const { return name_; } void set_name(const QString& name); + int color() const { return color_; } + void set_color(int c); + + static int GetMarkerHeight(const QFontMetrics &fm); + QRect Draw(QPainter *p, const QPoint &pt, double scale, bool selected); + signals: void TimeChanged(const TimeRange& time); void NameChanged(const QString& name); + void ColorChanged(int c); + private: TimeRange time_; QString name_; + int color_; + }; class TimelineMarkerList : public QObject { Q_OBJECT public: - TimelineMarkerList() = default; + TimelineMarkerList(QObject *parent = nullptr) : + QObject(parent) + { + } - virtual ~TimelineMarkerList() override; + inline bool empty() const { return markers_.empty(); } + inline std::vector::iterator begin() { return markers_.begin(); } + inline std::vector::iterator end() { return markers_.end(); } + inline std::vector::const_iterator cbegin() const { return markers_.cbegin(); } + inline std::vector::const_iterator cend() const { return markers_.cend(); } + inline TimelineMarker *back() const { return markers_.back(); } + inline TimelineMarker *front() const { return markers_.front(); } + inline size_t size() const { return markers_.size(); } - TimelineMarker *AddMarker(const TimeRange& time = TimeRange(), const QString& name = QString()); + TimelineMarker *GetMarkerAtTime(const rational &t) const + { + for (auto it=markers_.cbegin(); it!=markers_.cend(); it++) { + TimelineMarker *m = *it; + if (m->time() == t) { + return m; + } + } - void RemoveMarker(TimelineMarker* marker); + return nullptr; + } - const QList &list() const; + TimelineMarker *GetClosestMarkerToTime(const rational &t) const + { + TimelineMarker *closest = nullptr; + + for (auto it=markers_.cbegin(); it!=markers_.cend(); it++) { + TimelineMarker *m = *it; + + rational this_diff = qAbs(m->time() - t); + + if (closest) { + rational stored_diff = qAbs(closest->time() - t); + + if (this_diff > stored_diff) { + // Since the list is organized by time, if the diff increases, assume we are only going + // to move further away from here and there's no need to check + break; + } + } + + closest = m; + } + + return closest; + } signals: void MarkerAdded(TimelineMarker* marker); void MarkerRemoved(TimelineMarker* marker); + void MarkerModified(TimelineMarker* marker); + +protected: + virtual void childEvent(QChildEvent *e) override; + private: - QList markers_; + void InsertIntoList(TimelineMarker *m); + bool RemoveFromList(TimelineMarker *m); + + std::vector markers_; + +private slots: + void HandleMarkerModification(); + + void HandleMarkerTimeChange(); + +}; + +class MarkerAddCommand : public UndoCommand { +public: + MarkerAddCommand(TimelineMarkerList* marker_list, const TimeRange& range, const QString& name, int color); + MarkerAddCommand(TimelineMarkerList* marker_list, TimelineMarker *marker); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + TimelineMarkerList* marker_list_; + + TimelineMarker* added_marker_; + QObject memory_manager_; + +}; + +class MarkerRemoveCommand : public UndoCommand { +public: + MarkerRemoveCommand(TimelineMarker* marker); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + TimelineMarker* marker_; + QObject* marker_list_; + + QObject memory_manager_; + +}; + +class MarkerChangeColorCommand : public UndoCommand { +public: + MarkerChangeColorCommand(TimelineMarker* marker, int new_color); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + TimelineMarker* marker_; + int old_color_; + int new_color_; + +}; + +class MarkerChangeNameCommand : public UndoCommand { +public: + MarkerChangeNameCommand(TimelineMarker* marker, QString name); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + TimelineMarker* marker_; + QString old_name_; + QString new_name_; +}; + +class MarkerChangeTimeCommand : public UndoCommand { +public: + MarkerChangeTimeCommand(TimelineMarker* marker, TimeRange time); + + virtual Project* GetRelevantProject() const override; + +protected: + virtual void redo() override; + virtual void undo() override; + +private: + TimelineMarker* marker_; + TimeRange old_time_; + TimeRange new_time_; }; diff --git a/app/timeline/timelinepoints.cpp b/app/timeline/timelinepoints.cpp index 4329c42b4..61ed3633d 100644 --- a/app/timeline/timelinepoints.cpp +++ b/app/timeline/timelinepoints.cpp @@ -24,24 +24,31 @@ namespace olive { +TimelinePoints::TimelinePoints(QObject *parent) : + QObject(parent) +{ + markers_ = new TimelineMarkerList(this); + workarea_ = new TimelineWorkArea(this); +} + TimelineMarkerList *TimelinePoints::markers() { - return &markers_; + return markers_; } const TimelineMarkerList *TimelinePoints::markers() const { - return &markers_; + return markers_; } const TimelineWorkArea *TimelinePoints::workarea() const { - return &workarea_; + return workarea_; } TimelineWorkArea *TimelinePoints::workarea() { - return &workarea_; + return workarea_; } } diff --git a/app/timeline/timelinepoints.h b/app/timeline/timelinepoints.h index 14527608d..62290bef4 100644 --- a/app/timeline/timelinepoints.h +++ b/app/timeline/timelinepoints.h @@ -29,10 +29,11 @@ namespace olive { -class TimelinePoints +class TimelinePoints : public QObject { + Q_OBJECT public: - TimelinePoints() = default; + TimelinePoints(QObject *parent = nullptr); TimelineMarkerList* markers(); const TimelineMarkerList* markers() const; @@ -41,9 +42,9 @@ public: const TimelineWorkArea* workarea() const; private: - TimelineMarkerList markers_; + TimelineMarkerList *markers_; - TimelineWorkArea workarea_; + TimelineWorkArea *workarea_; }; diff --git a/app/ui/icons/icons.cpp b/app/ui/icons/icons.cpp index 5c16000a5..cc79d834a 100644 --- a/app/ui/icons/icons.cpp +++ b/app/ui/icons/icons.cpp @@ -55,6 +55,7 @@ QIcon icon::ToolSlip; QIcon icon::ToolSlide; QIcon icon::ToolHand; QIcon icon::ToolTransition; +QIcon icon::ToolTrackSelect; QIcon icon::Folder; QIcon icon::Sequence; QIcon icon::Video; @@ -68,6 +69,8 @@ QIcon icon::TriRight; QIcon icon::TextBold; QIcon icon::TextItalic; QIcon icon::TextUnderline; +QIcon icon::TextStrikethrough; +QIcon icon::TextSmallCaps; QIcon icon::TextAlignLeft; QIcon icon::TextAlignRight; QIcon icon::TextAlignCenter; @@ -116,6 +119,7 @@ void icon::LoadAll(const QString& theme) ToolSlide = Create(theme, "slide"); ToolHand = Create(theme, "hand"); ToolTransition = Create(theme, "transition-tool"); + ToolTrackSelect = Create(theme, "track-tool"); Folder = Create(theme, "folder"); Sequence = Create(theme, "sequence"); @@ -133,6 +137,8 @@ void icon::LoadAll(const QString& theme) TextBold = Create(theme, "text-bold"); TextItalic = Create(theme, "text-italic"); TextUnderline = Create(theme, "text-underline"); + TextStrikethrough = Create(theme, "text-strikethrough"); + TextSmallCaps = Create(theme, "text-small-caps"); TextAlignLeft = Create(theme, "align-left"); TextAlignRight = Create(theme, "align-right"); TextAlignCenter = Create(theme, "align-center"); diff --git a/app/ui/icons/icons.h b/app/ui/icons/icons.h index c3e330685..987ab2b3b 100644 --- a/app/ui/icons/icons.h +++ b/app/ui/icons/icons.h @@ -57,6 +57,7 @@ extern QIcon ToolSlip; extern QIcon ToolSlide; extern QIcon ToolHand; extern QIcon ToolTransition; +extern QIcon ToolTrackSelect; // Project Icons extern QIcon Folder; @@ -78,6 +79,8 @@ extern QIcon TriRight; extern QIcon TextBold; extern QIcon TextItalic; extern QIcon TextUnderline; +extern QIcon TextStrikethrough; +extern QIcon TextSmallCaps; extern QIcon TextAlignLeft; extern QIcon TextAlignRight; extern QIcon TextAlignCenter; diff --git a/app/ui/style/olive-dark/png/text-small-caps.128.disabled.png b/app/ui/style/olive-dark/png/text-small-caps.128.disabled.png new file mode 100644 index 000000000..2a9a47157 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-small-caps.128.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-small-caps.128.png b/app/ui/style/olive-dark/png/text-small-caps.128.png new file mode 100644 index 000000000..0f990ae37 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-small-caps.128.png differ diff --git a/app/ui/style/olive-dark/png/text-small-caps.16.disabled.png b/app/ui/style/olive-dark/png/text-small-caps.16.disabled.png new file mode 100644 index 000000000..549f4925e Binary files /dev/null and b/app/ui/style/olive-dark/png/text-small-caps.16.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-small-caps.16.png b/app/ui/style/olive-dark/png/text-small-caps.16.png new file mode 100644 index 000000000..c93767c3c Binary files /dev/null and b/app/ui/style/olive-dark/png/text-small-caps.16.png differ diff --git a/app/ui/style/olive-dark/png/text-small-caps.32.disabled.png b/app/ui/style/olive-dark/png/text-small-caps.32.disabled.png new file mode 100644 index 000000000..59d7ebaf3 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-small-caps.32.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-small-caps.32.png b/app/ui/style/olive-dark/png/text-small-caps.32.png new file mode 100644 index 000000000..3efb4b22f Binary files /dev/null and b/app/ui/style/olive-dark/png/text-small-caps.32.png differ diff --git a/app/ui/style/olive-dark/png/text-small-caps.64.disabled.png b/app/ui/style/olive-dark/png/text-small-caps.64.disabled.png new file mode 100644 index 000000000..f07a84e91 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-small-caps.64.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-small-caps.64.png b/app/ui/style/olive-dark/png/text-small-caps.64.png new file mode 100644 index 000000000..9dbdc46e2 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-small-caps.64.png differ diff --git a/app/ui/style/olive-dark/png/text-strikethrough.128.disabled.png b/app/ui/style/olive-dark/png/text-strikethrough.128.disabled.png new file mode 100644 index 000000000..1f02133ce Binary files /dev/null and b/app/ui/style/olive-dark/png/text-strikethrough.128.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-strikethrough.128.png b/app/ui/style/olive-dark/png/text-strikethrough.128.png new file mode 100644 index 000000000..160d5a1db Binary files /dev/null and b/app/ui/style/olive-dark/png/text-strikethrough.128.png differ diff --git a/app/ui/style/olive-dark/png/text-strikethrough.16.disabled.png b/app/ui/style/olive-dark/png/text-strikethrough.16.disabled.png new file mode 100644 index 000000000..81008bbe2 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-strikethrough.16.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-strikethrough.16.png b/app/ui/style/olive-dark/png/text-strikethrough.16.png new file mode 100644 index 000000000..b385f9bf6 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-strikethrough.16.png differ diff --git a/app/ui/style/olive-dark/png/text-strikethrough.32.disabled.png b/app/ui/style/olive-dark/png/text-strikethrough.32.disabled.png new file mode 100644 index 000000000..69ddfaf84 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-strikethrough.32.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-strikethrough.32.png b/app/ui/style/olive-dark/png/text-strikethrough.32.png new file mode 100644 index 000000000..26a3bca03 Binary files /dev/null and b/app/ui/style/olive-dark/png/text-strikethrough.32.png differ diff --git a/app/ui/style/olive-dark/png/text-strikethrough.64.disabled.png b/app/ui/style/olive-dark/png/text-strikethrough.64.disabled.png new file mode 100644 index 000000000..9e4a15cdc Binary files /dev/null and b/app/ui/style/olive-dark/png/text-strikethrough.64.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-strikethrough.64.png b/app/ui/style/olive-dark/png/text-strikethrough.64.png new file mode 100644 index 000000000..a5009da5e Binary files /dev/null and b/app/ui/style/olive-dark/png/text-strikethrough.64.png differ diff --git a/app/ui/style/olive-dark/png/text-underline.128.disabled.png b/app/ui/style/olive-dark/png/text-underline.128.disabled.png index 15bab9d97..06bb1cb3d 100644 Binary files a/app/ui/style/olive-dark/png/text-underline.128.disabled.png and b/app/ui/style/olive-dark/png/text-underline.128.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-underline.128.png b/app/ui/style/olive-dark/png/text-underline.128.png index 8ebd4ab8d..8dd2db527 100644 Binary files a/app/ui/style/olive-dark/png/text-underline.128.png and b/app/ui/style/olive-dark/png/text-underline.128.png differ diff --git a/app/ui/style/olive-dark/png/text-underline.16.disabled.png b/app/ui/style/olive-dark/png/text-underline.16.disabled.png index d22caf8b1..7ffee0088 100644 Binary files a/app/ui/style/olive-dark/png/text-underline.16.disabled.png and b/app/ui/style/olive-dark/png/text-underline.16.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-underline.16.png b/app/ui/style/olive-dark/png/text-underline.16.png index d48167781..3f94216a0 100644 Binary files a/app/ui/style/olive-dark/png/text-underline.16.png and b/app/ui/style/olive-dark/png/text-underline.16.png differ diff --git a/app/ui/style/olive-dark/png/text-underline.32.disabled.png b/app/ui/style/olive-dark/png/text-underline.32.disabled.png index f8f958b5d..f3db85a0d 100644 Binary files a/app/ui/style/olive-dark/png/text-underline.32.disabled.png and b/app/ui/style/olive-dark/png/text-underline.32.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-underline.32.png b/app/ui/style/olive-dark/png/text-underline.32.png index c378d7f20..52ab2ca1a 100644 Binary files a/app/ui/style/olive-dark/png/text-underline.32.png and b/app/ui/style/olive-dark/png/text-underline.32.png differ diff --git a/app/ui/style/olive-dark/png/text-underline.64.disabled.png b/app/ui/style/olive-dark/png/text-underline.64.disabled.png index 74eb19fdd..aadca969f 100644 Binary files a/app/ui/style/olive-dark/png/text-underline.64.disabled.png and b/app/ui/style/olive-dark/png/text-underline.64.disabled.png differ diff --git a/app/ui/style/olive-dark/png/text-underline.64.png b/app/ui/style/olive-dark/png/text-underline.64.png index 14b9eac39..f28eb381d 100644 Binary files a/app/ui/style/olive-dark/png/text-underline.64.png and b/app/ui/style/olive-dark/png/text-underline.64.png differ diff --git a/app/ui/style/olive-dark/png/track-tool.128.disabled.png b/app/ui/style/olive-dark/png/track-tool.128.disabled.png new file mode 100644 index 000000000..b1e9aad3a Binary files /dev/null and b/app/ui/style/olive-dark/png/track-tool.128.disabled.png differ diff --git a/app/ui/style/olive-dark/png/track-tool.128.png b/app/ui/style/olive-dark/png/track-tool.128.png new file mode 100644 index 000000000..f3901941e Binary files /dev/null and b/app/ui/style/olive-dark/png/track-tool.128.png differ diff --git a/app/ui/style/olive-dark/png/track-tool.16.disabled.png b/app/ui/style/olive-dark/png/track-tool.16.disabled.png new file mode 100644 index 000000000..bd3c9bf7e Binary files /dev/null and b/app/ui/style/olive-dark/png/track-tool.16.disabled.png differ diff --git a/app/ui/style/olive-dark/png/track-tool.16.png b/app/ui/style/olive-dark/png/track-tool.16.png new file mode 100644 index 000000000..0aff1afa0 Binary files /dev/null and b/app/ui/style/olive-dark/png/track-tool.16.png differ diff --git a/app/ui/style/olive-dark/png/track-tool.32.disabled.png b/app/ui/style/olive-dark/png/track-tool.32.disabled.png new file mode 100644 index 000000000..460050257 Binary files /dev/null and b/app/ui/style/olive-dark/png/track-tool.32.disabled.png differ diff --git a/app/ui/style/olive-dark/png/track-tool.32.png b/app/ui/style/olive-dark/png/track-tool.32.png new file mode 100644 index 000000000..8f214b0f6 Binary files /dev/null and b/app/ui/style/olive-dark/png/track-tool.32.png differ diff --git a/app/ui/style/olive-dark/png/track-tool.64.disabled.png b/app/ui/style/olive-dark/png/track-tool.64.disabled.png new file mode 100644 index 000000000..bf2010d5a Binary files /dev/null and b/app/ui/style/olive-dark/png/track-tool.64.disabled.png differ diff --git a/app/ui/style/olive-dark/png/track-tool.64.png b/app/ui/style/olive-dark/png/track-tool.64.png new file mode 100644 index 000000000..4e761d4e1 Binary files /dev/null and b/app/ui/style/olive-dark/png/track-tool.64.png differ diff --git a/app/ui/style/olive-dark/svg/text-small-caps.svg b/app/ui/style/olive-dark/svg/text-small-caps.svg new file mode 100644 index 000000000..b9ec71364 --- /dev/null +++ b/app/ui/style/olive-dark/svg/text-small-caps.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-dark/svg/text-strikethrough.svg b/app/ui/style/olive-dark/svg/text-strikethrough.svg new file mode 100644 index 000000000..c45927743 --- /dev/null +++ b/app/ui/style/olive-dark/svg/text-strikethrough.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-dark/svg/text-underline.svg b/app/ui/style/olive-dark/svg/text-underline.svg index dc1cc0eba..1ce678e44 100644 --- a/app/ui/style/olive-dark/svg/text-underline.svg +++ b/app/ui/style/olive-dark/svg/text-underline.svg @@ -1,7 +1,7 @@ - - - - - - - - - - - - - - image/svg+xml - - - - Martin Ruskov - - - http://commons.wikimedia.org/wiki/Tango_icon - - - - - - - - - - - - - + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + - + y="-62.000004" + transform="rotate(90)" + inkscape:path-effect="#path-effect2895" + d="m 43,-62.000004 c 1.656881,0 3,1.343146 3,3 V -5 c 0,1.6568542 -1.343146,3 -3,3 -1.656881,0 -3,-1.3431458 -3,-3 v -54.000004 c 0,-1.656854 1.343146,-3 3,-3 z" + sodipodi:type="rect" /> diff --git a/app/ui/style/olive-dark/svg/track-tool.svg b/app/ui/style/olive-dark/svg/track-tool.svg new file mode 100644 index 000000000..56e84ff1e --- /dev/null +++ b/app/ui/style/olive-dark/svg/track-tool.svg @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/png/text-small-caps.128.disabled.png b/app/ui/style/olive-light/png/text-small-caps.128.disabled.png new file mode 100644 index 000000000..a8eba7215 Binary files /dev/null and b/app/ui/style/olive-light/png/text-small-caps.128.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-small-caps.128.png b/app/ui/style/olive-light/png/text-small-caps.128.png new file mode 100644 index 000000000..e4abf3abb Binary files /dev/null and b/app/ui/style/olive-light/png/text-small-caps.128.png differ diff --git a/app/ui/style/olive-light/png/text-small-caps.16.disabled.png b/app/ui/style/olive-light/png/text-small-caps.16.disabled.png new file mode 100644 index 000000000..7cf071540 Binary files /dev/null and b/app/ui/style/olive-light/png/text-small-caps.16.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-small-caps.16.png b/app/ui/style/olive-light/png/text-small-caps.16.png new file mode 100644 index 000000000..1f3c05233 Binary files /dev/null and b/app/ui/style/olive-light/png/text-small-caps.16.png differ diff --git a/app/ui/style/olive-light/png/text-small-caps.32.disabled.png b/app/ui/style/olive-light/png/text-small-caps.32.disabled.png new file mode 100644 index 000000000..4ef9f9eac Binary files /dev/null and b/app/ui/style/olive-light/png/text-small-caps.32.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-small-caps.32.png b/app/ui/style/olive-light/png/text-small-caps.32.png new file mode 100644 index 000000000..fce1b4002 Binary files /dev/null and b/app/ui/style/olive-light/png/text-small-caps.32.png differ diff --git a/app/ui/style/olive-light/png/text-small-caps.64.disabled.png b/app/ui/style/olive-light/png/text-small-caps.64.disabled.png new file mode 100644 index 000000000..d32d95d97 Binary files /dev/null and b/app/ui/style/olive-light/png/text-small-caps.64.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-small-caps.64.png b/app/ui/style/olive-light/png/text-small-caps.64.png new file mode 100644 index 000000000..e7f72cb70 Binary files /dev/null and b/app/ui/style/olive-light/png/text-small-caps.64.png differ diff --git a/app/ui/style/olive-light/png/text-strikethrough.128.disabled.png b/app/ui/style/olive-light/png/text-strikethrough.128.disabled.png new file mode 100644 index 000000000..9480bbfd8 Binary files /dev/null and b/app/ui/style/olive-light/png/text-strikethrough.128.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-strikethrough.128.png b/app/ui/style/olive-light/png/text-strikethrough.128.png new file mode 100644 index 000000000..789c9ab2f Binary files /dev/null and b/app/ui/style/olive-light/png/text-strikethrough.128.png differ diff --git a/app/ui/style/olive-light/png/text-strikethrough.16.disabled.png b/app/ui/style/olive-light/png/text-strikethrough.16.disabled.png new file mode 100644 index 000000000..1027e6ac3 Binary files /dev/null and b/app/ui/style/olive-light/png/text-strikethrough.16.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-strikethrough.16.png b/app/ui/style/olive-light/png/text-strikethrough.16.png new file mode 100644 index 000000000..8c738fbb4 Binary files /dev/null and b/app/ui/style/olive-light/png/text-strikethrough.16.png differ diff --git a/app/ui/style/olive-light/png/text-strikethrough.32.disabled.png b/app/ui/style/olive-light/png/text-strikethrough.32.disabled.png new file mode 100644 index 000000000..8bfde72af Binary files /dev/null and b/app/ui/style/olive-light/png/text-strikethrough.32.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-strikethrough.32.png b/app/ui/style/olive-light/png/text-strikethrough.32.png new file mode 100644 index 000000000..11faf6979 Binary files /dev/null and b/app/ui/style/olive-light/png/text-strikethrough.32.png differ diff --git a/app/ui/style/olive-light/png/text-strikethrough.64.disabled.png b/app/ui/style/olive-light/png/text-strikethrough.64.disabled.png new file mode 100644 index 000000000..b41aa1fb2 Binary files /dev/null and b/app/ui/style/olive-light/png/text-strikethrough.64.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-strikethrough.64.png b/app/ui/style/olive-light/png/text-strikethrough.64.png new file mode 100644 index 000000000..889d5c67e Binary files /dev/null and b/app/ui/style/olive-light/png/text-strikethrough.64.png differ diff --git a/app/ui/style/olive-light/png/text-underline.128.disabled.png b/app/ui/style/olive-light/png/text-underline.128.disabled.png index c418f8363..91450be78 100644 Binary files a/app/ui/style/olive-light/png/text-underline.128.disabled.png and b/app/ui/style/olive-light/png/text-underline.128.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-underline.128.png b/app/ui/style/olive-light/png/text-underline.128.png index 8ac50d833..3eced673c 100644 Binary files a/app/ui/style/olive-light/png/text-underline.128.png and b/app/ui/style/olive-light/png/text-underline.128.png differ diff --git a/app/ui/style/olive-light/png/text-underline.16.disabled.png b/app/ui/style/olive-light/png/text-underline.16.disabled.png index 2f76da2f8..6e64c9f87 100644 Binary files a/app/ui/style/olive-light/png/text-underline.16.disabled.png and b/app/ui/style/olive-light/png/text-underline.16.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-underline.16.png b/app/ui/style/olive-light/png/text-underline.16.png index 19c1473a2..6dd6ca8d5 100644 Binary files a/app/ui/style/olive-light/png/text-underline.16.png and b/app/ui/style/olive-light/png/text-underline.16.png differ diff --git a/app/ui/style/olive-light/png/text-underline.32.disabled.png b/app/ui/style/olive-light/png/text-underline.32.disabled.png index 729a56f4b..98542afd7 100644 Binary files a/app/ui/style/olive-light/png/text-underline.32.disabled.png and b/app/ui/style/olive-light/png/text-underline.32.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-underline.32.png b/app/ui/style/olive-light/png/text-underline.32.png index 04bec5131..c6f100182 100644 Binary files a/app/ui/style/olive-light/png/text-underline.32.png and b/app/ui/style/olive-light/png/text-underline.32.png differ diff --git a/app/ui/style/olive-light/png/text-underline.64.disabled.png b/app/ui/style/olive-light/png/text-underline.64.disabled.png index 73a37cc65..343b9bfad 100644 Binary files a/app/ui/style/olive-light/png/text-underline.64.disabled.png and b/app/ui/style/olive-light/png/text-underline.64.disabled.png differ diff --git a/app/ui/style/olive-light/png/text-underline.64.png b/app/ui/style/olive-light/png/text-underline.64.png index 372aac286..5e5601379 100644 Binary files a/app/ui/style/olive-light/png/text-underline.64.png and b/app/ui/style/olive-light/png/text-underline.64.png differ diff --git a/app/ui/style/olive-light/png/track-tool.128.disabled.png b/app/ui/style/olive-light/png/track-tool.128.disabled.png new file mode 100644 index 000000000..b96770b4c Binary files /dev/null and b/app/ui/style/olive-light/png/track-tool.128.disabled.png differ diff --git a/app/ui/style/olive-light/png/track-tool.128.png b/app/ui/style/olive-light/png/track-tool.128.png new file mode 100644 index 000000000..77a7f4fd8 Binary files /dev/null and b/app/ui/style/olive-light/png/track-tool.128.png differ diff --git a/app/ui/style/olive-light/png/track-tool.16.disabled.png b/app/ui/style/olive-light/png/track-tool.16.disabled.png new file mode 100644 index 000000000..7cbeecef8 Binary files /dev/null and b/app/ui/style/olive-light/png/track-tool.16.disabled.png differ diff --git a/app/ui/style/olive-light/png/track-tool.16.png b/app/ui/style/olive-light/png/track-tool.16.png new file mode 100644 index 000000000..b54876c1f Binary files /dev/null and b/app/ui/style/olive-light/png/track-tool.16.png differ diff --git a/app/ui/style/olive-light/png/track-tool.32.disabled.png b/app/ui/style/olive-light/png/track-tool.32.disabled.png new file mode 100644 index 000000000..7f83119eb Binary files /dev/null and b/app/ui/style/olive-light/png/track-tool.32.disabled.png differ diff --git a/app/ui/style/olive-light/png/track-tool.32.png b/app/ui/style/olive-light/png/track-tool.32.png new file mode 100644 index 000000000..ea58f94cf Binary files /dev/null and b/app/ui/style/olive-light/png/track-tool.32.png differ diff --git a/app/ui/style/olive-light/png/track-tool.64.disabled.png b/app/ui/style/olive-light/png/track-tool.64.disabled.png new file mode 100644 index 000000000..425f68053 Binary files /dev/null and b/app/ui/style/olive-light/png/track-tool.64.disabled.png differ diff --git a/app/ui/style/olive-light/png/track-tool.64.png b/app/ui/style/olive-light/png/track-tool.64.png new file mode 100644 index 000000000..672e0fab2 Binary files /dev/null and b/app/ui/style/olive-light/png/track-tool.64.png differ diff --git a/app/ui/style/olive-light/svg/text-small-caps.svg b/app/ui/style/olive-light/svg/text-small-caps.svg new file mode 100644 index 000000000..429f5b250 --- /dev/null +++ b/app/ui/style/olive-light/svg/text-small-caps.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/svg/text-strikethrough.svg b/app/ui/style/olive-light/svg/text-strikethrough.svg new file mode 100644 index 000000000..ed6c97315 --- /dev/null +++ b/app/ui/style/olive-light/svg/text-strikethrough.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + diff --git a/app/ui/style/olive-light/svg/text-underline.svg b/app/ui/style/olive-light/svg/text-underline.svg index b0b4f89fa..1cfb91d12 100644 --- a/app/ui/style/olive-light/svg/text-underline.svg +++ b/app/ui/style/olive-light/svg/text-underline.svg @@ -1,7 +1,7 @@ - - - - - - - - - - - - - - image/svg+xml - - - - Martin Ruskov - - - http://commons.wikimedia.org/wiki/Tango_icon - - - - - - - - - - - - - + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + - + y="-62.000004" + transform="rotate(90)" + inkscape:path-effect="#path-effect2895" + d="m 43,-62.000004 c 1.656881,0 3,1.343146 3,3 V -5 c 0,1.6568542 -1.343146,3 -3,3 -1.656881,0 -3,-1.3431458 -3,-3 v -54.000004 c 0,-1.656854 1.343146,-3 3,-3 z" + sodipodi:type="rect" /> diff --git a/app/ui/style/olive-light/svg/track-tool.svg b/app/ui/style/olive-light/svg/track-tool.svg new file mode 100644 index 000000000..68fb8ec52 --- /dev/null +++ b/app/ui/style/olive-light/svg/track-tool.svg @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Martin Ruskov + + + http://commons.wikimedia.org/wiki/Tango_icon + + + + + + + + + + + + + + + + + + diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index e756d7376..49748e5d2 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -406,7 +406,7 @@ void CurveView::FirstChanceMouseRelease(QMouseEvent *event) void CurveView::KeyframeDragStart(QMouseEvent *event) { drag_keyframe_values_.resize(GetSelectedKeyframes().size()); - for (int i=0; ivalue(); } @@ -418,7 +418,7 @@ void CurveView::KeyframeDragMove(QMouseEvent *event, QString &tip) { if (event->modifiers() & Qt::ShiftModifier) { // Lock to X axis only and set original values on all keys - for (int i=0; iset_value(drag_keyframe_values_.at(i)); } @@ -429,7 +429,7 @@ void CurveView::KeyframeDragMove(QMouseEvent *event, QString &tip) double scaled_diff = (mapToScene(event->pos()).y() - drag_start_.y()) / GetYScale(); // Validate movement - ensure no keyframe goes above its max point or below its min point - for (int i=0; iset_value(FloatSlider::TransformDisplayToValue(FloatSlider::TransformValueToDisplay(drag_keyframe_values_.at(i).toDouble(), display) - scaled_diff, display)); } - NodeKeyframe *tip_item = GetSelectedKeyframes().first(); + NodeKeyframe *tip_item = GetSelectedKeyframes().front(); bool ok; double num_value = tip_item->value().toDouble(&ok); @@ -472,7 +472,7 @@ void CurveView::KeyframeDragMove(QMouseEvent *event, QString &tip) void CurveView::KeyframeDragRelease(QMouseEvent *event, MultiUndoCommand *command) { - for (int i=0; ivalue().toDouble(), drag_keyframe_values_.at(i).toDouble())) { command->add_child(new NodeParamSetKeyframeValueCommand(k, k->value(), drag_keyframe_values_.at(i))); diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index 4d56896ae..516e4b378 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -31,6 +31,7 @@ #include "common/timecodefunctions.h" #include "node/node.h" #include "widget/keyframeview/keyframeviewundo.h" +#include "widget/timeruler/timeruler.h" namespace olive { @@ -92,6 +93,7 @@ CurveWidget::CurveWidget(QWidget *parent) : view_ = new CurveView(); ConnectTimelineView(view_); + view_->SetSnapService(this); ruler_view_layout->addWidget(view_); layout->addLayout(ruler_view_layout); @@ -251,16 +253,16 @@ void CurveWidget::ConnectInputInternal(Node *node, const QString &input, int ele void CurveWidget::SelectionChanged() { - const QVector &selected = view_->GetSelectedKeyframes(); + const std::vector &selected = view_->GetSelectedKeyframes(); SetKeyframeButtonChecked(false); - SetKeyframeButtonEnabled(!selected.isEmpty()); + SetKeyframeButtonEnabled(!selected.empty()); - if (!selected.isEmpty()) { + if (!selected.empty()) { bool all_same_type = true; - NodeKeyframe::Type type = selected.first()->type(); + NodeKeyframe::Type type = selected.front()->type(); - for (int i=1;i &selected = view_->GetSelectedKeyframes(); - if (selected.isEmpty()) { + const std::vector &selected = view_->GetSelectedKeyframes(); + if (selected.empty()) { return; } diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index ba6d1304b..3fd8ca4fe 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -67,6 +67,16 @@ protected: virtual void ConnectedNodeChangeEvent(ViewerOutput* n) override; + virtual const QVector *GetSnapKeyframes() const override + { + return &view_->GetKeyframeTracks(); + } + + virtual const std::vector *GetSnapIgnoreKeyframes() const override + { + return &view_->GetSelectedKeyframes(); + } + private: void SetKeyframeButtonEnabled(bool enable); diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp index 0d5241e3c..16d845352 100644 --- a/app/widget/keyframeview/keyframeview.cpp +++ b/app/widget/keyframeview/keyframeview.cpp @@ -462,11 +462,11 @@ void KeyframeView::ShowContextMenu() QAction* bezier_key_action = nullptr; QAction* hold_key_action = nullptr; - if (!GetSelectedKeyframes().isEmpty()) { + if (!GetSelectedKeyframes().empty()) { bool all_keys_are_same_type = true; - NodeKeyframe::Type type = GetSelectedKeyframes().first()->type(); + NodeKeyframe::Type type = GetSelectedKeyframes().front()->type(); - for (int i=1;i &GetSelectedKeyframes() const + const std::vector &GetSelectedKeyframes() const { return selection_manager_.GetSelectedObjects(); } + const QVector &GetKeyframeTracks() const + { + return tracks_; + } + virtual void SelectionManagerSelectEvent(void *obj) override; virtual void SelectionManagerDeselectEvent(void *obj) override; diff --git a/app/widget/keyframeview/keyframeviewinputconnection.h b/app/widget/keyframeview/keyframeviewinputconnection.h index b6b0c1da5..5b3275fc0 100644 --- a/app/widget/keyframeview/keyframeviewinputconnection.h +++ b/app/widget/keyframeview/keyframeviewinputconnection.h @@ -50,7 +50,7 @@ public: void SetYBehavior(YBehavior e); - const QVector GetKeyframes() const + const QVector &GetKeyframes() const { return input_.input().node()->GetKeyframeTracks(input_.input()).at(input_.track()); } diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 2aba90ed7..8e3beb82e 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -28,6 +28,8 @@ #include "common/functiontimer.h" #include "common/timecodefunctions.h" #include "node/output/viewer/viewer.h" +#include "widget/nodeview/nodeviewundo.h" +#include "widget/timeruler/timeruler.h" namespace olive { @@ -116,6 +118,7 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : // Create keyframe view keyframe_view_ = new KeyframeView(); keyframe_view_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + keyframe_view_->SetSnapService(this); ConnectTimelineView(keyframe_view_); keyframe_area_layout->addWidget(keyframe_view_); @@ -283,6 +286,16 @@ void NodeParamView::UpdateContexts() void NodeParamView::ItemAboutToBeRemoved(NodeParamViewItem *item) { + if (keyframe_view_) { + for (auto it=item->GetKeyframeConnections().begin(); it!=item->GetKeyframeConnections().end(); it++) { + for (auto jt=it->begin(); jt!=it->end(); jt++) { + for (auto kt=jt->begin(); kt!=jt->end(); kt++) { + keyframe_view_->RemoveKeyframesOfTrack(*kt); + } + } + } + } + if (focused_node_ == item) { focused_node_ = nullptr; emit FocusedNodeChanged(nullptr); @@ -359,8 +372,28 @@ Node *NodeParamView::GetTimeTarget() const void NodeParamView::DeleteSelected() { - if (keyframe_view_) { + if (keyframe_view_ && keyframe_view_->hasFocus()) { keyframe_view_->DeleteSelected(); + } else if (focused_node_) { + MultiUndoCommand *c = new MultiUndoCommand(); + Node *n = focused_node_->GetNode(); + + // Create command to delete node from context and/or graph + NodeViewDeleteCommand *dc = new NodeViewDeleteCommand(); + dc->AddNode(n, focused_node_->GetContext()); + c->add_child(dc); + + // Copy any outputs that were connected + if (n->GetEffectInput().IsValid()) { + if (Node *out = n->GetEffectInput().GetConnectedOutput()) { + for (auto it=n->output_connections().cbegin(); it!=n->output_connections().cend(); it++) { + c->add_child(new NodeEdgeAddCommand(out, it->second)); + } + } + } + + + Core::instance()->undo_stack()->push(c); } } @@ -455,27 +488,6 @@ void NodeParamView::AddNode(Node *n, Node *ctx, NodeParamViewContext *context) } } -void NodeParamView::RemoveNode(Node *n, Node *ctx) -{ - foreach (NodeParamViewContext *ctx_item, context_items_) { - NodeParamViewItem *item = ctx_item->GetItem(n, ctx); - - if (item) { - if (keyframe_view_) { - for (auto it=item->GetKeyframeConnections().begin(); it!=item->GetKeyframeConnections().end(); it++) { - for (auto jt=it->begin(); jt!=it->end(); jt++) { - for (auto kt=jt->begin(); kt!=jt->end(); kt++) { - keyframe_view_->RemoveKeyframesOfTrack(*kt); - } - } - } - } - } - - ctx_item->RemoveNode(n, ctx); - } -} - int GetDistanceBetweenNodes(Node *start, Node *end) { if (start == end) { @@ -675,7 +687,9 @@ void NodeParamView::NodeRemovedFromContext(Node *n) { Node *ctx = static_cast(sender()); - RemoveNode(n, ctx); + foreach (NodeParamViewContext *ctx_item, context_items_) { + ctx_item->RemoveNode(n, ctx); + } if (keyframe_view_) { QueueKeyframePositionUpdate(); diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index 9a9207cb7..c0078b83e 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -88,6 +88,16 @@ protected: virtual void ConnectedNodeChangeEvent(ViewerOutput* n) override; + virtual const QVector *GetSnapKeyframes() const override + { + return keyframe_view_ ? &keyframe_view_->GetKeyframeTracks() : nullptr; + } + + virtual const std::vector *GetSnapIgnoreKeyframes() const override + { + return keyframe_view_ ? &keyframe_view_->GetSelectedKeyframes() : nullptr; + } + private: void UpdateItemTime(const rational &time); @@ -99,8 +109,6 @@ private: void AddNode(Node* n, Node *ctx, NodeParamViewContext *context); - void RemoveNode(Node *n, Node *ctx); - void SortItemsInContext(NodeParamViewContext *context); NodeParamViewContext *GetContextItemFromContext(Node *context); diff --git a/app/widget/nodeparamview/nodeparamviewcontext.cpp b/app/widget/nodeparamview/nodeparamviewcontext.cpp index a07c3975b..8fe70ca7a 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.cpp +++ b/app/widget/nodeparamview/nodeparamviewcontext.cpp @@ -23,6 +23,8 @@ #include #include "node/block/clip/clip.h" +#include "node/factory.h" +#include "widget/nodeview/nodeviewundo.h" namespace olive { @@ -130,7 +132,45 @@ void NodeParamViewContext::Retranslate() void NodeParamViewContext::AddEffectButtonClicked() { - QMessageBox::information(this, tr("STUB"), tr("This feature is coming soon. Thanks for testing development builds of Olive :)")); + Menu *m = NodeFactory::CreateMenu(this, false, Node::kCategoryUnknown, Node::kVideoEffect); + connect(m, &Menu::triggered, this, &NodeParamViewContext::AddEffectMenuItemTriggered); + m->exec(QCursor::pos()); + delete m; +} + +void NodeParamViewContext::AddEffectMenuItemTriggered(QAction *a) +{ + Node *n = NodeFactory::CreateFromMenuAction(a); + + if (n) { + NodeInput new_node_input = n->GetEffectInput(); + MultiUndoCommand *command = new MultiUndoCommand(); + + QVector graphs_added_to; + + foreach (Node *ctx, contexts_) { + NodeInput ctx_input = ctx->GetEffectInput(); + + if (!graphs_added_to.contains(ctx->parent())) { + command->add_child(new NodeAddCommand(ctx->parent(), n)); + graphs_added_to.append(ctx->parent()); + } + + command->add_child(new NodeSetPositionCommand(n, ctx, ctx->GetNodePositionInContext(ctx))); + command->add_child(new NodeSetPositionCommand(ctx, ctx, ctx->GetNodePositionInContext(ctx) + QPointF(1, 0))); + + if (ctx_input.IsConnected()) { + Node *prev_output = ctx_input.GetConnectedOutput(); + + command->add_child(new NodeEdgeRemoveCommand(prev_output, ctx_input)); + command->add_child(new NodeEdgeAddCommand(prev_output, new_node_input)); + } + + command->add_child(new NodeEdgeAddCommand(n, ctx_input)); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); + } } } diff --git a/app/widget/nodeparamview/nodeparamviewcontext.h b/app/widget/nodeparamview/nodeparamviewcontext.h index 5652277e9..29be6dd58 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.h +++ b/app/widget/nodeparamview/nodeparamviewcontext.h @@ -91,6 +91,8 @@ private: private slots: void AddEffectButtonClicked(); + void AddEffectMenuItemTriggered(QAction *a); + }; } diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 7fedb2fea..9a61e0718 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -63,6 +63,11 @@ NodeParamViewItem::NodeParamViewItem(Node *node, NodeParamViewCheckBoxBehavior c setBackgroundRole(QPalette::Window); + // Connect title bar enabled checkbox + //title_bar()->SetEnabledCheckBoxVisible(true); + //title_bar()->SetEnabledCheckBoxChecked(node_->IsEnabled()); + //connect(title_bar(), &NodeParamViewItemTitleBar::EnabledCheckBoxClicked, node_, &Node::SetEnabled); + Retranslate(); } diff --git a/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp b/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp index 6d61bd1ba..1d79ff4d9 100644 --- a/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp +++ b/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp @@ -56,6 +56,11 @@ NodeParamViewItemTitleBar::NodeParamViewItemTitleBar(QWidget *parent) : pin_btn_->setVisible(false); layout->addWidget(pin_btn_); connect(pin_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::PinToggled); + + enabled_checkbox_ = new QCheckBox(); + enabled_checkbox_->setVisible(false); + layout->addWidget(enabled_checkbox_); + connect(enabled_checkbox_, &QCheckBox::clicked, this, &NodeParamViewItemTitleBar::EnabledCheckBoxClicked); } void NodeParamViewItemTitleBar::SetExpanded(bool e) diff --git a/app/widget/nodeparamview/nodeparamviewitemtitlebar.h b/app/widget/nodeparamview/nodeparamviewitemtitlebar.h index 7ddaac1aa..9bea7f06f 100644 --- a/app/widget/nodeparamview/nodeparamviewitemtitlebar.h +++ b/app/widget/nodeparamview/nodeparamviewitemtitlebar.h @@ -21,6 +21,7 @@ #ifndef NODEPARAMVIEWITEMTITLEBAR_H #define NODEPARAMVIEWITEMTITLEBAR_H +#include #include #include @@ -59,6 +60,16 @@ public slots: add_fx_btn_->setVisible(e); } + void SetEnabledCheckBoxVisible(bool e) + { + enabled_checkbox_->setVisible(e); + } + + void SetEnabledCheckBoxChecked(bool e) + { + enabled_checkbox_->setChecked(e); + } + signals: void ExpandedStateChanged(bool e); @@ -66,6 +77,8 @@ signals: void AddEffectButtonClicked(); + void EnabledCheckBoxClicked(bool e); + protected: virtual void paintEvent(QPaintEvent *event) override; @@ -82,6 +95,8 @@ private: QPushButton *add_fx_btn_; + QCheckBox *enabled_checkbox_; + }; } diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 9105d8cb1..07251995d 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -32,6 +32,7 @@ #include "node/distort/transform/transformdistortnode.h" #include "node/factory.h" #include "node/group/group.h" +#include "node/project/serializer/serializer.h" #include "node/traverser.h" #include "ui/icons/icons.h" #include "widget/menu/menushared.h" @@ -203,7 +204,31 @@ void NodeView::CopySelected(bool cut) return; } - CopyNodesToClipboard(selected_nodes_); + QString copy_str; + QXmlStreamWriter writer(©_str); + + ProjectSerializer::SaveData sdata(selected_nodes_.first()->project()); + sdata.SetOnlySerializeNodesAndResolveGroups(selected_nodes_); + + ProjectSerializer::SerializedProperties properties; + + for (Node *n : selected_nodes_) { + NodeViewItem *item = GetAssumedItemForSelectedNode(n); + + if (item) { + Node::Position pos = item->GetNodePositionData(); + + properties[n][QStringLiteral("x")] = QString::number(pos.position.x()); + properties[n][QStringLiteral("y")] = QString::number(pos.position.y()); + properties[n][QStringLiteral("expanded")] = QString::number(pos.expanded); + } + } + + sdata.SetProperties(properties); + + ProjectSerializer::Save(&writer, sdata, QStringLiteral("nodes")); + + Core::CopyStringToClipboard(copy_str); if (cut) { DeleteSelected(); @@ -212,9 +237,31 @@ void NodeView::CopySelected(bool cut) void NodeView::Paste() { - if (!contexts_.isEmpty()) { - PasteNodesFromClipboard(); + if (contexts_.isEmpty()) { + return; } + + ProjectSerializer::Result res = ProjectSerializer::Paste(QStringLiteral("nodes")); + if (res.GetLoadedNodes().isEmpty()) { + return; + } + + Node::PositionMap map; + + for (auto it=res.GetLoadData().properties.cbegin(); it!=res.GetLoadData().properties.cend(); it++) { + Node::Position pos; + + const QMap &node_props = it.value(); + pos.position.setX(node_props.value(QStringLiteral("x")).toDouble()); + pos.position.setY(node_props.value(QStringLiteral("y")).toDouble()); + pos.expanded = node_props.value(QStringLiteral("expanded")).toDouble(); + + qDebug() << it.key() << pos.position; + + map.insert(it.key(), pos); + } + + PostPaste(res.GetLoadedNodes(), map); } void NodeView::Duplicate() @@ -450,8 +497,10 @@ void NodeView::mousePressEvent(QMouseEvent *event) } } - // Default QGraphicsView functionality (selecting, dragging, etc.) - super::mousePressEvent(event); + if (attached_items_.isEmpty()) { + // Default QGraphicsView functionality (selecting, dragging, etc.) + super::mousePressEvent(event); + } // For any selected item, store its position in case the user is dragging it somewhere else auto selected_items = scene_.GetSelectedItems(); @@ -472,7 +521,9 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) return; } - super::mouseMoveEvent(event); + if (attached_items_.isEmpty()) { + super::mouseMoveEvent(event); + } // See if there are any items attached if (!attached_items_.isEmpty()) { @@ -499,26 +550,26 @@ void NodeView::mouseMoveEvent(QMouseEvent *event) if (new_drop_edge) { drop_input_.Reset(); - NodeValue::Type drop_edge_data_type = NodeValue::kNone; + NodeValue::Type drop_edge_data_type = new_drop_edge->input().GetDataType(); - // Run the Node and determine what type is being used - NodeTraverser traverser; - NodeValue drop_edge_value = traverser.GenerateRow(new_drop_edge->output(), TimeRange(0, 0))[new_drop_edge->input().input()]; - drop_edge_data_type = drop_edge_value.type(); + // Determine best input to connect to our new node + if (attached_node->GetEffectInput().IsValid()) { + // If node specifies an effect input, use that immediately + drop_input_ = attached_node->GetEffectInput(); + } else { + // Otherwise, we may have to iterate to find a valid one + for (const QString& input : attached_node->inputs()) { + NodeInput i(attached_node, input); - // Iterate through the inputs of our dragging node and see if our node has any acceptable - // inputs to connect to for this type - for (const QString& input : attached_node->inputs()) { - NodeInput i(attached_node, input); - - if (attached_node->IsInputConnectable(input)) { - if (attached_node->GetInputDataType(input) == drop_edge_data_type) { - // Found exactly the type we're looking for, set and break this loop - drop_input_ = i; - break; - } else if (!drop_input_.IsValid()) { - // Default to first connectable input - drop_input_ = i; + if (attached_node->IsInputConnectable(input)) { + if (attached_node->GetInputDataType(input) == drop_edge_data_type) { + // Found exactly the type we're looking for, set and break this loop + drop_input_ = i; + break; + } else if (!drop_input_.IsValid()) { + // Default to first connectable input + drop_input_ = i; + } } } } @@ -559,6 +610,8 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) Node *select_context = nullptr; QVector select_nodes; + bool had_attached_items = !attached_items_.isEmpty(); + if (!attached_items_.isEmpty()) { select_context = nullptr; @@ -642,7 +695,9 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) Core::instance()->undo_stack()->pushIfHasChildren(command); - super::mouseReleaseEvent(event); + if (!had_attached_items) { + super::mouseReleaseEvent(event); + } if (select_context) { scene_.context_map().value(select_context)->Select(select_nodes); @@ -998,43 +1053,6 @@ bool NodeView::eventFilter(QObject *object, QEvent *event) return super::eventFilter(object, event); } -void NodeView::CopyNodesToClipboardCallback(const QVector &nodes, ProjectSerializer::SaveData *sdata, void *userdata) -{ - ProjectSerializer::SerializedProperties properties; - - for (Node *n : nodes) { - NodeViewItem *item = GetAssumedItemForSelectedNode(n); - - if (item) { - Node::Position pos = item->GetNodePositionData(); - - properties[n][QStringLiteral("x")] = QString::number(pos.position.x()); - properties[n][QStringLiteral("y")] = QString::number(pos.position.y()); - properties[n][QStringLiteral("expanded")] = QString::number(pos.expanded); - } - } - - sdata->SetProperties(properties); -} - -void NodeView::PasteNodesToClipboardCallback(const QVector &nodes, const ProjectSerializer::LoadData &ldata, void *userdata) -{ - Node::PositionMap map; - - for (auto it=ldata.properties.cbegin(); it!=ldata.properties.cend(); it++) { - Node::Position pos; - - const QMap &node_props = it.value(); - pos.position.setX(node_props.value(QStringLiteral("x")).toDouble()); - pos.position.setY(node_props.value(QStringLiteral("y")).toDouble()); - pos.expanded = node_props.value(QStringLiteral("expanded")).toDouble(); - - map.insert(it.key(), pos); - } - - PostPaste(nodes, map); -} - void NodeView::changeEvent(QEvent *e) { // Add translation code @@ -1539,7 +1557,7 @@ void NodeView::PostPaste(const QVector &new_nodes, const Node::PositionM AttachedItem &ai = new_attached[i]; if (ai.item) { - ai.original_pos = first_item->pos() - ai.item->pos(); + ai.original_pos = ai.item->pos() - first_item->pos(); } } } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 3c62818db..26894511e 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -26,7 +26,6 @@ #include "core.h" #include "node/graph.h" -#include "node/nodecopypaste.h" #include "nodeviewedge.h" #include "nodeviewcontext.h" #include "nodeviewminimap.h" @@ -42,7 +41,7 @@ namespace olive { * This widget takes a NodeGraph object and constructs a QGraphicsScene representing its data, viewing and allowing * the user to make modifications to it. */ -class NodeView : public HandMovableView, public NodeCopyPasteService +class NodeView : public HandMovableView { Q_OBJECT public: @@ -139,9 +138,6 @@ protected: virtual bool eventFilter(QObject *object, QEvent *event) override; - virtual void CopyNodesToClipboardCallback(const QVector &nodes, ProjectSerializer::SaveData *data, void* userdata) override; - virtual void PasteNodesToClipboardCallback(const QVector &nodes, const ProjectSerializer::LoadData &ldata, void *userdata) override; - virtual void changeEvent(QEvent *e) override; private: diff --git a/app/widget/playbackcontrols/playbackcontrols.cpp b/app/widget/playbackcontrols/playbackcontrols.cpp index db318ab98..6a0c7ab6d 100644 --- a/app/widget/playbackcontrols/playbackcontrols.cpp +++ b/app/widget/playbackcontrols/playbackcontrols.cpp @@ -55,6 +55,7 @@ PlaybackControls::PlaybackControls(QWidget *parent) : cur_tc_lbl_ = new RationalSlider(); cur_tc_lbl_->SetDisplayType(RationalSlider::kTime); + cur_tc_lbl_->SetMinimum(0); connect(cur_tc_lbl_, &RationalSlider::ValueChanged, this, &PlaybackControls::TimeChanged); lower_left_layout->addWidget(cur_tc_lbl_); lower_left_layout->addStretch(); @@ -155,6 +156,10 @@ PlaybackControls::PlaybackControls(QWidget *parent) : SetAudioVideoDragButtonsVisible(false); connect(Core::instance(), &Core::TimecodeDisplayChanged, this, &PlaybackControls::TimecodeChanged); + + play_blink_timer_ = new QTimer(this); + play_blink_timer_->setInterval(500); + connect(play_blink_timer_, &QTimer::timeout, this, &PlaybackControls::PlayBlink); } void PlaybackControls::SetTimecodeEnabled(bool enabled) @@ -230,10 +235,20 @@ void PlaybackControls::UpdateIcons() audio_drag_btn_->setIcon(icon::Audio); } +void PlaybackControls::SetButtonRecordingState(QPushButton *btn, bool on) +{ + btn->setStyleSheet(on ? QStringLiteral("background: red;") : QString()); +} + void PlaybackControls::TimecodeChanged() { // Update end time SetEndTime(end_time_); } +void PlaybackControls::PlayBlink() +{ + SetButtonRecordingState(play_btn_, play_btn_->styleSheet().isEmpty()); +} + } diff --git a/app/widget/playbackcontrols/playbackcontrols.h b/app/widget/playbackcontrols/playbackcontrols.h index b1d283f56..f467a1924 100644 --- a/app/widget/playbackcontrols/playbackcontrols.h +++ b/app/widget/playbackcontrols/playbackcontrols.h @@ -61,6 +61,23 @@ public slots: void ShowPlayButton(); + void StartPlayBlink() + { + play_blink_timer_->start(); + SetButtonRecordingState(play_btn_, true); + } + + void StopPlayBlink() + { + play_blink_timer_->stop(); + SetButtonRecordingState(play_btn_, false); + } + + void SetPauseButtonRecordingState(bool on) + { + SetButtonRecordingState(pause_btn_, on); + } + signals: /** * @brief Signal emitted when "Go to Start" is clicked @@ -108,6 +125,8 @@ protected: private: void UpdateIcons(); + static void SetButtonRecordingState(QPushButton *btn, bool on); + QWidget* lower_left_container_; QWidget* lower_right_container_; @@ -129,9 +148,13 @@ private: QStackedWidget* playpause_stack_; + QTimer *play_blink_timer_; + private slots: void TimecodeChanged(); + void PlayBlink(); + }; } diff --git a/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp b/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp index f928779bf..0b06ac1bc 100644 --- a/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp +++ b/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp @@ -25,6 +25,8 @@ #include #include +#include "ui/colorcoding.h" + namespace olive { ResizableTimelineScrollBar::ResizableTimelineScrollBar(QWidget* parent) : @@ -48,6 +50,7 @@ void ResizableTimelineScrollBar::ConnectTimelinePoints(TimelinePoints *points) disconnect(points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&ResizableTimelineScrollBar::update)); disconnect(points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&ResizableTimelineScrollBar::update)); disconnect(points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&ResizableTimelineScrollBar::update)); + disconnect(points_->markers(), &TimelineMarkerList::MarkerModified, this, static_cast(&ResizableTimelineScrollBar::update)); } points_ = points; @@ -57,6 +60,7 @@ void ResizableTimelineScrollBar::ConnectTimelinePoints(TimelinePoints *points) connect(points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&ResizableTimelineScrollBar::update)); connect(points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&ResizableTimelineScrollBar::update)); connect(points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&ResizableTimelineScrollBar::update)); + connect(points_->markers(), &TimelineMarkerList::MarkerModified, this, static_cast(&ResizableTimelineScrollBar::update)); } update(); @@ -75,7 +79,7 @@ void ResizableTimelineScrollBar::paintEvent(QPaintEvent *event) if (points_ && !timebase().isNull() - && (points_->workarea()->enabled() || !points_->markers()->list().isEmpty())) { + && (points_->workarea()->enabled() || !points_->markers()->empty())) { QStyleOptionSlider opt; initStyleOption(&opt); @@ -108,11 +112,13 @@ void ResizableTimelineScrollBar::paintEvent(QPaintEvent *event) workarea_color); } - if (!points_->markers()->list().isEmpty()) { - QColor marker_color(0, 255, 0, 128); - foreach (TimelineMarker* marker, points_->markers()->list()) { - int64_t in = qRound64(ratio * TimeToScene(marker->time().in())); - int64_t out = qRound64(ratio * TimeToScene(marker->time().out())); + if (!points_->markers()->empty()) { + for (auto it=points_->markers()->cbegin(); it!=points_->markers()->cend(); it++) { + TimelineMarker* marker = *it; + + QColor marker_color = ColorCoding::GetColor(marker->color()).toQColor(); + int64_t in = qRound64(ratio * TimeToScene(marker->time_range().in())); + int64_t out = qRound64(ratio * TimeToScene(marker->time_range().out())); int64_t length = qMax(int64_t(1), out-in); p.fillRect(gr.x() + in, diff --git a/app/widget/snapservice/snapservice.cpp b/app/widget/snapservice/snapservice.cpp deleted file mode 100644 index b3a853f1e..000000000 --- a/app/widget/snapservice/snapservice.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "snapservice.h" diff --git a/app/widget/snapservice/snapservice.h b/app/widget/snapservice/snapservice.h deleted file mode 100644 index ad83fb3c9..000000000 --- a/app/widget/snapservice/snapservice.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef SNAPSERVICE_H -#define SNAPSERVICE_H - -#include "common/rational.h" - -namespace olive { - -class SnapService -{ -public: - SnapService() = default; - - enum SnapPoints { - kSnapToClips = 0x1, - kSnapToPlayhead = 0x2, - kSnapToMarkers = 0x4, - kSnapAll = 0xFF - }; - - /** - * @brief Snaps point `start_point` that is moving by `movement` to currently existing clips - */ - virtual bool SnapPoint(QVector start_times, rational *movement, int snap_points = kSnapAll) = 0; - - virtual void HideSnaps() = 0; - -}; - -} - -#endif // SNAPSERVICE_H diff --git a/app/widget/timebased/timebasedview.cpp b/app/widget/timebased/timebasedview.cpp index 05a749976..dc5e68cd8 100644 --- a/app/widget/timebased/timebasedview.cpp +++ b/app/widget/timebased/timebasedview.cpp @@ -26,6 +26,7 @@ #include #include "common/timecodefunctions.h" +#include "widget/timebased/timebasedwidget.h" namespace olive { @@ -63,7 +64,7 @@ void TimeBasedView::TimebaseChangedEvent(const rational &) viewport()->update(); } -void TimeBasedView::EnableSnap(const QVector &points) +void TimeBasedView::EnableSnap(const std::vector &points) { snapped_ = true; snap_time_ = points; @@ -78,11 +79,6 @@ void TimeBasedView::DisableSnap() viewport()->update(); } -void TimeBasedView::SetSnapService(SnapService *service) -{ - snap_service_ = service; -} - const double &TimeBasedView::GetYScale() const { return y_scale_; @@ -211,7 +207,7 @@ bool TimeBasedView::PlayheadMove(QMouseEvent *event) if (Core::instance()->snapping() && snap_service_) { rational movement; - snap_service_->SnapPoint({mouse_time}, &movement, SnapService::kSnapAll & ~SnapService::kSnapToPlayhead); + snap_service_->SnapPoint({mouse_time}, &movement, TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToPlayhead); mouse_time += movement; } diff --git a/app/widget/timebased/timebasedview.h b/app/widget/timebased/timebasedview.h index a51728afb..8771f6c01 100644 --- a/app/widget/timebased/timebasedview.h +++ b/app/widget/timebased/timebasedview.h @@ -22,28 +22,33 @@ #define TIMELINEVIEWBASE_H #include +#include #include "core.h" #include "timescaledobject.h" #include "widget/handmovableview/handmovableview.h" -#include "widget/snapservice/snapservice.h" namespace olive { +class TimeBasedWidget; + class TimeBasedView : public HandMovableView, public TimeScaledObject { Q_OBJECT public: TimeBasedView(QWidget* parent = nullptr); - void EnableSnap(const QVector &points); + void EnableSnap(const std::vector &points); void DisableSnap(); bool IsSnapped() const { return snapped_; } - void SetSnapService(SnapService* service); + const rational &GetTime() const { return playhead_; } + + TimeBasedWidget *GetSnapService() const { return snap_service_; } + void SetSnapService(TimeBasedWidget* service) { snap_service_ = service; } const double& GetYScale() const; void SetYScale(const double& y_scale); @@ -85,11 +90,6 @@ protected: virtual void ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, const QPointF &cursor_pos) override; - const rational &GetPlayheadTime() const - { - return playhead_; - } - bool PlayheadPress(QMouseEvent* event); bool PlayheadMove(QMouseEvent* event); bool PlayheadRelease(QMouseEvent* event); @@ -119,11 +119,11 @@ private: QGraphicsScene scene_; bool snapped_; - QVector snap_time_; + std::vector snap_time_; rational end_time_; - SnapService* snap_service_; + TimeBasedWidget* snap_service_; bool y_axis_enabled_; diff --git a/app/widget/timebased/timebasedviewselectionmanager.cpp b/app/widget/timebased/timebasedviewselectionmanager.cpp index dd44a172e..577046aff 100644 --- a/app/widget/timebased/timebasedviewselectionmanager.cpp +++ b/app/widget/timebased/timebasedviewselectionmanager.cpp @@ -22,5 +22,4 @@ namespace olive { - } diff --git a/app/widget/timebased/timebasedviewselectionmanager.h b/app/widget/timebased/timebasedviewselectionmanager.h index ae5e32230..106eef182 100644 --- a/app/widget/timebased/timebasedviewselectionmanager.h +++ b/app/widget/timebased/timebasedviewselectionmanager.h @@ -29,6 +29,7 @@ #include "common/rational.h" #include "common/timecodefunctions.h" #include "timebasedview.h" +#include "timebasedwidget.h" namespace olive { @@ -38,9 +39,15 @@ class TimeBasedViewSelectionManager public: TimeBasedViewSelectionManager(TimeBasedView *view) : view_(view), - rubberband_(nullptr) + rubberband_(nullptr), + snap_mask_(TimeBasedWidget::kSnapAll) {} + void SetSnapMask(TimeBasedWidget::SnapMask e) + { + snap_mask_ = e; + } + void ClearDrawnObjects() { drawn_objects_.clear(); @@ -48,7 +55,7 @@ public: void DeclareDrawnObject(T *object, const QRectF &pos) { - drawn_objects_.append({object, pos}); + drawn_objects_.push_back({object, pos}); } bool Select(T *key) @@ -56,7 +63,7 @@ public: Q_ASSERT(key); if (!IsSelected(key)) { - selected_.append(key); + selected_.push_back(key); return true; } @@ -67,7 +74,13 @@ public: { Q_ASSERT(key); - return selected_.removeOne(key); + auto it = std::find(selected_.cbegin(), selected_.cend(), key); + if (it == selected_.cend()) { + return false; + } else { + selected_.erase(it); + return true; + } } void ClearSelection() @@ -77,10 +90,10 @@ public: bool IsSelected(T *key) const { - return selected_.contains(key); + return std::find(selected_.cbegin(), selected_.cend(), key) != selected_.cend(); } - const QVector &GetSelectedObjects() const + const std::vector &GetSelectedObjects() const { return selected_; } @@ -142,7 +155,7 @@ public: bool IsDragging() const { - return !dragging_.isEmpty(); + return !dragging_.empty(); } void DragStart(T *initial_item, QMouseEvent *event) @@ -150,39 +163,80 @@ public: initial_drag_item_ = initial_item; dragging_.resize(selected_.size()); - for (int i=0; itime(), view_->TimeToScene(obj->time())}; + dragging_[i] = obj->time(); + + snap_points_[i] = obj->time(); + snap_points_[i+selected_.size()] = obj->time_range().out(); } drag_mouse_start_ = view_->mapToScene(event->pos()); } - void DragMove(QMouseEvent *event, const QString &tip_format) + void SnapPoints(rational *movement) { - QPointF diff = view_->mapToScene(event->pos()) - drag_mouse_start_; + if (Core::instance()->snapping() && view_->GetSnapService()) { + view_->GetSnapService()->SnapPoint(snap_points_, movement, snap_mask_); + } + } - for (int i=0; iSceneToTimeNoGrid(dragging_.at(i).x + diff.x()); + void Unsnap() + { + if (view_->GetSnapService()) { + view_->GetSnapService()->HideSnaps(); + } + } + + void DragMove(QMouseEvent *event, const QString &tip_format = QString()) + { + rational time_diff = view_->SceneToTimeNoGrid(view_->mapToScene(event->pos()).x() - drag_mouse_start_.x()); + + // Snap points + SnapPoints(&time_diff); + + // Validate movement + for (size_t i=0; iparent()->GetKeyframeAtTimeOnTrack(sel->input(), proposed_time, sel->track(), sel->element()); - if (!key_at_time || key_at_time == sel) { - break; + bool loop; + do { + loop = false; + while (sel->has_sibling_at_time(proposed_time)) { + proposed_time += adj; + Unsnap(); } - proposed_time += adj; - } + if (proposed_time < 0) { + // Prevent any object from going below zero + proposed_time = 0; + Unsnap(); - sel->set_time(proposed_time); + // Setting our proposed time to zero may (re)introduce a conflict that we just avoided + // with the sibling check above, so we request it to happen again. To avoid a negative + // adj bringing us back below zero, we force adj to positive so it'll only nudge higher + adj = qAbs(adj); + + loop = true; + } + } while (loop); + + time_diff = proposed_time - dragging_.at(i); + } + + // Apply movement + for (size_t i=0; iset_time(dragging_.at(i) + time_diff); } // Show information about this keyframe @@ -201,11 +255,12 @@ public: { QToolTip::hideText(); - for (int i=0; iadd_child(new SetTimeCommand(selected_.at(i), selected_.at(i)->time(), dragging_.at(i).time)); + for (size_t i=0; iadd_child(new SetTimeCommand(selected_.at(i), selected_.at(i)->time(), dragging_.at(i))); } dragging_.clear(); + Unsnap(); } void RubberBandStart(QMouseEvent *event) @@ -271,7 +326,7 @@ private: virtual Project* GetRelevantProject() const override { - return key_->parent()->project(); + return Project::GetProjectFromObject(key_); } protected: @@ -296,17 +351,12 @@ private: TimeBasedView *view_; using DrawnObject = QPair; - QVector drawn_objects_; + std::vector drawn_objects_; - QVector selected_; + std::vector selected_; - struct DragObject - { - rational time; - double x; - }; - - QVector dragging_; + std::vector dragging_; + std::vector snap_points_; T *initial_drag_item_; @@ -316,7 +366,9 @@ private: QRubberBand *rubberband_; QPoint rubberband_start_; - QVector rubberband_preselected_; + std::vector rubberband_preselected_; + + TimeBasedWidget::SnapMask snap_mask_; }; diff --git a/app/widget/timebased/timebasedwidget.cpp b/app/widget/timebased/timebasedwidget.cpp index f15479cd8..379dd8102 100644 --- a/app/widget/timebased/timebasedwidget.cpp +++ b/app/widget/timebased/timebasedwidget.cpp @@ -23,10 +23,13 @@ #include #include "common/autoscroll.h" +#include "common/range.h" #include "common/timecodefunctions.h" #include "config/config.h" #include "core.h" +#include "dialog/markerproperties/markerpropertiesdialog.h" #include "node/project/sequence/sequence.h" +#include "widget/timeruler/timeruler.h" #include "widget/timelinewidget/undo/timelineundoworkarea.h" namespace olive { @@ -39,7 +42,8 @@ TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, bool ruler_cache_statu auto_set_timebase_(true) { ruler_ = new TimeRuler(ruler_text_visible, ruler_cache_status_visible, this); - connect(ruler_, &TimeRuler::TimeChanged, this, &TimeBasedWidget::SetTimeAndSignal); + ConnectTimelineView(ruler_, true); + ruler()->SetSnapService(this); scrollbar_ = new ResizableTimelineScrollBar(Qt::Horizontal, this); connect(scrollbar_, &ResizableScrollBar::ResizeBegan, this, &TimeBasedWidget::ScrollBarResizeBegan); @@ -588,18 +592,36 @@ void TimeBasedWidget::SetMarker() return; } - bool ok; - QString marker_name; + TimelineMarkerList *markers = GetConnectedNode()->GetTimelinePoints()->markers(); - if (Config::Current()[QStringLiteral("SetNameWithMarker")].toBool()) { - marker_name = QInputDialog::getText(this, tr("Set Marker"), tr("Marker name:"), QLineEdit::Normal, QString(), &ok); + if (TimelineMarker *existing = markers->GetMarkerAtTime(GetTime())) { + // We already have a marker here, so pop open the edit dialog + MarkerPropertiesDialog mpd({existing}, timebase(), this); + mpd.exec(); } else { - ok = true; - } + // Create a new marker and place it here + int color; + if (TimelineMarker *closest = markers->GetClosestMarkerToTime(GetTime())) { + // Copy color of closest marker to this time + color = closest->color(); + } else { + // Fallback to default color in preferences + color = Config::Current()[QStringLiteral("MarkerColor")].toInt(); + } - if (ok) { - Core::instance()->undo_stack()->push(new MarkerAddCommand(GetConnectedNode()->project(), - GetConnectedNode()->GetTimelinePoints()->markers(), TimeRange(GetTime(), GetTime()), marker_name)); + TimelineMarker *marker = new TimelineMarker(color, TimeRange(GetTime(), GetTime())); + + if (Config::Current()[QStringLiteral("SetNameWithMarker")].toBool()) { + MarkerPropertiesDialog mpd({marker}, timebase(), this); + if (mpd.exec() != QDialog::Accepted) { + delete marker; + marker = nullptr; + } + } + + if (marker) { + Core::instance()->undo_stack()->push(new MarkerAddCommand(markers, marker)); + } } } @@ -658,27 +680,11 @@ void TimeBasedWidget::GoToOut() } } -TimeBasedWidget::MarkerAddCommand::MarkerAddCommand(Project *project, TimelineMarkerList *marker_list, const TimeRange &range, const QString &name) : - project_(project), - marker_list_(marker_list), - range_(range), - name_(name) +void TimeBasedWidget::DeleteSelected() { -} - -Project *TimeBasedWidget::MarkerAddCommand::GetRelevantProject() const -{ - return project_; -} - -void TimeBasedWidget::MarkerAddCommand::redo() -{ - added_marker_ = marker_list_->AddMarker(range_, name_); -} - -void TimeBasedWidget::MarkerAddCommand::undo() -{ - marker_list_->RemoveMarker(added_marker_); + if (ruler_->underMouse()) { + ruler_->DeleteSelected(); + } } bool TimeBasedWidget::eventFilter(QObject *object, QEvent *event) @@ -690,4 +696,155 @@ bool TimeBasedWidget::eventFilter(QObject *object, QEvent *event) return false; } +struct SnapData { + rational time; + rational movement; +}; + +void AttemptSnap(std::vector &snap_data, + const std::vector& screen_pt, + double compare_pt, + const std::vector& start_times, + const rational& compare_time) +{ + const qreal kSnapRange = 10; // FIXME: Hardcoded number + + for (size_t i=0;i &start_times, rational *movement, SnapMask snap_points) +{ + std::vector screen_pt(start_times.size()); + + for (size_t i=0; i potential_snaps; + + if (snap_points & kSnapToPlayhead) { + rational playhead_abs_time = GetTime(); + qreal playhead_pos = TimeToScene(playhead_abs_time); + AttemptSnap(potential_snaps, screen_pt, playhead_pos, start_times, playhead_abs_time); + } + + if ((snap_points & kSnapToClips) && GetSnapBlocks()) { + for (auto it=GetSnapBlocks()->cbegin(); it!=GetSnapBlocks()->cend(); it++) { + Block *b = *it; + + qreal rect_left = TimeToScene(b->in()); + qreal rect_right = TimeToScene(b->out()); + + // Attempt snapping to clip in point + AttemptSnap(potential_snaps, screen_pt, rect_left, start_times, b->in()); + + // Attempt snapping to clip out point + AttemptSnap(potential_snaps, screen_pt, rect_right, start_times, b->out()); + + if (snap_points & kSnapToMarkers) { + // Snap to clip markers too + if (ClipBlock *clip = dynamic_cast(b)) { + if (clip->connected_viewer()) { + TimelineMarkerList *markers = clip->connected_viewer()->GetTimelinePoints()->markers(); + for (auto jt=markers->cbegin(); jt!=markers->cend(); jt++) { + TimelineMarker *marker = *jt; + + TimeRange marker_range = marker->time_range() + clip->in() - clip->media_in(); + + qreal marker_in_screen = TimeToScene(marker_range.in()); + qreal marker_out_screen = TimeToScene(marker_range.out()); + + AttemptSnap(potential_snaps, screen_pt, marker_in_screen, start_times, marker_range.in()); + AttemptSnap(potential_snaps, screen_pt, marker_out_screen, start_times, marker_range.out()); + } + } + } + } + } + } + + if ((snap_points & kSnapToMarkers) && ruler()->GetTimelinePoints()) { + for (auto it=ruler()->GetTimelinePoints()->markers()->cbegin(); it!=ruler()->GetTimelinePoints()->markers()->cend(); it++) { + TimelineMarker* m = *it; + + qreal marker_pos = TimeToScene(m->time_range().in()); + AttemptSnap(potential_snaps, screen_pt, marker_pos, start_times, m->time_range().in()); + + if (m->time_range().in() != m->time_range().out()) { + marker_pos = TimeToScene(m->time_range().out()); + AttemptSnap(potential_snaps, screen_pt, marker_pos, start_times, m->time_range().out()); + } + } + } + + if ((snap_points & kSnapToKeyframes) && GetSnapKeyframes()) { + for (auto it=GetSnapKeyframes()->cbegin(); it!=GetSnapKeyframes()->cend(); it++) { + const QVector &keys = (*it)->GetKeyframes(); + for (auto jt=keys.cbegin(); jt!=keys.cend(); jt++) { + NodeKeyframe *key = *jt; + + auto ignore = GetSnapIgnoreKeyframes(); + if (ignore && std::find(ignore->cbegin(), ignore->cend(), key) != ignore->cend()) { + continue; + } + + qreal key_scene_pt = TimeToScene(key->time()); + + AttemptSnap(potential_snaps, screen_pt, key_scene_pt, start_times, key->time()); + } + } + } + + if (potential_snaps.empty()) { + HideSnaps(); + return false; + } + + int closest_snap = 0; + rational closest_diff = qAbs(potential_snaps.at(0).movement - *movement); + + // Determine which snap point was the closest + for (size_t i=1; i snap_times; + foreach (const SnapData& d, potential_snaps) { + if (d.movement == *movement) { + snap_times.push_back(d.time); + } + } + + ShowSnaps(snap_times); + + return true; +} + +void TimeBasedWidget::ShowSnaps(const std::vector ×) +{ + foreach (TimeBasedView* view, timeline_views_) { + view->EnableSnap(times); + } +} + +void TimeBasedWidget::HideSnaps() +{ + foreach (TimeBasedView* view, timeline_views_) { + view->DisableSnap(); + } +} + } diff --git a/app/widget/timebased/timebasedwidget.h b/app/widget/timebased/timebasedwidget.h index 2e479d84b..3dce38e5a 100644 --- a/app/widget/timebased/timebasedwidget.h +++ b/app/widget/timebased/timebasedwidget.h @@ -25,13 +25,15 @@ #include "node/output/viewer/viewer.h" #include "timeline/timelinecommon.h" +#include "widget/keyframeview/keyframeviewinputconnection.h" #include "widget/resizablescrollbar/resizabletimelinescrollbar.h" #include "widget/timebased/timescaledobject.h" #include "widget/timelinewidget/view/timelineview.h" -#include "widget/timeruler/timeruler.h" namespace olive { +class TimeRuler; + class TimeBasedWidget : public TimelineScaledWidget { Q_OBJECT @@ -54,6 +56,22 @@ public: virtual bool eventFilter(QObject* object, QEvent* event) override; + using SnapMask = uint32_t; + enum SnapPoints { + kSnapToClips = 0x1, + kSnapToPlayhead = 0x2, + kSnapToMarkers = 0x4, + kSnapToKeyframes = 0x8, + kSnapAll = UINT32_MAX + }; + + /** + * @brief Snaps point `start_point` that is moving by `movement` to currently existing clips + */ + bool SnapPoint(const std::vector &start_times, rational *movement, SnapMask snap_points = kSnapAll); + void ShowSnaps(const std::vector ×); + void HideSnaps(); + public slots: void SetTime(const rational &time); @@ -91,6 +109,8 @@ public slots: void GoToOut(); + void DeleteSelected(); + protected slots: void SetTimeAndSignal(const rational& t); @@ -117,6 +137,10 @@ protected: void PassWheelEventsToScrollBar(QObject* object); + virtual const QVector *GetSnapBlocks() const { return nullptr; } + virtual const QVector *GetSnapKeyframes() const { return nullptr; } + virtual const std::vector *GetSnapIgnoreKeyframes() const { return nullptr; } + protected slots: /** * @brief Slot to center the horizontal scroll bar on the playhead's current position @@ -139,26 +163,7 @@ signals: void ConnectedNodeChanged(ViewerOutput* old, ViewerOutput* now); private: - class MarkerAddCommand : public UndoCommand - { - public: - MarkerAddCommand(Project* project, TimelineMarkerList* marker_list, const TimeRange& range, const QString& name); - virtual Project* GetRelevantProject() const override; - - protected: - virtual void redo() override; - virtual void undo() override; - - private: - Project* project_; - TimelineMarkerList* marker_list_; - TimeRange range_; - QString name_; - - TimelineMarker* added_marker_; - - }; /** * @brief Set either in or out point to the current playhead diff --git a/app/widget/timebased/timescaledobject.h b/app/widget/timebased/timescaledobject.h index acdc44acb..d5cd102e2 100644 --- a/app/widget/timebased/timescaledobject.h +++ b/app/widget/timebased/timescaledobject.h @@ -24,6 +24,7 @@ #include #include "common/rational.h" +#include "node/block/block.h" namespace olive { diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 07c3fe52c..58629b2c2 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -31,6 +31,8 @@ #include "dialog/sequence/sequence.h" #include "dialog/speedduration/speeddurationdialog.h" #include "node/block/transition/transition.h" +#include "node/project/serializer/serializer.h" +#include "task/project/import/import.h" #include "tool/add.h" #include "tool/beam.h" #include "tool/edit.h" @@ -53,6 +55,7 @@ #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "widget/nodeview/nodeviewundo.h" +#include "widget/timeruler/timeruler.h" namespace olive { @@ -76,11 +79,11 @@ TimelineWidget::TimelineWidget(QWidget *parent) : timecode_label_->SetAlignment(Qt::AlignCenter); timecode_label_->SetDisplayType(RationalSlider::kTime); timecode_label_->setVisible(false); + timecode_label_->SetMinimum(0); connect(timecode_label_, &RationalSlider::ValueChanged, this, &TimelineWidget::SetTimeAndSignal); ruler_and_time_layout->addWidget(timecode_label_); ruler_and_time_layout->addWidget(ruler()); - ruler()->SetSnapService(this); // Create list of TimelineViews - these MUST correspond to the ViewType enum @@ -310,66 +313,6 @@ void TimelineWidget::DisconnectNodeEvent(ViewerOutput *n) } } -void TimelineWidget::CopyNodesToClipboardCallback(const QVector &nodes, ProjectSerializer::SaveData *sdata, void *userdata) -{ - // Cache the earliest in point so all copied clips have a "relative" in point that can be pasted anywhere - QVector& selected = *static_cast*>(userdata); - rational earliest_in = RATIONAL_MAX; - ProjectSerializer::SerializedProperties properties; - - foreach (Block* block, selected) { - earliest_in = qMin(earliest_in, block->in()); - } - - foreach (Block* block, selected) { - properties[block][QStringLiteral("in")] = (block->in() - earliest_in).toString(); - properties[block][QStringLiteral("track")] = block->track()->ToReference().ToString(); - } - - sdata->SetProperties(properties); -} - -void TimelineWidget::PasteNodesToClipboardCallback(const QVector &nodes, const ProjectSerializer::LoadData &load_data, void *userdata) -{ - bool insert = *(bool*)userdata; - - MultiUndoCommand *command = new MultiUndoCommand(); - - foreach (Node *n, nodes) { - command->add_child(new NodeAddCommand(GetConnectedNode()->project(), n)); - } - - rational paste_start = GetTime(); - - if (insert) { - rational paste_end = GetTime(); - - for (auto it=load_data.properties.cbegin(); it!=load_data.properties.cend(); it++) { - rational length = static_cast(it.key())->length(); - rational in = rational::fromString(it.value()[QStringLiteral("in")]); - - paste_end = qMax(paste_end, paste_start + in + length); - } - - if (paste_end != paste_start) { - InsertGapsAt(paste_start, paste_end - paste_start, command); - } - } - - for (auto it=load_data.properties.cbegin(); it!=load_data.properties.cend(); it++) { - Block *block = static_cast(it.key()); - rational in = rational::fromString(it.value()[QStringLiteral("in")]); - Track::Reference track = Track::Reference::FromString(it.value()[QStringLiteral("track")]); - - command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track.type()), - track.index(), - block, - paste_start + in)); - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); -} - void TimelineWidget::SelectAll() { QVector newly_selected_blocks; @@ -494,6 +437,11 @@ void TimelineWidget::ReplaceBlocksWithGaps(const QVector &blocks, void TimelineWidget::DeleteSelected(bool ripple) { + if (ruler()->hasFocus()) { + ruler()->DeleteSelected(); + return; + } + QVector selected_list = GetSelectedBlocks(); QVector blocks_to_delete; @@ -626,15 +574,17 @@ void TimelineWidget::CopySelected(bool cut) return; } - QVector selected = GetSelectedBlocks(); + if (ruler()->hasFocus() && ruler()->CopySelected(cut)) { + return; + } - if (selected.isEmpty()) { + if (selected_blocks_.isEmpty()) { return; } QVector selected_nodes; - foreach (Block* block, selected) { + foreach (Block* block, selected_blocks_) { selected_nodes.append(block); QVector deps = block->GetDependencies(); @@ -646,7 +596,25 @@ void TimelineWidget::CopySelected(bool cut) } } - CopyNodesToClipboard(selected_nodes, &selected); + ProjectSerializer::SaveData sdata(selected_nodes.first()->project()); + sdata.SetOnlySerializeNodesAndResolveGroups(selected_nodes); + + // Cache the earliest in point so all copied clips have a "relative" in point that can be pasted anywhere + rational earliest_in = RATIONAL_MAX; + ProjectSerializer::SerializedProperties properties; + + foreach (Block* block, selected_blocks_) { + earliest_in = qMin(earliest_in, block->in()); + } + + foreach (Block* block, selected_blocks_) { + properties[block][QStringLiteral("in")] = (block->in() - earliest_in).toString(); + properties[block][QStringLiteral("track")] = block->track()->ToReference().ToString(); + } + + sdata.SetProperties(properties); + + ProjectSerializer::Copy(sdata, QStringLiteral("timeline")); if (cut) { DeleteSelected(); @@ -659,7 +627,50 @@ void TimelineWidget::Paste(bool insert) return; } - PasteNodesFromClipboard(&insert); + if (ruler()->hasFocus() && ruler()->PasteMarkers(insert, GetTime())) { + return; + } + + ProjectSerializer::Result res = ProjectSerializer::Paste(QStringLiteral("timeline")); + if (res.GetLoadedNodes().isEmpty()) { + return; + } + + MultiUndoCommand *command = new MultiUndoCommand(); + + foreach (Node *n, res.GetLoadedNodes()) { + command->add_child(new NodeAddCommand(GetConnectedNode()->project(), n)); + } + + rational paste_start = GetTime(); + + if (insert) { + rational paste_end = GetTime(); + + for (auto it=res.GetLoadData().properties.cbegin(); it!=res.GetLoadData().properties.cend(); it++) { + rational length = static_cast(it.key())->length(); + rational in = rational::fromString(it.value()[QStringLiteral("in")]); + + paste_end = qMax(paste_end, paste_start + in + length); + } + + if (paste_end != paste_start) { + InsertGapsAt(paste_start, paste_end - paste_start, command); + } + } + + for (auto it=res.GetLoadData().properties.cbegin(); it!=res.GetLoadData().properties.cend(); it++) { + Block *block = static_cast(it.key()); + rational in = rational::fromString(it.value()[QStringLiteral("in")]); + Track::Reference track = Track::Reference::FromString(it.value()[QStringLiteral("track")]); + + command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track.type()), + track.index(), + block, + paste_start + in)); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); } void TimelineWidget::DeleteInToOut(bool ripple) @@ -778,6 +789,35 @@ void TimelineWidget::ShowSpeedDurationDialogForSelectedClips() } } +void TimelineWidget::RecordingCallback(const QString &filename, const TimeRange &time, const Track::Reference &track) +{ + ProjectImportTask task(GetConnectedNode()->project()->root(), {filename}); + task.Start(); + + MultiUndoCommand *import_command = task.GetCommand(); + Core::instance()->undo_stack()->pushIfHasChildren(import_command); + + if (task.GetImportedFootage().empty()) { + qCritical() << "Failed to import recorded audio file" << filename; + } else { + import_tool_->PlaceAt({task.GetImportedFootage().front()}, time.in(), false, track.index()); + } +} + +void TimelineWidget::EnableRecordingOverlay(const TimelineCoordinate &coord) +{ + foreach (TimelineAndTrackView* tview, views_) { + tview->view()->EnableRecordingOverlay(coord); + } +} + +void TimelineWidget::DisableRecordingOverlay() +{ + foreach (TimelineAndTrackView* tview, views_) { + tview->view()->DisableRecordingOverlay(); + } +} + void TimelineWidget::InsertGapsAt(const rational &earliest_point, const rational &insert_length, MultiUndoCommand *command) { for (int i=0;iundo_stack()->pushIfHasChildren(command); } -void TimelineWidget::ShowSnap(const QVector ×) -{ - foreach (TimelineAndTrackView* tview, views_) { - tview->view()->EnableSnap(times); - } -} - void TimelineWidget::UpdateViewports(const Track::Type &type) { if (type == Track::kNone) { @@ -1563,13 +1596,6 @@ QVector TimelineWidget::GetBlocksInGlobalRect(const QPoint &p1, const Q return blocks_in_rect; } -void TimelineWidget::HideSnaps() -{ - foreach (TimelineAndTrackView* tview, views_) { - tview->view()->DisableSnap(); - } -} - QByteArray TimelineWidget::SaveSplitterState() const { return view_splitter_->saveState(); @@ -1724,107 +1750,6 @@ Block *TimelineWidget::GetItemAtScenePos(const TimelineCoordinate& coord) return views_.at(coord.GetTrack().type())->view()->GetItemAtScenePos(coord.GetFrame(), coord.GetTrack().index()); } -struct SnapData { - rational time; - rational movement; -}; - -QVector AttemptSnap(const QVector& screen_pt, - double compare_pt, - const QVector& start_times, - const rational& compare_time) { - const qreal kSnapRange = 10; // FIXME: Hardcoded number - - QVector snap_data; - - for (int i=0;i start_times, rational* movement, int snap_points) -{ - if (!GetConnectedNode()) { - return false; - } - - QVector screen_pt; - - foreach (const rational& s, start_times) { - screen_pt.append(TimeToScene(s + *movement)); - } - - QVector potential_snaps; - - if (snap_points & kSnapToPlayhead) { - rational playhead_abs_time = GetTime(); - qreal playhead_pos = TimeToScene(playhead_abs_time); - potential_snaps.append(AttemptSnap(screen_pt, playhead_pos, start_times, playhead_abs_time)); - } - - if (snap_points & kSnapToClips) { - foreach (Block* b, added_blocks_) { - qreal rect_left = TimeToScene(b->in()); - qreal rect_right = TimeToScene(b->out()); - - // Attempt snapping to clip in point - potential_snaps.append(AttemptSnap(screen_pt, rect_left, start_times, b->in())); - - // Attempt snapping to clip out point - potential_snaps.append(AttemptSnap(screen_pt, rect_right, start_times, b->out())); - } - } - - if ((snap_points & kSnapToMarkers)) { - foreach (TimelineMarker* m, GetConnectedNode()->GetTimelinePoints()->markers()->list()) { - qreal marker_pos = TimeToScene(m->time().in()); - potential_snaps.append(AttemptSnap(screen_pt, marker_pos, start_times, m->time().in())); - - if (m->time().in() != m->time().out()) { - marker_pos = TimeToScene(m->time().out()); - potential_snaps.append(AttemptSnap(screen_pt, marker_pos, start_times, m->time().out())); - } - } - } - - if (potential_snaps.isEmpty()) { - HideSnaps(); - return false; - } - - int closest_snap = 0; - rational closest_diff = qAbs(potential_snaps.at(0).movement - *movement); - - // Determine which snap point was the closest - for (int i=1; i snap_times; - foreach (const SnapData& d, potential_snaps) { - if (d.movement == *movement) { - snap_times.append(d.time); - } - } - - ShowSnap(snap_times); - - return true; -} - void TimelineWidget::SetSplitterSizesCommand::redo() { old_sizes_ = splitter_->sizes(); diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index a79195252..c23e7093f 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -27,12 +27,10 @@ #include "core.h" #include "node/block/transition/transition.h" -#include "node/nodecopypaste.h" #include "node/output/viewer/viewer.h" #include "timeline/timelinecommon.h" #include "timelineandtrackview.h" #include "widget/slider/rationalslider.h" -#include "widget/snapservice/snapservice.h" #include "widget/timebased/timebasedwidget.h" #include "widget/timelinewidget/timelinewidgetselections.h" #include "widget/timelinewidget/tool/import.h" @@ -45,7 +43,7 @@ namespace olive { * * Encapsulates TimelineViews, TimeRulers, and scrollbars for a complete widget to manipulate Timelines */ -class TimelineWidget : public TimeBasedWidget, public NodeCopyPasteService, public SnapService +class TimelineWidget : public TimeBasedWidget { Q_OBJECT public: @@ -101,6 +99,12 @@ public: void ShowSpeedDurationDialogForSelectedClips(); + void RecordingCallback(const QString &filename, const TimeRange &time, const Track::Reference &track); + + void EnableRecordingOverlay(const TimelineCoordinate &coord); + + void DisableRecordingOverlay(); + /** * @brief Timelines should always be connected to sequences */ @@ -114,10 +118,6 @@ public: return selected_blocks_; } - virtual bool SnapPoint(QVector start_times, rational *movement, int snap_points = kSnapAll) override; - - virtual void HideSnaps() override; - QByteArray SaveSplitterState() const; void RestoreSplitterState(const QByteArray& state); @@ -216,6 +216,11 @@ public: */ void SignalDeselectedAllBlocks(); + void Refresh() + { + UpdateViewports(); + } + MultiUndoCommand *TakeSubtitleSectionCommand() { // Copy pointer @@ -262,6 +267,8 @@ public: signals: void BlockSelectionChanged(const QVector& selected_blocks); + void RequestCaptureStart(const TimeRange &time, const Track::Reference &track); + protected: virtual void resizeEvent(QResizeEvent *event) override; @@ -272,8 +279,7 @@ protected: virtual void ConnectNodeEvent(ViewerOutput* n) override; virtual void DisconnectNodeEvent(ViewerOutput* n) override; - virtual void CopyNodesToClipboardCallback(const QVector &nodes, ProjectSerializer::SaveData *data, void *userdata) override; - virtual void PasteNodesToClipboardCallback(const QVector &nodes, const ProjectSerializer::LoadData &load_data, void *userdata) override; + virtual const QVector *GetSnapBlocks() const override { return &added_blocks_; } private: QVector GetEditToInfo(const rational &playhead_time, Timeline::MovementMode mode); @@ -282,8 +288,6 @@ private: void EditTo(Timeline::MovementMode mode); - void ShowSnap(const QVector& times); - void UpdateViewports(const Track::Type& type = Track::kNone); QVector GetBlocksInGlobalRect(const QPoint &p1, const QPoint &p2); diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index a8f778b5f..2dd7455c5 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -79,7 +79,7 @@ void AddTool::MousePress(TimelineViewMouseEvent *event) ghost_->SetTrack(track); parent()->AddGhost(ghost_); - snap_points_.append(drag_start_point_); + snap_points_.push_back(drag_start_point_); } } diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index f9ab4b6f8..fdcd49afc 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -173,7 +173,7 @@ void ImportTool::DragDrop(TimelineViewMouseEvent *event) } } -void ImportTool::PlaceAt(const QVector &footage, const rational &start, bool insert) +void ImportTool::PlaceAt(const QVector &footage, const rational &start, bool insert, int track_offset) { DraggedFootageData refs; @@ -181,10 +181,10 @@ void ImportTool::PlaceAt(const QVector &footage, const rational refs.append({f, f->GetEnabledStreamsAsReferences()}); } - PlaceAt(refs, start, insert); + PlaceAt(refs, start, insert, track_offset); } -void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &start, bool insert) +void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &start, bool insert, int track_offset) { dragged_footage_ = footage; @@ -192,7 +192,7 @@ void ImportTool::PlaceAt(const DraggedFootageData &footage, const rational &star return; } - PrepGhosts(start, 0); + PrepGhosts(start, track_offset); DropGhosts(insert); } @@ -243,8 +243,8 @@ void ImportTool::FootageToGhosts(rational ghost_start, const DraggedFootageData ghost->SetMediaIn(ghost_in); ghost->SetTrack(Track::Reference(track_type, track_offsets.at(track_type))); - snap_points_.append(ghost->GetIn()); - snap_points_.append(ghost->GetOut()); + snap_points_.push_back(ghost->GetIn()); + snap_points_.push_back(ghost->GetOut()); // Increment track count for this track type track_offsets[track_type]++; diff --git a/app/widget/timelinewidget/tool/import.h b/app/widget/timelinewidget/tool/import.h index 70ad33676..965f5960d 100644 --- a/app/widget/timelinewidget/tool/import.h +++ b/app/widget/timelinewidget/tool/import.h @@ -37,8 +37,8 @@ public: using DraggedFootageData = QVector > >; - void PlaceAt(const QVector &footage, const rational& start, bool insert); - void PlaceAt(const DraggedFootageData &footage, const rational& start, bool insert); + void PlaceAt(const QVector &footage, const rational& start, bool insert, int track_offset = 0); + void PlaceAt(const DraggedFootageData &footage, const rational& start, bool insert, int track_offset = 0); enum DropWithoutSequenceBehavior { kDWSAsk, diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index 8ab2eaef8..ad76ff6a6 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -35,6 +35,7 @@ #include "pointer.h" #include "widget/nodeview/nodeviewundo.h" #include "widget/timelinewidget/undo/timelineundopointer.h" +#include "widget/timeruler/timeruler.h" namespace olive { @@ -136,6 +137,9 @@ void PointerTool::MousePress(TimelineViewMouseEvent *event) if (can_rubberband_select_) { drag_global_start_ = QCursor::pos(); } + + // If we click anywhere other than a marker, deselect all markers + parent()->ruler()->DeselectAllMarkers(); } void PointerTool::MouseMove(TimelineViewMouseEvent *event) @@ -790,14 +794,14 @@ void PointerTool::AddGhostInternal(TimelineViewGhostItem* ghost, Timeline::Movem // Prepare snap points (optimizes snapping for later) switch (mode) { case Timeline::kMove: - snap_points_.append(ghost->GetIn()); - snap_points_.append(ghost->GetOut()); + snap_points_.push_back(ghost->GetIn()); + snap_points_.push_back(ghost->GetOut()); break; case Timeline::kTrimIn: - snap_points_.append(ghost->GetIn()); + snap_points_.push_back(ghost->GetIn()); break; case Timeline::kTrimOut: - snap_points_.append(ghost->GetOut()); + snap_points_.push_back(ghost->GetOut()); break; default: break; diff --git a/app/widget/timelinewidget/tool/record.cpp b/app/widget/timelinewidget/tool/record.cpp index 4a904fb14..c8a7fc001 100644 --- a/app/widget/timelinewidget/tool/record.cpp +++ b/app/widget/timelinewidget/tool/record.cpp @@ -1,11 +1,88 @@ #include "record.h" +#include "widget/timelinewidget/timelinewidget.h" + namespace olive { RecordTool::RecordTool(TimelineWidget *parent) : - BeamTool(parent) + BeamTool(parent), + ghost_(nullptr) { } +void RecordTool::MousePress(TimelineViewMouseEvent *event) +{ + const Track::Reference& track = event->GetTrack(); + + // Check if track is locked + Track* t = parent()->GetTrackFromReference(track); + if (t && t->IsLocked()) { + return; + } + + if (t->type() != Track::kAudio) { + // We only support audio tracks here + return; + } + + drag_start_point_ = ValidatedCoordinate(event->GetCoordinates(true)).GetFrame(); + + ghost_ = new TimelineViewGhostItem(); + ghost_->SetIn(drag_start_point_); + ghost_->SetOut(drag_start_point_); + ghost_->SetTrack(track); + parent()->AddGhost(ghost_); + + snap_points_.push_back(drag_start_point_); +} + +void RecordTool::MouseMove(TimelineViewMouseEvent *event) +{ + if (!ghost_) { + return; + } + + // Calculate movement + rational movement = event->GetFrame() - drag_start_point_; + + // Validation: Ensure in point never goes below 0 + if (movement < -ghost_->GetIn()) { + movement = -ghost_->GetIn(); + } + + // Snap movement + bool snapped; + + if (Core::instance()->snapping()) { + snapped = parent()->SnapPoint(snap_points_, &movement); + } else { + snapped = false; + } + + // Make adjustment + if (!movement) { + ghost_->SetInAdjustment(0); + ghost_->SetOutAdjustment(0); + } else if (movement > 0) { + ghost_->SetInAdjustment(0); + ghost_->SetOutAdjustment(movement); + } else if (movement < 0) { + ghost_->SetInAdjustment(movement); + ghost_->SetOutAdjustment(0); + } + + Q_UNUSED(snapped) +} + +void RecordTool::MouseRelease(TimelineViewMouseEvent *event) +{ + if (ghost_) { + emit parent()->RequestCaptureStart(TimeRange(ghost_->GetAdjustedIn(), ghost_->GetAdjustedOut()), ghost_->GetTrack()); + parent()->ClearGhosts(); + snap_points_.clear(); + ghost_ = nullptr; + } +} + } diff --git a/app/widget/timelinewidget/tool/record.h b/app/widget/timelinewidget/tool/record.h index 89c1a1738..4fbe7f85b 100644 --- a/app/widget/timelinewidget/tool/record.h +++ b/app/widget/timelinewidget/tool/record.h @@ -29,6 +29,18 @@ class RecordTool : public BeamTool { public: RecordTool(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_; + }; } diff --git a/app/widget/timelinewidget/tool/tool.h b/app/widget/timelinewidget/tool/tool.h index c627f542e..47fafbd90 100644 --- a/app/widget/timelinewidget/tool/tool.h +++ b/app/widget/timelinewidget/tool/tool.h @@ -78,7 +78,7 @@ protected: void InsertGapsAtGhostDestination(MultiUndoCommand* command); - QVector snap_points_; + std::vector snap_points_; bool dragging_; diff --git a/app/widget/timelinewidget/tool/transition.cpp b/app/widget/timelinewidget/tool/transition.cpp index 594659ccb..bd95a429a 100644 --- a/app/widget/timelinewidget/tool/transition.cpp +++ b/app/widget/timelinewidget/tool/transition.cpp @@ -73,7 +73,7 @@ void TransitionTool::MousePress(TimelineViewMouseEvent *event) parent()->AddGhost(ghost_); - snap_points_.append(transition_start_point); + snap_points_.push_back(transition_start_point); // Set the drag start point drag_start_point_ = event->GetFrame(); diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index 3488a8e80..32f268c03 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -32,7 +32,10 @@ #include "common/qtutils.h" #include "common/timecodefunctions.h" #include "node/project/footage/footage.h" +#include "panel/panelmanager.h" +#include "panel/timeline/timeline.h" #include "ui/colorcoding.h" +#include "widget/timelinewidget/timelinewidget.h" namespace olive { @@ -59,6 +62,22 @@ TimelineView::TimelineView(Qt::Alignment vertical_alignment, QWidget *parent) : void TimelineView::mousePressEvent(QMouseEvent *event) { + // If we click on marker, jump to that point in the timeline + QPointF scene_pos = mapToScene(event->pos()); + for (auto it=clip_marker_rects_.cbegin(); it!=clip_marker_rects_.cend(); it++) { + if (it.value().contains(scene_pos)) { + QObject *p = this->parent(); + while (p) { + if (TimelineWidget *timeline = dynamic_cast(p)) { + timeline->SetTime(it.key()->time()); + break; + } + + p = p->parent(); + } + } + } + TimelineViewMouseEvent timeline_event = CreateMouseEvent(event); if (HandPress(event) @@ -329,6 +348,16 @@ void TimelineView::drawForeground(QPainter *painter, const QRectF &rect) track_y + GetTrackHeight(track_index)); } + // Draw recording overlay + if (recording_overlay_ && recording_coord_.GetTrack().type() == connected_track_list_->type()) { + painter->setPen(QPen(Qt::red, 2)); + painter->setBrush(QColor(255, 128, 128)); + + int x = TimeToScene(recording_coord_.GetFrame()); + painter->drawRect(x, GetTrackY(recording_coord_.GetTrack().index()), + TimeToScene(GetTime()) - x, GetTrackHeight(recording_coord_.GetTrack().index())); + } + // Draw standard TimelineViewBase things (such as playhead) super::drawForeground(painter, rect); } @@ -345,6 +374,7 @@ void TimelineView::ToolChangedEvent(Tool::Item tool) case Tool::kAdd: case Tool::kTransition: case Tool::kZoom: + case Tool::kRecord : setCursor(Qt::CrossCursor); break; case Tool::kTrackSelect: @@ -497,21 +527,41 @@ void TimelineView::DrawBlock(QPainter *painter, bool foreground, Block *block, q SceneToTime(block_left - block_in, GetScale(), connected_track_list_->parent()->GetAudioParams().sample_rate_as_time_base())); } - // Draw zebra stripes - if (clip->connected_viewer() && !clip->connected_viewer()->GetLength().isNull()) { - if (clip->media_in() < 0) { - // Draw stripes for sections of clip < 0 - qreal zebra_right = TimeToScene(-clip->media_in()); - if (zebra_right > GetTimelineLeftBound()) { - DrawZebraStripes(painter, QRectF(block_left, block_top, zebra_right, block_height)); + // Draw zebra stripes and markers + if (clip->connected_viewer()) { + if (!clip->connected_viewer()->GetLength().isNull()) { + if (clip->media_in() < 0) { + // Draw stripes for sections of clip < 0 + qreal zebra_right = TimeToScene(-clip->media_in()); + if (zebra_right > GetTimelineLeftBound()) { + DrawZebraStripes(painter, QRectF(block_left, block_top, zebra_right, block_height)); + } + } + + if (clip->length() + clip->media_in() > clip->connected_viewer()->GetLength()) { + // Draw stripes for sections for clip > clip length + qreal zebra_left = TimeToScene(clip->out() - (clip->media_in() + clip->length() - clip->connected_viewer()->GetLength())); + if (zebra_left < GetTimelineRightBound()) { + DrawZebraStripes(painter, QRectF(zebra_left, block_top, block_right - zebra_left, block_height)); + } } } - if (clip->length() + clip->media_in() > clip->connected_viewer()->GetLength()) { - // Draw stripes for sections for clip > clip length - qreal zebra_left = TimeToScene(clip->out() - (clip->media_in() + clip->length() - clip->connected_viewer()->GetLength())); - if (zebra_left < GetTimelineRightBound()) { - DrawZebraStripes(painter, QRectF(zebra_left, block_top, block_right - zebra_left, block_height)); + TimelineMarkerList *marker_list = clip->connected_viewer()->GetTimelinePoints()->markers(); + if (!marker_list->empty()) { + + clip_marker_rects_.clear(); + + for (auto it=marker_list->cbegin(); it!=marker_list->cend(); it++) { + TimelineMarker *marker = *it; + // Make sure marker is within In/Out points of the clip + if (marker->time_range().in() >= clip->media_in() && marker->time_range().out() <= clip->media_in() + clip->length()) { + QPoint marker_pt(TimeToScene(clip->in() - clip->media_in() + marker->time_range().in()), block_top + block_height); + painter->setClipRect(r); + QRect marker_rect = marker->Draw(painter, marker_pt, GetScale(), false); + clip_marker_rects_.insert(marker, marker_rect); + painter->setClipping(false); + } } } } @@ -706,6 +756,19 @@ void TimelineView::SetTransitionOverlay(ClipBlock *out, ClipBlock *in) } } +void TimelineView::EnableRecordingOverlay(const TimelineCoordinate &coord) +{ + recording_overlay_ = true; + recording_coord_ = coord; + viewport()->update(); +} + +void TimelineView::DisableRecordingOverlay() +{ + recording_overlay_ = false; + viewport()->update(); +} + int TimelineView::SceneToTrack(double y) { int track = -1; diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index 878efca76..3d5513637 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -56,6 +56,8 @@ public: void SetBeamCursor(const TimelineCoordinate& coord); void SetTransitionOverlay(ClipBlock *out, ClipBlock *in); + void EnableRecordingOverlay(const TimelineCoordinate &coord); + void DisableRecordingOverlay(); void SetSelectionList(QHash* s) { @@ -155,6 +157,11 @@ private: ClipBlock *transition_overlay_out_; ClipBlock *transition_overlay_in_; + QMap clip_marker_rects_; + + bool recording_overlay_; + TimelineCoordinate recording_coord_; + private slots: void TrackListChanged(); diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index a5845c25c..b5334fe83 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -20,21 +20,29 @@ #include "seekablewidget.h" +#include #include #include #include #include "common/qtutils.h" #include "core.h" +#include "dialog/markerproperties/markerpropertiesdialog.h" +#include "node/project/serializer/serializer.h" +#include "widget/colorlabelmenu/colorlabelmenu.h" +#include "widget/menu/menushared.h" +#include "widget/timebased/timebasedwidget.h" namespace olive { +#define super TimeBasedView + SeekableWidget::SeekableWidget(QWidget* parent) : - TimelineScaledWidget(parent), + super(parent), timeline_points_(nullptr), - scroll_(0), - snap_service_(nullptr), - dragging_(false) + dragging_(false), + ignore_next_focus_out_(false), + selection_manager_(this) { QFontMetrics fm = fontMetrics(); @@ -44,113 +52,214 @@ SeekableWidget::SeekableWidget(QWidget* parent) : playhead_width_ = QtUtils::QFontMetricsWidth(fm, "H"); setContextMenuPolicy(Qt::CustomContextMenu); + setFocusPolicy(Qt::ClickFocus); + + selection_manager_.SetSnapMask(TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToMarkers); } void SeekableWidget::ConnectTimelinePoints(TimelinePoints *points) { if (timeline_points_) { - disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&SeekableWidget::update)); - disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&SeekableWidget::update)); - disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&SeekableWidget::update)); - disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&SeekableWidget::update)); + selection_manager_.ClearSelection(); + + disconnect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, viewport(), static_cast(&QWidget::update)); + disconnect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, viewport(), static_cast(&QWidget::update)); + disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, viewport(), static_cast(&QWidget::update)); + disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, viewport(), static_cast(&QWidget::update)); + disconnect(timeline_points_->markers(), &TimelineMarkerList::MarkerModified, viewport(), static_cast(&QWidget::update)); } timeline_points_ = points; if (timeline_points_) { - connect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, this, static_cast(&SeekableWidget::update)); - connect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, this, static_cast(&SeekableWidget::update)); - connect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, this, static_cast(&SeekableWidget::update)); - connect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, this, static_cast(&SeekableWidget::update)); + connect(timeline_points_->workarea(), &TimelineWorkArea::RangeChanged, viewport(), static_cast(&QWidget::update)); + connect(timeline_points_->workarea(), &TimelineWorkArea::EnabledChanged, viewport(), static_cast(&QWidget::update)); + connect(timeline_points_->markers(), &TimelineMarkerList::MarkerAdded, viewport(), static_cast(&QWidget::update)); + connect(timeline_points_->markers(), &TimelineMarkerList::MarkerRemoved, viewport(), static_cast(&QWidget::update)); + connect(timeline_points_->markers(), &TimelineMarkerList::MarkerModified, viewport(), static_cast(&QWidget::update)); } - update(); + viewport()->update(); } -void SeekableWidget::SetSnapService(SnapService *service) +void SeekableWidget::DeleteSelected() { - snap_service_ = service; + MultiUndoCommand* command = new MultiUndoCommand(); + + foreach (TimelineMarker *marker, selection_manager_.GetSelectedObjects()) { + command->add_child(new MarkerRemoveCommand(marker)); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); } -const int &SeekableWidget::GetScroll() const +bool SeekableWidget::CopySelected(bool cut) { - return scroll_; + if (!selection_manager_.GetSelectedObjects().empty()) { + ProjectSerializer::SaveData sdata(Project::GetProjectFromObject(timeline_points_)); + sdata.SetOnlySerializeMarkers(selection_manager_.GetSelectedObjects()); + + ProjectSerializer::Copy(sdata, QStringLiteral("markers")); + + if (cut) { + DeleteSelected(); + } + + return true; + } else { + return false; + } +} + +bool SeekableWidget::PasteMarkers(bool insert, rational insert_time) +{ + ProjectSerializer::Result res = ProjectSerializer::Paste(QStringLiteral("markers")); + if (res == ProjectSerializer::kSuccess) { + const std::vector &markers = res.GetLoadData().markers; + if (!markers.empty()) { + MultiUndoCommand *command = new MultiUndoCommand(); + + // Normalize markers to start at playhead + rational min = RATIONAL_MAX; + for (auto it=markers.cbegin(); it!=markers.cend(); it++) { + min = qMin(min, (*it)->time()); + } + min -= GetTime(); + + // Avoid duplicates + bool loop; + do { + loop = false; + for (auto it=markers.cbegin(); it!=markers.cend(); it++) { + rational proposed_time = (*it)->time() - min; + + if (timeline_points_->markers()->GetMarkerAtTime(proposed_time)) { + min -= timebase(); + loop = true; + break; + } + } + } while (loop); + + for (auto it=markers.cbegin(); it!=markers.cend(); it++) { + TimelineMarker *m = *it; + + m->set_time(m->time() - min); + + command->add_child(new MarkerAddCommand(timeline_points_->markers(), m)); + } + + Core::instance()->undo_stack()->push(command); + return true; + } + } + + return false; } void SeekableWidget::mousePressEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton) { - SeekToScreenPoint(event->pos().x()); + if (TimelineMarker *initial = selection_manager_.MousePress(event)) { + selection_manager_.DragStart(initial, event); + } else if (!selection_manager_.GetObjectAtPoint(event->pos()) && event->button() == Qt::LeftButton) { + SeekToScenePoint(mapToScene(event->pos()).x()); dragging_ = true; + + DeselectAllMarkers(); } } void SeekableWidget::mouseMoveEvent(QMouseEvent *event) { - if (event->buttons() & Qt::LeftButton) { - SeekToScreenPoint(event->pos().x()); + if (selection_manager_.IsDragging()) { + selection_manager_.DragMove(event); + } else if (dragging_) { + SeekToScenePoint(mapToScene(event->pos()).x()); } } void SeekableWidget::mouseReleaseEvent(QMouseEvent *event) { - Q_UNUSED(event) + if (selection_manager_.IsDragging()) { + MultiUndoCommand *command = new MultiUndoCommand(); + selection_manager_.DragStop(command); + Core::instance()->undo_stack()->pushIfHasChildren(command); + } - if (snap_service_) { - snap_service_->HideSnaps(); + if (GetSnapService()) { + GetSnapService()->HideSnaps(); } dragging_ = false; } -void SeekableWidget::ScaleChangedEvent(const double &) +void SeekableWidget::mouseDoubleClickEvent(QMouseEvent *event) { - update(); + super::mouseDoubleClickEvent(event); + + if (selection_manager_.GetObjectAtPoint(event->pos()) && !selection_manager_.GetSelectedObjects().empty()) { + ShowMarkerProperties(); + } } -TimelinePoints *SeekableWidget::timeline_points() const +void SeekableWidget::focusOutEvent(QFocusEvent *event) { - return timeline_points_; + super::focusOutEvent(event); + + if (ignore_next_focus_out_) { + ignore_next_focus_out_ = false; + } else { + // Deselect everything when we lose focus + DeselectAllMarkers(); + } } -void SeekableWidget::SetTime(const rational &r) +void SeekableWidget::DeselectAllMarkers() { - time_ = r; + selection_manager_.ClearSelection(); - update(); + viewport()->update(); } -void SeekableWidget::SetScroll(int s) +void SeekableWidget::SetMarkerColor(int c) { - scroll_ = s; + MultiUndoCommand *command = new MultiUndoCommand(); - update(); + foreach(TimelineMarker* marker, selection_manager_.GetSelectedObjects()) { + command->add_child(new MarkerChangeColorCommand(marker, c)); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); } -int SeekableWidget::TimeToScreen(const rational &time) const +void SeekableWidget::ShowMarkerProperties() { - return qFloor(TimeToScene(time)) - scroll_; + MarkerPropertiesDialog mpd(selection_manager_.GetSelectedObjects(), timebase(), this); + ignore_next_focus_out_ = true; + mpd.exec(); } -rational SeekableWidget::ScreenToTime(int x) const +void SeekableWidget::TimebaseChangedEvent(const rational &t) { - return qMax(rational(0), SceneToTime(x + scroll_)); + super::TimebaseChangedEvent(t); + + selection_manager_.SetTimebase(t); } -void SeekableWidget::SeekToScreenPoint(int screen) +void SeekableWidget::SeekToScenePoint(qreal scene) { if (timebase().isNull()) { return; } - rational playhead_time = ScreenToTime(screen); + rational playhead_time = SceneToTime(scene); - if (Core::instance()->snapping() && snap_service_) { + if (Core::instance()->snapping() && GetSnapService()) { rational movement; - snap_service_->SnapPoint({playhead_time}, - &movement, - SnapService::kSnapAll & ~SnapService::kSnapToPlayhead); + GetSnapService()->SnapPoint({playhead_time}, + &movement, + TimeBasedWidget::kSnapAll & ~TimeBasedWidget::kSnapToPlayhead); playhead_time += movement; } @@ -162,59 +271,62 @@ void SeekableWidget::SeekToScreenPoint(int screen) } } +void SeekableWidget::SelectionManagerSelectEvent(void *obj) +{ + super::SelectionManagerSelectEvent(obj); + + viewport()->update(); +} + +void SeekableWidget::SelectionManagerDeselectEvent(void *obj) +{ + super::SelectionManagerDeselectEvent(obj); + + viewport()->update(); +} + void SeekableWidget::DrawTimelinePoints(QPainter* p, int marker_bottom) { - if (!timeline_points()) { + if (!GetTimelinePoints()) { return; } + int lim_left = GetScroll(); + int lim_right = lim_left + width(); + + selection_manager_.ClearDrawnObjects(); + // Draw in/out workarea - if (timeline_points()->workarea()->enabled()) { - int workarea_left = qMax(0, TimeToScreen(timeline_points()->workarea()->in())); + if (GetTimelinePoints()->workarea()->enabled()) { + int workarea_left = qMax(qreal(lim_left), TimeToScene(GetTimelinePoints()->workarea()->in())); int workarea_right; - if (timeline_points()->workarea()->out() == TimelineWorkArea::kResetOut) { - workarea_right = width(); + if (GetTimelinePoints()->workarea()->out() == TimelineWorkArea::kResetOut) { + workarea_right = lim_right; } else { - workarea_right = qMin(width(), TimeToScreen(timeline_points()->workarea()->out())); + workarea_right = qMin(qreal(lim_right), TimeToScene(GetTimelinePoints()->workarea()->out())); } p->fillRect(workarea_left, 0, workarea_right - workarea_left, height(), palette().highlight()); } // Draw markers - if (marker_bottom > 0 && !timeline_points()->markers()->list().isEmpty()) { + if (marker_bottom > 0 && !GetTimelinePoints()->markers()->empty()) { + for (auto it=GetTimelinePoints()->markers()->cbegin(); it!=GetTimelinePoints()->markers()->cend(); it++) { + TimelineMarker* marker = *it; - int marker_top = marker_bottom - text_height_; - - // FIXME: Hardcoded marker colors - p->setPen(Qt::black); - p->setBrush(Qt::green); - - foreach (TimelineMarker* marker, timeline_points()->markers()->list()) { - int marker_left = TimeToScreen(marker->time().in()); - int marker_right = TimeToScreen(marker->time().out()); - - if (marker_left >= width() || marker_right < 0) { + int marker_right = TimeToScene(marker->time_range().out()); + if (marker_right < lim_left) { continue; } - if (marker->time().length() == 0) { - // Single point in time marker - DrawPlayhead(p, marker_left, marker_bottom); - } else { - // Marker range - int rect_left = qMax(0, marker_left); - int rect_right = qMin(width(), marker_right); - - QRect marker_rect(rect_left, marker_top, rect_right - rect_left, marker_bottom - marker_top); - - p->drawRect(marker_rect); - - if (!marker->name().isEmpty()) { - p->drawText(marker_rect, marker->name()); - } + int marker_left = TimeToScene(marker->time_range().in()); + if (marker_left >= lim_right) { + break; } + + QRect marker_rect = marker->Draw(p, QPoint(marker_left, marker_bottom), GetScale(), selection_manager_.IsSelected(marker)); + selection_manager_.DeclareDrawnObject(marker, marker_rect); } } } @@ -245,4 +357,31 @@ void SeekableWidget::DrawPlayhead(QPainter *p, int x, int y) p->setRenderHint(QPainter::Antialiasing, false); } +bool SeekableWidget::ShowContextMenu(const QPoint &p) +{ + if (selection_manager_.GetObjectAtPoint(p) && !selection_manager_.GetSelectedObjects().empty()) { + // Show marker-specific menu + Menu m; + + ColorLabelMenu color_coding_menu; + connect(&color_coding_menu, &ColorLabelMenu::ColorSelected, this, &SeekableWidget::SetMarkerColor); + m.addMenu(&color_coding_menu); + + m.addSeparator(); + + MenuShared::instance()->AddItemsForEditMenu(&m, false); + + m.addSeparator(); + + QAction *properties_action = m.addAction(tr("Properties")); + connect(properties_action, &QAction::triggered, this, &SeekableWidget::ShowMarkerProperties); + + ignore_next_focus_out_ = true; + m.exec(mapToGlobal(p)); + return true; + } else { + return false; + } +} + } diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h index 8cccaf64b..1bc8e61de 100644 --- a/app/widget/timeruler/seekablewidget.h +++ b/app/widget/timeruler/seekablewidget.h @@ -21,56 +21,66 @@ #ifndef SEEKABLEWIDGET_H #define SEEKABLEWIDGET_H +#include +#include + #include "common/rational.h" #include "timeline/timelinepoints.h" -#include "widget/snapservice/snapservice.h" -#include "widget/timebased/timescaledobject.h" +#include "widget/menu/menu.h" +#include "widget/timebased/timebasedviewselectionmanager.h" namespace olive { -class SeekableWidget : public TimelineScaledWidget +class SeekableWidget : public TimeBasedView { Q_OBJECT public: SeekableWidget(QWidget *parent = nullptr); - const rational& GetTime() const + int GetScroll() const { - return time_; + return horizontalScrollBar()->value(); } - const int& GetScroll() const; - + TimelinePoints* GetTimelinePoints() const { return timeline_points_; } void ConnectTimelinePoints(TimelinePoints* points); - void SetSnapService(SnapService* service); - bool IsDraggingPlayhead() const { return dragging_; } -public slots: - void SetTime(const rational &r); + void DeleteSelected(); - void SetScroll(int s); + bool CopySelected(bool cut); + + bool PasteMarkers(bool insert, rational insert_time); + + void DeselectAllMarkers(); + + void SeekToScenePoint(qreal scene); + + virtual void SelectionManagerSelectEvent(void *obj) override; + virtual void SelectionManagerDeselectEvent(void *obj) override; + +public slots: + void SetScroll(int i) + { + horizontalScrollBar()->setValue(i); + } + + virtual void TimebaseChangedEvent(const rational &) override; protected: - void SeekToScreenPoint(int screen); - virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseMoveEvent(QMouseEvent *event) override; virtual void mouseReleaseEvent(QMouseEvent *event) override; + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - virtual void ScaleChangedEvent(const double&) override; + virtual void focusOutEvent(QFocusEvent *event) override; void DrawTimelinePoints(QPainter *p, int marker_bottom = 0); - TimelinePoints* timeline_points() const; - - int TimeToScreen(const rational& time) const; - rational ScreenToTime(int x) const; - void DrawPlayhead(QPainter* p, int x, int y); inline const int& text_height() const { @@ -81,27 +91,27 @@ protected: return playhead_width_; } -signals: - /** - * @brief Signal emitted whenever the time changes on this ruler, either by user or programmatically - */ - void TimeChanged(const rational &time); +protected slots: + virtual bool ShowContextMenu(const QPoint &p); private: - rational time_; - TimelinePoints* timeline_points_; - int scroll_; - int text_height_; int playhead_width_; - SnapService* snap_service_; - bool dragging_; + bool ignore_next_focus_out_; + + TimeBasedViewSelectionManager selection_manager_; + +private slots: + void SetMarkerColor(int c); + + void ShowMarkerProperties(); + }; } diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index db887fe7e..61f95b020 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -32,8 +32,10 @@ namespace olive { +#define super SeekableWidget + TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* parent) : - SeekableWidget(parent), + super(parent), text_visible_(text_visible), centered_text_(true), show_cache_status_(cache_status_visible), @@ -58,6 +60,17 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, QWidget* pare // Connect context menu connect(this, &TimeRuler::customContextMenuRequested, this, &TimeRuler::ShowContextMenu); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + //horizontalScrollBar()->setVisible(false); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setBackgroundRole(QPalette::Window); + setFrameShape(QFrame::NoFrame); + + // NOTE: One day it might be preferable to use AlignBottom because the lines are anchored to + // the bottom of the widget. However, for now this makes sense since we just ported this + // from a QWidget's paintEvent. + setAlignment(Qt::AlignLeft | Qt::AlignTop); } void TimeRuler::SetPlaybackCache(PlaybackCache *cache) @@ -83,28 +96,17 @@ void TimeRuler::SetPlaybackCache(PlaybackCache *cache) update(); } -void TimeRuler::paintEvent(QPaintEvent *) +void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) { // Nothing to paint if the timebase is invalid if (timebase().isNull()) { return; } - QPainter p(this); - // Draw timeline points if connected - if (timeline_points()) { - int marker_bottom = height() - text_height(); - - if (show_cache_status_) { - marker_bottom -= cache_status_height_; - } - - if (text_visible_) { - marker_bottom -= cache_status_height_; - } - - DrawTimelinePoints(&p, marker_bottom); + int marker_height = TimelineMarker::GetMarkerHeight(p->fontMetrics()); + if (GetTimelinePoints()) { + DrawTimelinePoints(p, marker_height); } double width_of_frame = timebase_dbl() * GetScale(); @@ -173,11 +175,11 @@ void TimeRuler::paintEvent(QPaintEvent *) } // Set line color to main text color - p.setBrush(Qt::NoBrush); - p.setPen(palette().text().color()); + p->setBrush(Qt::NoBrush); + p->setPen(palette().text().color()); // Calculate line dimensions - QFontMetrics fm = p.fontMetrics(); + QFontMetrics fm = p->fontMetrics(); int line_bottom = height(); if (show_cache_status_) { @@ -197,8 +199,8 @@ void TimeRuler::paintEvent(QPaintEvent *) // FIXME: Hardcoded number const int kAverageTextWidth = 200; - for (int i=-kAverageTextWidth;i(i + GetScroll()); + for (int i=GetScroll()-kAverageTextWidth;i(i); if (long_interval > -1) { int this_long_unit = qFloor(screen_pt/long_interval); @@ -208,16 +210,16 @@ void TimeRuler::paintEvent(QPaintEvent *) if (text_visible_) { QRect text_rect; Qt::Alignment text_align; - QString timecode_str = Timecode::time_to_timecode(ScreenToTime(i), timebase(), Core::instance()->GetTimecodeDisplay()); + QString timecode_str = Timecode::time_to_timecode(SceneToTime(i), timebase(), Core::instance()->GetTimecodeDisplay()); int timecode_width = QtUtils::QFontMetricsWidth(fm, timecode_str); int timecode_left; if (centered_text_) { - text_rect = QRect(i - kAverageTextWidth/2, 0, kAverageTextWidth, fm.height()); + text_rect = QRect(i - kAverageTextWidth/2, marker_height, kAverageTextWidth, fm.height()); text_align = Qt::AlignCenter; timecode_left = i - timecode_width/2; } else { - text_rect = QRect(i, 0, kAverageTextWidth, fm.height()); + text_rect = QRect(i, marker_height, kAverageTextWidth, fm.height()); text_align = Qt::AlignLeft | Qt::AlignVCenter; timecode_left = i; @@ -226,9 +228,9 @@ void TimeRuler::paintEvent(QPaintEvent *) } if (timecode_left > last_text_draw) { - p.drawText(text_rect, - static_cast(text_align), - timecode_str); + p->drawText(text_rect, + static_cast(text_align), + timecode_str); last_text_draw = timecode_left + timecode_width; @@ -238,7 +240,7 @@ void TimeRuler::paintEvent(QPaintEvent *) } } - p.drawLine(i, line_y, i, line_bottom); + p->drawLine(i, line_y, i, line_bottom); last_long_unit = this_long_unit; } } @@ -246,7 +248,7 @@ void TimeRuler::paintEvent(QPaintEvent *) if (short_interval > -1) { int this_short_unit = qFloor(screen_pt/short_interval); if (this_short_unit != last_short_unit) { - p.drawLine(i, short_y, i, line_bottom); + p->drawLine(i, short_y, i, line_bottom); last_short_unit = this_short_unit; } } @@ -256,45 +258,49 @@ void TimeRuler::paintEvent(QPaintEvent *) if (show_cache_status_ && playback_cache_) { // FIXME: Hardcoded to get video length, if we ever need audio length, this will have to change rational len = playback_cache_->viewer_parent()->GetVideoLength(); + int lim_left = GetScroll(); + int lim_right = lim_left + width(); - int cache_screen_length = qMin(TimeToScreen(len), width()); + int cache_screen_length = TimeToScene(len); if (cache_screen_length > 0) { int cache_y = height() - cache_status_height_; - p.fillRect(0, cache_y, cache_screen_length , cache_status_height_, Qt::green); + p->fillRect(0, cache_y, cache_screen_length, cache_status_height_, Qt::green); foreach (const TimeRange& range, playback_cache_->GetInvalidatedRanges(len)) { - int range_left = TimeToScreen(range.in()); + int range_left = TimeToScene(range.in()); if (range_left >= width()) { continue; } - int range_right = TimeToScreen(range.out()); + int range_right = TimeToScene(range.out()); if (range_right < 0) { continue; } - int adjusted_left = qMax(0, range_left); + int adjusted_left = qMax(lim_left, range_left); - p.fillRect(adjusted_left, - cache_y, - qMin(width(), range_right) - adjusted_left, - cache_status_height_, - Qt::red); + p->fillRect(adjusted_left, + cache_y, + qMin(lim_right, range_right) - adjusted_left, + cache_status_height_, + Qt::red); } } } // Draw the playhead if it's on screen at the moment - int playhead_pos = TimeToScreen(GetTime()); - p.setPen(Qt::NoPen); - p.setBrush(PLAYHEAD_COLOR); - DrawPlayhead(&p, playhead_pos, line_bottom); + int playhead_pos = TimeToScene(GetTime()); + p->setPen(Qt::NoPen); + p->setBrush(PLAYHEAD_COLOR); + DrawPlayhead(p, playhead_pos, line_bottom); } void TimeRuler::TimebaseChangedEvent(const rational &tb) { + super::TimebaseChangedEvent(tb); + timebase_flipped_dbl_ = tb.flipped().toDouble(); update(); @@ -305,14 +311,20 @@ int TimeRuler::CacheStatusHeight() const return fontMetrics().height() / 4; } -void TimeRuler::ShowContextMenu() +bool TimeRuler::ShowContextMenu(const QPoint &p) { - Menu m(this); + if (super::ShowContextMenu(p)) { + return true; + } else { + Menu m(this); - MenuShared::instance()->AddItemsForTimeRulerMenu(&m); - MenuShared::instance()->AboutToShowTimeRulerActions(timebase()); + MenuShared::instance()->AddItemsForTimeRulerMenu(&m); + MenuShared::instance()->AboutToShowTimeRulerActions(timebase()); - m.exec(QCursor::pos()); + m.exec(mapToGlobal(p)); + + return true; + } } void TimeRuler::UpdateHeight() @@ -330,7 +342,7 @@ void TimeRuler::UpdateHeight() } // Add marker height - height += text_height(); + height += TimelineMarker::GetMarkerHeight(fontMetrics()); setFixedHeight(height); } diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index 496117238..b6bfc8858 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -41,10 +41,13 @@ public: void SetPlaybackCache(PlaybackCache* cache); protected: - virtual void paintEvent(QPaintEvent* e) override; + virtual void drawForeground(QPainter *painter, const QRectF &rect) override; virtual void TimebaseChangedEvent(const rational& tb) override; +protected slots: + virtual bool ShowContextMenu(const QPoint &p) override; + private: void UpdateHeight(); @@ -64,9 +67,6 @@ private: PlaybackCache* playback_cache_; -private slots: - void ShowContextMenu(); - }; } diff --git a/app/widget/toolbar/toolbar.cpp b/app/widget/toolbar/toolbar.cpp index dd30d4372..ddce86c7a 100644 --- a/app/widget/toolbar/toolbar.cpp +++ b/app/widget/toolbar/toolbar.cpp @@ -126,7 +126,7 @@ void Toolbar::Retranslate() void Toolbar::UpdateIcons() { btn_pointer_tool_->setIcon(icon::ToolPointer); - btn_trackselect_tool_->setIcon(icon::TriRight); // FIXME: Procure real icon + btn_trackselect_tool_->setIcon(icon::ToolTrackSelect); btn_edit_tool_->setIcon(icon::ToolEdit); btn_ripple_tool_->setIcon(icon::ToolRipple); btn_rolling_tool_->setIcon(icon::ToolRolling); diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 198cff332..59f1dc4f6 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -38,6 +38,13 @@ AudioWaveformView::AudioWaveformView(QWidget *parent) : { setAutoFillBackground(true); setBackgroundRole(QPalette::Base); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + // NOTE: At some point it might make sense for this to be AlignCenter since the waveform + // originates from the center. But we're leaving it top/left for now since it was just + // ported from a QWidget's paintEvent. + setAlignment(Qt::AlignLeft | Qt::AlignTop); } void AudioWaveformView::SetViewer(AudioPlaybackCache *playback) @@ -60,9 +67,9 @@ void AudioWaveformView::SetViewer(AudioPlaybackCache *playback) } } -void AudioWaveformView::paintEvent(QPaintEvent *event) +void AudioWaveformView::drawForeground(QPainter *p, const QRectF &rect) { - super::paintEvent(event); + super::drawForeground(p, rect); if (!playback_) { return; @@ -74,20 +81,18 @@ void AudioWaveformView::paintEvent(QPaintEvent *event) return; } - QPainter p(this); - // Draw in/out points - DrawTimelinePoints(&p); + DrawTimelinePoints(p); // Draw waveform - p.setPen(QColor(64, 255, 160)); // FIXME: Hardcoded color - AudioVisualWaveform::DrawWaveform(&p, rect(), GetScale(), playback_->visual(), SceneToTime(GetScroll())); + p->setPen(QColor(64, 255, 160)); // FIXME: Hardcoded color + AudioVisualWaveform::DrawWaveform(p, rect.toRect(), GetScale(), playback_->visual(), SceneToTime(GetScroll())); // Draw playhead - p.setPen(PLAYHEAD_COLOR); + p->setPen(PLAYHEAD_COLOR); - int playhead_x = TimeToScreen(GetTime()); - p.drawLine(playhead_x, 0, playhead_x, height()); + int playhead_x = TimeToScene(GetTime()); + p->drawLine(playhead_x, 0, playhead_x, height()); } } diff --git a/app/widget/viewer/audiowaveformview.h b/app/widget/viewer/audiowaveformview.h index b89e56224..92f34d80e 100644 --- a/app/widget/viewer/audiowaveformview.h +++ b/app/widget/viewer/audiowaveformview.h @@ -39,7 +39,7 @@ public: void SetViewer(AudioPlaybackCache *playback); protected: - virtual void paintEvent(QPaintEvent* event) override; + virtual void drawForeground(QPainter *painter, const QRectF &rect) override; private: QThreadPool pool_; diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index d589b409f..acc7b45c4 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -43,6 +43,7 @@ #include "viewerpreventsleep.h" #include "widget/menu/menu.h" #include "window/mainwindow/mainwindow.h" +#include "widget/timeruler/timeruler.h" namespace olive { @@ -64,7 +65,9 @@ ViewerWidget::ViewerWidget(QWidget *parent) : color_menu_enabled_(true), time_changed_from_timer_(false), prequeuing_video_(false), - prequeuing_audio_(0) + prequeuing_audio_(0), + record_armed_(false), + recording_(false) { // Set up main layout QVBoxLayout* layout = new QVBoxLayout(this); @@ -97,6 +100,7 @@ ViewerWidget::ViewerWidget(QWidget *parent) : // Create waveform view when audio is connected and video isn't waveform_view_ = new AudioWaveformView(); + ConnectTimelineView(waveform_view_, true); PassWheelEventsToScrollBar(waveform_view_); stack_->addWidget(waveform_view_); @@ -125,7 +129,6 @@ ViewerWidget::ViewerWidget(QWidget *parent) : SetScale(48.0); // Ensures that seeking on the waveform view updates the time as expected - connect(waveform_view_, &AudioWaveformView::TimeChanged, this, &ViewerWidget::SetTimeAndSignal); connect(waveform_view_, &AudioWaveformView::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu); connect(&playback_backup_timer_, &QTimer::timeout, this, &ViewerWidget::PlaybackTimerUpdate); @@ -157,6 +160,10 @@ void ViewerWidget::TimeChangedEvent(const rational &time) PauseInternal(); } + if (record_armed_) { + DisarmRecording(); + } + controls_->SetTime(time); waveform_view_->SetTime(time); @@ -376,6 +383,17 @@ void ViewerWidget::SetGizmos(Node *node) display_widget_->SetGizmos(node); } +void ViewerWidget::StartCapture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track) +{ + SetTimeAndSignal(time.in()); + ArmForRecording(); + + recording_filename_ = QStringLiteral("/home/matt/Desktop/ass.mp3"); + recording_callback_ = source; + recording_range_ = time; + recording_track_ = track; +} + FramePtr ViewerWidget::DecodeCachedImage(const QString &cache_path, const QByteArray& hash, const rational& time) { FramePtr frame = FrameHashCache::LoadCacheFrame(cache_path, hash); @@ -427,6 +445,18 @@ void ViewerWidget::DecrementPrequeuedAudio() } } +void ViewerWidget::ArmForRecording() +{ + controls_->StartPlayBlink(); + record_armed_ = true; +} + +void ViewerWidget::DisarmRecording() +{ + controls_->StopPlayBlink(); + record_armed_ = false; +} + void ViewerWidget::QueueNextAudioBuffer() { rational queue_end = audio_playback_queue_time_ + (kAudioPlaybackInterval * playback_speed_); @@ -594,13 +624,20 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) viewer->auto_cacher_.SetAudioPaused(true); } + // Disarm recording if armed + if (record_armed_) { + DisarmRecording(); + } + // If the playhead is beyond the end, restart at 0 - rational last_frame = GetConnectedNode()->GetLength() - timebase(); - if (!in_to_out_only && GetTime() >= last_frame) { - if (speed > 0) { - SetTimeAndSignal(0); - } else { - SetTimeAndSignal(last_frame); + if (!recording_) { + rational last_frame = GetConnectedNode()->GetLength() - timebase(); + if (!in_to_out_only && GetTime() >= last_frame) { + if (speed > 0) { + SetTimeAndSignal(0); + } else { + SetTimeAndSignal(last_frame); + } } } @@ -656,6 +693,15 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) void ViewerWidget::PauseInternal() { + if (recording_) { + AudioManager::instance()->StopRecording(); + recording_ = false; + controls_->SetPauseButtonRecordingState(false); + + recording_callback_->DisableRecordingOverlay(); + recording_callback_->RecordingCallback(recording_filename_, recording_range_, recording_track_); + } + if (IsPlaying()) { playback_speed_ = 0; controls_->ShowPlayButton(); @@ -1120,6 +1166,17 @@ void ViewerWidget::Play(bool in_to_out_only) } else { in_to_out_only = false; } + } else if (record_armed_) { + if (AudioManager::instance()->StartRecording(recording_filename_, GetConnectedNode()->GetAudioParams())) { + recording_ = true; + controls_->SetPauseButtonRecordingState(true); + recording_callback_->EnableRecordingOverlay(TimelineCoordinate(recording_range_.in(), recording_track_)); + } else { + QMessageBox::critical(this, tr("Audio Recording"), tr("Failed to start audio recording")); + return; + } + + DisarmRecording(); } PlayInternal(1, in_to_out_only); @@ -1204,7 +1261,13 @@ void ViewerWidget::PlaybackTimerUpdate() rational min_time, max_time; - if (play_in_to_out_only_ && GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { + if (recording_ && recording_range_.out() != recording_range_.in()) { + + // Limit recording range if applicable + min_time = recording_range_.in(); + max_time = recording_range_.out(); + + } else if (play_in_to_out_only_ && GetConnectedNode()->GetTimelinePoints()->workarea()->enabled()) { // If "play in to out" is enabled or we're looping AND we have a workarea, only play the workarea min_time = GetConnectedNode()->GetTimelinePoints()->workarea()->in(); @@ -1228,8 +1291,9 @@ void ViewerWidget::PlaybackTimerUpdate() bool end_of_line = false; bool play_after_pause = false; - if ((playback_speed_ < 0 && current_time <= min_time) - || (playback_speed_ > 0 && current_time >= max_time)) { + if ((!recording_ || recording_range_.out() != recording_range_.in()) + && ((playback_speed_ < 0 && current_time <= min_time) + || (playback_speed_ > 0 && current_time >= max_time))) { // Determine which timestamp we tripped rational tripped_time; @@ -1244,7 +1308,7 @@ void ViewerWidget::PlaybackTimerUpdate() // or restart playback end_of_line = true; - if (Config::Current()[QStringLiteral("Loop")].toBool()) { + if (Config::Current()[QStringLiteral("Loop")].toBool() && !recording_) { // If we're looping, jump to the other side of the workarea and continue time_to_set = (tripped_time == min_time) ? max_time : min_time; diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index e8ef472c9..4bf043af1 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -41,6 +41,7 @@ #include "viewerwindow.h" #include "widget/playbackcontrols/playbackcontrols.h" #include "widget/timebased/timebasedwidget.h" +#include "widget/timelinewidget/timelinewidget.h" namespace olive { @@ -87,6 +88,8 @@ public: void SetGizmos(Node* node); + void StartCapture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track); + public slots: void Play(bool in_to_out_only); @@ -205,6 +208,10 @@ private: void DecrementPrequeuedAudio(); + void ArmForRecording(); + + void DisarmRecording(); + QStackedWidget* stack_; ViewerSizer* sizer_; @@ -255,6 +262,13 @@ private: static QVector instances_; + bool record_armed_; + bool recording_; + TimelineWidget *recording_callback_; + TimeRange recording_range_; + Track::Reference recording_track_; + QString recording_filename_; + private slots: void PlaybackTimerUpdate(); diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index a74d65d75..aeabbd3af 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -519,7 +519,7 @@ void ViewerDisplayWidget::OnPaint() QPainter p(inner_widget()); p.setWorldTransform(GenerateWorldTransform()); - p.setPen(Qt::lightGray); + p.setPen(QPen(Qt::lightGray, 0)); p.setBrush(Qt::NoBrush); int x = 0, y = 0, w = width(), h = height(); diff --git a/app/widget/viewer/viewertexteditor.cpp b/app/widget/viewer/viewertexteditor.cpp index a3480a67c..cdcc769ee 100644 --- a/app/widget/viewer/viewertexteditor.cpp +++ b/app/widget/viewer/viewertexteditor.cpp @@ -359,7 +359,7 @@ ViewerTextEditorToolBar::ViewerTextEditorToolBar(QWidget *parent) : strikethrough_btn_ = new QPushButton(); connect(strikethrough_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::StrikethroughChanged); strikethrough_btn_->setCheckable(true); - strikethrough_btn_->setText(tr("S")); // FIXME: Source icon + strikethrough_btn_->setIcon(icon::TextStrikethrough); basic_layout->addWidget(strikethrough_btn_); basic_layout->addWidget(QtUtils::CreateVerticalLine()); @@ -441,7 +441,8 @@ ViewerTextEditorToolBar::ViewerTextEditorToolBar(QWidget *parent) : connect(line_height_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::LineHeightChanged); advanced_layout->addWidget(line_height_slider_); - small_caps_btn_ = new QPushButton(tr("Small Caps")); // FIXME: Procure icon + small_caps_btn_ = new QPushButton(); + small_caps_btn_->setIcon(icon::TextSmallCaps); small_caps_btn_->setCheckable(true); connect(small_caps_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::SmallCapsChanged); advanced_layout->addWidget(small_caps_btn_); diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index f956e9051..5a5fc6693 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -555,6 +555,7 @@ TimelinePanel* MainWindow::AppendTimelinePanel() connect(panel, &TimelinePanel::TimeChanged, curve_panel_, &ParamPanel::SetTime); connect(panel, &TimelinePanel::TimeChanged, param_panel_, &ParamPanel::SetTime); connect(panel, &TimelinePanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTime); + connect(panel, &TimelinePanel::RequestCaptureStart, sequence_viewer_panel_, &SequenceViewerPanel::StartCapture); connect(panel, &TimelinePanel::BlockSelectionChanged, this, &MainWindow::TimelinePanelSelectionChanged); connect(param_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTime); connect(curve_panel_, &ParamPanel::TimeChanged, panel, &TimelinePanel::SetTime);