engine: timeline panel core edit commands migrate to the facade (part 1)
- new batch primitives: split_clips (link-preserving, single undo command), delete_clips (gap replace + optional ripple with explicit region support), ripple_delete_range, marker_add_ex with color - razor/split-at-playhead, clip delete, ripple-to-point, track delete, and the non-dialog marker path now issue facade commands instead of the app's own undo command classes - batch operations deliberately produce one undo command per user action (deleting twenty clips is one entry, not twenty); selection and transition removal stay UI-side as documented leftovers
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
#include "common/current.h"
|
#include "common/current.h"
|
||||||
#include "dialog/markerproperties/markerpropertiesdialog.h"
|
#include "dialog/markerproperties/markerpropertiesdialog.h"
|
||||||
#include "node/project/sequence/sequence.h"
|
#include "node/project/sequence/sequence.h"
|
||||||
|
#include "oakengine/timeline.h"
|
||||||
#include "timeline/timelineundoworkarea.h"
|
#include "timeline/timelineundoworkarea.h"
|
||||||
#include "widget/timeruler/timeruler.h"
|
#include "widget/timeruler/timeruler.h"
|
||||||
|
|
||||||
@@ -751,17 +752,35 @@ void TimeBasedWidget::set_marker()
|
|||||||
color, TimeRange(get_connected_node()->get_playhead(),
|
color, TimeRange(get_connected_node()->get_playhead(),
|
||||||
get_connected_node()->get_playhead()));
|
get_connected_node()->get_playhead()));
|
||||||
|
|
||||||
|
bool edited_in_dialog = false;
|
||||||
if (OAK_CONFIG("SetNameWithMarker").toBool()) {
|
if (OAK_CONFIG("SetNameWithMarker").toBool()) {
|
||||||
MarkerPropertiesDialog mpd({ marker }, timebase(), this);
|
MarkerPropertiesDialog mpd({ marker }, timebase(), this);
|
||||||
if (mpd.exec() != QDialog::Accepted) {
|
if (mpd.exec() != QDialog::Accepted) {
|
||||||
delete marker;
|
delete marker;
|
||||||
marker = nullptr;
|
marker = nullptr;
|
||||||
|
} else {
|
||||||
|
edited_in_dialog = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (marker) {
|
if (marker) {
|
||||||
Core::instance()->undo_stack()->push(
|
if (edited_in_dialog) {
|
||||||
new MarkerAddCommand(markers, marker), tr("Added Marker"));
|
// The dialog pushed undo commands referencing this exact
|
||||||
|
// marker object, so it must be the one added to the list.
|
||||||
|
Core::instance()->undo_stack()->push(
|
||||||
|
new MarkerAddCommand(markers, marker), tr("Added Marker"));
|
||||||
|
} else {
|
||||||
|
// Pristine marker: add through the liboakengine C ABI
|
||||||
|
// facade (one undoable command) and drop the temporary.
|
||||||
|
oakengine_sequence_marker_add_ex(
|
||||||
|
reinterpret_cast<OakEngineSequence *>(
|
||||||
|
get_connected_node()),
|
||||||
|
Timecode::time_to_timestamp(marker->time().in(),
|
||||||
|
timebase(),
|
||||||
|
Timecode::k_round),
|
||||||
|
"", marker->color());
|
||||||
|
delete marker;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
#include "node/nodeundo.h"
|
#include "node/nodeundo.h"
|
||||||
#include "node/project/footage/footage.h"
|
#include "node/project/footage/footage.h"
|
||||||
#include "node/project/serializer/serializer.h"
|
#include "node/project/serializer/serializer.h"
|
||||||
|
#include "oakengine/timeline.h"
|
||||||
#include "render/audiowaveformcache.h"
|
#include "render/audiowaveformcache.h"
|
||||||
#include "task/project/import/import.h"
|
#include "task/project/import/import.h"
|
||||||
#include "timeline/timelineundogeneral.h"
|
#include "timeline/timelineundogeneral.h"
|
||||||
@@ -556,10 +557,20 @@ void TimelineWidget::split_at_playhead()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!blocks_to_split.isEmpty()) {
|
if (!blocks_to_split.isEmpty()) {
|
||||||
Core::instance()->undo_stack()->push(
|
// Split through the liboakengine C ABI facade: one undoable,
|
||||||
new BlockSplitPreservingLinksCommand(blocks_to_split,
|
// link-preserving command with the same semantics as the old
|
||||||
{ playhead_time }),
|
// app-side BlockSplitPreservingLinksCommand push.
|
||||||
tr("Split Clips At Playhead"));
|
QVector<OakEngineClip *> clips;
|
||||||
|
clips.reserve(blocks_to_split.size());
|
||||||
|
foreach (Block *b, blocks_to_split) {
|
||||||
|
clips.append(reinterpret_cast<OakEngineClip *>(
|
||||||
|
static_cast<ClipBlock *>(b)));
|
||||||
|
}
|
||||||
|
oakengine_sequence_split_clips(
|
||||||
|
reinterpret_cast<OakEngineSequence *>(sequence()), clips.data(),
|
||||||
|
clips.size(),
|
||||||
|
Timecode::time_to_timestamp(playhead_time, timebase(),
|
||||||
|
Timecode::k_round));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,32 +650,47 @@ void TimelineWidget::DeleteSelected(bool ripple)
|
|||||||
command->add_child(trc);
|
command->add_child(trc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace clips with gaps (effectively deleting them)
|
// Selection clearing and transition removal stay app-side (selection
|
||||||
replace_blocks_with_gaps(clips_to_delete, true, command, false);
|
// state and transition commands have no facade equivalent); the clip
|
||||||
|
// deletion core below goes through the facade and lands as one undoable
|
||||||
|
// command right after this one, keeping the undo order intact.
|
||||||
|
Core::instance()->undo_stack()->push(command, tr("Deleted Clips"));
|
||||||
|
|
||||||
// Insert ripple command now that it's all cleaned up gaps
|
// Delete the clips through the liboakengine C ABI facade (gap
|
||||||
TimelineRippleDeleteGapsAtRegionsCommand *ripple_command = nullptr;
|
// replacement + graph removal, optionally rippling the selected ranges
|
||||||
Rational new_playhead = RATIONAL_MAX;
|
// closed), same semantics as the old in-command children.
|
||||||
if (ripple) {
|
QVector<OakEngineClip *> facade_clips;
|
||||||
TimelineRippleDeleteGapsAtRegionsCommand::RangeList range_list;
|
facade_clips.reserve(clips_to_delete.size());
|
||||||
|
foreach (Block *b, clips_to_delete) {
|
||||||
foreach (Block *b, selected_list) {
|
facade_clips.append(
|
||||||
range_list.append({ b->track(), b->range() });
|
reinterpret_cast<OakEngineClip *>(static_cast<ClipBlock *>(b)));
|
||||||
new_playhead = qMin(new_playhead, b->in());
|
|
||||||
}
|
|
||||||
|
|
||||||
ripple_command = new TimelineRippleDeleteGapsAtRegionsCommand(
|
|
||||||
sequence(), range_list);
|
|
||||||
command->add_child(ripple_command);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Core::instance()->undo_stack()->push(command, tr("Deleted Clips"));
|
Rational new_playhead = RATIONAL_MAX;
|
||||||
|
QVector<int64_t> ripple_ranges;
|
||||||
|
if (ripple) {
|
||||||
|
foreach (Block *b, selected_list) {
|
||||||
|
ripple_ranges.append(int64_t(b->track()->type()));
|
||||||
|
ripple_ranges.append(b->track()->index());
|
||||||
|
ripple_ranges.append(Timecode::time_to_timestamp(
|
||||||
|
b->in(), timebase(), Timecode::k_round));
|
||||||
|
ripple_ranges.append(Timecode::time_to_timestamp(
|
||||||
|
b->out(), timebase(), Timecode::k_round));
|
||||||
|
new_playhead = qMin(new_playhead, b->in());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int rippled = 0;
|
||||||
|
oakengine_sequence_delete_clips(
|
||||||
|
reinterpret_cast<OakEngineSequence *>(sequence()),
|
||||||
|
facade_clips.data(), facade_clips.size(), ripple ? 1 : 0,
|
||||||
|
ripple ? ripple_ranges.constData() : nullptr,
|
||||||
|
ripple ? ripple_ranges.size() / 4 : 0, &rippled);
|
||||||
|
|
||||||
// Ensures any current drag operations are cancelled
|
// Ensures any current drag operations are cancelled
|
||||||
clear_ghosts();
|
clear_ghosts();
|
||||||
|
|
||||||
if (ripple_command && ripple_command->has_commands() &&
|
if (ripple && rippled && new_playhead != RATIONAL_MAX) {
|
||||||
new_playhead != RATIONAL_MAX) {
|
|
||||||
get_connected_node()->set_playhead(new_playhead);
|
get_connected_node()->set_playhead(new_playhead);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2535,10 +2561,14 @@ void TimelineWidget::ripple_to(Timeline::MovementMode mode)
|
|||||||
Rational in_ripple = qMin(closest_point_to_playhead, playhead_time);
|
Rational in_ripple = qMin(closest_point_to_playhead, playhead_time);
|
||||||
Rational out_ripple = qMax(closest_point_to_playhead, playhead_time);
|
Rational out_ripple = qMax(closest_point_to_playhead, playhead_time);
|
||||||
|
|
||||||
TimelineRippleRemoveAreaCommand *c =
|
// Ripple the region out through the liboakengine C ABI facade (one
|
||||||
new TimelineRippleRemoveAreaCommand(sequence(), in_ripple, out_ripple);
|
// undoable all-tracks ripple, same as the old
|
||||||
|
// TimelineRippleRemoveAreaCommand push).
|
||||||
Core::instance()->undo_stack()->push(c, tr("Rippled Clip(s) To Point"));
|
oakengine_sequence_ripple_delete_range(
|
||||||
|
reinterpret_cast<OakEngineSequence *>(sequence()),
|
||||||
|
Timecode::time_to_timestamp(in_ripple, timebase(), Timecode::k_round),
|
||||||
|
Timecode::time_to_timestamp(out_ripple, timebase(),
|
||||||
|
Timecode::k_round));
|
||||||
|
|
||||||
// If we rippled, ump to where new cut is if applicable
|
// If we rippled, ump to where new cut is if applicable
|
||||||
if (mode == Timeline::k_trim_in) {
|
if (mode == Timeline::k_trim_in) {
|
||||||
|
|||||||
@@ -21,8 +21,7 @@
|
|||||||
|
|
||||||
#include "razor.h"
|
#include "razor.h"
|
||||||
|
|
||||||
#include "node/nodeundo.h"
|
#include "oakengine/timeline.h"
|
||||||
#include "timeline/timelineundosplit.h"
|
|
||||||
#include "widget/timelinewidget/timelinewidget.h"
|
#include "widget/timelinewidget/timelinewidget.h"
|
||||||
|
|
||||||
namespace olive
|
namespace olive
|
||||||
@@ -94,10 +93,21 @@ void RazorTool::mouse_release(TimelineViewMouseEvent *event)
|
|||||||
split_tracks_.clear();
|
split_tracks_.clear();
|
||||||
|
|
||||||
if (!blocks_to_split.isEmpty()) {
|
if (!blocks_to_split.isEmpty()) {
|
||||||
Core::instance()->undo_stack()->push(
|
// Split through the liboakengine C ABI facade: one undoable,
|
||||||
new BlockSplitPreservingLinksCommand(blocks_to_split,
|
// link-preserving command with the same semantics as the old
|
||||||
{ split_time }),
|
// app-side BlockSplitPreservingLinksCommand push.
|
||||||
qApp->translate("RazorTool", "Split Clips"));
|
QVector<OakEngineClip *> clips;
|
||||||
|
clips.reserve(blocks_to_split.size());
|
||||||
|
foreach (Block *b, blocks_to_split) {
|
||||||
|
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(b)) {
|
||||||
|
clips.append(reinterpret_cast<OakEngineClip *>(clip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oakengine_sequence_split_clips(
|
||||||
|
reinterpret_cast<OakEngineSequence *>(parent()->sequence()),
|
||||||
|
clips.data(), clips.size(),
|
||||||
|
Timecode::time_to_timestamp(split_time, parent()->timebase(),
|
||||||
|
Timecode::k_round));
|
||||||
}
|
}
|
||||||
|
|
||||||
dragging_ = false;
|
dragging_ = false;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
#include "oakengine/timeline.h"
|
||||||
#include "timeline/timelineundogeneral.h"
|
#include "timeline/timelineundogeneral.h"
|
||||||
#include "ui/icons/icons.h"
|
#include "ui/icons/icons.h"
|
||||||
#include "widget/menu/menu.h"
|
#include "widget/menu/menu.h"
|
||||||
@@ -159,9 +160,11 @@ void TrackViewItem::show_context_menu(const QPoint &p)
|
|||||||
void TrackViewItem::delete_track()
|
void TrackViewItem::delete_track()
|
||||||
{
|
{
|
||||||
emit about_to_delete_track(track_);
|
emit about_to_delete_track(track_);
|
||||||
Core::instance()->undo_stack()->push(
|
// Through the liboakengine C ABI facade (one undoable command, same as
|
||||||
new TimelineRemoveTrackCommand(track_),
|
// the old TimelineRemoveTrackCommand push).
|
||||||
tr("Deleted Track \"%1\"").arg(track_->get_label_or_name()));
|
oakengine_sequence_remove_track(
|
||||||
|
reinterpret_cast<OakEngineSequence *>(track_->sequence()),
|
||||||
|
int(track_->type()), track_->index());
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrackViewItem::delete_all_empty_tracks()
|
void TrackViewItem::delete_all_empty_tracks()
|
||||||
|
|||||||
@@ -274,12 +274,14 @@ oakengine_sequence_marker_count(const OakEngineSequence *self);
|
|||||||
/**
|
/**
|
||||||
* @brief Marker at `index`: `time` receives its in-point as a timestamp in
|
* @brief Marker at `index`: `time` receives its in-point as a timestamp in
|
||||||
* timebase units (may be NULL), `name` its label using the buf/size
|
* timebase units (may be NULL), `name` its label using the buf/size
|
||||||
* truncation convention (may be NULL to only fetch the time). Returns
|
* truncation convention (may be NULL to only fetch the time), `color` its
|
||||||
|
* color index (may be NULL). Returns
|
||||||
* OAKENGINE_OK on success, OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
* OAKENGINE_OK on success, OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||||
*/
|
*/
|
||||||
OAKENGINE_API int oakengine_sequence_marker_at(const OakEngineSequence *self,
|
OAKENGINE_API int oakengine_sequence_marker_at(const OakEngineSequence *self,
|
||||||
int index, int64_t *time,
|
int index, int64_t *time,
|
||||||
char *name, int name_size);
|
char *name, int name_size,
|
||||||
|
int *color);
|
||||||
|
|
||||||
/* ---- Timeline editing primitives ---------------------------------------- */
|
/* ---- Timeline editing primitives ---------------------------------------- */
|
||||||
|
|
||||||
@@ -437,6 +439,61 @@ OAKENGINE_API int oakengine_sequence_move_clip(OakEngineSequence *seq,
|
|||||||
int clip_index,
|
int clip_index,
|
||||||
int64_t new_in);
|
int64_t new_in);
|
||||||
|
|
||||||
|
/* ---- Batch editing (timeline panel) ------------------------------------------
|
||||||
|
*
|
||||||
|
* Higher-level operations mirroring the application's timeline panel
|
||||||
|
* (app/widget/timelinewidget), each undoable as ONE command like the
|
||||||
|
* panel's own undo entries. Clip arrays hold borrowed handles
|
||||||
|
* (oakengine_sequence_clip_at(); the handle is the engine ClipBlock
|
||||||
|
* pointer in this family, so the application can pass its own clips
|
||||||
|
* directly). All times are frame timestamps in the sequence's frame-rate
|
||||||
|
* timebase.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Split every given clip at timeline `time_ts`, preserving links
|
||||||
|
* (undoable; olive::BlockSplitPreservingLinksCommand -- the application's
|
||||||
|
* razor tool / split-at-playhead command).
|
||||||
|
*
|
||||||
|
* Clips not spanning `time_ts` are skipped (same as the engine command);
|
||||||
|
* when none of the clips spans it, the call fails with
|
||||||
|
* OAKENGINE_E_NOT_FOUND and nothing is pushed. The halves of linked clips
|
||||||
|
* come out linked, like the application's split.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_split_clips(
|
||||||
|
OakEngineSequence *seq, OakEngineClip **clips, int clip_count,
|
||||||
|
int64_t time_ts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Delete clips leaving gaps, optionally rippling regions closed
|
||||||
|
* (undoable; the clip-deletion core of the application's
|
||||||
|
* TimelineWidget::DeleteSelected).
|
||||||
|
*
|
||||||
|
* Each clip is replaced with a gap (olive::TrackReplaceBlockWithGapCommand,
|
||||||
|
* transitions left to the caller like the application) and removed from
|
||||||
|
* the graph with its exclusive dependencies
|
||||||
|
* (olive::NodeRemoveWithExclusiveDependenciesAndDisconnect).
|
||||||
|
*
|
||||||
|
* When `ripple` != 0, a olive::TimelineRippleDeleteGapsAtRegionsCommand
|
||||||
|
* follows over `ripple_ranges_ts` -- 4 int64 per range: track_type,
|
||||||
|
* track_index, in_ts, out_ts (NULL with `ripple_range_count` 0 ripples the
|
||||||
|
* deleted clips' own ranges instead). `rippled` (may be NULL) receives 1
|
||||||
|
* when the ripple actually produced commands. Everything lands as one
|
||||||
|
* undoable command.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_delete_clips(
|
||||||
|
OakEngineSequence *seq, OakEngineClip **clips, int clip_count, int ripple,
|
||||||
|
const int64_t *ripple_ranges_ts, int ripple_range_count, int *rippled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove the area [in_ts, out_ts) on every track and shift the
|
||||||
|
* following content left (undoable;
|
||||||
|
* olive::TimelineRippleRemoveAreaCommand -- the application's
|
||||||
|
* "ripple to playhead"). `in_ts` must be >= 0 and `out_ts` > `in_ts`.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_ripple_delete_range(
|
||||||
|
OakEngineSequence *seq, int64_t in_ts, int64_t out_ts);
|
||||||
|
|
||||||
/* ---- Track structure and markers ------------------------------------------
|
/* ---- Track structure and markers ------------------------------------------
|
||||||
*
|
*
|
||||||
* Track structure edits are undoable like the other editing primitives.
|
* Track structure edits are undoable like the other editing primitives.
|
||||||
@@ -524,6 +581,18 @@ OAKENGINE_API int oakengine_sequence_marker_add(OakEngineSequence *seq,
|
|||||||
int64_t time_ts,
|
int64_t time_ts,
|
||||||
const char *name);
|
const char *name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add a timeline marker with an explicit color index (undoable).
|
||||||
|
*
|
||||||
|
* Same as oakengine_sequence_marker_add() (which passes color 0) but the
|
||||||
|
* caller picks the marker color, like the application's "set marker"
|
||||||
|
* action (color of the closest marker, or the configured default).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_marker_add_ex(OakEngineSequence *seq,
|
||||||
|
int64_t time_ts,
|
||||||
|
const char *name,
|
||||||
|
int color);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Remove the (first) marker at `time_ts` (undoable;
|
* @brief Remove the (first) marker at `time_ts` (undoable;
|
||||||
* olive::MarkerRemoveCommand). OAKENGINE_E_NOT_FOUND when no marker exists
|
* olive::MarkerRemoveCommand). OAKENGINE_E_NOT_FOUND when no marker exists
|
||||||
|
|||||||
@@ -714,7 +714,8 @@ int oakengine_sequence_marker_count(const OakEngineSequence *self)
|
|||||||
}
|
}
|
||||||
|
|
||||||
int oakengine_sequence_marker_at(const OakEngineSequence *self, int index,
|
int oakengine_sequence_marker_at(const OakEngineSequence *self, int index,
|
||||||
int64_t *time, char *name, int name_size)
|
int64_t *time, char *name, int name_size,
|
||||||
|
int *color)
|
||||||
{
|
{
|
||||||
if (!self || index < 0) {
|
if (!self || index < 0) {
|
||||||
return OAKENGINE_E_INVALID;
|
return OAKENGINE_E_INVALID;
|
||||||
@@ -734,6 +735,9 @@ int oakengine_sequence_marker_at(const OakEngineSequence *self, int index,
|
|||||||
if (name && name_size > 0) {
|
if (name && name_size > 0) {
|
||||||
copy_to_buf(marker->name(), name, size_t(name_size));
|
copy_to_buf(marker->name(), name, size_t(name_size));
|
||||||
}
|
}
|
||||||
|
if (color) {
|
||||||
|
*color = marker->color();
|
||||||
|
}
|
||||||
return OAKENGINE_OK;
|
return OAKENGINE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,6 +1080,189 @@ int oakengine_sequence_move_clip(OakEngineSequence *seq, int track_type,
|
|||||||
return OAKENGINE_OK;
|
return OAKENGINE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Batch editing (timeline panel) ------------------------------------------ */
|
||||||
|
|
||||||
|
int oakengine_sequence_split_clips(OakEngineSequence *seq,
|
||||||
|
OakEngineClip **clips, int clip_count,
|
||||||
|
int64_t time_ts)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || !clips || clip_count <= 0) {
|
||||||
|
set_seq_error(QStringLiteral("invalid arguments"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
olive::Rational tb;
|
||||||
|
if (!time_base_of(sequence, &tb)) {
|
||||||
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
const olive::Rational time =
|
||||||
|
olive::core::Timecode::timestamp_to_time(time_ts, tb);
|
||||||
|
|
||||||
|
QVector<olive::Block *> blocks;
|
||||||
|
blocks.reserve(clip_count);
|
||||||
|
bool any_spanning = false;
|
||||||
|
for (int i = 0; i < clip_count; i++) {
|
||||||
|
olive::ClipBlock *clip =
|
||||||
|
reinterpret_cast<olive::ClipBlock *>(clips[i]);
|
||||||
|
if (!clip) {
|
||||||
|
set_seq_error(QStringLiteral("invalid clip at index %1").arg(i));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
if (blocks.contains(clip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
blocks.append(clip);
|
||||||
|
if (clip->in() < time && clip->out() > time) {
|
||||||
|
any_spanning = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any_spanning) {
|
||||||
|
set_seq_error(QStringLiteral("no clip spans time %1").arg(time_ts));
|
||||||
|
return OAKENGINE_E_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as the application's razor tool / split-at-playhead
|
||||||
|
// (BlockSplitPreservingLinksCommand): split every block spanning the
|
||||||
|
// time and link the halves of linked blocks, one undoable command.
|
||||||
|
push_or_run(new olive::BlockSplitPreservingLinksCommand(blocks, { time }),
|
||||||
|
QStringLiteral("Split Clips"));
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_delete_clips(OakEngineSequence *seq,
|
||||||
|
OakEngineClip **clips, int clip_count,
|
||||||
|
int ripple, const int64_t *ripple_ranges_ts,
|
||||||
|
int ripple_range_count, int *rippled)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
if (rippled) {
|
||||||
|
*rippled = 0;
|
||||||
|
}
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || clip_count < 0 || (clip_count > 0 && !clips) ||
|
||||||
|
ripple_range_count < 0 ||
|
||||||
|
(ripple_range_count > 0 && !ripple_ranges_ts)) {
|
||||||
|
set_seq_error(QStringLiteral("invalid arguments"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
if (clip_count == 0 && (!ripple || ripple_range_count == 0)) {
|
||||||
|
// Nothing to delete and nothing to ripple.
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
olive::Rational tb;
|
||||||
|
if (!time_base_of(sequence, &tb)) {
|
||||||
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||||
|
olive::TimelineRippleDeleteGapsAtRegionsCommand::RangeList clip_ranges;
|
||||||
|
for (int i = 0; i < clip_count; i++) {
|
||||||
|
olive::ClipBlock *clip =
|
||||||
|
reinterpret_cast<olive::ClipBlock *>(clips[i]);
|
||||||
|
if (!clip || !clip->track()) {
|
||||||
|
set_seq_error(QStringLiteral("invalid clip at index %1").arg(i));
|
||||||
|
delete command;
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
// Same as the application's delete-selection core
|
||||||
|
// (TimelineWidget::DeleteSelected): replace the clip with a gap
|
||||||
|
// (transitions are the caller's job) and remove it from the graph
|
||||||
|
// with its exclusive dependencies.
|
||||||
|
command->add_child(new olive::TrackReplaceBlockWithGapCommand(
|
||||||
|
clip->track(), clip, false));
|
||||||
|
command->add_child(
|
||||||
|
new olive::NodeRemoveWithExclusiveDependenciesAndDisconnect(clip));
|
||||||
|
clip_ranges.append({ clip->track(), clip->range() });
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::TimelineRippleDeleteGapsAtRegionsCommand *ripple_command = nullptr;
|
||||||
|
if (ripple) {
|
||||||
|
olive::TimelineRippleDeleteGapsAtRegionsCommand::RangeList ranges;
|
||||||
|
if (ripple_ranges_ts && ripple_range_count > 0) {
|
||||||
|
for (int i = 0; i < ripple_range_count; i++) {
|
||||||
|
const int64_t *range = ripple_ranges_ts + i * 4;
|
||||||
|
const int track_type = int(range[0]);
|
||||||
|
const int track_index = int(range[1]);
|
||||||
|
if (track_type < OAKENGINE_TRACK_TYPE_VIDEO ||
|
||||||
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
||||||
|
set_seq_error(QStringLiteral("invalid track type in "
|
||||||
|
"ripple range %1")
|
||||||
|
.arg(i));
|
||||||
|
delete command;
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
olive::TrackList *list =
|
||||||
|
sequence->track_list(to_track_type(track_type));
|
||||||
|
if (track_index < 0 ||
|
||||||
|
track_index >= list->get_track_count()) {
|
||||||
|
set_seq_error(QStringLiteral("no track at index %1 in "
|
||||||
|
"ripple range %2")
|
||||||
|
.arg(track_index)
|
||||||
|
.arg(i));
|
||||||
|
delete command;
|
||||||
|
return OAKENGINE_E_NOT_FOUND;
|
||||||
|
}
|
||||||
|
ranges.append({ list->get_track_at(track_index),
|
||||||
|
olive::TimeRange(
|
||||||
|
olive::core::Timecode::timestamp_to_time(
|
||||||
|
range[2], tb),
|
||||||
|
olive::core::Timecode::timestamp_to_time(
|
||||||
|
range[3], tb)) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ranges = clip_ranges;
|
||||||
|
}
|
||||||
|
if (!ranges.isEmpty()) {
|
||||||
|
ripple_command =
|
||||||
|
new olive::TimelineRippleDeleteGapsAtRegionsCommand(sequence,
|
||||||
|
ranges);
|
||||||
|
command->add_child(ripple_command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push_or_run(command, QStringLiteral("Delete Clips"));
|
||||||
|
if (rippled) {
|
||||||
|
// has_commands() is valid after the stack prepared the command;
|
||||||
|
// without an engine the command ran directly and a non-null ripple
|
||||||
|
// command means regions were queued.
|
||||||
|
*rippled = (ripple_command && (!olive::EngineCore::instance() ||
|
||||||
|
ripple_command->has_commands())) ?
|
||||||
|
1 :
|
||||||
|
0;
|
||||||
|
}
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_ripple_delete_range(OakEngineSequence *seq,
|
||||||
|
int64_t in_ts, int64_t out_ts)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || in_ts < 0 || out_ts <= in_ts) {
|
||||||
|
set_seq_error(QStringLiteral("invalid range [%1, %2)")
|
||||||
|
.arg(in_ts)
|
||||||
|
.arg(out_ts));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
olive::Rational tb;
|
||||||
|
if (!time_base_of(sequence, &tb)) {
|
||||||
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
// Same as the application's "ripple to playhead" (TimelineWidget::
|
||||||
|
// ripple_to): remove the area on every track and shift the following
|
||||||
|
// content left, one undoable command.
|
||||||
|
push_or_run(new olive::TimelineRippleRemoveAreaCommand(
|
||||||
|
sequence,
|
||||||
|
olive::core::Timecode::timestamp_to_time(in_ts, tb),
|
||||||
|
olive::core::Timecode::timestamp_to_time(out_ts, tb)),
|
||||||
|
QStringLiteral("Ripple Delete Range"));
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Track structure and markers ------------------------------------------ */
|
/* ---- Track structure and markers ------------------------------------------ */
|
||||||
|
|
||||||
int oakengine_sequence_remove_track(OakEngineSequence *seq, int track_type,
|
int oakengine_sequence_remove_track(OakEngineSequence *seq, int track_type,
|
||||||
@@ -1278,6 +1465,12 @@ int oakengine_track_set_locked(OakEngineSequence *seq, int track_type,
|
|||||||
|
|
||||||
int oakengine_sequence_marker_add(OakEngineSequence *seq, int64_t time_ts,
|
int oakengine_sequence_marker_add(OakEngineSequence *seq, int64_t time_ts,
|
||||||
const char *name)
|
const char *name)
|
||||||
|
{
|
||||||
|
return oakengine_sequence_marker_add_ex(seq, time_ts, name, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_marker_add_ex(OakEngineSequence *seq, int64_t time_ts,
|
||||||
|
const char *name, int color)
|
||||||
{
|
{
|
||||||
set_seq_error(QString());
|
set_seq_error(QString());
|
||||||
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
@@ -1301,7 +1494,7 @@ int oakengine_sequence_marker_add(OakEngineSequence *seq, int64_t time_ts,
|
|||||||
}
|
}
|
||||||
push_or_run(new olive::MarkerAddCommand(
|
push_or_run(new olive::MarkerAddCommand(
|
||||||
markers, olive::TimeRange(time, time),
|
markers, olive::TimeRange(time, time),
|
||||||
QString::fromUtf8(name ? name : ""), 0),
|
QString::fromUtf8(name ? name : ""), color),
|
||||||
QStringLiteral("Add Marker"));
|
QStringLiteral("Add Marker"));
|
||||||
return OAKENGINE_OK;
|
return OAKENGINE_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ static void test_sequence_and_save_load(void)
|
|||||||
|
|
||||||
// No markers on a fresh sequence.
|
// No markers on a fresh sequence.
|
||||||
assert(oakengine_sequence_marker_count(seq) == 0);
|
assert(oakengine_sequence_marker_count(seq) == 0);
|
||||||
assert(oakengine_sequence_marker_at(seq, 0, NULL, NULL, 0) ==
|
assert(oakengine_sequence_marker_at(seq, 0, NULL, NULL, 0, NULL) ==
|
||||||
OAKENGINE_E_NOT_FOUND);
|
OAKENGINE_E_NOT_FOUND);
|
||||||
|
|
||||||
// Save; the modified flag clears and the filename is adopted.
|
// Save; the modified flag clears and the filename is adopted.
|
||||||
@@ -384,7 +384,7 @@ static void test_null_safety(void)
|
|||||||
assert(oakengine_sequence_set_workarea(NULL, 0, 0, 0) ==
|
assert(oakengine_sequence_set_workarea(NULL, 0, 0, 0) ==
|
||||||
OAKENGINE_E_INVALID);
|
OAKENGINE_E_INVALID);
|
||||||
assert(oakengine_sequence_marker_count(NULL) == 0);
|
assert(oakengine_sequence_marker_count(NULL) == 0);
|
||||||
assert(oakengine_sequence_marker_at(NULL, 0, NULL, NULL, 0) ==
|
assert(oakengine_sequence_marker_at(NULL, 0, NULL, NULL, 0, NULL) ==
|
||||||
OAKENGINE_E_INVALID);
|
OAKENGINE_E_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -584,7 +584,7 @@ static void test_markers(void)
|
|||||||
assert(oakengine_sequence_marker_add(seq, 30, "Chapter 1") ==
|
assert(oakengine_sequence_marker_add(seq, 30, "Chapter 1") ==
|
||||||
OAKENGINE_OK);
|
OAKENGINE_OK);
|
||||||
assert(oakengine_sequence_marker_count(seq) == 1);
|
assert(oakengine_sequence_marker_count(seq) == 1);
|
||||||
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name)) ==
|
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name), NULL) ==
|
||||||
OAKENGINE_OK);
|
OAKENGINE_OK);
|
||||||
assert(ts == 30 && strcmp(name, "Chapter 1") == 0);
|
assert(ts == 30 && strcmp(name, "Chapter 1") == 0);
|
||||||
|
|
||||||
@@ -595,29 +595,29 @@ static void test_markers(void)
|
|||||||
// Earlier marker sorts in front.
|
// Earlier marker sorts in front.
|
||||||
assert(oakengine_sequence_marker_add(seq, 10, "Intro") == OAKENGINE_OK);
|
assert(oakengine_sequence_marker_add(seq, 10, "Intro") == OAKENGINE_OK);
|
||||||
assert(oakengine_sequence_marker_count(seq) == 2);
|
assert(oakengine_sequence_marker_count(seq) == 2);
|
||||||
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name)) ==
|
assert(oakengine_sequence_marker_at(seq, 0, &ts, name, sizeof(name), NULL) ==
|
||||||
OAKENGINE_OK);
|
OAKENGINE_OK);
|
||||||
assert(ts == 10 && strcmp(name, "Intro") == 0);
|
assert(ts == 10 && strcmp(name, "Intro") == 0);
|
||||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
|
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name), NULL) ==
|
||||||
OAKENGINE_OK);
|
OAKENGINE_OK);
|
||||||
assert(ts == 30 && strcmp(name, "Chapter 1") == 0);
|
assert(ts == 30 && strcmp(name, "Chapter 1") == 0);
|
||||||
assert(oakengine_sequence_marker_at(seq, 2, &ts, name, sizeof(name)) ==
|
assert(oakengine_sequence_marker_at(seq, 2, &ts, name, sizeof(name), NULL) ==
|
||||||
OAKENGINE_E_NOT_FOUND);
|
OAKENGINE_E_NOT_FOUND);
|
||||||
|
|
||||||
// Rename with undo/redo.
|
// Rename with undo/redo.
|
||||||
assert(oakengine_sequence_marker_rename(seq, 30, "Chapter 2") ==
|
assert(oakengine_sequence_marker_rename(seq, 30, "Chapter 2") ==
|
||||||
OAKENGINE_OK);
|
OAKENGINE_OK);
|
||||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
|
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name), NULL) ==
|
||||||
OAKENGINE_OK);
|
OAKENGINE_OK);
|
||||||
assert(strcmp(name, "Chapter 2") == 0);
|
assert(strcmp(name, "Chapter 2") == 0);
|
||||||
assert(oakengine_sequence_marker_rename(seq, 77, "nope") ==
|
assert(oakengine_sequence_marker_rename(seq, 77, "nope") ==
|
||||||
OAKENGINE_E_NOT_FOUND);
|
OAKENGINE_E_NOT_FOUND);
|
||||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
|
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name), NULL) ==
|
||||||
OAKENGINE_OK);
|
OAKENGINE_OK);
|
||||||
assert(strcmp(name, "Chapter 1") == 0);
|
assert(strcmp(name, "Chapter 1") == 0);
|
||||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||||
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name)) ==
|
assert(oakengine_sequence_marker_at(seq, 1, &ts, name, sizeof(name), NULL) ==
|
||||||
OAKENGINE_OK);
|
OAKENGINE_OK);
|
||||||
assert(strcmp(name, "Chapter 2") == 0);
|
assert(strcmp(name, "Chapter 2") == 0);
|
||||||
|
|
||||||
@@ -810,6 +810,158 @@ static void test_sequence_params(void)
|
|||||||
oakengine_project_free(p2);
|
oakengine_project_free(p2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_batch_editing(const char *media_path)
|
||||||
|
{
|
||||||
|
OakEngineProject *project = oakengine_project_create();
|
||||||
|
assert(project != NULL);
|
||||||
|
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||||
|
OakEngineSequence *seq = oakengine_sequence_new(project, "Batch");
|
||||||
|
assert(seq != NULL);
|
||||||
|
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_VIDEO) ==
|
||||||
|
0);
|
||||||
|
OakEngineFootage *footage =
|
||||||
|
oakengine_project_import_footage(project, media_path);
|
||||||
|
assert(footage != NULL);
|
||||||
|
|
||||||
|
int64_t in = -1, out = -1;
|
||||||
|
|
||||||
|
// Two clips on the video track: [0, 100) and [100, 160).
|
||||||
|
OakEngineClip *c0 = oakengine_sequence_add_footage_clip(
|
||||||
|
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 100, 0);
|
||||||
|
OakEngineClip *c1 = oakengine_sequence_add_footage_clip(
|
||||||
|
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 100, 160, 0);
|
||||||
|
assert(c0 != NULL && c1 != NULL);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
|
||||||
|
// split_clips at 50: the first clip splits in two.
|
||||||
|
{
|
||||||
|
OakEngineClip *arr[1] = { c0 };
|
||||||
|
assert(oakengine_sequence_split_clips(seq, arr, 1, 50) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
}
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 3);
|
||||||
|
OakEngineClip *left = oakengine_sequence_clip_at(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
|
||||||
|
OakEngineClip *right = oakengine_sequence_clip_at(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1);
|
||||||
|
assert(oakengine_clip_get_range(left, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 50);
|
||||||
|
assert(oakengine_clip_get_range(right, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 50 && out == 100);
|
||||||
|
|
||||||
|
// Nothing spans 200; invalid arrays are rejected.
|
||||||
|
{
|
||||||
|
OakEngineClip *arr[2] = { left, right };
|
||||||
|
assert(oakengine_sequence_split_clips(seq, arr, 2, 200) ==
|
||||||
|
OAKENGINE_E_NOT_FOUND);
|
||||||
|
assert(oakengine_sequence_split_clips(seq, NULL, 1, 50) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_split_clips(seq, arr, 0, 50) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_split_clips(NULL, arr, 1, 50) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo the split.
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
|
||||||
|
// delete_clips without ripple: a gap is left, the second clip stays.
|
||||||
|
int rippled = -1;
|
||||||
|
{
|
||||||
|
OakEngineClip *arr[1] = { c0 };
|
||||||
|
assert(oakengine_sequence_delete_clips(seq, arr, 1, 0, NULL, 0,
|
||||||
|
&rippled) == OAKENGINE_OK);
|
||||||
|
}
|
||||||
|
assert(rippled == 0);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 1);
|
||||||
|
assert(oakengine_clip_get_range(c1, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 100 && out == 160);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
|
||||||
|
// delete_clips with ripple (auto ranges): the second clip shifts left.
|
||||||
|
rippled = -1;
|
||||||
|
{
|
||||||
|
OakEngineClip *arr[1] = { c0 };
|
||||||
|
assert(oakengine_sequence_delete_clips(seq, arr, 1, 1, NULL, 0,
|
||||||
|
&rippled) == OAKENGINE_OK);
|
||||||
|
}
|
||||||
|
assert(rippled == 1);
|
||||||
|
assert(oakengine_clip_get_range(c1, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 60);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(c1, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 100 && out == 160);
|
||||||
|
|
||||||
|
// delete_clips with an explicit ripple range: same shift.
|
||||||
|
rippled = -1;
|
||||||
|
{
|
||||||
|
OakEngineClip *arr[1] = { c0 };
|
||||||
|
const int64_t ranges[4] = { OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 100 };
|
||||||
|
assert(oakengine_sequence_delete_clips(seq, arr, 1, 1, ranges, 1,
|
||||||
|
&rippled) == OAKENGINE_OK);
|
||||||
|
}
|
||||||
|
assert(rippled == 1);
|
||||||
|
assert(oakengine_clip_get_range(c1, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 60);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
|
||||||
|
// A bad explicit range coordinate is rejected without side effects.
|
||||||
|
{
|
||||||
|
OakEngineClip *arr[1] = { c0 };
|
||||||
|
const int64_t bad[4] = { OAKENGINE_TRACK_TYPE_VIDEO, 9, 0, 100 };
|
||||||
|
assert(oakengine_sequence_delete_clips(seq, arr, 1, 1, bad, 1,
|
||||||
|
NULL) == OAKENGINE_E_NOT_FOUND);
|
||||||
|
}
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
assert(oakengine_sequence_delete_clips(NULL, NULL, 0, 0, NULL, 0,
|
||||||
|
NULL) == OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
// ripple_delete_range over [0, 100): the area is removed from every
|
||||||
|
// track and the following content shifts left.
|
||||||
|
assert(oakengine_sequence_ripple_delete_range(seq, 0, 100) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 1);
|
||||||
|
OakEngineClip *remaining = oakengine_sequence_clip_at(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
|
||||||
|
assert(oakengine_clip_get_range(remaining, &in, &out, NULL) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 60);
|
||||||
|
assert(oakengine_sequence_ripple_delete_range(seq, 100, 100) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_ripple_delete_range(seq, -1, 5) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_ripple_delete_range(NULL, 0, 1) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
|
||||||
|
// Marker with an explicit color.
|
||||||
|
assert(oakengine_sequence_marker_add_ex(seq, 10, "colored", 5) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
int color = -1;
|
||||||
|
int64_t ts = -1;
|
||||||
|
assert(oakengine_sequence_marker_at(seq, 0, &ts, NULL, 0, &color) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(ts == 10 && color == 5);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_marker_count(seq) == 0);
|
||||||
|
|
||||||
|
oakengine_footage_free(footage);
|
||||||
|
oakengine_project_free(project);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
make_tmpdir();
|
make_tmpdir();
|
||||||
@@ -839,6 +991,7 @@ int main(void)
|
|||||||
test_track_structure(path);
|
test_track_structure(path);
|
||||||
test_markers();
|
test_markers();
|
||||||
test_sequence_params();
|
test_sequence_params();
|
||||||
|
test_batch_editing(path);
|
||||||
|
|
||||||
oakengine_project_free(project);
|
oakengine_project_free(project);
|
||||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||||
|
|||||||
Reference in New Issue
Block a user