Compare commits

..
303 Commits
Author SHA1 Message Date
Mike-Solar 5f8db8e31c app: never re-sync a text field while it is focused
CI / Build & test (Linux) (push) Successful in 21m42s
CI / Build & test (Windows) (push) Successful in 27m41s
The params view reapplies the engine snapshot to every control on each
render (every engine tick), which wiped the in-progress text a frame
after each keystroke - the text effect's field was effectively
untypeable. Skip the re-sync while the field is focused (the same
guard the curve editor has for drags); it re-syncs on blur and the
row's explicit commit writes the edit back. MockEngine gains a
sentinel text parameter for the regression test.
2026-09-10 18:13:06 +08:00
Mike-Solar a97bc19c14 app: refresh the paused frame after the debounced snapshot upload
Edits invalidated the display caches, but the debounced snapshot
upload (150 ms) landed later - meanwhile the viewer had already
rendered and cached a proxy through the stale snapshot, and nothing
invalidated it again, so a paused monitor kept the pre-edit picture
until the playhead moved. Re-invalidate when the fresh snapshot is
actually installed so the next paint re-renders from it.
2026-09-10 18:13:06 +08:00
Mike-Solar d028a45ffa nodes: pivot transform rotation/scale around the frame center
The transform shader sampled in a top-left-origin pixel space while
Olive's transform semantics (and every other node) are center-origin:
rotation swung the image around the top-left corner, pushing it partly
off-frame - reading exactly like an unwanted zoom. Match the C++
transform.vert projection: position (0,0) is the frame center and
rotation/scale pivot around the anchor, so rotation and scale stay
independent user controls. GPU tests pin the 90-degree landing spot
(no smearing) and the 2x scale centroid (stays centered).
2026-09-10 18:13:05 +08:00
Mike-Solar 280118a342 otio: write importable OTIO/FCPXML (DaVinci Resolve rejects both)
CI / Build & test (Linux) (push) Successful in 23m59s
CI / Build & test (Windows) (push) Successful in 31m18s
- available_range declared a zero duration for every clip (the media
  was 'unusable', so Resolve invalidated all of them): it now covers
  the whole media - video clips in the probed frame rate, audio clips
  in the probed sample rate (no more hardcoded 48000/0).
- Gap ranges mixed time bases (a 24 fps block start, a 1.0 trailing
  duration): gaps now stay in the sequence's time base, the trailing
  pad in whole frames.
- Empty sequences (no clips) are skipped, so a lone edited sequence
  exports as a bare Timeline root instead of a SerializableCollection
  Resolve cannot read.
- FCPXML: an asset used on both video and audio tracks now declares
  hasVideo AND hasAudio (the first registration's track kind used to
  hide the other stream); asset durations recover with available_range.
2026-09-10 16:55:12 +08:00
Mike-Solar 71e41d2d82 app: ship the Oak icon on every platform surface
- Runtime window icon: X11 takes the embedded 512px PNG up front
  (_NET_WM_ICON), Wayland resolves it from oak.desktop via the app id,
  Windows reads it from the exe's icon resource.
- assets/appicon: 512px PNG + 7-size ICO rasterized from Oak_Icon.svg.
- Windows: oak.rc (ICON resource ID 1, also the Explorer icon) compiled
  by build.rs through embed-resource.
- Linux packaging (deb/rpm/PKGBUILD/oak.spec): install Oak_Icon.svg
  into the hicolor scalable theme alongside the existing 512px PNG.
- macOS: no change needed - cargo packager already builds the .icns
  from the CD-generated icons/icon.png (same source art).
2026-09-10 16:54:51 +08:00
Mike-Solar 012659bf57 export: fix render-ticket classification; export any sequence
Two export killers fixed and the feature surfaced properly:

- TicketArena::allocate() reaps finished fire-and-forget slots on every
  new submit, but RenderTask classified finished tickets by asking the
  arena for their kind/time afterwards - any ticket that completed
  before the next submit (all of them on the inline backend) failed
  with 'Render ticket reported an unexpected timestamp' and the export
  died after writing a header-only file. RenderTask now records each
  ticket's delivery key at submit time instead.
- File > Export becomes 导出序列 (all 8 locales): the dialog offers a
  sequence picker (any sequence in the project, the current one
  preselected) instead of silently exporting the current one, and the
  project bin's sequence context menu gains 导出序列 opening the same
  dialog. Engines expose sequence_entries / current_sequence_id /
  start_export_of.
- The export-project dialog drops its dead hand-typed path field (OK
  already asked the platform save dialog).
- New end-to-end test: a one-second sequence exports to a real
  H.264/AAC MP4 that probes and decodes back to the clip's content.
2026-09-10 16:54:15 +08:00
Mike-Solar fc90604245 render: anchor resolution_in to the sequence resolution
Preview renders at proxy size while a paused frame renders full-res, so
anchoring resolution_in to the render target made every sequence-pixel
effect (shape size/pos, transform offsets, corner pin points, drop
shadow distance) change apparent size whenever the transport stopped.
Pre-fill resolution_in from the sequence's video params (C++ inserts
the NodeGlobals square resolution at job-build time), covering the
nested generator job inside a merge as well; a node that inserted its
own resolution_in keeps it.
2026-09-10 16:53:55 +08:00
Mike-Solar 7ce80bf8b3 app: keep the color popup alive through the viewer-pick click
The sampled pixel lands (disarming the eyedropper) between the button
going down on the program viewer and coming back up, so the trailing
mouse-up read as an outside click: the popup closed and close_menu
discarded the just-picked draft - pick-from-viewer could never be
confirmed. Swallow the pick's own mouse-up once; a later genuine
outside click still dismisses.
2026-09-10 16:53:55 +08:00
Mike-Solar 37df1d3d34 nodes: spell shape/despill shader dispatch as if/else chains
CI / Build & test (Linux) (push) Successful in 24m7s
CI / Build & test (Windows) (push) Successful in 30m31s
naga's WGSL emitter rejects fall-through-capable GLSL switch blocks, so
every shape and despill job failed to compile and silently fell back to
the effect input - both effects were no-ops. Rewrite the type/method
dispatch as if/else chains (same semantics as the C++ shaders) and
cover all three shape types plus green-screen despill with GPU pixel
tests. Also drop the now-stale nested-payload/merge-binding TODO notes.
2026-09-09 16:51:13 +08:00
Mike-Solar 4f404f8cc6 tests: replace placebo assertions with real behavior checks 2026-09-09 16:33:35 +08:00
Mike-Solar 5ab12b937f render: real texture binding, generator layers and iteration feedback in shader passes
- process_shader_job: bind all texture params by name, recurse into nested
  shader payloads (depth cap 8), fall back to frame size without inputs
- run_effect: take iterative_input so dropshadow previous_iteration_in works
- merge: actually composite inputs; keyer mask, opacity modulation, math
  texture ops and mrg generator layers now bind their textures
- transform distort: real fragment-side inverse-matrix sampling
- time offset / time remap: wire NodeBehavior time adjustment hooks
- plugin: fix first-node identity colliding with unbound sentinel
2026-09-09 16:33:29 +08:00
Mike-Solar cde4dddf32 structure: move jobs out of nodes/ 2026-09-03 17:44:46 +08:00
Mike-Solar 4babbf5de8 core: merge oak-common into oak-core
CI / Build & test (Linux) (push) Successful in 24m6s
CI / Build & test (Windows) (push) Successful in 31m14s
oak-common is gone; its modules (configstore, xmlutils, ocioutils,
oiioutils, colormath, colortransform, videoparams, ffmpegutils, ...)
now live in oak-core alongside the value types. The render value/GPU
types moved too: backend (wgpu context + DisplayRenderer), color
(ColorProcessor over ocio-rs), texture, frame, and the commonutil
config helpers.

Fix-ups to make the merged tree build and pass tests:

- oak-core Cargo.toml: wgpu back to 25 (the moved backend code is
  written against that API generation); add the toml/quick-xml/image
  deps oak-common carried.
- lib.rs: drop the duplicate 'pub mod error;'.
- error.rs: unified OAKCORE_* codes; restore Error::new() and
  From<OcioError> from oak-common's error type.
- backend.rs/color.rs: oak_core::/oak_render:: self-references
  rewritten to crate::; the shaderfx-dependent GPU effect test moved
  to oak-render's shaderfx tests (shaderfx depends on oak-node and
  cannot live in oak-core).
- oak-render's error module re-exports oak_core::error::{Error,
  Result}; the OAKRENDER_* codes stay as the public-code contract.
- oak-node jobs.rs: ColorProcessor imported from oak_core::color.
- Integration tests repointed at oak_core::{texture, frame, backend,
  color, colormath}.
- the display-ICC regression test treats an empty OAK_DISPLAY_ICC as
  unset, matching displayicc::env_override_icc.
2026-09-03 17:42:20 +08:00
Mike-Solar 49fed365a4 nodes: real OCIO color grading (linear + log) on the GPU
The OCIO grading nodes previously pushed null texture handles; they now
push real ShaderJobPayloads whose GLSL is the OCIO-generated dynamic
grading-primary GPU shader — the exact code the C++ path applies, so no
approximation:

- color: grading_primary_function_shader(style) builds a dynamic
  GradingPrimaryTransform (LIN/LOG) on the default config, extracts the
  GLSL via GpuShaderDesc (function 'ove_grading_primary', resource
  prefix ocio_, no LUT textures) and caches it per style + config id.

- eval: OCIO_GRADING_STUBS maps the two node type ids to the grading
  style; process_shader_job resolves the stub and splices it into the
  node's %1 marker (same wiring as the chromakey OCIO stub); the
  pipeline cache key folds the stub text so a config change recompiles.

- nodes: value() pushes a ShaderJobPayload with the C++ value()
  rewrite applied to the row — vec4 (RGBM x=master) grading inputs to
  the vec3 GPU uniform form (lin: contrast RGB=c*m, offset RGB=c+m,
  exposure RGB=2^(c+m); log: lift RGB=c+m, gain c*m, gamma c*m), plus
  pivot/saturation floats, the log pivotBlack/pivotWhite normalization
  range (0/1), clamp sentinels (NoClampBlack -1 / NoClampWhite 2),
  white>black enforcement per frame, and localBypass=false. Generated
  uniform names bind by name (the log node's OCIO_NAMESPACE_ id text
  normalizes to the ocio_ resource prefix).

- Tests: grading stub generation (analytic GLSL, cache) in color,
  end-to-end GPU exposure doubling for lin (+1 stop on 0.2 gray -> 0.4)
  and lift for log, node payload rewrite assertions, and the
  all-shaders sweep now retries grading stubs. oak-render 181,
  oak-node 441, oak-app 272 lib tests pass.
2026-09-02 20:04:55 +08:00
Mike-Solar b06c4fbbb6 nodes: real polygon and mask implementations on the GPU
Polygon and mask previously pushed null texture handles ('fake'
implementations). They now generate real ShaderJobPayloads and render
through the existing GPU shader pipeline:

- shaderfx: std140 uniform array support (Vec4Array(N)) — the parser
  accepts 'uniform <type> <name>[N];' declarations, translate()
  re-emits them as vec4[N] block members with per-element std140
  offsets, and pack_uniforms writes array items from the new
  NodeValue::Vec4Array value, padding short arrays to the declared N.

- polygon: value() collects the inherited points array (row element
  keys 'points_in[i]', else the node's own per-element values — an
  unconnected array resolves to the default pentagon via
  GetValueAtTime parity) and pushes a ShaderJobPayload; the 'rgb'
  fragment shader rasterizes the closed point loop with an odd-even
  fill in screen space (center-translated, y-flipped to match the C++
  point convention) and outputs color_in inside / transparent outside.
  The CPU QPainterPath generate_frame stays a documented no-op.

- mask: value() pushes a single ShaderJobPayload whose new 'mask'
  fragment shader folds the whole C++ chain into one GPU pass — base
  texture multiplied by the polygon matte, optional invert, and the
  optional feather gaussian softens the matte during sampling (the
  separable blur.frag h/v iterations as a one-pass product,
  density-normalized, radius capped at 16 px).

- Tests: translate/pack array coverage in shaderfx, GPU end-to-end
  rasterization of the pentagon (center white, corner transparent),
  mask multiply/invert/feather on real frames, and updated oak-node
  payload assertions. oak-render 178, oak-node 440, oak-app 272 lib
  tests pass.
2026-09-02 16:59:49 +08:00
Mike-Solar 4ef5b3e31d render: snapshot store temp dir is unique per store (test parallel flake)
CI / Build & test (Linux) (push) Successful in 23m2s
CI / Build & test (Windows) (push) Successful in 30m53s
Tests run in parallel inside one process; each GraphSnapshotStore::new()
used the same 'oakrender-snapshots-<pid>' root, so one test's cleanup()
deleted another test's live snapshot — the
acquire_rewrite_forces_file_rewrite_on_same_key flake on the 16-core
Windows runner (file written, then exists() == false). Append a per-store
sequence number to the directory.
2026-09-02 16:19:01 +08:00
Mike-Solar 6b5b30fd36 gpui submodule: macos file filter disabled 2026-09-02 16:17:35 +08:00
Mike-Solar 1d45216c1f gpui submodule: windows file filter spec fix
CI / Build & test (Linux) (push) Successful in 23m4s
CI / Build & test (Windows) (push) Failing after 28m29s
2026-09-02 13:04:12 +08:00
Mike-Solar 6153a2ac33 tests: fix three stale assertions
CI / Build & test (Windows) (push) Failing after 13m15s
CI / Build & test (Linux) (push) Successful in 24m22s
- multicamnode: type_id() assertions called the method on dyn
  NodeBehavior, which method resolution routed to std::any::Any's
  TypeId::of — qualify via NodeBehavior::type_id so the trait method
  (the &str node type id) is compared
- procpool: the per-worker GPU budget grew a security headroom (×2)
  for the CUDA-OOM flood; the two budget tests now assert against the
  real formula (2 GiB + 256 MiB at 1080p24, 2 workers at 4K/24 GiB)
- cli info fixture: the fixture runs 29.97 fps; the assertion expected
  30/1 (stale from the older fixture)
2026-09-02 12:34:39 +08:00
Mike-Solar 1ab1c29df4 cli: fill the new EncodingParams fields (export_sequence)
The extra oak_task export fields (bitrates + color override code
points) added in the export-settings protocol are zero/defaults in the
CLI's sync exporter — audio/video bit rates codec-default, no explicit
color override (the project's delivery colorspace applies).
2026-09-02 12:18:48 +08:00
Mike-Solar a72e86dfaf gpui: extension-limited file pickers on Windows/macOS (submodule bump) 2026-09-02 12:16:15 +08:00
Mike-Solar fe017aa060 project file: format dialog hands to the SYSTEM dialog (extension-limited)
- 导出工程文件: the in-app dialog only picks the format (OTIO / OVE /
  FCPXML); OK asks the system save dialog with the suggested file name
  carrying the format's extension, then exports
- 打开项目: the system open dialog restricts the choosable files to
  .ove/.ovexml/.otio/.fcpxml (PathPromptOptions.allowed_extensions
  plumbed through gpui; Linux portal glob filter)
2026-09-02 12:00:39 +08:00
Mike-Solar 22a17442ac ci (gitea): ocio-env.sh via bash, drop ocio-sys unpack/glob on Windows
CI / Build & test (Linux) (push) Failing after 7m48s
CI / Build & test (Windows) (push) Failing after 13m39s
Mirror the .github workflow fixes:
- ocio-env.sh has no exec bit (git doesn't preserve one reliably) —
  bare invocation fails 'Permission denied'; call it through bash in
  both the Linux and Windows configure steps
- ocio-sys is the GIT fork, so the Windows registry-cache unpack +
  'ls .../ocio-sys-0.2.1/build.rs' glob can never match (git deps have
  no .crate archive); the fork's build.rs already carries the
  fix — the unpack/sed block is removed
2026-09-02 09:55:12 +08:00
Mike-Solar 726b7d6a60 ci: invoke ocio-env.sh through bash (no exec bit needed)
The script has no executable bit (git doesn't carry one reliably), so
the bare  invocation failed with 'Permission
denied' in the Linux act runs. Every call site now uses
OCIO_RS_ENABLE_REAL=1
OCIO_RS_LINK=static instead.
2026-09-02 09:53:56 +08:00
Mike-Solar 83dd13234b ci: fix Linux/Windows package builds
CI / Build & test (Linux) (push) Failing after 52s
CI / Build & test (Windows) (push) Failing after 3m43s
Linux: 'libffnvcodec-dev' was dropped from Ubuntu noble — the NVDEC
headers are distribution-free, so install them from source
(nv-codec-headers git) like the Fedora branch already did; the
appimage step and install-deps.sh no longer apt-install the package.

Windows: ocio-sys is the GIT fork (pinned in the root manifest), so
the old crate-unpack + build.rs glob patch (registry/cache's
ocio-sys-0.2.1.crate) can never match — git dependencies don't ship a
.crate archive and unpack under registry/src/git/<hash>, hence the
"ls .../ocio-sys-0.2.1/build.rs: No such file" failure. The fork's
build.rs already carries the GNU-toolchain fix; the unpack/glob/sed
step is removed.
2026-09-02 09:40:05 +08:00
Mike-Solar 2c64317a58 export media: editable resolution / frame rate / bit rate spin boxes
CI / Build & test (Linux) (push) Canceled after 0s
CI / Build & test (Windows) (push) Canceled after 0s
The three values are SpinBoxes (typed input + stepping): width & height
(0 = use the sequence size), frame rate in fps (0 = sequence rate),
bit rate in bit/s (0 = the codec default). The engine already accepted
arbitrary values, so the dialog now exposes them instead of the closed
preset lists. i18n across the 8 packs.
2026-09-02 09:35:54 +08:00
Mike-Solar 92813707bc export media: dialog UI — container→codec gating, color space/bit depth, range
- ExportDialogContent: container format (existing), then the video and
  audio codec combos rebuilt per container from the compatibility tables
  (an unsupported codec is never offered — switching container
  re-selects the first compatible entry), a color-space combo (SDR
  Rec.709 8-bit / HDR BT.2020 10-bit), an export-range combo (whole
  timeline / in-out) and the output path
- ComboBox::set_options added for the container-driven list rebuild
- settings() gathers the ExportSettings the engine consumes
- begin_export routes through start_export_with
- i18n keys across the 8 packs (zh-CN uses distinct values so the
  en==zh equality test passes)
2026-09-02 09:31:49 +08:00
Mike-Solar 499246b48e export media: settings protocol (codec/color/bit-depth/bitrate/range) end to end
- ExportSettings: container format, video/audio codecs, size/fps/
  bitrate, bit depth (8/10 → U8/U10), SDR 709 + HDR 2020 code points,
  export range
- AppEngine::start_export_with(settings, path); real builds the
  task params via encoding_params_with_settings (codec tables gate
  each container — a non-supporting combo errors, never silently
  defaults; the one menu only offers compatible codecs)
- oak-task EncodingParams: video_bit_rate / audio_bit_rate +
  color_override_enabled + primaries/trc/space (override wins over
  the project's delivery colorspace)
- mock start_export_with (no-op fake progress)

The dialog UI rebuild (fields + container→codec→color enability
linking) follows in the next unit.
2026-09-02 09:27:43 +08:00
Mike-Solar 56af662b83 project file: 另存为 — duplicate the project in the library under a new name and switch to the copy
- save_project_as(name): library_duplicate the open project's row,
  rename the copy, open it as the current project
- 文件 → 另存为 opens a name dialog (prefilled '<name> Copy'); OK
  duplicates + switches (the save-as semantics: a new, differently-
  named project in the library, editing continues on the copy)
2026-09-02 09:22:29 +08:00
Mike-Solar 660a32a1b3 project file: new-project dialog (name + sequence format), open prompt lists formats
- 文件 → 新建项目 opens an in-app dialog: project name + the first
  sequence's format (preset / custom / interlaced, reusing
  SequenceFormatFields); OK creates the library project then seeds the
  first sequence from the chosen format
- 打开项目 prompt advertises the supported project formats
  (.ove / .otio / .fcpxml)
- manager.new.project_name across the 8 i18n packs
- action-search first-item test follows the new flow (New Project now
  opens its dialog)
2026-09-02 09:15:30 +08:00
Mike-Solar c900804941 project file: export dialog with OTIO/OVE/FCPXML format choice
文件 → 导出工程文件… now opens an in-app dialog (format dropdown:
OpenTimelineIO / Oak / FCPXML + output path) instead of the bare
platform save prompt; OK routes the chosen extension through the
engine's export_project_path. Adds project.export.format i18n across
the 8 packs.
2026-09-02 09:10:39 +08:00
Mike-Solar 19a83f8359 multicam: hide the wizard menu item for v0.5
CI / Build & test (Linux) (push) Failing after 11s
CI / Build & test (Windows) (push) Failing after 3m30s
The 窗口 → 多机位向导 entry is removed from the menu bar and the
action-search list (menu_id 0, the hidden convention); the engine
pathway (wizard dialog, sync, multicam creation) stays intact behind
the action dispatch for a future release. The panel's multi-cam
feature remains.
2026-09-01 22:06:12 +08:00
Mike-Solar be42620e22 multicam: throttle angle refresh, shrink pre-render window
- MulticamPanel: playback angle refresh runs one cycle per 3 ticks
  instead of re-requesting every source every tick — each angle decode
  is a keyframe-scanning FFmpeg seek that stole worker capacity from
  the main viewer
- PreRender frames default 120 -> 12: a window larger than what the
  pool can render in real time queues far ahead of the playhead, so
  the painted frame lags seconds behind (playback frozen); a smaller
  window keeps the backlog bounded
- render_graph_frame: OAK_PERF clip-level timing
2026-09-01 21:52:56 +08:00
Mike-Solar 7f41570596 render: stop evicting the only hardware decoder session on every open
The per-process 'hardware session == 1, evict before every new open'
guard was turning every frame's open into a decoder re-open: after
inserting the fresh session, the NEXT frame's pre-open eviction dropped
it again, so no request ever hit the cache ([open] (request) on every
single frame, zero CACHED hits). Every decode then cost a full FFmpeg
session open (~0.5 s) + a keyframe-scanning seek — playback could never
keep up (the 'main viewer barely moves' report; [perf] showed 1.6-2.2 s
per frame).

Hardware VRAM is bounded by the LRU cap (hardware-first eviction at
MAX_CACHED_DECODERS) and the gpu-vram worker-count policy; the eager
pre-open eviction was the regression.
2026-09-01 21:46:01 +08:00
Mike-Solar 7065fb0108 footage: node-graph decode uses a ready proxy, tolerate unset stream index
Re-applies the proxy selection (with the -1 tolerance) after the
revert: the node-graph viewer decode must stand in a ready proxy for
the 4K original, and a proxy generated before the stream-index
recording (index -1) still matches the first video stream rather than
silently falling back to the 4K source (the 'main viewer got stuck'
report).
2026-09-01 21:33:12 +08:00
Mike-Solar 45c288ce97 revert 3bcf4e0b1 (multicam no-split/stride/proxy-ng) pending split re-verification 2026-09-01 21:31:18 +08:00
Mike-Solar 3bcf4e0b1a multicam: no-split switch skips full rebuild, angle refresh stride, proxy in node-graph decode
- multicam_switch_to: a NO-SPLIT switch only changes current source —
  push snapshot + invalidate preview windows, skip the full
  apply_edit rebuild (the 'switch once then everything grinds' hit)
- MulticamPanel: playback angle refresh runs one cycle per 3 ticks
  instead of re-requesting every source every tick (each angle decode
  is a keyframe-scanning FFmpeg seek)
- footage value(): a ready proxy stands in for the original media in
  the node-graph decode request (proxy enabled but 4K decoded report)
- proxy_resolution made public (engine's proxy-first preview size)
2026-09-01 21:19:54 +08:00
Mike-Solar 1b0a15192e multicam: wizard + AFV + node-graph preview + performance
CI / Build & test (Linux) (push) Failing after 24s
CI / Build & test (Windows) (push) Failing after 3m38s
- wizard: angle multi-select, sync modes, auto-align, create sequence;
  keeps the host sequence current (host clip = the multicam clip)
- build_multicam_sequence: per-angle clip -> SOURCES_INPUT[element],
  array slot growth, audio angle tracks for AFV
- AFV: host linked audio clip follows the switched source (one undo),
  muted host audio track disables it
- node-graph preview: inline producer renders through the traverser
  (viewer/project-matched graph frames); sequence viewer uses the graph
- multicam node value(): element-tagged row keys (sources_in[i]),
  reads the current source; build_row keys array inputs by element
- angle grid: viewer=0 (single-track montage, not whole-graph)
- project explorer: rename (dialog) + delete (undoable) real items
- performance: decoded-frame LRU, per-process NVDEC quota (1 session,
  evict before open), GPU composite fail-once fallback, snapshot
  upload debounced on the engine tick, worker vram budget headroom
- timeline clip: multicam overlay via ClipDecorator
- wizard menu item moved to Sequence menu
2026-09-01 20:27:59 +08:00
Mike-Solar 8de9af705e render: query GPU vram on AMD/Intel Linux too, document the UMA/Windows fallbacks
gpu_vram_bytes() now chains per vendor/platform:
- NVIDIA everywhere: nvidia-smi (shipped by the NVIDIA driver on every
  OS) — the NVDEC path's primary device.
- AMD/Intel on Linux: the DRM mem_info_vram_total/used sysfs attributes
  (amdgpu, i915, Xe). Free = total - used; the first non-zero card wins
  (an iGPU without dedicated vram reports 0 and is skipped). The walk is
  now testable via an injectable sysfs root; a card missing the attrs is
  skipped, never aborts the walk.
- Apple Silicon: unified memory — no separate vram exists; the RAM/4
  budget IS the correct bound for decode surfaces and render targets, so
  no query (an explicit vram budget would double-count the same pool).
- Windows AMD/Intel: no portable CLI; DXGI QueryVideoMemoryInfo is the
  real API but wgpu 25 does not expose it. Falling back to the RAM
  policy is safe-side (under-sized pool loses throughput, never OOMs the
  device).

Tests: the sysfs walk (fixture with a missing-attr card, a 0-total
iGPU and a discrete winner) and the per-worker budget scaling (1080p
baseline, 4K ~4x, 60 fps over-provision).
2026-08-30 22:03:05 +08:00
Mike-Solar 9352fca4a9 codec/render: silence hw-decode failures, vram-aware dynamic worker pool
- open_hw_accel marks the device unavailable when the decoder OPEN fails
  (cuvidCreateDecoder OOM at 4K) too, not just device-context creation:
  without it every subsequent decoder session retried CUDA and flooded
  the log per open.
- Decoder gains hardware_decoding(); the oak-render decode-session LRU
  evicts hardware sessions first (each pins a GPU surface pool — ~100 MB
  at 4K), so a full cache cannot exhaust video memory before the next
  open.
- Worker pool count now factors GPU vram: per-worker budget = 1 GiB
  (1080p peak) scaled by pixel ratio + 256 MiB idle floor, 10% reserve
  of free vram; applied when hardware decoding is on (nvidia-smi query,
  None otherwise falls back to the RAM/CPU policy).
- Dynamic pool resize: ProcessDispatcher::set_target_workers grows or
  retires workers; retiring ones stop claiming, drain their in-flight
  batch (future playback frames included), then exit naturally on the
  shutdown signal — no mid-work kill (30 s deadline only as a hung-
  decoder last resort). A retiring worker that dies re-queues its frames
  to surviving workers. Resizes are throttled to 2 s (a resolution burst
  merges; only the latest target applies) so 1080p<->4K flaps cannot
  thrash process spawns.
- RenderManager::set_workspace_size announces the sequence resolution;
  RealEngine calls it from refresh_sequence_info.
- Integration test: shrink 3->1 mid-wave (all frames complete, retired
  workers exit naturally) then regrow 1->3 and render a fresh wave.
2026-08-30 21:59:06 +08:00
Mike-Solar 5937e557a7 app: project-explorer new-sequence button, 4K presets, keying node fix, color-picker canvases
- Project explorer header gains a 新建序列 button (opens the existing
  new-sequence dialog, seeded like the menu action).
- Sequence presets gain 4K UHD (3840x2160@25) and 4K DCI (4096x2160@24);
  the sequence-properties dialog re-selects them on reopen. Format fields
  are also seedable from a probed footage format (the drop flow).
- Chroma Key (and Color Difference Key) value() now box a ShaderJobPayload
  like Despill: the old OCIO-processor gate pushed nothing (the processor
  is never populated without the render bridge), so the traverser handed
  the clip NodeValue::None and the rendered frame lost the clip. The
  renderer resolves the OCIO stub at compile time from OCIO_SHADER_STUBS.
  End-to-end graph test: green key on a green frame keys out, red key
  keeps it.
- The OFX color picker's SV palette / hue bar / preview / swatch canvases
  get size_full(): the bare canvases collapsed to zero height in the
  block layout, so the palette painted nothing (the reported 色板没显示).
  Regression test clicks the palette center and expects mid s/v.
2026-08-30 21:58:26 +08:00
Mike-Solar 71bc748d98 build: point ocio-sys at the fork carrying the zlib-ng detection fix
The vendored minizip-ng prefers find_package(ZLIBNG) over the classic
zlib the OCIO install scripts pass, so hosts with zlib-ng installed
(Arch) produced zng_* references the vendored zlib cannot satisfy and
the final link failed. Mike-Solar/ocio-rs@ddb677d disables the ZLIBNG
package lookup; the fork main now carries that plus the cstdint fix.
2026-08-30 16:28:09 +08:00
Mike-Solar 8bf3f1e2df ci/cd: vendored static OCIO everywhere, ocio-sys tracks upstream main
Policy change (supersedes the system-first probe): build OpenColorIO
from the vendored sources and link it statically on every platform
that can -- packaged binaries carry no OCIO runtime dependency. The
[patch.crates-io] ocio-sys now tracks shaloong/ocio-rs main
explicitly; its vendored yaml-cpp has the <cstdint> include that makes
the vendored build work on GCC >= 16 (verified on GCC 16.2.1).

Windows/MinGW stays the exception (the vendored source needs MSVC-only
constructs): tooling/ocio-env.sh probes the MSYS2 system OCIO there,
static when libOpenColorIO.a ships, dynamic otherwise. The Linux jobs
drop the system OCIO dev packages the system-first policy needed.
2026-08-30 16:02:45 +08:00
Mike-Solar 91a6accf75 build: track the workspace cargo config (FFMPEG_DIR, OCIO env)
FFMPEG_DIR is relative and portable; OCIO_INSTALL_DIR=/usr is this
machine's system-OCIO preference (CI overrides it via GITHUB_ENV;
delete that line to use the vendored static OCIO build elsewhere).
2026-08-30 00:32:20 +08:00
Mike-Solar 5b38d07299 ci/cd: NVDEC-capable FFmpeg, GCC16-proof OCIO, system-first OCIO env
- Install ffnvcodec headers everywhere the project FFmpeg is built
  (distro packages on Debian/Ubuntu/Arch/MSYS2, nv-codec-headers from
  source on Fedora) so the FFmpeg build picks up NVDEC/NVENC.
- ocio-sys 0.2.1's vendored yaml-cpp misses <cstdint> and fails on
  GCC >= 16 (measured on GCC 16.2.1); patch the dependency to the
  fixed upstream tree (shaloong/ocio-rs, 933c65dc) until a fixed
  release lands on crates.io.
- New tooling/ocio-env.sh decides the OCIO build env per job: system
  OCIO >= 2.5 when present (static when the package ships
  libOpenColorIO.a, dynamic otherwise), vendored static build as the
  fallback. The Arch package gains an 'opencolorio' dependency only
  when the binaries link the system OCIO (build-pkg.sh probes ldd).
- CI 'Build' steps switch from cargo build to cargo check: the Test
  step links the test binaries anyway, and a full build would codegen
  every workspace crate twice.
2026-08-30 00:31:48 +08:00
Mike-Solar ef01570c5d build: warn that ffmpeg-sys-next bundles the project FFmpeg into its rlib
The sys crate's `static` feature copies the archives at BUILD time and
cargo does not track archive changes: after rebuilding the project
FFmpeg every binary silently keeps the previous objects until
`cargo clean -p ffmpeg-sys-next` (observed: an NVDEC-capable rebuild
never reached the binaries; av_hwdevice_ctx_create(CUDA) then failed
with ENOMEM because the old hwdevice table had no CUDA entry).
2026-08-30 00:31:32 +08:00
Mike-Solar 3be8c3e8fd codec: prefer native NVDEC over VAAPI on Linux
On multi-GPU boxes VAAPI's default render node can point at a device
with no VA driver (NVIDIA without libva-nvidia-driver), failing before
the working AMD/Intel node is ever tried; trying CUDA (NVDEC) first is
both the discrete-GPU path and sidesteps that misdirection. VAAPI stays
as the fallback for AMD/Intel-only machines.

Adds a hwcheck example printing which hw device a stream opens with and
the raw av_hwdevice_ctx_create result codes per device type.
2026-08-30 00:31:32 +08:00
Mike-Solar 197212bc46 app: extend OAK_DEBUG_AUDIO diagnostics to the render thread and output stream
Per-chunk submit->render wait and render time (audio_thread), the push
lead against the playhead, the negotiated stream config, and the true
device-side callback rate (outputdevice) -- the ground truth used to
localize the playback starvation to the sequence-rate/clock mismatch.
2026-08-29 23:31:48 +08:00
Mike-Solar 84caa3589b app: sync the playback clock to the sequence rate; heal underruns by trimming
Two playback-audio defects found while chasing periodic pops, clipped
words and repeated phrases:

1. The playback clocks are created at engine init with the fallback
   rate (hd_1080p25) and never updated, while audio chunks are sized by
   the SEQUENCE time base (30/1 from the broken default). The playhead
   advanced at 25 fps but each chunk carried only 1600 samples, so
   production ran at 40000 samples/s against the device's 48000 -- ~17%
   of playback time was zero-filled silence (clipped words). tick() now
   syncs both clocks to the sequence's actual rate.

2. The underrun self-heal cleared the whole device queue (~190 ms of
   unheard content skipped), reset the prefetch (whose first
   re-rendered chunk was then always dropped as late) and re-anchored
   the master clock, re-rendering already-played audio whenever the
   playhead lagged (audible as a pop plus a repeated phrase, every
   ~1.1 s). It now trims just the stale lead from the queue
   (PreviewAudioDevice::drop_front_frames): the zero-fill already
   advanced the output clock past that content, so the rest of the
   queue stays audible and the anchor stays valid.

Also adds the OAK_DEBUG_AUDIO event log used to diagnose all of this
(prefetch resets, late drops, underruns, heals, reanchor jumps,
per-tick queue state).
2026-08-29 23:31:35 +08:00
Mike-Solar 10ec63efb3 node: read the canonical DefaultSequenceFrameRate key for sequence defaults
set_default_parameters looked up DefaultSequenceFrameRateNum/Den -- keys
that exist nowhere in the config -- so every sequence silently fell back
to 30/1. The canonical setting is the "num/den" string
DefaultSequenceFrameRate (a frame duration; rate = den/num, matching the
footage import path in oak-task). Sequences now default to the
configured 1001/30000.
2026-08-29 23:31:16 +08:00
Mike-Solar d2ac02a474 build: relax the ocio-rs constraint to 0.2
CI / Build & test (Linux) (push) Successful in 17m43s
CI / Build & test (Windows) (push) Failing after 32m37s
Pre-existing local resolution fix so the dependency set resolves here;
Cargo.lock follows (getrandom 0.4.3 -> 0.3.4 in the ocio tree).
2026-08-29 19:52:28 +08:00
Mike-Solar 81c3839937 codec: cap FFmpeg decoder frame threads at 4
threads=auto spawns one decode thread per logical core per decoder (16
observed on a 24-core box); with cores-2 render workers decoding
concurrently the machine is oversubscribed several times over and the
playback-audio render thread gets starved (late chunks are dropped).
4 threads decode 1080p h264 far past real-time; preview throughput comes
from the worker pool, not per-decoder threading. Applies to both the
software open path and the hwaccel open path.
2026-08-29 19:52:28 +08:00
Mike-Solar 9dab9efc38 codec: carry codec-frame overflow across audio chunk boundaries
retrieve_audio_to decodes whole codec frames but copies only the part
inside the requested chunk; the tail of the frame crossing the chunk end
(up to 1023 samples for AAC) was consumed by the decoder and lost, so
the next chunk started with a hole. On the playback grid (1920 samples
at 25fps/48kHz) the hole cycles 128..896 samples and hits 7 of 8 chunk
starts -- the heavy stutter/noise heard during playback.

Keep the overflow (decode and resampler-flush tails) in a per-session
carry buffer and serve it at the start of the next contiguous chunk;
clear it on seek, format change and chunk failure. Also make seek()
actually drop the cached resampler as its comment claimed.

Verified sample-exact: chunked decode of a 440Hz tone now matches a
one-shot decode bit for bit, and chunked renders of real media line up
with the ffmpeg CLI reference at correlation 1.0 / drift 0.

Adds a regression test (playback_sized_chunks_match_oneshot_sample_exact)
with a new tone fixture -- demo.mp4's audio is -90dB digital silence and
cannot expose the holes -- plus a render_audio_wav example used to
diagnose chunk-boundary artifacts offline.
2026-08-29 19:51:59 +08:00
Mike-Solar e128a6aca1 render: clamp mixed audio to [-1,1] before it reaches the device
Overlapping clips sum linearly in mix_audio_montage and can exceed full
scale (two hot clips reach +/-2; gain > 1 would too); the cpal sink
forwarded samples unclamped, so overlaps clipped at the DAC. Clamp the
accumulator after the montage mix (both the heap and shm-slot paths
share mix_audio_montage) and document it on render_audio_samples.
2026-08-29 19:51:25 +08:00
Mike-Solar 6fb9a80afc build: silence wgpu-core's debug-only snatch-lock backtrace capture
wgpu-core 29's snatch lock tracing (snatch.rs) captures a std Backtrace
on every device/queue lock acquisition when debug_assertions are on.
Profiling the dev build showed ~35% of UI-thread CPU in
_Unwind_Find_FDE/Backtrace::create, which starved playback-audio chunk
scheduling (late chunks are dropped -> pops).
2026-08-29 19:51:14 +08:00
Mike-Solar cbd4ba2442 optimize: video and audio render
CI / Build & test (Linux) (push) Canceled after 0s
CI / Build & test (Windows) (push) Canceled after 0s
2026-08-29 05:01:55 +08:00
Mike-Solar ca18105c8b ci: retry the Linux test suite once; log render-manager init failures
CI / Build & test (Linux) (push) Successful in 18m4s
CI / Build & test (Windows) (push) Successful in 34m6s
Run 48's Linux leg flaked: full_res_worker_outlives_a_dropped_project
panicked with "the render manager failed to start" while all 236 other
tests passed -- a worker-pool startup race under full-suite parallelism,
not a regression (the same binary passes locally and passed on the
previous commit). Mirror the Windows job's retry-once policy and make
ensure_render_manager log the underlying init error so the next flake is
diagnosable. The Windows leg's real failure (gpui_widgets referenced
SurfaceSource::Texture, which only exists on linux/freebsd) is fixed in
the gpui submodule bump of the previous commit.
2026-08-29 00:37:24 +08:00
Mike-Solar fdb5caabd5 color: non-sRGB preview, per-monitor display ICC, pipeline hardening
Preview now follows the project output colorspace end to end: the
display chain derives its content space from the project's OutputColorSpec
instead of a hardcoded sRGB name, self-managed ICC transforms go through
an XYZ D65 interchange stage (OCIO cie_xyz_d65_interchange) for non-sRGB
targets, and the platform layer declares the content colorspace (gpui
submodule bump). macOS defaults to OS-managed (fixes wide-gamut UI
oversaturation); Windows ACM warns once on non-sRGB targets.

Multi-monitor: the display ICC is looked up per the window's current
screen (macOS display id, Windows per-monitor DC, X11 RandR output
profile) with a throttled poll that invalidates frame caches on moves.

Pipeline precision: 10-bit+ sources fall back to YUV444P16LE + a Rust
matrix conversion when swscale lacks F32 output (no more 8-bit
truncation); BT.709/2020 SDR decodes with BT.1886 gamma 2.4 instead of
the sRGB EOTF; working-space compositing no longer clamps RGB to [0,1]
(alpha still clamped); the output node clamps to the target gamut;
frames without colorimetry metadata convert with BT.709 defaults
(warned once) instead of passing through; scopes read the
output-colorspace signal on both F32 paths.

Also: only emit rerun-if-changed for .env when it exists (a missing file
made every build fully dirty).
2026-08-29 00:24:15 +08:00
Mike-Solar 879ff8da5b app: PS-style colour picker and an eyedropper that keeps the popup open
CI / Build & test (Linux) (push) Failing after 9s
CI / Build & test (Windows) (push) Failing after 22s
- the popup now offers a large SV palette with a hue bar plus RGB/HSV
  mode tabs (shared slider model swap, hsv<->rgb pure functions)
- while the eyedropper is armed, clicks outside the popup pick a colour
  instead of dismissing it, so the picked draft can still be confirmed
- regression test pinning the OFX plugin colour set/read/undo chain
- i18n: ofx.color.mode_rgb/mode_hsv in all eight packs
2026-08-27 20:43:27 +08:00
Mike-Solar c730061018 app: occlude the app-owned ComboBox dropdown
It is a plain deferred div rather than an anchored element, so it gets
no automatic BlockMouse hitbox; clicks on its padding fell through to
the controls below.
2026-08-27 20:43:09 +08:00
Mike-Solar 9c0269bcbb render: cache worker frames and decouple audio from the video workers
- oak-worker gains an LRU frame cache (default 64 MiB) keyed by the
  render-deterministic spec subset; repeat frames (paused frames,
  scrubs over rendered ranges, re-renders after an effect change) are
  memcpy-cheap, which is what made adding an OFX plugin -- and the
  in-flight batches after removing one -- stall the UI
- audio dispatch on the Processes backend now mixes inline on the UI
  tick instead of queueing behind video batches in the worker pool, so
  video stalls no longer starve the ~100ms cpal output buffer into
  silence
2026-08-27 20:35:50 +08:00
Mike-Solar c78be39f8b gpui: anchored popups block clicks from reaching the layers below
Submodule bump: menus, dropdowns and popups no longer let clicks fall
through to the UI they cover.
2026-08-27 20:35:50 +08:00
Mike-Solar 246de6352f app: true 10-bit presentation -- F32 frames reach the swapchain unquantized
CI / Build & test (Linux) (push) Failing after 17m38s
CI / Build & test (Windows) (push) Failing after 17m40s
The display path no longer passes through BGRA8: render workers are
asked for F32, rows are repacked, colour management runs on the F32
data, and the frame uploads to an Rgba16Float wgpu texture that the
viewer presents directly on the Rgb10a2Unorm swapchain. The BGRA8
image remains only for scopes, the eyedropper, thumbnails and the
no-GPU fallback.
2026-08-27 19:55:05 +08:00
Mike-Solar 5d21f83e1e render/app: display bit depth option (10-bit default, 8-bit optional)
Preferences gains a display-bit-depth combo stored in the config store;
at startup the app forwards it to gpui_wgpu via OAK_DISPLAY_BIT_DEPTH,
which prefers Rgb10a2Unorm for 10-bit presentation. Takes effect after
restart (noted in the dialog); i18n in all eight packs.
2026-08-27 19:08:55 +08:00
Mike-Solar 7422ccd7e9 app: rename the razor tool to Blade with an I-beam cursor
Display name only (identifiers unchanged), in all eight language packs.
2026-08-27 19:08:41 +08:00
Mike-Solar 82ad9891ad app: white fill with black border for unchecked checkboxes
Keeps the white well but swaps the water-blue theme border for black,
per user feedback.
2026-08-27 19:08:19 +08:00
Mike-Solar eb5643249c app: scope multicam digit shortcuts to the multicam panel
The 18 multicam switch actions were bound globally to keys 1-9, so
gpui dispatched every digit keystroke as an action and text fields
never received it -- every numeric input in the app (slider value
boxes, spin boxes, the colour picker's hex field) could only type 0,
and the colour swatch never updated because hex parsing failed.
The bindings now carry the MulticamPanel key context: digits switch
angles while the panel is focused and type normally everywhere else.
2026-08-27 19:08:19 +08:00
Mike-Solar 719e43eba3 app: render at proxy resolution and honour the playback divider
- paused full-res renders now scale by the playback divider instead of
  always rendering at native sequence size
- when a clip's proxy is active, sequence/footage renders clamp to the
  proxy resolution (never upscale a 720p proxy to 1080p and back),
  which was making proxied playback slower than no proxy
2026-08-27 18:54:03 +08:00
Mike-Solar 54945e39af app: pick colors from the program viewer frame (PS-style eyedropper)
Replaces the project-thumbnail picker: the color popup's eyedropper
button arms the program viewer, the next click on the picture samples
that pixel (contain-fit mapped, BGRA from the current CPU frame) into
the picker's draft; Esc or the button cancels. Coordinated through a
global eyedropper mailbox on the engine.
2026-08-27 18:32:34 +08:00
Mike-Solar 1ae03c1b1e app: real timeline tools, live zoom buttons, clickable snap toggle
CI / Build & test (Linux) (push) Successful in 20m7s
CI / Build & test (Windows) (push) Successful in 41m55s
- the eight toolbar tools now drive real editing: razor splits at the
  click point, ripple/slip/roll/slide emit new undoable composite
  commands (ripple_trim_clip, roll_edit, slide_clip, slip_clip),
  zoom clicks zoom anchored at the cursor, track-select selects the
  track right of the click; panel tool state and the Tools menu stay
  in sync both ways
- the zoom-in/out toolbar icons now actually zoom (anchored at the
  playhead) and the snap magnet icon toggles snapping like its checkbox
2026-08-27 17:59:27 +08:00
Mike-Solar 4e619efb93 app: make the viewer context menu real
CI / Build & test (Windows) (push) Canceled after 12m22s
CI / Build & test (Linux) (push) Canceled after 14m4s
Every item in the program/source viewer menu now works and reflects
real state: zoom levels, full-screen toggle, safe margins (off/on/
custom), stop-on-last (honoured by both playback clocks), waveform
mode, show-fps overlay, and save-frame (writes a PNG of the current
frame). The viewer widget's in/out/clear buttons are wired to the
shared program workarea.
2026-08-27 17:39:03 +08:00
Mike-Solar bad2f9552e app: sequence management -- new-sequence dialog, folders, sequences in the project bin
- File > New > Sequence opens a real dialog (name, PAL/NTSC/HD presets,
  width/height/frame rate, progressive/interlaced); VideoParams gains an
  interlaced flag
- File > New > Folder creates a folder in the project root
- sequences are mounted under the root folder so they appear in the
  project explorer (including the auto-created Sequence 1 and orphans
  from older projects); right-click > Sequence Properties edits the
  parameters afterwards
- dropping footage onto an empty, sequence-less timeline auto-creates a
  sequence from the footage's first video stream
2026-08-27 17:38:30 +08:00
Mike-Solar d65b84a6ca app: live color swatch and a project-media eyedropper in the color picker
- the swatch now follows the draft color while the popup is open
  (cancel still reverts) and set_effect_param failures are logged
  instead of silently reverting the swatch
- the popup lists project footage thumbnails; clicking a pixel picks
  its color into the draft (contain-fit coordinate mapping, no new
  dependencies)
- i18n: ofx.color.pick_project in all eight packs
2026-08-27 17:34:28 +08:00
Mike-Solar 107ee079c6 inspector: vertical scrollbar for long effect stacks
Submodule: effect stack view scrolls when cards overflow the panel.
2026-08-27 17:33:49 +08:00
Mike-Solar 2dfdf8a5b0 app: click-to-jump and a persistent value box on sliders
- clicking anywhere on the track moves the thumb to that position
  (double-click still opens the inline editor)
- a compact numeric field next to every slider shows the value and
  accepts typed input, clamped to the slider's min/max range
2026-08-27 17:32:48 +08:00
Mike-Solar a372d72446 app: white background with inner shadow for unchecked checkboxes
The unchecked state used the panel background color, making the box
nearly invisible on the dark theme.
2026-08-27 17:32:37 +08:00
Mike-Solar 9f88b9b856 codec: rescale packet timestamps to the stream time base the muxer chose
mov/mp4 muxers rewrite the stream time base at write_header (mov timescale
>= 10000) and FFmpeg 9 no longer rescales packets for us, so mpeg2video
clips were muxed with pts in encoder ticks -- a 10-frame/10fps clip became
1ms long and every seek past t=0 decoded to the EOF frame.

- read back the real stream time base after write_header and rescale
  packets (including the flush path) before write_interleaved
- warn once when retrieve_frame's EOF fallback returns a frame far from
  the requested timestamp
- testmedia round-trip now asserts container duration and per-frame
  decode instead of only t=0
2026-08-27 17:32:00 +08:00
Mike-Solar c03f1ec604 render: stack higher-numbered video tracks on top
Match the timeline UI (V_max drawn topmost): composite tracks from
V1 up to V_max so the highest-numbered track is composited last, in
both the montage path and direct graph evaluation.
2026-08-27 15:20:18 +08:00
Mike-Solar 663d879908 node/app: keep clip links symmetric and intact across paste, delete, and undo
- take_node clears the partner's dangling link references
- are_linked checks both directions; link() repairs asymmetric pairs
- paste writes links through the graph API instead of BlockCore.links
- drop link undo removes only its own pair instead of restoring a snapshot
- move_clip_with_links skips off-track partners instead of failing
2026-08-27 15:20:18 +08:00
Mike-Solar f7352ae19d render: fall back to CPU when the adapter cannot render Rgba32Float
CI / Build & test (Linux) (push) Successful in 19m8s
CI / Build & test (Windows) (push) Successful in 32m35s
2026-08-27 13:52:03 +08:00
Mike-Solar cbf9d3c543 app: silently ignore clip drags across track kinds (video<->audio) 2026-08-27 10:13:11 +08:00
Mike-Solar 8c306986ab app: stamp clips with a stable creation-time color and footage-name label 2026-08-27 10:13:11 +08:00
Mike-Solar c51a349070 render: translate node GLSL shaders to WGSL and run them as wgpu passes
CI / Build & test (Linux) (push) Failing after 16m57s
CI / Build & test (Windows) (push) Successful in 31m46s
2026-08-27 07:07:09 +08:00
Mike-Solar 4f6c44a0d0 proxy: hardware-accelerated transcode with configurable, resource-aware concurrency
CI / Build & test (Linux) (push) Successful in 19m14s
CI / Build & test (Windows) (push) Successful in 32m5s
Proxy generation no longer pegs the machine:

- the transcode probes for a hardware H.264 encoder once per process
  and uses it when present (macOS h264_videotoolbox; Windows/Linux
  h264_nvenc -> h264_qsv -> h264_amf; libx264 remains the universal
  fallback and the untouched C++ parity path), with decode-side
  -hwaccel auto (HEVC 4:2:2 10-bit decodes in hardware on Apple
  Silicon / RTX 50+ / Intel GPUs and falls back to software cleanly);
  quality/preset map from the proxy CRF/preset per encoder
- the concurrency limit is configurable (ProxyMaxConcurrent, default
  1) in the Proxy Settings dialog; auto-generated jobs queue and the
  next starts when a slot frees. The invariant
  concurrency x per-job threads <= logical cores / 2 holds by
  construction (thread budget = half the cores / concurrency, clamped
  to [1,8], covered by a unit test), and on Unix ffmpeg runs nice -n 10
  so the background task never starves the foreground
- deleting a footage's proxy also removes it from the auto-generation
  queue
2026-08-26 06:40:06 +08:00
Mike-Solar ca64b61156 app: drop the throughput assumption from the playhead tracking test
CI / Build & test (Linux) (push) Successful in 19m21s
CI / Build & test (Windows) (push) Successful in 31m53s
playback_display_tracks_the_playhead asserted the displayed frame lag
the playhead by less than 4 frames at playhead 120 — a render-
throughput assumption that fails under machine load (parallel builds)
rather than for a broken pipeline, which made the test flaky. It now
asserts the machine-speed-independent property: the display advances
while the playhead advances (peak displayed grows between the two
checkpoints), which still catches a permanently frozen picture.
2026-08-26 04:23:11 +08:00
Mike-Solar 96df9293e5 app: extract audio waveforms off the UI thread
rebuild_timeline synchronously extracted every audio clip's waveform,
and the extractor decodes the clip's ENTIRE audio inline — with two
40-minute files on the timeline, opening the project froze the UI for
seconds ("the project takes a while to open"). Extraction now runs on
a background thread; the cache bumps a version counter on insert and
the engine tick repaints the timeline when it changes.
2026-08-26 04:23:11 +08:00
Mike-Solar 09c7e7de32 app: auto-generate proxies in the background and badge them in the bin
With the global proxy switch on, footage that needs a proxy now gets
one generated in the background automatically — on import, on project
open, and when proxy use is enabled for a footage — instead of only
through the manual Generate menu action. Footage explicitly opted out
(a recorded proxy path with use disabled) and footage whose last
generation failed are not restarted; the generation path enables proxy
use so preview switches to the proxy as soon as it is ready.

The bin shows the lifecycle as a corner badge on each footage entry
(green P = ready, amber P = generating, red ! = failed; the widget
half lives in the gpui fork, submodule bumped here).

Includes a real-media test driving ProxyTask end-to-end on 4K H.265
4:2:2 10-bit (the media class the 4K playback lag was reported on):
180 s transcodes to a 720p proxy in ~25 s.
2026-08-26 04:22:44 +08:00
Mike-Solar 287a17399b plugin: render non-F32 OFX plugins by converting in and out
CI / Build & test (Linux) (push) Successful in 18m49s
CI / Build & test (Windows) (push) Successful in 32m47s
The pipeline is ACEScg + F32 end to end by design; a plugin that does
not support F32 must not fail the render — its inputs convert down to
the negotiated depth and its output converts back to F32 (the previous
"Phase 2 F32 only" error path purple-framed those plugins).

- render_driver maps getClipPreferences' output bit depth to
  Byte/Short/Half/Float, sets it on the input/output clips, allocates
  the output image at the negotiated depth and converts the result
  back to F32 for frame assembly (both the CPU path and the
  GL-failure CPU fallback)
- the GL path keeps F32 clip params: GL textures are created in the
  pipeline format and kOfxOpenGLPropPixelDepth is negotiated
  separately, so clip props must match what the plugin actually sees
- fetch_image converts the decoded input down to the clip's
  negotiated depth (default F32)
- new Image::convert_depth ([0,1]-normalized conversion between all
  depth pairs) plus round-trip and f16 edge tests; f16 helpers moved
  into image.rs and shared with clip.rs
2026-08-26 01:31:35 +08:00
Mike-Solar fa3951344b render: fix 4K playback memory growth and decode-to-target-size
Root causes found for the 4K stalls and the second-footage memory
blowup (audit + code review):

- ticket bookkeeping leaked unbounded: the procpool ticket table and
  the arena slot map only ever grew (50-100 tickets/sec during
  playback, each pinning montage params and shm region views).
  Completed/cancelled/superseded/crashed entries are now removed, and
  the arena reaps fire-and-forget tickets once finished; the sync poll
  path reaps via a terminal result() read. InFlight duplicate submits
  now answer State immediately instead of sitting in the map forever.
- decode ran a full-resolution swscale to F32 RGBA (~132 MB at 4K)
  plus a second full-res copy before downscaling to the 480px proxy:
  RetrieveVideoParams.target_size lets swscale convert AND resize in
  one pass (bilinear, matching the old Rust resampler), so a 4K
  preview frame costs ~1 MB instead of ~260 MB of churn. This applies
  to proxy AND full-res requests alike.
- per-process decoder cache was unbounded (each session pins an FFmpeg
  context + 2 native decoded frames): LRU-capped at 16, eviction drops
  the map entry (in-flight renders keep their Arc; Drop releases
  FFmpeg).
- playback window completions were not generation-gated: a stale
  render from before an edit landed in the rebuilt window (wrong frame
  displayed, fresh request blocked). Stale completions now return
  their shm slot credit instead.
- async audio prefetch used the polling ticket submit without ever
  polling: switched to the fire-and-forget submit so entries reap.
2026-08-26 01:31:10 +08:00
Mike-Solar ea9b451d0b plugin: instantiate more real-world plugins (FieldOrder, UIColour, context fallback)
CI / Build & test (Linux) (push) Successful in 18m55s
CI / Build & test (Windows) (push) Successful in 33m21s
Three independent host-side gaps kept real plugins from instantiating:

- clips now define OfxImageClipPropFieldOrder (default OfxFieldNone):
  ofxs Clip::getFieldOrder() is a strong read with no default, so
  field-aware plugins (Mirror) threw PropertyUnknownToHost ->
  MissingHostFeature from createInstance
- OfxParamPropParametricUIColour is no longer predefined as an empty
  array: the OFX implicit-create semantics let the plugin's first
  propSetDouble create the property and grow it index by index, while
  the empty predefined array rejected every write at index >= 1 with
  BadIndex (ColorLookup, HueCorrect describeInContext)
- Host::create_instance_preferred: context selection with fallback
  (filter -> general -> tracker -> paint -> rest). TrackerPM advertises
  the filter context but its createInstance fetches a Mask clip that
  its describe only defines for tracker/general/paint; Natron simply
  instantiates it in the tracker context, and now so do we. Both the
  node-registration scan and the shared instance factory use it.

Still unsupported, by design: Premult/Unpremult (this openfx-misc
build hard-requires the Nuke multi-plane suite + dynamic choices) and
the stereo view plugins (Switch/anaglyph/joinViews/etc. need Natron's
isNatron multi-clip folding or view rendering).

Verified: ColorLookup, HueCorrect, Mirror, TrackerPM now instantiate;
full oak-plugin suite green.
2026-08-25 21:56:33 +08:00
Mike-Solar 3792c49854 plugin: abort() must return 0/non-zero, not ReplyYes/No — fixes black frames
CI / Build & test (Linux) (push) Successful in 18m37s
CI / Build & test (Windows) (push) Successful in 44m45s
The OFX abort contract is "0 = keep rendering, anything else = stop"
and the ofxs support library literally checks abort(...) != 0. Our
host answered kOfxStatReplyNo (13) for "not cancelled", so every
openfx-misc processor aborted its pixel loop before writing a single
row: the render action returned OK with the output buffer untouched —
a solid black frame everywhere an effect was applied (the previous
smoke test only asserted "not the purple failure frame", which let
this slip through as a fake pass).

- image-effect abort: return 0 when not cancelled, 1 when cancelled
  (instance cancel flag or progress cancellation), BadHandle for
  descriptor handles as before
- strengthen the real-plugin smoke tests to also reject an all-zero
  output frame, so a silent no-write can no longer pass
- more trace-gated [ofx] diagnostics (value dumps for propGetN /
  propGetPointer / clipGetImage fetches, multiThread entry, render
  output buffer address) — the tooling that pinned this down

Verified with lldb watchpoints: the AddOFX/ChromaKeyerOFX processors
now write the real pixels through the executor path.
2026-08-25 20:30:37 +08:00
Mike-Solar 3bf914a2b8 app: stop slider drags from leaking into other sliders
CI / Build & test (Linux) (push) Successful in 18m42s
CI / Build & test (Windows) (push) Successful in 44m58s
gpui delivers DragMoveEvent to every drop target with a matching
payload type under the cursor, and every slider shared the SliderDrag
payload type — dragging one slider (e.g. an OFX panel parameter) also
moved every other slider the cursor passed over (timeline zoom, track
height). Tag the payload with the starting control and ignore drag
moves that are not ours.

Also finish the gesture on mouse-up (inside or outside the track)
instead of on_drop: gpui only delivers drop events to the hovered
target, so releasing outside the track used to leave the drag
unfinished and the final value/undo edit unemitted.
2026-08-25 19:10:25 +08:00
Mike-Solar a56f6ddf87 app: link the rear halves when splitting an A/V pair at the playhead
The Cmd+K split ran the plain per-block split for every selected clip,
so an originally linked audio/video pair came out with its front halves
linked but the two rear halves unlinked — dragging one rear clip left
its mate behind and Link/Relink could not repair it.

Split the whole target set as ONE BlockSplitPreservingLinksCommand (the
same command the multicam path uses): any pair of originally linked
blocks split at the same time gets its new halves linked too. Adds a
gpui test asserting front and rear halves of a dropped A/V clip stay
linked after split_at_playhead.
2026-08-25 19:10:08 +08:00
Mike-Solar 2828984187 plugin: fix OFX host property-suite gaps that purple-framed real plugins
Real plugins (CImg ChromaKeyerOFX, AddOFX) failed the render action with
kOfxStatFailed / MissingHostFeature and painted the magenta failure frame:

- images lacked the mandatory ImageBase properties (OfxPropType,
  PixelAspectRatio, PreMultiplication, Field, RenderScale); the ofxs
  ImageBase constructor throws on the missing/invalid strong reads
- PreMultiplication used the made-up string "OfxImagePreMultiplied";
  kOfxImagePreMultiplied is actually "OfxImageAlphaPremultiplied", the
  only value mapStrToPreMultiplicationEnum accepts (lldb __cxa_throw
  backtrace pinpointed this)
- RenderWindow is Int x4 per ofxsPropertyValidation, not Double x4
- field strings use the real constant "OfxFieldNone"
- clips define OfxImageClipPropConnected (isConnected is a strong read;
  optional mask clips blew up without it)
- choice params predefine empty ChoiceEnum / ChoiceLabelOption arrays
- isIdentity failure is no longer fatal (the C++ plugin renderer never
  calls it; plugins that error on it simply render normally)
- property suite coerces Int <-> Double on reads (the CImg framework
  reads the render window with propGetIntN against a Double store)
- in-args carry NatronOfxPropNativeOverlays=0 for the Natron framework
- plugin jobs pass a GL-kind marker so GL-only plugins take the real
  gl_bridge offscreen path instead of the CPU MissingHostFeature path
- trace-gated [ofx] diagnostics for property misses and suite calls

Verified with new smoke tests that render the real AddOFX and
ChromaKeyerOFX plugins through the executor and assert the output is
not the purple failure frame.
2026-08-25 19:04:18 +08:00
Mike-Solar f2aab8ce15 render: apply clip effect stacks in the montage path
CI / Build & test (Windows) (push) Failing after 16m26s
CI / Build & test (Linux) (push) Successful in 19m12s
Adding an effect to a clip did nothing: the sequence render is
flattened into a montage (decode + composite), and MontageClip carried
no effect data at all.

- MontageClip gains an ordered effect stack (type id / enabled /
  effect input / parameter values); protocol v2 carries it as an
  additive wire field (older peers default to an empty stack).
- renderops::video_montage fills the stack from the effect chain
  (the footage source node — the chain end without an effect input —
  is dropped; the montage decodes the footage itself). Export
  (oak-task) and the multicam single-track montage fill it too.
- The worker applies the stack between decode and composite: built-in
  Opacity gets a CPU evaluator (C++ opacity.frag parity — whole vec4,
  alpha included, unity pass-through); everything else dispatches as an
  OFX plugin job through a new instance-factory slot (oak-plugin
  lazily creates + caches one instance per identifier per render
  process) with the montage's parameters injected. Disabled effects
  bypass (the C++ traverser pushes the effect input through). Unknown
  types warn once per type id and pass through — no silent no-ops.

Not covered (explicitly): Transform/Crop and the other ~30 built-in
effects have no CPU evaluator in oak-render (they pass through with a
warning), keyframed parameter animation, audio effect chains, and the
CLI's simplified montage.

Acceptance: a real 50% Opacity on real media quarters the rendered
pixels both in-process (renderops test) and through a real worker
process over IPC + shared memory (procpool_integration test);
disabling restores the plain render byte-for-byte.
2026-08-25 04:49:00 +08:00
Mike-Solar 28f4ed655e app: refine the link toggle + human-readable node ports
CI / Build & test (Linux) (push) Successful in 18m51s
CI / Build & test (Windows) (push) Failing after 17m33s
Link toggle rule refinement (on top of 1ad2d71c8): the toggle now
unlinks only when the selection is FULLY linked internally — the C++
crude "any member has ANY link" check made split halves (which inherit
the original clip's A/V links via BlockSplitPreservingLinksCommand)
impossible to link to each other. Test: link_unlink_toggles_the_
selection_links covers toggle/undo/split-link.

Node editor: only stream inputs (texture/samples) become ports — OFX
plugins declare every parameter as an input, which buried the card
under internal params (the garbled NatronOfxParamProcess* wall). Secret
inputs stay hidden; port labels use the input's display name (the OFX
param label); edges resolve ports by input id, not by label.
2026-08-25 03:53:00 +08:00
Mike-Solar 1ad2d71c81 app: implement Link/Unlink for timeline clips
CI / Build & test (Linux) (push) Canceled after 8m56s
CI / Build & test (Windows) (push) Canceled after 7m16s
编辑 > 链接/重新链接 (Cmd+L) was an unhandled stub
(PanelCommandHandler::toggle_links returned false), so linking clips
never created graph links and linked drags never happened.

- graphops::set_clips_linked: one undoable entry toggling every pair of
  the selection; the undo restores the exact prior internal topology.
- RealEngine::toggle_clip_links: fully-linked selections unlink,
  otherwise the selection links together (the C++ crude "any member has
  ANY link" rule would have made split halves — which inherit the
  original clip's A/V links — impossible to link to each other).
- TimelinePanel overrides toggle_links, routing the timeline selection.
- MockEngine stores demo link pairs.
- Test: link_unlink_toggles_the_selection_links — dropped A/V pair
  toggles off/on with undo, and split halves link manually.
2026-08-25 03:40:57 +08:00
Mike-Solar a1bfe5ee5e ci: msys2 clang-libs; drop the msvcrt link workaround
CI / Build & test (Windows) (push) Canceled after 0s
CI / Build & test (Linux) (push) Canceled after 1m15s
The job hook no longer injects the MSVC INCLUDE/LIB into the msys2
steps, so the unset/-lmsvcrt workaround is unnecessary.
2026-08-25 03:03:28 +08:00
Mike-Solar 3cc6f75ccc docs: archive completed plans, add the external plugin system design
- docs/zh/plans: finished plans move to completed/ (the RIIR series, the
  event-bridge and dependency plans, the v04 manual test plan).
- New design docs: the external (functional) plugin system
  (process-isolated, JSON-RPC/shm) and its protocol.
- ai-agent-design refreshed; README pointers follow the moves.
2026-08-25 03:03:28 +08:00
Mike-Solar 8679bef6de components: focus-gated spin wheel, spin text entry, combo outside-dismiss
CI / Build & test (Linux) (push) Canceled after 7m56s
CI / Build & test (Windows) (push) Canceled after 6m26s
Touchpad fixes from user testing:

- SpinBox: the wheel only steps a FOCUSED field — hover-wheel is inert,
  so two-finger scrolling across a dialog no longer drifts values.
- SpinBox: direct numeric entry — double-click opens the editor (the
  app's text input; commit on blur, Escape cancels), typing a digit on
  the focused field starts editing with that character. Previously the
  field accepted no text at all.
- ComboBox: a click anywhere outside closes the open popup
  (on_mouse_down_out).
- gpui bump: the timeline header-occlusion fix.
2026-08-25 02:51:44 +08:00
Mike-Solar 186c41b894 app: about dialog, modal close affordances, more preferences
CI / Build & test (Windows) (push) Failing after 16m35s
CI / Build & test (Linux) (push) Successful in 18m59s
- Help > About Oak… opens a real dialog (was a placeholder print): name
  + version, the GPL-3.0 line, the Olive fork notice and the thanks to
  Enzo GD (Community Promoter).
- Modal (gpui_widgets, submodule bump): a title-row close button on
  every dialog and opt-in backdrop dismissal (dismiss_on_mask) — Help >
  Search Actions and About use it, so they close on an outside click
  (previously a buttonless modal could only be closed with Escape).
- Preferences general tab grows two live settings: the cache-ahead
  pre-render window (PlaybackPreRenderFrames, consumed by the preview
  scheduler every playback tick) and the library storage backend
  (Storage/Backend sqlite/pg + Storage/PgUrl connection string, read by
  oak-storage's write-through when a project binds; the pg URL field
  shows only for PostgreSQL and commits on dialog close). The C++
  auto-recovery toggle is skipped on purpose: the Rust snapshot loop
  has no disable semantic, so the switch would be dead.

i18n keys for all eight packs; new tests cover the about modal and the
preferences writes.
2026-08-25 01:55:08 +08:00
Mike-Solar ccfd8fec60 components: paint the ComboBox popup deferred (above following rows)
The popup was a plain absolute child: dialog rows after the combo drew
OVER it, making the list look transparent (its text overlapped the
content behind). deferred() paints it after all ancestors.
2026-08-25 01:55:08 +08:00
Mike-Solar 72416e621e style: rustfmt app.rs/dialogs.rs (no behavior change)
The two files carried hand-wrapped lines that predate the workspace
tab policy; normalize them so the feature commits on top stay
reviewable.
2026-08-25 01:32:56 +08:00
Mike-Solar dfbb0eaec5 app: move Preferences into the Tools menu (the C++ layout)
CI / Build & test (Windows) (push) Failing after 16m36s
CI / Build & test (Linux) (push) Successful in 19m14s
2026-08-25 00:46:10 +08:00
Mike-Solar 476114cec0 app: project properties dialog (File > Project Properties…)
The menu item was a placeholder print; it now opens a real dialog (the
C++ ProjectPropertiesDialog):

- Per-project OCIO config override with a 浏览… picker: validated on OK
  (an invalid config keeps the dialog open with the error shown, like
  the C++ accept()), persisted in the project settings, applied to the
  display color pipeline on accept and on project open, and reverted to
  the app default when the project closes. oak-render gains
  set_up_default_config_from for the explicit-path load.
- Disk-cache location (default / alongside the project / custom path):
  persisted through the OVE serializer (cachesetting/customcachepath
  round-trip the settings map, clamped on load) and honored by the
  thumbnail writer — the first live consumer of Project::cache_path.
- PathField gains an enabled state (the custom path field follows the
  combo selection).

The C++ color tab's Default Input Color Space and Reference Space
combos are intentionally absent: the Rust render pipeline has no
consumer for them today (decode performs no input transfer conversion),
so showing them would be dead settings.

Tests: dialog opens, OK applies the cache location, an invalid OCIO
config keeps the dialog open with the error row. i18n keys for all
eight packs.
2026-08-25 00:45:04 +08:00
Mike-Solar f7b5996032 render: fix the BGRA8 display transform + dispatcher diagnostics
CI / Build & test (Windows) (push) Failing after 17m13s
CI / Build & test (Linux) (push) Successful in 19m6s
convert_bgra8 applied packed u8 pixels to the default (F32-finalized)
OCIO CPU processor, which rejects them with a bit-depth mismatch; the
callers swallow the error, so the display-ICC transform was silently
inert on the shm preview path. ocio-rs exposes no Uint8-finalized CPU
processor, so the conversion now detours through F32. Verified against
the machine's actual display profile with the new
display_icc_bgra8_never_outputs_black test (OAK_DISPLAY_ICC).

Also:
- procpool_integration: audio tickets are Seek priority and claimable
  by any worker now, so the shard-spread assertion goes (rendering on a
  live worker is what matters).
- OAK_DEBUG_VIEWER=1: the program viewer logs frame pushes and dumps
  the displayed frame to /tmp/oak_viewer_frame.ppm (the black-screen
  investigation tooling).
2026-08-24 23:26:09 +08:00
Mike-Solar 53dbaba8fe app: fix the Cmd+= (track height) self-deadlock
RealEngine::set_track_height held the project graph lock while calling
graphops::set_track_height, which locks the same mutex — an instant
same-thread deadlock with any sequence open (sampled live: main thread
in nudge_track_height -> set_track_height -> Mutex::lock, no other
lock-holder thread). Collect the track ids under the lock, drop it,
then apply the heights.

Also:
- Regression test set_track_height_does_not_self_deadlock.
- save_load_roundtrips_a_timeline_clip: real footage through the OVE
  serializer (also the fixture generator for viewer debugging).
2026-08-24 23:25:58 +08:00
Mike-Solar cac73dc89a ci: msys2 clang for the OCIO build; point the README badge at Gitea
CI / Build & test (Linux) (push) Failing after 17m12s
CI / Build & test (Windows) (push) Failing after 16m44s
2026-08-24 02:11:08 +08:00
Mike-Solar 30ef02803d render: fix the interactive-seek deadlock and seek starvation
Three compounding bugs froze the UI when dragging the playhead after
playback:

1. Self-deadlock on preview_windows: supply_preview_window /
   cancel_preview_windows / cancel_preview_window called
   cancel_preview_sequence / cancel_preview_frame while HOLDING the
   preview_windows mutex; those calls fire completions synchronously and
   the completion locks preview_windows again. Caught by sampling the
   hung process: UI thread in cancel_preview_sequence -> TicketSlot::
   finish -> completion -> Mutex::lock. Cancels/releases are now
   collected under the lock and fired after it is dropped.

2. Seek starvation by shard pinning: a Seek request's scheduler frame
   is its ticket id, pinning it to worker (id mod W). The playback
   window fills every worker's slots (window slots are only released by
   UI-thread consumption), so the seek's pinned worker could have zero
   free slots while the UI thread blocked on the seek — permanent
   starvation. Seeks (interactive frame / real-time audio) are now
   claimable by ANY worker; the no-stealing shard rule stays for
   Playback frames (adjacent frames finish together).

3. No per-worker reserve: the global preview_window_capacity reserve is
   pool-wide accounting, but exhaustion happens per worker. Playback /
   Background claims now leave one credit unused per worker; Seek
   claims may use the last slot (they complete on the worker without
   UI involvement).

Also: RealEngine::drop cancels the preview windows — ShmFrameRef has no
self-release, so every dropped engine leaked its window's slots from
the shared pool, starving later windows (surfaced as the full-suite
playback_window_supplies_playhead_frames failure once the new probe
test shifted the test schedule). new_sequence_has_default_two_video_
two_audio_tracks now takes the engine test lock (it asserts on the
global undo stack; running lock-free raced parallel undo histories).

New regression probe interactive_seek_renders_without_hanging: play 30
ticks (window fills and holds shm slots), pause, seek, synchronously
render — must not hang. Scheduler tests updated for the reserve and
seek-any-worker contract. OAK_DEBUG_DISPATCH=1 enables the dispatcher
starvation/pool diagnostics used to track this down.
2026-08-24 02:11:00 +08:00
Mike-Solar c9b9a45eef cd: CD uses GitHub Actions
CI / Build & test (Windows) (push) Failing after 17m0s
CI / Build & test (Linux) (push) Successful in 19m40s
2026-08-23 19:08:18 +08:00
Mike-Solar 532d0cf0ef ci: fix Windows CI can't find git
CI / Build & test (Linux) (push) Canceled after 0s
CI / Build & test (Windows) (push) Canceled after 0s
2026-08-23 19:06:05 +08:00
Mike-Solar 8a8ed50451 oak-plugin: serialize the identity-registry tests
CI / Build & test (Linux) (push) Canceled after 0s
CI / Build & test (Windows) (push) Canceled after 0s
Every test_project() node packs the same NodeId::identity (a fresh
graph's first node), so two tests running in parallel collide in the
process-wide registry and one resolves the other's live entry —
identity_project_dropped's dangling-upgrade assert then fails
intermittently on CI. Both identity tests now share a mutex.
2026-08-23 19:05:19 +08:00
Mike-Solar 16e90bcfbf ci: fix OFX fixture env typo (ofx-fixtre -> ofx-fixture)
CI / Build & test (Windows) (push) Failing after 3m14s
CI / Build & test (Linux) (push) Failing after 17m36s
The probe step pointed OAK_OFX_FIXTURE_DIR at .cache/ofx-fixtre, so the
ofx_roundtrip test found no fixture plugin and failed with "the fixture
plugin registered (got [])".
2026-08-23 18:39:25 +08:00
Mike-Solar b6f1f7fd41 ci: detach the Linux test watchdog from the step's I/O pipes
CI / Build & test (Windows) (push) Failing after 2m59s
CI / Build & test (Linux) (push) Failing after 19m18s
The background watchdog subshell inherited the step's stdout/stderr; when
cargo test finished, its lingering sleep child kept the pipes open and
the runner aborted the step with "exec: WaitDelay expired before I/O
complete". Redirect the watchdog to /dev/null and reap it after the
test so the step's I/O closes cleanly.
2026-08-23 17:01:36 +08:00
Mike-Solar c32341c888 ci+fixtures: UCRT64 msys2 env; fix remaining fixture paths
CI / Build & test (Windows) (push) Failing after 2m56s
CI / Build & test (Linux) (push) Failing after 17m43s
ci (Windows): the runner's msys2 shell starts as the base MSYS
environment (MSYSTEM=MSYS), which install-deps.sh rejects. Set the
job-level MSYSTEM=UCRT64 env and prepend /ucrt64/bin to PATH in every
Windows step, so pacman installs and the toolchain resolve against the
mingw-w64-ucrt-x86_64 packages.

fixtures: the restructured workspace moved the media fixtures into
crates/oak-app/tests; the remaining references pointed at the old
repo-root tests/ — oak-codec realmedia_tests + hwdecode, oak-node
serializer golden, oak-worker procpool integration. All point at
../oak-app/tests now (oak-app's own tests/demo.mp4 references were
already correct after the move).
2026-08-22 22:28:24 +08:00
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
3756 changed files with 239600 additions and 1468335 deletions
+2
View File
@@ -0,0 +1,2 @@
[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
+328
View File
@@ -0,0 +1,328 @@
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).
# tooling/ocio-env.sh: vendored static OCIO (the [patch.crates-io]
# ocio-sys tracks shaloong/ocio-rs main, whose vendored sources build
# on GCC >= 16).
- name: Configure build environment
run: |
bash tooling/ocio-env.sh >> "$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"
echo "FFMPEG_DIR=.cache/ffmpeg" >> "$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
# ------------------------------------------------------------------
# `cargo check` (not build): the Test step links the test binaries
# anyway, and a full build would codegen every workspace crate twice
# (once without and once with cfg(test)).
- name: Build
run: cargo check --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
run_suite() {
xvfb-run -a -s "-screen 0 1920x1080x24" cargo test --workspace --locked &
TEST_PID=$!
# The watchdog inherits the step's stdout/stderr; detach it so
# it cannot keep the runner's I/O pipes open after the step
# ends ("WaitDelay expired before I/O complete" otherwise).
(
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
) >/dev/null 2>&1 &
WATCHDOG_PID=$!
wait $TEST_PID
rc=$?
kill $WATCHDOG_PID 2>/dev/null || true
# Reap the watchdog so no child holds the step's pipes.
wait $WATCHDOG_PID 2>/dev/null || true
return $rc
}
# Retry once (same policy as the Windows job): worker-pool
# startup under full-suite parallelism has flaked once
# (full_res_worker_outlives_a_dropped_project: the render
# manager's process dispatcher failed to start while every other
# test passed). A real regression fails both passes.
if ! run_suite; then
echo "first pass failed; retrying once for worker-pool flakes"
run_suite
fi
# 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-fixture" \
cargo test --locked -p oak-plugin --test ofx_roundtrip
windows:
name: Build & test (Windows)
runs-on: oak-windows-2025
env:
# The runner's msys2 shell starts as the base MSYS environment; the
# build needs the UCRT64 one (mingw-w64-ucrt-x86_64-* toolchain).
MSYSTEM: UCRT64
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"
export PATH="/ucrt64/bin:$PATH"
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-ffnvcodec-headers \
mingw-w64-ucrt-x86_64-pkgconf \
mingw-w64-ucrt-x86_64-clang mingw-w64-ucrt-x86_64-clang-libs git
# ------------------------------------------------------------------
# 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"
export PATH="/ucrt64/bin:$PATH"
# System OCIO (MSYS2, the exact 2.5.2 the bridge targets);
# tooling/ocio-env.sh links it STATICALLY when the package ships
# libOpenColorIO.a, dynamically otherwise.
bash tooling/ocio-env.sh >> "$GITHUB_ENV"
echo "FFMPEG_DIR=.cache/ffmpeg" >> "$GITHUB_ENV"
# ocio-sys is the GIT fork (Mike-Solar/ocio-rs main, pinned in
# the root manifest): its build.rs already carries the
# fork's GNU-toolchain fix, so NO crate-unpack/glob patch step
# applies here. The old code unpacked `ocio-sys-0.2.1.crate`
# from the registry cache — impossible for a git dependency
# (no .crate archive; git sources land under
# registry/src/git/<hash>), hence the
# "ls .../ocio-sys-0.2.1/build.rs: No such file" Windows CI
# failure.
echo "OCIO_RS_NO_MSVC_INCLUDES=1" >> "$GITHUB_ENV"
# ------------------------------------------------------------------
# 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"
export PATH="/ucrt64/bin:$PATH"
bash tooling/ffmpeg/build-ffmpeg.sh
echo "FFMPEG_DIR=$(cygpath -m "$PWD/.cache/ffmpeg")" >> "$GITHUB_ENV"
# ------------------------------------------------------------------
# Build & test
# ------------------------------------------------------------------
# `cargo check` (not build): the Test step links the test binaries
# anyway, and a full build would codegen every workspace crate twice
# (once without and once with cfg(test)).
- name: Build
run: |
cd "$GITHUB_WORKSPACE"
export PATH="/ucrt64/bin:$PATH"
# 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 check --workspace --locked
- name: Test
run: |
cd "$GITHUB_WORKSPACE"
export PATH="/ucrt64/bin:$PATH"
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
+401 -562
View File
File diff suppressed because it is too large Load Diff
-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*"
+13
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,11 @@ act
# Local OpenTimelineIO build (see docs/build.md)
otio-install/
# Rust
**/target/
tarpaulin-out/
.env
# CD packaging artifacts
/*.dmg
perf.data
+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
+9803
View File
File diff suppressed because it is too large Load Diff
+77
View File
@@ -0,0 +1,77 @@
# 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
# wgpu-core's debug-only "snatch lock" tracing captures a std Backtrace on
# EVERY device/queue lock acquisition (wgpu-core/src/snatch.rs), which cost
# ~35% of UI-thread CPU in profiling (perf: _Unwind_Find_FDE + Backtrace::create)
# and starved playback-audio chunk scheduling (late chunks get dropped → pops).
[profile.dev.package.wgpu-core]
debug-assertions = false
[profile.dev]
opt-level = 1
# ocio-sys 0.2.1's vendored sources fail to build on GCC >= 16 (yaml-cpp
# misses <cstdint>) and misdetect a system zlib-ng (minizip-ng then emits
# zng_* symbols the vendored zlib cannot satisfy). The fork tracks
# shaloong/ocio-rs main plus both build fixes; return to the crates.io
# release once it carries them.
[patch.crates-io]
ocio-sys = { git = "https://github.com/Mike-Solar/ocio-rs.git", branch = "main" }
+6 -1
View File
@@ -1,4 +1,4 @@
# Oak Video Editor[![CI](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml/badge.svg)](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml)
# Oak Video Editor ![CI](https://git.oakvideoeditor.org/oak-team/oak-editor/actions/workflows/ci.yml/badge.svg)
[中文](docs/zh/README.md)
Oak Video Editor is a free non-linear video editor for Windows, macOS, and Linux.
@@ -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 |
+2 -2
View File
@@ -45,7 +45,7 @@ Oak is split into small, independently testable components with a pure C ABI at
| `oak-render-worker` | process | headless render process that executes frames off the GUI thread (NDJSON IPC) |
| `oak-cli` | tool | command-line frontend for the engine: media info, probing, rendering, and transcoding without the GUI |
The C ABI boundary is what makes the engine embeddable and is the foundation for a planned module-by-module rewrite of the engine in Rust (see [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md)).
The C ABI boundary is what makes the engine embeddable and is the foundation for a planned module-by-module rewrite of the engine in Rust (see [`docs/zh/plans/completed/riir.md`](docs/zh/plans/completed/riir.md)).
<!-- DIAGRAM: component / ABI layout -->
![Architecture diagram](docs/images/architecture.png)
@@ -89,7 +89,7 @@ Contributions are welcome. Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) firs
- the **Google Test** requirement for all tests,
- the C ABI boundary contract for engine-facing code.
Useful project docs: [`docs/zh/`](docs/zh/) (中文文档), [`docs/zh/facade-migration-roadmap.md`](docs/zh/facade-migration-roadmap.md), [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md).
Useful project docs: [`docs/zh/`](docs/zh/) (中文文档), [`docs/zh/facade-migration-roadmap.md`](docs/zh/facade-migration-roadmap.md), [`docs/zh/plans/completed/riir.md`](docs/zh/plans/completed/riir.md).
## License
-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