implement entire project in nodes
Project items are now represented directly in nodes opening up more versatility and possibilities. This is the first iteration of this and will be buggy. Need to test thoroughly.
This commit is contained in:
@@ -27,80 +27,65 @@
|
||||
|
||||
namespace olive {
|
||||
|
||||
Item::Type Folder::type() const
|
||||
Folder::Folder()
|
||||
{
|
||||
return kFolder;
|
||||
}
|
||||
|
||||
bool Folder::CanHaveChildren() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QIcon Folder::icon()
|
||||
QIcon Folder::icon() const
|
||||
{
|
||||
return icon::Folder;
|
||||
}
|
||||
|
||||
void Folder::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, uint version, const QAtomicInt *cancelled)
|
||||
bool ChildExistsWithNameInternal(const Folder* n, const QString& s)
|
||||
{
|
||||
XMLAttributeLoop(reader, attr) {
|
||||
if (cancelled && *cancelled) {
|
||||
return;
|
||||
}
|
||||
foreach (const Node::OutputConnection& c, n->output_connections()) {
|
||||
Node* connected = c.second.node();
|
||||
|
||||
if (attr.name() == QStringLiteral("name")) {
|
||||
set_name(attr.value().toString());
|
||||
} else if (attr.name() == QStringLiteral("ptr")) {
|
||||
xml_node_data.item_ptrs.insert(attr.value().toULongLong(), this);
|
||||
}
|
||||
}
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (cancelled && *cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Item* child;
|
||||
|
||||
if (reader->name() == QStringLiteral("folder")) {
|
||||
child = new Folder();
|
||||
} else if (reader->name() == QStringLiteral("footage")) {
|
||||
child = new Footage();
|
||||
} else if (reader->name() == QStringLiteral("sequence")) {
|
||||
child = new Sequence();
|
||||
if (connected->GetLabel() == s) {
|
||||
return true;
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
continue;
|
||||
}
|
||||
Folder* subfolder = dynamic_cast<Folder*>(connected);
|
||||
|
||||
child->setParent(this);
|
||||
child->Load(reader, xml_node_data, version, cancelled);
|
||||
if (subfolder && ChildExistsWithNameInternal(subfolder, s)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Folder::Save(QXmlStreamWriter *writer) const
|
||||
bool Folder::ChildExistsWithName(const QString &s) const
|
||||
{
|
||||
writer->writeAttribute(QStringLiteral("name"), name());
|
||||
return ChildExistsWithNameInternal(this, s);
|
||||
}
|
||||
|
||||
writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast<quintptr>(this)));
|
||||
void Folder::OutputConnectedEvent(const QString &output, const NodeInput &input)
|
||||
{
|
||||
Q_UNUSED(output)
|
||||
|
||||
foreach (Item* child, children()) {
|
||||
switch (child->type()) {
|
||||
case Item::kFootage:
|
||||
writer->writeStartElement(QStringLiteral("footage"));
|
||||
break;
|
||||
case Item::kSequence:
|
||||
writer->writeStartElement(QStringLiteral("sequence"));
|
||||
break;
|
||||
case Item::kFolder:
|
||||
writer->writeStartElement(QStringLiteral("folder"));
|
||||
break;
|
||||
}
|
||||
Item* item = dynamic_cast<Item*>(input.node());
|
||||
|
||||
child->Save(writer);
|
||||
if (item) {
|
||||
// The insert index is always our "count" because we only support appending in our internal
|
||||
// model. For sorting/organizing, a QSortFilterProxyModel is used instead.
|
||||
emit BeginInsertItem(item, item_child_count());
|
||||
item_children_.append(item);
|
||||
emit EndInsertItem();
|
||||
}
|
||||
}
|
||||
|
||||
writer->writeEndElement(); // footage/folder/sequence
|
||||
void Folder::OutputDisconnectedEvent(const QString &output, const NodeInput &input)
|
||||
{
|
||||
Q_UNUSED(output)
|
||||
|
||||
Item* item = dynamic_cast<Item*>(input.node());
|
||||
|
||||
if (item) {
|
||||
int child_index = item_children_.indexOf(item);
|
||||
emit BeginRemoveItem(item, child_index);
|
||||
item_children_.removeAt(child_index);
|
||||
emit EndRemoveItem();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#ifndef FOLDER_H
|
||||
#define FOLDER_H
|
||||
|
||||
#include "node/node.h"
|
||||
#include "project/item/item.h"
|
||||
|
||||
namespace olive {
|
||||
@@ -33,20 +34,113 @@ namespace olive {
|
||||
*/
|
||||
class Folder : public Item
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Folder() = default;
|
||||
Folder();
|
||||
|
||||
virtual Type type() const override;
|
||||
virtual Node* copy() const override
|
||||
{
|
||||
return new Folder();
|
||||
}
|
||||
|
||||
virtual bool CanHaveChildren() const override;
|
||||
virtual QString Name() const override
|
||||
{
|
||||
return tr("Folder");
|
||||
}
|
||||
|
||||
virtual QIcon icon() override;
|
||||
virtual QString id() const override
|
||||
{
|
||||
return QStringLiteral("org.olivevideoeditor.Olive.folder");
|
||||
}
|
||||
|
||||
virtual void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt *cancelled) override;
|
||||
virtual QVector<CategoryID> Category() const override
|
||||
{
|
||||
return {kCategoryProject};
|
||||
}
|
||||
|
||||
virtual void Save(QXmlStreamWriter* writer) const override;
|
||||
virtual QString Description() const override
|
||||
{
|
||||
return tr("Organize several items into a single collection.");
|
||||
}
|
||||
|
||||
virtual QIcon icon() const override;
|
||||
|
||||
bool ChildExistsWithName(const QString& s) const;
|
||||
|
||||
int item_child_count() const
|
||||
{
|
||||
return item_children_.size();
|
||||
}
|
||||
|
||||
Item* item_child(int i) const
|
||||
{
|
||||
return item_children_.at(i);
|
||||
}
|
||||
|
||||
const QVector<Item*>& children() const
|
||||
{
|
||||
return item_children_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a list of nodes that are of a certain type that this node outputs to
|
||||
*/
|
||||
template <typename T>
|
||||
QVector<T*> ListOutputsOfType(bool recursive = true) const
|
||||
{
|
||||
QVector<T *> list;
|
||||
|
||||
ListOutputsOfTypeInternal(this, list, recursive);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
int index_of_child(Item* item) const
|
||||
{
|
||||
return item_children_.indexOf(item);
|
||||
}
|
||||
|
||||
signals:
|
||||
void BeginInsertItem(Item* n, int index);
|
||||
|
||||
void EndInsertItem();
|
||||
|
||||
void BeginRemoveItem(Item* n, int index);
|
||||
|
||||
void EndRemoveItem();
|
||||
|
||||
protected:
|
||||
virtual void OutputConnectedEvent(const QString& output, const NodeInput& input) override;
|
||||
|
||||
virtual void OutputDisconnectedEvent(const QString& output, const NodeInput& input) override;
|
||||
|
||||
private:
|
||||
template<typename T>
|
||||
static void ListOutputsOfTypeInternal(const Folder* n, QVector<T*>& list, bool recursive)
|
||||
{
|
||||
foreach (const Node::OutputConnection& c, n->output_connections()) {
|
||||
Node* connected = c.second.node();
|
||||
|
||||
T* cast_test = dynamic_cast<T*>(connected);
|
||||
|
||||
if (cast_test) {
|
||||
// Avoid duplicates
|
||||
if (!list.contains(cast_test)) {
|
||||
list.append(cast_test);
|
||||
}
|
||||
}
|
||||
|
||||
if (recursive) {
|
||||
Folder* subfolder = dynamic_cast<Folder*>(connected);
|
||||
|
||||
if (subfolder) {
|
||||
ListOutputsOfTypeInternal(subfolder, list, recursive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVector<Item*> item_children_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -17,13 +17,9 @@
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
project/item/footage/audiostream.h
|
||||
project/item/footage/audiostream.cpp
|
||||
project/item/footage/footage.h
|
||||
project/item/footage/footage.cpp
|
||||
project/item/footage/stream.h
|
||||
project/item/footage/footage.h
|
||||
project/item/footage/stream.cpp
|
||||
project/item/footage/videostream.h
|
||||
project/item/footage/videostream.cpp
|
||||
project/item/footage/stream.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2020 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 "audiostream.h"
|
||||
|
||||
#include "common/xmlutils.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
AudioStream::AudioStream()
|
||||
{
|
||||
set_type(kAudio);
|
||||
}
|
||||
|
||||
QString AudioStream::description() const
|
||||
{
|
||||
return QCoreApplication::translate("Stream", "%1: Audio - %2 Channel(s), %3Hz")
|
||||
.arg(QString::number(index()),
|
||||
QString::number(channels()),
|
||||
QString::number(sample_rate()));
|
||||
}
|
||||
|
||||
const int &AudioStream::channels() const
|
||||
{
|
||||
return channels_;
|
||||
}
|
||||
|
||||
void AudioStream::set_channels(const int &channels)
|
||||
{
|
||||
channels_ = channels;
|
||||
}
|
||||
|
||||
const uint64_t &AudioStream::channel_layout() const
|
||||
{
|
||||
return layout_;
|
||||
}
|
||||
|
||||
void AudioStream::set_channel_layout(const uint64_t &layout)
|
||||
{
|
||||
layout_ = layout;
|
||||
}
|
||||
|
||||
const int &AudioStream::sample_rate() const
|
||||
{
|
||||
return sample_rate_;
|
||||
}
|
||||
|
||||
void AudioStream::set_sample_rate(const int &sample_rate)
|
||||
{
|
||||
sample_rate_ = sample_rate;
|
||||
}
|
||||
|
||||
QIcon AudioStream::icon() const
|
||||
{
|
||||
return icon::Audio;
|
||||
}
|
||||
|
||||
void AudioStream::LoadCustomParameters(QXmlStreamReader *reader)
|
||||
{
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("channels")) {
|
||||
set_channels(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("layout")) {
|
||||
set_channel_layout(reader->readElementText().toULongLong());
|
||||
} else if (reader->name() == QStringLiteral("rate")) {
|
||||
set_sample_rate(reader->readElementText().toInt());
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioStream::SaveCustomParameters(QXmlStreamWriter *writer) const
|
||||
{
|
||||
writer->writeTextElement(QStringLiteral("channels"), QString::number(channels_));
|
||||
writer->writeTextElement(QStringLiteral("layout"), QString::number(layout_));
|
||||
writer->writeTextElement(QStringLiteral("rate"), QString::number(sample_rate_));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2020 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 AUDIOSTREAM_H
|
||||
#define AUDIOSTREAM_H
|
||||
|
||||
#include <QVector>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "stream.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
/**
|
||||
* @brief A Stream derivative containing audio-specific information
|
||||
*/
|
||||
class AudioStream : public Stream
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioStream();
|
||||
|
||||
virtual QString description() const override;
|
||||
|
||||
const int& channels() const;
|
||||
void set_channels(const int& channels);
|
||||
|
||||
const uint64_t& channel_layout() const;
|
||||
void set_channel_layout(const uint64_t& channel_layout);
|
||||
|
||||
const int& sample_rate() const;
|
||||
void set_sample_rate(const int& sample_rate);
|
||||
|
||||
virtual QIcon icon() const override;
|
||||
|
||||
protected:
|
||||
virtual void LoadCustomParameters(QXmlStreamReader *reader) override;
|
||||
|
||||
virtual void SaveCustomParameters(QXmlStreamWriter* writer) const override;
|
||||
|
||||
private:
|
||||
int channels_;
|
||||
uint64_t layout_;
|
||||
int sample_rate_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // AUDIOSTREAM_H
|
||||
@@ -22,28 +22,51 @@
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "codec/decoder.h"
|
||||
#include "common/filefunctions.h"
|
||||
#include "common/xmlutils.h"
|
||||
#include "config/config.h"
|
||||
#include "core.h"
|
||||
#include "render/job/footagejob.h"
|
||||
#include "ui/icons/icons.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
Footage::Footage()
|
||||
const QString Footage::kFilenameInput = QStringLiteral("file_in");
|
||||
const QString Footage::kStreamPropertiesFormat = QStringLiteral("stream_properties:%1");
|
||||
|
||||
#define super Item
|
||||
|
||||
Footage::Footage(const QString &filename) :
|
||||
super(true, false),
|
||||
stream_count_(0),
|
||||
cancelled_(nullptr)
|
||||
{
|
||||
AddInput(kFilenameInput, NodeValue::kFile, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
|
||||
|
||||
Clear();
|
||||
|
||||
set_filename(filename);
|
||||
}
|
||||
|
||||
Footage::~Footage()
|
||||
void Footage::Retranslate()
|
||||
{
|
||||
ClearStreams();
|
||||
super::Retranslate();
|
||||
|
||||
SetInputName(kFilenameInput, tr("Filename"));
|
||||
|
||||
for (auto it=inputs_for_stream_properties_.cbegin(); it!=inputs_for_stream_properties_.cend(); it++) {
|
||||
StreamReference ref = GetReferenceFromRealIndex(it.key());
|
||||
|
||||
SetInputName(it.value(), QStringLiteral("%1 %2").arg(GetStreamTypeName(ref.type()), QString::number(ref.index())));
|
||||
}
|
||||
}
|
||||
|
||||
void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt* cancelled)
|
||||
void Footage::LoadInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt* cancelled)
|
||||
{
|
||||
Q_UNUSED(xml_node_data)
|
||||
Q_UNUSED(version)
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
@@ -51,16 +74,8 @@ void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uint ve
|
||||
return;
|
||||
}
|
||||
|
||||
if (reader->name() == QStringLiteral("name")) {
|
||||
set_name(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("filename")) {
|
||||
set_filename(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("stream")) {
|
||||
add_stream(Stream::Load(reader, xml_node_data, cancelled));
|
||||
} else if (reader->name() == QStringLiteral("timestamp")) {
|
||||
if (reader->name() == QStringLiteral("timestamp")) {
|
||||
set_timestamp(reader->readElementText().toLongLong());
|
||||
} else if (reader->name() == QStringLiteral("decoder")) {
|
||||
set_decoder(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("points")) {
|
||||
TimelinePoints::Load(reader);
|
||||
} else {
|
||||
@@ -69,28 +84,182 @@ void Footage::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uint ve
|
||||
}
|
||||
}
|
||||
|
||||
void Footage::Save(QXmlStreamWriter *writer) const
|
||||
void Footage::SaveInternal(QXmlStreamWriter *writer) const
|
||||
{
|
||||
writer->writeTextElement(QStringLiteral("name"), name());
|
||||
writer->writeTextElement(QStringLiteral("filename"), filename());
|
||||
writer->writeTextElement(QStringLiteral("timestamp"), QString::number(timestamp_));
|
||||
writer->writeTextElement(QStringLiteral("decoder"), decoder_);
|
||||
|
||||
writer->writeStartElement(QStringLiteral("points"));
|
||||
TimelinePoints::Save(writer);
|
||||
TimelinePoints::Save(writer);
|
||||
writer->writeEndElement(); // points
|
||||
}
|
||||
|
||||
foreach (Stream* stream, streams_) {
|
||||
writer->writeStartElement(QStringLiteral("stream"));
|
||||
stream->Save(writer);
|
||||
writer->writeEndElement(); // stream
|
||||
void Footage::InputValueChangedEvent(const QString &input, int element)
|
||||
{
|
||||
Q_UNUSED(element)
|
||||
|
||||
if (input == kFilenameInput) {
|
||||
// Reset internal stream cache
|
||||
Clear();
|
||||
|
||||
// Determine if file still exists
|
||||
QFileInfo info(filename());
|
||||
|
||||
if (info.exists()) {
|
||||
// Grab timestamp
|
||||
set_timestamp(info.lastModified().toMSecsSinceEpoch());
|
||||
|
||||
// Determine if we've already cached the metadata of this file
|
||||
QString meta_cache_file = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)).filePath(FileFunctions::GetUniqueFileIdentifier(filename()));
|
||||
|
||||
MetadataCache footage_info;
|
||||
|
||||
if (QFileInfo::exists(meta_cache_file)) {
|
||||
|
||||
// Load meta cache file
|
||||
footage_info = LoadStreamCache(meta_cache_file);
|
||||
|
||||
} else {
|
||||
|
||||
// Probe and create cache
|
||||
QVector<DecoderPtr> decoder_list = Decoder::ReceiveListOfAllDecoders();
|
||||
|
||||
foreach (DecoderPtr decoder, decoder_list) {
|
||||
footage_info.streams = decoder->Probe(filename(), cancelled_);
|
||||
|
||||
if (!footage_info.streams.isEmpty()) {
|
||||
footage_info.decoder = decoder->id();
|
||||
SetValid();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!SaveStreamCache(meta_cache_file, footage_info)) {
|
||||
qWarning() << "Failed to save stream cache, footage will have to be re-probed";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
stream_count_ = footage_info.streams.size();
|
||||
|
||||
if (!footage_info.streams.isEmpty()) {
|
||||
set_decoder(footage_info.decoder);
|
||||
|
||||
for (int i=0; i<footage_info.streams.size(); i++) {
|
||||
const Stream& s = footage_info.streams.at(i);
|
||||
|
||||
if (s.type() == Stream::kVideo || s.type() == Stream::kAudio) {
|
||||
QString id = GetInputIDOfIndex(i);
|
||||
|
||||
NodeValue::Type type;
|
||||
|
||||
if (s.type() == Stream::kVideo) {
|
||||
type = NodeValue::kVideoStreamProperties;
|
||||
} else {
|
||||
type = NodeValue::kAudioStreamProperties;
|
||||
}
|
||||
|
||||
// Create input for properties
|
||||
AddInput(id, type, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
|
||||
SetStandardValue(id, QVariant::fromValue(s));
|
||||
inputs_for_stream_properties_.insert(i, id);
|
||||
|
||||
// Create output for stream
|
||||
Footage::StreamReference ref = GetReferenceFromRealIndex(i);
|
||||
QString out_id = GetStringFromReference(ref);
|
||||
AddOutput(out_id);
|
||||
outputs_for_streams_.insert(i, out_id);
|
||||
}
|
||||
}
|
||||
|
||||
SetValid();
|
||||
}
|
||||
|
||||
} else {
|
||||
set_timestamp(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString Footage::DescribeStream(int index) const
|
||||
{
|
||||
Stream stream = GetStreamAt(index);
|
||||
|
||||
switch (stream.type()) {
|
||||
case Stream::kVideo:
|
||||
if (stream.video_type() == Stream::kVideoTypeStill) {
|
||||
return tr("%1: Image - %2x%3").arg(QString::number(index),
|
||||
QString::number(stream.width()),
|
||||
QString::number(stream.height()));
|
||||
} else {
|
||||
return tr("%1: Video - %2x%3").arg(QString::number(index),
|
||||
QString::number(stream.width()),
|
||||
QString::number(stream.height()));
|
||||
}
|
||||
case Stream::kAudio:
|
||||
return QCoreApplication::translate("Stream", "%1: Audio - %2 Channel(s), %3Hz")
|
||||
.arg(QString::number(index),
|
||||
QString::number(stream.channel_count()),
|
||||
QString::number(stream.sample_rate()));
|
||||
case Stream::kUnknown:
|
||||
case Stream::kData:
|
||||
case Stream::kSubtitle:
|
||||
case Stream::kAttachment:
|
||||
break;
|
||||
}
|
||||
|
||||
return tr("%1: Unknown").arg(QString::number(index));
|
||||
}
|
||||
|
||||
Footage::StreamReference Footage::GetReferenceFromOutput(const QString &s) const
|
||||
{
|
||||
Stream::Type type = GetTypeFromOutput(s);
|
||||
|
||||
if (type != Stream::kUnknown) {
|
||||
bool ok;
|
||||
int index = s.mid(2).toInt(&ok);
|
||||
|
||||
if (ok) {
|
||||
return StreamReference(this, type, index);
|
||||
}
|
||||
}
|
||||
|
||||
return StreamReference();
|
||||
}
|
||||
|
||||
int Footage::GetStreamTypeCount(Stream::Type type) const
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
for (auto it=inputs_for_stream_properties_.cbegin(); it!=inputs_for_stream_properties_.cend(); it++) {
|
||||
Stream s = GetStreamAt(it.key());
|
||||
|
||||
if (s.type() == type) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void Footage::Clear()
|
||||
{
|
||||
// Clear all streams
|
||||
ClearStreams();
|
||||
// Clear all dynamically created inputs
|
||||
foreach (const QString& s, inputs_for_stream_properties_) {
|
||||
RemoveInput(s);
|
||||
}
|
||||
inputs_for_stream_properties_.clear();
|
||||
|
||||
// Clear all dynamically created outputs
|
||||
foreach (const QString& s, outputs_for_streams_) {
|
||||
RemoveOutput(s);
|
||||
}
|
||||
outputs_for_streams_.clear();
|
||||
|
||||
// Reset stream count
|
||||
stream_count_ = 0;
|
||||
|
||||
// Clear decoder link
|
||||
decoder_.clear();
|
||||
|
||||
// Reset ready state
|
||||
valid_ = false;
|
||||
@@ -101,14 +270,14 @@ void Footage::SetValid()
|
||||
valid_ = true;
|
||||
}
|
||||
|
||||
const QString &Footage::filename() const
|
||||
QString Footage::filename() const
|
||||
{
|
||||
return filename_;
|
||||
return GetStandardValue(kFilenameInput).toString();
|
||||
}
|
||||
|
||||
void Footage::set_filename(const QString &s)
|
||||
{
|
||||
filename_ = s;
|
||||
SetStandardValue(kFilenameInput, s);
|
||||
}
|
||||
|
||||
const qint64 &Footage::timestamp() const
|
||||
@@ -121,37 +290,117 @@ void Footage::set_timestamp(const qint64 &t)
|
||||
timestamp_ = t;
|
||||
}
|
||||
|
||||
void Footage::add_stream(Stream* s)
|
||||
int64_t Footage::GetTimeInTimebaseUnits(int index, const rational &time) const
|
||||
{
|
||||
// Set its footage parent to this
|
||||
s->setParent(this);
|
||||
Stream s = GetStreamAt(index);
|
||||
|
||||
// Add a copy of this stream to the list
|
||||
streams_.append(s);
|
||||
}
|
||||
|
||||
void Footage::add_streams(const QVector<Stream *> &streams)
|
||||
{
|
||||
foreach (Stream* s, streams) {
|
||||
s->setParent(this);
|
||||
if (!s.IsValid()) {
|
||||
return AV_NOPTS_VALUE;
|
||||
}
|
||||
|
||||
streams_.append(streams);
|
||||
return Timecode::time_to_timestamp(time, s.timebase()) + s.start_time();
|
||||
}
|
||||
|
||||
Stream* Footage::stream(int index) const
|
||||
int Footage::GetRealStreamIndex(Stream::Type type, int index) const
|
||||
{
|
||||
return streams_.at(index);
|
||||
int lookup_index = 0;
|
||||
|
||||
for (auto it=inputs_for_stream_properties_.cbegin(); it!=inputs_for_stream_properties_.cend(); it++) {
|
||||
Stream stream = GetStandardValue(it.value()).value<Stream>();
|
||||
|
||||
if (stream.type() == type) {
|
||||
if (lookup_index == index) {
|
||||
return it.key();
|
||||
} else {
|
||||
lookup_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int Footage::stream_count() const
|
||||
QString Footage::GetStringFromReference(Stream::Type type, int index)
|
||||
{
|
||||
return streams_.size();
|
||||
QString type_string;
|
||||
|
||||
if (type == Stream::kVideo) {
|
||||
type_string = QStringLiteral("v");
|
||||
} else if (type == Stream::kAudio) {
|
||||
type_string = QStringLiteral("a");
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QStringLiteral("%1:%2").arg(type_string, QString::number(index));
|
||||
}
|
||||
|
||||
Item::Type Footage::type() const
|
||||
Footage::StreamReference Footage::GetReferenceFromRealIndex(int real_index) const
|
||||
{
|
||||
return kFootage;
|
||||
Stream s = GetStreamAt(real_index);
|
||||
|
||||
if (!s.IsValid()) {
|
||||
// Return invalid/null reference
|
||||
return StreamReference();
|
||||
}
|
||||
|
||||
int index_in_type = 0;
|
||||
for (auto it=inputs_for_stream_properties_.cbegin(); it!=inputs_for_stream_properties_.cend(); it++) {
|
||||
if (it.key() == real_index) {
|
||||
break;
|
||||
} else {
|
||||
Stream temp = GetStreamAt(it.key());
|
||||
|
||||
if (temp.type() == s.type()) {
|
||||
index_in_type++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return StreamReference(this, s.type(), index_in_type);
|
||||
}
|
||||
|
||||
Stream::Type Footage::GetTypeFromOutput(const QString &s) const
|
||||
{
|
||||
if (s.at(1) == ':') {
|
||||
if (s.at(0) == 'v') {
|
||||
// Video stream
|
||||
return Stream::kVideo;
|
||||
} else if (s.at(0) == 'a') {
|
||||
// Audio stream
|
||||
return Stream::kAudio;
|
||||
}
|
||||
}
|
||||
|
||||
return Stream::kUnknown;
|
||||
}
|
||||
|
||||
Stream Footage::GetFirstEnabledStreamOfType(Stream::Type type) const
|
||||
{
|
||||
for (auto it=inputs_for_stream_properties_.cbegin(); it!=inputs_for_stream_properties_.cend(); it++) {
|
||||
Stream stream = GetStandardValue(it.value()).value<Stream>();
|
||||
|
||||
if (stream.enabled() && stream.type() == type) {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
return Stream();
|
||||
}
|
||||
|
||||
QVector<int> Footage::GetStreamIndexesOfType(Stream::Type type) const
|
||||
{
|
||||
QVector<int> indexes;
|
||||
|
||||
for (auto it=inputs_for_stream_properties_.cbegin(); it!=inputs_for_stream_properties_.cend(); it++) {
|
||||
Stream stream = GetStandardValue(it.value()).value<Stream>();
|
||||
|
||||
if (stream.enabled() && stream.type() == type) {
|
||||
indexes.append(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
const QString &Footage::decoder() const
|
||||
@@ -164,17 +413,17 @@ void Footage::set_decoder(const QString &id)
|
||||
decoder_ = id;
|
||||
}
|
||||
|
||||
QIcon Footage::icon()
|
||||
QIcon Footage::icon() const
|
||||
{
|
||||
if (valid_ && !streams_.isEmpty()) {
|
||||
if (valid_ && !inputs_for_stream_properties_.isEmpty()) {
|
||||
// Prioritize video > audio > image
|
||||
Stream* s = get_first_enabled_stream_of_type(Stream::kVideo);
|
||||
Stream s = GetFirstEnabledStreamOfType(Stream::kVideo);
|
||||
|
||||
if (s && static_cast<VideoStream*>(s)->video_type() != VideoStream::kVideoTypeStill) {
|
||||
if (s.IsValid() && s.video_type() != Stream::kVideoTypeStill) {
|
||||
return icon::Video;
|
||||
} else if (HasEnabledStreamsOfType(Stream::kAudio)) {
|
||||
return icon::Audio;
|
||||
} else if (s && static_cast<VideoStream*>(s)->video_type() == VideoStream::kVideoTypeStill) {
|
||||
} else if (s.IsValid() && s.video_type() == Stream::kVideoTypeStill) {
|
||||
return icon::Image;
|
||||
}
|
||||
}
|
||||
@@ -185,32 +434,31 @@ QIcon Footage::icon()
|
||||
QString Footage::duration()
|
||||
{
|
||||
// Find longest stream duration
|
||||
Stream* longest_stream = nullptr;
|
||||
Stream longest_stream;
|
||||
rational longest;
|
||||
|
||||
foreach (Stream* stream, streams_) {
|
||||
if (stream->enabled() && (stream->type() == Stream::kVideo || stream->type() == Stream::kAudio)) {
|
||||
rational this_stream_dur = Timecode::timestamp_to_time(stream->duration(),
|
||||
stream->timebase());
|
||||
for (auto it=inputs_for_stream_properties_.cbegin(); it!=inputs_for_stream_properties_.cend(); it++) {
|
||||
Stream s = GetStandardValue(it.value()).value<Stream>();
|
||||
|
||||
if (s.enabled() && (s.type() == Stream::kVideo || s.type() == Stream::kAudio)) {
|
||||
rational this_stream_dur = Timecode::timestamp_to_time(s.duration(), s.timebase());
|
||||
|
||||
if (this_stream_dur > longest) {
|
||||
longest_stream = stream;
|
||||
longest_stream = s;
|
||||
longest = this_stream_dur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (longest_stream) {
|
||||
if (longest_stream->type() == Stream::kVideo) {
|
||||
VideoStream* video_stream = static_cast<VideoStream*>(longest_stream);
|
||||
if (longest_stream.IsValid()) {
|
||||
if (longest_stream.type() == Stream::kVideo) {
|
||||
if (longest_stream.video_type() != Stream::kVideoTypeStill) {
|
||||
int64_t duration = longest_stream.duration();
|
||||
rational frame_rate_timebase = longest_stream.frame_rate().flipped();
|
||||
|
||||
if (video_stream->video_type() != VideoStream::kVideoTypeStill) {
|
||||
int64_t duration = video_stream->duration();
|
||||
rational frame_rate_timebase = video_stream->frame_rate().flipped();
|
||||
|
||||
if (video_stream->timebase() != frame_rate_timebase) {
|
||||
if (longest_stream.timebase() != frame_rate_timebase) {
|
||||
// Convert from timebase to frame rate
|
||||
rational duration_time = Timecode::timestamp_to_time(duration, video_stream->timebase());
|
||||
rational duration_time = Timecode::timestamp_to_time(duration, longest_stream.timebase());
|
||||
duration = Timecode::time_to_timestamp(duration_time, frame_rate_timebase);
|
||||
}
|
||||
|
||||
@@ -218,7 +466,7 @@ QString Footage::duration()
|
||||
frame_rate_timebase,
|
||||
Core::instance()->GetTimecodeDisplay());
|
||||
}
|
||||
} else if (longest_stream->type() == Stream::kAudio) {
|
||||
} else if (longest_stream.type() == Stream::kAudio) {
|
||||
// If we're showing in a timecode, we prefer showing audio in seconds instead
|
||||
Timecode::Display display = Core::instance()->GetTimecodeDisplay();
|
||||
if (display == Timecode::kTimecodeDropFrame
|
||||
@@ -226,8 +474,8 @@ QString Footage::duration()
|
||||
display = Timecode::kTimecodeSeconds;
|
||||
}
|
||||
|
||||
return Timecode::timestamp_to_timecode(longest_stream->duration(),
|
||||
longest_stream->timebase(),
|
||||
return Timecode::timestamp_to_timecode(longest_stream.duration(),
|
||||
longest_stream.timebase(),
|
||||
display);
|
||||
}
|
||||
}
|
||||
@@ -237,21 +485,21 @@ QString Footage::duration()
|
||||
|
||||
QString Footage::rate()
|
||||
{
|
||||
if (streams_.isEmpty()) {
|
||||
if (inputs_for_stream_properties_.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (HasEnabledStreamsOfType(Stream::kVideo)) {
|
||||
// This is a video editor, prioritize video streams
|
||||
VideoStream* video_stream = static_cast<VideoStream*>(get_first_enabled_stream_of_type(Stream::kVideo));
|
||||
Stream video_stream = GetFirstEnabledStreamOfType(Stream::kVideo);
|
||||
|
||||
if (video_stream->video_type() != VideoStream::kVideoTypeStill) {
|
||||
return QCoreApplication::translate("Footage", "%1 FPS").arg(video_stream->frame_rate().toDouble());
|
||||
if (video_stream.video_type() != Stream::kVideoTypeStill) {
|
||||
return QCoreApplication::translate("Footage", "%1 FPS").arg(video_stream.frame_rate().toDouble());
|
||||
}
|
||||
} else if (HasEnabledStreamsOfType(Stream::kAudio)) {
|
||||
// No video streams, return audio
|
||||
AudioStream* audio_stream = static_cast<AudioStream*>(streams_.first());
|
||||
return QCoreApplication::translate("Footage", "%1 Hz").arg(audio_stream->sample_rate());
|
||||
Stream audio_stream = GetStreamAt(0);
|
||||
return QCoreApplication::translate("Footage", "%1 Hz").arg(audio_stream.sample_rate());
|
||||
}
|
||||
|
||||
return QString();
|
||||
@@ -260,30 +508,23 @@ QString Footage::rate()
|
||||
quint64 Footage::get_enabled_stream_flags() const
|
||||
{
|
||||
quint64 enabled_streams = 0;
|
||||
quint64 stream_enabler = 1;
|
||||
|
||||
foreach (Stream* s, streams_) {
|
||||
if (s->enabled()) {
|
||||
enabled_streams |= stream_enabler;
|
||||
for (int i=0; i<stream_count_; i++) {
|
||||
if (IsStreamEnabled(i)) {
|
||||
enabled_streams |= (1 << i);
|
||||
}
|
||||
|
||||
stream_enabler <<= 1;
|
||||
}
|
||||
|
||||
return enabled_streams;
|
||||
}
|
||||
|
||||
void Footage::ClearStreams()
|
||||
{
|
||||
// Delete all streams
|
||||
streams_.clear();
|
||||
}
|
||||
|
||||
bool Footage::HasEnabledStreamsOfType(const Stream::Type &type) const
|
||||
{
|
||||
// Return true if any streams are video streams
|
||||
foreach (Stream* stream, streams_) {
|
||||
if (stream->enabled() && stream->type() == type) {
|
||||
for (int i=0; i<stream_count_; i++) {
|
||||
Stream s = GetStreamAt(i);
|
||||
|
||||
if (s.enabled() && s.type() == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -291,37 +532,29 @@ bool Footage::HasEnabledStreamsOfType(const Stream::Type &type) const
|
||||
return false;
|
||||
}
|
||||
|
||||
Stream *Footage::get_first_enabled_stream_of_type(const Stream::Type &type) const
|
||||
{
|
||||
foreach (Stream* stream, streams_) {
|
||||
if (stream->enabled() && stream->type() == type) {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Footage::CompareFootageToFile(Footage *footage, const QString &filename)
|
||||
{
|
||||
// Heuristic to determine if file has changed
|
||||
QFileInfo info(filename);
|
||||
|
||||
if (info.exists()) {
|
||||
if (info.lastModified().toMSecsSinceEpoch() == footage->timestamp()) {
|
||||
/*if (info.lastModified().toMSecsSinceEpoch() == footage->timestamp()) {
|
||||
// Footage has not been modified and is where we expect
|
||||
return true;
|
||||
} else {
|
||||
// Footage may have changed and we'll have to re-probe it. It also may not have, in which
|
||||
// case nothing needs to change.
|
||||
std::unique_ptr<Footage> item(Decoder::Probe(footage->project(), filename, nullptr));
|
||||
DecoderPtr decoder = Decoder::CreateFromID(footage->decoder());
|
||||
|
||||
if (item) {
|
||||
// Item is the same type, that's a good sign. Let's look for any differences.
|
||||
// FIXME: Implement this
|
||||
Streams probed_streams = decoder->Probe(filename, nullptr);
|
||||
|
||||
if (probed_streams == footage->streams_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// Simplified, since our footage node is much more tolerant, we'll try this
|
||||
return true;
|
||||
}
|
||||
|
||||
// Footage file couldn't be found or resolved to something we didn't expect
|
||||
@@ -333,16 +566,106 @@ bool Footage::CompareFootageToItsFilename(Footage *footage)
|
||||
return CompareFootageToFile(footage, footage->filename());
|
||||
}
|
||||
|
||||
void Footage::Hash(const QString& output, QCryptographicHash &hash, const rational &time) const
|
||||
{
|
||||
super::Hash(output, hash, time);
|
||||
|
||||
// Translate output ID to stream
|
||||
StreamReference ref = GetReferenceFromOutput(output);
|
||||
|
||||
QString fn = filename();
|
||||
|
||||
if (!fn.isEmpty()) {
|
||||
Stream stream = GetStreamAt(GetReferenceFromOutput(output));
|
||||
|
||||
if (stream.IsValid()) {
|
||||
// Add footage details to hash
|
||||
|
||||
// Footage filename
|
||||
hash.addData(filename().toUtf8());
|
||||
|
||||
// Footage last modified date
|
||||
hash.addData(QString::number(timestamp()).toUtf8());
|
||||
|
||||
// Footage stream
|
||||
hash.addData(QString::number(ref.index()).toUtf8());
|
||||
|
||||
if (ref.type() == Stream::kVideo) {
|
||||
// Current color config and space
|
||||
hash.addData(project()->color_manager()->GetConfigFilename().toUtf8());
|
||||
hash.addData(stream.colorspace().toUtf8());
|
||||
|
||||
// Alpha associated setting
|
||||
hash.addData(QString::number(stream.premultiplied_alpha()).toUtf8());
|
||||
|
||||
// Pixel aspect ratio
|
||||
hash.addData(reinterpret_cast<const char*>(&stream.pixel_aspect_ratio()), sizeof(stream.pixel_aspect_ratio()));
|
||||
|
||||
// Footage timestamp
|
||||
if (stream.video_type() != Stream::kVideoTypeStill) {
|
||||
int64_t video_ts = Timecode::time_to_timestamp(time, stream.timebase());
|
||||
|
||||
// Add timestamp in units of the video stream's timebase
|
||||
hash.addData(reinterpret_cast<const char*>(&video_ts), sizeof(int64_t));
|
||||
|
||||
// Add start time - used for both image sequences and video streams
|
||||
hash.addData(QString::number(stream.start_time()).toUtf8());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeValueTable Footage::Value(const QString &output, NodeValueDatabase &value) const
|
||||
{
|
||||
StreamReference ref = GetReferenceFromOutput(output);
|
||||
|
||||
// Pop filename from table
|
||||
QString file = value[kFilenameInput].Take(NodeValue::kFile).toString();
|
||||
|
||||
// Merge table
|
||||
NodeValueTable table = value.Merge();
|
||||
|
||||
// If the file exists and the reference is valid, push a footage job to the renderer
|
||||
if (QFileInfo(file).exists() && ref.IsValid()) {
|
||||
table.Push(NodeValue::kFootageJob, QVariant::fromValue(ref), this);
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
QString Footage::GetStreamTypeName(Stream::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Stream::kVideo:
|
||||
return tr("Video");
|
||||
case Stream::kAudio:
|
||||
return tr("Audio");
|
||||
case Stream::kSubtitle:
|
||||
return tr("Subtitle");
|
||||
case Stream::kData:
|
||||
return tr("Data");
|
||||
case Stream::kAttachment:
|
||||
return tr("Attachment");
|
||||
case Stream::kUnknown:
|
||||
break;
|
||||
}
|
||||
|
||||
return tr("Unknown");
|
||||
}
|
||||
|
||||
void Footage::UpdateTooltip()
|
||||
{
|
||||
if (valid_) {
|
||||
QString tip = QCoreApplication::translate("Footage", "Filename: %1").arg(filename());
|
||||
|
||||
if (!streams_.isEmpty()) {
|
||||
foreach (Stream* s, streams_) {
|
||||
if (s->enabled()) {
|
||||
if (!inputs_for_stream_properties_.isEmpty()) {
|
||||
for (int i=0; i<stream_count_; i++) {
|
||||
Stream s = GetStreamAt(i);
|
||||
|
||||
if (s.enabled()) {
|
||||
tip.append("\n");
|
||||
tip.append(s->description());
|
||||
tip.append(DescribeStream(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,4 +676,116 @@ void Footage::UpdateTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
Footage::MetadataCache Footage::LoadStreamCache(const QString &filename)
|
||||
{
|
||||
MetadataCache cache;
|
||||
QFile file(filename);
|
||||
|
||||
if (file.open(QFile::ReadOnly)) {
|
||||
QXmlStreamReader reader(&file);
|
||||
|
||||
while (XMLReadNextStartElement(&reader)) {
|
||||
if (reader.name() == QStringLiteral("streamcache")) {
|
||||
while (XMLReadNextStartElement(&reader)) {
|
||||
if (reader.name() == QStringLiteral("decoder")) {
|
||||
cache.decoder = reader.readElementText();
|
||||
} else if (reader.name() == QStringLiteral("streams")) {
|
||||
while (XMLReadNextStartElement(&reader)) {
|
||||
if (reader.name() == QStringLiteral("stream")) {
|
||||
Stream s;
|
||||
s.Load(&reader);
|
||||
cache.streams.append(s);
|
||||
} else {
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
bool Footage::SaveStreamCache(const QString &filename, const Footage::MetadataCache &data)
|
||||
{
|
||||
QFile file(filename);
|
||||
|
||||
if (!file.open(QFile::WriteOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QXmlStreamWriter writer(&file);
|
||||
|
||||
writer.writeStartDocument();
|
||||
|
||||
writer.writeStartElement(QStringLiteral("streamcache"));
|
||||
|
||||
writer.writeTextElement(QStringLiteral("decoder"), data.decoder);
|
||||
|
||||
writer.writeStartElement(QStringLiteral("streams"));
|
||||
|
||||
foreach (const Stream& s, data.streams) {
|
||||
writer.writeStartElement(QStringLiteral("stream"));
|
||||
s.Save(&writer);
|
||||
writer.writeEndElement(); // stream
|
||||
}
|
||||
|
||||
writer.writeEndElement(); // streams
|
||||
|
||||
writer.writeEndElement(); // streamcache
|
||||
|
||||
writer.writeEndDocument();
|
||||
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Footage::CheckFootage()
|
||||
{
|
||||
QString fn = filename();
|
||||
|
||||
if (!fn.isEmpty()) {
|
||||
QFileInfo info(fn);
|
||||
|
||||
qint64 current_file_timestamp = info.lastModified().toMSecsSinceEpoch();
|
||||
|
||||
if (current_file_timestamp != timestamp()) {
|
||||
// File has changed!
|
||||
set_timestamp(current_file_timestamp);
|
||||
InvalidateAll(kFilenameInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString Footage::StreamReference::video_colorspace(bool default_if_empty) const
|
||||
{
|
||||
if (IsValid()) {
|
||||
Stream stream = footage_->GetStreamAt(type_, index_);
|
||||
|
||||
if (stream.IsValid()) {
|
||||
if (stream.colorspace().isEmpty() && default_if_empty) {
|
||||
return footage_->project()->color_manager()->GetDefaultInputColorSpace();
|
||||
} else {
|
||||
return stream.colorspace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
uint qHash(const Footage::StreamReference &ref, uint seed)
|
||||
{
|
||||
return qHash(ref.footage(), seed) ^ qHash(ref.type(), seed) ^ qHash(ref.index(), seed);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
#include <QDateTime>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "node/node.h"
|
||||
#include "project/item/item.h"
|
||||
#include "project/item/footage/audiostream.h"
|
||||
#include "project/item/footage/videostream.h"
|
||||
#include "stream.h"
|
||||
#include "timeline/timelinepoints.h"
|
||||
|
||||
namespace olive {
|
||||
@@ -46,24 +46,34 @@ public:
|
||||
/**
|
||||
* @brief Footage Constructor
|
||||
*/
|
||||
Footage();
|
||||
Footage(const QString& filename = QString());
|
||||
|
||||
/**
|
||||
* @brief Footage Destructor
|
||||
*
|
||||
* Makes sure Stream objects are cleared properly
|
||||
*/
|
||||
virtual ~Footage() override;
|
||||
virtual Node* copy() const override
|
||||
{
|
||||
return new Footage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Load function
|
||||
*/
|
||||
virtual void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt *cancelled) override;
|
||||
virtual QString Name() const override
|
||||
{
|
||||
return tr("Footage");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Save function
|
||||
*/
|
||||
virtual void Save(QXmlStreamWriter *writer) const override;
|
||||
virtual QString id() const override
|
||||
{
|
||||
return QStringLiteral("org.olivevideoeditor.Olive.footage");
|
||||
}
|
||||
|
||||
virtual QVector<CategoryID> Category() const override
|
||||
{
|
||||
return {kCategoryProject};
|
||||
}
|
||||
|
||||
virtual QString Description() const override
|
||||
{
|
||||
return tr("Import video, audio, or still image files into the composition.");
|
||||
}
|
||||
|
||||
virtual void Retranslate() override;
|
||||
|
||||
/**
|
||||
* @brief Reset Footage state ready for running through Probe() again
|
||||
@@ -90,7 +100,7 @@ public:
|
||||
/**
|
||||
* @brief Return the current filename of this Footage object
|
||||
*/
|
||||
const QString& filename() const;
|
||||
QString filename() const;
|
||||
|
||||
/**
|
||||
* @brief Set the filename
|
||||
@@ -123,54 +133,189 @@ public:
|
||||
*/
|
||||
void set_timestamp(const qint64 &t);
|
||||
|
||||
/**
|
||||
* @brief Add a stream metadata object to this footage
|
||||
*
|
||||
* Usually done during a Decoder::Probe() function for retrieving metadata about the video/audio/other streams
|
||||
* inside a container. Streams can have non-video/audio types so that they can be equivalent to the file's actual
|
||||
* stream list, though the only streams officially supported are video and audio streams.
|
||||
*
|
||||
* @param s
|
||||
*
|
||||
* A pointer to a stream object. The Footage takes ownership of this object and will free it when it's deleted.
|
||||
*/
|
||||
void add_stream(Stream *s);
|
||||
|
||||
void add_streams(const QVector<Stream*>& streams);
|
||||
|
||||
/**
|
||||
* @brief Retrieve a stream at the given index.
|
||||
*
|
||||
* @param index
|
||||
*
|
||||
* The index will be equivalent to the stream's index in the file (or in FFmpeg
|
||||
* terms AVStream->file_index). Must be < stream_count().
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* The stream at the index provided
|
||||
*/
|
||||
Stream *stream(int index) const;
|
||||
|
||||
/**
|
||||
* @brief Returns a list of the streams in this Footage
|
||||
*/
|
||||
const QVector<Stream*>& streams() const
|
||||
void SetCancelPointer(const QAtomicInt* c)
|
||||
{
|
||||
return streams_;
|
||||
cancelled_ = c;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrieve total number of streams in this Footage file
|
||||
*/
|
||||
int stream_count() const;
|
||||
class StreamReference
|
||||
{
|
||||
public:
|
||||
StreamReference()
|
||||
{
|
||||
footage_ = nullptr;
|
||||
type_ = Stream::kUnknown;
|
||||
index_ = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Item::Type() override
|
||||
*
|
||||
* @return kFootage
|
||||
*/
|
||||
virtual Type type() const override;
|
||||
StreamReference(const Footage* footage, Stream::Type type, int index)
|
||||
{
|
||||
footage_ = footage;
|
||||
type_ = type;
|
||||
index_ = index;
|
||||
}
|
||||
|
||||
bool operator==(const StreamReference& rhs) const
|
||||
{
|
||||
return footage_ == rhs.footage_ && type_ == rhs.type_ && index_ == rhs.index_;
|
||||
}
|
||||
|
||||
bool IsValid() const
|
||||
{
|
||||
return footage_ && index_ >= 0;
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
*this = StreamReference();
|
||||
}
|
||||
|
||||
const Footage* footage() const
|
||||
{
|
||||
return footage_;
|
||||
}
|
||||
|
||||
Stream::Type type() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
int index() const
|
||||
{
|
||||
return index_;
|
||||
}
|
||||
|
||||
Stream GetStream() const
|
||||
{
|
||||
if (IsValid()) {
|
||||
return footage_->GetStreamAt(*this);
|
||||
} else {
|
||||
return Stream();
|
||||
}
|
||||
}
|
||||
|
||||
int64_t GetTimeInTimebaseUnits(const rational& timecode) const
|
||||
{
|
||||
if (IsValid()) {
|
||||
return footage_->GetTimeInTimebaseUnits(type_, index_, timecode);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int GetRealStreamIndex() const
|
||||
{
|
||||
if (IsValid()) {
|
||||
return footage_->GetRealStreamIndex(*this);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
QString filename() const
|
||||
{
|
||||
if (footage_) {
|
||||
return footage_->filename();
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
VideoParams video_params() const
|
||||
{
|
||||
return GetStream().video_params();
|
||||
}
|
||||
|
||||
AudioParams audio_params() const
|
||||
{
|
||||
return GetStream().audio_params();
|
||||
}
|
||||
|
||||
int64_t duration() const
|
||||
{
|
||||
return GetStream().duration();
|
||||
}
|
||||
|
||||
QString video_colorspace(bool default_if_empty = true) const;
|
||||
|
||||
private:
|
||||
const Footage* footage_;
|
||||
Stream::Type type_;
|
||||
int index_;
|
||||
|
||||
};
|
||||
|
||||
Stream GetStreamAt(int index) const
|
||||
{
|
||||
return GetStandardValue(GetInputIDOfIndex(index)).value<Stream>();
|
||||
}
|
||||
|
||||
Stream GetStreamAt(Stream::Type type, int index_within_type) const
|
||||
{
|
||||
return GetStreamAt(GetRealStreamIndex(type, index_within_type));
|
||||
}
|
||||
|
||||
Stream GetStreamAt(const StreamReference& ref) const
|
||||
{
|
||||
return GetStreamAt(ref.type(), ref.index());
|
||||
}
|
||||
|
||||
void SetStreamAt(int index, const Stream& stream)
|
||||
{
|
||||
SetStandardValue(GetInputIDOfIndex(index), QVariant::fromValue(stream));
|
||||
}
|
||||
|
||||
void SetStreamAt(Stream::Type type, int index_within_type, const Stream& stream)
|
||||
{
|
||||
SetStreamAt(GetRealStreamIndex(type, index_within_type), stream);
|
||||
}
|
||||
|
||||
void SetStreamAt(const StreamReference& ref, const Stream& stream)
|
||||
{
|
||||
SetStreamAt(ref.type(), ref.index(), stream);
|
||||
}
|
||||
|
||||
int64_t GetTimeInTimebaseUnits(int index, const rational& time) const;
|
||||
int64_t GetTimeInTimebaseUnits(Stream::Type type, int index_within_type, const rational& time) const
|
||||
{
|
||||
return GetTimeInTimebaseUnits(GetRealStreamIndex(type, index_within_type), time);
|
||||
}
|
||||
|
||||
int GetRealStreamIndex(Stream::Type type, int index_within_type) const;
|
||||
int GetRealStreamIndex(const StreamReference& ref) const
|
||||
{
|
||||
return GetRealStreamIndex(ref.type(), ref.index());
|
||||
}
|
||||
|
||||
static QString GetStringFromReference(Stream::Type type, int index);
|
||||
static QString GetStringFromReference(const StreamReference& ref)
|
||||
{
|
||||
return GetStringFromReference(ref.type(), ref.index());
|
||||
}
|
||||
|
||||
StreamReference GetReferenceFromRealIndex(int real_index) const;
|
||||
|
||||
Stream::Type GetTypeFromOutput(const QString& output) const;
|
||||
|
||||
StreamReference GetReferenceFromOutput(const QString& s) const;
|
||||
|
||||
int GetStreamCount() const
|
||||
{
|
||||
return stream_count_;
|
||||
}
|
||||
|
||||
int GetStreamTypeCount(Stream::Type type) const;
|
||||
|
||||
bool IsStreamEnabled(int index) const
|
||||
{
|
||||
return GetStreamAt(index).enabled();
|
||||
}
|
||||
|
||||
Stream GetFirstEnabledStreamOfType(Stream::Type type) const;
|
||||
|
||||
QVector<int> GetStreamIndexesOfType(Stream::Type type) const;
|
||||
|
||||
Stream::Type GetStreamType(int index);
|
||||
|
||||
/**
|
||||
* @brief Get the Decoder ID set when this Footage was probed
|
||||
@@ -186,7 +331,7 @@ public:
|
||||
*/
|
||||
void set_decoder(const QString& id);
|
||||
|
||||
virtual QIcon icon() override;
|
||||
virtual QIcon icon() const override;
|
||||
|
||||
virtual QString duration() override;
|
||||
|
||||
@@ -203,16 +348,38 @@ public:
|
||||
*/
|
||||
bool HasEnabledStreamsOfType(const Stream::Type& type) const;
|
||||
|
||||
Stream* get_first_enabled_stream_of_type(const Stream::Type& type) const;
|
||||
|
||||
static bool CompareFootageToFile(Footage* footage, const QString& filename);
|
||||
static bool CompareFootageToItsFilename(Footage* footage);
|
||||
|
||||
private:
|
||||
virtual void Hash(const QString& output, QCryptographicHash &hash, const rational &time) const override;
|
||||
|
||||
virtual NodeValueTable Value(const QString &output, NodeValueDatabase& value) const override;
|
||||
|
||||
static QString GetStreamTypeName(Stream::Type type);
|
||||
|
||||
static const QString kFilenameInput;
|
||||
static const QString kStreamPropertiesFormat;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Internal function to delete all Stream children and empty the array
|
||||
* @brief Load function
|
||||
*/
|
||||
void ClearStreams();
|
||||
virtual void LoadInternal(QXmlStreamReader* reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt *cancelled) override;
|
||||
|
||||
/**
|
||||
* @brief Save function
|
||||
*/
|
||||
virtual void SaveInternal(QXmlStreamWriter *writer) const override;
|
||||
|
||||
virtual void InputValueChangedEvent(const QString &input, int element) override;
|
||||
|
||||
private:
|
||||
QString DescribeStream(int index) const;
|
||||
|
||||
struct MetadataCache {
|
||||
QString decoder;
|
||||
Streams streams;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Update the icon based on the Footage status
|
||||
@@ -230,30 +397,50 @@ private:
|
||||
*/
|
||||
void UpdateTooltip();
|
||||
|
||||
MetadataCache LoadStreamCache(const QString& filename);
|
||||
|
||||
bool SaveStreamCache(const QString& filename, const MetadataCache& data);
|
||||
|
||||
static QString GetInputIDOfIndex(int index)
|
||||
{
|
||||
return kStreamPropertiesFormat.arg(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Internal filename string
|
||||
* @brief List of dynamic inputs added for stream properties
|
||||
*/
|
||||
QString filename_;
|
||||
QMap<int, QString> inputs_for_stream_properties_;
|
||||
|
||||
/**
|
||||
* @brief List of dynamic outputs added for streams
|
||||
*/
|
||||
QMap<int, QString> outputs_for_streams_;
|
||||
|
||||
/**
|
||||
* @brief Internal timestamp object
|
||||
*/
|
||||
qint64 timestamp_;
|
||||
|
||||
/**
|
||||
* @brief Internal streams array
|
||||
*/
|
||||
QVector<Stream*> streams_;
|
||||
|
||||
/**
|
||||
* @brief Internal attached decoder ID
|
||||
*/
|
||||
QString decoder_;
|
||||
|
||||
int stream_count_;
|
||||
|
||||
bool valid_;
|
||||
|
||||
const QAtomicInt* cancelled_;
|
||||
|
||||
private slots:
|
||||
void CheckFootage();
|
||||
|
||||
};
|
||||
|
||||
uint qHash(const Footage::StreamReference& ref, uint seed = 0);
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::Footage::StreamReference)
|
||||
|
||||
#endif // FOOTAGE_H
|
||||
|
||||
@@ -20,167 +20,112 @@
|
||||
|
||||
#include "stream.h"
|
||||
|
||||
#include "footage.h"
|
||||
#include "ui/icons/icons.h"
|
||||
#include "common/xmlutils.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
Stream::Stream() :
|
||||
type_(kUnknown),
|
||||
enabled_(true)
|
||||
void Stream::Load(QXmlStreamReader *reader)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
Stream::~Stream()
|
||||
{
|
||||
}
|
||||
|
||||
Stream *Stream::Load(QXmlStreamReader *reader, XMLNodeData &xml_node_data, const QAtomicInt* cancelled)
|
||||
{
|
||||
Stream* stream = nullptr;
|
||||
|
||||
XMLAttributeLoop(reader, attr) {
|
||||
if (attr.name() == QStringLiteral("type")) {
|
||||
Stream::Type type = static_cast<Stream::Type>(attr.value().toInt());
|
||||
switch (type) {
|
||||
case Stream::kVideo:
|
||||
stream = new VideoStream();
|
||||
break;
|
||||
case Stream::kAudio:
|
||||
stream = new AudioStream();
|
||||
break;
|
||||
default:
|
||||
stream = new Stream();
|
||||
stream->set_type(type);
|
||||
break;
|
||||
}
|
||||
|
||||
// This is the only attribute we need
|
||||
*this = Stream(static_cast<Type>(attr.value().toInt()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("ptr")) {
|
||||
//xml_node_data.footage_ptrs.insert(reader->readElementText().toULongLong(), stream);
|
||||
} else if (reader->name() == QStringLiteral("index")) {
|
||||
stream->set_index(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("timebase")) {
|
||||
stream->set_timebase(rational::fromString(reader->readElementText()));
|
||||
} else if (reader->name() == QStringLiteral("duration")) {
|
||||
stream->set_duration(reader->readElementText().toLongLong());
|
||||
} else if (reader->name() == QStringLiteral("enabled")) {
|
||||
stream->set_enabled(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("custom")) {
|
||||
stream->LoadCustomParameters(reader);
|
||||
if (reader->name() == QStringLiteral("global")) {
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("timebase")) {
|
||||
timebase_ = rational::fromString(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("duration")) {
|
||||
duration_ = reader->readElementText().toLongLong();
|
||||
} else if (reader->name() == QStringLiteral("channelcount")) {
|
||||
channel_count_ = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("enabled")) {
|
||||
enabled_ = reader->readElementText().toInt();
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
} else if (reader->name() == QStringLiteral("video")) {
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("width")) {
|
||||
width_ = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("height")) {
|
||||
height_ = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("pixelaspectratio")) {
|
||||
pixel_aspect_ratio_ = rational::fromString(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("videotype")) {
|
||||
video_type_ = static_cast<VideoType>(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("interlacing")) {
|
||||
interlacing_ = static_cast<VideoParams::Interlacing>(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("pixelformat")) {
|
||||
pixel_format_ = static_cast<VideoParams::Format>(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("framerate")) {
|
||||
frame_rate_ = rational::fromString(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("starttime")) {
|
||||
start_time_ = reader->readElementText().toLongLong();
|
||||
} else if (reader->name() == QStringLiteral("premultipliedalpha")) {
|
||||
premultiplied_alpha_ = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("colorspace")) {
|
||||
colorspace_ = reader->readElementText();
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
} else if (reader->name() == QStringLiteral("audio")) {
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("samplerate")) {
|
||||
sample_rate_ = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("channellayout")) {
|
||||
channel_layout_ = reader->readElementText().toULongLong();
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
reader->skipCurrentElement();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
void Stream::Save(QXmlStreamWriter *writer) const
|
||||
{
|
||||
writer->writeAttribute(QStringLiteral("type"), QString::number(type_));
|
||||
|
||||
writer->writeTextElement(QStringLiteral("ptr"), QString::number(reinterpret_cast<quintptr>(this)));
|
||||
writer->writeStartElement(QStringLiteral("global"));
|
||||
writer->writeTextElement(QStringLiteral("timebase"), timebase_.toString());
|
||||
writer->writeTextElement(QStringLiteral("duration"), QString::number(duration_));
|
||||
writer->writeTextElement(QStringLiteral("channelcount"), QString::number(channel_count_));
|
||||
writer->writeTextElement(QStringLiteral("enabled"), QString::number(enabled_));
|
||||
writer->writeEndElement(); // global
|
||||
|
||||
writer->writeTextElement(QStringLiteral("index"), QString::number(index_));
|
||||
writer->writeStartElement(QStringLiteral("video"));
|
||||
writer->writeTextElement(QStringLiteral("width"), QString::number(width_));
|
||||
writer->writeTextElement(QStringLiteral("height"), QString::number(height_));
|
||||
writer->writeTextElement(QStringLiteral("pixelaspectratio"), pixel_aspect_ratio_.toString());
|
||||
writer->writeTextElement(QStringLiteral("videotype"), QString::number(video_type_));
|
||||
writer->writeTextElement(QStringLiteral("interlacing"), QString::number(interlacing_));
|
||||
writer->writeTextElement(QStringLiteral("pixelformat"), QString::number(pixel_format_));
|
||||
writer->writeTextElement(QStringLiteral("framerate"), frame_rate_.toString());
|
||||
writer->writeTextElement(QStringLiteral("starttime"), QString::number(start_time_));
|
||||
writer->writeTextElement(QStringLiteral("premultipliedalpha"), QString::number(premultiplied_alpha_));
|
||||
writer->writeTextElement(QStringLiteral("colorspace"), colorspace_);
|
||||
writer->writeEndElement(); // video
|
||||
|
||||
writer->writeTextElement(QStringLiteral("timebase"), timebase_.toString());
|
||||
|
||||
writer->writeTextElement(QStringLiteral("duration"), QString::number(duration_));
|
||||
|
||||
writer->writeTextElement(QStringLiteral("enabled"), QString::number(enabled_));
|
||||
|
||||
writer->writeStartElement(QStringLiteral("custom"));
|
||||
|
||||
SaveCustomParameters(writer);
|
||||
|
||||
writer->writeEndElement();
|
||||
}
|
||||
|
||||
QString Stream::description() const
|
||||
{
|
||||
return QCoreApplication::translate("Stream", "%1: Unknown").arg(index());
|
||||
}
|
||||
|
||||
const Stream::Type &Stream::type() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
void Stream::set_type(const Stream::Type &type)
|
||||
{
|
||||
type_ = type;
|
||||
}
|
||||
|
||||
Footage *Stream::footage() const
|
||||
{
|
||||
return dynamic_cast<Footage*>(parent());
|
||||
}
|
||||
|
||||
const rational &Stream::timebase() const
|
||||
{
|
||||
return timebase_;
|
||||
}
|
||||
|
||||
void Stream::set_timebase(const rational &timebase)
|
||||
{
|
||||
timebase_ = timebase;
|
||||
}
|
||||
|
||||
const int &Stream::index() const
|
||||
{
|
||||
return index_;
|
||||
}
|
||||
|
||||
void Stream::set_index(const int &index)
|
||||
{
|
||||
index_ = index;
|
||||
}
|
||||
|
||||
const int64_t &Stream::duration() const
|
||||
{
|
||||
return duration_;
|
||||
}
|
||||
|
||||
void Stream::set_duration(const int64_t &duration)
|
||||
{
|
||||
duration_ = duration;
|
||||
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
bool Stream::enabled() const
|
||||
{
|
||||
return enabled_;
|
||||
}
|
||||
|
||||
void Stream::set_enabled(bool e)
|
||||
{
|
||||
enabled_ = e;
|
||||
}
|
||||
|
||||
QIcon Stream::icon() const
|
||||
{
|
||||
return QIcon();
|
||||
}
|
||||
|
||||
void Stream::LoadCustomParameters(QXmlStreamReader* reader)
|
||||
{
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
|
||||
void Stream::SaveCustomParameters(QXmlStreamWriter*) const
|
||||
{
|
||||
writer->writeStartElement(QStringLiteral("audio"));
|
||||
writer->writeTextElement(QStringLiteral("samplerate"), QString::number(sample_rate_));
|
||||
writer->writeTextElement(QStringLiteral("channellayout"), QString::number(channel_layout_));
|
||||
writer->writeEndElement(); // audio
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,35 +21,20 @@
|
||||
#ifndef STREAM_H
|
||||
#define STREAM_H
|
||||
|
||||
#include <memory>
|
||||
#include <QCoreApplication>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "ui/icons/icons.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class Footage;
|
||||
struct XMLNodeData;
|
||||
|
||||
/**
|
||||
* @brief A base class for keeping metadata about a media stream.
|
||||
*
|
||||
* A Stream can contain video data, audio data, subtitle data,
|
||||
* etc. and a Stream object stores metadata about it.
|
||||
*
|
||||
* The Stream class is fairly simple and is intended to be subclassed for data that pertains specifically to one
|
||||
* Stream::Type. \see VideoStream and \see AudioStream.
|
||||
*/
|
||||
class Stream : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
class Stream {
|
||||
public:
|
||||
enum Type {
|
||||
kUnknown,
|
||||
kUnknown = -1,
|
||||
kVideo,
|
||||
kAudio,
|
||||
kData,
|
||||
@@ -57,69 +42,283 @@ public:
|
||||
kAttachment
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stream constructor
|
||||
*/
|
||||
Stream();
|
||||
enum VideoType {
|
||||
kVideoTypeVideo,
|
||||
kVideoTypeStill,
|
||||
kVideoTypeImageSequence
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Required virtual destructor, serves no purpose
|
||||
*/
|
||||
virtual ~Stream() override;
|
||||
|
||||
static Stream* Load(QXmlStreamReader* reader, XMLNodeData& xml_node_data, const QAtomicInt* cancelled);
|
||||
|
||||
void Save(QXmlStreamWriter *writer) const;
|
||||
|
||||
virtual QString description() const;
|
||||
|
||||
const Type& type() const;
|
||||
void set_type(const Type& type);
|
||||
|
||||
Footage* footage() const;
|
||||
|
||||
const rational& timebase() const;
|
||||
void set_timebase(const rational& timebase);
|
||||
|
||||
const int& index() const;
|
||||
void set_index(const int& index);
|
||||
|
||||
const int64_t& duration() const;
|
||||
void set_duration(const int64_t& duration);
|
||||
|
||||
bool enabled() const;
|
||||
void set_enabled(bool e);
|
||||
|
||||
virtual QIcon icon() const;
|
||||
|
||||
QMutex* mutex()
|
||||
Stream(Type type = kUnknown) :
|
||||
type_(type)
|
||||
{
|
||||
return &mutex_;
|
||||
Init();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void LoadCustomParameters(QXmlStreamReader *reader);
|
||||
bool IsValid() const
|
||||
{
|
||||
return type_ != kUnknown;
|
||||
}
|
||||
|
||||
virtual void SaveCustomParameters(QXmlStreamWriter* writer) const;
|
||||
Type type() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
signals:
|
||||
void ParametersChanged();
|
||||
const rational& timebase() const
|
||||
{
|
||||
return timebase_;
|
||||
}
|
||||
|
||||
void set_timebase(const rational& timebase)
|
||||
{
|
||||
timebase_ = timebase;
|
||||
}
|
||||
|
||||
int64_t duration() const
|
||||
{
|
||||
return duration_;
|
||||
}
|
||||
|
||||
void set_duration(int64_t duration)
|
||||
{
|
||||
duration_ = duration;
|
||||
}
|
||||
|
||||
int channel_count() const
|
||||
{
|
||||
return channel_count_;
|
||||
}
|
||||
|
||||
void set_channel_count(int c)
|
||||
{
|
||||
channel_count_ = c;
|
||||
}
|
||||
|
||||
bool enabled() const
|
||||
{
|
||||
return enabled_;
|
||||
}
|
||||
|
||||
void set_enabled(bool e)
|
||||
{
|
||||
enabled_ = e;
|
||||
}
|
||||
|
||||
int width() const
|
||||
{
|
||||
return width_;
|
||||
}
|
||||
|
||||
void set_width(int w)
|
||||
{
|
||||
width_ = w;
|
||||
}
|
||||
|
||||
int height() const
|
||||
{
|
||||
return height_;
|
||||
}
|
||||
|
||||
void set_height(int h)
|
||||
{
|
||||
height_ = h;
|
||||
}
|
||||
|
||||
const rational& pixel_aspect_ratio() const
|
||||
{
|
||||
return pixel_aspect_ratio_;
|
||||
}
|
||||
|
||||
void set_pixel_aspect_ratio(const rational& pixel_aspect_ratio)
|
||||
{
|
||||
pixel_aspect_ratio_ = pixel_aspect_ratio;
|
||||
}
|
||||
|
||||
VideoType video_type() const
|
||||
{
|
||||
return video_type_;
|
||||
}
|
||||
|
||||
void set_video_type(VideoType t)
|
||||
{
|
||||
video_type_ = t;
|
||||
}
|
||||
|
||||
VideoParams::Interlacing interlacing() const
|
||||
{
|
||||
return interlacing_;
|
||||
}
|
||||
|
||||
void set_interlacing(VideoParams::Interlacing interlacing)
|
||||
{
|
||||
interlacing_ = interlacing;
|
||||
}
|
||||
|
||||
VideoParams::Format pixel_format() const
|
||||
{
|
||||
return pixel_format_;
|
||||
}
|
||||
|
||||
void set_pixel_format(VideoParams::Format pixel_format)
|
||||
{
|
||||
pixel_format_ = pixel_format;
|
||||
}
|
||||
|
||||
const rational& frame_rate() const
|
||||
{
|
||||
return frame_rate_;
|
||||
}
|
||||
|
||||
void set_frame_rate(const rational& frame_rate)
|
||||
{
|
||||
frame_rate_ = frame_rate;
|
||||
}
|
||||
|
||||
int64_t start_time() const
|
||||
{
|
||||
return start_time_;
|
||||
}
|
||||
|
||||
void set_start_time(int64_t start_time)
|
||||
{
|
||||
start_time_ = start_time;
|
||||
}
|
||||
|
||||
bool premultiplied_alpha() const
|
||||
{
|
||||
return premultiplied_alpha_;
|
||||
}
|
||||
|
||||
void set_premultiplied_alpha(bool premultiplied_alpha)
|
||||
{
|
||||
premultiplied_alpha_ = premultiplied_alpha;
|
||||
}
|
||||
|
||||
const QString& colorspace() const
|
||||
{
|
||||
return colorspace_;
|
||||
}
|
||||
|
||||
void set_colorspace(const QString& c)
|
||||
{
|
||||
colorspace_ = c;
|
||||
}
|
||||
|
||||
int sample_rate() const
|
||||
{
|
||||
return sample_rate_;
|
||||
}
|
||||
|
||||
void set_sample_rate(int sample_rate)
|
||||
{
|
||||
sample_rate_ = sample_rate;
|
||||
}
|
||||
|
||||
uint64_t channel_layout() const
|
||||
{
|
||||
return channel_layout_;
|
||||
}
|
||||
|
||||
void set_channel_layout(uint64_t channel_layout)
|
||||
{
|
||||
channel_layout_ = channel_layout;
|
||||
}
|
||||
|
||||
VideoParams video_params() const
|
||||
{
|
||||
if (type_ == kVideo) {
|
||||
return VideoParams(width_, height_, timebase_,
|
||||
pixel_format_, channel_count_, pixel_aspect_ratio_,
|
||||
interlacing_);
|
||||
} else {
|
||||
return VideoParams();
|
||||
}
|
||||
}
|
||||
|
||||
AudioParams audio_params() const
|
||||
{
|
||||
if (type_ == kAudio) {
|
||||
return AudioParams(sample_rate_, channel_layout_, AudioParams::kInternalFormat);
|
||||
} else {
|
||||
return AudioParams();
|
||||
}
|
||||
}
|
||||
|
||||
void Load(QXmlStreamReader* reader);
|
||||
|
||||
void Save(QXmlStreamWriter* writer) const;
|
||||
|
||||
QByteArray toBytes() const
|
||||
{
|
||||
QByteArray arr;
|
||||
|
||||
arr.append(reinterpret_cast<const char*>(&type_), sizeof(type_));
|
||||
arr.append(reinterpret_cast<const char*>(&timebase_), sizeof(timebase_));
|
||||
arr.append(reinterpret_cast<const char*>(&duration_), sizeof(duration_));
|
||||
arr.append(reinterpret_cast<const char*>(&channel_count_), sizeof(channel_count_));
|
||||
arr.append(reinterpret_cast<const char*>(&enabled_), sizeof(enabled_));
|
||||
arr.append(reinterpret_cast<const char*>(&width_), sizeof(width_));
|
||||
arr.append(reinterpret_cast<const char*>(&height_), sizeof(height_));
|
||||
arr.append(reinterpret_cast<const char*>(&pixel_aspect_ratio_), sizeof(pixel_aspect_ratio_));
|
||||
arr.append(reinterpret_cast<const char*>(&video_type_), sizeof(video_type_));
|
||||
arr.append(reinterpret_cast<const char*>(&interlacing_), sizeof(interlacing_));
|
||||
arr.append(reinterpret_cast<const char*>(&pixel_format_), sizeof(pixel_format_));
|
||||
arr.append(reinterpret_cast<const char*>(&frame_rate_), sizeof(frame_rate_));
|
||||
arr.append(reinterpret_cast<const char*>(&start_time_), sizeof(start_time_));
|
||||
arr.append(reinterpret_cast<const char*>(&premultiplied_alpha_), sizeof(premultiplied_alpha_));
|
||||
arr.append(colorspace_.toUtf8());
|
||||
arr.append(reinterpret_cast<const char*>(&sample_rate_), sizeof(sample_rate_));
|
||||
arr.append(reinterpret_cast<const char*>(&channel_layout_), sizeof(channel_layout_));
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
private:
|
||||
rational timebase_;
|
||||
|
||||
int64_t duration_;
|
||||
|
||||
int index_;
|
||||
void Init()
|
||||
{
|
||||
duration_ = AV_NOPTS_VALUE;
|
||||
channel_count_ = 0;
|
||||
enabled_ = true;
|
||||
width_ = 0;
|
||||
height_ = 0;
|
||||
video_type_ = VideoType::kVideoTypeVideo;
|
||||
interlacing_ = VideoParams::kInterlaceNone;
|
||||
pixel_format_ = VideoParams::kFormatInvalid;
|
||||
start_time_ = 0;
|
||||
premultiplied_alpha_ = false;
|
||||
sample_rate_ = 0;
|
||||
channel_layout_ = 0;
|
||||
}
|
||||
|
||||
// Global members
|
||||
Type type_;
|
||||
|
||||
rational timebase_;
|
||||
int64_t duration_;
|
||||
int channel_count_;
|
||||
bool enabled_;
|
||||
|
||||
QMutex mutex_;
|
||||
// Video members
|
||||
int width_;
|
||||
int height_;
|
||||
rational pixel_aspect_ratio_;
|
||||
VideoType video_type_;
|
||||
VideoParams::Interlacing interlacing_;
|
||||
VideoParams::Format pixel_format_;
|
||||
rational frame_rate_;
|
||||
int64_t start_time_;
|
||||
bool premultiplied_alpha_;
|
||||
QString colorspace_;
|
||||
|
||||
// Audio members
|
||||
int sample_rate_;
|
||||
uint64_t channel_layout_;
|
||||
|
||||
};
|
||||
|
||||
using Streams = QVector<Stream>;
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::Stream)
|
||||
|
||||
#endif // STREAM_H
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2020 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 "videostream.h"
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#include "common/timecodefunctions.h"
|
||||
#include "common/xmlutils.h"
|
||||
#include "footage.h"
|
||||
#include "project/project.h"
|
||||
#include "render/colormanager.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
VideoStream::VideoStream() :
|
||||
premultiplied_alpha_(false),
|
||||
interlacing_(VideoParams::kInterlaceNone),
|
||||
video_type_(VideoStream::kVideoTypeVideo),
|
||||
pixel_aspect_ratio_(1),
|
||||
start_time_(0)
|
||||
{
|
||||
set_type(Stream::kVideo);
|
||||
}
|
||||
|
||||
QString VideoStream::description() const
|
||||
{
|
||||
if (video_type_ == VideoStream::kVideoTypeStill) {
|
||||
return QCoreApplication::translate("Stream", "%1: Image - %2x%3").arg(QString::number(index()),
|
||||
QString::number(width()),
|
||||
QString::number(height()));
|
||||
} else {
|
||||
return QCoreApplication::translate("Stream", "%1: Video - %2x%3").arg(QString::number(index()),
|
||||
QString::number(width()),
|
||||
QString::number(height()));
|
||||
}
|
||||
}
|
||||
|
||||
const rational &VideoStream::frame_rate() const
|
||||
{
|
||||
return frame_rate_;
|
||||
}
|
||||
|
||||
void VideoStream::set_frame_rate(const rational &frame_rate)
|
||||
{
|
||||
frame_rate_ = frame_rate;
|
||||
}
|
||||
|
||||
const int64_t &VideoStream::start_time() const
|
||||
{
|
||||
return start_time_;
|
||||
}
|
||||
|
||||
void VideoStream::set_start_time(const int64_t &start_time)
|
||||
{
|
||||
start_time_ = start_time;
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
int64_t VideoStream::get_time_in_timebase_units(const rational &time) const
|
||||
{
|
||||
return Timecode::time_to_timestamp(time, timebase()) + start_time();
|
||||
}
|
||||
|
||||
QIcon VideoStream::icon() const
|
||||
{
|
||||
if (video_type_ == kVideoTypeStill) {
|
||||
return icon::Image;
|
||||
} else {
|
||||
return icon::Video;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoStream::LoadCustomParameters(QXmlStreamReader *reader)
|
||||
{
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("width")) {
|
||||
set_width(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("height")) {
|
||||
set_height(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("premultiplied")) {
|
||||
set_premultiplied_alpha(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("colorspace")) {
|
||||
set_colorspace(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("interlacing")) {
|
||||
set_interlacing(static_cast<VideoParams::Interlacing>(reader->readElementText().toInt()));
|
||||
} else if (reader->name() == QStringLiteral("type")) {
|
||||
set_video_type(static_cast<VideoType>(reader->readElementText().toInt()));
|
||||
} else if (reader->name() == QStringLiteral("format")) {
|
||||
set_format(static_cast<VideoParams::Format>(reader->readElementText().toInt()));
|
||||
} else if (reader->name() == QStringLiteral("channels")) {
|
||||
set_channel_count(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("pixelaspect")) {
|
||||
set_pixel_aspect_ratio(rational::fromString(reader->readElementText()));
|
||||
} else if (reader->name() == QStringLiteral("framerate")) {
|
||||
set_frame_rate(rational::fromString(reader->readElementText()));
|
||||
} else if (reader->name() == QStringLiteral("starttime")) {
|
||||
set_start_time(reader->readElementText().toLongLong());
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VideoStream::SaveCustomParameters(QXmlStreamWriter *writer) const
|
||||
{
|
||||
writer->writeTextElement(QStringLiteral("width"), QString::number(width_));
|
||||
writer->writeTextElement(QStringLiteral("height"), QString::number(height_));
|
||||
writer->writeTextElement(QStringLiteral("premultiplied"), QString::number(premultiplied_alpha_));
|
||||
writer->writeTextElement(QStringLiteral("colorspace"), colorspace_);
|
||||
writer->writeTextElement(QStringLiteral("interlacing"), QString::number(interlacing_));
|
||||
writer->writeTextElement(QStringLiteral("type"), QString::number(video_type_));
|
||||
writer->writeTextElement(QStringLiteral("format"), QString::number(format_));
|
||||
writer->writeTextElement(QStringLiteral("channels"), QString::number(channel_count_));
|
||||
writer->writeTextElement(QStringLiteral("pixelaspect"), pixel_aspect_ratio_.toString());
|
||||
writer->writeTextElement(QStringLiteral("framerate"), frame_rate_.toString());
|
||||
writer->writeTextElement(QStringLiteral("starttime"), QString::number(start_time_));
|
||||
}
|
||||
|
||||
bool VideoStream::premultiplied_alpha()
|
||||
{
|
||||
QMutexLocker locker(mutex());
|
||||
|
||||
return premultiplied_alpha_;
|
||||
}
|
||||
|
||||
void VideoStream::set_premultiplied_alpha(bool e)
|
||||
{
|
||||
mutex()->lock();
|
||||
premultiplied_alpha_ = e;
|
||||
mutex()->unlock();
|
||||
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
const QString &VideoStream::colorspace(bool default_if_empty)
|
||||
{
|
||||
QMutexLocker locker(mutex());
|
||||
|
||||
if (colorspace_.isEmpty() && default_if_empty) {
|
||||
return footage()->project()->color_manager()->GetDefaultInputColorSpace();
|
||||
} else {
|
||||
return colorspace_;
|
||||
}
|
||||
}
|
||||
|
||||
void VideoStream::set_colorspace(const QString &color)
|
||||
{
|
||||
mutex()->lock();
|
||||
colorspace_ = color;
|
||||
mutex()->unlock();
|
||||
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
void VideoStream::ColorConfigChanged()
|
||||
{
|
||||
ColorManager* color_manager = footage()->project()->color_manager();
|
||||
|
||||
// Check if this colorspace is in the new config
|
||||
if (!colorspace_.isEmpty()) {
|
||||
QStringList colorspaces = color_manager->ListAvailableColorspaces();
|
||||
if (!colorspaces.contains(colorspace_)) {
|
||||
// Set to empty if not
|
||||
colorspace_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Either way, the color calculation has likely changed so we signal here
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
void VideoStream::DefaultColorSpaceChanged()
|
||||
{
|
||||
// If no colorspace is set, this stream uses the default color space and it's just changed
|
||||
if (colorspace_.isEmpty()) {
|
||||
emit ParametersChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2020 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 VIDEOSTREAM_H
|
||||
#define VIDEOSTREAM_H
|
||||
|
||||
#include "render/videoparams.h"
|
||||
#include "stream.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
/**
|
||||
* @brief A Stream derivative containing video-specific information
|
||||
*/
|
||||
class VideoStream : public Stream
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
VideoStream();
|
||||
|
||||
enum VideoType {
|
||||
kVideoTypeVideo,
|
||||
kVideoTypeStill,
|
||||
kVideoTypeImageSequence
|
||||
};
|
||||
|
||||
virtual QString description() const override;
|
||||
|
||||
VideoType video_type() const
|
||||
{
|
||||
return video_type_;
|
||||
}
|
||||
|
||||
void set_video_type(VideoType t)
|
||||
{
|
||||
video_type_ = t;
|
||||
}
|
||||
|
||||
const int& width() const
|
||||
{
|
||||
return width_;
|
||||
}
|
||||
|
||||
void set_width(const int& width)
|
||||
{
|
||||
width_ = width;
|
||||
}
|
||||
|
||||
const int& height() const
|
||||
{
|
||||
return height_;
|
||||
}
|
||||
|
||||
void set_height(const int& height)
|
||||
{
|
||||
height_ = height;
|
||||
}
|
||||
|
||||
const VideoParams::Format& format() const
|
||||
{
|
||||
return format_;
|
||||
}
|
||||
|
||||
void set_format(const VideoParams::Format& format)
|
||||
{
|
||||
format_ = format;
|
||||
}
|
||||
|
||||
int channel_count() const
|
||||
{
|
||||
return channel_count_;
|
||||
}
|
||||
|
||||
void set_channel_count(int c)
|
||||
{
|
||||
channel_count_ = c;
|
||||
}
|
||||
|
||||
bool premultiplied_alpha();
|
||||
void set_premultiplied_alpha(bool e);
|
||||
|
||||
const QString& colorspace(bool default_if_empty = true);
|
||||
void set_colorspace(const QString& color);
|
||||
|
||||
VideoParams::Interlacing interlacing() const
|
||||
{
|
||||
return interlacing_;
|
||||
}
|
||||
|
||||
void set_interlacing(VideoParams::Interlacing i)
|
||||
{
|
||||
interlacing_ = i;
|
||||
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
const rational& pixel_aspect_ratio() const
|
||||
{
|
||||
return pixel_aspect_ratio_;
|
||||
}
|
||||
|
||||
void set_pixel_aspect_ratio(const rational& r)
|
||||
{
|
||||
// Auto-correct null aspect ratio to 1:1
|
||||
if (r.isNull()) {
|
||||
pixel_aspect_ratio_ = 1;
|
||||
} else {
|
||||
pixel_aspect_ratio_ = r;
|
||||
}
|
||||
|
||||
emit ParametersChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get this video stream's frame rate
|
||||
*
|
||||
* Used purely for metadata, rendering uses the timebase instead.
|
||||
*/
|
||||
const rational& frame_rate() const;
|
||||
void set_frame_rate(const rational& frame_rate);
|
||||
|
||||
const int64_t& start_time() const;
|
||||
void set_start_time(const int64_t& start_time);
|
||||
|
||||
int64_t get_time_in_timebase_units(const rational& time) const;
|
||||
|
||||
virtual QIcon icon() const override;
|
||||
|
||||
public slots:
|
||||
void ColorConfigChanged();
|
||||
|
||||
void DefaultColorSpaceChanged();
|
||||
|
||||
protected:
|
||||
virtual void LoadCustomParameters(QXmlStreamReader *reader) override;
|
||||
|
||||
virtual void SaveCustomParameters(QXmlStreamWriter* writer) const override;
|
||||
|
||||
private:
|
||||
int width_;
|
||||
int height_;
|
||||
bool premultiplied_alpha_;
|
||||
QString colorspace_;
|
||||
VideoParams::Interlacing interlacing_;
|
||||
|
||||
VideoType video_type_;
|
||||
|
||||
VideoParams::Format format_;
|
||||
|
||||
int channel_count_;
|
||||
|
||||
rational pixel_aspect_ratio_;
|
||||
|
||||
rational frame_rate_;
|
||||
|
||||
int64_t start_time_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // VIDEOSTREAM_H
|
||||
+13
-100
@@ -20,31 +20,21 @@
|
||||
|
||||
#include "item.h"
|
||||
|
||||
#include "folder/folder.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
#define super QObject
|
||||
|
||||
Item::Item() :
|
||||
item_parent_(nullptr),
|
||||
project_(nullptr)
|
||||
{
|
||||
}
|
||||
const QString Item::kParentInput = QStringLiteral("parent_in");
|
||||
|
||||
Item::~Item()
|
||||
Item::Item(bool create_folder_input, bool create_default_output) :
|
||||
Node(create_default_output)
|
||||
{
|
||||
setParent(nullptr);
|
||||
}
|
||||
|
||||
const QString &Item::name() const
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
void Item::set_name(const QString &n)
|
||||
{
|
||||
name_ = n;
|
||||
|
||||
NameChangedEvent(n);
|
||||
if (create_folder_input) {
|
||||
// Hierarchy input for items
|
||||
AddInput(kParentInput, NodeValue::kNone);
|
||||
}
|
||||
}
|
||||
|
||||
const QString &Item::tooltip() const
|
||||
@@ -67,91 +57,14 @@ QString Item::rate()
|
||||
return QString();
|
||||
}
|
||||
|
||||
Project *Item::project() const
|
||||
Folder *Item::item_parent() const
|
||||
{
|
||||
return project_;
|
||||
return dynamic_cast<Folder*>(GetConnectedNode(kParentInput));
|
||||
}
|
||||
|
||||
void Item::set_project(Project *project)
|
||||
void Item::Retranslate()
|
||||
{
|
||||
project_ = project;
|
||||
|
||||
foreach (Item* i, item_children_) {
|
||||
i->set_project(project_);
|
||||
}
|
||||
}
|
||||
|
||||
QVector<Item *> Item::get_children_of_type(Type type, bool recursive) const
|
||||
{
|
||||
QVector<Item *> list;
|
||||
|
||||
foreach (Item* item, item_children_) {
|
||||
if (item->type() == type) {
|
||||
list.append(item);
|
||||
}
|
||||
|
||||
if (recursive && item->CanHaveChildren()) {
|
||||
list.append(item->get_children_of_type(type, recursive));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
bool Item::CanHaveChildren() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Item::ChildExistsWithName(const QString &name)
|
||||
{
|
||||
return ChildExistsWithNameInternal(name, this);
|
||||
}
|
||||
|
||||
void Item::NameChangedEvent(const QString &)
|
||||
{
|
||||
}
|
||||
|
||||
void Item::childEvent(QChildEvent *event)
|
||||
{
|
||||
super::childEvent(event);
|
||||
|
||||
Item* cast_test = dynamic_cast<Item*>(event->child());
|
||||
|
||||
if (cast_test) {
|
||||
if (event->type() == QEvent::ChildAdded) {
|
||||
|
||||
item_children_.append(cast_test);
|
||||
cast_test->item_parent_ = this;
|
||||
cast_test->set_project(project_);
|
||||
|
||||
} else if (event->type() == QEvent::ChildRemoved) {
|
||||
|
||||
item_children_.removeOne(cast_test);
|
||||
cast_test->item_parent_ = nullptr;
|
||||
cast_test->set_project(nullptr);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Item::ChildExistsWithNameInternal(const QString &name, Item *folder)
|
||||
{
|
||||
// Loop through all children
|
||||
foreach (Item* child, folder->item_children_) {
|
||||
// If this child has the same name, return true
|
||||
if (child->name() == name) {
|
||||
return true;
|
||||
} else if (child->CanHaveChildren()) {
|
||||
// If the child has children, run function recursively on this item
|
||||
if (ChildExistsWithNameInternal(name, child)) {
|
||||
// If it returns true, we've found a child so we can return now
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
SetInputName(kParentInput, tr("Folder"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-67
@@ -30,10 +30,11 @@
|
||||
|
||||
#include "common/threadedobject.h"
|
||||
#include "common/xmlutils.h"
|
||||
#include "project/item/footage/stream.h"
|
||||
#include "node/node.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class Folder;
|
||||
class Project;
|
||||
|
||||
/**
|
||||
@@ -42,90 +43,29 @@ class Project;
|
||||
* Project objects implement a parent-child hierarchy of Items that can be used throughout the Project. The Item class
|
||||
* itself is abstract and will need to be subclassed to be used in a Project.
|
||||
*/
|
||||
class Item : public QObject
|
||||
class Item : public Node
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Type {
|
||||
kFolder,
|
||||
kFootage,
|
||||
kSequence
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Item constructor
|
||||
*/
|
||||
Item();
|
||||
|
||||
/**
|
||||
* @brief Required virtual Item destructor
|
||||
*/
|
||||
virtual ~Item() override;
|
||||
|
||||
virtual void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt *cancelled) = 0;
|
||||
|
||||
virtual void Save(QXmlStreamWriter* writer) const = 0;
|
||||
|
||||
virtual Type type() const = 0;
|
||||
|
||||
int item_child_count() const
|
||||
{
|
||||
return item_children_.size();
|
||||
}
|
||||
|
||||
Item* item_child(int i) const
|
||||
{
|
||||
return item_children_.at(i);
|
||||
}
|
||||
|
||||
const QVector<Item*>& children() const
|
||||
{
|
||||
return item_children_;
|
||||
}
|
||||
|
||||
const QString& name() const;
|
||||
void set_name(const QString& n);
|
||||
Item(bool create_folder_input = true, bool create_default_output = true);
|
||||
|
||||
const QString& tooltip() const;
|
||||
void set_tooltip(const QString& t);
|
||||
|
||||
virtual QIcon icon() = 0;
|
||||
|
||||
virtual QString duration();
|
||||
|
||||
virtual QString rate();
|
||||
|
||||
Item *item_parent() const
|
||||
{
|
||||
return item_parent_;
|
||||
}
|
||||
Folder *item_parent() const;
|
||||
|
||||
Project* project() const;
|
||||
static const QString kParentInput;
|
||||
|
||||
void set_project(Project* project);
|
||||
|
||||
QVector<Item*> get_children_of_type(Type type, bool recursive) const;
|
||||
|
||||
virtual bool CanHaveChildren() const;
|
||||
|
||||
bool ChildExistsWithName(const QString& name);
|
||||
|
||||
protected:
|
||||
virtual void NameChangedEvent(const QString& name);
|
||||
|
||||
virtual void childEvent(QChildEvent *event) override;
|
||||
virtual void Retranslate() override;
|
||||
|
||||
private:
|
||||
static bool ChildExistsWithNameInternal(const QString& name, Item* folder);
|
||||
|
||||
QVector<Item*> item_children_;
|
||||
|
||||
Item* item_parent_;
|
||||
|
||||
Project* project_;
|
||||
|
||||
QString name_;
|
||||
|
||||
QString tooltip_;
|
||||
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
#include "sequence.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
|
||||
#include "config/config.h"
|
||||
@@ -38,192 +37,67 @@
|
||||
|
||||
namespace olive {
|
||||
|
||||
Sequence::Sequence()
|
||||
{
|
||||
viewer_output_ = new ViewerOutput();
|
||||
viewer_output_->SetCanBeDeleted(false);
|
||||
viewer_output_->setParent(this);
|
||||
connect(viewer_output_, &ViewerOutput::LabelChanged, this, &Sequence::set_name);
|
||||
}
|
||||
const QString Sequence::kTextureInput = QStringLiteral("tex_in");
|
||||
const QString Sequence::kSamplesInput = QStringLiteral("samples_in");
|
||||
const QString Sequence::kTrackInputFormat = QStringLiteral("track_in_%1");
|
||||
|
||||
void Sequence::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, uint version, const QAtomicInt *cancelled)
|
||||
{
|
||||
{
|
||||
XMLAttributeLoop(reader, attr) {
|
||||
if (cancelled && *cancelled) {
|
||||
return;
|
||||
}
|
||||
#define super Item
|
||||
|
||||
if (attr.name() == QStringLiteral("name")) {
|
||||
set_name(attr.value().toString());
|
||||
} else if (attr.name() == QStringLiteral("ptr")) {
|
||||
xml_node_data.item_ptrs.insert(attr.value().toULongLong(), this);
|
||||
}
|
||||
Sequence::Sequence(bool viewer_only_mode) :
|
||||
Item(!viewer_only_mode, true),
|
||||
video_frame_cache_(this),
|
||||
audio_playback_cache_(this),
|
||||
operation_stack_(0)
|
||||
{
|
||||
AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable));
|
||||
|
||||
AddInput(kSamplesInput, NodeValue::kSamples, InputFlags(kInputFlagNotKeyframable));
|
||||
|
||||
if (!viewer_only_mode) {
|
||||
// Create TrackList instances
|
||||
track_lists_.resize(Track::kCount);
|
||||
|
||||
for (int i=0;i<Track::kCount;i++) {
|
||||
// Create track input
|
||||
QString track_input_id = kTrackInputFormat.arg(i);
|
||||
|
||||
AddInput(track_input_id, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable | kInputFlagArray));
|
||||
|
||||
IgnoreInvalidationsFrom(track_input_id);
|
||||
|
||||
TrackList* list = new TrackList(this, static_cast<Track::Type>(i), track_input_id);
|
||||
track_lists_.replace(i, list);
|
||||
connect(list, &TrackList::TrackListChanged, this, &Sequence::UpdateTrackCache);
|
||||
connect(list, &TrackList::LengthChanged, this, &Sequence::VerifyLength);
|
||||
connect(list, &TrackList::TrackAdded, this, &Sequence::TrackAdded);
|
||||
connect(list, &TrackList::TrackRemoved, this, &Sequence::TrackRemoved);
|
||||
}
|
||||
}
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (cancelled && *cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reader->name() == QStringLiteral("video")) {
|
||||
int video_width = 0, video_height = 0, preview_div = 1;
|
||||
rational video_timebase, video_pixel_aspect;
|
||||
VideoParams::Interlacing video_interlacing = VideoParams::kInterlaceNone;
|
||||
VideoParams::Format preview_format = VideoParams::kFormatInvalid;
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (cancelled && *cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reader->name() == QStringLiteral("width")) {
|
||||
video_width = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("height")) {
|
||||
video_height = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("timebase")) {
|
||||
video_timebase = rational::fromString(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("divider")) {
|
||||
preview_div = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("format")) {
|
||||
preview_format = static_cast<VideoParams::Format>(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("pixelaspect")) {
|
||||
video_pixel_aspect = rational::fromString(reader->readElementText());
|
||||
} else if (reader->name() == QStringLiteral("interlacing")) {
|
||||
video_interlacing = static_cast<VideoParams::Interlacing>(reader->readElementText().toInt());
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
set_video_params(VideoParams(video_width, video_height, video_timebase, preview_format,
|
||||
VideoParams::kInternalChannelCount, video_pixel_aspect,
|
||||
video_interlacing, preview_div));
|
||||
} else if (reader->name() == QStringLiteral("audio")) {
|
||||
int rate = 0;
|
||||
uint64_t layout = 0;
|
||||
AudioParams::Format format = AudioParams::kFormatInvalid;
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("rate")) {
|
||||
rate = reader->readElementText().toInt();
|
||||
} else if (reader->name() == QStringLiteral("layout")) {
|
||||
layout = reader->readElementText().toULongLong();
|
||||
} else if (reader->name() == QStringLiteral("format")) {
|
||||
format = static_cast<AudioParams::Format>(reader->readElementText().toInt());
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
set_audio_params(AudioParams(rate, layout, format));
|
||||
} else if (reader->name() == QStringLiteral("points")) {
|
||||
|
||||
TimelinePoints::Load(reader);
|
||||
|
||||
} else if (reader->name() == QStringLiteral("node") || reader->name() == QStringLiteral("viewer")) {
|
||||
Node* node;
|
||||
|
||||
if (reader->name() == QStringLiteral("node")) {
|
||||
node = nullptr;
|
||||
|
||||
{
|
||||
XMLAttributeLoop(reader, attr) {
|
||||
if (attr.name() == QStringLiteral("id")) {
|
||||
QString id = attr.value().toString();
|
||||
|
||||
node = NodeFactory::CreateFromID(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
node = viewer_output_;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
node->Load(reader, xml_node_data, cancelled);
|
||||
node->setParent(this);
|
||||
}
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
// Make connections
|
||||
XMLConnectNodes(xml_node_data);
|
||||
|
||||
// Link blocks
|
||||
XMLLinkBlocks(xml_node_data);
|
||||
// Create UUID for this node
|
||||
uuid_ = QUuid::createUuid();
|
||||
}
|
||||
|
||||
void Sequence::Save(QXmlStreamWriter *writer) const
|
||||
Sequence::~Sequence()
|
||||
{
|
||||
writer->writeAttribute(QStringLiteral("name"), name());
|
||||
|
||||
writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast<quintptr>(this)));
|
||||
|
||||
writer->writeStartElement(QStringLiteral("video"));
|
||||
|
||||
writer->writeTextElement(QStringLiteral("width"), QString::number(video_params().width()));
|
||||
writer->writeTextElement(QStringLiteral("height"), QString::number(video_params().height()));
|
||||
writer->writeTextElement(QStringLiteral("timebase"), video_params().time_base().toString());
|
||||
writer->writeTextElement(QStringLiteral("pixelaspect"), video_params().pixel_aspect_ratio().toString());
|
||||
writer->writeTextElement(QStringLiteral("interlacing"), QString::number(video_params().interlacing()));
|
||||
writer->writeTextElement(QStringLiteral("divider"), QString::number(video_params().divider()));
|
||||
writer->writeTextElement(QStringLiteral("format"), QString::number(video_params().format()));
|
||||
|
||||
writer->writeEndElement(); // video
|
||||
|
||||
writer->writeStartElement(QStringLiteral("audio"));
|
||||
|
||||
writer->writeTextElement(QStringLiteral("rate"), QString::number(audio_params().sample_rate()));
|
||||
writer->writeTextElement(QStringLiteral("layout"), QString::number(audio_params().channel_layout()));
|
||||
writer->writeTextElement(QStringLiteral("format"), QString::number(audio_params().format()));
|
||||
|
||||
writer->writeEndElement(); // audio
|
||||
|
||||
// Write TimelinePoints
|
||||
writer->writeStartElement(QStringLiteral("points"));
|
||||
TimelinePoints::Save(writer);
|
||||
writer->writeEndElement(); // points
|
||||
|
||||
foreach (Node* node, nodes()) {
|
||||
if (node != viewer_output_) {
|
||||
writer->writeStartElement(QStringLiteral("node"));
|
||||
writer->writeAttribute(QStringLiteral("id"), node->id());
|
||||
node->Save(writer);
|
||||
writer->writeEndElement(); // node;
|
||||
}
|
||||
}
|
||||
|
||||
writer->writeStartElement(QStringLiteral("viewer"));
|
||||
writer->writeAttribute(QStringLiteral("id"), viewer_output_->id());
|
||||
viewer_output_->Save(writer);
|
||||
writer->writeEndElement(); // viewer;
|
||||
DisconnectAll();
|
||||
}
|
||||
|
||||
void Sequence::add_default_nodes()
|
||||
void Sequence::add_default_nodes(MultiUndoCommand* command)
|
||||
{
|
||||
// Create tracks and connect them to the viewer
|
||||
TimelineAddTrackCommand(viewer_output_->track_list(Track::kVideo)).redo();
|
||||
TimelineAddTrackCommand(viewer_output_->track_list(Track::kAudio)).redo();
|
||||
command->add_child(new TimelineAddTrackCommand(track_list(Track::kVideo)));
|
||||
command->add_child(new TimelineAddTrackCommand(track_list(Track::kAudio)));
|
||||
}
|
||||
|
||||
Item::Type Sequence::type() const
|
||||
{
|
||||
return kSequence;
|
||||
}
|
||||
|
||||
QIcon Sequence::icon()
|
||||
QIcon Sequence::icon() const
|
||||
{
|
||||
return icon::Sequence;
|
||||
}
|
||||
|
||||
QString Sequence::duration()
|
||||
{
|
||||
rational timeline_length = viewer_output_->GetLength();
|
||||
rational timeline_length = GetLength();
|
||||
|
||||
int64_t timestamp = Timecode::time_to_timestamp(timeline_length, video_params().time_base());
|
||||
|
||||
@@ -232,27 +106,7 @@ QString Sequence::duration()
|
||||
|
||||
QString Sequence::rate()
|
||||
{
|
||||
return QCoreApplication::translate("Sequence", "%1 FPS").arg(video_params().time_base().flipped().toDouble());
|
||||
}
|
||||
|
||||
const VideoParams &Sequence::video_params() const
|
||||
{
|
||||
return viewer_output_->video_params();
|
||||
}
|
||||
|
||||
void Sequence::set_video_params(const VideoParams &vparam)
|
||||
{
|
||||
viewer_output_->set_video_params(vparam);
|
||||
}
|
||||
|
||||
const AudioParams &Sequence::audio_params() const
|
||||
{
|
||||
return viewer_output_->audio_params();
|
||||
}
|
||||
|
||||
void Sequence::set_audio_params(const AudioParams ¶ms)
|
||||
{
|
||||
viewer_output_->set_audio_params(params);
|
||||
return tr("%1 FPS").arg(video_params().time_base().flipped().toDouble());
|
||||
}
|
||||
|
||||
void Sequence::set_default_parameters()
|
||||
@@ -279,45 +133,48 @@ void Sequence::set_parameters_from_footage(const QVector<Footage *> footage)
|
||||
bool found_audio_params = false;
|
||||
|
||||
foreach (Footage* f, footage) {
|
||||
foreach (Stream* s, f->streams()) {
|
||||
if (!s->enabled()) {
|
||||
for (int i=0; i<f->GetStreamCount(); i++) {
|
||||
if (!f->IsStreamEnabled(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (s->type()) {
|
||||
Stream s = f->GetStreamAt(i);
|
||||
|
||||
if (!s.IsValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (s.type()) {
|
||||
case Stream::kVideo:
|
||||
{
|
||||
VideoStream* vs = static_cast<VideoStream*>(s);
|
||||
|
||||
// If this is a video stream, use these parameters
|
||||
if (!found_video_params) {
|
||||
rational using_timebase;
|
||||
|
||||
if (vs->video_type() == VideoStream::kVideoTypeStill) {
|
||||
if (s.video_type() == Stream::kVideoTypeStill) {
|
||||
// If this is a still image, we'll use it's resolution but won't set
|
||||
// `found_video_params` in case something with a frame rate comes along which we'll
|
||||
// prioritize
|
||||
using_timebase = video_params().time_base();
|
||||
} else {
|
||||
using_timebase = vs->frame_rate().flipped();
|
||||
using_timebase = s.frame_rate().flipped();
|
||||
found_video_params = true;
|
||||
}
|
||||
|
||||
set_video_params(VideoParams(vs->width(),
|
||||
vs->height(),
|
||||
set_video_params(VideoParams(s.width(),
|
||||
s.height(),
|
||||
using_timebase,
|
||||
static_cast<VideoParams::Format>(Config::Current()["OfflinePixelFormat"].toInt()),
|
||||
static_cast<VideoParams::Format>(Config::Current()[QStringLiteral("OfflinePixelFormat")].toInt()),
|
||||
VideoParams::kInternalChannelCount,
|
||||
vs->pixel_aspect_ratio(),
|
||||
vs->interlacing(),
|
||||
VideoParams::generate_auto_divider(vs->width(), vs->height())));
|
||||
s.pixel_aspect_ratio(),
|
||||
s.interlacing(),
|
||||
VideoParams::generate_auto_divider(s.width(), s.height())));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Stream::kAudio:
|
||||
if (!found_audio_params) {
|
||||
AudioStream* as = static_cast<AudioStream*>(s);
|
||||
set_audio_params(AudioParams(as->sample_rate(), as->channel_layout(), AudioParams::kInternalFormat));
|
||||
set_audio_params(AudioParams(s.sample_rate(), s.channel_layout(), AudioParams::kInternalFormat));
|
||||
found_audio_params = true;
|
||||
}
|
||||
break;
|
||||
@@ -336,14 +193,296 @@ void Sequence::set_parameters_from_footage(const QVector<Footage *> footage)
|
||||
}
|
||||
}
|
||||
|
||||
ViewerOutput *Sequence::viewer_output() const
|
||||
QVector<Track *> Sequence::GetUnlockedTracks() const
|
||||
{
|
||||
return viewer_output_;
|
||||
QVector<Track*> tracks = GetTracks();
|
||||
|
||||
for (int i=0;i<tracks.size();i++) {
|
||||
if (tracks.at(i)->IsLocked()) {
|
||||
tracks.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
void Sequence::NameChangedEvent(const QString &name)
|
||||
void Sequence::Retranslate()
|
||||
{
|
||||
viewer_output_->SetLabel(name);
|
||||
super::Retranslate();
|
||||
|
||||
SetInputName(kTextureInput, tr("Texture"));
|
||||
|
||||
SetInputName(kSamplesInput, tr("Samples"));
|
||||
|
||||
for (int i=0;i<Track::kCount;i++) {
|
||||
QString input_name;
|
||||
|
||||
switch (static_cast<Track::Type>(i)) {
|
||||
case Track::kVideo:
|
||||
input_name = tr("Video Tracks");
|
||||
break;
|
||||
case Track::kAudio:
|
||||
input_name = tr("Audio Tracks");
|
||||
break;
|
||||
case Track::kSubtitle:
|
||||
input_name = tr("Subtitle Tracks");
|
||||
break;
|
||||
case Track::kNone:
|
||||
case Track::kCount:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!input_name.isEmpty()) {
|
||||
SetInputName(kTrackInputFormat.arg(i), input_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rational Sequence::GetCustomLength(Track::Type type) const
|
||||
{
|
||||
switch (type) {
|
||||
case Track::kVideo:
|
||||
return track_lists_.at(Track::kVideo)->GetTotalLength();
|
||||
case Track::kAudio:
|
||||
return track_lists_.at(Track::kAudio)->GetTotalLength();
|
||||
case Track::kSubtitle:
|
||||
return track_lists_.at(Track::kSubtitle)->GetTotalLength();
|
||||
case Track::kNone:
|
||||
case Track::kCount:
|
||||
break;
|
||||
}
|
||||
|
||||
return rational();
|
||||
}
|
||||
|
||||
void Sequence::InputConnectedEvent(const QString &input, int element, const NodeOutput &output)
|
||||
{
|
||||
if (input == kTextureInput) {
|
||||
emit TextureInputChanged();
|
||||
} else {
|
||||
foreach (TrackList* list, track_lists_) {
|
||||
if (list->track_input() == input) {
|
||||
// Return because we found our input
|
||||
list->TrackConnected(output.node(), element);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super::InputConnectedEvent(input, element, output);
|
||||
}
|
||||
|
||||
void Sequence::InputDisconnectedEvent(const QString &input, int element, const NodeOutput &output)
|
||||
{
|
||||
if (input == kTextureInput) {
|
||||
emit TextureInputChanged();
|
||||
} else {
|
||||
foreach (TrackList* list, track_lists_) {
|
||||
if (list->track_input() == input) {
|
||||
// Return because we found our input
|
||||
list->TrackDisconnected(output.node(), element);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super::InputDisconnectedEvent(input, element, output);
|
||||
}
|
||||
|
||||
void Sequence::ShiftAudioEvent(const rational &from, const rational &to)
|
||||
{
|
||||
foreach (Track* track, track_lists_.at(Track::kAudio)->GetTracks()) {
|
||||
track->waveform().Shift(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
void Sequence::UpdateTrackCache()
|
||||
{
|
||||
track_cache_.clear();
|
||||
|
||||
foreach (TrackList* list, track_lists_) {
|
||||
foreach (Track* track, list->GetTracks()) {
|
||||
track_cache_.append(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Sequence::ShiftVideoCache(const rational &from, const rational &to)
|
||||
{
|
||||
video_frame_cache_.Shift(from, to);
|
||||
|
||||
ShiftVideoEvent(from, to);
|
||||
}
|
||||
|
||||
void Sequence::ShiftAudioCache(const rational &from, const rational &to)
|
||||
{
|
||||
audio_playback_cache_.Shift(from, to);
|
||||
|
||||
ShiftAudioEvent(from, to);
|
||||
}
|
||||
|
||||
void Sequence::ShiftCache(const rational &from, const rational &to)
|
||||
{
|
||||
ShiftVideoCache(from, to);
|
||||
ShiftAudioCache(from, to);
|
||||
}
|
||||
|
||||
void Sequence::InvalidateCache(const TimeRange& range, const QString& from, int element)
|
||||
{
|
||||
Q_UNUSED(element)
|
||||
|
||||
if (operation_stack_ == 0) {
|
||||
if (from == kTextureInput || from == kSamplesInput) {
|
||||
TimeRange invalidated_range(qMax(rational(), range.in()),
|
||||
qMin(GetLength(), range.out()));
|
||||
|
||||
if (invalidated_range.in() != invalidated_range.out()) {
|
||||
if (from == kTextureInput) {
|
||||
video_frame_cache_.Invalidate(invalidated_range);
|
||||
} else {
|
||||
audio_playback_cache_.Invalidate(invalidated_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerifyLength();
|
||||
}
|
||||
|
||||
super::InvalidateCache(range, from);
|
||||
}
|
||||
|
||||
void Sequence::set_video_params(const VideoParams &video)
|
||||
{
|
||||
bool size_changed = video_params_.width() != video.width() || video_params_.height() != video.height();
|
||||
bool timebase_changed = video_params_.time_base() != video.time_base();
|
||||
bool pixel_aspect_changed = video_params_.pixel_aspect_ratio() != video.pixel_aspect_ratio();
|
||||
bool interlacing_changed = video_params_.interlacing() != video.interlacing();
|
||||
|
||||
video_params_ = video;
|
||||
|
||||
if (size_changed) {
|
||||
emit SizeChanged(video_params_.width(), video_params_.height());
|
||||
}
|
||||
|
||||
if (pixel_aspect_changed) {
|
||||
emit PixelAspectChanged(video_params_.pixel_aspect_ratio());
|
||||
}
|
||||
|
||||
if (interlacing_changed) {
|
||||
emit InterlacingChanged(video_params_.interlacing());
|
||||
}
|
||||
|
||||
if (timebase_changed) {
|
||||
video_frame_cache_.SetTimebase(video_params_.time_base());
|
||||
emit TimebaseChanged(video_params_.time_base());
|
||||
}
|
||||
|
||||
emit VideoParamsChanged();
|
||||
|
||||
video_frame_cache_.InvalidateAll();
|
||||
}
|
||||
|
||||
void Sequence::set_audio_params(const AudioParams &audio)
|
||||
{
|
||||
audio_params_ = audio;
|
||||
|
||||
emit AudioParamsChanged();
|
||||
|
||||
// This will automatically InvalidateAll
|
||||
audio_playback_cache_.SetParameters(audio_params());
|
||||
}
|
||||
|
||||
rational Sequence::GetLength()
|
||||
{
|
||||
return last_length_;
|
||||
}
|
||||
|
||||
void Sequence::VerifyLength()
|
||||
{
|
||||
if (operation_stack_ != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NodeTraverser traverser;
|
||||
|
||||
rational video_length, audio_length, subtitle_length;
|
||||
|
||||
{
|
||||
video_length = GetCustomLength(Track::kVideo);
|
||||
|
||||
if (video_length.isNull() && IsInputConnected(kTextureInput)) {
|
||||
NodeValueTable t = traverser.GenerateTable(GetConnectedOutput(kTextureInput), TimeRange(0, 0));
|
||||
video_length = t.Get(NodeValue::kRational, QStringLiteral("length")).value<rational>();
|
||||
}
|
||||
|
||||
video_frame_cache_.SetLength(video_length);
|
||||
}
|
||||
|
||||
{
|
||||
audio_length = GetCustomLength(Track::kAudio);
|
||||
|
||||
if (audio_length.isNull() && IsInputConnected(kSamplesInput)) {
|
||||
NodeValueTable t = traverser.GenerateTable(GetConnectedOutput(kSamplesInput), TimeRange(0, 0));
|
||||
audio_length = t.Get(NodeValue::kRational, QStringLiteral("length")).value<rational>();
|
||||
}
|
||||
|
||||
audio_playback_cache_.SetLength(audio_length);
|
||||
}
|
||||
|
||||
{
|
||||
subtitle_length = GetCustomLength(Track::kSubtitle);
|
||||
}
|
||||
|
||||
rational real_length = qMax(subtitle_length, qMax(video_length, audio_length));
|
||||
|
||||
if (real_length != last_length_) {
|
||||
last_length_ = real_length;
|
||||
emit LengthChanged(last_length_);
|
||||
}
|
||||
}
|
||||
|
||||
void Sequence::BeginOperation()
|
||||
{
|
||||
operation_stack_++;
|
||||
|
||||
super::BeginOperation();
|
||||
}
|
||||
|
||||
void Sequence::EndOperation()
|
||||
{
|
||||
operation_stack_--;
|
||||
|
||||
super::EndOperation();
|
||||
}
|
||||
|
||||
void Sequence::LoadInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt *cancelled)
|
||||
{
|
||||
Q_UNUSED(xml_node_data)
|
||||
Q_UNUSED(version)
|
||||
Q_UNUSED(cancelled)
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("points")) {
|
||||
timeline_points_.Load(reader);
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Sequence::SaveInternal(QXmlStreamWriter *writer) const
|
||||
{
|
||||
// Write TimelinePoints
|
||||
writer->writeStartElement(QStringLiteral("points"));
|
||||
timeline_points_.Save(writer);
|
||||
writer->writeEndElement(); // points
|
||||
}
|
||||
|
||||
void Sequence::ShiftVideoEvent(const rational &from, const rational &to)
|
||||
{
|
||||
Q_UNUSED(from)
|
||||
Q_UNUSED(to)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,12 +21,24 @@
|
||||
#ifndef SEQUENCE_H
|
||||
#define SEQUENCE_H
|
||||
|
||||
#include <QUuid>
|
||||
|
||||
#include "common/rational.h"
|
||||
#include "node/block/block.h"
|
||||
#include "node/graph.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "project/item/footage/stream.h"
|
||||
#include "node/node.h"
|
||||
#include "node/output/track/track.h"
|
||||
#include "node/output/track/tracklist.h"
|
||||
#include "node/traverser.h"
|
||||
#include "project/item/footage/footage.h"
|
||||
#include "project/item/item.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/audioplaybackcache.h"
|
||||
#include "render/framehashcache.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "timeline/timelinecommon.h"
|
||||
#include "timeline/timelinepoints.h"
|
||||
#include "timeline/timelinepoints.h"
|
||||
|
||||
namespace olive {
|
||||
@@ -34,51 +46,180 @@ namespace olive {
|
||||
/**
|
||||
* @brief The main timeline object, an graph of edited clips that forms a complete edit
|
||||
*/
|
||||
class Sequence : public NodeGraph, public TimelinePoints
|
||||
class Sequence : public Item, public TimelinePoints
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Sequence();
|
||||
Sequence(bool viewer_only_mode = false);
|
||||
|
||||
/**
|
||||
* @brief Load function
|
||||
*/
|
||||
virtual void Load(QXmlStreamReader* reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt* cancelled) override;
|
||||
virtual ~Sequence() override;
|
||||
|
||||
/**
|
||||
* @brief Save function
|
||||
*/
|
||||
virtual void Save(QXmlStreamWriter *writer) const override;
|
||||
virtual Node* copy() const override
|
||||
{
|
||||
return new Sequence();
|
||||
}
|
||||
|
||||
void add_default_nodes();
|
||||
virtual QString Name() const override
|
||||
{
|
||||
return tr("Sequence");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Item::Type() override
|
||||
*/
|
||||
virtual Type type() const override;
|
||||
virtual QString id() const override
|
||||
{
|
||||
return QStringLiteral("org.olivevideoeditor.Olive.sequence");
|
||||
}
|
||||
|
||||
virtual QIcon icon() override;
|
||||
virtual QVector<CategoryID> Category() const override
|
||||
{
|
||||
return {kCategoryProject};
|
||||
}
|
||||
|
||||
virtual QString Description() const override
|
||||
{
|
||||
return tr("A series of cuts that result in an edited video. Also called a timeline.");
|
||||
}
|
||||
|
||||
void add_default_nodes(MultiUndoCommand *command);
|
||||
|
||||
virtual QIcon icon() const override;
|
||||
|
||||
virtual QString duration() override;
|
||||
virtual QString rate() override;
|
||||
|
||||
const VideoParams &video_params() const;
|
||||
void set_video_params(const VideoParams &vparam);
|
||||
|
||||
const AudioParams& audio_params() const;
|
||||
void set_audio_params(const AudioParams& params);
|
||||
|
||||
void set_default_parameters();
|
||||
|
||||
void set_parameters_from_footage(const QVector<Footage *> footage);
|
||||
|
||||
ViewerOutput* viewer_output() const;
|
||||
const QVector<Track *> &GetTracks() const
|
||||
{
|
||||
return track_cache_;
|
||||
}
|
||||
|
||||
Track* GetTrackFromReference(const Track::Reference& track_ref) const
|
||||
{
|
||||
return track_lists_.at(track_ref.type())->GetTrackAt(track_ref.index());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Same as GetTracks() but omits tracks that are locked.
|
||||
*/
|
||||
QVector<Track *> GetUnlockedTracks() const;
|
||||
|
||||
TrackList* track_list(Track::Type type) const
|
||||
{
|
||||
return track_lists_.at(type);
|
||||
}
|
||||
|
||||
void ShiftVideoCache(const rational& from, const rational& to);
|
||||
void ShiftAudioCache(const rational& from, const rational& to);
|
||||
void ShiftCache(const rational& from, const rational& to);
|
||||
|
||||
virtual void InvalidateCache(const TimeRange& range, const QString& from, int element = -1) override;
|
||||
|
||||
const VideoParams& video_params() const
|
||||
{
|
||||
return video_params_;
|
||||
}
|
||||
|
||||
const AudioParams& audio_params() const
|
||||
{
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
void set_video_params(const VideoParams &video);
|
||||
void set_audio_params(const AudioParams &audio);
|
||||
|
||||
rational GetLength();
|
||||
|
||||
virtual void Retranslate() override;
|
||||
|
||||
FrameHashCache* video_frame_cache()
|
||||
{
|
||||
return &video_frame_cache_;
|
||||
}
|
||||
|
||||
AudioPlaybackCache* audio_playback_cache()
|
||||
{
|
||||
return &audio_playback_cache_;
|
||||
}
|
||||
|
||||
virtual void BeginOperation() override;
|
||||
|
||||
virtual void EndOperation() override;
|
||||
|
||||
static const QString kTextureInput;
|
||||
static const QString kSamplesInput;
|
||||
static const QString kTrackInputFormat;
|
||||
|
||||
TimelinePoints* timeline_points()
|
||||
{
|
||||
return &timeline_points_;
|
||||
}
|
||||
|
||||
const QUuid& uuid() const
|
||||
{
|
||||
return uuid_;
|
||||
}
|
||||
|
||||
signals:
|
||||
void TimebaseChanged(const rational&);
|
||||
|
||||
void LengthChanged(const rational& length);
|
||||
|
||||
void SizeChanged(int width, int height);
|
||||
|
||||
void PixelAspectChanged(const rational& pixel_aspect);
|
||||
|
||||
void InterlacingChanged(VideoParams::Interlacing mode);
|
||||
|
||||
void VideoParamsChanged();
|
||||
void AudioParamsChanged();
|
||||
|
||||
void TrackAdded(Track* track);
|
||||
void TrackRemoved(Track* track);
|
||||
|
||||
void TextureInputChanged();
|
||||
|
||||
public slots:
|
||||
void VerifyLength();
|
||||
|
||||
protected:
|
||||
virtual void NameChangedEvent(const QString& name) override;
|
||||
virtual void InputConnectedEvent(const QString &input, int element, const NodeOutput &output) override;
|
||||
|
||||
virtual void InputDisconnectedEvent(const QString &input, int element, const NodeOutput &output) override;
|
||||
|
||||
virtual rational GetCustomLength(Track::Type type) const;
|
||||
|
||||
virtual void ShiftVideoEvent(const rational &from, const rational &to);
|
||||
|
||||
virtual void ShiftAudioEvent(const rational &from, const rational &to);
|
||||
|
||||
virtual void LoadInternal(QXmlStreamReader* reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt* cancelled) override;
|
||||
|
||||
virtual void SaveInternal(QXmlStreamWriter *writer) const override;
|
||||
|
||||
private:
|
||||
ViewerOutput* viewer_output_;
|
||||
QVector<TrackList*> track_lists_;
|
||||
|
||||
QVector<Track*> track_cache_;
|
||||
|
||||
QUuid uuid_;
|
||||
|
||||
rational last_length_;
|
||||
|
||||
FrameHashCache video_frame_cache_;
|
||||
|
||||
AudioPlaybackCache audio_playback_cache_;
|
||||
|
||||
int operation_stack_;
|
||||
|
||||
VideoParams video_params_;
|
||||
AudioParams audio_params_;
|
||||
|
||||
TimelinePoints timeline_points_;
|
||||
|
||||
private slots:
|
||||
void UpdateTrackCache();
|
||||
|
||||
};
|
||||
|
||||
|
||||
+47
-20
@@ -26,6 +26,7 @@
|
||||
#include "common/xmlutils.h"
|
||||
#include "core.h"
|
||||
#include "dialog/progress/progress.h"
|
||||
#include "node/factory.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "window/mainwindow/mainwindow.h"
|
||||
|
||||
@@ -36,7 +37,6 @@ Project::Project() :
|
||||
autorecovery_saved_(true)
|
||||
{
|
||||
root_.setParent(this);
|
||||
root_.set_project(this);
|
||||
|
||||
connect(&color_manager_, &ColorManager::ConfigChanged,
|
||||
this, &Project::ColorConfigChanged);
|
||||
@@ -79,6 +79,38 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, uint
|
||||
|
||||
*layout = MainWindowLayoutInfo::fromXml(reader, xml_node_data);
|
||||
|
||||
} else if (reader->name() == QStringLiteral("nodes")) {
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("node")) {
|
||||
QString id;
|
||||
|
||||
{
|
||||
XMLAttributeLoop(reader, attr) {
|
||||
if (attr.name() == QStringLiteral("id")) {
|
||||
id = attr.value().toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
qWarning() << "Failed to load node with empty ID";
|
||||
} else {
|
||||
Node* node = NodeFactory::CreateFromID(id);
|
||||
|
||||
if (!node) {
|
||||
qWarning() << "Failed to find node with ID" << id;
|
||||
} else {
|
||||
node->Load(reader, xml_node_data, version, cancelled);
|
||||
node->setParent(this);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Skip this
|
||||
@@ -86,6 +118,12 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, uint
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Make connections
|
||||
XMLConnectNodes(xml_node_data);
|
||||
|
||||
// Link blocks
|
||||
XMLLinkBlocks(xml_node_data);
|
||||
}
|
||||
|
||||
void Project::Save(QXmlStreamWriter *writer) const
|
||||
@@ -156,11 +194,6 @@ ColorManager *Project::color_manager()
|
||||
return &color_manager_;
|
||||
}
|
||||
|
||||
QVector<Item *> Project::get_items_of_type(Item::Type type) const
|
||||
{
|
||||
return root_.get_children_of_type(type, true);
|
||||
}
|
||||
|
||||
bool Project::is_modified() const
|
||||
{
|
||||
return is_modified_;
|
||||
@@ -199,27 +232,21 @@ const QString &Project::cache_path(bool default_if_empty) const
|
||||
|
||||
void Project::ColorConfigChanged()
|
||||
{
|
||||
QVector<Item*> footage = this->get_items_of_type(Item::kFootage);
|
||||
QVector<Footage*> footage = root()->ListOutputsOfType<Footage>();
|
||||
|
||||
foreach (Item* item, footage) {
|
||||
foreach (Stream* s, static_cast<Footage*>(item)->streams()) {
|
||||
if (s->type() == Stream::kVideo) {
|
||||
static_cast<VideoStream*>(s)->ColorConfigChanged();
|
||||
}
|
||||
}
|
||||
foreach (Footage* item, footage) {
|
||||
item->InvalidateAll(QString());
|
||||
//static_cast<VideoStream*>(s)->ColorConfigChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void Project::DefaultColorSpaceChanged()
|
||||
{
|
||||
QVector<Item*> footage = this->get_items_of_type(Item::kFootage);
|
||||
QVector<Footage*> footage = root_.ListOutputsOfType<Footage>();
|
||||
|
||||
foreach (Item* item, footage) {
|
||||
foreach (Stream* s, static_cast<Footage*>(item)->streams()) {
|
||||
if (s->type() == Stream::kVideo) {
|
||||
static_cast<VideoStream*>(s)->DefaultColorSpaceChanged();
|
||||
}
|
||||
}
|
||||
foreach (Footage* item, footage) {
|
||||
item->InvalidateAll(QString());
|
||||
//static_cast<VideoStream*>(s)->DefaultColorSpaceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace olive {
|
||||
* * Project Settings
|
||||
* * Window Layout
|
||||
*/
|
||||
class Project : public QObject
|
||||
class Project : public NodeGraph
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -61,8 +61,6 @@ public:
|
||||
|
||||
ColorManager* color_manager();
|
||||
|
||||
QVector<Item*> get_items_of_type(Item::Type type) const;
|
||||
|
||||
bool is_modified() const;
|
||||
void set_modified(bool e);
|
||||
|
||||
|
||||
+120
-216
@@ -25,7 +25,7 @@
|
||||
#include <QUrl>
|
||||
|
||||
#include "core.h"
|
||||
#include "node/input/media/media.h"
|
||||
#include "widget/nodeview/nodeviewundo.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
@@ -48,8 +48,16 @@ void ProjectViewModel::set_project(Project *p)
|
||||
{
|
||||
beginResetModel();
|
||||
|
||||
if (project_) {
|
||||
DisconnectItem(project_->root());
|
||||
}
|
||||
|
||||
project_ = p;
|
||||
|
||||
if (project_) {
|
||||
ConnectItem(project_->root());
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
@@ -60,8 +68,8 @@ QModelIndex ProjectViewModel::index(int row, int column, const QModelIndex &pare
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
// Get the parent object (project root if the index is invalid)
|
||||
Item* item_parent = GetItemObjectFromIndex(parent);
|
||||
// Get the parent object, we assume it's a folder since only folders can have children
|
||||
Folder* item_parent = static_cast<Folder*>(GetItemObjectFromIndex(parent));
|
||||
|
||||
// Return an index to this object
|
||||
return createIndex(row, column, item_parent->item_child(row));
|
||||
@@ -103,7 +111,7 @@ int ProjectViewModel::rowCount(const QModelIndex &parent) const
|
||||
}
|
||||
|
||||
// Otherwise, the index must contain a valid pointer, so we just return its child count
|
||||
return GetItemObjectFromIndex(parent)->item_child_count();
|
||||
return static_cast<Folder*>(GetItemObjectFromIndex(parent))->item_child_count();
|
||||
}
|
||||
|
||||
int ProjectViewModel::columnCount(const QModelIndex &parent) const
|
||||
@@ -131,7 +139,7 @@ QVariant ProjectViewModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
switch (column_type) {
|
||||
case kName:
|
||||
return internal_item->name();
|
||||
return internal_item->GetLabel();
|
||||
case kDuration:
|
||||
return internal_item->duration();
|
||||
case kRate:
|
||||
@@ -175,20 +183,11 @@ QVariant ProjectViewModel::headerData(int section, Qt::Orientation orientation,
|
||||
|
||||
bool ProjectViewModel::hasChildren(const QModelIndex &parent) const
|
||||
{
|
||||
// Check if this is a valid index
|
||||
if (parent.isValid()) {
|
||||
Item* item = GetItemObjectFromIndex(parent);
|
||||
// If it's a folder, we always return TRUE in order to always show the "expand triangle" icon,
|
||||
// even when there are no "physical" children
|
||||
Item* item = GetItemObjectFromIndex(parent);
|
||||
|
||||
// Check if this item is a kFolder type
|
||||
// If it's a folder, we always return TRUE in order to always show the "expand triangle" icon,
|
||||
// even when there are no "physical" children
|
||||
if (item->CanHaveChildren()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, return default behavior
|
||||
return QAbstractItemModel::hasChildren(parent);
|
||||
return dynamic_cast<Folder*>(item);
|
||||
}
|
||||
|
||||
bool ProjectViewModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
@@ -197,9 +196,11 @@ bool ProjectViewModel::setData(const QModelIndex &index, const QVariant &value,
|
||||
if (index.isValid() && columns_.at(index.column()) == kName && role == Qt::EditRole) {
|
||||
Item* item = GetItemObjectFromIndex(index);
|
||||
|
||||
RenameItemCommand* ric = new RenameItemCommand(this, item, value.toString());
|
||||
NodeRenameCommand* nrc = new NodeRenameCommand();
|
||||
|
||||
Core::instance()->undo_stack()->push(ric);
|
||||
nrc->AddNode(item, value.toString());
|
||||
|
||||
Core::instance()->undo_stack()->push(nrc);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -209,20 +210,8 @@ bool ProjectViewModel::setData(const QModelIndex &index, const QVariant &value,
|
||||
|
||||
bool ProjectViewModel::canFetchMore(const QModelIndex &parent) const
|
||||
{
|
||||
// Check if this is a valid index
|
||||
if (parent.isValid()) {
|
||||
Item* item = GetItemObjectFromIndex(parent);
|
||||
|
||||
// Check if this item is a kFolder type
|
||||
// If it's a folder, we always return TRUE in order to always show the "expand triangle" icon,
|
||||
// even when there are no "physical" children
|
||||
if (item->CanHaveChildren()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, return default behavior
|
||||
return QAbstractItemModel::canFetchMore(parent);
|
||||
// Use the same hack that always returns true with folders so the expand triangle is always visible
|
||||
return hasChildren(parent);
|
||||
}
|
||||
|
||||
Qt::ItemFlags ProjectViewModel::flags(const QModelIndex &index) const
|
||||
@@ -234,7 +223,7 @@ Qt::ItemFlags ProjectViewModel::flags(const QModelIndex &index) const
|
||||
|
||||
Qt::ItemFlags f = Qt::ItemIsDragEnabled | QAbstractItemModel::flags(index);
|
||||
|
||||
if (GetItemObjectFromIndex(index)->CanHaveChildren()) {
|
||||
if (dynamic_cast<Folder*>(GetItemObjectFromIndex(index))) {
|
||||
f |= Qt::ItemIsDropEnabled;
|
||||
}
|
||||
|
||||
@@ -277,7 +266,7 @@ QMimeData *ProjectViewModel::mimeData(const QModelIndexList &indexes) const
|
||||
// If not, add it to the stream (and also keep track of it in the vector)
|
||||
quint64 stream_flags;
|
||||
|
||||
if (static_cast<Item*>(index.internalPointer())->type() == Item::kFootage) {
|
||||
if (dynamic_cast<Folder*>(static_cast<Item*>(index.internalPointer()))) {
|
||||
stream_flags = static_cast<Footage*>(index.internalPointer())->get_enabled_stream_flags();
|
||||
} else {
|
||||
stream_flags = UINT64_MAX;
|
||||
@@ -320,7 +309,7 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action
|
||||
Item* drop_location = GetItemObjectFromIndex(drop);
|
||||
|
||||
// If this is not a folder, we cannot drop these items here
|
||||
if (!drop_location->CanHaveChildren()) {
|
||||
if (!dynamic_cast<Folder*>(drop_location)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -342,8 +331,11 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action
|
||||
// Check if Item is already the drop location or if its parent is the drop location, in which case this is a
|
||||
// no-op
|
||||
|
||||
if (item != drop_location && item->parent() != drop_location && !ItemIsParentOfChild(item, drop_location)) {
|
||||
move_command->add_child(new MoveItemCommand(this, item, static_cast<Folder*>(drop_location)));
|
||||
if (item != drop_location && item->item_parent() != drop_location && !ItemIsParentOfChild(item, drop_location)) {
|
||||
NodeInput child_input(item, Item::kParentInput);
|
||||
|
||||
move_command->add_child(new NodeEdgeRemoveCommand(item->item_parent(), child_input));
|
||||
move_command->add_child(new NodeEdgeAddCommand(static_cast<Folder*>(drop_location), child_input));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +364,7 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action
|
||||
Item* drop_item = GetItemObjectFromIndex(drop);
|
||||
|
||||
// If we didn't drop onto an item, find the nearest parent folder (should eventually terminate at root either way)
|
||||
while (!drop_item->CanHaveChildren()) {
|
||||
while (!dynamic_cast<Folder*>(drop_item)) {
|
||||
drop_item = drop_item->item_parent();
|
||||
}
|
||||
|
||||
@@ -383,83 +375,25 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, Qt::DropAction action
|
||||
return false;
|
||||
}
|
||||
|
||||
void ProjectViewModel::AddChild(Item *parent, Item *child)
|
||||
{
|
||||
QModelIndex parent_index;
|
||||
|
||||
if (parent != project_->root()) {
|
||||
parent_index = CreateIndexFromItem(parent);
|
||||
}
|
||||
|
||||
beginInsertRows(parent_index, parent->item_child_count(), parent->item_child_count());
|
||||
|
||||
child->setParent(parent);
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void ProjectViewModel::RemoveChild(Item *parent, Item *child, QObject *new_parent)
|
||||
{
|
||||
QModelIndex parent_index;
|
||||
|
||||
if (parent != project_->root()) {
|
||||
parent_index = CreateIndexFromItem(parent);
|
||||
}
|
||||
|
||||
int child_row = IndexOfChild(child);
|
||||
|
||||
beginRemoveRows(parent_index, child_row, child_row);
|
||||
|
||||
child->setParent(new_parent);
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
void ProjectViewModel::RenameChild(Item *item, const QString &name)
|
||||
{
|
||||
item->set_name(name);
|
||||
|
||||
QModelIndex index = CreateIndexFromItem(item, columns_.indexOf(kName));
|
||||
|
||||
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
|
||||
}
|
||||
|
||||
int ProjectViewModel::IndexOfChild(Item *item) const
|
||||
{
|
||||
// Find parent's index within its own parent
|
||||
// (FIXME: this model should handle sorting, which means it'll have to "know" the indices)
|
||||
Folder* parent = item->item_parent();
|
||||
|
||||
if (item == project_->root()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
Item* parent = item->item_parent();
|
||||
|
||||
if (parent != nullptr) {
|
||||
for (int i=0;i<parent->item_child_count();i++) {
|
||||
if (parent->item_child(i) == item) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (parent) {
|
||||
return parent->index_of_child(item);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ProjectViewModel::ChildCount(const QModelIndex &index)
|
||||
{
|
||||
Item* item = GetItemObjectFromIndex(index);
|
||||
|
||||
return item->item_child_count();
|
||||
}
|
||||
|
||||
Item *ProjectViewModel::GetItemObjectFromIndex(const QModelIndex &index) const
|
||||
{
|
||||
if (index.isValid()) {
|
||||
return static_cast<Item*>(index.internalPointer());
|
||||
}
|
||||
|
||||
return project_->root();
|
||||
return project_ ? project_->root() : nullptr;
|
||||
}
|
||||
|
||||
bool ProjectViewModel::ItemIsParentOfChild(Item *parent, Item *child) const
|
||||
@@ -476,18 +410,95 @@ bool ProjectViewModel::ItemIsParentOfChild(Item *parent, Item *child) const
|
||||
return false;
|
||||
}
|
||||
|
||||
void ProjectViewModel::MoveItemInternal(Item *item, Item *destination)
|
||||
void ProjectViewModel::ConnectItem(Item *n)
|
||||
{
|
||||
QModelIndex item_index = CreateIndexFromItem(item);
|
||||
connect(n, &Item::LabelChanged, this, &ProjectViewModel::ItemRenamed);
|
||||
|
||||
QModelIndex destination_index = CreateIndexFromItem(destination);
|
||||
Folder* f = dynamic_cast<Folder*>(n);
|
||||
if (f) {
|
||||
connect(f, &Folder::BeginInsertItem, this, &ProjectViewModel::FolderBeginInsertItem);
|
||||
connect(f, &Folder::EndInsertItem, this, &ProjectViewModel::FolderEndInsertItem);
|
||||
connect(f, &Folder::BeginRemoveItem, this, &ProjectViewModel::FolderBeginRemoveItem);
|
||||
connect(f, &Folder::EndRemoveItem, this, &ProjectViewModel::FolderEndRemoveItem);
|
||||
|
||||
beginMoveRows(item_index.parent(), item_index.row(), item_index.row(),
|
||||
destination_index, destination->item_child_count());
|
||||
foreach (const Node::OutputConnection& c, f->output_connections()) {
|
||||
Item* item = dynamic_cast<Item*>(c.second.node());
|
||||
|
||||
item->setParent(destination);
|
||||
if (item) {
|
||||
ConnectItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endMoveRows();
|
||||
void ProjectViewModel::DisconnectItem(Item *n)
|
||||
{
|
||||
disconnect(n, &Item::LabelChanged, this, &ProjectViewModel::ItemRenamed);
|
||||
|
||||
Folder* f = dynamic_cast<Folder*>(n);
|
||||
if (f) {
|
||||
disconnect(f, &Folder::BeginInsertItem, this, &ProjectViewModel::FolderBeginInsertItem);
|
||||
disconnect(f, &Folder::EndInsertItem, this, &ProjectViewModel::FolderEndInsertItem);
|
||||
disconnect(f, &Folder::BeginRemoveItem, this, &ProjectViewModel::FolderBeginRemoveItem);
|
||||
disconnect(f, &Folder::EndRemoveItem, this, &ProjectViewModel::FolderEndRemoveItem);
|
||||
|
||||
foreach (const Node::OutputConnection& c, f->output_connections()) {
|
||||
Item* item = dynamic_cast<Item*>(c.second.node());
|
||||
|
||||
if (item) {
|
||||
DisconnectItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectViewModel::FolderBeginInsertItem(Item *n, int insert_index)
|
||||
{
|
||||
Folder* folder = static_cast<Folder*>(sender());
|
||||
|
||||
ConnectItem(n);
|
||||
|
||||
QModelIndex index;
|
||||
|
||||
if (folder != project_->root()) {
|
||||
index = CreateIndexFromItem(folder);
|
||||
}
|
||||
|
||||
beginInsertRows(index, insert_index, insert_index);
|
||||
}
|
||||
|
||||
void ProjectViewModel::FolderEndInsertItem()
|
||||
{
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void ProjectViewModel::FolderBeginRemoveItem(Item *n, int child_index)
|
||||
{
|
||||
Folder* folder = static_cast<Folder*>(sender());
|
||||
|
||||
DisconnectItem(n);
|
||||
|
||||
QModelIndex index;
|
||||
|
||||
if (folder != project_->root()) {
|
||||
index = CreateIndexFromItem(folder);
|
||||
}
|
||||
|
||||
beginRemoveRows(index, child_index, child_index);
|
||||
}
|
||||
|
||||
void ProjectViewModel::FolderEndRemoveItem()
|
||||
{
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
void ProjectViewModel::ItemRenamed()
|
||||
{
|
||||
Item* item = static_cast<Item*>(sender());
|
||||
|
||||
QModelIndex index = CreateIndexFromItem(item);
|
||||
|
||||
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
|
||||
}
|
||||
|
||||
QModelIndex ProjectViewModel::CreateIndexFromItem(Item *item, int column)
|
||||
@@ -495,111 +506,4 @@ QModelIndex ProjectViewModel::CreateIndexFromItem(Item *item, int column)
|
||||
return createIndex(IndexOfChild(item), column, item);
|
||||
}
|
||||
|
||||
ProjectViewModel::MoveItemCommand::MoveItemCommand(ProjectViewModel *model,
|
||||
Item *item,
|
||||
Folder *destination) :
|
||||
model_(model),
|
||||
item_(item),
|
||||
destination_(destination)
|
||||
{
|
||||
source_ = static_cast<Folder*>(item->parent());
|
||||
|
||||
set_name(QCoreApplication::translate("MoveItemCommand", "Move Item"));
|
||||
}
|
||||
|
||||
Project *ProjectViewModel::MoveItemCommand::GetRelevantProject() const
|
||||
{
|
||||
return model_->project();
|
||||
}
|
||||
|
||||
void ProjectViewModel::MoveItemCommand::redo()
|
||||
{
|
||||
model_->MoveItemInternal(item_, destination_);
|
||||
}
|
||||
|
||||
void ProjectViewModel::MoveItemCommand::undo()
|
||||
{
|
||||
model_->MoveItemInternal(item_, source_);
|
||||
}
|
||||
|
||||
ProjectViewModel::RenameItemCommand::RenameItemCommand(ProjectViewModel* model, Item *item, const QString &name) :
|
||||
model_(model),
|
||||
item_(item),
|
||||
new_name_(name)
|
||||
{
|
||||
old_name_ = item->name();
|
||||
|
||||
set_name(QCoreApplication::translate("RenameItemCommand", "Rename Item"));
|
||||
}
|
||||
|
||||
Project *ProjectViewModel::RenameItemCommand::GetRelevantProject() const
|
||||
{
|
||||
return model_->project();
|
||||
}
|
||||
|
||||
void ProjectViewModel::RenameItemCommand::redo()
|
||||
{
|
||||
model_->RenameChild(item_, new_name_);
|
||||
}
|
||||
|
||||
void ProjectViewModel::RenameItemCommand::undo()
|
||||
{
|
||||
model_->RenameChild(item_, old_name_);
|
||||
}
|
||||
|
||||
ProjectViewModel::AddItemCommand::AddItemCommand(ProjectViewModel* model, Item* folder, Item* child) :
|
||||
model_(model),
|
||||
parent_(folder),
|
||||
child_(child)
|
||||
{
|
||||
// Ensure all operations are done in folder's thread
|
||||
if (memory_manager_.thread() != parent_->thread()) {
|
||||
memory_manager_.moveToThread(parent_->thread());
|
||||
}
|
||||
|
||||
child_->setParent(&memory_manager_);
|
||||
}
|
||||
|
||||
Project *ProjectViewModel::AddItemCommand::GetRelevantProject() const
|
||||
{
|
||||
return model_->project();
|
||||
}
|
||||
|
||||
void ProjectViewModel::AddItemCommand::redo()
|
||||
{
|
||||
model_->AddChild(parent_, child_);
|
||||
}
|
||||
|
||||
void ProjectViewModel::AddItemCommand::undo()
|
||||
{
|
||||
model_->RemoveChild(parent_, child_, &memory_manager_);
|
||||
}
|
||||
|
||||
ProjectViewModel::RemoveItemCommand::RemoveItemCommand(ProjectViewModel *model, Item *item) :
|
||||
model_(model),
|
||||
item_(item)
|
||||
{
|
||||
// Ensure all operations are done in folder's thread
|
||||
parent_ = item_->item_parent();
|
||||
|
||||
if (memory_manager_.thread() != item_->thread()) {
|
||||
memory_manager_.moveToThread(item_->thread());
|
||||
}
|
||||
}
|
||||
|
||||
Project *ProjectViewModel::RemoveItemCommand::GetRelevantProject() const
|
||||
{
|
||||
return model_->project();
|
||||
}
|
||||
|
||||
void ProjectViewModel::RemoveItemCommand::redo()
|
||||
{
|
||||
model_->RemoveChild(parent_, item_, &memory_manager_);
|
||||
}
|
||||
|
||||
void ProjectViewModel::RemoveItemCommand::undo()
|
||||
{
|
||||
model_->AddChild(parent_, item_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+15
-110
@@ -99,99 +99,11 @@ public:
|
||||
virtual QMimeData * mimeData(const QModelIndexList &indexes) const override;
|
||||
virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
|
||||
|
||||
/** Other model functions */
|
||||
void AddChild(Item* parent, Item* child);
|
||||
void RemoveChild(Item* parent, Item* child, QObject* new_parent);
|
||||
void RenameChild(Item* item, const QString& name);
|
||||
|
||||
/**
|
||||
* @brief Convenience function for creating QModelIndexes from an Item object
|
||||
*/
|
||||
QModelIndex CreateIndexFromItem(Item* item, int column = 0);
|
||||
|
||||
/**
|
||||
* @brief An UndoCommand for moving an item from one folder to another folder
|
||||
*/
|
||||
class MoveItemCommand : public UndoCommand {
|
||||
public:
|
||||
MoveItemCommand(ProjectViewModel* model, Item* item, Folder* destination);
|
||||
|
||||
virtual Project* GetRelevantProject() const override;
|
||||
|
||||
virtual void redo() override;
|
||||
|
||||
virtual void undo() override;
|
||||
|
||||
private:
|
||||
ProjectViewModel* model_;
|
||||
Item* item_;
|
||||
Folder* source_;
|
||||
Folder* destination_;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief An UndoCommand for renaming an item
|
||||
*/
|
||||
class RenameItemCommand : public UndoCommand {
|
||||
public:
|
||||
RenameItemCommand(ProjectViewModel* model, Item* item, const QString& name);
|
||||
|
||||
virtual Project* GetRelevantProject() const override;
|
||||
|
||||
virtual void redo() override;
|
||||
|
||||
virtual void undo() override;
|
||||
|
||||
private:
|
||||
ProjectViewModel* model_;
|
||||
Item* item_;
|
||||
QString old_name_;
|
||||
QString new_name_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief An UndoCommand for adding an item
|
||||
*/
|
||||
class AddItemCommand : public UndoCommand {
|
||||
public:
|
||||
AddItemCommand(ProjectViewModel* model, Item* folder, Item *child);
|
||||
|
||||
virtual Project* GetRelevantProject() const override;
|
||||
|
||||
virtual void redo() override;
|
||||
|
||||
virtual void undo() override;
|
||||
|
||||
private:
|
||||
ProjectViewModel* model_;
|
||||
Item* parent_;
|
||||
Item* child_;
|
||||
QObject memory_manager_;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief An undo command for removing an item
|
||||
*/
|
||||
class RemoveItemCommand : public UndoCommand {
|
||||
public:
|
||||
RemoveItemCommand(ProjectViewModel* model, Item* item);
|
||||
|
||||
virtual Project* GetRelevantProject() const override;
|
||||
|
||||
virtual void redo() override;
|
||||
|
||||
virtual void undo() override;
|
||||
|
||||
private:
|
||||
ProjectViewModel* model_;
|
||||
Item* item_;
|
||||
Item* parent_;
|
||||
QObject memory_manager_;
|
||||
|
||||
};
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Retrieve the index of `item` in its parent
|
||||
@@ -205,17 +117,6 @@ private:
|
||||
*/
|
||||
int IndexOfChild(Item* item) const;
|
||||
|
||||
/**
|
||||
* @brief Get the child count of an index
|
||||
*
|
||||
* @param index
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* Return number of children (immediate children only)
|
||||
*/
|
||||
int ChildCount(const QModelIndex& index);
|
||||
|
||||
/**
|
||||
* @brief Retrieves the Item object from a given index
|
||||
*
|
||||
@@ -230,21 +131,25 @@ private:
|
||||
*/
|
||||
bool ItemIsParentOfChild(Item* parent, Item* child) const;
|
||||
|
||||
/**
|
||||
* @brief Moves an item to a new destination updating all views in the process
|
||||
*
|
||||
* This function will emit a signal indicating that rows are moving, set `destination` as the new parent of `item`,
|
||||
* and then emit a signal that the row has finished moving.
|
||||
*
|
||||
* It's not recommended to use this function directly in most cases since it does not create an UndoCommand allowing
|
||||
* the user to undo the move. Instead this function should primarily be called from UndoCommands belonging to this
|
||||
* class (e.g. MoveItemCommand).
|
||||
*/
|
||||
void MoveItemInternal(Item* item, Item* destination);
|
||||
void ConnectItem(Item* n);
|
||||
|
||||
void DisconnectItem(Item* n);
|
||||
|
||||
Project* project_;
|
||||
|
||||
QVector<ColumnType> columns_;
|
||||
|
||||
private slots:
|
||||
void FolderBeginInsertItem(Item* n, int insert_index);
|
||||
|
||||
void FolderEndInsertItem();
|
||||
|
||||
void FolderBeginRemoveItem(Item* n, int child_index);
|
||||
|
||||
void FolderEndRemoveItem();
|
||||
|
||||
void ItemRenamed();
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user