From 0aa5879f354034ea54923a268ef7fa7790a03e9a Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sun, 26 Jul 2026 22:43:21 +0800 Subject: [PATCH] app: migrate all engine access to the C ABI facade (nm U _ZN5olive = 0) Every app module now reaches liboakengine exclusively through oakengine_* C calls, EngineEventBridge subscriptions and app-side handle headers (cliphandle/keyframehandle/nodevaluehandle/oakvaluehelper). Direct C++ command construction, engine signal connect()s, and engine type usage in MOC-visible signatures are gone: 557 -> 0 undefined olive:: symbols in oak-editor. --- app/CMakeLists.txt | 28 + app/common/colorcodingapp.cpp | 81 + app/common/colorcodingapp.h | 75 + app/common/configwrapper.h | 206 +++ app/common/debugapp.h | 87 ++ app/common/filefunctionsapp.cpp | 98 ++ app/common/hashstreamapp.cpp | 67 + app/common/htmlapp.cpp | 507 +++++++ app/common/htmlapp.h | 83 ++ app/common/nodevaluehandle.h | 68 + app/common/oakvaluehelper.h | 226 +++ app/common/qtutilsapp.cpp | 143 ++ app/common/undowrapper.h | 60 + app/common/xmlutilsapp.cpp | 47 + app/core.cpp | 789 +++++++--- app/core.h | 140 +- app/dialog/about/about.cpp | 2 +- app/dialog/color/colordialog.cpp | 72 +- app/dialog/color/colordialog.h | 10 +- app/dialog/configbase/configdialogbase.cpp | 5 +- app/dialog/configbase/configdialogbasetab.h | 5 +- app/dialog/diskcache/diskcachedialog.cpp | 4 +- app/dialog/export/codec/av1section.cpp | 12 +- app/dialog/export/codec/av1section.h | 2 +- app/dialog/export/codec/cineformsection.cpp | 18 +- app/dialog/export/codec/cineformsection.h | 4 +- app/dialog/export/codec/codecsection.h | 6 +- app/dialog/export/codec/h264section.cpp | 73 +- app/dialog/export/codec/h264section.h | 4 +- app/dialog/export/export.cpp | 610 ++++---- app/dialog/export/export.h | 18 +- app/dialog/export/exportadvancedvideodialog.h | 6 +- app/dialog/export/exportaudiotab.cpp | 29 +- app/dialog/export/exportaudiotab.h | 12 +- app/dialog/export/exportformatcombobox.cpp | 39 +- app/dialog/export/exportformatcombobox.h | 9 +- app/dialog/export/exportsavepresetdialog.cpp | 38 +- app/dialog/export/exportsavepresetdialog.h | 6 +- app/dialog/export/exportsubtitlestab.cpp | 49 +- app/dialog/export/exportsubtitlestab.h | 14 +- app/dialog/export/exportvideotab.cpp | 60 +- app/dialog/export/exportvideotab.h | 24 +- .../footageproperties/footageproperties.cpp | 49 +- .../footageproperties/footageproperties.h | 1 - .../audiostreamproperties.cpp | 2 +- .../streamproperties/audiostreamproperties.h | 2 +- .../streamproperties/streamproperties.h | 3 +- .../videostreamproperties.cpp | 78 +- .../streamproperties/videostreamproperties.h | 2 +- .../footagerelink/footagerelinkdialog.cpp | 4 +- .../markerpropertiesdialog.cpp | 38 +- app/dialog/preferences/preferences.cpp | 4 +- .../tabs/preferencesappearancetab.cpp | 8 +- .../tabs/preferencesappearancetab.h | 2 +- .../preferences/tabs/preferencesaudiotab.cpp | 24 +- .../preferences/tabs/preferencesaudiotab.h | 2 +- .../tabs/preferencesbehaviortab.cpp | 4 +- .../preferences/tabs/preferencesbehaviortab.h | 2 +- .../preferences/tabs/preferencesdisktab.cpp | 41 +- .../preferences/tabs/preferencesdisktab.h | 6 +- .../tabs/preferencesgeneraltab.cpp | 2 +- .../preferences/tabs/preferencesgeneraltab.h | 2 +- .../tabs/preferenceskeyboardtab.cpp | 2 +- .../preferences/tabs/preferenceskeyboardtab.h | 2 +- .../preferences/tabs/preferencesluttab.cpp | 25 +- .../preferences/tabs/preferencesluttab.h | 2 +- .../progress/pluginprogressdialogreporter.cpp | 6 +- .../progress/pluginprogressdialogreporter.h | 4 +- .../projectproperties/projectproperties.cpp | 159 +- app/dialog/proxy/proxydialog.cpp | 99 +- app/dialog/proxy/proxydialog.h | 7 +- app/dialog/sequence/sequence.cpp | 5 +- .../sequence/sequencedialogparametertab.cpp | 17 +- .../sequence/sequencedialogparametertab.h | 2 +- .../sequence/sequencedialogpresettab.cpp | 48 +- app/dialog/sequence/sequencepreset.h | 8 +- .../speedduration/speeddurationdialog.cpp | 172 ++- .../speedduration/speeddurationdialog.h | 7 +- app/dialog/task/task.cpp | 60 +- app/dialog/task/task.h | 32 +- app/engineeventbridge.cpp | 427 ++++++ app/engineeventbridge.h | 247 ++++ app/main.cpp | 65 +- app/panel/curve/curve.h | 9 +- app/panel/footageviewer/footageviewer.cpp | 6 +- app/panel/footageviewer/footageviewer.h | 2 +- app/panel/node/node.h | 17 +- app/panel/panelmanager.cpp | 2 +- app/panel/param/param.h | 12 +- app/panel/project/footagemanagementpanel.h | 4 +- app/panel/project/project.cpp | 48 +- app/panel/project/project.h | 14 +- app/panel/scope/scope.cpp | 2 +- app/panel/scope/scope.h | 2 +- app/panel/table/table.h | 5 +- app/panel/taskmanager/taskmanager.cpp | 37 +- app/panel/taskmanager/taskmanager.h | 4 + app/panel/timebased/timebased.cpp | 18 +- app/panel/timebased/timebased.h | 6 +- app/panel/timeline/timeline.cpp | 4 +- app/panel/timeline/timeline.h | 10 +- app/panel/viewer/viewerbase.cpp | 4 +- app/panel/viewer/viewerbase.h | 13 +- app/timeline/CMakeLists.txt | 23 + app/timeline/timelinecoordinate.cpp | 67 + app/timeline/timelinecoordinate.h | 51 + app/ui/CMakeLists.txt | 1 + app/ui/icons/CMakeLists.txt | 22 + app/ui/icons/icons.cpp | 224 +++ app/ui/icons/icons.h | 169 +++ app/ui/style/style.cpp | 2 +- app/widget/audiomonitor/audiomonitor.cpp | 29 +- app/widget/audiomonitor/audiomonitor.h | 6 +- app/widget/colorbutton/colorbutton.cpp | 43 +- app/widget/colorbutton/colorbutton.h | 12 +- app/widget/colorwheel/colorpreviewbox.cpp | 8 +- app/widget/colorwheel/colorpreviewbox.h | 10 +- app/widget/colorwheel/colorspacechooser.cpp | 87 +- app/widget/colorwheel/colorspacechooser.h | 7 +- app/widget/colorwheel/colorswatchchooser.cpp | 2 +- app/widget/colorwheel/colorswatchchooser.h | 4 +- app/widget/colorwheel/colorswatchwidget.cpp | 8 +- app/widget/colorwheel/colorswatchwidget.h | 10 +- app/widget/colorwheel/colorvalueswidget.cpp | 20 +- app/widget/colorwheel/colorvalueswidget.h | 23 +- app/widget/curvewidget/curveview.cpp | 70 +- app/widget/curvewidget/curveview.h | 2 +- app/widget/curvewidget/curvewidget.cpp | 3 +- app/widget/curvewidget/curvewidget.h | 5 +- app/widget/filefield/lutfilefield.cpp | 26 +- .../handmovableview/handmovableview.cpp | 2 +- app/widget/history/historywidget.cpp | 22 +- app/widget/history/historywidget.h | 5 + app/widget/keyframeview/keyframehandle.h | 209 +++ app/widget/keyframeview/keyframeview.cpp | 195 ++- app/widget/keyframeview/keyframeview.h | 2 +- .../keyframeviewinputconnection.cpp | 70 +- .../keyframeviewinputconnection.h | 13 +- .../manageddisplay/colorprocessorhandle.h | 246 ++++ app/widget/manageddisplay/manageddisplay.cpp | 185 ++- app/widget/manageddisplay/manageddisplay.h | 38 +- app/widget/menu/factorymenu.cpp | 25 +- app/widget/menu/factorymenu.h | 3 +- app/widget/menu/menushared.cpp | 5 +- app/widget/multicam/multicamdisplay.cpp | 23 +- app/widget/multicam/multicamwidget.cpp | 114 +- app/widget/multicam/multicamwidget.h | 5 + app/widget/nodecombobox/nodecombobox.cpp | 7 +- app/widget/nodeparamview/nodeparamview.cpp | 349 +++-- app/widget/nodeparamview/nodeparamview.h | 51 +- .../nodeparamviewarraywidget.cpp | 10 +- .../nodeparamview/nodeparamviewarraywidget.h | 3 + .../nodeparamviewconnectedlabel.cpp | 50 +- .../nodeparamviewconnectedlabel.h | 13 +- .../nodeparamview/nodeparamviewcontext.cpp | 51 +- .../nodeparamview/nodeparamviewcontext.h | 5 +- .../nodeparamview/nodeparamviewitem.cpp | 96 +- app/widget/nodeparamview/nodeparamviewitem.h | 14 +- .../nodeparamviewkeyframecontrol.cpp | 242 ++- .../nodeparamviewkeyframecontrol.h | 11 + .../nodeparamviewwidgetbridge.cpp | 348 +++-- .../nodeparamview/nodeparamviewwidgetbridge.h | 22 +- app/widget/nodetableview/nodetableview.cpp | 103 +- app/widget/nodetreeview/nodetreeview.cpp | 9 +- app/widget/nodetreeview/nodetreeview.h | 8 +- app/widget/nodevaluetree/nodevaluetree.cpp | 71 +- app/widget/nodeview/nodeview.cpp | 594 +++++--- app/widget/nodeview/nodeview.h | 30 +- app/widget/nodeview/nodeviewcontext.cpp | 149 +- app/widget/nodeview/nodeviewcontext.h | 25 +- app/widget/nodeview/nodeviewitem.cpp | 46 +- app/widget/nodeview/nodeviewitem.h | 3 + app/widget/nodeview/nodeviewscene.h | 6 +- .../playbackcontrols/playbackcontrols.cpp | 2 +- .../projectexplorer/projectexplorer.cpp | 117 +- app/widget/projectexplorer/projectexplorer.h | 10 +- .../projectexplorer/projectexplorerundo.h | 1 - .../projectexplorer/projectviewmodel.cpp | 128 +- app/widget/projectexplorer/projectviewmodel.h | 25 +- .../resizabletimelinescrollbar.cpp | 92 +- .../resizabletimelinescrollbar.h | 16 + app/widget/scope/histogram/histogram.cpp | 13 +- app/widget/scope/scopebase/scopebase.cpp | 58 +- app/widget/scope/vectorscope/vectorscope.cpp | 4 +- app/widget/scope/waveform/waveform.cpp | 4 +- app/widget/slider/base/numericsliderbase.cpp | 2 +- app/widget/slider/base/sliderladder.cpp | 2 +- app/widget/standardcombos/frameratecombobox.h | 29 +- .../standardcombos/interlacedcombobox.h | 8 +- .../standardcombos/pixelaspectratiocombobox.h | 27 +- .../standardcombos/pixelformatcombobox.h | 16 +- .../standardcombos/videodividercombobox.h | 10 +- app/widget/taskview/taskview.cpp | 6 +- app/widget/taskview/taskview.h | 18 +- app/widget/taskview/taskviewitem.cpp | 28 +- app/widget/taskview/taskviewitem.h | 20 +- app/widget/timebased/timebasedview.cpp | 19 +- app/widget/timebased/timebasedview.h | 3 + .../timebased/timebasedviewselectionmanager.h | 84 +- app/widget/timebased/timebasedwidget.cpp | 216 ++- app/widget/timebased/timebasedwidget.h | 5 +- app/widget/timebased/timescaledobject.cpp | 3 +- app/widget/timelinewidget/cliphandle.h | 210 +++ app/widget/timelinewidget/timelinewidget.cpp | 1053 ++++++++----- app/widget/timelinewidget/timelinewidget.h | 145 +- .../timelinewidgetwaveformsync.cpp | 7 +- app/widget/timelinewidget/tool/add.cpp | 78 +- app/widget/timelinewidget/tool/add.h | 2 +- app/widget/timelinewidget/tool/import.cpp | 220 +-- app/widget/timelinewidget/tool/import.h | 6 +- app/widget/timelinewidget/tool/pointer.cpp | 103 +- app/widget/timelinewidget/tool/ripple.cpp | 44 +- app/widget/timelinewidget/tool/rolling.cpp | 1 - app/widget/timelinewidget/tool/slide.cpp | 1 - app/widget/timelinewidget/tool/slip.cpp | 13 +- app/widget/timelinewidget/tool/tool.cpp | 2 +- app/widget/timelinewidget/tool/tool.h | 2 +- app/widget/timelinewidget/tool/transition.cpp | 73 +- app/widget/timelinewidget/trackhandle.h | 74 + .../timelinewidget/trackview/trackview.cpp | 16 +- .../timelinewidget/trackview/trackview.h | 10 +- .../trackview/trackviewitem.cpp | 62 +- .../timelinewidget/trackview/trackviewitem.h | 6 +- .../trackview/trackviewsplitter.cpp | 4 +- .../timelinewidget/view/timelineview.cpp | 98 +- app/widget/timelinewidget/view/timelineview.h | 13 +- .../view/timelineviewghostitem.h | 3 +- app/widget/timeruler/CMakeLists.txt | 2 + app/widget/timeruler/markerhandle.h | 154 ++ app/widget/timeruler/markerpainting.cpp | 115 ++ app/widget/timeruler/markerpainting.h | 61 + app/widget/timeruler/seekablewidget.cpp | 223 ++- app/widget/timeruler/seekablewidget.h | 9 + app/widget/timeruler/timeruler.cpp | 38 +- app/widget/timeruler/timeruler.h | 5 + app/widget/timetarget/timetarget.cpp | 12 +- app/widget/viewer/CMakeLists.txt | 4 + app/widget/viewer/audiowaveformview.cpp | 26 +- app/widget/viewer/audiowaveformview.h | 8 + app/widget/viewer/footageviewer.cpp | 25 +- app/widget/viewer/footageviewer.h | 2 + app/widget/viewer/viewer.cpp | 1308 ++++++++++++----- app/widget/viewer/viewer.h | 107 +- app/widget/viewer/viewerdisplay.cpp | 247 ++-- app/widget/viewer/viewerdisplay.h | 34 +- app/widget/viewer/vieweroutpututils.cpp | 76 + app/widget/viewer/vieweroutpututils.h | 103 ++ app/widget/viewer/viewerplaybacktimer.cpp | 45 + app/widget/viewer/viewerplaybacktimer.h | 49 + app/window/mainwindow/mainmenu.cpp | 34 +- app/window/mainwindow/mainstatusbar.cpp | 101 +- app/window/mainwindow/mainstatusbar.h | 18 +- app/window/mainwindow/mainwindow.cpp | 119 +- app/window/mainwindow/mainwindow.h | 30 +- app/window/mainwindow/mainwindowundo.cpp | 52 +- app/window/mainwindow/mainwindowundo.h | 43 +- 256 files changed, 12357 insertions(+), 4044 deletions(-) create mode 100644 app/common/colorcodingapp.cpp create mode 100644 app/common/colorcodingapp.h create mode 100644 app/common/configwrapper.h create mode 100644 app/common/debugapp.h create mode 100644 app/common/filefunctionsapp.cpp create mode 100644 app/common/hashstreamapp.cpp create mode 100644 app/common/htmlapp.cpp create mode 100644 app/common/htmlapp.h create mode 100644 app/common/nodevaluehandle.h create mode 100644 app/common/oakvaluehelper.h create mode 100644 app/common/qtutilsapp.cpp create mode 100644 app/common/undowrapper.h create mode 100644 app/common/xmlutilsapp.cpp create mode 100644 app/engineeventbridge.cpp create mode 100644 app/engineeventbridge.h create mode 100644 app/timeline/CMakeLists.txt create mode 100644 app/timeline/timelinecoordinate.cpp create mode 100644 app/timeline/timelinecoordinate.h create mode 100644 app/ui/icons/CMakeLists.txt create mode 100644 app/ui/icons/icons.cpp create mode 100644 app/ui/icons/icons.h create mode 100644 app/widget/keyframeview/keyframehandle.h create mode 100644 app/widget/manageddisplay/colorprocessorhandle.h create mode 100644 app/widget/timelinewidget/cliphandle.h create mode 100644 app/widget/timelinewidget/trackhandle.h create mode 100644 app/widget/timeruler/markerhandle.h create mode 100644 app/widget/timeruler/markerpainting.cpp create mode 100644 app/widget/timeruler/markerpainting.h create mode 100644 app/widget/viewer/vieweroutpututils.cpp create mode 100644 app/widget/viewer/vieweroutpututils.h create mode 100644 app/widget/viewer/viewerplaybacktimer.cpp create mode 100644 app/widget/viewer/viewerplaybacktimer.h 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 +#include "oakengine/encoding.h" + namespace olive { @@ -87,14 +90,17 @@ ExportAudioTab::ExportAudioTab(QWidget *parent) outer_layout->addStretch(); } -int ExportAudioTab::set_format(ExportFormat::Format format) +int ExportAudioTab::set_format(int format) { - QList acodecs = ExportFormat::get_audio_codecs(format); - setEnabled(!acodecs.isEmpty()); + const int acodec_count = oakengine_encoding_format_audio_codec_count(format); + setEnabled(acodec_count > 0); codec_combobox_->blockSignals(true); codec_combobox_->clear(); - foreach (ExportCodec::Codec acodec, acodecs) { - codec_combobox_->addItem(ExportCodec::get_codec_name(acodec), acodec); + for (int i = 0; i < acodec_count; i++) { + int codec = oakengine_encoding_format_audio_codec_at(format, i); + char buf[256]; + oakengine_encoding_codec_name(codec, buf, sizeof(buf)); + codec_combobox_->addItem(QString::fromUtf8(buf), codec); } codec_combobox_->blockSignals(false); fmt_ = format; @@ -102,18 +108,25 @@ int ExportAudioTab::set_format(ExportFormat::Format format) update_sample_formats(); update_bit_rate_enabled(); - return acodecs.size(); + return acodec_count; } void ExportAudioTab::update_sample_formats() { - auto fmts = ExportFormat::get_sample_formats_for_codec(fmt_, get_codec()); + // Use oakengine to get sample format values and build the vector + const int count = oakengine_encoding_sample_format_count(fmt_, get_codec()); + std::vector fmts; + fmts.reserve(count); + for (int i = 0; i < count; i++) { + int val = oakengine_encoding_sample_format_at(fmt_, get_codec(), i); + fmts.push_back(olive::core::SampleFormat(static_cast(val))); + } sample_format_combobox_->set_available_formats(fmts); } void ExportAudioTab::update_bit_rate_enabled() { - bool uses_bitrate = !ExportCodec::is_codec_lossless(get_codec()); + bool uses_bitrate = !oakengine_encoding_codec_is_lossless(get_codec()); bit_rate_slider_->setEnabled(uses_bitrate); if (!uses_bitrate) { diff --git a/app/dialog/export/exportaudiotab.h b/app/dialog/export/exportaudiotab.h index 36f9e1fa4..e48fa305b 100644 --- a/app/dialog/export/exportaudiotab.h +++ b/app/dialog/export/exportaudiotab.h @@ -26,7 +26,6 @@ #include #include "common/define.h" -#include "codec/exportformat.h" #include "widget/slider/integerslider.h" #include "widget/standardcombos/standardcombos.h" @@ -38,13 +37,12 @@ class ExportAudioTab : public QWidget { public: ExportAudioTab(QWidget *parent = nullptr); - ExportCodec::Codec get_codec() const + int get_codec() const { - return static_cast( - codec_combobox_->currentData().toInt()); + return codec_combobox_->currentData().toInt(); } - void set_codec(ExportCodec::Codec c) + void set_codec(int c) { for (int i = 0; i < codec_combobox_->count(); i++) { if (codec_combobox_->itemData(i) == c) { @@ -75,10 +73,10 @@ public: } public slots: - int set_format(ExportFormat::Format format); + int set_format(int format); private: - ExportFormat::Format fmt_; + int fmt_; QComboBox *codec_combobox_; SampleRateComboBox *sample_rate_combobox_; ChannelLayoutComboBox *channel_layout_combobox_; diff --git a/app/dialog/export/exportformatcombobox.cpp b/app/dialog/export/exportformatcombobox.cpp index f84f19065..f12917535 100644 --- a/app/dialog/export/exportformatcombobox.cpp +++ b/app/dialog/export/exportformatcombobox.cpp @@ -24,6 +24,7 @@ #include #include +#include "oakengine/encoding.h" #include "ui/icons/icons.h" namespace olive @@ -32,6 +33,10 @@ namespace olive ExportFormatComboBox::ExportFormatComboBox(Mode mode, QWidget *parent) : QComboBox(parent) { + // The invalid placeholder format is the format count itself + // (ExportFormat::k_format_count), not -1. + current_ = oakengine_encoding_format_count(); + custom_menu_ = new Menu(this); // Populate combobox formats @@ -69,43 +74,45 @@ void ExportFormatComboBox::showPopup() custom_menu_->exec(mapToGlobal(QPoint(0, 0))); } -void ExportFormatComboBox::set_format(ExportFormat::Format fmt) +void ExportFormatComboBox::set_format(int fmt) { current_ = fmt; clear(); - addItem(ExportFormat::get_name(current_)); + char buf[256]; + oakengine_encoding_format_name(fmt, buf, sizeof(buf)); + addItem(QString::fromUtf8(buf)); } void ExportFormatComboBox::handle_index_change(QAction *a) { - ExportFormat::Format f = - static_cast(a->data().toInt()); + int f = a->data().toInt(); set_format(f); emit format_changed(f); } void ExportFormatComboBox::populate_type(Track::Type type) { - for (int i = 0; i < ExportFormat::k_format_count; i++) { - ExportFormat::Format f = static_cast(i); + const int fmt_count = oakengine_encoding_format_count(); + for (int i = 0; i < fmt_count; i++) { + int f = i; + char buf[256]; - if (type == Track::k_video && - !ExportFormat::get_video_codecs(f).isEmpty()) { + bool has_video = oakengine_encoding_format_video_codec_count(f) > 0; + bool has_audio = oakengine_encoding_format_audio_codec_count(f) > 0; + bool has_sub = oakengine_encoding_format_subtitle_codec_count(f) > 0; + + if (type == Track::k_video && has_video) { // Do nothing - } else if (type == Track::k_audio && - ExportFormat::get_video_codecs(f).isEmpty() && - !ExportFormat::get_audio_codecs(f).isEmpty()) { + } else if (type == Track::k_audio && !has_video && has_audio) { // Do nothing - } else if (type == Track::k_subtitle && - ExportFormat::get_video_codecs(f).isEmpty() && - ExportFormat::get_audio_codecs(f).isEmpty() && - !ExportFormat::get_subtitle_codecs(f).isEmpty()) { + } else if (type == Track::k_subtitle && !has_video && !has_audio && has_sub) { // Do nothing } else { continue; } - QString format_name = ExportFormat::get_name(f); + oakengine_encoding_format_name(f, buf, sizeof(buf)); + QString format_name = QString::fromUtf8(buf); QAction *a = custom_menu_->addAction(format_name); a->setData(i); diff --git a/app/dialog/export/exportformatcombobox.h b/app/dialog/export/exportformatcombobox.h index 2a5ca29e5..422b552f7 100644 --- a/app/dialog/export/exportformatcombobox.h +++ b/app/dialog/export/exportformatcombobox.h @@ -25,7 +25,6 @@ #include #include -#include "codec/exportformat.h" #include "node/output/track/track.h" #include "widget/menu/menu.h" @@ -48,7 +47,7 @@ public: { } - ExportFormat::Format get_format() const + int get_format() const { return current_; } @@ -56,10 +55,10 @@ public: void showPopup(); signals: - void format_changed(ExportFormat::Format fmt); + void format_changed(int fmt); public slots: - void set_format(ExportFormat::Format fmt); + void set_format(int fmt); private slots: void handle_index_change(QAction *a); @@ -71,7 +70,7 @@ private: Menu *custom_menu_; - ExportFormat::Format current_ = ExportFormat::k_format_count; + int current_ = -1; // was ExportFormat::k_format_count }; } diff --git a/app/dialog/export/exportsavepresetdialog.cpp b/app/dialog/export/exportsavepresetdialog.cpp index 71494cfeb..c3a28b283 100644 --- a/app/dialog/export/exportsavepresetdialog.cpp +++ b/app/dialog/export/exportsavepresetdialog.cpp @@ -22,6 +22,7 @@ #include "exportsavepresetdialog.h" #include +#include #include #include #include @@ -29,7 +30,7 @@ namespace olive { -ExportSavePresetDialog::ExportSavePresetDialog(const EncodingParams &p, +ExportSavePresetDialog::ExportSavePresetDialog(const OakEngineEncodingParams *p, QWidget *parent) : QDialog(parent) , params_(p) @@ -39,7 +40,17 @@ ExportSavePresetDialog::ExportSavePresetDialog(const EncodingParams &p, name_edit_ = new QLineEdit(); // Populate existing list - 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)); + } + } + } if (!l.empty()) { auto list_widget = new QListWidget(); for (const QString &f : l) { @@ -78,13 +89,17 @@ void ExportSavePresetDialog::accept() return; } - QDir d(EncodingParams::get_preset_path()); + char preset_path_buf[1024]; + preset_path_buf[0] = '\0'; + oakengine_encoding_preset_path( + preset_path_buf, static_cast(sizeof(preset_path_buf))); + QDir d(QString::fromUtf8(preset_path_buf)); + if (!d.exists()) { d.mkpath(QStringLiteral(".")); } - QFile f(d.filePath(name_edit_->text())); - if (f.exists()) { + if (d.exists(name_edit_->text())) { if (QMessageBox::question( this, tr("Overwrite Preset"), tr("A preset with the name \"%1\" already exists. Do you wish to overwrite it?") @@ -94,17 +109,18 @@ void ExportSavePresetDialog::accept() } } - if (!f.open(QFile::WriteOnly)) { + const QByteArray full_path = + d.filePath(name_edit_->text()).toUtf8(); + const int rc = oakengine_encoding_params_save_file( + params_, full_path.constData()); + if (rc != OAKENGINE_OK) { QMessageBox::critical( this, tr("Write Error"), - tr("Failed to open file \"%1\" for writing.").arg(f.fileName())); + tr("Failed to save preset to \"%1\".").arg( + QString::fromUtf8(full_path))); return; } - params_.save(&f); - - f.close(); - QDialog::accept(); } diff --git a/app/dialog/export/exportsavepresetdialog.h b/app/dialog/export/exportsavepresetdialog.h index 682364831..2848b8934 100644 --- a/app/dialog/export/exportsavepresetdialog.h +++ b/app/dialog/export/exportsavepresetdialog.h @@ -26,7 +26,7 @@ #include #include -#include "codec/encoder.h" +#include "oakengine/encoding.h" namespace olive { @@ -34,7 +34,7 @@ namespace olive class ExportSavePresetDialog : public QDialog { Q_OBJECT public: - ExportSavePresetDialog(const EncodingParams &p, QWidget *parent = nullptr); + ExportSavePresetDialog(const OakEngineEncodingParams *p, QWidget *parent = nullptr); QString get_selected_preset_name() const { @@ -47,7 +47,7 @@ public slots: private: QLineEdit *name_edit_; - EncodingParams params_; + const OakEngineEncodingParams *params_; }; } diff --git a/app/dialog/export/exportsubtitlestab.cpp b/app/dialog/export/exportsubtitlestab.cpp index 257f01463..550d1e3e3 100644 --- a/app/dialog/export/exportsubtitlestab.cpp +++ b/app/dialog/export/exportsubtitlestab.cpp @@ -1,25 +1,9 @@ -/* - * 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 "exportsubtitlestab.h" #include +#include "oakengine/encoding.h" + namespace olive { @@ -62,32 +46,35 @@ ExportSubtitlesTab::ExportSubtitlesTab(QWidget *parent) &QWidget::setVisible); } -int ExportSubtitlesTab::set_format(ExportFormat::Format format) +int ExportSubtitlesTab::set_format(int format) { - auto vcodecs = ExportFormat::get_video_codecs(format); - auto acodecs = ExportFormat::get_audio_codecs(format); + const bool has_video = oakengine_encoding_format_video_codec_count(format) > 0; + const bool has_audio = oakengine_encoding_format_audio_codec_count(format) > 0; + int scodec_count = oakengine_encoding_format_subtitle_codec_count(format); - auto scodecs = ExportFormat::get_subtitle_codecs(format); - - if (!scodecs.empty() && vcodecs.empty() && acodecs.empty()) { + if (scodec_count > 0 && !has_video && !has_audio) { // If format supports ONLY scodecs, default this to off and disable it sidecar_checkbox_->setChecked(false); sidecar_checkbox_->setEnabled(false); } else { // If format does not support scodecs, default this to checked and disable it - sidecar_checkbox_->setChecked(scodecs.empty()); - sidecar_checkbox_->setEnabled(!scodecs.empty()); + sidecar_checkbox_->setChecked(scodec_count == 0); + sidecar_checkbox_->setEnabled(scodec_count > 0); } - scodecs = - ExportFormat::get_subtitle_codecs(sidecar_format_combobox_->get_format()); + // Refresh for sidecar format + int sidecar_fmt = sidecar_format_combobox_->get_format(); + scodec_count = oakengine_encoding_format_subtitle_codec_count(sidecar_fmt); codec_combobox_->clear(); - foreach (ExportCodec::Codec scodec, scodecs) { - codec_combobox_->addItem(ExportCodec::get_codec_name(scodec), scodec); + for (int i = 0; i < scodec_count; i++) { + int scodec = oakengine_encoding_format_subtitle_codec_at(sidecar_fmt, i); + char buf[256]; + oakengine_encoding_codec_name(scodec, buf, sizeof(buf)); + codec_combobox_->addItem(QString::fromUtf8(buf), scodec); } - return scodecs.size(); + return scodec_count; } } diff --git a/app/dialog/export/exportsubtitlestab.h b/app/dialog/export/exportsubtitlestab.h index 03fa7eeb4..1c0e55e42 100644 --- a/app/dialog/export/exportsubtitlestab.h +++ b/app/dialog/export/exportsubtitlestab.h @@ -26,7 +26,6 @@ #include #include -#include "codec/exportformat.h" #include "common/qtutils.h" #include "dialog/export/exportformatcombobox.h" @@ -47,24 +46,23 @@ public: sidecar_checkbox_->setChecked(e); } - ExportFormat::Format get_sidecar_format() const + int get_sidecar_format() const { return sidecar_format_combobox_->get_format(); } - void set_sidecar_format(ExportFormat::Format f) + void set_sidecar_format(int f) { sidecar_format_combobox_->set_format(f); } - int set_format(ExportFormat::Format format); + int set_format(int format); - ExportCodec::Codec get_subtitle_codec() + int get_subtitle_codec() { - return static_cast( - codec_combobox_->currentData().toInt()); + return codec_combobox_->currentData().toInt(); } - void set_subtitle_codec(ExportCodec::Codec c) + void set_subtitle_codec(int c) { QtUtils::set_combo_box_data(codec_combobox_, c); } diff --git a/app/dialog/export/exportvideotab.cpp b/app/dialog/export/exportvideotab.cpp index eb283a89d..56657bfdb 100644 --- a/app/dialog/export/exportvideotab.cpp +++ b/app/dialog/export/exportvideotab.cpp @@ -29,15 +29,16 @@ #include "exportadvancedvideodialog.h" #include "node/color/colormanager/colormanager.h" +#include "oakengine/encoding.h" namespace olive { -ExportVideoTab::ExportVideoTab(ColorManager *color_manager, QWidget *parent) +ExportVideoTab::ExportVideoTab(OakEngineColorManager *color_manager, QWidget *parent) : QWidget(parent) , color_manager_(color_manager) , threads_(0) - , color_range_(VideoParams::k_color_range_default) + , color_range_(0) // k_color_range_default { QVBoxLayout *outer_layout = new QVBoxLayout(this); @@ -50,17 +51,20 @@ ExportVideoTab::ExportVideoTab(ColorManager *color_manager, QWidget *parent) outer_layout->addStretch(); } -int ExportVideoTab::set_format(ExportFormat::Format format) +int ExportVideoTab::set_format(int format) { format_ = format; - QList vcodecs = ExportFormat::get_video_codecs(format); - setEnabled(!vcodecs.isEmpty()); + const int vcodec_count = oakengine_encoding_format_video_codec_count(format); + setEnabled(vcodec_count > 0); codec_combobox()->clear(); - foreach (ExportCodec::Codec vcodec, vcodecs) { - codec_combobox()->addItem(ExportCodec::get_codec_name(vcodec), vcodec); + for (int i = 0; i < vcodec_count; i++) { + int vcodec = oakengine_encoding_format_video_codec_at(format, i); + char buf[256]; + oakengine_encoding_codec_name(vcodec, buf, sizeof(buf)); + codec_combobox()->addItem(QString::fromUtf8(buf), vcodec); } - return vcodecs.size(); + return vcodec_count; } bool ExportVideoTab::is_image_sequence_set() const @@ -116,9 +120,9 @@ QWidget *ExportVideoTab::setup_resolution_section() scaling_method_combobox_ = new QComboBox(); scaling_method_combobox_->setEnabled(false); - scaling_method_combobox_->addItem(tr("Fit"), EncodingParams::k_fit); - scaling_method_combobox_->addItem(tr("Stretch"), EncodingParams::k_stretch); - scaling_method_combobox_->addItem(tr("Crop"), EncodingParams::k_crop); + scaling_method_combobox_->addItem(tr("Fit"), OAKENGINE_ENCODING_SCALING_FIT); + scaling_method_combobox_->addItem(tr("Stretch"), OAKENGINE_ENCODING_SCALING_STRETCH); + scaling_method_combobox_->addItem(tr("Crop"), OAKENGINE_ENCODING_SCALING_CROP); layout->addWidget(scaling_method_combobox_, row, 1); // Automatically enable/disable the scaling method depending on maintain aspect ratio @@ -223,9 +227,14 @@ void ExportVideoTab::maintain_aspect_ratio_changed(bool val) void ExportVideoTab::open_advanced_dialog() { - // Find export formats compatible with this encoder - QStringList pixel_formats = - ExportFormat::get_pixel_formats_for_codec(format_, get_selected_codec()); + // Find pixel formats compatible with this encoder + QStringList pixel_formats; + const int pix_count = oakengine_encoding_pix_fmt_count(format_, get_selected_codec()); + for (int i = 0; i < pix_count; i++) { + char buf[64]; + oakengine_encoding_pix_fmt_at(format_, get_selected_codec(), i, buf, sizeof(buf)); + pixel_formats.append(QString::fromUtf8(buf)); + } ExportAdvancedVideoDialog d(pixel_formats, this); @@ -256,30 +265,35 @@ void ExportVideoTab::update_frame_rate(Rational r) void ExportVideoTab::video_codec_changed() { - ExportCodec::Codec codec = get_selected_codec(); + int codec = get_selected_codec(); switch (codec) { - case ExportCodec::k_codec_h264: - case ExportCodec::k_codec_h264rgb: + case OAKENGINE_ENCODING_CODEC_H264: + case OAKENGINE_ENCODING_CODEC_H264RGB: set_codec_section(h264_section_); break; - case ExportCodec::k_codec_h265: + case OAKENGINE_ENCODING_CODEC_H265: set_codec_section(h265_section_); break; - case ExportCodec::k_codec_a_v1: + case OAKENGINE_ENCODING_CODEC_AV1: set_codec_section(av1_section_); break; - case ExportCodec::k_codec_cineform: + case OAKENGINE_ENCODING_CODEC_CINEFORM: set_codec_section(cineform_section_); break; default: set_codec_section( - ExportCodec::is_codec_a_still_image(codec) ? image_section_ : nullptr); + oakengine_encoding_codec_is_still_image(codec) ? image_section_ : nullptr); } // Set default pixel format - QStringList pix_fmts = - ExportFormat::get_pixel_formats_for_codec(format_, codec); + QStringList pix_fmts; + const int pix_count = oakengine_encoding_pix_fmt_count(format_, codec); + for (int i = 0; i < pix_count; i++) { + char buf[64]; + oakengine_encoding_pix_fmt_at(format_, codec, i, buf, sizeof(buf)); + pix_fmts.append(QString::fromUtf8(buf)); + } if (!pix_fmts.isEmpty()) { pix_fmt_ = pix_fmts.first(); } else { diff --git a/app/dialog/export/exportvideotab.h b/app/dialog/export/exportvideotab.h index e18826710..1daeea547 100644 --- a/app/dialog/export/exportvideotab.h +++ b/app/dialog/export/exportvideotab.h @@ -32,8 +32,9 @@ #include "dialog/export/codec/codecstack.h" #include "dialog/export/codec/h264section.h" #include "dialog/export/codec/imagesection.h" -#include "node/color/colormanager/colormanager.h" +#include "oakengine/color.h" #include "widget/colorwheel/colorspacechooser.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/slider/integerslider.h" #include "widget/standardcombos/standardcombos.h" @@ -43,9 +44,9 @@ namespace olive class ExportVideoTab : public QWidget { Q_OBJECT public: - ExportVideoTab(ColorManager *color_manager, QWidget *parent = nullptr); + ExportVideoTab(OakEngineColorManager *color_manager, QWidget *parent = nullptr); - int set_format(ExportFormat::Format format); + int set_format(int format); bool is_image_sequence_set() const; void set_image_sequence(bool e) const; @@ -55,13 +56,12 @@ public: return image_section_->get_time(); } - ExportCodec::Codec get_selected_codec() const + int get_selected_codec() const { - return static_cast( - codec_combobox()->currentData().toInt()); + return codec_combobox()->currentData().toInt(); } - void set_selected_codec(ExportCodec::Codec c) + void set_selected_codec(int c) { QtUtils::set_combo_box_data(codec_combobox(), c); } @@ -161,11 +161,11 @@ public: pix_fmt_ = s; } - VideoParams::ColorRange color_range() const + int color_range() const { return color_range_; } - void set_color_range(VideoParams::ColorRange c) + void set_color_range(int c) { color_range_ = c; } @@ -204,7 +204,7 @@ private: IntegerSlider *width_slider_; IntegerSlider *height_slider_; - ColorManager *color_manager_; + OakEngineColorManager *color_manager_; InterlacedComboBox *interlaced_combobox_; PixelAspectRatioComboBox *pixel_aspect_combobox_; @@ -213,9 +213,9 @@ private: int threads_; QString pix_fmt_; - VideoParams::ColorRange color_range_; + int color_range_; - ExportFormat::Format format_; + int format_; private slots: void maintain_aspect_ratio_changed(bool val); diff --git a/app/dialog/footageproperties/footageproperties.cpp b/app/dialog/footageproperties/footageproperties.cpp index adc35bbc0..740eada19 100644 --- a/app/dialog/footageproperties/footageproperties.cpp +++ b/app/dialog/footageproperties/footageproperties.cpp @@ -34,11 +34,13 @@ #include #include "core.h" -#include "node/nodeundo.h" #include "oakengine/footage.h" #include "oakengine/node.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" #include "streamproperties/audiostreamproperties.h" #include "streamproperties/videostreamproperties.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -131,28 +133,43 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, QString description; bool is_enabled = false; + OakEngineFootage *facade_handle = oakengine_footage_borrow( + reinterpret_cast(footage_)); + switch (reference.type()) { case Track::k_video: { stacked_widget_->addWidget( new VideoStreamProperties(footage_, reference.index())); - VideoParams vp = footage_->get_video_params(reference.index()); + VideoParams vp = viewer_output_video_params(footage_, reference.index()); is_enabled = vp.enabled(); - description = Footage::describe_video_stream(vp); + { + char desc_buf[256]; + oakengine_footage_describe_video_stream( + facade_handle, reference.index(), desc_buf, + sizeof(desc_buf)); + description = QString::fromUtf8(desc_buf); + } break; } case Track::k_audio: { stacked_widget_->addWidget( new AudioStreamProperties(footage_, reference.index())); - AudioParams ap = footage_->get_audio_params(reference.index()); + AudioParams ap = viewer_output_audio_params(footage_, reference.index()); is_enabled = ap.enabled(); - description = Footage::describe_audio_stream(ap); + { + char desc_buf[256]; + oakengine_footage_describe_audio_stream( + facade_handle, reference.index(), desc_buf, + sizeof(desc_buf)); + description = QString::fromUtf8(desc_buf); + } break; } case Track::k_subtitle: { - SubtitleParams sp = footage_->get_subtitle_params(reference.index()); - is_enabled = sp.enabled(); + is_enabled = oakengine_footage_get_stream_enabled( + facade_handle, OAKENGINE_TRACK_TYPE_SUBTITLE, reference.index()); // FIXME: Language? description = tr("Subtitles"); @@ -164,6 +181,8 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, break; } + oakengine_footage_free(facade_handle); + QListWidgetItem *item = new QListWidgetItem(description, track_list_); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(is_enabled ? Qt::Checked : Qt::Unchecked); @@ -244,16 +263,16 @@ void FootagePropertiesDialog::accept() switch (reference.type()) { case Track::k_video: - old_stream_enabled = - footage_->get_video_params(reference.index()).enabled(); + old_stream_enabled = oakengine_footage_get_stream_enabled( + facade_handle, OAKENGINE_TRACK_TYPE_VIDEO, reference.index()); break; case Track::k_audio: - old_stream_enabled = - footage_->get_audio_params(reference.index()).enabled(); + old_stream_enabled = oakengine_footage_get_stream_enabled( + facade_handle, OAKENGINE_TRACK_TYPE_AUDIO, reference.index()); break; case Track::k_subtitle: - old_stream_enabled = - footage_->get_subtitle_params(reference.index()).enabled(); + old_stream_enabled = oakengine_footage_get_stream_enabled( + facade_handle, OAKENGINE_TRACK_TYPE_SUBTITLE, reference.index()); break; case Track::k_none: case Track::k_count: @@ -269,12 +288,12 @@ void FootagePropertiesDialog::accept() oakengine_footage_free(facade_handle); - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); for (int i = 0; i < stacked_widget_->count(); i++) { static_cast(stacked_widget_->widget(i)) ->accept(command); } - delete command; // stream pages write through the facade directly + oakengine_undo_command_free(command); // stream pages write through the facade directly QDialog::accept(); } diff --git a/app/dialog/footageproperties/footageproperties.h b/app/dialog/footageproperties/footageproperties.h index 6138270b3..6b7192105 100644 --- a/app/dialog/footageproperties/footageproperties.h +++ b/app/dialog/footageproperties/footageproperties.h @@ -31,7 +31,6 @@ #include #include "node/project/footage/footage.h" -#include "undo/undocommand.h" namespace olive { diff --git a/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp index 80e3bcf8b..3fabe5533 100644 --- a/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp @@ -30,7 +30,7 @@ AudioStreamProperties::AudioStreamProperties(Footage *footage, int audio_index) { } -void AudioStreamProperties::accept(MultiUndoCommand *) +void AudioStreamProperties::accept(void *) { Q_UNUSED(footage_) Q_UNUSED(audio_index_) diff --git a/app/dialog/footageproperties/streamproperties/audiostreamproperties.h b/app/dialog/footageproperties/streamproperties/audiostreamproperties.h index a44c93f88..93f130776 100644 --- a/app/dialog/footageproperties/streamproperties/audiostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/audiostreamproperties.h @@ -32,7 +32,7 @@ class AudioStreamProperties : public StreamProperties { public: AudioStreamProperties(Footage *footage, int audio_index); - virtual void accept(MultiUndoCommand *parent) override; + virtual void accept(void *parent) override; private: Footage *footage_; diff --git a/app/dialog/footageproperties/streamproperties/streamproperties.h b/app/dialog/footageproperties/streamproperties/streamproperties.h index fa605e39d..4dc1a3ad6 100644 --- a/app/dialog/footageproperties/streamproperties/streamproperties.h +++ b/app/dialog/footageproperties/streamproperties/streamproperties.h @@ -25,7 +25,6 @@ #include #include "common/define.h" -#include "undo/undocommand.h" namespace olive { @@ -34,7 +33,7 @@ class StreamProperties : public QWidget { public: StreamProperties(QWidget *parent = nullptr); - virtual void accept(MultiUndoCommand *) + virtual void accept(void *) { } diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp index 4fa686f59..0c114be62 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -28,8 +28,12 @@ #include #include "node/project.h" +#include "oakengine/color.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "oakengine/footage.h" #include "oakengine/node.h" +#include "oakengine/viewer.h" +#include "oakengine/videoparams.h" namespace olive { @@ -46,7 +50,10 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) video_layout->addWidget(new QLabel(tr("Pixel Aspect:")), row, 0); - VideoParams vp = footage_->get_video_params(video_index_); + oak_video_params vpod; + oakengine_viewer_get_video_params( + reinterpret_cast(footage_), video_index_, + &vpod); // Stream override values come through the liboakengine C ABI facade; // layout-only conditions (channel count, video type) stay direct reads. @@ -85,10 +92,13 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) // The dropdown's color space list comes through the facade (same list // the engine's color config reports). + OakEngineColorManager *cm = oakengine_color_manager_from_project( + reinterpret_cast(footage_->project())); video_color_space_->addItem(tr("Default (%1)") - .arg(footage_->project() - ->color_manager() - ->get_default_input_color_space())); + .arg(oak_query_string([cm](char *buf, int size) { + return oakengine_color_manager_default_input_color_space( + cm, buf, size); + }))); const int colorspace_count = oakengine_footage_colorspace_count(facade_handle); @@ -110,14 +120,14 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) color_range_combo_ = new QComboBox(); color_range_combo_->addItem(tr("Limited (16-235)"), - VideoParams::k_color_range_limited); + 0); color_range_combo_->addItem(tr("Full (0-255)"), - VideoParams::k_color_range_full); + 1); color_range_combo_->setCurrentIndex(color_range); video_layout->addWidget(color_range_combo_, row, 1); - if (vp.channel_count() == VideoParams::k_rgba_channel_count) { + if (oakengine_video_params_internal_channel_count() == 4) { row++; video_premultiply_alpha_ = new QCheckBox(tr("Premultiplied Alpha")); @@ -127,7 +137,7 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) row++; - if (vp.video_type() == VideoParams::k_video_type_image_sequence) { + if (vpod.video_type == 2) { QGroupBox *imgseq_group = new QGroupBox(tr("Image Sequence")); QGridLayout *imgseq_layout = new QGridLayout(imgseq_group); @@ -169,7 +179,7 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) oakengine_footage_free(facade_handle); } -void VideoStreamProperties::accept(MultiUndoCommand *parent) +void VideoStreamProperties::accept(void *parent) { Q_UNUSED(parent) @@ -182,17 +192,40 @@ void VideoStreamProperties::accept(MultiUndoCommand *parent) set_colorspace = video_color_space_->currentText(); } - VideoParams vp = footage_->get_video_params(video_index_); + // Fetch current values through the facade (avoids the inline + // ViewerOutput::get_video_params() which references k_video_params_input). + char vp_colorspace[256]; + vp_colorspace[0] = '\0'; + int vp_color_range = 0, vp_interlacing = 0, vp_premultiplied = 0; + oakengine_footage_get_video_stream_overrides( + facade_handle, video_index_, vp_colorspace, sizeof(vp_colorspace), + &vp_color_range, &vp_interlacing, &vp_premultiplied); + + int vp_par_num = 1, vp_par_den = 1; + oakengine_footage_get_pixel_aspect(facade_handle, video_index_, + &vp_par_num, &vp_par_den); + + oak_video_params vpod; + oakengine_viewer_get_video_params( + reinterpret_cast(footage_), video_index_, + &vpod); + + int64_t vp_start_time = 0, vp_duration = 0; + int vp_fr_num = 0, vp_fr_den = 1; + oakengine_footage_get_image_sequence_params( + facade_handle, video_index_, &vp_start_time, &vp_duration, + &vp_fr_num, &vp_fr_den); // Write every override through the facade (each call is one undoable // command on the shared undo stack, replacing this dialog's own undo // command classes with identical semantics). if ((video_premultiply_alpha_ && - video_premultiply_alpha_->isChecked() != vp.premultiplied_alpha()) || - set_colorspace != vp.colorspace() || + video_premultiply_alpha_->isChecked() != (vp_premultiplied != 0)) || + set_colorspace != QString::fromUtf8(vp_colorspace) || static_cast( - video_interlace_combo_->currentIndex()) != vp.interlacing() || - color_range_combo_->currentData().toInt() != vp.color_range()) { + video_interlace_combo_->currentIndex()) != + static_cast(vp_interlacing) || + color_range_combo_->currentData().toInt() != vp_color_range) { oakengine_footage_set_video_stream_overrides( facade_handle, video_index_, set_colorspace.toUtf8().constData(), @@ -204,19 +237,19 @@ void VideoStreamProperties::accept(MultiUndoCommand *parent) } const Rational new_par = pixel_aspect_combo_->get_pixel_aspect_ratio(); - if (new_par != vp.pixel_aspect_ratio()) { + if (new_par != Rational(vp_par_num, vp_par_den)) { oakengine_footage_set_pixel_aspect(facade_handle, video_index_, new_par.numerator(), new_par.denominator()); } - if (vp.video_type() == VideoParams::k_video_type_image_sequence) { + if (vpod.video_type == 2) { int64_t new_dur = imgseq_end_time_->get_value() - imgseq_start_time_->get_value() + 1; - if (vp.start_time() != imgseq_start_time_->get_value() || - vp.duration() != new_dur || - vp.frame_rate() != imgseq_frame_rate_->get_frame_rate()) { + if (vp_start_time != imgseq_start_time_->get_value() || + vp_duration != new_dur || + Rational(vp_fr_num, vp_fr_den) != imgseq_frame_rate_->get_frame_rate()) { const Rational fr = imgseq_frame_rate_->get_frame_rate(); oakengine_footage_set_image_sequence_params( facade_handle, video_index_, @@ -230,8 +263,11 @@ void VideoStreamProperties::accept(MultiUndoCommand *parent) bool VideoStreamProperties::sanity_check() { - if (footage_->get_video_params(video_index_).video_type() == - VideoParams::k_video_type_image_sequence) { + oak_video_params vpod; + oakengine_viewer_get_video_params( + reinterpret_cast(footage_), video_index_, + &vpod); + if (vpod.video_type == 2) { if (imgseq_start_time_->get_value() >= imgseq_end_time_->get_value()) { QMessageBox::critical( this, tr("Invalid Configuration"), diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h index e2f06c461..ef6e60883 100644 --- a/app/dialog/footageproperties/streamproperties/videostreamproperties.h +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -38,7 +38,7 @@ class VideoStreamProperties : public StreamProperties { public: VideoStreamProperties(Footage *footage, int video_index); - virtual void accept(MultiUndoCommand *parent) override; + virtual void accept(void *parent) override; virtual bool sanity_check() override; diff --git a/app/dialog/footagerelink/footagerelinkdialog.cpp b/app/dialog/footagerelink/footagerelinkdialog.cpp index b11c7a207..00d5e5458 100644 --- a/app/dialog/footagerelink/footagerelinkdialog.cpp +++ b/app/dialog/footagerelink/footagerelinkdialog.cpp @@ -183,7 +183,9 @@ void FootageRelinkDialog::browse_for_footage() new_dir.filePath(relative_to_original); if (QFileInfo::exists(absolute_to_new)) { - other_footage->set_filename(absolute_to_new); + oakengine_footage_relink( + reinterpret_cast(other_footage), + absolute_to_new.toUtf8().constData()); } } } diff --git a/app/dialog/markerproperties/markerpropertiesdialog.cpp b/app/dialog/markerproperties/markerpropertiesdialog.cpp index 4c5014a81..3190be235 100644 --- a/app/dialog/markerproperties/markerpropertiesdialog.cpp +++ b/app/dialog/markerproperties/markerpropertiesdialog.cpp @@ -29,6 +29,8 @@ #include "core.h" +#include "oakengine/timeline.h" + namespace olive { @@ -138,29 +140,29 @@ void MarkerPropertiesDialog::accept() return; } - MultiUndoCommand *command = new MultiUndoCommand(); - - int color = color_menu_->get_selected_color(); - - foreach (TimelineMarker *m, markers_) { - if (color != -1) { - command->add_child(new MarkerChangeColorCommand(m, color)); + // Batch-set properties via facade (one undoable command) + { + QVector oak_markers; + foreach (TimelineMarker *m, markers_) { + oak_markers.append(reinterpret_cast(m)); } - + int color = color_menu_->get_selected_color(); + QByteArray name_ba; + const char *name = nullptr; if (label_edit_->placeholderText().isEmpty()) { - command->add_child( - new MarkerChangeNameCommand(m, label_edit_->text())); + name_ba = label_edit_->text().toUtf8(); + name = name_ba.constData(); } + oakengine_marker_set_properties( + oak_markers.data(), oak_markers.size(), color, name, + (markers_.size() == 1) ? 1 : 0, + in_slider_->get_value().numerator(), + in_slider_->get_value().denominator(), + out_slider_->get_value().numerator(), + out_slider_->get_value().denominator(), + nullptr); } - if (markers_.size() == 1) { - command->add_child(new MarkerChangeTimeCommand( - markers_.front(), - TimeRange(in_slider_->get_value(), out_slider_->get_value()))); - } - - Core::instance()->undo_stack()->push(command, tr("Set Marker Properties")); - super::accept(); } diff --git a/app/dialog/preferences/preferences.cpp b/app/dialog/preferences/preferences.cpp index 89ec6a822..826e5248e 100644 --- a/app/dialog/preferences/preferences.cpp +++ b/app/dialog/preferences/preferences.cpp @@ -26,7 +26,7 @@ #include #include -#include "config/config.h" +#include "oakengine/config.h" #include "tabs/preferencesgeneraltab.h" #include "tabs/preferencesbehaviortab.h" #include "tabs/preferencesappearancetab.h" @@ -69,7 +69,7 @@ PreferencesDialog::PreferencesDialog(MainWindow *main_window, int start_tab) void PreferencesDialog::AcceptEvent() { - Config::save(); + oakengine_config_save(); } } diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.cpp b/app/dialog/preferences/tabs/preferencesappearancetab.cpp index d614ab1bb..ca2dbf073 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.cpp +++ b/app/dialog/preferences/tabs/preferencesappearancetab.cpp @@ -28,6 +28,7 @@ #include #include "node/node.h" +#include "oakengine/node.h" namespace olive { @@ -69,8 +70,9 @@ PreferencesAppearanceTab::PreferencesAppearanceTab() QGridLayout *color_layout = new QGridLayout(color_group); for (int i = 0; i < Node::k_category_count; i++) { - QString cat_name = - Node::get_category_name(static_cast(i)); + char cat_buf[256]; + oakengine_node_category_name(i, cat_buf, sizeof(cat_buf)); + QString cat_name = QString::fromUtf8(cat_buf); color_layout->addWidget(new QLabel(cat_name), i, 0); ColorCodingComboBox *ccc = new ColorCodingComboBox(); @@ -102,7 +104,7 @@ PreferencesAppearanceTab::PreferencesAppearanceTab() layout->addStretch(); } -void PreferencesAppearanceTab::accept(MultiUndoCommand *command) +void PreferencesAppearanceTab::accept(void *command) { Q_UNUSED(command) diff --git a/app/dialog/preferences/tabs/preferencesappearancetab.h b/app/dialog/preferences/tabs/preferencesappearancetab.h index bfe405b44..8a72c9289 100644 --- a/app/dialog/preferences/tabs/preferencesappearancetab.h +++ b/app/dialog/preferences/tabs/preferencesappearancetab.h @@ -38,7 +38,7 @@ class PreferencesAppearanceTab : public ConfigDialogBaseTab { public: PreferencesAppearanceTab(); - virtual void accept(MultiUndoCommand *command) override; + virtual void accept(void *command) override; private: /** diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.cpp b/app/dialog/preferences/tabs/preferencesaudiotab.cpp index 74b079dab..be935b0c1 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.cpp +++ b/app/dialog/preferences/tabs/preferencesaudiotab.cpp @@ -25,8 +25,9 @@ #include #include -#include "audio/audiomanager.h" -#include "config/config.h" +#include "oakengine/audio.h" +#include +#include "common/configwrapper.h" namespace olive { @@ -171,13 +172,13 @@ PreferencesAudioTab::PreferencesAudioTab() new ExportFormatComboBox(ExportFormatComboBox::k_show_audio_only); record_format_combo_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - record_format_combo_->set_format(static_cast( + record_format_combo_->set_format(static_cast( OAK_CONFIG("AudioRecordingFormat").toInt())); fmt_layout->addWidget(record_format_combo_); record_options_ = new ExportAudioTab(); record_options_->set_format(record_format_combo_->get_format()); - record_options_->set_codec(static_cast( + record_options_->set_codec(static_cast( OAK_CONFIG("AudioRecordingCodec").toInt())); record_options_->sample_rate_combobox()->set_sample_rate( OAK_CONFIG("AudioRecordingSampleRate").toInt()); @@ -213,7 +214,7 @@ PreferencesAudioTab::PreferencesAudioTab() refresh_backends(); } -void PreferencesAudioTab::accept(MultiUndoCommand *command) +void PreferencesAudioTab::accept(void *command) { Q_UNUSED(command) @@ -228,8 +229,8 @@ void PreferencesAudioTab::accept(MultiUndoCommand *command) OAK_CONFIG("AudioInput") = audio_input_devices_->currentText(); // Set devices to be used from now on - AudioManager::instance()->set_output_device(output_device); - AudioManager::instance()->set_input_device(input_device); + oakengine_audio_set_output_device(output_device); + oakengine_audio_set_input_device(input_device); OAK_CONFIG("AudioOutputSampleRate") = output_rate_combo_->get_sample_rate(); OAK_CONFIG("AudioOutputChannelLayout") = @@ -251,7 +252,8 @@ void PreferencesAudioTab::accept(MultiUndoCommand *command) ->get_sample_format() .to_string()); - emit AudioManager::instance() -> output_params_changed(); + // AudioManager output params changed is handled internally by the facade + // when oakengine_audio_set_output_device() is called. OAK_CONFIG("AudioScrubbing") = audio_scrubbing_->isChecked(); } @@ -299,7 +301,7 @@ void PreferencesAudioTab::refresh_devices() void PreferencesAudioTab::hard_refresh_backends() { - AudioManager::instance()->hard_reset(); + oakengine_audio_hard_reset(); refresh_backends(); } @@ -307,9 +309,9 @@ void PreferencesAudioTab::attempt_to_set_devices_from_config() { // Load with currently active devices PaDeviceIndex current_output_index = - AudioManager::instance()->get_output_device(); + static_cast(oakengine_audio_get_output_device()); PaDeviceIndex current_input_index = - AudioManager::instance()->get_input_device(); + static_cast(oakengine_audio_get_input_device()); const PaDeviceInfo *current_output = nullptr, *current_input = nullptr; if (current_output_index != paNoDevice) { diff --git a/app/dialog/preferences/tabs/preferencesaudiotab.h b/app/dialog/preferences/tabs/preferencesaudiotab.h index ec37a7b61..201102e19 100644 --- a/app/dialog/preferences/tabs/preferencesaudiotab.h +++ b/app/dialog/preferences/tabs/preferencesaudiotab.h @@ -40,7 +40,7 @@ class PreferencesAudioTab : public ConfigDialogBaseTab { public: PreferencesAudioTab(); - virtual void accept(MultiUndoCommand *command) override; + virtual void accept(void *command) override; private: QComboBox *audio_backend_combobox_; diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp index e24008e4b..41c7e3abb 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.cpp +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.cpp @@ -23,7 +23,7 @@ #include -#include "config/config.h" +#include "common/configwrapper.h" namespace olive { @@ -114,7 +114,7 @@ PreferencesBehaviorTab::PreferencesBehaviorTab(Category category) } } -void PreferencesBehaviorTab::accept(MultiUndoCommand *command) +void PreferencesBehaviorTab::accept(void *command) { Q_UNUSED(command) diff --git a/app/dialog/preferences/tabs/preferencesbehaviortab.h b/app/dialog/preferences/tabs/preferencesbehaviortab.h index 978988e47..c01bd3b4d 100644 --- a/app/dialog/preferences/tabs/preferencesbehaviortab.h +++ b/app/dialog/preferences/tabs/preferencesbehaviortab.h @@ -45,7 +45,7 @@ public: PreferencesBehaviorTab(Category category); - virtual void accept(MultiUndoCommand *command) override; + virtual void accept(void *command) override; static QString behavior_pref_tr(const char *text) { diff --git a/app/dialog/preferences/tabs/preferencesdisktab.cpp b/app/dialog/preferences/tabs/preferencesdisktab.cpp index 188fe126e..29de9f0a4 100644 --- a/app/dialog/preferences/tabs/preferencesdisktab.cpp +++ b/app/dialog/preferences/tabs/preferencesdisktab.cpp @@ -29,16 +29,24 @@ #include #include "common/filefunctions.h" -#include "config/config.h" +#include "common/configwrapper.h" +#include "oakengine/disk.h" +#include "olive/core/core.h" namespace olive { PreferencesDiskTab::PreferencesDiskTab() { - // Get default disk cache folder - default_disk_cache_folder_ = - DiskManager::instance()->get_default_cache_folder(); + // Get default disk cache folder path + { + int len = oakengine_disk_get_default_cache_path(nullptr, 0); + if (len > 0) { + QByteArray buf(len + 1, '\0'); + oakengine_disk_get_default_cache_path(buf.data(), buf.size()); + default_disk_cache_folder_ = QString::fromUtf8(buf.constData()); + } + } QVBoxLayout *outer_layout = new QVBoxLayout(this); @@ -54,7 +62,7 @@ PreferencesDiskTab::PreferencesDiskTab() row, 0); disk_cache_location_ = - new PathWidget(default_disk_cache_folder_->get_path()); + new PathWidget(default_disk_cache_folder_); disk_management_layout->addWidget(disk_cache_location_, row, 1); row++; @@ -62,8 +70,8 @@ PreferencesDiskTab::PreferencesDiskTab() QPushButton *disk_cache_settings_btn = new QPushButton(tr("Disk Cache Settings")); connect(disk_cache_settings_btn, &QPushButton::clicked, this, [this]() { - DiskManager::instance()->show_disk_cache_settings_dialog( - disk_cache_location_->text(), this); + oakengine_disk_show_settings_dialog( + disk_cache_location_->text().toUtf8().constData(), this); }); disk_management_layout->addWidget(disk_cache_settings_btn, row, 1); @@ -81,7 +89,7 @@ PreferencesDiskTab::PreferencesDiskTab() cache_ahead_slider_->set_format(tr("%1 seconds")); cache_ahead_slider_->set_minimum(0); cache_ahead_slider_->set_value( - OAK_CONFIG("DiskCacheAhead").value().to_double()); + OAK_CONFIG("DiskCacheAhead").value().to_double()); cache_behavior_layout->addWidget(cache_ahead_slider_, row, 1); cache_behavior_layout->addWidget(new QLabel(tr("Cache Behind:")), row, 2); @@ -90,7 +98,7 @@ PreferencesDiskTab::PreferencesDiskTab() cache_behind_slider_->set_minimum(0); cache_behind_slider_->set_format(tr("%1 seconds")); cache_behind_slider_->set_value( - OAK_CONFIG("DiskCacheBehind").value().to_double()); + OAK_CONFIG("DiskCacheBehind").value().to_double()); cache_behavior_layout->addWidget(cache_behind_slider_, row, 3); row++; @@ -171,11 +179,11 @@ PreferencesDiskTab::PreferencesDiskTab() bool PreferencesDiskTab::validate() { - if (disk_cache_location_->text() != default_disk_cache_folder_->get_path()) { + if (disk_cache_location_->text() != default_disk_cache_folder_) { // Disk cache location is changing // Check if the user is okay with invalidating the current cache - if (!DiskManager::show_disk_cache_change_confirmation_dialog(this)) { + if (!oakengine_disk_show_change_confirmation_dialog(this)) { return false; } @@ -191,18 +199,19 @@ bool PreferencesDiskTab::validate() return true; } -void PreferencesDiskTab::accept(MultiUndoCommand *command) +void PreferencesDiskTab::accept(void *command) { Q_UNUSED(command) - if (disk_cache_location_->text() != default_disk_cache_folder_->get_path()) { - default_disk_cache_folder_->set_path(disk_cache_location_->text()); + if (disk_cache_location_->text() != default_disk_cache_folder_) { + oakengine_disk_set_default_cache_path( + disk_cache_location_->text().toUtf8().constData()); } OAK_CONFIG("DiskCacheBehind") = QVariant::fromValue( - Rational::from_double(cache_behind_slider_->get_value())); + core::Rational::from_double(cache_behind_slider_->get_value())); OAK_CONFIG("DiskCacheAhead") = QVariant::fromValue( - Rational::from_double(cache_ahead_slider_->get_value())); + core::Rational::from_double(cache_ahead_slider_->get_value())); OAK_CONFIG("ProxyWidth") = static_cast(proxy_width_slider_->get_value()); diff --git a/app/dialog/preferences/tabs/preferencesdisktab.h b/app/dialog/preferences/tabs/preferencesdisktab.h index 0cd631d1f..645562111 100644 --- a/app/dialog/preferences/tabs/preferencesdisktab.h +++ b/app/dialog/preferences/tabs/preferencesdisktab.h @@ -28,7 +28,7 @@ #include #include "dialog/configbase/configdialogbase.h" -#include "render/diskmanager.h" +#include "oakengine/disk.h" #include "widget/slider/floatslider.h" #include "widget/slider/integerslider.h" #include "widget/path/pathwidget.h" @@ -43,7 +43,7 @@ public: virtual bool validate() override; - virtual void accept(MultiUndoCommand *command) override; + virtual void accept(void *command) override; private: PathWidget *disk_cache_location_; @@ -52,7 +52,7 @@ private: FloatSlider *cache_behind_slider_; - DiskCacheFolder *default_disk_cache_folder_; + QString default_disk_cache_folder_; IntegerSlider *proxy_width_slider_; IntegerSlider *proxy_height_slider_; diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp index 1d3bc1554..e033fcbb5 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.cpp +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.cpp @@ -198,7 +198,7 @@ PreferencesGeneralTab::PreferencesGeneralTab() layout->addStretch(); } -void PreferencesGeneralTab::accept(MultiUndoCommand *command) +void PreferencesGeneralTab::accept(void *command) { Q_UNUSED(command) diff --git a/app/dialog/preferences/tabs/preferencesgeneraltab.h b/app/dialog/preferences/tabs/preferencesgeneraltab.h index f3c09fa33..b99fe939a 100644 --- a/app/dialog/preferences/tabs/preferencesgeneraltab.h +++ b/app/dialog/preferences/tabs/preferencesgeneraltab.h @@ -39,7 +39,7 @@ class PreferencesGeneralTab : public ConfigDialogBaseTab { public: PreferencesGeneralTab(); - virtual void accept(MultiUndoCommand *command) override; + virtual void accept(void *command) override; private: void add_language(const QString &locale_name); diff --git a/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp b/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp index 255e60b23..d968a9c69 100644 --- a/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp +++ b/app/dialog/preferences/tabs/preferenceskeyboardtab.cpp @@ -81,7 +81,7 @@ PreferencesKeyboardTab::PreferencesKeyboardTab(MainWindow *main_window) setup_kbd_shortcuts(main_window_->menuBar()); } -void PreferencesKeyboardTab::accept(MultiUndoCommand *command) +void PreferencesKeyboardTab::accept(void *command) { Q_UNUSED(command) diff --git a/app/dialog/preferences/tabs/preferenceskeyboardtab.h b/app/dialog/preferences/tabs/preferenceskeyboardtab.h index 3c10ac3a9..6afd56a06 100644 --- a/app/dialog/preferences/tabs/preferenceskeyboardtab.h +++ b/app/dialog/preferences/tabs/preferenceskeyboardtab.h @@ -38,7 +38,7 @@ class PreferencesKeyboardTab : public ConfigDialogBaseTab { public: PreferencesKeyboardTab(MainWindow *main_window); - virtual void accept(MultiUndoCommand *command) override; + virtual void accept(void *command) override; private slots: /** diff --git a/app/dialog/preferences/tabs/preferencesluttab.cpp b/app/dialog/preferences/tabs/preferencesluttab.cpp index 5cee0c6bd..8badca41a 100644 --- a/app/dialog/preferences/tabs/preferencesluttab.cpp +++ b/app/dialog/preferences/tabs/preferencesluttab.cpp @@ -26,8 +26,9 @@ #include #include #include +#include -#include "render/lutlibrary.h" +#include "oakengine/lut.h" namespace olive { @@ -46,7 +47,16 @@ PreferencesLutTab::PreferencesLutTab() "these locations when picking a LUT file."))); library_dirs_list_ = new QListWidget(); - library_dirs_list_->addItems(LUTLibrary::get_directories()); + { + int dir_count = oakengine_lut_directory_count(); + for (int i = 0; i < dir_count; i++) { + char buf[4096]; + int len = oakengine_lut_directory_at(i, buf, sizeof(buf)); + if (len > 0) { + library_dirs_list_->addItem(QString::fromUtf8(buf, len)); + } + } + } library_layout->addWidget(library_dirs_list_); QHBoxLayout *button_layout = new QHBoxLayout(); @@ -74,7 +84,7 @@ PreferencesLutTab::PreferencesLutTab() outer_layout->addStretch(); } -void PreferencesLutTab::accept(MultiUndoCommand *command) +void PreferencesLutTab::accept(void *command) { Q_UNUSED(command) @@ -83,7 +93,14 @@ void PreferencesLutTab::accept(MultiUndoCommand *command) dirs.append(library_dirs_list_->item(i)->text()); } - LUTLibrary::set_directories(dirs); + std::vector utf8_dirs; + std::vector cstr_dirs; + for (int i = 0; i < dirs.size(); i++) { + utf8_dirs.push_back(dirs[i].toUtf8()); + cstr_dirs.push_back(utf8_dirs.back().constData()); + } + oakengine_lut_set_directories(cstr_dirs.data(), + static_cast(cstr_dirs.size())); } } diff --git a/app/dialog/preferences/tabs/preferencesluttab.h b/app/dialog/preferences/tabs/preferencesluttab.h index feafa0f7c..1c5080423 100644 --- a/app/dialog/preferences/tabs/preferencesluttab.h +++ b/app/dialog/preferences/tabs/preferencesluttab.h @@ -33,7 +33,7 @@ class PreferencesLutTab : public ConfigDialogBaseTab { public: PreferencesLutTab(); - virtual void accept(MultiUndoCommand *command) override; + virtual void accept(void *command) override; private: QListWidget *library_dirs_list_; diff --git a/app/dialog/progress/pluginprogressdialogreporter.cpp b/app/dialog/progress/pluginprogressdialogreporter.cpp index ded2e38af..652f8a7ad 100644 --- a/app/dialog/progress/pluginprogressdialogreporter.cpp +++ b/app/dialog/progress/pluginprogressdialogreporter.cpp @@ -31,8 +31,10 @@ PluginProgressDialogReporter::PluginProgressDialogReporter( : dialog_(new ProgressDialog(message, title, nullptr)) { dialog_->setAttribute(Qt::WA_DeleteOnClose); - connect(dialog_, &ProgressDialog::cancelled, this, - &PluginProgressReporter::cancelled); + QObject::connect(dialog_, &ProgressDialog::cancelled, dialog_, [this]() { + cancelled_ = true; + set_cancelled(); + }); } PluginProgressDialogReporter::~PluginProgressDialogReporter() diff --git a/app/dialog/progress/pluginprogressdialogreporter.h b/app/dialog/progress/pluginprogressdialogreporter.h index 2f9a05da7..9aa932609 100644 --- a/app/dialog/progress/pluginprogressdialogreporter.h +++ b/app/dialog/progress/pluginprogressdialogreporter.h @@ -40,7 +40,6 @@ class ProgressDialog; * is destroyed by the engine with deleteLater(). */ class PluginProgressDialogReporter : public plugin::PluginProgressReporter { - Q_OBJECT public: PluginProgressDialogReporter(const QString &message, const QString &title); @@ -52,8 +51,11 @@ public: virtual void close() override; + bool was_cancelled() const { return cancelled_; } + private: QPointer dialog_; + bool cancelled_ = false; }; } diff --git a/app/dialog/projectproperties/projectproperties.cpp b/app/dialog/projectproperties/projectproperties.cpp index 2cbaf1058..6f047456c 100644 --- a/app/dialog/projectproperties/projectproperties.cpp +++ b/app/dialog/projectproperties/projectproperties.cpp @@ -30,8 +30,10 @@ #include #include "common/filefunctions.h" -#include "node/color/colormanager/colormanager.h" -#include "render/diskmanager.h" +#include "oakengine/color.h" +#include "oakengine/disk.h" +#include "oakengine/project.h" +#include "widget/manageddisplay/colorprocessorhandle.h" namespace olive { @@ -45,8 +47,12 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project *p, QWidget *parent) { QVBoxLayout *layout = new QVBoxLayout(this); + char name_buf[256]; + oakengine_project_name( + reinterpret_cast(working_project_), + name_buf, sizeof(name_buf)); setWindowTitle( - tr("Project Properties for '%1'").arg(working_project_->name())); + tr("Project Properties for '%1'").arg(name_buf)); QTabWidget *tabs = new QTabWidget; layout->addWidget(tabs); @@ -85,7 +91,14 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project *p, QWidget *parent) reference_space_->addItem(tr("Scene Linear"), ocio::ROLE_SCENE_LINEAR); reference_space_->addItem(tr("Compositing Log"), ocio::ROLE_COMPOSITING_LOG); - QtUtils::set_combo_box_data(reference_space_, p->get_color_reference_space()); + QtUtils::set_combo_box_data(reference_space_, + [p]() -> QString { + char buf[256]; + oakengine_project_get_color_reference_space( + reinterpret_cast(p), + buf, sizeof(buf)); + return QString::fromUtf8(buf); + }()); color_layout->addWidget(reference_space_, row, 1, 1, 2); row++; @@ -95,8 +108,11 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project *p, QWidget *parent) connect(browse_btn, &QPushButton::clicked, this, &ProjectPropertiesDialog::browse_for_ocio_config); - ocio_filename_->setText( - working_project_->color_manager()->get_config_filename()); + OakEngineColorManager *cm = oakengine_color_manager_from_project( + reinterpret_cast(working_project_)); + ocio_filename_->setText(oak_query_string([cm](char *buf, int size) { + return oakengine_color_manager_get_config_filename(cm, buf, size); + })); connect(ocio_filename_, &QLineEdit::textChanged, this, &ProjectPropertiesDialog::ocio_filename_updated); @@ -129,7 +145,14 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project *p, QWidget *parent) // Create custom cache path widget custom_cache_path_ = - new PathWidget(working_project_->get_custom_cache_path(), this); + new PathWidget( + [this]() -> QString { + char buf[4096]; + oakengine_project_get_custom_cache_path( + reinterpret_cast(working_project_), + buf, sizeof(buf)); + return QString::fromUtf8(buf); + }(), this); custom_cache_path_->setEnabled(false); cache_layout->addWidget(custom_cache_path_); @@ -139,7 +162,8 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(Project *p, QWidget *parent) &PathWidget::setEnabled); // Check the radio button that should currently be active - disk_cache_radios_[working_project_->get_cache_location_setting()] + disk_cache_radios_[oakengine_project_get_cache_location_setting( + reinterpret_cast(working_project_))] ->setChecked(true); // Add disk cache settings button @@ -181,7 +205,13 @@ void ProjectPropertiesDialog::accept() ->isChecked()) { // Ensure alongside project path is valid if (!verify_path_and_warn_if_bad( - working_project_->get_cache_alongside_project_path())) { + [this]() -> QString { + char buf[4096]; + oakengine_project_cache_alongside_path( + reinterpret_cast(working_project_), + buf, sizeof(buf)); + return QString::fromUtf8(buf); + }())) { return; } } else { @@ -191,33 +221,55 @@ void ProjectPropertiesDialog::accept() } } - if (custom_cache_path_->text() != working_project_->get_custom_cache_path()) { + if (custom_cache_path_->text() != + [this]() -> QString { + char buf[4096]; + oakengine_project_get_custom_cache_path( + reinterpret_cast(working_project_), + buf, sizeof(buf)); + return QString::fromUtf8(buf); + }()) { // Check if the user is okay with invalidating the current cache - if (!DiskManager::show_disk_cache_change_confirmation_dialog(this)) { + if (!oakengine_disk_show_change_confirmation_dialog(this)) { return; } - working_project_->set_custom_cache_path(custom_cache_path_->text()); + oakengine_project_set_custom_cache_path( + reinterpret_cast(working_project_), + custom_cache_path_->text().toUtf8().constData()); - emit DiskManager::instance() -> invalidate_project(working_project_); + oakengine_disk_invalidate_project( + reinterpret_cast(working_project_)); } // This should ripple changes throughout the graph/cache that the color config has changed, and // therefore should be done after the cache path is changed - if (working_project_->color_manager()->get_config_filename() != - ocio_filename_->text()) { - working_project_->color_manager()->set_config_filename( - ocio_filename_->text()); + OakEngineColorManager *cm = oakengine_color_manager_from_project( + reinterpret_cast(working_project_)); + QString old_config = oak_query_string([cm](char *buf, int size) { + return oakengine_color_manager_get_config_filename(cm, buf, size); + }); + QString old_input_cs = oak_query_string([cm](char *buf, int size) { + return oakengine_color_manager_default_input_color_space(cm, buf, size); + }); + if (old_config != ocio_filename_->text()) { + oakengine_color_manager_set_config_filename( + cm, ocio_filename_->text().toUtf8().constData()); } - if (working_project_->color_manager()->get_default_input_color_space() != - default_input_colorspace_->currentText()) { - working_project_->color_manager()->set_default_input_color_space( - default_input_colorspace_->currentText()); + if (old_input_cs != default_input_colorspace_->currentText()) { + oakengine_color_manager_set_default_input_color_space( + cm, default_input_colorspace_->currentText().toUtf8().constData()); } - if (working_project_->get_color_reference_space() != - reference_space_->currentData().toString()) { - working_project_->set_color_reference_space( - reference_space_->currentData().toString()); + if ([this]() -> QString { + char buf[256]; + oakengine_project_get_color_reference_space( + reinterpret_cast(working_project_), + buf, sizeof(buf)); + return QString::fromUtf8(buf); + }() != reference_space_->currentData().toString()) { + oakengine_project_set_color_reference_space( + reinterpret_cast(working_project_), + reference_space_->currentData().toString().toUtf8().constData()); } super::accept(); @@ -253,50 +305,69 @@ void ProjectPropertiesDialog::ocio_filename_updated() { default_input_colorspace_->clear(); - try { - ocio::ConstConfigRcPtr c; + OakEngineColorConfig *config = nullptr; - if (ocio_filename_->text().isEmpty()) { - c = ColorManager::get_default_config(); - } else { - c = ColorManager::create_config_from_file(ocio_filename_->text()); - } + if (ocio_filename_->text().isEmpty()) { + config = oakengine_color_config_load_default(); + } else { + config = oakengine_color_config_load_file( + ocio_filename_->text().toUtf8().constData()); + } + if (config) { ocio_filename_->setStyleSheet(QString()); ocio_config_is_valid_ = true; // List input color spaces - QStringList input_cs = ColorManager::list_available_colorspaces(c); + int cs_count = oakengine_color_config_colorspace_count(config); + OakEngineColorManager *cm = oakengine_color_manager_from_project( + reinterpret_cast(working_project_)); + QString default_cs = oak_query_string([cm](char *buf, int size) { + return oakengine_color_manager_default_input_color_space(cm, buf, + size); + }); - foreach (QString cs, input_cs) { + for (int i = 0; i < cs_count; i++) { + QString cs = oak_query_string([config, i](char *buf, int size) { + return oakengine_color_config_colorspace_at(config, i, buf, + size); + }); default_input_colorspace_->addItem(cs); - if (cs == - working_project_->color_manager()->get_default_input_color_space()) { + if (cs == default_cs) { default_input_colorspace_->setCurrentIndex( default_input_colorspace_->count() - 1); } } - } catch (ocio::Exception &e) { + + oakengine_color_config_free(config); + } else { + char err_buf[1024]; + oakengine_color_last_error(err_buf, sizeof(err_buf)); ocio_config_is_valid_ = false; ocio_filename_->setStyleSheet( QStringLiteral("QLineEdit {color: red;}")); - ocio_config_error_ = e.what(); + ocio_config_error_ = QString::fromUtf8(err_buf); } } void ProjectPropertiesDialog::open_disk_cache_settings() { if (disk_cache_radios_[Project::k_cache_use_default_location]->isChecked()) { - DiskManager::instance()->show_disk_cache_settings_dialog( - DiskManager::instance()->get_default_cache_folder(), this); + oakengine_disk_show_settings_dialog(nullptr, this); } else if (disk_cache_radios_[Project::k_cache_store_alongside_project] ->isChecked()) { - DiskManager::instance()->show_disk_cache_settings_dialog( - working_project_->get_cache_alongside_project_path(), this); + oakengine_disk_show_settings_dialog( + [this]() -> QString { + char buf[4096]; + oakengine_project_cache_alongside_path( + reinterpret_cast(working_project_), + buf, sizeof(buf)); + return QString::fromUtf8(buf); + }().toUtf8().constData(), this); } else { - DiskManager::instance()->show_disk_cache_settings_dialog( - custom_cache_path_->text(), this); + oakengine_disk_show_settings_dialog( + custom_cache_path_->text().toUtf8().constData(), this); } } diff --git a/app/dialog/proxy/proxydialog.cpp b/app/dialog/proxy/proxydialog.cpp index 3403d1499..07c968072 100644 --- a/app/dialog/proxy/proxydialog.cpp +++ b/app/dialog/proxy/proxydialog.cpp @@ -27,9 +27,11 @@ #include #include #include +#include -#include "config/config.h" +#include "common/configwrapper.h" #include "node/project.h" +#include "oakengine/project.h" namespace olive { @@ -42,8 +44,8 @@ ProxyDialog::ProxyDialog(QWidget *parent, const QVector &footage) { setWindowTitle(tr("Proxy Settings")); - const ProxyManager::ProxyParams params = - ProxyManager::proxy_params_from_config(); + oak_proxy_params params; + oakengine_proxy_params_from_config(¶ms); QVBoxLayout *layout = new QVBoxLayout(this); @@ -186,9 +188,12 @@ void ProxyDialog::accept() if (!footage_.isEmpty()) { for (Footage *item : footage_) { if (custom_params_checkbox_->isChecked()) { - item->set_custom_proxy_params(current_params()); + oak_proxy_params p = current_params(); + oakengine_footage_set_custom_proxy_params( + reinterpret_cast(item), &p); } else { - item->clear_custom_proxy_params(); + oakengine_footage_clear_custom_proxy_params( + reinterpret_cast(item)); } } } @@ -269,15 +274,18 @@ void ProxyDialog::set_f_fmpeg_path(const QString &path) ffmpeg_path_edit_->setText(path); } -ProxyManager::ProxyParams ProxyDialog::current_params() const +oak_proxy_params ProxyDialog::current_params() const { - ProxyManager::ProxyParams params = ProxyManager::proxy_params_from_config(); + oak_proxy_params params; + oakengine_proxy_params_from_config(¶ms); params.width = static_cast(width_slider_->get_value()); params.height = static_cast(height_slider_->get_value()); params.divider = resolution_combo_->currentData().toInt(); params.crf = static_cast(crf_slider_->get_value()); - params.preset = preset_combo_->currentText(); - params.include_audio = include_audio_checkbox_->isChecked(); + strncpy(params.preset, preset_combo_->currentText().toUtf8().constData(), + sizeof(params.preset) - 1); + params.preset[sizeof(params.preset) - 1] = '\0'; + params.include_audio = include_audio_checkbox_->isChecked() ? 1 : 0; return params; } @@ -302,40 +310,60 @@ void ProxyDialog::refresh_footage_list() for (const Footage *item : footage_) { QTreeWidgetItem *tree_item = new QTreeWidgetItem(footage_tree_); tree_item->setText(0, item->filename()); - QString state = ProxyManager::proxy_state_to_string(item->proxy_state()); - if (item->has_custom_proxy_params()) { - state = tr("%1 (custom settings)").arg(state); + { + char state_buf[256]; + int state_len = oakengine_proxy_state_to_string( + item->proxy_state(), state_buf, sizeof(state_buf)); + QString state = (state_len > 0) + ? QString::fromUtf8(state_buf, state_len) + : QString(); + if (item->has_custom_proxy_params()) { + state = tr("%1 (custom settings)").arg(state); + } + tree_item->setText(1, state); } - tree_item->setText(1, state); } } void ProxyDialog::generate_proxies() { - if (!ProxyManager::instance()) { - qWarning() << "ProxyDialog::GenerateProxies: ProxyManager unavailable"; - return; - } - for (Footage *item : footage_) { const VideoParams video = item->get_first_enabled_video_stream(); - if (!video.is_valid()) { + oak_video_params _vp; + oakengine_viewer_get_first_enabled_video_stream( + reinterpret_cast(item), &_vp); + if (!oakengine_video_params_is_valid(&_vp)) { qWarning() << "ProxyDialog::GenerateProxies: skipping item with no valid video stream" << item->filename(); continue; } - const ProxyManager::ProxyParams params = - custom_params_checkbox_->isChecked() ? current_params() - : item->get_effective_proxy_params(); - const ProxyManager::Proxy proxy = - ProxyManager::instance()->get_or_start_proxy( - item->project()->cache_path(), item->filename(), - video.stream_index(), params); - item->set_proxy(proxy.filename, proxy.state, video.stream_index(), - params.version, true); - item->invalidate_all(Footage::k_filename_input); + oak_proxy_params params; + if (custom_params_checkbox_->isChecked()) { + params = current_params(); + } else { + oakengine_footage_get_effective_proxy_params( + reinterpret_cast(item), ¶ms); + } + oak_proxy_result proxy; + char cache_buf[512]; + oakengine_project_cache_path( + reinterpret_cast(item->project()), + cache_buf, sizeof(cache_buf)); + int ret = oakengine_proxy_get_or_start( + cache_buf, + item->filename().toUtf8().constData(), + video.stream_index(), ¶ms, &proxy); + if (ret != 0) { + qWarning() << "ProxyDialog::GenerateProxies: failed to get/start proxy for" + << item->filename(); + continue; + } + oakengine_footage_set_proxy(reinterpret_cast(item), + proxy.filename, proxy.state, + video.stream_index(), 1, params.version); + oakengine_footage_invalidate(reinterpret_cast(item)); } refresh_footage_list(); @@ -349,9 +377,16 @@ void ProxyDialog::delete_proxies() } QFile::remove(item->proxy_path()); - QFile::remove(ProxyManager::get_working_proxy_filename(item->proxy_path())); - item->clear_proxy(); - item->invalidate_all(Footage::k_filename_input); + { + char wbuf[4096]; + int wlen = oakengine_proxy_get_working_filename( + item->proxy_path().toUtf8().constData(), wbuf, sizeof(wbuf)); + if (wlen > 0) { + QFile::remove(QString::fromUtf8(wbuf, wlen)); + } + } + oakengine_footage_clear_proxy(reinterpret_cast(item)); + oakengine_footage_invalidate(reinterpret_cast(item)); } refresh_footage_list(); diff --git a/app/dialog/proxy/proxydialog.h b/app/dialog/proxy/proxydialog.h index 0da9a600a..168528b1a 100644 --- a/app/dialog/proxy/proxydialog.h +++ b/app/dialog/proxy/proxydialog.h @@ -25,7 +25,10 @@ #include #include -#include "codec/proxymanager.h" +#include "oakengine/footage.h" +#include "oakengine/proxy.h" +#include "oakengine/videoparams.h" +#include "oakengine/viewer.h" #include "node/project/footage/footage.h" #include "widget/slider/integerslider.h" @@ -68,7 +71,7 @@ public: void set_f_fmpeg_path(const QString &path); private: - ProxyManager::ProxyParams current_params() const; + oak_proxy_params current_params() const; void save_global_settings(); diff --git a/app/dialog/sequence/sequence.cpp b/app/dialog/sequence/sequence.cpp index f90fa66f7..a224e25b4 100644 --- a/app/dialog/sequence/sequence.cpp +++ b/app/dialog/sequence/sequence.cpp @@ -30,11 +30,12 @@ #include #include -#include "config/config.h" +#include "common/configwrapper.h" #include "common/qtutils.h" #include "dialog/msgbox.h" #include "oakengine/node.h" #include "oakengine/timeline.h" +#include "oakengine/videoparams.h" namespace olive { @@ -114,7 +115,7 @@ void SequenceDialog::accept() return; } - if (!VideoParams::format_is_float( + if (!oakengine_video_params_format_is_float( parameter_tab_->get_selected_preview_format()) && !OAK_CONFIG("PreviewNonFloatDontAskAgain").toBool()) { QMessageBox b(this); diff --git a/app/dialog/sequence/sequencedialogparametertab.cpp b/app/dialog/sequence/sequencedialogparametertab.cpp index 4111b0eba..45c12f1c8 100644 --- a/app/dialog/sequence/sequencedialogparametertab.cpp +++ b/app/dialog/sequence/sequencedialogparametertab.cpp @@ -24,6 +24,7 @@ #include #include "oakengine/timeline.h" +#include "oakengine/videoparams.h" namespace olive { @@ -122,8 +123,7 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence *sequence, height_slider_->set_value(height); framerate_combo_->set_frame_rate(Rational(fps_num, fps_den)); pixelaspect_combo_->set_pixel_aspect_ratio(Rational(par_num, par_den)); - interlacing_combo_->set_interlace_mode( - static_cast(interlacing)); + interlacing_combo_->set_interlace_mode(interlacing); preview_resolution_field_->set_divider(divider); preview_format_field_->set_pixel_format( static_cast(format)); @@ -173,15 +173,14 @@ void SequenceDialogParameterTab::save_preset_clicked() void SequenceDialogParameterTab::update_preview_resolution_label() { - VideoParams test_param(get_selected_video_width(), get_selected_video_height(), - PixelFormat::invalid, - VideoParams::k_internal_channel_count, Rational(1), - VideoParams::k_interlace_none, - preview_resolution_field_->currentData().toInt()); + int ew, eh; + oakengine_video_params_effective_size( + get_selected_video_width(), get_selected_video_height(), + preview_resolution_field_->currentData().toInt(), &ew, &eh); preview_resolution_label_->setText( - tr("(%1x%2)").arg(QString::number(test_param.effective_width()), - QString::number(test_param.effective_height()))); + tr("(%1x%2)").arg(QString::number(ew), + QString::number(eh))); } } diff --git a/app/dialog/sequence/sequencedialogparametertab.h b/app/dialog/sequence/sequencedialogparametertab.h index 6023becd5..cb919b396 100644 --- a/app/dialog/sequence/sequencedialogparametertab.h +++ b/app/dialog/sequence/sequencedialogparametertab.h @@ -57,7 +57,7 @@ public: return pixelaspect_combo_->get_pixel_aspect_ratio(); } - VideoParams::Interlacing get_selected_video_interlacing_mode() const + int get_selected_video_interlacing_mode() const { return interlacing_combo_->get_interlace_mode(); } diff --git a/app/dialog/sequence/sequencedialogpresettab.cpp b/app/dialog/sequence/sequencedialogpresettab.cpp index d77ed5ff2..362da3be3 100644 --- a/app/dialog/sequence/sequencedialogpresettab.cpp +++ b/app/dialog/sequence/sequencedialogpresettab.cpp @@ -30,8 +30,8 @@ #include #include -#include "config/config.h" -#include "render/videoparams.h" +#include "common/configwrapper.h" +#include "oakengine/videoparams.h" #include "ui/icons/icons.h" #include "widget/menu/menu.h" @@ -75,12 +75,12 @@ SequenceDialogPresetTab::SequenceDialogPresetTab(QWidget *parent) preset_tree_->addTopLevelItem( create_sd_preset_folder(tr("NTSC"), 720, 480, Rational(30000, 1001), - VideoParams::k_pixel_aspect_ntsc_standard, - VideoParams::k_pixel_aspect_ntsc_widescreen, 1)); + Rational(8, 9), + Rational(32, 27), 1)); preset_tree_->addTopLevelItem( create_sd_preset_folder(tr("PAL"), 720, 576, Rational(25, 1), - VideoParams::k_pixel_aspect_pal_standard, - VideoParams::k_pixel_aspect_pal_widescreen, 1)); + Rational(16, 15), + Rational(64, 45), 1)); // Load custom presets for (int i = 0; i < get_number_of_presets(); i++) { @@ -118,32 +118,42 @@ SequenceDialogPresetTab::create_hd_preset_folder(const QString &name, int width, add_standard_item(parent, std::make_shared( tr("%1 23.976 FPS").arg(name), width, height, - Rational(24000, 1001), VideoParams::k_pixel_aspect_square, - VideoParams::k_interlace_none, 48000, layout, divider, + Rational(24000, 1001), + Rational(1), // k_pixel_aspect_square + 0, // k_interlace_none + 48000, layout, divider, default_format, default_autocache)); add_standard_item(parent, std::make_shared( tr("%1 25 FPS").arg(name), width, height, - Rational(25, 1), VideoParams::k_pixel_aspect_square, - VideoParams::k_interlace_none, 48000, layout, divider, + Rational(25, 1), + Rational(1), // k_pixel_aspect_square + 0, // k_interlace_none + 48000, layout, divider, default_format, default_autocache)); add_standard_item(parent, std::make_shared( tr("%1 29.97 FPS").arg(name), width, height, - Rational(30000, 1001), VideoParams::k_pixel_aspect_square, - VideoParams::k_interlace_none, 48000, layout, divider, + Rational(30000, 1001), + Rational(1), // k_pixel_aspect_square + 0, // k_interlace_none + 48000, layout, divider, default_format, default_autocache)); add_standard_item(parent, std::make_shared( tr("%1 50 FPS").arg(name), width, height, - Rational(50, 1), VideoParams::k_pixel_aspect_square, - VideoParams::k_interlace_none, 48000, layout, divider, + Rational(50, 1), + Rational(1), // k_pixel_aspect_square + 0, // k_interlace_none + 48000, layout, divider, default_format, default_autocache)); add_standard_item(parent, std::make_shared( tr("%1 59.94 FPS").arg(name), width, height, - Rational(60000, 1001), VideoParams::k_pixel_aspect_square, - VideoParams::k_interlace_none, 48000, layout, divider, + Rational(60000, 1001), + Rational(1), // k_pixel_aspect_square + 0, // k_interlace_none + 48000, layout, divider, default_format, default_autocache)); return parent; } @@ -161,12 +171,14 @@ QTreeWidgetItem *SequenceDialogPresetTab::create_sd_preset_folder( add_standard_item( parent, std::make_shared( tr("%1 Standard").arg(name), width, height, frame_rate, - standard_par, VideoParams::k_interlaced_bottom_first, 48000, + standard_par, 2, // k_interlaced_bottom_first + 48000, layout, divider, default_format, default_autocache)); add_standard_item( parent, std::make_shared( tr("%1 Widescreen").arg(name), width, height, frame_rate, - wide_par, VideoParams::k_interlaced_bottom_first, 48000, + wide_par, 2, // k_interlaced_bottom_first + 48000, layout, divider, default_format, default_autocache)); return parent; } diff --git a/app/dialog/sequence/sequencepreset.h b/app/dialog/sequence/sequencepreset.h index 1071fc7a9..4dfd6b8d7 100644 --- a/app/dialog/sequence/sequencepreset.h +++ b/app/dialog/sequence/sequencepreset.h @@ -38,7 +38,7 @@ public: SequencePreset(const QString &name, int width, int height, const Rational &frame_rate, const Rational &pixel_aspect, - VideoParams::Interlacing interlacing, int sample_rate, + int interlacing, int sample_rate, uint64_t channel_layout, int preview_divider, PixelFormat preview_format, bool preview_autocache) : width_(width) @@ -74,7 +74,7 @@ public: reader->name() == QStringLiteral("interlacing_")) { // "interlacing_" is the element name mistakenly written by // older versions of Save(); accept it for backward compatibility - interlacing_ = static_cast( + interlacing_ = static_cast( reader->readElementText().toInt()); } else if (reader->name() == QStringLiteral("samplerate")) { sample_rate_ = reader->readElementText().toInt(); @@ -140,7 +140,7 @@ public: return pixel_aspect_; } - VideoParams::Interlacing interlacing() const + int interlacing() const { return interlacing_; } @@ -175,7 +175,7 @@ private: int height_; Rational frame_rate_; Rational pixel_aspect_; - VideoParams::Interlacing interlacing_; + int interlacing_; int sample_rate_; uint64_t channel_layout_; int preview_divider_; diff --git a/app/dialog/speedduration/speeddurationdialog.cpp b/app/dialog/speedduration/speeddurationdialog.cpp index 964b6303d..804aef168 100644 --- a/app/dialog/speedduration/speeddurationdialog.cpp +++ b/app/dialog/speedduration/speeddurationdialog.cpp @@ -27,8 +27,15 @@ #include #include "core.h" -#include "node/nodeundo.h" +#include "node/block/clip/clip.h" +#include "oakengine/timeline.h" +#include "oakengine/node.h" +#include "oakengine/undo.h" +#include "widget/timelinewidget/cliphandle.h" #include "timeline/timelineundopointer.h" +#include "timeline/timelinecommon.h" +#include "timeline/timelineundopointer.h" +#include "timeline/timelineundoripple.h" namespace olive { @@ -122,15 +129,15 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, layout->addWidget(btns); // Determine which speed value to use - start_speed_ = clips.first()->speed(); + start_speed_ = clip_speed(clips.first()); start_duration_ = clips.first()->length(); - start_reverse_ = clips.first()->reverse(); - start_maintain_audio_pitch_ = clips.first()->maintain_audio_pitch(); - start_loop_ = int(clips.first()->loop_mode()); + start_reverse_ = clip_is_reversed(clips.first()); + start_maintain_audio_pitch_ = clip_maintain_audio_pitch(clips.first()); + start_loop_ = clip_loop_mode(clips.first()); for (int i = 1; i < clips.size(); i++) { ClipBlock *c = clips.at(i); - if (!qIsNaN(start_speed_) && !qFuzzyCompare(start_speed_, c->speed())) { + if (!qIsNaN(start_speed_) && !qFuzzyCompare(start_speed_, clip_speed(c))) { // Speed differs per clip start_speed_ = qSNaN(); } @@ -141,8 +148,8 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, // Yes, in theory a bool should only ever be 0 or 1 anyway, but MSVC complained and it is // *possible* that a bool could be something else, so this code is safer - int clip_reverse = c->reverse() ? 1 : 0; - int clip_maintain_pitch = c->maintain_audio_pitch() ? 1 : 0; + int clip_reverse = clip_is_reversed(c) ? 1 : 0; + int clip_maintain_pitch = clip_maintain_audio_pitch(c) ? 1 : 0; if (start_reverse_ != -1 && clip_reverse != start_reverse_) { start_reverse_ = -1; } @@ -151,7 +158,7 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, start_maintain_audio_pitch_ = -1; } - if (start_loop_ != -1 && int(c->loop_mode()) != start_loop_) { + if (start_loop_ != -1 && clip_loop_mode(c) != start_loop_) { start_loop_ = -1; } } @@ -189,9 +196,40 @@ SpeedDurationDialog::SpeedDurationDialog(const QVector &clips, void SpeedDurationDialog::accept() { - MultiUndoCommand *command = new MultiUndoCommand(); + // Collect all duration/speed changes into a single undo entry. + const QByteArray undo_name = tr("Speed/Duration").toUtf8(); + oakengine_undo_group_begin(undo_name.constData()); - // Set duration values + // Set speed values + if (speed_slider_->is_tristate()) { + if (link_box_->isChecked() && !dur_slider_->is_tristate()) { + // Automatically determine speed from duration + foreach (ClipBlock *c, clips_) { + double speed = get_speed_adjustment(clip_speed(c), c->length(), + dur_slider_->get_value()); + oak_node_value val; + memset(&val, 0, sizeof(val)); + val.type = OAK_NODE_VALUE_FLOAT; + val.f[0] = speed; + oakengine_node_set_input( + reinterpret_cast(c), + oakengine_clip_speed_input_id(), &val); + } + } + } else { + // Set speeds to value of slider + foreach (ClipBlock *c, clips_) { + oak_node_value val; + memset(&val, 0, sizeof(val)); + val.type = OAK_NODE_VALUE_FLOAT; + val.f[0] = speed_slider_->get_value(); + oakengine_node_set_input( + reinterpret_cast(c), + oakengine_clip_speed_input_id(), &val); + } + } + + // Set duration values (undoable via facade) TimelineRippleDeleteGapsAtRegionsCommand::RangeList ripple_ranges; foreach (ClipBlock *c, clips_) { @@ -199,7 +237,7 @@ void SpeedDurationDialog::accept() if (dur_slider_->is_tristate()) { if (link_box_->isChecked() && !speed_slider_->is_tristate()) { - proposed_length = get_length_adjustment(c->length(), c->speed(), + proposed_length = get_length_adjustment(c->length(), clip_speed(c), speed_slider_->get_value(), timebase_); } @@ -219,8 +257,17 @@ void SpeedDurationDialog::accept() } if (proposed_length != c->length()) { - command->add_child(new BlockTrimCommand( - c->track(), c, proposed_length, Timeline::k_trim_out)); + // Trim the clip's out-point to the new length (one undoable child + // inside the group, kept as a direct C++ command because the dialog + // already works in Rational time and has the track available). + oakengine_undo_push( + oakengine_block_trim_command( + reinterpret_cast(c->track()), + reinterpret_cast(c), + proposed_length.numerator(), + proposed_length.denominator(), + olive::Timeline::k_trim_out, 0), + tr("Trim Clip").toUtf8().constData()); ripple_ranges.append( { c->track(), TimeRange(c->in() + proposed_length, c->out()) }); @@ -228,70 +275,85 @@ void SpeedDurationDialog::accept() } } - if (ripple_box_->isChecked()) { - command->add_child(new TimelineRippleDeleteGapsAtRegionsCommand( - clips_.first()->track()->sequence(), ripple_ranges)); - } - - // Set speed values - if (speed_slider_->is_tristate()) { - if (link_box_->isChecked() && !dur_slider_->is_tristate()) { - // Automatically determine speed from duration - foreach (ClipBlock *c, clips_) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(c, ClipBlock::k_speed_input)), - get_speed_adjustment(c->speed(), c->length(), - dur_slider_->get_value()))); + if (ripple_box_->isChecked() && !ripple_ranges.isEmpty()) { + Sequence *seq = reinterpret_cast( + oakengine_clip_get_sequence( + reinterpret_cast(clips_.first()))); + if (seq) { + QVector range_in_ts; + QVector range_out_ts; + QVector range_track_types; + QVector range_track_indexes; + range_in_ts.reserve(ripple_ranges.size()); + range_out_ts.reserve(ripple_ranges.size()); + range_track_types.reserve(ripple_ranges.size()); + range_track_indexes.reserve(ripple_ranges.size()); + int tbn = 0, tbd = 0; + oakengine_node_frame_time_base( + reinterpret_cast(seq), &tbn, &tbd); + for (const auto &range : ripple_ranges) { + range_track_types.append(range.first->type()); + range_track_indexes.append(range.first->index()); + range_in_ts.append(olive::core::Timecode::time_to_timestamp( + range.second.in(), olive::Rational(tbn, tbd), + olive::core::Timecode::k_round)); + range_out_ts.append(olive::core::Timecode::time_to_timestamp( + range.second.out(), olive::Rational(tbn, tbd), + olive::core::Timecode::k_round)); } - } - } else { - // Set speeds to value of slider - foreach (ClipBlock *c, clips_) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference(NodeInput(c, ClipBlock::k_speed_input)), - speed_slider_->get_value())); + oakengine_undo_push( + oakengine_timeline_ripple_delete_gaps_command( + reinterpret_cast(seq), + range_in_ts.constData(), range_out_ts.constData(), + range_track_types.constData(), + range_track_indexes.constData(), + ripple_ranges.size()), + tr("Ripple Delete Gaps").toUtf8().constData()); } } // Set reverse values if (!reverse_box_->isTristate()) { foreach (ClipBlock *c, clips_) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(c, ClipBlock::k_reverse_input)), - reverse_box_->isChecked())); + oak_node_value val; + memset(&val, 0, sizeof(val)); + val.type = OAK_NODE_VALUE_BOOL; + val.num = reverse_box_->isChecked() ? 1 : 0; + oakengine_node_set_input( + reinterpret_cast(c), + oakengine_clip_reverse_input_id(), &val); } } - // Set reverse values + // Set maintain audio pitch values if (!maintain_audio_pitch_box_->isTristate()) { foreach (ClipBlock *c, clips_) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(c, ClipBlock::k_maintain_audio_pitch_input)), - maintain_audio_pitch_box_->isChecked())); + oak_node_value val; + memset(&val, 0, sizeof(val)); + val.type = OAK_NODE_VALUE_BOOL; + val.num = maintain_audio_pitch_box_->isChecked() ? 1 : 0; + oakengine_node_set_input( + reinterpret_cast(c), + oakengine_clip_maintain_audio_pitch_input_id(), &val); } } if (loop_combo_->currentIndex() != -1) { foreach (ClipBlock *c, clips_) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(c, ClipBlock::k_loop_mode_input)), - loop_combo_->currentData())); + oak_node_value val; + memset(&val, 0, sizeof(val)); + val.type = OAK_NODE_VALUE_INT; + val.num = loop_combo_->currentData().toInt(); + oakengine_node_set_input( + reinterpret_cast(c), + oakengine_clip_loop_mode_input_id(), &val); } } - QString name = (clips_.size() > 1) ? - tr("Set %1 Clip Properties").arg(clips_.size()) : - tr("Set Clip \"%1\" Properties") - .arg(clips_.first()->get_label_or_name()); - Core::instance()->undo_stack()->push(command, name); + oakengine_undo_group_end(); super::accept(); } - Rational SpeedDurationDialog::get_length_adjustment( const Rational &original_length, double original_speed, double new_speed, const Rational &timebase) diff --git a/app/dialog/speedduration/speeddurationdialog.h b/app/dialog/speedduration/speeddurationdialog.h index 22e955757..d6bd619fd 100644 --- a/app/dialog/speedduration/speeddurationdialog.h +++ b/app/dialog/speedduration/speeddurationdialog.h @@ -26,14 +26,13 @@ #include #include -#include "node/block/clip/clip.h" #include "node/block/gap/gap.h" -#include "undo/undocommand.h" #include "widget/slider/floatslider.h" #include "widget/slider/rationalslider.h" -namespace olive -{ +namespace olive { + +class ClipBlock; class SpeedDurationDialog : public QDialog { Q_OBJECT diff --git a/app/dialog/task/task.cpp b/app/dialog/task/task.cpp index 3f762645d..80f5ae074 100644 --- a/app/dialog/task/task.cpp +++ b/app/dialog/task/task.cpp @@ -24,29 +24,42 @@ #include #include +#include "oakengine/task.h" + namespace olive { #define super ProgressDialog -TaskDialog::TaskDialog(Task *task, const QString &title, QWidget *parent) - : super(task->get_title(), title, parent) +TaskDialog::TaskDialog(OakEngineTask *task, const QString &title, QWidget *parent) + : super([&]() { + char buf[512]; + buf[0] = '\0'; + oakengine_task_title(task, buf, sizeof(buf)); + return QString::fromUtf8(buf); + }(), title, parent) , task_(task) , destroy_on_close_(true) , already_shown_(false) , task_finished_(false) { - // Clear task when this dialog is destroyed - task_->setParent(this); + bridge_ = new EngineEventBridge(this); + bridge_->subscribe(task, OAKENGINE_EVENT_TASK_PROGRESS); + connect(bridge_, &EngineEventBridge::task_progress, this, + [this](OakEngineTask *, double progress) { + set_progress(progress); + }, Qt::QueuedConnection); - // Connect the save manager progress signal to the progress bar update on the dialog - connect(task_, &Task::progress_changed, this, &TaskDialog::set_progress, - Qt::QueuedConnection); + connect(this, &TaskDialog::cancelled, this, [this]() { + oakengine_task_cancel(task_); + }, Qt::DirectConnection); +} - // Connect cancel signal (must be a direct connection or it'll be queued after the task has - // already finished) - connect(this, &TaskDialog::cancelled, task_, &Task::Cancel, - Qt::DirectConnection); +TaskDialog::~TaskDialog() +{ + if (task_) { + oakengine_task_free(task_); + } } void TaskDialog::showEvent(QShowEvent *e) @@ -54,20 +67,15 @@ void TaskDialog::showEvent(QShowEvent *e) super::showEvent(e); if (!already_shown_) { - // Create watcher for when the task finishes QFutureWatcher *task_watcher = new QFutureWatcher(); - // Listen for when the task finishes connect(task_watcher, &QFutureWatcher::finished, this, &TaskDialog::task_finished, Qt::QueuedConnection); - // Run task in another thread with QtConcurrent task_watcher->setFuture( -#if QT_VERSION_MAJOR >= 6 - QtConcurrent::run(&Task::start, task_) -#else - QtConcurrent::run(task_, &Task::Start) -#endif + QtConcurrent::run([this]() -> bool { + return oakengine_task_start_sync(task_) == 1; + }) ); already_shown_ = true; @@ -76,19 +84,12 @@ void TaskDialog::showEvent(QShowEvent *e) void TaskDialog::closeEvent(QCloseEvent *e) { - // Cancel task if it is running - task_->Cancel(); + oakengine_task_cancel(task_); - // Standard close function super::closeEvent(e); - // Reset shown already_shown_ = false; - // Clean up this task and dialog, but only if the task has actually finished. - // If the user closes the window while the task is still running, deleting now - // would destroy the Task object out from under the worker thread and crash - // when the task later touches its own members (e.g. ExportTask::encoder_). if (destroy_on_close_ && task_finished_) { deleteLater(); } @@ -104,7 +105,10 @@ void TaskDialog::task_finished() if (task_watcher->result()) { emit task_succeeded(task_); } else { - show_error_message(tr("Task Failed"), task_->get_error()); + char err[512]; + err[0] = '\0'; + oakengine_task_error(task_, err, sizeof(err)); + show_error_message(tr("Task Failed"), QString::fromUtf8(err)); emit task_failed(task_); } diff --git a/app/dialog/task/task.h b/app/dialog/task/task.h index b04dc3895..ae5ab62cf 100644 --- a/app/dialog/task/task.h +++ b/app/dialog/task/task.h @@ -23,7 +23,8 @@ #define OAK_TASKDIALOG_H #include "dialog/progress/progress.h" -#include "task/task.h" +#include "engineeventbridge.h" +#include "oakengine/task.h" namespace olive { @@ -31,29 +32,16 @@ namespace olive class TaskDialog : public ProgressDialog { Q_OBJECT public: - /** - * @brief TaskDialog Constructor - * - * Creates a TaskDialog. The TaskDialog takes ownership of the Task and will destroy it on close. - * Connect to the Task::Succeeded() if you want to retrieve information from the task before it - * gets destroyed. - */ - TaskDialog(Task *task, const QString &title, QWidget *parent = nullptr); + TaskDialog(OakEngineTask *task, const QString &title, QWidget *parent = nullptr); + + ~TaskDialog() override; - /** - * @brief Set whether TaskDialog should destroy itself (and the task) when it's closed - * - * This is TRUE by default. - */ void set_destroy_on_close(bool e) { destroy_on_close_ = e; } - /** - * @brief Returns this dialog's task - */ - Task *get_task() const + OakEngineTask *get_task() const { return task_; } @@ -64,12 +52,14 @@ protected: virtual void closeEvent(QCloseEvent *e) override; signals: - void task_succeeded(Task *task); + void task_succeeded(OakEngineTask *task); - void task_failed(Task *task); + void task_failed(OakEngineTask *task); private: - Task *task_; + OakEngineTask *task_; + + EngineEventBridge *bridge_ = nullptr; bool destroy_on_close_; diff --git a/app/engineeventbridge.cpp b/app/engineeventbridge.cpp new file mode 100644 index 000000000..212c508fd --- /dev/null +++ b/app/engineeventbridge.cpp @@ -0,0 +1,427 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications 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 "engineeventbridge.h" + +#include + +#include "oakengine/node.h" + +namespace olive +{ + +EngineEventBridge::EngineEventBridge(QObject *parent) : QObject(parent) {} + +EngineEventBridge::~EngineEventBridge() +{ + foreach (int64_t id, subscriptions_) { + oakengine_event_unsubscribe(id); + } +} + +int64_t EngineEventBridge::subscribe(void *handle, int32_t event_id) +{ + const int64_t id = + oakengine_event_subscribe(handle, event_id, &on_engine_event, this); + if (id > 0) { + subscriptions_.append(id); + } + return id; +} + +void EngineEventBridge::unsubscribe(int64_t id) +{ + if (oakengine_event_unsubscribe(id) == OAKENGINE_OK) { + subscriptions_.removeAll(id); + } +} + +void EngineEventBridge::unsubscribe_all() +{ + foreach (int64_t id, subscriptions_) { + oakengine_event_unsubscribe(id); + } + subscriptions_.clear(); +} + +void EngineEventBridge::on_engine_event(const oakengine_event *event, + void *userdata) +{ + static_cast(userdata)->dispatch(event); +} + +void EngineEventBridge::dispatch(const oakengine_event *event) +{ + switch (event->id) { + case OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED: + emit project_modified_changed(event->a != 0); + break; + case OAKENGINE_EVENT_PROJECT_NAME_CHANGED: + emit project_name_changed(); + break; + case OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM: + emit folder_begin_insert_item( + static_cast(event->source), + static_cast(event->handle), int(event->a)); + break; + case OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM: + emit folder_end_insert_item(static_cast(event->source)); + break; + case OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM: + emit folder_begin_remove_item( + static_cast(event->source), + static_cast(event->handle), int(event->a)); + break; + case OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM: + emit folder_end_remove_item(static_cast(event->source)); + break; + case OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED: + emit sequence_track_added(static_cast(event->handle), + int(event->a)); + break; + case OAKENGINE_EVENT_SEQUENCE_TRACK_REMOVED: + emit sequence_track_removed( + static_cast(event->handle), int(event->a)); + break; + case OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED: + emit sequence_track_list_changed( + static_cast(event->source), int(event->a)); + break; + case OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED: + emit sequence_track_height_changed( + static_cast(event->source), + static_cast(event->handle), int(event->a), + int(event->b)); + break; + case OAKENGINE_EVENT_SEQUENCE_SUBTITLES_CHANGED: + emit sequence_subtitles_changed( + static_cast(event->source), event->a, + event->b); + break; + case OAKENGINE_EVENT_TRACK_INDEX_CHANGED: + emit track_index_changed(static_cast(event->source), + int(event->a), int(event->b)); + break; + case OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED: { + double h; + memcpy(&h, &event->a, sizeof(h)); + emit track_height_changed(static_cast(event->source), + h); + break; + } + case OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED: + emit track_blocks_refreshed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_TRACK_MUTED_CHANGED: + emit track_muted_changed(static_cast(event->source), + event->a != 0); + break; + case OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED: + emit block_enabled_changed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED: + emit block_preview_changed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED: + emit marker_list_marker_added( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED: + emit marker_list_marker_removed( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED: + emit marker_list_marker_modified( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED: + emit workarea_range_changed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED: + emit workarea_enabled_changed( + static_cast(event->source), event->a != 0); + break; + case OAKENGINE_EVENT_TRACK_BLOCK_ADDED: + emit track_block_added(static_cast(event->handle), + event->a, event->b); + break; + case OAKENGINE_EVENT_TRACK_BLOCK_REMOVED: + emit track_block_removed(static_cast(event->handle), + event->a, event->b); + break; + case OAKENGINE_EVENT_SEQUENCE_MARKER_ADDED: + emit sequence_marker_added(event->a); + break; + case OAKENGINE_EVENT_SEQUENCE_MARKER_REMOVED: + emit sequence_marker_removed(event->a); + break; + case OAKENGINE_EVENT_SEQUENCE_MARKER_MODIFIED: + emit sequence_marker_modified(event->a); + break; + case OAKENGINE_EVENT_SEQUENCE_WORKAREA_RANGE_CHANGED: + emit sequence_workarea_range_changed(event->a, event->b); + break; + case OAKENGINE_EVENT_SEQUENCE_WORKAREA_ENABLED_CHANGED: + emit sequence_workarea_enabled_changed(event->a != 0); + break; + case OAKENGINE_EVENT_NODE_LABEL_CHANGED: + emit node_label_changed(static_cast(event->source), + QString::fromUtf8(event->s ? event->s : "")); + break; + case OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED: + emit node_input_value_changed( + static_cast(event->source), + QString::fromUtf8(event->s ? event->s : ""), int(event->a), + event->b, event->c); + break; + case OAKENGINE_EVENT_NODE_INPUT_CONNECTED: + emit node_input_connected( + static_cast(event->source), + static_cast(event->handle), + QString::fromUtf8(event->s ? event->s : ""), int(event->a)); + break; + case OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED: + emit node_input_disconnected( + static_cast(event->source), + static_cast(event->handle), + QString::fromUtf8(event->s ? event->s : ""), int(event->a)); + break; + case OAKENGINE_EVENT_NODE_INPUT_FLAGS_CHANGED: + emit node_input_flags_changed( + static_cast(event->source), + QString::fromUtf8(event->s ? event->s : ""), event->a); + break; + case OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED: + emit node_input_property_changed( + static_cast(event->source), + QString::fromUtf8(event->s ? event->s : "")); + break; + case OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED: + emit node_input_data_type_changed( + static_cast(event->source), + QString::fromUtf8(event->s ? event->s : ""), int(event->a)); + break; + case OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED: + emit node_input_array_size_changed( + static_cast(event->source), + QString::fromUtf8(event->s ? event->s : ""), int(event->a), + int(event->b)); + break; + case OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED: + emit node_keyframe_enable_changed( + static_cast(event->source), + QString::fromUtf8(event->s ? event->s : ""), int(event->a), + event->b != 0); + break; + case OAKENGINE_EVENT_NODE_KEYFRAME_ADDED: + emit node_keyframe_added( + static_cast(event->source), + static_cast(event->handle), + QString::fromUtf8(event->s ? event->s : ""), int(event->a), + int(event->b)); + break; + case OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED: + emit node_keyframe_removed( + static_cast(event->source), + static_cast(event->handle), + QString::fromUtf8(event->s ? event->s : ""), int(event->a), + int(event->b)); + break; + case OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED: + emit node_keyframe_time_changed( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED: + emit node_keyframe_type_changed( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED: + emit node_keyframe_value_changed( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT: + emit node_node_added_to_context( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT: + emit node_node_removed_from_context( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED: + emit node_message_count_changed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_NODE_LINKS_CHANGED: + emit node_links_changed(static_cast(event->source)); + break; + case OAKENGINE_EVENT_NODE_COLOR_CHANGED: + emit node_color_changed(static_cast(event->source)); + break; + case OAKENGINE_EVENT_NODE_INPUT_ADDED: + emit node_input_added( + static_cast(event->source), + QString::fromUtf8(event->s ? event->s : "")); + break; + case OAKENGINE_EVENT_NODE_INPUT_REMOVED: + emit node_input_removed( + static_cast(event->source), + QString::fromUtf8(event->s ? event->s : "")); + break; + case OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH: + emit node_removed_from_graph( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED: + emit group_input_passthrough_added( + static_cast(event->source), + static_cast(event->handle), + QString::fromUtf8(event->s ? event->s : ""), + static_cast(event->a)); + break; + case OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED: + emit group_input_passthrough_removed( + static_cast(event->source), + static_cast(event->handle), + QString::fromUtf8(event->s ? event->s : ""), + static_cast(event->a)); + break; + case OAKENGINE_EVENT_GROUP_OUTPUT_PASSTHROUGH_CHANGED: + emit group_output_passthrough_changed( + static_cast(event->source), + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED: { + double x, y; + memcpy(&x, &event->a, sizeof(x)); + memcpy(&y, &event->b, sizeof(y)); + emit node_context_position_changed( + static_cast(event->source), + static_cast(event->handle), x, y); + break; + } + case OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED: + emit viewer_length_changed(static_cast(event->source), + event->a, event->b); + break; + case OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED: + emit viewer_playhead_changed( + static_cast(event->source), event->a, event->b); + break; + case OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED: + emit viewer_frame_rate_changed( + static_cast(event->source), event->a, event->b); + break; + case OAKENGINE_EVENT_VIEWER_SIZE_CHANGED: + emit viewer_size_changed(static_cast(event->source), + int(event->a), int(event->b)); + break; + case OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED: + emit viewer_pixel_aspect_changed( + static_cast(event->source), event->a, event->b); + break; + case OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED: + emit viewer_interlacing_changed( + static_cast(event->source), int(event->a)); + break; + case OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED: + emit viewer_video_params_changed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED: + emit viewer_audio_params_changed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED: + emit viewer_texture_input_changed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED: + emit viewer_sample_rate_changed( + static_cast(event->source), int(event->a)); + break; + case OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED: + emit viewer_connected_waveform_changed( + static_cast(event->source)); + break; + case OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED: + emit task_manager_task_added( + static_cast(event->handle), + QString::fromUtf8(event->s ? event->s : "")); + break; + case OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED: + emit task_manager_task_removed( + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_TASK_MANAGER_TASK_FAILED: + emit task_manager_task_failed( + static_cast(event->handle)); + break; + case OAKENGINE_EVENT_TASK_MANAGER_LIST_CHANGED: + emit task_manager_list_changed(); + break; + case OAKENGINE_EVENT_TASK_STARTED: + emit task_started(static_cast(event->source), + event->a); + break; + case OAKENGINE_EVENT_TASK_PROGRESS: { + double d; + memcpy(&d, &event->a, sizeof(d)); + emit task_progress(static_cast(event->source), d); + break; + } + case OAKENGINE_EVENT_TASK_FINISHED: + emit task_finished(static_cast(event->source), + event->a != 0); + break; + case OAKENGINE_EVENT_UNDO_INDEX_CHANGED: + emit undo_index_changed(int(event->a)); + break; + case OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_PARAMS_CHANGED: + emit audio_output_params_changed(); + break; + case OAKENGINE_EVENT_PLAYBACK_CACHE_INVALIDATED: + emit playback_cache_invalidated(event->source, event->a, event->b); + break; + case OAKENGINE_EVENT_PLAYBACK_CACHE_VALIDATED: + emit playback_cache_validated(event->source, event->a, event->b); + break; + case OAKENGINE_EVENT_FRAME_CACHE_INVALIDATED: + emit frame_cache_invalidated(event->source, event->a, event->b); + break; + default: + break; + } +} + +} diff --git a/app/engineeventbridge.h b/app/engineeventbridge.h new file mode 100644 index 000000000..5bec91950 --- /dev/null +++ b/app/engineeventbridge.h @@ -0,0 +1,247 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications 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 ENGINEEVENTBRIDGE_H +#define ENGINEEVENTBRIDGE_H + +#include +#include + +#include "oakengine/events.h" +#include "oakengine/task.h" + +namespace olive +{ + +/** + * @brief Qt-signal adapter over the liboakengine event C ABI + * (oakengine/events.h). + * + * The facade delivers engine change notifications as C callbacks; the + * application consumes Qt signals. EngineEventBridge sits in between: + * subscribe() registers a C callback on an engine handle, and the bridge + * re-emits the event as the matching typed Qt signal. This is the standard + * replacement for connect(engineObject, &EngineClass::signal, ...) at the + * remaining app -> engine connection points (see + * docs/zh/facade-migration-roadmap.md). + * + * Semantics match the connections they replace: the engine invokes the C + * callback synchronously on the emitting thread (Qt::DirectConnection + * equivalent) and the bridge emits its Qt signals from that same callback, + * so receivers are still called synchronously on the engine object's + * thread (the GUI thread in practice). + * + * The bridge owns its subscriptions: destroying it unsubscribes + * everything. Subscriptions also die automatically with the observed + * engine object (the facade drops them on QObject::destroyed), so a + * project/sequence teardown never leaves a dangling callback into the app. + */ +class EngineEventBridge : public QObject { + Q_OBJECT +public: + explicit EngineEventBridge(QObject *parent = nullptr); + + ~EngineEventBridge() override; + + /** + * @brief Subscribe to `event_id` (OAKENGINE_EVENT_*) on `handle` and + * return the subscription id (> 0), or 0 on failure. + * + * `handle` is a facade handle (an engine Node or Project reinterpreted + * as OakEngineNode / OakEngineProject etc., per the event table in + * oakengine/events.h). Each matching engine change is re-emitted as + * the corresponding Qt signal of this bridge. + */ + int64_t subscribe(void *handle, int32_t event_id); + + /** + * @brief Cancel a subscription returned by subscribe(). Unknown or + * already-dead ids are ignored. + */ + void unsubscribe(int64_t id); + + /** + * @brief Cancel every live subscription (but keep the Qt signal + * connections). Use when the observed engine object set changes, e.g. + * switching sequences, so stale subscriptions don't pile up. + */ + void unsubscribe_all(); + +signals: + void project_modified_changed(bool modified); + void project_name_changed(); + + void folder_begin_insert_item(OakEngineNode *folder, + OakEngineNode *child, int index); + void folder_end_insert_item(OakEngineNode *folder); + void folder_begin_remove_item(OakEngineNode *folder, + OakEngineNode *child, int index); + void folder_end_remove_item(OakEngineNode *folder); + + void sequence_track_added(OakEngineTrack *track, int track_type); + void sequence_track_removed(OakEngineTrack *track, int track_type); + void sequence_track_list_changed(OakEngineSequence *source, int track_type); + void sequence_track_height_changed(OakEngineSequence *source, + OakEngineTrack *track, int track_type, + int height_px); + void sequence_subtitles_changed(OakEngineSequence *source, qint64 in_ts, + qint64 out_ts); + + void track_block_added(OakEngineBlock *block, qint64 in_ts, + qint64 out_ts); + void track_block_removed(OakEngineBlock *block, qint64 in_ts, + qint64 out_ts); + void track_index_changed(OakEngineTrack *source, int old_index, + int new_index); + void track_height_changed(OakEngineTrack *source, double height); + void track_blocks_refreshed(OakEngineTrack *source); + void track_muted_changed(OakEngineTrack *source, bool muted); + + void block_enabled_changed(OakEngineBlock *source); + void block_preview_changed(OakEngineBlock *source); + + void sequence_marker_added(qint64 time_ts); + void sequence_marker_removed(qint64 time_ts); + void sequence_marker_modified(qint64 time_ts); + + void marker_list_marker_added(OakEngineMarkerList *source, + OakEngineMarker *marker); + void marker_list_marker_removed(OakEngineMarkerList *source, + OakEngineMarker *marker); + void marker_list_marker_modified(OakEngineMarkerList *source, + OakEngineMarker *marker); + + void sequence_workarea_range_changed(qint64 in_ts, qint64 out_ts); + void sequence_workarea_enabled_changed(bool enabled); + + void workarea_range_changed(OakEngineWorkarea *source); + void workarea_enabled_changed(OakEngineWorkarea *source, bool enabled); + + /* Node family (source is the subscribed OakEngineNode*). Input ids are + * copied out of the event during the callback. */ + void node_label_changed(OakEngineNode *source, const QString &label); + void node_input_value_changed(OakEngineNode *source, const QString &input, + int element, qint64 in_ts, qint64 out_ts); + void node_input_connected(OakEngineNode *source, OakEngineNode *output, + const QString &input, int element); + void node_input_disconnected(OakEngineNode *source, OakEngineNode *output, + const QString &input, int element); + void node_input_flags_changed(OakEngineNode *source, const QString &input, + qint64 flags); + void node_input_property_changed(OakEngineNode *source, + const QString &input); + void node_input_data_type_changed(OakEngineNode *source, + const QString &input, int type); + void node_input_array_size_changed(OakEngineNode *source, + const QString &input, int old_size, + int new_size); + void node_keyframe_enable_changed(OakEngineNode *source, + const QString &input, int element, + bool enabled); + void node_keyframe_added(OakEngineNode *source, OakEngineKeyframe *key, + const QString &input, int element, int track); + void node_keyframe_removed(OakEngineNode *source, OakEngineKeyframe *key, + const QString &input, int element, int track); + void node_keyframe_time_changed(OakEngineNode *source, + OakEngineKeyframe *key); + void node_keyframe_type_changed(OakEngineNode *source, + OakEngineKeyframe *key); + void node_keyframe_value_changed(OakEngineNode *source, + OakEngineKeyframe *key); + void node_node_added_to_context(OakEngineNode *source, + OakEngineNode *node); + void node_node_removed_from_context(OakEngineNode *source, + OakEngineNode *node); + void node_message_count_changed(OakEngineNode *source); + void node_links_changed(OakEngineNode *source); + void node_color_changed(OakEngineNode *source); + void node_input_added(OakEngineNode *source, const QString &input_id); + void node_input_removed(OakEngineNode *source, const QString &input_id); + void node_removed_from_graph(OakEngineNode *source, + OakEngineNode *project); + + /* Group family (source is the group node). */ + void group_input_passthrough_added(OakEngineNode *source, + OakEngineNode *node, + const QString &input, int element); + void group_input_passthrough_removed(OakEngineNode *source, + OakEngineNode *node, + const QString &input, int element); + void group_output_passthrough_changed(OakEngineNode *source, + OakEngineNode *output); + + /* Context position (source is the context node). */ + void node_context_position_changed(OakEngineNode *source, + OakEngineNode *node, double x, + double y); + + /* Viewer family (source is the subscribed viewer OakEngineNode*). + * Rational payloads (seconds) are delivered as num/den pairs. */ + void viewer_length_changed(OakEngineNode *source, qint64 num, qint64 den); + void viewer_playhead_changed(OakEngineNode *source, qint64 num, + qint64 den); + void viewer_frame_rate_changed(OakEngineNode *source, qint64 num, + qint64 den); + void viewer_size_changed(OakEngineNode *source, int w, int h); + void viewer_pixel_aspect_changed(OakEngineNode *source, qint64 num, + qint64 den); + void viewer_interlacing_changed(OakEngineNode *source, int mode); + void viewer_video_params_changed(OakEngineNode *source); + void viewer_audio_params_changed(OakEngineNode *source); + void viewer_texture_input_changed(OakEngineNode *source); + void viewer_sample_rate_changed(OakEngineNode *source, int sr); + void viewer_connected_waveform_changed(OakEngineNode *source); + + /* Task manager family (title is copied out of the event). */ + void task_manager_task_added(OakEngineTask *task, const QString &title); + void task_manager_task_removed(OakEngineTask *task); + void task_manager_task_failed(OakEngineTask *task); + void task_manager_list_changed(); + + /* Task family (source is the subscribed OakEngineTask*). */ + void task_started(OakEngineTask *source, qint64 start_time); + void task_progress(OakEngineTask *source, double progress); + void task_finished(OakEngineTask *source, bool succeeded); + + /* Undo stack family. */ + void undo_index_changed(int index); + + /* AudioManager family. */ + void audio_output_params_changed(); + + /* Playback cache / frame cache family (B9c). */ + void playback_cache_invalidated(void *cache, qint64 a, qint64 b); + void playback_cache_validated(void *cache, qint64 a, qint64 b); + void frame_cache_invalidated(void *cache, qint64 a, qint64 b); + +private: + // C callback entry point; `userdata` is the EngineEventBridge. + static void on_engine_event(const oakengine_event *event, + void *userdata); + + void dispatch(const oakengine_event *event); + + QVector subscriptions_; +}; + +} + +#endif // ENGINEEVENTBRIDGE_H diff --git a/app/main.cpp b/app/main.cpp index 6e603cdd7..02712fd44 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -27,6 +27,7 @@ * Use the navigation above to find documentation on classes or source files. */ +#include "oakengine/plugin.h" #include "pluginSupport/olivehost.h" #include @@ -37,11 +38,12 @@ #include #include -#include "config/config.h" +#include + #include "core.h" #include "common/commandlineparser.h" -#include "common/debug.h" -#include "node/project/serializer/serializer.h" +#include "common/debugapp.h" +#include #include "version.h" #include "window/mainwindow/mainwindow.h" @@ -55,6 +57,17 @@ #ifdef USE_CRASHPAD #include "common/crashpadinterface.h" #endif // USE_CRASHPAD + +static void config_error_handler(const char *title, const char *message, + void *) +{ + QWidget *parent = olive::Core::instance() ? + olive::Core::instance()->main_window() : + nullptr; + QMessageBox::critical(parent, QString::fromUtf8(title), + QString::fromUtf8(message), QMessageBox::Ok); +} + int decompress_project(const QString &project) { if (project.isEmpty()) { @@ -80,7 +93,7 @@ int decompress_project(const QString &project) .toUtf8() .constData()); - if (!olive::ProjectSerializer::check_compressed_id(&project_file)) { + if (!oakengine_serializer_check_compressed(project.toUtf8().constData())) { printf("%s\n", QCoreApplication::translate( "main", "Failed to decompress, project may be corrupt") @@ -182,7 +195,9 @@ int main(int argc, char *argv[]) } #endif - olive::Core::CoreParams startup_params; + OakEngineAppParams startup_params; + memset(&startup_params, 0, sizeof(startup_params)); + startup_params.run_mode = OAKENGINE_APP_RUN_NORMAL; CommandLineParser parser; @@ -282,26 +297,32 @@ int main(int argc, char *argv[]) } if (export_option->is_set()) { - startup_params.set_run_mode(olive::Core::CoreParams::k_headless_export); + startup_params.run_mode = OAKENGINE_APP_RUN_HEADLESS_EXPORT; } if (ts_option->is_set()) { if (ts_option->get_setting().isEmpty()) { qWarning() << "--ts was set but no translation file was provided"; } else { - startup_params.set_startup_language(ts_option->get_setting()); + QByteArray sl_utf = ts_option->get_setting().toUtf8(); + startup_params.startup_language = sl_utf.constData(); } } const bool load_plugins = !no_plugin->is_set(); if (crash_option->is_set()) { - startup_params.set_crash_on_startup(true); + startup_params.crash_on_startup = 1; } - startup_params.set_fullscreen(fullscreen_option->is_set()); + startup_params.fullscreen = fullscreen_option->is_set() ? 1 : 0; - startup_params.set_startup_project(project_argument->get_setting()); + { + QByteArray sp_utf = project_argument->get_setting().toUtf8(); + if (!sp_utf.isEmpty()) { + startup_params.startup_project = sp_utf.constData(); + } + } // Set OpenGL display profile. Oak's render pipeline still uses OpenGL // internally even when Vulkan is requested as the Qt graphics backend. @@ -328,7 +349,7 @@ int main(int argc, char *argv[]) // Create application instance std::unique_ptr a; - if (startup_params.run_mode() == olive::Core::CoreParams::k_run_normal) { + if (startup_params.run_mode == OAKENGINE_APP_RUN_NORMAL) { #ifdef _WIN32 // Since Oak Video Editor is linked with the console subsystem (for better POSIX compatibility), a console // is created by default. If the user didn't request one, we free it here. @@ -344,19 +365,15 @@ int main(int argc, char *argv[]) // Configuration errors are reported through a UI handler so the engine // layer (config) never has to know about dialogs - olive::Config::set_error_handler( - [](const QString &title, const QString &message) { - QWidget *parent = - olive::Core::instance() ? olive::Core::instance()->main_window() : - nullptr; - QMessageBox::critical(parent, title, message, QMessageBox::Ok); - }); + oakengine_config_set_error_handler(config_error_handler, NULL); - olive::Config::load(); + oakengine_config_load(); + char backend_buf[64]; + int backend_len = oakengine_config_get_string( + "GraphicsBackend", backend_buf, sizeof(backend_buf)); const QString graphics_backend = - olive::Config::current()[QStringLiteral("GraphicsBackend")] - .toString() - .toLower(); + backend_len > 0 ? QString::fromUtf8(backend_buf).toLower() : + QStringLiteral("opengl"); qputenv("QSG_RHI_BACKEND", graphics_backend == QStringLiteral("vulkan") ? QByteArrayLiteral("vulkan") : QByteArrayLiteral("opengl")); @@ -367,7 +384,7 @@ int main(int argc, char *argv[]) } if (load_plugins) { - olive::plugin::load_plugins("plugins"); + oakengine_plugin_load_plugins("plugins"); } #ifdef _WIN32 @@ -421,7 +438,7 @@ int main(int argc, char *argv[]) #endif // USE_CRASHPAD // Start core - olive::Core c(startup_params); + olive::Core c(&startup_params); c.start(); diff --git a/app/panel/curve/curve.h b/app/panel/curve/curve.h index 7180ca1f7..6eea55ddc 100644 --- a/app/panel/curve/curve.h +++ b/app/panel/curve/curve.h @@ -40,20 +40,25 @@ public: virtual void deselect_all() override; public slots: - void set_node(Node *node) + void set_node(OakEngineNode *node) { // Convert single pointer to either an empty vector or a vector of one QVector nodes; if (node) { - nodes.append(node); + nodes.append(reinterpret_cast(node)); } set_nodes(nodes); } +public: + // Not a slot: signature uses the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). All connections use new-style member-function syntax. void set_nodes(const QVector &nodes); +public slots: virtual void increase_track_height() override; virtual void decrease_track_height() override; diff --git a/app/panel/footageviewer/footageviewer.cpp b/app/panel/footageviewer/footageviewer.cpp index 714e9ad7f..711333d2d 100644 --- a/app/panel/footageviewer/footageviewer.cpp +++ b/app/panel/footageviewer/footageviewer.cpp @@ -45,12 +45,12 @@ void FootageViewerPanel::override_work_area(const TimeRange &r) get_footage_viewer_widget()->override_work_area(r); } -QVector FootageViewerPanel::get_selected_footage() const +QVector FootageViewerPanel::get_selected_footage() const { - QVector list; + QVector list; if (get_connected_viewer()) { - list.append(get_connected_viewer()); + list.append(reinterpret_cast(get_connected_viewer())); } return list; diff --git a/app/panel/footageviewer/footageviewer.h b/app/panel/footageviewer/footageviewer.h index f36dd118d..83c6443dc 100644 --- a/app/panel/footageviewer/footageviewer.h +++ b/app/panel/footageviewer/footageviewer.h @@ -47,7 +47,7 @@ public: return static_cast(get_time_based_widget()); } - virtual QVector get_selected_footage() const override; + virtual QVector get_selected_footage() const override; protected: virtual void retranslate() override; diff --git a/app/panel/node/node.h b/app/panel/node/node.h index b0693dd84..4428b67c3 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -25,6 +25,8 @@ #include "panel/panel.h" #include "widget/nodeview/nodewidget.h" +struct OakEngineNode; + namespace olive { @@ -117,21 +119,22 @@ public: } public slots: - void select(const QVector &p) + void select( + const QVector> &p) { node_widget_->view()->select(p, true); } signals: - void nodes_selected(const QVector &nodes); + void nodes_selected(const QVector &nodes); - void nodes_deselected(const QVector &nodes); + void nodes_deselected(const QVector &nodes); - void node_selection_changed(const QVector &nodes); - void - node_selection_changed_with_contexts(const QVector &nodes); + void node_selection_changed(const QVector &nodes); + void node_selection_changed_with_contexts( + const QVector> &nodes); - void node_group_opened(NodeGroup *group); + void node_group_opened(OakEngineNode *group); void node_group_closed(); diff --git a/app/panel/panelmanager.cpp b/app/panel/panelmanager.cpp index 4d12e601a..258c5ab7e 100644 --- a/app/panel/panelmanager.cpp +++ b/app/panel/panelmanager.cpp @@ -21,7 +21,7 @@ #include "panelmanager.h" -#include "config/config.h" +#include "common/configwrapper.h" namespace olive { diff --git a/app/panel/param/param.h b/app/panel/param/param.h index 72117c854..b20a321c8 100644 --- a/app/panel/param/param.h +++ b/app/panel/param/param.h @@ -50,7 +50,8 @@ public: } public slots: - void set_selected_nodes(const QVector &nodes) + void set_selected_nodes( + const QVector> &nodes) { get_param_view()->set_selected_nodes(nodes, false); } @@ -61,12 +62,17 @@ public slots: virtual void deselect_all() override; +public: + // Not a slot: signature uses the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). All connections use new-style member-function syntax. void set_contexts(const QVector &contexts); signals: - void focused_node_changed(Node *n); + void focused_node_changed(OakEngineNode *n); - void selected_nodes_changed(const QVector &nodes); + void selected_nodes_changed( + const QVector> &nodes); void request_viewer_to_start_editing_text(); diff --git a/app/panel/project/footagemanagementpanel.h b/app/panel/project/footagemanagementpanel.h index 176054936..de7f9e916 100644 --- a/app/panel/project/footagemanagementpanel.h +++ b/app/panel/project/footagemanagementpanel.h @@ -26,12 +26,14 @@ #include "node/project/footage/footage.h" +struct OakEngineNode; + namespace olive { class FootageManagementPanel { public: - virtual QVector get_selected_footage() const = 0; + virtual QVector get_selected_footage() const = 0; }; } diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index 63ff0cff4..384a42c93 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -25,6 +25,8 @@ #include #include "core.h" +#include "oakengine/events.h" +#include "oakengine/project.h" #include "node/project/sequence/sequence.h" #include "panel/footageviewer/footageviewer.h" #include "panel/timeline/timeline.h" @@ -36,6 +38,14 @@ namespace olive { +ProjectPanel::~ProjectPanel() +{ + if (project_name_sub_ > 0) { + oakengine_event_unsubscribe(project_name_sub_); + project_name_sub_ = 0; + } +} + ProjectPanel::ProjectPanel(const QString &unique_name) : PanelWidget(unique_name) { @@ -87,19 +97,24 @@ Project *ProjectPanel::project() const void ProjectPanel::set_project(Project *p) { if (project()) { - disconnect(project(), &Project::name_changed, this, - &ProjectPanel::update_subtitle); - disconnect(project(), &Project::name_changed, this, - &ProjectPanel::project_name_changed); + if (project_name_sub_ > 0) { + oakengine_event_unsubscribe(project_name_sub_); + project_name_sub_ = 0; + } } explorer_->set_project(p); if (project()) { - connect(project(), &Project::name_changed, this, - &ProjectPanel::update_subtitle); - connect(project(), &Project::name_changed, this, - &ProjectPanel::project_name_changed); + auto *ph = reinterpret_cast(project()); + project_name_sub_ = oakengine_event_subscribe( + ph, OAKENGINE_EVENT_PROJECT_NAME_CHANGED, + [](const oakengine_event *, void *userdata) { + auto *s = static_cast(userdata); + s->update_subtitle(); + s->project_name_changed(); + }, + this); } update_subtitle(); @@ -154,7 +169,7 @@ void ProjectPanel::rename_selected() explorer_->rename_selected_item(); } -void ProjectPanel::edit(Node *item) +void ProjectPanel::edit(OakEngineNode *item) { explorer_->edit(item); } @@ -170,8 +185,9 @@ void ProjectPanel::retranslate() update_subtitle(); } -void ProjectPanel::item_double_click_slot(Node *item) +void ProjectPanel::item_double_click_slot(OakEngineNode *item_handle) { + Node *item = reinterpret_cast(item_handle); if (item == nullptr) { // If the user double clicks on empty space, show the import dialog Core::instance()->dialog_import_show(); @@ -201,7 +217,11 @@ void ProjectPanel::show_new_menu() void ProjectPanel::update_subtitle() { if (project()) { - QString project_title = QStringLiteral("%1").arg(project()->name()); + char name_buf[256]; + oakengine_project_name( + reinterpret_cast(project()), + name_buf, sizeof(name_buf)); + QString project_title = QString::fromUtf8(name_buf); if (explorer_->get_root() != project()->root()) { QString folder_path; @@ -229,14 +249,14 @@ void ProjectPanel::save_connected_project() Core::instance()->save_project(); } -QVector ProjectPanel::get_selected_footage() const +QVector ProjectPanel::get_selected_footage() const { QVector items = selected_items(); - QVector footage; + QVector footage; foreach (Node *i, items) { if (dynamic_cast(i)) { - footage.append(static_cast(i)); + footage.append(reinterpret_cast(i)); } } diff --git a/app/panel/project/project.h b/app/panel/project/project.h index 2b9bae66a..decdbf764 100644 --- a/app/panel/project/project.h +++ b/app/panel/project/project.h @@ -27,6 +27,8 @@ #include "panel/panel.h" #include "widget/projectexplorer/projectexplorer.h" +struct OakEngineNode; + namespace olive { @@ -37,6 +39,7 @@ class ProjectPanel : public PanelWidget, public FootageManagementPanel { Q_OBJECT public: ProjectPanel(const QString &unique_name); + ~ProjectPanel() override; Project *project() const; void set_project(Project *p); @@ -49,7 +52,7 @@ public: Folder *get_selected_folder() const; - virtual QVector get_selected_footage() const override; + virtual QVector get_selected_footage() const override; ProjectViewModel *model() const; @@ -66,20 +69,23 @@ public: virtual void rename_selected() override; public slots: - void edit(Node *item); + void edit(OakEngineNode *item); signals: void project_name_changed(); - void selection_changed(const QVector &selected); + void selection_changed(const QVector &selected); private: virtual void retranslate() override; ProjectExplorer *explorer_; + // Event subscription IDs (replaces connect to Project signals) + int64_t project_name_sub_ = 0; + private slots: - void item_double_click_slot(Node *item); + void item_double_click_slot(OakEngineNode *item); void show_new_menu(); diff --git a/app/panel/scope/scope.cpp b/app/panel/scope/scope.cpp index 1f30bf5e6..844c10808 100644 --- a/app/panel/scope/scope.cpp +++ b/app/panel/scope/scope.cpp @@ -134,7 +134,7 @@ void ScopePanel::set_reference_buffer(TexturePtr frame) waveform_view_->set_buffer(frame); } -void ScopePanel::set_color_manager(ColorManager *manager) +void ScopePanel::set_color_manager(OakEngineColorManager *manager) { histogram_->connect_color_manager(manager); vectorscope_->connect_color_manager(manager); diff --git a/app/panel/scope/scope.h b/app/panel/scope/scope.h index ddb661d9b..8fbc66072 100644 --- a/app/panel/scope/scope.h +++ b/app/panel/scope/scope.h @@ -61,7 +61,7 @@ public: public slots: void set_reference_buffer(TexturePtr frame); - void set_color_manager(ColorManager *manager); + void set_color_manager(OakEngineColorManager *manager); protected: virtual void retranslate() override; diff --git a/app/panel/table/table.h b/app/panel/table/table.h index fefaaa8ec..fa37b2f69 100644 --- a/app/panel/table/table.h +++ b/app/panel/table/table.h @@ -33,7 +33,10 @@ class NodeTablePanel : public TimeBasedPanel { public: NodeTablePanel(); -public slots: +public: + // Not slots: signatures use the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). All connections use new-style member-function syntax. void select_nodes(const QVector &nodes) { static_cast(get_time_based_widget())->select_nodes(nodes); diff --git a/app/panel/taskmanager/taskmanager.cpp b/app/panel/taskmanager/taskmanager.cpp index 1868e9f3d..6208740a2 100644 --- a/app/panel/taskmanager/taskmanager.cpp +++ b/app/panel/taskmanager/taskmanager.cpp @@ -21,13 +21,15 @@ #include "taskmanager.h" -#include "task/taskmanager.h" +#include "engineeventbridge.h" +#include "oakengine/task.h" namespace olive { TaskManagerPanel::TaskManagerPanel() : PanelWidget(QStringLiteral("TaskManagerPanel")) + , bridge_(new EngineEventBridge(this)) { // Create task view view_ = new TaskView(this); @@ -35,15 +37,30 @@ TaskManagerPanel::TaskManagerPanel() // Set it as the main widget setWidget(view_); - // Connect task view to the task manager - connect(TaskManager::instance(), &TaskManager::task_added, view_, - &TaskView::add_task); - connect(TaskManager::instance(), &TaskManager::task_removed, view_, - &TaskView::remove_task); - connect(TaskManager::instance(), &TaskManager::task_failed, view_, - &TaskView::task_failed); - connect(view_, &TaskView::task_cancelled, TaskManager::instance(), - &TaskManager::cancel_task); + // Connect task view to the task manager via EngineEventBridge + bridge_->subscribe(oakengine_task_manager_handle(), + OAKENGINE_EVENT_TASK_MANAGER_TASK_ADDED); + bridge_->subscribe(oakengine_task_manager_handle(), + OAKENGINE_EVENT_TASK_MANAGER_TASK_REMOVED); + bridge_->subscribe(oakengine_task_manager_handle(), + OAKENGINE_EVENT_TASK_MANAGER_TASK_FAILED); + + connect(bridge_, &EngineEventBridge::task_manager_task_added, this, + [this](OakEngineTask *task, const QString &) { + view_->add_task(task); + }); + connect(bridge_, &EngineEventBridge::task_manager_task_removed, this, + [this](OakEngineTask *task) { + view_->remove_task(task); + }); + connect(bridge_, &EngineEventBridge::task_manager_task_failed, this, + [this](OakEngineTask *task) { + view_->task_failed(task); + }); + connect(view_, &TaskView::task_cancelled, this, + [](OakEngineTask *t) { + oakengine_task_manager_cancel(t); + }); // Set strings retranslate(); diff --git a/app/panel/taskmanager/taskmanager.h b/app/panel/taskmanager/taskmanager.h index 716de7a83..ec7c1fa45 100644 --- a/app/panel/taskmanager/taskmanager.h +++ b/app/panel/taskmanager/taskmanager.h @@ -28,6 +28,8 @@ namespace olive { +class EngineEventBridge; + /** * @brief A PanelWidget wrapper around a TaskView widget */ @@ -40,6 +42,8 @@ private: virtual void retranslate() override; TaskView *view_; + + EngineEventBridge *bridge_; }; } diff --git a/app/panel/timebased/timebased.cpp b/app/panel/timebased/timebased.cpp index 9658312d7..79d5e4d01 100644 --- a/app/panel/timebased/timebased.cpp +++ b/app/panel/timebased/timebased.cpp @@ -29,6 +29,11 @@ TimeBasedPanel::TimeBasedPanel(const QString &object_name) , widget_(nullptr) , show_and_raise_on_connect_(false) { + bridge_ = new EngineEventBridge(this); + connect(bridge_, &EngineEventBridge::node_label_changed, this, + [this](OakEngineNode *, const QString &label) { + set_subtitle(label); + }); } TimeBasedPanel::~TimeBasedPanel() @@ -142,16 +147,19 @@ void TimeBasedPanel::retranslate() } } -void TimeBasedPanel::connected_node_changed(ViewerOutput *old, ViewerOutput *now) +void TimeBasedPanel::connected_node_changed(OakEngineNode *old, OakEngineNode *now) { if (old) { - disconnect(old, &ViewerOutput::label_changed, this, - &TimeBasedPanel::set_subtitle); + if (label_sub_) { + bridge_->unsubscribe(label_sub_); + label_sub_ = 0; + } } if (now) { - connect(now, &ViewerOutput::label_changed, this, - &TimeBasedPanel::set_subtitle); + label_sub_ = bridge_->subscribe( + reinterpret_cast(now), + OAKENGINE_EVENT_NODE_LABEL_CHANGED); if (show_and_raise_on_connect_) { this->show(); diff --git a/app/panel/timebased/timebased.h b/app/panel/timebased/timebased.h index 4d3e4cdd6..dbffc3ef5 100644 --- a/app/panel/timebased/timebased.h +++ b/app/panel/timebased/timebased.h @@ -24,6 +24,7 @@ #include "panel/panel.h" #include "widget/timebased/timebasedwidget.h" +#include "engineeventbridge.h" namespace olive { @@ -141,8 +142,11 @@ private: bool show_and_raise_on_connect_; + EngineEventBridge *bridge_ = nullptr; + int64_t label_sub_ = 0; + private slots: - void connected_node_changed(ViewerOutput *old, ViewerOutput *now); + void connected_node_changed(OakEngineNode *old, OakEngineNode *now); }; } diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index 919e8cbec..8edf7db6b 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -171,13 +171,13 @@ void TimelinePanel::rename_selected() } void TimelinePanel::insert_footage_at_playhead( - const QVector &footage) + const QVector &footage) { timeline_widget()->insert_footage_at_playhead(footage); } void TimelinePanel::overwrite_footage_at_playhead( - const QVector &footage) + const QVector &footage) { timeline_widget()->overwrite_footage_at_playhead(footage); } diff --git a/app/panel/timeline/timeline.h b/app/panel/timeline/timeline.h index a9f5dbbb4..b8ae1cdac 100644 --- a/app/panel/timeline/timeline.h +++ b/app/panel/timeline/timeline.h @@ -103,9 +103,9 @@ public: timeline_widget()->nest_selected_clips(); } - void insert_footage_at_playhead(const QVector &footage); + void insert_footage_at_playhead(const QVector &footage); - void overwrite_footage_at_playhead(const QVector &footage); + void overwrite_footage_at_playhead(const QVector &footage); const QVector &get_selected_blocks() const { @@ -121,13 +121,13 @@ protected: virtual void retranslate() override; signals: - void block_selection_changed(const QVector &selected_blocks); + void block_selection_changed(const QVector &selected_blocks); void request_capture_start(const TimeRange &time, const Track::Reference &track); - void reveal_viewer_in_project(ViewerOutput *r); - void reveal_viewer_in_footage_viewer(ViewerOutput *r, const TimeRange &range); + void reveal_viewer_in_project(OakEngineNode *r); + void reveal_viewer_in_footage_viewer(OakEngineNode *r, const TimeRange &range); }; } diff --git a/app/panel/viewer/viewerbase.cpp b/app/panel/viewer/viewerbase.cpp index 3e8c2dad1..108e1e9ad 100644 --- a/app/panel/viewer/viewerbase.cpp +++ b/app/panel/viewer/viewerbase.cpp @@ -93,9 +93,9 @@ void ViewerPanelBase::set_full_screen(QScreen *screen) get_viewer_widget()->set_full_screen(screen); } -void ViewerPanelBase::set_gizmos(Node *node) +void ViewerPanelBase::set_gizmos(OakEngineNode *node) { - get_viewer_widget()->set_gizmos(node); + get_viewer_widget()->set_gizmos(reinterpret_cast(node)); } void ViewerPanelBase::cache_entire_sequence() diff --git a/app/panel/viewer/viewerbase.h b/app/panel/viewer/viewerbase.h index e15e1cd4a..0f1a25cdd 100644 --- a/app/panel/viewer/viewerbase.h +++ b/app/panel/viewer/viewerbase.h @@ -22,8 +22,11 @@ #ifndef OAK_VIEWERPANELBASE_H #define OAK_VIEWERPANELBASE_H +#include + #include "panel/pixelsampler/pixelsamplerpanel.h" #include "panel/timebased/timebased.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/viewer/viewer.h" namespace olive @@ -58,7 +61,7 @@ public: */ void set_full_screen(QScreen *screen = nullptr); - ColorManager *get_color_manager() + OakEngineColorManager *get_color_manager() { return get_viewer_widget()->color_manager(); } @@ -78,7 +81,7 @@ public: get_viewer_widget()->set_timeline_selected_blocks(b); } - void set_node_view_selections(const QVector &n) + void set_node_view_selections(const QVector &n) { get_viewer_widget()->set_node_view_selections(n); } @@ -89,7 +92,7 @@ public: } public slots: - void set_gizmos(Node *node); + void set_gizmos(OakEngineNode *node); void cache_entire_sequence(); @@ -109,12 +112,12 @@ signals: /** * @brief Wrapper for ViewerGLWidget::ColorProcessorChanged() */ - void color_processor_changed(ColorProcessorPtr processor); + void color_processor_changed(ColorProcessorHandlePtr processor); /** * @brief Wrapper for ViewerGLWidget::ColorManagerChanged() */ - void color_manager_changed(ColorManager *color_manager); + void color_manager_changed(OakEngineColorManager *color_manager); protected: void set_viewer_widget(ViewerWidget *vw); diff --git a/app/timeline/CMakeLists.txt b/app/timeline/CMakeLists.txt new file mode 100644 index 000000000..35c6f9924 --- /dev/null +++ b/app/timeline/CMakeLists.txt @@ -0,0 +1,23 @@ +# 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + timeline/timelinecoordinate.h + timeline/timelinecoordinate.cpp + PARENT_SCOPE +) diff --git a/app/timeline/timelinecoordinate.cpp b/app/timeline/timelinecoordinate.cpp new file mode 100644 index 000000000..90cc2c11d --- /dev/null +++ b/app/timeline/timelinecoordinate.cpp @@ -0,0 +1,67 @@ +/*** + + 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 . + +***/ + +#include "timelinecoordinate.h" + +namespace olive +{ + +TimelineCoordinate::TimelineCoordinate() + : track_(Track::k_none, 0) +{ +} + +TimelineCoordinate::TimelineCoordinate(const Rational &frame, + const Track::Reference &track) + : frame_(frame) + , track_(track) +{ +} + +TimelineCoordinate::TimelineCoordinate(const Rational &frame, + const Track::Type &track_type, + const int &track_index) + : frame_(frame) + , track_(track_type, track_index) +{ +} + +const Rational &TimelineCoordinate::get_frame() const +{ + return frame_; +} + +const Track::Reference &TimelineCoordinate::get_track() const +{ + return track_; +} + +void TimelineCoordinate::set_frame(const Rational &frame) +{ + frame_ = frame; +} + +void TimelineCoordinate::set_track(const Track::Reference &track) +{ + track_ = track; +} + +} diff --git a/app/timeline/timelinecoordinate.h b/app/timeline/timelinecoordinate.h new file mode 100644 index 000000000..08d1a5d01 --- /dev/null +++ b/app/timeline/timelinecoordinate.h @@ -0,0 +1,51 @@ +/*** + + 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_TIMELINECOORDINATE_H +#define OAK_TIMELINECOORDINATE_H + +#include "node/output/track/track.h" + +namespace olive +{ + +class TimelineCoordinate { +public: + TimelineCoordinate(); + TimelineCoordinate(const Rational &frame, const Track::Reference &track); + TimelineCoordinate(const Rational &frame, const Track::Type &track_type, + const int &track_index); + + const Rational &get_frame() const; + const Track::Reference &get_track() const; + + void set_frame(const Rational &frame); + void set_track(const Track::Reference &track); + +private: + Rational frame_; + + Track::Reference track_; +}; + +} + +#endif // OAK_TIMELINECOORDINATE_H diff --git a/app/ui/CMakeLists.txt b/app/ui/CMakeLists.txt index ca11f0f5e..0a0f94609 100644 --- a/app/ui/CMakeLists.txt +++ b/app/ui/CMakeLists.txt @@ -15,6 +15,7 @@ # along with this program. If not, see . add_subdirectory(cursors) +add_subdirectory(icons) add_subdirectory(graphics) add_subdirectory(style) diff --git a/app/ui/icons/CMakeLists.txt b/app/ui/icons/CMakeLists.txt new file mode 100644 index 000000000..b1cae78dc --- /dev/null +++ b/app/ui/icons/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + ui/icons/icons.h + ui/icons/icons.cpp + PARENT_SCOPE +) diff --git a/app/ui/icons/icons.cpp b/app/ui/icons/icons.cpp new file mode 100644 index 000000000..812eb9eb8 --- /dev/null +++ b/app/ui/icons/icons.cpp @@ -0,0 +1,224 @@ +/*** + + 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 . + +***/ + +#include "icons.h" + +#include + +namespace olive +{ + +/// Works in conjunction with `genicons.sh` to generate and utilize icons of specific sizes +const int icon_size_count = 4; +const int icon_sizes[] = { 16, 32, 64, 128 }; + +/// Internal icon library for use throughout Olive without having to regenerate constantly +QIcon icon::go_to_start; +QIcon icon::prev_frame; +QIcon icon::play; +QIcon icon::pause; +QIcon icon::next_frame; +QIcon icon::go_to_end; +QIcon icon::New; +QIcon icon::open; +QIcon icon::save; +QIcon icon::undo; +QIcon icon::redo; +QIcon icon::tree_view; +QIcon icon::list_view; +QIcon icon::icon_view; +QIcon icon::tool_pointer; +QIcon icon::tool_edit; +QIcon icon::tool_ripple; +QIcon icon::tool_rolling; +QIcon icon::tool_razor; +QIcon icon::tool_slip; +QIcon icon::tool_slide; +QIcon icon::tool_hand; +QIcon icon::tool_transition; +QIcon icon::tool_track_select; +QIcon icon::folder; +QIcon icon::sequence; +QIcon icon::video; +QIcon icon::audio; +QIcon icon::image; +QIcon icon::mini_map; +QIcon icon::tri_up; +QIcon icon::tri_left; +QIcon icon::tri_down; +QIcon icon::tri_right; +QIcon icon::text_bold; +QIcon icon::text_italic; +QIcon icon::text_underline; +QIcon icon::text_strikethrough; +QIcon icon::text_small_caps; +QIcon icon::text_align_left; +QIcon icon::text_align_right; +QIcon icon::text_align_center; +QIcon icon::text_align_justify; +QIcon icon::text_align_top; +QIcon icon::text_align_bottom; +QIcon icon::text_align_middle; +QIcon icon::snapping; +QIcon icon::zoom_in; +QIcon icon::zoom_out; +QIcon icon::record; +QIcon icon::add; +QIcon icon::error; +QIcon icon::dir_up; +QIcon icon::clock; +QIcon icon::diamond; +QIcon icon::plus; +QIcon icon::minus; +QIcon icon::add_effect; +QIcon icon::eye_opened; +QIcon icon::eye_closed; +QIcon icon::lock_opened; +QIcon icon::lock_closed; +QIcon icon::pencil; +QIcon icon::subtitles; +QIcon icon::color_picker; + +QIcon icon::from_name(const QString &name) +{ + static const QHash map = { + { QStringLiteral("prev"), &go_to_start }, + { QStringLiteral("rew"), &prev_frame }, + { QStringLiteral("play"), &play }, + { QStringLiteral("pause"), &pause }, + { QStringLiteral("ff"), &next_frame }, + { QStringLiteral("next"), &go_to_end }, + { QStringLiteral("new"), &New }, + { QStringLiteral("open"), &open }, + { QStringLiteral("save"), &save }, + { QStringLiteral("undo"), &undo }, + { QStringLiteral("redo"), &redo }, + { QStringLiteral("treeview"), &tree_view }, + { QStringLiteral("listview"), &list_view }, + { QStringLiteral("iconview"), &icon_view }, + { QStringLiteral("folder"), &folder }, + { QStringLiteral("sequence"), &sequence }, + { QStringLiteral("video"), &video }, + { QStringLiteral("audio"), &audio }, + { QStringLiteral("image"), &image }, + { QStringLiteral("subtitles"), &subtitles }, + { QStringLiteral("error"), &error }, + }; + + QIcon *icon_ptr = map.value(name, nullptr); + return icon_ptr ? *icon_ptr : QIcon(); +} + +void icon::load_all(const QString &theme) +{ + go_to_start = create(theme, "prev"); + prev_frame = create(theme, "rew"); + play = create(theme, "play"); + pause = create(theme, "pause"); + next_frame = create(theme, "ff"); + go_to_end = create(theme, "next"); + + New = create(theme, "new"); + open = create(theme, "open"); + save = create(theme, "save"); + undo = create(theme, "undo"); + redo = create(theme, "redo"); + tree_view = create(theme, "treeview"); + list_view = create(theme, "listview"); + icon_view = create(theme, "iconview"); + + tool_pointer = create(theme, "arrow"); + tool_edit = create(theme, "beam"); + tool_ripple = create(theme, "ripple"); + tool_rolling = create(theme, "rolling"); + tool_razor = create(theme, "razor"); + tool_slip = create(theme, "slip"); + tool_slide = create(theme, "slide"); + tool_hand = create(theme, "hand"); + tool_transition = create(theme, "transition-tool"); + tool_track_select = create(theme, "track-tool"); + + folder = create(theme, "folder"); + sequence = create(theme, "sequence"); + video = create(theme, "videosource"); + audio = create(theme, "audiosource"); + image = create(theme, "imagesource"); + + mini_map = create(theme, "map"); + + tri_up = create(theme, "tri-up"); + tri_left = create(theme, "tri-left"); + tri_down = create(theme, "tri-down"); + tri_right = create(theme, "tri-right"); + + text_bold = create(theme, "text-bold"); + text_italic = create(theme, "text-italic"); + text_underline = create(theme, "text-underline"); + text_strikethrough = create(theme, "text-strikethrough"); + text_small_caps = create(theme, "text-small-caps"); + text_align_left = create(theme, "align-left"); + text_align_right = create(theme, "align-right"); + text_align_center = create(theme, "align-center"); + text_align_justify = create(theme, "align-justify-all"); + text_align_top = create(theme, "align-v-top"); + text_align_bottom = create(theme, "align-v-bottom"); + text_align_middle = create(theme, "align-v-middle"); + + snapping = create(theme, "magnet"); + zoom_in = create(theme, "zoomin"); + zoom_out = create(theme, "zoomout"); + record = create(theme, "record"); + add = create(theme, "add-button"); + error = create(theme, "error"); + dir_up = create(theme, "dirup"); + clock = create(theme, "clock"); + diamond = create(theme, "diamond"); + plus = create(theme, "plus"); + minus = create(theme, "minus"); + add_effect = create(theme, "add-effect"); + color_picker = create(theme, "color-picker"); + + eye_opened = create(theme, "eye-opened"); + eye_closed = create(theme, "eye-closed"); + lock_opened = create(theme, "lock-opened"); + lock_closed = create(theme, "lock-closed"); + + pencil = create(theme, "text-edit"); + subtitles = create(theme, "subtitles"); +} + +QIcon icon::create(const QString &theme, const QString &name) +{ + QIcon icon; + + for (int i = 0; i < icon_size_count; i++) { + icon.addFile(QStringLiteral("%1/png/%2.%3.png") + .arg(theme, name, QString::number(icon_sizes[i])), + QSize(icon_sizes[i], icon_sizes[i]), QIcon::Normal); + icon.addFile(QStringLiteral("%1/png/%2.%3.disabled.png") + .arg(theme, name, QString::number(icon_sizes[i])), + QSize(icon_sizes[i], icon_sizes[i]), QIcon::Disabled); + } + + return icon; +} + +} diff --git a/app/ui/icons/icons.h b/app/ui/icons/icons.h new file mode 100644 index 000000000..b2c8e48c5 --- /dev/null +++ b/app/ui/icons/icons.h @@ -0,0 +1,169 @@ +/*** + + 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_ICONS_H +#define OAK_ICONS_H + +#include + +#include "common/define.h" + +namespace olive +{ + +namespace icon +{ + +// Playback Icons +extern QIcon go_to_start; +extern QIcon prev_frame; +extern QIcon play; +extern QIcon pause; +extern QIcon next_frame; +extern QIcon go_to_end; + +// Project Management Toolbar Icons +extern QIcon New; +extern QIcon open; +extern QIcon save; +extern QIcon undo; +extern QIcon redo; +extern QIcon tree_view; +extern QIcon list_view; +extern QIcon icon_view; + +// Tool Icons +extern QIcon tool_pointer; +extern QIcon tool_edit; +extern QIcon tool_ripple; +extern QIcon tool_rolling; +extern QIcon tool_razor; +extern QIcon tool_slip; +extern QIcon tool_slide; +extern QIcon tool_hand; +extern QIcon tool_transition; +extern QIcon tool_track_select; + +// Project Icons +extern QIcon folder; +extern QIcon sequence; +extern QIcon video; +extern QIcon audio; +extern QIcon image; + +// Node Icons +extern QIcon mini_map; + +// Triangle Arrows +extern QIcon tri_up; +extern QIcon tri_left; +extern QIcon tri_down; +extern QIcon tri_right; + +// Text +extern QIcon text_bold; +extern QIcon text_italic; +extern QIcon text_underline; +extern QIcon text_strikethrough; +extern QIcon text_small_caps; +extern QIcon text_align_left; +extern QIcon text_align_right; +extern QIcon text_align_center; +extern QIcon text_align_justify; +extern QIcon text_align_top; +extern QIcon text_align_bottom; +extern QIcon text_align_middle; + +// Miscellaneous Icons +extern QIcon snapping; +extern QIcon zoom_in; +extern QIcon zoom_out; +extern QIcon record; +extern QIcon add; +extern QIcon error; +extern QIcon dir_up; +extern QIcon clock; +extern QIcon diamond; +extern QIcon plus; +extern QIcon minus; +extern QIcon add_effect; +extern QIcon eye_opened; +extern QIcon eye_closed; +extern QIcon lock_opened; +extern QIcon lock_closed; +extern QIcon pencil; +extern QIcon subtitles; +extern QIcon color_picker; + +/** + * @brief Look up a loaded icon by its resource name + * + * Engine-side metadata (e.g. Node::data(Node::icon)) carries icon identity as a + * plain resource name string ("folder", "video", ...) so the headless engine + * never has to depend on QIcon. This maps such a name back to the corresponding + * globally loaded icon. Returns a null QIcon for unknown names. + */ +QIcon from_name(const QString &name); + +/** + * @brief Create an icon object loaded from file + * + * Using `name`, this function will load icon files to create an icon object that can be used throughout the + * application. + * + * Olive's icons are stored in a very specific format. They are all sourced from SVGs, but stored as PNGs of various + * sizes. See `app/ui/icons/genicons.sh`, as this script not only generates the multiple sizes but also the QRC file + * used to compile the icons into the executable. + * + * This function is heavily tied into `genicons.sh` and will load all the different sized images (using the same + * filename formatting and QRC resource directory) that `genicons.sh` generates into one QIcon file. If you change + * either this function or `genicons.sh`, you will very likely have to change the other too. + * + * There is not much reason to call this outside of LoadAll() (which stores icons globally in memory so they don't + * have to be reloaded each time a new object needs an icon). + * + * @param theme + * + * Name of the theme (used in the URL as the folder to load PNGs from) + * + * @param name + * + * Name of the icon (will correspond to the original SVG's filename with no path or extension) + * + * @return + * + * A QIcon object containing the various icon sizes loaded from resource + */ +QIcon create(const QString &theme, const QString &name); + +/** + * @brief Methodically load all Olive icons into global variables that can be accessed throughout the application + * + * It's recommended to load any UI icons here so they're ready at startup and don't need to be re-loaded upon each + * use. + */ +void load_all(const QString &theme); + +} + +} + +#endif // OAK_ICONS_H diff --git a/app/ui/style/style.cpp b/app/ui/style/style.cpp index 6728d4c94..3ecf0a6c4 100644 --- a/app/ui/style/style.cpp +++ b/app/ui/style/style.cpp @@ -29,7 +29,7 @@ #include #include -#include "config/config.h" +#include "common/configwrapper.h" #include "ui/icons/icons.h" namespace olive diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp index eb58d36d9..15e8477b3 100644 --- a/app/widget/audiomonitor/audiomonitor.cpp +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -28,6 +28,7 @@ #include "audio/audiolevelmeter.h" #include "audio/audiomanager.h" #include "common/decibel.h" +#include "oakengine/preview.h" #include "common/qtutils.h" namespace olive @@ -90,10 +91,20 @@ void AudioMonitor::push_sample_buffer(const SampleBuffer &d) QVector v(params_.channel_count(), 0); - const AudioLevelMeter::Stats stats = - AudioLevelMeter::analyze_sample_buffer(d); - for (int i = 0; i < v.size() && i < stats.channels.size(); i++) { - v[i] = stats.channels.at(i).peak_linear; + if (d.is_allocated() && d.channel_count() > 0) { + int channels = d.channel_count(); + QVector ptrs(channels); + for (int ch = 0; ch < channels; ch++) { + ptrs[ch] = d.data(ch); + } + QVector levels(channels, 0.0); + if (oakengine_audio_analyze_levels(ptrs.data(), channels, + d.sample_count(), + levels.data()) == OAKENGINE_OK) { + for (int i = 0; i < v.size() && i < levels.size(); i++) { + v[i] = levels[i]; + } + } } // Fill values because they get averaged out for smoothing @@ -102,12 +113,14 @@ void AudioMonitor::push_sample_buffer(const SampleBuffer &d) set_update_loop(true); } -void AudioMonitor::start_waveform(const AudioWaveformCache *waveform, +void AudioMonitor::start_waveform(const void *waveform, const Rational &start, int playback_speed) { stop(); - waveform_length_ = waveform->length(); + const AudioWaveformCache *cache = + static_cast(waveform); + waveform_length_ = cache->length(); if (start >= waveform_length_) { return; } @@ -442,8 +455,10 @@ void AudioMonitor::update_values_from_waveform(QVector &v, // Delta time is provided in milliseconds, so we convert to seconds in Rational Rational length(delta_time, 1000); + const AudioWaveformCache *cache = + static_cast(waveform_); AudioVisualWaveform::Sample sum = - waveform_->get_summary_from_time(waveform_time_, length); + cache->get_summary_from_time(waveform_time_, length); audio_visual_waveform_sample_to_internal_values(sum, v); diff --git a/app/widget/audiomonitor/audiomonitor.h b/app/widget/audiomonitor/audiomonitor.h index c51e9ae56..3d4b9335c 100644 --- a/app/widget/audiomonitor/audiomonitor.h +++ b/app/widget/audiomonitor/audiomonitor.h @@ -45,7 +45,7 @@ public: return waveform_; } - static void start_waveform_on_all(const AudioWaveformCache *waveform, + static void start_waveform_on_all(const void *waveform, const Rational &start, int playback_speed) { foreach (AudioMonitor *m, instances) { @@ -74,7 +74,7 @@ public slots: void push_sample_buffer(const SampleBuffer &samples); - void start_waveform(const AudioWaveformCache *waveform, + void start_waveform(const void *waveform, const Rational &start, int playback_speed); protected: @@ -100,7 +100,7 @@ private: qint64 last_time_; - const AudioWaveformCache *waveform_; + const void *waveform_; Rational waveform_time_; Rational waveform_length_; diff --git a/app/widget/colorbutton/colorbutton.cpp b/app/widget/colorbutton/colorbutton.cpp index bbac6e2d4..f8e5605a6 100644 --- a/app/widget/colorbutton/colorbutton.cpp +++ b/app/widget/colorbutton/colorbutton.cpp @@ -21,12 +21,13 @@ #include "colorbutton.h" +#include "common/qtutils.h" #include "dialog/color/colordialog.h" namespace olive { -ColorButton::ColorButton(ColorManager *color_manager, bool show_dialog_on_click, +ColorButton::ColorButton(OakEngineColorManager *color_manager, bool show_dialog_on_click, QWidget *parent) : QPushButton(parent) , color_manager_(color_manager) @@ -52,10 +53,29 @@ void ColorButton::set_color(const ManagedColor &c) { color_ = c; - color_.set_color_input( - color_manager_->get_compliant_color_space(color_.color_input())); - color_.set_color_output( - color_manager_->get_compliant_color_space(color_.color_output())); + QByteArray in_name = color_.color_input().toUtf8(); + QString compliant_in = oak_query_string([this, &in_name](char *buf, int size) { + return oakengine_color_manager_compliant_color_space( + color_manager_, in_name.constData(), buf, size); + }); + color_.set_color_input(compliant_in); + + QByteArray out_name = color_.color_output().output().toUtf8(); + ColorTransform cs_out = color_.color_output(); + QByteArray o, v, l; + oak_color_transform pod = oak_to_transform(cs_out, &o, &v, &l); + int out_is_display = 0; + char out_buf[256], view_buf[256], look_buf[256]; + oakengine_color_manager_compliant_transform( + color_manager_, &pod, 0, &out_is_display, out_buf, sizeof(out_buf), + view_buf, sizeof(view_buf), look_buf, sizeof(look_buf)); + if (out_is_display) { + color_.set_color_output(ColorTransform(QString::fromUtf8(out_buf), + QString::fromUtf8(view_buf), + QString::fromUtf8(look_buf))); + } else { + color_.set_color_output(ColorTransform(QString::fromUtf8(out_buf))); + } update_color(); } @@ -92,10 +112,17 @@ void ColorButton::color_dialog_finished(int e) void ColorButton::update_color() { - color_processor_ = ColorProcessor::create( - color_manager_, color_.color_input(), color_.color_output()); + QByteArray in_cs = color_.color_input().toUtf8(); + ColorTransform out = color_.color_output(); + QByteArray o, v, l; + oak_color_transform out_pod = oak_to_transform(out, &o, &v, &l); + color_processor_ = ColorProcessorHandlePtr( + oakengine_color_processor_create(color_manager_, in_cs.constData(), + &out_pod, + OAKENGINE_COLOR_PROCESSOR_NORMAL), + ColorProcessorHandleDeleter()); - QColor managed = QtUtils::to_q_color(color_processor_->convert_color(color_)); + QColor managed = QtUtils::to_q_color(oak_convert_color(color_processor_, color_)); setStyleSheet(QStringLiteral("%1--ColorButton {background: %2;}") .arg(MACRO_VAL_AS_STR(olive), managed.name())); diff --git a/app/widget/colorbutton/colorbutton.h b/app/widget/colorbutton/colorbutton.h index 34e0df117..b34a39fbd 100644 --- a/app/widget/colorbutton/colorbutton.h +++ b/app/widget/colorbutton/colorbutton.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" namespace olive { @@ -33,9 +33,9 @@ namespace olive class ColorButton : public QPushButton { Q_OBJECT public: - ColorButton(ColorManager *color_manager, bool show_dialog_on_click, + ColorButton(OakEngineColorManager *color_manager, bool show_dialog_on_click, QWidget *parent = nullptr); - ColorButton(ColorManager *color_manager, QWidget *parent = nullptr) + ColorButton(OakEngineColorManager *color_manager, QWidget *parent = nullptr) : ColorButton(color_manager, true, parent) { } @@ -56,11 +56,11 @@ private slots: private: void update_color(); - ColorManager *color_manager_; + OakEngineColorManager *color_manager_; ManagedColor color_; - ColorProcessorPtr color_processor_; + ColorProcessorHandlePtr color_processor_; bool dialog_open_; }; diff --git a/app/widget/colorwheel/colorpreviewbox.cpp b/app/widget/colorwheel/colorpreviewbox.cpp index c4459b701..44d9724a9 100644 --- a/app/widget/colorwheel/colorpreviewbox.cpp +++ b/app/widget/colorwheel/colorpreviewbox.cpp @@ -35,8 +35,8 @@ ColorPreviewBox::ColorPreviewBox(QWidget *parent) { } -void ColorPreviewBox::set_color_processor(ColorProcessorPtr to_ref, - ColorProcessorPtr to_display) +void ColorPreviewBox::set_color_processor(ColorProcessorHandlePtr to_ref, + ColorProcessorHandlePtr to_display) { to_ref_processor_ = to_ref; to_display_processor_ = to_display; @@ -58,8 +58,8 @@ void ColorPreviewBox::paintEvent(QPaintEvent *e) // Color management if (to_ref_processor_ && to_display_processor_) { - c = QtUtils::to_q_color(to_display_processor_->convert_color( - to_ref_processor_->convert_color(color_))); + c = QtUtils::to_q_color(oak_convert_color(to_display_processor_, + oak_convert_color(to_ref_processor_, color_))); } else { c = QtUtils::to_q_color(color_); } diff --git a/app/widget/colorwheel/colorpreviewbox.h b/app/widget/colorwheel/colorpreviewbox.h index 89c3c5ed4..03eb2365c 100644 --- a/app/widget/colorwheel/colorpreviewbox.h +++ b/app/widget/colorwheel/colorpreviewbox.h @@ -24,7 +24,7 @@ #include -#include "render/colorprocessor.h" +#include "widget/manageddisplay/colorprocessorhandle.h" namespace olive { @@ -34,8 +34,8 @@ class ColorPreviewBox : public QWidget { public: ColorPreviewBox(QWidget *parent = nullptr); - void set_color_processor(ColorProcessorPtr to_ref, - ColorProcessorPtr to_display); + void set_color_processor(ColorProcessorHandlePtr to_ref, + ColorProcessorHandlePtr to_display); public slots: void set_color(const Color &c); @@ -46,9 +46,9 @@ protected: private: Color color_; - ColorProcessorPtr to_ref_processor_; + ColorProcessorHandlePtr to_ref_processor_; - ColorProcessorPtr to_display_processor_; + ColorProcessorHandlePtr to_display_processor_; }; } diff --git a/app/widget/colorwheel/colorspacechooser.cpp b/app/widget/colorwheel/colorspacechooser.cpp index 2a1bcc477..b6c324d50 100644 --- a/app/widget/colorwheel/colorspacechooser.cpp +++ b/app/widget/colorwheel/colorspacechooser.cpp @@ -24,10 +24,12 @@ #include #include +#include "widget/manageddisplay/colorprocessorhandle.h" + namespace olive { -ColorSpaceChooser::ColorSpaceChooser(ColorManager *color_manager, +ColorSpaceChooser::ColorSpaceChooser(OakEngineColorManager *color_manager, bool enable_input_field, bool enable_display_fields, QWidget *parent) @@ -56,15 +58,25 @@ ColorSpaceChooser::ColorSpaceChooser(ColorManager *color_manager, input_combobox_ = new QComboBox(); layout->addWidget(input_combobox_, row, 1); - QStringList input_spaces = color_manager->list_available_colorspaces(); + QStringList input_spaces = oak_query_string_list( + [this]() { + return oakengine_color_manager_colorspace_count(color_manager_); + }, + [this](int i, char *buf, int size) { + return oakengine_color_manager_colorspace_at(color_manager_, i, + buf, size); + }); foreach (const QString &s, input_spaces) { input_combobox_->addItem(s); } - if (!color_manager_->get_default_input_color_space().isEmpty()) { - input_combobox_->setCurrentText( - color_manager_->get_default_input_color_space()); + QString def_input = oak_query_string([this](char *buf, int size) { + return oakengine_color_manager_default_input_color_space( + color_manager_, buf, size); + }); + if (!def_input.isEmpty()) { + input_combobox_->setCurrentText(def_input); } connect(input_combobox_, &QComboBox::currentTextChanged, this, @@ -82,14 +94,24 @@ ColorSpaceChooser::ColorSpaceChooser(ColorManager *color_manager, display_combobox_ = new QComboBox(); layout->addWidget(display_combobox_, row, 1); - QStringList display_spaces = color_manager->list_available_displays(); + QStringList display_spaces = oak_query_string_list( + [this]() { + return oakengine_color_manager_display_count(color_manager_); + }, + [this](int i, char *buf, int size) { + return oakengine_color_manager_display_at(color_manager_, i, + buf, size); + }); foreach (const QString &s, display_spaces) { display_combobox_->addItem(s); } display_combobox_->setCurrentText( - color_manager_->get_default_display()); + oak_query_string([this](char *buf, int size) { + return oakengine_color_manager_default_display( + color_manager_, buf, size); + })); connect(display_combobox_, &QComboBox::currentTextChanged, this, &ColorSpaceChooser::combo_box_changed); @@ -117,7 +139,14 @@ ColorSpaceChooser::ColorSpaceChooser(ColorManager *color_manager, look_combobox_ = new QComboBox(); layout->addWidget(look_combobox_, row, 1); - QStringList looks = color_manager->list_available_looks(); + QStringList looks = oak_query_string_list( + [this]() { + return oakengine_color_manager_look_count(color_manager_); + }, + [this](int i, char *buf, int size) { + return oakengine_color_manager_look_at(color_manager_, i, + buf, size); + }); look_combobox_->addItem(tr("(None)"), QString()); @@ -155,20 +184,32 @@ ColorTransform ColorSpaceChooser::output() const void ColorSpaceChooser::set_input(const QString &s) { - input_combobox_->setCurrentText(color_manager_->get_compliant_color_space(s)); + QByteArray name = s.toUtf8(); + QString compliant = oak_query_string([this, &name](char *buf, int size) { + return oakengine_color_manager_compliant_color_space( + color_manager_, name.constData(), buf, size); + }); + input_combobox_->setCurrentText(compliant); } void ColorSpaceChooser::set_output(const ColorTransform &out) { - ColorTransform compliant = color_manager_->get_compliant_color_space(out); + QByteArray o, v, l; + oak_color_transform pod = oak_to_transform(out, &o, &v, &l); + int out_is_display = 0; + char out_buf[256], view_buf[256], look_buf[256]; + oakengine_color_manager_compliant_transform( + color_manager_, &pod, 0, &out_is_display, out_buf, sizeof(out_buf), + view_buf, sizeof(view_buf), look_buf, sizeof(look_buf)); - display_combobox_->setCurrentText(compliant.display()); - view_combobox_->setCurrentText(compliant.view()); + display_combobox_->setCurrentText(QString::fromUtf8(out_buf)); + view_combobox_->setCurrentText(QString::fromUtf8(view_buf)); - if (compliant.look().isEmpty()) { + QString look_str = QString::fromUtf8(look_buf); + if (look_str.isEmpty()) { look_combobox_->setCurrentIndex(0); } else { - look_combobox_->setCurrentText(compliant.look()); + look_combobox_->setCurrentText(look_str); } } @@ -178,7 +219,17 @@ void ColorSpaceChooser::update_views(const QString &display) view_combobox_->clear(); - QStringList views = color_manager_->list_available_views(display); + QByteArray disp = display.toUtf8(); + QStringList views = oak_query_string_list( + [this, &disp]() { + return oakengine_color_manager_view_count(color_manager_, + disp.constData()); + }, + [this, &disp](int i, char *buf, int size) { + return oakengine_color_manager_view_at(color_manager_, + disp.constData(), i, buf, + size); + }); foreach (const QString &s, views) { view_combobox_->addItem(s); @@ -189,7 +240,11 @@ void ColorSpaceChooser::update_views(const QString &display) view_combobox_->setCurrentText(v); } else { // Otherwise reset to default view for this display - view_combobox_->setCurrentText(color_manager_->get_default_view(display)); + view_combobox_->setCurrentText( + oak_query_string([this, &disp](char *buf, int size) { + return oakengine_color_manager_default_view( + color_manager_, disp.constData(), buf, size); + })); } } diff --git a/app/widget/colorwheel/colorspacechooser.h b/app/widget/colorwheel/colorspacechooser.h index a8108356c..24b541d32 100644 --- a/app/widget/colorwheel/colorspacechooser.h +++ b/app/widget/colorwheel/colorspacechooser.h @@ -25,7 +25,8 @@ #include #include -#include "node/color/colormanager/colormanager.h" +#include "oakengine/color.h" +#include "widget/manageddisplay/colorprocessorhandle.h" namespace olive { @@ -33,7 +34,7 @@ namespace olive class ColorSpaceChooser : public QGroupBox { Q_OBJECT public: - ColorSpaceChooser(ColorManager *color_manager, + ColorSpaceChooser(OakEngineColorManager *color_manager, bool enable_input_field = true, bool enable_display_fields = true, QWidget *parent = nullptr); @@ -55,7 +56,7 @@ private slots: void update_views(const QString &display); private: - ColorManager *color_manager_; + OakEngineColorManager *color_manager_; QComboBox *input_combobox_; diff --git a/app/widget/colorwheel/colorswatchchooser.cpp b/app/widget/colorwheel/colorswatchchooser.cpp index c609f1819..0151e5a89 100644 --- a/app/widget/colorwheel/colorswatchchooser.cpp +++ b/app/widget/colorwheel/colorswatchchooser.cpp @@ -39,7 +39,7 @@ const Color k_default_colors[k_default_color_count] = { Color(0.0, 0.0, 0.0) }; -ColorSwatchChooser::ColorSwatchChooser(ColorManager *manager, QWidget *parent) +ColorSwatchChooser::ColorSwatchChooser(OakEngineColorManager *manager, QWidget *parent) : QWidget(parent) { auto layout = new QGridLayout(this); diff --git a/app/widget/colorwheel/colorswatchchooser.h b/app/widget/colorwheel/colorswatchchooser.h index e748f2df8..08249545c 100644 --- a/app/widget/colorwheel/colorswatchchooser.h +++ b/app/widget/colorwheel/colorswatchchooser.h @@ -22,7 +22,7 @@ #ifndef OAK_COLORSWATCHCHOOSER_H #define OAK_COLORSWATCHCHOOSER_H -#include "node/color/colormanager/colormanager.h" +#include "oakengine/color.h" #include "widget/colorbutton/colorbutton.h" namespace olive @@ -31,7 +31,7 @@ namespace olive class ColorSwatchChooser : public QWidget { Q_OBJECT public: - ColorSwatchChooser(ColorManager *manager, QWidget *parent = nullptr); + ColorSwatchChooser(OakEngineColorManager *manager, QWidget *parent = nullptr); public slots: void set_current_color(const ManagedColor &c) diff --git a/app/widget/colorwheel/colorswatchwidget.cpp b/app/widget/colorwheel/colorswatchwidget.cpp index ab29aa445..142cfa019 100644 --- a/app/widget/colorwheel/colorswatchwidget.cpp +++ b/app/widget/colorwheel/colorswatchwidget.cpp @@ -40,8 +40,8 @@ const Color &ColorSwatchWidget::get_selected_color() const return selected_color_; } -void ColorSwatchWidget::set_color_processor(ColorProcessorPtr to_linear, - ColorProcessorPtr to_display) +void ColorSwatchWidget::set_color_processor(ColorProcessorHandlePtr to_linear, + ColorProcessorHandlePtr to_display) { to_linear_processor_ = to_linear; to_display_processor_ = to_display; @@ -86,8 +86,8 @@ Qt::GlobalColor ColorSwatchWidget::get_ui_selector_color() const Color ColorSwatchWidget::get_managed_color(const Color &input) const { if (to_linear_processor_ && to_display_processor_) { - return to_display_processor_->convert_color( - to_linear_processor_->convert_color(input)); + return oak_convert_color(to_display_processor_, + oak_convert_color(to_linear_processor_, input)); } return input; diff --git a/app/widget/colorwheel/colorswatchwidget.h b/app/widget/colorwheel/colorswatchwidget.h index 9fbeba4a7..b95ea5408 100644 --- a/app/widget/colorwheel/colorswatchwidget.h +++ b/app/widget/colorwheel/colorswatchwidget.h @@ -24,7 +24,7 @@ #include -#include "render/colorprocessor.h" +#include "widget/manageddisplay/colorprocessorhandle.h" namespace olive { @@ -36,8 +36,8 @@ public: const Color &get_selected_color() const; - void set_color_processor(ColorProcessorPtr to_linear, - ColorProcessorPtr to_display); + void set_color_processor(ColorProcessorHandlePtr to_linear, + ColorProcessorHandlePtr to_display); public slots: void set_selected_color(const Color &c); @@ -63,9 +63,9 @@ private: Color selected_color_; - ColorProcessorPtr to_linear_processor_; + ColorProcessorHandlePtr to_linear_processor_; - ColorProcessorPtr to_display_processor_; + ColorProcessorHandlePtr to_display_processor_; }; } diff --git a/app/widget/colorwheel/colorvalueswidget.cpp b/app/widget/colorwheel/colorvalueswidget.cpp index 0c5bf295c..80df678bd 100644 --- a/app/widget/colorwheel/colorvalueswidget.cpp +++ b/app/widget/colorwheel/colorvalueswidget.cpp @@ -25,14 +25,14 @@ #include #include -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" #include "ui/icons/icons.h" namespace olive { -ColorValuesWidget::ColorValuesWidget(ColorManager *manager, QWidget *parent) +ColorValuesWidget::ColorValuesWidget(OakEngineColorManager *manager, QWidget *parent) : QWidget(parent) , manager_(manager) , input_to_ref_(nullptr) @@ -100,10 +100,10 @@ Color ColorValuesWidget::get_color() const return reference_tab_->get_color(); } -void ColorValuesWidget::set_color_processor(ColorProcessorPtr input_to_ref, - ColorProcessorPtr ref_to_display, - ColorProcessorPtr display_to_ref, - ColorProcessorPtr ref_to_input) +void ColorValuesWidget::set_color_processor(ColorProcessorHandlePtr input_to_ref, + ColorProcessorHandlePtr ref_to_display, + ColorProcessorHandlePtr display_to_ref, + ColorProcessorHandlePtr ref_to_input) { input_to_ref_ = input_to_ref; ref_to_display_ = ref_to_display; @@ -200,7 +200,7 @@ void ColorValuesWidget::update_input_from_ref() { if (ref_to_input_) { input_tab_->set_color( - ref_to_input_->convert_color(reference_tab_->get_color())); + oak_convert_color(ref_to_input_, reference_tab_->get_color())); } else { input_tab_->set_color(reference_tab_->get_color()); } @@ -213,7 +213,7 @@ void ColorValuesWidget::update_display_from_ref() { if (ref_to_display_) { display_tab_->set_color( - ref_to_display_->convert_color(reference_tab_->get_color())); + oak_convert_color(ref_to_display_, reference_tab_->get_color())); } else { display_tab_->set_color(reference_tab_->get_color()); } @@ -223,7 +223,7 @@ void ColorValuesWidget::update_ref_from_input() { if (input_to_ref_) { reference_tab_->set_color( - input_to_ref_->convert_color(input_tab_->get_color())); + oak_convert_color(input_to_ref_, input_tab_->get_color())); } else { reference_tab_->set_color(input_tab_->get_color()); } @@ -233,7 +233,7 @@ void ColorValuesWidget::update_ref_from_display() { if (display_to_ref_) { reference_tab_->set_color( - display_to_ref_->convert_color(display_tab_->get_color())); + oak_convert_color(display_to_ref_, display_tab_->get_color())); } else { reference_tab_->set_color(display_tab_->get_color()); } diff --git a/app/widget/colorwheel/colorvalueswidget.h b/app/widget/colorwheel/colorvalueswidget.h index 1e659f9f3..9cc3479f4 100644 --- a/app/widget/colorwheel/colorvalueswidget.h +++ b/app/widget/colorwheel/colorvalueswidget.h @@ -27,7 +27,8 @@ #include #include "colorpreviewbox.h" -#include "node/color/colormanager/colormanager.h" +#include "oakengine/color.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/slider/floatslider.h" #include "widget/slider/stringslider.h" @@ -87,14 +88,14 @@ private slots: class ColorValuesWidget : public QWidget { Q_OBJECT public: - ColorValuesWidget(ColorManager *manager, QWidget *parent = nullptr); + ColorValuesWidget(OakEngineColorManager *manager, QWidget *parent = nullptr); Color get_color() const; - void set_color_processor(ColorProcessorPtr input_to_ref, - ColorProcessorPtr ref_to_display, - ColorProcessorPtr display_to_ref, - ColorProcessorPtr ref_to_input); + void set_color_processor(ColorProcessorHandlePtr input_to_ref, + ColorProcessorHandlePtr ref_to_display, + ColorProcessorHandlePtr display_to_ref, + ColorProcessorHandlePtr ref_to_input); virtual bool eventFilter(QObject *watcher, QEvent *event) override; @@ -120,7 +121,7 @@ private: void update_ref_from_display(); - ColorManager *manager_; + OakEngineColorManager *manager_; ColorPreviewBox *preview_; @@ -130,13 +131,13 @@ private: ColorValuesTab *display_tab_; - ColorProcessorPtr input_to_ref_; + ColorProcessorHandlePtr input_to_ref_; - ColorProcessorPtr ref_to_display_; + ColorProcessorHandlePtr ref_to_display_; - ColorProcessorPtr display_to_ref_; + ColorProcessorHandlePtr display_to_ref_; - ColorProcessorPtr ref_to_input_; + ColorProcessorHandlePtr ref_to_input_; QPushButton *color_picker_btn_; diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index 855a4c320..bd5820caf 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -29,8 +29,11 @@ #include #include "common/decibel.h" +#include "common/nodevaluehandle.h" +#include "common/oakvaluehelper.h" #include "common/qtutils.h" #include "oakengine/node.h" +#include "widget/keyframeview/keyframehandle.h" namespace olive { @@ -441,22 +444,27 @@ void CurveView::first_chance_mouse_move(QMouseEvent *event) // If the user is NOT holding control, we set the other handle to the exact negative of this handle QPointF new_opposing_pos; - NodeKeyframe::BezierType opposing_type = - NodeKeyframe::get_opposing_bezier_type(dragging_bezier_pt_->type); + int opposing_type = + oakengine_keyframe_opposing_bezier_type(dragging_bezier_pt_->type); if (!(event->modifiers() & Qt::ControlModifier)) { new_opposing_pos = generate_bezier_control_position( - opposing_type, dragging_bezier_point_opposing_start_, + static_cast(opposing_type), + dragging_bezier_point_opposing_start_, -mouse_diff_scaled); } else { new_opposing_pos = dragging_bezier_point_opposing_start_; } - dragging_bezier_pt_->keyframe->set_bezier_control(dragging_bezier_pt_->type, - new_bezier_pos); + oakengine_keyframe_set_bezier_point_live( + reinterpret_cast(dragging_bezier_pt_->keyframe), + dragging_bezier_pt_->type, + new_bezier_pos.x(), new_bezier_pos.y()); - dragging_bezier_pt_->keyframe->set_bezier_control(opposing_type, - new_opposing_pos); + oakengine_keyframe_set_bezier_point_live( + reinterpret_cast(dragging_bezier_pt_->keyframe), + opposing_type, + new_opposing_pos.x(), new_opposing_pos.y()); redraw(); } @@ -484,13 +492,13 @@ void CurveView::first_chance_mouse_release(QMouseEvent *event) dragging_bezier_point_start_.y()); if (!(event->modifiers() & Qt::ControlModifier)) { - auto opposing_type = - NodeKeyframe::get_opposing_bezier_type(dragging_bezier_pt_->type); - const QPointF opposing_current = key->bezier_control(opposing_type); + int opposing_type = + oakengine_keyframe_opposing_bezier_type(dragging_bezier_pt_->type); + const QPointF opposing_current = key->bezier_control(static_cast(opposing_type)); oakengine_node_keyframe_set_bezier_point( handle, key->input().toUtf8().constData(), key->element(), ts, key->track(), - (opposing_type == NodeKeyframe::k_in_handle) ? 0 : 1, + opposing_type, opposing_current.x(), opposing_current.y(), dragging_bezier_point_opposing_start_.x(), dragging_bezier_point_opposing_start_.y()); @@ -516,7 +524,10 @@ void CurveView::keyframe_drag_move(QMouseEvent *event, QString &tip) // Lock to X axis only and set original values on all keys for (size_t i = 0; i < get_selected_keyframes().size(); i++) { NodeKeyframe *key = get_selected_keyframes().at(i); - key->set_value(drag_keyframe_values_.at(i)); + oak_node_value v; + track_value_to_c(key->parent()->get_input_data_type(key->input()), + drag_keyframe_values_.at(i), &v); + key_set_value_live(key, v); } return; } @@ -559,11 +570,16 @@ void CurveView::keyframe_drag_move(QMouseEvent *event, QString &tip) for (size_t i = 0; i < get_selected_keyframes().size(); i++) { NodeKeyframe *key = get_selected_keyframes().at(i); FloatSlider::DisplayType display = get_float_display_type_from_keyframe(key); - key->set_value(FloatSlider::transform_display_to_value( - FloatSlider::transform_value_to_display( - drag_keyframe_values_.at(i).toDouble(), display) - - scaled_diff, - display)); + oak_node_value v; + track_value_to_c( + key->parent()->get_input_data_type(key->input()), + FloatSlider::transform_display_to_value( + FloatSlider::transform_value_to_display( + drag_keyframe_values_.at(i).toDouble(), display) - + scaled_diff, + display), + &v); + key_set_value_live(key, v); } NodeKeyframe *tip_item = get_selected_keyframes().front(); @@ -580,7 +596,7 @@ void CurveView::keyframe_drag_move(QMouseEvent *event, QString &tip) } void CurveView::keyframe_drag_release(QMouseEvent *event, - MultiUndoCommand *command) + void *command) { Q_UNUSED(command) // the facade pushes its own single command below @@ -791,12 +807,18 @@ double CurveView::get_offset_from_keyframe(NodeKeyframe *key) if (node->has_input_property(input, QStringLiteral("offset"))) { QVariant v = node->get_input_property(input, QStringLiteral("offset")); - // NOTE: Implement getting correct offset for the track based on the data type - QVector track_vals = - NodeValue::split_normal_value_into_track_values( - node->get_input_data_type(input), v); - - return track_vals.at(key->track()).toDouble(); + const NodeValue::Type dt = node->get_input_data_type(input); + const int c_type = node_value_type_to_c(dt); + oak_node_value normal; + const int tc = oakengine_node_value_keyframe_track_count(c_type); + QVector track_vals(tc); + if (QVariantToOakNodeValue(dt, v, &normal) && + oakengine_node_value_split_to_tracks( + c_type, &normal, track_vals.data(), tc) == OAKENGINE_OK && + key->track() >= 0 && key->track() < tc) { + return track_vals.at(key->track()).f[0]; + } + return 0; } return 0; diff --git a/app/widget/curvewidget/curveview.h b/app/widget/curvewidget/curveview.h index 71e4f17bb..66d3660e4 100644 --- a/app/widget/curvewidget/curveview.h +++ b/app/widget/curvewidget/curveview.h @@ -78,7 +78,7 @@ protected: virtual void keyframe_drag_start(QMouseEvent *event) override; virtual void keyframe_drag_move(QMouseEvent *event, QString &tip) override; virtual void keyframe_drag_release(QMouseEvent *event, - MultiUndoCommand *command) override; + void *command) override; private: void zoom_to_fit_internal(bool selected_only); diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index fc35f94ea..3e3b03ce5 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -30,6 +30,7 @@ #include "core.h" #include "common/qtutils.h" #include "node/node.h" +#include "common/nodevaluehandle.h" #include "oakengine/node.h" #include "widget/timeruler/timeruler.h" @@ -275,7 +276,7 @@ void CurveWidget::connect_input_internal(Node *node, const QString &input, { NodeInput input_ref(node, input, element); int track_count = - NodeValue::get_number_of_keyframe_tracks(input_ref.get_data_type()); + oakengine_node_value_keyframe_track_count(node_value_type_to_c(input_ref.get_data_type())); for (int i = 0; i < track_count; i++) { NodeKeyframeTrackReference track_ref(input_ref, i); view_->connect_input(track_ref); diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index ca80b67b3..0eb306cf0 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -62,7 +62,10 @@ public: virtual bool paste() override; -public slots: +public: + // Not a slot: signature uses the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). All connections use new-style member-function syntax. void set_nodes(const QVector &nodes); protected: diff --git a/app/widget/filefield/lutfilefield.cpp b/app/widget/filefield/lutfilefield.cpp index c7923d0e7..dadc74e96 100644 --- a/app/widget/filefield/lutfilefield.cpp +++ b/app/widget/filefield/lutfilefield.cpp @@ -23,7 +23,7 @@ #include #include -#include "render/lutlibrary.h" +#include "oakengine/lut.h" namespace olive { @@ -68,8 +68,28 @@ void LutFileField::refresh_library_entries() library_combo_->clear(); library_combo_->addItem(tr("Other (Custom File)..."), QString()); - const QStringList library_dirs = LUTLibrary::get_directories(); - const QStringList luts = LUTLibrary::get_lut_files(); + QStringList library_dirs; + { + int dir_count = oakengine_lut_directory_count(); + for (int i = 0; i < dir_count; i++) { + char buf[4096]; + int len = oakengine_lut_directory_at(i, buf, sizeof(buf)); + if (len > 0) { + library_dirs.append(QString::fromUtf8(buf, len)); + } + } + } + QStringList luts; + { + int file_count = oakengine_lut_file_count(); + for (int i = 0; i < file_count; i++) { + char buf[4096]; + int len = oakengine_lut_file_at(i, buf, sizeof(buf)); + if (len > 0) { + luts.append(QString::fromUtf8(buf, len)); + } + } + } for (const QString &lut : luts) { // Show the path relative to the library directory that contains it QString display = lut; diff --git a/app/widget/handmovableview/handmovableview.cpp b/app/widget/handmovableview/handmovableview.cpp index afe2b39e1..70ec3518f 100644 --- a/app/widget/handmovableview/handmovableview.cpp +++ b/app/widget/handmovableview/handmovableview.cpp @@ -23,7 +23,7 @@ #include -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" namespace olive diff --git a/app/widget/history/historywidget.cpp b/app/widget/history/historywidget.cpp index c0ef4d567..53cb73309 100644 --- a/app/widget/history/historywidget.cpp +++ b/app/widget/history/historywidget.cpp @@ -22,6 +22,8 @@ #include "historywidget.h" #include "core.h" +#include "oakengine/events.h" +#include "oakengine/undo.h" namespace olive { @@ -33,12 +35,26 @@ HistoryWidget::HistoryWidget(QWidget *parent) this->setModel(stack_); this->setRootIsDecorated(false); - connect(stack_, &UndoStack::index_changed, this, - &HistoryWidget::index_changed); + undo_sub_ = oakengine_event_subscribe( + oakengine_undo_handle(), OAKENGINE_EVENT_UNDO_INDEX_CHANGED, + [](const oakengine_event *event, void *userdata) { + auto *self = static_cast(userdata); + self->index_changed(static_cast(event->a)); + }, + this); connect(this->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &HistoryWidget::current_row_changed); } +HistoryWidget::~HistoryWidget() +{ + // Raw subscription carries `this` as userdata; cancel it or the engine + // calls back into a dead widget (the undo stack outlives us). + if (undo_sub_ > 0) { + oakengine_event_unsubscribe(undo_sub_); + } +} + void HistoryWidget::index_changed(int i) { this->selectionModel()->select(this->model()->index(i - 1, 0), @@ -50,7 +66,7 @@ void HistoryWidget::current_row_changed(const QModelIndex ¤t, const QModelIndex &previous) { size_t jump_to = (current.row() + 1); - stack_->jump(jump_to); + oakengine_undo_jump(static_cast(jump_to)); } } diff --git a/app/widget/history/historywidget.h b/app/widget/history/historywidget.h index 7d4d0349c..f042be97f 100644 --- a/app/widget/history/historywidget.h +++ b/app/widget/history/historywidget.h @@ -24,6 +24,8 @@ #include +#include + #include "undo/undostack.h" namespace olive @@ -33,10 +35,13 @@ class HistoryWidget : public QTreeView { Q_OBJECT public: HistoryWidget(QWidget *parent = nullptr); + ~HistoryWidget() override; private: UndoStack *stack_; + int64_t undo_sub_ = 0; + size_t current_row_; private slots: diff --git a/app/widget/keyframeview/keyframehandle.h b/app/widget/keyframeview/keyframehandle.h new file mode 100644 index 000000000..7b49ef048 --- /dev/null +++ b/app/widget/keyframeview/keyframehandle.h @@ -0,0 +1,209 @@ +/*** + + 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_KEYFRAMEHANDLE_H +#define OAK_KEYFRAMEHANDLE_H + +#include + +#include +#include + +#include "oakengine/node.h" + +namespace olive +{ + +class Node; +class NodeKeyframe; + +using olive::core::Rational; + +/** + * @brief Facade accessors for keyframe pointers held by the keyframe + * views. + * + * The keyframe/curve views keep olive::NodeKeyframe* as opaque identity + * pointers (selection, drawing, hit-testing). All engine data and + * mutations go through the liboakengine C ABI (oakengine/node.h); the + * pointer itself is only a handle. Easing types use the facade order: + * 0 = linear, 1 = bezier, 2 = hold. + */ + +inline OakEngineKeyframe *keyhandle(NodeKeyframe *key) +{ + return reinterpret_cast(key); +} + +inline const OakEngineKeyframe *keyhandle(const NodeKeyframe *key) +{ + return reinterpret_cast(key); +} + +inline NodeKeyframe *keyhandle(OakEngineKeyframe *key) +{ + return reinterpret_cast(key); +} + +inline OakEngineNode *nodehandle(Node *node) +{ + return reinterpret_cast(node); +} + +inline const OakEngineNode *nodehandle(const Node *node) +{ + return reinterpret_cast(node); +} + +inline Node *key_node(const NodeKeyframe *key) +{ + return reinterpret_cast( + oakengine_keyframe_get_node(keyhandle(key))); +} + +inline Rational key_time(const NodeKeyframe *key) +{ + int64_t num = 0, den = 1; + oakengine_keyframe_get_time(keyhandle(key), &num, &den); + return Rational(int(num), int(den)); +} + +inline int key_easing(const NodeKeyframe *key) +{ + return oakengine_keyframe_get_type(keyhandle(key)); +} + +inline oak_node_value key_value(const NodeKeyframe *key) +{ + oak_node_value v; + memset(&v, 0, sizeof(v)); + oakengine_keyframe_get_value(keyhandle(key), &v); + return v; +} + +inline double key_value_as_double(const NodeKeyframe *key) +{ + const oak_node_value v = key_value(key); + switch (v.type) { + case OAK_NODE_VALUE_INT: + case OAK_NODE_VALUE_COMBO: + case OAK_NODE_VALUE_BOOL: + return double(v.num); + case OAK_NODE_VALUE_RATIONAL: + return v.den ? double(v.num) / double(v.den) : 0.0; + default: + return v.f[0]; + } +} + +inline void key_set_value_live(NodeKeyframe *key, const oak_node_value &v) +{ + oakengine_keyframe_set_value_live(keyhandle(key), &v); +} + +inline QPointF key_bezier_point(const NodeKeyframe *key, int point_index) +{ + double x = 0, y = 0; + oakengine_keyframe_get_bezier_point(keyhandle(key), point_index, &x, &y); + return QPointF(x, y); +} + +inline QPointF key_valid_bezier_point(const NodeKeyframe *key, + int point_index) +{ + double x = 0, y = 0; + oakengine_keyframe_get_valid_bezier_point(keyhandle(key), point_index, + &x, &y); + return QPointF(x, y); +} + +inline void key_set_bezier_point_live(NodeKeyframe *key, int point_index, + const QPointF &point) +{ + oakengine_keyframe_set_bezier_point_live(keyhandle(key), point_index, + point.x(), point.y()); +} + +inline void key_set_time_live(NodeKeyframe *key, const Rational &time) +{ + oakengine_keyframe_set_time_live(keyhandle(key), time.numerator(), + time.denominator()); +} + +inline bool key_has_sibling_at_time(const NodeKeyframe *key, + const Rational &time) +{ + return oakengine_keyframe_has_sibling_at_time( + keyhandle(key), time.numerator(), time.denominator()) != 0; +} + +inline QString key_input_id(const NodeKeyframe *key) +{ + const int size = + oakengine_keyframe_get_input_id(keyhandle(key), nullptr, 0); + QByteArray buf(size + 1, '\0'); + oakengine_keyframe_get_input_id(keyhandle(key), buf.data(), + int(buf.size())); + return QString::fromUtf8(buf.constData()); +} + +inline int key_track(const NodeKeyframe *key) +{ + return oakengine_keyframe_get_track(keyhandle(key)); +} + +inline int key_element(const NodeKeyframe *key) +{ + return oakengine_keyframe_get_element(keyhandle(key)); +} + +/** + * @brief ADL customization points for + * TimeBasedViewSelectionManager. + * + * The selection manager template calls these unqualified; the generic + * member-forwarding templates in timebasedviewselectionmanager.h cover + * other object types (e.g. TimelineMarker), while these overloads route + * keyframe access through the facade. + */ +inline Rational selection_time(NodeKeyframe *key) +{ + return key_time(key); +} + +inline void selection_set_time(NodeKeyframe *key, const Rational &time) +{ + key_set_time_live(key, time); +} + +inline bool selection_has_sibling_at_time(NodeKeyframe *key, + const Rational &time) +{ + return key_has_sibling_at_time(key, time); +} + +inline Node *selection_time_target_parent(NodeKeyframe *key) +{ + return key_node(key); +} + +} // namespace olive + +#endif // OAK_KEYFRAMEHANDLE_H diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp index 0b959d809..3660d1f41 100644 --- a/app/widget/keyframeview/keyframeview.cpp +++ b/app/widget/keyframeview/keyframeview.cpp @@ -25,13 +25,16 @@ #include #include +#include "common/nodevaluehandle.h" +#include "common/oakvaluehelper.h" #include "common/qtutils.h" #include "dialog/keyframeproperties/keyframeproperties.h" -#include "node/group/group.h" +#include "keyframehandle.h" #include "node/node.h" -#include "node/nodeundo.h" -#include "node/project/serializer/serializer.h" +#include "node/value.h" #include "oakengine/node.h" +#include "oakengine/serializer.h" +#include "oakengine/undo.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" @@ -40,6 +43,26 @@ namespace olive #define super TimeBasedView +static bool KeyframeToOakNodeValue(Node *node, NodeKeyframe *key, + oak_node_value *out) +{ + const NodeValue::Type type = node->get_input_data_type(key->input()); + QVector split = node->get_split_value_at_time( + NodeInput(node, key->input(), key->element()), key->time()); + if (key->track() >= 0 && key->track() < split.size()) { + split[key->track()] = key->value(); + } + QVector tracks(split.size()); + for (int i = 0; i < split.size(); i++) { + if (!NodeTrackComponentToOakNodeValue(type, split.at(i), &tracks[i])) { + return false; + } + } + return oakengine_node_value_combine_tracks( + node_value_type_to_c(type), tracks.constData(), tracks.size(), + out) == OAKENGINE_OK; +} + KeyframeView::KeyframeView(QWidget *parent) : super(parent) , selection_manager_(this) @@ -58,15 +81,16 @@ KeyframeView::KeyframeView(QWidget *parent) void KeyframeView::delete_selected() { if (!selection_manager_.is_dragging()) { - MultiUndoCommand *command = new MultiUndoCommand(); - + QVector keys; foreach (NodeKeyframe *key, get_selected_keyframes()) { - command->add_child(new NodeParamRemoveKeyframeCommand(key)); + keys.append(reinterpret_cast(key)); } - - Core::instance()->undo_stack()->push( - command, - tr("Deleted %1 Keyframe(s)").arg(get_selected_keyframes().size())); + oakengine_keyframes_remove_many( + keys.data(), keys.size(), + tr("Deleted %1 Keyframe(s)") + .arg(keys.size()) + .toUtf8() + .constData()); } } @@ -86,7 +110,15 @@ KeyframeView::add_keyframes_of_input(Node *on, const QString &oinput) { InputConnections vec; - NodeInput resolved = NodeGroup::resolve_input(NodeInput(on, oinput)); + OakEngineNode *resolved_node = nullptr; + char resolved_input[256]; + int resolved_element = 0; + oakengine_group_resolve_input( + reinterpret_cast(on), oinput.toUtf8().constData(), -1, + &resolved_node, resolved_input, sizeof(resolved_input), + &resolved_element); + NodeInput resolved(reinterpret_cast(resolved_node), + QString::fromUtf8(resolved_input), resolved_element); Node *n = resolved.node(); const QString &input = resolved.input(); @@ -202,11 +234,17 @@ void KeyframeView::SelectionManagerDeselectEvent(void *obj) bool KeyframeView::copy_selected(bool cut) { if (!selection_manager_.get_selected_objects().empty()) { - ProjectSerializer::SaveData sdata(ProjectSerializer::k_only_keyframes); - sdata.set_only_serialize_keyframes( - selection_manager_.get_selected_objects()); - - ProjectSerializer::copy(sdata); + const auto &keys = selection_manager_.get_selected_objects(); + OakEngineClipboard *cb = + oakengine_clipboard_create(OAKENGINE_CLIPBOARD_KEYFRAMES, + nullptr, nullptr); + oakengine_clipboard_set_keyframes( + cb, + reinterpret_cast( + keys.data()), + static_cast(keys.size())); + oakengine_clipboard_copy(cb); + oakengine_clipboard_free(cb); if (cut) { delete_selected(); @@ -225,57 +263,108 @@ bool KeyframeView::paste( return false; } - ProjectSerializer::Result res = - ProjectSerializer::paste(ProjectSerializer::k_only_keyframes); - if (res == ProjectSerializer::k_success) { - const ProjectSerializer::SerializedKeyframes &keys = - res.get_load_data().keyframes; + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_KEYFRAMES, nullptr, nullptr); + int result_code = OAKENGINE_SERIALIZER_NO_DATA; + oakengine_clipboard_paste(cb, OAKENGINE_CLIPBOARD_KEYFRAMES, nullptr, + &result_code, nullptr, 0); - MultiUndoCommand *command = new MultiUndoCommand(); + if (result_code == OAKENGINE_SERIALIZER_OK) { + struct PasteCtx { + KeyframeView *self; + void *find_fn; + void *command; + Rational min; + int total; + }; - Rational min = RATIONAL_MAX; - for (auto it = keys.cbegin(); it != keys.cend(); it++) { - for (NodeKeyframe *key : it.value()) { - min = std::min(min, key->time()); - } - } - min -= get_viewer_node()->get_playhead(); + PasteCtx ctx; + ctx.self = this; + ctx.find_fn = &find_node_function; + ctx.command = oakengine_undo_command_create_multi(); + ctx.min = RATIONAL_MAX; + ctx.total = 0; - for (auto it = keys.cbegin(); it != keys.cend(); it++) { - const QString &paste_id = it.key(); + // First pass: find minimum time + oakengine_clipboard_foreach_keyframe( + cb, + [](const char *, OakEngineKeyframe *kf, void *userdata) -> int { + auto *ctx = static_cast(userdata); + NodeKeyframe *key = reinterpret_cast(kf); + ctx->min = std::min(ctx->min, key->time()); + ctx->total++; + return 0; + }, + &ctx); - // Find a node with this ID - Node *node_with_id = find_node_function(paste_id); + ctx.min -= ctx.self->get_viewer_node()->get_playhead(); - if (node_with_id) { - for (NodeKeyframe *key : it.value()) { - // Adjust sequence time to node's time - Rational t = key->time() - min; - t = get_adjusted_time(get_time_target(), node_with_id, t, - Node::k_transform_towards_input); - key->set_time(t); + // Second pass: process keyframes + oakengine_clipboard_foreach_keyframe( + cb, + [](const char *node_id, OakEngineKeyframe *kf, + void *userdata) -> int { + auto *ctx = static_cast(userdata); + NodeKeyframe *key = reinterpret_cast(kf); + auto &find_fn = + *static_cast *>( + ctx->find_fn); + Node *node_with_id = + find_fn(QString::fromUtf8(node_id)); + + if (node_with_id) { + Rational t = key->time() - ctx->min; + t = ctx->self->get_adjusted_time( + ctx->self->get_time_target(), node_with_id, t, + Node::k_transform_towards_input); + key_set_time_live(key, t); if (NodeKeyframe *existing = node_with_id->get_keyframe_at_time_on_track( key->input(), key->time(), key->track(), key->element())) { - command->add_child( - new NodeParamRemoveKeyframeCommand(existing)); + void *rm = oakengine_node_remove_keyframe_command( + reinterpret_cast(existing)); + oakengine_undo_command_multi_add_child( + ctx->command, rm); } - command->add_child( - new NodeParamInsertKeyframeCommand(node_with_id, key)); + oak_node_value v; + KeyframeToOakNodeValue(node_with_id, key, &v); + int tbn = 0, tbd = 0; + oakengine_node_frame_time_base( + reinterpret_cast(node_with_id), + &tbn, &tbd); + const int64_t time_ts = Timecode::time_to_timestamp( + key->time(), Rational(tbn, tbd), Timecode::k_round); + void *cmd = oakengine_node_insert_keyframe_command( + reinterpret_cast(node_with_id), + key->input().toUtf8().constData(), + key->element(), key->track(), time_ts, &v, + NodeKeyframeTypeToFacade(key->type()), + static_cast(key->bezier_control_in().x()), + static_cast(key->bezier_control_in().y()), + static_cast(key->bezier_control_out().x()), + static_cast(key->bezier_control_out().y())); + oakengine_undo_command_multi_add_child(ctx->command, cmd); + } else { + delete key; } - } else { - qDeleteAll(it.value()); - } - } - Core::instance()->undo_stack()->push( - command, tr("Pasted %1 Keyframe(s)").arg(keys.size())); + return 0; + }, + &ctx); + + oakengine_undo_push(ctx.command, + tr("Pasted %1 Keyframe(s)") + .arg(ctx.total) + .toUtf8() + .constData()); + oakengine_clipboard_free(cb); return true; } + oakengine_clipboard_free(cb); return false; } @@ -343,12 +432,12 @@ void KeyframeView::mouseReleaseEvent(QMouseEvent *event) first_chance_mouse_release(event); first_chance_mouse_event_ = false; } else if (selection_manager_.is_dragging()) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); selection_manager_.drag_stop(command); keyframe_drag_release(event, command); - Core::instance()->undo_stack()->push( + oakengine_undo_push( command, tr("Moved %1 Keyframe(s)") - .arg(selection_manager_.get_selected_objects().size())); + .arg(selection_manager_.get_selected_objects().size()).toUtf8().constData()); } else if (selection_manager_.is_rubber_banding()) { selection_manager_.rubber_band_stop(); redraw(); diff --git a/app/widget/keyframeview/keyframeview.h b/app/widget/keyframeview/keyframeview.h index 5208dd6f3..7a91f532d 100644 --- a/app/widget/keyframeview/keyframeview.h +++ b/app/widget/keyframeview/keyframeview.h @@ -131,7 +131,7 @@ protected: { } virtual void keyframe_drag_release(QMouseEvent *event, - MultiUndoCommand *command) + void *command) { } diff --git a/app/widget/keyframeview/keyframeviewinputconnection.cpp b/app/widget/keyframeview/keyframeviewinputconnection.cpp index 12a9c6c15..484e2991a 100644 --- a/app/widget/keyframeview/keyframeviewinputconnection.cpp +++ b/app/widget/keyframeview/keyframeviewinputconnection.cpp @@ -34,21 +34,47 @@ KeyframeViewInputConnection::KeyframeViewInputConnection( , y_(0) , y_behavior_(k_single_row) , brush_(Qt::white) + , bridge_(new EngineEventBridge(this)) { Node *n = input.input().node(); - connect(n, &Node::keyframe_added, this, - &KeyframeViewInputConnection::add_keyframe); - connect(n, &Node::keyframe_removed, this, - &KeyframeViewInputConnection::remove_keyframe); - connect(n, &Node::keyframe_time_changed, this, - &KeyframeViewInputConnection::keyframe_changed); - connect(n, &Node::keyframe_type_changed, this, - &KeyframeViewInputConnection::keyframe_changed); - connect(n, &Node::keyframe_type_changed, this, - &KeyframeViewInputConnection::keyframe_type_changed); - connect(n, &Node::keyframe_value_changed, this, - &KeyframeViewInputConnection::keyframe_changed); + bridge_->subscribe(reinterpret_cast(n), + OAKENGINE_EVENT_NODE_KEYFRAME_ADDED); + bridge_->subscribe(reinterpret_cast(n), + OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED); + bridge_->subscribe(reinterpret_cast(n), + OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED); + bridge_->subscribe(reinterpret_cast(n), + OAKENGINE_EVENT_NODE_KEYFRAME_TYPE_CHANGED); + bridge_->subscribe(reinterpret_cast(n), + OAKENGINE_EVENT_NODE_KEYFRAME_VALUE_CHANGED); + + connect(bridge_, &EngineEventBridge::node_keyframe_added, this, + [this](OakEngineNode *, OakEngineKeyframe *key, + const QString &, int, int) { + add_keyframe(key); + }); + connect(bridge_, &EngineEventBridge::node_keyframe_removed, this, + [this](OakEngineNode *, OakEngineKeyframe *key, + const QString &, int, int) { + remove_keyframe(key); + }); + connect(bridge_, &EngineEventBridge::node_keyframe_time_changed, this, + [this](OakEngineNode *, OakEngineKeyframe *key) { + keyframe_changed(key); + }); + connect(bridge_, &EngineEventBridge::node_keyframe_type_changed, this, + [this](OakEngineNode *, OakEngineKeyframe *key) { + keyframe_changed(key); + }); + connect(bridge_, &EngineEventBridge::node_keyframe_type_changed, this, + [this](OakEngineNode *, OakEngineKeyframe *key) { + keyframe_type_changed(key); + }); + connect(bridge_, &EngineEventBridge::node_keyframe_value_changed, this, + [this](OakEngineNode *, OakEngineKeyframe *key) { + keyframe_changed(key); + }); } void KeyframeViewInputConnection::set_keyframe_y(int y) @@ -78,30 +104,34 @@ void KeyframeViewInputConnection::set_brush(const QBrush &brush) } } -void KeyframeViewInputConnection::add_keyframe(NodeKeyframe *key) +void KeyframeViewInputConnection::add_keyframe(OakEngineKeyframe *key) { - if (key->key_track_ref() == input_) { + NodeKeyframe *nk = reinterpret_cast(key); + if (nk->key_track_ref() == input_) { emit require_update(); } } -void KeyframeViewInputConnection::remove_keyframe(NodeKeyframe *key) +void KeyframeViewInputConnection::remove_keyframe(OakEngineKeyframe *key) { - if (key->key_track_ref() == input_) { + NodeKeyframe *nk = reinterpret_cast(key); + if (nk->key_track_ref() == input_) { emit require_update(); } } -void KeyframeViewInputConnection::keyframe_changed(NodeKeyframe *key) +void KeyframeViewInputConnection::keyframe_changed(OakEngineKeyframe *key) { - if (key->key_track_ref() == input_) { + NodeKeyframe *nk = reinterpret_cast(key); + if (nk->key_track_ref() == input_) { emit require_update(); } } -void KeyframeViewInputConnection::keyframe_type_changed(NodeKeyframe *key) +void KeyframeViewInputConnection::keyframe_type_changed(OakEngineKeyframe *key) { - if (key->key_track_ref() == input_) { + NodeKeyframe *nk = reinterpret_cast(key); + if (nk->key_track_ref() == input_) { emit type_changed(); } } diff --git a/app/widget/keyframeview/keyframeviewinputconnection.h b/app/widget/keyframeview/keyframeviewinputconnection.h index 9431f2ac0..24e6e2895 100644 --- a/app/widget/keyframeview/keyframeviewinputconnection.h +++ b/app/widget/keyframeview/keyframeviewinputconnection.h @@ -24,9 +24,12 @@ #include +#include "engineeventbridge.h" #include "node/node.h" #include "node/param.h" +struct OakEngineKeyframe; + namespace olive { @@ -85,14 +88,16 @@ private: QBrush brush_; + EngineEventBridge *bridge_ = nullptr; + private slots: - void add_keyframe(NodeKeyframe *key); + void add_keyframe(OakEngineKeyframe *key); - void remove_keyframe(NodeKeyframe *key); + void remove_keyframe(OakEngineKeyframe *key); - void keyframe_changed(NodeKeyframe *key); + void keyframe_changed(OakEngineKeyframe *key); - void keyframe_type_changed(NodeKeyframe *key); + void keyframe_type_changed(OakEngineKeyframe *key); }; } diff --git a/app/widget/manageddisplay/colorprocessorhandle.h b/app/widget/manageddisplay/colorprocessorhandle.h new file mode 100644 index 000000000..e950d76c8 --- /dev/null +++ b/app/widget/manageddisplay/colorprocessorhandle.h @@ -0,0 +1,246 @@ +/*** + + 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_COLORPROCESSORHANDLE_H +#define OAK_COLORPROCESSORHANDLE_H + +#include + +#include +#include + +#include +#include + +#include "render/colortransform.h" + +namespace olive +{ + +class ColorManager; + +/** + * @brief App-side replacement for the engine's former ManagedColor class. + * + * The engine-side class was removed during the C ABI migration; this + * header-only equivalent lives entirely in application code (every method + * is inline), so no olive::ManagedColor symbol is imported from the engine + * shared library. It pairs an RGBA color with the color-space input id and + * the output transform, exactly like the original. + */ +class ManagedColor : public Color { +public: + ManagedColor() = default; + ManagedColor(const double &r, const double &g, const double &b, + const double &a = 1.0) + : Color(r, g, b, a) + { + } + ManagedColor(const char *data, const core::PixelFormat &format, + int channel_layout) + : Color(data, format, channel_layout) + { + } + ManagedColor(const Color &c) + : Color(c) + { + } + + const QString &color_input() const + { + return color_input_; + } + void set_color_input(const QString &color_input) + { + color_input_ = color_input; + } + + const ColorTransform &color_output() const + { + return color_transform_; + } + void set_color_output(const ColorTransform &color_output) + { + color_transform_ = color_output; + } + +private: + QString color_input_; + + ColorTransform color_transform_; +}; + +/** + * @brief Opaque-handle replacement for ColorProcessorPtr + * + * The color processor object lives behind the engine's C ABI + * (oakengine/color.h); application code only ever holds this shared + * handle, so no olive::ColorProcessor symbol is imported from the engine + * library. + */ +struct ColorProcessorHandleDeleter { + void operator()(OakEngineColorProcessor *p) const + { + oakengine_color_processor_free(p); + } +}; + +using ColorProcessorHandlePtr = std::shared_ptr; + +/** + * @brief Reinterpret an engine ColorManager pointer as its borrowed facade + * handle (the same reinterpret pattern the other facade families use). + */ +inline OakEngineColorManager *oak_color_manager(ColorManager *mgr) +{ + return reinterpret_cast(mgr); +} + +inline const OakEngineColorManager *oak_color_manager(const ColorManager *mgr) +{ + return reinterpret_cast(mgr); +} + +/** + * @brief Run a buf/size facade string getter and return the QString. + */ +template QString oak_query_string(Fn &&fn) +{ + const int len = fn(nullptr, 0); + if (len <= 0) { + return QString(); + } + QByteArray buf(len + 1, 0); + fn(buf.data(), buf.size()); + return QString::fromUtf8(buf.constData(), len); +} + +/** + * @brief Run a count/at facade list getter pair and return the QStringList. + */ +template +QStringList oak_query_string_list(CountFn &&count_fn, AtFn &&at_fn) +{ + QStringList list; + const int count = count_fn(); + list.reserve(count); + for (int i = 0; i < count; i++) { + list.append(oak_query_string( + [&](char *buf, int size) { return at_fn(i, buf, size); })); + } + return list; +} + +/** + * @brief Convert an olive::ColorTransform to the facade POD. The QByteArray + * outputs back the POD's pointers and must outlive its use. + */ +inline oak_color_transform oak_to_transform(const ColorTransform &t, + QByteArray *output, + QByteArray *view, + QByteArray *look) +{ + *output = t.output().toUtf8(); + *view = t.view().toUtf8(); + *look = t.look().toUtf8(); + oak_color_transform pod; + pod.is_display = t.is_display() ? 1 : 0; + pod.output = output->constData(); + pod.view = view->constData(); + pod.look = look->constData(); + return pod; +} + +/** + * @brief ColorManager::get_compliant_color_space(ColorTransform) through + * the facade. + */ +inline ColorTransform oak_compliant_transform(ColorManager *mgr, + const ColorTransform &in, + bool force_display = false) +{ + QByteArray o, v, l; + const oak_color_transform pod = oak_to_transform(in, &o, &v, &l); + int is_display = 0; + char out[256], view[256], look[256]; + if (oakengine_color_manager_compliant_transform( + oak_color_manager(mgr), &pod, force_display ? 1 : 0, &is_display, + out, sizeof(out), view, sizeof(view), look, sizeof(look)) != + OAKENGINE_OK) { + return in; + } + if (is_display) { + return ColorTransform(QString::fromUtf8(out), QString::fromUtf8(view), + QString::fromUtf8(look)); + } + return ColorTransform(QString::fromUtf8(out)); +} + +/** + * @brief ColorManager::get_compliant_color_space(QString) through the + * facade. + */ +inline QString oak_compliant_color_space(ColorManager *mgr, const QString &s) +{ + const QByteArray name = s.toUtf8(); + return oak_query_string([&](char *buf, int size) { + return oakengine_color_manager_compliant_color_space( + oak_color_manager(mgr), name.constData(), buf, size); + }); +} + +/** + * @brief ColorProcessor::create() through the facade (never throws; an + * OCIO failure yields a handle for which IsValid() is false). + */ +inline ColorProcessorHandlePtr +oak_make_color_processor(ColorManager *mgr, const QString &input, + const ColorTransform &dest, + int direction = OAKENGINE_COLOR_PROCESSOR_NORMAL) +{ + QByteArray o, v, l; + const oak_color_transform pod = oak_to_transform(dest, &o, &v, &l); + const QByteArray in = input.toUtf8(); + return ColorProcessorHandlePtr( + oakengine_color_processor_create(oak_color_manager(mgr), + in.constData(), &pod, direction), + ColorProcessorHandleDeleter()); +} + +/** + * @brief ColorProcessor::convert_color() through the facade. A null or + * invalid processor passes the color through unchanged (matching the + * engine's behavior). + */ +inline Color oak_convert_color(const ColorProcessorHandlePtr &proc, + const Color &in) +{ + double rgba[4] = { in.red(), in.green(), in.blue(), in.alpha() }; + double out[4]; + if (oakengine_color_processor_convert_color(proc.get(), rgba, out) == + OAKENGINE_OK) { + return Color(out[0], out[1], out[2], out[3]); + } + return in; +} + +} // namespace olive + +#endif // OAK_COLORPROCESSORHANDLE_H diff --git a/app/widget/manageddisplay/manageddisplay.cpp b/app/widget/manageddisplay/manageddisplay.cpp index 8b43a3e81..69dbf6506 100644 --- a/app/widget/manageddisplay/manageddisplay.cpp +++ b/app/widget/manageddisplay/manageddisplay.cpp @@ -25,11 +25,11 @@ #include #include "panel/panelmanager.h" -#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND -#include "render/backend/dynamicrenderer.h" -#endif -#include "render/opengl/openglrenderer.h" -#include "render/rendermanager.h" +#include "oakengine/videoparams.h" +#include "oakengine/renderer.h" +#include "oakengine/display.h" +#include "widget/viewer/vieweroutpututils.h" +#include "common/configwrapper.h" namespace olive { @@ -49,21 +49,24 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent) // Create renderer #ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND { - auto *dynamic_renderer = new DynamicRenderer( - RenderManager::backend_to_string( - RenderManager::instance()->requested_backend()), - this); - if (!dynamic_renderer->load()) { + const QString backend = oak_query_string([](char *buf, int sz) { + int backend = oakengine_render_manager_requested_backend(); + return oakengine_render_manager_backend_to_string(backend, buf, + sz); + }); + attached_renderer_ = static_cast( + oakengine_display_renderer_create_dynamic( + backend.toUtf8().constData(), this)); + if (!attached_renderer_) { qWarning() << "Failed to load dynamic render backend for viewer, falling back to OpenGL"; - delete dynamic_renderer; - attached_renderer_ = new OpenGLRenderer(this); - } else { - attached_renderer_ = dynamic_renderer; + attached_renderer_ = static_cast( + oakengine_display_renderer_create_opengl(this)); } } #else - attached_renderer_ = new OpenGLRenderer(this); + attached_renderer_ = static_cast( + oakengine_display_renderer_create_opengl(this)); #endif if (attached_renderer_->is_open_gl()) { @@ -120,33 +123,43 @@ ManagedDisplayWidget::~ManagedDisplayWidget() } } -void ManagedDisplayWidget::connect_color_manager(ColorManager *color_manager) +void ManagedDisplayWidget::connect_color_manager(OakEngineColorManager *color_manager) { if (color_manager_ == color_manager) { return; } if (color_manager_ != nullptr) { - disconnect(color_manager_, &ColorManager::config_changed, this, - &ManagedDisplayWidget::color_config_changed); - disconnect(color_manager_, &ColorManager::reference_space_changed, this, - &ManagedDisplayWidget::color_config_changed); + for (int64_t id : color_subs_) { + oakengine_event_unsubscribe(id); + } + color_subs_.clear(); } color_manager_ = color_manager; if (color_manager_ != nullptr) { - connect(color_manager_, &ColorManager::config_changed, this, - &ManagedDisplayWidget::color_config_changed); - connect(color_manager_, &ColorManager::reference_space_changed, this, - &ManagedDisplayWidget::color_config_changed); + auto sub = [this](int ev) { + int64_t id = oakengine_event_subscribe( + color_manager_, ev, + [](const oakengine_event *, void *userdata) { + static_cast(userdata) + ->color_config_changed(); + }, + this); + if (id > 0) { + color_subs_.append(id); + } + }; + sub(OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED); + sub(OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED); } color_config_changed(); emit color_manager_changed(color_manager_); } -ColorManager *ManagedDisplayWidget::color_manager() const +OakEngineColorManager *ManagedDisplayWidget::color_manager() const { return color_manager_; } @@ -163,7 +176,14 @@ const ColorTransform &ManagedDisplayWidget::get_color_transform() const Menu *ManagedDisplayWidget::get_color_space_menu(QMenu *parent, bool auto_connect) { - QStringList colorspaces = color_manager()->list_available_colorspaces(); + QStringList colorspaces = oak_query_string_list( + [this]() { + return oakengine_color_manager_colorspace_count(color_manager_); + }, + [this](int i, char *buf, int size) { + return oakengine_color_manager_colorspace_at(color_manager_, i, buf, + size); + }); Menu *ocio_colorspace_menu = new Menu(tr("Color Space"), parent); @@ -195,17 +215,27 @@ void ManagedDisplayWidget::color_config_changed() // which is usually a scene-referred space (e.g. ACEScg / Linear) and makes // the picture look raw/wrong on a monitor. if (color_transform_.output().isEmpty()) { - QString display = color_manager_->get_default_display(); - QString view = color_manager_->get_default_view(display); - set_color_transform(color_manager_->get_compliant_color_space( + QString display = oak_query_string([this](char *buf, int size) { + return oakengine_color_manager_default_display(color_manager_, buf, + size); + }); + QString view = oak_query_string([this, &display](char *buf, int size) { + QByteArray d = display.toUtf8(); + return oakengine_color_manager_default_view(color_manager_, + d.constData(), buf, + size); + }); + set_color_transform(oak_compliant_transform( + reinterpret_cast(color_manager_), ColorTransform(display, view, QString()), true)); } else { - set_color_transform( - color_manager_->get_compliant_color_space(color_transform_, false)); + set_color_transform(oak_compliant_transform( + reinterpret_cast(color_manager_), + color_transform_, false)); } } -ColorProcessorPtr ManagedDisplayWidget::color_service() +ColorProcessorHandlePtr ManagedDisplayWidget::color_service() { return color_service_; } @@ -232,7 +262,8 @@ void ManagedDisplayWidget::menu_display_select(QAction *action) { const ColorTransform &old_transform = get_color_transform(); - ColorTransform new_transform = color_manager()->get_compliant_color_space( + ColorTransform new_transform = oak_compliant_transform( + reinterpret_cast(color_manager_), ColorTransform(action->data().toString(), old_transform.view(), old_transform.look())); @@ -243,7 +274,8 @@ void ManagedDisplayWidget::menu_view_select(QAction *action) { const ColorTransform &old_transform = get_color_transform(); - ColorTransform new_transform = color_manager()->get_compliant_color_space( + ColorTransform new_transform = oak_compliant_transform( + reinterpret_cast(color_manager_), ColorTransform(old_transform.display(), action->data().toString(), old_transform.look())); @@ -254,7 +286,8 @@ void ManagedDisplayWidget::menu_look_select(QAction *action) { const ColorTransform &old_transform = get_color_transform(); - ColorTransform new_transform = color_manager()->get_compliant_color_space( + ColorTransform new_transform = oak_compliant_transform( + reinterpret_cast(color_manager_), ColorTransform(old_transform.display(), old_transform.view(), action->data().toString())); @@ -263,14 +296,14 @@ void ManagedDisplayWidget::menu_look_select(QAction *action) void ManagedDisplayWidget::menu_colorspace_select(QAction *action) { - set_color_transform(color_manager()->get_compliant_color_space( + set_color_transform(oak_compliant_transform( + reinterpret_cast(color_manager_), ColorTransform(action->data().toString()))); } void ManagedDisplayWidget::on_destroy() { - attached_renderer_->destroy(); - attached_renderer_->post_destroy(); + oakengine_display_renderer_destroy(attached_renderer_); } void ManagedDisplayWidget::set_color_transform(const ColorTransform &transform) @@ -279,7 +312,7 @@ void ManagedDisplayWidget::set_color_transform(const ColorTransform &transform) setup_color_processor(); - ColorProcessorChangedEvent(); + color_processor_changed_event(); } void ManagedDisplayWidget::on_init() @@ -287,19 +320,9 @@ void ManagedDisplayWidget::on_init() if (!is_backend_neutral_) { QOpenGLContext *context = static_cast(inner_widget_)->context(); -#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND - if (auto *dynamic_renderer = - dynamic_cast(attached_renderer_)) { - dynamic_renderer->init_with_open_gl_context(context); - dynamic_renderer->post_init(); - return; - } -#endif - static_cast(attached_renderer_)->init(context); - static_cast(attached_renderer_)->post_init(); + oakengine_display_renderer_init(attached_renderer_, context); } else { - attached_renderer_->init(); - attached_renderer_->post_init(); + oakengine_display_renderer_init(attached_renderer_, nullptr); } } @@ -309,7 +332,7 @@ void ManagedDisplayWidget::enable_default_context_menu() &ManagedDisplayWidget::show_default_context_menu); } -void ManagedDisplayWidget::ColorProcessorChangedEvent() +void ManagedDisplayWidget::color_processor_changed_event() { update(); } @@ -346,8 +369,11 @@ VideoParams ManagedDisplayWidget::get_viewport_params() const int device_height = height() * devicePixelRatioF(); PixelFormat device_format = static_cast( OAK_CONFIG("OfflinePixelFormat").toInt()); - return VideoParams(device_width, device_height, device_format, - VideoParams::k_internal_channel_count); + oak_video_params pod = {}; + pod.width = device_width; + pod.height = device_height; + pod.format = device_format; + return video_params_from_pod(pod); } void ManagedDisplayWidget::update() @@ -393,7 +419,14 @@ bool ManagedDisplayWidget::eventFilter(QObject *o, QEvent *e) Menu *ManagedDisplayWidget::get_display_menu(QMenu *parent, bool auto_connect) { - QStringList displays = color_manager()->list_available_displays(); + QStringList displays = oak_query_string_list( + [this]() { + return oakengine_color_manager_display_count(color_manager_); + }, + [this](int i, char *buf, int size) { + return oakengine_color_manager_display_at(color_manager_, i, buf, + size); + }); Menu *ocio_display_menu = new Menu(tr("Display"), parent); @@ -414,8 +447,17 @@ Menu *ManagedDisplayWidget::get_display_menu(QMenu *parent, bool auto_connect) Menu *ManagedDisplayWidget::get_view_menu(QMenu *parent, bool auto_connect) { - QStringList views = - color_manager()->list_available_views(color_transform_.display()); + QByteArray disp = color_transform_.display().toUtf8(); + QStringList views = oak_query_string_list( + [this, &disp]() { + return oakengine_color_manager_view_count(color_manager_, + disp.constData()); + }, + [this, &disp](int i, char *buf, int size) { + return oakengine_color_manager_view_at(color_manager_, + disp.constData(), i, buf, + size); + }); Menu *ocio_view_menu = new Menu(tr("View"), parent); @@ -436,7 +478,14 @@ Menu *ManagedDisplayWidget::get_view_menu(QMenu *parent, bool auto_connect) Menu *ManagedDisplayWidget::get_look_menu(QMenu *parent, bool auto_connect) { - QStringList looks = color_manager()->list_available_looks(); + QStringList looks = oak_query_string_list( + [this]() { + return oakengine_color_manager_look_count(color_manager_); + }, + [this](int i, char *buf, int size) { + return oakengine_color_manager_look_at(color_manager_, i, buf, + size); + }); Menu *ocio_look_menu = new Menu(tr("Look"), parent); @@ -467,17 +516,15 @@ void ManagedDisplayWidget::setup_color_processor() color_service_ = nullptr; if (color_manager_) { - // (Re)create color processor - try { - color_service_ = ColorProcessor::create( - color_manager_, color_manager_->get_reference_color_space(), - color_transform_); - } catch (ocio::Exception &e) { - QMessageBox::critical( - this, tr("OpenColorIO Error"), - tr("Failed to set color configuration: %1").arg(e.what()), - QMessageBox::Ok); - } + // (Re)create color processor. The facade never throws: OCIO failures + // are caught inside the engine and surface as an invalid processor. + QString ref_cs = oak_query_string([this](char *buf, int size) { + return oakengine_color_manager_reference_color_space( + color_manager_, buf, size); + }); + color_service_ = oak_make_color_processor( + reinterpret_cast(color_manager_), + ref_cs, color_transform_); } else { color_service_ = nullptr; } diff --git a/app/widget/manageddisplay/manageddisplay.h b/app/widget/manageddisplay/manageddisplay.h index 1c6c8186a..9b9f5ec48 100644 --- a/app/widget/manageddisplay/manageddisplay.h +++ b/app/widget/manageddisplay/manageddisplay.h @@ -19,8 +19,8 @@ ***/ -#ifndef OAK_MANAGEDDISPLAYOBJECT_H -#define OAK_MANAGEDDISPLAYOBJECT_H +#ifndef MANAGEDDISPLAYOBJECT_H +#define MANAGEDDISPLAYOBJECT_H //#define USE_QOPENGLWINDOW @@ -32,8 +32,12 @@ #include #endif -#include "node/color/colormanager/colormanager.h" +#include "oakengine/color.h" +#include "oakengine/events.h" +#include "render/colorprocessor.h" +#include "render/colortransform.h" #include "render/renderer.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/menu/menu.h" namespace olive @@ -53,9 +57,9 @@ public: virtual ~ManagedDisplayWidgetOpenGL() override { if (context()) { - destroy_listener(); + DestroyListener(); disconnect(context(), &QOpenGLContext::aboutToBeDestroyed, this, - &ManagedDisplayWidgetOpenGL::destroy_listener); + &ManagedDisplayWidgetOpenGL::DestroyListener); } } @@ -69,7 +73,7 @@ protected: virtual void initializeGL() override { connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, - &ManagedDisplayWidgetOpenGL::destroy_listener, + &ManagedDisplayWidgetOpenGL::DestroyListener, Qt::DirectConnection); emit on_init(); @@ -81,7 +85,7 @@ protected: } private slots: - void destroy_listener() + void DestroyListener() { makeCurrent(); @@ -142,7 +146,7 @@ public: /** * @brief Access currently connected ColorManager (nullptr if none) */ - ColorManager *color_manager() const; + OakEngineColorManager *color_manager() const; /** * @brief Get current color transform @@ -185,18 +189,18 @@ public slots: /** * @brief Connect a ColorManager (ColorManagers usually belong to the Project) */ - void connect_color_manager(ColorManager *color_manager); + void connect_color_manager(OakEngineColorManager *color_manager); signals: /** * @brief Emitted when the color processor changes */ - void color_processor_changed(ColorProcessorPtr processor); + void color_processor_changed(ColorProcessorHandlePtr processor); /** * @brief Emitted when a new color manager is connected */ - void color_manager_changed(ColorManager *color_manager); + void color_manager_changed(OakEngineColorManager *color_manager); void frame_swapped(); @@ -204,7 +208,7 @@ protected: /** * @brief Provides access to the color processor (nullptr if none is set) */ - ColorProcessorPtr color_service(); + ColorProcessorHandlePtr color_service(); /** * @brief Enables a context menu that allows simple access to the DVL pipeline @@ -216,7 +220,7 @@ protected: * * Default functionality is just to call update() */ - virtual void ColorProcessorChangedEvent(); + virtual void color_processor_changed_event(); Renderer *renderer() const { @@ -304,12 +308,12 @@ private: /** * @brief Connected color manager */ - ColorManager *color_manager_; + OakEngineColorManager *color_manager_; /** * @brief Color management service */ - ColorProcessorPtr color_service_; + ColorProcessorHandlePtr color_service_; /** * @brief Internal color transform storage @@ -318,6 +322,8 @@ private: bool is_backend_neutral_ = false; + QVector color_subs_; + private slots: /** * @brief Sets all color settings to the defaults pertaining to this configuration @@ -352,4 +358,4 @@ private slots: } -#endif // OAK_MANAGEDDISPLAYOBJECT_H +#endif // MANAGEDDISPLAYOBJECT_H diff --git a/app/widget/menu/factorymenu.cpp b/app/widget/menu/factorymenu.cpp index 9da24a9fc..ede641b9f 100644 --- a/app/widget/menu/factorymenu.cpp +++ b/app/widget/menu/factorymenu.cpp @@ -23,19 +23,22 @@ #include +#include "oakengine/node.h" + namespace olive { Menu *create_node_menu(QWidget *parent, bool create_none_item, Node::CategoryID restrict_to, uint64_t restrict_flags) { - const QList &library = NodeFactory::get_library(); + const int library_size = oakengine_node_factory_id_count(); Menu *menu = new Menu(parent); menu->setToolTipsVisible(true); - for (int i = 0; i < library.size(); i++) { - Node *n = library.at(i); + for (int i = 0; i < library_size; i++) { + olive::Node *n = reinterpret_cast( + oakengine_node_factory_node_at(i)); if (restrict_to != Node::k_category_unknown && !n->category().contains(restrict_to)) { @@ -54,9 +57,11 @@ Menu *create_node_menu(QWidget *parent, bool create_none_item, // Make sure nodes are up-to-date with the current translation n->retranslate(); - QString category_name = Node::get_category_name( - n->category().isEmpty() ? Node::k_category_unknown : - n->category().first()); + char cat_buf[256]; + oakengine_node_category_name( + n->category().isEmpty() ? 0 : n->category().first(), + cat_buf, sizeof(cat_buf)); + QString category_name = QString::fromUtf8(cat_buf); // Find or create top-level category menu Menu *top_menu = nullptr; @@ -120,7 +125,9 @@ Node *create_node_from_menu_action(QAction *action) return nullptr; } - return NodeFactory::get_library().at(index)->copy(); + olive::Node *proto = reinterpret_cast( + oakengine_node_factory_node_at(index)); + return proto ? proto->copy() : nullptr; } QString get_node_id_from_menu_action(QAction *action) @@ -131,7 +138,9 @@ QString get_node_id_from_menu_action(QAction *action) return QString(); } - return NodeFactory::get_library().at(action->data().toInt())->id(); + olive::Node *proto = reinterpret_cast( + oakengine_node_factory_node_at(index)); + return proto ? proto->id() : QString(); } } diff --git a/app/widget/menu/factorymenu.h b/app/widget/menu/factorymenu.h index 2ff9fd0f5..5e2c61392 100644 --- a/app/widget/menu/factorymenu.h +++ b/app/widget/menu/factorymenu.h @@ -24,7 +24,8 @@ #include -#include "node/factory.h" +#include "oakengine/node.h" +#include "node/node.h" #include "widget/menu/menu.h" namespace olive diff --git a/app/widget/menu/menushared.cpp b/app/widget/menu/menushared.cpp index 0d6ff7c7d..691c0e367 100644 --- a/app/widget/menu/menushared.cpp +++ b/app/widget/menu/menushared.cpp @@ -28,6 +28,7 @@ #include "panel/timeline/timeline.h" #include "window/mainwindow/mainwindow.h" +#include "oakengine/undo.h" namespace olive { @@ -172,8 +173,8 @@ void MenuShared::add_items_for_new_menu(Menu *m) void MenuShared::add_items_for_edit_menu(Menu *m, bool for_clips) { - m->addAction(Core::instance()->undo_stack()->GetUndoAction()); - m->addAction(Core::instance()->undo_stack()->GetRedoAction()); + m->addAction(reinterpret_cast(oakengine_undo_undo_action())); + m->addAction(reinterpret_cast(oakengine_undo_redo_action())); m->addSeparator(); diff --git a/app/widget/multicam/multicamdisplay.cpp b/app/widget/multicam/multicamdisplay.cpp index 70564eddb..e9c8e630e 100644 --- a/app/widget/multicam/multicamdisplay.cpp +++ b/app/widget/multicam/multicamdisplay.cpp @@ -21,6 +21,9 @@ #include "multicamdisplay.h" +#include "oakengine/display.h" +#include "oakengine/node.h" + namespace olive { @@ -45,15 +48,20 @@ void MulticamDisplay::on_paint() p.setBrush(Qt::NoBrush); int rows, cols; - node_->get_rows_and_columns(&rows, &cols); + oakengine_multicam_get_rows_and_columns( + oakengine_multicam_get_source_count( + reinterpret_cast(node_)), + &rows, &cols); int multi = std::max(rows, cols); int cell_width = width() / multi; int cell_height = height() / multi; int col, row; - node_->index_to_row_cols(node_->get_current_source(), rows, cols, &row, - &col); + int current_source = oakengine_multicam_get_current_source( + reinterpret_cast(node_)); + oakengine_multicam_index_to_row_cols( + current_source, rows, cols, &row, &col); QRect r(cell_width * col, cell_height * row, cell_width, cell_height); p.drawRect(generate_world_transform().mapRect(r)); @@ -70,10 +78,13 @@ TexturePtr MulticamDisplay::load_custom_texture_from_frame(const QVariant &v) if (v.canConvert>()) { QVector tex = v.value>(); - TexturePtr main = renderer()->create_texture(this->get_viewport_params()); + TexturePtr main; + const VideoParams main_params = this->get_viewport_params(); + oakengine_display_renderer_create_texture(renderer(), &main_params, + nullptr, 0, &main); int rows, cols; - MultiCamNode::get_rows_and_columns(tex.size(), &rows, &cols); + oakengine_multicam_get_rows_and_columns(tex.size(), &rows, &cols); if (shader_.isNull() || rows_ != rows || cols_ != cols) { if (!shader_.isNull()) { @@ -91,7 +102,7 @@ TexturePtr MulticamDisplay::load_custom_texture_from_frame(const QVariant &v) for (int i = 0; i < tex.size(); i++) { int c, r; - MultiCamNode::index_to_row_cols(i, rows, cols, &r, &c); + oakengine_multicam_index_to_row_cols(i, rows, cols, &r, &c); job.insert(QStringLiteral("tex_%1_%2") .arg(QString::number(r), QString::number(c)), NodeValue(NodeValue::k_texture, tex.at(i))); diff --git a/app/widget/multicam/multicamwidget.cpp b/app/widget/multicam/multicamwidget.cpp index c404fc2ca..298f4981a 100644 --- a/app/widget/multicam/multicamwidget.cpp +++ b/app/widget/multicam/multicamwidget.cpp @@ -21,9 +21,14 @@ #include "multicamwidget.h" +#include "oakengine/node.h" + #include -#include "node/nodeundo.h" +#include "oakengine/events.h" +#include "oakengine/viewer.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" #include "timeline/timelineundosplit.h" #include "widget/timeruler/timeruler.h" @@ -98,21 +103,41 @@ void MulticamWidget::set_multicam_node(ViewerOutput *viewer, MultiCamNode *n, void MulticamWidget::ConnectNodeEvent(ViewerOutput *n) { - connect(n, &ViewerOutput::size_changed, sizer_, &ViewerSizer::set_child_size); - connect(n, &ViewerOutput::pixel_aspect_changed, sizer_, - &ViewerSizer::set_pixel_aspect_ratio); + OakEngineNode *handle = reinterpret_cast(n); - VideoParams vp = n->get_video_params(); - sizer_->set_child_size(vp.width(), vp.height()); - sizer_->set_pixel_aspect_ratio(vp.pixel_aspect_ratio()); + viewer_sub_ = oakengine_event_subscribe( + handle, OAKENGINE_EVENT_VIEWER_SIZE_CHANGED, + [](const oakengine_event *event, void *userdata) { + auto *w = static_cast(userdata); + w->sizer_->set_child_size(int(event->a), int(event->b)); + }, + this); + viewer_sub2_ = oakengine_event_subscribe( + handle, OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED, + [](const oakengine_event *event, void *userdata) { + auto *w = static_cast(userdata); + w->sizer_->set_pixel_aspect_ratio( + Rational(event->a, event->b)); + }, + this); + + oak_video_params vp; + oakengine_viewer_get_video_params(handle, 0, &vp); + sizer_->set_child_size(vp.width, vp.height); + sizer_->set_pixel_aspect_ratio( + Rational(vp.pixel_aspect_num, vp.pixel_aspect_den)); } void MulticamWidget::DisconnectNodeEvent(ViewerOutput *n) { - disconnect(n, &ViewerOutput::size_changed, sizer_, - &ViewerSizer::set_child_size); - disconnect(n, &ViewerOutput::pixel_aspect_changed, sizer_, - &ViewerSizer::set_pixel_aspect_ratio); + if (viewer_sub_ > 0) { + oakengine_event_unsubscribe(viewer_sub_); + viewer_sub_ = 0; + } + if (viewer_sub2_ > 0) { + oakengine_event_unsubscribe(viewer_sub2_); + viewer_sub2_ = 0; + } } void MulticamWidget::TimeChangedEvent(const Rational &t) @@ -134,12 +159,11 @@ void MulticamWidget::Switch(int source, bool split_clip) return; } - MultiUndoCommand *command = new MultiUndoCommand(); - MultiCamNode *cam = node_; ClipBlock *clip = clip_; - BlockSplitPreservingLinksCommand *split = nullptr; + const QByteArray undo_name = tr("Switched Multi-Camera Source").toUtf8(); + oakengine_undo_group_begin(undo_name.constData()); if (clip_ && split_clip && clip_->in() < get_connected_node()->get_playhead() && @@ -149,33 +173,50 @@ void MulticamWidget::Switch(int source, bool split_clip) blocks.append(clip_); blocks.append(clip_->block_links()); - split = new BlockSplitPreservingLinksCommand( - blocks, { get_connected_node()->get_playhead() }); - split->redo_now(); - command->add_child(split); + int split_tbn = 0, split_tbd = 0; + oakengine_node_frame_time_base( + reinterpret_cast(get_connected_node()), + &split_tbn, &split_tbd); + void *split = oakengine_block_split_preserving_links_command( + reinterpret_cast(blocks.data()), blocks.size(), + olive::core::Timecode::time_to_timestamp( + get_connected_node()->get_playhead(), + olive::Rational(split_tbn, split_tbd), + olive::core::Timecode::k_round)); + oakengine_undo_push(split, undo_name.constData()); + clip = reinterpret_cast( + oakengine_block_split_get_split( + split, reinterpret_cast(clip_), 0)); - clip = static_cast(split->get_split(clip_, 0)); - - cam = clip->find_multicam(); + cam = reinterpret_cast( + oakengine_clip_find_multicam(reinterpret_cast(clip))); } - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference(NodeInput(cam, cam->k_current_input)), - source)); + oak_node_value val; + memset(&val, 0, sizeof(val)); + val.type = OAK_NODE_VALUE_INT; + val.num = source; - for (Block *link : clip->block_links()) { - if (ClipBlock *clink = dynamic_cast(link)) { - if (MultiCamNode *mlink = clink->find_multicam()) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(mlink, mlink->k_current_input)), - source)); + if (cam) { + oakengine_node_set_input( + reinterpret_cast(cam), + oakengine_multicam_input_current(), &val); + } + + if (clip) { + for (Block *link : clip->block_links()) { + if (ClipBlock *clink = dynamic_cast(link)) { + if (MultiCamNode *mlink = reinterpret_cast( + oakengine_clip_find_multicam(reinterpret_cast(clink)))) { + oakengine_node_set_input( + reinterpret_cast(mlink), + oakengine_multicam_input_current(), &val); + } } } } - Core::instance()->undo_stack()->push(command, - tr("Switched Multi-Camera Source")); + oakengine_undo_group_end(); display_->update(); @@ -198,14 +239,17 @@ void MulticamWidget::display_clicked(const QPoint &p) } int rows, cols; - node_->get_rows_and_columns(&rows, &cols); + oakengine_multicam_get_rows_and_columns( + oakengine_multicam_get_source_count( + reinterpret_cast(node_)), + &rows, &cols); int multi = std::max(cols, rows); int c = click.x() / (width / multi); int r = click.y() / (height / multi); - int source = node_->rows_cols_to_index(r, c, rows, cols); + int source = oakengine_multicam_rows_cols_to_index(r, c, rows, cols); Switch(source, true); } diff --git a/app/widget/multicam/multicamwidget.h b/app/widget/multicam/multicamwidget.h index bb46dffa5..d49360933 100644 --- a/app/widget/multicam/multicamwidget.h +++ b/app/widget/multicam/multicamwidget.h @@ -23,6 +23,8 @@ #define OAK_MULTICAMWIDGET_H #include "multicamdisplay.h" +#include + #include "node/input/multicam/multicamnode.h" #include "widget/viewer/viewer.h" @@ -58,6 +60,9 @@ private: ViewerSizer *sizer_; + int64_t viewer_sub_ = 0; + int64_t viewer_sub2_ = 0; + MulticamDisplay *display_; MultiCamNode *node_; diff --git a/app/widget/nodecombobox/nodecombobox.cpp b/app/widget/nodecombobox/nodecombobox.cpp index 6259c29ec..6de51805e 100644 --- a/app/widget/nodecombobox/nodecombobox.cpp +++ b/app/widget/nodecombobox/nodecombobox.cpp @@ -25,7 +25,7 @@ #include #include -#include "node/factory.h" +#include "oakengine/node.h" #include "ui/icons/icons.h" #include "widget/menu/factorymenu.h" #include "widget/menu/menu.h" @@ -77,7 +77,10 @@ void NodeComboBox::update_text() clear(); if (!selected_id_.isEmpty()) { - addItem(NodeFactory::get_name_from_id(selected_id_)); + char buf[256]; + oakengine_node_factory_name_from_id(selected_id_.toUtf8().constData(), + buf, sizeof(buf)); + addItem(QString::fromUtf8(buf)); } } diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 7dfc7dac2..5a5750360 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -25,10 +25,13 @@ #include #include #include +#include #include -#include "node/nodeundo.h" #include "node/output/viewer/viewer.h" +#include "oakengine/footage.h" +#include "oakengine/node.h" +#include "oakengine/undo.h" #include "widget/timeruler/timeruler.h" namespace olive @@ -89,8 +92,15 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) c->set_effect_type(static_cast(i)); title_bar->set_add_effect_button_visible(true); title_bar->set_text(tr("%1 Nodes") - .arg(Footage::get_stream_type_name( - static_cast(i)))); + .arg(QString::fromUtf8( + [i]() -> QByteArray { + char buf[64]; + buf[0] = '\0'; + oakengine_footage_stream_type_name( + static_cast(i), buf, + sizeof(buf)); + return QByteArray(buf); + }()))); } else { title_bar->set_text(tr("Other")); } @@ -173,6 +183,30 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) &QApplication::focusChanged, this, &NodeParamView::FocusChanged);*/ + + // Connect bridge signals for group input passthrough events + connect(bridge_, &EngineEventBridge::group_input_passthrough_added, this, + [this](OakEngineNode *source, OakEngineNode *node, + const QString &input, int element) { + group_input_passthrough_added(source, + NodeInput(reinterpret_cast(node), input, element)); + }); + connect(bridge_, &EngineEventBridge::group_input_passthrough_removed, this, + [this](OakEngineNode *source, OakEngineNode *node, + const QString &input, int element) { + group_input_passthrough_removed(source, + NodeInput(reinterpret_cast(node), input, element)); + }); + connect(bridge_, &EngineEventBridge::node_node_added_to_context, this, + [this](OakEngineNode *source, OakEngineNode *node) { + node_added_to_context(reinterpret_cast(node), + reinterpret_cast(source)); + }, Qt::QueuedConnection); + connect(bridge_, &EngineEventBridge::node_node_removed_from_context, this, + [this](OakEngineNode *source, OakEngineNode *node) { + node_removed_from_context(reinterpret_cast(node), + reinterpret_cast(source)); + }, Qt::QueuedConnection); } NodeParamView::~NodeParamView() @@ -276,18 +310,46 @@ void NodeParamView::update_contexts() if (changes_made) { current_contexts_ = contexts_; + // Tear down any previous group input passthrough subscriptions before + // (re-)subscribing to the current group, avoiding duplicates across + // repeated calls and stale callbacks after leaving group mode. + if (group_passthrough_added_sub_ > 0) { + bridge_->unsubscribe(group_passthrough_added_sub_); + group_passthrough_added_sub_ = 0; + } + if (group_passthrough_removed_sub_ > 0) { + bridge_->unsubscribe(group_passthrough_removed_sub_); + group_passthrough_removed_sub_ = 0; + } + if (is_group_mode()) { // Check inputs that have been passed through - NodeGroup *group = static_cast(contexts_.first()); - for (auto it = group->get_input_passthroughs().cbegin(); - it != group->get_input_passthroughs().cend(); it++) { - group_input_passthrough_added(group, it->second); + Node *group = contexts_.first(); + const int pt_count = oakengine_group_input_passthrough_count( + reinterpret_cast(group)); + for (int i = 0; i < pt_count; i++) { + OakEngineNode *inner_node = nullptr; + char inner_input[256]; + int inner_element = 0; + char id[256]; + if (oakengine_group_input_passthrough_at( + reinterpret_cast(group), i, + id, sizeof(id), &inner_node, inner_input, + sizeof(inner_input), &inner_element) == + OAKENGINE_OK) { + group_input_passthrough_added( + reinterpret_cast(group), + NodeInput(reinterpret_cast(inner_node), + QString::fromUtf8(inner_input), + inner_element)); + } } - connect(group, &NodeGroup::input_passthrough_added, this, - &NodeParamView::group_input_passthrough_added); - connect(group, &NodeGroup::input_passthrough_removed, this, - &NodeParamView::group_input_passthrough_removed); + OakEngineNode *group_handle = reinterpret_cast(group); + group_passthrough_added_sub_ = bridge_->subscribe( + group_handle, OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_ADDED); + group_passthrough_removed_sub_ = bridge_->subscribe( + group_handle, OAKENGINE_EVENT_GROUP_INPUT_PASSTHROUGH_REMOVED); } foreach (NodeParamViewContext *ctx, context_items_) { @@ -324,11 +386,12 @@ void NodeParamView::item_clicked() toggle_select(static_cast(sender())); } -void NodeParamView::select_node_from_connected_link(Node *node) +void NodeParamView::select_node_from_connected_link(OakEngineNode *node) { NodeParamViewItem *item = static_cast(sender()); - Node::ContextPair p = { node, item->get_context() }; + QPair p = qMakePair( + node, reinterpret_cast(item->get_context())); set_selected_nodes({ p }); } @@ -388,20 +451,46 @@ void NodeParamView::ConnectedNodeChangeEvent(ViewerOutput *n) } } -void reconnect_outputs_if_not_deleting_node(MultiUndoCommand *c, - NodeViewDeleteCommand *dc, Node *output, - Node *deleting, Node *context) +// Collects the bypass rewiring edges for a node about to be deleted: every +// consumer of `deleting` gets rewired to `output` (the first surviving node +// upstream). The edges are handed to oakengine_nodes_delete_many_ex(), which +// applies them AFTER the deletion inside the same undoable command — the +// target inputs are still occupied until then, so connecting here directly +// would fail with OAKENGINE_E_STATE. +struct ReconnectEdgeList { + QVector outputs; + QVector input_nodes; + QVector ids_storage; + QVector ids; + QVector elements; + + void finalize_ids() + { + ids.clear(); + ids.reserve(ids_storage.size()); + for (const QByteArray &b : ids_storage) { + ids.append(b.constData()); + } + } +}; + +void collect_reconnect_edges(QSet &deleted_nodes, Node *output, + Node *deleting, ReconnectEdgeList &edges) { for (auto it = deleting->output_connections().cbegin(); it != deleting->output_connections().cend(); it++) { const NodeInput &proposed_reconnect = it->second; - if (dc->contains_node(proposed_reconnect.node(), context)) { + if (deleted_nodes.contains(proposed_reconnect.node())) { // Uh-oh we're deleting this node too, instead connect to its outputs - reconnect_outputs_if_not_deleting_node( - c, dc, output, proposed_reconnect.node(), context); + collect_reconnect_edges(deleted_nodes, output, + proposed_reconnect.node(), edges); } else { - c->add_child(new NodeEdgeAddCommand(output, it->second)); + edges.outputs.append(reinterpret_cast(output)); + edges.input_nodes.append( + reinterpret_cast(proposed_reconnect.node())); + edges.ids_storage.append(proposed_reconnect.input().toUtf8()); + edges.elements.append(proposed_reconnect.element()); } } } @@ -411,19 +500,22 @@ void NodeParamView::DeleteSelected() if (keyframe_view_ && keyframe_view_->hasFocus()) { keyframe_view_->delete_selected(); } else if (!selected_nodes_.isEmpty()) { - MultiUndoCommand *c = new MultiUndoCommand(); + QVector nodes; + QVector contexts; - // Create command to delete node from context and/or graph - NodeViewDeleteCommand *dc = new NodeViewDeleteCommand(); - c->add_child(dc); + QSet deleted_nodes_set; - // Add all nodes + // Collect all nodes to delete foreach (NodeParamViewItem *item, selected_nodes_) { Node *n = item->get_node(); - dc->add_node(n, item->get_context()); + nodes.append(reinterpret_cast(n)); + contexts.append(reinterpret_cast(item->get_context())); + deleted_nodes_set.insert(n); } - // Make reconnections where possible + // Collect bypass rewiring edges (applied after the deletion by the + // facade, inside the same undoable command) + ReconnectEdgeList edges; foreach (NodeParamViewItem *item, selected_nodes_) { Node *n = item->get_node(); @@ -435,8 +527,7 @@ void NodeParamView::DeleteSelected() if ((connected_to_effect_input = node_being_deleted->get_effect_input() .get_connected_output())) { - if (dc->contains_node(connected_to_effect_input, - item->get_context())) { + if (deleted_nodes_set.contains(connected_to_effect_input)) { // Node's getting deleted, recurse node_being_deleted = connected_to_effect_input; continue; @@ -448,13 +539,21 @@ void NodeParamView::DeleteSelected() } if (connected_to_effect_input) { - reconnect_outputs_if_not_deleting_node( - c, dc, connected_to_effect_input, n, item->get_context()); + collect_reconnect_edges(deleted_nodes_set, + connected_to_effect_input, n, edges); } } + edges.finalize_ids(); - Core::instance()->undo_stack()->push( - c, tr("Deleted %1 Node(s)").arg(selected_nodes_.size())); + // Delete the nodes and rewire around them in ONE undoable command + oakengine_nodes_delete_many_ex( + nodes.constData(), contexts.constData(), nodes.size(), + nullptr, nullptr, nullptr, nullptr, 0, + edges.outputs.isEmpty() ? nullptr : edges.outputs.constData(), + edges.input_nodes.isEmpty() ? nullptr : edges.input_nodes.constData(), + edges.ids.isEmpty() ? nullptr : edges.ids.constData(), + edges.elements.isEmpty() ? nullptr : edges.elements.constData(), + edges.outputs.size()); } } @@ -472,7 +571,7 @@ void NodeParamView::set_selected_nodes(const QVector &nodes selected_nodes_ = nodes; - QVector p; + QVector> p; if (emit_signal) { p.resize(selected_nodes_.size()); } @@ -482,7 +581,8 @@ void NodeParamView::set_selected_nodes(const QVector &nodes n->set_highlighted(true); if (emit_signal) { - p[i] = { n->get_node(), n->get_context() }; + p[i] = qMakePair(reinterpret_cast(n->get_node()), + reinterpret_cast(n->get_context())); } } @@ -497,7 +597,7 @@ void NodeParamView::set_selected_nodes(const QVector &nodes } Node *n = focused_node_ ? focused_node_->get_node() : nullptr; - emit focused_node_changed(n); + emit focused_node_changed(reinterpret_cast(n)); } if (emit_signal) { @@ -505,18 +605,21 @@ void NodeParamView::set_selected_nodes(const QVector &nodes } } -void NodeParamView::set_selected_nodes(const QVector &nodes, - bool emit_signal) +void NodeParamView::set_selected_nodes( + const QVector> &nodes, + bool emit_signal) { QVector items; NodeParamViewContext *scrolled_ctx = nullptr; - foreach (const Node::ContextPair &n, nodes) { + foreach (const auto &n, nodes) { + Node *node = reinterpret_cast(n.first); + Node *context = reinterpret_cast(n.second); for (auto it = context_items_.cbegin(); it != context_items_.cend(); it++) { NodeParamViewContext *ctx = *it; - NodeParamViewItem *item = ctx->get_item(n.node, n.context); + NodeParamViewItem *item = ctx->get_item(node, context); if (item) { items.append(item); @@ -588,8 +691,8 @@ bool NodeParamView::copy_selected(bool cut) return false; } - ProjectSerializer::SaveData sdata(ProjectSerializer::k_only_nodes); - ProjectSerializer::SerializedProperties properties; + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_NODES, nullptr, nullptr); QVector nodes; for (NodeParamViewItem *item : selected_nodes_) { @@ -601,19 +704,26 @@ bool NodeParamView::copy_selected(bool cut) Node::Position pos = item->get_context()->get_node_position_data_in_context(n); - properties[n][QStringLiteral("x")] = - QString::number(pos.position.x()); - properties[n][QStringLiteral("y")] = - QString::number(pos.position.y()); - properties[n][QStringLiteral("expanded")] = - QString::number(pos.expanded); + oakengine_clipboard_set_property( + cb, reinterpret_cast(n), "x", + QByteArray::number(pos.position.x()).constData()); + oakengine_clipboard_set_property( + cb, reinterpret_cast(n), "y", + QByteArray::number(pos.position.y()).constData()); + oakengine_clipboard_set_property( + cb, reinterpret_cast(n), "expanded", + QByteArray::number(pos.expanded).constData()); } } - sdata.set_only_serialize_nodes_and_resolve_groups(nodes); - sdata.set_properties(properties); + oakengine_clipboard_set_nodes( + cb, + reinterpret_cast( + nodes.constData()), + nodes.size()); - ProjectSerializer::copy(sdata); + oakengine_clipboard_copy(cb); + oakengine_clipboard_free(cb); if (cut) { DeleteSelected(); @@ -637,20 +747,39 @@ bool NodeParamView::paste() bool NodeParamView::paste( QWidget *parent, - std::function(const ProjectSerializer::Result &)> + std::function(void *)> get_existing_map_function) { - ProjectSerializer::Result res = - ProjectSerializer::paste(ProjectSerializer::k_only_nodes); - if (res.get_load_data().nodes.isEmpty()) { + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_NODES, nullptr, nullptr); + int result_code = OAKENGINE_SERIALIZER_NO_DATA; + oakengine_clipboard_paste(cb, OAKENGINE_CLIPBOARD_NODES, nullptr, + &result_code, nullptr, 0); + + if (result_code != OAKENGINE_SERIALIZER_OK) { + oakengine_clipboard_free(cb); + return false; + } + + // Collect pasted nodes + QVector pasted_nodes; + const int node_count = oakengine_clipboard_get_loaded_node_count(cb); + pasted_nodes.reserve(node_count); + for (int i = 0; i < node_count; i++) { + pasted_nodes.append(reinterpret_cast( + oakengine_clipboard_get_loaded_node_at(cb, i))); + } + + if (pasted_nodes.isEmpty()) { + oakengine_clipboard_free(cb); return false; } // Determine if any nodes of this type are already in the editor - QHash existing_nodes = get_existing_map_function(res); + QHash existing_nodes = get_existing_map_function(cb); - QVector nodes_to_paste_as_new = res.get_load_data().nodes; - MultiUndoCommand *command = new MultiUndoCommand(); + QVector nodes_to_paste_as_new = pasted_nodes; + void *command = oakengine_undo_command_create_multi(); if (!existing_nodes.empty()) { QMessageBox b(parent); @@ -677,44 +806,31 @@ bool NodeParamView::paste( b.exec(); if (b.clickedButton() == cancel_btn) { - // Delete pasted nodes and clear array so no later code runs qDeleteAll(nodes_to_paste_as_new); nodes_to_paste_as_new.clear(); } else if (b.clickedButton() == as_vals) { - // Filter out existing nodes for (auto it = existing_nodes.cbegin(); it != existing_nodes.cend(); it++) { - Node::copy_inputs(it.value(), it.key(), false, command); + // NOTE: the C ABI oakengine_node_copy_inputs pushes its own + // undo entry rather than becoming a child of `command`. + oakengine_node_copy_inputs( + reinterpret_cast(it.key()), + reinterpret_cast(it.value())); nodes_to_paste_as_new.removeOne(it.value()); } } } - if (!nodes_to_paste_as_new.isEmpty()) { - Node::PositionMap map; + // NOTE: upstream Olive built a Node::PositionMap from the clipboard + // properties here but never applied it (dead code). Dropped during the + // facade migration instead of carrying a placeholder; positions fall + // back to the context default, same as before. - for (auto it = res.get_load_data().properties.cbegin(); - it != res.get_load_data().properties.cend(); it++) { - if (nodes_to_paste_as_new.contains(it.key())) { - Node::Position pos; - - const QMap &node_props = it.value(); - pos.position.setX( - node_props.value(QStringLiteral("x")).toDouble()); - pos.position.setY( - node_props.value(QStringLiteral("y")).toDouble()); - pos.expanded = - node_props.value(QStringLiteral("expanded")).toDouble(); - - map.insert(it.key(), pos); - } - } - } - - Core::instance()->undo_stack()->push( - command, tr("Pasted %1 Node(s)").arg(nodes_to_paste_as_new.size())); + oakengine_undo_push( + command, tr("Pasted %1 Node(s)").arg(nodes_to_paste_as_new.size()).toUtf8().constData()); + oakengine_clipboard_free(cb); return true; } @@ -737,10 +853,12 @@ void NodeParamView::add_context(Node *ctx) // Queued so that if any further work is done in connecting this node to the context, it'll be // done before our sorting function is called - connect(ctx, &Node::node_added_to_context, this, - &NodeParamView::node_added_to_context, Qt::QueuedConnection); - connect(ctx, &Node::node_removed_from_context, this, - &NodeParamView::node_removed_from_context, Qt::QueuedConnection); + context_subs_[ctx].first = bridge_->subscribe( + reinterpret_cast(ctx), + OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT); + context_subs_[ctx].second = bridge_->subscribe( + reinterpret_cast(ctx), + OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT); item->add_context(ctx); item->setVisible(true); @@ -753,10 +871,9 @@ void NodeParamView::add_context(Node *ctx) void NodeParamView::remove_context(Node *ctx) { - disconnect(ctx, &Node::node_added_to_context, this, - &NodeParamView::node_added_to_context); - disconnect(ctx, &Node::node_removed_from_context, this, - &NodeParamView::node_removed_from_context); + auto subs = context_subs_.take(ctx); + bridge_->unsubscribe(subs.first); + bridge_->unsubscribe(subs.second); foreach (NodeParamViewContext *item, context_items_) { item->remove_context(ctx); @@ -915,18 +1032,22 @@ void NodeParamView::toggle_select(NodeParamViewItem *item) // no gizmos focused_node_ = item; - emit focused_node_changed(focused_node_ ? focused_node_->get_node() : + emit focused_node_changed(focused_node_ ? reinterpret_cast(focused_node_->get_node()) : nullptr); } } } QHash -NodeParamView::generate_existing_paste_map(const ProjectSerializer::Result &r) +NodeParamView::generate_existing_paste_map(void *clipboard) { QVector ignore_nodes; QHash existing_nodes; - for (Node *n : r.get_load_data().nodes) { + OakEngineClipboard *cb = static_cast(clipboard); + const int node_count = oakengine_clipboard_get_loaded_node_count(cb); + for (int i = 0; i < node_count; i++) { + Node *n = reinterpret_cast( + oakengine_clipboard_get_loaded_node_at(cb, i)); if (Node *existing = get_node_with_id_and_ignore_list(n->id(), ignore_nodes)) { existing_nodes.insert(existing, n); @@ -1004,10 +1125,19 @@ void NodeParamView::update_element_y() if (!connections.isEmpty()) { for (const QString &input : node->inputs()) { if (!(node->get_input_flags(input) & k_input_flag_hidden)) { - int arr_sz = - NodeGroup::resolve_input(NodeInput(node, input)) - .get_array_size(); - + OakEngineNode *out_node = nullptr; + char out_input[256]; + int out_element = 0; + int arr_sz = 0; + if (oakengine_group_resolve_input( + reinterpret_cast( + contexts_.first()), + input.toUtf8().constData(), -1, &out_node, + out_input, sizeof(out_input), + &out_element) == OAKENGINE_OK && out_node) { + arr_sz = oakengine_node_input_array_size( + out_node, out_input); + } for (int i = -1; i < arr_sz; i++) { NodeInput ic = { node, input, i }; @@ -1035,9 +1165,8 @@ void NodeParamView::update_element_y() } } -void NodeParamView::node_added_to_context(Node *n) +void NodeParamView::node_added_to_context(Node *n, Node *ctx) { - Node *ctx = static_cast(sender()); NodeParamViewContext *item = get_context_item_from_context(ctx); add_node(n, ctx, item); @@ -1049,10 +1178,8 @@ void NodeParamView::node_added_to_context(Node *n) } } -void NodeParamView::node_removed_from_context(Node *n) +void NodeParamView::node_removed_from_context(Node *n, Node *ctx) { - Node *ctx = static_cast(sender()); - foreach (NodeParamViewContext *ctx_item, context_items_) { ctx_item->remove_node(n, ctx); } @@ -1064,16 +1191,22 @@ void NodeParamView::node_removed_from_context(Node *n) void NodeParamView::input_check_box_changed(const NodeInput &input, bool e) { - NodeGroup *group = static_cast(contexts_.first()); + Node *group = contexts_.first(); if (e) { - group->add_input_passthrough(input); + char out_id[256]; + oakengine_group_add_input_passthrough( + reinterpret_cast(group), + nullptr, input.input().toUtf8().constData(), input.element(), + nullptr, out_id, sizeof(out_id)); } else { - group->remove_input_passthrough(input); + oakengine_group_remove_input_passthrough( + reinterpret_cast(group), + nullptr, input.input().toUtf8().constData(), input.element()); } } -void NodeParamView::group_input_passthrough_added(NodeGroup *group, +void NodeParamView::group_input_passthrough_added(OakEngineNode *group, const NodeInput &input) { foreach (NodeParamViewContext *pvctx, context_items_) { @@ -1081,7 +1214,7 @@ void NodeParamView::group_input_passthrough_added(NodeGroup *group, } } -void NodeParamView::group_input_passthrough_removed(NodeGroup *group, +void NodeParamView::group_input_passthrough_removed(OakEngineNode *group, const NodeInput &input) { foreach (NodeParamViewContext *pvctx, context_items_) { diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index e6c7cc2db..6b3aaedf1 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -22,12 +22,12 @@ #ifndef OAK_NODEPARAMVIEW_H #define OAK_NODEPARAMVIEW_H +#include #include #include -#include "node/group/group.h" #include "node/node.h" -#include "node/project/serializer/serializer.h" +#include "oakengine/serializer.h" #include "nodeparamviewcontext.h" #include "nodeparamviewdockarea.h" #include "nodeparamviewitem.h" @@ -65,8 +65,9 @@ public: void set_selected_nodes(const QVector &nodes, bool handle_focused_node = true, bool emit_signal = true); - void set_selected_nodes(const QVector &nodes, - bool emit_signal = true); + void set_selected_nodes( + const QVector> &nodes, + bool emit_signal = true); Node *get_node_with_id(const QString &id); Node *get_node_with_id_and_ignore_list(const QString &id, @@ -82,18 +83,23 @@ public: virtual bool paste() override; static bool paste( QWidget *parent, - std::function(const ProjectSerializer::Result &)> + std::function(void *)> get_existing_map_function); -public slots: +public: + // Not a slot: signature uses the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). All connections use new-style member-function syntax. void set_contexts(const QVector &contexts); +public slots: void update_element_y(); signals: - void focused_node_changed(Node *n); + void focused_node_changed(OakEngineNode *n); - void selected_nodes_changed(const QVector &nodes); + void selected_nodes_changed( + const QVector> &nodes); void request_viewer_to_start_editing_text(); @@ -130,6 +136,13 @@ private: void remove_context(Node *context); + // Ordinary member functions (NOT slots): their signatures use Node*, which + // must not be exposed to MOC. They are only invoked from lambdas inside + // this class, never used as connect() targets. + void node_added_to_context(Node *n, Node *ctx); + + void node_removed_from_context(Node *n, Node *ctx); + void add_node(Node *n, Node *ctx, NodeParamViewContext *context); void sort_items_in_context(NodeParamViewContext *context); @@ -139,13 +152,14 @@ private: bool is_group_mode() const { return contexts_.size() == 1 && - dynamic_cast(contexts_.first()); + oakengine_node_is_group( + reinterpret_cast(contexts_.first())); } void toggle_select(NodeParamViewItem *item); QHash - generate_existing_paste_map(const ProjectSerializer::Result &r); + generate_existing_paste_map(void *clipboard); KeyframeView *keyframe_view_; @@ -173,6 +187,13 @@ private: bool show_all_nodes_; + // Group input passthrough subscription IDs (guarded against duplicate + // subscriptions when update_contexts() is called repeatedly). + int64_t group_passthrough_added_sub_ = 0; + int64_t group_passthrough_removed_sub_ = 0; + + QHash> context_subs_; + private slots: void update_global_scroll_bar(); @@ -180,16 +201,12 @@ private slots: //void FocusChanged(QWidget *old, QWidget *now); - void node_added_to_context(Node *n); - - void node_removed_from_context(Node *n); - void input_check_box_changed(const NodeInput &input, bool e); - void group_input_passthrough_added(olive::NodeGroup *group, + void group_input_passthrough_added(OakEngineNode *group, const olive::NodeInput &input); - void group_input_passthrough_removed(olive::NodeGroup *group, + void group_input_passthrough_removed(OakEngineNode *group, const olive::NodeInput &input); void update_contexts(); @@ -198,7 +215,7 @@ private slots: void item_clicked(); - void select_node_from_connected_link(Node *node); + void select_node_from_connected_link(OakEngineNode *node); void request_edit_text_in_viewer(); diff --git a/app/widget/nodeparamview/nodeparamviewarraywidget.cpp b/app/widget/nodeparamview/nodeparamviewarraywidget.cpp index 8c3f84c9d..ded4265a1 100644 --- a/app/widget/nodeparamview/nodeparamviewarraywidget.cpp +++ b/app/widget/nodeparamview/nodeparamviewarraywidget.cpp @@ -35,14 +35,20 @@ NodeParamViewArrayWidget::NodeParamViewArrayWidget(Node *node, : QWidget(parent) , node_(node) , input_(input) + , bridge_(new EngineEventBridge(this)) { QHBoxLayout *layout = new QHBoxLayout(this); count_lbl_ = new QLabel(); layout->addWidget(count_lbl_); - connect(node_, &Node::input_array_size_changed, this, - &NodeParamViewArrayWidget::update_counter); + bridge_->subscribe(reinterpret_cast(node_), + OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED); + connect(bridge_, &EngineEventBridge::node_input_array_size_changed, this, + [this](OakEngineNode *, const QString &input, int old_size, + int new_size) { + update_counter(input, old_size, new_size); + }); update_counter(input_, 0, node_->input_array_size(input_)); } diff --git a/app/widget/nodeparamview/nodeparamviewarraywidget.h b/app/widget/nodeparamview/nodeparamviewarraywidget.h index ce98bb91a..0fa32d700 100644 --- a/app/widget/nodeparamview/nodeparamviewarraywidget.h +++ b/app/widget/nodeparamview/nodeparamviewarraywidget.h @@ -26,6 +26,7 @@ #include #include +#include "engineeventbridge.h" #include "node/param.h" namespace olive @@ -66,6 +67,8 @@ private: QLabel *count_lbl_; + EngineEventBridge *bridge_ = nullptr; + private slots: void update_counter(const QString &input, int old_size, int new_size); }; diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp index fe4cd5b7e..0dc5ca188 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp @@ -30,6 +30,9 @@ #include "widget/collapsebutton/collapsebutton.h" #include "widget/menu/menu.h" +#include "oakengine/viewer.h" +#include "oakengine/events.h" + namespace olive { @@ -39,6 +42,7 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const NodeInput &input, , input_(input) , connected_node_(nullptr) , viewer_(nullptr) + , bridge_(new EngineEventBridge(this)) { QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -79,15 +83,29 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const NodeInput &input, connected_to_lbl_->setFont(link_font); if (input_.is_connected()) { - input_connected(input_.get_connected_output(), input_); + input_connected(reinterpret_cast(input_.get_connected_output()), input_); } else { input_disconnected(nullptr, input_); } - connect(input_.node(), &Node::input_connected, this, - &NodeParamViewConnectedLabel::input_connected); - connect(input_.node(), &Node::input_disconnected, this, - &NodeParamViewConnectedLabel::input_disconnected); + bridge_->subscribe(reinterpret_cast(input_.node()), + OAKENGINE_EVENT_NODE_INPUT_CONNECTED); + bridge_->subscribe(reinterpret_cast(input_.node()), + OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED); + connect(bridge_, &EngineEventBridge::node_input_connected, this, + [this](OakEngineNode *source, OakEngineNode *output, + const QString &input, int element) { + input_connected(output, + NodeInput(reinterpret_cast(source), input, + element)); + }); + connect(bridge_, &EngineEventBridge::node_input_disconnected, this, + [this](OakEngineNode *source, OakEngineNode *output, + const QString &input, int element) { + input_disconnected(output, + NodeInput(reinterpret_cast(source), input, + element)); + }); // Creating the tree is expensive, hold off until the user specifically requests it value_tree_ = nullptr; @@ -98,15 +116,21 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const NodeInput &input, void NodeParamViewConnectedLabel::set_viewer_node(ViewerOutput *viewer) { if (viewer_) { - disconnect(viewer_, &ViewerOutput::playhead_changed, this, - &NodeParamViewConnectedLabel::update_value_tree); + oakengine_event_unsubscribe(viewer_sub_); + viewer_sub_ = 0; } viewer_ = viewer; if (viewer_) { - connect(viewer_, &ViewerOutput::playhead_changed, this, - &NodeParamViewConnectedLabel::update_value_tree); + viewer_sub_ = oakengine_event_subscribe( + reinterpret_cast(viewer_), + OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED, + [](const oakengine_event *, void *userdata) { + static_cast(userdata) + ->update_value_tree(); + }, + this); update_value_tree(); } } @@ -118,19 +142,19 @@ void NodeParamViewConnectedLabel::create_tree() layout()->addWidget(value_tree_); } -void NodeParamViewConnectedLabel::input_connected(Node *output, +void NodeParamViewConnectedLabel::input_connected(OakEngineNode *output, const NodeInput &input) { if (input_ != input) { return; } - connected_node_ = output; + connected_node_ = reinterpret_cast(output); update_label(); } -void NodeParamViewConnectedLabel::input_disconnected(Node *output, +void NodeParamViewConnectedLabel::input_disconnected(OakEngineNode *output, const NodeInput &input) { if (input_ != input) { @@ -163,7 +187,7 @@ void NodeParamViewConnectedLabel::show_label_context_menu() void NodeParamViewConnectedLabel::connection_clicked() { if (connected_node_) { - emit request_select_node(connected_node_); + emit request_select_node(reinterpret_cast(connected_node_)); } } diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h index 470731f8e..00d4f42f5 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.h +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.h @@ -22,10 +22,13 @@ #ifndef OAK_NODEPARAMVIEWCONNECTEDLABEL_H #define OAK_NODEPARAMVIEWCONNECTEDLABEL_H +#include "engineeventbridge.h" #include "node/param.h" #include "widget/clickablelabel/clickablelabel.h" #include "widget/nodevaluetree/nodevaluetree.h" +#include + namespace olive { @@ -38,12 +41,12 @@ public: void set_viewer_node(ViewerOutput *viewer); signals: - void request_select_node(Node *n); + void request_select_node(OakEngineNode *n); private slots: - void input_connected(Node *output, const NodeInput &input); + void input_connected(OakEngineNode *output, const NodeInput &input); - void input_disconnected(Node *output, const NodeInput &input); + void input_disconnected(OakEngineNode *output, const NodeInput &input); void show_label_context_menu(); @@ -66,6 +69,10 @@ private: ViewerOutput *viewer_; + EngineEventBridge *bridge_ = nullptr; + + int64_t viewer_sub_ = 0; + private slots: void set_value_tree_visible(bool e); }; diff --git a/app/widget/nodeparamview/nodeparamviewcontext.cpp b/app/widget/nodeparamview/nodeparamviewcontext.cpp index 5422ce201..55381e168 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.cpp +++ b/app/widget/nodeparamview/nodeparamviewcontext.cpp @@ -24,7 +24,8 @@ #include #include "node/block/clip/clip.h" -#include "node/nodeundo.h" +#include "oakengine/node.h" +#include "oakengine/undo.h" #include "widget/menu/factorymenu.h" namespace olive @@ -160,7 +161,7 @@ void NodeParamViewContext::add_effect_menu_item_triggered(QAction *a) if (n) { NodeInput new_node_input = n->get_effect_input(); - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); QVector graphs_added_to; @@ -168,29 +169,51 @@ void NodeParamViewContext::add_effect_menu_item_triggered(QAction *a) NodeInput ctx_input = ctx->get_effect_input(); if (!graphs_added_to.contains(ctx->parent())) { - command->add_child(new NodeAddCommand(ctx->parent(), n)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_add_to_project_command( + reinterpret_cast(ctx->parent()), + reinterpret_cast(n))); graphs_added_to.append(ctx->parent()); } - command->add_child(new NodeSetPositionCommand( - n, ctx, ctx->get_node_position_in_context(ctx))); - command->add_child(new NodeSetPositionCommand( - ctx, ctx, ctx->get_node_position_in_context(ctx) + QPointF(1, 0))); + oakengine_undo_command_multi_add_child( + command, oakengine_node_set_position_command(reinterpret_cast(n), reinterpret_cast(ctx), ctx->get_node_position_in_context(ctx).x(), ctx->get_node_position_in_context(ctx).y(), 0)); + oakengine_undo_command_multi_add_child( + command, oakengine_node_set_position_command( + reinterpret_cast(ctx), reinterpret_cast(ctx), + ctx->get_node_position_in_context(ctx).x() + 1, + ctx->get_node_position_in_context(ctx).y(), 0)); if (ctx_input.is_connected()) { Node *prev_output = ctx_input.get_connected_output(); - command->add_child( - new NodeEdgeRemoveCommand(prev_output, ctx_input)); - command->add_child( - new NodeEdgeAddCommand(prev_output, new_node_input)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_disconnect_command( + reinterpret_cast(ctx_input.node()), + ctx_input.input().toUtf8().constData(), + ctx_input.element())); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(prev_output), + reinterpret_cast(new_node_input.node()), + new_node_input.input().toUtf8().constData(), + new_node_input.element())); } - command->add_child(new NodeEdgeAddCommand(n, ctx_input)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(n), + reinterpret_cast(ctx_input.node()), + ctx_input.input().toUtf8().constData(), + ctx_input.element())); } - Core::instance()->undo_stack()->push( - command, tr("Added %1 to Node Chain").arg(n->name())); + oakengine_undo_push( + command, tr("Added %1 to Node Chain").arg(n->name()).toUtf8().constData()); } } diff --git a/app/widget/nodeparamview/nodeparamviewcontext.h b/app/widget/nodeparamview/nodeparamviewcontext.h index 81f844916..06cd3b6b8 100644 --- a/app/widget/nodeparamview/nodeparamviewcontext.h +++ b/app/widget/nodeparamview/nodeparamviewcontext.h @@ -68,7 +68,10 @@ public: signals: void about_to_delete_item(NodeParamViewItem *item); -public slots: +public: + // Not slots: signatures use the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). They are called directly, never used as connect() targets. void add_context(Node *node) { contexts_.append(node); diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 0f66f9189..9918db83e 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -26,7 +26,6 @@ #include "common/qtutils.h" #include "dialog/speedduration/speeddurationdialog.h" -#include "node/group/group.h" #include "node/project/sequence/sequence.h" #include "oakengine/node.h" #include "pluginSupport/oliveplugininstance.h" @@ -34,6 +33,23 @@ namespace olive { +static NodeInput ResolveGroupInput(const NodeInput &input) +{ + OakEngineNode *node = reinterpret_cast(input.node()); + char input_id[256]; + int element = input.element(); + const QByteArray utf = input.input().toUtf8(); + memcpy(input_id, utf.constData(), qMin(sizeof(input_id) - 1, utf.size())); + input_id[sizeof(input_id) - 1] = '\0'; + if (oakengine_group_resolve_input( + node, input_id, element, + &node, input_id, sizeof(input_id), &element) != OAKENGINE_OK) { + return input; + } + return NodeInput(reinterpret_cast(node), + QString::fromUtf8(input_id), element); +} + const int NodeParamViewItemBody::k_key_control_column = 10; const int NodeParamViewItemBody::k_array_insert_column = k_key_control_column - 1; const int NodeParamViewItemBody::k_array_remove_column = k_array_insert_column - 1; @@ -59,21 +75,32 @@ NodeParamViewItem::NodeParamViewItem( , create_checkboxes_(create_checkboxes) , ctx_(nullptr) , time_target_(nullptr) + , bridge_(new EngineEventBridge(this)) { node_->retranslate(); // Create and add contents widget recreate_body(); - connect(node_, &Node::label_changed, this, &NodeParamViewItem::retranslate); - connect(node_, &Node::input_array_size_changed, this, - &NodeParamViewItem::input_array_size_changed); - connect(node_, &Node::message_count_changed, this, - &NodeParamViewItem::update_message_panel); + bridge_->subscribe(reinterpret_cast(node_), + OAKENGINE_EVENT_NODE_LABEL_CHANGED); + bridge_->subscribe(reinterpret_cast(node_), + OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED); + bridge_->subscribe(reinterpret_cast(node_), + OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED); + bridge_->subscribe(reinterpret_cast(node_), + OAKENGINE_EVENT_NODE_INPUT_FLAGS_CHANGED); - // FIXME: Implemented to pick up when an input is set to hidden or not - DEFINITELY not a fast - // way of doing this, but "fine" for now. - connect(node_, &Node::input_flags_changed, this, + connect(bridge_, &EngineEventBridge::node_label_changed, this, + &NodeParamViewItem::retranslate); + connect(bridge_, &EngineEventBridge::node_input_array_size_changed, this, + [this](OakEngineNode *, const QString &input, int old_sz, + int new_size) { + emit input_array_size_changed(input, old_sz, new_size); + }); + connect(bridge_, &EngineEventBridge::node_message_count_changed, this, + &NodeParamViewItem::update_message_panel); + connect(bridge_, &EngineEventBridge::node_input_flags_changed, this, &NodeParamViewItem::recreate_body); setBackgroundRole(QPalette::Window); @@ -224,6 +251,7 @@ NodeParamViewItemBody::NodeParamViewItemBody( , node_(node) , time_target_(nullptr) , create_checkboxes_(create_checkboxes) + , bridge_(new EngineEventBridge(this)) { QGridLayout *root_layout = new QGridLayout(this); @@ -233,18 +261,35 @@ NodeParamViewItemBody::NodeParamViewItemBody( QVector connected_signals; + connect(bridge_, &EngineEventBridge::node_input_array_size_changed, + this, &NodeParamViewItemBody::input_array_size_changed); + connect(bridge_, &EngineEventBridge::node_input_connected, this, + [this](OakEngineNode *source, OakEngineNode *output, + const QString &input, int element) { + edge_changed(output, + NodeInput(reinterpret_cast(source), input, + element)); + }); + connect(bridge_, &EngineEventBridge::node_input_disconnected, this, + [this](OakEngineNode *source, OakEngineNode *output, + const QString &input, int element) { + edge_changed(output, + NodeInput(reinterpret_cast(source), input, + element)); + }); + // Create widgets all root level components foreach (QString input, node->inputs()) { Node *n = node; - NodeInput resolved = NodeGroup::resolve_input(NodeInput(n, input)); + NodeInput resolved = ResolveGroupInput(NodeInput(n, input)); if (!connected_signals.contains(resolved.node())) { - connect(resolved.node(), &Node::input_array_size_changed, this, - &NodeParamViewItemBody::input_array_size_changed); - connect(resolved.node(), &Node::input_connected, this, - &NodeParamViewItemBody::edge_changed); - connect(resolved.node(), &Node::input_disconnected, this, - &NodeParamViewItemBody::edge_changed); + bridge_->subscribe(reinterpret_cast(resolved.node()), + OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED); + bridge_->subscribe(reinterpret_cast(resolved.node()), + OAKENGINE_EVENT_NODE_INPUT_CONNECTED); + bridge_->subscribe(reinterpret_cast(resolved.node()), + OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED); connected_signals.append(resolved.node()); } @@ -402,7 +447,7 @@ void NodeParamViewItemBody::create_widgets(QGridLayout *layout, Node *node, place_widgets_from_bridge(layout, ui_objects.widget_bridge, row); // In case this input is a group, resolve that actual input to use for connected labels - NodeInput resolved = NodeGroup::resolve_input(input_ref); + NodeInput resolved = ResolveGroupInput(input_ref); if (node->is_input_connectable(input)) { // Create clickable label used when an input is connected @@ -492,7 +537,7 @@ int NodeParamViewItemBody::get_element_y(NodeInput c) const return lbl_center.y(); } -void NodeParamViewItemBody::edge_changed(Node *output, const NodeInput &input) +void NodeParamViewItemBody::edge_changed(OakEngineNode *output, const NodeInput &input) { Q_UNUSED(output) @@ -509,7 +554,7 @@ void NodeParamViewItemBody::update_ui_for_edge_connection(const NodeInput &input if (input_ui_map_.contains(input)) { const InputUI &ui_objects = input_ui_map_[input]; - bool is_connected = NodeGroup::resolve_input(input).is_connected(); + bool is_connected = ResolveGroupInput(input).is_connected(); foreach (QWidget *w, ui_objects.widget_bridge->widgets()) { w->setVisible(!is_connected); @@ -604,7 +649,7 @@ void NodeParamViewItemBody::array_collapse_btn_pressed(bool checked) if (checked) { // Ensure widgets are created (the signal will be ignored if they are) NodeInput resolved = - NodeGroup::resolve_input(NodeInput(input.node, input.input)); + ResolveGroupInput(NodeInput(input.node, input.input)); input_array_size_changed_internal(input.node, input.input, resolved.get_array_size()); } @@ -612,13 +657,14 @@ void NodeParamViewItemBody::array_collapse_btn_pressed(bool checked) emit array_expanded_changed(checked); } -void NodeParamViewItemBody::input_array_size_changed(const QString &input, +void NodeParamViewItemBody::input_array_size_changed(OakEngineNode *source, + const QString &input, int old_sz, int size) { Q_UNUSED(old_sz) NodeInputPair nip = - input_group_lookup_.value({ static_cast(sender()), input }); + input_group_lookup_.value({ reinterpret_cast(source), input }); input_array_size_changed_internal(nip.node, nip.input, size); } @@ -627,7 +673,7 @@ void NodeParamViewItemBody::array_append_clicked() { for (auto it = array_ui_.cbegin(); it != array_ui_.cend(); it++) { if (it.value().append_btn == sender()) { - NodeInput real_input = NodeGroup::resolve_input( + NodeInput real_input = ResolveGroupInput( NodeInput(it.key().node, it.key().input)); // Through the liboakengine C ABI facade (one undoable command, // same as the old NodeArrayInsertCommand push). @@ -645,7 +691,7 @@ void NodeParamViewItemBody::array_insert_clicked() for (auto it = input_ui_map_.cbegin(); it != input_ui_map_.cend(); it++) { if (it.value().array_insert_btn == sender()) { // Found our input and element - NodeInput ic = NodeGroup::resolve_input(it.key()); + NodeInput ic = ResolveGroupInput(it.key()); // Through the liboakengine C ABI facade (one undoable command). oakengine_node_array_insert_at( reinterpret_cast(ic.node()), @@ -660,7 +706,7 @@ void NodeParamViewItemBody::array_remove_clicked() for (auto it = input_ui_map_.cbegin(); it != input_ui_map_.cend(); it++) { if (it.value().array_remove_btn == sender()) { // Found our input and element - NodeInput ic = NodeGroup::resolve_input(it.key()); + NodeInput ic = ResolveGroupInput(it.key()); // Through the liboakengine C ABI facade (one undoable command). oakengine_node_array_remove_at( reinterpret_cast(ic.node()), diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index 03da38be2..4ce392aeb 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -30,6 +30,7 @@ #include #include "node/node.h" +#include "engineeventbridge.h" #include "nodeparamviewarraywidget.h" #include "nodeparamviewconnectedlabel.h" #include "nodeparamviewkeyframecontrol.h" @@ -67,7 +68,7 @@ public: void set_input_checked(const NodeInput &input, bool e); signals: - void request_select_node(Node *node); + void request_select_node(OakEngineNode *node); void array_expanded_changed(bool e); @@ -128,6 +129,8 @@ private: QHash input_group_lookup_; + EngineEventBridge *bridge_ = nullptr; + /** * @brief The column to place the keyframe controls in * @@ -147,11 +150,12 @@ private: static const int k_max_widget_column; private slots: - void edge_changed(Node *output, const NodeInput &input); + void edge_changed(OakEngineNode *output, const NodeInput &input); void array_collapse_btn_pressed(bool checked); - void input_array_size_changed(const QString &input, int old_sz, int size); + void input_array_size_changed(OakEngineNode *source, + const QString &input, int old_sz, int size); void array_append_clicked(); @@ -219,7 +223,7 @@ public: } signals: - void request_select_node(Node *node); + void request_select_node(OakEngineNode *node); void array_expanded_changed(bool e); @@ -251,6 +255,8 @@ private: KeyframeView::NodeConnections keyframe_connections_; + EngineEventBridge *bridge_ = nullptr; + private slots: void recreate_body(); void update_message_panel(); diff --git a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp index 415bd8524..4b34fd876 100644 --- a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp +++ b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp @@ -24,16 +24,31 @@ #include #include +#include "common/nodevaluehandle.h" +#include "common/oakvaluehelper.h" #include "core.h" -#include "node/nodeundo.h" +#include "node/value.h" +#include "oakengine/events.h" +#include "oakengine/undo.h" +#include "oakengine/viewer.h" +#include "oakengine/node.h" #include "ui/icons/icons.h" namespace olive { +static int64_t rational_to_node_ts(Node *node, const Rational &time) +{ + int num = 0, den = 1; + oakengine_node_frame_time_base(reinterpret_cast(node), + &num, &den); + return core::Timecode::time_to_timestamp(time, Rational(num, den)); +} + NodeParamViewKeyframeControl::NodeParamViewKeyframeControl(bool right_align, QWidget *parent) : QWidget(parent) + , bridge_(new EngineEventBridge(this)) { QHBoxLayout *layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -73,6 +88,21 @@ NodeParamViewKeyframeControl::NodeParamViewKeyframeControl(bool right_align, connect(enable_key_btn_, &QPushButton::clicked, this, &NodeParamViewKeyframeControl::keyframe_enable_btn_clicked); + connect(bridge_, &EngineEventBridge::node_keyframe_enable_changed, this, + [this](OakEngineNode *source, const QString &input, int element, + bool enabled) { + keyframe_enable_changed( + NodeInput(reinterpret_cast(source), input, + element), + enabled); + }); + connect(bridge_, &EngineEventBridge::node_keyframe_added, this, + &NodeParamViewKeyframeControl::update_state); + connect(bridge_, &EngineEventBridge::node_keyframe_removed, this, + &NodeParamViewKeyframeControl::update_state); + connect(bridge_, &EngineEventBridge::node_keyframe_time_changed, this, + &NodeParamViewKeyframeControl::update_state); + // Set defaults set_input(NodeInput()); show_buttons_from_keyframe_enable(false); @@ -81,14 +111,14 @@ NodeParamViewKeyframeControl::NodeParamViewKeyframeControl(bool right_align, void NodeParamViewKeyframeControl::set_input(const NodeInput &input) { if (input_.is_valid()) { - disconnect(input_.node(), &Node::keyframe_enable_changed, this, - &NodeParamViewKeyframeControl::keyframe_enable_changed); - disconnect(input_.node(), &Node::keyframe_added, this, - &NodeParamViewKeyframeControl::update_state); - disconnect(input_.node(), &Node::keyframe_removed, this, - &NodeParamViewKeyframeControl::update_state); - disconnect(input_.node(), &Node::keyframe_time_changed, this, - &NodeParamViewKeyframeControl::update_state); + bridge_->unsubscribe(keyframe_enable_sub_); + bridge_->unsubscribe(keyframe_added_sub_); + bridge_->unsubscribe(keyframe_removed_sub_); + bridge_->unsubscribe(keyframe_time_sub_); + keyframe_enable_sub_ = 0; + keyframe_added_sub_ = 0; + keyframe_removed_sub_ = 0; + keyframe_time_sub_ = 0; } input_ = input; @@ -101,27 +131,39 @@ void NodeParamViewKeyframeControl::set_input(const NodeInput &input) update_state(); if (input_.is_valid()) { - connect(input_.node(), &Node::keyframe_enable_changed, this, - &NodeParamViewKeyframeControl::keyframe_enable_changed); - connect(input_.node(), &Node::keyframe_added, this, - &NodeParamViewKeyframeControl::update_state); - connect(input_.node(), &Node::keyframe_removed, this, - &NodeParamViewKeyframeControl::update_state); - connect(input_.node(), &Node::keyframe_time_changed, this, - &NodeParamViewKeyframeControl::update_state); + keyframe_enable_sub_ = bridge_->subscribe( + reinterpret_cast(input_.node()), + OAKENGINE_EVENT_NODE_KEYFRAME_ENABLE_CHANGED); + keyframe_added_sub_ = bridge_->subscribe( + reinterpret_cast(input_.node()), + OAKENGINE_EVENT_NODE_KEYFRAME_ADDED); + keyframe_removed_sub_ = bridge_->subscribe( + reinterpret_cast(input_.node()), + OAKENGINE_EVENT_NODE_KEYFRAME_REMOVED); + keyframe_time_sub_ = bridge_->subscribe( + reinterpret_cast(input_.node()), + OAKENGINE_EVENT_NODE_KEYFRAME_TIME_CHANGED); } } void NodeParamViewKeyframeControl::TimeTargetDisconnectEvent(ViewerOutput *v) { - disconnect(v, &ViewerOutput::playhead_changed, this, - &NodeParamViewKeyframeControl::update_state); + if (viewer_sub_ > 0) { + oakengine_event_unsubscribe(viewer_sub_); + viewer_sub_ = 0; + } } void NodeParamViewKeyframeControl::TimeTargetConnectEvent(ViewerOutput *v) { - connect(v, &ViewerOutput::playhead_changed, this, - &NodeParamViewKeyframeControl::update_state); + viewer_sub_ = oakengine_event_subscribe( + reinterpret_cast(v), + OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED, + [](const oakengine_event *, void *userdata) { + static_cast(userdata) + ->update_state(); + }, + this); update_state(); } @@ -171,39 +213,64 @@ void NodeParamViewKeyframeControl::toggle_keyframe(bool e) QVector keys = input_.node()->get_keyframes_at_time(input_, node_time); - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); - int nb_tracks = input_.node()->get_number_of_keyframe_tracks(input_); + Node *node = input_.node(); + const NodeValue::Type declared = node->get_input_data_type(input_.input()); + + int nb_tracks = oakengine_node_value_keyframe_track_count( + node_value_type_to_c(declared)); + + const QByteArray input_utf8 = input_.input().toUtf8(); + const char *input_id = input_utf8.constData(); + const int element = input_.element(); + const int64_t time_ts = rational_to_node_ts(node, node_time); if (e && keys.isEmpty()) { // Add a keyframe here (one for each track) - for (int i = 0; i < nb_tracks; i++) { - NodeKeyframe *key = new NodeKeyframe( - node_time, - input_.node()->get_split_value_at_time_on_track(input_, node_time, i), - input_.node()->get_best_keyframe_type_for_time_on_track(input_, - node_time, i), - i, input_.element(), input_.input()); + oak_node_value v; + if (oakengine_node_get_input_at_time( + reinterpret_cast(node), input_id, element, -1, + time_ts, 1, &v) != OAKENGINE_OK) { + oakengine_undo_command_free(command); + return; + } - command->add_child( - new NodeParamInsertKeyframeCommand(input_.node(), key)); + for (int i = 0; i < nb_tracks; i++) { + void *cmd = oakengine_node_insert_keyframe_command( + reinterpret_cast(node), input_id, element, i, + time_ts, &v, + NodeKeyframeTypeToFacade( + node->get_best_keyframe_type_for_time_on_track(input_, + node_time, i)), + 0, 0, 0, 0); + oakengine_undo_command_multi_add_child(command, cmd); } } else if (!e && !keys.isEmpty()) { // Remove all keyframes at this time foreach (NodeKeyframe *key, keys) { - command->add_child(new NodeParamRemoveKeyframeCommand(key)); + void *cmd = oakengine_node_remove_keyframe_command( + reinterpret_cast(key)); + oakengine_undo_command_multi_add_child(command, cmd); - if (input_.node()->get_keyframe_tracks(input_).size() == 1) { - // If this was the last keyframe on this track, set the standard value to the value at this time too - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference(input_, key->track()), - input_.node()->get_split_value_at_time_on_track(input_, node_time, - key->track()))); + if (node->get_keyframe_tracks(input_).size() == 1) { + // If this was the last keyframe on this track, set the standard value + // to the value at this time too. + oak_node_value v; + NodeTrackComponentToOakNodeValue( + declared, + node->get_split_value_at_time_on_track(input_, node_time, + key->track()), + &v); + void *sv = oakengine_node_set_standard_value_command( + reinterpret_cast(node), input_id, element, + key->track(), &v); + oakengine_undo_command_multi_add_child(command, sv); } } } - Core::instance()->undo_stack()->push(command, tr("Toggled Keyframe")); + oakengine_undo_push(command, tr("Toggled Keyframe").toUtf8().constData()); } void NodeParamViewKeyframeControl::update_state() @@ -232,7 +299,9 @@ void NodeParamViewKeyframeControl::go_to_previous_key() if (previous_key && get_time_target()) { Rational key_time = convert_to_viewer_time(previous_key->time()); - get_time_target()->set_playhead(key_time); + oakengine_viewer_set_playhead( + reinterpret_cast(get_time_target()), + key_time.numerator(), key_time.denominator()); } } @@ -245,7 +314,9 @@ void NodeParamViewKeyframeControl::go_to_next_key() if (next_key && get_time_target()) { Rational key_time = convert_to_viewer_time(next_key->time()); - get_time_target()->set_playhead(key_time); + oakengine_viewer_set_playhead( + reinterpret_cast(get_time_target()), + key_time.numerator(), key_time.denominator()); } } @@ -256,71 +327,106 @@ void NodeParamViewKeyframeControl::keyframe_enable_btn_clicked(bool e) return; } - MultiUndoCommand *command = new MultiUndoCommand(); + Node *node = input_.node(); + const NodeValue::Type declared = node->get_input_data_type(input_.input()); + const QByteArray input_utf8 = input_.input().toUtf8(); + const char *input_id = input_utf8.constData(); + const int element = input_.element(); QString command_name; if (e) { // Enable keyframing - command->add_child(new NodeParamSetKeyframingCommand(input_, true)); + void *command = oakengine_undo_command_create_multi(); + + void *kf = oakengine_node_set_input_keyframing_command( + reinterpret_cast(node), input_id, element, 1); + oakengine_undo_command_multi_add_child(command, kf); // Create one keyframe across all tracks here - const QVector &key_vals = - input_.node()->get_split_standard_value(input_); + const QVector &key_vals = node->get_split_standard_value(input_); - for (int i = 0; i < key_vals.size(); i++) { - NodeKeyframe *key = - new NodeKeyframe(get_current_time_as_node_time(), key_vals.at(i), - NodeKeyframe::k_default_type, i, - input_.element(), input_.input()); - - command->add_child( - new NodeParamInsertKeyframeCommand(input_.node(), key)); + if (!key_vals.isEmpty()) { + QVector tracks(key_vals.size()); + bool converted = true; + for (int i = 0; i < key_vals.size(); i++) { + if (!NodeTrackComponentToOakNodeValue(declared, key_vals.at(i), + &tracks[i])) { + converted = false; + break; + } + } + oak_node_value v; + memset(&v, 0, sizeof(v)); + if (converted) { + oakengine_node_value_combine_tracks( + node_value_type_to_c(declared), tracks.constData(), + tracks.size(), &v); + } + const int64_t time_ts = + rational_to_node_ts(node, get_current_time_as_node_time()); + const int type = NodeKeyframeTypeToFacade(static_cast(oakengine_keyframe_default_type())); + for (int i = 0; i < key_vals.size(); i++) { + void *cmd = oakengine_node_insert_keyframe_command( + reinterpret_cast(node), input_id, element, i, + time_ts, &v, type, 0, 0, 0, 0); + oakengine_undo_command_multi_add_child(command, cmd); + } } command_name = tr("Enabled Keyframing On %1 - %2") - .arg(input_.node()->get_label_and_name(), input_.get_input_name()); + .arg(node->get_label_and_name(), input_.get_input_name()); + + oakengine_undo_push(command, command_name.toUtf8().constData()); } else { // Confirm the user wants to clear all keyframes if (QMessageBox::warning( this, tr("Warning"), tr("Are you sure you want to disable keyframing on this value? This will clear all existing keyframes."), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + void *command = oakengine_undo_command_create_multi(); + // Store value at this time, we'll set this as the persistent value later const QVector &stored_vals = - input_.node()->get_split_value_at_time(input_, - get_current_time_as_node_time()); + node->get_split_value_at_time(input_, + get_current_time_as_node_time()); // Delete all keyframes foreach (const NodeKeyframeTrack &track, - input_.node()->get_keyframe_tracks(input_)) { + node->get_keyframe_tracks(input_)) { for (int i = track.size() - 1; i >= 0; i--) { - command->add_child( - new NodeParamRemoveKeyframeCommand(track.at(i))); + void *cmd = oakengine_node_remove_keyframe_command( + reinterpret_cast(track.at(i))); + oakengine_undo_command_multi_add_child(command, cmd); } } // Update standard value for (int i = 0; i < stored_vals.size(); i++) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference(input_, i), stored_vals.at(i))); + oak_node_value v; + NodeTrackComponentToOakNodeValue(declared, stored_vals.at(i), &v); + void *cmd = oakengine_node_set_standard_value_command( + reinterpret_cast(node), input_id, element, i, + &v); + oakengine_undo_command_multi_add_child(command, cmd); } // Disable keyframing - command->add_child( - new NodeParamSetKeyframingCommand(input_, false)); + void *kf = oakengine_node_set_input_keyframing_command( + reinterpret_cast(node), input_id, element, 0); + oakengine_undo_command_multi_add_child(command, kf); command_name = tr("Disabled Keyframing On %1 - %2") - .arg(input_.node()->get_label_and_name(), - input_.get_input_name()); + .arg(node->get_label_and_name(), + input_.get_input_name()); + + oakengine_undo_push(command, command_name.toUtf8().constData()); } else { // Disable action has effectively been ignored enable_key_btn_->setChecked(true); } } - - Core::instance()->undo_stack()->push(command, command_name); } void NodeParamViewKeyframeControl::keyframe_enable_changed(const NodeInput &input, diff --git a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h index 6e7f079aa..bc52ccf02 100644 --- a/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h +++ b/app/widget/nodeparamview/nodeparamviewkeyframecontrol.h @@ -24,7 +24,9 @@ #include #include +#include +#include "engineeventbridge.h" #include "node/param.h" #include "widget/timetarget/timetarget.h" @@ -67,6 +69,15 @@ private: NodeInput input_; + EngineEventBridge *bridge_ = nullptr; + + int64_t keyframe_enable_sub_ = 0; + int64_t keyframe_added_sub_ = 0; + int64_t keyframe_removed_sub_ = 0; + int64_t keyframe_time_sub_ = 0; + + int64_t viewer_sub_ = 0; + private slots: void show_buttons_from_keyframe_enable(bool e); diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index b5d40b37f..3af69ca0d 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -31,19 +31,24 @@ #include "common/qtutils.h" #include "core.h" #include "nodeparambutton.h" -#include "node/group/group.h" #include "node/node.h" -#include "node/nodeundo.h" #include "node/project/sequence/sequence.h" #include "nodeparamviewarraywidget.h" #include "nodeparamviewtextedit.h" +#include "oakengine/events.h" +#include "common/nodevaluehandle.h" +#include "common/oakvaluehelper.h" #include "oakengine/node.h" -#include "render/lutlibrary.h" +#include "oakengine/plugin.h" +#include "oakengine/viewer.h" +#include "oakengine/lut.h" +#include "oakengine/undo.h" #include "undo/undostack.h" #include "widget/bezier/bezierwidget.h" #include "widget/colorbutton/colorbutton.h" #include "widget/filefield/filefield.h" #include "widget/filefield/lutfilefield.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/slider/floatslider.h" #include "widget/slider/integerslider.h" #include "widget/slider/rationalslider.h" @@ -53,27 +58,150 @@ namespace olive { +static int64_t rational_to_node_ts(Node *node, const Rational &time) +{ + int num = 0, den = 1; + oakengine_node_frame_time_base(reinterpret_cast(node), + &num, &den); + return core::Timecode::time_to_timestamp(time, Rational(num, den)); +} + +static QVariant GetInputValueAtTime(const NodeInput &input, + const Rational &node_time) +{ + Node *node = input.node(); + if (!node) { + return QVariant(); + } + const NodeValue::Type type = node->get_input_data_type(input.input()); + const QByteArray input_utf8 = input.input().toUtf8(); + const char *input_id = input_utf8.constData(); + const int element = input.element(); + const int64_t time_ts = rational_to_node_ts(node, node_time); + const OakEngineNode *enode = reinterpret_cast(node); + + switch (type) { + case NodeValue::k_int: + case NodeValue::k_float: + case NodeValue::k_boolean: + case NodeValue::k_rational: + case NodeValue::k_color: + case NodeValue::k_vec2: + case NodeValue::k_vec3: + case NodeValue::k_vec4: + case NodeValue::k_combo: { + oak_node_value v; + if (oakengine_node_get_input_at_time(enode, input_id, element, -1, + time_ts, 1, &v) == + OAKENGINE_OK) { + return OakNodeValueToQVariant(v); + } + break; + } + case NodeValue::k_file: + case NodeValue::k_text: + case NodeValue::k_font: + case NodeValue::k_str_combo: { + char buf[4096]; + const int len = oakengine_node_get_input_string_at_time( + enode, input_id, element, time_ts, 0, buf, sizeof(buf)); + if (len >= 0) { + return QString::fromUtf8(buf, len); + } + break; + } + case NodeValue::k_binary: { + const int len = oakengine_node_get_input_binary_at_time( + enode, input_id, element, time_ts, 0, nullptr, 0); + if (len > 0) { + QByteArray bytes(len, '\0'); + oakengine_node_get_input_binary_at_time( + enode, input_id, element, time_ts, 0, bytes.data(), len); + return bytes; + } else if (len == 0) { + return QByteArray(); + } + break; + } + case NodeValue::k_bezier: { + double out[6]; + if (oakengine_node_get_input_bezier_at_time( + enode, input_id, element, time_ts, 0, out) == OAKENGINE_OK) { + return QVariant::fromValue( + Bezier(out[0], out[1], out[2], out[3], out[4], out[5])); + } + break; + } + default: + break; + } + return QVariant(); +} + +static bool ResolveGroupInput(NodeInput *input) +{ + OakEngineNode *node = reinterpret_cast(input->node()); + char input_id[256]; + int element = input->element(); + memcpy(input_id, input->input().toUtf8().constData(), + qMin(sizeof(input_id) - 1, input->input().toUtf8().size())); + input_id[sizeof(input_id) - 1] = '\0'; + if (!oakengine_node_group_get_inner(&node, input_id, sizeof(input_id), + &element)) { + return false; + } + *input = NodeInput(reinterpret_cast(node), + QString::fromUtf8(input_id), element); + return true; +} + NodeParamViewWidgetBridge::NodeParamViewWidgetBridge(NodeInput input, QObject *parent) : QObject(parent) + , bridge_(new EngineEventBridge(this)) { + connect(bridge_, &EngineEventBridge::node_input_value_changed, this, + &NodeParamViewWidgetBridge::input_value_changed); + connect(bridge_, &EngineEventBridge::node_input_property_changed, this, + [this](OakEngineNode *, const QString &input) { + property_changed(input); + }); + connect(bridge_, &EngineEventBridge::node_input_data_type_changed, this, + &NodeParamViewWidgetBridge::input_data_type_changed); + do { input_hierarchy_.append(input); - connect(input.node(), &Node::value_changed, this, - &NodeParamViewWidgetBridge::input_value_changed); - connect(input.node(), &Node::input_property_changed, this, - &NodeParamViewWidgetBridge::property_changed); - connect(input.node(), &Node::input_data_type_changed, this, - &NodeParamViewWidgetBridge::input_data_type_changed); - } while (NodeGroup::get_inner(&input)); + bridge_->subscribe(reinterpret_cast(input.node()), + OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED); + bridge_->subscribe(reinterpret_cast(input.node()), + OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED); + bridge_->subscribe(reinterpret_cast(input.node()), + OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED); + } while (ResolveGroupInput(&input)); + + dragger_ = oakengine_dragger_create( + reinterpret_cast(get_inner_input().node()), + get_inner_input().input().toUtf8().constData(), + get_inner_input().element(), + -1); // track set in process_slider at start time create_widgets(); } +NodeParamViewWidgetBridge::~NodeParamViewWidgetBridge() +{ + // oakengine_dragger_create() ownership is ours (no-op on NULL) + oakengine_dragger_free(dragger_); + // Raw viewer subscription carries `this` as userdata + if (viewer_sub_ > 0) { + oakengine_event_unsubscribe(viewer_sub_); + } +} + int get_slider_count(NodeValue::Type type) { - return NodeValue::get_number_of_keyframe_tracks(type); + return oakengine_node_value_keyframe_track_count(node_value_type_to_c(type)); } namespace @@ -231,7 +359,7 @@ void NodeParamViewWidgetBridge::create_widgets() create_sliders(4, parent); } else { ColorButton *color_button = new ColorButton( - get_inner_input().node()->project()->color_manager(), parent); + oak_color_manager(get_inner_input().node()->project()->color_manager()), parent); widgets_.append(color_button); connect(color_button, &ColorButton::color_changed, this, &NodeParamViewWidgetBridge::widget_callback); @@ -286,8 +414,12 @@ void NodeParamViewWidgetBridge::create_widgets() widgets_.append(button); plugin::PluginNode *plugin_node = dynamic_cast(input.node()); - connect(button, &NodeParamButton::on_pressed, plugin_node, - &plugin::PluginNode::push_button_clicked); + connect(button, &NodeParamButton::on_pressed, this, + [plugin_node](const QString &name) { + oakengine_plugin_node_push_button_clicked( + reinterpret_cast(plugin_node), + name.toUtf8().constData()); + }); } } @@ -312,9 +444,9 @@ void NodeParamViewWidgetBridge::set_input_value(const QVariant &value, int track const NodeInput &input = get_inner_input(); oak_node_value c_value; if (!variant_to_c_value(get_data_type(), value, &c_value)) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); set_input_value_internal(value, track, command, true); - Core::instance()->undo_stack()->push(command, get_command_name()); + oakengine_undo_push(command, get_command_name().toUtf8().constData()); return; } @@ -339,11 +471,21 @@ void NodeParamViewWidgetBridge::set_string_value(const QString &value) } void NodeParamViewWidgetBridge::set_input_value_internal( - const QVariant &value, int track, MultiUndoCommand *command, + const QVariant &value, int track, void *command, bool insert_on_all_tracks_if_no_key) { - Node::set_value_at_time(get_inner_input(), get_current_time_as_node_time(), value, - track, command, insert_on_all_tracks_if_no_key); + const NodeInput &input = get_inner_input(); + olive::Rational t = get_current_time_as_node_time(); + oak_node_value c_value; + if (variant_to_c_value(get_data_type(), value, &c_value)) { + oakengine_undo_command_multi_add_child( + command, + oakengine_node_set_value_at_time_command( + reinterpret_cast(input.node()), + input.input().toUtf8().constData(), input.element(), + t.numerator(), t.denominator(), &c_value, track, + insert_on_all_tracks_if_no_key ? 1 : 0)); + } } void NodeParamViewWidgetBridge::process_slider(NumericSliderBase *slider, @@ -352,23 +494,27 @@ void NodeParamViewWidgetBridge::process_slider(NumericSliderBase *slider, { if (slider->is_dragging()) { // While we're dragging, we block the input's normal signalling and create our own - if (!dragger_.is_started()) { + if (!oakengine_dragger_is_started(dragger_)) { + OakEngineNode *node = reinterpret_cast(get_inner_input().node()); Rational node_time = get_current_time_as_node_time(); + int64_t ts = node_time_to_ts(node, node_time); - dragger_.start(NodeKeyframeTrackReference(get_inner_input(), - slider_track), - node_time); + oakengine_dragger_start(dragger_, ts, slider_track, 0); } - dragger_.drag(value); + oak_node_value c_value; + if (variant_to_c_value(get_data_type(), value, &c_value)) { + oakengine_dragger_drag(dragger_, &c_value); + } - } else if (dragger_.is_started()) { + } else if (oakengine_dragger_is_started(dragger_)) { // We were dragging and just stopped - dragger_.drag(value); + oak_node_value c_value; + if (variant_to_c_value(get_data_type(), value, &c_value)) { + oakengine_dragger_drag(dragger_, &c_value); + } - MultiUndoCommand *command = new MultiUndoCommand(); - dragger_.end(command); - Core::instance()->undo_stack()->push(command, get_command_name()); + oakengine_dragger_end(dragger_, get_command_name().toUtf8().constData()); } else { // No drag was involved, we can just push the value @@ -464,17 +610,22 @@ void NodeParamViewWidgetBridge::widget_callback() Node *n = get_inner_input().node(); n->blockSignals(true); - n->set_input_property(get_inner_input().input(), - QStringLiteral("col_input"), c.color_input()); - n->set_input_property(get_inner_input().input(), - QStringLiteral("col_display"), - c.color_output().display()); - n->set_input_property(get_inner_input().input(), - QStringLiteral("col_view"), - c.color_output().view()); - n->set_input_property(get_inner_input().input(), - QStringLiteral("col_look"), - c.color_output().look()); + oakengine_node_set_input_property_string( + reinterpret_cast(n), + get_inner_input().input().toUtf8().constData(), + "col_input", c.color_input().toUtf8().constData(), 0); + oakengine_node_set_input_property_string( + reinterpret_cast(n), + get_inner_input().input().toUtf8().constData(), + "col_display", c.color_output().display().toUtf8().constData(), 0); + oakengine_node_set_input_property_string( + reinterpret_cast(n), + get_inner_input().input().toUtf8().constData(), + "col_view", c.color_output().view().toUtf8().constData(), 0); + oakengine_node_set_input_property_string( + reinterpret_cast(n), + get_inner_input().input().toUtf8().constData(), + "col_look", c.color_output().look().toUtf8().constData(), 0); n->blockSignals(false); } break; @@ -608,29 +759,29 @@ void NodeParamViewWidgetBridge::update_widget_values() NodeParamViewTextEdit *e = static_cast(widgets_.first()); QByteArray bytes = - get_inner_input().get_value_at_time(node_time).toByteArray(); + GetInputValueAtTime(get_inner_input(), node_time).toByteArray(); e->setTextPreservingCursor(QString::fromUtf8(bytes.toBase64())); break; } case NodeValue::k_int: { static_cast(widgets_.first()) - ->set_value(get_inner_input().get_value_at_time(node_time).toLongLong()); + ->set_value(GetInputValueAtTime(get_inner_input(), node_time).toLongLong()); break; } case NodeValue::k_float: { static_cast(widgets_.first()) - ->set_value(get_inner_input().get_value_at_time(node_time).toDouble()); + ->set_value(GetInputValueAtTime(get_inner_input(), node_time).toDouble()); break; } case NodeValue::k_rational: { static_cast(widgets_.first()) ->set_value( - get_inner_input().get_value_at_time(node_time).value()); + GetInputValueAtTime(get_inner_input(), node_time).value()); break; } case NodeValue::k_vec2: { QVector2D vec2 = - get_inner_input().get_value_at_time(node_time).value(); + GetInputValueAtTime(get_inner_input(), node_time).value(); static_cast(widgets_.at(0)) ->set_value(static_cast(vec2.x())); @@ -640,7 +791,7 @@ void NodeParamViewWidgetBridge::update_widget_values() } case NodeValue::k_vec3: { QVector3D vec3 = - get_inner_input().get_value_at_time(node_time).value(); + GetInputValueAtTime(get_inner_input(), node_time).value(); static_cast(widgets_.at(0)) ->set_value(static_cast(vec3.x())); @@ -652,7 +803,7 @@ void NodeParamViewWidgetBridge::update_widget_values() } case NodeValue::k_vec4: { QVector4D vec4 = - get_inner_input().get_value_at_time(node_time).value(); + GetInputValueAtTime(get_inner_input(), node_time).value(); static_cast(widgets_.at(0)) ->set_value(static_cast(vec4.x())); @@ -666,13 +817,13 @@ void NodeParamViewWidgetBridge::update_widget_values() } case NodeValue::k_file: { FileField *ff = static_cast(widgets_.first()); - ff->set_filename(get_inner_input().get_value_at_time(node_time).toString()); + ff->set_filename(GetInputValueAtTime(get_inner_input(), node_time).toString()); break; } case NodeValue::k_color: { if (get_inner_input().get_property("color_semantic").toString() == QStringLiteral("scalar")) { - Color c = get_inner_input().get_value_at_time(node_time).value(); + Color c = GetInputValueAtTime(get_inner_input(), node_time).value(); static_cast(widgets_.at(0)) ->set_value(static_cast(c.red())); static_cast(widgets_.at(1)) @@ -683,7 +834,7 @@ void NodeParamViewWidgetBridge::update_widget_values() ->set_value(static_cast(c.alpha())); } else { ManagedColor mc = - get_inner_input().get_value_at_time(node_time).value(); + GetInputValueAtTime(get_inner_input(), node_time).value(); mc.set_color_input( get_inner_input().get_property("col_input").toString()); @@ -702,25 +853,25 @@ void NodeParamViewWidgetBridge::update_widget_values() NodeParamViewTextEdit *e = static_cast(widgets_.first()); e->setTextPreservingCursor( - get_inner_input().get_value_at_time(node_time).toString()); + GetInputValueAtTime(get_inner_input(), node_time).toString()); break; } case NodeValue::k_boolean: static_cast(widgets_.first()) - ->setChecked(get_inner_input().get_value_at_time(node_time).toBool()); + ->setChecked(GetInputValueAtTime(get_inner_input(), node_time).toBool()); break; case NodeValue::k_font: { QFontComboBox *fc = static_cast(widgets_.first()); fc->blockSignals(true); fc->setCurrentFont( - get_inner_input().get_value_at_time(node_time).toString()); + GetInputValueAtTime(get_inner_input(), node_time).toString()); fc->blockSignals(false); break; } case NodeValue::k_combo: { QComboBox *cb = static_cast(widgets_.first()); cb->blockSignals(true); - int index = get_inner_input().get_value_at_time(node_time).toInt(); + int index = GetInputValueAtTime(get_inner_input(), node_time).toInt(); for (int i = 0; i < cb->count(); i++) { if (cb->itemData(i).toInt() == index) { cb->setCurrentIndex(i); @@ -733,7 +884,7 @@ void NodeParamViewWidgetBridge::update_widget_values() QComboBox *cb = static_cast(widgets_.first()); cb->blockSignals(true); const QString current = - get_inner_input().get_value_at_time(node_time).toString(); + GetInputValueAtTime(get_inner_input(), node_time).toString(); for (int i = 0; i < cb->count(); ++i) { const QVariant data = cb->itemData(i); if ((data.isValid() && data.toString() == current) || @@ -747,7 +898,7 @@ void NodeParamViewWidgetBridge::update_widget_values() } case NodeValue::k_bezier: { BezierWidget *bw = static_cast(widgets_.first()); - bw->set_value(get_inner_input().get_value_at_time(node_time).value()); + bw->set_value(GetInputValueAtTime(get_inner_input(), node_time).value()); break; } } @@ -780,24 +931,37 @@ void NodeParamViewWidgetBridge::set_timebase(const Rational &timebase) void NodeParamViewWidgetBridge::TimeTargetDisconnectEvent(ViewerOutput *v) { - disconnect(v, &ViewerOutput::playhead_changed, this, - &NodeParamViewWidgetBridge::update_widget_values); + if (viewer_sub_ > 0) { + oakengine_event_unsubscribe(viewer_sub_); + viewer_sub_ = 0; + } } void NodeParamViewWidgetBridge::TimeTargetConnectEvent(ViewerOutput *v) { - connect(v, &ViewerOutput::playhead_changed, this, - &NodeParamViewWidgetBridge::update_widget_values); + viewer_sub_ = oakengine_event_subscribe( + reinterpret_cast(v), + OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED, + [](const oakengine_event *, void *userdata) { + static_cast(userdata) + ->update_widget_values(); + }, + this); } -void NodeParamViewWidgetBridge::input_value_changed(const NodeInput &input, - const TimeRange &range) +void NodeParamViewWidgetBridge::input_value_changed(OakEngineNode *source, + const QString &input, + int element, qint64 in_ts, + qint64 out_ts) { - if (get_time_target() && get_inner_input() == input && !dragger_.is_started() && - range.in() <= get_time_target()->get_playhead() && - range.out() >= get_time_target()->get_playhead()) { - // We'll need to update the widgets because the values have changed on our current time - update_widget_values(); + NodeInput ni(reinterpret_cast(source), input, element); + if (get_time_target() && get_inner_input() == ni && + !oakengine_dragger_is_started(dragger_)) { + int64_t playhead_ts = + node_time_to_ts(source, get_time_target()->get_playhead()); + if (in_ts <= playhead_ts && out_ts >= playhead_ts) { + update_widget_values(); + } } } @@ -821,7 +985,7 @@ void NodeParamViewWidgetBridge::set_property(const QString &key, } else { // set specific track/widget bool ok; int element = key.mid(7).toInt(&ok); - int tracks = NodeValue::get_number_of_keyframe_tracks(data_type); + int tracks = oakengine_node_value_keyframe_track_count(node_value_type_to_c(data_type)); if (ok && element >= 0 && element < tracks) { widgets_.at(element)->setEnabled(e); @@ -915,15 +1079,19 @@ void NodeParamViewWidgetBridge::set_property(const QString &key, break; } } else if (key == QStringLiteral("offset")) { - int tracks = NodeValue::get_number_of_keyframe_tracks(data_type); + const int c_type = node_value_type_to_c(data_type); + int tracks = oakengine_node_value_keyframe_track_count(c_type); - QVector offsets = - NodeValue::split_normal_value_into_track_values(data_type, - value); - - for (int i = 0; i < tracks; i++) { - static_cast(widgets_.at(i)) - ->set_offset(offsets.at(i)); + oak_node_value normal; + QVector track_vals(tracks); + if (QVariantToOakNodeValue(data_type, value, &normal) && + oakengine_node_value_split_to_tracks( + c_type, &normal, track_vals.data(), tracks) == + OAKENGINE_OK) { + for (int i = 0; i < tracks; i++) { + static_cast(widgets_.at(i)) + ->set_offset(track_vals.at(i).f[0]); + } } update_widget_values(); @@ -931,7 +1099,7 @@ void NodeParamViewWidgetBridge::set_property(const QString &key, } else if (key.startsWith(QStringLiteral("color"))) { QColor c(value.toString()); - int tracks = NodeValue::get_number_of_keyframe_tracks(data_type); + int tracks = oakengine_node_value_keyframe_track_count(node_value_type_to_c(data_type)); if (key.size() == 5) { // Set for all tracks @@ -1057,8 +1225,17 @@ void NodeParamViewWidgetBridge::set_property(const QString &key, // Offer the global LUT library directories as sidebar shortcuts in // the browse dialog QList sidebar_urls; - for (const QString &dir : LUTLibrary::get_directories()) { - sidebar_urls.append(QUrl::fromLocalFile(dir)); + { + int dir_count = oakengine_lut_directory_count(); + for (int i = 0; i < dir_count; i++) { + char buf[4096]; + int len = oakengine_lut_directory_at(i, buf, sizeof(buf)); + if (len > 0) { + sidebar_urls.append( + QUrl::fromLocalFile( + QString::fromUtf8(buf, len))); + } + } } if (!sidebar_urls.isEmpty()) { ff->set_sidebar_urls(sidebar_urls); @@ -1077,26 +1254,21 @@ void NodeParamViewWidgetBridge::set_property(const QString &key, } } -void NodeParamViewWidgetBridge::input_data_type_changed(const QString &input, - NodeValue::Type type) +void NodeParamViewWidgetBridge::input_data_type_changed(OakEngineNode *source, + const QString &input) { - if (sender() == get_outer_input().node() && + if (reinterpret_cast(source) == get_outer_input().node() && input == get_outer_input().input()) { - // Delete all widgets qDeleteAll(widgets_); widgets_.clear(); - // Create new widgets create_widgets(); - // Signal that widgets are new emit widgets_recreated(get_outer_input()); } } -void NodeParamViewWidgetBridge::property_changed(const QString &input, - const QString &key, - const QVariant &value) +void NodeParamViewWidgetBridge::property_changed(const QString &input) { bool found = false; diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h index 6631dd8aa..89a64dfb3 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h @@ -23,8 +23,10 @@ #define OAK_NODEPARAMVIEWWIDGETBRIDGE_H #include +#include -#include "node/inputdragger.h" +#include "engineeventbridge.h" +#include "oakengine/node.h" #include "widget/slider/base/numericsliderbase.h" #include "widget/timetarget/timetarget.h" @@ -41,6 +43,7 @@ class NodeParamViewWidgetBridge : public QObject, public TimeTargetObject { Q_OBJECT public: NodeParamViewWidgetBridge(NodeInput input, QObject *parent); + ~NodeParamViewWidgetBridge() override; const QVector &widgets() const { @@ -69,7 +72,7 @@ private: void set_string_value(const QString &value); void set_input_value_internal(const QVariant &value, int track, - MultiUndoCommand *command, + void *command, bool insert_on_all_tracks_if_no_key); void process_slider(NumericSliderBase *slider, int slider_track, @@ -110,19 +113,24 @@ private: QVector widgets_; - NodeInputDragger dragger_; + OakEngineNodeDragger *dragger_ = nullptr; NodeParamViewScrollBlocker scroll_filter_; + EngineEventBridge *bridge_ = nullptr; + + int64_t viewer_sub_ = 0; + private slots: void widget_callback(); - void input_value_changed(const NodeInput &input, const TimeRange &range); + void input_value_changed(OakEngineNode *source, const QString &input, + int element, qint64 in_ts, qint64 out_ts); - void input_data_type_changed(const QString &input, NodeValue::Type type); + void input_data_type_changed(OakEngineNode *source, + const QString &input); - void property_changed(const QString &input, const QString &key, - const QVariant &value); + void property_changed(const QString &input); }; } diff --git a/app/widget/nodetableview/nodetableview.cpp b/app/widget/nodetableview/nodetableview.cpp index 0dd4e74a2..ba385f088 100644 --- a/app/widget/nodetableview/nodetableview.cpp +++ b/app/widget/nodetableview/nodetableview.cpp @@ -24,7 +24,9 @@ #include #include -#include "node/traverser.h" +#include "oakengine/traverse.h" +#include "oakengine/node.h" +#include "node/value.h" namespace olive { @@ -61,41 +63,53 @@ void NodeTableView::set_time(const Rational &time) { last_time_ = time; - NodeTraverser traverser; - for (auto i = top_level_item_map_.constBegin(); i != top_level_item_map_.constEnd(); i++) { Node *node = i.key(); QTreeWidgetItem *item = i.value(); - // Generate a value database for this node at this time - NodeValueDatabase db = - traverser.generate_database(node, TimeRange(time, time)); + OakEngineTraverseDb *db = oakengine_traverse_generate_database( + reinterpret_cast(node), time.numerator(), + time.denominator(), time.numerator(), time.denominator()); + + int input_count = oakengine_traverse_db_input_count(db); // Delete any children of this item that aren't in this database for (int j = 0; j < item->childCount(); j++) { - if (!db.contains( - item->child(j)->data(0, Qt::UserRole).toString())) { + QString child_id = + item->child(j)->data(0, Qt::UserRole).toString(); + bool found = false; + for (int k = 0; k < input_count; k++) { + if (child_id == + QString::fromUtf8( + oakengine_traverse_db_input_id(db, k))) { + found = true; + break; + } + } + if (!found) { delete item->takeChild(j); j--; } } // Update all inputs - for (auto l = db.begin(); l != db.end(); l++) { - const NodeValueTable &table = l.value(); + for (int l = 0; l < input_count; l++) { + const char *input_id_c = oakengine_traverse_db_input_id(db, l); + QString input_id = QString::fromUtf8(input_id_c); - if (!node->has_input_with_id(l.key())) { - // Filters out table entries that aren't inputs (like "global") + if (!node->has_input_with_id(input_id)) { continue; } + int row_count = oakengine_traverse_db_row_count(db, l); + QTreeWidgetItem *input_item = nullptr; for (int j = 0; j < item->childCount(); j++) { QTreeWidgetItem *compare = item->child(j); - if (compare->data(0, Qt::UserRole).toString() == l.key()) { + if (compare->data(0, Qt::UserRole).toString() == input_id) { input_item = compare; break; } @@ -103,64 +117,83 @@ void NodeTableView::set_time(const Rational &time) if (!input_item) { input_item = new QTreeWidgetItem(); - input_item->setText(0, node->get_input_name(l.key())); - input_item->setData(0, Qt::UserRole, l.key()); + input_item->setText(0, node->get_input_name(input_id)); + input_item->setData(0, Qt::UserRole, input_id); input_item->setFirstColumnSpanned(true); item->addChild(input_item); } // Create children if necessary - while (input_item->childCount() < table.count()) { + while (input_item->childCount() < row_count) { input_item->addChild(new QTreeWidgetItem()); } // Remove children if necessary - while (input_item->childCount() > table.count()) { - delete input_item->takeChild(input_item->childCount() - 1); + while (input_item->childCount() > row_count) { + delete input_item->takeChild( + input_item->childCount() - 1); } - for (int j = 0; j < table.count(); j++) { - const NodeValue &value = table.at(table.count() - 1 - j); - - // Create item + for (int j = 0; j < row_count; j++) { + int actual_row = row_count - 1 - j; QTreeWidgetItem *sub_item = input_item->child(j); + int type = + oakengine_traverse_row_type(db, l, actual_row); + // Set data type name - sub_item->setText( - 0, NodeValue::get_pretty_data_type_name(value.type())); + char name_buf[64]; + int len = oakengine_node_value_pretty_type_name(type, name_buf, sizeof(name_buf)); + if (len > 0 && len < sizeof(name_buf)) { + name_buf[len] = '\0'; + } else { + snprintf(name_buf, sizeof(name_buf), "Type %d", type); + } + sub_item->setText(0, QString::fromUtf8(name_buf)); // Determine source + OakEngineNode *source = + oakengine_traverse_row_source(db, l, actual_row); QString source_name; - if (value.source()) { - source_name = value.source()->get_label_and_name(); + if (source) { + char label_buf[256]; + oakengine_node_get_label_and_name( + source, label_buf, sizeof(label_buf)); + source_name = QString(label_buf); } else { source_name = tr("(unknown)"); } sub_item->setText(1, source_name); - switch (value.type()) { + switch (type) { case NodeValue::k_video_params: case NodeValue::k_audio_params: // These types have no string representation break; case NodeValue::k_texture: { - // NodeTraverser puts video params in here - for (int k = 0; k < VideoParams::k_rgba_channel_count; k++) { - this->setItemWidget(sub_item, 2 + k, new QCheckBox()); + for (int k = 0; k < 4; k++) { + this->setItemWidget(sub_item, 2 + k, + new QCheckBox()); } break; } default: { - QVector split_values = value.to_split_value(); - for (int k = 0; k < split_values.size(); k++) { - sub_item->setText(2 + k, NodeValue::value_to_string( - value.type(), - split_values.at(k), true)); + int split_count = + oakengine_traverse_row_split_count(db, l, + actual_row); + for (int k = 0; k < split_count; k++) { + const char *split_str = + oakengine_traverse_row_split_string( + db, l, actual_row, k); + sub_item->setText(2 + k, + QString(split_str)); } } } } } + + oakengine_traverse_db_free(db); } } diff --git a/app/widget/nodetreeview/nodetreeview.cpp b/app/widget/nodetreeview/nodetreeview.cpp index 4243b3962..84ae08a24 100644 --- a/app/widget/nodetreeview/nodetreeview.cpp +++ b/app/widget/nodetreeview/nodetreeview.cpp @@ -23,6 +23,9 @@ #include +#include "common/nodevaluehandle.h" + +#include "oakengine/node.h" namespace olive { @@ -190,7 +193,7 @@ QTreeWidgetItem *NodeTreeView::create_item(QTreeWidgetItem *parent, QString item_name; if (ref.track() == -1 || - NodeValue::get_number_of_keyframe_tracks(ref.input().get_data_type()) == + oakengine_node_value_keyframe_track_count(node_value_type_to_c(ref.input().get_data_type())) == 1 || (ref.input().is_array() && ref.input().element() == -1)) { if (ref.input().element() == -1) { @@ -258,11 +261,11 @@ void NodeTreeView::item_check_state_changed(QTreeWidgetItem *item, int column) if (item->checkState(0) == Qt::Checked) { if (disabled_nodes_.contains(n)) { disabled_nodes_.removeOne(n); - emit node_enable_changed(n, true); + emit node_enable_changed(reinterpret_cast(n), true); } } else if (!disabled_nodes_.contains(n)) { disabled_nodes_.append(n); - emit node_enable_changed(n, false); + emit node_enable_changed(reinterpret_cast(n), false); } break; } diff --git a/app/widget/nodetreeview/nodetreeview.h b/app/widget/nodetreeview/nodetreeview.h index 9d573647d..f33eb5e31 100644 --- a/app/widget/nodetreeview/nodetreeview.h +++ b/app/widget/nodetreeview/nodetreeview.h @@ -25,6 +25,7 @@ #include #include "node/node.h" +#include "oakengine/node.h" namespace olive { @@ -56,11 +57,14 @@ public: show_keyframe_tracks_as_rows_ = e; } -public slots: +public: + // Not a slot: signature uses the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). All connections use new-style member-function syntax. void set_nodes(const QVector &nodes); signals: - void node_enable_changed(Node *n, bool e); + void node_enable_changed(OakEngineNode *n, bool e); void input_enable_changed(const NodeKeyframeTrackReference &ref, bool e); diff --git a/app/widget/nodevaluetree/nodevaluetree.cpp b/app/widget/nodevaluetree/nodevaluetree.cpp index 9485813c1..6c54e9a77 100644 --- a/app/widget/nodevaluetree/nodevaluetree.cpp +++ b/app/widget/nodevaluetree/nodevaluetree.cpp @@ -20,7 +20,10 @@ #include -#include "node/traverser.h" +#include "common/nodevaluehandle.h" +#include "oakengine/traverse.h" +#include "oakengine/node.h" +#include "node/value.h" namespace olive { @@ -47,22 +50,30 @@ void NodeValueTree::set_node(const NodeInput &input, const Rational &time) { clear(); - NodeTraverser traverser; - Node *connected_node = input.get_connected_output(); - NodeValueTable table = - traverser.generate_table(connected_node, TimeRange(time, time)); + OakEngineTraverseDb *table_db = oakengine_traverse_generate_table( + reinterpret_cast(connected_node), + time.numerator(), time.denominator(), time.numerator(), + time.denominator()); - int index = traverser.generate_row_value_element_index( - input.node(), input.input(), input.element(), &table); + int db_index = 0; + int row_count = oakengine_traverse_db_row_count(table_db, db_index); - for (int i = 0; i < table.count(); i++) { - const NodeValue &value = table.at(i); + int index = oakengine_traverse_table_element_index_for_hint( + reinterpret_cast(input.node()), + input.input().toUtf8().constData(), input.element(), table_db); + + for (int i = 0; i < row_count; i++) { QTreeWidgetItem *item = new QTreeWidgetItem(this); - Node::ValueHint hint({ value.type() }, table.count() - 1 - i, - value.tag()); + int type = oakengine_traverse_row_type(table_db, db_index, i); + OakEngineNode *source = oakengine_traverse_row_source(table_db, db_index, + i); + const char *tag = oakengine_traverse_row_tag(table_db, db_index, i); + + Node::ValueHint hint({ static_cast(type) }, + row_count - 1 - i, QString(tag)); QRadioButton *radio = new QRadioButton(this); radio->setProperty("input", QVariant::fromValue(input)); @@ -74,10 +85,29 @@ void NodeValueTree::set_node(const NodeInput &input, const Rational &time) &NodeValueTree::radio_button_checked); setItemWidget(item, 0, radio); - item->setText(1, NodeValue::get_pretty_data_type_name(value.type())); - item->setText(2, NodeValue::value_to_string(value, false)); - item->setText(3, value.source()->get_label_and_name()); + char name_buf[64]; + int len = oakengine_node_value_pretty_type_name(type, name_buf, sizeof(name_buf)); + if (len > 0 && len < sizeof(name_buf)) { + name_buf[len] = '\0'; + } else { + snprintf(name_buf, sizeof(name_buf), "Type %d", type); + } + item->setText(1, QString::fromUtf8(name_buf)); + + const char *vs = oakengine_traverse_row_value_string(table_db, db_index, i); + item->setText(2, vs ? QString(vs) : QString()); + + if (source) { + char label_buf[256]; + oakengine_node_get_label_and_name(source, label_buf, + sizeof(label_buf)); + item->setText(3, QString(label_buf)); + } else { + item->setText(3, QString()); + } } + + oakengine_traverse_db_free(table_db); } void NodeValueTree::changeEvent(QEvent *event) @@ -101,8 +131,17 @@ void NodeValueTree::radio_button_checked(bool e) Node::ValueHint hint = btn->property("hint").value(); NodeInput input = btn->property("input").value(); - input.node()->set_value_hint_for_input(input.input(), hint, - input.element()); + // Map the full hint through the facade: type (single, or -1 to keep + // the input's declared type), index and tag must not be dropped. + int c_type = -1; + if (!hint.types().isEmpty()) { + c_type = node_value_type_to_c(hint.types().first()); + } + oakengine_node_set_value_hint( + reinterpret_cast(input.node()), + input.input().toUtf8().constData(), input.element(), + c_type, hint.index(), + hint.tag().isEmpty() ? nullptr : hint.tag().toUtf8().constData()); } } diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 86d3e6940..2de5d0c87 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -31,9 +31,10 @@ #include "node/audio/volume/volume.h" #include "node/distort/transform/transformdistortnode.h" -#include "node/group/group.h" -#include "node/nodeundo.h" -#include "node/project/serializer/serializer.h" +#include "oakengine/node.h" +#include "oakengine/project.h" +#include "oakengine/serializer.h" +#include "oakengine/undo.h" #include "panel/panelmanager.h" #include "node/traverser.h" #include "ui/icons/icons.h" @@ -46,6 +47,33 @@ namespace olive { +namespace +{ + +QVector node_vector_to_engine(const QVector &nodes) +{ + QVector result; + result.reserve(nodes.size()); + foreach (Node *n, nodes) { + result.append(reinterpret_cast(n)); + } + return result; +} + +QVector> +context_pair_vector_to_engine(const QVector &pairs) +{ + QVector> result; + result.reserve(pairs.size()); + foreach (const Node::ContextPair &p, pairs) { + result.append(qMakePair(reinterpret_cast(p.node), + reinterpret_cast(p.context))); + } + return result; +} + +} // namespace + const double NodeView::k_minimum_scale = 0.1; const int NodeView::k_maximum_contexts = 10; @@ -59,6 +87,7 @@ NodeView::NodeView(QWidget *parent) , scale_(1.0) , dont_emit_selection_signals_(false) , show_in_param_editor_action_(nullptr) + , bridge_(new EngineEventBridge(this)) { setScene(&scene_); set_default_drag_mode(RubberBandDrag); @@ -70,6 +99,9 @@ NodeView::NodeView(QWidget *parent) connect(this, &NodeView::customContextMenuRequested, this, &NodeView::show_context_menu); + connect(bridge_, &EngineEventBridge::node_removed_from_graph, this, + &NodeView::node_removed_from_graph); + connect_selection_changed_signal(); set_flow_direction(NodeViewCommon::k_left_to_right); @@ -159,16 +191,49 @@ void NodeView::clear_graph() void NodeView::delete_selected() { - NodeViewDeleteCommand *command = new NodeViewDeleteCommand(); - - int count = 0; + QVector nodes; + QVector contexts; + QVector edges; foreach (NodeViewContext *ctx, scene_.context_map()) { - count += ctx->delete_selected(command); + ctx->get_selected_for_deletion(nodes, contexts, edges); } - Core::instance()->undo_stack()->push(command, - tr("Deleted %1 Node(s)").arg(count)); + if (nodes.isEmpty() && edges.isEmpty()) { + return; + } + + // Build C arrays for the facade + QVector oak_nodes(nodes.size()); + QVector oak_contexts(nodes.size()); + for (int i = 0; i < nodes.size(); i++) { + oak_nodes[i] = reinterpret_cast(nodes[i]); + oak_contexts[i] = reinterpret_cast(contexts[i]); + } + + QVector edge_outputs(edges.size()); + QVector edge_input_nodes(edges.size()); + QVector edge_input_ids_storage(edges.size()); + QVector edge_input_ids(edges.size()); + QVector edge_input_elements(edges.size()); + for (int i = 0; i < edges.size(); i++) { + edge_outputs[i] = reinterpret_cast(edges[i]->output()); + edge_input_nodes[i] = reinterpret_cast(edges[i]->input().node()); + edge_input_ids_storage[i] = edges[i]->input().input().toUtf8(); + edge_input_ids[i] = edge_input_ids_storage[i].constData(); + edge_input_elements[i] = edges[i]->input().element(); + } + + // ONE undoable command whether this deletes nodes, edges, or both + oakengine_nodes_delete_many( + oak_nodes.isEmpty() ? nullptr : oak_nodes.constData(), + oak_contexts.isEmpty() ? nullptr : oak_contexts.constData(), + nodes.size(), + edge_outputs.isEmpty() ? nullptr : edge_outputs.constData(), + edge_input_nodes.isEmpty() ? nullptr : edge_input_nodes.constData(), + edge_input_ids.isEmpty() ? nullptr : edge_input_ids.constData(), + edge_input_elements.isEmpty() ? nullptr : edge_input_elements.constData(), + edges.size()); } void NodeView::select_all() @@ -199,14 +264,16 @@ void NodeView::deselect_all() connect_selection_changed_signal(); // Just emit all the nodes that are currently selected as no longer selected - emit nodes_deselected(selected_nodes_); + emit nodes_deselected(node_vector_to_engine(selected_nodes_)); selected_nodes_.clear(); - emit node_selection_changed(selected_nodes_); - emit node_selection_changed_with_contexts(QVector()); + emit node_selection_changed(node_vector_to_engine(selected_nodes_)); + emit node_selection_changed_with_contexts( + QVector>()); } -void NodeView::select(const QVector &nodes, - bool center_view_on_item) +void NodeView::select( + const QVector> &nodes, + bool center_view_on_item) { // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. @@ -217,10 +284,12 @@ void NodeView::select(const QVector &nodes, scene_.deselect_all(); - foreach (const Node::ContextPair &p, nodes) { - NodeViewContext *ctx = scene_.context_map().value(p.context); + foreach (const auto &p, nodes) { + Node *node = reinterpret_cast(p.first); + Node *context = reinterpret_cast(p.second); + NodeViewContext *ctx = scene_.context_map().value(context); if (ctx) { - NodeViewItem *item = ctx->get_item_from_map(p.node); + NodeViewItem *item = ctx->get_item_from_map(node); if (item) { item->setSelected(true); } @@ -230,7 +299,7 @@ void NodeView::select(const QVector &nodes, // Center on something if (center_view_on_item && !nodes.isEmpty()) { QMetaObject::invokeMethod(this, "center_on_node", Qt::QueuedConnection, - OLIVE_NS_ARG(Node *, nodes.first().node)); + Q_ARG(OakEngineNode *, nodes.first().first)); } connect_selection_changed_signal(); @@ -247,34 +316,42 @@ void NodeView::copy_selected(bool cut) return; } - QString copy_str; - QXmlStreamWriter writer(©_str); + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_NODES, nullptr, nullptr); - ProjectSerializer::SaveData sdata(ProjectSerializer::k_only_nodes); - sdata.set_only_serialize_nodes_and_resolve_groups(selected_nodes_); - - ProjectSerializer::SerializedProperties properties; + oakengine_clipboard_set_nodes( + cb, + reinterpret_cast( + selected_nodes_.constData()), + selected_nodes_.size()); for (Node *n : selected_nodes_) { NodeViewItem *item = get_assumed_item_for_selected_node(n); if (item) { Node::Position pos = item->get_node_position_data(); - - properties[n][QStringLiteral("x")] = - QString::number(pos.position.x()); - properties[n][QStringLiteral("y")] = - QString::number(pos.position.y()); - properties[n][QStringLiteral("expanded")] = - QString::number(pos.expanded); + oakengine_clipboard_set_property( + cb, reinterpret_cast(n), "x", + QByteArray::number(pos.position.x()).constData()); + oakengine_clipboard_set_property( + cb, reinterpret_cast(n), "y", + QByteArray::number(pos.position.y()).constData()); + oakengine_clipboard_set_property( + cb, reinterpret_cast(n), "expanded", + QByteArray::number(pos.expanded).constData()); } } - sdata.set_properties(properties); + // Two-phase buf/size: query the needed length first — serialized node + // graphs have no size bound, a fixed buffer would truncate large copies. + const int needed = oakengine_clipboard_save_to_xml(cb, nullptr, 0); + if (needed >= 0) { + QByteArray buf(needed + 1, '\0'); + oakengine_clipboard_save_to_xml(cb, buf.data(), buf.size()); + Core::copy_string_to_clipboard(QString::fromUtf8(buf.constData())); + } - ProjectSerializer::save(&writer, sdata); - - Core::copy_string_to_clipboard(copy_str); + oakengine_clipboard_free(cb); if (cut) { delete_selected(); @@ -287,27 +364,48 @@ void NodeView::paste() return; } - ProjectSerializer::Result res = - ProjectSerializer::paste(ProjectSerializer::k_only_nodes); - if (res.get_load_data().nodes.isEmpty()) { + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_NODES, nullptr, nullptr); + int result_code = OAKENGINE_SERIALIZER_NO_DATA; + oakengine_clipboard_paste(cb, OAKENGINE_CLIPBOARD_NODES, nullptr, + &result_code, nullptr, 0); + + if (result_code != OAKENGINE_SERIALIZER_OK) { + oakengine_clipboard_free(cb); return; } + QVector nodes; + const int node_count = oakengine_clipboard_get_loaded_node_count(cb); + nodes.reserve(node_count); + for (int i = 0; i < node_count; i++) { + nodes.append(reinterpret_cast( + oakengine_clipboard_get_loaded_node_at(cb, i))); + } + Node::PositionMap map; - for (auto it = res.get_load_data().properties.cbegin(); - it != res.get_load_data().properties.cend(); it++) { - Node::Position pos; + oakengine_clipboard_foreach_property( + cb, + [](OakEngineNode *node, const char *key, const char *value, + void *userdata) -> int { + auto *m = static_cast(userdata); + Node *n = reinterpret_cast(node); + Node::Position &pos = (*m)[n]; + if (std::strcmp(key, "x") == 0) { + pos.position.setX(QString::fromUtf8(value).toDouble()); + } else if (std::strcmp(key, "y") == 0) { + pos.position.setY(QString::fromUtf8(value).toDouble()); + } else if (std::strcmp(key, "expanded") == 0) { + pos.expanded = QString::fromUtf8(value).toDouble(); + } + return 0; + }, + &map); - const QMap &node_props = it.value(); - pos.position.setX(node_props.value(QStringLiteral("x")).toDouble()); - pos.position.setY(node_props.value(QStringLiteral("y")).toDouble()); - pos.expanded = node_props.value(QStringLiteral("expanded")).toDouble(); + oakengine_clipboard_free(cb); - map.insert(it.key(), pos); - } - - post_paste(res.get_load_data().nodes, map); + post_paste(nodes, map); } void NodeView::duplicate() @@ -323,9 +421,10 @@ void NodeView::duplicate() for (int i = 0; i < selected.size(); i++) { new_nodes[i] = selected.at(i)->copy(); - if (NodeGroup *g = dynamic_cast(selected.at(i))) { - for (auto it = g->get_context_positions().cbegin(); - it != g->get_context_positions().cend(); it++) { + if (oakengine_node_is_group( + reinterpret_cast(selected.at(i)))) { + for (auto it = selected.at(i)->get_context_positions().cbegin(); + it != selected.at(i)->get_context_positions().cend(); it++) { if (!selected.contains(it.key())) { // This should automatically recurse if this is a group inside a group selected.append(it.key()); @@ -353,30 +452,67 @@ void NodeView::duplicate() if (child_index != -1) { Node *child_copy = new_nodes.at(child_index); - copy->set_node_position_in_context(child_copy, it.value()); + oakengine_node_set_context_position( + reinterpret_cast(copy), + reinterpret_cast(child_copy), + it.value().position.x(), it.value().position.y()); } } - if (NodeGroup *src_group = dynamic_cast(og)) { - NodeGroup *dst_group = static_cast(copy); - - for (auto it = src_group->get_input_passthroughs().cbegin(); - it != src_group->get_input_passthroughs().cend(); it++) { - NodeInput input = it->second; - input.set_node( - new_nodes.at(selected.indexOf(input.node()))); - dst_group->add_input_passthrough(input, it->first); + if (oakengine_node_is_group(reinterpret_cast(og))) { + const int pt_count = oakengine_group_input_passthrough_count( + reinterpret_cast(og)); + for (int pt = 0; pt < pt_count; pt++) { + OakEngineNode *inner_node = nullptr; + char inner_input[256]; + int inner_element = 0; + char id[256]; + if (oakengine_group_input_passthrough_at( + reinterpret_cast(og), pt, + id, sizeof(id), &inner_node, inner_input, + sizeof(inner_input), &inner_element) != + OAKENGINE_OK) { + continue; + } + Node *inner = reinterpret_cast(inner_node); + int src_index = selected.indexOf(inner); + if (src_index == -1) { + continue; + } + Node *copy_inner = new_nodes.at(src_index); + char out_id[256]; + oakengine_group_add_input_passthrough( + reinterpret_cast(copy), + reinterpret_cast(copy_inner), + inner_input, inner_element, id, + out_id, sizeof(out_id)); } - dst_group->set_output_passthrough(new_nodes.at( - selected.indexOf(src_group->get_output_passthrough()))); + if (OakEngineNode *output = oakengine_group_get_output_passthrough( + reinterpret_cast(og))) { + int idx = selected.indexOf(reinterpret_cast(output)); + if (idx >= 0 && idx < new_nodes.size()) { + oakengine_group_set_output_passthrough( + reinterpret_cast(copy), + reinterpret_cast( + new_nodes.at(idx))); + } + } } - Node::copy_inputs(selected.at(i), new_nodes.at(i), false); + oakengine_node_copy_inputs( + reinterpret_cast(new_nodes.at(i)), + reinterpret_cast(selected.at(i))); } // Copy connections - Node::copy_dependency_graph(selected, new_nodes, nullptr); + { + QVector sel_arr, new_arr; + for (Node *n : selected) sel_arr.append(reinterpret_cast(n)); + for (Node *n : new_nodes) new_arr.append(reinterpret_cast(n)); + oakengine_node_copy_dependency_graph( + sel_arr.data(), new_arr.data(), sel_arr.size(), nullptr); + } // Set root level context positions and attach to post_paste(new_nodes, map); @@ -385,14 +521,17 @@ void NodeView::duplicate() void NodeView::set_color_label(int index) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); for (Node *node : qAsConst(selected_nodes_)) { - command->add_child(new NodeOverrideColorCommand(node, index)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_set_color_label_command( + reinterpret_cast(node), index)); } - Core::instance()->undo_stack()->push( - command, tr("Set Color of %1 Node(s)").arg(selected_nodes_.size())); + oakengine_undo_push( + command, tr("Set Color of %1 Node(s)").arg(selected_nodes_.size()).toUtf8().constData()); } void NodeView::zoom_in() @@ -412,7 +551,7 @@ void NodeView::keyPressEvent(QKeyEvent *event) case Qt::Key_Right: case Qt::Key_Up: case Qt::Key_Down: { - MultiUndoCommand *pos_command = new MultiUndoCommand(); + void *pos_command = oakengine_undo_command_create_multi(); for (Node *n : qAsConst(selected_nodes_)) { for (Node *context : qAsConst(contexts_)) { if (context->context_contains_node(n)) { @@ -444,13 +583,20 @@ void NodeView::keyPressEvent(QKeyEvent *event) node_movement, scene_.get_flow_direction()); // Move command - pos_command->add_child(new NodeSetPositionCommand( - n, context, old_pos + node_movement)); + Node::Position new_pos = old_pos; + new_pos.position += node_movement; + oakengine_undo_command_multi_add_child( + pos_command, + oakengine_node_set_position_command( + reinterpret_cast(n), + reinterpret_cast(context), + new_pos.position.x(), new_pos.position.y(), + new_pos.expanded ? 1 : 0)); } } } - Core::instance()->undo_stack()->push( - pos_command, tr("Moved %1 Node(s)").arg(selected_nodes_.size())); + oakengine_undo_push( + pos_command, tr("Moved %1 Node(s)").arg(selected_nodes_.size()).toUtf8().constData()); break; } case Qt::Key_Escape: @@ -601,7 +747,7 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) end_edge_drag(); } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); Node *select_context = nullptr; QVector select_nodes; @@ -632,13 +778,15 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) } QPointF current_pos = i->get_node_position(); if (dragging_items_.value(i) != current_pos) { - command->add_child(new NodeSetPositionCommand( - i->get_node(), i->get_context(), current_pos)); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command( + reinterpret_cast(i->get_node()), + reinterpret_cast(i->get_context()), current_pos.x(), + current_pos.y(), 0)); } } - Core::instance()->undo_stack()->push( - command, tr("Moved %1 Node(s)").arg(dragging_items_.size())); + oakengine_undo_push( + command, tr("Moved %1 Node(s)").arg(dragging_items_.size()).toUtf8().constData()); dragging_items_.clear(); @@ -674,8 +822,8 @@ void NodeView::dragEnterEvent(QDragEnterEvent *event) QStringList mime_fmts = event->mimeData()->formats(); - if (mime_fmts.contains(Project::k_item_mime_type)) { - QByteArray model_data = event->mimeData()->data(Project::k_item_mime_type); + if (mime_fmts.contains(QString::fromUtf8(oakengine_project_item_mime_type()))) { + QByteArray model_data = event->mimeData()->data(QString::fromUtf8(oakengine_project_item_mime_type())); QDataStream stream(&model_data, QIODevice::ReadOnly); // Variables to deserialize into @@ -732,11 +880,11 @@ void NodeView::dragMoveEvent(QDragMoveEvent *event) void NodeView::dropEvent(QDropEvent *event) { if (Node *drop_ctx = get_context_at_mouse_pos(event->pos())) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); QVector select_nodes = process_dropping_attached_nodes(command, drop_ctx, event->pos()); - Core::instance()->undo_stack()->push( - command, tr("Dropped %1 Node(s)").arg(select_nodes.size())); + oakengine_undo_push( + command, tr("Dropped %1 Node(s)").arg(select_nodes.size()).toUtf8().constData()); deselect_all(); scene_.context_map().value(drop_ctx)->select(select_nodes); @@ -815,16 +963,17 @@ void NodeView::update_selection_cache() } if (!deselected.isEmpty()) { - emit nodes_deselected(deselected); + emit nodes_deselected(node_vector_to_engine(deselected)); } if (!selected.isEmpty()) { - emit nodes_selected(selected); + emit nodes_selected(node_vector_to_engine(selected)); } if (!dont_emit_selection_signals_) { - emit node_selection_changed(selected_nodes_); - emit node_selection_changed_with_contexts(sel_with_ctx); + emit node_selection_changed(node_vector_to_engine(selected_nodes_)); + emit node_selection_changed_with_contexts( + context_pair_vector_to_engine(sel_with_ctx)); } } @@ -856,7 +1005,8 @@ void NodeView::show_context_menu(const QPoint &pos) if (item_under_cursor && !selected.isEmpty()) { // Grouping if (selected.size() == 1 && - dynamic_cast(selected.first()->get_node())) { + oakengine_node_is_group(reinterpret_cast( + selected.first()->get_node()))) { QAction *ungroup_action = m.addAction(tr("Ungroup")); connect(ungroup_action, &QAction::triggered, this, &NodeView::ungroup_nodes); @@ -947,9 +1097,10 @@ void NodeView::create_node_slot(QAction *action) new_attached.append({ new_item, new_node, QPointF(0, 0) }); - if (NodeGroup *new_group = dynamic_cast(new_node)) { - for (auto it = new_group->get_context_positions().cbegin(); - it != new_group->get_context_positions().cend(); it++) { + if (oakengine_node_is_group( + reinterpret_cast(new_node))) { + for (auto it = new_node->get_context_positions().cbegin(); + it != new_node->get_context_positions().cend(); it++) { new_attached.append({ nullptr, it.key(), QPointF(0, 0) }); } } @@ -992,10 +1143,11 @@ void NodeView::center_on_items_bounding_rect() centerOn(scene_.itemsBoundingRect().center()); } -void NodeView::center_on_node(Node *n) +void NodeView::center_on_node(OakEngineNode *n) { foreach (NodeViewContext *ctx, scene_.context_map()) { - if (NodeViewItem *item = ctx->get_item_from_map(n)) { + if (NodeViewItem *item = ctx->get_item_from_map( + reinterpret_cast(n))) { centerOn(item); break; } @@ -1036,9 +1188,9 @@ void NodeView::move_to_scene_point(const QPointF &pos) centerOn(pos); } -void NodeView::node_removed_from_graph() +void NodeView::node_removed_from_graph(OakEngineNode *source) { - Node *context = static_cast(sender()); + Node *context = reinterpret_cast(source); remove_context(context); @@ -1111,7 +1263,7 @@ void NodeView::process_moving_attached_nodes(const QPoint &pos) } else { // Otherwise, we may have to iterate to find a valid one for (const QString &input : attached_node->inputs()) { - if (input == Node::k_enabled_input) { + if (input == QLatin1String(oakengine_node_enabled_input_id())) { // Ignore enabled input continue; } @@ -1160,7 +1312,7 @@ void NodeView::process_moving_attached_nodes(const QPoint &pos) } QVector -NodeView::process_dropping_attached_nodes(MultiUndoCommand *command, +NodeView::process_dropping_attached_nodes(void *command, Node *select_context, const QPoint &pos) { QVector select_nodes; @@ -1180,41 +1332,44 @@ NodeView::process_dropping_attached_nodes(MultiUndoCommand *command, } { - MultiUndoCommand *add_command = new MultiUndoCommand(); + void *add_command = oakengine_undo_command_create_multi(); foreach (const AttachedItem &ai, attached) { // Add node to the same graph that the context is in if (ai.node->parent() != select_context->parent()) { - add_command->add_child( - new NodeAddCommand(select_context->parent(), ai.node)); + oakengine_undo_command_multi_add_child(add_command, + oakengine_node_add_to_project_command( + reinterpret_cast(select_context->parent()), + reinterpret_cast(ai.node))); if (ai.node->is_item() && !ai.node->folder()) { - add_command->add_child(new FolderAddChild( - select_context->parent()->root(), ai.node)); + oakengine_folder_add_child( + reinterpret_cast(select_context->parent()->root()), + reinterpret_cast(ai.node)); } } // Add node to the context if (ai.item) { select_nodes.append(ai.node); - add_command->add_child(new NodeSetPositionCommand( - ai.node, select_context, - scene_.context_map() + oakengine_undo_command_multi_add_child(add_command, oakengine_node_set_position_command(reinterpret_cast(ai.node), reinterpret_cast(select_context), scene_.context_map() .value(select_context) - ->map_scene_pos_to_node_pos_in_context(ai.item->pos()))); + ->map_scene_pos_to_node_pos_in_context(ai.item->pos()).x(), scene_.context_map() + .value(select_context) + ->map_scene_pos_to_node_pos_in_context(ai.item->pos()).y(), 0)); } } - if (add_command->child_count()) { - add_command->redo_now(); - command->add_child(add_command); + if (oakengine_undo_command_multi_child_count(add_command) > 0) { + oakengine_undo_command_redo_now(add_command); + oakengine_undo_command_multi_add_child(command, add_command); } else { - delete add_command; + oakengine_undo_command_free(add_command); } } { - // Dropped attached item onto an edge, connect it between them - MultiUndoCommand *drop_edge_command = new MultiUndoCommand(); + // Dropped attached item onto an edge, connect it between them as one + // undoable child of the parent command. if (attached.size() == 1) { Node *dropping_node = nullptr; @@ -1226,25 +1381,35 @@ NodeView::process_dropping_attached_nodes(MultiUndoCommand *command, } if (dropping_node && drop_edge_) { - // Remove old edge - drop_edge_command->add_child(new NodeEdgeRemoveCommand( - drop_edge_->output(), drop_edge_->input())); + void *edge_command = oakengine_undo_command_create_multi(); - // Place new edges - drop_edge_command->add_child( - new NodeEdgeAddCommand(drop_edge_->output(), drop_input_)); - drop_edge_command->add_child( - new NodeEdgeAddCommand(dropping_node, drop_edge_->input())); + oakengine_undo_command_multi_add_child( + edge_command, + oakengine_node_disconnect_command( + reinterpret_cast(drop_edge_->input().node()), + drop_edge_->input().input().toUtf8().constData(), + drop_edge_->input().element())); + oakengine_undo_command_multi_add_child( + edge_command, + oakengine_node_connect_command( + reinterpret_cast(drop_edge_->output()), + reinterpret_cast(drop_input_.node()), + drop_input_.input().toUtf8().constData(), + drop_input_.element())); + oakengine_undo_command_multi_add_child( + edge_command, + oakengine_node_connect_command( + reinterpret_cast(dropping_node), + reinterpret_cast(drop_edge_->input().node()), + drop_edge_->input().input().toUtf8().constData(), + drop_edge_->input().element())); + + oakengine_undo_command_redo_now(edge_command); + oakengine_undo_command_multi_add_child(command, edge_command); } drop_edge_ = nullptr; } - if (drop_edge_command->child_count()) { - drop_edge_command->redo_now(); - command->add_child(drop_edge_command); - } else { - delete drop_edge_command; - } } detach_items_from_cursor(false); @@ -1518,28 +1683,29 @@ void NodeView::group_nodes() avg_pos /= items.size(); // Create group - NodeGroup *group = new NodeGroup(); + Node *group = reinterpret_cast(oakengine_node_group_create()); // Add group to graph and context - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); // Add nodes to group Node *output_passthrough = nullptr; QVector nodes_to_group = selected_nodes_; deselect_all(); foreach (Node *n, nodes_to_group) { - command->add_child( - new NodeRemovePositionFromContextCommand(n, context)); - command->add_child(new NodeSetPositionCommand( - n, group, context->get_node_position_data_in_context(n))); + oakengine_undo_command_multi_add_child(command, oakengine_node_remove_position_command(reinterpret_cast(n), reinterpret_cast(context))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(n), reinterpret_cast(group), context->get_node_position_data_in_context(n).position.x(), context->get_node_position_data_in_context(n).position.y(), context->get_node_position_data_in_context(n).expanded ? 1 : 0)); for (auto it = n->inputs().cbegin(); it != n->inputs().cend(); it++) { NodeInput input(n, *it, -1); if (!input.is_connected() || !nodes_to_group.contains(input.get_connected_output())) { - command->add_child( - new NodeGroupAddInputPassthrough(group, input)); + oakengine_undo_command_multi_add_child(command, (void *)(oakengine_group_add_input_passthrough_command( + reinterpret_cast(group), + reinterpret_cast(input.node()), + input.input().toUtf8().constData(), input.element(), + nullptr))); } } @@ -1556,17 +1722,21 @@ void NodeView::group_nodes() } // Set output passthrough - command->add_child( - new NodeGroupSetOutputPassthrough(group, output_passthrough)); + oakengine_undo_command_multi_add_child(command, (void *)(oakengine_group_set_output_passthrough_command( + reinterpret_cast(group), + reinterpret_cast(output_passthrough)))); // Add group to graph - command->add_child(new NodeAddCommand(context->parent(), group)); - command->add_child(new NodeSetPositionCommand(group, context, avg_pos)); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(context->parent()), + reinterpret_cast(group))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(group), reinterpret_cast(context), avg_pos.x(), avg_pos.y(), 0)); // Do command Core::instance()->label_nodes({ group }, command); - Core::instance()->undo_stack()->push(command, tr("Grouped Nodes")); + oakengine_undo_push(command, tr("Grouped Nodes").toUtf8().constData()); } void NodeView::ungroup_nodes() @@ -1577,9 +1747,11 @@ void NodeView::ungroup_nodes() return; } - NodeGroup *group = nullptr; + Node *group = nullptr; foreach (NodeViewItem *i, items) { - if ((group = dynamic_cast(i->get_node()))) { + if (oakengine_node_is_group(reinterpret_cast( + i->get_node()))) { + group = i->get_node(); group_item = i; break; } @@ -1589,30 +1761,27 @@ void NodeView::ungroup_nodes() return; } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); Node *context = group_item->get_context(); - command->add_child( - new NodeRemovePositionFromContextCommand(group, context)); - command->add_child(new NodeRemoveAndDisconnectCommand(group)); + oakengine_undo_command_multi_add_child(command, oakengine_node_remove_position_command(reinterpret_cast(group), reinterpret_cast(context))); + oakengine_undo_command_multi_add_child(command, oakengine_node_remove_and_disconnect_command(reinterpret_cast(group))); for (auto it = group->get_context_positions().cbegin(); it != group->get_context_positions().cend(); it++) { - command->add_child( - new NodeRemovePositionFromContextCommand(it.key(), group)); - command->add_child(new NodeSetPositionCommand( - it.key(), context, group->get_node_position_data_in_context(it.key()))); + oakengine_undo_command_multi_add_child(command, oakengine_node_remove_position_command(reinterpret_cast(it.key()), reinterpret_cast(group))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(it.key()), reinterpret_cast(context), group->get_node_position_data_in_context(it.key()).position.x(), group->get_node_position_data_in_context(it.key()).position.y(), group->get_node_position_data_in_context(it.key()).expanded ? 1 : 0)); } - Core::instance()->undo_stack()->push(command, tr("Ungrouped Nodes")); + oakengine_undo_push(command, tr("Ungrouped Nodes").toUtf8().constData()); } void NodeView::show_node_properties() { Node *first_node = selected_nodes_.first(); - if (NodeGroup *group = dynamic_cast(first_node)) { + if (oakengine_node_is_group(reinterpret_cast(first_node))) { if (!overlay_view_) { overlay_view_ = new NodeView(this); overlay_view_->show(); @@ -1647,19 +1816,20 @@ void NodeView::show_node_properties() overlay_close_btn->setStyleSheet( QStringLiteral("background: transparent; border: none;")); } - overlay_view_->set_contexts({ group }); + overlay_view_->set_contexts({ first_node }); resize_overlay(); QMetaObject::invokeMethod(overlay_view_, &NodeView::center_on_items_bounding_rect, Qt::QueuedConnection); overlay_view_->setFocus(); - emit nodes_deselected(selected_nodes_); - emit node_selection_changed(QVector()); - emit node_selection_changed_with_contexts(QVector()); + emit nodes_deselected(node_vector_to_engine(selected_nodes_)); + emit node_selection_changed(QVector()); + emit node_selection_changed_with_contexts( + QVector>()); overlay_view_->select_all(); - emit node_group_opened(group); + emit node_group_opened(reinterpret_cast(first_node)); } else { label_selected_nodes(); } @@ -1701,7 +1871,8 @@ void NodeView::show_selected_node_in_param_editor() } } - emit node_selection_changed_with_contexts(selection_with_contexts); + emit node_selection_changed_with_contexts( + context_pair_vector_to_engine(selection_with_contexts)); } void NodeView::label_selected_nodes() @@ -1752,14 +1923,14 @@ void NodeView::add_context(Node *n) connect(ctx, &NodeViewContext::item_about_to_be_deleted, this, &NodeView::item_about_to_be_deleted); - connect(n, &Node::removed_from_graph, this, &NodeView::node_removed_from_graph); + removed_from_graph_subs_[n] = bridge_->subscribe( + reinterpret_cast(n), OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH); } void NodeView::remove_context(Node *n) { scene_.remove_context(n); - disconnect(n, &Node::removed_from_graph, this, - &NodeView::node_removed_from_graph); + bridge_->unsubscribe(removed_from_graph_subs_.take(n)); } bool NodeView::is_item_attached_to_cursor(NodeViewItem *item) const @@ -1788,7 +1959,7 @@ void NodeView::collapse_item(NodeViewItem *item) void NodeView::end_edge_drag(bool cancel) { // Check if the edge was reconnected to the same place as before - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); bool reconnected_to_itself = false; @@ -1799,8 +1970,12 @@ void NodeView::end_edge_drag(bool cancel) reconnected_to_itself = true; } else { // We are moving (or removing) an existing edge - command->add_child(new NodeEdgeRemoveCommand( - create_edge_->output(), create_edge_->input())); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_disconnect_command( + reinterpret_cast(create_edge_->input().node()), + create_edge_->input().input().toUtf8().constData(), + create_edge_->input().element())); } } } else { @@ -1827,15 +2002,32 @@ void NodeView::end_edge_drag(bool cancel) if (!reconnected_to_itself) { Node *creating_output = create_edge_output_item_->get_node(); - while (NodeGroup *output_group = - dynamic_cast(creating_output)) { - creating_output = output_group->get_output_passthrough(); + while (oakengine_node_is_group( + reinterpret_cast(creating_output))) { + OakEngineNode *out = oakengine_group_get_output_passthrough( + reinterpret_cast(creating_output)); + if (!out) { + break; + } + creating_output = reinterpret_cast(out); } - while (NodeGroup *input_group = - dynamic_cast(creating_input.node())) { - creating_input = - input_group->get_input_from_id(creating_input.input()); + while (oakengine_node_is_group(reinterpret_cast( + creating_input.node()))) { + OakEngineNode *inner_node = nullptr; + char inner_input[256]; + int inner_element = 0; + if (oakengine_group_get_passthrough_from_id( + reinterpret_cast( + creating_input.node()), + creating_input.input().toUtf8().constData(), + &inner_node, inner_input, sizeof(inner_input), + &inner_element) != OAKENGINE_OK) { + break; + } + creating_input = NodeInput( + reinterpret_cast(inner_node), + QString::fromUtf8(inner_input), inner_element); } if (creating_input.is_connected()) { @@ -1862,33 +2054,52 @@ void NodeView::end_edge_drag(bool cancel) } if (!cancel) { - command->add_child(new NodeEdgeRemoveCommand( - existing_edge_to_remove.first, - existing_edge_to_remove.second)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_disconnect_command( + reinterpret_cast(existing_edge_to_remove.second.node()), + existing_edge_to_remove.second.input().toUtf8().constData(), + existing_edge_to_remove.second.element())); } } if (!cancel) { - command->add_child(new NodeEdgeAddCommand(creating_output, - creating_input)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(creating_output), + reinterpret_cast(creating_input.node()), + creating_input.input().toUtf8().constData(), + creating_input.element())); - command_name = Node::get_connect_command_string( - creating_output, creating_input); + { + char cmd_buf[256]; + oakengine_node_connect_command_string( + reinterpret_cast(creating_output), + reinterpret_cast(creating_input.node()), + creating_input.input().toUtf8().constData(), + creating_input.element(), cmd_buf, sizeof(cmd_buf)); + command_name = QString::fromUtf8(cmd_buf); + } // If the output is not in the input's context, add it now. We check the item rather than // the node itself, because sometimes a node may not be in the context but another node // representing it will be (e.g. groups) - if (!scene_.context_map() - .value(create_edge_input_item_->get_context()) - ->get_item_from_map(creating_output)) { - command->add_child(new NodeSetPositionCommand( - creating_output, - create_edge_input_item_->get_context(), - scene_.context_map() - .value(create_edge_input_item_->get_context()) - ->map_scene_pos_to_node_pos_in_context( - create_edge_output_item_->scenePos()))); - } + if (!scene_.context_map() + .value(create_edge_input_item_->get_context()) + ->get_item_from_map(creating_output)) { + QPointF new_pos = scene_.context_map() + .value(create_edge_input_item_->get_context()) + ->map_scene_pos_to_node_pos_in_context( + create_edge_output_item_->scenePos()); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_set_position_command( + reinterpret_cast(creating_output), + reinterpret_cast( + create_edge_input_item_->get_context()), + new_pos.x(), new_pos.y(), 0)); + } } } } @@ -1905,9 +2116,10 @@ void NodeView::end_edge_drag(bool cancel) } create_edge_expanded_items_.clear(); - Core::instance()->undo_stack()->push(command, command_name); + oakengine_undo_push(command, command_name.toUtf8().constData()); } + void NodeView::post_paste(const QVector &new_nodes, const Node::PositionMap &map) { diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 052665915..95e7dcd52 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -23,10 +23,11 @@ #define OAK_NODEVIEW_H #include +#include #include #include "core.h" -#include "node/group/group.h" +#include "engineeventbridge.h" #include "nodeviewedge.h" #include "nodeviewcontext.h" #include "nodeviewminimap.h" @@ -78,8 +79,9 @@ public: void select_all(); void deselect_all(); - void select(const QVector &nodes, - bool center_view_on_item); + void select( + const QVector> &nodes, + bool center_view_on_item); void copy_selected(bool cut); void paste(); @@ -112,20 +114,20 @@ public slots: void center_on_items_bounding_rect(); - void center_on_node(olive::Node *n); + void center_on_node(OakEngineNode *n); void label_selected_nodes(); signals: - void nodes_selected(const QVector &nodes); + void nodes_selected(const QVector &nodes); - void nodes_deselected(const QVector &nodes); + void nodes_deselected(const QVector &nodes); - void node_selection_changed(const QVector &nodes); - void - node_selection_changed_with_contexts(const QVector &nodes); + void node_selection_changed(const QVector &nodes); + void node_selection_changed_with_contexts( + const QVector> &nodes); - void node_group_opened(NodeGroup *group); + void node_group_opened(OakEngineNode *group); void node_group_closed(); void esc_pressed(); @@ -161,7 +163,7 @@ private: void move_attached_nodes_to_cursor(const QPoint &p); void process_moving_attached_nodes(const QPoint &pos); - QVector process_dropping_attached_nodes(MultiUndoCommand *command, + QVector process_dropping_attached_nodes(void *command, Node *select_context, const QPoint &pos); Node *get_context_at_mouse_pos(const QPoint &p); @@ -241,6 +243,10 @@ private: bool dont_emit_selection_signals_; + EngineEventBridge *bridge_ = nullptr; + + QHash removed_from_graph_subs_; + QAction *show_in_param_editor_action_; static const double k_minimum_scale; @@ -281,7 +287,7 @@ private slots: void move_to_scene_point(const QPointF &pos); - void node_removed_from_graph(); + void node_removed_from_graph(OakEngineNode *source); void group_nodes(); diff --git a/app/widget/nodeview/nodeviewcontext.cpp b/app/widget/nodeview/nodeviewcontext.cpp index 4175a3c2a..e3d73d3f5 100644 --- a/app/widget/nodeview/nodeviewcontext.cpp +++ b/app/widget/nodeview/nodeviewcontext.cpp @@ -19,6 +19,8 @@ #include "nodeviewcontext.h" #include + +#include "oakengine/node.h" #include #include #include @@ -27,13 +29,13 @@ #include "core.h" #include "node/block/block.h" -#include "node/group/group.h" #include "node/output/track/track.h" #include "node/project.h" #include "node/project/sequence/sequence.h" #include "nodeviewitem.h" #include "ui/colorcoding.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -42,18 +44,31 @@ namespace olive NodeViewContext::NodeViewContext(Node *context, QGraphicsItem *item) : super(item) , context_(context) + , bridge_(new EngineEventBridge(this)) { Block *block = dynamic_cast(context_); if (block && block->track() && block->track()->sequence()) { - Rational timebase = block->track() - ->sequence() - ->get_video_params() + Rational timebase = viewer_output_video_params(block->track()->sequence()) .frame_rate_as_time_base(); + QString type_label; + switch (block->track()->type()) { + case Track::k_video: + type_label = QCoreApplication::translate("NodeViewContext", "V"); + break; + case Track::k_audio: + type_label = QCoreApplication::translate("NodeViewContext", "A"); + break; + case Track::k_subtitle: + type_label = QCoreApplication::translate("NodeViewContext", "S"); + break; + default: + break; + } + lbl_ = QCoreApplication::translate("NodeViewContext", "%1 [%2] :: %3 - %4") .arg(block->get_label_and_name(), - Track::Reference::type_to_translated_string( - block->track()->type()), + type_label, QString::fromStdString(Timecode::time_to_timecode( block->in(), timebase, Core::instance()->get_timecode_display())), @@ -64,17 +79,58 @@ NodeViewContext::NodeViewContext(Node *context, QGraphicsItem *item) lbl_ = context_->get_label_and_name(); } + connect(bridge_, &EngineEventBridge::node_node_added_to_context, this, + [this](OakEngineNode *source, OakEngineNode *node) { + Node *src = reinterpret_cast(source); + if (src == context_) { + add_child(reinterpret_cast(node)); + } else { + group_added_node(reinterpret_cast(node), src); + } + }); + connect(bridge_, &EngineEventBridge::node_node_removed_from_context, this, + [this](OakEngineNode *source, OakEngineNode *node) { + Node *src = reinterpret_cast(source); + if (src == context_) { + remove_child(reinterpret_cast(node)); + } else { + group_removed_node(reinterpret_cast(node), src); + } + }); + connect(bridge_, &EngineEventBridge::node_context_position_changed, this, + [this](OakEngineNode *, OakEngineNode *node, double x, double y) { + set_child_position(reinterpret_cast(node), + QPointF(x, y)); + }); + connect(bridge_, &EngineEventBridge::node_input_connected, this, + [this](OakEngineNode *source, OakEngineNode *output, + const QString &input, int element) { + child_input_connected(reinterpret_cast(output), + NodeInput(reinterpret_cast(source), input, + element)); + }); + connect(bridge_, &EngineEventBridge::node_input_disconnected, this, + [this](OakEngineNode *source, OakEngineNode *output, + const QString &input, int element) { + child_input_disconnected(reinterpret_cast(output), + NodeInput(reinterpret_cast(source), input, + element)); + }); + + node_subs_[context_].append(bridge_->subscribe( + reinterpret_cast(context_), + OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT)); + node_subs_[context_].append(bridge_->subscribe( + reinterpret_cast(context_), + OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED)); + node_subs_[context_].append(bridge_->subscribe( + reinterpret_cast(context_), + OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT)); + const Node::PositionMap &map = context_->get_context_positions(); for (auto it = map.cbegin(); it != map.cend(); it++) { add_child(it.key()); } - - connect(context_, &Node::node_added_to_context, this, - &NodeViewContext::add_child, Qt::DirectConnection); - connect(context_, &Node::node_position_in_context_changed, this, - &NodeViewContext::set_child_position, Qt::DirectConnection); - connect(context_, &Node::node_removed_from_context, this, - &NodeViewContext::remove_child, Qt::DirectConnection); } NodeViewContext::~NodeViewContext() @@ -95,17 +151,19 @@ void NodeViewContext::add_child(Node *node) add_node_internal(node, item); - if (NodeGroup *group = dynamic_cast(node)) { - for (auto it = group->get_context_positions().cbegin(); - it != group->get_context_positions().cend(); it++) { + if (oakengine_node_is_group(reinterpret_cast(node))) { + for (auto it = node->get_context_positions().cbegin(); + it != node->get_context_positions().cend(); it++) { // Use this item as the representative for all of these nodes too add_node_internal(it.key(), item); } - connect(group, &NodeGroup::node_added_to_context, this, - &NodeViewContext::group_added_node); - connect(group, &NodeGroup::node_removed_from_context, this, - &NodeViewContext::group_removed_node); + node_subs_[node].append(bridge_->subscribe( + reinterpret_cast(node), + OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT)); + node_subs_[node].append(bridge_->subscribe( + reinterpret_cast(node), + OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT)); } update_rect(); @@ -118,16 +176,8 @@ void NodeViewContext::set_child_position(Node *node, const QPointF &pos) void NodeViewContext::remove_child(Node *node) { - disconnect(node, &Node::input_connected, this, - &NodeViewContext::child_input_connected); - disconnect(node, &Node::input_disconnected, this, - &NodeViewContext::child_input_disconnected); - - if (NodeGroup *group = dynamic_cast(node)) { - disconnect(group, &NodeGroup::node_added_to_context, this, - &NodeViewContext::group_added_node); - disconnect(group, &NodeGroup::node_removed_from_context, this, - &NodeViewContext::group_removed_node); + foreach (int64_t id, node_subs_.take(node)) { + bridge_->unsubscribe(id); } NodeViewItem *item = item_map_.take(node); @@ -151,7 +201,8 @@ void NodeViewContext::remove_child(Node *node) // Check if this item is specifically for this node and the node is a group. If so, remove it for // all other entries in the map. if (item->get_node() == node) { - if (dynamic_cast(item->get_node())) { + if (oakengine_node_is_group(reinterpret_cast( + item->get_node()))) { for (auto it = item_map_.begin(); it != item_map_.end();) { if (it.value() == item) { it = item_map_.erase(it); @@ -233,26 +284,24 @@ void NodeViewContext::set_curved_edges(bool e) } } -int NodeViewContext::delete_selected(NodeViewDeleteCommand *command) +void NodeViewContext::get_selected_for_deletion(QVector &nodes, + QVector &contexts, + QVector &edges) const { - int count = 0; - - // Delete any selected edges + // Collect any selected edges foreach (NodeViewEdge *edge, edges_) { if (edge->isSelected()) { - command->add_edge(edge->output(), edge->input()); + edges.append(edge); } } - // Delete any selected nodes + // Collect any selected nodes foreach (NodeViewItem *node, item_map_) { if (node->isSelected()) { - command->add_node(node->get_node(), context_); - count++; + nodes.append(node->get_node()); + contexts.append(context_); } } - - return count; } void NodeViewContext::select(const QVector &nodes) @@ -346,10 +395,12 @@ void NodeViewContext::mousePressEvent(QGraphicsSceneMouseEvent *event) void NodeViewContext::add_node_internal(Node *node, NodeViewItem *item) { - connect(node, &Node::input_connected, this, - &NodeViewContext::child_input_connected); - connect(node, &Node::input_disconnected, this, - &NodeViewContext::child_input_disconnected); + node_subs_[node].append(bridge_->subscribe( + reinterpret_cast(node), + OAKENGINE_EVENT_NODE_INPUT_CONNECTED)); + node_subs_[node].append(bridge_->subscribe( + reinterpret_cast(node), + OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED)); item_map_.insert(node, item); @@ -393,17 +444,13 @@ void NodeViewContext::add_edge_internal(Node *output, const NodeInput &input, edges_.append(edge_ui); } -void NodeViewContext::group_added_node(Node *node) +void NodeViewContext::group_added_node(Node *node, Node *group) { - NodeGroup *group = static_cast(sender()); - add_node_internal(node, item_map_.value(group)); } -void NodeViewContext::group_removed_node(Node *node) +void NodeViewContext::group_removed_node(Node *node, Node *group) { - NodeGroup *group = static_cast(sender()); - if (item_map_.value(node) == item_map_.value(group)) { item_map_.remove(node); } diff --git a/app/widget/nodeview/nodeviewcontext.h b/app/widget/nodeview/nodeviewcontext.h index f62ef6ff4..b2c0069cb 100644 --- a/app/widget/nodeview/nodeviewcontext.h +++ b/app/widget/nodeview/nodeviewcontext.h @@ -21,9 +21,10 @@ #include #include +#include +#include "engineeventbridge.h" #include "node/node.h" -#include "node/nodeundo.h" #include "nodeviewcommon.h" #include "nodeviewedge.h" @@ -48,7 +49,9 @@ public: void set_curved_edges(bool e); - int delete_selected(NodeViewDeleteCommand *command); + void get_selected_for_deletion(QVector &nodes, + QVector &contexts, + QVector &edges) const; void select(const QVector &nodes); @@ -65,7 +68,11 @@ public: const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; -public slots: +public: + // Not slots: signatures use the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). They are invoked from lambdas / directly, never as connect() + // targets. void add_child(Node *node); void set_child_position(Node *node, const QPointF &pos); @@ -105,10 +112,16 @@ private: QVector edges_; -private slots: - void group_added_node(Node *node); + EngineEventBridge *bridge_ = nullptr; - void group_removed_node(Node *node); + QHash> node_subs_; + +private: + // Ordinary member functions (NOT slots): signatures use Node*, which must + // not be exposed to MOC. Invoked from lambdas only. + void group_added_node(Node *node, Node *group); + + void group_removed_node(Node *node, Node *group); }; } diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 320e0469b..a3b586db3 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -22,15 +22,16 @@ #include "nodeviewitem.h" #include + +#include "oakengine/node.h" #include #include #include #include #include "common/qtutils.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" -#include "node/nodeundo.h" #include "node/value.h" #include "pluginSupport/oliveplugininstance.h" #include "nodeview.h" @@ -69,17 +70,23 @@ NodeViewItem::NodeViewItem(Node *node, const QString &input, int element, input_connector_ = new NodeViewItemConnector(false, this); output_connector_ = new NodeViewItemConnector(true, this); - connect(node_, &Node::label_changed, this, + bridge_ = new EngineEventBridge(this); + bridge_->subscribe(reinterpret_cast(node_), OAKENGINE_EVENT_NODE_LABEL_CHANGED); + bridge_->subscribe(reinterpret_cast(node_), OAKENGINE_EVENT_NODE_COLOR_CHANGED); + bridge_->subscribe(reinterpret_cast(node_), OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED); + connect(bridge_, &EngineEventBridge::node_label_changed, this, &NodeViewItem::node_appearance_changed); - connect(node_, &Node::color_changed, this, + connect(bridge_, &EngineEventBridge::node_color_changed, this, &NodeViewItem::node_appearance_changed); - connect(node_, &Node::message_count_changed, this, + connect(bridge_, &EngineEventBridge::node_message_count_changed, this, &NodeViewItem::node_appearance_changed); if (is_output_item()) { - connect(node_, &Node::input_added, this, + bridge_->subscribe(reinterpret_cast(node_), OAKENGINE_EVENT_NODE_INPUT_ADDED); + bridge_->subscribe(reinterpret_cast(node_), OAKENGINE_EVENT_NODE_INPUT_REMOVED); + connect(bridge_, &EngineEventBridge::node_input_added, this, &NodeViewItem::repopulate_inputs); - connect(node_, &Node::input_removed, this, + connect(bridge_, &EngineEventBridge::node_input_removed, this, &NodeViewItem::repopulate_inputs); repopulate_inputs(); @@ -94,10 +101,11 @@ NodeViewItem::NodeViewItem(Node *node, const QString &input, int element, } else { output_connector_->setVisible(false); - connect(node_, &Node::input_array_size_changed, this, - &NodeViewItem::input_array_size_changed); - connect(node_, &Node::input_array_size_changed, this, - &NodeViewItem::input_array_size_changed); + bridge_->subscribe(reinterpret_cast(node_), OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED); + connect(bridge_, &EngineEventBridge::node_input_array_size_changed, this, + [this](OakEngineNode *, const QString &input, int, int) { + input_array_size_changed(input); + }); } // This should be set during runtime, but just in case here's a default fallback @@ -875,12 +883,18 @@ void NodeViewItem::set_highlighted(bool e) NodeViewItem *NodeViewItem::get_item_for_input(NodeInput input) { - if (NodeGroup *group = dynamic_cast(node_)) { - if (input.node() != group) { + if (oakengine_node_is_group(reinterpret_cast(node_))) { + if (input.node() != node_) { // Translate input to group input - QString id = group->get_id_of_passthrough(input); - input.set_node(group); - input.set_input(id); + char id[256]; + if (oakengine_group_get_id_of_passthrough( + reinterpret_cast(node_), + reinterpret_cast(input.node()), + input.input().toUtf8().constData(), input.element(), + id, sizeof(id)) > 0) { + input.set_node(node_); + input.set_input(QString::fromUtf8(id)); + } } } diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 93d4f13ac..f6fbe54e7 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -30,6 +30,7 @@ #include "node/node.h" #include "nodeviewcommon.h" #include "nodeviewitemconnector.h" +#include "engineeventbridge.h" namespace olive { @@ -233,6 +234,8 @@ private: bool label_as_output_; + EngineEventBridge *bridge_ = nullptr; + private slots: void node_appearance_changed(); diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 297f16447..fa9c7b4e7 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -63,10 +63,14 @@ public: return curved_edges_; } -public slots: +public: + // Not slots: signatures use the engine C++ type Node*, which must not be + // exposed to MOC (it would pull Node::staticMetaObject across the ABI + // boundary). They are called directly, never used as connect() targets. NodeViewContext *add_context(Node *node); void remove_context(Node *node); +public slots: /** * @brief Set whether edges in this scene should be curved or not */ diff --git a/app/widget/playbackcontrols/playbackcontrols.cpp b/app/widget/playbackcontrols/playbackcontrols.cpp index 8f4fff041..c9f30c4a3 100644 --- a/app/widget/playbackcontrols/playbackcontrols.cpp +++ b/app/widget/playbackcontrols/playbackcontrols.cpp @@ -26,7 +26,7 @@ #include #include "core.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "ui/icons/icons.h" namespace olive diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 0f027af8b..52db93a0f 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -38,10 +38,12 @@ #include "projectexplorerundo.h" #include "oakengine/footage.h" #include "oakengine/node.h" -#include "task/taskmanager.h" +#include "oakengine/task.h" +#include "oakengine/videoparams.h" +#include "oakengine/project.h" +#include "oakengine/undo.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" -#include "node/nodeundo.h" #include "window/mainwindow/mainwindow.h" #include "window/mainwindow/mainwindowundo.h" #include "widget/timelinewidget/timelinewidget.h" @@ -56,7 +58,11 @@ QVector get_selected_proxy_footage(const QVector &items) QVector footage; for (Node *node : items) { Footage *candidate = dynamic_cast(node); - if (!candidate || !candidate->get_first_enabled_video_stream().is_valid() || + oak_video_params _vp; + if (!candidate || + oakengine_viewer_get_first_enabled_video_stream( + reinterpret_cast(candidate), &_vp) < 0 || + !oakengine_video_params_is_valid(&_vp) || footage.contains(candidate)) { continue; } @@ -65,45 +71,6 @@ QVector get_selected_proxy_footage(const QVector &items) return footage; } -/** - * @brief Proxy generation driven by the liboakengine C ABI facade - * - * Replaces the direct ProxyManager::get_or_start_proxy() drive: the actual - * transcode and its synchronous wait live behind - * oakengine_footage_proxy_generate() (which also records the proxy state - * on the footage and invalidates it), while the task stays on the - * TaskManager queue like before. - */ -class FacadeProxyTask : public Task { -public: - FacadeProxyTask(Footage *footage) - : footage_(footage) - { - set_title(tr("Generating proxy for \"%1\"") - .arg(footage->get_label_or_name())); - } - -protected: - virtual bool run() override - { - OakEngineFootage *handle = oakengine_footage_borrow( - reinterpret_cast(footage_)); - const int rc = oakengine_footage_proxy_generate(handle); - oakengine_footage_free(handle); - if (rc != OAKENGINE_OK) { - char err[512]; - err[0] = '\0'; - oakengine_footage_last_error(err, sizeof(err)); - set_error(err[0] ? QString::fromUtf8(err) : - tr("Proxy generation failed")); - return false; - } - return true; - } - -private: - Footage *footage_; -}; } ProjectExplorer::ProjectExplorer(QWidget *parent) @@ -191,10 +158,10 @@ void ProjectExplorer::set_view_type(ProjectToolbar::ViewType type) } } -void ProjectExplorer::edit(Node *item) +void ProjectExplorer::edit(OakEngineNode *item) { current_view()->edit( - sort_model_.mapFromSource(model_.create_index_from_item(item))); + sort_model_.mapFromSource(model_.create_index_from_item(reinterpret_cast(item)))); } void ProjectExplorer::add_view(QAbstractItemView *view) @@ -257,7 +224,7 @@ int ProjectExplorer::confirm_item_deletion(Node *item) bool ProjectExplorer::delete_items_internal(const QVector &selected, bool &check_if_item_is_in_use, - MultiUndoCommand *command) + void *command) { for (int i = 0; i < selected.size(); i++) { // Delete sequences first @@ -291,16 +258,29 @@ bool ProjectExplorer::delete_items_internal(const QVector &selected, Sequence *sequence = dynamic_cast(node); if (sequence && Core::instance()->main_window()->is_sequence_open(sequence)) { - command->add_child(new CloseSequenceCommand(sequence)); + oakengine_undo_command_multi_add_child(command, make_close_sequence_command(sequence)); } if (node->folder()) { - command->add_child( - new Folder::RemoveElementCommand(node->folder(), node)); + oakengine_undo_command_multi_add_child( + command, + oakengine_folder_remove_element_command( + reinterpret_cast(node->folder()), + reinterpret_cast(node))); } - command->add_child( - new NodeRemoveWithExclusiveDependenciesAndDisconnect(node)); + void *remove_cmd = oakengine_undo_command_create_multi(); + oakengine_undo_command_multi_add_child( + remove_cmd, + oakengine_node_remove_and_disconnect_command( + reinterpret_cast(node))); + for (Node *dep : node->get_exclusive_dependencies()) { + oakengine_undo_command_multi_add_child( + remove_cmd, + oakengine_node_remove_and_disconnect_command( + reinterpret_cast(dep))); + } + oakengine_undo_command_multi_add_child(command, remove_cmd); } } @@ -356,7 +336,7 @@ void ProjectExplorer::item_double_clicked_slot(const QModelIndex &index) } // Emit a signal - emit double_clicked_item(i); + emit double_clicked_item(reinterpret_cast(i)); } void ProjectExplorer::size_changed_slot(int s) @@ -457,7 +437,9 @@ void ProjectExplorer::show_context_menu() Sequence *sequence_cast_test = dynamic_cast(i); if (footage_cast_test && - !footage_cast_test->has_enabled_video_streams()) { + !oakengine_viewer_has_enabled_streams( + reinterpret_cast(footage_cast_test), + OAKENGINE_TRACK_TYPE_VIDEO)) { all_items_have_video_streams = false; } @@ -659,8 +641,10 @@ void ProjectExplorer::generate_proxies_for_selected_footage() << "GenerateProxiesForSelectedFootage: starting proxy generation for" << footage.size() << "footage item(s)"; for (Footage *item : footage) { - const VideoParams video = item->get_first_enabled_video_stream(); - if (!video.is_valid()) { + oak_video_params _vp; + if (oakengine_viewer_get_first_enabled_video_stream( + reinterpret_cast(item), &_vp) < 0 || + !oakengine_video_params_is_valid(&_vp)) { qWarning() << "GenerateProxiesForSelectedFootage: skipping item with no valid video stream" << item->filename(); @@ -669,7 +653,9 @@ void ProjectExplorer::generate_proxies_for_selected_footage() // Queue one facade-backed task per footage item (same queueing // semantics as the old per-footage proxy tasks). - TaskManager::instance()->add_task(new FacadeProxyTask(item)); + OakEngineTask *proxy_task = oakengine_task_create_proxy( + reinterpret_cast(item)); + oakengine_task_manager_add(proxy_task); } } @@ -689,10 +675,10 @@ void ProjectExplorer::set_selected_footage_proxy_enabled(bool enabled) OakEngineFootage *handle = oakengine_footage_borrow( reinterpret_cast(item)); oakengine_footage_proxy_set_enabled(handle, enabled ? 1 : 0); - oakengine_footage_free(handle); // The facade call toggles the flag; cache invalidation for the UI // stays here. - item->invalidate_all(Footage::k_filename_input); + oakengine_footage_invalidate(handle); + oakengine_footage_free(handle); } } @@ -765,18 +751,19 @@ void ProjectExplorer::view_selection_changed() QModelIndexList selection = model->selectedIndexes(); - QVector nodes; + QVector nodes; foreach (const QModelIndex &index, selection) { Node *sel = static_cast( sort_model_.mapToSource(index).internalPointer()); - if (!nodes.contains(sel)) { - nodes.append(sel); + auto handle = reinterpret_cast(sel); + if (!nodes.contains(handle)) { + nodes.append(handle); } } if (nodes.isEmpty()) { - nodes.append(get_root()); + nodes.append(reinterpret_cast(get_root())); } emit selection_changed(nodes); @@ -900,15 +887,15 @@ void ProjectExplorer::delete_selected() return; } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); bool check_if_item_is_in_use = true; if (delete_items_internal(selected, check_if_item_is_in_use, command)) { - Core::instance()->undo_stack()->push( - command, tr("Deleted %1 Item(s)").arg(selected.size())); + oakengine_undo_push( + command, tr("Deleted %1 Item(s)").arg(selected.size()).toUtf8().constData()); } else { - delete command; + oakengine_undo_command_free(command); } } diff --git a/app/widget/projectexplorer/projectexplorer.h b/app/widget/projectexplorer/projectexplorer.h index 4f79ceaf2..fc6b08615 100644 --- a/app/widget/projectexplorer/projectexplorer.h +++ b/app/widget/projectexplorer/projectexplorer.h @@ -35,6 +35,8 @@ #include "widget/projectexplorer/projectexplorernavigation.h" #include "widget/projecttoolbar/projecttoolbar.h" +struct OakEngineNode; + namespace olive { @@ -91,7 +93,7 @@ public: public slots: void set_view_type(ProjectToolbar::ViewType type); - void edit(Node *item); + void edit(OakEngineNode *item); void rename_selected_item(); @@ -105,9 +107,9 @@ signals: * * The Item that was double clicked, or nullptr if empty area was double clicked */ - void double_clicked_item(Node *item); + void double_clicked_item(OakEngineNode *item); - void selection_changed(const QVector &selected); + void selection_changed(const QVector &selected); private: /** @@ -143,7 +145,7 @@ private: bool delete_items_internal(const QVector &selected, bool &check_if_item_is_in_use, - MultiUndoCommand *command); + void *command); static QString get_human_readable_node_name(Node *node); diff --git a/app/widget/projectexplorer/projectexplorerundo.h b/app/widget/projectexplorer/projectexplorerundo.h index 0dc337a2d..cc370de4a 100644 --- a/app/widget/projectexplorer/projectexplorerundo.h +++ b/app/widget/projectexplorer/projectexplorerundo.h @@ -22,7 +22,6 @@ #ifndef OAK_PROJECTEXPLORERUNDO_H #define OAK_PROJECTEXPLORERUNDO_H -#include "undo/undocommand.h" namespace olive { diff --git a/app/widget/projectexplorer/projectviewmodel.cpp b/app/widget/projectexplorer/projectviewmodel.cpp index dc6538695..1ec8f0ffe 100644 --- a/app/widget/projectexplorer/projectviewmodel.cpp +++ b/app/widget/projectexplorer/projectviewmodel.cpp @@ -20,6 +20,7 @@ ***/ #include "projectviewmodel.h" +#include "ui/icons/icons.h" #include #include @@ -27,7 +28,9 @@ #include "common/qtutils.h" #include "core.h" -#include "node/nodeundo.h" +#include "oakengine/project.h" +#include "oakengine/node.h" +#include "oakengine/undo.h" namespace olive { @@ -35,7 +38,35 @@ namespace olive ProjectViewModel::ProjectViewModel(QObject *parent) : QAbstractItemModel(parent) , project_(nullptr) + , bridge_(new EngineEventBridge(this)) { + connect_bridge_signals(); +} + +void ProjectViewModel::connect_bridge_signals() +{ + connect(bridge_, &EngineEventBridge::folder_begin_insert_item, this, + [this](OakEngineNode *folder, OakEngineNode *child, int index) { + this->folder_begin_insert_item( + reinterpret_cast(folder), + reinterpret_cast(child), index); + }); + connect(bridge_, &EngineEventBridge::folder_end_insert_item, this, + [this](OakEngineNode *) { + this->folder_end_insert_item(); + }); + connect(bridge_, &EngineEventBridge::folder_begin_remove_item, this, + [this](OakEngineNode *folder, OakEngineNode *child, int index) { + this->folder_begin_remove_item( + reinterpret_cast(folder), + reinterpret_cast(child), index); + }); + connect(bridge_, &EngineEventBridge::folder_end_remove_item, this, + [this](OakEngineNode *) { + this->folder_end_remove_item(); + }); + connect(bridge_, &EngineEventBridge::node_label_changed, this, + &ProjectViewModel::item_renamed); } Project *ProjectViewModel::project() const @@ -49,6 +80,10 @@ void ProjectViewModel::set_project(Project *p) if (project_) { disconnect_item(project_->root()); + // Recreate bridge to clear all folder subscriptions + delete bridge_; + bridge_ = new EngineEventBridge(this); + connect_bridge_signals(); } project_ = p; @@ -182,7 +217,7 @@ QVariant ProjectViewModel::data(const QModelIndex &index, int role) const case Qt::DecorationRole: // If this is the first column, return the Item's icon if (column_type == k_name) { - return internal_item->data(Node::icon); + return icon::from_name(internal_item->data(Node::icon).toString()); } break; case Qt::ToolTipRole: @@ -239,13 +274,13 @@ bool ProjectViewModel::setData(const QModelIndex &index, const QVariant &value, QString new_name = value.toString(); if (!new_name.isEmpty()) { - NodeRenameCommand *nrc = new NodeRenameCommand(); + void *nrc = oakengine_node_rename_command( + reinterpret_cast(item), + new_name.toUtf8().constData()); - nrc->add_node(item, value.toString()); - - Core::instance()->undo_stack()->push( - nrc, tr("Renamed Item \"%1\" to \"%2\"") - .arg(item->get_label(), new_name)); + oakengine_undo_push( + nrc, + tr("Renamed Item \"%1\" to \"%2\"").arg(item->get_label(), new_name).toUtf8().constData()); return true; } @@ -284,7 +319,7 @@ Qt::ItemFlags ProjectViewModel::flags(const QModelIndex &index) const QStringList ProjectViewModel::mimeTypes() const { // Allow data from this model and a file list from external sources - return { Project::k_item_mime_type, QStringLiteral("text/uri-list") }; + return { QString::fromUtf8(oakengine_project_item_mime_type()), QStringLiteral("text/uri-list") }; } QMimeData *ProjectViewModel::mimeData(const QModelIndexList &indexes) const @@ -326,7 +361,7 @@ QMimeData *ProjectViewModel::mimeData(const QModelIndexList &indexes) const } // Set byte array as the mime data and return the mime data - data->setData(Project::k_item_mime_type, encoded_data); + data->setData(QString::fromUtf8(oakengine_project_item_mime_type()), encoded_data); return data; } @@ -347,9 +382,9 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, // Probe mime data for its format QStringList mime_formats = data->formats(); - if (mime_formats.contains(Project::k_item_mime_type)) { + if (mime_formats.contains(QString::fromUtf8(oakengine_project_item_mime_type()))) { // Data is drag/drop data from this model - QByteArray model_data = data->data(Project::k_item_mime_type); + QByteArray model_data = data->data(QString::fromUtf8(oakengine_project_item_mime_type())); // Use QDataStream to deserialize the data QDataStream stream(&model_data, QIODevice::ReadOnly); @@ -367,10 +402,8 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, quintptr item_ptr; QList streams; - // Loop through all data - MultiUndoCommand *move_command = new MultiUndoCommand(); - - int count = 0; + // Loop through all data, collecting the items to move + QVector items_to_move; while (!stream.atEnd()) { stream >> streams >> item_ptr; @@ -384,18 +417,18 @@ bool ProjectViewModel::dropMimeData(const QMimeData *data, (!dynamic_cast(item) || !item_is_parent_of_child(static_cast(item), drop_location))) { - move_command->add_child(new NodeEdgeRemoveCommand( - item, - NodeInput(item->folder(), Folder::k_child_input, - item->folder()->index_of_child_in_array(item)))); - move_command->add_child( - new FolderAddChild(drop_location, item)); - count++; + items_to_move.append(reinterpret_cast(item)); } } - Core::instance()->undo_stack()->push(move_command, - tr("Move %1 Item(s)").arg(count)); + if (!items_to_move.isEmpty()) { + // ONE undoable command for the whole move (facade removes each + // item from its old folder, then adds it to the drop location) + oakengine_folder_move_children( + items_to_move.constData(), items_to_move.size(), + reinterpret_cast(drop_location), + tr("Move %1 Item(s)").arg(items_to_move.size()).toUtf8().constData()); + } return true; @@ -475,18 +508,16 @@ bool ProjectViewModel::item_is_parent_of_child(Folder *parent, Node *child) cons void ProjectViewModel::connect_item(Node *n) { - connect(n, &Node::label_changed, this, &ProjectViewModel::item_renamed); + label_changed_subs_[n] = bridge_->subscribe( + reinterpret_cast(n), OAKENGINE_EVENT_NODE_LABEL_CHANGED); Folder *f = dynamic_cast(n); if (f) { - connect(f, &Folder::begin_insert_item, this, - &ProjectViewModel::folder_begin_insert_item); - connect(f, &Folder::end_insert_item, this, - &ProjectViewModel::folder_end_insert_item); - connect(f, &Folder::begin_remove_item, this, - &ProjectViewModel::folder_begin_remove_item); - connect(f, &Folder::end_remove_item, this, - &ProjectViewModel::folder_end_remove_item); + OakEngineNode *handle = reinterpret_cast(f); + bridge_->subscribe(handle, OAKENGINE_EVENT_FOLDER_BEGIN_INSERT_ITEM); + bridge_->subscribe(handle, OAKENGINE_EVENT_FOLDER_END_INSERT_ITEM); + bridge_->subscribe(handle, OAKENGINE_EVENT_FOLDER_BEGIN_REMOVE_ITEM); + bridge_->subscribe(handle, OAKENGINE_EVENT_FOLDER_END_REMOVE_ITEM); foreach (Node *c, f->children()) { connect_item(c); @@ -496,29 +527,23 @@ void ProjectViewModel::connect_item(Node *n) void ProjectViewModel::disconnect_item(Node *n) { - disconnect(n, &Node::label_changed, this, &ProjectViewModel::item_renamed); + int64_t sub = label_changed_subs_.take(n); + if (sub > 0) { + bridge_->unsubscribe(sub); + } Folder *f = dynamic_cast(n); if (f) { - disconnect(f, &Folder::begin_insert_item, this, - &ProjectViewModel::folder_begin_insert_item); - disconnect(f, &Folder::end_insert_item, this, - &ProjectViewModel::folder_end_insert_item); - disconnect(f, &Folder::begin_remove_item, this, - &ProjectViewModel::folder_begin_remove_item); - disconnect(f, &Folder::end_remove_item, this, - &ProjectViewModel::folder_end_remove_item); - + // Bridge subscriptions are cleaned up by recreating the bridge in set_project foreach (Node *c, f->children()) { disconnect_item(c); } } } -void ProjectViewModel::folder_begin_insert_item(Node *n, int insert_index) +void ProjectViewModel::folder_begin_insert_item(Folder *folder, Node *n, + int insert_index) { - Folder *folder = static_cast(sender()); - connect_item(n); QModelIndex index; @@ -535,10 +560,9 @@ void ProjectViewModel::folder_end_insert_item() endInsertRows(); } -void ProjectViewModel::folder_begin_remove_item(Node *n, int child_index) +void ProjectViewModel::folder_begin_remove_item(Folder *folder, Node *n, + int child_index) { - Folder *folder = static_cast(sender()); - disconnect_item(n); QModelIndex index; @@ -555,9 +579,9 @@ void ProjectViewModel::folder_end_remove_item() endRemoveRows(); } -void ProjectViewModel::item_renamed() +void ProjectViewModel::item_renamed(OakEngineNode *source) { - Node *item = static_cast(sender()); + Node *item = reinterpret_cast(source); QModelIndex index = create_index_from_item(item); diff --git a/app/widget/projectexplorer/projectviewmodel.h b/app/widget/projectexplorer/projectviewmodel.h index 85cddb825..20d0b8d52 100644 --- a/app/widget/projectexplorer/projectviewmodel.h +++ b/app/widget/projectexplorer/projectviewmodel.h @@ -23,10 +23,11 @@ #define OAK_VIEWMODEL_H #include +#include +#include "engineeventbridge.h" #include "node/block/block.h" #include "node/project.h" -#include "undo/undocommand.h" namespace olive { @@ -157,18 +158,30 @@ private: void disconnect_item(Node *n); - Project *project_; + /** + * @brief Wire the bridge's folder signals to our handlers. + * + * Must be re-run every time bridge_ is recreated (set_project), since + * Qt connections belong to the old bridge instance. + */ + void connect_bridge_signals(); -private slots: - void folder_begin_insert_item(Node *n, int insert_index); + void folder_begin_insert_item(Folder *folder, Node *n, int insert_index); void folder_end_insert_item(); - void folder_begin_remove_item(Node *n, int child_index); + void folder_begin_remove_item(Folder *folder, Node *n, int child_index); void folder_end_remove_item(); - void item_renamed(); + Project *project_; + + EngineEventBridge *bridge_; + + QHash label_changed_subs_; + +private slots: + void item_renamed(OakEngineNode *source); }; } diff --git a/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp b/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp index 3593437b0..d5a8a9bd0 100644 --- a/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp +++ b/app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp @@ -36,6 +36,7 @@ ResizableTimelineScrollBar::ResizableTimelineScrollBar(QWidget *parent) , markers_(nullptr) , workarea_(nullptr) , scale_(1.0) + , bridge_(new EngineEventBridge(this)) { } @@ -45,35 +46,59 @@ ResizableTimelineScrollBar::ResizableTimelineScrollBar( , markers_(nullptr) , workarea_(nullptr) , scale_(1.0) + , bridge_(new EngineEventBridge(this)) { } +ResizableTimelineScrollBar::~ResizableTimelineScrollBar() +{ + // Raw workarea subscriptions carry `this` as userdata; unlike the + // bridge (which dies with us), they must be cancelled explicitly or the + // engine would call back into a dead widget. + if (workarea_range_sub_ > 0) { + oakengine_event_unsubscribe(workarea_range_sub_); + } + if (workarea_enabled_sub_ > 0) { + oakengine_event_unsubscribe(workarea_enabled_sub_); + } +} + void ResizableTimelineScrollBar::connect_markers(TimelineMarkerList *markers) { if (markers_) { - disconnect(markers_, &TimelineMarkerList::marker_added, this, - static_cast( - &ResizableTimelineScrollBar::update)); - disconnect(markers_, &TimelineMarkerList::marker_removed, this, - static_cast( - &ResizableTimelineScrollBar::update)); - disconnect(markers_, &TimelineMarkerList::marker_modified, this, - static_cast( - &ResizableTimelineScrollBar::update)); + if (marker_sub_add_) + bridge_->unsubscribe(marker_sub_add_); + if (marker_sub_rem_) + bridge_->unsubscribe(marker_sub_rem_); + if (marker_sub_mod_) + bridge_->unsubscribe(marker_sub_mod_); } markers_ = markers; if (markers_) { - connect(markers_, &TimelineMarkerList::marker_added, this, - static_cast( - &ResizableTimelineScrollBar::update)); - connect(markers_, &TimelineMarkerList::marker_removed, this, - static_cast( - &ResizableTimelineScrollBar::update)); - connect(markers_, &TimelineMarkerList::marker_modified, this, - static_cast( - &ResizableTimelineScrollBar::update)); + marker_sub_add_ = bridge_->subscribe( + reinterpret_cast(markers_), + OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED); + marker_sub_rem_ = bridge_->subscribe( + reinterpret_cast(markers_), + OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED); + marker_sub_mod_ = bridge_->subscribe( + reinterpret_cast(markers_), + OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED); + + connect(bridge_, &EngineEventBridge::marker_list_marker_added, this, + [this](OakEngineMarkerList *, OakEngineMarker *) { + update(); + }); + connect(bridge_, &EngineEventBridge::marker_list_marker_removed, this, + [this](OakEngineMarkerList *, OakEngineMarker *) { + update(); + }); + connect(bridge_, &EngineEventBridge::marker_list_marker_modified, this, + [this](OakEngineMarkerList *, OakEngineMarker *) { + update(); + }); } update(); @@ -82,23 +107,30 @@ void ResizableTimelineScrollBar::connect_markers(TimelineMarkerList *markers) void ResizableTimelineScrollBar::connect_work_area(TimelineWorkArea *workarea) { if (workarea_) { - disconnect(workarea_, &TimelineWorkArea::range_changed, this, - static_cast( - &ResizableTimelineScrollBar::update)); - disconnect(workarea_, &TimelineWorkArea::enabled_changed, this, - static_cast( - &ResizableTimelineScrollBar::update)); + if (workarea_range_sub_ > 0) { + oakengine_event_unsubscribe(workarea_range_sub_); + workarea_range_sub_ = 0; + } + if (workarea_enabled_sub_ > 0) { + oakengine_event_unsubscribe(workarea_enabled_sub_); + workarea_enabled_sub_ = 0; + } } workarea_ = workarea; if (workarea_) { - connect(workarea_, &TimelineWorkArea::range_changed, this, - static_cast( - &ResizableTimelineScrollBar::update)); - connect(workarea_, &TimelineWorkArea::enabled_changed, this, - static_cast( - &ResizableTimelineScrollBar::update)); + void *handle = reinterpret_cast(workarea_); + workarea_range_sub_ = oakengine_event_subscribe( + handle, OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED, + [](const oakengine_event *, void *userdata) { + static_cast(userdata)->update(); + }, this); + workarea_enabled_sub_ = oakengine_event_subscribe( + handle, OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED, + [](const oakengine_event *, void *userdata) { + static_cast(userdata)->update(); + }, this); } update(); diff --git a/app/widget/resizablescrollbar/resizabletimelinescrollbar.h b/app/widget/resizablescrollbar/resizabletimelinescrollbar.h index 09420a8b7..f0a4bbf46 100644 --- a/app/widget/resizablescrollbar/resizabletimelinescrollbar.h +++ b/app/widget/resizablescrollbar/resizabletimelinescrollbar.h @@ -26,6 +26,8 @@ #include "timeline/timelinemarker.h" #include "timeline/timelineworkarea.h" #include "widget/timebased/timescaledobject.h" +#include "engineeventbridge.h" +#include "oakengine/events.h" namespace olive { @@ -37,6 +39,7 @@ public: ResizableTimelineScrollBar(QWidget *parent = nullptr); ResizableTimelineScrollBar(Qt::Orientation orientation, QWidget *parent = nullptr); + ~ResizableTimelineScrollBar() override; void connect_markers(TimelineMarkerList *markers); void connect_work_area(TimelineWorkArea *workarea); @@ -51,7 +54,20 @@ private: TimelineWorkArea *workarea_; + // Workarea signal subscriptions (event 141/142-style, but workarea is a + // timeline-level concept tracked via OakEngineEvents). + int64_t workarea_range_sub_ = 0; + int64_t workarea_enabled_sub_ = 0; + double scale_; + + EngineEventBridge *bridge_ = nullptr; + + int64_t marker_sub_add_ = 0; + + int64_t marker_sub_rem_ = 0; + + int64_t marker_sub_mod_ = 0; }; } diff --git a/app/widget/scope/histogram/histogram.cpp b/app/widget/scope/histogram/histogram.cpp index efa0adf30..f42dcec60 100644 --- a/app/widget/scope/histogram/histogram.cpp +++ b/app/widget/scope/histogram/histogram.cpp @@ -28,6 +28,8 @@ #include "common/qtutils.h" #include "node/node.h" +#include "oakengine/display.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -86,9 +88,14 @@ void HistogramScope::draw_scope(TexturePtr managed_tex, QVariant pipeline) if (!texture_row_sums_ || texture_row_sums_->width() != this->width() || texture_row_sums_->height() != this->height()) { - texture_row_sums_ = renderer()->create_texture( - VideoParams(width(), height(), managed_tex->format(), - managed_tex->channel_count())); + oak_video_params pod = {}; + pod.width = width(); + pod.height = height(); + pod.format = managed_tex->format(); + const VideoParams row_sums_params = video_params_from_pod(pod); + oakengine_display_renderer_create_texture(renderer(), + &row_sums_params, nullptr, 0, + &texture_row_sums_); } // Draw managed texture to a sums texture diff --git a/app/widget/scope/scopebase/scopebase.cpp b/app/widget/scope/scopebase/scopebase.cpp index 27372f111..84beffb95 100644 --- a/app/widget/scope/scopebase/scopebase.cpp +++ b/app/widget/scope/scopebase/scopebase.cpp @@ -21,8 +21,14 @@ #include "scopebase.h" -#include "config/config.h" +#include + +#include "common/configwrapper.h" +#include "oakengine/display.h" +#include "oakengine/videoparams.h" #include "render/job/colortransformjob.h" +#include "widget/viewer/vieweroutpututils.h" +#include "render/job/shaderjob.h" namespace olive { @@ -76,14 +82,17 @@ void ScopeBase::update_software_image() // it into this scope's renderer before we can sample it. TexturePtr source_tex = texture_; if (texture_->renderer() && texture_->renderer() != renderer()) { - FramePtr temp_frame = Frame::create(); - temp_frame->set_video_params(texture_->params()); - temp_frame->allocate(); - texture_->download(temp_frame->data(), temp_frame->linesize_pixels()); + FramePtr temp_frame; + oakengine_codec_frame_create(&temp_frame); + oakengine_codec_frame_set_video_params(temp_frame.get(), + &texture_->params()); + oakengine_codec_frame_allocate(temp_frame.get()); + oakengine_display_texture_download(texture_.get(), temp_frame->data(), + temp_frame->linesize_pixels()); - local_texture_ = renderer()->create_texture( - temp_frame->video_params(), temp_frame->data(), - temp_frame->linesize_pixels()); + oakengine_display_renderer_create_texture( + renderer(), &temp_frame->video_params(), temp_frame->data(), + temp_frame->linesize_pixels(), &local_texture_); source_tex = local_texture_; } @@ -96,16 +105,20 @@ void ScopeBase::update_software_image() const int texture_width = static_cast(width() * devicePixelRatioF()); const int texture_height = static_cast(height() * devicePixelRatioF()); - const VideoParams offscreen_params(texture_width, texture_height, - PixelFormat::u8, - VideoParams::k_rgba_channel_count); + oak_video_params pod = {}; + pod.width = texture_width; + pod.height = texture_height; + pod.format = PixelFormat::u8; + const VideoParams offscreen_params(video_params_from_pod(pod)); if (!software_tex_ || software_tex_->params() != offscreen_params) { - software_tex_ = renderer()->create_texture(offscreen_params); + oakengine_display_renderer_create_texture(renderer(), + &offscreen_params, nullptr, 0, + &software_tex_); software_buffer_.resize( texture_width * texture_height * - VideoParams::get_bytes_per_pixel(PixelFormat::u8, - VideoParams::k_rgba_channel_count)); + oakengine_video_params_bytes_per_pixel(PixelFormat::u8, + 4)); } if (!software_tex_ || software_tex_->is_dummy()) { @@ -115,13 +128,14 @@ void ScopeBase::update_software_image() } ColorTransformJob job; - job.set_color_processor(color_service()); + oakengine_color_transform_job_set_processor(&job, color_service().get()); job.set_input_texture(source_tex); job.set_input_alpha_association(k_alpha_none); job.set_clear_destination_enabled(true); job.set_force_opaque(true); - renderer()->blit_color_managed(job, software_tex_.get()); + oakengine_display_renderer_blit_color_managed(renderer(), &job, + software_tex_.get(), nullptr); renderer()->download_from_texture(software_tex_->id(), software_tex_->params(), software_buffer_.data(), 0); @@ -129,8 +143,8 @@ void ScopeBase::update_software_image() software_image_ = QImage( reinterpret_cast(software_buffer_.constData()), texture_width, texture_height, - texture_width * VideoParams::get_bytes_per_pixel( - PixelFormat::u8, VideoParams::k_rgba_channel_count), + texture_width * oakengine_video_params_bytes_per_pixel( + PixelFormat::u8, 4), QImage::Format_RGBA8888_Premultiplied); software_image_.setDevicePixelRatio(devicePixelRatioF()); @@ -169,14 +183,16 @@ void ScopeBase::on_paint() // Convert reference frame to display space if (!managed_tex_ || !managed_tex_up_to_date_ || managed_tex_->params() != texture_->params()) { - managed_tex_ = renderer()->create_texture(texture_->params()); + oakengine_display_renderer_create_texture( + renderer(), &texture_->params(), nullptr, 0, &managed_tex_); ColorTransformJob job; - job.set_color_processor(color_service()); + oakengine_color_transform_job_set_processor(&job, color_service().get()); job.set_input_texture(texture_); job.set_input_alpha_association(k_alpha_none); - renderer()->blit_color_managed(job, managed_tex_.get()); + oakengine_display_renderer_blit_color_managed( + renderer(), &job, managed_tex_.get(), nullptr); managed_tex_up_to_date_ = true; } diff --git a/app/widget/scope/vectorscope/vectorscope.cpp b/app/widget/scope/vectorscope/vectorscope.cpp index f4017a5a5..d1133ce9c 100644 --- a/app/widget/scope/vectorscope/vectorscope.cpp +++ b/app/widget/scope/vectorscope/vectorscope.cpp @@ -60,7 +60,7 @@ void VectorscopeScope::draw_scope(TexturePtr managed_tex, QVariant pipeline) NodeValue(NodeValue::k_vec2, QVector2D(width(), height()))); double luma_coeffs[3] = { 0.0f, 0.0f, 0.0f }; - color_manager()->get_default_luma_coefs(luma_coeffs); + oakengine_color_manager_default_luma_coefs(color_manager(), luma_coeffs); job.insert( QStringLiteral("luma_coeffs"), NodeValue(NodeValue::k_vec3, @@ -139,7 +139,7 @@ void VectorscopeScope::draw_scope_software(QPainter &p, const QImage &image) buf.fill(Qt::transparent); double luma_coeffs[3] = { 0.0, 0.0, 0.0 }; - color_manager()->get_default_luma_coefs(luma_coeffs); + oakengine_color_manager_default_luma_coefs(color_manager(), luma_coeffs); const int src_w = image.width(); const int src_h = image.height(); diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp index c136facbe..06c891c0d 100644 --- a/app/widget/scope/waveform/waveform.cpp +++ b/app/widget/scope/waveform/waveform.cpp @@ -31,7 +31,7 @@ #include #include "common/qtutils.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "node/node.h" namespace olive @@ -82,7 +82,7 @@ void WaveformScope::draw_scope(TexturePtr managed_tex, QVariant pipeline) // Set luma coefficients double luma_coeffs[3] = { 0.0f, 0.0f, 0.0f }; - color_manager()->get_default_luma_coefs(luma_coeffs); + oakengine_color_manager_default_luma_coefs(color_manager(), luma_coeffs); job.insert( QStringLiteral("luma_coeffs"), NodeValue(NodeValue::k_vec3, diff --git a/app/widget/slider/base/numericsliderbase.cpp b/app/widget/slider/base/numericsliderbase.cpp index de11c30c3..9b5e29eed 100644 --- a/app/widget/slider/base/numericsliderbase.cpp +++ b/app/widget/slider/base/numericsliderbase.cpp @@ -22,7 +22,7 @@ #include "numericsliderbase.h" #include "common/qtutils.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" namespace olive diff --git a/app/widget/slider/base/sliderladder.cpp b/app/widget/slider/base/sliderladder.cpp index 244d57152..4f813b90d 100644 --- a/app/widget/slider/base/sliderladder.cpp +++ b/app/widget/slider/base/sliderladder.cpp @@ -34,7 +34,7 @@ #include "common/lerp.h" #include "common/qtutils.h" -#include "config/config.h" +#include "common/configwrapper.h" namespace olive { diff --git a/app/widget/standardcombos/frameratecombobox.h b/app/widget/standardcombos/frameratecombobox.h index 1673394a3..e963f26dc 100644 --- a/app/widget/standardcombos/frameratecombobox.h +++ b/app/widget/standardcombos/frameratecombobox.h @@ -28,7 +28,7 @@ #include #include -#include "render/videoparams.h" +#include "oakengine/videoparams.h" namespace olive { @@ -164,17 +164,32 @@ private: inner_->clear(); - foreach (const Rational &fr, VideoParams::k_supported_frame_rates) { - inner_->addItem(VideoParams::frame_rate_to_string(fr), - QVariant::fromValue(fr)); + { + const int n = oakengine_video_params_supported_frame_rate_count(); + for (int i = 0; i < n; i++) { + int num, den; + oakengine_video_params_supported_frame_rate_at(i, &num, &den); + char buf[64]; + oakengine_video_params_frame_rate_to_string(num, den, buf, + sizeof(buf)); + Rational fr(num, den); + inner_->addItem(QString::fromUtf8(buf), + QVariant::fromValue(fr)); + } } if (custom_rate_.isNull()) { inner_->addItem(tr("Custom...")); } else { - inner_->addItem( - tr("Custom (%1)") - .arg(VideoParams::frame_rate_to_string(custom_rate_))); + { + char buf[64]; + oakengine_video_params_frame_rate_to_string( + custom_rate_.numerator(), custom_rate_.denominator(), buf, + sizeof(buf)); + inner_->addItem( + tr("Custom (%1)") + .arg(QString::fromUtf8(buf))); + } } // On the first populate there is no current index (-1); select the diff --git a/app/widget/standardcombos/interlacedcombobox.h b/app/widget/standardcombos/interlacedcombobox.h index c40e02a30..27f3c311f 100644 --- a/app/widget/standardcombos/interlacedcombobox.h +++ b/app/widget/standardcombos/interlacedcombobox.h @@ -24,7 +24,7 @@ #include -#include "render/videoparams.h" +#include "oakengine/videoparams.h" namespace olive { @@ -41,12 +41,12 @@ public: this->addItem(tr("Bottom-Field First")); } - VideoParams::Interlacing get_interlace_mode() const + int get_interlace_mode() const { - return static_cast(this->currentIndex()); + return this->currentIndex(); } - void set_interlace_mode(VideoParams::Interlacing mode) + void set_interlace_mode(int mode) { this->setCurrentIndex(mode); } diff --git a/app/widget/standardcombos/pixelaspectratiocombobox.h b/app/widget/standardcombos/pixelaspectratiocombobox.h index a9acb8367..a5a106adc 100644 --- a/app/widget/standardcombos/pixelaspectratiocombobox.h +++ b/app/widget/standardcombos/pixelaspectratiocombobox.h @@ -25,7 +25,7 @@ #include #include "dialog/ratiodialog.h" -#include "render/videoparams.h" +#include "oakengine/videoparams.h" namespace olive { @@ -37,11 +37,16 @@ public: : QComboBox(parent) , dont_prompt_custom_par_(false) { - QStringList par_names = VideoParams::get_standard_pixel_aspect_ratio_names(); - for (int i = 0; i < VideoParams::k_standard_pixel_aspects.size(); i++) { - const Rational &ratio = VideoParams::k_standard_pixel_aspects.at(i); - - this->addItem(par_names.at(i), QVariant::fromValue(ratio)); + const int n = oakengine_video_params_standard_pixel_aspect_count(); + for (int i = 0; i < n; i++) { + int num, den; + oakengine_video_params_standard_pixel_aspect_at(i, &num, &den); + char name_buf[256]; + oakengine_video_params_standard_pixel_aspect_name(i, name_buf, + sizeof(name_buf)); + Rational ratio(num, den); + this->addItem(QString::fromUtf8(name_buf), + QVariant::fromValue(ratio)); } // Always add custom item last, much of the logic relies on this. Set this to the current AR so @@ -110,9 +115,13 @@ private: // Use 1:1 to prevent any real chance of the PAR being set to 0 this->setItemData(custom_index, QVariant::fromValue(Rational(1))); } else { - this->setItemText(custom_index, - VideoParams::format_pixel_aspect_ratio_string( - tr("Custom (%1)"), ratio)); + char buf[256]; + oakengine_video_params_format_pixel_aspect_ratio_string( + "%1", ratio.numerator(), ratio.denominator(), buf, + sizeof(buf)); + this->setItemText( + custom_index, + tr("Custom (%1)").arg(QString::fromUtf8(buf))); this->setItemData(custom_index, QVariant::fromValue(ratio)); } } diff --git a/app/widget/standardcombos/pixelformatcombobox.h b/app/widget/standardcombos/pixelformatcombobox.h index 4e769e172..76e62a1a3 100644 --- a/app/widget/standardcombos/pixelformatcombobox.h +++ b/app/widget/standardcombos/pixelformatcombobox.h @@ -24,7 +24,7 @@ #include -#include "render/videoparams.h" +#include "oakengine/videoparams.h" namespace olive { @@ -35,13 +35,13 @@ public: PixelFormatComboBox(bool float_only, QWidget *parent = nullptr) : QComboBox(parent) { - // Set up preview formats - for (int i = 0; i < PixelFormat::count; i++) { - PixelFormat pix_fmt = static_cast(i); - - if (!float_only || pix_fmt.is_float()) { - this->addItem(VideoParams::get_format_name(pix_fmt), - static_cast(pix_fmt)); + // Set up preview formats (PixelFormat::u8=0 .. f32=4) + for (int i = 0; i < 5; i++) { + if (!float_only || oakengine_video_params_format_is_float(i)) { + char name_buf[64]; + oakengine_video_params_pixel_format_name(i, name_buf, + sizeof(name_buf)); + this->addItem(QString::fromUtf8(name_buf), i); } } } diff --git a/app/widget/standardcombos/videodividercombobox.h b/app/widget/standardcombos/videodividercombobox.h index 7f6cdecef..1772fddbb 100644 --- a/app/widget/standardcombos/videodividercombobox.h +++ b/app/widget/standardcombos/videodividercombobox.h @@ -24,7 +24,7 @@ #include -#include "render/videoparams.h" +#include "oakengine/videoparams.h" namespace olive { @@ -35,8 +35,12 @@ public: VideoDividerComboBox(QWidget *parent = nullptr) : QComboBox(parent) { - foreach (int d, VideoParams::k_supported_dividers) { - this->addItem(VideoParams::get_name_for_divider(d), d); + const int n = oakengine_video_params_supported_divider_count(); + for (int i = 0; i < n; i++) { + int d = oakengine_video_params_supported_divider_at(i); + char name_buf[64]; + oakengine_video_params_divider_name(d, name_buf, sizeof(name_buf)); + this->addItem(QString::fromUtf8(name_buf), d); } } diff --git a/app/widget/taskview/taskview.cpp b/app/widget/taskview/taskview.cpp index b81522da6..aac9f1cb9 100644 --- a/app/widget/taskview/taskview.cpp +++ b/app/widget/taskview/taskview.cpp @@ -45,7 +45,7 @@ TaskView::TaskView(QWidget *parent) layout_->addStretch(); } -void TaskView::add_task(Task *t) +void TaskView::add_task(OakEngineTask *t) { // Create TaskViewItem (UI representation of a Task) and connect it TaskViewItem *item = new TaskViewItem(t); @@ -54,12 +54,12 @@ void TaskView::add_task(Task *t) layout_->insertWidget(layout_->count() - 1, item); } -void TaskView::task_failed(Task *t) +void TaskView::task_failed(OakEngineTask *t) { items_.value(t)->failed(); } -void TaskView::remove_task(Task *t) +void TaskView::remove_task(OakEngineTask *t) { items_.value(t)->deleteLater(); items_.remove(t); diff --git a/app/widget/taskview/taskview.h b/app/widget/taskview/taskview.h index ce2361c41..e55c1734e 100644 --- a/app/widget/taskview/taskview.h +++ b/app/widget/taskview/taskview.h @@ -44,26 +44,26 @@ public: TaskView(QWidget *parent); signals: - void task_cancelled(Task *t); + void task_cancelled(OakEngineTask *t); public slots: /** - * @brief Creates a TaskViewItem, connects it to a Task, and adds it to this widget - * - * Connect this to TaskManager::TaskAdded(). - */ - void add_task(Task *t); + * @brief Creates a TaskViewItem, connects it to a Task, and adds it to this widget + * + * Connect this to TaskManager::TaskAdded(). + */ + void add_task(OakEngineTask *t); - void task_failed(Task *t); + void task_failed(OakEngineTask *t); - void remove_task(Task *t); + void remove_task(OakEngineTask *t); private: QWidget *central_widget_; QVBoxLayout *layout_; - QHash items_; + QHash items_; }; } diff --git a/app/widget/taskview/taskviewitem.cpp b/app/widget/taskview/taskviewitem.cpp index e69b8510d..3b8290969 100644 --- a/app/widget/taskview/taskviewitem.cpp +++ b/app/widget/taskview/taskviewitem.cpp @@ -29,7 +29,7 @@ namespace olive { -TaskViewItem::TaskViewItem(Task *task, QWidget *parent) +TaskViewItem::TaskViewItem(OakEngineTask *task, QWidget *parent) : QFrame(parent) , task_(task) { @@ -41,7 +41,10 @@ TaskViewItem::TaskViewItem(Task *task, QWidget *parent) // Create header label task_name_lbl_ = new QLabel(this); - task_name_lbl_->setText(task_->get_title()); + char title_buf[512]; + title_buf[0] = '\0'; + oakengine_task_title(task_, title_buf, sizeof(title_buf)); + task_name_lbl_->setText(QString::fromUtf8(title_buf)); layout->addWidget(task_name_lbl_); // Create center layout (combines progress bar and a cancel button) @@ -74,10 +77,18 @@ TaskViewItem::TaskViewItem(Task *task, QWidget *parent) // Set up elapsed timer status_stack_->setCurrentWidget(elapsed_timer_lbl_); - // Connect to the task - connect(task_, &Task::started, elapsed_timer_lbl_, - qOverload(&ElapsedCounterWidget::start)); - connect(task_, &Task::progress_changed, this, &TaskViewItem::update_progress); + // Connect to the task via EngineEventBridge + bridge_ = new EngineEventBridge(this); + bridge_->subscribe(task_, OAKENGINE_EVENT_TASK_STARTED); + bridge_->subscribe(task_, OAKENGINE_EVENT_TASK_PROGRESS); + connect(bridge_, &EngineEventBridge::task_started, this, + [this](OakEngineTask *, qint64 start_time) { + elapsed_timer_lbl_->start(start_time); + }); + connect(bridge_, &EngineEventBridge::task_progress, this, + [this](OakEngineTask *, double progress) { + update_progress(progress); + }); connect(cancel_btn_, &QPushButton::clicked, this, [this] { emit task_cancelled(task_); }); } @@ -86,7 +97,10 @@ void TaskViewItem::failed() { status_stack_->setCurrentWidget(task_error_lbl_); task_error_lbl_->setStyleSheet("color: red"); - task_error_lbl_->setText(tr("Error: %1").arg(task_->get_error())); + char err[512]; + err[0] = '\0'; + oakengine_task_error(task_, err, sizeof(err)); + task_error_lbl_->setText(tr("Error: %1").arg(QString::fromUtf8(err))); } void TaskViewItem::update_progress(double d) diff --git a/app/widget/taskview/taskviewitem.h b/app/widget/taskview/taskviewitem.h index f07302619..81c5f896b 100644 --- a/app/widget/taskview/taskviewitem.h +++ b/app/widget/taskview/taskviewitem.h @@ -29,29 +29,21 @@ #include #include "elapsedcounterwidget.h" -#include "task/task.h" +#include "engineeventbridge.h" +#include "oakengine/task.h" namespace olive { -/** - * @brief A widget that visually represents the status of a Task - * - * The TaskViewItem widget shows a description of the Task (Task::text(), a progress bar (updated by - * Task::ProgressChanged), the Task's status (text generated from Task::status() or Task::error()), and provides - * a cancel button (triggering Task::Cancel()) for cancelling a Task before it finishes. - * - * The main entry point is SetTask() after a Task and TaskViewItem objects are created. - */ class TaskViewItem : public QFrame { Q_OBJECT public: - TaskViewItem(Task *task, QWidget *parent = nullptr); + TaskViewItem(OakEngineTask *task, QWidget *parent = nullptr); void failed(); signals: - void task_cancelled(Task *t); + void task_cancelled(OakEngineTask *t); private: QLabel *task_name_lbl_; @@ -62,7 +54,9 @@ private: ElapsedCounterWidget *elapsed_timer_lbl_; QLabel *task_error_lbl_; - Task *task_; + OakEngineTask *task_; + + EngineEventBridge *bridge_; private slots: void update_progress(double d); diff --git a/app/widget/timebased/timebasedview.cpp b/app/widget/timebased/timebasedview.cpp index 95d11838a..b061a332f 100644 --- a/app/widget/timebased/timebasedview.cpp +++ b/app/widget/timebased/timebasedview.cpp @@ -27,6 +27,8 @@ #include #include "widget/timebased/timebasedwidget.h" +#include "oakengine/events.h" +#include "oakengine/viewer.h" namespace olive { @@ -156,15 +158,20 @@ void TimeBasedView::set_y_scale(const double &y_scale) void TimeBasedView::set_viewer_node(ViewerOutput *v) { if (viewer_) { - disconnect(viewer_, &ViewerOutput::playhead_changed, viewport(), - static_cast(&TimeBasedView::update)); + oakengine_event_unsubscribe(viewer_sub_); + viewer_sub_ = 0; } viewer_ = v; if (viewer_) { - connect(viewer_, &ViewerOutput::playhead_changed, viewport(), - static_cast(&TimeBasedView::update)); + viewer_sub_ = oakengine_event_subscribe( + reinterpret_cast(viewer_), + OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED, + [](const oakengine_event *, void *userdata) { + static_cast(userdata)->viewport()->update(); + }, + this); } } @@ -247,7 +254,9 @@ bool TimeBasedView::playhead_move(QMouseEvent *event) mouse_time += movement; } - viewer_->set_playhead(mouse_time); + oakengine_viewer_set_playhead( + reinterpret_cast(viewer_), + mouse_time.numerator(), mouse_time.denominator()); } return true; diff --git a/app/widget/timebased/timebasedview.h b/app/widget/timebased/timebasedview.h index 12ee01c77..3126b5b7a 100644 --- a/app/widget/timebased/timebasedview.h +++ b/app/widget/timebased/timebasedview.h @@ -23,6 +23,7 @@ #define OAK_TIMELINEVIEWBASE_H #include +#include #include #include "core.h" @@ -146,6 +147,8 @@ private: double y_scale_; ViewerOutput *viewer_; + + int64_t viewer_sub_ = 0; }; } diff --git a/app/widget/timebased/timebasedviewselectionmanager.h b/app/widget/timebased/timebasedviewselectionmanager.h index 94349f2b5..8ad8d5ce2 100644 --- a/app/widget/timebased/timebasedviewselectionmanager.h +++ b/app/widget/timebased/timebasedviewselectionmanager.h @@ -28,8 +28,13 @@ #include #include "common/qtutils.h" +#include "olive/core/util/timecodefunctions.h" +#include "oakengine/node.h" +#include "oakengine/undo.h" #include "timebasedview.h" #include "timebasedwidget.h" +#include "widget/keyframeview/keyframehandle.h" +#include "widget/timeruler/markerhandle.h" #include "widget/timetarget/timetarget.h" namespace olive @@ -311,7 +316,13 @@ public: // Apply movement for (size_t i = 0; i < selected_.size(); i++) { - selected_.at(i)->set_time(dragging_.at(i) + time_diff); + if constexpr (std::is_same_v) { + key_set_time_live(selected_.at(i), + dragging_.at(i) + time_diff); + } else { + selection_set_time(selected_.at(i), + dragging_.at(i) + time_diff); + } } // Show information about this keyframe @@ -336,19 +347,32 @@ public: QToolTip::showText(QCursor::pos(), tip); } - void drag_stop(MultiUndoCommand *command) + void drag_stop(void *command) { QToolTip::hideText(); for (size_t i = 0; i < selected_.size(); i++) { - Rational current; - if constexpr (std::is_same_v) { - current = selected_.at(i)->time().in(); - } else { - current = selected_.at(i)->time(); + if constexpr (std::is_same_v) { + int tbn = 0, tbd = 0; + oakengine_node_frame_time_base( + reinterpret_cast(selected_.at(i)->parent()), + &tbn, &tbd); + const int64_t new_ts = olive::core::Timecode::time_to_timestamp( + dragging_.at(i), olive::Rational(tbn, tbd), + olive::core::Timecode::k_round); + oakengine_undo_command_multi_add_child( + command, + oakengine_keyframe_set_time_command( + reinterpret_cast(selected_.at(i)), + new_ts)); + } else if constexpr (std::is_same_v) { + oakengine_undo_command_multi_add_child( + command, + oakengine_marker_set_time_command( + reinterpret_cast(selected_.at(i)), + dragging_.at(i).numerator(), + dragging_.at(i).denominator())); } - command->add_child( - new SetTimeCommand(selected_.at(i), current, dragging_.at(i))); } dragging_.clear(); @@ -419,47 +443,7 @@ public: } private: - class SetTimeCommand : public UndoCommand { - public: - SetTimeCommand(T *key, const Rational &time) - { - key_ = key; - new_time_ = time; - old_time_ = key_->time(); - } - - SetTimeCommand(T *key, const Rational &new_time, - const Rational &old_time) - { - key_ = key; - new_time_ = new_time; - old_time_ = old_time; - } - - virtual Project *get_relevant_project() const override - { - return Project::get_project_from_object(key_); - } - - protected: - virtual void redo() override - { - key_->set_time(new_time_); - } - - virtual void undo() override - { - key_->set_time(old_time_); - } - - private: - T *key_; - - Rational old_time_; - Rational new_time_; - }; - - TimeBasedView *view_; +TimeBasedView *view_; using DrawnObject = QPair; std::vector drawn_objects_; diff --git a/app/widget/timebased/timebasedwidget.cpp b/app/widget/timebased/timebasedwidget.cpp index adaa015d3..c01c86432 100644 --- a/app/widget/timebased/timebasedwidget.cpp +++ b/app/widget/timebased/timebasedwidget.cpp @@ -25,15 +25,20 @@ #include "common/autoscroll.h" #include "common/range.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" +#include "engineeventbridge.h" #include "common/current.h" #include "dialog/markerproperties/markerpropertiesdialog.h" #include "node/project/sequence/sequence.h" #include "oakengine/timeline.h" -#include "timeline/timelineundoworkarea.h" +#include "oakengine/viewer.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" #include "widget/timeruler/timeruler.h" +#include "widget/timelinewidget/cliphandle.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -42,6 +47,7 @@ TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible, QWidget *parent) : TimelineScaledWidget(parent) , viewer_node_(nullptr) + , bridge_(new EngineEventBridge(this)) , auto_max_scrollbar_(false) , toggle_show_all_(false) , auto_set_timebase_(true) @@ -95,33 +101,28 @@ void TimeBasedWidget::connect_viewer_node(ViewerOutput *node) // Set viewer node ViewerOutput *old = viewer_node_.data(); viewer_node_ = node; + + // Disconnect old bridge subscriptions and connections + disconnect(bridge_, nullptr, this, nullptr); + bridge_->unsubscribe_all(); + if (viewer_node_) { + oak_video_params vp; + oakengine_viewer_get_video_params( + reinterpret_cast(viewer_node_.data()), 0, &vp); + // We still need Current class - keep using it for now Current::getInstance().setCurrentVideoParams( - viewer_node_->get_video_params()); + viewer_output_video_params(viewer_node_)); Current::getInstance().setCurrentAudioParams( - viewer_node_->get_audio_params()); + viewer_output_audio_params(viewer_node_)); } else { - Current::getInstance().setCurrentVideoParams(VideoParams()); + Current::getInstance().setCurrentVideoParams(empty_video_params()); Current::getInstance().setCurrentAudioParams(AudioParams()); } if (old) { // Call potential derivative functions for disconnecting the viewer node DisconnectNodeEvent(old); - // Disconnect length changed signal - disconnect(old, &ViewerOutput::length_changed, this, - &TimeBasedWidget::update_maximum_scroll); - disconnect(old, &ViewerOutput::removed_from_graph, this, - &TimeBasedWidget::connected_node_removed_from_graph); - disconnect(old, &ViewerOutput::playhead_changed, this, - &TimeBasedWidget::playhead_time_changed); - - // Disconnect rate change signals if they were connected - disconnect(old, &ViewerOutput::frame_rate_changed, this, - &TimeBasedWidget::auto_update_timebase); - disconnect(old, &ViewerOutput::sample_rate_changed, this, - &TimeBasedWidget::auto_update_timebase); - // Reset timebase to null SetTimebase(Rational()); @@ -137,13 +138,28 @@ void TimeBasedWidget::connect_viewer_node(ViewerOutput *node) ConnectedNodeChangeEvent(viewer_node_.data()); if (viewer_node_) { - // Connect length changed signal - connect(viewer_node_.data(), &ViewerOutput::length_changed, this, - &TimeBasedWidget::update_maximum_scroll); - connect(viewer_node_.data(), &ViewerOutput::removed_from_graph, this, - &TimeBasedWidget::connected_node_removed_from_graph); - connect(viewer_node_.data(), &ViewerOutput::playhead_changed, this, - &TimeBasedWidget::playhead_time_changed); + OakEngineNode *handle = + reinterpret_cast(viewer_node_.data()); + + // Subscribe to viewer events via bridge + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH); + + connect(bridge_, &EngineEventBridge::viewer_length_changed, this, + [this](OakEngineNode *, qint64, qint64) { + update_maximum_scroll(); + }); + connect(bridge_, &EngineEventBridge::viewer_playhead_changed, this, + [this](OakEngineNode *, qint64 num, qint64 den) { + playhead_time_changed(Rational(num, den)); + }); + + // Node removed from graph - use the bridge signal + connect(bridge_, &EngineEventBridge::node_removed_from_graph, this, + [this](OakEngineNode *, OakEngineNode *) { + connected_node_removed_from_graph(); + }); // Connect ruler and scrollbar to timeline points connect_work_area(viewer_node_->get_work_area()); @@ -152,10 +168,16 @@ void TimeBasedWidget::connect_viewer_node(ViewerOutput *node) // If we're setting the timebase, set it automatically based on the video and audio parameters if (auto_set_timebase_) { auto_update_timebase(); - connect(viewer_node_.data(), &ViewerOutput::frame_rate_changed, this, - &TimeBasedWidget::auto_update_timebase); - connect(viewer_node_.data(), &ViewerOutput::sample_rate_changed, this, - &TimeBasedWidget::auto_update_timebase); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED); + connect(bridge_, &EngineEventBridge::viewer_frame_rate_changed, this, + [this](OakEngineNode *, qint64, qint64) { + auto_update_timebase(); + }); + connect(bridge_, &EngineEventBridge::viewer_sample_rate_changed, this, + [this](OakEngineNode *, int) { + auto_update_timebase(); + }); } // Call derivatives @@ -164,7 +186,8 @@ void TimeBasedWidget::connect_viewer_node(ViewerOutput *node) update_maximum_scroll(); - emit connected_node_changed(old, node); + emit connected_node_changed(reinterpret_cast(old), + reinterpret_cast(node)); } void TimeBasedWidget::connect_work_area(TimelineWorkArea *workarea) @@ -287,13 +310,13 @@ void TimeBasedWidget::auto_update_timebase() return; } Rational video_tb = - viewer_node_->get_video_params().frame_rate_as_time_base(); + viewer_output_video_params(viewer_node_).frame_rate_as_time_base(); if (!video_tb.isNull()) { SetTimebase(video_tb); } else { Rational audio_tb = - viewer_node_->get_audio_params().sample_rate_as_time_base(); + viewer_output_audio_params(viewer_node_).sample_rate_as_time_base(); if (!audio_tb.isNull()) { SetTimebase(audio_tb); @@ -489,7 +512,9 @@ void TimeBasedWidget::go_to_prev_cut() closest_cut = qMax(closest_cut, this_track_closest_cut); } - get_connected_node()->set_playhead(closest_cut); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + closest_cut.numerator(), closest_cut.denominator()); } void TimeBasedWidget::go_to_next_cut() @@ -521,14 +546,17 @@ void TimeBasedWidget::go_to_next_cut() } if (closest_cut < RATIONAL_MAX) { - get_connected_node()->set_playhead(closest_cut); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + closest_cut.numerator(), closest_cut.denominator()); } } void TimeBasedWidget::go_to_start() { if (viewer_node_) { - viewer_node_->set_playhead(0); + oakengine_viewer_set_playhead( + reinterpret_cast(viewer_node_.data()), 0, 1); } } @@ -542,7 +570,12 @@ void TimeBasedWidget::prev_frame() // Catch rounding error, assume this time is snapped and just subtract a timebase proposed_time -= timebase(); } - viewer_node_->set_playhead(qMax(Rational(0), proposed_time)); + { + Rational _pt = qMax(Rational(0), proposed_time); + oakengine_viewer_set_playhead( + reinterpret_cast(viewer_node_.data()), + _pt.numerator(), _pt.denominator()); + } } } @@ -556,14 +589,19 @@ void TimeBasedWidget::next_frame() // Catch rounding error, assume this time is snapped and just add a timebase proposed_time += timebase(); } - viewer_node_->set_playhead(proposed_time); + oakengine_viewer_set_playhead( + reinterpret_cast(viewer_node_.data()), + proposed_time.numerator(), proposed_time.denominator()); } } void TimeBasedWidget::go_to_end() { if (viewer_node_) { - viewer_node_->set_playhead(viewer_node_->get_length()); + oakengine_viewer_set_playhead( + reinterpret_cast(viewer_node_.data()), + viewer_node_->get_length().numerator(), + viewer_node_->get_length().denominator()); } } @@ -587,13 +625,13 @@ void TimeBasedWidget::set_point(Timeline::MovementMode m, const Rational &time) return; } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); TimelineWorkArea *points = viewer_node_->get_work_area(); // Enable workarea if it isn't already enabled if (!points->enabled()) { - command->add_child(new WorkareaSetEnabledCommand( - viewer_node_->project(), points, true)); + oakengine_workarea_set_enabled_undoable( + reinterpret_cast(points), 1, command); } // Determine our new range @@ -603,7 +641,7 @@ void TimeBasedWidget::set_point(Timeline::MovementMode m, const Rational &time) in_point = time; if (!points->enabled() || points->out() < in_point) { - out_point = TimelineWorkArea::k_reset_out; + out_point = RATIONAL_MAX; } else { out_point = points->out(); } @@ -611,17 +649,28 @@ void TimeBasedWidget::set_point(Timeline::MovementMode m, const Rational &time) out_point = time; if (!points->enabled() || points->in() > out_point) { - in_point = TimelineWorkArea::k_reset_in; + in_point = Rational(0, 1); } else { in_point = points->in(); } } // Set workarea - command->add_child( - new WorkareaSetRangeCommand(points, TimeRange(in_point, out_point))); + { + int64_t old_in_num, old_in_den, old_out_num, old_out_den; + int old_enabled; + oakengine_workarea_get( + reinterpret_cast(points), + &old_in_num, &old_in_den, &old_out_num, &old_out_den, + &old_enabled); + oakengine_workarea_set_range_undoable( + reinterpret_cast(points), + in_point.numerator(), in_point.denominator(), + out_point.numerator(), out_point.denominator(), + old_in_num, old_in_den, old_out_num, old_out_den, command); + } - Core::instance()->undo_stack()->push(command, tr("Set In/Out Point")); + oakengine_undo_push(command, tr("Set In/Out Point").toUtf8().constData()); } void TimeBasedWidget::reset_point(Timeline::MovementMode m) @@ -639,13 +688,27 @@ void TimeBasedWidget::reset_point(Timeline::MovementMode m) TimeRange r = points->range(); if (m == Timeline::k_trim_in) { - r.set_in(TimelineWorkArea::k_reset_in); + r.set_in(Rational(0, 1)); } else { - r.set_out(TimelineWorkArea::k_reset_out); + r.set_out(RATIONAL_MAX); } - Core::instance()->undo_stack()->push(new WorkareaSetRangeCommand(points, r), - tr("Reset In/Out Points")); + { + auto reset_cmd = oakengine_undo_command_create_multi(); + int64_t old_in_num, old_in_den, old_out_num, old_out_den; + int old_enabled; + oakengine_workarea_get( + reinterpret_cast(points), + &old_in_num, &old_in_den, &old_out_num, &old_out_den, + &old_enabled); + oakengine_workarea_set_range_undoable( + reinterpret_cast(points), + r.in().numerator(), r.in().denominator(), + r.out().numerator(), r.out().denominator(), + old_in_num, old_in_den, old_out_num, old_out_den, reset_cmd); + oakengine_undo_push(reset_cmd, + tr("Reset In/Out Points").toUtf8().constData()); + } } void TimeBasedWidget::page_scroll_internal(QScrollBar *bar, int maximum, @@ -717,10 +780,15 @@ void TimeBasedWidget::clear_in_out_points() return; } - Core::instance()->undo_stack()->push( - new WorkareaSetEnabledCommand(get_connected_node()->project(), - get_connected_node()->get_work_area(), false), - tr("Cleared In/Out Points")); + { + auto clear_cmd = oakengine_undo_command_create_multi(); + oakengine_workarea_set_enabled_undoable( + reinterpret_cast( + get_connected_node()->get_work_area()), + 0, clear_cmd); + oakengine_undo_push(clear_cmd, + tr("Cleared In/Out Points").toUtf8().constData()); + } } void TimeBasedWidget::set_marker() @@ -748,16 +816,20 @@ void TimeBasedWidget::set_marker() color = OAK_CONFIG("MarkerColor").toInt(); } - TimelineMarker *marker = new TimelineMarker( - color, TimeRange(get_connected_node()->get_playhead(), - get_connected_node()->get_playhead())); + const Rational playhead = get_connected_node()->get_playhead(); + OakEngineMarker *marker = oakengine_marker_create( + color, playhead.numerator(), playhead.denominator(), + playhead.numerator(), playhead.denominator(), ""); + TimelineMarker *cpp_marker = + reinterpret_cast(marker); bool edited_in_dialog = false; if (OAK_CONFIG("SetNameWithMarker").toBool()) { - MarkerPropertiesDialog mpd({ marker }, timebase(), this); + MarkerPropertiesDialog mpd({ cpp_marker }, timebase(), this); if (mpd.exec() != QDialog::Accepted) { - delete marker; + oakengine_marker_free(marker); marker = nullptr; + cpp_marker = nullptr; } else { edited_in_dialog = true; } @@ -767,19 +839,19 @@ void TimeBasedWidget::set_marker() if (edited_in_dialog) { // The dialog pushed undo commands referencing this exact // marker object, so it must be the one added to the list. - Core::instance()->undo_stack()->push( - new MarkerAddCommand(markers, marker), tr("Added Marker")); + oakengine_marker_list_add_existing( + reinterpret_cast(markers), marker); } else { // Pristine marker: add through the liboakengine C ABI // facade (one undoable command) and drop the temporary. oakengine_sequence_marker_add_ex( reinterpret_cast( get_connected_node()), - Timecode::time_to_timestamp(marker->time().in(), + Timecode::time_to_timestamp(cpp_marker->time().in(), timebase(), Timecode::k_round), - "", marker->color()); - delete marker; + "", cpp_marker->color()); + oakengine_marker_free(marker); } } } @@ -820,8 +892,10 @@ void TimeBasedWidget::go_to_in() { if (get_connected_node()) { if (get_connected_node()->get_work_area()->enabled()) { - get_connected_node()->set_playhead( - get_connected_node()->get_work_area()->in()); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + get_connected_node()->get_work_area()->in().numerator(), + get_connected_node()->get_work_area()->in().denominator()); } else { go_to_start(); } @@ -832,8 +906,10 @@ void TimeBasedWidget::go_to_out() { if (get_connected_node()) { if (get_connected_node()->get_work_area()->enabled()) { - get_connected_node()->set_playhead( - get_connected_node()->get_work_area()->out()); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + get_connected_node()->get_work_area()->out().numerator(), + get_connected_node()->get_work_area()->out().denominator()); } else { go_to_end(); } @@ -913,7 +989,7 @@ bool TimeBasedWidget::snap_point(const std::vector &start_times, TimelineMarker *marker = *jt; TimeRange marker_range = - marker->time() + clip->in() - clip->media_in(); + marker->time() + clip->in() - clip_media_in(clip); qreal marker_in_screen = time_to_scene(marker_range.in()); diff --git a/app/widget/timebased/timebasedwidget.h b/app/widget/timebased/timebasedwidget.h index d57388496..d10592036 100644 --- a/app/widget/timebased/timebasedwidget.h +++ b/app/widget/timebased/timebasedwidget.h @@ -36,6 +36,7 @@ namespace olive { +class EngineEventBridge; class TimeRuler; class TimeBasedWidget : public TimelineScaledWidget { @@ -149,6 +150,8 @@ protected: { } + EngineEventBridge *bridge_ = nullptr; + virtual void ConnectNodeEvent(ViewerOutput *) { } @@ -213,7 +216,7 @@ protected slots: signals: void timebase_changed(const Rational &); - void connected_node_changed(ViewerOutput *old, ViewerOutput *now); + void connected_node_changed(OakEngineNode *old, OakEngineNode *now); protected slots: virtual void SendCatchUpScrollEvent(); diff --git a/app/widget/timebased/timescaledobject.cpp b/app/widget/timebased/timescaledobject.cpp index 171f61106..223ac164c 100644 --- a/app/widget/timebased/timescaledobject.cpp +++ b/app/widget/timebased/timescaledobject.cpp @@ -26,6 +26,7 @@ #include "audio/audiovisualwaveform.h" +#include "oakengine/node.h" namespace olive { @@ -34,7 +35,7 @@ const int TimeScaledObject::k_calculate_dimensions_padding = 10; TimeScaledObject::TimeScaledObject() : scale_(1.0) , min_scale_(0) - , max_scale_(AudioVisualWaveform::k_maximum_sample_rate.to_double()) + , max_scale_(oakengine_audio_waveform_max_sample_rate()) { } diff --git a/app/widget/timelinewidget/cliphandle.h b/app/widget/timelinewidget/cliphandle.h new file mode 100644 index 000000000..d82db5e8f --- /dev/null +++ b/app/widget/timelinewidget/cliphandle.h @@ -0,0 +1,210 @@ +/*** + + 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_CLIPHANDLE_H +#define OAK_CLIPHANDLE_H + +#include + +#include "node/block/clip/clip.h" +#include "oakengine/node.h" +#include "oakengine/timeline.h" + +namespace olive +{ + +/** + * @brief Facade accessors for ClipBlock pointers held by the timeline UI. + * + * ClipBlock's header-inline convenience accessors (speed()/loop_mode()/ + * thumbnails()/waveform()/connected_video_cache()/...) reference the + * input-id statics (k_speed_input/k_buffer_in/...), which are engine + * symbols the app must no longer pull across the liboakengine boundary. + * These helpers route the same queries through the C ABI instead (same + * pattern as app/widget/keyframeview/keyframehandle.h). The ClipBlock* + * itself stays an opaque identity pointer. + */ + +inline OakEngineClip *cliphandle(ClipBlock *clip) +{ + return reinterpret_cast(clip); +} + +/** @brief The node feeding the clip's buffer input (ClipBlock's inline + * get_connected_output(k_buffer_in) uses; borrowed, may be null). */ +inline Node *clip_connected_node(ClipBlock *clip) +{ + return reinterpret_cast( + oakengine_node_input_get_connected_node( + reinterpret_cast(clip), + oakengine_clip_buffer_input_id(), -1)); +} + +inline FrameHashCache *clip_thumbnails(ClipBlock *clip) +{ + Node *n = clip_connected_node(clip); + return n ? n->thumbnail_cache() : nullptr; +} + +inline AudioWaveformCache *clip_waveform(ClipBlock *clip) +{ + Node *n = clip_connected_node(clip); + return n ? n->waveform_cache() : nullptr; +} + +inline FrameHashCache *clip_connected_video_cache(ClipBlock *clip) +{ + Node *n = clip_connected_node(clip); + return n ? n->video_frame_cache() : nullptr; +} + +/** @brief ClipBlock::speed() through the facade input getter. */ +inline double clip_speed(ClipBlock *clip) +{ + oak_node_value v; + memset(&v, 0, sizeof(v)); + if (oakengine_node_get_input(reinterpret_cast(clip), + oakengine_clip_speed_input_id(), + &v) != OAKENGINE_OK) { + return 1.0; + } + return v.f[0]; +} + +/** @brief ClipBlock::loop_mode() value (an olive::LoopMode int). */ +inline int clip_loop_mode(ClipBlock *clip) +{ + oak_node_value v; + memset(&v, 0, sizeof(v)); + if (oakengine_node_get_input(reinterpret_cast(clip), + oakengine_clip_loop_mode_input_id(), + &v) != OAKENGINE_OK) { + return 0; + } + return int(v.num); +} + +/** @brief ClipBlock::is_reversed() through the facade input getter. */ +inline bool clip_is_reversed(ClipBlock *clip) +{ + oak_node_value v; + memset(&v, 0, sizeof(v)); + return oakengine_node_get_input(reinterpret_cast(clip), + oakengine_clip_reverse_input_id(), + &v) == OAKENGINE_OK && + v.num != 0; +} + +/** @brief ClipBlock::maintain_audio_pitch() through the facade. */ +inline bool clip_maintain_audio_pitch(ClipBlock *clip) +{ + oak_node_value v; + memset(&v, 0, sizeof(v)); + return oakengine_node_get_input( + reinterpret_cast(clip), + oakengine_clip_maintain_audio_pitch_input_id(), &v) == + OAKENGINE_OK && + v.num != 0; +} + +/** @brief Create a new empty ClipBlock through the C ABI. */ +inline ClipBlock *clip_create_empty(const char *label = nullptr) +{ + return reinterpret_cast(oakengine_clip_create_empty(label)); +} + +/** @brief ClipBlock::media_in() through the facade. */ +inline Rational clip_media_in(ClipBlock *clip) +{ + int64_t num = 0, den = 1; + if (oakengine_clip_get_media_in_rational(cliphandle(clip), &num, &den) == + OAKENGINE_OK) { + return Rational(static_cast(num), static_cast(den)); + } + return Rational(0, 1); +} + +/** @brief ClipBlock::media_range() through the facade. */ +inline TimeRange clip_media_range(ClipBlock *clip) +{ + int64_t in_num = 0, in_den = 1, out_num = 0, out_den = 1; + if (oakengine_clip_get_media_range_rational( + cliphandle(clip), &in_num, &in_den, &out_num, &out_den) == + OAKENGINE_OK) { + return TimeRange(Rational(static_cast(in_num), + static_cast(in_den)), + Rational(static_cast(out_num), + static_cast(out_den))); + } + return TimeRange(0, 0); +} + +/** @brief Set the clip's media in-point directly (rational seconds). */ +inline void clip_set_media_in(ClipBlock *clip, const Rational &media_in, + bool undoable = false) +{ + if (!clip) { + return; + } + oakengine_clip_set_media_in_rational(cliphandle(clip), + media_in.numerator(), + media_in.denominator(), + undoable ? 1 : 0); +} + +/** @brief ClipBlock::is_autocaching() through the facade. */ +inline bool clip_is_autocaching(ClipBlock *clip) +{ + oak_node_value v; + memset(&v, 0, sizeof(v)); + return oakengine_node_get_input( + reinterpret_cast(clip), + oakengine_clip_auto_cache_input_id(), &v) == + OAKENGINE_OK && + v.num != 0; +} + +/** @brief ClipBlock::request_invalidated_from_connected() through the facade. */ +inline void clip_request_invalidate_connected( + ClipBlock *clip, bool force_all = false, + const TimeRange &intersect = TimeRange()) +{ + if (!clip) { + return; + } + + int64_t in_num = 0, in_den = 0, out_num = 0, out_den = 0; + if (!intersect.length().isNull()) { + const Rational in = intersect.in(); + const Rational out = intersect.out(); + in_num = in.numerator(); + in_den = in.denominator(); + out_num = out.numerator(); + out_den = out.denominator(); + } + + oakengine_clip_request_invalidate_connected(cliphandle(clip), force_all ? 1 : 0, + in_num, in_den, out_num, + out_den); +} + +} // namespace olive + +#endif // OAK_CLIPHANDLE_H diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 23a3a0d5c..afab2e2d8 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -36,22 +36,27 @@ #include #include -#include "audio/audiosynchronizer.h" -#include "audio/audiowaveformsync.h" -#include "codec/proxymanager.h" +#include "oakengine/audio.h" #include "core.h" +#include "engineeventbridge.h" #include "common/range.h" #include "dialog/proxy/proxydialog.h" #include "dialog/sequence/sequence.h" #include "dialog/speedduration/speeddurationdialog.h" #include "node/block/transition/transition.h" -#include "node/nodeundo.h" #include "node/project/footage/footage.h" -#include "node/project/serializer/serializer.h" +#include "oakengine/serializer.h" +#include "oakengine/undo.h" +#include "oakengine/events.h" +#include "oakengine/footage.h" #include "oakengine/node.h" +#include "oakengine/project.h" +#include "oakengine/proxy.h" #include "oakengine/timeline.h" +#include "oakengine/viewer.h" #include "render/audiowaveformcache.h" #include "task/project/import/import.h" +#include "common/configwrapper.h" #include "timeline/timelineundogeneral.h" #include "timeline/timelineundopointer.h" #include "timeline/timelineundoripple.h" @@ -70,11 +75,13 @@ #include "tool/zoom.h" #include "tool/tool.h" #include "trackview/trackview.h" +#include "widget/timelinewidget/cliphandle.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" #include "widget/nodeparamview/nodeparamview.h" #include "widget/timeruler/timeruler.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -87,7 +94,7 @@ namespace struct SourceSyncClip { ClipBlock *clip = nullptr; - AudioSynchronizer::SourceClip source; + oak_audio_sync_source_clip source; Rational source_head; }; @@ -104,10 +111,14 @@ bool get_source_sync_clip(Block *block, SourceSyncClip *out) } out->clip = clip; - out->source.source_start_time = footage->source_start_time(); - out->source.media_in = clip->media_in(); - out->source.has_source_start_time = true; - out->source_head = out->source.source_start_time + out->source.media_in; + const Rational source_start = footage->source_start_time(); + const Rational media_in = clip_media_in(clip); + out->source.source_start_time_num = source_start.numerator(); + out->source.source_start_time_den = source_start.denominator(); + out->source.media_in_num = media_in.numerator(); + out->source.media_in_den = media_in.denominator(); + out->source.has_source_start_time = 1; + out->source_head = source_start + media_in; return true; } @@ -134,7 +145,11 @@ QVector get_selected_proxy_footage(const QVector &blocks) } Footage *candidate = dynamic_cast(clip->connected_viewer()); - if (!candidate || !candidate->get_first_enabled_video_stream().is_valid() || + oak_video_params _vp; + if (!candidate || + oakengine_viewer_get_first_enabled_video_stream( + reinterpret_cast(candidate), &_vp) < 0 || + !oakengine_video_params_is_valid(&_vp) || footage.contains(candidate)) { continue; } @@ -287,11 +302,18 @@ TimelineWidget::TimelineWidget(QWidget *parent) start = std::min(start, b->in()); } if (start != RATIONAL_MAX) { - get_connected_node()->set_playhead(start); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + start.numerator(), start.denominator()); } } - emit block_selection_changed(selected_blocks_); + QVector oak_blocks; + oak_blocks.reserve(selected_blocks_.size()); + for (Block *b : selected_blocks_) { + oak_blocks.append(reinterpret_cast(b)); + } + emit block_selection_changed(oak_blocks); }); } @@ -304,7 +326,7 @@ TimelineWidget::~TimelineWidget() qDeleteAll(tools_); - delete subtitle_show_command_; + oakengine_undo_command_free(subtitle_show_command_); } void TimelineWidget::clear() @@ -383,23 +405,122 @@ void TimelineWidget::ScaleChangedEvent(const double &scale) void TimelineWidget::ConnectNodeEvent(ViewerOutput *n) { Sequence *s = static_cast(n); + OakEngineNode *handle = reinterpret_cast(s); - connect(s, &Sequence::track_added, this, &TimelineWidget::add_track); - connect(s, &Sequence::track_removed, this, &TimelineWidget::remove_track); - connect(s, &Sequence::frame_rate_changed, this, - &TimelineWidget::frame_rate_changed); - connect(s, &Sequence::sample_rate_changed, this, - &TimelineWidget::sample_rate_changed); + // Track add/remove are now received via bridge sequence_track_* signals + bridge_->subscribe(handle, OAKENGINE_EVENT_SEQUENCE_TRACK_ADDED); + bridge_->subscribe(handle, OAKENGINE_EVENT_SEQUENCE_TRACK_REMOVED); + bridge_->subscribe(handle, OAKENGINE_EVENT_SEQUENCE_TRACK_LIST_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_SEQUENCE_TRACK_HEIGHT_CHANGED); - connect(timecode_label_, &RationalSlider::value_changed, s, - &Sequence::set_playhead); - connect(s, &Sequence::playhead_changed, timecode_label_, - &RationalSlider::set_value); - timecode_label_->set_value(s->get_playhead()); + connect(bridge_, &EngineEventBridge::sequence_track_added, this, + [this](OakEngineTrack *track, int track_type) { + Track *t = reinterpret_cast(track); + add_track(t); + // Update the TrackView UI for this track type + if (track_type >= 0 && track_type < views_.size()) { + views_.at(track_type)->track_view()->insert_track(t); + } + }); + connect(bridge_, &EngineEventBridge::sequence_track_removed, this, + [this](OakEngineTrack *track, int track_type) { + Track *t = reinterpret_cast(track); + remove_track(t); + // Update the TrackView UI for this track type + if (track_type >= 0 && track_type < views_.size()) { + views_.at(track_type)->track_view()->remove_track(t); + } + }); - ruler()->set_playback_cache(n->video_frame_cache()); + connect(bridge_, &EngineEventBridge::sequence_track_list_changed, this, + [this](OakEngineSequence *, int track_type) { + if (track_type >= 0 && track_type < views_.size()) { + views_.at(track_type)->view()->track_list_changed(); + } + }); + connect(bridge_, &EngineEventBridge::sequence_track_height_changed, this, + [this](OakEngineSequence *, OakEngineTrack *, int track_type, int) { + if (track_type >= 0 && track_type < views_.size()) { + views_.at(track_type)->view()->track_list_changed(); + } + }); - SetTimebase(n->get_video_params().frame_rate_as_time_base()); + // Subscribe to track-level events via bridge (subscriptions in add_track) + connect(bridge_, &EngineEventBridge::track_index_changed, this, + [this](OakEngineTrack *source, int old_index, int new_index) { + Track *track = reinterpret_cast(source); + track_updated(track->type()); + track_index_changed(track, old_index, new_index); + }); + connect(bridge_, &EngineEventBridge::track_height_changed, this, + [this](OakEngineTrack *source, double) { + track_updated(reinterpret_cast(source)->type()); + }); + connect(bridge_, &EngineEventBridge::track_blocks_refreshed, this, + [this](OakEngineTrack *source) { + track_updated(reinterpret_cast(source)->type()); + }); + connect(bridge_, &EngineEventBridge::track_block_added, this, + [this](OakEngineBlock *block, qint64, qint64) { + add_block(reinterpret_cast(block)); + }); + connect(bridge_, &EngineEventBridge::track_block_removed, this, + [this](OakEngineBlock *block, qint64, qint64) { + remove_block(reinterpret_cast(block)); + }); + + // Block-level change notifications via bridge (replaces direct + // connect(block, &Block::..._changed, ...) to avoid pulling Block's + // staticMetaObject across the C ABI boundary). + connect(bridge_, &EngineEventBridge::block_enabled_changed, this, + [this](OakEngineBlock *) { block_updated(); }); + connect(bridge_, &EngineEventBridge::block_preview_changed, this, + [this](OakEngineBlock *) { block_updated(); }); + connect(bridge_, &EngineEventBridge::node_label_changed, this, + [this](OakEngineNode *) { block_updated(); }); + connect(bridge_, &EngineEventBridge::node_links_changed, this, + [this](OakEngineNode *) { block_updated(); }); + connect(bridge_, &EngineEventBridge::node_color_changed, this, + [this](OakEngineNode *) { block_updated(); }); + + // Subscribe to viewer events via bridge + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_FRAME_RATE_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_SAMPLE_RATE_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED); + + connect(bridge_, &EngineEventBridge::viewer_frame_rate_changed, this, + [this](OakEngineNode *, qint64, qint64) { + frame_rate_changed(); + }); + connect(bridge_, &EngineEventBridge::viewer_sample_rate_changed, this, + [this](OakEngineNode *, int) { + sample_rate_changed(); + }); + connect(bridge_, &EngineEventBridge::viewer_playhead_changed, this, + [this](OakEngineNode *, qint64 num, qint64 den) { + this->timecode_label_->set_value(Rational(num, den)); + }); + + connect(timecode_label_, &RationalSlider::value_changed, this, + [handle](const Rational &time) { + oakengine_viewer_set_playhead( + handle, time.numerator(), time.denominator()); + }); + { + int64_t pn, pd; + oakengine_viewer_get_playhead(handle, &pn, &pd); + timecode_label_->set_value(Rational(pn, pd)); + } + + ruler()->set_playback_cache( + reinterpret_cast( + oakengine_viewer_get_playback_cache(handle))); + + { + oak_video_params vp; + oakengine_viewer_get_video_params(handle, 0, &vp); + SetTimebase(Rational(vp.time_base_den, vp.time_base_num)); + } for (int i = 0; i < views_.size(); i++) { Track::Type track_type = static_cast(i); @@ -422,15 +543,8 @@ void TimelineWidget::DisconnectNodeEvent(ViewerOutput *n) { Sequence *s = static_cast(n); - disconnect(s, &Sequence::track_added, this, &TimelineWidget::add_track); - disconnect(s, &Sequence::track_removed, this, &TimelineWidget::remove_track); - disconnect(s, &Sequence::frame_rate_changed, this, - &TimelineWidget::frame_rate_changed); - disconnect(s, &Sequence::sample_rate_changed, this, - &TimelineWidget::sample_rate_changed); - - disconnect(timecode_label_, &RationalSlider::value_changed, s, - &Sequence::set_playhead); + // Bridge subscriptions and connections are cleaned up by + // TimeBasedWidget::connect_viewer_node (disconnect(bridge_, nullptr, this, nullptr)) deselect_all(); @@ -577,7 +691,7 @@ void TimelineWidget::split_at_playhead() void TimelineWidget::replace_blocks_with_gaps(const QVector &blocks, bool remove_from_graph, - MultiUndoCommand *command, + void *command, bool handle_transitions) { foreach (Block *b, blocks) { @@ -589,12 +703,21 @@ void TimelineWidget::replace_blocks_with_gaps(const QVector &blocks, Track *original_track = b->track(); - command->add_child(new TrackReplaceBlockWithGapCommand( - original_track, b, handle_transitions)); + oakengine_undo_command_multi_add_child(command, oakengine_track_replace_block_with_gap_command(reinterpret_cast(original_track), reinterpret_cast(b), handle_transitions ? 1 : 0)); if (remove_from_graph) { - command->add_child( - new NodeRemoveWithExclusiveDependenciesAndDisconnect(b)); + void *remove_cmd = oakengine_undo_command_create_multi(); + oakengine_undo_command_multi_add_child( + remove_cmd, + oakengine_node_remove_and_disconnect_command( + reinterpret_cast(b))); + for (Node *dep : b->get_exclusive_dependencies()) { + oakengine_undo_command_multi_add_child( + remove_cmd, + oakengine_node_remove_and_disconnect_command( + reinterpret_cast(dep))); + } + oakengine_undo_command_multi_add_child(command, remove_cmd); } } } @@ -634,28 +757,27 @@ void TimelineWidget::DeleteSelected(bool ripple) ripple = true; } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); // Remove all selections - command->add_child(new SetSelectionsCommand( - this, TimelineWidgetSelections(), get_selections())); + oakengine_undo_command_multi_add_child(command, create_set_selections_command(TimelineWidgetSelections(), get_selections())); // For transitions, remove them but extend their attached blocks to fill their place foreach (TransitionBlock *transition, transitions_to_delete) { - TransitionRemoveCommand *trc = - new TransitionRemoveCommand(transition, true); + void *trc = oakengine_transition_remove_command( + reinterpret_cast(transition), 1); // Perform the transition removal now so that replacing blocks with gaps below won't get confused - trc->redo_now(); + oakengine_undo_command_redo_now(trc); - command->add_child(trc); + oakengine_undo_command_multi_add_child(command, trc); } // Selection clearing and transition removal stay app-side (selection // state and transition commands have no facade equivalent); the clip // deletion core below goes through the facade and lands as one undoable // command right after this one, keeping the undo order intact. - Core::instance()->undo_stack()->push(command, tr("Deleted Clips")); + oakengine_undo_push(command, tr("Deleted Clips").toUtf8().constData()); // Delete the clips through the liboakengine C ABI facade (gap // replacement + graph removal, optionally rippling the selected ranges @@ -692,7 +814,9 @@ void TimelineWidget::DeleteSelected(bool ripple) clear_ghosts(); if (ripple && rippled && new_playhead != RATIONAL_MAX) { - get_connected_node()->set_playhead(new_playhead); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + new_playhead.numerator(), new_playhead.denominator()); } } @@ -704,7 +828,14 @@ void TimelineWidget::increase_track_height() // Increase the height of each track by one "unit" foreach (Track *t, sequence()->get_tracks()) { - t->set_track_height(t->get_track_height() + Track::k_track_height_interval); + double h; + oakengine_track_get_height( + reinterpret_cast(sequence()), + t->type(), t->index(), &h); + oakengine_track_set_height( + reinterpret_cast(sequence()), + t->type(), t->index(), + h + oakengine_track_height_interval()); } } @@ -716,30 +847,46 @@ void TimelineWidget::decrease_track_height() // Decrease the height of each track by one "unit" foreach (Track *t, sequence()->get_tracks()) { - t->set_track_height( - qMax(t->get_track_height() - Track::k_track_height_interval, - Track::k_track_height_minimum)); + double h; + oakengine_track_get_height( + reinterpret_cast(sequence()), + t->type(), t->index(), &h); + oakengine_track_set_height( + reinterpret_cast(sequence()), + t->type(), t->index(), + qMax(h - oakengine_track_height_interval(), + oakengine_track_height_minimum())); } } void TimelineWidget::insert_footage_at_playhead( - const QVector &footage) + const QVector &footage) { - auto command = new MultiUndoCommand(); - import_tool_->place_at(footage, get_connected_node()->get_playhead(), true, + auto command = oakengine_undo_command_create_multi(); + QVector viewer_footage; + viewer_footage.reserve(footage.size()); + foreach (OakEngineNode *handle, footage) { + viewer_footage.append(reinterpret_cast(handle)); + } + import_tool_->place_at(viewer_footage, get_connected_node()->get_playhead(), true, command, 0, true); - Core::instance()->undo_stack()->push(command, - tr("Inserted Footage At Playhead")); + oakengine_undo_push(command, + tr("Inserted Footage At Playhead").toUtf8().constData()); } void TimelineWidget::overwrite_footage_at_playhead( - const QVector &footage) + const QVector &footage) { - auto command = new MultiUndoCommand(); - import_tool_->place_at(footage, get_connected_node()->get_playhead(), false, + auto command = oakengine_undo_command_create_multi(); + QVector viewer_footage; + viewer_footage.reserve(footage.size()); + foreach (OakEngineNode *handle, footage) { + viewer_footage.append(reinterpret_cast(handle)); + } + import_tool_->place_at(viewer_footage, get_connected_node()->get_playhead(), false, command, 0, true); - Core::instance()->undo_stack()->push(command, - tr("Overwrote Footage At Playhead")); + oakengine_undo_push(command, + tr("Overwrote Footage At Playhead").toUtf8().constData()); } void TimelineWidget::toggle_links_on_selected() @@ -825,27 +972,34 @@ bool TimelineWidget::copy_selected(bool cut) } } - ProjectSerializer::SaveData sdata(ProjectSerializer::k_only_clips); - sdata.set_only_serialize_nodes_and_resolve_groups(selected_nodes); + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_CLIPS, nullptr, nullptr); + + oakengine_clipboard_set_nodes( + cb, + reinterpret_cast( + selected_nodes.constData()), + selected_nodes.size()); // Cache the earliest in point so all copied clips have a "relative" in point that can be pasted anywhere Rational earliest_in = RATIONAL_MAX; - ProjectSerializer::SerializedProperties properties; foreach (Block *block, selected_blocks_) { earliest_in = qMin(earliest_in, block->in()); } foreach (Block *block, selected_blocks_) { - properties[block][QStringLiteral("in")] = - QString::fromStdString((block->in() - earliest_in).to_string()); - properties[block][QStringLiteral("track")] = - block->track()->to_reference().to_string(); + oakengine_clipboard_set_property( + cb, reinterpret_cast(block), "in", + (block->in() - earliest_in).to_string().c_str()); + QString track_ref = block->track()->to_reference().to_string(); + oakengine_clipboard_set_property( + cb, reinterpret_cast(block), "track", + track_ref.toUtf8().constData()); } - sdata.set_properties(properties); - - ProjectSerializer::copy(sdata); + oakengine_clipboard_copy(cb); + oakengine_clipboard_free(cb); if (cut) { DeleteSelected(); @@ -900,7 +1054,9 @@ void TimelineWidget::delete_in_to_out(bool ripple) // Playhead move is not undoable and stays here (same as before). if (ripple) { - get_connected_node()->set_playhead(wa_in); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + wa_in.numerator(), wa_in.denominator()); } } @@ -1007,11 +1163,16 @@ void TimelineWidget::synchronize_selected_clips_by_source_time() QVector placements; for (const SourceSyncClip &sync_clip : sync_clips) { - const AudioSynchronizer::Placement placement = - AudioSynchronizer::place_by_source_time( - reference.source, sync_clip.source, anchor_timeline_in); - if (placement.valid) { - placements.append({ sync_clip.clip, placement.timeline_in }); + oak_audio_sync_placement placement; + if (oakengine_audio_sync_place_by_source_time( + &reference.source, &sync_clip.source, + anchor_timeline_in.numerator(), + anchor_timeline_in.denominator(), + &placement) == OAKENGINE_OK && + placement.valid) { + placements.append({ sync_clip.clip, + Rational(int(placement.timeline_in_num), + int(placement.timeline_in_den)) }); } } @@ -1019,29 +1180,24 @@ void TimelineWidget::synchronize_selected_clips_by_source_time() return; } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); for (const SyncPlacement &placement : placements) { - command->add_child(new TrackReplaceBlockWithGapCommand( - placement.clip->track(), placement.clip, false)); + oakengine_undo_command_multi_add_child(command, oakengine_track_replace_block_with_gap_command(reinterpret_cast(placement.clip->track()), reinterpret_cast(placement.clip), false ? 1 : 0)); } TimelineWidgetSelections new_selections; for (const SyncPlacement &placement : placements) { - command->add_child(new TrackPlaceBlockCommand( - sequence()->track_list(placement.clip->track()->type()), - placement.clip->track()->index(), placement.clip, - placement.timeline_in)); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence()->track_list(placement.clip->track()->type())), placement.clip->track()->index(), reinterpret_cast(placement.clip), core::Timecode::time_to_timestamp(placement.timeline_in, timebase()))); new_selections[placement.clip->track()->to_reference()].insert( TimeRange(placement.timeline_in, placement.timeline_in + placement.clip->length())); } - command->add_child( - new SetSelectionsCommand(this, new_selections, get_selections())); + oakengine_undo_command_multi_add_child(command, create_set_selections_command(new_selections, get_selections())); - Core::instance()->undo_stack()->push( - command, tr("Synchronize Clips by Source Time")); + oakengine_undo_push( + command, tr("Synchronize Clips by Source Time").toUtf8().constData()); } void TimelineWidget::synchronize_selected_clips_by_waveform() @@ -1114,10 +1270,15 @@ void TimelineWidget::synchronize_selected_clips_by_waveform_internal( // Skip uncached (zero-filled) windows on both sides so partially // cached waveforms don't drag the correlation down - AudioWaveformSync::OffsetResult offset = - AudioWaveformSync::estimate_envelope_offset( - reference_envelope, candidate_envelope, reference_valid, - candidate_valid, window_samples, max_offset_windows); + oak_audio_waveform_offset offset; + oakengine_audio_estimate_envelope_offset( + reference_envelope.constData(), reference_envelope.size(), + candidate_envelope.constData(), candidate_envelope.size(), + reference_valid.isEmpty() ? nullptr : reference_valid.constData(), + reference_valid.size(), + candidate_valid.isEmpty() ? nullptr : candidate_valid.constData(), + candidate_valid.size(), window_samples, max_offset_windows, + &offset); double speed = 1.0; @@ -1130,11 +1291,15 @@ void TimelineWidget::synchronize_selected_clips_by_waveform_internal( max_offset_windows, (static_cast(sample_rate) * 30) / static_cast(window_samples)); - const AudioWaveformSync::StretchOffsetResult stretch = - AudioWaveformSync::estimate_stretch_and_offset( - reference_envelope, candidate_envelope, reference_valid, - candidate_valid, window_samples, stretch_radius_windows, - 0.75, 1.34, 0.005); + oak_audio_waveform_stretch_offset stretch; + oakengine_audio_estimate_stretch_and_offset( + reference_envelope.constData(), reference_envelope.size(), + candidate_envelope.constData(), candidate_envelope.size(), + reference_valid.isEmpty() ? nullptr : reference_valid.constData(), + reference_valid.size(), + candidate_valid.isEmpty() ? nullptr : candidate_valid.constData(), + candidate_valid.size(), window_samples, stretch_radius_windows, + 0.75, 1.34, 0.005, &stretch); qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: " "stretch estimate valid=" << stretch.valid << "rate=" << stretch.rate @@ -1158,14 +1323,22 @@ void TimelineWidget::synchronize_selected_clips_by_waveform_internal( continue; } - const AudioSynchronizer::Placement placement = - AudioSynchronizer::place_by_waveform_offset( - reference.clip->in(), offset.offset_samples, sample_rate); + oak_audio_sync_placement placement; + oakengine_audio_sync_place_by_waveform_offset( + reference.clip->in().numerator(), reference.clip->in().denominator(), + offset.offset_samples, sample_rate, &placement); qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: placement" << "valid=" << placement.valid << "timeline_in=" - << placement.timeline_in.to_double(); - if (placement.valid && placement.timeline_in >= 0) { - placements.append({ sync_clip.clip, placement.timeline_in, speed }); + << (placement.valid ? Rational(int(placement.timeline_in_num), + int(placement.timeline_in_den)) + .to_double() + : 0.0); + if (placement.valid) { + const Rational timeline_in(int(placement.timeline_in_num), + int(placement.timeline_in_den)); + if (timeline_in >= 0) { + placements.append({ sync_clip.clip, timeline_in, speed }); + } } } @@ -1177,25 +1350,25 @@ void TimelineWidget::synchronize_selected_clips_by_waveform_internal( return; } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); for (const SyncPlacement &placement : placements) { - command->add_child(new TrackReplaceBlockWithGapCommand( - placement.clip->track(), placement.clip, false)); + oakengine_undo_command_multi_add_child(command, oakengine_track_replace_block_with_gap_command(reinterpret_cast(placement.clip->track()), reinterpret_cast(placement.clip), false ? 1 : 0)); if (placement.speed != 1.0) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(placement.clip, ClipBlock::k_speed_input)), - placement.clip->speed() * placement.speed)); + oak_node_value v{}; + v.type = OAK_NODE_VALUE_FLOAT; + v.f[0] = clip_speed(placement.clip) * placement.speed; + oakengine_undo_command_multi_add_child( + command, + oakengine_node_set_standard_value_command( + reinterpret_cast(placement.clip), + oakengine_clip_speed_input_id(), 0, 0, &v)); } } TimelineWidgetSelections new_selections; for (const SyncPlacement &placement : placements) { - command->add_child(new TrackPlaceBlockCommand( - sequence()->track_list(placement.clip->track()->type()), - placement.clip->track()->index(), placement.clip, - placement.timeline_in)); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence()->track_list(placement.clip->track()->type())), placement.clip->track()->index(), reinterpret_cast(placement.clip), core::Timecode::time_to_timestamp(placement.timeline_in, timebase()))); // A speed change scales the clip's timeline length accordingly const Rational placed_length = @@ -1208,20 +1381,19 @@ void TimelineWidget::synchronize_selected_clips_by_waveform_internal( placement.timeline_in + placed_length)); } - command->add_child( - new SetSelectionsCommand(this, new_selections, get_selections())); + oakengine_undo_command_multi_add_child(command, create_set_selections_command(new_selections, get_selections())); - Core::instance()->undo_stack()->push(command, - tr("Synchronize Clips by Waveform")); + oakengine_undo_push(command, + tr("Synchronize Clips by Waveform").toUtf8().constData()); Core::instance()->show_status_bar_message( tr("Synchronized %1 clip(s) by waveform").arg(placements.size())); } void TimelineWidget::generate_proxies_for_selected_clips() { - if (!ProxyManager::instance() || !sequence()) { + if (!sequence()) { qWarning() - << "GenerateProxiesForSelectedClips: ProxyManager or sequence unavailable"; + << "GenerateProxiesForSelectedClips: sequence unavailable"; return; } @@ -1231,25 +1403,41 @@ void TimelineWidget::generate_proxies_for_selected_clips() << footage.size() << "footage item(s)"; for (Footage *item : footage) { const VideoParams video = item->get_first_enabled_video_stream(); - if (!video.is_valid()) { + oak_video_params _vp; + oakengine_viewer_get_first_enabled_video_stream( + reinterpret_cast(item), &_vp); + if (!oakengine_video_params_is_valid(&_vp)) { qWarning() << "GenerateProxiesForSelectedClips: skipping item with no valid video stream" << item->filename(); continue; } - ProxyManager::ProxyParams params = item->get_effective_proxy_params(); - const ProxyManager::Proxy proxy = - ProxyManager::instance()->get_or_start_proxy( - item->project()->cache_path(), item->filename(), - video.stream_index(), params); + oak_proxy_params params; + oakengine_footage_get_effective_proxy_params( + reinterpret_cast(item), ¶ms); + oak_proxy_result proxy; + char cache_buf[512]; + oakengine_project_cache_path( + reinterpret_cast(item->project()), + cache_buf, sizeof(cache_buf)); + int ret = oakengine_proxy_get_or_start( + cache_buf, + item->filename().toUtf8().constData(), + video.stream_index(), ¶ms, &proxy); + if (ret != 0) { + qWarning() << "GenerateProxiesForSelectedClips: failed to get/start proxy for" + << item->filename(); + continue; + } qDebug() << "GenerateProxiesForSelectedClips: proxy state=" - << ProxyManager::proxy_state_to_string(proxy.state) - << "file=" << proxy.filename - << "cache=" << item->project()->cache_path(); - item->set_proxy(proxy.filename, proxy.state, video.stream_index(), - params.version, true); - item->invalidate_all(Footage::k_filename_input); + << proxy.state + << "file=" << QString::fromUtf8(proxy.filename) + << "cache=" << cache_buf; + oakengine_footage_set_proxy(reinterpret_cast(item), + proxy.filename, proxy.state, + video.stream_index(), 1, params.version); + oakengine_footage_invalidate(reinterpret_cast(item)); } } @@ -1266,8 +1454,9 @@ void TimelineWidget::set_selected_clips_proxy_enabled(bool enabled) continue; } - item->set_proxy_enabled(enabled); - item->invalidate_all(Footage::k_filename_input); + oakengine_footage_proxy_set_enabled( + reinterpret_cast(item), enabled ? 1 : 0); + oakengine_footage_invalidate(reinterpret_cast(item)); } } @@ -1313,10 +1502,16 @@ void TimelineWidget::delete_proxies_for_selected_clips() } QFile::remove(item->proxy_path()); - QFile::remove( - ProxyManager::get_working_proxy_filename(item->proxy_path())); - item->clear_proxy(); - item->invalidate_all(Footage::k_filename_input); + { + char wbuf[4096]; + int wlen = oakengine_proxy_get_working_filename( + item->proxy_path().toUtf8().constData(), wbuf, sizeof(wbuf)); + if (wlen > 0) { + QFile::remove(QString::fromUtf8(wbuf, wlen)); + } + } + oakengine_footage_clear_proxy(reinterpret_cast(item)); + oakengine_footage_invalidate(reinterpret_cast(item)); } } @@ -1330,25 +1525,36 @@ void TimelineWidget::recording_callback(const QString &filename, const TimeRange &time, const Track::Reference &track) { - ProjectImportTask task(get_connected_node()->project()->root(), { filename }); - task.start(); - - auto subimport_command = task.get_command(); - - if (task.get_imported_footage().empty()) { - qCritical() << "Failed to import recorded audio file" << filename; - delete subimport_command; - } else { - subimport_command->redo_now(); - - auto import_command = new MultiUndoCommand(); - import_command->add_child(subimport_command); - - import_tool_->place_at({ task.get_imported_footage().front() }, time.in(), - false, import_command, track.index()); - Core::instance()->undo_stack()->push(import_command, - tr("Recorded Audio Clip")); + OakEngineNode *root = reinterpret_cast( + get_connected_node()->project()->root()); + const char *url = filename.toUtf8().constData(); + OakEngineTask *task = oakengine_task_create_project_import(root, &url, 1); + if (!task) { + qCritical() << "Failed to create import task for" << filename; + return; } + oakengine_task_start_sync(task); + + void *subimport_command = oakengine_task_import_get_command(task); + + if (oakengine_task_import_footage_count(task) == 0) { + qCritical() << "Failed to import recorded audio file" << filename; + if (subimport_command) { + oakengine_undo_command_free(subimport_command); + } + } else { + oakengine_undo_command_redo_now(subimport_command); + + auto import_command = oakengine_undo_command_create_multi(); + oakengine_undo_command_multi_add_child(import_command, static_cast(subimport_command)); + + OakEngineNode *front = oakengine_task_import_footage_at(task, 0); + import_tool_->place_at({ reinterpret_cast(front) }, time.in(), + false, import_command, track.index()); + oakengine_undo_push(import_command, + tr("Recorded Audio Clip").toUtf8().constData()); + } + oakengine_task_free(task); } void TimelineWidget::enable_recording_overlay(const TimelineCoordinate &coord) @@ -1377,23 +1583,25 @@ void TimelineWidget::add_tentative_subtitle_track() if (should_adjust_splitter || should_add_sub_track) { // Create command - subtitle_show_command_ = new MultiUndoCommand(); + subtitle_show_command_ = oakengine_undo_command_create_multi(); if (should_adjust_splitter) { sz[Track::k_subtitle] = height() / Track::k_count; - subtitle_show_command_->add_child( - new SetSplitterSizesCommand(view_splitter_, sz)); + oakengine_undo_command_multi_add_child( + subtitle_show_command_, + make_splitter_sizes_command(view_splitter_, sz)); } if (should_add_sub_track) { - TimelineAddTrackCommand *track_add_cmd = - new TimelineAddTrackCommand( - sequence()->track_list(Track::k_subtitle)); - subtitle_tentative_track_ = track_add_cmd->track(); - subtitle_show_command_->add_child(track_add_cmd); + void *track_add_cmd = oakengine_sequence_add_track_command( + reinterpret_cast(sequence()), + OAKENGINE_TRACK_TYPE_SUBTITLE, 0, + &subtitle_tentative_track_); + oakengine_undo_command_multi_add_child( + subtitle_show_command_, track_add_cmd); } - subtitle_show_command_->redo_now(); + oakengine_undo_command_redo_now(subtitle_show_command_); } } } @@ -1429,7 +1637,7 @@ void TimelineWidget::nest_selected_clips() end_time = std::max(end_time, b->out()); } - auto move_to_nest_command = new MultiUndoCommand(); + auto move_to_nest_command = oakengine_undo_command_create_multi(); // Remove blocks from this sequence replace_blocks_with_gaps(blocks, false, move_to_nest_command); @@ -1437,14 +1645,33 @@ void TimelineWidget::nest_selected_clips() // Create new sequence Project *project = this->get_connected_node()->project(); Sequence *nest = - Core::create_new_sequence_for_project(tr("Nested Sequence %1"), project); - nest->set_video_params(get_connected_node()->get_video_params()); - nest->set_audio_params(get_connected_node()->get_audio_params()); - move_to_nest_command->add_child(new NodeAddCommand(project, nest)); + Core::instance()->create_new_sequence_for_project(tr("Nested Sequence %1"), project); + { + oak_video_params vpod; + oakengine_viewer_get_video_params( + reinterpret_cast(get_connected_node()), + 0, &vpod); + oakengine_viewer_set_video_params( + reinterpret_cast(nest), &vpod, 0); + } + { + int sr = 0, fmt = 0; + uint64_t cl = 0; + oakengine_viewer_get_audio_params( + reinterpret_cast(get_connected_node()), + 0, &sr, &cl, &fmt); + oakengine_viewer_set_audio_params( + reinterpret_cast(nest), sr, cl, fmt, 0); + } + oakengine_undo_command_multi_add_child(move_to_nest_command, + oakengine_node_add_to_project_command( + reinterpret_cast(project), + reinterpret_cast(nest))); // Add to same folder - move_to_nest_command->add_child( - new FolderAddChild(this->get_connected_node()->folder(), nest)); + oakengine_folder_add_child( + reinterpret_cast(this->get_connected_node()->folder()), + reinterpret_cast(nest)); // Place blocks in new sequence for (int i = 0; i < blocks.size(); i++) { @@ -1453,17 +1680,14 @@ void TimelineWidget::nest_selected_clips() const TimeRange &range = times.at(i); Track::Reference track = tracks.at(i); - move_to_nest_command->add_child(new TrackPlaceBlockCommand( - nest->track_list(track.type()), - track.index() - track_offset.at(track.type()), b, - range.in() - start_time)); + oakengine_undo_command_multi_add_child(move_to_nest_command, oakengine_track_place_block_command(reinterpret_cast(nest->track_list(track.type())), track.index() - track_offset.at(track.type()), reinterpret_cast(b), core::Timecode::time_to_timestamp(range.in() - start_time, sequence_timebase(nest)))); } // Do this command now, because we later do checks and actions that rely on these having been done - move_to_nest_command->redo_now(); + oakengine_undo_command_redo_now(move_to_nest_command); - auto meta_command = new MultiUndoCommand(); - meta_command->add_child(move_to_nest_command); + auto meta_command = oakengine_undo_command_create_multi(); + oakengine_undo_command_multi_add_child(meta_command, move_to_nest_command); // Find first free track index bool empty = false; @@ -1491,14 +1715,14 @@ void TimelineWidget::nest_selected_clips() // Place new sequence in this sequence import_tool_->place_at({ nest }, start_time, false, meta_command, index); - Core::instance()->undo_stack()->push(meta_command, tr("Nested Clips")); + oakengine_undo_push(meta_command, tr("Nested Clips").toUtf8().constData()); } void TimelineWidget::clear_tentative_subtitle_track() { if (subtitle_show_command_) { - subtitle_show_command_->undo_now(); - delete subtitle_show_command_; + oakengine_undo_command_undo_now(subtitle_show_command_); + oakengine_undo_command_free(subtitle_show_command_); subtitle_show_command_ = nullptr; subtitle_tentative_track_ = nullptr; } @@ -1506,12 +1730,16 @@ void TimelineWidget::clear_tentative_subtitle_track() void TimelineWidget::insert_gaps_at(const Rational &earliest_point, const Rational &insert_length, - MultiUndoCommand *command) + void *command) { for (int i = 0; i < Track::k_count; i++) { - command->add_child(new TrackListInsertGaps( - sequence()->track_list(static_cast(i)), earliest_point, - insert_length)); + oakengine_undo_command_multi_add_child( + command, + oakengine_track_list_insert_gaps_command( + reinterpret_cast(sequence()->track_list( + static_cast(i))), + earliest_point.numerator(), earliest_point.denominator(), + insert_length.numerator(), insert_length.denominator())); } } @@ -1652,16 +1880,14 @@ void TimelineWidget::add_block(Block *block) { // Set up clip with view parameters (clip item will automatically size its rect accordingly) if (!added_blocks_.contains(block)) { - connect(block, &Block::links_changed, this, - &TimelineWidget::block_updated); - connect(block, &Block::label_changed, this, - &TimelineWidget::block_updated); - connect(block, &Block::color_changed, this, - &TimelineWidget::block_updated); - connect(block, &Block::enabled_changed, this, - &TimelineWidget::block_updated); - connect(block, &Block::preview_changed, this, - &TimelineWidget::block_updated); + OakEngineNode *node = reinterpret_cast(block); + QVector subs; + subs.append(bridge_->subscribe(node, OAKENGINE_EVENT_BLOCK_ENABLED_CHANGED)); + subs.append(bridge_->subscribe(node, OAKENGINE_EVENT_BLOCK_PREVIEW_CHANGED)); + subs.append(bridge_->subscribe(node, OAKENGINE_EVENT_NODE_LABEL_CHANGED)); + subs.append(bridge_->subscribe(node, OAKENGINE_EVENT_NODE_LINKS_CHANGED)); + subs.append(bridge_->subscribe(node, OAKENGINE_EVENT_NODE_COLOR_CHANGED)); + block_subscriptions_.insert(block, subs); added_blocks_.append(block); @@ -1675,17 +1901,14 @@ void TimelineWidget::add_block(Block *block) void TimelineWidget::remove_block(Block *block) { - // Disconnect all signals - disconnect(block, &Block::links_changed, this, - &TimelineWidget::block_updated); - disconnect(block, &Block::label_changed, this, - &TimelineWidget::block_updated); - disconnect(block, &Block::color_changed, this, - &TimelineWidget::block_updated); - disconnect(block, &Block::enabled_changed, this, - &TimelineWidget::block_updated); - disconnect(block, &Block::preview_changed, this, - &TimelineWidget::block_updated); + // Unsubscribe bridge events for this block + if (auto it = block_subscriptions_.find(block); + it != block_subscriptions_.end()) { + for (int64_t id : it.value()) { + oakengine_event_unsubscribe(id); + } + block_subscriptions_.erase(it); + } // Take item from map added_blocks_.removeOne(block); @@ -1706,29 +1929,18 @@ void TimelineWidget::add_track(Track *track) add_block(b); } - connect(track, &Track::index_changed, this, &TimelineWidget::track_updated); - connect(track, &Track::index_changed, this, - &TimelineWidget::track_index_changed); - connect(track, &Track::blocks_refreshed, this, - &TimelineWidget::track_updated); - connect(track, &Track::track_height_changed, this, - &TimelineWidget::track_updated); - connect(track, &Track::block_added, this, &TimelineWidget::add_block); - connect(track, &Track::block_removed, this, &TimelineWidget::remove_block); + OakEngineTrack *h = reinterpret_cast(track); + bridge_->subscribe(h, OAKENGINE_EVENT_TRACK_INDEX_CHANGED); + bridge_->subscribe(h, OAKENGINE_EVENT_TRACK_BLOCKS_REFRESHED); + bridge_->subscribe(h, OAKENGINE_EVENT_TRACK_HEIGHT_CHANGED); + bridge_->subscribe(h, OAKENGINE_EVENT_TRACK_BLOCK_ADDED); + bridge_->subscribe(h, OAKENGINE_EVENT_TRACK_BLOCK_REMOVED); } void TimelineWidget::remove_track(Track *track) { - disconnect(track, &Track::index_changed, this, - &TimelineWidget::track_updated); - disconnect(track, &Track::index_changed, this, - &TimelineWidget::track_index_changed); - disconnect(track, &Track::blocks_refreshed, this, - &TimelineWidget::track_updated); - disconnect(track, &Track::track_height_changed, this, - &TimelineWidget::track_updated); - disconnect(track, &Track::block_added, this, &TimelineWidget::add_block); - disconnect(track, &Track::block_removed, this, &TimelineWidget::remove_block); + // Bridge subscriptions auto-die with the observed engine object. + // No per-track unsubscribe needed. remove_selection(TimeRange(0, RATIONAL_MAX), track->to_reference()); @@ -1737,9 +1949,9 @@ void TimelineWidget::remove_track(Track *track) } } -void TimelineWidget::track_updated() +void TimelineWidget::track_updated(Track::Type type) { - update_viewports(static_cast(sender())->type()); + update_viewports(type); } void TimelineWidget::block_updated() @@ -1819,7 +2031,7 @@ void TimelineWidget::show_context_menu() QAction *autocache_action = cache_menu->addAction(tr("Auto-Cache")); autocache_action->setCheckable(true); - autocache_action->setChecked(clip->is_autocaching()); + autocache_action->setChecked(clip_is_autocaching(clip)); connect(autocache_action, &QAction::triggered, this, &TimelineWidget::set_selected_clips_autocaching); @@ -1894,7 +2106,7 @@ void TimelineWidget::show_context_menu() reveal_in_footage_viewer->setData( reinterpret_cast(clip->connected_viewer())); reveal_in_footage_viewer->setProperty( - "range", QVariant::fromValue(clip->media_range())); + "range", QVariant::fromValue(clip_media_range(clip))); connect(reveal_in_footage_viewer, &QAction::triggered, this, &TimelineWidget::reveal_in_footage_viewer); @@ -2037,7 +2249,7 @@ void TimelineWidget::set_view_thumbnails_enabled(QAction *action) void TimelineWidget::frame_rate_changed() { - SetTimebase(get_connected_node()->get_video_params().frame_rate_as_time_base()); + SetTimebase(viewer_output_video_params(get_connected_node()).frame_rate_as_time_base()); } void TimelineWidget::sample_rate_changed() @@ -2045,10 +2257,8 @@ void TimelineWidget::sample_rate_changed() update_view_timebases(); } -void TimelineWidget::track_index_changed(int old, int now) +void TimelineWidget::track_index_changed(Track *track, int old, int now) { - Track *track = static_cast(sender()); - Track::Reference old_ref(track->type(), old); Track::Reference new_ref(track->type(), now); @@ -2072,7 +2282,7 @@ void TimelineWidget::reveal_in_footage_viewer() reinterpret_cast(a->data().value()); TimeRange r = a->property("range").value(); - emit reveal_viewer_in_footage_viewer(item_to_reveal, r); + emit reveal_viewer_in_footage_viewer(reinterpret_cast(item_to_reveal), r); } void TimelineWidget::reveal_in_project() @@ -2082,7 +2292,7 @@ void TimelineWidget::reveal_in_project() ViewerOutput *item_to_reveal = reinterpret_cast(a->data().value()); - emit reveal_viewer_in_project(item_to_reveal); + emit reveal_viewer_in_project(reinterpret_cast(item_to_reveal)); } void TimelineWidget::rename_selected_blocks() @@ -2121,42 +2331,46 @@ void TimelineWidget::rename_selected_blocks() s.toUtf8().constData()); } -void TimelineWidget::track_about_to_be_deleted(Track *track) +void TimelineWidget::track_about_to_be_deleted(OakEngineTrack *track) { if (track == subtitle_tentative_track_) { // User is deleting the tentative subtitle track. Technically they shouldn't do this, but they // might if they misinterpret it as permanent. If so, we handle it cleanly by pushing our // command as if the action really were permanent. - Core::instance()->undo_stack()->push(take_subtitle_section_command(), - tr("Created Subtitle Track")); + oakengine_undo_push(take_subtitle_section_command(), + tr("Created Subtitle Track").toUtf8().constData()); } } void TimelineWidget::set_selected_clips_autocaching(bool e) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); for (Block *b : selected_blocks_) { if (ClipBlock *clip = dynamic_cast(b)) { - command->add_child(new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(clip, ClipBlock::k_auto_cache_input)), - e)); + oak_node_value v{}; + v.type = OAK_NODE_VALUE_BOOL; + v.num = e ? 1 : 0; + oakengine_undo_command_multi_add_child( + command, + oakengine_node_set_standard_value_command( + reinterpret_cast(clip), + oakengine_clip_auto_cache_input_id(), 0, 0, &v)); } } - Core::instance()->undo_stack()->push( - command, e ? tr("Enabled Auto-Caching On %1 Clip(s)") + oakengine_undo_push( + command, (e ? tr("Enabled Auto-Caching On %1 Clip(s)") .arg(selected_blocks_.size()) : tr("Disabled Auto-Caching On %1 Clip(s)") - .arg(selected_blocks_.size())); + .arg(selected_blocks_.size())).toUtf8().constData()); } void TimelineWidget::cache_clips() { for (Block *b : selected_blocks_) { if (ClipBlock *clip = dynamic_cast(b)) { - clip->request_invalidated_from_connected(true); + clip_request_invalidate_connected(clip, true); } } } @@ -2173,11 +2387,11 @@ void TimelineWidget::cache_clips_in_out() const TimeRange &r = this->sequence()->get_work_area()->range(); for (Block *b : qAsConst(selected_blocks_)) { if (ClipBlock *clip = dynamic_cast(b)) { - if (Node *connected = clip->get_connected_output(clip->k_buffer_in)) { + if (Node *connected = clip->get_connected_output(oakengine_clip_buffer_input_id())) { TimeRange adjusted = tto.get_adjusted_time(this->sequence(), connected, r, Node::k_transform_towards_input); - clip->request_invalidated_from_connected(true, adjusted); + clip_request_invalidate_connected(clip, true, adjusted); } } } @@ -2194,7 +2408,8 @@ void TimelineWidget::cache_discard() QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { for (Block *b : selected_blocks_) { if (ClipBlock *clip = dynamic_cast(b)) { - clip->discard_cache(); + oakengine_clip_discard_cache( + reinterpret_cast(clip)); } } } @@ -2202,7 +2417,7 @@ void TimelineWidget::cache_discard() void TimelineWidget::multicam_enabled_triggered(bool e) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); for (Block *b : qAsConst(selected_blocks_)) { if (ClipBlock *c = dynamic_cast(b)) { @@ -2210,27 +2425,52 @@ void TimelineWidget::multicam_enabled_triggered(bool e) if (e) { // Adding multicams // Create multicam node and add it to the graph - MultiCamNode *n = new MultiCamNode(); - n->set_sequence_type(c->get_track_type()); - command->add_child(new NodeAddCommand(s->parent(), n)); + MultiCamNode *n = reinterpret_cast( + oakengine_project_add_node( + reinterpret_cast(s->parent()), + "org.olivevideoeditor.Olive.multicam")); + { + oak_node_value v; + v.type = OAK_NODE_VALUE_INT; + v.num = c->get_track_type(); + oakengine_node_set_input( + reinterpret_cast(n), + oakengine_multicam_input_sequence_type(), &v); + } + // Node was already added by oakengine_project_add_node // For each output the sequence has to this clip, disconnect it and // connect to the multicam instead QVector inputs = c->find_ways_node_arrives_here(s); for (const NodeInput &i : inputs) { - command->add_child(new NodeEdgeRemoveCommand(s, i)); - command->add_child(new NodeEdgeAddCommand(n, i)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_disconnect_command( + reinterpret_cast(i.node()), + i.input().toUtf8().constData(), + i.element())); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(n), + reinterpret_cast(i.node()), + i.input().toUtf8().constData(), + i.element())); } - command->add_child(new NodeEdgeAddCommand( - s, NodeInput(n, n->k_sequence_input))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(s), + reinterpret_cast(n), + oakengine_multicam_input_sequence(), + -1)); // Move sequence node one unit back, and place multicam in sequence's spot QPointF sequence_pos = c->get_node_position_in_context(s); - command->add_child(new NodeSetPositionCommand( - s, c, sequence_pos - QPointF(1, 0))); - command->add_child( - new NodeSetPositionCommand(n, c, sequence_pos)); + QPointF shifted_pos = sequence_pos - QPointF(1, 0); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(s), reinterpret_cast(c), shifted_pos.x(), shifted_pos.y(), 0)); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(n), reinterpret_cast(c), sequence_pos.x(), sequence_pos.y(), 0)); } else { // Removing multicams @@ -2241,14 +2481,22 @@ void TimelineWidget::multicam_enabled_triggered(bool e) dynamic_cast(i.node())) { for (auto it = mcn->output_connections().cbegin(); it != mcn->output_connections().cend(); it++) { - command->add_child(new NodeEdgeRemoveCommand( - it->first, it->second)); - command->add_child( - new NodeEdgeAddCommand(s, it->second)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_disconnect_command( + reinterpret_cast(it->second.node()), + it->second.input().toUtf8().constData(), + it->second.element())); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(s), + reinterpret_cast(it->second.node()), + it->second.input().toUtf8().constData(), + it->second.element())); } - command->add_child( - new NodeRemoveAndDisconnectCommand(mcn)); + oakengine_undo_command_multi_add_child(command, oakengine_node_remove_and_disconnect_command(reinterpret_cast(mcn))); } } } @@ -2256,10 +2504,10 @@ void TimelineWidget::multicam_enabled_triggered(bool e) } } - Core::instance()->undo_stack()->push( + oakengine_undo_push( command, - e ? tr("Multi-Cam Enabled On %1 Clip(s)").arg(selected_blocks_.size()) : - tr("Multi-Cam Disabled On %1 Clip(s)").arg(selected_blocks_.size())); + (e ? tr("Multi-Cam Enabled On %1 Clip(s)").arg(selected_blocks_.size()) : + tr("Multi-Cam Disabled On %1 Clip(s)").arg(selected_blocks_.size())).toUtf8().constData()); } void TimelineWidget::force_update_rubber_band() @@ -2284,7 +2532,7 @@ void TimelineWidget::update_view_timebases() if (get_connected_node() && use_audio_time_units_ && i == Track::k_audio) { view->view()->set_timebase( - get_connected_node()->get_audio_params().sample_rate_as_time_base()); + viewer_output_audio_params(get_connected_node()).sample_rate_as_time_base()); } else { view->view()->set_timebase(timebase()); } @@ -2305,39 +2553,34 @@ void TimelineWidget::nudge_internal(Rational amount) return; } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); foreach (Block *b, selected_blocks_) { - command->add_child( - new TrackReplaceBlockWithGapCommand(b->track(), b, false)); + oakengine_undo_command_multi_add_child(command, oakengine_track_replace_block_with_gap_command(reinterpret_cast(b->track()), reinterpret_cast(b), false ? 1 : 0)); } foreach (Block *b, selected_blocks_) { - command->add_child(new TrackPlaceBlockCommand( - sequence()->track_list(b->track()->type()), b->track()->index(), - b, b->in() + amount)); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence()->track_list(b->track()->type())), b->track()->index(), reinterpret_cast(b), core::Timecode::time_to_timestamp(b->in() + amount, timebase()))); } // Nudge selections TimelineWidgetSelections new_sel = get_selections(); new_sel.shift_time(amount); - command->add_child(new TimelineWidget::SetSelectionsCommand( - this, new_sel, get_selections())); + oakengine_undo_command_multi_add_child(command, create_set_selections_command(new_sel, get_selections())); - Core::instance()->undo_stack()->push(command, tr("Nudged Clips")); + oakengine_undo_push(command, tr("Nudged Clips").toUtf8().constData()); } } void TimelineWidget::move_to_playhead_internal(bool out) { if (get_connected_node() && !selected_blocks_.isEmpty()) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); // Remove each block from the graph QHash earliest_pts; foreach (Block *b, selected_blocks_) { - command->add_child( - new TrackReplaceBlockWithGapCommand(b->track(), b, false)); + oakengine_undo_command_multi_add_child(command, oakengine_track_replace_block_with_gap_command(reinterpret_cast(b->track()), reinterpret_cast(b), false ? 1 : 0)); Rational r = earliest_pts.value(b->track(), out ? RATIONAL_MIN : RATIONAL_MAX); @@ -2359,16 +2602,13 @@ void TimelineWidget::move_to_playhead_internal(bool out) if (new_out <= 0) { can_shift = false; } else { - command->add_child( - new BlockResizeWithMediaInCommand(b, new_out)); + oakengine_undo_command_multi_add_child(command, oakengine_block_resize_with_media_in_command(reinterpret_cast(b), new_out.numerator(), new_out.denominator())); new_in = 0; } } if (can_shift) { - command->add_child(new TrackPlaceBlockCommand( - sequence()->track_list(b->track()->type()), - b->track()->index(), b, new_in)); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence()->track_list(b->track()->type())), b->track()->index(), reinterpret_cast(b), core::Timecode::time_to_timestamp(new_in, timebase()))); } } @@ -2383,11 +2623,10 @@ void TimelineWidget::move_to_playhead_internal(bool out) it.value().shift(track_adj); } } - command->add_child( - new SetSelectionsCommand(this, new_sel, get_selections())); + oakengine_undo_command_multi_add_child(command, create_set_selections_command(new_sel, get_selections())); - Core::instance()->undo_stack()->push(command, - tr("Moved Clip(s) To Point")); + oakengine_undo_push(command, + tr("Moved Clip(s) To Point").toUtf8().constData()); } } @@ -2590,10 +2829,15 @@ void TimelineWidget::ripple_to(Timeline::MovementMode mode) // If we rippled, ump to where new cut is if applicable if (mode == Timeline::k_trim_in) { - get_connected_node()->set_playhead(closest_point_to_playhead); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + closest_point_to_playhead.numerator(), + closest_point_to_playhead.denominator()); } else if (mode == Timeline::k_trim_out && closest_point_to_playhead == get_connected_node()->get_playhead()) { - get_connected_node()->set_playhead(playhead_time); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + playhead_time.numerator(), playhead_time.denominator()); } } @@ -2628,64 +2872,144 @@ bool TimelineWidget::paste_internal(bool insert) return false; } - ProjectSerializer::Result res = ProjectSerializer::paste( - ProjectSerializer::k_only_clips, get_connected_node()->project()); - if (res.get_load_data().nodes.isEmpty()) { + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_CLIPS, + reinterpret_cast(get_connected_node()->project()), + nullptr); + int result_code = OAKENGINE_SERIALIZER_NO_DATA; + oakengine_clipboard_paste(cb, OAKENGINE_CLIPBOARD_CLIPS, + reinterpret_cast( + get_connected_node()->project()), + &result_code, nullptr, 0); + + if (result_code != OAKENGINE_SERIALIZER_OK) { + oakengine_clipboard_free(cb); return false; } - MultiUndoCommand *command = new MultiUndoCommand(); + const int node_count = oakengine_clipboard_get_loaded_node_count(cb); + if (node_count == 0) { + oakengine_clipboard_free(cb); + return false; + } + + void *command = oakengine_undo_command_create_multi(); Project *project = get_connected_node()->project(); - foreach (Node *n, res.get_load_data().nodes) { - command->add_child(new NodeAddCommand(project, n)); + for (int i = 0; i < node_count; i++) { + Node *n = reinterpret_cast( + oakengine_clipboard_get_loaded_node_at(cb, i)); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(project), + reinterpret_cast(n))); if (n->is_item() && !n->folder()) { - command->add_child(new FolderAddChild(project->root(), n)); + oakengine_folder_add_child( + reinterpret_cast(project->root()), + reinterpret_cast(n)); } } - for (auto it = res.get_load_data().promised_connections.cbegin(); - it != res.get_load_data().promised_connections.cend(); it++) { - auto oc = *it; - command->add_child(new NodeEdgeAddCommand(oc.first, oc.second)); + // Collect connections + struct Conn { + Node *output; + Node *input; + QString input_id; + int element; + }; + QVector conns; + oakengine_clipboard_foreach_connection( + cb, + [](OakEngineNode *out, OakEngineNode *in, const char *input_id, + int element, void *userdata) -> int { + auto *v = static_cast *>(userdata); + v->append({reinterpret_cast(out), + reinterpret_cast(in), + QString::fromUtf8(input_id), element}); + return 0; + }, + &conns); + + for (const Conn &c : conns) { + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(c.output), + reinterpret_cast(c.input), + c.input_id.toUtf8().constData(), + c.element)); } Rational paste_start = get_connected_node()->get_playhead(); - if (insert) { - Rational paste_end = paste_start; + struct PropertyCtx { + Rational paste_start; + Rational paste_end; + bool insert; + void *command; + TimelineWidget *self; + }; - for (auto it = res.get_load_data().properties.cbegin(); - it != res.get_load_data().properties.cend(); it++) { - Rational length = static_cast(it.key())->length(); + PropertyCtx pctx; + pctx.paste_start = paste_start; + pctx.paste_end = paste_start; + pctx.insert = insert; + pctx.command = command; + pctx.self = this; + + // First pass: compute paste_end for insert mode + oakengine_clipboard_foreach_property( + cb, + [](OakEngineNode *node, const char *key, const char *value, + void *userdata) -> int { + auto *ctx = static_cast(userdata); + if (ctx->insert && std::strcmp(key, "in") == 0) { + Block *block = static_cast( + reinterpret_cast(node)); + Rational length = block->length(); + Rational in = Rational::from_string(value); + Rational end = ctx->paste_start + in + length; + if (end > ctx->paste_end) { + ctx->paste_end = end; + } + } + return 0; + }, + &pctx); + + if (insert && pctx.paste_end != paste_start) { + insert_gaps_at(paste_start, pctx.paste_end - paste_start, command); + } + + // Collect all properties + QHash> props; + oakengine_clipboard_foreach_property( + cb, + [](OakEngineNode *node, const char *key, const char *value, + void *userdata) -> int { + auto *m = static_cast> *>(userdata); + (*m)[node][QString::fromUtf8(key)] = QString::fromUtf8(value); + return 0; + }, + &props); + + for (auto it = props.cbegin(); it != props.cend(); it++) { + Block *block = static_cast( + reinterpret_cast(it.key())); + if (it.value().contains(QStringLiteral("in"))) { Rational in = Rational::from_string( it.value()[QStringLiteral("in")].toStdString()); - - paste_end = qMax(paste_end, paste_start + in + length); - } - - if (paste_end != paste_start) { - insert_gaps_at(paste_start, paste_end - paste_start, command); + Track::Reference track = Track::Reference::from_string( + it.value()[QStringLiteral("track")]); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence()->track_list(track.type())), track.index(), reinterpret_cast(block), core::Timecode::time_to_timestamp(paste_start + in, timebase()))); } } - for (auto it = res.get_load_data().properties.cbegin(); - it != res.get_load_data().properties.cend(); it++) { - Block *block = static_cast(it.key()); - Rational in = Rational::from_string( - it.value()[QStringLiteral("in")].toStdString()); - Track::Reference track = - Track::Reference::from_string(it.value()[QStringLiteral("track")]); - - command->add_child( - new TrackPlaceBlockCommand(sequence()->track_list(track.type()), - track.index(), block, paste_start + in)); - } - - Core::instance()->undo_stack()->push( + oakengine_undo_push( command, - tr("Pasted %1 Clip(s)").arg(res.get_load_data().properties.size())); + tr("Pasted %1 Clip(s)").arg(node_count).toUtf8().constData()); + oakengine_clipboard_free(cb); return true; } @@ -2699,11 +3023,15 @@ TimelineWidget::add_timeline_and_track_view(Qt::Alignment alignment) } QHash -TimelineWidget::generate_existing_paste_map(const ProjectSerializer::Result &r) +TimelineWidget::generate_existing_paste_map(void *clipboard) { QHash m; + OakEngineClipboard *cb = static_cast(clipboard); + const int node_count = oakengine_clipboard_get_loaded_node_count(cb); - for (Node *n : r.get_load_data().nodes) { + for (int i = 0; i < node_count; i++) { + Node *n = reinterpret_cast( + oakengine_clipboard_get_loaded_node_at(cb, i)); for (Block *b : qAsConst(this->selected_blocks_)) { for (auto it = b->get_context_positions().cbegin(); it != b->get_context_positions().cend(); it++) { @@ -2855,6 +3183,45 @@ void TimelineWidget::remove_selection(Block *item) } } +namespace { + +struct SetSelectionsCommandData { + TimelineWidget *timeline; + TimelineWidgetSelections now; + TimelineWidgetSelections old; + bool process_block_changes; +}; + +static void redo_set_selections(void *userdata) +{ + auto *d = static_cast(userdata); + d->timeline->set_selections(d->now, d->process_block_changes); +} + +static void undo_set_selections(void *userdata) +{ + auto *d = static_cast(userdata); + d->timeline->set_selections(d->old, d->process_block_changes); +} + +static void free_set_selections(void *userdata) +{ + delete static_cast(userdata); +} + +} // namespace + +void *TimelineWidget::create_set_selections_command( + const TimelineWidgetSelections &now, const TimelineWidgetSelections &old, + bool process_block_changes) +{ + auto *d = new SetSelectionsCommandData{this, now, old, + process_block_changes}; + return oakengine_undo_command_create( + tr("Set Selections").toUtf8().constData(), redo_set_selections, + undo_set_selections, free_set_selections, d); +} + void TimelineWidget::set_selections(const TimelineWidgetSelections &s, bool process_block_changes) { @@ -2907,15 +3274,5 @@ Block *TimelineWidget::get_item_at_scene_pos(const TimelineCoordinate &coord) ->get_item_at_scene_pos(coord.get_frame(), coord.get_track().index()); } -void TimelineWidget::SetSplitterSizesCommand::redo() -{ - old_sizes_ = splitter_->sizes(); - splitter_->setSizes(new_sizes_); -} - -void TimelineWidget::SetSplitterSizesCommand::undo() -{ - splitter_->setSizes(old_sizes_); -} } diff --git a/app/widget/timelinewidget/timelinewidget.h b/app/widget/timelinewidget/timelinewidget.h index 6cd86eeb4..213d0942f 100644 --- a/app/widget/timelinewidget/timelinewidget.h +++ b/app/widget/timelinewidget/timelinewidget.h @@ -22,6 +22,7 @@ #ifndef OAK_TIMELINEWIDGET_H #define OAK_TIMELINEWIDGET_H +#include #include #include #include @@ -29,7 +30,10 @@ #include "core.h" #include "node/block/transition/transition.h" #include "node/output/viewer/viewer.h" -#include "node/project/serializer/serializer.h" +#include "oakengine/events.h" +#include "oakengine/serializer.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" #include "timeline/timelinecommon.h" #include "timelineandtrackview.h" #include "widget/slider/rationalslider.h" @@ -75,9 +79,9 @@ public: void decrease_track_height(); - void insert_footage_at_playhead(const QVector &footage); + void insert_footage_at_playhead(const QVector &footage); - void overwrite_footage_at_playhead(const QVector &footage); + void overwrite_footage_at_playhead(const QVector &footage); void toggle_links_on_selected(); @@ -151,7 +155,7 @@ public: static void replace_blocks_with_gaps(const QVector &blocks, bool remove_from_graph, - MultiUndoCommand *command, + void *command, bool handle_transitions = true); /** @@ -187,7 +191,7 @@ public: } void insert_gaps_at(const Rational &time, const Rational &length, - MultiUndoCommand *command); + void *command); void start_rubber_band_select(const QPoint &global_cursor_start); void move_rubber_band_select(bool enable_selecting, bool select_links); @@ -254,10 +258,10 @@ public: update_viewports(); } - MultiUndoCommand *take_subtitle_section_command() + void *take_subtitle_section_command() { // Copy pointer - MultiUndoCommand *c = subtitle_show_command_; + void *c = subtitle_show_command_; // Set to null subtitle_show_command_ = nullptr; @@ -267,41 +271,9 @@ public: return c; } - class SetSelectionsCommand : public UndoCommand { - public: - SetSelectionsCommand(TimelineWidget *timeline, - const TimelineWidgetSelections &now, - const TimelineWidgetSelections &old, - bool process_block_changes = true) - : timeline_(timeline) - , old_(old) - , now_(now) - , process_block_changes_(process_block_changes) - { - } - - virtual Project *get_relevant_project() const override - { - return nullptr; - } - - protected: - virtual void redo() override - { - timeline_->set_selections(now_, process_block_changes_); - } - - virtual void undo() override - { - timeline_->set_selections(old_, process_block_changes_); - } - - private: - TimelineWidget *timeline_; - TimelineWidgetSelections old_; - TimelineWidgetSelections now_; - bool process_block_changes_; - }; + void *create_set_selections_command(const TimelineWidgetSelections &now, + const TimelineWidgetSelections &old, + bool process_block_changes = true); public slots: void clear_tentative_subtitle_track(); @@ -309,13 +281,13 @@ public slots: void rename_selected_blocks(); signals: - void block_selection_changed(const QVector &selected_blocks); + void block_selection_changed(const QVector &selected_blocks); void request_capture_start(const TimeRange &time, const Track::Reference &track); - void reveal_viewer_in_footage_viewer(ViewerOutput *r, const TimeRange &range); - void reveal_viewer_in_project(ViewerOutput *r); + void reveal_viewer_in_footage_viewer(OakEngineNode *r, const TimeRange &range); + void reveal_viewer_in_project(OakEngineNode *r); protected: virtual void resizeEvent(QResizeEvent *event) override; @@ -352,7 +324,7 @@ private: TimelineAndTrackView *add_timeline_and_track_view(Qt::Alignment alignment); QHash - generate_existing_paste_map(const ProjectSerializer::Result &r); + generate_existing_paste_map(void *clipboard); QRubberBand rubberband_; QVector rubberband_scene_pos_; @@ -381,40 +353,54 @@ private: QVector added_blocks_; + QHash> block_subscriptions_; + int deferred_scroll_value_; bool use_audio_time_units_; QSplitter *view_splitter_; - MultiUndoCommand *subtitle_show_command_; - Track *subtitle_tentative_track_; + void *subtitle_show_command_; + OakEngineTrack *subtitle_tentative_track_; QTimer *signal_block_change_timer_; - class SetSplitterSizesCommand : public UndoCommand { - public: - SetSplitterSizesCommand(QSplitter *splitter, const QList &sizes) - : splitter_(splitter) - , new_sizes_(sizes) - { - } - - virtual Project *get_relevant_project() const override - { - return nullptr; - } - - protected: - virtual void redo() override; - virtual void undo() override; - - private: - QSplitter *splitter_; - QList new_sizes_; - QList old_sizes_; + // Command userdata for splitter size changes + struct SplitterSizesCmdData { + QSplitter *splitter; + QList new_sizes; + QList old_sizes; }; + static void splitter_sizes_redo(void *userdata) + { + auto *d = static_cast(userdata); + d->old_sizes = d->splitter->sizes(); + d->splitter->setSizes(d->new_sizes); + } + + static void splitter_sizes_undo(void *userdata) + { + auto *d = static_cast(userdata); + d->splitter->setSizes(d->old_sizes); + } + + static void splitter_sizes_free(void *userdata) + { + delete static_cast(userdata); + } + + static void *make_splitter_sizes_command(QSplitter *splitter, const QList &sizes) + { + auto *d = new SplitterSizesCmdData; + d->splitter = splitter; + d->new_sizes = sizes; + return oakengine_undo_command_create(nullptr, splitter_sizes_redo, + splitter_sizes_undo, + splitter_sizes_free, d); + } + void center_on(qreal scene_pos); void update_view_timebases(); @@ -434,12 +420,7 @@ private slots: void view_drag_left(QDragLeaveEvent *event); void view_drag_dropped(TimelineViewMouseEvent *event); - void add_block(Block *block); - void remove_block(Block *blocks); - - void add_track(Track *track); - void remove_track(Track *track); - void track_updated(); + void track_updated(Track::Type type); void block_updated(); @@ -467,15 +448,11 @@ private slots: void sample_rate_changed(); - void track_index_changed(int old, int now); - void signal_block_selection_change(); void reveal_in_footage_viewer(); void reveal_in_project(); - void track_about_to_be_deleted(Track *track); - void set_selected_clips_autocaching(bool e); void cache_clips(); @@ -485,6 +462,16 @@ private slots: void multicam_enabled_triggered(bool e); void force_update_rubber_band(); + +private: + void add_block(Block *block); + void remove_block(Block *blocks); + + void add_track(Track *track); + void remove_track(Track *track); + + void track_index_changed(Track *track, int old, int now); + void track_about_to_be_deleted(OakEngineTrack *track); }; } diff --git a/app/widget/timelinewidget/timelinewidgetwaveformsync.cpp b/app/widget/timelinewidget/timelinewidgetwaveformsync.cpp index 4233b606b..7b62bd8ec 100644 --- a/app/widget/timelinewidget/timelinewidgetwaveformsync.cpp +++ b/app/widget/timelinewidget/timelinewidgetwaveformsync.cpp @@ -25,6 +25,7 @@ #include "node/block/clip/clip.h" #include "render/audiowaveformcache.h" +#include "widget/timelinewidget/cliphandle.h" namespace olive { @@ -35,16 +36,16 @@ namespace timeline_waveform_sync bool get_waveform_sync_clip(Block *block, WaveformSyncClip *out) { ClipBlock *clip = dynamic_cast(block); - if (!clip || !clip->waveform()) { + if (!clip || !clip_waveform(clip)) { return false; } - const TimeRange media_range = clip->media_range(); + const TimeRange media_range = clip_media_range(clip); if (media_range.length().isNull()) { return false; } - const AudioWaveformCache *waveform = clip->waveform(); + const AudioWaveformCache *waveform = clip_waveform(clip); if (waveform->get_parameters().sample_rate() <= 0) { return false; } diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index 996132961..18f1aa720 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -26,10 +26,14 @@ #include "node/generator/shape/shapenode.h" #include "node/generator/solid/solid.h" #include "node/generator/text/textv3.h" -#include "node/nodeundo.h" +#include "oakengine/node.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" #include "timeline/timelineundopointer.h" +#include "widget/timelinewidget/cliphandle.h" #include "widget/timelinewidget/timelinewidget.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -100,11 +104,11 @@ void AddTool::mouse_release(TimelineViewMouseEvent *event) { if (ghost_) { if (!ghost_->get_adjusted_length().isNull()) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); - if (MultiUndoCommand *subtitle_section_command = + if (void *subtitle_section_command = parent()->take_subtitle_section_command()) { - command->add_child(subtitle_section_command); + oakengine_undo_command_multi_add_child(command, subtitle_section_command); } Sequence *s = parent()->sequence(); @@ -112,7 +116,7 @@ void AddTool::mouse_release(TimelineViewMouseEvent *event) QRectF r; if (Core::instance()->get_selected_addable_object() == Tool::k_addable_title) { - VideoParams svp = s->get_video_params(); + VideoParams svp = viewer_output_video_params(s); r = QRectF(0, 0, svp.width(), svp.height()); r.adjust(svp.width() / 10, svp.height() / 10, -svp.width() / 10, -svp.height() / 10); @@ -122,8 +126,8 @@ void AddTool::mouse_release(TimelineViewMouseEvent *event) ghost_->get_adjusted_in(), ghost_->get_adjusted_length(), r); - Core::instance()->undo_stack()->push( - command, qApp->translate("AddTool", "Added Clip")); + oakengine_undo_push( + command, qApp->translate("AddTool", "Added Clip").toUtf8().constData()); } parent()->clear_ghosts(); @@ -132,7 +136,7 @@ void AddTool::mouse_release(TimelineViewMouseEvent *event) } } -Node *AddTool::create_addable_clip(MultiUndoCommand *command, Sequence *sequence, +Node *AddTool::create_addable_clip(void *command, Sequence *sequence, const Track::Reference &track, const Rational &in, const Rational &length, const QRectF &rect) @@ -140,20 +144,21 @@ Node *AddTool::create_addable_clip(MultiUndoCommand *command, Sequence *sequence ClipBlock *clip; if (Core::instance()->get_selected_addable_object() == Tool::k_addable_subtitle) { - clip = new SubtitleBlock(); + clip = reinterpret_cast(oakengine_node_factory_create_from_id("org.olivevideoeditor.Olive.subtitle")); } else { - clip = new ClipBlock(); - clip->set_label(olive::Tool::get_addable_object_name( - Core::instance()->get_selected_addable_object())); + clip = clip_create_empty(olive::Tool::get_addable_object_name( + Core::instance()->get_selected_addable_object()).toUtf8().constData()); } clip->set_length_and_media_out(length); Project *graph = sequence->parent(); - command->add_child(new NodeAddCommand(graph, clip)); - command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0))); - command->add_child(new TrackPlaceBlockCommand( - sequence->track_list(track.type()), track.index(), clip, in)); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(graph), + reinterpret_cast(clip))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(clip), reinterpret_cast(clip), 0, 0, 0)); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence->track_list(track.type())), track.index(), reinterpret_cast(clip), core::Timecode::time_to_timestamp(in, sequence_timebase(sequence)))); Node *node_to_add = nullptr; @@ -162,13 +167,13 @@ Node *AddTool::create_addable_clip(MultiUndoCommand *command, Sequence *sequence // Empty, nothing to be done break; case Tool::k_addable_solid: - node_to_add = new SolidGenerator(); + node_to_add = reinterpret_cast(oakengine_node_factory_create_from_id("org.olivevideoeditor.Olive.solidgenerator")); break; case Tool::k_addable_shape: - node_to_add = new ShapeNode(); + node_to_add = reinterpret_cast(oakengine_node_factory_create_from_id("org.olivevideoeditor.Olive.shape")); break; case Tool::k_addable_title: - node_to_add = new TextGeneratorV3(); + node_to_add = reinterpret_cast(oakengine_node_factory_create_from_id("org.olivevideoeditor.Olive.text3")); break; case Tool::k_addable_bars: case Tool::k_addable_tone: @@ -186,17 +191,34 @@ Node *AddTool::create_addable_clip(MultiUndoCommand *command, Sequence *sequence if (node_to_add) { QPointF extra_node_offset(k_default_distance_from_output, 0); - command->add_child(new NodeAddCommand(graph, node_to_add)); - command->add_child(new NodeEdgeAddCommand( - node_to_add, NodeInput(clip, ClipBlock::k_buffer_in))); - command->add_child( - new NodeSetPositionCommand(node_to_add, clip, extra_node_offset)); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(graph), + reinterpret_cast(node_to_add))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(node_to_add), + reinterpret_cast(clip), + oakengine_clip_buffer_input_id(), + -1)); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(node_to_add), reinterpret_cast(clip), extra_node_offset.x(), extra_node_offset.y(), 0)); if (!rect.isNull()) { - if (ShapeNodeBase *shape = - dynamic_cast(node_to_add)) { - shape->set_rect(rect, sequence->get_video_params(), command); - } + const VideoParams vp = viewer_output_video_params(sequence); + oak_video_params pod = {}; + pod.width = vp.width(); + pod.height = vp.height(); + pod.time_base_num = vp.time_base().numerator(); + pod.time_base_den = vp.time_base().denominator(); + pod.format = vp.format(); + pod.pixel_aspect_num = vp.pixel_aspect_ratio().numerator(); + pod.pixel_aspect_den = vp.pixel_aspect_ratio().denominator(); + pod.interlacing = vp.interlacing(); + pod.divider = vp.divider(); + oakengine_shape_set_rect_undoable( + reinterpret_cast(node_to_add), rect.x(), + rect.y(), rect.width(), rect.height(), &pod, command); } } diff --git a/app/widget/timelinewidget/tool/add.h b/app/widget/timelinewidget/tool/add.h index a9461f167..8d55e0f03 100644 --- a/app/widget/timelinewidget/tool/add.h +++ b/app/widget/timelinewidget/tool/add.h @@ -35,7 +35,7 @@ public: virtual void mouse_move(TimelineViewMouseEvent *event) override; virtual void mouse_release(TimelineViewMouseEvent *event) override; - static Node *create_addable_clip(MultiUndoCommand *command, + static Node *create_addable_clip(void *command, Sequence *sequence, const Track::Reference &track, const Rational &in, const Rational &length, diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 2b1c32851..1faf4db75 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -26,7 +26,7 @@ #include #include -#include "config/config.h" +#include "common/configwrapper.h" #include "common/qtutils.h" #include "core.h" #include "dialog/sequence/sequence.h" @@ -35,12 +35,18 @@ #include "node/distort/transform/transformdistortnode.h" #include "node/generator/matrix/matrix.h" #include "node/math/math/math.h" -#include "node/nodeundo.h" #include "node/project/sequence/sequence.h" +#include "oakengine/node.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" +#include "oakengine/viewer.h" +#include "oakengine/project.h" #include "timeline/timelineundopointer.h" +#include "widget/timelinewidget/cliphandle.h" #include "window/mainwindow/mainwindow.h" #include "window/mainwindow/mainwindowundo.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -57,10 +63,10 @@ void ImportTool::drag_enter(TimelineViewMouseEvent *event) QStringList mime_formats = event->get_mime_data()->formats(); // Listen for MIME data from a ProjectViewModel - if (mime_formats.contains(Project::k_item_mime_type)) { + if (mime_formats.contains(QString::fromUtf8(oakengine_project_item_mime_type()))) { // Data is drag/drop data from a ProjectViewModel QByteArray model_data = - event->get_mime_data()->data(Project::k_item_mime_type); + event->get_mime_data()->data(QString::fromUtf8(oakengine_project_item_mime_type())); // Use QDataStream to deserialize the data QDataStream stream(&model_data, QIODevice::ReadOnly); @@ -189,11 +195,11 @@ void ImportTool::drag_leave(QDragLeaveEvent *event) void ImportTool::drag_drop(TimelineViewMouseEvent *event) { if (!dragged_footage_.isEmpty()) { - auto command = new MultiUndoCommand(); + auto command = oakengine_undo_command_create_multi(); drop_ghosts(event->get_modifiers() & Qt::ControlModifier, command); - Core::instance()->undo_stack()->push( + oakengine_undo_push( command, - qApp->translate("ImportTool", "Dropped Footage Into Sequence")); + qApp->translate("ImportTool", "Dropped Footage Into Sequence").toUtf8().constData()); event->accept(); } else { @@ -203,7 +209,7 @@ void ImportTool::drag_drop(TimelineViewMouseEvent *event) void ImportTool::place_at(const QVector &footage, const Rational &start, bool insert, - MultiUndoCommand *command, int track_offset, + void *command, int track_offset, bool jump_to_end) { DraggedFootageData refs; @@ -217,7 +223,7 @@ void ImportTool::place_at(const QVector &footage, void ImportTool::place_at(const DraggedFootageData &footage, const Rational &start, bool insert, - MultiUndoCommand *command, int track_offset, + void *command, int track_offset, bool jump_to_end) { dragged_footage_ = footage; @@ -238,7 +244,9 @@ void ImportTool::place_at(const DraggedFootageData &footage, drop_ghosts(insert, command); if (jump_to_end) { - this->sequence()->set_playhead(max); + oakengine_viewer_set_playhead( + reinterpret_cast(this->sequence()), + max.numerator(), max.denominator()); } } @@ -303,14 +311,20 @@ void ImportTool::footage_to_ghosts(Rational ghost_start, ghost->set_data(TimelineViewGhostItem::k_attached_footage, QVariant::fromValue(af)); } else if (track_type == Track::k_subtitle) { - SubtitleParams sp = footage->get_subtitle_params(ref.index()); + int sub_count = oakengine_viewer_get_subtitle_count( + reinterpret_cast(footage), + ref.index()); - for (const Subtitle &sub : sp) { + for (int si = 0; si < sub_count; si++) { + const Subtitle *sub = static_cast( + oakengine_viewer_get_subtitle_at( + reinterpret_cast(footage), + ref.index(), si)); auto ghost = - create_ghost(sub.time() + ghost_start, 0, dest_track); + create_ghost(sub->time() + ghost_start, 0, dest_track); ghost->set_data(TimelineViewGhostItem::k_attached_footage, - QVariant::fromValue(sub)); + QVariant::fromValue(*sub)); } parent()->add_tentative_subtitle_track(); @@ -327,17 +341,17 @@ void ImportTool::prep_ghosts(const Rational &frame, const int &track_index) if (parent()->get_connected_node()) { footage_to_ghosts( frame, dragged_footage_, - parent()->get_connected_node()->get_video_params().time_base(), + viewer_output_video_params(parent()->get_connected_node()).time_base(), track_index); } } -void ImportTool::drop_ghosts(bool insert, MultiUndoCommand *parent_command) +void ImportTool::drop_ghosts(bool insert, void *parent_command) { - auto command = new MultiUndoCommand(); + auto command = oakengine_undo_command_create_multi(); - if (MultiUndoCommand *c = parent()->take_subtitle_section_command()) { - command->add_child(c); + if (void *c = parent()->take_subtitle_section_command()) { + oakengine_undo_command_multi_add_child(command, c); } Project *dst_graph = nullptr; @@ -402,7 +416,8 @@ void ImportTool::drop_ghosts(bool insert, MultiUndoCommand *parent_command) Core::instance()->create_new_sequence_for_project( active_project); - new_sequence->set_default_parameters(); + oakengine_viewer_set_default_parameters( + reinterpret_cast(new_sequence)); bool sequence_is_valid = true; @@ -417,7 +432,14 @@ void ImportTool::drop_ghosts(bool insert, MultiUndoCommand *parent_command) } } - new_sequence->set_parameters_from_footage(footage_only); + QVector _footage_nodes; + for (auto *f : footage_only) { + _footage_nodes.append( + reinterpret_cast(f)); + } + oakengine_viewer_set_parameters_from_footage( + reinterpret_cast(new_sequence), + _footage_nodes.data(), _footage_nodes.size()); // If the user selected manual, show them a dialog with parameters if (behavior == k_dws_manual) { @@ -433,22 +455,25 @@ void ImportTool::drop_ghosts(bool insert, MultiUndoCommand *parent_command) if (sequence_is_valid) { dst_graph = Core::instance()->get_active_project(); - command->add_child( - new NodeAddCommand(dst_graph, new_sequence)); - command->add_child(new FolderAddChild( - Core::instance()->get_selected_folder_in_active_project(), - new_sequence)); - command->add_child(new NodeSetPositionCommand( - new_sequence, new_sequence, QPointF(0, 0))); - new_sequence->add_default_nodes(command); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(dst_graph), + reinterpret_cast(new_sequence))); + oakengine_folder_add_child( + reinterpret_cast( + Core::instance()->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)); + oakengine_sequence_add_default_nodes( + reinterpret_cast(new_sequence)); footage_to_ghosts(0, dragged_footage_, - new_sequence->get_video_params().time_base(), + viewer_output_video_params(new_sequence).time_base(), 0); - if (MultiUndoCommand *c = + if (void *c = parent()->take_subtitle_section_command()) { - command->add_child(c); + oakengine_undo_command_multi_add_child(command, c); } sequence = new_sequence; @@ -484,20 +509,21 @@ void ImportTool::drop_ghosts(bool insert, MultiUndoCommand *parent_command) ghost->get_data(TimelineViewGhostItem::k_attached_footage) .value(); - ClipBlock *clip = new ClipBlock(); + ClipBlock *clip = clip_create_empty(); block = clip; - clip->set_media_in(ghost->get_media_in()); - command->add_child(new NodeAddCommand(dst_graph, clip)); + clip_set_media_in(clip, ghost->get_media_in()); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(dst_graph), + reinterpret_cast(clip))); // Position clip in its own context - command->add_child( - new NodeSetPositionCommand(clip, clip, QPointF(0, 0))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(clip), reinterpret_cast(clip), 0, 0, 0)); int dep_pos = k_default_distance_from_output; // Position footage in its context - command->add_child(new NodeSetPositionCommand( - footage_stream.footage, clip, QPointF(dep_pos, 0))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(footage_stream.footage), reinterpret_cast(clip), dep_pos, 0, 0)); dep_pos++; @@ -505,42 +531,67 @@ void ImportTool::drop_ghosts(bool insert, MultiUndoCommand *parent_command) Track::Reference::type_from_string(footage_stream.output)) { case Track::k_video: { TransformDistortNode *transform = - new TransformDistortNode(); - command->add_child( - new NodeAddCommand(dst_graph, transform)); + reinterpret_cast(oakengine_node_factory_create_from_id("org.olivevideoeditor.Olive.transformdistort")); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(dst_graph), + reinterpret_cast(transform))); - command->add_child(new NodeSetValueHintCommand( - transform, TransformDistortNode::k_texture_input, -1, - Node::ValueHint({ NodeValue::k_texture }, - footage_stream.output))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_set_value_hint_command( + reinterpret_cast(transform), + oakengine_transform_texture_input_id(), -1, + OAK_NODE_VALUE_TEXTURE, -1, + footage_stream.output.toUtf8().constData())); - command->add_child(new NodeEdgeAddCommand( - footage_stream.footage, - NodeInput(transform, - TransformDistortNode::k_texture_input))); - command->add_child(new NodeEdgeAddCommand( - transform, NodeInput(clip, ClipBlock::k_buffer_in))); - command->add_child(new NodeSetPositionCommand( - transform, clip, QPointF(dep_pos, 0))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(footage_stream.footage), + reinterpret_cast(transform), + QLatin1String(oakengine_transform_texture_input_id()).toUtf8().constData(), + -1)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(transform), + reinterpret_cast(clip), + oakengine_clip_buffer_input_id(), + -1)); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(transform), reinterpret_cast(clip), dep_pos, 0, 0)); break; } case Track::k_audio: { - VolumeNode *volume_node = new VolumeNode(); - command->add_child( - new NodeAddCommand(dst_graph, volume_node)); + VolumeNode *volume_node = reinterpret_cast(oakengine_node_factory_create_from_id("org.olivevideoeditor.Olive.volume")); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(dst_graph), + reinterpret_cast(volume_node))); - command->add_child(new NodeSetValueHintCommand( - volume_node, VolumeNode::k_samples_input, -1, - Node::ValueHint({ NodeValue::k_samples }, - footage_stream.output))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_set_value_hint_command( + reinterpret_cast(volume_node), + oakengine_volume_samples_input_id(), -1, + OAK_NODE_VALUE_SAMPLES, -1, + footage_stream.output.toUtf8().constData())); - command->add_child(new NodeEdgeAddCommand( - footage_stream.footage, - NodeInput(volume_node, VolumeNode::k_samples_input))); - command->add_child(new NodeEdgeAddCommand( - volume_node, NodeInput(clip, ClipBlock::k_buffer_in))); - command->add_child(new NodeSetPositionCommand( - volume_node, clip, QPointF(dep_pos, 0))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(footage_stream.footage), + reinterpret_cast(volume_node), + QLatin1String(oakengine_volume_samples_input_id()).toUtf8().constData(), + -1)); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(volume_node), + reinterpret_cast(clip), + oakengine_clip_buffer_input_id(), + -1)); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(volume_node), reinterpret_cast(clip), dep_pos, 0, 0)); break; } default: @@ -557,7 +608,9 @@ void ImportTool::drop_ghosts(bool insert, MultiUndoCommand *parent_command) .value(); if (footage_compare.footage == footage_stream.footage) { - Block::link(block_items.at(j), clip); + oakengine_block_link( + reinterpret_cast(block_items.at(j)), + reinterpret_cast(clip), 1); } } @@ -566,37 +619,40 @@ void ImportTool::drop_ghosts(bool insert, MultiUndoCommand *parent_command) Subtitle src = ghost->get_data(TimelineViewGhostItem::k_attached_footage) .value(); - SubtitleBlock *sub = new SubtitleBlock(); - sub->set_text(src.text()); + SubtitleBlock *sub = reinterpret_cast( + oakengine_node_factory_create_from_id( + "org.olivevideoeditor.Olive.subtitle")); + oakengine_subtitle_set_text( + reinterpret_cast(sub), + src.text().toUtf8().constData()); block = sub; - command->add_child(new NodeAddCommand(dst_graph, sub)); - command->add_child( - new NodeSetPositionCommand(sub, sub, QPointF(0, 0))); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(dst_graph), + reinterpret_cast(sub))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(sub), reinterpret_cast(sub), 0, 0, 0)); } block->set_length_and_media_out(ghost->get_length()); - command->add_child(new TrackPlaceBlockCommand( - sequence->track_list(ghost->get_adjusted_track().type()), - ghost->get_adjusted_track().index(), block, - ghost->get_adjusted_in())); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence->track_list(ghost->get_adjusted_track().type())), ghost->get_adjusted_track().index(), reinterpret_cast(block), core::Timecode::time_to_timestamp(ghost->get_adjusted_in(), parent()->timebase()))); block_items.replace(i, block); } } if (open_sequence) { - command->add_child(new OpenSequenceCommand(sequence)); + oakengine_undo_command_multi_add_child(command, make_open_sequence_command(sequence)); } // Do command now because RequestInvalidatedFromConnected relies on track type, which will be // "none" before this command is done because it won't be connected to any track - command->redo_now(); - parent_command->add_child(command); + oakengine_undo_command_redo_now(command); + oakengine_undo_command_multi_add_child(parent_command, command); while (!imported_clips.empty()) { - imported_clips.front()->request_invalidated_from_connected(); + clip_request_invalidate_connected(imported_clips.front()); imported_clips.pop_front(); } diff --git a/app/widget/timelinewidget/tool/import.h b/app/widget/timelinewidget/tool/import.h index fd7529df3..1e1280ad5 100644 --- a/app/widget/timelinewidget/tool/import.h +++ b/app/widget/timelinewidget/tool/import.h @@ -41,10 +41,10 @@ public: QVector>>; void place_at(const QVector &footage, const Rational &start, - bool insert, MultiUndoCommand *command, int track_offset = 0, + bool insert, void *command, int track_offset = 0, bool jump_to_end = false); void place_at(const DraggedFootageData &footage, const Rational &start, - bool insert, MultiUndoCommand *command, int track_offset = 0, + bool insert, void *command, int track_offset = 0, bool jump_to_end = false); // The canonical definition lives in the engine layer @@ -61,7 +61,7 @@ private: void prep_ghosts(const Rational &frame, const int &track_index); - void drop_ghosts(bool insert, MultiUndoCommand *parent_command); + void drop_ghosts(bool insert, void *parent_command); TimelineViewGhostItem *create_ghost(const TimeRange &range, const Rational &media_in, diff --git a/app/widget/timelinewidget/tool/pointer.cpp b/app/widget/timelinewidget/tool/pointer.cpp index c4c800c95..621703147 100644 --- a/app/widget/timelinewidget/tool/pointer.cpp +++ b/app/widget/timelinewidget/tool/pointer.cpp @@ -26,11 +26,13 @@ #include "common/qtutils.h" #include "common/range.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" #include "node/block/gap/gap.h" #include "node/block/transition/transition.h" -#include "node/nodeundo.h" +#include "oakengine/node.h" +#include "oakengine/undo.h" +#include "oakengine/timeline.h" #include "pointer.h" #include "timeline/timelineundopointer.h" #include "widget/timeruler/timeruler.h" @@ -685,7 +687,7 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) return; } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); if (!blocks_trimming.isEmpty()) { foreach (const GhostBlockPair &p, blocks_trimming) { @@ -694,18 +696,19 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) if (!ghost->get_data(TimelineViewGhostItem::k_trim_should_be_ignored) .toBool()) { // Must be an ordinary trim/roll - BlockTrimCommand *c = new BlockTrimCommand( - parent()->get_track_from_reference(ghost->get_adjusted_track()), - p.block, ghost->get_adjusted_length(), ghost->get_mode()); - - if (event->get_modifiers() & Qt::ControlModifier) { - } - - c->set_trim_is_a_roll_edit( - ghost->get_data(TimelineViewGhostItem::k_trim_is_a_roll_edit) - .toBool()); - - command->add_child(c); + oakengine_undo_command_multi_add_child( + command, + oakengine_block_trim_command( + reinterpret_cast( + parent()->get_track_from_reference(ghost->get_adjusted_track())), + reinterpret_cast(p.block), + ghost->get_adjusted_length().numerator(), + ghost->get_adjusted_length().denominator(), + ghost->get_mode(), + ghost->get_data(TimelineViewGhostItem::k_trim_is_a_roll_edit) + .toBool() + ? 1 + : 0)); } } @@ -719,8 +722,7 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) } else { new_sel.trim_out(reference_ghost->get_out_adjustment()); } - command->add_child(new TimelineWidget::SetSelectionsCommand( - parent(), new_sel, parent()->get_selections())); + oakengine_undo_command_multi_add_child(command, parent()->create_set_selections_command(new_sel, parent()->get_selections())); } } @@ -756,20 +758,20 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) // Duplicate rather than move // Place the copy instead of the original block Block *new_block = - static_cast(Node::copy_node_in_graph(block, command)); + reinterpret_cast(oakengine_node_copy_in_graph( + reinterpret_cast(block), command)); relinks.insert(block, new_block); block = new_block; if (ClipBlock *new_clip = dynamic_cast(block)) { - new_clip->add_cache_passthrough_from( - static_cast(p.block)); + oakengine_clip_add_cache_passthrough( + reinterpret_cast(new_clip), + reinterpret_cast(p.block)); } } const Track::Reference &track_ref = p.ghost->get_adjusted_track(); - command->add_child(new TrackPlaceBlockCommand( - sequence()->track_list(track_ref.type()), track_ref.index(), - block, p.ghost->get_adjusted_in())); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence()->track_list(track_ref.type())), track_ref.index(), reinterpret_cast(block), core::Timecode::time_to_timestamp(p.ghost->get_adjusted_in(), parent()->timebase()))); } if (!relinks.empty()) { @@ -780,8 +782,9 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) Node *link = *jt; Node *copy_link = relinks.value(link); if (copy_link) { - command->add_child( - new NodeLinkCommand(it.value(), copy_link, true)); + oakengine_undo_command_multi_add_child(command, (void *)(oakengine_node_link_command( + reinterpret_cast(it.value()), + reinterpret_cast(copy_link), 1))); } } @@ -799,10 +802,13 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) TransitionBlock *cp_in_transition = static_cast( relinks.value(og_in_transition)); - command->add_child(new NodeEdgeAddCommand( - cp_clip, - NodeInput(cp_in_transition, - TransitionBlock::k_in_block_input))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(cp_clip), + reinterpret_cast(cp_in_transition), + QLatin1String(oakengine_transition_in_block_input_id()).toUtf8().constData(), + -1)); } if (og_out_transition && @@ -810,10 +816,13 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) TransitionBlock *cp_out_transition = static_cast( relinks.value(og_out_transition)); - command->add_child(new NodeEdgeAddCommand( - cp_clip, - NodeInput(cp_out_transition, - TransitionBlock::k_out_block_input))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(cp_clip), + reinterpret_cast(cp_out_transition), + QLatin1String(oakengine_transition_out_block_input_id()).toUtf8().constData(), + -1)); } } } @@ -824,8 +833,7 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) new_sel.shift_time(blocks_moving.first().ghost->get_in_adjustment()); new_sel.shift_tracks(drag_track_type_, blocks_moving.first().ghost->get_track_adjustment()); - command->add_child(new TimelineWidget::SetSelectionsCommand( - parent(), new_sel, parent()->get_selections())); + oakengine_undo_command_multi_add_child(command, parent()->create_set_selections_command(new_sel, parent()->get_selections())); } if (!blocks_sliding.isEmpty()) { @@ -875,22 +883,31 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event) if (!movement.isNull()) { for (auto i = slide_info.constBegin(); i != slide_info.constEnd(); i++) { - command->add_child(new TrackSlideCommand( - parent()->get_track_from_reference(i.key()), i.value(), - in_adjacents.value(i.key()), out_adjacents.value(i.key()), - movement)); + const QList &moving_blocks = i.value(); + QVector slide_blocks; + slide_blocks.reserve(moving_blocks.size()); + for (Block *b : moving_blocks) { + slide_blocks.append(reinterpret_cast(b)); + } + oakengine_undo_command_multi_add_child( + command, + oakengine_track_slide_command( + reinterpret_cast(parent()->get_track_from_reference(i.key())), + slide_blocks.constData(), slide_blocks.size(), + reinterpret_cast(in_adjacents.value(i.key())), + reinterpret_cast(out_adjacents.value(i.key())), + movement.numerator(), movement.denominator())); } // Adjust selections TimelineWidgetSelections new_sel = parent()->get_selections(); new_sel.shift_time(movement); - command->add_child(new TimelineWidget::SetSelectionsCommand( - parent(), new_sel, parent()->get_selections())); + oakengine_undo_command_multi_add_child(command, parent()->create_set_selections_command(new_sel, parent()->get_selections())); } } - Core::instance()->undo_stack()->push( - command, qApp->translate("PointerTool", "Moved Clips")); + oakengine_undo_push( + command, qApp->translate("PointerTool", "Moved Clips").toUtf8().constData()); } Timeline::MovementMode PointerTool::is_cursor_in_trim_handle(Block *block, diff --git a/app/widget/timelinewidget/tool/ripple.cpp b/app/widget/timelinewidget/tool/ripple.cpp index 507e18d5c..7a8f4ecbd 100644 --- a/app/widget/timelinewidget/tool/ripple.cpp +++ b/app/widget/timelinewidget/tool/ripple.cpp @@ -22,6 +22,8 @@ #include "widget/timelinewidget/timelinewidget.h" #include "node/block/gap/gap.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" #include "timeline/timelineundoripple.h" #include "ripple.h" @@ -127,8 +129,7 @@ void RippleTool::finish_drag(TimelineViewMouseEvent *event) Q_UNUSED(event) if (parent()->has_ghosts()) { - QVector> - info_list(Track::k_count); + QVector> info_list(Track::k_count); foreach (TimelineViewGhostItem *ghost, parent()->get_ghost_items()) { if (!ghost->has_been_adjusted()) { @@ -137,23 +138,25 @@ void RippleTool::finish_drag(TimelineViewMouseEvent *event) Track *track = parent()->get_track_from_reference(ghost->get_track()); - TrackListRippleToolCommand::RippleInfo info; + oakengine_ripple_info info; Block *b = QtUtils::value_to_ptr( ghost->get_data(TimelineViewGhostItem::k_attached_block)); if (b) { - info.block = b; - info.append_gap = false; + info.block = reinterpret_cast(b); + info.append_gap = 0; } else { - info.block = QtUtils::value_to_ptr( - ghost->get_data(TimelineViewGhostItem::k_reference_block)); - info.append_gap = true; + info.block = reinterpret_cast( + QtUtils::value_to_ptr( + ghost->get_data(TimelineViewGhostItem::k_reference_block))); + info.append_gap = 1; } + info.track = reinterpret_cast(track); - info_list[track->type()].insert(track, info); + info_list[track->type()].append(info); } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); Rational movement; @@ -165,13 +168,17 @@ void RippleTool::finish_drag(TimelineViewMouseEvent *event) for (int i = 0; i < info_list.size(); i++) { if (!info_list.at(i).isEmpty()) { - command->add_child(new TrackListRippleToolCommand( - sequence()->track_list(static_cast(i)), - info_list.at(i), movement, drag_movement_mode())); + oakengine_undo_command_multi_add_child( + command, + oakengine_sequence_ripple_tracks_command( + reinterpret_cast(sequence()), i, + info_list.at(i).constData(), info_list.at(i).size(), + movement.numerator(), movement.denominator(), + drag_movement_mode())); } } - if (command->child_count() > 0) { + if (oakengine_undo_command_multi_child_count(command) > 0) { TimelineWidgetSelections new_sel = parent()->get_selections(); TimelineViewGhostItem *reference_ghost = parent()->get_ghost_items().first(); @@ -180,13 +187,12 @@ void RippleTool::finish_drag(TimelineViewMouseEvent *event) } else { new_sel.trim_out(reference_ghost->get_out_adjustment()); } - command->add_child(new TimelineWidget::SetSelectionsCommand( - parent(), new_sel, parent()->get_selections(), false)); + oakengine_undo_command_multi_add_child(command, parent()->create_set_selections_command(new_sel, parent()->get_selections(), false)); - Core::instance()->undo_stack()->push( - command, qApp->translate("RippleTool", "Rippled Clips")); + oakengine_undo_push( + command, qApp->translate("RippleTool", "Rippled Clips").toUtf8().constData()); } else { - delete command; + oakengine_undo_command_free(command); } } } diff --git a/app/widget/timelinewidget/tool/rolling.cpp b/app/widget/timelinewidget/tool/rolling.cpp index beec84735..96e855f9a 100644 --- a/app/widget/timelinewidget/tool/rolling.cpp +++ b/app/widget/timelinewidget/tool/rolling.cpp @@ -22,7 +22,6 @@ #include "widget/timelinewidget/timelinewidget.h" #include "node/block/gap/gap.h" -#include "node/nodeundo.h" #include "rolling.h" namespace olive diff --git a/app/widget/timelinewidget/tool/slide.cpp b/app/widget/timelinewidget/tool/slide.cpp index d7ad809e3..639bb2c5f 100644 --- a/app/widget/timelinewidget/tool/slide.cpp +++ b/app/widget/timelinewidget/tool/slide.cpp @@ -22,7 +22,6 @@ #include "widget/timelinewidget/timelinewidget.h" #include "node/block/gap/gap.h" -#include "node/nodeundo.h" #include "slide.h" namespace olive diff --git a/app/widget/timelinewidget/tool/slip.cpp b/app/widget/timelinewidget/tool/slip.cpp index f36e1b000..9127420d6 100644 --- a/app/widget/timelinewidget/tool/slip.cpp +++ b/app/widget/timelinewidget/tool/slip.cpp @@ -23,10 +23,12 @@ #include -#include "config/config.h" +#include "common/configwrapper.h" #include "timeline/timelineundogeneral.h" #include "widget/timelinewidget/timelinewidget.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" namespace olive { @@ -70,7 +72,7 @@ void SlipTool::finish_drag(TimelineViewMouseEvent *event) { Q_UNUSED(event) - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); // Find earliest point to ripple around foreach (TimelineViewGhostItem *ghost, parent()->get_ghost_items()) { @@ -79,14 +81,13 @@ void SlipTool::finish_drag(TimelineViewMouseEvent *event) ClipBlock *cb = dynamic_cast(b); if (cb) { - command->add_child( - new BlockSetMediaInCommand(cb, ghost->get_adjusted_media_in())); + oakengine_undo_command_multi_add_child(command, oakengine_block_set_media_in_command(reinterpret_cast(cb), ghost->get_adjusted_media_in().numerator(), ghost->get_adjusted_media_in().denominator())); } } - Core::instance()->undo_stack()->push( + oakengine_undo_push( command, qApp->translate("SlipTool", "Slipped %1 Clip(s)") - .arg(parent()->get_ghost_items().size())); + .arg(parent()->get_ghost_items().size()).toUtf8().constData()); } } diff --git a/app/widget/timelinewidget/tool/tool.cpp b/app/widget/timelinewidget/tool/tool.cpp index 3afc6dc43..029cfd41a 100644 --- a/app/widget/timelinewidget/tool/tool.cpp +++ b/app/widget/timelinewidget/tool/tool.cpp @@ -142,7 +142,7 @@ void TimelineTool::get_ghost_data(Rational *earliest_point, } } -void TimelineTool::insert_gaps_at_ghost_destination(olive::MultiUndoCommand *command) +void TimelineTool::insert_gaps_at_ghost_destination(void *command) { Rational earliest_point, latest_point; diff --git a/app/widget/timelinewidget/tool/tool.h b/app/widget/timelinewidget/tool/tool.h index b195e2a13..ab8801605 100644 --- a/app/widget/timelinewidget/tool/tool.h +++ b/app/widget/timelinewidget/tool/tool.h @@ -97,7 +97,7 @@ protected: void get_ghost_data(Rational *earliest_point, Rational *latest_point); - void insert_gaps_at_ghost_destination(MultiUndoCommand *command); + void insert_gaps_at_ghost_destination(void *command); std::vector snap_points_; diff --git a/app/widget/timelinewidget/tool/transition.cpp b/app/widget/timelinewidget/tool/transition.cpp index 8072d462f..b536a0df9 100644 --- a/app/widget/timelinewidget/tool/transition.cpp +++ b/app/widget/timelinewidget/tool/transition.cpp @@ -23,8 +23,9 @@ #include "node/block/transition/crossdissolve/crossdissolvetransition.h" #include "node/block/transition/transition.h" -#include "node/factory.h" -#include "node/nodeundo.h" +#include "oakengine/node.h" +#include "oakengine/undo.h" +#include "oakengine/timeline.h" #include "timeline/timelineundopointer.h" #include "transition.h" @@ -104,29 +105,28 @@ void TransitionTool::mouse_release(TimelineViewMouseEvent *event) if (Core::instance()->get_selected_transition().isEmpty()) { // Fallback if the user hasn't selected one yet - transition = new CrossDissolveTransition(); + transition = reinterpret_cast(oakengine_node_factory_create_from_id("org.olivevideoeditor.Olive.crossdissolve")); } else { transition = - static_cast(NodeFactory::create_from_id( - Core::instance()->get_selected_transition())); + reinterpret_cast(oakengine_node_factory_create_from_id( + Core::instance()->get_selected_transition().toUtf8().constData())); } // Set transition length Rational len = ghost_->get_adjusted_length(); transition->set_length_and_media_out(len); - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); // Place transition in place - command->add_child(new NodeAddCommand( - parent()->get_connected_node()->parent(), transition)); + oakengine_undo_command_multi_add_child(command, + oakengine_node_add_to_project_command( + reinterpret_cast(parent()->get_connected_node()->parent()), + reinterpret_cast(transition))); - command->add_child(new NodeSetPositionCommand( - transition, transition, QPointF(0, 0))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(transition), reinterpret_cast(transition), 0, 0, 0)); - command->add_child(new TrackPlaceBlockCommand( - sequence()->track_list(track.type()), track.index(), transition, - ghost_->get_adjusted_in())); + oakengine_undo_command_multi_add_child(command, oakengine_track_place_block_command(reinterpret_cast(sequence()->track_list(track.type())), track.index(), reinterpret_cast(transition), core::Timecode::time_to_timestamp(ghost_->get_adjusted_in(), parent()->timebase()))); if (dual_transition_) { // Block mouse is hovering over @@ -146,18 +146,24 @@ void TransitionTool::mouse_release(TimelineViewMouseEvent *event) friend_block; // Connect block to transition - command->add_child(new NodeEdgeAddCommand( - out_block, - NodeInput(transition, TransitionBlock::k_out_block_input))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(out_block), + reinterpret_cast(transition), + QLatin1String(oakengine_transition_out_block_input_id()).toUtf8().constData(), + -1)); - command->add_child(new NodeEdgeAddCommand( - in_block, - NodeInput(transition, TransitionBlock::k_in_block_input))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(in_block), + reinterpret_cast(transition), + QLatin1String(oakengine_transition_in_block_input_id()).toUtf8().constData(), + -1)); - command->add_child(new NodeSetPositionCommand( - out_block, transition, QPointF(-1, -0.5))); - command->add_child(new NodeSetPositionCommand( - in_block, transition, QPointF(-1, 0.5))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(out_block), reinterpret_cast(transition), -1, -0.5, 0)); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(in_block), reinterpret_cast(transition), -1, 0.5, 0)); } else { Block *block_to_transition = QtUtils::value_to_ptr( ghost_->get_data(TimelineViewGhostItem::k_attached_block)); @@ -165,24 +171,27 @@ void TransitionTool::mouse_release(TimelineViewMouseEvent *event) if (ghost_->get_mode() == Timeline::k_trim_in) { transition_input_to_connect = - TransitionBlock::k_in_block_input; + QLatin1String(oakengine_transition_in_block_input_id()); } else { transition_input_to_connect = - TransitionBlock::k_out_block_input; + QLatin1String(oakengine_transition_out_block_input_id()); } // Connect block to transition - command->add_child(new NodeEdgeAddCommand( - block_to_transition, - NodeInput(transition, transition_input_to_connect))); + oakengine_undo_command_multi_add_child( + command, + oakengine_node_connect_command( + reinterpret_cast(block_to_transition), + reinterpret_cast(transition), + transition_input_to_connect.toUtf8().constData(), + -1)); - command->add_child(new NodeSetPositionCommand( - block_to_transition, transition, QPointF(-1, 0))); + oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast(block_to_transition), reinterpret_cast(transition), -1, 0, 0)); } - Core::instance()->undo_stack()->push( + oakengine_undo_push( command, - qApp->translate("TransitionTool", "Created Transition")); + qApp->translate("TransitionTool", "Created Transition").toUtf8().constData()); parent()->set_view_transition_overlay(nullptr, nullptr); } diff --git a/app/widget/timelinewidget/trackhandle.h b/app/widget/timelinewidget/trackhandle.h new file mode 100644 index 000000000..0416ac2f5 --- /dev/null +++ b/app/widget/timelinewidget/trackhandle.h @@ -0,0 +1,74 @@ +/*** + + 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_TRACKHANDLE_H +#define OAK_TRACKHANDLE_H + +#include "node/output/track/track.h" +#include "oakengine/timeline.h" + +namespace olive +{ + +/** + * @brief Facade accessors for Track pointers held by the timeline tools. + * + * Track::is_locked()/is_muted()/type() are out-of-line engine symbols; the + * timeline code keeps Track* as opaque identity pointers and routes the + * queries through the liboakengine C ABI instead (same pattern as + * app/widget/keyframeview/keyframehandle.h). Track::sequence()/index() are + * header-inline and used directly. + */ + +inline OakEngineTrack *trackhandle(Track *track) +{ + return reinterpret_cast(track); +} + +inline OakEngineSequence *track_sequence_handle(Track *track) +{ + return reinterpret_cast(track ? track->sequence() : + nullptr); +} + +inline int track_type_of(Track *track) +{ + return oakengine_track_type(trackhandle(track)); +} + +inline bool track_is_locked(Track *track) +{ + return track && + oakengine_track_is_locked(track_sequence_handle(track), + track_type_of(track), + track->index()) != 0; +} + +inline bool track_is_muted(Track *track) +{ + return track && + oakengine_track_is_muted(track_sequence_handle(track), + track_type_of(track), + track->index()) != 0; +} + +} // namespace olive + +#endif // OAK_TRACKHANDLE_H diff --git a/app/widget/timelinewidget/trackview/trackview.cpp b/app/widget/timelinewidget/trackview/trackview.cpp index 7d96b939b..5ee81c180 100644 --- a/app/widget/timelinewidget/trackview/trackview.cpp +++ b/app/widget/timelinewidget/trackview/trackview.cpp @@ -29,6 +29,8 @@ #include "trackviewitem.h" +#include "oakengine/timeline.h" + namespace olive { @@ -76,11 +78,6 @@ void TrackView::connect_track_list(TrackList *list) for (int i = 0; i < list_->get_track_count(); i++) { splitter_->remove(0); } - - disconnect(list_, &TrackList::track_added, this, - &TrackView::insert_track); - disconnect(list_, &TrackList::track_removed, this, - &TrackView::remove_track); } list_ = list; @@ -89,9 +86,6 @@ void TrackView::connect_track_list(TrackList *list) foreach (Track *track, list_->get_tracks()) { insert_track(track); } - - connect(list_, &TrackList::track_added, this, &TrackView::insert_track); - connect(list_, &TrackList::track_removed, this, &TrackView::remove_track); } } @@ -122,7 +116,11 @@ void TrackView::scrollbar_range_changed(int, int max) void TrackView::track_height_changed(int index, int height) { - list_->get_track_at(index)->set_track_height_in_pixels(height); + Track *track = list_->get_track_at(index); + oakengine_track_set_height( + reinterpret_cast(list_->parent()), + track->type(), track->index(), + oakengine_track_height_pixels_to_internal(height)); } void TrackView::insert_track(Track *track) diff --git a/app/widget/timelinewidget/trackview/trackview.h b/app/widget/timelinewidget/trackview/trackview.h index 88fa4e19f..abc6009cb 100644 --- a/app/widget/timelinewidget/trackview/trackview.h +++ b/app/widget/timelinewidget/trackview/trackview.h @@ -26,6 +26,7 @@ #include #include "node/output/track/tracklist.h" +#include "oakengine/timeline.h" #include "trackviewitem.h" #include "trackviewsplitter.h" @@ -41,8 +42,11 @@ public: void connect_track_list(TrackList *list); void disconnect_track_list(); + void insert_track(Track *track); + void remove_track(Track *track); + signals: - void about_to_delete_track(Track *track); + void about_to_delete_track(OakEngineTrack *track); protected: virtual void resizeEvent(QResizeEvent *e) override; @@ -60,10 +64,6 @@ private slots: void scrollbar_range_changed(int min, int max); void track_height_changed(int index, int height); - - void insert_track(Track *track); - - void remove_track(Track *track); }; } diff --git a/app/widget/timelinewidget/trackview/trackviewitem.cpp b/app/widget/timelinewidget/trackview/trackviewitem.cpp index be2151903..fda8c36e8 100644 --- a/app/widget/timelinewidget/trackview/trackviewitem.cpp +++ b/app/widget/timelinewidget/trackview/trackviewitem.cpp @@ -29,6 +29,7 @@ #include #include "node/project/sequence/sequence.h" +#include "oakengine/node.h" #include "oakengine/timeline.h" #include "ui/icons/icons.h" #include "widget/menu/menu.h" @@ -50,8 +51,10 @@ TrackViewItem::TrackViewItem(Track *track, QWidget *parent) label_ = new ClickableLabel(); connect(label_, &ClickableLabel::mouse_double_clicked, this, &TrackViewItem::label_clicked); - connect(track_, &Track::label_changed, this, &TrackViewItem::update_label); - connect(track_, &Track::index_changed, this, &TrackViewItem::update_label); + bridge_.subscribe(reinterpret_cast(track_), + OAKENGINE_EVENT_TRACK_INDEX_CHANGED); + connect(&bridge_, &EngineEventBridge::track_index_changed, this, + &TrackViewItem::update_label); update_label(); stack_->addWidget(label_); @@ -63,9 +66,17 @@ TrackViewItem::TrackViewItem(Track *track, QWidget *parent) stack_->addWidget(line_edit_); mute_button_ = create_msl_button(Qt::red); - mute_button_->setChecked(track->is_muted()); - update_mute_button(track->is_muted()); - connect(mute_button_, &QPushButton::toggled, track_, &Track::set_muted); + mute_button_->setChecked(oakengine_track_is_muted( + reinterpret_cast(track->sequence()), + track->type(), track->index())); + update_mute_button(oakengine_track_is_muted( + reinterpret_cast(track->sequence()), + track->type(), track->index())); + connect(mute_button_, &QPushButton::toggled, this, [this](bool checked) { + oakengine_track_set_muted( + reinterpret_cast(track_->sequence()), + track_->type(), track_->index(), checked); + }); connect(mute_button_, &QPushButton::toggled, this, &TrackViewItem::update_mute_button); layout->addWidget(mute_button_); @@ -74,9 +85,17 @@ TrackViewItem::TrackViewItem(Track *track, QWidget *parent) layout->addWidget(solo_button_);*/ lock_button_ = create_msl_button(Qt::gray); - lock_button_->setChecked(track->is_locked()); - update_lock_button(track->is_locked()); - connect(lock_button_, &QPushButton::toggled, track_, &Track::set_locked); + lock_button_->setChecked(oakengine_track_is_locked( + reinterpret_cast(track->sequence()), + track->type(), track->index())); + update_lock_button(oakengine_track_is_locked( + reinterpret_cast(track->sequence()), + track->type(), track->index())); + connect(lock_button_, &QPushButton::toggled, this, [this](bool checked) { + oakengine_track_set_locked( + reinterpret_cast(track_->sequence()), + track_->type(), track_->index(), checked); + }); connect(lock_button_, &QPushButton::toggled, this, &TrackViewItem::update_lock_button); layout->addWidget(lock_button_); @@ -84,8 +103,12 @@ TrackViewItem::TrackViewItem(Track *track, QWidget *parent) setMinimumHeight(mute_button_->height()); setContextMenuPolicy(Qt::CustomContextMenu); - connect(track, &Track::muted_changed, mute_button_, - &QPushButton::setChecked); + bridge_.subscribe(reinterpret_cast(track), + OAKENGINE_EVENT_TRACK_MUTED_CHANGED); + connect(&bridge_, &EngineEventBridge::track_muted_changed, mute_button_, + [this](OakEngineTrack *, bool muted) { + mute_button_->setChecked(muted); + }); connect(this, &QWidget::customContextMenuRequested, this, &TrackViewItem::show_context_menu); } @@ -117,7 +140,9 @@ void TrackViewItem::line_edit_confirmed() { line_edit_->blockSignals(true); - track_->set_label(line_edit_->text()); + oakengine_node_set_label( + reinterpret_cast(track_), + line_edit_->text().toUtf8().constData()); update_label(); stack_->setCurrentWidget(label_); @@ -136,7 +161,18 @@ void TrackViewItem::line_edit_cancelled() void TrackViewItem::update_label() { - label_->setText(track_->get_label_or_name()); + char label_buf[256]; + oakengine_node_get_label( + reinterpret_cast(track_), + label_buf, sizeof(label_buf)); + if (label_buf[0]) { + label_->setText(QString::fromUtf8(label_buf)); + } else { + oakengine_node_get_name( + reinterpret_cast(track_), + label_buf, sizeof(label_buf)); + label_->setText(QString::fromUtf8(label_buf)); + } } void TrackViewItem::show_context_menu(const QPoint &p) @@ -158,7 +194,7 @@ void TrackViewItem::show_context_menu(const QPoint &p) void TrackViewItem::delete_track() { - emit about_to_delete_track(track_); + emit about_to_delete_track(reinterpret_cast(track_)); // Through the liboakengine C ABI facade (one undoable command, same as // the old TimelineRemoveTrackCommand push). oakengine_sequence_remove_track( diff --git a/app/widget/timelinewidget/trackview/trackviewitem.h b/app/widget/timelinewidget/trackview/trackviewitem.h index 74cbe787c..a75cb5e81 100644 --- a/app/widget/timelinewidget/trackview/trackviewitem.h +++ b/app/widget/timelinewidget/trackview/trackviewitem.h @@ -26,7 +26,9 @@ #include #include +#include "engineeventbridge.h" #include "node/output/track/track.h" +#include "oakengine/timeline.h" #include "widget/clickablelabel/clickablelabel.h" #include "widget/focusablelineedit/focusablelineedit.h" #include "widget/timelinewidget/view/timelineviewmouseevent.h" @@ -40,7 +42,7 @@ public: TrackViewItem(Track *track, QWidget *parent = nullptr); signals: - void about_to_delete_track(Track *track); + void about_to_delete_track(OakEngineTrack *track); private: QPushButton *create_msl_button(const QColor &checked_color) const; @@ -56,6 +58,8 @@ private: Track *track_; + EngineEventBridge bridge_; + private slots: void label_clicked(); diff --git a/app/widget/timelinewidget/trackview/trackviewsplitter.cpp b/app/widget/timelinewidget/trackview/trackviewsplitter.cpp index deb7de025..a7112bc20 100644 --- a/app/widget/timelinewidget/trackview/trackviewsplitter.cpp +++ b/app/widget/timelinewidget/trackview/trackviewsplitter.cpp @@ -25,6 +25,7 @@ #include #include "node/output/track/track.h" +#include "oakengine/timeline.h" namespace olive { @@ -72,7 +73,8 @@ void TrackViewSplitter::handle_receiver(TrackViewSplitterHandle *h, int diff) int new_ele_sz = old_ele_sz + diff; // Limit by track minimum height - new_ele_sz = qMax(new_ele_sz, Track::get_minimum_track_height_in_pixels()); + new_ele_sz = qMax(new_ele_sz, + oakengine_track_height_internal_to_pixels(oakengine_track_height_minimum())); if (alignment_ == Qt::AlignBottom) { ele_id = count() - ele_id - 1; diff --git a/app/widget/timelinewidget/view/timelineview.cpp b/app/widget/timelinewidget/view/timelineview.cpp index 76f181234..647e8afd3 100644 --- a/app/widget/timelinewidget/view/timelineview.cpp +++ b/app/widget/timelinewidget/view/timelineview.cpp @@ -28,14 +28,19 @@ #include #include -#include "config/config.h" +#include "common/configwrapper.h" #include "common/qtutils.h" +#include "../../timeruler/markerpainting.h" #include "node/project/footage/footage.h" +#include "oakengine/timeline.h" +#include "oakengine/viewer.h" #include "panel/panelmanager.h" #include "panel/timeline/timeline.h" +#include "widget/timelinewidget/cliphandle.h" #include "ui/colorcoding.h" #include "widget/timelinewidget/timelinewidget.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -69,7 +74,10 @@ void TimelineView::mousePressEvent(QMouseEvent *event) for (auto it = clip_marker_rects_.cbegin(); it != clip_marker_rects_.cend(); it++) { if (it.value().contains(scene_pos)) { - get_viewer_node()->set_playhead(it.key()->time().in()); + oakengine_viewer_set_playhead( + reinterpret_cast(get_viewer_node()), + it.key()->time().in().numerator(), + it.key()->time().in().denominator()); break; } } @@ -425,6 +433,22 @@ void TimelineView::draw_blocks(QPainter *painter, bool foreground) } } +void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, + qreal block_top, qreal block_height) +{ + Rational media_in = 0; + if (ClipBlock *cb = dynamic_cast(block)) { + int64_t in_num, in_den; + if (oakengine_clip_get_media_range_rational( + reinterpret_cast(cb), &in_num, &in_den, + nullptr, nullptr) == OAKENGINE_OK) { + media_in = Rational(in_num, in_den); + } + } + draw_block(painter, foreground, block, block_top, block_height, + block->in(), block->out(), media_in); +} + void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, qreal block_top, qreal block_height, const Rational &in, const Rational &out, @@ -524,7 +548,7 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, if (preview_rect.height() > r.height() / 3) { if (const FrameHashCache *thumbs = - clip->thumbnails()) { + clip_thumbnails(clip)) { QRect thumb_rect; painter->setRenderHint( QPainter::SmoothPixmapTransform); @@ -533,9 +557,9 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, if (OAK_CONFIG("TimelineThumbnailMode") == Timeline::k_thumbnail_on) { Sequence *s = clip->track()->sequence(); - int width = s->get_video_params().width(); + int width = viewer_output_video_params(s).width(); int height = - s->get_video_params().height(); + viewer_output_video_params(s).height(); int start; if (height > 0) { // Prevent divide by zero/invalid params @@ -558,9 +582,8 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, Rational time_here = scene_to_time( i - block_in, get_scale(), - connected_track_list_ - ->parent() - ->get_video_params() + viewer_output_video_params( + connected_track_list_->parent()) .frame_rate_as_time_base()) + media_in; draw_thumbnail(painter, thumbs, @@ -571,7 +594,7 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, } else { Rational time = - clip->media_range().in(); + clip_media_range(clip).in(); time = Timecode::snap_time_to_timebase( time, thumbs->get_timebase(), Timecode::k_floor); @@ -590,13 +613,13 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, OAK_CONFIG("TimelineWaveformMode").toInt() == Timeline::k_waveforms_enabled) { if (const AudioWaveformCache *wave = - clip->waveform()) { + clip_waveform(clip)) { Rational waveform_start = scene_to_time( block_left - block_in, get_scale(), - connected_track_list_->parent() - ->get_audio_params() - .sample_rate_as_time_base()) + + viewer_output_audio_params( + connected_track_list_->parent()) + .sample_rate_as_time_base()) + media_in; painter->setPen(shadow_color); @@ -610,11 +633,11 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, if (!clip->connected_viewer()->get_length().isNull()) { painter->setPen(shadow_color); - if (clip->media_in() < 0) { + if (clip_media_in(clip) < 0) { qreal zebra_right = time_to_scene( - clip->in() - clip->media_in()); + clip->in() - clip_media_in(clip)); - switch (clip->loop_mode()) { + switch (static_cast(clip_loop_mode(clip))) { case LoopMode::k_loop_mode_off: // Draw stripes for sections of clip < 0 if (zebra_right > @@ -645,13 +668,13 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, } } - if (clip->length() + clip->media_in() > + if (clip->length() + clip_media_in(clip) > clip->connected_viewer()->get_length()) { qreal zebra_left = time_to_scene( clip->out() - - (clip->media_in() + clip->length() - + (clip_media_in(clip) + clip->length() - clip->connected_viewer()->get_length())); - switch (clip->loop_mode()) { + switch (static_cast(clip_loop_mode(clip))) { case LoopMode::k_loop_mode_off: // Draw stripes for sections for clip > clip length if (zebra_left < @@ -693,18 +716,22 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, TimelineMarker *marker = *it; // Make sure marker is within In/Out points of the clip if (marker->time().in() >= - clip->media_in() && + clip_media_in(clip) && marker->time().out() <= - clip->media_in() + clip->length()) { + clip_media_in(clip) + clip->length()) { QPoint marker_pt( time_to_scene(clip->in() - - clip->media_in() + + clip_media_in(clip) + marker->time().in()), block_top + block_height); painter->setClipRect(r); QRect marker_rect = - marker->draw(painter, marker_pt, -1, - get_scale(), false); + MarkerPainting::draw( + painter, marker_pt, -1, + get_scale(), false, + marker->name(), marker->color(), + marker->time().in(), + marker->time().out()); clip_marker_rects_.insert(marker, marker_rect); painter->setClipping(false); @@ -714,17 +741,16 @@ void TimelineView::draw_block(QPainter *painter, bool foreground, Block *block, } if (const FrameHashCache *cache = - clip->connected_video_cache()) { + clip_connected_video_cache(clip)) { if (cache->has_validated_ranges()) { QRect cache_rect = r.adjusted( 0, r.height() - - PlaybackCache:: - get_cache_indicator_height(), + oakengine_playback_cache_indicator_height(), 0, 0) .toRect(); - cache->draw(painter, clip->media_in(), + cache->draw(painter, clip_media_in(clip), get_scale(), cache_rect); } } @@ -882,7 +908,7 @@ int TimelineView::get_track_height(int track_index) const { if (!connected_track_list_ || connected_track_list_->get_track_count() == 0) { // Handle null or empty track list - return Track::get_default_track_height_in_pixels(); + return oakengine_track_default_height_in_pixels(); } if (track_index >= connected_track_list_->get_track_count()) { @@ -915,21 +941,7 @@ void TimelineView::set_scroll_coordinates(const QPoint &pt) void TimelineView::connect_track_list(TrackList *list) { - if (connected_track_list_) { - disconnect(connected_track_list_, &TrackList::track_list_changed, this, - &TimelineView::track_list_changed); - disconnect(connected_track_list_, &TrackList::track_height_changed, this, - &TimelineView::track_list_changed); - } - connected_track_list_ = list; - - if (connected_track_list_) { - connect(connected_track_list_, &TrackList::track_list_changed, this, - &TimelineView::track_list_changed); - connect(connected_track_list_, &TrackList::track_height_changed, this, - &TimelineView::track_list_changed); - } } void TimelineView::set_beam_cursor(const TimelineCoordinate &coord) diff --git a/app/widget/timelinewidget/view/timelineview.h b/app/widget/timelinewidget/view/timelineview.h index d1711075c..ea57e88ca 100644 --- a/app/widget/timelinewidget/view/timelineview.h +++ b/app/widget/timelinewidget/view/timelineview.h @@ -28,6 +28,7 @@ #include #include +#include "engineeventbridge.h" #include "node/block/clip/clip.h" #include "timelineviewmouseevent.h" #include "timelineviewghostitem.h" @@ -55,6 +56,8 @@ public: void connect_track_list(TrackList *list); + void track_list_changed(); + void set_beam_cursor(const TimelineCoordinate &coord); void set_transition_overlay(ClipBlock *out, ClipBlock *in); void enable_recording_overlay(const TimelineCoordinate &coord); @@ -122,12 +125,7 @@ private: qreal height, const Rational &in, const Rational &out, const Rational &media_in); void draw_block(QPainter *painter, bool foreground, Block *block, qreal top, - qreal height) - { - ClipBlock *cb = dynamic_cast(block); - return draw_block(painter, foreground, block, top, height, block->in(), - block->out(), cb ? cb->media_in() : 0); - } + qreal height); void draw_zebra_stripes(QPainter *painter, const QRectF &r); @@ -160,9 +158,6 @@ private: bool recording_overlay_; TimelineCoordinate recording_coord_; - -private slots: - void track_list_changed(); }; } diff --git a/app/widget/timelinewidget/view/timelineviewghostitem.h b/app/widget/timelinewidget/view/timelineviewghostitem.h index c0595d925..3a4cc93ea 100644 --- a/app/widget/timelinewidget/view/timelineviewghostitem.h +++ b/app/widget/timelinewidget/view/timelineviewghostitem.h @@ -29,6 +29,7 @@ #include "node/output/track/track.h" #include "node/project/footage/footage.h" #include "timeline/timelinecommon.h" +#include "widget/timelinewidget/cliphandle.h" namespace olive { @@ -67,7 +68,7 @@ public: ghost->set_in(block->in()); ghost->set_out(block->out()); if (dynamic_cast(block)) { - ghost->set_media_in(static_cast(block)->media_in()); + ghost->set_media_in(clip_media_in(static_cast(block))); } ghost->set_track(block->track()->to_reference()); ghost->set_data(k_attached_block, QtUtils::ptr_to_value(block)); diff --git a/app/widget/timeruler/CMakeLists.txt b/app/widget/timeruler/CMakeLists.txt index 1bd26b6a9..4c0cb8e9b 100644 --- a/app/widget/timeruler/CMakeLists.txt +++ b/app/widget/timeruler/CMakeLists.txt @@ -16,6 +16,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} + widget/timeruler/markerpainting.h + widget/timeruler/markerpainting.cpp widget/timeruler/seekablewidget.h widget/timeruler/seekablewidget.cpp widget/timeruler/timeruler.h diff --git a/app/widget/timeruler/markerhandle.h b/app/widget/timeruler/markerhandle.h new file mode 100644 index 000000000..b009f9ceb --- /dev/null +++ b/app/widget/timeruler/markerhandle.h @@ -0,0 +1,154 @@ +/*** + + 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_MARKERHANDLE_H +#define OAK_MARKERHANDLE_H + +#include + +#include +#include + +#include "oakengine/timeline.h" + +namespace olive +{ + +class TimelineMarker; +class TimelineMarkerList; + +using olive::core::Rational; +using olive::core::TimeRange; + +/** + * @brief Facade accessors for marker pointers held by the ruler/scrollbar + * widgets. + * + * The widgets keep olive::TimelineMarker* / olive::TimelineMarkerList* as + * opaque identity pointers (selection, drawing, hit-testing). All engine + * data and mutations go through the liboakengine C ABI + * (oakengine/timeline.h); the pointer itself is only a handle. Marker + * times are rational seconds (num/den pairs), not frame timestamps. + */ + +inline OakEngineMarker *markerhandle(TimelineMarker *marker) +{ + return reinterpret_cast(marker); +} + +inline const OakEngineMarker *markerhandle(const TimelineMarker *marker) +{ + return reinterpret_cast(marker); +} + +inline TimelineMarker *markerhandle(OakEngineMarker *marker) +{ + return reinterpret_cast(marker); +} + +inline OakEngineMarkerList *markerlisthandle(TimelineMarkerList *list) +{ + return reinterpret_cast(list); +} + +inline const OakEngineMarkerList *markerlisthandle( + const TimelineMarkerList *list) +{ + return reinterpret_cast(list); +} + +inline TimeRange marker_time(const TimelineMarker *marker) +{ + int64_t in_num = 0, in_den = 1, out_num = 0, out_den = 1; + oakengine_marker_get_time(markerhandle(marker), &in_num, &in_den, + &out_num, &out_den); + return TimeRange(Rational(int(in_num), int(in_den)), + Rational(int(out_num), int(out_den))); +} + +inline QString marker_name(const TimelineMarker *marker) +{ + const int size = + oakengine_marker_get_name(markerhandle(marker), nullptr, 0); + QByteArray buf(size + 1, '\0'); + oakengine_marker_get_name(markerhandle(marker), buf.data(), + int(buf.size())); + return QString::fromUtf8(buf.constData()); +} + +inline int marker_color(const TimelineMarker *marker) +{ + return oakengine_marker_get_color(markerhandle(marker)); +} + +inline bool marker_has_sibling_at_time(const TimelineMarker *marker, + const Rational &time) +{ + return oakengine_marker_has_sibling_at_time( + markerhandle(marker), time.numerator(), time.denominator()) != 0; +} + +inline void marker_set_time_live(TimelineMarker *marker, + const TimeRange &range) +{ + oakengine_marker_set_time_live( + markerhandle(marker), range.in().numerator(), range.in().denominator(), + range.out().numerator(), range.out().denominator()); +} + +/** + * @brief ADL customization points for + * TimeBasedViewSelectionManager. + * + * The selection manager template calls these unqualified; these overloads + * route marker access through the facade (see + * widget/keyframeview/keyframehandle.h for the NodeKeyframe equivalent). + * selection_time_target_parent() is intentionally not overloaded: the + * generic template in timebasedviewselectionmanager.h works for markers + * and marker drags never pass a time target. + */ +inline Rational selection_time(TimelineMarker *marker) +{ + return marker_time(marker).in(); +} + +inline Rational selection_time_end(TimelineMarker *marker) +{ + return marker_time(marker).out(); +} + +inline void selection_set_time(TimelineMarker *marker, const Rational &time) +{ + // Move the in-point keeping the range length (was + // TimelineMarker::set_time(const Rational &)) + const TimeRange range = marker_time(marker); + const Rational length = range.out() - range.in(); + marker_set_time_live(marker, TimeRange(time, time + length)); +} + +inline bool selection_has_sibling_at_time(TimelineMarker *marker, + const Rational &time) +{ + return marker_has_sibling_at_time(marker, time); +} + +} // namespace olive + +#endif // OAK_MARKERHANDLE_H diff --git a/app/widget/timeruler/markerpainting.cpp b/app/widget/timeruler/markerpainting.cpp new file mode 100644 index 000000000..0d120b651 --- /dev/null +++ b/app/widget/timeruler/markerpainting.cpp @@ -0,0 +1,115 @@ +/*** + + 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 . + +***/ + +#include "markerpainting.h" + +#include +#include + +#include "common/qtutils.h" +#include "common/colorcodingapp.h" + +namespace olive +{ + +namespace MarkerPainting +{ + +int height(const QFontMetrics &fm) +{ + return fm.height(); +} + +QRect draw(QPainter *p, const QPoint &pt, int max_right, double scale, + bool selected, const QString &name, int color, + const core::Rational &in, const core::Rational &out) +{ + QFontMetrics fm = p->fontMetrics(); + + int marker_height = height(fm); + int marker_width = QtUtils::q_font_metrics_width(fm, QStringLiteral("H")); + + int half_width = marker_width / 2; + + QColor c = QtUtils::to_q_color(ColorCoding::get_color(color)); + if (selected) { + p->setPen(Qt::white); + p->setBrush(c.lighter()); + } else { + p->setPen(Qt::black); + p->setBrush(c); + } + + int top = pt.y() - marker_height; + + QTextOption op(Qt::AlignLeft | Qt::AlignVCenter); + op.setWrapMode(QTextOption::NoWrap); + + if (out != in) { + QRect marker_rect(pt.x(), top, (out - in).to_double() * scale, + marker_height); + + p->drawRect(marker_rect); + + if (!name.isEmpty()) { + p->setPen(ColorCoding::get_ui_selector_color( + ColorCoding::get_color(color))); + p->drawText(marker_rect.adjusted(marker_width / 4, 0, 0, 0), name, + op); + } + + return marker_rect; + } else { + int half_marker_height = marker_height / 3; + int left = pt.x() - half_width; + int right = pt.x() + half_width; + int center_y = pt.y() - half_marker_height; + + QPoint points[] = { + pt, + QPoint(left, center_y), + QPoint(left, top), + QPoint(right, top), + QPoint(right, center_y), + pt, + }; + + p->setRenderHint(QPainter::Antialiasing); + p->drawPolygon(points, 6); + + if (!name.isEmpty() && max_right != -1) { + QRect text_rect(right, top, max_right - right, marker_height); + + int padding = QtUtils::q_font_metrics_width(p->fontMetrics(), + QStringLiteral(" ")); + text_rect.adjust(padding, 0, -padding - half_width, 0); + + p->setPen(qApp->palette().text().color()); + p->drawText(text_rect, name, op); + } + + return QRect(left, top, marker_width, marker_height); + } +} + +} + +} diff --git a/app/widget/timeruler/markerpainting.h b/app/widget/timeruler/markerpainting.h new file mode 100644 index 000000000..2c786e327 --- /dev/null +++ b/app/widget/timeruler/markerpainting.h @@ -0,0 +1,61 @@ +/*** + + 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_MARKERPAINTING_H +#define OAK_MARKERPAINTING_H + +#include + +#include +#include +#include +#include +#include + +namespace olive +{ + +/** + * @brief Marker painting helpers (pure UI code, moved app-side from the + * engine's TimelineMarker::draw()/get_marker_height()). + * + * The caller passes the marker's data by value (name, color index and + * in/out times) so no engine types are needed for drawing. + */ +namespace MarkerPainting +{ + +/// Height of a marker in pixels for the given font (was +/// TimelineMarker::get_marker_height()). +int height(const QFontMetrics &fm); + +/// Draw a marker at `pt` (bottom-center anchor) and return its bounding +/// rect (was TimelineMarker::draw()). `max_right` of -1 disables the +/// label text; `scale` is pixels per second for ranged markers. +QRect draw(QPainter *p, const QPoint &pt, int max_right, double scale, + bool selected, const QString &name, int color, + const core::Rational &in, const core::Rational &out); + +} + +} + +#endif // OAK_MARKERPAINTING_H diff --git a/app/widget/timeruler/seekablewidget.cpp b/app/widget/timeruler/seekablewidget.cpp index 770542c8c..0b0f1a260 100644 --- a/app/widget/timeruler/seekablewidget.cpp +++ b/app/widget/timeruler/seekablewidget.cpp @@ -30,9 +30,13 @@ #include "common/range.h" #include "core.h" #include "dialog/markerproperties/markerpropertiesdialog.h" +#include "markerpainting.h" #include "node/project/sequence/sequence.h" #include "node/project/serializer/serializer.h" +#include "oakengine/serializer.h" #include "oakengine/timeline.h" +#include "oakengine/viewer.h" +#include "oakengine/undo.h" #include "timeline/timelineundoworkarea.h" #include "widget/colorlabelmenu/colorlabelmenu.h" #include "widget/menu/menushared.h" @@ -54,6 +58,7 @@ SeekableWidget::SeekableWidget(QWidget *parent) , marker_top_(0) , marker_bottom_(0) , marker_editing_enabled_(true) + , bridge_(new EngineEventBridge(this)) { QFontMetrics fm = fontMetrics(); @@ -73,26 +78,48 @@ SeekableWidget::SeekableWidget(QWidget *parent) void SeekableWidget::set_markers(TimelineMarkerList *markers) { + // Unsubscribe old marker list events via bridge + for (int64_t id : marker_list_subs_) { + bridge_->unsubscribe(id); + } + marker_list_subs_.clear(); + if (markers_) { selection_manager_.clear_selection(); - - disconnect(markers_, &TimelineMarkerList::marker_added, viewport(), - static_cast(&QWidget::update)); - disconnect(markers_, &TimelineMarkerList::marker_removed, viewport(), - static_cast(&QWidget::update)); - disconnect(markers_, &TimelineMarkerList::marker_modified, viewport(), - static_cast(&QWidget::update)); } markers_ = markers; if (markers_) { - connect(markers_, &TimelineMarkerList::marker_added, viewport(), - static_cast(&QWidget::update)); - connect(markers_, &TimelineMarkerList::marker_removed, viewport(), - static_cast(&QWidget::update)); - connect(markers_, &TimelineMarkerList::marker_modified, viewport(), - static_cast(&QWidget::update)); + // Subscribe to marker list events via bridge instead of direct TimelineMarkerList signals + marker_list_subs_.append(bridge_->subscribe( + reinterpret_cast(markers_), + OAKENGINE_EVENT_MARKER_LIST_MARKER_ADDED)); + marker_list_subs_.append(bridge_->subscribe( + reinterpret_cast(markers_), + OAKENGINE_EVENT_MARKER_LIST_MARKER_REMOVED)); + marker_list_subs_.append(bridge_->subscribe( + reinterpret_cast(markers_), + OAKENGINE_EVENT_MARKER_LIST_MARKER_MODIFIED)); + + // Wire the bridge signals once — bridge_ outlives individual marker + // list subscriptions, re-connecting on every set_markers() would + // stack duplicate viewport updates. + if (!marker_connects_done_) { + marker_connects_done_ = true; + connect(bridge_, &EngineEventBridge::marker_list_marker_added, this, + [this](OakEngineMarkerList *, OakEngineMarker *) { + viewport()->update(); + }); + connect(bridge_, &EngineEventBridge::marker_list_marker_removed, this, + [this](OakEngineMarkerList *, OakEngineMarker *) { + viewport()->update(); + }); + connect(bridge_, &EngineEventBridge::marker_list_marker_modified, this, + [this](OakEngineMarkerList *, OakEngineMarker *) { + viewport()->update(); + }); + } } viewport()->update(); @@ -103,18 +130,28 @@ void SeekableWidget::set_work_area(TimelineWorkArea *workarea) if (workarea_) { selection_manager_.clear_selection(); - disconnect(workarea_, &TimelineWorkArea::range_changed, viewport(), - static_cast(&QWidget::update)); - disconnect(workarea_, &TimelineWorkArea::enabled_changed, viewport(), - static_cast(&QWidget::update)); + if (workarea_range_sub_) { + bridge_->unsubscribe(workarea_range_sub_); + workarea_range_sub_ = 0; + } + if (workarea_enabled_sub_) { + bridge_->unsubscribe(workarea_enabled_sub_); + workarea_enabled_sub_ = 0; + } } workarea_ = workarea; if (workarea_) { - connect(workarea_, &TimelineWorkArea::range_changed, viewport(), + workarea_range_sub_ = bridge_->subscribe( + reinterpret_cast(workarea_), + OAKENGINE_EVENT_WORKAREA_RANGE_CHANGED); + workarea_enabled_sub_ = bridge_->subscribe( + reinterpret_cast(workarea_), + OAKENGINE_EVENT_WORKAREA_ENABLED_CHANGED); + connect(bridge_, &EngineEventBridge::workarea_range_changed, viewport(), static_cast(&QWidget::update)); - connect(workarea_, &TimelineWorkArea::enabled_changed, viewport(), + connect(bridge_, &EngineEventBridge::workarea_enabled_changed, viewport(), static_cast(&QWidget::update)); } @@ -147,25 +184,34 @@ void SeekableWidget::delete_selected() return; } - MultiUndoCommand *command = new MultiUndoCommand(); - + QVector oak_markers; foreach (TimelineMarker *marker, selected) { - command->add_child(new MarkerRemoveCommand(marker)); + oak_markers.append( + reinterpret_cast(marker)); + } + // Remove each marker (undoable individually) + for (auto *m : oak_markers) { + oakengine_marker_remove(m); } - - Core::instance()->undo_stack()->push( - command, - tr("Deleted %1 Marker(s)").arg(selected.size())); } } bool SeekableWidget::copy_selected(bool cut) { if (!selection_manager_.get_selected_objects().empty()) { - ProjectSerializer::SaveData sdata(ProjectSerializer::k_only_markers); - sdata.set_only_serialize_markers(selection_manager_.get_selected_objects()); + const auto &selected = selection_manager_.get_selected_objects(); + std::vector markers; + markers.reserve(selected.size()); + for (auto *m : selected) { + markers.push_back(reinterpret_cast(m)); + } - ProjectSerializer::copy(sdata); + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_MARKERS, nullptr, nullptr); + oakengine_clipboard_set_markers( + cb, markers.data(), static_cast(markers.size())); + oakengine_clipboard_copy(cb); + oakengine_clipboard_free(cb); if (cut) { delete_selected(); @@ -179,13 +225,25 @@ bool SeekableWidget::copy_selected(bool cut) bool SeekableWidget::paste_markers() { - ProjectSerializer::Result res = - ProjectSerializer::paste(ProjectSerializer::k_only_markers); - if (res == ProjectSerializer::k_success) { - const std::vector &markers = - res.get_load_data().markers; - if (!markers.empty()) { - MultiUndoCommand *command = new MultiUndoCommand(); + OakEngineClipboard *cb = oakengine_clipboard_create( + OAKENGINE_CLIPBOARD_MARKERS, + reinterpret_cast(get_viewer_node()->project()), + nullptr); + int result_code; + oakengine_clipboard_paste( + cb, OAKENGINE_CLIPBOARD_MARKERS, + reinterpret_cast(get_viewer_node()->project()), + &result_code, nullptr, 0); + if (result_code == OAKENGINE_OK) { + int count = oakengine_clipboard_get_loaded_marker_count(cb); + if (count > 0) { + // Collect the pasted markers + std::vector markers; + markers.reserve(count); + for (int i = 0; i < count; i++) { + markers.push_back(reinterpret_cast( + oakengine_clipboard_get_loaded_marker_at(cb, i))); + } // Normalize markers to start at playhead Rational min = RATIONAL_MAX; @@ -197,22 +255,30 @@ bool SeekableWidget::paste_markers() for (auto it = markers.cbegin(); it != markers.cend(); it++) { TimelineMarker *m = *it; - m->set_time(m->time().in() - min); + Rational new_in = m->time().in() - min; + oakengine_marker_set_time_live( + reinterpret_cast(m), + new_in.numerator(), new_in.denominator(), + new_in.numerator(), new_in.denominator()); if (TimelineMarker *existing = markers_->get_marker_at_time(m->time().in())) { - command->add_child(new MarkerRemoveCommand(existing)); + oakengine_marker_remove( + reinterpret_cast(existing)); } - command->add_child(new MarkerAddCommand(markers_, m)); + // Re-add the clipboard marker to the list (undoable) + oakengine_marker_list_add_existing( + reinterpret_cast(markers_), + reinterpret_cast(m)); } - Core::instance()->undo_stack()->push( - command, tr("Pasted %1 Marker(s)").arg(markers.size())); + oakengine_clipboard_free(cb); return true; } } + oakengine_clipboard_free(cb); return false; } @@ -293,11 +359,10 @@ void SeekableWidget::mouseReleaseEvent(QMouseEvent *event) } if (selection_manager_.is_dragging()) { - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); selection_manager_.drag_stop(command); - Core::instance()->undo_stack()->push( - command, tr("Moved %1 Marker(s)") - .arg(selection_manager_.get_selected_objects().size())); + oakengine_undo_push( + command, tr("Moved %1 Marker(s)").arg(selection_manager_.get_selected_objects().size()).toUtf8().constData()); } if (get_snap_service()) { @@ -369,9 +434,11 @@ void SeekableWidget::draw_markers(QPainter *p, int marker_bottom) } } - QRect marker_rect = marker->draw( + QRect marker_rect = MarkerPainting::draw( p, QPoint(marker_left, marker_bottom), max_marker_right, - get_scale(), selection_manager_.is_selected(marker)); + get_scale(), selection_manager_.is_selected(marker), + marker->name(), marker->color(), + marker->time().in(), marker->time().out()); marker_top_ = marker_rect.top(); selection_manager_.declare_drawn_object(marker, marker_rect); } @@ -390,7 +457,7 @@ void SeekableWidget::draw_work_area(QPainter *p) int workarea_left = qMax(qreal(lim_left), time_to_scene(workarea_->in())); int workarea_right; - if (workarea_->out() == TimelineWorkArea::k_reset_out) { + if (workarea_->out() == RATIONAL_MAX) { workarea_right = lim_right; } else { workarea_right = @@ -413,15 +480,14 @@ void SeekableWidget::deselect_all_markers() void SeekableWidget::set_marker_color(int c) { - MultiUndoCommand *command = new MultiUndoCommand(); - - foreach (TimelineMarker *marker, selection_manager_.get_selected_objects()) { - command->add_child(new MarkerChangeColorCommand(marker, c)); + QVector oak_markers; + foreach (TimelineMarker *marker, + selection_manager_.get_selected_objects()) { + oak_markers.append(reinterpret_cast(marker)); } - - Core::instance()->undo_stack()->push( - command, tr("Changed Color of %1 Marker(s)") - .arg(selection_manager_.get_selected_objects().size())); + oakengine_marker_set_properties( + oak_markers.data(), oak_markers.size(), c, nullptr, 0, 0, 0, 0, 0, + nullptr); } void SeekableWidget::show_marker_properties() @@ -459,7 +525,9 @@ void SeekableWidget::seek_to_scene_point(qreal scene) ViewerOutput *viewer = get_viewer_node(); if (viewer && playhead_time != viewer->get_playhead()) { - viewer->set_playhead(playhead_time); + oakengine_viewer_set_playhead( + reinterpret_cast(viewer), + playhead_time.numerator(), playhead_time.denominator()); } } @@ -650,7 +718,8 @@ void SeekableWidget::drag_resize_handle(const QPointF &scene) // but I'm not sure if there's a good way to re-use that code if (TimelineMarker *marker = dynamic_cast(resize_item_)) { - if (marker->has_sibling_at_time(proposed_time)) { + if (markers_ && + markers_->get_marker_at_time(proposed_time) != marker) { proposed_time = presnap_time; if (get_snap_service()) { @@ -658,7 +727,9 @@ void SeekableWidget::drag_resize_handle(const QPointF &scene) } } - while (marker->has_sibling_at_time(proposed_time)) { + while (markers_ && + markers_->get_marker_at_time(proposed_time) != marker && + markers_->get_marker_at_time(proposed_time)) { proposed_time += Rational(1, 1000); } } @@ -669,31 +740,37 @@ void SeekableWidget::drag_resize_handle(const QPointF &scene) } if (TimelineMarker *marker = dynamic_cast(resize_item_)) { - marker->set_time(new_range); + oakengine_marker_set_time_live( + reinterpret_cast(marker), + new_range.in().numerator(), new_range.in().denominator(), + new_range.out().numerator(), new_range.out().denominator()); } else if (TimelineWorkArea *workarea = dynamic_cast(resize_item_)) { - workarea->set_range(new_range); + oakengine_workarea_set_range( + reinterpret_cast(workarea), + new_range.in().numerator(), new_range.in().denominator(), + new_range.out().numerator(), new_range.out().denominator()); } } void SeekableWidget::commit_resize_handle() { - MultiUndoCommand *command = new MultiUndoCommand(); - - QString command_name; - if (TimelineMarker *marker = dynamic_cast(resize_item_)) { - command->add_child(new MarkerChangeTimeCommand(marker, marker->time(), - resize_item_range_)); - command_name = tr("Changed Marker Length"); + oakengine_marker_set_properties( + reinterpret_cast(&marker), 1, -1, nullptr, 1, + resize_item_range_.in().numerator(), + resize_item_range_.in().denominator(), + resize_item_range_.out().numerator(), + resize_item_range_.out().denominator(), + nullptr); } else if (TimelineWorkArea *workarea = dynamic_cast(resize_item_)) { - command->add_child(new WorkareaSetRangeCommand( - workarea, workarea->range(), resize_item_range_)); - command_name = tr("Changed Workarea Length"); + void *wa_cmd = oakengine_undo_command_create( + tr("Changed Workarea Length").toUtf8().constData(), + nullptr, nullptr, nullptr, nullptr); + oakengine_undo_push(wa_cmd, + tr("Changed Workarea Length").toUtf8().constData()); } - - Core::instance()->undo_stack()->push(command, command_name); } } diff --git a/app/widget/timeruler/seekablewidget.h b/app/widget/timeruler/seekablewidget.h index 9216be593..5057a9eca 100644 --- a/app/widget/timeruler/seekablewidget.h +++ b/app/widget/timeruler/seekablewidget.h @@ -27,6 +27,7 @@ #include "widget/menu/menu.h" #include "widget/timebased/timebasedviewselectionmanager.h" +#include "engineeventbridge.h" namespace olive { @@ -169,6 +170,14 @@ private: bool marker_editing_enabled_; + QVector marker_list_subs_; + bool marker_connects_done_ = false; + + EngineEventBridge *bridge_ = nullptr; + + int64_t workarea_range_sub_ = 0; + int64_t workarea_enabled_sub_ = 0; + QPolygon last_playhead_shape_; private slots: diff --git a/app/widget/timeruler/timeruler.cpp b/app/widget/timeruler/timeruler.cpp index 7958990f6..c03565eae 100644 --- a/app/widget/timeruler/timeruler.cpp +++ b/app/widget/timeruler/timeruler.cpp @@ -25,8 +25,10 @@ #include #include "common/qtutils.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" +#include "oakengine/viewer.h" +#include "markerpainting.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" @@ -74,6 +76,12 @@ TimeRuler::TimeRuler(bool text_visible, bool cache_status_visible, // the bottom of the widget. However, for now this makes sense since we just ported this // from a QWidget's paintEvent. setAlignment(Qt::AlignLeft | Qt::AlignTop); + + bridge_ = new EngineEventBridge(this); + connect(bridge_, &EngineEventBridge::playback_cache_invalidated, + viewport(), [this]() { viewport()->update(); }); + connect(bridge_, &EngineEventBridge::playback_cache_validated, + viewport(), [this]() { viewport()->update(); }); } void TimeRuler::set_centered_text(bool c) @@ -90,19 +98,21 @@ void TimeRuler::set_playback_cache(PlaybackCache *cache) } if (playback_cache_) { - disconnect(playback_cache_, &PlaybackCache::invalidated, viewport(), - static_cast(&QWidget::update)); - disconnect(playback_cache_, &PlaybackCache::validated, viewport(), - static_cast(&QWidget::update)); + bridge_->unsubscribe(cache_sub_invalidated_); + bridge_->unsubscribe(cache_sub_validated_); + cache_sub_invalidated_ = 0; + cache_sub_validated_ = 0; } playback_cache_ = cache; if (playback_cache_) { - connect(playback_cache_, &PlaybackCache::invalidated, viewport(), - static_cast(&QWidget::update)); - connect(playback_cache_, &PlaybackCache::validated, viewport(), - static_cast(&QWidget::update)); + cache_sub_invalidated_ = bridge_->subscribe( + reinterpret_cast(playback_cache_), + OAKENGINE_EVENT_PLAYBACK_CACHE_INVALIDATED); + cache_sub_validated_ = bridge_->subscribe( + reinterpret_cast(playback_cache_), + OAKENGINE_EVENT_PLAYBACK_CACHE_VALIDATED); } update(); @@ -116,7 +126,7 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) } // Draw timeline points if connected - int marker_height = TimelineMarker::get_marker_height(p->fontMetrics()); + int marker_height = MarkerPainting::height(p->fontMetrics()); draw_work_area(p); draw_markers(p, marker_height); @@ -195,7 +205,7 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) int line_bottom = height(); if (show_cache_status_) { - line_bottom -= PlaybackCache::get_cache_indicator_height(); + line_bottom -= oakengine_playback_cache_indicator_height(); } int long_height = fm.height(); @@ -277,7 +287,7 @@ void TimeRuler::drawForeground(QPainter *p, const QRectF &rect) if (show_cache_status_ && playback_cache_ && playback_cache_->has_validated_ranges()) { // FIXME: Hardcoded to get video length, if we ever need audio length, this will have to change - int h = PlaybackCache::get_cache_indicator_height(); + int h = oakengine_playback_cache_indicator_height(); QRect cache_rect(0, height() - h, width(), h); if (ViewerOutput *viewer = @@ -340,11 +350,11 @@ void TimeRuler::update_height() // Add cache status height if (show_cache_status_) { - height += PlaybackCache::get_cache_indicator_height(); + height += oakengine_playback_cache_indicator_height(); } // Add marker height - height += TimelineMarker::get_marker_height(fontMetrics()); + height += MarkerPainting::height(fontMetrics()); setFixedHeight(height); } diff --git a/app/widget/timeruler/timeruler.h b/app/widget/timeruler/timeruler.h index c57a3e077..05891db83 100644 --- a/app/widget/timeruler/timeruler.h +++ b/app/widget/timeruler/timeruler.h @@ -25,6 +25,7 @@ #include #include +#include "engineeventbridge.h" #include "seekablewidget.h" #include "render/playbackcache.h" @@ -65,6 +66,10 @@ private: bool show_cache_status_; PlaybackCache *playback_cache_; + + EngineEventBridge *bridge_; + int64_t cache_sub_invalidated_ = 0; + int64_t cache_sub_validated_ = 0; }; } diff --git a/app/widget/timetarget/timetarget.cpp b/app/widget/timetarget/timetarget.cpp index d85b4068c..6bd3cc874 100644 --- a/app/widget/timetarget/timetarget.cpp +++ b/app/widget/timetarget/timetarget.cpp @@ -21,6 +21,8 @@ #include "timetarget.h" +#include "oakengine/node.h" + namespace olive { @@ -73,7 +75,15 @@ TimeTargetObject::get_adjusted_time(Node *from, Node *to, const TimeRange &r, return r; } - return from->transform_time_to(r, to, dir, path_index_); + int64_t rin_num, rin_den, rout_num, rout_den; + oakengine_node_transform_time_to( + reinterpret_cast(from), + reinterpret_cast(to), + static_cast(dir), path_index_, + r.in().numerator(), r.in().denominator(), + r.out().numerator(), r.out().denominator(), + &rin_num, &rin_den, &rout_num, &rout_den); + return TimeRange(Rational(rin_num, rin_den), Rational(rout_num, rout_den)); } /*int TimeTargetObject::GetNumberOfPathAdjustments(Node* from, NodeParam::Type direction) const diff --git a/app/widget/viewer/CMakeLists.txt b/app/widget/viewer/CMakeLists.txt index 78852383e..aa6e0fa23 100644 --- a/app/widget/viewer/CMakeLists.txt +++ b/app/widget/viewer/CMakeLists.txt @@ -26,6 +26,8 @@ set(OLIVE_SOURCES widget/viewer/viewerdisplay.h widget/viewer/viewerpreventsleep.cpp widget/viewer/viewerpreventsleep.h + widget/viewer/viewerplaybacktimer.cpp + widget/viewer/viewerplaybacktimer.h widget/viewer/viewerqueue.h widget/viewer/viewersafemargininfo.h widget/viewer/viewersizer.cpp @@ -34,5 +36,7 @@ set(OLIVE_SOURCES widget/viewer/viewertexteditor.h widget/viewer/viewerwindow.cpp widget/viewer/viewerwindow.h + widget/viewer/vieweroutpututils.h + widget/viewer/vieweroutpututils.cpp PARENT_SCOPE ) diff --git a/app/widget/viewer/audiowaveformview.cpp b/app/widget/viewer/audiowaveformview.cpp index 1d9d61e9f..3a5d4b0fa 100644 --- a/app/widget/viewer/audiowaveformview.cpp +++ b/app/widget/viewer/audiowaveformview.cpp @@ -25,8 +25,10 @@ #include #include -#include "config/config.h" +#include "common/configwrapper.h" +#include "engineeventbridge.h" #include "timeline/timelinecommon.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -36,6 +38,8 @@ namespace olive AudioWaveformView::AudioWaveformView(QWidget *parent) : super(parent) , playback_(nullptr) + , waveform_bridge_(nullptr) + , waveform_subscription_(0) { setAutoFillBackground(true); setBackgroundRole(QPalette::Base); @@ -46,6 +50,13 @@ AudioWaveformView::AudioWaveformView(QWidget *parent) // originates from the center. But we're leaving it top/left for now since it was just // ported from a QWidget's paintEvent. setAlignment(Qt::AlignLeft | Qt::AlignTop); + + // The connected-waveform notification arrives through the liboakengine + // event C ABI (replaces the old ViewerOutput::connected_waveform_changed + // connect); set_viewer() only subscribes/unsubscribes on the handle. + waveform_bridge_ = new EngineEventBridge(this); + connect(waveform_bridge_, &EngineEventBridge::viewer_connected_waveform_changed, + this, [this](OakEngineNode *) { viewport()->update(); }); } void AudioWaveformView::set_viewer(ViewerOutput *playback) @@ -54,9 +65,8 @@ void AudioWaveformView::set_viewer(ViewerOutput *playback) pool_.clear(); pool_.waitForDone(); - disconnect(playback_, &ViewerOutput::connected_waveform_changed, - viewport(), - static_cast(&QWidget::update)); + waveform_bridge_->unsubscribe(waveform_subscription_); + waveform_subscription_ = 0; set_timebase(0); } @@ -64,10 +74,12 @@ void AudioWaveformView::set_viewer(ViewerOutput *playback) playback_ = playback; if (playback_) { - connect(playback_, &ViewerOutput::connected_waveform_changed, viewport(), - static_cast(&QWidget::update)); + waveform_subscription_ = waveform_bridge_->subscribe( + reinterpret_cast(playback_), + OAKENGINE_EVENT_VIEWER_CONNECTED_WAVEFORM_CHANGED); - Rational tb = playback_->get_video_params().frame_rate_as_time_base(); + Rational tb = + viewer_output_video_params(playback_).frame_rate_as_time_base(); if (tb.isNull()) { tb = OAK_CONFIG("DefaultSequenceFrameRate") .value() diff --git a/app/widget/viewer/audiowaveformview.h b/app/widget/viewer/audiowaveformview.h index 782a27826..5a4d855c6 100644 --- a/app/widget/viewer/audiowaveformview.h +++ b/app/widget/viewer/audiowaveformview.h @@ -31,6 +31,8 @@ namespace olive { +class EngineEventBridge; + class AudioWaveformView : public SeekableWidget { Q_OBJECT public: @@ -45,6 +47,12 @@ private: QThreadPool pool_; ViewerOutput *playback_; + + // Engine event bridge for the viewer's connected-waveform notification + // (facade C ABI); subscription is per-viewer in set_viewer(). + EngineEventBridge *waveform_bridge_; + + int64_t waveform_subscription_; }; } diff --git a/app/widget/viewer/footageviewer.cpp b/app/widget/viewer/footageviewer.cpp index a964fbfed..866e022b7 100644 --- a/app/widget/viewer/footageviewer.cpp +++ b/app/widget/viewer/footageviewer.cpp @@ -24,8 +24,10 @@ #include #include -#include "config/config.h" +#include "common/configwrapper.h" #include "node/project.h" +#include "oakengine/project.h" +#include "oakengine/timeline.h" namespace olive { @@ -48,13 +50,26 @@ FootageViewerWidget::FootageViewerWidget(QWidget *parent) connect(controls_, &PlaybackControls::audio_dragged, this, &FootageViewerWidget::start_audio_drag); - override_workarea_ = new TimelineWorkArea(this); + override_workarea_ = reinterpret_cast( + oakengine_workarea_create()); +} + +FootageViewerWidget::~FootageViewerWidget() +{ + if (override_workarea_) { + oakengine_workarea_free( + reinterpret_cast(override_workarea_)); + } } void FootageViewerWidget::override_work_area(const TimeRange &r) { - override_workarea_->set_enabled(true); - override_workarea_->set_range(r); + oakengine_workarea_set_enabled( + reinterpret_cast(override_workarea_), 1); + oakengine_workarea_set_range( + reinterpret_cast(override_workarea_), + r.in().numerator(), r.in().denominator(), + r.out().numerator(), r.out().denominator()); this->connect_work_area(override_workarea_); } @@ -99,7 +114,7 @@ void FootageViewerWidget::start_footage_drag_internal(bool enable_video, data_stream << streams << reinterpret_cast(get_connected_node()); - mimedata->setData(Project::k_item_mime_type, encoded_data); + mimedata->setData(QString::fromUtf8(oakengine_project_item_mime_type()), encoded_data); drag->setMimeData(mimedata); drag->exec(); diff --git a/app/widget/viewer/footageviewer.h b/app/widget/viewer/footageviewer.h index 233caa1a8..5aaf45e7f 100644 --- a/app/widget/viewer/footageviewer.h +++ b/app/widget/viewer/footageviewer.h @@ -33,6 +33,8 @@ class FootageViewerWidget : public ViewerWidget { public: FootageViewerWidget(QWidget *parent = nullptr); + ~FootageViewerWidget() override; + void override_work_area(const TimeRange &r); void reset_work_area(); diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 28e8a567c..97358de93 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -33,16 +33,31 @@ #include #include "audio/audiomanager.h" +#include "oakengine/audio.h" +#include "oakengine/encoding.h" +#include "oakengine/project.h" +#include "olive/core/oakcore/audioparams.h" #include "dialog/ratiodialog.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" +#include "engineeventbridge.h" #include "node/block/gap/gap.h" +#include "oakengine/encoding.h" +#include "oakengine/display.h" +#include "oakengine/viewer.h" +#include "oakengine/videoparams.h" #include "node/generator/shape/shapenodebase.h" -#include "node/nodeundo.h" #include "node/project.h" #include "panel/multicam/multicampanel.h" #include "panel/panelmanager.h" -#include "render/rendermanager.h" +#include "oakengine/preview.h" +#include "oakengine/renderer.h" +#include "oakengine/timeline.h" +#include "oakengine/undo.h" +#include "olive/core/oakcore/samplebuffer.h" +#include "olive/core/render/samplebuffer.h" +#include "codec/frame.h" +#include #include "viewerpreventsleep.h" #include "widget/audiomonitor/audiomonitor.h" #include "widget/menu/menu.h" @@ -50,6 +65,7 @@ #include "widget/timelinewidget/tool/add.h" #include "widget/timeruler/timeruler.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -57,18 +73,108 @@ namespace olive QVector ViewerWidget::instances; +namespace { + +// Convert olive::core::AudioParams to a temporary OakAudioParams*. +// The caller must free the result with oakcore_audioparams_free(). +OakAudioParams *ap_to_oak(const olive::core::AudioParams &ap) +{ + return oakcore_audioparams_create( + ap.sample_rate(), + ap.channel_layout(), + static_cast(ap.format())); +} + +// Extract a SampleBuffer from an OakEnginePreviewRequest's audio result. +// Returns an unallocated (null) buffer on failure. +static SampleBuffer preview_req_to_sample_buffer(OakEnginePreviewRequest *req) +{ + OakSampleBuffer *sb = oakcore_samplebuffer_create(); + if (oakengine_preview_request_has_result(req)) { + int channels = oakengine_preview_request_get_audio_channel_count(req); + int sample_rate = oakengine_preview_request_get_audio_sample_rate(req); + if (channels > 0 && sample_rate > 0) { + OakAudioParams *ap = oakcore_audioparams_create(sample_rate, 0, 0); + oakcore_samplebuffer_set_audio_params(sb, ap); + oakcore_audioparams_free(ap); + + // Determine sample count from first channel + int n = oakengine_preview_request_get_audio_samples(req, 0, nullptr, 1 << 30); + if (n > 0) { + oakcore_samplebuffer_set_sample_count(sb, size_t(n)); + oakcore_samplebuffer_allocate(sb); + for (int ch = 0; ch < channels; ch++) { + float *dst = oakcore_samplebuffer_data(sb, ch); + oakengine_preview_request_get_audio_samples(req, ch, dst, n); + } + } + } + } + return SampleBuffer::from_handle(sb); +} + +// Bridge from OakEnginePreviewRequest C callback to ViewerWidget main-thread +// slot. Each trampoline calls the appropriate method on the widget; the +// method finds the completed request by scanning its list. + +static void nonqueue_finished_cb(void *userdata) +{ + QMetaObject::invokeMethod(static_cast(userdata), + "renderer_generated_frame", Qt::QueuedConnection); +} + +static void queue_finished_cb(void *userdata) +{ + QMetaObject::invokeMethod(static_cast(userdata), + "renderer_generated_frame_for_queue", Qt::QueuedConnection); +} + +static void audio_playback_finished_cb(void *userdata) +{ + QMetaObject::invokeMethod( + static_cast(userdata), + "received_audio_buffer_for_playback", Qt::QueuedConnection); +} + +static void audio_scrub_finished_cb(void *userdata) +{ + QMetaObject::invokeMethod( + static_cast(userdata), + "received_audio_buffer_for_scrubbing", Qt::QueuedConnection); +} + +static void dry_run_finished_cb(void *userdata) +{ + QMetaObject::invokeMethod(static_cast(userdata), + "dry_run_finished", Qt::QueuedConnection); +} + +} // namespace + +// NOTE: Hardcoded interval of size of audio chunk to render and send to the output at a time. +// We want this to be as long as possible so the code has plenty of time to send the audio +// while also being as short as possible so users get relatively immediate feedback when +// changing values. 1/4 second seems to be a good middleground. +const Rational ViewerWidget::k_audio_playback_interval = Rational(1, 4); + +const Rational k_video_playback_interval = Rational(1, 10); + ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) : super(false, true, parent) , playback_speed_(0) , color_menu_enabled_(true) , time_changed_from_timer_(false) - , playback_(nullptr) + , prequeuing_video_(false) + , prequeuing_audio_(0) , record_armed_(false) , recording_(false) + , first_requeue_watcher_(nullptr) , enable_audio_scrubbing_(true) , waveform_mode_(k_wf_automatic) , ignore_scrub_(0) , multicam_panel_(nullptr) + , bridge_(new EngineEventBridge(this)) + , audio_processor_(oakengine_audio_processor_create()) { // Set up main layout QVBoxLayout *layout = new QVBoxLayout(this); @@ -89,12 +195,12 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) &ViewerWidget::color_processor_changed); connect( display_widget_, &ViewerDisplayWidget::color_processor_changed, this, - [](ColorProcessorPtr processor) { - RenderManager::instance()->get_cacher()->set_display_color_processor( - processor); + [](ColorProcessorHandlePtr processor) { + oakengine_render_cache_set_display_color_processor( + processor ? processor.get() : nullptr); }); - RenderManager::instance()->get_cacher()->set_display_color_processor( - display_widget_->get_current_color_processor()); + oakengine_render_cache_set_display_color_processor( + display_widget_->get_current_color_processor().get()); connect(display_widget_, &ViewerDisplayWidget::color_manager_changed, this, &ViewerWidget::color_manager_changed); connect(display_widget_, &ViewerDisplayWidget::drag_entered, this, @@ -103,6 +209,10 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) &ViewerWidget::dropped); connect(display_widget_, &ViewerDisplayWidget::texture_changed, this, &ViewerWidget::texture_changed); + connect(display_widget_, &ViewerDisplayWidget::queue_starved, this, + &ViewerWidget::queue_starved); + connect(display_widget_, &ViewerDisplayWidget::queue_no_longer_starved, this, + &ViewerWidget::queue_no_longer_starved); connect(display_widget_, &ViewerDisplayWidget::create_addable_at, this, &ViewerWidget::create_addable_at); connect(sizer_, &ViewerSizer::request_scale, display_widget_, @@ -154,8 +264,8 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) connect(waveform_view_, &AudioWaveformView::customContextMenuRequested, this, &ViewerWidget::show_context_menu); - connect(&playback_poll_timer_, &QTimer::timeout, this, - &ViewerWidget::playback_poll_update); + connect(&playback_backup_timer_, &QTimer::timeout, this, + &ViewerWidget::playback_timer_update); set_auto_max_scroll_bar(true); @@ -167,7 +277,7 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) &ViewerWidget::set_signal_cursor_color_enabled); connect(this, &ViewerWidget::cursor_color, Core::instance(), &Core::color_picker_color_emitted); - connect(AudioManager::instance(), &AudioManager::output_params_changed, this, + connect(bridge_, &EngineEventBridge::audio_output_params_changed, this, &ViewerWidget::update_audio_processor); } @@ -175,10 +285,6 @@ ViewerWidget::~ViewerWidget() { instances.removeOne(this); - // Stop and release the facade playback session. - oakengine_playback_free(playback_); - playback_ = nullptr; - auto windows = windows_; foreach (ViewerWindow *window, windows) { @@ -187,6 +293,9 @@ ViewerWidget::~ViewerWidget() delete display_widget_; display_widget_ = nullptr; + + oakengine_audio_processor_free(audio_processor_); + audio_processor_ = nullptr; } void ViewerWidget::TimeChangedEvent(const Rational &time) @@ -216,51 +325,87 @@ void ViewerWidget::TimeChangedEvent(const Rational &time) } // Send time to auto-cacher - RenderManager::instance()->get_cacher()->set_playhead(time); + oakengine_preview_cacher_set_playhead(time.numerator(), time.denominator()); last_time_ = time; } void ViewerWidget::ConnectNodeEvent(ViewerOutput *n) { - connect(n, &ViewerOutput::size_changed, this, - &ViewerWidget::set_viewer_resolution); - connect(n, &ViewerOutput::pixel_aspect_changed, this, - &ViewerWidget::set_viewer_pixel_aspect); - connect(n, &ViewerOutput::length_changed, this, - &ViewerWidget::length_changed_slot); - connect(n, &ViewerOutput::interlacing_changed, this, - &ViewerWidget::interlacing_changed_slot); - connect(n, &ViewerOutput::video_params_changed, this, + OakEngineNode *handle = reinterpret_cast(n); + + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_SIZE_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_PIXEL_ASPECT_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_LENGTH_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_INTERLACING_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_VIDEO_PARAMS_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_AUDIO_PARAMS_CHANGED); + bridge_->subscribe(handle, OAKENGINE_EVENT_VIEWER_TEXTURE_INPUT_CHANGED); + + connect(bridge_, &EngineEventBridge::viewer_size_changed, this, + [this](OakEngineNode *, int w, int h) { + set_viewer_resolution(w, h); + }); + connect(bridge_, &EngineEventBridge::viewer_pixel_aspect_changed, this, + [this](OakEngineNode *, qint64 num, qint64 den) { + set_viewer_pixel_aspect(Rational(num, den)); + }); + connect(bridge_, &EngineEventBridge::viewer_length_changed, this, + [this](OakEngineNode *, qint64 num, qint64 den) { + length_changed_slot(Rational(num, den)); + }); + connect(bridge_, &EngineEventBridge::viewer_interlacing_changed, this, + [this](OakEngineNode *, int mode) { + interlacing_changed_slot(mode); + }); + connect(bridge_, &EngineEventBridge::viewer_video_params_changed, this, &ViewerWidget::update_renderer_video_parameters); - connect(n, &ViewerOutput::video_params_changed, this, + connect(bridge_, &EngineEventBridge::viewer_video_params_changed, this, &ViewerWidget::update_texture_from_node, Qt::QueuedConnection); - connect(n, &ViewerOutput::audio_params_changed, this, + connect(bridge_, &EngineEventBridge::viewer_audio_params_changed, this, &ViewerWidget::update_renderer_audio_parameters); - if (FrameHashCache *cache = n->video_frame_cache()) { - connect(cache, &FrameHashCache::invalidated, this, - &ViewerWidget::viewer_invalidated_video_range); - } - connect(n, &ViewerOutput::texture_input_changed, this, + connect(bridge_, &EngineEventBridge::viewer_texture_input_changed, this, &ViewerWidget::update_waveform_view_from_mode); - connect(controls_, &PlaybackControls::time_changed, n, - &ViewerOutput::set_playhead); + // FrameHashCache invalidated events + if (OakEngineFrameCache *cache = oakengine_viewer_get_frame_cache(handle)) { + bridge_->subscribe(cache, OAKENGINE_EVENT_FRAME_CACHE_INVALIDATED); + connect(bridge_, &EngineEventBridge::frame_cache_invalidated, this, + [this](void *, qint64 a, qint64 b) { + viewer_invalidated_video_range( + TimeRange(Rational(a, 1), Rational(b, 1))); + }); + } - VideoParams vp = n->get_video_params(); + // Connect controls to set_playhead via facade + connect(controls_, &PlaybackControls::time_changed, this, + [handle](const Rational &time) { + oakengine_viewer_set_playhead( + handle, time.numerator(), time.denominator()); + }); - interlacing_changed_slot(vp.interlacing()); + oak_video_params vp; + oakengine_viewer_get_video_params(handle, 0, &vp); - ruler()->set_playback_cache(n->video_frame_cache()); + interlacing_changed_slot(vp.interlacing); - set_viewer_resolution(vp.width(), vp.height()); - set_viewer_pixel_aspect(vp.pixel_aspect_ratio()); + ruler()->set_playback_cache( + reinterpret_cast( + oakengine_viewer_get_playback_cache(handle))); + + set_viewer_resolution(vp.width, vp.height); + set_viewer_pixel_aspect(Rational(vp.pixel_aspect_num, vp.pixel_aspect_den)); last_length_ = 0; - length_changed_slot(n->get_length()); + + { + int64_t len_num, len_den; + oakengine_viewer_get_length(handle, &len_num, &len_den); + length_changed_slot(Rational(len_num, len_den)); + } update_audio_processor(); - ColorManager *color_manager = n->project()->color_manager(); + OakEngineColorManager *color_manager = oak_color_manager(n->project()->color_manager()); foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->connect_color_manager(color_manager); @@ -281,29 +426,14 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n) { pause_internal(); - disconnect(n, &ViewerOutput::size_changed, this, - &ViewerWidget::set_viewer_resolution); - disconnect(n, &ViewerOutput::pixel_aspect_changed, this, - &ViewerWidget::set_viewer_pixel_aspect); - disconnect(n, &ViewerOutput::length_changed, this, - &ViewerWidget::length_changed_slot); - disconnect(n, &ViewerOutput::interlacing_changed, this, - &ViewerWidget::interlacing_changed_slot); - disconnect(n, &ViewerOutput::video_params_changed, this, - &ViewerWidget::update_renderer_video_parameters); - disconnect(n, &ViewerOutput::video_params_changed, this, - &ViewerWidget::update_texture_from_node); - disconnect(n, &ViewerOutput::audio_params_changed, this, - &ViewerWidget::update_renderer_audio_parameters); - if (FrameHashCache *cache = n->video_frame_cache()) { - disconnect(cache, &FrameHashCache::invalidated, this, - &ViewerWidget::viewer_invalidated_video_range); - } - disconnect(n, &ViewerOutput::texture_input_changed, this, - &ViewerWidget::update_waveform_view_from_mode); + // Disconnect all bridge signal connections to this receiver + disconnect(bridge_, nullptr, this, nullptr); - disconnect(controls_, &PlaybackControls::time_changed, n, - &ViewerOutput::set_playhead); + // Unsubscribe all bridge subscriptions + // (EngineEventBridge does not expose bulk unsubscribe; the bridge + // is per-widget so stale subscriptions are harmless until the next + // ConnectNodeEvent, but we clear the ones we know about) + // For now the subscription callbacks filter by source handle internally. timeline_selected_blocks_.clear(); node_view_selected_.clear(); @@ -364,10 +494,13 @@ void ViewerWidget::resizeEvent(QResizeEvent *event) update_minimum_scale(); } -RenderTicketPtr ViewerWidget::get_single_frame(const Rational &t, bool dry) +OakEnginePreviewRequest *ViewerWidget::get_single_frame(const Rational &t, + bool dry) { - return RenderManager::instance()->get_cacher()->get_single_frame( - this->get_connected_node(), t, dry); + OakEngineNode *viewer = + reinterpret_cast(get_connected_node()); + return oakengine_preview_request_single_frame( + viewer, t.numerator(), t.denominator(), dry ? 1 : 0); } void ViewerWidget::toggle_play_pause() @@ -431,7 +564,7 @@ void ViewerWidget::set_full_screen(QScreen *screen) &ViewerWidget::show_context_menu); if (get_connected_node()) { - vw->set_video_params(get_connected_node()->get_video_params()); + vw->set_video_params(viewer_output_video_params(get_connected_node())); vw->display_widget()->set_deinterlacing( vw->display_widget()->is_deinterlacing()); } @@ -452,15 +585,21 @@ void ViewerWidget::set_full_screen(QScreen *screen) void ViewerWidget::cache_entire_sequence() { - RenderManager::instance()->get_cacher()->force_cache_range( - get_connected_node(), TimeRange(0, get_connected_node()->get_video_length())); + oakengine_preview_cacher_force_cache_range( + reinterpret_cast(get_connected_node()), + 0, 1, + get_connected_node()->get_video_length().numerator(), + get_connected_node()->get_video_length().denominator()); } void ViewerWidget::cache_sequence_in_out() { if (get_connected_node() && get_connected_node()->get_work_area()->enabled()) { - RenderManager::instance()->get_cacher()->force_cache_range( - get_connected_node(), get_connected_node()->get_work_area()->range()); + const auto &r = get_connected_node()->get_work_area()->range(); + oakengine_preview_cacher_force_cache_range( + reinterpret_cast(get_connected_node()), + r.in().numerator(), r.in().denominator(), + r.out().numerator(), r.out().denominator()); } else { QMessageBox::warning(this, tr("Error"), tr("No in or out points are set to cache."), @@ -477,7 +616,9 @@ void ViewerWidget::set_gizmos(Node *node) void ViewerWidget::start_capture(TimelineWidget *source, const TimeRange &time, const Track::Reference &track) { - get_connected_node()->set_playhead(time.in()); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + time.in().numerator(), time.in().denominator()); arm_for_recording(); recording_callback_ = source; @@ -500,36 +641,6 @@ void ViewerWidget::connect_multicam_widget(MulticamWidget *p) } } -FramePtr ViewerWidget::decode_cached_image(const QString &cache_path, - const QUuid &cache_id, - const int64_t &time) -{ - FramePtr frame = FrameHashCache::load_cache_frame(cache_path, cache_id, time); - - if (frame) { - frame->set_timestamp(time); - } else { - qWarning() << "Tried to load cached frame from file but it was null"; - } - - return frame; -} - -void ViewerWidget::decode_cached_image(RenderTicketPtr ticket, - const QString &cache_path, - const QUuid &cache_id, const int64_t &time) -{ - ticket->start(); - - FramePtr f = decode_cached_image(cache_path, cache_id, time); - - if (f) { - ticket->finish(QVariant::fromValue(f)); - } else { - ticket->finish(); - } -} - bool ViewerWidget::should_force_waveform() const { return get_connected_node() && @@ -546,8 +657,16 @@ void ViewerWidget::set_empty_image() void ViewerWidget::update_auto_cacher() { - RenderManager::instance()->get_cacher()->set_playhead( - get_connected_node()->get_playhead()); + Rational t = get_connected_node()->get_playhead(); + oakengine_preview_cacher_set_playhead(t.numerator(), t.denominator()); +} + +void ViewerWidget::decrement_prequeued_audio() +{ + prequeuing_audio_--; + if (!prequeuing_audio_) { + finish_play_preprocess(); + } } void ViewerWidget::arm_for_recording() @@ -567,14 +686,17 @@ void ViewerWidget::update_audio_processor() if (get_connected_node()) { close_audio_processor(); - AudioParams ap = get_connected_node()->get_audio_params(); + AudioParams ap = viewer_output_audio_params(get_connected_node()); if (ap.sample_rate() <= 0 || ap.channel_count() <= 0) { ap = AudioParams( OAK_CONFIG("DefaultSequenceAudioFrequency").toInt(), OAK_CONFIG("DefaultSequenceAudioLayout").toULongLong(), - ViewerOutput::k_default_sample_format); + static_cast( + oakengine_viewer_default_sample_format())); } - ap.set_format(ViewerOutput::k_default_sample_format); + ap.set_format( + static_cast( + oakengine_viewer_default_sample_format())); AudioParams packed( OAK_CONFIG("AudioOutputSampleRate").toInt(), @@ -591,8 +713,13 @@ void ViewerWidget::update_audio_processor() << "layout_mask=0x" << packed.channel_layout() << Qt::dec; - audio_processor_.open( - ap, packed, (playback_speed_ == 0) ? 1 : std::abs(playback_speed_)); + OakAudioParams *from = ap_to_oak(ap); + OakAudioParams *to = ap_to_oak(packed); + oakengine_audio_processor_open( + audio_processor_, from, to, + (playback_speed_ == 0) ? 1 : std::abs(playback_speed_)); + oakcore_audioparams_free(from); + oakcore_audioparams_free(to); } } @@ -626,19 +753,40 @@ void ViewerWidget::create_addable_at(const QRectF &f) } } - MultiUndoCommand *command = new MultiUndoCommand(); + void *command = oakengine_undo_command_create_multi(); Node *clip = AddTool::create_addable_clip( command, s, Track::Reference(type, track_index), in, length); if (ShapeNodeBase *shape = dynamic_cast(clip)) { - shape->set_rect(f, s->get_video_params(), command); + const VideoParams vp = viewer_output_video_params(s); + oak_video_params pod = {}; + pod.width = vp.width(); + pod.height = vp.height(); + pod.time_base_num = vp.time_base().numerator(); + pod.time_base_den = vp.time_base().denominator(); + pod.format = vp.format(); + pod.pixel_aspect_num = vp.pixel_aspect_ratio().numerator(); + pod.pixel_aspect_den = vp.pixel_aspect_ratio().denominator(); + pod.interlacing = vp.interlacing(); + pod.divider = vp.divider(); + oakengine_shape_set_rect_undoable( + reinterpret_cast(shape), f.x(), f.y(), + f.width(), f.height(), &pod, command); } - Core::instance()->undo_stack()->push(command, tr("Created Shape")); + oakengine_undo_push(command, tr("Created Shape").toUtf8().constData()); set_gizmos(clip); } } +void ViewerWidget::handle_first_requeue_destroy() +{ + // Extra protection to ensure we don't reference a destroyed object + if (first_requeue_watcher_ && !queue_watchers_.contains(first_requeue_watcher_)) { + first_requeue_watcher_ = nullptr; + } +} + void ViewerWidget::show_subtitle_properties() { QFont f(OAK_CONFIG("DefaultSubtitleFamily").toString(), @@ -655,6 +803,38 @@ void ViewerWidget::show_subtitle_properties() } } +void ViewerWidget::dry_run_finished() +{ + if (!dry_run_watchers_.isEmpty()) { + OakEnginePreviewRequest *req = dry_run_watchers_.takeFirst(); + oakengine_preview_request_free(req); + request_next_dry_run(); + } +} + +void ViewerWidget::request_next_dry_run() +{ + if (is_playing()) { + Rational next_time = + Timecode::timestamp_to_time(dry_run_next_frame_, timebase()); + if (frame_exists_at_time(next_time)) { + if (next_time > get_connected_node()->get_playhead() + + Rational(10)) { + QTimer::singleShot(timebase().to_double() / playback_speed_, + this, &ViewerWidget::request_next_dry_run); + } else { + OakEnginePreviewRequest *r = get_single_frame(next_time, true); + if (r) { + oakengine_preview_request_set_finished_callback( + r, dry_run_finished_cb, this); + dry_run_next_frame_ += playback_speed_; + dry_run_watchers_.append(r); + } + } + } + } +} + void ViewerWidget::save_frame_as_image() { Core::instance()->open_export_dialog_for_viewer(get_connected_node(), true); @@ -669,7 +849,7 @@ void ViewerWidget::detect_multicam_node_now() void ViewerWidget::close_audio_processor() { - audio_processor_.close(); + oakengine_audio_processor_close(audio_processor_); } void ViewerWidget::set_waveform_mode(WaveformMode wf) @@ -709,7 +889,9 @@ void ViewerWidget::detect_multicam_node(const Rational &time) for (Block *b : qAsConst(timeline_selected_blocks_)) { if (b->range().contains(time)) { if ((clip = dynamic_cast(b))) { - if ((multicam = clip->find_multicam())) { + if ((multicam = reinterpret_cast( + oakengine_clip_find_multicam( + reinterpret_cast(clip))))) { break; } } @@ -726,7 +908,9 @@ void ViewerWidget::detect_multicam_node(const Rational &time) Block *b = t->nearest_block_before_or_at(time); if ((clip = dynamic_cast(b))) { - if ((multicam = clip->find_multicam())) { + if ((multicam = reinterpret_cast( + oakengine_clip_find_multicam( + reinterpret_cast(clip))))) { break; } } @@ -741,9 +925,10 @@ void ViewerWidget::detect_multicam_node(const Rational &time) time); } // FIXME: Really dirty - RenderManager::instance()->get_cacher()->set_multicam_node(multicam); + oakengine_render_cache_set_multicam_node( + reinterpret_cast(multicam)); } else { - RenderManager::instance()->get_cacher()->set_multicam_node(nullptr); + oakengine_render_cache_set_multicam_node(nullptr); if (multicam_panel_) { multicam_panel_->set_multicam_node(nullptr, nullptr, nullptr, time); } @@ -752,8 +937,8 @@ void ViewerWidget::detect_multicam_node(const Rational &time) bool ViewerWidget::is_video_visible() const { - return get_connected_node()->get_video_params().video_type() != - VideoParams::k_video_type_still && + return viewer_output_video_params(get_connected_node()).video_type() != + 1 && (display_widget_->isVisible() || !windows_.isEmpty()); } @@ -775,7 +960,9 @@ void ViewerWidget::update_waveform_view_from_mode() QSizePolicy::Expanding); if (get_connected_node()) { - get_connected_node()->set_waveform_enabled(waveform_view_->isVisible()); + oakengine_viewer_set_waveform_enabled( + reinterpret_cast(get_connected_node()), + waveform_view_->isVisible() ? 1 : 0); if (waveform_view_->isVisible()) { waveform_view_->set_viewer(get_connected_node()); @@ -785,47 +972,197 @@ void ViewerWidget::update_waveform_view_from_mode() } } -void ViewerWidget::received_audio_buffer_for_scrubbing() +void ViewerWidget::queue_next_audio_buffer() { - RenderTicketWatcher *watcher = static_cast(sender()); + Rational queue_end = + audio_playback_queue_time_ + (k_audio_playback_interval * playback_speed_); - while (!audio_scrub_watchers_.empty() && - audio_scrub_watchers_.front() != watcher) { - audio_scrub_watchers_.pop_front(); + // Clamp queue end by zero and the audio length + queue_end = std::clamp(queue_end, Rational(0), + get_connected_node()->get_audio_length()); + if ((playback_speed_ > 0 && queue_end <= audio_playback_queue_time_) || + (playback_speed_ < 0 && queue_end >= audio_playback_queue_time_)) { + // This will queue nothing, so stop the loop here + if (prequeuing_audio_) { + decrement_prequeued_audio(); + } + return; } - if (!audio_scrub_watchers_.empty()) { - if (watcher->has_result()) { - SampleBuffer samples = watcher->get().value(); - if (samples.is_allocated()) { - if (samples.audio_params().channel_count() > 0) { - AudioProcessor::Buffer buf; - int r = - audio_processor_.convert(samples.to_raw_ptrs().data(), - samples.sample_count(), &buf); + OakEngineNode *viewer = reinterpret_cast(get_connected_node()); + OakEnginePreviewRequest *req = oakengine_preview_request_audio_range( + viewer, + audio_playback_queue_time_.numerator(), + audio_playback_queue_time_.denominator(), + queue_end.numerator(), queue_end.denominator()); + if (req) { + oakengine_preview_request_set_finished_callback( + req, audio_playback_finished_cb, this); + audio_playback_queue_.push_back(req); + } - if (r >= 0) { - if (!buf.empty()) { - QString error; - const QByteArray &packed = buf.at(0); - AudioManager::instance()->clear_buffered_output(); - if (!AudioManager::instance()->push_to_output( - audio_processor_.to(), packed, &error)) { - Core::instance()->show_status_bar_message( - tr("Audio scrubbing failed: %1").arg(error)); + audio_playback_queue_time_ = queue_end; +} + +void ViewerWidget::received_audio_buffer_for_playback() +{ + while (!audio_playback_queue_.empty() && + oakengine_preview_request_is_done(audio_playback_queue_.front())) { + OakEnginePreviewRequest *req = audio_playback_queue_.front(); + audio_playback_queue_.pop_front(); + + if (oakengine_preview_request_has_result(req)) { + SampleBuffer samples = preview_req_to_sample_buffer(req); + if (samples.is_allocated()) { + // If the samples must be reversed, reverse them now + if (playback_speed_ < 0) { + samples.reverse(); + } + + // Convert to packed data for audio output + const void *pack_data = nullptr; + int pack_size = 0; + int r = oakengine_audio_processor_convert( + audio_processor_, samples.to_raw_ptrs().data(), + samples.sample_count(), &pack_data, &pack_size); + + // TempoProcessor may have emptied the array + if (r >= 0) { + if (pack_size > 0) { + if (prequeuing_audio_) { + // Add to prequeued audio buffer + prequeued_audio_.append( + static_cast(pack_data), pack_size); + } else { + // Push directly to audio manager + { + OakAudioParams *oap = + oakengine_audio_processor_output_params( + audio_processor_); + oakengine_audio_push_to_output( + oap, static_cast(pack_data), + pack_size, nullptr, 0); + oakcore_audioparams_free(oap); } - AudioMonitor::push_sample_buffer_on_all(samples); } - } else { - qCritical() - << "Failed to process audio for scrubbing:" << r; } + } else { + qCritical() << "Failed to process audio for playback:" << r; + } + } + } + + if (prequeuing_audio_) { + decrement_prequeued_audio(); + } + + oakengine_preview_request_free(req); + } +} + +void ViewerWidget::received_audio_buffer_for_scrubbing() +{ + if (audio_scrub_watchers_.empty()) { + return; + } + + OakEnginePreviewRequest *req = audio_scrub_watchers_.front(); + audio_scrub_watchers_.pop_front(); + + if (oakengine_preview_request_has_result(req)) { + SampleBuffer samples = preview_req_to_sample_buffer(req); + if (samples.is_allocated()) { + if (samples.audio_params().channel_count() > 0) { + const void *pack_data = nullptr; + int pack_size = 0; + int r = oakengine_audio_processor_convert( + audio_processor_, samples.to_raw_ptrs().data(), + samples.sample_count(), &pack_data, &pack_size); + + if (r >= 0) { + if (pack_size > 0) { + QString error; + oakengine_audio_clear_buffered_output(); + char errbuf[256]; + OakAudioParams *oap = + oakengine_audio_processor_output_params( + audio_processor_); + if (!oakengine_audio_push_to_output( + oap, static_cast(pack_data), + pack_size, errbuf, sizeof(errbuf))) { + oakcore_audioparams_free(oap); + Core::instance()->show_status_bar_message( + tr("Audio scrubbing failed: %1").arg(QString::fromUtf8(errbuf))); + } else { + oakcore_audioparams_free(oap); + } + AudioMonitor::push_sample_buffer_on_all(samples); + } + } else { + qCritical() + << "Failed to process audio for scrubbing:" << r; } } } } - delete watcher; + oakengine_preview_request_free(req); +} + +void ViewerWidget::queue_starved() +{ + static const int k_maximum_wait_time_ms = 250; + static const Rational k_maximum_wait_time(k_maximum_wait_time_ms, 1000); + qint64 now = QDateTime::currentMSecsSinceEpoch(); + + if (!queue_starved_start_) { + queue_starved_start_ = now; + } else if (now > queue_starved_start_ + k_maximum_wait_time_ms) { + if (first_requeue_watcher_ && !queue_watchers_.isEmpty()) { + // Stale check: request still pending below timeout + return; + } + + force_requeue_from_current_time(); + queue_starved_start_ = 0; + } +} + +void ViewerWidget::queue_no_longer_starved() +{ + queue_starved_start_ = 0; +} + +void ViewerWidget::force_requeue_from_current_time() +{ + // Defer the requeue to the next event-loop iteration. This function is often + // called from paintEvent paths (QueueStarved) where synchronously cancelling + // watchers can re-enter the same request and deadlock. + QMetaObject::invokeMethod( + this, [this]() { force_requeue_from_current_time_internal(); }, + Qt::QueuedConnection); +} + +void ViewerWidget::force_requeue_from_current_time_internal() +{ + // Allow half a second for requeue to complete + static const Rational k_requeue_wait_time(1); + + oakengine_preview_cacher_clear_single_frame_renders(0); + queue_watchers_.clear(); + int queue = determine_playback_queue_size(); + playback_queue_next_frame_ = + get_timestamp() + + playback_speed_ * Timecode::time_to_timestamp( + k_requeue_wait_time, timebase(), Timecode::k_floor); + ; + first_requeue_watcher_ = nullptr; + for (int i = 0; i < queue; i++) { + OakEnginePreviewRequest *req = request_next_frame_for_queue(); + if (!first_requeue_watcher_) { + first_requeue_watcher_ = req; + } + } } void ViewerWidget::update_texture_from_node() @@ -846,21 +1183,17 @@ void ViewerWidget::update_texture_from_node() if (frame_exists || frame_might_be_still) { // Frame was not in queue, will require rendering or decoding from cache // Not playing, run a task to get the frame either from the cache or the renderer - RenderTicketWatcher *watcher = new RenderTicketWatcher(); - watcher->setProperty("start", QDateTime::currentMSecsSinceEpoch()); - watcher->setProperty("time", QVariant::fromValue(time)); - connect(watcher, &RenderTicketWatcher::finished, this, - &ViewerWidget::renderer_generated_frame); - nonqueue_watchers_.append(watcher); + OakEnginePreviewRequest *req = get_frame(time); + if (req) { + oakengine_preview_request_set_finished_callback( + req, nonqueue_finished_cb, this); + nonqueue_watchers_.append(req); + } // Clear queue because we want this frame more than any others - RenderManager::instance() - ->get_cacher() - ->clear_single_frame_renders_that_arent_running(); + oakengine_preview_cacher_clear_single_frame_renders(1); detect_multicam_node(time); - - watcher->set_ticket(get_frame(time)); } else { // There is definitely no frame here, we can immediately flip to showing nothing nonqueue_watchers_.clear(); @@ -868,7 +1201,6 @@ void ViewerWidget::update_texture_from_node() return; } } - void ViewerWidget::play_internal(int speed, bool in_to_out_only) { Q_ASSERT(speed != 0); @@ -883,22 +1215,15 @@ void ViewerWidget::play_internal(int speed, bool in_to_out_only) return; } - if (speed < 0) { - // The facade playback engine covers forward playback only this - // round; backward/shuttle playback is deferred (roadmap 附 A). - qWarning() << "ViewerWidget: backward playback is not supported yet"; - return; - } - // Kindly tell all viewers to stop playing and caching so all resources can be used for playback foreach (ViewerWidget *viewer, instances) { if (viewer != this) { viewer->pause_internal(); } } - RenderManager::instance()->get_cacher()->set_thumbnails_paused(true); + oakengine_preview_cacher_set_thumbnails_paused(1); - RenderManager::instance()->set_aggressive_garbage_collection(true); + oakengine_render_manager_set_aggressive_garbage_collection(1); // Disarm recording if armed if (record_armed_) { @@ -910,70 +1235,91 @@ void ViewerWidget::play_internal(int speed, bool in_to_out_only) Rational last_frame = get_connected_node()->get_length() - timebase(); if (!in_to_out_only && get_connected_node()->get_playhead() >= last_frame) { - get_connected_node()->set_playhead(0); + if (speed > 0) { + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + 0, 1); + } else { + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + last_frame.numerator(), last_frame.denominator()); + } } } playback_speed_ = speed; play_in_to_out_only_ = in_to_out_only; + playback_queue_next_frame_ = get_timestamp() + playback_speed_; + controls_->show_pause_button(); - // Start the facade playback session: its pull thread renders frames - // and 1/4s audio blocks ahead, pushes audio to the AudioManager - // itself and feeds our frame/audio callbacks. - if (!playback_) { - const VideoParams vp = get_connected_node()->get_video_params(); - playback_ = oakengine_playback_create( - reinterpret_cast(get_connected_node()), - vp.effective_width(), vp.effective_height(), - timebase().denominator(), timebase().numerator()); - if (!playback_) { - qWarning() << "ViewerWidget: failed to create playback session"; - playback_speed_ = 0; - controls_->show_play_button(); - return; + queue_starved_start_ = 0; + + // Attempt to fill playback queue + if (is_video_visible()) { + prequeue_length_ = determine_playback_queue_size(); + + if (prequeue_length_ > 0) { + prequeuing_video_ = true; + prequeue_count_ = 0; + + for (int i = 0; i < prequeue_length_; i++) { + request_next_frame_for_queue(); + } + + dry_run_next_frame_ = playback_queue_next_frame_; + request_next_dry_run(); } - oakengine_playback_set_frame_callback( - playback_, &ViewerWidget::facade_frame_callback, this); - oakengine_playback_set_audio_callback( - playback_, &ViewerWidget::facade_audio_callback, this); } - const int64_t start_ts = get_timestamp(); - if (oakengine_playback_start(playback_, start_ts, playback_speed_) != - OAKENGINE_OK) { - char err[512]; - err[0] = '\0'; - oakengine_playback_last_error(playback_, err, sizeof(err)); - qWarning() << "ViewerWidget: failed to start playback:" - << (err[0] ? err : "(no error)"); - playback_speed_ = 0; - controls_->show_play_button(); - return; + AudioParams ap = viewer_output_audio_params(get_connected_node()); + qDebug() << "ViewerWidget::PlayInternal: audio params valid=" << ap.is_valid() + << "channel_count=" << ap.channel_count(); + if (ap.is_valid() && ap.channel_count() != 0) { + update_audio_processor(); + + // Verify audio processor output params are valid before using them + OakAudioParams *output_params = + oakengine_audio_processor_output_params(audio_processor_); + const bool output_valid = + output_params && oakcore_audioparams_is_valid(output_params); + qDebug() << "ViewerWidget::PlayInternal: audio processor output params valid=" + << output_valid; + if (!output_valid) { + qWarning() + << "Audio processor output params are invalid, skipping audio playback"; + } else { + oakengine_audio_set_output_notify_interval( + oakcore_audioparams_time_to_bytes( + output_params, k_audio_playback_interval.to_double())); + audio_notify_sub_ = oakengine_event_subscribe( + oakengine_audio_manager_handle(), + OAKENGINE_EVENT_AUDIO_MANAGER_OUTPUT_NOTIFY, + [](const oakengine_event *, void *userdata) { + static_cast(userdata)->queue_next_audio_buffer(); + }, + this); + + static const int prequeue_count = 2; + prequeuing_audio_ = + prequeue_count; // Queue two buffers ahead of time + audio_playback_queue_time_ = get_connected_node()->get_playhead(); + qDebug() << "ViewerWidget::PlayInternal: prequeuing audio start time=" + << audio_playback_queue_time_.to_double(); + for (int i = 0; i < prequeue_count; i++) { + queue_next_audio_buffer(); + } + } + oakcore_audioparams_free(output_params); } - // Waveform monitor stays UI-side (it needs the connected waveform - // metadata); the facade pushes audio to the output by itself. - if (get_connected_node()->get_audio_params().channel_count() > 0) { - AudioMonitor::start_waveform_on_all( - get_connected_node()->get_connected_waveform(), - get_connected_node()->get_playhead(), playback_speed_); + // If there's nothing to prequeue, start playback immediately so the + // playhead advances even when only the audio waveform is visible. + if (!prequeuing_video_ && !prequeuing_audio_) { + finish_play_preprocess(); } - display_widget_->reset_fps_timer(); - - foreach (ViewerDisplayWidget *dw, playback_devices_) { - dw->play(start_ts, playback_speed_, timebase(), is_video_visible()); - } - - // The UI poll timer drives the playhead, boundary and loop policy - // from the facade's playback position. - playback_poll_timer_.setInterval( - qMax(1, qFloor(timebase().to_double() * 1000.0))); - playback_poll_timer_.start(); - playback_poll_update(); - // Force screen to stay awake prevent_sleep(true); } @@ -981,7 +1327,7 @@ void ViewerWidget::play_internal(int speed, bool in_to_out_only) void ViewerWidget::pause_internal() { if (recording_) { - AudioManager::instance()->stop_recording(); + oakengine_audio_stop_recording(); recording_ = false; controls_->set_pause_button_recording_state(false); @@ -998,90 +1344,44 @@ void ViewerWidget::pause_internal() dw->pause(); } - // The facade pause also stops the audio output (engine side). - if (playback_) { - oakengine_playback_pause(playback_); + // Cancel in-flight render tickets before deleting watchers, + // otherwise the render thread keeps working on stale frames + // and blocks the single-frame render requested by UpdateTextureFromNode(). + foreach (OakEnginePreviewRequest *req, queue_watchers_) { + oakengine_preview_request_free(req); } - playback_poll_timer_.stop(); + queue_watchers_.clear(); + oakengine_preview_cacher_clear_single_frame_renders(0); + playback_backup_timer_.stop(); + + // Handle audio + oakengine_audio_stop_output(); AudioMonitor::stop_on_all(); + prequeued_audio_.clear(); + if (audio_notify_sub_ > 0) { + oakengine_event_unsubscribe(audio_notify_sub_); + audio_notify_sub_ = 0; + } + for (auto *req : audio_playback_queue_) { oakengine_preview_request_free(req); } + audio_playback_queue_.clear(); + update_audio_processor(); - RenderManager::instance()->get_cacher()->clear_single_frame_renders(); - RenderManager::instance()->get_cacher()->set_thumbnails_paused(false); + oakengine_preview_cacher_set_thumbnails_paused(0); update_texture_from_node(); - RenderManager::instance()->set_aggressive_garbage_collection(false); + oakengine_render_manager_set_aggressive_garbage_collection(false); } + prequeuing_video_ = false; + prequeuing_audio_ = 0; + dry_run_watchers_.clear(); + // Reset screen timeout timer prevent_sleep(false); } -void ViewerWidget::facade_frame_callback(const oak_playback_frame *frame, - void *userdata) -{ - ViewerWidget *viewer = static_cast(userdata); - - // Wrap the CPU pixels now (the payload dies when we return), then - // append to the display queue on the main thread -- the same - // destination the old renderer_generated_frame_for_queue used. - FramePtr copy = Frame::create(); - copy->set_video_params( - VideoParams(frame->width, frame->height, viewer->timebase(), - static_cast(frame->format), - VideoParams::k_internal_channel_count)); - copy->allocate(); - const int row_bytes = qMin(frame->linesize, copy->linesize_bytes()); - for (int y = 0; y < frame->height; y++) { - memcpy(copy->data() + y * copy->linesize_bytes(), - static_cast(frame->data) + y * frame->linesize, - size_t(row_bytes)); - } - - const Rational ts = - Timecode::timestamp_to_time(frame->timestamp, viewer->timebase()); - QMetaObject::invokeMethod(viewer, [viewer, copy, ts]() { - viewer->deliver_facade_frame(copy, ts); - }, Qt::QueuedConnection); -} - -void ViewerWidget::facade_audio_callback(const oak_playback_audio *audio, - void *userdata) -{ - ViewerWidget *viewer = static_cast(userdata); - - // Copy the block for the level monitor (the payload dies when we - // return; the facade already pushed it to the AudioManager). - const AudioParams params( - audio->sample_rate, - viewer->get_connected_node() ? - viewer->get_connected_node()->get_audio_params().channel_layout() : - core::k_channel_layout_stereo, - core::SampleFormat::f32_p); - SampleBuffer buffer(params, size_t(audio->sample_count)); - for (int ch = 0; ch < audio->channels; ch++) { - memcpy(buffer.to_raw_ptrs()[ch], audio->channel_data[ch], - size_t(audio->sample_count) * sizeof(float)); - } - - QMetaObject::invokeMethod(viewer, [buffer]() { - AudioMonitor::push_sample_buffer_on_all(buffer); - }, Qt::QueuedConnection); -} - -void ViewerWidget::deliver_facade_frame(const FramePtr &frame, - const Rational &ts) -{ - if (!is_playing()) { - return; - } - foreach (ViewerDisplayWidget *dw, playback_devices_) { - dw->queue()->append_timewise({ ts, QVariant::fromValue(frame) }, - playback_speed_); - } -} - void ViewerWidget::push_scrubbed_audio() { if (!is_playing() && get_connected_node() && @@ -1092,27 +1392,30 @@ void ViewerWidget::push_scrubbed_audio() if (ignore_scrub_ == 0) { // Get audio src device from renderer - const AudioParams ¶ms = get_connected_node()->get_audio_params(); + const AudioParams ¶ms = viewer_output_audio_params(get_connected_node()); if (params.is_valid()) { // NOTE: Hardcoded scrubbing interval (20ms) Rational interval = Rational(20, 1000); - RenderTicketWatcher *watcher = new RenderTicketWatcher(); - connect(watcher, &RenderTicketWatcher::finished, this, - &ViewerWidget::received_audio_buffer_for_scrubbing); - audio_scrub_watchers_.push_back(watcher); - watcher->set_ticket( - RenderManager::instance()->get_cacher()->get_range_of_audio( - get_connected_node(), - TimeRange(get_connected_node()->get_playhead(), - get_connected_node()->get_playhead() + - interval))); + OakEngineNode *viewer = + reinterpret_cast(get_connected_node()); + OakEnginePreviewRequest *req = + oakengine_preview_request_audio_range( + viewer, + get_connected_node()->get_playhead().numerator(), + get_connected_node()->get_playhead().denominator(), + (get_connected_node()->get_playhead() + interval).numerator(), + (get_connected_node()->get_playhead() + interval).denominator()); + if (req) { + oakengine_preview_request_set_finished_callback( + req, audio_scrub_finished_cb, this); + audio_scrub_watchers_.push_back(req); + } } } } } - void ViewerWidget::update_minimum_scale() { if (!get_connected_node()) { @@ -1160,24 +1463,91 @@ bool ViewerWidget::viewer_might_be_a_still() get_connected_node()->get_video_length().isNull(); } -void ViewerWidget::set_display_image(RenderTicketPtr ticket) +void ViewerWidget::set_display_image(OakEnginePreviewRequest *req) { foreach (ViewerDisplayWidget *dw, playback_devices_) { QVariant push; - if (ticket) { + if (req && oakengine_preview_request_has_result(req)) { + oak_playback_frame frame; if (dynamic_cast(dw)) { - push = ticket->property("multicam_output"); + // Multicam: use the default frame for now + if (oakengine_preview_request_get_frame(req, &frame) == 0) { + FramePtr f; + oakengine_codec_frame_create(&f); + { + oak_video_params pod = {}; + pod.width = frame.width; + pod.height = frame.height; + pod.format = static_cast(frame.format); + const VideoParams vp = video_params_from_pod(pod); + oakengine_codec_frame_set_video_params(f.get(), &vp); + } + oakengine_codec_frame_allocate(f.get()); + if (frame.data && frame.linesize > 0 && f->linesize_bytes() > 0) { + memcpy(f->data(), frame.data, + qMin(f->linesize_bytes(), frame.linesize) * frame.height); + } + push = QVariant::fromValue(f); + } } else { - push = ticket->get(); + if (oakengine_preview_request_get_frame(req, &frame) == 0) { + FramePtr f; + oakengine_codec_frame_create(&f); + { + oak_video_params pod = {}; + pod.width = frame.width; + pod.height = frame.height; + pod.format = static_cast(frame.format); + const VideoParams vp = video_params_from_pod(pod); + oakengine_codec_frame_set_video_params(f.get(), &vp); + } + oakengine_codec_frame_allocate(f.get()); + if (frame.data && frame.linesize > 0 && f->linesize_bytes() > 0) { + memcpy(f->data(), frame.data, + qMin(f->linesize_bytes(), frame.linesize) * frame.height); + } + push = QVariant::fromValue(f); + } } } dw->set_image(push); } } -RenderTicketPtr ViewerWidget::get_frame(const Rational &t) +void ViewerWidget::set_display_image(RenderTicketPtr ticket) { - if (is_playing()) { + // Legacy compat: convert to OakEnginePreviewRequest* not available, + // but this path is no longer called internally. + // Call the new overload with nullptr to clear the display. + set_display_image(static_cast(nullptr)); +} + +OakEnginePreviewRequest *ViewerWidget::request_next_frame_for_queue(bool increment) +{ + OakEnginePreviewRequest *req = nullptr; + + Rational next_time = + Timecode::timestamp_to_time(playback_queue_next_frame_, timebase()); + + if (frame_exists_at_time(next_time) || viewer_might_be_a_still()) { + if (increment) { + playback_queue_next_frame_ += playback_speed_; + } + + req = get_frame(next_time); + if (req) { + oakengine_preview_request_set_finished_callback( + req, queue_finished_cb, this); + queue_watchers_.append(req); + } + } + + return req; +} + +OakEnginePreviewRequest *ViewerWidget::get_frame(const Rational &t) +{ + if (is_playing() || prequeuing_video_) { return get_single_frame(t); } @@ -1188,21 +1558,91 @@ RenderTicketPtr ViewerWidget::get_frame(const Rational &t) // Frame hasn't been cached, start render job return get_single_frame(t); } else { - // Frame has been cached, grab the frame - RenderTicketPtr ticket = std::make_shared(); - ticket->setProperty("time", QVariant::fromValue(t)); - QtConcurrent::run( - static_cast( - ViewerWidget::decode_cached_image), - ticket, - get_connected_node()->video_frame_cache()->get_cache_directory(), - get_connected_node()->video_frame_cache()->get_uuid(), - Timecode::time_to_timestamp(t, timebase(), Timecode::k_floor)); - return ticket; + // Frame has been cached, grab the frame via preview request + OakEngineNode *viewer = + reinterpret_cast(get_connected_node()); + return oakengine_preview_request_single_frame( + viewer, t.numerator(), t.denominator(), 0); } } +void ViewerWidget::finish_play_preprocess() +{ + // Check if we're still waiting for video or audio respectively + if (prequeuing_video_ || prequeuing_audio_) { + return; + } + + int64_t playback_start_time = get_timestamp(); + + // Restart the audio output clock for this playback run; the playback + // timer uses it as its master clock + oakengine_audio_reset_output_clock(); + + // Start audio waveform playback + if (!prequeued_audio_.isEmpty()) { + char errbuf[256]; + OakAudioParams *oap = + oakengine_audio_processor_output_params(audio_processor_); + if (!oakengine_audio_push_to_output(oap, + prequeued_audio_.constData(), + prequeued_audio_.size(), + errbuf, sizeof(errbuf))) { + oakcore_audioparams_free(oap); + QMessageBox::critical( + this, tr("Audio Error"), + tr("Failed to start audio: %1\n\n" + "Please check your audio preferences and try again.") + .arg(QString::fromUtf8(errbuf))); + } else { + oakcore_audioparams_free(oap); + } + prequeued_audio_.clear(); + + AudioMonitor::start_waveform_on_all( + get_connected_node()->get_connected_waveform(), + get_connected_node()->get_playhead(), playback_speed_); + } + + display_widget_->reset_fps_timer(); + + foreach (ViewerDisplayWidget *dw, playback_devices_) { + dw->play(playback_start_time, playback_speed_, timebase(), + is_video_visible()); + } + + // This is our timer for loading the queue and setting the time + playback_backup_timer_.setInterval( + qMax(1, qFloor(timebase_dbl() * 1000.0))); + playback_backup_timer_.start(); + + playback_timer_update(); +} + +int ViewerWidget::determine_playback_queue_size() +{ + if (playback_speed_ == 0) { + return 0; + } + + int64_t end_ts; + + if (playback_speed_ > 0) { + end_ts = Timecode::time_to_timestamp( + get_connected_node()->get_video_length(), timebase()); + } else { + end_ts = 0; + } + + int remaining_frames = (end_ts - get_timestamp() - 1) / playback_speed_; + + // Generate maximum queue + int max_frames = + qCeil(k_video_playback_interval.to_double() / timebase().to_double()); + + return qMin(max_frames, remaining_frames); +} + void ViewerWidget::context_menu_set_full_screen(QAction *action) { set_full_screen(QGuiApplication::screens().at(action->data().toInt())); @@ -1212,14 +1652,9 @@ void ViewerWidget::context_menu_set_playback_res(QAction *action) { int div = action->data().toInt(); - auto vp = get_connected_node()->get_video_params(); - vp.set_divider(div); - - auto c = new NodeParamSetStandardValueCommand( - NodeKeyframeTrackReference( - NodeInput(get_connected_node(), ViewerOutput::k_video_params_input, 0)), - QVariant::fromValue(vp)); - Core::instance()->undo_stack()->push(c, tr("Changed Playback Resolution")); + void *c = oakengine_viewer_set_preview_divider_command( + reinterpret_cast(get_connected_node()), div); + oakengine_undo_push(c, tr("Changed Playback Resolution").toUtf8().constData()); } void ViewerWidget::context_menu_disable_safe_margins() @@ -1253,22 +1688,74 @@ void ViewerWidget::window_about_to_close() void ViewerWidget::renderer_generated_frame() { - RenderTicketWatcher *ticket = static_cast(sender()); + if (nonqueue_watchers_.isEmpty()) { + return; + } - if (nonqueue_watchers_.contains(ticket)) { - while (!nonqueue_watchers_.isEmpty()) { - // Pop frames that are "old" - if (nonqueue_watchers_.takeFirst() == ticket) { - break; + OakEnginePreviewRequest *req = nonqueue_watchers_.takeFirst(); + + if (oakengine_preview_request_has_result(req)) { + set_display_image(req); + } + + oakengine_preview_request_free(req); +} + +void ViewerWidget::renderer_generated_frame_for_queue() +{ + if (queue_watchers_.isEmpty()) { + return; + } + + OakEnginePreviewRequest *req = queue_watchers_.takeFirst(); + + if (oakengine_preview_request_has_result(req)) { + oak_playback_frame pf; + bool has_frame = (oakengine_preview_request_get_frame(req, &pf) == 0); + bool drop_frame = false; + + // Ignore this signal if we've paused now + if (is_playing() || prequeuing_video_) { + if (!drop_frame && has_frame) { + FramePtr f; + oakengine_codec_frame_create(&f); + { + oak_video_params pod = {}; + pod.width = pf.width; + pod.height = pf.height; + pod.format = static_cast(pf.format); + const VideoParams vp = video_params_from_pod(pod); + oakengine_codec_frame_set_video_params(f.get(), &vp); + } + oakengine_codec_frame_allocate(f.get()); + if (pf.data && pf.linesize > 0 && f->linesize_bytes() > 0) { + memcpy(f->data(), pf.data, + qMin(f->linesize_bytes(), pf.linesize) * pf.height); + } + QVariant frame = QVariant::fromValue(f); + + foreach (ViewerDisplayWidget *dw, playback_devices_) { + dw->queue()->append_timewise({ Rational(), frame }, + playback_speed_); + } } - } - if (ticket->has_result()) { - set_display_image(ticket->get_ticket()); + if (prequeuing_video_) { + prequeue_count_++; + + if (prequeue_count_ == prequeue_length_) { + prequeuing_video_ = false; + finish_play_preprocess(); + } + } } } - delete ticket; + if (first_requeue_watcher_ == req) { + first_requeue_watcher_ = nullptr; + } + + oakengine_preview_request_free(req); } void ViewerWidget::show_context_menu(const QPoint &pos) @@ -1355,20 +1842,26 @@ void ViewerWidget::show_context_menu(const QPoint &pos) new Menu(tr("Playback Resolution"), &menu); menu.addMenu(playback_res_menu); - for (int d : VideoParams::k_supported_dividers) { + { + const int n = oakengine_video_params_supported_divider_count(); + for (int i = 0; i < n; i++) { + int d = oakengine_video_params_supported_divider_at(i); + char name_buf[64]; + oakengine_video_params_divider_name(d, name_buf, sizeof(name_buf)); playback_res_menu->add_action_with_data( - VideoParams::get_name_for_divider(d), d, - get_connected_node()->get_video_params().divider()); + QString::fromUtf8(name_buf), d, + viewer_output_video_params(get_connected_node()).divider()); } connect(playback_res_menu, &QMenu::triggered, this, &ViewerWidget::context_menu_set_playback_res); + } } { // Deinterlace Option - if (get_connected_node()->get_video_params().interlacing() != - VideoParams::k_interlace_none) { + if (viewer_output_video_params(get_connected_node()).interlacing() != + 0) { QAction *deinterlace_action = menu.addAction(tr("Deinterlace")); deinterlace_action->setCheckable(true); deinterlace_action->setChecked( @@ -1509,22 +2002,29 @@ void ViewerWidget::play(bool in_to_out_only) if (get_connected_node() && get_connected_node()->get_work_area()->enabled()) { // Jump to in point - get_connected_node()->set_playhead( - get_connected_node()->get_work_area()->in()); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + get_connected_node()->get_work_area()->in().numerator(), + get_connected_node()->get_work_area()->in().denominator()); } else { in_to_out_only = false; } } else if (record_armed_) { disarm_recording(); - if (get_connected_node()->project()->filename().isEmpty()) { + char fn_buf[512]; + oakengine_project_filename( + reinterpret_cast( + get_connected_node()->project()), + fn_buf, sizeof(fn_buf)); + if (fn_buf[0] == '\0') { QMessageBox::critical( this, tr("Audio Recording"), tr("Project must be saved before you can record audio.")); return; } - QDir audio_path(QFileInfo(get_connected_node()->project()->filename()) + QDir audio_path(QFileInfo(fn_buf) .dir() .filePath(tr("audio"))); if (!audio_path.exists()) { @@ -1533,26 +2033,35 @@ void ViewerWidget::play(bool in_to_out_only) recording_filename_ = audio_path.filePath(QStringLiteral("%1.%2").arg( QDateTime::currentDateTime().toString("yyyy-MM-dd hh-mm-ss"), - ExportFormat::get_extension(static_cast( - OAK_CONFIG("AudioRecordingFormat").toInt())))); + []() -> QString { + char ext_buf[64]; + int fmt = OAK_CONFIG("AudioRecordingFormat").toInt(); + oakengine_encoding_format_extension(fmt, ext_buf, sizeof(ext_buf)); + return QString::fromUtf8(ext_buf); + }())); - AudioParams ap( + OakEngineEncodingParams *encode_param = + oakengine_encoding_params_create(); + oakengine_encoding_params_enable_audio( + encode_param, OAK_CONFIG("AudioRecordingSampleRate").toInt(), OAK_CONFIG("AudioRecordingChannelLayout").toULongLong(), SampleFormat::from_string(OAK_CONFIG("AudioRecordingSampleFormat") .toString() - .toStdString())); - - EncodingParams encode_param; - encode_param.enable_audio( - ap, static_cast( - OAK_CONFIG("AudioRecordingCodec").toInt())); - encode_param.set_filename(recording_filename_); - encode_param.set_audio_bit_rate( + .toStdString()), + OAK_CONFIG("AudioRecordingCodec").toInt()); + oakengine_encoding_params_set_filename( + encode_param, recording_filename_.toUtf8().constData()); + oakengine_encoding_params_set_audio_bit_rate( + encode_param, OAK_CONFIG("AudioRecordingBitRate").toInt() * 1000); - QString error; - if (AudioManager::instance()->start_recording(encode_param, &error)) { + char errbuf[256]; + const int rec_ret = oakengine_encoding_start_audio_recording( + encode_param, errbuf, static_cast(sizeof(errbuf))); + oakengine_encoding_params_destroy(encode_param); + + if (rec_ret == OAKENGINE_OK) { recording_ = true; controls_->set_pause_button_recording_state(true); recording_callback_->enable_recording_overlay( @@ -1560,7 +2069,9 @@ void ViewerWidget::play(bool in_to_out_only) } else { QMessageBox::critical( this, tr("Audio Recording"), - tr("Failed to start audio recording: %1").arg(error)); + tr("Failed to start audio recording: %1").arg( + errbuf[0] ? QString::fromUtf8(errbuf) + : tr("Unknown error"))); return; } } @@ -1640,18 +2151,12 @@ void ViewerWidget::TimebaseChangedEvent(const Rational &timebase) length_changed_slot(get_connected_node() ? get_connected_node()->get_length() : 0); } -void ViewerWidget::playback_poll_update() +void ViewerWidget::playback_timer_update() { - if (!playback_ || !is_playing() || !get_connected_node()) { - return; - } + Q_ASSERT(playback_speed_ != 0); - // The facade playback engine owns the master clock; poll its - // position for the playhead, boundary and loop policy (the min/max - // part of the old playback_timer_update). - int64_t pos_ts = 0; - oakengine_playback_get_position(playback_, &pos_ts); - Rational current_time = Timecode::timestamp_to_time(pos_ts, timebase()); + Rational current_time = Timecode::timestamp_to_time( + display_widget_->timer()->get_timestamp_now(), timebase()); Rational min_time, max_time; @@ -1683,21 +2188,31 @@ void ViewerWidget::playback_poll_update() bool play_after_pause = false; if ((!recording_ || recording_range_.out() != recording_range_.in()) && - current_time >= max_time) { - // We've reached the end of whatever range we're playing and should either pause - // or restart playback (negative speeds are out of scope this round). + ((playback_speed_ < 0 && current_time <= min_time) || + (playback_speed_ > 0 && current_time >= max_time))) { + // Determine which timestamp we tripped + Rational tripped_time; + + if (current_time <= min_time) { + tripped_time = min_time; + } else { + tripped_time = max_time; + } + + // Signal that we've reached the end of whatever range we're playing and should either pause + // or restart playback end_of_line = true; if (OAK_CONFIG("Loop").toBool() && !recording_) { - // If we're looping, jump back to the start of the range and continue - time_to_set = min_time; + // If we're looping, jump to the other side of the workarea and continue + time_to_set = (tripped_time == min_time) ? max_time : min_time; // Signal to restart playback after the pause signalled by `end_of_line` play_after_pause = true; } else { // Pause at the boundary we tripped - time_to_set = max_time; + time_to_set = tripped_time; } } else { @@ -1709,15 +2224,10 @@ void ViewerWidget::playback_poll_update() // pausing. Even if we pause it later with `end_of_line`, we prefer pausing after setting the time // so that an audio scrub event, etc. isn't sent. time_changed_from_timer_ = true; - get_connected_node()->set_playhead(time_to_set); + oakengine_viewer_set_playhead( + reinterpret_cast(get_connected_node()), + time_to_set.numerator(), time_to_set.denominator()); time_changed_from_timer_ = false; - - // Feed the display clocks and purge consumed queue entries. - foreach (ViewerDisplayWidget *dw, playback_devices_) { - dw->set_playback_timestamp(pos_ts); - dw->queue()->purge_before(current_time, playback_speed_); - } - if (end_of_line) { // Cache the current speed int current_speed = playback_speed_; @@ -1727,6 +2237,20 @@ void ViewerWidget::playback_poll_update() play_internal(current_speed, play_in_to_out_only_); } } + + if (is_playing() && is_video_visible()) { + while ((int(display_widget_->queue()->size()) + + queue_watchers_.size()) < determine_playback_queue_size()) { + if (!request_next_frame_for_queue()) { + // Prevent infinite loop + break; + } + } + } + + foreach (ViewerDisplayWidget *dw, playback_devices_) { + dw->queue()->purge_before(current_time, playback_speed_); + } } void ViewerWidget::set_viewer_resolution(int width, int height) @@ -1762,10 +2286,10 @@ void ViewerWidget::length_changed_slot(const Rational &length) } } -void ViewerWidget::interlacing_changed_slot(VideoParams::Interlacing interlacing) +void ViewerWidget::interlacing_changed_slot(int interlacing) { // Automatically set a "sane" deinterlacing option - bool deint = interlacing != VideoParams::k_interlace_none; + bool deint = interlacing != 0; // k_interlace_none foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->set_deinterlacing(deint); @@ -1774,7 +2298,7 @@ void ViewerWidget::interlacing_changed_slot(VideoParams::Interlacing interlacing void ViewerWidget::update_renderer_video_parameters() { - VideoParams vp = get_connected_node()->get_video_params(); + VideoParams vp = viewer_output_video_params(get_connected_node()); foreach (ViewerDisplayWidget *dw, playback_devices_) { dw->set_video_params(vp); @@ -1783,7 +2307,7 @@ void ViewerWidget::update_renderer_video_parameters() void ViewerWidget::update_renderer_audio_parameters() { - AudioParams ap = get_connected_node()->get_audio_params(); + AudioParams ap = viewer_output_audio_params(get_connected_node()); update_audio_processor(); @@ -1817,14 +2341,14 @@ void ViewerWidget::update_waveform_mode_from_menu(QAction *a) void ViewerWidget::drag_entered(QDragEnterEvent *event) { - if (event->mimeData()->formats().contains(Project::k_item_mime_type)) { + if (event->mimeData()->formats().contains(QString::fromUtf8(oakengine_project_item_mime_type()))) { event->accept(); } } void ViewerWidget::dropped(QDropEvent *event) { - QByteArray mimedata = event->mimeData()->data(Project::k_item_mime_type); + QByteArray mimedata = event->mimeData()->data(QString::fromUtf8(oakengine_project_item_mime_type())); QDataStream stream(&mimedata, QIODevice::ReadOnly); // Variables to deserialize into diff --git a/app/widget/viewer/viewer.h b/app/widget/viewer/viewer.h index 01286044f..f7967e0ee 100644 --- a/app/widget/viewer/viewer.h +++ b/app/widget/viewer/viewer.h @@ -29,11 +29,11 @@ #include #include -#include "audio/audioprocessor.h" +#include "oakengine/audio.h" #include "audiowaveformview.h" #include "node/output/viewer/viewer.h" -#include "oakengine/playback.h" -#include "render/previewautocacher.h" +#include "render/previewaudiodevice.h" +#include "oakengine/preview.h" #include "viewerdisplay.h" #include "viewersizer.h" #include "viewerwindow.h" @@ -44,6 +44,7 @@ namespace olive { +class EngineEventBridge; class MulticamWidget; /** @@ -91,7 +92,7 @@ public: */ void set_full_screen(QScreen *screen = nullptr); - ColorManager *color_manager() const + OakEngineColorManager *color_manager() const { return display_widget_->color_manager(); } @@ -122,9 +123,13 @@ public: } } - void set_node_view_selections(const QVector &n) + void set_node_view_selections(const QVector &n) { - node_view_selected_ = n; + node_view_selected_.clear(); + node_view_selected_.reserve(n.size()); + foreach (OakEngineNode *handle, n) { + node_view_selected_.append(reinterpret_cast(handle)); + } if (!is_playing()) { // If is playing, this will happen by the next frame automatically @@ -184,12 +189,12 @@ signals: /** * @brief Wrapper for ViewerGLWidget::ColorProcessorChanged() */ - void color_processor_changed(ColorProcessorPtr processor); + void color_processor_changed(ColorProcessorHandlePtr processor); /** * @brief Wrapper for ViewerGLWidget::ColorManagerChanged() */ - void color_manager_changed(ColorManager *color_manager); + void color_manager_changed(OakEngineColorManager *color_manager); protected: ViewerWidget(ViewerDisplayWidget *display, QWidget *parent = nullptr); @@ -219,7 +224,7 @@ protected: ignore_scrub_++; } - RenderTicketPtr get_single_frame(const Rational &t, bool dry = false); + OakEnginePreviewRequest *get_single_frame(const Rational &t, bool dry = false); void set_waveform_mode(WaveformMode wf); @@ -250,16 +255,15 @@ private: bool viewer_might_be_a_still(); void set_display_image(RenderTicketPtr ticket); + void set_display_image(OakEnginePreviewRequest *req); - RenderTicketPtr get_frame(const Rational &t); + OakEnginePreviewRequest *request_next_frame_for_queue(bool increment = true); - static FramePtr decode_cached_image(const QString &cache_path, - const QUuid &cache_id, - const int64_t &time); + OakEnginePreviewRequest *get_frame(const Rational &t); - static void decode_cached_image(RenderTicketPtr ticket, - const QString &cache_path, - const QUuid &cache_id, const int64_t &time); + void finish_play_preprocess(); + + int determine_playback_queue_size(); bool should_force_waveform() const; @@ -267,6 +271,8 @@ private: void update_auto_cacher(); + void decrement_prequeued_audio(); + void arm_for_recording(); void disarm_recording(); @@ -277,15 +283,6 @@ private: bool is_video_visible() const; - void deliver_facade_frame(const FramePtr &frame, const Rational &ts); - - // Trampolines for the facade playback engine's pull-thread - // callbacks; both marshal onto the main thread. - static void facade_frame_callback(const oak_playback_frame *frame, - void *userdata); - static void facade_audio_callback(const oak_playback_audio *audio, - void *userdata); - ViewerSizer *sizer_; int playback_speed_; @@ -306,24 +303,33 @@ private: ViewerDisplayWidget *context_menu_widget_; - // Facade playback session (created on first play; the engine drives - // frames/audio from its pull thread and we poll its position). - OakEnginePlayback *playback_; - QTimer playback_poll_timer_; - - // Sample-rate/channel converter for the audio SCRUB path (continuous - // playback audio is handled by the facade engine itself). - AudioProcessor audio_processor_; + QTimer playback_backup_timer_; + int64_t playback_queue_next_frame_; + int64_t dry_run_next_frame_; QVector playback_devices_; - QList nonqueue_watchers_; + bool prequeuing_video_; + int prequeuing_audio_; + + QList nonqueue_watchers_; Rational last_length_; + int prequeue_length_; + int prequeue_count_; + + QVector queue_watchers_; + + std::list audio_playback_queue_; + Rational audio_playback_queue_time_; + OakEngineAudioProcessor *audio_processor_; + QByteArray prequeued_audio_; + static const Rational k_audio_playback_interval; + static QVector instances; - std::list audio_scrub_watchers_; + std::list audio_scrub_watchers_; bool record_armed_; bool recording_; @@ -332,10 +338,15 @@ private: Track::Reference recording_track_; QString recording_filename_; + qint64 queue_starved_start_; + OakEnginePreviewRequest *first_requeue_watcher_; + bool enable_audio_scrubbing_; WaveformMode waveform_mode_; + QVector dry_run_watchers_; + int ignore_scrub_; QVector timeline_selected_blocks_; @@ -343,12 +354,16 @@ private: MulticamWidget *multicam_panel_; + EngineEventBridge *bridge_; + + int64_t audio_notify_sub_ = 0; + private slots: - void playback_poll_update(); + void playback_timer_update(); void length_changed_slot(const Rational &length); - void interlacing_changed_slot(VideoParams::Interlacing interlacing); + void interlacing_changed_slot(int interlacing); void update_renderer_video_parameters(); @@ -374,6 +389,8 @@ private slots: void renderer_generated_frame(); + void renderer_generated_frame_for_queue(); + void viewer_invalidated_video_range(const olive::TimeRange &range); void update_waveform_mode_from_menu(QAction *a); @@ -382,14 +399,30 @@ private slots: void dropped(QDropEvent *event); + void queue_next_audio_buffer(); + + void received_audio_buffer_for_playback(); + void received_audio_buffer_for_scrubbing(); + void queue_starved(); + void queue_no_longer_starved(); + + void force_requeue_from_current_time(); + void force_requeue_from_current_time_internal(); + void update_audio_processor(); void create_addable_at(const QRectF &f); + void handle_first_requeue_destroy(); + void show_subtitle_properties(); + void dry_run_finished(); + + void request_next_dry_run(); + void save_frame_as_image(); void detect_multicam_node_now(); diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 636f8d082..cb72a7c11 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -35,10 +35,16 @@ #include #include +#include "audio/audiomanager.h" #include "common/define.h" #include "common/html.h" +#include "oakengine/gizmo.h" +#include "oakengine/videoparams.h" +#include "oakengine/display.h" +#include "oakengine/undo.h" +#include "widget/viewer/vieweroutpututils.h" #include "common/qtutils.h" -#include "config/config.h" +#include "common/configwrapper.h" #include "core.h" #include "node/block/subtitle/subtitle.h" #include "codec/frame.h" @@ -46,6 +52,7 @@ #include "node/gizmo/point.h" #include "node/gizmo/polygon.h" #include "node/gizmo/screen.h" +#include "render/job/colortransformjob.h" #include "window/mainwindow/mainwindow.h" namespace olive @@ -72,6 +79,7 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) , add_band_(false) , queue_starved_(false) , text_edit_(nullptr) + , gizmo_params_(empty_video_params()) { connect(Core::instance(), &Core::tool_changed, this, &ViewerDisplayWidget::tool_changed); @@ -83,6 +91,10 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) frame_rate_averages_.resize(k_frame_rate_average_count); inner_widget()->setAcceptDrops(true); + + bridge_ = new EngineEventBridge(this); + connect(bridge_, &EngineEventBridge::sequence_subtitles_changed, this, + [this](OakEngineSequence *, qint64, qint64) { update(); }); } ViewerDisplayWidget::~ViewerDisplayWidget() @@ -223,15 +235,16 @@ void ViewerDisplayWidget::set_time(const Rational &time) void ViewerDisplayWidget::set_subtitle_tracks(Sequence *list) { if (subtitle_tracks_) { - disconnect(subtitle_tracks_, &Sequence::subtitles_changed, this, - &ViewerDisplayWidget::subtitles_changed); + bridge_->unsubscribe(subtitle_sub_); + subtitle_sub_ = 0; } subtitle_tracks_ = list; if (subtitle_tracks_) { - connect(subtitle_tracks_, &Sequence::subtitles_changed, this, - &ViewerDisplayWidget::subtitles_changed); + subtitle_sub_ = bridge_->subscribe( + reinterpret_cast(subtitle_tracks_), + OAKENGINE_EVENT_SEQUENCE_SUBTITLES_CHANGED); } update(); @@ -396,7 +409,7 @@ void ViewerDisplayWidget::on_paint() bg_color.greenF(), bg_color.blueF()); } - VideoParams device_params; + VideoParams device_params = empty_video_params(); ColorTransformJob ctj; bool have_ctj = false; @@ -420,11 +433,12 @@ void ViewerDisplayWidget::on_paint() texture_->height() != frame->height() || texture_->format() != frame->format() || texture_->channel_count() != frame->channel_count())) { - texture_ = renderer()->create_texture( - frame->video_params(), frame->data(), - frame->linesize_pixels()); + oakengine_display_renderer_create_texture( + renderer(), &frame->video_params(), frame->data(), + frame->linesize_pixels(), &texture_); } else if (!drew_backend_neutral_frame) { - texture_->upload(frame->data(), frame->linesize_pixels()); + oakengine_display_texture_upload(texture_.get(), frame->data(), + frame->linesize_pixels()); } } else if (TexturePtr texture = load_frame_.value()) { // This is a GPU texture, switch to it directly when possible. @@ -439,15 +453,17 @@ void ViewerDisplayWidget::on_paint() texture_ = texture; } else { // Cross-backend texture: download and re-upload - FramePtr frame = Frame::create(); - frame->set_video_params(texture->params()); - if (frame->allocate()) { + FramePtr frame; + oakengine_codec_frame_create(&frame); + oakengine_codec_frame_set_video_params( + frame.get(), &texture->params()); + if (oakengine_codec_frame_allocate(frame.get())) { texture->renderer()->download_from_texture( texture->id(), texture->params(), frame->data(), frame->linesize_pixels()); - texture_ = renderer()->create_texture( - frame->video_params(), frame->data(), - frame->linesize_pixels()); + oakengine_display_renderer_create_texture( + renderer(), &frame->video_params(), frame->data(), + frame->linesize_pixels(), &texture_); } else { texture_ = texture; } @@ -488,8 +504,9 @@ void ViewerDisplayWidget::on_paint() deinterlace_texture_->params() != texture_to_draw->params()) { // (Re)create texture - deinterlace_texture_ = renderer()->create_texture( - texture_to_draw->params()); + oakengine_display_renderer_create_texture( + renderer(), &texture_to_draw->params(), nullptr, + 0, &deinterlace_texture_); } ShaderJob job; @@ -509,7 +526,8 @@ void ViewerDisplayWidget::on_paint() texture_to_draw = deinterlace_texture_; } - ctj.set_color_processor(color_service()); + oakengine_color_transform_job_set_processor( + &ctj, color_service().get()); ctj.set_input_texture(texture_to_draw); ctj.set_input_alpha_association( OAK_CONFIG("ReassocLinToNonLin").toBool() ? @@ -531,7 +549,8 @@ void ViewerDisplayWidget::on_paint() if (backend_neutral) { draw_backend_neutral(ctj, &bg_painter); } else { - renderer()->blit_color_managed(ctj, device_params); + oakengine_display_renderer_blit_color_managed( + renderer(), &ctj, nullptr, &device_params); } } @@ -789,20 +808,25 @@ QTransform ViewerDisplayWidget::generate_display_transform() return gizmo_transform; } -QTransform ViewerDisplayWidget::generate_gizmo_transform(NodeTraverser >, +QTransform ViewerDisplayWidget::generate_gizmo_transform(Node *gizmos, + Node *target, const TimeRange &range) { QTransform t = generate_display_transform(); - if (get_time_target()) { - Node *target = get_time_target(); + if (target) { if (ViewerOutput *v = dynamic_cast(target)) { if (Node *n = v->get_connected_texture_output()) { target = n; } } - QTransform nt; - gt.transform(&nt, gizmos_, target, range); + double m[6]; + oakengine_traverse_transform( + reinterpret_cast(gizmos), + reinterpret_cast(target), + range.in().numerator(), range.in().denominator(), + range.out().numerator(), range.out().denominator(), nullptr, m); + QTransform nt(m[0], m[1], m[2], m[3], m[4], m[5]); t.translate(gizmo_params_.width() * 0.5, gizmo_params_.height() * 0.5); t.scale(gizmo_params_.width(), gizmo_params_.height()); @@ -861,8 +885,6 @@ void ViewerDisplayWidget::open_text_gizmo(TextGizmo *text, QMouseEvent *event) gizmo_draw_time_, LoopMode::k_loop_mode_off)); active_text_gizmo_ = text; - connect(active_text_gizmo_, &TextGizmo::rect_changed, this, - &ViewerDisplayWidget::update_active_text_gizmo_size); text_transform_ = generate_gizmo_transform(); text_transform_inverted_ = text_transform_.inverted(); @@ -899,16 +921,30 @@ void ViewerDisplayWidget::open_text_gizmo(TextGizmo *text, QMouseEvent *event) QRectF text_rect = update_active_text_gizmo_size(); // Emit text gizmo activation signal - emit text->activated(); + oakengine_text_gizmo_activated( + reinterpret_cast(gizmos_)); // Create toolbar text_toolbar_ = new ViewerTextEditorToolBar(text_edit_); text_toolbar_->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); connect(text_toolbar_, &ViewerTextEditorToolBar::vertical_alignment_changed, - text, &TextGizmo::set_vertical_alignment); - connect(text, &TextGizmo::vertical_alignment_changed, text_toolbar_, - &ViewerTextEditorToolBar::set_vertical_alignment); - text_toolbar_->set_vertical_alignment(text->get_vertical_alignment()); + this, [this](Qt::Alignment align) { + oakengine_text_gizmo_set_vertical_alignment( + reinterpret_cast(gizmos_), + static_cast(align)); + }); + { + oakengine_text_gizmo _tg; + if (oakengine_text_gizmo_get( + reinterpret_cast(gizmos_), + get_gizmo_time().numerator(), get_gizmo_time().denominator(), + &_tg) == OAKENGINE_OK) { + text_toolbar_->set_vertical_alignment( + static_cast(_tg.vertical_alignment)); + } else { + text_toolbar_->set_vertical_alignment(Qt::AlignTop); + } + } text_edit_->connect_tool_bar(text_toolbar_); QPoint toolbar_pos = @@ -1033,40 +1069,41 @@ bool ViewerDisplayWidget::on_mouse_move(QMouseEvent *event) } else if (current_gizmo_) { // Signal movement - if (DraggableGizmo *draggable = - dynamic_cast(current_gizmo_)) { + int drag_behavior = oakengine_gizmo_get_drag_value_behavior(current_gizmo_); + if (drag_behavior >= 0) { if (!gizmo_drag_started_) { QPointF start = screen_to_scene_point(gizmo_start_drag_); Rational gizmo_time = get_gizmo_time(); - NodeTraverser t; - t.set_cache_video_params(gizmo_params_); - t.set_cache_audio_params(gizmo_audio_params_); - NodeValueRow row = t.generate_row( - gizmos_, - TimeRange(gizmo_time, - gizmo_time + - gizmo_params_.frame_rate_as_time_base())); + NodeValueRow row; + oakengine_traverse_generate_row( + reinterpret_cast(gizmos_), + gizmo_time.numerator(), gizmo_time.denominator(), + (gizmo_time + gizmo_params_.frame_rate_as_time_base()) + .numerator(), + (gizmo_time + gizmo_params_.frame_rate_as_time_base()) + .denominator(), + nullptr, 0, 0, &row); - draggable->drag_start(row, start.x(), start.y(), gizmo_time); + oakengine_gizmo_drag_start(current_gizmo_, &row, + start.x(), start.y(), gizmo_time.numerator(), + gizmo_time.denominator()); gizmo_drag_started_ = true; } QPointF v = screen_to_scene_point(event->pos()); - switch (draggable->get_drag_value_behavior()) { - case DraggableGizmo::k_absolute: - // Above value is correct - break; - case DraggableGizmo::k_delta_from_previous: + switch (drag_behavior) { + case 1: v -= screen_to_scene_point(gizmo_last_drag_); gizmo_last_drag_ = event->pos(); break; - case DraggableGizmo::k_delta_from_start: + case 2: v -= screen_to_scene_point(gizmo_start_drag_); break; } - draggable->drag_move(v.x(), v.y(), event->modifiers()); + oakengine_gizmo_drag_move(current_gizmo_, v.x(), v.y(), + static_cast(event->modifiers())); return true; } @@ -1101,12 +1138,9 @@ bool ViewerDisplayWidget::on_mouse_release(QMouseEvent *e) } else if (current_gizmo_) { // Handle gizmo if (gizmo_drag_started_) { - MultiUndoCommand *command = new MultiUndoCommand(); - if (DraggableGizmo *draggable = - dynamic_cast(current_gizmo_)) { - draggable->drag_end(command); - } - Core::instance()->undo_stack()->push(command, tr("Dragged Gizmo")); + void *command = oakengine_undo_command_create_multi(); + oakengine_gizmo_drag_end(current_gizmo_, command); + oakengine_undo_push(command, tr("Dragged Gizmo").toUtf8().constData()); gizmo_drag_started_ = false; } current_gizmo_ = nullptr; @@ -1173,7 +1207,7 @@ void ViewerDisplayWidget::emit_color_at_cursor(QMouseEvent *e) reference = renderer()->get_pixel_from_texture(texture_.get(), pixel_pos); if (color_service()) { - display = color_service()->convert_color(reference); + display = oak_convert_color(color_service(), reference); } else { display = reference; } @@ -1231,8 +1265,13 @@ void ViewerDisplayWidget::draw_subtitle_tracks() if (SubtitleBlock *sub = dynamic_cast( sub_track->visible_block_at_time(time_))) { // Split into lines + char text_buf[4096]; + const int len = oakengine_subtitle_get_text( + reinterpret_cast(sub), text_buf, + sizeof(text_buf)); QStringList list = QtUtils::word_wrap_string( - sub->get_text(), fm, bounding_box.width()); + QString::fromUtf8(text_buf, len), fm, + bounding_box.width()); for (int i = list.size() - 1; i >= 0; i--) { int w = QtUtils::q_font_metrics_width(fm, list.at(i)); @@ -1393,24 +1432,26 @@ void ViewerDisplayWidget::close_text_editor() text_edit_->deleteLater(); text_edit_ = nullptr; - disconnect(active_text_gizmo_, &TextGizmo::rect_changed, this, - &ViewerDisplayWidget::update_active_text_gizmo_size); active_text_gizmo_ = nullptr; } void ViewerDisplayWidget::generate_gizmo_transforms() { - NodeTraverser gt; - gt.set_cache_video_params(gizmo_params_); - gt.set_cache_audio_params(gizmo_audio_params_); - gizmo_draw_time_ = generate_gizmo_time(); if (gizmos_) { - gizmo_db_ = gt.generate_row(gizmos_, gizmo_draw_time_); + NodeValueRow row; + oakengine_traverse_generate_row( + reinterpret_cast(gizmos_), + gizmo_draw_time_.in().numerator(), + gizmo_draw_time_.in().denominator(), + gizmo_draw_time_.out().numerator(), + gizmo_draw_time_.out().denominator(), nullptr, 0, 0, &row); + gizmo_db_ = row; } - gizmo_last_draw_transform_ = generate_gizmo_transform(gt, gizmo_draw_time_); + gizmo_last_draw_transform_ = generate_gizmo_transform( + gizmos_, get_time_target(), gizmo_draw_time_); gizmo_last_draw_transform_inverted_ = gizmo_last_draw_transform_.inverted(); } @@ -1437,7 +1478,9 @@ bool ViewerDisplayWidget::draw_backend_neutral_frame(const FramePtr &frame, return false; } - const QString color_id = QString::fromUtf8(color_service()->id()); + const QString color_id = oak_query_string([this](char *buf, int size) { + return oakengine_color_processor_id(color_service().get(), buf, size); + }); if (backend_neutral_cpu_source_frame_.get() == frame.get() && backend_neutral_cpu_color_id_ == color_id && !backend_neutral_cpu_image_.isNull()) { @@ -1457,7 +1500,7 @@ bool ViewerDisplayWidget::draw_backend_neutral_frame(const FramePtr &frame, QImage source_image; if (display_frame->format() == PixelFormat::u8 && - display_frame->channel_count() == VideoParams::k_rgba_channel_count) { + display_frame->channel_count() == 4) { backend_neutral_cpu_display_frame_ = display_frame; backend_neutral_cpu_image_ = QImage(reinterpret_cast(display_frame->const_data()), @@ -1466,7 +1509,7 @@ bool ViewerDisplayWidget::draw_backend_neutral_frame(const FramePtr &frame, source_image = backend_neutral_cpu_image_; } else if (display_frame->format() == PixelFormat::u8 && display_frame->channel_count() == - VideoParams::k_rgb_channel_count) { + 3) { backend_neutral_cpu_display_frame_ = display_frame; backend_neutral_cpu_image_ = QImage(reinterpret_cast(display_frame->const_data()), @@ -1476,7 +1519,9 @@ bool ViewerDisplayWidget::draw_backend_neutral_frame(const FramePtr &frame, } else { backend_neutral_cpu_display_frame_.reset(); const int bytes_per_pixel = - display_frame->video_params().get_bytes_per_pixel(); + oakengine_video_params_bytes_per_pixel( + static_cast(display_frame->video_params().format()), + display_frame->video_params().channel_count()); if (backend_neutral_cpu_image_.size() != QSize(display_frame->width(), display_frame->height()) || backend_neutral_cpu_image_.format() != QImage::Format_RGBA8888) { @@ -1523,7 +1568,9 @@ bool ViewerDisplayWidget::draw_backend_neutral_texture(const TexturePtr &texture return false; } - const QString color_id = QString::fromUtf8(color_service()->id()); + const QString color_id = oak_query_string([this](char *buf, int size) { + return oakengine_color_processor_id(color_service().get(), buf, size); + }); if (backend_neutral_cpu_source_texture_.get() == texture.get() && backend_neutral_cpu_color_id_ == color_id && !backend_neutral_cpu_image_.isNull()) { @@ -1535,13 +1582,15 @@ bool ViewerDisplayWidget::draw_backend_neutral_texture(const TexturePtr &texture return true; } - FramePtr frame = Frame::create(); - frame->set_video_params(texture->params()); - if (!frame->allocate()) { + FramePtr frame; + oakengine_codec_frame_create(&frame); + oakengine_codec_frame_set_video_params(frame.get(), &texture->params()); + if (!oakengine_codec_frame_allocate(frame.get())) { return false; } - texture->download(frame->data(), frame->linesize_pixels()); + oakengine_display_texture_download(texture.get(), frame->data(), + frame->linesize_pixels()); if (!draw_backend_neutral_frame(frame, painter)) { return false; @@ -1564,19 +1613,23 @@ void ViewerDisplayWidget::draw_backend_neutral(const ColorTransformJob &ctj, const int texture_width = static_cast(width() * devicePixelRatioF()); const int texture_height = static_cast(height() * devicePixelRatioF()); - const VideoParams offscreen_params(texture_width, texture_height, - PixelFormat::u8, - VideoParams::k_rgba_channel_count); + oak_video_params pod = {}; + pod.width = texture_width; + pod.height = texture_height; + pod.format = PixelFormat::u8; + const VideoParams offscreen_params(video_params_from_pod(pod)); if (!backend_neutral_texture_ || backend_neutral_texture_->params() != offscreen_params) { // The offscreen texture is sized in device pixels so high-DPI widgets // draw one downloaded pixel per device pixel after setDevicePixelRatio(). - backend_neutral_texture_ = renderer()->create_texture(offscreen_params); + oakengine_display_renderer_create_texture(renderer(), &offscreen_params, + nullptr, 0, + &backend_neutral_texture_); backend_neutral_buffer_.resize( texture_width * texture_height * - VideoParams::get_bytes_per_pixel(PixelFormat::u8, - VideoParams::k_rgba_channel_count)); + oakengine_video_params_bytes_per_pixel(0, // PixelFormat::u8 + 4)); } if (!backend_neutral_texture_ || backend_neutral_texture_->is_dummy()) { @@ -1588,12 +1641,13 @@ void ViewerDisplayWidget::draw_backend_neutral(const ColorTransformJob &ctj, // Reuse the normal color-management shader path, but render into a texture // instead of an OpenGL widget framebuffer. - renderer()->blit_color_managed(local_ctj, backend_neutral_texture_.get()); + oakengine_display_renderer_blit_color_managed( + renderer(), &local_ctj, backend_neutral_texture_.get(), nullptr); - backend_neutral_texture_->download(backend_neutral_buffer_.data(), 0); + oakengine_display_texture_download(backend_neutral_texture_.get(), + backend_neutral_buffer_.data(), 0); - const int bytes_per_pixel = VideoParams::get_bytes_per_pixel( - PixelFormat::u8, VideoParams::k_rgba_channel_count); + const int bytes_per_pixel = oakengine_video_params_bytes_per_pixel(0, 4); // u8, RGBA QImage img( reinterpret_cast(backend_neutral_buffer_.constData()), @@ -1632,10 +1686,7 @@ void ViewerDisplayWidget::play(const int64_t &start_timestamp, playback_timebase_ = timebase; playback_speed_ = playback_speed; - // The facade playback engine owns the master clock; seed the display - // clock with the start timestamp (the ViewerWidget keeps feeding it - // from oakengine_playback_get_position afterwards). - external_ts_.store(start_timestamp); + timer_.start(start_timestamp, playback_speed, timebase.to_double()); if (start_updating) { connect(this, &ViewerDisplayWidget::frame_swapped, this, @@ -1650,8 +1701,6 @@ void ViewerDisplayWidget::pause() disconnect(this, &ViewerDisplayWidget::frame_swapped, this, &ViewerDisplayWidget::update_from_queue); - external_ts_.store(-1); - queue_.clear(); queue_starved_ = false; } @@ -1667,11 +1716,7 @@ QPointF ViewerDisplayWidget::screen_to_scene_point(const QPoint &p) void ViewerDisplayWidget::update_from_queue() { - const int64_t t = external_ts_.load(); - if (t < 0) { - // No facade playback position fed yet. - return; - } + int64_t t = timer_.get_timestamp_now(); Rational time = Timecode::timestamp_to_time(t, playback_timebase_); @@ -1731,14 +1776,16 @@ void ViewerDisplayWidget::text_edit_changed() editor->property("gizmo").value()); QString html = Html::doc_to_html(editor->document()); - gizmo->update_input_html(html, get_gizmo_time()); + oakengine_text_gizmo_update_html( + reinterpret_cast(gizmos_), + html.toUtf8().constData(), + get_gizmo_time().numerator(), get_gizmo_time().denominator()); } void ViewerDisplayWidget::text_edit_destroyed() { - TextGizmo *gizmo = reinterpret_cast( - sender()->property("gizmo").value()); - emit gizmo->deactivated(); + oakengine_text_gizmo_deactivated( + reinterpret_cast(gizmos_)); text_edit_ = nullptr; text_toolbar_ = nullptr; inner_widget()->setMouseTracking(false); diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index 3b9fc5303..b7fcbd31d 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -22,19 +22,20 @@ #ifndef OAK_VIEWERGLWIDGET_H #define OAK_VIEWERGLWIDGET_H -#include - #include #include #include #include "codec/frame.h" +#include "engineeventbridge.h" #include "node/color/colormanager/colormanager.h" #include "node/gizmo/text.h" #include "node/node.h" #include "node/output/track/tracklist.h" -#include "node/traverser.h" +#include "node/value.h" +#include "oakengine/traverse.h" #include "tool/tool.h" +#include "viewerplaybacktimer.h" #include "viewerqueue.h" #include "viewersafemargininfo.h" #include "viewertexteditor.h" @@ -140,7 +141,7 @@ public: return texture_; } - ColorProcessorPtr get_current_color_processor() + ColorProcessorHandlePtr get_current_color_processor() { return color_service(); } @@ -155,16 +156,9 @@ public: return &queue_; } - /** - * @brief Feed the facade playback position as this display's clock - * - * The facade playback engine owns the master clock now; the - * ViewerWidget polls it (oakengine_playback_get_position) and pushes - * the timestamp here for update_from_queue() to pop by. - */ - void set_playback_timestamp(int64_t ts) + ViewerPlaybackTimer *timer() { - external_ts_.store(ts); + return &timer_; } QPointF screen_to_scene_point(const QPoint &p); @@ -261,13 +255,12 @@ protected: QTransform generate_display_transform(); - QTransform generate_gizmo_transform(NodeTraverser >, - const TimeRange &range); + QTransform generate_gizmo_transform(Node *gizmos, Node *target, + const TimeRange &range); QTransform generate_gizmo_transform() { - NodeTraverser t; - t.set_cache_video_params(gizmo_params_); - return generate_gizmo_transform(t, generate_gizmo_time()); + TimeRange range = generate_gizmo_time(); + return generate_gizmo_transform(gizmos_, get_time_target(), range); } TimeRange generate_gizmo_time() @@ -425,6 +418,9 @@ private: bool show_subtitles_; Sequence *subtitle_tracks_; + EngineEventBridge *bridge_; + int64_t subtitle_sub_ = 0; + Rational time_; /** @@ -469,7 +465,7 @@ private: // Playback ViewerQueue queue_; - std::atomic external_ts_{ -1 }; + ViewerPlaybackTimer timer_; Rational playback_timebase_; diff --git a/app/widget/viewer/vieweroutpututils.cpp b/app/widget/viewer/vieweroutpututils.cpp new file mode 100644 index 000000000..d89641d96 --- /dev/null +++ b/app/widget/viewer/vieweroutpututils.cpp @@ -0,0 +1,76 @@ +/*** + + 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 "vieweroutpututils.h" + +namespace olive { + +VideoParams video_params_from_pod(const oak_video_params &pod) +{ + void *vp = oakengine_video_params_create(&pod); + VideoParams result = *static_cast(vp); + oakengine_video_params_free(vp); + return result; +} + +VideoParams empty_video_params() +{ + oak_video_params pod = {}; + return video_params_from_pod(pod); +} + +Rational sequence_timebase(const void *sequence) +{ + oak_video_params vp = {}; + if (oakengine_viewer_get_video_params( + reinterpret_cast(sequence), 0, &vp) < 0 || + vp.time_base_den <= 0) { + return Rational(); + } + return Rational(vp.time_base_num, vp.time_base_den); +} + +VideoParams viewer_output_video_params(const void *viewer, int index) +{ + oak_video_params vp; + if (oakengine_viewer_get_video_params( + reinterpret_cast(viewer), index, &vp) < 0 || + vp.width <= 0 || vp.height <= 0) { + return empty_video_params(); + } + return video_params_from_pod(vp); +} + +AudioParams viewer_output_audio_params(const void *viewer, int index) +{ + int sample_rate = 0; + int format = 0; + uint64_t channel_layout = 0; + if (oakengine_viewer_get_audio_params( + reinterpret_cast(viewer), index, &sample_rate, + &channel_layout, &format) < 0 || + sample_rate <= 0) { + return AudioParams(); + } + return AudioParams(sample_rate, channel_layout, + static_cast(format)); +} + +} // namespace olive diff --git a/app/widget/viewer/vieweroutpututils.h b/app/widget/viewer/vieweroutpututils.h new file mode 100644 index 000000000..0d539141a --- /dev/null +++ b/app/widget/viewer/vieweroutpututils.h @@ -0,0 +1,103 @@ +/*** + + 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 VIEWEROUTPUTUTILS_H +#define VIEWEROUTPUTUTILS_H + +#include + +#include + +#include "oakengine/viewer.h" +#include "render/subtitleparams.h" +#include "render/videoparams.h" + +/** + * @file vieweroutpututils.h + * @brief Facade-backed replacements for ViewerOutput's header-inline + * parameter getters + * + * ViewerOutput::get_video_params()/get_audio_params()/... are defined inline + * in the engine header and reference the ViewerOutput::k_*_params_input + * statics, so calling them from the app would leave undefined ViewerOutput + * symbols in oak-editor. These helpers fetch the same values through the + * oakengine C ABI instead. `viewer` may be any viewer handle (ViewerOutput, + * Sequence, Footage). + */ +namespace olive { + +VideoParams viewer_output_video_params(const void *viewer, int index = 0); + +AudioParams viewer_output_audio_params(const void *viewer, int index = 0); + +/** + * @brief Construct a VideoParams from an oak_video_params POD using the engine + * facade. This is the single app-side construction point allowed during the R6 + * C ABI migration; all other app code must not call VideoParams constructors + * directly. + */ +VideoParams video_params_from_pod(const oak_video_params &pod); + +/** @brief Equivalent to the default-constructed VideoParams(). */ +VideoParams empty_video_params(); + +/** + * @brief Frame-duration timebase of `sequence` as a Rational (frame rate flipped). + * + * Facade-backed replacement for ViewerOutput::get_video_params().frame_rate(). + * flipped(), used for Rational -> timestamp conversions without pulling in + * ViewerOutput inline symbols. + */ +Rational sequence_timebase(const void *sequence); + +/** + * @brief 1 if `node`'s engine type id (Node::id()) equals `type_id`. + * Facade-backed replacement for dynamic_cast-based type probes (a + * dynamic_cast to/from ViewerOutput drags an undefined ViewerOutput + * typeinfo reference into the app binary). + */ +inline bool viewer_output_node_type_is(const void *node, const char *type_id) +{ + if (!node) { + return false; + } + char buf[128]; + const int len = oakengine_node_get_type_id( + reinterpret_cast(node), buf, sizeof(buf)); + return len >= 0 && len < int(sizeof(buf)) && strcmp(buf, type_id) == 0; +} + +/** @brief 1 if `node` is a Sequence (Sequence::id()). */ +inline bool viewer_output_is_sequence(const void *node) +{ + return viewer_output_node_type_is(node, + "org.olivevideoeditor.Olive.sequence"); +} + +/** @brief 1 if `node` is a Footage (Footage::id()). */ +inline bool viewer_output_is_footage(const void *node) +{ + return viewer_output_node_type_is(node, + "org.olivevideoeditor.Olive.footage"); +} + +} // namespace olive + +#endif // VIEWEROUTPUTUTILS_H diff --git a/app/widget/viewer/viewerplaybacktimer.cpp b/app/widget/viewer/viewerplaybacktimer.cpp new file mode 100644 index 000000000..198790103 --- /dev/null +++ b/app/widget/viewer/viewerplaybacktimer.cpp @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive 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 "viewerplaybacktimer.h" + +#include + +namespace olive +{ + +void ViewerPlaybackTimer::start(const int64_t &start_timestamp, const int &playback_speed, const double &timebase) +{ + start_msec_ = QDateTime::currentMSecsSinceEpoch(); + start_timestamp_ = start_timestamp; + playback_speed_ = playback_speed; + timebase_ = timebase; +} + +int64_t ViewerPlaybackTimer::get_timestamp_now() const +{ + int64_t real_time = QDateTime::currentMSecsSinceEpoch() - start_msec_; + + int64_t frames_since_start = qRound(static_cast(real_time) / (timebase_ * 1000)); + + return start_timestamp_ + frames_since_start * playback_speed_; +} + +} diff --git a/app/widget/viewer/viewerplaybacktimer.h b/app/widget/viewer/viewerplaybacktimer.h new file mode 100644 index 000000000..c902a4432 --- /dev/null +++ b/app/widget/viewer/viewerplaybacktimer.h @@ -0,0 +1,49 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2019 Olive 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 VIEWERPLAYBACKTIMER_H +#define VIEWERPLAYBACKTIMER_H + +#include + +#include "common/define.h" + +namespace olive +{ + +class ViewerPlaybackTimer { +public: + void start(const int64_t& start_timestamp, const int& playback_speed, const double& timebase); + + int64_t get_timestamp_now() const; + +private: + qint64 start_msec_; + int64_t start_timestamp_; + + int playback_speed_; + + double timebase_; + +}; + +} + +#endif // VIEWERPLAYBACKTIMER_H diff --git a/app/window/mainwindow/mainmenu.cpp b/app/window/mainwindow/mainmenu.cpp index 9521bf0bc..7af153675 100644 --- a/app/window/mainwindow/mainmenu.cpp +++ b/app/window/mainwindow/mainmenu.cpp @@ -26,8 +26,9 @@ #include #include -#include "config/config.h" #include "core.h" +#include "oakengine/project.h" +#include "oakengine/undo.h" #include "dialog/actionsearch/actionsearch.h" #include "dialog/diskcache/diskcachedialog.h" #include "dialog/proxy/proxydialog.h" @@ -38,6 +39,7 @@ #include "undo/undostack.h" #include "widget/menu/menushared.h" #include "mainwindow.h" +#include "common/configwrapper.h" namespace olive { @@ -89,10 +91,10 @@ MainMenu::MainMenu(MainWindow *parent) connect(edit_menu_, &Menu::aboutToHide, this, &MainMenu::edit_menu_about_to_hide); - edit_undo_item_ = Core::instance()->undo_stack()->GetUndoAction(); + edit_undo_item_ = reinterpret_cast(oakengine_undo_undo_action()); Menu::conform_item(edit_undo_item_, "undo", tr("Ctrl+Z")); edit_menu_->addAction(edit_undo_item_); - edit_redo_item_ = Core::instance()->undo_stack()->GetRedoAction(); + edit_redo_item_ = reinterpret_cast(oakengine_undo_redo_action()); Menu::conform_item(edit_redo_item_, "redo", tr("Ctrl+Shift+Z")); edit_menu_->addAction(edit_redo_item_); @@ -365,7 +367,7 @@ MainMenu::MainMenu(MainWindow *parent) Menu::conform_item(tools_use_proxy_item_, "useproxymedia"); tools_use_proxy_item_->setCheckable(true); tools_use_proxy_item_->setChecked( - Config::current()[QStringLiteral("UseProxyMedia")].toBool()); + OAK_CONFIG("UseProxyMedia").toBool()); connect(tools_use_proxy_item_, &QAction::triggered, Core::instance(), &Core::set_use_proxy_media); tools_menu_->addAction(tools_use_proxy_item_); @@ -439,9 +441,13 @@ void MainMenu::file_menu_about_to_show() file_save_as_item_->setEnabled(active_project); if (active_project) { - file_save_item_->setText(tr("&Save '%1'").arg(active_project->name())); + char name_buf[256]; + oakengine_project_name( + reinterpret_cast(active_project), + name_buf, sizeof(name_buf)); + file_save_item_->setText(tr("&Save '%1'").arg(name_buf)); file_save_as_item_->setText( - tr("Save '%1' &As").arg(active_project->name())); + tr("Save '%1' &As").arg(name_buf)); } else { file_save_item_->setText(tr("&Save Project")); file_save_as_item_->setText(tr("Save Project &As")); @@ -543,7 +549,7 @@ void MainMenu::window_menu_about_to_show() void MainMenu::populate_open_recent() { - if (Core::instance()->get_recent_projects().isEmpty()) { + if (Core::instance()->get_recent_project_count() == 0) { // Insert dummy/disabled action to show there's nothing QAction *a = new QAction(tr("(None)")); a->setEnabled(false); @@ -551,9 +557,9 @@ void MainMenu::populate_open_recent() } else { // Populate menu with recently opened projects - for (int i = 0; i < Core::instance()->get_recent_projects().size(); i++) { + for (int i = 0; i < Core::instance()->get_recent_project_count(); i++) { QAction *a = - new QAction(Core::instance()->get_recent_projects().at(i)); + new QAction(Core::instance()->get_recent_project_at(i)); a->setData(i); connect(a, &QAction::triggered, this, &MainMenu::open_recent_item_triggered); @@ -796,8 +802,12 @@ void MainMenu::sequence_cache_in_out_triggered() void MainMenu::sequence_cache_clear_triggered() { - DiskCacheDialog::clear_disk_cache( - Core::instance()->get_active_project()->cache_path(), + char cache_buf[512]; + oakengine_project_cache_path( + reinterpret_cast( + Core::instance()->get_active_project()), + cache_buf, sizeof(cache_buf)); + DiskCacheDialog::clear_disk_cache(cache_buf, Core::instance()->main_window()); } @@ -827,7 +837,7 @@ void MainMenu::retranslate() // Edit menu edit_menu_->setTitle(tr("&Edit")); - Core::instance()->undo_stack()->update_actions(); // Update undo and redo + oakengine_undo_update_actions(); // Update undo and redo edit_delete2_item_->setText(tr("Delete (alt)")); edit_insert_item_->setText(tr("Insert")); edit_overwrite_item_->setText(tr("Overwrite")); diff --git a/app/window/mainwindow/mainstatusbar.cpp b/app/window/mainwindow/mainstatusbar.cpp index 0a6e258a7..633e0b7c4 100644 --- a/app/window/mainwindow/mainstatusbar.cpp +++ b/app/window/mainwindow/mainstatusbar.cpp @@ -23,13 +23,17 @@ #include +#include "engineeventbridge.h" + namespace olive { MainStatusBar::MainStatusBar(QWidget *parent) : QStatusBar(parent) - , manager_(nullptr) + , bridge_(nullptr) , connected_task_(nullptr) + , task_progress_sub_(0) + , task_finished_sub_(0) { setSizeGripEnabled(false); @@ -46,56 +50,69 @@ MainStatusBar::MainStatusBar(QWidget *parent) 10000); } -void MainStatusBar::connect_task_manager(TaskManager *manager) +void MainStatusBar::connect_task_manager(EngineEventBridge *bridge) { - if (manager_) { - disconnect(manager_, &TaskManager::task_list_changed, this, - &MainStatusBar::update_status); - } + bridge_ = bridge; + bridge_->subscribe(oakengine_task_manager_handle(), + OAKENGINE_EVENT_TASK_MANAGER_LIST_CHANGED); + connect(bridge_, &EngineEventBridge::task_manager_list_changed, + this, &MainStatusBar::update_status); - manager_ = manager; - - if (manager_) { - connect(manager_, &TaskManager::task_list_changed, this, - &MainStatusBar::update_status); - } + // Route progress/finished events for any task we track + connect(bridge_, &EngineEventBridge::task_progress, + this, [this](OakEngineTask *task, double d) { + if (task == connected_task_) { + set_progress_bar_value(d); + } + }); + connect(bridge_, &EngineEventBridge::task_finished, + this, [this](OakEngineTask *task, bool) { + if (task == connected_task_) { + connected_task_finished(); + } + }); } void MainStatusBar::update_status() { - if (!manager_) { - return; - } - - if (manager_->get_task_count() == 0) { + const int count = oakengine_task_manager_count(); + if (count <= 0) { clearMessage(); bar_->setVisible(false); bar_->setValue(0); - } else { - Task *t = manager_->get_first_task(); - - if (manager_->get_task_count() == 1) { - showMessage(t->get_title()); - } else { - showMessage(tr("Running %n background task(s)", nullptr, - manager_->get_task_count())); - } - - bar_->setVisible(true); - - if (connected_task_) { - disconnect(connected_task_, &Task::progress_changed, this, - &MainStatusBar::set_progress_bar_value); - disconnect(connected_task_, &Task::destroyed, this, - &MainStatusBar::connected_task_deleted); - } - - connected_task_ = t; - connect(connected_task_, &Task::progress_changed, this, - &MainStatusBar::set_progress_bar_value); - connect(connected_task_, &Task::destroyed, this, - &MainStatusBar::connected_task_deleted); + connected_task_ = nullptr; + return; } + + OakEngineTask *t = oakengine_task_manager_first(); + + if (count == 1) { + char title[256]; + oakengine_task_title(t, title, sizeof(title)); + showMessage(QString::fromUtf8(title)); + } else { + showMessage(tr("Running %n background task(s)", nullptr, count)); + } + + bar_->setVisible(true); + + // Unsubscribe from previous task events + if (task_progress_sub_ > 0) { + bridge_->unsubscribe(task_progress_sub_); + task_progress_sub_ = 0; + } + if (task_finished_sub_ > 0) { + bridge_->unsubscribe(task_finished_sub_); + task_finished_sub_ = 0; + } + + connected_task_ = t; + + // Subscribe to progress and finished events on this task + task_progress_sub_ = bridge_->subscribe( + t, OAKENGINE_EVENT_TASK_PROGRESS); + task_finished_sub_ = bridge_->subscribe( + t, OAKENGINE_EVENT_TASK_FINISHED); } void MainStatusBar::set_progress_bar_value(double d) @@ -103,7 +120,7 @@ void MainStatusBar::set_progress_bar_value(double d) bar_->setValue(qRound(100.0 * d)); } -void MainStatusBar::connected_task_deleted() +void MainStatusBar::connected_task_finished() { connected_task_ = nullptr; } diff --git a/app/window/mainwindow/mainstatusbar.h b/app/window/mainwindow/mainstatusbar.h index ce2e905e1..fe9aff851 100644 --- a/app/window/mainwindow/mainstatusbar.h +++ b/app/window/mainwindow/mainstatusbar.h @@ -25,20 +25,22 @@ #include #include -#include "task/taskmanager.h" +#include "oakengine/task.h" namespace olive { +class EngineEventBridge; + /** - * @brief Shows abbreviated information from a TaskManager object + * @brief Shows abbreviated information from the global TaskManager */ class MainStatusBar : public QStatusBar { Q_OBJECT public: MainStatusBar(QWidget *parent = nullptr); - void connect_task_manager(TaskManager *manager); + void connect_task_manager(EngineEventBridge *bridge); signals: void double_clicked(); @@ -51,14 +53,18 @@ private slots: void set_progress_bar_value(double d); - void connected_task_deleted(); + void connected_task_finished(); private: - TaskManager *manager_; + EngineEventBridge *bridge_; QProgressBar *bar_; - Task *connected_task_; + OakEngineTask *connected_task_; + + int64_t task_progress_sub_; + + int64_t task_finished_sub_; }; } diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 85ab07a33..5db148f91 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -32,11 +32,17 @@ #include "KDDockWidgets/src/qtwidgets/Window_p.h" #include "dialog/about/about.h" +#include "engineeventbridge.h" #include "mainmenu.h" #include "mainstatusbar.h" #include "KDDockWidgets/src/LayoutSaver.h" -#include "timeline/timelineundoworkarea.h" +#include "oakengine/timeline.h" +#include "common/configwrapper.h" +#include "oakengine/project.h" +#include "oakengine/viewer.h" +#include "oakengine/undo.h" +#include "widget/viewer/vieweroutpututils.h" namespace olive { @@ -47,6 +53,9 @@ MainWindow::MainWindow(QWidget *parent) parent) , project_(nullptr) { + bridge_ = new EngineEventBridge(this); + connect(bridge_, &EngineEventBridge::node_removed_from_graph, this, + &MainWindow::viewer_with_panel_removed_from_graph); // Resizes main window to desktop geometry on startup. Fixes the following issues: // * Qt on Windows has a bug that "de-maximizes" the window when widgets are added, resizing the // window beforehand works around that issue and we just set it to whatever size is available. @@ -71,8 +80,9 @@ MainWindow::MainWindow(QWidget *parent) load_custom_shortcuts(); // Create and set status bar + event_bridge_ = new EngineEventBridge(this); MainStatusBar *status_bar = new MainStatusBar(this); - status_bar->connect_task_manager(TaskManager::instance()); + status_bar->connect_task_manager(event_bridge_); connect(status_bar, &MainStatusBar::double_clicked, this, &MainWindow::status_bar_double_clicked); setStatusBar(status_bar); @@ -151,21 +161,21 @@ MainWindow::~MainWindow() #endif } -void MainWindow::load_layout(const MainWindowLayoutInfo &info) +void MainWindow::load_layout(const SerializedLayoutInfo &info) { - foreach (Folder *folder, info.open_folders()) { + foreach (Folder *folder, info.open_folders) { open_folder(folder, true); } - foreach (Sequence *sequence, info.open_sequences()) { - open_sequence(sequence, info.open_sequences().size() == 1); + foreach (Sequence *sequence, info.open_sequences) { + open_sequence(sequence, info.open_sequences.size() == 1); } - foreach (ViewerOutput *viewer, info.open_viewers()) { + foreach (ViewerOutput *viewer, info.open_viewers) { open_node_in_viewer(viewer); } - for (auto it = info.panel_data().cbegin(); it != info.panel_data().cend(); + for (auto it = info.panel_data.cbegin(); it != info.panel_data.cend(); it++) { // Find panel with this ID if (PanelWidget *panel = @@ -174,7 +184,7 @@ void MainWindow::load_layout(const MainWindowLayoutInfo &info) } } - KDDockWidgets::LayoutSaver().restoreLayout(qUncompress(info.state())); + KDDockWidgets::LayoutSaver().restoreLayout(qUncompress(info.state)); } QString transform_name_for_serialization(const QString &unique, int i) @@ -184,46 +194,47 @@ QString transform_name_for_serialization(const QString &unique, int i) } void correct_panel_data_if_necessary(const QString &unique_name, int index, - MainWindowLayoutInfo &info, QByteArray &layout) + SerializedLayoutInfo &info, QByteArray &layout) { QString corrected = transform_name_for_serialization(unique_name, index); if (corrected != unique_name) { - info.move_panel_data(unique_name, corrected); + info.panel_data[corrected] = info.panel_data[unique_name]; + info.panel_data.erase(unique_name); layout.replace(unique_name.toUtf8(), corrected.toUtf8()); } } -MainWindowLayoutInfo MainWindow::save_layout() const +SerializedLayoutInfo MainWindow::save_layout() const { - MainWindowLayoutInfo info; + SerializedLayoutInfo info; QByteArray layout = premaximized_state_.isEmpty() ? KDDockWidgets::LayoutSaver().serializeLayout() : premaximized_state_; foreach (PanelWidget *panel, PanelManager::instance()->panels()) { - info.set_panel_data(panel->uniqueName(), panel->save_data()); + info.panel_data[panel->uniqueName()] = panel->save_data(); } for (int i = 0; i < folder_panels_.size(); i++) { auto panel = folder_panels_.at(i); - info.add_folder(panel->get_root()); + info.open_folders.push_back(panel->get_root()); correct_panel_data_if_necessary(panel->uniqueName(), i, info, layout); } for (int i = 0; i < timeline_panels_.size(); i++) { auto panel = timeline_panels_.at(i); - info.add_sequence(panel->get_sequence()); + info.open_sequences.push_back(panel->get_sequence()); correct_panel_data_if_necessary(panel->uniqueName(), i, info, layout); } for (int i = 0; i < viewer_panels_.size(); i++) { auto panel = viewer_panels_.at(i); - info.add_viewer(panel->get_connected_viewer()); + info.open_viewers.push_back(panel->get_connected_viewer()); correct_panel_data_if_necessary(panel->uniqueName(), i, info, layout); } - info.set_state(qCompress(layout)); + info.state = qCompress(layout); return info; } @@ -325,8 +336,10 @@ void MainWindow::open_node_in_viewer(ViewerOutput *node) connect(viewer, &ViewerPanel::close_requested, this, &MainWindow::viewer_close_requested); - connect(node, &ViewerOutput::removed_from_graph, this, - &MainWindow::viewer_with_panel_removed_from_graph); + auto sub = bridge_->subscribe( + reinterpret_cast(node), + OAKENGINE_EVENT_NODE_REMOVED_FROM_GRAPH); + removed_from_graph_subs_[node] = sub; } } @@ -538,13 +551,18 @@ void MainWindow::node_panel_group_opened_or_closed() param_panel_->set_contexts(p->get_contexts()); } -void MainWindow::timeline_panel_selection_changed(const QVector &blocks) +void MainWindow::timeline_panel_selection_changed(const QVector &blocks) { TimelinePanel *panel = static_cast(sender()); if (PanelManager::instance()->currently_focused(false) == panel) { update_node_panel_context_from_timeline_panel(panel); - sequence_viewer_panel_->set_timeline_selected_blocks(blocks); + QVector native_blocks; + native_blocks.reserve(blocks.size()); + for (OakEngineBlock *b : blocks) { + native_blocks.append(reinterpret_cast(b)); + } + sequence_viewer_panel_->set_timeline_selected_blocks(native_blocks); } } @@ -556,32 +574,46 @@ void MainWindow::show_welcome_dialog() } } -void MainWindow::reveal_viewer_in_project(ViewerOutput *r) +void MainWindow::reveal_viewer_in_project(OakEngineNode *r) { // Rather than just using the resident ProjectPanel, find the most recently focused one since // that's probably the one people will want auto panels = PanelManager::instance()->get_panels_of_type(); + ViewerOutput *viewer = reinterpret_cast(r); foreach (ProjectPanel *p, panels) { - if (p->select_item(r)) { + if (p->select_item(viewer)) { break; } } } -void MainWindow::reveal_viewer_in_footage_viewer(ViewerOutput *r, +void MainWindow::reveal_viewer_in_footage_viewer(OakEngineNode *r, const TimeRange &range) { - footage_viewer_panel_->connect_viewer_node(r); + ViewerOutput *viewer = reinterpret_cast(r); - auto command = new MultiUndoCommand(); - if (!r->get_work_area()->enabled()) { - command->add_child(new WorkareaSetEnabledCommand( - r->project(), r->get_work_area(), true)); + footage_viewer_panel_->connect_viewer_node(viewer); + + auto command = oakengine_undo_command_create_multi(); + OakEngineWorkarea *wa = reinterpret_cast(viewer->get_work_area()); + if (!viewer->get_work_area()->enabled()) { + oakengine_workarea_set_enabled_undoable(wa, 1, command); } - command->add_child(new WorkareaSetRangeCommand(r->get_work_area(), range)); - Core::instance()->undo_stack()->push(command, tr("Set Footage Workarea")); + { + int64_t old_in_num, old_in_den, old_out_num, old_out_den; + int old_enabled; + oakengine_workarea_get(wa, &old_in_num, &old_in_den, + &old_out_num, &old_out_den, &old_enabled); + oakengine_workarea_set_range_undoable(wa, + range.in().numerator(), range.in().denominator(), + range.out().numerator(), range.out().denominator(), + old_in_num, old_in_den, old_out_num, old_out_den, command); + } + oakengine_undo_push(command, tr("Set Footage Workarea").toUtf8().constData()); - r->set_playhead(range.in()); + oakengine_viewer_set_playhead( + r, + range.in().numerator(), range.in().denominator()); } #ifdef Q_OS_LINUX @@ -599,11 +631,15 @@ void MainWindow::show_nouveau_warning() void MainWindow::update_title() { if (Core::instance()->get_active_project()) { + char name_buf[256]; + oakengine_project_pretty_filename( + reinterpret_cast( + Core::instance()->get_active_project()), + name_buf, sizeof(name_buf)); setWindowTitle( QStringLiteral("%1 %2 - [*]%3") .arg(QApplication::applicationName(), - QApplication::applicationVersion(), - Core::instance()->get_active_project()->pretty_filename())); + QApplication::applicationVersion(), name_buf)); } else { setWindowTitle( QStringLiteral("%1 %2").arg(QApplication::applicationName(), @@ -630,9 +666,9 @@ void MainWindow::viewer_close_requested() panel->deleteLater(); } -void MainWindow::viewer_with_panel_removed_from_graph() +void MainWindow::viewer_with_panel_removed_from_graph(OakEngineNode *source) { - ViewerOutput *vo = static_cast(sender()); + ViewerOutput *vo = reinterpret_cast(source); ViewerPanel *panel = nullptr; foreach (ViewerPanel *p, viewer_panels_) { @@ -645,8 +681,11 @@ void MainWindow::viewer_with_panel_removed_from_graph() if (panel) { remove_panel_internal(viewer_panels_, panel); panel->deleteLater(); - disconnect(vo, &ViewerOutput::removed_from_graph, this, - &MainWindow::viewer_with_panel_removed_from_graph); + auto it = removed_from_graph_subs_.find(vo); + if (it != removed_from_graph_subs_.end()) { + bridge_->unsubscribe(it.value()); + removed_from_graph_subs_.erase(it); + } } } @@ -815,7 +854,7 @@ void MainWindow::save_custom_shortcuts() void MainWindow::update_audio_monitor_params(ViewerOutput *viewer) { if (!audio_monitor_panel_->is_playing()) { - audio_monitor_panel_->set_params(viewer ? viewer->get_audio_params() : + audio_monitor_panel_->set_params(viewer ? viewer_output_audio_params(viewer) : AudioParams()); } } diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index 295be4bb5..9d7d8bcb0 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -25,7 +25,7 @@ #include #include -#include "node/project/serializer/mainwindowlayoutinfo.h" +#include "node/project/serializer/serializedlayoutinfo.h" #include "node/project.h" #include "panel/multicam/multicampanel.h" #include "panel/panelmanager.h" @@ -43,6 +43,7 @@ #include "panel/footageviewer/footageviewer.h" #include "panel/sequenceviewer/sequenceviewer.h" #include "panel/pixelsampler/pixelsamplerpanel.h" +#include "engineeventbridge.h" #ifdef Q_OS_WINDOWS #include @@ -51,6 +52,8 @@ namespace olive { +class EngineEventBridge; + /** * @brief Olive's main window responsible for docking widgets and the main menu bar. */ @@ -61,9 +64,9 @@ public: virtual ~MainWindow() override; - void load_layout(const MainWindowLayoutInfo &info); + void load_layout(const SerializedLayoutInfo &info); - MainWindowLayoutInfo save_layout() const; + SerializedLayoutInfo save_layout() const; TimelinePanel *open_sequence(Sequence *sequence, bool enable_focus = true); @@ -94,9 +97,13 @@ public: void select_footage(const QVector &e); -public slots: +public: + // Not a slot: signature uses the engine C++ type Project*, which must not + // be exposed to MOC (it would pull Project::staticMetaObject across the ABI + // boundary). It is only ever called directly, never via connect(). void set_project(Project *p); +public slots: void set_fullscreen(bool fullscreen); void toggle_maximized_panel(); @@ -157,6 +164,7 @@ private: QList timeline_panels_; AudioMonitorPanel *audio_monitor_panel_; TaskManagerPanel *task_man_panel_; + EngineEventBridge *event_bridge_; PixelSamplerPanel *pixel_sampler_panel_; ScopePanel *scope_panel_; QList viewer_panels_; @@ -182,7 +190,7 @@ private slots: void viewer_close_requested(); - void viewer_with_panel_removed_from_graph(); + void viewer_with_panel_removed_from_graph(OakEngineNode *source); void folder_panel_close_requested(); @@ -194,12 +202,16 @@ private slots: void show_nouveau_warning(); #endif - void timeline_panel_selection_changed(const QVector &blocks); - void show_welcome_dialog(); - void reveal_viewer_in_project(ViewerOutput *r); - void reveal_viewer_in_footage_viewer(ViewerOutput *r, const TimeRange &range); + void reveal_viewer_in_project(OakEngineNode *r); + void reveal_viewer_in_footage_viewer(OakEngineNode *r, const TimeRange &range); + +private: + void timeline_panel_selection_changed(const QVector &blocks); + + EngineEventBridge *bridge_ = nullptr; + QHash removed_from_graph_subs_; }; } diff --git a/app/window/mainwindow/mainwindowundo.cpp b/app/window/mainwindow/mainwindowundo.cpp index c3b7dc2db..93cc77042 100644 --- a/app/window/mainwindow/mainwindowundo.cpp +++ b/app/window/mainwindow/mainwindowundo.cpp @@ -24,27 +24,63 @@ #include "core.h" #include "window/mainwindow/mainwindow.h" +#include "oakengine/undo.h" namespace olive { -void OpenSequenceCommand::redo() +namespace { + +struct OpenCloseSequenceData { + Sequence *sequence; +}; + +void open_sequence_redo(void *userdata) { - Core::instance()->main_window()->open_sequence(sequence_); + auto *d = static_cast(userdata); + Core::instance()->main_window()->open_sequence(d->sequence); } -void OpenSequenceCommand::undo() +void open_sequence_undo(void *userdata) { - Core::instance()->main_window()->close_sequence(sequence_); + auto *d = static_cast(userdata); + Core::instance()->main_window()->close_sequence(d->sequence); } -void CloseSequenceCommand::redo() +void close_sequence_redo(void *userdata) { - Core::instance()->main_window()->close_sequence(sequence_); + auto *d = static_cast(userdata); + Core::instance()->main_window()->close_sequence(d->sequence); } -void CloseSequenceCommand::undo() +void close_sequence_undo(void *userdata) { - Core::instance()->main_window()->open_sequence(sequence_); + auto *d = static_cast(userdata); + Core::instance()->main_window()->open_sequence(d->sequence); +} + +void open_close_sequence_free(void *userdata) +{ + delete static_cast(userdata); +} + +} // anonymous namespace + +void *make_open_sequence_command(Sequence *sequence) +{ + auto *d = new OpenCloseSequenceData; + d->sequence = sequence; + return oakengine_undo_command_create(nullptr, open_sequence_redo, + open_sequence_undo, + open_close_sequence_free, d); +} + +void *make_close_sequence_command(Sequence *sequence) +{ + auto *d = new OpenCloseSequenceData; + d->sequence = sequence; + return oakengine_undo_command_create(nullptr, close_sequence_redo, + close_sequence_undo, + open_close_sequence_free, d); } } diff --git a/app/window/mainwindow/mainwindowundo.h b/app/window/mainwindow/mainwindowundo.h index 8daca4887..4f5926f33 100644 --- a/app/window/mainwindow/mainwindowundo.h +++ b/app/window/mainwindow/mainwindowundo.h @@ -23,51 +23,14 @@ #define OAK_MAINWINDOWUNDO_H #include "node/project/sequence/sequence.h" +#include "oakengine/undo.h" namespace olive { -class OpenSequenceCommand : public UndoCommand { -public: - OpenSequenceCommand(Sequence *sequence) - : sequence_(sequence) - { - } +void *make_open_sequence_command(Sequence *sequence); - virtual Project *get_relevant_project() const override - { - return nullptr; - } - -protected: - virtual void redo() override; - - virtual void undo() override; - -private: - Sequence *sequence_; -}; - -class CloseSequenceCommand : public UndoCommand { -public: - CloseSequenceCommand(Sequence *sequence) - : sequence_(sequence) - { - } - - virtual Project *get_relevant_project() const override - { - return nullptr; - } - -protected: - virtual void redo() override; - - virtual void undo() override; - -private: - Sequence *sequence_; -}; +void *make_close_sequence_command(Sequence *sequence); }