feat: dock editor panels and add resizable pinned track headers

This commit is contained in:
Johannes Findeisen 2026-11-06 00:06:56 +01:00
commit ae8556d83f
13 changed files with 346 additions and 87 deletions

View file

@ -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,

View file

@ -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.

View file

@ -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.
---

View file

@ -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.

View file

@ -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

View file

@ -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;

View file

@ -180,8 +180,12 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
auto *w=static_cast<MainWindow *>(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<MainWindow *>(c); w->detach_transport(false); w->detach_mixer(false);
auto *w=static_cast<MainWindow *>(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<MainWindow *>(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<MainWindow *>(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<MainWindow *>(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<MainWindow *>(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<SequencerWidget *>(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<GraphWidget *>(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<EditorPanel *>(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<EditorPanel *>(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<double>(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<Fl_Scroll *>(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<EditorPanel *>(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<EditorPanel *>(c);p->owner->detach_editor(p->index,false);},&panel);
restore_panel(panel.window.get(),static_cast<int>(index==0 ? settings_.sequencer_x : settings_.graph_x),static_cast<int>(index==0 ? settings_.sequencer_y : settings_.graph_y),static_cast<int>(index==0 ? settings_.sequencer_width : settings_.graph_width),static_cast<int>(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<Fl_Menu_Item *>(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<Fl_Menu_Item *>(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<Fl_Scroll *>(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<MainWindow *>(widget->window());
window->tabs_->value(static_cast<Fl_Widget *>(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<Fl_Double_Window>(900, 620, "xtc — Piano roll");
piano_window_ = std::make_unique<PanelWindow>(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<MainWindow *>(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<int64_t>(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<int>(settings_.track_header_width));
for(unsigned int i=0;i<2;++i)if(editors_[i].window)restore_panel(editors_[i].window.get(),static_cast<int>(i==0 ? settings_.sequencer_x : settings_.graph_x),static_cast<int>(i==0 ? settings_.sequencer_y : settings_.graph_y),static_cast<int>(i==0 ? settings_.sequencer_width : settings_.graph_width),static_cast<int>(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<int>(settings_.transport_x),static_cast<int>(settings_.transport_y),static_cast<int>(settings_.transport_width),static_cast<int>(settings_.transport_height),400,130);
@ -2335,7 +2436,7 @@ void MainWindow::apply_ui_preferences()
sequencer_->new_clip_ticks=static_cast<uint64_t>(settings_.clip_ticks);
sequencer_->loop_region(static_cast<uint64_t>(settings_.loop_start),static_cast<uint64_t>(settings_.loop_end),settings_.loop_enabled!=0);
if(piano_) {
piano_window_->resize(piano_window_->x(),piano_window_->y(),static_cast<int>(settings_.piano_width),static_cast<int>(settings_.piano_height));
restore_panel(piano_window_.get(),static_cast<int>(settings_.piano_x),static_cast<int>(settings_.piano_y),static_cast<int>(settings_.piano_width),static_cast<int>(settings_.piano_height),800,520);
piano_->set_grid(static_cast<uint64_t>(settings_.piano_snap));piano_->set_filter(static_cast<int>(settings_.filter_channel),static_cast<unsigned int>(settings_.filter_low),static_cast<unsigned int>(settings_.filter_high));
piano_->new_note_ticks=static_cast<uint64_t>(settings_.note_ticks);piano_->new_note_velocity=static_cast<unsigned int>(settings_.note_velocity);piano_->new_note_channel=static_cast<unsigned int>(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<int>(sequencer_->position() * static_cast<unsigned int>(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());
}

View file

@ -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<Fl_Double_Window> 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<Fl_Double_Window> transport_window_, mixer_window_;
Fl_Group *transport_group_=nullptr, *mixer_page_=nullptr, *sidebar_=nullptr;
Fl_Group *mixer_placeholder_=nullptr;

View file

@ -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<Fl_Scroll *>(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<Fl_Scroll *>(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<int64_t>(std::max(0,px-x()-180))*960/scale_;
int64_t raw = static_cast<int64_t>(std::max(0,px-x()-header_width_))*960/scale_;
return std::min(XTC_PROJECT_MAX_TICK,static_cast<uint64_t>(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<int>(end * static_cast<unsigned int>(scale_) / 960U + 200U),
size(static_cast<int>(end * static_cast<unsigned int>(scale_) / 960U + static_cast<unsigned int>(header_width_+20)),
std::max(400, static_cast<int>(tracks) * 56 + 100));
refresh_instruments();
if (parent())
@ -239,48 +249,35 @@ void SequencerWidget::draw()
int py = y()+28+static_cast<int>(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<unsigned int>(scale_)/960 < 6) visual *= 2;
int spacing = static_cast<int>(visual*static_cast<unsigned int>(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<uint64_t>(step)*visual;
int px = x()+180+static_cast<int>(tick*static_cast<unsigned int>(scale_)/960);
int px = x()+header_width_+static_cast<int>(tick*static_cast<unsigned int>(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<int>(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<int>(c.start_tick*static_cast<unsigned int>(scale_)/960U);
int left = x()+header_width_+static_cast<int>(c.start_tick*static_cast<unsigned int>(scale_)/960U);
int right = left+std::max(4,static_cast<int>(c.length_tick*static_cast<unsigned int>(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<int>(begin*static_cast<unsigned int>(scale_)/960);
int right=x()+180+static_cast<int>(end*static_cast<unsigned int>(scale_)/960);
int left=x()+header_width_+static_cast<int>(begin*static_cast<unsigned int>(scale_)/960);
int right=x()+header_width_+static_cast<int>(end*static_cast<unsigned int>(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<int>(cursor_*static_cast<unsigned int>(scale_)/960U);
int playhead = x()+header_width_+static_cast<int>(cursor_*static_cast<unsigned int>(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<tracks;++i) {
xtc_track_info t{};xtc_project_get_track(project_,i,&t);int py=y()+28+static_cast<int>(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(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;i<track_choices_.size();++i)if(track_choices_[i].widget)
track_choices_[i].widget->resize(pinned+10,y()+58+static_cast<int>(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()<header_left()+header_width_) {
mode_=5;header_drag_x_=Fl::event_x();header_drag_original_=header_width_;return 1;
}
if(mode_==5 && (event==FL_DRAG || event==FL_RELEASE)) {
header_width(header_drag_original_+Fl::event_x()-header_drag_x_);if(event==FL_RELEASE)mode_=0;return 1;
}
if(mode_==5 && event==FL_KEYDOWN && Fl::event_key()==FL_Escape) {header_width(header_drag_original_);mode_=0;return 1;}
for (const auto &choice : track_choices_) {
if (!choice.widget || !choice.widget->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()<header_left()+header_width_)return 1;
uint64_t target = tick(Fl::event_x());
xtc_result r = seek_(context_, target);
if (r == XTC_OK)
@ -379,10 +407,10 @@ int SequencerWidget::handle(int event)
xtc_project_get_track(project_, static_cast<size_t>(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<int>(c.start_tick * static_cast<unsigned int>(scale_) / 960U);
int left = x() + header_width_ + static_cast<int>(c.start_tick * static_cast<unsigned int>(scale_) / 960U);
int right = left + std::max(4, static_cast<int>(c.length_tick * static_cast<unsigned int>(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<int>(i)*56,158,20);
entry.widget = new Fl_Choice(header_left()+10,y()+58+static_cast<int>(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<int>(i)*56,158,20);
entry.widget->resize(header_left()+10,y()+58+static_cast<int>(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,&current) == XTC_OK;

View file

@ -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.

View file

@ -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;i<frames;++i) {
float x=in && isfinite(in[i]) ? in[i] : 0, wet=0;
@ -157,7 +157,7 @@ xtc_result xtc_builtin_process(xtc_builtin *p, const float *in, float *out, size
xtc_result xtc_builtin_stereo(xtc_builtin *p,const float *left,const float *right,float *out,float *out_right,size_t frames,uint64_t position)
{
size_t i;unsigned int side,j;
if(!p || p->spec.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;i<frames;++i) {
float input[2]={left && isfinite(left[i]) ? left[i] : 0,right && isfinite(right[i]) ? right[i] : 0};
float wet[2]={0,0};float a=parameter(p,0,position+i),b=parameter(p,1,position+i),mix=parameter(p,2,position+i);

View file

@ -29,6 +29,14 @@ static const xtc_setting_info schema[] = {
NUM("interface",transport_width,"Transport width",400,3840),NUM("interface",transport_height,"Transport height",130,2160),
NUM("interface",mixer_x,"Mixer window X",-32768,32768),NUM("interface",mixer_y,"Mixer window Y",-32768,32768),
NUM("interface",mixer_width,"Mixer window width",600,3840),NUM("interface",mixer_height,"Mixer window height",420,2160),
NUM("interface",sequencer_detached,"Floating sequencer: 0 or 1",0,1),NUM("interface",graph_detached,"Floating graph: 0 or 1",0,1),
NUM("interface",piano_detached,"Floating piano roll: 0 or 1",0,1),
NUM("interface",sequencer_x,"Sequencer X",-32768,32768),NUM("interface",sequencer_y,"Sequencer Y",-32768,32768),
NUM("interface",sequencer_width,"Sequencer window width",1100,3840),NUM("interface",sequencer_height,"Sequencer window height",420,2160),
NUM("interface",graph_x,"Graph X",-32768,32768),NUM("interface",graph_y,"Graph Y",-32768,32768),
NUM("interface",graph_width,"Graph window width",1000,3840),NUM("interface",graph_height,"Graph window height",420,2160),
NUM("interface",piano_x,"Piano X",-32768,32768),NUM("interface",piano_y,"Piano Y",-32768,32768),
NUM("interface",track_header_width,"Pinned track header width",180,360),
NUM("interface",refresh_ms,"Display refresh interval, ms",20,1000),
NUM("editing",sequencer_snap,"Sequencer snap ticks (1 means Off)",1,3840),
NUM("editing",piano_snap,"Piano snap ticks (1 means Off)",1,3840),
@ -60,6 +68,9 @@ void xtc_settings_defaults(xtc_settings *s)
strcpy(s->ladspa_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;

View file

@ -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<Fl_Group *>(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<Fl_Group *>(graph_group->child(i)))
for (int j = 0; j < scroll->children(); ++j)
if (auto *found = dynamic_cast<GraphWidget *>(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<Fl_Value_Input *>(find(&window,"BPM"));
auto *loop=dynamic_cast<Fl_Check_Button *>(find(&window,"Loop"));
auto *tabs=dynamic_cast<Fl_Tabs *>(sequence->parent()->parent()->parent());
auto *tabs=dynamic_cast<Fl_Tabs *>(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<Fl_Scroll *>(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<Fl_Scroll *>(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;