From ae8556d83f1f4d3a226fa39fa7076e619fecee00 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 00:06:56 +0100 Subject: [PATCH] feat: dock editor panels and add resizable pinned track headers --- docs/BUILTIN_DEVICES.md | 6 +- docs/PREFERENCES.md | 6 ++ docs/ROADMAP.md | 56 +++++++++----- docs/WORKSPACE.md | 43 ++++++++++- include/xtc/xtc_plugin.h | 3 +- include/xtc/xtc_settings.h | 4 +- src/gui/main_window.cpp | 135 ++++++++++++++++++++++++++++----- src/gui/main_window.hpp | 12 +++ src/gui/sequencer_widget.cpp | 90 ++++++++++++++-------- src/gui/sequencer_widget.hpp | 4 + src/plugin/builtin.c | 4 +- src/settings/settings.c | 11 +++ tests/integration/test_gui.cpp | 59 +++++++++++--- 13 files changed, 346 insertions(+), 87 deletions(-) diff --git a/docs/BUILTIN_DEVICES.md b/docs/BUILTIN_DEVICES.md index 2ad0164..dacea20 100644 --- a/docs/BUILTIN_DEVICES.md +++ b/docs/BUILTIN_DEVICES.md @@ -23,9 +23,9 @@ Changes use the existing stopped-engine preparation and undo workflow. These are basic mono processors; stereo plans use independent instances. Reverb is deliberately simple and has no room model or stereo diffusion. Delay time -changes can click; there is no crossfade or tempo sync. Transport seeks retain -existing effect history, like LADSPA. Effect tails require continued rendering; -playback/export does not automatically extend the song for tails. +changes can click; these original devices have no crossfade or tempo sync. +Internal effect history resets on transport discontinuities. Phase 44 below +documents configurable export tails and optional playback stopping after tails. Project format 13 stores internal devices as format 4 with reserved module path `/xtc/internal`, stable numeric identity and index (0 keys, 1 bass, 2 delay, diff --git a/docs/PREFERENCES.md b/docs/PREFERENCES.md index a07748e..54dd689 100644 --- a/docs/PREFERENCES.md +++ b/docs/PREFERENCES.md @@ -76,3 +76,9 @@ Phase 45 adds `[interface] transport_detached`, `mixer_detached`, and each panel's `_x`, `_y`, `_width`, `_height` rectangle. Use Preferences / Apply after moving panels to save the captured layout. Restoration clamps rectangles to the current display; the floating transport wraps at narrow widths. + +Phase 46 extends `[interface]` with `sequencer_detached`, `graph_detached`, +`piano_detached`, editor window geometry and `track_header_width` (180..360). +Piano retains `piano_width`/`piano_height` and adds `piano_x`/`piano_y`. +Reset workspace layout changes the session; Preferences / Apply persists it. +Only original-tab docking and floating windows are supported. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 09f1bbb..84d753e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1065,9 +1065,9 @@ track in other views. Command search lists active menu actions; it is not a plugin scanner or a key-binding editor. See WORKSPACE.md. ### Future usability work -Dockable editor panels, arrangement multiselection, independent track inserts, -resizable pinned track headers and customizable shortcuts remain future work. -They require separate model/layout designs and acceptance tests. +Arrangement multiselection, independent track inserts and customizable shortcuts +remain future work. Dockable editor panels and resizable pinned track headers +are implemented in Phase 46. --- @@ -1099,10 +1099,9 @@ keys/delay project round trip. Shared GUI tests cover instrument selection, history and plugin editing, but do not constitute a dedicated end-to-end matrix for all four internal devices. Additional qualification is tracked in Phase 40. -Missing extensions: editable instrument parameters/presets, pitch bend, sample -instruments, click-free/tempo-synced delay, stereo reverb/diffusion, explicit seek -history policy and automatic tail rendering. These are planned in Phases 41–44; -they are not part of the checked initial subset. See +The initial subset lacked editable presets, pitch bend, sample instruments, +smooth/tempo-synced delay, stereo reverb, seek history policy and automatic tails. +These audit gaps are addressed by Phases 41–44 below. See [BUILTIN_DEVICES.md](BUILTIN_DEVICES.md) for controls and explicit limitations. --- @@ -1128,10 +1127,9 @@ detach/dock, close-to-dock, shared BPM/loop state, floating playback shortcuts, main/floating resizing and failed/successful shutdown. Floating screenshots are listed in [screenshots/README.md](../screenshots/README.md). -Layout and window positions are session-only. The floating transport requires -1220 pixels of width; mixer minimum size is 600 × 420. Other editors cannot be -detached, and general dock layouts are absent. Persistence, compact transport -and additional panels are tracked in Phases 45–46. See +The original subset used session-only positions, a 1220-pixel transport and +only two detachable panels. Phases 45–46 add persisted geometry, a compact +transport and editor panels. Mixer minimum remains 600 × 420. See [WORKSPACE.md](WORKSPACE.md) for usage and limits. ### Audit validation (Phases 38–39) @@ -1282,17 +1280,35 @@ The full-window minimum is unchanged. See WORKSPACE.md and PREFERENCES.md. Extend Phase 39's two-panel arrangement to a general editor workspace. ### Tasks and acceptance criteria -- [ ] Define supported dock locations and layout ownership for sequencer, piano roll and modular graph panels. -- [ ] Detach/dock each supported editor without duplicating project, selection or history state. -- [ ] Support resizable pinned track headers and preserve editing coordinates, scrolling and focus after reparenting. -- [ ] Integrate with Phase 45 layout persistence and provide a safe reset-layout action. -- [ ] Verify minimum/large layouts, cross-panel editing, repeated reparenting and shutdown/error paths. +- [x] Define supported dock locations and layout ownership for sequencer, piano roll and modular graph panels. +- [x] Detach/dock each supported editor without duplicating project, selection or history state. +- [x] Support resizable pinned track headers and preserve editing coordinates, scrolling and focus after reparenting. +- [x] Integrate with Phase 45 layout persistence and provide a safe reset-layout action. +- [x] Verify minimum/large layouts, cross-panel editing, repeated reparenting and shutdown/error paths. ### Implementation status -Planned. Phase 39 only detaches transport and mixer; broader docking and -resizable pinned headers from Phase 37 remain outstanding. These follow-up -phases cover gaps found in Phases 38–39 and their documented limitations; -unrelated outstanding features in earlier phases retain their existing status. +Implemented: complete Sequencer/Graph panels and Piano Roll reparent between +floating windows and their main tabs, with shared model/history/selection and +preserved scroll positions. Validated preferences persist layouts; reset docks +panels and restores default rectangles. Arrangement headers/selectors stay pinned +and resize from 180 to 360 pixels. Supported docking is explicitly limited to +original tabs; arbitrary split layouts and drag docking are future extensions. +Core/settings, GUI lifecycle/editing/restart and integrated checks are recorded +below. See WORKSPACE.md and PREFERENCES.md. + +--- + +### Integrated validation for Phases 40–46 + +Each phase was built and tested before its separate implementation commit. +Core/reliability tests cover devices, migration, persistence, DSP partitions, +transport history and export. GUI tests cover editing, shared state and panel +lifecycle; CLAP/VST2/VST3/LADSPA fixture and live software JACK suites retain the +existing workflows. Sanitizers cover built-in DSP/export and GUI lifetimes; +static analysis and staged installation check the final integrated changes. +Human listening, arbitrary third-party plugins and physical hardware remain +unqualified. Listening WAV generation and manual procedures are documented in +BUILTIN_DEVICES.md. Application project format is now 15, reading versions 1–14. --- diff --git a/docs/WORKSPACE.md b/docs/WORKSPACE.md index 4f8ae9b..de4ddef 100644 --- a/docs/WORKSPACE.md +++ b/docs/WORKSPACE.md @@ -169,9 +169,10 @@ Space and menu playback shortcuts work from either floating panel, with normal numeric/text editing protection. Closing the main window closes all panels only after the existing unsaved/recording/backend shutdown checks succeed. -Window detachment and positions are session-only; startup uses the docked layout. -The floating transport is a wide toolbar (minimum 1220 pixels); the floating -mixer resizes down to 600 × 420 and retains horizontal track scrolling. +The initial Phase 39 layout used session-only positions and a transport minimum +width of 1220 pixels. Phase 45 below adds saved layouts and transport wrapping +down to 400 pixels. The floating mixer resizes down to 600 × 420 and retains +horizontal track scrolling. Verified with `make BUILD_DIR=build/phase39 gui test-gui test-gui-instruments test-gui-jack` (one line), plus the GUI suite under AddressSanitizer/UBSan. @@ -193,3 +194,39 @@ the wide toolbar and arrangement geometry. Mixer minimum remains 600 × 420. Existing close-to-dock, Dock all and shortcuts retain their shared widget state. Tests cover narrow control bounds, save/restart, off-screen restoration, invalid geometry, repeated docking/resizing and shared transport state. + +## Phase 46 editor panels and pinned track headers + +**View → Detach sequencer** and **Detach modular graph** float each complete +editor, including its toolbar and scroll area. Its main tab offers Show and Dock +buttons. Closing the floating window docks it; View shortcuts also raise floating +editors. Supported dock locations are their original main-window tabs; arbitrary +split regions, tab reordering and drag-to-dock are not implemented. Sequencer +minimum floating size is 1100 × 420; Graph is 1000 × 420. + +The Piano roll now also has a main-window tab. Select a MIDI clip and open it as +before. **View → Float piano roll** switches its existing widgets between the tab +and a floating window; closing the floating window docks it. The tab's Open +selected MIDI clip button opens the current selection. Default floating behavior +is retained for older preferences. A missing/invalid MIDI selection is reported. +All panels share project/history, preserve editor selections and scroll offsets, +and retain playback shortcuts and text-input protection. + +**View → Dock all panels** includes all editors. **Reset workspace layout** docks +all panels, restores default floating rectangles and resets header width. Use +Preferences / Apply to persist either result. Editor dock state, floating window +rectangles and header width use the same validated settings/restore path as the +transport/mixer. Piano content opens lazily for the selected MIDI clip; startup +does not invent a clip or save a project-specific selection in application settings. + +Arrangement track headers and instrument selectors now stay pinned during +horizontal scrolling. Drag the divider within the header's final five pixels to +resize from 180 to 360 pixels; Escape cancels the drag. Clip timing coordinates, +snap and horizontal scrolling use the resized origin. Header layout is a UI +preference, not an undoable project edit. Vertical scrolling remains shared with +the track rows. Clips covered by the pinned header cannot be selected through it. + +GUI regressions cover repeated detach/dock and close-to-dock, toolbar ownership, +minimum/large resize, shared model/note/clip selection, scrolling, header dragging, +short-clip hit tests, Apply/restart, reset layout and failed/successful shutdown. +ASan/UBSan checks retain only the existing system Fontconfig leak suppression. diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index f9b108a..2367e50 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -5,7 +5,8 @@ extern "C" { #endif typedef enum { XTC_PLUGIN_LADSPA = 0, XTC_PLUGIN_CLAP, XTC_PLUGIN_VST2, XTC_PLUGIN_VST3, XTC_PLUGIN_BUILTIN } xtc_plugin_format; -/* Internal device indices: 0 Soft Keys, 1 Sub Bass, 2 Delay, 3 Reverb. */ +/* Internal device indices: 0 Soft Keys, 1 Sub Bass, 2 Delay, 3 Reverb, + * 4 Sample Player, 5 Stereo Delay, 6 Stereo Reverb. */ #define XTC_BUILTIN_PATH "/xtc/internal" const char *xtc_plugin_format_name(xtc_plugin_format format); #define XTC_PLUGIN_CONTROLS 16U diff --git a/include/xtc/xtc_settings.h b/include/xtc/xtc_settings.h index e5a45e7..692d45f 100644 --- a/include/xtc/xtc_settings.h +++ b/include/xtc/xtc_settings.h @@ -11,7 +11,9 @@ typedef struct { char client_name[64], clap_paths[2048], vst2_paths[2048], vst3_paths[2048], ladspa_paths[2048]; int64_t max_frames, channels, transition_ms, offline_rate, recording_seconds; int64_t width, height, piano_width, piano_height, refresh_ms; - int64_t transport_detached,mixer_detached; + int64_t transport_detached,mixer_detached,sequencer_detached,graph_detached,piano_detached; + int64_t sequencer_x,sequencer_y,sequencer_width,sequencer_height; + int64_t graph_x,graph_y,graph_width,graph_height,piano_x,piano_y,track_header_width; int64_t transport_x,transport_y,transport_width,transport_height; int64_t mixer_x,mixer_y,mixer_width,mixer_height; int64_t sequencer_snap, piano_snap, sequencer_zoom; diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index 9a43ed1..00b5235 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -180,8 +180,12 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) auto *w=static_cast(c); w->detach_mixer(!w->mixer_detached_); },this,FL_MENU_TOGGLE); menu->add("View/Dock all panels",0,[](Fl_Widget *,void *c) { - auto *w=static_cast(c); w->detach_transport(false); w->detach_mixer(false); + 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,[](Fl_Widget *,void *c){auto *w=static_cast(c);w->detach_editor(0,!w->editors_[0].detached);},this,FL_MENU_TOGGLE); + menu->add("View/Detach modular graph",0,[](Fl_Widget *,void *c){auto *w=static_cast(c);w->detach_editor(1,!w->editors_[1].detached);},this,FL_MENU_TOGGLE); + menu->add("View/Float piano roll",0,[](Fl_Widget *,void *c){auto *w=static_cast(c);w->detach_piano(!w->piano_detached_);},this,FL_MENU_TOGGLE|FL_MENU_VALUE); + menu->add("View/Reset workspace layout",0,[](Fl_Widget *,void *c){static_cast(c)->reset_layout();},this); transport_group_=new Fl_Group(0,28,1280,90); transport_group_->begin(); play_ = new Fl_Button(12, 40, 80, 30, "Play"); @@ -275,6 +279,9 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) tabs_->begin(); auto *sequencer = new Fl_Group(216, 158, 1056, 546, "Sequencer"); sequencer->begin(); + editors_[0].owner=this;editors_[0].index=0;editors_[0].page=sequencer; + editors_[0].content=new Fl_Group(216,158,1056,546); + editors_[0].content->begin(); auto *add_track = new Fl_Button(228, 168, 100, 26, "Add track"); auto *add_region = new Fl_Button(336, 168, 110, 26, "Add region"); auto *delete_region = new Fl_Button(454, 168, 130, 26, "Delete region/track"); @@ -303,7 +310,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) if (r != XTC_OK) w->report_error("Track instrument unchanged",r); }; // Keep this in the editor group rather than the scrollable timeline. - auto *parent = Fl_Group::current(); Fl_Group::current(sequencer); + auto *parent = Fl_Group::current(); Fl_Group::current(editors_[0].content); snap_ = new Fl_Choice(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())); @@ -359,7 +366,8 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) static_cast(c)->zoom(-1); }, sequencer_); - sequencer->resizable(timeline_scroll); + editors_[0].content->resizable(timeline_scroll);editors_[0].content->end(); + sequencer->resizable(editors_[0].content); sequencer->end(); auto *mixer = new Fl_Group(216, 158, 1056, 546, "Mixer"); mixer_page_=mixer; @@ -382,6 +390,8 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) mixer->end(); auto *graph = new Fl_Group(216, 158, 1056, 546, "Modular Graph"); graph->begin(); + editors_[1].owner=this;editors_[1].index=1;editors_[1].page=graph; + editors_[1].content=new Fl_Group(216,158,1056,546);editors_[1].content->begin(); graph_state_ = new Fl_Box(228, 200, 956, 24, "Graph: offline"); graph_state_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); graph_state_->labelsize(12); @@ -437,8 +447,22 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) (void)static_cast(context)->delete_selected(); }, graph_widget_); - graph->resizable(scroll); - graph->end(); + editors_[1].content->resizable(scroll);editors_[1].content->end(); + graph->resizable(editors_[1].content);graph->end(); + piano_page_=new Fl_Group(216,158,1056,546,"Piano roll"); + piano_placeholder_=new Fl_Group(228,210,1000,120); + auto *piano_hint=new Fl_Box(240,220,900,30,"Select a MIDI clip, then open its piano roll. Float or dock it from View.");piano_hint->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE); + auto *open_piano=new Fl_Button(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 Fl_Group(228,210,1000,120); + auto *hint=new Fl_Box(240,220,900,30,"This editor is open in a separate window.");hint->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE); + auto *show=new Fl_Button(240,260,170,30,"Show editor window"); + show->callback([](Fl_Widget *,void *c){auto *p=static_cast(c);if(p->window)p->window->show();},&panel); + auto *dock=new Fl_Button(424,260,140,30,"Dock editor"); + dock->callback([](Fl_Widget *,void *c){auto *p=static_cast(c);p->owner->detach_editor(p->index,false);},&panel); + panel.placeholder->end();panel.placeholder->hide();panel.page->end(); + } tabs_->end(); tabs_->value(sequencer); menu->add("View/Sequencer", FL_CTRL + '1', view_callback, sequencer); @@ -514,6 +538,67 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) Fl::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=Fl_Group::current();Fl_Group::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 Fl_Button(12,6,150,28,"Dock editor");dock->callback([](Fl_Widget *,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([](Fl_Widget *,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); + Fl_Group::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::resize(int x,int y,int width,int height) { Fl_Double_Window::resize(x,y,width,height); @@ -638,7 +723,7 @@ MainWindow::~MainWindow() Fl::remove_timeout(timer_callback, this); // Return ownership before member windows and the main widget tree destruct. detach_transport(false); - detach_mixer(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. @@ -986,6 +1071,7 @@ bool MainWindow::close_window() running_ = false; closing_ = true; Fl::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(); @@ -1076,6 +1162,8 @@ void MainWindow::view_callback(Fl_Widget *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(); } @@ -1635,8 +1723,9 @@ xtc_result MainWindow::open_piano_roll() 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"); + piano_window_ = std::make_unique(900, 620, "xtc — Piano roll (close to dock)",this); piano_window_->begin(); + piano_content_=new Fl_Group(0,0,900,620);piano_content_->begin(); auto *menu = new Fl_Menu_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)", @@ -1674,21 +1763,21 @@ xtc_result MainWindow::open_piano_roll() 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([](Fl_Widget *,void *c){static_cast(c)->detach_piano(false);},this); piano_window_->size_range(800,520); xtc_ui::style(piano_window_.get()); - piano_window_->resizable(scroll); + piano_window_->resizable(piano_content_); scroll->scroll_to(0, 60 * 16); - 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_.width=w();settings_.height=h(); - settings_.sequencer_snap=static_cast(sequencer_->grid());settings_.sequencer_zoom=sequencer_->zoom_level(); + 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(); - piano_window_->show(); + detach_piano(settings_.piano_detached!=0); + if(piano_detached_)piano_window_->show();else {tabs_->value(piano_page_);tabs_->redraw();} return XTC_OK; } @@ -2314,6 +2403,14 @@ xtc_result MainWindow::set_loop(uint64_t start,uint64_t end,bool enabled) } 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();} @@ -2325,6 +2422,10 @@ void MainWindow::collect_preferences() } 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); @@ -2335,7 +2436,7 @@ void MainWindow::apply_ui_preferences() 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_) { - piano_window_->resize(piano_window_->x(),piano_window_->y(),static_cast(settings_.piano_width),static_cast(settings_.piano_height)); + 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); } @@ -2468,7 +2569,7 @@ void MainWindow::follow_playhead() if (!running_ || !follow_->value() || tabs_->value() != tabs_->child(0) || Fl::pushed()) return; int pixel = static_cast(sequencer_->position() * static_cast(sequencer_->zoom_level()) / 960); - int screen = sequencer_->x() + 180 + pixel; - if (screen > timeline_scroll_->x() + timeline_scroll_->w() - 48 || screen < timeline_scroll_->x() + 180) + 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()); } diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index af5bf80..4776306 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -36,6 +36,10 @@ class MainWindow : public Fl_Double_Window { xtc_result set_tempo(unsigned int milli_bpm); void show_commands(); /* 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(); + Fl_Double_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); @@ -203,6 +207,14 @@ class MainWindow : public Fl_Double_Window { xtc_result return_to_start(); private: + struct EditorPanel { + MainWindow *owner=nullptr;unsigned int index=0; + Fl_Group *page=nullptr,*content=nullptr,*placeholder=nullptr; + std::unique_ptr window;bool detached=false; + }; + EditorPanel editors_[2]; + Fl_Group *piano_page_=nullptr,*piano_content_=nullptr,*piano_placeholder_=nullptr; + bool piano_detached_=true; std::unique_ptr transport_window_, mixer_window_; Fl_Group *transport_group_=nullptr, *mixer_page_=nullptr, *sidebar_=nullptr; Fl_Group *mixer_placeholder_=nullptr; diff --git a/src/gui/sequencer_widget.cpp b/src/gui/sequencer_widget.cpp index 88a1ab3..63f435c 100644 --- a/src/gui/sequencer_widget.cpp +++ b/src/gui/sequencer_widget.cpp @@ -74,9 +74,19 @@ void SequencerWidget::set_project(xtc_project *project, bool reset) * @param px Screen x coordinate in pixels. * @return Snapped tick within the supported project range. */ +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()-180))*960/scale_; + 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_))); } @@ -93,7 +103,7 @@ void SequencerWidget::extent() xtc_project_get_clip(project_, i, &c); end = std::max(end, c.start_tick + c.length_tick + 3840U); } - size(static_cast(end * static_cast(scale_) / 960U + 200U), + size(static_cast(end * static_cast(scale_) / 960U + static_cast(header_width_+20)), std::max(400, static_cast(tracks) * 56 + 100)); refresh_instruments(); if (parent()) @@ -239,48 +249,35 @@ void SequencerWidget::draw() int py = y()+28+static_cast(i)*56; if (py+56 < cy || py > cy+ch) continue; fl_color(i%2 ? fl_rgb_color(30,34,40) : fl_rgb_color(33,37,43)); - fl_rectf(x()+180,py,w()-180,55); + fl_rectf(x()+header_width_,py,w()-header_width_,55); } fl_font(FL_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()-180)/spacing); - int last = (cx+cw-x()-180)/spacing+1; + 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()+180+static_cast(tick*static_cast(scale_)/960); + int px = x()+header_width_+static_cast(tick*static_cast(scale_)/960); fl_color(tick%3840 == 0 ? fl_rgb_color(76,84,96) : tick%960 == 0 ? fl_rgb_color(56,63,73) : fl_rgb_color(42,48,56)); fl_line(px,y()+28,px,y()+h()); } - first = std::max(0,(cx-x()-180)/(scale_*4)); - last = (cx+cw-x()-180)/(scale_*4)+1; + first = std::max(0,(cx-x()-header_width_)/(scale_*4)); + last = (cx+cw-x()-header_width_)/(scale_*4)+1; for (int bar = first; bar <= last; ++bar) { char label[32]; std::snprintf(label,sizeof(label),"%d",bar+1); - fl_color(xtc_ui::muted()); fl_draw(label,x()+180+bar*scale_*4+6,y()+19); + fl_color(xtc_ui::muted()); fl_draw(label,x()+header_width_+bar*scale_*4+6,y()+19); } - fl_color(xtc_ui::muted()); fl_draw("TRACKS / 4:4",x()+12,y()+19); for (size_t i = 0; i < tracks; ++i) { xtc_track_info t{}; xtc_project_get_track(project_,i,&t); int py = y()+28+static_cast(i)*56; if (py+56 < cy || py > cy+ch) continue; Fl_Color color = xtc_ui::track(t.id); - fl_color(t.id == track_ ? fl_rgb_color(49,62,70) : xtc_ui::panel()); - fl_rectf(x(),py,178,55); - fl_color(color); fl_rectf(x(),py,3,55); - fl_push_clip(x()+9,py,162,55); - fl_color(xtc_ui::text()); fl_font(FL_HELVETICA_BOLD,12); - fl_draw(t.name,x()+12,py+18); - char detail[64]; - std::snprintf(detail,sizeof(detail),"%02zu %s%s%s",i+1,t.is_bus ? "BUS" : "TRACK", - t.mute ? " MUTE" : "",t.solo ? " SOLO" : ""); - if (t.is_bus) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(detail,x()+12,py+42); } - if (t.mute || t.solo) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(t.mute ? "M" : "S",x()+164,py+18); } - fl_pop_clip(); for (size_t j = 0; j < clips; ++j) { xtc_clip_info c{}; xtc_project_get_clip(project_,j,&c); if (c.track_id != t.id) continue; - int left = x()+180+static_cast(c.start_tick*static_cast(scale_)/960U); + int left = x()+header_width_+static_cast(c.start_tick*static_cast(scale_)/960U); int right = left+std::max(4,static_cast(c.length_tick*static_cast(scale_)/960U)); int a = std::max(left,cx), b = std::min(right,cx+cw); if (b <= a) continue; @@ -324,16 +321,39 @@ void SequencerWidget::draw() { uint64_t begin=mode_==4 ? std::min(loop_anchor_,loop_preview_) : loop_begin_; uint64_t end=mode_==4 ? std::max(loop_anchor_,loop_preview_) : loop_end_; - int left=x()+180+static_cast(begin*static_cast(scale_)/960); - int right=x()+180+static_cast(end*static_cast(scale_)/960); + int left=x()+header_width_+static_cast(begin*static_cast(scale_)/960); + int right=x()+header_width_+static_cast(end*static_cast(scale_)/960); fl_color(loop_enabled_ ? xtc_ui::accent() : xtc_ui::muted()); fl_rectf(left,y()+23,std::max(0,right-left),4); } - int playhead = x()+180+static_cast(cursor_*static_cast(scale_)/960U); + int playhead = x()+header_width_+static_cast(cursor_*static_cast(scale_)/960U); if (playhead >= cx && playhead <= cx+cw) { fl_color(xtc_ui::accent()); fl_line(playhead,y(),playhead,y()+h()); fl_polygon(playhead-4,y(),playhead+4,y(),playhead,y()+6); } + int pinned=header_left(); + fl_color(xtc_ui::background());fl_rectf(pinned,y(),header_width_,28); + fl_color(xtc_ui::muted());fl_draw("TRACKS / drag edge to resize",pinned+10,y()+19); + for(size_t i=0;i(i)*56; + if(py+56cy+ch)continue; + Fl_Color color = xtc_ui::track(t.id); + fl_color(t.id == track_ ? fl_rgb_color(49,62,70) : xtc_ui::panel()); + fl_rectf(pinned,py,header_width_-2,55); + fl_color(color); fl_rectf(pinned,py,3,55); + fl_push_clip(pinned+9,py,header_width_-18,55); + fl_color(xtc_ui::text()); fl_font(FL_HELVETICA_BOLD,12); + fl_draw(t.name,pinned+12,py+18); + char detail[64]; + std::snprintf(detail,sizeof(detail),"%02zu %s%s%s",i+1,t.is_bus ? "BUS" : "TRACK", + t.mute ? " MUTE" : "",t.solo ? " SOLO" : ""); + if (t.is_bus) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(detail,pinned+12,py+42); } + if (t.mute || t.solo) { fl_font(FL_HELVETICA,10); fl_color(xtc_ui::muted()); fl_draw(t.mute ? "M" : "S",pinned+header_width_-16,py+18); } + fl_pop_clip(); + } + fl_color(xtc_ui::muted());fl_rectf(pinned+header_width_-3,cy,3,ch); + for(size_t i=0;iresize(pinned+10,y()+58+static_cast(i)*56,header_width_-22,20); draw_children(); fl_pop_clip(); } @@ -346,6 +366,13 @@ void SequencerWidget::draw() */ int SequencerWidget::handle(int event) { + if(event==FL_PUSH && Fl::event_button()==FL_LEFT_MOUSE && Fl::event_x()>=header_left()+header_width_-5 && Fl::event_x()visible()) continue; if ((event == FL_PUSH && Fl::event_inside(choice.widget)) || @@ -356,11 +383,12 @@ int SequencerWidget::handle(int event) if (event == FL_PUSH && (Fl::event_button() == FL_LEFT_MOUSE || Fl::event_button() == FL_RIGHT_MOUSE)) { take_focus(); mode_ = 0; - if (Fl::event_y() < y() + 28 && Fl::event_x() >= x()+180 && + if (Fl::event_y() < y() + 28 && Fl::event_x() >= header_left()+header_width_ && Fl::event_button()==FL_LEFT_MOUSE && Fl::event_state(FL_SHIFT)) { loop_anchor_=loop_preview_=tick(Fl::event_x()); mode_=4; redraw(); return 1; } if (Fl::event_y() < y() + 28) { + if(Fl::event_x()(row), &t); track_ = t.id; clip_ = UINT64_MAX; - for (size_t j = clips; j > 0; --j) { + for (size_t j = clips; j > 0 && Fl::event_x()>=header_left()+header_width_; --j) { xtc_clip_info c{}; xtc_project_get_clip(project_, j - 1, &c); - int left = x() + 180 + static_cast(c.start_tick * static_cast(scale_) / 960U); + int left = x() + header_width_ + static_cast(c.start_tick * static_cast(scale_) / 960U); int right = left + std::max(4, static_cast(c.length_tick * static_cast(scale_) / 960U)); if (c.track_id == track_ && Fl::event_x() >= left && Fl::event_x() < right) { clip_ = c.id; @@ -638,13 +666,13 @@ void SequencerWidget::refresh_instruments() try explicit GroupScope(Fl_Group *group) : previous(Fl_Group::current()) { Fl_Group::current(group); } ~GroupScope() { Fl_Group::current(previous); } } scope(this); - entry.widget = new Fl_Choice(x()+10,y()+58+static_cast(i)*56,158,20); + entry.widget = new Fl_Choice(header_left()+10,y()+58+static_cast(i)*56,header_width_-22,20); xtc_ui::style(entry.widget); entry.widget->textsize(10); entry.widget->textcolor(xtc_ui::text()); entry.widget->tooltip("Track instrument: built-in sine, an instrument used in this project, or Load plugin..."); entry.widget->callback(instrument_callback,this); } entry.track = t.id; - entry.widget->resize(x()+10,y()+58+static_cast(i)*56,158,20); + entry.widget->resize(header_left()+10,y()+58+static_cast(i)*56,header_width_-22,20); entry.widget->clear(); entry.widget->add("Built-in sine"); xtc_plugin_spec current{}; int selected = 0; bool assigned = xtc_project_get_instrument(project_,t.id,¤t) == XTC_OK; diff --git a/src/gui/sequencer_widget.hpp b/src/gui/sequencer_widget.hpp index 3aefe69..7ffaab6 100644 --- a/src/gui/sequencer_widget.hpp +++ b/src/gui/sequencer_widget.hpp @@ -19,6 +19,9 @@ class SequencerWidget : public Fl_Group { 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(); } @@ -173,6 +176,7 @@ class SequencerWidget : public Fl_Group { 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. diff --git a/src/plugin/builtin.c b/src/plugin/builtin.c index 45428c9..1f64991 100644 --- a/src/plugin/builtin.c +++ b/src/plugin/builtin.c @@ -132,7 +132,7 @@ static float parameter(const xtc_builtin *p, unsigned int port, uint64_t positio xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size_t frames, uint64_t position) { size_t i; unsigned int j; - if(!p || !out || frames>p->max_frames || p->spec.instrument || position>UINT64_MAX-frames) return XTC_ERR_INVALID_ARGUMENT; + if(!p || !out || !p->length || !p->line || frames>p->max_frames || p->spec.instrument || position>UINT64_MAX-frames) return XTC_ERR_INVALID_ARGUMENT; if(p->spec.index>=5)return xtc_builtin_stereo(p,in,in,out,NULL,frames,position); for(i=0;ispec.index<5 || !out || frames>p->max_frames || position>UINT64_MAX-frames || out==out_right)return XTC_ERR_INVALID_ARGUMENT; + if(!p || !p->length || !p->line || p->spec.index<5 || !out || frames>p->max_frames || position>UINT64_MAX-frames || out==out_right)return XTC_ERR_INVALID_ARGUMENT; for(i=0;iladspa_paths,"~/.ladspa:/usr/local/lib/ladspa:/usr/lib/ladspa"); s->max_frames=65536; s->channels=2; s->transition_ms=5; s->offline_rate=48000; s->recording_seconds=2; s->width=1280; s->height=800; s->piano_width=900; s->piano_height=620; s->refresh_ms=100; + s->piano_detached=1;s->piano_x=s->piano_y=100;s->track_header_width=180; + s->sequencer_x=s->sequencer_y=s->graph_x=s->graph_y=80; + s->sequencer_width=s->graph_width=1100;s->sequencer_height=s->graph_height=650; s->transport_x=s->transport_y=80;s->transport_width=800;s->transport_height=300; s->mixer_x=s->mixer_y=100;s->mixer_width=1056;s->mixer_height=600; s->sequencer_snap=s->piano_snap=240; s->sequencer_zoom=48; s->clip_ticks=3840; s->note_ticks=960; diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 7a07f23..61b33c6 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -255,14 +255,7 @@ int main(int argc, char **argv) CHECK(std::strcmp(tabs->value()->label(), std::strchr(view, '/') + 1) == 0); } } - GraphWidget *canvas = nullptr; - auto *graph_group = dynamic_cast(find(&window, "Modular Graph")); - CHECK(graph_group != nullptr); - if (graph_group != nullptr) { - for (int i = 0; i < graph_group->children(); ++i) if (auto *scroll = dynamic_cast(graph_group->child(i))) - for (int j = 0; j < scroll->children(); ++j) - if (auto *found = dynamic_cast(scroll->child(j))) canvas = found; - } + GraphWidget *canvas = graph_canvas(&window); CHECK(canvas != nullptr); if (canvas == nullptr) return 1; menu->picked(menu->find_item("View/Modular Graph")); @@ -1204,7 +1197,7 @@ int main(int argc, char **argv) auto *play=find(&window,"Play"); auto *bpm=dynamic_cast(find(&window,"BPM")); auto *loop=dynamic_cast(find(&window,"Loop")); - auto *tabs=dynamic_cast(sequence->parent()->parent()->parent()); + auto *tabs=dynamic_cast(sequence->parent()->parent()->parent()->parent()); CHECK(play && bpm && loop && tabs); int original_y=tabs->y(); for (int pass=0;pass<3;++pass) { @@ -1266,6 +1259,54 @@ int main(int argc, char **argv) } unlink(path.c_str()); } + reset(); + { + std::string path="/tmp/xtc-editors-"+std::to_string(getpid())+".conf"; + { + MainWindow window(false,path.c_str());window.show();CHECK(window.create_track(true)==XTC_OK); + auto *sequence=timeline(&window);auto *graph=graph_canvas(&window);const xtc_project *project=sequence->project(); + uint64_t track=sequence->selected_track(),clip=sequence->selected_clip(); + auto *scroll=dynamic_cast(sequence->parent());CHECK(scroll);scroll->scroll_to(400,0);pump(); + int pinned=sequence->header_left();CHECK(pinned==scroll->x()); + sequence->header_width(260);pump();CHECK(sequence->header_width()==260 && scroll->xposition()==400); + CHECK(sequence->instrument_choice(track)->x()==pinned+10 && sequence->instrument_choice(track)->w()==238); + gesture(sequence,pinned-sequence->x()+258,16,40,0);CHECK(sequence->header_width()==300); + sequence->header_width(260); + CHECK(window.open_piano_roll()==XTC_OK);auto *piano=window.piano_roll(); + CHECK(piano->add_note(0,240,60)==XTC_OK);uint64_t note=piano->selected_note(); + project=sequence->project(); + auto *piano_scroll=dynamic_cast(piano->parent());CHECK(piano_scroll);piano_scroll->scroll_to(10,500); + for(int pass=0;pass<3;++pass) { + window.detach_editor(0,true);window.detach_editor(1,true);window.detach_piano(false);pump(); + CHECK(sequence->window()==window.editor_window(0) && graph->window()==window.editor_window(1)); + CHECK(find(window.editor_window(0),"Snap")!=nullptr); + CHECK(piano->window()==&window && piano->selected_note()==note && sequence->project()==project); + CHECK(sequence->selected_track()==track && sequence->selected_clip()==clip); + CHECK(scroll->xposition()==400 && piano_scroll->yposition()==500); + window.editor_window(0)->resize(20,20,1200,700);window.editor_window(1)->resize(30,30,1000,500);pump(); + window.detach_piano(true);CHECK(piano->window()!=&window); + auto *piano_window=piano->window();piano_window->do_callback();CHECK(piano->window()==&window); + window.editor_window(0)->do_callback();window.editor_window(1)->do_callback();pump(); + CHECK(sequence->window()==&window && graph->window()==&window && scroll->xposition()==400); + CHECK(piano->selected_note()==note && sequence->project()==project); + } + window.detach_editor(0,true);window.detach_editor(1,true);window.detach_piano(false); + Fl::add_timeout(.05,[](void *) {auto *d=Fl::first_window();press(find(d,"Apply"));press(find(d,"Close"));}); + window.show_preferences();xtc_settings saved{};CHECK(xtc_settings_load(&saved,path.c_str())==XTC_OK); + CHECK(saved.sequencer_detached && saved.graph_detached && !saved.piano_detached && saved.track_header_width==260); + close_result=XTC_ERR_IO;window.do_callback();CHECK(window.editor_window(0)->shown() && window.editor_window(1)->shown()); + close_result=XTC_OK;window.do_callback();CHECK(!window.editor_window(0)->shown() && !window.editor_window(1)->shown()); + } + { + MainWindow window(false,path.c_str());window.show();pump(); + CHECK(window.editor_window(0)->shown() && window.editor_window(1)->shown()); + CHECK(window.create_track(true)==XTC_OK && window.open_piano_roll()==XTC_OK && window.piano_roll()->window()==&window); + window.reset_layout();pump();CHECK(timeline(&window) && graph_canvas(&window)); + CHECK(!window.editor_window(0)->shown() && !window.editor_window(1)->shown()); + CHECK(timeline(&window)->header_width()==180);window.do_callback(); + } + unlink(path.c_str()); + } if (failures != 0) return 1; std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes); return 0;