/*** Oak - 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 . ***/ #include "oakengine/app.h" #include #include #include #include #include #include "coreengine.h" #include "node/project.h" #include "node/project/footage/footage.h" #include "node/project/sequence/sequence.h" #include "node/project/serializer/serializedlayoutinfo.h" #include "task/task.h" #include "undo/undostack.h" namespace { olive::Project *impl(OakEngineProject *h) { return reinterpret_cast(h); } OakEngineProject *wrap(olive::Project *p) { return reinterpret_cast(p); } OakEngineSequence *wrap_seq(olive::Sequence *s) { return reinterpret_cast(s); } // buf/size convention: returns the would-be length excluding the NUL. int string_to_buf(const QString &s, char *buf, int buf_size) { const QByteArray utf = s.toUtf8(); if (buf && buf_size > 0) { snprintf(buf, size_t(buf_size), "%s", utf.constData()); } return int(utf.size()); } // Registered callback set (all fields may be null). OakEngineAppCallbacks g_callbacks = {}; // Whether oakengine_app_start() has run (and oakengine_app_stop() has not). bool g_started = false; // The EngineCore the notification signals are currently connected to. olive::EngineCore *g_connected_core = nullptr; olive::EngineCore *app_core() { return olive::EngineCore::instance(); } // The EngineCore constructor's UndoStack member creates QActions, which need // QGuiApplication state (same reason as oakengine_init()). void ensure_qcoreapplication() { if (QCoreApplication::instance()) { return; } if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) { qputenv("QT_QPA_PLATFORM", "offscreen"); } static int argc = 1; static char app_name[] = "oakengine"; static char *argv[] = { app_name, nullptr }; new QGuiApplication(argc, argv); QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org")); QCoreApplication::setApplicationName(QStringLiteral("Oak Video Editor")); } // Forward engine signals to the registered C callbacks. Connected once per // EngineCore instance; dropped events are fine while no callback is set. void connect_notifications(olive::EngineCore *core) { if (!core || g_connected_core == core) { return; } g_connected_core = core; QObject::connect(core, &olive::EngineCore::status_message_show, core, [](const QString &message, int timeout) { if (g_callbacks.status_message_show) { g_callbacks.status_message_show( message.toUtf8().constData(), timeout, g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::status_message_clear, core, [] { if (g_callbacks.status_message_clear) { g_callbacks.status_message_clear( g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::cache_full_warning_requested, core, [] { if (g_callbacks.cache_full_warning) { g_callbacks.cache_full_warning( g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::active_project_changed, core, [](olive::Project *p) { if (g_callbacks.active_project_changed) { g_callbacks.active_project_changed( wrap(p), g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::tool_changed, core, [](const olive::Tool::Item &tool) { if (g_callbacks.tool_changed) { g_callbacks.tool_changed(int(tool), g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::addable_object_changed, core, [](olive::Tool::AddableObject o) { if (g_callbacks.addable_object_changed) { g_callbacks.addable_object_changed( int(o), g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::snapping_changed, core, [](const bool &b) { if (g_callbacks.snapping_changed) { g_callbacks.snapping_changed(b ? 1 : 0, g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::timecode_display_changed, core, [](olive::core::Timecode::Display d) { if (g_callbacks.timecode_display_changed) { g_callbacks.timecode_display_changed( int(d), g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::open_recent_list_changed, core, [] { if (g_callbacks.open_recent_list_changed) { g_callbacks.open_recent_list_changed( g_callbacks.userdata); } }); QObject::connect(core, &olive::EngineCore::color_picker_enabled, core, [](bool e) { if (g_callbacks.color_picker_enabled) { g_callbacks.color_picker_enabled( e ? 1 : 0, g_callbacks.userdata); } }); } // Translate the C handler callbacks into the std::function handlers // EngineCore calls when it needs user interaction. void install_handlers(olive::EngineCore *core) { if (g_callbacks.confirm_image_sequence) { core->set_confirm_image_sequence_handler([](const QString &filename) { return g_callbacks.confirm_image_sequence( filename.toUtf8().constData(), g_callbacks.userdata) != 0; }); } else { core->set_confirm_image_sequence_handler(nullptr); } if (g_callbacks.relink_footage) { core->set_relink_handler([](QVector footage) { return g_callbacks.relink_footage( reinterpret_cast(footage.data()), int(footage.size()), g_callbacks.userdata) != 0; }); } else { core->set_relink_handler(nullptr); } if (g_callbacks.save_project) { core->set_save_project_handler([](const QString &override_filename) { g_callbacks.save_project(override_filename.toUtf8().constData(), g_callbacks.userdata); }); } else { core->set_save_project_handler(nullptr); } if (g_callbacks.close_project) { core->set_close_project_handler([] { return g_callbacks.close_project(g_callbacks.userdata) != 0; }); } else { core->set_close_project_handler(nullptr); } if (g_callbacks.load_layout) { core->set_load_layout_handler( [](const olive::SerializedLayoutInfo &layout) { g_callbacks.load_layout(&layout, g_callbacks.userdata); }); } else { core->set_load_layout_handler(nullptr); } #ifdef USE_OTIO if (g_callbacks.otio_import) { core->set_otio_import_handler( [](const QList &sequences) { QVector handles; handles.reserve(sequences.size()); for (olive::Sequence *s : sequences) { handles.append(wrap_seq(s)); } return g_callbacks.otio_import(handles.data(), int(handles.size()), g_callbacks.userdata) != 0; }); } else { core->set_otio_import_handler(nullptr); } #endif } } // namespace extern "C" { int oakengine_app_create(const OakEngineAppParams *params) { if (app_core()) { return OAKENGINE_E_STATE; } ensure_qcoreapplication(); olive::EngineCore::CoreParams core_params; if (params) { switch (params->run_mode) { case OAKENGINE_APP_RUN_HEADLESS_EXPORT: core_params.set_run_mode( olive::EngineCore::CoreParams::k_headless_export); break; case OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE: core_params.set_run_mode( olive::EngineCore::CoreParams::k_headless_pre_cache); break; default: core_params.set_run_mode( olive::EngineCore::CoreParams::k_run_normal); break; } core_params.set_fullscreen(params->fullscreen != 0); if (params->startup_project) { core_params.set_startup_project( QString::fromUtf8(params->startup_project)); } if (params->startup_language) { core_params.set_startup_language( QString::fromUtf8(params->startup_language)); } if (params->crash_on_startup) { core_params.set_crash_on_startup(true); } } // Never deleted: backs the process-wide EngineCore singleton (same // lifetime rule as the oakengine_init() shell). new olive::EngineCore(core_params); return OAKENGINE_OK; } int oakengine_app_start(void) { if (!app_core() || g_started) { return OAKENGINE_E_STATE; } app_core()->start(); g_started = true; return OAKENGINE_OK; } int oakengine_app_stop(void) { if (!app_core() || !g_started) { return OAKENGINE_E_STATE; } app_core()->stop(); g_started = false; return OAKENGINE_OK; } int oakengine_app_set_callbacks(const OakEngineAppCallbacks *callbacks) { if (callbacks) { g_callbacks = *callbacks; } else { g_callbacks = OakEngineAppCallbacks{}; } if (olive::EngineCore *core = app_core()) { connect_notifications(core); install_handlers(core); } return OAKENGINE_OK; } int oakengine_app_run_mode(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } switch (app_core()->core_params().run_mode()) { case olive::EngineCore::CoreParams::k_headless_export: return OAKENGINE_APP_RUN_HEADLESS_EXPORT; case olive::EngineCore::CoreParams::k_headless_pre_cache: return OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE; default: return OAKENGINE_APP_RUN_NORMAL; } } int oakengine_app_fullscreen(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } return app_core()->core_params().fullscreen() ? 1 : 0; } int oakengine_app_startup_project(char *buf, int buf_size) { if (!app_core()) { return OAKENGINE_E_INVALID; } return string_to_buf(app_core()->core_params().startup_project(), buf, buf_size); } void *oakengine_app_undo_stack(void) { if (!app_core()) { return nullptr; } return app_core()->undo_stack(); } int oakengine_app_tool(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } return int(app_core()->tool()); } int oakengine_app_set_tool(int tool) { if (!app_core()) { return OAKENGINE_E_INVALID; } if (tool < 0 || tool >= int(olive::Tool::k_count)) { return OAKENGINE_E_INVALID; } app_core()->set_tool(static_cast(tool)); return OAKENGINE_OK; } int oakengine_app_addable_object(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } return int(app_core()->get_selected_addable_object()); } int oakengine_app_set_addable_object(int object) { if (!app_core()) { return OAKENGINE_E_INVALID; } if (object < 0 || object >= int(olive::Tool::k_addable_count)) { return OAKENGINE_E_INVALID; } app_core()->set_selected_addable_object( static_cast(object)); return OAKENGINE_OK; } int oakengine_app_selected_transition(char *buf, int buf_size) { if (!app_core()) { return OAKENGINE_E_INVALID; } return string_to_buf(app_core()->get_selected_transition(), buf, buf_size); } int oakengine_app_set_selected_transition(const char *id) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->set_selected_transition_object( id ? QString::fromUtf8(id) : QString()); return OAKENGINE_OK; } int oakengine_app_snapping(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } return app_core()->snapping() ? 1 : 0; } int oakengine_app_set_snapping(int enabled) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->set_snapping(enabled != 0); return OAKENGINE_OK; } int oakengine_app_timecode_display(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } return int(app_core()->get_timecode_display()); } int oakengine_app_set_timecode_display(int display) { if (!app_core()) { return OAKENGINE_E_INVALID; } if (display < 0 || display > int(olive::core::Timecode::k_milliseconds)) { return OAKENGINE_E_INVALID; } app_core()->set_timecode_display( static_cast(display)); return OAKENGINE_OK; } int oakengine_app_recent_projects_count(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } return int(app_core()->get_recent_projects().size()); } int oakengine_app_recent_project_at(int index, char *buf, int buf_size) { if (!app_core()) { return OAKENGINE_E_INVALID; } const QStringList &recent = app_core()->get_recent_projects(); if (index < 0 || index >= recent.size()) { return OAKENGINE_E_NOT_FOUND; } return string_to_buf(recent.at(index), buf, buf_size); } int oakengine_app_remove_recent_project(int index) { if (!app_core()) { return OAKENGINE_E_INVALID; } if (index < 0 || index >= app_core()->get_recent_projects().size()) { return OAKENGINE_E_NOT_FOUND; } app_core()->remove_recently_opened_project(index); return OAKENGINE_OK; } int oakengine_app_clear_recent_projects(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->clear_open_recent_list(); return OAKENGINE_OK; } int oakengine_app_show_status_message(const char *message, int timeout) { if (!app_core() || !message) { return OAKENGINE_E_INVALID; } app_core()->show_status_bar_message(QString::fromUtf8(message), timeout); return OAKENGINE_OK; } int oakengine_app_clear_status_message(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->clear_status_bar_message(); return OAKENGINE_OK; } int oakengine_app_set_language(const char *locale) { if (!app_core() || !locale) { return OAKENGINE_E_INVALID; } return app_core()->set_language(QString::fromUtf8(locale)) ? 1 : 0; } int oakengine_app_set_autorecovery_interval(int minutes) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->set_autorecovery_interval(minutes); return OAKENGINE_OK; } int oakengine_app_set_use_proxy_media(int enabled) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->set_use_proxy_media(enabled != 0); return OAKENGINE_OK; } int oakengine_app_request_pixel_sampling(int enable) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->request_pixel_sampling_in_viewers(enable != 0); return OAKENGINE_OK; } int oakengine_app_set_magic(int enabled) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->set_magic(enabled != 0); return OAKENGINE_OK; } int oakengine_app_is_magic_enabled(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } return app_core()->is_magic_enabled() ? 1 : 0; } int oakengine_app_copy_to_clipboard(const char *text) { if (!app_core() || !text) { return OAKENGINE_E_INVALID; } olive::EngineCore::copy_string_to_clipboard(QString::fromUtf8(text)); return OAKENGINE_OK; } int oakengine_app_paste_from_clipboard(char *buf, int buf_size) { if (!app_core()) { return OAKENGINE_E_INVALID; } return string_to_buf(olive::EngineCore::paste_string_from_clipboard(), buf, buf_size); } int oakengine_app_footage_file_dialog_filter(char *buf, int buf_size) { return string_to_buf(olive::EngineCore::footage_file_dialog_filter(), buf, buf_size); } int oakengine_app_is_footage_extension_allowed(const char *path) { if (!path) { return OAKENGINE_E_INVALID; } return olive::EngineCore::is_footage_extension_allowed( QString::fromUtf8(path)) ? 1 : 0; } OakEngineSequence *oakengine_app_create_sequence(OakEngineProject *project, const char *name_format) { if (!app_core() || !project) { return nullptr; } const QString format = name_format ? QString::fromUtf8(name_format) : QStringLiteral("Sequence %1"); return wrap_seq(olive::EngineCore::create_new_sequence_for_project( format, impl(project))); } int oakengine_app_auto_recovery_index_filename(char *buf, int buf_size) { if (!app_core()) { return OAKENGINE_E_INVALID; } return string_to_buf(olive::EngineCore::get_auto_recovery_index_filename(), buf, buf_size); } OakEngineProject *oakengine_app_open_project(void) { if (!app_core()) { return nullptr; } return wrap(app_core()->open_project()); } int oakengine_app_create_new_project(void) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->create_new_project(); return OAKENGINE_OK; } int oakengine_app_add_open_project(OakEngineProject *project, int add_to_recents) { if (!app_core() || !project) { return OAKENGINE_E_INVALID; } app_core()->add_open_project(impl(project), add_to_recents != 0); return OAKENGINE_OK; } int oakengine_app_add_open_project_from_task(void *task, int add_to_recents) { if (!app_core() || !task) { return OAKENGINE_E_INVALID; } return app_core()->add_open_project_from_task( static_cast(task), add_to_recents != 0) ? 1 : 0; } int oakengine_app_add_recovery_project_from_task(void *task) { if (!app_core() || !task) { return OAKENGINE_E_INVALID; } app_core()->add_recovery_project_from_task(static_cast(task)); return OAKENGINE_OK; } int oakengine_app_on_project_saved(OakEngineProject *project) { if (!app_core() || !project) { return OAKENGINE_E_INVALID; } app_core()->on_project_saved(impl(project)); return OAKENGINE_OK; } int oakengine_app_set_active_project(OakEngineProject *project) { if (!app_core()) { return OAKENGINE_E_INVALID; } app_core()->set_active_project(impl(project)); return OAKENGINE_OK; } // ---- Individual handler setter convenience wrappers ---- int oakengine_app_set_confirm_image_sequence_handler( int (*fn)(const char *filename, void *userdata), void *userdata) { OakEngineAppCallbacks cb = g_callbacks; cb.userdata = userdata; cb.confirm_image_sequence = fn; return oakengine_app_set_callbacks(&cb); } int oakengine_app_set_relink_handler( int (*fn)(OakEngineFootage **footage, int count, void *userdata), void *userdata) { OakEngineAppCallbacks cb = g_callbacks; cb.userdata = userdata; cb.relink_footage = fn; return oakengine_app_set_callbacks(&cb); } int oakengine_app_set_save_project_handler( void (*fn)(const char *override_filename, void *userdata), void *userdata) { OakEngineAppCallbacks cb = g_callbacks; cb.userdata = userdata; cb.save_project = fn; return oakengine_app_set_callbacks(&cb); } int oakengine_app_set_close_project_handler( int (*fn)(void *userdata), void *userdata) { OakEngineAppCallbacks cb = g_callbacks; cb.userdata = userdata; cb.close_project = fn; return oakengine_app_set_callbacks(&cb); } int oakengine_app_set_load_layout_handler( void (*fn)(const void *layout, void *userdata), void *userdata) { OakEngineAppCallbacks cb = g_callbacks; cb.userdata = userdata; cb.load_layout = fn; return oakengine_app_set_callbacks(&cb); } // ---- void*-based convenience overloads ---- int oakengine_app_get_auto_recovery_index_filename(char *buf, int buf_size) { return oakengine_app_auto_recovery_index_filename(buf, buf_size); } int oakengine_app_remove_recently_opened_project(int index) { return oakengine_app_remove_recent_project(index); } int oakengine_app_on_project_saved_vp(void *project) { return oakengine_app_on_project_saved( reinterpret_cast(project)); } int oakengine_app_set_active_project_vp(void *project) { return oakengine_app_set_active_project( reinterpret_cast(project)); } int oakengine_app_add_open_project_vp(void *project, int add_to_recents) { return oakengine_app_add_open_project( reinterpret_cast(project), add_to_recents); } } // extern "C"