refactor(timeline): de-Qt oaktimeline and wrap it in a pure C ABI

- de-Qt all 11 sources: markers/workarea lose QObject/signals (list
  re-sort via direct resort() call, drawing moves to the app layer),
  explicit unique_ptr ownership for marker lists and viewer's
  workarea/markers
- timeline undo command families (split/pointer/ripple/general/track)
  now hold oaknode C handles and do all graph work through the oaknode
  C ABI (no adapter layer); gaps are attached to the project graph
  before insertion, orphan handles tracked and freed
- fix the de-Qt severed Block->Track length-changed chain with a
  direct block_length_changed() call (positions were stale after
  split/trim)
- expand oaknode C API by ~20 functions needed by timeline
  (copy_in_graph, block kind/casts, connect_element, input arrays,
  tracklist family, marker/workarea borrowed outlets)
- pure C ABI in include/timeline (marker/workarea/edit, command
  factories returning OakUndoCommand), oakcommon XML get_native
  accessors
- oaknode<->oaktimeline resolve each other at runtime; standalone
  drivers link both into test binaries
- tests: oaktimeline 117, regressions oakcommon 193 / oaknode 96 /
  oakrender 42 / oakcodec 18 / oakaudio 36 all green
This commit is contained in:
2026-08-07 04:37:24 +08:00
parent 354df2e194
commit 66831a870e
59 changed files with 7459 additions and 98 deletions
+24
View File
@@ -112,3 +112,27 @@ OAKTL_API int64_t oaktimeline_nearest_block_ts(OakNodeTrack *track,
- edit:每个 `_command` 工厂 1 个"构造→入栈→undo 还原"用例
track 增删、place/replace/trim/split/ripple/slide)。
- `oaktimeline_debug_alive_count()` 泄漏断言。
## 实施现状(2026-08-05
- 目录结构:`src/timeline/{src,c_api,tests,standalone}` +
`include/timeline/{error,marker,workarea,edit}.h`
- 构建测试:`cmake -S src/timeline/standalone -B build-oaktimeline &&
cmake --build build-oaktimeline -j && ctest --test-dir
build-oaktimeline`——117/117(本模块 21 用例 + oaknode 回归 96)。
全量回归:oakcommon 193、oaknode 96、oakrender 42、oakcodec 18、
oakaudio 36 全绿。
- C API 与 §2 冻结表的差异:marker/workarea 的增删改统一为
`_command` 工厂形态(返回 OakUndoCommand,调用方 redo/push),
workarea set_range_command 需要调用方提供旧值(facade 知情原则);
edit.h 的 `_command` 工厂按 §2.3 落地并补 insert_gaps/
ripple_remove_area`oaktimeline_marker_list_of/workarea_of` 经
oaknode 新增的借用出口(oaknode_node_get_markers/get_work_area
实现。marker list 无 freeborrowedM4 §2.1 既定)。
- 与计划的差异:undo 命令类未走"语义函数 + 适配类",而是命令类整体
留在模块内、成员全部换成 oaknode C 句柄直调 C ABI01 §0 铁律 6
的直接执行);UndoCommand 跨模块继承保留为例外(notes.md 有记录)。
- oaknode 侧适配:transition/timeline/* 由 stub 改为桥接真身头;
viewer 的 workarea/markers 改 unique_ptr4 个 serializer 的 marker
构造点适配显式 add_marker。
- 已知问题:无(de-Qt 断链的 block→track 长度通知已在本轮修复)。
+39
View File
@@ -246,3 +246,42 @@ ColorManager)去Qt化过程中的删除与语义变化,迁移调用方时需
- displayinternal.h 分层违规(render→src/capi)并入 texturehandle.h。
- Renderer 线程亲和由 QObject::thread() 改显式 owner thread
`set_owner_thread_to_current()`/`called_on_owner_thread()`)。
## oaktimeline 去Qt化的删除与语义变更(2026-08-05)
- TimelineMarker/TimelineWorkArea 的信号(time_changed/name_changed/
color_changed/enabled_changed/range_changed/marker_added/removed/
modified)全部删除,变更通知由 facade 层在命令后发出;list 的重排由
TimelineMarker::set_time 直调 parent_->resort(this)。
- TimelineMarker::draw()/get_marker_height()QPainter/QFontMetrics
删除,归 app 层。
- TimelineMarkerList 的 QObject childEvent 自动注册改为显式
add_marker(std::unique_ptr)/remove_marker 所有权转移;marker 构造
不再带 parent 参数(oaknode 的 4 个 serializer 调用点已适配)。
- ViewerOutput 的 workarea_/markers_ 由 QObject 父子管理改为
std::unique_ptr 持有。
- timeline 全部 undo 命令类改为持有 oaknode C 句柄
OakNodeTrack/OakNodeBlock/OakNodeSequence),图操作全部经
oaknode C ABI01 §0 铁律 6);为此 oaknode C API 新增:
copy_in_graph、clip_add_cache_passthrough_from、block_get_kind、
block_as_node/from_node、track_as_node、nearest_block_before/after_or_at、
command_create_remove_node/add_node/set_position_recursive、
node_get_project、input_array_insert/remove、connect/disconnect_element、
tracklist_get_sequence/track_input_id/array_append/array_remove_last/
get_array_index_from_cache_index、sequence_as_node、node_get_markers/
get_work_area(后两个为 oaktimeline 的借用出口)。
- UndoCommand 跨模块继承(oaktimeline 命令类继承 oakundo 的
olive::UndoCommand)保留——undo 架构的既有跨模块模式,记录为
01 §5 的明确例外(oaknode 的 nodeundo 同例)。
- **修复 de-Qt 断链**:Block 长度变化不再经信号通知 Track,改为
Block::InputValueChangedEvent 里对 track_->block_length_changed(this)
直调(原 Qt 版 connect 链,wave-2 遗留的 facade 接线项提前兑现)。
- 命令创建的 GapBlock 在插入 track 前必须先进项目图
block_add_to_graph/block_remove_from_graph helper,对齐原版
setParent(graph)/setParent(memory) 语义);孤立句柄用 orphan 标志
跟踪,析构时 oaknode_block_free。
- oaknode↔oaktimeline 运行期互相解析(add_default_nodes →
TimelineAddTrackCommand),双方 dylib dynamic_lookup,测试二进制
必须同时链两个库(各 standalone 驱动已接线)。
- oakcommon xml C API 新增 get_native 借用访问器(C++ only),
oaktimeline 的 load/save 经它取回 XmlStreamReader/Writer。
+15
View File
@@ -64,6 +64,21 @@ typedef struct OakXmlWriter {
*/
OakXmlReader oakcommon_xml_reader_init(const char *data);
#ifdef __cplusplus
} /* extern "C" */
namespace olive { class XmlStreamReader; class XmlStreamWriter; }
extern "C" {
#endif
/**
* @brief Borrowed access to the underlying C++ reader/writer (C++ only,
* for adapter layers). Valid while the handle is held. NULL-safe.
*/
olive::XmlStreamReader *oakcommon_xml_reader_get_native(OakXmlReader reader);
olive::XmlStreamWriter *oakcommon_xml_writer_get_native(OakXmlWriter writer);
/**
* @brief Release one reference to a reader.
*
+49
View File
@@ -49,6 +49,13 @@ typedef struct OakNodeBlock OakNodeBlock;
*/
typedef struct OakNodeTrack OakNodeTrack;
/**
* @brief Opaque handle to a node (olive::Node), see node/node.h.
*
* Re-declared here so block.h is self-contained; the typedef is identical.
*/
typedef struct OakNodeNode OakNodeNode;
/**
* @brief Concrete transition kinds for oaknode_block_transition_create().
*/
@@ -57,6 +64,14 @@ enum OakNodeTransitionKind {
OAKNODE_TRANSITION_DIP_TO_COLOR = 1 /**< DipToColorTransition. */
};
/**
* @brief Input ids of a TransitionBlock's block connections
* (TransitionBlock::k_out_block_input / k_in_block_input). Pinned by
* test; pass to oaknode_node_connect()/oaknode_node_disconnect().
*/
#define OAKNODE_TRANSITION_OUT_BLOCK_INPUT "out_block_in"
#define OAKNODE_TRANSITION_IN_BLOCK_INPUT "in_block_in"
/**
* @brief Create a ClipBlock.
*
@@ -91,6 +106,32 @@ OakNodeBlock *oaknode_block_transition_create(int kind);
*/
void oaknode_block_free(OakNodeBlock *block);
enum OakNodeBlockKind {
OAKNODE_BLOCK_OTHER = 0,
OAKNODE_BLOCK_CLIP = 1,
OAKNODE_BLOCK_GAP = 2,
OAKNODE_BLOCK_TRANSITION = 3
};
/**
* @brief Concrete kind of a block (dynamic_cast query).
*/
int oaknode_block_get_kind(OakNodeBlock *block, int *out_kind);
/**
* @brief Borrowed cast from a block handle to its node handle.
*
* Every Block is a Node; the result must not be freed. NULL for NULL.
*/
OakNodeNode *oaknode_block_as_node(OakNodeBlock *block);
/**
* @brief Borrowed cast from a node handle to a block handle.
*
* Returns NULL if the node is not a Block (or for NULL).
*/
OakNodeBlock *oaknode_block_from_node(OakNodeNode *node);
/**
* @brief Rational getters/setters use numerator/denominator out pairs.
*
@@ -233,6 +274,14 @@ int oaknode_transition_get_connected_out_block(OakNodeBlock *transition,
int oaknode_transition_get_connected_in_block(OakNodeBlock *transition,
OakNodeBlock **out);
/**
* @brief Forward cache passthroughs from another clip
* (ClipBlock::add_cache_passthrough_from()). Used after splitting a
* clip so the new part shares the render caches.
*/
int oaknode_clip_add_cache_passthrough_from(OakNodeBlock *clip,
OakNodeBlock *other);
#ifdef __cplusplus
}
#endif
+84
View File
@@ -99,6 +99,17 @@ typedef struct oaknode_value {
*/
typedef struct OakNodeNode OakNodeNode;
/* Re-declared here so node.h is self-contained; see node/project.h. */
typedef struct OakNodeProject OakNodeProject;
/**
* @brief Opaque borrowed handles to the timeline data owned by viewer
* nodes (TimelineMarkerList / TimelineWorkArea in oaktimeline).
* oaktimeline reinterprets these into its own handle types.
*/
typedef struct OakNodeMarkerList OakNodeMarkerList;
typedef struct OakNodeWorkArea OakNodeWorkArea;
/**
* @brief Number of live owned objects created through this API
* (nodes from oaknode_factory_create_from_id()/oaknode_node_create_copy(),
@@ -469,6 +480,79 @@ int oaknode_node_remove_from_context(OakNodeNode *node, OakNodeNode *context);
*/
OakNodeNode *oaknode_node_create_copy(const OakNodeNode *node);
/**
* @brief Copy a node inside its graph (Node::copy_node_in_graph()),
* recording the reconnect operations in a new MultiUndoCommand.
*
* `*out_command` receives an owned undo command handle (free with
* oakundo_command_free()). The copy is inserted into the graph only when
* the returned command is redone; treat it as owned (oaknode_node_free())
* until then. Returns NULL on failure.
*/
OakNodeNode *oaknode_node_copy_in_graph(OakNodeNode *node,
OakUndoCommand **out_command);
/**
* @brief Get the project this node belongs to (borrowed). *out may be
* NULL if the node is orphaned.
*/
int oaknode_node_get_project(const OakNodeNode *node,
OakNodeProject **out);
/**
* @brief Insert/remove an element in an input array (live,
* Node::input_array_insert/remove()). OAKNODE_E_NOT_FOUND for an
* unknown input id.
*/
int oaknode_node_input_array_insert(OakNodeNode *node,
const char *input_id, int index);
int oaknode_node_input_array_remove(OakNodeNode *node,
const char *input_id, int index);
/**
* @brief Element-aware variants of oaknode_node_connect()/disconnect()
* (NodeInput element != -1, e.g. Sequence's track_in_N array inputs).
*/
int oaknode_node_connect_element(OakNodeNode *output_node,
OakNodeNode *input_node,
const char *input_id, int element);
int oaknode_node_disconnect_element(OakNodeNode *input_node,
const char *input_id, int element);
/**
* @brief Create a command that adds a node to a project's graph
* (olive::NodeAddCommand). Owned; free with oakundo_command_free().
*/
OakUndoCommand *oaknode_command_create_add_node(OakNodeProject *graph,
OakNodeNode *node);
/**
* @brief Create a command that sets a node's position in a context and
* repositions its dependencies recursively
* (olive::NodeSetPositionAndDependenciesRecursivelyCommand). Owned.
*/
OakUndoCommand *oaknode_command_create_set_position_recursive(
OakNodeNode *node, OakNodeNode *context, double x, double y);
/**
* @brief Borrowed marker list / work area of a viewer node. *out is NULL
* when the node is not a viewer (or for NULL input).
*/
int oaknode_node_get_markers(const OakNodeNode *node,
OakNodeMarkerList **out);
int oaknode_node_get_work_area(const OakNodeNode *node,
OakNodeWorkArea **out);
/**
* @brief Create a command that removes a node from its graph together
* with its exclusive dependencies and disconnects its edges
* (NodeRemoveWithExclusiveDependenciesAndDisconnect).
*
* Owned command handle; free with oakundo_command_free(). Returns NULL
* on failure.
*/
OakUndoCommand *oaknode_command_create_remove_node(OakNodeNode *node);
/**
* @brief Destroy an OWNED node immediately (C++ delete). NULL is a no-op.
*
+18
View File
@@ -33,6 +33,18 @@
extern "C" {
#endif
/**
* @brief Sequence texture/samples input ids (ViewerOutput::k_texture_input
* / k_samples_input) and the track input id format
* (Sequence::k_track_input_format). Pinned by test.
*/
#define OAKNODE_SEQUENCE_TEXTURE_INPUT "tex_in"
#define OAKNODE_SEQUENCE_SAMPLES_INPUT "samples_in"
#define OAKNODE_SEQUENCE_TRACK_INPUT_FORMAT "track_in_%1"
/* Re-declared here so sequence.h is self-contained; see node/node.h. */
typedef struct OakNodeNode OakNodeNode;
/**
* @brief Opaque handle to a sequence (olive::Sequence).
*
@@ -71,6 +83,12 @@ void oaknode_sequence_free(OakNodeSequence *sequence);
* @param type One of OAKNODE_TRACK_TYPE_VIDEO / _AUDIO / _SUBTITLE.
* @return OAKNODE_OK, OAKNODE_E_INVALID or OAKNODE_E_NOT_FOUND (bad type).
*/
/**
* @brief Borrowed cast from a sequence handle to its node handle.
* NULL for NULL.
*/
OakNodeNode *oaknode_sequence_as_node(OakNodeSequence *sequence);
int oaknode_sequence_get_track_list(OakNodeSequence *sequence, int type,
OakNodeTrackList **out);
+45
View File
@@ -67,6 +67,15 @@ enum OakNodeTrackType {
OAKNODE_TRACK_TYPE_COUNT = 3
};
/* Re-declared here so track.h is self-contained; see node/node.h. */
typedef struct OakNodeNode OakNodeNode;
/**
* @brief Borrowed cast from a track handle to its node handle.
* NULL for NULL.
*/
OakNodeNode *oaknode_track_as_node(OakNodeTrack *track);
/* ---------------------------------------------------------------- Track */
/**
@@ -220,6 +229,42 @@ int oaknode_track_is_range_free(OakNodeTrack *track, int in_num, int in_den,
/**
* @brief Track list type (OakNodeTrackType values).
*/
/**
* @brief Nearest block lookups (Track::nearest_block_before_or_at /
* nearest_block_after_or_at). *out is a borrowed handle or NULL.
*/
int oaknode_track_get_nearest_block_before_or_at(OakNodeTrack *track,
int numerator, int denominator, OakNodeBlock **out);
int oaknode_track_get_nearest_block_after_or_at(OakNodeTrack *track,
int numerator, int denominator, OakNodeBlock **out);
/**
* @brief Borrowed sequence owning this track list.
*/
int oaknode_tracklist_get_sequence(OakNodeTrackList *list,
OakNodeSequence **out);
/**
* @brief The list's track input id on the parent sequence
* (e.g. "track_in_0"). Two-stage string getter.
*/
int oaknode_tracklist_get_track_input_id(OakNodeTrackList *list,
char *buf, int buf_size);
/**
* @brief Live input-array append/remove on the parent sequence for this
* list's track input (TrackList::array_append/array_remove_last()).
*/
int oaknode_tracklist_array_append(OakNodeTrackList *list);
int oaknode_tracklist_array_remove_last(OakNodeTrackList *list);
/**
* @brief Map a cached track index to the input-array element index
* (TrackList::get_array_index_from_cache_index()).
*/
int oaknode_tracklist_get_array_index_from_cache_index(
OakNodeTrackList *list, int cache_index, int *out_index);
int oaknode_tracklist_get_type(OakNodeTrackList *list, int *type);
/**
+115
View File
@@ -0,0 +1,115 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EDITOR_TIMELINE_EDIT_H
#define OAK_EDITOR_TIMELINE_EDIT_H
#include "node/block.h"
#include "node/sequence.h"
#include "node/track.h"
#include "timeline/error.h"
#include "undo/undocommand.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Timeline edit primitives (M4 §2.3).
*
* The timeline undo command classes stay inside oaktimeline (01 §5);
* consumers create commands through these factories, receiving base
* OakUndoCommand handles (owned; free with oakundo_command_free()).
* Redo a command directly or push it on an undo stack.
*/
/** @brief olive::TimelineAddTrackCommand. */
OakUndoCommand *oaktimeline_add_track_command(OakNodeTrackList *list);
/** @brief olive::TimelineRemoveTrackCommand. */
OakUndoCommand *oaktimeline_remove_track_command(OakNodeTrack *track);
/** @brief olive::TrackPlaceBlockCommand. */
OakUndoCommand *oaktimeline_place_block_command(OakNodeTrackList *list,
int track_index,
OakNodeBlock *block,
int64_t in_num,
int64_t in_den);
/** @brief olive::TrackReplaceBlockWithGapCommand. */
OakUndoCommand *oaktimeline_replace_block_with_gap_command(
OakNodeTrack *track, OakNodeBlock *block);
/**
* @brief olive::BlockTrimCommand. `mode` is an OakTimelineMovementMode
* value (k_trim_in / k_trim_out).
*/
OakUndoCommand *oaktimeline_trim_command(OakNodeTrack *track,
OakNodeBlock *block,
int64_t new_length_num,
int64_t new_length_den, int mode);
/** @brief olive::BlockSplitCommand on a set of blocks at one point. */
OakUndoCommand *oaktimeline_split_command(OakNodeBlock *const *blocks,
int count, int64_t point_num,
int64_t point_den);
/** @brief olive::BlockSplitPreservingLinksCommand. */
OakUndoCommand *oaktimeline_split_preserving_links_command(
OakNodeBlock *const *blocks, int count, const int64_t *point_nums,
const int64_t *point_dens, int time_count);
/** @brief olive::TimelineRippleDeleteGapsAtRegionsCommand. */
OakUndoCommand *oaktimeline_ripple_delete_gaps_command(
OakNodeSequence *sequence, const int64_t *in_nums,
const int64_t *in_dens, const int64_t *out_nums,
const int64_t *out_dens, OakNodeTrack *const *tracks, int range_count);
/** @brief olive::TrackSlideCommand. */
OakUndoCommand *oaktimeline_slide_command(
OakNodeTrack *track, OakNodeBlock *const *blocks, int block_count,
OakNodeBlock *in_adjacent, OakNodeBlock *out_adjacent,
int64_t movement_num, int64_t movement_den);
/** @brief olive::TrackRippleRemoveAreaCommand. */
OakUndoCommand *oaktimeline_ripple_remove_area_command(
OakNodeTrack *track, int64_t in_num, int64_t in_den, int64_t out_num,
int64_t out_den);
/** @brief olive::TrackListInsertGaps. */
OakUndoCommand *oaktimeline_insert_gaps_command(OakNodeTrackList *list,
int64_t point_num,
int64_t point_den,
int64_t length_num,
int64_t length_den);
/** @brief Movement modes (olive::Timeline::MovementMode). */
enum OakTimelineMovementMode {
OAKTIMELINE_MOVEMENT_NONE = 0,
OAKTIMELINE_MOVEMENT_MOVE = 1,
OAKTIMELINE_MOVEMENT_TRIM_IN = 2,
OAKTIMELINE_MOVEMENT_TRIM_OUT = 3
};
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_TIMELINE_EDIT_H
+39
View File
@@ -0,0 +1,39 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EDITOR_TIMELINE_ERROR_H
#define OAK_EDITOR_TIMELINE_ERROR_H
/**
* @brief Status and error codes shared by all oaktimeline C API families.
*
* Return-code convention (mirrors engine/include/oakengine/init.h):
* 0 (OAKTIMELINE_OK) on success, a negative OAKTIMELINE_E_* error code on
* failure. String getters return the required buffer size in bytes
* (including the terminating NUL) as a non-negative value instead.
*/
#define OAKTIMELINE_OK 0 /**< Success. */
#define OAKTIMELINE_E_INVALID (-1) /**< NULL handle or invalid argument. */
#define OAKTIMELINE_E_STATE (-2) /**< Call not valid in the current state. */
#define OAKTIMELINE_E_FAILED (-3) /**< The underlying operation failed. */
#define OAKTIMELINE_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */
#define OAKTIMELINE_E_NOMEM (-5) /**< Allocation failed. */
#endif //OAK_EDITOR_TIMELINE_ERROR_H
+108
View File
@@ -0,0 +1,108 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EDITOR_TIMELINE_MARKER_H
#define OAK_EDITOR_TIMELINE_MARKER_H
#include "common/xmlutils.h"
#include "node/node.h"
#include "timeline/error.h"
#include "undo/undocommand.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Borrowed handle to a timeline marker list
* (olive::TimelineMarkerList), owned by a viewer node.
*
* Obtained via oaktimeline_marker_list_of(); never freed by the caller.
*/
typedef struct OakTimelineMarkerList OakTimelineMarkerList;
/**
* @brief Borrowed marker list of a viewer node (sequence). NULL for NULL
* or when the node is not a viewer.
*/
OakTimelineMarkerList *oaktimeline_marker_list_of(OakNodeNode *owner);
/**
* @brief Number of markers. Out-param convention; OAKTIMELINE_E_INVALID
* for NULL arguments.
*/
int oaktimeline_marker_count(const OakTimelineMarkerList *list,
int *out_count);
/**
* @brief Marker at index: time as num/den pairs, color and name
* (two-stage string). OAKTIMELINE_E_NOT_FOUND when out of range.
*/
int oaktimeline_marker_at(const OakTimelineMarkerList *list, int index,
int *in_num, int *in_den, int *out_num, int *out_den,
int *color, char *name_buf, int buf_size);
/**
* @brief Create a command that adds a marker (olive::MarkerAddCommand).
*
* Owned command; free with oakundo_command_free(). Redo it directly or
* push it on an undo stack. Returns NULL on failure.
*/
OakUndoCommand *oaktimeline_marker_add_command(
OakTimelineMarkerList *list, int in_num, int in_den, int out_num,
int out_den, const char *name, int color);
/**
* @brief Create a command that removes the marker at `index`.
* OAKTIMELINE_E_NOT_FOUND (as NULL result documented by error) is
* reported by returning NULL.
*/
OakUndoCommand *oaktimeline_marker_remove_at_command(
OakTimelineMarkerList *list, int index);
/**
* @brief Create a command that sets a marker's time range.
*/
OakUndoCommand *oaktimeline_marker_set_time_command(
OakTimelineMarkerList *list, int index, int in_num, int in_den,
int out_num, int out_den);
/**
* @brief Create a command that sets a marker's color and/or name.
* `name` may be NULL to leave the name unchanged (color still applies
* when >= 0; both NULL-name and color < 0 is a no-op error).
*/
OakUndoCommand *oaktimeline_marker_set_props_command(
OakTimelineMarkerList *list, int index, int color, const char *name);
/**
* @brief Load/save the list through oakcommon XML handles. The reader
* must be positioned on the wrapping element (e.g. "markers").
*/
int oaktimeline_marker_list_load(OakTimelineMarkerList *list,
OakXmlReader reader);
int oaktimeline_marker_list_save(const OakTimelineMarkerList *list,
OakXmlWriter writer);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_TIMELINE_MARKER_H
+92
View File
@@ -0,0 +1,92 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EDITOR_TIMELINE_WORKAREA_H
#define OAK_EDITOR_TIMELINE_WORKAREA_H
#include "common/xmlutils.h"
#include "node/node.h"
#include "timeline/error.h"
#include "undo/undocommand.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Borrowed handle to a timeline work area
* (olive::TimelineWorkArea), owned by a viewer node.
*/
typedef struct OakTimelineWorkArea OakTimelineWorkArea;
/**
* @brief Borrowed work area of a viewer node (sequence). NULL for NULL
* or when the node is not a viewer.
*/
OakTimelineWorkArea *oaktimeline_workarea_of(OakNodeNode *owner);
/**
* @brief Read the work area state. Out params may individually be NULL.
*/
int oaktimeline_workarea_get(const OakTimelineWorkArea *w, int *in_num,
int *in_den, int *out_num, int *out_den,
int *enabled);
/**
* @brief Set the range directly (live).
*/
int oaktimeline_workarea_set_range(OakTimelineWorkArea *w, int in_num,
int in_den, int out_num, int out_den);
/**
* @brief Create a set-range command (olive::WorkareaSetRangeCommand).
* The old range must be supplied by the caller (facade knows what it
* changed from). Owned; free with oakundo_command_free().
*/
OakUndoCommand *oaktimeline_workarea_set_range_command(
OakTimelineWorkArea *w, int in_num, int in_den, int out_num,
int out_den, int old_in_num, int old_in_den, int old_out_num,
int old_out_den);
/**
* @brief Create a set-enabled command (olive::WorkareaSetEnabledCommand).
*/
OakUndoCommand *oaktimeline_workarea_set_enabled_command(
OakTimelineWorkArea *w, int enabled);
/**
* @brief The reset sentinel range (TimelineWorkArea::k_reset_in/out).
*/
int oaktimeline_workarea_reset(int *in_num, int *in_den, int *out_num,
int *out_den);
/**
* @brief Load/save through oakcommon XML handles. The reader must be
* positioned on the "workarea" element.
*/
int oaktimeline_workarea_load(OakTimelineWorkArea *w, OakXmlReader reader);
int oaktimeline_workarea_save(const OakTimelineWorkArea *w,
OakXmlWriter writer);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_TIMELINE_WORKAREA_H
+1
View File
@@ -4,3 +4,4 @@ add_subdirectory(node)
add_subdirectory(render)
add_subdirectory(codec)
add_subdirectory(audio)
add_subdirectory(timeline)
+16
View File
@@ -316,3 +316,19 @@ int oakcommon_xml_writer_output(OakXmlWriter writer, char *buf,
}
} // extern "C"
olive::XmlStreamReader *oakcommon_xml_reader_get_native(OakXmlReader reader)
{
if (!reader.ctx) {
return nullptr;
}
return &oakcommon::handle_impl<XmlReaderState>(reader.ctx)->reader;
}
olive::XmlStreamWriter *oakcommon_xml_writer_get_native(OakXmlWriter writer)
{
if (!writer.ctx) {
return nullptr;
}
return oakcommon::handle_impl<olive::XmlStreamWriter>(writer.ctx);
}
+54
View File
@@ -475,3 +475,57 @@ int oaknode_transition_get_connected_in_block(OakNodeBlock *transition,
*out = wrap(t->connected_in_block());
return OAKNODE_OK;
}
int oaknode_clip_add_cache_passthrough_from(OakNodeBlock *clip,
OakNodeBlock *other)
{
if (!clip || !other) {
return OAKNODE_E_INVALID;
}
olive::ClipBlock *c = clip_impl(clip);
olive::ClipBlock *o = clip_impl(other);
if (!c || !o) {
return OAKNODE_E_INVALID;
}
try {
c->add_cache_passthrough_from(o);
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
OakNodeNode *oaknode_block_as_node(OakNodeBlock *block)
{
return reinterpret_cast<OakNodeNode *>(block);
}
OakNodeBlock *oaknode_block_from_node(OakNodeNode *node)
{
if (!node) {
return NULL;
}
olive::Node *n = reinterpret_cast<olive::Node *>(node);
return wrap(dynamic_cast<olive::Block *>(n));
}
int oaknode_block_get_kind(OakNodeBlock *block, int *out_kind)
{
if (!block || !out_kind) {
return OAKNODE_E_INVALID;
}
olive::Block *b = impl(block);
if (dynamic_cast<olive::TransitionBlock *>(b)) {
*out_kind = OAKNODE_BLOCK_TRANSITION;
} else if (dynamic_cast<olive::ClipBlock *>(b)) {
*out_kind = OAKNODE_BLOCK_CLIP;
} else if (dynamic_cast<olive::GapBlock *>(b)) {
*out_kind = OAKNODE_BLOCK_GAP;
} else {
*out_kind = OAKNODE_BLOCK_OTHER;
}
return OAKNODE_OK;
}
+202
View File
@@ -25,6 +25,10 @@
#include "nodeundo.h"
#include "project.h"
#include "output/viewer/viewer.h"
#include "valueconvert.h"
namespace
@@ -1028,3 +1032,201 @@ void oaknode_node_free(OakNodeNode *node)
} catch (...) {
}
}
OakNodeNode *oaknode_node_copy_in_graph(OakNodeNode *node,
OakUndoCommand **out_command)
{
if (!node || !out_command) {
return NULL;
}
try {
auto *command = new olive::MultiUndoCommand();
olive::Node *copy =
olive::Node::copy_node_in_graph(to_node(node), command);
if (!copy) {
delete command;
return NULL;
}
*out_command = oaknode_c_api::wrap_command(command);
if (!*out_command) {
delete command;
return NULL;
}
oaknode_c_api::alive_inc();
return from_node(copy);
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaknode_command_create_remove_node(OakNodeNode *node)
{
if (!node) {
return NULL;
}
try {
return oaknode_c_api::wrap_command(
new olive::NodeRemoveWithExclusiveDependenciesAndDisconnect(
to_node(node)));
} catch (...) {
return NULL;
}
}
int oaknode_node_get_project(const OakNodeNode *node, OakNodeProject **out)
{
if (!node || !out) {
return OAKNODE_E_INVALID;
}
*out = reinterpret_cast<OakNodeProject *>(to_node(node)->project());
return OAKNODE_OK;
}
int oaknode_node_input_array_insert(OakNodeNode *node, const char *input_id,
int index)
{
if (!node || !input_id) {
return OAKNODE_E_INVALID;
}
if (!has_input(to_node(node), input_id)) {
return OAKNODE_E_NOT_FOUND;
}
try {
to_node(node)->input_array_insert(input_id, index);
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_node_input_array_remove(OakNodeNode *node, const char *input_id,
int index)
{
if (!node || !input_id) {
return OAKNODE_E_INVALID;
}
if (!has_input(to_node(node), input_id)) {
return OAKNODE_E_NOT_FOUND;
}
try {
to_node(node)->input_array_remove(input_id, index);
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_node_connect_element(OakNodeNode *output_node,
OakNodeNode *input_node,
const char *input_id, int element)
{
if (!output_node || !input_node) {
return OAKNODE_E_INVALID;
}
if (!has_input(to_node(input_node), input_id)) {
return OAKNODE_E_NOT_FOUND;
}
try {
olive::Node::connect_edge(
to_node(output_node),
olive::NodeInput(to_node(input_node), input_id, element));
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_node_disconnect_element(OakNodeNode *input_node,
const char *input_id, int element)
{
if (!input_node) {
return OAKNODE_E_INVALID;
}
if (!has_input(to_node(input_node), input_id)) {
return OAKNODE_E_NOT_FOUND;
}
try {
olive::Node *output = to_node(input_node)->get_connected_output(
input_id, element);
if (!output) {
return OAKNODE_E_NOT_FOUND;
}
olive::Node::disconnect_edge(
output, olive::NodeInput(to_node(input_node), input_id, element));
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
OakUndoCommand *oaknode_command_create_add_node(OakNodeProject *graph,
OakNodeNode *node)
{
if (!graph || !node) {
return NULL;
}
try {
return oaknode_c_api::wrap_command(
new olive::NodeAddCommand(
reinterpret_cast<olive::Project *>(graph), to_node(node)));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaknode_command_create_set_position_recursive(
OakNodeNode *node, OakNodeNode *context, double x, double y)
{
if (!node || !context) {
return NULL;
}
try {
return oaknode_c_api::wrap_command(
new olive::NodeSetPositionAndDependenciesRecursivelyCommand(
to_node(node), to_node(context),
olive::Node::Position(olive::PointF(x, y))));
} catch (...) {
return NULL;
}
}
int oaknode_node_get_markers(const OakNodeNode *node,
OakNodeMarkerList **out)
{
if (!node || !out) {
return OAKNODE_E_INVALID;
}
const olive::Node *n = to_node(node);
if (auto *v = dynamic_cast<const olive::ViewerOutput *>(n)) {
*out = reinterpret_cast<OakNodeMarkerList *>(v->get_markers());
} else {
*out = NULL;
}
return OAKNODE_OK;
}
int oaknode_node_get_work_area(const OakNodeNode *node,
OakNodeWorkArea **out)
{
if (!node || !out) {
return OAKNODE_E_INVALID;
}
const olive::Node *n = to_node(node);
if (auto *v = dynamic_cast<const olive::ViewerOutput *>(n)) {
*out = reinterpret_cast<OakNodeWorkArea *>(v->get_work_area());
} else {
*out = NULL;
}
return OAKNODE_OK;
}
+5
View File
@@ -318,3 +318,8 @@ int oaknode_sequence_set_audio_params(OakNodeSequence *sequence, int index,
}
return OAKNODE_OK;
}
OakNodeNode *oaknode_sequence_as_node(OakNodeSequence *sequence)
{
return reinterpret_cast<OakNodeNode *>(sequence);
}
+88
View File
@@ -24,6 +24,8 @@
#include "alivecount.h"
#include "valueconvert.h"
#include "block/block.h"
#include "output/track/track.h"
#include "output/track/tracklist.h"
@@ -572,3 +574,89 @@ int oaknode_tracklist_remove_track(OakNodeTrackList *list, OakNodeTrack *track)
}
return OAKNODE_OK;
}
int oaknode_track_get_nearest_block_before_or_at(OakNodeTrack *track,
int numerator, int denominator, OakNodeBlock **out)
{
olive::Track *t = impl(track);
if (!t || !out) {
return OAKNODE_E_INVALID;
}
*out = wrap_block(
t->nearest_block_before_or_at(olive::core::Rational(numerator, denominator)));
return OAKNODE_OK;
}
int oaknode_track_get_nearest_block_after_or_at(OakNodeTrack *track,
int numerator, int denominator, OakNodeBlock **out)
{
olive::Track *t = impl(track);
if (!t || !out) {
return OAKNODE_E_INVALID;
}
*out = wrap_block(
t->nearest_block_after_or_at(olive::core::Rational(numerator, denominator)));
return OAKNODE_OK;
}
int oaknode_tracklist_get_sequence(OakNodeTrackList *list,
OakNodeSequence **out)
{
if (!list || !out) {
return OAKNODE_E_INVALID;
}
*out = reinterpret_cast<OakNodeSequence *>(list_impl(list)->parent());
return OAKNODE_OK;
}
int oaknode_tracklist_get_track_input_id(OakNodeTrackList *list, char *buf,
int buf_size)
{
if (!list) {
return OAKNODE_E_INVALID;
}
return oaknode_c_api::copy_string(list_impl(list)->track_input(), buf,
buf_size);
}
int oaknode_tracklist_array_append(OakNodeTrackList *list)
{
if (!list) {
return OAKNODE_E_INVALID;
}
try {
list_impl(list)->array_append();
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_tracklist_array_remove_last(OakNodeTrackList *list)
{
if (!list) {
return OAKNODE_E_INVALID;
}
try {
list_impl(list)->array_remove_last();
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_tracklist_get_array_index_from_cache_index(
OakNodeTrackList *list, int cache_index, int *out_index)
{
if (!list || !out_index) {
return OAKNODE_E_INVALID;
}
*out_index =
list_impl(list)->get_array_index_from_cache_index(cache_index);
return OAKNODE_OK;
}
OakNodeNode *oaknode_track_as_node(OakNodeTrack *track)
{
return reinterpret_cast<OakNodeNode *>(track);
}
+8
View File
@@ -24,6 +24,8 @@
#include <algorithm>
#include "inputdragger.h"
#include "output/track/track.h"
#include "sliderdisplaytype.h"
namespace olive
@@ -95,6 +97,12 @@ void Block::set_enabled(bool e)
void Block::InputValueChangedEvent(const std::string &input, int element)
{
super::InputValueChangedEvent(input, element);
if (input == k_length_input && track_) {
// Formerly the length_changed signal connected by Track::insert;
// direct call after de-Qt (see DEQT.md §7)
track_->block_length_changed(this);
}
}
void Block::set_length_internal(const Rational &length)
+2 -2
View File
@@ -77,8 +77,8 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs,
set_flag(k_dont_show_in_param_view);
workarea_ = new TimelineWorkArea(this);
markers_ = new TimelineMarkerList(this);
workarea_ = std::make_unique<TimelineWorkArea>();
markers_ = std::make_unique<TimelineMarkerList>();
}
std::string ViewerOutput::name() const
+4 -4
View File
@@ -176,11 +176,11 @@ public:
TimelineWorkArea *get_work_area() const
{
return workarea_;
return workarea_.get();
}
TimelineMarkerList *get_markers() const
{
return markers_;
return markers_.get();
}
virtual TimeRange get_video_cache_range() const override
@@ -275,8 +275,8 @@ private:
AudioParams cached_audio_params_;
TimelineWorkArea *workarea_;
TimelineMarkerList *markers_;
std::unique_ptr<TimelineWorkArea> workarea_;
std::unique_ptr<TimelineMarkerList> markers_;
bool autocache_input_video_;
bool autocache_input_audio_;
+4 -4
View File
@@ -84,10 +84,10 @@ Sequence::~Sequence()
void Sequence::add_default_nodes(MultiUndoCommand *command)
{
// Create tracks and connect them to the viewer
UndoCommand *video_track_command =
new TimelineAddTrackCommand(track_list(Track::k_video));
UndoCommand *audio_track_command =
new TimelineAddTrackCommand(track_list(Track::k_audio));
UndoCommand *video_track_command = new TimelineAddTrackCommand(
reinterpret_cast<OakNodeTrackList *>(track_list(Track::k_video)));
UndoCommand *audio_track_command = new TimelineAddTrackCommand(
reinterpret_cast<OakNodeTrackList *>(track_list(Track::k_audio)));
if (command) {
command->add_child(video_track_command);
@@ -838,8 +838,8 @@ void ProjectSerializer210528::load_marker_list(XmlStreamReader *reader,
// MarkerColor resolves through configaccessor.h
// (oakcommon_config_* C ABI)
new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(),
TimeRange(in, out), name, markers);
markers->add_marker(std::make_unique<TimelineMarker>(
OAK_CONFIG("MarkerColor").toInt(), TimeRange(in, out), name));
}
reader->skip_current_element();
@@ -827,8 +827,8 @@ void ProjectSerializer210907::load_marker_list(XmlStreamReader *reader,
// MarkerColor resolves through configaccessor.h
// (oakcommon_config_* C ABI)
new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(),
TimeRange(in, out), name, markers);
markers->add_marker(std::make_unique<TimelineMarker>(
OAK_CONFIG("MarkerColor").toInt(), TimeRange(in, out), name));
}
reader->skip_current_element();
@@ -881,8 +881,8 @@ void ProjectSerializer211228::load_marker_list(XmlStreamReader *reader,
// MarkerColor resolves through configaccessor.h
// (oakcommon_config_* C ABI)
new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(),
TimeRange(in, out), name, markers);
markers->add_marker(std::make_unique<TimelineMarker>(
OAK_CONFIG("MarkerColor").toInt(), TimeRange(in, out), name));
}
reader->skip_current_element();
@@ -1165,8 +1165,9 @@ void ProjectSerializer220403::load_marker_list(XmlStreamReader *reader,
{
while (xml_read_next_start_element(reader)) {
if (reader->name() == "marker") {
TimelineMarker *marker = new TimelineMarker(markers);
load_marker(reader, marker);
auto marker = std::make_unique<TimelineMarker>();
load_marker(reader, marker.get());
markers->add_marker(std::move(marker));
} else {
reader->skip_current_element();
}
+11 -1
View File
@@ -71,6 +71,16 @@ set(BUILD_TESTS ON)
enable_testing()
add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node)
# M4 收尾:oaknode 的 Sequence::add_default_nodes() 依赖 oaktimeline 的
# TimelineAddTrackCommandsrc/node/transition/timeline/* 已桥接真身),
# 两个库在运行期互相解析(dynamic_lookup
set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/timeline ${CMAKE_BINARY_DIR}/timeline)
set(BUILD_TESTS ON)
target_link_options(oaktimeline PRIVATE
"-undefined" "dynamic_lookup"
)
# The transition stub dir must precede ${OAK_REPO_ROOT}/include so that e.g.
# "common/current.h" resolves to the transitional forwarder, not the stale
# public mirror in include/common.
@@ -138,7 +148,7 @@ target_link_libraries(oakrender PRIVATE
"-framework QuartzCore"
)
target_link_libraries(oaknode-gtest PRIVATE oakrender)
target_link_libraries(oaknode-gtest PRIVATE oakrender oaktimeline)
# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
# dynamic_lookup; the real implementation (engine/src/capi/ipc.cpp) is still
+2 -21
View File
@@ -1,22 +1,3 @@
#pragma once
#include "olive/core/util/rational.h"
namespace olive {
enum class TrackType { k_track_type_none, k_track_type_video, k_track_type_audio, k_track_type_subtitle, k_track_type_count };
class Block;
class Track;
class Timeline {
public:
enum MovementMode { k_none, k_move, k_trim_in, k_trim_out };
enum ThumbnailMode { k_thumbnail_off, k_thumbnail_in_out, k_thumbnail_on };
enum WaveformMode { k_waveforms_disabled, k_waveforms_enabled };
static bool is_a_trim_mode(MovementMode mode)
{
return mode == k_trim_in || mode == k_trim_out;
}
struct EditToInfo {
Track *track;
core::Rational nearest_time;
Block *nearest_block;
};
};
}
// M4 收尾:桥接到 oaktimeline 真身
#include "../../../timeline/src/timelinecommon.h"
+2 -24
View File
@@ -1,25 +1,3 @@
#pragma once
#include <string>
#include "xmlutils.h"
#include "olive/core/util/timerange.h"
namespace olive {
using core::TimeRange;
class TimelineMarkerList {
public:
template <typename T> explicit TimelineMarkerList(T *) {}
bool load(XmlStreamReader *) { return true; }
void save(XmlStreamWriter *) const {}
};
class TimelineMarker {
public:
TimelineMarker() {}
explicit TimelineMarker(TimelineMarkerList *) {}
TimelineMarker(int, const TimeRange &, const std::string &,
TimelineMarkerList *) {}
void set_name(const std::string &) {}
void set_color(int) {}
void set_time(const TimeRange &) {}
bool load(XmlStreamReader *) { return true; }
void save(XmlStreamWriter *) const {}
};
}
// M4 收尾:桥接到 oaktimeline 真身
#include "../../../timeline/src/timelinemarker.h"
@@ -1,15 +1,3 @@
#pragma once
// Transitional stub for engine/timeline/timelineundogeneral.h (still
// Qt-based). Only the surface oaknode uses. M4/M7 replace this with the real
// oaktimeline boundary.
#include "undocommand.h"
namespace olive {
class TrackList;
class TimelineAddTrackCommand : public UndoCommand {
public:
explicit TimelineAddTrackCommand(TrackList *) {}
protected:
void redo() override {}
void undo() override {}
};
}
// M4 收尾:桥接到 oaktimeline 真身
#include "../../../timeline/src/timelineundogeneral.h"
@@ -1,21 +1,3 @@
#pragma once
#include "xmlutils.h"
#include "olive/core/util/rational.h"
#include "olive/core/util/timerange.h"
namespace olive {
using core::Rational;
using core::TimeRange;
class TimelineWorkArea {
public:
template <typename T> explicit TimelineWorkArea(T *) {}
const Rational &in() const { return in_; }
const Rational &out() const { return out_; }
void set_enabled(bool) {}
void set_range(const TimeRange &) {}
TimeRange range() const { return TimeRange(); }
bool load(XmlStreamReader *) { return true; }
void save(XmlStreamWriter *) const {}
private:
Rational in_, out_;
};
}
// M4 收尾:桥接到 oaktimeline 真身
#include "../../../timeline/src/timelineworkarea.h"
+9
View File
@@ -59,6 +59,15 @@ set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo)
add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common)
add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node)
# M4 收尾:oaknode 运行期依赖 oaktimelineadd_default_nodes ->
# TimelineAddTrackCommand),测试二进制须链入
set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/timeline ${CMAKE_BINARY_DIR}/timeline)
set(BUILD_TESTS ON)
target_link_options(oaktimeline PRIVATE
"-undefined" "dynamic_lookup"
)
set(BUILD_TESTS ON)
# oaknode needs its transition stubs too when built in this tree (see
+1
View File
@@ -24,6 +24,7 @@ add_executable(oakrender-gtest
)
target_link_libraries(oakrender-gtest PRIVATE
oaktimeline
oakrender
oaknode
oakcommon
+6
View File
@@ -0,0 +1,6 @@
add_subdirectory(src)
add_subdirectory(c_api)
if(BUILD_TESTS)
add_subdirectory(tests)
endif()
+5
View File
@@ -0,0 +1,5 @@
target_sources(oaktimeline PRIVATE
marker.cpp
workarea.cpp
edit.cpp
)
+263
View File
@@ -0,0 +1,263 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "timeline/edit.h"
#include <new>
#include <vector>
#include "../src/timelineundogeneral.h"
#include "../src/timelineundopointer.h"
#include "../src/timelineundoripple.h"
#include "../src/timelineundosplit.h"
#include "../../undo/c_api/commandhandle.h"
namespace
{
OakUndoCommand *wrap_command(olive::UndoCommand *command)
{
if (!command) {
return NULL;
}
OakUndoCommand *handle = new (std::nothrow) OakUndoCommand{command, true};
if (!handle) {
delete command;
}
return handle;
}
olive::core::Rational rat(int64_t n, int64_t d)
{
return olive::core::Rational(int(n), int(d));
}
olive::Timeline::MovementMode to_mode(int mode)
{
return static_cast<olive::Timeline::MovementMode>(mode);
}
} // namespace
OakUndoCommand *oaktimeline_add_track_command(OakNodeTrackList *list)
{
if (!list) {
return NULL;
}
try {
return wrap_command(new olive::TimelineAddTrackCommand(list));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_remove_track_command(OakNodeTrack *track)
{
if (!track) {
return NULL;
}
try {
return wrap_command(new olive::TimelineRemoveTrackCommand(track));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_place_block_command(OakNodeTrackList *list,
int track_index,
OakNodeBlock *block,
int64_t in_num,
int64_t in_den)
{
if (!list || !block) {
return NULL;
}
try {
return wrap_command(new olive::TrackPlaceBlockCommand(
list, track_index, block, rat(in_num, in_den)));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_replace_block_with_gap_command(
OakNodeTrack *track, OakNodeBlock *block)
{
if (!track || !block) {
return NULL;
}
try {
return wrap_command(
new olive::TrackReplaceBlockWithGapCommand(track, block));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_trim_command(OakNodeTrack *track,
OakNodeBlock *block,
int64_t new_length_num,
int64_t new_length_den, int mode)
{
if (!track || !block || (mode != OAKTIMELINE_MOVEMENT_TRIM_IN &&
mode != OAKTIMELINE_MOVEMENT_TRIM_OUT)) {
return NULL;
}
try {
return wrap_command(
new olive::BlockTrimCommand(track, block,
rat(new_length_num, new_length_den),
to_mode(mode)));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_split_command(OakNodeBlock *const *blocks,
int count, int64_t point_num,
int64_t point_den)
{
if (!blocks || count <= 0) {
return NULL;
}
try {
auto *multi = new olive::MultiUndoCommand();
for (int i = 0; i < count; i++) {
if (blocks[i]) {
multi->add_child(new olive::BlockSplitCommand(
blocks[i], rat(point_num, point_den)));
}
}
return wrap_command(multi);
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_split_preserving_links_command(
OakNodeBlock *const *blocks, int count, const int64_t *point_nums,
const int64_t *point_dens, int time_count)
{
if (!blocks || count <= 0 || !point_nums || !point_dens ||
time_count <= 0) {
return NULL;
}
try {
std::vector<OakNodeBlock *> block_vec(blocks, blocks + count);
std::vector<olive::core::Rational> times;
times.reserve(time_count);
for (int i = 0; i < time_count; i++) {
times.push_back(rat(point_nums[i], point_dens[i]));
}
return wrap_command(new olive::BlockSplitPreservingLinksCommand(
block_vec, times));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_ripple_delete_gaps_command(
OakNodeSequence *sequence, const int64_t *in_nums,
const int64_t *in_dens, const int64_t *out_nums,
const int64_t *out_dens, OakNodeTrack *const *tracks, int range_count)
{
if (!sequence || !in_nums || !in_dens || !out_nums || !out_dens ||
!tracks || range_count <= 0) {
return NULL;
}
try {
olive::TimelineRippleDeleteGapsAtRegionsCommand::RangeList regions;
regions.reserve(range_count);
for (int i = 0; i < range_count; i++) {
regions.emplace_back(
tracks[i],
olive::core::TimeRange(rat(in_nums[i], in_dens[i]),
rat(out_nums[i], out_dens[i])));
}
return wrap_command(
new olive::TimelineRippleDeleteGapsAtRegionsCommand(sequence,
regions));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_slide_command(
OakNodeTrack *track, OakNodeBlock *const *blocks, int block_count,
OakNodeBlock *in_adjacent, OakNodeBlock *out_adjacent,
int64_t movement_num, int64_t movement_den)
{
if (!track || !blocks || block_count <= 0) {
return NULL;
}
try {
std::vector<OakNodeBlock *> block_vec(blocks, blocks + block_count);
return wrap_command(new olive::TrackSlideCommand(
track, block_vec, in_adjacent, out_adjacent,
rat(movement_num, movement_den)));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_ripple_remove_area_command(
OakNodeTrack *track, int64_t in_num, int64_t in_den, int64_t out_num,
int64_t out_den)
{
if (!track) {
return NULL;
}
try {
return wrap_command(new olive::TrackRippleRemoveAreaCommand(
track, olive::core::TimeRange(rat(in_num, in_den),
rat(out_num, out_den))));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_insert_gaps_command(OakNodeTrackList *list,
int64_t point_num,
int64_t point_den,
int64_t length_num,
int64_t length_den)
{
if (!list) {
return NULL;
}
try {
return wrap_command(
new olive::TrackListInsertGaps(list, rat(point_num, point_den),
rat(length_num, length_den)));
} catch (...) {
return NULL;
}
}
+247
View File
@@ -0,0 +1,247 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "timeline/marker.h"
#include <new>
#include "../src/timelinemarker.h"
#include "../../undo/c_api/commandhandle.h"
namespace
{
olive::TimelineMarkerList *impl(OakTimelineMarkerList *h)
{
return reinterpret_cast<olive::TimelineMarkerList *>(h);
}
const olive::TimelineMarkerList *impl(const OakTimelineMarkerList *h)
{
return reinterpret_cast<const olive::TimelineMarkerList *>(h);
}
OakTimelineMarkerList *wrap(olive::TimelineMarkerList *l)
{
return reinterpret_cast<OakTimelineMarkerList *>(l);
}
OakUndoCommand *wrap_command(olive::UndoCommand *command)
{
if (!command) {
return NULL;
}
OakUndoCommand *handle = new (std::nothrow) OakUndoCommand{command, true};
if (!handle) {
delete command;
}
return handle;
}
olive::core::Rational rat(int n, int d)
{
return olive::core::Rational(n, d);
}
olive::TimelineMarker *marker_at(OakTimelineMarkerList *list, int index)
{
if (!list || index < 0 || index >= int(impl(list)->size())) {
return nullptr;
}
return impl(list)->at(index);
}
} // namespace
OakTimelineMarkerList *oaktimeline_marker_list_of(OakNodeNode *owner)
{
if (!owner) {
return NULL;
}
OakNodeMarkerList *markers = NULL;
if (oaknode_node_get_markers(owner, &markers) != OAKNODE_OK) {
return NULL;
}
return reinterpret_cast<OakTimelineMarkerList *>(markers);
}
int oaktimeline_marker_count(const OakTimelineMarkerList *list,
int *out_count)
{
if (!list || !out_count) {
return OAKTIMELINE_E_INVALID;
}
*out_count = int(impl(list)->size());
return OAKTIMELINE_OK;
}
int oaktimeline_marker_at(const OakTimelineMarkerList *list, int index,
int *in_num, int *in_den, int *out_num, int *out_den,
int *color, char *name_buf, int buf_size)
{
if (!list) {
return OAKTIMELINE_E_INVALID;
}
if (index < 0 || index >= int(impl(list)->size())) {
return OAKTIMELINE_E_NOT_FOUND;
}
const olive::TimelineMarker *m = impl(list)->at(index);
if (in_num) {
*in_num = m->time().in().numerator();
}
if (in_den) {
*in_den = m->time().in().denominator();
}
if (out_num) {
*out_num = m->time().out().numerator();
}
if (out_den) {
*out_den = m->time().out().denominator();
}
if (color) {
*color = m->color();
}
const std::string &name = m->name();
int needed = int(name.size()) + 1;
if (name_buf && buf_size >= needed) {
memcpy(name_buf, name.c_str(), needed);
}
return needed;
}
OakUndoCommand *oaktimeline_marker_add_command(
OakTimelineMarkerList *list, int in_num, int in_den, int out_num,
int out_den, const char *name, int color)
{
if (!list) {
return NULL;
}
try {
return wrap_command(new olive::MarkerAddCommand(
impl(list),
olive::core::TimeRange(rat(int(in_num), int(in_den)),
rat(int(out_num), int(out_den))),
name ? name : "", color));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_marker_remove_at_command(
OakTimelineMarkerList *list, int index)
{
olive::TimelineMarker *m = marker_at(list, index);
if (!m) {
return NULL;
}
try {
return wrap_command(new olive::MarkerRemoveCommand(m, impl(list)));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_marker_set_time_command(
OakTimelineMarkerList *list, int index, int in_num, int in_den,
int out_num, int out_den)
{
olive::TimelineMarker *m = marker_at(list, index);
if (!m) {
return NULL;
}
try {
return wrap_command(new olive::MarkerChangeTimeCommand(
m, olive::core::TimeRange(rat(int(in_num), int(in_den)),
rat(int(out_num), int(out_den)))));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_marker_set_props_command(
OakTimelineMarkerList *list, int index, int color, const char *name)
{
olive::TimelineMarker *m = marker_at(list, index);
if (!m || (color < 0 && !name)) {
return NULL;
}
try {
if (color >= 0 && name) {
auto *multi = new olive::MultiUndoCommand();
multi->add_child(new olive::MarkerChangeColorCommand(m, color));
multi->add_child(new olive::MarkerChangeNameCommand(m, name));
return wrap_command(multi);
}
if (color >= 0) {
return wrap_command(new olive::MarkerChangeColorCommand(m, color));
}
return wrap_command(new olive::MarkerChangeNameCommand(m, name));
} catch (...) {
return NULL;
}
}
int oaktimeline_marker_list_load(OakTimelineMarkerList *list,
OakXmlReader reader)
{
if (!list || !reader.ctx) {
return OAKTIMELINE_E_INVALID;
}
olive::XmlStreamReader *native = oakcommon_xml_reader_get_native(reader);
if (!native) {
return OAKTIMELINE_E_INVALID;
}
try {
return impl(list)->load(native) ? OAKTIMELINE_OK
: OAKTIMELINE_E_FAILED;
} catch (...) {
return OAKTIMELINE_E_FAILED;
}
}
int oaktimeline_marker_list_save(const OakTimelineMarkerList *list,
OakXmlWriter writer)
{
if (!list || !writer.ctx) {
return OAKTIMELINE_E_INVALID;
}
olive::XmlStreamWriter *native = oakcommon_xml_writer_get_native(writer);
if (!native) {
return OAKTIMELINE_E_INVALID;
}
try {
impl(list)->save(native);
return OAKTIMELINE_OK;
} catch (...) {
return OAKTIMELINE_E_FAILED;
}
}
+201
View File
@@ -0,0 +1,201 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "timeline/workarea.h"
#include <new>
#include "../src/timelineundoworkarea.h"
#include "../src/timelineworkarea.h"
#include "../../undo/c_api/commandhandle.h"
namespace
{
olive::TimelineWorkArea *impl(OakTimelineWorkArea *h)
{
return reinterpret_cast<olive::TimelineWorkArea *>(h);
}
const olive::TimelineWorkArea *impl(const OakTimelineWorkArea *h)
{
return reinterpret_cast<const olive::TimelineWorkArea *>(h);
}
OakUndoCommand *wrap_command(olive::UndoCommand *command)
{
if (!command) {
return NULL;
}
OakUndoCommand *handle = new (std::nothrow) OakUndoCommand{command, true};
if (!handle) {
delete command;
}
return handle;
}
olive::core::Rational rat(int64_t n, int64_t d)
{
return olive::core::Rational(int(n), int(d));
}
} // namespace
OakTimelineWorkArea *oaktimeline_workarea_of(OakNodeNode *owner)
{
if (!owner) {
return NULL;
}
OakNodeWorkArea *workarea = NULL;
if (oaknode_node_get_work_area(owner, &workarea) != OAKNODE_OK) {
return NULL;
}
return reinterpret_cast<OakTimelineWorkArea *>(workarea);
}
int oaktimeline_workarea_get(const OakTimelineWorkArea *w, int *in_num,
int *in_den, int *out_num, int *out_den,
int *enabled)
{
if (!w) {
return OAKTIMELINE_E_INVALID;
}
if (in_num) {
*in_num = impl(w)->in().numerator();
}
if (in_den) {
*in_den = impl(w)->in().denominator();
}
if (out_num) {
*out_num = impl(w)->out().numerator();
}
if (out_den) {
*out_den = impl(w)->out().denominator();
}
if (enabled) {
*enabled = impl(w)->enabled() ? 1 : 0;
}
return OAKTIMELINE_OK;
}
int oaktimeline_workarea_set_range(OakTimelineWorkArea *w, int in_num,
int in_den, int out_num, int out_den)
{
if (!w) {
return OAKTIMELINE_E_INVALID;
}
try {
impl(w)->set_range(
olive::core::TimeRange(rat(in_num, in_den), rat(out_num, out_den)));
return OAKTIMELINE_OK;
} catch (...) {
return OAKTIMELINE_E_FAILED;
}
}
OakUndoCommand *oaktimeline_workarea_set_range_command(
OakTimelineWorkArea *w, int in_num, int in_den, int out_num,
int out_den, int old_in_num, int old_in_den, int old_out_num,
int old_out_den)
{
if (!w) {
return NULL;
}
try {
olive::core::TimeRange range(rat(in_num, in_den), rat(out_num, out_den));
olive::core::TimeRange old_range(rat(old_in_num, old_in_den),
rat(old_out_num, old_out_den));
return wrap_command(
new olive::WorkareaSetRangeCommand(impl(w), range, old_range));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktimeline_workarea_set_enabled_command(
OakTimelineWorkArea *w, int enabled)
{
if (!w) {
return NULL;
}
try {
return wrap_command(
new olive::WorkareaSetEnabledCommand(impl(w), enabled != 0));
} catch (...) {
return NULL;
}
}
int oaktimeline_workarea_reset(int *in_num, int *in_den, int *out_num,
int *out_den)
{
if (!in_num || !in_den || !out_num || !out_den) {
return OAKTIMELINE_E_INVALID;
}
*in_num = olive::TimelineWorkArea::k_reset_in.numerator();
*in_den = olive::TimelineWorkArea::k_reset_in.denominator();
*out_num = olive::TimelineWorkArea::k_reset_out.numerator();
*out_den = olive::TimelineWorkArea::k_reset_out.denominator();
return OAKTIMELINE_OK;
}
int oaktimeline_workarea_load(OakTimelineWorkArea *w, OakXmlReader reader)
{
if (!w || !reader.ctx) {
return OAKTIMELINE_E_INVALID;
}
olive::XmlStreamReader *native = oakcommon_xml_reader_get_native(reader);
if (!native) {
return OAKTIMELINE_E_INVALID;
}
try {
return impl(w)->load(native) ? OAKTIMELINE_OK : OAKTIMELINE_E_FAILED;
} catch (...) {
return OAKTIMELINE_E_FAILED;
}
}
int oaktimeline_workarea_save(const OakTimelineWorkArea *w,
OakXmlWriter writer)
{
if (!w || !writer.ctx) {
return OAKTIMELINE_E_INVALID;
}
olive::XmlStreamWriter *native = oakcommon_xml_writer_get_native(writer);
if (!native) {
return OAKTIMELINE_E_INVALID;
}
try {
impl(w)->save(native);
return OAKTIMELINE_OK;
} catch (...) {
return OAKTIMELINE_E_FAILED;
}
}
+35
View File
@@ -0,0 +1,35 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
# Modifications Copyright (C) 2025 mikesolar
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
file(GLOB_RECURSE OAKTIMELINE_SOURCES CONFIGURE_DEPENDS *.cpp)
add_library(oaktimeline SHARED ${OAKTIMELINE_SOURCES})
target_include_directories(oaktimeline PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${OAK_REPO_ROOT}/include
${OAK_REPO_ROOT}/src/common/src
${OAK_REPO_ROOT}/src/undo/src
${OAK_REPO_ROOT}/core/include
)
target_link_libraries(oaktimeline PUBLIC
oaknode
oakundo
oakcommon
olivecore
)
+60
View File
@@ -0,0 +1,60 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINECOMMON_H
#define OAK_TIMELINECOMMON_H
#include <olive/core/core.h>
using namespace olive::core;
namespace olive
{
class Block;
class Track;
class Timeline {
public:
enum MovementMode { k_none, k_move, k_trim_in, k_trim_out };
enum ThumbnailMode { k_thumbnail_off, k_thumbnail_in_out, k_thumbnail_on };
enum WaveformMode { k_waveforms_disabled, k_waveforms_enabled };
static bool is_a_trim_mode(MovementMode mode)
{
return mode == k_trim_in || mode == k_trim_out;
}
struct EditToInfo {
Track *track;
Rational nearest_time;
Block *nearest_block;
};
};
#define PLAYHEAD_COLOR palette().highlight().color()
}
#endif // OAK_TIMELINECOMMON_H
+291
View File
@@ -0,0 +1,291 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "timelinemarker.h"
#include <cstdlib>
#include "common/config.h"
namespace olive
{
TimelineMarker::TimelineMarker()
: color_(oakcommon_config_get_int(NULL, "MarkerColor", 0))
, parent_(nullptr)
{
}
TimelineMarker::TimelineMarker(int color, const TimeRange &time,
const std::string &name)
: time_(time)
, name_(name)
, color_(color)
, parent_(nullptr)
{
}
void TimelineMarker::set_time(const TimeRange &time)
{
time_ = time;
// Formerly the time_changed signal; the list must re-sort
if (parent_) {
parent_->resort(this);
}
}
void TimelineMarker::set_time(const Rational &time)
{
set_time(TimeRange(time, time + time_.length()));
}
bool TimelineMarker::has_sibling_at_time(const Rational &t) const
{
if (!parent_) {
return false;
}
TimelineMarker *m = parent_->get_marker_at_time(t);
return m && m != this;
}
void TimelineMarker::set_name(const std::string &name)
{
name_ = name;
}
void TimelineMarker::set_color(int c)
{
color_ = c;
}
bool TimelineMarker::load(XmlStreamReader *reader)
{
Rational in, out;
for (const XmlStreamAttribute &attr : reader->attributes()) {
if (attr.name == "name") {
this->set_name(attr.value);
} else if (attr.name == "in") {
in = Rational::from_string(attr.value);
} else if (attr.name == "out") {
out = Rational::from_string(attr.value);
} else if (attr.name == "color") {
this->set_color(atoi(attr.value.c_str()));
}
}
this->set_time(TimeRange(in, out));
// This element has no inner text, so just skip it
reader->skip_current_element();
return true;
}
void TimelineMarker::save(XmlStreamWriter *writer) const
{
writer->write_attribute("name", this->name());
writer->write_attribute("in", this->time().in().to_string());
writer->write_attribute("out", this->time().out().to_string());
writer->write_attribute("color", std::to_string(this->color()));
}
bool TimelineMarkerList::load(XmlStreamReader *reader)
{
while (xml_read_next_start_element(reader)) {
if (reader->name() == "marker") {
auto marker = std::make_unique<TimelineMarker>();
if (!marker->load(reader)) {
return false;
}
this->add_marker(std::move(marker));
} else {
reader->skip_current_element();
}
}
return true;
}
void TimelineMarkerList::save(XmlStreamWriter *writer) const
{
for (const auto &marker : markers_) {
writer->write_start_element("marker");
marker->save(writer);
writer->write_end_element(); // marker
}
}
void TimelineMarkerList::add_marker(std::unique_ptr<TimelineMarker> marker)
{
marker->parent_ = this;
insert_into_list(std::move(marker));
}
std::unique_ptr<TimelineMarker>
TimelineMarkerList::remove_marker(TimelineMarker *m)
{
for (auto it = markers_.begin(); it != markers_.end(); it++) {
if (it->get() == m) {
std::unique_ptr<TimelineMarker> removed = std::move(*it);
markers_.erase(it);
removed->parent_ = nullptr;
return removed;
}
}
return nullptr;
}
void TimelineMarkerList::resort(TimelineMarker *m)
{
std::unique_ptr<TimelineMarker> holder = remove_marker(m);
if (holder) {
insert_into_list(std::move(holder));
}
}
void TimelineMarkerList::insert_into_list(
std::unique_ptr<TimelineMarker> marker)
{
// Insertion sort by time to allow some loop optimizations
for (auto it = markers_.begin(); it != markers_.end(); it++) {
if ((*it)->time().in() > marker->time().in()) {
markers_.insert(it, std::move(marker));
return;
}
}
markers_.push_back(std::move(marker));
}
MarkerAddCommand::MarkerAddCommand(TimelineMarkerList *marker_list,
const TimeRange &range,
const std::string &name, int color)
: marker_list_(marker_list)
, added_marker_(nullptr)
{
memory_manager_ =
std::make_unique<TimelineMarker>(color, range, name);
added_marker_ = memory_manager_.get();
}
MarkerAddCommand::MarkerAddCommand(
TimelineMarkerList *marker_list, std::unique_ptr<TimelineMarker> marker)
: marker_list_(marker_list)
, added_marker_(marker.get())
{
memory_manager_ = std::move(marker);
}
void MarkerAddCommand::redo()
{
if (!memory_manager_) {
// Marker is already in the list (redo after redo)
return;
}
marker_list_->add_marker(std::move(memory_manager_));
}
void MarkerAddCommand::undo()
{
memory_manager_ = marker_list_->remove_marker(added_marker_);
}
MarkerRemoveCommand::MarkerRemoveCommand(TimelineMarker *marker,
TimelineMarkerList *marker_list)
: marker_(marker)
, marker_list_(marker_list)
{
}
void MarkerRemoveCommand::redo()
{
memory_manager_ = marker_list_->remove_marker(marker_);
}
void MarkerRemoveCommand::undo()
{
if (memory_manager_) {
marker_list_->add_marker(std::move(memory_manager_));
}
}
MarkerChangeColorCommand::MarkerChangeColorCommand(TimelineMarker *marker,
int new_color)
: marker_(marker)
, old_color_(marker->color())
, new_color_(new_color)
{
}
void MarkerChangeColorCommand::redo()
{
marker_->set_color(new_color_);
}
void MarkerChangeColorCommand::undo()
{
marker_->set_color(old_color_);
}
MarkerChangeNameCommand::MarkerChangeNameCommand(TimelineMarker *marker,
std::string new_name)
: marker_(marker)
, old_name_(marker->name())
, new_name_(std::move(new_name))
{
}
void MarkerChangeNameCommand::redo()
{
marker_->set_name(new_name_);
}
void MarkerChangeNameCommand::undo()
{
marker_->set_name(old_name_);
}
MarkerChangeTimeCommand::MarkerChangeTimeCommand(TimelineMarker *marker,
const TimeRange &time,
const TimeRange &old_time)
: marker_(marker)
, old_time_(old_time)
, new_time_(time)
{
}
void MarkerChangeTimeCommand::redo()
{
marker_->set_time(new_time_);
}
void MarkerChangeTimeCommand::undo()
{
marker_->set_time(old_time_);
}
}
+269
View File
@@ -0,0 +1,269 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEMARKER_H
#define OAK_TIMELINEMARKER_H
#include <memory>
#include <string>
#include <vector>
#include <olive/core/core.h>
#include "undocommand.h"
#include "xmlutils.h"
using namespace olive::core;
namespace olive
{
class TimelineMarkerList;
/**
* @brief A marker on a timeline, with a time range, a name and a color
*
* De-Qt version: no QObject, no signals. Modification notifications are
* emitted by the caller's layer (facade). The draw() helper moved to the
* app layer.
*/
class TimelineMarker {
public:
TimelineMarker();
TimelineMarker(int color, const TimeRange &time,
const std::string &name = std::string());
const TimeRange &time() const
{
return time_;
}
void set_time(const TimeRange &time);
void set_time(const Rational &time);
bool has_sibling_at_time(const Rational &t) const;
const std::string &name() const
{
return name_;
}
void set_name(const std::string &name);
int color() const
{
return color_;
}
void set_color(int c);
bool load(XmlStreamReader *reader);
void save(XmlStreamWriter *writer) const;
private:
TimeRange time_;
std::string name_;
int color_;
TimelineMarkerList *parent_;
friend class TimelineMarkerList;
};
class TimelineMarkerList {
public:
TimelineMarkerList() = default;
inline bool empty() const
{
return markers_.empty();
}
inline size_t size() const
{
return markers_.size();
}
inline TimelineMarker *at(size_t i) const
{
return markers_[i].get();
}
inline TimelineMarker *back() const
{
return markers_.back().get();
}
inline TimelineMarker *front() const
{
return markers_.front().get();
}
bool load(XmlStreamReader *reader);
void save(XmlStreamWriter *writer) const;
/**
* @brief Insert a marker, keeping the list sorted by time
*
* Takes ownership. Replaces the old QObject childEvent
* auto-registration.
*/
void add_marker(std::unique_ptr<TimelineMarker> marker);
/**
* @brief Detach a marker from the list, returning ownership
*
* Returns nullptr if the marker is not in the list.
*/
std::unique_ptr<TimelineMarker> remove_marker(TimelineMarker *m);
TimelineMarker *get_marker_at_time(const Rational &t) const
{
for (const auto &m : markers_) {
if (m->time().in() == t) {
return m.get();
}
}
return nullptr;
}
TimelineMarker *get_closest_marker_to_time(const Rational &t) const
{
TimelineMarker *closest = nullptr;
for (const auto &m : markers_) {
Rational this_diff = rational_abs(m->time().in() - t);
if (closest) {
Rational stored_diff = rational_abs(closest->time().in() - t);
if (this_diff > stored_diff) {
// Since the list is organized by time, if the diff
// increases, assume we are only going to move further
// away from here and there's no need to check
break;
}
}
closest = m.get();
}
return closest;
}
/**
* @brief Re-sort a marker after its time changed
*
* Formerly triggered by the marker's time_changed signal; now called
* directly by TimelineMarker::set_time().
*/
void resort(TimelineMarker *m);
private:
static Rational rational_abs(const Rational &r)
{
return r < Rational(0) ? -r : r;
}
void insert_into_list(std::unique_ptr<TimelineMarker> marker);
std::vector<std::unique_ptr<TimelineMarker>> markers_;
};
class MarkerAddCommand : public UndoCommand {
public:
MarkerAddCommand(TimelineMarkerList *marker_list, const TimeRange &range,
const std::string &name, int color);
MarkerAddCommand(TimelineMarkerList *marker_list,
std::unique_ptr<TimelineMarker> marker);
protected:
virtual void redo() override;
virtual void undo() override;
private:
TimelineMarkerList *marker_list_;
TimelineMarker *added_marker_;
std::unique_ptr<TimelineMarker> memory_manager_;
};
class MarkerRemoveCommand : public UndoCommand {
public:
MarkerRemoveCommand(TimelineMarker *marker, TimelineMarkerList *marker_list);
protected:
virtual void redo() override;
virtual void undo() override;
private:
TimelineMarker *marker_;
TimelineMarkerList *marker_list_;
std::unique_ptr<TimelineMarker> memory_manager_;
};
class MarkerChangeColorCommand : public UndoCommand {
public:
MarkerChangeColorCommand(TimelineMarker *marker, int new_color);
protected:
virtual void redo() override;
virtual void undo() override;
private:
TimelineMarker *marker_;
int old_color_;
int new_color_;
};
class MarkerChangeNameCommand : public UndoCommand {
public:
MarkerChangeNameCommand(TimelineMarker *marker, std::string name);
protected:
virtual void redo() override;
virtual void undo() override;
private:
TimelineMarker *marker_;
std::string old_name_;
std::string new_name_;
};
class MarkerChangeTimeCommand : public UndoCommand {
public:
MarkerChangeTimeCommand(TimelineMarker *marker, const TimeRange &time,
const TimeRange &old_time);
MarkerChangeTimeCommand(TimelineMarker *marker, const TimeRange &time)
: MarkerChangeTimeCommand(marker, time, marker->time())
{
}
protected:
virtual void redo() override;
virtual void undo() override;
private:
TimelineMarker *marker_;
TimeRange old_time_;
TimeRange new_time_;
};
}
#endif // OAK_TIMELINEMARKER_H
+109
View File
@@ -0,0 +1,109 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEUNDOCOMMON_H
#define OAK_TIMELINEUNDOCOMMON_H
#include "node/node.h"
#include "undo/undocommand.h"
namespace olive
{
inline bool node_can_be_removed(OakNodeNode *n)
{
int count = 0;
oaknode_node_output_connection_count(n, &count);
return count == 0;
}
inline bool node_can_be_removed(OakNodeBlock *b)
{
return node_can_be_removed(oaknode_block_as_node(b));
}
inline OakUndoCommand *create_remove_command(OakNodeNode *n)
{
return oaknode_command_create_remove_node(n);
}
inline OakUndoCommand *create_remove_command(OakNodeBlock *b)
{
return oaknode_command_create_remove_node(oaknode_block_as_node(b));
}
inline OakUndoCommand *create_and_run_remove_command(OakNodeNode *n)
{
OakUndoCommand *command = create_remove_command(n);
oakundo_command_redo_now(command);
return command;
}
inline OakUndoCommand *create_and_run_remove_command(OakNodeBlock *b)
{
return create_and_run_remove_command(oaknode_block_as_node(b));
}
inline void free_remove_command(OakUndoCommand *command)
{
oakundo_command_free(command);
}
/**
* @brief Wrap an oakundo C command handle as an olive::UndoCommand so it
* can live in this module's C++ command trees
*/
class CHandleCommandWrapper : public UndoCommand {
public:
CHandleCommandWrapper(OakUndoCommand *command)
: command_(command)
{
}
virtual ~CHandleCommandWrapper() override
{
if (command_) {
oakundo_command_free(command_);
}
}
protected:
virtual void redo() override
{
if (command_) {
oakundo_command_redo_now(command_);
}
}
virtual void undo() override
{
if (command_) {
oakundo_command_undo_now(command_);
}
}
private:
OakUndoCommand *command_;
};
}
#endif // OAK_TIMELINEUNDOCOMMON_H
File diff suppressed because it is too large Load Diff
+373
View File
@@ -0,0 +1,373 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEUNDOGENERAL_H
#define OAK_TIMELINEUNDOGENERAL_H
#include <map>
#include <string>
#include <vector>
#include <olive/core/core.h>
#include "node/block.h"
#include "node/sequence.h"
#include "node/track.h"
#include "timelineundosplit.h"
#include "undocommand.h"
using namespace olive::core;
namespace olive
{
class BlockResizeCommand : public UndoCommand {
public:
BlockResizeCommand(OakNodeBlock *block, Rational new_length)
: block_(block)
, new_length_(new_length)
{
}
protected:
virtual void redo() override;
virtual void undo() override;
private:
OakNodeBlock *block_;
Rational old_length_;
Rational new_length_;
};
class BlockResizeWithMediaInCommand : public UndoCommand {
public:
BlockResizeWithMediaInCommand(OakNodeBlock *block, Rational new_length)
: block_(block)
, new_length_(new_length)
{
}
protected:
virtual void redo();
virtual void undo();
private:
OakNodeBlock *block_;
Rational old_length_;
Rational new_length_;
};
class BlockSetMediaInCommand : public UndoCommand {
public:
BlockSetMediaInCommand(OakNodeBlock *block, Rational new_media_in)
: block_(block)
, new_media_in_(new_media_in)
{
}
protected:
virtual void redo();
virtual void undo();
private:
OakNodeBlock *block_;
Rational old_media_in_;
Rational new_media_in_;
};
class TimelineAddTrackCommand : public UndoCommand {
public:
TimelineAddTrackCommand(OakNodeTrackList *timeline);
TimelineAddTrackCommand(OakNodeTrackList *timeline, bool automerge_tracks);
virtual ~TimelineAddTrackCommand() override;
static OakNodeTrack *run_immediately(OakNodeTrackList *timeline)
{
TimelineAddTrackCommand c(timeline);
c.redo();
return c.track();
}
static OakNodeTrack *run_immediately(OakNodeTrackList *timeline,
bool automerge)
{
TimelineAddTrackCommand c(timeline, automerge);
c.redo();
return c.track();
}
OakNodeTrack *track() const
{
return track_;
}
protected:
virtual void redo() override;
virtual void undo() override;
private:
OakNodeTrackList *timeline_;
OakNodeTrack *track_;
OakNodeNode *merge_;
std::string base_input_;
std::string blend_input_;
std::string direct_input_;
OakUndoCommand *position_command_;
bool automerge_tracks_;
// Ownership: true while the node is detached from the graph and this
// command must free it on destruction
bool track_orphaned_;
bool merge_orphaned_;
};
class TimelineRemoveTrackCommand : public UndoCommand {
public:
TimelineRemoveTrackCommand(OakNodeTrack *track)
: track_(track)
, list_(nullptr)
, index_(0)
, remove_command_(nullptr)
{
}
virtual ~TimelineRemoveTrackCommand() override;
protected:
virtual void prepare() override;
virtual void redo() override;
virtual void undo() override;
private:
OakNodeTrack *track_;
OakNodeTrackList *list_;
int index_;
OakUndoCommand *remove_command_;
};
class TransitionRemoveCommand : public UndoCommand {
public:
TransitionRemoveCommand(OakNodeBlock *block, bool remove_from_graph)
: block_(block)
, track_(nullptr)
, out_block_(nullptr)
, in_block_(nullptr)
, remove_from_graph_(remove_from_graph)
, remove_command_(nullptr)
{
}
virtual ~TransitionRemoveCommand() override;
protected:
virtual void redo() override;
virtual void undo() override;
private:
OakNodeBlock *block_;
OakNodeTrack *track_;
OakNodeBlock *out_block_;
OakNodeBlock *in_block_;
bool remove_from_graph_;
OakUndoCommand *remove_command_;
};
class TrackReplaceBlockWithGapCommand : public UndoCommand {
public:
TrackReplaceBlockWithGapCommand(OakNodeTrack *track, OakNodeBlock *block,
bool handle_transitions = true)
: track_(track)
, block_(block)
, existing_gap_(nullptr)
, existing_merged_gap_(nullptr)
, existing_gap_precedes_(false)
, our_gap_(nullptr)
, handle_transitions_(handle_transitions)
, our_gap_orphaned_(false)
, merged_gap_orphaned_(false)
{
}
virtual ~TrackReplaceBlockWithGapCommand() override;
protected:
virtual void redo() override;
virtual void undo() override;
private:
void create_remove_transition_command_if_necessary(bool next);
OakNodeTrack *track_;
OakNodeBlock *block_;
OakNodeBlock *existing_gap_;
OakNodeBlock *existing_merged_gap_;
bool existing_gap_precedes_;
OakNodeBlock *our_gap_;
bool handle_transitions_;
bool our_gap_orphaned_;
bool merged_gap_orphaned_;
std::vector<TransitionRemoveCommand *> transition_remove_commands_;
};
class BlockEnableDisableCommand : public UndoCommand {
public:
BlockEnableDisableCommand(OakNodeBlock *block, bool enabled)
: block_(block)
, new_enabled_(enabled)
{
old_enabled_ = false;
oaknode_block_get_enabled(block_, &old_enabled_);
}
protected:
virtual void redo() override
{
oaknode_block_set_enabled(block_, new_enabled_);
}
virtual void undo() override
{
oaknode_block_set_enabled(block_, old_enabled_);
}
private:
OakNodeBlock *block_;
int old_enabled_;
int new_enabled_;
};
class TrackListInsertGaps : public UndoCommand {
public:
TrackListInsertGaps(OakNodeTrackList *track_list, const Rational &point,
const Rational &length)
: track_list_(track_list)
, point_(point)
, length_(length)
, split_command_(nullptr)
{
}
virtual ~TrackListInsertGaps() override;
protected:
virtual void prepare() override;
virtual void redo() override;
virtual void undo() override;
private:
OakNodeTrackList *track_list_;
Rational point_;
Rational length_;
std::vector<OakNodeTrack *> working_tracks_;
std::vector<OakNodeBlock *> gaps_to_extend_;
struct AddGap {
OakNodeBlock *gap;
bool orphaned;
OakNodeBlock *before;
OakNodeTrack *track;
};
std::vector<AddGap> gaps_added_;
BlockSplitPreservingLinksCommand *split_command_;
};
class TimelineAddDefaultTransitionCommand : public UndoCommand {
public:
TimelineAddDefaultTransitionCommand(
const std::vector<OakNodeBlock *> &clips, const Rational &timebase)
: clips_(clips)
, timebase_(timebase)
{
}
virtual ~TimelineAddDefaultTransitionCommand() override
{
for (UndoCommand *c : commands_) {
delete c;
}
}
protected:
virtual void prepare() override;
virtual void redo() override
{
for (UndoCommand *c : commands_) {
c->redo_now();
}
}
virtual void undo() override
{
for (auto it = commands_.rbegin(); it != commands_.rend(); it++) {
(*it)->undo_now();
}
}
private:
enum CreateTransitionMode { k_in, k_out, k_out_dual };
void add_transition(OakNodeBlock *c, CreateTransitionMode mode);
void adjust_clip_length(OakNodeBlock *c, const Rational &transition_length,
bool out);
void validate_transition_length(OakNodeBlock *c,
Rational &transition_length);
std::vector<OakNodeBlock *> clips_;
Rational timebase_;
std::vector<UndoCommand *> commands_;
std::map<OakNodeBlock *, Rational> lengths_;
};
}
#endif // OAK_TIMELINEUNDOGENERAL_H
+423
View File
@@ -0,0 +1,423 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "timelineundopointer.h"
#include "timelineundocommon.h"
#include "timelineutil.h"
#include "undo/undocommand.h"
namespace olive
{
//
// BlockTrimCommand
//
BlockTrimCommand::~BlockTrimCommand()
{
if (deleted_adjacent_command_) {
free_remove_command(deleted_adjacent_command_);
}
if (adjacent_orphaned_ && adjacent_) {
oaknode_block_free(adjacent_);
}
}
void BlockTrimCommand::redo()
{
if (doing_nothing_) {
return;
}
if (mode_ == Timeline::k_trim_in) {
block_set_length_and_media_in(block_, new_length_);
} else {
block_set_length_and_media_out(block_, new_length_);
}
if (needs_adjacent_) {
if (we_created_adjacent_) {
// Add adjacent and insert it
block_add_to_graph(adjacent_, track_);
if (mode_ == Timeline::k_trim_in) {
oaknode_track_insert_block_before(track_, adjacent_, block_);
} else {
oaknode_track_insert_block_after(track_, adjacent_, block_);
}
adjacent_orphaned_ = false;
} else if (we_removed_adjacent_) {
oaknode_track_ripple_remove_block(track_, adjacent_);
// It no longer inputs/outputs anything, remove it
if (remove_block_from_graph_ && node_can_be_removed(adjacent_)) {
if (!deleted_adjacent_command_) {
deleted_adjacent_command_ =
create_and_run_remove_command(adjacent_);
} else {
oakundo_command_redo_now(deleted_adjacent_command_);
}
}
} else {
Rational adjacent_length = block_length(adjacent_) + trim_diff_;
if (mode_ == Timeline::k_trim_in) {
block_set_length_and_media_out(adjacent_, adjacent_length);
} else {
block_set_length_and_media_in(adjacent_, adjacent_length);
}
}
}
}
void BlockTrimCommand::undo()
{
if (doing_nothing_) {
return;
}
// Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer
if (needs_adjacent_) {
if (we_created_adjacent_) {
// Adjacent is ours, just delete it
oaknode_track_ripple_remove_block(track_, adjacent_);
block_remove_from_graph(adjacent_, track_);
adjacent_orphaned_ = true;
} else {
if (we_removed_adjacent_) {
if (deleted_adjacent_command_) {
// We deleted adjacent, restore it now
oakundo_command_undo_now(deleted_adjacent_command_);
}
if (mode_ == Timeline::k_trim_in) {
oaknode_track_insert_block_before(track_, adjacent_,
block_);
} else {
oaknode_track_insert_block_after(track_, adjacent_,
block_);
}
} else {
Rational adjacent_length =
block_length(adjacent_) - trim_diff_;
if (mode_ == Timeline::k_trim_in) {
block_set_length_and_media_out(adjacent_,
adjacent_length);
} else {
block_set_length_and_media_in(adjacent_, adjacent_length);
}
}
}
}
if (mode_ == Timeline::k_trim_in) {
block_set_length_and_media_in(block_, old_length_);
} else {
block_set_length_and_media_out(block_, old_length_);
}
}
void BlockTrimCommand::prepare()
{
// Store old length
old_length_ = block_length(block_);
// Determine if the length isn't changing, in which case we set a flag to do nothing
if ((doing_nothing_ = (old_length_ == new_length_))) {
return;
}
// Will be POSITIVE if trimming shorter and NEGATIVE if trimming longer
trim_diff_ = old_length_ - new_length_;
// Retrieve our adjacent block (or nullptr if none)
if (mode_ == Timeline::k_trim_in) {
adjacent_ = block_previous(block_);
} else {
adjacent_ = block_next(block_);
}
// Ignore when trimming the out with no adjacent, because the user must have trimmed the end
// of the last block in the track, so we don't need to do anything elses
needs_adjacent_ = (mode_ == Timeline::k_trim_in || adjacent_);
if (needs_adjacent_) {
// If we're trimming shorter, we need an adjacent, so check if we have a viable one.
int adjacent_kind = OAKNODE_BLOCK_OTHER;
if (adjacent_) {
oaknode_block_get_kind(adjacent_, &adjacent_kind);
}
we_created_adjacent_ =
(trim_diff_ > 0 && (!adjacent_ || (adjacent_kind !=
OAKNODE_BLOCK_GAP &&
!trim_is_a_roll_edit_)));
if (we_created_adjacent_) {
// We shortened but don't have a viable adjacent to lengthen, so we create one
adjacent_ = oaknode_block_gap_create();
block_set_length_and_media_out(adjacent_, trim_diff_);
adjacent_orphaned_ = true;
} else {
// Determine if we're removing the adjacent
Rational adjacent_length = block_length(adjacent_) + trim_diff_;
we_removed_adjacent_ = adjacent_length.isNull();
}
}
}
//
// TrackSlideCommand
//
TrackSlideCommand::~TrackSlideCommand()
{
if (in_adjacent_remove_command_) {
free_remove_command(in_adjacent_remove_command_);
}
if (out_adjacent_remove_command_) {
free_remove_command(out_adjacent_remove_command_);
}
if (in_adjacent_orphaned_ && in_adjacent_) {
oaknode_block_free(in_adjacent_);
}
if (out_adjacent_orphaned_ && out_adjacent_) {
oaknode_block_free(out_adjacent_);
}
}
void TrackSlideCommand::redo()
{
// We will always have an in adjacent if there was a valid slide
if (we_created_in_adjacent_) {
// We created in adjacent, so all we have to do is insert it
block_add_to_graph(in_adjacent_, track_);
oaknode_track_insert_block_before(track_, in_adjacent_,
blocks_.front());
in_adjacent_orphaned_ = false;
} else if (-movement_ == block_length(in_adjacent_)) {
// Movement will remove in adjacent
oaknode_track_ripple_remove_block(track_, in_adjacent_);
if (node_can_be_removed(in_adjacent_)) {
if (!in_adjacent_remove_command_) {
in_adjacent_remove_command_ =
create_remove_command(in_adjacent_);
}
oakundo_command_redo_now(in_adjacent_remove_command_);
}
we_removed_in_adjacent_ = true;
} else {
// Simply resize adjacent
block_set_length_and_media_out(
in_adjacent_, block_length(in_adjacent_) + movement_);
}
// We may not have an out adjacent if the slide was at the end of the track
if (out_adjacent_) {
if (we_created_out_adjacent_) {
// We created out adjacent, so we just have to insert it
block_add_to_graph(out_adjacent_, track_);
oaknode_track_insert_block_after(track_, out_adjacent_,
blocks_.back());
out_adjacent_orphaned_ = false;
} else if (movement_ == block_length(out_adjacent_)) {
// Movement will remove out adjacent
oaknode_track_ripple_remove_block(track_, out_adjacent_);
if (node_can_be_removed(out_adjacent_)) {
if (!out_adjacent_remove_command_) {
out_adjacent_remove_command_ =
create_remove_command(out_adjacent_);
}
oakundo_command_redo_now(out_adjacent_remove_command_);
}
we_removed_out_adjacent_ = true;
} else {
// Simply resize adjacent
block_set_length_and_media_in(
out_adjacent_,
block_length(out_adjacent_) - movement_);
}
}
}
void TrackSlideCommand::undo()
{
if (we_created_in_adjacent_) {
// We created this, so we can remove it now
oaknode_track_ripple_remove_block(track_, in_adjacent_);
block_remove_from_graph(in_adjacent_, track_);
in_adjacent_orphaned_ = true;
} else if (we_removed_in_adjacent_) {
if (in_adjacent_remove_command_) {
// We removed this, so we can restore it now
oakundo_command_undo_now(in_adjacent_remove_command_);
}
oaknode_track_insert_block_before(track_, in_adjacent_,
blocks_.front());
} else {
// Simply resize adjacent
block_set_length_and_media_out(
in_adjacent_, block_length(in_adjacent_) - movement_);
}
if (out_adjacent_) {
if (we_created_out_adjacent_) {
// We created this, so we can remove it now
oaknode_track_ripple_remove_block(track_, out_adjacent_);
block_remove_from_graph(out_adjacent_, track_);
out_adjacent_orphaned_ = true;
} else if (we_removed_out_adjacent_) {
if (out_adjacent_remove_command_) {
oakundo_command_undo_now(out_adjacent_remove_command_);
}
oaknode_track_insert_block_after(track_, out_adjacent_,
blocks_.back());
} else {
// Simply resize adjacent
block_set_length_and_media_in(
out_adjacent_,
block_length(out_adjacent_) + movement_);
}
}
}
void TrackSlideCommand::prepare()
{
if (!in_adjacent_) {
in_adjacent_ = oaknode_block_gap_create();
block_set_length_and_media_out(in_adjacent_, movement_);
in_adjacent_orphaned_ = true;
we_created_in_adjacent_ = true;
} else {
we_created_in_adjacent_ = false;
}
if (!out_adjacent_ && block_next(blocks_.back())) {
out_adjacent_ = oaknode_block_gap_create();
block_set_length_and_media_out(out_adjacent_, -movement_);
out_adjacent_orphaned_ = true;
we_created_out_adjacent_ = true;
} else {
we_created_out_adjacent_ = false;
}
}
//
// TrackPlaceBlockCommand
//
TrackPlaceBlockCommand::~TrackPlaceBlockCommand()
{
delete ripple_remove_command_;
for (TimelineAddTrackCommand *c : add_track_commands_) {
delete c;
}
if (gap_orphaned_ && gap_) {
oaknode_block_free(gap_);
}
}
void TrackPlaceBlockCommand::redo()
{
// Determine if we need to add tracks
int track_count = 0;
oaknode_tracklist_get_track_count(timeline_, &track_count);
if (track_index_ >= track_count) {
if (add_track_commands_.empty()) {
// First redo, create tracks now
add_track_commands_.resize(track_index_ - track_count + 1);
for (size_t i = 0; i < add_track_commands_.size(); i++) {
add_track_commands_[i] =
new TimelineAddTrackCommand(timeline_);
}
}
for (TimelineAddTrackCommand *c : add_track_commands_) {
c->redo_now();
}
}
OakNodeTrack *track = nullptr;
oaknode_tracklist_get_track_at(timeline_, track_index_, &track);
bool append = (in_ >= track_length(track));
// Check if the placement location is past the end of the timeline
if (append) {
if (in_ > track_length(track)) {
// If so, insert a gap here
if (!gap_) {
gap_ = oaknode_block_gap_create();
block_set_length_and_media_out(gap_,
in_ - track_length(track));
}
block_add_to_graph(gap_, track);
oaknode_track_append_block(track, gap_);
gap_orphaned_ = false;
}
oaknode_track_append_block(track, insert_);
} else {
// Place the Block at this point
if (!ripple_remove_command_) {
ripple_remove_command_ = new TrackRippleRemoveAreaCommand(
track, TimeRange(in_, in_ + block_length(insert_)));
ripple_remove_command_->set_allow_splitting_gaps(true);
}
ripple_remove_command_->redo_now();
oaknode_track_insert_block_after(
track, insert_, ripple_remove_command_->get_insertion_index());
}
}
void TrackPlaceBlockCommand::undo()
{
OakNodeTrack *t = nullptr;
oaknode_tracklist_get_track_at(timeline_, track_index_, &t);
// Firstly, remove our insert
oaknode_track_ripple_remove_block(t, insert_);
if (ripple_remove_command_) {
// If we ripple removed, just undo that
ripple_remove_command_->undo_now();
} else if (gap_) {
oaknode_track_ripple_remove_block(t, gap_);
block_remove_from_graph(gap_, t);
gap_orphaned_ = true;
}
// Remove tracks if we added them
for (auto it = add_track_commands_.rbegin();
it != add_track_commands_.rend(); it++) {
(*it)->undo_now();
}
}
}
+210
View File
@@ -0,0 +1,210 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEUNDOPOINTER_H
#define OAK_TIMELINEUNDOPOINTER_H
#include <vector>
#include <olive/core/core.h>
#include "node/block.h"
#include "node/track.h"
#include "timelinecommon.h"
#include "timelineundogeneral.h"
#include "timelineundoripple.h"
#include "undocommand.h"
using namespace olive::core;
namespace olive
{
/**
* @brief Performs a trim in the timeline that only affects the block and the block adjacent
*
* Changes the length of one block while also changing the length of the block directly adjacent
* to compensate so that the rest of the track is unaffected.
*
* By default, this will only affect the length of gaps. If the adjacent needs to increase its
* length and is not a gap, a gap will be created and inserted to fill that time. This command can
* be set to always trim even if the adjacent clip isn't a gap with set_trim_is_a_roll_edit()
*/
class BlockTrimCommand : public UndoCommand {
public:
BlockTrimCommand(OakNodeTrack *track, OakNodeBlock *block,
Rational new_length, Timeline::MovementMode mode)
: track_(track)
, block_(block)
, new_length_(new_length)
, mode_(mode)
, deleted_adjacent_command_(nullptr)
, trim_is_a_roll_edit_(false)
, remove_block_from_graph_(true)
, doing_nothing_(false)
, adjacent_(nullptr)
, needs_adjacent_(false)
, we_created_adjacent_(false)
, we_removed_adjacent_(false)
, adjacent_orphaned_(false)
{
}
virtual ~BlockTrimCommand() override;
/**
* @brief Set this if the trim should always affect the adjacent clip and not create a gap
*/
void set_trim_is_a_roll_edit(bool e)
{
trim_is_a_roll_edit_ = e;
}
/**
* @brief Set whether adjacent blocks set to zero length should be removed from the whole graph
*
* If an adjacent block's length is set to 0, it's automatically removed from the track. By
* default it also gets removed from the whole graph. Set this to FALSE to disable that
* functionality.
*/
void set_remove_zero_length_from_graph(bool e)
{
remove_block_from_graph_ = e;
}
protected:
virtual void prepare() override;
virtual void redo() override;
virtual void undo() override;
private:
bool doing_nothing_;
Rational trim_diff_;
OakNodeTrack *track_;
OakNodeBlock *block_;
Rational old_length_;
Rational new_length_;
Timeline::MovementMode mode_;
OakNodeBlock *adjacent_;
bool needs_adjacent_;
bool we_created_adjacent_;
bool we_removed_adjacent_;
OakUndoCommand *deleted_adjacent_command_;
bool trim_is_a_roll_edit_;
bool remove_block_from_graph_;
// Owned while the created adjacent gap is detached from the graph
bool adjacent_orphaned_;
};
class TrackSlideCommand : public UndoCommand {
public:
TrackSlideCommand(OakNodeTrack *track,
const std::vector<OakNodeBlock *> &moving_blocks,
OakNodeBlock *in_adjacent, OakNodeBlock *out_adjacent,
const Rational &movement)
: track_(track)
, blocks_(moving_blocks)
, movement_(movement)
, we_created_in_adjacent_(false)
, we_removed_in_adjacent_(false)
, in_adjacent_(in_adjacent)
, in_adjacent_remove_command_(nullptr)
, in_adjacent_orphaned_(false)
, we_created_out_adjacent_(false)
, we_removed_out_adjacent_(false)
, out_adjacent_(out_adjacent)
, out_adjacent_remove_command_(nullptr)
, out_adjacent_orphaned_(false)
{
}
virtual ~TrackSlideCommand() override;
protected:
virtual void prepare() override;
virtual void redo() override;
virtual void undo() override;
private:
OakNodeTrack *track_;
std::vector<OakNodeBlock *> blocks_;
Rational movement_;
bool we_created_in_adjacent_;
bool we_removed_in_adjacent_;
OakNodeBlock *in_adjacent_;
OakUndoCommand *in_adjacent_remove_command_;
bool in_adjacent_orphaned_;
bool we_created_out_adjacent_;
bool we_removed_out_adjacent_;
OakNodeBlock *out_adjacent_;
OakUndoCommand *out_adjacent_remove_command_;
bool out_adjacent_orphaned_;
};
/**
* @brief Destructively places `block` at the in point `start`
*
* The Block is guaranteed to be placed at the starting point specified. If there are Blocks in this area, they are
* either trimmed or removed to make space for this Block. Additionally, if the Block is placed beyond the end of
* the Sequence, a GapBlock is inserted to compensate.
*/
class TrackPlaceBlockCommand : public UndoCommand {
public:
TrackPlaceBlockCommand(OakNodeTrackList *timeline, int track,
OakNodeBlock *block, Rational in)
: timeline_(timeline)
, track_index_(track)
, in_(in)
, gap_(nullptr)
, gap_orphaned_(false)
, insert_(block)
, ripple_remove_command_(nullptr)
{
}
virtual ~TrackPlaceBlockCommand() override;
protected:
virtual void redo() override;
virtual void undo() override;
private:
OakNodeTrackList *timeline_;
int track_index_;
Rational in_;
OakNodeBlock *gap_;
bool gap_orphaned_;
OakNodeBlock *insert_;
std::vector<TimelineAddTrackCommand *> add_track_commands_;
TrackRippleRemoveAreaCommand *ripple_remove_command_;
};
}
#endif // OAK_TIMELINEUNDOPOINTER_H
+623
View File
@@ -0,0 +1,623 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "timelineundoripple.h"
#include <algorithm>
#include <cstdio>
#include "timelineundocommon.h"
#include "timelineutil.h"
#include "undo/undocommand.h"
namespace olive
{
//
// TrackRippleRemoveAreaCommand
//
TrackRippleRemoveAreaCommand::TrackRippleRemoveAreaCommand(
OakNodeTrack *track, const TimeRange &range)
: track_(track)
, range_(range)
, insert_previous_(nullptr)
, allow_splitting_gaps_(false)
, splice_split_command_(nullptr)
{
trim_out_.block = nullptr;
trim_in_.block = nullptr;
}
TrackRippleRemoveAreaCommand::~TrackRippleRemoveAreaCommand()
{
delete splice_split_command_;
for (OakUndoCommand *c : remove_block_commands_) {
free_remove_command(c);
}
}
void TrackRippleRemoveAreaCommand::prepare()
{
int n, d;
rat_nd(range_.in(), &n, &d);
// Determine precisely what will be happening to these tracks
OakNodeBlock *first_block = nullptr;
oaknode_track_get_nearest_block_before_or_at(track_, n, d, &first_block);
if (!first_block) {
// No blocks at this time, nothing to be done on this track
return;
}
// Determine if this first block is getting trimmed or removed
bool first_block_is_out_trimmed = block_in(first_block) < range_.in();
bool first_block_is_in_trimmed = block_out(first_block) > range_.out();
// Set's the block that any insert command should insert AFTER. If the first block is not
// getting out-trimmed, that means first block is either getting removed or in-trimmed, which
// means any insert should happen before it
insert_previous_ =
first_block_is_out_trimmed ? first_block : block_previous(first_block);
// If it's getting trimmed, determine if it's actually getting spliced
if (first_block_is_out_trimmed && first_block_is_in_trimmed) {
int kind = OAKNODE_BLOCK_OTHER;
oaknode_block_get_kind(first_block, &kind);
if (!allow_splitting_gaps_ && kind == OAKNODE_BLOCK_GAP) {
// As a rule, we don't split gaps, so we just treat it as a trim of the range requested
trim_out_ = { first_block, block_length(first_block),
block_length(first_block) - range_.length() };
} else {
// This block is getting spliced, so we'll handle that later
splice_split_command_ =
new BlockSplitCommand(first_block, range_.in());
}
} else {
// It's just getting trimmed or removed, so we'll append that operation
if (first_block_is_out_trimmed) {
trim_out_ = { first_block, block_length(first_block),
block_length(first_block) -
(block_out(first_block) - range_.in()) };
} else if (first_block_is_in_trimmed) {
// Block is getting in trimmed
trim_in_ = { first_block, block_length(first_block),
block_length(first_block) -
(range_.out() - block_in(first_block)) };
} else {
// We know for sure this block is within the range so it will be removed
removals_.push_back(
RemoveOperation({ first_block, block_previous(first_block) }));
}
// If the first block is getting in trimmed, we're already at the end of our range
if (!first_block_is_in_trimmed) {
// Loop through the rest of the blocks and determine what to do with those
for (OakNodeBlock *next = block_next(first_block); next;
next = block_next(next)) {
bool trimming = (block_out(next) > range_.out());
if (trimming) {
trim_in_ = { next, block_length(next),
block_length(next) -
(range_.out() - block_in(next)) };
break;
} else {
removals_.push_back(
RemoveOperation({ next, block_previous(next) }));
if (block_out(next) == range_.out()) {
break;
}
}
}
}
}
}
void TrackRippleRemoveAreaCommand::redo()
{
if (splice_split_command_) {
// We're just splicing
splice_split_command_->redo_now();
// Trim the in of the split
OakNodeBlock *split = splice_split_command_->new_block();
block_set_length_and_media_in(
split, block_length(split) - (range_.out() - block_in(split)));
} else {
if (trim_out_.block) {
block_set_length_and_media_out(trim_out_.block,
trim_out_.new_length);
}
if (trim_in_.block) {
block_set_length_and_media_in(trim_in_.block, trim_in_.new_length);
}
// Perform removals
if (!removals_.empty()) {
for (const RemoveOperation &op : removals_) {
// Ripple remove them all first
oaknode_track_ripple_remove_block(track_, op.block);
}
// Create undo commands for node removals where possible
if (remove_block_commands_.empty()) {
for (const RemoveOperation &op : removals_) {
if (node_can_be_removed(op.block)) {
remove_block_commands_.push_back(
create_remove_command(op.block));
}
}
}
for (OakUndoCommand *c : remove_block_commands_) {
oakundo_command_redo_now(c);
}
}
}
}
void TrackRippleRemoveAreaCommand::undo()
{
if (splice_split_command_) {
splice_split_command_->undo_now();
} else {
if (trim_out_.block) {
block_set_length_and_media_out(trim_out_.block,
trim_out_.old_length);
}
if (trim_in_.block) {
block_set_length_and_media_in(trim_in_.block, trim_in_.old_length);
}
// Un-remove any blocks
for (auto it = remove_block_commands_.rbegin();
it != remove_block_commands_.rend(); it++) {
oakundo_command_undo_now(*it);
}
for (const RemoveOperation &op : removals_) {
oaknode_track_insert_block_after(track_, op.block, op.before);
}
}
}
//
// TrackListRippleRemoveAreaCommand
//
void TrackListRippleRemoveAreaCommand::prepare()
{
int count = 0;
oaknode_tracklist_get_track_count(list_, &count);
for (int i = 0; i < count; i++) {
OakNodeTrack *track = nullptr;
oaknode_tracklist_get_track_at(list_, i, &track);
if (!track) {
continue;
}
int locked = 0;
oaknode_track_get_locked(track, &locked);
if (locked) {
continue;
}
TrackRippleRemoveAreaCommand *c =
new TrackRippleRemoveAreaCommand(track, range_);
commands_.push_back(c);
working_tracks_.push_back(track);
}
}
void TrackListRippleRemoveAreaCommand::redo()
{
for (TrackRippleRemoveAreaCommand *c : commands_) {
c->redo_now();
}
}
void TrackListRippleRemoveAreaCommand::undo()
{
for (TrackRippleRemoveAreaCommand *c : commands_) {
c->undo_now();
}
}
//
// TimelineRippleRemoveAreaCommand
//
TimelineRippleRemoveAreaCommand::TimelineRippleRemoveAreaCommand(
OakNodeSequence *timeline, Rational in, Rational out)
{
for (int i = 0; i < OAKNODE_TRACK_TYPE_COUNT; i++) {
OakNodeTrackList *list = nullptr;
oaknode_sequence_get_track_list(timeline, i, &list);
if (list) {
add_child(new TrackListRippleRemoveAreaCommand(list, in, out));
}
}
}
//
// TrackListRippleToolCommand
//
TrackListRippleToolCommand::TrackListRippleToolCommand(
OakNodeTrackList *track_list,
const std::map<OakNodeTrack *, RippleInfo> &info,
const Rational &ripple_movement,
const Timeline::MovementMode &movement_mode)
: track_list_(track_list)
, info_(info)
, ripple_movement_(ripple_movement)
, movement_mode_(movement_mode)
{
}
TrackListRippleToolCommand::~TrackListRippleToolCommand()
{
// Free any gaps still owned by this command (detached from the graph)
for (auto &pair : working_data_) {
WorkingData &wd = pair.second;
if (wd.created_gap && wd.created_gap_orphaned) {
oaknode_block_free(wd.created_gap);
}
if (wd.removed_gap && wd.removed_gap_orphaned) {
oaknode_block_free(wd.removed_gap);
}
}
}
void TrackListRippleToolCommand::ripple(bool redo)
{
if (info_.empty()) {
return;
}
// The following variables are used to determine how much of the cache to invalidate
// If we can shift, we will shift from the latest out before the ripple to the latest out after,
// since those sections will be unchanged by this ripple
Rational pre_latest_out = RATIONAL_MIN;
Rational post_latest_out = RATIONAL_MIN;
// Make timeline changes
for (const auto &pair : info_) {
OakNodeTrack *track = pair.first;
const RippleInfo &info = pair.second;
WorkingData working_data = working_data_[track];
OakNodeBlock *b = info.block;
// Generate block length
Rational new_block_length;
Rational operation_movement = ripple_movement_;
if (movement_mode_ == Timeline::k_trim_in) {
operation_movement = -operation_movement;
}
if (!redo) {
operation_movement = -operation_movement;
}
if (b) {
new_block_length = block_length(b) + operation_movement;
}
Rational pre_shift;
Rational post_shift;
if (info.append_gap) {
// Rather than rippling the referenced block, we'll insert a gap and ripple with that
OakNodeBlock *gap = working_data.created_gap;
if (redo) {
if (!gap) {
gap = oaknode_block_gap_create();
block_set_length_and_media_out(
gap, ripple_movement_ < Rational(0)
? -ripple_movement_
: ripple_movement_);
working_data.created_gap = gap;
working_data.created_gap_orphaned = true;
}
block_add_to_graph(gap, track);
oaknode_track_insert_block_before(track, gap, b);
working_data.created_gap_orphaned = false;
// As an insertion, we will shift from the gap's in to the gap's out
pre_shift = block_in(gap);
post_shift = block_out(gap);
working_data.earliest_point_of_change = block_in(gap);
} else {
// As a removal, we will shift from the gap's out to the gap's in
pre_shift = block_out(gap);
post_shift = block_in(gap);
oaknode_track_ripple_remove_block(track, gap);
block_remove_from_graph(gap, track);
working_data.created_gap_orphaned = true;
}
} else if ((redo && new_block_length.isNull()) ||
(!redo && !block_track(b))) {
// The ripple is the length of this block. We assume that for this to happen, it must have
// been a gap that we will now remove.
if (redo) {
// The earliest point changes will happen is at the start of this block
working_data.earliest_point_of_change = block_in(b);
// As a removal, we will be shifting from the out point to the in point
pre_shift = block_out(b);
post_shift = block_in(b);
// Remove gap from track and from graph
working_data.removed_gap = b;
working_data.removed_gap_after = block_previous(b);
oaknode_track_ripple_remove_block(track, b);
block_remove_from_graph(b, track);
working_data.removed_gap_orphaned = true;
} else {
// Restore gap to graph and track
block_add_to_graph(b, track);
oaknode_track_insert_block_after(
track, b, working_data.removed_gap_after);
working_data.removed_gap_orphaned = false;
// The earliest point changes will happen is at the start of this block
working_data.earliest_point_of_change = block_in(b);
// As an insert, we will be shifting from the block's in point to its out point
pre_shift = block_in(b);
post_shift = block_out(b);
}
} else {
// Store old length
working_data.old_length = block_length(b);
if (movement_mode_ == Timeline::k_trim_in) {
// The earliest point changes will occur is in point of this bloc
working_data.earliest_point_of_change = block_in(b);
// Undo the trim in inversion we do above, this will still be inverted accurately for
// undoing where appropriate
Rational inverted = -operation_movement;
if (inverted > 0) {
pre_shift = block_in(b) + inverted;
post_shift = block_in(b);
} else {
pre_shift = block_in(b);
post_shift = block_in(b) - inverted;
}
// Update length
block_set_length_and_media_in(b, new_block_length);
} else {
// The earliest point changes will occur is the out point if trimming out or the in point
// if trimming in
working_data.earliest_point_of_change = block_out(b);
// The latest out before the ripple is this block's current out point
pre_shift = block_out(b);
// Update length
block_set_length_and_media_out(b, new_block_length);
// The latest out after the ripple is this block's out point after the length change
post_shift = block_out(b);
}
}
working_data_[track] = working_data;
pre_latest_out = std::max(pre_latest_out, pre_shift);
post_latest_out = std::max(post_latest_out, post_shift);
}
}
//
// TimelineRippleDeleteGapsAtRegionsCommand
//
namespace
{
bool is_gap(OakNodeBlock *b)
{
if (!b) {
return false;
}
int kind = OAKNODE_BLOCK_OTHER;
oaknode_block_get_kind(b, &kind);
return kind == OAKNODE_BLOCK_GAP;
}
} // namespace
void TimelineRippleDeleteGapsAtRegionsCommand::prepare()
{
size_t max_gaps = 0;
std::map<OakNodeTrack *, std::vector<RemovalRequest>> requested_gaps;
// Convert regions to gaps
for (const auto &region : regions_) {
OakNodeTrack *track = region.first;
const TimeRange &range = region.second;
int n, d;
rat_nd(range.in(), &n, &d);
OakNodeBlock *block = nullptr;
oaknode_track_get_nearest_block_before_or_at(track, n, d, &block);
if (is_gap(block)) {
std::vector<RemovalRequest> &gaps_on_track =
requested_gaps[track];
RemovalRequest this_req = { block, range };
// Insertion sort
bool inserted = false;
for (size_t i = 0; i < gaps_on_track.size(); i++) {
if (gaps_on_track.at(i).range.in() < range.in()) {
gaps_on_track.insert(gaps_on_track.begin() + i, this_req);
inserted = true;
break;
}
}
if (!inserted) {
gaps_on_track.push_back(this_req);
}
max_gaps = std::max(max_gaps, gaps_on_track.size());
} else {
fprintf(stderr, "Failed to find corresponding gap to region\n");
}
}
// For each gap on each track, find a corresponding gap on every other track (which may include
// a requested gap) to ripple in order to keep everything synchronized
std::map<OakNodeBlock *, Rational> gap_lengths;
for (size_t gap_index = 0; gap_index < max_gaps; gap_index++) {
Rational earliest_point = RATIONAL_MAX;
Rational ripple_length = RATIONAL_MAX;
Rational latest_point = RATIONAL_MIN;
for (const auto &pair : requested_gaps) {
const std::vector<RemovalRequest> &gaps_on_track = pair.second;
if (gap_index < gaps_on_track.size()) {
const RemovalRequest &gap = gaps_on_track.at(gap_index);
earliest_point = std::min(earliest_point, gap.range.in());
ripple_length = std::min(ripple_length, gap.range.length());
latest_point = std::max(latest_point, gap.range.out());
}
}
// Determine which gaps will be involved in this operation
std::vector<OakNodeBlock *> gaps;
int track_count = 0;
oaknode_sequence_get_all_track_count(timeline_, &track_count);
for (int ti = 0; ti < track_count; ti++) {
OakNodeTrack *track = nullptr;
oaknode_sequence_get_all_track_at(timeline_, ti, &track);
if (!track) {
continue;
}
int locked = 0;
oaknode_track_get_locked(track, &locked);
if (locked) {
continue;
}
auto req_it = requested_gaps.find(track);
const std::vector<RemovalRequest> empty_list;
const std::vector<RemovalRequest> &requested_gaps_on_track =
req_it != requested_gaps.end() ? req_it->second : empty_list;
OakNodeBlock *gap = nullptr;
if (gap_index < requested_gaps_on_track.size()) {
// A requested gap was at this index, use it
gap = requested_gaps_on_track.at(gap_index).gap;
} else {
// No requested gap was at this index, find one
int n, d;
rat_nd(earliest_point, &n, &d);
OakNodeBlock *block = nullptr;
oaknode_track_get_nearest_block_after_or_at(track, n, d,
&block);
if (block) {
// Found a block, test if it's a gap
if (is_gap(block)) {
gap = block;
} else {
if (block_in(block) == earliest_point) {
OakNodeBlock *next = block_next(block);
if (is_gap(next)) {
gap = next;
} else {
ripple_length = 0;
}
} else {
OakNodeBlock *prev = block_previous(block);
if (is_gap(prev)) {
gap = prev;
} else {
ripple_length = 0;
}
}
}
} else {
// Assume track finishes here and track won't be affected by this operation
}
}
if (gap) {
gaps.push_back(gap);
if (!gap_lengths.count(gap)) {
gap_lengths[gap] = block_length(gap);
}
ripple_length = std::min(ripple_length, gap_lengths[gap]);
}
if (ripple_length == 0) {
break;
}
}
if (ripple_length > 0) {
for (OakNodeBlock *gap : gaps) {
if (gap_lengths[gap] == ripple_length) {
commands_.push_back(new TrackRippleRemoveBlockCommand(
block_track(gap), gap));
} else {
gap_lengths[gap] -= ripple_length;
commands_.push_back(
new BlockResizeCommand(gap, gap_lengths[gap]));
}
}
}
}
}
void TimelineRippleDeleteGapsAtRegionsCommand::redo()
{
for (UndoCommand *c : commands_) {
c->redo_now();
}
}
void TimelineRippleDeleteGapsAtRegionsCommand::undo()
{
for (auto it = commands_.rbegin(); it != commands_.rend(); it++) {
(*it)->undo_now();
}
}
}
+243
View File
@@ -0,0 +1,243 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEUNDORIPPLE_H
#define OAK_TIMELINEUNDORIPPLE_H
#include <map>
#include <utility>
#include <vector>
#include <olive/core/core.h>
#include "node/block.h"
#include "node/sequence.h"
#include "node/track.h"
#include "timelinecommon.h"
#include "timelineundogeneral.h"
#include "timelineundosplit.h"
#include "timelineundotrack.h"
#include "undocommand.h"
using namespace olive::core;
namespace olive
{
/**
* @brief Clears the area between in and out
*
* The area between `in` and `out` is guaranteed to be freed. Blocks are trimmed and removed to free this space.
* By default, nothing takes this area meaning all subsequent clips are pushed backward, however you can specify
* a block to insert at the `in` point. No checking is done to ensure `insert` is the same length as `in` to `out`.
*/
class TrackRippleRemoveAreaCommand : public UndoCommand {
public:
TrackRippleRemoveAreaCommand(OakNodeTrack *track, const TimeRange &range);
virtual ~TrackRippleRemoveAreaCommand() override;
/**
* @brief Block to insert after if you want to insert something between this ripple
*/
OakNodeBlock *get_insertion_index() const
{
return insert_previous_;
}
OakNodeBlock *get_spliced_block() const
{
if (splice_split_command_) {
return splice_split_command_->new_block();
}
return nullptr;
}
void set_allow_splitting_gaps(bool e)
{
allow_splitting_gaps_ = e;
}
protected:
virtual void prepare() override;
virtual void redo() override;
virtual void undo() override;
private:
struct TrimOperation {
OakNodeBlock *block;
Rational old_length;
Rational new_length;
};
struct RemoveOperation {
OakNodeBlock *block;
OakNodeBlock *before;
};
OakNodeTrack *track_;
TimeRange range_;
TrimOperation trim_out_;
std::vector<RemoveOperation> removals_;
TrimOperation trim_in_;
OakNodeBlock *insert_previous_;
bool allow_splitting_gaps_;
BlockSplitCommand *splice_split_command_;
std::vector<OakUndoCommand *> remove_block_commands_;
};
class TrackListRippleRemoveAreaCommand : public UndoCommand {
public:
TrackListRippleRemoveAreaCommand(OakNodeTrackList *list, Rational in,
Rational out)
: list_(list)
, range_(in, out)
{
}
virtual ~TrackListRippleRemoveAreaCommand() override
{
for (TrackRippleRemoveAreaCommand *c : commands_) {
delete c;
}
}
protected:
virtual void prepare() override;
virtual void redo() override;
virtual void undo() override;
private:
OakNodeTrackList *list_;
std::vector<OakNodeTrack *> working_tracks_;
TimeRange range_;
std::vector<TrackRippleRemoveAreaCommand *> commands_;
};
class TimelineRippleRemoveAreaCommand : public MultiUndoCommand {
public:
TimelineRippleRemoveAreaCommand(OakNodeSequence *timeline, Rational in,
Rational out);
};
class TrackListRippleToolCommand : public UndoCommand {
public:
struct RippleInfo {
OakNodeBlock *block;
bool append_gap;
};
TrackListRippleToolCommand(
OakNodeTrackList *track_list,
const std::map<OakNodeTrack *, RippleInfo> &info,
const Rational &ripple_movement,
const Timeline::MovementMode &movement_mode);
virtual ~TrackListRippleToolCommand() override;
protected:
virtual void redo() override
{
ripple(true);
}
virtual void undo() override
{
ripple(false);
}
private:
void ripple(bool redo);
OakNodeTrackList *track_list_;
std::map<OakNodeTrack *, RippleInfo> info_;
Rational ripple_movement_;
Timeline::MovementMode movement_mode_;
struct WorkingData {
OakNodeBlock *created_gap = nullptr;
bool created_gap_orphaned = false;
OakNodeBlock *removed_gap = nullptr;
bool removed_gap_orphaned = false;
OakNodeBlock *removed_gap_after = nullptr;
Rational old_length;
Rational earliest_point_of_change;
};
std::map<OakNodeTrack *, WorkingData> working_data_;
};
class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand {
public:
using RangeList = std::vector<std::pair<OakNodeTrack *, TimeRange>>;
TimelineRippleDeleteGapsAtRegionsCommand(OakNodeSequence *vo,
const RangeList &regions)
: timeline_(vo)
, regions_(regions)
{
}
virtual ~TimelineRippleDeleteGapsAtRegionsCommand() override
{
for (UndoCommand *c : commands_) {
delete c;
}
}
bool has_commands() const
{
return !commands_.empty();
}
protected:
virtual void prepare() override;
virtual void redo() override;
virtual void undo() override;
private:
OakNodeSequence *timeline_;
RangeList regions_;
std::vector<UndoCommand *> commands_;
struct RemovalRequest {
OakNodeBlock *gap;
TimeRange range;
};
};
}
#endif // OAK_TIMELINEUNDORIPPLE_H
+308
View File
@@ -0,0 +1,308 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "timelineundosplit.h"
#include <cassert>
#include "node/node.h"
#include "undo/undostack.h"
namespace olive
{
namespace
{
void rat_nd(const Rational &r, int *n, int *d)
{
*n = r.numerator();
*d = r.denominator();
}
/**
* @brief oaknode-block-link undo command (replaces NodeLinkCommand from
* oaknode's nodeundo, which does not cross the module boundary)
*/
class BlockLinkUndoCommand : public UndoCommand {
public:
BlockLinkUndoCommand(OakNodeBlock *a, OakNodeBlock *b, bool link)
: a_(a)
, b_(b)
, link_(link)
{
}
protected:
virtual void redo() override
{
if (link_) {
oaknode_block_link(a_, b_);
} else {
oaknode_block_unlink(a_, b_);
}
}
virtual void undo() override
{
if (link_) {
oaknode_block_unlink(a_, b_);
} else {
oaknode_block_link(a_, b_);
}
}
private:
OakNodeBlock *a_;
OakNodeBlock *b_;
bool link_;
};
} // namespace
//
// BlockSplitCommand
//
BlockSplitCommand::~BlockSplitCommand()
{
if (reconnect_tree_command_) {
oakundo_command_free(reconnect_tree_command_);
}
}
void BlockSplitCommand::prepare()
{
OakNodeNode *copy = oaknode_node_copy_in_graph(
oaknode_block_as_node(block_), &reconnect_tree_command_);
new_block_ = oaknode_block_from_node(copy);
}
void BlockSplitCommand::redo()
{
int n, d;
oaknode_block_get_length(block_, &n, &d);
old_length_ = Rational(n, d);
int block_in_n, block_in_d, block_out_n, block_out_d;
oaknode_block_get_in(block_, &block_in_n, &block_in_d);
oaknode_block_get_out(block_, &block_out_n, &block_out_d);
Rational block_in(block_in_n, block_in_d);
Rational block_out(block_out_n, block_out_d);
assert(point_ > block_in && point_ < block_out);
if (reconnect_tree_command_) {
oakundo_command_redo_now(reconnect_tree_command_);
}
// Determine our new lengths
Rational new_length = point_ - block_in;
Rational new_part_length = block_out - point_;
// Begin an operation
OakNodeTrack *track = nullptr;
oaknode_block_get_track(block_, &track);
// Set lengths
rat_nd(new_length, &n, &d);
oaknode_block_set_length_and_media_out(block_, n, d);
rat_nd(new_part_length, &n, &d);
oaknode_block_set_length_and_media_in(new_block_, n, d);
// Insert new block
oaknode_track_insert_block_after(track, new_block_, block_);
oaknode_clip_add_cache_passthrough_from(new_block_, block_);
// If the block had an out transition, we move it to the new block
moved_transition_ = nullptr;
moved_transition_input_.clear();
OakNodeBlock *next = nullptr;
oaknode_block_get_next(new_block_, &next);
int next_kind = OAKNODE_BLOCK_OTHER;
if (next) {
oaknode_block_get_kind(next, &next_kind);
}
if (next_kind == OAKNODE_BLOCK_TRANSITION) {
// A transition is a node connected to the old block's output
int conn_count = 0;
oaknode_node_output_connection_count(
oaknode_block_as_node(block_), &conn_count);
for (int i = 0; i < conn_count; i++) {
OakNodeNode *conn_node = nullptr;
oaknode_node_output_connection_node_at(
oaknode_block_as_node(block_), i, &conn_node);
if (oaknode_block_from_node(conn_node) != next) {
continue;
}
char id_buf[64];
if (oaknode_node_output_connection_input_id_at(
oaknode_block_as_node(block_), i, id_buf,
sizeof(id_buf)) < 0) {
continue;
}
moved_transition_ = next;
moved_transition_input_ = id_buf;
oaknode_node_disconnect(oaknode_block_as_node(next), id_buf);
oaknode_node_connect(oaknode_block_as_node(new_block_),
oaknode_block_as_node(next),
id_buf);
break;
}
}
}
void BlockSplitCommand::undo()
{
OakNodeTrack *track = nullptr;
oaknode_block_get_track(block_, &track);
if (moved_transition_) {
oaknode_node_disconnect(oaknode_block_as_node(moved_transition_),
moved_transition_input_.c_str());
oaknode_node_connect(oaknode_block_as_node(block_),
oaknode_block_as_node(moved_transition_),
moved_transition_input_.c_str());
}
int n, d;
rat_nd(old_length_, &n, &d);
oaknode_block_set_length_and_media_out(block_, n, d);
oaknode_track_ripple_remove_block(track, new_block_);
// If we ran a reconnect command, disconnect now
if (reconnect_tree_command_) {
oakundo_command_undo_now(reconnect_tree_command_);
}
}
//
// BlockSplitPreservingLinksCommand
//
BlockSplitPreservingLinksCommand::~BlockSplitPreservingLinksCommand()
{
for (UndoCommand *c : commands_) {
delete c;
}
}
OakNodeBlock *
BlockSplitPreservingLinksCommand::get_split(OakNodeBlock *original,
int time_index) const
{
if (time_index >= 0 && time_index < int(times_.size())) {
for (size_t i = 0; i < blocks_.size(); i++) {
if (blocks_[i] == original) {
return splits_.at(time_index).at(i);
}
}
}
return nullptr;
}
void BlockSplitPreservingLinksCommand::prepare()
{
splits_.resize(times_.size());
for (size_t i = 0; i < times_.size(); i++) {
const Rational &time = times_.at(i);
// FIXME: I realize this isn't going to work if the times aren't ordered. I'm lazy so rather
// than writing in a sorting algorithm here, I'll just put an assert as a reminder
// if this ever becomes an issue.
assert(i == 0 || time > times_.at(i - 1));
std::vector<OakNodeBlock *> splits(blocks_.size(), nullptr);
for (size_t j = 0; j < blocks_.size(); j++) {
OakNodeBlock *b = blocks_.at(j);
int in_n, in_d, out_n, out_d;
oaknode_block_get_in(b, &in_n, &in_d);
oaknode_block_get_out(b, &out_n, &out_d);
Rational b_in(in_n, in_d);
Rational b_out(out_n, out_d);
if (b_in < time && b_out > time) {
BlockSplitCommand *split_command =
new BlockSplitCommand(b, time);
split_command->redo_now();
splits[j] = split_command->new_block();
commands_.push_back(split_command);
}
}
splits_[i] = splits;
}
// Now that we've determined all the splits, we can relink everything
for (size_t i = 0; i < blocks_.size(); i++) {
OakNodeBlock *a = blocks_.at(i);
for (size_t j = 0; j < blocks_.size(); j++) {
if (i == j) {
continue;
}
OakNodeBlock *b = blocks_.at(j);
int linked = 0;
oaknode_block_are_linked(a, b, &linked);
if (linked) {
// These blocks are linked, ensure all the splits are linked too
for (const std::vector<OakNodeBlock *> &split_list :
splits_) {
if (!split_list.at(i) || !split_list.at(j)) {
continue;
}
BlockLinkUndoCommand *blc = new BlockLinkUndoCommand(
split_list.at(i), split_list.at(j), true);
blc->redo_now();
commands_.push_back(blc);
}
}
}
}
}
//
// TrackSplitAtTimeCommand
//
void TrackSplitAtTimeCommand::prepare()
{
// Find Block that contains this time
int n, d;
rat_nd(point_, &n, &d);
OakNodeBlock *b = nullptr;
oaknode_track_get_block_containing_time(track_, n, d, &b);
if (b) {
command_ = new BlockSplitCommand(b, point_);
}
}
}
+168
View File
@@ -0,0 +1,168 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEUNDOSPLIT_H
#define OAK_TIMELINEUNDOSPLIT_H
#include <string>
#include <vector>
#include <olive/core/core.h>
#include "node/block.h"
#include "node/track.h"
#include "undo/undocommand.h"
#include "undocommand.h"
using namespace olive::core;
namespace olive
{
/**
* @brief Undo commands for splitting blocks, over oaknode C handles
*
* All graph operations go through the oaknode C ABI (01 §0 rule 6).
*/
class BlockSplitCommand : public UndoCommand {
public:
BlockSplitCommand(OakNodeBlock *block, Rational point)
: block_(block)
, new_block_(nullptr)
, point_(point)
, reconnect_tree_command_(nullptr)
, moved_transition_(nullptr)
{
}
virtual ~BlockSplitCommand() override;
/**
* @brief Access the second block created as a result. Only valid after redo().
*/
OakNodeBlock *new_block()
{
return new_block_;
}
protected:
virtual void prepare() override;
virtual void redo() override;
virtual void undo() override;
private:
OakNodeBlock *block_;
OakNodeBlock *new_block_;
Rational old_length_;
Rational point_;
OakUndoCommand *reconnect_tree_command_;
OakNodeBlock *moved_transition_;
std::string moved_transition_input_;
};
class BlockSplitPreservingLinksCommand : public UndoCommand {
public:
BlockSplitPreservingLinksCommand(
const std::vector<OakNodeBlock *> &blocks,
const std::vector<Rational> &times)
: blocks_(blocks)
, times_(times)
{
}
virtual ~BlockSplitPreservingLinksCommand() override;
OakNodeBlock *get_split(OakNodeBlock *original, int time_index) const;
protected:
virtual void prepare() override;
virtual void redo() override
{
for (UndoCommand *c : commands_) {
c->redo_now();
}
}
virtual void undo() override
{
for (auto it = commands_.rbegin(); it != commands_.rend(); it++) {
(*it)->undo_now();
}
}
private:
std::vector<OakNodeBlock *> blocks_;
std::vector<Rational> times_;
std::vector<UndoCommand *> commands_;
std::vector<std::vector<OakNodeBlock *>> splits_;
};
class TrackSplitAtTimeCommand : public UndoCommand {
public:
TrackSplitAtTimeCommand(OakNodeTrack *track, Rational point)
: track_(track)
, point_(point)
, command_(nullptr)
{
}
virtual ~TrackSplitAtTimeCommand() override
{
delete command_;
}
protected:
virtual void prepare() override;
virtual void redo() override
{
if (command_) {
command_->redo_now();
}
}
virtual void undo() override
{
if (command_) {
command_->undo_now();
}
}
private:
OakNodeTrack *track_;
Rational point_;
UndoCommand *command_;
};
}
#endif // OAK_TIMELINEUNDOSPLIT_H
+154
View File
@@ -0,0 +1,154 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEUNDOTRACK_H
#define OAK_TIMELINEUNDOTRACK_H
#include "node/block.h"
#include "node/track.h"
#include "undocommand.h"
namespace olive
{
/**
* @brief Undo commands over oaknode track/block handles
*
* All graph operations go through the oaknode C ABI (01 §0 rule 6: no
* cross-module C++ member calls).
*/
class TrackRippleRemoveBlockCommand : public UndoCommand {
public:
TrackRippleRemoveBlockCommand(OakNodeTrack *track, OakNodeBlock *block)
: track_(track)
, block_(block)
, before_(nullptr)
{
}
protected:
virtual void redo() override
{
oaknode_block_get_previous(block_, &before_);
oaknode_track_ripple_remove_block(track_, block_);
}
virtual void undo() override
{
oaknode_track_insert_block_after(track_, block_, before_);
}
private:
OakNodeTrack *track_;
OakNodeBlock *block_;
OakNodeBlock *before_;
};
class TrackPrependBlockCommand : public UndoCommand {
public:
TrackPrependBlockCommand(OakNodeTrack *track, OakNodeBlock *block)
: track_(track)
, block_(block)
{
}
protected:
virtual void redo() override
{
oaknode_track_prepend_block(track_, block_);
}
virtual void undo() override
{
oaknode_track_ripple_remove_block(track_, block_);
}
private:
OakNodeTrack *track_;
OakNodeBlock *block_;
};
class TrackInsertBlockAfterCommand : public UndoCommand {
public:
TrackInsertBlockAfterCommand(OakNodeTrack *track, OakNodeBlock *block,
OakNodeBlock *before)
: track_(track)
, block_(block)
, before_(before)
{
}
protected:
virtual void redo() override
{
oaknode_track_insert_block_after(track_, block_, before_);
}
virtual void undo() override
{
oaknode_track_ripple_remove_block(track_, block_);
}
private:
OakNodeTrack *track_;
OakNodeBlock *block_;
OakNodeBlock *before_;
};
/**
* @brief Replaces Block `old` with Block `replace`
*
* Both blocks must have equal lengths.
*/
class TrackReplaceBlockCommand : public UndoCommand {
public:
TrackReplaceBlockCommand(OakNodeTrack *track, OakNodeBlock *old_block,
OakNodeBlock *replace)
: track_(track)
, old_(old_block)
, replace_(replace)
{
}
protected:
virtual void redo() override
{
oaknode_track_replace_block(track_, old_, replace_);
}
virtual void undo() override
{
oaknode_track_replace_block(track_, replace_, old_);
}
private:
OakNodeTrack *track_;
OakNodeBlock *old_;
OakNodeBlock *replace_;
};
}
#endif // OAK_TIMELINEUNDOTRACK_H
+95
View File
@@ -0,0 +1,95 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEUNDOWORKAREA_H
#define OAK_TIMELINEUNDOWORKAREA_H
#include "timelineworkarea.h"
#include "undocommand.h"
namespace olive
{
class WorkareaSetEnabledCommand : public UndoCommand {
public:
WorkareaSetEnabledCommand(TimelineWorkArea *points, bool enabled)
: points_(points)
, old_enabled_(points_->enabled())
, new_enabled_(enabled)
{
}
protected:
virtual void redo() override
{
points_->set_enabled(new_enabled_);
}
virtual void undo() override
{
points_->set_enabled(old_enabled_);
}
private:
TimelineWorkArea *points_;
bool old_enabled_;
bool new_enabled_;
};
class WorkareaSetRangeCommand : public UndoCommand {
public:
WorkareaSetRangeCommand(TimelineWorkArea *workarea, const TimeRange &range,
const TimeRange &old_range)
: workarea_(workarea)
, old_range_(old_range)
, new_range_(range)
{
}
WorkareaSetRangeCommand(TimelineWorkArea *workarea, const TimeRange &range)
: WorkareaSetRangeCommand(workarea, range, workarea->range())
{
}
protected:
virtual void redo() override
{
workarea_->set_range(new_range_);
}
virtual void undo() override
{
workarea_->set_range(old_range_);
}
private:
TimelineWorkArea *workarea_;
TimeRange old_range_;
TimeRange new_range_;
};
}
#endif // OAK_TIMELINEUNDOWORKAREA_H
+144
View File
@@ -0,0 +1,144 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEUTIL_H
#define OAK_TIMELINEUTIL_H
#include <olive/core/core.h>
#include "node/block.h"
#include "node/project.h"
#include "node/sequence.h"
#include "node/track.h"
namespace olive
{
/**
* @brief Small helpers translating between olive::core::Rational and the
* oaknode C ABI's numerator/denominator pairs
*/
inline void rat_nd(const olive::core::Rational &r, int *n, int *d)
{
*n = r.numerator();
*d = r.denominator();
}
inline olive::core::Rational block_in(OakNodeBlock *b)
{
int n, d;
oaknode_block_get_in(b, &n, &d);
return olive::core::Rational(n, d);
}
inline olive::core::Rational block_out(OakNodeBlock *b)
{
int n, d;
oaknode_block_get_out(b, &n, &d);
return olive::core::Rational(n, d);
}
inline olive::core::Rational block_length(OakNodeBlock *b)
{
int n, d;
oaknode_block_get_length(b, &n, &d);
return olive::core::Rational(n, d);
}
inline void block_set_length_and_media_out(OakNodeBlock *b,
const olive::core::Rational &len)
{
int n, d;
rat_nd(len, &n, &d);
oaknode_block_set_length_and_media_out(b, n, d);
}
inline void block_set_length_and_media_in(OakNodeBlock *b,
const olive::core::Rational &len)
{
int n, d;
rat_nd(len, &n, &d);
oaknode_block_set_length_and_media_in(b, n, d);
}
inline olive::core::Rational track_length(OakNodeTrack *t)
{
int n, d;
oaknode_track_get_length(t, &n, &d);
return olive::core::Rational(n, d);
}
inline OakNodeBlock *block_previous(OakNodeBlock *b)
{
OakNodeBlock *out = nullptr;
oaknode_block_get_previous(b, &out);
return out;
}
inline OakNodeBlock *block_next(OakNodeBlock *b)
{
OakNodeBlock *out = nullptr;
oaknode_block_get_next(b, &out);
return out;
}
inline OakNodeTrack *block_track(OakNodeBlock *b)
{
OakNodeTrack *out = nullptr;
oaknode_block_get_track(b, &out);
return out;
}
/**
* @brief Attach/detach a block to/from the project graph that owns a
* track (replaces the original setParent(graph) / setParent(memory))
*/
inline OakNodeProject *track_project(OakNodeTrack *track)
{
OakNodeSequence *sequence = nullptr;
if (oaknode_track_get_sequence(track, &sequence) != OAKNODE_OK ||
!sequence) {
return nullptr;
}
OakNodeProject *project = nullptr;
oaknode_node_get_project(oaknode_sequence_as_node(sequence), &project);
return project;
}
inline void block_add_to_graph(OakNodeBlock *b, OakNodeTrack *track)
{
OakNodeProject *project = track_project(track);
if (project) {
oaknode_project_add_node(project, oaknode_block_as_node(b));
}
}
inline void block_remove_from_graph(OakNodeBlock *b, OakNodeTrack *track)
{
OakNodeProject *project = track_project(track);
if (project) {
oaknode_project_remove_node(project, oaknode_block_as_node(b));
}
}
}
#endif // OAK_TIMELINEUTIL_H
+112
View File
@@ -0,0 +1,112 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "timelineworkarea.h"
#include <cstdlib>
namespace olive
{
const Rational TimelineWorkArea::k_reset_in = 0;
const Rational TimelineWorkArea::k_reset_out = RATIONAL_MAX;
TimelineWorkArea::TimelineWorkArea()
: workarea_enabled_(false)
{
}
bool TimelineWorkArea::enabled() const
{
return workarea_enabled_;
}
void TimelineWorkArea::set_enabled(bool e)
{
workarea_enabled_ = e;
}
const TimeRange &TimelineWorkArea::range() const
{
return workarea_range_;
}
void TimelineWorkArea::set_range(const TimeRange &range)
{
workarea_range_ = range;
}
bool TimelineWorkArea::load(XmlStreamReader *reader)
{
Rational range_in = this->in();
Rational range_out = this->out();
for (const XmlStreamAttribute &attr : reader->attributes()) {
// version is currently unused
(void)attr;
}
while (xml_read_next_start_element(reader)) {
if (reader->name() == "enabled") {
this->set_enabled(reader->read_element_text() != "0");
} else if (reader->name() == "in") {
range_in = Rational::from_string(reader->read_element_text());
} else if (reader->name() == "out") {
range_out = Rational::from_string(reader->read_element_text());
} else {
reader->skip_current_element();
}
}
TimeRange loaded_workarea(range_in, range_out);
if (loaded_workarea != this->range()) {
this->set_range(loaded_workarea);
}
return true;
}
void TimelineWorkArea::save(XmlStreamWriter *writer) const
{
writer->write_attribute("version", "1");
writer->write_text_element("enabled", std::to_string(this->enabled()));
writer->write_text_element("in", this->in().to_string());
writer->write_text_element("out", this->out().to_string());
}
Rational TimelineWorkArea::in() const
{
return workarea_range_.in();
}
Rational TimelineWorkArea::out() const
{
return workarea_range_.out();
}
Rational TimelineWorkArea::length() const
{
return workarea_range_.length();
}
}
+67
View File
@@ -0,0 +1,67 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TIMELINEWORKAREA_H
#define OAK_TIMELINEWORKAREA_H
#include <olive/core/core.h>
#include "xmlutils.h"
namespace olive
{
using namespace core;
/**
* @brief A work area (in/out range) on a timeline
*
* De-Qt version: no QObject, no signals. Change notifications are
* emitted by the caller's layer (facade).
*/
class TimelineWorkArea {
public:
TimelineWorkArea();
bool enabled() const;
void set_enabled(bool e);
Rational in() const;
Rational out() const;
Rational length() const;
const TimeRange &range() const;
void set_range(const TimeRange &range);
bool load(XmlStreamReader *reader);
void save(XmlStreamWriter *writer) const;
static const Rational k_reset_in;
static const Rational k_reset_out;
private:
bool workarea_enabled_;
TimeRange workarea_range_;
};
}
#endif // OAK_TIMELINEWORKAREA_H
+158
View File
@@ -0,0 +1,158 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
# Modifications Copyright (C) 2025 mikesolar
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
cmake_minimum_required(VERSION 3.16)
project(oaktimeline-standalone CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# oaktimeline manipulates node graphs through the oaknode C ABI; the
# not-yet-split engine modules' symbols are allowed to dangle, resolved
# by transitional stubs in src/{node,render}/transition.
set(OAK_REPO_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake")
set(BUILD_TESTS ON CACHE BOOL "" FORCE)
set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# The /opt/otio OpenTimelineIO dylibs use @loader_path install names that
# break when linked from a build tree; oaktimeline does not need OTIO.
set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON)
find_package(EXPAT REQUIRED)
find_package(OpenColorIO CONFIG REQUIRED)
find_package(OpenImageIO CONFIG REQUIRED)
set(OCIO_LIBRARIES OpenColorIO::OpenColorIO)
set(OCIO_INCLUDE_DIRS ${OpenColorIO_INCLUDE_DIRS})
set(OIIO_LIBRARIES OpenImageIO::OpenImageIO)
set(OIIO_INCLUDE_DIRS ${OpenImageIO_INCLUDE_DIRS})
add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core)
target_include_directories(olivecore PUBLIC
${OAK_REPO_ROOT}/third_party/openfx/include
)
add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge)
set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo)
add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common)
add_subdirectory(${OAK_REPO_ROOT}/src/render/src ${CMAKE_BINARY_DIR}/render)
set(BUILD_TESTS ON)
enable_testing()
add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node)
add_subdirectory(${OAK_REPO_ROOT}/src/timeline ${CMAKE_BINARY_DIR}/timeline)
# oaknode needs its transition stubs and the oakrender real headers
# (src/node/transition/render/* bridge there); src/render/transition
# must precede src/node/transition.
target_include_directories(oaknode BEFORE PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
${OAK_REPO_ROOT}/src/render/src
)
target_include_directories(oaknode PUBLIC
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
/opt/homebrew/include
/opt/homebrew/include/Imath
)
target_link_options(oaknode PRIVATE
"-undefined" "dynamic_lookup"
)
target_include_directories(oakrender BEFORE PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
)
target_include_directories(oakrender PUBLIC
${OAK_REPO_ROOT}/engine/include
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
/opt/homebrew/include
/opt/homebrew/include/Imath
/opt/homebrew/include/OpenEXR
)
foreach(t oakrender oakgl oakgl2 oakvulkan)
if(TARGET ${t})
target_link_options(${t} PRIVATE
"-undefined" "dynamic_lookup"
)
endif()
endforeach()
target_include_directories(oaktimeline PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
${OAK_REPO_ROOT}/src/render/src
${OAK_REPO_ROOT}/src/node/src
)
target_link_options(oaktimeline PRIVATE
"-undefined" "dynamic_lookup"
)
target_link_libraries(oakrender PRIVATE
oaknode
oakcommon
oakundo
olivecore
ffmpeg_bridge
${OCIO_LIBRARIES}
${OIIO_LIBRARIES}
"-framework OpenGL"
"-framework CoreVideo"
"-framework Metal"
"-framework QuartzCore"
)
target_link_libraries(oaknode-gtest PRIVATE oakrender)
# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
# dynamic_lookup; the real implementation (engine/src/capi/ipc.cpp) is still
# Qt-based, so test binaries link an inert shim instead.
target_sources(oaknode-gtest PRIVATE
${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp
)
target_include_directories(oaknode-gtest PRIVATE
${OAK_REPO_ROOT}/engine/include
)
target_link_libraries(oaktimeline-gtest PRIVATE oakrender)
target_sources(oaktimeline-gtest PRIVATE
${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp
)
target_include_directories(oaktimeline-gtest PRIVATE
${OAK_REPO_ROOT}/engine/include
)
# OFX typeinfo is referenced via dynamic_lookup; force-load the host
# support archive so RTTI resolves (same as oaknode's test wiring).
find_library(OAKNODE_OFX_HOST_ARCHIVE NAMES OfxHost
PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport
NO_DEFAULT_PATH)
if(NOT OAKNODE_OFX_HOST_ARCHIVE)
message(FATAL_ERROR
"libOfxHost.a not found; run the full-tree build once or set "
"OAKNODE_OFX_HOST_ARCHIVE")
endif()
target_link_options(oaktimeline-gtest PRIVATE
"-Wl,-force_load,${OAKNODE_OFX_HOST_ARCHIVE}")
target_link_options(oaknode-gtest PRIVATE
"-Wl,-force_load,${OAKNODE_OFX_HOST_ARCHIVE}")
# oaknode's Sequence::add_default_nodes() constructs
# TimelineAddTrackCommand, which lives in liboaktimeline; the two
# libraries resolve each other at runtime (dynamic_lookup).
target_link_libraries(oaknode-gtest PRIVATE oaktimeline)
+19
View File
@@ -0,0 +1,19 @@
find_package(GTest REQUIRED)
include(GoogleTest)
add_executable(oaktimeline-gtest
timelinemarker_test.cpp
timeline_test.cpp
)
target_link_libraries(oaktimeline-gtest PRIVATE
oaktimeline
GTest::gtest
GTest::gtest_main
)
gtest_discover_tests(oaktimeline-gtest
DISCOVERY_MODE PRE_TEST
PROPERTIES ENVIRONMENT
"OCIO=${OAK_REPO_ROOT}/engine/render/ocioconf/config.ocio"
)
+602
View File
@@ -0,0 +1,602 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include <gtest/gtest.h>
#include "common/xmlutils.h"
#include "node/project.h"
#include "node/sequence.h"
#include "timeline/edit.h"
#include "timeline/marker.h"
#include "timeline/workarea.h"
#include "undo/undocommand.h"
namespace
{
class TimelineSequenceFixture : public ::testing::Test {
protected:
void SetUp() override
{
project_ = oaknode_project_init();
ASSERT_NE(project_, nullptr);
sequence_ = oaknode_sequence_create();
ASSERT_NE(sequence_, nullptr);
node_ = oaknode_sequence_as_node(sequence_);
OakNodeProject *owner = nullptr;
ASSERT_EQ(oaknode_node_get_project(node_, &owner), OAKNODE_OK);
if (!owner) {
ASSERT_EQ(oaknode_project_add_node(project_, node_),
OAKNODE_OK);
}
}
void TearDown() override
{
// The project owns the sequence and everything in its graph
oaknode_project_free(project_);
}
OakNodeProject *project_ = nullptr;
OakNodeSequence *sequence_ = nullptr;
OakNodeNode *node_ = nullptr;
};
// ---- marker ---------------------------------------------------------------
TEST_F(TimelineSequenceFixture, MarkerListOfReturnsList)
{
EXPECT_NE(oaktimeline_marker_list_of(node_), nullptr);
EXPECT_EQ(oaktimeline_marker_list_of(nullptr), nullptr);
}
TEST_F(TimelineSequenceFixture, MarkerAddCountAtRemove)
{
OakTimelineMarkerList *list = oaktimeline_marker_list_of(node_);
ASSERT_NE(list, nullptr);
int count = -1;
EXPECT_EQ(oaktimeline_marker_count(list, &count), OAKTIMELINE_OK);
EXPECT_EQ(count, 0);
EXPECT_EQ(oaktimeline_marker_count(nullptr, &count),
OAKTIMELINE_E_INVALID);
EXPECT_EQ(oaktimeline_marker_count(list, nullptr),
OAKTIMELINE_E_INVALID);
OakUndoCommand *cmd =
oaktimeline_marker_add_command(list, 1, 2, 3, 4, "mark", 5);
ASSERT_NE(cmd, nullptr);
oakundo_command_redo_now(cmd);
oakundo_command_free(cmd);
EXPECT_EQ(oaktimeline_marker_count(list, &count), OAKTIMELINE_OK);
ASSERT_EQ(count, 1);
int in_n = 0, in_d = 0, out_n = 0, out_d = 0, color = 0;
char name[32];
int needed = oaktimeline_marker_at(list, 0, &in_n, &in_d, &out_n,
&out_d, &color, name, sizeof(name));
ASSERT_GT(needed, 0);
EXPECT_EQ(in_n, 1);
EXPECT_EQ(in_d, 2);
EXPECT_EQ(out_n, 3);
EXPECT_EQ(out_d, 4);
EXPECT_EQ(color, 5);
EXPECT_STREQ(name, "mark");
EXPECT_EQ(oaktimeline_marker_at(list, 9, &in_n, &in_d, &out_n,
&out_d, &color, name, sizeof(name)),
OAKTIMELINE_E_NOT_FOUND);
EXPECT_EQ(oaktimeline_marker_at(nullptr, 0, &in_n, &in_d, &out_n,
&out_d, &color, name, sizeof(name)),
OAKTIMELINE_E_INVALID);
OakUndoCommand *rm = oaktimeline_marker_remove_at_command(list, 0);
ASSERT_NE(rm, nullptr);
oakundo_command_redo_now(rm);
oakundo_command_free(rm);
EXPECT_EQ(oaktimeline_marker_count(list, &count), OAKTIMELINE_OK);
EXPECT_EQ(count, 0);
EXPECT_EQ(oaktimeline_marker_remove_at_command(list, 4), nullptr);
}
TEST_F(TimelineSequenceFixture, MarkerSetTimeAndPropsUndo)
{
OakTimelineMarkerList *list = oaktimeline_marker_list_of(node_);
ASSERT_NE(list, nullptr);
OakUndoCommand *add =
oaktimeline_marker_add_command(list, 0, 1, 1, 1, "a", 1);
ASSERT_NE(add, nullptr);
oakundo_command_redo_now(add);
oakundo_command_free(add);
OakUndoCommand *set_time =
oaktimeline_marker_set_time_command(list, 0, 10, 1, 20, 1);
ASSERT_NE(set_time, nullptr);
oakundo_command_redo_now(set_time);
int in_n = 0;
EXPECT_GT(oaktimeline_marker_at(list, 0, &in_n, NULL, NULL, NULL,
NULL, NULL, 0),
0);
EXPECT_EQ(in_n, 10);
oakundo_command_undo_now(set_time);
EXPECT_GT(oaktimeline_marker_at(list, 0, &in_n, NULL, NULL, NULL,
NULL, NULL, 0),
0);
EXPECT_EQ(in_n, 0);
oakundo_command_free(set_time);
OakUndoCommand *props =
oaktimeline_marker_set_props_command(list, 0, 7, "renamed");
ASSERT_NE(props, nullptr);
oakundo_command_redo_now(props);
oakundo_command_free(props);
int color = 0;
char name[32];
EXPECT_GT(oaktimeline_marker_at(list, 0, NULL, NULL, NULL, NULL,
&color, name, sizeof(name)),
0);
EXPECT_EQ(color, 7);
EXPECT_STREQ(name, "renamed");
EXPECT_EQ(oaktimeline_marker_set_props_command(list, 0, -1, NULL),
nullptr);
EXPECT_EQ(oaktimeline_marker_set_time_command(list, 9, 0, 1, 1, 1),
nullptr);
}
TEST_F(TimelineSequenceFixture, MarkerListXmlRoundTrip)
{
OakTimelineMarkerList *list = oaktimeline_marker_list_of(node_);
ASSERT_NE(list, nullptr);
OakUndoCommand *add =
oaktimeline_marker_add_command(list, 1, 3, 2, 3, "xml", 4);
ASSERT_NE(add, nullptr);
oakundo_command_redo_now(add);
oakundo_command_free(add);
OakXmlWriter writer = oakcommon_xml_writer_init();
ASSERT_NE(writer.ctx, nullptr);
EXPECT_EQ(oakcommon_xml_writer_write_start_element(writer, "markers"),
OAKCOMMON_OK);
EXPECT_EQ(oaktimeline_marker_list_save(list, writer), OAKTIMELINE_OK);
EXPECT_EQ(oakcommon_xml_writer_write_end_element(writer),
OAKCOMMON_OK);
char xml[1024];
int needed = oakcommon_xml_writer_output(writer, xml, sizeof(xml));
ASSERT_GT(needed, 0);
ASSERT_LT(needed, int(sizeof(xml)));
oakcommon_xml_writer_free(&writer);
// Load into a fresh sequence's marker list
OakNodeSequence *seq2 = oaknode_sequence_create();
ASSERT_NE(seq2, nullptr);
ASSERT_EQ(oaknode_project_add_node(project_,
oaknode_sequence_as_node(seq2)),
OAKNODE_OK);
OakTimelineMarkerList *list2 =
oaktimeline_marker_list_of(oaknode_sequence_as_node(seq2));
ASSERT_NE(list2, nullptr);
OakXmlReader reader = oakcommon_xml_reader_init(xml);
ASSERT_NE(reader.ctx, nullptr);
int start = 0;
ASSERT_EQ(oakcommon_xml_reader_read_next_start_element(reader, &start),
OAKCOMMON_OK);
ASSERT_NE(start, 0);
EXPECT_EQ(oaktimeline_marker_list_load(list2, reader), OAKTIMELINE_OK);
oakcommon_xml_reader_free(&reader);
int count = 0;
EXPECT_EQ(oaktimeline_marker_count(list2, &count), OAKTIMELINE_OK);
ASSERT_EQ(count, 1);
int color = 0;
char name[32];
EXPECT_GT(oaktimeline_marker_at(list2, 0, NULL, NULL, NULL, NULL,
&color, name, sizeof(name)),
0);
EXPECT_EQ(color, 4);
EXPECT_STREQ(name, "xml");
EXPECT_EQ(oaktimeline_marker_list_load(nullptr, reader),
OAKTIMELINE_E_INVALID);
}
// ---- workarea -------------------------------------------------------------
TEST_F(TimelineSequenceFixture, WorkareaGetSetLive)
{
OakTimelineWorkArea *w = oaktimeline_workarea_of(node_);
ASSERT_NE(w, nullptr);
EXPECT_EQ(oaktimeline_workarea_of(nullptr), nullptr);
int in_n = 0, in_d = 0, out_n = 0, out_d = 0, enabled = -1;
EXPECT_EQ(oaktimeline_workarea_get(w, &in_n, &in_d, &out_n, &out_d,
&enabled),
OAKTIMELINE_OK);
EXPECT_EQ(enabled, 0);
EXPECT_EQ(oaktimeline_workarea_set_range(w, 1, 4, 3, 4),
OAKTIMELINE_OK);
EXPECT_EQ(oaktimeline_workarea_get(w, &in_n, &in_d, &out_n, &out_d,
&enabled),
OAKTIMELINE_OK);
EXPECT_EQ(in_n, 1);
EXPECT_EQ(in_d, 4);
EXPECT_EQ(out_n, 3);
EXPECT_EQ(out_d, 4);
EXPECT_EQ(oaktimeline_workarea_set_range(nullptr, 0, 1, 1, 1),
OAKTIMELINE_E_INVALID);
EXPECT_EQ(oaktimeline_workarea_get(nullptr, &in_n, &in_d, &out_n,
&out_d, &enabled),
OAKTIMELINE_E_INVALID);
}
TEST_F(TimelineSequenceFixture, WorkareaUndoCommands)
{
OakTimelineWorkArea *w = oaktimeline_workarea_of(node_);
ASSERT_NE(w, nullptr);
OakUndoCommand *range_cmd = oaktimeline_workarea_set_range_command(
w, 1, 2, 1, 1, 0, 1, 1, 1);
ASSERT_NE(range_cmd, nullptr);
oakundo_command_redo_now(range_cmd);
int in_n = 0, out_n = 0;
EXPECT_EQ(oaktimeline_workarea_get(w, &in_n, NULL, &out_n, NULL, NULL),
OAKTIMELINE_OK);
EXPECT_EQ(in_n, 1);
EXPECT_EQ(out_n, 1);
oakundo_command_undo_now(range_cmd);
EXPECT_EQ(oaktimeline_workarea_get(w, &in_n, NULL, &out_n, NULL, NULL),
OAKTIMELINE_OK);
EXPECT_EQ(in_n, 0);
oakundo_command_free(range_cmd);
OakUndoCommand *enable_cmd =
oaktimeline_workarea_set_enabled_command(w, 1);
ASSERT_NE(enable_cmd, nullptr);
oakundo_command_redo_now(enable_cmd);
int enabled = 0;
EXPECT_EQ(oaktimeline_workarea_get(w, NULL, NULL, NULL, NULL,
&enabled),
OAKTIMELINE_OK);
EXPECT_EQ(enabled, 1);
oakundo_command_undo_now(enable_cmd);
EXPECT_EQ(oaktimeline_workarea_get(w, NULL, NULL, NULL, NULL,
&enabled),
OAKTIMELINE_OK);
EXPECT_EQ(enabled, 0);
oakundo_command_free(enable_cmd);
EXPECT_EQ(oaktimeline_workarea_set_range_command(nullptr, 0, 1, 1, 1,
0, 1, 1, 1),
nullptr);
EXPECT_EQ(oaktimeline_workarea_set_enabled_command(nullptr, 1),
nullptr);
}
TEST_F(TimelineSequenceFixture, WorkareaResetSentinels)
{
int in_n = 0, in_d = 0, out_n = 0, out_d = 0;
EXPECT_EQ(oaktimeline_workarea_reset(&in_n, &in_d, &out_n, &out_d),
OAKTIMELINE_OK);
EXPECT_EQ(in_n, 0);
EXPECT_EQ(out_n, INT32_MAX);
EXPECT_EQ(oaktimeline_workarea_reset(nullptr, &in_d, &out_n, &out_d),
OAKTIMELINE_E_INVALID);
}
TEST_F(TimelineSequenceFixture, WorkareaXmlRoundTrip)
{
OakTimelineWorkArea *w = oaktimeline_workarea_of(node_);
ASSERT_NE(w, nullptr);
EXPECT_EQ(oaktimeline_workarea_set_range(w, 1, 3, 2, 3),
OAKTIMELINE_OK);
OakXmlWriter writer = oakcommon_xml_writer_init();
ASSERT_NE(writer.ctx, nullptr);
EXPECT_EQ(oakcommon_xml_writer_write_start_element(writer, "workarea"),
OAKCOMMON_OK);
EXPECT_EQ(oaktimeline_workarea_save(w, writer), OAKTIMELINE_OK);
EXPECT_EQ(oakcommon_xml_writer_write_end_element(writer),
OAKCOMMON_OK);
char xml[1024];
int needed = oakcommon_xml_writer_output(writer, xml, sizeof(xml));
ASSERT_GT(needed, 0);
ASSERT_LT(needed, int(sizeof(xml)));
oakcommon_xml_writer_free(&writer);
OakNodeSequence *seq2 = oaknode_sequence_create();
ASSERT_NE(seq2, nullptr);
ASSERT_EQ(oaknode_project_add_node(project_,
oaknode_sequence_as_node(seq2)),
OAKNODE_OK);
OakTimelineWorkArea *w2 =
oaktimeline_workarea_of(oaknode_sequence_as_node(seq2));
ASSERT_NE(w2, nullptr);
OakXmlReader reader = oakcommon_xml_reader_init(xml);
ASSERT_NE(reader.ctx, nullptr);
int start = 0;
ASSERT_EQ(oakcommon_xml_reader_read_next_start_element(reader, &start),
OAKCOMMON_OK);
ASSERT_NE(start, 0);
EXPECT_EQ(oaktimeline_workarea_load(w2, reader), OAKTIMELINE_OK);
oakcommon_xml_reader_free(&reader);
int in_n = 0, out_n = 0;
EXPECT_EQ(oaktimeline_workarea_get(w2, &in_n, NULL, &out_n, NULL,
NULL),
OAKTIMELINE_OK);
EXPECT_EQ(in_n, 1);
EXPECT_EQ(out_n, 2);
}
// ---- edit -----------------------------------------------------------------
TEST_F(TimelineSequenceFixture, AddAndRemoveTrackCommands)
{
OakNodeTrackList *list = nullptr;
ASSERT_EQ(oaknode_sequence_get_track_list(
sequence_, OAKNODE_TRACK_TYPE_VIDEO, &list),
OAKNODE_OK);
ASSERT_NE(list, nullptr);
int count = -1;
EXPECT_EQ(oaknode_tracklist_get_track_count(list, &count), OAKNODE_OK);
const int before = count;
OakUndoCommand *add = oaktimeline_add_track_command(list);
ASSERT_NE(add, nullptr);
oakundo_command_redo_now(add);
EXPECT_EQ(oaknode_tracklist_get_track_count(list, &count), OAKNODE_OK);
EXPECT_EQ(count, before + 1);
OakNodeTrack *track = nullptr;
EXPECT_EQ(oaknode_tracklist_get_track_at(list, before, &track),
OAKNODE_OK);
ASSERT_NE(track, nullptr);
OakUndoCommand *rm = oaktimeline_remove_track_command(track);
ASSERT_NE(rm, nullptr);
oakundo_command_redo_now(rm);
EXPECT_EQ(oaknode_tracklist_get_track_count(list, &count), OAKNODE_OK);
EXPECT_EQ(count, before);
oakundo_command_undo_now(rm);
EXPECT_EQ(oaknode_tracklist_get_track_count(list, &count), OAKNODE_OK);
EXPECT_EQ(count, before + 1);
oakundo_command_undo_now(add);
EXPECT_EQ(oaknode_tracklist_get_track_count(list, &count), OAKNODE_OK);
EXPECT_EQ(count, before);
oakundo_command_free(add);
oakundo_command_free(rm);
EXPECT_EQ(oaktimeline_add_track_command(nullptr), nullptr);
EXPECT_EQ(oaktimeline_remove_track_command(nullptr), nullptr);
}
TEST_F(TimelineSequenceFixture, PlaceTrimSplitRemoveAreaCommands)
{
OakNodeTrackList *list = nullptr;
ASSERT_EQ(oaknode_sequence_get_track_list(
sequence_, OAKNODE_TRACK_TYPE_VIDEO, &list),
OAKNODE_OK);
OakNodeTrack *track = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO);
ASSERT_NE(track, nullptr);
ASSERT_EQ(oaknode_project_add_node(project_,
oaknode_track_as_node(track)),
OAKNODE_OK);
ASSERT_EQ(oaknode_tracklist_add_track(list, track), OAKNODE_OK);
OakNodeBlock *clip = oaknode_block_clip_create();
ASSERT_NE(clip, nullptr);
EXPECT_EQ(oaknode_block_set_length_and_media_out(clip, 10, 1),
OAKNODE_OK);
ASSERT_EQ(oaknode_project_add_node(project_,
oaknode_block_as_node(clip)),
OAKNODE_OK);
OakUndoCommand *place =
oaktimeline_place_block_command(list, 0, clip, 0, 1);
ASSERT_NE(place, nullptr);
oakundo_command_redo_now(place);
int block_count = 0;
EXPECT_EQ(oaknode_track_get_block_count(track, &block_count),
OAKNODE_OK);
ASSERT_EQ(block_count, 1);
// Trim out to length 4
OakUndoCommand *trim = oaktimeline_trim_command(
track, clip, 4, 1, OAKTIMELINE_MOVEMENT_TRIM_OUT);
ASSERT_NE(trim, nullptr);
oakundo_command_redo_now(trim);
int n = 0, d = 0;
EXPECT_EQ(oaknode_block_get_length(clip, &n, &d), OAKNODE_OK);
EXPECT_EQ(n, 4);
oakundo_command_undo_now(trim);
EXPECT_EQ(oaknode_block_get_length(clip, &n, &d), OAKNODE_OK);
EXPECT_EQ(n, 10);
oakundo_command_free(trim);
// Split at 5: two blocks of 5
OakNodeBlock *blocks[] = { clip };
OakUndoCommand *split =
oaktimeline_split_command(blocks, 1, 5, 1);
ASSERT_NE(split, nullptr);
oakundo_command_redo_now(split);
EXPECT_EQ(oaknode_track_get_block_count(track, &block_count),
OAKNODE_OK);
ASSERT_EQ(block_count, 2);
oakundo_command_undo_now(split);
EXPECT_EQ(oaknode_track_get_block_count(track, &block_count),
OAKNODE_OK);
ASSERT_EQ(block_count, 1);
oakundo_command_free(split);
// Ripple remove [3..5] then undo
OakUndoCommand *remove_area =
oaktimeline_ripple_remove_area_command(track, 3, 1, 5, 1);
ASSERT_NE(remove_area, nullptr);
oakundo_command_redo_now(remove_area);
// Removing [3..5] from [0..10] splices it into [0..3] + [5..10]
EXPECT_EQ(oaknode_track_get_block_count(track, &block_count),
OAKNODE_OK);
ASSERT_EQ(block_count, 2);
EXPECT_EQ(oaknode_block_get_length(clip, &n, &d), OAKNODE_OK);
EXPECT_EQ(n, 3);
OakNodeBlock *second = nullptr;
EXPECT_EQ(oaknode_track_get_block_at(track, 1, &second), OAKNODE_OK);
ASSERT_NE(second, nullptr);
EXPECT_EQ(oaknode_block_get_length(second, &n, &d), OAKNODE_OK);
EXPECT_EQ(n, 5);
oakundo_command_undo_now(remove_area);
EXPECT_EQ(oaknode_block_get_length(clip, &n, &d), OAKNODE_OK);
EXPECT_EQ(n, 10);
oakundo_command_free(remove_area);
oakundo_command_undo_now(place);
EXPECT_EQ(oaknode_track_get_block_count(track, &block_count),
OAKNODE_OK);
EXPECT_EQ(block_count, 0);
oakundo_command_free(place);
EXPECT_EQ(oaktimeline_place_block_command(nullptr, 0, clip, 0, 1),
nullptr);
EXPECT_EQ(oaktimeline_trim_command(track, clip, 1, 1, 99), nullptr);
EXPECT_EQ(oaktimeline_split_command(blocks, 0, 1, 1), nullptr);
EXPECT_EQ(oaktimeline_ripple_remove_area_command(nullptr, 0, 1, 1, 1),
nullptr);
}
TEST_F(TimelineSequenceFixture, ReplaceWithGapCommand)
{
OakNodeTrackList *list = nullptr;
ASSERT_EQ(oaknode_sequence_get_track_list(
sequence_, OAKNODE_TRACK_TYPE_VIDEO, &list),
OAKNODE_OK);
OakNodeTrack *track = oaknode_track_create(OAKNODE_TRACK_TYPE_VIDEO);
ASSERT_NE(track, nullptr);
ASSERT_EQ(oaknode_project_add_node(project_,
oaknode_track_as_node(track)),
OAKNODE_OK);
ASSERT_EQ(oaknode_tracklist_add_track(list, track), OAKNODE_OK);
OakNodeBlock *clip = oaknode_block_clip_create();
ASSERT_NE(clip, nullptr);
EXPECT_EQ(oaknode_block_set_length_and_media_out(clip, 6, 1),
OAKNODE_OK);
ASSERT_EQ(oaknode_project_add_node(project_,
oaknode_block_as_node(clip)),
OAKNODE_OK);
EXPECT_EQ(oaknode_track_append_block(track, clip), OAKNODE_OK);
// Second clip so the first has a next (gap required)
OakNodeBlock *clip2 = oaknode_block_clip_create();
ASSERT_NE(clip2, nullptr);
EXPECT_EQ(oaknode_block_set_length_and_media_out(clip2, 6, 1),
OAKNODE_OK);
ASSERT_EQ(oaknode_project_add_node(project_,
oaknode_block_as_node(clip2)),
OAKNODE_OK);
EXPECT_EQ(oaknode_track_append_block(track, clip2), OAKNODE_OK);
OakUndoCommand *replace =
oaktimeline_replace_block_with_gap_command(track, clip);
ASSERT_NE(replace, nullptr);
oakundo_command_redo_now(replace);
int block_count = 0;
EXPECT_EQ(oaknode_track_get_block_count(track, &block_count),
OAKNODE_OK);
ASSERT_EQ(block_count, 2);
OakNodeBlock *first = nullptr;
EXPECT_EQ(oaknode_track_get_block_at(track, 0, &first), OAKNODE_OK);
int kind = OAKNODE_BLOCK_OTHER;
EXPECT_EQ(oaknode_block_get_kind(first, &kind), OAKNODE_OK);
EXPECT_EQ(kind, OAKNODE_BLOCK_GAP);
oakundo_command_undo_now(replace);
EXPECT_EQ(oaknode_track_get_block_at(track, 0, &first), OAKNODE_OK);
EXPECT_EQ(first, clip);
oakundo_command_free(replace);
EXPECT_EQ(oaktimeline_replace_block_with_gap_command(nullptr, clip),
nullptr);
EXPECT_EQ(oaknode_track_ripple_remove_block(track, clip), OAKNODE_OK);
EXPECT_EQ(oaknode_track_ripple_remove_block(track, clip2), OAKNODE_OK);
}
TEST_F(TimelineSequenceFixture, SlideAndInsertGapsAndRippleDeleteGapsFactories)
{
OakNodeBlock *clip = oaknode_block_clip_create();
EXPECT_EQ(oaktimeline_slide_command(nullptr, &clip, 1, nullptr,
nullptr, 1, 1),
nullptr);
OakNodeTrackList *list = nullptr;
ASSERT_EQ(oaknode_sequence_get_track_list(
sequence_, OAKNODE_TRACK_TYPE_VIDEO, &list),
OAKNODE_OK);
EXPECT_EQ(oaktimeline_insert_gaps_command(nullptr, 0, 1, 1, 1),
nullptr);
EXPECT_EQ(oaktimeline_ripple_delete_gaps_command(
sequence_, nullptr, nullptr, nullptr, nullptr, nullptr, 0),
nullptr);
oaknode_block_free(clip);
}
} // namespace
+169
View File
@@ -0,0 +1,169 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include <gtest/gtest.h>
#include "timelinemarker.h"
using namespace olive;
TEST(TimelineMarker, AddKeepsSortedByTime)
{
TimelineMarkerList list;
list.add_marker(
std::make_unique<TimelineMarker>(0, TimeRange(Rational(10), Rational(20))));
list.add_marker(
std::make_unique<TimelineMarker>(1, TimeRange(Rational(0), Rational(5))));
ASSERT_EQ(list.size(), 2);
EXPECT_EQ(list.at(0)->time().in(), Rational(0));
EXPECT_EQ(list.at(1)->time().in(), Rational(10));
}
TEST(TimelineMarker, SetTimeResorts)
{
TimelineMarkerList list;
auto *late = new TimelineMarker(0, TimeRange(Rational(10), Rational(20)));
list.add_marker(std::unique_ptr<TimelineMarker>(late));
list.add_marker(
std::make_unique<TimelineMarker>(1, TimeRange(Rational(5), Rational(6))));
late->set_time(TimeRange(Rational(1), Rational(2)));
EXPECT_EQ(list.at(0)->time().in(), Rational(1));
EXPECT_EQ(list.at(1)->time().in(), Rational(5));
}
TEST(TimelineMarker, RemoveMarkerReturnsOwnership)
{
TimelineMarkerList list;
auto *raw = new TimelineMarker(0, TimeRange(Rational(0), Rational(5)));
list.add_marker(std::unique_ptr<TimelineMarker>(raw));
auto removed = list.remove_marker(raw);
ASSERT_TRUE(removed != nullptr);
EXPECT_EQ(removed.get(), raw);
EXPECT_EQ(list.size(), 0);
}
TEST(TimelineMarker, RemoveMissingMarkerReturnsNull)
{
TimelineMarkerList list;
TimelineMarker not_in_list;
EXPECT_EQ(list.remove_marker(&not_in_list), nullptr);
}
TEST(TimelineMarker, GetMarkerAtTime)
{
TimelineMarkerList list;
list.add_marker(
std::make_unique<TimelineMarker>(0, TimeRange(Rational(10), Rational(20))));
EXPECT_NE(list.get_marker_at_time(Rational(10)), nullptr);
EXPECT_EQ(list.get_marker_at_time(Rational(11)), nullptr);
}
TEST(TimelineMarker, MarkerAddCommandUndoRedo)
{
TimelineMarkerList list;
MarkerAddCommand cmd(&list, TimeRange(Rational(0), Rational(5)), "m", 2);
cmd.redo_now();
ASSERT_EQ(list.size(), 1);
EXPECT_EQ(list.at(0)->name(), "m");
EXPECT_EQ(list.at(0)->color(), 2);
cmd.undo_now();
EXPECT_EQ(list.size(), 0);
cmd.redo_now();
EXPECT_EQ(list.size(), 1);
}
TEST(TimelineMarker, MarkerRemoveCommandUndoRedo)
{
TimelineMarkerList list;
auto *raw = new TimelineMarker(0, TimeRange(Rational(0), Rational(5)));
list.add_marker(std::unique_ptr<TimelineMarker>(raw));
MarkerRemoveCommand cmd(raw, &list);
cmd.redo_now();
EXPECT_EQ(list.size(), 0);
cmd.undo_now();
ASSERT_EQ(list.size(), 1);
EXPECT_EQ(list.at(0), raw);
}
TEST(TimelineMarker, ChangeCommandsUndoRedo)
{
TimelineMarker marker(1, TimeRange(Rational(0), Rational(5)), "old");
MarkerChangeColorCommand color_cmd(&marker, 3);
color_cmd.redo_now();
EXPECT_EQ(marker.color(), 3);
color_cmd.undo_now();
EXPECT_EQ(marker.color(), 1);
MarkerChangeNameCommand name_cmd(&marker, "new");
name_cmd.redo_now();
EXPECT_EQ(marker.name(), "new");
name_cmd.undo_now();
EXPECT_EQ(marker.name(), "old");
MarkerChangeTimeCommand time_cmd(&marker, TimeRange(Rational(7), Rational(9)));
time_cmd.redo_now();
EXPECT_EQ(marker.time().in(), Rational(7));
time_cmd.undo_now();
EXPECT_EQ(marker.time().in(), Rational(0));
}
TEST(TimelineMarker, XmlRoundTrip)
{
TimelineMarkerList list;
list.add_marker(std::make_unique<TimelineMarker>(
3, TimeRange(Rational(1, 2), Rational(3, 4)), "hello"));
XmlStreamWriter writer;
writer.write_start_element("markers");
list.save(&writer);
writer.write_end_element();
std::string xml = writer.output();
XmlStreamReader reader(xml);
TimelineMarkerList loaded;
ASSERT_TRUE(xml_read_next_start_element(&reader));
ASSERT_TRUE(loaded.load(&reader));
ASSERT_EQ(loaded.size(), 1);
EXPECT_EQ(loaded.at(0)->name(), "hello");
EXPECT_EQ(loaded.at(0)->color(), 3);
EXPECT_EQ(loaded.at(0)->time().in(), Rational(1, 2));
EXPECT_EQ(loaded.at(0)->time().out(), Rational(3, 4));
}