Compare commits

...
188 Commits
Author SHA1 Message Date
Mike-Solar 4edea377bf ci: cd into the workspace in every Windows msys2 step
CI / Build & test (Windows) (push) Failing after 29s
CI / Build & test (Linux) (push) Failing after 17m30s
The Gitea runner's msys2 shell does not start in the checkout
directory, so "bash ./tooling/install-deps.sh" reported the file
missing even though the checkout succeeded (the Linux steps were fine —
only the custom msys2 shell has the wrong CWD). Every Windows step now
begins with cd "$GITHUB_WORKSPACE" to make all relative file
references resolve.
2026-08-22 20:40:14 +08:00
Mike-Solar 0e68806351 gitattributes: keep workflow manifests LF on Windows checkouts
CI / Build & test (Windows) (push) Failing after 27s
CI / Build & test (Linux) (push) Canceled after 6m11s
A CRLF ci.yml turns every step command into "cmd\r", so bash fails to
find the referenced scripts on the Windows runner ("bash
./tooling/install-deps.sh: No such file or directory" — the file is
there, the command carries a trailing CR). Same class as the golden
JSON/OVE fix: pin *.yml/*.yaml to eol=lf.
2026-08-22 20:30:20 +08:00
Mike-Solar a8836275dc ci: fix Windows CI
CI / Build & test (Windows) (push) Failing after 30s
CI / Build & test (Linux) (push) Canceled after 3m31s
2026-08-22 20:23:45 +08:00
Mike-Solar a5208b6cae test: replace wall-clock loop deadlines with progress criteria
CI / Build & test (Windows) (push) Failing after 30s
CI / Build & test (Linux) (push) Canceled after 8m31s
Three real-engine throughput loops (thumbnail pipeline, full-res fill
behind the proxy, playback window supply) failed on machine slowness:
their pass/fail was a wall-clock Instant deadline, so a loaded machine
broke them for speed, not for a broken pipeline. Each loop now counts
engine pumps — machine-speed independent — and asserts the condition
within a bounded number of pumps. The two single-frame worker channel
receives keep a generous 60 s recv_timeout (a one-shot bounded
operation, not a throughput loop).

oak-cli: the integration fixtures moved with the app crate during the
workspace restructure; point the fixture helpers at
../oak-app/tests instead of the (now empty) repo-root tests/.
2026-08-22 20:11:43 +08:00
Mike-Solar c4705cbb2c test: playback tracking asserts on playhead progress, not wall time
CI / Build & test (Windows) (push) Failing after 22s
CI / Build & test (Linux) (push) Failing after 18m10s
playback_display_tracks_the_playhead used a 30 s wall-clock deadline as
its pass/fail criterion, so a slow or loaded machine failed the test
for machine speed, not for a broken pipeline — spurious, unrelated to
any race. The loop now terminates on playback-clock progress (playhead
>= 120, ~5 s of playback; the transport advances independently of
render speed, so termination is guaranteed) and the only judgment is
the tracking invariant at that point. No Instant::now() remains.
Verified green on a heavily loaded machine in 84 s.
2026-08-22 19:40:18 +08:00
Mike-Solar 9b35a9c6d4 components: effect controls with full mouse+keyboard; app-move path fixes
CI / Build & test (Windows) (push) Failing after 27s
CI / Build & test (Linux) (push) Failing after 18m29s
oakui/component/controls.rs owns the effect/editor controls instead of
reaching into gpui_widgets:

- Slider: horizontal 1:1-cursor drag (the gpui_widgets slider only
  responded to vertical cursor movement, so horizontal drags did
  nothing), wheel, middle-click reset, arrow keys (Shift = 1/10 step,
  Home/End = range ends), and double-click numeric editing (app text
  input; commit on blur, Escape cancels). A gesture emits
  ValueChanged exactly once on drop — one undoable edit per drag, so
  per-mouse-move edits + frame invalidation can no longer freeze the
  UI thread.
- CheckBox: click / Space / Enter toggle, request-only contract
  (Toggled + set_state), theme colors, optional label.
- ComboBox: click opens a popup list, Up/Down navigate (open) or
  change the selection (closed), Enter commits/opens, Escape closes.
- SpinBox: wheel + Up/Down (Shift fine) + Home/End.

The params panel, timeline and dialogs import from the component
module. App-move fallout: i18n packs resolve from the repo-root
assets/i18n via CARGO_MANIFEST_DIR (crates/oak-app is not the repo
root anymore), and the render tests' worker-binary paths point at
../../target/debug/oak-worker.
2026-08-22 19:09:33 +08:00
Mike-Solar aee368cb78 ci: finish fix Windows CI
CI / Build & test (Windows) (push) Failing after 4s
CI / Build & test (Linux) (push) Failing after 19m38s
2026-08-22 18:06:34 +08:00
Mike-Solar 6b497ccb97 ci: remove probes
CI / Build & test (Windows) (push) Failing after 13s
2026-08-22 17:49:05 +08:00
Mike-Solar b8f9e755c4 ci: remove windows panic
CI / Build & test (Windows) (push) Failing after 15s
2026-08-22 17:43:00 +08:00
Mike-Solar 244d5e860f workspace: kebab-case crates, app under crates/oak-app, shared versions
CI / Build & test (Windows) (push) Failing after 7s
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00
Mike-Solar 7e2b3cb4b8 components: app-owned text input and menu; use-import style
oakui/component gains the two app-facing components:

text_input: the app's text field — the gpui_elements editing engine
(IME composition, caret, selection, undo) wrapped with the app theme's
colors (text via a text_color refinement on the wrapping div so the
engine's run layout picks it up; selection/caret/placeholder/IME-marked
directly) — and install_text_input_bindings(), which binds the
Backspace/Delete/arrow/Home/End/select-all editing keys into the app
keymap scoped to the EditableText context. The app never installed
them before, so every field accepted IME text but ignored its editing
keys; the bindings are now wired once at OakApp::new. All seven call
sites (ofx_params x2, effect_library, manager, dialogs x3) use the
component instead of gpui_elements directly.

menu: all app menu code consolidates here — the model types are
re-exported, the shared context-menu plumbing (ContextMenuHandle,
ContextMenuTriggered) and the shared segments (edit/clip-edit/in-out/
color-label/new, the viewer context menu, the dynamic language menu)
move in from src/menus, which is deleted; app.rs and every panel
import from the component. Inline fully-qualified crate paths in
non-use positions are replaced with use imports.
2026-08-22 16:44:52 +08:00
Mike-Solar e7483ba735 ci: add throw exception to debug Windows CI
CI / Build & test (Windows) (push) Failing after 9s
2026-08-22 14:44:03 +08:00
Mike-Solar e8d08c77f0 ci: add probe to debug Windows CI
CI / Build & test (Windows) (push) Failing after 27s
2026-08-22 14:35:00 +08:00
Mike-Solar 2721225a3f ci: debug Windows CI
CI / Build & test (Linux) (push) Canceled after 0s
CI / Build & test (Windows) (push) Canceled after 0s
2026-08-22 14:32:51 +08:00
Mike-Solar 2289bc70d8 ci: debug missing bash and missing limits.h
CI / Build & test (Windows) (push) Failing after 28s
CI / Build & test (Linux) (push) Failing after 7m5s
2026-08-22 14:09:00 +08:00
Mike-Solar b4c8a00ce7 ci: debug missing bash and missing cargo
CI / Build & test (Linux) (push) Canceled after 0s
CI / Build & test (Windows) (push) Canceled after 0s
2026-08-22 06:10:42 +08:00
Mike-Solar ca33f310bb ci: debug missing bash
CI / Build & test (Windows) (push) Failing after 46s
CI / Build & test (Linux) (push) Failing after 3m41s
2026-08-22 05:59:10 +08:00
Mike-Solar 69ab45a103 ci: configure GitHub environment vars and use bash instead of msys2
CI / Build & test (Windows) (push) Failing after 21s
CI / Build & test (Linux) (push) Failing after 3m58s
2026-08-22 05:42:36 +08:00
Mike-Solar cea28f8bbc ci: migrate to Gitea and AWS
CI / Build & test (Windows) (push) Failing after 27s
CI / Build & test (Linux) (push) Failing after 4m5s
2026-08-22 05:25:04 +08:00
Mike-Solar 4637a84c85 ci: split Linux and Windows into separate jobs; drop macOS
The single matrix job becomes two independent jobs on the oak Gitea
runners (oak-ubuntu-2404 / oak-windows-2025 — the user's labels, kept
as-is). Every runner.os conditional collapses into the owning job and
all macOS steps are removed. actionlint.yaml whitelists the two oak
runner labels (actionlint needs -config-file now that the config lives
under .gitea/ instead of the default .github/ path).
2026-08-22 01:00:00 +08:00
Mike-Solar 1564844bab gitea: move workflows to .gitea, disable caches; settle the keystroke flake
CI / Build & test (ubuntu-latest) (push) Failing after 12s
CI / Build & test (windows-latest) (push) Canceled after 0s
Gitea prep: .github becomes .gitea (the act runner looks there), and
every actions/cache + Swatinem/rust-cache step is commented out until
the self-hosted instance has a cache provisioned. The remaining
marketplace actions (checkout/upload-artifact are act-compatible;
msys2/setup-msys2, dtolnay/rust-toolchain and softprops/action-gh-release
need a runner test / replacement) are a follow-up.

tests: the two gpui keystroke tests that flaked on Windows CI (undo
pair, snapping toggle — each once, values identical to the pass state,
Global-route keys) now dispatch each key with a double park. The root
cause is not fully pinned: the loss happens inside gpui's synthetic
key dispatch on Windows (both tests hold every test lock; production
is unaffected). The CI retry-once remains the backstop. The earlier
idea of advancing the simulated clock to flush gpui's pending-input
timer is off the table: the mock engine's playback ticks with executor
time, so a clock advance moves the playhead out from under the
assertions (observed: playhead 14 vs expected 9).
2026-08-21 23:53:14 +08:00
Mike-Solar 08269a680d ci: use AWS instead of warpbuild 2026-08-21 23:26:11 +08:00
Mike-Solar 24a2ce80e1 ci: retry the Windows test suite once on failure
Two different gpui keystroke tests flaked on Windows CI with the same
signature: a synthetic keystroke occasionally never reaches the action
(secondary-z lost while secondary-shift-z delivered; then a plain 's'
lost). Both passed every other run — a gpui test-harness delivery
flake, not an oak regression. A single retry pass absorbs it; a real
regression fails both passes.
2026-08-21 21:33:41 +08:00
Mike-Solar 6946226a0a tests: park between the undo/redo keystrokes (Windows CI flake)
edit_shortcuts_dispatch_to_the_engine dispatched secondary-z and
secondary-shift-z before a single run_until_parked; on Windows CI the
first key's binding hit was intermittently lost (undo 0, redo 1 —
twice, identically). Park after each key like every other assertion in
this test, and assert both reached the engine (>= 1) instead of the
exact pair count.
2026-08-21 21:08:39 +08:00
Mike-Solar 40c8043a76 render: size the preview-window headroom by alive workers
preview_window_capacity used the *configured* worker count, so a
window opened while workers were still handshaking (or after a crash)
could claim every slot of the smaller live pool — the synchronous
render ticket then never gets a free slot, and since the slot-releasing
cleanup runs on the same UI thread that is blocked in TicketArena::wait,
playback deadlocks permanently. Intermittent on Linux CI (the
playback_display_tracks_the_playhead hang, caught by the new test
watchdog): depends on how many workers had handshaken when playback
started. Count only Alive workers (fall back to the configured count
while none are alive, keeping the existing unit test semantics).
2026-08-21 20:37:17 +08:00
Mike-Solar d48b04da5a ci+tests: oakstorage Windows URIs, Linux hang watchdog, cache-on-failure
oakstorage: the sqlite URI parse tests used /tmp/lib.db, which is not
absolute on Windows, so parse_target's is_absolute check rejected it.
Pick the absolute path per platform (C:/tmp/lib.db on Windows).

ci (Linux): wrap the test step in a 1500 s watchdog — a deadlocked
test prints nothing and never fails; on timeout the watchdog dumps
every test/worker process's thread stacks with gdb and then kills the
suite. (One such hang already ate a run; the previous green run needed
~4 min.)

ci+cd: Swatinem/rust-cache gains cache-on-failure everywhere, so a
red run still saves its compile cache (the actions/cache FFmpeg cache
already saves in its post phase regardless of outcome).
2026-08-21 18:48:38 +08:00
Mike-Solar 9aab5623bf gitattributes: keep LF in golden/test data files on Windows
The oakotio parity tests byte-compare serialized JSON against the
tests/data goldens; with only `* text=auto`, a Windows checkout hands
out CRLF and every round-trip assertion fails on the line endings
alone. Pin *.json and *.ove (XML) to eol=lf.
2026-08-21 17:49:11 +08:00
Mike-Solar 7323a6d2be tests: make the shortcuts-test mutex poison-tolerant
Same pattern as the language lock: one test panicking while holding
shortcuts_test_lock cascaded into 15 PoisonError failures on Windows
CI (the real failure — one lost undo keystroke dispatch — was
invisible under the fallout). Every acquisition now recovers the guard
instead of unwrapping, so a single flaky test fails alone.
2026-08-21 17:22:47 +08:00
Mike-Solar cada2f4c3a fix: CI fallout from the i18n refactor + Windows path assertions
examples/screenshot.rs: migrate to language_code()/set_language_code
(the Language enum is gone); only CI's example build caught it — local
--lib runs never compile examples.

oakcodec tests: build path expectations with Path::join instead of
'/'-joined literals — production uses platform-native separators, so
the derivation assertions failed on Windows ("dir\img007.jpg" vs
"dir/img007.jpg"). Behaviour unchanged; the tests were never reached
on Windows before (earlier failures aborted the run first).

oakui::ofx interact test: poll up to 5s for the destroy marker record
before asserting. The active-interact slot is process-global; a
concurrent viewer frame sync from another test's real engine can take
the interact out of the slot and be preempted between take and
destroy, so the plugin's destroy record occasionally lands a few
milliseconds after this test removed the marker env var (Linux CI:
"lifecycle actions missing: [...]" with everything but destroy
present).
2026-08-21 16:34:27 +08:00
Mike-Solar 4622a1af1e i18n: six new languages; packs become fully data-driven
New complete language packs (431 keys each, key-aligned with en-US):
French, German, Russian, Japanese, Spanish, Portuguese — contributed
via the translation subagent, reviewed for key parity and genuine
translations.

Runtime discovery replaces compiled-in packs: include_str! and the
Language enum are gone; every <code>.yaml found in the searched
directories registers a language, so adding one needs neither a
rebuild nor a code change. Each pack names itself via its
"language.name" key (the endonym), which the pickers display. The
menu bar language submenu is built from the discovered packs (menu ids
in the new LANG_ITEM_BASE range, dispatched before the registry), the
preferences combo enumerates them the same way, and switching goes
through set_language_code. The C++-style LangZh/LangEn registry
actions are removed (menu ids 303/304 stay reserved). pack_dirs gains
the system-install layout <exe>/../share/oak/i18n, and en.yaml is
renamed en-US.yaml so the discovered code matches the fallback code.

Test hardening (a Windows/Linux CI flake chain exposed by this work):
the two language-test mutexes are one shared poison-tolerant mutex;
every test that flips the process language restores en-US before
releasing it; tests that match menus by localized labels take the lock
and pin en-US first.
2026-08-21 15:35:10 +08:00
Mike-Solar 3efdee5a10 fix: share one language-test mutex; read the OFX marker via Win32 env
i18n: test_lock() and lang_test_lock() used to be TWO different static
mutexes, so the i18n tests and the ~30 app/actions/dialogs tests that
mutate the language global never excluded each other. Windows thread
scheduling exposed the race: tr_falls_back_to_english_then_the_key got
the English value because another test flipped the language mid-assert.
Both entry points now lock the same mutex.

oak_test_plugin.c: on Windows the plugin DLL has its own CRT
environment block, so getenv() never sees what the host's
std::env::set_var set via SetEnvironmentVariableW — the interact
lifecycle test's marker file stayed empty ("lifecycle actions
missing: []"). Read the marker path through GetEnvironmentVariableA
on _WIN32.
2026-08-21 13:36:19 +08:00
Mike-Solar d9dc16ec26 cd: bundle all runtime dylibs on Windows/macOS; version from workspace
Windows: tooling/package/bundle-dylibs-windows.sh collects the MSYS2
runtime DLLs (libstdc++, libgcc, OpenColorIO, ...) with ntldd -R,
iterated to a fixpoint over freshly copied DLLs; a packager resources
glob places them next to the executables in the NSIS installer.

macOS: tooling/package/bundle-dylibs-macos.sh copies every non-system
dylib otool reports into Contents/Frameworks, rewrites the install
names to @executable_path/../Frameworks to a fixpoint, and ad-hoc
re-signs every modified Mach-O (rewriting invalidates the seal).

The CD package version no longer comes from the git tag: the root
Cargo.toml gains [workspace.package] version = "0.5.0", the oak
package inherits it (version.workspace = true — which cargo-packager
also picks up), and the Linux container packaging parses that field.
2026-08-21 12:53:24 +08:00
Mike-Solar 498669509a docs: current bilingual build guide; retire the C++/CMake one
docs/build.md + docs/zh/build.md rewritten for the Rust workspace:
project-built FFmpeg 8.1 (.cargo/config.toml presets FFMPEG_DIR),
vendored static OCIO on Linux/macOS vs MSYS2 dynamic OCIO on Windows
(with the OCIO_INSTALL_DIR/OCIO_RS_LINK env), the Windows GNU toolchain
requirements (MSYS2 Rust, RUSTFLAGS=-C link-args=-lmsvcrt for the
mingw-w64 _assert forwarding, unset INCLUDE/LIB), Linux audio dev
packages and xvfb headless testing, container packaging, and a
troubleshooting section. The macOS-only guides gain a deprecation
pointer. Also correct two stale comments in tooling/install-deps.sh
(FFmpeg is built by tooling/ffmpeg/build-ffmpeg.sh, not by cargo).
2026-08-21 12:53:11 +08:00
Mike-Solar eacc47930a tests: assemble the OFX fixture bundle correctly on Windows
The fixture plugin binary was copied into Contents/Linux-x86-64 under
the extension-less name "plugin" on every non-macOS platform. On
Windows the host never loads it: LoadLibrary appends .dll to
extension-less module names, so the scan found the bundle but produced
no plugin — and because the (passing) draw-overlay test scans first,
the path dedupe then hid the failure from the lifecycle test, which
died with "interact variant instance: NotFound". Use Contents/Win64
and plugin.dll on Windows in both bundle assembly sites.
2026-08-21 12:53:11 +08:00
Mike-Solar e163702a6b fix: entry-point-less test binaries on Linux; gate timeformat test FFI
oakrender/build.rs: -Wl,-export_dynamic is the macOS spelling. Since
Rust 1.90 x86_64-unknown-linux-gnu links with rust-lld by default, and
lld parses the single-dash form as '-e xport_dynamic', every oakrender
integration test binary was linked with NO entry point and died with
SIGSEGV inside ld.so's dl_main (jumping to the image base) before
printing anything — the copier_test CI failure. Emit the flag on macOS
only.

oaknode timeformat: value_localtime_flag_routes_to_localtime_r called
localtime_r/gmtime_r directly, which do not exist on Windows. Factor
the cfg-gated FFI (localtime_r/gmtime_r vs _localtime64_s/_gmtime64_s)
into break_down_time() and use it from both value() and the test.
2026-08-21 11:56:24 +08:00
Mike-Solar 8660cbcf97 ci: unbreak Windows tests; sharpen Linux loader-crash forensics
oakcodec: gate find_ffmpeg_searches_path to unix (chmod 0755 + shebang
fixture) and make find_ffmpeg_missing_returns_empty assert absoluteness
instead of a '/' prefix so the tests compile and pass on Windows.

ci (Windows): export RUSTFLAGS=-C link-args=-lmsvcrt in the build and
test steps. mingw-w64 (Nov 2025) forwards _assert to __msvcrt_assert
inside libmingwex.a, and rustc's link order leaves -lmingwex last, so
binaries that pull _assert.o (oakcommon's real_ocio test) fail to link;
a trailing -lmsvcrt re-scans the CRT import lib afterwards.

ci (Linux): copier_test dies inside ld.so before printing anything.
Replace the LD_DEBUG probe with stronger forensics: exported dynsyms
(interposition suspects), strace tail, valgrind tail, and siginfo
(si_code/si_addr) from the gdb run.
2026-08-21 11:17:39 +08:00
Mike-Solar bea50f4d80 oakaudio: drop asio-sys from the lock (feature removal follow-up) 2026-08-21 10:37:22 +08:00
Mike-Solar e837277557 ci: fix Windows asio-sys link failure; improve Linux crash diagnostics
oakaudio: drop cpal's `asio` feature. asio-sys needs the proprietary
Steinberg ASIO SDK at link time (undefined ASIOGetSamplePosition etc.
on the GNU toolchain); WASAPI remains the Windows backend.

ci: the failure-only gdb step passed test args without --args, so gdb
treated --nocapture as a core file. Also collect loader-stage evidence
for the copier_test dl_main SIGSEGV: IRELATIVE reloc count, LD_DEBUG
tail, full backtrace and registers.
2026-08-21 10:29:54 +08:00
Mike-Solar b5a2ea7697 ci+fix: link the exported UCRT time symbols; gdb over copier_test too
- localtime_s/gmtime_s are MinGW header inlines, not symbols — link
  _localtime64_s/_gmtime64_s
- copier_test also segfaults only on the Linux runner; add it to the
  on-failure gdb backtrace
2026-08-21 09:51:22 +08:00
Mike-Solar 7fb9c92931 ci+fix: UCRT time functions on Windows; skip libsnappy on MinGW; NUL-terminated names in the mt test
- timeformat node: localtime_s/gmtime_s (reversed args, 64-bit time_t)
  on Windows — MinGW has no localtime_r/gmtime_r
- the multithread suite test passed non-NUL-terminated property names
  (str::as_ptr) to the C property suite — UB that resolved to garbage
  lookups on the CI runner
- libsnappy off on the MinGW FFmpeg build (only feeds the hap encoder;
  its pkg-config entry does not reach the static link)
2026-08-21 09:17:29 +08:00
Mike-Solar 032a3a559b ci+tests: skip libopenh264 on MinGW; fix UB varargs in the message-suite test
- FFmpeg for Windows no longer enables libopenh264 (redundant with the
  native h264 decoder + x264 encoder; its MinGW packaging does not
  satisfy the static link — Wels* undefined references)
- suites_test's question-type call passed a 3-placeholder format with
  one variadic arg — UB that vsnprintf turns into a SIGSEGV on glibc
  (masked on macOS); use a placeholder-free format
2026-08-21 08:41:54 +08:00
Mike-Solar cd95514cf8 ci: skip -ldl on MinGW links; widen the gdb backtrace to the plugin test binaries
- oakffmpeg-link forwards pkg-config --static --libs verbatim; FFmpeg's
  .pc files can list -ldl via external deps, and MinGW has no libdl
- suites_test segfaults on the Linux runner too; run both plugin test
  binaries under gdb on test failure
2026-08-21 08:09:11 +08:00
Mike-Solar 2794f8bb80 fix(plugin): guard plugin calls across host shutdown generations
Tests (and any host that shuts down then rescans) can hold instances of
a PREVIOUS plugin generation; their entry points dangle after dlclose,
so the next shutdown's destroyInstance notification jumped into
unmapped memory (SIGSEGV on Linux; masked on macOS). Plugin gains an
 flag set by unload_all before dlclose; call_action/call_entry
fail fast instead of calling into freed code.
2026-08-21 07:33:18 +08:00
Mike-Solar 74b18ea71a ci: gate ocio-sys' forced MSVC includes behind OCIO_RS_NO_MSVC_INCLUDES
The crate's build.rs unconditionally adds the MSVC + Windows SDK
include dirs on Windows (for MSVC hosts); on the GNU toolchain that
breaks the bridge compile with MSVC-only headers. The runner's job
hook re-exports INCLUDE/LIB per step, so the in-step unset did not
help — patch the extracted build.rs instead (both the env-var failure
modes are now documented in the step comment).
2026-08-21 07:08:31 +08:00
Mike-Solar ed7389ea95 ci: clear hook-injected MSVC env in-step; gdb backtrace for the Linux segfault
- the warp runner's job hook re-exports MSVC INCLUDE/LIB per step, so
  the GITHUB_ENV clear did not stick — unset in the Build/Test steps
  themselves
- node_e2e_test segfaults only on the Linux runner; rerun the binary
  under gdb on failure to capture the native stack
2026-08-21 06:38:11 +08:00
Mike-Solar f6e7bf20a4 cd: the package builder scripts (ignored by the *build-* glob)
Force-added: .gitignore's *build-* pattern matches the filenames.
2026-08-21 05:59:41 +08:00
Mike-Solar 32cd8f46f0 cd: container-native Linux packaging (deb/rpm/pacman) + AppImage
Each distro package builds inside that distro's container so declared
dependencies always resolve to native names: hand-rolled deb via
dpkg-shlibdeps + dpkg-deb, rpm via rpmbuild's auto-requires, Arch via
makepkg (non-root builder user). git/curl install before checkout
(container jobs). AppImage keeps cargo-packager on the Ubuntu runner.
The release gates on all four package jobs plus macOS/Windows.
2026-08-21 05:58:43 +08:00
Mike-Solar 551909df62 ci: clear MSVC INCLUDE/LIB for the GNU build; worker test helper fixes; hw tolerance
- the Windows runner image exports MSVC's INCLUDE/LIB; cc-rs was
  appending the MSVC SDK headers to MinGW compiles (vcruntime.h not
  found)
- oak-worker handshake test helper advertised the input pool's total
  byte size as per-slot data bytes (macOS tolerated the oversized
  attach; Linux correctly rejects it)
- hw/sw decode comparison tolerance 0.05 -> 0.08 (VideoToolbox's
  YUV->RGB legitimately differs by ~1 LSB of intermediate depth)
2026-08-21 05:52:30 +08:00
Mike-Solar 738120bffc ci: Windows uses the MSYS2 OpenColorIO (2.5.2, dynamic); test diagnostics
- the vendored OCIO source needs MSVC-only constructs (wide-path
  ifstream); MSYS2's mingw build of the exact 2.5.2 the bridge targets
  is the sane Windows path — DLLs get packaged next to the binaries
- oak-worker handshake test prints the error response on failure
  (CI-only attach failure needs the message)
2026-08-21 05:17:54 +08:00
Mike-Solar f6a7dfd0b3 feat(ui): curve editor for OFX parametric parameters
Each dimension of a parametric param renders as a CurveEditor in the
inspector (bezier handles map to the Hermite slopes of the host curve
model; edits serialize back through the JSON mirror — undoable and
project-persisted). The engine re-sync skips in-progress drags and
identical curves so the per-render sync neither steals gestures nor
loops. Also: physical-memory probe for the worker-count policy on
Windows (GlobalMemoryStatusEx).
2026-08-21 04:42:33 +08:00
Mike-Solar 13b1799c71 ci: resolve CARGO_HOME via cygpath for the ocio-sys patch; gate screenshot example to macOS
- rust-toolchain sets CARGO_HOME to the Windows userprofile path while
  the msys2 shell's HOME is elsewhere — the yaml-cpp patch targeted an
  empty directory and the assertion ls failed
- examples/screenshot.rs uses the macOS-only VisualTestAppContext; its
  items are now cfg-gated with a non-macOS stub main so workspace test
  builds pass on Linux/Windows
2026-08-21 04:25:43 +08:00
Mike-Solar a43302d9b7 feat(plugin,node): bridge parametric params into the node/inspector path
- ValueType::Parametric; the node input carries the whole curve set as
  NodeValue::Text(JSON) so undo and project serialization come for free
- translation pass builds the input with the default-curve JSON and the
  dimension/range/ui-colour properties
- edits flow both ways: node input (UI) -> curves_from_json ->
  set_ofx(Parametric) on the instance; plugin-side Set/Add/Delete ->
  notify_instance_changed -> JSON written back to the input (undoable)
- screenshot example: gate the macOS-only offscreen capture items so
  the workspace tests build on Linux/Windows
2026-08-21 04:20:14 +08:00
Mike-Solar 980c41acec feat(plugin,codec): Windows plugin loading and ffmpeg discovery
- oakplugin host: Win32 LoadLibraryExW/GetProcAddress/FreeLibrary
  backend (LOAD_WITH_ALTERED_SEARCH_PATH so bundle-sibling DLLs
  resolve), same dl_open/dl_sym/dlclose surface — the POSIX path is
  untouched; OFX hosts now compile on Windows
- proxymanager: PATH split via std::env::split_paths (Windows ';'),
  ffmpeg.exe name, Windows candidate locations; split logic unit
  tested
2026-08-21 04:06:53 +08:00
Mike-Solar 4f3c140b28 feat(render): Windows shared memory (CreateFileMapping/MapViewOfFile)
SharedMemoryRegion gains a Win32 backend behind the unchanged public
API: Local\OakShm<key> names, OpenFileMapping for attach, VirtualQuery
for the size check, UnmapViewOfFile/CloseHandle for teardown. Semantic
differences from POSIX are documented: unlink_key is a no-op (the
kernel destroys the object with the last handle, so crashed owners
self-heal) and Create on a live name fails instead of replacing.
Windows CI builds the workspace again.
2026-08-21 03:47:09 +08:00
Mike-Solar e8aac4a30d ci: create the registry src dir before patching ocio-sys
On a fresh runner registry/src has no hash subdir yet, so the unpack
glob never expanded and the step exited 2; derive it from the cache
dir and assert the patched file exists at the end.
2026-08-21 03:34:45 +08:00
Mike-Solar c9d557e127 feat(plugin): OfxParametricParameterSuite v1
Parametric (curve/LUT) parameters: ParamValue::Parametric holds one
ordered control-point curve per dimension (identity default over the
declared range), evaluated as piecewise cubic Hermite with auto
(centered-difference) slopes; the full suite — evaluate / count / get /
set / add / delete / delete-all — with the spec's error codes, descriptor
defaults copied to instances, and instanceChanged notifications on
edits. paramDefine accepts OfxParamTypeParametric; the dimension/range
and UI-colour properties round-trip. 148/148 real plugins discovered,
135 registered (one more than before: the parametric-suite consumer).
2026-08-21 03:03:50 +08:00
Mike-Solar 09cfd9f09e fix(ui): don't bump the display-color generation on the first build
The first processor build has no prior state to be stale against; the
spurious bump made the next cpu_frame call drop the freshly cached
image (playback_display_tracks_the_playhead regression).
2026-08-21 02:47:39 +08:00
Mike-Solar 2d4342ff48 ci: unpack ocio-sys before patching yaml-cpp; fix oakaudio test compile
- cargo fetch does not extract sources; the yaml-cpp <cstdint> patch
  now untars the .crate into the registry src dir first (the glob
  found nothing and the step failed with exit 2)
- oakaudio: the watchdog-wrapped audio test called Self::... from a
  free-function test module (compile error in lib test)
2026-08-21 02:40:44 +08:00
Mike-Solar d5b8cd99d6 chore: bump gpui (macOS layer colorspace coordination) 2026-08-21 02:17:19 +08:00
Mike-Solar eff845a5cd ci: linux xkbcommon-x11, yaml-cpp cstdint patch, hwaccel test skips on VT-less hosts
- Linux: libxkbcommon-x11-dev for the gpui X11 client link
- Windows: patch <cstdint> into the vendored yaml-cpp (a cached cmake
  configure ignores CXXFLAGS; the patch is idempotent and runs after
  cargo fetch)
- macOS: the hw-decode test skips its VideoToolbox engagement
  assertions on hosts where VT cannot initialize (headless/virtualized
  runners) instead of failing
- display color management: the display ICC (system or custom) is
  applied to viewer frames at present time (F32 in place, or in place
  on the BGRA staging copy with the R/B swizzle baked into the OCIO
  chain); preferences get a Color section (mode + custom ICC file); on
  macOS the Metal layer is tagged with the display colorspace when
  self-managing so ColorSync passes pixels through (no double
  correction); frame caches track the transform generation so a mode
  or profile change drops stale pixels
2026-08-21 02:16:39 +08:00
Mike-Solar fa7d8153bd cd: declare the full shlib dependency set on the deb
dpkg-shlibdeps over the three shipped binaries resolves every NEEDED
library to exact build-distro package names (FFmpeg/OCIO are static so
only base-OS packages appear) and rewrites the deb's Depends. Distros
with divergent package names (openKylin) get their own build instead
of a wrong-name dependency list.
2026-08-21 00:51:28 +08:00
Mike-Solar e1871494ae cd: audit the release binaries' NEEDED list on Linux
Static FFmpeg + static OCIO leave only base-OS libraries; the audit
step prints objdump NEEDED for each packaged binary so any accidental
dynamic dependency (and any distro-specific package-name surface) is
visible in the build log.
2026-08-21 00:46:40 +08:00
Mike-Solar 6b91748a5a ci: static OCIO link, full audio dev set on Linux, yaml-cpp cstdint fix
- OCIO_RS_LINK=static everywhere: the vendored OCIO is linked into the
  binaries statically — the package carries no OCIO dependency
- Linux: libasound2-dev (alsa-sys), libpulse-dev, libsndfile1-dev —
  the full audio dev set
- Windows: -include cstdint for the vendored yaml-cpp (pre-GCC-13
  transitive includes)
2026-08-21 00:44:35 +08:00
Mike-Solar 05fda5bcfc ci: libjack-jackd2-dev on Linux (cpal's JACK backend build dep) 2026-08-21 00:20:14 +08:00
Mike-Solar a1e4f47300 ci: GNU-target Rust on Windows, pipewire dev packages on Linux
- Windows: install MSYS2's own Rust (x86_64-pc-windows-gnu host); the
  rustup MSVC toolchain is not on the msys2 shell's PATH and the MSVC
  linker rejects the Unix-style link args the build scripts emit
- Linux: libpipewire-0.3-dev + libspa-0.2-dev for libspa-sys (gpui's
  Linux screen-capture/audio stack)
2026-08-21 00:12:55 +08:00
Mike-Solar 3c31c67f99 ci: build OpenColorIO from the ocio-sys vendored source on every platform
The distro OCIO is too old for the bridge's API floor where it matters
(Ubuntu 24.04 ships 2.1; the bridge uses 2.4+ APIs), and version drift
across platforms is a support hazard — enable ocio-rs' bundled feature
and drop the OCIO_INSTALL_DIR/system-package wiring from CI and CD so
Linux, macOS and Windows all build the same vendored OCIO. cmake/make/
diffutils added where the runners lack them (Windows FFmpeg build needs
make + cmp).
2026-08-21 00:01:14 +08:00
Mike-Solar 9c77d27b7f ci: fix dependency install on macOS and Windows runners
- Homebrew renamed libtheora->theora and libwebp->webp; the old names
  no longer resolve, failing the macOS dependency step
- retry the MSYS2 pacman install (3 attempts, --needed resumes): CI
  mirrors stall mid-download ("Operation too slow") often enough to
  matter
2026-08-20 23:53:50 +08:00
Mike-Solar 9003b78176 feat: built-in effect params, clip click-select, effect drag-and-drop, project load with plugins
- serializer resolves node types through the factory's dynamic
  (runtime-registered OpenFX) entries, so a project carrying plugin
  nodes loads again (was: "unknown node type"); covered by a new
  CI-gated round-trip test driving the real fixture plugin
- built-in effect nodes expose their inputs as inspector parameters
  like the C++ parameter editor: localized input names from the
  behavior, combo option tables via the new
  NodeBehavior::input_combo_strings (16 nodes, string-for-string from
  the C++ set_combo_box_strings), connection/data inputs excluded
- effect library: live drag-and-drop — onto the inspector's effect
  stack (lands at the indicator position) and onto the node editor
  canvas (creates the node at the drop point); double-click still
  appends to the selected clip
- inspector parameter controls are no longer recreated per render
  (gpui stack view caches them per effect), so sliders drag and
  checkboxes click; the view observes the engine and silently re-syncs
  values (undo/redo land on the widgets)
- timeline: left-press selects clips (plain/keep-multi/Ctrl-Cmd
  toggle); clip moves clamp the shared delta so no clip of a linked
  group lands before frame 0 instead of failing with "invalid move
  target"
- oakplugin: createInstance-rejected instances skip the destroyInstance
  notification (the plugin never owned them); vendor-suite fetchSuite
  misses moved behind OAK_OFX_TRACE; the worker logs the discovered/
  registered plugin counts
- CI: the OFX probe step also runs the serialization round-trip test
- gpui submodule: params view caching, clip click-select, library
  drag payload, graph_position_at
2026-08-20 23:46:40 +08:00
Mike-Solar 0a7f3766e2 ci: probe OFX plugin discovery with a real fixture plugin; cd: restore Windows NSIS job
- new minimal C OFX plugin fixture (ci_test_plugin.c) compiled into a
  real .ofx.bundle by build_fixture.sh; the CI step points
  OFX_PLUGIN_PATH at it and asserts the scan_probe example discovers
  AND registers it (Linux/macOS)
- host bundle binary search now also covers the OFX-standard Win64
  platform directory
- cd.yml: restore the Windows NSIS packaging job (obsolete oakengine
  cdylib prebuild dropped) and repair the job indentation that had
  silently detached the macos/release jobs from the jobs: map;
  releases now gate on all three platforms
2026-08-20 22:37:22 +08:00
Mike-Solar 12ffce77db feat(common): persist configuration as TOML, migrate legacy config.ini
- save() writes <config>/config.toml (atomic temp+rename), flat keys
  at the top level and group/sub keys as [group] tables, values as
  native TOML int/float/bool/string (non-finite doubles degrade to
  strings and restore via the declared type)
- load() prefers config.toml; a legacy config.ini (C++ or pre-TOML
  Rust builds) is read once and immediately re-persisted as TOML; the
  INI file is left in place; a corrupt TOML is reported, never
  silently discarded
- cd.yml: drop a stale oakengine comment (the crate is retired)
2026-08-20 22:28:58 +08:00
Mike-Solar c5455c7521 fix(ui): track growth direction, proxy status, effect library search
- NLE track growth is now a display concern: video/subtitle track
  lists render reversed (a new track lands on top), audio lists render
  in order (a new track lands at the bottom); the graph list always
  appends. Track-add undo removes THIS track by id instead of blindly
  removing the last one
- add_track returns the actual index of the new track (diffed against
  the pre-command list) instead of assuming append-at-end
- status bar proxy segment reflects the real Use Proxy Media switch
  instead of a static "Proxy: Off"
- proxy transcode PROGRESS events no longer invalidate the rendered
  frame cache on every tick (only completion does) — progress updates
  used to keep the playback cache permanently cold while generating
- effect library: live search box (name/type-id substring), Built-in
  group header, and the addable-effects table is sorted alphabetically
  (built-ins first, then OFX sub-category groups)
2026-08-20 22:28:43 +08:00
Mike-Solar 5498504398 fix(plugin): full OFX plugin discovery — host conformance fixes
Real openfx-misc/CImg/Shadertoy bundles (148 plugins at
/Library/OFX/Plugins) all failed to load before; every failure was
silent. Root causes found one by one with a probe example + lldb:

- property suite rejected propSet on undefined properties and
  propGetDimension on empty ones, and disallowed the index==size
  append — OFX semantics are create-on-set and appendable dimensions
  (this alone failed every plugin's describe)
- host property set missed the mandatory OfxPropType/OfxPropAPIVersion
  and the capability props ofxs' fetchHostDescription reads with
  throwOnFailure=true (IsBackground, TemporalClipAccess, MaxPages,
  PageRowColumnCount, host SupportedContexts, ...) — one missing prop
  aborted the read chain and left a half-initialised host description,
  which made every temporal plugin refuse to load
- MultiThreadSuiteV1 lacked the five mutex functions (the plugin reads
  past the short table — UB); implemented as a real counting-semaphore
  registry
- the OfxHost struct was a stack local; ofxs keeps the POINTER past
  setHost, so describe/render-time fetchSuite calls dereferenced a
  dangling stack address (bus error once plugins actually loaded) —
  the struct is now a leaked process global
- General is a standard OFX context and is no longer filtered out
  (Roto/AppendClip/STMap declare only it)
- every scan/load/describe early-out now logs its reason; suite entry
  points report non-OK statuses with caller location under
  OAK_OFX_TRACE
- examples/scan_probe.rs: scans the real plugin dirs and prints
  discovered/registered counts (also usable from CI)

Result: 148/148 plugins discovered, 134 registered as node types (the
remaining 14 need vendor suites — Vegas stereoscopic etc. — and are
logged, not silent)
2026-08-20 22:28:23 +08:00
Mike-Solar bf0416c50b feat(i18n): externalize UI strings to YAML language packs
- string tables live in assets/i18n/<lang>.yaml, loaded at runtime
  (user pack dir ~/.oak/i18n, app bundle Resources/i18n, dev checkout)
  with the compiled-in English/Chinese tables as fallback
- new status.proxy.on/off keys; inspector.params copy no longer says
  "placeholder"; effect_library.group.builtin key
- bundle the packs as cargo-bundle resources; dev profile dep opt-level
  dropped to 1 for faster iteration builds
2026-08-20 22:27:57 +08:00
Mike-Solar 73bb686940 fix(app): thumbnail PNG encoding, effect-card collapse echo, linked A/V drag
- Thumbnails never appeared because the PNG was written to a .part
  file with format inferred from the extension (always failing); the
  writer now uses an explicit PNG encoder, and an e2e test proves the
  pipeline yields real files.
- Mock engine: the CardSelected -> SelectionChanged echo no longer
  re-expands a card the same click just collapsed.
- Dragging a clip moves its linked audio/video partners by the same
  frame offset in a single undo entry; the dragged clip may change
  tracks while partners keep theirs.
2026-08-20 18:56:01 +08:00
Mike-Solar cc0a15919f style(app): apply the refreshed theme, drop-ghost timeline shell, misc
- Root view paints the near-black base so dock gaps match.
- Waveform trace follows the deep-green design color.
- actions.rs: reset the shortcut overrides in
  save_writes_only_entries_that_differ_from_default (pre-existing
  flake: a leaked override from a previous test poisoned the shared
  lock under parallel test order).
2026-08-20 17:43:44 +08:00
Mike-Solar 02cea7e3ee feat(app): node editor follows the selected clip, two-way selection sync
- With a clip selected, the node editor shows that clip's context
  chain (footage -> effects -> clip) instead of the global graph; the
  clip's node is highlighted. No selection keeps the full graph.
- Node clicks in the graph select the node and expand/highlight the
  matching effect card in the inspector; clicking an inspector card
  highlights the node in the graph (single source of truth: the
  engine's selected_graph_node).
2026-08-20 17:08:26 +08:00
Mike-Solar d7cbeba850 feat(app): window menu checkmarks with panel toggle, OFX standard search paths
- The Window menu lists every panel, checks the open ones, and toggles
  visibility on click (closed panels reopen at their last dock target,
  falling back to the default group) — a panel closed by accident
  (e.g. the inspector) is one menu click away again. The menu refreshes
  on dock structure events.
- OFX plugin scanning now covers the full standard location set:
  per-user (~/.OFX/Plugins, ~/.local/share, ~/Library/OFX/Plugins on
  macOS), system-level (/Library/OFX/Plugins, /usr/OFX/Plugins,
  /usr/local, %ProgramFiles%\Common Files\OFX\Plugins), app-relative,
  and the OFX_PLUGIN_PATH environment variable.
2026-08-20 16:20:49 +08:00
Mike-Solar 877f577564 fix(oaktimeline): placement sets the block's in point (drop-at-cursor fix)
TrackPlaceBlockCommand::redo now homes the block's in point to the
placement target (capturing the original for undo): the Rust block
stores its position on the block, so a fresh clip that never had its
in point set always rendered at the timeline zero — the 'drops always
land at zero' bug. The original in point is captured on the first redo
and restored on undo, keeping the sync re-place round-trip exact. The
A/V drop test now asserts the clip lands at the drop frame.
2026-08-20 15:33:43 +08:00
Mike-Solar 1f6ed30423 chore: bump gpui submodule (viewer interact events) 2026-08-20 15:11:29 +08:00
Mike-Solar 844c747681 feat(app): drop-at-cursor placement and a translucent drop ghost
- Footage drops no longer clamp to the sequence length — that clamp
  squashed every drop on an empty or short timeline to frame zero.
  Dropping past the end now extends the timeline, so the clip lands
  where it is released.
- While dragging, a translucent ghost (35% opacity, accent border)
  previews the resolved track, start frame and footage length at the
  cursor; a new AppEngine::footage_length_frames (probed duration x
  frame rate) feeds its extent, with a mock implementation for demo
  mode.
2026-08-20 15:10:50 +08:00
Mike-Solar 55bd1132cd feat(app): OFX Interact viewer integration - overlay drawing and event forwarding
- Main-process interact instances for the selected OFX effect card
  (create on selection change, describe, destroy on deselect/close),
  coexisting with the render-worker plugin instances per the OFX
  multi-instance model.
- Program viewer composites the interact's overlay: draw into a GL
  FBO via gl_bridge, read back, straight-alpha 'over' composite onto
  the displayed frame; cached and only re-rendered on frame/time/
  viewport/instance change or plugin redraw requests.
- Event forwarding: picture-area pointer maps through the contain-fit
  letterbox inverse to OFX pen coordinates (pen_motion/down/up);
  Keystroke to OFX key symbols (ASCII, navigation, F1-F35) for
  key_down/up; a 50ms idle pump; global shortcut consumption keeps
  precedence.
- e2e with the real test plugin: lifecycle marker assertions, pen/key
  event records, and macOS GL overlay compositing verified (265 tests
  green incl. gpui_widgets viewer suite).
2026-08-20 00:28:14 +08:00
Mike-Solar b4ceaa9cab feat(oakplugin): GL render bridge, color picker, push-button action, worker progress, Interact host
- gl_bridge: macOS CGL offscreen context (process-wide singleton,
  serialized GlGuard), real GL output textures/FBOs, glReadPixels
  readback with vertical flip and format conversion; use_opengl now
  really engages for OpenGLRenderSupported plugins (verified with real
  GL rendering: C smoke 11/11, unit tests, GL e2e).
- OfxColor: color params get a swatch button plus a real picker popup
  (RGBA sliders, live preview, hex input, undoable commit) replacing
  the four spinboxes.
- Push buttons route kOfxActionInstanceChanged (UserEdited) per the
  OFX contract; test plugin asserts the callback.
- Worker-side plugin progress flows to the main-process progress
  dialog over the NDJSON control channel, with cancel propagation.
- OFX Interact host: NewInteract/Describe lifecycle, Draw/Pen/Key/Idle
  action surface with proper in-args, DrawSuite v1 host implementation
  sharing the gl_bridge context; interact test plugin verifies the
  event stream and real GL drawing.
2026-08-19 22:14:54 +08:00
Mike-Solar 29696479b5 fix(app): context-menu actions, clip clipboard, A/V drop, add-track, default tracks, undo divergence
- Right-clicking an unselected clip selects it first (C++ parity) —
  this is what made Cut/Delete appear to do nothing.
- Cut/Copy/Paste clipboard: clipboard_copy/cut/paste on the engine,
  clipboard clips keep footage/range/speed/track kind and stay linked
  in the pasted group; paste lands at the playhead as one undo entry.
- Dropping a video-with-audio footage places the video clip plus a
  linked audio clip at the same range in ONE 'Add Clip' undo entry.
- Add Video/Audio Track buttons in the timeline toolbar and the track
  header context menu; new sequences start with 2 video + 2 audio
  tracks (not an undoable edit).
- oaknode Graph::add_entry now reclaims the slot from the free list —
  before, a detached-then-reattached node left its slot in the free
  list, so node_count undercounted and the next add_node silently
  clobbered the restored node. This was the user's 'undo, redo, undo,
  redo and the result changed' bug; regression covered by cycle tests
  (move/trim/delete/split/add-track/linked-placement all converge).
2026-08-19 17:42:21 +08:00
Mike-Solar dffa94127a feat(app): A/V drop creates linked clips, add-track affordances, default 2V+2A layout
- Dropping a video-with-audio footage now places a video clip AND a
  linked audio clip at the same range in ONE undoable 'Add Clip' entry
  (the links live on NodeCore.links, the canonical links_of storage;
  auto-creates the missing track kind).
- The timeline toolbar gains 'Add Video/Audio Track' buttons and the
  track-header context menu offers the same two entries above
  Delete/Delete All Empty.
- create_sequence now starts every new sequence with the default
  2 video + 2 audio track layout (driven directly through the add-track
  commands, not through the undo stack). Tests updated for the new
  default track counts.
2026-08-19 15:21:54 +08:00
Mike-Solar 35b9ad9541 feat(oakcodec): hardware video decoding by default on all platforms
FFmpeg 8 removed the standalone hardware decoders (h264_videotoolbox/
vaapi/nvdec/d3d11va no longer exist in its configure) — hardware decode
now only exists as a hwaccel attached to the software decoder. The new
oakcodec::hwdecode module therefore opens the regular decoder with the
platform's hardware device context attached (VideoToolbox on macOS,
VA-API then NVDEC on Linux, D3D11VA then NVDEC on Windows): FFmpeg
engages the matching hwaccel, decodes into hardware surfaces, and we
transfer them to system memory (NV12/P010) ahead of swscale.

- HardwareDecoding config switch, default ON by mandate; a checkbox in
  Preferences > Rendering (EN/ZH); device creation failure skips to the
  next candidate and finally to software; a decode-time failure on a
  hardware session reopens it as software and retries once.
- hw_decoder_name() observability hook plus a HW_TRANSFERS counter so
  tests can prove the hwaccel really engaged (not silently software).
- Verification: demo.mp4 H.264 decodes through VideoToolbox with a
  transferred hardware surface, and the pixels match the software
  decode within 0.05; switch off forces software.
- build-ffmpeg.sh also enables nvdec when ffnvcodec headers exist.
2026-08-19 14:04:55 +08:00
Mike-Solar 4dd4ceb08a feat(app): playback resolution divider (Full/Half/Quarter/Eighth)
The C++ viewer Playback Resolution menu, wired end to end: the radio in
the viewer context menu reflects and sets the PlaybackDivider config,
proxy_render_size renders the preview at 480/divider long edge, and
changing the divider invalidates the cached and in-flight preview
frames. This is the escape hatch for machines that cannot keep up with
playback (measured: a debug build of the worker pool reaches only 11
fps vs 152 fps in release on 1080p H.264, which no amount of
scheduling can make realtime).
2026-08-19 13:11:02 +08:00
Mike-Solar 46e43b51d9 fix(app): playback tracks the playhead after stalls - clamp, prune, never teleport
Two compounding causes behind 'playhead advances but the picture stays
frozen' and 'pause freezes the app':

- The wall-anchored clock teleported the playhead past the pre-render
  window during any long stall (the first render after pressing play
  costs seconds while the worker pool spins up: measured +104 frames in
  one 4.1s block). The window then started behind and, with stale
  in-flight frames occupying the workers, never converged.
  RealClock::tick now clamps the advance to 2 frames/tick and
  re-anchors the dropped time (NLE drop-frames semantics).

- Window frames the playhead had already passed stayed pending/in
  flight, burning worker time on frames that could never be displayed.
  update_preview_window now cancels them per tick via the new
  JobDispatch::cancel_preview_frame, keeping the workers on frames
  around the playhead.

Includes a production-shaped regression test (real 1080p media on the
timeline, actual cpu_frame display path) that failed with the exact
production signature (playhead 240 / displayed 0 / 36 stale slots)
before the fix and passes after.
2026-08-19 12:17:10 +08:00
Mike-Solar 9e9c6d9863 fix(app): never block the UI thread on a sync render during playback
The main-process sample showed the UI thread spending 100% of its time
in TicketArena::wait from the painted frame's synchronous render: every
cache-missed playhead frame sync-rendered inline, and the seek-priority
ticket then stole worker capacity from the pre-render window while the
blocked tick loop could not feed it — a self-reinforcing loop that made
playback unusably choppy.

On a playback miss the viewer now shows the last displayed frame while
the pre-render window warms up/catches up (paused monitors and the very
first frame keep the synchronous path). Adds a gpui test driving real
playback that requires the window to supply playhead frames, and the
real-footage bench_playback example used for the measurements
(152 fps aggregate on 1080p H.264 at 480p preview, decode-bound).
2026-08-19 05:12:30 +08:00
Mike-Solar a17d5be56a fix(render): assign batch slots in the worker's acquisition order
A claim mixing audio and video tickets is delivered as the video
message first and the audio message second, and the worker pops one
free-ring slot per ticket in that message order, checking each pop
against the assignment. The dispatcher however assigned slots in the
scheduler's interleaved frame order, so every audio ticket inside a
mixed batch mismatched, and each mismatch consumed a worker slot
without recycling it — cascading into the 'slot assignment mismatch'
flood and failed frames during playback.

Slot assignment now partitions the claim: video tickets first, then
audio. The mixed_audio_video integration test forces mixed claims
(queue depth > slot count with immediate releases) and fails with the
exact production signature when the fix is reverted.
2026-08-19 04:49:15 +08:00
Mike-Solar 345c464e55 fix(render): cap the playback pre-render window to the slot headroom
Pressing play froze the app: the 120-frame pre-render window could
hold every shm slot in the pool (e.g. 8 workers x 3 F32 slots = 24 <
120). Once the wall-clock playhead outran the renders, the UI's
synchronous frame wait had no credit to dispatch, and the
slot-releasing cleanup runs on that same blocked UI thread — a hard
deadlock.

The window is now capped to (workers x slots - workers), reserving one
slot per worker so interactive (seek/sync display) and audio tickets
always dispatch. preview_window_capacity is exposed through
JobDispatch; a unit test pins the reserve math.
2026-08-19 01:55:59 +08:00
Mike-Solar 6dc8285f2a fix(app): probed stream indices for original media, central modal-defer, one less onscreen copy
- preview_footage_media decodes the original from the footage's actual
  first stream of the kind instead of hardcoded 0/1, fixing audio-first
  and other atypical stream layouts (with a unit test).
- spawn_modal now probes for a nested window update and defers the
  build instead of silently dropping the dialog — the phase-7
  Preferences/Action Search fix applied centrally to every modal
  (export, proxy settings, project manager, progress dialogs).
- The gpui_wgpu atlas no longer double-copies identity-format uploads,
  leaving a single CPU staging copy (gpui RenderImage ownership) plus
  the GPU upload on the onscreen path; the residual copy and the
  IOSurface route to true zero-copy are documented in the M15 design.
2026-08-19 01:19:52 +08:00
Mike-Solar adf2cef32c feat(oakrender): render-process isolation S3 - audio over shm, per-ticket slot formats, tuning
- Audio tickets join the process backend: render_audio_batch wire
  message, workers mix straight into shm slots (SLOT_FORMAT_AUDIO_F32),
  ShmAudio payload with release semantics, crash isolation covers audio
  renders; playback audio uses an async 4-chunk prefetch drained on the
  UI tick (also fixes the sub-60fps chunk truncation bug); oversized
  ranges and dispatcher outages fall back to in-process inline.
- Per-ticket slot formats: force_format is honored (exports request
  F32 slots, dropping the BGRA8 round-trip and its 8-bit quantization);
  segments grow on demand via worker-idle rebuild with generation
  handoff; the scheduler filters over-capacity tickets.
- Adaptive defaults: 128-256MB/worker segment budgets drive slots per
  worker, batch size follows workers/slots; bench_process example
  measures throughput and adjacent-frame completion deltas
  (e.g. 4 workers: 841 fps, 4.6ms mean delta).
2026-08-19 00:42:03 +08:00
Mike-Solar 194d761ade refactor(oakundo): replace the CHandle vtable layer with owned trait objects
With the C ABI facade (oakengine) retired, the frozen-ABI rationale is
gone. UndoCommand now boxes a Send Command trait (new/from_closures/
multi), dropping OakUndoCommandVtable, the userdata trampolines, the
refcount shell, the handle module, and all undostack_* handle exports.
The global facade loses its raw-pointer out-params (can_undo/can_redo
return bool, command_name returns String). oaktimeline/oaknode/
oakplugin/oaktask construct commands directly via UndoCommand::new.
oakundo src is now free of unsafe; behavior (ordering, idempotence,
done flags, groups, observers, 200-row cap) is unchanged and pinned by
the rewritten tests.
2026-08-19 00:41:49 +08:00
Mike-Solar e0aa597f1e feat(app): custom shortcuts, Keyboard preferences tab, Action Search
- Shortcut override layer over the action registry: <config>/shortcuts
  file (id<TAB>keystroke, gpui syntax), loaded before bind_keys at
  startup, saved as diff-only (all-default removes the file), conflict
  resolution steals the key from its previous owner; rebind_keys
  applies changes live (clear_key_bindings + bind_keys + menu rebuild).
- Preferences gains a Keyboard tab: menu-hierarchy action list,
  name/path/shortcut filter, click-to-capture key editor (any key
  assigns, Backspace unbinds, Esc cancels), Reset Selected/All,
  Import/Export.
- Action Search on '/': modal listing 'Menu > Submenu > Action',
  live filter, arrows + Enter dispatch through the same path as menu
  clicks. Keystroke interception handles capture/search input ahead of
  the global keymap; modal opening is deferred to avoid a nested
  update_window failure.
2026-08-18 23:09:53 +08:00
Mike-Solar ec7b7e6d13 feat(app): OpenFX UI wiring - effect library, inspector params, startup glue
- src/oakui/ofx.rs: startup sequence (host scan, register_plugin_nodes,
  progress reporter factory -> app progress dialog channel, active
  viewer time provider, project extent sync); all failures degrade to
  logs. oak-worker runtime also scans and registers plugins.
- Effect library groups OpenFX entries by sub-category (Filter/
  Generator/Transition/General); effect insertion goes through
  Factory::create_any so dynamic plugin nodes resolve.
- Inspector renders OFX parameters from node inputs (sliders, combo
  boxes from repeated combo_option/combo_value properties, vec/color
  spinboxes, text with explicit commit, push buttons), edits are
  undoable; persistent plugin messages surface as a card badge.
- oakplugin: push_button_clicked and per-instance persistent message
  counting (thin public layers).
2026-08-18 22:16:08 +08:00
Mike-Solar d61acb9e0a chore(crates): retire oakengine facade, drop oakcommon handle module
- crates/oakengine moved to crates/oakengine.bk (excluded from the
  workspace): the frozen C-ABI cdylib had no in-workspace consumers
  left after the direct-rlib migration (M14); git history is the
  authoritative backup.
- oakcommon: remove the CHandle module (no remaining users); config
  store and shared value types are unaffected.
2026-08-18 21:40:35 +08:00
Mike-Solar f12cf3ffef refactor(oakundo): mark raw-pointer facade functions unsafe
Part of the CHandle/unsafe cleanup: can_undo/can_redo/command_text/
command_is_done and command_init take raw pointers and are now unsafe
fn, with call sites wrapped in explicit unsafe blocks.
2026-08-18 21:40:22 +08:00
Mike-Solar cf459d7e4c feat(app): multicam panel with live angle grid, switching, timeline enable
- New MulticamPanel: rows/cols angle grid with the current angle
  highlighted, click-to-switch, 1-9 switch-and-split and cmd-1-9
  switch-only shortcuts (focused-panel routed), deferred switch queue
  during playback.
- src/oakui/multicam.rs: clip->connected-sequence resolution, multicam
  state detection (selection then playhead fallbacks), per-angle frame
  requests rendered through the process backend into an LRU cache.
- Timeline clip context menu Multi-Cam checkable item wired to
  oaktimeline::multicam enable/disable with undo.
- Engine trait extended (real + mock); mock drives the real command
  path with synthesized angle frames.
2026-08-18 21:40:00 +08:00
Mike-Solar cad1d93544 feat(oakrender): render-process isolation S2 - process backend by default, zero-copy onscreen
- WorkerPool thread pool deleted; RenderManager defaults to the
  Processes backend (oak-worker children), Threads kept as a test-only
  inline dispatcher; audio tickets stay in-process until S3.
- Onscreen path reads worker shm slots directly: BGRA8 slot format,
  RenderedFrame::Shm wrapped into the display buffer (single disclosed
  GPU-staging memcpy), scopes analyze BGRA8; the long-lived full-res /
  thumbnail paths take the counted slot_to_vec copy and release.
- Playback pre-render window: forward 120 frames (configurable) fed to
  the PreviewScheduler at Playback priority, interleaved across
  workers, cached in shm slots until the playhead consumes them;
  generation-based invalidation cancels and releases on edits.
- oaktask export and oak-cli run on private ProcessDispatchers (fixed
  a pump-while-locked self-deadlock in the export loop); facade
  get_frame handles ShmFrame payloads.
- Acceptance: preview path main_heap_frame_copies == 0 with spawned
  workers, CLI transcode/render verified end to end.
2026-08-18 20:45:24 +08:00
Mike-Solar 74b080f88a feat(oaktimeline): multicam enable/disable/switch commands, split copies the dependency graph
- oaktimeline::multicam: clip_find_multicam (buffer/tex_in depth-1
  lookup), multicam_enable/disable (rewire sequence<->clip through a
  MultiCamNode), multicam_switch (split-preserving-links at the
  playhead, each half owns an independent multicam copy, linked clips
  switched together) as single undo commands with C++ labels.
- BlockSplitCommand now duplicates the clip's whole dependency graph
  (copy_node_and_dependency_graph_minus_items) instead of just the
  block core, matching the C++ BlockSplitCommand::prepare semantics;
  undo detaches the copied subgraph, redo re-attaches identity-
  preserving.
- oaknode: fix serializer dropping edges from the first-created node
  (ptr=0 was not registered in id_map), restoring sequence_in edge
  round-trips; multicam node and clip wiring serializer round-trip
  tests.
2026-08-18 20:45:01 +08:00
Mike-Solar 431b9ed2b1 feat(oakrender): render-process isolation S1 - dispatcher, scheduler, real worker
Per the M15 design (docs/zh/plans/riir/M15-render-process-isolation.md):

- ipc.rs moved into oakrender with protocol v2: hello_caps,
  render_batch, batch_accepted, frame_failed; main-process-assigned
  slots; BGRA8 slot format. POSIX shm verified to 1GiB on macOS.
- ProcessDispatcher: spawns oak-worker processes, handshake, stdio
  NDJSON control, shm segment lifecycle with generation-tagged keys,
  crash detection with bounded restart and frame redispatch, zero-copy
  ShmFrameRef delivery and copy counters.
- PreviewScheduler: interleaved batch claiming (frame % W per worker,
  no work stealing), seek > playback-distance > background priority,
  credit-based flow control, crash recovery.
- oak-worker renders for real: graph snapshot deserialization, montage
  decode+composite straight into the assigned shm slot, F32->BGRA8
  final conversion in-worker, OFX plugin executor installed in-worker,
  crash hooks for isolation testing.

Thread pool coexists for now (S2 removes it). Integration tests cover
two-worker zero-copy rendering, crash isolation with redelivery, and
real H.264 footage decode into slots.
2026-08-18 18:58:38 +08:00
Mike-Solar f2af92958a feat(app): proxy editing and audio sync, aligned with the C++ version
Proxy: preview-path proxy substitution (global UseProxyMedia AND
per-footage enabled AND on-disk ready; export always uses originals),
proxy generate/delete/reveal/enable actions, ProxyDialog with global
and per-footage custom params, Tools menu + context-menu Proxy
submenus, progress in the status bar, OVE serialization of proxy
metadata and source_start_time.

Sync: timeline context-menu Synchronize by Source Time / by Waveform /
by Waveform (Adjust Speed) with ctrl-shift-w, cache-envelope
extraction with validity masks, reference/anchor selection and
single multi-undo application (replace-with-gap, speed adjust,
re-place) mirroring timelinewidget.cpp semantics.
2026-08-18 18:58:26 +08:00
Mike-Solar 2db1615453 feat(oakplugin): wire OpenFX plugins into the node graph and renderer
- oaknode: dynamic node factory registration, PluginNode value model
  pushing PluginJobPayload, traverser texture passthrough for texture
  inputs, type-stamped RefBox::get_checked.
- oakrender: PluginExecutor dependency-inversion slot; eval resolves
  and executes plugin jobs, purple frame on failure.
- oakplugin: node_factory with full OFX param -> node input
  translation (15 types, color semantics heuristic, combo ordering,
  secret/ui_group/ui_page, clip inputs), plugin instance registry,
  render executor + duplicator installation, progress reporter and
  active-viewer provider injection points, U8/U16/F16 input
  conversion with NaN scrubbing, in-place output frame writeback fix.
- gl_bridge.rs documents the wgpu<->GL interop spike: Metal-first on
  macOS rules out wgpu-hal GL interop; offscreen GL context deferred.

End-to-end tests cover registration, param translation, CPU render
pixel assertions, identity passthrough and NaN fallback.
2026-08-18 17:15:13 +08:00
Mike-Solar 9daa266189 feat(app): gpui action/keymap shortcut system, full main menu, context menus
Port the C++ menu/shortcut architecture (origin/main) to the Rust shell:

- src/actions.rs: action registry (123 entries with stable C++ ids,
  i18n keys, default key bindings, routing targets) driving both the
  menu bar and App::bind_keys; src/shortcuts.rs flat table removed.
- src/panels/commands.rs: PanelCommandHandler trait routing playback,
  editing, zoom, markers etc. to the currently focused panel.
- make_menus rebuilt from the registry: full File/Edit/View/Playback/
  Sequence/Window/Tools/Help trees aligned with the C++ main menu.
- src/menus/: shared context-menu infrastructure; right-click menus
  for timeline (clip/empty/track head/ruler), project explorer,
  viewers, node editor, inspector effect stack, with i18n EN/ZH.

Synchronize/Proxy/Multi-Cam entries exist but stay disabled pending
their engine wiring phases.
2026-08-18 17:15:00 +08:00
Mike-Solar 6576113a69 chore: bump gpui submodule (node zoom scaling, explorer thumbnails, divider drag fix) 2026-08-18 12:42:09 +08:00
Mike-Solar 33db5657e0 feat(app): reprobe legacy footage on load; background thumbnail pipeline
- graphops: reprobe_unprobed_footage resolves relative filenames against
  the project dir and probes footage whose stream metadata is missing
  (C++ saves and older Rust files), restoring durations and track kinds
- real: legacy footage is reprobed on open/adopt; a background worker
  renders first-frame thumbnails (hash-cached PNGs) and the project
  explorer data source attaches them as they complete
2026-08-18 12:42:03 +08:00
Mike-Solar b8a3beaee4 feat(probe): record and print the footage stream inventory
FootageBehavior now keeps the probed stream list (video/audio, per-stream
duration in rationals) instead of dropping it, and the probe CLI walks
that inventory to report real durations, frame rates and stream counts
rather than the previous zero placeholders.
2026-08-18 12:41:55 +08:00
Mike-Solar 61da70ecf8 fix(build): launch crash — @rpath/libz.1.dylib had no LC_RPATH
The static FFmpeg's external codec libs pull in -lz, which on this
toolchain resolves to a copy whose install name is @rpath/libz.1.dylib
(zlib-ng-compat); without an LC_RPATH entry all three binaries died in
dyld at startup. The app/cli/worker build scripts now emit
-Wl,-rpath,/usr/lib.

Also: FFMPEG_DIR moves into the committed .cargo/config.toml as a
workspace-relative [env] entry — ffmpeg-sys-next's build script cannot
read .env files, and without it the crate silently linked the shared
Homebrew FFmpeg while oakffmpeg-link emitted the static transitive
flags (mixed linkage). docs/build.md updated.
2026-08-17 19:42:45 +08:00
Mike-Solar b36cbd6b6f refactor: purge CHandle from module internals (M14 R5)
Module-internal object references are Rust types now (values, Arc,
Mutex); CHandle remains only at the oakengine C-ABI boundary:

- oakundo: the global stack holds UndoStack/UndoCommand values
  directly (stack token is the static's address)
- oaktimeline: marker/workarea boxes carry Arc<Mutex<T>>; commands
  share the same allocation through Arc clones (readers in oakengine
  stubs and the app's graphops updated to lock)
- oaktask/oakstorage: sessions, write-through bindings and the
  database backend pass ProjectArc; the Session drops its manual
  release bookkeeping; nodeutil keeps the CHandle<->Arc boundary
  conversion (release_project restored for the app)
- oakcodec: handle.rs deleted outright (no facade entry needed it);
  texture/block placeholders are unit structs
- oakrender: copier's project handle is an identity u64; alive-count
  machinery removed; handle.rs is make_owned/get/get_mut only
- oakplugin: the instance registry is gone (its unregister key never
  matched, leaking weak entries); handle.rs is the RefBox boundary type
- oaknode/oakcommon: only dead guard/borrow helpers removed; external
  payload handles (texture/processor) documented as the boundary

Flake hunts landed along the way: the audio recording test serializes
on the shared manager lock with a normalized state; the autocacher
cancel test uses a slow producer so cancellation is deterministic.
2026-08-17 16:40:15 +08:00
Mike-Solar ede03d0bfe chore: bump gpui submodule (track toggles + explorer icon buttons) 2026-08-17 01:37:46 +08:00
Mike-Solar 21cc2fba24 feat(app): track-header toggles, effect library, panel titles (design parity)
- track headers: name + visible/mute/lock toggles, undoable through
  graphops, honored by the montage (muted tracks are skipped) and by
  the edit guards (locked tracks reject trims/moves/deletes)
- project explorer gains a panel title row; the tree/icons toggle is
  now small icon buttons with tooltips
- new effect library dock panel (every addable effect; double-click
  appends to the selected clip's chain)
- screenshots refreshed (zh/en, window/manager/preferences)
2026-08-17 01:37:31 +08:00
Mike-Solar c5f1d0d76c refactor(engine): pure cdylib + undo-stack test race fix (M14 R4)
- oakengine is now cdylib-only (no rlib/staticlib consumers anywhere;
  cargo tree verified) — the plugin/external C ABI layer; README and
  docs updated
- cd.yml drops the dylib embedding/re-sign steps (the app no longer
  links it)
- test race root-caused and fixed for good: the global undo stack lock
  is now a re-entrant mutex (parking_lot) shared by every test that
  drives the stack, including the previously unlocked node/render
  families; the render-manager serial-ordering bug (an earlier repro
  test initialized the global manager before the not-initialized test)
  is fixed with a shared SERIAL guard and a manager shutdown
- 5 consecutive parallel runs clean; serial 209/209
2026-08-17 00:51:36 +08:00
Mike-Solar 022c0a7a5a refactor(app): cut liboakengine, link module rlibs directly (M14 R3)
- real.rs rewritten over module Rust APIs (Arc<Mutex<Project>> +
  NodeId; the addref handle dance and renderer boxes are gone);
  AppEngine trait and all panels untouched
- new app assembly layers: graphops (project/timeline/edit
  primitives), effectchain (chain composition with undo groups),
  renderops (montage build + ticket render + ExportTask export),
  library via oakstorage directly
- module-side safe API additions: oakundo global value-semantic
  push/undo/redo + from_closures, oakstorage project_arc_of
- deleted: src/oakui/ffi.rs, src/oakui/host_syms.rs, the dylib link
  config in build.rs (only the gpui IOSurface framework link remains)
- the binary carries zero liboakengine references (otool/nm verified);
  101 app tests green incl. the real-render and full-res e2e tests
- behavior improvements for free: sequences land in the project graph
  (the facade scratch-project deviation is gone), footage drops take
  one undo record, effect remove/reorder undo restores edges
2026-08-16 23:36:43 +08:00
Mike-Solar a45a7af2ac refactor(cli,worker): cut liboakengine, link module rlibs directly (M14 R2)
- oak-cli: new engine.rs assembly layer maps every facade call to
  module Rust APIs (oaknode graph/serializer, oaktimeline commands,
  oakrender ticket arena, oaktask ExportTask, oakcommon config); the
  ffi/optional/host layers and build.rs link config are gone
- oak-worker: the worker session + POSIX shm transport moved into the
  crate (oakrender backend + serde_json control plane); no dylib
- both binaries carry zero liboakengine references (otool verified);
  tests green (30 cli / 41 worker)
2026-08-16 21:16:27 +08:00
Mike-Solar 8a2e45225f refactor: sink facade glue into modules (M14 R1)
- oakundo::global: the process-global undo stack, grouping and a
  command observer API; facade undo.rs becomes a thin forwarder
- oakstorage::writethrough: the binding table, snapshot thread, flush
  and config resolution; it subscribes to oakundo's observer itself
- the remaining non-forwarding facade logic (TaskMeta, effect chains,
  timeline composites, RendererBox, exporter path) is documented as
  facade-owned with reasons
- facade exports unchanged; full suite stays green (the one
  render_manager_not_initialized failure is pre-existing on the base
  commit)
2026-08-16 20:37:45 +08:00
Mike-Solar f59c7c8476 docs(riir): M14 — frontends link module rlibs directly (plan A) 2026-08-16 19:54:48 +08:00
Mike-Solar 4f6c903edb chore: bump gpui submodule (dock panel drag-drop layout) 2026-08-16 19:46:34 +08:00
Mike-Solar b2d353fe99 fix(app): footage drop auto-creates the media's track
A fresh sequence has no tracks and a drop rejected with 'display track
does not exist'; wrong-kind drops also rejected. Dropping footage now
creates a matching track first (the Premiere convention), so the
drag-to-timeline path works on empty timelines too.
2026-08-16 19:46:05 +08:00
Mike-Solar 2958464bd7 chore: bump gpui submodule (explorer icons + footage drag) 2026-08-16 19:07:35 +08:00
Mike-Solar 061aab9180 fix(app): full-res worker crash + source playback frozen + cli/worker DS batch
- full-res source jobs carried only the footage node box: dropping the
  project mid-flight left the node dangling (crash in the worker's free
  path) and the node was also freed twice (at renderer creation AND at
  release). The request now carries an addref'd project copy and the
  node is freed exactly once; regression test drops the project before
  the worker runs
- the source clock ticked at the SEQUENCE length, so playing footage
  with an empty sequence froze the source playhead at 0; the source
  clock now loops at the selected footage's probed duration
- oak-cli/oak-worker: DeepSeek's refactor batch (clap migration, engine
  FFI consumers); the stale exporter-family test flipped to the real
  contract (mp4 is written)
- engine: render_audio smoke test on an empty sequence
2026-08-16 19:07:07 +08:00
Mike-Solar 36ca413a84 ci: restore macOS and move all runners back to WarpBuild
CI matrix: warp-ubuntu-latest-x64-8x / warp-macos-15-arm64-6x /
warp-windows-latest-x64-16x (the macOS steps were still in place, only
the matrix entry was missing). CD: macOS DMG job re-enabled on the warp
Apple Silicon runner; linux and release jobs back on warp-ubuntu;
Windows NSIS stays disabled until the engine links there.
2026-08-16 18:21:10 +08:00
Mike-Solar f3f3748173 ci(cd): release needs only the linux job while macos/windows stay disabled 2026-08-16 18:09:36 +08:00
Mike-Solar 8334894984 ci(cd): packaging verified end-to-end on macOS
- every job builds -p oakengine first (the dylib is a build-dep-only
  artifact otherwise and cli/worker link-search the profile dir)
- 512px icon (tauri-icns only maps 512@1x/1024@2x); dylib embedding
  derives the path from the binary's otool reference; fpm invoked from
  the gem bin dir; GITHUB_ENV blocks batched (SC2129); actionlint.yaml
  whitelists the warp runner labels
- Windows job disabled with a reference block until the engine links
  there
- real run: Oak-macOS-arm64.dmg produced, app launches from the volume
  (known gap recorded: the dmg still dynamically links Homebrew codec
  libs; not self-contained yet)
2026-08-16 18:05:03 +08:00
Mike-Solar 4e5d8747b5 refactor(oakengine): absorb oakcore host symbols into the dylib
The 'host-provided' oakcore_audioparams_* runtime imports dated from
the deleted C++ host; the facade is their only caller. The dylib now
defines and exports the six symbols itself (repr(C) AudioParams mirror,
liboakcore-compatible semantics), -Wl,-undefined,dynamic_lookup is gone,
and the Windows DLL undefined-symbol blocker is removed by construction
(Windows CI/packaging stays off until a real toolchain verifies links).
2026-08-16 18:05:03 +08:00
Mike-Solar e346ea5338 feat(app): P5 — async full-res render, full preferences, shortcut map
- viewers show the 480px proxy immediately and a background thread
  fills the sequence-resolution frame (per-monitor in-flight job,
  generation-based staleness, playback skips full-res)
- preferences dialog complete: cache dir (now consumed by
  default_disk_cache_path), proxy policy/divider, snapshot interval
  (write-through era autosave), default transition length, audio
  in/out devices (new facade device-enumeration exports; audio init
  from config — playback was never creating the audio instance),
  language/theme/renderer backend, all persisted via config
- shortcut map (src/shortcuts.rs): space/J/K/L, I/O, S split, A/^A,
  ⌘Z/⌘⇧Z, ⌘N/⌘O/⌘S/⌘E/⌘Q, frame step, Home, track zoom; dispatch
  shares the menu action path and stays silent over modals
- screenshots: preferences dialog zh/en captured and reviewed
2026-08-16 18:05:03 +08:00
Mike-Solar 466cdcb7d7 chore: bump gpui submodule (ruler markers + work-area drag) 2026-08-16 16:06:36 +08:00
Mike-Solar 92daff1a83 feat(engine,app): timeline markers, work area, cross-track move (M12 P4)
- facade: oakengine_sequence_set_workarea_undoable (enable+range as one
  undo record); marker add/remove/list already existed and are now
  covered by it_timeline (10 new tests: markers, workarea, cross-track)
- fixes: stubs workarea_get honored NULL out-params (is_enabled always
  failed); export tasks honor custom ranges (export_params_pod dropped
  them; oaktask EncodingParams carries has_custom_range)
- app: markers on the ruler (gpui diamond markers), menu
  sequence-add/remove marker, set/clear work area (selected-clip bounds
  or playhead+1), ruler drag previews live and commits one undoable
  record (C++ ruler semantics); export uses the work area when enabled
- cross-track clip moves were already wired; now covered end to end
2026-08-16 16:06:19 +08:00
Mike-Solar 23e6fa7a5b chore: bump gpui submodule (project explorer drop fix) 2026-08-16 15:05:36 +08:00
Mike-Solar 693e2df5eb feat(app): project browser on real project data + drag-drop import (M12 P3)
- ProjectDataSource<RealEngine> reads the real bin tree (roots/
  children via the facade folder/footage enumeration)
- AppEngine::import_footage implemented (facade project_import_footage,
  last_error surfaced); double-click opens in the source viewer
- dragging files onto the browser imports them (also fixes a real gpui
  bug: ExternalPaths arrives as the bare type, not Arc-wrapped, so
  drops never fired)
- tests: real-engine import->browser listing, widget drop routing,
  facade folder enumeration failure matrix
2026-08-16 15:05:01 +08:00
Mike-Solar 0f82092e95 refactor(app): retire manual-save semantics (M13 D5)
- AppEngine: save_project -> export_project_path(explicit path);
  project_modified removed (write-through has no dirty flag)
- File menu's save/save-as are the export path (extension-dispatched
  ove/otio/fcpxml); the library row export path is unchanged
- facade project_save/is_modified stay frozen, documented as the
  legacy export surface
- docs: M13 D5 checked off; M12 inventory updated
2026-08-16 14:17:45 +08:00
Mike-Solar 025dc88c25 feat(storage,app): PostgreSQL backend (D3) + project manager window (D4)
D3: oakdb+pg:// fully wired (shared sea-orm entities, BIGSERIAL DDL,
connect-probe instead of pool retry on dead servers); Storage/Backend=pg
+ Storage/PgUrl config; 13 OAK_TEST_PG_URL-gated PG tests (verified
against a Docker postgres:16), always-on clean-error tests otherwise.

D4: DaVinci-style project manager — list with derived stats, create/
rename/duplicate/delete (confirm)/import/export (native dialogs,
ove/otio/fcpxml), shown at startup and from the file menu; facade
oakengine_library_* exports (list/create/delete/rename/duplicate/
import/export + project_load_library that binds write-through);
save/save-as menu becomes 'export project file', open splits into
from-library/from-file; status bar shows library write state; storage
activates on app start and flushes on exit; spawn_modal reentrancy
fixed (window-callback path) with a doc note.

Also: the P1 audio test's environment probe was lost in the ffi purge;
restored on cpal (the output device is cpal now).
2026-08-16 10:39:31 +08:00
Mike-Solar 5fabad8efd feat(engine): write-through persistence (M13 D2)
- facade storage session manager: project handles bind to the default
  SQLite library on project_new/load; every undo push/group_end/jump
  write-throughs via DatabaseBackend::save (diff journal); project_free
  flushes and unbinds
- background snapshot thread (Storage/SnapshotIntervalSec, latest-wins,
  newest 3 kept) with exit flush (oakengine_storage_flush)
- config-gated: storage only activates with an explicit
  Storage/Backend=sqlite, so headless consumers and tests never touch
  the real library; new exports: storage_flush/is_bound/last_error
- it_storage: kill -9 recovery, cross-session undo, snapshot pruning,
  multi-project isolation, graceful degradation
- also fixes a real config test polluting the user config.ini and the
  undo-stack test races
2026-08-16 08:52:23 +08:00
Mike-Solar b35f3b49bc docs: project storage architecture (en+zh), linked from README and docs index 2026-08-16 01:40:53 +08:00
Mike-Solar 3dfeed67f5 feat(storage): database backend D1 — SQLite, node-granular journal, snapshots, persistent undo
- sea-orm schema: projects/settings/snapshots/journal (journal rows
  are per-node before/after images produced by diffing the serializer
  output, never full-project copies)
- replay = latest snapshot + journal; load_at() rewinds to any command
  seq (cross-session undo history); snapshot interval and journal
  retention configurable; snapshots pruned to the newest 3
- project manager API: list/delete/duplicate/rename, export/import
  (.ove/.otio/.fcpxml), stats derivation; PG surfaces as E_NO_BACKEND
  until D3
- docs(riir): M13 finalized (aggregate-granular persistence)
2026-08-16 01:38:27 +08:00
Mike-Solar 530bc762dc docs(riir): M13 final — node-granular journal (diff-produced), snapshots, persistent undo 2026-08-16 01:19:41 +08:00
Mike-Solar 78d8073c89 docs(riir): M13 — database backend, write-through persistence, project manager 2026-08-16 00:40:02 +08:00
Mike-Solar ab1a2e9c7b refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are
gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/
oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain
Rust, CHandle marshalling shrinks to the oakengine boundary, and tests
call the Rust APIs directly (pure C-ABI wrapper tests removed where
the domain layer already covers the behavior).

exporter.h family implemented: oakengine_export_render (CLI contract),
oakengine_export_render_with_params (was a stub), last_error and
progress callback; synchronous path reuses task_create_export +
start_sync. Fixes on the way: oaktask video ticket self-deadlock,
audio params dropped on the export path, codec encoder AAC slicing and
H.264 time base. Real-mp4 tests cover both entry points, progress and
the illegal-argument matrix.

Also: oakstorage session maps null project handles to None (version-
info path), configstore test double literal 3.14 -> 3.15 (clippy PI
lint), oakaudio output callback scratch buffer + env-aware P1 test,
cli media round-trip test uses a generated 16-frame clip (no more
minute-long debug runs).
2026-08-16 00:33:45 +08:00
Mike-Solar 2248be8567 feat(storage): real oakstorage file backends + full-timeline .ove serializer
oakstorage (new workspace member): URI dispatch, pluggable backends
(ove-xml built in, otio/fcpxml via oakotio, C-vtable foreign
registration), the M10 C API surface, version info codes, last-error
and alive accounting; round-trip tests per backend.

oaknode serializer: persists the full timeline — sequence track lists,
track block lists, block ranges/media_in/speed/flags, clip footage
references, footage filename+streams, folder children — through
<custom> behavior hooks with two-phase reference resolution; loads the
C++ <olive><project><layout> containers (golden: tests/
project_with_footage.ove); round-trip is field-by-field and
byte-idempotent.
2026-08-14 15:14:38 +08:00
Mike-Solar 48d3027d50 feat(engine,app): node editor bound to the facade graph (M12 P2)
facade (API only extended): oakengine_sequence_as_node /
_sequence_node_count / _sequence_node_at / _sequence_remove_node;
remove validates ownership by project UUID (arena slot/generation
collide across projects). it_node covers enumeration/edits and the
NULL/illegal matrix.

app: NodeGraphDataSource enumerates the current sequence's graph —
footage / effect / clip / output cards at their context positions
(deterministic role-grid fallback), real edges plus synthesized
clip->sequence tex_in edges; connect/disconnect/remove/drag-release
all go through undoable facade commands, drag previews stay local.
2026-08-14 13:33:49 +08:00
Mike-Solar 4240df1d71 refactor(crates): implement std::error::Error for all module error enums
thiserror derive across oakundo/oakcommon/oaknode/oaktimeline/oaktask/
oakotio/oakcodec/oakaudio/oakrender/oakplugin/oakengine/oakstorage;
Display carries the module prefix and the Failed context, source()
stays default except oakotio's #[from] forwarding. code() mappings and
variants unchanged; each error.rs gains Display/object-safety/code
regression tests.
2026-08-14 05:45:05 +08:00
Mike-Solar d460d57805 fix(oakaudio): P1 output hardening
- output callback reuses a scratch buffer instead of allocating per
  call (real-time rule)
- P1 consumption test probes real callback delivery and skips on
  headless/background sessions (CoreAudio starts the stream but never
  runs it outside the GUI session), with a 30s poll for slow HAL
  startup; restores the manager singleton state afterwards
- waveform: drop leftover DBG-WF debug prints
2026-08-14 05:45:05 +08:00
Mike-Solar c8c282f06c chore: bump gpui submodule (content-width dock tabs, rounded clips) 2026-08-13 23:39:42 +08:00
Mike-Solar f0517fe6af feat(app): UI walkthrough pass — faithful screenshots, dock labels, timeline clips, status bar, meter
- screenshot example inits i18n and captures both zh-CN and en-US
  (docs/screenshot-window{,-en}.png)
- dock tabs size to content (no more 64px truncation); viewer/panel
  titles follow the design (素材查看器·name etc.)
- mock project carries V1/V2 video + A1/A2 audio clips rendered as
  rounded green bars; timeline clip geometry/rounded corners match the
  design; status bar visible with full content; audio meter strip
  docked at the program viewer's right edge; project explorer rows
  have icons
- tests/waveform_e2e.rs: waveform cache extracts real peaks and hits
  cache on re-query (P4 acceptance)
2026-08-13 23:37:57 +08:00
Mike-Solar c5287ff234 chore: bump gpui submodule (menu scrubbing + density pass) 2026-08-13 18:16:27 +08:00
Mike-Solar 5afa95c80e feat(render,app): footage decode lands (M12 P0) + UI density pass
engine:
- oakrender eval footage hook decodes via oakcodec (JobSpec::Footage
  carries filename/stream); ticket/ffi/manager wiring, real-media
  decode test with programmatically generated MPEG-2
- oakrender bridge/codec.rs + node.rs: direct oakcodec/oaknode calls;
  the crate's dlsym module is gone (project_deep_copy/sync_copy remain
  documented always-fail stubs — never implemented in oaknode)
- oakaudio waveform/decoder path adjustments for the decode hook

app (gpui + gpui_widgets):
- menu bar scrubbing: hovering another top-level title while a menu is
  open switches to it; popup width is content-aware (CJK-aware) instead
  of fixed 160px
- density pass: window rem 16 -> 14px, menu rows 26 -> 22px, dock tabs
  32 -> 26px, viewer transport tightened
- open/import/save-as use the native platform file dialogs
  (prompt_for_paths / prompt_for_new_path; multi-select import);
  MockEngine records imported footage for tests
- project explorer Tree/Icons toggle is localized (explorer.tree /
  explorer.icons widget keys)
2026-08-13 18:16:10 +08:00
Mike-Solar 3625e37081 docs(riir): M12 app rewrite plan (gpui shell, P0-P5 phases) 2026-08-12 14:35:14 +08:00
Mike-Solar 06f47235b2 feat(engine,app): source-monitor frames + real audio meter
source monitor:
- new facade exports oakengine_renderer_create_for_node (render any
  node, not just sequences) and oakengine_project_footage_at (fetch a
  footage node by index); it_render covers the e2e render plus the
  NULL/illegal matrix
- RealEngine renders the selected footage's real frames to the source
  viewer (per-node renderer slot, frame cache invalidated on selection
  change, synthetic fallback kept)
- known gap (documented): pixels stay transparent black until
  oakrender's footage decode hook lands — the render surface itself is
  real end to end

audio meter:
- oakaudio manager can now report per-channel linear peaks of the
  buffered output (PreviewAudioDevice::peek_tail + levelmeter analysis
  of the newest 8192 frames, packed/planar F32)
- new facade export oakengine_audio_output_levels (negative codes pass
  through); RealEngine's AudioMeterDataSource reads it instead of
  returning hardcoded silence
- tests: module peak readback + facade validation/readback matrix
2026-08-12 14:19:18 +08:00
Mike-Solar f1f4124b18 chore: bump gpui submodule (tab-policy formatting pass) 2026-08-11 23:06:40 +08:00
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00
Mike-Solar a209f63d52 ci: rewrite CI/CD for the Rust workspace
- CI: push/PR to main only; ubuntu/macos-15-arm64/windows-ucrt64 matrix;
  deps via tooling/install-deps.sh; project FFmpeg built once and cached
  under .cache/ffmpeg keyed on the build script; rust-cache for target/
- CD: tag v* packages with cargo-packager — deb/AppImage/pacman on
  Linux (rpm converted from the deb with fpm), NSIS on Windows, and a
  DMG (Apple Silicon) whose .app embeds liboakengine.dylib via
  install_name_tool; tag pushes publish a GitHub release with all
  packages attached
- root Cargo.toml gains [package.metadata.packager]; the app icon is
  generated from Oak_Icon.svg with rsvg-convert in CI
- root build.rs now also links the app on Linux (link-search + rpath +
  --export-dynamic); Windows remains blocked on the DLL undefined-symbol
  problem (oakcore_* host imports), documented in build.rs
2026-08-11 20:04:44 +08:00
Mike-Solar 05e42668cb build(ffmpeg): static GPL FFmpeg 8.0 via project script + FFMPEG_DIR
- tooling/ffmpeg/build-ffmpeg.sh builds release/8.0 static+PIC into
  .cache/ffmpeg: GPL/version3, every free-license external codec lib
  probed via pkg-config (enabled when present), per-OS hardware
  acceleration (VideoToolbox/AudioToolbox, VAAPI/VDPAU/libdrm,
  D3D11VA/DXVA2/MediaFoundation, nvenc when ffnvcodec exists)
- tooling/install-deps.sh installs those libraries on Homebrew / MSYS2
  UCRT64 / Debian-Ubuntu / Fedora / Arch; nothing in the build sudo's
- ffmpeg-next's own build feature is unusable (every crate-version to
  FFmpeg-release pairing is broken upstream: 9.0.0->FF9 AVCodec fields,
  8.1.0->FF8.1 new enum variants, 8.0.0->FF8 FF_PROFILE rename), so
  ffmpeg-next 9 + FFmpeg 8.x headers via FFMPEG_DIR it is
- new links-crate oakffmpeg-link emits the static FFmpeg's transitive
  link flags from its .pc files (cargo only propagates them from links
  crates, and rustc prunes the flags unless the rlib is referenced —
  hence the force_link statics)
- docs/build.md updated for the Rust workspace flow
2026-08-11 20:04:44 +08:00
Mike-Solar cdda643d64 feat(engine,app): effect chain facade + app effect stack wiring
facade (oakengine/oaknode, API only extended):
- oaknode_node_get_effect_input / oaknode_node_get_flags module exports
- oakengine_clip_as_node, oakengine_node_effect_count/at/insert/remove/
  move/set_enabled, oakengine_node_identity/is_enabled/get_type_id,
  oakengine_node_factory_id_at — chain edits wrap disconnect/reconnect
  into single undo groups; insert/move/remove validated against the
  factory and the host's effect input
- it_node covers enumeration/insert/remove/move/toggle/undo-redo and
  the NULL/illegal-input matrix

app:
- RealEngine binds the selected clip's effect chain to the inspector's
  effect stack (cards, enable, reorder, remove, expansion state), all
  edits undoable through the facade
- AppEngine gains set_selected_clips / addable_effects / add_effect
  (default no-ops keep the mock untouched); the timeline selection
  drives the stack target; the inspector gets a small add-effect menu
- known module gap (documented): ClipBlockBehavior sets no effect_input
  yet, so timeline clips cannot host effects until oaknode grows tex_in
2026-08-11 20:04:44 +08:00
Mike-Solar f86a895c8a Revert "build: root .cargo/config.toml enables the real OCIO bridge workspace-wide"
Machine-specific Homebrew paths do not belong in the repo; set
OCIO_RS_ENABLE_REAL/OCIO_INSTALL_DIR/OCIO_RS_LINK locally instead.
2026-08-11 18:05:56 +08:00
Mike-Solar 43003d0e33 build: root .cargo/config.toml enables the real OCIO bridge workspace-wide
cargo test from the workspace root does not read
crates/oakcommon/.cargo/config.toml, so ocio-sys silently built its
stub bridge and oakcommon's OCIO tests failed with empty configs.
Mirror the same env (OCIO_RS_ENABLE_REAL/OCIO_INSTALL_DIR/OCIO_RS_LINK)
at the root; no manual environment variables are needed any more.
2026-08-11 18:00:08 +08:00
Mike-Solar 7b295b7661 feat(app): scopes (histogram/waveform/vectorscope) fed by rendered frames
- program viewer gains Picture/Scopes tabs; the scopes page hosts the
  gpui_widgets histogram, waveform and vectorscope side by side
- RealEngine analyzes the same F32 RGBA samples it renders (BT.709 luma,
  normalized Cb/Cr); the mock engine analyzes its synthetic frame
  through the same path; results ride the per-frame cache
- AppEngine::scope_data(monitor) exposes ScopeData to panels
- unit tests for the analysis math + a gpui test rendering the scopes
  tab; zh/en i18n keys added
2026-08-11 17:04:42 +08:00
Mike-Solar f908724a7a feat(app): real engine renders true frames to the viewers
- RealEngine caches a per-sequence renderer and serves cpu_frame via
  oakengine_renderer_render_frame (F32 RGBA, 480px-long-edge proxy)
  instead of the synthetic test pattern; edits/undo invalidate the
  per-frame cache
- frames.rs: F32 RGBA -> BGRA8 downconvert extracted as
  f32_rgba_to_bgra_image (shared with the synthetic fallback)
- ffi.rs: renderer/frame C ABI declarations + OakEngineFrame /
  OakEngineRenderer handle mirrors, oakrender_manager_init/available
- new real_render_frame_e2e test renders through the dylib; mock engine
  and headless tests unaffected
- known gaps (documented in code): source monitor still synthetic
  (facade renders sequences only), render is synchronous on the UI
  thread, renderer not rebuilt on sequence geometry change
2026-08-11 16:10:14 +08:00
Mike-Solar 26009a5ea5 refactor(oakaudio): drop ffmpeg_bridge fb_* imports, call ffmpeg-next in-process
- processor.rs: resample/channel-convert/time-stretch now runs an
  in-process FFmpeg filter graph (abuffer -> atempo -> aformat ->
  abuffersink) via ffmpeg-next
- waveform.rs: extraction decodes through oakcodec's FFmpegDecoder
  (interleaved f32) instead of the fb_decoder/fb_audio_graph pair
- bridge/ffmpeg.rs and the null fb_* test stubs deleted;
  oakcommon::ffmpegutils bridge constants become plain ints
- liboakengine.dylib no longer imports any fb_* symbol (nm -u clean);
  the remaining runtime imports are the host-provided oakcore_* symbols
- real decode/resample tests added (generated PCM input, no network)
2026-08-11 16:10:14 +08:00
Mike-Solar aa5fcef66e fix(oakengine): facade bugs found by integration tests
undo:
- NULL/empty label no longer crosses to oakundo as a dangling 0x1
  pointer (push, group_begin/end) — fixed SIGSEGV
- group_abort now undoes each executed child in reverse order

task:
- create_project_import addrefs the borrowed project handle instead of
  freeing it under the async task — fixed UAF/SIGSEGV

timeline:
- toggle_enabled/delete_clips guard NULL+0 slices — fixed SIGABRT
- BlockSplitCommand halves placed correctly (oaktimeline undosplit)
- PreservingLinks / ripple remove / ripple delete-gaps commands
  self-prepare on first redo — fixes silent no-op split/ripple
- trim_clips_to targets the block containing the point, not the track
- delete_empty_tracks applies the live track removal
- ripple facades no longer free borrowed track handles still referenced
  by commands — fixed UAF

node:
- project_add_node releases the factory handle — fixes per-call leak
- inputs_from(recursive=0) matches direct feeders (BFS off-by-one)
- group passthrough id/resolve treat two-stage string length as success
- node_connect(_command) reject duplicate connects with E_STATE
- folder_add_child enforces one-folder-per-node
- value_split_to_tracks splits vector/color per component
- set_context_position/expanded establish the first entry
- node_get_flags on an empty box returns 0, not u64::MAX
- footage_borrow addrefs its wrapper — fixes double-free

render:
- renderer_create rejects invalid pixel formats (real range check)
- render_frame forwards renderer width/height to the ticket

tests: repro #[ignore]s removed, bug-behavior assertions corrected,
it_undo global-stack tests serialized with a shared lock
2026-08-11 01:30:18 +08:00
Mike-Solar b564e7a71f chore: bump gpui submodule (submenu hover fix, icons/tooltip helpers) 2026-08-11 00:25:21 +08:00
Mike-Solar 063837446d feat(app): link liboakengine.dylib via frozen C ABI; UI fixes
- App no longer depends on the oakengine rlib: build.rs links the
  built liboakengine.dylib (+rpath, -export_dynamic, IOSurface) and
  src/oakui/ffi.rs declares the pure-C surface; RealEngine calls only
  the frozen oakengine_* C ABI
- host_syms.rs provides the oakcore_*/fb_* host symbols the dylib
  imports via dynamic lookup
- Fix Preferences dialog crash (spawn_modal reentrancy) with a
  regression test
- Timeline toolbar and viewer transport render C++-era icons (16px
  grid, dark/light themes) with localized tooltips
- i18n: complete en-US table, add untranslated-key detection test
- New dialogs module (preferences, export, progress)
2026-08-11 00:25:20 +08:00
Mike-Solar 3110e5cc3a test(oakengine): rename integration test families to English names
it_*族.rs -> it_audio/codec/common/node/plugin/render/task/timeline/undo.rs;
also fixes a pre-existing racy assertion in it_task.
2026-08-11 00:24:57 +08:00
Mike-Solar d9c477365a remove: remove some unneccessary files 2026-08-10 21:12:29 +08:00
Mike-Solar 013a175707 refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
2026-08-10 20:24:25 +08:00
Mike-Solar f8540e3892 refactor(engine): single-lib phase 3 — plugin/timeline/task/codec bridges converted
- oakplugin bridges -> direct (dlsym 28->0; OFX suites untouched)
- oaktimeline bridges -> direct (teststubs slimmed to pure-Rust mocks)
- oaktask bridges -> direct; facade EncodingParamsPOD -> oakcodec alias
- dlsym total 102->24 (all in documented dead/host directions)
- All crates build+test green; dylib exports unchanged
2026-08-10 19:11:25 +08:00
Mike-Solar 96e57b3705 refactor(engine): single-lib phase 2 — node/render/audio bridges to direct calls
- oaknode bridge/{common,codec} -> oakcommon/oakcodec direct calls
  (drops node's test-stubs XML mocks; fixes decoder_probe signature drift)
- oakrender bridge/common -> direct; dead bridge/codec deleted;
  dead copier kept as dlsym (documented)
- oakaudio bridge/{codec,common} -> direct (CHandle alignment)
- oakcodec test-stubs split to support node test linkage
- dlsym: node 52->20, render 22->4
- single-lib.md updated (sec 11.3/11.4)
2026-08-10 18:29:18 +08:00
Mike-Solar e1423553c7 refactor(engine): single-lib phase 1 — direct Rust calls across the facade
- unify CHandle in oakcore-rs (all crates re-export it)
- facade bridge: 618/624 externs converted to direct module calls
- signature-drift fixes surfaced by direct calls (videoparams,
  seconds, sync estimate, POD unification)
- dead node<->render call sites removed; cancelatom -> oakcommon
- node bridge/undo -> oakundo direct calls
- docs/zh/plans/riir/single-lib.md: design + live status
2026-08-10 17:48:13 +08:00
Mike-Solar f0d551d4a8 docs(riir): settle stale tech-debt entries in notes.md 2026-08-10 17:00:37 +08:00
Mike-Solar 0ca9cad448 feat(engine): rename facade to oakengine, build liboakengine.dylib
- src/facade/rust -> src/engine/rust; package oakfacade -> oakengine
- crate-type += cdylib; module crates are real deps; linkage anchors
  force-link module C ABIs into the dylib
- build.rs: -undefined dynamic_lookup for host-provided oakcore_*/fb_*
- nm: 749 oakengine_* + 687 module oak*_* exports; undefined set is
  only the intended host-provided symbols
- worker/cli updated to the new path/name; undo test race fix
2026-08-10 16:55:35 +08:00
Mike-Solar e563b340ac feat: oakengine facade, oaknode/oakrender impls, worker+CLI, app skeleton
- oaknode Rust crate: full implementation (core engine, sequence/
  track/block/footage, traverser, serializer, 43 node behaviors;
  493 tests green)
- oakrender Rust crate: full implementation incl. wgpu backend
  skeleton, ticket arena, worker pool (136 tests green; fixed
  lost-wakeup and ticket ordering races)
- src/facade/rust (oakfacade): 222 oakengine_* exports over the
  module C ABIs (61 tests green); worker_main + real POSIX shm
  frame-slot transport (SpscRingBuffer/FrameSlotPool, wire-compatible
  with engine/render/ipc)
- cli/rust + worker/rust binaries (29 + 29 tests green)
- oakotio: FCPXML import/export (49 tests green)
- oaktask: OTIO/FCPXML format dispatch (90 tests green)
- app/rust: gpui app skeleton — dock panels (viewers/timeline/
  explorer/inspector/node editor), transport, olive themes,
  i18n (en/zh), 37 tests green
- gpui submodule: menu checkmarks, dock ratios, vertical meter,
  CPU-frame viewer surface, drop-frame timecode
2026-08-10 08:12:08 +08:00
Mike-Solar d11d80ea53 chore: add oak-gpui submodule (UI framework for the app rewrite) 2026-08-09 20:21:28 +08:00
Mike-Solar cac41d92c1 feat(rust): oakcodec/oaktask/oakplugin crates + oakotio + node/render/storage skeletons
- oakcodec: full crate incl. real FFmpeg decode/encode via ffmpeg-next
  (162 tests, 84.4% cov); new oakcodec_encoding_* metadata family
  (include/codec/format.h, C++ + Rust sides)
- oaktask: manager/tasks/project load-save incl. OTIO via oakotio
  (82 tests, 86.4% cov); concurrent render loop with reorder buffer
- oakplugin: M11 phase 1+2 — self-contained OFX host, GL path,
  ofxColour, pluginrenderer absorbed as render_driver (99 tests,
  81.7% cov); instance.h additions documented
- oakotio: native serde-based OTIO read/write (24 tests)
- oaknode gap fill: dragger/keyframe-helper/multicam C ABI families
  (113/113 gtest); fixes a latent NodeInputDragger segfault
- oaknode Rust skeleton: 43 built-in node type declarations
- oakrender/oakstorage: declaration skeletons (implementation pending)
- notes.md: gap analysis + tech-debt ledger
2026-08-09 05:49:15 +08:00
Mike-Solar 4b24aa9d67 feat(rust): first green Rust crates — oakcore-rs, oakundo, oakcommon, oaktimeline, oakaudio
- oakcore-rs: Rational/TimeRange/TimeRangeList/PixelFormat/SampleFormat
  mirroring oakcore C++ semantics (91.6% coverage)
- oakundo: UndoCommand/UndoStack with vtable commands (99.6%)
- oakcommon: config/XML/VideoParams/logging etc. (89.8%, 516 tests);
  XML via quick-xml, logging via the log facade
- oaktimeline: markers/workarea/edit commands (95.4%)
- oakaudio: processor/manager/sync/waveform/levelmeter (88.4%)
- unified -MMCCCC error codes across all C ABI headers (registry in
  include/common/error.h; pass-through rule)
- olive::Variant moved node -> common (cross-module value type)
- oakcommon_videoparams_set_is_3d added to the C ABI
- M11 (oakplugin Rust rewrite) plan + notes.md freeze line for the
  inter-module C ABI wiring
2026-08-09 04:24:28 +08:00
Mike-Solar 5a564f30ca refactor(node,render): route oaknode->oakrender calls through the C ABI
- oakrender cache C API: add create_for_node (four cache kinds),
  get_uuid, request, load/save_state, set_saving_enabled,
  set_passthrough, get_passthroughs, get_valid_cache_filename,
  get_timebase, lock/unlock, invalidate_range (rational variant),
  get_native (C++ only, for the in-module PreviewAutoCacher)
- Node's four caches become owned OakRenderCache value handles; all
  cache access in node.cpp/clip/viewer/traverser/serializers goes
  through the C ABI
- oaknode_node_get_video_frame_cache returns an addref'd
  OakRenderCache; drop OakNodeFrameCache; oaktask precache/export and
  oakrender_video_ticket_params take the value handle
- color: OCIOBaseNode/OCIOLutNode hold OakColorProcessor handles
  (dtors added); oakrender gains color_processor_create_transform/
  create_lut/create_grading_primary/get_native and LUT library
  queries, keeping all OCIO transform construction inside oakrender;
  oaknode gains colormanager_wrap_borrowed/get_native
- RenderManager::instance() check -> oakrender_manager_available();
  cancel_video_tasks and disk cache path go through manager C API
- remaining node->render C++ symbol: only olive::Texture::~Texture
  via Variant's shared_ptr payload (recorded exception, same class as
  the UndoCommand inheritance)

Tests: new cache/color/manager/colormanager cases; all standalone
trees green (common 196, codec 22, audio 40, task 112, render 63,
timeline 125, node 98, plugin 102).
2026-08-08 03:56:17 +08:00
Mike-Solar 81431d180d refactor(node,timeline): route oaknode->oaktimeline calls through the C ABI
- ViewerOutput owns timeline markers/work area via refcounted
  OakTimeline* value handles (oaktimeline_*_create) instead of C++
  objects
- oaktimeline C API: add marker_list_create/marker_add/
  workarea_create/workarea_set_enabled; handle boxes gain an optional
  deleter for owning handles
- oaknode_node_get_markers/get_work_area return addref'd OakTimeline*
  handles via out params (named-struct forward declarations avoid the
  public header cycle); drop OakNodeMarkerList/OakNodeWorkArea
- serializers 210528-230220 load/save markers and work areas through
  the oaktimeline C API; SerializedMarker POD replaces
  std::vector<TimelineMarker*> in the serializer interface
- Sequence::add_default_nodes uses oaktimeline_add_track_command
- clip.cpp display-mode enums come from the new neutral
  include/timeline/displaymode.h; delete src/node/transition/timeline
- oakcommon xml: add oakcommon_xml_reader/writer_wrap_native borrowed
  wrappers (C++ adapter use)
- liboaknode now has zero C++ symbol references into liboaktimeline

Tests: new owning-handle and wrap_native cases; all standalone trees
green (common 196, codec 22, audio 40, task 110, render 49,
timeline 123, node 96, plugin 100).
2026-08-07 22:53:44 +08:00
Mike-Solar fd2111d560 refactor(plugin): de-Qt oakplugin and wrap it in a pure C ABI
- de-Qt src/plugin/src (olivehost/oliveplugininstance/oliveclip/
  paraminstance/image/pluginprogressreporter); QMessageBox/
  QApplication replaced by facade callbacks
- new C ABI in include/plugin/{error,host,instance}.h with
  refcounted OakPluginInstance value handle
- paraminstance bridges via oaknode C ABI (OakNodeNode value handle,
  oaknode_node_identity registry); undo via oakundo C ABI
- oliveclip textures are OakRenderTexture value handles; oakrender
  gains texture/copier/ticket C API additions
- oaknode gains get_input_at_time/set_input_at_time_undoable/
  node_identity/sequence_from_node/sequence_set_default_parameters/
  find_input_footage
- move avframeptr.h to src/common/src (shared by codec and render)
- node transition pluginSupport stubs bridge the real oakplugin
  headers; all oaknode-building standalone trees add src/plugin and
  link oakplugin into their test binaries
- plugin tests self-sufficient (ipc shim, OfxHost force_load,
  PRE_TEST discovery, OCIO env); timeline standalone gains
  render/c_api
- restore ffmpegdecoder.cpp in src/codec/src/ffmpeg/CMakeLists.txt
  (dropped in 3d004c081, caused jump-to-0 in decoder/encoder tests)

All six standalone trees green: codec 22, audio 40, task 110,
render 49, timeline 121, plugin 100.
2026-08-07 22:18:36 +08:00
Mike-Solar 0462842f8c refactor(render,timeline,task): switch remaining handles to refcounted value structs
- oakrender: renderer/texture/frame/cache/colorprocessor/ticket/copier
  all become by-value {ctx, addref, release, abi_version} handles over a
  generic box; retain() folds into addref; new
  oakrender_cache_wrap_borrowed for native caches from oaknode
- oaktimeline: marker list/workarea borrowed handles become value
  handles (non-owning boxes) with explicit free
- oaktask: OakTaskTask becomes a value handle; ownership still moves
  to the manager on start (owns flag flips)
- consumers (timeline/task/codec sources) migrated; tests everywhere
  updated; suites green: render 45, timeline 117, task 106, node 96,
  common 193, codec 18, audio 36
2026-08-07 18:05:34 +08:00
Mike-Solar 67176281d9 refactor(node): switch oaknode to refcounted value handles, migrate consumers
- all 15 OakNode* handle types become neutral by-value structs
  {ctx, addref, release, abi_version}; shared box in
  src/node/c_api/nodehandle.h with owns flag (borrowed accessors
  return non-owning boxes; graph insertion flips owns off)
- oaktimeline/oaktask/oakrender call sites and their own public
  headers migrated to value handles; identity comparisons in
  timeline/task now compare native pointers
- regressions green: oaknode 96, oaktimeline 117, oaktask 106,
  oakrender 44, oakcommon 193, oakcodec 18, oakaudio 36
2026-08-07 17:20:42 +08:00
Mike-Solar 295ea1bfe5 refactor(undo): switch oakundo to refcounted value handles, migrate consumers
- OakUndoCommand/OakUndoStack become neutral by-value handles
  {ctx, addref, release, abi_version}; a handle is a shared_ptr
  equivalent at the ABI level, internals untouched (box owns/observes
  flag; containers adopt, boxes created by factories own)
- oaknode command factories and undoable variants return/write value
  handles; timeline command classes hold value handles; task import
  take_command returns a value handle
- tests updated everywhere; suites green: oakundo 22, oaktimeline 117,
  oaktask 106, oaknode 96, oakrender 44, oakcodec 18, oakcommon 193,
  oakaudio 36
2026-08-07 15:48:34 +08:00
Mike-Solar da2ab512c6 refactor(task): OTIO load/save tasks over oaknode/oaktimeline C ABIs
- LoadOTIOTask/SaveOTIOTask de-Qt'd: graph traversal and construction
  through oaknode C ABI (find_input_footage, sequence defaults, block
  factories, context positions), track creation through oaktimeline's
  add_track command; the OTIO import dialog becomes a facade callback
  (headless default: accept all)
- oaknode additions: find_input_footage, sequence_set_default_parameters
- USE_OTIO + otio-install libs wired into oaktask; OTIO dylibs copied
  next to liboaktask for @loader_path resolution
- OTIO round-trip test passes; build-oaktask 106/106 green
2026-08-07 14:58:56 +08:00
Mike-Solar 40a336276f refactor(task,render): RenderTask family over a new oakrender ticket C ABI
- oakrender: new ticket family (render_frame/render_audio with finished
  callback, result frame/samples access, cancel/wait), project copier
  C API, cache get_invalidated_ranges, manager set_aggressive_gc
- oaknode: node_copy_inputs, set_value_hint_track, viewer params
  setters, video frame cache borrowed outlet, project copy_settings
- oakcodec: encoder desired pixel format, export format extension,
  encoding generate_matrix, custom range in encoding params POD
- oaktask: RenderTask orchestrates oakrender tickets (no Qt threads/
  watchers), PreCacheTask and ExportTask rewritten over the oaknode/
  oakrender/oakcodec C ABIs; frames cross from oakrender to oakcodec
  handles by pixel copy (different control blocks)
- the two-step texture/download render path collapses into single-step
  (tickets always yield frames); subtitle sidecar kept
- tests: 105 in build-oaktask (export/precache factory paths,
  construction, conform end-to-end); regressions all green
2026-08-07 14:40:12 +08:00
Mike-Solar 6ded1d2d63 refactor(task): de-Qt oaktask base + project tasks, wire codec task submitter
- Task base: Qt signals become lifecycle listeners (the async-command
  callback exception), cancellation uses oakrender's OakCancelAtom C
  handle; TaskManager runs std::thread workers, no signals
- ConformTask/ProxyTask implement oakcodec's submit-callback contract
  (synchronous interim); register_codec_task_submitter() closes the
  conform/proxy loop - conform of demo.mp4 produces pcm caches in tests
- ProjectImportTask/ProjectLoadTask/ProjectSaveTask over the oaknode
  C ABI; image-sequence confirmation becomes a facade callback
  (default: not a sequence)
- C API additions needed by oaktask: oaknode serializer file-level
  save/load (auto-initializing), footage video-params/cancel-atom/
  as-node, folder add-child command factory; oakcodec decoder
  conform_audio + image-sequence helpers; oakrender cancelatom
  get_native
- fix double-ownership: submitting a task to the manager transfers
  ownership, freeing its handle only releases the wrapper
- C ABI in include/task (task/manager/project, OakTaskTask opaque
  handle + lifecycle subscribe), every function tested (103 cases in
  build-oaktask incl. regressions)
- RenderTask family and OTIO tasks follow in separate commits
2026-08-07 14:01:23 +08:00
Mike-Solar 66831a870e refactor(timeline): de-Qt oaktimeline and wrap it in a pure C ABI
- de-Qt all 11 sources: markers/workarea lose QObject/signals (list
  re-sort via direct resort() call, drawing moves to the app layer),
  explicit unique_ptr ownership for marker lists and viewer's
  workarea/markers
- timeline undo command families (split/pointer/ripple/general/track)
  now hold oaknode C handles and do all graph work through the oaknode
  C ABI (no adapter layer); gaps are attached to the project graph
  before insertion, orphan handles tracked and freed
- fix the de-Qt severed Block->Track length-changed chain with a
  direct block_length_changed() call (positions were stale after
  split/trim)
- expand oaknode C API by ~20 functions needed by timeline
  (copy_in_graph, block kind/casts, connect_element, input arrays,
  tracklist family, marker/workarea borrowed outlets)
- pure C ABI in include/timeline (marker/workarea/edit, command
  factories returning OakUndoCommand), oakcommon XML get_native
  accessors
- oaknode<->oaktimeline resolve each other at runtime; standalone
  drivers link both into test binaries
- tests: oaktimeline 117, regressions oakcommon 193 / oaknode 96 /
  oakrender 42 / oakcodec 18 / oakaudio 36 all green
2026-08-07 04:37:24 +08:00
Mike-Solar 354df2e194 refactor(config,audio): merge config into oakcommon, split oakaudio
- config moves into oakcommon as ConfigStore + oakcommon_config_* C
  API (INI storage, typed entries, error-handler injection); node and
  render call sites keep OAK_CONFIG() macro shape via a local shim
  that forwards to the C API; transition config stubs removed
- oakaudio: de-Qt all six classes, C ABI in include/audio with
  refcounted handles (processor/manager/waveform/levelmeter/sync,
  48 functions); PreviewAudioDevice moved in from render; recording
  goes through oakcodec encoder; waveform extract uses probe +
  ffmpeg_bridge decode (decode_audio needs M8 task system)
- fix re_sum_samples min/max init bug (values clamped to 0 for
  same-sign ranges)
- every C API function has positive + error-path tests; suites:
  oakcommon 193, oakaudio 36, oaknode 96, oakrender 42, oakcodec 18
2026-08-06 20:53:07 +08:00
Mike-Solar 3d004c081b refactor(codec): de-Qt oakcodec and wrap it in a pure C ABI; switch common handles to refcounted value structs
- oakcodec: de-Qt all 20 sources (QThread decode loop -> std::thread,
  QObject/signals -> callbacks), pure C ABI in include/codec with
  refcounted neutral handles (OakFrame/OakDecoder/OakEncoder),
  framemanager moved in from render, frame_to_buffer/buffer_to_frame
  moved in from oakcommon oiioutils, codec->task via submit callback
  (M8 will register), all cross-module calls go through the other
  side's C API, -fvisibility=hidden + OAKCODEC_API
- oakcommon: handles become refcounted value structs
  {ctx, addref, release, abi_version} (FFmpeg-style), pass-by-value
  signatures, free() as release wrapper; init_from_native/get_native
  for copyable value objects; OakCommonXxx renamed to OakXxx
- oakcommon: add logging (log_debug/info/warning/critical with level
  filtering and sink injection) + printf-style oakcommon_log C wrapper
- oakrender: add CancelAtom C API family; complete
  oakrender_color_processor_convert_frame; fix get_processor() missing
  definition and OCIO env var lookup
- tests: oakcommon 174, oaknode 96, oakrender 42, oakcodec 18, all
  green in their standalone builds
2026-08-06 18:50:07 +08:00
Mike-Solar edbd3913af refactor(render): de-Qt oakrender and wrap it in a pure C ABI
- copy engine/render to src/render/src (sunk param types excluded),
  de-Qt in five parallel groups: core machinery (tickets/worker pool/
  jobs), caches, color/texture, preview/IPC, GPU backends
- replace Qt GL/Vulkan wrappers with native context abstractions
  (CGL/EGL/WGL, raw vulkan.h), QProcess with POSIX WorkerProcess,
  QJsonObject with a minimal NDJSON-compatible workerjson (wire
  protocol unchanged), QDataStream disk state with a byte-compatible
  BinaryStream
- signals become single std::function callbacks or facade-triggered
  calls per the documented signal/slot strategy
- pure C ABI in include/render + src/render/c_api (renderer/cache/
  color/manager families, OAKRENDER_E_* codes), 37 gtest cases
- bridge src/node/transition/render/* stubs to the real oakrender
  headers, closing the node<->render cycle: liboaknode links
  liboakrender, zero dangling symbols
- docs: M7 implementation status + oakrender semantic-change notes
2026-08-06 03:21:26 +08:00
Mike-Solar d77348ad9f refactor(node): de-Qt oaknode and wrap it in a pure C ABI
- copy engine/node (188 files) to src/node/src, de-Qt in waves:
  core infra (Node/Param/Value/Variant/mathtypes), project/serializer,
  block/output, color, effect leaves, generator, gizmo/plugins
- strip QObject/signals/slots: notifications move to the facade's
  oakengine_event channel, ownership becomes explicit (unique_ptr,
  add_keyframe/add_gizmo), sender() replaced by current_gizmo
- QVariant replaced by olive::Variant, Qt math types by POD mathtypes,
  QXmlStreamReader/Writer by oakcommon's expat-based classes
- sink VideoParams/SubtitleParams/LoopMode/ColorTransform to oakcommon
  (M3.5); polygon/text rasterization behind backend hooks
- pure C ABI in include/node + src/node/c_api (oaknode_ prefix,
  OAKNODE_E_* codes, undoable variants take OakUndoCommand out-params)
- fix Project::clear() root_ reset + disconnect assert, Sequence
  TrackList leak
- 96 gtest cases green in standalone build (build-oaknode)
- docs: signal/slot handling strategy + M3 implementation status
2026-08-05 23:55:54 +08:00
Mike-Solar c50017127b refactor(undo): de-Qt oakundo and wrap it in a pure C ABI
- strip QAbstractItemModel/QAction/signals-slots from UndoStack (UI
  concerns belong to the app layer), index_changed becomes a
  std::function callback
- decouple UndoCommand from Project via an optional modified-flag
  callback pair
- add pure C ABI in include/undo + src/undo/c_api (oakundo_ prefix,
  vtable-based command wrapper, OAKUNDO_E_* error codes)
- fix three engine-side defects: jump(0) infinite loop,
  MultiUndoCommand child leak, push_pre_executed not undoable
- add gtest suites (22 cases) and a standalone build driver
2026-08-05 18:02:24 +08:00
Mike-Solar bad52feda3 refactor(common): de-Qt oakcommon and wrap it in a pure C ABI
- de-Qt all classes under src/common (std::string/vector/mutex,
  std::filesystem, expat-based XmlStreamReader/Writer)
- add pure C ABI in include/common + src/common/c_api: opaque handles,
  init returns NULL on failure, free(NULL) is a no-op, out-params with
  negative OAKCOMMON_E_* error codes (include/common/error.h)
- remove single-consumer classes from oakcommon (html, jobtime,
  otioutils, playbackaudioclock, tohex, util, avframeptr,
  crashpadinterface/crashpadutils, autoscroll, digit, range); their
  destinations are recorded in docs/zh/plans/riir/notes.md
- add gtest suites under src/common/tests (127 cases), standalone
  build driver in src/common/standalone
2026-08-05 17:20:19 +08:00
3714 changed files with 207028 additions and 1467673 deletions
+24
View File
@@ -0,0 +1,24 @@
# Oak Video Editor - 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 <http://www.gnu.org/licenses/>.
# The project FFmpeg (built by tooling/ffmpeg/build-ffmpeg.sh into
# .cache/ffmpeg) is the only supported FFmpeg: ffmpeg-sys-next reads
# FFMPEG_DIR at build-script time, which cannot come from a .env file —
# a relative [env] entry here is the only machine-agnostic way to set
# it. Run tooling/ffmpeg/build-ffmpeg.sh once before the first build.
[env]
FFMPEG_DIR = { value = ".cache/ffmpeg", relative = true }
-116
View File
@@ -1,116 +0,0 @@
# SPDX-License-Identifier: GPL-2.0
#
# clang-format configuration file. Intended for clang-format >= 11.
#
# For more information, see:
#
# Documentation/dev-tools/clang-format.rst
# https://clang.llvm.org/docs/ClangFormat.html
# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
#
---
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Custom
BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: false
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeComma
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: false
ColumnLimit: 80
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: false
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '.*'
Priority: 1
IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: false
IndentGotoLabels: false
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
# Taken from git's rules
PenaltyBreakAssignment: 10
PenaltyBreakBeforeFirstCallParameter: 30
PenaltyBreakComment: 10
PenaltyBreakFirstLessLess: 0
PenaltyBreakString: 10
PenaltyExcessCharacter: 100
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
ReflowComments: false
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 4
UseTab: Always
...
-182
View File
@@ -1,182 +0,0 @@
# Generated from CLion Inspection settings
---
Checks: '-*,
bugprone-argument-comment,
bugprone-assert-side-effect,
bugprone-bad-signal-to-kill-thread,
bugprone-branch-clone,
bugprone-copy-constructor-init,
bugprone-dangling-handle,
bugprone-dynamic-static-initializers,
bugprone-fold-init-type,
bugprone-forward-declaration-namespace,
bugprone-forwarding-reference-overload,
bugprone-inaccurate-erase,
bugprone-incorrect-roundings,
bugprone-integer-division,
bugprone-lambda-function-name,
bugprone-macro-parentheses,
bugprone-macro-repeated-side-effects,
bugprone-misplaced-operator-in-strlen-in-alloc,
bugprone-misplaced-pointer-arithmetic-in-alloc,
bugprone-misplaced-widening-cast,
bugprone-move-forwarding-reference,
bugprone-multiple-statement-macro,
bugprone-no-escape,
bugprone-parent-virtual-call,
bugprone-posix-return,
bugprone-reserved-identifier,
bugprone-sizeof-container,
bugprone-sizeof-expression,
bugprone-spuriously-wake-up-functions,
bugprone-string-constructor,
bugprone-string-integer-assignment,
bugprone-string-literal-with-embedded-nul,
bugprone-suspicious-enum-usage,
bugprone-suspicious-include,
bugprone-suspicious-memset-usage,
bugprone-suspicious-missing-comma,
bugprone-suspicious-semicolon,
bugprone-suspicious-string-compare,
bugprone-suspicious-memory-comparison,
bugprone-suspicious-realloc-usage,
bugprone-swapped-arguments,
bugprone-terminating-continue,
bugprone-throw-keyword-missing,
bugprone-too-small-loop-variable,
bugprone-undefined-memory-manipulation,
bugprone-undelegated-constructor,
bugprone-unhandled-self-assignment,
bugprone-unused-raii,
bugprone-unused-return-value,
bugprone-use-after-move,
bugprone-virtual-near-miss,
cert-dcl21-cpp,
cert-dcl58-cpp,
cert-err34-c,
cert-err52-cpp,
cert-err60-cpp,
cert-flp30-c,
cert-msc50-cpp,
cert-msc51-cpp,
cert-str34-c,
cppcoreguidelines-interfaces-global-init,
cppcoreguidelines-narrowing-conversions,
cppcoreguidelines-pro-type-member-init,
cppcoreguidelines-pro-type-static-cast-downcast,
cppcoreguidelines-slicing,
google-default-arguments,
google-explicit-constructor,
google-runtime-operator,
hicpp-exception-baseclass,
hicpp-multiway-paths-covered,
misc-misplaced-const,
misc-new-delete-overloads,
misc-no-recursion,
misc-non-copyable-objects,
misc-throw-by-value-catch-by-reference,
misc-unconventional-assign-operator,
misc-uniqueptr-reset-release,
modernize-avoid-bind,
modernize-concat-nested-namespaces,
modernize-deprecated-headers,
modernize-deprecated-ios-base-aliases,
modernize-loop-convert,
modernize-make-shared,
modernize-make-unique,
modernize-pass-by-value,
modernize-raw-string-literal,
modernize-redundant-void-arg,
modernize-replace-auto-ptr,
modernize-replace-disallow-copy-and-assign-macro,
modernize-replace-random-shuffle,
modernize-return-braced-init-list,
modernize-shrink-to-fit,
modernize-unary-static-assert,
modernize-use-auto,
modernize-use-bool-literals,
modernize-use-emplace,
modernize-use-equals-default,
modernize-use-equals-delete,
modernize-use-nodiscard,
modernize-use-noexcept,
modernize-use-nullptr,
modernize-use-override,
modernize-use-transparent-functors,
modernize-use-uncaught-exceptions,
mpi-buffer-deref,
mpi-type-mismatch,
openmp-use-default-none,
performance-faster-string-find,
performance-for-range-copy,
performance-implicit-conversion-in-loop,
performance-inefficient-algorithm,
performance-inefficient-string-concatenation,
performance-inefficient-vector-operation,
performance-move-const-arg,
performance-move-constructor-init,
performance-no-automatic-move,
performance-noexcept-move-constructor,
performance-trivially-destructible,
performance-type-promotion-in-math-fn,
performance-unnecessary-copy-initialization,
performance-unnecessary-value-param,
portability-simd-intrinsics,
readability-avoid-const-params-in-decls,
readability-const-return-type,
readability-container-size-empty,
readability-convert-member-functions-to-static,
readability-delete-null-pointer,
readability-deleted-default,
readability-inconsistent-declaration-parameter-name,
readability-make-member-function-const,
readability-misleading-indentation,
readability-misplaced-array-index,
readability-non-const-parameter,
readability-redundant-control-flow,
readability-redundant-declaration,
readability-redundant-function-ptr-dereference,
readability-redundant-smartptr-get,
readability-redundant-string-cstr,
readability-redundant-string-init,
readability-simplify-subscript-expr,
readability-static-accessed-through-instance,
readability-static-definition-in-anonymous-namespace,
readability-string-compare,
readability-uniqueptr-delete-release,
readability-use-anyofallof,
readability-identifier-naming'
CheckOptions:
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.StructCase: CamelCase
readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.UnionCase: CamelCase
readability-identifier-naming.TypeAliasCase: CamelCase
readability-identifier-naming.TypeAliasIgnoredRegexp: '^(const_)?(reverse_)?iterator$|^const_(reference|pointer)$|^(value|size|difference|reference|pointer)_type$'
readability-identifier-naming.UsingCase: CamelCase
readability-identifier-naming.UsingIgnoredRegexp: '^(const_)?(reverse_)?iterator$|^const_(reference|pointer)$|^(value|size|difference|reference|pointer)_type$'
readability-identifier-naming.StaticVariableCase: lower_case
readability-identifier-naming.StaticVariableSuffix: _
readability-identifier-naming.TemplateParameterCase: CamelCase
readability-identifier-naming.EnumConstantCase: lower_case
readability-identifier-naming.ConstantCase: lower_case
readability-identifier-naming.ConstexprVariableCase: lower_case
readability-identifier-naming.ClassConstantCase: lower_case
readability-identifier-naming.StaticConstantCase: lower_case
readability-identifier-naming.GlobalConstantCase: lower_case
readability-identifier-naming.LocalConstantCase: lower_case
readability-identifier-naming.VariableCase: lower_case
readability-identifier-naming.ParameterCase: lower_case
readability-identifier-naming.LocalVariableCase: lower_case
readability-identifier-naming.MemberCase: lower_case
readability-identifier-naming.PrivateMemberSuffix: _
readability-identifier-naming.ProtectedMemberSuffix: _
readability-identifier-naming.FunctionCase: lower_case
readability-identifier-naming.ClassMethodCase: lower_case
readability-identifier-naming.GlobalFunctionCase: lower_case
readability-identifier-naming.NamespaceCase: lower_case
readability-identifier-naming.MacroDefinitionIgnoredRegexp: '.*'
# Qt and third-party (OFX) virtual overrides / framework callbacks keep
# their original names — renaming them would break the override.
readability-identifier-naming.FunctionIgnoredRegexp: '^(.*Event|eventFilter|sizeHint|minimumSizeHint|heightForWidth|hasHeightForWidth|initializeGL|resizeGL|paintGL|readData|writeData|readLineData|itemChange|boundingRect|sceneEvent|drawForeground|drawBackground|createEditor|setEditorData|setModelData|updateEditorGeometry|editorEvent|canFetchMore|fetchMore|mimeData|mimeTypes|dropMimeData|canDropMimeData|supportedDropActions|supportedDragActions|roleNames|connectNotify|disconnectNotify|initStyleOption|isSequential|showPopup|hidePopup|inputMethodQuery|viewportEvent|scrollContentsBy|updateGeometries|keyboardSearch|startDrag|viewOptions|setSelection|currentChanged|selectionChanged|createWidget|deleteWidget|createMimeDataFromSelection|canInsertFromMimeData|insertFromMimeData|dropIndicatorPosition|qHash|get[A-Z].*|set[A-Z].*|can[A-Z].*|calc[A-Z].*|is[A-Z].*|.*Action|new.*|addParam|multiThread.*|mutex.*|timeLine.*|editBegin|editEnd|freeMem|copyFrom|deleteKey|deleteAllKeys|makeDescriptor|initDescriptor|initParamDescriptor|loadFromPlugin|examineOutArgs|paramChangedByPlugin|saveXML|verifyMagic|pluginSupported|loadingStatus|confirmPlugin|callEntry|mainEntry|deriveV|integrateV|getV|setV|swapBuffers|loadTexture|flushOpenGLResources|clearPersistentMessage|progressStart|progressEnd|progressUpdate|beginXmlParsing|endXmlParsing|xmlCharacterHandler|xmlElementBegin|xmlElementEnd)$'
readability-identifier-naming.ClassMethodIgnoredRegexp: '^(.*Event|eventFilter|sizeHint|minimumSizeHint|heightForWidth|hasHeightForWidth|initializeGL|resizeGL|paintGL|readData|writeData|readLineData|itemChange|boundingRect|sceneEvent|drawForeground|drawBackground|createEditor|setEditorData|setModelData|updateEditorGeometry|editorEvent|canFetchMore|fetchMore|mimeData|mimeTypes|dropMimeData|canDropMimeData|supportedDropActions|supportedDragActions|roleNames|connectNotify|disconnectNotify|initStyleOption|isSequential|showPopup|hidePopup|inputMethodQuery|viewportEvent|scrollContentsBy|updateGeometries|keyboardSearch|startDrag|viewOptions|setSelection|currentChanged|selectionChanged|createWidget|deleteWidget|createMimeDataFromSelection|canInsertFromMimeData|insertFromMimeData|dropIndicatorPosition|get[A-Z].*|set[A-Z].*|can[A-Z].*|calc[A-Z].*|is[A-Z].*|.*Action|new.*|addParam|multiThread.*|mutex.*|timeLine.*|editBegin|editEnd|freeMem|copyFrom|deleteKey|deleteAllKeys|makeDescriptor|initDescriptor|initParamDescriptor|loadFromPlugin|examineOutArgs|paramChangedByPlugin|saveXML|verifyMagic|pluginSupported|loadingStatus|confirmPlugin|callEntry|mainEntry|deriveV|integrateV|getV|setV|swapBuffers|loadTexture|flushOpenGLResources|clearPersistentMessage|progressStart|progressEnd|progressUpdate|beginXmlParsing|endXmlParsing|xmlCharacterHandler|xmlElementBegin|xmlElementEnd)$'
+11
View File
@@ -6,3 +6,14 @@
*.cpp text eol=lf
*.sh text eol=lf
*.desktop text eol=lf
# Golden/project data files are byte-compared by tests — keep LF on
# Windows checkouts too (text=auto would hand out CRLF there).
*.json text eol=lf
*.ove text eol=lf
# Workflow manifests: a CRLF ci.yml turns every step command into
# "cmd\r", and bash then cannot find the referenced scripts on the
# Windows runner ("No such file or directory").
*.yml text eol=lf
*.yaml text eol=lf
+10
View File
@@ -0,0 +1,10 @@
# actionlint configuration: whitelist the custom self-hosted runner labels
# used by the CI/CD workflows (Warp.dev runners on GitHub; the oak Gitea
# instance's runners) so `actionlint ci.yml` / `actionlint cd.yml` passes.
# See https://github.com/rhysd/actionlint/blob/main/docs/config.md
self-hosted-runner:
labels:
- ubuntu-latest
- windows-latest
- oak-ubuntu-2404
- oak-windows-2025
+446
View File
@@ -0,0 +1,446 @@
name: CD
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
# ------------------------------------------------------------------
# Linux: deb + AppImage + pacman in one job. cargo-packager does not
# support rpm (its format list is deb/appimage/pacman/nsis/dmg/app/wix),
# and its "pacman" format emits a PKGBUILD + source tarball rather than a
# compiled pkg.tar.zst — both are upstream limitations.
# ------------------------------------------------------------------
# ------------------------------------------------------------------
# Linux: one native package per distro, each built INSIDE that
# distro's container so the declared dependencies always resolve to
# the distro's own package names (dpkg-shlibdeps / rpmbuild
# auto-requires / Arch static base list). deb: hand-rolled dpkg-deb;
# rpm: rpmbuild; arch: makepkg. AppImage stays on the Ubuntu runner
# (self-contained by design).
# ------------------------------------------------------------------
linux:
name: Linux packages (${{ matrix.distro }})
runs-on: warp-ubuntu-latest-x64-8x
container: ${{ matrix.image }}
strategy:
fail-fast: false
matrix:
include:
- distro: debian
image: debian:12
- distro: fedora
image: fedora:41
- distro: arch
image: archlinux:latest
steps:
# git/curl must land BEFORE actions/checkout runs inside the
# container.
- name: Install git and fetch tools
run: |
case "${{ matrix.distro }}" in
debian) apt-get update && apt-get install -y git curl ;;
fedora) dnf install -y git curl ;;
arch) pacman -Sy --noconfirm git curl ;;
esac
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@stable
- name: Install system dependencies
run: |
case "${{ matrix.distro }}" in
debian)
apt-get install -y \
build-essential cmake pkg-config nasm dpkg-dev \
libpipewire-0.3-dev libspa-0.2-dev libjack-jackd2-dev \
libasound2-dev libpulse-dev libsndfile1-dev \
libgl1-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers \
libvulkan-dev libxkbcommon-dev libxkbcommon-x11-dev \
librsvg2-bin
;;
fedora)
dnf install -y \
gcc gcc-c++ cmake pkgconf-pkg-config nasm \
pipewire-devel jack-audio-connection-kit-devel \
alsa-lib-devel pulseaudio-libs-devel libsndfile-devel \
mesa-libGL-devel mesa-vulkan-drivers \
vulkan-headers vulkan-loader-devel \
libxkbcommon-devel libxkbcommon-x11-devel \
rpm-build librsvg2-tools
;;
arch)
pacman -S --needed --noconfirm \
base-devel cmake pkgconf nasm \
pipewire jack2 alsa-lib libpulse libsndfile \
mesa vulkan-headers vulkan-icd-loader \
libxkbcommon libxkbcommon-x11 librsvg
;;
esac
- name: Configure build environment
run: |
# OCIO builds from the ocio-sys vendored source (static) on
# Linux; the distro packages are too old for the bridge.
echo "OCIO_RS_ENABLE_REAL=1" >> "$GITHUB_ENV"
echo "OCIO_RS_LINK=static" >> "$GITHUB_ENV"
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache cargo artifacts)
# uses: Swatinem/rust-cache@v2
# with:
# shared-key: oak-${{ matrix.distro }}
# cache-on-failure: true
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache project FFmpeg)
# uses: actions/cache@v4
# with:
# path: .cache/ffmpeg
# key: ffmpeg-${{ matrix.distro }}-${{ hashFiles('tooling/ffmpeg/build-ffmpeg.sh') }}
- name: Build project FFmpeg (static, GPL + free codecs + hwaccel)
run: tooling/ffmpeg/build-ffmpeg.sh
- name: Build (release)
run: cargo build --release --locked
- name: Generate app icon (PNG from Oak_Icon.svg)
run: rsvg-convert -w 512 -h 512 Oak_Icon.svg -o icons/icon.png
- name: Package
run: |
set -euo pipefail
# The release version lives in [workspace.package] of the root
# Cargo.toml (single source of truth; tags do not carry it).
VERSION=$(sed -n '/^\[workspace\.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
case "${{ matrix.distro }}" in
debian) tooling/package/build-deb.sh "$VERSION" ;;
fedora) tooling/package/build-rpm.sh "$VERSION" ;;
arch) tooling/package/build-pkg.sh "$VERSION" ;;
esac
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-linux-${{ matrix.distro }}
path: |
target/release/*.deb
target/release/*.rpm
target/release/*.pkg.tar.zst
if-no-files-found: error
# ------------------------------------------------------------------
# AppImage (self-contained; cargo-packager on the Ubuntu runner).
# ------------------------------------------------------------------
appimage:
name: Linux AppImage
runs-on: warp-ubuntu-latest-x64-8x
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@stable
- name: Install system dependencies
run: |
tooling/install-deps.sh
sudo apt-get install -y \
cmake librsvg2-bin \
libpipewire-0.3-dev libspa-0.2-dev libjack-jackd2-dev \
libasound2-dev libpulse-dev libsndfile1-dev \
libgl1-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers \
libvulkan-dev libxkbcommon-dev libxkbcommon-x11-dev
- name: Configure build environment
run: |
echo "OCIO_RS_ENABLE_REAL=1" >> "$GITHUB_ENV"
echo "OCIO_RS_LINK=static" >> "$GITHUB_ENV"
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache cargo artifacts)
# uses: Swatinem/rust-cache@v2
# with:
# shared-key: oak-appimage
# cache-on-failure: true
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache project FFmpeg)
# uses: actions/cache@v4
# with:
# path: .cache/ffmpeg
# key: ffmpeg-appimage-${{ hashFiles('tooling/ffmpeg/build-ffmpeg.sh') }}
- name: Build project FFmpeg (static, GPL + free codecs + hwaccel)
run: tooling/ffmpeg/build-ffmpeg.sh
- name: Install cargo-packager
run: cargo install cargo-packager --locked
- name: Generate app icon (PNG from Oak_Icon.svg)
run: |
mkdir -p icons
rsvg-convert -w 512 -h 512 Oak_Icon.svg -o icons/icon.png
file icons/icon.png
- name: Build (release)
run: cargo build --release --locked
- name: Package (AppImage)
run: cargo packager --release --formats appimage
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-linux-appimage
path: target/release/*.AppImage
if-no-files-found: error
macos:
name: macOS DMG (Apple Silicon)
runs-on: warp-macos-15-arm64-6x
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@stable
- name: Install system dependencies
run: |
tooling/install-deps.sh
brew install cmake librsvg
- name: Configure build environment
run: |
{
# OCIO comes from the ocio-sys vendored source build (same on
# every platform); no OCIO_INSTALL_DIR override.
echo "OCIO_RS_ENABLE_REAL=1"
echo "OCIO_RS_LINK=static"
echo "CFLAGS=-I/opt/homebrew/include"
echo "LDFLAGS=-L/opt/homebrew/lib"
echo "PKG_CONFIG_PATH=/opt/homebrew/lib/pkgconfig/openjpeg"
} >> "$GITHUB_ENV"
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache cargo artifacts)
# uses: Swatinem/rust-cache@v2
# with:
# shared-key: oak-workspace
# cache-on-failure: true
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache project FFmpeg)
# uses: actions/cache@v4
# with:
# path: .cache/ffmpeg
# key: ffmpeg-${{ runner.os }}-${{ hashFiles('tooling/ffmpeg/build-ffmpeg.sh') }}
- name: Build project FFmpeg (static, GPL + free codecs + hwaccel)
run: |
tooling/ffmpeg/build-ffmpeg.sh
echo "FFMPEG_DIR=$PWD/.cache/ffmpeg" >> "$GITHUB_ENV"
- name: Install cargo-packager
run: cargo install cargo-packager --locked
- name: Generate app icon (PNG from Oak_Icon.svg)
run: |
mkdir -p icons
# cargo-packager's tauri-icns 0.1.0 maps only 512x512@1x (and
# 1024x1024@2x); a plain 1024x1024 PNG aborts with "No matching
# IconType", so render 512x512.
rsvg-convert -w 512 -h 512 Oak_Icon.svg -o icons/icon.png
file icons/icon.png
# Build the packaged binaries (default members: the app, oak-cli,
# oak-worker).
- name: Build (release)
run: cargo build --release --locked
- name: Package .app bundle
run: cargo packager --release --formats app
# Pull the Homebrew dylibs the binaries reference into
# Contents/Frameworks and rewrite install names to
# @executable_path-relative (the script ad-hoc re-signs the bundle).
- name: Bundle dylibs into the .app
run: tooling/package/bundle-dylibs-macos.sh target/release/Oak.app
- name: Create DMG
run: |
rm -rf dmg-staging
mkdir -p dmg-staging
cp -R target/release/Oak.app dmg-staging/
ln -s /Applications dmg-staging/Applications
hdiutil create -volname "Oak Video Editor" \
-srcfolder dmg-staging -ov -format UDZO Oak-macOS-arm64.dmg
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-macos
path: Oak-macOS-arm64.dmg
if-no-files-found: error
# ------------------------------------------------------------------
# Windows: NSIS installer (restored; cargo-packager downloads its own
# makensis, SHA-1 verified). The obsolete `-p oakengine` cdylib prebuild
# from before M14 R4 is dropped — no packaged binary links the cdylib.
# ------------------------------------------------------------------
windows:
name: Windows installer (NSIS)
runs-on: warp-windows-latest-x64-16x
defaults:
run:
shell: msys2 {0}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@stable
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
# MSYS2's own Rust targets x86_64-pc-windows-gnu by default —
# the Windows build is GNU-target (the MSVC linker rejects the
# Unix-style link args the build scripts emit).
install: >-
git
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-rust
- name: Install system dependencies
run: |
bash tooling/install-deps.sh
pacman -S --needed --noconfirm \
mingw-w64-ucrt-x86_64-cmake \
mingw-w64-ucrt-x86_64-opencolorio \
mingw-w64-ucrt-x86_64-librsvg
- name: Configure build environment
run: |
# Windows uses the MSYS2 OpenColorIO package (the exact 2.5.2 the
# bridge targets; the vendored source needs MSVC-only constructs).
# Dynamic; the DLLs are packaged next to the binaries.
echo "OCIO_RS_ENABLE_REAL=1" >> "$GITHUB_ENV"
echo "OCIO_INSTALL_DIR=/ucrt64" >> "$GITHUB_ENV"
echo "OCIO_RS_LINK=dynamic" >> "$GITHUB_ENV"
# ocio-sys' build.rs force-adds the MSVC + Windows SDK include
# dirs on Windows (meant for MSVC hosts); with the GNU toolchain
# that drags MSVC-only headers into the g++ compile. Unpack the
# crate and gate that block behind OCIO_RS_NO_MSVC_INCLUDES.
echo "OCIO_RS_NO_MSVC_INCLUDES=1" >> "$GITHUB_ENV"
CH=$(cygpath -u "${CARGO_HOME:-$HOME/.cargo}")
cargo fetch --locked
for cache in "$CH"/registry/cache/*/; do
src="$CH/registry/src/$(basename "$cache")"
mkdir -p "$src"
[ -f "$cache/ocio-sys-0.2.1.crate" ] && tar xzf "$cache/ocio-sys-0.2.1.crate" -C "$src"
done
BS=$(ls "$CH"/registry/src/*/ocio-sys-0.2.1/build.rs)
grep -q 'OCIO_RS_NO_MSVC_INCLUDES' "$BS" || sed -i \
's|if cfg!(target_os = "windows") && has_real_ocio {|if cfg!(target_os = "windows") \&\& has_real_ocio \&\& std::env::var_os("OCIO_RS_NO_MSVC_INCLUDES").is_none() {|' \
"$BS"
grep -q 'OCIO_RS_NO_MSVC_INCLUDES' "$BS"
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache cargo artifacts)
# uses: Swatinem/rust-cache@v2
# with:
# shared-key: oak-workspace
# cache-on-failure: true
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache project FFmpeg)
# uses: actions/cache@v4
# with:
# path: .cache/ffmpeg
# key: ffmpeg-${{ runner.os }}-${{ hashFiles('tooling/ffmpeg/build-ffmpeg.sh') }}
- name: Build project FFmpeg (static, GPL + free codecs + hwaccel)
run: |
bash tooling/ffmpeg/build-ffmpeg.sh
echo "FFMPEG_DIR=$(cygpath -m "$PWD/.cache/ffmpeg")" >> "$GITHUB_ENV"
- name: Install cargo-packager
run: cargo install cargo-packager --locked
- name: Generate app icon (PNG from Oak_Icon.svg)
run: |
mkdir -p icons
rsvg-convert -w 512 -h 512 Oak_Icon.svg -o icons/icon.png
- name: Build (release)
run: |
# Clear the job-hook-injected MSVC INCLUDE/LIB before the GNU
# build (they poison the MinGW compiles with MSVC SDK headers).
unset INCLUDE LIB
cargo build --release --locked
# Collect the MSYS2 runtime DLLs (libstdc++/libgcc/OpenColorIO/...)
# into target/pkg/win-dlls; the packager `resources` glob then
# installs them next to the executables.
- name: Bundle runtime DLLs
run: |
unset INCLUDE LIB
pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-ntldd
tooling/package/bundle-dylibs-windows.sh target/pkg/win-dlls \
target/release/oak-editor.exe target/release/oak-cli.exe \
target/release/oak-worker.exe
- name: Package (NSIS)
run: cargo packager --release --formats nsis
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-windows
path: target/release/*-setup.exe
if-no-files-found: error
# ------------------------------------------------------------------
# Publish: attach every platform package to the v* tag's GitHub release
# (skipped on workflow_dispatch, which only uploads artifacts).
# ------------------------------------------------------------------
release:
name: Publish GitHub release
needs: [linux, appimage, macos, windows]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: warp-ubuntu-latest-x64-8x
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
files: artifacts/*
+299
View File
@@ -0,0 +1,299 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
linux:
name: Build & test (Linux)
runs-on: oak-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# gpui/ is a git submodule; its crates are workspace members of
# their own repo and build as path dependencies of oakapp.
submodules: true
# ------------------------------------------------------------------
# System dependencies
# ------------------------------------------------------------------
- name: Install system dependencies
run: |
# Codec/filter libraries for the ffmpeg-sys-next build feature
# (see tooling/install-deps.sh).
tooling/install-deps.sh
# cmake/make for the vendored OpenColorIO build (ocio-sys
# `bundled`; Ubuntu's libopencolorio-dev is 2.1, older than the
# bridge's API floor) plus the headless test infra gpui needs:
# X11, software Mesa Vulkan (lavapipe) and xvfb.
sudo apt-get install -y \
cmake \
libpipewire-0.3-dev libspa-0.2-dev libjack-jackd2-dev \
libasound2-dev libpulse-dev libsndfile1-dev \
libgl1-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers \
libvulkan-dev libxkbcommon-dev libxkbcommon-x11-dev xvfb build-essential clang libclang-dev
# ------------------------------------------------------------------
# Build environment
# ------------------------------------------------------------------
# ocio-sys builds a stub bridge unless these are set; the oak-common
# ocioutils tests need the real library (see crates/oak-common/.cargo/
# config.toml, which only applies to builds run from that directory).
# ocio-sys builds its vendored OpenColorIO from source on Linux (the
# `bundled` feature; the distro package is too old for the bridge),
# so no OCIO_INSTALL_DIR here.
- name: Configure build environment
run: |
echo "OCIO_RS_ENABLE_REAL=1" >> "$GITHUB_ENV"
echo "OCIO_RS_LINK=static" >> "$GITHUB_ENV"
echo "RUSTUP_HOME=/opt/rust/rustup" >> "$GITHUB_ENV"
echo "CARGO_HOME=/opt/rust/cargo" >> "$GITHUB_ENV"
echo "PATH=$PATH:/opt/rust/cargo/bin" >> "$GITHUB_ENV"
# ------------------------------------------------------------------
# Caches
# ------------------------------------------------------------------
# Covers the whole target/ dir plus ~/.cargo; shared across branches
# of the same OS.
# TEMP: cache disabled ntil the Gitea instance cache is provisioned (Cache cargo artifacts)
# uses: Swatinem/rust-cache@v2
# with:
# shared-key: oak-workspace
# cache-on-failure: true
# The project FFmpeg (release/8.0, static, all free codecs + hwaccel)
# is built by tooling/ffmpeg/build-ffmpeg.sh — 10-20 min on a cold
# cache. It does not depend on the Rust toolchain, so key it on the
# script itself and keep it out of rust-cache.
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache project FFmpeg)
# uses: actions/cache@v4
# with:
# path: .cache/ffmpeg
# key: ffmpeg-${{ runner.os }}-${{ hashFiles('tooling/ffmpeg/build-ffmpeg.sh') }}
# ------------------------------------------------------------------
# Project FFmpeg (script + FFMPEG_DIR; see docs/build.md)
# ------------------------------------------------------------------
- name: Build project FFmpeg
run: |
tooling/ffmpeg/build-ffmpeg.sh
echo "FFMPEG_DIR=$PWD/.cache/ffmpeg" >> "$GITHUB_ENV"
# ------------------------------------------------------------------
# Build & test
# ------------------------------------------------------------------
- name: Build
run: cargo build --workspace --locked
# xvfb + 24-bit screen: the gpui #[gpui::test] tests open real windows
# and render through wgpu on Mesa's software Vulkan (lavapipe).
# The watchdog bounds the step: a deadlocked test produces no output
# and no failure, so after 1500 s (a green run needs ~4 min) it dumps
# every hung process's thread stacks and kills the suite.
- name: Test
run: |
sudo apt-get install -y gdb
xvfb-run -a -s "-screen 0 1920x1080x24" cargo test --workspace --locked &
TEST_PID=$!
(
sleep 1500
echo "::warning::test suite exceeded 1500s; dumping hung-process stacks"
for p in $(pgrep -f 'target/debug/deps/|target/debug/oak-worker'); do
echo "===== thread stacks of pid $p ($(readlink /proc/$p/exe 2>/dev/null)) ====="
sudo gdb -batch -ex 'thread apply all bt' -p "$p" || true
done
pkill -9 -f 'target/debug/deps/' || true
pkill -9 -f 'target/debug/oak-worker' || true
) &
WATCHDOG_PID=$!
wait $TEST_PID
rc=$?
kill $WATCHDOG_PID 2>/dev/null || true
exit $rc
# A crashing (SIGSEGV) test gives no Rust backtrace; rerun the
# crashing test binaries under gdb to capture the native stack.
# `--args` is required — plain `--` makes gdb treat the test args as
# a core file. The extra probes target loader-stage crashes (the
# copier_test SIGSEGV happens inside ld.so's dl_main): si_addr/si_code
# pin down the fault type, the dynsym dump exposes symbols the
# executable exports for interposition, strace shows the last loader
# syscalls, and valgrind catches a corrupting static initializer.
- name: Backtrace on test failure
if: failure()
run: |
sudo apt-get install -y gdb strace valgrind
for name in node_e2e_test suites_test copier_test; do
BIN=$(ls -t target/debug/deps/$name-* | grep -v '\.d$' | head -1)
[ -n "$BIN" ] || continue
echo "===== $BIN ====="
file "$BIN" || true
echo "--- exported defined dynsyms:"
readelf --dyn-syms -W "$BIN" 2>/dev/null | grep -v ' UND ' | tail -n +4 | head -30 || true
echo "--- strace tail:"
strace -f "$BIN" --list 2>&1 | tail -15 || true
echo "--- valgrind tail:"
valgrind -q "$BIN" --list 2>&1 | tail -25 || true
echo "--- gdb:"
xvfb-run -a gdb -batch \
-ex run \
-ex 'bt' \
-ex 'p $_siginfo.si_code' \
-ex 'p/x $_siginfo._sifields._sigfault.si_addr' \
-ex 'x/6i $rip' \
--args "$BIN" --nocapture || true
done
# ------------------------------------------------------------------
# OFX plugin discovery end-to-end
# ------------------------------------------------------------------
# Build a minimal but real OFX plugin into a .ofx.bundle, point
# OFX_PLUGIN_PATH at it and let the scan_probe example run the full
# host path (directory scan -> dlopen -> setHost -> load -> describe
# -> register). The assertion is the plugin's registration line; CI
# machines have no system-wide OFX plugins, so the fixture is the
# only discovery.
- name: Build OFX fixture plugin
run: crates/oak-plugin/tests/fixtures/build_fixture.sh .cache/ofx-fixture
- name: Probe OFX plugin discovery
run: |
OFX_PLUGIN_PATH="$PWD/.cache/ofx-fixture" \
cargo run --locked -p oak-plugin --example scan_probe > probe.log 2>&1
grep -q 'type_id=rs.oak.CiTestPlugin' probe.log
# A project carrying a plugin node must survive save/load (the
# serializer resolves plugin types via the dynamic factory).
OAK_OFX_FIXTURE_DIR="$PWD/.cache/ofx-fixtre" \
cargo test --locked -p oak-plugin --test ofx_roundtrip
windows:
name: Build & test (Windows)
runs-on: oak-windows-2025
defaults:
run:
shell: msys2 {0}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# gpui/ is a git submodule; its crates are workspace members of
# their own repo and build as path dependencies of oakapp.
submodules: true
# ------------------------------------------------------------------
# System dependencies
# ------------------------------------------------------------------
- name: Install system dependencies
run: |
cd "$GITHUB_WORKSPACE"
bash ./tooling/install-deps.sh
pacman -S --needed --noconfirm \
mingw-w64-ucrt-x86_64-cmake \
mingw-w64-ucrt-x86_64-opencolorio
# ------------------------------------------------------------------
# Build environment
# ------------------------------------------------------------------
# Windows uses the MSYS2 OpenColorIO package (the exact 2.5.2 the
# bridge targets; the vendored source needs MSVC-only constructs).
# Dynamic here — the CD packages the DLLs next to the binaries.
- name: Configure build environment
run: |
cd "$GITHUB_WORKSPACE"
echo "OCIO_RS_ENABLE_REAL=1" >> "$GITHUB_ENV"
echo "OCIO_INSTALL_DIR=/ucrt64" >> "$GITHUB_ENV"
echo "OCIO_RS_LINK=dynamic" >> "$GITHUB_ENV"
# ocio-sys' build.rs force-adds the MSVC + Windows SDK include
# dirs on Windows (meant for MSVC hosts); with the GNU toolchain
# that drags MSVC-only headers into the g++ compile. Unpack the
# crate and gate that block behind OCIO_RS_NO_MSVC_INCLUDES.
echo "OCIO_RS_NO_MSVC_INCLUDES=1" >> "$GITHUB_ENV"
CH=$(cygpath -u "${CARGO_HOME:-$HOME/.cargo}")
cargo fetch --locked
for cache in "$CH"/registry/cache/*/; do
src="$CH/registry/src/$(basename "$cache")"
mkdir -p "$src"
[ -f "$cache/ocio-sys-0.2.1.crate" ] && tar xzf "$cache/ocio-sys-0.2.1.crate" -C "$src"
done
BS=$(ls "$CH"/registry/src/*/ocio-sys-0.2.1/build.rs)
grep -q 'OCIO_RS_NO_MSVC_INCLUDES' "$BS" || sed -i \
's|if cfg!(target_os = "windows") && has_real_ocio {|if cfg!(target_os = "windows") \&\& has_real_ocio \&\& std::env::var_os("OCIO_RS_NO_MSVC_INCLUDES").is_none() {|' \
"$BS"
grep -q 'OCIO_RS_NO_MSVC_INCLUDES' "$BS"
# ------------------------------------------------------------------
# Caches
# ------------------------------------------------------------------
# Covers the whole target/ dir plus ~/.cargo; shared across branches
# of the same OS.
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache cargo artifacts)
# uses: Swatinem/rust-cache@v2
# with:
# shared-key: oak-workspace
# cache-on-failure: true
# The project FFmpeg (release/8.0, static, all free codecs + hwaccel)
# is built by tooling/ffmpeg/build-ffmpeg.sh — 10-20 min on a cold
# cache. It does not depend on the Rust toolchain, so key it on the
# script itself and keep it out of rust-cache.
# TEMP: cache disabled until the Gitea instance cache is provisioned (Cache project FFmpeg)
# uses: actions/cache@v4
# with:
# path: .cache/ffmpeg
# key: ffmpeg-${{ runner.os }}-${{ hashFiles('tooling/ffmpeg/build-ffmpeg.sh') }}
# ------------------------------------------------------------------
# Project FFmpeg (script + FFMPEG_DIR; see docs/build.md)
# ------------------------------------------------------------------
- name: Build project FFmpeg
run: |
cd "$GITHUB_WORKSPACE"
bash tooling/ffmpeg/build-ffmpeg.sh
echo "FFMPEG_DIR=$(cygpath -m "$PWD/.cache/ffmpeg")" >> "$GITHUB_ENV"
# ------------------------------------------------------------------
# Build & test
# ------------------------------------------------------------------
- name: Build
run: |
cd "$GITHUB_WORKSPACE"
# The runner's job hook injects the MSVC INCLUDE/LIB into every
# step; clear them in-step (they poison the MinGW compiles with
# MSVC SDK headers).
unset INCLUDE LIB
# mingw-w64 >= Nov 2025 forwards _assert to __msvcrt_assert inside
# libmingwex.a; rustc's link order puts -lmingwex last, so any
# binary that pulls _assert.o leaves _fileno/_setmode/
# __imp___msvcrt_assert unresolved. A trailing -lmsvcrt re-scans
# the CRT import lib after libmingwex.
export RUSTFLAGS="-C link-args=-lmsvcrt"
cargo build --workspace --locked
- name: Test
run: |
cd "$GITHUB_WORKSPACE"
unset INCLUDE LIB
# See Build (Windows): trailing -lmsvcrt for the mingw-w64
# _assert/__msvcrt_assert link-order breakage.
export RUSTFLAGS="-C link-args=-lmsvcrt"
if ! cargo test --workspace --locked; then
# Retry once: a few gpui keystroke tests flake on Windows CI —
# a synthetic keystroke is occasionally never delivered (the
# undo/redo pair and a plain 's' toggle both failed once,
# each identically to its pass state). A real regression
# fails both passes.
echo "first pass failed; retrying once for gpui keystroke flakes"
cargo test --workspace --locked
fi
-616
View File
@@ -1,616 +0,0 @@
name: CD
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
# ------------------------------------------------------------------
# Windows Installer (MSYS2)
# ------------------------------------------------------------------
windows:
runs-on: warp-windows-latest-x64-32x
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
git
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-qt6-base
mingw-w64-ucrt-x86_64-qt6-tools
mingw-w64-ucrt-x86_64-ffmpeg
mingw-w64-ucrt-x86_64-openimageio
mingw-w64-ucrt-x86_64-opencolorio
mingw-w64-ucrt-x86_64-openexr
mingw-w64-ucrt-x86_64-fmt
mingw-w64-ucrt-x86_64-expat
mingw-w64-ucrt-x86_64-portaudio
mingw-w64-ucrt-x86_64-vulkan-headers
mingw-w64-ucrt-x86_64-vulkan-loader
mingw-w64-ucrt-x86_64-nsis
mingw-w64-ucrt-x86_64-ntldd
- name: Build OpenTimelineIO
shell: msys2 {0}
run: |
git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git
cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \
-DOTIO_SHARED_LIBS=ON \
-DOTIO_PYTHON_BINDINGS=OFF \
-DOTIO_FIND_IMATH=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/otio-install"
cmake --build OpenTimelineIO/build
cmake --install OpenTimelineIO/build
- name: Configure
shell: msys2 {0}
run: |
cmake -S . -B build -G Ninja \
-DBUILD_QT6=ON \
-DOTIO_LOCATION="${PWD}/otio-install" \
-DCMAKE_BUILD_TYPE=Release
- name: Build
shell: msys2 {0}
run: cmake --build build --config Release
- name: Deploy dependencies
shell: msys2 {0}
run: |
# Let ntldd discover the OTIO DLLs (no rpath on Windows)
export PATH="${GITHUB_WORKSPACE}/otio-install/bin:${PATH}"
mkdir -p app/packaging/windows/nsis/oak-editor
cp build/app/oak-editor.exe app/packaging/windows/nsis/oak-editor/
cp build/worker/oak-render-worker.exe app/packaging/windows/nsis/oak-editor/
cp build/app/oakgl.dll app/packaging/windows/nsis/oak-editor/
if [ -f build/app/oakvulkan.dll ]; then
cp build/app/oakvulkan.dll app/packaging/windows/nsis/oak-editor/
fi
# The FFmpeg bridge DLL is built outside app/ and is not reachable
# via PATH, so ntldd cannot discover it from the executables alone
cp build/ffmpeg_bridge/bin/ffmpeg_bridge.dll app/packaging/windows/nsis/oak-editor/
# Same for the liboakcore DLL
cp build/core/oakcore.dll app/packaging/windows/nsis/oak-editor/
# Same for the liboakengine DLL
cp build/engine/oakengine.dll app/packaging/windows/nsis/oak-editor/
windeployqt6 app/packaging/windows/nsis/oak-editor/oak-editor.exe
# Copy all non-Qt MSYS2 DLLs recursively for every binary we ship
cd app/packaging/windows/nsis/oak-editor
for binary in oak-editor.exe oak-render-worker.exe oakgl.dll oakvulkan.dll ffmpeg_bridge.dll oakcore.dll oakengine.dll; do
[ -f "$binary" ] || continue
for l in $(ntldd -R "$binary" | grep -E 'mingw64|ucrt64|clang64' | sed 's/^[ \t]*//' | cut -d' ' -f3); do
cp -v "$l" .
done
done
- name: Build installer
shell: msys2 {0}
run: |
cd app/packaging/windows/nsis
cp "${GITHUB_WORKSPACE}"/LICENSE .
makensis oak.nsi
mv setup.exe "${GITHUB_WORKSPACE}"/Oak-Video-Editor-Windows-x64.exe
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-windows-installer
path: Oak-Video-Editor-Windows-x64.exe
# ------------------------------------------------------------------
# macOS DMG
# ------------------------------------------------------------------
macos:
runs-on: warp-macos-15-arm64-12x
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
run: |
brew update
brew install cmake ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat molten-vk vulkan-headers vulkan-loader dylibbundler
- name: Build OpenTimelineIO
run: |
git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git
cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \
-DOTIO_SHARED_LIBS=ON \
-DOTIO_PYTHON_BINDINGS=OFF \
-DOTIO_FIND_IMATH=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/otio-install"
cmake --build OpenTimelineIO/build
cmake --install OpenTimelineIO/build
- name: Configure
run: |
export PATH="$(brew --prefix qt@6)/bin:$PATH"
export CMAKE_PREFIX_PATH="$(brew --prefix qt@6)"
# CMake's FindVulkan locates the Homebrew loader via VULKAN_SDK
export VULKAN_SDK="$(brew --prefix vulkan-loader)"
cmake -S . -B build -G Ninja \
-DBUILD_QT6=ON \
-DCMAKE_BUILD_TYPE=Release \
-DOTIO_LOCATION="${PWD}/otio-install" \
-DOCIO_LOCATION="$(brew --prefix opencolorio)"
- name: Build
run: cmake --build build --config Release
- name: Deploy Qt dependencies
run: |
export PATH="$(brew --prefix qt@6)/bin:$PATH"
macdeployqt build/app/Oak.app \
-verbose=2 \
-executable=build/app/Oak.app/Contents/MacOS/oak-render-worker
# Re-sign after macdeployqt (it breaks signatures when modifying libs)
codesign --force --deep --sign - build/app/Oak.app
- name: Verify macOS bundle plugins
run: |
ls -la build/app/Oak.app/Contents/PlugIns/platforms/ || (echo "Missing platform plugins!" && exit 1)
ls -la build/app/Oak.app/Contents/PlugIns/imageformats/ || true
echo "=== otool -L main binary ==="
otool -L build/app/Oak.app/Contents/MacOS/Oak | head -30
echo "=== otool -L worker ==="
otool -L build/app/Oak.app/Contents/MacOS/oak-render-worker | head -30
- name: Bundle non-Qt libraries
run: |
mkdir -p build/app/Oak.app/Contents/Frameworks
python3 << 'PYEOF'
import os, shutil, subprocess
APP = "build/app/Oak.app"
MACOS_DIR = f"{APP}/Contents/MacOS"
DEST = f"{APP}/Contents/Frameworks"
os.makedirs(DEST, exist_ok=True)
QT_PREFIXES = ("Qt", "libQt")
def is_qt_lib(name):
return name.startswith(QT_PREFIXES)
def get_rpaths(binary):
out = subprocess.run(["otool", "-l", binary], capture_output=True, text=True).stdout
rpaths = []
for i, line in enumerate(out.splitlines()):
if "cmd LC_RPATH" in line and i + 2 < len(out.splitlines()):
parts = out.splitlines()[i + 2].strip().split()
if len(parts) >= 2:
rpaths.append(parts[1])
return rpaths
def get_deps(binary):
out = subprocess.run(["otool", "-L", binary], capture_output=True, text=True).stdout
deps = []
for line in out.splitlines()[1:]:
line = line.strip()
if not line:
continue
dep = line.split()[0]
if dep.startswith("/usr/lib/") or dep.startswith("/System/"):
continue
deps.append(dep)
return deps
def resolve(ref, rpaths, loader_dir=""):
if ref.startswith("/"):
return ref if os.path.isfile(ref) else None
if ref.startswith("@rpath/"):
for rp in rpaths:
p = os.path.join(rp, ref[7:])
if os.path.isfile(p):
return p
if ref.startswith("@loader_path/") and loader_dir:
p = os.path.join(loader_dir, ref[13:])
if os.path.isfile(p):
return p
if ref.startswith("@executable_path/"):
p = os.path.join(APP, "Contents/MacOS", ref[17:])
if os.path.isfile(p):
return p
return None
PROCESSED = set()
def process(target, exec_rpaths):
tdir = os.path.dirname(target)
print(f"Processing: {target}")
for dep in get_deps(target):
# Skip Qt frameworks and Qt dylibs; macdeployqt already deploys them.
dep_base = os.path.basename(dep)
if is_qt_lib(dep_base):
continue
resolved = resolve(dep, exec_rpaths, tdir)
if not resolved or not os.path.isfile(resolved):
continue
resolved = os.path.realpath(resolved)
if not (resolved.startswith("/opt/homebrew/") or resolved.startswith("/usr/local/") or resolved.startswith(os.path.realpath(os.getcwd()))):
continue
base = os.path.basename(resolved)
if is_qt_lib(base):
continue
if base in PROCESSED:
subprocess.run(["install_name_tool", "-change", dep, f"@rpath/{base}", target], capture_output=True)
continue
PROCESSED.add(base)
dst = os.path.join(DEST, base)
if os.path.abspath(resolved) == os.path.abspath(dst):
subprocess.run(["install_name_tool", "-id", f"@rpath/{base}", dst], capture_output=True)
subprocess.run(["install_name_tool", "-change", dep, f"@rpath/{base}", target], capture_output=True)
process(dst, exec_rpaths)
continue
print(f" Copying: {resolved} -> {dst}")
shutil.copy2(resolved, dst)
subprocess.run(["install_name_tool", "-id", f"@rpath/{base}", dst], capture_output=True)
subprocess.run(["install_name_tool", "-change", dep, f"@rpath/{base}", target], capture_output=True)
process(dst, exec_rpaths)
binaries = [os.path.join(MACOS_DIR, f) for f in os.listdir(MACOS_DIR)
if os.path.isfile(os.path.join(MACOS_DIR, f))]
if not binaries:
raise RuntimeError(f"No binaries found in {MACOS_DIR}")
main_binary = os.path.join(MACOS_DIR, "Oak")
main_rpaths = get_rpaths(main_binary)
for binary in binaries:
process(binary, main_rpaths)
# Ensure every binary can find libraries in Frameworks
for rp in get_rpaths(binary):
subprocess.run(["install_name_tool", "-delete_rpath", rp, binary], capture_output=True)
subprocess.run(["install_name_tool", "-add_rpath", "@executable_path/../Frameworks", binary], capture_output=True)
subprocess.run(["codesign", "--force", "--deep", "--sign", "-", APP], check=True)
print("Done. Frameworks:")
for f in sorted(os.listdir(DEST)):
print(f" {f}")
PYEOF
- name: Debug bundle contents
run: |
echo "=== otool -L ==="
otool -L build/app/Oak.app/Contents/MacOS/Oak
echo "=== Frameworks dir ==="
ls -la build/app/Oak.app/Contents/Frameworks/ || echo "(empty)"
echo "=== App bundle size ==="
du -sh build/app/Oak.app
- name: Create DMG
run: |
mkdir -p build/app/dmg-staging
cp -R build/app/Oak.app build/app/dmg-staging/
ln -s /Applications build/app/dmg-staging/Applications
hdiutil create \
-srcfolder build/app/dmg-staging \
-volname "Oak Video Editor" \
-fs HFS+ \
-format UDZO \
Oak-Video-Editor-macOS.dmg
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-macos-dmg
path: Oak-Video-Editor-macOS.dmg
# ------------------------------------------------------------------
# Linux AppImage (Ubuntu 24.04 — distro FFmpeg 6.x)
# ------------------------------------------------------------------
appimage:
runs-on: warp-ubuntu-latest-x64-16x
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
cmake ninja-build pkg-config \
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev \
libvulkan-dev \
wget fuse3 desktop-file-utils
- name: Build OpenTimelineIO
run: |
git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git
cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \
-DOTIO_SHARED_LIBS=ON \
-DOTIO_PYTHON_BINDINGS=OFF \
-DOTIO_FIND_IMATH=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/otio-install"
cmake --build OpenTimelineIO/build
cmake --install OpenTimelineIO/build
- name: Build
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DOTIO_LOCATION="${PWD}/otio-install" \
-DBUILD_QT6=ON
cmake --build build
- name: Install to AppDir
run: |
DESTDIR="${PWD}"/AppDir cmake --install build
# Copy custom AppRun
cp app/packaging/linux/AppRun AppDir/
- name: Create AppImage
env:
APPIMAGE_EXTRACT_AND_RUN: 1
run: |
wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
wget -q "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
wget -q "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage appimagetool-x86_64.AppImage
export PATH="${PWD}:${PATH}"
DEPLOY_ARGS=""
# The Vulkan backend is dlopen'ed at runtime, so linuxdeploy cannot
# discover it (and its dependencies) from the executable alone
if [ -f AppDir/usr/lib/liboakvulkan.so ]; then
DEPLOY_ARGS="--library AppDir/usr/lib/liboakvulkan.so"
fi
./linuxdeploy-x86_64.AppImage \
--appdir AppDir \
--plugin qt \
$DEPLOY_ARGS \
--desktop-file AppDir/usr/share/applications/org.oakvideoeditor.Oak.desktop \
--icon-file AppDir/usr/share/icons/hicolor/512x512/apps/org.oakvideoeditor.Oak.png \
--output appimage
- name: Verify bundled libraries
run: |
# Ensure FFmpeg shared libraries are present inside the AppDir
ls AppDir/usr/lib/libavcodec.so* || (echo "Missing libavcodec" && exit 1)
ls AppDir/usr/lib/libavformat.so* || (echo "Missing libavformat" && exit 1)
ls AppDir/usr/lib/libavutil.so* || (echo "Missing libavutil" && exit 1)
ls AppDir/usr/lib/libswscale.so* || (echo "Missing libswscale" && exit 1)
ls AppDir/usr/lib/libswresample.so* || (echo "Missing libswresample" && exit 1)
# Ensure other runtime dependencies are present
ls AppDir/usr/lib/libOpenImageIO.so* || (echo "Missing libOpenImageIO" && exit 1)
ls AppDir/usr/lib/libOpenColorIO.so* || (echo "Missing libOpenColorIO" && exit 1)
ls AppDir/usr/lib/libportaudio.so* || (echo "Missing libportaudio" && exit 1)
ls AppDir/usr/lib/libOpenEXR*.so* || (echo "Missing libOpenEXR" && exit 1)
ls AppDir/usr/lib/libopentimelineio.so* || (echo "Missing libopentimelineio" && exit 1)
ls AppDir/usr/lib/libopentime.so* || (echo "Missing libopentime" && exit 1)
# Ensure Oak's own binaries and shared libraries are present
ls AppDir/usr/bin/oak-editor || (echo "Missing oak-editor" && exit 1)
ls AppDir/usr/bin/oak-render-worker || (echo "Missing oak-render-worker" && exit 1)
ls AppDir/usr/lib/liboakgl.so || (echo "Missing liboakgl.so" && exit 1)
ls AppDir/usr/lib/liboakvulkan.so || (echo "Missing liboakvulkan.so" && exit 1)
ls AppDir/usr/lib/libvulkan.so* || (echo "Missing libvulkan" && exit 1)
ls AppDir/usr/lib/libffmpeg_bridge.so || (echo "Missing libffmpeg_bridge.so" && exit 1)
ls AppDir/usr/lib/liboakcore.so || (echo "Missing liboakcore.so" && exit 1)
ls AppDir/usr/lib/liboakengine.so || (echo "Missing liboakengine.so" && exit 1)
# Ensure every shipped binary resolves all of its dependencies
! ldd AppDir/usr/bin/oak-editor | grep "not found"
! ldd AppDir/usr/bin/oak-render-worker | grep "not found"
! ldd AppDir/usr/lib/liboakcore.so | grep "not found"
! ldd AppDir/usr/lib/liboakengine.so | grep "not found"
! ldd AppDir/usr/lib/liboakgl.so | grep "not found"
! ldd AppDir/usr/lib/liboakvulkan.so | grep "not found"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-linux-appimage
path: Oak_Video_Editor-*.AppImage
# ------------------------------------------------------------------
# Debian package (.deb)
# ------------------------------------------------------------------
deb:
runs-on: warp-ubuntu-latest-x64-16x
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
cmake ninja-build pkg-config \
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev \
libvulkan-dev
- name: Build OpenTimelineIO
run: |
git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git
cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \
-DOTIO_SHARED_LIBS=ON \
-DOTIO_PYTHON_BINDINGS=OFF \
-DOTIO_FIND_IMATH=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/otio-install"
cmake --build OpenTimelineIO/build
cmake --install OpenTimelineIO/build
- name: Build
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DOTIO_LOCATION="${PWD}/otio-install" \
-DBUILD_QT6=ON
cmake --build build
- name: Create DEB package
run: |
cd build
cpack -G DEB
mv *.deb "${GITHUB_WORKSPACE}/"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-deb-package
path: oak-video-editor_*.deb
# ------------------------------------------------------------------
# RPM package (.rpm)
# ------------------------------------------------------------------
rpm:
runs-on: warp-ubuntu-latest-x64-16x
container:
image: fedora:latest
steps:
- name: Install Git
run: dnf install -y git
- uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
run: |
dnf install -y \
cmake ninja-build pkgconf-pkg-config \
qt6-qtbase-devel qt6-qtbase-private-devel qt6-qttools-devel \
ffmpeg-free-devel \
OpenImageIO-devel \
OpenColorIO-devel \
openexr-devel \
expat-devel \
portaudio-devel \
mesa-libGL-devel \
vulkan-headers \
vulkan-loader-devel \
libxkbcommon-devel \
gcc-c++ \
bzip2-devel \
rpm-build
- name: Build OpenTimelineIO
run: |
git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git
cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \
-DOTIO_SHARED_LIBS=ON \
-DOTIO_PYTHON_BINDINGS=OFF \
-DOTIO_FIND_IMATH=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/otio-install"
cmake --build OpenTimelineIO/build
cmake --install OpenTimelineIO/build
- name: Build
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DOTIO_LOCATION="${PWD}/otio-install" \
-DBUILD_QT6=ON
cmake --build build
- name: Create RPM package
run: |
cd build
cpack -G RPM
mv *.rpm "${GITHUB_WORKSPACE}/"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-rpm-package
path: oak-video-editor-*.rpm
# ------------------------------------------------------------------
# Arch Linux package (.pkg.tar.zst)
# ------------------------------------------------------------------
archlinux:
runs-on: warp-ubuntu-latest-x64-16x
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Prepare source tarball and PKGBUILD
run: |
VERSION="${GITHUB_REF_NAME#v}"
# Arch pkgver cannot contain hyphens
PKGVER="${VERSION//-/_}"
TARBALL="oak-video-editor-${PKGVER}.tar.gz"
tar czf "/tmp/${TARBALL}" \
--transform "s,^,oak-video-editor-${PKGVER}/," \
--exclude='.git' \
--exclude='build' \
--exclude='*.tar.gz' \
.
mv "/tmp/${TARBALL}" "${TARBALL}"
sed "s/@VERSION@/${PKGVER}/g" app/packaging/arch/PKGBUILD.in > PKGBUILD
- name: Build package in Arch Linux container
run: |
docker run --rm \
-v "${PWD}:/build" \
-w /build \
archlinux:base-devel \
bash -c '
set -e
pacman-key --init
pacman -Syu --noconfirm
pacman -S --noconfirm --needed \
cmake ninja git pkgconf \
qt6-base qt6-tools ffmpeg openimageio opencolorio openexr expat portaudio \
mesa vulkan-headers vulkan-icd-loader libxkbcommon fmt \
opentimelineio
useradd -m builder
chown -R builder /build
su builder -c "makepkg -s --noconfirm"
'
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: oak-arch-package
path: oak-video-editor-*.pkg.tar.zst
# ------------------------------------------------------------------
# Create Draft Release
# ------------------------------------------------------------------
release:
needs: [windows, macos, appimage, deb, rpm, archlinux]
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create Draft Release
uses: softprops/action-gh-release@v2
with:
draft: true
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
files: artifacts/*
-378
View File
@@ -1,378 +0,0 @@
name: CI
on:
push:
pull_request:
schedule:
# Daily at 03:17 UTC to keep ccache/dependency caches warm
- cron: '17 3 * * *'
jobs:
build-test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [warp-ubuntu-latest-x64-8x, warp-macos-15-arm64-6x, warp-windows-latest-x64-16x]
env:
CMAKE_BUILD_TYPE: Release
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 1G
# Enable OFX integration tests that require external plugin bundles
RUN_OFX_ITEST: "1"
# Enable plugin subsystem smoke tests
OAK_PLUGIN_SMOKE_TEST: "1"
steps:
- uses: actions/checkout@v4
with:
submodules: 'true'
# ------------------------------------------------------------------
# Linux dependencies
# ------------------------------------------------------------------
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
ninja-build pkg-config nasm \
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
ffmpeg \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libgl1-mesa-dri libxkbcommon-dev ccache \
xvfb libvulkan-dev mesa-vulkan-drivers vulkan-tools libshaderc-dev
# ------------------------------------------------------------------
# macOS dependencies
# ------------------------------------------------------------------
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew update
brew install ninja pkg-config ccache qt@6 ffmpeg openimageio opencolorio openexr portaudio expat molten-vk vulkan-headers
echo "$(brew --prefix qt@6)/bin" >> "$GITHUB_PATH"
echo "CMAKE_PREFIX_PATH=$(brew --prefix qt@6)" >> "$GITHUB_ENV"
# ------------------------------------------------------------------
# OpenTimelineIO (required dependency, built from source; cached)
# ------------------------------------------------------------------
- name: Cache OpenTimelineIO (Unix)
if: runner.os != 'Windows'
id: otio-cache
uses: actions/cache@v4
with:
path: otio-install
key: otio-0.16.0-${{ runner.os }}-v2
- name: Set OTIO location (Unix)
if: runner.os != 'Windows'
run: |
echo "OTIO_LOCATION=${PWD}/otio-install" >> "$GITHUB_ENV"
# With OTIO_FIND_IMATH=ON the system (brew/apt) Imath must be the
# only Imath header set in play; drop any bundled headers a stale
# cache entry may have brought back (they collide with the system
# include guards and break ImathBox.h).
rm -rf "${PWD}/otio-install/include/Imath"
- name: Build OpenTimelineIO (Unix)
if: runner.os != 'Windows' && steps.otio-cache.outputs.cache-hit != 'true'
run: |
git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git
cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \
-DOTIO_SHARED_LIBS=ON \
-DOTIO_PYTHON_BINDINGS=OFF \
-DOTIO_FIND_IMATH=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/otio-install"
cmake --build OpenTimelineIO/build
cmake --install OpenTimelineIO/build
# ------------------------------------------------------------------
# Windows dependencies (MSYS2)
# ------------------------------------------------------------------
- name: Setup MSYS2
if: runner.os == 'Windows'
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
cache: true
install: >-
git
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-qt6-base
mingw-w64-ucrt-x86_64-qt6-tools
mingw-w64-ucrt-x86_64-ffmpeg
mingw-w64-ucrt-x86_64-openimageio
mingw-w64-ucrt-x86_64-opencolorio
mingw-w64-ucrt-x86_64-openexr
mingw-w64-ucrt-x86_64-fmt
mingw-w64-ucrt-x86_64-expat
mingw-w64-ucrt-x86_64-portaudio
mingw-w64-ucrt-x86_64-vulkan-headers
mingw-w64-ucrt-x86_64-vulkan-loader
mingw-w64-ucrt-x86_64-make
mingw-w64-ucrt-x86_64-ccache
# ------------------------------------------------------------------
# OpenTimelineIO (required dependency, built from source; cached).
# Must come after Setup MSYS2 (uses the msys2 shell).
# ------------------------------------------------------------------
- name: Cache OpenTimelineIO (Windows)
if: runner.os == 'Windows'
id: otio-cache-win
uses: actions/cache@v4
with:
path: otio-install
key: otio-0.16.0-${{ runner.os }}-v2
- name: Set OTIO location (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: |
echo "OTIO_LOCATION=${PWD}/otio-install" >> "$GITHUB_ENV"
# No rpath on Windows: test executables need the OTIO DLLs on PATH.
# OTIO's MinGW install puts the DLLs in lib/, not bin/.
echo "$(cygpath -w "${PWD}/otio-install/lib")" >> "$GITHUB_PATH"
- name: Build OpenTimelineIO (Windows)
if: runner.os == 'Windows' && steps.otio-cache-win.outputs.cache-hit != 'true'
shell: msys2 {0}
run: |
git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git
cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \
-DOTIO_SHARED_LIBS=ON \
-DOTIO_PYTHON_BINDINGS=OFF \
-DOTIO_FIND_IMATH=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="${PWD}/otio-install"
cmake --build OpenTimelineIO/build
cmake --install OpenTimelineIO/build
# ------------------------------------------------------------------
# Compile cache (ccache) — restored before configure so the launcher
# flags below pick it up on all three platforms
# ------------------------------------------------------------------
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.ccache
key: ccache-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
ccache-${{ runner.os }}-${{ github.ref_name }}-
ccache-${{ runner.os }}-
# ------------------------------------------------------------------
# Configure
# ------------------------------------------------------------------
- name: Configure (Linux)
if: runner.os == 'Linux'
run: |
cmake -S . -B build -G Ninja \
-DBUILD_TESTS=ON \
-DBUILD_QT6=ON \
-DOCIO_LOCATION=/usr \
-DOTIO_LOCATION=${OTIO_LOCATION} \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
- name: Configure (macOS)
if: runner.os == 'macOS'
run: |
cmake -S . -B build -G Ninja \
-DBUILD_TESTS=ON \
-DBUILD_QT6=ON \
-DOCIO_LOCATION=$(brew --prefix opencolorio) \
-DOTIO_LOCATION=${OTIO_LOCATION} \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
- name: Configure (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: |
cmake -S . -B build -G Ninja \
-DBUILD_TESTS=ON \
-DBUILD_QT6=ON \
-DOTIO_LOCATION=${OTIO_LOCATION} \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
# ------------------------------------------------------------------
# Build Oak
# ------------------------------------------------------------------
- name: Build
if: runner.os != 'Windows'
run: |
cmake --build build --config ${{ env.CMAKE_BUILD_TYPE }}
ccache -s
- name: Build (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: |
cmake --build build --config ${{ env.CMAKE_BUILD_TYPE }}
ccache -s
# ------------------------------------------------------------------
# Clone and build OpenFX-Misc plugins (used by OFX integration tests)
# ------------------------------------------------------------------
- name: Clone OpenFX-Misc
run: git clone --recursive --depth 1 https://github.com/NatronGitHub/openfx-misc.git
- name: Build OpenFX-Misc plugins (Unix)
if: runner.os != 'Windows'
run: |
cd openfx-misc
make -j"$(nproc)"
sudo make install
PLUGIN_FILE=$(find /usr/OFX/Plugins /usr/local/OFX/Plugins . -name "*.ofx.bundle" -print -quit 2>/dev/null | head -1)
if [ -n "$PLUGIN_FILE" ]; then
PLUGIN_DIR=$(dirname "$PLUGIN_FILE")
else
PLUGIN_DIR="$PWD"
fi
echo "OAK_OFX_ITEST=1" >> "$GITHUB_ENV"
echo "OAK_OFX_PLUGIN_PATH=$PLUGIN_DIR" >> "$GITHUB_ENV"
echo "OAK_OFX_PLUGIN_ID=net.sf.openfx.ChromaKeyerPlugin" >> "$GITHUB_ENV"
# The Windows runners have no GPU: WGL falls back to the GDI software
# OpenGL 1.1 implementation and every render worker dies calling 3.2
# core entry points. Drop Mesa's llvmpipe opengl32.dll next to every
# binary that creates a GL context (the application directory wins the
# DLL search order over System32).
- name: Install Mesa software OpenGL (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: |
curl -sSL -o /tmp/mesa.7z \
https://github.com/pal1000/mesa-dist-win/releases/download/26.1.5/mesa3d-26.1.5-release-mingw.7z
"/c/Program Files/7-Zip/7z.exe" x /tmp/mesa.7z -o/tmp/mesa > /dev/null
ls -la /tmp/mesa /tmp/mesa/x64
for d in build/engine build/worker build/cli build/tests/gtest build/app; do
if [ -d "$d" ]; then
cp /tmp/mesa/x64/*.dll "$d/"
# Qt ignores an app-local opengl32.dll for WGL (it always binds
# the System32 one); its documented software-GL override is
# opengl32sw.dll, loaded when QT_OPENGL=software.
cp "$d/opengl32.dll" "$d/opengl32sw.dll"
ls "$d"/opengl32sw.dll "$d"/libgallium_wgl.dll
fi
done
echo "QT_OPENGL=software" >> "$GITHUB_ENV"
# ------------------------------------------------------------------
# Run all tests (including previously environment-gated OFX tests)
# ------------------------------------------------------------------
- name: Test (Linux)
if: runner.os == 'Linux'
env:
QT_QPA_PLATFORM: offscreen
run: xvfb-run -a ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }}
# Diagnostic: if the Linux test run failed, re-run the suspect gtest
# filter under gdb to capture a native backtrace of the segfault.
# Also break on dlclose to see who unloads the render backend library
# before RenderManager's destructor calls into it.
- name: gtest segfault backtrace (Linux)
if: runner.os == 'Linux' && failure()
env:
QT_QPA_PLATFORM: offscreen
run: |
sudo apt-get update && sudo apt-get install -y gdb
xvfb-run -a gdb -batch \
-ex run \
-ex 'bt full' \
-ex 'info proc mappings' \
-ex 'info symbol $pc' \
--args ./build/tests/gtest/olive-gtest \
--gtest_filter='MainWindow.ConstructsOffscreenWithPanelsAndMenus' || true
- name: Test (macOS)
if: runner.os == 'macOS'
run: ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }}
- name: Test (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }}
# Diagnostic: if the Windows test run failed, show which DLLs the
# engine test executables cannot resolve (0xc0000135).
- name: Missing DLL diagnosis (Windows)
if: runner.os == 'Windows' && failure()
shell: msys2 {0}
run: |
echo "PATH=$PATH"
ldd build/engine/oakengine_ipc_test.exe | grep -i "not found" || true
objdump -p build/engine/oakengine_ipc_test.exe | grep "DLL Name" | sort -u
ls otio-install/lib otio-install/bin 2>/dev/null || true
echo "=== render worker stderr logs ==="
for f in "$TEMP"/oak-render-worker-*.stderr.log "$TMP"/oak-render-worker-*.stderr.log /tmp/oak-render-worker-*.stderr.log; do
[ -f "$f" ] && { echo "--- $f"; tail -50 "$f"; }
done
# ------------------------------------------------------------------
# Filtered gtest runs for clearer CI output
# ------------------------------------------------------------------
- name: Plugin Smoke Tests (Linux)
if: runner.os == 'Linux'
env:
QT_QPA_PLATFORM: offscreen
run: xvfb-run -a ./build/tests/gtest/olive-gtest --gtest_filter="PluginSmoke*"
- name: Plugin Smoke Tests (macOS)
if: runner.os == 'macOS'
run: ./build/tests/gtest/olive-gtest --gtest_filter="PluginSmoke*"
- name: Plugin Smoke Tests (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: ./build/tests/gtest/olive-gtest --gtest_filter="PluginSmoke*"
- name: OFX Integration Tests (Linux)
if: runner.os == 'Linux'
env:
QT_QPA_PLATFORM: offscreen
run: xvfb-run -a ./build/tests/gtest/olive-gtest --gtest_filter="PluginIntegration.*:PluginMisc.*"
- name: OFX Integration Tests (macOS)
if: runner.os == 'macOS'
run: ./build/tests/gtest/olive-gtest --gtest_filter="PluginIntegration.*:PluginMisc.*"
- name: Audio Smoke Tests (Linux)
if: runner.os == 'Linux'
env:
QT_QPA_PLATFORM: offscreen
run: xvfb-run -a ./build/tests/gtest/olive-gtest --gtest_filter="AudioSmoke*"
- name: Audio Smoke Tests (macOS)
if: runner.os == 'macOS'
run: ./build/tests/gtest/olive-gtest --gtest_filter="AudioSmoke*"
- name: Audio Smoke Tests (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: ./build/tests/gtest/olive-gtest --gtest_filter="AudioSmoke*"
- name: Viewer Smoke Tests (Linux)
if: runner.os == 'Linux'
env:
QT_QPA_PLATFORM: offscreen
run: xvfb-run -a ./build/tests/gtest/olive-gtest --gtest_filter="ViewerSmoke*"
- name: Viewer Smoke Tests (macOS)
if: runner.os == 'macOS'
run: ./build/tests/gtest/olive-gtest --gtest_filter="ViewerSmoke*"
- name: Viewer Smoke Tests (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: ./build/tests/gtest/olive-gtest --gtest_filter="ViewerSmoke*"
+12
View File
@@ -15,6 +15,10 @@ CmakeSettings.json
# clangd's index and likely other things that need not be in the repository
.cache/
# Generated packaging assets (icons/icon.png is produced from Oak_Icon.svg
# by rsvg-convert in the CD workflow; see .github/workflows/cd.yml)
/icons/
# macOS General
.DS_Store
.AppleDouble
@@ -70,6 +74,7 @@ ui_*.h
*.jsc
Makefile*
*build-*
!tooling/ffmpeg/build-ffmpeg.sh
*.qm
*.prl
@@ -109,3 +114,10 @@ act
# Local OpenTimelineIO build (see docs/build.md)
otio-install/
# Rust
**/target/
tarpaulin-out/
.env
# CD packaging artifacts
/*.dmg
+3 -4
View File
@@ -1,4 +1,3 @@
[submodule "third_party/KDDockWidgets"]
path = third_party/KDDockWidgets
url = https://github.com/OliveCommunity/KDDockWidgets.git
branch = main
[submodule "gpui"]
path = gpui
url = https://git.oakvideoeditor.org/oak-team/oak-gpui.git
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-403
View File
@@ -1,403 +0,0 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2023 Olive Studios LLC
# 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 <http://www.gnu.org/licenses/>.
cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
project(olive-editor VERSION 0.4.1 LANGUAGES CXX)
# Fallback version used only when git is unavailable (e.g. source tarball);
# normally overridden by the git tag / commit hash logic further below.
# Edit version.txt to change it.
if(EXISTS "${CMAKE_SOURCE_DIR}/version.txt")
file(STRINGS "${CMAKE_SOURCE_DIR}/version.txt" PROJECT_VERSION LIMIT_COUNT 1)
string(STRIP "${PROJECT_VERSION}" PROJECT_VERSION)
else()
set(PROJECT_VERSION "0.0.0-unknown")
endif()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOFX_SUPPORTS_OPENGLRENDER")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DOFX_SUPPORTS_OPENGLRENDER")
option(BUILD_QT6 "Build with Qt 6 over 5 (experimental)" ON)
option(BUILD_DOXYGEN "Build Doxygen documentation" OFF)
option(BUILD_TESTS "Build unit tests" OFF)
option(USE_WERROR "Error on compile warning" OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
# Generates a compile_commands.json in the build dir, link that to the repo
# root to enrich your IDE with clangd language server protocol functionalities
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Sanitizers
add_library(olive-sanitizers INTERFACE)
include(cmake/Sanitizers.cmake)
enable_sanitizers(olive-sanitizers)
list(APPEND OLIVE_LIBRARIES olive-sanitizers)
# Set compiler options
if(MSVC)
set(OLIVE_COMPILE_OPTIONS
/wd4267
/wd4244
/experimental:external
/external:anglebrackets
/external:W0
"$<$<CONFIG:RELEASE>:/O2>"
"$<$<COMPILE_LANGUAGE:CXX>:/MP>"
/DOFX_SUPPORTS_OPENGLRENDER
)
if (USE_WERROR)
list(APPEND OLIVE_COMPILE_OPTIONS "/WX")
endif()
else()
set(OLIVE_COMPILE_OPTIONS
"$<$<CONFIG:RELEASE>:-O2>"
-Wuninitialized
-pedantic-errors
-Wall
-Wextra
-Wno-unused-parameter
-Wshadow
-DOFX_SUPPORTS_OPENGLRENDER
)
if (USE_WERROR)
list(APPEND OLIVE_COMPILE_OPTIONS "-Werror")
endif()
endif()
# MinGW does not define WIN32/WINDOWS the way OpenFX HostSupport expects.
# Ensure these are visible so ofxhBinary.h et al. pick the Windows code path.
if(MINGW)
add_compile_definitions(WIN32 WINDOWS)
endif()
set(OLIVE_DEFINITIONS -DQT_DEPRECATED_WARNINGS)
if (WIN32)
list(APPEND OLIVE_DEFINITIONS -DUNICODE -D_UNICODE)
endif()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# OFX HostSupport requires expat::expat
find_package(EXPAT REQUIRED)
if (TARGET EXPAT::EXPAT AND NOT TARGET expat::expat)
add_library(expat::expat ALIAS EXPAT::EXPAT)
endif()
# Link OpenGL
if(UNIX AND NOT APPLE AND NOT DEFINED OpenGL_GL_PREFERENCE)
set(OpenGL_GL_PREFERENCE LEGACY)
endif()
find_package(OpenGL REQUIRED)
list(APPEND OLIVE_LIBRARIES OpenGL::GL)
# Optional: Link Vulkan and shaderc (for the Vulkan render backend)
find_package(Vulkan)
find_package(PkgConfig)
if(PkgConfig_FOUND)
pkg_check_modules(SHADERC shaderc)
endif()
if(Vulkan_FOUND)
message(STATUS "Vulkan found: ${Vulkan_LIBRARIES}")
else()
message(STATUS " Vulkan not found. The Vulkan render backend will be disabled.")
endif()
# Link OpenColorIO
find_package(OpenColorIO 2.1.1 REQUIRED)
list(APPEND OLIVE_LIBRARIES ${OCIO_LIBRARIES})
list(APPEND OLIVE_INCLUDE_DIRS ${OCIO_INCLUDE_DIRS})
# Link OpenImageIO
find_package(OpenImageIO 2.1.12 REQUIRED)
list(APPEND OLIVE_LIBRARIES ${OIIO_LIBRARIES})
list(APPEND OLIVE_INCLUDE_DIRS ${OIIO_INCLUDE_DIRS})
# Link OpenEXR
find_package(OpenEXR REQUIRED)
list(APPEND OLIVE_LIBRARIES ${OPENEXR_LIBRARIES})
list(APPEND OLIVE_INCLUDE_DIRS ${OPENEXR_INCLUDES})
# Link Olive
list(APPEND OLIVE_LIBRARIES olivecore)
list(APPEND OLIVE_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/core/include)
# Shared header-only utilities (used by both engine/ and app/)
list(APPEND OLIVE_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/shared/include)
# Link Qt
set(QT_LIBRARIES
Core
Gui
Widgets
OpenGL
LinguistTools
Concurrent
)
if (UNIX AND NOT APPLE)
list(APPEND QT_LIBRARIES DBus)
endif()
if (BUILD_QT6)
set(QT_NAME Qt6)
else()
set(QT_NAME Qt5)
endif()
find_package(QT
NAMES
${QT_NAME}
REQUIRED
COMPONENTS
${QT_LIBRARIES}
OPTIONAL_COMPONENTS
Network
)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED
COMPONENTS
${QT_LIBRARIES}
OPTIONAL_COMPONENTS
Network
)
if (NOT Qt${QT_VERSION_MAJOR}Network_FOUND)
message(" Qt${QT_VERSION_MAJOR}::Network module not found, crash reporting will be disabled.")
endif()
list(APPEND OLIVE_LIBRARIES
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::OpenGL
Qt${QT_VERSION_MAJOR}::Concurrent
)
if (${QT_VERSION_MAJOR} EQUAL "6")
find_package(Qt${QT_VERSION_MAJOR}
REQUIRED
OpenGLWidgets
)
list(APPEND OLIVE_LIBRARIES
Qt${QT_VERSION_MAJOR}::OpenGLWidgets
)
# Link KDDockWidgets
#find_package(KDDockWidgets-qt6 CONFIG REQUIRED)
else()
# Link KDDockWidgets
#find_package(KDDockWidgets CONFIG REQUIRED)
endif()
list(APPEND OLIVE_LIBRARIES
KDAB::kddockwidgets
)
# Link OFX HostSupport wherever libolive-editor objects are used.
list(APPEND OLIVE_LIBRARIES OfxHost)
# FFmpeg isolation: all FFmpeg access in the editor goes through this shared
# library's pure C API. It is the only component that links FFmpeg.
add_subdirectory(ffmpeg_bridge)
list(APPEND OLIVE_LIBRARIES ffmpeg_bridge)
list(APPEND OLIVE_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg_bridge/include)
# Link PortAudio
find_package(PortAudio REQUIRED)
set(CMAKE_REQUIRED_INCLUDES ${PORTAUDIO_INCLUDE_DIRS})
include(CheckIncludeFileCXX)
check_include_file_cxx( "pa_jack.h" PA_HAS_JACK)
if (PA_HAS_JACK)
list(APPEND OLIVE_DEFINITIONS PA_HAS_JACK)
endif()
list(APPEND OLIVE_INCLUDE_DIRS ${PORTAUDIO_INCLUDE_DIRS})
list(APPEND OLIVE_LIBRARIES ${PORTAUDIO_LIBRARIES})
# Required: Link OpenTimelineIO
find_package(OpenTimelineIO REQUIRED)
list(APPEND OLIVE_DEFINITIONS USE_OTIO)
list(APPEND OLIVE_INCLUDE_DIRS ${OTIO_INCLUDE_DIRS})
list(APPEND OLIVE_LIBRARIES ${OTIO_LIBRARIES})
# Bundle the OTIO shared libraries into our install tree so that packages
# (deb/rpm/AppImage/Windows installer) ship them — most distros have no OTIO
# package to depend on. Distro-native packaging (e.g. Arch PKGBUILD, where
# opentimelineio is a proper depends entry) should pass -DOAK_BUNDLE_OTIO=OFF.
option(OAK_BUNDLE_OTIO "Install OTIO runtime libraries alongside Oak" ON)
if (OAK_BUNDLE_OTIO AND UNIX AND NOT APPLE)
include(GNUInstallDirs)
file(GLOB _otio_runtime_libs
"${OTIO_LIBRARY_DIR}/libopentimelineio.so*"
"${OTIO_LIBRARY_DIR}/libopentime.so*")
if (_otio_runtime_libs)
install(FILES ${_otio_runtime_libs} DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()
endif()
# OTIO's macOS dylibs use @loader_path install names: every binary that
# (transitively) links them needs a copy of the dylibs next to itself.
# Windows has no rpath either, so its DLLs must also sit next to each
# executable (relying on PATH is fragile in CI shells).
# Call oak_copy_otio_runtime(<target>) for each executable/shared library.
if (OAK_BUNDLE_OTIO AND (APPLE OR WIN32) AND OTIO_LIBRARY_DIR)
if (APPLE)
file(GLOB OAK_OTIO_DYLIBS
"${OTIO_LIBRARY_DIR}/libopentimelineio*.dylib"
"${OTIO_LIBRARY_DIR}/libopentime*.dylib")
else()
file(GLOB OAK_OTIO_DYLIBS
"${OTIO_LIBRARY_DIR}/libopentimelineio*.dll"
"${OTIO_LIBRARY_DIR}/libopentime*.dll")
endif()
function(oak_copy_otio_runtime target)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${OAK_OTIO_DYLIBS} $<TARGET_FILE_DIR:${target}>)
endfunction()
endif()
# Optional: Link Google Crashpad
find_package(GoogleCrashpad)
if (GoogleCrashpad_FOUND)
list(APPEND OLIVE_DEFINITIONS USE_CRASHPAD)
list(APPEND OLIVE_INCLUDE_DIRS ${CRASHPAD_INCLUDE_DIRS})
list(APPEND OLIVE_LIBRARIES ${CRASHPAD_LIBRARIES})
else()
message(" Automatic crash reporting will be disabled.")
if (APPLE)
# Enables use of special functions for slider dragging, only linked if Crashpad isn't found
# because Crashpad links it itself and will cause duplicate references if we also link it
list(APPEND OLIVE_LIBRARIES "-framework ApplicationServices")
endif()
endif()
if (APPLE)
list(APPEND OLIVE_LIBRARIES "-framework IOKit")
elseif(UNIX)
list(APPEND OLIVE_LIBRARIES Qt${QT_VERSION_MAJOR}::DBus)
endif()
# Determine version from git: the tag name if HEAD is exactly on a tag,
# otherwise the first 8 hex digits of the commit hash. Falls back to
# version.txt (read above) when git is unavailable (e.g. tarball).
set(PROJECT_LONG_VERSION ${PROJECT_VERSION})
if(EXISTS "${CMAKE_SOURCE_DIR}/.git")
find_package(Git)
if(GIT_FOUND)
execute_process(COMMAND ${GIT_EXECUTABLE} describe --exact-match --tags HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_TAG
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(GIT_TAG)
# Tags are named like "v0.4.1-alpha"; drop the leading "v"
string(REGEX REPLACE "^v" "" PROJECT_VERSION "${GIT_TAG}")
else()
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short=8 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE PROJECT_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
set(PROJECT_LONG_VERSION ${PROJECT_VERSION})
endif()
endif()
# Optional: Find Doxygen if requested
if(BUILD_DOXYGEN)
find_package(Doxygen)
endif()
set(CMAKE_INCLUDE_CURRENT_DIR ON)
list(APPEND OLIVE_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/third_party)
# Google Test discovery (shared by core/tests, engine/tests, tests/)
if (BUILD_TESTS)
include(FetchContent)
find_package(GTest QUIET)
if (NOT GTest_FOUND)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.zip
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
endif()
endif()
add_subdirectory(core)
set(KDDockWidgets_STATIC ON CACHE INTERNAL "Force KDDockWidgets to build statically")
set(KDDockWidgets_QT6 ${BUILD_QT6} CACHE INTERNAL "Conform KDDockWidgets' Qt 6 setting to ours")
# Oak only uses the QtWidgets frontend; building the QtQuick frontend causes
# duplicate QML module registration on macOS and pulls in unused dependencies.
set(KDDockWidgets_FRONTENDS "qtwidgets" CACHE INTERNAL "Only build the QtWidgets frontend for Oak")
add_subdirectory(third_party/KDDockWidgets EXCLUDE_FROM_ALL)
add_subdirectory(third_party/openfx/HostSupport)
add_subdirectory(engine)
add_subdirectory(cli)
# Internal consumers (app, tests) link oakengine-obj directly instead of the
# oakengine shared library (see app/CMakeLists.txt). Linking both breaks the
# Windows build: the DLL import library re-defines every symbol already
# provided by the object files (multiple definition errors at link time).
add_subdirectory(app)
add_subdirectory(worker)
if (BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# ------------------------------------------------------------------------------
# CPack / system package configuration
# ------------------------------------------------------------------------------
set(CPACK_PACKAGE_NAME "oak-video-editor")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_VENDOR "Oak Video Editor Team")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Oak - Non-linear video editor")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://oakvideoeditor.org")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
# Debian
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Oak Video Editor Team")
set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF)
set(CPACK_DEBIAN_PACKAGE_DEPENDS
"libqt6core6, libqt6gui6, libqt6widgets6, libqt6opengl6, libqt6openglwidgets6, libqt6network6, libqt6concurrent6, libavcodec60, libavformat60, libavutil58, libswscale7, libswresample4, libavfilter9, libopenimageio2.4, libopencolorio2, libopenexr-3-1-30, libexpat1, libportaudio2, libgl1, libvulkan1, libxkbcommon0")
# RPM
set(CPACK_RPM_PACKAGE_LICENSE "GPLv3")
set(CPACK_RPM_PACKAGE_GROUP "Applications/Multimedia")
set(CPACK_RPM_PACKAGE_URL "https://oakvideoeditor.org")
set(CPACK_RPM_PACKAGE_REQUIRES
"qt6-qtbase >= 6.0, qt6-qtbase-gui >= 6.0, qt6-qttools, ffmpeg-libs >= 6.0, OpenImageIO >= 2.4, OpenColorIO >= 2.0, openexr >= 3.1, expat, portaudio, mesa-libGL, vulkan-loader, libxkbcommon")
include(CPack)
+1 -21
View File
@@ -13,24 +13,4 @@ implementation details.
### Code Standards
In order to keep the code as readable and maintainable as possible, code
submitted should abide by the following standards:
* The code style generally follows the
[Linux Kernel Coding Style](https://www.kernel.org/doc/html/latest/process/coding-style.html)
with the following project-specific exceptions and notes:
* Indentation uses **tabs**, not spaces.
* Documentation comments should use **Javadoc-style** (`/** ... */`) where appropriate.
* Naming rules (enforced by `readability-identifier-naming` in `.clang-tidy`):
* Types (`class`, `struct`, `enum`, type aliases, template parameters): `PascalCase`
* `typedef` of structs is permitted (e.g. the opaque-handle pattern `typedef struct OakEngineNode OakEngineNode;`); struct typedefs follow `PascalCase`
* Functions, variables, member variables: `snake_case`
* Private/protected members: trailing underscore, `class_member_variables_`
* Constants and enum values: `snake_case` (e.g. `k_dry_run_interval`, `k_linear`); `ALL_CAPS` is reserved for macros — save the fear for things that are actually dangerous
* Macros: `OAK_ALL_CAPS` (project prefix), and avoid them when a constant or function will do
* File names: all lowercase, `mystring.h` / `mystring.cpp`
* Namespaces: short `snake_case`
* Getters: same name as the private member without the trailing underscore (`foo_``foo()`); setters: `set_foo()`
* Exception: Qt and third-party (e.g. OpenFX) virtual overrides and framework callbacks keep their original names (`paintEvent`, `getParams`, ...) — renaming them would break the override
* Tests are written with **Google Test** (`TEST`/`TEST_F`/`TEST_P` + `EXPECT_*`/`ASSERT_*`). Do not add hand-written test `main()`s, raw `assert()`-based test files, or custom test macros/frameworks. CTest stays the runner only — register cases through `gtest_discover_tests()`; use `GTEST_SKIP()` for environment-dependent cases (GPU, missing codecs) instead of relying on crashes or timeouts..
* 100 column limit (where it doesn't impair readability)
* Unix line endings (only LF no CRLF)
submitted should be formatted using cargo fmt.
Generated
+9800
View File
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
# Oak Video Editor - 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 <http://www.gnu.org/licenses/>.
# Root manifest: the Cargo workspace only — every package lives under
# crates/ (the GUI app in oak-app, the CLI in oak-cli, the render worker
# in oak-worker, the module rlibs in oak-*) or examples/ (standalone
# integration crates, each with its own Cargo.toml).
#
# gpui (the oak-gpui fork at gpui/) is excluded: it is a separate git
# repository with its own workspace (resolver 3, edition 2024,
# workspace.package/workspace.dependencies). Without the exclusion its
# crates would be auto-included here via oak-app's path dependencies and
# would inherit from THIS workspace's [workspace.package] (which lacks the
# keys gpui expects). Excluded, each gpui crate resolves against gpui's own
# workspace root, exactly as before the monorepo workspace existed.
[workspace]
members = ["crates/*", "examples/*"]
exclude = ["gpui", "crates/oakengine.bk"]
# NOTE: oak-storage is a workspace member but NOT a default member (it
# stays out of the default-members test matrix to keep `cargo test` at
# the root fast; the app links it as a normal path dependency, so it
# builds with the app). Build/test it explicitly with `cargo test -p oak-storage`.
# NOTE: `crates/oakengine` (the frozen C-ABI facade cdylib) is retired:
# every consumer (app/cli/worker/plugins) links the module rlibs
# directly, so nothing in the workspace referenced it. The sources are
# kept at crates/oakengine.bk (excluded from the workspace) as a
# reference snapshot; git history is the authoritative backup.
default-members = ["crates/oak-app", "crates/oak-cli", "crates/oak-worker"]
resolver = "2"
[workspace.package]
# Single source of truth for the release version: the app package
# inherits it, and with it cargo-packager and the CD packaging scripts.
version = "0.5.0"
[profile.release]
# FFI discipline: every module crate exports an `extern "C"` ABI whose
# entry points must never unwind/abort across the boundary; panics are
# caught by catch_unwind and mapped to error codes instead. `unwind` is
# also rustc's default, but this makes the project-wide policy explicit
# (it used to live in each member's Cargo.toml, which a workspace root
# ignores).
panic = "unwind"
[profile.dev.package."*"]
opt-level = 1
[profile.dev]
opt-level = 1
+5
View File
@@ -23,6 +23,11 @@ The binary can be downloaded here:
See [`docs/build.md`](docs/build.md) for build instructions on Windows (MSYS2), Linux (Debian/Ubuntu, Fedora, Arch Linux), and macOS.
## Documentation
- [Project Storage Architecture](docs/project-storage.md) ([中文](docs/zh/project-storage.md)) — database write-through persistence, node-granular journal, persistent undo
- [Build guide](docs/build.md) · [工程文件格式](docs/zh/project-file-reference.md)
## Roadmap
| Version | Theme | Core Deliverables | Boundary Notes |
-221
View File
@@ -1,221 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
# Set Olive sources and resources
set(OLIVE_SOURCES
core.h
core.cpp
engineeventbridge.h
engineeventbridge.cpp
common/colorcodingapp.h
common/colorcodingapp.cpp
common/nodevaluehandle.h
playback/playbackcontroller.h
playback/playbackcontroller.cpp
)
#set(OLIVE_RESOURCES)
# Add subdirectories, which will populate the above variables
add_subdirectory(dialog)
add_subdirectory(packaging)
add_subdirectory(panel)
add_subdirectory(timeline)
add_subdirectory(ts)
add_subdirectory(ui)
add_subdirectory(widget)
add_subdirectory(window)
# Add translations
qt_add_translation(OLIVE_QM_FILES ${OLIVE_TS_FILES})
set(QRC_BODY "")
foreach (QM_FILE ${OLIVE_QM_FILES})
get_filename_component(QM_FILENAME_COMPONENT ${QM_FILE} NAME_WE)
string(APPEND QRC_BODY "<file alias=\"${QM_FILENAME_COMPONENT}\">${QM_FILE}</file>\n")
endforeach ()
configure_file(ts/translations.qrc.in ts/translations.qrc @ONLY)
set(OLIVE_RESOURCES
${OLIVE_RESOURCES}
${CMAKE_CURRENT_BINARY_DIR}/ts/translations.qrc
widget/nodeparamview/nodeparambutton.cpp
widget/nodeparamview/nodeparambutton.h
)
# Add main library
add_library(libolive-editor
OBJECT
${OLIVE_SOURCES}
${OLIVE_RESOURCES}
)
target_compile_features(libolive-editor PUBLIC cxx_std_23)
include_directories(../third_party/openfx/include
../third_party/openfx/HostSupport/include)
# Remove prefix - prevents CMake calling it "liblibolive-editor"
set_target_properties(libolive-editor PROPERTIES PREFIX "")
option(OAK_ENABLE_DYNAMIC_RENDER_BACKEND "Build and use the dynamic render backend adapter" ON)
if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
set_target_properties(libolive-editor PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_compile_definitions(libolive-editor PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
foreach (target olivecore kddockwidgets)
if (TARGET ${target})
set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif ()
endforeach ()
endif ()
# Add application
add_executable(olive-editor
main.cpp
$<TARGET_OBJECTS:libolive-editor>
$<TARGET_OBJECTS:olive-version-obj>
)
if (COMMAND oak_copy_otio_runtime)
oak_copy_otio_runtime(olive-editor)
endif()
target_link_libraries(olive-editor PUBLIC OfxHost)
add_dependencies(olive-editor oakgl-cabi-check)
if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
add_dependencies(olive-editor oakgl)
if (TARGET oakvulkan)
add_dependencies(olive-editor oakvulkan)
endif ()
endif ()
set_target_properties(olive-editor PROPERTIES OUTPUT_NAME "oak-editor")
# Create docs if doxygen was found
if (DOXYGEN_FOUND)
set(DOXYGEN_PROJECT_NAME "Oak Video Editor")
set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs")
set(DOXYGEN_EXTRACT_ALL "YES")
set(DOXYGEN_EXTRACT_PRIVATE "YES")
doxygen_add_docs(docs ALL ${OLIVE_SOURCES})
endif ()
# Platform-specific deployment preferences
if (WIN32)
# Set Windows application icon
target_sources(olive-editor PRIVATE packaging/windows/resources.rc)
# Preserve folder structure in visual studio
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${OLIVE_SOURCES})
elseif (APPLE)
# Set Mac application icon
set(OLIVE_ICON packaging/macos/oak.icns)
target_sources(olive-editor PRIVATE ${OLIVE_ICON})
# Set Mac bundle properties
set_target_properties(olive-editor PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/packaging/macos/MacOSXBundleInfo.plist.in
MACOSX_BUNDLE_GUI_IDENTIFIER org.oakvideoeditor.Oak
MACOSX_BUNDLE_ICON_FILE oak.icns
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION}
MACOSX_BUNDLE_BUNDLE_NAME "Oak Video Editor"
MACOSX_BUNDLE_INFO_STRING "Oak Video Editor ${PROJECT_LONG_VERSION}"
MACOSX_BUNDLE_COPYRIGHT "©2018-2021 Olive Studios LLC and others. Fork maintained by Oak Video Editor Team."
RESOURCE "${OLIVE_ICON}"
OUTPUT_NAME "Oak"
)
# Copy the render worker, dynamic render backends, and the FFmpeg bridge
# library into the app bundle. They are looked up in
# QCoreApplication::applicationDirPath(), which on macOS points to
# Oak.app/Contents/MacOS.
add_custom_command(TARGET olive-editor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:olive-render-worker> $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS/
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:oakgl> $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS/
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:ffmpeg_bridge> $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS/
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:olivecore> $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS/
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:oakengine> $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS/
COMMENT "Copying oak-render-worker, render backends, ffmpeg_bridge, liboakcore and liboakengine into Oak.app"
)
if (TARGET oakvulkan)
add_custom_command(TARGET olive-editor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:oakvulkan> $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS/
)
endif ()
elseif (UNIX)
# Set Linux-specific properties for application
install(TARGETS olive-editor RUNTIME DESTINATION bin)
endif ()
if (WIN32)
# Windows has no RPATH: shared libraries must sit next to the executable
add_custom_command(TARGET olive-editor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:ffmpeg_bridge> $<TARGET_FILE_DIR:olive-editor>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:olivecore> $<TARGET_FILE_DIR:olive-editor>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:oakengine> $<TARGET_FILE_DIR:olive-editor>
)
endif ()
# Set link libraries
target_link_libraries(olive-editor PRIVATE ${OLIVE_LIBRARIES})
target_link_libraries(libolive-editor PRIVATE ${OLIVE_LIBRARIES})
# The app is an internal consumer that still uses engine C++ classes directly.
# Link the object library to bypass the version-script restrictions on
# liboakengine.so (which only exposes the C ABI for external consumers).
target_link_libraries(olive-editor PRIVATE oakengine-obj)
target_link_libraries(libolive-editor PRIVATE oakengine-obj)
# The ffmpeg_bridge shared library ships next to the binaries: inside the
# macOS app bundle (Contents/MacOS, resolved via @loader_path), and in the
# standard lib directory on other platforms.
if (APPLE)
# macOS bundles are distributed via POST_BUILD copies (not install()), so
# @loader_path must already be in the build-tree binaries' RPATH.
set(OLIVE_FB_RPATH "@loader_path")
set_target_properties(olive-editor PROPERTIES
BUILD_RPATH "@loader_path")
elseif (UNIX)
set(OLIVE_FB_RPATH "$ORIGIN/../lib")
endif ()
if (OLIVE_FB_RPATH)
set_target_properties(olive-editor PROPERTIES INSTALL_RPATH "${OLIVE_FB_RPATH}")
if (TARGET oakgl)
set_target_properties(oakgl PROPERTIES INSTALL_RPATH "${OLIVE_FB_RPATH}")
endif ()
if (TARGET oakvulkan)
set_target_properties(oakvulkan PROPERTIES INSTALL_RPATH "${OLIVE_FB_RPATH}")
endif ()
endif ()
# Set compile options
target_compile_options(olive-editor PRIVATE ${OLIVE_COMPILE_OPTIONS})
target_compile_options(libolive-editor PRIVATE ${OLIVE_COMPILE_OPTIONS})
# Set global definitions
target_compile_definitions(olive-editor PRIVATE ${OLIVE_DEFINITIONS})
target_compile_definitions(libolive-editor PRIVATE ${OLIVE_DEFINITIONS})
# Set include dirs
target_include_directories(olive-editor PRIVATE ${OLIVE_INCLUDE_DIRS})
target_include_directories(libolive-editor PRIVATE ${OLIVE_INCLUDE_DIRS})
# Add crash handler
if (GoogleCrashpad_FOUND AND Qt${QT_VERSION_MAJOR}Network_FOUND)
add_subdirectory(crashhandler)
endif ()
-81
View File
@@ -1,81 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "common/colorcodingapp.h"
#include <QObject>
namespace olive
{
QVector<Color> AppColorCoding::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<Color> &AppColorCoding::standard_colors()
{
return colors;
}
QString AppColorCoding::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("Oak");
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 AppColorCoding::get_color(int c)
{
return colors.at(c);
}
Qt::GlobalColor AppColorCoding::get_ui_selector_color(const Color &c)
{
if (c.get_rough_luminance() > 0.40f) {
return Qt::black;
} else {
return Qt::white;
}
}
}
-75
View File
@@ -1,75 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_COLORCODINGAPP_H
#define OAK_COLORCODINGAPP_H
#include <olive/core/core.h>
#include <QString>
#include <QVector>
namespace olive
{
using namespace core;
/**
* @brief App-side color label mapping (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 AppColorCoding {
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<Color> &standard_colors();
private:
static QVector<Color> colors;
};
} // namespace olive
#endif // OAK_COLORCODINGAPP_H
-213
View File
@@ -1,213 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_CONFIGWRAPPER_H
#define OAK_CONFIGWRAPPER_H
#include <QVariant>
#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<int>(oakengine_config_get_int(key_utf8(), 0));
}
operator qint64() const
{
return static_cast<qint64>(oakengine_config_get_int(key_utf8(), 0));
}
operator quint64() const
{
return static_cast<quint64>(oakengine_config_get_int(key_utf8(), 0));
}
// int64_t/uint64_t overloads only exist where they differ from
// qint64/quint64 (Linux LP64: int64_t is long; on macOS/Windows both are
// long long, where declaring them would be a redeclaration).
#if defined(__linux__)
operator int64_t() const
{
return oakengine_config_get_int(key_utf8(), 0);
}
operator uint64_t() const
{
return static_cast<uint64_t>(oakengine_config_get_int(key_utf8(), 0));
}
#endif
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<QString>(*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<int64_t>(v));
return *this;
}
OakConfigValue &operator=(uint v)
{
oakengine_config_set_int(key_utf8(), static_cast<int64_t>(v));
return *this;
}
OakConfigValue &operator=(qint64 v)
{
oakengine_config_set_int(key_utf8(), static_cast<int64_t>(v));
return *this;
}
OakConfigValue &operator=(quint64 v)
{
oakengine_config_set_int(key_utf8(), static_cast<int64_t>(v));
return *this;
}
#if defined(__linux__)
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<int64_t>(v));
return *this;
}
#endif
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<int64_t>(v.toDouble());
break;
default:
*this = v.toString();
break;
}
return *this;
}
bool toBool() const { return static_cast<bool>(*this); }
int toInt() const { return static_cast<int>(*this); }
qint64 toLongLong() const { return static_cast<qint64>(*this); }
quint64 toULongLong() const { return static_cast<quint64>(*this); }
QString toString() const { return static_cast<QString>(*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 <typename T> T value() const
{
if constexpr (std::is_same_v<T, olive::core::Rational>) {
const QString s = static_cast<QString>(*this);
const QByteArray utf8 = s.toUtf8();
return olive::core::Rational::from_string(
std::string(utf8.constData(), size_t(utf8.size())));
} else {
return static_cast<T>(*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
-87
View File
@@ -1,87 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_DEBUGAPP_H
#define OAK_DEBUGAPP_H
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QDateTime>
#include <QMutex>
#include <QTextStream>
#include <iostream>
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.
*/
[[maybe_unused]] [[maybe_unused]] 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 : "<null>",
QString::number(context.line), context.function != nullptr ?
context.function : "<null>");
std::cerr << log_line.toUtf8().constData();
if (type == QtFatalMsg) {
abort();
}
}
} // namespace olive
#endif // OAK_DEBUGAPP_H
-98
View File
@@ -1,98 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
// 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 "oakutil/filefunctions.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTextStream>
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();
}
}
-507
View File
@@ -1,507 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "htmlapp.h"
#include <QAbstractTextDocumentLayout>
#include <QFont>
#include <QTextBlock>
#include <QTextBlockFormat>
#include <QTextCharFormat>
#include <QTextDocument>
#include <QTextFragment>
#include <QTextList>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include "oakutil/xmlutils.h"
#include <QDebug>
#include <QTextBlock>
#include "oakutil/xmlutils.h"
namespace olive
{
const QVector<QString> 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<HtmlNode> &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("<html>").append(html).append("</html>");
QXmlStreamReader reader(wrapped);
QVector<HtmlNode> 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<QString, QStringList> Html::get_css_from_style(const QString &s)
{
QMap<QString, QStringList> 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 &current_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;
}
}
-83
View File
@@ -1,83 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_HTMLAPP_H
#define OAK_HTMLAPP_H
#include <QTextDocument>
#include <QTextFragment>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
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<QString, QStringList> get_css_from_style(const QString &s);
static const QVector<QString> k_block_tags;
};
}
#endif // OAK_HTML_H
-59
View File
@@ -1,59 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_KEYFRAMETYPES_H
#define OAK_KEYFRAMETYPES_H
namespace olive
{
/**
* @brief App-side keyframe enum mirrors.
*
* The engine olive::NodeKeyframe::Type mirror lives in
* app/common/nodevaluehandle.h as NodeKeyframeType (kept with the
* NodeValueType mirror); the enums below cover the remaining keyframe
* domains. Enumerator ordinals must stay in sync with the engine / facade:
* the C ABI transports these as ints.
*/
class KeyframeTypes {
public:
/// Mirror of engine's olive::NodeKeyframe::BezierType
/// (engine/node/keyframe.h; oakengine_keyframe_opposing_bezier_type()
/// transports these ordinals).
enum BezierType { k_in_handle, k_out_handle };
/**
* @brief Facade easing order used by the C ABI
* (oakengine_keyframe_get_type(), oak::Keyframe::type()): NOT the same
* order as the engine Type enum (see NodeKeyframeType in
* common/nodevaluehandle.h).
*/
enum FacadeType {
k_facade_invalid = -1,
k_facade_linear = 0,
k_facade_bezier = 1,
k_facade_hold = 2
};
};
}
#endif // OAK_KEYFRAMETYPES_H
-46
View File
@@ -1,46 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEDATATYPES_H
#define OAK_NODEDATATYPES_H
namespace olive
{
/**
* @brief App-side mirror of engine's olive::Node::DataType
* (engine/node/node.h).
*
* Enumerator ordinals must stay in sync with the engine enum: the C ABI
* oakengine_node_get_data() (and the oak::Node::data() wrapper) takes the
* `role` argument as a plain int carrying these ordinals.
*/
enum NodeDataType {
k_node_data_icon,
k_node_data_duration,
k_node_data_created_time,
k_node_data_modified_time,
k_node_data_frequency_rate,
k_node_data_tooltip
};
}
#endif // OAK_NODEDATATYPES_H
-141
View File
@@ -1,141 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEVALUEHANDLE_H
#define OAK_NODEVALUEHANDLE_H
#include "oakengine/node.h"
namespace olive
{
/**
* @brief App-side mirror of engine NodeValue::Type (engine/node/value.h).
*
* Ordinals MUST stay in sync with the engine enum: call sites that still
* hold an engine NodeValue::Type reach the helpers below through an
* implicit int conversion. The static_asserts pin the engine ordinals as
* of the R8 wave 1 migration; update both sides together.
*
* NOTE: the C ABI oak_node_value_type (OAK_NODE_VALUE_*) does NOT share
* these ordinals (e.g. k_boolean=4 vs OAK_NODE_VALUE_BOOL=3), so a plain
* int cast between the two domains is a bug. Convert with
* node_value_type_to_c().
*/
class NodeValueType
{
public:
enum Type {
k_none = 0,
k_int,
k_float,
k_rational,
k_boolean,
k_color,
k_matrix,
k_text,
k_font,
k_file,
k_texture,
k_samples,
k_vec2,
k_vec3,
k_vec4,
k_bezier,
k_combo,
k_str_combo,
k_video_params,
k_audio_params,
k_subtitle_params,
k_binary,
k_push_button,
k_data_type_count
};
};
// Ordinal sync guards against engine/node/value.h.
static_assert(NodeValueType::k_boolean == 4,
"NodeValueType out of sync with engine NodeValue::Type");
static_assert(NodeValueType::k_vec2 == 12,
"NodeValueType out of sync with engine NodeValue::Type");
static_assert(NodeValueType::k_data_type_count == 23,
"NodeValueType out of sync with engine NodeValue::Type");
/**
* @brief App-side mirror of engine NodeKeyframe::Type
* (engine/node/keyframe.h).
*
* Ordinals MUST stay in sync with the engine enum (k_invalid=-1,
* k_linear=0, k_hold=1, k_bezier=2). The facade easing type transported
* over the C ABI is a DIFFERENT numbering: 0=linear, 1=bezier, 2=hold
* (see oakengine/node.h) — convert with NodeKeyframeTypeToFacade() in
* oakvaluehelper.h, never with a plain cast.
*/
class NodeKeyframeType
{
public:
enum Type { k_invalid = -1, k_linear = 0, k_hold = 1, k_bezier = 2 };
};
// Ordinal sync guard against engine/node/keyframe.h.
static_assert(NodeKeyframeType::k_bezier == 2,
"NodeKeyframeType out of sync with engine NodeKeyframe::Type");
/**
* @brief Convert engine NodeValue::Type ordinals to oak_node_value_type (app-side).
*
* `t` uses engine NodeValue::Type ordinals (see the NodeValueType mirror
* above); callers holding an engine NodeValue::Type pass it through an
* implicit int conversion. 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(int t)
{
switch (t) {
case NodeValueType::k_int: return OAK_NODE_VALUE_INT;
case NodeValueType::k_float: return OAK_NODE_VALUE_FLOAT;
case NodeValueType::k_boolean: return OAK_NODE_VALUE_BOOL;
case NodeValueType::k_rational: return OAK_NODE_VALUE_RATIONAL;
case NodeValueType::k_color: return OAK_NODE_VALUE_COLOR;
case NodeValueType::k_vec2: return OAK_NODE_VALUE_VEC2;
case NodeValueType::k_vec3: return OAK_NODE_VALUE_VEC3;
case NodeValueType::k_vec4: return OAK_NODE_VALUE_VEC4;
case NodeValueType::k_combo: return OAK_NODE_VALUE_COMBO;
case NodeValueType::k_file: return OAK_NODE_VALUE_STRING;
case NodeValueType::k_text: return OAK_NODE_VALUE_TEXT;
case NodeValueType::k_font: return OAK_NODE_VALUE_FONT;
case NodeValueType::k_str_combo: return OAK_NODE_VALUE_STR_COMBO;
case NodeValueType::k_binary: return OAK_NODE_VALUE_BINARY;
case NodeValueType::k_bezier: return OAK_NODE_VALUE_BEZIER;
case NodeValueType::k_texture: return OAK_NODE_VALUE_TEXTURE;
case NodeValueType::k_samples: return OAK_NODE_VALUE_SAMPLES;
case NodeValueType::k_video_params: return OAK_NODE_VALUE_VIDEO_PARAMS;
case NodeValueType::k_audio_params: return OAK_NODE_VALUE_AUDIO_PARAMS;
default: return -1;
}
}
} // namespace olive
#endif // OAK_NODEVALUEHANDLE_H
-233
View File
@@ -1,233 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAKVALUEHELPER_H
#define OAKVALUEHELPER_H
#include <cstring>
#include <QVariant>
#include <QVector2D>
#include <QVector3D>
#include <QVector4D>
#include "nodevaluehandle.h"
#include "oakengine/node.h"
#include "olive/core/util/color.h"
#include "olive/core/util/rational.h"
namespace olive {
/**
* @brief Convert a per-track component QVariant into the C ABI oak_node_value POD.
*
* `type` uses engine NodeValue::Type ordinals (see the NodeValueType mirror
* in nodevaluehandle.h; an engine NodeValue::Type converts implicitly) and
* 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(int type, const QVariant &v,
oak_node_value *out)
{
memset(out, 0, sizeof(*out));
switch (type) {
case NodeValueType::k_int:
case NodeValueType::k_combo:
out->type = (type == NodeValueType::k_combo) ? OAK_NODE_VALUE_COMBO
: OAK_NODE_VALUE_INT;
out->num = v.toLongLong();
return true;
case NodeValueType::k_float:
out->type = OAK_NODE_VALUE_FLOAT;
out->f[0] = v.toDouble();
return true;
case NodeValueType::k_boolean:
out->type = OAK_NODE_VALUE_BOOL;
out->num = v.toBool() ? 1 : 0;
return true;
case NodeValueType::k_rational:
out->type = OAK_NODE_VALUE_RATIONAL;
{
const core::Rational r = v.value<core::Rational>();
out->num = r.numerator();
out->den = r.denominator();
}
return true;
case NodeValueType::k_color:
out->type = OAK_NODE_VALUE_COLOR;
{
const core::Color c = v.value<core::Color>();
out->f[0] = c.red();
out->f[1] = c.green();
out->f[2] = c.blue();
out->f[3] = c.alpha();
}
return true;
case NodeValueType::k_vec2:
out->type = OAK_NODE_VALUE_VEC2;
{
const QVector2D vec = v.value<QVector2D>();
out->f[0] = vec.x();
out->f[1] = vec.y();
}
return true;
case NodeValueType::k_vec3:
out->type = OAK_NODE_VALUE_VEC3;
{
const QVector3D vec = v.value<QVector3D>();
out->f[0] = vec.x();
out->f[1] = vec.y();
out->f[2] = vec.z();
}
return true;
case NodeValueType::k_vec4:
out->type = OAK_NODE_VALUE_VEC4;
{
const QVector4D vec = v.value<QVector4D>();
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.
*
* `type` uses engine NodeValue::Type ordinals (NodeValueType mirror).
*/
static inline bool NodeTrackComponentToOakNodeValue(int type,
const QVariant &v,
oak_node_value *out)
{
memset(out, 0, sizeof(*out));
switch (type) {
case NodeValueType::k_int:
case NodeValueType::k_combo:
out->type = (type == NodeValueType::k_combo) ? OAK_NODE_VALUE_COMBO
: OAK_NODE_VALUE_INT;
out->num = v.toLongLong();
return true;
case NodeValueType::k_float:
case NodeValueType::k_bezier:
out->type = OAK_NODE_VALUE_FLOAT;
out->f[0] = v.toDouble();
return true;
case NodeValueType::k_boolean:
out->type = OAK_NODE_VALUE_BOOL;
out->num = v.toBool() ? 1 : 0;
return true;
case NodeValueType::k_rational:
out->type = OAK_NODE_VALUE_RATIONAL;
{
const core::Rational r = v.value<core::Rational>();
out->num = r.numerator();
out->den = r.denominator();
}
return true;
case NodeValueType::k_color:
out->type = OAK_NODE_VALUE_COLOR;
out->f[0] = v.toFloat();
return true;
case NodeValueType::k_vec2:
out->type = OAK_NODE_VALUE_VEC2;
out->f[0] = v.toFloat();
return true;
case NodeValueType::k_vec3:
out->type = OAK_NODE_VALUE_VEC3;
out->f[0] = v.toFloat();
return true;
case NodeValueType::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<qlonglong>(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(
core::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>(int(v.num));
default:
return QVariant();
}
}
/**
* @brief Map engine NodeKeyframe::Type ordinals (NodeKeyframeType mirror)
* to the facade easing type (0=linear, 1=bezier, 2=hold).
*/
static inline int NodeKeyframeTypeToFacade(int type)
{
switch (type) {
case NodeKeyframeType::k_bezier:
return 1;
case NodeKeyframeType::k_hold:
return 2;
case NodeKeyframeType::k_linear:
default:
return 0;
}
}
} // namespace olive
#endif // OAKVALUEHELPER_H
-46
View File
@@ -1,46 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_PROJECTTYPES_H
#define OAK_PROJECTTYPES_H
namespace olive
{
/**
* @brief App-side mirror of the engine's olive::Project enum(s)
* (engine/node/project.h).
*
* Enumerator ordinals must stay in sync with the engine enum: the C ABI
* (oakengine_project_get_cache_location_setting() etc.) transports these
* values as plain ints.
*/
class Project {
public:
enum CacheSetting {
k_cache_use_default_location,
k_cache_store_alongside_project,
k_cache_custom_path
};
};
}
#endif // OAK_PROJECTTYPES_H
-144
View File
@@ -1,144 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "oakutil/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)
{
// ISO-style date/time (e.g. "2026-06-03 20:25") per the UI design reference
return dt.toString(QStringLiteral("yyyy-MM-dd HH:mm"));
}
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;
}
}
-76
View File
@@ -1,76 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_SERIALIZEDLAYOUTINFOAPP_H
#define OAK_SERIALIZEDLAYOUTINFOAPP_H
#include <map>
#include <vector>
#include <QByteArray>
#include <QString>
#include "oakengine/node.h"
namespace olive
{
/**
* @brief App-side mirror of the engine's olive::SerializedLayoutInfo
* (engine/node/project/serializer/serializedlayoutinfo.h).
*
* SYNC OBLIGATION: the data member layout (types AND order) must stay
* identical to the engine type. Instances of this struct cross the engine
* boundary as `void *` (Core::save_project_internal() passes one to
* oakengine_task_create_project_save(), and the engine hands one back
* through the load_layout callback); the engine side reinterprets the
* pointer as its own olive::SerializedLayoutInfo and copies the members,
* so any layout divergence is silent memory corruption.
*
* The engine's std::vector<Folder*> / std::vector<Sequence*> /
* std::vector<ViewerOutput*> members are mirrored as
* std::vector<OakEngineNode*> (same pointer size and semantics: borrowed
* node handles). panel_data mirrors
* std::map<QString, PanelLayoutInfo> where PanelLayoutInfo is
* std::map<QString, QString> (identical to PanelWidget::Info).
*
* Only the data members are mirrored; the engine type's XML
* (de)serialization methods (to_xml/from_xml) stay engine-side.
*/
class SerializedLayoutInfo {
public:
SerializedLayoutInfo() = default;
QByteArray state;
std::vector<OakEngineNode *> open_folders;
std::vector<OakEngineNode *> open_sequences;
std::vector<OakEngineNode *> open_viewers;
std::map<QString, std::map<QString, QString>> panel_data;
};
}
Q_DECLARE_METATYPE(olive::SerializedLayoutInfo)
#endif // OAK_SERIALIZEDLAYOUTINFOAPP_H
-87
View File
@@ -1,87 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_SUBTITLEAPP_H
#define OAK_SUBTITLEAPP_H
#include <QString>
#include <QVariant>
#include <olive/core/util/timerange.h>
namespace olive
{
using olive::core::TimeRange;
/**
* @brief App-side mirror of the engine Subtitle value class
* (engine/render/subtitleparams.h).
*
* Pure value type (time range + text), identical semantics to the engine
* version. It is named SubtitleApp (not Subtitle) because the engine class
* still reaches some app translation units transitively (e.g. via
* engine/node/output/viewer/viewer.h) and an identical name would be an
* ODR redefinition there.
*
* The member layout MUST stay in sync with the engine class:
* oakengine_viewer_get_subtitle_at() returns pointers to engine Subtitle
* objects which the app reads through this mirror. Update both sides
* together.
*/
class SubtitleApp {
public:
SubtitleApp() = default;
SubtitleApp(const TimeRange &time, const QString &text)
: range_(time)
, text_(text)
{
}
const TimeRange &time() const
{
return range_;
}
void set_time(const TimeRange &t)
{
range_ = t;
}
const QString &text() const
{
return text_;
}
void set_text(const QString &t)
{
text_ = t;
}
private:
TimeRange range_;
QString text_;
};
} // namespace olive
Q_DECLARE_METATYPE(olive::SubtitleApp)
#endif // OAK_SUBTITLEAPP_H
-168
View File
@@ -1,168 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TOOLTYPES_H
#define OAK_TOOLTYPES_H
#include <QCoreApplication>
#include <QString>
namespace olive
{
/**
* @brief App-side mirror of engine's olive::Tool (engine/tool/tool.h).
*
* Pure enum + static string mapping, identical to the engine version.
* Enumerator ordinals must stay in sync with the engine enum: the C ABI
* (oakengine_app_tool() etc.) transports these as ints.
*/
class Tool {
public:
/**
* @brief A list of tools that can be used throughout the application
*/
enum Item {
/// No tool. This should never be set as the application tool, its only real purpose is to indicate the lack of
/// a tool somewhere similar to nullptr.
k_none,
/// Pointer tool
k_pointer,
/// Edit tool
k_edit,
/// Ripple tool
k_ripple,
/// Rolling tool
k_rolling,
/// Razor tool
k_razor,
/// Slip tool
k_slip,
/// Slide tool
k_slide,
/// Hand tool
k_hand,
/// Zoom tool
k_zoom,
/// Transition tool
k_transition,
/// Record tool
k_record,
/// Add tool
k_add,
/// Track select tool
k_track_select,
k_count
};
/**
* @brief Tools that can be added using the kAdd tool
*/
enum AddableObject {
/// An empty clip
k_addable_empty,
/// A video clip showing a generic video placeholder
k_addable_bars,
/// A video clip showing a primitive shape
k_addable_shape,
/// A video clip with a solid connected
k_addable_solid,
/// A video clip with a title connected
k_addable_title,
/// An audio clip with a sine connected to it
k_addable_tone,
/// A subtitle clip
k_addable_subtitle,
k_addable_count
};
static QString get_addable_object_name(const AddableObject &a)
{
switch (a) {
case k_addable_empty:
return QCoreApplication::translate("Tool", "Empty");
case k_addable_bars:
return QCoreApplication::translate("Tool", "Bars");
case k_addable_shape:
return QCoreApplication::translate("Tool", "Shape");
case k_addable_solid:
return QCoreApplication::translate("Tool", "Solid");
case k_addable_title:
return QCoreApplication::translate("Tool", "Title");
case k_addable_tone:
return QCoreApplication::translate("Tool", "Tone");
case k_addable_subtitle:
return QCoreApplication::translate("Tool", "Subtitle");
case k_addable_count:
break;
}
return QCoreApplication::translate("Tool", "Unknown");
}
static QString get_addable_object_id(const AddableObject &a)
{
switch (a) {
case k_addable_empty:
return QStringLiteral("empty");
case k_addable_bars:
return QStringLiteral("bars");
case k_addable_shape:
return QStringLiteral("shape");
case k_addable_solid:
return QStringLiteral("solid");
case k_addable_title:
return QStringLiteral("title");
case k_addable_tone:
return QStringLiteral("tone");
case k_addable_subtitle:
return QStringLiteral("subtitle");
case k_addable_count:
break;
}
return QString();
}
};
}
#endif // OAK_TOOLTYPES_H
-220
View File
@@ -1,220 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_TRACKREFERENCEHANDLE_H
#define OAK_TRACKREFERENCEHANDLE_H
#include <QCoreApplication>
#include <QDataStream>
#include <QHash>
#include <QString>
#include "oakengine/timeline.h"
namespace olive
{
/**
* @brief App-side mirror of the engine Track::Reference value class
* (engine/node/output/track/track.h).
*
* Pure value type (track type + index) used throughout timeline UI code.
* Semantics are identical to the engine version. The nested Type enum
* mirrors engine Track::Type; ordinals MUST stay in sync with the engine
* enum — the C ABI transports track types as ints
* (OAKENGINE_TRACK_TYPE_* in oakengine/timeline.h), pinned by the
* static_asserts below. Update both sides together.
*/
class TrackReference
{
public:
enum Type { k_none = -1, k_video, k_audio, k_subtitle, k_count };
TrackReference()
: type_(k_none)
, index_(-1)
{
}
TrackReference(const Type &type, const int &index)
: type_(type)
, index_(index)
{
}
const Type &type() const
{
return type_;
}
const int &index() const
{
return index_;
}
bool operator==(const TrackReference &ref) const
{
return type_ == ref.type_ && index_ == ref.index_;
}
bool operator!=(const TrackReference &ref) const
{
return !(*this == ref);
}
bool operator<(const TrackReference &rhs) const
{
if (type_ != rhs.type_) {
return type_ < rhs.type_;
}
return index_ < rhs.index_;
}
QString to_string() const
{
QString type_string = type_to_string(type_);
if (type_string.isEmpty()) {
return QString();
} else {
return QStringLiteral("%1:%2").arg(type_string,
QString::number(index_));
}
}
/// For IDs that shouldn't change between localizations
static QString type_to_string(Type type)
{
switch (type) {
case k_video:
return QStringLiteral("v");
case k_audio:
return QStringLiteral("a");
case k_subtitle:
return QStringLiteral("s");
case k_count:
case k_none:
break;
}
return QString();
}
/// For human-facing strings (translation context "Track" kept
/// identical to the engine version)
static QString type_to_translated_string(Type type)
{
switch (type) {
case k_video:
return QCoreApplication::translate("Track", "V");
case k_audio:
return QCoreApplication::translate("Track", "A");
case k_subtitle:
return QCoreApplication::translate("Track", "S");
case k_count:
case k_none:
break;
}
return QString();
}
static Type type_from_string(const QString &s)
{
if (s.size() >= 3) {
if (s.at(1) == ':') {
if (s.at(0) == 'v') {
// Video stream
return k_video;
} else if (s.at(0) == 'a') {
// Audio stream
return k_audio;
} else if (s.at(0) == 's') {
// Subtitle stream
return k_subtitle;
}
}
}
return k_none;
}
static TrackReference from_string(const QString &s)
{
TrackReference ref;
Type parse_type = type_from_string(s);
if (parse_type != k_none) {
bool ok;
int parse_index = s.mid(2).toInt(&ok);
if (ok) {
ref.type_ = parse_type;
ref.index_ = parse_index;
}
}
return ref;
}
bool is_valid() const
{
return type_ > k_none && type_ < k_count && index_ >= 0;
}
private:
Type type_;
int index_;
};
// Ordinal sync guards: C ABI OAKENGINE_TRACK_TYPE_* (oakengine/timeline.h)
// carry the same values as engine Track::Type, and this mirror matches both.
static_assert(TrackReference::k_video == OAKENGINE_TRACK_TYPE_VIDEO,
"TrackReference::Type out of sync with C ABI track types");
static_assert(TrackReference::k_audio == OAKENGINE_TRACK_TYPE_AUDIO,
"TrackReference::Type out of sync with C ABI track types");
static_assert(TrackReference::k_subtitle == OAKENGINE_TRACK_TYPE_SUBTITLE,
"TrackReference::Type out of sync with C ABI track types");
inline uint qHash(const TrackReference &r, uint seed = 0)
{
return ::qHash(QStringLiteral("%1:%2").arg(QString::number(r.type()),
QString::number(r.index())),
seed);
}
inline QDataStream &operator<<(QDataStream &out, const TrackReference &ref)
{
out << static_cast<int>(ref.type()) << ref.index();
return out;
}
inline QDataStream &operator>>(QDataStream &in, TrackReference &ref)
{
int type, index;
in >> type >> index;
ref = TrackReference(static_cast<TrackReference::Type>(type), index);
return in;
}
} // namespace olive
#endif // OAK_TRACKREFERENCEHANDLE_H
-60
View File
@@ -1,60 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#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 <typename Cmd>
void *wrap_app_undo_command(const char *name, Cmd *cmd)
{
return oakengine_undo_command_create(
name,
[](void *userdata) {
static_cast<Cmd *>(userdata)->redo();
},
[](void *userdata) {
static_cast<Cmd *>(userdata)->undo();
},
[](void *userdata) {
delete static_cast<Cmd *>(userdata);
},
cmd);
}
} // namespace olive
#endif // OAK_UNDOWRAPPER_H
-47
View File
@@ -1,47 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
// App-side implementation of xml_read_next_start_element
// Provides a local definition so the app doesn't import this from liboakengine.
#include "oakutil/xmlutils.h"
namespace olive
{
bool xml_read_next_start_element(QXmlStreamReader *reader,
void *cancel_atom)
{
QXmlStreamReader::TokenType token;
while ((token = reader->readNext()) != QXmlStreamReader::Invalid &&
token != QXmlStreamReader::EndDocument &&
(!cancel_atom)) {
if (reader->isEndElement()) {
return false;
} else if (reader->isStartElement()) {
return true;
}
}
return false;
}
}
-1589
View File
File diff suppressed because it is too large Load Diff
-381
View File
@@ -1,381 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_CORE_H
#define OAK_CORE_H
#include <QObject>
#include <olive/core/core.h>
#include "common/tooltypes.h"
#include "oakutil/qtutils.h"
#include "oakengine/app.h"
#include "oakengine/node.h"
#include "oakengine/project.h"
#include "oakengine/timeline.h"
#include "oakengine/undo.h"
#include "oakengine/init.h"
#include "oakengine/task.h"
namespace olive
{
using namespace core;
class MainWindow;
/**
* @brief The main central Olive application instance_
*
* 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 QObject {
Q_OBJECT
public:
/**
* @brief Core Constructor
*
* Creates the EngineCore engine instance and registers the UI handlers
* that the engine uses to request user interaction.
*/
Core(const OakEngineAppParams *params = nullptr);
~Core()
{
instance_ = nullptr;
}
/**
* @brief Core object accessible from anywhere in the code
*
* Returns the application Core singleton (no EngineCore::instance() call).
*/
static Core *instance()
{
return instance_;
}
/**
* @brief Start Olive Core
*
* Main application launcher. Starts the engine first, then the GUI (if entering a GUI mode).
*/
void start();
/**
* @brief Stop Olive Core
*
* Tears down the UI services first, then the engine, ready for the application to exit.
*/
void stop();
/**
* @brief Retrieve main window instance_
*
* @return
*
* Pointer to the olive::MainWindow object, or nullptr if running in CLI mode.
*/
MainWindow *main_window();
/**
* @brief Import a list of files
*
* FIXME: I kind of hate this, it needs a model to update correctly. Is there a way that Items can signal enough to
* make passing references to the model unnecessary?
*
* @param urls
*/
void import_files(const QStringList &urls, OakEngineNode *parent);
/**
* @brief Get the currently active project
*
* Uses the UI/Panel system to determine which Project was the last focused on and assumes this is the active Project
* that the user wishes to work on.
*
* @return
*
* The active Project file, or nullptr if the heuristic couldn't find one.
*/
OakEngineProject *get_active_project() const;
OakEngineNode *get_selected_folder_in_active_project() const;
/**
* @brief Show a dialog to the user to rename a set of nodes
*/
bool label_nodes(const QVector<OakEngineNode *> &nodes,
void *parent = nullptr);
/**
* @brief Opens a project from the recently opened list
*/
void open_project_from_recent_list(int index);
/**
* @brief Closes a project
*/
bool close_project(bool auto_open_new, bool ignore_modified = false);
/**
* @brief Runs a modal cache task on the currently active sequence
*/
void cache_active_sequence(bool in_out_only);
void open_recovery_project(const QString &filename);
void open_node_in_viewer(OakEngineNode *viewer);
void open_export_dialog_for_viewer(OakEngineNode *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
*/
void open_project();
/**
* @brief Saves the current project
*/
bool save_project();
/**
* @brief Performs a "save as" on the current project
*/
bool save_project_as();
void revert_project();
/**
* @brief Show an About dialog
*/
void dialog_about_show();
/**
* @brief Open the import footage dialog and import the files selected (runs ImportFiles())
*/
void dialog_import_show();
/**
* @brief Show Preferences dialog
*/
void dialog_preferences_show(int start_tab = 0);
/**
* @brief Show Project Properties dialog
*/
void dialog_project_properties_show();
/**
* @brief Show Export dialog
*/
void dialog_export_show();
/**
* @brief Create a new folder in the currently active project
*/
void create_new_folder();
/**
* @brief Create a new sequence in the currently active project
*/
void create_new_sequence();
void check_for_auto_recoveries();
void browse_auto_recoveries();
public:
// The following methods are ordinary member functions, NOT slots. They are
// deliberately kept out of the `public slots:` section; 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<OakEngineSequence *> &sequences);
#endif
// ---- Facade-wrapping methods (shadow EngineCore to avoid symbol refs) ----
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();
OakEngineSequence *create_new_sequence_for_project(const QString &format,
OakEngineProject *project);
static OakEngineSequence *create_new_sequence_for_project(OakEngineProject *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(OakEngineProject *p);
static QString get_auto_recovery_index_filename();
void add_open_project(OakEngineProject *p, bool add_to_recents = false);
void remove_recently_opened_project(int index);
void set_active_project(OakEngineProject *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);
/**
* @brief App-internal re-broadcast of the engine undo-stack index change
* (issue 7 of the EventBridge elimination plan). Widgets connect to this
* instead of raw oakengine_event_subscribe callbacks on
* OAKENGINE_EVENT_UNDO_INDEX_CHANGED. The argument is the new stack index.
*/
void undo_index_changed(int index);
/**
* @brief App-internal re-broadcast of the active project's modified-flag
* change (issue 8 of the EventBridge elimination plan). The main window
* drives setWindowModified from this instead of a raw
* oakengine_event_subscribe callback on
* OAKENGINE_EVENT_PROJECT_MODIFIED_CHANGED.
*/
void project_modified_changed(bool modified);
private:
/**
* @brief Get the file filter than can be used with QFileDialog to open and save compatible projects
*/
static QString get_project_filter(bool include_any_filter);
/**
* @brief Start GUI portion of Olive
*
* Starts services and objects required for the GUI of Olive. It's guaranteed that running without this function will
* create an application instance_ that is completely valid minus the UI (e.g. for CLI modes).
*/
void start_gui(bool full_screen);
/**
* @brief Internal function for saving a project to a file
*/
void save_project_internal(const QString &override_filename = QString());
/**
* @brief Retrieves the currently most active sequence for exporting
*/
OakEngineNode *get_sequence_to_export();
bool revert_project_internal(bool by_opening_existing);
/**
* @brief Shows the "disk cache full" warning (connected to EngineCore::cache_full_warning_requested)
*/
void show_cache_full_warning();
/**
* @brief Applies a new active project to the main window (connected to EngineCore::active_project_changed)
*/
void on_active_project_changed(OakEngineProject *p);
/**
* @brief Internal main window object
*/
MainWindow *main_window_;
/**
* @brief Cached Core* singleton
*/
static Core *instance_;
private slots:
void project_save_succeeded(OakEngineTask *task);
bool add_open_project_from_task_and_add_to_recents(OakEngineTask *task)
{
return instance()->add_open_project_from_task(task, true);
}
void import_task_complete(OakEngineTask *task);
bool confirm_image_sequence(const QString &filename);
bool start_headless_export();
void open_startup_project();
/**
* @brief Internal project open
*/
void open_project_internal(const QString &filename,
bool recovery_project = false);
void import_single_file(const QString &f);
};
}
#endif // OAK_CORE_H
-68
View File
@@ -1,68 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
# Create crash handler executable
add_executable(
olive-crashhandler
crashhandler.cpp
crashhandler.h
$<TARGET_OBJECTS:olive-version-obj>
)
# Rename the generated binary to oak-crashhandler so it matches the Oak branding.
set_target_properties(olive-crashhandler PROPERTIES OUTPUT_NAME "oak-crashhandler")
# Disable console appearing on crash handler dialog
set_target_properties(olive-crashhandler PROPERTIES
WIN32_EXECUTABLE TRUE
)
# Set crash handler includes
target_include_directories(
olive-crashhandler
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CRASHPAD_INCLUDE_DIRS}
)
# Set crash handler libs
target_link_libraries(
olive-crashhandler
PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Network
${CRASHPAD_LIBRARIES}
)
set(CRASHPAD_HANDLER "crashpad_handler${CMAKE_EXECUTABLE_SUFFIX}")
set(MINIDUMP_STACKWALK "minidump_stackwalk${CMAKE_EXECUTABLE_SUFFIX}")
if (APPLE)
# Move crash handler executables inside Mac app bundle
add_custom_command(TARGET olive-crashhandler POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:olive-crashhandler> $<TARGET_FILE_DIR:olive-editor>
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CRASHPAD_LIBRARY_DIRS}/${CRASHPAD_HANDLER} $<TARGET_FILE_DIR:olive-editor>
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BREAKPAD_BIN_DIR}/${MINIDUMP_STACKWALK} $<TARGET_FILE_DIR:olive-editor>
)
elseif (UNIX)
install(TARGETS olive-crashhandler RUNTIME DESTINATION bin)
install(PROGRAMS ${CRASHPAD_LIBRARY_DIRS}/${CRASHPAD_HANDLER} DESTINATION bin)
install(PROGRAMS ${BREAKPAD_BIN_DIR}/${MINIDUMP_STACKWALK} DESTINATION bin)
endif ()
target_compile_definitions(olive-crashhandler PRIVATE ${OLIVE_DEFINITIONS})
-410
View File
@@ -1,410 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "crashhandler.h"
#include <QApplication>
#include <QCloseEvent>
#include <QDir>
#include <QFile>
#include <QFontDatabase>
#include <QLabel>
#include <QHttpMultiPart>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QProcess>
#include <QScrollBar>
#include <QSplitter>
#include <QThread>
#include <QTimer>
#include <QVBoxLayout>
#include "oakutil/crashpadutils.h"
#include "oakutil/filefunctions.h"
#include "version.h"
namespace olive
{
CrashHandlerDialog::CrashHandlerDialog(const QString &report_path)
{
setWindowTitle(tr("Oak Video Editor"));
setWindowFlags(Qt::WindowStaysOnTopHint);
report_filename_ = report_path;
waiting_for_upload_ = false;
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(new QLabel(tr(
"We're sorry, Oak Video Editor has crashed. Please help us fix it by "
"sending an error report.")));
QSplitter *splitter = new QSplitter(Qt::Vertical);
splitter->setChildrenCollapsible(false);
layout->addWidget(splitter);
summary_edit_ = new QTextEdit();
summary_edit_->setPlaceholderText(
tr("Describe what you were doing in as much detail as "
"possible. If you can, provide steps to reproduce this crash."));
splitter->addWidget(summary_edit_);
QWidget *crash_widget = new QWidget();
QVBoxLayout *crash_widget_layout = new QVBoxLayout(crash_widget);
crash_widget_layout->setMargin(0);
crash_widget_layout->addWidget(new QLabel(tr("Crash Report:")));
crash_report_ = new QTextEdit();
crash_report_->setReadOnly(true);
crash_report_->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
crash_widget_layout->addWidget(crash_report_);
splitter->addWidget(crash_widget);
QHBoxLayout *btn_layout = new QHBoxLayout();
btn_layout->setMargin(0);
btn_layout->addStretch();
send_report_btn_ = new QPushButton(tr("Send Error Report"));
connect(send_report_btn_, &QPushButton::clicked, this,
&CrashHandlerDialog::SendErrorReport);
btn_layout->addWidget(send_report_btn_);
dont_send_btn_ = new QPushButton(tr("Don't Send"));
connect(dont_send_btn_, &QPushButton::clicked, this,
&CrashHandlerDialog::reject);
btn_layout->addWidget(dont_send_btn_);
layout->addLayout(btn_layout);
crash_report_->setEnabled(false);
send_report_btn_->setEnabled(false);
crash_report_->setText(tr("Waiting for crash report to be generated..."));
GenerateReport();
}
void CrashHandlerDialog::SetGUIObjectsEnabled(bool e)
{
summary_edit_->setEnabled(e);
crash_report_->setEnabled(e);
send_report_btn_->setEnabled(e);
dont_send_btn_->setEnabled(e);
}
QString CrashHandlerDialog::GetSymbolPath()
{
QDir app_path(qApp->applicationDirPath());
QString symbols_path;
#if BUILDFLAG(IS_WIN)
symbols_path = app_path.filePath(QStringLiteral("symbols"));
#elif BUILDFLAG(IS_LINUX)
app_path.cdUp();
symbols_path =
app_path.filePath(QStringLiteral("share/oak-editor/symbols"));
#elif BUILDFLAG(IS_APPLE)
app_path.cdUp();
symbols_path = app_path.filePath(QStringLiteral("Resources/symbols"));
#endif
return symbols_path;
}
void CrashHandlerDialog::GenerateReport()
{
QProcess *p = new QProcess();
connect(p, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &CrashHandlerDialog::ReadProcessFinished);
connect(p, &QProcess::readyReadStandardOutput, this,
&CrashHandlerDialog::ReadProcessHasData);
QString stackwalk_filename =
FileFunctions::GetFormattedExecutableForPlatform(
QStringLiteral("minidump_stackwalk"));
QString stackwalk_bin =
QDir(qApp->applicationDirPath()).filePath(stackwalk_filename);
p->start(stackwalk_bin, { report_filename_, GetSymbolPath() });
crash_report_->setText(
QStringLiteral("Trying to run: %1").arg(stackwalk_bin));
}
void CrashHandlerDialog::ReplyFinished(QNetworkReply *reply)
{
waiting_for_upload_ = false;
if (reply->error() == QNetworkReply::NoError) {
// Close dialog
QDialog::accept();
} else {
QMessageBox b(this);
b.setIcon(QMessageBox::Critical);
b.setWindowModality(Qt::WindowModal);
b.setWindowTitle(tr("Upload Failed"));
b.setText(
tr("Failed to send error report (%1). Please try again later.")
.arg(QString::number(reply->error())));
b.addButton(QMessageBox::Ok);
b.exec();
SetGUIObjectsEnabled(true);
}
}
void CrashHandlerDialog::HandleSslErrors(QNetworkReply *reply,
const QList<QSslError> &se)
{
QStringList errors;
for (const QSslError &err : se) {
errors.append(err.errorString());
}
QMessageBox b(this);
b.setIcon(QMessageBox::Critical);
b.setWindowModality(Qt::WindowModal);
b.setWindowTitle(tr("SSL Error"));
b.setText(tr("Encountered the following SSL errors:\n\n%1")
.arg(errors.join('\n')));
b.addButton(QMessageBox::Ok);
b.exec();
}
void CrashHandlerDialog::AttemptToFindReport()
{
// If we found it, use it, otherwise wait a second and try again
if (report_filename_.isEmpty()) {
// Couldn't find report, try again in one second
QTimer::singleShot(500, this, &CrashHandlerDialog::AttemptToFindReport);
} else {
GenerateReport();
}
}
void CrashHandlerDialog::ReadProcessHasData()
{
report_data_.append(
static_cast<QProcess *>(sender())->readAllStandardOutput());
}
void CrashHandlerDialog::ReadProcessFinished()
{
SetGUIObjectsEnabled(true);
crash_report_->setText(report_data_);
delete sender();
}
void CrashHandlerDialog::SendErrorReport()
{
if (summary_edit_->document()->isEmpty()) {
QMessageBox b(this);
b.setIcon(QMessageBox::Question);
b.setWindowModality(Qt::WindowModal);
b.setText(
tr("You must write a description to submit this crash report."));
b.addButton(QMessageBox::Ok);
b.exec();
return;
}
QNetworkAccessManager *manager = new QNetworkAccessManager();
connect(manager, &QNetworkAccessManager::finished, this,
&CrashHandlerDialog::ReplyFinished);
connect(manager, &QNetworkAccessManager::sslErrors, this,
&CrashHandlerDialog::HandleSslErrors);
QNetworkRequest request;
request.setSslConfiguration(QSslConfiguration::defaultConfiguration());
request.setUrl(
QStringLiteral("https://olivevideoeditor.org/crashpad/report.php"));
// Create HTTP form
QHttpMultiPart *multipart =
new QHttpMultiPart(QHttpMultiPart::FormDataType);
// Create description section
QHttpPart desc_part;
desc_part.setHeader(QNetworkRequest::ContentTypeHeader,
QStringLiteral("text/plain; charset=UTF-8"));
desc_part.setHeader(QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"description\""));
desc_part.setBody(summary_edit_->toPlainText().toUtf8());
multipart->append(desc_part);
// Create report section
QHttpPart report_part;
report_part.setHeader(QNetworkRequest::ContentTypeHeader,
QStringLiteral("text/plain; charset=UTF-8"));
report_part.setHeader(QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"report\""));
report_part.setBody(report_data_);
multipart->append(report_part);
// Create commit section
QHttpPart commit_part;
commit_part.setHeader(QNetworkRequest::ContentTypeHeader,
QStringLiteral("text/plain; charset=UTF-8"));
commit_part.setHeader(QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"commit\""));
commit_part.setBody(kAppVersionLong.toUtf8());
multipart->append(commit_part);
// Create dump section
QHttpPart dump_part;
dump_part.setHeader(QNetworkRequest::ContentTypeHeader,
QStringLiteral("application/octet-stream"));
dump_part.setHeader(
QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"dump\"; filename=\"%1\"")
.arg(QFileInfo(report_filename_).fileName()));
QFile *dump_file = new QFile(report_filename_);
dump_file->open(QFile::ReadOnly);
dump_part.setBodyDevice(dump_file);
dump_file->setParent(multipart); // Delete file with multipart
multipart->append(dump_part);
// Find symbol file
QDir symbol_dir(GetSymbolPath());
QString symbol_bin_name;
#if BUILDFLAG(IS_WIN)
symbol_bin_name = QStringLiteral("oak-editor.pdb");
#elif BUILDFLAG(IS_APPLE)
symbol_bin_name = QStringLiteral("Oak");
#else
symbol_bin_name = QStringLiteral("oak-editor");
#endif
symbol_dir = QDir(symbol_dir.filePath(symbol_bin_name));
QStringList folders_in_symbol_path =
symbol_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
if (folders_in_symbol_path.size() > 0) {
symbol_dir = QDir(symbol_dir.filePath(folders_in_symbol_path.first()));
} else {
QMessageBox b(this);
b.setIcon(QMessageBox::Critical);
b.setWindowModality(Qt::WindowModal);
b.setWindowTitle(tr("Failed to send report"));
b.setText(tr("Failed to find symbols necessary to send report. "
"This is a packaging issue. Please notify "
"the maintainers of this package."));
b.addButton(QMessageBox::Ok);
b.exec();
return;
}
// Create sym section
QString symbol_filename;
#if BUILDFLAG(IS_APPLE)
symbol_filename = QStringLiteral("Oak.sym");
#else
symbol_filename = QStringLiteral("oak-editor.sym");
#endif
QString symbol_full_path = symbol_dir.filePath(symbol_filename);
QHttpPart sym_part;
sym_part.setHeader(QNetworkRequest::ContentTypeHeader,
QStringLiteral("application/octet-stream"));
sym_part.setHeader(
QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"sym\"; filename=\"%1\"")
.arg(symbol_filename));
QFile sym_file(symbol_full_path);
if (!sym_file.open(QFile::ReadOnly)) {
QMessageBox b(this);
b.setIcon(QMessageBox::Critical);
b.setWindowModality(Qt::WindowModal);
b.setWindowTitle(tr("Failed to send report"));
b.setText(tr("Failed to open symbol file. You may not have "
"permission to access it."));
b.addButton(QMessageBox::Ok);
b.exec();
return;
}
QByteArray symbol_data = qCompress(sym_file.readAll(), 9);
sym_file.close();
sym_part.setBody(symbol_data);
multipart->append(sym_part);
manager->post(request, multipart);
SetGUIObjectsEnabled(false);
waiting_for_upload_ = true;
}
void CrashHandlerDialog::closeEvent(QCloseEvent *e)
{
QMessageBox b(this);
b.setIcon(QMessageBox::Warning);
b.setWindowModality(Qt::WindowModal);
b.setWindowTitle(tr("Confirm Close"));
b.setText(
tr("Crash report is still uploading. Closing now may result in no "
"report being sent. Are you sure you wish to close?"));
b.addButton(QMessageBox::Ok);
b.addButton(QMessageBox::Cancel);
if (waiting_for_upload_ && b.exec() == QMessageBox::Cancel) {
e->ignore();
} else {
e->accept();
}
}
}
int main(int argc, char *argv[])
{
QString report;
#ifdef Q_OS_WINDOWS
int num_args;
LPWSTR *args = CommandLineToArgvW(GetCommandLineW(), &num_args);
if (num_args < 2) {
LocalFree(args);
return 1;
}
report = QString::fromWCharArray(args[1]);
LocalFree(args);
#else
if (argc < 2) {
return 1;
}
report = argv[1];
#endif
QApplication a(argc, argv);
olive::CrashHandlerDialog chd(report);
chd.open();
return a.exec();
}
-82
View File
@@ -1,82 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_CRASHHANDLERDIALOG_H
#define OAK_CRASHHANDLERDIALOG_H
#include <client/crash_report_database.h>
#include <QDialog>
#include <QDialogButtonBox>
#include <QNetworkReply>
#include <QPushButton>
#include <QTextEdit>
#include "oakutil/define.h"
namespace olive
{
class CrashHandlerDialog : public QDialog {
Q_OBJECT
public:
CrashHandlerDialog(const QString &report_path);
private:
void SetGUIObjectsEnabled(bool e);
void GenerateReport();
static QString GetSymbolPath();
QTextEdit *summary_edit_;
QTextEdit *crash_report_;
QPushButton *send_report_btn_;
QPushButton *dont_send_btn_;
QString report_filename_;
QByteArray report_data_;
bool waiting_for_upload_;
protected:
virtual void closeEvent(QCloseEvent *e) override;
private slots:
void ReplyFinished(QNetworkReply *reply);
void HandleSslErrors(QNetworkReply *reply, const QList<QSslError> &errors);
void AttemptToFindReport();
void ReadProcessHasData();
void ReadProcessFinished();
void SendErrorReport();
};
}
#endif // OAK_CRASHHANDLERDIALOG_H
-47
View File
@@ -1,47 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
add_subdirectory(about)
add_subdirectory(actionsearch)
add_subdirectory(autorecovery)
add_subdirectory(color)
add_subdirectory(configbase)
add_subdirectory(diskcache)
add_subdirectory(export)
add_subdirectory(footageproperties)
add_subdirectory(footagerelink)
add_subdirectory(keyframeproperties)
add_subdirectory(markerproperties)
if (OpenTimelineIO_FOUND)
add_subdirectory(otioproperties)
endif ()
add_subdirectory(preferences)
add_subdirectory(progress)
add_subdirectory(projectimport)
add_subdirectory(proxy)
add_subdirectory(projectproperties)
add_subdirectory(rendercancel)
add_subdirectory(sequence)
add_subdirectory(speedduration)
add_subdirectory(task)
add_subdirectory(text)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/ratiodialog.cpp
dialog/ratiodialog.h
PARENT_SCOPE
)
-25
View File
@@ -1,25 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/about/about.cpp
dialog/about/about.h
dialog/about/patreon.h
dialog/about/scrollinglabel.cpp
dialog/about/scrollinglabel.h
PARENT_SCOPE
)
-159
View File
@@ -1,159 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "about.h"
#include <QApplication>
#include <QDialogButtonBox>
#include <QLabel>
#include <QVBoxLayout>
#include "common/configwrapper.h"
#include "patreon.h"
#include "scrollinglabel.h"
namespace olive
{
AboutDialog::AboutDialog(bool welcome_dialog, QWidget *parent)
: QDialog(parent)
{
if (welcome_dialog) {
setWindowTitle(
tr("Welcome to %1").arg(QApplication::applicationName()));
} else {
setWindowTitle(tr("About %1").arg(QApplication::applicationName()));
}
QFontMetrics fm = fontMetrics();
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(fm.height(), fm.height(), fm.height(),
fm.height());
QHBoxLayout *horiz_layout = new QHBoxLayout();
horiz_layout->setContentsMargins(fm.height(), fm.height(), fm.height(),
fm.height());
horiz_layout->setSpacing(fm.height() * 2);
QLabel *icon = new QLabel(
QStringLiteral("<html><img src=':/graphics/oak-logo.png'></html>"));
icon->setAlignment(Qt::AlignCenter);
horiz_layout->addWidget(icon);
// Construct About text
QLabel *label = new QLabel(
QStringLiteral("<html><head/><body>"
"<p><b>%1</b> %2</p>" // AppName (version identifier)
"<p>%3</p>" // Description
"<p>%4</p>" // Fork notice
"<p>%5</p>" // Special thanks
"</body></html>")
.arg(
QApplication::applicationName(),
QApplication::applicationVersion(),
tr("Oak Video Editor is a free open source non-linear video editor. "
"This software is licensed under the GNU GPL Version 3."),
tr("This project is a fork of "
"<a href=\"https://github.com/olive-editor/olive\">Olive Video Editor</a>."),
tr("Special thanks to Enzo GD, administrator of the Olive "
"Facebook user group, for his generous support in spreading "
"the word about this project in its early days.")));
// Set text formatting
label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
label->setWordWrap(true);
label->setOpenExternalLinks(true);
label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
label->setTextInteractionFlags(Qt::TextSelectableByMouse |
Qt::LinksAccessibleByMouse);
label->setCursor(Qt::IBeamCursor);
horiz_layout->addWidget(label);
layout->addLayout(horiz_layout);
// Patrons where possible
layout->addWidget(new QLabel());
QString opening_statement;
if (welcome_dialog || patrons.isEmpty()) {
opening_statement = tr(
"<b>Oak Video Editor relies on support from the community to continue its development.</b>");
} else {
opening_statement = tr(
"Oak Video Editor wouldn't be possible without the support of gracious donations from the following people.");
}
QLabel *support_lbl = new QLabel(
tr("<html>%1 "
"If you like this project, please consider making a "
"one-time donation or pledging monthly to support its development.</html>")
.arg(opening_statement));
support_lbl->setWordWrap(true);
support_lbl->setAlignment(Qt::AlignCenter);
support_lbl->setOpenExternalLinks(true);
layout->addWidget(support_lbl);
if (!patrons.isEmpty()) {
ScrollingLabel *scroll = new ScrollingLabel(patrons);
scroll->start_animating();
layout->addWidget(scroll);
}
layout->addWidget(new QLabel());
QHBoxLayout *btn_layout = new QHBoxLayout();
btn_layout->setContentsMargins(0, 0, 0, 0);
btn_layout->setSpacing(0);
if (welcome_dialog) {
dont_show_again_checkbox_ =
new QCheckBox(tr("Don't show this message again"));
btn_layout->addWidget(dont_show_again_checkbox_);
} else {
dont_show_again_checkbox_ = nullptr;
}
QDialogButtonBox *buttons =
new QDialogButtonBox(QDialogButtonBox::Ok, this);
if (!welcome_dialog) {
buttons->setCenterButtons(true);
}
btn_layout->addWidget(buttons);
layout->addLayout(btn_layout);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
setFixedSize(sizeHint());
}
void AboutDialog::accept()
{
if (dont_show_again_checkbox_ && dont_show_again_checkbox_->isChecked()) {
OAK_CONFIG("ShowWelcomeDialog") = false;
}
QDialog::accept();
}
}
-62
View File
@@ -1,62 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_ABOUTDIALOG_H
#define OAK_ABOUTDIALOG_H
#include <QCheckBox>
#include <QDialog>
#include "oakutil/define.h"
namespace olive
{
/**
* @brief The AboutDialog class
*
* The About dialog (accessible through Help > About). Contains license and version information. This can be run from
* anywhere
*/
class AboutDialog : public QDialog {
Q_OBJECT
public:
/**
* @brief AboutDialog Constructor
*
* Creates About dialog.
*
* @param parent
*
* QWidget parent object. Usually this will be MainWindow.
*/
explicit AboutDialog(bool welcome_dialog, QWidget *parent = nullptr);
public slots:
virtual void accept() override;
private:
QCheckBox *dont_show_again_checkbox_;
};
}
#endif // OAK_ABOUTDIALOG_H
-26
View File
@@ -1,26 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_PATREON_H
#define OAK_PATREON_H
#include <QStringList>
QStringList patrons;
#endif // OAK_PATREON_H
-84
View File
@@ -1,84 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
#
# 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 <http://www.gnu.org/licenses/>.
#
#
#
# /***
#
#
# 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 <http://www.gnu.org/licenses/>.
#
# ***/
#
import json
import requests
import os
url = 'https://www.patreon.com/api/oauth2/v2/campaigns/1478705/members?include=currently_entitled_tiers&fields%5Bmember%5D=full_name'
name_list = ''
while True:
member_data = requests.get(url, headers={"authorization": "Bearer " + os.environ.get('PATREON_KEY')})
member_data_decoded = json.loads(member_data.text)
for member in member_data_decoded["data"]:
if len(member["relationships"]["currently_entitled_tiers"]["data"]) > 0:
if member["relationships"]["currently_entitled_tiers"]["data"][0]["id"] == "3952333":
if len(name_list) > 0:
name_list += ',\n'
name = member["attributes"]["full_name"]
name_list += " QStringLiteral(\""
name_list += name.translate(str.maketrans({
"\"": "\\\"",
"\\": "\\\\"
}))
name_list += "\")"
if "links" in member_data_decoded:
url = member_data_decoded["links"]["next"]
else:
break
text_file = open("patreon.h", "w", encoding="utf-8")
text_file.write(
"#ifndef PATREON_H\n#define PATREON_H\n\n#include <QStringList>\n\nQStringList patrons = {\n%s\n};\n\n#endif // PATREON_H\n" % name_list)
text_file.close()
-125
View File
@@ -1,125 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "scrollinglabel.h"
#include <QPainter>
#include "oakutil/qtutils.h"
namespace olive
{
const int ScrollingLabel::k_min_line_height = 10;
ScrollingLabel::ScrollingLabel(QWidget *parent)
: QWidget(parent)
, animate_(0)
{
timer_.setInterval(50);
connect(&timer_, &QTimer::timeout, this, &ScrollingLabel::animation_update);
}
ScrollingLabel::ScrollingLabel(const QStringList &text, QWidget *parent)
: ScrollingLabel(parent)
{
set_text(text);
}
void ScrollingLabel::set_text(const QStringList &text)
{
text_ = text;
QFontMetrics fm = fontMetrics();
text_height_ = fm.height();
int width = 0;
foreach (const QString &s, text_) {
width = qMax(width, QtUtils::q_font_metrics_width(fm, s));
}
setMinimumSize(width, text_height_ * k_min_line_height);
}
void ScrollingLabel::paintEvent(QPaintEvent *e)
{
QImage map(width(), height(), QImage::Format_RGBA8888_Premultiplied);
map.fill(Qt::transparent);
{
QPainter p(&map);
p.setPen(palette().text().color());
QFontMetrics fm = p.fontMetrics();
int half_width = width();
for (int i = 0; i < text_.size(); i++) {
int text_y = fm.ascent() + height() - animate_ + (fm.height() * i);
int text_bottom = text_y + fm.descent();
int text_top = text_y - fm.ascent();
if (text_bottom < 0 || text_top >= height()) {
continue;
}
const QString &s = text_.at(i);
int width = QtUtils::q_font_metrics_width(fm, s);
p.drawText(half_width / 2 - width / 2, text_y, s);
}
for (int y = 0; y < text_height_; y++) {
double mul = double(y) / double(text_height_);
set_opacity_of_scan_line(map.scanLine(y), map.width(), 4, mul);
set_opacity_of_scan_line(map.scanLine(map.height() - 1 - y),
map.width(), 4, mul);
}
}
QPainter wp(this);
wp.drawImage(0, 0, map);
}
void ScrollingLabel::set_opacity_of_scan_line(uchar *scan_line, int width,
int channels, double mul)
{
for (int x = 0; x < width; x++) {
uchar *pixel = &scan_line[x * 4];
for (int c = 0; c < channels; c++) {
pixel[c] *= mul;
}
}
}
void ScrollingLabel::animation_update()
{
animate_++;
if (animate_ >= height() + text_.size() * text_height_) {
animate_ = 0;
}
update();
}
}
-72
View File
@@ -1,72 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_SCROLLINGLABEL_H
#define OAK_SCROLLINGLABEL_H
#include <QTimer>
#include <QWidget>
namespace olive
{
class ScrollingLabel : public QWidget {
Q_OBJECT
public:
ScrollingLabel(QWidget *parent = nullptr);
ScrollingLabel(const QStringList &text, QWidget *parent = nullptr);
void set_text(const QStringList &text);
void start_animating()
{
timer_.start();
}
void stop_animating()
{
timer_.stop();
}
protected:
virtual void paintEvent(QPaintEvent *e) override;
private:
static void set_opacity_of_scan_line(uchar *scan_line, int width, int channels,
double mul);
static const int k_min_line_height;
QStringList text_;
int text_height_;
QTimer timer_;
int animate_;
private slots:
void animation_update();
};
}
#endif // OAK_SCROLLINGLABEL_H
-22
View File
@@ -1,22 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/actionsearch/actionsearch.h
dialog/actionsearch/actionsearch.cpp
PARENT_SCOPE
)
-276
View File
@@ -1,276 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
***/
#include "actionsearch.h"
#include <QKeyEvent>
#include <QLabel>
#include <QMenuBar>
#include <QVBoxLayout>
namespace olive
{
ActionSearch::ActionSearch(QWidget *parent)
: QDialog(parent)
, menu_bar_(nullptr)
{
// ActionSearch requires a parent widget
Q_ASSERT(parent != nullptr);
// Set styling (object name is required for CSS specific to this object)
setObjectName("ASDiag");
setStyleSheet("#ASDiag{border: 2px solid #808080;}");
// Size proportionally to the parent (usually MainWindow).
resize(parent->width() / 3, parent->height() / 3);
// Show dialog as a "popup", which will make the dialog close if the user clicks out of it.
setWindowFlags(Qt::Popup);
QVBoxLayout *layout = new QVBoxLayout(this);
// Construct the main entry text field.
ActionSearchEntry *entry_field = new ActionSearchEntry(this);
// Set the main entry field font size to 1.2x its standard font size.
QFont entry_field_font = entry_field->font();
entry_field_font.setPointSize(qRound(entry_field_font.pointSize() * 1.2));
entry_field->setFont(entry_field_font);
// Set placeholder text for the main entry field
entry_field->setPlaceholderText(tr("Search for action..."));
// Connect signals/slots
connect(entry_field, SIGNAL(textChanged(const QString &)), this,
SLOT(search_update(const QString &)));
connect(entry_field, SIGNAL(returnPressed()), this, SLOT(perform_action()));
// moveSelectionUp() and moveSelectionDown() are emitted when the user pressed up or down on the text field.
// We override it here to select the upper or lower item in the list.
connect(entry_field, SIGNAL(move_selection_up()), this,
SLOT(move_selection_up()));
connect(entry_field, SIGNAL(move_selection_down()), this,
SLOT(move_selection_down()));
layout->addWidget(entry_field);
// Construct list of actions
list_widget_ = new ActionSearchList(this);
// Set list's font to 1.2x its standard font size
QFont list_widget_font = list_widget_->font();
list_widget_font.setPointSize(qRound(list_widget_font.pointSize() * 1.2));
list_widget_->setFont(list_widget_font);
layout->addWidget(list_widget_);
connect(list_widget_, SIGNAL(dbl_click()), this, SLOT(perform_action()));
// Instantly focus on the entry field to allow for fully keyboard operation (if this popup was initiated by keyboard
// shortcut for example).
entry_field->setFocus();
}
void ActionSearch::set_menu_bar(QMenuBar *menu_bar)
{
menu_bar_ = menu_bar;
}
void ActionSearch::search_update(const QString &s, const QString &p,
QMenu *parent)
{
// Do nothing if there's no menu bar to work with
if (menu_bar_ == nullptr) {
return;
}
// This function is recursive, using the `parent` parameter to loop through a menu's items. It functions in two
// modes - the parent being NULL, meaning it'll get MainWindow's menubar and loop over its menus, and the parent
// referring to a menu at which point it'll loop over its actions (and call itself recursively if it finds any
// submenus).
if (parent == nullptr) {
// If parent is NULL, we'll pull from the MainWindow's menubar and call this recursively on all of its submenus
// (and their submenus).
// We'll clear all the current items in the list since if we're here, we're just starting.
list_widget_->clear();
QList<QAction *> menus = menu_bar_->actions();
// Loop through all menus from the menubar and run this function on each one.
for (int i = 0; i < menus.size(); i++) {
QMenu *menu = menus.at(i)->menu();
search_update(s, p, menu);
}
// Once we're here, all the recursion/item retrieval is complete. We auto-select the first item for better
// keyboard-exclusive functionality.
if (list_widget_->count() > 0) {
list_widget_->item(0)->setSelected(true);
}
} else {
// Parent was not NULL, so we loop over the actions in the menu we were given in `parent`.
// The list shows a '>' delimited hierarchy of the menus in which this action came from. We construct it here by
// adding the current menu's text to the existing hierarchy (passed in `p`).
QString menu_text;
if (!p.isEmpty())
menu_text += p + " > ";
menu_text += parent->title().replace(
"&", ""); // Strip out any &s used in menu action names
// Loop over the menu's actions
QList<QAction *> actions = parent->actions();
for (int i = 0; i < actions.size(); i++) {
QAction *a = actions.at(i);
// Ignore separator actions
if (!a->isSeparator()) {
if (a->menu() != nullptr) {
// If the action is a menu, run this function recursively on it
search_update(s, menu_text, a->menu());
} else {
// This is a valid non-separator non-menu action, so check it against the currently entered string.
// Strip out all &s from the action's name
QString comp = a->text().replace("&", "");
// See if the action's name contains any of the currently entered string
if (comp.contains(s, Qt::CaseInsensitive)) {
// If so, we add it to the list widget.
QListWidgetItem *item = new QListWidgetItem(
QStringLiteral("%1\n(%2)").arg(comp, menu_text),
list_widget_);
// Add a pointer to the original QAction in the item's data
item->setData(Qt::UserRole + 1,
reinterpret_cast<quintptr>(a));
list_widget_->addItem(item);
}
}
}
}
}
}
void ActionSearch::perform_action()
{
// Loop over all the items in the list and if we find one that's selected, we trigger it.
QList<QListWidgetItem *> selected_items = list_widget_->selectedItems();
if (list_widget_->count() > 0 && selected_items.size() > 0) {
QListWidgetItem *item = selected_items.at(0);
// Get QAction pointer from item's data
QAction *a = reinterpret_cast<QAction *>(
item->data(Qt::UserRole + 1).value<quintptr>());
a->trigger();
}
// Close this popup
accept();
}
void ActionSearch::move_selection_up()
{
// Here we loop over all the items to find the currently selected one, and then select the one above it. We start
// iterating at 1 (instead of 0) to efficiently ignore the first item (since the selection can't go below the very
// bottom item).
int lim = list_widget_->count();
for (int i = 1; i < lim; i++) {
if (list_widget_->item(i)->isSelected()) {
list_widget_->item(i - 1)->setSelected(true);
list_widget_->scrollToItem(list_widget_->item(i - 1));
break;
}
}
}
void ActionSearch::move_selection_down()
{
// Here we loop over all the items to find the currently selected one, and then select the one below it. We limit it
// one entry before count() to efficiently ignore the item at the end (since the selection can't go below the very
// bottom item).
int lim = list_widget_->count() - 1;
for (int i = 0; i < lim; i++) {
if (list_widget_->item(i)->isSelected()) {
list_widget_->item(i + 1)->setSelected(true);
list_widget_->scrollToItem(list_widget_->item(i + 1));
break;
}
}
}
ActionSearchEntry::ActionSearchEntry(QWidget *parent)
: QLineEdit(parent)
{
}
bool ActionSearchEntry::event(QEvent *e)
{
switch (e->type()) {
case QEvent::ShortcutOverride:
switch (static_cast<QKeyEvent *>(e)->key()) {
case Qt::Key_Up:
case Qt::Key_Down:
e->accept();
return true;
}
break;
case QEvent::KeyPress:
// Listen for up/down, otherwise pass the key event to the base class.
switch (static_cast<QKeyEvent *>(e)->key()) {
case Qt::Key_Up:
e->accept();
emit move_selection_up();
return true;
case Qt::Key_Down:
e->accept();
emit move_selection_down();
return true;
}
break;
default:
break;
}
return QLineEdit::event(e);
}
ActionSearchList::ActionSearchList(QWidget *parent)
: QListWidget(parent)
{
}
void ActionSearchList::mouseDoubleClickEvent(QMouseEvent *)
{
// Indiscriminately emit a signal on any double click
emit dbl_click();
}
}
-193
View File
@@ -1,193 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_ACTIONSEARCH_H
#define OAK_ACTIONSEARCH_H
#include <QDialog>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QMenuBar>
#include "oakutil/define.h"
namespace olive
{
class ActionSearchList;
/**
* @brief The ActionSearch class
*
* A popup window (accessible through Help > Action Search) that allows users to search for a menu command by typing
* rather than browsing through the menu bar. This can be created from anywhere provided olive::MainWindow is valid.
*/
class ActionSearch : public QDialog {
Q_OBJECT
public:
/**
* @brief ActionSearch Constructor
*
* Create ActionSearch popup.
*
* @param parent
*
* QWidget parent. Usually MainWindow.
*/
ActionSearch(QWidget *parent);
/**
* @brief Set the menu bar to use in this action search
*/
void set_menu_bar(QMenuBar *menu_bar);
private slots:
/**
* @brief Update the list of actions according to a search query
*
* This function adds/removes actions in the action list according to a given search query entered by the user.
*
* To loop over the menubar and all of its menus and submenus, this function will call itself recursively. As such
* some of its parameters do not need to be set externally, as these will be set by the function itself as it calls
* itself.
*
* @param s
*
* The search text. This is the only parameter that should be set externally.
*
* @param p
*
* The current parent hierarchy. In most cases, this should be left as nullptr when called externally.
* search_update() will fill this automatically as it needs while calling itself recursively.
*
* @param parent
*
* The current menu to loop over. In most cases, this should be left as nullptr when called externally.
* search_update() will fill this automatically as it needs while calling itself recursively.
*/
void search_update(const QString &s, const QString &p = nullptr,
QMenu *parent = nullptr);
/**
* @brief Perform the currently selected action
*
* Usually triggered by pressing Enter on the ActionSearchEntry field, this will trigger whatever action is currently
* highlighted and then close this popup. If no entries are highlighted (i.e. the list is empty), no action is
* triggered and the popup closes anyway.
*/
void perform_action();
/**
* @brief Move selection up
*
* A slot for pressing up on the ActionSearchEntry field. Moves the selection in the list up once. If the
* selection is already at the top of the list, this is a no-op.
*/
void move_selection_up();
/**
* @brief Move selection down
*
* A slot for pressing down on the ActionSearchEntry field. Moves the selection in the list down once. If the
* selection is already at the bottom of the list, this is a no-op.
*/
void move_selection_down();
private:
/**
* @brief Main widget that shows the list of commands
*/
ActionSearchList *list_widget_;
/**
* @brief Attached menu bar object
*/
QMenuBar *menu_bar_;
};
/**
* @brief The ActionSearchList class
*
* Simple wrapper around QListWidget that emits a signal when an item is double clicked that ActionSearch connects
* to a slot that triggers the currently selected action.
*/
class ActionSearchList : public QListWidget {
Q_OBJECT
public:
/**
* @brief ActionSearchList Constructor
* @param parent
*
* Usually ActionSearch.
*/
ActionSearchList(QWidget *parent);
protected:
/**
* @brief Override of QListWidget's double click event that emits a signal.
*/
void mouseDoubleClickEvent(QMouseEvent *);
signals:
/**
* @brief Signal emitted when a QListWidget item is double clicked.
*/
void dbl_click();
};
/**
* @brief The ActionSearchEntry class
*
* Simple wrapper around QLineEdit that emits signals when the up or down arrow keys are pressed so that ActionSearch
* can connect them to moving the current selection up or down.
*/
class ActionSearchEntry : public QLineEdit {
Q_OBJECT
public:
/**
* @brief ActionSearchEntry
* @param parent
*
* Usually ActionSearch.
*/
ActionSearchEntry(QWidget *parent);
protected:
/**
* @brief Override of QLineEdit's key press event that listens for up/down key presses.
* @param event
*/
virtual bool event(QEvent *e) override;
signals:
/**
* @brief Emitted when the user presses the up arrow key.
*/
void move_selection_up();
/**
* @brief Emitted when the user presses the down arrow key.
*/
void move_selection_down();
};
}
#endif // OAK_ACTIONSEARCH_H
-22
View File
@@ -1,22 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/autorecovery/autorecoverydialog.h
dialog/autorecovery/autorecoverydialog.cpp
PARENT_SCOPE
)
@@ -1,157 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "autorecoverydialog.h"
#include <QDateTime>
#include <QDialogButtonBox>
#include <QDir>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include "core.h"
#include "oakutil/filefunctions.h"
namespace olive
{
#define super QDialog
AutoRecoveryDialog::AutoRecoveryDialog(const QString &message,
const QStringList &recoveries,
bool autocheck_latest, QWidget *parent)
: QDialog(parent)
{
init(message);
populate_tree(recoveries, autocheck_latest);
}
void AutoRecoveryDialog::accept()
{
foreach (QTreeWidgetItem *checkable, checkable_items_) {
if (checkable->checkState(0) == Qt::Checked) {
QString filename = checkable->data(0, k_filename_role).toString();
Core::instance()->open_recovery_project(filename);
}
}
super::accept();
}
void AutoRecoveryDialog::init(const QString &header_text)
{
QVBoxLayout *layout = new QVBoxLayout(this);
setWindowTitle(tr("Auto-Recovery"));
layout->addWidget(new QLabel(header_text));
tree_widget_ = new QTreeWidget();
tree_widget_->setHeaderHidden(true);
layout->addWidget(tree_widget_);
QDialogButtonBox *buttons =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttons->button(QDialogButtonBox::Ok)->setText(tr("Load"));
connect(buttons, &QDialogButtonBox::accepted, this,
&AutoRecoveryDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this,
&AutoRecoveryDialog::reject);
layout->addWidget(buttons);
}
void AutoRecoveryDialog::populate_tree(const QStringList &recoveries,
bool autocheck_latest)
{
// Each entry in `recoveries` is a directory with 1+ recovery projects in it
QDir autorecovery_root(FileFunctions::get_auto_recovery_root());
foreach (const QString &recovery_folder, recoveries) {
QDir recovery_dir(autorecovery_root.filePath(recovery_folder));
QString pretty_name;
{
// Retrieve pretty name
QFile pretty_name_file(
recovery_dir.filePath(QStringLiteral("realname.txt")));
if (pretty_name_file.open(QFile::ReadOnly)) {
// Read pretty name that we should have written in the autorecovery process
pretty_name = QString::fromUtf8(pretty_name_file.readAll());
pretty_name_file.close();
}
if (pretty_name.isEmpty()) {
// Fallback to just the UUID. While it won't mean much to the user, it's better than nothing.
pretty_name = recovery_dir.dirName();
}
}
QTreeWidgetItem *top_level = new QTreeWidgetItem(tree_widget_);
top_level->setText(0, pretty_name);
{
// Populate with recoveries
QStringList entries =
recovery_dir.entryList(QDir::Files | QDir::NoDotAndDotDot,
QDir::Name | QDir::Reversed);
for (int i = 0; i < entries.size(); i++) {
const QString &entry = entries.at(i);
if (entry.endsWith(QStringLiteral(".ove"),
Qt::CaseInsensitive)) {
QTreeWidgetItem *entry_item =
new QTreeWidgetItem(top_level);
bool ok;
qint64 recovery_time =
entry.left(entry.indexOf('.')).toLongLong(&ok);
QString entry_name;
if (ok) {
// Set as time/date of recovery
entry_name =
QDateTime::fromSecsSinceEpoch(recovery_time)
.toString();
} else {
// Fallback if we couldn't discern a date from this
entry_name = entry;
}
entry_item->setText(0, entry_name);
entry_item->setData(0, k_filename_role,
recovery_dir.filePath(entry));
// Allow to be checked, auto-checking the first entry
entry_item->setCheckState(
0, (autocheck_latest && top_level->childCount() == 1) ?
Qt::Checked :
Qt::Unchecked);
checkable_items_.append(entry_item);
}
}
}
}
}
}
@@ -1,56 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AUTORECOVERYDIALOG_H
#define OAK_AUTORECOVERYDIALOG_H
#include <QDialog>
#include <QTreeWidget>
#include "oakutil/define.h"
namespace olive
{
class AutoRecoveryDialog : public QDialog {
Q_OBJECT
public:
AutoRecoveryDialog(const QString &message, const QStringList &recoveries,
bool autocheck_latest, QWidget *parent);
public slots:
virtual void accept() override;
private:
void init(const QString &header_text);
void populate_tree(const QStringList &recoveries, bool autocheck);
QTreeWidget *tree_widget_;
QVector<QTreeWidgetItem *> checkable_items_;
enum DataRole { k_filename_role = Qt::UserRole };
};
}
#endif // OAK_AUTORECOVERYDIALOG_H
-22
View File
@@ -1,22 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/color/colordialog.h
dialog/color/colordialog.cpp
PARENT_SCOPE
)
-252
View File
@@ -1,252 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "colordialog.h"
#include <QDialogButtonBox>
#include <QSplitter>
#include <QVBoxLayout>
#include "oakutil/qtutils.h"
namespace olive
{
ColorDialog::ColorDialog(OakEngineColorManager *color_manager, const ManagedColor &start,
QWidget *parent)
: QDialog(parent)
, color_manager_(color_manager)
{
setWindowTitle(tr("Select Color"));
QVBoxLayout *layout = new QVBoxLayout(this);
QSplitter *splitter = new QSplitter(Qt::Horizontal);
splitter->setChildrenCollapsible(false);
layout->addWidget(splitter);
QWidget *graphics_area = new QWidget();
splitter->addWidget(graphics_area);
QVBoxLayout *graphics_layout = new QVBoxLayout(graphics_area);
QHBoxLayout *wheel_layout = new QHBoxLayout();
graphics_layout->addLayout(wheel_layout);
color_wheel_ = new ColorWheelWidget();
wheel_layout->addWidget(color_wheel_);
hsv_value_gradient_ = new ColorGradientWidget(Qt::Vertical);
hsv_value_gradient_->setFixedWidth(
QtUtils::q_font_metrics_width(fontMetrics(), QStringLiteral("HHH")));
wheel_layout->addWidget(hsv_value_gradient_);
QHBoxLayout *swatch_layout = new QHBoxLayout();
graphics_layout->addLayout(swatch_layout);
swatch_layout->addStretch();
swatch_ = new ColorSwatchChooser(color_manager_);
swatch_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
swatch_layout->addWidget(swatch_);
swatch_layout->addStretch();
QWidget *value_area = new QWidget();
QVBoxLayout *value_layout = new QVBoxLayout(value_area);
value_layout->setSpacing(0);
splitter->addWidget(value_area);
color_values_widget_ = new ColorValuesWidget(color_manager_);
color_values_widget_->ignore_pick_from(this);
value_layout->addWidget(color_values_widget_);
chooser_ = new ColorSpaceChooser(color_manager_);
value_layout->addWidget(chooser_);
// Split window 50/50
splitter->setSizes({ INT_MAX, INT_MAX });
connect(color_wheel_, &ColorWheelWidget::selected_color_changed,
color_values_widget_, &ColorValuesWidget::set_color);
connect(color_wheel_, &ColorWheelWidget::selected_color_changed,
hsv_value_gradient_, &ColorGradientWidget::set_selected_color);
connect(color_wheel_, &ColorWheelWidget::selected_color_changed, swatch_,
&ColorSwatchChooser::set_current_color);
connect(hsv_value_gradient_, &ColorGradientWidget::selected_color_changed,
color_values_widget_, &ColorValuesWidget::set_color);
connect(hsv_value_gradient_, &ColorGradientWidget::selected_color_changed,
color_wheel_, &ColorWheelWidget::set_selected_color);
connect(hsv_value_gradient_, &ColorGradientWidget::selected_color_changed,
swatch_, &ColorSwatchChooser::set_current_color);
connect(color_values_widget_, &ColorValuesWidget::color_changed,
hsv_value_gradient_, &ColorGradientWidget::set_selected_color);
connect(color_values_widget_, &ColorValuesWidget::color_changed,
color_wheel_, &ColorWheelWidget::set_selected_color);
connect(color_values_widget_, &ColorValuesWidget::color_changed, swatch_,
&ColorSwatchChooser::set_current_color);
connect(swatch_, &ColorSwatchChooser::color_clicked, hsv_value_gradient_,
&ColorGradientWidget::set_selected_color);
connect(swatch_, &ColorSwatchChooser::color_clicked, color_wheel_,
&ColorWheelWidget::set_selected_color);
connect(swatch_, &ColorSwatchChooser::color_clicked, color_values_widget_,
&ColorValuesWidget::set_color);
connect(color_wheel_, &ColorWheelWidget::diameter_changed,
hsv_value_gradient_, &ColorGradientWidget::setFixedHeight);
QDialogButtonBox *buttons =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addWidget(buttons);
set_color(start);
connect(chooser_, &ColorSpaceChooser::color_space_changed, this,
&ColorDialog::color_space_changed);
color_space_changed(chooser_->input(), chooser_->output());
// Set default size ratio to 2:1
resize(sizeHint().height() * 2, sizeHint().height());
}
void ColorDialog::set_color(const ManagedColor &start)
{
chooser_->set_input(start.color_input());
chooser_->set_output(start.color_output());
Color managed_start;
if (start.color_input().isEmpty()) {
managed_start = start;
} else {
// Convert reference color to the input 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_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 = oak_convert_color(linear_to_input, start);
}
color_wheel_->set_selected_color(managed_start);
hsv_value_gradient_->set_selected_color(managed_start);
color_values_widget_->set_color(managed_start);
swatch_->set_current_color(managed_start);
}
ManagedColor ColorDialog::get_selected_color() const
{
ManagedColor selected = color_wheel_->get_selected_color();
// Convert to linear and return a linear color
if (input_to_ref_processor_) {
selected = oak_convert_color(input_to_ref_processor_, selected);
}
selected.set_color_input(get_color_space_input());
selected.set_color_output(get_color_space_output());
return selected;
}
QString ColorDialog::get_color_space_input() const
{
return chooser_->input();
}
oak::ColorTransform ColorDialog::get_color_space_output() const
{
return chooser_->output();
}
void ColorDialog::color_space_changed(const QString &input,
const oak::ColorTransform &output)
{
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);
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());
};
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.
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;
}
color_wheel_->set_color_processor(input_to_ref_processor_, ref_to_display);
hsv_value_gradient_->set_color_processor(input_to_ref_processor_,
ref_to_display);
color_values_widget_->set_color_processor(
input_to_ref_processor_, ref_to_display, display_to_ref, ref_to_input);
}
}
-99
View File
@@ -1,99 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_COLORDIALOG_H
#define OAK_COLORDIALOG_H
#include <QDialog>
#include "oakengine/color.h"
#include "widget/manageddisplay/colorprocessorhandle.h"
#include "widget/colorwheel/colorgradientwidget.h"
#include "widget/colorwheel/colorspacechooser.h"
#include "widget/colorwheel/colorswatchchooser.h"
#include "widget/colorwheel/colorvalueswidget.h"
#include "widget/colorwheel/colorwheelwidget.h"
namespace olive
{
class ColorDialog : public QDialog {
Q_OBJECT
public:
/**
* @brief ColorDialog Constructor
*
* @param color_manager
*
* The ColorManager to use for color management. This must be valid.
*
* @param start
*
* The color to start with. This must be in the color_manager's reference space
*
* @param input_cs
*
* The input range that the user should see. The start color will be converted to this for UI object.
*
* @param parent
*
* QWidget parent.
*/
ColorDialog(OakEngineColorManager *color_manager,
const ManagedColor &start = Color(1.0f, 1.0f, 1.0f),
QWidget *parent = nullptr);
/**
* @brief Retrieves the color selected by the user
*
* The color is always returned in the ColorManager's reference space (usually scene linear).
*/
ManagedColor get_selected_color() const;
QString get_color_space_input() const;
oak::ColorTransform get_color_space_output() const;
public slots:
void set_color(const ManagedColor &c);
private:
OakEngineColorManager *color_manager_;
ColorWheelWidget *color_wheel_;
ColorValuesWidget *color_values_widget_;
ColorGradientWidget *hsv_value_gradient_;
ColorProcessorHandlePtr input_to_ref_processor_;
ColorSpaceChooser *chooser_;
ColorSwatchChooser *swatch_;
private slots:
void color_space_changed(const QString &input, const oak::ColorTransform &output);
};
}
#endif // OAK_COLORDIALOG_H
-24
View File
@@ -1,24 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/configbase/configdialogbase.cpp
dialog/configbase/configdialogbase.h
dialog/configbase/configdialogbasetab.cpp
dialog/configbase/configdialogbasetab.h
PARENT_SCOPE
)
-102
View File
@@ -1,102 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "configdialogbase.h"
#include <QDialogButtonBox>
#include <QSplitter>
#include <QVBoxLayout>
#include "core.h"
#include "oakengine/undo.h"
namespace olive
{
ConfigDialogBase::ConfigDialogBase(QWidget *parent)
: QDialog(parent)
{
QVBoxLayout *layout = new QVBoxLayout(this);
QSplitter *splitter = new QSplitter();
splitter->setChildrenCollapsible(false);
layout->addWidget(splitter);
list_widget_ = new QListWidget();
preference_pane_stack_ = new QStackedWidget(this);
splitter->addWidget(list_widget_);
splitter->addWidget(preference_pane_stack_);
QDialogButtonBox *button_box = new QDialogButtonBox(this);
button_box->setOrientation(Qt::Horizontal);
button_box->setStandardButtons(QDialogButtonBox::Cancel |
QDialogButtonBox::Ok);
layout->addWidget(button_box);
connect(button_box, &QDialogButtonBox::accepted, this,
&ConfigDialogBase::accept);
connect(button_box, &QDialogButtonBox::rejected, this,
&ConfigDialogBase::reject);
connect(list_widget_, &QListWidget::currentRowChanged,
preference_pane_stack_, &QStackedWidget::setCurrentIndex);
}
void ConfigDialogBase::accept()
{
foreach (ConfigDialogBaseTab *tab, tabs_) {
if (!tab->validate()) {
return;
}
}
void *command = oakengine_undo_command_create_multi();
foreach (ConfigDialogBaseTab *tab, tabs_) {
tab->accept(command);
}
oakengine_undo_push(command, tr("Set Configuration").toUtf8().constData());
AcceptEvent();
QDialog::accept();
}
void ConfigDialogBase::add_tab(ConfigDialogBaseTab *tab, const QString &title)
{
list_widget_->addItem(title);
preference_pane_stack_->addWidget(tab);
tabs_.append(tab);
}
void ConfigDialogBase::set_current_tab(int index)
{
if (index >= 0 && index < list_widget_->count()) {
list_widget_->setCurrentRow(index);
}
}
}
-64
View File
@@ -1,64 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_CONFIGBASE_H
#define OAK_CONFIGBASE_H
#include <QDialog>
#include <QListWidget>
#include <QStackedWidget>
#include "configdialogbasetab.h"
namespace olive
{
class ConfigDialogBase : public QDialog {
Q_OBJECT
public:
ConfigDialogBase(QWidget *parent = nullptr);
void set_current_tab(int index);
private slots:
/**
* @brief Override of accept to save preferences to Config.
*/
virtual void accept() override;
protected:
void add_tab(ConfigDialogBaseTab *tab, const QString &title);
virtual void AcceptEvent()
{
}
private:
QListWidget *list_widget_;
QStackedWidget *preference_pane_stack_;
QList<ConfigDialogBaseTab *> tabs_;
};
}
#endif // OAK_CONFIGBASE_H
@@ -1,32 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "configdialogbasetab.h"
namespace olive
{
bool ConfigDialogBaseTab::validate()
{
return true;
}
}
@@ -1,43 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_PREFERENCESTAB_H
#define OAK_PREFERENCESTAB_H
#include <QWidget>
#include "common/configwrapper.h"
namespace olive
{
class ConfigDialogBaseTab : public QWidget {
public:
ConfigDialogBaseTab() = default;
virtual bool validate();
virtual void accept(void *parent) = 0;
};
}
#endif // OAK_PREFERENCESTAB_H
-22
View File
@@ -1,22 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/diskcache/diskcachedialog.h
dialog/diskcache/diskcachedialog.cpp
PARENT_SCOPE
)
-151
View File
@@ -1,151 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "diskcachedialog.h"
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QLabel>
#include <QMessageBox>
#include "oakengine/disk.h"
#include "oakutil/define.h"
namespace olive
{
namespace
{
/// DiskCacheFolder::get_path() through the C ABI (buf/size convention)
QString disk_folder_path(const void *folder)
{
char buf[1024];
buf[0] = '\0';
oakengine_disk_folder_get_path(folder, buf, sizeof(buf));
return QString::fromUtf8(buf);
}
} // namespace
DiskCacheDialog::DiskCacheDialog(void *folder, QWidget *parent)
: QDialog(parent)
, folder_(folder)
{
QGridLayout *layout = new QGridLayout(this);
int row = 0;
layout->addWidget(
new QLabel(tr("Disk Cache: %1").arg(disk_folder_path(folder))), row,
0, 1, 2);
setWindowTitle(tr("Disk Cache Settings"));
row++;
layout->addWidget(new QLabel(tr("Maximum Disk Cache:")), row, 0);
maximum_cache_slider_ = new FloatSlider();
maximum_cache_slider_->set_format(tr("%1 GB"));
maximum_cache_slider_->set_minimum(1.0);
// The folder limit is a byte count; the slider works in GB
maximum_cache_slider_->set_value(oakengine_disk_folder_get_limit(folder) /
static_cast<double>(k_bytes_in_gigabyte));
layout->addWidget(maximum_cache_slider_, row, 1);
row++;
clear_cache_btn_ = new QPushButton(tr("Clear Disk Cache"));
connect(clear_cache_btn_, &QPushButton::clicked, this,
static_cast<void (DiskCacheDialog::*)()>(
&DiskCacheDialog::clear_disk_cache));
layout->addWidget(clear_cache_btn_, row, 1);
row++;
clear_disk_cache_ =
new QCheckBox(tr("Automatically clear disk cache on close"));
clear_disk_cache_->setChecked(
oakengine_disk_folder_get_clear_on_close(folder) != 0);
layout->addWidget(clear_disk_cache_, row, 1);
row++;
QDialogButtonBox *buttons =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, this,
&DiskCacheDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this,
&DiskCacheDialog::reject);
layout->addWidget(buttons, row, 0, 1, 2);
}
void DiskCacheDialog::accept()
{
qint64 new_disk_cache_limit =
qRound64(maximum_cache_slider_->get_value() * k_bytes_in_gigabyte);
if (static_cast<double>(new_disk_cache_limit) !=
oakengine_disk_folder_get_limit(folder_)) {
oakengine_disk_folder_set_limit(
folder_, static_cast<double>(new_disk_cache_limit));
}
const bool clear_on_close = clear_disk_cache_->isChecked();
if ((oakengine_disk_folder_get_clear_on_close(folder_) != 0) !=
clear_on_close) {
oakengine_disk_folder_set_clear_on_close(folder_,
clear_on_close ? 1 : 0);
}
QDialog::accept();
}
void DiskCacheDialog::clear_disk_cache()
{
clear_disk_cache(disk_folder_path(folder_), this, clear_cache_btn_);
}
void DiskCacheDialog::clear_disk_cache(const QString &path, QWidget *parent,
QPushButton *clear_btn)
{
if (QMessageBox::question(
parent, tr("Clear Disk Cache"),
tr("Are you sure you want to clear the disk cache in '%1'?")
.arg(path),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
if (clear_btn)
clear_btn->setEnabled(false);
if (oakengine_disk_clear_cache(path.toUtf8().constData())) {
if (clear_btn)
clear_btn->setText(tr("Disk Cache Cleared"));
} else {
QMessageBox::information(
parent, tr("Clear Disk Cache"),
tr("Disk cache failed to fully clear. You may have to delete the cache files manually."),
QMessageBox::Ok);
if (clear_btn)
clear_btn->setText(tr("Disk Cache Partially Cleared"));
}
}
}
}
-65
View File
@@ -1,65 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_DISKCACHEDIALOG_H
#define OAK_DISKCACHEDIALOG_H
#include <QCheckBox>
#include <QDialog>
#include <QPushButton>
#include "widget/slider/floatslider.h"
namespace olive
{
class DiskCacheDialog : public QDialog {
Q_OBJECT
public:
/**
* @param folder Opaque engine DiskCacheFolder handle (from
* oakengine_disk_get_open_folder()), accessed through the
* oakengine_disk_folder_* C ABI.
*/
DiskCacheDialog(void *folder, QWidget *parent = nullptr);
static void clear_disk_cache(const QString &path, QWidget *parent,
QPushButton *clear_btn = nullptr);
public slots:
virtual void accept() override;
private:
void *folder_;
FloatSlider *maximum_cache_slider_;
QCheckBox *clear_disk_cache_;
QPushButton *clear_cache_btn_;
private slots:
void clear_disk_cache();
};
}
#endif // OAK_DISKCACHEDIALOG_H
-36
View File
@@ -1,36 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
add_subdirectory(codec)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/export/export.cpp
dialog/export/export.h
dialog/export/exportadvancedvideodialog.cpp
dialog/export/exportadvancedvideodialog.h
dialog/export/exportaudiotab.cpp
dialog/export/exportaudiotab.h
dialog/export/exportformatcombobox.cpp
dialog/export/exportformatcombobox.h
dialog/export/exportsavepresetdialog.cpp
dialog/export/exportsavepresetdialog.h
dialog/export/exportsubtitlestab.cpp
dialog/export/exportsubtitlestab.h
dialog/export/exportvideotab.cpp
dialog/export/exportvideotab.h
PARENT_SCOPE
)
-32
View File
@@ -1,32 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/export/codec/av1section.cpp
dialog/export/codec/av1section.h
dialog/export/codec/cineformsection.cpp
dialog/export/codec/cineformsection.h
dialog/export/codec/codecsection.cpp
dialog/export/codec/codecsection.h
dialog/export/codec/codecstack.cpp
dialog/export/codec/codecstack.h
dialog/export/codec/h264section.cpp
dialog/export/codec/h264section.h
dialog/export/codec/imagesection.cpp
dialog/export/codec/imagesection.h
PARENT_SCOPE
)
-141
View File
@@ -1,141 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "av1section.h"
#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QLabel>
#include "oakutil/qtutils.h"
#include "widget/slider/integerslider.h"
namespace olive
{
AV1Section::AV1Section(QWidget *parent)
: AV1Section(AV1CRFSection::k_default_a_v1_crf, parent)
{
}
AV1Section::AV1Section(int default_crf, QWidget *parent)
: CodecSection(parent)
{
QGridLayout *layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
int row = 0;
layout->addWidget(new QLabel(tr("Preset:")), row, 0);
preset_combobox_ = new QComboBox();
preset_combobox_->setToolTip(tr(
"This parameter governs the efficiency/encode-time trade-off.\n"
"Lower presets will result in an output with better quality for a given file size, but will take longer to encode.\n"
"Higher presets can result in a very fast encode, but will make some compromises on visual quality for a given crf value."));
for (int i = 0; i <= 13; i++)
preset_combobox_->addItem(QString::number(i));
preset_combobox_->setCurrentIndex(8);
layout->addWidget(preset_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Compression Method:")), row, 0);
QComboBox *compression_box = new QComboBox();
compression_box->setToolTip(tr(
"This parameter governs the quality/size trade-off.\n"
"Higher CRF values will result in a final output that takes less space, but begins to lose detail.\n"
"Lower CRF values retain more detail at the cost of larger file sizes.\n"
"The possible range of CRF in SVT-AV1 is 1-63."));
// These items must correspond to the CompressionMethod enum
compression_box->addItem(tr("Constant Rate Factor"));
layout->addWidget(compression_box, row, 1);
row++;
compression_method_stack_ = new QStackedWidget();
layout->addWidget(compression_method_stack_, row, 0, 1, 2);
crf_section_ = new AV1CRFSection(default_crf);
compression_method_stack_->addWidget(crf_section_);
connect(
compression_box,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
compression_method_stack_, &QStackedWidget::setCurrentIndex);
}
void AV1Section::add_opts(OakEngineEncodingParams *params)
{
CompressionMethod method = static_cast<CompressionMethod>(
compression_method_stack_->currentIndex());
if (method == k_constant_rate_factor) {
// Set Quantizer value
oakengine_encoding_params_set_video_option(
params, "qp",
QByteArray::number(crf_section_->get_value()).constData());
}
oakengine_encoding_params_set_video_option(
params, "preset",
QByteArray::number(preset_combobox_->currentIndex()).constData());
}
AV1CRFSection::AV1CRFSection(int default_crf, QWidget *parent)
: QWidget(parent)
{
QHBoxLayout *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
crf_slider_ = new QSlider(Qt::Horizontal);
crf_slider_->setMinimum(k_minimum_crf);
crf_slider_->setMaximum(k_maximum_crf);
crf_slider_->setValue(default_crf);
layout->addWidget(crf_slider_);
IntegerSlider *crf_input = new IntegerSlider();
crf_input->setMaximumWidth(QtUtils::q_font_metrics_width(
crf_input->fontMetrics(), QStringLiteral("HHHH")));
crf_input->set_minimum(k_minimum_crf);
crf_input->set_maximum(k_maximum_crf);
crf_input->set_value(default_crf);
crf_input->SetDefaultValue(default_crf);
layout->addWidget(crf_input);
connect(crf_slider_, &QSlider::valueChanged, crf_input,
&IntegerSlider::set_value);
connect(crf_input, &IntegerSlider::value_changed, crf_slider_,
&QSlider::setValue);
}
int AV1CRFSection::get_value() const
{
return crf_slider_->value();
}
}
-73
View File
@@ -1,73 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AV1SECTION_H
#define OAK_AV1SECTION_H
#include <QSlider>
#include <QStackedWidget>
#include <QComboBox>
#include "codecsection.h"
#include "widget/slider/floatslider.h"
namespace olive
{
class AV1CRFSection : public QWidget {
Q_OBJECT
public:
AV1CRFSection(int default_crf, QWidget *parent = nullptr);
int get_value() const;
static const int k_default_a_v1_crf = 30;
private:
static const int k_minimum_crf = 0;
static const int k_maximum_crf = 63;
QSlider *crf_slider_;
};
class AV1Section : public CodecSection {
Q_OBJECT
public:
enum CompressionMethod {
k_constant_rate_factor,
};
AV1Section(QWidget *parent = nullptr);
AV1Section(int default_crf, QWidget *parent);
virtual void add_opts(OakEngineEncodingParams *params) override;
private:
QStackedWidget *compression_method_stack_;
AV1CRFSection *crf_section_;
QComboBox *preset_combobox_;
};
}
#endif // OAK_AV1SECTION_H
@@ -1,99 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "cineformsection.h"
#include <QGridLayout>
#include <QLabel>
namespace olive
{
CineformSection::CineformSection(QWidget *parent)
: CodecSection(parent)
{
QGridLayout *layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
int row = 0;
layout->addWidget(new QLabel(tr("Quality:")), row, 0);
quality_combobox_ = new QComboBox();
/* Correspond to the following indexes for FFmpeg
*
* -quality <int> E..V....... set quality (from 0 to 12) (default film3+)
* film3+ 0 E..V.......
* film3 1 E..V.......
* film2+ 2 E..V.......
* film2 3 E..V.......
* film1.5 4 E..V.......
* film1+ 5 E..V.......
* film1 6 E..V.......
* high+ 7 E..V.......
* high 8 E..V.......
* medium+ 9 E..V.......
* medium 10 E..V.......
* low+ 11 E..V.......
* low 12 E..V.......
*
*/
quality_combobox_->addItem(tr("Film Scan 3+"));
quality_combobox_->addItem(tr("Film Scan 3"));
quality_combobox_->addItem(tr("Film Scan 2+"));
quality_combobox_->addItem(tr("Film Scan 2"));
quality_combobox_->addItem(tr("Film Scan 1.5"));
quality_combobox_->addItem(tr("Film Scan 1+"));
quality_combobox_->addItem(tr("Film Scan 1"));
quality_combobox_->addItem(tr("High+"));
quality_combobox_->addItem(tr("High"));
quality_combobox_->addItem(tr("Medium+"));
quality_combobox_->addItem(tr("Medium"));
quality_combobox_->addItem(tr("Low+"));
quality_combobox_->addItem(tr("Low"));
// Default to "medium"
quality_combobox_->setCurrentIndex(10);
layout->addWidget(quality_combobox_, row, 1);
}
void CineformSection::add_opts(OakEngineEncodingParams *params)
{
oakengine_encoding_params_set_video_option(
params, "quality",
QByteArray::number(quality_combobox_->currentIndex()).constData());
}
void CineformSection::set_opts(const OakEngineEncodingParams *p)
{
char buf[64];
const int ret = oakengine_encoding_params_video_option(
p, "quality", buf, static_cast<int>(sizeof(buf)));
if (ret > 0) {
quality_combobox_->setCurrentIndex(QString::fromUtf8(buf).toInt());
}
}
}
-47
View File
@@ -1,47 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_CINEFORMSECTION_H
#define OAK_CINEFORMSECTION_H
#include <QComboBox>
#include "codecsection.h"
namespace olive
{
class CineformSection : public CodecSection {
Q_OBJECT
public:
CineformSection(QWidget *parent = nullptr);
virtual void add_opts(OakEngineEncodingParams *params) override;
virtual void set_opts(const OakEngineEncodingParams *p) override;
private:
QComboBox *quality_combobox_;
};
}
#endif // OAK_CINEFORMSECTION_H
-32
View File
@@ -1,32 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "codecsection.h"
namespace olive
{
CodecSection::CodecSection(QWidget *parent)
: QWidget(parent)
{
}
}
-50
View File
@@ -1,50 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_CODECSECTION_H
#define OAK_CODECSECTION_H
#include <QWidget>
#include "oakengine/encoding.h"
namespace olive
{
class CodecSection : public QWidget {
Q_OBJECT
public:
CodecSection(QWidget *parent = nullptr);
virtual void add_opts(OakEngineEncodingParams *params)
{
Q_UNUSED(params)
}
virtual void set_opts(const OakEngineEncodingParams *p)
{
Q_UNUSED(p)
}
};
}
#endif // OAK_CODECSECTION_H
-57
View File
@@ -1,57 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "codecstack.h"
namespace olive
{
#define super QStackedWidget
CodecStack::CodecStack(QWidget *parent)
: super{ parent }
{
connect(this, &CodecStack::currentChanged, this, &CodecStack::on_change);
}
void CodecStack::addWidget(QWidget *widget)
{
super::addWidget(widget);
on_change(currentIndex());
}
void CodecStack::on_change(int index)
{
for (int i = 0; i < count(); i++) {
if (i == index) {
widget(i)->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
} else {
widget(i)->setSizePolicy(QSizePolicy::Ignored,
QSizePolicy::Ignored);
}
widget(i)->adjustSize();
}
adjustSize();
}
}
-45
View File
@@ -1,45 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_CODECSTACK_H
#define OAK_CODECSTACK_H
#include <QStackedWidget>
namespace olive
{
class CodecStack : public QStackedWidget {
Q_OBJECT
public:
explicit CodecStack(QWidget *parent = nullptr);
void addWidget(QWidget *widget);
signals:
private slots:
void on_change(int index);
};
}
#endif // OAK_CODECSTACK_H
-336
View File
@@ -1,336 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "h264section.h"
#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QLabel>
#include "oakutil/qtutils.h"
#include "widget/slider/integerslider.h"
namespace olive
{
H264Section::H264Section(QWidget *parent)
: H264Section(H264CRFSection::k_default_h264_crf, parent)
{
}
H264Section::H264Section(int default_crf, QWidget *parent)
: CodecSection(parent)
{
QGridLayout *layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
int row = 0;
layout->addWidget(new QLabel(tr("Encode Speed:")), row, 0);
preset_combobox_ = new QComboBox();
preset_combobox_->setToolTip(tr(
"This setting allows you to tweak the ratio of export speed to compression quality. \n\n"
"If using Constant Rate Factor, slower speeds will result in smaller file sizes for the same quality. \n\n"
"If using Target Bit Rate or Target File Size, slower speeds will result in higher quality for the same bitrate/filesize. \n\n"
"This setting is equivalent to the `preset` setting in libx264."));
preset_combobox_->addItem(tr("Ultra Fast"));
preset_combobox_->addItem(tr("Super Fast"));
preset_combobox_->addItem(tr("Very Fast"));
preset_combobox_->addItem(tr("Faster"));
preset_combobox_->addItem(tr("Fast"));
preset_combobox_->addItem(tr("Medium"));
preset_combobox_->addItem(tr("Slow"));
preset_combobox_->addItem(tr("Slower"));
preset_combobox_->addItem(tr("Very Slow"));
//Default to "medium"
preset_combobox_->setCurrentIndex(5);
layout->addWidget(preset_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Compression Method:")), row, 0);
QComboBox *compression_box = new QComboBox();
// These items must correspond to the CompressionMethod enum
compression_box->addItem(tr("Constant Rate Factor"));
compression_box->addItem(tr("Target Bit Rate"));
compression_box->addItem(tr("Target File Size"));
layout->addWidget(compression_box, row, 1);
row++;
compression_method_stack_ = new QStackedWidget();
layout->addWidget(compression_method_stack_, row, 0, 1, 2);
crf_section_ = new H264CRFSection(default_crf);
compression_method_stack_->addWidget(crf_section_);
bitrate_section_ = new H264BitRateSection();
compression_method_stack_->addWidget(bitrate_section_);
filesize_section_ = new H264FileSizeSection();
compression_method_stack_->addWidget(filesize_section_);
connect(
compression_box,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
compression_method_stack_, &QStackedWidget::setCurrentIndex);
}
void H264Section::add_opts(OakEngineEncodingParams *params)
{
// FIXME: Implement two-pass
CompressionMethod method = static_cast<CompressionMethod>(
compression_method_stack_->currentIndex());
// 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
oakengine_encoding_params_set_video_option(
params, "ove_compressionmethod",
QByteArray::number(method).constData());
if (method == k_constant_rate_factor) {
// Simply set CRF 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;
if (method == k_target_bit_rate) {
// Use user-supplied values for the bit rate
target_rate = bitrate_section_->get_target_bit_rate();
min_rate = 0;
max_rate = bitrate_section_->get_maximum_bit_rate();
} 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();
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<double>(export_len_num)
/ static_cast<double>(export_len_den)
: 1.0;
target_rate = qRound64(static_cast<double>(target_fs) / export_len_sec);
min_rate = target_rate;
max_rate = target_rate;
oakengine_encoding_params_set_video_option(
params, "ove_targetfilesize",
QByteArray::number(target_fs).constData());
}
// Disable CRF encoding
oakengine_encoding_params_set_video_option(params, "crf", "-1");
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);
}
oakengine_encoding_params_set_video_option(
params, "preset",
QByteArray::number(preset_combobox_->currentIndex()).constData());
}
void H264Section::set_opts(const OakEngineEncodingParams *p)
{
char buf[64];
CompressionMethod method = k_constant_rate_factor;
if (oakengine_encoding_params_video_option(
p, "ove_compressionmethod", buf,
static_cast<int>(sizeof(buf))) > 0) {
method = static_cast<CompressionMethod>(QString::fromUtf8(buf).toInt());
}
compression_method_stack_->setCurrentIndex(method);
if (method == k_constant_rate_factor) {
if (oakengine_encoding_params_video_option(
p, "crf", buf, static_cast<int>(sizeof(buf))) > 0) {
crf_section_->set_value(QString::fromUtf8(buf).toInt());
}
} else {
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
bitrate_section_->set_target_bit_rate(target_rate);
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)
if (oakengine_encoding_params_video_option(
p, "ove_targetfilesize", buf,
static_cast<int>(sizeof(buf))) > 0) {
filesize_section_->set_file_size(
QString::fromUtf8(buf).toLongLong());
}
}
}
}
H264CRFSection::H264CRFSection(int default_crf, QWidget *parent)
: QWidget(parent)
{
QHBoxLayout *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
crf_slider_ = new QSlider(Qt::Horizontal);
crf_slider_->setMinimum(k_minimum_crf);
crf_slider_->setMaximum(k_maximum_crf);
crf_slider_->setValue(default_crf);
layout->addWidget(crf_slider_);
IntegerSlider *crf_input = new IntegerSlider();
crf_input->setMaximumWidth(QtUtils::q_font_metrics_width(
crf_input->fontMetrics(), QStringLiteral("HHHH")));
crf_input->set_minimum(k_minimum_crf);
crf_input->set_maximum(k_maximum_crf);
crf_input->set_value(default_crf);
crf_input->SetDefaultValue(default_crf);
layout->addWidget(crf_input);
connect(crf_slider_, &QSlider::valueChanged, crf_input,
&IntegerSlider::set_value);
connect(crf_input, &IntegerSlider::value_changed, crf_slider_,
&QSlider::setValue);
}
int H264CRFSection::get_value() const
{
return crf_slider_->value();
}
void H264CRFSection::set_value(int c)
{
crf_slider_->setValue(c);
}
H264BitRateSection::H264BitRateSection(QWidget *parent)
: QWidget(parent)
{
QGridLayout *layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
int row = 0;
layout->addWidget(new QLabel(tr("Target Bit Rate (Mbps):")), row, 0);
target_rate_ = new FloatSlider();
target_rate_->set_minimum(0);
layout->addWidget(target_rate_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Maximum Bit Rate (Mbps):")), row, 0);
max_rate_ = new FloatSlider();
max_rate_->set_minimum(0);
layout->addWidget(max_rate_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Two-Pass")), row, 0);
QCheckBox *two_pass_box = new QCheckBox();
layout->addWidget(two_pass_box, row, 1);
// Bit rate defaults
target_rate_->set_value(16.0);
max_rate_->set_value(32.0);
}
int64_t H264BitRateSection::get_target_bit_rate() const
{
return qRound64(target_rate_->get_value() * 1000000.0);
}
void H264BitRateSection::set_target_bit_rate(int64_t b)
{
target_rate_->set_value(double(b) * 0.000001);
}
int64_t H264BitRateSection::get_maximum_bit_rate() const
{
return qRound64(max_rate_->get_value() * 1000000.0);
}
void H264BitRateSection::set_maximum_bit_rate(int64_t b)
{
max_rate_->set_value(double(b) * 0.000001);
}
H264FileSizeSection::H264FileSizeSection(QWidget *parent)
: QWidget(parent)
{
QGridLayout *layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
int row = 0;
layout->addWidget(new QLabel(tr("Target File Size (MB):")), row, 0);
file_size_ = new FloatSlider();
file_size_->set_minimum(0);
layout->addWidget(file_size_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Two-Pass")), row, 0);
QCheckBox *two_pass_box = new QCheckBox();
layout->addWidget(two_pass_box, row, 1);
// File size defaults
file_size_->set_value(700.0);
}
int64_t H264FileSizeSection::get_file_size() const
{
// Convert megabytes to BITS
return qRound64(file_size_->get_value() * 1024.0 * 1024.0 * 8.0);
}
void H264FileSizeSection::set_file_size(int64_t f)
{
// Convert bits back to megabytes
file_size_->set_value(double(f) / 8.0 / 1024.0 / 1024.0);
}
H265Section::H265Section(QWidget *parent)
: H264Section(H264CRFSection::k_default_h265_crf, parent)
{
}
}
-127
View File
@@ -1,127 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_H264SECTION_H
#define OAK_H264SECTION_H
#include <QSlider>
#include <QStackedWidget>
#include <QComboBox>
#include "codecsection.h"
#include "widget/slider/floatslider.h"
namespace olive
{
class H264CRFSection : public QWidget {
Q_OBJECT
public:
H264CRFSection(int default_crf, QWidget *parent = nullptr);
int get_value() const;
void set_value(int c);
static constexpr int k_default_h264_crf = 18;
static constexpr int k_default_h265_crf = 23;
private:
static constexpr int k_minimum_crf = 0;
static constexpr int k_maximum_crf = 51;
QSlider *crf_slider_;
};
class H264BitRateSection : public QWidget {
Q_OBJECT
public:
H264BitRateSection(QWidget *parent = nullptr);
/**
* @brief Get user-selected target bit rate (returns in BITS)
*/
int64_t get_target_bit_rate() const;
void set_target_bit_rate(int64_t b);
/**
* @brief Get user-selected maximum bit rate (returns in BITS)
*/
int64_t get_maximum_bit_rate() const;
void set_maximum_bit_rate(int64_t b);
private:
FloatSlider *target_rate_;
FloatSlider *max_rate_;
};
class H264FileSizeSection : public QWidget {
Q_OBJECT
public:
H264FileSizeSection(QWidget *parent = nullptr);
/**
* @brief Returns file size in BITS
*/
int64_t get_file_size() const;
void set_file_size(int64_t f);
private:
FloatSlider *file_size_;
};
class H264Section : public CodecSection {
Q_OBJECT
public:
enum CompressionMethod {
k_constant_rate_factor,
k_target_bit_rate,
k_target_file_size
};
H264Section(QWidget *parent = nullptr);
H264Section(int default_crf, QWidget *parent);
virtual void add_opts(OakEngineEncodingParams *params) override;
virtual void set_opts(const OakEngineEncodingParams *p) override;
private:
QStackedWidget *compression_method_stack_;
H264CRFSection *crf_section_;
H264BitRateSection *bitrate_section_;
H264FileSizeSection *filesize_section_;
QComboBox *preset_combobox_;
};
class H265Section : public H264Section {
Q_OBJECT
public:
H265Section(QWidget *parent = nullptr);
};
}
#endif // OAK_H264SECTION_H
-63
View File
@@ -1,63 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "imagesection.h"
#include <QGridLayout>
#include <QLabel>
namespace olive
{
ImageSection::ImageSection(QWidget *parent)
: CodecSection(parent)
{
QGridLayout *layout = new QGridLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
int row = 0;
layout->addWidget(new QLabel(tr("Image Sequence:")), row, 0);
image_sequence_checkbox_ = new QCheckBox();
connect(image_sequence_checkbox_, &QCheckBox::toggled, this,
&ImageSection::image_sequence_check_box_toggled);
layout->addWidget(image_sequence_checkbox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Frame to Export:")), row, 0);
frame_slider_ = new RationalSlider();
frame_slider_->set_minimum(0);
frame_slider_->set_value(0);
frame_slider_->set_display_type(slider::k_time);
connect(frame_slider_, &RationalSlider::value_changed, this,
&ImageSection::time_changed);
layout->addWidget(frame_slider_, row, 1);
}
void ImageSection::image_sequence_check_box_toggled(bool e)
{
frame_slider_->setEnabled(!e);
}
}
-77
View File
@@ -1,77 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_IMAGESECTION_H
#define OAK_IMAGESECTION_H
#include <QCheckBox>
#include "codecsection.h"
#include "widget/slider/rationalslider.h"
namespace olive
{
class ImageSection : public CodecSection {
Q_OBJECT
public:
ImageSection(QWidget *parent = nullptr);
bool is_image_sequence_checked() const
{
return image_sequence_checkbox_->isChecked();
}
void set_image_sequence_checked(bool e)
{
image_sequence_checkbox_->setChecked(e);
}
void set_timebase(const Rational &r)
{
frame_slider_->set_timebase(r);
}
Rational get_time() const
{
return frame_slider_->get_value();
}
void set_time(const Rational &t)
{
frame_slider_->set_value(t);
}
signals:
void time_changed(const Rational &t);
private:
QCheckBox *image_sequence_checkbox_;
RationalSlider *frame_slider_;
private slots:
void image_sequence_check_box_toggled(bool e);
};
}
#endif // OAK_IMAGESECTION_H
File diff suppressed because it is too large Load Diff
-143
View File
@@ -1,143 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EXPORTDIALOG_H
#define OAK_EXPORTDIALOG_H
#include <QComboBox>
#include <QDialog>
#include <cstdint>
#include <QDialogButtonBox>
#include <QLineEdit>
#include <QProgressBar>
#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"
namespace olive
{
class ExportDialog : public QDialog {
Q_OBJECT
public:
ExportDialog(OakEngineNode *viewer_node, bool stills_only_mode,
QWidget *parent = nullptr);
ExportDialog(OakEngineNode *viewer_node, QWidget *parent = nullptr)
: ExportDialog(viewer_node, false, parent)
{
}
Rational get_selected_timebase() const;
void set_selected_timebase(const Rational &r);
OakEngineEncodingParams *generate_params() const;
void set_params(const OakEngineEncodingParams *e);
virtual bool eventFilter(QObject *o, QEvent *e) override;
public slots:
virtual void done(int r) override;
signals:
void request_import_file(const QString &s);
private:
void add_preferences_tab(QWidget *inner_widget, const QString &title);
void load_presets();
void set_default_filename();
bool sequence_has_subtitles() const;
void set_defaults();
OakEngineNode *viewer_node_;
int previously_selected_format_;
Rational get_export_length() const;
int64_t get_export_length_in_timebase_units() const;
enum RangeSelection { k_range_entire_sequence, k_range_in_to_out };
enum AutoPreset {
k_preset_default = -1,
k_preset_last_used = -2,
};
QTabWidget *preferences_tabs_;
QComboBox *preset_combobox_;
QComboBox *range_combobox_;
std::vector<OakEngineEncodingParams *> presets_;
QCheckBox *video_enabled_;
QCheckBox *audio_enabled_;
QCheckBox *subtitles_enabled_;
ViewerWidget *preview_viewer_;
QLineEdit *filename_edit_;
ExportFormatComboBox *format_combobox_;
ExportVideoTab *video_tab_;
ExportAudioTab *audio_tab_;
ExportSubtitlesTab *subtitle_tab_;
double video_aspect_ratio_;
OakEngineColorManager *color_manager_;
QWidget *preferences_area_;
QCheckBox *export_bkg_box_;
QCheckBox *import_file_after_export_;
bool stills_only_mode_;
bool loading_presets_;
private slots:
void browse_filename();
void format_changed(int current_format);
void resolution_changed();
void update_viewer_dimensions();
void start_export();
void export_finished();
void image_sequence_check_box_changed(bool e);
void save_preset();
void preset_combo_box_changed();
};
}
#endif // OAK_EXPORTDIALOG_H
@@ -1,93 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "exportadvancedvideodialog.h"
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
namespace olive
{
ExportAdvancedVideoDialog::ExportAdvancedVideoDialog(
const QList<QString> &pix_fmts, QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("Advanced"));
QVBoxLayout *layout = new QVBoxLayout(this);
{
// Pixel Settings
QGroupBox *pixel_group = new QGroupBox();
layout->addWidget(pixel_group);
pixel_group->setTitle(tr("Pixel"));
QGridLayout *pixel_layout = new QGridLayout(pixel_group);
int row = 0;
pixel_layout->addWidget(new QLabel(tr("Pixel Format:")), row, 0);
pixel_format_combobox_ = new QComboBox();
pixel_format_combobox_->addItems(pix_fmts);
pixel_layout->addWidget(pixel_format_combobox_, row, 1);
row++;
pixel_layout->addWidget(new QLabel(tr("YUV Color Range:")), row, 0);
yuv_color_range_combobox_ = new QComboBox();
yuv_color_range_combobox_->addItems(
{ tr("Limited (16-235)"), tr("Full (0-255)") });
pixel_layout->addWidget(yuv_color_range_combobox_, row, 1);
}
{
// Performance Settings
QGroupBox *performance_group = new QGroupBox();
layout->addWidget(performance_group);
performance_group->setTitle(tr("Performance"));
QGridLayout *performance_layout = new QGridLayout(performance_group);
int row = 0;
performance_layout->addWidget(new QLabel(tr("Threads:")), row, 0);
thread_slider_ = new IntegerSlider();
thread_slider_->set_minimum(0);
thread_slider_->SetDefaultValue(0);
thread_slider_->insert_label_substitution(0, tr("Auto"));
performance_layout->addWidget(thread_slider_, row, 1);
row++;
}
QDialogButtonBox *buttons =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttons, &QDialogButtonBox::accepted, this,
&ExportAdvancedVideoDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this,
&ExportAdvancedVideoDialog::reject);
layout->addWidget(buttons);
}
}
@@ -1,77 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_EXPORTADVANCEDVIDEODIALOG_H
#define OAK_EXPORTADVANCEDVIDEODIALOG_H
#include <QComboBox>
#include <QDialog>
#include "widget/slider/integerslider.h"
namespace olive
{
class ExportAdvancedVideoDialog : public QDialog {
Q_OBJECT
public:
ExportAdvancedVideoDialog(const QList<QString> &pix_fmts,
QWidget *parent = nullptr);
int threads() const
{
return static_cast<int>(thread_slider_->get_value());
}
void set_threads(int t)
{
thread_slider_->set_value(t);
}
QString pix_fmt() const
{
return pixel_format_combobox_->currentText();
}
void set_pix_fmt(const QString &s)
{
pixel_format_combobox_->setCurrentText(s);
}
int yuv_range() const
{
return static_cast<int>(
yuv_color_range_combobox_->currentIndex());
}
void set_yuv_range(int i)
{
yuv_color_range_combobox_->setCurrentIndex(i);
}
private:
IntegerSlider *thread_slider_;
QComboBox *pixel_format_combobox_;
QComboBox *yuv_color_range_combobox_;
};
}
#endif // OAK_EXPORTADVANCEDVIDEODIALOG_H
-139
View File
@@ -1,139 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "exportaudiotab.h"
#include <QGridLayout>
#include <QLabel>
#include <olive/core/core.h>
#include "oakengine/encoding.h"
namespace olive
{
const int ExportAudioTab::k_default_bit_rate = 320;
ExportAudioTab::ExportAudioTab(QWidget *parent)
: QWidget(parent)
{
QVBoxLayout *outer_layout = new QVBoxLayout(this);
QGridLayout *layout = new QGridLayout();
outer_layout->addLayout(layout);
int row = 0;
layout->addWidget(new QLabel(tr("Codec:")), row, 0);
codec_combobox_ = new QComboBox();
connect(
codec_combobox_,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
this, &ExportAudioTab::update_sample_formats);
connect(
codec_combobox_,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
this, &ExportAudioTab::update_bit_rate_enabled);
layout->addWidget(codec_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Sample Rate:")), row, 0);
sample_rate_combobox_ = new SampleRateComboBox();
layout->addWidget(sample_rate_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Channel Layout:")), row, 0);
channel_layout_combobox_ = new ChannelLayoutComboBox();
layout->addWidget(channel_layout_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Format:")), row, 0);
sample_format_combobox_ = new SampleFormatComboBox();
layout->addWidget(sample_format_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Bit Rate:")), row, 0);
bit_rate_slider_ = new IntegerSlider();
bit_rate_slider_->set_minimum(32);
bit_rate_slider_->set_maximum(320);
bit_rate_slider_->set_value(k_default_bit_rate);
bit_rate_slider_->set_format(tr("%1 kbps"));
layout->addWidget(bit_rate_slider_, row, 1);
outer_layout->addStretch();
}
int ExportAudioTab::set_format(int format)
{
const int acodec_count = oakengine_encoding_format_audio_codec_count(format);
setEnabled(acodec_count > 0);
codec_combobox_->blockSignals(true);
codec_combobox_->clear();
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;
update_sample_formats();
update_bit_rate_enabled();
return acodec_count;
}
void ExportAudioTab::update_sample_formats()
{
// Use oakengine to get sample format values and build the vector
const int count = oakengine_encoding_sample_format_count(fmt_, get_codec());
std::vector<olive::core::SampleFormat> 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<olive::core::SampleFormat::Format>(val)));
}
sample_format_combobox_->set_available_formats(fmts);
}
void ExportAudioTab::update_bit_rate_enabled()
{
bool uses_bitrate = !oakengine_encoding_codec_is_lossless(get_codec());
bit_rate_slider_->setEnabled(uses_bitrate);
if (!uses_bitrate) {
bit_rate_slider_->set_tristate();
} else {
bit_rate_slider_->set_value(k_default_bit_rate);
}
}
}
-96
View File
@@ -1,96 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EXPORTAUDIOTAB_H
#define OAK_EXPORTAUDIOTAB_H
#include <QComboBox>
#include <QWidget>
#include "oakutil/define.h"
#include "widget/slider/integerslider.h"
#include "widget/standardcombos/standardcombos.h"
namespace olive
{
class ExportAudioTab : public QWidget {
Q_OBJECT
public:
ExportAudioTab(QWidget *parent = nullptr);
int get_codec() const
{
return codec_combobox_->currentData().toInt();
}
void set_codec(int c)
{
for (int i = 0; i < codec_combobox_->count(); i++) {
if (codec_combobox_->itemData(i) == c) {
codec_combobox_->setCurrentIndex(i);
break;
}
}
}
SampleRateComboBox *sample_rate_combobox() const
{
return sample_rate_combobox_;
}
SampleFormatComboBox *sample_format_combobox() const
{
return sample_format_combobox_;
}
ChannelLayoutComboBox *channel_layout_combobox() const
{
return channel_layout_combobox_;
}
IntegerSlider *bit_rate_slider() const
{
return bit_rate_slider_;
}
public slots:
int set_format(int format);
private:
int fmt_;
QComboBox *codec_combobox_;
SampleRateComboBox *sample_rate_combobox_;
ChannelLayoutComboBox *channel_layout_combobox_;
SampleFormatComboBox *sample_format_combobox_;
IntegerSlider *bit_rate_slider_;
static const int k_default_bit_rate;
private slots:
void update_sample_formats();
void update_bit_rate_enabled();
};
}
#endif // OAK_EXPORTAUDIOTAB_H
-151
View File
@@ -1,151 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "exportformatcombobox.h"
#include <QHBoxLayout>
#include <QLabel>
#include "oakengine/encoding.h"
#include "ui/icons/icons.h"
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
switch (mode) {
case k_show_all_formats:
custom_menu_->addAction(create_header(icon::video, tr("Video")));
populate_type(TrackReference::k_video);
custom_menu_->addSeparator();
custom_menu_->addAction(create_header(icon::audio, tr("Audio")));
populate_type(TrackReference::k_audio);
custom_menu_->addSeparator();
custom_menu_->addAction(create_header(icon::subtitles, tr("Subtitle")));
populate_type(TrackReference::k_subtitle);
break;
case k_show_audio_only:
populate_type(TrackReference::k_audio);
break;
case k_show_video_only:
populate_type(TrackReference::k_video);
break;
case k_show_subtitles_only:
populate_type(TrackReference::k_subtitle);
break;
}
connect(custom_menu_, &Menu::triggered, this,
&ExportFormatComboBox::handle_index_change);
}
void ExportFormatComboBox::showPopup()
{
custom_menu_->setMinimumWidth(this->width());
custom_menu_->exec(mapToGlobal(QPoint(0, 0)));
}
void ExportFormatComboBox::set_format(int fmt)
{
current_ = fmt;
clear();
char buf[256];
oakengine_encoding_format_name(fmt, buf, sizeof(buf));
addItem(QString::fromUtf8(buf));
}
void ExportFormatComboBox::handle_index_change(QAction *a)
{
int f = a->data().toInt();
set_format(f);
emit format_changed(f);
}
void ExportFormatComboBox::populate_type(TrackReference::Type type)
{
const int fmt_count = oakengine_encoding_format_count();
for (int i = 0; i < fmt_count; i++) {
int f = i;
char buf[256];
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 == TrackReference::k_video && has_video) {
// Do nothing
} else if (type == TrackReference::k_audio && !has_video && has_audio) {
// Do nothing
} else if (type == TrackReference::k_subtitle && !has_video && !has_audio && has_sub) {
// Do nothing
} else {
continue;
}
oakengine_encoding_format_name(f, buf, sizeof(buf));
QString format_name = QString::fromUtf8(buf);
QAction *a = custom_menu_->addAction(format_name);
a->setData(i);
a->setIconVisibleInMenu(false);
}
}
QWidgetAction *ExportFormatComboBox::create_header(const QIcon &icon,
const QString &title)
{
QWidgetAction *a = new QWidgetAction(this);
QWidget *w = new QWidget();
QHBoxLayout *layout = new QHBoxLayout(w);
QLabel *icon_lbl = new QLabel();
QLabel *text_lbl = new QLabel(title);
text_lbl->setAlignment(Qt::AlignCenter);
QFont f = text_lbl->font();
f.setWeight(QFont::Bold);
text_lbl->setFont(f);
icon_lbl->setPixmap(icon.pixmap(text_lbl->sizeHint()));
layout->addStretch();
layout->addWidget(icon_lbl);
layout->addWidget(text_lbl);
layout->addStretch();
a->setDefaultWidget(w);
a->setEnabled(false);
return a;
}
}
-78
View File
@@ -1,78 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EXPORTFORMATCOMBOBOX_H
#define OAK_EXPORTFORMATCOMBOBOX_H
#include <QComboBox>
#include <QWidgetAction>
#include "common/trackreferencehandle.h"
#include "widget/menu/menu.h"
namespace olive
{
class ExportFormatComboBox : public QComboBox {
Q_OBJECT
public:
enum Mode {
k_show_all_formats,
k_show_audio_only,
k_show_video_only,
k_show_subtitles_only
};
ExportFormatComboBox(Mode mode, QWidget *parent = nullptr);
ExportFormatComboBox(QWidget *parent = nullptr)
: ExportFormatComboBox(k_show_all_formats, parent)
{
}
int get_format() const
{
return current_;
}
void showPopup();
signals:
void format_changed(int fmt);
public slots:
void set_format(int fmt);
private slots:
void handle_index_change(QAction *a);
private:
void populate_type(TrackReference::Type type);
QWidgetAction *create_header(const QIcon &icon, const QString &title);
Menu *custom_menu_;
int current_ = -1; // was ExportFormat::k_format_count
};
}
#endif // OAK_EXPORTFORMATCOMBOBOX_H
@@ -1,127 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "exportsavepresetdialog.h"
#include <QDialogButtonBox>
#include <QDir>
#include <QLabel>
#include <QMessageBox>
#include <QVBoxLayout>
namespace olive
{
ExportSavePresetDialog::ExportSavePresetDialog(const OakEngineEncodingParams *p,
QWidget *parent)
: QDialog(parent)
, params_(p)
{
auto layout = new QVBoxLayout(this);
name_edit_ = new QLineEdit();
// Populate existing list
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<int>(sizeof(name_buf))) > 0) {
l.append(QString::fromUtf8(name_buf));
}
}
}
if (!l.empty()) {
auto list_widget = new QListWidget();
for (const QString &f : l) {
list_widget->addItem(f);
}
connect(list_widget, &QListWidget::currentTextChanged, name_edit_,
&QLineEdit::setText);
layout->addWidget(list_widget);
}
auto name_layout = new QHBoxLayout();
layout->addLayout(name_layout);
name_layout->addWidget(new QLabel(tr("Name:")));
name_edit_->setFocus();
name_layout->addWidget(name_edit_);
auto btns =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(btns, &QDialogButtonBox::accepted, this,
&ExportSavePresetDialog::accept);
connect(btns, &QDialogButtonBox::rejected, this,
&ExportSavePresetDialog::reject);
layout->addWidget(btns);
setWindowTitle(tr("Save Export Preset"));
}
void ExportSavePresetDialog::accept()
{
if (name_edit_->text().isEmpty()) {
QMessageBox::critical(
this, tr("Invalid Name"),
tr("You must enter a name to save an export preset."));
return;
}
char preset_path_buf[1024];
preset_path_buf[0] = '\0';
oakengine_encoding_preset_path(
preset_path_buf, static_cast<int>(sizeof(preset_path_buf)));
QDir d(QString::fromUtf8(preset_path_buf));
if (!d.exists()) {
d.mkpath(QStringLiteral("."));
}
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?")
.arg(name_edit_->text()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
return;
}
}
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 save preset to \"%1\".").arg(
QString::fromUtf8(full_path)));
return;
}
QDialog::accept();
}
}
@@ -1,55 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EXPORTSAVEPRESETDIALOG_H
#define OAK_EXPORTSAVEPRESETDIALOG_H
#include <QDialog>
#include <QLineEdit>
#include <QListWidget>
#include "oakengine/encoding.h"
namespace olive
{
class ExportSavePresetDialog : public QDialog {
Q_OBJECT
public:
ExportSavePresetDialog(const OakEngineEncodingParams *p, QWidget *parent = nullptr);
QString get_selected_preset_name() const
{
return name_edit_->text();
}
public slots:
virtual void accept() override;
private:
QLineEdit *name_edit_;
const OakEngineEncodingParams *params_;
};
}
#endif // OAK_EXPORTSAVEPRESETDIALOG_H
-80
View File
@@ -1,80 +0,0 @@
#include "exportsubtitlestab.h"
#include <QGridLayout>
#include "oakengine/encoding.h"
namespace olive
{
ExportSubtitlesTab::ExportSubtitlesTab(QWidget *parent)
: QWidget(parent)
{
QVBoxLayout *outer_layout = new QVBoxLayout(this);
QGridLayout *layout = new QGridLayout();
outer_layout->addLayout(layout);
int row = 0;
sidecar_checkbox_ = new QCheckBox(tr("Export to sidecar file"));
layout->addWidget(sidecar_checkbox_, row, 0, 1, 2);
row++;
sidecar_format_label_ = new QLabel(tr("Sidecar Format:"));
sidecar_format_label_->setVisible(false);
layout->addWidget(sidecar_format_label_, row, 0);
sidecar_format_combobox_ =
new ExportFormatComboBox(ExportFormatComboBox::k_show_subtitles_only);
sidecar_format_combobox_->setVisible(true);
layout->addWidget(sidecar_format_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Codec:")), row, 0);
codec_combobox_ = new QComboBox();
layout->addWidget(codec_combobox_, row, 1);
outer_layout->addStretch();
connect(sidecar_checkbox_, &QCheckBox::toggled, sidecar_format_label_,
&QWidget::setVisible);
connect(sidecar_checkbox_, &QCheckBox::toggled, sidecar_format_combobox_,
&QWidget::setVisible);
}
int ExportSubtitlesTab::set_format(int 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);
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(scodec_count == 0);
sidecar_checkbox_->setEnabled(scodec_count > 0);
}
// 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();
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 scodec_count;
}
}
-81
View File
@@ -1,81 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EXPORTSUBTITLESTAB_H
#define OAK_EXPORTSUBTITLESTAB_H
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include "oakutil/qtutils.h"
#include "dialog/export/exportformatcombobox.h"
namespace olive
{
class ExportSubtitlesTab : public QWidget {
Q_OBJECT
public:
ExportSubtitlesTab(QWidget *parent = nullptr);
bool get_sidecar_enabled() const
{
return sidecar_checkbox_->isChecked();
}
void set_sidecar_enabled(bool e)
{
sidecar_checkbox_->setChecked(e);
}
int get_sidecar_format() const
{
return sidecar_format_combobox_->get_format();
}
void set_sidecar_format(int f)
{
sidecar_format_combobox_->set_format(f);
}
int set_format(int format);
int get_subtitle_codec()
{
return codec_combobox_->currentData().toInt();
}
void set_subtitle_codec(int c)
{
QtUtils::set_combo_box_data(codec_combobox_, c);
}
private:
QCheckBox *sidecar_checkbox_;
QLabel *sidecar_format_label_;
ExportFormatComboBox *sidecar_format_combobox_;
QComboBox *codec_combobox_;
};
}
#endif // OAK_EXPORTSUBTITLESTAB_H
-314
View File
@@ -1,314 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#include "exportvideotab.h"
#include <QCheckBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QPushButton>
#include "exportadvancedvideodialog.h"
#include "oakengine/encoding.h"
namespace olive
{
ExportVideoTab::ExportVideoTab(OakEngineColorManager *color_manager, QWidget *parent)
: QWidget(parent)
, color_manager_(color_manager)
, threads_(0)
, color_range_(0) // k_color_range_default
{
QVBoxLayout *outer_layout = new QVBoxLayout(this);
outer_layout->addWidget(setup_resolution_section());
outer_layout->addWidget(setup_codec_section());
outer_layout->addWidget(setup_color_section());
outer_layout->addStretch();
}
int ExportVideoTab::set_format(int format)
{
format_ = format;
const int vcodec_count = oakengine_encoding_format_video_codec_count(format);
setEnabled(vcodec_count > 0);
codec_combobox()->clear();
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 vcodec_count;
}
bool ExportVideoTab::is_image_sequence_set() const
{
ImageSection *img_section =
dynamic_cast<ImageSection *>(codec_stack_->currentWidget());
return (img_section && img_section->is_image_sequence_checked());
}
void ExportVideoTab::set_image_sequence(bool e) const
{
if (ImageSection *img_section =
dynamic_cast<ImageSection *>(codec_stack_->currentWidget())) {
img_section->set_image_sequence_checked(e);
}
}
QWidget *ExportVideoTab::setup_resolution_section()
{
int row = 0;
QGroupBox *resolution_group = new QGroupBox();
resolution_group->setTitle(tr("General"));
QGridLayout *layout = new QGridLayout(resolution_group);
layout->addWidget(new QLabel(tr("Width:")), row, 0);
width_slider_ = new IntegerSlider();
width_slider_->set_minimum(1);
layout->addWidget(width_slider_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Height:")), row, 0);
height_slider_ = new IntegerSlider();
height_slider_->set_minimum(1);
layout->addWidget(height_slider_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Maintain Aspect Ratio:")), row, 0);
maintain_aspect_checkbox_ = new QCheckBox();
maintain_aspect_checkbox_->setChecked(true);
layout->addWidget(maintain_aspect_checkbox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Scaling Method:")), row, 0);
scaling_method_combobox_ = new QComboBox();
scaling_method_combobox_->setEnabled(false);
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
connect(maintain_aspect_checkbox_, &QCheckBox::toggled, this,
&ExportVideoTab::maintain_aspect_ratio_changed);
row++;
layout->addWidget(new QLabel(tr("Frame Rate:")), row, 0);
frame_rate_combobox_ = new FrameRateComboBox();
connect(frame_rate_combobox_, &FrameRateComboBox::frame_rate_changed, this,
&ExportVideoTab::update_frame_rate);
layout->addWidget(frame_rate_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Pixel Aspect Ratio:")), row, 0);
pixel_aspect_combobox_ = new PixelAspectRatioComboBox();
layout->addWidget(pixel_aspect_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Interlacing:")), row, 0);
interlaced_combobox_ = new InterlacedComboBox();
layout->addWidget(interlaced_combobox_, row, 1);
row++;
layout->addWidget(new QLabel(tr("Quality:")), row, 0);
pixel_format_field_ = new PixelFormatComboBox(false);
layout->addWidget(pixel_format_field_, row, 1);
return resolution_group;
}
QWidget *ExportVideoTab::setup_color_section()
{
color_space_chooser_ = new ColorSpaceChooser(color_manager_, true, false);
connect(color_space_chooser_, &ColorSpaceChooser::input_color_space_changed,
this, &ExportVideoTab::color_space_changed);
return color_space_chooser_;
}
QWidget *ExportVideoTab::setup_codec_section()
{
int row = 0;
QGroupBox *codec_group = new QGroupBox();
codec_group->setTitle(tr("Codec"));
QGridLayout *codec_layout = new QGridLayout(codec_group);
codec_layout->addWidget(new QLabel(tr("Codec:")), row, 0);
codec_combobox_ = new QComboBox();
codec_layout->addWidget(codec_combobox_, row, 1);
connect(
codec_combobox_,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
this, &ExportVideoTab::video_codec_changed);
row++;
codec_stack_ = new CodecStack();
codec_layout->addWidget(codec_stack_, row, 0, 1, 2);
image_section_ = new ImageSection();
connect(image_section_, &ImageSection::time_changed, this,
&ExportVideoTab::time_changed);
codec_stack_->addWidget(image_section_);
h264_section_ = new H264Section();
codec_stack_->addWidget(h264_section_);
h265_section_ = new H265Section();
codec_stack_->addWidget(h265_section_);
av1_section_ = new AV1Section();
codec_stack_->addWidget(av1_section_);
cineform_section_ = new CineformSection();
codec_stack_->addWidget(cineform_section_);
row++;
QPushButton *advanced_btn = new QPushButton(tr("Advanced"));
connect(advanced_btn, &QPushButton::clicked, this,
&ExportVideoTab::open_advanced_dialog);
codec_layout->addWidget(advanced_btn, row, 1);
return codec_group;
}
void ExportVideoTab::maintain_aspect_ratio_changed(bool val)
{
scaling_method_combobox_->setEnabled(!val);
}
void ExportVideoTab::open_advanced_dialog()
{
// 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);
d.set_threads(threads_);
d.set_pix_fmt(pix_fmt_);
d.set_yuv_range(color_range_);
if (d.exec() == QDialog::Accepted) {
threads_ = d.threads();
pix_fmt_ = d.pix_fmt();
color_range_ = d.yuv_range();
}
}
void ExportVideoTab::update_frame_rate(Rational r)
{
// Convert frame rate to timebase
r.flip();
for (int i = 0; i < codec_stack_->count(); i++) {
ImageSection *img =
dynamic_cast<ImageSection *>(codec_stack_->widget(i));
if (img) {
img->set_timebase(r);
}
}
}
void ExportVideoTab::video_codec_changed()
{
int codec = get_selected_codec();
switch (codec) {
case OAKENGINE_ENCODING_CODEC_H264:
case OAKENGINE_ENCODING_CODEC_H264RGB:
set_codec_section(h264_section_);
break;
case OAKENGINE_ENCODING_CODEC_H265:
set_codec_section(h265_section_);
break;
case OAKENGINE_ENCODING_CODEC_AV1:
set_codec_section(av1_section_);
break;
case OAKENGINE_ENCODING_CODEC_CINEFORM:
set_codec_section(cineform_section_);
break;
default:
set_codec_section(
oakengine_encoding_codec_is_still_image(codec) ? image_section_ : nullptr);
}
// Set default pixel format
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 {
pix_fmt_.clear();
}
}
void ExportVideoTab::set_time(const Rational &time)
{
for (int i = 0; i < codec_stack_->count(); i++) {
ImageSection *img =
dynamic_cast<ImageSection *>(codec_stack_->widget(i));
if (img) {
img->set_time(time);
}
}
}
}
-230
View File
@@ -1,230 +0,0 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EXPORTVIDEOTAB_H
#define OAK_EXPORTVIDEOTAB_H
#include <QCheckBox>
#include <QComboBox>
#include <QWidget>
#include "oakutil/qtutils.h"
#include "dialog/export/codec/av1section.h"
#include "dialog/export/codec/cineformsection.h"
#include "dialog/export/codec/codecstack.h"
#include "dialog/export/codec/h264section.h"
#include "dialog/export/codec/imagesection.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"
namespace olive
{
class ExportVideoTab : public QWidget {
Q_OBJECT
public:
ExportVideoTab(OakEngineColorManager *color_manager, QWidget *parent = nullptr);
int set_format(int format);
bool is_image_sequence_set() const;
void set_image_sequence(bool e) const;
Rational get_still_image_time() const
{
return image_section_->get_time();
}
int get_selected_codec() const
{
return codec_combobox()->currentData().toInt();
}
void set_selected_codec(int c)
{
QtUtils::set_combo_box_data(codec_combobox(), c);
}
QComboBox *codec_combobox() const
{
return codec_combobox_;
}
IntegerSlider *width_slider() const
{
return width_slider_;
}
IntegerSlider *height_slider() const
{
return height_slider_;
}
QCheckBox *maintain_aspect_checkbox() const
{
return maintain_aspect_checkbox_;
}
QComboBox *scaling_method_combobox() const
{
return scaling_method_combobox_;
}
Rational get_selected_frame_rate() const
{
return frame_rate_combobox_->get_frame_rate();
}
void set_selected_frame_rate(const Rational &fr)
{
frame_rate_combobox_->set_frame_rate(fr);
update_frame_rate(fr);
}
QString current_ocio_color_space()
{
return color_space_chooser_->input();
}
void set_ocio_color_space(const QString &s)
{
color_space_chooser_->set_input(s);
}
CodecSection *get_codec_section() const
{
return static_cast<CodecSection *>(codec_stack_->currentWidget());
}
void set_codec_section(CodecSection *section)
{
if (section) {
codec_stack_->setVisible(true);
codec_stack_->setCurrentWidget(section);
} else {
codec_stack_->setVisible(false);
}
}
InterlacedComboBox *interlaced_combobox() const
{
return interlaced_combobox_;
}
PixelAspectRatioComboBox *pixel_aspect_combobox() const
{
return pixel_aspect_combobox_;
}
PixelFormatComboBox *pixel_format_field() const
{
return pixel_format_field_;
}
const int &threads() const
{
return threads_;
}
void set_threads(int t)
{
threads_ = t;
}
const QString &pix_fmt() const
{
return pix_fmt_;
}
void set_pix_fmt(const QString &s)
{
pix_fmt_ = s;
}
int color_range() const
{
return color_range_;
}
void set_color_range(int c)
{
color_range_ = c;
}
public slots:
void video_codec_changed();
void set_time(const Rational &time);
signals:
void color_space_changed(const QString &colorspace);
void image_sequence_check_box_changed(bool e);
void time_changed(const Rational &time);
private:
QWidget *setup_resolution_section();
QWidget *setup_color_section();
QWidget *setup_codec_section();
QComboBox *codec_combobox_;
FrameRateComboBox *frame_rate_combobox_;
QCheckBox *maintain_aspect_checkbox_;
QComboBox *scaling_method_combobox_;
CodecStack *codec_stack_;
ImageSection *image_section_;
H264Section *h264_section_;
H264Section *h265_section_;
AV1Section *av1_section_;
CineformSection *cineform_section_;
ColorSpaceChooser *color_space_chooser_;
IntegerSlider *width_slider_;
IntegerSlider *height_slider_;
OakEngineColorManager *color_manager_;
InterlacedComboBox *interlaced_combobox_;
PixelAspectRatioComboBox *pixel_aspect_combobox_;
PixelFormatComboBox *pixel_format_field_;
int threads_;
QString pix_fmt_;
int color_range_;
int format_;
private slots:
void maintain_aspect_ratio_changed(bool val);
void open_advanced_dialog();
void update_frame_rate(Rational r);
};
}
#endif // OAK_EXPORTVIDEOTAB_H
@@ -1,24 +0,0 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
add_subdirectory(streamproperties)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/footageproperties/footageproperties.cpp
dialog/footageproperties/footageproperties.h
PARENT_SCOPE
)
@@ -1,340 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
***/
#include "footageproperties.h"
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QComboBox>
#include <QLineEdit>
#include <QDialogButtonBox>
#include <QTreeWidgetItem>
#include <QGroupBox>
#include <QListWidget>
#include <QCheckBox>
#include <QSpinBox>
#include "core.h"
#include "oakengine/footage.h"
#include "oakengine/node.h"
#include "oakengine/timeline.h"
#include "oakengine/undo.h"
#include "oakutil/oaknode.h"
#include "streamproperties/audiostreamproperties.h"
#include "streamproperties/videostreamproperties.h"
#include "widget/viewer/vieweroutpututils.h"
namespace olive
{
FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent,
OakEngineNode *footage)
: QDialog(parent)
, footage_(footage)
{
QGridLayout *layout = new QGridLayout(this);
// WRAPPER-GAP: oakengine_node_get_label_or_name -- emulate inline
// (Node::get_label_or_name(): the label, falling back to the name).
const QString footage_label = oak::Node(footage_).get_label();
setWindowTitle(
tr("\"%1\" Properties")
.arg(footage_label.isEmpty() ? oak::Node(footage_).name() :
footage_label));
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
int row = 0;
layout->addWidget(new QLabel(tr("Name:")), row, 0);
footage_name_field_ = new QLineEdit(footage_label);
layout->addWidget(footage_name_field_, row, 1);
row++;
// Manual source start time: audio/timecode sync relies on this value,
// which is otherwise only auto-detected from file metadata
layout->addWidget(new QLabel(tr("Source Start Time:")), row, 0);
{
QHBoxLayout *start_time_layout = new QHBoxLayout();
OakEngineFootage *start_time_handle = oakengine_footage_borrow(footage_);
int sst_num = 0, sst_den = 1;
const bool has_sst =
oakengine_footage_get_source_start_time(start_time_handle,
&sst_num,
&sst_den) == 1;
source_start_time_enable_ = new QCheckBox(tr("Set"));
source_start_time_enable_->setChecked(has_sst);
start_time_layout->addWidget(source_start_time_enable_);
source_start_time_spin_ = new QDoubleSpinBox();
source_start_time_spin_->setRange(-86400.0, 86400.0);
source_start_time_spin_->setDecimals(3);
source_start_time_spin_->setSuffix(QStringLiteral(" s"));
source_start_time_spin_->setValue(
has_sst ? double(sst_num) / double(sst_den) : 0.0);
source_start_time_spin_->setEnabled(
source_start_time_enable_->isChecked());
start_time_layout->addWidget(source_start_time_spin_, 1);
QString detection_note;
// Detection source comes through the facade (auto-detected field or
// "manual"), matching the engine's stored value.
if (has_sst) {
char source_buf[64];
source_buf[0] = '\0';
oakengine_footage_get_source_start_time_source(
start_time_handle, source_buf, sizeof(source_buf));
const QString source = QString::fromUtf8(source_buf);
detection_note =
(source == QStringLiteral("manual")) ?
tr("(set manually)") :
tr("(auto-detected: %1)").arg(source);
} else {
detection_note = tr("(not detected)");
}
oakengine_footage_free(start_time_handle);
start_time_layout->addWidget(new QLabel(detection_note));
connect(source_start_time_enable_, &QCheckBox::toggled,
source_start_time_spin_, &QDoubleSpinBox::setEnabled);
layout->addLayout(start_time_layout, row, 1);
}
row++;
layout->addWidget(new QLabel(tr("Tracks:")), row, 0, 1, 2);
row++;
track_list_ = new QListWidget();
layout->addWidget(track_list_, row, 0, 1, 2);
row++;
stacked_widget_ = new QStackedWidget();
layout->addWidget(stacked_widget_, row, 0, 1, 2);
int first_usable_stream = -1;
int total_stream_count = 0;
{
OakEngineFootage *count_handle = oakengine_footage_borrow(footage_);
total_stream_count =
oakengine_footage_get_video_stream_count(count_handle) +
oakengine_footage_get_audio_stream_count(count_handle) +
oakengine_footage_get_subtitle_stream_count(count_handle);
oakengine_footage_free(count_handle);
}
for (int i = 0; i < total_stream_count; i++) {
QString description;
bool is_enabled = false;
OakEngineFootage *facade_handle = oakengine_footage_borrow(footage_);
// (track_type, stream_index) pair for this real stream index;
// track types are OAKENGINE_TRACK_TYPE_* ordinals (identical to
// engine Track::Type).
int reference_type = -1;
int reference_index = -1;
oakengine_footage_get_stream_reference(facade_handle, i,
&reference_type,
&reference_index);
switch (reference_type) {
case OAKENGINE_TRACK_TYPE_VIDEO: {
stacked_widget_->addWidget(
new VideoStreamProperties(footage_, reference_index));
is_enabled = oakengine_viewer_get_stream_enabled(
reinterpret_cast<const OakEngineNode *>(footage_),
OAKENGINE_TRACK_TYPE_VIDEO, reference_index) == 1;
{
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 OAKENGINE_TRACK_TYPE_AUDIO: {
stacked_widget_->addWidget(
new AudioStreamProperties(footage_, reference_index));
AudioParams ap = viewer_output_audio_params(footage_, reference_index);
is_enabled = ap.enabled();
{
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 OAKENGINE_TRACK_TYPE_SUBTITLE: {
is_enabled = oakengine_footage_get_stream_enabled(
facade_handle, OAKENGINE_TRACK_TYPE_SUBTITLE, reference_index);
// FIXME: Language?
description = tr("Subtitles");
break;
}
default:
stacked_widget_->addWidget(new StreamProperties());
description = tr("Unknown");
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);
track_list_->addItem(item);
if (first_usable_stream == -1 &&
(reference_type == OAKENGINE_TRACK_TYPE_VIDEO ||
reference_type == OAKENGINE_TRACK_TYPE_AUDIO ||
reference_type == OAKENGINE_TRACK_TYPE_SUBTITLE)) {
first_usable_stream = i;
}
}
row++;
QDialogButtonBox *buttons =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttons->setCenterButtons(true);
layout->addWidget(buttons, row, 0, 1, 2);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(track_list_, &QListWidget::currentRowChanged, stacked_widget_,
&QStackedWidget::setCurrentIndex);
// Auto-select first item that actually has properties
if (first_usable_stream >= 0) {
track_list_->setCurrentRow(first_usable_stream);
}
track_list_->setFocus();
}
void FootagePropertiesDialog::accept()
{
// Perform sanity check on all pages
for (int i = 0; i < stacked_widget_->count(); i++) {
if (!static_cast<StreamProperties *>(stacked_widget_->widget(i))
->sanity_check()) {
// Switch to the failed panel in question
stacked_widget_->setCurrentIndex(i);
// Do nothing (it's up to the property panel itself to throw the error message)
return;
}
}
OakEngineFootage *facade_handle = oakengine_footage_borrow(footage_);
// All writes go through the liboakengine C ABI facade; each call lands
// on the shared undo stack as an undoable command (replacing this
// dialog's own undo command classes with identical semantics).
if (oak::Node(footage_).get_label() != footage_name_field_->text()) {
oakengine_node_set_label(
footage_, footage_name_field_->text().toUtf8().constData());
}
// Apply source start time changes
{
const bool new_enabled = source_start_time_enable_->isChecked();
const Rational new_time =
Rational::from_double(source_start_time_spin_->value());
int cur_sst_num = 0, cur_sst_den = 1;
const bool cur_has_sst =
oakengine_footage_get_source_start_time(facade_handle,
&cur_sst_num,
&cur_sst_den) == 1;
if (new_enabled != cur_has_sst ||
(new_enabled &&
new_time != Rational(cur_sst_num, cur_sst_den))) {
oakengine_footage_set_source_start_time(
facade_handle, new_enabled ? 1 : 0, new_time.numerator(),
new_time.denominator());
}
}
int total_stream_count =
oakengine_footage_get_video_stream_count(facade_handle) +
oakengine_footage_get_audio_stream_count(facade_handle) +
oakengine_footage_get_subtitle_stream_count(facade_handle);
for (int i = 0; i < total_stream_count; i++) {
int reference_type = -1;
int reference_index = -1;
oakengine_footage_get_stream_reference(facade_handle, i,
&reference_type,
&reference_index);
bool new_stream_enabled =
(track_list_->item(i)->checkState() == Qt::Checked);
bool old_stream_enabled = new_stream_enabled;
switch (reference_type) {
case OAKENGINE_TRACK_TYPE_VIDEO:
old_stream_enabled = oakengine_footage_get_stream_enabled(
facade_handle, OAKENGINE_TRACK_TYPE_VIDEO, reference_index);
break;
case OAKENGINE_TRACK_TYPE_AUDIO:
old_stream_enabled = oakengine_footage_get_stream_enabled(
facade_handle, OAKENGINE_TRACK_TYPE_AUDIO, reference_index);
break;
case OAKENGINE_TRACK_TYPE_SUBTITLE:
old_stream_enabled = oakengine_footage_get_stream_enabled(
facade_handle, OAKENGINE_TRACK_TYPE_SUBTITLE, reference_index);
break;
default:
break;
}
if (old_stream_enabled != new_stream_enabled) {
oakengine_footage_set_stream_enabled(
facade_handle, reference_type, reference_index,
new_stream_enabled ? 1 : 0);
}
}
oakengine_footage_free(facade_handle);
void *command = oakengine_undo_command_create_multi();
for (int i = 0; i < stacked_widget_->count(); i++) {
static_cast<StreamProperties *>(stacked_widget_->widget(i))
->accept(command);
}
oakengine_undo_command_free(command); // stream pages write through the facade directly
QDialog::accept();
}
}
@@ -1,105 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_MEDIAPROPERTIESDIALOG_H
#define OAK_MEDIAPROPERTIESDIALOG_H
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QDoubleSpinBox>
#include <QLineEdit>
#include <QListWidget>
#include <QStackedWidget>
struct OakEngineNode;
namespace olive
{
/**
* @brief The MediaPropertiesDialog class
*
* A dialog for setting properties on Media. This can be loaded from any part of the application provided it's given
* a valid Media object.
*/
class FootagePropertiesDialog : public QDialog {
Q_OBJECT
public:
/**
* @brief MediaPropertiesDialog Constructor
*
* @param parent
*
* QWidget parent. Usually MainWindow or Project panel.
*
* @param i
*
* Media object to set properties for.
*/
FootagePropertiesDialog(QWidget *parent, OakEngineNode *footage);
private:
/**
* @brief Stack of widgets that changes based on whether the stream is a video or audio stream
*/
QStackedWidget *stacked_widget_;
/**
* @brief Media name text field
*/
QLineEdit *footage_name_field_;
/**
* @brief Whether a manual source start time should be used
*/
QCheckBox *source_start_time_enable_;
/**
* @brief Source start time in seconds
*/
QDoubleSpinBox *source_start_time_spin_;
/**
* @brief Internal handle to the footage node (set in constructor)
*/
OakEngineNode *footage_;
/**
* @brief A list widget for listing the tracks in Media
*/
QListWidget *track_list_;
/**
* @brief Frame rate to conform to
*/
QDoubleSpinBox *conform_fr_;
private slots:
/**
* @brief Overridden accept function for saving the properties back to the Media class
*/
void accept();
};
}
#endif // OAK_MEDIAPROPERTIESDIALOG_H
@@ -1,26 +0,0 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
dialog/footageproperties/streamproperties/streamproperties.h
dialog/footageproperties/streamproperties/streamproperties.cpp
dialog/footageproperties/streamproperties/audiostreamproperties.h
dialog/footageproperties/streamproperties/audiostreamproperties.cpp
dialog/footageproperties/streamproperties/videostreamproperties.h
dialog/footageproperties/streamproperties/videostreamproperties.cpp
PARENT_SCOPE
)

Some files were not shown because too many files have changed in this diff Show More