feat(storage): real oakstorage file backends + full-timeline .ove serializer
oakstorage (new workspace member): URI dispatch, pluggable backends (ove-xml built in, otio/fcpxml via oakotio, C-vtable foreign registration), the M10 C API surface, version info codes, last-error and alive accounting; round-trip tests per backend. oaknode serializer: persists the full timeline — sequence track lists, track block lists, block ranges/media_in/speed/flags, clip footage references, footage filename+streams, folder children — through <custom> behavior hooks with two-phase reference resolution; loads the C++ <olive><project><layout> containers (golden: tests/ project_with_footage.ove); round-trip is field-by-field and byte-idempotent.
This commit is contained in:
@@ -162,6 +162,77 @@ pub mod transition_input {
|
||||
pub const IN_BLOCK: &str = "in_block_in";
|
||||
}
|
||||
|
||||
/// Save the shared [`BlockCore`] custom fields (C++ persists the
|
||||
/// timeline span through the `length_in` input and the track's block
|
||||
/// order; the Rust model owns the range directly, so the custom
|
||||
/// segment carries it — new elements old readers skip).
|
||||
fn save_block_core(writer: &mut dyn crate::serializer::XmlWrite, core: &BlockCore) {
|
||||
writer.start_element("range");
|
||||
writer.attribute("in", &core.in_().to_display_string());
|
||||
writer.attribute("out", &core.out().to_display_string());
|
||||
writer.end_element(); // range
|
||||
writer.text_element("media_in", &core.media_in.to_display_string());
|
||||
writer.text_element("speed", &format!("{}", core.speed));
|
||||
writer.text_element("reversed", if core.reversed { "1" } else { "0" });
|
||||
writer.text_element("enabled", if core.enabled { "1" } else { "0" });
|
||||
writer.text_element(
|
||||
"maintain_audio_pitch",
|
||||
if core.maintain_audio_pitch { "1" } else { "0" },
|
||||
);
|
||||
writer.text_element("loop_mode", &core.loop_mode.to_string());
|
||||
if let Some(t) = core.track {
|
||||
writer.text_element("track", &t.identity().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse one block custom element. Elements owned by the block core are
|
||||
/// applied to `core`; everything else is handed to `extra` so subclass
|
||||
/// state (clip footage, transition offsets) can hook in.
|
||||
fn load_block_core(
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
core: &mut BlockCore,
|
||||
extra: &mut dyn FnMut(&str, &mut dyn crate::serializer::XmlRead) -> bool,
|
||||
) {
|
||||
while reader.next_start_element() {
|
||||
let name = reader.name().to_string();
|
||||
match name.as_str() {
|
||||
"range" => {
|
||||
let in_ = reader
|
||||
.attribute("in")
|
||||
.map(|t| Rational::from_string(&t))
|
||||
.unwrap_or_else(|| core.in_());
|
||||
let out = reader
|
||||
.attribute("out")
|
||||
.map(|t| Rational::from_string(&t))
|
||||
.unwrap_or_else(|| core.out());
|
||||
core.range = TimeRange::new(in_, out);
|
||||
// Consume the element (self-closing `<range/>` emits an
|
||||
// EndElement token that the element loop must not treat
|
||||
// as its own terminator).
|
||||
let _ = reader.read_element_text();
|
||||
}
|
||||
"media_in" => core.media_in = Rational::from_string(&reader.read_element_text()),
|
||||
"speed" => {
|
||||
core.speed = reader.read_element_text().trim().parse().unwrap_or(core.speed)
|
||||
}
|
||||
"reversed" => core.reversed = reader.read_element_text().trim() == "1",
|
||||
"enabled" => core.enabled = reader.read_element_text().trim() != "0",
|
||||
"maintain_audio_pitch" => {
|
||||
core.maintain_audio_pitch = reader.read_element_text().trim() == "1"
|
||||
}
|
||||
"loop_mode" => {
|
||||
core.loop_mode = reader.read_element_text().trim().parse().unwrap_or(core.loop_mode)
|
||||
}
|
||||
"track" => core.track = crate::serializer::parse_node_ref(&reader.read_element_text()),
|
||||
_ => {
|
||||
if !extra(&name, reader) {
|
||||
reader.skip_current_element();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipBlockBehavior {
|
||||
/// New clip with a default length of one second.
|
||||
pub fn new() -> Self {
|
||||
@@ -230,6 +301,35 @@ impl NodeBehavior for ClipBlockBehavior {
|
||||
footage: self.footage,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Custom project save: the shared block span/state plus the
|
||||
/// footage reference (C++ persists the span through inputs; the Rust
|
||||
/// block owns it in [`BlockCore`]).
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = core;
|
||||
save_block_core(writer, &self.core);
|
||||
if let Some(f) = self.footage {
|
||||
writer.text_element("footage", &f.identity().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom project load; the footage/track references resolve in the
|
||||
/// serializer's post-load pass.
|
||||
fn load_custom(
|
||||
&mut self,
|
||||
_core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
let footage = &mut self.footage;
|
||||
load_block_core(reader, &mut self.core, &mut |name, reader| match name {
|
||||
"footage" => {
|
||||
*footage = crate::serializer::parse_node_ref(&reader.read_element_text());
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for GapBlockBehavior {
|
||||
@@ -258,6 +358,23 @@ impl NodeBehavior for GapBlockBehavior {
|
||||
core: self.core.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Custom project save: the shared block span/state only.
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = core;
|
||||
save_block_core(writer, &self.core);
|
||||
}
|
||||
|
||||
/// Custom project load; the track reference resolves in the
|
||||
/// serializer's post-load pass.
|
||||
fn load_custom(
|
||||
&mut self,
|
||||
_core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
load_block_core(reader, &mut self.core, &mut |_, _| false);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for TransitionBlockBehavior {
|
||||
@@ -288,6 +405,36 @@ impl NodeBehavior for TransitionBlockBehavior {
|
||||
out_offset: self.out_offset,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Custom project save: the shared block span/state plus the
|
||||
/// transition offsets.
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = core;
|
||||
save_block_core(writer, &self.core);
|
||||
writer.text_element("in_offset", &self.in_offset.to_display_string());
|
||||
writer.text_element("out_offset", &self.out_offset.to_display_string());
|
||||
}
|
||||
|
||||
/// Custom project load; the track reference resolves in the
|
||||
/// serializer's post-load pass.
|
||||
fn load_custom(
|
||||
&mut self,
|
||||
_core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
load_block_core(reader, &mut self.core, &mut |name, reader| match name {
|
||||
"in_offset" => {
|
||||
self.in_offset = Rational::from_string(&reader.read_element_text());
|
||||
true
|
||||
}
|
||||
"out_offset" => {
|
||||
self.out_offset = Rational::from_string(&reader.read_element_text());
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor for a clip block (C++ `ClipBlock::ClipBlock()`): adds the
|
||||
|
||||
@@ -9667,7 +9667,14 @@ pub mod serializer {
|
||||
.filter(|(_, to, _, _)| *to == id)
|
||||
.map(|(from, _, input, element)| (from, input, element))
|
||||
.collect();
|
||||
crate::serializer::save_node(&mut writer, &entry.core, id, &type_id, &connections)?;
|
||||
crate::serializer::save_node(
|
||||
&mut writer,
|
||||
&entry.core,
|
||||
&*entry.behavior,
|
||||
id,
|
||||
&type_id,
|
||||
&connections,
|
||||
)?;
|
||||
// Serialize per-node properties as text elements.
|
||||
let mut props: Vec<(&String, &String)> = sd
|
||||
.properties
|
||||
|
||||
@@ -111,12 +111,66 @@ impl NodeBehavior for FolderBehavior {
|
||||
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
Some(Box::new(FolderBehavior::new(&self.name)))
|
||||
}
|
||||
|
||||
/// Custom project save: the bin children (C++ attaches children
|
||||
/// through the `child_in` input connections; the Rust model keeps
|
||||
/// them in [`FolderBehavior::children`], so the custom segment is
|
||||
/// the persistence channel — old readers skip it).
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = core;
|
||||
if !self.children.is_empty() {
|
||||
writer.start_element("children");
|
||||
for c in &self.children {
|
||||
writer.text_element("child", &c.identity().to_string());
|
||||
}
|
||||
writer.end_element(); // children
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom project load; the child references resolve in the
|
||||
/// serializer's post-load pass (which also folds in `child_in`
|
||||
/// connections from C++ files).
|
||||
fn load_custom(
|
||||
&mut self,
|
||||
_core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
while reader.next_start_element() {
|
||||
match reader.name() {
|
||||
"children" => {
|
||||
self.children.clear();
|
||||
while reader.next_start_element() {
|
||||
if reader.name() == "child" {
|
||||
if let Some(id) =
|
||||
crate::serializer::parse_node_ref(&reader.read_element_text())
|
||||
{
|
||||
self.children.push(id);
|
||||
}
|
||||
} else {
|
||||
reader.skip_current_element();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => reader.skip_current_element(),
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `Folder::Folder()`): a folder node has no inputs.
|
||||
/// Constructor (C++ `Folder::Folder()`): a folder node declares the
|
||||
/// `child_in` array input (C++ attaches bin children through it) but no
|
||||
/// `enabled_in` (`// CPP-PARITY: folder.cpp:26`).
|
||||
pub fn create(name: &str) -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
// Folders carry no `enabled_in` in C++; keep the bare core.
|
||||
(NodeCore::empty(), Box::new(FolderBehavior::new(name)))
|
||||
let mut core = NodeCore::empty();
|
||||
let mut child = crate::input::Input::new(
|
||||
"child_in",
|
||||
crate::value::ValueType::None,
|
||||
crate::value::NodeValue::None,
|
||||
);
|
||||
child.flags |= crate::input::flags::ARRAY | crate::input::flags::NOT_KEYFRAMABLE;
|
||||
core.add_input(child);
|
||||
(core, Box::new(FolderBehavior::new(name)))
|
||||
}
|
||||
|
||||
/// Register a folder-typed node (used by the serializer for bin folders;
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::input::Input;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
use crate::value::{AudioParams, VideoParams};
|
||||
use crate::value::{AudioParams, NodeValue, ValueType, VideoParams};
|
||||
|
||||
/// One media stream inside a footage file.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -208,6 +209,33 @@ impl FootageBehavior {
|
||||
self.proxy_preset_version = 0;
|
||||
self.proxy_enabled = false;
|
||||
}
|
||||
|
||||
/// Constructor for the serializer: the C++ `Footage` input surface
|
||||
/// (`file_in` + the viewer parameter stream arrays) with an unprobed
|
||||
/// behavior (`// CPP-PARITY: footage.cpp:83`, `viewer.cpp:84`).
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
let mut core = NodeCore::new();
|
||||
let mut file = Input::new(
|
||||
"file_in",
|
||||
ValueType::Text,
|
||||
NodeValue::Text(String::new()),
|
||||
);
|
||||
file.flags |= crate::input::flags::NOT_CONNECTABLE | crate::input::flags::NOT_KEYFRAMABLE;
|
||||
core.add_input(file);
|
||||
for (id, ty) in [
|
||||
("video_param_in", ValueType::VideoParams),
|
||||
("audio_param_in", ValueType::AudioParams),
|
||||
("subtitle_param_in", ValueType::None),
|
||||
] {
|
||||
let mut input = Input::new(id, ty, NodeValue::None);
|
||||
input.flags |= crate::input::flags::NOT_CONNECTABLE
|
||||
| crate::input::flags::NOT_KEYFRAMABLE
|
||||
| crate::input::flags::ARRAY
|
||||
| crate::input::flags::HIDDEN;
|
||||
core.add_input(input);
|
||||
}
|
||||
(core, Box::new(FootageBehavior::new("")))
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for FootageBehavior {
|
||||
@@ -239,6 +267,175 @@ impl NodeBehavior for FootageBehavior {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Custom project save (C++ `Footage::SaveCustom`): the file name,
|
||||
/// media timestamp, proxy state and the probed streams. `<filename>`
|
||||
/// and `<streams>` are Rust additions (C++ reads the name from the
|
||||
/// `file_in` input); old readers skip them.
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = core;
|
||||
if !self.filename.is_empty() {
|
||||
writer.text_element("filename", &self.filename);
|
||||
}
|
||||
if self.timestamp != 0 {
|
||||
writer.text_element("timestamp", &self.timestamp.to_string());
|
||||
}
|
||||
if !self.proxy.is_empty() || self.proxy_enabled {
|
||||
writer.start_element("proxy");
|
||||
writer.attribute("enabled", if self.proxy_enabled { "1" } else { "0" });
|
||||
writer.attribute("state", &self.proxy_state.to_string());
|
||||
writer.attribute("stream", &self.proxy_video_stream_index.to_string());
|
||||
writer.attribute("preset", &self.proxy_preset_version.to_string());
|
||||
writer.characters(&self.proxy);
|
||||
writer.end_element(); // proxy
|
||||
}
|
||||
if !self.streams.is_empty() {
|
||||
writer.start_element("streams");
|
||||
for s in &self.streams {
|
||||
writer.start_element("stream");
|
||||
writer.attribute("index", &s.index.to_string());
|
||||
writer.attribute("video", if s.is_video { "1" } else { "0" });
|
||||
writer.attribute("duration", &s.duration.to_display_string());
|
||||
if let Some(v) = s.video {
|
||||
writer.start_element("video");
|
||||
writer.attribute("width", &v.width.to_string());
|
||||
writer.attribute("height", &v.height.to_string());
|
||||
writer.attribute("framerate", &v.frame_rate.to_display_string());
|
||||
writer.attribute("pixelformat", &v.pixel_format.to_string());
|
||||
writer.attribute("channels", &v.channels.to_string());
|
||||
writer.end_element(); // video
|
||||
}
|
||||
if let Some(a) = s.audio {
|
||||
writer.start_element("audio");
|
||||
writer.attribute("samplerate", &a.sample_rate.to_string());
|
||||
writer.attribute("channellayout", &a.channel_layout.to_string());
|
||||
writer.attribute("format", &a.format.to_string());
|
||||
writer.end_element(); // audio
|
||||
}
|
||||
writer.end_element(); // stream
|
||||
}
|
||||
writer.end_element(); // streams
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom project load. C++ segments without a Rust counterpart
|
||||
/// (`sourcestarttime`, `viewer` workarea/markers) are skipped.
|
||||
fn load_custom(
|
||||
&mut self,
|
||||
_core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
while reader.next_start_element() {
|
||||
match reader.name() {
|
||||
"filename" => self.filename = reader.read_element_text(),
|
||||
"timestamp" => {
|
||||
self.timestamp = reader.read_element_text().trim().parse().unwrap_or(0)
|
||||
}
|
||||
"proxy" => {
|
||||
self.proxy_enabled = reader
|
||||
.attribute("enabled")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
self.proxy_state = reader
|
||||
.attribute("state")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
self.proxy_video_stream_index = reader
|
||||
.attribute("stream")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(-1);
|
||||
self.proxy_preset_version = reader
|
||||
.attribute("preset")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
self.proxy = reader.read_element_text();
|
||||
}
|
||||
"streams" => {
|
||||
self.streams.clear();
|
||||
while reader.next_start_element() {
|
||||
if reader.name() == "stream" {
|
||||
let index = reader
|
||||
.attribute("index")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
let is_video = reader
|
||||
.attribute("video")
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
let duration = reader
|
||||
.attribute("duration")
|
||||
.map(|v| oakcore_rs::Rational::from_string(&v))
|
||||
.unwrap_or_else(|| oakcore_rs::Rational::new(0, 1));
|
||||
let mut video = None;
|
||||
let mut audio = None;
|
||||
while reader.next_start_element() {
|
||||
match reader.name() {
|
||||
"video" => {
|
||||
video = Some(VideoParams {
|
||||
width: reader
|
||||
.attribute("width")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
height: reader
|
||||
.attribute("height")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
frame_rate: reader
|
||||
.attribute("framerate")
|
||||
.map(|v| oakcore_rs::Rational::from_string(&v))
|
||||
.unwrap_or_else(|| {
|
||||
oakcore_rs::Rational::new(0, 1)
|
||||
}),
|
||||
pixel_format: reader
|
||||
.attribute("pixelformat")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
channels: reader
|
||||
.attribute("channels")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
});
|
||||
// Consume the (self-closing) element.
|
||||
let _ = reader.read_element_text();
|
||||
}
|
||||
"audio" => {
|
||||
audio = Some(AudioParams {
|
||||
sample_rate: reader
|
||||
.attribute("samplerate")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
channel_layout: reader
|
||||
.attribute("channellayout")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
format: reader
|
||||
.attribute("format")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
});
|
||||
// Consume the (self-closing) element.
|
||||
let _ = reader.read_element_text();
|
||||
}
|
||||
_ => reader.skip_current_element(),
|
||||
}
|
||||
}
|
||||
self.streams.push(StreamInfo {
|
||||
index,
|
||||
is_video,
|
||||
video,
|
||||
audio,
|
||||
duration,
|
||||
});
|
||||
} else {
|
||||
reader.skip_current_element();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => reader.skip_current_element(),
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn as_any(&self) -> Option<&dyn std::any::Any> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
@@ -677,7 +677,11 @@ pub trait NodeBehavior: Send {
|
||||
core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
let _ = (core, reader);
|
||||
let _ = core;
|
||||
// The default has no custom state; consume the segment so the
|
||||
// node-body parser continues at the correct depth (the reader is
|
||||
// positioned on the `<custom>` start element).
|
||||
reader.skip_current_element();
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
//! `// CPP-PARITY: src/node/src/project/sequence/sequence.{h,cpp}`.
|
||||
|
||||
use crate::id::NodeId;
|
||||
use crate::input::Input;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
use crate::value::{AudioParams, VideoParams};
|
||||
use crate::value::{AudioParams, NodeValue, ValueType, VideoParams};
|
||||
|
||||
/// Sequence texture/samples input ids (ViewerOutput::k_texture_input /
|
||||
/// k_samples_input) and the track input id format (Sequence::
|
||||
@@ -68,6 +69,53 @@ impl SequenceBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor: the C++ `Sequence` input surface with default
|
||||
/// parameters but no track lists (`// CPP-PARITY: sequence.cpp:36`,
|
||||
/// `viewer.cpp:84`). Used by the serializer to rebuild a sequence
|
||||
/// from a file — the track lists arrive as separate nodes.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
let mut core = NodeCore::new();
|
||||
// Viewer parameter streams (C++ ViewerOutput::kVideoParamsInput /
|
||||
// kAudioParamsInput / kSubtitleParamsInput arrays).
|
||||
for (id, ty) in [
|
||||
("video_param_in", ValueType::VideoParams),
|
||||
("audio_param_in", ValueType::AudioParams),
|
||||
("subtitle_param_in", ValueType::None),
|
||||
] {
|
||||
let mut input = Input::new(id, ty, NodeValue::None);
|
||||
input.flags |= crate::input::flags::NOT_CONNECTABLE
|
||||
| crate::input::flags::NOT_KEYFRAMABLE
|
||||
| crate::input::flags::ARRAY
|
||||
| crate::input::flags::HIDDEN;
|
||||
core.add_input(input);
|
||||
}
|
||||
core.add_input(Input::new(
|
||||
TEXTURE_INPUT,
|
||||
ValueType::Texture,
|
||||
NodeValue::None,
|
||||
));
|
||||
core.add_input(Input::new(
|
||||
SAMPLES_INPUT,
|
||||
ValueType::Samples,
|
||||
NodeValue::None,
|
||||
));
|
||||
// One array input per track list (`track_in_%1`; the video list
|
||||
// owns track_in_0, audio track_in_1, subtitle track_in_2 —
|
||||
// `// CPP-PARITY: sequence.h`).
|
||||
for base in 0..3 {
|
||||
let mut track_input = Input::new(
|
||||
&TRACK_INPUT_FORMAT.replace("%1", &base.to_string()),
|
||||
ValueType::None,
|
||||
NodeValue::None,
|
||||
);
|
||||
track_input.flags |= crate::input::flags::ARRAY;
|
||||
core.add_input(track_input);
|
||||
}
|
||||
let mut behavior = SequenceBehavior::new();
|
||||
behavior.set_default_parameters();
|
||||
(core, Box::new(behavior))
|
||||
}
|
||||
|
||||
/// Apply the default video/audio parameters (C++
|
||||
/// `ViewerOutput::set_default_parameters()`; the config lookups use
|
||||
/// oakcommon's defaults when the config module is absent).
|
||||
@@ -141,6 +189,51 @@ impl NodeBehavior for SequenceBehavior {
|
||||
Some(Box::new(SequenceBehavior::new()))
|
||||
}
|
||||
|
||||
/// Custom project save (C++ `Sequence::SaveCustom` writes the
|
||||
/// workarea and markers; those live behind opaque oaktimeline
|
||||
/// handles in Rust, so the track-list references are all that
|
||||
/// persists).
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = core;
|
||||
if !self.track_lists.is_empty() {
|
||||
writer.start_element("tracklists");
|
||||
for t in &self.track_lists {
|
||||
writer.text_element("tracklist", &t.identity().to_string());
|
||||
}
|
||||
writer.end_element(); // tracklists
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom project load (C++ `Sequence::LoadCustom`): the track-list
|
||||
/// references are collected here and resolved to live ids by the
|
||||
/// serializer's post-load pass; the C++ workarea/markers segments
|
||||
/// are skipped (opaque handles).
|
||||
fn load_custom(
|
||||
&mut self,
|
||||
_core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
while reader.next_start_element() {
|
||||
match reader.name() {
|
||||
"tracklists" => {
|
||||
while reader.next_start_element() {
|
||||
if reader.name() == "tracklist" {
|
||||
if let Some(id) =
|
||||
crate::serializer::parse_node_ref(&reader.read_element_text())
|
||||
{
|
||||
self.track_lists.push(id);
|
||||
}
|
||||
} else {
|
||||
reader.skip_current_element();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => reader.skip_current_element(),
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn as_any(&self) -> Option<&dyn std::any::Any> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
@@ -278,6 +278,15 @@ pub fn interpolation_from_c(t: i32) -> Interpolation {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a serialized node identity (a `<node>` `ptr` value or a
|
||||
/// timeline reference) into a packed [`NodeId`]. The id is not resolved
|
||||
/// to a live graph slot until the serializer's post-load pass maps it
|
||||
/// through the id table (the packing only survives Rust-written files;
|
||||
/// C++ `ptr` values are arbitrary addresses).
|
||||
pub fn parse_node_ref(text: &str) -> Option<NodeId> {
|
||||
text.trim().parse::<u64>().ok().and_then(NodeId::from_identity)
|
||||
}
|
||||
|
||||
/// Save a whole project to the current-version XML format.
|
||||
pub fn save(project: &Project) -> crate::error::Result<String> {
|
||||
use crate::error::Error;
|
||||
@@ -303,7 +312,7 @@ pub fn save(project: &Project) -> crate::error::Result<String> {
|
||||
.filter(|(_, to, _, _)| *to == id)
|
||||
.map(|(from, _, input, element)| (from, input, element))
|
||||
.collect();
|
||||
save_node(&mut writer, &entry.core, id, &type_id, &connections)?;
|
||||
save_node(&mut writer, &entry.core, &*entry.behavior, id, &type_id, &connections)?;
|
||||
writer.end_element(); // node
|
||||
}
|
||||
writer.end_element(); // nodes
|
||||
@@ -321,10 +330,12 @@ pub fn save(project: &Project) -> crate::error::Result<String> {
|
||||
}
|
||||
|
||||
/// Save one node (C++ `Node::save`). `connections` lists the node's
|
||||
/// input connections `(source, input_id, element)`.
|
||||
/// input connections `(source, input_id, element)`. The per-type custom
|
||||
/// segment is written through [`NodeBehavior::save_custom`].
|
||||
pub fn save_node(
|
||||
writer: &mut dyn XmlWrite,
|
||||
core: &NodeCore,
|
||||
behavior: &dyn crate::node::NodeBehavior,
|
||||
id: NodeId,
|
||||
type_id: &str,
|
||||
connections: &[(NodeId, String, i32)],
|
||||
@@ -333,8 +344,25 @@ pub fn save_node(
|
||||
writer.attribute("id", type_id);
|
||||
writer.attribute("ptr", &id.identity().to_string());
|
||||
|
||||
if !core.label.is_empty() {
|
||||
writer.text_element("label", &core.label);
|
||||
// Folders persist their display name through the label (C++
|
||||
// `Folder::Name()` returns the label; the Rust model keeps the name
|
||||
// in the behavior, so fall back to it when the label is empty).
|
||||
let label = if !core.label.is_empty() {
|
||||
Some(core.label.as_str())
|
||||
} else if let Some(f) = behavior
|
||||
.as_any()
|
||||
.and_then(|a| a.downcast_ref::<crate::folder::FolderBehavior>())
|
||||
{
|
||||
if f.name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(f.name.as_str())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(label) = label {
|
||||
writer.text_element("label", label);
|
||||
}
|
||||
if core.override_color != -1 {
|
||||
writer.text_element("color", &core.override_color.to_string());
|
||||
@@ -375,6 +403,7 @@ pub fn save_node(
|
||||
writer.end_element(); // caches
|
||||
|
||||
writer.start_element("custom");
|
||||
behavior.save_custom(core, writer);
|
||||
writer.end_element(); // custom
|
||||
|
||||
Ok(())
|
||||
@@ -507,7 +536,11 @@ pub fn load(xml: &str) -> crate::error::Result<Arc<Mutex<Project>>> {
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
/// Parse the `<project>` body: uuid, nodes, settings.
|
||||
/// Parse the `<project>` body: uuid, nodes, settings. C++ full saves
|
||||
/// wrap the data in a container (`<olive><project><project version="1">
|
||||
/// ...</project><layout>...</layout></project>` — `// CPP-PARITY:
|
||||
/// serializer230220.cpp`); a nested `<project>` element is descended
|
||||
/// into transparently instead of skipped.
|
||||
fn load_project_body(reader: &mut dyn XmlRead, project: &mut Project) -> crate::error::Result<()> {
|
||||
use crate::error::Error;
|
||||
// Identity -> NodeId map for connection resolution.
|
||||
@@ -517,8 +550,14 @@ fn load_project_body(reader: &mut dyn XmlRead, project: &mut Project) -> crate::
|
||||
// Deferred links: (identity_a, identity_b).
|
||||
let mut links: Vec<(u64, u64)> = Vec::new();
|
||||
|
||||
while reader.next_start_element() {
|
||||
loop {
|
||||
if !reader.next_start_element() {
|
||||
break;
|
||||
}
|
||||
match reader.name() {
|
||||
// The C++ full-save container: descend and parse its children
|
||||
// as the project body.
|
||||
"project" => {}
|
||||
"uuid" => {
|
||||
project.uuid = reader.read_element_text();
|
||||
}
|
||||
@@ -579,6 +618,13 @@ fn load_project_body(reader: &mut dyn XmlRead, project: &mut Project) -> crate::
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the timeline structure: the custom segments carry packed
|
||||
// references that only resolve now that every node is live.
|
||||
resolve_timeline_refs(&mut project.graph, &id_map);
|
||||
// Fold C++ `child_in` connections into the folder children and
|
||||
// reattach each child to its bin folder.
|
||||
resolve_folder_children(&mut project.graph, &id_map);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -598,20 +644,20 @@ fn load_node(
|
||||
.and_then(|p| p.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Instantiate the node type; folders and unknown types fall back to
|
||||
// an empty folder-ish core.
|
||||
// Instantiate the node type; timeline structural types (which are
|
||||
// not in the factory menu) are reconstructed directly, unknown
|
||||
// types fall back to an error.
|
||||
let (mut core, behavior): (NodeCore, Box<dyn crate::node::NodeBehavior>) =
|
||||
if type_id == "org.olivevideoeditor.Olive.folder" {
|
||||
crate::folder::create("Folder")
|
||||
} else {
|
||||
match crate::factory::Factory::global().find(&type_id) {
|
||||
match create_timeline_type(&type_id) {
|
||||
Some(x) => x,
|
||||
None => match crate::factory::Factory::global().find(&type_id) {
|
||||
Some(meta) => (meta.create)(),
|
||||
None => {
|
||||
// Unknown type: skip the element body.
|
||||
reader.skip_current_element();
|
||||
return Err(Error::Failed(format!("unknown node type '{}'", type_id)));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// The node enters the graph before its body is parsed so deferred
|
||||
@@ -621,17 +667,45 @@ fn load_node(
|
||||
id_map.insert(ptr, id);
|
||||
}
|
||||
|
||||
// Parse the node body (into the entry's core).
|
||||
// Parse the node body (into the entry's core and behavior).
|
||||
let entry = graph.get_mut(id).ok_or(Error::NotFound)?;
|
||||
load_node_body(reader, &mut entry.core, id, connections, links)?;
|
||||
load_node_body(reader, &mut entry.core, &mut *entry.behavior, id, connections, links)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Reconstruct a timeline structural node type for loading. These types
|
||||
/// are absent from the factory menu (the app creates them through
|
||||
/// dedicated APIs, C++ `factory.cpp` lists only user-creatable nodes);
|
||||
/// the serializer instantiates them directly, following the existing
|
||||
/// folder special case.
|
||||
fn create_timeline_type(
|
||||
type_id: &str,
|
||||
) -> Option<(NodeCore, Box<dyn crate::node::NodeBehavior>)> {
|
||||
match type_id {
|
||||
"org.olivevideoeditor.Olive.folder" => Some(crate::folder::create("Folder")),
|
||||
"org.olivevideoeditor.Olive.footage" => Some(crate::footage::FootageBehavior::create()),
|
||||
"org.olivevideoeditor.Olive.sequence" => {
|
||||
Some(crate::sequence::SequenceBehavior::create())
|
||||
}
|
||||
"org.olivevideoeditor.Olive.tracklist" => {
|
||||
Some(crate::track::TrackListBehavior::create())
|
||||
}
|
||||
"org.olivevideoeditor.Olive.track" => Some(crate::track::TrackBehavior::create()),
|
||||
"org.olivevideoeditor.Olive.clipblock" => Some(crate::block::clip_create()),
|
||||
"org.olivevideoeditor.Olive.gapblock" => Some(crate::block::gap_create()),
|
||||
"org.olivevideoeditor.Olive.transitionblock" => {
|
||||
Some(crate::block::transition_create())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the body of a `<node>` element (label/color/inputs/links/...).
|
||||
fn load_node_body(
|
||||
reader: &mut dyn XmlRead,
|
||||
core: &mut NodeCore,
|
||||
behavior: &mut dyn crate::node::NodeBehavior,
|
||||
node_id: NodeId,
|
||||
connections: &mut Vec<(u64, NodeId, String, i32)>,
|
||||
links: &mut Vec<(u64, u64)>,
|
||||
@@ -679,7 +753,12 @@ fn load_node_body(
|
||||
}
|
||||
}
|
||||
}
|
||||
"caches" | "custom" => reader.skip_current_element(),
|
||||
"caches" => reader.skip_current_element(),
|
||||
"custom" => {
|
||||
// The reader is positioned at `<custom>`; the behavior
|
||||
// parses its own segment (the default no-op skips it).
|
||||
behavior.load_custom(core, reader);
|
||||
}
|
||||
_ => reader.skip_current_element(),
|
||||
}
|
||||
}
|
||||
@@ -814,6 +893,178 @@ fn load_immediate(
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the packed node references the timeline custom segments
|
||||
/// parsed during load: each packed id is mapped through the load-time
|
||||
/// id table to the live graph id (Rust files write packed identities;
|
||||
/// C++ files use arbitrary pointer values). Unresolvable references are
|
||||
/// dropped. Track kinds missing from the custom segment (C++ files)
|
||||
/// are derived from the sequence `track_in_%1` connection.
|
||||
fn resolve_timeline_refs(graph: &mut Graph, id_map: &std::collections::HashMap<u64, NodeId>) {
|
||||
let resolve = |id: &NodeId| id_map.get(&id.identity()).copied();
|
||||
|
||||
for id in graph.node_ids() {
|
||||
let entry = match graph.get_mut(id) {
|
||||
Some(e) => e,
|
||||
None => continue,
|
||||
};
|
||||
let behavior = &mut *entry.behavior;
|
||||
if let Some(s) = behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::sequence::SequenceBehavior>())
|
||||
{
|
||||
s.track_lists = s.track_lists.iter().filter_map(resolve).collect();
|
||||
} else if let Some(tl) = behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::track::TrackListBehavior>())
|
||||
{
|
||||
tl.tracks = tl.tracks.iter().filter_map(resolve).collect();
|
||||
tl.sequence = tl.sequence.and_then(|s| resolve(&s));
|
||||
} else if let Some(t) = behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::track::TrackBehavior>())
|
||||
{
|
||||
t.blocks = t.blocks.iter().filter_map(resolve).collect();
|
||||
t.track_list = t.track_list.and_then(|l| resolve(&l));
|
||||
} else if let Some(c) = behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::block::ClipBlockBehavior>())
|
||||
{
|
||||
c.core.track = c.core.track.and_then(|t| resolve(&t));
|
||||
c.footage = c.footage.and_then(|f| resolve(&f));
|
||||
// Block links mirror the node links (C++ LinkChangeEvent).
|
||||
c.core.links = entry.core.links.clone();
|
||||
} else if let Some(g) = behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::block::GapBlockBehavior>())
|
||||
{
|
||||
g.core.track = g.core.track.and_then(|t| resolve(&t));
|
||||
g.core.links = entry.core.links.clone();
|
||||
} else if let Some(t) = behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::block::TransitionBlockBehavior>())
|
||||
{
|
||||
t.core.track = t.core.track.and_then(|r| resolve(&r));
|
||||
t.core.links = entry.core.links.clone();
|
||||
} else if let Some(f) = behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::footage::FootageBehavior>())
|
||||
{
|
||||
// C++ files carry the file name in the `file_in` input.
|
||||
if f.filename.is_empty() {
|
||||
if let crate::value::NodeValue::Text(s) =
|
||||
&entry.core.standard_value("file_in", -1)
|
||||
{
|
||||
f.filename = s.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track kinds absent from the custom segment (C++ files) are
|
||||
// derived from the sequence `track_in_%1` input the track feeds
|
||||
// (`track_in_0` = video, `track_in_1` = audio, `track_in_2` =
|
||||
// subtitle — `// CPP-PARITY: sequence.h`).
|
||||
let mut kind_fixes: Vec<(NodeId, crate::track::TrackType)> = Vec::new();
|
||||
for id in graph.node_ids() {
|
||||
let is_track = graph
|
||||
.get(id)
|
||||
.map(|e| {
|
||||
e.behavior
|
||||
.as_any()
|
||||
.and_then(|a| a.downcast_ref::<crate::track::TrackBehavior>())
|
||||
.is_some()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !is_track {
|
||||
continue;
|
||||
}
|
||||
for (_target, input_id, _element) in graph.output_connections(id) {
|
||||
if let Some(base) = input_id.strip_prefix("track_in_") {
|
||||
if let Ok(n) = base.parse::<i32>() {
|
||||
if let Some(kind) = crate::track::TrackType::from_c(n) {
|
||||
kind_fixes.push((id, kind));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (id, kind) in kind_fixes {
|
||||
if let Some(entry) = graph.get_mut(id) {
|
||||
if let Some(t) = entry
|
||||
.behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::track::TrackBehavior>())
|
||||
{
|
||||
t.kind = kind;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fold C++ `child_in` connections into the folder children (the Rust
|
||||
/// writer persists them in the folder custom segment) and reattach each
|
||||
/// child to its bin folder (`// CPP-PARITY: folder.cpp:43`
|
||||
/// `InputConnectedEvent` sets the child's folder).
|
||||
fn resolve_folder_children(
|
||||
graph: &mut Graph,
|
||||
id_map: &std::collections::HashMap<u64, NodeId>,
|
||||
) {
|
||||
for id in graph.node_ids() {
|
||||
let is_folder = graph
|
||||
.get(id)
|
||||
.map(|e| e.behavior.type_id() == "org.olivevideoeditor.Olive.folder")
|
||||
.unwrap_or(false);
|
||||
if !is_folder {
|
||||
continue;
|
||||
}
|
||||
// Children attached through the input (C++ files).
|
||||
let size = graph
|
||||
.get(id)
|
||||
.map(|e| e.core.input_array_size("child_in"))
|
||||
.unwrap_or(0);
|
||||
let mut connected = Vec::new();
|
||||
for element in 0..size {
|
||||
if let Some(child) = graph.connected_output(id, "child_in", element as i32) {
|
||||
connected.push(child);
|
||||
}
|
||||
}
|
||||
// Resolve the custom-parsed children, then merge the
|
||||
// input-derived ones.
|
||||
let children = {
|
||||
let entry = graph.get_mut(id).unwrap();
|
||||
let folder = match entry
|
||||
.behavior
|
||||
.as_any_mut()
|
||||
.and_then(|a| a.downcast_mut::<crate::folder::FolderBehavior>())
|
||||
{
|
||||
Some(f) => f,
|
||||
None => continue,
|
||||
};
|
||||
// The persisted display name lives in the node label
|
||||
// (C++ `Folder::Name()` returns the label).
|
||||
if !entry.core.label.is_empty() {
|
||||
folder.name = entry.core.label.clone();
|
||||
}
|
||||
folder.children = folder
|
||||
.children
|
||||
.iter()
|
||||
.filter_map(|c| id_map.get(&c.identity()).copied())
|
||||
.collect();
|
||||
for c in connected {
|
||||
folder.add_child(c);
|
||||
}
|
||||
folder.children.clone()
|
||||
};
|
||||
// Reattach each child to its bin folder.
|
||||
for c in children {
|
||||
if let Some(e) = graph.get_mut(c) {
|
||||
e.core.bin_folder = Some(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock a project mutex (poison-tolerant).
|
||||
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
m.lock().unwrap_or_else(|e| e.into_inner())
|
||||
|
||||
@@ -119,6 +119,14 @@ impl TrackBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor for the serializer: a track core carries only the
|
||||
/// inherited `enabled_in` (C++ `Track` also declares the `block_in`
|
||||
/// array and `arraymap_in`; the Rust model keeps block membership in
|
||||
/// [`TrackBehavior::blocks`]).
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(NodeCore::new(), Box::new(TrackBehavior::new(TrackType::Video)))
|
||||
}
|
||||
|
||||
/// Block at `index` (None out of range).
|
||||
pub fn block_at(&self, index: usize) -> Option<NodeId> {
|
||||
self.blocks.get(index).copied()
|
||||
@@ -274,6 +282,13 @@ impl TrackListBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor for the serializer: a track list core carries only
|
||||
/// the inherited `enabled_in` (the list's state lives in the
|
||||
/// behavior; the C++ `TrackList` is a plain QObject, not a node).
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(NodeCore::new(), Box::new(TrackListBehavior::new(TrackType::Video)))
|
||||
}
|
||||
|
||||
/// Track at `index`.
|
||||
pub fn track_at(&self, index: usize) -> Option<NodeId> {
|
||||
self.tracks.get(index).copied()
|
||||
@@ -327,6 +342,85 @@ impl NodeBehavior for TrackBehavior {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Custom project save (C++ `Track::SaveCustom` writes only the
|
||||
/// height; the type/muted/locked and the block/track-list
|
||||
/// references are Rust additions that older readers skip).
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = core;
|
||||
writer.text_element("type", &self.kind.to_c().to_string());
|
||||
writer.text_element("index", &self.index.to_string());
|
||||
writer.text_element("muted", if self.muted { "1" } else { "0" });
|
||||
writer.text_element("locked", if self.locked { "1" } else { "0" });
|
||||
writer.text_element("height", &format!("{}", self.height));
|
||||
if let Some(tl) = self.track_list {
|
||||
writer.text_element("tracklist", &tl.identity().to_string());
|
||||
}
|
||||
if !self.blocks.is_empty() {
|
||||
writer.start_element("blocks");
|
||||
for b in &self.blocks {
|
||||
writer.text_element("block", &b.identity().to_string());
|
||||
}
|
||||
writer.end_element(); // blocks
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom project load. The block/track-list references are packed
|
||||
/// ids resolved by the serializer's post-load pass; a track saved by
|
||||
/// C++ carries only `<height>`, so the kind is filled from the
|
||||
/// sequence `track_in_%1` connection when the custom has none.
|
||||
fn load_custom(
|
||||
&mut self,
|
||||
_core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
while reader.next_start_element() {
|
||||
match reader.name() {
|
||||
"type" => {
|
||||
if let Some(kind) = reader
|
||||
.read_element_text()
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
.and_then(TrackType::from_c)
|
||||
{
|
||||
self.kind = kind;
|
||||
}
|
||||
}
|
||||
"index" => {
|
||||
self.index = reader.read_element_text().trim().parse().unwrap_or(self.index)
|
||||
}
|
||||
"muted" => self.muted = reader.read_element_text().trim() == "1",
|
||||
"locked" => self.locked = reader.read_element_text().trim() == "1",
|
||||
"height" => {
|
||||
self.height = reader
|
||||
.read_element_text()
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap_or(self.height)
|
||||
}
|
||||
"tracklist" => {
|
||||
self.track_list = crate::serializer::parse_node_ref(&reader.read_element_text())
|
||||
}
|
||||
"blocks" => {
|
||||
self.blocks.clear();
|
||||
while reader.next_start_element() {
|
||||
if reader.name() == "block" {
|
||||
if let Some(id) =
|
||||
crate::serializer::parse_node_ref(&reader.read_element_text())
|
||||
{
|
||||
self.blocks.push(id);
|
||||
}
|
||||
} else {
|
||||
reader.skip_current_element();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => reader.skip_current_element(),
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn as_any(&self) -> Option<&dyn std::any::Any> {
|
||||
Some(self)
|
||||
}
|
||||
@@ -362,6 +456,71 @@ impl NodeBehavior for TrackListBehavior {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Custom project save: the list kind, array base and its node
|
||||
/// references (the C++ `TrackList` has no serialization of its own —
|
||||
/// it is a plain QObject owned by the sequence).
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = core;
|
||||
writer.text_element("type", &self.kind.to_c().to_string());
|
||||
writer.text_element("arraybase", &self.array_base.to_string());
|
||||
if let Some(s) = self.sequence {
|
||||
writer.text_element("sequence", &s.identity().to_string());
|
||||
}
|
||||
if !self.tracks.is_empty() {
|
||||
writer.start_element("tracks");
|
||||
for t in &self.tracks {
|
||||
writer.text_element("track", &t.identity().to_string());
|
||||
}
|
||||
writer.end_element(); // tracks
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom project load; references resolve in the serializer's
|
||||
/// post-load pass.
|
||||
fn load_custom(
|
||||
&mut self,
|
||||
_core: &mut NodeCore,
|
||||
reader: &mut dyn crate::serializer::XmlRead,
|
||||
) -> bool {
|
||||
while reader.next_start_element() {
|
||||
match reader.name() {
|
||||
"type" => {
|
||||
if let Some(kind) = reader
|
||||
.read_element_text()
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
.and_then(TrackType::from_c)
|
||||
{
|
||||
self.kind = kind;
|
||||
}
|
||||
}
|
||||
"arraybase" => {
|
||||
self.array_base = reader.read_element_text().trim().parse().unwrap_or(0)
|
||||
}
|
||||
"sequence" => {
|
||||
self.sequence = crate::serializer::parse_node_ref(&reader.read_element_text())
|
||||
}
|
||||
"tracks" => {
|
||||
self.tracks.clear();
|
||||
while reader.next_start_element() {
|
||||
if reader.name() == "track" {
|
||||
if let Some(id) =
|
||||
crate::serializer::parse_node_ref(&reader.read_element_text())
|
||||
{
|
||||
self.tracks.push(id);
|
||||
}
|
||||
} else {
|
||||
reader.skip_current_element();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => reader.skip_current_element(),
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn as_any(&self) -> Option<&dyn std::any::Any> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user