engine: timeline panel batch/composite commands migrate to the facade (part 2)
- new primitives: ripple_delete_in_to_out (ripple or gap fill plus work-area state, one undo command), trim_clips_to (batch edge trim returning a count), delete_empty_tracks (type-filtered batch), and marker_remove_many (sparse marker deletion by timestamp array) - delete-in-to-out, edit-to, delete-all-empty-tracks, and sequence viewer marker deletion now go through the facade; empty operations no longer push empty undo entries - footage viewer marker deletion keeps its app path deliberately (facade marker handles are Sequences, not generic viewers); the tentative subtitle track and pointer drag chain stay as documented leftovers
This commit is contained in:
@@ -53,7 +53,6 @@
|
|||||||
#include "timeline/timelineundogeneral.h"
|
#include "timeline/timelineundogeneral.h"
|
||||||
#include "timeline/timelineundopointer.h"
|
#include "timeline/timelineundopointer.h"
|
||||||
#include "timeline/timelineundoripple.h"
|
#include "timeline/timelineundoripple.h"
|
||||||
#include "timeline/timelineundoworkarea.h"
|
|
||||||
#include "tool/add.h"
|
#include "tool/add.h"
|
||||||
#include "tool/beam.h"
|
#include "tool/beam.h"
|
||||||
#include "tool/edit.h"
|
#include "tool/edit.h"
|
||||||
@@ -873,44 +872,22 @@ void TimelineWidget::delete_in_to_out(bool ripple)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiUndoCommand *command = new MultiUndoCommand();
|
// Compound through the liboakengine C ABI facade (ripple removal or
|
||||||
|
// per-track gap fill + workarea disable, one undoable command with the
|
||||||
|
// same semantics as the old app-side assembly).
|
||||||
|
const Rational wa_in = get_connected_node()->get_work_area()->in();
|
||||||
|
const Rational wa_out = get_connected_node()->get_work_area()->out();
|
||||||
|
oakengine_sequence_ripple_delete_in_to_out(
|
||||||
|
reinterpret_cast<OakEngineSequence *>(sequence()), ripple ? 1 : 0,
|
||||||
|
Timecode::time_to_timestamp(wa_in, timebase(), Timecode::k_round),
|
||||||
|
Timecode::time_to_timestamp(wa_out, timebase(), Timecode::k_round));
|
||||||
|
|
||||||
|
// Playhead move is not undoable and stays here (same as before).
|
||||||
if (ripple) {
|
if (ripple) {
|
||||||
command->add_child(new TimelineRippleRemoveAreaCommand(
|
get_connected_node()->set_playhead(wa_in);
|
||||||
sequence(), get_connected_node()->get_work_area()->in(),
|
|
||||||
get_connected_node()->get_work_area()->out()));
|
|
||||||
|
|
||||||
} else {
|
|
||||||
QVector<Track *> unlocked_tracks = sequence()->get_unlocked_tracks();
|
|
||||||
|
|
||||||
foreach (Track *track, unlocked_tracks) {
|
|
||||||
GapBlock *gap = new GapBlock();
|
|
||||||
|
|
||||||
gap->set_length_and_media_out(
|
|
||||||
get_connected_node()->get_work_area()->length());
|
|
||||||
|
|
||||||
command->add_child(new NodeAddCommand(
|
|
||||||
static_cast<Project *>(track->parent()), gap));
|
|
||||||
|
|
||||||
command->add_child(new TrackPlaceBlockCommand(
|
|
||||||
sequence()->track_list(track->type()), track->index(), gap,
|
|
||||||
get_connected_node()->get_work_area()->in()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear workarea after this
|
|
||||||
command->add_child(new WorkareaSetEnabledCommand(
|
|
||||||
get_connected_node()->project(), get_connected_node()->get_work_area(),
|
|
||||||
false));
|
|
||||||
|
|
||||||
if (ripple) {
|
|
||||||
get_connected_node()->set_playhead(
|
|
||||||
get_connected_node()->get_work_area()->in());
|
|
||||||
}
|
|
||||||
|
|
||||||
Core::instance()->undo_stack()->push(command, tr("Deleted In To Out"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void TimelineWidget::toggle_selected_enabled()
|
void TimelineWidget::toggle_selected_enabled()
|
||||||
{
|
{
|
||||||
QVector<Block *> items = get_selected_blocks();
|
QVector<Block *> items = get_selected_blocks();
|
||||||
@@ -2583,34 +2560,14 @@ void TimelineWidget::edit_to(Timeline::MovementMode mode)
|
|||||||
{
|
{
|
||||||
const Rational playhead_time = get_connected_node()->get_playhead();
|
const Rational playhead_time = get_connected_node()->get_playhead();
|
||||||
|
|
||||||
// Get list of unlocked tracks
|
// Batch trim through the liboakengine C ABI facade (one undoable
|
||||||
QVector<Timeline::EditToInfo> tracks = get_edit_to_info(playhead_time, mode);
|
// command; the per-track nearest-block semantics of the old app-side
|
||||||
|
// assembly live behind the facade now).
|
||||||
if (tracks.isEmpty()) {
|
oakengine_sequence_trim_clips_to(
|
||||||
return;
|
reinterpret_cast<OakEngineSequence *>(sequence()),
|
||||||
}
|
(mode == Timeline::k_trim_in) ? 0 : 1,
|
||||||
|
Timecode::time_to_timestamp(playhead_time, timebase(),
|
||||||
MultiUndoCommand *command = new MultiUndoCommand();
|
Timecode::k_round));
|
||||||
|
|
||||||
foreach (const Timeline::EditToInfo &info, tracks) {
|
|
||||||
if (info.nearest_block &&
|
|
||||||
!dynamic_cast<GapBlock *>(info.nearest_block) &&
|
|
||||||
info.nearest_time != playhead_time) {
|
|
||||||
Rational new_len;
|
|
||||||
|
|
||||||
if (mode == Timeline::k_trim_in) {
|
|
||||||
new_len = playhead_time - info.nearest_time;
|
|
||||||
} else {
|
|
||||||
new_len = info.nearest_time - playhead_time;
|
|
||||||
}
|
|
||||||
new_len = info.nearest_block->length() - new_len;
|
|
||||||
|
|
||||||
command->add_child(new BlockTrimCommand(
|
|
||||||
info.track, info.nearest_block, new_len, mode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Core::instance()->undo_stack()->push(command, tr("Cut Clip(s) To Point"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TimelineWidget::update_viewports(const Track::Type &type)
|
void TimelineWidget::update_viewports(const Track::Type &type)
|
||||||
|
|||||||
@@ -28,9 +28,8 @@
|
|||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
|
||||||
#include "core.h"
|
#include "node/project/sequence/sequence.h"
|
||||||
#include "oakengine/timeline.h"
|
#include "oakengine/timeline.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"
|
||||||
|
|
||||||
@@ -189,12 +188,10 @@ void TrackViewItem::delete_all_empty_tracks()
|
|||||||
tr("This will delete the following tracks:\n\n%1\n\nDo you wish to continue?")
|
tr("This will delete the following tracks:\n\n%1\n\nDo you wish to continue?")
|
||||||
.arg(track_names_to_remove.join('\n')),
|
.arg(track_names_to_remove.join('\n')),
|
||||||
QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) {
|
QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) {
|
||||||
MultiUndoCommand *command = new MultiUndoCommand();
|
// Batch removal through the liboakengine C ABI facade (one
|
||||||
foreach (Track *track, tracks_to_remove) {
|
// undoable command, same as the old per-track children).
|
||||||
command->add_child(new TimelineRemoveTrackCommand(track));
|
oakengine_sequence_delete_empty_tracks(
|
||||||
}
|
reinterpret_cast<OakEngineSequence *>(sequence), -1);
|
||||||
Core::instance()->undo_stack()->push(
|
|
||||||
command, tr("Deleted All Empty Tracks"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
#include "common/range.h"
|
#include "common/range.h"
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
#include "dialog/markerproperties/markerpropertiesdialog.h"
|
#include "dialog/markerproperties/markerpropertiesdialog.h"
|
||||||
|
#include "node/project/sequence/sequence.h"
|
||||||
#include "node/project/serializer/serializer.h"
|
#include "node/project/serializer/serializer.h"
|
||||||
|
#include "oakengine/timeline.h"
|
||||||
#include "timeline/timelineundoworkarea.h"
|
#include "timeline/timelineundoworkarea.h"
|
||||||
#include "widget/colorlabelmenu/colorlabelmenu.h"
|
#include "widget/colorlabelmenu/colorlabelmenu.h"
|
||||||
#include "widget/menu/menushared.h"
|
#include "widget/menu/menushared.h"
|
||||||
@@ -122,16 +124,38 @@ void SeekableWidget::set_work_area(TimelineWorkArea *workarea)
|
|||||||
void SeekableWidget::delete_selected()
|
void SeekableWidget::delete_selected()
|
||||||
{
|
{
|
||||||
if (!selection_manager_.is_dragging()) {
|
if (!selection_manager_.is_dragging()) {
|
||||||
|
const auto &selected = selection_manager_.get_selected_objects();
|
||||||
|
if (selected.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Sequence *sequence =
|
||||||
|
dynamic_cast<Sequence *>(get_viewer_node())) {
|
||||||
|
// Batch removal through the liboakengine C ABI facade (one
|
||||||
|
// undoable command). The facade family only wraps sequences;
|
||||||
|
// markers of other viewer nodes (e.g. footage viewers) keep
|
||||||
|
// the old per-marker command path below.
|
||||||
|
QVector<int64_t> times;
|
||||||
|
times.reserve(int(selected.size()));
|
||||||
|
for (TimelineMarker *marker : selected) {
|
||||||
|
times.append(Timecode::time_to_timestamp(
|
||||||
|
marker->time().in(), timebase(), Timecode::k_round));
|
||||||
|
}
|
||||||
|
oakengine_sequence_marker_remove_many(
|
||||||
|
reinterpret_cast<OakEngineSequence *>(sequence),
|
||||||
|
times.constData(), times.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MultiUndoCommand *command = new MultiUndoCommand();
|
MultiUndoCommand *command = new MultiUndoCommand();
|
||||||
|
|
||||||
foreach (TimelineMarker *marker,
|
foreach (TimelineMarker *marker, selected) {
|
||||||
selection_manager_.get_selected_objects()) {
|
|
||||||
command->add_child(new MarkerRemoveCommand(marker));
|
command->add_child(new MarkerRemoveCommand(marker));
|
||||||
}
|
}
|
||||||
|
|
||||||
Core::instance()->undo_stack()->push(
|
Core::instance()->undo_stack()->push(
|
||||||
command, tr("Deleted %1 Marker(s)")
|
command,
|
||||||
.arg(selection_manager_.get_selected_objects().size()));
|
tr("Deleted %1 Marker(s)").arg(selected.size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -494,6 +494,62 @@ OAKENGINE_API int oakengine_sequence_delete_clips(
|
|||||||
OAKENGINE_API int oakengine_sequence_ripple_delete_range(
|
OAKENGINE_API int oakengine_sequence_ripple_delete_range(
|
||||||
OakEngineSequence *seq, int64_t in_ts, int64_t out_ts);
|
OakEngineSequence *seq, int64_t in_ts, int64_t out_ts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Delete the workarea range on every track (undoable, ONE command;
|
||||||
|
* the application's TimelineWidget::delete_in_to_out).
|
||||||
|
*
|
||||||
|
* `ripple` != 0: the area is removed and following content shifts left
|
||||||
|
* (olive::TimelineRippleRemoveAreaCommand). `ripple` == 0: a gap of the
|
||||||
|
* range's length is placed at `in_ts` on every unlocked track (the
|
||||||
|
* application's gap-insert path). Either way the workarea is disabled
|
||||||
|
* afterwards (olive::WorkareaSetEnabledCommand), all as one undoable
|
||||||
|
* command. Requires an enabled workarea on the sequence
|
||||||
|
* (OAKENGINE_E_STATE otherwise) and 0 <= in_ts < out_ts. The application's
|
||||||
|
* playhead move after a ripple stays with the caller (not undoable).
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_ripple_delete_in_to_out(
|
||||||
|
OakEngineSequence *seq, int ripple, int64_t in_ts, int64_t out_ts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Trim the nearest clip of every unlocked track to `point_ts`
|
||||||
|
* (undoable, ONE command; the application's TimelineWidget::edit_to).
|
||||||
|
*
|
||||||
|
* `edge` 0 (in): per track the nearest block starting before or at the
|
||||||
|
* point gets its in-point trimmed to the point; `edge` 1 (out): the
|
||||||
|
* nearest block before the point gets its out-point trimmed to it
|
||||||
|
* (olive::BlockTrimCommand per affected track). Gap blocks and blocks
|
||||||
|
* already at the point are skipped, like the application. Returns the
|
||||||
|
* number of trimmed clips (>= 0; 0 when nothing qualified and no command
|
||||||
|
* is pushed) or a negative code.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_trim_clips_to(OakEngineSequence *seq,
|
||||||
|
int edge, int64_t point_ts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove every empty track (undoable, ONE command; the
|
||||||
|
* application's "delete all empty tracks").
|
||||||
|
*
|
||||||
|
* `track_type` is an OAKENGINE_TRACK_TYPE_* value to only purge that
|
||||||
|
* type, or -1 for all types (the application's behavior). Returns the
|
||||||
|
* number of removed tracks (>= 0; 0 when no track was empty and no
|
||||||
|
* command is pushed) or a negative code.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_delete_empty_tracks(OakEngineSequence *seq,
|
||||||
|
int track_type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove the markers at the given times (undoable, ONE command;
|
||||||
|
* the marker-list equivalent of the application's marker
|
||||||
|
* SeekableWidget::delete_selected).
|
||||||
|
*
|
||||||
|
* `times_ts` holds `count` marker in-point timestamps; each must name an
|
||||||
|
* existing marker (markers are unique per time in the engine) or the
|
||||||
|
* whole call fails with OAKENGINE_E_NOT_FOUND and nothing is pushed.
|
||||||
|
* Returns the number of removed markers (>= 0) or a negative code.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_sequence_marker_remove_many(
|
||||||
|
OakEngineSequence *seq, const int64_t *times_ts, int count);
|
||||||
|
|
||||||
/* ---- 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.
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include "timeline/timelineundopointer.h"
|
#include "timeline/timelineundopointer.h"
|
||||||
#include "timeline/timelineundoripple.h"
|
#include "timeline/timelineundoripple.h"
|
||||||
#include "timeline/timelineundosplit.h"
|
#include "timeline/timelineundosplit.h"
|
||||||
|
#include "timeline/timelineundoworkarea.h"
|
||||||
#include "timeline/timelineworkarea.h"
|
#include "timeline/timelineworkarea.h"
|
||||||
#include "undo/undocommand.h"
|
#include "undo/undocommand.h"
|
||||||
#include "undo/undostack.h"
|
#include "undo/undostack.h"
|
||||||
@@ -1263,6 +1264,182 @@ int oakengine_sequence_ripple_delete_range(OakEngineSequence *seq,
|
|||||||
return OAKENGINE_OK;
|
return OAKENGINE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_ripple_delete_in_to_out(OakEngineSequence *seq,
|
||||||
|
int ripple, 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::TimelineWorkArea *workarea = sequence->get_work_area();
|
||||||
|
if (!workarea || !workarea->enabled()) {
|
||||||
|
set_seq_error(QStringLiteral("sequence workarea is not enabled"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
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 in_time =
|
||||||
|
olive::core::Timecode::timestamp_to_time(in_ts, tb);
|
||||||
|
const olive::Rational out_time =
|
||||||
|
olive::core::Timecode::timestamp_to_time(out_ts, tb);
|
||||||
|
|
||||||
|
// The application's delete_in_to_out, one undoable command: ripple the
|
||||||
|
// area out (or fill it with a fresh gap per unlocked track), then
|
||||||
|
// disable the workarea.
|
||||||
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||||
|
if (ripple) {
|
||||||
|
command->add_child(new olive::TimelineRippleRemoveAreaCommand(
|
||||||
|
sequence, in_time, out_time));
|
||||||
|
} else {
|
||||||
|
const QVector<olive::Track *> unlocked_tracks =
|
||||||
|
sequence->get_unlocked_tracks();
|
||||||
|
for (olive::Track *track : unlocked_tracks) {
|
||||||
|
auto *gap = new olive::GapBlock();
|
||||||
|
gap->set_length_and_media_out(out_time - in_time);
|
||||||
|
command->add_child(new olive::NodeAddCommand(
|
||||||
|
static_cast<olive::Project *>(track->parent()), gap));
|
||||||
|
command->add_child(new olive::TrackPlaceBlockCommand(
|
||||||
|
sequence->track_list(track->type()), track->index(), gap,
|
||||||
|
in_time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
command->add_child(new olive::WorkareaSetEnabledCommand(
|
||||||
|
sequence->project(), workarea, false));
|
||||||
|
push_or_run(command, QStringLiteral("Delete In To Out"));
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_trim_clips_to(OakEngineSequence *seq, int edge,
|
||||||
|
int64_t point_ts)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || point_ts < 0 || edge < 0 || edge > 1) {
|
||||||
|
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 point =
|
||||||
|
olive::core::Timecode::timestamp_to_time(point_ts, tb);
|
||||||
|
const olive::Timeline::MovementMode mode =
|
||||||
|
(edge == 0) ? olive::Timeline::k_trim_in : olive::Timeline::k_trim_out;
|
||||||
|
|
||||||
|
// The application's edit_to: per unlocked track, trim the nearest
|
||||||
|
// block's edge to the point (gaps and blocks already at the point are
|
||||||
|
// skipped), all as one undoable command.
|
||||||
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||||
|
int trimmed = 0;
|
||||||
|
const QVector<olive::Track *> tracks = sequence->get_unlocked_tracks();
|
||||||
|
for (olive::Track *track : tracks) {
|
||||||
|
olive::Block *block =
|
||||||
|
(mode == olive::Timeline::k_trim_in) ?
|
||||||
|
track->nearest_block_before_or_at(point) :
|
||||||
|
track->nearest_block_before(point);
|
||||||
|
if (!block || dynamic_cast<olive::GapBlock *>(block)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const olive::Rational nearest_time =
|
||||||
|
(mode == olive::Timeline::k_trim_in) ? block->in() : block->out();
|
||||||
|
if (nearest_time == point) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
olive::Rational new_length =
|
||||||
|
(mode == olive::Timeline::k_trim_in) ? point - nearest_time :
|
||||||
|
nearest_time - point;
|
||||||
|
new_length = block->length() - new_length;
|
||||||
|
command->add_child(
|
||||||
|
new olive::BlockTrimCommand(track, block, new_length, mode));
|
||||||
|
trimmed++;
|
||||||
|
}
|
||||||
|
if (trimmed == 0) {
|
||||||
|
delete command;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
push_or_run(command, QStringLiteral("Trim Clips To Point"));
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_delete_empty_tracks(OakEngineSequence *seq,
|
||||||
|
int track_type)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || track_type < -1 ||
|
||||||
|
track_type > OAKENGINE_TRACK_TYPE_SUBTITLE) {
|
||||||
|
set_seq_error(QStringLiteral("invalid arguments"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The application's "delete all empty tracks", one undoable command.
|
||||||
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||||
|
int removed = 0;
|
||||||
|
for (olive::Track *track : sequence->get_tracks()) {
|
||||||
|
if (track_type >= 0 && track->type() != to_track_type(track_type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (track->blocks().isEmpty()) {
|
||||||
|
command->add_child(new olive::TimelineRemoveTrackCommand(track));
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removed == 0) {
|
||||||
|
delete command;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
push_or_run(command, QStringLiteral("Delete Empty Tracks"));
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oakengine_sequence_marker_remove_many(OakEngineSequence *seq,
|
||||||
|
const int64_t *times_ts, int count)
|
||||||
|
{
|
||||||
|
set_seq_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || count < 0 || (count > 0 && !times_ts)) {
|
||||||
|
set_seq_error(QStringLiteral("invalid arguments"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
olive::Rational tb;
|
||||||
|
if (!time_base_of(sequence, &tb)) {
|
||||||
|
set_seq_error(QStringLiteral("sequence has no valid frame rate"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
olive::TimelineMarkerList *markers = sequence->get_markers();
|
||||||
|
|
||||||
|
// Resolve all markers first so a bad time fails without side effects
|
||||||
|
// (markers are unique per time in the engine).
|
||||||
|
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
const olive::Rational time =
|
||||||
|
olive::core::Timecode::timestamp_to_time(times_ts[i], tb);
|
||||||
|
olive::TimelineMarker *marker = markers->get_marker_at_time(time);
|
||||||
|
if (!marker) {
|
||||||
|
set_seq_error(QStringLiteral("no marker at time %1")
|
||||||
|
.arg(times_ts[i]));
|
||||||
|
delete command;
|
||||||
|
return OAKENGINE_E_NOT_FOUND;
|
||||||
|
}
|
||||||
|
command->add_child(new olive::MarkerRemoveCommand(marker));
|
||||||
|
}
|
||||||
|
push_or_run(command, QStringLiteral("Remove Markers"));
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 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,
|
||||||
|
|||||||
@@ -962,6 +962,172 @@ static void test_batch_editing(const char *media_path)
|
|||||||
oakengine_project_free(project);
|
oakengine_project_free(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_batch_editing_round2(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, "Batch2");
|
||||||
|
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;
|
||||||
|
|
||||||
|
OakEngineClip *c0 = oakengine_sequence_add_footage_clip(
|
||||||
|
seq, footage, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0, 100, 0);
|
||||||
|
assert(c0 != NULL);
|
||||||
|
|
||||||
|
// trim_clips_to: out-edge to 60, then in-edge to 40 (one clip each).
|
||||||
|
assert(oakengine_sequence_trim_clips_to(seq, 1, 60) == 1);
|
||||||
|
assert(oakengine_clip_get_range(c0, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 60);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_clip_get_range(c0, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 100);
|
||||||
|
assert(oakengine_sequence_trim_clips_to(seq, 0, 40) == 1);
|
||||||
|
assert(oakengine_clip_get_range(c0, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 40 && out == 100);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
|
||||||
|
// Already at the point: nothing qualifies, 0 and no command.
|
||||||
|
assert(oakengine_sequence_trim_clips_to(seq, 1, 100) == 0);
|
||||||
|
// Past the content there is no block under the point either (the
|
||||||
|
// application's edit_to only shortens blocks containing the point).
|
||||||
|
assert(oakengine_sequence_trim_clips_to(seq, 1, 200) == 0);
|
||||||
|
assert(oakengine_sequence_trim_clips_to(seq, 0, 200) == 0);
|
||||||
|
// Invalid arguments.
|
||||||
|
assert(oakengine_sequence_trim_clips_to(seq, 2, 60) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_trim_clips_to(seq, 1, -1) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_trim_clips_to(NULL, 1, 60) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_clip_get_range(c0, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 100);
|
||||||
|
|
||||||
|
// ripple_delete_in_to_out (ripple): area removed, workarea disabled,
|
||||||
|
// one undo entry restores everything including the workarea.
|
||||||
|
assert(oakengine_sequence_set_workarea(seq, 1, 20, 80) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_workarea_is_enabled(seq) == 1);
|
||||||
|
assert(oakengine_sequence_ripple_delete_in_to_out(seq, 1, 20, 80) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_workarea_is_enabled(seq) == 0);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
OakEngineClip *piece0 = oakengine_sequence_clip_at(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 0);
|
||||||
|
OakEngineClip *piece1 = oakengine_sequence_clip_at(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_VIDEO, 0, 1);
|
||||||
|
assert(oakengine_clip_get_range(piece0, &in, &out, NULL) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 20);
|
||||||
|
assert(oakengine_clip_get_range(piece1, &in, &out, NULL) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 20 && out == 40);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_workarea_is_enabled(seq) == 1);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 1);
|
||||||
|
assert(oakengine_clip_get_range(c0, &in, &out, NULL) == OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 100);
|
||||||
|
|
||||||
|
// ripple_delete_in_to_out (no ripple): a gap fills the area, later
|
||||||
|
// content shifts right by the range length.
|
||||||
|
assert(oakengine_sequence_ripple_delete_in_to_out(seq, 0, 20, 80) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_workarea_is_enabled(seq) == 0);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 2);
|
||||||
|
piece0 = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
0);
|
||||||
|
piece1 = oakengine_sequence_clip_at(seq, OAKENGINE_TRACK_TYPE_VIDEO, 0,
|
||||||
|
1);
|
||||||
|
assert(oakengine_clip_get_range(piece0, &in, &out, NULL) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 0 && out == 20);
|
||||||
|
assert(oakengine_clip_get_range(piece1, &in, &out, NULL) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(in == 80 && out == 100);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_clip_count(seq, OAKENGINE_TRACK_TYPE_VIDEO,
|
||||||
|
0) == 1);
|
||||||
|
|
||||||
|
// Workarea disabled: rejected; bad range: invalid (range is validated
|
||||||
|
// before the workarea state).
|
||||||
|
assert(oakengine_sequence_set_workarea(seq, 0, 20, 80) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_ripple_delete_in_to_out(seq, 1, 20, 80) ==
|
||||||
|
OAKENGINE_E_STATE);
|
||||||
|
assert(oakengine_sequence_ripple_delete_in_to_out(seq, 1, 80, 20) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_ripple_delete_in_to_out(NULL, 1, 20, 80) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
// delete_empty_tracks: add an empty audio and an empty subtitle track.
|
||||||
|
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
|
||||||
|
0);
|
||||||
|
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_SUBTITLE) ==
|
||||||
|
0);
|
||||||
|
int v = 0, a = 0, s = 0;
|
||||||
|
assert(oakengine_sequence_track_count(seq, &v, &a, &s) == OAKENGINE_OK);
|
||||||
|
assert(v == 1 && a == 1 && s == 1);
|
||||||
|
// Type-filtered purge first: only the audio track goes.
|
||||||
|
assert(oakengine_sequence_delete_empty_tracks(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_AUDIO) == 1);
|
||||||
|
assert(oakengine_sequence_track_count(seq, &v, &a, &s) == OAKENGINE_OK);
|
||||||
|
assert(v == 1 && a == 0 && s == 1);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_track_count(seq, &v, &a, &s) == OAKENGINE_OK);
|
||||||
|
assert(v == 1 && a == 1 && s == 1);
|
||||||
|
// All types at once (the application's behavior).
|
||||||
|
assert(oakengine_sequence_delete_empty_tracks(seq, -1) == 2);
|
||||||
|
assert(oakengine_sequence_track_count(seq, &v, &a, &s) == OAKENGINE_OK);
|
||||||
|
assert(v == 1 && a == 0 && s == 0);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_track_count(seq, &v, &a, &s) == OAKENGINE_OK);
|
||||||
|
assert(v == 1 && a == 1 && s == 1);
|
||||||
|
// Nothing empty: 0, no command; invalid type rejected.
|
||||||
|
assert(oakengine_sequence_delete_empty_tracks(
|
||||||
|
seq, OAKENGINE_TRACK_TYPE_VIDEO) == 0);
|
||||||
|
assert(oakengine_sequence_delete_empty_tracks(seq, 3) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_delete_empty_tracks(NULL, -1) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
// marker_remove_many: batch delete by time, one undo entry.
|
||||||
|
assert(oakengine_sequence_marker_add_ex(seq, 10, "m1", 0) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_marker_add_ex(seq, 20, "m2", 0) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_marker_add_ex(seq, 30, "m3", 0) ==
|
||||||
|
OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_marker_count(seq) == 3);
|
||||||
|
{
|
||||||
|
const int64_t times[2] = { 10, 30 };
|
||||||
|
assert(oakengine_sequence_marker_remove_many(seq, times, 2) == 2);
|
||||||
|
}
|
||||||
|
assert(oakengine_sequence_marker_count(seq) == 1);
|
||||||
|
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||||
|
assert(oakengine_sequence_marker_count(seq) == 3);
|
||||||
|
{
|
||||||
|
const int64_t bad[1] = { 99 };
|
||||||
|
assert(oakengine_sequence_marker_remove_many(seq, bad, 1) ==
|
||||||
|
OAKENGINE_E_NOT_FOUND);
|
||||||
|
}
|
||||||
|
assert(oakengine_sequence_marker_count(seq) == 3);
|
||||||
|
assert(oakengine_sequence_marker_remove_many(seq, NULL, 1) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_sequence_marker_remove_many(seq, NULL, 0) == 0);
|
||||||
|
assert(oakengine_sequence_marker_remove_many(NULL, NULL, 0) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
oakengine_footage_free(footage);
|
||||||
|
oakengine_project_free(project);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
make_tmpdir();
|
make_tmpdir();
|
||||||
@@ -992,6 +1158,7 @@ int main(void)
|
|||||||
test_markers();
|
test_markers();
|
||||||
test_sequence_params();
|
test_sequence_params();
|
||||||
test_batch_editing(path);
|
test_batch_editing(path);
|
||||||
|
test_batch_editing_round2(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