/*** 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 . ***/ #include "sequence.h" #include #include "timelinefunctions.h" #include "panels/panels.h" #include "global/clipboard.h" #include "global/config.h" #include "global/debug.h" Sequence::Sequence() : playhead(0), using_workarea(false), workarea_in(0), workarea_out(0), wrapper_sequence(false) { AddTrack(olive::kTypeVideo); AddTrack(olive::kTypeAudio); } SequencePtr Sequence::copy() { SequencePtr s = std::make_shared(); s->name_ = tr("%1 (copy)").arg(name_); s->width_ = width_; s->height_ = height_; s->frame_rate_ = frame_rate_; s->audio_frequency_ = audio_frequency_; s->audio_layout_ = audio_layout_; /* FIXME // deep copy all of the sequence's clips for (int i=0;itrack_lists_[i] = track_lists_.at(i)->copy(s.get()); } */ // copy all of the sequence's markers s->markers = markers; return s; } void Sequence::Save(QXmlStreamWriter &stream) { // Provide unique IDs for each Clip QVector all_clips = GetAllClips(); for (int i=0;iload_id = i; } stream.writeStartElement("sequence"); stream.writeAttribute("id", QString::number(save_id)); stream.writeAttribute("name", name_); stream.writeAttribute("width", QString::number(width_)); stream.writeAttribute("height", QString::number(height_)); stream.writeAttribute("framerate", QString::number(frame_rate_, 'f', 10)); stream.writeAttribute("afreq", QString::number(audio_frequency_)); stream.writeAttribute("alayout", QString::number(audio_layout_)); if (this == Timeline::GetTopSequence().get()) { stream.writeAttribute("open", "1"); } stream.writeAttribute("workarea", QString::number(using_workarea)); stream.writeAttribute("workareaIn", QString::number(workarea_in)); stream.writeAttribute("workareaOut", QString::number(workarea_out)); QVector transition_save_cache; QVector transition_clip_save_cache; /* FIXME for (int j=0;jSave(stream); } */ for (int j=0;jGetEndFrame(), end_frame); } return end_frame; } QVector Sequence::GetAllClips() { QVector all_clips; for (int j=0;jGetAllClips()); } return all_clips; } QVector Sequence::GetTrackList(olive::TrackType type) { QVector tracks; for (int i=0;itype() == type) { tracks.append(tracks_.at(i)); } } return tracks; } void Sequence::Close() { QVector all_clips = GetAllClips(); for (int i=0;iClose(true); } } void Sequence::RefreshClipsUsingMedia(Media *m) { QVector all_clips = GetAllClips(); for (int i=0;imedia() == m) { c->Close(true); c->refresh(); } } } QVector Sequence::SelectedClips(bool containing) { QVector selected_clips; for (int j=0;jGetSelectedClips(containing)); } return selected_clips; } void Sequence::AddClipsFromGhosts(ComboAction* ca, const QVector& ghosts) { // add clips long earliest_point = LONG_MAX; QVector added_clips; for (int i=0;i(g.track->Sibling(g.track_movement)); c->set_media(g.media, g.media_stream); c->set_timeline_in(g.in); c->set_timeline_out(g.out); c->set_clip_in(g.clip_in); if (c->media()->get_type() == MEDIA_TYPE_FOOTAGE) { Footage* m = c->media()->to_footage(); if (m->video_tracks.size() == 0) { // audio only (greenish) c->set_color(128, 192, 128); } else if (m->audio_tracks.size() == 0) { // video only (orangeish) c->set_color(192, 160, 128); } else { // video and audio (blueish) c->set_color(128, 128, 192); } c->set_name(m->name); } else if (c->media()->get_type() == MEDIA_TYPE_SEQUENCE) { // sequence (red?ish?) c->set_color(192, 128, 128); c->set_name(c->media()->to_sequence()->name()); } c->refresh(); added_clips.append(c); } ca->append(new AddClipCommand(added_clips)); // link clips from the same media for (int i=0;imedia() == cc->media()) { c->linked.append(cc.get()); } } if (olive::config.add_default_effects_to_clips) { if (c->type() == olive::kTypeVideo) { // add default video effects c->effects.append(olive::node_library[kTransformEffect]->Create(c.get())); } else if (c->type() == olive::kTypeAudio) { // add default audio effects c->effects.append(olive::node_library[kVolumeEffect]->Create(c.get())); c->effects.append(olive::node_library[kPanEffect]->Create(c.get())); } } } if (olive::config.enable_seek_to_import) { panel_sequence_viewer->seek(earliest_point); } olive::timeline::snapped = false; } void Sequence::MoveClip(Clip *c, ComboAction *ca, long iin, long iout, long iclip_in, Track *itrack, bool verify_transitions, bool relative) { ClipPtr clip_ptr = c->track()->GetClipObjectFromRawPtr(c); ca->append(new MoveClipAction(clip_ptr, iin, iout, iclip_in, itrack, relative)); if (verify_transitions) { // if this is a shared transition, and the corresponding clip will be moved away somehow if (c->opening_transition != nullptr && c->opening_transition->secondary_clip != nullptr && c->opening_transition->secondary_clip->timeline_out() != iin) { // separate transition ca->append(new SetPointer(reinterpret_cast(&c->opening_transition->secondary_clip), nullptr)); ca->append(new AddTransitionCommand(nullptr, c->opening_transition->secondary_clip, c->opening_transition, kInvalidNode, 0)); } if (c->closing_transition != nullptr && c->closing_transition->secondary_clip != nullptr && c->closing_transition->parent_clip->timeline_in() != iout) { // separate transition ca->append(new SetPointer(reinterpret_cast(&c->closing_transition->secondary_clip), nullptr)); ca->append(new AddTransitionCommand(nullptr, c, c->closing_transition, kInvalidNode, 0)); } } } void Sequence::EditToPoint(bool in, bool ripple) { QVector all_clips = GetAllClips(); if (all_clips.size() > 0) { long sequence_end = 0; bool playhead_falls_on_in = false; bool playhead_falls_on_out = false; long next_cut = LONG_MAX; long prev_cut = 0; // find closest in point to playhead for (int i=0;itimeline_out(), sequence_end); if (c->timeline_in() == playhead) playhead_falls_on_in = true; if (c->timeline_out() == playhead) playhead_falls_on_out = true; if (c->timeline_in() > playhead) next_cut = qMin(c->timeline_in(), next_cut); if (c->timeline_out() > playhead) next_cut = qMin(c->timeline_out(), next_cut); if (c->timeline_in() < playhead) prev_cut = qMax(c->timeline_in(), prev_cut); if (c->timeline_out() < playhead) prev_cut = qMax(c->timeline_out(), prev_cut); } next_cut = qMin(sequence_end, next_cut); QVector areas; ComboAction* ca = new ComboAction(); bool push_undo = true; long seek = playhead; if ((in && (playhead_falls_on_out || (playhead_falls_on_in && playhead == 0))) || (!in && (playhead_falls_on_in || (playhead_falls_on_out && playhead == sequence_end)))) { // one frame mode if (ripple) { // set up deletion areas based on track count long in_point = playhead; if (!in) { in_point--; seek--; } if (in_point >= 0) { for (int j=0;jseek(seek); } } else { delete ca; } } else { panel_sequence_viewer->seek(0); } } bool Sequence::SnapPoint(long *l, double zoom, bool use_playhead, bool use_markers, bool use_workarea) { olive::timeline::snapped = false; if (olive::timeline::snapping) { if (use_playhead && !panel_sequence_viewer->playing) { // snap to playhead if (olive::timeline::SnapToPoint(playhead, l, zoom)) return true; } // snap to marker if (use_markers) { for (int i=0;i all_clips = GetAllClips(); for (int i=0;itimeline_in(), l, zoom)) { return true; } else if (olive::timeline::SnapToPoint(c->timeline_out(), l, zoom)) { return true; } else if (c->opening_transition != nullptr && olive::timeline::SnapToPoint(c->timeline_in() + c->opening_transition->get_true_length(), l, zoom)) { return true; } else if (c->closing_transition != nullptr && olive::timeline::SnapToPoint(c->timeline_out() - c->closing_transition->get_true_length(), l, zoom)) { return true; } else { // try to snap to clip markers for (int j=0;jget_markers().size();j++) { if (olive::timeline::SnapToPoint(c->get_markers().at(j).frame + c->timeline_in() - c->clip_in(), l, zoom)) { return true; } } } } } return false; } void Sequence::DeleteInToOut(bool ripple) { if (using_workarea) { QVector areas_to_delete; for (int j=0;jappend(new SetTimelineInOutCommand(this, false, 0, 0)); olive::undo_stack.push(ca); update_ui(true); } } void Sequence::DeleteClipsUsingMedia(const QVector& media) { QVector all_clips = GetAllClips(); ComboAction* ca = new ComboAction(); bool deleted = false; for (int j=0;jmedia() == media.at(j)) { ca->append(new DeleteClipAction(c)); deleted = true; } } } if (deleted) { olive::undo_stack.push(ca); update_ui(true); } else { delete ca; } } void Sequence::Ripple(ComboAction *ca, long point, long length, const QVector &ignore) { ca->append(new RippleAction(this, point, length, ignore)); } void Sequence::ChangeTrackHeightsRelatively(int diff) { for (int j=0;jset_height(t->height() + diff); } } void Sequence::ToggleLinksOnSelected() { QVector selected_clips = SelectedClips(); bool link = true; QVector link_clips; for (int i=0;ilinked.size() > 0) { link = false; // prioritize unlinking for (int j=0;jlinked.size();j++) { // add links to the command if (!link_clips.contains(c->linked.at(j))) { link_clips.append(c->linked.at(j)); } } } } if (!link_clips.isEmpty()) { olive::undo_stack.push(new LinkCommand(link_clips, link)); } } void Sequence::Split() { ComboAction* ca = new ComboAction(); bool split_occurred = false; // See if there are any selected clips at the current playhead to split QVector selected_clips = SelectedClips(true); if (selected_clips.size() > 0) { // see if whole clips are selected QVector pre_clips; QVector post_clips; for (int i=0;iappend(new AddClipCommand(post_clips)); } } // If we weren't able to split any selected clips above, see if there are arbitrary selections to split if (!split_occurred) { Track* track; foreach (track, tracks_) { QVector track_selections = track->Selections(); QVector split_positions; for (int j=0;jClipCount();i++) { Clip* c = track->GetClip(i).get(); if (SplitClipAtPositions(ca, c, split_positions, false)) { split_occurred = true; } } } } // if nothing was selected or no selections fell within playhead, simply split at playhead if (!split_occurred) { split_occurred = SplitAllClipsAtPoint(ca, playhead); } if (split_occurred) { olive::undo_stack.push(ca); update_ui(true); } else { delete ca; } } void Sequence::DeleteAreas(ComboAction* ca, QVector areas, bool deselect_areas, bool ripple) { if (areas.isEmpty()) { return; } Selection::Tidy(areas); panel_graph_editor->set_row(nullptr); panel_effect_controls->Clear(true); QVector pre_clips; QVector post_clips; QVector all_clips = GetAllClips(); for (int i=0;itrack() == s.track() && !c->undeletable) { if (s.ContainsTransition(c, kTransitionOpening)) { // delete opening transition ca->append(new DeleteTransitionCommand(c->opening_transition)); } else if (s.ContainsTransition(c, kTransitionClosing)) { // delete closing transition ca->append(new DeleteTransitionCommand(c->closing_transition)); } else if (c->timeline_in() >= s.in() && c->timeline_out() <= s.out()) { // clips falls entirely within deletion area ca->append(new DeleteClipAction(c)); } else if (c->timeline_in() < s.in() && c->timeline_out() > s.out()) { // middle of clip is within deletion area // duplicate clip ClipPtr post = SplitClip(ca, true, c, s.in(), s.out()); pre_clips.append(c); post_clips.append(post); } else if (c->timeline_in() < s.in() && c->timeline_out() > s.in()) { // only out point is in deletion area MoveClip(c, ca, c->timeline_in(), s.in(), c->clip_in(), c->track()); if (c->closing_transition != nullptr) { if (s.in() < c->timeline_out() - c->closing_transition->get_true_length()) { ca->append(new DeleteTransitionCommand(c->closing_transition)); } else { ca->append(new ModifyTransitionCommand(c->closing_transition, c->closing_transition->get_true_length() - (c->timeline_out() - s.in()))); } } } else if (c->timeline_in() < s.out() && c->timeline_out() > s.out()) { // only in point is in deletion area MoveClip(c, ca, s.out(), c->timeline_out(), c->clip_in() + (s.out() - c->timeline_in()), c->track()); if (c->opening_transition != nullptr) { if (s.out() > c->timeline_in() + c->opening_transition->get_true_length()) { ca->append(new DeleteTransitionCommand(c->opening_transition)); } else { ca->append(new ModifyTransitionCommand(c->opening_transition, c->opening_transition->get_true_length() - (s.out() - c->timeline_in()))); } } } } } } // Get ripple point and ripple length long minimum_in = LONG_MAX; long minimum_length = LONG_MAX; for (int i=0;i area_copy = areas; for (int i=0;iDeselectArea(s.in(), s.out()); } } if (ripple) { RippleDeleteArea(ca, minimum_in, minimum_length); } olive::timeline::RelinkClips(pre_clips, post_clips); ca->append(new AddClipCommand(post_clips)); } bool Sequence::SplitAllClipsAtPoint(ComboAction *ca, long point) { bool split = false; QVector all_clips = GetAllClips(); for (int j=0;jIsActiveAt(point)) { SplitClipAtPositions(ca, c, {point}, true); split = true; } } return split; } bool Sequence::SplitClipAtPositions(ComboAction *ca, Clip* clip, QVector positions, bool also_split_links) { // Add the clip and each of its links to the pre_splits array bool split_occurred = false; QVector pre_splits; pre_splits.append(clip); if (also_split_links) { for (int i=0;ilinked.size();i++) { pre_splits.append(clip->linked.at(i)); } } std::sort(positions.begin(), positions.end()); // Remove any duplicate positions for (int i=1;i > post_splits(positions.size()); for (int i=positions.size()-1;i>=0;i--) { post_splits[i].resize(pre_splits.size()); for (int j=0;jset_timeline_out(qMin(post_splits[i][j]->timeline_out(), positions.at(i+1))); } } } } for (int i=0;iappend(new AddClipCommand(post_splits[i])); } return split_occurred; } void Sequence::RippleDeleteEmptySpace(ComboAction* ca, Track* track, long point) { QVector track_clips = track->GetAllClips(); long ripple_start = LONG_MAX; long ripple_end = LONG_MAX; for (int i=0;itimeline_in() <= point && c->timeline_out() >= point) { // This point is not actually empty, so there's nothing to do here return; } if (c->timeline_out() <= point) { ripple_start = qMin(c->timeline_out(), ripple_start); } else if (c->timeline_in() >= point) { ripple_end = qMin(c->timeline_in(), ripple_end); } } // We now know the maximum ripple we could do to clear this empty space, but we need to ensure it won't cause // overlaps of clips in other tracks if (ripple_start == ripple_end) { return; } RippleDeleteArea(ca, point, ripple_start - ripple_end); } void Sequence::RippleDeleteArea(ComboAction* ca, long ripple_point, long ripple_length) { for (int j=0;j track_clips = t->GetAllClips(); // Find the in point of the clip directly after the point for (int k=0;ktimeline_in() >= ripple_point) { first_in_point_after_point = qMin(first_in_point_after_point, c->timeline_in()); } } // Ensure we found a valid in point before proceeding if (first_in_point_after_point != LONG_MAX) { // Find the out point of the clip directly before the clip found above for (int k=0;ktimeline_out() <= first_in_point_after_point) { out_point_just_before_first_in_point = qMax(out_point_just_before_first_in_point, c->timeline_out()); } } long ripple_test = first_in_point_after_point - out_point_just_before_first_in_point + ripple_length; if (ripple_test < 0) { ripple_length -= ripple_test; } } } if (ripple_length != 0) { Ripple(ca, ripple_point, ripple_length); } } OldEffectNode *Sequence::GetSelectedGizmo() { OldEffectNode* gizmo_ptr = nullptr; QVector clips = GetAllClips(); for (int i=0;iIsActiveAt(playhead) && c->IsSelected()) { // This clip is selected and currently active - we'll use this for gizmos if (!c->effects.isEmpty()) { // find which effect has gizmos selected, or default to the first gizmo effect we find if there is // none selected for (int j=0;jeffects.size();j++) { OldEffectNode* e = c->effects.at(j).get(); // retrieve gizmo data from effect if (e->are_gizmos_enabled()) { if (gizmo_ptr == nullptr) { gizmo_ptr = e; } if (panel_effect_controls->IsEffectSelected(e)) { gizmo_ptr = e; break; } } } } if (gizmo_ptr != nullptr) { break; } } } return gizmo_ptr; } void Sequence::SelectAll() { for (int i=0;iSelectAll(); } } void Sequence::SelectAtPlayhead() { for (int i=0;iSelectAtPoint(playhead); } } void Sequence::ClearSelections() { for (int i=0;iClearSelections(); } } void Sequence::AddSelectionsToClipboard(bool delete_originals) { olive::clipboard.Clear(); olive::clipboard.SetType(Clipboard::CLIPBOARD_TYPE_CLIP); QVector original_clips; QVector copied_clips; long min_in = LONG_MAX; QVector selections = Selections(); for (int i=0;i track_clips = s.track()->GetAllClips(); for (int j=0;jtimeline_out() < s.in() || c->timeline_in() > s.out())) { // If so, we'll be copying this clip original_clips.append(c); ClipPtr copy = c->copy(nullptr); // If we only copied part of this clip, adjust the copy so it's only that part of the clip if (copy->timeline_in() < s.in()) { copy->set_clip_in(copy->clip_in() + (s.in() - copy->timeline_in())); copy->set_timeline_in(s.in()); } if (copy->timeline_out() > s.out()) { copy->set_timeline_out(s.out()); } // Store the minimum in point as all copies will be stored offset from 0 min_in = qMin(min_in, s.in()); copied_clips.append(copy); olive::clipboard.Append(copy); } } } // Determine whether we actually copied anything if (min_in < LONG_MAX) { // Offset all copied clips to 0 for (int i=0;iset_timeline_in(copy->timeline_in() - min_in); copy->set_timeline_out(copy->timeline_out() - min_in); } // Relink the copied clips with each other olive::timeline::RelinkClips(original_clips, copied_clips); // If we're deleting the originals (i.e. cutting), delete them now if (delete_originals) { ComboAction* ca = new ComboAction(); DeleteAreas(ca, selections, true); olive::undo_stack.push(ca); } } } QVector Sequence::Selections() { QVector selections; for (int i=0;iSelections()); } return selections; } void Sequence::SetSelections(const QVector &selections) { ClearSelections(); for (int i=0;iSelectArea(s.in(), s.out()); } } void Sequence::TidySelections() { QVector selections = Selections(); Selection::Tidy(selections); SetSelections(selections); } Track *Sequence::PreviousTrack(Track *t) { Track* previous_track = nullptr; // Loop through tracks for (int i=0;itype() == t->type()) { // Otherwise, we'll keep "track" of it previous_track = t; } } return previous_track; } Track *Sequence::NextTrack(Track *t) { if (LastTrack(t->type()) == t) { return AddTrack(t->type()); } else { Track* next_track = nullptr; for (int i=tracks_.size()-1;i>=0;i--) { if (tracks_.at(i) == t) { break; } if (tracks_.at(i)->type() == t->type()) { next_track = tracks_.at(i); } } return next_track; } } Track *Sequence::SiblingTrack(Track *t, int diff) { if (diff == 0) { return t; } return TrackAt(t->type(), qMax(0, IndexOfTrack(t) + diff)); } int Sequence::IndexOfTrack(Track *t) { int counter = -1; for (int i=0;itype() == t->type()) { counter++; } if (tracks_.at(i) == t) { return counter; } } return -1; } Track *Sequence::FirstTrack(olive::TrackType type) { for (int i=0;itype() == type) { return tracks_.at(i); } } return nullptr; } Track *Sequence::LastTrack(olive::TrackType type) { for (int i=tracks_.size()-1;i>=0;i--) { if (tracks_.at(i)->type() == type) { return tracks_.at(i); } } return nullptr; } Track *Sequence::TrackAt(olive::TrackType type, int index) { int counter = -1; for (int i=0;itype() == type) { counter++; } if (counter == index) { return tracks_.at(i); } } Track* t; do { t = AddTrack(type); counter++; } while (index > counter); return t; } int Sequence::TrackCount(olive::TrackType type) { int counter = 0; for (int i=0;itype() == type) { counter++; } } return counter; } Track* Sequence::AddTrack(olive::TrackType type) { Track* track = new Track(this, type); tracks_.append(track); emit TrackCountChanged(); return track; } ClipPtr Sequence::SplitClip(ComboAction *ca, bool transitions, Clip* pre, long frame) { return SplitClip(ca, transitions, pre, frame, frame); } ClipPtr Sequence::SplitClip(ComboAction *ca, bool transitions, Clip* pre, long frame, long post_in) { if (pre == nullptr) { return nullptr; } if (pre->timeline_in() < frame && pre->timeline_out() > frame) { // duplicate clip without duplicating its transitions, we'll restore them later ClipPtr post = pre->copy(pre->track()); long new_clip_length = frame - pre->timeline_in(); post->set_timeline_in(post_in); post->set_clip_in(pre->clip_in() + (post->timeline_in() - pre->timeline_in())); MoveClip(pre, ca, pre->timeline_in(), frame, pre->clip_in(), pre->track(), false); if (transitions) { // check if this clip has a closing transition if (pre->closing_transition != nullptr) { // if so, move closing transition to the post clip post->closing_transition = pre->closing_transition; // and set the original clip's closing transition to nothing ca->append(new SetPointer(reinterpret_cast(&pre->closing_transition), nullptr)); // and set the transition's reference to the post clip if (post->closing_transition->parent_clip == pre) { ca->append(new SetPointer(reinterpret_cast(&post->closing_transition->parent_clip), post.get())); } if (post->closing_transition->secondary_clip == pre) { ca->append(new SetPointer(reinterpret_cast(&post->closing_transition->secondary_clip), post.get())); } // and make sure it's at the correct size to the closing clip if (post->closing_transition != nullptr && post->closing_transition->get_true_length() > post->length()) { ca->append(new ModifyTransitionCommand(post->closing_transition, post->length())); post->closing_transition->set_length(post->length()); } } // we're keeping the opening clip, so ensure that's a correct size too if (pre->opening_transition != nullptr && pre->opening_transition->get_true_length() > new_clip_length) { ca->append(new ModifyTransitionCommand(pre->opening_transition, new_clip_length)); } } return post; } else if (frame == pre->timeline_in() && pre->opening_transition != nullptr && pre->opening_transition->secondary_clip != nullptr) { // special case for shared transitions to split it into two // set transition to single-clip mode ca->append(new SetPointer(reinterpret_cast(&pre->opening_transition->secondary_clip), nullptr)); // clone transition for other clip ca->append(new AddTransitionCommand(nullptr, pre->opening_transition->secondary_clip, pre->opening_transition, kInvalidNode, 0) ); } return nullptr; } bool Sequence::SplitSelection(ComboAction *ca, QVector selections) { bool ret = false; QVector all_clips = GetAllClips(); for (int i=0;i points; for (int j=0;jtrack()) { if (c->timeline_in() < s.in() && c->timeline_out() > s.in()) { points.append(s.in()); } if (c->timeline_in() < s.out() && c->timeline_out() > s.out()) { points.append(s.out()); } } } if (SplitClipAtPositions(ca, c, points, false)) { ret = true; } } return ret; }