feat: dock editor panels and add resizable pinned track headers
This commit is contained in:
parent
73e8bf09dc
commit
ae8556d83f
13 changed files with 346 additions and 87 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,¤t) == XTC_OK;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue