diff --git a/app/dialog/sequence/sequence.cpp b/app/dialog/sequence/sequence.cpp index 454331cad..f90fa66f7 100644 --- a/app/dialog/sequence/sequence.cpp +++ b/app/dialog/sequence/sequence.cpp @@ -31,10 +31,10 @@ #include #include "config/config.h" -#include "core.h" #include "common/qtutils.h" #include "dialog/msgbox.h" -#include "undo/undostack.h" +#include "oakengine/node.h" +#include "oakengine/timeline.h" namespace olive { @@ -139,40 +139,36 @@ void SequenceDialog::accept() } } - // Generate video and audio parameter structs from data - VideoParams video_params = - VideoParams(parameter_tab_->get_selected_video_width(), - parameter_tab_->get_selected_video_height(), - parameter_tab_->get_selected_video_frame_rate().flipped(), - parameter_tab_->get_selected_preview_format(), - VideoParams::k_internal_channel_count, - parameter_tab_->get_selected_video_pixel_aspect(), - parameter_tab_->get_selected_video_interlacing_mode(), - parameter_tab_->get_selected_preview_resolution()); - - AudioParams audio_params = - AudioParams(parameter_tab_->get_selected_audio_sample_rate(), - parameter_tab_->get_selected_audio_channel_layout(), - Sequence::k_default_sample_format); - - if (make_undoable_) { - // Make undoable command to change the parameters - SequenceParamCommand *param_command = new SequenceParamCommand( - sequence_, video_params, audio_params, name_field_->text(), - parameter_tab_->get_selected_preview_auto_cache()); - - Core::instance()->undo_stack()->push( - param_command, - tr("Set Sequence Parameters For \"%1\"").arg(sequence_->get_label())); - - } else { - // Set sequence values directly with no undo command - sequence_->set_video_params(video_params); - sequence_->set_audio_params(audio_params); - sequence_->set_label(name_field_->text()); - sequence_->set_video_auto_cache_enabled( - parameter_tab_->get_selected_preview_auto_cache()); - } + // All parameter writes go through the liboakengine C ABI facade; in + // undoable mode each call lands on the shared undo stack as one command, + // otherwise it applies directly (the flag mirrors the old + // SequenceParamCommand / direct-set split). + const int undoable = make_undoable_ ? 1 : 0; + OakEngineSequence *facade_handle = + reinterpret_cast(sequence_); + const Rational frame_rate = parameter_tab_->get_selected_video_frame_rate(); + const Rational pixel_aspect = + parameter_tab_->get_selected_video_pixel_aspect(); + oakengine_sequence_set_video_params( + facade_handle, parameter_tab_->get_selected_video_width(), + parameter_tab_->get_selected_video_height(), frame_rate.numerator(), + frame_rate.denominator(), pixel_aspect.numerator(), + pixel_aspect.denominator(), + int(parameter_tab_->get_selected_video_interlacing_mode()), + int(parameter_tab_->get_selected_preview_format()), undoable); + oakengine_sequence_set_preview_divider( + facade_handle, parameter_tab_->get_selected_preview_resolution(), + undoable); + oakengine_sequence_set_audio_params( + facade_handle, parameter_tab_->get_selected_audio_sample_rate(), + parameter_tab_->get_selected_audio_channel_layout(), undoable); + oakengine_node_set_label_ex( + reinterpret_cast(sequence_), + name_field_->text().toUtf8().constData(), undoable); + oakengine_sequence_set_video_auto_cache( + facade_handle, parameter_tab_->get_selected_preview_auto_cache() ? 1 : + 0, + undoable); QDialog::accept(); } @@ -201,48 +197,4 @@ void SequenceDialog::set_as_default_clicked() } } -SequenceDialog::SequenceParamCommand::SequenceParamCommand( - Sequence *s, const VideoParams &video_params, - const AudioParams &audio_params, const QString &name, bool autocache) - : sequence_(s) - , new_video_params_(video_params) - , new_audio_params_(audio_params) - , new_name_(name) - , new_autocache_(autocache) - , old_video_params_(s->get_video_params()) - , old_audio_params_(s->get_audio_params()) - , old_name_(s->get_label()) - , old_autocache_(s->is_video_auto_cache_enabled()) -{ -} - -Project *SequenceDialog::SequenceParamCommand::get_relevant_project() const -{ - return sequence_->project(); -} - -void SequenceDialog::SequenceParamCommand::redo() -{ - if (sequence_->get_video_params() != new_video_params_) { - sequence_->set_video_params(new_video_params_); - } - if (sequence_->get_audio_params() != new_audio_params_) { - sequence_->set_audio_params(new_audio_params_); - } - sequence_->set_label(new_name_); - sequence_->set_video_auto_cache_enabled(new_autocache_); -} - -void SequenceDialog::SequenceParamCommand::undo() -{ - if (sequence_->get_video_params() != old_video_params_) { - sequence_->set_video_params(old_video_params_); - } - if (sequence_->get_audio_params() != old_audio_params_) { - sequence_->set_audio_params(old_audio_params_); - } - sequence_->set_label(old_name_); - sequence_->set_video_auto_cache_enabled(old_autocache_); -} - } diff --git a/app/dialog/sequence/sequence.h b/app/dialog/sequence/sequence.h index 3a344b948..327130c55 100644 --- a/app/dialog/sequence/sequence.h +++ b/app/dialog/sequence/sequence.h @@ -29,7 +29,6 @@ #include "node/project/sequence/sequence.h" #include "sequencedialogparametertab.h" #include "sequencedialogpresettab.h" -#include "undo/undocommand.h" namespace olive { @@ -101,35 +100,6 @@ private: QLineEdit *name_field_; - /** - * @brief An UndoCommand for setting the parameters on a sequence - */ - class SequenceParamCommand : public UndoCommand { - public: - SequenceParamCommand(Sequence *s, const VideoParams &video_params, - const AudioParams &audio_params, - const QString &name, bool autocache); - - virtual Project *get_relevant_project() const override; - - protected: - virtual void redo() override; - virtual void undo() override; - - private: - Sequence *sequence_; - - VideoParams new_video_params_; - AudioParams new_audio_params_; - QString new_name_; - bool new_autocache_; - - VideoParams old_video_params_; - AudioParams old_audio_params_; - QString old_name_; - bool old_autocache_; - }; - private slots: void set_as_default_clicked(); }; diff --git a/app/dialog/sequence/sequencedialogparametertab.cpp b/app/dialog/sequence/sequencedialogparametertab.cpp index a3541e084..4111b0eba 100644 --- a/app/dialog/sequence/sequencedialogparametertab.cpp +++ b/app/dialog/sequence/sequencedialogparametertab.cpp @@ -23,6 +23,8 @@ #include #include +#include "oakengine/timeline.h" + namespace olive { @@ -100,19 +102,35 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence *sequence, layout->addWidget(preview_group); - // Set values based on input sequence - VideoParams vp = sequence->get_video_params(); - AudioParams ap = sequence->get_audio_params(); - width_slider_->set_value(vp.width()); - height_slider_->set_value(vp.height()); - framerate_combo_->set_frame_rate(vp.time_base().flipped()); - pixelaspect_combo_->set_pixel_aspect_ratio(vp.pixel_aspect_ratio()); - interlacing_combo_->set_interlace_mode(vp.interlacing()); - preview_resolution_field_->set_divider(vp.divider()); - preview_format_field_->set_pixel_format(vp.format()); - preview_autocache_field_->setChecked(sequence->is_video_auto_cache_enabled()); - audio_sample_rate_field_->set_sample_rate(ap.sample_rate()); - audio_channels_field_->set_channel_layout(ap.channel_layout()); + // Set values based on input sequence; the reads go through the + // liboakengine C ABI facade (the sequence handle is the engine node + // pointer in this family). + OakEngineSequence *facade_handle = + reinterpret_cast(sequence); + int width = 0, height = 0, fps_num = 0, fps_den = 1, par_num = 1, + par_den = 1, interlacing = 0, format = 0, divider = 1; + oakengine_sequence_get_video_params_ex(facade_handle, &width, &height, + &fps_num, &fps_den, &par_num, + &par_den, &interlacing, &format, + ÷r); + int sample_rate = 0; + uint64_t channel_layout = 0; + oakengine_sequence_get_audio_params(facade_handle, &sample_rate, + &channel_layout); + + width_slider_->set_value(width); + height_slider_->set_value(height); + framerate_combo_->set_frame_rate(Rational(fps_num, fps_den)); + pixelaspect_combo_->set_pixel_aspect_ratio(Rational(par_num, par_den)); + interlacing_combo_->set_interlace_mode( + static_cast(interlacing)); + preview_resolution_field_->set_divider(divider); + preview_format_field_->set_pixel_format( + static_cast(format)); + preview_autocache_field_->setChecked( + oakengine_sequence_get_video_auto_cache(facade_handle) != 0); + audio_sample_rate_field_->set_sample_rate(sample_rate); + audio_channels_field_->set_channel_layout(channel_layout); connect( preview_resolution_field_, diff --git a/engine/include/oakengine/node.h b/engine/include/oakengine/node.h index bf583b41f..261e5dbff 100644 --- a/engine/include/oakengine/node.h +++ b/engine/include/oakengine/node.h @@ -140,6 +140,17 @@ OAKENGINE_API int oakengine_node_get_label(const OakEngineNode *self, OAKENGINE_API int oakengine_node_set_label(OakEngineNode *self, const char *label); +/** + * @brief Set the node's user label with an explicit undoable flag. + * + * `undoable` != 0 behaves like oakengine_node_set_label() (one undoable + * NodeRenameCommand on the global undo stack, degrading to a direct + * rename when the engine is not initialized); 0 renames directly with no + * undo entry. + */ +OAKENGINE_API int oakengine_node_set_label_ex(OakEngineNode *self, + const char *label, int undoable); + /* ---- Input introspection ---------------------------------------------------- */ /** diff --git a/engine/include/oakengine/timeline.h b/engine/include/oakengine/timeline.h index ef853da42..39ba9e1f8 100644 --- a/engine/include/oakengine/timeline.h +++ b/engine/include/oakengine/timeline.h @@ -37,10 +37,10 @@ extern "C" { * @brief C ABI for sequences (Oak timelines) * * An OakEngineSequence wraps the engine's olive::Sequence node - * (engine/node/project/sequence/sequence.h, a ViewerOutput). This family is - * intentionally read-mostly this round: playhead and workarea are simple - * writes, everything else is inspection. Clip/track editing is a later - * milestone. + * (engine/node/project/sequence/sequence.h, a ViewerOutput). Most of the + * family is inspection; playhead, workarea, the sequence parameters + * (video/audio/preview/auto-cache, see below) and clip/track editing are + * writes, undoable unless documented otherwise. * * Handles are borrowed from their owning OakEngineProject (Qt QObject parent * chain; sequences are added to the project graph which becomes their @@ -118,6 +118,100 @@ OAKENGINE_API int oakengine_sequence_get_video_params(const OakEngineSequence *self, int *width, int *height, int *par_num, int *par_den); +/* ---- Sequence parameters (sequence dialog) ------------------------------------ + * + * The parameter set shown by the application's sequence dialog + * (app/dialog/sequence): video size/frame rate/pixel aspect/interlacing, + * preview pixel format and resolution divider, audio sample rate/channel + * layout, and the video auto-cache flag. Setters take an `undoable` flag: + * 1 pushes one undoable command onto the global undo stack (direct + * application when the engine is not initialized), 0 applies directly with + * no undo entry -- mirroring the dialog's two modes (SetUndoable()). + * Fields pass -1 (ints/rationals) or <= 0 / 0 (audio) to leave them + * unchanged; a call that changes nothing pushes no command and returns + * OAKENGINE_OK. The video channel count and the audio sample format are + * internal constants in the engine (VideoParams::k_internal_channel_count, + * ViewerOutput::k_default_sample_format) and are kept as-is. + */ + +/** + * @brief Full read of the sequence's video parameters + * (ViewerOutput::get_video_params()). Any output pointer may be NULL. + * `fps_*` is the frame rate (the time base flipped), `format` a + * PixelFormat::Format value, `divider` the preview resolution divider. + */ +OAKENGINE_API int oakengine_sequence_get_video_params_ex( + const OakEngineSequence *self, int *width, int *height, int *fps_num, + int *fps_den, int *par_num, int *par_den, int *interlacing, int *format, + int *divider); + +/** + * @brief Write the sequence's video parameters (see the section comment + * for the undoable/unchanged conventions). + * + * -1 leaves a field unchanged; both fps and pixel-aspect must be given as + * num/den pairs (both -1 or both > 0). `interlacing` is a + * VideoParams::Interlacing value (0..2), `format` a PixelFormat::Format + * value (0..PixelFormat::count-1). Invalid values yield + * OAKENGINE_E_INVALID. The frame rate is stored as its flipped time base + * exactly like the application's dialog (VideoParams constructor), so the + * derived effective size and frame_rate stay in sync. + */ +OAKENGINE_API int oakengine_sequence_set_video_params( + OakEngineSequence *self, int width, int height, int fps_num, int fps_den, + int par_num, int par_den, int interlacing, int format, int undoable); + +/** + * @brief Sequence audio sample rate and channel layout + * (ViewerOutput::get_audio_params()). Any output pointer may be NULL. + */ +OAKENGINE_API int oakengine_sequence_get_audio_params( + const OakEngineSequence *self, int *sample_rate, + uint64_t *channel_layout); + +/** + * @brief Write the sequence's audio parameters (undoable flag as above). + * `sample_rate` <= 0 or `channel_layout` == 0 leaves the field unchanged. + * The sample format is kept unchanged (the dialog always uses + * ViewerOutput::k_default_sample_format). + */ +OAKENGINE_API int oakengine_sequence_set_audio_params( + OakEngineSequence *self, int sample_rate, uint64_t channel_layout, + int undoable); + +/** + * @brief Preview resolution divider (VideoParams::divider()). 0 on a NULL + * handle. + */ +OAKENGINE_API int +oakengine_sequence_get_preview_divider(const OakEngineSequence *self); + +/** + * @brief Set the preview resolution divider (undoable flag as above). + * `divider` < 1 yields OAKENGINE_E_INVALID. + */ +OAKENGINE_API int oakengine_sequence_set_preview_divider( + OakEngineSequence *self, int divider, int undoable); + +/** + * @brief 1 when video auto-cache is enabled + * (ViewerOutput::is_video_auto_cache_enabled()). 0 on a NULL handle. + */ +OAKENGINE_API int +oakengine_sequence_get_video_auto_cache(const OakEngineSequence *self); + +/** + * @brief Enable or disable video auto-cache. + * + * The engine's auto-cache accessors are currently stubs (the read always + * reports 0, the write is ignored; the application's dialog has the + * checkbox TEMP-disabled for the same reason). This forwards to the stub + * without an undo command so the ABI is ready when the engine + * implementation lands. `undoable` is accepted for symmetry and ignored. + */ +OAKENGINE_API int oakengine_sequence_set_video_auto_cache( + OakEngineSequence *self, int enabled, int undoable); + /** * @brief Number of tracks per track type (Sequence::track_list(type)-> * get_track_count()). Any of `video`/`audio`/`subtitle` may be NULL. diff --git a/engine/src/capi/node.cpp b/engine/src/capi/node.cpp index 37b49e090..a168f5e04 100644 --- a/engine/src/capi/node.cpp +++ b/engine/src/capi/node.cpp @@ -487,15 +487,26 @@ int oakengine_node_get_label(const OakEngineNode *self, char *buf, } int oakengine_node_set_label(OakEngineNode *self, const char *label) +{ + return oakengine_node_set_label_ex(self, label, 1); +} + +int oakengine_node_set_label_ex(OakEngineNode *self, const char *label, + int undoable) { set_error(QString()); if (!self) { set_error(QStringLiteral("invalid node")); return OAKENGINE_E_INVALID; } - push_or_run(new olive::NodeRenameCommand( - impl(self), QString::fromUtf8(label ? label : "")), - QStringLiteral("Rename Node")); + olive::UndoCommand *command = new olive::NodeRenameCommand( + impl(self), QString::fromUtf8(label ? label : "")); + if (undoable) { + push_or_run(command, QStringLiteral("Rename Node")); + } else { + command->redo_now(); + delete command; + } return OAKENGINE_OK; } diff --git a/engine/src/capi/timeline.cpp b/engine/src/capi/timeline.cpp index c062ba191..caa3abca7 100644 --- a/engine/src/capi/timeline.cpp +++ b/engine/src/capi/timeline.cpp @@ -152,6 +152,88 @@ void push_or_run(olive::UndoCommand *command, const QString &name) } } +// Apply a command honoring an explicit undoable flag: 1 pushes through +// push_or_run(), 0 applies directly with no undo entry (the sequence +// dialog's non-undoable mode for freshly created sequences). +void apply_or_push(olive::UndoCommand *command, const QString &name, + int undoable) +{ + if (undoable) { + push_or_run(command, name); + } else { + command->redo_now(); + delete command; + } +} + +// Undo commands for sequence parameter writes. The engine has no undo +// commands for these (the application's sequence dialog carries its own +// SequenceParamCommand at the app layer), so the facade carries +// read-modify-write equivalents with the same semantics. +class SequenceVideoParamsCommand : public olive::UndoCommand { +public: + SequenceVideoParamsCommand(olive::Sequence *sequence, + const olive::VideoParams ¶ms) + : sequence_(sequence) + , new_params_(params) + { + } + + virtual olive::Project *get_relevant_project() const override + { + return sequence_->project(); + } + +protected: + virtual void redo() override + { + old_params_ = sequence_->get_video_params(); + sequence_->set_video_params(new_params_); + } + + virtual void undo() override + { + sequence_->set_video_params(old_params_); + } + +private: + olive::Sequence *sequence_; + olive::VideoParams old_params_; + olive::VideoParams new_params_; +}; + +class SequenceAudioParamsCommand : public olive::UndoCommand { +public: + SequenceAudioParamsCommand(olive::Sequence *sequence, + const olive::AudioParams ¶ms) + : sequence_(sequence) + , new_params_(params) + { + } + + virtual olive::Project *get_relevant_project() const override + { + return sequence_->project(); + } + +protected: + virtual void redo() override + { + old_params_ = sequence_->get_audio_params(); + sequence_->set_audio_params(new_params_); + } + + virtual void undo() override + { + sequence_->set_audio_params(old_params_); + } + +private: + olive::Sequence *sequence_; + olive::AudioParams old_params_; + olive::AudioParams new_params_; +}; + // The clip at (track_index, clip_index) within the given track list, // skipping gap blocks; nullptr when out of range. olive::ClipBlock *clip_at_index(olive::TrackList *list, int track_index, @@ -309,6 +391,220 @@ int oakengine_sequence_get_video_params(const OakEngineSequence *self, return OAKENGINE_OK; } +/* ---- Sequence parameters (sequence dialog) ------------------------------------ */ + +int oakengine_sequence_get_video_params_ex( + const OakEngineSequence *self, int *width, int *height, int *fps_num, + int *fps_den, int *par_num, int *par_den, int *interlacing, int *format, + int *divider) +{ + if (!self) { + return OAKENGINE_E_INVALID; + } + const olive::VideoParams params = impl(self)->get_video_params(); + if (width) { + *width = params.width(); + } + if (height) { + *height = params.height(); + } + const olive::Rational frame_rate = params.frame_rate(); + if (fps_num) { + *fps_num = frame_rate.numerator(); + } + if (fps_den) { + *fps_den = frame_rate.denominator(); + } + const olive::Rational par = params.pixel_aspect_ratio(); + if (par_num) { + *par_num = par.numerator(); + } + if (par_den) { + *par_den = par.denominator(); + } + if (interlacing) { + *interlacing = int(params.interlacing()); + } + if (format) { + *format = int(params.format()); + } + if (divider) { + *divider = params.divider(); + } + return OAKENGINE_OK; +} + +int oakengine_sequence_set_video_params( + OakEngineSequence *self, int width, int height, int fps_num, int fps_den, + int par_num, int par_den, int interlacing, int format, int undoable) +{ + set_seq_error(QString()); + if (!self) { + set_seq_error(QStringLiteral("invalid sequence")); + return OAKENGINE_E_INVALID; + } + // -1 leaves a field unchanged; 0 / out-of-range values are rejected. + if (width < -1 || width == 0 || height < -1 || height == 0) { + set_seq_error(QStringLiteral("invalid video size %1x%2") + .arg(width) + .arg(height)); + return OAKENGINE_E_INVALID; + } + if ((fps_num == -1) != (fps_den == -1) || fps_num < -1 || fps_num == 0 || + fps_den < -1 || fps_den == 0) { + set_seq_error(QStringLiteral("invalid frame rate %1/%2") + .arg(fps_num) + .arg(fps_den)); + return OAKENGINE_E_INVALID; + } + if ((par_num == -1) != (par_den == -1) || par_num < -1 || par_num == 0 || + par_den < -1 || par_den == 0) { + set_seq_error(QStringLiteral("invalid pixel aspect %1/%2") + .arg(par_num) + .arg(par_den)); + return OAKENGINE_E_INVALID; + } + if (interlacing < -1 || + interlacing > int(olive::VideoParams::k_interlaced_bottom_first)) { + set_seq_error( + QStringLiteral("invalid interlacing %1").arg(interlacing)); + return OAKENGINE_E_INVALID; + } + if (format < -1 || format >= int(olive::PixelFormat::count)) { + set_seq_error(QStringLiteral("invalid pixel format %1").arg(format)); + return OAKENGINE_E_INVALID; + } + + olive::Sequence *sequence = impl(self); + const olive::VideoParams current = sequence->get_video_params(); + // Rebuild through the same constructor the application's dialog uses so + // the frame rate (flipped time base) and effective size stay in sync. + const olive::VideoParams updated( + width >= 0 ? width : current.width(), + height >= 0 ? height : current.height(), + fps_num >= 0 ? olive::Rational(fps_den, fps_num) : current.time_base(), + format >= 0 ? olive::PixelFormat::Format(format) : + olive::PixelFormat::Format(current.format()), + current.channel_count(), + par_num >= 0 ? olive::Rational(par_num, par_den) : + current.pixel_aspect_ratio(), + interlacing >= 0 ? olive::VideoParams::Interlacing(interlacing) : + current.interlacing(), + current.divider()); + if (updated == current) { + return OAKENGINE_OK; + } + apply_or_push(new SequenceVideoParamsCommand(sequence, updated), + QStringLiteral("Set Sequence Video Parameters"), undoable); + return OAKENGINE_OK; +} + +int oakengine_sequence_get_audio_params(const OakEngineSequence *self, + int *sample_rate, + uint64_t *channel_layout) +{ + if (!self) { + return OAKENGINE_E_INVALID; + } + const olive::AudioParams params = impl(self)->get_audio_params(); + if (sample_rate) { + *sample_rate = params.sample_rate(); + } + if (channel_layout) { + *channel_layout = params.channel_layout(); + } + return OAKENGINE_OK; +} + +int oakengine_sequence_set_audio_params(OakEngineSequence *self, + int sample_rate, + uint64_t channel_layout, int undoable) +{ + set_seq_error(QString()); + if (!self) { + set_seq_error(QStringLiteral("invalid sequence")); + return OAKENGINE_E_INVALID; + } + olive::Sequence *sequence = impl(self); + const olive::AudioParams current = sequence->get_audio_params(); + if (sample_rate <= 0) { + sample_rate = current.sample_rate(); + } + if (channel_layout == 0) { + channel_layout = current.channel_layout(); + } + const olive::AudioParams updated(sample_rate, channel_layout, + current.format()); + if (updated == current) { + return OAKENGINE_OK; + } + apply_or_push(new SequenceAudioParamsCommand(sequence, updated), + QStringLiteral("Set Sequence Audio Parameters"), undoable); + return OAKENGINE_OK; +} + +int oakengine_sequence_get_preview_divider(const OakEngineSequence *self) +{ + if (!self) { + return 0; + } + return impl(self)->get_video_params().divider(); +} + +int oakengine_sequence_set_preview_divider(OakEngineSequence *self, + int divider, int undoable) +{ + set_seq_error(QString()); + if (!self) { + set_seq_error(QStringLiteral("invalid sequence")); + return OAKENGINE_E_INVALID; + } + if (divider < 1) { + set_seq_error(QStringLiteral("invalid preview divider %1") + .arg(divider)); + return OAKENGINE_E_INVALID; + } + olive::Sequence *sequence = impl(self); + const olive::VideoParams current = sequence->get_video_params(); + if (current.divider() == divider) { + return OAKENGINE_OK; + } + // Same constructor rebuild as in set_video_params (keeps the derived + // effective size in sync). + const olive::VideoParams updated( + current.width(), current.height(), current.time_base(), + current.format(), current.channel_count(), + current.pixel_aspect_ratio(), current.interlacing(), divider); + apply_or_push(new SequenceVideoParamsCommand(sequence, updated), + QStringLiteral("Set Sequence Preview Divider"), undoable); + return OAKENGINE_OK; +} + +int oakengine_sequence_get_video_auto_cache(const OakEngineSequence *self) +{ + if (!self) { + return 0; + } + return impl(self)->is_video_auto_cache_enabled() ? 1 : 0; +} + +int oakengine_sequence_set_video_auto_cache(OakEngineSequence *self, + int enabled, int undoable) +{ + set_seq_error(QString()); + Q_UNUSED(undoable) + if (!self) { + set_seq_error(QStringLiteral("invalid sequence")); + return OAKENGINE_E_INVALID; + } + // The engine's auto-cache accessors are stubs for now (the read always + // returns false, the write is a no-op; the application's dialog has the + // checkbox TEMP-disabled accordingly). Forwarded without an undo + // command so the facade surface is ready when the engine lands it. + impl(self)->set_video_auto_cache_enabled(enabled != 0); + return OAKENGINE_OK; +} + int oakengine_sequence_track_count(const OakEngineSequence *self, int *video, int *audio, int *subtitle) { diff --git a/engine/tests/oakengine_timeline_edit_test.cpp b/engine/tests/oakengine_timeline_edit_test.cpp index f0c0db72c..a4a0f1519 100644 --- a/engine/tests/oakengine_timeline_edit_test.cpp +++ b/engine/tests/oakengine_timeline_edit_test.cpp @@ -37,6 +37,7 @@ #include "oakengine/footage.h" #include "oakengine/init.h" +#include "oakengine/node.h" #include "oakengine/project.h" #include "oakengine/timeline.h" @@ -641,6 +642,174 @@ static void test_markers(void) oakengine_project_free(project); } +static void test_sequence_params(void) +{ + OakEngineProject *project = oakengine_project_create(); + assert(project != NULL); + assert(oakengine_project_new(project) == OAKENGINE_OK); + OakEngineSequence *seq = oakengine_sequence_new(project, "Params"); + assert(seq != NULL); + + int w = 0, h = 0, fn = 0, fd = 0, pn = 0, pd = 0, il = -1, fmt = -1, + div = 0; + assert(oakengine_sequence_get_video_params_ex(seq, &w, &h, &fn, &fd, &pn, + &pd, &il, &fmt, + &div) == OAKENGINE_OK); + const int base_w = w, base_h = h, base_div = div; + assert(base_w > 0 && base_h > 0 && fn > 0 && fd > 0 && div >= 1); + assert(oakengine_sequence_get_video_params_ex(NULL, &w, &h, &fn, &fd, + &pn, &pd, &il, &fmt, + &div) == OAKENGINE_E_INVALID); + + // Full video parameter write (f32 = 4, bottom-first = 2) + readback. + assert(oakengine_sequence_set_video_params(seq, 1280, 720, 24, 1, 1, 1, 2, + 4, 1) == OAKENGINE_OK); + assert(oakengine_sequence_get_video_params_ex(seq, &w, &h, &fn, &fd, &pn, + &pd, &il, &fmt, + &div) == OAKENGINE_OK); + assert(w == 1280 && h == 720 && fn == 24 && fd == 1); + assert(pn == 1 && pd == 1 && il == 2 && fmt == 4 && div == base_div); + int gn = 0, gd = 0; + assert(oakengine_sequence_get_frame_rate(seq, &gn, &gd) == OAKENGINE_OK); + assert(gn == 24 && gd == 1); + + // Partial update: -1 leaves the other fields unchanged. + assert(oakengine_sequence_set_video_params(seq, 640, -1, -1, -1, -1, -1, + -1, -1, 1) == OAKENGINE_OK); + assert(oakengine_sequence_get_video_params_ex(seq, &w, &h, &fn, &fd, &pn, + &pd, &il, &fmt, + &div) == OAKENGINE_OK); + assert(w == 640 && h == 720 && fn == 24 && il == 2); + + // Invalid values: zero size, half a rational pair, out-of-range enum. + assert(oakengine_sequence_set_video_params(seq, 0, 720, -1, -1, -1, -1, + -1, -1, + 1) == OAKENGINE_E_INVALID); + assert(oakengine_sequence_set_video_params(seq, -1, -1, 24, -1, -1, -1, + -1, -1, + 1) == OAKENGINE_E_INVALID); + assert(oakengine_sequence_set_video_params(seq, -1, -1, -1, -1, -1, -1, 3, + -1, + 1) == OAKENGINE_E_INVALID); + assert(oakengine_sequence_set_video_params(seq, -1, -1, -1, -1, -1, -1, + -1, 5, + 1) == OAKENGINE_E_INVALID); + assert(oakengine_sequence_set_video_params(NULL, 1, 1, -1, -1, -1, -1, -1, + -1, + 1) == OAKENGINE_E_INVALID); + + // Undo restores step by step (partial write, then full write). + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_sequence_get_video_params_ex(seq, &w, &h, &fn, &fd, &pn, + &pd, &il, &fmt, + &div) == OAKENGINE_OK); + assert(w == 1280 && h == 720); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_sequence_get_video_params_ex(seq, &w, &h, &fn, &fd, &pn, + &pd, &il, &fmt, + &div) == OAKENGINE_OK); + assert(w == base_w && h == base_h); + + // Audio round trip; 0 / <= 0 leaves the field unchanged. + int sr = 0; + uint64_t layout = 0; + assert(oakengine_sequence_get_audio_params(seq, &sr, &layout) == + OAKENGINE_OK); + assert(sr > 0 && layout != 0); + const int base_sr = sr; + const uint64_t base_layout = layout; + assert(oakengine_sequence_set_audio_params(seq, 44100, 3, 1) == + OAKENGINE_OK); + assert(oakengine_sequence_get_audio_params(seq, &sr, &layout) == + OAKENGINE_OK); + assert(sr == 44100 && layout == 3); + assert(oakengine_sequence_set_audio_params(seq, 48000, 0, 1) == + OAKENGINE_OK); + assert(oakengine_sequence_get_audio_params(seq, &sr, &layout) == + OAKENGINE_OK); + assert(sr == 48000 && layout == 3); + assert(oakengine_sequence_set_audio_params(NULL, 48000, 3, 1) == + OAKENGINE_E_INVALID); + assert(oakengine_sequence_get_audio_params(NULL, &sr, &layout) == + OAKENGINE_E_INVALID); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_sequence_get_audio_params(seq, &sr, &layout) == + OAKENGINE_OK); + assert(sr == base_sr && layout == base_layout); + + // Preview divider; the other video params stay untouched. + assert(oakengine_sequence_get_preview_divider(seq) == base_div); + assert(oakengine_sequence_set_preview_divider(seq, 4, 1) == OAKENGINE_OK); + assert(oakengine_sequence_get_preview_divider(seq) == 4); + assert(oakengine_sequence_set_preview_divider(seq, 0, 1) == + OAKENGINE_E_INVALID); + assert(oakengine_sequence_set_preview_divider(NULL, 1, 1) == + OAKENGINE_E_INVALID); + assert(oakengine_sequence_get_preview_divider(NULL) == 0); + assert(oakengine_sequence_get_video_params_ex(seq, &w, &h, &fn, &fd, &pn, + &pd, &il, &fmt, + &div) == OAKENGINE_OK); + assert(w == base_w && h == base_h && div == 4); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_sequence_get_preview_divider(seq) == base_div); + + // Auto-cache flag: the engine accessors are stubs (read always 0, write + // ignored), so there is nothing to undo here. + assert(oakengine_sequence_get_video_auto_cache(seq) == 0); + assert(oakengine_sequence_set_video_auto_cache(seq, 1, 1) == OAKENGINE_OK); + assert(oakengine_sequence_get_video_auto_cache(seq) == 0); + assert(oakengine_sequence_get_video_auto_cache(NULL) == 0); + assert(oakengine_sequence_set_video_auto_cache(NULL, 1, 1) == + OAKENGINE_E_INVALID); + + // Label through the node family with the undoable flag. + assert(oakengine_node_set_label_ex((OakEngineNode *)seq, "Renamed", 1) == + OAKENGINE_OK); + char name[64]; + assert(oakengine_sequence_name(seq, name, sizeof(name)) > 0); + assert(strcmp(name, "Renamed") == 0); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_sequence_name(seq, name, sizeof(name)) > 0); + assert(strcmp(name, "Params") == 0); + + oakengine_project_free(project); + + // undoable == 0 applies directly with no undo entry: on a fresh project + // the creation is undone first so the stack is empty (the node stays + // alive under its creation command). + OakEngineProject *p2 = oakengine_project_create(); + assert(p2 != NULL); + assert(oakengine_project_new(p2) == OAKENGINE_OK); + OakEngineSequence *s2 = oakengine_sequence_new(p2, "Direct"); + assert(s2 != NULL); + assert(oakengine_project_undo(p2) == OAKENGINE_OK); + assert(oakengine_project_can_undo(p2) == 0); + assert(oakengine_sequence_set_video_params(s2, 800, 600, -1, -1, -1, -1, + -1, -1, + 0) == OAKENGINE_OK); + assert(oakengine_sequence_get_video_params_ex(s2, &w, &h, &fn, &fd, &pn, + &pd, &il, &fmt, + &div) == OAKENGINE_OK); + assert(w == 800 && h == 600); + assert(oakengine_project_can_undo(p2) == 0); + assert(oakengine_sequence_set_audio_params(s2, 22050, 0, 0) == + OAKENGINE_OK); + assert(oakengine_sequence_get_audio_params(s2, &sr, &layout) == + OAKENGINE_OK); + assert(sr == 22050); + assert(oakengine_project_can_undo(p2) == 0); + assert(oakengine_sequence_set_preview_divider(s2, 2, 0) == OAKENGINE_OK); + assert(oakengine_sequence_get_preview_divider(s2) == 2); + assert(oakengine_project_can_undo(p2) == 0); + assert(oakengine_node_set_label_ex((OakEngineNode *)s2, "NoUndo", 0) == + OAKENGINE_OK); + assert(oakengine_sequence_name(s2, name, sizeof(name)) > 0); + assert(strcmp(name, "NoUndo") == 0); + assert(oakengine_project_can_undo(p2) == 0); + oakengine_project_free(p2); +} + int main(void) { make_tmpdir(); @@ -669,6 +838,7 @@ int main(void) test_edit_round2(path); test_track_structure(path); test_markers(); + test_sequence_params(); oakengine_project_free(project); assert(oakengine_shutdown() == OAKENGINE_OK);