Lightningbeam/lightningbeam-ui/lightningbeam-editor/src/sample_import_dialog.rs

238 lines
10 KiB
Rust

//! Import dialog for MultiSampler folder import.
//!
//! Shows a preview of parsed samples with editable note mappings, velocity ranges,
//! and loop mode before committing the import.
use eframe::egui;
use egui_node_graph2::NodeId;
use std::path::PathBuf;
use crate::sample_import::{
FolderScanResult, midi_to_note_name, recalc_key_ranges,
};
use daw_backend::audio::node_graph::nodes::LoopMode;
pub struct SampleImportDialog {
pub folder_path: PathBuf,
pub scan_result: FolderScanResult,
pub loop_mode: LoopMode,
pub auto_key_ranges: bool,
pub confirmed: bool,
pub should_close: bool,
pub track_id: u32,
pub backend_node_id: u32,
pub node_id: NodeId,
}
impl SampleImportDialog {
pub fn new(
folder_path: PathBuf,
scan_result: FolderScanResult,
track_id: u32,
backend_node_id: u32,
node_id: NodeId,
) -> Self {
let loop_mode = scan_result.loop_mode;
Self {
folder_path,
scan_result,
loop_mode,
auto_key_ranges: true,
confirmed: false,
should_close: false,
track_id,
backend_node_id,
node_id,
}
}
/// Returns true while the dialog is still open.
pub fn show(&mut self, ctx: &egui::Context) -> bool {
let mut open = true;
let mut should_import = false;
let mut should_cancel = false;
let mut recalc = false;
egui::Window::new("Import Samples")
.open(&mut open)
.resizable(true)
.collapsible(false)
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
.default_width(700.0)
.default_height(500.0)
.show(ctx, |ui| {
// Folder info
ui.label(format!("Folder: {}", self.folder_path.display()));
let enabled_count = self.scan_result.layers.iter().filter(|l| l.enabled).count();
let unique_notes: std::collections::HashSet<u8> = self.scan_result.layers.iter()
.filter(|l| l.enabled)
.map(|l| l.root_key)
.collect();
let vel_count = self.scan_result.velocity_markers.len();
ui.label(format!(
"Found: {} samples, {} notes, {} velocity layer{}",
enabled_count,
unique_notes.len(),
vel_count,
if vel_count != 1 { "s" } else { "" },
));
ui.add_space(4.0);
// Global controls
ui.horizontal(|ui| {
ui.label("Loop mode:");
egui::ComboBox::from_id_salt("loop_mode")
.selected_text(match self.loop_mode {
LoopMode::OneShot => "One Shot",
LoopMode::Continuous => "Continuous",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.loop_mode, LoopMode::OneShot, "One Shot");
ui.selectable_value(&mut self.loop_mode, LoopMode::Continuous, "Continuous");
});
ui.add_space(16.0);
if ui.checkbox(&mut self.auto_key_ranges, "Auto key ranges").changed() {
if self.auto_key_ranges {
recalc = true;
}
}
});
ui.add_space(4.0);
// Velocity mapping table
if !self.scan_result.velocity_ranges.is_empty() {
ui.collapsing("Velocity Mapping", |ui| {
egui::Grid::new("vel_grid").striped(true).show(ui, |ui| {
ui.label(egui::RichText::new("Marker").strong());
ui.label(egui::RichText::new("Min").strong());
ui.label(egui::RichText::new("Max").strong());
ui.end_row();
for (marker, min, max) in &mut self.scan_result.velocity_ranges {
ui.label(&*marker);
ui.add(egui::DragValue::new(min).range(0..=127).speed(1));
ui.add(egui::DragValue::new(max).range(0..=127).speed(1));
ui.end_row();
}
});
});
ui.add_space(4.0);
}
// Layers table
ui.separator();
ui.label(egui::RichText::new("Layers").strong());
let available_height = ui.available_height() - 40.0; // reserve space for buttons
egui::ScrollArea::vertical()
.max_height(available_height.max(100.0))
.show(ui, |ui| {
egui::Grid::new("layers_grid")
.striped(true)
.min_col_width(20.0)
.show(ui, |ui| {
// Header
ui.label(""); // checkbox column
ui.label(egui::RichText::new("File").strong());
ui.label(egui::RichText::new("Root").strong());
ui.label(egui::RichText::new("Key Range").strong());
ui.label(egui::RichText::new("Vel Range").strong());
ui.end_row();
for i in 0..self.scan_result.layers.len() {
let layer = &mut self.scan_result.layers[i];
if ui.checkbox(&mut layer.enabled, "").changed() && self.auto_key_ranges {
recalc = true;
}
// Filename (truncated)
let name = if layer.filename.len() > 40 {
format!("...{}", &layer.filename[layer.filename.len()-37..])
} else {
layer.filename.clone()
};
ui.label(&name).on_hover_text(&layer.filename);
// Root note
let mut root = layer.root_key as i32;
if ui.add(egui::DragValue::new(&mut root)
.range(0..=127)
.speed(1)
.custom_formatter(|v, _| midi_to_note_name(v as u8))
).changed() {
layer.root_key = root as u8;
if self.auto_key_ranges {
recalc = true;
}
}
// Key range
if self.auto_key_ranges {
ui.label(format!("{}-{}", midi_to_note_name(layer.key_min), midi_to_note_name(layer.key_max)));
} else {
let mut kmin = layer.key_min as i32;
let mut kmax = layer.key_max as i32;
ui.horizontal(|ui| {
if ui.add(egui::DragValue::new(&mut kmin).range(0..=127).speed(1)
.custom_formatter(|v, _| midi_to_note_name(v as u8))
).changed() {
layer.key_min = kmin as u8;
}
ui.label("-");
if ui.add(egui::DragValue::new(&mut kmax).range(0..=127).speed(1)
.custom_formatter(|v, _| midi_to_note_name(v as u8))
).changed() {
layer.key_max = kmax as u8;
}
});
}
// Velocity range
ui.label(format!("{}-{}", layer.velocity_min, layer.velocity_max));
ui.end_row();
}
});
// Unmapped section
if !self.scan_result.unmapped.is_empty() {
ui.add_space(8.0);
ui.label(egui::RichText::new(format!("Unmapped ({})", self.scan_result.unmapped.len())).strong());
for sample in &self.scan_result.unmapped {
ui.label(format!(" {}", sample.filename));
}
}
});
// Buttons
ui.add_space(4.0);
ui.separator();
ui.horizontal(|ui| {
if ui.button("Cancel").clicked() {
should_cancel = true;
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let import_text = format!("Import {} layers", enabled_count);
if ui.add_enabled(enabled_count > 0, egui::Button::new(&import_text)).clicked() {
should_import = true;
}
});
});
});
if recalc {
recalc_key_ranges(&mut self.scan_result.layers);
}
if should_import {
self.confirmed = true;
self.should_close = true;
}
if should_cancel || !open {
self.should_close = true;
}
!self.should_close
}
}