From d6e46f67d7d77f411145df10f85848f2e3368f7e Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 23:22:44 +0100 Subject: [PATCH] Revert "Added an alternative GUI based on JUCE; I like FLTK more..." This reverts commit cd2f87a8b38a5f7e0d709b0c382a72d530f4ff0c. --- .gitignore | 1 - CMakeLists.txt | 2 - README.md | 5 +- cmake/Install.cmake | 1 - cmake/InstallDriver.cmake.in | 3 - cmake/JuceGui.cmake | 91 - docs/ARCHITECTURE.md | 16 +- docs/BUILD.md | 19 - docs/GUI.md | 6 +- docs/JUCE.md | 7 +- docs/LICENSES.md | 8 - docs/ROADMAP.md | 41 - examples/juce/CMakeLists.txt | 4 +- screenshots/ALL.md | 9 - screenshots/README.md | 17 - screenshots/juce-workstation.png | Bin 93069 -> 0 bytes src/gui/JUCE/README.md | 74 - src/gui/JUCE/command_palette.hpp | 100 - src/gui/JUCE/graph_widget.cpp | 645 ----- src/gui/JUCE/graph_widget.hpp | 212 -- src/gui/JUCE/insert_meter.hpp | 45 - src/gui/JUCE/main.cpp | 59 - src/gui/JUCE/main_window.cpp | 2856 -------------------- src/gui/JUCE/main_window.hpp | 557 ---- src/gui/JUCE/mixer_widget.cpp | 856 ------ src/gui/JUCE/mixer_widget.hpp | 202 -- src/gui/JUCE/piano_roll.cpp | 455 ---- src/gui/JUCE/piano_roll.hpp | 150 - src/gui/JUCE/plugin_dialog.cpp | 102 - src/gui/JUCE/plugin_dialog.hpp | 9 - src/gui/JUCE/plugin_library.hpp | 90 - src/gui/JUCE/preferences_dialog.hpp | 103 - src/gui/JUCE/sample_editor.cpp | 78 - src/gui/JUCE/sample_editor.hpp | 44 - src/gui/JUCE/sequencer_clip_widget.cpp | 78 - src/gui/JUCE/sequencer_clip_widget.hpp | 17 - src/gui/JUCE/sequencer_commands.cpp | 412 --- src/gui/JUCE/sequencer_group_scope.hpp | 12 - src/gui/JUCE/sequencer_instruments.cpp | 63 - src/gui/JUCE/sequencer_ruler_widget.cpp | 66 - src/gui/JUCE/sequencer_ruler_widget.hpp | 13 - src/gui/JUCE/sequencer_track_widget.cpp | 146 - src/gui/JUCE/sequencer_track_widget.hpp | 41 - src/gui/JUCE/sequencer_velocity_widget.cpp | 188 -- src/gui/JUCE/sequencer_velocity_widget.hpp | 26 - src/gui/JUCE/sequencer_waveforms.cpp | 59 - src/gui/JUCE/sequencer_widget.cpp | 382 --- src/gui/JUCE/sequencer_widget.hpp | 260 -- src/gui/JUCE/snap.hpp | 19 - src/gui/JUCE/theme.hpp | 65 - src/gui/JUCE/track_inserts.hpp | 206 -- src/gui/JUCE/ui.cpp | 1234 --------- src/gui/JUCE/ui.hpp | 647 ----- src/gui/JUCE/velocity_lane.hpp | 73 - tests/integration/README.md | 26 - tests/integration/test_juce_playback.cpp | 240 -- tools/install.py | 5 +- tools/install_layout.py | 1 - 58 files changed, 10 insertions(+), 11136 deletions(-) delete mode 100644 cmake/JuceGui.cmake delete mode 100644 screenshots/juce-workstation.png delete mode 100644 src/gui/JUCE/README.md delete mode 100644 src/gui/JUCE/command_palette.hpp delete mode 100644 src/gui/JUCE/graph_widget.cpp delete mode 100644 src/gui/JUCE/graph_widget.hpp delete mode 100644 src/gui/JUCE/insert_meter.hpp delete mode 100644 src/gui/JUCE/main.cpp delete mode 100644 src/gui/JUCE/main_window.cpp delete mode 100644 src/gui/JUCE/main_window.hpp delete mode 100644 src/gui/JUCE/mixer_widget.cpp delete mode 100644 src/gui/JUCE/mixer_widget.hpp delete mode 100644 src/gui/JUCE/piano_roll.cpp delete mode 100644 src/gui/JUCE/piano_roll.hpp delete mode 100644 src/gui/JUCE/plugin_dialog.cpp delete mode 100644 src/gui/JUCE/plugin_dialog.hpp delete mode 100644 src/gui/JUCE/plugin_library.hpp delete mode 100644 src/gui/JUCE/preferences_dialog.hpp delete mode 100644 src/gui/JUCE/sample_editor.cpp delete mode 100644 src/gui/JUCE/sample_editor.hpp delete mode 100644 src/gui/JUCE/sequencer_clip_widget.cpp delete mode 100644 src/gui/JUCE/sequencer_clip_widget.hpp delete mode 100644 src/gui/JUCE/sequencer_commands.cpp delete mode 100644 src/gui/JUCE/sequencer_group_scope.hpp delete mode 100644 src/gui/JUCE/sequencer_instruments.cpp delete mode 100644 src/gui/JUCE/sequencer_ruler_widget.cpp delete mode 100644 src/gui/JUCE/sequencer_ruler_widget.hpp delete mode 100644 src/gui/JUCE/sequencer_track_widget.cpp delete mode 100644 src/gui/JUCE/sequencer_track_widget.hpp delete mode 100644 src/gui/JUCE/sequencer_velocity_widget.cpp delete mode 100644 src/gui/JUCE/sequencer_velocity_widget.hpp delete mode 100644 src/gui/JUCE/sequencer_waveforms.cpp delete mode 100644 src/gui/JUCE/sequencer_widget.cpp delete mode 100644 src/gui/JUCE/sequencer_widget.hpp delete mode 100644 src/gui/JUCE/snap.hpp delete mode 100644 src/gui/JUCE/theme.hpp delete mode 100644 src/gui/JUCE/track_inserts.hpp delete mode 100644 src/gui/JUCE/ui.cpp delete mode 100644 src/gui/JUCE/ui.hpp delete mode 100644 src/gui/JUCE/velocity_lane.hpp delete mode 100644 tests/integration/test_juce_playback.cpp diff --git a/.gitignore b/.gitignore index cac3fab..fecdb38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Build products /build/ -/build-juce-gui/ /cmake-build-*/ /CMakeUserPresets.json /*.o diff --git a/CMakeLists.txt b/CMakeLists.txt index 411f6cf..5e9be23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,6 @@ find_package(Python3 3.12 REQUIRED COMPONENTS Interpreter) find_package(PkgConfig QUIET) option(XTC_WITH_JACK "Discover the optional JACK backend" ON) option(XTC_WITH_LADSPA "Enable LADSPA when its system header is available" ON) -option(XTC_BUILD_JUCE_GUI "Build the optional JUCE workstation frontend" OFF) option(XTC_BUILD_GUI "Build the FLTK application when FLTK is available" ON) option(XTC_ENABLE_LIVE_TESTS "Register tests requiring an existing JACK server" OFF) option(XTC_BUILD_JUCE "Build optional externally supplied JUCE examples" OFF) @@ -123,7 +122,6 @@ foreach(preview quality builtin) endforeach() include(cmake/Tests.cmake) include(cmake/Gui.cmake) -include(cmake/JuceGui.cmake) include(cmake/Developer.cmake) include(cmake/Install.cmake) if(XTC_BUILD_STEINBERG) diff --git a/README.md b/README.md index 4b4c1a3..f9e199d 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,6 @@ - **Modular engine first:** sequencer, instruments, effects, mixers and I/O are nodes in one processing graph. - **C core:** project model, graph, DSP and audio engine are implemented in C. -- **Optional JUCE GUI:** build `xtc-juce` with `-DXTC_BUILD_JUCE_GUI=ON -DXTC_JUCE_ROOT=/path/to/JUCE`. Keep or disable FLTK independently with `XTC_BUILD_GUI`. See [the JUCE frontend](src/gui/JUCE/README.md) for build/run instructions and SDK licensing. - **FLTK/C++ GUI:** C++ is confined to the graphical interface and calls the core through a C API. The sequencer coordinates nested ruler, track, header, clip and velocity widgets; see [GUI ownership](docs/ARCHITECTURE.md#sequencer-widget-ownership). - **Linux first:** keep backend-specific details behind an interface. - **Small increments:** build and test each milestone before adding features. @@ -88,7 +87,7 @@ The JACK backend has passed software-loopback testing; physical-device playback ## Build -Requirements: CMake 3.25+, a C99 compiler, Python 3.12+ and a build backend such as Ninja or Unix Makefiles. The default optional GUI needs C++17 and FLTK; a separately selectable JUCE frontend uses an external JUCE SDK; JACK and LADSPA development files enable their optional integrations. +Requirements: CMake 3.25+, a C99 compiler, Python 3.12+ and a build backend such as Ninja or Unix Makefiles. The optional GUI needs C++17 and FLTK; JACK and LADSPA development files enable their optional integrations. ```sh cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug @@ -108,7 +107,7 @@ make clean # clean products; keep the CMake cache Override `BUILD_DIR`, `BUILD_TYPE` or `JOBS` as needed, for example `make BUILD_DIR=build/release BUILD_TYPE=Release JOBS=8`. Pass additional options with `CMAKE_ARGS` and `CTEST_ARGS`. CMake remains the authoritative build system. -This builds `libxtc.a`, the headless `xtc-check`, tests, examples and the FLTK application when available. Live JACK tests are opt-in. Use `-DXTC_BUILD_GUI=OFF -DXTC_BUILD_JUCE_GUI=OFF` for the C-only build, or the `debug`, `release`, `minimal` and `sanitize` presets for separate configurations. See [docs/BUILD.md](docs/BUILD.md) for options, focused tests, installation and IDE setup. [Phase 108](docs/ROADMAP.md#phase-108--complete-cmake-build-migration) records the CMake migration. +This builds `libxtc.a`, the headless `xtc-check`, tests, examples and the FLTK application when available. Live JACK tests are opt-in. Use `-DXTC_BUILD_GUI=OFF` for the C-only build, or the `debug`, `release`, `minimal` and `sanitize` presets for separate configurations. See [docs/BUILD.md](docs/BUILD.md) for options, focused tests, installation and IDE setup. [Phase 108](docs/ROADMAP.md#phase-108--complete-cmake-build-migration) records the CMake migration. ## Run diff --git a/cmake/Install.cmake b/cmake/Install.cmake index 4520d7f..f206289 100644 --- a/cmake/Install.cmake +++ b/cmake/Install.cmake @@ -1,6 +1,5 @@ # Retain the audited owned-file inventory and symlink-safe staging implementation. set(XTC_INSTALL_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}" CACHE PATH "Directory containing built install inputs") -option(XTC_INSTALL_JUCE_GUI "Include the optional JUCE workstation in installation" ${XTC_BUILD_JUCE_GUI}) option(XTC_INSTALL_GUI "Include the GUI in installation" ${XTC_GUI_AVAILABLE}) set(XTC_INSTALL_LICENSEDIR "${CMAKE_INSTALL_DATAROOTDIR}/licenses/xtc" CACHE STRING "License install directory") set(XTC_INSTALL_EXAMPLESDIR "${CMAKE_INSTALL_DATAROOTDIR}/xtc/examples" CACHE STRING "Example install directory") diff --git a/cmake/InstallDriver.cmake.in b/cmake/InstallDriver.cmake.in index 5021dbe..0d98e1c 100644 --- a/cmake/InstallDriver.cmake.in +++ b/cmake/InstallDriver.cmake.in @@ -22,9 +22,6 @@ else() if("@XTC_INSTALL_GUI@") list(APPEND extra --gui) endif() - if("@XTC_INSTALL_JUCE_GUI@") - list(APPEND extra --juce-gui) - endif() set(jack) if("@JACK_FOUND@" AND "@XTC_WITH_JACK@") set(jack jack) diff --git a/cmake/JuceGui.cmake b/cmake/JuceGui.cmake deleted file mode 100644 index dd016c1..0000000 --- a/cmake/JuceGui.cmake +++ /dev/null @@ -1,91 +0,0 @@ -if(NOT XTC_BUILD_JUCE_GUI) - return() -endif() -enable_language(CXX) -set(XTC_JUCE_ROOT "" CACHE PATH "External JUCE source checkout") -if(NOT TARGET juce::juce_gui_extra) - if(NOT EXISTS "${XTC_JUCE_ROOT}/modules/juce_gui_extra/juce_gui_extra.h") - message(FATAL_ERROR "XTC_BUILD_JUCE_GUI requires XTC_JUCE_ROOT pointing to an external JUCE SDK checkout") - endif() - set(JUCE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) - set(JUCE_BUILD_EXTRAS OFF CACHE BOOL "" FORCE) - add_subdirectory("${XTC_JUCE_ROOT}" juce-gui-sdk EXCLUDE_FROM_ALL) -endif() -message(STATUS "Optional JUCE frontend: JUCE is AGPLv3/commercial; review the SDK licence before distributing") -set(xtc_juce_sources main_window.cpp graph_widget.cpp sequencer_widget.cpp - sequencer_commands.cpp sequencer_track_widget.cpp sequencer_clip_widget.cpp - sequencer_velocity_widget.cpp sequencer_ruler_widget.cpp sequencer_instruments.cpp - sequencer_waveforms.cpp piano_roll.cpp mixer_widget.cpp plugin_dialog.cpp sample_editor.cpp ui.cpp) -list(TRANSFORM xtc_juce_sources PREPEND "${PROJECT_SOURCE_DIR}/src/gui/JUCE/") -add_library(xtc-juce-gui STATIC ${xtc_juce_sources}) -xtc_warnings(xtc-juce-gui) -target_compile_definitions(xtc-juce-gui PUBLIC JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0 JUCE_MODAL_LOOPS_PERMITTED=1 JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1 JUCE_STANDALONE_APPLICATION=1 - "$<$:DEBUG=1;_DEBUG=1>" "$<$>:_NDEBUG=1>") -target_link_libraries(xtc-juce-gui PUBLIC xtc-core PRIVATE juce::juce_gui_extra) -target_include_directories(xtc-juce-gui SYSTEM PUBLIC "${XTC_JUCE_ROOT}/modules") -juce_add_gui_app(xtc-juce PRODUCT_NAME "xtc JUCE" COMPANY_NAME xtc BUNDLE_ID org.xtc.workstation) -set_target_properties(xtc-juce PROPERTIES OUTPUT_NAME xtc-juce RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") -target_sources(xtc-juce PRIVATE src/gui/JUCE/main.cpp) -target_link_libraries(xtc-juce PRIVATE xtc-juce-gui) -xtc_warnings(xtc-juce) -add_custom_target(gui-juce DEPENDS xtc-juce) -if(BUILD_TESTING) - find_program(XTC_JUCE_XVFB_RUN xvfb-run) - # Exercise the same C boundary, editing workflows and fault injection as FLTK. - # Adapt toolkit names/initialisation and show the inspector before native focus. - file(READ "${PROJECT_SOURCE_DIR}/tests/integration/test_gui.cpp" juce_test) - string(REGEX REPLACE "#include ]+>" "#include \"ui.hpp\"" juce_test "${juce_test}") - string(REPLACE "../../src/gui/" "${PROJECT_SOURCE_DIR}/src/gui/JUCE/" juce_test "${juce_test}") - string(REPLACE "Fl_" "Ui" juce_test "${juce_test}") - string(REPLACE "Fl::" "Ui::" juce_test "${juce_test}") - string(REPLACE "FL_" "UI_" juce_test "${juce_test}") - string(REPLACE "int main(int argc, char **argv)\n{" "int main(int argc, char **argv)\n{\n UiInitialiser initialise;" juce_test "${juce_test}") - # JUCE requires a visible component before native keyboard focus is requested. - string(REPLACE "if(gain){Ui::focus(&gain->input);" "if(gain){window.show_track_inspector(true);Ui::focus(&gain->input);" juce_test "${juce_test}") - # Native TextEditor commits on blur; unwind that additional edit before - # the shared test checks undo of its earlier explicit gain edit. - string(REPLACE "Ui::focus(nullptr);}" "Ui::focus(nullptr);pump();xtc_track_info blurred{};xtc_project_get_track(seq->project(),0,&blurred);if(blurred.gain==.123f)CHECK(window.undo()==XTC_OK);}" juce_test "${juce_test}") - file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/test_juce_gui.cpp" CONTENT "${juce_test}") - add_executable(test_juce_gui "${CMAKE_CURRENT_BINARY_DIR}/test_juce_gui.cpp") - target_include_directories(test_juce_gui PRIVATE "${PROJECT_SOURCE_DIR}/src/gui/JUCE") - target_link_libraries(test_juce_gui PRIVATE xtc-juce-gui) - xtc_wrap(test_juce_gui xtc_history_reserve) - xtc_warnings(test_juce_gui) - if(XTC_JUCE_XVFB_RUN) - foreach(pair "editing|" "sequencer|SEQUENCER_COMPONENTS" "midi|MIDI_WORKFLOWS" "samples|SAMPLE_EDITOR" "creative|CREATIVE" "next-daw|NEXT_DAW") - string(REPLACE "|" ";" pair "${pair}") - list(GET pair 0 name) - list(GET pair 1 env) - add_test(NAME juce-gui-${name} COMMAND ${XTC_JUCE_XVFB_RUN} -a -s "-screen 0 1440x960x24" $) - set_tests_properties(juce-gui-${name} PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" LABELS "gui;juce" RUN_SERIAL TRUE) - if(env) - set_property(TEST juce-gui-${name} PROPERTY ENVIRONMENT "XTC_TEST_${env}_ONLY=1") - endif() - endforeach() - endif() -endif() -if(BUILD_TESTING AND JACK_FOUND AND XTC_WITH_JACK) - add_executable(test_juce_playback tests/integration/test_juce_playback.cpp) - target_link_libraries(test_juce_playback PRIVATE xtc-juce-gui) - xtc_warnings(test_juce_playback) - if(XTC_ENABLE_LIVE_TESTS AND XTC_JUCE_XVFB_RUN) - add_test(NAME juce-playback COMMAND ${XTC_JUCE_XVFB_RUN} -a -s "-screen 0 1440x960x24" $) - set_tests_properties(juce-playback PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" LABELS "live;juce" RUN_SERIAL TRUE) - endif() -endif() -if(BUILD_TESTING AND XTC_JUCE_XVFB_RUN) - function(xtc_juce_gui_variant name) - add_test(NAME juce-gui-${name} COMMAND ${XTC_JUCE_XVFB_RUN} -a -s "-screen 0 1440x960x24" $) - set_tests_properties(juce-gui-${name} PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" LABELS "gui;juce" RUN_SERIAL TRUE ENVIRONMENT "${ARGN}") - endfunction() - xtc_juce_gui_variant(modern-editing "XTC_TEST_MODERN_EDITING_ONLY=1") - xtc_juce_gui_variant(midi-labels "XTC_TEST_MIDI_LABELS_ONLY=1") - set_property(TEST juce-gui-next-daw APPEND PROPERTY ENVIRONMENT XTC_TEST_MODERN_EDITING_ONLY=1) - xtc_juce_gui_variant(modern-plugins "XTC_TEST_CLAP=$" "XTC_TEST_VST2=$" "XTC_TEST_VST3=$") - xtc_juce_gui_variant(instruments "XTC_TEST_SYNTH_CLAP=$" "XTC_TEST_SYNTH_VST2=$" "XTC_TEST_SYNTH_VST3=$") - if(XTC_LADSPA_AVAILABLE) - xtc_juce_gui_variant(plugins "XTC_TEST_PLUGIN=$") - xtc_juce_gui_variant(track-fx "XTC_TEST_TRACK_FX_ONLY=1" "XTC_TEST_PLUGIN=$" "XTC_TEST_CLAP=$" "XTC_TEST_VST2=$" "XTC_TEST_VST3=$") - endif() -endif() -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/tests/integration/test_gui.cpp") diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d020866..a7e3d3c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -11,8 +11,7 @@ the graph editor is an advanced view of the same project. ## Layers -1. **C++ GUI:** input, drawing, windows, view state. FLTK is the default; - `src/gui/JUCE` is an independently optional JUCE frontend using the same C API. +1. **FLTK/C++ GUI:** input, drawing, windows, view state. 2. **C core:** project model, commands, parameters, persistence, validation. 3. **C engine:** prepared processing graph, DSP, transport timing, event delivery. @@ -219,16 +218,3 @@ files; unsupported native saves preserve files. See NATIVE_API.md and decision 0008. Release tooling lives outside the runtime, with source manifests, deterministic exports and staged installation/consumer checks. See RELEASING.md for reproducibility scope. - -## JUCE frontend ownership - -The optional `xtc-juce` application ports the complete retained GUI command -structure under `src/gui/JUCE`, without linking FLTK. `ui.*` supplies JUCE -components, native controls, graphics, event dispatch, timers, scrolling and -dialogs while preserving the original C-facing command boundaries. The master -sequencer owns JUCE ruler/track groups; tracks own header, instrument, clip and -velocity components. Group ownership, borrowed indexes, immutable copied C -metadata and deferred deletion follow the same rules as the FLTK frontend. -JUCE renders overlays after child components and routes callbacks on the message -thread. The audio backend and callbacks remain in C. See the -[JUCE source map](../src/gui/JUCE/README.md#components-and-maintenance). diff --git a/docs/BUILD.md b/docs/BUILD.md index 3142795..a982f76 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -79,7 +79,6 @@ lists, dependencies, SDK and packaging rules in CMake. | Cache option | Default | Meaning | | --- | --- | --- | | `BUILD_TESTING` | `ON` | Build/register the test suite | -| `XTC_BUILD_JUCE_GUI` | `OFF` | Build the JUCE workstation frontend (`xtc-juce`), independently of FLTK; requires `XTC_JUCE_ROOT` | | `XTC_BUILD_GUI` | `ON` | Discover FLTK and build the application when available | | `XTC_WITH_JACK` | `ON` | Discover/build the JACK/PipeWire backend | | `XTC_WITH_LADSPA` | `ON` | Enable LADSPA when the system header exists | @@ -192,21 +191,3 @@ Clean products with `cmake --build build --target clean`; to discard the cache, remove only the selected build directory. Never configure in the source tree. Historical validation entries in ROADMAP describe the build used at that time; Phase 108 records the CMake migration and its verified results. - -## Optional JUCE workstation frontend - -See [src/gui/JUCE/README.md](../src/gui/JUCE/README.md) for the component map and -licence requirements. The SDK is supplied by the user; no download occurs. - -```sh -cmake -S . -B build/juce-gui -DCMAKE_BUILD_TYPE=Debug \ - -DXTC_BUILD_GUI=OFF -DXTC_BUILD_JUCE_GUI=ON -DXTC_JUCE_ROOT=/path/to/JUCE -cmake --build build/juce-gui --parallel 4 --target xtc-juce test_juce_gui -./build/juce-gui/xtc-juce examples/projects/playback-test.xtc -ctest --test-dir build/juce-gui -R juce-gui --output-on-failure -j1 -``` - -`XTC_BUILD_GUI=ON` builds both frontends. `XTC_BUILD_JUCE` continues to mean -JUCE plugin/processor examples and can be enabled independently or alongside -the workstation frontend. A missing SDK is a configuration error only when a -JUCE option is enabled. The existing default FLTK build is unchanged. diff --git a/docs/GUI.md b/docs/GUI.md index 51db44e..21b098b 100644 --- a/docs/GUI.md +++ b/docs/GUI.md @@ -1,10 +1,6 @@ # GUI Guidelines -The default frontend uses FLTK/C++ for windows, menus, dialogs, and the event loop. -An optional JUCE port is available under `src/gui/JUCE`; build `xtc-juce` with -`XTC_BUILD_JUCE_GUI=ON` and an external `XTC_JUCE_ROOT`. Both frontends can be -built together and use the same project files and C API. See -[JUCE build and component details](../src/gui/JUCE/README.md). Keep all audio +Use FLTK/C++ for windows, menus, dialogs, and the event loop. Keep all audio engine and project-domain logic in C. Main views: diff --git a/docs/JUCE.md b/docs/JUCE.md index 8d0b4c7..9e8b76a 100644 --- a/docs/JUCE.md +++ b/docs/JUCE.md @@ -1,8 +1,7 @@ -# JUCE integration +# JUCE integration (Phase 107) -JUCE supplies an [optional workstation frontend](../src/gui/JUCE/README.md) -(`XTC_BUILD_JUCE_GUI`) as well as runnable plugin examples (`XTC_BUILD_JUCE`) -alongside xtc's C99 engine and FLTK application. The two options are independent. It is an external SDK dependency, not vendored, fetched, linked by +JUCE is used by optional, runnable examples alongside xtc's C99 engine and FLTK +application. It is an external SDK dependency, not vendored, fetched, linked by normal builds or automatically installed. The integration uses: | JUCE capability | Implemented use | diff --git a/docs/LICENSES.md b/docs/LICENSES.md index b9b5875..a683894 100644 --- a/docs/LICENSES.md +++ b/docs/LICENSES.md @@ -79,11 +79,3 @@ repository code; no Steinberg VST2 SDK or third-party plugin is bundled. Hosting an installed binary is distinct from SDK, trademark and plugin redistribution rights. Review those separately for any proposed distribution. Source archives retain upstream notices; binary installs also ship both header licenses. - -## Optional JUCE workstation frontend - -`XTC_BUILD_JUCE_GUI` links the user-supplied JUCE SDK under its AGPLv3 or -commercial licence. It is off by default and separate from the MIT C core and -FLTK frontend. No SDK source is copied into this repository or downloaded by -normal configuration. Review the supplied SDK's licence and its graphics/text -dependencies when distributing `xtc-juce`. See [the frontend guide](../src/gui/JUCE/README.md). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 23e0067..2da2677 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -3739,47 +3739,6 @@ Play/Stop/Space/stereo check also passed. See the integration-test guide for com --- -## Phase 113 — Optional JUCE workstation frontend - -### Tasks and acceptance criteria - -- [x] Preserve the FLTK sources and default build; add an independently optional - `XTC_BUILD_JUCE_GUI` workstation under `src/gui/JUCE`. -- [x] Require a user-supplied SDK through `XTC_JUCE_ROOT`, with an explicit error - when enabled without the SDK and no SDK discovery/download in normal builds. -- [x] Port the main window, sequencer/master and child views, graph, mixer, - automation, piano roll, velocity editing, sample editor, plugin dialogs, - insert chains, preferences, searchable commands and support widgets. -- [x] Use JUCE components, controls, graphics, native events, dialogs and timers; - keep project/history/DSP/backend/plugin APIs in C and preserve `.xtc` files. -- [x] Allow JUCE-only, FLTK-only, both and core-only configurations; share the - externally supplied SDK with the independently optional plugin examples. -- [x] Include the JUCE binary in the owned install/uninstall inventory when selected. -- [x] Pass the retained GUI regression workflows against JUCE, including failure - rollback, child ownership, gestures, preferences and MIDI device fallback. -- [x] Verify native JUCE transport events and finite stereo playback with the - bundled test song and missing saved MIDI devices. -- [x] Validate the original FLTK build/tests and document final build results. - -### Implementation notes - -The frontend retains the original C-facing command structure and filenames. -`ui.*` maps the retained coordinates and callback conventions to JUCE, with no -FLTK headers or runtime dependency. Native controls may look different. Both -frontends continue to use the existing JACK/PipeWire backend. See the -[frontend source and build guide](../src/gui/JUCE/README.md). -JUCE is an optional external AGPLv3/commercial SDK; normal builds do not link it. -Qualification is for Linux, rather than a new cross-platform audio backend. -The original 57 FLTK/core tests, core-only build, missing-SDK diagnostic, shared -SDK configuration and staged install/uninstall checks passed. The frontend was -built with warnings enabled against external JUCE 8.0.15. Native Play/Stop/Space, -expanded track rendering and finite stereo playback passed with missing saved -MIDI devices. All 12 JUCE GUI workflows passed, with focused reruns after the -native focus/blur adjustments. The [workstation capture](../screenshots/juce-workstation.png) -comes from this real-backend regression, with no physical routes changed. - ---- - ## Recommended Execution Order Reorder the phases when the audit reveals a blocking dependency. Do not mark a diff --git a/examples/juce/CMakeLists.txt b/examples/juce/CMakeLists.txt index a3974e7..6bd1342 100644 --- a/examples/juce/CMakeLists.txt +++ b/examples/juce/CMakeLists.txt @@ -11,9 +11,7 @@ endif() message(STATUS "JUCE is AGPLv3/commercial: review the external SDK LICENSE.md before distributing these binaries") set(JUCE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(JUCE_BUILD_EXTRAS OFF CACHE BOOL "" FORCE) -if(NOT TARGET juce::juce_audio_utils) - add_subdirectory("${XTC_JUCE_ROOT}" juce-sdk EXCLUDE_FROM_ALL) -endif() +add_subdirectory("${XTC_JUCE_ROOT}" juce-sdk EXCLUDE_FROM_ALL) get_filename_component(XTC_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) if(NOT TARGET xtc-core) set(XTC_BUILD_GUI OFF CACHE BOOL "" FORCE) diff --git a/screenshots/ALL.md b/screenshots/ALL.md index 702987f..e7fee09 100644 --- a/screenshots/ALL.md +++ b/screenshots/ALL.md @@ -5,15 +5,6 @@ See [capture notes and reproduction instructions](README.md) for image provenanc --- -## The optional JUCE workstation - -![JUCE workstation playing the bundled test project](juce-workstation.png) - -The JUCE frontend plays the bundled test song with an expanded velocity lane; -missing saved MIDI devices appear as a warning while stereo playback continues. - ---- - ## The xtc workspace at a glance ![The xtc workspace at a glance](overview.png) diff --git a/screenshots/README.md b/screenshots/README.md index 4b39f4b..a224ddf 100644 --- a/screenshots/README.md +++ b/screenshots/README.md @@ -32,23 +32,6 @@ APIs and GUI history/preparation paths. Original projects, media and user configuration are not overwritten. Black margins are the virtual desktop; floating views show the actual detached windows together. -## Optional JUCE workstation (Phase 113) - -![JUCE workstation playing the bundled test project](juce-workstation.png) - -Rendered directly from the actual JUCE component hierarchy, with the first -track expanded and playback running. The test deliberately configures missing -MIDI devices and captures finite stereo output through private JACK probe ports. -It preserves user preferences and physical routes. A running JACK-compatible -server is required. After building the optional frontend and tests: - -```sh -XTC_JUCE_SCREENSHOT="$PWD/screenshots/juce-workstation.png" \ - xvfb-run -a -s '-screen 0 1440x960x24' ./build/juce-gui/test_juce_playback -``` - -See the [JUCE frontend guide](../src/gui/JUCE/README.md) for SDK and build options. - ## Continuous audio velocity editing (Phase 110) ![Audio velocity envelope beneath a waveform track](audio-velocity-expanded.png) diff --git a/screenshots/juce-workstation.png b/screenshots/juce-workstation.png deleted file mode 100644 index 2b62f5d169aa3bb16f3bd1d6ddede643eae85f2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93069 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 Lj7Y(UuRs0|gw#c= diff --git a/src/gui/JUCE/README.md b/src/gui/JUCE/README.md deleted file mode 100644 index c4656b7..0000000 --- a/src/gui/JUCE/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# JUCE workstation frontend - -This directory ports the workstation GUI to JUCE. The sibling FLTK sources are -retained. Both frontends call the same C99 project, history, DSP, plugin and JACK -APIs and open the same `.xtc` files. No FLTK headers or library are required by -this frontend. - -## Build and run - -Supply your own JUCE SDK source checkout. CMake does not download the SDK: - -```sh -cmake -S . -B build/juce-gui -DCMAKE_BUILD_TYPE=Debug \ - -DXTC_BUILD_JUCE_GUI=ON -DXTC_BUILD_GUI=OFF \ - -DXTC_JUCE_ROOT=/absolute/path/to/JUCE -cmake --build build/juce-gui --parallel 4 --target xtc-juce -./build/juce-gui/xtc-juce examples/projects/playback-test.xtc -``` - -Leave `XTC_BUILD_GUI=ON` to build both `xtc` (FLTK) and `xtc-juce`. Set both GUI -options off for a core-only build. `XTC_BUILD_JUCE` independently enables the -Soft Keys plugin examples; it is not required for this application. Both JUCE -options can share one SDK checkout in the same build. - -`cmake --install build/juce-gui` includes `xtc-juce` through the existing owned -file inventory; set `XTC_INSTALL_JUCE_GUI=OFF` to exclude it. `uninstall` removes -the same owned binary. Standard binary packages remain core/FLTK packages. - -The qualified platform is Linux with JUCE 8.0.15 and the existing JACK/PipeWire -backend. This port does not replace JACK with JUCE AudioDeviceManager or change -the C engine's platform support. Playback needs a running JACK-compatible server; -MIDI devices are optional. Route the stereo outputs to speakers to listen. - -JUCE is **AGPLv3/commercial**, supplied externally. Review its SDK licence before -redistributing linked binaries. The SDK increases compile time and requires its -normal Linux graphics/windowing development dependencies. Normal FLTK and core -builds neither discover nor link JUCE. - -## Components and maintenance - -The matching filenames preserve the existing command and model boundaries: - -| Files | Responsibility | -| --- | --- | -| `main.cpp`, `main_window.*` | Application, menus, transport, project lifecycle, history and backend control | -| `sequencer_widget.*` | Master selection, coordinates, commands and view reconciliation | -| `sequencer_{track,clip,velocity,ruler}_widget.*` | Actual JUCE child components for tracks, headers, clips, velocity and ruler | -| `sequencer_commands.cpp`, `sequencer_instruments.cpp`, `sequencer_waveforms.cpp` | Editing commands, instrument choices and waveform caches | -| `graph_widget.*`, `mixer_widget.*`, `piano_roll.*`, `sample_editor.*` | Graph editing, mixer/automation, note editing and destructive sample-copy editing | -| `preferences_dialog.hpp`, `plugin_dialog.*`, `plugin_library.hpp`, `track_inserts.hpp`, `command_palette.hpp` | Preferences, plugin discovery/controls, insert chains and searchable commands | -| `theme.hpp`, `snap.hpp`, `velocity_lane.hpp`, `insert_meter.hpp` | Appearance, snapping, controller lane and meters | -| `ui.*` | JUCE component adapter: ownership, retained coordinates, drawing, events, controls, menus, dialogs, scrolling and timers | - -`UiWidget` derives from `juce::Component`; the sequencer hierarchy is a JUCE -component hierarchy. Groups own their workstation children; JUCE also tracks -native child controls. Explicit removal detaches without deleting. Deferred -removal uses a JUCE safe pointer and the message queue. JUCE text editors, -buttons, sliders, list boxes, scrollbars, popup menus and file choosers provide -the platform interaction. Custom canvases draw with `juce::Graphics`; overlays -paint after child components. The rotary pan control uses JUCE's rotary slider -and double-click reset. GUI operations and timers stay on the message thread; -audio callbacks continue to run only C code. - -This is a retained-command port rather than a redesign of the workstation. -When changing a GUI workflow, update both frontend implementations. The common -GUI regression source is compiled against each toolkit, preventing separate -test suites from drifting. The JUCE variant shows the track inspector before -requesting native keyboard focus, since hidden components cannot receive it, and -accounts for JUCE text editors committing an additional undoable edit on blur. The JUCE adapter intentionally retains the existing -coordinate and callback conventions; it does not emulate or link the FLTK -runtime. Frontend-specific appearance and native dialogs differ. - -See [build and validation guidance](../../../docs/BUILD.md) and -[GUI architecture](../../../docs/ARCHITECTURE.md). diff --git a/src/gui/JUCE/command_palette.hpp b/src/gui/JUCE/command_palette.hpp deleted file mode 100644 index d4c1c27..0000000 --- a/src/gui/JUCE/command_palette.hpp +++ /dev/null @@ -1,100 +0,0 @@ -#ifndef XTC_COMMAND_PALETTE_HPP -#define XTC_COMMAND_PALETTE_HPP -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include - -/* UI command search only; execute the chosen action after this modal is gone. */ -class CommandPalette : public UiDouble_Window -{ - std::vector commands_, visible_; - UiInput *search_; - UiHold_Browser *list_; - std::string selected_; - static std::string lower(std::string value) - { - for (auto &c : value) - c = static_cast(std::tolower(static_cast(c))); - return value; - } - void filter() - { - list_->clear(); - visible_.clear(); - const auto query = lower(search_->value()); - for (const auto &command : commands_) - if (lower(command).find(query) != std::string::npos) { - visible_.push_back(command); - list_->add(command.c_str()); - } - if (!visible_.empty()) - list_->select(1); - } - void accept() - { - int i = list_->value(); - if (i > 0 && static_cast(i) <= visible_.size()) { - selected_ = visible_[static_cast(i - 1)]; - hide(); - } - } - - public: - explicit CommandPalette(std::vector commands) - : UiDouble_Window(700, 440, "xtc — Commands"), commands_(std::move(commands)) - { - set_modal(); - begin(); - search_ = new UiInput(136, 18, 544, 32, "Search commands"); - search_->when(UI_WHEN_CHANGED); - search_->callback([](UiWidget *, void *p) { static_cast(p)->filter(); }, this); - list_ = new UiHold_Browser(20, 64, 660, 302); - list_->format_char(0); - list_->callback( - [](UiWidget *, void *p) { - if (Ui::event_clicks()) - static_cast(p)->accept(); - }, - this); - auto *cancel = new UiButton(450, 390, 100, 30, "Cancel"); - cancel->callback([](UiWidget *, void *p) { static_cast(p)->hide(); }, this); - auto *run = new UiButton(564, 390, 116, 30, "Run command"); - run->callback([](UiWidget *, void *p) { static_cast(p)->accept(); }, this); - callback([](UiWidget *, void *p) { static_cast(p)->hide(); }, this); - end(); - xtc_ui::style(this); - filter(); - } - int handle(int event) override - { - if (event == UI_KEYDOWN) { - int key = Ui::event_key(); - if (key == UI_Enter || key == UI_KP_Enter) { - accept(); - return 1; - } - if (key == UI_Escape) { - hide(); - return 1; - } - if ((key == UI_Up || key == UI_Down) && !visible_.empty()) { - list_->select( - std::clamp(list_->value() + (key == UI_Up ? -1 : 1), 1, static_cast(visible_.size()))); - return 1; - } - } - return UiDouble_Window::handle(event); - } - std::string run() - { - show(); - search_->take_focus(); - while (shown()) - Ui::wait(); - return selected_; - } -}; -#endif diff --git a/src/gui/JUCE/graph_widget.cpp b/src/gui/JUCE/graph_widget.cpp deleted file mode 100644 index 8642d6b..0000000 --- a/src/gui/JUCE/graph_widget.cpp +++ /dev/null @@ -1,645 +0,0 @@ -#include "theme.hpp" -#include "graph_widget.hpp" -#include "xtc/xtc_plugin.h" -#include "ui.hpp" -#include -#include -#include - -namespace { -constexpr int node_width = 150, node_height = 74; - -/** - * @brief Returns the display label for a node type. - * - * @param type Node type to describe. - * @return Borrowed static string; never NULL, falling back to "Unknown". - */ -const char *node_name(xtc_node_type type) -{ - switch (type) { - case XTC_NODE_OSCILLATOR: - return "Oscillator"; - case XTC_NODE_GAIN: - return "Gain / bus"; - case XTC_NODE_METER: - return "Meter"; - case XTC_NODE_PLUGIN: - return "Effect plugin"; - case XTC_NODE_NATIVE: - return "Native processor"; - case XTC_NODE_INSTRUMENT: - return "MIDI mix"; - case XTC_NODE_AUDIO: - return "Audio mix"; - case XTC_NODE_OUTPUT: - return "Output sum"; - } - return "Unknown"; -} -} // namespace - -/** - * @brief Constructs the widget and initializes its cached layout from the graph. - * - * The graph, handler, context, and feedback box are stored without taking - * ownership. Layout population is delegated to set_graph(). - * - * @param left Left coordinate in JUCE screen space. - * @param top Top coordinate in JUCE screen space. - * @param width Widget width in pixels. - * @param height Widget height in pixels. - * @param graph Borrowed graph snapshot; must outlive the widget's use of it. - * @param handler Edit callback invoked for every graph mutation. - * @param context Opaque pointer passed back to @p handler; may be NULL. - * @param feedback_box Borrowed label used to report edit results. - */ -GraphWidget::GraphWidget(int left, int top, int width, int height, const xtc_graph *graph, EditHandler handler, - void *context, UiBox *feedback_box) - : UiWidget(left, top, width, height), graph_(graph), handler_(handler), context_(context), feedback_(feedback_box) -{ - set_graph(graph); -} - -/** - * @brief Replaces the graph snapshot and rebuilds cached node positions. - * - * Existing positions are preserved by node ID. New nodes get grid slots that do - * not overlap previously known nodes. The selection is dropped when the selected - * node or wire is no longer present. The widget does not take ownership of the - * graph. - * - * @param graph Borrowed graph that must outlive the widget's use of it. - */ -void GraphWidget::set_graph(const xtc_graph *graph) -{ - graph_ = graph; - routing_count_ = 0; - (void)xtc_graph_analyze(graph_, routing_.data(), routing_.size(), &routing_count_); - size_t nodes = 0U, edges = 0U; - if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) { - count_ = 0U; - redraw(); - return; - } - auto old = positions_; - size_t old_count = count_; - count_ = 0U; - for (size_t i = 0U; i < nodes && i < positions_.size(); ++i) { - xtc_node_info node{}; - if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) - continue; - Position position; - position.id = node.id; - bool found = false; - for (size_t j = 0U; j < old_count; ++j) - if (old[j].id == node.id) { - position = old[j]; - found = true; - break; - } - if (!found) { - for (size_t slot = 0U; slot <= positions_.size(); ++slot) { - position.left = 40 + static_cast(slot % 8U) * 210; - position.top = 40 + static_cast(slot / 8U) * 115; - bool occupied = false; - for (size_t j = 0U; j < old_count; ++j) - if (std::abs(old[j].left - position.left) < node_width + 12 && - std::abs(old[j].top - position.top) < node_height + 12) - occupied = true; - for (size_t j = 0U; j < count_; ++j) - if (std::abs(positions_[j].left - position.left) < node_width + 12 && - std::abs(positions_[j].top - position.top) < node_height + 12) - occupied = true; - if (!occupied) - break; - } - } - positions_[count_++] = position; - } - int left, top; - if (!node_position(selected_node_, left, top)) - selected_node_ = SIZE_MAX; - if (edge_selected_ && !select_connection(selected_edge_.source_node, selected_edge_.destination_node)) - edge_selected_ = false; - grow_canvas(); - redraw(); -} - -/** - * @brief Looks up the cached position of a node. - * - * @param id Stable node identifier. - * @param left Receives the left position in widget-local coordinates. - * @param top Receives the top position in widget-local coordinates. - * @return True when the node has a cached position, false otherwise. - */ -bool GraphWidget::node_position(size_t id, int &left, int &top) const -{ - for (size_t i = 0U; i < count_; ++i) - if (positions_[i].id == id) { - left = positions_[i].left; - top = positions_[i].top; - return true; - } - return false; -} - -/** - * @brief Moves a node within the widget, clamped to the visible bounds. - * - * The node keeps a 12-pixel margin from the widget edges. - * - * @param id Stable node identifier. - * @param left Requested left position in widget-local coordinates. - * @param top Requested top position in widget-local coordinates. - * @return True when the node has a cached position, false otherwise. - */ -bool GraphWidget::move_node(size_t id, int left, int top) -{ - for (size_t i = 0U; i < count_; ++i) - if (positions_[i].id == id) { - positions_[i].left = std::max(12, std::min(left, w() - node_width - 12)); - positions_[i].top = std::max(12, std::min(top, h() - node_height - 12)); - redraw(); - return true; - } - return false; -} - -/** - * @brief Selects a node by ID and clears any wire selection. - * - * @param id Stable node identifier. - * @return True when the node has a cached position, false otherwise. - */ -bool GraphWidget::select_node(size_t id) -{ - int left, top; - if (!node_position(id, left, top)) - return false; - selected_node_ = id; - edge_selected_ = false; - describe_selection(); - redraw(); - return true; -} - -/** - * @brief Selects a wire between two nodes and clears the node selection. - * - * @param source Source node identifier. - * @param destination Destination node identifier. - * @return True when a matching connection exists, false otherwise. - */ -bool GraphWidget::select_connection(size_t source, size_t destination) -{ - size_t nodes, edges; - if (xtc_graph_get_counts(graph_, &nodes, &edges) != XTC_OK) - return false; - for (size_t i = 0U; i < edges; ++i) { - xtc_connection_info edge{}; - if (xtc_graph_get_connection(graph_, i, &edge) == XTC_OK && edge.source_node == source && - edge.destination_node == destination) { - selected_edge_ = edge; - edge_selected_ = true; - selected_node_ = SIZE_MAX; - describe_selection(); - redraw(); - return true; - } - } - return false; -} - -/** - * @brief Shows the outcome of an edit in the feedback label. - * - * @param result Result returned by the edit handler. - * @param success Text shown when @p result is XTC_OK; must be valid then and is ignored otherwise. - */ -void GraphWidget::feedback(xtc_result result, const char *success) -{ - char text[256]; - if (result == XTC_OK) - std::snprintf(text, sizeof(text), "%s", success); - else if (result == XTC_ERR_CAPACITY) - std::snprintf( - text, sizeof(text), - "Graph unchanged: capacity exceeded (node limit or pending edit). Retry after the next audio block."); - else if (result == XTC_ERR_GRAPH_CYCLE) - std::snprintf(text, sizeof(text), "Graph unchanged: %s. Feedback loops are unsupported.", xtc_result_string(result)); - else - std::snprintf(text, sizeof(text), "Graph unchanged: %s. Connect an audio output (right) to an input (left); duplicate wires are rejected.", xtc_result_string(result)); - feedback_->copy_label(text); -} - -/** - * @brief Requests creation of a node through the edit handler. - * - * Selects the new node when the edit succeeds. - * - * @param type Kind of node to add. - * @return Result reported by the edit handler. - */ -xtc_result GraphWidget::add_node(xtc_node_type type) -{ - xtc_graph_edit edit{}; - edit.kind = XTC_EDIT_ADD_NODE; - edit.data.add.type = type; - edit.data.add.value = type == XTC_NODE_OSCILLATOR ? 440.0f : type == XTC_NODE_GAIN ? 0.1f : 0.0f; - size_t added = SIZE_MAX; - xtc_result result = handler_(context_, edit, &added); - if (result == XTC_OK) - select_node(added); - feedback(result, "Node added. Drag its title to move; drag an output port to an input to connect."); - return result; -} - -/** - * @brief Requests a connection between two ports through the edit handler. - * - * @param connection Connection describing the source and destination node/port pairs. - * @return Result reported by the edit handler. - */ -xtc_result GraphWidget::connect_ports(const xtc_connection_info &connection) -{ - xtc_graph_edit edit{}; - edit.kind = XTC_EDIT_CONNECT; - edit.data.connection = connection; - xtc_result result = handler_(context_, edit, nullptr); - feedback(result, "Connection added. Click a wire and press Delete to disconnect."); - return result; -} - -/** - * @brief Requests removal of the selected wire or node through the edit handler. - * - * @return Result reported by the edit handler, or XTC_ERR_INVALID_STATE when - * nothing is selected. - */ -xtc_result GraphWidget::delete_selected() -{ - xtc_graph_edit edit{}; - if (edge_selected_) { - edit.kind = XTC_EDIT_DISCONNECT; - edit.data.connection = selected_edge_; - } else if (selected_node_ != SIZE_MAX) { - edit.kind = XTC_EDIT_REMOVE_NODE; - edit.data.node_id = selected_node_; - } else { - feedback_->copy_label("Select a node or wire first."); - return XTC_ERR_INVALID_STATE; - } - xtc_result result = handler_(context_, edit, nullptr); - feedback(result, "Selection deleted. Unwired inputs use external audio; select a node to inspect routing."); - return result; -} - -/** - * @brief Computes the widget-local center of a node port. - * - * @param id Stable node identifier. - * @param port Port index; 0 selects the input and 1 the output. - * @param left Receives the port center x coordinate. - * @param top Receives the port center y coordinate. - * @return True when the node and port exist, false otherwise. - */ -bool GraphWidget::port_position(size_t id, size_t port, int &left, int &top) const -{ - xtc_port_info info{}; - if (xtc_graph_get_port(graph_, id, port, &info) != XTC_OK || !node_position(id, left, top)) - return false; - left += info.direction == XTC_PORT_OUTPUT ? node_width : 0; - top += 48; - return true; -} - -/** - * @brief Finds the port nearest to a widget-local point. - * - * Searches nodes in reverse draw order and accepts points within 9 pixels of a - * port center. - * - * @param left Widget-local x coordinate to test. - * @param top Widget-local y coordinate to test. - * @param node Receives the matching node ID when true is returned. - * @param port Receives the matching port index (0 input, 1 output) when true is returned. - * @return True when a port is within range, false otherwise. - */ -bool GraphWidget::hit_port(int left, int top, size_t &node, size_t &port) const -{ - for (size_t i = count_; i > 0U; --i) - for (size_t p = 0U; p < 2U; ++p) { - int px, py; - if (port_position(positions_[i - 1U].id, p, px, py) && std::abs(px - left) <= 9 && - std::abs(py - top) <= 9) { - node = positions_[i - 1U].id; - port = p; - return true; - } - } - return false; -} - -/** - * @brief Draws the background, wires, nodes, ports, and any pending wire. - */ -void GraphWidget::draw() -{ - ui_push_clip(x(), y(), w(), h()); - ui_color(ui_rgb_color(25, 28, 33)); - ui_rectf(x(), y(), w(), h()); - size_t nodes = 0U, edges = 0U; - (void)xtc_graph_get_counts(graph_, &nodes, &edges); - for (size_t i = 0U; i < edges; ++i) { - xtc_connection_info edge{}; - int sx, sy, dx, dy; - if (xtc_graph_get_connection(graph_, i, &edge) != XTC_OK || !port_position(edge.source_node, 1U, sx, sy) || - !port_position(edge.destination_node, 0U, dx, dy)) - continue; - bool selected = (edge_selected_ && selected_edge_.source_node == edge.source_node && - selected_edge_.destination_node == edge.destination_node) || - selected_node_ == edge.source_node || selected_node_ == edge.destination_node; - xtc_routing_info route{}; - bool audible_path = routing(edge.destination_node, route) && route.reaches_output; - ui_color(selected ? xtc_ui::accent() : audible_path ? ui_rgb_color(100, 200, 190) : ui_rgb_color(110, 115, 125)); - ui_line_style(UI_SOLID, selected ? 3 : 2); - ui_line(x() + sx, y() + sy, x() + dx, y() + dy); - double length = std::hypot(dx-sx, dy-sy); - if (length > 20) { - double ux = (dx-sx)/length, uy = (dy-sy)/length; - int ax = x() + (sx+dx)/2, ay = y() + (sy+dy)/2; - ui_line(ax, ay, ax-static_cast(ux*9-uy*5), ay-static_cast(uy*9+ux*5)); - ui_line(ax, ay, ax-static_cast(ux*9+uy*5), ay-static_cast(uy*9-ux*5)); - } - } - ui_line_style(0); - for (size_t i = 0U; i < count_; ++i) { - xtc_node_info node{}; - if (xtc_graph_get_node(graph_, i, &node) != XTC_OK) - continue; - int left = x() + positions_[i].left, top = y() + positions_[i].top; - ui_color(node.type == XTC_NODE_OUTPUT ? ui_rgb_color(100, 50, 55) - : node.type == XTC_NODE_GAIN ? ui_rgb_color(75, 55, 95) - : node.type == XTC_NODE_OSCILLATOR || node.type == XTC_NODE_AUDIO || node.type == XTC_NODE_INSTRUMENT - ? ui_rgb_color(35, 80, 75) : ui_rgb_color(49, 65, 85)); - ui_rectf(left, top, node_width, node_height); - ui_color(selected_node_ == node.id ? xtc_ui::accent() : ui_rgb_color(115, 130, 145)); - ui_rect(left, top, node_width, node_height); - ui_color(UI_WHITE); - ui_font(UI_HELVETICA_BOLD, 12); - char text[96]; - xtc_plugin_spec title_plugin{}; - if(node.type==XTC_NODE_PLUGIN && xtc_graph_get_plugin(graph_,node.id,&title_plugin)==XTC_OK) - std::snprintf(text,sizeof(text),"%s effect #%zu",xtc_plugin_format_name(title_plugin.format),node.id); - else std::snprintf(text, sizeof(text), "%s #%zu", node_name(node.type), node.id); - ui_push_clip(left+6, top+2, node_width-12, 40); - ui_draw(text, left + 10, top + 20); - ui_font(UI_HELVETICA, 12); - if (node.type == XTC_NODE_OSCILLATOR) - std::snprintf(text, sizeof(text), "%.0f Hz", static_cast(node.value)); - else if (node.type == XTC_NODE_GAIN) - std::snprintf(text, sizeof(text), "x %.2f", static_cast(node.value)); - else if (node.type == XTC_NODE_PLUGIN) { - xtc_plugin_spec plugin{}; - if (xtc_graph_get_plugin(graph_,node.id,&plugin) == XTC_OK) - std::snprintf(text,sizeof(text),"%.20s",plugin.label); - } else - std::snprintf(text, sizeof(text), "%s", - node.type == XTC_NODE_METER ? "Peak pass-through" - : node.type == XTC_NODE_INSTRUMENT ? "MIDI sine source" - : node.type == XTC_NODE_AUDIO ? "Timeline mix source" - : "Summed engine output"); - ui_draw(text, left + 10, top + 37); - ui_pop_clip(); - xtc_routing_info route{}; - if (routing(node.id, route) && !route.reaches_output) { - ui_color(xtc_ui::accent()); - ui_draw("! no output path", left + 10, top + 72); - } - for (size_t port = 0U; port < 2U; ++port) { - int px, py; - if (!port_position(node.id, port, px, py)) - continue; - ui_color(ui_rgb_color(110, 220, 185)); - ui_pie(x() + px - 5, y() + py - 5, 10, 10, 0, 360); - ui_color(UI_WHITE); - ui_draw(port == 0U ? (route.external_input ? "ext in" : "sum in") : "audio out", left + (port == 0U ? 10 : 90), top + 56); - } - } - if (wiring_) { - int px, py; - if (port_position(wire_node_, wire_port_, px, py)) { - ui_color(xtc_ui::accent()); - ui_line(x() + px, y() + py, x() + mouse_x_, y() + mouse_y_); - } - } - ui_pop_clip(); -} - -/** - * @brief Handles mouse and keyboard events for selection, dragging, and wiring. - * - * @param event JUCE event code. - * @return 1 when the event was consumed, 0 for unsupported left-button input, - * or the result of UiWidget::handle() for unhandled events. - */ -bool GraphWidget::select_at(int left, int top) -{ - for (size_t i = count_; i > 0; --i) { - const auto &pos = positions_[i-1]; - if (left >= pos.left && left <= pos.left+node_width && top >= pos.top && top <= pos.top+node_height) - return select_node(pos.id); - } - size_t nodes = 0, edges = 0; - xtc_graph_get_counts(graph_,&nodes,&edges); - for (size_t i = 0; i < edges; ++i) { - xtc_connection_info edge{}; int sx, sy, dx, dy; - xtc_graph_get_connection(graph_,i,&edge); - if (!port_position(edge.source_node,1,sx,sy) || !port_position(edge.destination_node,0,dx,dy)) continue; - double vx = dx-sx, vy = dy-sy, length = vx*vx+vy*vy; - double t = length == 0 ? 0 : std::clamp(((left-sx)*vx+(top-sy)*vy)/length,0.0,1.0); - if (std::hypot(left-sx-t*vx,top-sy-t*vy) <= 6) return select_connection(edge.source_node,edge.destination_node); - } - selected_node_ = SIZE_MAX; edge_selected_ = false; describe_selection(); redraw(); return false; -} - -int GraphWidget::handle(int event) -{ - const int left = Ui::event_x() - x(), top = Ui::event_y() - y(); - switch (event) { - case UI_FOCUS: - case UI_UNFOCUS: - return 1; - case UI_PUSH: { - if (Ui::event_button() == UI_RIGHT_MOUSE) { - take_focus(); select_at(left,top); - UiMenu_Item items[5]{}; - items[0].text = "Inspect selection"; - items[1].text = "Arrange by signal flow (L)"; - items[2].text = "Insert unity gain in selected wire (I)"; - items[3].text = "Delete selection"; - const auto *picked = items->popup(Ui::event_x(), Ui::event_y()); - if (picked == items) describe_selection(); - else if (picked == items+1) auto_layout(); - else if (picked == items+2) insert_gain(); - else if (picked == items+3) delete_selected(); - return 1; - } - if (Ui::event_button() != UI_LEFT_MOUSE) return 0; - take_focus(); - mouse_x_ = left; - mouse_y_ = top; - dragging_ = false; - wiring_ = false; - if (hit_port(left, top, wire_node_, wire_port_)) { - wiring_ = true; - redraw(); - return 1; - } - for (size_t i = count_; i > 0U; --i) { - const Position &pos = positions_[i - 1U]; - if (left >= pos.left && left <= pos.left + node_width && top >= pos.top && top <= pos.top + node_height) { - select_node(pos.id); - dragging_ = true; - drag_x_ = left - pos.left; - drag_y_ = top - pos.top; - return 1; - } - } - size_t nodes, edges; - (void)xtc_graph_get_counts(graph_, &nodes, &edges); - for (size_t i = 0U; i < edges; ++i) { - xtc_connection_info edge{}; - int sx, sy, dx, dy; - (void)xtc_graph_get_connection(graph_, i, &edge); - if (!port_position(edge.source_node, 1U, sx, sy) || !port_position(edge.destination_node, 0U, dx, dy)) - continue; - double vx = dx - sx, vy = dy - sy; - double length = vx * vx + vy * vy; - double t = - length == 0.0 ? 0.0 : std::max(0.0, std::min(1.0, ((left - sx) * vx + (top - sy) * vy) / length)); - if (std::hypot(left - sx - t * vx, top - sy - t * vy) <= 6.0) { - select_connection(edge.source_node, edge.destination_node); - return 1; - } - } - selected_node_ = SIZE_MAX; - edge_selected_ = false; - redraw(); - return 1; - } - case UI_DRAG: - mouse_x_ = left; - mouse_y_ = top; - if (dragging_) - move_node(selected_node_, left - drag_x_, top - drag_y_); - if (wiring_) - redraw(); - return 1; - case UI_RELEASE: - dragging_ = false; - if (wiring_) { - size_t node, port; - wiring_ = false; - if (hit_port(left, top, node, port)) - connect_ports({wire_node_, wire_port_, node, port}); - else - feedback_->copy_label("Connection cancelled. Drag an output port to an input port."); - redraw(); - } - return 1; - case UI_KEYDOWN: - if (Ui::event_key() == 'l' && !Ui::event_state(UI_CTRL | UI_ALT)) { auto_layout(); return 1; } - if (Ui::event_key() == 'i' && !Ui::event_state(UI_CTRL | UI_ALT)) { insert_gain(); return 1; } - if ((Ui::event_key() == UI_Left || Ui::event_key() == UI_Right) && count_) { - size_t selected = 0; - for (size_t i = 0; i < count_; ++i) if (positions_[i].id == selected_node_) { - selected = Ui::event_key() == UI_Left ? (i+count_-1)%count_ : (i+1)%count_; break; - } - select_node(positions_[selected].id); - return 1; - } - if (Ui::event_key() == UI_Delete || Ui::event_key() == UI_BackSpace) { - delete_selected(); - return 1; - } - if (Ui::event_key() == UI_Escape) { - wiring_ = dragging_ = false; - redraw(); - return 1; - } - break; - default: - break; - } - return UiWidget::handle(event); -} - -bool GraphWidget::routing(size_t id, xtc_routing_info &info) const -{ - for (size_t i = 0; i < routing_count_; ++i) if (routing_[i].id == id) { info = routing_[i]; return true; } - return false; -} - -void GraphWidget::grow_canvas() -{ - int width = 2000, height = 4000; - for (size_t i = 0; i < count_; ++i) { - width = std::max(width, positions_[i].left + node_width + 40); - height = std::max(height, positions_[i].top + node_height + 40); - } - size(width, height); - if (parent()) parent()->redraw(); -} - -bool GraphWidget::auto_layout() -{ - if (routing_count_ != count_) return false; - std::array rows{}; - for (size_t i = 0; i < count_; ++i) { - xtc_routing_info info{}; - if (!routing(positions_[i].id, info) || info.level >= rows.size()) return false; - positions_[i].left = 40 + static_cast(info.level) * 210; - positions_[i].top = 40 + static_cast(rows[info.level]++) * 115; - } - wiring_ = dragging_ = false; - grow_canvas(); - feedback_->copy_label("Arranged left to right by signal flow. Layout is session-only; no audio or routing changes."); - redraw(); - return true; -} - -xtc_result GraphWidget::insert_gain() -{ - if (!edge_selected_) { - feedback_->copy_label("Select a wire first, then insert a unity gain/bus node."); - return XTC_ERR_INVALID_STATE; - } - xtc_graph_edit edit{}; - edit.kind = XTC_EDIT_INSERT_GAIN; - edit.data.connection = selected_edge_; - size_t added = SIZE_MAX; - xtc_result result = handler_(context_, edit, &added); - if (result == XTC_OK) select_node(added); - feedback(result, "Unity gain inserted into wire as one edit. Inputs sum; fan-out copies the signal without normalization."); - return result; -} - -void GraphWidget::describe_selection() -{ - char text[320]; - if (edge_selected_) { - std::snprintf(text, sizeof(text), "Wire: #%zu audio out -> #%zu sum in. Delete disconnects; I inserts a unity gain. Destination sums its inputs.", - selected_edge_.source_node, selected_edge_.destination_node); - } else { - xtc_routing_info info{}; - if (!routing(selected_node_, info)) { feedback_->copy_label("Select a node or wire to inspect routing."); return; } - std::snprintf(text, sizeof(text), "Node #%zu: %zu incoming, %zu outgoing | %s | %s. Ports are audio lanes; stereo uses paired independent processing.", - info.id, info.inputs, info.outputs, - info.reaches_output ? "path to engine output" : "NO output path", - info.external_input ? "unwired input uses external audio" : "wired sum or source"); - } - feedback_->copy_label(text); -} diff --git a/src/gui/JUCE/graph_widget.hpp b/src/gui/JUCE/graph_widget.hpp deleted file mode 100644 index aefa6a8..0000000 --- a/src/gui/JUCE/graph_widget.hpp +++ /dev/null @@ -1,212 +0,0 @@ -#ifndef XTC_GRAPH_WIDGET_HPP -#define XTC_GRAPH_WIDGET_HPP -#include "ui.hpp" -#include -#include "xtc/xtc_engine.h" - -class UiBox; - -/* GUI positions/selection only. Domain snapshots and edits stay in the C API. */ -class GraphWidget : public UiWidget { - public: - /** - * @brief Applies a graph edit using an opaque context and optional added-node output. - * - * The handler receives the context, edit command, and output identifier in that order. - * @return XTC_OK on success, or an edit error. - */ - using EditHandler = xtc_result (*)(void *, const xtc_graph_edit &, size_t *); - /** - * @brief Constructs a graph widget bound to a graph and an edit callback. - * - * The widget stores the graph, handler, context, and feedback box without - * taking ownership of any of them. - * - * @param x Left coordinate in JUCE screen space. - * @param y Top coordinate in JUCE screen space. - * @param width Widget width in pixels. - * @param height Widget height in pixels. - * @param graph Borrowed graph snapshot; must outlive the widget's use of it. - * @param handler Edit callback invoked for every graph mutation. - * @param context Opaque pointer passed back to @p handler; may be NULL. - * @param feedback Borrowed label used to report edit results; must outlive the widget. - */ - GraphWidget(int x, int y, int width, int height, const xtc_graph *graph, EditHandler handler, void *context, - UiBox *feedback); - - /** - * @brief Clears cached node positions, selection, and interaction state. - * - * Does not change the borrowed graph pointer or the graph's own layout. - */ - void reset_layout() - { - count_ = 0; - selected_node_ = SIZE_MAX; - edge_selected_ = dragging_ = wiring_ = false; - } - - /** - * @brief Replaces the graph snapshot and rebuilds cached node positions. - * - * Existing positions are preserved by node ID; new nodes are placed on a grid - * that avoids overlapping previously known nodes. Selections that no longer - * exist are cleared. The widget does not take ownership of @p graph. - * - * @param graph Borrowed graph that must outlive the widget's use of it. - */ - void set_graph(const xtc_graph *graph); - - /** - * @brief Returns the graph snapshot currently displayed. - * - * The returned pointer is borrowed and valid for GUI thread use only. - * - * @return Borrowed graph pointer; may be NULL when none was supplied. - */ - size_t selected_node() const { return selected_node_; } - - const xtc_graph *graph() const - { - return graph_; - } // Borrowed, GUI thread only. - - /** - * @brief Requests creation of a node through the edit handler. - * - * Selects the new node when the edit succeeds. - * - * @param type Kind of node to add. - * @return Result reported by the edit handler. - */ - xtc_result add_node(xtc_node_type type); - xtc_result insert_gain(); - bool selected_connection(xtc_connection_info &out) const { if (!edge_selected_) return false; out = selected_edge_; return true; } - bool auto_layout(); // Presentation only, no engine publication or undo step. - bool routing(size_t id, xtc_routing_info &info) const; - - - /** - * @brief Requests a connection between two ports through the edit handler. - * - * @param connection Connection describing the source and destination node/port pairs. - * @return Result reported by the edit handler. - */ - xtc_result connect_ports(const xtc_connection_info &connection); - - /** - * @brief Requests removal of the selected wire or node through the edit handler. - * - * @return Result reported by the edit handler, or XTC_ERR_INVALID_STATE when - * nothing is selected. - */ - xtc_result delete_selected(); - - /** - * @brief Selects a node by ID and clears any wire selection. - * - * @param id Stable node identifier. - * @return True when the node has a cached position, false otherwise. - */ - bool select_node(size_t id); - bool select_at(int left, int top); // Canvas-local coordinates; selection only. - - /** - * @brief Selects a wire between two nodes and clears the node selection. - * - * @param source Source node identifier. - * @param destination Destination node identifier. - * @return True when a matching connection exists, false otherwise. - */ - bool select_connection(size_t source, size_t destination); - - /** - * @brief Moves a node within the widget, clamped to the visible bounds. - * - * @param id Stable node identifier. - * @param left Requested left position in widget-local coordinates. - * @param top Requested top position in widget-local coordinates. - * @return True when the node has a cached position, false otherwise. - */ - bool move_node(size_t id, int left, int top); - - /** - * @brief Looks up the cached position of a node. - * - * @param id Stable node identifier. - * @param left Receives the left position in widget-local coordinates. - * @param top Receives the top position in widget-local coordinates. - * @return True when the node has a cached position, false otherwise. - */ - bool node_position(size_t id, int &left, int &top) const; - - /** - * @brief Handles mouse and keyboard events for selection, dragging, and wiring. - * - * @param event JUCE event code. - * @return 1 when the event was consumed, 0 for unsupported left-button input, - * or the result of UiWidget::handle() for unhandled events. - */ - int handle(int event) override; - - protected: - /** - * @brief Draws the background, wires, nodes, ports, and any pending wire. - */ - void draw() override; - - private: - struct Position { - size_t id = SIZE_MAX; - int left = 0, top = 0; - }; - - std::array positions_{}; - size_t count_ = 0U, routing_count_ = 0U; - std::array routing_{}; - void describe_selection(); - void grow_canvas(); - const xtc_graph *graph_; - EditHandler handler_; - void *context_; - UiBox *feedback_; - size_t selected_node_ = SIZE_MAX; - xtc_connection_info selected_edge_{}; - bool edge_selected_ = false; - bool dragging_ = false, wiring_ = false; - size_t wire_node_ = SIZE_MAX, wire_port_ = 0U; - int drag_x_ = 0, drag_y_ = 0, mouse_x_ = 0, mouse_y_ = 0; - /** - * @brief Shows the outcome of an edit in the feedback label. - * - * @param result Result returned by the edit handler. - * @param success Text shown when @p result is XTC_OK; must be valid then and is ignored otherwise. - */ - void feedback(xtc_result result, const char *success); - - /** - * @brief Finds the port nearest to a widget-local point. - * - * Searches nodes in reverse draw order and accepts points within 9 pixels of a - * port center. - * - * @param left Widget-local x coordinate to test. - * @param top Widget-local y coordinate to test. - * @param node Receives the matching node ID when true is returned. - * @param port Receives the matching port index (0 input, 1 output) when true is returned. - * @return True when a port is within range, false otherwise. - */ - bool hit_port(int left, int top, size_t &node, size_t &port) const; - - /** - * @brief Computes the widget-local center of a node port. - * - * @param id Stable node identifier. - * @param port Port index; 0 selects the input and 1 the output. - * @param left Receives the port center x coordinate. - * @param top Receives the port center y coordinate. - * @return True when the node and port exist, false otherwise. - */ - bool port_position(size_t id, size_t port, int &left, int &top) const; -}; -#endif diff --git a/src/gui/JUCE/insert_meter.hpp b/src/gui/JUCE/insert_meter.hpp deleted file mode 100644 index 6148376..0000000 --- a/src/gui/JUCE/insert_meter.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef XTC_INSERT_METER_HPP -#define XTC_INSERT_METER_HPP -#include "xtc/xtc_backend.h" -#include "xtc/xtc_mixer.h" -#include "theme.hpp" -#include -#include -#include -/* UI-only display fed by the existing single backend snapshot reader. */ -class InsertMeter : public UiButton { - xtc_peak_meter left_{-90,0},right_{-90,0}; - uint64_t track_=UINT64_MAX; - public: - InsertMeter(int x,int y,int w,int h):UiButton(x,y,w,h) { - tooltip("Post-track-fader, pre-bus stereo sample peaks. Click to clear the sticky clip indicator."); - callback([](UiWidget *w,void *){static_cast(w)->clear_clips();}); - } - void clear_clips(){left_.clipped=right_.clipped=0;redraw();} - float left_db() const {return left_.db;} - float right_db() const {return right_.db;} - bool clipped() const {return left_.clipped || right_.clipped;} - void feed(const xtc_meter_snapshot &snapshot,uint64_t track,bool active,double seconds) { - if(track_!=track){track_=track;xtc_peak_meter_reset(&left_);xtc_peak_meter_reset(&right_);} - bool found=false;float l=0,r=0; - for(size_t i=0;i=0 ? l : 0,seconds); - (void)xtc_peak_meter_update(&right_,std::isfinite(r) && r>=0 ? r : 0,seconds); - } - redraw(); - } - void draw() override { - draw_box();char text[120];std::snprintf(text,sizeof(text),"Post-fader L %.1f R %.1f dBFS%s",static_cast(left_.db),static_cast(right_.db),clipped() ? " CLIP" : ""); - ui_font(UI_HELVETICA,11);ui_color(clipped() ? ui_rgb_color(245,132,142) : xtc_ui::muted());ui_draw(text,x()+8,y()+12); - int width=std::max(0,(w()-24)/2); - for(unsigned int side=0;side<2;++side) { - int left=x()+8+static_cast(side)*(width+8);float db=side ? right_.db : left_.db; - ui_color(xtc_ui::background());ui_rectf(left,y()+h()-7,width,4); - ui_color(clipped() ? ui_rgb_color(245,132,142) : xtc_ui::accent()); - ui_rectf(left,y()+h()-7,static_cast(static_cast(width)*std::min(1.f,std::max(0.f,(db+90)/90))),4); - } - } -}; -#endif diff --git a/src/gui/JUCE/main.cpp b/src/gui/JUCE/main.cpp deleted file mode 100644 index 695b2f7..0000000 --- a/src/gui/JUCE/main.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "ui.hpp" -#include -#include -#include -#include "main_window.hpp" -#include "xtc/xtc_version.h" -#include "xtc/xtc.h" -#include - -/** - * @brief Runs the JUCE application and reports command-line or startup failures. - * - * @param argc Number of command-line arguments including the program name. - * @param argv Command-line argument vector. - * @return EXIT_SUCCESS after the event loop exits, or EXIT_FAILURE for invalid usage or startup failure. - */ -int main(int argc, char **argv) -{ - if (argc == 2 && !std::strcmp(argv[1], "--version")) { - std::printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING); - return EXIT_SUCCESS; - } - if (argc == 2 && !std::strcmp(argv[1], "--diagnose")) { - unsigned int features = xtc_build_features(); - std::printf("xtc %s; project formats 1..%u; JACK %s; LADSPA %s; CLAP/VST2/VST3 mono effects/instruments compiled\nFor limits and recovery commands run xtc-check --diagnose/--help.\n", - XTC_VERSION_STRING,XTC_PROJECT_FORMAT_VERSION,features & XTC_FEATURE_JACK ? "compiled" : "disabled", - features & XTC_FEATURE_LADSPA ? "compiled" : "disabled"); - return EXIT_SUCCESS; - } - if (argc == 2 && !std::strcmp(argv[1], "--help")) { - std::puts( - "Usage: xtc-juce [PROJECT]\n--version, --diagnose and --help work without a display or audio server."); - return EXIT_SUCCESS; - } - const char *project_path = nullptr; - if (argc == 2 && argv[1][0] != '-') { - project_path = argv[1]; - argc = 1; - argv[1] = nullptr; - } - try { - UiInitialiser initialise; - MainWindow window; - if (project_path) { - xtc_result result = window.open_project(project_path); - if (result != XTC_OK) { - std::fprintf(stderr, "Cannot open project: %s\n", xtc_result_string(result)); - return EXIT_FAILURE; - } - } - window.show(argc, argv); - return Ui::run(); - } catch (const std::exception &error) { - std::fprintf(stderr, "GUI startup/runtime failure: %s\n", error.what()); - } catch (...) { - std::fputs("Unexpected GUI startup/runtime failure.\n", stderr); - } - return EXIT_FAILURE; -} diff --git a/src/gui/JUCE/main_window.cpp b/src/gui/JUCE/main_window.cpp deleted file mode 100644 index 5e7f9e6..0000000 --- a/src/gui/JUCE/main_window.cpp +++ /dev/null @@ -1,2856 +0,0 @@ -#include "main_window.hpp" -#include "xtc/xtc_midi_file.h" -#include "graph_widget.hpp" -#include "sequencer_widget.hpp" -#include "piano_roll.hpp" -#include "mixer_widget.hpp" -#include "plugin_dialog.hpp" -#include "sample_editor.hpp" -#include "theme.hpp" -#include "snap.hpp" -#include "velocity_lane.hpp" -#include "preferences_dialog.hpp" -#include "command_palette.hpp" -#include "ui.hpp" -#include "plugin_library.hpp" -#include "track_inserts.hpp" -#include "xtc/xtc_version.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { -class PanelWindow : public UiDouble_Window { - MainWindow *owner_; - public: - PanelWindow(int width, int height, const char *title, MainWindow *owner) - : UiDouble_Window(width,height,title), owner_(owner) {} - void resize(int x,int y,int width,int height) override { - UiDouble_Window::resize(x,y,width,height); - if(owner_ && owner_->transport_window()==this)owner_->layout_transport(); - } - int handle(int event) override { - if (event == UI_SHORTCUT && owner_->panel_shortcut(event)) return 1; - return UiDouble_Window::handle(event); - } -}; -void restore_panel(UiWindow *window,int x,int y,int width,int height,int min_width,int min_height) -{ - int sx,sy,sw,sh;Ui::screen_work_area(sx,sy,sw,sh,Ui::screen_num(x,y,width,height)); - width=std::min(std::max(width,min_width),sw);height=std::min(std::max(height,min_height),sh); - x=std::clamp(x,sx,sx+sw-width);y=std::clamp(y,sy,sy+sh-height); - window->resize(x,y,width,height); -} -bool editing_number(UiValue_Input *input) -{ - return Ui::focus() == input || Ui::focus() == &input->input || Ui::pushed() == input || - Ui::pushed() == &input->input; -} -/** - * @brief Updates a box label only when the text differs. - * - * Avoids redundant relayout and redraw when status text is refreshed - * repeatedly. The box takes a copy of the text. - * - * @param box Box whose label is updated; must not be NULL. - * @param text New label text; must not be NULL. - */ -void set_label(UiBox *box, const char *text) -{ - if (box->label() == nullptr || std::strcmp(box->label(), text) != 0) - box->copy_label(text); -} -/* Paint progress without dispatching input/reentering edit commands. */ -class BusyOperation { - UiWindow &window_; - public: - BusyOperation(UiWindow &window, UiBox *status, const char *message) : window_(window) { - set_label(status,message); window_.cursor(UI_CURSOR_WAIT); Ui::flush(); - } - ~BusyOperation() { window_.cursor(UI_CURSOR_DEFAULT); } -}; -} // namespace - -/** - * @brief Constructs the window, its widgets, and the initial project. - * - * Creates an empty project and undo history, opens the audio backend, and - * schedules the periodic status timer. Throws std::runtime_error when the - * initial project or history cannot be created. - * - * @param prompt_unsaved Whether open, new, and close actions ask before - * discarding unsaved changes. - */ -MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) - : UiDouble_Window(1280, 800, XTC_PROJECT_NAME " — modular audio workstation"), prompt_unsaved_(prompt_unsaved) -{ - xtc_ui::initialize(); - xtc_settings_defaults(&settings_); - char config_path[4096]; - if(configuration) configuration_=configuration; - else if(xtc_settings_path(config_path,sizeof(config_path))==XTC_OK) configuration_=config_path; - xtc_result config_result=configuration_.empty() ? XTC_ERR_NOT_FOUND : xtc_settings_load(&settings_,configuration_.c_str()); - - configuration_valid_=config_result==XTC_OK || config_result==XTC_ERR_NOT_FOUND; - xtc_project *initial = nullptr; - xtc_result result = xtc_project_create(&initial); - if (result != XTC_OK) - throw std::runtime_error(xtc_result_string(result)); - project_defaults(initial); - project_.reset(initial); - xtc_history *history = nullptr; - result = xtc_history_create(initial, &history); - if (result != XTC_OK) - throw std::runtime_error(xtc_result_string(result)); - history_.reset(history); - begin(); - auto *menu = new UiMenu_Bar(0, 0, 1280, 28); - menu_ = menu; - menu->add("File/New", UI_CTRL + 'n', file_callback, this); - menu->add("File/Open", UI_CTRL + 'o', file_callback, this); - menu->add("File/Save", UI_CTRL + 's', file_callback, this); - menu->add("File/Import MIDI...",0,[](UiWidget *,void *c){static_cast(c)->import_midi_dialog();},this); - menu->add("File/Export selected clip as MIDI...",0,[](UiWidget *,void *c){static_cast(c)->export_midi_dialog();},this); - menu->add("File/Export WAV",0,[](UiWidget *,void *context){static_cast(context)->export_audio();},this); - menu->add("File/Save As", UI_CTRL + UI_SHIFT + 's', file_callback, this); - menu->add("File/Import WAV", UI_CTRL + 'i', audio_file_callback, this); - menu->add("File/Relink selected audio", 0, audio_file_callback, this); - menu->add("Edit/Preferences",UI_CTRL+',',[](UiWidget *,void *c){static_cast(c)->show_preferences();},this); - menu->add("Edit/Undo", UI_CTRL + 'z', history_callback, this); - menu->add("Edit/Redo", UI_CTRL + UI_SHIFT + 'z', history_callback, this); - menu->add("Edit/Redo alternate", UI_CTRL + 'y', history_callback, this); - menu->add("Audio/MIDI controller mapping", 0, control_map_callback, this); - menu->add("Audio/Panic", UI_F + 9, panic_callback, this); - menu->add("Edit/Mixer comparison/Capture A",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->mix_snapshot(0,true);if(r!=XTC_OK)w->report_error("Mixer capture failed",r);},this); - menu->add("Edit/Mixer comparison/Recall A",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->mix_snapshot(0,false);if(r!=XTC_OK)w->report_error("Mixer recall failed",r);},this); - menu->add("Edit/Mixer comparison/Capture B",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->mix_snapshot(1,true);if(r!=XTC_OK)w->report_error("Mixer capture failed",r);},this); - menu->add("Edit/Mixer comparison/Recall B",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->mix_snapshot(1,false);if(r!=XTC_OK)w->report_error("Mixer recall failed",r);},this); - menu->add("View/Previous marker",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->navigate_marker(-1);if(r!=XTC_OK)w->report_error("Marker navigation failed",r);},this); - menu->add("View/Next marker",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->navigate_marker(1);if(r!=XTC_OK)w->report_error("Marker navigation failed",r);},this); - menu->add("View/Loop current section",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->loop_section();if(r!=XTC_OK)w->report_error("Section loop unchanged",r);},this); - menu->add("File/Collect project media...",0,[](UiWidget *,void *c){auto *w=static_cast(c);const char *path=ui_input("New absolute project folder (must not exist)","");if(path){auto r=w->collect_project(path);if(r!=XTC_OK)w->report_error("Project collection failed",r);}},this); - menu->add("View/Commands",UI_CTRL+'p',[](UiWidget *,void *c){static_cast(c)->show_commands();},this); - menu->add("View/Fit song",UI_CTRL+'0',[](UiWidget *,void *c){static_cast(c)->fit_song();},this); - menu->add("View/Piano roll", UI_CTRL + '4', piano_callback, this); - menu->add("Audio/Record WAV", UI_SHIFT + UI_F + 5, audio_file_callback, this); - menu->add("Track/Load instrument plugin", 0, instrument_callback, this); - menu->add("Track/Use built-in instrument", 0, instrument_callback, this); - menu->add("Track/Live MIDI to selected instrument", 0, instrument_callback, this); - menu->add("Track/Live MIDI to built-in sine", 0, instrument_callback, this); - menu->add("Edit/Selected instrument controls",0,instrument_callback,this); - menu->add("Edit/Selected instrument automation",0,instrument_callback,this); - menu->add("File/Load sampler WAV",0,instrument_callback,this); - menu->add("File/Save instrument preset",0,instrument_callback,this); - menu->add("File/Load instrument preset",0,instrument_callback,this); - menu->add("Edit/Insert internal delay on wire", 0, plugin_callback, this); - menu->add("Edit/Insert internal reverb on wire", 0, plugin_callback, this); - menu->add("Edit/Insert stereo delay on wire",0,plugin_callback,this); - menu->add("Edit/Insert stereo reverb on wire",0,plugin_callback,this); - menu->add("File/Add stereo delay",0,plugin_callback,this); - menu->add("File/Add stereo reverb",0,plugin_callback,this); - menu->add("File/Add internal delay", 0, plugin_callback, this); - menu->add("File/Add internal reverb", 0, plugin_callback, this); - menu->add("File/Load plugin", 0, plugin_callback, this); - menu->add("Edit/Replace selected plugin", 0, plugin_callback, this); - menu->add("Edit/Insert plugin on selected wire", 0, plugin_callback, this); - menu->add("Edit/Reset selected plugin", 0, plugin_callback, this); - menu->add("Edit/Selected plugin automation", 0, plugin_callback, this); - menu->add("Edit/Selected plugin control", 0, plugin_callback, this); - menu->add("File/Quit", UI_CTRL + 'q', close_callback, this); - - menu->add("Audio/Start engine", UI_F + 5, play_callback, this); - menu->add("Audio/Stop engine", UI_F + 8, stop_callback, this); - menu->add("Audio/Reconnect", UI_F + 6, reconnect_callback, this); - - menu->add("Audio/Pause", UI_F + 7, [](UiWidget *, void *c) { - (void)static_cast(c)->pause_playback(); - }, this); - menu->add("Audio/Return to start", UI_Home, [](UiWidget *, void *c) { - (void)static_cast(c)->return_to_start(); - }, this); - - menu->add("View/Detach transport",0,[](UiWidget *,void *c) { - auto *w=static_cast(c); w->detach_transport(!w->transport_detached_); - },this,UI_MENU_TOGGLE); - menu->add("View/Detach mixer",0,[](UiWidget *,void *c) { - auto *w=static_cast(c); w->detach_mixer(!w->mixer_detached_); - },this,UI_MENU_TOGGLE); - menu->add("View/Dock all panels",0,[](UiWidget *,void *c) { - auto *w=static_cast(c); w->detach_transport(false); w->detach_mixer(false);w->detach_editor(0,false);w->detach_editor(1,false);w->detach_piano(false); - },this); - menu->add("View/Detach sequencer",0,[](UiWidget *,void *c){auto *w=static_cast(c);w->detach_editor(0,!w->editors_[0].detached);},this,UI_MENU_TOGGLE); - menu->add("View/Detach modular graph",0,[](UiWidget *,void *c){auto *w=static_cast(c);w->detach_editor(1,!w->editors_[1].detached);},this,UI_MENU_TOGGLE); - menu->add("View/Float piano roll",0,[](UiWidget *,void *c){auto *w=static_cast(c);w->detach_piano(!w->piano_detached_);},this,UI_MENU_TOGGLE|UI_MENU_VALUE); - menu->add("View/Reset workspace layout",0,[](UiWidget *,void *c){static_cast(c)->reset_layout();},this); - transport_group_=new UiGroup(0,28,1280,90); - transport_group_->begin(); - play_ = new UiButton(12, 40, 80, 30, "Play"); - play_->callback(play_callback, this); - play_->tooltip("Start the audio engine (F5 or Space). Play audio clips and the modular graph."); - stop_ = new UiButton(100, 40, 80, 30, "Stop"); - stop_->callback(stop_callback, this); - stop_->tooltip("Stop playback at its current position; finalize recording (F8 or Space)."); - auto *reconnect = new UiButton(188, 40, 110, 30, "Reconnect"); - reconnect->callback(reconnect_callback, this); - reconnect->tooltip("Close and reopen JACK, leaving the engine stopped (F6)."); - record_ = new UiButton(306, 40, 90, 30, "Record"); - record_->callback(audio_file_callback, this); - record_->tooltip("Record JACK input to WAV; choose path, then Stop to finalize and add the take."); - auto *panic = new UiButton(404, 40, 70, 30, "Panic"); - panic->callback(panic_callback, this); - panic->tooltip("Silence held instrument notes (F9)."); - pause_ = new UiButton(482, 40, 70, 30, "Pause"); - pause_->callback([](UiWidget *, void *c) { - (void)static_cast(c)->pause_playback(); - }, this); - pause_->tooltip("Pause at the current sample position (F7). Play resumes; recording uses Stop."); - home_ = new UiButton(560, 40, 100, 30, "To start"); - home_->callback([](UiWidget *, void *c) { - (void)static_cast(c)->return_to_start(); - }, this); - home_->tooltip("Stop and return to the project beginning (Home). Unavailable while recording."); - indicator_ = new UiBox(1000, 40, 268, 30, "Audio: disconnected"); - indicator_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - indicator_->labelfont(UI_BOLD); - indicator_->labelsize(12); - transport_position_ = new UiBox(680,40,190,30,"001 : 1 : 000"); - transport_position_->tooltip("Musical position: bar : beat : tick (4/4, 960 ticks per beat)"); - tempo_display_ = new UiValue_Input(880, 40, 78, 30, "BPM"); - tempo_display_->align(UI_ALIGN_RIGHT); - tempo_display_->bounds(20, 300); - tempo_display_->step(.1); - tempo_display_->when(UI_WHEN_RELEASE | UI_WHEN_ENTER_KEY); - tempo_display_->tooltip("Project tempo in BPM. Enter or release to apply; undoable. Stops playback."); - tempo_display_->callback( - [](UiWidget *, void *c) { - auto *w = static_cast(c); - double bpm = w->tempo_display_->value(); - xtc_result r = (!std::isfinite(bpm) || bpm < 20 || bpm > 300) - ? XTC_ERR_INVALID_ARGUMENT - : w->set_tempo(static_cast(std::lround(bpm * 1000))); - if (r != XTC_OK) - w->report_error("Tempo unchanged", r); - w->tempo_display_->value(xtc_project_tempo(w->project_.get()) / 1000.0); - w->refresh_status(); - }, - this); - auto *hint = new UiBox(12, 82, 160, 28, "xtc / SESSION"); - hint->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - session_info_ = new UiBox(180, 82, 280, 28); - session_info_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - auto *commands = new UiButton(480, 82, 120, 28, "Commands"); - commands->tooltip("Search actions (Ctrl+P)"); - commands->callback([](UiWidget *, void *c) { static_cast(c)->show_commands(); }, this); - follow_ = new UiCheck_Button(620, 82, 154, 28, "Follow playhead"); - follow_->tooltip("Keep playback visible in the arrangement; pauses while dragging."); - follow_->callback( - [](UiWidget *widget, void *c) { - static_cast(c)->settings_.follow_playhead = static_cast(widget)->value(); - }, - this); - auto *fit = new UiButton(790, 82, 100, 28, "Fit song"); - fit->tooltip("Choose the largest supported zoom that fits the song (Ctrl+0)"); - fit->callback([](UiWidget *, void *c) { static_cast(c)->fit_song(); }, this); - auto *tap=new UiButton(905,82,80,28,"Tap"); - tap->tooltip("Tap tempo: averages up to four intervals, 20–300 BPM; pauses over three seconds restart."); - tap->callback([](UiWidget *,void *c){auto *w=static_cast(c);double now=std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count();auto r=w->tap_tempo(now);if(r!=XTC_OK && r!=XTC_ERR_NOT_FOUND && r!=XTC_ERR_INVALID_ARGUMENT)w->report_error("Tap tempo unchanged",r);},this); - loop_button_ = new UiCheck_Button(1000, 82, 100, 28, "Loop"); - loop_button_->callback([](UiWidget *widget,void *c){auto *w=static_cast(c); - xtc_result r=w->set_loop(static_cast(w->settings_.loop_start),static_cast(w->settings_.loop_end),static_cast(widget)->value()!=0); - if(r!=XTC_OK) w->report_error("Loop unchanged",r); - w->refresh_status();},this); - auto *range=new UiButton(1110,82,150,28,"Loop range..."); - range->tooltip("Shift-drag the sequencer ruler to select a loop, or enter start/end ticks here."); - range->callback([](UiWidget *,void *c){auto *w=static_cast(c);char initial[100]; - std::snprintf(initial,sizeof(initial),"%lld %lld",static_cast(w->settings_.loop_start),static_cast(w->settings_.loop_end)); - const char *text=ui_input("Loop start and end ticks (end excluded)",initial);if(!text)return; - unsigned long long start,end;char extra;xtc_result r=XTC_ERR_INVALID_ARGUMENT; - if(std::sscanf(text,"%llu %llu %c",&start,&end,&extra)==2)r=w->set_loop(start,end,w->settings_.loop_enabled!=0); - if(r!=XTC_OK)w->report_error("Loop unchanged",r);},this); - - transport_group_->end(); - for(int i=0;ichildren();++i) { - auto *item=transport_group_->child(i); - transport_items_.push_back({item,item->x(),item->y()-28,item->w(),item->h()}); - } - - tabs_ = new UiTabs(216, 126, 1056, 578); - tabs_->begin(); - auto *sequencer = new UiGroup(216, 158, 1056, 546, "Sequencer"); - sequencer->begin(); - editors_[0].owner=this;editors_[0].index=0;editors_[0].page=sequencer; - editors_[0].content=new UiGroup(216,158,1056,546); - editors_[0].content->begin(); - auto *add_track = new UiMenu_Button(228, 168, 100, 26, "Create"); - auto *add_region = new UiButton(336, 168, 110, 26, "Add region"); - auto *delete_region = new UiButton(454, 168, 130, 26, "Delete region/track"); - auto *zoom_in = new UiButton(592, 168, 80, 26, "Zoom +"); - auto *zoom_out = new UiButton(680, 168, 80, 26, "Zoom -"); - auto *add_midi = new UiButton(768, 168, 110, 26, "Add MIDI clip"); - auto *piano = new UiButton(886, 168, 100, 26, "Piano roll"); - piano->callback(piano_callback, this); - auto *timeline_hint = - new UiBox(228, 666, 1032, 28, - "Drag clips to move, edges to trim. Click the ruler while stopped to seek; Home returns to start."); - timeline_hint->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - timeline_hint->labelsize(12); - auto *timeline_scroll = new UiScroll(228, 206, 1032, 452); - timeline_scroll_=timeline_scroll; - sequencer_ = new SequencerWidget(228, 206, project_.get(), seek, this); - timeline_scroll->end(); - sequencer_->before_edit = before_arrangement_edit; - sequencer_->loop_select=[](void *c,uint64_t start,uint64_t end){auto *w=static_cast(c);return w->set_loop(start,end,w->settings_.loop_enabled!=0);}; - sequencer_->open_clip = [](void *c) {auto *w=static_cast(c);xtc_clip_info clip{}; - if(xtc_project_find_clip(w->project_.get(),w->sequencer_->selected_clip(),&clip)==XTC_OK && !clip.midi){auto r=w->open_sample_editor();if(r!=XTC_OK)w->report_error("Sample editor unavailable",r);}else piano_callback(nullptr,c);}; - sequencer_->instrument_select = [](void *c, uint64_t track, const xtc_plugin_spec *spec, bool browse) { - auto *w = static_cast(c); - w->sequencer_->select(track); - if (browse) { instrument_callback(nullptr,c); return; } - xtc_result r = w->assign_instrument(track,spec ? spec->path : nullptr,spec ? spec->index : 0); - if (r != XTC_OK) w->report_error("Track instrument unchanged",r); - }; - // Keep this in the editor group rather than the scrollable timeline. - auto *parent = UiGroup::current(); UiGroup::current(editors_[0].content); - snap_ = new UiChoice(1040,168,210,26,"Snap"); - for (size_t i = 0; i < xtc_ui::snap_count; ++i) { int item = snap_->add("Snap"); snap_->replace(item,xtc_ui::snap_labels[i]); } - snap_->value(xtc_ui::snap_index(sequencer_->grid())); - snap_->callback([](UiWidget *widget, void *c) { - auto *choice = static_cast(widget); int value = choice->value(); - if (value >= 0 && static_cast(value) < xtc_ui::snap_count) - (void)static_cast(c)->sequencer_->grid(xtc_ui::snap_ticks[value]); - },this); - UiGroup::current(parent); - menu->add("Edit/Nudge selection left",0,[](UiWidget *,void *c){static_cast(c)->nudge_selection(-1);},sequencer_); - menu->add("Edit/Nudge selection right",0,[](UiWidget *,void *c){static_cast(c)->nudge_selection(1);},sequencer_); - menu->add("View/Sample editor",0,[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->open_sample_editor();if(r!=XTC_OK)w->report_error("Sample editor unavailable",r);},this); - menu->add("Edit/Audio tools/Normalize source peak...",0,[](UiWidget *,void *c){static_cast(c)->normalize_dialog();},sequencer_); - menu->add("Edit/Audio tools/Set boundary fades...",0,[](UiWidget *,void *c){static_cast(c)->fades_dialog();},sequencer_); - menu->add("Edit/Audio tools/Remove boundary fades",0,[](UiWidget *,void *c){static_cast(c)->transform_selection(XTC_CLIPS_FADE,0);},sequencer_); - menu->add("Edit/MIDI tools/Generate arpeggio...",0,[](UiWidget *,void *c){static_cast(c)->arpeggio_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Build chords...",0,[](UiWidget *,void *c){static_cast(c)->chord_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Strum chords...",0,[](UiWidget *,void *c){static_cast(c)->strum_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Humanize...",0,[](UiWidget *,void *c){static_cast(c)->humanize_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Shape velocities...",0,[](UiWidget *,void *c){static_cast(c)->velocity_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Correct to scale...",0,[](UiWidget *,void *c){static_cast(c)->scale_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Note gate...",0,[](UiWidget *,void *c){static_cast(c)->gate_dialog();},sequencer_); - menu->add("Edit/MIDI tools/Legato",0,[](UiWidget *,void *c){xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_LEGATO;static_cast(c)->transform_midi(edit);},sequencer_); - menu->add("Edit/MIDI tools/Swing quantize...",0,[](UiWidget *,void *c){static_cast(c)->swing_dialog();},sequencer_); - menu->add("Edit/Align clip starts to cursor",0,[](UiWidget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_ALIGN,static_cast(s->position()));},sequencer_); - menu->add("Edit/Slip sources earlier",0,[](UiWidget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_SLIP,-static_cast(s->grid()));},sequencer_); - menu->add("Edit/Slip sources later",0,[](UiWidget *,void *c){auto *s=static_cast(c);s->transform_selection(XTC_CLIPS_SLIP,static_cast(s->grid()));},sequencer_); - menu->add("Edit/Repeat selected phrase...",0,[](UiWidget *,void *c){static_cast(c)->repeat_dialog();},sequencer_); - menu->add("Edit/Copy clips",0,[](UiWidget *,void *c){static_cast(c)->copy_selection();},sequencer_); - menu->add("Edit/Paste clips at cursor",0,[](UiWidget *,void *c){static_cast(c)->paste_selection();},sequencer_); - menu->add("Edit/Consolidate MIDI clips",0,[](UiWidget *,void *c){static_cast(c)->merge_midi();},sequencer_); - menu->add("Edit/Duplicate complete track...",0,[](UiWidget *,void *c){static_cast(c)->duplicate_track_dialog();},sequencer_); - menu->add("Edit/Time/Insert gap...",0,[](UiWidget *,void *c){static_cast(c)->ripple_dialog(false);},sequencer_); - menu->add("Edit/Time/Remove range...",0,[](UiWidget *,void *c){static_cast(c)->ripple_dialog(true);},sequencer_); - menu->add("Edit/Markers/Set at cursor...",0,[](UiWidget *,void *c){static_cast(c)->marker_dialog();},sequencer_); - menu->add("Edit/Markers/Remove at cursor",0,[](UiWidget *,void *c){static_cast(c)->marker(nullptr);},sequencer_); - menu->add("Edit/Select all clips",0,[](UiWidget *,void *c){static_cast(c)->select_all_clips();},sequencer_); - menu->add("Edit/Duplicate clip",UI_CTRL+'d',[](UiWidget *, void *c) { (void)static_cast(c)->duplicate_selection(); },sequencer_); - menu->add("Edit/Split clip at cursor", 0, [](UiWidget *, void *c) { (void)static_cast(c)->split_selection(); }, sequencer_); - menu->add("Edit/Audio tools/Crossfade two selected clips",0,[](UiWidget *,void *c){static_cast(c)->crossfade_selection();},sequencer_); - menu->add("Edit/Audio clip gain and fades", 0, [](UiWidget *, void *c) { static_cast(c)->audio_properties_dialog(); }, sequencer_); - menu->add("Edit/Rename clip or track",UI_F+2,[](UiWidget *, void *c) { - const char *name = ui_input("Name (1..63 UTF-8 bytes)",""); if (name) (void)static_cast(c)->rename_selection(name); - },sequencer_); - for (const char *label : {"View/Snap/1 tick","View/Snap/120 ticks","View/Snap/160 ticks","View/Snap/240 ticks","View/Snap/320 ticks","View/Snap/480 ticks","View/Snap/960 ticks","View/Snap/1920 ticks","View/Snap/3840 ticks"}) - menu->add(label,0,[](UiWidget *widget,void *c) { - auto *m = static_cast(widget); - (void)static_cast(c)->grid(std::strtoull(m->mvalue()->label(),nullptr,10)); - },sequencer_); - sequencer_->callback( - [](UiWidget *, void *c) { - static_cast(c)->mark_dirty(); - }, - this); - add_midi->callback( - [](UiWidget *, void *c) { - static_cast(c)->add_midi_clip(); - }, - sequencer_); - add_region->callback( - [](UiWidget *, void *c) { - static_cast(c)->add_region(); - }, - sequencer_); - delete_region->callback( - [](UiWidget *, void *c) { - static_cast(c)->delete_selection(); - }, - sequencer_); - zoom_in->callback( - [](UiWidget *, void *c) { - static_cast(c)->zoom(1); - }, - sequencer_); - zoom_out->callback( - [](UiWidget *, void *c) { - static_cast(c)->zoom(-1); - }, - sequencer_); - editors_[0].content->resizable(timeline_scroll);editors_[0].content->end(); - sequencer->resizable(editors_[0].content); - sequencer->end(); - auto *mixer = new UiGroup(216, 158, 1056, 546, "Mixer"); - mixer_page_=mixer; - mixer->begin(); - mixer_ = new MixerWidget(228, 168, 1032, 522, project_.get(), before_arrangement_edit, this); - mixer_->callback( - [](UiWidget *, void *c) { - static_cast(c)->mark_dirty(); - }, - this); - mixer_placeholder_=new UiGroup(228,210,1032,120); - auto *detached_hint=new UiBox(240,220,600,30,"The mixer is open in a separate window."); - detached_hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *show_mixer=new UiButton(240,260,170,30,"Show mixer window"); - show_mixer->callback([](UiWidget *,void *c){static_cast(c)->mixer_window_->show();},this); - auto *dock_mixer=new UiButton(424,260,140,30,"Dock mixer"); - dock_mixer->callback([](UiWidget *,void *c){static_cast(c)->detach_mixer(false);},this); - mixer_placeholder_->end(); mixer_placeholder_->hide(); - mixer->resizable(mixer_); - mixer->end(); - auto *graph = new UiGroup(216, 158, 1056, 546, "Modular Graph"); - graph->begin(); - editors_[1].owner=this;editors_[1].index=1;editors_[1].page=graph; - editors_[1].content=new UiGroup(216,158,1056,546);editors_[1].content->begin(); - graph_state_ = new UiBox(228, 200, 956, 24, "Graph: offline"); - graph_state_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - graph_state_->labelsize(12); - auto *feedback = - new UiBox(228, 666, 1032, 28, - "Add nodes. Drag output to input to connect; click a node or wire then Delete. Scroll to pan."); - feedback->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE | UI_ALIGN_WRAP); - feedback->labelsize(12); - auto *scroll = new UiScroll(228, 230, 1032, 430); - graph_widget_ = new GraphWidget(228, 230, 2000, 4000, xtc_project_graph(project_.get()), edit_graph, this, feedback); - scroll->end(); - menu->add("View/Arrange graph by signal flow", 0, [](UiWidget *, void *c) { (void)static_cast(c)->auto_layout(); }, graph_widget_); - menu->add("Edit/Insert unity gain in selected wire", 0, [](UiWidget *, void *c) { (void)static_cast(c)->insert_gain(); }, graph_widget_); - auto *oscillator = new UiButton(228, 168, 130, 26, "Add oscillator"); - oscillator->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_OSCILLATOR); - }, - graph_widget_); - auto *gain = new UiButton(366, 168, 110, 26, "Add gain"); - gain->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_GAIN); - }, - graph_widget_); - auto *meter = new UiButton(484, 168, 110, 26, "Add meter"); - meter->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_METER); - }, - graph_widget_); - auto *output = new UiButton(602, 168, 110, 26, "Add output"); - output->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_OUTPUT); - }, - graph_widget_); - auto *audio = new UiButton(858, 168, 130, 26, "Add audio source"); - audio->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->add_node(XTC_NODE_AUDIO); - }, - graph_widget_); - auto *instrument = new UiButton(996, 168, 130, 26, "Add instrument"); - instrument->callback( - [](UiWidget *, void *c) { - (void)static_cast(c)->add_node(XTC_NODE_INSTRUMENT); - }, - graph_widget_); - auto *remove = new UiButton(720, 168, 130, 26, "Delete selection"); - remove->callback( - [](UiWidget *, void *context) { - (void)static_cast(context)->delete_selected(); - }, - graph_widget_); - editors_[1].content->resizable(scroll);editors_[1].content->end(); - graph->resizable(editors_[1].content);graph->end(); - piano_page_=new UiGroup(216,158,1056,546,"Piano roll"); - piano_placeholder_=new UiGroup(228,210,1000,120); - auto *piano_hint=new UiBox(240,220,900,30,"Select a MIDI clip, then open its piano roll. Float or dock it from View.");piano_hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *open_piano=new UiButton(240,260,190,30,"Open selected MIDI clip");open_piano->callback(piano_callback,this); - piano_placeholder_->end();piano_page_->end(); - for(auto &panel:editors_) { - panel.page->begin();panel.placeholder=new UiGroup(228,210,1000,120); - auto *hint=new UiBox(240,220,900,30,"This editor is open in a separate window.");hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *show=new UiButton(240,260,170,30,"Show editor window"); - show->callback([](UiWidget *,void *c){auto *p=static_cast(c);if(p->window)p->window->show();},&panel); - auto *dock=new UiButton(424,260,140,30,"Dock editor"); - dock->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->owner->detach_editor(p->index,false);},&panel); - panel.placeholder->end();panel.placeholder->hide();panel.page->end(); - } - track_inserts_=new TrackInsertsPanel(216,158,1056,546,this); - tabs_->end(); - tabs_->value(sequencer); - menu->add("View/Track inserts",0,view_callback,track_inserts_); - menu->add("Track/Show selected track inserts",UI_CTRL+'5',[](UiWidget *,void *c){auto *w=static_cast(c);auto r=w->open_track_inserts();if(r!=XTC_OK)w->report_error("Select an audio or MIDI track to open inserts",r);},this); - menu->add("View/Sequencer", UI_CTRL + '1', view_callback, sequencer); - menu->add("View/Mixer", UI_CTRL + '2', view_callback, mixer); - menu->add("View/Modular Graph", UI_CTRL + '3', view_callback, graph); - menu->add("Help/About", 0, about_callback); - - auto *sidebar = new UiGroup(12,126,192,578); - sidebar_=sidebar; - sidebar->box(UI_BORDER_BOX); - auto midi_create = [](UiWidget *, void *c) { auto *w=static_cast(c); - xtc_result r=w->create_track(true); if(r!=XTC_OK)w->report_error("Create MIDI track",r); }; - auto audio_create = [](UiWidget *, void *c) { auto *w=static_cast(c); - xtc_result r=w->create_track(false); if(r!=XTC_OK)w->report_error("Create audio track",r); }; - add_track->add("Empty track",0,[](UiWidget *,void *c){static_cast(c)->add_track();},sequencer_); - add_track->add("New MIDI track",0,midi_create,this); - add_track->add("New audio track",0,audio_create,this); - add_track->add("Import MIDI",0,[](UiWidget *,void *c){static_cast(c)->import_midi_dialog();},this); - add_track->add("Import WAV",0,audio_file_callback,this); - add_track->add("Load instrument plugin",0,instrument_callback,this); - add_track->add("Edit MIDI notes",0,piano_callback,this); - menu->add("Track/New MIDI track",UI_CTRL+UI_SHIFT+'m',midi_create,this); - menu->add("Track/New audio track",UI_CTRL+UI_SHIFT+'a',audio_create,this); - menu->add("View/Track inspector",0,[](UiWidget *,void *c){ - auto *w=static_cast(c);w->show_track_inspector(!w->sidebar_->visible()); - },this,UI_MENU_TOGGLE); - auto *selection_title = new UiBox(24,140,168,24,"TRACK INSPECTOR"); - selection_title->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - selection_info_ = new UiBox(24,174,168,82,"Select a track to inspect it."); - selection_info_->align(UI_ALIGN_TOP_LEFT | UI_ALIGN_INSIDE | UI_ALIGN_WRAP); - inspector_gain_ = new UiValue_Input(78, 264, 114, 26, "Gain"); - inspector_gain_->bounds(0, 4); - inspector_gain_->step(.01); - inspector_pan_ = new UiValue_Input(78, 298, 114, 26, "Pan"); - inspector_pan_->bounds(-1, 1); - inspector_pan_->step(.01); - inspector_mute_ = new UiCheck_Button(24, 332, 78, 24, "Mute"); - inspector_solo_ = new UiCheck_Button(108, 332, 84, 24, "Solo"); - for (auto *control : {static_cast(inspector_gain_), static_cast(inspector_pan_), - static_cast(inspector_mute_), static_cast(inspector_solo_)}) { - control->when(UI_WHEN_RELEASE); - control->callback(inspector_callback, this); - } - inspector_gain_->tooltip( - "Selected track linear gain: 0 silence, 1 unity, 4 maximum. Automation overrides static gain."); - inspector_pan_->tooltip("Selected track pan: -1 left, 0 center, +1 right."); - auto *tip = new UiBox(24, 368, 168, 52, "Ctrl+P Commands\nCtrl+1/2/3 Views\nCtrl+0 Fit song"); - tip->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - sidebar->end(); - status_ = new UiBox(12,714,1256,26,"JACK: not connected"); - status_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE); - error_ = new UiBox(12,744,1256,44,""); - error_->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE | UI_ALIGN_WRAP); - xtc_ui::style(this); - color(xtc_ui::background()); - selection_title->labelcolor(xtc_ui::muted()); - tip->labelcolor(xtc_ui::muted()); hint->labelcolor(xtc_ui::muted()); - transport_position_->box(xtc_ui::flat); transport_position_->color(xtc_ui::background()); - transport_position_->labelfont(UI_COURIER_BOLD); transport_position_->labelsize(18); - tempo_display_->labelcolor(xtc_ui::muted()); - play_->labelcolor(xtc_ui::accent()); record_->labelcolor(ui_rgb_color(245,132,142)); - error_->labelcolor(ui_rgb_color(255,166,139)); - end(); - resizable(tabs_); - size_range(1220, 720); - callback(close_callback, this); - apply_ui_preferences(); - show_track_inspector(settings_.inspector_visible!=0); - open_backend(); - if(config_result!=XTC_OK && config_result!=XTC_ERR_NOT_FOUND) report_error("Preferences file invalid; using defaults",config_result); - Ui::add_timeout(static_cast(settings_.refresh_ms)/1000,timer_callback,this); -} - -void MainWindow::detach_editor(unsigned int index,bool detached) -{ - if(index>=2)return; - auto &panel=editors_[index];if(!panel.content || panel.detached==detached)return; - auto *scroll=index==0 ? timeline_scroll_ : dynamic_cast(graph_widget_->parent()); - int scroll_x=scroll ? scroll->xposition() : 0,scroll_y=scroll ? scroll->yposition() : 0; - if(detached && !panel.window) { - auto *current=UiGroup::current();UiGroup::current(nullptr); - panel.window.reset(new PanelWindow(1100,650,index==0 ? "xtc — Sequencer (close to dock)" : "xtc — Modular Graph (close to dock)",this)); - auto *dock=new UiButton(12,6,150,28,"Dock editor");dock->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->owner->detach_editor(p->index,false);},&panel); - panel.window->end();panel.window->size_range(index==0 ? 1100 : 1000,420);xtc_ui::style(panel.window.get()); - panel.window->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->owner->detach_editor(p->index,false);},&panel); - restore_panel(panel.window.get(),static_cast(index==0 ? settings_.sequencer_x : settings_.graph_x),static_cast(index==0 ? settings_.sequencer_y : settings_.graph_y),static_cast(index==0 ? settings_.sequencer_width : settings_.graph_width),static_cast(index==0 ? settings_.sequencer_height : settings_.graph_height),index==0 ? 1100 : 1000,420); - UiGroup::current(current); - } - panel.content->parent()->remove(panel.content);panel.detached=detached; - if(detached) { - panel.page->resizable(nullptr);panel.window->add(panel.content); - panel.content->resize(12,44,panel.window->w()-24,panel.window->h()-56);panel.window->resizable(panel.content);panel.placeholder->show(); - } else { - panel.window->resizable(nullptr);panel.page->add(panel.content); - panel.content->resize(panel.page->x(),panel.page->y(),panel.page->w(),panel.page->h()); - panel.page->resizable(panel.content);panel.placeholder->hide();panel.window->hide(); - } - if(scroll)scroll->scroll_to(scroll_x,scroll_y); - panel.content->show();panel.page->init_sizes(); - if(index==0)settings_.sequencer_detached=detached;else settings_.graph_detached=detached; - auto *item=const_cast(menu_->find_item(index==0 ? "View/Detach sequencer" : "View/Detach modular graph"));if(detached)item->set();else item->clear(); - if(detached)panel.window->show(); - redraw(); -} -void MainWindow::detach_piano(bool detached) -{ - settings_.piano_detached=detached; - auto *item=const_cast(menu_->find_item("View/Float piano roll"));if(detached)item->set();else item->clear(); - if(!piano_content_) {piano_detached_=detached;return;} - if(piano_content_->window()==piano_window_.get() && detached) {piano_detached_=true;return;} - if(piano_content_->parent()==piano_page_ && !detached) {piano_detached_=false;return;} - auto *scroll=dynamic_cast(piano_->parent());int sx=scroll->xposition(),sy=scroll->yposition(); - piano_content_->parent()->remove(piano_content_);piano_detached_=detached; - if(detached) { - piano_page_->resizable(nullptr);piano_placeholder_->show();piano_window_->add(piano_content_); - piano_content_->resize(0,0,piano_window_->w(),piano_window_->h());piano_window_->resizable(piano_content_);piano_window_->show(); - } else { - piano_window_->resizable(nullptr);piano_placeholder_->hide();piano_page_->add(piano_content_); - piano_content_->resize(piano_page_->x(),piano_page_->y(),piano_page_->w(),piano_page_->h()); - piano_page_->resizable(piano_content_);piano_window_->hide();tabs_->value(piano_page_); - } - scroll->scroll_to(sx,sy);piano_content_->show();piano_page_->init_sizes(); - redraw(); -} -void MainWindow::reset_layout() -{ - detach_transport(false);detach_mixer(false);detach_editor(0,false);detach_editor(1,false);detach_piano(false); - settings_.transport_x=settings_.transport_y=settings_.sequencer_x=settings_.sequencer_y=settings_.graph_x=settings_.graph_y=80; - settings_.mixer_x=settings_.mixer_y=settings_.piano_x=settings_.piano_y=100; - settings_.transport_width=800;settings_.transport_height=300;settings_.mixer_width=1056;settings_.mixer_height=600; - settings_.sequencer_width=settings_.graph_width=1100;settings_.sequencer_height=settings_.graph_height=650; - settings_.piano_width=900;settings_.piano_height=620;settings_.track_header_width=180; - sequencer_->header_width(180);tabs_->value(editors_[0].page);apply_ui_preferences(); -} -void MainWindow::show_track_inspector(bool visible) -{ - const int left=visible ? 216 : 12; - if(visible)sidebar_->show();else sidebar_->hide(); - tabs_->resize(left,tabs_->y(),w()-left-8,tabs_->h()); - auto *menu=dynamic_cast(child(0)); - if(menu) { auto *item=const_cast(menu->find_item("View/Track inspector")); - if(item) { if(visible)item->set();else item->clear(); } } - init_sizes();redraw(); -} -void MainWindow::resize(int x,int y,int width,int height) -{ - UiDouble_Window::resize(x,y,width,height); - if(transport_group_)layout_transport(); -} -void MainWindow::layout_transport() -{ - if(layout_transport_busy_ || !transport_group_ || transport_items_.empty())return; - layout_transport_busy_=true; - int width=transport_detached_ && transport_window_ ? transport_window_->w() : w(); - int origin=transport_detached_ ? 32 : 28; - bool compact=transport_detached_ && width<1220; - int left=12,top=12; - transport_group_->resizable(nullptr); - for(const auto &item:transport_items_) { - int x,y; - if(compact) { - if(left+item.w+(item.widget==tempo_display_ ? 36 : 0)>width-12){left=12;top+=40;} - x=left;y=origin+top;left+=item.w+(item.widget==tempo_display_ ? 36 : 0)+8; - } else {x=item.x*width/1280;y=origin+item.y;} - item.widget->resize(x,y,item.w,item.h); - } - int height=compact ? top+48 : 90; - transport_group_->resize(0,origin,width,height); - // Group resize can transform children: restore exact layout after it. - left=12;top=12; - for(const auto &item:transport_items_) { - int x,y;if(compact){if(left+item.w+(item.widget==tempo_display_ ? 36 : 0)>width-12){left=12;top+=40;}x=left;y=origin+top;left+=item.w+(item.widget==tempo_display_ ? 36 : 0)+8;} - else{x=item.x*width/1280;y=origin+item.y;} - item.widget->resize(x,y,item.w,item.h); - } - transport_group_->init_sizes(); - if(transport_detached_ && transport_window_ && transport_window_->h()resize(transport_window_->x(),transport_window_->y(),width,origin+height+8); - layout_transport_busy_=false; -} -void MainWindow::detach_transport(bool detached) -{ - if (detached==transport_detached_ || !transport_group_) return; - if (detached && !transport_window_) { - auto *current=UiGroup::current(); UiGroup::current(nullptr); - transport_window_.reset(new PanelWindow(static_cast(settings_.transport_width),static_cast(settings_.transport_height),"xtc — Transport (close to dock)",this)); - auto *dock=new UiButton(12,4,140,24,"Dock transport"); - dock->callback([](UiWidget *,void *c){static_cast(c)->detach_transport(false);},this); - xtc_ui::style(transport_window_.get()); - transport_window_->end(); transport_window_->size_range(400,130); - transport_window_->callback([](UiWidget *,void *c){static_cast(c)->detach_transport(false);},this); - restore_panel(transport_window_.get(),static_cast(settings_.transport_x),static_cast(settings_.transport_y),static_cast(settings_.transport_width),static_cast(settings_.transport_height),400,130); - UiGroup::current(current); - } - transport_detached_=detached;settings_.transport_detached=detached; - transport_group_->parent()->remove(transport_group_); - if (detached) { - transport_window_->add(transport_group_); - transport_group_->resize(0,32,transport_window_->w(),90); - transport_window_->resizable(transport_group_); - } else { - transport_window_->resizable(nullptr); - add(transport_group_); transport_group_->resize(0,28,w(),90); - transport_window_->hide(); - } - int delta=detached ? -90 : 90; - tabs_->resize(tabs_->x(),tabs_->y()+delta,tabs_->w(),tabs_->h()-delta); - sidebar_->resize(sidebar_->x(),sidebar_->y()+delta,sidebar_->w(),sidebar_->h()-delta); - init_sizes(); - auto *item=const_cast(menu_->find_item("View/Detach transport")); - if (detached) item->set(); else item->clear(); - layout_transport(); - if (detached) transport_window_->show(); - redraw(); -} - -void MainWindow::detach_mixer(bool detached) -{ - if (detached==mixer_detached_ || !mixer_) return; - if (detached && !mixer_window_) { - auto *current=UiGroup::current(); UiGroup::current(nullptr); - mixer_window_.reset(new PanelWindow(1056,600,"xtc — Mixer (close to dock)",this)); - auto *dock=new UiButton(12,6,140,28,"Dock mixer"); - dock->callback([](UiWidget *,void *c){static_cast(c)->detach_mixer(false);},this); - xtc_ui::style(mixer_window_.get()); - mixer_window_->end(); mixer_window_->size_range(600,420); - mixer_window_->callback([](UiWidget *,void *c){static_cast(c)->detach_mixer(false);},this); - restore_panel(mixer_window_.get(),static_cast(settings_.mixer_x),static_cast(settings_.mixer_y),static_cast(settings_.mixer_width),static_cast(settings_.mixer_height),600,420); - UiGroup::current(current); - } - mixer_detached_=detached;settings_.mixer_detached=detached; - mixer_->parent()->remove(mixer_); - if (detached) { - mixer_page_->resizable(nullptr); - mixer_window_->add(mixer_); mixer_->resize(12,44,mixer_window_->w()-24,mixer_window_->h()-56); - mixer_window_->resizable(mixer_); mixer_placeholder_->show(); - } else { - mixer_window_->resizable(nullptr); - mixer_page_->add(mixer_); - mixer_->resize(mixer_page_->x()+12,mixer_page_->y()+10,mixer_page_->w()-24,mixer_page_->h()-24); - mixer_page_->resizable(mixer_); mixer_placeholder_->hide(); mixer_window_->hide(); - } - mixer_->show(); mixer_page_->init_sizes(); - auto *item=const_cast(menu_->find_item("View/Detach mixer")); - if (detached) item->set(); else item->clear(); - if (detached) mixer_window_->show(); - redraw(); -} - -void MainWindow::draw() -{ - // JUCE may reload the platform scheme when a window first becomes visible. - // Keep built-in arrows, popups and choice fields flat as well as our boxes. - xtc_ui::configure_boxes(); - UiDouble_Window::draw(); -} - -/** - * @brief Removes the status timer and releases backend and recording state. - * - * A backend that fails to close is intentionally retained with its recorder - * until process exit so active callbacks never observe freed state. - */ -MainWindow::~MainWindow() -{ - Ui::remove_timeout(timer_callback, this); - // Return ownership before member windows and the main widget tree destruct. - detach_transport(false); - detach_mixer(false);detach_editor(0,false);detach_editor(1,false);detach_piano(false); - // Normal window/menu close already synchronized callbacks. This fallback - // covers exceptional event-loop exit. A failed close must retain its C - // callback context until process exit, rather than free active audio state. - if (backend_ != nullptr && xtc_backend_close(backend_) != XTC_OK) - std::fputs("Failed to close JACK during GUI destruction; backend and recording retained until process exit.\n", - stderr); - else if (recorder_) { - xtc_result result = xtc_recorder_finish(recorder_); - std::fprintf(stderr, "Recording finalized during shutdown (%s): %s\n", xtc_result_string(result), - xtc_recorder_recovery_path(recorder_)); - xtc_recorder_destroy(recorder_); - } -} - -/** - * @brief Shows an operation failure and a context-sensitive hint. - * - * @param operation Human-readable operation name; names containing - * "Project" select a file and permission hint. - * @param result Failing result code whose name is included in the message. - */ -void MainWindow::report_error(const char *operation, xtc_result result) -{ - char text[384]; - const char *hint = std::strstr(operation, "Project") != nullptr - ? "Check the project path, file format and write permissions." - : result == XTC_ERR_OUT_OF_MEMORY ? "Insufficient memory; the command was not accepted." - : result == XTC_ERR_CAPACITY ? "A project capacity limit was reached." - : result == XTC_ERR_NOT_FOUND || result == XTC_ERR_INVALID_ARGUMENT ? "Check the selected target and entered values." - : result == XTC_ERR_UNSUPPORTED ? "Check optional build support and the requested format or plugin layout." - : "Check the operation details; for audio failures use Reconnect to retry."; - std::snprintf(text, sizeof(text), "%s: %s. %s", operation, xtc_result_string(result), hint); - set_label(error_, text); -} - -/** - * @brief Edit handler passed to the modular graph widget. - * - * Applies the edit to a candidate graph, clones the project around it, and - * submits the new graph to the engine when connected. The live project is - * replaced only after every step succeeds. - * - * @param context MainWindow * passed as the graph widget owner context. - * @param edit Graph edit to apply. - * @param out_added_id Optional output receiving the new node ID when the edit - * adds a node; untouched otherwise or when nullptr. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an - * error from the graph, project, or backend operation. - */ -xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id) -{ - auto *window = static_cast(context); - if (window->recorder_) - return XTC_ERR_INVALID_STATE; - xtc_graph *candidate = nullptr; - size_t added_id = SIZE_MAX; - xtc_result result = xtc_graph_apply_edit(xtc_project_graph(window->project_.get()), &edit, &candidate, &added_id); - if (result != XTC_OK) - return result; - xtc_project *replacement = nullptr; - result = xtc_project_clone(window->project_.get(), &replacement); - if (result == XTC_OK) - result = xtc_project_set_graph(replacement, candidate); - xtc_graph_destroy(candidate); - if (result == XTC_OK) result = xtc_history_reserve(window->history_.get()); - if (result == XTC_OK && window->ready_) - result = xtc_backend_submit(window->backend_, xtc_project_graph(replacement)); - if (result != XTC_OK) { - xtc_project_destroy(replacement); - return result; - } - window->project_.reset(replacement); - window->graph_widget_->set_graph(xtc_project_graph(replacement)); - window->sequencer_->set_project(replacement, false); - window->mark_dirty(); - if (out_added_id != nullptr && (edit.kind == XTC_EDIT_ADD_NODE || edit.kind == XTC_EDIT_INSERT_GAIN)) - *out_added_id = added_id; - window->refresh_status(); - return XTC_OK; -} - -/** - * @brief Opens the JACK backend and submits the current graph. - * - * Uses the default backend configuration and leaves the engine stopped. - * Failure is reported and leaves the backend disconnected; a handle that - * opened but failed to accept the graph is retained for reconnect. - */ -void MainWindow::open_backend() -{ - xtc_backend_config config = {XTC_BACKEND_JACK, settings_.client_name, static_cast(settings_.max_frames), static_cast(settings_.channels)}; - xtc_result result = xtc_backend_open(&config, &backend_); - ready_ = result == XTC_OK; - running_ = false; - if (result != XTC_OK) - report_error("Open JACK failed", result); - else { - xtc_backend_status format{}; - result = xtc_backend_get_status(backend_, &format); - if (result == XTC_OK) - result = xtc_backend_set_transition_frames(backend_, static_cast(format.sample_rate)*static_cast(settings_.transition_ms)/1000U); - if (result == XTC_OK) - result = xtc_backend_submit(backend_, xtc_project_graph(project_.get())); - if (result != XTC_OK) { - ready_ = false; // Handle is retained for close/reconnect; graph was not installed. - report_error("Submit graph failed", result); - } else - set_label(error_, ""); - } - refresh_status(); -} - -/** - * @brief Starts the audio engine with the current project. - * - * Does nothing when already running. Reports an error when disconnected. - * Otherwise prepares the project and starts the engine. Missing or - * unreadable audio is reported as a warning after a successful start. - */ -void MainWindow::start_engine() -{ - if (!ready_) { - report_error("Audio is disconnected; use Reconnect", XTC_ERR_INVALID_STATE); - return; - } - if (running_) - return; - xtc_result result = configure_loop(project_.get(),settings_); - if(result==XTC_OK) result = xtc_backend_prepare_project(backend_, project_.get()); - if (result == XTC_OK) - result = start_configured_backend(); - if (result != XTC_OK) - report_error("Start failed; check audio and saved MIDI devices in Preferences", result); - else { - set_label(error_, midi_route_warning_.c_str()); - const char *issue = xtc_backend_asset_issue(backend_); - if (issue[0]) { - char message[640]; - std::snprintf(message, sizeof(message), - "Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair.", issue); - std::string warning=midi_route_warning_; - if(!warning.empty())warning+=" "; - warning+=message; - set_label(error_, warning.c_str()); - } - } - refresh_status(); -} - -/** - * @brief Requests a graceful playback stop, or immediately finalizes recording. - * - * Does nothing when disconnected. Failures are reported in the window. - */ -void MainWindow::stop_engine() -{ - if (!ready_) - return; - xtc_result result = recorder_ ? xtc_backend_stop(backend_) : xtc_backend_request_stop(backend_); - if (result != XTC_OK) - report_error("Stop engine failed", result); - else { - set_label(error_, ""); - if (recorder_) - finish_recording(); - } - refresh_status(); -} - -xtc_result MainWindow::pause_playback() -{ - xtc_result result = recorder_ || !ready_ ? XTC_ERR_INVALID_STATE : xtc_backend_request_stop(backend_); - if (result != XTC_OK) - report_error("Pause failed; use Stop to finish recording or Reconnect audio", result); - else - set_label(error_, ""); - refresh_status(); - return result; -} - -xtc_result MainWindow::return_to_start() -{ - if (recorder_) { - report_error("Stop recording before returning to start", XTC_ERR_INVALID_STATE); - return XTC_ERR_INVALID_STATE; - } - xtc_result result = XTC_OK; - if (ready_) { - result = xtc_backend_stop(backend_); - if (result == XTC_OK) - result = xtc_backend_seek(backend_, 0); - } - if (result != XTC_OK) - report_error("Return to start failed", result); - else { - sequencer_->position(0); - set_label(error_, ""); - } - refresh_status(); - return result; -} - -/** - * @brief Closes and reopens the JACK backend, leaving the engine stopped. - * - * Refuses to run while recording. If closing fails the existing handle is - * retained for a later retry and the failure is reported. - */ -void MainWindow::reconnect_engine() -{ - if (recorder_) { - report_error("Stop recording before reconnecting", XTC_ERR_INVALID_STATE); - return; - } - xtc_result result = xtc_backend_close(backend_); - if (result != XTC_OK) { - report_error("Reconnect could not close JACK", result); - return; // Preserve the existing handle for a later retry. - } - backend_ = nullptr; - ready_ = false; - running_ = false; - open_backend(); -} - -/** - * @brief Polls the backend and updates labels, meters, and button states. - * - * When disconnected, shows offline text and disables transport controls. - * Otherwise updates the sequencer position, mixer peaks, status line, and - * error indicators from the latest engine status, then collects backend - * work. - */ -void MainWindow::update_history_controls() -{ - size_t undo_count = 0, redo_count = 0; - if (!menu_ || xtc_history_counts(history_.get(),&undo_count,&redo_count) != XTC_OK) return; - for (const char *path : {"Edit/Undo","Edit/Redo","Edit/Redo alternate"}) { - int index = menu_->find_index(path); - if (index >= 0) menu_->mode(index,(!recorder_ && (std::strcmp(path,"Edit/Undo") == 0 ? undo_count : redo_count)) ? 0 : UI_MENU_INACTIVE); - } -} -void MainWindow::refresh_status() -{ - refresh_workspace(); - update_history_controls(); - if (mixer_ && !ready_) mixer_->meters(xtc_meter_snapshot{}); - if(track_inserts_ && !ready_)track_inserts_->meters(xtc_meter_snapshot{},false); - bool can_play = false, can_record = false; - if (!ready_) { - set_label(indicator_, "Audio: disconnected"); - set_label(status_, "JACK: not connected — use Reconnect after starting the server"); - indicator_->labelcolor(ui_rgb_color(255, 155, 120)); - set_label(graph_state_, "Graph: offline — edits stored locally; Reconnect to play"); - } else { - xtc_backend_collect(backend_); // Completes an acknowledged graceful stop before reading status. - xtc_backend_status status = {}; - xtc_result result = xtc_backend_get_status(backend_, &status); - if (result != XTC_OK) { - report_error("Read engine status failed", result); - if(track_inserts_)track_inserts_->meters(xtc_meter_snapshot{},false); - set_label(indicator_, "Audio: status unavailable"); - set_label(graph_state_, "Graph: engine status unavailable"); - set_label(status_, "Unable to read JACK status — use Reconnect"); - indicator_->labelcolor(ui_rgb_color(255, 155, 120)); - } else { - uint64_t tick = 0; - if (xtc_project_sample_to_tick(project_.get(), status.sample_position, status.sample_rate, &tick) == XTC_OK) - sequencer_->position(tick); - running_ = status.running != 0; - const bool fault = status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK; - can_play = !running_ && !fault; - can_record = !fault && recorder_ == nullptr; - const char *state = status.server_lost ? "Audio: server lost — Reconnect required" - : status.sample_rate_changed ? "Audio: sample rate changed — Reconnect required" - : status.process_result != XTC_OK ? "Audio: processing error" - : status.stopping ? "Audio: stopping — fading to silence" - : recorder_ ? "Audio: recording JACK input — Stop to finish" - : running_ ? "Audio: running — modular graph" - : "Audio: ready / stopped"; - set_label(indicator_, state); - indicator_->labelcolor(fault ? ui_rgb_color(255, 155, 120) - : running_ ? ui_rgb_color(125, 225, 160) - : ui_rgb_color(215, 215, 215)); - xtc_meter_snapshot meters{}; - if (xtc_backend_read_meters(backend_, &meters) == XTC_OK) mixer_->meters(meters); - else { meters.master_left=status.peak_left;meters.master_right=status.peak_right;mixer_->meters(meters); } - if(track_inserts_)track_inserts_->meters(meters,running_ && !fault,static_cast(settings_.refresh_ms)/1000); - char text[256]; - std::snprintf( - text, sizeof(text), - "JACK | %u Hz | %u frames | stereo | %u blocks | %u xruns | %s | %u asset issues | %u MIDI drops", - status.sample_rate, status.buffer_frames, status.processed_blocks, status.xruns, - xtc_result_string(status.process_result), status.asset_issues, status.midi_dropped); - set_label(status_, text); - if (fault) - set_label(graph_state_, "Graph: engine unavailable — Reconnect required"); - else if (status.submitted_revision != status.active_revision) - set_label(graph_state_, running_ ? "Graph: queued — engine still processing previous graph" - : "Graph: prepared — starts on Play"); - else - set_label(graph_state_, running_ ? "Graph: active in engine" : "Graph: stopped"); - } - } - if (can_play) - play_->activate(); - else - play_->deactivate(); - if (ready_ && running_ && !recorder_) - pause_->activate(); - else - pause_->deactivate(); - if (!recorder_) - home_->activate(); - else - home_->deactivate(); - if (can_record) - record_->activate(); - else - record_->deactivate(); - if (ready_ && (running_ || recorder_)) - stop_->activate(); - else - stop_->deactivate(); -} - -/** - * @brief Attempts to close the window after discarding unsaved changes. - * - * Refuses while recording. On success closes the backend, removes the status - * timer, hides the piano-roll window, and hides this window. - * - * @return True when the window closed; false when recording is active, the - * user cancels discarding, or the backend cannot close. - */ -bool MainWindow::close_window() -{ - if (recorder_) { - report_error("Stop recording before closing", XTC_ERR_INVALID_STATE); - return false; - } - if (!discard_changes()) - return false; - xtc_result result = xtc_backend_close(backend_); - if (result != XTC_OK) { - report_error("Close JACK failed; window kept open for retry", result); - return false; - } - backend_ = nullptr; - ready_ = false; - running_ = false; - if(!configuration_.empty() && configuration_valid_) { - xtc_result saved=save_preferences(); - if(saved!=XTC_OK) {report_error("Could not save application settings; window kept open for retry",saved);return false;} - } - closing_ = true; - Ui::remove_timeout(timer_callback, this); - for(auto &panel:editors_)if(panel.window)panel.window->hide(); - if (piano_window_) - piano_window_->hide(); - if (transport_window_) transport_window_->hide(); - if (mixer_window_) mixer_window_->hide(); - hide(); - return true; -} - -/** - * @brief Handles JUCE events for the window. - * - * Space without Ctrl/Alt/Meta toggles the audio engine and is consumed. - * - * @param event JUCE event code, such as UI_SHORTCUT. - * @return 1 when Space was consumed; otherwise the UiDouble_Window result. - */ -int MainWindow::panel_shortcut(int event) -{ - if (event == UI_SHORTCUT && dynamic_cast(Ui::focus()) && - (Ui::event_key() == ' ' || (Ui::event_state(UI_CTRL) && - (Ui::event_key() == 'z' || Ui::event_key() == 'y' || Ui::event_key() == 'd')))) return 0; - if (event == UI_SHORTCUT && Ui::event_key() == ' ' && (Ui::event_state() & (UI_CTRL | UI_ALT | UI_META)) == 0) { - if (running_) - stop_engine(); - else - start_engine(); - return 1; - } - if (event == UI_SHORTCUT) return menu_->test_shortcut() != nullptr; - return 0; -} - -int MainWindow::handle(int event) -{ - if (panel_shortcut(event)) return 1; - return UiDouble_Window::handle(event); -} - -/** - * @brief JUCE callback for the Play menu item and button. - * - * @param context MainWindow * whose engine is started. - */ -void MainWindow::play_callback(UiWidget *, void *context) -{ - static_cast(context)->start_engine(); -} - -/** - * @brief JUCE callback for the Stop menu item and button. - * - * @param context MainWindow * whose engine is stopped. - */ -void MainWindow::stop_callback(UiWidget *, void *context) -{ - static_cast(context)->stop_engine(); -} - -/** - * @brief JUCE callback for the Reconnect menu item and button. - * - * @param context MainWindow * whose backend is reopened. - */ -void MainWindow::reconnect_callback(UiWidget *, void *context) -{ - static_cast(context)->reconnect_engine(); -} - -/** - * @brief JUCE callback for the window close box, File/Quit, and close events. - * - * @param context MainWindow * whose close is requested; a refused close - * simply leaves the window open. - */ -void MainWindow::close_callback(UiWidget *, void *context) -{ - (void)static_cast(context)->close_window(); -} - -/** - * @brief JUCE menu callback that selects a tab page in the main window. - * - * @param widget Triggering menu item; window() must return the MainWindow. - * @param view UiWidget * tab page to make current, not the MainWindow. - */ -void MainWindow::view_callback(UiWidget *widget, void *view) -{ - auto *window = static_cast(widget->window()); - window->tabs_->value(static_cast(view)); - window->tabs_->redraw(); - for(auto &panel:window->editors_)if(view==panel.page && panel.detached)panel.window->show(); - if(view==window->piano_page_) {if(window->open_piano_roll()!=XTC_OK)window->tabs_->value(window->editors_[0].page);} - if (view==window->mixer_page_ && window->mixer_detached_) window->mixer_window_->show(); -} - -/** - * @brief JUCE help callback that shows the application name and version. - * - */ -void MainWindow::about_callback(UiWidget *, void *) -{ - ui_message("%s %s\nModular audio workstation\nArrangement markers, MIDI composition, audio editing and modular mixing.", - XTC_PROJECT_NAME, XTC_VERSION_STRING); -} - -/** - * @brief JUCE timeout callback that refreshes status and supervises recording. - * - * While recording, drains the recorder and stops the engine if draining - * fails or the backend reports a lost server, sample-rate change, or - * processing error. Reschedules itself every 0.1 seconds until closing. - * - * @param context MainWindow * registered with Ui::add_timeout(). - */ -void MainWindow::timer_callback(void *context) -{ - xtc_plugin_idle(); - auto *window = static_cast(context); - if (window->recorder_) { - xtc_result r = xtc_recorder_drain(window->recorder_); - xtc_backend_status status{}; - xtc_backend_get_status(window->backend_, &status); - if (r != XTC_OK || status.server_lost || status.sample_rate_changed || status.process_result != XTC_OK) - window->stop_engine(); - } - if(window->settings_.stop_after_tail && !window->settings_.loop_enabled && !window->recorder_) { - xtc_backend_status status{};uint64_t end=0; - if(xtc_backend_get_status(window->backend_,&status)==XTC_OK && status.running && - xtc_project_render_length(window->project_.get(),status.sample_rate,static_cast(window->settings_.tail_ms),&end)==XTC_OK && status.sample_position>=end) - window->stop_engine(); - } - window->refresh_status(); - window->follow_playhead(); - if (!window->closing_) - Ui::repeat_timeout(static_cast(window->settings_.refresh_ms)/1000,timer_callback,context); -} - -/** - * @brief Records the current project in history and marks the window dirty. - * - * Dependent widgets are refreshed with the current project and graph, and - * the title gains the unsaved marker. A history failure is reported. - */ -void MainWindow::mark_dirty() -{ - xtc_result history_result = xtc_history_record(history_.get(), project_.get()); - if (history_result != XTC_OK) - report_error("Undo history could not retain edit", history_result); - mixer_->set_project(project_.get()); - graph_widget_->set_graph(xtc_project_graph(project_.get())); - if (piano_) - piano_->set_project(project_.get(), piano_->clip()); - dirty_ = true; - update_history_controls(); - copy_label("xtc * — unsaved changes"); -} - -/** - * @brief Asks whether unsaved changes may be discarded. - * - * @return True when the project is clean, prompting is disabled, or the user - * chose Discard; false when the user cancels. - */ -bool MainWindow::discard_changes() -{ - return !dirty_ || !prompt_unsaved_ || - ui_choice("Discard unsaved project changes?", "Cancel", "Discard", nullptr) == 1; -} - -/** - * @brief Hook used by the sequencer to seek the audio engine. - * - * Does nothing when the backend is disconnected. Otherwise reads the engine - * sample rate, converts the tick position to samples, and seeks the backend. - * - * @param context MainWindow * passed as the sequencer owner context. - * @param tick Target timeline position in project ticks. - * @return XTC_OK when disconnected or the seek succeeded, or the status, - * conversion, or backend seek error. - */ -xtc_result MainWindow::seek(void *context, uint64_t tick) -{ - auto *window = static_cast(context); - if (!window->ready_) - return XTC_OK; - xtc_backend_status status{}; - xtc_result r = xtc_backend_get_status(window->backend_, &status); - uint64_t sample = 0; - if (r == XTC_OK) - r = xtc_project_tick_to_sample(window->project_.get(), tick, status.sample_rate, &sample); - return r == XTC_OK ? xtc_backend_seek(window->backend_, sample) : r; -} - -/** - * @brief Saves the current project to a path and updates the window title. - * - * On success the path is remembered and the unsaved marker is cleared. - * - * @param path Destination project path; nullptr is invalid. - * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, - * XTC_ERR_INVALID_STATE while recording, or the save error. - */ -xtc_result MainWindow::save_project(const char *path) -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - if (path == nullptr) - return XTC_ERR_INVALID_ARGUMENT; - std::string saved_path(path); - xtc_result r = xtc_project_save(project_.get(), path); - if (r == XTC_OK) { - path_ = saved_path; - dirty_ = false; - copy_label(path_.c_str()); - } - return r; -} - -/** - * @brief Installs a candidate project as the active project. - * - * Rejected while recording. Optionally builds fresh undo history, stops and - * prepares the connected backend, or validates offline audio and MIDI plans - * when disconnected. On success the window takes ownership of candidate, - * rebinds widgets, resets the graph layout, clears the dirty flag, and shows - * any missing-asset warning. On failure ownership stays with the caller. - * - * @param candidate Project to install; must be non-null. - * @param reset_history True to discard existing undo/redo history and start - * from candidate. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an - * error from history creation, the backend, or offline planning. - */ -xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_history) -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - std::unique_ptr new_history(nullptr, xtc_history_destroy); - if (reset_history) { - xtc_history *h = nullptr; - xtc_result result = xtc_history_create(candidate, &h); - if (result != XTC_OK) - return result; - new_history.reset(h); - } - xtc_result r = XTC_OK; - if (ready_) { - r = xtc_backend_stop(backend_); - if (r == XTC_OK) - r = xtc_backend_seek(backend_, 0); - if(r==XTC_OK) r=configure_loop(candidate,settings_); - if (r == XTC_OK) - r = xtc_backend_prepare_project(backend_, candidate); - } - xtc_audio_plan *offline = nullptr; - if (r == XTC_OK && !ready_) - r = xtc_audio_plan_create(candidate, static_cast(settings_.offline_rate), &offline); - xtc_midi_plan *offline_midi = nullptr; - if (r == XTC_OK && !ready_) - r = xtc_midi_plan_create(candidate, static_cast(settings_.offline_rate), &offline_midi); - size_t sample_issues=xtc_midi_plan_asset_issues(offline_midi); - xtc_midi_plan_destroy(offline_midi); - if (r == XTC_OK && !ready_) { - xtc_plan *graph = nullptr; - r = xtc_graph_compile(xtc_project_graph(candidate),static_cast(settings_.offline_rate),256,&graph); - xtc_plan_destroy(graph); - } - if (r != XTC_OK) { - xtc_audio_plan_destroy(offline); - refresh_status(); // A failed install may have stopped playback. - return r; - } - std::string issue = ready_ ? xtc_backend_asset_issue(backend_) : ""; - if (offline) { - const char *path = ""; - xtc_audio_plan_issues(offline, &path); - issue = path; - xtc_audio_plan_destroy(offline); - } - if(sample_issues)issue="Sampler WAV missing/invalid; File/Load sampler WAV relinks the selected track"; - if (piano_window_) - piano_window_->hide(); - if (piano_) - piano_->set_project(candidate, UINT64_MAX); - project_.reset(candidate); - mixer_->set_project(candidate); - if (reset_history) { - history_.swap(new_history); - for(auto &snapshot:mix_snapshots_)snapshot.reset(); - if(track_inserts_)track_inserts_->clear_session(); - xtc_tap_tempo_reset(&tap_state_); - } - graph_widget_->reset_layout(); - graph_widget_->set_graph(xtc_project_graph(candidate)); - sequencer_->set_project(candidate,true,!reset_history); - if (reset_history) - tabs_->value(tabs_->child(0)); - dirty_ = false; - if (!issue.empty()) { - char text[640]; - std::snprintf(text, sizeof(text), "Missing/unreadable audio (silent): %s. Use File/Relink selected audio or Load sampler WAV to repair.", - issue.c_str()); - set_label(error_, text); - } else - set_label(error_, ""); - refresh_status(); - return XTC_OK; -} - -/** - * @brief Loads a project from a path and makes it the current project. - * - * The file is loaded into a candidate project and installed only on success. - * - * @param path Source project path; nullptr is invalid. - * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, or an - * error from loading or installing the project. - */ -xtc_result MainWindow::open_project(const char *path) -{ - BusyOperation busy(*this,status_,"Loading and preparing project…"); - if (path == nullptr) - return XTC_ERR_INVALID_ARGUMENT; - project_issue_.clear(); - std::string loaded_path(path); - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_create(&candidate); - xtc_project_diagnostic diagnostic{}; - if (r == XTC_OK) { - r = xtc_project_load_diagnostic(candidate,path,&diagnostic); - if (r != XTC_OK) { - project_issue_ = "Project load line " + std::to_string(diagnostic.line) + " (" + diagnostic.record + "): " + diagnostic.message; - report_error(project_issue_.c_str(),r); - } - } - if (r == XTC_OK) - r = install_project(candidate); - if (r != XTC_OK) { - if (candidate) { - const xtc_graph *graph = xtc_project_graph(candidate); - size_t nodes = 0, edges = 0; - xtc_graph_get_counts(graph,&nodes,&edges); - for (size_t i = 0; i < nodes; ++i) { - xtc_node_info node{}; xtc_plugin_spec spec{}; - if (xtc_graph_get_node(graph,i,&node) != XTC_OK || node.type != XTC_NODE_PLUGIN || - xtc_graph_get_plugin(graph,node.id,&spec) != XTC_OK) continue; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin *check = nullptr; - xtc_result plugin_result = xtc_plugin_create(&spec,rate,256,&check); - xtc_plugin_destroy(check); - if (plugin_result != XTC_OK) { - project_issue_ = std::string("Missing/incompatible plugin ") + spec.label + " at " + spec.path + - "; restore a compatible module and retry Open. Saved document retained"; - report_error(project_issue_.c_str(),plugin_result); - break; - } - } - } - xtc_project_destroy(candidate); - } else { - path_ = loaded_path; - copy_label(path_.c_str()); - } - return r; -} - -xtc_result MainWindow::new_project() -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - xtc_project *candidate = nullptr; - xtc_result result = xtc_project_create(&candidate); - if(result==XTC_OK) project_defaults(candidate); - if (result == XTC_OK) - result = install_project(candidate); - if (result != XTC_OK) - xtc_project_destroy(candidate); - else { - path_.clear(); - copy_label("xtc — Untitled"); - } - return result; -} - -/** - * @brief JUCE menu callback for the File/New, Open, Save, and Save As items. - * - * Reads the action from the selected menu label, confirms discarding unsaved - * changes for New and Open, and uses a native file chooser unless Save can - * reuse the remembered path. Refuses file operations while recording and - * reports failures in the window. - * - * @param widget Triggering UiMenu_Bar whose selected label names the action. - * @param context MainWindow * owning the project and file path. - */ -void MainWindow::file_callback(UiWidget *widget, void *context) -{ - auto *window = static_cast(context); - if (window->recorder_) { - window->report_error("Stop recording before file operations", XTC_ERR_INVALID_STATE); - return; - } - auto *menu = static_cast(widget); - const char *action = menu->mvalue()->label(); - xtc_result r = XTC_OK; - window->project_issue_.clear(); - if (std::strcmp(action, "New") == 0) { - if (!window->discard_changes()) - return; - r = window->new_project(); - } else { - bool open = std::strcmp(action, "Open") == 0; - if (open && !window->discard_changes()) - return; - if (!open && std::strcmp(action, "Save") == 0 && !window->path_.empty()) - r = window->save_project(window->path_.c_str()); - else { - UiNative_File_Chooser chooser; - chooser.title(open ? "Open project" : "Save project"); - chooser.type(open ? UiNative_File_Chooser::BROWSE_FILE : UiNative_File_Chooser::BROWSE_SAVE_FILE); - chooser.filter("xtc projects *.xtc"); - if (!open) - chooser.options(UiNative_File_Chooser::SAVEAS_CONFIRM); - int result = chooser.show(); - if (result == 1) - return; - if (result < 0) - r = XTC_ERR_IO; - else - r = open ? window->open_project(chooser.filename()) : window->save_project(chooser.filename()); - } - } - if (r != XTC_OK) - window->report_error(window->project_issue_.empty() ? "Project operation failed" : window->project_issue_.c_str(), r); -} - -/** - * @brief Hook called by arrangement widgets before mutating the project. - * - * Refuses edits while recording. When connected, stops the engine so the - * mutation cannot race the audio thread, then refreshes the status display. - * - * @param context MainWindow * passed as the widget owner context. - * @return XTC_OK when disconnected or the stop succeeded, - * XTC_ERR_INVALID_STATE while recording, or the backend stop error. - */ -xtc_result MainWindow::before_arrangement_edit(void *context) -{ - auto *window = static_cast(context); - if (window->recorder_) return XTC_ERR_INVALID_STATE; - xtc_result r = xtc_history_reserve(window->history_.get()); - if (r == XTC_OK && window->ready_) r = xtc_backend_stop(window->backend_); - window->refresh_status(); - return r; -} - -/** - * @brief Imports a WAV file as an audio clip in a cloned project. - * - * Stops the engine first, creates an "Audio" track when none is selected, - * and places the clip at the sequencer cursor. The clone replaces the live - * project only after every step succeeds. - * - * @param path Path to the WAV file to import. - * @return XTC_OK on success, or an error from the engine stop, clone, import, - * or backend preparation. - */ -xtc_result MainWindow::import_audio(const char *path) -{ - BusyOperation busy(*this,status_,"Importing audio and preparing playback…"); - xtc_result r = before_arrangement_edit(this); - if (r != XTC_OK) - return r; - xtc_project *candidate = nullptr; - r = xtc_project_clone(project_.get(), &candidate); - uint64_t track = sequencer_->selected_track(), clip = 0; - if (r == XTC_OK && track == UINT64_MAX) - r = xtc_project_add_track(candidate, "Audio", &track); - if (r == XTC_OK) - r = xtc_project_import_audio(candidate, track, path, sequencer_->position(), &clip); - if (r == XTC_OK && ready_) - r = xtc_backend_prepare_project(backend_, candidate); - if (r != XTC_OK) { - xtc_project_destroy(candidate); - return r; - } - project_.reset(candidate); - sequencer_->set_project(candidate, false); - sequencer_->select(track,clip); - graph_widget_->set_graph(xtc_project_graph(candidate)); - mark_dirty(); - refresh_status(); - return XTC_OK; -} - -/** - * @brief Starts recording JACK input to a WAV file. - * - * Requires a connected backend and no active recording. The engine is - * stopped, the current position and selected track are remembered, and a - * stereo recorder at the engine sample rate is attached before starting. - * Failure leaves the window without a recorder. - * - * @param path Destination WAV path chosen by the user. - * @return XTC_OK when recording started, XTC_ERR_INVALID_STATE when - * disconnected or already recording, or another backend or recorder - * error. - */ -xtc_result MainWindow::record_audio(const char *path) -{ - if (!ready_ || recorder_ || settings_.loop_enabled) - return XTC_ERR_INVALID_STATE; - xtc_result r = xtc_backend_stop(backend_); - xtc_backend_status status{}; - if (r == XTC_OK) - r = xtc_backend_get_status(backend_, &status); - if (r == XTC_OK) - r = xtc_project_sample_to_tick(project_.get(), status.sample_position, status.sample_rate, &recording_tick_); - if (r == XTC_OK) - r = xtc_backend_prepare_project(backend_, project_.get()); - if (r == XTC_OK) - r = xtc_recorder_create(path, status.sample_rate, static_cast(status.sample_rate) * static_cast(settings_.recording_seconds), &recorder_); - if (r != XTC_OK) - return r; - recording_path_ = path; - recording_track_ = sequencer_->selected_track(); - r = xtc_backend_set_recorder(backend_, recorder_); - if (r == XTC_OK) - r = start_configured_backend(); - if (r != XTC_OK) { - (void)xtc_backend_set_recorder(backend_, nullptr); - xtc_recorder_destroy(recorder_); - recorder_ = nullptr; - } - refresh_status(); - return r; -} - -/** - * @brief Finalizes the active recording and adds the take to the project. - * - * Must run only after the engine has stopped, so no audio callback can - * reference the recorder ring. The recorder is detached, finished, and - * destroyed. A failed finalize or empty take is reported and not added. If - * the subsequent import fails, the saved WAV path is reported for recovery. - */ -void MainWindow::finish_recording() -{ - // Called only after successful backend_stop: no producer can reference ring. - (void)xtc_backend_set_recorder(backend_, nullptr); - uint64_t frames = 0; - (void)xtc_recorder_status(recorder_, &frames); - xtc_result r = xtc_recorder_finish(recorder_); - std::string recovery_path = xtc_recorder_recovery_path(recorder_); - xtc_recorder_destroy(recorder_); - recorder_ = nullptr; - if (r != XTC_OK) { - char text[768]; - std::snprintf(text, sizeof(text), - "Recording failed (%s). Retained take/temporary: %s. Recover with xtc-check --recover-recording. Not added.", - xtc_result_string(r), recovery_path.c_str()); - set_label(error_, text); - return; - } - if (!frames) { - set_label(error_, "Empty recording saved; no clip added."); - return; - } - xtc_project *candidate = nullptr; - r = xtc_project_clone(project_.get(), &candidate); - uint64_t clip = 0, track = recording_track_; - if (r == XTC_OK && track == UINT64_MAX) - r = xtc_project_add_track(candidate, "Recording", &track); - if (r == XTC_OK) - r = xtc_project_import_audio(candidate, track, recording_path_.c_str(), recording_tick_, &clip); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) - r = xtc_backend_prepare_project(backend_, candidate); - if (r != XTC_OK) { - xtc_project_destroy(candidate); - char text[768]; - std::snprintf(text, sizeof(text), "Take saved to %s; adding clip failed: %s. Import the WAV to recover.", - recording_path_.c_str(), xtc_result_string(r)); - set_label(error_, text); - return; - } - project_.reset(candidate); - sequencer_->set_project(candidate, false); - graph_widget_->set_graph(xtc_project_graph(candidate)); - mark_dirty(); -} - -/** - * @brief JUCE callback for the audio import, record, and relink commands. - * - * The triggering widget supplies the menu label; the Record button uses the - * "Record WAV" default. A native file chooser selects the file, after which - * the selected clip is relinked, recording starts, or a WAV is imported. - * Failures are reported in the window. - * - * @param widget Triggering UiMenu_Bar or UiButton; menu labels select the - * action. - * @param context MainWindow * owning the project and sequencer. - */ -void MainWindow::audio_file_callback(UiWidget *widget, void *context) -{ - auto *window = static_cast(context); - auto *menu = dynamic_cast(widget); - const char *action = menu ? menu->mvalue()->label() : - (widget && widget->label() && !std::strcmp(widget->label(),"Import WAV") ? "Import WAV" : "Record WAV"); - bool recording = std::strcmp(action, "Record WAV") == 0; - bool relink = std::strcmp(action, "Relink selected audio") == 0; - if (window->recorder_) { - window->report_error("Stop recording first", XTC_ERR_INVALID_STATE); - return; - } - UiNative_File_Chooser chooser; - chooser.title(recording ? "Record JACK input to WAV" : relink ? "Relink selected clip" : "Import WAV"); - chooser.type(recording ? UiNative_File_Chooser::BROWSE_SAVE_FILE : UiNative_File_Chooser::BROWSE_FILE); - chooser.filter("WAV audio\t*.wav"); - // Recordings require a new destination; existing files are never replaced. - int chosen = chooser.show(); - if (chosen == 1) - return; - xtc_result r = chosen < 0 ? XTC_ERR_IO : XTC_OK; - if (r == XTC_OK && relink) { - r = before_arrangement_edit(window); - if (r == XTC_OK) - r = xtc_project_set_clip_asset(window->project_.get(), window->sequencer_->selected_clip(), - chooser.filename()); - if (r == XTC_OK) { - window->mark_dirty(); - window->sequencer_->redraw(); - } - } else if (r == XTC_OK) - r = recording ? window->record_audio(chooser.filename()) : window->import_audio(chooser.filename()); - if (r != XTC_OK) - window->report_error(recording ? "Project recording failed; choose a new WAV path in a writable directory" - : "Project audio operation failed", r); -} - -/** - * @brief Shows the piano roll for the selected MIDI clip. - * - * The piano-roll window is created on first use and reused afterwards. - * - * @return XTC_OK on success, or XTC_ERR_INVALID_STATE when the selected clip - * does not exist or is not MIDI. - */ -xtc_result MainWindow::open_piano_roll() -{ - xtc_clip_info clip{}; - if (xtc_project_find_clip(project_.get(), sequencer_->selected_clip(), &clip) != XTC_OK || !clip.midi) - return XTC_ERR_INVALID_STATE; - if (!piano_window_) { - piano_window_ = std::make_unique(900, 620, "xtc — Piano roll (close to dock)",this); - piano_window_->begin(); - piano_content_=new UiGroup(0,0,900,620);piano_content_->begin(); - auto *menu = new UiMenu_Bar(12, 8, 876, 30); - const char *commands[] = {"Notes/Grid (G)", "Notes/Filter (F)", "Notes/Velocity and channel (V)", - "Notes/Quantize (Q)", "Notes/Transpose (T)", "Notes/Repeat (Ctrl+D)", - "Notes/Select filtered (Ctrl+A)", "Notes/Delete"}; - for (int i = 0; i < 8; ++i) - menu->add(commands[i], 0, [](UiWidget *widget, void *data) { - auto *owner = static_cast(data); - auto *bar = static_cast(widget); - owner->piano_->command(static_cast(bar->mvalue() - bar->menu())); - }, this); - menu->add("Edit/Undo",UI_CTRL+'z',history_callback,this); - menu->add("Edit/Redo",UI_CTRL+UI_SHIFT+'z',history_callback,this); - menu->add("Edit/Redo alternate",UI_CTRL+'y',history_callback,this); - piano_snap_ = new UiChoice(60,46,150,26,"Snap"); - for (size_t i = 0; i < xtc_ui::snap_count; ++i) { int item = piano_snap_->add("Snap"); piano_snap_->replace(item,xtc_ui::snap_labels[i]); } - piano_snap_->callback([](UiWidget *widget, void *c) { - auto *w = static_cast(c); int value = static_cast(widget)->value(); - if (value >= 0 && static_cast(value) < xtc_ui::snap_count) (void)w->piano_->set_grid(xtc_ui::snap_ticks[value]); - },this); - note_velocity_ = new UiValue_Slider(340,46,300,26,"Note velocity"); - note_velocity_->align(UI_ALIGN_LEFT); note_velocity_->value(100); - note_velocity_->tooltip("Selected note MIDI velocity, 1..127. Instrument patches determine the response."); - note_velocity_->type(UI_HOR_FILL_SLIDER); note_velocity_->bounds(1,127); note_velocity_->step(1); - note_velocity_->when(UI_WHEN_RELEASE); - note_velocity_->callback([](UiWidget *widget, void *c) { - auto *w = static_cast(c); - (void)w->piano_->set_velocity(w->piano_->selected_note(),static_cast(static_cast(widget)->value())); - },this); - auto *scroll = new UiScroll(12,84,876,404); - piano_ = new PianoRollWidget(12,84, project_.get(), clip.id, before_arrangement_edit, this); - piano_->callback( - [](UiWidget *, void *c) { - static_cast(c)->mark_dirty(); - }, - this); - scroll->end(); - velocity_lane_ = new VelocityLane(12,500,876,108,*piano_); - piano_content_->resizable(scroll);piano_content_->end(); - piano_window_->end(); - piano_window_->callback([](UiWidget *,void *c){static_cast(c)->detach_piano(false);},this); - piano_window_->size_range(800,520); - xtc_ui::style(piano_window_.get()); - piano_window_->resizable(piano_content_); - scroll->scroll_to(0, 60 * 16); - auto px=settings_.piano_x,py=settings_.piano_y,pw=settings_.piano_width,ph=settings_.piano_height; - collect_preferences();settings_.piano_x=px;settings_.piano_y=py;settings_.piano_width=pw;settings_.piano_height=ph; - apply_ui_preferences(); - } else - piano_->set_project(project_.get(), clip.id); - refresh_workspace(); - detach_piano(settings_.piano_detached!=0); - if(piano_detached_)piano_window_->show();else {tabs_->value(piano_page_);tabs_->redraw();} - return XTC_OK; -} - -/** - * @brief JUCE callback for the Piano roll menu item and button. - * - * Refuses to open the editor while recording and asks the user to select a - * MIDI clip when open_piano_roll() fails. - * - * @param context MainWindow * owning the piano roll. - */ -void MainWindow::piano_callback(UiWidget *, void *context) -{ - auto *window = static_cast(context); - if (window->recorder_) { - window->report_error("Stop recording before editing MIDI", XTC_ERR_INVALID_STATE); - return; - } - xtc_result r = window->open_piano_roll(); - if (r != XTC_OK) - set_label(window->error_, "Select a MIDI clip, then open Piano roll."); -} - -/** - * @brief JUCE callback for the Panic menu item and button. - * - * When the backend is connected, sends a panic request that silences held - * instrument notes and reports failures. - * - * @param context MainWindow * owning the backend. - */ -void MainWindow::panic_callback(UiWidget *, void *context) -{ - auto *window = static_cast(context); - if (!window->ready_) - return; - xtc_result r = xtc_backend_panic(window->backend_); - if (r != XTC_OK) - window->report_error("Panic failed", r); -} - -/** - * @brief Applies one undo or redo step from the project history. - * - * The selected snapshot is installed without resetting history and then - * accepted, leaving the window marked dirty. - * - * @param redo True to redo, false to undo. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, - * XTC_ERR_NOT_FOUND when no step is available, or another error from - * checkout, install, or accept. - */ -xtc_result MainWindow::undo(bool redo) -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - const uint64_t track = sequencer_->selected_track(), clip = sequencer_->selected_clip(); - const uint64_t mixer_track = mixer_->selected_track(); - const size_t node = graph_widget_->selected_node(); - const uint64_t piano_clip = piano_ ? piano_->clip() : UINT64_MAX; - const uint64_t note = piano_ ? piano_->selected_note() : UINT64_MAX; - const bool piano_visible = piano_window_ && piano_window_->shown(); - xtc_project *candidate = nullptr; - xtc_result r = xtc_history_checkout(history_.get(), redo ? 1 : 0, &candidate); - if (r == XTC_OK) - r = install_project(candidate, false); - if (r != XTC_OK) { - xtc_project_destroy(candidate); - return r; - } - xtc_clip_info selected_clip{}; - if (xtc_project_find_clip(project_.get(),clip,&selected_clip) == XTC_OK) - sequencer_->select(selected_clip.track_id,clip); - else sequencer_->select(track); - mixer_->select_track(mixer_track); - graph_widget_->select_node(node); - xtc_clip_info info{}; - if (piano_ && xtc_project_find_clip(project_.get(),piano_clip,&info) == XTC_OK && info.midi) { - piano_->set_project(project_.get(),piano_clip); piano_->select_note(note); - if (piano_visible) piano_window_->show(); - } - r = xtc_history_accept(history_.get(), redo ? 1 : 0); - dirty_ = true; - update_history_controls(); - copy_label("xtc * — unsaved changes"); - return r; -} - -/** - * @brief JUCE menu callback for the Undo and Redo menu items. - * - * The selected menu label starting with "Redo" chooses redo; all other - * labels undo. Failures other than XTC_ERR_NOT_FOUND are shown in the window. - * - * @param widget UiMenu_Bar whose selected item triggered the callback. - * @param context MainWindow * owning the menu and history. - */ -void MainWindow::history_callback(UiWidget *widget, void *context) -{ - auto *window = static_cast(context); - auto *menu = static_cast(widget); - bool redo = std::strncmp(menu->mvalue()->label(), "Redo", 4) == 0; - xtc_result r = window->undo(redo); - if (r != XTC_OK && r != XTC_ERR_NOT_FOUND) - window->report_error("Undo/redo failed", r); -} - -xtc_result MainWindow::create_track(bool midi) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - const uint64_t position = sequencer_->position(); - xtc_project *candidate = nullptr; - uint64_t track = UINT64_MAX, clip = UINT64_MAX; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_add_track(candidate,midi ? "MIDI track" : "Audio track",&track); - if (r == XTC_OK && midi) r = xtc_project_add_midi_clip(candidate,track,position,static_cast(settings_.clip_ticks),&clip); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); - else { - sequencer_->select(track,clip); - mixer_->select_track(track); tabs_->value(tabs_->child(0)); mark_dirty(); - } - return r; -} -void MainWindow::refresh_workspace() -{ - if(track_inserts_)track_inserts_->refresh(); - if (loop_button_) { - loop_button_->value(settings_.loop_enabled != 0); - if (recorder_) - loop_button_->deactivate(); - else - loop_button_->activate(); - } - if (snap_) - snap_->value(xtc_ui::snap_index(sequencer_->grid())); - if (piano_) { - piano_snap_->value(xtc_ui::snap_index(piano_->grid())); - size_t count = 0; - bool selected = false; - xtc_project_note_count(project_.get(), &count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; - xtc_project_get_note(project_.get(), i, ¬e); - if (note.id == piano_->selected_note() && piano_->note_visible(note)) { - selected = true; - if (Ui::pushed() != note_velocity_) - note_velocity_->value(note.velocity); - break; - } - } - if (selected) - note_velocity_->activate(); - else - note_velocity_->deactivate(); - velocity_lane_->redraw(); - } - char position[64], info[512]; - uint64_t tick = sequencer_->position(); - std::snprintf(position, sizeof(position), "%03llu : %u : %03u", static_cast(tick / 3840 + 1), - static_cast((tick / 960) % 4 + 1), static_cast(tick % 960)); - set_label(transport_position_, position); - if (!editing_number(tempo_display_)) - tempo_display_->value(xtc_project_tempo(project_.get()) / 1000.0); - if (recorder_) - tempo_display_->deactivate(); - else - tempo_display_->activate(); - size_t tracks = 0, clips = 0; - xtc_project_get_counts(project_.get(), &tracks, &clips); - uint64_t end = 0; - for (size_t i = 0; i < clips; ++i) { - xtc_clip_info clip{}; - xtc_project_get_clip(project_.get(), i, &clip); - end = std::max(end, clip.start_tick + clip.length_tick); - } - std::snprintf(info, sizeof(info), "%zu tracks / %zu clips / %llu bars", tracks, clips, - static_cast((end + 3839) / 3840)); - set_label(session_info_, info); - bool mixer_context = tabs_->value() == mixer_page_ || - (mixer_detached_ && Ui::focus() && Ui::focus()->window() == mixer_window_.get()); - uint64_t target = mixer_context ? mixer_->selected_track() : sequencer_->selected_track(); - bool selection_changed = target != inspected_track_; - inspected_track_ = UINT64_MAX; - for (size_t i = 0; i < tracks; ++i) { - xtc_track_info track{}; - xtc_project_get_track(project_.get(), i, &track); - if (track.id != target) - continue; - inspected_track_ = track.id; - for (auto *control : {static_cast(inspector_gain_), static_cast(inspector_pan_), - static_cast(inspector_mute_), static_cast(inspector_solo_)}) { - if (recorder_) - control->deactivate(); - else - control->activate(); - } - if (selection_changed || !editing_number(inspector_gain_)) - inspector_gain_->value(track.gain); - if (selection_changed || !editing_number(inspector_pan_)) - inspector_pan_->value(track.pan); - if (Ui::pushed() != inspector_mute_) - inspector_mute_->value(track.mute); - if (Ui::pushed() != inspector_solo_) - inspector_solo_->value(track.solo); - xtc_plugin_spec spec{}; - bool plugin = xtc_project_get_instrument(project_.get(), track.id, &spec) == XTC_OK; - std::snprintf(info, sizeof(info), "%s\n%s %s%s\n%s", track.name, track.is_bus ? "BUS" : "TRACK", - track.mute ? "Muted " : "", track.solo ? "Solo" : "", - track.is_bus ? "Bus output" - : plugin ? spec.label - : "Built-in MIDI sine"); - set_label(selection_info_, info); - return; - } - for (auto *control : {static_cast(inspector_gain_), static_cast(inspector_pan_), - static_cast(inspector_mute_), static_cast(inspector_solo_)}) - control->deactivate(); - std::snprintf(info, sizeof(info), "No track selected\nCreate a MIDI track or import a WAV to begin."); - set_label(selection_info_, info); -} - -xtc_result MainWindow::assign_instrument(uint64_t track, const char *path, unsigned int index) -{ - uint64_t clip = sequencer_->selected_track() == track ? sequencer_->selected_clip() : UINT64_MAX; - if (recorder_) return XTC_ERR_INVALID_STATE; - xtc_project *candidate = nullptr; - xtc_plugin_spec spec{}; - xtc_result r = path ? xtc_plugin_probe(path,index,sample_rate(),&spec) : XTC_OK; - if (r == XTC_OK && path && !spec.instrument) return XTC_ERR_UNSUPPORTED; - if (r == XTC_OK) r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_set_instrument(candidate,track,path ? &spec : nullptr); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); else { sequencer_->select(track,clip); mixer_->select_track(track); mark_dirty(); } - return r; -} -xtc_result MainWindow::import_midi_file(const char *path) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - uint64_t track=sequencer_->selected_track(),start=sequencer_->position(),clip=0;xtc_project *candidate=nullptr; - xtc_result r=before_arrangement_edit(this); - if(r==XTC_OK)r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK && track==UINT64_MAX)r=xtc_project_add_track(candidate,"Imported MIDI",&track); - if(r==XTC_OK)r=xtc_project_import_midi(candidate,track,start,path,&clip); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK)xtc_project_destroy(candidate); - else{sequencer_->select(track,clip);mixer_->select_track(track);mark_dirty();refresh_status();} - return r; -} -void MainWindow::import_midi_dialog() -{ - if(recorder_){report_error("Stop recording before importing MIDI",XTC_ERR_INVALID_STATE);return;} - UiNative_File_Chooser chooser;chooser.title("Import MIDI notes (keeps project tempo)");chooser.type(UiNative_File_Chooser::BROWSE_FILE);chooser.filter("Standard MIDI File\t*.{mid,midi}"); - if(chooser.show()!=0)return; - auto r=import_midi_file(chooser.filename());if(r!=XTC_OK)report_error("MIDI import failed (type 0/1 PPQN notes required)",r); - else set_label(error_,"Imported MIDI notes at cursor; file tracks merged and project tempo retained. Undo reverses import."); -} -xtc_result MainWindow::export_midi_clip(const char *path) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - auto ids=sequencer_->selected_clips();if(ids.size()!=1)return XTC_ERR_INVALID_ARGUMENT; - return xtc_project_export_midi_clip(project_.get(),ids[0],path); -} -void MainWindow::export_midi_dialog() -{ - auto ids=sequencer_->selected_clips();xtc_clip_info clip{}; - if(ids.size()!=1 || xtc_project_find_clip(project_.get(),ids[0],&clip)!=XTC_OK || !clip.midi){report_error("Select one MIDI clip to export",XTC_ERR_INVALID_ARGUMENT);return;} - UiNative_File_Chooser chooser;chooser.title("Export selected MIDI clip");chooser.type(UiNative_File_Chooser::BROWSE_SAVE_FILE); - chooser.filter("Standard MIDI File\t*.mid");chooser.options(UiNative_File_Chooser::SAVEAS_CONFIRM);chooser.preset_file("clip.mid"); - if(chooser.show()!=0)return; - auto r=export_midi_clip(chooser.filename());if(r!=XTC_OK)report_error("MIDI export failed",r);else set_label(error_,"MIDI clip exported (notes, channels, velocities and tempo)."); -} -void MainWindow::export_audio() -{ - if(recorder_){report_error("Stop recording before export",XTC_ERR_INVALID_STATE);return;} - const char *answer=ui_input("Export stereo WAV to a new absolute path (tail configured in Preferences)",""); - if(!answer)return; - std::string path=answer;xtc_export *job=nullptr; - xtc_result r=xtc_export_create(project_.get(),path.c_str(),static_cast(settings_.offline_rate),static_cast(settings_.tail_ms),&job); - if(r==XTC_OK) { - UiDouble_Window dialog(460,130,"Export WAV");dialog.set_modal();dialog.begin(); - UiBox progress(12,12,436,45,"Preparing export");UiButton cancel(330,80,110,30,"Cancel export"); - cancel.callback([](UiWidget *,void *c){static_cast(c)->hide();},&dialog); - dialog.callback([](UiWidget *,void *c){static_cast(c)->hide();},&dialog);dialog.end();xtc_ui::style(&dialog);dialog.show(); - int done=0; - while(dialog.shown() && !done && r==XTC_OK) { - r=xtc_export_step(job,&done);uint64_t position=0,total=0;xtc_export_progress(job,&position,&total); - std::string text=std::to_string(position)+" / "+std::to_string(total)+" frames";progress.copy_label(text.c_str());Ui::check(); - } - if(r==XTC_OK && !done)set_label(status_,"Export cancelled; destination retained"); - else if(r==XTC_OK)set_label(status_,"Export complete"); - dialog.hide(); - } - xtc_export_destroy(job); - if(r!=XTC_OK)report_error("Export failed; destination must be new and media available",r); -} -xtc_result MainWindow::load_sampler(uint64_t track,const char *path) -{ - if(!path || path[0]!='/' || std::strlen(path)>=sizeof(xtc_plugin_spec{}.sample_path))return XTC_ERR_INVALID_ARGUMENT; - xtc_plugin_spec spec{};xtc_result r=xtc_project_get_instrument(project_.get(),track,&spec); - if(r!=XTC_OK || spec.format!=XTC_PLUGIN_BUILTIN || spec.index!=4)r=xtc_plugin_probe(XTC_BUILTIN_PATH,4,sample_rate(),&spec); - if(r!=XTC_OK)return r; - std::strcpy(spec.sample_path,path);return set_instrument_spec(track,spec); -} -xtc_result MainWindow::set_instrument_spec(uint64_t track,const xtc_plugin_spec &spec) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - uint64_t clip=sequencer_->selected_clip();xtc_project *candidate=nullptr; - xtc_result r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_set_instrument(candidate,track,&spec); - if(r==XTC_OK)r=xtc_history_reserve(history_.get()); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK)xtc_project_destroy(candidate); - else {sequencer_->select(track,clip);mixer_->select_track(track);mark_dirty();} - return r; -} -xtc_result MainWindow::select_live_instrument(uint64_t track) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_set_live_instrument(candidate,track); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty(); - return r; -} -void MainWindow::instrument_callback(UiWidget *widget, void *context) -{ - auto *w = static_cast(context); - if (w->recorder_) { w->report_error("Stop recording before instrument changes",XTC_ERR_INVALID_STATE); return; } - auto *menu = dynamic_cast(widget); - const char *label = menu ? menu->mvalue()->label() : "Load instrument plugin"; - uint64_t track = w->sequencer_->selected_track(); - xtc_result r; - if(std::strcmp(label,"Load sampler WAV")==0) { - const char *path=ui_input("Sampler WAV path (absolute; replaces/relinks selected track sample)",""); - if(path) {r=w->load_sampler(track,path);if(r!=XTC_OK)w->report_error("Sampler preparation failed",r);} - return; - } - if(std::strcmp(label,"Selected instrument controls")==0 || std::strcmp(label,"Selected instrument automation")==0 || - std::strcmp(label,"Save instrument preset")==0 || std::strcmp(label,"Load instrument preset")==0) { - xtc_plugin_spec spec{}; - if(std::strcmp(label,"Load instrument preset")==0) { - const char *path=ui_input("Instrument preset path (absolute)","");if(!path)return; - r=xtc_plugin_preset_load(path,&spec); - if(r==XTC_OK)r=w->set_instrument_spec(track,spec); - } else { - r=xtc_project_get_instrument(w->project_.get(),track,&spec); - if(r==XTC_OK && spec.format==XTC_PLUGIN_BUILTIN && !spec.control_count) - r=xtc_plugin_probe(XTC_BUILTIN_PATH,spec.index,w->sample_rate(),&spec); - if(r==XTC_OK && std::strcmp(label,"Save instrument preset")==0) { - const char *path=ui_input("Save instrument preset path (absolute)","");if(!path)return; - r=xtc_plugin_preset_save(&spec,path); - } else if(r==XTC_OK && std::strcmp(label,"Selected instrument controls")==0) { - xtc_plugin_details details{};xtc_plugin_spec edited{}; - r=xtc_plugin_describe(&spec,w->sample_rate(),&details); - if(r==XTC_OK && xtc_edit_plugin(spec,details,edited))r=w->set_instrument_spec(track,edited); - } else if(r==XTC_OK) { - xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{};r=xtc_plugin_parameters(&spec,w->sample_rate(),info); - std::string prompt="Instrument automation: port tick mode value (mode -1 removes)\n"; - for(unsigned int i=0;i1)r=XTC_ERR_INVALID_ARGUMENT; - else { - unsigned int i=0;for(;i(tick),port,static_cast(mode),value};} - if(r==XTC_OK)r=w->set_instrument_spec(track,spec); - } - } - } - } - if(r!=XTC_OK)w->report_error("Instrument edit failed; check track, preset and control ranges",r); - return; - } - if (std::strcmp(label,"Use built-in instrument") == 0) r = w->assign_instrument(track,nullptr,0); - else if (std::strcmp(label,"Live MIDI to selected instrument") == 0) r = w->select_live_instrument(track); - else if (std::strcmp(label,"Live MIDI to built-in sine") == 0) r = w->select_live_instrument(UINT64_MAX); - else { - std::string module; if(!xtc_choose_plugin_file(w->settings_,module)) return; - unsigned int index=0; - if(!xtc_choose_plugin(module.c_str(),w->sample_rate(),index,true))return; - r=w->assign_instrument(track,module.c_str(),index); - } - if (r != XTC_OK) w->report_error("Instrument assignment failed; select a non-bus track and compatible mono instrument",r); -} - -xtc_result MainWindow::set_control_map(unsigned int channel, const unsigned int controllers[3]) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_set_control_map(candidate,channel,controllers); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty(); - return r; -} -void MainWindow::control_map_callback(UiWidget *, void *context) -{ - auto *w = static_cast(context); - if (w->recorder_) { w->report_error("Stop recording before controller mapping",XTC_ERR_INVALID_STATE); return; } - unsigned int controllers[3]{}; - std::string prompt = "Live sine MIDI bindings (channels 0..15; 128 disables). CC120/123 reserved.\n"; - for (unsigned int channel = 0; channel < 16; ++channel) { - xtc_project_get_control_map(w->project_.get(),channel,controllers); - prompt += std::to_string(channel) + ": volume " + std::to_string(controllers[0]) + - ", pan " + std::to_string(controllers[1]) + ", sustain " + std::to_string(controllers[2]) + "\n"; - } - prompt += "Enter channel volume_CC pan_CC sustain_CC (distinct enabled CCs):"; - const char *answer = ui_input("%s","0 7 10 64",prompt.c_str()); - if (!answer) return; - unsigned int channel; char extra; - xtc_result r = std::sscanf(answer,"%u %u %u %u %c",&channel,controllers,controllers+1,controllers+2,&extra) == 4 ? - w->set_control_map(channel,controllers) : XTC_ERR_INVALID_ARGUMENT; - if (r != XTC_OK) w->report_error("Controller mapping rejected: check channel, unique CCs and reserved panic controllers",r); -} - -xtc_result MainWindow::install_plugin_graph(xtc_graph *graph) -{ - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - if (r == XTC_OK) r = xtc_project_set_graph(candidate,graph); - if (r == XTC_OK) r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) r = install_project(candidate,false); - if (r != XTC_OK) xtc_project_destroy(candidate); - else mark_dirty(); - return r; -} -xtc_result MainWindow::add_plugin(const char *path, unsigned int index, size_t *id) -{ - BusyOperation busy(*this,status_,"Loading plugin and preparing playback…"); - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); - xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_spec spec{}; - xtc_result r = xtc_plugin_probe(path,index,rate,&spec); - xtc_graph *graph = nullptr; size_t added = SIZE_MAX; - if (r == XTC_OK) r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph); - if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added); - xtc_plan *check = nullptr; - if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check); - xtc_plan_destroy(check); - if (r == XTC_OK) r = install_plugin_graph(graph); - xtc_graph_destroy(graph); - if (r == XTC_OK) graph_widget_->select_node(added); - if (r == XTC_OK && id) *id = added; - return r; -} -xtc_result MainWindow::replace_plugin(size_t id, const char *path, unsigned int index) -{ - BusyOperation busy(*this,status_,"Replacing plugin and preparing playback…"); - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_spec spec{}; - xtc_result r = xtc_plugin_probe(path,index,rate,&spec); - if (r == XTC_OK) r = apply_plugin_spec(id,spec,true); - if (r == XTC_OK) graph_widget_->select_node(id); - return r; -} -xtc_result MainWindow::reset_plugin(size_t id, unsigned int port) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_spec spec{}; - xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec); - if (r == XTC_OK) r = xtc_plugin_reset(&spec,rate,port); - return r == XTC_OK ? apply_plugin_spec(id,spec) : r; -} -xtc_result MainWindow::insert_plugin(const xtc_connection_info &wire, const char *path, unsigned int index, size_t *id) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_spec spec{}; - xtc_result r = xtc_plugin_probe(path,index,rate,&spec); - if (r == XTC_OK && !spec.inputs) r = XTC_ERR_UNSUPPORTED; - xtc_graph *graph = nullptr; size_t added = SIZE_MAX; - xtc_graph_edit edit{}; edit.kind = XTC_EDIT_DISCONNECT; edit.data.connection = wire; - if (r == XTC_OK) r = xtc_graph_apply_edit(xtc_project_graph(project_.get()),&edit,&graph,nullptr); - if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added); - if (r == XTC_OK) r = xtc_graph_connect_ports(graph,wire.source_node,wire.source_port,added,0); - if (r == XTC_OK) r = xtc_graph_connect_ports(graph,added,1,wire.destination_node,wire.destination_port); - xtc_plan *check = nullptr; - if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check); - xtc_plan_destroy(check); - if (r == XTC_OK) r = install_plugin_graph(graph); - xtc_graph_destroy(graph); - if (r == XTC_OK) { graph_widget_->select_node(added); if (id) *id = added; } - return r; -} -xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float value) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - xtc_plugin_spec spec{}; - xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec); - if (r != XTC_OK) return r; - size_t i; - for (i = 0; i < spec.control_count && spec.ports[i] != port; ++i) {} - if (i == spec.control_count) return XTC_ERR_NOT_FOUND; - spec.values[i] = value; - return apply_plugin_spec(id,spec); -} -xtc_result MainWindow::set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove) -{ - if (recorder_) return XTC_ERR_INVALID_STATE; - double rate = sample_rate(); xtc_backend_status status{}; - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(),&candidate); - xtc_parameter_key key{XTC_PARAMETER_PLUGIN,static_cast(id),point.port}; - if (r == XTC_OK) r = xtc_project_automate(candidate,key,rate,point.tick,point.step,point.value,remove ? 1 : 0); - xtc_plugin_spec spec{}; - if (r == XTC_OK) r = xtc_graph_get_plugin(xtc_project_graph(candidate),id,&spec); - xtc_project_destroy(candidate); - return r == XTC_OK ? apply_plugin_spec(id,spec) : r; -} -xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace) -{ - xtc_graph *graph = nullptr; - xtc_result r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph); - if (r == XTC_OK) r = replace ? xtc_graph_replace_plugin(graph,id,&spec) : xtc_graph_set_plugin(graph,id,&spec); - /* Compile before adoption even with a mocked/disconnected backend. */ - xtc_plan *check = nullptr; - xtc_backend_status status{}; double rate = sample_rate(); - if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate; - if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check); - xtc_plan_destroy(check); - if (r == XTC_OK) r = install_plugin_graph(graph); - xtc_graph_destroy(graph); return r; -} -void MainWindow::plugin_callback(UiWidget *widget, void *context) -{ - auto *w = static_cast(context); - if (w->recorder_) { w->report_error("Stop recording before plugin changes",XTC_ERR_INVALID_STATE); return; } - auto *menu = static_cast(widget); - const char *label = menu->mvalue()->label(); - xtc_result r; - if(std::strcmp(label,"Insert stereo delay on wire")==0 || std::strcmp(label,"Insert stereo reverb on wire")==0) { - xtc_connection_info wire{}; - r=w->graph_widget_->selected_connection(wire) ? w->insert_plugin(wire,XTC_BUILTIN_PATH,std::strcmp(label,"Insert stereo delay on wire")==0 ? 5U : 6U) : XTC_ERR_INVALID_STATE; - } else if(std::strcmp(label,"Add stereo delay")==0 || std::strcmp(label,"Add stereo reverb")==0) { - r=w->add_plugin(XTC_BUILTIN_PATH,std::strcmp(label,"Add stereo delay")==0 ? 5U : 6U); - } else if (std::strcmp(label,"Insert internal delay on wire")==0 || std::strcmp(label,"Insert internal reverb on wire")==0) { - xtc_connection_info wire{}; - r=w->graph_widget_->selected_connection(wire) ? w->insert_plugin(wire,XTC_BUILTIN_PATH,std::strcmp(label,"Insert internal delay on wire")==0 ? 2U : 3U) : XTC_ERR_INVALID_STATE; - } else if (std::strcmp(label,"Add internal delay") == 0 || std::strcmp(label,"Add internal reverb") == 0) { - size_t id=0; r=w->add_plugin(XTC_BUILTIN_PATH, std::strcmp(label,"Add internal delay")==0 ? 2U : 3U, &id); - } else if (std::strcmp(label,"Reset selected plugin") == 0) { - r = w->reset_plugin(w->graph_widget_->selected_node()); - } else if (std::strcmp(label,"Load plugin") == 0 || - std::strcmp(label,"Replace selected plugin") == 0 || - std::strcmp(label,"Insert plugin on selected wire") == 0) { - const size_t selected = w->graph_widget_->selected_node(); - xtc_connection_info wire{}; - const bool insert = std::strcmp(label,"Insert plugin on selected wire") == 0; - const bool replace = std::strcmp(label,"Replace selected plugin") == 0; - if (insert && !w->graph_widget_->selected_connection(wire)) { - w->report_error("Select an existing wire before inserting an effect",XTC_ERR_INVALID_STATE); return; - } - if (replace) { - xtc_plugin_spec old{}; - r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),selected,&old); - if (r != XTC_OK) { w->report_error("Select a plugin node to replace",r); return; } - } - std::string module; if(!xtc_choose_plugin_file(w->settings_,module))return; - double rate = w->sample_rate(); xtc_backend_status status{}; - if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate; - unsigned int index = 0; - xtc_plugin_spec probe{}; - r = xtc_plugin_probe(module.c_str(),0,rate,&probe); - if (r != XTC_OK && r != XTC_ERR_UNSUPPORTED) { - w->report_error("Cannot discover plugin; check binary path, format, mono layout and dependencies",r); return; - } - if (!xtc_choose_plugin(module.c_str(),rate,index)) return; - if (replace) r = w->replace_plugin(selected,module.c_str(),index); - else if (insert) r = w->insert_plugin(wire,module.c_str(),index); - else r = w->add_plugin(module.c_str(),index); - } else { - size_t id = w->graph_widget_->selected_node(); xtc_plugin_spec spec{}; - r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),id,&spec); - if (r == XTC_OK) { - std::string prompt = std::string(spec.label) + " control ports and values:\n"; - if (std::strcmp(label,"Selected plugin control") == 0) { - double rate = w->sample_rate(); xtc_backend_status status{}; - if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate; - xtc_plugin_details details{}; xtc_plugin_spec edited{}; - r = xtc_plugin_describe(&spec,rate,&details); - if (r == XTC_OK && xtc_edit_plugin(spec,details,edited)) r = w->apply_plugin_spec(id,edited); - if (r != XTC_OK) w->report_error("Plugin editor failed; configuration retained. Check module identity and control ranges",r); - return; - } - xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{}; - double rate = w->sample_rate(); xtc_backend_status status{}; - if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate; - r = xtc_plugin_parameters(&spec,rate,info); - if (r != XTC_OK) { w->report_error("Read plugin controls failed",r); return; } - for (unsigned i = 0; i < spec.control_count; ++i) - prompt += std::to_string(spec.ports[i]) + ": " + info[i].name + " = " + std::to_string(spec.values[i]) + - " [" + std::to_string(info[i].minimum) + ", " + std::to_string(info[i].maximum) + "]\n"; - if (std::strcmp(label,"Selected plugin automation") == 0) { - prompt += "Enter port tick mode value; mode 0 linear, 1 step, -1 remove.\nCurrent tick: " + - std::to_string(w->sequencer_->position()); - const char *answer = ui_input("%s","",prompt.c_str()); - if (!answer) return; - unsigned port; unsigned long long tick; int mode; float value; char extra; - if (std::sscanf(answer,"%u %llu %d %f %c",&port,&tick,&mode,&value,&extra) != 4 || mode < -1 || mode > 1) - r = XTC_ERR_INVALID_ARGUMENT; - else { - xtc_plugin_point point{static_cast(tick),port,mode == 1 ? 1U : 0U,value}; - r = w->set_plugin_automation(id,point,mode == -1); - } - if (r != XTC_OK) w->report_error("Plugin automation rejected: check tick, port, range and discrete mode",r); - return; - } - } - } - if (r != XTC_OK) w->report_error("Plugin change failed: check module, mono layout, control port/range and JACK format",r); -} - -void MainWindow::project_defaults(xtc_project *project) -{ - (void)xtc_project_set_tempo(project,static_cast(settings_.tempo)); - unsigned int cc[3]={static_cast(settings_.volume_cc),static_cast(settings_.pan_cc),static_cast(settings_.sustain_cc)}; - for(unsigned int channel=0;channel<16;++channel)(void)xtc_project_set_control_map(project,channel,cc); -} -double MainWindow::sample_rate() const -{ - xtc_backend_status status{}; - return ready_ && xtc_backend_get_status(backend_,&status)==XTC_OK ? status.sample_rate : static_cast(settings_.offline_rate); -} -xtc_result MainWindow::configure_loop(const xtc_project *project,const xtc_settings &settings) -{ - if(!ready_)return XTC_OK; - uint64_t start,end;unsigned int rate=static_cast(sample_rate()); - xtc_result r=xtc_project_tick_to_sample(project,static_cast(settings.loop_start),rate,&start); - if(r==XTC_OK)r=xtc_project_tick_to_sample(project,static_cast(settings.loop_end),rate,&end); - if(r==XTC_OK)r=xtc_backend_set_loop(backend_,start,end,settings.loop_enabled!=0); - return r; -} -xtc_result MainWindow::set_loop(uint64_t start,uint64_t end,bool enabled) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - if(start>=end || end>XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT; - xtc_settings candidate=settings_;candidate.loop_start=static_cast(start);candidate.loop_end=static_cast(end);candidate.loop_enabled=enabled; - bool resume=running_;xtc_result r=ready_ ? xtc_backend_stop(backend_) : XTC_OK; - if(r==XTC_OK)r=configure_loop(project_.get(),candidate); - if(r==XTC_OK) {settings_=candidate;sequencer_->loop_region(start,end,enabled);} - if(ready_ && resume && r==XTC_OK) {xtc_result restarted=start_configured_backend();if(r==XTC_OK)r=restarted;} - refresh_status();return r; -} -void MainWindow::collect_preferences() -{ - settings_.sequencer_detached=editors_[0].detached;settings_.graph_detached=editors_[1].detached;settings_.piano_detached=piano_detached_; - settings_.track_header_width=sequencer_->header_width(); - for(unsigned int i=0;i<2;++i)if(editors_[i].window) { - auto *p=editors_[i].window.get(); - if(i==0){settings_.sequencer_x=p->x();settings_.sequencer_y=p->y();settings_.sequencer_width=p->w();settings_.sequencer_height=p->h();} - else{settings_.graph_x=p->x();settings_.graph_y=p->y();settings_.graph_width=p->w();settings_.graph_height=p->h();} - } - if(piano_window_){settings_.piano_x=piano_window_->x();settings_.piano_y=piano_window_->y();} - settings_.transport_detached=transport_detached_;settings_.mixer_detached=mixer_detached_; - if(transport_window_) {settings_.transport_x=transport_window_->x();settings_.transport_y=transport_window_->y();settings_.transport_width=transport_window_->w();settings_.transport_height=transport_window_->h();} - if(mixer_window_) {settings_.mixer_x=mixer_window_->x();settings_.mixer_y=mixer_window_->y();settings_.mixer_width=mixer_window_->w();settings_.mixer_height=mixer_window_->h();} - settings_.main_x=x();settings_.main_y=y();settings_.inspector_visible=sidebar_->visible(); - settings_.width=w();settings_.height=h();settings_.sequencer_snap=static_cast(sequencer_->grid());settings_.sequencer_zoom=sequencer_->zoom_level(); - if(piano_) { - settings_.piano_width=piano_window_->w();settings_.piano_height=piano_window_->h();settings_.piano_snap=static_cast(piano_->grid()); - settings_.filter_channel=piano_->filter_channel();settings_.filter_low=piano_->filter_low();settings_.filter_high=piano_->filter_high(); - settings_.note_ticks=static_cast(piano_->new_note_ticks);settings_.note_velocity=piano_->new_note_velocity;settings_.note_channel=piano_->new_note_channel; - } -} -void MainWindow::apply_ui_preferences() -{ - bool seq=settings_.sequencer_detached!=0,graph=settings_.graph_detached!=0,piano=settings_.piano_detached!=0; - detach_editor(0,seq);detach_editor(1,graph);detach_piano(piano); - sequencer_->header_width(static_cast(settings_.track_header_width)); - for(unsigned int i=0;i<2;++i)if(editors_[i].window)restore_panel(editors_[i].window.get(),static_cast(i==0 ? settings_.sequencer_x : settings_.graph_x),static_cast(i==0 ? settings_.sequencer_y : settings_.graph_y),static_cast(i==0 ? settings_.sequencer_width : settings_.graph_width),static_cast(i==0 ? settings_.sequencer_height : settings_.graph_height),i==0 ? 1100 : 1000,420); - bool floating_transport=settings_.transport_detached!=0,floating_mixer=settings_.mixer_detached!=0; - detach_transport(floating_transport);detach_mixer(floating_mixer); - if(transport_window_)restore_panel(transport_window_.get(),static_cast(settings_.transport_x),static_cast(settings_.transport_y),static_cast(settings_.transport_width),static_cast(settings_.transport_height),400,130); - if(mixer_window_)restore_panel(mixer_window_.get(),static_cast(settings_.mixer_x),static_cast(settings_.mixer_y),static_cast(settings_.mixer_width),static_cast(settings_.mixer_height),600,420); - follow_->value(static_cast(settings_.follow_playhead)); - restore_panel(this,static_cast(settings_.main_x),static_cast(settings_.main_y),static_cast(settings_.width),static_cast(settings_.height),1220,720); - show_track_inspector(settings_.inspector_visible!=0); - sequencer_->grid(static_cast(settings_.sequencer_snap));sequencer_->set_zoom(static_cast(settings_.sequencer_zoom)); - sequencer_->new_clip_ticks=static_cast(settings_.clip_ticks); - sequencer_->loop_region(static_cast(settings_.loop_start),static_cast(settings_.loop_end),settings_.loop_enabled!=0); - if(piano_) { - restore_panel(piano_window_.get(),static_cast(settings_.piano_x),static_cast(settings_.piano_y),static_cast(settings_.piano_width),static_cast(settings_.piano_height),800,520); - piano_->set_grid(static_cast(settings_.piano_snap));piano_->set_filter(static_cast(settings_.filter_channel),static_cast(settings_.filter_low),static_cast(settings_.filter_high)); - piano_->new_note_ticks=static_cast(settings_.note_ticks);piano_->new_note_velocity=static_cast(settings_.note_velocity);piano_->new_note_channel=static_cast(settings_.note_channel); - } -} -xtc_result MainWindow::apply_preferences(const xtc_settings &candidate) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - xtc_result r=xtc_settings_validate(&candidate);if(r!=XTC_OK)return r; - if(configuration_.empty())return XTC_ERR_INVALID_STATE; - if(ready_)r=xtc_backend_stop(backend_); - if(r==XTC_OK)r=configure_loop(project_.get(),candidate); - if(r==XTC_OK)r=xtc_settings_save(&candidate,configuration_.c_str()); - if(r!=XTC_OK) {if(ready_)(void)configure_loop(project_.get(),settings_);refresh_status();return r;} - settings_=candidate;configuration_valid_=true;apply_ui_preferences();refresh_status();return XTC_OK; -} -xtc_result MainWindow::save_preferences() -{ - if(configuration_.empty() || !configuration_valid_)return XTC_ERR_INVALID_STATE; - collect_preferences();return xtc_settings_save(&settings_,configuration_.c_str()); -} -std::vector MainWindow::midi_devices(bool input) -{ - std::vector ports; - if(ready_)(void)xtc_backend_visit_midi_ports(backend_,input ? XTC_PORT_OUTPUT : XTC_PORT_INPUT, - [](const char *name,void *c){static_cast *>(c)->emplace_back(name);return 0;},&ports); - return ports; -} -xtc_result MainWindow::start_configured_backend() -{ - midi_route_warning_.clear(); - const char *input=settings_.midi_input_port, *output=settings_.midi_output_port; - // Saved production devices are optional. Keep their names so a later start - // reconnects them, but do not ask the strict backend API to route absent ports. - auto available=[this](const char *name, xtc_port_direction direction, bool &found) { - struct Search { const char *name; bool found; } search{name,false}; - xtc_result result=xtc_backend_visit_midi_ports(backend_,direction, - [](const char *port,void *context) { - auto &search=*static_cast(context); - search.found=std::strcmp(port,search.name)==0; - return search.found ? 1 : 0; - },&search); - found=search.found; - return result; - }; - auto optional_route=[&](const char *&name,xtc_port_direction direction) { - if(!*name)return XTC_OK; - bool found=false; - xtc_result result=available(name,direction,found); - if(result==XTC_OK && !found) { - if(midi_route_warning_.empty())midi_route_warning_="MIDI device unavailable: "; - else midi_route_warning_+=", "; - midi_route_warning_+=name; - name=""; - } - return result; - }; - xtc_result r=xtc_backend_set_midi_options(backend_,static_cast(settings_.midi_input_channel),static_cast(settings_.midi_output_channel),settings_.midi_thru!=0); - if(r==XTC_OK)r=optional_route(input,XTC_PORT_OUTPUT); - if(r==XTC_OK)r=optional_route(output,XTC_PORT_INPUT); - if(r==XTC_OK)r=xtc_backend_start_routed(backend_,input,output); - if(r==XTC_OK) { - if(!midi_route_warning_.empty())midi_route_warning_+=". Playback continues; saved device choices are kept."; - set_label(error_,midi_route_warning_.c_str()); - } - return r; -} -void MainWindow::show_preferences() -{ - if(recorder_) {report_error("Stop recording before preferences",XTC_ERR_INVALID_STATE);return;} - collect_preferences();PreferencesDialog dialog(settings_,configuration_,[this](const xtc_settings &s){return apply_preferences(s);},[this](bool input){return midi_devices(input);});dialog.run(); -} - -xtc_result MainWindow::set_tempo(unsigned int milli_bpm) -{ - if (recorder_) - return XTC_ERR_INVALID_STATE; - if (milli_bpm < 20000 || milli_bpm > 300000) - return XTC_ERR_INVALID_ARGUMENT; - if (milli_bpm == xtc_project_tempo(project_.get())) - return XTC_OK; - uint64_t track = sequencer_->selected_track(), clip = sequencer_->selected_clip(), mix = mixer_->selected_track(); - uint64_t piano_clip = piano_ ? piano_->clip() : UINT64_MAX, note = piano_ ? piano_->selected_note() : UINT64_MAX; - bool piano_visible = piano_window_ && piano_window_->shown(); - xtc_project *candidate = nullptr; - xtc_result r = xtc_project_clone(project_.get(), &candidate); - if (r == XTC_OK) - r = xtc_project_set_tempo(candidate, milli_bpm); - if (r == XTC_OK) - r = xtc_history_reserve(history_.get()); - if (r == XTC_OK) - r = install_project(candidate, false); - if (r != XTC_OK) - xtc_project_destroy(candidate); - else { - sequencer_->select(track, clip); - mixer_->select_track(mix); - xtc_clip_info info{}; - if (piano_ && xtc_project_find_clip(project_.get(), piano_clip, &info) == XTC_OK) { - piano_->set_project(project_.get(), piano_clip); - piano_->select_note(note); - if (piano_visible) - piano_window_->show(); - } - mark_dirty(); - } - return r; -} -void MainWindow::inspector_callback(UiWidget *widget, void *context) -{ - auto *w = static_cast(context); - const uint64_t id = w->inspected_track_; - float gain = static_cast(w->inspector_gain_->value()), pan = static_cast(w->inspector_pan_->value()); - int mute = w->inspector_mute_->value(), solo = w->inspector_solo_->value(); - // Editing a single field must not commit another field's unfinished input. - size_t count = 0, clips = 0; - xtc_project_get_counts(w->project_.get(), &count, &clips); - for (size_t i = 0; i < count; ++i) { - xtc_track_info track{}; - xtc_project_get_track(w->project_.get(), i, &track); - if (track.id != id) - continue; - if (widget != w->inspector_gain_) - gain = track.gain; - if (widget != w->inspector_pan_) - pan = track.pan; - if (widget != w->inspector_mute_) - mute = track.mute; - if (widget != w->inspector_solo_) - solo = track.solo; - if (gain != track.gain || pan != track.pan || mute != track.mute || solo != track.solo) - if (w->mixer_->select_track(id)) - w->mixer_->mix(gain, pan, mute, solo); - break; - } - w->inspected_track_ = UINT64_MAX; - w->refresh_status(); -} -xtc_result MainWindow::mix_snapshot(unsigned int slot,bool capture) -{ - if(slot>=2)return XTC_ERR_INVALID_ARGUMENT; - if(capture) { - xtc_mix_snapshot *snapshot=nullptr;xtc_result r=xtc_mix_snapshot_create(project_.get(),&snapshot); - if(r==XTC_OK)mix_snapshots_[slot].reset(snapshot); - return r; - } - if(!mix_snapshots_[slot])return XTC_ERR_NOT_FOUND; - xtc_result r=before_arrangement_edit(this); - if(r==XTC_OK)r=xtc_mix_snapshot_recall(mix_snapshots_[slot].get(),project_.get()); - if(r==XTC_OK)mark_dirty(); - return r; -} -xtc_result MainWindow::navigate_marker(int direction) -{ - xtc_marker marker{};if(recorder_)return XTC_ERR_INVALID_STATE; - xtc_result r=xtc_project_adjacent_marker(project_.get(),sequencer_->position(),direction,&marker); - if(r==XTC_OK && ready_)r=xtc_backend_stop(backend_); - if(r==XTC_OK)r=seek(this,marker.tick); - if(r==XTC_OK)sequencer_->position(marker.tick); - return r; -} -xtc_result MainWindow::loop_section() -{ - uint64_t begin=0,end=0;xtc_result r=xtc_project_section(project_.get(),sequencer_->position(),&begin,&end); - return r==XTC_OK ? set_loop(begin,end,true) : r; -} -xtc_result MainWindow::collect_project(const char *directory) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - BusyOperation busy(*this,status_,"Collecting project media…"); - return xtc_project_collect(project_.get(),directory); -} -xtc_result MainWindow::adopt_sample_copy(uint64_t id,const xtc_sample *sample,const char *path) -{ - if(!sample||!path)return XTC_ERR_INVALID_ARGUMENT; - auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; - xtc_clip_info clip{};r=xtc_project_find_clip(project_.get(),id,&clip);if(r!=XTC_OK)return r; - if(clip.midi||!clip.asset_path[0])return XTC_ERR_INVALID_STATE; - xtc_sample *original=nullptr;r=xtc_sample_load(clip.asset_path,&original);if(r!=XTC_OK)return r; - unsigned int old_rate=0,new_rate=0;size_t old_frames=0,new_frames=0; - xtc_sample_info(original,&old_rate,&old_frames);xtc_sample_info(sample,&new_rate,&new_frames);xtc_sample_destroy(original); - if(old_rate!=new_rate||old_frames!=new_frames)return XTC_ERR_INVALID_ARGUMENT; - xtc_project *candidate=nullptr;r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_set_clip_asset(candidate,id,path); - bool written=false; - if(r==XTC_OK){r=xtc_sample_write(sample,0,new_frames,path);written=r==XTC_OK;} - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK){if(written)std::remove(path);xtc_project_destroy(candidate);return r;} - sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK; -} -xtc_result MainWindow::slice_sample_clip(uint64_t id,unsigned int rate,const std::vector &positions) -{ - auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; - xtc_project *candidate=nullptr;size_t count=0;r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_slice_source_frames(candidate,id,rate,positions.data(),positions.size(),&count); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK){xtc_project_destroy(candidate);return r;} - xtc_clip_info clip{};xtc_project_find_clip(project_.get(),id,&clip);sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK; -} -xtc_result MainWindow::trim_sample_clip(uint64_t id,unsigned int rate,size_t begin,size_t end) -{ - auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; - xtc_project *candidate=nullptr;r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_trim_source_frames(candidate,id,rate,begin,end); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK){xtc_project_destroy(candidate);return r;} - xtc_clip_info clip{};xtc_project_find_clip(project_.get(),id,&clip);sequencer_->select(clip.track_id,id);mark_dirty();return XTC_OK; -} -xtc_result MainWindow::open_sample_editor() -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - xtc_clip_info clip{};auto r=xtc_project_find_clip(project_.get(),sequencer_->selected_clip(),&clip); - if(r!=XTC_OK)return r; - if(clip.midi||!clip.asset_path[0])return XTC_ERR_INVALID_STATE; - xtc_sample *sample=nullptr;r=xtc_sample_load(clip.asset_path,&sample);if(r!=XTC_OK)return r; - try { - SampleEditor editor(SampleEditor::Data(sample,xtc_sample_destroy)); - uint64_t first=0,last=0;unsigned int rate;size_t frames;xtc_sample_info(sample,&rate,&frames); - if(xtc_project_tick_to_sample(project_.get(),clip.offset_tick,rate,&first)==XTC_OK && xtc_project_tick_to_sample(project_.get(),clip.offset_tick+clip.length_tick,rate,&last)==XTC_OK) - editor.select(static_cast(std::min(first,static_cast(frames))),static_cast(std::min(last,static_cast(frames)))); - editor.slice=[this,id=clip.id](unsigned int sr,const std::vector &positions){return slice_sample_clip(id,sr,positions);}; - editor.adopt=[this,id=clip.id](const xtc_sample *data,const char *path){return adopt_sample_copy(id,data,path);}; - editor.trim=[this,id=clip.id](unsigned int sr,size_t b,size_t e){return trim_sample_clip(id,sr,b,e);}; - editor.run();return XTC_OK; - } catch(const std::bad_alloc &){return XTC_ERR_OUT_OF_MEMORY;} -} -void MainWindow::show_commands() -{ - std::vector commands; - for (int i = 0; i < menu_->size(); ++i) { - const auto &item = menu_->menu()[i]; - if (!item.label() || item.submenu() || !item.active()) - continue; - char path[512]; - if (menu_->item_pathname(path, sizeof(path), &item) == 0) - commands.emplace_back(path); - } - std::string selected; - { - CommandPalette palette(std::move(commands)); - selected = palette.run(); - } - if (!selected.empty()) { - auto *item = menu_->find_item(selected.c_str()); - if (item && item->active()) - menu_->picked(item); - } -} -void MainWindow::fit_song() -{ - size_t tracks = 0, clips = 0; - uint64_t end = 3840; - xtc_project_get_counts(project_.get(), &tracks, &clips); - for (size_t i = 0; i < clips; ++i) { - xtc_clip_info clip{}; - xtc_project_get_clip(project_.get(), i, &clip); - end = std::max(end, clip.start_tick + clip.length_tick + 960); - } - int scale = 24; - for (int candidate : {24, 48, 96, 192, 384}) - if (end * static_cast(candidate) / 960 <= - static_cast(std::max(1, timeline_scroll_->w() - 204))) - scale = candidate; - sequencer_->set_zoom(scale); - timeline_scroll_->scroll_to(0, 0); - timeline_scroll_->redraw(); -} -void MainWindow::follow_playhead() -{ - if (!running_ || !follow_->value() || tabs_->value() != tabs_->child(0) || Ui::pushed()) - return; - int pixel = static_cast(sequencer_->position() * static_cast(sequencer_->zoom_level()) / 960); - int screen = sequencer_->x() + sequencer_->header_width() + pixel; - if (screen > timeline_scroll_->x() + timeline_scroll_->w() - 48 || screen < timeline_scroll_->x() + sequencer_->header_width()) - timeline_scroll_->scroll_to(std::max(0, pixel - 160), timeline_scroll_->yposition()); -} - -xtc_result MainWindow::set_track_chain(uint64_t track,const xtc_track_chain &chain) -{ - auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; - xtc_project *candidate=nullptr;r=xtc_project_clone(project_.get(),&candidate); - if(r==XTC_OK)r=xtc_project_set_chain(candidate,track,&chain); - if(r==XTC_OK)r=install_project(candidate,false); - if(r!=XTC_OK){xtc_project_destroy(candidate);return r;} - mark_dirty();track_inserts_->refresh(true);return XTC_OK; -} -xtc_result MainWindow::add_track_effect(uint64_t track,const char *file,unsigned int index) -{ - xtc_track_chain chain{};auto r=xtc_project_get_chain(project_.get(),track,&chain); - if(r!=XTC_OK)return r; - if(chain.count==XTC_TRACK_INSERTS)return XTC_ERR_CAPACITY; - xtc_track_insert slot{};r=xtc_plugin_probe(file,index,sample_rate(),&slot.plugin); - if(r!=XTC_OK)return r; - slot.enabled=1;slot.wet=1;chain.slots[chain.count++]=slot; - return set_track_chain(track,chain); -} - -xtc_result MainWindow::open_track_inserts(uint64_t track) -{ - if(track==UINT64_MAX) { - bool mixer_context=tabs_->value()==mixer_page_ || - (mixer_detached_ && Ui::focus() && Ui::focus()->window()==mixer_window_.get()); - track=mixer_context ? mixer_->selected_track() : sequencer_->selected_track(); - } - size_t count=0,clips=0;xtc_project_get_counts(project_.get(),&count,&clips); - for(size_t i=0;iselect_track(track);tabs_->value(track_inserts_);tabs_->redraw();return XTC_OK; - } - return XTC_ERR_NOT_FOUND; -} - -xtc_result MainWindow::tap_tempo(double seconds) -{ - if(recorder_)return XTC_ERR_INVALID_STATE; - auto state=tap_state_;unsigned int tempo=0;auto r=xtc_tap_tempo_push(&state,seconds,&tempo); - if(r==XTC_ERR_NOT_FOUND){tap_state_=state;return r;} - if(r!=XTC_OK)return r; - r=set_tempo(tempo);if(r==XTC_OK)tap_state_=state;return r; -} diff --git a/src/gui/JUCE/main_window.hpp b/src/gui/JUCE/main_window.hpp deleted file mode 100644 index a700c28..0000000 --- a/src/gui/JUCE/main_window.hpp +++ /dev/null @@ -1,557 +0,0 @@ -#ifndef XTC_MAIN_WINDOW_HPP -#define XTC_MAIN_WINDOW_HPP - -#include "ui.hpp" -#include -#include -#include -#include "xtc/xtc_project.h" -#include "xtc/xtc_sample.h" -#include "xtc/xtc_editing.h" -#include "xtc/xtc_backend.h" -#include "xtc/xtc_mixer.h" -#include "xtc/xtc_plugin.h" -#include "xtc/xtc_tempo.h" -#include "xtc/xtc_control.h" -#include "xtc/xtc_settings.h" - -class GraphWidget; -class MixerWidget; -class PianoRollWidget; -class SequencerWidget; -class UiBox; -class UiGroup; -class UiButton; -class UiTabs; -class UiMenu_Bar; -class UiChoice; -class UiValue_Slider; -class UiValue_Input; -class UiScroll; -class UiCheck_Button; - -class TrackInsertsPanel; -class MainWindow : public UiDouble_Window { - public: - xtc_result set_track_chain(uint64_t track,const xtc_track_chain &chain); - xtc_result add_track_effect(uint64_t track,const char *file,unsigned int index); - TrackInsertsPanel *track_inserts() const {return track_inserts_;} - void show_track_inspector(bool visible); - xtc_result open_track_inserts(uint64_t track=UINT64_MAX); - /* One undoable candidate edit. MIDI also adds a one-bar clip at the cursor. - * Preparation stops/resets transport; recording and failed candidates retain - * the current document. No engine/model changes happen in audio callbacks. */ - xtc_result create_track(bool midi); - xtc_result set_tempo(unsigned int milli_bpm); - xtc_result tap_tempo(double seconds); - xtc_result collect_project(const char *directory); - xtc_result navigate_marker(int direction); - xtc_result loop_section(); - void show_commands(); - xtc_result mix_snapshot(unsigned int slot,bool capture); - /* Reparent the existing controls on the UI thread; closing a panel docks it. */ - void detach_editor(unsigned int index,bool detached); /* 0 sequencer, 1 graph */ - void detach_piano(bool detached); - void reset_layout(); - UiDouble_Window *editor_window(unsigned int index) const {return index<2 ? editors_[index].window.get() : nullptr;} - void layout_transport(); - void resize(int x,int y,int width,int height) override; - void detach_transport(bool detached); - void detach_mixer(bool detached); - UiDouble_Window *transport_window() const { return transport_window_.get(); } - UiDouble_Window *mixer_window() const { return mixer_window_.get(); } - int panel_shortcut(int event); - - void fit_song(); - const xtc_settings &settings() const { return settings_; } - xtc_result apply_preferences(const xtc_settings &settings); - void show_preferences(); - std::vector midi_devices(bool input); - xtc_result start_configured_backend(); - xtc_result save_preferences(); - xtc_result set_loop(uint64_t start,uint64_t end,bool enabled); - UiWidget *velocity_lane() const { return velocity_lane_; } - /* Control-thread plugin commands, transactional model adoption. */ - xtc_result assign_instrument(uint64_t track, const char *path, unsigned int index); - xtc_result adopt_sample_copy(uint64_t clip,const xtc_sample *sample,const char *path); - xtc_result slice_sample_clip(uint64_t clip,unsigned int rate,const std::vector &positions); - xtc_result trim_sample_clip(uint64_t clip,unsigned int rate,size_t begin,size_t end); - xtc_result open_sample_editor(); - void export_audio(); - xtc_result export_midi_clip(const char *path); - void export_midi_dialog(); - xtc_result import_midi_file(const char *path); - void import_midi_dialog(); - xtc_result load_sampler(uint64_t track, const char *path); - xtc_result set_instrument_spec(uint64_t track, const xtc_plugin_spec &spec); - xtc_result select_live_instrument(uint64_t track); - static void instrument_callback(UiWidget *, void *); - xtc_result set_control_map(unsigned int channel, const unsigned int controllers[3]); - xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr); - xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false); - xtc_result replace_plugin(size_t id, const char *path, unsigned int index); - xtc_result reset_plugin(size_t id, unsigned int port = UINT32_MAX); - xtc_result insert_plugin(const xtc_connection_info &wire, const char *path, unsigned int index, size_t *id = nullptr); - xtc_result set_plugin_control(size_t id, unsigned int port, float value); - /** - * @brief Constructs the window, its widgets, and the initial project. - * - * Creates an empty project and undo history, opens the audio backend, and - * schedules the periodic status timer. Throws std::runtime_error when the - * initial project or history cannot be created. - * - * @param prompt_unsaved Whether open, new, and close actions ask before - * discarding unsaved changes. - */ - explicit MainWindow(bool prompt_unsaved = true, const char *configuration = nullptr); - /** - * @brief Removes the status timer and releases backend and recording state. - * - * A backend that fails to close is intentionally retained with its recorder - * until process exit so active callbacks never observe freed state. - */ - ~MainWindow() override; - /** - * @brief Deleted copy constructor; a window uniquely owns its project and backend. - */ - MainWindow(const MainWindow &) = delete; - /** - * @brief Deleted copy assignment; a window uniquely owns its project and backend. - */ - MainWindow &operator=(const MainWindow &) = delete; - using UiDouble_Window::show; - /** - * @brief Handles JUCE events for the window. - * - * Space without Ctrl/Alt/Meta toggles the audio engine and is consumed. - * - * @param event JUCE event code, such as UI_SHORTCUT. - * @return 1 when Space was consumed; otherwise the UiDouble_Window result. - */ - int handle(int event) override; - - /** - * @brief Applies one undo or redo step from the project history. - * - * The selected snapshot is installed without resetting history and then - * accepted, leaving the window marked dirty. - * - * @param redo True to redo, false to undo. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, - * XTC_ERR_NOT_FOUND when no step is available, or another error from - * checkout, install, or accept. - */ - xtc_result undo(bool redo = false); - - /** - * @brief Returns the mixer tab widget owned by the window. - * - * @return Borrowed mixer widget, or nullptr before construction completes. - */ - MixerWidget *mixer() const - { - return mixer_; - } - - /** - * @brief Shows the piano roll for the selected MIDI clip. - * - * The piano-roll window is created on first use and reused afterwards. - * - * @return XTC_OK on success, or XTC_ERR_INVALID_STATE when the selected clip - * does not exist or is not MIDI. - */ - xtc_result open_piano_roll(); - - /** - * @brief Returns the piano-roll editor widget. - * - * @return Borrowed widget, or nullptr until open_piano_roll() first succeeds. - */ - PianoRollWidget *piano_roll() const - { - return piano_; - } - - /** - * @brief Imports a WAV file as an audio clip in a cloned project. - * - * Stops the engine first, creates an "Audio" track when none is selected, - * and places the clip at the sequencer cursor. The clone replaces the live - * project only after every step succeeds. - * - * @param path Path to the WAV file to import. - * @return XTC_OK on success, or an error from the engine stop, clone, import, - * or backend preparation. - */ - xtc_result import_audio(const char *path); - /** - * @brief Starts recording JACK input to a WAV file. - * - * Requires a connected backend and no active recording. The engine is - * stopped, the current position and selected track are remembered, and a - * stereo recorder at the engine sample rate is attached before starting. - * Failure leaves the window without a recorder. - * - * @param path Destination WAV path chosen by the user. - * @return XTC_OK when recording started, XTC_ERR_INVALID_STATE when - * disconnected or already recording, or another backend or recorder - * error. - */ - xtc_result record_audio(const char *path); - /** - * @brief Saves the current project to a path and updates the window title. - * - * On success the path is remembered and the unsaved marker is cleared. - * - * @param path Destination project path; nullptr is invalid. - * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, - * XTC_ERR_INVALID_STATE while recording, or the save error. - */ - xtc_result save_project(const char *path); - /** - * @brief Loads a project from a path and makes it the current project. - * - * The file is loaded into a candidate project and installed only on success. - * - * @param path Source project path; nullptr is invalid. - * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, or an - * error from loading or installing the project. - */ - xtc_result open_project(const char *path); - - /** Creates a fresh untitled project; the File menu checks unsaved changes first. - * On failure the current document remains owned by the window. */ - xtc_result new_project(); - /** Requests a playback fade/stop; the timer completes deactivation after - * callback acknowledgement. Recording must use immediate Stop instead. - * Returns request acceptance or the backend failure. */ - xtc_result pause_playback(); - /** Stops playback and seeks to zero. Refuses during recording. A failed - * stop/seek is reported without moving the displayed cursor to zero. */ - xtc_result return_to_start(); - - private: - friend class TrackInsertsPanel; - xtc_tap_tempo tap_state_{}; - TrackInsertsPanel *track_inserts_=nullptr; - using MixSnapshot=std::unique_ptr; - MixSnapshot mix_snapshots_[2]{{nullptr,xtc_mix_snapshot_destroy},{nullptr,xtc_mix_snapshot_destroy}}; - struct EditorPanel { - MainWindow *owner=nullptr;unsigned int index=0; - UiGroup *page=nullptr,*content=nullptr,*placeholder=nullptr; - std::unique_ptr window;bool detached=false; - }; - EditorPanel editors_[2]; - UiGroup *piano_page_=nullptr,*piano_content_=nullptr,*piano_placeholder_=nullptr; - bool piano_detached_=true; - std::unique_ptr transport_window_, mixer_window_; - UiGroup *transport_group_=nullptr, *mixer_page_=nullptr, *sidebar_=nullptr; - UiGroup *mixer_placeholder_=nullptr; - struct TransportItem {UiWidget *widget;int x,y,w,h;}; - std::vector transport_items_; - bool layout_transport_busy_=false; - bool transport_detached_=false, mixer_detached_=false; - xtc_settings settings_{}; - std::string configuration_; - std::string midi_route_warning_; - bool configuration_valid_=true; - UiCheck_Button *loop_button_=nullptr; - xtc_result configure_loop(const xtc_project *project,const xtc_settings &settings); - void apply_ui_preferences(); - void collect_preferences(); - void project_defaults(xtc_project *project); - double sample_rate() const; - void draw() override; - void refresh_workspace(); - UiChoice *snap_ = nullptr, *piano_snap_ = nullptr; - UiValue_Slider *note_velocity_ = nullptr; - UiWidget *velocity_lane_ = nullptr; - UiBox *transport_position_ = nullptr; - UiValue_Input *tempo_display_ = nullptr; - UiValue_Input *inspector_gain_=nullptr,*inspector_pan_=nullptr; - UiCheck_Button *inspector_mute_=nullptr,*inspector_solo_=nullptr,*follow_=nullptr; - UiScroll *timeline_scroll_=nullptr; - UiBox *session_info_=nullptr; - uint64_t inspected_track_=UINT64_MAX; - static void inspector_callback(UiWidget *,void *); - void follow_playhead(); - UiBox *selection_info_ = nullptr; - MixerWidget *mixer_ = nullptr; - UiMenu_Bar *menu_ = nullptr; - void update_history_controls(); - std::unique_ptr history_{nullptr, xtc_history_destroy}; - /** - * @brief JUCE menu callback for the Undo and Redo menu items. - * - * The selected menu label starting with "Redo" chooses redo; all other - * labels undo. Failures other than XTC_ERR_NOT_FOUND are shown in the window. - * - * @param widget UiMenu_Bar whose selected item triggered the callback. - * @param context MainWindow * owning the menu and history. - */ - static void history_callback(UiWidget * widget, void * context); - std::unique_ptr project_{nullptr, xtc_project_destroy}; - std::unique_ptr piano_window_; - PianoRollWidget *piano_ = nullptr; - /** - * @brief JUCE callback for the Piano roll menu item and button. - * - * Refuses to open the editor while recording and asks the user to select a - * MIDI clip when open_piano_roll() fails. - * - * @param widget Unused triggering widget. - * @param context MainWindow * owning the piano roll. - */ - static void piano_callback(UiWidget * widget, void *context); - /** - * @brief JUCE callback for the Panic menu item and button. - * - * When the backend is connected, sends a panic request that silences held - * instrument notes and reports failures. - * - * @param widget Unused triggering widget. - * @param context MainWindow * owning the backend. - */ - static void panic_callback(UiWidget * widget, void *context); - SequencerWidget *sequencer_ = nullptr; - xtc_recorder *recorder_ = nullptr; - std::string recording_path_; - uint64_t recording_tick_ = 0, recording_track_ = UINT64_MAX; - /** - * @brief Finalizes the active recording and adds the take to the project. - * - * Must run only after the engine has stopped, so no audio callback can - * reference the recorder ring. The recorder is detached, finished, and - * destroyed. A failed finalize or empty take is reported and not added. If - * the subsequent import fails, the saved WAV path is reported for recovery. - */ - void finish_recording(); - /** - * @brief Hook called by arrangement widgets before mutating the project. - * - * Refuses edits while recording. When connected, stops the engine so the - * mutation cannot race the audio thread, then refreshes the status display. - * - * @param context MainWindow * passed as the widget owner context. - * @return XTC_OK when disconnected or the stop succeeded, - * XTC_ERR_INVALID_STATE while recording, or the backend stop error. - */ - static xtc_result before_arrangement_edit(void *context); - /** - * @brief JUCE callback for the audio import, record, and relink commands. - * - * The triggering widget supplies the menu label; the Record button uses the - * "Record WAV" default. A native file chooser selects the file, after which - * the selected clip is relinked, recording starts, or a WAV is imported. - * Failures are reported in the window. - * - * @param widget Triggering UiMenu_Bar or UiButton; menu labels select the - * action. - * @param context MainWindow * owning the project and sequencer. - */ - static void audio_file_callback(UiWidget * widget, void *context); - bool dirty_ = false, prompt_unsaved_ = true; - std::string path_; - /** - * @brief Asks whether unsaved changes may be discarded. - * - * @return True when the project is clean, prompting is disabled, or the user - * chose Discard; false when the user cancels. - */ - bool discard_changes(); - /** - * @brief Records the current project in history and marks the window dirty. - * - * Dependent widgets are refreshed with the current project and graph, and - * the title gains the unsaved marker. A history failure is reported. - */ - void mark_dirty(); - /** - * @brief Installs a candidate project as the active project. - * - * Rejected while recording. Optionally builds fresh undo history, stops and - * prepares the connected backend, or validates offline audio and MIDI plans - * when disconnected. On success the window takes ownership of candidate, - * rebinds widgets, resets the graph layout, clears the dirty flag, and shows - * any missing-asset warning. On failure ownership stays with the caller. - * - * @param candidate Project to install; must be non-null. - * @param reset_history True to discard existing undo/redo history and start - * from candidate. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an - * error from history creation, the backend, or offline planning. - */ - xtc_result install_project(xtc_project *candidate, bool reset_history = true); - /** - * @brief Hook used by the sequencer to seek the audio engine. - * - * Does nothing when the backend is disconnected. Otherwise reads the engine - * sample rate, converts the tick position to samples, and seeks the backend. - * - * @param context MainWindow * passed as the sequencer owner context. - * @param tick Target timeline position in project ticks. - * @return XTC_OK when disconnected or the seek succeeded, or the status, - * conversion, or backend seek error. - */ - static xtc_result seek(void *context, uint64_t tick); - /** - * @brief JUCE menu callback for the File/New, Open, Save, and Save As items. - * - * Reads the action from the selected menu label, confirms discarding unsaved - * changes for New and Open, and uses a native file chooser unless Save can - * reuse the remembered path. Refuses file operations while recording and - * reports failures in the window. - * - * @param widget Triggering UiMenu_Bar whose selected label names the action. - * @param context MainWindow * owning the project and file path. - */ - static void control_map_callback(UiWidget *widget, void *context); - static void plugin_callback(UiWidget *widget, void *context); - xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace = false); - xtc_result install_plugin_graph(xtc_graph *graph); - static void file_callback(UiWidget * widget, void *context); - GraphWidget *graph_widget_ = nullptr; - UiBox *graph_state_ = nullptr; - xtc_backend *backend_ = nullptr; - std::string project_issue_; - bool ready_ = false; - bool running_ = false; - bool closing_ = false; - UiButton *record_ = nullptr; - UiButton *play_ = nullptr; - UiButton *stop_ = nullptr; - UiButton *pause_ = nullptr; - UiButton *home_ = nullptr; - UiBox *indicator_ = nullptr; - UiBox *status_ = nullptr; - UiBox *error_ = nullptr; - UiTabs *tabs_ = nullptr; - - /** - * @brief Edit handler passed to the modular graph widget. - * - * Applies the edit to a candidate graph, clones the project around it, and - * submits the new graph to the engine when connected. The live project is - * replaced only after every step succeeds. - * - * @param context MainWindow * passed as the graph widget owner context. - * @param edit Graph edit to apply. - * @param out_added_id Optional output receiving the new node ID when the edit - * adds a node; untouched otherwise or when nullptr. - * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an - * error from the graph, project, or backend operation. - */ - static xtc_result edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id); - /** - * @brief Opens the JACK backend and submits the current graph. - * - * Uses the default backend configuration and leaves the engine stopped. - * Failure is reported and leaves the backend disconnected; a handle that - * opened but failed to accept the graph is retained for reconnect. - */ - void open_backend(); - /** - * @brief Starts the audio engine with the current project. - * - * Does nothing when already running. Reports an error when disconnected. - * Otherwise prepares the project and starts the engine. Missing or - * unreadable audio is reported as a warning after a successful start. - */ - void start_engine(); - /** - * @brief Requests a graceful playback stop, or immediately finalizes recording. - * - * Does nothing when disconnected. Failures are reported in the window. - */ - void stop_engine(); - /** - * @brief Closes and reopens the JACK backend, leaving the engine stopped. - * - * Refuses to run while recording. If closing fails the existing handle is - * retained for a later retry and the failure is reported. - */ - void reconnect_engine(); - /** - * @brief Polls the backend and updates labels, meters, and button states. - * - * When disconnected, shows offline text and disables transport controls. - * Otherwise updates the sequencer position, mixer peaks, status line, and - * error indicators from the latest engine status, then collects backend - * work. - */ - void refresh_status(); - /** - * @brief Attempts to close the window after discarding unsaved changes. - * - * Refuses while recording. On success closes the backend, removes the status - * timer, hides the piano-roll window, and hides this window. - * - * @return True when the window closed; false when recording is active, the - * user cancels discarding, or the backend cannot close. - */ - bool close_window(); - /** - * @brief Shows an operation failure and a context-sensitive hint. - * - * @param operation Human-readable operation name; names containing - * "Project" select a file and permission hint. - * @param result Failing result code whose name is included in the message. - */ - void report_error(const char *operation, xtc_result result); - /** - * @brief JUCE callback for the Play menu item and button. - * - * @param widget Unused triggering widget. - * @param context MainWindow * whose engine is started. - */ - static void play_callback(UiWidget * widget, void *context); - /** - * @brief JUCE callback for the Stop menu item and button. - * - * @param widget Unused triggering widget. - * @param context MainWindow * whose engine is stopped. - */ - static void stop_callback(UiWidget * widget, void *context); - /** - * @brief JUCE callback for the Reconnect menu item and button. - * - * @param widget Unused triggering widget. - * @param context MainWindow * whose backend is reopened. - */ - static void reconnect_callback(UiWidget * widget, void *context); - /** - * @brief JUCE callback for the window close box, File/Quit, and close events. - * - * @param widget Unused triggering widget or window. - * @param context MainWindow * whose close is requested; a refused close - * simply leaves the window open. - */ - static void close_callback(UiWidget * widget, void *context); - /** - * @brief JUCE menu callback that selects a tab page in the main window. - * - * @param widget Triggering menu item; window() must return the MainWindow. - * @param view UiWidget * tab page to make current, not the MainWindow. - */ - static void view_callback(UiWidget *widget, void *view); - /** - * @brief JUCE help callback that shows the application name and version. - * - * @param widget Unused triggering widget. - * @param context Unused callback context. - */ - static void about_callback(UiWidget * widget, void * context); - /** - * @brief JUCE timeout callback that refreshes status and supervises recording. - * - * While recording, drains the recorder and stops the engine if draining - * fails or the backend reports a lost server, sample-rate change, or - * processing error. Reschedules itself every 0.1 seconds until closing. - * - * @param context MainWindow * registered with Ui::add_timeout(). - */ - static void timer_callback(void *context); -}; - -#endif diff --git a/src/gui/JUCE/mixer_widget.cpp b/src/gui/JUCE/mixer_widget.cpp deleted file mode 100644 index 195d0bc..0000000 --- a/src/gui/JUCE/mixer_widget.cpp +++ /dev/null @@ -1,856 +0,0 @@ -#include "theme.hpp" -#include "mixer_widget.hpp" -#include "xtc/xtc_control.h" -#include "ui.hpp" -#include -#include -#include -#include -#include - -namespace { -constexpr uint64_t lane_ticks = 15360; -} - -class AutomationLane : public UiWidget { - MixerWidget &mixer_; - - public: - /** - * @brief Creates an automation lane bound to its owning mixer. - * - * @param x Left edge in parent coordinates. - * @param y Top edge in parent coordinates. - * @param w Lane width in pixels; tick math requires a value greater than 1. - * @param h Lane height in pixels. - * @param m Mixer that owns the lane; stored by reference and must outlive it. - */ - AutomationLane(int x, int y, int w, int h, MixerWidget &m) : UiWidget(x, y, w, h), mixer_(m) - { - } - - /** - * @brief Draws the lane grid, automation curve, points, and empty hint. - * - * Points for the selected track and parameter are sorted by tick, rendered - * as an interpolated curve, and marked within the visible tick range. - */ - void draw() override - { - ui_push_clip(x(), y(), w(), h()); - ui_color(ui_rgb_color(32, 35, 40)); - ui_rectf(x(), y(), w(), h()); - ui_font(UI_HELVETICA, 11); - for (int i = 0; i <= 16; ++i) { - int px = x() + i * (w() - 1) / 16; - ui_color(ui_rgb_color(i % 4 ? 55 : 95, 60, 65)); - ui_line(px, y(), px, y() + h()); - if (i % 4 == 0) { - char label[32]; - std::snprintf(label, sizeof(label), "%llu", - static_cast(mixer_.lane_start() + static_cast(i) * 960)); - ui_color(UI_WHITE); - ui_draw(label, px + 3, y() + 14); - } - } - size_t count = 0; - xtc_project_automation_count(mixer_.project(), &count); - std::vector points; - for (size_t i = 0; i < count; ++i) { - xtc_automation_point a{}; - xtc_project_get_automation(mixer_.project(), i, &a); - if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter()) - points.push_back(a); - } - std::sort(points.begin(), points.end(), [](const auto &a, const auto &b) { - return a.tick < b.tick; - }); - float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f; - float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f; - auto py = [&](float v) { - return y() + h() - 6 - static_cast((v - min) / range * (h() - 28)); - }; - auto value = [&](uint64_t tick) { - if (points.empty()) - return min; - auto it = std::upper_bound(points.begin(), points.end(), tick, [](uint64_t t, const auto &a) { - return t < a.tick; - }); - if (it == points.begin()) - return it->value; - if (it == points.end()) - return points.back().value; - const auto &a = *(it - 1); - if (a.step) return a.value; - return a.value + - static_cast(static_cast(tick - a.tick) / static_cast(it->tick - a.tick)) * - (it->value - a.value); - }; - ui_color(xtc_ui::accent()); - if (!points.empty()) - for (int px = 1; px < w(); ++px) { - uint64_t a = - mixer_.lane_start() + static_cast(px - 1) * lane_ticks / static_cast(w() - 1); - uint64_t b = - mixer_.lane_start() + static_cast(px) * lane_ticks / static_cast(w() - 1); - ui_line(x() + px - 1, py(value(a)), x() + px, py(value(b))); - } - for (const auto &a : points) - if (a.tick >= mixer_.lane_start() && a.tick <= mixer_.lane_start() + lane_ticks) { - int px = x() + - static_cast((a.tick - mixer_.lane_start()) * static_cast(w() - 1) / lane_ticks); - ui_rectf(px - 4, py(a.value) - 4, 8, 8); - } - if (points.empty()) { - ui_color(UI_WHITE); - ui_draw("Click to add automation; right-click a point to remove. Values replace the " - "static control.", - x() + 12, y() + h() / 2); - } - ui_pop_clip(); - } - - /** - * @brief Handles JUCE input events and delegates unhandled events to the base widget. - * - * @param event JUCE event code. - * @return Nonzero if the event was handled, zero otherwise. - */ - int handle(int event) override - { - if (event != UI_PUSH) - return UiWidget::handle(event); - if (Ui::event_button() != UI_LEFT_MOUSE && Ui::event_button() != UI_RIGHT_MOUSE) - return 0; - uint64_t tick = mixer_.lane_start() + static_cast(std::clamp(Ui::event_x() - x(), 0, w() - 1)) * - lane_ticks / static_cast(w() - 1); - tick = (tick + 120) / 240 * 240; - tick = std::min(tick, XTC_PROJECT_MAX_TICK); - float min = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 0.0f : -1.0f; - float range = mixer_.parameter() == XTC_AUTOMATION_GAIN ? 4.0f : 2.0f; - float value = min + static_cast(std::clamp(y() + h() - 6 - Ui::event_y(), 0, h() - 28)) / - static_cast(h() - 28) * range; - if (Ui::event_button() == UI_RIGHT_MOUSE) { - size_t count = 0; - xtc_project_automation_count(mixer_.project(), &count); - uint64_t distance = static_cast(8) * lane_ticks / static_cast(w() - 1) + 1, - closest = UINT64_MAX; - for (size_t i = 0; i < count; ++i) { - xtc_automation_point a{}; - xtc_project_get_automation(mixer_.project(), i, &a); - uint64_t d = a.tick > tick ? a.tick - tick : tick - a.tick; - if (a.track_id == mixer_.selected_track() && a.parameter == mixer_.parameter() && d < distance) { - distance = d; - closest = a.tick; - } - } - if (closest != UINT64_MAX) - mixer_.point(closest, 0, true); - } else - mixer_.point(tick, value); - return 1; - } -}; - -/* UiMenu_::add merges duplicate labels and interprets menu paths. Append a - * unique temporary entry, then replace its text literally so IDs stay dense. */ -static int literal_item(UiChoice *choice, const char *label) -{ - std::string key="#"; - while (choice->find_item(key.c_str())) key+="#"; - int index=choice->add(key.c_str()); - choice->replace(index,label); - return index; -} - -class VerticalPeak : public UiProgress -{ - public: - VerticalPeak(int x, int y, int w, int h, const char *label) : UiProgress(x, y, w, h, label) - { - labeltype(UI_NO_LABEL); - } - void draw() override - { - ui_color(xtc_ui::background()); - ui_rectf(x(), y(), w(), h()); - int height = static_cast((std::clamp(value(), -90.0f, 6.0f) + 90) / 96 * (h() - 2)); - ui_color(selection_color()); - ui_rectf(x() + 1, y() + h() - 1 - height, w() - 2, height); - ui_color(xtc_ui::border()); - ui_rect(x(), y(), w(), h()); - } -}; - -/* A flat pan dial: no embossed face, bevels or simulated lighting. */ -class PanKnob : public UiDial -{ - bool centering_ = false; - - public: - PanKnob(int x, int y, int w, int h) : UiDial(x, y, w, h, "Pan") - { - bounds(-1, 1); - step(.01); - type(UI_LINE_DIAL); - when(UI_WHEN_RELEASE); - tooltip("Pan: -1 left, 0 center, +1 right. Double-click to center."); - } - -}; - -/* JUCE owns these children. Identity is a model ID, never a dense track index. */ -class MixerStrip : public UiGroup -{ - MixerWidget &owner_; - UiButton *name_; - UiCheck_Button *mute_, *solo_; - PanKnob *pan_; - UiValue_Slider *gain_; - UiChoice *route_; - UiBox *pan_text_, *readout_; - std::vector destinations_; - static void control(UiWidget *widget, void *context) - { - auto *s = static_cast(context); - // Copy values before the owner refreshes dependent widgets. - const auto id = s->id; - xtc_track_info track{}; - size_t count = 0, clips = 0; - xtc_project_get_counts(s->owner_.project(), &count, &clips); - bool found = false; - for (size_t i = 0; i < count; ++i) { - xtc_project_get_track(s->owner_.project(), i, &track); - if (track.id == id) { - found = true; - break; - } - } - if (!found) - return; - const float gain = - widget == s->gain_ - ? (s->gain_->value() <= -60 ? 0.0f - : std::min(4.0f, static_cast(std::pow(10, s->gain_->value() / 20)))) - : track.gain; - const float pan = widget == s->pan_ ? static_cast(s->pan_->value()) : track.pan; - const int mute = widget == s->mute_ ? s->mute_->value() : track.mute; - const int solo = widget == s->solo_ ? s->solo_->value() : track.solo; - auto &owner = s->owner_; - if (owner.select_track(id)) - owner.mix(gain, pan, mute, solo); - } - - public: - uint64_t id; - xtc_peak_meter meter[2]{}; - MixerStrip(MixerWidget &owner, uint64_t track) : UiGroup(0, 0, 148, 350), owner_(owner), id(track) - { - box(xtc_ui::flat); - color(xtc_ui::panel()); - begin(); - name_ = new UiButton(0, 0, 120, 26); - name_->callback( - [](UiWidget *, void *c) { - auto *s = static_cast(c); - s->owner_.select_track(s->id); - }, - this); - mute_ = new UiCheck_Button(0, 0, 64, 24, "Mute"); - solo_ = new UiCheck_Button(0, 0, 64, 24, "Solo"); - pan_ = new PanKnob(0, 0, 48, 48); - pan_text_ = new UiBox(0, 0, 124, 18); - route_ = new UiChoice(0, 0, 124, 24); - route_->tooltip("Output route: Master or project bus"); - route_->callback( - [](UiWidget *, void *c) { - auto *s = static_cast(c); - int i = s->route_->value(); - if (i < 0 || static_cast(i) >= s->destinations_.size()) - return; - auto bus = s->destinations_[static_cast(i)]; - auto &owner = s->owner_; - auto id = s->id; - if (owner.select_track(id)) - owner.route(bus); - }, - this); - gain_ = new UiValue_Slider(0, 0, 42, 150, "dB"); - gain_->align(UI_ALIGN_TOP); - gain_->labelsize(10); - gain_->type(UI_VERT_SLIDER); - gain_->slider(UI_FLAT_BOX); - gain_->slider_size(.10); - gain_->bounds(20 * std::log10(4.0), -60); - gain_->step(.1); - gain_->when(UI_WHEN_RELEASE); - gain_->tooltip( - "Level in dB: bottom is silence, 0 dB is unity, top is +12.04 dB. Automation overrides this static value."); - for (auto *w : {static_cast(gain_), static_cast(pan_), - static_cast(mute_), static_cast(solo_)}) - w->callback(control, this); - readout_ = new UiBox(0, 0, 132, 24); - readout_->labelsize(10); - end(); - xtc_ui::style(this); - pan_->label(""); - reset(); - resize(0, 0, 148, 350); - } - void reset() - { - xtc_peak_meter_reset(meter); - xtc_peak_meter_reset(meter + 1); - } - void sync(const xtc_track_info &track) - { - name_->copy_label((std::string(track.is_bus ? "Bus: " : "") + track.name).c_str()); - name_->tooltip(track.is_bus ? "Bus — select for automation" : "Track — select for automation"); - name_->color(owner_.selected_track() == id ? xtc_ui::track(id) : xtc_ui::panel()); - gain_->value(track.gain > 0 ? std::max(-60.0, 20 * std::log10(static_cast(track.gain))) : -60); - pan_->value(track.pan); - mute_->value(track.mute); - solo_->value(track.solo); - char text[64]; - std::snprintf(text, sizeof(text), "Pan %s %d%%", - track.pan < 0 ? "L" - : track.pan > 0 ? "R" - : "C", - static_cast(std::fabs(track.pan) * 100)); - pan_text_->copy_label(text); - route_->clear(); - destinations_.clear(); - destinations_.push_back(UINT64_MAX); - literal_item(route_,"Master"); - route_->value(0); - size_t tracks = 0, clips = 0; - xtc_project_get_counts(owner_.project(), &tracks, &clips); - for (size_t i = 0; i < tracks; ++i) { - xtc_track_info bus{}; - xtc_project_get_track(owner_.project(), i, &bus); - if (!bus.is_bus) - continue; - // Literal labels, including JUCE menu metacharacters. - int index = literal_item(route_,(std::string("Bus: ")+bus.name).c_str()); - destinations_.push_back(bus.id); - if (bus.id == track.bus_id) - route_->value(index); - } - if (track.is_bus) - route_->deactivate(); - else - route_->activate(); - redraw(); - } - void update(float left, float right, double elapsed) - { - xtc_peak_meter_update(meter, left, elapsed); - xtc_peak_meter_update(meter + 1, right, elapsed); - char text[96]; - std::snprintf(text, sizeof(text), "L %.1f R %.1f dBFS%s", static_cast(meter[0].db), - static_cast(meter[1].db), meter[0].clipped || meter[1].clipped ? " CLIP" : ""); - readout_->copy_label(text); - readout_->labelcolor(meter[0].clipped || meter[1].clipped ? UI_RED : xtc_ui::muted()); - redraw(); - } - void resize(int x, int y, int w, int h) override - { - UiWidget::resize(x, y, w, h); - name_->resize(x + 8, y + 8, w - 16, 26); - mute_->resize(x + 6, y + 42, 50, 24); - solo_->resize(x + 56, y + 42, 50, 24); - pan_->resize(x + 105, y + 38, 38, 38); - pan_text_->resize(x + 8, y + 78, w - 16, 18); - route_->resize(x + 8, y + 104, w - 16, 24); - gain_->resize(x + 10, y + 150, 44, h - 184); - readout_->resize(x + 4, y + h - 30, w - 8, 24); - redraw(); - } - void draw() override - { - UiGroup::draw(); - ui_color(xtc_ui::track(id)); - ui_rectf(x() + 1, y() + 1, w() - 2, 3); - ui_font(UI_HELVETICA, 10); - for (int db : {12, 0, -12, -24, -48, -60}) { - char text[12]; - std::snprintf(text, sizeof(text), "%d", db); - if (h() < 300 && db != 0 && db != -60) - continue; - int py = y() + 175 + static_cast((12.0412 - db) / 72.0412 * (h() - 212)); - ui_color(xtc_ui::muted()); - ui_draw(text, x() + 112, py); - } - int top = y() + 150, height = h() - 184; - for (int side = 0; side < 2; ++side) { - int px = x() + 70 + side * 15; - ui_color(xtc_ui::background()); - ui_rectf(px, top, 11, height); - const float db = std::clamp(meter[side].db, -60.0f, 6.0f); - int pixels = static_cast((db + 60) / 66 * height); - ui_color(meter[side].clipped ? UI_RED : xtc_ui::accent()); - ui_rectf(px, top + height - pixels, 11, pixels); - ui_color(xtc_ui::border()); - ui_rect(px, top, 11, height); - ui_color(meter[side].clipped ? UI_RED : xtc_ui::border()); - ui_rectf(px, top - 6, 11, 3); - } - } -}; - -/** - * @brief Creates the mixer panel and its child controls. - * - * Builds the track chooser, channel strips, pan knobs, mute/solo buttons, output - * meters, automation controls, and lane, then displays the given project. - * - * @param x Left edge in parent coordinates. - * @param y Top edge in parent coordinates. - * @param w Widget width in pixels. - * @param h Widget height in pixels. - * @param p Project to display; borrowed and must outlive its use. - * @param before Callback invoked before each edit; may be nullptr. - * @param context Opaque pointer forwarded to before. - */ -MixerWidget::MixerWidget(int x, int y, int w, int h, xtc_project *p, BeforeEdit before, void *context) - : UiGroup(x, y, w, h), project_(p), before_(before), context_(context) -{ - begin(); - tracks_ = new UiChoice(x + 70, y + 8, 240, 26, "Track"); - tracks_->callback(select, this); - routes_ = new UiChoice(x + 600, y + 8, 180, 26, "Route"); - routes_->callback( - [](UiWidget *, void *c) { - auto *m = static_cast(c); - int index = m->routes_->value(); - if (index >= 0 && static_cast(index) < m->route_ids_.size()) - m->route(m->route_ids_[static_cast(index)]); - }, - this); - bus_ = new UiButton(x + 820, y + 8, 110, 26, "Add bus"); - bus_->callback([](UiWidget *, void *c) { static_cast(c)->add_bus(); }, this); - left_ = new VerticalPeak(x + 70, y + 90, 320, 20, "Output L"); - right_ = new VerticalPeak(x + 480, y + 90, 320, 20, "Output R"); - for (auto *meter : {left_, right_}) { - meter->minimum(-90); - meter->maximum(6); - } - reset_ = new UiButton(x, y, 120, 26, "Reset peaks"); - reset_->callback([](UiWidget *, void *c) { static_cast(c)->reset_meters(); }, this); - bank_ = new UiScroll(x, y + 40, w - 154, h - 172); - bank_->type(UiScroll::HORIZONTAL_ALWAYS); - bank_->end(); - master_panel_ = new UiGroup(x + w - 144, y + 40, 144, h - 172); - master_panel_->box(xtc_ui::flat); - auto *master_name = new UiBox(x + w - 140, y + 48, 136, 26, "MASTER"); - master_name->labelfont(UI_HELVETICA_BOLD); - auto *master_hint = new UiBox(x + w - 140, y + 80, 136, 24, "Output monitor"); - master_hint->labelsize(11); - master_readout_ = new UiBox(x + w - 140, y + 340, 136, 40); - master_readout_->labelsize(10); - master_panel_->add(left_); - master_panel_->add(right_); - master_panel_->end(); - reset_meters(); - parameter_ = new UiChoice(x + 110, y + 146, 150, 26, "Automation"); - parameter_->add("Gain|Pan"); - parameter_->value(0); - parameter_->callback(select, this); - start_ = new UiValue_Input(x + 420, y + 146, 140, 26, "First tick"); - start_->bounds(0, XTC_PROJECT_MAX_TICK - lane_ticks); - start_->step(3840); - start_->callback(select, this); - mode_ = new UiChoice(x + 705, y + 146, 150, 26, "Interpolation"); - mode_->add("Linear|Hold (step)"); - mode_->value(0); - hint_ = new UiBox(x, y, w, 18, - "Select a strip for automation. M/S toggles; arrows adjust gain, Shift+arrows pan. Faders show " - "static dB gain."); - hint_->labelsize(11); - lane_ = new AutomationLane(x + 10, y + h - 80, w - 20, 76, *this); - end(); - set_project(p); - layout(); -} - -/** - * @brief Returns the ID of the track selected in the track chooser. - * - * @return Selected track ID, or UINT64_MAX when the chooser selection does - * not resolve to a project track. - */ -uint64_t MixerWidget::selected_track() const -{ - return selected_id_; -} - -/** - * @brief Returns the automation parameter selected in the chooser. - * - * @return XTC_AUTOMATION_PAN when the chooser selects "Pan", otherwise - * XTC_AUTOMATION_GAIN. - */ -xtc_automation_parameter MixerWidget::parameter() const -{ - return parameter_->value() == 1 ? XTC_AUTOMATION_PAN : XTC_AUTOMATION_GAIN; -} - -/** - * @brief Returns the first tick shown by the automation lane. - * - * @return Clamped first tick in project ticks, or 0 when the input value is - * not finite. - */ -uint64_t MixerWidget::lane_start() const -{ - if (!std::isfinite(start_->value())) - return 0; - return static_cast( - std::clamp(start_->value(), 0.0, static_cast(XTC_PROJECT_MAX_TICK - lane_ticks))); -} - -/** - * @brief Displays a different project and refreshes the controls. - * - * The current track selection is preserved only when the same project - * pointer is passed again. The project is borrowed and must outlive use. - * - * @param p Project to display; must remain valid while shown. - */ -void MixerWidget::set_project(xtc_project *p) -{ - bool reset = project_ != p; - uint64_t selected = project_ == p ? selected_track() : UINT64_MAX; - project_ = p; - tracks_->clear(); - routes_->clear(); routes_->add("Master"); - route_ids_.clear(); route_ids_.push_back(UINT64_MAX); - size_t n = 0, c = 0; - xtc_project_get_counts(p, &n, &c); - int index = 0; - for (size_t i = 0; i < n; ++i) { - xtc_track_info t{}; - xtc_project_get_track(p, i, &t); - literal_item(tracks_,(std::string(t.is_bus?"Bus: ":"Track: ")+t.name).c_str()); - if(t.is_bus){literal_item(routes_,(std::string("Bus: ")+t.name).c_str());route_ids_.push_back(t.id);} - if (t.id == selected) - index = static_cast(i); - } - tracks_->value(n ? index : -1); - selected_id_=UINT64_MAX; - if(n){xtc_track_info track{};xtc_project_get_track(p,static_cast(index),&track);selected_id_=track.id;} - sync_strips(reset); - selection(); - redraw(); -} - -/** - * @brief Synchronizes the child controls with the selected track. - * - * Reconcile strip values and routing with model IDs, highlight the selected - * track, and keep its automation controls in sync. - */ -void MixerWidget::selection() -{ - xtc_track_info t{}; - bool valid = xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK; - if (valid) - lane_->activate(); - else - lane_->deactivate(); - for (auto *s : strips_) { - xtc_track_info track{}; - size_t count = 0, clips = 0; - xtc_project_get_counts(project_, &count, &clips); - for (size_t i = 0; i < count; ++i) { - xtc_project_get_track(project_, i, &track); - if (track.id == s->id) { - s->sync(track); - break; - } - } - } - if (valid && !t.is_bus) - routes_->activate(); - else - routes_->deactivate(); - routes_->value(0); - for (size_t i = 0; i < route_ids_.size(); ++i) - if (route_ids_[i] == t.bus_id) - routes_->value(static_cast(i)); - if (auto *selected = strip(selected_track())) { - int target = bank_->xposition(); - if (selected->x() < bank_->x()) - target -= bank_->x() - selected->x(); - else if (selected->x() + selected->w() > bank_->x() + bank_->w() - 12) - target += selected->x() + selected->w() - (bank_->x() + bank_->w() - 12); - bank_->scroll_to(std::max(0, target), 0); - } - lane_->redraw(); -} - -/** - * @brief Updates the left and right output meters. - * - * Meter values are clamped to 1.0, while the over-range color is chosen - * from the unclamped level. - * - * @param left Left-channel peak level. - * @param right Right-channel peak level. - */ -void MixerWidget::peaks(float left, float right) -{ - auto now = std::chrono::steady_clock::now(); - meter_elapsed_ = std::clamp(std::chrono::duration(now-peak_time_).count(), 0.0, 60.0); - peak_time_ = now; - (void)xtc_peak_meter_update(master_, left, meter_elapsed_); - (void)xtc_peak_meter_update(master_+1, right, meter_elapsed_); - display_peaks(left_, right_, master_, "Master"); - char text[100];std::snprintf(text,sizeof(text),"L %.1f / R %.1f dBFS%s",static_cast(master_[0].db),static_cast(master_[1].db),master_clipped()?" CLIP":""); - master_readout_->copy_label(text);master_readout_->align(UI_ALIGN_WRAP|UI_ALIGN_INSIDE); -} - -/** - * @brief Reports an edit result and refreshes the controls. - * - * A successful result invokes the widget callback so the owner can mark - * the project dirty; a failure shows a modal alert. The controls are - * resynchronized in both cases. - * - * @param r Result of the attempted edit. - * @return The same result passed in. - */ -xtc_result MixerWidget::changed(xtc_result r) -{ - if (r == XTC_OK) - do_callback(); - else - ui_alert("Mixer edit failed: %s", xtc_result_string(r)); - selection(); - return r; -} - -/** - * @brief Applies gain, pan, mute, and solo to the selected track. - * - * The constructor's before-edit callback runs first; the edit is skipped - * if it returns an error. On success the widget callback is invoked and - * the controls are refreshed. - * - * @param gain Linear gain in the range 0..4. - * @param pan Pan position in the range -1 (left)..1 (right). - * @param mute Mute state: 0 or 1. - * @param solo Solo state: 0 or 1. - * @return XTC_OK on success, or the before-edit or project mutation error. - */ -xtc_result MixerWidget::mix(float gain, float pan, int mute, int solo) -{ - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) - r = xtc_project_set_mix(project_, selected_track(), gain, pan, mute, solo); - return changed(r); -} - -/** - * @brief Adds, replaces, or removes an automation point. - * - * The constructor's before-edit callback runs first; the edit is skipped - * if it returns an error. On success the widget callback is invoked and - * the controls are refreshed. - * - * @param tick Automation tick to modify; must be within the project range. - * @param value New value; ignored when remove is true. - * @param remove When true, removes the point instead of setting it. - * @return XTC_OK on success, or the before-edit or project mutation error. - */ -xtc_result MixerWidget::point(uint64_t tick, float value, bool remove) -{ - xtc_result r = before_ ? before_(context_) : XTC_OK; - xtc_parameter_key key{XTC_PARAMETER_TRACK,selected_track(),static_cast(parameter())}; - if (r == XTC_OK) - r = xtc_project_automate(project_,key,48000,tick,mode_->value() == 1 ? 1U : 0U,value,remove ? 1 : 0); - return changed(r); -} - -/** - * @brief JUCE callback for the track, parameter, and first-tick controls. - * - * @param c Opaque MixerWidget pointer supplied as callback context. - */ -void MixerWidget::select(UiWidget *widget, void *c) -{ - auto *m=static_cast(c); - if(widget==m->tracks_){xtc_track_info track{}; - if(xtc_project_get_track(m->project_,static_cast(m->tracks_->value()),&track)==XTC_OK)m->selected_id_=track.id; - } - m->selection(); -} - -/** - * @brief Handles keyboard shortcuts and forwards other events. - * - * M and S toggle mute and solo, Left/Right adjust gain (Shift adjusts pan), - * and Up/Down change the selected track. Modified key presses fall through. - * - * @param e JUCE event code. - * @return 1 when the event was consumed, otherwise the UiGroup result. - */ -int MixerWidget::handle(int e) -{ - if (e == UI_KEYDOWN && dynamic_cast(Ui::focus())) return UiGroup::handle(e); - if (e == UI_KEYDOWN && !(Ui::event_state() & (UI_CTRL | UI_ALT | UI_META))) { - int key = Ui::event_key(); - xtc_track_info t{}; - if (xtc_project_get_track(project_, static_cast(tracks_->value()), &t) == XTC_OK) { - if (key == 'm' || key == 's') { - mix(t.gain, t.pan, key == 'm' ? !t.mute : t.mute, key == 's' ? !t.solo : t.solo); - return 1; - } - if (key == UI_Left || key == UI_Right) { - float d = key == UI_Left ? -0.05f : 0.05f; - bool pan = (Ui::event_state() & UI_SHIFT) != 0; - mix(pan ? t.gain : std::clamp(t.gain + d, 0.0f, 4.0f), pan ? std::clamp(t.pan + d, -1.0f, 1.0f) : t.pan, - t.mute, t.solo); - return 1; - } - if (key == UI_Up || key == UI_Down) { - size_t n = 0, c = 0; - xtc_project_get_counts(project_, &n, &c); - tracks_->value(std::clamp(tracks_->value() + (key == UI_Up ? -1 : 1), 0, static_cast(n) - 1)); - select(tracks_,this); - return 1; - } - } - } - return UiGroup::handle(e); -} - -void MixerWidget::display_peaks(UiProgress *left, UiProgress *right, xtc_peak_meter *meters, const char *prefix) -{ - for (unsigned int side = 0; side < 2; ++side) { - auto *bar = side ? right : left; - char label[80]; - std::snprintf(label, sizeof(label), "%s %c %.1f dBFS%s", prefix, side ? 'R' : 'L', - static_cast(meters[side].db), meters[side].clipped ? " CLIP" : ""); - bar->copy_label(label); - bar->value(std::clamp(meters[side].db, -90.0f, 6.0f)); - bar->selection_color(meters[side].clipped ? UI_RED : xtc_ui::accent()); - } -} -void MixerWidget::track_peaks(float left, float right) -{ - for(auto *s:strips_)if(s->id==selected_track())s->update(left,right,meter_elapsed_); -} -void MixerWidget::reset_meters() -{ - for (unsigned int i = 0; i < 2; ++i) { xtc_peak_meter_reset(master_+i); } - display_peaks(left_, right_, master_, "Master"); - char text[100];std::snprintf(text,sizeof(text),"L %.1f / R %.1f dBFS%s",static_cast(master_[0].db),static_cast(master_[1].db),master_clipped()?" CLIP":""); - master_readout_->copy_label(text);master_readout_->align(UI_ALIGN_WRAP|UI_ALIGN_INSIDE); - for(auto *s:strips_){s->reset();s->update(0,0,0);} -} -xtc_result MixerWidget::add_bus() -{ - uint64_t id; - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_add_bus(project_, "Bus", &id); - if (r == XTC_OK) { changed(r); select_track(id); return r; } - return changed(r); -} -xtc_result MixerWidget::route(uint64_t bus) -{ - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_route_track(project_, selected_track(), bus); - return changed(r); -} -bool MixerWidget::select_track(uint64_t id) -{ - size_t n = 0, clips = 0; xtc_project_get_counts(project_, &n, &clips); - for (size_t i = 0; i < n; ++i) { - xtc_track_info track{}; xtc_project_get_track(project_, i, &track); - if (track.id == id) { selected_id_=id;tracks_->value(static_cast(i)); selection(); return true; } - } - return false; -} - -UiGroup *MixerWidget::strip(uint64_t id) const -{ - for (auto *s : strips_) - if (s->id == id) - return s; - return nullptr; -} -void MixerWidget::meters(const xtc_meter_snapshot &snapshot) -{ - peaks(snapshot.master_left, snapshot.master_right); - for (auto *s : strips_) { - float left = 0, right = 0; - for (size_t i = 0; i < std::min(snapshot.count, static_cast(XTC_PROJECT_MAX_TRACKS)); ++i) - if (snapshot.tracks[i].id == s->id) { - left = snapshot.tracks[i].left; - right = snapshot.tracks[i].right; - break; - } - s->update(left, right, meter_elapsed_); - } -} -void MixerWidget::sync_strips(bool reset) -{ - size_t n = 0, clips = 0; - xtc_project_get_counts(project_, &n, &clips); - bool rebuild = n != strips_.size(); - for (size_t i = 0; i < n && !rebuild; ++i) { - xtc_track_info t{}; - xtc_project_get_track(project_, i, &t); - rebuild = t.id != strips_[i]->id; - } - if (rebuild) { - auto *previous = UiGroup::current(); - bank_->clear(); - strips_.clear(); - bank_->begin(); - for (size_t i = 0; i < n; ++i) { - xtc_track_info t{}; - xtc_project_get_track(project_, i, &t); - strips_.push_back(new MixerStrip(*this, t.id)); - } - bank_->end(); - UiGroup::current(previous); - layout(); - } - if (reset) - for (auto *s : strips_) - s->reset(); -} -void MixerWidget::layout() -{ - const int bank_height = std::max(250, h() - 172); - tracks_->resize(x() + 48, y() + 4, 210, 26); - routes_->resize(x() + 326, y() + 4, 160, 26); - bus_->resize(x() + 500, y() + 4, 100, 26); - reset_->resize(x() + 612, y() + 4, 112, 26); - bank_->resize(x(), y() + 40, w() - 154, bank_height); - int offset = bank_->xposition(); - for (size_t i = 0; i < strips_.size(); ++i) - strips_[i]->resize(bank_->x() + static_cast(i) * 156 - offset, bank_->y(), 148, bank_height - 16); - master_panel_->resize(x() + w() - 144, y() + 40, 144, bank_height); - // Master is pinned outside the horizontally scrolling bank. - left_->resize(x() + w() - 118, y() + 118, 32, bank_height - 140); - right_->resize(x() + w() - 66, y() + 118, 32, bank_height - 140); - master_readout_->resize(x() + w() - 140, y() + 40 + bank_height - 52, 136, 42); - left_->align(UI_ALIGN_TOP); - right_->align(UI_ALIGN_TOP); - left_->labelsize(10); - right_->labelsize(10); - parameter_->resize(x() + 84, y() + h() - 120, 100, 26); - start_->resize(x() + 264, y() + h() - 120, 116, 26); - mode_->resize(x() + 488, y() + h() - 120, 136, 26); - hint_->resize(x() + 8, y() + h() - 92, w() - 16, 18); - lane_->resize(x() + 8, y() + h() - 72, w() - 16, 68); - redraw(); -} -void MixerWidget::resize(int x, int y, int w, int h) -{ - UiWidget::resize(x, y, w, h); - layout(); -} diff --git a/src/gui/JUCE/mixer_widget.hpp b/src/gui/JUCE/mixer_widget.hpp deleted file mode 100644 index bc187bb..0000000 --- a/src/gui/JUCE/mixer_widget.hpp +++ /dev/null @@ -1,202 +0,0 @@ -#ifndef XTC_MIXER_WIDGET_HPP -#define XTC_MIXER_WIDGET_HPP -#include "xtc/xtc_mixer.h" -#include "xtc/xtc_backend.h" -#include "ui.hpp" -#include -#include - -class UiChoice; -class UiButton; -class UiValue_Slider; -class UiCheck_Button; -class UiProgress; -class UiValue_Input; -class AutomationLane; -class MixerStrip; -class UiScroll; -class UiBox; - -class MixerWidget : public UiGroup { - public: - /** - * @brief Prepares a model edit using the widget's opaque callback context. - * - * @return XTC_OK to allow the edit, or an error to cancel it. - */ - using BeforeEdit = xtc_result (*)(void *); - - /** - * @brief Creates the mixer panel and its child controls. - * - * Builds the track chooser, channel strips, pan knobs, mute/solo buttons, output - * meters, automation controls, and lane, then displays the given project. - * - * @param x Left edge in parent coordinates. - * @param y Top edge in parent coordinates. - * @param w Widget width in pixels. - * @param h Widget height in pixels. - * @param p Project to display; borrowed and must outlive its use. - * @param before Callback invoked before each edit; may be nullptr. - * @param context Opaque pointer forwarded to before. - */ - MixerWidget(int x, int y, int w, int h, xtc_project * p, BeforeEdit before, void * context); - - /** - * @brief Displays a different project and refreshes the controls. - * - * The current track selection is preserved only when the same project - * pointer is passed again. The project is borrowed and must outlive use. - * - * @param p Project to display; must remain valid while shown. - */ - void set_project(xtc_project * p); - - /** - * @brief Updates the left and right output meters. - * - * Meter values are clamped to 1.0, while the over-range color is chosen - * from the unclamped level. - * - * @param left Left-channel peak level. - * @param right Right-channel peak level. - */ - void peaks(float left, float right); - - /** - * @brief Returns the ID of the track selected in the track chooser. - * - * @return Selected track ID, or UINT64_MAX when the chooser selection does - * not resolve to a project track. - */ - uint64_t selected_track() const; - - /** - * @brief Returns the project currently displayed by the widget. - * - * @return Borrowed project pointer, possibly nullptr; the widget does not - * own it. - */ - xtc_project *project() const - { - return project_; - } - - /** - * @brief Returns the automation parameter selected in the chooser. - * - * @return XTC_AUTOMATION_PAN when the chooser selects "Pan", otherwise - * XTC_AUTOMATION_GAIN. - */ - xtc_automation_parameter parameter() const; - - /** - * @brief Returns the first tick shown by the automation lane. - * - * @return Clamped first tick in project ticks, or 0 when the input value is - * not finite. - */ - uint64_t lane_start() const; - - /** - * @brief Adds, replaces, or removes an automation point. - * - * The constructor's before-edit callback runs first; the edit is skipped - * if it returns an error. On success the widget callback is invoked and - * the controls are refreshed. - * - * @param tick Automation tick to modify; must be within the project range. - * @param value New value; ignored when remove is true. - * @param remove When true, removes the point instead of setting it. - * @return XTC_OK on success, or the before-edit or project mutation error. - */ - xtc_result point(uint64_t tick, float value, bool remove = false); - - /** - * @brief Applies gain, pan, mute, and solo to the selected track. - * - * The constructor's before-edit callback runs first; the edit is skipped - * if it returns an error. On success the widget callback is invoked and - * the controls are refreshed. - * - * @param gain Linear gain in the range 0..4. - * @param pan Pan position in the range -1 (left)..1 (right). - * @param mute Mute state: 0 or 1. - * @param solo Solo state: 0 or 1. - * @return XTC_OK on success, or the before-edit or project mutation error. - */ - xtc_result add_bus(); - xtc_result route(uint64_t bus); - bool select_track(uint64_t id); - void track_peaks(float left, float right); - /* One control-thread snapshot updates all strips; missing taps decay to silence. */ - void meters(const xtc_meter_snapshot &snapshot); - UiGroup *strip(uint64_t id) const; - size_t strip_count() const { return strips_.size(); } - void resize(int x, int y, int w, int h) override; - void reset_meters(); - bool master_clipped() const { return master_[0].clipped || master_[1].clipped; } - xtc_result mix(float gain, float pan, int mute, int solo); - - /** - * @brief Handles keyboard shortcuts and forwards other events. - * - * M and S toggle mute and solo, Left/Right adjust gain (Shift adjusts pan), - * and Up/Down change the selected track. Modified key presses fall through. - * - * @param event JUCE event code. - * @return 1 when the event was consumed, otherwise the UiGroup result. - */ - int handle(int event) override; - - private: - xtc_project *project_; - BeforeEdit before_; - void *context_; - UiChoice *tracks_, *parameter_, *routes_, *mode_; - std::vector route_ids_; - UiProgress *left_, *right_; - UiScroll *bank_; - UiGroup *master_panel_; - UiButton *bus_, *reset_; - UiBox *hint_, *master_readout_; - std::vector strips_; - void sync_strips(bool reset = false); - void layout(); - xtc_peak_meter master_[2]{}; - uint64_t selected_id_ = UINT64_MAX; - std::chrono::steady_clock::time_point peak_time_ = std::chrono::steady_clock::now(); - double meter_elapsed_ = 0.1; - void display_peaks(UiProgress *left, UiProgress *right, xtc_peak_meter *meters, const char *prefix); - - UiValue_Input *start_; - AutomationLane *lane_; - /** - * @brief Reports an edit result and refreshes the controls. - * - * A successful result invokes the widget callback so the owner can mark - * the project dirty; a failure shows a modal alert. The controls are - * resynchronized in both cases. - * - * @param r Result of the attempted edit. - * @return The same result passed in. - */ - xtc_result changed(xtc_result r); - - /** - * @brief Synchronizes the child controls with the selected track. - * - * Reconcile strip controls with stable model IDs and the selected - * automation target. - */ - void selection(); - - /** - * @brief JUCE callback for the track, parameter, and first-tick controls. - * - * @param widget Track chooser or automation control that changed. - * @param c Opaque MixerWidget pointer supplied as callback context. - */ - static void select(UiWidget * widget, void * c); -}; -#endif diff --git a/src/gui/JUCE/piano_roll.cpp b/src/gui/JUCE/piano_roll.cpp deleted file mode 100644 index c16f7ed..0000000 --- a/src/gui/JUCE/piano_roll.cpp +++ /dev/null @@ -1,455 +0,0 @@ -#include "theme.hpp" -#include "snap.hpp" -#include "piano_roll.hpp" -#include "ui.hpp" -#include -#include -#include - -namespace { -constexpr int key_width = 64, row_height = 16, scale = 96, header = 28; -template bool parse(const char *text, Values &...values) -{ - std::istringstream input(text); - (input >> ... >> values); - return !input.fail() && (input >> std::ws).eof(); -} -} - -/** - * @brief Creates a piano-roll editor for a MIDI clip. - * - * The pre-edit guard, when non-NULL, is called before every project mutation - * and can veto the edit by returning an error. - * - * @param left Left edge of the widget in the parent's coordinate system. - * @param top Top edge of the widget in the parent's coordinate system. - * @param project Borrowed project that must outlive the widget. - * @param clip ID of the MIDI clip to edit. - * @param before Optional pre-edit guard; NULL means no guard is used. - * @param context Opaque pointer passed to @p before on every edit. - */ -PianoRollWidget::PianoRollWidget(int left, int top, xtc_project *project, uint64_t clip, BeforeEdit before, - void *context) - : UiWidget(left, top, 900, header + 128 * row_height), project_(project), clip_(clip), before_(before), - context_(context) -{ - set_project(project, clip); -} - -/** - * @brief Points the editor at a different project or clip. - * - * Clears any in-progress drag when the project or clip changes and resizes the - * widget to fit the clip's source length. Selection is cleared on a new model or clip. - * - * @param project Borrowed project that must outlive the widget. - * @param clip ID of the MIDI clip to display. - */ -bool PianoRollWidget::select_note(uint64_t id) -{ - size_t count = 0; - xtc_project_note_count(project_,&count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; - if (xtc_project_get_note(project_,i,¬e) == XTC_OK && note.id == id && visible(note)) { - selected_ = id; all_ = false; mode_ = 0; redraw(); return true; - } - } - return false; -} - -void PianoRollWidget::set_project(xtc_project *project, uint64_t clip) -{ - if (project != project_ || clip != clip_) { - mode_ = 0; - selected_ = UINT64_MAX; - all_ = false; - } - project_ = project; - clip_ = clip; - xtc_clip_info c{}; - int width = xtc_project_find_clip(project_, clip_, &c) == XTC_OK - ? static_cast(c.source_length_tick * scale / 960) + key_width + 20 - : 900; - size(std::max(900, width), header + 128 * row_height); - redraw(); -} - -/** - * @brief Converts a horizontal pixel position to a snapped tick. - * - * The position is measured in absolute window coordinates and snapped down to - * the current configured grid. - * - * @param px Horizontal position in window coordinates. - * @return Tick offset within the clip source, or 0 for positions on or left of - * the keyboard. - */ -uint64_t PianoRollWidget::tick(int px) const -{ - return static_cast(std::max(0, px - x() - key_width)) * 960U / scale / grid_ * grid_; -} - -/** - * @brief Reports an edit result and notifies listeners on success. - * - * On success the widget callback runs and the editor is redrawn; on failure an - * alert shows the error and the project is left unchanged. - * - * @param result Result of the attempted project mutation. - * @return The same result code. - */ -xtc_result PianoRollWidget::change(xtc_result result) -{ - if (result == XTC_OK) { - do_callback(); - redraw(); - } else - ui_alert("Note unchanged: %s", xtc_result_string(result)); - return result; -} - -/** - * @brief Adds a note to the current clip and selects the new note. - * - * Invokes the pre-edit guard, if any, before mutating the project. - * - * @param start Note start in clip source ticks. - * @param length Note length in ticks. - * @param pitch MIDI pitch from 0 to 127. - * @param velocity MIDI velocity from 1 to 127. - * @param channel MIDI channel from 0 to 15. - * @return XTC_OK on success, the guard's error, or the project error. - */ -xtc_result PianoRollWidget::add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity, - unsigned int channel) -{ - xtc_result r = before_ ? before_(context_) : XTC_OK; - uint64_t id; - if (r == XTC_OK) - r = xtc_project_add_note(project_, clip_, start, length, pitch, velocity, channel, &id); - if (r == XTC_OK) { - selected_ = id; - all_ = false; - } - return change(r); -} - -/** - * @brief Removes the selected note from the current clip. - * - * Invokes the pre-edit guard, if any, before mutating the project. The - * selection is cleared only when the removal succeeds. - * - * @return XTC_OK on success, the guard's error, or the project error. - */ -xtc_result PianoRollWidget::delete_selected() -{ - xtc_note_edit edit{}; - edit.action = XTC_NOTES_DELETE; - return apply(edit); -} - -/** - * @brief Draws the keyboard, beat grid, and notes of the current clip. - * - * Paints only the background when the clip is missing or is not a MIDI clip. - */ -void PianoRollWidget::draw() -{ - ui_push_clip(x(), y(), w(), h()); - ui_color(ui_rgb_color(30, 33, 38)); - ui_rectf(x(), y(), w(), h()); - xtc_clip_info clip{}; - if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || !clip.midi) { - ui_pop_clip(); - return; - } - int cx, cy, cw, ch; - ui_clip_box(x(), y(), w(), h(), cx, cy, cw, ch); - ui_font(UI_HELVETICA, 11); - for (int pitch = 0; pitch < 128; ++pitch) { - int py = y() + header + (127 - pitch) * row_height; - if (py + row_height < cy || py > cy + ch) - continue; - int pc = pitch % 12; - bool black = pc == 1 || pc == 3 || pc == 6 || pc == 8 || pc == 10; - ui_color(black ? ui_rgb_color(29, 31, 35) : ui_rgb_color(43, 46, 52)); - ui_rectf(cx, py, cw, row_height - 1); - ui_color(black ? ui_rgb_color(60, 63, 70) : ui_rgb_color(130, 135, 140)); - ui_rectf(x(), py, key_width - 2, row_height - 1); - if (!pc) { - char text[20]; - std::snprintf(text, sizeof(text), "C%d", pitch / 12 - 1); - ui_color(UI_WHITE); - ui_draw(text, x() + 6, py + 12); - } - } - // At 1-tick snap, show at least a 120-tick visual grid to avoid pixel overdraw. - uint64_t visual = std::max(120, grid_); - int spacing = static_cast(visual * scale / 960); - int first = std::max(0, (cx - x() - key_width) / spacing); - int last = (cx + cw - x() - key_width) / spacing + 1; - for (int step = first; step <= last; ++step) { - int px = x() + key_width + static_cast(static_cast(step) * visual * scale / 960); - ui_color((static_cast(step) * visual) % 3840 == 0 ? ui_rgb_color(95, 98, 105) : ui_rgb_color(52, 55, 62)); - ui_line(px, cy, px, cy + ch); - } - size_t count = 0; - xtc_project_note_count(project_, &count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info n{}; - xtc_project_get_note(project_, i, &n); - if (!visible(n)) - continue; - int left = x() + key_width + static_cast(n.start_tick * scale / 960); - int right = left + std::max(3, static_cast(n.length_tick * scale / 960)); - int py = y() + header + (127 - static_cast(n.pitch)) * row_height; - int a = std::max(cx, left), b = std::min(cx + cw, right); - if (a >= b || py + row_height < cy || py > cy + ch) - continue; - ui_color(all_ || n.id == selected_ ? xtc_ui::accent() - : ui_rgb_color(50, static_cast(70 + n.velocity), 110)); - ui_rectf(a, py + 1, b - a, row_height - 3); - } - ui_color(ui_rgb_color(30, 33, 38)); - ui_rectf(x(), y(), w(), header); - char status[160]; - std::snprintf(status, sizeof(status), "Snap %llu ticks | Channel %d (-1 all) | Pitches %u..%u | %s", - static_cast(grid_), channel_filter_, low_, high_, - all_ ? "All filtered notes selected" : "Click selects; Ctrl+A selects filtered notes"); - ui_color(UI_WHITE); - ui_draw(status, x() + 6, y() + 18); - ui_pop_clip(); -} - -/** - * @brief Handles mouse, keyboard, and focus events for the editor. - * - * Left-click on empty space adds a note, clicking a note selects it, and - * dragging moves or resizes it. Right-click and Delete remove the selected - * note; Escape cancels an in-progress drag. - * - * @param event JUCE event code. - * @return 1 when the event was handled, otherwise the result of the base - * class handler. - */ -int PianoRollWidget::handle(int event) -{ - if (event == UI_PUSH) { - take_focus(); - mode_ = 0; - size_t count = 0; - xtc_project_note_count(project_, &count); - selected_ = UINT64_MAX; - all_ = false; - for (size_t i = count; i > 0; --i) { - xtc_note_info n{}; - xtc_project_get_note(project_, i - 1, &n); - int left = x() + key_width + static_cast(n.start_tick * scale / 960); - int right = left + std::max(3, static_cast(n.length_tick * scale / 960)); - int top = y() + header + (127 - static_cast(n.pitch)) * row_height; - if (visible(n) && Ui::event_x() >= left && Ui::event_x() < right && Ui::event_y() >= top && - Ui::event_y() < top + row_height) { - selected_ = n.id; - original_ = n; - drag_x_ = Ui::event_x(); - drag_y_ = Ui::event_y(); - mode_ = right - Ui::event_x() <= 6 ? 2 : 1; - break; - } - } - if (selected_ != UINT64_MAX && Ui::event_button() == UI_RIGHT_MOUSE) { - mode_ = 0; - UiMenu_Item items[5]{}; - items[0].text = "Velocity and channel (V)"; - items[1].text = "Quantize (Q)"; - items[2].text = "Repeat (Ctrl+D)"; - items[3].text = "Delete (undoable)"; - const auto *picked = items->popup(Ui::event_x(),Ui::event_y()); - if (picked == items) command(2); - else if (picked == items+1) command(3); - else if (picked == items+2) command(5); - else if (picked == items+3) delete_selected(); - } else if (Ui::event_button() == UI_LEFT_MOUSE && selected_ == UINT64_MAX && Ui::event_x() >= x() + key_width && - Ui::event_y() >= y() + header) { - int pitch = 127 - (Ui::event_y() - y() - header) / row_height; - xtc_clip_info c{}; - uint64_t start = tick(Ui::event_x()); - if (pitch >= static_cast(low_) && pitch <= static_cast(high_) && xtc_project_find_clip(project_, clip_, &c) == XTC_OK && - start < c.source_length_tick) - add_note(start, std::min(new_note_ticks, c.source_length_tick - start), - static_cast(pitch), new_note_velocity, channel_filter_ < 0 ? new_note_channel : static_cast(channel_filter_)); - } - redraw(); - return 1; - } - if (event == UI_DRAG) - return mode_ != 0; - if (event == UI_RELEASE && mode_) { - int mode = mode_; - mode_ = 0; - int64_t delta = static_cast(Ui::event_x() - drag_x_) * 960 / scale / static_cast(grid_) * static_cast(grid_); - xtc_clip_info c{}; - if (xtc_project_find_clip(project_, clip_, &c) != XTC_OK) - return 1; - int64_t start = static_cast(original_.start_tick), - length = static_cast(original_.length_tick); - unsigned int pitch = original_.pitch; - if (mode == 1) { - start = std::clamp(start + delta, 0, static_cast(c.source_length_tick) - length); - pitch = static_cast( - std::clamp(static_cast(pitch) - (Ui::event_y() - drag_y_) / row_height, 0, 127)); - } else { - int64_t maximum = static_cast(c.source_length_tick) - start; - length = std::clamp(length + delta, std::min(static_cast(grid_), maximum), maximum); - } - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) - r = xtc_project_edit_note(project_, selected_, static_cast(start), static_cast(length), - pitch, original_.velocity, original_.channel); - change(r); - return 1; - } - if (event == UI_KEYDOWN) { - int key = Ui::event_key(); - if (key == 'a' && (Ui::event_state() & UI_CTRL)) { select_filtered(); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 'q') { command(3); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 'v') { command(2); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 'g') { command(0); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 'f') { command(1); return 1; } - if (!Ui::event_state(UI_CTRL|UI_ALT|UI_META) && key == 't') { command(4); return 1; } - if (key == 'd' && (Ui::event_state() & UI_CTRL)) { command(5); return 1; } - if (key == UI_Up || key == UI_Down) { - xtc_note_edit edit{}; - edit.velocity = edit.channel = -1; - edit.transpose = key == UI_Up ? 1 : -1; - apply(edit); - return 1; - } - } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Delete) { - delete_selected(); - return 1; - } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Escape) { - mode_ = 0; - return 1; - } - if (event == UI_FOCUS || event == UI_UNFOCUS) - return 1; - return UiWidget::handle(event); -} - -bool PianoRollWidget::visible(const xtc_note_info &n) const -{ - return n.clip_id == clip_ && (channel_filter_ < 0 || n.channel == static_cast(channel_filter_)) && - n.pitch >= low_ && n.pitch <= high_; -} - -xtc_result PianoRollWidget::set_grid(uint64_t ticks) -{ - if (xtc_ui::snap_index(ticks) < 0) - return XTC_ERR_INVALID_ARGUMENT; - grid_ = ticks; - redraw(); - return XTC_OK; -} - -xtc_result PianoRollWidget::set_filter(int channel, unsigned int low, unsigned int high) -{ - if (channel < -1 || channel > 15 || low > high || high > 127) return XTC_ERR_INVALID_ARGUMENT; - channel_filter_ = channel; - low_ = low; - high_ = high; - selected_ = UINT64_MAX; - all_ = false; - mode_ = 0; - redraw(); - return XTC_OK; -} - -void PianoRollWidget::select_filtered() -{ - selected_ = UINT64_MAX; - all_ = true; - mode_ = 0; - redraw(); -} - -xtc_result PianoRollWidget::apply(const xtc_note_edit &edit) -{ - if (!all_ && selected_ == UINT64_MAX) return XTC_ERR_NOT_FOUND; - xtc_note_filter filter{clip_, all_ ? UINT64_MAX : selected_, channel_filter_, low_, high_}; - size_t count; - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_edit_notes(project_, &filter, &edit, &count); - if (r == XTC_OK && edit.action == XTC_NOTES_DELETE) { - selected_ = UINT64_MAX; - all_ = false; - } - return change(r); -} - -void PianoRollWidget::command(int action) -{ - const char *text; - xtc_note_edit edit{}; - edit.velocity = edit.channel = -1; - if (action == 0) { - text = ui_input("Snap ticks: 1, 120, 160 (1/16 triplet), 240, 320 (1/8 triplet), 480, 960, 1920, 3840", "240"); - unsigned int value; - if (text && (!parse(text, value) || set_grid(value) != XTC_OK)) - ui_alert("Choose one of the listed grids."); - return; - } - if (action == 1) { - text = ui_input("Channel (-1 all, 0..15), lowest pitch, highest pitch", "-1 0 127"); - int channel; unsigned int low, high; - if (text && (!parse(text, channel, low, high) || - set_filter(channel, low, high) != XTC_OK)) ui_alert("Invalid note filter."); - return; - } - if (action == 6) { select_filtered(); return; } - if (action == 7) { delete_selected(); return; } - if (action == 2) { - text = ui_input("Velocity (1..127, -1 preserve), channel (0..15, -1 preserve)", "100 -1"); - if (!text) return; - if (!parse(text, edit.velocity, edit.channel)) { - ui_alert("Enter velocity and channel."); return; - } - } else if (action == 3) edit.grid_tick = grid_; - else if (action == 4) { - text = ui_input("Transpose semitones (-127..127)", "12"); - if (!text) return; - if (!parse(text, edit.transpose)) { - ui_alert("Enter a semitone count."); return; - } - } else if (action == 5) { - text = ui_input("Additional copies (1..4096), spacing in source ticks (clip must have room)", "1 960"); - unsigned long long interval; - if (!text) return; - if (!parse(text, edit.copies, interval)) { - ui_alert("Enter copy count and spacing."); return; - } - edit.action = XTC_NOTES_REPEAT; - edit.interval_tick = static_cast(interval); - } else return; - apply(edit); -} - -xtc_result PianoRollWidget::set_velocity(uint64_t id, unsigned int velocity) -{ - if (velocity < 1 || velocity > 127) return XTC_ERR_INVALID_ARGUMENT; - size_t count = 0; xtc_project_note_count(project_,&count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; xtc_project_get_note(project_,i,¬e); - if (note.id != id || !visible(note)) continue; - if (note.velocity == velocity) return XTC_OK; - xtc_result r = before_ ? before_(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_edit_note(project_,id,note.start_tick,note.length_tick,note.pitch,velocity,note.channel); - return change(r); - } - return XTC_ERR_NOT_FOUND; -} diff --git a/src/gui/JUCE/piano_roll.hpp b/src/gui/JUCE/piano_roll.hpp deleted file mode 100644 index 944d186..0000000 --- a/src/gui/JUCE/piano_roll.hpp +++ /dev/null @@ -1,150 +0,0 @@ -#ifndef XTC_PIANO_ROLL_HPP -#define XTC_PIANO_ROLL_HPP -#include "ui.hpp" -#include "xtc/xtc_midi.h" - -class PianoRollWidget : public UiWidget { - public: - /** - * @brief Prepares a model edit using the widget's opaque callback context. - * - * @return XTC_OK to allow the edit, or an error to cancel it. - */ - using BeforeEdit = xtc_result (*)(void *); - /** - * @brief Creates a piano-roll editor for a MIDI clip. - * - * The pre-edit guard, when non-NULL, is called before every project - * mutation and can veto the edit by returning an error. - * - * @param x Left edge of the widget in the parent's coordinate system. - * @param y Top edge of the widget in the parent's coordinate system. - * @param project Borrowed project that must outlive the widget. - * @param clip ID of the MIDI clip to edit. - * @param before Optional pre-edit guard; NULL means no guard is used. - * @param context Opaque pointer passed to @p before on every edit. - */ - PianoRollWidget(int x, int y, xtc_project *project, uint64_t clip, BeforeEdit before, void *context); - /** - * @brief Points the editor at a different project or clip. - * - * Clears any in-progress drag when the project or clip changes and resizes - * the widget to fit the clip's source length. Selection is cleared on a new - * model or clip. - * - * @param project Borrowed project that must outlive the widget. - * @param clip ID of the MIDI clip to display. - */ - void set_project(xtc_project *project, uint64_t clip); - - /** - * @brief Returns the ID of the clip currently being edited. - * - * @return Clip identifier supplied to the constructor or set_project(). - */ - uint64_t clip() const - { - return clip_; - } - - /** - * @brief Adds a note to the current clip and selects the new note. - * - * Invokes the pre-edit guard, if any, before mutating the project. - * - * @param start Note start in clip source ticks. - * @param length Note length in ticks. - * @param pitch MIDI pitch from 0 to 127. - * @param velocity MIDI velocity from 1 to 127. - * @param channel MIDI channel from 0 to 15. - * @return XTC_OK on success, the guard's error, or the project error. - */ - xtc_result add_note(uint64_t start, uint64_t length, unsigned int pitch, unsigned int velocity = 100, - unsigned int channel = 0); - /** - * @brief Removes the selected note from the current clip. - * - * Invokes the pre-edit guard, if any, before mutating the project. The - * selection is cleared only when the removal succeeds. - * - * @return XTC_OK on success, the guard's error, or the project error. - */ - xtc_project *project() const { return project_; } - bool note_visible(const xtc_note_info ¬e) const { return visible(note); } - int note_x(uint64_t tick) const { return x()+64+static_cast(tick*96/960); } - uint64_t new_note_ticks=960; - unsigned int new_note_velocity=100,new_note_channel=0; - int filter_channel() const { return channel_filter_; } - unsigned int filter_low() const { return low_; } - unsigned int filter_high() const { return high_; } - uint64_t grid() const { return grid_; } - xtc_result set_velocity(uint64_t note, unsigned int velocity); - uint64_t selected_note() const { return selected_; } - bool select_note(uint64_t id); - xtc_result delete_selected(); - /* UI preferences; independent of persisted project state. */ - xtc_result set_grid(uint64_t ticks); - xtc_result set_filter(int channel, unsigned int low, unsigned int high); - void select_filtered(); - xtc_result apply(const xtc_note_edit &edit); - /* Menu/keyboard commands: grid, filter, properties, quantize, transpose, - repeat, select all, delete. */ - void command(int action); - - /** - * @brief Handles mouse, keyboard, and focus events for the editor. - * - * Left-click on empty space adds a note, clicking a note selects it, and - * dragging moves or resizes it. Right-click and Delete remove the selected - * note; Escape cancels an in-progress drag. - * - * @param event JUCE event code. - * @return 1 when the event was handled, otherwise the result of the base - * class handler. - */ - int handle(int event) override; - - protected: - /** - * @brief Draws the keyboard, beat grid, and notes of the current clip. - * - * Paints only the background when the clip is missing or is not a MIDI clip. - */ - void draw() override; - - private: - xtc_project *project_; - uint64_t clip_, selected_ = UINT64_MAX; - BeforeEdit before_; - void *context_; - xtc_note_info original_{}; - int mode_ = 0, drag_x_ = 0, drag_y_ = 0; - uint64_t grid_ = 240; - int channel_filter_ = -1; - unsigned int low_ = 0, high_ = 127; - bool all_ = false; - bool visible(const xtc_note_info &n) const; - - /** - * @brief Reports an edit result and notifies listeners on success. - * - * On success the widget callback runs and the editor is redrawn; on failure - * an alert shows the error and the project is left unchanged. - * - * @param result Result of the attempted project mutation. - * @return The same result code. - */ - xtc_result change(xtc_result result); - /** - * @brief Converts a horizontal pixel position to a snapped tick. - * - * The position is measured in absolute window coordinates and snapped down - * to the current configured grid. - * - * @param px Horizontal position in window coordinates. - * @return Tick offset within the clip source, or 0 for positions on or left - * of the keyboard. - */ - uint64_t tick(int px) const; -}; -#endif diff --git a/src/gui/JUCE/plugin_dialog.cpp b/src/gui/JUCE/plugin_dialog.cpp deleted file mode 100644 index 678396d..0000000 --- a/src/gui/JUCE/plugin_dialog.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "theme.hpp" -#include "plugin_dialog.hpp" -#include "ui.hpp" -#include -#include -#include -#include - -namespace { -struct Dialog : UiDouble_Window { - bool accepted = false; - Dialog(int width, int height, const char *title) : UiDouble_Window(width,height,title) { set_modal(); } - static void accept(UiWidget *, void *data) { auto *d = static_cast(data); d->accepted = true; d->hide(); } - static void cancel(UiWidget *, void *data) { static_cast(data)->hide(); } - bool run() { callback(cancel,this); end(); xtc_ui::style(this); show(); while (shown()) Ui::wait(); return accepted; } -}; -} -bool xtc_choose_plugin(const char *path, double rate, unsigned int &index, bool instrument) -{ - Dialog dialog(760,450,"Plugin descriptors"); - auto *heading = new UiBox(15,10,730,40); - heading->copy_label(path); heading->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_CLIP); - auto *list = new UiHold_Browser(15,55,730,325); - list->format_char(0); // Plugin names are data, never browser formatting. - std::vector indices; - size_t rejected = 0; - for (unsigned int i = 0; i < 1024; ++i) { - xtc_plugin_spec spec{}; xtc_plugin_details details{}; - xtc_result r = xtc_plugin_probe(path,i,rate,&spec); - if (r == XTC_ERR_NOT_FOUND) break; - if (r != XTC_OK) { ++rejected; continue; } - if (!!spec.instrument != instrument) { ++rejected; continue; } - if (xtc_plugin_describe(&spec,rate,&details) != XTC_OK) { ++rejected; continue; } - std::string row = std::to_string(i) + ": " + details.name + " (" + spec.label + ") — " + details.maker + - (spec.inputs ? " [effect]" : " [source]"); - list->add(row.c_str()); indices.push_back(i); - } - auto *status = new UiBox(15,385,520,50); - std::string summary = std::to_string(indices.size()) + " compatible; " + std::to_string(rejected) + - " unsupported/unavailable. Select a descriptor."; - status->copy_label(summary.c_str()); status->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP); - auto *cancel = new UiButton(540,400,90,30,"Cancel"); cancel->callback(Dialog::cancel,&dialog); - auto *choose = new UiReturn_Button(645,400,100,30,"Choose"); choose->callback(Dialog::accept,&dialog); - if (indices.empty()) choose->deactivate(); else list->select(1); - if (!dialog.run() || list->value() < 1) return false; - index = indices[static_cast(list->value()-1)]; return true; -} -bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details, xtc_plugin_spec &edited) -{ - Dialog dialog(800,600,"Plugin identity and controls"); - auto *identity = new UiBox(15,10,770,125); - std::string text = std::string(details.name) + " — " + details.maker + "\n" + spec.label + - " | " + std::string(xtc_plugin_format_name(spec.format)) + " ID " + std::to_string(spec.unique_id) + " | descriptor " + std::to_string(spec.index) + " | identity " + spec.identifier + - "\n" + spec.path + "\n" + details.copyright + - "\nMetadata verified; Apply validates initialization. Adapter may not expose units or editable controls."; - identity->copy_label(text.c_str()); identity->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP); - auto *scroll = new UiScroll(10,140,780,370); - std::array inputs{}; - for (unsigned int i = 0; i < spec.control_count; ++i) { - int y = 150 + static_cast(i)*65; - const auto &p = details.parameters[i]; - std::string name = std::to_string(p.id) + ": " + p.name; - auto *label = new UiBox(25,y,425,25); label->copy_label(name.c_str()); label->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - inputs[i] = new UiValue_Input(460,y,150,25); inputs[i]->value(spec.values[i]); - inputs[i]->step((details.flags[i] & (XTC_PLUGIN_INTEGER|XTC_PLUGIN_TOGGLED)) ? 1 : 0.001); - std::string hint = "Range " + std::string(details.flags[i] & XTC_PLUGIN_BOUNDED_BELOW ? std::to_string(p.minimum) : "unbounded") + - " .. " + (details.flags[i] & XTC_PLUGIN_BOUNDED_ABOVE ? std::to_string(p.maximum) : "unbounded") + - "; default " + std::to_string(p.default_value) + - ((details.flags[i] & XTC_PLUGIN_TOGGLED) ? "; toggle 0/1" : (details.flags[i] & XTC_PLUGIN_INTEGER) ? "; integer" : "; continuous") + - ((details.flags[i] & XTC_PLUGIN_LOGARITHMIC) ? "; logarithmic hint" : ""); - auto *range = new UiBox(25,y+27,715,25); range->copy_label(hint.c_str()); range->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - } - if(!spec.control_count) { - auto *unavailable=new UiBox(25,160,730,75,"This adapter exposes no editable parameters. The plugin runs with its default configuration.\nNative plugin editor windows are not available in this host."); - unavailable->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP); - } - scroll->end(); - auto *note = new UiBox(15,515,770,30,"Automation overrides static controls. Reset defaults also clears all plugin automation."); - note->labelsize(12); - struct Reset { const xtc_plugin_details *details; std::array *inputs; unsigned int count; bool used; } reset{&details,&inputs,spec.control_count,false}; - auto *defaults = new UiButton(20,555,170,30,"Reset defaults"); - if(!spec.control_count)defaults->deactivate(); - defaults->callback([](UiWidget *, void *data) { - auto *r = static_cast(data); r->used = true; - for (unsigned int i = 0; i < r->count; ++i) (*r->inputs)[i]->value(r->details->parameters[i].default_value); - },&reset); - auto *cancel = new UiButton(570,555,90,30,"Cancel"); cancel->callback(Dialog::cancel,&dialog); - auto *apply = new UiReturn_Button(675,555,105,30,"Apply"); apply->callback(Dialog::accept,&dialog); - if (!dialog.run()) return false; - xtc_plugin_spec candidate = spec; - for (unsigned int i = 0; i < spec.control_count; ++i) { - double value = inputs[i]->value(); const auto &p = details.parameters[i]; unsigned int flags = details.flags[i]; - if (!std::isfinite(value) || value < p.minimum || value > p.maximum || - ((flags & XTC_PLUGIN_INTEGER) && value != std::floor(value)) || - ((flags & XTC_PLUGIN_TOGGLED) && value != 0 && value != 1)) { - ui_alert("Invalid value for port %u (%s); no controls were changed.",p.id,p.name); return false; - } - candidate.values[i] = static_cast(value); - } - if (reset.used) candidate.point_count = 0; - edited = candidate; return true; -} diff --git a/src/gui/JUCE/plugin_dialog.hpp b/src/gui/JUCE/plugin_dialog.hpp deleted file mode 100644 index a1aad8f..0000000 --- a/src/gui/JUCE/plugin_dialog.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef XTC_PLUGIN_DIALOG_HPP -#define XTC_PLUGIN_DIALOG_HPP -#include "xtc/xtc_plugin.h" -/* Modal, control-thread-only views. They own copied metadata and widget state, - * never engine/module handles. Cancel/close leaves caller configuration intact. */ -bool xtc_choose_plugin(const char *path, double rate, unsigned int &index, bool instrument = false); -bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details, - xtc_plugin_spec &edited); -#endif diff --git a/src/gui/JUCE/plugin_library.hpp b/src/gui/JUCE/plugin_library.hpp deleted file mode 100644 index c80dd44..0000000 --- a/src/gui/JUCE/plugin_library.hpp +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef XTC_PLUGIN_LIBRARY_HPP -#define XTC_PLUGIN_LIBRARY_HPP -#include "xtc/xtc_settings.h" -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include -#include -#include - -inline std::vector xtc_plugin_files(const xtc_settings &settings) -{ - namespace fs=std::filesystem; - std::set files; - size_t visited=0; - for(const char *list : {settings.clap_paths,settings.vst2_paths,settings.vst3_paths,settings.ladspa_paths}) { - std::string paths=list; size_t start=0; - while(start<=paths.size()) { - size_t end=paths.find(':',start); std::string root=paths.substr(start,end==std::string::npos ? end : end-start); - if(root.rfind("~/",0)==0) { char config[4096]; if(xtc_settings_path(config,sizeof(config))==XTC_OK) { - std::string home=config; home.resize(home.size()-10); root=home+root.substr(1); - } } - std::error_code error; - if(!root.empty() && fs::path(root).is_absolute()) { - fs::recursive_directory_iterator it(root,fs::directory_options::skip_permission_denied,error), finish; - while(!error && it!=finish && visited<4096 && files.size()<512) { - ++visited; - if(it.depth()>=6) it.disable_recursion_pending(); - if(it->is_regular_file(error)) { - auto extension=it->path().extension().string(); - if(extension==".so" || extension==".clap") files.insert(it->path().lexically_normal().string()); - } - error.clear(); it.increment(error); - } - } - if(end==std::string::npos) break; - start=end+1; - } - } - return {files.begin(),files.end()}; -} -/* Paths are a discovery hint, never a compatibility claim. Ambiguous .so files - * remain visible under both LADSPA and VST2; probing identifies the real ABI. */ -inline bool xtc_plugin_file_matches(const std::string &file,const std::string &query,int format) -{ - auto lower=[](std::string value){for(char &c:value)c=static_cast(std::tolower(static_cast(c)));return value;}; - const auto name=lower(file);bool vst3=name.find(".vst3/")!=std::string::npos; - bool clap=std::filesystem::path(name).extension()==".clap"; - if(format==3 && !vst3)return false; - if(format==4 && !clap)return false; - if((format==1 || format==2) && (vst3 || clap))return false; - return name.find(lower(query))!=std::string::npos; -} -inline bool xtc_choose_plugin_file(const xtc_settings &settings,std::string &path) -{ - struct Library : UiDouble_Window { - bool accepted=false,browse=false; - UiInput *search;UiChoice *format;UiHold_Browser *list; - UiButton *choose;std::vector files,visible; - explicit Library(const xtc_settings &settings):UiDouble_Window(900,560,"Plugin library — effects and instruments"),files(xtc_plugin_files(settings)) { - set_modal(); - auto *hint=new UiBox(16,8,868,42,"Search installed binaries; VST3 bundles are discovered recursively. Modules load only when inspected.\nLADSPA/VST2 share .so candidates; inspection verifies the format and compatible mono layout.");hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - search=new UiInput(78,60,510,28,"Search");search->when(UI_WHEN_CHANGED); - format=new UiChoice(666,60,218,28,"Format");format->add("All formats|LADSPA|VST2|VST3|CLAP");format->value(0); - for(auto *w:{static_cast(search),static_cast(format)})w->callback([](UiWidget *,void *c){static_cast(c)->filter();},this); - list=new UiHold_Browser(16,102,868,370);list->format_char(0); - auto *browse_button=new UiButton(16,506,130,32,"Browse files...");browse_button->callback([](UiWidget *,void *c){auto *d=static_cast(c);d->browse=true;d->hide();},this); - auto *cancel=new UiButton(650,506,100,32,"Cancel");cancel->callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - choose=new UiButton(764,506,120,32,"Inspect plugin");choose->callback([](UiWidget *,void *c){auto *d=static_cast(c);d->accepted=true;d->hide();},this); - callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - end();filter();xtc_ui::style(this); - } - void filter() { - list->clear();visible.clear(); - for(const auto &file:files)if(xtc_plugin_file_matches(file,search->value(),format->value())){visible.push_back(file);list->add(file.c_str());} - if(visible.empty())choose->deactivate();else{choose->activate();list->select(1);} - } - } dialog(settings); - dialog.show();while(dialog.shown())Ui::wait(); - if(dialog.browse) { - UiNative_File_Chooser chooser;chooser.title("Choose trusted plugin binary");chooser.filter("Plugin binaries\t*.{so,clap}\nAll files\t*"); - if(chooser.show()!=0 || !chooser.filename())return false; - path=chooser.filename();return true; - } - if(!dialog.accepted || dialog.list->value()<1)return false; - path=dialog.visible[static_cast(dialog.list->value()-1)];return true; -} -#endif diff --git a/src/gui/JUCE/preferences_dialog.hpp b/src/gui/JUCE/preferences_dialog.hpp deleted file mode 100644 index 3de0f2e..0000000 --- a/src/gui/JUCE/preferences_dialog.hpp +++ /dev/null @@ -1,103 +0,0 @@ -#ifndef XTC_PREFERENCES_DIALOG_HPP -#define XTC_PREFERENCES_DIALOG_HPP -#include "xtc/xtc_settings.h" -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include -#include - -class PreferencesDialog : public UiDouble_Window { - xtc_settings original_; - std::function apply_; - std::vector inputs_; - UiBox *message_; - std::function(bool)> devices_; - std::vector midi_names_[2]; - UiInput_Choice *midi_input_=nullptr,*midi_output_=nullptr; - void refresh_devices() { - for(auto pair:{std::pair{midi_input_,true},{midi_output_,false}}){ - if(!pair.first)continue; - std::string value=pair.first->value(); - // copy() owns the menu array but borrows its label strings. - // Release old items before replacing their backing storage. - pair.first->menubutton()->clear(); - auto &names=midi_names_[pair.second ? 0 : 1]; - names=devices_ ? devices_(pair.second) : std::vector{}; - names.insert(names.begin(),"");std::vector items(names.size()+1); - for(size_t i=0;imenubutton()->copy(items.data());pair.first->value(value.c_str()); - } - } - - static void apply_callback(UiWidget *,void *context) { - auto *d=static_cast(context); - xtc_settings candidate=d->original_; size_t count=0; - const xtc_setting_info *schema=xtc_settings_schema(&count); - for(size_t i=0;iinputs_[i]->value())!=XTC_OK) { - std::string text=std::string("Invalid value: ")+schema[i].label; - d->message_->copy_label(text.c_str()); return; - } - xtc_result r=xtc_settings_validate(&candidate); - if(r==XTC_OK) r=d->apply_(candidate); - if(r==XTC_OK) { - d->original_=candidate; - d->message_->copy_label("Saved and applied. Audio options take effect on Reconnect; project defaults on New."); - } else { - std::string text=std::string("Preferences unchanged: ")+xtc_result_string(r)+". Check values, loop range and configuration file permissions."; - d->message_->copy_label(text.c_str()); - } - d->redraw(); - } - public: - PreferencesDialog(const xtc_settings &settings,const std::string &path, - std::function apply, - std::function(bool)> devices={}) - : UiDouble_Window(1050,660,"xtc — Preferences"),original_(settings),apply_(std::move(apply)),devices_(std::move(devices)) { - set_modal(); begin(); - auto *location=new UiBox(16,12,1018,30); location->copy_label(path.c_str()); location->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *tabs=new UiTabs(12,50,1026,510); tabs->begin(); - size_t count=0; const auto *schema=xtc_settings_schema(&count); - inputs_.resize(count); std::string section; UiGroup *group=nullptr; UiScroll *scroll=nullptr; int row=0; - for(size_t i=0;iend(); group->end(); } - section=schema[i].section; tabs->begin(); - group=new UiGroup(12,82,1026,478); group->copy_label(section.c_str()); group->begin(); - scroll=new UiScroll(22,94,1000,450); scroll->begin(); row=0; - } - UiInput *input; - if(!strcmp(schema[i].key,"midi_input_port") || !strcmp(schema[i].key,"midi_output_port")){ - auto *choice=new UiInput_Choice(510,108+row*48,480,30,schema[i].label); - choice->callback([](UiWidget *w,void *){auto *c=static_cast(w);auto *item=c->menubutton()->mvalue();if(item && !strcmp(item->label(),"(None)"))c->value("");}); - input=choice->input();if(!strcmp(schema[i].key,"midi_input_port"))midi_input_=choice;else midi_output_=choice; - }else input=new UiInput(510,108+row*48,480,30,schema[i].label); - input->align(UI_ALIGN_LEFT); input->maximum_size(schema[i].capacity ? static_cast(schema[i].capacity-1) : 22); - char value[4096]; xtc_settings_get(&settings,i,value,sizeof(value)); input->value(value); - inputs_[i]=input; ++row; - } - if(scroll) { scroll->end(); group->end(); } tabs->end(); - tabs->begin();auto *midi_info=new UiGroup(12,82,1026,478,"MIDI help"); - auto *help=new UiBox(42,110,950,190,"Choose JACK/PipeWire MIDI ports on the midi tab. Refresh to see newly connected devices.\nYou can also type a remembered port name while it is offline; blank means no automatic route.\nRouting is restored on Play/Record; missing ports are skipped with a warning; playback continues and choices stay saved.\nInput/output channels use 0..15 for MIDI channels 1..16. MIDI thru defaults off.\nNote length/velocity/channel defaults are on editing; CC defaults are on project_defaults.\nHardware MIDI devices must be exposed by your host JACK/PipeWire MIDI bridge."); - help->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP);midi_info->end();tabs->end(); - refresh_devices(); - auto *refresh=new UiButton(16,614,200,30,"Refresh MIDI devices"); - refresh->callback([](UiWidget *,void *c){static_cast(c)->refresh_devices();},this); - message_=new UiBox(16,568,1018,36,"Apply saves every tab. Existing project contents are not overwritten by defaults."); - message_->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE|UI_ALIGN_WRAP); - auto *apply_button=new UiButton(812,614,100,30,"Apply"); apply_button->callback(apply_callback,this); - auto *close=new UiButton(924,614,110,30,"Close"); - close->callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - end(); xtc_ui::style(this); - } - ~PreferencesDialog() override { - // Destroy borrowed-label menus before the member strings are released. - if(midi_input_)midi_input_->menubutton()->clear(); - if(midi_output_)midi_output_->menubutton()->clear(); - } - void run() { show(); while(shown()) Ui::wait(); } -}; -#endif diff --git a/src/gui/JUCE/sample_editor.cpp b/src/gui/JUCE/sample_editor.cpp deleted file mode 100644 index dc2cbed..0000000 --- a/src/gui/JUCE/sample_editor.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "sample_editor.hpp" -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include -#include -class SampleCanvas : public UiWidget { - SampleEditor &editor_;size_t anchor_=0; - size_t frame(int px) const {size_t width=std::max(size_t(1),editor_.frames()/editor_.zoom());return std::min(editor_.frames()-1,editor_.origin()+static_cast(std::max(0,std::min(w()-1,px-x())))*width/static_cast(w()));} -public: - SampleCanvas(int x,int y,int w,int h,SampleEditor &e):UiWidget(x,y,w,h),editor_(e){} - int handle(int event) override { - if(event==UI_PUSH && Ui::event_button()==UI_LEFT_MOUSE){anchor_=frame(Ui::event_x());editor_.select(anchor_,anchor_+1);return 1;} - if(event==UI_DRAG){size_t f=frame(Ui::event_x());editor_.select(std::min(f,anchor_),std::max(f,anchor_)+1);return 1;} - return UiWidget::handle(event); - } - void draw() override { - ui_push_clip(x(),y(),w(),h());ui_color(xtc_ui::background());ui_rectf(x(),y(),w(),h()); - size_t width=std::max(size_t(1),editor_.frames()/editor_.zoom()); - for(int px=0;px(px)*width/static_cast(w());size_t e=std::min(editor_.frames(),editor_.origin()+static_cast(px+1)*width/static_cast(w()));if(b>=editor_.frames())break;if(e<=b)e=b+1; - if(beditor_.begin()){ui_color(ui_rgb_color(45,76,80));ui_rectf(x()+px,y(),1,h());} - xtc_sample_stats stats{};if(xtc_sample_analyze(editor_.sample(),b,e,&stats)==XTC_OK){ui_color(ui_rgb_color(95,204,182));int a=static_cast(std::max(-1.f,std::min(1.f,stats.maximum))*(h()/2-8));int z=static_cast(std::max(-1.f,std::min(1.f,stats.minimum))*(h()/2-8));ui_line(x()+px,y()+h()/2-a,x()+px,y()+h()/2-z);} - } - ui_color(ui_rgb_color(220,164,92));for(size_t frame:editor_.attacks()){if(frame=width)continue;int px=x()+static_cast((frame-editor_.origin())*static_cast(w())/width);ui_line(px,y(),px,y()+h());} - ui_color(xtc_ui::border());ui_line(x(),y()+h()/2,x()+w(),y()+h()/2);ui_pop_clip(); - } -}; -SampleEditor::SampleEditor(Data sample):UiDouble_Window(1100,650,"xtc — Sample editor"),sample_(std::move(sample)) -{ - if(!sample_)throw std::invalid_argument("Sample editor requires a loaded source"); - xtc_sample_info(sample_.get(),&rate_,&frames_);end_=frames_;set_modal();UiGroup::begin(); - auto *hint=new UiBox(16,12,1068,34,"Source frames / mono downmix — drag a range; numeric endpoints are exclusive. Original file unchanged.");hint->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - begin_input_=new UiValue_Input(90,58,140,28,"Start frame");end_input_=new UiValue_Input(330,58,140,28,"End frame");origin_input_=new UiValue_Input(580,58,140,28,"View start"); - for(auto *input:{begin_input_,end_input_,origin_input_}){input->step(1);input->bounds(0,static_cast(frames_));input->when(UI_WHEN_RELEASE);input->callback([](UiWidget *,void *c){auto *e=static_cast(c);double b=e->begin_input_->value(),z=e->end_input_->value(),o=e->origin_input_->value();if(std::isfinite(b)&&std::isfinite(z)&&std::isfinite(o)&&b>=0&&z>=0&&o>=0&&b<=e->frames_&&z<=e->frames_&&oframes_){e->select(static_cast(b),static_cast(z));e->view(static_cast(o),e->zoom_);}e->refresh();},this);} - auto *plus=new UiButton(750,58,100,28,"Zoom in");plus->callback([](UiWidget *,void *c){auto *e=static_cast(c);e->view(e->begin_,std::min(64U,e->zoom_*2));},this); - auto *minus=new UiButton(860,58,100,28,"Zoom out");minus->callback([](UiWidget *,void *c){auto *e=static_cast(c);e->view(e->origin_,std::max(1U,e->zoom_/2));},this); - canvas_=new SampleCanvas(16,104,1068,340,*this);stats_=new UiBox(16,452,1068,50);stats_->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - auto *trim_button=new UiButton(16,520,180,30,"Trim clip to selection");trim_button->callback([](UiWidget *,void *c){auto r=static_cast(c)->trim_clip();if(r!=XTC_OK)ui_alert("Clip trim unchanged: %s",xtc_result_string(r));},this); - auto *save=new UiButton(210,520,210,30,"Save full copy and relink...");save->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *path=ui_input("NEW absolute WAV path (full source; project undo keeps the new file)","");if(path){auto r=e->save_copy(path,true);if(r!=XTC_OK)ui_alert("Copy not adopted: %s",xtc_result_string(r));}},this); - auto *export_button=new UiButton(434,520,200,30,"Export selected frames...");export_button->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *path=ui_input("NEW absolute WAV path (selection only)","");if(path){auto r=e->export_selection(path);if(r!=XTC_OK)ui_alert("Export failed: %s",xtc_result_string(r));}},this); - auto *reverse=new UiButton(16,562,160,30,"Reverse selection");reverse->callback([](UiWidget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_REVERSE);if(r!=XTC_OK)ui_alert("Reversal failed: %s",xtc_result_string(r));},this); - auto *invert=new UiButton(188,562,160,30,"Invert polarity");invert->callback([](UiWidget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_INVERT);if(r!=XTC_OK)ui_alert("Polarity edit failed: %s",xtc_result_string(r));},this); - auto *dc=new UiButton(360,562,160,30,"Remove DC offset");dc->callback([](UiWidget *,void *c){auto r=static_cast(c)->process(XTC_SAMPLE_REMOVE_DC);if(r!=XTC_OK)ui_alert("DC removal failed: %s",xtc_result_string(r));},this); - auto *fade=new UiButton(532,562,160,30,"Sample fades...");fade->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Fade: 0 linear in, 1 linear out, 2 equal-power in, 3 equal-power out","0");if(!text)return;unsigned int mode;char extra;if(std::sscanf(text,"%u %c",&mode,&extra)!=1||mode>3){ui_alert("Enter a fade mode 0..3.");return;}auto r=e->process(static_cast(XTC_SAMPLE_FADE_IN+mode));if(r!=XTC_OK)ui_alert("Fade failed: %s",xtc_result_string(r));},this); - auto *gain=new UiButton(704,562,160,30,"Gain / normalize...");gain->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Mode (0 gain, 1 normalize), linear gain 0..16 or target peak >0..1","1 0.9");if(!text)return;unsigned int mode;double value;char extra;if(std::sscanf(text,"%u %lf %c",&mode,&value,&extra)!=2||mode>1){ui_alert("Enter mode and linear value.");return;}auto r=e->process(mode ? XTC_SAMPLE_NORMALIZE : XTC_SAMPLE_GAIN,value);if(r!=XTC_OK)ui_alert("Gain edit failed: %s",xtc_result_string(r));},this); - auto *silence=new UiButton(876,562,200,30,"Select audible bounds...");silence->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Silence threshold dBFS (-120..0), padding ms (0..10000)","-60 2");if(!text)return;double db,ms;char extra;if(std::sscanf(text,"%lf %lf %c",&db,&ms,&extra)!=2||!std::isfinite(db)||!std::isfinite(ms)||db< -120||db>0||ms<0||ms>10000){ui_alert("Enter a threshold and padding within the shown bounds.");return;}auto r=e->select_audible(std::pow(10.,db/20),static_cast(std::min(static_cast(e->frames_),ms*e->rate_/1000)));if(r!=XTC_OK)ui_alert("Audible bounds not found: %s",xtc_result_string(r));},this); - auto *zero=new UiButton(16,604,180,30,"Snap to zero crossings...");zero->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Search radius in source frames (0..65536)","256");if(!text)return;unsigned int radius;char extra;if(std::sscanf(text,"%u %c",&radius,&extra)!=1||radius>65536){ui_alert("Enter a radius 0..65536.");return;}auto r=e->snap_zero_crossings(radius);if(r!=XTC_OK)ui_alert("Selection unchanged: %s",xtc_result_string(r));},this); - auto *attacks=new UiButton(210,604,180,30,"Detect attacks...");attacks->callback([](UiWidget *,void *c){auto *e=static_cast(c);const char *text=ui_input("Attack threshold dBFS (-120..0), minimum gap ms (>0..10000)","-12 80");if(!text)return;double db,ms;char extra;if(std::sscanf(text,"%lf %lf %c",&db,&ms,&extra)!=2||!std::isfinite(db)||!std::isfinite(ms)||db< -120||db>0||ms<=0||ms>10000){ui_alert("Enter a threshold and positive minimum gap.");return;}auto r=e->detect_attacks(std::pow(10.,db/20),std::max(size_t(1),static_cast(std::min(static_cast(e->frames_),ms*e->rate_/1000))));if(r!=XTC_OK)ui_alert("Attack detection failed: %s",xtc_result_string(r));},this); - auto *slices=new UiButton(404,604,180,30,"Slice clip at attacks");slices->callback([](UiWidget *,void *c){auto r=static_cast(c)->slice_clip();if(r!=XTC_OK)ui_alert("Clip unchanged: %s",xtc_result_string(r));else static_cast(c)->hide();},this); - auto *undo=new UiButton(850,604,110,30,"Undo / redo");undo->callback([](UiWidget *,void *c){auto r=static_cast(c)->undo_sample();if(r!=XTC_OK)ui_alert("No local sample edit to undo.");},this); - auto *close=new UiButton(970,604,110,30,"Close");close->callback([](UiWidget *,void *c){static_cast(c)->hide();},this);callback([](UiWidget *,void *c){static_cast(c)->hide();},this); - UiGroup::end();xtc_ui::style(this);refresh(); -} -xtc_result SampleEditor::select(size_t b,size_t e){if(b>=e||e>frames_)return XTC_ERR_INVALID_ARGUMENT;if(begin_!=b||end_!=e)attacks_.clear();begin_=b;end_=e;refresh();return XTC_OK;} -xtc_result SampleEditor::view(size_t b,unsigned int zoom){if(b>=frames_||!zoom||zoom>64)return XTC_ERR_INVALID_ARGUMENT;origin_=b;zoom_=zoom;refresh();return XTC_OK;} -void SampleEditor::refresh(){begin_input_->value(static_cast(begin_));end_input_->value(static_cast(end_));origin_input_->value(static_cast(origin_));xtc_sample_stats s{};xtc_sample_analyze(sample_.get(),begin_,end_,&s);char text[256];std::snprintf(text,sizeof(text),"%u Hz / %zu source frames / selection %zu..%zu (%0.6f s) / zoom %ux\nMin %.6f Max %.6f RMS %.6f DC %.6f",rate_,frames_,begin_,end_,static_cast(end_-begin_)/rate_,zoom_,s.minimum,s.maximum,s.rms,s.mean);stats_->copy_label(text);std::string attacks="Detected source frames (nearest tick used for slicing):";for(size_t frame:attacks_)attacks+=" "+std::to_string(frame);stats_->copy_tooltip(attacks.c_str());canvas_->redraw();} -void SampleEditor::run(){show();while(shown())Ui::wait();} - -xtc_result SampleEditor::trim_clip(){return trim ? trim(rate_,begin_,end_) : XTC_ERR_INVALID_STATE;} - -xtc_result SampleEditor::save_copy(const char *path,bool use_in_project){if(use_in_project)return adopt ? adopt(sample_.get(),path) : XTC_ERR_INVALID_STATE;return xtc_sample_write(sample_.get(),0,frames_,path);} -xtc_result SampleEditor::export_selection(const char *path){return xtc_sample_write(sample_.get(),begin_,end_,path);} - -xtc_result SampleEditor::process(xtc_sample_operation operation,double amount) -{ - xtc_sample *next=nullptr;auto r=xtc_sample_process(sample_.get(),begin_,end_,operation,amount,&next);if(r!=XTC_OK)return r; - previous_=std::move(sample_);sample_.reset(next);attacks_.clear();copy_label("xtc — Sample editor / local sample changes; save a copy to keep");refresh();return XTC_OK; -} -xtc_result SampleEditor::undo_sample(){if(!previous_)return XTC_ERR_NOT_FOUND;sample_.swap(previous_);attacks_.clear();refresh();return XTC_OK;} - -xtc_result SampleEditor::select_audible(double threshold,size_t padding){size_t b=0,e=0;auto r=xtc_sample_audible_bounds(sample_.get(),begin_,end_,threshold,padding,&b,&e);return r==XTC_OK ? select(b,e) : r;} - -xtc_result SampleEditor::snap_zero_crossings(size_t radius){size_t b=0,e=0;auto r=xtc_sample_zero_crossing(sample_.get(),begin_,radius,&b);if(r==XTC_OK)r=xtc_sample_zero_crossing(sample_.get(),end_,radius,&e);return r==XTC_OK ? select(b,e) : r;} - -xtc_result SampleEditor::detect_attacks(double threshold,size_t gap){size_t positions[XTC_SAMPLE_MAX_TRANSIENTS],count=0;auto r=xtc_sample_transients(sample_.get(),begin_,end_,threshold,gap,positions,XTC_SAMPLE_MAX_TRANSIENTS,&count);if(r!=XTC_OK)return r;try{std::vector next(positions,positions+count);attacks_.swap(next);}catch(const std::bad_alloc &){return XTC_ERR_OUT_OF_MEMORY;}refresh();return XTC_OK;} -xtc_result SampleEditor::slice_clip(){return slice&&!attacks_.empty() ? slice(rate_,attacks_) : XTC_ERR_INVALID_STATE;} diff --git a/src/gui/JUCE/sample_editor.hpp b/src/gui/JUCE/sample_editor.hpp deleted file mode 100644 index c2b35ac..0000000 --- a/src/gui/JUCE/sample_editor.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef XTC_SAMPLE_EDITOR_HPP -#define XTC_SAMPLE_EDITOR_HPP -#include "xtc/xtc_sample.h" -#include "ui.hpp" -#include -#include -#include -class UiValue_Input; -class UiBox; -class SampleCanvas; -class SampleEditor : public UiDouble_Window { -public: - /* Takes ownership of loaded snapshot. Modal edits never touch audio callbacks. */ - using Data=std::unique_ptr; - explicit SampleEditor(Data sample); - std::function trim; - xtc_result trim_clip(); - std::function adopt; - xtc_result save_copy(const char *path,bool use_in_project); - xtc_result export_selection(const char *path); - xtc_result process(xtc_sample_operation operation,double amount=0); - xtc_result undo_sample(); - xtc_result select_audible(double threshold,size_t padding); - xtc_result snap_zero_crossings(size_t radius); - std::function &)> slice; - xtc_result detect_attacks(double threshold,size_t gap); - xtc_result slice_clip(); - const std::vector &attacks() const {return attacks_;} - void run(); - xtc_result select(size_t begin,size_t end); - xtc_result view(size_t begin,unsigned int zoom); - const xtc_sample *sample() const {return sample_.get();} - size_t begin() const {return begin_;} size_t end() const {return end_;} - size_t frames() const {return frames_;} size_t origin() const {return origin_;} - unsigned int zoom() const {return zoom_;} - void refresh(); -private: - std::unique_ptr sample_{nullptr,xtc_sample_destroy}; - std::unique_ptr previous_{nullptr,xtc_sample_destroy}; - std::vector attacks_; - size_t frames_=0,begin_=0,end_=0,origin_=0;unsigned int rate_=0,zoom_=1; - UiValue_Input *begin_input_,*end_input_,*origin_input_;UiBox *stats_;SampleCanvas *canvas_; -}; -#endif diff --git a/src/gui/JUCE/sequencer_clip_widget.cpp b/src/gui/JUCE/sequencer_clip_widget.cpp deleted file mode 100644 index c493c43..0000000 --- a/src/gui/JUCE/sequencer_clip_widget.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "sequencer_clip_widget.hpp" -#include "sequencer_widget.hpp" -#include "theme.hpp" -#include "xtc/xtc_midi.h" -#include "ui.hpp" -#include - -SequencerClipWidget::SequencerClipWidget(SequencerWidget &owner, const xtc_clip_info &clip) - : UiWidget(0,0,1,46), owner_(owner), info_(clip) {} -void SequencerClipWidget::sync(const xtc_clip_info &clip, int top) -{ - info_=clip; - int left=owner_.x()+owner_.header_width_+static_cast(clip.start_tick*static_cast(owner_.scale_)/960U); - int width=std::max(4,static_cast(clip.length_tick*static_cast(owner_.scale_)/960U)); - resize(left,top+5,width,46); -} -void SequencerClipWidget::draw() -{ - const auto &c=info_; - int left=x(),py=y()-5,cx,cy,cw,ch; - ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - int a=std::max(left,cx),b=std::min(x()+w(),cx+cw); - if(b<=a) return; - size_t notes=0; xtc_project_note_count(owner_.project_,¬es); - UiColor color=xtc_ui::track(c.track_id); - ui_color(ui_color_average(color,xtc_ui::background(),owner_.clip_selected(c.id) ? .75f : .45f)); - ui_rectf(a,py+5,b-a,46); - ui_color(owner_.clip_selected(c.id) ? xtc_ui::text() : color); ui_rect(a,py+5,b-a,46); - ui_push_clip(a+1,py+6,std::max(0,b-a-2),44); - if (c.midi) { - /* Real note positions, pitch and duration; no placeholder waveform. */ - unsigned int low = 127, high = 0; - for (size_t n = 0; n < notes; ++n) { - xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e); - if (note.clip_id == c.id) { low = std::min(low,note.pitch); high = std::max(high,note.pitch); } - } - unsigned int range = high >= low ? std::max(12U,high-low) : 12U; - ui_color(ui_color_average(color,UI_WHITE,.55f)); - for (size_t n = 0; n < notes; ++n) { - xtc_note_info note{}; xtc_project_get_note(owner_.project_,n,¬e); - if (note.clip_id != c.id) continue; - uint64_t begin = std::max(note.start_tick,c.offset_tick); - uint64_t end = std::min(note.start_tick+note.length_tick,c.offset_tick+c.length_tick); - if (begin >= end) continue; - int nx = left+static_cast((begin-c.offset_tick)*static_cast(owner_.scale_)/960U); - int nw = std::max(2,static_cast((end-begin)*static_cast(owner_.scale_)/960U)); - ui_rectf(nx,py+26+static_cast((high-note.pitch)*18/range),nw,2); - } - } else owner_.draw_waveform(c,left,py); - ui_color(ui_color_average(color,xtc_ui::background(),.35f)); ui_rectf(a+1,py+6,std::max(0,b-a-2),15); - ui_font(UI_HELVETICA,11); ui_color(xtc_ui::text()); - ui_draw(c.name,left+7,py+18); - ui_pop_clip(); -} - -int SequencerClipWidget::handle(int event) -{ - if(event==UI_PUSH && (Ui::event_button()==UI_LEFT_MOUSE || Ui::event_button()==UI_RIGHT_MOUSE)) { - owner_.take_focus(); - uint64_t previous=owner_.clip_; - owner_.track_=info_.track_id; owner_.clip_=info_.id; - owner_.original_=info_; owner_.drag_x_=Ui::event_x(); - owner_.mode_=Ui::event_x()-x()<7 ? 2 : x()+w()-Ui::event_x()<7 ? 3 : 1; - if(Ui::event_state(UI_SHIFT)) { - owner_.clip_=previous; owner_.toggle_clip(info_.id); owner_.mode_=0; return 1; - } - owner_.selection_.clear(); - if(Ui::event_button()==UI_LEFT_MOUSE && Ui::event_clicks()) { - owner_.mode_=0; - if(owner_.open_clip) owner_.open_clip(owner_.context_); - return 1; - } - if(Ui::event_button()==UI_RIGHT_MOUSE) { owner_.mode_=0; owner_.context_menu(); } - owner_.redraw(); return 1; - } - if(event==UI_DRAG || event==UI_RELEASE) return owner_.handle(event); - return 0; -} diff --git a/src/gui/JUCE/sequencer_clip_widget.hpp b/src/gui/JUCE/sequencer_clip_widget.hpp deleted file mode 100644 index 7aea906..0000000 --- a/src/gui/JUCE/sequencer_clip_widget.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef XTC_SEQUENCER_CLIP_WIDGET_HPP -#define XTC_SEQUENCER_CLIP_WIDGET_HPP -#include "ui.hpp" -#include "xtc/xtc_project.h" -class SequencerWidget; -class SequencerClipWidget : public UiWidget { - SequencerWidget &owner_; - xtc_clip_info info_{}; - public: - SequencerClipWidget(SequencerWidget &owner, const xtc_clip_info &clip); - uint64_t clip_id() const { return info_.id; } - void sync(const xtc_clip_info &clip, int track_top); - int handle(int event) override; - protected: - void draw() override; -}; -#endif diff --git a/src/gui/JUCE/sequencer_commands.cpp b/src/gui/JUCE/sequencer_commands.cpp deleted file mode 100644 index 8fe10a9..0000000 --- a/src/gui/JUCE/sequencer_commands.cpp +++ /dev/null @@ -1,412 +0,0 @@ -#include "sequencer_widget.hpp" -#include "snap.hpp" -#include "xtc/xtc_midi.h" -#include "ui.hpp" -#include -#include -#include -#include -#include - -// User-facing editing commands use the master's single history/publication path. - -xtc_result SequencerWidget::add_track() -{ - if (before_edit) { - xtc_result r = before_edit(context_); - if (r != XTC_OK) { - changed(r); - return r; - } - } - uint64_t id; - xtc_result r = xtc_project_add_track(project_, "Track", &id); - if (r == XTC_OK) { - track_ = id; - clip_ = UINT64_MAX;selection_.clear(); - } - changed(r); - return r; -} - -xtc_result SequencerWidget::add_region() -{ - if (before_edit) { - xtc_result r = before_edit(context_); - if (r != XTC_OK) { - changed(r); - return r; - } - } - size_t tracks, clips; - xtc_project_get_counts(project_, &tracks, &clips); - if (track_ == UINT64_MAX && tracks) { - xtc_track_info t{}; - xtc_project_get_track(project_, 0, &t); - track_ = t.id; - } - uint64_t id; - xtc_result r = xtc_project_add_clip(project_, track_, "Region (silent)", cursor_, new_clip_ticks, &id); - if (r == XTC_OK) {clip_ = id;selection_.clear();} - changed(r); - return r; -} - -xtc_result SequencerWidget::add_midi_clip() -{ - xtc_result r = before_edit ? before_edit(context_) : XTC_OK; - uint64_t id; - if (r == XTC_OK) { - if (track_ == UINT64_MAX) { - xtc_track_info t{}; - r = xtc_project_get_track(project_, 0, &t); - track_ = r == XTC_OK ? t.id : UINT64_MAX; - } - if (r == XTC_OK) - r = xtc_project_add_midi_clip(project_, track_, cursor_, new_clip_ticks, &id); - if (r == XTC_OK) {clip_ = id;selection_.clear();} - } - changed(r); - return r; -} - -xtc_result SequencerWidget::delete_selection() -{ - if(!selected_clips().empty())return transform_selection(XTC_CLIPS_DELETE,0); - xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_remove_track(project_,track_); - if(r==XTC_OK)track_=clip_=UINT64_MAX; - changed(r);return r; -} - -xtc_result SequencerWidget::merge_midi() -{ - auto ids=selected_clips();uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_merge_midi(project_,ids.data(),ids.size(),&id); - if(r==XTC_OK){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);select(c.track_id,id);} - changed(r);return r; -} - -xtc_result SequencerWidget::duplicate_track(const char *name) -{ - const uint64_t source=track_;uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_duplicate_track(project_,source,name,&id); - if(r==XTC_OK)select(id); - changed(r);return r; -} - -void SequencerWidget::duplicate_track_dialog() -{ - const char *name=ui_input("Name for complete track duplicate","Track copy");if(name)duplicate_track(name); -} - -xtc_result SequencerWidget::ripple_time(uint64_t length,bool remove) -{ - const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_ripple_time(project_,at,length,remove); - changed(r);return r; -} - -void SequencerWidget::ripple_dialog(bool remove) -{ - const char *input=ui_input(remove ? "Remove time from cursor: ticks (split crossing clips first)" : "Insert time at cursor: ticks (split crossing clips first)","3840");if(!input)return; - uint64_t length=0;char extra;std::istringstream values(input); - if(!(values>>length) || (values>>extra) || !length || length>XTC_PROJECT_MAX_TICK){ui_alert("Enter a positive bounded tick duration.");return;} - ripple_time(length,remove); -} - -xtc_result SequencerWidget::marker(const char *name) -{ - const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=name ? xtc_project_set_marker(project_,at,name) : xtc_project_remove_marker(project_,at); - changed(r);return r; -} - -void SequencerWidget::marker_dialog() -{ - const char *name=ui_input("Marker at cursor (1..63 UTF-8 bytes)","Section");if(name)marker(name); -} - -std::vector SequencerWidget::selected_clips() const -{ - std::vector ids;xtc_clip_info c{}; - for(auto id:selection_)if(xtc_project_find_clip(project_,id,&c)==XTC_OK)ids.push_back(id); - if(ids.empty() && clip_!=UINT64_MAX && xtc_project_find_clip(project_,clip_,&c)==XTC_OK)ids.push_back(clip_); - return ids; -} - -bool SequencerWidget::clip_selected(uint64_t id) const -{ - return selection_.empty() ? id==clip_ : std::find(selection_.begin(),selection_.end(),id)!=selection_.end(); -} - -bool SequencerWidget::toggle_clip(uint64_t id) -{ - xtc_clip_info c{};if(xtc_project_find_clip(project_,id,&c)!=XTC_OK)return false; - if(selection_.empty() && clip_!=UINT64_MAX)selection_.push_back(clip_); - auto found=std::find(selection_.begin(),selection_.end(),id); - if(found==selection_.end())selection_.push_back(id);else selection_.erase(found); - clip_=selection_.empty() ? UINT64_MAX : selection_.back(); - if(clip_!=UINT64_MAX)xtc_project_find_clip(project_,clip_,&c); - track_=c.track_id;redraw();return true; -} - -void SequencerWidget::select_all_clips() -{ - selection_.clear();size_t tracks=0,clips=0;xtc_project_get_counts(project_,&tracks,&clips); - for(size_t i=0;i output(ids.size()); - xtc_result r=ids.empty() ? XTC_ERR_NOT_FOUND : XTC_OK; - if(r==XTC_OK && before_edit)r=before_edit(context_); - if(r==XTC_OK)r=xtc_project_transform_clips(project_,ids.data(),ids.size(),op,amount,output.data()); - if(r==XTC_OK) { - selection_=op==XTC_CLIPS_DELETE ? std::vector{} : output; - clip_=selection_.empty() ? UINT64_MAX : selection_.back(); - } - changed(r);return r; -} - -xtc_result SequencerWidget::transform_midi(const xtc_midi_transform &edit) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_transform_midi(project_,ids.data(),ids.size(),&edit); - changed(r);return r; -} - -void SequencerWidget::fades_dialog() -{ - const char *input=ui_input("Symmetric fade length in ticks (0 removes; at most half each clip)","10");if(!input)return; - int64_t ticks=0;char extra;std::istringstream values(input); - if(!(values>>ticks) || (values>>extra) || ticks<0 || ticks>static_cast(XTC_PROJECT_MAX_TICK)){ui_alert("Enter a valid nonnegative tick length.");return;} - transform_selection(XTC_CLIPS_FADE,ticks); -} - -xtc_result SequencerWidget::make_chord(const std::vector &intervals) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_chordify(project_,ids.data(),ids.size(),intervals.data(),intervals.size()); - changed(r);return r; -} - -xtc_result SequencerWidget::arpeggiate(uint64_t step,unsigned int gate,bool descending) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_arpeggiate(project_,ids.data(),ids.size(),step,gate,descending); - changed(r);return r; -} - -xtc_result SequencerWidget::normalize_audio(float target) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_normalize_audio(project_,ids.data(),ids.size(),target); - changed(r);return r; -} - -void SequencerWidget::normalize_dialog() -{ - const char *input=ui_input("Source sample peak target in dBFS (-60..0; excludes fades and mixer)","-1");if(!input)return; - float db=0;char extra;std::istringstream values(input); - if(!(values>>db) || (values>>extra) || !std::isfinite(db) || db < -60 || db>0){ui_alert("Enter a finite sample peak target from -60 to 0 dBFS.");return;} - normalize_audio(std::pow(10.0f,db/20.0f)); -} - -void SequencerWidget::arpeggio_dialog() -{ - const char *input=ui_input("Step ticks (1..3840), gate percent (1..100), direction (0=up, 1=down)","240 80 0");if(!input)return; - uint64_t step=0;unsigned int gate=0;int down=0;char extra;std::istringstream values(input); - if(!(values>>step>>gate>>down) || (values>>extra) || !step || step>3840 || !gate || gate>100 || down<0 || down>1){ui_alert("Enter a valid step, gate and direction.");return;} - arpeggiate(step,gate,down!=0); -} - -void SequencerWidget::chord_dialog() -{ - const char *input=ui_input("Additional semitone intervals: 1..8 unique nonzero values (-127..127)","4 7");if(!input)return; - std::istringstream values(input);std::vector intervals;int interval; - while(values>>interval){intervals.push_back(interval);if(intervals.size()>8)break;} - if(!values.eof() || intervals.empty() || intervals.size()>8){ui_alert("Enter 1 to 8 semitone intervals.");return;} - make_chord(intervals); -} - -void SequencerWidget::humanize_dialog() -{ - const char *input=ui_input("Timing variation ticks (0..960), velocity variation (0..127), seed","12 8 1");if(!input)return; - unsigned int timing=0,velocity=0,seed=0;char extra;std::istringstream values(input); - if(!(values>>timing>>velocity>>seed) || (values>>extra) || timing>960 || velocity>127){ui_alert("Enter bounded timing/velocity variation and a 32-bit seed.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_HUMANIZE;edit.timing_variation=timing;edit.velocity_variation=velocity;edit.seed=seed;transform_midi(edit); -} - -void SequencerWidget::velocity_dialog() -{ - const char *input=ui_input("Velocity percentage (0..400), then offset (-127..127)","100 0");if(!input)return; - unsigned int percent=0;int offset=0;char extra;std::istringstream values(input); - if(!(values>>percent>>offset) || (values>>extra) || percent>400 || offset < -127 || offset>127){ui_alert("Enter a valid percentage and velocity offset.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=percent;edit.velocity_offset=offset;transform_midi(edit); -} - -void SequencerWidget::scale_dialog() -{ - const char *input=ui_input("Root (0=C..11=B), scale (0=chromatic, 1=major, 2=natural minor)","0 1");if(!input)return; - unsigned int root=0,scale=0;char extra;std::istringstream values(input); - if(!(values>>root>>scale) || (values>>extra) || root>11 || scale>2){ui_alert("Enter a root from 0 to 11 and scale from 0 to 2.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_SCALE;edit.root=root;edit.scale=scale;transform_midi(edit); -} - -void SequencerWidget::gate_dialog() -{ - const char *input=ui_input("Note length percentage (1..400)","50");if(!input)return; - unsigned int percent=0;char extra;std::istringstream values(input); - if(!(values>>percent) || (values>>extra) || percent<1 || percent>400){ui_alert("Enter a whole percentage from 1 to 400.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_GATE;edit.gate_percent=percent;transform_midi(edit); -} - -void SequencerWidget::swing_dialog() -{ - const char *input=ui_input("Grid ticks (1..3840), swing (0..100), strength (0..100)","240 50 100");if(!input)return; - int grid=0,swing=-1,strength=-1;char extra; - std::istringstream values(input); - if(!(values>>grid>>swing>>strength) || (values>>extra) || grid<1 || grid>3840 || swing<0 || swing>100 || strength<0 || strength>100){ui_alert("Enter a valid grid, swing and strength.");return;} - xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_SWING;edit.grid=static_cast(grid);edit.swing=static_cast(swing);edit.strength=static_cast(strength);transform_midi(edit); -} - -xtc_result SequencerWidget::repeat_selection(unsigned int copies) -{ - auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; - if(r==XTC_OK)r=xtc_project_repeat_clips(project_,ids.data(),ids.size(),copies); - changed(r);return r; -} - -void SequencerWidget::repeat_dialog() -{ - const char *input=ui_input("Additional phrase copies (1..1024)","3");if(!input)return; - char *end=nullptr;unsigned long copies=std::strtoul(input,&end,10); - if(end==input || *end || copies<1 || copies>1024){ui_alert("Enter a whole number from 1 to 1024.");return;} - repeat_selection(static_cast(copies)); -} - -xtc_result SequencerWidget::copy_selection() -{ - auto ids=selected_clips();xtc_clipboard *copy=nullptr; - xtc_result r=xtc_clipboard_create(project_,ids.data(),ids.size(),©); - if(r==XTC_OK)clipboard_.reset(copy); - else ui_alert("Copy unchanged: %s",xtc_result_string(r)); - return r; -} - -xtc_result SequencerWidget::paste_selection() -{ - const uint64_t start=cursor_,track=track_; - std::vector output(xtc_clipboard_count(clipboard_.get())); - xtc_result r=output.empty() ? XTC_ERR_INVALID_STATE : XTC_OK; - if(r==XTC_OK && before_edit)r=before_edit(context_); - if(r==XTC_OK)r=xtc_clipboard_paste(clipboard_.get(),project_,track,start,output.data()); - if(r==XTC_OK){selection_=output;clip_=selection_.back();xtc_clip_info c{};xtc_project_find_clip(project_,clip_,&c);track_=c.track_id;} - changed(r);return r; -} - -xtc_result SequencerWidget::nudge_selection(int direction) -{ - return transform_selection(XTC_CLIPS_MOVE,direction<0 ? -static_cast(grid_) : static_cast(grid_)); -} - -xtc_result SequencerWidget::duplicate_selection() -{ - auto ids=selected_clips();uint64_t begin=XTC_PROJECT_MAX_TICK,end=0; - for(auto id:ids){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);begin=std::min(begin,c.start_tick);end=std::max(end,c.start_tick+c.length_tick);} - return transform_selection(XTC_CLIPS_DUPLICATE,static_cast(end)-static_cast(begin)); -} - -xtc_result SequencerWidget::rename_selection(const char *name) -{ - xtc_result r = before_edit ? before_edit(context_) : XTC_OK; - if (r == XTC_OK) r = clip_ != UINT64_MAX ? xtc_project_rename_clip(project_,clip_,name) : xtc_project_rename_track(project_,track_,name); - changed(r); return r; -} - -xtc_result SequencerWidget::move_track(int direction) -{ - size_t tracks, clips, i; xtc_project_get_counts(project_,&tracks,&clips); - xtc_track_info track{}; - for (i = 0; i < tracks; ++i) { xtc_project_get_track(project_,i,&track); if (track.id == track_) break; } - if (i == tracks || (direction < 0 ? i == 0 : i+1 == tracks)) return XTC_ERR_NOT_FOUND; - xtc_result r = before_edit ? before_edit(context_) : XTC_OK; - if (r == XTC_OK) r = xtc_project_move_track(project_,track_,direction < 0 ? i-1 : i+1); - changed(r); return r; -} - -xtc_result SequencerWidget::split_selection() -{ - uint64_t right; - const uint64_t split_tick = cursor_; // Stop/status refresh may move the playhead. - xtc_result result = before_edit ? before_edit(context_) : XTC_OK; - if (result == XTC_OK) { - xtc_clip_info c{};result=xtc_project_find_clip(project_,clip_,&c); - if(result==XTC_OK)result=c.midi ? xtc_project_split_midi(project_,clip_,split_tick,&right) : xtc_project_split_audio(project_,clip_,split_tick,&right); - } - if (result == XTC_OK) {clip_ = right;selection_.clear();} - changed(result); - return result; -} - -xtc_result SequencerWidget::audio_properties(float gain, uint64_t in, uint64_t out) -{ - xtc_result result = before_edit ? before_edit(context_) : XTC_OK; - if (result == XTC_OK) result = xtc_project_set_clip_audio(project_, clip_, gain, in, out); - changed(result); - return result; -} - -void SequencerWidget::audio_properties_dialog() -{ - xtc_clip_info clip{}; - if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || clip.midi) { - ui_alert("Select an audio clip or silent region first."); - return; - } - char defaults[128]; - std::snprintf(defaults, sizeof(defaults), "%.9g %llu %llu", static_cast(clip.gain), - static_cast(clip.fade_in_tick), static_cast(clip.fade_out_tick)); - const char *text = ui_input("Linear gain (0..4), fade-in ticks, fade-out ticks (0 disables)", defaults); - if (!text) return; - std::istringstream input(text); - float gain; uint64_t in, out; - if (!(input >> gain >> in >> out) || !(input >> std::ws).eof()) { - ui_alert("Enter gain and two fade durations in ticks."); return; - } - audio_properties(gain, in, out); -} - -xtc_result SequencerWidget::crossfade_selection() -{ - auto ids=selected_clips();xtc_result r=ids.size()==2 ? XTC_OK : XTC_ERR_INVALID_ARGUMENT; - if(r==XTC_OK && before_edit)r=before_edit(context_); - if(r==XTC_OK)r=xtc_project_crossfade(project_,ids[0],ids[1]); - changed(r);return r; -} - -xtc_result SequencerWidget::strum_chords(uint64_t spacing,bool descending) -{ - auto ids=selected_clips();xtc_result r=XTC_OK; - if(before_edit)r=before_edit(context_); - if(r==XTC_OK)r=xtc_project_strum(project_,ids.data(),ids.size(),spacing,descending ? 1 : 0); - changed(r);return r; -} - -void SequencerWidget::strum_dialog() -{ - const char *input=ui_input("Spacing ticks (1..960), direction (0=up, 1=down)","30 0");if(!input)return; - uint64_t spacing=0;int down=0;char extra;std::istringstream values(input); - if(!(values>>spacing>>down) || (values>>extra) || !spacing || spacing>960 || down<0 || down>1){ui_alert("Enter a valid spacing and direction.");return;} - strum_chords(spacing,down!=0); -} diff --git a/src/gui/JUCE/sequencer_group_scope.hpp b/src/gui/JUCE/sequencer_group_scope.hpp deleted file mode 100644 index dabcb02..0000000 --- a/src/gui/JUCE/sequencer_group_scope.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef XTC_SEQUENCER_GROUP_SCOPE_HPP -#define XTC_SEQUENCER_GROUP_SCOPE_HPP -#include "ui.hpp" -class SequencerGroupScope { - UiGroup *previous_; - public: - explicit SequencerGroupScope(UiGroup *group) : previous_(UiGroup::current()) { UiGroup::current(group); } - ~SequencerGroupScope() { UiGroup::current(previous_); } - SequencerGroupScope(const SequencerGroupScope &) = delete; - SequencerGroupScope &operator=(const SequencerGroupScope &) = delete; -}; -#endif diff --git a/src/gui/JUCE/sequencer_instruments.cpp b/src/gui/JUCE/sequencer_instruments.cpp deleted file mode 100644 index bd5c330..0000000 --- a/src/gui/JUCE/sequencer_instruments.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "sequencer_widget.hpp" -#include "sequencer_track_widget.hpp" -#include -#include - -UiChoice *SequencerWidget::instrument_choice(uint64_t track) const -{ - auto *view=track_widget(track); - return view && !view->is_bus() ? view->instrument_choice() : nullptr; -} - -void SequencerWidget::refresh_instruments() try -{ - sync_views(); - size_t tracks = 0, clips = 0; - xtc_project_get_counts(project_,&tracks,&clips); - catalog_.clear(); - for (unsigned int i=0;i<2;++i) { xtc_plugin_spec s{}; if (xtc_plugin_probe(XTC_BUILTIN_PATH,i,48000,&s)==XTC_OK) catalog_.push_back(s); } - for (size_t i = 0; i < tracks; ++i) { - xtc_track_info t{}; xtc_plugin_spec spec{}; - xtc_project_get_track(project_,i,&t); - if (xtc_project_get_instrument(project_,t.id,&spec) != XTC_OK) continue; - bool present = false; - for (const auto &item : catalog_) if (!std::strcmp(item.path,spec.path) && item.index == spec.index) present = true; - if (!present) catalog_.push_back(spec); - } - for(auto *view:track_widgets_) { - auto *choice=view->instrument_choice(); - xtc_track_info t{}; - for(size_t i=0;itrack_id()) break;} - if(t.is_bus) {choice->hide(); continue;} - choice->clear(); choice->add("Built-in sine"); - xtc_plugin_spec current{}; int selected = 0; - bool assigned = xtc_project_get_instrument(project_,t.id,¤t) == XTC_OK; - for (size_t j = 0; j < catalog_.size(); ++j) { - std::string name = std::string(xtc_plugin_format_name(catalog_[j].format))+": "+catalog_[j].label; - int item = choice->add("Instrument"); choice->replace(item,name.c_str()); - if (assigned && !std::strcmp(current.path,catalog_[j].path) && current.index == catalog_[j].index) selected = static_cast(j)+1; - } - choice->add("Load plugin..."); choice->value(selected); choice->show(); - } -} catch (const std::bad_alloc &) { - catalog_.clear(); - for(auto *view:track_widgets_) view->instrument_choice()->hide(); -} - -void SequencerWidget::instrument_callback(UiWidget *widget, void *context) -{ - auto *owner = static_cast(context); - auto *choice = static_cast(widget); - for (auto *view : owner->track_widgets_) if (view->instrument_choice() == choice) { - uint64_t track = view->track_id(); - int index = choice->value(); - bool browse = index == static_cast(owner->catalog_.size())+1; - xtc_plugin_spec spec{}; - if (index > 0 && !browse) { - if (static_cast(index) > owner->catalog_.size()) return; - spec = owner->catalog_[static_cast(index)-1]; - } - if (owner->instrument_select) owner->instrument_select(owner->context_,track,index > 0 && !browse ? &spec : nullptr,browse); - owner->refresh_instruments(); owner->redraw(); return; - } -} diff --git a/src/gui/JUCE/sequencer_ruler_widget.cpp b/src/gui/JUCE/sequencer_ruler_widget.cpp deleted file mode 100644 index 2370367..0000000 --- a/src/gui/JUCE/sequencer_ruler_widget.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "sequencer_ruler_widget.hpp" -#include "sequencer_widget.hpp" -#include "theme.hpp" -#include "ui.hpp" -#include -#include - -SequencerRulerWidget::SequencerRulerWidget(SequencerWidget &owner) - : UiWidget(owner.x(),owner.y(),owner.w(),28), owner_(owner) {} -void SequencerRulerWidget::draw() -{ - int cx,cy,cw,ch; ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - ui_push_clip(x(),y(),w(),h()); - ui_font(UI_HELVETICA,11); - int first=std::max(0,(cx-owner_.x()-owner_.header_width_)/(owner_.scale_*4)); - int last=(cx+cw-owner_.x()-owner_.header_width_)/(owner_.scale_*4)+1; - for(int bar=first;bar<=last;++bar) { - char label[32]; std::snprintf(label,sizeof(label),"%d",bar+1); - ui_color(xtc_ui::muted()); ui_draw(label,owner_.x()+owner_.header_width_+bar*owner_.scale_*4+6,y()+19); - } - uint64_t begin=owner_.mode_==4 ? std::min(owner_.loop_anchor_,owner_.loop_preview_) : owner_.loop_begin_; - uint64_t end=owner_.mode_==4 ? std::max(owner_.loop_anchor_,owner_.loop_preview_) : owner_.loop_end_; - int left=owner_.x()+owner_.header_width_+static_cast(begin*static_cast(owner_.scale_)/960U); - int right=owner_.x()+owner_.header_width_+static_cast(end*static_cast(owner_.scale_)/960U); - ui_color(owner_.loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted()); ui_rectf(left,y()+23,std::max(0,right-left),4); - size_t markers=0; xtc_project_marker_count(owner_.project_,&markers); - for(size_t i=0;i(marker.tick*static_cast(owner_.scale_)/960U); - if(pxcx+cw) continue; - ui_push_clip(px,y(),160,12); ui_font(UI_HELVETICA,10); ui_color(xtc_ui::accent()); ui_draw(marker.name,px+3,y()+10); ui_pop_clip(); - } - int playhead=owner_.x()+owner_.header_width_+static_cast(owner_.cursor_*static_cast(owner_.scale_)/960U); - ui_color(xtc_ui::accent()); ui_polygon(playhead-4,y(),playhead+4,y(),playhead,y()+6); - int pinned=owner_.header_left(); - ui_color(xtc_ui::background()); ui_rectf(pinned,y(),owner_.header_width_,28); - ui_color(xtc_ui::muted()); ui_font(UI_HELVETICA,10); ui_draw("TRACKS / drag edge to resize",pinned+10,y()+19); - ui_pop_clip(); -} -int SequencerRulerWidget::handle(int event) -{ - if (event==UI_DRAG && owner_.mode_==4) {owner_.loop_preview_=owner_.tick(Ui::event_x());owner_.redraw();return 1;} - if (event==UI_RELEASE && owner_.mode_==4) { - owner_.loop_preview_=owner_.tick(Ui::event_x()); owner_.mode_=0; - uint64_t begin=std::min(owner_.loop_anchor_,owner_.loop_preview_),end=std::max(owner_.loop_anchor_,owner_.loop_preview_); - if(end>begin && owner_.loop_select) { - xtc_result r=owner_.loop_select(owner_.context_,begin,end); - if(r!=XTC_OK) ui_alert("Loop unchanged: %s",xtc_result_string(r)); - } - owner_.redraw();return 1; - } - - if(event==UI_PUSH && (Ui::event_button()==UI_LEFT_MOUSE || Ui::event_button()==UI_RIGHT_MOUSE)) { - owner_.take_focus(); owner_.mode_=0; - if(Ui::event_x() -#include - -SequencerTrackWidget::SequencerTrackWidget(SequencerWidget &owner, const xtc_track_info &track) - : UiGroup(owner.x(),owner.y()+28,owner.w(),56), owner_(owner), info_(track) -{ - resizable(nullptr); - clips_.reserve(XTC_PROJECT_MAX_CLIPS); - velocity_=new SequencerVelocityWidget(owner,track.id); - header_=new SequencerTrackHeader(*this); - end(); -} - -UiChoice *SequencerTrackWidget::instrument_choice() const { return header_->instrument_choice(); } -UiGroup *SequencerTrackWidget::header() const { return header_; } -SequencerClipWidget *SequencerTrackWidget::clip_widget(uint64_t clip) const -{ - for(auto *view:clips_) if(view->clip_id()==clip) return view; - return nullptr; -} - -void SequencerTrackWidget::sync(size_t row, const xtc_track_info &track) -{ - row_=row; info_=track; - resize(owner_.x(),owner_.track_top(row),owner_.w(),owner_.track_height(row)); - header_->resize(owner_.header_left(),y(),owner_.header_width_,h()); - instrument_choice()->resize(header_->x()+10,y()+30,header_->w()-22,20); - if(info_.is_bus) instrument_choice()->hide(); - velocity_->resize(owner_.x()+owner_.header_width_,y()+56,owner_.w()-owner_.header_width_,88); - if(owner_.track_expanded(info_.id)) velocity_->show(); else velocity_->hide(); - for(auto it=clips_.begin();it!=clips_.end();) { - xtc_clip_info c{}; - if(xtc_project_find_clip(owner_.project_,(*it)->clip_id(),&c)!=XTC_OK || c.track_id!=info_.id) { - auto *removed=*it; it=clips_.erase(it); - removed->hide(); remove(removed); Ui::delete_widget(removed); - } else ++it; - } - size_t tracks=0,clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips); - SequencerGroupScope scope(this); - for(size_t i=0;isync(c,y()); - // Match dense model order for overlapping clip drawing and hit testing. - insert(*view,header_); - } -} - -void SequencerTrackWidget::draw() -{ - ui_push_clip(x(),y(),w(),h()); - draw_children(); - ui_pop_clip(); -} - -int SequencerTrackWidget::handle(int event) -{ - if(event==UI_PUSH) { - if(Ui::event_inside(header_)) return header_->handle(event); - if(velocity_->visible() && Ui::event_inside(velocity_)) return velocity_->handle(event); - // Preserve the full-row clip hit area, including its small vertical margins. - for(int i=children()-1;i>=0;--i) { - auto *view=dynamic_cast(child(i)); - if(view && Ui::event_x()>=view->x() && Ui::event_x()x()+view->w()) return view->handle(event); - } - owner_.take_focus(); owner_.track_=info_.id; owner_.clip_=UINT64_MAX; - owner_.selection_.clear(); owner_.mode_=0; - if(Ui::event_button()==UI_RIGHT_MOUSE) owner_.context_menu(); - owner_.redraw(); return 1; - } - if(event==UI_DRAG || event==UI_RELEASE) return owner_.handle(event); - return UiGroup::handle(event); -} - -SequencerTrackHeader::SequencerTrackHeader(SequencerTrackWidget &track) - : UiGroup(track.x(),track.y(),180,56), track_(track) -{ - resizable(nullptr); - instrument_=new UiChoice(x()+10,y()+30,w()-22,20); - xtc_ui::style(instrument_); instrument_->textsize(10); instrument_->textcolor(xtc_ui::text()); - instrument_->tooltip("Track instrument: built-in sine, an instrument used in this project, or Load plugin..."); - instrument_->callback(SequencerWidget::instrument_callback,&track.owner_); - end(); -} - -void SequencerTrackHeader::draw() -{ - auto &owner=track_.owner_; const auto &t=track_.info_; - ui_push_clip(x(),y(),w(),h()); - ui_color(t.id==owner.track_ ? ui_rgb_color(49,62,70) : xtc_ui::panel()); - ui_rectf(x(),y(),w()-2,h()-1); - ui_color(xtc_ui::track(t.id)); ui_rectf(x(),y(),3,h()-1); - ui_push_clip(x()+9,y(),w()-18,55); - ui_color(xtc_ui::text()); ui_font(UI_HELVETICA_BOLD,12); - if(owner.track_expanded(t.id)) ui_polygon(x()+10,y()+10,x()+20,y()+10,x()+15,y()+16); - else ui_polygon(x()+12,y()+8,x()+12,y()+18,x()+18,y()+13); - ui_draw(t.name,x()+26,y()+18); - if(t.is_bus) { - char detail[64]; std::snprintf(detail,sizeof(detail),"%02zu BUS%s%s",track_.row_+1,t.mute ? " MUTE" : "",t.solo ? " SOLO" : ""); - ui_font(UI_HELVETICA,10); ui_color(xtc_ui::muted()); ui_draw(detail,x()+12,y()+42); - } - if(t.mute || t.solo) { - ui_font(UI_HELVETICA,10); ui_color(xtc_ui::muted()); ui_draw(t.mute ? "M" : "S",x()+w()-16,y()+18); - } - ui_pop_clip(); - if(owner.track_expanded(t.id)) { - ui_color(xtc_ui::muted()); ui_font(UI_HELVETICA,11); - bool audio=track_.velocity_->audio(); - ui_draw(audio ? "AUDIO VELOCITY" : "VELOCITY 1..127",x()+12,y()+76); - ui_font(UI_HELVETICA,10); - ui_draw(audio ? "Level envelope 0..400%" : "Drag bars to edit notes",x()+12,y()+94); - char value[32]; if(track_.velocity_->preview_label(value,sizeof(value))) ui_draw(value,x()+12,y()+114); - } - draw_children(); - ui_pop_clip(); -} - -int SequencerTrackHeader::handle(int event) -{ - auto &owner=track_.owner_; - if(event==UI_PUSH) { - if(instrument_->visible() && Ui::event_inside(instrument_)) return UiGroup::handle(event); - owner.take_focus(); - if(Ui::event_button()==UI_LEFT_MOUSE && Ui::event_x()>=x()+7 && Ui::event_x()=y()+56) return 1; - owner.track_=track_.info_.id; owner.clip_=UINT64_MAX; owner.selection_.clear(); owner.mode_=0; - if(Ui::event_button()==UI_RIGHT_MOUSE) owner.context_menu(); - owner.redraw(); return 1; - } - return UiGroup::handle(event); -} diff --git a/src/gui/JUCE/sequencer_track_widget.hpp b/src/gui/JUCE/sequencer_track_widget.hpp deleted file mode 100644 index 45900a8..0000000 --- a/src/gui/JUCE/sequencer_track_widget.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef XTC_SEQUENCER_TRACK_WIDGET_HPP -#define XTC_SEQUENCER_TRACK_WIDGET_HPP -#include "ui.hpp" -#include "xtc/xtc_project.h" -#include -class SequencerWidget; -class SequencerClipWidget; -class SequencerVelocityWidget; -class SequencerTrackHeader; -class SequencerTrackWidget : public UiGroup { - SequencerWidget &owner_; - xtc_track_info info_{}; - size_t row_ = 0; - SequencerTrackHeader *header_; - SequencerVelocityWidget *velocity_; - std::vector clips_; // Borrowed; JUCE owns children. - friend class SequencerTrackHeader; - public: - SequencerTrackWidget(SequencerWidget &owner, const xtc_track_info &track); - uint64_t track_id() const { return info_.id; } - bool is_bus() const { return info_.is_bus != 0; } - void sync(size_t row, const xtc_track_info &track); - UiChoice *instrument_choice() const; - SequencerClipWidget *clip_widget(uint64_t clip) const; - SequencerVelocityWidget *velocity_lane() const { return velocity_; } - UiGroup *header() const; - int handle(int event) override; - protected: - void draw() override; -}; -class SequencerTrackHeader : public UiGroup { - SequencerTrackWidget &track_; - UiChoice *instrument_; - public: - explicit SequencerTrackHeader(SequencerTrackWidget &track); - UiChoice *instrument_choice() const { return instrument_; } - int handle(int event) override; - protected: - void draw() override; -}; -#endif diff --git a/src/gui/JUCE/sequencer_velocity_widget.cpp b/src/gui/JUCE/sequencer_velocity_widget.cpp deleted file mode 100644 index 0b519a5..0000000 --- a/src/gui/JUCE/sequencer_velocity_widget.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include "sequencer_velocity_widget.hpp" -#include "sequencer_widget.hpp" -#include "theme.hpp" -#include "ui.hpp" -#include -#include -#include -#include - -SequencerVelocityWidget::SequencerVelocityWidget(SequencerWidget &owner, uint64_t track) - : UiWidget(0,0,1,88), owner_(owner), track_(track) {} -bool SequencerVelocityWidget::preview_label(char *text, size_t size) const -{ - if(owner_.active_velocity_!=this) return false; - if(owner_.mode_==7) std::snprintf(text,size,"Level: %.0f%%",audio_preview_.value*100); - else if(owner_.mode_==6) std::snprintf(text,size,"Velocity: %u",preview_); - else return false; - return true; -} -bool SequencerVelocityWidget::note_position(const xtc_note_info ¬e, int &px) const -{ - xtc_clip_info c{}; - if(xtc_project_find_clip(owner_.project_,note.clip_id,&c)!=XTC_OK || c.track_id!=track_ || !c.midi || - note.start_tick=c.length_tick) return false; - px=owner_.x()+owner_.header_width_+static_cast((c.start_tick+note.start_tick-c.offset_tick)*static_cast(owner_.scale_)/960U); - return true; -} - -bool SequencerVelocityWidget::audio() const -{ - size_t tracks=0, clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips); - bool midi=false; - for(size_t i=0;i(y()+h()-8-py)*4.0f/(h()-28),0.0f,4.0f); -} - -void SequencerVelocityWidget::draw_audio(const xtc_track_info &track, int left, int right) -{ - std::array points{}; - size_t count=0,total=0; xtc_project_automation_count(owner_.project_,&total); - for(size_t i=0;i(t*static_cast(owner_.scale_)/960U);}; - auto py=[&](float v){return y()+h()-8-static_cast(v*(h()-28)/4.0f);}; - ui_color(xtc_ui::track(track.id)); - if(!count) ui_line(left,py(track.gain),right,py(track.gain)); - else { - ui_line(left,py(points[0].value),px(points[0].tick),py(points[0].value)); - for(size_t i=1;iright+4) continue; - ui_rectf(point_x-4,py(points[i].value)-3,8,6); - } - } -} - -unsigned int SequencerVelocityWidget::note_value(int py) const -{ - return static_cast(std::clamp((y()+h()-8-py)*127/(h()-28),1,127)); -} - -void SequencerVelocityWidget::draw() -{ - int cx,cy,cw,ch; ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - int left=std::max(cx,owner_.header_left()+owner_.header_width_); - ui_push_clip(left,y(),std::max(0,cx+cw-left),h()); - ui_color(xtc_ui::border()); ui_line(x(),y(),x()+w(),y()); - ui_font(UI_HELVETICA,10); ui_color(xtc_ui::muted()); - bool is_audio=audio(); - ui_draw(is_audio ? "Click anywhere to add a level point; drag vertically; right-click a point to remove" : "Drag a note bar to change velocity",left+8,y()+14); - xtc_track_info track{}; - size_t tracks=0,clips=0; xtc_project_get_counts(owner_.project_,&tracks,&clips); - for(size_t i=0;i(value)*(h()-28)/127; - ui_color(editing ? xtc_ui::text() : xtc_ui::track(track_)); - ui_rectf(px-2,ny,4,bottom-ny); ui_rectf(px-4,ny-2,8,4); - } - ui_pop_clip(); -} - -int SequencerVelocityWidget::handle(int event) -{ - if(owner_.mode_==7) { - if(event==UI_KEYDOWN && Ui::event_key()==UI_Escape) { owner_.mode_=0; owner_.redraw(); return 1; } - if(event==UI_DRAG) { audio_preview_.value=audio_value(Ui::event_y()); owner_.redraw(); return 1; } - if(event==UI_RELEASE) { - owner_.mode_=0; - auto point=audio_preview_; - point.value=audio_value(Ui::event_y()); - if(audio_existing_) { - size_t count=0; xtc_project_automation_count(owner_.project_,&count); - for(size_t i=0;i(std::max(0,Ui::event_x()-owner_.x()-owner_.header_width_))*960/static_cast(owner_.scale_)); - audio_preview_={track_,position,XTC_AUTOMATION_GAIN,audio_value(Ui::event_y()),0}; - audio_existing_=false; - size_t count=0; xtc_project_automation_count(owner_.project_,&count); int distance=12; - for(size_t i=0;i(a.tick*static_cast(owner_.scale_)/960U); - int py=y()+h()-8-static_cast(a.value*(h()-28)/4.0f); - int d=std::abs(Ui::event_x()-px)+std::abs(Ui::event_y()-py); - if(d(note.velocity)*(h()-28)/127; - int d=std::abs(Ui::event_x()-px)+std::abs(Ui::event_y()-ny); - if(d -#include - -void SequencerWidget::refresh_waveforms() try -{ - // Control-thread loading only. Keep cached failures as well as successes; - // removed/relinked paths are evicted. Reopening rebuilds externally changed files. - size_t tracks, clips; - xtc_project_get_counts(project_, &tracks, &clips); - std::vector next; - for (size_t i = 0; i < clips && next.size() < 64; ++i) { - xtc_clip_info c{}; - xtc_project_get_clip(project_, i, &c); - if (!c.asset_path[0]) continue; - bool present = false; - for (const auto &wave : next) if (wave.path == c.asset_path) { present = true; break; } - if (present) continue; - Wave entry; - entry.path = c.asset_path; - for (auto &wave : waves_) if (wave.path == entry.path) { - entry.data = std::move(wave.data); present = true; break; - } - if (!present) { - xtc_waveform *summary = nullptr; - (void)xtc_waveform_create(c.asset_path, &summary); - entry.data.reset(summary); - } - next.push_back(std::move(entry)); - } - waves_ = std::move(next); -} catch (const std::bad_alloc &) { - waves_.clear(); // Preview failure must not interrupt the model/history edit. -} - -void SequencerWidget::draw_waveform(const xtc_clip_info &c, int left, int top) -{ - const xtc_waveform *summary = nullptr; - for (const auto &wave : waves_) if (wave.path == c.asset_path) { summary = wave.data.get(); break; } - if (!summary) return; - unsigned int rate; size_t frames, bins; - if (xtc_waveform_info(summary, &rate, &frames, &bins) != XTC_OK) return; - double ticks_per_frame = static_cast(xtc_project_tempo(project_)) * 960 / (rate * 60000.0); - ui_color(ui_rgb_color(30, 65, 80)); - for (size_t bin = 0; bin < bins; ++bin) { - double start = static_cast(bin * frames / bins) * ticks_per_frame; - double end = static_cast((bin+1) * frames / bins) * ticks_per_frame; - double offset = static_cast(c.offset_tick); - if (end <= offset || start >= offset + static_cast(c.length_tick)) continue; - int px = left + static_cast((std::max(start, offset) - offset) * scale_ / 960); - int width = std::max(1, static_cast((end - std::max(start, offset)) * scale_ / 960)); - float low, high; - if (xtc_waveform_peak(summary, bin, &low, &high) != XTC_OK) continue; - int y1 = top + 28 - static_cast(std::clamp(high, -1.0f, 1.0f) * 19); - int y2 = top + 28 - static_cast(std::clamp(low, -1.0f, 1.0f) * 19); - ui_rectf(px, y1, width, std::max(1, y2-y1+1)); - } -} diff --git a/src/gui/JUCE/sequencer_widget.cpp b/src/gui/JUCE/sequencer_widget.cpp deleted file mode 100644 index d3ae57e..0000000 --- a/src/gui/JUCE/sequencer_widget.cpp +++ /dev/null @@ -1,382 +0,0 @@ -#include "sequencer_widget.hpp" -#include "sequencer_track_widget.hpp" -#include "sequencer_velocity_widget.hpp" -#include "sequencer_ruler_widget.hpp" -#include "sequencer_group_scope.hpp" -#include "theme.hpp" -#include "snap.hpp" -#include "ui.hpp" -#include -#include - -SequencerWidget::SequencerWidget(int left, int top, xtc_project *project, Seek seek, void *context) - : UiGroup(left, top, 6300, 3700), project_(project), seek_(seek), context_(context) -{ - expanded_tracks_.fill(UINT64_MAX); - resizable(nullptr); - track_widgets_.reserve(XTC_PROJECT_MAX_TRACKS); - ruler_=new SequencerRulerWidget(*this); - extent(); - end(); -} - -bool SequencerWidget::select(uint64_t track, uint64_t clip) -{ - size_t tracks = 0, clips = 0; - xtc_project_get_counts(project_,&tracks,&clips); - bool found = false; - for (size_t i = 0; i < tracks; ++i) { - xtc_track_info info{}; - if (xtc_project_get_track(project_,i,&info) == XTC_OK && info.id == track) found = true; - } - if (!found) return false; - if (clip != UINT64_MAX) { - xtc_clip_info info{}; - if (xtc_project_find_clip(project_,clip,&info) != XTC_OK || info.track_id != track) return false; - } - selection_.clear(); - track_ = track; clip_ = clip; mode_ = 0; redraw(); - return true; -} - -void SequencerWidget::set_project(xtc_project *project, bool reset, bool preserve_expansion) -{ - if(project_ != project) { mode_=0; active_velocity_=nullptr; } - project_ = project; - if (reset) { - if(!preserve_expansion) expanded_tracks_.fill(UINT64_MAX); - waves_.clear(); - selection_.clear(); - track_ = clip_ = UINT64_MAX; - cursor_ = 0; - mode_ = 0; - } - refresh_waveforms(); - extent(); - redraw(); -} - -int SequencerWidget::header_left() const -{ - auto *scroll=dynamic_cast(parent());return scroll ? std::max(x(),scroll->x()) : x(); -} - -void SequencerWidget::header_width(int width) -{ - width=std::clamp(width,180,360);if(width==header_width_)return; - auto *scroll=dynamic_cast(parent());int sx=scroll ? scroll->xposition() : 0,sy=scroll ? scroll->yposition() : 0; - header_width_=width;extent();if(scroll)scroll->scroll_to(sx,sy);redraw(); -} - -uint64_t SequencerWidget::tick(int px) const -{ - int64_t raw = static_cast(std::max(0,px-x()-header_width_))*960/scale_; - return std::min(XTC_PROJECT_MAX_TICK,static_cast(xtc_ui::snap_position(raw,grid_))); -} - -bool SequencerWidget::track_expanded(uint64_t track) const -{ - return track != UINT64_MAX && std::find(expanded_tracks_.begin(),expanded_tracks_.end(),track) != expanded_tracks_.end(); -} - -bool SequencerWidget::expand_track(uint64_t track, bool expanded) -{ - size_t tracks=0, clips=0; - xtc_project_get_counts(project_,&tracks,&clips); - bool found=false; - for(size_t i=0;i=top && py(i); - top=bottom; - } - return -1; -} - -void SequencerWidget::extent() -{ - size_t tracks, clips; - xtc_project_get_counts(project_, &tracks, &clips); - for(auto &id:expanded_tracks_) { - if(id==UINT64_MAX) continue; - bool found=false; - for(size_t i=0;i(end * static_cast(scale_) / 960U + static_cast(header_width_+20)), - std::max(400, track_top(tracks)-y()+72)); - sync_views(); - refresh_instruments(); - if (parent()) - parent()->redraw(); -} - -void SequencerWidget::changed(xtc_result result) -{ - if (result != XTC_OK) - ui_alert("%s", xtc_result_string(result)); - else { - refresh_waveforms(); - extent(); - do_callback(); - } - redraw(); -} - -void SequencerWidget::zoom(int direction) -{ - scale_ = std::clamp(direction > 0 ? scale_ * 2 : scale_ / 2, 24, 384); - extent(); - redraw(); -} - -xtc_result SequencerWidget::set_zoom(int scale) -{ - if(scale!=24 && scale!=48 && scale!=96 && scale!=192 && scale!=384) return XTC_ERR_INVALID_ARGUMENT; - scale_=scale;extent();redraw();return XTC_OK; -} - -void SequencerWidget::draw() -{ - // Reconcile on project/layout changes and input, never during JUCE painting. - ui_push_clip(x(),y(),w(),h()); - int cx, cy, cw, ch; - ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - ui_color(xtc_ui::background()); ui_rectf(cx,cy,cw,ch); - size_t tracks = 0, clips = 0; - xtc_project_get_counts(project_,&tracks,&clips); - for (size_t i = 0; i < tracks; ++i) { - int py = track_top(i); - if (py+track_height(i) < cy || py > cy+ch) continue; - ui_color(i%2 ? ui_rgb_color(30,34,40) : ui_rgb_color(33,37,43)); - ui_rectf(x()+header_width_,py,w()-header_width_,track_height(i)-1); - } - ui_font(UI_HELVETICA,11); - uint64_t visual = grid_ == 1 ? 960 : grid_; - while (visual*static_cast(scale_)/960 < 6) visual *= 2; - int spacing = static_cast(visual*static_cast(scale_)/960); - int first = std::max(0,(cx-x()-header_width_)/spacing); - int last = (cx+cw-x()-header_width_)/spacing+1; - for (int step = first; step <= last; ++step) { - uint64_t tick = static_cast(step)*visual; - int px = x()+header_width_+static_cast(tick*static_cast(scale_)/960); - ui_color(tick%3840 == 0 ? ui_rgb_color(76,84,96) : tick%960 == 0 ? ui_rgb_color(56,63,73) : ui_rgb_color(42,48,56)); - ui_line(px,y()+28,px,y()+h()); - } - if (!tracks) { - ui_font(UI_HELVETICA_BOLD,18); ui_color(xtc_ui::text()); - ui_draw("Your next track starts here",x()+220,y()+105); - ui_font(UI_HELVETICA,13); ui_color(xtc_ui::muted()); - ui_draw("Create a MIDI track or import audio from the panel on the left.",x()+220,y()+135); - ui_draw("Arrange clips here. Open the piano roll to draw and edit MIDI notes.",x()+220,y()+161); - } - ui_pop_clip(); -} -void SequencerWidget::draw_overlay() -{ - int cx,cy,cw,ch; - ui_clip_box(x(),y(),w(),h(),cx,cy,cw,ch); - ui_push_clip(x(),y(),w(),h()); - size_t markers=0;xtc_project_marker_count(project_,&markers); - for(size_t i=0;i(marker.tick*static_cast(scale_)/960U); - if(pxcx+cw)continue; - ui_color(xtc_ui::accent());ui_line(px,y()+24,px,y()+h()); - } - int playhead = x()+header_width_+static_cast(cursor_*static_cast(scale_)/960U); - if (playhead >= std::max(cx,header_left()+header_width_) && playhead <= cx+cw) { - ui_color(xtc_ui::accent()); ui_line(playhead,y(),playhead,y()+h()); - } - ui_color(xtc_ui::muted()); ui_rectf(header_left()+header_width_-3,cy,3,ch); - ui_pop_clip(); -} - -int SequencerWidget::handle(int event) -{ - if((mode_==6 || mode_==7) && active_velocity_ && - (event==UI_DRAG || event==UI_RELEASE || (event==UI_KEYDOWN && Ui::event_key()==UI_Escape))) - return active_velocity_->handle(event); - if(mode_==4 && (event==UI_DRAG || event==UI_RELEASE)) return ruler_->handle(event); - if(event==UI_PUSH && Ui::event_button()==UI_LEFT_MOUSE && Ui::event_x()>=header_left()+header_width_-5 && Ui::event_x()instrument_choice(); - if(((event==UI_DRAG || event==UI_RELEASE) && Ui::pushed()==choice) || - (event==UI_KEYDOWN && Ui::focus()==choice)) return choice->handle(event); - } - if(event==UI_PUSH) { - sync_views(); - if(Ui::event_y()handle(event); - mode_=0; - UiGroup::handle(event); - return 1; - } - if (event == UI_DRAG) - return mode_ != 0; - if (event == UI_RELEASE && mode_) { - int mode = mode_; - mode_ = 0; - int64_t delta = static_cast(Ui::event_x() - drag_x_) * 960 / scale_; - int64_t start = static_cast(original_.start_tick); - int64_t length = static_cast(original_.length_tick); - int64_t offset = static_cast(original_.offset_tick); - uint64_t target = original_.track_id; - if (mode == 1) { - start = xtc_ui::snap_position(start+delta,grid_); - int row = track_row(Ui::event_y()); - xtc_track_info t{}; - if (row >= 0 && xtc_project_get_track(project_, static_cast(row), &t) == XTC_OK) - target = t.id; - } else if (mode == 2) { - delta = xtc_ui::snap_position(start+delta,grid_)-start; - start += delta; - offset += delta; - length -= delta; - } else - length = xtc_ui::snap_position(start+length+delta,grid_)-start; - if (start < 0 || offset < 0 || length <= 0) - return 1; - if (before_edit) { - xtc_result r = before_edit(context_); - if (r != XTC_OK) { - changed(r); - return 1; - } - } - changed(xtc_project_edit_clip(project_, clip_, target, static_cast(start), - static_cast(length), static_cast(offset))); - return 1; - } - // A child may forward a release after its gesture was cancelled. Do not - // send that release back through UiGroup to the same captured child. - if(event==UI_RELEASE) return 0; - if (event == UI_KEYDOWN && !Ui::event_state(UI_CTRL | UI_ALT)) { - if (Ui::event_key() == 's') { split_selection(); return 1; } - if (Ui::event_key() == 'a') { audio_properties_dialog(); return 1; } - } - if(event==UI_KEYDOWN && !Ui::event_state(UI_CTRL|UI_ALT|UI_SHIFT) && (Ui::event_key()==UI_Left || Ui::event_key()==UI_Right)) {nudge_selection(Ui::event_key()==UI_Left ? -1 : 1);return 1;} - if(event==UI_KEYDOWN && Ui::event_state(UI_CTRL) && Ui::event_key()=='c'){copy_selection();return 1;} - if(event==UI_KEYDOWN && Ui::event_state(UI_CTRL) && Ui::event_key()=='v'){paste_selection();return 1;} - if (event == UI_KEYDOWN && Ui::event_state(UI_CTRL) && Ui::event_key() == 'a') {select_all_clips();return 1;} - if (event == UI_KEYDOWN && Ui::event_state(UI_CTRL) && Ui::event_key() == 'd') { duplicate_selection(); return 1; } - if (event == UI_KEYDOWN && Ui::event_key() == UI_F+2) { - const char *name = ui_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); return 1; - } - if (event == UI_KEYDOWN && Ui::event_state(UI_ALT) && - (Ui::event_key() == UI_Up || Ui::event_key() == UI_Down)) { move_track(Ui::event_key() == UI_Up ? -1 : 1); return 1; } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Escape) { - mode_ = 0; - return 1; - } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Delete) { - delete_selection(); - return 1; - } - if (event == UI_FOCUS || event == UI_UNFOCUS) - return 1; - return UiGroup::handle(event); -} - -void SequencerWidget::context_menu() -{ - UiMenu_Item items[6]{}; - items[0].text = "Duplicate clip (Ctrl+D)"; items[1].text = "Rename (F2)"; items[2].text = "Delete (undoable)"; - items[3].text = "Split clip at cursor (S)"; items[4].text = "Audio gain and fades (A)"; - const UiMenu_Item *picked = items->popup(Ui::event_x(),Ui::event_y()); - if (picked == items) duplicate_selection(); - else if (picked == items+1) { const char *name = ui_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); } - else if (picked == items+2) delete_selection(); - else if (picked == items+3) split_selection(); - else if (picked == items+4) audio_properties_dialog(); -} - -SequencerTrackWidget *SequencerWidget::track_widget(uint64_t track) const -{ - for(auto *view:track_widgets_) if(view->track_id()==track) return view; - return nullptr; -} - -void SequencerWidget::sync_views() try -{ - size_t tracks=0,clips=0; xtc_project_get_counts(project_,&tracks,&clips); - ruler_->resize(x(),y(),w(),28); - for(auto it=track_widgets_.begin();it!=track_widgets_.end();) { - bool found=false; - for(size_t i=0;itrack_id()) { found=true; break; } - } - if(!found) { - auto *removed=*it; it=track_widgets_.erase(it); - if(active_velocity_==removed->velocity_lane()) { active_velocity_=nullptr; mode_=0; } - removed->hide(); remove(removed); Ui::delete_widget(removed); - } else ++it; - } - SequencerGroupScope scope(this); - for(size_t i=0;isync(i,t); view->show(); - } -} catch(const std::bad_alloc &) { - // Rendering allocation failure must not invalidate an accepted C model edit. - active_velocity_=nullptr; mode_=0; - for(auto *view:track_widgets_) view->hide(); -} - -void SequencerWidget::resize(int left,int top,int width,int height) -{ - UiGroup::resize(left,top,width,height); - if(ruler_)sync_views(); // Includes pinned headers after native scrolling/reparenting. -} diff --git a/src/gui/JUCE/sequencer_widget.hpp b/src/gui/JUCE/sequencer_widget.hpp deleted file mode 100644 index ad5cd4a..0000000 --- a/src/gui/JUCE/sequencer_widget.hpp +++ /dev/null @@ -1,260 +0,0 @@ -#ifndef XTC_SEQUENCER_WIDGET_HPP -#define XTC_SEQUENCER_WIDGET_HPP -#include "ui.hpp" -#include "xtc/xtc_plugin.h" -#include "xtc/xtc_editing.h" -#include "xtc/xtc_mixer.h" -#include -#include "xtc/xtc_audio.h" -#include -#include -#include - -class SequencerTrackWidget; -class SequencerTrackHeader; -class SequencerClipWidget; -class SequencerVelocityWidget; -class SequencerRulerWidget; - -class SequencerWidget : public UiGroup { - public: - /** - * @brief Prepares a model edit using the widget's opaque callback context. - * - * @return XTC_OK to allow the edit, or an error to cancel it. - */ - using LoopSelect = xtc_result (*)(void *,uint64_t,uint64_t); - LoopSelect loop_select = nullptr; - uint64_t new_clip_ticks = 3840; - int header_width() const {return header_width_;} - void header_width(int width); - int header_left() const; - int zoom_level() const { return scale_; } - xtc_result set_zoom(int scale); - void loop_region(uint64_t start,uint64_t end,bool enabled) { loop_begin_=start; loop_end_=end; loop_enabled_=enabled; redraw(); } - using OpenClip = void (*)(void *); - using InstrumentSelect = void (*)(void *, uint64_t, const xtc_plugin_spec *, bool); - OpenClip open_clip = nullptr; - InstrumentSelect instrument_select = nullptr; - UiChoice *instrument_choice(uint64_t track) const; - SequencerTrackWidget *track_widget(uint64_t track) const; - void refresh_instruments(); - bool track_expanded(uint64_t track) const; - bool expand_track(uint64_t track, bool expanded); - int track_top(size_t row) const; - bool select(uint64_t track, uint64_t clip = UINT64_MAX); - xtc_result merge_midi(); - xtc_result duplicate_track(const char *name); - void duplicate_track_dialog(); - xtc_result ripple_time(uint64_t length,bool remove); - void ripple_dialog(bool remove); - xtc_result marker(const char *name); /* NULL removes at cursor. */ - void marker_dialog(); - bool toggle_clip(uint64_t clip); - void select_all_clips(); - bool clip_selected(uint64_t clip) const; - std::vector selected_clips() const; - xtc_result transform_selection(xtc_clip_operation operation,int64_t amount); - xtc_result transform_midi(const xtc_midi_transform &edit); - void swing_dialog(); - void gate_dialog(); - void scale_dialog(); - void velocity_dialog(); - void humanize_dialog(); - void strum_dialog(); - xtc_result strum_chords(uint64_t spacing,bool descending); - xtc_result make_chord(const std::vector &intervals); - void chord_dialog(); - xtc_result arpeggiate(uint64_t step,unsigned int gate,bool descending); - void arpeggio_dialog(); - xtc_result normalize_audio(float target); - void normalize_dialog(); - void fades_dialog(); - xtc_result crossfade_selection(); - xtc_result repeat_selection(unsigned int copies); - void repeat_dialog(); - xtc_result copy_selection(); - xtc_result paste_selection(); - xtc_result nudge_selection(int direction); - xtc_result duplicate_selection(); - xtc_result split_selection(); - xtc_result audio_properties(float gain, uint64_t fade_in, uint64_t fade_out); - void audio_properties_dialog(); - void refresh_waveforms(); - size_t waveform_count() const { return waves_.size(); } - - xtc_result rename_selection(const char *name); - xtc_result move_track(int direction); - xtc_result grid(uint64_t ticks); - uint64_t grid() const { return grid_; } - using BeforeEdit = xtc_result (*)(void *); - BeforeEdit before_edit = nullptr; - - /** - * @brief Returns the selected track identifier, or UINT64_MAX when none is selected. - * @return Stable track identifier, or UINT64_MAX if no track is selected. - */ - uint64_t selected_track() const - { - return track_; - } - - /** - * @brief Returns the selected clip identifier, or UINT64_MAX when none is selected. - * @return Stable clip identifier, or UINT64_MAX if no clip is selected. - */ - uint64_t selected_clip() const - { - return clip_; - } - - /** - * @brief Requests a transport seek using an opaque context and musical tick. - * - * @return XTC_OK on success, or a transport error. - */ - using Seek = xtc_result (*)(void *, uint64_t); - /** - * @brief Constructs the timeline widget with a borrowed project and seek callback. - * - * @param x Left coordinate in pixels. - * @param y Top coordinate in pixels. - * @param project Borrowed project; must outlive the widget's use of it. - * @param seek Callback used to request a transport seek. - * @param context Opaque callback context. - */ - SequencerWidget(int x, int y, xtc_project *project, Seek seek, void *context); - /** - * @brief Replaces the borrowed project and refreshes the widget's displayed state. - * - * @param project Borrowed project; must outlive the widget's use of it. - * @param reset Whether to clear selection, cursor, and drag state. - * @param preserve_expansion Keep disclosure state when installing history snapshots. - */ - void set_project(xtc_project *project, bool reset = true, bool preserve_expansion = false); - - /** - * @brief Returns the timeline cursor position in musical ticks. - * @return Cursor position in musical ticks. - */ - uint64_t position() const - { - return cursor_; - } - - /** - * @brief Moves the timeline cursor to the supplied musical tick and requests a redraw. - * - * @param tick Musical position in ticks. - */ - void position(uint64_t tick) - { - cursor_ = tick; - redraw(); - } - - /** - * @brief Creates and selects a track through the arrangement edit hook. - * @return XTC_OK on success, or an error code describing the failure. - */ - xtc_result add_track(); - /** - * @brief Creates and selects a one-bar silent region at the timeline cursor. - * @return XTC_OK on success, or an error code describing the failure. - */ - xtc_result add_region(); - /** - * @brief Creates and selects a one-bar MIDI clip at the timeline cursor. - * @return XTC_OK on success, or an error code describing the failure. - */ - xtc_result add_midi_clip(); - /** - * @brief Removes the selected clip, or the selected track when no clip is selected. - * @return XTC_OK on success, or an error code describing the failure. - */ - xtc_result delete_selection(); - /** - * @brief Adjusts timeline zoom within its supported bounds and updates the scroll extent. - * - * @param direction Zoom direction; positive zooms in, nonpositive zooms out. - */ - void zoom(int direction); - - /** - * @brief Returns the borrowed project currently displayed by the widget. - * @return Borrowed project pointer. - */ - xtc_project *project() const - { - return project_; - } - - /** - * @brief Handles JUCE input events and delegates unhandled events to the base widget. - * - * @param event JUCE event code. - * @return Nonzero if the event was handled, zero otherwise. - */ - int handle(int event) override; - - protected: - /** - * @brief Draws the widget's current model, selection, and interaction feedback. - */ - void resize(int,int,int,int)override; - void draw() override; - void draw_overlay() override; - - private: - static constexpr int velocity_height_ = 88; - std::array expanded_tracks_{}; - int track_height(size_t row) const; - int track_row(int py) const; - friend class SequencerTrackWidget; - friend class SequencerTrackHeader; - friend class SequencerClipWidget; - friend class SequencerVelocityWidget; - friend class SequencerRulerWidget; - std::vector track_widgets_; // JUCE owns children. - SequencerRulerWidget *ruler_ = nullptr; - SequencerVelocityWidget *active_velocity_ = nullptr; // Captured gesture, not ownership. - void sync_views(); - void context_menu(); - std::vector catalog_; - static void instrument_callback(UiWidget *, void *); - struct Wave { - std::string path; - std::unique_ptr data{nullptr, xtc_waveform_destroy}; - }; - std::vector waves_; - std::vector selection_; - std::unique_ptr clipboard_{nullptr,xtc_clipboard_destroy}; - void draw_waveform(const xtc_clip_info &clip, int left, int top); - xtc_project *project_; - Seek seek_; - void *context_; - uint64_t track_ = UINT64_MAX, clip_ = UINT64_MAX, cursor_ = 0, grid_ = 240; - uint64_t loop_begin_=0,loop_end_=3840,loop_anchor_=0,loop_preview_=0; - bool loop_enabled_=false; - int scale_ = 48, drag_x_ = 0, mode_ = 0; - int header_width_=180,header_drag_x_=0,header_drag_original_=180; - xtc_clip_info original_{}; - /** - * @brief Converts a screen x coordinate to a bounded timeline tick snapped to the selected grid. - * - * @param px Screen x coordinate in pixels. - * @return Snapped tick within the supported project range. - */ - uint64_t tick(int px) const; - /** - * @brief Reports an edit result, refreshes the widget, and notifies listeners on success. - * - * @param result Result of the attempted operation. - */ - void changed(xtc_result result); - /** - * @brief Resizes the timeline's scrollable area to fit its tracks and clip ends. - */ - void extent(); -}; -#endif diff --git a/src/gui/JUCE/snap.hpp b/src/gui/JUCE/snap.hpp deleted file mode 100644 index 4743e01..0000000 --- a/src/gui/JUCE/snap.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef XTC_JUCE_GUI_SNAP_HPP -#define XTC_JUCE_GUI_SNAP_HPP -#include -#include -namespace xtc_ui { -constexpr uint64_t snap_ticks[] = {1,120,160,240,320,480,960,1920,3840}; -constexpr const char *snap_labels[] = {"Off","1/32","1/16 triplet","1/16","1/8 triplet","1/8","1/4","1/2","1 bar"}; -constexpr size_t snap_count = sizeof(snap_ticks)/sizeof(snap_ticks[0]); -inline int snap_index(uint64_t ticks) { - for (size_t i = 0; i < snap_count; ++i) if (snap_ticks[i] == ticks) return static_cast(i); - return -1; -} -inline int64_t snap_position(int64_t ticks, uint64_t grid) { - if (ticks <= 0) return 0; - int64_t step = static_cast(grid); - return (ticks+step/2)/step*step; -} -} -#endif diff --git a/src/gui/JUCE/theme.hpp b/src/gui/JUCE/theme.hpp deleted file mode 100644 index 0c61cd3..0000000 --- a/src/gui/JUCE/theme.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef XTC_JUCE_GUI_THEME_HPP -#define XTC_JUCE_GUI_THEME_HPP - -#include -#include "ui.hpp" - -/* Shared flat palette. No gradients, shadows, textures or external assets. */ -namespace xtc_ui { -constexpr UiBoxtype flat = static_cast(UI_FREE_BOXTYPE); -constexpr UiBoxtype pressed = static_cast(UI_FREE_BOXTYPE+1); -inline UiColor background() { return ui_rgb_color(25,28,33); } -inline UiColor panel() { return ui_rgb_color(34,38,44); } -inline UiColor border() { return ui_rgb_color(58,64,73); } -inline UiColor text() { return ui_rgb_color(231,235,240); } -inline UiColor muted() { return ui_rgb_color(163,174,188); } -inline UiColor accent() { return ui_rgb_color(92,211,184); } -inline UiColor track(uint64_t id) { - static const unsigned char colors[][3] = {{74,155,184},{111,143,206},{160,123,191}, - {74,163,139},{191,145,78},{183,113,139}}; - const auto &c = colors[id % 6]; return ui_rgb_color(c[0],c[1],c[2]); -} -inline void flat_box(int x, int y, int w, int h, UiColor color) { - ui_color(color); ui_rectf(x,y,w,h); - ui_color(border()); ui_rect(x,y,w,h); -} -inline void pressed_box(int x, int y, int w, int h, UiColor color) { - flat_box(x,y,w,h,ui_darker(color)); -} -inline void configure_boxes() { - Ui::set_boxtype(flat,flat_box,1,1,2,2); - Ui::set_boxtype(pressed,pressed_box,1,1,2,2); - Ui::set_boxtype(UI_UP_BOX,flat_box,1,1,2,2); - Ui::set_boxtype(UI_DOWN_BOX,pressed_box,1,1,2,2); - Ui::set_boxtype(UI_THIN_UP_BOX,flat_box,1,1,2,2); - Ui::set_boxtype(UI_THIN_DOWN_BOX,pressed_box,1,1,2,2); - Ui::set_boxtype(UI_ENGRAVED_BOX,flat_box,1,1,2,2); - Ui::set_boxtype(UI_BORDER_BOX,flat_box,1,1,2,2); -} -inline void initialize() { - static bool initialized = false; - if (initialized) return; - initialized = true; - Ui::background(34,38,44); Ui::background2(25,28,33); Ui::foreground(231,235,240); - Ui::set_color(UI_SELECTION_COLOR,53,116,111); - configure_boxes(); - Ui::scrollbar_size(12); -} -inline void style(UiWidget *widget) { - widget->labelfont(UI_HELVETICA); widget->labelsize(12); widget->labelcolor(text()); - widget->color(panel()); widget->selection_color(ui_rgb_color(53,116,111)); - if (widget->box() != UI_NO_BOX && widget->box() != UI_FLAT_BOX) widget->box(flat); - if (auto *button = dynamic_cast(widget)) { - button->box(flat); button->down_box(pressed); - button->color(ui_rgb_color(45,51,59)); - } - if (auto *group = dynamic_cast(widget)) { - for (int i = 0; i < group->children(); ++i) style(group->child(i)); - if (auto *scroll = dynamic_cast(group)) { - scroll->scrollbar.box(flat); scroll->hscrollbar.box(flat); - scroll->scrollbar.slider(UI_FLAT_BOX); scroll->hscrollbar.slider(UI_FLAT_BOX); - } - } -} -} -#endif diff --git a/src/gui/JUCE/track_inserts.hpp b/src/gui/JUCE/track_inserts.hpp deleted file mode 100644 index 54075dd..0000000 --- a/src/gui/JUCE/track_inserts.hpp +++ /dev/null @@ -1,206 +0,0 @@ -#ifndef XTC_TRACK_INSERTS_HPP -#define XTC_TRACK_INSERTS_HPP -#include "main_window.hpp" -#include "graph_widget.hpp" -#include "plugin_library.hpp" -#include "plugin_dialog.hpp" -#include "sequencer_widget.hpp" -#include "insert_meter.hpp" -#include "ui.hpp" -#include -#include -#include -class TrackInsertsPanel : public UiGroup { - MainWindow *owner_; - UiChoice *tracks_; - UiMenu_Button *actions_; - InsertMeter *meter_; - UiHold_Browser *slots_; - UiBox *message_; - UiValue_Input *wet_,*gain_; - GraphWidget *graph_; - UiScroll *scroll_; - xtc_graph *model_=nullptr; - uint64_t track_=UINT64_MAX,meter_track_=UINT64_MAX; - xtc_track_chain cached_{}; - xtc_track_chain clipboard_{}; - bool clipboard_ready_=false; - struct Comparison {xtc_track_chain chain{};uint64_t track=UINT64_MAX;bool ready=false;} comparisons_[2]; - std::vector ids_; - std::vector names_; - void result(xtc_result r) { if(r!=XTC_OK)owner_->report_error("Track inserts",r);refresh(true); } - public: - TrackInsertsPanel(int x,int y,int w,int h,MainWindow *owner):UiGroup(x,y,w,h,"Track inserts"),owner_(owner) { - tracks_=new UiChoice(x+64,y+12,240,28,"Track");tracks_->callback([](UiWidget *,void *c){auto *p=static_cast(c);int i=p->tracks_->value();if(i>=0 && static_cast(i)ids_.size()){p->track_=p->ids_[static_cast(i)];p->refresh(true);}},this); - auto *add=new UiMenu_Button(x+320,y+12,150,28,"Add effect"); - add->add("Internal Delay",0,[](UiWidget *,void *c){static_cast(c)->add(XTC_BUILTIN_PATH,2);},this); - add->add("Internal Reverb",0,[](UiWidget *,void *c){static_cast(c)->add(XTC_BUILTIN_PATH,3);},this); - add->add("Plugin library...",0,[](UiWidget *,void *c){auto *p=static_cast(c);std::string file;if(!xtc_choose_plugin_file(p->owner_->settings(),file))return;unsigned int index=0;if(xtc_choose_plugin(file.c_str(),p->owner_->sample_rate(),index))p->add(file.c_str(),index);},this); - for(unsigned int preset=0;preset<3;++preset) { - std::string label=std::string("Templates (replace)/")+xtc_track_chain_template_name(preset); - add->add(label.c_str(),0,[](UiWidget *w,void *c){auto *p=static_cast(c);auto *menu=static_cast(w);unsigned int index=0;for(;index<3;++index)if(!strcmp(menu->mvalue()->label(),xtc_track_chain_template_name(index)))break;p->result(p->apply_template(index));},this); - } - struct Action {const char *name;int command;}; - int left=x+486; - for(const auto &action:{Action{"Up",-1},Action{"Down",1},Action{"Bypass",2},Action{"Remove",3}}) { - auto *button=new UiButton(left,y+12,82,28,action.name);left+=90; - button->callback([](UiWidget *w,void *c){auto *p=static_cast(c);int command=!strcmp(w->label(),"Up") ? -1 : !strcmp(w->label(),"Down") ? 1 : !strcmp(w->label(),"Bypass") ? 2 : 3;p->result(p->edit_slot(command));},this); - } - auto *inspect=new UiButton(x+850,y+12,120,28,"Inspect / edit"); - inspect->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->inspect_slot());},this); - auto *presets=new UiMenu_Button(x+978,y+12,68,28,"Preset"); - presets->add("Save chain...",0,[](UiWidget *,void *c){static_cast(c)->preset(true);},this); - presets->add("Load chain...",0,[](UiWidget *,void *c){static_cast(c)->preset(false);},this); - wet_=new UiValue_Input(x+110,y+52,100,26,"Insert wet");wet_->bounds(0,1);wet_->step(.01);wet_->when(UI_WHEN_RELEASE); - gain_=new UiValue_Input(x+360,y+52,100,26,"Chain gain");gain_->bounds(0,4);gain_->step(.01);gain_->when(UI_WHEN_RELEASE); - for(auto *input:{wet_,gain_})input->callback([](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->set_mix(p->wet_->value(),p->gain_->value()));},this); - actions_=new UiMenu_Button(x+486,y+52,150,26,"Rack actions"); - actions_->add("Enable all effects",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->bulk(XTC_CHAIN_ENABLE_ALL));},this); - actions_->add("Bypass all effects",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->bulk(XTC_CHAIN_BYPASS_ALL));},this); - actions_->add("Clear chain",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->bulk(XTC_CHAIN_CLEAR));},this); - actions_->add("Compare/Capture A",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->comparison(0,true));},this); - actions_->add("Compare/Recall A",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->comparison(0,false));},this,UI_MENU_INACTIVE); - actions_->add("Compare/Capture B",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->comparison(1,true));},this); - actions_->add("Compare/Recall B",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->comparison(1,false));},this,UI_MENU_INACTIVE); - actions_->add("Copy chain",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->copy_chain());},this); - actions_->add("Paste chain (replace)",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->paste_chain());},this,UI_MENU_INACTIVE); - actions_->add("Duplicate effect",0,[](UiWidget *,void *c){auto *p=static_cast(c);p->result(p->duplicate_slot());},this); - meter_=new InsertMeter(x+650,y+52,w-666,26); - message_=new UiBox(x+16,y+84,w-32,30,"Track input > inserts > output gain > track fader > bus/master");message_->align(UI_ALIGN_LEFT|UI_ALIGN_INSIDE); - slots_=new UiHold_Browser(x+16,y+126,230,h-142);slots_->format_char(0);slots_->callback([](UiWidget *,void *c){static_cast(c)->update_mix();},this); - scroll_=new UiScroll(x+260,y+126,w-276,h-142); - graph_=new GraphWidget(x+260,y+126,2000,4000,nullptr,[](void *,const xtc_graph_edit &,size_t *){return XTC_ERR_UNSUPPORTED;},nullptr,message_); - scroll_->end();resizable(scroll_);end(); - } - ~TrackInsertsPanel() override { graph_->set_graph(nullptr);xtc_graph_destroy(model_); } - uint64_t selected_track() const {return track_;} - size_t selected_slot() const {return slots_->value()>0 ? static_cast(slots_->value()-1) : SIZE_MAX;} - void select_track(uint64_t id) {track_=id;refresh(true);} - const xtc_graph *graph() const {return model_;} - void add(const char *file,unsigned int index) { result(owner_->add_track_effect(track_,file,index)); } - xtc_result bulk(xtc_chain_action action) { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); - if(r==XTC_OK && !chain.count)return XTC_ERR_NOT_FOUND; - if(r==XTC_OK)r=xtc_track_chain_bulk(&chain,action,&chain); - return r==XTC_OK ? owner_->set_track_chain(track_,chain) : r; - } - xtc_result apply_template(unsigned int index) { - xtc_track_chain chain{};auto r=xtc_track_chain_template(index,xtc_project_tempo(owner_->project_.get()),&chain); - if(r==XTC_OK)r=owner_->set_track_chain(track_,chain); - return r; - } - InsertMeter *meter() const {return meter_;} - void meters(const xtc_meter_snapshot &snapshot,bool active,double seconds=.02) {meter_->feed(snapshot,track_,active,seconds);} - void clear_session() {meter_->feed(xtc_meter_snapshot{},UINT64_MAX,false,.02);clipboard_={};clipboard_ready_=false;for(auto &slot:comparisons_)slot=Comparison{};update_actions();} - bool clipboard_ready() const {return clipboard_ready_;} - bool comparison_ready(unsigned int slot) const {return slot<2 && comparisons_[slot].ready && comparisons_[slot].track==track_;} - xtc_result comparison(unsigned int slot,bool capture) { - if(slot>=2)return XTC_ERR_INVALID_ARGUMENT; - if(capture) { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); - if(r==XTC_OK){comparisons_[slot].chain=chain;comparisons_[slot].track=track_;comparisons_[slot].ready=true;update_actions();} - return r; - } - if(!comparison_ready(slot))return XTC_ERR_NOT_FOUND; - return owner_->set_track_chain(track_,comparisons_[slot].chain); - } - void update_actions() { - for(unsigned int slot=0;slot<2;++slot){int item=actions_->find_index(slot ? "Compare/Recall B" : "Compare/Recall A");if(item>=0)actions_->mode(item,comparison_ready(slot) ? 0 : UI_MENU_INACTIVE);} - int index=actions_->find_index("Paste chain (replace)"); - if(index>=0)actions_->mode(index,clipboard_ready_ ? 0 : UI_MENU_INACTIVE); - } - xtc_result copy_chain() { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); - if(r==XTC_OK){clipboard_=chain;clipboard_ready_=true;update_actions();}return r; - } - xtc_result paste_chain() { - if(!clipboard_ready_)return XTC_ERR_NOT_FOUND; - return owner_->set_track_chain(track_,clipboard_); - } - xtc_result duplicate_slot() { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot(); - if(r==XTC_OK)r=xtc_track_chain_duplicate(&chain,index,&chain); - if(r==XTC_OK)r=owner_->set_track_chain(track_,chain); - if(r==XTC_OK){slots_->select(static_cast(index+2));update_mix();} - return r; - } - void update_mix() { - xtc_track_chain chain{};if(xtc_project_get_chain(owner_->project_.get(),track_,&chain)!=XTC_OK){wet_->deactivate();gain_->deactivate();return;} - gain_->activate();gain_->value(chain.output_gain);size_t index=selected_slot(); - if(indexactivate();wet_->value(chain.slots[index].wet);}else wet_->deactivate(); - } - xtc_result set_mix(double wet,double gain) { - if(!std::isfinite(wet) || wet<0 || wet>1 || !std::isfinite(gain) || gain<0 || gain>4)return XTC_ERR_INVALID_ARGUMENT; - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);if(r!=XTC_OK)return r; - size_t index=selected_slot();if(index(wet); - chain.output_gain=static_cast(gain);return owner_->set_track_chain(track_,chain); - } - xtc_result save_preset(const char *file) { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); - return r==XTC_OK ? xtc_track_chain_preset_save(&chain,file) : r; - } - xtc_result load_preset(const char *file) { - xtc_track_chain chain{};auto r=xtc_track_chain_preset_load(file,&chain); - return r==XTC_OK ? owner_->set_track_chain(track_,chain) : r; - } - void preset(bool save) { - UiNative_File_Chooser chooser;chooser.title(save ? "Save effect chain" : "Load effect chain"); - chooser.type(save ? UiNative_File_Chooser::BROWSE_SAVE_FILE : UiNative_File_Chooser::BROWSE_FILE); - chooser.filter("Effect chain project\t*.xtc");if(save)chooser.options(UiNative_File_Chooser::SAVEAS_CONFIRM); - if(chooser.show()!=0 || !chooser.filename())return; - result(save ? save_preset(chooser.filename()) : load_preset(chooser.filename())); - } - xtc_result inspect_slot() { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot(); - if(r!=XTC_OK)return r; - if(index>=chain.count)return XTC_ERR_INVALID_STATE; - xtc_plugin_details details{};xtc_plugin_spec edited{}; - r=xtc_plugin_describe(&chain.slots[index].plugin,owner_->sample_rate(),&details);if(r!=XTC_OK)return r; - if(!xtc_edit_plugin(chain.slots[index].plugin,details,edited))return XTC_OK; - chain.slots[index].plugin=edited;return owner_->set_track_chain(track_,chain); - } - /* Commands: -1 up, 1 down, 2 toggle bypass, 3 remove. */ - xtc_result edit_slot(int command) { - xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot(); - if(r!=XTC_OK)return r; - if(index>=chain.count)return XTC_ERR_INVALID_STATE; - size_t selected=index; - if(command==-1 || command==1) { - if((command==-1 && !index) || (command==1 && index+1>=chain.count))return XTC_ERR_INVALID_ARGUMENT; - selected=command==-1 ? index-1 : index+1;std::swap(chain.slots[index],chain.slots[selected]); - }else if(command==2)chain.slots[index].enabled=!chain.slots[index].enabled; - else if(command==3) {for(size_t i=index+1;iset_track_chain(track_,chain); - if(r==XTC_OK && chain.count)slots_->select(static_cast(std::min(selected,chain.count-1)+1)); - update_mix();return r; - } - void refresh(bool force=false) { - const xtc_project *project=owner_->project_.get();size_t count=0,clips=0;xtc_project_get_counts(project,&count,&clips); - std::vector ids;std::vector names; - for(size_t i=0;iclear();for(const auto &name:names) {tracks_->add("track");tracks_->replace(tracks_->size()-2,name.c_str());}} - if(std::find(ids_.begin(),ids_.end(),track_)==ids_.end())track_=ids_.empty() ? UINT64_MAX : ids_[0]; - for(size_t i=0;ivalue(static_cast(i)); - update_actions(); - if(track_!=meter_track_){meter_track_=track_;meters(xtc_meter_snapshot{},false);} - xtc_track_chain chain{};chain.output_gain=1; - if(track_!=UINT64_MAX)xtc_project_get_chain(project,track_,&chain); - if(!rebuild && !std::memcmp(&cached_,&chain,sizeof(chain)))return; - cached_=chain;int selected=slots_->value();slots_->clear(); - for(size_t i=0;iadd(label.c_str());} - if(chain.count)slots_->select(selected>0 && static_cast(selected)<=chain.count ? selected : 1); - xtc_graph *next=nullptr;auto r=xtc_track_chain_graph(&chain,&next); - if(r==XTC_OK){graph_->set_graph(next);xtc_graph_destroy(model_);model_=next;graph_->auto_layout(); - size_t nodes=0,edges=0,max_level=0,max_row=0;std::array rows{}; - xtc_graph_get_counts(model_,&nodes,&edges); - for(size_t i=0;irouting(node.id,route))max_level=std::max(max_level,route.level);} - graph_->size(std::max(scroll_->w()-20,static_cast(max_level+1)*174+40),4000); - for(size_t i=0;irouting(node.id,route) && route.levelmove_node(node.id,20+static_cast(route.level)*174,24+static_cast(row)*104);}} - graph_->size(graph_->w(),std::max(scroll_->h()-20,static_cast(max_row+1)*104+48));scroll_->scroll_to(0,0); - message_->copy_label("Track input > inserts > output gain > track fader > bus/master. Scroll to inspect long chains.");} - update_mix();redraw(); - } -}; -#endif diff --git a/src/gui/JUCE/ui.cpp b/src/gui/JUCE/ui.cpp deleted file mode 100644 index 27594b9..0000000 --- a/src/gui/JUCE/ui.cpp +++ /dev/null @@ -1,1234 +0,0 @@ -#include "ui.hpp" -#include "xtc/xtc_version.h" -#include -namespace -{ -thread_local juce::Graphics *graphics = nullptr; -thread_local std::unique_ptr font; -thread_local float stroke = 1; -std::vector windows; -struct Timeout : juce::Timer -{ - void (*function)(void *); - void *context; - bool cancelled = false, executing = false; - Timeout(double seconds, void (*f)(void *), void *c) : function(f), context(c) - { - startTimer(std::max(1, static_cast(seconds * 1000))); - } - void timerCallback() override; -}; -std::vector> timers; -void Timeout::timerCallback() -{ - stopTimer(); - if (cancelled) - return; - cancelled = true; - executing = true; - function(context); - executing = false; -} -juce::Colour colour(UiColor c) { return juce::Colour(0xff000000U | c); } -std::string formatted(const char *text, va_list args) -{ - char buffer[8192]; - std::vsnprintf(buffer, sizeof(buffer), text, args); - return buffer; -} -int key_code(const juce::KeyPress &key) -{ - const int k = key.getKeyCode(); - if (k == juce::KeyPress::escapeKey) - return UI_Escape; - if (k == juce::KeyPress::returnKey) - return UI_Enter; - if (k == juce::KeyPress::deleteKey) - return UI_Delete; - if (k == juce::KeyPress::backspaceKey) - return UI_BackSpace; - if (k == juce::KeyPress::leftKey) - return UI_Left; - if (k == juce::KeyPress::rightKey) - return UI_Right; - if (k == juce::KeyPress::upKey) - return UI_Up; - if (k == juce::KeyPress::downKey) - return UI_Down; - if (k == juce::KeyPress::homeKey) - return UI_Home; - if (k >= juce::KeyPress::F1Key && k <= juce::KeyPress::F16Key) - return UI_F + k - juce::KeyPress::F1Key + 1; - return juce::CharacterFunctions::toLowerCase(static_cast(k)); -} -int modifiers(juce::ModifierKeys m) -{ - return (m.isCtrlDown() ? UI_CTRL : 0) | (m.isShiftDown() ? UI_SHIFT : 0) | - (m.isAltDown() ? UI_ALT : 0) | (m.isCommandDown() && !m.isCtrlDown() ? UI_META : 0); -} -} // namespace -int Ui::ex = 0, Ui::ey = 0, Ui::ek = 0, Ui::es = 0, Ui::eb = 0, Ui::clicks = 0; -UiWidget *Ui::pressed = nullptr; -UiGroup *UiGroup::current_ = nullptr; -bool Ui::event_inside(const UiWidget *w) -{ - return ex >= w->x() && ey >= w->y() && ex < w->x() + w->w() && ey < w->y() + w->h(); -} -UiWidget *Ui::focus() -{ - auto *c = juce::Component::getCurrentlyFocusedComponent(); - while (c) - { - if (auto *w = dynamic_cast(c)) - return w; - c = c->getParentComponent(); - } - return nullptr; -} -void Ui::delete_widget(UiWidget *w) -{ - juce::Component::SafePointer safe(w); - juce::MessageManager::callAsync( - [safe] - { - if (safe) - delete safe.getComponent(); - }); -} -void Ui::add_timeout(double s, void (*f)(void *), void *c) -{ - timers.erase(std::remove_if(timers.begin(), timers.end(), - [](auto &t) { return t->cancelled && !t->executing; }), - timers.end()); - timers.push_back(std::make_unique(s, f, c)); -} -void Ui::remove_timeout(void (*f)(void *), void *c) -{ - for (auto &t : timers) - if (t->function == f && t->context == c) - { - t->stopTimer(); - t->cancelled = true; - } - timers.erase(std::remove_if(timers.begin(), timers.end(), - [](auto &t) { return t->cancelled && !t->executing; }), - timers.end()); -} -double Ui::wait(double s) -{ - juce::MessageManager::getInstance()->runDispatchLoopUntil( - std::max(1, static_cast(s * 1000))); - timers.erase(std::remove_if(timers.begin(), timers.end(), - [](auto &t) { return t->cancelled && !t->executing; }), - timers.end()); - return s; -} -int Ui::run() -{ - for (;;) - { - bool any = false; - for (auto *w : windows) - any |= w->shown(); - if (!any) - break; - wait(); - } - timers.clear(); - return 0; -} -void Ui::flush() -{ - for (auto *w : windows) - if (auto *p = w->getPeer()) - p->performAnyPendingRepaintsNow(); -} -void Ui::screen_work_area(int &x, int &y, int &w, int &h, int) -{ - auto *d = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay(); - auto r = d ? d->userBounds.toNearestInt() : juce::Rectangle(0, 0, 1440, 960); - x = r.getX(); - y = r.getY(); - w = r.getWidth(); - h = r.getHeight(); -} -UiColor ui_rgb_color(unsigned char r, unsigned char g, unsigned char b) -{ - return (static_cast(r) << 16) | (static_cast(g) << 8) | b; -} -UiColor ui_darker(UiColor c) { return colour(c).darker(.25f).getARGB() & 0xffffff; } -UiColor ui_color_average(UiColor a, UiColor b, float weight) -{ - return colour(b).interpolatedWith(colour(a), weight).getARGB() & 0xffffff; -} -void ui_color(UiColor c) -{ - if (graphics) - graphics->setColour(colour(c)); -} -void ui_font(int f, int size) -{ - font = std::make_unique( - juce::FontOptions(static_cast(size), f ? juce::Font::bold : juce::Font::plain)); - if (f == UI_COURIER_BOLD) - font->setTypefaceName(juce::Font::getDefaultMonospacedFontName()); - if (graphics) - graphics->setFont(*font); -} -void ui_rectf(int x, int y, int w, int h) -{ - if (graphics && w > 0 && h > 0) - graphics->fillRect(x, y, w, h); -} -void ui_rect(int x, int y, int w, int h) -{ - if (graphics && w > 0 && h > 0) - graphics->drawRect(x, y, w, h); -} -void ui_line(int x, int y, int a, int b) -{ - if (graphics) - graphics->drawLine(static_cast(x), static_cast(y), static_cast(a), - static_cast(b), stroke); -} -void ui_line_style(int, int width) { stroke = static_cast(std::max(1, width)); } -void ui_polygon(int a, int b, int c, int d, int e, int f) -{ - if (graphics) - { - juce::Path p; - p.startNewSubPath(static_cast(a), static_cast(b)); - p.lineTo(static_cast(c), static_cast(d)); - p.lineTo(static_cast(e), static_cast(f)); - p.closeSubPath(); - graphics->fillPath(p); - } -} -void ui_pie(int x, int y, int w, int h, double, double) -{ - if (graphics) - graphics->fillEllipse(static_cast(x), static_cast(y), static_cast(w), - static_cast(h)); -} -void ui_draw(const char *s, int x, int y) -{ - if (graphics && s) - graphics->drawSingleLineText(juce::String::fromUTF8(s), x, y); -} -void ui_draw(const char *s, int x, int y, int w, int h, int align) -{ - if (!graphics || !s) - return; - auto justification = align & UI_ALIGN_LEFT - ? juce::Justification::centredLeft - : (align & UI_ALIGN_RIGHT ? juce::Justification::centredRight - : juce::Justification::centred); - if (align & UI_ALIGN_TOP) - justification = juce::Justification::topLeft; - graphics->drawFittedText(juce::String::fromUTF8(s), x, y, w, h, justification, - align & UI_ALIGN_WRAP ? std::max(1, h / 12) : 1); -} -void ui_push_clip(int x, int y, int w, int h) -{ - if (graphics) - { - graphics->saveState(); - graphics->reduceClipRegion(x, y, w, h); - } -} -void ui_pop_clip() -{ - if (graphics) - graphics->restoreState(); -} -int ui_clip_box(int x, int y, int w, int h, int &a, int &b, int &c, int &d) -{ - auto r = juce::Rectangle(x, y, w, h); - if (graphics) - r = r.getIntersection(graphics->getClipBounds()); - a = r.getX(); - b = r.getY(); - c = r.getWidth(); - d = r.getHeight(); - return !r.isEmpty(); -} -namespace -{ -void message_dialog(const std::string &text) -{ - UiWindow window(720, 240, "xtc"); - window.set_modal(); - auto *box = new UiBox(16, 16, 688, 164, text.c_str()); - box->align(UI_ALIGN_LEFT | UI_ALIGN_INSIDE | UI_ALIGN_WRAP); - auto *close = new UiReturn_Button(600, 194, 100, 30, "OK"); - close->callback([](UiWidget *, void *w) { static_cast(w)->hide(); }, &window); - window.callback([](UiWidget *w, void *) { w->hide(); }); - window.end(); - window.show(); - while (window.shown()) - Ui::wait(); -} -} // namespace -void ui_alert(const char *s, ...) -{ - va_list a; - va_start(a, s); - auto t = formatted(s, a); - va_end(a); - message_dialog(t); -} -void ui_message(const char *s, ...) -{ - va_list a; - va_start(a, s); - auto t = formatted(s, a); - va_end(a); - message_dialog(t); -} -int ui_choice(const char *s, const char *a, const char *b, const char *c, ...) -{ - va_list args; - va_start(args, c); - auto message = formatted(s, args); - va_end(args); - juce::AlertWindow dialog("xtc", message, juce::MessageBoxIconType::QuestionIcon); - if (a) - dialog.addButton(a, 1); - if (b) - dialog.addButton(b, 2); - if (c) - dialog.addButton(c, 3); - int result = dialog.runModalLoop(); - return result ? result - 1 : 0; -} -const char *ui_input(const char *s, const char *initial, ...) -{ - va_list args; - va_start(args, initial); - auto message = formatted(s, args); - va_end(args); - juce::AlertWindow dialog("xtc", message, juce::MessageBoxIconType::QuestionIcon); - dialog.addTextEditor("value", initial ? initial : ""); - dialog.addButton("OK", 1, juce::KeyPress(juce::KeyPress::returnKey)); - dialog.addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey)); - static std::string answer; - if (dialog.runModalLoop() != 1) - return nullptr; - answer = dialog.getTextEditorContents("value").toStdString(); - return answer.c_str(); -} - -UiWidget::UiWidget(int x, int y, int w, int h, const char *s) - : px_(x), py_(y), pw_(w), ph_(h), label_(s ? s : "") -{ - setWantsKeyboardFocus(true); - setVisible(true); - setName(label_); - if (UiGroup::current()) - UiGroup::current()->add(this); - else - setBounds(x, y, w, h); -} -UiWidget::~UiWidget() -{ - if (Ui::pressed == this) - Ui::pressed = nullptr; - if (parent_) - parent_->remove(this); -} -void UiWidget::resize(int x, int y, int w, int h) -{ - if (x == px_ && y == py_ && w == pw_ && h == ph_ && - getX() == x - (parent_ ? parent_->x() : 0) && getY() == y - (parent_ ? parent_->y() : 0) && - getWidth() == w && getHeight() == h) - return; - px_ = x; - py_ = y; - pw_ = std::max(1, w); - ph_ = std::max(1, h); - setBounds(x - (parent_ ? parent_->x() : 0), y - (parent_ ? parent_->y() : 0), pw_, ph_); -} -UiWindow *UiWidget::window() const -{ - auto *c = const_cast(this)->getTopLevelComponent(); - return dynamic_cast(c); -} -void UiWidget::paint(juce::Graphics &g) -{ - auto *previous = graphics; - graphics = &g; - g.saveState(); - g.setOrigin(-x(), -y()); - if (!font) - ui_font(0, 12); - g.setFont(*font); - draw(); - g.restoreState(); - graphics = previous; -} -void UiWidget::draw_box() -{ - if (box() != UI_NO_BOX) - { - ui_color(color()); - ui_rectf(x(), y(), w(), h()); - if (box() != UI_FLAT_BOX) - { - ui_color(0x3a4049); - ui_rect(x(), y(), w(), h()); - } - } -} -void UiWidget::draw_label() -{ - ui_color(labelcolor()); - ui_font(labelfont(), labelsize()); - ui_draw(label(), x() + 4, y(), w() - 8, h(), align()); -} -void UiWidget::event(const juce::MouseEvent &e, int type) -{ - Ui::ex = x() + e.x; - Ui::ey = y() + e.y; - Ui::es = modifiers(e.mods); - Ui::eb = e.mods.isRightButtonDown() ? UI_RIGHT_MOUSE : UI_LEFT_MOUSE; - Ui::clicks = e.getNumberOfClicks() - 1; - if (type == UI_PUSH) - { - Ui::pressed = this; - take_focus(); - } - handle(type); - if (type == UI_RELEASE) - Ui::pressed = nullptr; -} -void UiWidget::mouseDown(const juce::MouseEvent &e) { event(e, UI_PUSH); } -void UiWidget::mouseDrag(const juce::MouseEvent &e) { event(e, UI_DRAG); } -void UiWidget::mouseUp(const juce::MouseEvent &e) { event(e, UI_RELEASE); } -void UiWidget::mouseDoubleClick(const juce::MouseEvent &e) { event(e, UI_PUSH); } -bool UiWidget::keyPressed(const juce::KeyPress &key) -{ - Ui::ek = key_code(key); - Ui::es = modifiers(key.getModifiers()); - if (handle(UI_KEYDOWN)) - return true; - if (auto *w = window()) - return w->handle(UI_SHORTCUT) != 0; - return false; -} -UiGroup::UiGroup(int x, int y, int w, int h, const char *s) : UiWidget(x, y, w, h, s) { begin(); } -UiGroup::~UiGroup() -{ - if (current_ == this) - current_ = parent(); - clear(); -} -void UiGroup::add(UiWidget *w) -{ - if (w->parent_ == this) - return; - if (w->parent_) - w->parent_->remove(w); - children_.push_back(w); - w->parent_ = this; - addChildComponent(w); - w->setVisible(true); - w->resize(w->x(), w->y(), w->w(), w->h()); -} -void UiGroup::remove(UiWidget *w) -{ - children_.erase(std::remove(children_.begin(), children_.end(), w), children_.end()); - removeChildComponent(w); - if (w->parent_ == this) - w->parent_ = nullptr; - if (resizable_ == w) - resizable_ = nullptr; -} -void UiGroup::clear() -{ - while (!children_.empty()) - delete children_.back(); -} -void UiGroup::resize(int nx, int ny, int nw, int nh) -{ - if (nx == x() && ny == y() && nw == w() && nh == h() && - getX() == nx - (parent() ? parent()->x() : 0) && - getY() == ny - (parent() ? parent()->y() : 0) && getWidth() == nw && getHeight() == nh) - return; - int dx = nx - x(), dy = ny - y(), dw = nw - w(), dh = nh - h(); - int rx = resizable_ ? resizable_->x() : x(), ry = resizable_ ? resizable_->y() : y(), - rw = resizable_ ? resizable_->w() : w(), rh = resizable_ ? resizable_->h() : h(); - UiWidget::resize(nx, ny, nw, nh); - auto edge = [](int p, int r, int size, int delta) - { - if (p <= r) - return p; - if (p >= r + size) - return p + delta; - return p + - static_cast(std::round(static_cast(p - r) * delta / std::max(1, size))); - }; - for (auto *c : children_) - { - int a = c->x(), b = c->y(), z = a + c->w(), t = b + c->h(); - if (resizable_) - { - a = edge(a, rx, rw, dw); - z = edge(z, rx, rw, dw); - b = edge(b, ry, rh, dh); - t = edge(t, ry, rh, dh); - } - c->resize(a + dx, b + dy, z - a, t - b); - } - redraw(); -} -UiWindow::UiWindow(int w, int h, const char *s) : UiWindow(50, 50, w, h, s) {} -UiWindow::UiWindow(int x, int y, int w, int h, const char *s) : UiGroup(0, 0, w, h, s) -{ - box(UI_FLAT_BOX); - if (parent()) - parent()->remove(this); - setBounds(x, y, w, h); - setVisible(false); - windows.push_back(this); -} -UiWindow::~UiWindow() -{ - windows.erase(std::remove(windows.begin(), windows.end(), this), windows.end()); - if (isCurrentlyModal()) - exitModalState(0); - removeFromDesktop(); -} -void UiWindow::show() -{ - if (!isOnDesktop()) - addToDesktop( - juce::ComponentPeer::windowHasTitleBar | juce::ComponentPeer::windowHasCloseButton | - juce::ComponentPeer::windowIsResizable | juce::ComponentPeer::windowHasMinimiseButton | - juce::ComponentPeer::windowHasMaximiseButton); - setVisible(true); - toFront(true); - if (modal_ && !isCurrentlyModal()) - enterModalState(true, nullptr, false); -} -void UiWindow::hide() -{ - if (isCurrentlyModal()) - exitModalState(0); - setVisible(false); -} -UiButton::UiButton(int x, int y, int w, int h, const char *s) : UiWidget(x, y, w, h, s) -{ - addChildComponent(button_); - button_.onClick = [this] { do_callback(); }; - sync_control(); - resized(); -} -void UiButton::sync_control() -{ - button_.setTooltip(getTooltip()); - button_.setButtonText(label()); - button_.setColour(juce::TextButton::buttonColourId, colour(color())); - button_.setColour(juce::TextButton::textColourOffId, colour(labelcolor())); - button_.setColour(juce::TextButton::buttonOnColourId, colour(selection_color())); -} -UiInput_::UiInput_(int x, int y, int w, int h, const char *s) : UiWidget(x, y, w, h, s) -{ - align(UI_ALIGN_LEFT); - addAndMakeVisible(editor); - editor.setMultiLine(false); - editor.onTextChange = [this] - { - if (when() & UI_WHEN_CHANGED) - do_callback(); - }; - editor.onReturnKey = [this] { do_callback(); }; - editor.onFocusLost = [this] - { - if (when() & UI_WHEN_RELEASE) - do_callback(); - }; - sync_control(); - resized(); -} -void UiInput_::value(const char *s) { editor.setText(juce::String::fromUTF8(s ? s : ""), false); } -const char *UiInput_::value() const -{ - text_ = editor.getText().toStdString(); - return text_.c_str(); -} -void UiInput_::sync_control() -{ - editor.setTooltip(getTooltip()); - editor.setColour(juce::TextEditor::backgroundColourId, colour(color())); - editor.setColour(juce::TextEditor::textColourId, colour(labelcolor())); -} -UiValue_Input::UiValue_Input(int x, int y, int w, int h, const char *s) - : UiWidget(x, y, w, h, s), input(x, y, w, h) -{ - if (input.parent()) - input.parent()->remove(&input); - addAndMakeVisible(input); - input.callback( - [](UiWidget *, void *p) - { - auto &v = *static_cast(p); - char *end = nullptr; - double d = std::strtod(v.input.value(), &end); - if (end && *end == 0 && std::isfinite(d)) - { - v.value(std::clamp(d, v.low_, v.high_)); - v.do_callback(); - } - else - v.value(v.value_); - }, - this); - input.when(UI_WHEN_ENTER_KEY | UI_WHEN_RELEASE); - resized(); -} -void UiValue_Input::value(double v) -{ - value_ = v; - char b[64]; - std::snprintf(b, sizeof(b), "%.8g", v); - input.value(b); -} -void UiValue_Input::sync_control() -{ - input.tooltip(getTooltip().toRawUTF8()); - input.color(color()); - input.labelcolor(labelcolor()); -} -UiValue_Slider::UiValue_Slider(int x, int y, int w, int h, const char *s) : UiWidget(x, y, w, h, s) -{ - addAndMakeVisible(slider_); - slider_.setSliderStyle(juce::Slider::LinearHorizontal); - slider_.setTextBoxStyle(juce::Slider::TextBoxRight, false, 60, 20); - slider_.setRange(0, 1); - slider_.setDoubleClickReturnValue(true, 0); - slider_.onValueChange = [this] - { - if ((when() & UI_WHEN_CHANGED) || !slider_.isMouseButtonDown(true)) - do_callback(); - }; - slider_.onDragStart = [this] { Ui::pressed = this; }; - slider_.onDragEnd = [this] - { - if (when() & UI_WHEN_RELEASE) - do_callback(); - Ui::pressed = nullptr; - }; - resized(); -} -void UiValue_Slider::bounds(double l, double h) -{ - slider_.setRange(std::min(l, h), std::max(l, h), slider_.getInterval()); -} -void UiValue_Slider::step(double s) -{ - slider_.setRange(slider_.getMinimum(), slider_.getMaximum(), s); -} -void UiValue_Slider::type(int t) -{ - UiWidget::type(t); - slider_.setSliderStyle(t == UI_VERT_SLIDER ? juce::Slider::LinearVertical - : t == UI_LINE_DIAL ? juce::Slider::RotaryHorizontalVerticalDrag - : juce::Slider::LinearHorizontal); - slider_.setTextBoxStyle( - t == UI_VERT_SLIDER ? juce::Slider::NoTextBox : juce::Slider::TextBoxRight, false, 60, 20); -} -void UiValue_Slider::sync_control() -{ - slider_.setTooltip(getTooltip()); - slider_.setColour(juce::Slider::backgroundColourId, colour(color())); - slider_.setColour(juce::Slider::thumbColourId, colour(selection_color())); -} -void UiProgress::draw() -{ - draw_box(); - ui_color(selection_color()); - ui_rectf(x(), y(), - static_cast( - w() * std::clamp((value_ - low_) / std::max(1e-12, high_ - low_), 0.0, 1.0)), - h()); - draw_label(); -} -const UiMenu_Item *UiMenu_Item::popup(int, int) const -{ - juce::PopupMenu menu; - int i = 0; - while (this[i].text) - { - menu.addItem(i + 1, juce::String::fromUTF8(this[i].text), this[i].active(), - (this[i].flags & UI_MENU_VALUE) != 0); - ++i; - } - int picked = menu.show(); - return picked > 0 ? this + picked - 1 : nullptr; -} -void UiMenu_::repair() -{ - for (size_t i = 0; i < labels_.size(); ++i) - items_[i].text = labels_[i].c_str(); - items_.back() = {}; -} -int UiMenu_::add(const char *s, int shortcut, UiCallback callback, void *context, int flags) -{ - std::string all = s ? s : ""; - size_t start = 0; - int first = static_cast(items_.size()) - 1; - do - { - size_t end = all.find('|', start); - std::string path = all.substr(start, end == std::string::npos ? end : end - start); - paths_.push_back(path); - size_t slash = path.find_last_of('/'); - labels_.push_back(path.substr(slash == std::string::npos ? 0 : slash + 1)); - items_.insert(items_.end() - 1, {nullptr, shortcut, callback, context, flags}); - if (end == std::string::npos) - break; - start = end + 1; - } while (start <= all.size()); - repair(); - return first; -} -void UiMenu_::replace(int index, const char *s) -{ - if (index < 0 || index >= static_cast(items_.size())) - return; - labels_[static_cast(index)] = s ? s : ""; - repair(); - redraw(); -} -void UiMenu_::clear() -{ - literal_ = false; - items_.assign(1, {}); - paths_.clear(); - labels_.clear(); - selected_ = -1; -} -void UiMenu_::copy(const UiMenu_Item *items) -{ - clear(); - literal_ = true; - for (size_t i = 0; items[i].text; ++i) - { - paths_.emplace_back(items[i].text); - labels_.emplace_back(items[i].text); - items_.insert(items_.end() - 1, items[i]); - } - repair(); -} -const UiMenu_Item *UiMenu_::menu() const { return items_.data(); } -const UiMenu_Item *UiMenu_::mvalue() const -{ - return selected_ >= 0 && selected_ < static_cast(labels_.size()) - ? &items_[static_cast(selected_)] - : nullptr; -} -const UiMenu_Item *UiMenu_::find_item(const char *s) const -{ - for (size_t i = 0; i < paths_.size(); ++i) - if (paths_[i] == s) - return &items_[i]; - return nullptr; -} -const UiMenu_Item *UiMenu_::picked(const UiMenu_Item *item) -{ - if (!item || !item->active()) - return nullptr; - selected_ = static_cast(item - items_.data()); - if (item->flags & UI_MENU_TOGGLE) - items_[static_cast(selected_)].flags ^= UI_MENU_VALUE; - auto callback = item->callback_; - auto *context = item->user_data_; - if (callback) - callback(this, context); - else - do_callback(); - redraw(); - return mvalue(); -} -const UiMenu_Item *UiMenu_::test_shortcut() -{ - for (auto &item : items_) - if (item.shortcut_ && item.shortcut_ == (Ui::ek | Ui::es) && item.active()) - return picked(&item); - return nullptr; -} -int UiMenu_::item_pathname(char *out, int size, const UiMenu_Item *item) const -{ - auto index = item - items_.data(); - if (index < 0 || index >= static_cast(paths_.size())) - return -1; - std::snprintf(out, static_cast(size), "%s", paths_[static_cast(index)].c_str()); - return 0; -} -void UiMenu_::show_popup(bool bar) -{ - juce::PopupMenu root; - if (literal_) - { - for (size_t i = 0; i < labels_.size(); ++i) - root.addItem(static_cast(i) + 1, juce::String::fromUTF8(labels_[i].c_str()), - items_[i].active()); - int result = root.showMenu(juce::PopupMenu::Options().withTargetComponent(this)); - if (result > 0) - picked(&items_[static_cast(result - 1)]); - return; - } - std::function build = - [&](juce::PopupMenu &menu, const std::string &prefix) - { - std::vector groups; - for (size_t i = 0; i < paths_.size(); ++i) - { - if (paths_[i].compare(0, prefix.size(), prefix)) - continue; - auto tail = paths_[i].substr(prefix.size()); - auto slash = tail.find('/'); - if (slash == std::string::npos) - menu.addItem(static_cast(i) + 1, juce::String::fromUTF8(labels_[i].c_str()), - items_[i].active(), (items_[i].flags & UI_MENU_VALUE) != 0); - else - { - auto name = tail.substr(0, slash); - if (std::find(groups.begin(), groups.end(), name) != groups.end()) - continue; - groups.push_back(name); - juce::PopupMenu sub; - build(sub, prefix + name + "/"); - menu.addSubMenu(name, sub); - } - } - }; - if (bar) - { - std::vector groups; - for (auto &path : paths_) - { - auto name = path.substr(0, path.find('/')); - if (std::find(groups.begin(), groups.end(), name) == groups.end()) - groups.push_back(name); - } - int selected = - std::clamp((Ui::ex - x()) / 90, 0, std::max(0, static_cast(groups.size()) - 1)); - if (!groups.empty()) - build(root, groups[static_cast(selected)] + "/"); - } - else - build(root, ""); - int result = root.showMenu(juce::PopupMenu::Options().withTargetComponent(this)); - if (result > 0) - picked(&items_[static_cast(result - 1)]); -} -int UiMenu_::handle(int e) -{ - if (e == UI_PUSH) - { - show_popup(); - return 1; - } - return UiWidget::handle(e); -} -void UiMenu_::draw() -{ - ui_color(color()); - ui_rectf(x(), y(), w(), h()); - ui_color(labelcolor()); - ui_font(0, labelsize()); - ui_draw(mvalue() ? mvalue()->label() : label(), x() + 6, y(), w() - 20, h(), UI_ALIGN_LEFT); - ui_draw("v", x() + w() - 16, y(), 14, h()); -} -void UiMenu_Bar::draw() -{ - ui_color(color()); - ui_rectf(x(), y(), w(), h()); - std::vector groups; - for (auto &p : paths_) - { - auto name = p.substr(0, p.find('/')); - if (std::find(groups.begin(), groups.end(), name) == groups.end()) - groups.push_back(name); - } - ui_color(labelcolor()); - ui_font(0, labelsize()); - for (size_t i = 0; i < groups.size(); ++i) - ui_draw(groups[i].c_str(), x() + static_cast(i) * 90 + 6, y(), 86, h(), UI_ALIGN_LEFT); -} -int UiMenu_Bar::handle(int e) -{ - if (e == UI_PUSH) - { - show_popup(true); - return 1; - } - return UiMenu_::handle(e); -} -UiInput_Choice::UiInput_Choice(int x, int y, int w, int h, const char *s) : UiGroup(x, y, w, h, s) -{ - input_ = new UiInput(x, y, w - 30, h); - menu_ = new UiMenu_Button(x + w - 30, y, 30, h, "v"); - menu_->callback( - [](UiWidget *w, void *p) - { - auto &c = *static_cast(p); - auto *m = static_cast(w); - if (m->text()) - c.input_->value(m->text()); - c.do_callback(); - }, - this); - end(); -} -UiHold_Browser::UiHold_Browser(int x, int y, int w, int h, const char *s) - : UiWidget(x, y, w, h, s), list_("", this) -{ - addAndMakeVisible(list_); - list_.setColour(juce::ListBox::backgroundColourId, colour(color())); - resized(); -} -void UiHold_Browser::paintListBoxItem(int row, juce::Graphics &g, int w, int h, bool selected) -{ - if (row < 0 || row >= static_cast(rows_.size())) - return; - g.fillAll(colour(selected ? selection_color() : color())); - g.setColour(colour(labelcolor())); - g.setFont(juce::FontOptions(static_cast(labelsize()))); - g.drawText(juce::String::fromUTF8(rows_[static_cast(row)].c_str()), 4, 0, w - 8, h, - juce::Justification::centredLeft); -} -UiScroll::UiScroll(int x, int y, int w, int h, const char *s) - : UiGroup(x, y, w, h, s), scrollbar(0, 0, 1, 1), hscrollbar(0, 0, 1, 1) -{ - remove(scrollbar); - remove(hscrollbar); - addAndMakeVisible(vertical_); - addAndMakeVisible(horizontal_); - vertical_.addListener(&listener_); - horizontal_.addListener(&listener_); -} -void UiScroll::Listener::scrollBarMoved(juce::ScrollBar *bar, double start) -{ - if (owner.scrolling_) - return; - owner.scroll_to(bar == &owner.horizontal_ ? static_cast(start) : owner.sx_, - bar == &owner.vertical_ ? static_cast(start) : owner.sy_); -} -void UiScroll::update_bars() -{ - int right = w() - 14, bottom = h() - 14; - for (int i = 0; i < children(); ++i) - { - right = std::max(right, child(i)->x() - x() + child(i)->w() + sx_); - bottom = std::max(bottom, child(i)->y() - y() + child(i)->h() + sy_); - } - scrolling_ = true; - vertical_.setBounds(w() - 12, 0, 12, h() - 12); - horizontal_.setBounds(0, h() - 12, w() - 12, 12); - vertical_.setRangeLimits(0, std::max(h() - 12, bottom)); - vertical_.setCurrentRange(sy_, std::max(1, h() - 12), juce::dontSendNotification); - horizontal_.setRangeLimits(0, std::max(w() - 12, right)); - horizontal_.setCurrentRange(sx_, std::max(1, w() - 12), juce::dontSendNotification); - if (getNumChildComponents() < 2 || - getChildComponent(getNumChildComponents() - 1) != &horizontal_ || - getChildComponent(getNumChildComponents() - 2) != &vertical_) - { - vertical_.toFront(false); - horizontal_.toFront(false); - } - scrolling_ = false; -} -void UiScroll::scroll_to(int x, int y) -{ - x = std::max(0, x); - y = std::max(0, y); - int dx = sx_ - x, dy = sy_ - y; - sx_ = x; - sy_ = y; - for (int i = 0; i < children(); ++i) - { - auto *c = child(i); - c->position(c->x() + dx, c->y() + dy); - } - update_bars(); - redraw(); -} -void UiScroll::mouseWheelMove(const juce::MouseEvent &, const juce::MouseWheelDetails &wheel) -{ - scroll_to(std::max(0, sx_ - static_cast(wheel.deltaX * 200)), - std::max(0, sy_ - static_cast(wheel.deltaY * 200))); -} -void UiTabs::update() -{ - auto *s = value(); - for (int i = 0; i < children(); ++i) - child(i)->setVisible(child(i) == s); - redraw(); -} -void UiTabs::draw() -{ - UiGroup::draw(); - auto *s = value(); - int left = x(); - const int tab_width = std::max(1, std::min(140, w() / std::max(1, children()))); - for (int i = 0; i < children(); ++i) - { - auto *c = child(i); - c->setVisible(c == s); - ui_color(c == s ? selection_color() : color()); - ui_rectf(left, y(), tab_width, 30); - ui_color(labelcolor()); - ui_font(0, 12); - ui_draw(c->label(), left + 6, y(), std::max(1, tab_width - 12), 30, UI_ALIGN_LEFT); - left += tab_width; - } -} -int UiTabs::handle(int e) -{ - if (e == UI_PUSH && Ui::ey < y() + 30) - { - const int tab_width = std::max(1, std::min(140, w() / std::max(1, children()))); - int n = (Ui::ex - x()) / tab_width; - if (n >= 0 && n < children()) - { - selected_ = child(n); - update(); - do_callback(); - return 1; - } - } - return UiGroup::handle(e); -} -int UiNative_File_Chooser::show() -{ - // Accept the retained labelled filters, including brace alternatives and - // callers which supply escaped tabs/newlines. JUCE expects semicolon globs. - auto filters = juce::String(filter_).replace("\\t", "\t").replace("\\n", "\n"); - juce::StringArray patterns; - for (auto line : juce::StringArray::fromLines(filters)) - { - auto pattern = line.containsChar('\t') ? line.fromFirstOccurrenceOf("\t", false, false) - : line; - if (pattern.containsChar('{') && pattern.containsChar('}')) - { - auto prefix = pattern.upToFirstOccurrenceOf("{", false, false); - auto alternatives = pattern.fromFirstOccurrenceOf("{", false, false) - .upToFirstOccurrenceOf("}", false, false); - auto suffix = pattern.fromFirstOccurrenceOf("}", false, false); - for (auto extension : juce::StringArray::fromTokens(alternatives, ",", "")) - patterns.add(prefix + extension + suffix); - } - else if (pattern.isNotEmpty()) - patterns.add(pattern); - } - auto wildcard = patterns.isEmpty() ? juce::String("*") : patterns.joinIntoString(";"); - juce::File start = directory_.empty() - ? juce::File::getSpecialLocation(juce::File::userHomeDirectory) - : juce::File(directory_); - if (!filename_.empty()) - start = start.getChildFile(filename_); - juce::FileChooser chooser(title_, start, wildcard, true); - bool ok = type_ == BROWSE_SAVE_FILE ? chooser.browseForFileToSave(true) - : chooser.browseForFileToOpen(); - if (!ok) - return 1; - filename_ = chooser.getResult().getFullPathName().toStdString(); - return 0; -} - -void UiWidget::mouseWheelMove(const juce::MouseEvent &e, const juce::MouseWheelDetails &wheel) -{ - if (auto *p = getParentComponent()) - p->mouseWheelMove(e.getEventRelativeTo(p), wheel); -} -void UiGroup::draw() -{ - draw_box(); - for (int i = 0; i < children(); ++i) - { - auto *c = child(i); - if (!c->visible() || (c->align() & UI_ALIGN_INSIDE) || !*c->label()) - continue; - ui_color(c->labelcolor()); - ui_font(c->labelfont(), c->labelsize()); - ui_draw(c->label(), x() + 4, c->y(), std::max(1, c->x() - x() - 12), c->h(), - UI_ALIGN_RIGHT); - } -} -void UiWindow::resize(int nx, int ny, int nw, int nh) -{ - UiGroup::resize(0, 0, std::max(min_w_, nw), std::max(min_h_, nh)); - setTopLeftPosition(nx, ny); -} -void UiWindow::resized() -{ - if (getWidth() != w() || getHeight() != h()) - resize(getX(), getY(), getWidth(), getHeight()); -} - -int &Ui::e_x = Ui::ex, &Ui::e_y = Ui::ey, &Ui::e_keysym = Ui::ek, &Ui::e_state = Ui::es, - &Ui::e_clicks = Ui::clicks; -UiWindow *Ui::first_window() -{ - for (auto i = windows.rbegin(); i != windows.rend(); ++i) - if ((*i)->shown()) - return *i; - return nullptr; -} -UiWindow *Ui::modal() -{ - return dynamic_cast(juce::Component::getCurrentlyModalComponent()); -} -void Ui::focus(UiWidget *w) -{ - if (w) - w->take_focus(); - else - juce::Component::unfocusAllComponents(); -} -int Ui::handle(int type, UiWindow *window) -{ - if (type == UI_KEYDOWN || type == UI_SHORTCUT) - return window->handle(type); - UiWidget *target = pressed; - if (type == UI_PUSH) - { - auto *c = window->getComponentAt(ex, ey); - while (c && !dynamic_cast(c)) - c = c->getParentComponent(); - target = dynamic_cast(c); - pressed = target; - } - if (!target) - return 0; - int result = target->handle(type); - if (type == UI_RELEASE) - pressed = nullptr; - return result; -} -void UiWidget::paintOverChildren(juce::Graphics &g) -{ - auto *previous = graphics; - graphics = &g; - g.saveState(); - g.setOrigin(-x(), -y()); - draw_overlay(); - g.restoreState(); - graphics = previous; -} - -void UiGroup::insert(UiWidget &w, UiWidget *before) -{ - add(w); - auto pos = std::find(children_.begin(), children_.end(), before); - if (pos != children_.end()) - { - children_.erase(std::remove(children_.begin(), children_.end(), &w), children_.end()); - pos = std::find(children_.begin(), children_.end(), before); - children_.insert(pos, &w); - w.toBehind(before); - } -} - -int UiGroup::handle(int event) -{ - if (event == UI_PUSH) - { - for (auto i = children_.rbegin(); i != children_.rend(); ++i) - { - auto *c = *i; - if (c->visible() && c->active() && Ui::event_inside(c) && c->handle(event)) - { - Ui::pressed = c; - return 1; - } - } - } - return event == UI_FOCUS || event == UI_UNFOCUS; -} - -namespace -{ -class WorkstationApplication : public juce::JUCEApplication -{ - public: - const juce::String getApplicationName() override { return "xtc JUCE"; } - const juce::String getApplicationVersion() override { return XTC_VERSION_STRING; } - void initialise(const juce::String &) override {} - void shutdown() override {} - void systemRequestedQuit() override - { - if (auto *w = Ui::first_window()) - w->do_callback(); - } -}; -juce::JUCEApplicationBase *create_workstation_application() { return new WorkstationApplication; } -} // namespace -struct UiInitialiser::State -{ - std::unique_ptr gui; - std::unique_ptr app; - std::unique_ptr tips; - State() - { - juce::JUCEApplicationBase::createInstance = create_workstation_application; - gui = std::make_unique(); - app.reset(create_workstation_application()); - tips = std::make_unique(nullptr, 700); - } - ~State() - { - tips.reset(); - app.reset(); - Ui::shutdown(); - gui.reset(); - juce::JUCEApplicationBase::createInstance = nullptr; - } -}; -UiInitialiser::UiInitialiser() : state_(std::make_unique()) {} -UiInitialiser::~UiInitialiser() = default; -void Ui::shutdown() -{ - timers.clear(); - if (juce::MessageManager::getInstanceWithoutCreating()) - juce::MessageManager::getInstance()->runDispatchLoopUntil(20); - font.reset(); -} - -int UiValue_Slider::handle(int event) -{ - if (event == UI_PUSH) - { - dragging_ = true; - drag_value_ = value(); - drag_x_ = Ui::event_x(); - drag_y_ = Ui::event_y(); - if (Ui::event_clicks()) - { - value(0); - drag_value_ = 0; - } - return 1; - } - if (event == UI_DRAG && dragging_) - { - double pixels = type() == UI_VERT_SLIDER ? drag_y_ - Ui::event_y() - : type() == UI_LINE_DIAL ? Ui::event_x() - drag_x_ + drag_y_ - Ui::event_y() - : Ui::event_x() - drag_x_; - double extent = type() == UI_VERT_SLIDER ? h() : type() == UI_LINE_DIAL ? 200 : w(); - value(drag_value_ + - pixels * (slider_.getMaximum() - slider_.getMinimum()) / std::max(1.0, extent)); - if (when() & UI_WHEN_CHANGED) - do_callback(); - return 1; - } - if (event == UI_RELEASE && dragging_) - { - dragging_ = false; - do_callback(); - return 1; - } - return UiWidget::handle(event); -} diff --git a/src/gui/JUCE/ui.hpp b/src/gui/JUCE/ui.hpp deleted file mode 100644 index 4f9393e..0000000 --- a/src/gui/JUCE/ui.hpp +++ /dev/null @@ -1,647 +0,0 @@ -#ifndef XTC_JUCE_UI_HPP -#define XTC_JUCE_UI_HPP -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// JUCE component adapter for the workstation's retained coordinates and commands. -// This layer has no FLTK headers, objects or runtime dependency. -using uchar = unsigned char; -using UiColor = unsigned int; -using UiBoxtype = int; -constexpr int UI_NO_BOX = 0, UI_FLAT_BOX = 1, UI_UP_BOX = 2, UI_DOWN_BOX = 3, UI_THIN_UP_BOX = 4, - UI_THIN_DOWN_BOX = 5, UI_ENGRAVED_BOX = 6, UI_BORDER_BOX = 7, UI_FREE_BOXTYPE = 16; -constexpr int UI_ALIGN_LEFT = 1, UI_ALIGN_RIGHT = 2, UI_ALIGN_TOP = 4, UI_ALIGN_INSIDE = 8, - UI_ALIGN_CLIP = 16, UI_ALIGN_WRAP = 32, UI_ALIGN_TOP_LEFT = 5; -constexpr int UI_HELVETICA = 0, UI_HELVETICA_BOLD = 1, UI_COURIER_BOLD = 2, UI_BOLD = 1, - UI_NO_LABEL = 0, UI_SOLID = 0, UI_LINE_DIAL = 1, UI_HOR_FILL_SLIDER = 2, - UI_VERT_SLIDER = 3; -constexpr UiColor UI_WHITE = 0xffffff, UI_RED = 0xff0000, UI_SELECTION_COLOR = 0x35746f; -constexpr int UI_CTRL = 1 << 20, UI_SHIFT = 1 << 21, UI_ALT = 1 << 22, UI_META = 1 << 23; -constexpr int UI_PUSH = 1, UI_DRAG = 2, UI_RELEASE = 3, UI_FOCUS = 4, UI_UNFOCUS = 5, - UI_KEYDOWN = 6, UI_SHORTCUT = 7; -constexpr int UI_LEFT_MOUSE = 1, UI_RIGHT_MOUSE = 3, UI_Button = 0x10000, UI_BUTTON1 = 1 << 24; -constexpr int UI_Escape = 27, UI_Enter = 13, UI_KP_Enter = 13, UI_BackSpace = 8, UI_Delete = 127, - UI_Left = 0x100, UI_Right = 0x101, UI_Up = 0x102, UI_Down = 0x103, UI_Home = 0x104, - UI_F = 0x200; -constexpr int UI_WHEN_CHANGED = 1, UI_WHEN_ENTER_KEY = 2, UI_WHEN_RELEASE = 4, UI_MENU_TOGGLE = 1, - UI_MENU_VALUE = 2, UI_MENU_INACTIVE = 4, UI_CURSOR_DEFAULT = 0, UI_CURSOR_WAIT = 1; -class UiWidget; -class UiGroup; -class UiWindow; -using UiCallback = void (*)(UiWidget *, void *); -class UiInitialiser -{ - struct State; - std::unique_ptr state_; - - public: - UiInitialiser(); - ~UiInitialiser(); -}; -struct Ui -{ - static int ex, ey, ek, es, eb, clicks; - static int &e_x, &e_y, &e_keysym, &e_state, &e_clicks; - static UiWindow *first_window(); - static UiWindow *modal(); - static void focus(UiWidget *); - static int handle(int, UiWindow *); - static UiWidget *pressed; - static int event_x() { return ex; } - static int event_y() { return ey; } - static int event_key() { return ek; } - static int event_state() { return es; } - static int event_state(int mask) { return es & mask; } - static int event_button() { return ek >= 0x10000 && ek < 0x10010 ? ek - 0x10000 : eb; } - static int event_clicks() { return clicks; } - static bool event_inside(const UiWidget *); - static UiWidget *focus(); - static UiWidget *pushed() { return pressed; } - static void delete_widget(UiWidget *); - static void add_timeout(double, void (*)(void *), void * = nullptr); - static void repeat_timeout(double s, void (*f)(void *), void *c) { add_timeout(s, f, c); } - static void remove_timeout(void (*)(void *), void *); - static void shutdown(); - static int run(); - static double wait(double seconds = .01); - static void check() { wait(.001); } - static void flush(); - static int screen_num(int, int, int, int) { return 0; } - static void screen_work_area(int &, int &, int &, int &, int); - static void background(int, int, int) {} - static void background2(int, int, int) {} - static void foreground(int, int, int) {} - static void set_color(int, int, int, int) {} - static void set_boxtype(int, void (*)(int, int, int, int, UiColor), int, int, int, int) {} - static void scrollbar_size(int) {} -}; -UiColor ui_rgb_color(unsigned char, unsigned char, unsigned char); -UiColor ui_darker(UiColor); -UiColor ui_color_average(UiColor, UiColor, float); -void ui_color(UiColor); -void ui_font(int, int); -void ui_rectf(int, int, int, int); -void ui_rect(int, int, int, int); -void ui_line(int, int, int, int); -void ui_polygon(int, int, int, int, int, int); -void ui_pie(int, int, int, int, double, double); -void ui_draw(const char *, int, int); -void ui_draw(const char *, int, int, int, int, int = 0); -void ui_push_clip(int, int, int, int); -void ui_pop_clip(); -int ui_clip_box(int, int, int, int, int &, int &, int &, int &); -void ui_line_style(int, int = 1); -void ui_alert(const char *, ...); -void ui_message(const char *, ...); -int ui_choice(const char *, const char *, const char *, const char *, ...); -const char *ui_input(const char *, const char * = "", ...); - -class UiWidget : public juce::Component, public juce::SettableTooltipClient -{ - friend class UiGroup; - int px_, py_, pw_, ph_; - std::string label_, tooltip_; - UiCallback callback_ = nullptr; - void *context_ = nullptr; - UiGroup *parent_ = nullptr; - UiColor colour_ = 0x22262c, label_colour_ = 0xe7ebf0, selection_ = 0x35746f; - int box_ = UI_NO_BOX, alignment_ = UI_ALIGN_INSIDE, label_size_ = 12, font_ = 0, - when_ = UI_WHEN_RELEASE, type_ = 0; - - protected: - friend struct Ui; - void paint(juce::Graphics &) override; - void paintOverChildren(juce::Graphics &) override; - void mouseDown(const juce::MouseEvent &) override; - void mouseDrag(const juce::MouseEvent &) override; - void mouseUp(const juce::MouseEvent &) override; - void mouseDoubleClick(const juce::MouseEvent &) override; - void mouseWheelMove(const juce::MouseEvent &, const juce::MouseWheelDetails &) override; - bool keyPressed(const juce::KeyPress &) override; - void focusGained(FocusChangeType) override { handle(UI_FOCUS); } - void focusLost(FocusChangeType) override { handle(UI_UNFOCUS); } - void event(const juce::MouseEvent &, int); - virtual void sync_control() {} - - public: - UiWidget(int, int, int, int, const char * = nullptr); - ~UiWidget() override; - virtual void draw() - { - draw_box(); - draw_label(); - } - virtual void draw_overlay() {} - virtual int handle(int) { return 0; } - int x() const { return px_; } - int y() const { return py_; } - int w() const { return pw_; } - int h() const { return ph_; } - virtual void resize(int, int, int, int); - void position(int x, int y) { resize(x, y, w(), h()); } - void size(int w, int h) { resize(x(), y(), w, h); } - UiGroup *parent() const { return parent_; } - UiWindow *window() const; - const char *label() const { return label_.c_str(); } - void label(const char *s) { copy_label(s); } - void copy_label(const char *s) - { - label_ = s ? s : ""; - setName(label_); - sync_control(); - redraw(); - } - void tooltip(const char *s) - { - tooltip_ = s ? s : ""; - setTooltip(tooltip_); - sync_control(); - } - void copy_tooltip(const char *s) { tooltip(s); } - void callback(UiCallback f, void *c = nullptr) - { - callback_ = f; - context_ = c; - } - void do_callback() - { - if (callback_) - callback_(this, context_); - } - void *user_data() const { return context_; } - void user_data(void *c) { context_ = c; } - virtual void show() { setVisible(true); } - virtual void hide() { setVisible(false); } - bool visible_r() const { return isShowing(); } - bool visible() const { return isVisible(); } - bool shown() const { return isVisible(); } - void activate() { setEnabled(true); } - void deactivate() { setEnabled(false); } - int active() const { return isEnabled(); } - int active_r() const { return isEnabled(); } - void redraw() { repaint(); } - virtual int take_focus() - { - if (!isShowing() || !isEnabled()) - return 0; - grabKeyboardFocus(); - return 1; - } - void color(UiColor c) - { - colour_ = c; - sync_control(); - redraw(); - } - UiColor color() const { return colour_; } - void labelcolor(UiColor c) - { - label_colour_ = c; - sync_control(); - redraw(); - } - UiColor labelcolor() const { return label_colour_; } - void selection_color(UiColor c) - { - selection_ = c; - sync_control(); - } - UiColor selection_color() const { return selection_; } - void box(int b) { box_ = b; } - int box() const { return box_; } - void slider(int) {} - void down_box(int) {} - void labeltype(int) {} - void align(int a) { alignment_ = a; } - int align() const { return alignment_; } - void labelsize(int s) - { - label_size_ = s; - sync_control(); - } - int labelsize() const { return label_size_; } - void labelfont(int f) { font_ = f; } - int labelfont() const { return font_; } - void textsize(int s) { labelsize(s); } - void textcolor(UiColor c) { labelcolor(c); } - void when(int w) { when_ = w; } - int when() const { return when_; } - virtual void type(int t) { type_ = t; } - int type() const { return type_; } - void draw_box(); - void draw_label(); -}; -class UiGroup : public UiWidget -{ - std::vector children_; - UiWidget *resizable_ = nullptr; - static UiGroup *current_; - - public: - UiGroup(int, int, int, int, const char * = nullptr); - ~UiGroup() override; - static UiGroup *current() { return current_; } - static void current(UiGroup *g) { current_ = g; } - void begin() { current_ = this; } - void end() { current_ = parent(); } - void add(UiWidget *); - void add(UiWidget &w) { add(&w); } - void insert(UiWidget &, UiWidget *); - void remove(UiWidget *); - void remove(UiWidget &w) { remove(&w); } - void clear(); - int children() const { return static_cast(children_.size()); } - UiWidget *child(int i) const { return children_.at(static_cast(i)); } - void resizable(UiWidget *w) { resizable_ = w; } - void resizable(UiWidget &w) { resizable_ = &w; } - UiWidget *resizable() const { return resizable_; } - int handle(int) override; - void init_sizes() {} - void draw_children() {} - void resize(int, int, int, int) override; - void draw() override; -}; -class UiWindow : public UiGroup -{ - bool modal_ = false; - int min_w_ = 100, min_h_ = 100; - - protected: - void userTriedToCloseWindow() override { do_callback(); } - - public: - UiWindow(int, int, const char * = nullptr); - UiWindow(int, int, int, int, const char * = nullptr); - ~UiWindow() override; - void show() override; - void show(int, char **) { show(); } - void hide() override; - void set_modal() { modal_ = true; } - void set_non_modal() { modal_ = false; } - void size_range(int w, int h, int = 0, int = 0) - { - min_w_ = w; - min_h_ = h; - } - int x() const { return getX(); } - int y() const { return getY(); } - void resize(int, int, int, int) override; - void resized() override; - void cursor(int c) - { - setMouseCursor(c == UI_CURSOR_WAIT ? juce::MouseCursor::WaitCursor - : juce::MouseCursor::NormalCursor); - } -}; -using UiDouble_Window = UiWindow; -class UiBox : public UiWidget -{ - public: - using UiWidget::UiWidget; -}; -class UiButton : public UiWidget -{ - juce::TextButton button_; - bool toggle_ = false; - - protected: - void sync_control() override; - - public: - UiButton(int, int, int, int, const char * = nullptr); - int value() const { return button_.getToggleState() ? 1 : 0; } - void value(int v) { button_.setToggleState(v != 0, juce::dontSendNotification); } - void set_toggle() - { - toggle_ = true; - button_.setClickingTogglesState(true); - } - void resized() override { button_.setBounds(getLocalBounds()); } - void draw() override { button_.setVisible(true); } - int handle(int e) override - { - if (e == UI_PUSH) - return 1; - if (e == UI_RELEASE) - { - if (toggle_) - value(!value()); - do_callback(); - return 1; - } - return UiWidget::handle(e); - } -}; -class UiCheck_Button : public UiButton -{ - public: - UiCheck_Button(int x, int y, int w, int h, const char *s = nullptr) : UiButton(x, y, w, h, s) - { - set_toggle(); - } -}; -using UiReturn_Button = UiButton; -class UiInput_ : public UiWidget -{ - mutable std::string text_; - - protected: - void sync_control() override; - - public: - juce::TextEditor editor; - UiInput_(int, int, int, int, const char * = nullptr); - void value(const char *); - const char *value() const; - void maximum_size(int n) { editor.setInputRestrictions(n); } - int take_focus() override - { - if (!isShowing() || !isEnabled()) - return 0; - editor.grabKeyboardFocus(); - return 1; - } - void resized() override { editor.setBounds(getLocalBounds()); } - void draw() override {} -}; -using UiInput = UiInput_; -class UiValue_Input : public UiWidget -{ - double value_ = 0, low_ = -1e12, high_ = 1e12, step_ = 0; - - protected: - void sync_control() override; - - public: - UiInput input; - UiValue_Input(int, int, int, int, const char * = nullptr); - void value(double); - double value() const { return value_; } - void bounds(double l, double h) - { - low_ = std::min(l, h); - high_ = std::max(l, h); - } - void range(double l, double h) { bounds(l, h); } - void step(double s) { step_ = s; } - void precision(int) {} - void soft(int) {} - void resized() override { input.resize(0, 0, w(), h()); } - void draw() override {} -}; -class UiValue_Slider : public UiWidget -{ - protected: - juce::Slider slider_; - double drag_value_ = 0; - int drag_x_ = 0, drag_y_ = 0; - bool dragging_ = false; - void sync_control() override; - - public: - UiValue_Slider(int, int, int, int, const char * = nullptr); - double value() const { return slider_.getValue(); } - void value(double v) { slider_.setValue(v, juce::dontSendNotification); } - void bounds(double, double); - void range(double l, double h) { bounds(l, h); } - int handle(int) override; - void step(double); - using UiWidget::type; - void type(int) override; - void slider(int) {} - void slider_size(double) {} - void resized() override { slider_.setBounds(getLocalBounds()); } - void draw() override {} -}; -class UiDial : public UiValue_Slider -{ - public: - using UiValue_Slider::UiValue_Slider; -}; -class UiProgress : public UiWidget -{ - double value_ = 0, low_ = 0, high_ = 1; - - public: - using UiWidget::UiWidget; - void minimum(double v) { low_ = v; } - void maximum(double v) { high_ = v; } - void value(double v) - { - value_ = v; - redraw(); - } - float value() const { return static_cast(value_); } - void draw() override; -}; -struct UiMenu_Item -{ - const char *text = nullptr; - int shortcut_ = 0; - UiCallback callback_ = nullptr; - void *user_data_ = nullptr; - int flags = 0; - int shortcut() const { return shortcut_; } - const char *label() const { return text; } - void set() { flags |= UI_MENU_VALUE; } - void clear() { flags &= ~UI_MENU_VALUE; } - bool active() const { return !(flags & UI_MENU_INACTIVE); } - bool submenu() const { return false; } - const UiMenu_Item *popup(int, int) const; -}; -class UiMenu_ : public UiWidget -{ - protected: - std::vector items_{UiMenu_Item{}}; - std::vector paths_, labels_; - int selected_ = -1; - bool literal_ = false; - void repair(); - void show_popup(bool bar = false); - - public: - using UiWidget::UiWidget; - int add(const char *, int = 0, UiCallback = nullptr, void * = nullptr, int = 0); - void replace(int, const char *); - void clear(); - void copy(const UiMenu_Item *); - int find_index(const char *s) const - { - auto *p = find_item(s); - return p ? static_cast(p - items_.data()) : -1; - } - void mode(int i, int flags) - { - if (i >= 0 && i < static_cast(labels_.size())) - items_[static_cast(i)].flags = flags; - } - int size() const { return static_cast(items_.size()); } - const UiMenu_Item *menu() const; - const UiMenu_Item *mvalue() const; - const UiMenu_Item *find_item(const char *) const; - const UiMenu_Item *picked(const UiMenu_Item *); - const UiMenu_Item *test_shortcut(); - int item_pathname(char *, int, const UiMenu_Item *) const; - void value(int n) - { - selected_ = n; - sync_control(); - redraw(); - } - int value() const { return selected_; } - const char *text() const { return mvalue() ? mvalue()->label() : nullptr; } - const char *text(int n) const - { - return n >= 0 && n < static_cast(items_.size()) - ? items_[static_cast(n)].label() - : nullptr; - } - int handle(int) override; - void draw() override; -}; -class UiMenu_Bar : public UiMenu_ -{ - public: - using UiMenu_::UiMenu_; - void draw() override; - int handle(int) override; -}; -class UiMenu_Button : public UiMenu_ -{ - public: - using UiMenu_::UiMenu_; -}; -class UiChoice : public UiMenu_ -{ - public: - using UiMenu_::UiMenu_; -}; -class UiInput_Choice : public UiGroup -{ - UiInput *input_; - UiMenu_Button *menu_; - - public: - UiInput_Choice(int, int, int, int, const char * = nullptr); - UiInput *input() { return input_; } - UiMenu_Button *menubutton() { return menu_; } - const char *value() const { return input_->value(); } - void value(const char *v) { input_->value(v); } -}; -class UiHold_Browser : public UiWidget, private juce::ListBoxModel -{ - juce::ListBox list_; - std::vector rows_; - int getNumRows() override { return static_cast(rows_.size()); } - void paintListBoxItem(int, juce::Graphics &, int, int, bool) override; - void selectedRowsChanged(int) override { do_callback(); } - void listBoxItemDoubleClicked(int, const juce::MouseEvent &) override - { - Ui::clicks = 1; - do_callback(); - Ui::clicks = 0; - } - - public: - UiHold_Browser(int, int, int, int, const char * = nullptr); - ~UiHold_Browser() override { list_.setModel(nullptr); } - void add(const char *s) - { - rows_.emplace_back(s ? s : ""); - list_.updateContent(); - } - void clear() - { - rows_.clear(); - list_.updateContent(); - } - int value() const { return list_.getSelectedRow() + 1; } - void select(int n) { list_.selectRow(n - 1); } - int size() const { return static_cast(rows_.size()); } - void format_char(int) {} - void textsize(int) {} - void resized() override { list_.setBounds(getLocalBounds()); } - void draw() override {} -}; -class UiScroll : public UiGroup -{ - juce::ScrollBar vertical_{true}, horizontal_{false}; - int sx_ = 0, sy_ = 0; - bool scrolling_ = false; - struct Listener : juce::ScrollBar::Listener - { - UiScroll &owner; - explicit Listener(UiScroll &o) : owner(o) {} - void scrollBarMoved(juce::ScrollBar *, double) override; - } listener_{*this}; - void update_bars(); - - public: - enum - { - HORIZONTAL_ALWAYS = 1 - }; - UiWidget scrollbar, hscrollbar; - UiScroll(int, int, int, int, const char * = nullptr); - void scroll_to(int, int); - int xposition() const { return sx_; } - int yposition() const { return sy_; } - void draw() override - { - UiGroup::draw(); - update_bars(); - } - void mouseWheelMove(const juce::MouseEvent &, const juce::MouseWheelDetails &) override; -}; -class UiTabs : public UiGroup -{ - UiWidget *selected_ = nullptr; - void update(); - - public: - using UiGroup::UiGroup; - void value(UiWidget *w) - { - selected_ = w; - update(); - } - UiWidget *value() - { - if (!selected_ && children()) - selected_ = child(0); - return selected_; - } - void draw() override; - int handle(int) override; -}; -class UiNative_File_Chooser -{ - std::string title_, filter_, filename_, directory_; - int type_ = 0; - - public: - enum - { - BROWSE_FILE = 0, - BROWSE_SAVE_FILE = 1, - SAVEAS_CONFIRM = 2 - }; - void title(const char *s) { title_ = s ? s : ""; } - void type(int n) { type_ = n; } - void filter(const char *s) { filter_ = s ? s : ""; } - void directory(const char *s) { directory_ = s ? s : ""; } - void preset_file(const char *s) { filename_ = s ? s : ""; } - void options(int) {} - int show(); - const char *filename() const { return filename_.empty() ? nullptr : filename_.c_str(); } - const char *errmsg() const { return "File chooser failed"; } -}; -#endif diff --git a/src/gui/JUCE/velocity_lane.hpp b/src/gui/JUCE/velocity_lane.hpp deleted file mode 100644 index aec5c9d..0000000 --- a/src/gui/JUCE/velocity_lane.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef XTC_JUCE_GUI_VELOCITY_LANE_HPP -#define XTC_JUCE_GUI_VELOCITY_LANE_HPP -#include "piano_roll.hpp" -#include "theme.hpp" -#include -#include - -/* Fixed-height controller lane, separate from the vertically scrolled keys. - * X coordinates follow the piano widget, including its horizontal scroll. */ -class VelocityLane : public UiWidget { - PianoRollWidget &piano_; - uint64_t editing_ = UINT64_MAX; - unsigned int preview_ = 100; - const xtc_project *editing_project_ = nullptr; - uint64_t editing_clip_ = UINT64_MAX; - unsigned int value(int py) const { - return static_cast(std::clamp((y()+h()-8-py)*127/std::max(1,h()-34),1,127)); - } - public: - VelocityLane(int x, int y, int w, int h, PianoRollWidget &piano) - : UiWidget(x,y,w,h,"Note velocity"), piano_(piano) {} - void draw() override { - ui_push_clip(x(),y(),w(),h()); - ui_color(xtc_ui::background()); ui_rectf(x(),y(),w(),h()); - ui_color(xtc_ui::border()); ui_line(x(),y(),x()+w(),y()); - ui_font(UI_HELVETICA,11); ui_color(xtc_ui::muted()); - ui_draw("VELOCITY 1..127 / Select a note, then drag its bar or use the velocity slider",x()+8,y()+17); - size_t count = 0; xtc_project_note_count(piano_.project(),&count); - for (int pass = 0; pass < 2; ++pass) for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; xtc_project_get_note(piano_.project(),i,¬e); - if (!piano_.note_visible(note) || (note.id == piano_.selected_note()) != (pass == 1)) continue; - int px = piano_.note_x(note.start_tick); - unsigned int velocity = editing_ == note.id ? preview_ : note.velocity; - int top = y()+h()-8-static_cast(velocity)*(h()-34)/127; - ui_color(note.id == piano_.selected_note() ? xtc_ui::accent() : ui_rgb_color(95,137,172)); - ui_rectf(px-2,top,4,y()+h()-8-top); ui_rectf(px-4,top-2,8,4); - } - ui_pop_clip(); - } - int handle(int event) override { - if (event == UI_PUSH && Ui::event_button() == UI_LEFT_MOUSE && Ui::event_y() >= y()+24) { - take_focus(); editing_ = UINT64_MAX; - size_t count = 0; int distance = 9; - xtc_project_note_count(piano_.project(),&count); - for (size_t i = 0; i < count; ++i) { - xtc_note_info note{}; xtc_project_get_note(piano_.project(),i,¬e); - if (!piano_.note_visible(note)) continue; - int d = std::abs(Ui::event_x()-piano_.note_x(note.start_tick)); - if (d < distance || (d == distance && note.id == piano_.selected_note())) { - distance = d; editing_ = note.id; - } - } - if (editing_ != UINT64_MAX) { - editing_project_ = piano_.project(); editing_clip_ = piano_.clip(); - piano_.select_note(editing_); preview_ = value(Ui::event_y()); - } - redraw(); piano_.redraw(); return 1; - } - if (event == UI_DRAG && editing_ != UINT64_MAX) { - preview_ = value(Ui::event_y()); redraw(); return 1; - } - if (event == UI_RELEASE && editing_ != UINT64_MAX) { - uint64_t id = editing_; editing_ = UINT64_MAX; - if (editing_project_ == piano_.project() && editing_clip_ == piano_.clip()) - (void)piano_.set_velocity(id,value(Ui::event_y())); - redraw(); return 1; - } - if (event == UI_KEYDOWN && Ui::event_key() == UI_Escape) { editing_ = UINT64_MAX; redraw(); return 1; } - if (event == UI_FOCUS || event == UI_UNFOCUS) return 1; - return UiWidget::handle(event); - } -}; -#endif diff --git a/tests/integration/README.md b/tests/integration/README.md index 064c8ea..80646e3 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -254,29 +254,3 @@ It requires an existing JACK/PipeWire server, changes no user preferences and connects no physical outputs. Enable `XTC_ENABLE_LIVE_TESTS` to include `playback` in CTest. The fake-backend `gui-midi-workflows` test also checks absent input/output/both, returning devices and genuine backend failures. - -The optional JUCE workstation builds the shared GUI regression source against -`src/gui/JUCE` as `test_juce_gui`; `juce-gui-*` CTests cover the same retained -commands, fault injection, dialogs, hierarchy and gestures without FLTK. Build -all test fixtures before running them. The real-backend `test_juce_playback` -delivers JUCE MouseEvents to native transport buttons, resumes with a JUCE -KeyPress, and checks finite nonzero stereo samples through a private JACK probe -with missing saved MIDI devices. It writes no user preferences and connects no -physical ports. Run it from the repository root under Xvfb after building: - -```sh -cmake --build build/juce-gui --parallel 4 -ctest --test-dir build/juce-gui -R juce-gui --output-on-failure -j1 -xvfb-run -a ./build/juce-gui/test_juce_playback -``` - -`XTC_ENABLE_LIVE_TESTS` registers the latter as `juce-playback`. An optional -`XTC_JUCE_SCREENSHOT` absolute output path captures the actual workstation -component during that live regression. - -Linux qualification used JUCE 8.0.15: all 12 JUCE GUI workflows passed (with -focused reruns after native focus/blur fixes), alongside all 57 existing -FLTK/core tests. Native Play/Stop/Space, expanded-track rendering and finite -stereo output passed with deliberately unavailable saved MIDI devices. Bare -Xvfb can emit JUCE X11 BadAtom diagnostics without a window manager; these do -not indicate a failed assertion in the workflow checks. diff --git a/tests/integration/test_juce_playback.cpp b/tests/integration/test_juce_playback.cpp deleted file mode 100644 index 7cc693f..0000000 --- a/tests/integration/test_juce_playback.cpp +++ /dev/null @@ -1,240 +0,0 @@ -/* Native JUCE GUI playback regression. Connects only to a private software probe; - * neither user preferences nor physical audio/MIDI routes are changed. */ -#include "../../src/gui/JUCE/main_window.hpp" -#include "../../src/gui/JUCE/sequencer_track_widget.hpp" -#include "../../src/gui/JUCE/sequencer_widget.hpp" -#include "../../src/gui/JUCE/ui.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define REQUIRE(c) \ - do \ - { \ - if (!(c)) \ - throw std::runtime_error(#c); \ - } while (0) -namespace -{ -struct Probe -{ - jack_client_t *client = nullptr; - jack_port_t *ports[2]{}; - std::atomic heard[2]{}; - std::atomic invalid{false}; - ~Probe() - { - if (client) - jack_client_close(client); - } - static int process(jack_nframes_t frames, void *data) - { - auto &probe = *static_cast(data); - for (int channel = 0; channel < 2; ++channel) - { - auto *samples = - static_cast(jack_port_get_buffer(probe.ports[channel], frames)); - for (jack_nframes_t i = 0; i < frames; ++i) - { - if (!std::isfinite(samples[i])) - probe.invalid.store(true, std::memory_order_relaxed); - if (std::fabs(samples[i]) > .001f) - probe.heard[channel].store(true, std::memory_order_relaxed); - } - } - return 0; - } -}; -struct TemporaryDirectory -{ - std::string path; - TemporaryDirectory() - { - char name[] = "/tmp/xtc-playback-XXXXXX"; - REQUIRE(mkdtemp(name)); - path = name; - } - ~TemporaryDirectory() { std::filesystem::remove_all(path); } -}; -UiWidget *find(UiGroup &group, const char *text, bool exact = true) -{ - for (int i = 0; i < group.children(); ++i) - { - auto *child = group.child(i); - if (child->label() && (exact ? !std::strcmp(child->label(), text) - : std::strstr(child->label(), text) != nullptr)) - return child; - if (auto *nested = dynamic_cast(child)) - if (auto *result = find(*nested, text, exact)) - return result; - } - return nullptr; -} -SequencerWidget *timeline(UiGroup &group) -{ - for (int i = 0; i < group.children(); ++i) - { - if (auto *s = dynamic_cast(group.child(i))) - return s; - if (auto *nested = dynamic_cast(group.child(i))) - if (auto *s = timeline(*nested)) - return s; - } - return nullptr; -} -void pump(int milliseconds) -{ - auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds); - while (std::chrono::steady_clock::now() < end) - Ui::wait(.01); -} -void mouse_click(juce::Component *target, juce::Point point) -{ - REQUIRE(target); - auto now = juce::Time::getCurrentTime(); - juce::MouseEvent down(juce::Desktop::getInstance().getMainMouseSource(), point, - juce::ModifierKeys(juce::ModifierKeys::leftButtonModifier), 1, 0, 0, 0, 0, - target, target, now, point, now, 1, false); - target->mouseDown(down); - juce::MouseEvent up(juce::Desktop::getInstance().getMainMouseSource(), point, - juce::ModifierKeys(), 1, 0, 0, 0, 0, target, target, now, point, now, 1, - false); - target->mouseUp(up); -} -void click(MainWindow &, UiWidget *button) -{ - REQUIRE(button && button->active()); - // Deliver JUCE MouseEvents to the native TextButton child. - juce::Component *target = nullptr; - for (int i = 0; i < button->getNumChildComponents(); ++i) - if (dynamic_cast(button->getChildComponent(i))) - target = button->getChildComponent(i); - REQUIRE(target); - mouse_click(target, target->getLocalBounds().toFloat().getCentre()); -} - -} // namespace -int main() -{ - UiInitialiser initialise; - try - { - TemporaryDirectory directory; - xtc_settings settings{}; - xtc_settings_defaults(&settings); - std::strcpy(settings.midi_input_port, "xtc-missing-production-keyboard:out"); - std::strcpy(settings.midi_output_port, "xtc-missing-production-synth:in"); - std::snprintf(settings.client_name, sizeof(settings.client_name), "xtc-smoke-%ld", - static_cast(getpid())); - settings.main_x = 0; - settings.main_y = 0; - settings.width = 1280; - settings.height = 800; - settings.transport_detached = settings.sequencer_detached = settings.graph_detached = - settings.mixer_detached = 0; - settings.loop_enabled = 0; - settings.stop_after_tail = 0; - std::string config = directory.path + "/preferences.conf"; - REQUIRE(xtc_settings_save(&settings, config.c_str()) == XTC_OK); - Probe probe; - jack_status_t status{}; - probe.client = jack_client_open("xtc-playback-probe", JackNoStartServer, &status); - REQUIRE(probe.client); - probe.ports[0] = - jack_port_register(probe.client, "left", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); - probe.ports[1] = - jack_port_register(probe.client, "right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); - REQUIRE(probe.ports[0] && probe.ports[1]); - REQUIRE(jack_set_process_callback(probe.client, Probe::process, &probe) == 0 && - jack_activate(probe.client) == 0); - MainWindow window(false, config.c_str()); - window.show(); - pump(100); - REQUIRE(window.open_project("examples/projects/playback-test.xtc") == XTC_OK); - auto *s = timeline(window); - REQUIRE(s); - click(window, find(window, "Play")); - pump(300); - if (!find(window, "running", false)) - { - if (auto *error = find(window, "Start failed", false)) - std::fprintf(stderr, "Observed: %s\n", error->label()); - } - REQUIRE(find(window, "running", false)); - REQUIRE(find(window, "MIDI device unavailable", false)); - for (int channel = 0; channel < 2; ++channel) - { - std::string source = - std::string(settings.client_name) + (channel ? ":output_right" : ":output"); - int code = -1; - for (int attempt = 0; attempt < 50; ++attempt) - { - code = jack_connect(probe.client, source.c_str(), - jack_port_name(probe.ports[channel])); - if (code == 0 || code == EEXIST) - break; - pump(10); - } - REQUIRE(code == 0 || code == EEXIST); - } - auto start = s->position(); - pump(700); - REQUIRE(s->position() > start); - REQUIRE(probe.heard[0].load() && probe.heard[1].load() && !probe.invalid.load()); - click(window, find(window, "Stop")); - pump(300); - auto paused = s->position(); - pump(200); - REQUIRE(s->position() == paused); - // Space resumes after a sequencer child has taken focus. - s->take_focus(); - REQUIRE(static_cast(s)->keyPressed(juce::KeyPress(' '))); - pump(300); - REQUIRE(s->position() > paused); - // A child at local (0,0) must paint consistently when composed into - // the full workstation; this catches retained-coordinate origin errors. - xtc_track_info track{}; - REQUIRE(xtc_project_get_track(s->project(), 0, &track) == XTC_OK); - auto *header = s->track_widget(track.id)->header(); - auto *menu = window.child(0); - REQUIRE(header->isShowing() && menu->isShowing()); - mouse_click(header, {14, 12}); - pump(60); - REQUIRE(s->track_expanded(track.id)); - auto menu_image = menu->createComponentSnapshot(menu->getLocalBounds()); - auto header_image = header->createComponentSnapshot(header->getLocalBounds()); - auto image = window.createComponentSnapshot(window.getLocalBounds()); - REQUIRE(image.getPixelAt(2, 2) == menu_image.getPixelAt(2, 2)); - REQUIRE(image.getPixelAt(header->x() + 5, header->y() + 5) == - header_image.getPixelAt(5, 5)); - if (const char *capture = std::getenv("XTC_JUCE_SCREENSHOT")) - { - juce::FileOutputStream stream{juce::File(capture)}; - REQUIRE(stream.openedOk()); - juce::PNGImageFormat png; - REQUIRE(png.writeImageToStream(image, stream)); - } - window.do_callback(); - REQUIRE(!window.shown()); - xtc_settings saved{}; - REQUIRE(xtc_settings_load(&saved, config.c_str()) == XTC_OK); - REQUIRE(!std::strcmp(saved.midi_input_port, settings.midi_input_port) && - !std::strcmp(saved.midi_output_port, settings.midi_output_port)); - std::puts("Live Play/Stop/Space and stereo playback passed with unavailable saved MIDI " - "devices; preferences retained."); - return 0; - } - catch (const std::exception &error) - { - std::fprintf(stderr, "Playback regression failed: %s\n", error.what()); - return 1; - } -} diff --git a/tools/install.py b/tools/install.py index 37a2924..f6aabde 100644 --- a/tools/install.py +++ b/tools/install.py @@ -11,13 +11,12 @@ parser = argparse.ArgumentParser(description=__doc__) arguments(parser) parser.add_argument('--build-dir', type=Path, required=True) parser.add_argument('--gui', action='store_true') -parser.add_argument('--juce-gui', action='store_true') args = parser.parse_args() source = Path(__file__).resolve().parent.parent entries = [] for destination, original in inventory(source, layout(args)): name = destination.name - if (name == 'xtc-juce' and not args.juce_gui) or name == 'BINARY_MANIFEST.json' or (not args.gui and name in ('xtc', 'xtc.desktop')): + if name == 'BINARY_MANIFEST.json' or (not args.gui and name in ('xtc', 'xtc.desktop')): continue if original is None: original = args.build_dir / name @@ -33,7 +32,7 @@ for destination, original in entries: try: with os.fdopen(fd, 'wb') as output, original.open('rb') as input_file: shutil.copyfileobj(input_file, output) - os.fchmod(output.fileno(), 0o755 if destination.name in ('xtc', 'xtc-check', 'xtc-juce') else 0o644) + os.fchmod(output.fileno(), 0o755 if destination.name in ('xtc', 'xtc-check') else 0o644) os.replace(pending, destination) finally: Path(pending).unlink(missing_ok=True) diff --git a/tools/install_layout.py b/tools/install_layout.py index 7d11c4d..ff1ce27 100644 --- a/tools/install_layout.py +++ b/tools/install_layout.py @@ -30,7 +30,6 @@ def directory(value): def inventory(source, paths): entries = [('libdir', 'libxtc.a', None), ('pkgconfigdir', 'xtc.pc', None), ('bindir', 'xtc-check', None), ('bindir', 'xtc', None), - ('bindir', 'xtc-juce', None), ('desktopdir', 'xtc.desktop', source / 'packaging/xtc.desktop'), ('licensedir', 'LICENSE', source / 'LICENSE'), ('licensedir', 'CLAP-LICENSE', source / 'third_party/clap/LICENSE'),