From bac33e3d1615cb0f64ddbb198e7a15ed486315c5 Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Fri, 21 Feb 2020 17:28:17 +1100 Subject: [PATCH] importtool: create a sequence automatically from dropped footage if one isn't open already --- app/core.cpp | 72 ++-- app/core.h | 8 + app/project/item/sequence/sequence.cpp | 5 + app/project/item/sequence/sequence.h | 2 + app/widget/timelinewidget/timelinewidget.cpp | 16 +- app/widget/timelinewidget/timelinewidget.h | 5 + app/widget/timelinewidget/tool/import.cpp | 382 ++++++++++++------- 7 files changed, 306 insertions(+), 184 deletions(-) diff --git a/app/core.cpp b/app/core.cpp index 81c00f6a9..bd6f4f632 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -44,8 +44,6 @@ #include "project/projectimportmanager.h" #include "project/projectloadmanager.h" #include "project/projectsavemanager.h" -#include "project/item/footage/footage.h" -#include "project/item/sequence/sequence.h" #include "render/backend/indexmanager.h" #include "render/backend/opengl/opengltexturecache.h" #include "render/colormanager.h" @@ -312,34 +310,19 @@ void Core::CreateNewFolder() void Core::CreateNewSequence() { - // Locate the most recently focused Project panel (assume that's the panel the user wants to import into) - ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); - Project* active_project; + Project* active_project = GetActiveProject(); - if (active_project_panel == nullptr // Check that we found a Project panel - || (active_project = active_project_panel->project()) == nullptr) { // and that we could find an active Project + if (!active_project) { QMessageBox::critical(main_window_, tr("Failed to create new sequence"), tr("Failed to find active Project panel")); return; } - // Get the selected folder in this panel - Folder* folder = active_project_panel->GetSelectedFolder(); - // Create new sequence - SequencePtr new_sequence = std::make_shared(); + SequencePtr new_sequence = CreateNewSequenceForProject(active_project); // Set all defaults for the sequence new_sequence->set_default_parameters(); - // Get default name for this sequence (in the format "Sequence N", the first that doesn't exist) - int sequence_number = 1; - QString sequence_name; - do { - sequence_name = tr("Sequence %1").arg(sequence_number); - sequence_number++; - } while (active_project->root()->ChildExistsWithName(sequence_name)); - new_sequence->set_name(sequence_name); - SequenceDialog sd(new_sequence.get(), SequenceDialog::kNew, main_window_); // Make sure SequenceDialog doesn't make an undo command for editing the sequence, since we make an undo command for @@ -348,8 +331,8 @@ void Core::CreateNewSequence() if (sd.exec() == QDialog::Accepted) { // Create an undoable command - ProjectViewModel::AddItemCommand* aic = new ProjectViewModel::AddItemCommand(active_project_panel->model(), - folder, + ProjectViewModel::AddItemCommand* aic = new ProjectViewModel::AddItemCommand(GetActiveProjectModel(), + GetSelectedFolderInActiveProject(), new_sequence); new_sequence->add_default_nodes(); @@ -452,16 +435,35 @@ void Core::SaveAutorecovery() Project *Core::GetActiveProject() { - // Locate the most recently focused Project panel (assume that's the panel the user wants to import into) ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); - // If we couldn't find one, return nullptr - if (active_project_panel == nullptr) { + if (active_project_panel) { + return active_project_panel->project(); + } else { return nullptr; } +} - // Otherwise, return the project panel's project (which may be nullptr but in most cases shouldn't be) - return active_project_panel->project(); +ProjectViewModel *Core::GetActiveProjectModel() +{ + ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); + + if (active_project_panel) { + return active_project_panel->model(); + } else { + return nullptr; + } +} + +Folder *Core::GetSelectedFolderInActiveProject() +{ + ProjectPanel* active_project_panel = PanelManager::instance()->MostRecentlyFocused(); + + if (active_project_panel) { + return active_project_panel->GetSelectedFolder(); + } else { + return nullptr; + } } void Core::SetProjectModified() @@ -643,6 +645,22 @@ void Core::SetPreferenceForRenderMode(RenderMode::Mode mode, const QString &pref Config::Current()[GetRenderModePreferencePrefix(mode, preference)] = value; } +SequencePtr Core::CreateNewSequenceForProject(Project* project) const +{ + SequencePtr new_sequence = std::make_shared(); + + // Get default name for this sequence (in the format "Sequence N", the first that doesn't exist) + int sequence_number = 1; + QString sequence_name; + do { + sequence_name = tr("Sequence %1").arg(sequence_number); + sequence_number++; + } while (project->root()->ChildExistsWithName(sequence_name)); + new_sequence->set_name(sequence_name); + + return new_sequence; +} + void Core::OpenProject() { QString file = QFileDialog::getOpenFileName(main_window_, diff --git a/app/core.h b/app/core.h index b8e21336f..bb6093e0e 100644 --- a/app/core.h +++ b/app/core.h @@ -27,6 +27,7 @@ #include "common/rational.h" #include "project/item/footage/footage.h" +#include "project/item/sequence/sequence.h" #include "project/project.h" #include "project/projectviewmodel.h" #include "task/task.h" @@ -121,6 +122,8 @@ public: * The active Project file, or nullptr if the heuristic couldn't find one. */ Project* GetActiveProject(); + ProjectViewModel* GetActiveProjectModel(); + Folder* GetSelectedFolderInActiveProject(); /** * @brief Sets state to "modified" so that the GUI will prompt the user to save before closing @@ -174,6 +177,11 @@ public: static QVariant GetPreferenceForRenderMode(RenderMode::Mode mode, const QString& preference); static void SetPreferenceForRenderMode(RenderMode::Mode mode, const QString& preference, const QVariant& value); + /** + * @brief Create a new sequence named appropriately for the active project + */ + SequencePtr CreateNewSequenceForProject(Project *project) const; + public slots: /** * @brief Starts an open file dialog to load a project from file diff --git a/app/project/item/sequence/sequence.cpp b/app/project/item/sequence/sequence.cpp index edb0b5ab7..4a55a74a9 100644 --- a/app/project/item/sequence/sequence.cpp +++ b/app/project/item/sequence/sequence.cpp @@ -265,6 +265,11 @@ void Sequence::set_default_parameters() Config::Current()["DefaultSequenceAudioLayout"].toULongLong())); } +ViewerOutput *Sequence::viewer_output() const +{ + return viewer_output_; +} + void Sequence::NameChangedEvent(const QString &name) { viewer_output_->set_media_name(name); diff --git a/app/project/item/sequence/sequence.h b/app/project/item/sequence/sequence.h index c4ad7f4e8..b87753113 100644 --- a/app/project/item/sequence/sequence.h +++ b/app/project/item/sequence/sequence.h @@ -71,6 +71,8 @@ public: void set_default_parameters(); + ViewerOutput* viewer_output() const; + protected: virtual void NameChangedEvent(const QString& name) override; diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index d4d2b36f9..48aa01fb7 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -616,30 +616,22 @@ void TimelineWidget::ViewMouseDoubleClicked(TimelineViewMouseEvent *event) void TimelineWidget::ViewDragEntered(TimelineViewMouseEvent *event) { - if (GetConnectedNode()) { - import_tool_->DragEnter(event); - } + import_tool_->DragEnter(event); } void TimelineWidget::ViewDragMoved(TimelineViewMouseEvent *event) { - if (GetConnectedNode()) { - import_tool_->DragMove(event); - } + import_tool_->DragMove(event); } void TimelineWidget::ViewDragLeft(QDragLeaveEvent *event) { - if (GetConnectedNode()) { - import_tool_->DragLeave(event); - } + import_tool_->DragLeave(event); } void TimelineWidget::ViewDragDropped(TimelineViewMouseEvent *event) { - if (GetConnectedNode()) { - import_tool_->DragDrop(event); - } + import_tool_->DragDrop(event); } void TimelineWidget::AddBlock(Block *block, TrackReference track) diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index dad85c8d3..0bda11925 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -202,7 +202,12 @@ private: virtual void DragDrop(TimelineViewMouseEvent *event) override; private: + void FootageToGhosts(rational ghost_start, const QList& footage, const rational &dest_tb, const int &track_start); + + QList dragged_footage_; + int import_pre_buffer_; + }; class EditTool : public Tool diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 30193a7c1..945a446f2 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -30,6 +30,7 @@ #include "node/distort/transform/transform.h" #include "node/input/media/audio/audio.h" #include "node/input/media/video/video.h" +#include "project/item/sequence/sequence.h" #include "widget/nodeview/nodeviewundo.h" Timeline::TrackType TrackTypeFromStreamType(Stream::Type stream_type) @@ -78,10 +79,6 @@ void TimelineWidget::ImportTool::DragEnter(TimelineViewMouseEvent *event) // Set drag start position drag_start_ = event->GetCoordinates(); - // Set ghosts to start where the cursor entered - - rational ghost_start = drag_start_.GetFrame() - parent()->SceneToTime(import_pre_buffer_); - snap_points_.clear(); while (!stream.atEnd()) { @@ -92,62 +89,20 @@ void TimelineWidget::ImportTool::DragEnter(TimelineViewMouseEvent *event) // Check if Item is Footage if (item->type() == Item::kFootage) { + // If the Item is Footage, we can create a Ghost from it - Footage* footage = static_cast(item); + dragged_footage_.append(static_cast(item)); - // Each stream is offset by one track per track "type", we keep track of them in this vector - QVector track_offsets(Timeline::kTrackTypeCount); - track_offsets.fill(drag_start_.GetTrack().index()); - - rational footage_duration; - - // Loop through all streams in footage - foreach (StreamPtr stream, footage->streams()) { - Timeline::TrackType track_type = TrackTypeFromStreamType(stream->type()); - - // Check if this stream has a compatible TrackList - if (track_type == Timeline::kTrackTypeNone || !stream->enabled()) { - continue; - } - - TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); - - if (stream->type() == Stream::kImage) { - // Stream is essentially length-less - use config's default image length - footage_duration = Config::Current()["DefaultStillLength"].value(); - } else { - // Use duration from file - int64_t stream_duration = stream->duration(); - - // Rescale to timeline timebase - stream_duration = qCeil(static_cast(stream_duration) * stream->timebase().toDouble() / parent()->timebase_dbl()); - - // Convert to rational time - footage_duration = rational(parent()->timebase().numerator() * stream_duration, - parent()->timebase().denominator()); - } - - ghost->SetIn(ghost_start); - ghost->SetOut(ghost_start + footage_duration); - ghost->SetTrack(TrackReference(track_type, track_offsets.at(track_type))); - - // Increment track count for this track type - track_offsets[track_type]++; - - snap_points_.append(ghost->In()); - snap_points_.append(ghost->Out()); - - ghost->setData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(stream)); - ghost->SetMode(Timeline::kMove); - - parent()->AddGhost(ghost); - } - - // Stack each ghost one after the other - ghost_start += footage_duration; } } + if (parent()->GetConnectedNode()) { + FootageToGhosts(drag_start_.GetFrame() - parent()->SceneToTime(import_pre_buffer_), + dragged_footage_, + parent()->timebase(), + drag_start_.GetTrack().index()); + } + event->accept(); } else { // FIXME: Implement dropping from file @@ -157,45 +112,48 @@ void TimelineWidget::ImportTool::DragEnter(TimelineViewMouseEvent *event) void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) { - if (parent()->HasGhosts()) { - rational time_movement = event->GetFrame() - drag_start_.GetFrame(); - int track_movement = event->GetTrack().index() - drag_start_.GetTrack().index(); + if (!dragged_footage_.isEmpty()) { - // If snapping is enabled, check for snap points - if (Core::instance()->snapping()) { - SnapPoint(snap_points_, &time_movement); + if (parent()->HasGhosts()) { + rational time_movement = event->GetFrame() - drag_start_.GetFrame(); + int track_movement = event->GetTrack().index() - drag_start_.GetTrack().index(); + + // If snapping is enabled, check for snap points + if (Core::instance()->snapping()) { + SnapPoint(snap_points_, &time_movement); + } + + time_movement = ValidateFrameMovement(time_movement, parent()->ghost_items_); + track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); + + rational earliest_ghost = RATIONAL_MAX; + + // Move ghosts to the mouse cursor + foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { + ghost->SetInAdjustment(time_movement); + ghost->SetOutAdjustment(time_movement); + ghost->SetTrackAdjustment(track_movement); + + TrackReference adjusted_track = ghost->GetAdjustedTrack(); + ghost->SetYCoords(parent()->GetTrackY(adjusted_track), parent()->GetTrackHeight(adjusted_track)); + + earliest_ghost = qMin(earliest_ghost, ghost->GetAdjustedIn()); + } + + // Generate tooltip (showing earliest in point of imported clip) + int64_t earliest_timestamp = Timecode::time_to_timestamp(earliest_ghost, parent()->timebase()); + QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, + parent()->timebase(), + Timecode::CurrentDisplay()); + + // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way + // of the cursor) + QToolTip::hideText(); + QToolTip::showText(QCursor::pos(), + tooltip_text, + parent()); } - time_movement = ValidateFrameMovement(time_movement, parent()->ghost_items_); - track_movement = ValidateTrackMovement(track_movement, parent()->ghost_items_); - - rational earliest_ghost = RATIONAL_MAX; - - // Move ghosts to the mouse cursor - foreach (TimelineViewGhostItem* ghost, parent()->ghost_items_) { - ghost->SetInAdjustment(time_movement); - ghost->SetOutAdjustment(time_movement); - ghost->SetTrackAdjustment(track_movement); - - TrackReference adjusted_track = ghost->GetAdjustedTrack(); - ghost->SetYCoords(parent()->GetTrackY(adjusted_track), parent()->GetTrackHeight(adjusted_track)); - - earliest_ghost = qMin(earliest_ghost, ghost->GetAdjustedIn()); - } - - // Generate tooltip (showing earliest in point of imported clip) - int64_t earliest_timestamp = Timecode::time_to_timestamp(earliest_ghost, parent()->timebase()); - QString tooltip_text = Timecode::timestamp_to_timecode(earliest_timestamp, - parent()->timebase(), - Timecode::CurrentDisplay()); - - // Force tooltip to update (otherwise the tooltip won't move as written in the documentation, and could get in the way - // of the cursor) - QToolTip::hideText(); - QToolTip::showText(QCursor::pos(), - tooltip_text, - parent()); - event->accept(); } else { event->ignore(); @@ -204,8 +162,9 @@ void TimelineWidget::ImportTool::DragMove(TimelineViewMouseEvent *event) void TimelineWidget::ImportTool::DragLeave(QDragLeaveEvent* event) { - if (parent()->HasGhosts()) { + if (!dragged_footage_.isEmpty()) { parent()->ClearGhosts(); + dragged_footage_.clear(); event->accept(); } else { @@ -215,75 +174,144 @@ void TimelineWidget::ImportTool::DragLeave(QDragLeaveEvent* event) void TimelineWidget::ImportTool::DragDrop(TimelineViewMouseEvent *event) { - if (parent()->HasGhosts()) { + if (!dragged_footage_.isEmpty()) { + QUndoCommand* command = new QUndoCommand(); - NodeGraph* dst_graph = static_cast(parent()->GetConnectedNode()->parent()); - QVector block_items(parent()->ghost_items_.size()); + NodeGraph* dst_graph; + ViewerOutput* viewer_node; + Sequence* open_sequence = nullptr; - for (int i=0;ighost_items_.size();i++) { - TimelineViewGhostItem* ghost = parent()->ghost_items_.at(i); + if (parent()->GetConnectedNode()) { + viewer_node = parent()->GetConnectedNode(); + dst_graph = static_cast(parent()->GetConnectedNode()->parent()); + } else { + Project* active_project = Core::instance()->GetActiveProject(); - StreamPtr footage_stream = ghost->data(TimelineViewGhostItem::kAttachedFootage).value(); + if (active_project) { + SequencePtr new_sequence = Core::instance()->CreateNewSequenceForProject(active_project); - ClipBlock* clip = new ClipBlock(); - clip->set_length_and_media_out(ghost->Length()); - clip->set_block_name(footage_stream->footage()->name()); - new NodeAddCommand(dst_graph, clip, command); + new_sequence->set_default_parameters(); - switch (footage_stream->type()) { - case Stream::kVideo: - case Stream::kImage: - { - VideoInput* video_input = new VideoInput(); - video_input->SetFootage(footage_stream); - new NodeAddCommand(dst_graph, video_input, command); - new NodeEdgeAddCommand(video_input->output(), clip->texture_input(), command); + bool found_video_params = false; + bool found_audio_params = false; - TransformDistort* transform = new TransformDistort(); - new NodeAddCommand(dst_graph, transform, command); - new NodeEdgeAddCommand(transform->output(), video_input->matrix_input(), command); + foreach (Footage* f, dragged_footage_) { + foreach (StreamPtr s, f->streams()) { + if (!found_video_params && s->type() == Stream::kVideo) { - //OpacityNode* opacity = new OpacityNode(); - //NodeParam::ConnectEdge(opacity->texture_output(), clip->texture_input()); - //NodeParam::ConnectEdge(media->texture_output(), opacity->texture_input()); - break; - } - case Stream::kAudio: - { - AudioInput* audio_input = new AudioInput(); - audio_input->SetFootage(footage_stream); - new NodeAddCommand(dst_graph, audio_input, command); + VideoStream* vs = static_cast(s.get()); - VolumeNode* volume_node = new VolumeNode(); - new NodeAddCommand(dst_graph, volume_node, command); + if (vs->frame_rate() != 0) { + new_sequence->set_video_params(VideoParams(vs->width(), vs->height(), vs->frame_rate().flipped())); + found_video_params = true; + } - new NodeEdgeAddCommand(audio_input->output(), volume_node->samples_input(), command); - new NodeEdgeAddCommand(volume_node->output(), clip->texture_input(), command); - break; - } - default: - break; - } + } else if (!found_audio_params && s->type() == Stream::kAudio) { - if (event->GetModifiers() & Qt::ControlModifier) { - //emit parent()->RequestInsertBlockAtTime(clip, ghost->GetAdjustedIn()); + AudioStream* as = static_cast(s.get()); + new_sequence->set_audio_params(AudioParams(as->sample_rate(), as->channel_layout())); + found_audio_params = true; + + } + + if (found_video_params && found_audio_params) { + break; + } + } + + if (found_video_params && found_audio_params) { + break; + } + } + + new_sequence->add_default_nodes(); + + new ProjectViewModel::AddItemCommand(Core::instance()->GetActiveProjectModel(), + Core::instance()->GetSelectedFolderInActiveProject(), + new_sequence, + command); + + FootageToGhosts(0, dragged_footage_, new_sequence->video_params().time_base(), 0); + + dst_graph = new_sequence.get(); + viewer_node = new_sequence->viewer_output(); + + // Set this as the sequence to open + open_sequence = new_sequence.get(); } else { - new TrackPlaceBlockCommand(parent()->GetConnectedNode()->track_list(ghost->GetAdjustedTrack().type()), - ghost->GetAdjustedTrack().index(), - clip, - ghost->GetAdjustedIn(), - command); + dst_graph = nullptr; } + } - block_items.replace(i, clip); + if (dst_graph) { - // Link any clips so far that share the same Footage with this one - for (int j=0;jghost_items_.at(j)->data(TimelineViewGhostItem::kAttachedFootage).value(); + QVector block_items(parent()->ghost_items_.size()); - if (footage_compare->footage() == footage_stream->footage()) { - Block::Link(block_items.at(j), clip); + for (int i=0;ighost_items_.size();i++) { + TimelineViewGhostItem* ghost = parent()->ghost_items_.at(i); + + StreamPtr footage_stream = ghost->data(TimelineViewGhostItem::kAttachedFootage).value(); + + ClipBlock* clip = new ClipBlock(); + clip->set_length_and_media_out(ghost->Length()); + clip->set_block_name(footage_stream->footage()->name()); + new NodeAddCommand(dst_graph, clip, command); + + switch (footage_stream->type()) { + case Stream::kVideo: + case Stream::kImage: + { + VideoInput* video_input = new VideoInput(); + video_input->SetFootage(footage_stream); + new NodeAddCommand(dst_graph, video_input, command); + new NodeEdgeAddCommand(video_input->output(), clip->texture_input(), command); + + TransformDistort* transform = new TransformDistort(); + new NodeAddCommand(dst_graph, transform, command); + new NodeEdgeAddCommand(transform->output(), video_input->matrix_input(), command); + + //OpacityNode* opacity = new OpacityNode(); + //NodeParam::ConnectEdge(opacity->texture_output(), clip->texture_input()); + //NodeParam::ConnectEdge(media->texture_output(), opacity->texture_input()); + break; + } + case Stream::kAudio: + { + AudioInput* audio_input = new AudioInput(); + audio_input->SetFootage(footage_stream); + new NodeAddCommand(dst_graph, audio_input, command); + + VolumeNode* volume_node = new VolumeNode(); + new NodeAddCommand(dst_graph, volume_node, command); + + new NodeEdgeAddCommand(audio_input->output(), volume_node->samples_input(), command); + new NodeEdgeAddCommand(volume_node->output(), clip->texture_input(), command); + break; + } + default: + break; + } + + if (event->GetModifiers() & Qt::ControlModifier) { + //emit parent()->RequestInsertBlockAtTime(clip, ghost->GetAdjustedIn()); + } else { + new TrackPlaceBlockCommand(viewer_node->track_list(ghost->GetAdjustedTrack().type()), + ghost->GetAdjustedTrack().index(), + clip, + ghost->GetAdjustedIn(), + command); + } + + block_items.replace(i, clip); + + // Link any clips so far that share the same Footage with this one + for (int j=0;jghost_items_.at(j)->data(TimelineViewGhostItem::kAttachedFootage).value(); + + if (footage_compare->footage() == footage_stream->footage()) { + Block::Link(block_items.at(j), clip); + } } } } @@ -291,9 +319,73 @@ void TimelineWidget::ImportTool::DragDrop(TimelineViewMouseEvent *event) Core::instance()->undo_stack()->pushIfHasChildren(command); parent()->ClearGhosts(); + dragged_footage_.clear(); + + if (open_sequence) { + Sequence::Open(open_sequence); + } event->accept(); } else { event->ignore(); } } + +void TimelineWidget::ImportTool::FootageToGhosts(rational ghost_start, const QList &footage_list, const rational& dest_tb, const int& track_start) +{ + foreach (Footage* footage, footage_list) { + + // Each stream is offset by one track per track "type", we keep track of them in this vector + QVector track_offsets(Timeline::kTrackTypeCount); + track_offsets.fill(track_start); + + rational footage_duration; + + // Loop through all streams in footage + foreach (StreamPtr stream, footage->streams()) { + Timeline::TrackType track_type = TrackTypeFromStreamType(stream->type()); + + // Check if this stream has a compatible TrackList + if (track_type == Timeline::kTrackTypeNone || !stream->enabled()) { + continue; + } + + TimelineViewGhostItem* ghost = new TimelineViewGhostItem(); + + if (stream->type() == Stream::kImage) { + // Stream is essentially length-less - use config's default image length + footage_duration = Config::Current()["DefaultStillLength"].value(); + } else { + // Use duration from file + int64_t stream_duration = stream->duration(); + + // Rescale to timeline timebase + stream_duration = qCeil(static_cast(stream_duration) * stream->timebase().toDouble() / dest_tb.toDouble()); + + // Convert to rational time + footage_duration = rational(dest_tb.numerator() * stream_duration, + dest_tb.denominator()); + } + + ghost->SetIn(ghost_start); + ghost->SetOut(ghost_start + footage_duration); + ghost->SetTrack(TrackReference(track_type, track_offsets.at(track_type))); + + // Increment track count for this track type + track_offsets[track_type]++; + + snap_points_.append(ghost->In()); + snap_points_.append(ghost->Out()); + + ghost->setData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(stream)); + ghost->SetMode(Timeline::kMove); + + parent()->AddGhost(ghost); + + } + + // Stack each ghost one after the other + ghost_start += footage_duration; + + } +}