diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 1651d7ae5..b25a9f764 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -19,6 +19,33 @@
set(OLIVE_SOURCES
core.h
core.cpp
+ engineeventbridge.h
+ engineeventbridge.cpp
+ common/htmlapp.cpp
+ common/filefunctionsapp.cpp
+ common/colorcodingapp.h
+ common/colorcodingapp.cpp
+ common/xmlutilsapp.cpp
+ common/hashstreamapp.cpp
+ common/qtutilsapp.cpp
+ common/nodevaluehandle.h
+)
+
+# The app-side *app.cpp helpers define symbols whose qualified names also
+# exist in liboakengine.so (B10 moved utilities). Build them with hidden
+# visibility so the executable's definitions are not ELF-interposable: the
+# engine library keeps binding to its own copies and static data members
+# (ColorCoding::colors, Html::k_block_tags) are not double-initialized and
+# double-destroyed at process exit (was the exit-time heap corruption in
+# timeline-tests / olive-gtest).
+set_source_files_properties(
+ common/htmlapp.cpp
+ common/filefunctionsapp.cpp
+ common/colorcodingapp.cpp
+ common/xmlutilsapp.cpp
+ common/hashstreamapp.cpp
+ common/qtutilsapp.cpp
+ PROPERTIES COMPILE_OPTIONS "-fvisibility=hidden"
)
#set(OLIVE_RESOURCES)
@@ -27,6 +54,7 @@ set(OLIVE_SOURCES
add_subdirectory(dialog)
add_subdirectory(packaging)
add_subdirectory(panel)
+add_subdirectory(timeline)
add_subdirectory(ts)
add_subdirectory(ui)
add_subdirectory(widget)
diff --git a/app/common/colorcodingapp.cpp b/app/common/colorcodingapp.cpp
new file mode 100644
index 000000000..022fc74aa
--- /dev/null
+++ b/app/common/colorcodingapp.cpp
@@ -0,0 +1,81 @@
+/***
+
+ 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 "common/colorcodingapp.h"
+
+#include
+
+namespace olive
+{
+
+QVector ColorCoding::colors = {
+ Color(0.545f, 0.255f, 0.255f), Color(0.412f, 0.188f, 0.259f),
+ Color(0.561f, 0.427f, 0.239f), Color(0.486f, 0.306f, 0.235f),
+ Color(0.631f, 0.612f, 0.212f), Color(0.404f, 0.478f, 0.243f),
+ Color(0.349f, 0.576f, 0.275f), Color(0.224f, 0.459f, 0.251f),
+ Color(0.259f, 0.471f, 0.541f), Color(0.184f, 0.376f, 0.329f),
+ Color(0.259f, 0.365f, 0.541f), Color(0.196f, 0.216f, 0.412f),
+ Color(0.612f, 0.294f, 0.502f), Color(0.404f, 0.220f, 0.459f),
+ Color(0.800f, 0.800f, 0.800f), Color(0.502f, 0.502f, 0.502f)
+};
+
+const QVector &ColorCoding::standard_colors()
+{
+ return colors;
+}
+
+QString ColorCoding::get_color_name(int c)
+{
+ switch (c) {
+ case k_red: return QObject::tr("Red");
+ case k_maroon: return QObject::tr("Maroon");
+ case k_orange: return QObject::tr("Orange");
+ case k_brown: return QObject::tr("Brown");
+ case k_yellow: return QObject::tr("Yellow");
+ case k_olive: return QObject::tr("Olive");
+ case k_lime: return QObject::tr("Lime");
+ case k_green: return QObject::tr("Green");
+ case k_cyan: return QObject::tr("Cyan");
+ case k_teal: return QObject::tr("Teal");
+ case k_blue: return QObject::tr("Blue");
+ case k_navy: return QObject::tr("Navy");
+ case k_pink: return QObject::tr("Pink");
+ case k_purple: return QObject::tr("Purple");
+ case k_silver: return QObject::tr("Silver");
+ case k_gray: return QObject::tr("Gray");
+ }
+ return QString();
+}
+
+Color ColorCoding::get_color(int c)
+{
+ return colors.at(c);
+}
+
+Qt::GlobalColor ColorCoding::get_ui_selector_color(const Color &c)
+{
+ if (c.get_rough_luminance() > 0.40f) {
+ return Qt::black;
+ } else {
+ return Qt::white;
+ }
+}
+
+}
diff --git a/app/common/colorcodingapp.h b/app/common/colorcodingapp.h
new file mode 100644
index 000000000..453d2812c
--- /dev/null
+++ b/app/common/colorcodingapp.h
@@ -0,0 +1,75 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_COLORCODINGAPP_H
+#define OAK_COLORCODINGAPP_H
+
+#include
+#include
+#include
+
+namespace olive
+{
+
+using namespace core;
+
+/**
+ * @brief App-side ColorCoding (moved from engine/ui/colorcoding.h)
+ *
+ * Provides the same static color-label mapping as the engine version but
+ * without QObject inheritance (no moc symbols). Only the static methods
+ * used by app code are included.
+ */
+class ColorCoding {
+public:
+ enum Code {
+ k_red,
+ k_maroon,
+ k_orange,
+ k_brown,
+ k_yellow,
+ k_olive,
+ k_lime,
+ k_green,
+ k_cyan,
+ k_teal,
+ k_blue,
+ k_navy,
+ k_pink,
+ k_purple,
+ k_silver,
+ k_gray
+ };
+
+ static QString get_color_name(int c);
+
+ static Color get_color(int c);
+
+ static Qt::GlobalColor get_ui_selector_color(const Color &c);
+
+ static const QVector &standard_colors();
+
+private:
+ static QVector colors;
+};
+
+} // namespace olive
+
+#endif // OAK_COLORCODINGAPP_H
diff --git a/app/common/configwrapper.h b/app/common/configwrapper.h
new file mode 100644
index 000000000..652e10ef3
--- /dev/null
+++ b/app/common/configwrapper.h
@@ -0,0 +1,206 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+ Modifications Copyright (C) 2025 mikesolar
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef OAK_CONFIGWRAPPER_H
+#define OAK_CONFIGWRAPPER_H
+
+#include
+
+#include "olive/core/util/rational.h"
+#include "oakengine/config.h"
+
+// Facade migration B9b: replace the engine's OAK_CONFIG macro (which
+// references olive::Config::current()/operator[] and brings C++ symbols into
+// the editor binary) with a thin header-only wrapper around the C ABI.
+//
+// Include this header instead of "config/config.h" in app code. It undefines
+// the engine macros and redefines them to return an inline OakConfigValue that
+// forwards reads/writes to oakengine_config_*().
+
+namespace olive
+{
+
+class OakConfigValue {
+public:
+ explicit OakConfigValue(const QString &key) : key_(key) {}
+
+ operator bool() const
+ {
+ return oakengine_config_get_int(key_utf8(), 0) != 0;
+ }
+ operator int() const
+ {
+ return static_cast(oakengine_config_get_int(key_utf8(), 0));
+ }
+ operator qint64() const
+ {
+ return static_cast(oakengine_config_get_int(key_utf8(), 0));
+ }
+ operator quint64() const
+ {
+ return static_cast(oakengine_config_get_int(key_utf8(), 0));
+ }
+ operator int64_t() const
+ {
+ return oakengine_config_get_int(key_utf8(), 0);
+ }
+ operator uint64_t() const
+ {
+ return static_cast(oakengine_config_get_int(key_utf8(), 0));
+ }
+ operator QString() const
+ {
+ char buf[1024];
+ const int len = oakengine_config_get_string(key_utf8(), buf,
+ sizeof(buf));
+ return QString::fromUtf8(buf, len);
+ }
+ operator QVariant() const
+ {
+ return QVariant(static_cast(*this));
+ }
+
+ OakConfigValue &operator=(bool v)
+ {
+ oakengine_config_set_int(key_utf8(), v ? 1 : 0);
+ return *this;
+ }
+ OakConfigValue &operator=(int v)
+ {
+ oakengine_config_set_int(key_utf8(), static_cast(v));
+ return *this;
+ }
+ OakConfigValue &operator=(uint v)
+ {
+ oakengine_config_set_int(key_utf8(), static_cast(v));
+ return *this;
+ }
+ OakConfigValue &operator=(qint64 v)
+ {
+ oakengine_config_set_int(key_utf8(), static_cast(v));
+ return *this;
+ }
+ OakConfigValue &operator=(quint64 v)
+ {
+ oakengine_config_set_int(key_utf8(), static_cast(v));
+ return *this;
+ }
+ OakConfigValue &operator=(int64_t v)
+ {
+ oakengine_config_set_int(key_utf8(), v);
+ return *this;
+ }
+ OakConfigValue &operator=(uint64_t v)
+ {
+ oakengine_config_set_int(key_utf8(), static_cast(v));
+ return *this;
+ }
+ OakConfigValue &operator=(const QString &v)
+ {
+ const QByteArray utf8 = v.toUtf8();
+ oakengine_config_set_string(key_utf8(), utf8.constData());
+ return *this;
+ }
+ OakConfigValue &operator=(const char *v)
+ {
+ oakengine_config_set_string(key_utf8(), v ? v : "");
+ return *this;
+ }
+ OakConfigValue &operator=(const QVariant &v)
+ {
+ switch (v.typeId()) {
+ case QMetaType::Bool:
+ *this = v.toBool();
+ break;
+ case QMetaType::Int:
+ case QMetaType::UInt:
+ case QMetaType::LongLong:
+ case QMetaType::ULongLong:
+ case QMetaType::Long:
+ case QMetaType::Short:
+ case QMetaType::Char:
+ case QMetaType::ULong:
+ case QMetaType::UShort:
+ case QMetaType::UChar:
+ *this = v.toLongLong();
+ break;
+ case QMetaType::Double:
+ case QMetaType::Float:
+ *this = static_cast(v.toDouble());
+ break;
+ default:
+ *this = v.toString();
+ break;
+ }
+ return *this;
+ }
+
+ bool toBool() const { return static_cast(*this); }
+ int toInt() const { return static_cast(*this); }
+ qint64 toLongLong() const { return static_cast(*this); }
+ quint64 toULongLong() const { return static_cast(*this); }
+ QString toString() const { return static_cast(*this); }
+
+ bool operator==(int rhs) const { return toInt() == rhs; }
+ bool operator!=(int rhs) const { return toInt() != rhs; }
+ bool operator==(qint64 rhs) const { return toLongLong() == rhs; }
+ bool operator!=(qint64 rhs) const { return toLongLong() != rhs; }
+ bool operator==(const QString &rhs) const { return toString() == rhs; }
+ bool operator!=(const QString &rhs) const { return toString() != rhs; }
+ bool operator==(const char *rhs) const { return toString() == QString::fromUtf8(rhs); }
+ bool operator!=(const char *rhs) const { return toString() != QString::fromUtf8(rhs); }
+
+ template T value() const
+ {
+ if constexpr (std::is_same_v) {
+ const QString s = static_cast(*this);
+ const QByteArray utf8 = s.toUtf8();
+ return olive::core::Rational::from_string(
+ std::string(utf8.constData(), size_t(utf8.size())));
+ } else {
+ return static_cast(*this);
+ }
+ }
+
+private:
+ const char *key_utf8() const
+ {
+ key_utf8_ = key_.toUtf8();
+ return key_utf8_.constData();
+ }
+
+ QString key_;
+ mutable QByteArray key_utf8_;
+};
+
+} // namespace olive
+
+#ifdef OAK_CONFIG
+#undef OAK_CONFIG
+#endif
+#ifdef OAK_CONFIG_STR
+#undef OAK_CONFIG_STR
+#endif
+
+#define OAK_CONFIG(x) olive::OakConfigValue(QStringLiteral(x))
+#define OAK_CONFIG_STR(x) olive::OakConfigValue(x)
+
+#endif // OAK_CONFIGWRAPPER_H
diff --git a/app/common/debugapp.h b/app/common/debugapp.h
new file mode 100644
index 000000000..3b036e501
--- /dev/null
+++ b/app/common/debugapp.h
@@ -0,0 +1,87 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_DEBUGAPP_H
+#define OAK_DEBUGAPP_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace olive {
+
+/**
+ * @brief App-side debug handler (moved from engine/common/debug.cpp)
+ *
+ * Replaces engine's olive::debug_handler so oak-editor doesn't import
+ * that symbol. Only used in main.cpp's qInstallMessageHandler.
+ */
+static void debug_handler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+{
+ // Suppress noisy warnings from Qt's QXcbIntegration
+ if (type == QtWarningMsg && msg.contains("QXcbIntegration")) {
+ return;
+ }
+
+ // Suppress all Qt warnings during automated testing
+ static const bool is_testing = qEnvironmentVariableIsSet("OAK_TESTING");
+ if (is_testing && type == QtWarningMsg) {
+ return;
+ }
+
+ QString log_line;
+
+ switch (type) {
+ case QtDebugMsg:
+ log_line = QStringLiteral("Debug: %1 (%2:%3, %4)\n");
+ break;
+ case QtInfoMsg:
+ log_line = QStringLiteral("Info: %1 (%2:%3, %4)\n");
+ break;
+ case QtWarningMsg:
+ log_line = QStringLiteral("Warning: %1 (%2:%3, %4)\n");
+ break;
+ case QtCriticalMsg:
+ log_line = QStringLiteral("Critical: %1 (%2:%3, %4)\n");
+ break;
+ case QtFatalMsg:
+ log_line = QStringLiteral("Fatal: %1 (%2:%3, %4)\n");
+ break;
+ }
+
+ log_line = log_line.arg(msg, context.file != nullptr ? context.file : "",
+ QString::number(context.line), context.function != nullptr ?
+ context.function : "");
+
+ std::cerr << log_line.toUtf8().constData();
+
+ if (type == QtFatalMsg) {
+ abort();
+ }
+}
+
+} // namespace olive
+
+#endif // OAK_DEBUGAPP_H
diff --git a/app/common/filefunctionsapp.cpp b/app/common/filefunctionsapp.cpp
new file mode 100644
index 000000000..629753c24
--- /dev/null
+++ b/app/common/filefunctionsapp.cpp
@@ -0,0 +1,98 @@
+/***
+
+ 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 .
+
+***/
+
+// App-side implementations of FileFunctions methods that would otherwise
+// be imported from liboakengine. The declarations live in the engine header
+// (common/filefunctions.h) which is on the public include path; these
+// definitions resolve the symbols locally in the app binary.
+
+#include "common/filefunctions.h"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace olive
+{
+
+bool FileFunctions::directory_is_valid(const QDir &d,
+ bool try_to_create_if_not_exists)
+{
+ return d.exists() ||
+ (try_to_create_if_not_exists && d.mkpath(QStringLiteral(".")));
+}
+
+QString FileFunctions::read_file_as_string(const QString &filename)
+{
+ QFile f(filename);
+ QString file_data;
+ if (f.open(QFile::ReadOnly | QFile::Text)) {
+ QTextStream text_stream(&f);
+ file_data = text_stream.readAll();
+ f.close();
+ }
+ return file_data;
+}
+
+QString FileFunctions::get_auto_recovery_root()
+{
+ return QDir(QStandardPaths::writableLocation(
+ QStandardPaths::AppLocalDataLocation))
+ .filePath(QStringLiteral("autorecovery"));
+}
+
+QString FileFunctions::ensure_filename_extension(QString fn,
+ const QString &extension)
+{
+ if (!fn.isEmpty() && !extension.isEmpty()) {
+ QString extension_with_dot;
+ extension_with_dot.append('.');
+ extension_with_dot.append(extension);
+ if (!fn.endsWith(extension_with_dot, Qt::CaseInsensitive)) {
+ fn.append(extension_with_dot);
+ }
+ }
+ return fn;
+}
+
+QString FileFunctions::get_configuration_location()
+{
+ if (is_portable()) {
+ return get_application_path();
+ } else {
+ QString s = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
+ QDir(s).mkpath(".");
+ return s;
+ }
+}
+
+bool FileFunctions::is_portable()
+{
+ return QFileInfo::exists(QDir(get_application_path()).filePath("portable"));
+}
+
+QString FileFunctions::get_application_path()
+{
+ return QCoreApplication::applicationDirPath();
+}
+
+}
diff --git a/app/common/hashstreamapp.cpp b/app/common/hashstreamapp.cpp
new file mode 100644
index 000000000..92e6787d0
--- /dev/null
+++ b/app/common/hashstreamapp.cpp
@@ -0,0 +1,67 @@
+/***
+
+ 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 .
+
+***/
+
+// App-side implementations of qHash overloads and stream operators
+// used by QHash containers and QDataStream serialization in app code.
+// Provides local definitions so the app doesn't import these from liboakengine.
+
+#include "node/param.h"
+#include "node/output/track/track.h"
+
+namespace olive
+{
+
+uint qHash(const NodeInput &i)
+{
+ return qHash(i.node()) ^ qHash(i.input()) ^ ::qHash(i.element());
+}
+
+uint qHash(const NodeInputPair &i)
+{
+ return qHash(i.node) ^ qHash(i.input);
+}
+
+uint qHash(const NodeKeyframeTrackReference &i)
+{
+ return qHash(i.input()) ^ ::qHash(i.track());
+}
+
+uint qHash(const Track::Reference &r, uint seed)
+{
+ return ::qHash(QStringLiteral("%1:%2").arg(QString::number(r.type()),
+ QString::number(r.index())),
+ seed);
+}
+
+QDataStream &operator<<(QDataStream &out, const Track::Reference &ref)
+{
+ out << static_cast(ref.type()) << ref.index();
+ return out;
+}
+
+QDataStream &operator>>(QDataStream &in, Track::Reference &ref)
+{
+ int type, index;
+ in >> type >> index;
+ ref = Track::Reference(static_cast(type), index);
+ return in;
+}
+
+}
diff --git a/app/common/htmlapp.cpp b/app/common/htmlapp.cpp
new file mode 100644
index 000000000..ce76c9c77
--- /dev/null
+++ b/app/common/htmlapp.cpp
@@ -0,0 +1,507 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE 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 "htmlapp.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "common/xmlutils.h"
+
+#include
+#include
+
+#include "common/xmlutils.h"
+
+namespace olive
+{
+
+const QVector Html::k_block_tags = { QStringLiteral("p"),
+ QStringLiteral("div") };
+
+inline bool str_equals(const QStringView &a, const QStringView &b)
+{
+ return !a.compare(b, Qt::CaseInsensitive);
+}
+
+QString Html::doc_to_html(const QTextDocument *doc)
+{
+ QString html;
+ QXmlStreamWriter writer(&html);
+
+ //writer.setAutoFormatting(true);
+
+ for (auto it = doc->begin(); it != doc->end(); it = it.next()) {
+ write_block(&writer, it);
+ }
+
+ return html;
+}
+
+struct HtmlNode {
+ QString tag;
+ QTextCharFormat format;
+};
+
+QTextCharFormat merge_html_formats(const QVector &stack)
+{
+ QTextCharFormat f;
+
+ for (int i = 0; i < stack.size(); i++) {
+ f.merge(stack.at(i).format);
+ }
+
+ return f;
+}
+
+void Html::html_to_doc(QTextDocument *doc, const QString &html)
+{
+ // Empty doc
+ doc->clear();
+ bool inside_block = true;
+
+ // Create cursor, which appears to be Qt's official way of inserting blocks and fragments
+ QTextCursor c(doc);
+
+ QString wrapped = QStringLiteral("").append(html).append("");
+ QXmlStreamReader reader(wrapped);
+
+ QVector fmt_stack;
+
+ QTextCharFormat default_fmt;
+ default_fmt.setFontWeight(QFont::Normal);
+ fmt_stack.append({ QStringLiteral("html"), default_fmt });
+
+ QTextCharFormat current_fmt;
+
+ while (!reader.atEnd()) {
+ reader.readNext();
+
+ if (reader.tokenType() == QXmlStreamReader::StartElement) {
+ QString tag = reader.name().toString().toLower();
+
+ fmt_stack.append({ tag, read_char_format(reader.attributes()) });
+ current_fmt = merge_html_formats(fmt_stack);
+
+ if (k_block_tags.contains(tag)) {
+ QTextBlockFormat block_fmt =
+ read_block_format(reader.attributes());
+ if (inside_block) {
+ c.setBlockFormat(block_fmt);
+ c.setBlockCharFormat(current_fmt);
+ } else {
+ c.insertBlock(block_fmt, current_fmt);
+ inside_block = true;
+ }
+ }
+
+ } else if (reader.tokenType() == QXmlStreamReader::Characters) {
+ QString characters = reader.text().toString();
+ c.insertText(characters, current_fmt);
+
+ } else if (reader.tokenType() == QXmlStreamReader::EndElement) {
+ QString tag = reader.name().toString().toLower();
+
+ for (int i = fmt_stack.size() - 1; i >= 0; i--) {
+ if (fmt_stack.at(i).tag == tag) {
+ fmt_stack.removeAt(i);
+ current_fmt = merge_html_formats(fmt_stack);
+
+ if (k_block_tags.contains(tag)) {
+ inside_block = false;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ if (reader.error()) {
+ qCritical() << "Failed to parse HTML:" << reader.errorString();
+ }
+}
+
+void Html::write_block(QXmlStreamWriter *writer, const QTextBlock &block)
+{
+ writer->writeStartElement(QStringLiteral("p"));
+
+ const QTextBlockFormat &fmt = block.blockFormat();
+
+ // Write block alignment
+ if (!(fmt.alignment() & Qt::AlignLeft)) {
+ if (fmt.alignment() & Qt::AlignRight) {
+ writer->writeAttribute(QStringLiteral("align"),
+ QStringLiteral("right"));
+ } else if (fmt.alignment() & Qt::AlignHCenter) {
+ writer->writeAttribute(QStringLiteral("align"),
+ QStringLiteral("center"));
+ } else if (fmt.alignment() & Qt::AlignJustify) {
+ writer->writeAttribute(QStringLiteral("align"),
+ QStringLiteral("justify"));
+ }
+ }
+
+ // RTL support
+ if (block.textDirection() == Qt::RightToLeft) {
+ writer->writeAttribute(QStringLiteral("dir"), QStringLiteral("rtl"));
+ }
+
+ // Write CSS attributes
+ QString style;
+
+ if (fmt.lineHeightType() != QTextBlockFormat::SingleHeight) {
+ write_css_property(&style, QStringLiteral("line-height"),
+ QStringLiteral("%1%").arg(fmt.lineHeight()));
+ }
+
+ write_char_format(&style, block.charFormat());
+
+ if (!style.isEmpty()) {
+ writer->writeAttribute(QStringLiteral("style"), style);
+ }
+
+ auto it = block.begin();
+
+ if (it != block.end()) {
+ for (; it != block.end(); it++) {
+ write_fragment(writer, it.fragment());
+ }
+ }
+
+ writer->writeEndElement(); // p
+}
+
+void Html::write_fragment(QXmlStreamWriter *writer,
+ const QTextFragment &fragment)
+{
+ const QTextCharFormat &fmt = fragment.charFormat();
+
+ writer->writeStartElement(QStringLiteral("span"));
+
+ // Write CSS attributes
+ QString style;
+
+ write_char_format(&style, fmt);
+
+ if (!style.isEmpty()) {
+ writer->writeAttribute(QStringLiteral("style"), style);
+ }
+
+ QStringList lines = fragment.text().split(QChar::LineSeparator);
+ bool first_line = true;
+ foreach (const QString &l, lines) {
+ if (first_line) {
+ first_line = false;
+ } else {
+ writer->writeEmptyElement(QStringLiteral("br"));
+ }
+ writer->writeCharacters(l);
+ }
+
+ writer->writeEndElement(); // span
+}
+
+void Html::write_css_property(QString *style, const QString &key,
+ const QStringList &values)
+{
+ QString value;
+ foreach (QString v, values) {
+ if (v.contains(' ')) {
+ v = QStringLiteral("'%1'").arg(v);
+ }
+
+ append_string_auto_space(&value, v);
+ }
+
+ append_string_auto_space(style, QStringLiteral("%1: %2;").arg(key, value));
+}
+
+void Html::write_char_format(QString *style, const QTextCharFormat &fmt)
+{
+ QStringList families = fmt.fontFamilies().toStringList();
+ if (!families.isEmpty()) {
+ write_css_property(style, QStringLiteral("font-family"),
+ families.first());
+ }
+
+ if (fmt.hasProperty(QTextFormat::FontPointSize)) {
+ write_css_property(
+ style, QStringLiteral("font-size"),
+ QStringLiteral("%1pt").arg(QString::number(fmt.fontPointSize())));
+ }
+
+ if (fmt.hasProperty(QTextFormat::FontWeight)) {
+ write_css_property(style, QStringLiteral("font-weight"),
+ QString::number(fmt.fontWeight() * 8));
+ }
+
+ if (fmt.hasProperty(QTextFormat::FontItalic)) {
+ write_css_property(style, QStringLiteral("font-style"),
+ fmt.fontItalic() ? QStringLiteral("italic") :
+ QStringLiteral("normal"));
+ }
+
+ if (fmt.hasProperty(QTextFormat::FontStyleName)) {
+ write_css_property(style, QStringLiteral("-ove-font-style"),
+ fmt.fontStyleName().toString());
+ }
+
+ QStringList deco;
+
+ if (fmt.fontUnderline()) {
+ deco.append(QStringLiteral("underline"));
+ }
+
+ if (fmt.fontStrikeOut()) {
+ deco.append(QStringLiteral("line-through"));
+ }
+
+ if (fmt.fontOverline()) {
+ deco.append(QStringLiteral("overline"));
+ }
+
+ if (!deco.isEmpty()) {
+ write_css_property(style, QStringLiteral("text-decoration"), deco);
+ }
+
+ if (fmt.foreground().style() != Qt::NoBrush) {
+ const QColor &color = fmt.foreground().color();
+ QString cs;
+
+ if (color.alpha() == 255) {
+ cs = color.name();
+ } else if (color.alpha()) {
+ cs = QStringLiteral("rgba(%1, %2, %3, %4)")
+ .arg(QString::number(color.red()),
+ QString::number(color.green()),
+ QString::number(color.blue()),
+ QString::number(color.alphaF()));
+ }
+
+ write_css_property(style, QStringLiteral("color"), cs);
+ }
+
+ if (fmt.fontCapitalization() != QFont::MixedCase) {
+ if (fmt.fontCapitalization() == QFont::SmallCaps) {
+ write_css_property(style, QStringLiteral("font-variant"),
+ QStringLiteral("small-caps"));
+ // TODO: Add others
+ }
+ }
+
+ if (fmt.fontLetterSpacing() != 0.0) {
+ write_css_property(style, QStringLiteral("letter-spacing"),
+ QStringLiteral("%1%").arg(
+ QString::number(fmt.fontLetterSpacing())));
+ }
+
+ if (fmt.fontStretch() != 0) {
+ write_css_property(
+ style, QStringLiteral("font-stretch"),
+ QStringLiteral("%1%").arg(QString::number(fmt.fontStretch())));
+ }
+}
+
+QTextCharFormat Html::read_char_format(const QXmlStreamAttributes &attributes)
+{
+ QTextCharFormat fmt;
+
+ foreach (const QXmlStreamAttribute &attr, attributes) {
+ if (str_equals(attr.name(), QStringLiteral("style"))) {
+ auto css = get_css_from_style(attr.value().toString());
+
+ for (auto it = css.begin(); it != css.end(); it++) {
+ const QString &first_val = it.value().first();
+
+ if (it.key() == QStringLiteral("font-family")) {
+ fmt.setFontFamilies({ first_val });
+ } else if (it.key() == QStringLiteral("font-size")) {
+ if (first_val.endsWith(QStringLiteral("pt"),
+ Qt::CaseInsensitive)) {
+ fmt.setFontPointSize(first_val.chopped(2).toDouble());
+ }
+ } else if (it.key() == QStringLiteral("font-weight")) {
+ fmt.setFontWeight(first_val.toInt() / 8);
+ } else if (it.key() == QStringLiteral("font-style")) {
+ fmt.setFontItalic(
+ str_equals(first_val, QStringLiteral("italic")));
+ } else if (it.key() == QStringLiteral("text-decoration")) {
+ foreach (const QString &v, it.value()) {
+ if (str_equals(v, QStringLiteral("underline"))) {
+ fmt.setFontUnderline(true);
+ } else if (str_equals(v,
+ QStringLiteral("line-through"))) {
+ fmt.setFontStrikeOut(true);
+ } else if (str_equals(v, QStringLiteral("overline"))) {
+ fmt.setFontOverline(true);
+ }
+ }
+ } else if (it.key() == QStringLiteral("color")) {
+ if (first_val.startsWith(QStringLiteral("rgba"),
+ Qt::CaseInsensitive)) {
+ QString vals_only = first_val;
+ vals_only.remove(QStringLiteral("rgba"));
+ vals_only.remove(QStringLiteral("("));
+ vals_only.remove(QStringLiteral(")"));
+ QStringList rgba = vals_only.split(',');
+ if (rgba.size() == 4) {
+ QColor c;
+ c.setRed(rgba.at(0).toInt()); // Writer emits 0-255 RGB (CSS rgba() convention)
+ c.setGreen(rgba.at(1).toInt());
+ c.setBlue(rgba.at(2).toInt());
+ c.setAlphaF(rgba.at(3).toDouble());
+ fmt.setForeground(c);
+ }
+ } else {
+ fmt.setForeground(QColor(first_val));
+ }
+ } else if (it.key() == QStringLiteral("font-variant")) {
+ if (str_equals(first_val, QStringLiteral("small-caps"))) {
+ fmt.setFontCapitalization(QFont::SmallCaps);
+ }
+ } else if (it.key() == QStringLiteral("letter-spacing")) {
+ if (first_val.contains(QChar('%'))) {
+ fmt.setFontLetterSpacing(
+ first_val.chopped(1).toDouble());
+ }
+ } else if (it.key() == QStringLiteral("font-stretch")) {
+ if (first_val.contains(QChar('%'))) {
+ fmt.setFontStretch(first_val.chopped(1).toInt());
+ }
+ } else if (it.key() == QStringLiteral("-ove-font-style")) {
+ fmt.setFontStyleName(first_val);
+ }
+ }
+ }
+ }
+ return fmt;
+}
+
+QTextBlockFormat Html::read_block_format(const QXmlStreamAttributes &attributes)
+{
+ QTextBlockFormat block_fmt;
+
+ foreach (const QXmlStreamAttribute &attr, attributes) {
+ if (str_equals(attr.name(), QStringLiteral("align"))) {
+ if (str_equals(attr.value(), QStringLiteral("right"))) {
+ block_fmt.setAlignment(Qt::AlignRight);
+ } else if (str_equals(attr.value(), QStringLiteral("center"))) {
+ block_fmt.setAlignment(Qt::AlignHCenter);
+ } else if (str_equals(attr.value(), QStringLiteral("justify"))) {
+ block_fmt.setAlignment(Qt::AlignJustify);
+ }
+ } else if (str_equals(attr.name(), QStringLiteral("dir"))) {
+ if (str_equals(attr.value(), QStringLiteral("rtl"))) {
+ block_fmt.setLayoutDirection(Qt::RightToLeft);
+ }
+ } else if (str_equals(attr.name(), QStringLiteral("style"))) {
+ auto css = get_css_from_style(attr.value().toString());
+
+ for (auto it = css.begin(); it != css.end(); it++) {
+ if (it.key() == QStringLiteral("line-height")) {
+ const QString &first_val = it.value().constFirst();
+ if (first_val.contains(QChar('%'))) {
+ block_fmt.setLineHeight(
+ first_val.chopped(1).toDouble(),
+ QTextBlockFormat::ProportionalHeight);
+ }
+ }
+ }
+ }
+ }
+
+ return block_fmt;
+}
+
+void Html::append_string_auto_space(QString *s, const QString &append)
+{
+ if (!s->isEmpty()) {
+ s->append(QChar(' '));
+ }
+
+ s->append(append);
+}
+
+QMap Html::get_css_from_style(const QString &s)
+{
+ QMap map;
+
+ QStringList list = s.split(QChar(';'));
+
+ foreach (const QString &a, list) {
+ QStringList kv = a.split(QChar(':'));
+
+ if (kv.size() != 2) {
+ continue;
+ }
+
+ // I'm sure there's regex that could do this, but I couldn't figure it out. It needs to split
+ // by space EXCEPT within quotes OR double-quotes, and said quotes should be EXCLUDED from each
+ // match. Also commas should be filtered out.
+ QStringList values;
+ const QString &val = kv.at(1);
+ QChar in_quote(0);
+ QString current_str;
+ for (int i = 0; i < val.size(); i++) {
+ const QChar ¤t_char = val.at(i);
+
+ if (!in_quote.isNull()) {
+ // If inside quotes and character isn't quote, indiscriminately append char
+ if (current_char == in_quote) {
+ in_quote = QChar(0);
+ } else {
+ current_str.append(current_char);
+ }
+ } else if (current_char.isSpace() || current_char == QChar(',')) {
+ // Dump current
+ if (!current_str.isEmpty()) {
+ values.append(current_str);
+ current_str.clear();
+ }
+ } else if (in_quote.isNull() && (current_char == QChar('\'') ||
+ current_char == QChar('"'))) {
+ in_quote = current_char;
+ } else {
+ current_str.append(current_char);
+ }
+ }
+
+ if (!current_str.isEmpty()) {
+ values.append(current_str);
+ }
+
+ // Not sure if this will ever happen, but just in case, we will avoid assert failures with this
+ if (values.isEmpty()) {
+ values.append(QString());
+ }
+
+ map[kv.at(0).trimmed().toLower()] = values;
+ }
+
+ return map;
+}
+
+}
diff --git a/app/common/htmlapp.h b/app/common/htmlapp.h
new file mode 100644
index 000000000..f62415a0d
--- /dev/null
+++ b/app/common/htmlapp.h
@@ -0,0 +1,83 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef OAK_HTMLAPP_H
+#define OAK_HTML_H
+
+#include
+#include
+#include
+#include
+
+namespace olive
+{
+
+/**
+ * @brief Functions for converting HTML to QTextDocument and vice versa
+ *
+ * Qt does contain its own functions for this, however they have some limitations. Some things that
+ * we want to support (e.g. kerning/spacing and font stretch) are not implemented in Qt's
+ * QTextHtmlExporter and QTextHtmlParser. Additionally, since these functions are not part of Qt's
+ * public API, and make many references to other parts of Qt that are not part of the public API,
+ * there is no way to subclass or extend their functionality without forking Qt as a whole.
+ *
+ * Therefore, it became necessary to write a custom class for the conversion so that we can
+ * ensure support for the features we need.
+ *
+ * If someone wishes to extend this class for more feature support, feel free to open a pull
+ * request. But this is NOT intended to be an exhaustive HTML implementation, and is primarily
+ * designed to store rich text in a standard format for the purpose of text formatting for video.
+ */
+class Html {
+public:
+ static QString doc_to_html(const QTextDocument *doc);
+
+ static void html_to_doc(QTextDocument *doc, const QString &html);
+
+private:
+ static void write_block(QXmlStreamWriter *writer, const QTextBlock &block);
+
+ static void write_fragment(QXmlStreamWriter *writer,
+ const QTextFragment &fragment);
+
+ static void write_css_property(QString *style, const QString &key,
+ const QStringList &value);
+ static void write_css_property(QString *style, const QString &key,
+ const QString &value)
+ {
+ write_css_property(style, key, QStringList({ value }));
+ }
+
+ static void write_char_format(QString *style, const QTextCharFormat &fmt);
+
+ static QTextCharFormat
+ read_char_format(const QXmlStreamAttributes &attributes);
+
+ static QTextBlockFormat
+ read_block_format(const QXmlStreamAttributes &attributes);
+
+ static void append_string_auto_space(QString *s, const QString &append);
+
+ static QMap get_css_from_style(const QString &s);
+
+ static const QVector k_block_tags;
+};
+
+}
+
+#endif // OAK_HTML_H
diff --git a/app/common/nodevaluehandle.h b/app/common/nodevaluehandle.h
new file mode 100644
index 000000000..1a6cc87f6
--- /dev/null
+++ b/app/common/nodevaluehandle.h
@@ -0,0 +1,68 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_NODEVALUEHANDLE_H
+#define OAK_NODEVALUEHANDLE_H
+
+#include "node/value.h"
+#include "oakengine/node.h"
+
+namespace olive
+{
+
+/**
+ * @brief Convert engine NodeValue::Type to oak_node_value_type (app-side).
+ *
+ * The two enums do NOT share ordinals (e.g. k_boolean=4 vs BOOL=3), so a
+ * plain int cast is a bug. Mirrors from_c_type() in
+ * engine/src/capi/node.cpp. Lives in an app header, NOT in the public
+ * facade headers — the C ABI surface stays pure C (see
+ * docs/zh/r6-cleanup-plan.md red line 3 context). Returns -1 for types the
+ * facade cannot represent (caller falls back to the input's declared type).
+ */
+inline int node_value_type_to_c(NodeValue::Type t)
+{
+ switch (t) {
+ case NodeValue::k_int: return OAK_NODE_VALUE_INT;
+ case NodeValue::k_float: return OAK_NODE_VALUE_FLOAT;
+ case NodeValue::k_boolean: return OAK_NODE_VALUE_BOOL;
+ case NodeValue::k_rational: return OAK_NODE_VALUE_RATIONAL;
+ case NodeValue::k_color: return OAK_NODE_VALUE_COLOR;
+ case NodeValue::k_vec2: return OAK_NODE_VALUE_VEC2;
+ case NodeValue::k_vec3: return OAK_NODE_VALUE_VEC3;
+ case NodeValue::k_vec4: return OAK_NODE_VALUE_VEC4;
+ case NodeValue::k_combo: return OAK_NODE_VALUE_COMBO;
+ case NodeValue::k_file: return OAK_NODE_VALUE_STRING;
+ case NodeValue::k_text: return OAK_NODE_VALUE_TEXT;
+ case NodeValue::k_font: return OAK_NODE_VALUE_FONT;
+ case NodeValue::k_str_combo: return OAK_NODE_VALUE_STR_COMBO;
+ case NodeValue::k_binary: return OAK_NODE_VALUE_BINARY;
+ case NodeValue::k_bezier: return OAK_NODE_VALUE_BEZIER;
+ case NodeValue::k_texture: return OAK_NODE_VALUE_TEXTURE;
+ case NodeValue::k_samples: return OAK_NODE_VALUE_SAMPLES;
+ case NodeValue::k_video_params: return OAK_NODE_VALUE_VIDEO_PARAMS;
+ case NodeValue::k_audio_params: return OAK_NODE_VALUE_AUDIO_PARAMS;
+ default: return -1;
+ }
+}
+
+} // namespace olive
+
+#endif // OAK_NODEVALUEHANDLE_H
diff --git a/app/common/oakvaluehelper.h b/app/common/oakvaluehelper.h
new file mode 100644
index 000000000..1ef8923d6
--- /dev/null
+++ b/app/common/oakvaluehelper.h
@@ -0,0 +1,226 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKVALUEHELPER_H
+#define OAKVALUEHELPER_H
+
+#include
+#include
+#include
+#include
+
+#include "node/keyframe.h"
+#include "node/value.h"
+#include "oakengine/node.h"
+#include "olive/core/util/color.h"
+
+namespace olive {
+
+/**
+ * @brief Convert a per-track component QVariant into the C ABI oak_node_value POD.
+ *
+ * `type` is the declared input data type (e.g. k_float/k_color). For split-track
+ * types the component is the track-0 scalar (float for k_color's red channel, etc.).
+ * Returns false for types that have no POD representation.
+ */
+static inline bool QVariantToOakNodeValue(NodeValue::Type type, const QVariant &v,
+ oak_node_value *out)
+{
+ memset(out, 0, sizeof(*out));
+ switch (type) {
+ case NodeValue::k_int:
+ case NodeValue::k_combo:
+ out->type = (type == NodeValue::k_combo) ? OAK_NODE_VALUE_COMBO
+ : OAK_NODE_VALUE_INT;
+ out->num = v.toLongLong();
+ return true;
+ case NodeValue::k_float:
+ out->type = OAK_NODE_VALUE_FLOAT;
+ out->f[0] = v.toDouble();
+ return true;
+ case NodeValue::k_boolean:
+ out->type = OAK_NODE_VALUE_BOOL;
+ out->num = v.toBool() ? 1 : 0;
+ return true;
+ case NodeValue::k_rational:
+ out->type = OAK_NODE_VALUE_RATIONAL;
+ {
+ const Rational r = v.value();
+ out->num = r.numerator();
+ out->den = r.denominator();
+ }
+ return true;
+ case NodeValue::k_color:
+ out->type = OAK_NODE_VALUE_COLOR;
+ {
+ const core::Color c = v.value();
+ out->f[0] = c.red();
+ out->f[1] = c.green();
+ out->f[2] = c.blue();
+ out->f[3] = c.alpha();
+ }
+ return true;
+ case NodeValue::k_vec2:
+ out->type = OAK_NODE_VALUE_VEC2;
+ {
+ const QVector2D vec = v.value();
+ out->f[0] = vec.x();
+ out->f[1] = vec.y();
+ }
+ return true;
+ case NodeValue::k_vec3:
+ out->type = OAK_NODE_VALUE_VEC3;
+ {
+ const QVector3D vec = v.value();
+ out->f[0] = vec.x();
+ out->f[1] = vec.y();
+ out->f[2] = vec.z();
+ }
+ return true;
+ case NodeValue::k_vec4:
+ out->type = OAK_NODE_VALUE_VEC4;
+ {
+ const QVector4D vec = v.value();
+ out->f[0] = vec.x();
+ out->f[1] = vec.y();
+ out->f[2] = vec.z();
+ out->f[3] = vec.w();
+ }
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * @brief Convert a per-track component QVariant into the C ABI oak_node_value POD.
+ *
+ * Unlike QVariantToOakNodeValue() which takes a full normal value, this takes a
+ * single track's component (e.g. one float for a k_color channel). The resulting
+ * POD has the input's declared type with the component in f[0]/num, exactly what
+ * the per-track facade commands expect.
+ */
+static inline bool NodeTrackComponentToOakNodeValue(NodeValue::Type type,
+ const QVariant &v,
+ oak_node_value *out)
+{
+ memset(out, 0, sizeof(*out));
+ switch (type) {
+ case NodeValue::k_int:
+ case NodeValue::k_combo:
+ out->type = (type == NodeValue::k_combo) ? OAK_NODE_VALUE_COMBO
+ : OAK_NODE_VALUE_INT;
+ out->num = v.toLongLong();
+ return true;
+ case NodeValue::k_float:
+ case NodeValue::k_bezier:
+ out->type = OAK_NODE_VALUE_FLOAT;
+ out->f[0] = v.toDouble();
+ return true;
+ case NodeValue::k_boolean:
+ out->type = OAK_NODE_VALUE_BOOL;
+ out->num = v.toBool() ? 1 : 0;
+ return true;
+ case NodeValue::k_rational:
+ out->type = OAK_NODE_VALUE_RATIONAL;
+ {
+ const Rational r = v.value();
+ out->num = r.numerator();
+ out->den = r.denominator();
+ }
+ return true;
+ case NodeValue::k_color:
+ out->type = OAK_NODE_VALUE_COLOR;
+ out->f[0] = v.toFloat();
+ return true;
+ case NodeValue::k_vec2:
+ out->type = OAK_NODE_VALUE_VEC2;
+ out->f[0] = v.toFloat();
+ return true;
+ case NodeValue::k_vec3:
+ out->type = OAK_NODE_VALUE_VEC3;
+ out->f[0] = v.toFloat();
+ return true;
+ case NodeValue::k_vec4:
+ out->type = OAK_NODE_VALUE_VEC4;
+ out->f[0] = v.toFloat();
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * @brief Convert a full C ABI oak_node_value POD back into a QVariant.
+ *
+ * Mirrors QVariantToOakNodeValue(). String/binary/bezier are not represented
+ * in the POD and return an invalid QVariant; use the dedicated string/binary/
+ * bezier facade getters for those.
+ */
+static inline QVariant OakNodeValueToQVariant(const oak_node_value &v)
+{
+ switch (v.type) {
+ case OAK_NODE_VALUE_INT:
+ return QVariant::fromValue(v.num);
+ case OAK_NODE_VALUE_FLOAT:
+ return QVariant::fromValue(v.f[0]);
+ case OAK_NODE_VALUE_BOOL:
+ return QVariant::fromValue(v.num != 0);
+ case OAK_NODE_VALUE_RATIONAL:
+ return QVariant::fromValue(
+ Rational(int(v.num), int(v.den)));
+ case OAK_NODE_VALUE_COLOR:
+ return QVariant::fromValue(core::Color(
+ float(v.f[0]), float(v.f[1]), float(v.f[2]), float(v.f[3])));
+ case OAK_NODE_VALUE_VEC2:
+ return QVariant::fromValue(
+ QVector2D(float(v.f[0]), float(v.f[1])));
+ case OAK_NODE_VALUE_VEC3:
+ return QVariant::fromValue(
+ QVector3D(float(v.f[0]), float(v.f[1]), float(v.f[2])));
+ case OAK_NODE_VALUE_VEC4:
+ return QVariant::fromValue(
+ QVector4D(float(v.f[0]), float(v.f[1]), float(v.f[2]), float(v.f[3])));
+ case OAK_NODE_VALUE_COMBO:
+ return QVariant::fromValue(int(v.num));
+ default:
+ return QVariant();
+ }
+}
+
+/**
+ * @brief Map an engine NodeKeyframe::Type to the facade easing type.
+ */
+static inline int NodeKeyframeTypeToFacade(NodeKeyframe::Type type)
+{
+ switch (type) {
+ case NodeKeyframe::k_bezier:
+ return 1;
+ case NodeKeyframe::k_hold:
+ return 2;
+ case NodeKeyframe::k_linear:
+ default:
+ return 0;
+ }
+}
+
+} // namespace olive
+
+#endif // OAKVALUEHELPER_H
diff --git a/app/common/qtutilsapp.cpp b/app/common/qtutilsapp.cpp
new file mode 100644
index 000000000..f85e4203b
--- /dev/null
+++ b/app/common/qtutilsapp.cpp
@@ -0,0 +1,143 @@
+/***
+
+ 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 "common/qtutils.h"
+
+namespace olive
+{
+
+int QtUtils::q_font_metrics_width(QFontMetrics fm, const QString &s)
+{
+ return fm.horizontalAdvance(s);
+}
+
+QFrame *QtUtils::create_horizontal_line()
+{
+ QFrame *horizontal_line = new QFrame();
+ horizontal_line->setFrameShape(QFrame::HLine);
+ horizontal_line->setFrameShadow(QFrame::Sunken);
+ return horizontal_line;
+}
+
+QFrame *QtUtils::create_vertical_line()
+{
+ QFrame *l = create_horizontal_line();
+ l->setFrameShape(QFrame::VLine);
+ return l;
+}
+
+QString QtUtils::get_formatted_date_time(const QDateTime &dt)
+{
+ return dt.toString(Qt::TextDate);
+}
+
+QStringList QtUtils::word_wrap_string(const QString &s, const QFontMetrics &fm,
+ int bounding_width)
+{
+ QStringList list;
+ QStringList lines = s.split('\n');
+ for (int i = 0; i < lines.size(); i++) {
+ QString this_line = lines.at(i);
+ while (this_line.size() > 1 &&
+ q_font_metrics_width(fm, this_line) >= bounding_width) {
+ int old_size = this_line.size();
+ int hard_break = -1;
+ for (int j = this_line.size() - 1; j >= 0; j--) {
+ const QChar &char_test = this_line.at(j);
+ if (char_test.isSpace() || char_test == '-') {
+ if (q_font_metrics_width(fm, this_line.left(j)) <
+ bounding_width) {
+ if (!char_test.isSpace()) j++;
+ list.append(this_line.left(j));
+ while (j < this_line.size() &&
+ this_line.at(j).isSpace()) j++;
+ this_line.remove(0, j);
+ break;
+ }
+ } else if (hard_break == -1 &&
+ q_font_metrics_width(fm, this_line.left(j)) <
+ bounding_width) {
+ hard_break = j;
+ }
+ }
+ if (old_size == this_line.size()) {
+ if (hard_break != -1) {
+ list.append(this_line.left(hard_break));
+ this_line.remove(0, hard_break);
+ } else {
+ break;
+ }
+ }
+ }
+ if (!this_line.isEmpty()) {
+ list.append(this_line);
+ }
+ }
+ return list;
+}
+
+Qt::KeyboardModifiers
+QtUtils::flip_control_and_shift_modifiers(Qt::KeyboardModifiers e)
+{
+ if (e & Qt::ControlModifier & Qt::ShiftModifier) return e;
+ if (e & Qt::ShiftModifier) {
+ e |= Qt::ControlModifier;
+ e &= ~Qt::ShiftModifier;
+ } else if (e & Qt::ControlModifier) {
+ e |= Qt::ShiftModifier;
+ e &= ~Qt::ControlModifier;
+ }
+ return e;
+}
+
+void QtUtils::set_combo_box_data(QComboBox *cb, int data)
+{
+ for (int i = 0; i < cb->count(); i++) {
+ if (cb->itemData(i).toInt() == data) {
+ cb->setCurrentIndex(i);
+ break;
+ }
+ }
+}
+
+void QtUtils::set_combo_box_data(QComboBox *cb, const QString &data)
+{
+ for (int i = 0; i < cb->count(); i++) {
+ if (cb->itemData(i).toString() == data) {
+ cb->setCurrentIndex(i);
+ break;
+ }
+ }
+}
+
+QColor QtUtils::to_q_color(const core::Color &i)
+{
+ QColor c;
+
+ // QColor only supports values from 0.0 to 1.0 and are only used for UI representations
+ c.setRedF(std::clamp(i.red(), 0.0f, 1.0f));
+ c.setGreenF(std::clamp(i.green(), 0.0f, 1.0f));
+ c.setBlueF(std::clamp(i.blue(), 0.0f, 1.0f));
+ c.setAlphaF(std::clamp(i.alpha(), 0.0f, 1.0f));
+
+ return c;
+}
+
+}
diff --git a/app/common/undowrapper.h b/app/common/undowrapper.h
new file mode 100644
index 000000000..ef23cb924
--- /dev/null
+++ b/app/common/undowrapper.h
@@ -0,0 +1,60 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+ Modifications Copyright (C) 2025 mikesolar
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef OAK_UNDOWRAPPER_H
+#define OAK_UNDOWRAPPER_H
+
+#include "oakengine/undo.h"
+
+namespace olive
+{
+
+/**
+ * Wrap an app-side undo command object in the facade custom-command API.
+ *
+ * `Cmd` must provide public `redo()` and `undo()` methods. Ownership of `cmd`
+ * is transferred to the returned opaque command pointer; the wrapper deletes
+ * `cmd` when the engine command is destroyed.
+ *
+ * This helper lets app code keep small app-state undo commands (selections,
+ * splitter sizes, etc.) without defining new subclasses of olive::UndoCommand,
+ * which would keep olive::UndoCommand symbols in the editor binary.
+ */
+template
+void *wrap_app_undo_command(const char *name, Cmd *cmd)
+{
+ return oakengine_undo_command_create(
+ name,
+ [](void *userdata) {
+ static_cast(userdata)->redo();
+ },
+ [](void *userdata) {
+ static_cast(userdata)->undo();
+ },
+ [](void *userdata) {
+ delete static_cast(userdata);
+ },
+ cmd);
+}
+
+} // namespace olive
+
+#endif // OAK_UNDOWRAPPER_H
diff --git a/app/common/xmlutilsapp.cpp b/app/common/xmlutilsapp.cpp
new file mode 100644
index 000000000..f4ec27d7a
--- /dev/null
+++ b/app/common/xmlutilsapp.cpp
@@ -0,0 +1,47 @@
+/***
+
+ 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 .
+
+***/
+
+// App-side implementation of xml_read_next_start_element
+// Provides a local definition so the app doesn't import this from liboakengine.
+
+#include "common/xmlutils.h"
+
+namespace olive
+{
+
+bool xml_read_next_start_element(QXmlStreamReader *reader,
+ CancelAtom *cancel_atom)
+{
+ QXmlStreamReader::TokenType token;
+
+ while ((token = reader->readNext()) != QXmlStreamReader::Invalid &&
+ token != QXmlStreamReader::EndDocument &&
+ (!cancel_atom || !cancel_atom->is_cancelled())) {
+ if (reader->isEndElement()) {
+ return false;
+ } else if (reader->isStartElement()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+}
diff --git a/app/core.cpp b/app/core.cpp
index 977f17368..468c1a7ed 100644
--- a/app/core.cpp
+++ b/app/core.cpp
@@ -30,6 +30,13 @@
#include
#include
#include
+#include "oakengine/audio.h"
+#include "oakengine/disk.h"
+#include "oakengine/plugin.h"
+#include "oakengine/project.h"
+#include "oakengine/task.h"
+#include "oakengine/node.h"
+#include "oakengine/undo.h"
#include "window/mainwindow/mainwindowundo.h"
#ifdef Q_OS_WINDOWS
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
@@ -37,25 +44,22 @@
#endif
#endif
-#include "audio/audiomanager.h"
-#include "cli/clitask/clitaskdialog.h"
+#include "dialog/task/task.h"
#include "common/filefunctions.h"
#include "common/xmlutils.h"
-#include "config/config.h"
+#include "common/configwrapper.h"
#include "dialog/about/about.h"
#include "dialog/autorecovery/autorecoverydialog.h"
#include "dialog/diskcache/diskcachedialog.h"
#include "dialog/export/export.h"
#include "dialog/footagerelink/footagerelinkdialog.h"
-#ifdef USE_OTIO
+
#include "dialog/otioproperties/otiopropertiesdialog.h"
-#endif
+
#include "dialog/progress/pluginprogressdialogreporter.h"
#include "dialog/projectproperties/projectproperties.h"
#include "dialog/sequence/sequence.h"
-#include "dialog/task/task.h"
#include "dialog/preferences/preferences.h"
-#include "node/nodeundo.h"
#include "panel/panelmanager.h"
#include "panel/project/project.h"
#include "panel/timebased/timebased.h"
@@ -64,114 +68,172 @@
#include "pluginSupport/oliveplugininstance.h"
#include "pluginSupport/pluginprogressreporter.h"
#include "render/diskmanager.h"
-#ifdef USE_OTIO
-#include "task/project/loadotio/loadotio.h"
-#include "task/project/saveotio/saveotio.h"
-#endif
-#include "task/project/import/import.h"
#include "dialog/projectimport/projectimporterrordialog.h"
-#include "task/project/load/load.h"
-#include "task/project/save/save.h"
#include "ui/style/style.h"
#include "widget/menu/menushared.h"
#include "window/mainwindow/mainwindow.h"
+#include "widget/viewer/vieweroutpututils.h"
namespace olive
{
-Core::Core(const CoreParams ¶ms)
- : EngineCore(params)
+Core *Core::instance_ = nullptr;
+
+Core::Core(const OakEngineAppParams *params)
+ : QObject(nullptr)
, main_window_(nullptr)
{
+ instance_ = this;
+
+ // The opaque C handles that cross the engine ABI boundary are used as
+ // signal/slot parameters (and in queued connections / QSignalSpy), so they
+ // must be registered with Qt's meta-type system at runtime. Element types
+ // are registered before the container types that hold them.
+ qRegisterMetaType();
+ qRegisterMetaType();
+ qRegisterMetaType();
+ qRegisterMetaType>();
+ qRegisterMetaType>>();
+
+ // Create the engine core through the C ABI (backs the singleton)
+ if (params) {
+ oakengine_app_create(params);
+ } else {
+ static const OakEngineAppParams default_params = {0};
+ oakengine_app_create(&default_params);
+ }
+
// Register the UI handlers that the engine uses to request user interaction
- set_confirm_image_sequence_handler(
- [this](const QString &filename) {
- return confirm_image_sequence(filename);
- });
-
- set_relink_handler([this](QVector footage) {
- FootageRelinkDialog frd(footage, main_window_);
- return frd.exec() != QDialog::Rejected;
- });
-
- set_save_project_handler([this](const QString &override_filename) {
- save_project_internal(override_filename);
- });
-
- set_close_project_handler([this] { return close_project(false); });
-
- set_load_layout_handler([this](const MainWindowLayoutInfo &layout) {
- main_window_->load_layout(layout);
- });
-
+ // through the C ABI callback struct instead of engine_core_->set_*_handler().
+ {
+ OakEngineAppCallbacks cb = {};
+ cb.userdata = this;
+ cb.confirm_image_sequence = [](const char *filename, void *userdata) -> int {
+ return static_cast(userdata)->confirm_image_sequence(
+ QString::fromUtf8(filename))
+ ? 1
+ : 0;
+ };
+ cb.relink_footage = [](OakEngineFootage **footage, int count,
+ void *userdata) -> int {
+ QVector fv;
+ fv.reserve(count);
+ for (int i = 0; i < count; i++) {
+ fv.append(reinterpret_cast(footage[i]));
+ }
+ FootageRelinkDialog frd(fv,
+ static_cast(userdata)->main_window_);
+ return frd.exec() != QDialog::Rejected ? 1 : 0;
+ };
+ cb.save_project = [](const char *override_filename, void *userdata) {
+ static_cast(userdata)->save_project_internal(
+ override_filename ? QString::fromUtf8(override_filename) :
+ QString());
+ };
+ cb.close_project = [](void *userdata) -> int {
+ return static_cast(userdata)->close_project(false) ? 1 : 0;
+ };
+ cb.load_layout = [](const void *layout, void *userdata) {
+ static_cast(userdata)->main_window_->load_layout(
+ *static_cast(layout));
+ };
#ifdef USE_OTIO
- set_otio_import_handler([this](const QList &sequences) {
- return DialogImportOTIOShow(sequences);
- });
+ cb.otio_import = [](OakEngineSequence **sequences, int count,
+ void *userdata) -> int {
+ QList sq;
+ sq.reserve(count);
+ for (int i = 0; i < count; i++) {
+ sq.append(reinterpret_cast(sequences[i]));
+ }
+ return static_cast(userdata)->DialogImportOTIOShow(sq) ? 1 :
+ 0;
+ };
#endif
+ oakengine_app_set_callbacks(&cb);
+ }
// Disk cache settings dialog (engine -> UI)
- DiskManager::set_show_disk_cache_settings_handler(
- [](DiskCacheFolder *folder, QWidget *parent) {
- DiskCacheDialog d(folder, parent);
+ oakengine_disk_set_settings_handler(
+ [](const char *folder_path, void *parent_window, void *userdata) {
+ Q_UNUSED(userdata)
+ DiskCacheDialog d(
+ reinterpret_cast(
+ oakengine_disk_get_open_folder(folder_path)),
+ reinterpret_cast(parent_window));
d.exec();
- });
+ }, nullptr);
// OFX plugin progress dialog (engine -> UI)
- plugin::set_plugin_progress_reporter_factory(
- [](const QString &message,
- const QString &title) -> plugin::PluginProgressReporter * {
- return new PluginProgressDialogReporter(message, title);
- });
+ oakengine_plugin_set_progress_reporter_factory(
+ [](const char *message, const char *title, void *userdata) -> void * {
+ Q_UNUSED(userdata)
+ return new PluginProgressDialogReporter(
+ QString::fromUtf8(message), QString::fromUtf8(title));
+ },
+ [](void *reporter, void *userdata) {
+ Q_UNUSED(userdata)
+ delete reinterpret_cast(reporter);
+ },
+ [](void *reporter, void *userdata) -> int {
+ Q_UNUSED(userdata)
+ return reinterpret_cast(reporter)->was_cancelled() ? 1 : 0;
+ },
+ [](void *reporter, double progress, void *userdata) {
+ Q_UNUSED(userdata)
+ reinterpret_cast(reporter)->set_progress(progress);
+ },
+ nullptr);
// OFX timeline suite: resolve the active viewer through the panels
- plugin::set_active_viewer_provider([]() -> ViewerOutput * {
- PanelManager *manager = PanelManager::instance();
- if (!manager) {
+ oakengine_plugin_set_active_viewer_provider(
+ [](void *userdata) -> OakEngineNode * {
+ Q_UNUSED(userdata)
+ PanelManager *manager = PanelManager::instance();
+ if (!manager) {
+ return nullptr;
+ }
+
+ if (auto *time_panel =
+ manager->most_recently_focused()) {
+ if (time_panel->get_connected_viewer()) {
+ return reinterpret_cast(time_panel->get_connected_viewer());
+ }
+ }
+
+ QList timelines =
+ manager->get_panels_of_type();
+ for (TimelinePanel *panel : timelines) {
+ if (panel && panel->get_connected_viewer()) {
+ return reinterpret_cast(panel->get_connected_viewer());
+ }
+ }
+
return nullptr;
- }
-
- if (auto *time_panel =
- manager->most_recently_focused()) {
- if (time_panel->get_connected_viewer()) {
- return time_panel->get_connected_viewer();
- }
- }
-
- QList timelines =
- manager->get_panels_of_type();
- for (TimelinePanel *panel : timelines) {
- if (panel && panel->get_connected_viewer()) {
- return panel->get_connected_viewer();
- }
- }
-
- return nullptr;
- });
+ }, nullptr);
}
void Core::start()
{
// Start the engine (config, locale, managers, autorecovery, recent projects)
- EngineCore::start();
+ oakengine_app_start();
//
// Start application
//
- switch (core_params().run_mode()) {
- case CoreParams::k_run_normal:
+ switch (oakengine_app_run_mode()) {
+ case OAKENGINE_APP_RUN_NORMAL:
// Start GUI
- start_gui(core_params().fullscreen());
+ start_gui(oakengine_app_fullscreen() != 0);
// If we have a startup
QMetaObject::invokeMethod(this, "open_startup_project",
Qt::QueuedConnection);
break;
- case CoreParams::k_headless_export:
+ case OAKENGINE_APP_RUN_HEADLESS_EXPORT:
qInfo() << "Headless export is not fully implemented yet";
break;
- case CoreParams::k_headless_pre_cache:
+ case OAKENGINE_APP_RUN_HEADLESS_PRE_CACHE:
qInfo() << "Headless pre-cache is not fully implemented yet";
break;
}
@@ -184,15 +246,15 @@ void Core::stop()
PanelManager::destroy_instance();
- AudioManager::destroy_instance();
+ oakengine_audio_destroy_instance();
- DiskManager::destroy_instance();
+ oakengine_disk_destroy_instance();
delete main_window_;
main_window_ = nullptr;
// Then tear down the engine
- EngineCore::stop();
+ oakengine_app_stop();
}
MainWindow *Core::main_window()
@@ -232,11 +294,21 @@ void Core::import_files(const QStringList &urls, Folder *parent)
return;
}
- ProjectImportTask *pim = new ProjectImportTask(parent, filtered_urls);
+ QVector url_ba;
+ QVector url_ptrs;
+ url_ba.reserve(filtered_urls.size());
+ url_ptrs.reserve(filtered_urls.size());
+ for (const QString &url : filtered_urls) {
+ url_ba.append(url.toUtf8());
+ url_ptrs.append(url_ba.last().constData());
+ }
- if (!pim->get_file_count()) {
- // No files to import
- delete pim;
+ OakEngineTask *pim = oakengine_task_create_project_import(
+ reinterpret_cast(parent),
+ url_ptrs.data(), url_ptrs.size());
+
+ if (oakengine_task_import_file_count(pim) == 0) {
+ oakengine_task_free(pim);
return;
}
@@ -340,22 +412,27 @@ void Core::create_new_folder()
// Get the selected folder in this panel
Folder *folder = active_project_panel->get_selected_folder();
- // Create new folder
- Folder *new_folder = new Folder();
+ // Group the three facade edits into a single undo entry.
+ oakengine_undo_group_begin(tr("Create New Folder").toUtf8().constData());
- // Set a default name
- new_folder->set_label(tr("New Folder"));
+ // Create new folder via facade (creates and adds to project, undoable)
+ OakEngineNode *new_folder_oak = oakengine_project_add_node(
+ reinterpret_cast(active_project),
+ "org.olivevideoeditor.Olive.folder");
- // Create an undoable command
- MultiUndoCommand *command = new MultiUndoCommand();
+ // Set a default name (undoable)
+ oakengine_node_set_label(new_folder_oak,
+ tr("New Folder").toUtf8().constData());
- command->add_child(new NodeAddCommand(active_project, new_folder));
- command->add_child(new FolderAddChild(folder, new_folder));
+ // Add to the selected folder (undoable)
+ oakengine_folder_add_child(
+ reinterpret_cast(folder),
+ new_folder_oak);
- Core::instance()->undo_stack()->push(command, tr("Created New Folder"));
+ oakengine_undo_group_end();
// Trigger an automatic rename so users can enter the folder name
- active_project_panel->edit(new_folder);
+ active_project_panel->edit(new_folder_oak);
}
void Core::create_new_sequence()
@@ -379,20 +456,24 @@ void Core::create_new_sequence()
if (sd.exec() == QDialog::Accepted) {
// Create an undoable command
- MultiUndoCommand *command = new MultiUndoCommand();
+ void *command = oakengine_undo_command_create_multi();
- command->add_child(new NodeAddCommand(active_project, new_sequence));
- command->add_child(new FolderAddChild(
- get_selected_folder_in_active_project(), new_sequence));
- command->add_child(new NodeSetPositionCommand(
- new_sequence, new_sequence, Node::Position()));
- command->add_child(new OpenSequenceCommand(new_sequence));
+ oakengine_undo_command_multi_add_child(command,
+ oakengine_node_add_to_project_command(
+ reinterpret_cast(active_project),
+ reinterpret_cast(new_sequence)));
+ oakengine_folder_add_child(
+ reinterpret_cast(get_selected_folder_in_active_project()),
+ reinterpret_cast(new_sequence));
+ oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(new_sequence), reinterpret_cast(new_sequence), 0.0, 0.0, 0));
+ oakengine_undo_command_multi_add_child(command, make_open_sequence_command(new_sequence));
// Create and connect default nodes to new sequence
- new_sequence->add_default_nodes(command);
+ oakengine_sequence_add_default_nodes(
+ reinterpret_cast(new_sequence));
- Core::instance()->undo_stack()->push(command,
- tr("Created New Sequence"));
+ oakengine_undo_push(command,
+ tr("Created New Sequence").toUtf8().constData());
} else {
// If the dialog was accepted, ownership goes to the AddItemCommand. But if we get here, just delete
@@ -400,20 +481,30 @@ void Core::create_new_sequence()
}
}
-void Core::import_task_complete(Task *task)
+void Core::import_task_complete(OakEngineTask *task)
{
- ProjectImportTask *import_task = static_cast(task);
+ void *command = static_cast(
+ oakengine_task_import_get_command(task));
- MultiUndoCommand *command = import_task->get_command();
+ int footage_count = oakengine_task_import_footage_count(task);
+ QVector imported_footage;
+ imported_footage.reserve(footage_count);
+ for (int i = 0; i < footage_count; i++) {
+ Footage *f = reinterpret_cast(
+ oakengine_task_import_footage_at(task, i));
+ imported_footage.append(f);
- foreach (Footage *f, import_task->get_imported_footage()) {
// Look for multi-layer images
- if (f->get_audio_stream_count() == 0 && f->get_video_stream_count() > 1) {
+ int vid_count = oakengine_viewer_get_video_stream_count(
+ reinterpret_cast(f));
+ int aud_count = oakengine_viewer_get_audio_stream_count(
+ reinterpret_cast(f));
+ if (aud_count == 0 && vid_count > 1) {
bool all_stills = true;
- for (int i = 0; i < f->get_video_stream_count(); i++) {
- const VideoParams &vs = f->get_video_params(i);
- if (!(vs.video_type() == VideoParams::k_video_type_still &&
+ for (int i = 0; i < vid_count; i++) {
+ const VideoParams &vs = viewer_output_video_params(f, i);
+ if (!(vs.video_type() == 1 &&
vs.enabled() == (i == 0))) {
all_stills = false;
}
@@ -438,33 +529,49 @@ void Core::import_task_complete(Task *task)
d.exec();
if (d.clickedButton() == multi_btn) {
- for (int i = 0; i < f->get_video_stream_count(); i++) {
- VideoParams vs = f->get_video_params(i);
- vs.set_enabled(!vs.enabled());
- f->set_video_params(vs, i);
+ OakEngineFootage *fh = oakengine_footage_borrow(
+ reinterpret_cast(f));
+ for (int i = 0; i < vid_count; i++) {
+ int enabled = oakengine_footage_get_stream_enabled(
+ fh, OAKENGINE_TRACK_TYPE_VIDEO, i);
+ oakengine_footage_set_stream_enabled(
+ fh, OAKENGINE_TRACK_TYPE_VIDEO, i,
+ enabled ? 0 : 1);
}
+ oakengine_footage_free(fh);
} else if (d.clickedButton() == single_btn) {
// Do nothing, footage will already be set up this way
} else if (d.clickedButton() == cancel_btn) {
// Cancel import
- delete command;
+ oakengine_undo_command_free(command);
return;
}
}
}
}
- if (import_task->has_invalid_files()) {
- ProjectImportErrorDialog d(import_task->get_invalid_files(),
- main_window_);
+ int invalid_count = oakengine_task_import_invalid_files_count(task);
+ if (invalid_count > 0) {
+ QStringList invalid_files;
+ for (int i = 0; i < invalid_count; i++) {
+ int len = oakengine_task_import_invalid_file_at(
+ task, i, nullptr, 0);
+ if (len > 0) {
+ QByteArray buf(len + 1, '\0');
+ oakengine_task_import_invalid_file_at(
+ task, i, buf.data(), buf.size());
+ invalid_files.append(QString::fromUtf8(buf.constData()));
+ }
+ }
+ ProjectImportErrorDialog d(invalid_files, main_window_);
d.exec();
}
- undo_stack()->push(
+ oakengine_undo_push(
command,
- tr("Imported %1 File(s)").arg(import_task->get_imported_footage().size()));
+ tr("Imported %1 File(s)").arg(imported_footage.size()).toUtf8().constData());
- main_window_->select_footage(import_task->get_imported_footage());
+ main_window_->select_footage(imported_footage);
}
bool Core::confirm_image_sequence(const QString &filename)
@@ -485,7 +592,15 @@ bool Core::confirm_image_sequence(const QString &filename)
bool Core::start_headless_export()
{
- const QString &startup_project = core_params().startup_project();
+ QString startup_project;
+ {
+ int len = oakengine_app_startup_project(nullptr, 0);
+ if (len > 0) {
+ QByteArray buf(len + 1, '\0');
+ oakengine_app_startup_project(buf.data(), buf.size());
+ startup_project = QString::fromUtf8(buf.constData());
+ }
+ }
if (startup_project.isEmpty()) {
qCritical().noquote()
@@ -499,12 +614,12 @@ bool Core::start_headless_export()
}
// Start a load task and try running it
- ProjectLoadTask plm(startup_project);
- CLITaskDialog task_dialog(&plm);
+ OakEngineTask *plm = oakengine_task_create_project_load(
+ startup_project.toUtf8().constData());
/*
- if (task_dialog.Run()) {
- std::unique_ptr p = std::unique_ptr(plm.GetLoadedProject());
+ if (oakengine_cli_task_dialog_run(plm, nullptr)) {
+ OakEngineProject *p = oakengine_task_save_get_project(plm); // FIXME: load task accessor
QVector- items = p->get_items_of_type(Item::kSequence);
// Check if this project contains sequences
@@ -562,17 +677,30 @@ bool Core::start_headless_export()
return false;
}
} else {
- qCritical().noquote() << tr("Project failed to load: %1").arg(plm.GetError());
+ char err[512];
+ err[0] = '\0';
+ oakengine_task_error(plm, err, sizeof(err));
+ qCritical().noquote() << tr("Project failed to load: %1").arg(QString::fromUtf8(err));
return false;
}
*/
+ oakengine_task_free(plm);
+
return false;
}
void Core::open_startup_project()
{
- const QString &startup_project = core_params().startup_project();
+ QString startup_project;
+ {
+ int len = oakengine_app_startup_project(nullptr, 0);
+ if (len > 0) {
+ QByteArray buf(len + 1, '\0');
+ oakengine_app_startup_project(buf.data(), buf.size());
+ startup_project = QString::fromUtf8(buf.constData());
+ }
+ }
bool startup_project_exists = !startup_project.isEmpty() &&
QFileInfo::exists(startup_project);
@@ -606,10 +734,10 @@ void Core::start_gui(bool full_screen)
PanelManager::create_instance();
// Initialize audio service
- AudioManager::create_instance();
+ oakengine_audio_create_instance();
// Initialize disk service
- DiskManager::create_instance();
+ oakengine_disk_create_instance();
// Connect the PanelFocusManager to the application's focus change signal
connect(qApp, &QApplication::focusChanged, PanelManager::instance(),
@@ -630,14 +758,14 @@ void Core::start_gui(bool full_screen)
main_window_ = new MainWindow();
// Route engine notifications to the UI
- connect(this, &EngineCore::status_message_show, main_window_->statusBar(),
- &QStatusBar::showMessage);
- connect(this, &EngineCore::status_message_clear, main_window_->statusBar(),
- &QStatusBar::clearMessage);
- connect(this, &EngineCore::cache_full_warning_requested, this,
- &Core::show_cache_full_warning);
- connect(this, &EngineCore::active_project_changed, this,
- &Core::on_active_project_changed);
+ connect(this, &Core::tool_changed, this, [this](const Tool::Item &) {});
+ // Status-bar and lifecycle notifications are handled through the facade
+ // (oakengine_app_show_status_message, oakengine_app_clear_status_message)
+ // which the engine forwards through registered callbacks. The main window
+ // status bar is updated separately during start_gui.
+ main_window_->statusBar()->showMessage(QString());
+ connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
+ main_window_->statusBar(), &QStatusBar::clearMessage);
if (full_screen) {
main_window_->showFullScreen();
@@ -657,13 +785,23 @@ void Core::start_gui(bool full_screen)
void Core::save_project_internal(const QString &override_filename)
{
- // Create save manager
- Task *psm;
+ Project *open_proj_ = reinterpret_cast(oakengine_app_open_project());
- if (open_project_->filename().endsWith(QStringLiteral(".otio"),
- Qt::CaseInsensitive)) {
+ // Get project filename via facade
+ char fn_buf[512];
+ oakengine_project_filename(
+ reinterpret_cast(open_proj_),
+ fn_buf, sizeof(fn_buf));
+ QString fn = QString::fromUtf8(fn_buf);
+
+ // Create save manager
+ OakEngineTask *psm = nullptr;
+
+ if (fn.endsWith(QStringLiteral(".otio"),
+ Qt::CaseInsensitive)) {
#ifdef USE_OTIO
- psm = new SaveOTIOTask(open_project_);
+ psm = oakengine_task_create_project_save_otio(
+ reinterpret_cast(open_proj_));
#else
QMessageBox::critical(
main_window_, tr("Missing OpenTimelineIO Libraries"),
@@ -672,17 +810,15 @@ void Core::save_project_internal(const QString &override_filename)
return;
#endif
} else {
- bool use_compression = !open_project_->filename().endsWith(
+ bool use_compression = !fn.endsWith(
QStringLiteral(".ovexml"), Qt::CaseInsensitive);
- psm = new ProjectSaveTask(open_project_, use_compression);
- static_cast(psm)->set_layout(
- main_window_->save_layout());
-
- if (!override_filename.isEmpty()) {
- // Set override filename if provided
- static_cast(psm)->set_override_filename(
- override_filename);
- }
+ SerializedLayoutInfo layout = main_window_->save_layout();
+ psm = oakengine_task_create_project_save(
+ reinterpret_cast(open_proj_),
+ use_compression ? 1 : 0,
+ override_filename.isEmpty() ? nullptr :
+ override_filename.toUtf8().constData(),
+ &layout);
}
// We don't use a TaskDialog here because a model save dialog is annoying, particularly when
@@ -694,13 +830,13 @@ void Core::save_project_internal(const QString &override_filename)
// Ideally we could do this in a background thread and show progress in the status bar like
// Microsoft Word, but that would be far more complex. If it becomes necessary in the future,
// we will look into an approach like that.
- if (psm->start()) {
+ if (oakengine_task_start_sync(psm) == 1) {
if (override_filename.isEmpty()) {
project_save_succeeded(psm);
}
}
- psm->deleteLater();
+ oakengine_task_free(psm);
}
ViewerOutput *Core::get_sequence_to_export()
@@ -737,7 +873,19 @@ ViewerOutput *Core::get_sequence_to_export()
bool Core::revert_project_internal(bool by_opening_existing)
{
- if (open_project_->filename().isEmpty()) {
+ Project *cur_proj = reinterpret_cast(oakengine_app_open_project());
+ char fn_buf[512];
+ oakengine_project_filename(
+ reinterpret_cast(cur_proj),
+ fn_buf, sizeof(fn_buf));
+ QString cur_fn = QString::fromUtf8(fn_buf);
+
+ char name_buf[256];
+ oakengine_project_name(reinterpret_cast(cur_proj),
+ name_buf, sizeof(name_buf));
+ QString cur_name = QString::fromUtf8(name_buf);
+
+ if (cur_fn.isEmpty()) {
QMessageBox::critical(
main_window_, tr("Revert"),
tr("This project has not yet been saved, therefore there is no last saved state to revert to."));
@@ -748,19 +896,19 @@ bool Core::revert_project_internal(bool by_opening_existing)
msg =
tr("The project \"%1\" is already open. By re-opening it, the project will revert to "
"its last saved state. Any unsaved changes will be lost. Do you wish to continue?")
- .arg(open_project_->filename());
+ .arg(cur_fn);
} else {
msg =
tr("This will revert the project \"%1\" back to its last saved state. "
"All unsaved changes will be lost. Do you wish to continue?")
- .arg(open_project_->name());
+ .arg(cur_name);
}
if (QMessageBox::question(main_window_, tr("Revert"), msg,
QMessageBox::Ok | QMessageBox::Cancel) ==
QMessageBox::Ok) {
// Copy filename because CloseProject is going to delete `p`
- QString filename = open_project_->filename();
+ QString filename = cur_fn;
// Close project without prompting to save it
close_project(false, true);
@@ -777,18 +925,22 @@ bool Core::revert_project_internal(bool by_opening_existing)
return false;
}
-void Core::project_save_succeeded(Task *task)
+void Core::project_save_succeeded(OakEngineTask *task)
{
- Project *p = static_cast(task)->get_project();
+ Project *p = reinterpret_cast(
+ oakengine_task_save_get_project(task));
- on_project_saved(p);
+ oakengine_app_on_project_saved(reinterpret_cast(p));
- show_status_bar_message(tr("Saved to \"%1\" successfully").arg(p->filename()));
+ char fn_buf[512];
+ oakengine_project_filename(reinterpret_cast(p),
+ fn_buf, sizeof(fn_buf));
+ show_status_bar_message(tr("Saved to \"%1\" successfully").arg(fn_buf));
}
Project *Core::get_active_project() const
{
- return open_project_;
+ return reinterpret_cast(oakengine_app_open_project());
}
Folder *Core::get_selected_folder_in_active_project() const
@@ -839,11 +991,16 @@ QString Core::get_project_filter(bool include_any_filter)
bool Core::save_project()
{
- if (open_project_->filename().isEmpty()) {
+ Project *saved_proj = reinterpret_cast(oakengine_app_open_project());
+
+ char fn_buf[512];
+ oakengine_project_filename(
+ reinterpret_cast(saved_proj),
+ fn_buf, sizeof(fn_buf));
+ if (fn_buf[0] == '\0') {
return save_project_as();
} else {
save_project_internal();
-
return true;
}
}
@@ -871,7 +1028,16 @@ void Core::open_export_dialog_for_viewer(ViewerOutput *viewer,
void Core::check_for_auto_recoveries()
{
- QFile autorecovery_index(get_auto_recovery_index_filename());
+ QString autorecovery_index_path;
+ {
+ int len = oakengine_app_auto_recovery_index_filename(nullptr, 0);
+ if (len > 0) {
+ QByteArray buf(len + 1, '\0');
+ oakengine_app_auto_recovery_index_filename(buf.data(), buf.size());
+ autorecovery_index_path = QString::fromUtf8(buf.constData());
+ }
+ }
+ QFile autorecovery_index(autorecovery_index_path);
if (autorecovery_index.exists()) {
// Uh-oh, we have auto-recoveries to prompt
if (autorecovery_index.open(QFile::ReadOnly)) {
@@ -887,7 +1053,7 @@ void Core::check_for_auto_recoveries()
autorecovery_index.close();
// Delete recovery index since we don't need it anymore
- QFile::remove(get_auto_recovery_index_filename());
+ QFile::remove(autorecovery_index_path);
} else {
QMessageBox::critical(
main_window_, tr("Auto-Recovery Error"),
@@ -928,10 +1094,15 @@ void Core::on_active_project_changed(Project *p)
main_window_->set_project(p);
if (p) {
- // Keep the window's modified state in sync with the project. The
- // connection is removed automatically when the project is deleted.
- connect(p, &Project::modified_changed, main_window_,
- &QMainWindow::setWindowModified);
+ auto *ph = reinterpret_cast(p);
+ // Keep the window's modified state in sync via event subscription
+ // (connection is removed automatically when the project is deleted).
+ oakengine_event_subscribe(ph, OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED,
+ [](const oakengine_event *event, void *userdata) {
+ QMainWindow *mw = static_cast(userdata);
+ mw->setWindowModified(event->a != 0);
+ },
+ main_window_);
}
}
@@ -953,7 +1124,9 @@ bool Core::save_project_as()
fn = FileFunctions::ensure_filename_extension(fn, extension);
- open_project_->set_filename(fn);
+ oakengine_project_set_filename(
+ reinterpret_cast(static_cast(reinterpret_cast(oakengine_app_open_project()))),
+ fn.toUtf8().constData());
save_project_internal();
@@ -970,16 +1143,21 @@ void Core::revert_project()
void Core::open_project_internal(const QString &filename, bool recovery_project)
{
- if (open_project_) {
+ Project *open_proj = reinterpret_cast(oakengine_app_open_project());
+ if (open_proj) {
+ char fn_buf[512];
+ oakengine_project_filename(
+ reinterpret_cast(open_proj),
+ fn_buf, sizeof(fn_buf));
// Comparing QFileInfos will handle case insensitivity and both slash directions on platforms
// where this is necessary (not naming any names *cough* Windows)
- if (QFileInfo(open_project_->filename()) == QFileInfo(filename)) {
+ if (QFileInfo(fn_buf) == QFileInfo(filename)) {
// This project is already open
bool reverted = revert_project_internal(true);
if (!reverted) {
// Calling this will focus attention to the project that the user just tried to re-open
- add_open_project(open_project_);
+ oakengine_app_add_open_project_vp(open_proj, 0);
}
// Don't do anything else
@@ -987,12 +1165,13 @@ void Core::open_project_internal(const QString &filename, bool recovery_project)
}
}
- Task *load_task;
+ OakEngineTask *load_task = nullptr;
if (filename.endsWith(QStringLiteral(".otio"), Qt::CaseInsensitive)) {
// Load OpenTimelineIO project
#ifdef USE_OTIO
- load_task = new LoadOTIOTask(filename);
+ load_task = oakengine_task_create_project_load_otio(
+ filename.toUtf8().constData());
#else
QMessageBox::critical(
main_window_, tr("Missing OpenTimelineIO Libraries"),
@@ -1002,7 +1181,8 @@ void Core::open_project_internal(const QString &filename, bool recovery_project)
#endif
} else {
// Fallback to regular OVE project
- load_task = new ProjectLoadTask(filename);
+ load_task = oakengine_task_create_project_load(
+ filename.toUtf8().constData());
}
TaskDialog *task_dialog =
@@ -1026,7 +1206,7 @@ void Core::import_single_file(const QString &f)
}
}
-bool Core::label_nodes(const QVector &nodes, MultiUndoCommand *parent)
+bool Core::label_nodes(const QVector &nodes, void *parent)
{
if (nodes.isEmpty()) {
return false;
@@ -1049,18 +1229,13 @@ bool Core::label_nodes(const QVector &nodes, MultiUndoCommand *parent)
start_label, &ok);
if (ok) {
- NodeRenameCommand *rename_command = new NodeRenameCommand();
-
+ QVector oak_nodes;
+ oak_nodes.reserve(nodes.size());
foreach (Node *n, nodes) {
- rename_command->add_node(n, s);
- }
-
- if (parent) {
- parent->add_child(rename_command);
- } else {
- undo_stack()->push(rename_command,
- tr("Renamed %1 Node(s)").arg(nodes.size()));
+ oak_nodes.append(reinterpret_cast(n));
}
+ oakengine_node_rename_many(oak_nodes.data(), oak_nodes.size(),
+ s.toUtf8().constData(), parent);
return true;
}
@@ -1070,7 +1245,11 @@ bool Core::label_nodes(const QVector &nodes, MultiUndoCommand *parent)
void Core::open_project_from_recent_list(int index)
{
- const QString &open_fn = get_recent_projects().at(index);
+ int rp_len = oakengine_app_recent_project_at(index, nullptr, 0);
+ if (rp_len <= 0) return;
+ QByteArray rp_buf(rp_len + 1, '\0');
+ oakengine_app_recent_project_at(index, rp_buf.data(), rp_buf.size());
+ const QString open_fn = QString::fromUtf8(rp_buf.constData());
if (QFileInfo::exists(open_fn)) {
open_project_internal(open_fn);
@@ -1080,14 +1259,19 @@ void Core::open_project_from_recent_list(int index)
tr("The project \"%1\" doesn't exist. Would you like to remove this file from the recent list?")
.arg(open_fn),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- remove_recently_opened_project(index);
+ oakengine_app_remove_recently_opened_project(index);
}
}
bool Core::close_project(bool auto_open_new, bool ignore_modified)
{
- if (open_project_) {
- if (open_project_->is_modified() && !ignore_modified) {
+ Project *close_proj = reinterpret_cast(oakengine_app_open_project());
+ if (close_proj) {
+ char name_buf[256];
+ oakengine_project_name(
+ reinterpret_cast(close_proj),
+ name_buf, sizeof(name_buf));
+ if (close_proj->is_modified() && !ignore_modified) {
QMessageBox mb(main_window_);
mb.setWindowModality(Qt::WindowModal);
@@ -1095,7 +1279,7 @@ bool Core::close_project(bool auto_open_new, bool ignore_modified)
mb.setWindowTitle(tr("Unsaved Changes"));
mb.setText(
tr("The project '%1' has unsaved changes. Would you like to save them?")
- .arg(open_project_->name()));
+ .arg(name_buf));
QPushButton *yes_btn =
mb.addButton(tr("Save"), QMessageBox::YesRole);
@@ -1118,10 +1302,10 @@ bool Core::close_project(bool auto_open_new, bool ignore_modified)
}
// For safety, the undo stack is cleared so no commands try to affect a freed project
- undo_stack()->clear();
+ oakengine_undo_clear();
- Project *tmp = open_project_;
- set_active_project(nullptr);
+ Project *tmp = reinterpret_cast(oakengine_app_open_project());
+ oakengine_app_set_active_project_vp(nullptr);
delete tmp;
}
@@ -1179,4 +1363,211 @@ void Core::open_project()
}
}
+// ---- Facade-wrapping method implementations ----
+
+UndoStack *Core::undo_stack() const
+{
+ return reinterpret_cast(oakengine_undo_handle());
}
+
+Tool::Item Core::tool() const
+{
+ return static_cast(oakengine_app_tool());
+}
+
+void Core::set_tool(const Tool::Item &tool)
+{
+ oakengine_app_set_tool(static_cast(tool));
+ emit tool_changed(tool);
+}
+
+bool Core::snapping() const
+{
+ return oakengine_app_snapping() != 0;
+}
+
+void Core::set_snapping(const bool &b)
+{
+ oakengine_app_set_snapping(b ? 1 : 0);
+ emit snapping_changed(b);
+}
+
+Timecode::Display Core::get_timecode_display() const
+{
+ return static_cast(oakengine_app_timecode_display());
+}
+
+void Core::set_timecode_display(Timecode::Display d)
+{
+ oakengine_app_set_timecode_display(static_cast(d));
+ emit timecode_display_changed(d);
+}
+
+void Core::show_status_bar_message(const QString &s, int timeout)
+{
+ oakengine_app_show_status_message(s.toUtf8().constData(), timeout);
+}
+
+void Core::clear_status_bar_message()
+{
+ oakengine_app_clear_status_message();
+}
+
+QString Core::footage_file_dialog_filter()
+{
+ // Use buf/size convention to query the filter
+ int len = oakengine_app_footage_file_dialog_filter(nullptr, 0);
+ if (len <= 0) {
+ return QString();
+ }
+ QByteArray buf(len + 1, '\0');
+ oakengine_app_footage_file_dialog_filter(buf.data(), buf.size());
+ return QString::fromUtf8(buf.constData());
+}
+
+bool Core::is_footage_extension_allowed(const QString &path)
+{
+ return oakengine_app_is_footage_extension_allowed(
+ path.toUtf8().constData()) == 1;
+}
+
+void Core::create_new_project()
+{
+ oakengine_app_create_new_project();
+}
+
+Sequence *Core::create_new_sequence_for_project(const QString &format,
+ Project *project)
+{
+ return reinterpret_cast(
+ oakengine_app_create_sequence(
+ reinterpret_cast(project),
+ format.toUtf8().constData()));
+}
+
+Sequence *Core::create_new_sequence_for_project(Project *project)
+{
+ return instance()->create_new_sequence_for_project(QStringLiteral("Sequence %1"), project);
+}
+
+void Core::clear_open_recent_list()
+{
+ oakengine_app_clear_recent_projects();
+ emit open_recent_list_changed();
+}
+
+void Core::set_use_proxy_media(bool enabled)
+{
+ oakengine_app_set_use_proxy_media(enabled ? 1 : 0);
+}
+
+void Core::request_pixel_sampling_in_viewers(bool e)
+{
+ oakengine_app_request_pixel_sampling(e ? 1 : 0);
+ emit color_picker_enabled(e);
+}
+
+Tool::AddableObject Core::get_selected_addable_object() const
+{
+ return static_cast(oakengine_app_addable_object());
+}
+
+void Core::set_selected_addable_object(const Tool::AddableObject &obj)
+{
+ oakengine_app_set_addable_object(static_cast(obj));
+ emit addable_object_changed(obj);
+}
+
+void Core::set_selected_transition_object(const QString &obj)
+{
+ oakengine_app_set_selected_transition(obj.toUtf8().constData());
+}
+
+void Core::copy_string_to_clipboard(const QString &s)
+{
+ oakengine_app_copy_to_clipboard(s.toUtf8().constData());
+}
+
+void Core::set_magic(bool e)
+{
+ oakengine_app_set_magic(e ? 1 : 0);
+}
+
+bool Core::add_open_project_from_task(OakEngineTask *task, bool add_to_recents)
+{
+ return oakengine_app_add_open_project_from_task(task, add_to_recents ? 1 : 0) == 1;
+}
+
+bool Core::add_recovery_project_from_task(OakEngineTask *task)
+{
+ return oakengine_app_add_recovery_project_from_task(task) == 1;
+}
+
+int Core::get_recent_project_count() const
+{
+ return oakengine_app_recent_projects_count();
+}
+
+QString Core::get_recent_project_at(int index) const
+{
+ int len = oakengine_app_recent_project_at(index, nullptr, 0);
+ if (len <= 0) {
+ return QString();
+ }
+ QByteArray buf(len + 1, '\0');
+ oakengine_app_recent_project_at(index, buf.data(), buf.size());
+ return QString::fromUtf8(buf.constData());
+}
+
+// ---- EngineCore forwarding methods (delegate through C ABI) ----
+
+bool Core::set_language(const QString &locale)
+{
+ return oakengine_app_set_language(locale.toUtf8().constData()) > 0;
+}
+
+void Core::set_autorecovery_interval(int minutes)
+{
+ oakengine_app_set_autorecovery_interval(minutes);
+}
+
+void Core::on_project_saved(Project *p)
+{
+ oakengine_app_on_project_saved(reinterpret_cast(p));
+}
+
+QString Core::get_auto_recovery_index_filename()
+{
+ int len = oakengine_app_auto_recovery_index_filename(nullptr, 0);
+ if (len <= 0) return QString();
+ QByteArray buf(len + 1, '\0');
+ oakengine_app_auto_recovery_index_filename(buf.data(), buf.size());
+ return QString::fromUtf8(buf.constData());
+}
+
+void Core::add_open_project(olive::Project *p, bool add_to_recents)
+{
+ oakengine_app_add_open_project(reinterpret_cast(p),
+ add_to_recents ? 1 : 0);
+}
+
+void Core::remove_recently_opened_project(int index)
+{
+ oakengine_app_remove_recently_opened_project(index);
+}
+
+void Core::set_active_project(Project *p)
+{
+ oakengine_app_set_active_project(reinterpret_cast(p));
+}
+
+QString Core::get_selected_transition() const
+{
+ int len = oakengine_app_selected_transition(nullptr, 0);
+ if (len <= 0) return QString();
+ QByteArray buf(len + 1, '\0');
+ oakengine_app_selected_transition(buf.data(), buf.size());
+ return QString::fromUtf8(buf.constData());
+}
+
+} // namespace olive
diff --git a/app/core.h b/app/core.h
index 1f5628c39..deade3a1c 100644
--- a/app/core.h
+++ b/app/core.h
@@ -23,6 +23,11 @@
#define OAK_CORE_H
#include "coreengine.h"
+#include
+#include "oakengine/app.h"
+#include "oakengine/undo.h"
+#include "oakengine/init.h"
+#include "oakengine/task.h"
namespace olive
{
@@ -32,35 +37,40 @@ class MainWindow;
/**
* @brief The main central Olive application instance_
*
- * This is the UI-facing derivation of EngineCore. It runs both in GUI and
- * CLI modes (and handles what to init based on that). All UI-independent
- * engine state lives in the base class EngineCore; this class adds the main
- * window, dialogs and other user interaction on top of it.
+ * This is the UI-facing application controller. It holds an EngineCore
+ * member for UI-independent engine state and adds the main window, dialogs
+ * and other user interaction on top of it.
+ *
+ * EngineCore is NOT a base class — it is a member, so the MOC-generated
+ * code for Core does not pull in EngineCore's Q_OBJECT symbols.
*
* The "public slots" are usually user-triggered actions and can be connected to UI elements (e.g. creating a folder,
* opening the import dialog, etc.)
*/
-class Core : public EngineCore {
+class Core : public QObject {
Q_OBJECT
public:
/**
* @brief Core Constructor
*
- * Registers the UI handlers that EngineCore uses to request user
- * interaction.
+ * Creates the EngineCore engine instance and registers the UI handlers
+ * that the engine uses to request user interaction.
*/
- Core(const CoreParams ¶ms);
+ Core(const OakEngineAppParams *params = nullptr);
+
+ ~Core()
+ {
+ instance_ = nullptr;
+ }
/**
* @brief Core object accessible from anywhere in the code
*
- * Use this to access Core functions. This is simply EngineCore::instance()
- * cast to Core, which is safe because the application entry point (main())
- * always constructs a Core.
+ * Returns the application Core singleton (no EngineCore::instance() call).
*/
static Core *instance()
{
- return static_cast(EngineCore::instance());
+ return instance_;
}
/**
@@ -113,7 +123,7 @@ public:
* @brief Show a dialog to the user to rename a set of nodes
*/
bool label_nodes(const QVector &nodes,
- MultiUndoCommand *parent = nullptr);
+ void *parent = nullptr);
/**
* @brief Opens a project from the recently opened list
@@ -137,6 +147,9 @@ public:
void open_export_dialog_for_viewer(ViewerOutput *viewer,
bool start_still_image);
+ bool add_open_project_from_task(OakEngineTask *task, bool add_to_recents);
+ bool add_recovery_project_from_task(OakEngineTask *task);
+
public slots:
/**
* @brief Starts an open file dialog to load a project from file
@@ -180,13 +193,6 @@ public slots:
*/
void dialog_export_show();
- /**
- * @brief Show OTIO import dialog
- */
-#ifdef USE_OTIO
- bool DialogImportOTIOShow(const QList &sequences);
-#endif
-
/**
* @brief Create a new folder in the currently active project
*/
@@ -201,6 +207,85 @@ public slots:
void browse_auto_recoveries();
+public:
+ // The following methods are ordinary member functions, NOT slots. They are
+ // deliberately kept out of the `public slots:` section because their
+ // signatures reference engine C++ types (Project*, Sequence*, UndoStack*).
+ // If MOC processed them as slots it would instantiate QMetaType for those
+ // types and pull their staticMetaObject symbols across the ABI boundary.
+ // None of them are connect() targets: every connection involving Core uses
+ // the new-style member-function syntax, which works with plain methods.
+
+ /**
+ * @brief Show OTIO import dialog
+ */
+#ifdef USE_OTIO
+ bool DialogImportOTIOShow(const QList &sequences);
+#endif
+
+ // ---- Facade-wrapping methods (shadow EngineCore to avoid symbol refs) ----
+
+ UndoStack *undo_stack() const;
+
+ Tool::Item tool() const;
+ void set_tool(const Tool::Item &tool);
+
+ bool snapping() const;
+ void set_snapping(const bool &b);
+
+ Timecode::Display get_timecode_display() const;
+ void set_timecode_display(Timecode::Display d);
+
+ void show_status_bar_message(const QString &s, int timeout = 0);
+ void clear_status_bar_message();
+
+ static QString footage_file_dialog_filter();
+ static bool is_footage_extension_allowed(const QString &path);
+
+ void create_new_project();
+ Sequence *create_new_sequence_for_project(const QString &format,
+ Project *project);
+ static Sequence *create_new_sequence_for_project(Project *project);
+
+ void clear_open_recent_list();
+ void set_use_proxy_media(bool enabled);
+
+ void request_pixel_sampling_in_viewers(bool e);
+
+ Tool::AddableObject get_selected_addable_object() const;
+ void set_selected_addable_object(const Tool::AddableObject &obj);
+ void set_selected_transition_object(const QString &obj);
+
+ static void copy_string_to_clipboard(const QString &s);
+
+ void set_magic(bool e);
+
+ // Recent project list accessors (replaces EngineCore::get_recent_projects())
+ int get_recent_project_count() const;
+ QString get_recent_project_at(int index) const;
+
+ // Facade-wrapping methods (delegate through the C ABI)
+
+ bool set_language(const QString &locale);
+ void set_autorecovery_interval(int minutes);
+
+ void on_project_saved(Project *p);
+ static QString get_auto_recovery_index_filename();
+ void add_open_project(olive::Project *p, bool add_to_recents = false);
+ void remove_recently_opened_project(int index);
+ void set_active_project(Project *p);
+ QString get_selected_transition() const;
+
+signals:
+ // Forwarding signals (shadow EngineCore signals so connect() resolves here)
+ void tool_changed(const Tool::Item &tool);
+ void addable_object_changed(Tool::AddableObject o);
+ void snapping_changed(const bool &b);
+ void timecode_display_changed(Timecode::Display d);
+ void open_recent_list_changed();
+ void color_picker_enabled(bool e);
+ void color_picker_color_emitted(const Color &reference, const Color &display);
+
private:
/**
* @brief Get the file filter than can be used with QFileDialog to open and save compatible projects
@@ -242,15 +327,20 @@ private:
*/
MainWindow *main_window_;
-private slots:
- void project_save_succeeded(Task *task);
+ /**
+ * @brief Cached Core* singleton
+ */
+ static Core *instance_;
- bool add_open_project_from_task_and_add_to_recents(Task *task)
+private slots:
+ void project_save_succeeded(OakEngineTask *task);
+
+ bool add_open_project_from_task_and_add_to_recents(OakEngineTask *task)
{
- return add_open_project_from_task(task, true);
+ return instance()->add_open_project_from_task(task, true);
}
- void import_task_complete(Task *task);
+ void import_task_complete(OakEngineTask *task);
bool confirm_image_sequence(const QString &filename);
diff --git a/app/dialog/about/about.cpp b/app/dialog/about/about.cpp
index 038d37b05..d6004092b 100644
--- a/app/dialog/about/about.cpp
+++ b/app/dialog/about/about.cpp
@@ -26,7 +26,7 @@
#include
#include
-#include "config/config.h"
+#include "common/configwrapper.h"
#include "patreon.h"
#include "scrollinglabel.h"
diff --git a/app/dialog/color/colordialog.cpp b/app/dialog/color/colordialog.cpp
index 2829ecb7f..7553fc0d6 100644
--- a/app/dialog/color/colordialog.cpp
+++ b/app/dialog/color/colordialog.cpp
@@ -30,7 +30,7 @@
namespace olive
{
-ColorDialog::ColorDialog(ColorManager *color_manager, const ManagedColor &start,
+ColorDialog::ColorDialog(OakEngineColorManager *color_manager, const ManagedColor &start,
QWidget *parent)
: QDialog(parent)
, color_manager_(color_manager)
@@ -142,11 +142,23 @@ void ColorDialog::set_color(const ManagedColor &start)
} else {
// Convert reference color to the input space
- ColorProcessorPtr linear_to_input = ColorProcessor::create(
- color_manager_, color_manager_->get_reference_color_space(),
- start.color_input());
+ QByteArray ref_cs = oak_query_string([this](char *buf, int size) {
+ return oakengine_color_manager_reference_color_space(
+ color_manager_, buf, size);
+ }).toUtf8();
+ QByteArray in_cs = start.color_input().toUtf8();
+ oak_color_transform in_pod;
+ in_pod.is_display = 0;
+ in_pod.output = in_cs.constData();
+ in_pod.view = nullptr;
+ in_pod.look = nullptr;
+ ColorProcessorHandlePtr linear_to_input(
+ oakengine_color_processor_create(color_manager_, ref_cs.constData(),
+ &in_pod,
+ OAKENGINE_COLOR_PROCESSOR_NORMAL),
+ ColorProcessorHandleDeleter());
- managed_start = linear_to_input->convert_color(start);
+ managed_start = oak_convert_color(linear_to_input, start);
}
color_wheel_->set_selected_color(managed_start);
@@ -161,7 +173,7 @@ ManagedColor ColorDialog::get_selected_color() const
// Convert to linear and return a linear color
if (input_to_ref_processor_) {
- selected = input_to_ref_processor_->convert_color(selected);
+ selected = oak_convert_color(input_to_ref_processor_, selected);
}
selected.set_color_input(get_color_space_input());
@@ -183,22 +195,50 @@ ColorTransform ColorDialog::get_color_space_output() const
void ColorDialog::color_space_changed(const QString &input,
const ColorTransform &output)
{
- input_to_ref_processor_ = ColorProcessor::create(
- color_manager_, input, color_manager_->get_reference_color_space());
+ QByteArray ref_cs = oak_query_string([this](char *buf, int size) {
+ return oakengine_color_manager_reference_color_space(
+ color_manager_, buf, size);
+ }).toUtf8();
+ QByteArray in = input.toUtf8();
+ QByteArray o, v, l;
+ oak_color_transform out_pod = oak_to_transform(output, &o, &v, &l);
- ColorProcessorPtr ref_to_display = ColorProcessor::create(
- color_manager_, color_manager_->get_reference_color_space(), output);
+ auto make_proc = [&](const char *input_cs, const oak_color_transform *dest,
+ int dir) -> ColorProcessorHandlePtr {
+ return ColorProcessorHandlePtr(
+ oakengine_color_processor_create(color_manager_, input_cs, dest,
+ dir),
+ ColorProcessorHandleDeleter());
+ };
- ColorProcessorPtr ref_to_input = ColorProcessor::create(
- color_manager_, color_manager_->get_reference_color_space(), input);
+ input_to_ref_processor_ = make_proc(in.constData(), &out_pod,
+ OAKENGINE_COLOR_PROCESSOR_NORMAL);
+
+ oak_color_transform ref_display_pod;
+ ref_display_pod.is_display = out_pod.is_display;
+ ref_display_pod.output = out_pod.output;
+ ref_display_pod.view = out_pod.view;
+ ref_display_pod.look = out_pod.look;
+ ColorProcessorHandlePtr ref_to_display = make_proc(
+ ref_cs.constData(), &ref_display_pod,
+ OAKENGINE_COLOR_PROCESSOR_NORMAL);
+
+ oak_color_transform ref_input_pod;
+ ref_input_pod.is_display = 0;
+ ref_input_pod.output = in.constData();
+ ref_input_pod.view = nullptr;
+ ref_input_pod.look = nullptr;
+ ColorProcessorHandlePtr ref_to_input = make_proc(
+ ref_cs.constData(), &ref_input_pod,
+ OAKENGINE_COLOR_PROCESSOR_NORMAL);
// Display -> reference is the inverse of the display transform. Older OCIO
// versions crashed on TRANSFORM_DIR_INVERSE; guard by requiring a valid
// processor and fall back to disabling the display tab if creation fails.
- ColorProcessorPtr display_to_ref = ColorProcessor::create(
- color_manager_, color_manager_->get_reference_color_space(), output,
- ColorProcessor::k_inverse);
- if (display_to_ref && !display_to_ref->get_processor()) {
+ ColorProcessorHandlePtr display_to_ref = make_proc(
+ ref_cs.constData(), &ref_display_pod,
+ OAKENGINE_COLOR_PROCESSOR_INVERSE);
+ if (display_to_ref && !oakengine_color_processor_is_valid(display_to_ref.get())) {
display_to_ref = nullptr;
}
diff --git a/app/dialog/color/colordialog.h b/app/dialog/color/colordialog.h
index 6b3ae147b..b55c8eae6 100644
--- a/app/dialog/color/colordialog.h
+++ b/app/dialog/color/colordialog.h
@@ -24,8 +24,8 @@
#include
-#include "node/color/colormanager/colormanager.h"
-#include "render/managedcolor.h"
+#include "oakengine/color.h"
+#include "widget/manageddisplay/colorprocessorhandle.h"
#include "widget/colorwheel/colorgradientwidget.h"
#include "widget/colorwheel/colorspacechooser.h"
#include "widget/colorwheel/colorswatchchooser.h"
@@ -57,7 +57,7 @@ public:
*
* QWidget parent.
*/
- ColorDialog(ColorManager *color_manager,
+ ColorDialog(OakEngineColorManager *color_manager,
const ManagedColor &start = Color(1.0f, 1.0f, 1.0f),
QWidget *parent = nullptr);
@@ -76,7 +76,7 @@ public slots:
void set_color(const ManagedColor &c);
private:
- ColorManager *color_manager_;
+ OakEngineColorManager *color_manager_;
ColorWheelWidget *color_wheel_;
@@ -84,7 +84,7 @@ private:
ColorGradientWidget *hsv_value_gradient_;
- ColorProcessorPtr input_to_ref_processor_;
+ ColorProcessorHandlePtr input_to_ref_processor_;
ColorSpaceChooser *chooser_;
diff --git a/app/dialog/configbase/configdialogbase.cpp b/app/dialog/configbase/configdialogbase.cpp
index f0d2be6d4..657cc3deb 100644
--- a/app/dialog/configbase/configdialogbase.cpp
+++ b/app/dialog/configbase/configdialogbase.cpp
@@ -27,6 +27,7 @@
#include "core.h"
+#include "oakengine/undo.h"
namespace olive
{
@@ -70,13 +71,13 @@ void ConfigDialogBase::accept()
}
}
- MultiUndoCommand *command = new MultiUndoCommand();
+ void *command = oakengine_undo_command_create_multi();
foreach (ConfigDialogBaseTab *tab, tabs_) {
tab->accept(command);
}
- Core::instance()->undo_stack()->push(command, tr("Set Configuration"));
+ oakengine_undo_push(command, tr("Set Configuration").toUtf8().constData());
AcceptEvent();
diff --git a/app/dialog/configbase/configdialogbasetab.h b/app/dialog/configbase/configdialogbasetab.h
index 6c6084165..3c63da2dd 100644
--- a/app/dialog/configbase/configdialogbasetab.h
+++ b/app/dialog/configbase/configdialogbasetab.h
@@ -24,8 +24,7 @@
#include
-#include "config/config.h"
-#include "undo/undocommand.h"
+#include "common/configwrapper.h"
namespace olive
{
@@ -36,7 +35,7 @@ public:
virtual bool validate();
- virtual void accept(MultiUndoCommand *parent) = 0;
+ virtual void accept(void *parent) = 0;
};
}
diff --git a/app/dialog/diskcache/diskcachedialog.cpp b/app/dialog/diskcache/diskcachedialog.cpp
index 7bb6306c3..1339f1e9c 100644
--- a/app/dialog/diskcache/diskcachedialog.cpp
+++ b/app/dialog/diskcache/diskcachedialog.cpp
@@ -26,6 +26,8 @@
#include
#include
+#include "oakengine/disk.h"
+
namespace olive
{
@@ -109,7 +111,7 @@ void DiskCacheDialog::clear_disk_cache(const QString &path, QWidget *parent,
if (clear_btn)
clear_btn->setEnabled(false);
- if (DiskManager::instance()->clear_disk_cache(path)) {
+ if (oakengine_disk_clear_cache(path.toUtf8().constData())) {
if (clear_btn)
clear_btn->setText(tr("Disk Cache Cleared"));
} else {
diff --git a/app/dialog/export/codec/av1section.cpp b/app/dialog/export/codec/av1section.cpp
index b567c2364..cfb0fe827 100644
--- a/app/dialog/export/codec/av1section.cpp
+++ b/app/dialog/export/codec/av1section.cpp
@@ -89,19 +89,21 @@ AV1Section::AV1Section(int default_crf, QWidget *parent)
compression_method_stack_, &QStackedWidget::setCurrentIndex);
}
-void AV1Section::add_opts(EncodingParams *params)
+void AV1Section::add_opts(OakEngineEncodingParams *params)
{
CompressionMethod method = static_cast(
compression_method_stack_->currentIndex());
if (method == k_constant_rate_factor) {
// Set Quantizer value
- params->set_video_option(QStringLiteral("qp"),
- QString::number(crf_section_->get_value()));
+ oakengine_encoding_params_set_video_option(
+ params, "qp",
+ QByteArray::number(crf_section_->get_value()).constData());
}
- params->set_video_option(QStringLiteral("preset"),
- QString::number(preset_combobox_->currentIndex()));
+ oakengine_encoding_params_set_video_option(
+ params, "preset",
+ QByteArray::number(preset_combobox_->currentIndex()).constData());
}
AV1CRFSection::AV1CRFSection(int default_crf, QWidget *parent)
diff --git a/app/dialog/export/codec/av1section.h b/app/dialog/export/codec/av1section.h
index 664423909..c8bf4174f 100644
--- a/app/dialog/export/codec/av1section.h
+++ b/app/dialog/export/codec/av1section.h
@@ -58,7 +58,7 @@ public:
AV1Section(QWidget *parent = nullptr);
AV1Section(int default_crf, QWidget *parent);
- virtual void add_opts(EncodingParams *params) override;
+ virtual void add_opts(OakEngineEncodingParams *params) override;
private:
QStackedWidget *compression_method_stack_;
diff --git a/app/dialog/export/codec/cineformsection.cpp b/app/dialog/export/codec/cineformsection.cpp
index f0dd1aa61..dc9111279 100644
--- a/app/dialog/export/codec/cineformsection.cpp
+++ b/app/dialog/export/codec/cineformsection.cpp
@@ -79,17 +79,21 @@ CineformSection::CineformSection(QWidget *parent)
layout->addWidget(quality_combobox_, row, 1);
}
-void CineformSection::add_opts(EncodingParams *params)
+void CineformSection::add_opts(OakEngineEncodingParams *params)
{
- params->set_video_option(
- QStringLiteral("quality"),
- QString::number(quality_combobox_->currentIndex()));
+ oakengine_encoding_params_set_video_option(
+ params, "quality",
+ QByteArray::number(quality_combobox_->currentIndex()).constData());
}
-void CineformSection::set_opts(const EncodingParams *p)
+void CineformSection::set_opts(const OakEngineEncodingParams *p)
{
- quality_combobox_->setCurrentIndex(
- p->video_option(QStringLiteral("quality")).toInt());
+ char buf[64];
+ const int ret = oakengine_encoding_params_video_option(
+ p, "quality", buf, static_cast(sizeof(buf)));
+ if (ret > 0) {
+ quality_combobox_->setCurrentIndex(QString::fromUtf8(buf).toInt());
+ }
}
}
diff --git a/app/dialog/export/codec/cineformsection.h b/app/dialog/export/codec/cineformsection.h
index 8318c5928..d53d7d903 100644
--- a/app/dialog/export/codec/cineformsection.h
+++ b/app/dialog/export/codec/cineformsection.h
@@ -34,9 +34,9 @@ class CineformSection : public CodecSection {
public:
CineformSection(QWidget *parent = nullptr);
- virtual void add_opts(EncodingParams *params) override;
+ virtual void add_opts(OakEngineEncodingParams *params) override;
- virtual void set_opts(const EncodingParams *p) override;
+ virtual void set_opts(const OakEngineEncodingParams *p) override;
private:
QComboBox *quality_combobox_;
diff --git a/app/dialog/export/codec/codecsection.h b/app/dialog/export/codec/codecsection.h
index f5decc7f2..a062fac54 100644
--- a/app/dialog/export/codec/codecsection.h
+++ b/app/dialog/export/codec/codecsection.h
@@ -24,7 +24,7 @@
#include
-#include "codec/encoder.h"
+#include "oakengine/encoding.h"
namespace olive
{
@@ -34,12 +34,12 @@ class CodecSection : public QWidget {
public:
CodecSection(QWidget *parent = nullptr);
- virtual void add_opts(EncodingParams *params)
+ virtual void add_opts(OakEngineEncodingParams *params)
{
Q_UNUSED(params)
}
- virtual void set_opts(const EncodingParams *p)
+ virtual void set_opts(const OakEngineEncodingParams *p)
{
Q_UNUSED(p)
}
diff --git a/app/dialog/export/codec/h264section.cpp b/app/dialog/export/codec/h264section.cpp
index 3425a8531..79d295daa 100644
--- a/app/dialog/export/codec/h264section.cpp
+++ b/app/dialog/export/codec/h264section.cpp
@@ -101,7 +101,7 @@ H264Section::H264Section(int default_crf, QWidget *parent)
compression_method_stack_, &QStackedWidget::setCurrentIndex);
}
-void H264Section::add_opts(EncodingParams *params)
+void H264Section::add_opts(OakEngineEncodingParams *params)
{
// FIXME: Implement two-pass
@@ -110,13 +110,15 @@ void H264Section::add_opts(EncodingParams *params)
// This option is not used by the encoder (nor is anything with the ove_ prefix), it's to help us
// identify which option was chosen when params are restored
- params->set_video_option(QStringLiteral("ove_compressionmethod"),
- QString::number(method));
+ oakengine_encoding_params_set_video_option(
+ params, "ove_compressionmethod",
+ QByteArray::number(method).constData());
if (method == k_constant_rate_factor) {
// Simply set CRF value
- params->set_video_option(QStringLiteral("crf"),
- QString::number(crf_section_->get_value()));
+ oakengine_encoding_params_set_video_option(
+ params, "crf",
+ QByteArray::number(crf_section_->get_value()).constData());
} else {
int64_t target_rate, max_rate, min_rate;
@@ -129,40 +131,58 @@ void H264Section::add_opts(EncodingParams *params)
} else {
// Calculate the bit rate from the file size divided by the sequence length in seconds (bits per second)
int64_t target_fs = filesize_section_->get_file_size();
- target_rate = qRound64(static_cast(target_fs) /
- params->get_export_length().to_double());
+ int export_len_num = 0, export_len_den = 1;
+ oakengine_encoding_params_get_export_length(
+ params, &export_len_num, &export_len_den);
+ const double export_len_sec =
+ (export_len_den > 0)
+ ? static_cast(export_len_num)
+ / static_cast(export_len_den)
+ : 1.0;
+ target_rate = qRound64(static_cast(target_fs) / export_len_sec);
min_rate = target_rate;
max_rate = target_rate;
- params->set_video_option(QStringLiteral("ove_targetfilesize"),
- QString::number(target_fs));
+ oakengine_encoding_params_set_video_option(
+ params, "ove_targetfilesize",
+ QByteArray::number(target_fs).constData());
}
// Disable CRF encoding
- params->set_video_option(QStringLiteral("crf"), QStringLiteral("-1"));
+ oakengine_encoding_params_set_video_option(params, "crf", "-1");
- params->set_video_bit_rate(target_rate);
- params->set_video_min_bit_rate(min_rate);
- params->set_video_max_bit_rate(max_rate);
- params->set_video_buffer_size(2000000);
+ oakengine_encoding_params_set_video_bit_rate(params, target_rate);
+ oakengine_encoding_params_set_video_min_bit_rate(params, min_rate);
+ oakengine_encoding_params_set_video_max_bit_rate(params, max_rate);
+ oakengine_encoding_params_set_video_buffer_size(params, 2000000);
}
- params->set_video_option(QStringLiteral("preset"),
- QString::number(preset_combobox_->currentIndex()));
+ oakengine_encoding_params_set_video_option(
+ params, "preset",
+ QByteArray::number(preset_combobox_->currentIndex()).constData());
}
-void H264Section::set_opts(const EncodingParams *p)
+void H264Section::set_opts(const OakEngineEncodingParams *p)
{
- CompressionMethod method = static_cast(
- p->video_option(QStringLiteral("ove_compressionmethod")).toInt());
+ char buf[64];
+
+ CompressionMethod method = k_constant_rate_factor;
+ if (oakengine_encoding_params_video_option(
+ p, "ove_compressionmethod", buf,
+ static_cast(sizeof(buf))) > 0) {
+ method = static_cast(QString::fromUtf8(buf).toInt());
+ }
compression_method_stack_->setCurrentIndex(method);
if (method == k_constant_rate_factor) {
- crf_section_->set_value(p->video_option(QStringLiteral("crf")).toInt());
+ if (oakengine_encoding_params_video_option(
+ p, "crf", buf, static_cast(sizeof(buf))) > 0) {
+ crf_section_->set_value(QString::fromUtf8(buf).toInt());
+ }
} else {
- int64_t target_rate = p->video_bit_rate();
- int64_t max_rate = p->video_max_bit_rate();
+ int64_t target_rate = oakengine_encoding_params_video_bit_rate(p);
+ int64_t max_rate = oakengine_encoding_params_video_max_bit_rate(p);
if (method == k_target_bit_rate) {
// Use user-supplied values for the bit rate
@@ -170,9 +190,12 @@ void H264Section::set_opts(const EncodingParams *p)
bitrate_section_->set_maximum_bit_rate(max_rate);
} else {
// Calculate the bit rate from the file size divided by the sequence length in seconds (bits per second)
- filesize_section_->set_file_size(
- p->video_option(QStringLiteral("ove_targetfilesize"))
- .toLongLong());
+ if (oakengine_encoding_params_video_option(
+ p, "ove_targetfilesize", buf,
+ static_cast(sizeof(buf))) > 0) {
+ filesize_section_->set_file_size(
+ QString::fromUtf8(buf).toLongLong());
+ }
}
}
}
diff --git a/app/dialog/export/codec/h264section.h b/app/dialog/export/codec/h264section.h
index b308b6770..b70fb1534 100644
--- a/app/dialog/export/codec/h264section.h
+++ b/app/dialog/export/codec/h264section.h
@@ -100,9 +100,9 @@ public:
H264Section(QWidget *parent = nullptr);
H264Section(int default_crf, QWidget *parent);
- virtual void add_opts(EncodingParams *params) override;
+ virtual void add_opts(OakEngineEncodingParams *params) override;
- virtual void set_opts(const EncodingParams *p) override;
+ virtual void set_opts(const OakEngineEncodingParams *p) override;
private:
QStackedWidget *compression_method_stack_;
diff --git a/app/dialog/export/export.cpp b/app/dialog/export/export.cpp
index 84c332460..f65fcc215 100644
--- a/app/dialog/export/export.cpp
+++ b/app/dialog/export/export.cpp
@@ -33,16 +33,24 @@
#include "common/digit.h"
#include "common/qtutils.h"
-#include "codec/ffmpeg/ffmpegencoder.h"
+#include "codec/exportcodec.h"
+#include "codec/exportformat.h"
#include "dialog/msgbox.h"
#include "dialog/task/task.h"
#include "exportsavepresetdialog.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
+#include "oakengine/events.h"
+#include "widget/manageddisplay/colorprocessorhandle.h"
+#include "widget/viewer/vieweroutpututils.h"
#include "oakengine/exporter.h"
-#include "task/taskmanager.h"
+#include "oakengine/project.h"
+#include "oakengine/task.h"
+#include "oakengine/encoding.h"
+#include "oakengine/viewer.h"
#include "ui/icons/icons.h"
#include "widget/timeruler/timeruler.h"
+#include "common/configwrapper.h"
namespace olive
{
@@ -54,159 +62,126 @@ namespace
// pix_fmt string (e.g. "yuv420p") to its index in the codec's supported
// list; 0 (the codec's preferred format) when absent.
-int pix_fmt_index(ExportCodec::Codec codec, const QString &pix_fmt)
+int pix_fmt_index(int codec, const QString &pix_fmt)
{
if (pix_fmt.isEmpty()) {
return 0;
}
- FFmpegEncoder probe{ EncodingParams() };
- const int index = probe.get_pixel_formats_for_codec(codec).indexOf(pix_fmt);
- return index >= 0 ? index : 0;
+ return oakengine_encoding_pix_fmt_index(codec, pix_fmt.toUtf8().constData());
}
-// EncodingParams (assembled by the dialog) -> facade POD. One-to-one with
+// OakEngineEncodingParams (assembled by the dialog) -> facade POD. One-to-one with
// oak_export_options_ex; see oakengine/exporter.h for the field docs.
-oak_export_options_ex params_to_ex(const EncodingParams &p)
+oak_export_options_ex params_to_ex(const OakEngineEncodingParams *p)
{
oak_export_options_ex o = {};
- const VideoParams &vp = p.video_params();
- const Rational tb = vp.frame_rate().flipped();
+ int64_t vbrate = 0, abrate = 0;
+ int asample_rate = 0;
+ uint64_t ach_layout = 0;
+ int asample_fmt = 0;
+ int vthreads = 0;
+ int scaling = 0;
+ int is_img_seq = 0;
- if (p.has_custom_range()) {
+ oak_video_params vp = {};
+ oakengine_encoding_params_get_video_params(p, &vp);
+
+ vbrate = oakengine_encoding_params_video_bit_rate(p);
+ abrate = oakengine_encoding_params_audio_bit_rate(p);
+ vthreads = oakengine_encoding_params_video_threads(p);
+ scaling = oakengine_encoding_params_video_scaling_method(p);
+ is_img_seq = oakengine_encoding_params_video_is_image_sequence(p);
+
+ if (oakengine_encoding_params_has_custom_range(p)) {
o.range_mode = OAKENGINE_EXPORT_RANGE_CUSTOM;
- o.range_in_ts = Timecode::time_to_timestamp(p.custom_range().in(), tb);
+ int64_t r_in_num = 0, r_in_den = 1, r_out_num = 0, r_out_den = 1;
+ oakengine_encoding_params_get_custom_range(
+ p, &r_in_num, &r_in_den, &r_out_num, &r_out_den);
+ o.range_in_ts =
+ Timecode::time_to_timestamp(
+ Rational(r_in_num, r_in_den),
+ Rational(vp.time_base_num, vp.time_base_den));
o.range_out_ts =
- Timecode::time_to_timestamp(p.custom_range().out(), tb);
+ Timecode::time_to_timestamp(
+ Rational(r_out_num, r_out_den),
+ Rational(vp.time_base_num, vp.time_base_den));
} else {
o.range_mode = OAKENGINE_EXPORT_RANGE_ENTIRE;
}
- o.format = int(p.format());
- o.video_enabled = p.video_enabled() ? 1 : 0;
- o.video_codec = int(p.video_codec());
- o.audio_enabled = p.audio_enabled() ? 1 : 0;
- o.audio_codec = int(p.audio_codec());
- o.subtitles_enabled = p.subtitles_enabled() ? 1 : 0;
- o.subtitles_sidecar = p.subtitles_are_sidecar() ? 1 : 0;
+ o.format = oakengine_encoding_params_format(p);
+ o.video_enabled = oakengine_encoding_params_video_enabled(p) ? 1 : 0;
+ o.video_codec = oakengine_encoding_params_video_codec(p);
+ o.audio_enabled = oakengine_encoding_params_audio_enabled(p) ? 1 : 0;
+ o.audio_codec = oakengine_encoding_params_audio_codec(p);
+ o.subtitles_enabled = oakengine_encoding_params_subtitles_enabled(p) ? 1 : 0;
+ o.subtitles_sidecar = oakengine_encoding_params_subtitles_are_sidecar(p) ? 1 : 0;
o.subtitles_format =
- p.subtitles_are_sidecar() ? int(p.subtitle_sidecar_fmt()) : 0;
- o.subtitles_codec = p.subtitles_enabled() ? int(p.subtitles_codec()) : 0;
+ oakengine_encoding_params_subtitles_are_sidecar(p)
+ ? oakengine_encoding_params_subtitles_sidecar_format(p)
+ : 0;
+ o.subtitles_codec = oakengine_encoding_params_subtitles_enabled(p)
+ ? oakengine_encoding_params_subtitles_codec(p)
+ : 0;
- o.video_bit_rate = p.video_bit_rate();
- o.audio_bit_rate = p.audio_bit_rate();
- o.video_pix_fmt = pix_fmt_index(p.video_codec(), p.video_pix_fmt());
+ o.video_bit_rate = vbrate;
+ o.audio_bit_rate = abrate;
- o.audio_sample_rate = p.audio_params().sample_rate();
- o.audio_channel_layout = p.audio_params().channel_layout();
- o.audio_sample_format = int(p.audio_params().format());
-
- const QString ct = p.color_transform().output();
- if (ct.isEmpty()) {
- o.color_transform = OAKENGINE_EXPORT_COLOR_REFERENCE;
- } else if (ct == QStringLiteral("sRGB OETF")) {
- o.color_transform = OAKENGINE_EXPORT_COLOR_SRGB_OETF;
- } else if (ct == QStringLiteral("Rec.709 OETF")) {
- o.color_transform = OAKENGINE_EXPORT_COLOR_REC709_OETF;
- } else if (ct == QStringLiteral("BT.1886 EOTF")) {
- o.color_transform = OAKENGINE_EXPORT_COLOR_BT1886_EOTF;
+ char pix_fmt_buf[64];
+ if (oakengine_encoding_params_video_pix_fmt(
+ p, pix_fmt_buf, static_cast(sizeof(pix_fmt_buf))) > 0) {
+ o.video_pix_fmt = oakengine_encoding_pix_fmt_index(
+ o.video_codec, pix_fmt_buf);
} else {
- o.color_transform = OAKENGINE_EXPORT_COLOR_CUSTOM;
- const QByteArray utf = ct.toUtf8();
- snprintf(o.color_transform_name, sizeof(o.color_transform_name),
- "%s", utf.constData());
+ o.video_pix_fmt = 0;
}
- o.video_width = vp.width();
- o.video_height = vp.height();
- o.frame_rate_num = vp.frame_rate().numerator();
- o.frame_rate_den = vp.frame_rate().denominator();
- o.pixel_aspect_num = vp.pixel_aspect_ratio().numerator();
- o.pixel_aspect_den = vp.pixel_aspect_ratio().denominator();
- o.interlacing = int(vp.interlacing());
- o.pixel_format = int(vp.format());
- o.scaling_method = int(p.video_scaling_method());
- o.color_range = int(vp.color_range());
- o.video_threads = p.video_threads();
- o.is_image_sequence = p.video_is_image_sequence() ? 1 : 0;
+ if (oakengine_encoding_params_get_audio_params(
+ p, &asample_rate, &ach_layout, &asample_fmt) == OAKENGINE_OK) {
+ o.audio_sample_rate = asample_rate;
+ o.audio_channel_layout = ach_layout;
+ o.audio_sample_format = asample_fmt;
+ }
+
+ char ct_buf[128];
+ const int ct_ret = oakengine_encoding_params_color_transform_output(
+ p, ct_buf, static_cast(sizeof(ct_buf)));
+ if (ct_ret <= 0 || ct_buf[0] == '\0') {
+ o.color_transform = OAKENGINE_EXPORT_COLOR_REFERENCE;
+ } else {
+ const QString ct = QString::fromUtf8(ct_buf);
+ if (ct == QStringLiteral("sRGB OETF")) {
+ o.color_transform = OAKENGINE_EXPORT_COLOR_SRGB_OETF;
+ } else if (ct == QStringLiteral("Rec.709 OETF")) {
+ o.color_transform = OAKENGINE_EXPORT_COLOR_REC709_OETF;
+ } else if (ct == QStringLiteral("BT.1886 EOTF")) {
+ o.color_transform = OAKENGINE_EXPORT_COLOR_BT1886_EOTF;
+ } else {
+ o.color_transform = OAKENGINE_EXPORT_COLOR_CUSTOM;
+ snprintf(o.color_transform_name, sizeof(o.color_transform_name),
+ "%s", ct_buf);
+ }
+ }
+
+ o.video_width = vp.width;
+ o.video_height = vp.height;
+ o.frame_rate_num = vp.time_base_den; // time_base is frame duration, so rate = den/num
+ o.frame_rate_den = vp.time_base_num;
+ o.pixel_aspect_num = vp.pixel_aspect_num;
+ o.pixel_aspect_den = vp.pixel_aspect_den;
+ o.interlacing = vp.interlacing;
+ o.pixel_format = vp.format;
+ o.scaling_method = scaling;
+ o.color_range = vp.color_range;
+ o.video_threads = vthreads;
+ o.is_image_sequence = is_img_seq;
return o;
}
} // namespace
-/**
- * @brief ExportTask replacement driven by the liboakengine C ABI facade
- *
- * Same Task contract as the engine's ExportTask (progress via
- * progress_changed, cancel via CancelEvent), but the actual
- * render+encode goes through oakengine_export_render_ex(): the facade
- * owns the ExportTask instance, its event-loop drive and the conform
- * prewarm. Cancellation is forwarded to the facade
- * (oakengine_export_cancel()), which reports OAKENGINE_E_CANCELLED back.
- */
-class FacadeExportTask : public Task {
-public:
- FacadeExportTask(ViewerOutput *viewer_node, const EncodingParams ¶ms)
- : sequence_(reinterpret_cast(viewer_node))
- , params_(params)
- {
- set_title(tr("Exporting \"%1\"").arg(viewer_node->get_label()));
- }
-
-protected:
- virtual bool run() override
- {
- oak_export_options_ex o = params_to_ex(params_);
- // Pass the codec section's encoder-specific options through.
- for (auto it = params_.video_opts().cbegin();
- it != params_.video_opts().cend(); ++it) {
- oakengine_export_set_video_option(it.key().toUtf8().constData(),
- it.value().toUtf8().constData());
- }
- oakengine_export_set_progress_callback(
- &FacadeExportTask::forward_progress, this);
- const int rc = oakengine_export_render_ex(
- sequence_, params_.filename().toUtf8().constData(), &o);
- oakengine_export_set_progress_callback(nullptr, nullptr);
- oakengine_export_set_video_option("", nullptr);
-
- if (rc == OAKENGINE_E_CANCELLED) {
- // Mirror the engine task's cancelled state for TaskDialog.
- cancel();
- return false;
- }
- if (rc != OAKENGINE_OK) {
- char err[1024];
- err[0] = '\0';
- oakengine_export_last_error(err, sizeof(err));
- set_error(err[0] ? QString::fromUtf8(err) :
- QStringLiteral("Export failed"));
- return false;
- }
- return true;
- }
-
- virtual void CancelEvent() override
- {
- oakengine_export_cancel();
- }
-
-private:
- static void forward_progress(double fraction, void *userdata)
- {
- static_cast(userdata)->emit_progress(fraction);
- }
-
- void emit_progress(double fraction)
- {
- emit progress_changed(fraction);
- }
-
- OakEngineSequence *sequence_;
- EncodingParams params_;
-};
-
ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
QWidget *parent)
: super(parent)
@@ -312,16 +287,32 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
preferences_tabs_ = new QTabWidget();
- color_manager_ = viewer_node_->project()->color_manager();
+ color_manager_ = oak_color_manager(viewer_node_->project()->color_manager());
video_tab_ = new ExportVideoTab(color_manager_);
add_preferences_tab(video_tab_, tr("Video"));
// Set video tab time and make connections
- connect(viewer_node, &ViewerOutput::playhead_changed, video_tab_,
- &ExportVideoTab::set_time);
- connect(video_tab_, &ExportVideoTab::time_changed, viewer_node,
- &ViewerOutput::set_playhead);
- video_tab_->set_time(viewer_node->get_playhead());
+ viewer_sub_ = oakengine_event_subscribe(
+ reinterpret_cast(viewer_node),
+ OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED,
+ [](const oakengine_event *event, void *userdata) {
+ auto *dlg = static_cast(userdata);
+ auto *tab = dlg->video_tab_;
+ tab->set_time(Rational(event->a, event->b));
+ },
+ this);
+ connect(video_tab_, &ExportVideoTab::time_changed, this,
+ [viewer_node](const Rational &time) {
+ oakengine_viewer_set_playhead(
+ reinterpret_cast(viewer_node),
+ time.numerator(), time.denominator());
+ });
+ {
+ int64_t pn, pd;
+ oakengine_viewer_get_playhead(
+ reinterpret_cast(viewer_node), &pn, &pd);
+ video_tab_->set_time(Rational(pn, pd));
+ }
audio_tab_ = new ExportAudioTab();
add_preferences_tab(audio_tab_, tr("Audio"));
@@ -394,11 +385,11 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
set_default_filename();
// Set defaults
- previously_selected_format_ = ExportFormat::k_format_mpe_g4_video;
+ previously_selected_format_ = OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO;
connect(format_combobox_, &ExportFormatComboBox::format_changed, this,
&ExportDialog::format_changed);
- VideoParams vp = viewer_node_->get_video_params();
+ VideoParams vp = viewer_output_video_params(viewer_node_);
video_aspect_ratio_ =
static_cast(vp.width()) / static_cast(vp.height());
@@ -430,7 +421,8 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
// If the viewer already has cached params, use them
if (!stills_only_mode_ &&
- viewer_node_->get_last_used_encoding_params().is_valid()) {
+ oakengine_encoding_params_get_last_used(
+ reinterpret_cast(viewer_node_)) != nullptr) {
// This will automatically set the param data
QtUtils::set_combo_box_data(preset_combobox_, k_preset_last_used);
} else {
@@ -477,8 +469,9 @@ void ExportDialog::start_export()
// Validate if the entered filename contains the correct extension (the extension is necessary
// for both FFmpeg and OIIO to determine the output format)
- QString necessary_ext = QStringLiteral(".%1").arg(
- ExportFormat::get_extension(format_combobox_->get_format()));
+char ext_buf[64];
+ int ext_len = oakengine_encoding_format_extension(format_combobox_->get_format(), ext_buf, sizeof(ext_buf));
+ QString necessary_ext = QStringLiteral(".%1").arg(QString::fromUtf8(ext_buf, ext_len));
QString proposed_filename = filename_edit_->text().trimmed();
// If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export.
@@ -513,7 +506,7 @@ void ExportDialog::start_export()
// Validate if this is an image sequence and if the filename contains enough digits
if (video_tab_->is_image_sequence_set()) {
// Ensure filename contains digits
- if (!Encoder::filename_contains_digit_placeholder(proposed_filename)) {
+ if (!oakengine_encoding_filename_contains_digit_placeholder(proposed_filename.toUtf8().constData())) {
msg_box(
this, QMessageBox::Critical, tr("Invalid filename"),
tr("Export is set to an image sequence, but the filename does not have a section for digits "
@@ -524,7 +517,7 @@ void ExportDialog::start_export()
int64_t frame_count = get_export_length_in_timebase_units();
int64_t needed_digit_count = get_digit_count(frame_count);
int current_digit_count =
- Encoder::get_image_sequence_placeholder_digit_count(proposed_filename);
+ oakengine_encoding_image_sequence_digit_count(proposed_filename.toUtf8().constData());
if (current_digit_count < needed_digit_count) {
msg_box(
this, QMessageBox::Critical, tr("Invalid filename"),
@@ -549,8 +542,8 @@ void ExportDialog::start_export()
// Validate video resolution
if (video_enabled_->isChecked() &&
- (video_tab_->get_selected_codec() == ExportCodec::k_codec_h264 ||
- video_tab_->get_selected_codec() == ExportCodec::k_codec_h265) &&
+ (video_tab_->get_selected_codec() == OAKENGINE_ENCODING_CODEC_H264 ||
+ video_tab_->get_selected_codec() == OAKENGINE_ENCODING_CODEC_H265) &&
(video_tab_->width_slider()->get_value() % 2 != 0 ||
video_tab_->height_slider()->get_value() % 2 != 0)) {
msg_box(this, QMessageBox::Critical, tr("Invalid Parameters"),
@@ -558,12 +551,13 @@ void ExportDialog::start_export()
return;
}
- FacadeExportTask *task =
- new FacadeExportTask(viewer_node_, generate_params());
+ OakEngineTask *task = oakengine_task_create_export(
+ reinterpret_cast(viewer_node_),
+ generate_params());
if (export_bkg_box_->isChecked()) {
// Send to TaskManager to export in background
- TaskManager::instance()->add_task(task);
+ oakengine_task_manager_add(task);
this->accept();
} else {
// Use modal dialog box
@@ -578,7 +572,7 @@ void ExportDialog::export_finished()
{
TaskDialog *td = static_cast(sender());
- if (td->get_task()->is_cancelled()) {
+ if (oakengine_task_is_cancelled(td->get_task())) {
// If this task was cancelled, we stay open so the user can potentially queue another export
} else {
// Accept this dialog and close
@@ -600,11 +594,14 @@ void ExportDialog::image_sequence_check_box_changed(bool e)
QString suffix = current_fileinfo.suffix();
if (e) {
- if (!Encoder::filename_contains_digit_placeholder(basename)) {
+ if (!oakengine_encoding_filename_contains_digit_placeholder(basename.toUtf8().constData())) {
basename.append(QStringLiteral("_[#####]"));
}
} else {
- basename = Encoder::filename_remove_digit_placeholder(basename);
+ char buf[1024];
+ oakengine_encoding_filename_remove_digit_placeholder(
+ basename.toUtf8().constData(), buf, sizeof(buf));
+ basename = QString::fromUtf8(buf);
}
// Set filename
@@ -636,7 +633,14 @@ void ExportDialog::preset_combo_box_changed()
if (preset_number == k_preset_default) {
set_defaults();
} else if (preset_number == k_preset_last_used) {
- set_params(viewer_node_->get_last_used_encoding_params());
+ OakEngineEncodingParams *last =
+ oakengine_encoding_params_get_last_used(
+ reinterpret_cast(viewer_node_));
+ if (last) {
+ set_params(last);
+ } else {
+ set_defaults();
+ }
} else {
set_params(presets_.at(preset_number));
}
@@ -653,12 +657,17 @@ void ExportDialog::add_preferences_tab(QWidget *inner_widget,
void ExportDialog::browse_filename()
{
- ExportFormat::Format f = format_combobox_->get_format();
+ int f = format_combobox_->get_format();
+
+ char name_buf[256];
+ char ext_buf[64];
+ oakengine_encoding_format_name(f, name_buf, sizeof(name_buf));
+ oakengine_encoding_format_extension(f, ext_buf, sizeof(ext_buf));
QString browsed_fn = QFileDialog::getSaveFileName(
this, "", filename_edit_->text().trimmed(),
QStringLiteral("%1 (*.%2)")
- .arg(ExportFormat::get_name(f), ExportFormat::get_extension(f)),
+ .arg(QString::fromUtf8(name_buf), QString::fromUtf8(ext_buf)),
nullptr,
// We don't confirm overwrite here because we do it later
@@ -669,12 +678,14 @@ void ExportDialog::browse_filename()
}
}
-void ExportDialog::format_changed(ExportFormat::Format current_format)
+void ExportDialog::format_changed(int current_format)
{
QString current_filename = filename_edit_->text().trimmed();
- QString previously_selected_ext =
- ExportFormat::get_extension(previously_selected_format_);
- QString currently_selected_ext = ExportFormat::get_extension(current_format);
+ char ext_buf[64];
+ oakengine_encoding_format_extension(previously_selected_format_, ext_buf, sizeof(ext_buf));
+ QString previously_selected_ext = QString::fromUtf8(ext_buf);
+ oakengine_encoding_format_extension(current_format, ext_buf, sizeof(ext_buf));
+ QString currently_selected_ext = QString::fromUtf8(ext_buf);
// If the previous extension was added, remove it
if (current_filename.endsWith(previously_selected_ext,
@@ -742,25 +753,45 @@ void ExportDialog::load_presets()
preset_combobox_->addItem(tr("Default"), k_preset_default);
- if (viewer_node_->get_last_used_encoding_params().is_valid()) {
+ if (oakengine_encoding_params_get_last_used(
+ reinterpret_cast(viewer_node_)) != nullptr) {
preset_combobox_->addItem(tr("Last Used"), k_preset_last_used);
}
preset_combobox_->insertSeparator(preset_combobox_->count());
- QStringList l = EncodingParams::get_list_of_presets();
+ QStringList l;
+ {
+ const int n = oakengine_encoding_preset_count();
+ for (int i = 0; i < n; i++) {
+ char name_buf[256];
+ if (oakengine_encoding_preset_name(
+ i, name_buf, static_cast(sizeof(name_buf))) > 0) {
+ l.append(QString::fromUtf8(name_buf));
+ }
+ }
+ }
presets_.reserve(l.size());
for (const QString &preset : l) {
- EncodingParams p;
+ OakEngineEncodingParams *p = oakengine_encoding_params_create();
- QFile f(EncodingParams::get_preset_path().filePath(preset));
- if (f.open(QFile::ReadOnly)) {
- if (p.load(&f)) {
- preset_combobox_->addItem(preset, int(presets_.size()));
- presets_.push_back(p);
- }
- f.close();
+ char preset_path_buf[1024];
+ preset_path_buf[0] = '\0';
+ oakengine_encoding_preset_path(
+ preset_path_buf, static_cast(sizeof(preset_path_buf)));
+
+ const QByteArray preset_path_utf =
+ QDir(QString::fromUtf8(preset_path_buf))
+ .filePath(preset)
+ .toUtf8();
+ const int rc = oakengine_encoding_params_load_file(
+ p, preset_path_utf.constData());
+ if (rc == OAKENGINE_OK) {
+ preset_combobox_->addItem(preset, int(presets_.size()));
+ presets_.push_back(p);
+ } else {
+ oakengine_encoding_params_destroy(p);
}
}
@@ -771,13 +802,17 @@ void ExportDialog::set_default_filename()
{
Project *p = viewer_node_->project();
+ char fn_buf[512];
+ oakengine_project_filename(
+ reinterpret_cast(p),
+ fn_buf, sizeof(fn_buf));
QDir doc_location;
- if (p->filename().isEmpty()) {
+ if (fn_buf[0] == '\0') {
doc_location.setPath(QStandardPaths::writableLocation(
QStandardPaths::DocumentsLocation));
} else {
- doc_location = QFileInfo(p->filename()).dir();
+ doc_location = QFileInfo(fn_buf).dir();
}
QString file_location = doc_location.filePath(viewer_node_->get_label());
@@ -801,14 +836,14 @@ bool ExportDialog::sequence_has_subtitles() const
void ExportDialog::set_defaults()
{
if (!stills_only_mode_) {
- format_combobox_->set_format(ExportFormat::k_format_mpe_g4_video);
+ format_combobox_->set_format(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO);
} else {
- format_combobox_->set_format(ExportFormat::k_format_png);
+ format_combobox_->set_format(OAKENGINE_ENCODING_FORMAT_PNG);
}
format_changed(format_combobox_->get_format());
- VideoParams vp = viewer_node_->get_video_params();
- AudioParams ap = viewer_node_->get_audio_params();
+ VideoParams vp = viewer_output_video_params(viewer_node_);
+ AudioParams ap = viewer_output_audio_params(viewer_node_);
video_tab_->width_slider()->set_value(vp.width());
video_tab_->width_slider()->SetDefaultValue(vp.width());
@@ -826,151 +861,217 @@ void ExportDialog::set_defaults()
audio_tab_->channel_layout_combobox()->set_channel_layout(
ap.channel_layout());
subtitles_enabled_->setChecked(sequence_has_subtitles());
- subtitle_tab_->set_sidecar_format(ExportFormat::k_format_srt);
+ subtitle_tab_->set_sidecar_format(OAKENGINE_ENCODING_FORMAT_SRT);
}
-EncodingParams ExportDialog::generate_params() const
+OakEngineEncodingParams *ExportDialog::generate_params() const
{
- VideoParams video_render_params(
- static_cast(video_tab_->width_slider()->get_value()),
- static_cast(video_tab_->height_slider()->get_value()),
- get_selected_timebase(),
- video_tab_->pixel_format_field()->get_pixel_format(),
- VideoParams::k_internal_channel_count,
- video_tab_->pixel_aspect_combobox()->get_pixel_aspect_ratio(),
- video_tab_->interlaced_combobox()->get_interlace_mode(), 1);
+ OakEngineEncodingParams *params = oakengine_encoding_params_create();
- AudioParams audio_render_params(
- audio_tab_->sample_rate_combobox()->get_sample_rate(),
- audio_tab_->channel_layout_combobox()->get_channel_layout(),
- audio_tab_->sample_format_combobox()->get_sample_format());
+ oakengine_encoding_params_set_format(
+ params, format_combobox_->get_format());
+ oakengine_encoding_params_set_filename(
+ params, filename_edit_->text().trimmed().toUtf8().constData());
- EncodingParams params;
- params.set_format(format_combobox_->get_format());
- params.set_filename(filename_edit_->text().trimmed());
- params.set_export_length(viewer_node_->get_length());
+ const Rational export_len = viewer_node_->get_length();
+ oakengine_encoding_params_set_export_length(
+ params, export_len.numerator(), export_len.denominator());
- if (ExportCodec::is_codec_a_still_image(video_tab_->get_selected_codec()) &&
+ if (oakengine_encoding_codec_is_still_image(video_tab_->get_selected_codec()) &&
!video_tab_->is_image_sequence_set()) {
// Exporting as image without exporting image sequence, only export one frame
Rational export_time = video_tab_->get_still_image_time();
- params.set_custom_range(
- TimeRange(export_time, export_time + get_selected_timebase()));
+ const Rational tb = get_selected_timebase();
+ oakengine_encoding_params_set_custom_range(
+ params, export_time.numerator(), export_time.denominator(),
+ (export_time + tb).numerator(),
+ (export_time + tb).denominator());
} else if (range_combobox_->currentIndex() == k_range_in_to_out) {
- // Assume if this combobox is enabled, workarea is enabled - a check that we make in this dialog's constructor
- params.set_custom_range(viewer_node_->get_work_area()->range());
+ const TimeRange &r = viewer_node_->get_work_area()->range();
+ oakengine_encoding_params_set_custom_range(
+ params, r.in().numerator(), r.in().denominator(),
+ r.out().numerator(), r.out().denominator());
}
if (video_tab_->scaling_method_combobox()->isEnabled()) {
- params.set_video_scaling_method(
- static_cast(
- video_tab_->scaling_method_combobox()->currentData().toInt()));
+ oakengine_encoding_params_set_video_scaling_method(
+ params,
+ video_tab_->scaling_method_combobox()->currentData().toInt());
}
if (video_enabled_->isChecked()) {
- ExportCodec::Codec video_codec = video_tab_->get_selected_codec();
+ const int video_codec = video_tab_->get_selected_codec();
- video_render_params.set_color_range(video_tab_->color_range());
+ // Build video params from the tab
+ const int vw = static_cast(video_tab_->width_slider()->get_value());
+ const int vh = static_cast(video_tab_->height_slider()->get_value());
+ const Rational tb = get_selected_timebase();
+ const int pix_fmt = video_tab_->pixel_format_field()->get_pixel_format();
+ const int ch_count = oakengine_video_params_internal_channel_count();
+ const Rational par = video_tab_->pixel_aspect_combobox()->get_pixel_aspect_ratio();
+ const int interlace = video_tab_->interlaced_combobox()->get_interlace_mode();
- params.enable_video(video_render_params, video_codec);
+ oak_video_params vp = {};
+ vp.width = vw;
+ vp.height = vh;
+ vp.time_base_num = tb.numerator();
+ vp.time_base_den = tb.denominator();
+ vp.format = pix_fmt;
+ vp.pixel_aspect_num = par.numerator();
+ vp.pixel_aspect_den = par.denominator();
+ vp.interlacing = interlace;
+ vp.color_range = video_tab_->color_range();
- params.set_video_threads(video_tab_->threads());
+ oakengine_encoding_params_enable_video(params, &vp, video_codec);
+
+ oakengine_encoding_params_set_video_threads(
+ params, video_tab_->threads());
if (video_tab_->isVisible()) {
- video_tab_->get_codec_section()->add_opts(¶ms);
+ video_tab_->get_codec_section()->add_opts(params);
}
- params.set_color_transform(video_tab_->current_ocio_color_space());
+ {
+ const QString ct = video_tab_->current_ocio_color_space();
+ oakengine_encoding_params_set_color_transform(
+ params, ct.isEmpty() ? nullptr : ct.toUtf8().constData());
+ }
- params.set_video_pix_fmt(video_tab_->pix_fmt());
+ {
+ const QString pix_fmt_name = video_tab_->pix_fmt();
+ oakengine_encoding_params_set_video_pix_fmt(
+ params,
+ pix_fmt_name.isEmpty() ? nullptr
+ : pix_fmt_name.toUtf8().constData());
+ }
- params.set_video_is_image_sequence(video_tab_->is_image_sequence_set());
+ oakengine_encoding_params_set_video_is_image_sequence(
+ params, video_tab_->is_image_sequence_set() ? 1 : 0);
}
if (audio_enabled_->isChecked()) {
- ExportCodec::Codec audio_codec = audio_tab_->get_codec();
- params.enable_audio(audio_render_params, audio_codec);
+ const int audio_codec = audio_tab_->get_codec();
+ const int sample_rate = audio_tab_->sample_rate_combobox()->get_sample_rate();
+ const uint64_t ch_layout = audio_tab_->channel_layout_combobox()->get_channel_layout();
+ const int sample_fmt = audio_tab_->sample_format_combobox()->get_sample_format();
- params.set_audio_bit_rate(audio_tab_->bit_rate_slider()->get_value() *
- 1000);
+ oakengine_encoding_params_enable_audio(
+ params, sample_rate, ch_layout, sample_fmt, audio_codec);
+
+ oakengine_encoding_params_set_audio_bit_rate(
+ params,
+ audio_tab_->bit_rate_slider()->get_value() * 1000);
}
if (subtitles_enabled_->isEnabled() && subtitles_enabled_->isChecked()) {
if (!subtitle_tab_->get_sidecar_enabled()) {
// Export subtitles embedded in container
- params.enable_subtitles(subtitle_tab_->get_subtitle_codec());
+ oakengine_encoding_params_enable_subtitles(
+ params, subtitle_tab_->get_subtitle_codec());
} else {
// Export subtitles to a sidecar file
- params.enable_sidecar_subtitles(subtitle_tab_->get_sidecar_format(),
- subtitle_tab_->get_subtitle_codec());
+ oakengine_encoding_params_enable_sidecar_subtitles(
+ params, subtitle_tab_->get_sidecar_format(),
+ subtitle_tab_->get_subtitle_codec());
}
}
return params;
}
-void ExportDialog::set_params(const EncodingParams &e)
+void ExportDialog::set_params(const OakEngineEncodingParams *e)
{
- format_combobox_->set_format(e.format());
+ format_combobox_->set_format(oakengine_encoding_params_format(e));
format_changed(format_combobox_->get_format());
- if (e.has_custom_range() && viewer_node_->get_work_area()->enabled()) {
+ if (oakengine_encoding_params_has_custom_range(e) &&
+ viewer_node_->get_work_area()->enabled()) {
range_combobox_->setCurrentIndex(k_range_in_to_out);
}
QtUtils::set_combo_box_data(video_tab_->scaling_method_combobox(),
- e.video_scaling_method());
+ oakengine_encoding_params_video_scaling_method(e));
- video_enabled_->setChecked(e.video_enabled());
- if (e.video_enabled()) {
- video_tab_->width_slider()->set_value(e.video_params().width());
- video_tab_->height_slider()->set_value(e.video_params().height());
- set_selected_timebase(e.video_params().time_base());
+ const int video_enabled = oakengine_encoding_params_video_enabled(e);
+ video_enabled_->setChecked(video_enabled);
+ if (video_enabled) {
+ oak_video_params vp = {};
+ oakengine_encoding_params_get_video_params(e, &vp);
+
+ video_tab_->width_slider()->set_value(vp.width);
+ video_tab_->height_slider()->set_value(vp.height);
+ set_selected_timebase(Rational(vp.time_base_num, vp.time_base_den));
video_tab_->pixel_format_field()->set_pixel_format(
- e.video_params().format());
+ static_cast(vp.format));
video_tab_->pixel_aspect_combobox()->set_pixel_aspect_ratio(
- e.video_params().pixel_aspect_ratio());
- video_tab_->interlaced_combobox()->set_interlace_mode(
- e.video_params().interlacing());
+ Rational(vp.pixel_aspect_num, vp.pixel_aspect_den));
+ video_tab_->interlaced_combobox()->set_interlace_mode(vp.interlacing);
- video_tab_->set_selected_codec(e.video_codec());
+ video_tab_->set_selected_codec(oakengine_encoding_params_video_codec(e));
- video_tab_->set_color_range(e.video_params().color_range());
+ video_tab_->set_color_range(vp.color_range);
- video_tab_->set_threads(e.video_threads());
+ video_tab_->set_threads(oakengine_encoding_params_video_threads(e));
if (video_tab_->isVisible()) {
- video_tab_->get_codec_section()->set_opts(&e);
+ video_tab_->get_codec_section()->set_opts(e);
}
- video_tab_->set_ocio_color_space(e.color_transform().output());
+ {
+ char ct_buf[128];
+ if (oakengine_encoding_params_color_transform_output(
+ e, ct_buf, static_cast(sizeof(ct_buf))) > 0) {
+ video_tab_->set_ocio_color_space(QString::fromUtf8(ct_buf));
+ } else {
+ video_tab_->set_ocio_color_space(QString());
+ }
+ }
- video_tab_->set_pix_fmt(e.video_pix_fmt());
+ {
+ char pix_fmt_buf[64];
+ if (oakengine_encoding_params_video_pix_fmt(
+ e, pix_fmt_buf, static_cast(sizeof(pix_fmt_buf))) > 0) {
+ video_tab_->set_pix_fmt(QString::fromUtf8(pix_fmt_buf));
+ } else {
+ video_tab_->set_pix_fmt(QString());
+ }
+ }
- video_tab_->set_image_sequence(e.video_is_image_sequence());
+ video_tab_->set_image_sequence(
+ oakengine_encoding_params_video_is_image_sequence(e));
}
- audio_enabled_->setChecked(e.audio_enabled());
- if (e.audio_enabled()) {
- audio_tab_->sample_rate_combobox()->set_sample_rate(
- e.audio_params().sample_rate());
- audio_tab_->channel_layout_combobox()->set_channel_layout(
- e.audio_params().channel_layout());
+ const int audio_enabled = oakengine_encoding_params_audio_enabled(e);
+ audio_enabled_->setChecked(audio_enabled);
+ if (audio_enabled) {
+ int asample_rate = 0;
+ uint64_t ach_layout = 0;
+ int asample_fmt = 0;
+ oakengine_encoding_params_get_audio_params(
+ e, &asample_rate, &ach_layout, &asample_fmt);
+
+ audio_tab_->sample_rate_combobox()->set_sample_rate(asample_rate);
+ audio_tab_->channel_layout_combobox()->set_channel_layout(ach_layout);
audio_tab_->sample_format_combobox()->set_sample_format(
- e.audio_params().format());
+ static_cast(asample_fmt));
- audio_tab_->set_codec(e.audio_codec());
+ audio_tab_->set_codec(oakengine_encoding_params_audio_codec(e));
- audio_tab_->bit_rate_slider()->set_value(e.audio_bit_rate() / 1000);
+ audio_tab_->bit_rate_slider()->set_value(
+ oakengine_encoding_params_audio_bit_rate(e) / 1000);
}
if (subtitles_enabled_->isEnabled()) {
- subtitles_enabled_->setChecked(e.subtitles_enabled());
- subtitle_tab_->set_sidecar_enabled(e.subtitles_are_sidecar());
- if (e.subtitles_enabled()) {
- subtitle_tab_->set_subtitle_codec(e.subtitles_codec());
- if (e.subtitles_are_sidecar()) {
- subtitle_tab_->set_sidecar_format(e.subtitle_sidecar_fmt());
+ const int subs_enabled = oakengine_encoding_params_subtitles_enabled(e);
+ subtitles_enabled_->setChecked(subs_enabled);
+ subtitle_tab_->set_sidecar_enabled(
+ oakengine_encoding_params_subtitles_are_sidecar(e));
+ if (subs_enabled) {
+ subtitle_tab_->set_subtitle_codec(
+ oakengine_encoding_params_subtitles_codec(e));
+ if (oakengine_encoding_params_subtitles_are_sidecar(e)) {
+ subtitle_tab_->set_sidecar_format(
+ oakengine_encoding_params_subtitles_sidecar_format(e));
}
}
}
@@ -997,7 +1098,10 @@ void ExportDialog::done(int r)
preview_viewer_->connect_viewer_node(nullptr);
if (!stills_only_mode_) {
- viewer_node_->set_last_used_encoding_params(generate_params());
+ OakEngineEncodingParams *p = generate_params();
+ oakengine_encoding_params_set_last_used(
+ reinterpret_cast(viewer_node_), p);
+ oakengine_encoding_params_destroy(p);
}
super::done(r);
@@ -1024,14 +1128,16 @@ void ExportDialog::update_viewer_dimensions()
static_cast(video_tab_->width_slider()->get_value()),
static_cast(video_tab_->height_slider()->get_value()));
- VideoParams vp = viewer_node_->get_video_params();
+ VideoParams vp = viewer_output_video_params(viewer_node_);
- QMatrix4x4 transform = EncodingParams::generate_matrix(
- static_cast(
- video_tab_->scaling_method_combobox()->currentData().toInt()),
+ float mat16[16];
+ oakengine_encoding_generate_matrix(
+ video_tab_->scaling_method_combobox()->currentData().toInt(),
vp.width(), vp.height(),
static_cast(video_tab_->width_slider()->get_value()),
- static_cast(video_tab_->height_slider()->get_value()));
+ static_cast(video_tab_->height_slider()->get_value()),
+ mat16);
+ QMatrix4x4 transform(mat16);
preview_viewer_->set_matrix(transform);
}
diff --git a/app/dialog/export/export.h b/app/dialog/export/export.h
index f3a8cfc97..d5a6ff052 100644
--- a/app/dialog/export/export.h
+++ b/app/dialog/export/export.h
@@ -24,17 +24,17 @@
#include
#include
+#include
#include
#include
#include
#include "codec/encoder.h"
-#include "codec/exportcodec.h"
-#include "codec/exportformat.h"
#include "dialog/export/exportformatcombobox.h"
#include "exportaudiotab.h"
#include "exportsubtitlestab.h"
#include "exportvideotab.h"
+#include "oakengine/encoding.h"
#include "widget/nodeparamview/nodeparamviewwidgetbridge.h"
#include "widget/viewer/viewer.h"
@@ -54,8 +54,8 @@ public:
Rational get_selected_timebase() const;
void set_selected_timebase(const Rational &r);
- EncodingParams generate_params() const;
- void set_params(const EncodingParams &e);
+ OakEngineEncodingParams *generate_params() const;
+ void set_params(const OakEngineEncodingParams *e);
virtual bool eventFilter(QObject *o, QEvent *e) override;
@@ -77,7 +77,9 @@ private:
ViewerOutput *viewer_node_;
- ExportFormat::Format previously_selected_format_;
+ int64_t viewer_sub_ = 0;
+
+ int previously_selected_format_;
Rational get_export_length() const;
int64_t get_export_length_in_timebase_units() const;
@@ -93,7 +95,7 @@ private:
QComboBox *preset_combobox_;
QComboBox *range_combobox_;
- std::vector presets_;
+ std::vector presets_;
QCheckBox *video_enabled_;
QCheckBox *audio_enabled_;
@@ -109,7 +111,7 @@ private:
double video_aspect_ratio_;
- ColorManager *color_manager_;
+ OakEngineColorManager *color_manager_;
QWidget *preferences_area_;
QCheckBox *export_bkg_box_;
@@ -122,7 +124,7 @@ private:
private slots:
void browse_filename();
- void format_changed(ExportFormat::Format current_format);
+ void format_changed(int current_format);
void resolution_changed();
diff --git a/app/dialog/export/exportadvancedvideodialog.h b/app/dialog/export/exportadvancedvideodialog.h
index 6088cc649..6ebc6fb59 100644
--- a/app/dialog/export/exportadvancedvideodialog.h
+++ b/app/dialog/export/exportadvancedvideodialog.h
@@ -54,13 +54,13 @@ public:
pixel_format_combobox_->setCurrentText(s);
}
- VideoParams::ColorRange yuv_range() const
+ int yuv_range() const
{
- return static_cast(
+ return static_cast(
yuv_color_range_combobox_->currentIndex());
}
- void set_yuv_range(VideoParams::ColorRange i)
+ void set_yuv_range(int i)
{
yuv_color_range_combobox_->setCurrentIndex(i);
}
diff --git a/app/dialog/export/exportaudiotab.cpp b/app/dialog/export/exportaudiotab.cpp
index 23857cb7a..e65845b35 100644
--- a/app/dialog/export/exportaudiotab.cpp
+++ b/app/dialog/export/exportaudiotab.cpp
@@ -24,6 +24,9 @@
#include
#include
+#include