diff --git a/docs/zh/plans/riir/notes.md b/docs/zh/plans/riir/notes.md index 8531f71f7..cdb4a96e8 100644 --- a/docs/zh/plans/riir/notes.md +++ b/docs/zh/plans/riir/notes.md @@ -317,3 +317,38 @@ ColorManager)去Qt化过程中的删除与语义变化,迁移调用方时需 src/codec/src/ffmpeg/CMakeLists.txt 的回归:FFmpegDecoder 构造 函数在 flat namespace 悬空,decoder probe/decode/encoder roundtrip 三个测试 jump-to-0。 + +## oaknode→oaktimeline 切到 C ABI(2026-08-07) + +- ViewerOutput 不再持有 TimelineWorkArea/TimelineMarkerList 的 C++ + 对象,改持 OakTimelineWorkArea/OakTimelineMarkerList 拥有型值句柄 + (oaktimeline_workarea_create/marker_list_create,析构时 free); + workarea_handle()/markers_handle() 返回借用副本(调用方不得 + free)。 +- oaktimeline C API 新增:marker_list_create/marker_add/ + workarea_create/workarea_set_enabled;句柄 box 增加可选 deleter + 区分借用/拥有(timelinehandle.h)。 +- oaknode_node_get_markers/get_work_area 改为经 out 参数返回 + addref 后的 OakTimeline* 句柄(include/node/node.h 只做具名 + struct 前置声明,公开 C 头互相 include 构成环,前置声明规避); + timeline/marker.h、timeline/workarea.h 的 typedef 改为具名 + struct。OakNodeMarkerList/OakNodeWorkArea 不透明指针类型删除。 +- 4 个旧项目序列化器(210528/210907/211228/220403)的 + load_work_area/load_marker_list 改走 oaktimeline C API; + ProjectSerializer::SerializedMarker POD 取代 + std::vector(LoadData::markers 与 + only_serialize_markers_),serializer230220 的 marker 读写直接 + 展开为 XML 属性读写。**行为变化**:app 层 paste/markers 接口 + 签名改变(app 尚未拆分,编译点留待 facade 阶段)。 +- Sequence::add_default_nodes 的 TimelineAddTrackCommand 改为 + oaktimeline_add_track_command + oakundo_capi::to_command/ + mark_container_owned 转入 MultiUndoCommand。 +- clip.cpp 的 Timeline::ThumbnailMode/WaveformMode 枚举改自新中立 + 头 include/timeline/displaymode.h(OAK_TIMELINE_THUMBNAIL_*/ + OAK_TIMELINE_WAVEFORMS_*,值与 olive::Timeline 枚举保持兼容)。 +- tracklist.h/block.h 里 timelinecommon.h 的死引用删除; + src/node/transition/timeline/ 四个 stub 头删除。 +- oakcommon xml C API 新增 oakcommon_xml_reader_wrap_native/ + oakcommon_xml_writer_wrap_native(C++ only 借用包装), + XmlReaderState/XmlWriterState 支持 owning/borrowed 双模式。 +- liboaknode 对 liboaktimeline 的 C++ 符号引用降为 0(nm 验证)。 diff --git a/include/common/xmlutils.h b/include/common/xmlutils.h index 0ec1e08aa..fba5f3885 100644 --- a/include/common/xmlutils.h +++ b/include/common/xmlutils.h @@ -79,6 +79,16 @@ extern "C" { olive::XmlStreamReader *oakcommon_xml_reader_get_native(OakXmlReader reader); olive::XmlStreamWriter *oakcommon_xml_writer_get_native(OakXmlWriter writer); +/** + * @brief Wrap an existing C++ reader/writer in a borrowed handle (C++ + * only, for adapter layers). The box never owns the object; the caller + * must keep it alive and release the box with + * oakcommon_xml_reader_free()/oakcommon_xml_writer_free(). Empty handle + * for a NULL object or on allocation failure. + */ +OakXmlReader oakcommon_xml_reader_wrap_native(olive::XmlStreamReader *reader); +OakXmlWriter oakcommon_xml_writer_wrap_native(olive::XmlStreamWriter *writer); + /** * @brief Release one reference to a reader. * diff --git a/include/node/node.h b/include/node/node.h index e1bf12895..5b74f75c3 100644 --- a/include/node/node.h +++ b/include/node/node.h @@ -121,12 +121,13 @@ typedef struct OakNodeProject OakNodeProject; typedef struct OakNodeFootage OakNodeFootage; /** - * @brief Opaque borrowed handles to the timeline data owned by viewer - * nodes (TimelineMarkerList / TimelineWorkArea in oaktimeline). - * oaktimeline reinterprets these into its own handle types. + * @brief Timeline data owned by viewer nodes (TimelineMarkerList / + * TimelineWorkArea in oaktimeline) cross the boundary as oaktimeline + * value handles. Forward-declared here so node.h stays self-contained; + * include timeline/marker.h / timeline/workarea.h for the definitions. */ -typedef struct OakNodeMarkerList OakNodeMarkerList; -typedef struct OakNodeWorkArea OakNodeWorkArea; +struct OakTimelineMarkerList; +struct OakTimelineWorkArea; /** * @brief Opaque borrowed handle to a node's video frame cache @@ -562,11 +563,16 @@ 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 an empty handle). + * @brief Marker list / work area of a viewer node, as addref'd + * oaktimeline value handles (release with + * oaktimeline_marker_list_free()/oaktimeline_workarea_free()). *out is + * an empty handle (ctx == NULL) when the node is not a viewer or for + * an empty node handle. */ -int oaknode_node_get_markers(OakNodeNode node, OakNodeMarkerList **out); -int oaknode_node_get_work_area(OakNodeNode node, OakNodeWorkArea **out); +int oaknode_node_get_markers(OakNodeNode node, + struct OakTimelineMarkerList *out); +int oaknode_node_get_work_area(OakNodeNode node, + struct OakTimelineWorkArea *out); /** * @brief Borrowed video frame cache of a node (NULL when the node has diff --git a/include/timeline/displaymode.h b/include/timeline/displaymode.h new file mode 100644 index 000000000..4c831036d --- /dev/null +++ b/include/timeline/displaymode.h @@ -0,0 +1,51 @@ +/*** + + 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 . + +***/ + +#ifndef OAK_EDITOR_TIMELINE_DISPLAYMODE_H +#define OAK_EDITOR_TIMELINE_DISPLAYMODE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Shared timeline display-mode constants. + * + * Neutral home for the enum values behind the TimelineThumbnailMode / + * TimelineWaveformMode config keys; mirrors olive::Timeline::ThumbnailMode + * / WaveformMode (src/timeline/src/timelinecommon.h) and must stay + * value-compatible with them. + */ +enum OakTimelineThumbnailMode { + OAK_TIMELINE_THUMBNAIL_OFF = 0, + OAK_TIMELINE_THUMBNAIL_IN_OUT = 1, + OAK_TIMELINE_THUMBNAIL_ON = 2 +}; + +enum OakTimelineWaveformMode { + OAK_TIMELINE_WAVEFORMS_DISABLED = 0, + OAK_TIMELINE_WAVEFORMS_ENABLED = 1 +}; + +#ifdef __cplusplus +} +#endif + +#endif // OAK_EDITOR_TIMELINE_DISPLAYMODE_H diff --git a/include/timeline/marker.h b/include/timeline/marker.h index f291f5146..f0b6d1049 100644 --- a/include/timeline/marker.h +++ b/include/timeline/marker.h @@ -31,21 +31,28 @@ extern "C" { #endif /** - * @brief Borrowed by-value handle to a timeline marker list - * (olive::TimelineMarkerList), owned by a viewer node. + * @brief By-value handle to a timeline marker list + * (olive::TimelineMarkerList). * - * Obtained via oaktimeline_marker_list_of(). The handle boxes a - * reference into the owning node: addref/release manage the box only, - * never the list. Release the box with oaktimeline_marker_list_free() - * (or handle.release(handle.ctx)) when done. + * Borrowed handles are obtained via oaktimeline_marker_list_of() and box + * a reference into the owning node; owning handles are created by + * oaktimeline_marker_list_create(). Either way, release with + * oaktimeline_marker_list_free() (or handle.release(handle.ctx)) when + * done — release destroys the list only for owning handles. */ typedef struct OakTimelineMarkerList { - void *ctx; /**< Opaque pointer to the borrowed object's box. */ + void *ctx; /**< Opaque pointer to the object's box. */ void (*addref)(void *ctx); /**< Atomically increments the box count. */ void (*release)(void *ctx); /**< Decrements the count, frees the box. */ uint32_t abi_version; /**< OAKTIMELINE_ABI_VERSION. */ } OakTimelineMarkerList; +/** + * @brief Create an owning handle to a new, empty marker list. Empty + * handle (ctx == NULL) on allocation failure. + */ +OakTimelineMarkerList oaktimeline_marker_list_create(void); + /** * @brief Borrowed marker list of a viewer node (sequence). Empty handle * (ctx == NULL) for an empty node handle or when the node is not a @@ -54,11 +61,20 @@ typedef struct OakTimelineMarkerList { OakTimelineMarkerList oaktimeline_marker_list_of(OakNodeNode owner); /** - * @brief Release a borrowed marker list box (never the list itself). - * NULL / empty-handle no-op; clears list->ctx after releasing. + * @brief Release a marker list handle (destroys the list itself only + * for owning handles). NULL / empty-handle no-op; clears + * list->ctx after releasing. */ void oaktimeline_marker_list_free(OakTimelineMarkerList *list); +/** + * @brief Append a marker directly (no undo command). name may be NULL + * for an empty name. + */ +int oaktimeline_marker_add(OakTimelineMarkerList list, int in_num, + int in_den, int out_num, int out_den, + const char *name, int color); + /** * @brief Number of markers. Out-param convention; OAKTIMELINE_E_INVALID * for empty/NULL arguments. diff --git a/include/timeline/workarea.h b/include/timeline/workarea.h index 3f90b62ae..57058ae77 100644 --- a/include/timeline/workarea.h +++ b/include/timeline/workarea.h @@ -31,21 +31,28 @@ extern "C" { #endif /** - * @brief Borrowed by-value handle to a timeline work area - * (olive::TimelineWorkArea), owned by a viewer node. + * @brief By-value handle to a timeline work area + * (olive::TimelineWorkArea). * - * Obtained via oaktimeline_workarea_of(). The handle boxes a reference - * into the owning node: addref/release manage the box only, never the - * work area. Release the box with oaktimeline_workarea_free() (or - * handle.release(handle.ctx)) when done. + * Borrowed handles are obtained via oaktimeline_workarea_of() and box a + * reference into the owning node; owning handles are created by + * oaktimeline_workarea_create(). Either way, release with + * oaktimeline_workarea_free() (or handle.release(handle.ctx)) when + * done — release destroys the work area only for owning handles. */ typedef struct OakTimelineWorkArea { - void *ctx; /**< Opaque pointer to the borrowed object's box. */ + void *ctx; /**< Opaque pointer to the object's box. */ void (*addref)(void *ctx); /**< Atomically increments the box count. */ void (*release)(void *ctx); /**< Decrements the count, frees the box. */ uint32_t abi_version; /**< OAKTIMELINE_ABI_VERSION. */ } OakTimelineWorkArea; +/** + * @brief Create an owning handle to a new, default-constructed work + * area. Empty handle (ctx == NULL) on allocation failure. + */ +OakTimelineWorkArea oaktimeline_workarea_create(void); + /** * @brief Borrowed work area of a viewer node (sequence). Empty handle * (ctx == NULL) for an empty node handle or when the node is not a @@ -54,11 +61,17 @@ typedef struct OakTimelineWorkArea { OakTimelineWorkArea oaktimeline_workarea_of(OakNodeNode owner); /** - * @brief Release a borrowed work area box (never the work area itself). - * NULL / empty-handle no-op; clears w->ctx after releasing. + * @brief Release a work area handle (destroys the work area itself only + * for owning handles). NULL / empty-handle no-op; clears w->ctx + * after releasing. */ void oaktimeline_workarea_free(OakTimelineWorkArea *w); +/** + * @brief Set enabled directly (live). + */ +int oaktimeline_workarea_set_enabled(OakTimelineWorkArea w, int enabled); + /** * @brief Read the work area state. Out params may individually be NULL. */ diff --git a/src/common/c_api/xmlutils.cpp b/src/common/c_api/xmlutils.cpp index da85db5ff..4198a390c 100644 --- a/src/common/c_api/xmlutils.cpp +++ b/src/common/c_api/xmlutils.cpp @@ -21,6 +21,7 @@ #include "common/xmlutils.h" #include +#include #include #include "../src/xmlutils.h" @@ -31,14 +32,45 @@ namespace /** * @brief Reader state boxed behind the handle's ctx pointer. + * + * Owning states (oakcommon_xml_reader_init) hold the reader via + * `owned`; borrowed states (oakcommon_xml_reader_wrap_native) leave it + * null and never delete the reader. */ struct XmlReaderState { - olive::XmlStreamReader reader; + olive::XmlStreamReader *reader; + std::unique_ptr owned; std::string cached_text; bool has_cached_text = false; explicit XmlReaderState(const char *data) - : reader(data) + : reader(new olive::XmlStreamReader(data)) + , owned(reader) + { + } + + explicit XmlReaderState(olive::XmlStreamReader *borrowed) + : reader(borrowed) + { + } +}; + +/** + * @brief Writer state boxed behind the handle's ctx pointer (same + * owning/borrowed split as XmlReaderState). + */ +struct XmlWriterState { + olive::XmlStreamWriter *writer; + std::unique_ptr owned; + + XmlWriterState() + : writer(new olive::XmlStreamWriter()) + , owned(writer) + { + } + + explicit XmlWriterState(olive::XmlStreamWriter *borrowed) + : writer(borrowed) { } }; @@ -56,7 +88,8 @@ XmlReaderState *xr(OakXmlReader reader) */ olive::XmlStreamWriter *xw(OakXmlWriter writer) { - return oakcommon::handle_impl(writer.ctx); + XmlWriterState *state = oakcommon::handle_impl(writer.ctx); + return state ? state->writer : nullptr; } /** @@ -84,8 +117,8 @@ OakXmlReader oakcommon_xml_reader_init(const char *data) if (!data) return h; try { - return oakcommon::make_handle( - XmlReaderState(data)); + return oakcommon::make_handle_in_place( + data); } catch (...) { OakXmlReader empty = {}; return empty; @@ -104,7 +137,7 @@ int oakcommon_xml_reader_read_next_start_element(OakXmlReader reader, return OAKCOMMON_E_INVALID; try { xr(reader)->has_cached_text = false; - *found = olive::xml_read_next_start_element(&xr(reader)->reader) ? 1 : 0; + *found = olive::xml_read_next_start_element(xr(reader)->reader) ? 1 : 0; return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; @@ -117,7 +150,7 @@ int oakcommon_xml_reader_name(OakXmlReader reader, char *buf, if (!xr(reader)) return OAKCOMMON_E_INVALID; try { - return copy_string(xr(reader)->reader.name(), buf, buf_size); + return copy_string(xr(reader)->reader->name(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } @@ -132,7 +165,7 @@ int oakcommon_xml_reader_read_element_text(OakXmlReader reader, // read_element_text() consumes the stream, so cache the result to // keep the two-stage (size query then copy) buffer convention working. if (!xr(reader)->has_cached_text) { - xr(reader)->cached_text = xr(reader)->reader.read_element_text(); + xr(reader)->cached_text = xr(reader)->reader->read_element_text(); xr(reader)->has_cached_text = true; } return copy_string(xr(reader)->cached_text, buf, buf_size); @@ -147,7 +180,7 @@ int oakcommon_xml_reader_skip_current_element(OakXmlReader reader) return OAKCOMMON_E_INVALID; try { xr(reader)->has_cached_text = false; - xr(reader)->reader.skip_current_element(); + xr(reader)->reader->skip_current_element(); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; @@ -160,7 +193,7 @@ int oakcommon_xml_reader_attribute_count(OakXmlReader reader, if (!xr(reader) || !count) return OAKCOMMON_E_INVALID; try { - *count = static_cast(xr(reader)->reader.attributes().size()); + *count = static_cast(xr(reader)->reader->attributes().size()); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; @@ -173,7 +206,7 @@ int oakcommon_xml_reader_attribute_name(OakXmlReader reader, int index, if (!xr(reader)) return OAKCOMMON_E_INVALID; try { - const auto &attrs = xr(reader)->reader.attributes(); + const auto &attrs = xr(reader)->reader->attributes(); if (index < 0 || index >= static_cast(attrs.size())) return OAKCOMMON_E_NOT_FOUND; return copy_string(attrs[index].name, buf, buf_size); @@ -188,7 +221,7 @@ int oakcommon_xml_reader_attribute_value(OakXmlReader reader, if (!xr(reader)) return OAKCOMMON_E_INVALID; try { - const auto &attrs = xr(reader)->reader.attributes(); + const auto &attrs = xr(reader)->reader->attributes(); if (index < 0 || index >= static_cast(attrs.size())) return OAKCOMMON_E_NOT_FOUND; return copy_string(attrs[index].value, buf, buf_size); @@ -203,7 +236,7 @@ int oakcommon_xml_reader_has_error(OakXmlReader reader, if (!xr(reader) || !has_error) return OAKCOMMON_E_INVALID; try { - *has_error = xr(reader)->reader.has_error() ? 1 : 0; + *has_error = xr(reader)->reader->has_error() ? 1 : 0; return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; @@ -213,8 +246,7 @@ int oakcommon_xml_reader_has_error(OakXmlReader reader, OakXmlWriter oakcommon_xml_writer_init(void) { try { - return oakcommon::make_handle( - olive::XmlStreamWriter()); + return oakcommon::make_handle_in_place(); } catch (...) { OakXmlWriter h = {}; return h; @@ -322,7 +354,7 @@ olive::XmlStreamReader *oakcommon_xml_reader_get_native(OakXmlReader reader) if (!reader.ctx) { return nullptr; } - return &oakcommon::handle_impl(reader.ctx)->reader; + return oakcommon::handle_impl(reader.ctx)->reader; } olive::XmlStreamWriter *oakcommon_xml_writer_get_native(OakXmlWriter writer) @@ -330,5 +362,31 @@ olive::XmlStreamWriter *oakcommon_xml_writer_get_native(OakXmlWriter writer) if (!writer.ctx) { return nullptr; } - return oakcommon::handle_impl(writer.ctx); + return xw(writer); +} + +OakXmlReader oakcommon_xml_reader_wrap_native(olive::XmlStreamReader *reader) +{ + if (!reader) { + return OakXmlReader{}; + } + try { + return oakcommon::make_handle_in_place( + reader); + } catch (...) { + return OakXmlReader{}; + } +} + +OakXmlWriter oakcommon_xml_writer_wrap_native(olive::XmlStreamWriter *writer) +{ + if (!writer) { + return OakXmlWriter{}; + } + try { + return oakcommon::make_handle_in_place( + writer); + } catch (...) { + return OakXmlWriter{}; + } } diff --git a/src/common/tests/xmlutils_test.cpp b/src/common/tests/xmlutils_test.cpp index abace915d..bee837908 100644 --- a/src/common/tests/xmlutils_test.cpp +++ b/src/common/tests/xmlutils_test.cpp @@ -26,6 +26,8 @@ #include "common/xmlutils.h" +#include "../src/xmlutils.h" + namespace { @@ -326,3 +328,52 @@ TEST(CommonXmlUtilsCApi, WriterRoundTrip) oakcommon_xml_reader_free(&r); } + +TEST(CommonXmlUtilsCApi, WrapNativeNull) +{ + EXPECT_EQ(oakcommon_xml_reader_wrap_native(nullptr).ctx, nullptr); + EXPECT_EQ(oakcommon_xml_writer_wrap_native(nullptr).ctx, nullptr); +} + +TEST(CommonXmlUtilsCApi, WrapNativeReaderRoundTrip) +{ + olive::XmlStreamReader native("text"); + OakXmlReader r = oakcommon_xml_reader_wrap_native(&native); + ASSERT_NE(r.ctx, nullptr); + + // Borrowed box and native object must observe the same stream + EXPECT_EQ(oakcommon_xml_reader_get_native(r), &native); + + int found = 0; + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), + OAKCOMMON_OK); + EXPECT_EQ(found, 1); + + char name[32]; + EXPECT_GT(oakcommon_xml_reader_name(r, name, sizeof(name)), 0); + EXPECT_STREQ(name, "root"); + + oakcommon_xml_reader_free(&r); + EXPECT_EQ(r.ctx, nullptr); +} + +TEST(CommonXmlUtilsCApi, WrapNativeWriterRoundTrip) +{ + olive::XmlStreamWriter native; + OakXmlWriter w = oakcommon_xml_writer_wrap_native(&native); + ASSERT_NE(w.ctx, nullptr); + + EXPECT_EQ(oakcommon_xml_writer_get_native(w), &native); + EXPECT_EQ(oakcommon_xml_writer_write_start_element(w, "root"), + OAKCOMMON_OK); + EXPECT_EQ(oakcommon_xml_writer_write_end_element(w), OAKCOMMON_OK); + + char out[64]; + int needed = oakcommon_xml_writer_output(w, out, sizeof(out)); + EXPECT_GT(needed, 0); + EXPECT_LT(needed, int(sizeof(out))); + EXPECT_NE(strstr(out, "root"), nullptr); + + oakcommon_xml_writer_free(&w); + EXPECT_EQ(w.ctx, nullptr); +} diff --git a/src/node/c_api/node.cpp b/src/node/c_api/node.cpp index a62247c3a..887f37e1a 100644 --- a/src/node/c_api/node.cpp +++ b/src/node/c_api/node.cpp @@ -24,6 +24,8 @@ #include "node/footage.h" #include "node/project.h" #include "olive/core/oakcore/audioparams.h" +#include "timeline/marker.h" +#include "timeline/workarea.h" #include #include @@ -1279,7 +1281,8 @@ OakUndoCommand oaknode_command_create_set_position_recursive( } } -int oaknode_node_get_markers(OakNodeNode node, OakNodeMarkerList **out) +int oaknode_node_get_markers(OakNodeNode node, + struct OakTimelineMarkerList *out) { if (!node.ctx || !out) { return OAKNODE_E_INVALID; @@ -1287,14 +1290,18 @@ int oaknode_node_get_markers(OakNodeNode node, OakNodeMarkerList **out) const olive::Node *n = oaknode_c_api::to_native(node); if (auto *v = dynamic_cast(n)) { - *out = reinterpret_cast(v->get_markers()); + *out = v->markers_handle(); + if (out->ctx) { + out->addref(out->ctx); + } } else { - *out = NULL; + *out = OakTimelineMarkerList{}; } return OAKNODE_OK; } -int oaknode_node_get_work_area(OakNodeNode node, OakNodeWorkArea **out) +int oaknode_node_get_work_area(OakNodeNode node, + struct OakTimelineWorkArea *out) { if (!node.ctx || !out) { return OAKNODE_E_INVALID; @@ -1302,9 +1309,12 @@ int oaknode_node_get_work_area(OakNodeNode node, OakNodeWorkArea **out) const olive::Node *n = oaknode_c_api::to_native(node); if (auto *v = dynamic_cast(n)) { - *out = reinterpret_cast(v->get_work_area()); + *out = v->workarea_handle(); + if (out->ctx) { + out->addref(out->ctx); + } } else { - *out = NULL; + *out = OakTimelineWorkArea{}; } return OAKNODE_OK; } diff --git a/src/node/src/block/block.h b/src/node/src/block/block.h index f4197c451..3cef4952f 100644 --- a/src/node/src/block/block.h +++ b/src/node/src/block/block.h @@ -23,12 +23,12 @@ #define OAK_BLOCK_H #include "node.h" -#include "timeline/timelinecommon.h" namespace olive { class TransitionBlock; +class Track; /** * @brief A Node that represents a block of time, also displayable on a Timeline diff --git a/src/node/src/block/clip/clip.cpp b/src/node/src/block/clip/clip.cpp index 9f02cb4ae..8f6171346 100644 --- a/src/node/src/block/clip/clip.cpp +++ b/src/node/src/block/clip/clip.cpp @@ -30,7 +30,7 @@ #include "output/viewer/viewer.h" #include "project/sequence/sequence.h" #include "sliderdisplaytype.h" -#include "sliderdisplaytype.h" +#include "timeline/displaymode.h" namespace olive { @@ -297,7 +297,7 @@ void ClipBlock::request_range_from_connected(const TimeRange &range) request_range_for_cache( connected->waveform_cache(), max_range, range, true, (OAK_CONFIG("TimelineWaveformMode").to_int() == - Timeline::k_waveforms_enabled)); + OAK_TIMELINE_WAVEFORMS_ENABLED)); // Handle audio cache request_range_for_cache(connected->audio_playback_cache(), @@ -336,7 +336,7 @@ void ClipBlock::request_invalidated_from_connected(bool force_all, } else if (type == Track::k_audio) { // Handle waveforms if (OAK_CONFIG("TimelineWaveformMode").to_int() == - Timeline::k_waveforms_enabled) { + OAK_TIMELINE_WAVEFORMS_ENABLED) { request_invalidated_for_cache(connected->waveform_cache(), max_range); } @@ -383,12 +383,11 @@ void ClipBlock::request_invalidated_for_cache(PlaybackCache *cache, bool ClipBlock::get_adjusted_thumbnail_range(TimeRange *r) const { - switch (static_cast( - OAK_CONFIG("TimelineThumbnailMode").to_int())) { - case Timeline::k_thumbnail_off: + switch (OAK_CONFIG("TimelineThumbnailMode").to_int()) { + case OAK_TIMELINE_THUMBNAIL_OFF: // Don't cache any range return false; - case Timeline::k_thumbnail_in_out: { + case OAK_TIMELINE_THUMBNAIL_IN_OUT: { // Only cache in point Rational in = this->media_range().in(); if (r->contains(in)) { @@ -400,7 +399,7 @@ bool ClipBlock::get_adjusted_thumbnail_range(TimeRange *r) const return false; } } - case Timeline::k_thumbnail_on: + case OAK_TIMELINE_THUMBNAIL_ON: // Cache entire range return true; } diff --git a/src/node/src/output/track/tracklist.h b/src/node/src/output/track/tracklist.h index 06383eb00..4b2d688c9 100644 --- a/src/node/src/output/track/tracklist.h +++ b/src/node/src/output/track/tracklist.h @@ -27,7 +27,6 @@ #include #include "output/track/track.h" -#include "timeline/timelinecommon.h" namespace olive { diff --git a/src/node/src/output/viewer/viewer.cpp b/src/node/src/output/viewer/viewer.cpp index f8be3cd0b..d7ddb7912 100644 --- a/src/node/src/output/viewer/viewer.cpp +++ b/src/node/src/output/viewer/viewer.cpp @@ -21,6 +21,7 @@ #include "viewer.h" +#include "common/xmlutils.h" #include "configaccessor.h" #include "coreengine.h" #include "traverser.h" @@ -77,8 +78,15 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, set_flag(k_dont_show_in_param_view); - workarea_ = std::make_unique(); - markers_ = std::make_unique(); + workarea_ = oaktimeline_workarea_create(); + markers_ = oaktimeline_marker_list_create(); +} + +ViewerOutput::~ViewerOutput() +{ + disconnect_all(); + oaktimeline_workarea_free(&workarea_); + oaktimeline_marker_list_free(&markers_); } std::string ViewerOutput::name() const @@ -452,32 +460,44 @@ void ViewerOutput::value(const NodeValueRow &value, const NodeGlobals &globals, bool ViewerOutput::load_custom(XmlStreamReader *reader, SerializedData *data) { - while (xml_read_next_start_element(reader)) { + OakXmlReader xml = oakcommon_xml_reader_wrap_native(reader); + if (!xml.ctx) { + return false; + } + + bool ok = true; + while (ok && xml_read_next_start_element(reader)) { if (reader->name() == "markers") { - if (!this->get_markers()->load(reader)) { - return false; - } + ok = oaktimeline_marker_list_load(markers_, xml) == + OAKTIMELINE_OK; } else if (reader->name() == "workarea") { - if (!this->get_work_area()->load(reader)) { - return false; - } + ok = oaktimeline_workarea_load(workarea_, xml) == + OAKTIMELINE_OK; } else { reader->skip_current_element(); } } - return true; + oakcommon_xml_reader_free(&xml); + return ok; } void ViewerOutput::save_custom(XmlStreamWriter *writer) const { + OakXmlWriter xml = oakcommon_xml_writer_wrap_native(writer); + if (!xml.ctx) { + return; + } + writer->write_start_element("workarea"); - this->get_work_area()->save(writer); + oaktimeline_workarea_save(workarea_, xml); writer->write_end_element(); // workarea writer->write_start_element("markers"); - this->get_markers()->save(writer); + oaktimeline_marker_list_save(markers_, xml); writer->write_end_element(); // markers + + oakcommon_xml_writer_free(&xml); } void ViewerOutput::InputValueChangedEvent(const std::string &input, int element) diff --git a/src/node/src/output/viewer/viewer.h b/src/node/src/output/viewer/viewer.h index a9905cc3e..3014f37e3 100644 --- a/src/node/src/output/viewer/viewer.h +++ b/src/node/src/output/viewer/viewer.h @@ -31,8 +31,8 @@ #include "render/framehashcache.h" #include "subtitleparams.h" #include "videoparams.h" -#include "timeline/timelinemarker.h" -#include "timeline/timelineworkarea.h" +#include "timeline/marker.h" +#include "timeline/workarea.h" namespace olive { @@ -48,8 +48,9 @@ class ViewerOutput : public Node { public: ViewerOutput(bool create_buffer_inputs = true, bool create_default_streams = true); + virtual ~ViewerOutput() override; - NODE_DEFAULT_FUNCTIONS(ViewerOutput) + NODE_COPY_FUNCTION(ViewerOutput) virtual std::string name() const override; virtual std::string id() const override; @@ -174,13 +175,18 @@ public: return audio_length_; } - TimelineWorkArea *get_work_area() const + /** + * @brief Borrowed copies of the timeline handles owned by this + * viewer. Callers must NOT free them; addref first to keep + * one beyond the viewer's lifetime. + */ + const OakTimelineWorkArea &workarea_handle() const { - return workarea_.get(); + return workarea_; } - TimelineMarkerList *get_markers() const + const OakTimelineMarkerList &markers_handle() const { - return markers_.get(); + return markers_; } virtual TimeRange get_video_cache_range() const override @@ -275,8 +281,8 @@ private: AudioParams cached_audio_params_; - std::unique_ptr workarea_; - std::unique_ptr markers_; + OakTimelineWorkArea workarea_ = {}; + OakTimelineMarkerList markers_ = {}; bool autocache_input_video_; bool autocache_input_audio_; diff --git a/src/node/src/project/sequence/sequence.cpp b/src/node/src/project/sequence/sequence.cpp index a3a39968e..86264a430 100644 --- a/src/node/src/project/sequence/sequence.cpp +++ b/src/node/src/project/sequence/sequence.cpp @@ -21,7 +21,10 @@ #include "sequence.h" -#include "timeline/timelineundogeneral.h" +#include "timeline/edit.h" +#include "undo/undocommand.h" +#include "../../../c_api/nodehandle.h" +#include "../../../../undo/c_api/commandhandle.h" namespace olive { @@ -85,21 +88,25 @@ void Sequence::add_default_nodes(MultiUndoCommand *command) { // Create tracks and connect them to the viewer // (borrowed handles: the track lists are owned by this sequence) - UndoCommand *video_track_command = new TimelineAddTrackCommand( + OakUndoCommand video_track_command = oaktimeline_add_track_command( oaknode_c_api::make_handle( track_list(Track::k_video), false, nullptr)); - UndoCommand *audio_track_command = new TimelineAddTrackCommand( + OakUndoCommand audio_track_command = oaktimeline_add_track_command( oaknode_c_api::make_handle( track_list(Track::k_audio), false, nullptr)); if (command) { - command->add_child(video_track_command); - command->add_child(audio_track_command); + command->add_child(oakundo_capi::to_command(video_track_command)); + oakundo_capi::mark_container_owned(video_track_command); + command->add_child(oakundo_capi::to_command(audio_track_command)); + oakundo_capi::mark_container_owned(audio_track_command); + oakundo_command_free(&video_track_command); + oakundo_command_free(&audio_track_command); } else { - video_track_command->redo_now(); - audio_track_command->redo_now(); - delete video_track_command; - delete audio_track_command; + oakundo_command_redo_now(video_track_command); + oakundo_command_redo_now(audio_track_command); + oakundo_command_free(&video_track_command); + oakundo_command_free(&audio_track_command); } } diff --git a/src/node/src/project/serializer/serializer.h b/src/node/src/project/serializer/serializer.h index dcd5a7dba..8264b4101 100644 --- a/src/node/src/project/serializer/serializer.h +++ b/src/node/src/project/serializer/serializer.h @@ -30,6 +30,8 @@ #include "define.h" #include "project.h" #include "project/serializer/serializedlayoutinfo.h" +#include "timeline/marker.h" +#include "timeline/workarea.h" #include "typeserializer.h" namespace olive @@ -75,13 +77,27 @@ public: using SerializedKeyframes = std::map>; + /** + * @brief Plain-data snapshot of a timeline marker. + * + * Markers live in oaktimeline; serializers ferry them across the + * module boundary as plain data instead of C++ TimelineMarker + * pointers (01 §0 铁律 6). + */ + struct SerializedMarker { + core::Rational in; + core::Rational out; + std::string name; + int color = 0; + }; + class LoadData { public: LoadData() = default; SerializedProperties properties; - std::vector markers; + std::vector markers; SerializedKeyframes keyframes; @@ -198,12 +214,12 @@ public: void set_only_serialize_nodes_and_resolve_groups( std::vector only); - const std::vector &get_only_serialize_markers() const + const std::vector &get_only_serialize_markers() const { return only_serialize_markers_; } void set_only_serialize_markers( - const std::vector &only) + const std::vector &only) { only_serialize_markers_ = only; } @@ -240,7 +256,7 @@ public: SerializedProperties properties_; - std::vector only_serialize_markers_; + std::vector only_serialize_markers_; std::vector only_serialize_keyframes_; }; diff --git a/src/node/src/project/serializer/serializer210528.cpp b/src/node/src/project/serializer/serializer210528.cpp index 779b642bd..ccecfd7a7 100644 --- a/src/node/src/project/serializer/serializer210528.cpp +++ b/src/node/src/project/serializer/serializer210528.cpp @@ -784,9 +784,9 @@ void ProjectSerializer210528::load_timeline_points(XmlStreamReader *reader, { while (xml_read_next_start_element(reader)) { if (reader->name() == "markers") { - load_marker_list(reader, points->get_markers()); + load_marker_list(reader, points->markers_handle()); } else if (reader->name() == "workarea") { - load_work_area(reader, points->get_work_area()); + load_work_area(reader, points->workarea_handle()); } else { reader->skip_current_element(); } @@ -794,14 +794,23 @@ void ProjectSerializer210528::load_timeline_points(XmlStreamReader *reader, } void ProjectSerializer210528::load_work_area(XmlStreamReader *reader, - TimelineWorkArea *workarea) const + const OakTimelineWorkArea &workarea) const { - Rational range_in = workarea->in(); - Rational range_out = workarea->out(); + int cur_in_num, cur_in_den, cur_out_num, cur_out_den; + if (oaktimeline_workarea_get(workarea, &cur_in_num, &cur_in_den, + &cur_out_num, &cur_out_den, + NULL) != OAKTIMELINE_OK) { + reader->skip_current_element(); + return; + } + + Rational range_in(cur_in_num, cur_in_den); + Rational range_out(cur_out_num, cur_out_den); for (const XmlStreamAttribute &attr : reader->attributes()) { if (attr.name == "enabled") { - workarea->set_enabled(attr.value != "0"); + oaktimeline_workarea_set_enabled(workarea, + attr.value != "0"); } else if (attr.name == "in") { range_in = Rational::from_string(attr.value); } else if (attr.name == "out") { @@ -809,17 +818,18 @@ void ProjectSerializer210528::load_work_area(XmlStreamReader *reader, } } - TimeRange loaded_workarea(range_in, range_out); - - if (loaded_workarea != workarea->range()) { - workarea->set_range(loaded_workarea); + if (range_in != Rational(cur_in_num, cur_in_den) || + range_out != Rational(cur_out_num, cur_out_den)) { + oaktimeline_workarea_set_range(workarea, range_in.numerator(), + range_in.denominator(), + range_out.numerator(), + range_out.denominator()); } reader->skip_current_element(); } - void ProjectSerializer210528::load_marker_list(XmlStreamReader *reader, - TimelineMarkerList *markers) const + const OakTimelineMarkerList &markers) const { while (xml_read_next_start_element(reader)) { if (reader->name() == "marker") { @@ -838,14 +848,15 @@ void ProjectSerializer210528::load_marker_list(XmlStreamReader *reader, // MarkerColor resolves through configaccessor.h // (oakcommon_config_* C ABI) - markers->add_marker(std::make_unique( - OAK_CONFIG("MarkerColor").toInt(), TimeRange(in, out), name)); + oaktimeline_marker_add(markers, in.numerator(), in.denominator(), + out.numerator(), out.denominator(), + name.c_str(), + OAK_CONFIG("MarkerColor").toInt()); } reader->skip_current_element(); } } - void ProjectSerializer210528::load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const { diff --git a/src/node/src/project/serializer/serializer210528.h b/src/node/src/project/serializer/serializer210528.h index 56531eb81..2325a2862 100644 --- a/src/node/src/project/serializer/serializer210528.h +++ b/src/node/src/project/serializer/serializer210528.h @@ -93,10 +93,10 @@ private: ViewerOutput *points) const; void load_work_area(XmlStreamReader *reader, - TimelineWorkArea *workarea) const; + const OakTimelineWorkArea &workarea) const; void load_marker_list(XmlStreamReader *reader, - TimelineMarkerList *markers) const; + const OakTimelineMarkerList &markers) const; void load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const; }; diff --git a/src/node/src/project/serializer/serializer210907.cpp b/src/node/src/project/serializer/serializer210907.cpp index da363e910..353e429ad 100644 --- a/src/node/src/project/serializer/serializer210907.cpp +++ b/src/node/src/project/serializer/serializer210907.cpp @@ -773,9 +773,9 @@ void ProjectSerializer210907::load_timeline_points(XmlStreamReader *reader, { while (xml_read_next_start_element(reader)) { if (reader->name() == "markers") { - load_marker_list(reader, points->get_markers()); + load_marker_list(reader, points->markers_handle()); } else if (reader->name() == "workarea") { - load_work_area(reader, points->get_work_area()); + load_work_area(reader, points->workarea_handle()); } else { reader->skip_current_element(); } @@ -783,14 +783,23 @@ void ProjectSerializer210907::load_timeline_points(XmlStreamReader *reader, } void ProjectSerializer210907::load_work_area(XmlStreamReader *reader, - TimelineWorkArea *workarea) const + const OakTimelineWorkArea &workarea) const { - Rational range_in = workarea->in(); - Rational range_out = workarea->out(); + int cur_in_num, cur_in_den, cur_out_num, cur_out_den; + if (oaktimeline_workarea_get(workarea, &cur_in_num, &cur_in_den, + &cur_out_num, &cur_out_den, + NULL) != OAKTIMELINE_OK) { + reader->skip_current_element(); + return; + } + + Rational range_in(cur_in_num, cur_in_den); + Rational range_out(cur_out_num, cur_out_den); for (const XmlStreamAttribute &attr : reader->attributes()) { if (attr.name == "enabled") { - workarea->set_enabled(attr.value != "0"); + oaktimeline_workarea_set_enabled(workarea, + attr.value != "0"); } else if (attr.name == "in") { range_in = Rational::from_string(attr.value); } else if (attr.name == "out") { @@ -798,17 +807,18 @@ void ProjectSerializer210907::load_work_area(XmlStreamReader *reader, } } - TimeRange loaded_workarea(range_in, range_out); - - if (loaded_workarea != workarea->range()) { - workarea->set_range(loaded_workarea); + if (range_in != Rational(cur_in_num, cur_in_den) || + range_out != Rational(cur_out_num, cur_out_den)) { + oaktimeline_workarea_set_range(workarea, range_in.numerator(), + range_in.denominator(), + range_out.numerator(), + range_out.denominator()); } reader->skip_current_element(); } - void ProjectSerializer210907::load_marker_list(XmlStreamReader *reader, - TimelineMarkerList *markers) const + const OakTimelineMarkerList &markers) const { while (xml_read_next_start_element(reader)) { if (reader->name() == "marker") { @@ -827,14 +837,15 @@ void ProjectSerializer210907::load_marker_list(XmlStreamReader *reader, // MarkerColor resolves through configaccessor.h // (oakcommon_config_* C ABI) - markers->add_marker(std::make_unique( - OAK_CONFIG("MarkerColor").toInt(), TimeRange(in, out), name)); + oaktimeline_marker_add(markers, in.numerator(), in.denominator(), + out.numerator(), out.denominator(), + name.c_str(), + OAK_CONFIG("MarkerColor").toInt()); } reader->skip_current_element(); } } - void ProjectSerializer210907::load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const { diff --git a/src/node/src/project/serializer/serializer210907.h b/src/node/src/project/serializer/serializer210907.h index e09250f77..e9317ac99 100644 --- a/src/node/src/project/serializer/serializer210907.h +++ b/src/node/src/project/serializer/serializer210907.h @@ -92,10 +92,10 @@ private: ViewerOutput *points) const; void load_work_area(XmlStreamReader *reader, - TimelineWorkArea *workarea) const; + const OakTimelineWorkArea &workarea) const; void load_marker_list(XmlStreamReader *reader, - TimelineMarkerList *markers) const; + const OakTimelineMarkerList &markers) const; void load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const; }; diff --git a/src/node/src/project/serializer/serializer211228.cpp b/src/node/src/project/serializer/serializer211228.cpp index b1635b8f0..e7ebc9485 100644 --- a/src/node/src/project/serializer/serializer211228.cpp +++ b/src/node/src/project/serializer/serializer211228.cpp @@ -827,9 +827,9 @@ void ProjectSerializer211228::load_timeline_points(XmlStreamReader *reader, { while (xml_read_next_start_element(reader)) { if (reader->name() == "markers") { - load_marker_list(reader, points->get_markers()); + load_marker_list(reader, points->markers_handle()); } else if (reader->name() == "workarea") { - load_work_area(reader, points->get_work_area()); + load_work_area(reader, points->workarea_handle()); } else { reader->skip_current_element(); } @@ -837,14 +837,23 @@ void ProjectSerializer211228::load_timeline_points(XmlStreamReader *reader, } void ProjectSerializer211228::load_work_area(XmlStreamReader *reader, - TimelineWorkArea *workarea) const + const OakTimelineWorkArea &workarea) const { - Rational range_in = workarea->in(); - Rational range_out = workarea->out(); + int cur_in_num, cur_in_den, cur_out_num, cur_out_den; + if (oaktimeline_workarea_get(workarea, &cur_in_num, &cur_in_den, + &cur_out_num, &cur_out_den, + NULL) != OAKTIMELINE_OK) { + reader->skip_current_element(); + return; + } + + Rational range_in(cur_in_num, cur_in_den); + Rational range_out(cur_out_num, cur_out_den); for (const XmlStreamAttribute &attr : reader->attributes()) { if (attr.name == "enabled") { - workarea->set_enabled(attr.value != "0"); + oaktimeline_workarea_set_enabled(workarea, + attr.value != "0"); } else if (attr.name == "in") { range_in = Rational::from_string(attr.value); } else if (attr.name == "out") { @@ -852,17 +861,18 @@ void ProjectSerializer211228::load_work_area(XmlStreamReader *reader, } } - TimeRange loaded_workarea(range_in, range_out); - - if (loaded_workarea != workarea->range()) { - workarea->set_range(loaded_workarea); + if (range_in != Rational(cur_in_num, cur_in_den) || + range_out != Rational(cur_out_num, cur_out_den)) { + oaktimeline_workarea_set_range(workarea, range_in.numerator(), + range_in.denominator(), + range_out.numerator(), + range_out.denominator()); } reader->skip_current_element(); } - void ProjectSerializer211228::load_marker_list(XmlStreamReader *reader, - TimelineMarkerList *markers) const + const OakTimelineMarkerList &markers) const { while (xml_read_next_start_element(reader)) { if (reader->name() == "marker") { @@ -881,14 +891,15 @@ void ProjectSerializer211228::load_marker_list(XmlStreamReader *reader, // MarkerColor resolves through configaccessor.h // (oakcommon_config_* C ABI) - markers->add_marker(std::make_unique( - OAK_CONFIG("MarkerColor").toInt(), TimeRange(in, out), name)); + oaktimeline_marker_add(markers, in.numerator(), in.denominator(), + out.numerator(), out.denominator(), + name.c_str(), + OAK_CONFIG("MarkerColor").toInt()); } reader->skip_current_element(); } } - void ProjectSerializer211228::load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const { diff --git a/src/node/src/project/serializer/serializer211228.h b/src/node/src/project/serializer/serializer211228.h index 8dd92f8a2..1c5434db7 100644 --- a/src/node/src/project/serializer/serializer211228.h +++ b/src/node/src/project/serializer/serializer211228.h @@ -93,10 +93,10 @@ private: ViewerOutput *points) const; void load_work_area(XmlStreamReader *reader, - TimelineWorkArea *workarea) const; + const OakTimelineWorkArea &workarea) const; void load_marker_list(XmlStreamReader *reader, - TimelineMarkerList *markers) const; + const OakTimelineMarkerList &markers) const; void load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const; }; diff --git a/src/node/src/project/serializer/serializer220403.cpp b/src/node/src/project/serializer/serializer220403.cpp index 4784784ff..dd2402782 100644 --- a/src/node/src/project/serializer/serializer220403.cpp +++ b/src/node/src/project/serializer/serializer220403.cpp @@ -267,8 +267,9 @@ ProjectSerializer220403::load(Project *project, XmlStreamReader *reader, } else if (reader->name() == "markers") { while (xml_read_next_start_element(reader)) { if (reader->name() == "marker") { - TimelineMarker *marker = new TimelineMarker(); - load_marker(reader, marker); + SerializedMarker marker; + marker.color = OAK_CONFIG("MarkerColor").toInt(); + load_marker(reader, &marker); load_data.markers.push_back(marker); } else { reader->skip_current_element(); @@ -1103,9 +1104,9 @@ void ProjectSerializer220403::load_timeline_points(XmlStreamReader *reader, { while (xml_read_next_start_element(reader)) { if (reader->name() == "markers") { - load_marker_list(reader, viewer->get_markers()); + load_marker_list(reader, viewer->markers_handle()); } else if (reader->name() == "workarea") { - load_work_area(reader, viewer->get_work_area()); + load_work_area(reader, viewer->workarea_handle()); } else { reader->skip_current_element(); } @@ -1113,37 +1114,42 @@ void ProjectSerializer220403::load_timeline_points(XmlStreamReader *reader, } void ProjectSerializer220403::load_marker(XmlStreamReader *reader, - TimelineMarker *marker) const + SerializedMarker *marker) const { - Rational in, out; - for (const XmlStreamAttribute &attr : reader->attributes()) { if (attr.name == "name") { - marker->set_name(attr.value); + marker->name = attr.value; } else if (attr.name == "in") { - in = Rational::from_string(attr.value); + marker->in = Rational::from_string(attr.value); } else if (attr.name == "out") { - out = Rational::from_string(attr.value); + marker->out = Rational::from_string(attr.value); } else if (attr.name == "color") { - marker->set_color(atoi(attr.value.c_str())); + marker->color = atoi(attr.value.c_str()); } } - marker->set_time(TimeRange(in, out)); - // This element has no inner text, so just skip it reader->skip_current_element(); } void ProjectSerializer220403::load_work_area(XmlStreamReader *reader, - TimelineWorkArea *workarea) const + const OakTimelineWorkArea &workarea) const { - Rational range_in = workarea->in(); - Rational range_out = workarea->out(); + int cur_in_num, cur_in_den, cur_out_num, cur_out_den; + if (oaktimeline_workarea_get(workarea, &cur_in_num, &cur_in_den, + &cur_out_num, &cur_out_den, + NULL) != OAKTIMELINE_OK) { + reader->skip_current_element(); + return; + } + + Rational range_in(cur_in_num, cur_in_den); + Rational range_out(cur_out_num, cur_out_den); for (const XmlStreamAttribute &attr : reader->attributes()) { if (attr.name == "enabled") { - workarea->set_enabled(attr.value != "0"); + oaktimeline_workarea_set_enabled(workarea, + attr.value != "0"); } else if (attr.name == "in") { range_in = Rational::from_string(attr.value); } else if (attr.name == "out") { @@ -1151,23 +1157,30 @@ void ProjectSerializer220403::load_work_area(XmlStreamReader *reader, } } - TimeRange loaded_workarea(range_in, range_out); - - if (loaded_workarea != workarea->range()) { - workarea->set_range(loaded_workarea); + if (range_in != Rational(cur_in_num, cur_in_den) || + range_out != Rational(cur_out_num, cur_out_den)) { + oaktimeline_workarea_set_range(workarea, range_in.numerator(), + range_in.denominator(), + range_out.numerator(), + range_out.denominator()); } reader->skip_current_element(); } void ProjectSerializer220403::load_marker_list(XmlStreamReader *reader, - TimelineMarkerList *markers) const + const OakTimelineMarkerList &markers) const { while (xml_read_next_start_element(reader)) { if (reader->name() == "marker") { - auto marker = std::make_unique(); - load_marker(reader, marker.get()); - markers->add_marker(std::move(marker)); + SerializedMarker marker; + marker.color = OAK_CONFIG("MarkerColor").toInt(); + load_marker(reader, &marker); + oaktimeline_marker_add(markers, marker.in.numerator(), + marker.in.denominator(), + marker.out.numerator(), + marker.out.denominator(), + marker.name.c_str(), marker.color); } else { reader->skip_current_element(); } diff --git a/src/node/src/project/serializer/serializer220403.h b/src/node/src/project/serializer/serializer220403.h index 8fa92f51a..725716946 100644 --- a/src/node/src/project/serializer/serializer220403.h +++ b/src/node/src/project/serializer/serializer220403.h @@ -101,13 +101,13 @@ private: void load_timeline_points(XmlStreamReader *reader, ViewerOutput *viewer) const; - void load_marker(XmlStreamReader *reader, TimelineMarker *marker) const; + void load_marker(XmlStreamReader *reader, SerializedMarker *marker) const; void load_work_area(XmlStreamReader *reader, - TimelineWorkArea *workarea) const; + const OakTimelineWorkArea &workarea) const; void load_marker_list(XmlStreamReader *reader, - TimelineMarkerList *markers) const; + const OakTimelineMarkerList &markers) const; void load_value_hint(Node::ValueHint *hint, XmlStreamReader *reader) const; }; diff --git a/src/node/src/project/serializer/serializer230220.cpp b/src/node/src/project/serializer/serializer230220.cpp index 4e05ceab9..62af1213e 100644 --- a/src/node/src/project/serializer/serializer230220.cpp +++ b/src/node/src/project/serializer/serializer230220.cpp @@ -68,8 +68,21 @@ ProjectSerializer230220::load(Project *project, XmlStreamReader *reader, if (reader->name() == "markers") { while (xml_read_next_start_element(reader)) { if (reader->name() == "marker") { - TimelineMarker *marker = new TimelineMarker(); - marker->load(reader); + SerializedMarker marker; + marker.color = OAK_CONFIG("MarkerColor").toInt(); + for (const XmlStreamAttribute &attr : + reader->attributes()) { + if (attr.name == "name") { + marker.name = attr.value; + } else if (attr.name == "in") { + marker.in = Rational::from_string(attr.value); + } else if (attr.name == "out") { + marker.out = Rational::from_string(attr.value); + } else if (attr.name == "color") { + marker.color = atoi(attr.value.c_str()); + } + } + reader->skip_current_element(); load_data.markers.push_back(marker); } else { reader->skip_current_element(); @@ -404,9 +417,12 @@ void ProjectSerializer230220::save(XmlStreamWriter *writer, for (auto it = data.get_only_serialize_markers().cbegin(); it != data.get_only_serialize_markers().cend(); it++) { - TimelineMarker *marker = *it; + const SerializedMarker &marker = *it; writer->write_start_element("marker"); - marker->save(writer); + writer->write_attribute("name", marker.name); + writer->write_attribute("in", marker.in.to_string()); + writer->write_attribute("out", marker.out.to_string()); + writer->write_attribute("color", std::to_string(marker.color)); writer->write_end_element(); // marker } diff --git a/src/node/transition/timeline/timelinecommon.h b/src/node/transition/timeline/timelinecommon.h deleted file mode 100644 index dffea4e6f..000000000 --- a/src/node/transition/timeline/timelinecommon.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once -// M4 收尾:桥接到 oaktimeline 真身 -#include "../../../timeline/src/timelinecommon.h" diff --git a/src/node/transition/timeline/timelinemarker.h b/src/node/transition/timeline/timelinemarker.h deleted file mode 100644 index 47cff4105..000000000 --- a/src/node/transition/timeline/timelinemarker.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once -// M4 收尾:桥接到 oaktimeline 真身 -#include "../../../timeline/src/timelinemarker.h" diff --git a/src/node/transition/timeline/timelineundogeneral.h b/src/node/transition/timeline/timelineundogeneral.h deleted file mode 100644 index dc0436416..000000000 --- a/src/node/transition/timeline/timelineundogeneral.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once -// M4 收尾:桥接到 oaktimeline 真身 -#include "../../../timeline/src/timelineundogeneral.h" diff --git a/src/node/transition/timeline/timelineworkarea.h b/src/node/transition/timeline/timelineworkarea.h deleted file mode 100644 index 2f02eb677..000000000 --- a/src/node/transition/timeline/timelineworkarea.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once -// M4 收尾:桥接到 oaktimeline 真身 -#include "../../../timeline/src/timelineworkarea.h" diff --git a/src/timeline/c_api/marker.cpp b/src/timeline/c_api/marker.cpp index a171fef5f..7855789ad 100644 --- a/src/timeline/c_api/marker.cpp +++ b/src/timeline/c_api/marker.cpp @@ -20,6 +20,7 @@ #include "timeline/marker.h" +#include #include #include "../src/timelinemarker.h" @@ -57,19 +58,31 @@ olive::TimelineMarker *marker_at(OakTimelineMarkerList list, int index) } // namespace +OakTimelineMarkerList oaktimeline_marker_list_create(void) +{ + olive::TimelineMarkerList *list = + new (std::nothrow) olive::TimelineMarkerList(); + if (!list) { + return OakTimelineMarkerList{}; + } + return oaktimeline_capi::make_handle( + list, [](void *p) { delete static_cast(p); }); +} + OakTimelineMarkerList oaktimeline_marker_list_of(OakNodeNode owner) { if (!owner.ctx) { return OakTimelineMarkerList{}; } - OakNodeMarkerList *markers = NULL; + // The viewer node keeps the owning handle; oaknode returns an + // addref'd copy (still the same box; the list itself is owned by the + // node's own handle). + OakTimelineMarkerList markers = {}; if (oaknode_node_get_markers(owner, &markers) != OAKNODE_OK) { return OakTimelineMarkerList{}; } - // Borrowed box: the list stays owned by the viewer node. - return oaktimeline_capi::make_handle( - reinterpret_cast(markers)); + return markers; } void oaktimeline_marker_list_free(OakTimelineMarkerList *list) @@ -77,6 +90,22 @@ void oaktimeline_marker_list_free(OakTimelineMarkerList *list) oaktimeline_capi::free_handle(list); } +int oaktimeline_marker_add(OakTimelineMarkerList list, int in_num, + int in_den, int out_num, int out_den, + const char *name, int color) +{ + if (!list.ctx) { + return OAKTIMELINE_E_INVALID; + } + + auto marker = std::make_unique( + color, + olive::core::TimeRange(rat(in_num, in_den), rat(out_num, out_den)), + name ? name : ""); + impl(list)->add_marker(std::move(marker)); + return OAKTIMELINE_OK; +} + int oaktimeline_marker_count(OakTimelineMarkerList list, int *out_count) { if (!list.ctx || !out_count) { diff --git a/src/timeline/c_api/timelinehandle.h b/src/timeline/c_api/timelinehandle.h index 9bd927553..29ea2491d 100644 --- a/src/timeline/c_api/timelinehandle.h +++ b/src/timeline/c_api/timelinehandle.h @@ -30,18 +30,21 @@ * @brief Internal control block behind the oaktimeline value handles, * shared between the c_api translation units. * - * Both public handle types (OakTimelineMarkerList, OakTimelineWorkArea) - * are borrowed references into viewer nodes: the box never owns the - * underlying object, so release() only destroys the box. A single - * generic box and addref/release pair serves both families (identical + * Handles are either borrowed references into viewer nodes (destroy == + * nullptr: release() only frees the box) or owning handles created by + * oaktimeline_marker_list_create()/oaktimeline_workarea_create() + * (destroy deletes the native object). A single generic box and + * addref/release pair serves both families (identical * ctx/addref/release/abi_version layout). */ struct OakTimelineBox { void *object; + void (*destroy)(void *object); /**< NULL for borrowed boxes. */ std::atomic refs; - explicit OakTimelineBox(void *o) + explicit OakTimelineBox(void *o, void (*d)(void *) = nullptr) : object(o) + , destroy(d) , refs(1) { } @@ -64,25 +67,32 @@ inline void handle_release(void *ctx) } OakTimelineBox *box = static_cast(ctx); if (box->refs.fetch_sub(1) == 1) { + if (box->destroy) { + box->destroy(box->object); + } delete box; } } /** - * @brief Wrap a borrowed native object in a value handle with reference - * count 1. Returns an empty handle (ctx == nullptr) for a null - * object or on allocation failure. + * @brief Wrap a native object in a value handle with reference count 1. + * Returns an empty handle (ctx == nullptr) for a null object or on + * allocation failure. Pass a non-null deleter for an owning handle + * (release() destroys the object with the box). */ template -inline Handle make_handle(void *object) +inline Handle make_handle(void *object, void (*destroy)(void *) = nullptr) { Handle handle = {}; if (!object) { return handle; } - OakTimelineBox *box = new (std::nothrow) OakTimelineBox(object); + OakTimelineBox *box = new (std::nothrow) OakTimelineBox(object, destroy); if (!box) { + if (destroy) { + destroy(object); + } return handle; } diff --git a/src/timeline/c_api/workarea.cpp b/src/timeline/c_api/workarea.cpp index 4a1e87752..619bcd24d 100644 --- a/src/timeline/c_api/workarea.cpp +++ b/src/timeline/c_api/workarea.cpp @@ -50,19 +50,32 @@ olive::core::Rational rat(int64_t n, int64_t d) } // namespace +OakTimelineWorkArea oaktimeline_workarea_create(void) +{ + olive::TimelineWorkArea *workarea = + new (std::nothrow) olive::TimelineWorkArea(); + if (!workarea) { + return OakTimelineWorkArea{}; + } + return oaktimeline_capi::make_handle( + workarea, + [](void *p) { delete static_cast(p); }); +} + OakTimelineWorkArea oaktimeline_workarea_of(OakNodeNode owner) { if (!owner.ctx) { return OakTimelineWorkArea{}; } - OakNodeWorkArea *workarea = NULL; + // The viewer node keeps the owning handle; oaknode returns an + // addref'd copy (still the same box; the work area itself is owned + // by the node's own handle). + OakTimelineWorkArea workarea = {}; if (oaknode_node_get_work_area(owner, &workarea) != OAKNODE_OK) { return OakTimelineWorkArea{}; } - // Borrowed box: the work area stays owned by the viewer node. - return oaktimeline_capi::make_handle( - reinterpret_cast(workarea)); + return workarea; } void oaktimeline_workarea_free(OakTimelineWorkArea *w) @@ -70,6 +83,15 @@ void oaktimeline_workarea_free(OakTimelineWorkArea *w) oaktimeline_capi::free_handle(w); } +int oaktimeline_workarea_set_enabled(OakTimelineWorkArea w, int enabled) +{ + if (!w.ctx) { + return OAKTIMELINE_E_INVALID; + } + impl(w)->set_enabled(enabled != 0); + return OAKTIMELINE_OK; +} + int oaktimeline_workarea_get(OakTimelineWorkArea w, int *in_num, int *in_den, int *out_num, int *out_den, int *enabled) diff --git a/src/timeline/tests/timeline_test.cpp b/src/timeline/tests/timeline_test.cpp index 2f656e304..01552d6d4 100644 --- a/src/timeline/tests/timeline_test.cpp +++ b/src/timeline/tests/timeline_test.cpp @@ -617,3 +617,57 @@ TEST_F(TimelineSequenceFixture, SlideAndInsertGapsAndRippleDeleteGapsFactories) } } // namespace + +TEST(OakTimelineOwningHandles, MarkerListCreateAddFree) +{ + OakTimelineMarkerList list = oaktimeline_marker_list_create(); + ASSERT_NE(list.ctx, nullptr); + + EXPECT_EQ(oaktimeline_marker_add(list, 1, 2, 3, 4, "owned", 5), + OAKTIMELINE_OK); + EXPECT_EQ(oaktimeline_marker_add(OakTimelineMarkerList{}, 0, 1, 1, 1, + "x", 0), + OAKTIMELINE_E_INVALID); + + int count = -1; + EXPECT_EQ(oaktimeline_marker_count(list, &count), OAKTIMELINE_OK); + EXPECT_EQ(count, 1); + + int in_n = 0, in_d = 0, out_n = 0, out_d = 0, color = -1; + char name[32]; + EXPECT_GT(oaktimeline_marker_at(list, 0, &in_n, &in_d, &out_n, &out_d, + &color, name, sizeof(name)), + 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, "owned"); + + oaktimeline_marker_list_free(&list); + EXPECT_EQ(list.ctx, nullptr); + oaktimeline_marker_list_free(nullptr); +} + +TEST(OakTimelineOwningHandles, WorkareaCreateSetEnabledFree) +{ + OakTimelineWorkArea w = oaktimeline_workarea_create(); + ASSERT_NE(w.ctx, nullptr); + + int enabled = -1; + EXPECT_EQ(oaktimeline_workarea_get(w, NULL, NULL, NULL, NULL, &enabled), + OAKTIMELINE_OK); + EXPECT_EQ(enabled, 0); + + EXPECT_EQ(oaktimeline_workarea_set_enabled(w, 1), OAKTIMELINE_OK); + EXPECT_EQ(oaktimeline_workarea_get(w, NULL, NULL, NULL, NULL, &enabled), + OAKTIMELINE_OK); + EXPECT_EQ(enabled, 1); + + EXPECT_EQ(oaktimeline_workarea_set_enabled(OakTimelineWorkArea{}, 1), + OAKTIMELINE_E_INVALID); + + oaktimeline_workarea_free(&w); + EXPECT_EQ(w.ctx, nullptr); +}