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