timeline/various: implemented block copy/paste

Implemented the ability to copy/paste blocks/clips in the timeline. This did
require some large scale changes and reworking of the copy/paste system
introduced a few commits ago, but should be largely functional now.
This commit is contained in:
itsmattkc
2020-03-31 02:19:05 +11:00
parent 67d8ad571d
commit 2bda306866
35 changed files with 466 additions and 233 deletions
+1
View File
@@ -23,6 +23,7 @@ add_subdirectory(focusablelineedit)
add_subdirectory(footagecombobox)
add_subdirectory(keyframeview)
add_subdirectory(menu)
add_subdirectory(nodecopypaste)
add_subdirectory(nodeview)
add_subdirectory(nodeparamview)
add_subdirectory(panel)
+22
View File
@@ -0,0 +1,22 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2019 Olive Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
widget/nodecopypaste/nodecopypaste.h
widget/nodecopypaste/nodecopypaste.cpp
PARENT_SCOPE
)
+138
View File
@@ -0,0 +1,138 @@
#include "nodecopypaste.h"
#include <QMessageBox>
#include "core.h"
#include "widget/nodeview/nodeviewundo.h"
#include "window/mainwindow/mainwindow.h"
void NodeCopyPasteWidget::CopyNodesToClipboard(const QList<Node *> &nodes, void *userdata)
{
QString copy_str;
QXmlStreamWriter writer(&copy_str);
writer.setAutoFormatting(true);
writer.writeStartDocument();
writer.writeStartElement(QStringLiteral("olive"));
foreach (Node* n, nodes) {
n->Save(&writer);
}
CopyNodesToClipboardInternal(&writer, userdata);
writer.writeEndElement(); // olive
writer.writeEndDocument();
Core::CopyStringToClipboard(copy_str);
}
QList<Node *> NodeCopyPasteWidget::PasteNodesFromClipboard(Sequence *graph, QUndoCommand* command, void *userdata)
{
QString clipboard = Core::PasteStringFromClipboard();
if (clipboard.isEmpty()) {
return QList<Node*>();
}
QXmlStreamReader reader(clipboard);
QList<Node*> pasted_nodes;
XMLNodeData xml_node_data;
while (XMLReadNextStartElement(&reader)) {
if (reader.name() == QStringLiteral("olive")) {
while (XMLReadNextStartElement(&reader)) {
if (reader.name() == QStringLiteral("node")) {
Node* node = XMLLoadNode(&reader);
if (node) {
node->Load(&reader, xml_node_data, nullptr);
pasted_nodes.append(node);
}
} else {
PasteNodesFromClipboardInternal(&reader, userdata);
}
}
} else {
reader.skipCurrentElement();
}
}
if (pasted_nodes.isEmpty()) {
// If we passed through the whole string and there were no nodes, it must not be data for us after all
return QList<Node*>();
}
// If we have some nodes AND the XML data was malformed, the user should probably know
if (reader.hasError()) {
// Delete all nodes so this is a no-op
foreach (Node* n, pasted_nodes) {
delete n;
}
// If this was NOT an internal error, we assume it's an XML error that the user needs to know about
QMessageBox::critical(Core::instance()->main_window(),
QCoreApplication::translate("NodeCopyPasteWidget", "Error pasting nodes"),
QCoreApplication::translate("NodeCopyPasteWidget", "Failed to paste nodes: %1").arg(reader.errorString()),
QMessageBox::Ok);
return QList<Node*>();
}
// Add all nodes to graph
foreach (Node* n, pasted_nodes) {
new NodeAddCommand(graph, n, command);
}
// Make connections
if (!xml_node_data.desired_connections.isEmpty()) {
XMLConnectNodes(xml_node_data.output_ptrs, xml_node_data.desired_connections, command);
}
// Connect footage to existing footage if it exists
if (!xml_node_data.footage_connections.isEmpty()) {
// Get list of all footage from project
QList<ItemPtr> footage = graph->project()->get_items_of_type(Item::kFootage);
if (!footage.isEmpty()) {
foreach (const XMLNodeData::FootageConnection& con, xml_node_data.footage_connections) {
if (con.footage) {
// Assume this is a pointer to a Stream*
Stream* loaded_stream = reinterpret_cast<Stream*>(con.footage);
bool found = false;
foreach (ItemPtr item, footage) {
const QList<StreamPtr>& streams = std::static_pointer_cast<Footage>(item)->streams();
foreach (StreamPtr s, streams) {
if (s.get() == loaded_stream) {
con.input->set_standard_value(QVariant::fromValue(s));
found = true;
break;
}
}
if (found) {
break;
}
}
}
}
}
}
return pasted_nodes;
}
void NodeCopyPasteWidget::CopyNodesToClipboardInternal(QXmlStreamWriter*, void*)
{
}
void NodeCopyPasteWidget::PasteNodesFromClipboardInternal(QXmlStreamReader* reader, void*)
{
reader->skipCurrentElement();
}
+26
View File
@@ -0,0 +1,26 @@
#ifndef NODECOPYPASTEWIDGET_H
#define NODECOPYPASTEWIDGET_H
#include <QWidget>
#include <QUndoCommand>
#include "node/node.h"
#include "project/item/sequence/sequence.h"
class NodeCopyPasteWidget
{
public:
NodeCopyPasteWidget() = default;
protected:
void CopyNodesToClipboard(const QList<Node*>& nodes, void* userdata = nullptr);
QList<Node*> PasteNodesFromClipboard(Sequence *graph, QUndoCommand *command, void* userdata = nullptr);
virtual void CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void* userdata);
virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void* userdata);
};
#endif // NODECOPYPASTEWIDGET_H
+6 -2
View File
@@ -143,7 +143,7 @@ void NodeView::CopySelected(bool cut)
return;
}
Core::instance()->CopyNodesToClipboard(selected);
CopyNodesToClipboard(selected);
if (cut) {
DeleteSelected();
@@ -156,7 +156,11 @@ void NodeView::Paste()
return;
}
QList<Node*> pasted_nodes = Core::instance()->PasteNodesFromClipboard(static_cast<Sequence*>(graph_));
QUndoCommand* command = new QUndoCommand();
QList<Node*> pasted_nodes = PasteNodesFromClipboard(static_cast<Sequence*>(graph_), command);
Core::instance()->undo_stack()->pushIfHasChildren(command);
if (!pasted_nodes.isEmpty()) {
// FIXME: Attach to cursor so user can drop in place
+2 -1
View File
@@ -26,6 +26,7 @@
#include "node/graph.h"
#include "nodeviewscene.h"
#include "widget/nodecopypaste/nodecopypaste.h"
/**
* @brief A widget for viewing and editing node graphs
@@ -33,7 +34,7 @@
* This widget takes a NodeGraph object and constructs a QGraphicsScene representing its data, viewing and allowing
* the user to make modifications to it.
*/
class NodeView : public QGraphicsView
class NodeView : public QGraphicsView, public NodeCopyPasteWidget
{
Q_OBJECT
public:
+150 -11
View File
@@ -135,11 +135,8 @@ void TimelineWidget::Clear()
QMap<Block*, TimelineViewBlockItem*>::iterator iterator = block_items_.begin();
while (iterator != block_items_.end()) {
TimelineViewBlockItem* item = iterator.value();
delete iterator.value();
iterator = block_items_.erase(iterator);
delete item;
}
block_items_.clear();
@@ -254,6 +251,72 @@ void TimelineWidget::DisconnectNodeInternal(ViewerOutput *n)
}
}
void TimelineWidget::CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void* userdata)
{
writer->writeStartElement(QStringLiteral("timeline"));
// Cache the earliest in point so all copied clips have a "relative" in point that can be pasted anywhere
QList<TimelineViewBlockItem*>& selected = *static_cast<QList<TimelineViewBlockItem*>*>(userdata);
rational earliest_in = RATIONAL_MAX;
foreach (TimelineViewBlockItem* item, selected) {
Block* block = item->block();
earliest_in = qMin(earliest_in, block->in());
}
foreach (TimelineViewBlockItem* item, selected) {
Block* block = item->block();
writer->writeStartElement(QStringLiteral("block"));
writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast<quintptr>(block)));
writer->writeAttribute(QStringLiteral("in"), (block->in() - earliest_in).toString());
TrackOutput* track = TrackOutput::TrackFromBlock(block);
if (track) {
writer->writeAttribute(QStringLiteral("tracktype"), QString::number(track->track_type()));
writer->writeAttribute(QStringLiteral("trackindex"), QString::number(track->Index()));
}
writer->writeEndElement();
}
writer->writeEndElement(); // timeline
}
void TimelineWidget::PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void *userdata)
{
if (reader->name() == QStringLiteral("timeline")) {
QList<BlockPasteData>& paste_data = *static_cast<QList<BlockPasteData>*>(userdata);
while (XMLReadNextStartElement(reader)) {
if (reader->name() == QStringLiteral("block")) {
BlockPasteData bpd;
foreach (QXmlStreamAttribute attr, reader->attributes()) {
if (attr.name() == QStringLiteral("ptr")) {
bpd.ptr = attr.value().toULongLong();
} else if (attr.name() == QStringLiteral("in")) {
bpd.in = rational::fromString(attr.value().toString());
} else if (attr.name() == QStringLiteral("tracktype")) {
bpd.track_type = static_cast<Timeline::TrackType>(attr.value().toInt());
} else if (attr.name() == QStringLiteral("trackindex")) {
bpd.track_index = attr.value().toInt();
}
}
paste_data.append(bpd);
reader->skipCurrentElement();
}
}
} else {
NodeCopyPasteWidget::PasteNodesFromClipboardInternal(reader, userdata);
}
}
TimelineWidget::DraggedFootage TimelineWidget::FootageToDraggedFootage(Footage *f)
{
return DraggedFootage(f, f->get_enabled_stream_flags());
@@ -546,7 +609,7 @@ void TimelineWidget::CopySelected(bool cut)
}
}
Core::instance()->CopyNodesToClipboard(selected_nodes);
CopyNodesToClipboard(selected_nodes, &selected);
if (cut) {
DeleteSelected();
@@ -559,18 +622,45 @@ void TimelineWidget::Paste(bool insert)
return;
}
QList<Node*> pasted = Core::instance()->PasteNodesFromClipboard(static_cast<Sequence*>(GetConnectedNode()->parent()));
QUndoCommand* command = new QUndoCommand();
QList<BlockPasteData> paste_data;
QList<Node*> pasted = PasteNodesFromClipboard(static_cast<Sequence*>(GetConnectedNode()->parent()), command, &paste_data);
rational paste_start = GetTime();
if (insert) {
// FIXME: Implement this
}
rational paste_end = GetTime();
foreach (Node* n, pasted) {
// See if this block is a node and is a top level node
if (n->IsBlock() && !n->output()->IsConnected()) {
foreach (const BlockPasteData& bpd, paste_data) {
foreach (Node* n, pasted) {
if (n->property("xml_ptr") == bpd.ptr) {
paste_end = qMax(paste_end, paste_start + bpd.in + static_cast<Block*>(n)->length());
break;
}
}
}
if (paste_end != paste_start) {
InsertGapsAt(paste_start, paste_end - paste_start, command);
}
}
foreach (const BlockPasteData& bpd, paste_data) {
foreach (Node* n, pasted) {
if (n->property("xml_ptr") == bpd.ptr) {
qDebug() << "Placing" << n;
new TrackPlaceBlockCommand(GetConnectedNode()->track_list(bpd.track_type),
bpd.track_index,
static_cast<Block*>(n),
paste_start + bpd.in,
command);
break;
}
}
}
Core::instance()->undo_stack()->pushIfHasChildren(command);
}
QList<TimelineViewBlockItem *> TimelineWidget::GetSelectedBlocks()
@@ -657,6 +747,55 @@ void TimelineWidget::RippleEditTo(Timeline::MovementMode mode, bool insert_gaps)
}
}
void TimelineWidget::InsertGapsAt(const rational &earliest_point, const rational &insert_length, QUndoCommand *command)
{
QVector<Block*> blocks_to_split;
QList<Block*> blocks_to_append_gap_to;
QList<Block*> gaps_to_extend;
foreach (TrackOutput* track, GetConnectedNode()->Tracks()) {
if (track->IsLocked()) {
continue;
}
foreach (Block* b, track->Blocks()) {
if (b->out() >= earliest_point) {
if (b->type() == Block::kClip) {
if (b->out() > earliest_point) {
blocks_to_split.append(b);
}
blocks_to_append_gap_to.append(b);
} else if (b->type() == Block::kGap) {
gaps_to_extend.append(b);
}
break;
}
}
}
// Extend gaps that already exist
foreach (Block* gap, gaps_to_extend) {
new BlockResizeCommand(gap, gap->length() + insert_length, command);
}
// Split clips here
new BlockSplitPreservingLinksCommand(blocks_to_split, {earliest_point}, command);
// Insert gaps that don't exist yet
foreach (Block* b, blocks_to_append_gap_to) {
GapBlock* gap = new GapBlock();
gap->set_length_and_media_out(insert_length);
new NodeAddCommand(static_cast<NodeGraph*>(GetConnectedNode()->parent()), gap, command);
new TrackInsertBlockAfterCommand(TrackOutput::TrackFromBlock(b), gap, b, command);
}
}
TrackOutput *TimelineWidget::GetTrackFromReference(const TrackReference &ref)
{
return GetConnectedNode()->track_list(ref.type())->TrackAt(ref.index());
+14 -3
View File
@@ -8,6 +8,7 @@
#include "core.h"
#include "timelineandtrackview.h"
#include "node/output/viewer/viewer.h"
#include "widget/nodecopypaste/nodecopypaste.h"
#include "widget/slider/timeslider.h"
#include "widget/timebased/timebased.h"
@@ -16,7 +17,7 @@
*
* Encapsulates TimelineViews, TimeRulers, and scrollbars for a complete widget to manipulate Timelines
*/
class TimelineWidget : public TimeBasedWidget
class TimelineWidget : public TimeBasedWidget, public NodeCopyPasteWidget
{
Q_OBJECT
public:
@@ -78,6 +79,16 @@ protected:
virtual void ConnectNodeInternal(ViewerOutput* n) override;
virtual void DisconnectNodeInternal(ViewerOutput* n) override;
virtual void CopyNodesToClipboardInternal(QXmlStreamWriter *writer, void* userdata) override;
virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, void* userdata) override;
struct BlockPasteData {
quintptr ptr;
rational in;
Timeline::TrackType track_type;
int track_index;
};
private:
class DraggedFootage {
public:
@@ -162,8 +173,6 @@ private:
*/
bool SnapPoint(QList<rational> start_times, rational *movement, int snap_points = kSnapAll);
void InsertGapsAt(const rational& time, const rational& length, QUndoCommand* command);
void GetGhostData(const QVector<TimelineViewGhostItem*>& ghosts, rational *earliest_point, rational *latest_point);
void InsertGapsAtGhostDestination(const QVector<TimelineViewGhostItem*>& ghosts, QUndoCommand* command);
@@ -382,6 +391,8 @@ private:
bool dual_transition_;
};
void InsertGapsAt(const rational& time, const rational& length, QUndoCommand* command);
void DeleteSelectedInternal(const QList<Block *>& blocks, bool transition_aware, bool remove_from_graph, QUndoCommand* command);
void SetBlockLinksSelected(Block *block, bool selected);
+1 -50
View File
@@ -197,55 +197,6 @@ bool TimelineWidget::Tool::SnapPoint(QList<rational> start_times, rational* move
return (diff < DBL_MAX);
}
void TimelineWidget::Tool::InsertGapsAt(const rational &earliest_point, const rational &insert_length, QUndoCommand *command)
{
QVector<Block*> blocks_to_split;
QList<Block*> blocks_to_append_gap_to;
QList<Block*> gaps_to_extend;
foreach (TrackOutput* track, parent()->GetConnectedNode()->Tracks()) {
if (track->IsLocked()) {
continue;
}
foreach (Block* b, track->Blocks()) {
if (b->out() >= earliest_point) {
if (b->type() == Block::kClip) {
if (b->out() > earliest_point) {
blocks_to_split.append(b);
}
blocks_to_append_gap_to.append(b);
} else if (b->type() == Block::kGap) {
gaps_to_extend.append(b);
}
break;
}
}
}
// Extend gaps that already exist
foreach (Block* gap, gaps_to_extend) {
new BlockResizeCommand(gap, gap->length() + insert_length, command);
}
// Split clips here
new BlockSplitPreservingLinksCommand(blocks_to_split, {earliest_point}, command);
// Insert gaps that don't exist yet
foreach (Block* b, blocks_to_append_gap_to) {
GapBlock* gap = new GapBlock();
gap->set_length_and_media_out(insert_length);
new NodeAddCommand(static_cast<NodeGraph*>(parent()->GetConnectedNode()->parent()), gap, command);
new TrackInsertBlockAfterCommand(TrackOutput::TrackFromBlock(b), gap, b, command);
}
}
void TimelineWidget::Tool::GetGhostData(const QVector<TimelineViewGhostItem *> &ghosts, rational *earliest_point, rational *latest_point)
{
rational ep = RATIONAL_MAX;
@@ -271,5 +222,5 @@ void TimelineWidget::Tool::InsertGapsAtGhostDestination(const QVector<TimelineVi
GetGhostData(ghosts, &earliest_point, &latest_point);
InsertGapsAt(earliest_point, latest_point - earliest_point, command);
parent()->InsertGapsAt(earliest_point, latest_point - earliest_point, command);
}