Merge pull request #160 from grovebit/menu-fixes
Clip > Rename opens its dialog; Undo and Redo name their step; the Window menu's keys work
This commit is contained in:
commit
48c3f65e12
|
|
@ -113,6 +113,11 @@ impl NativeMenu {
|
|||
self.last_sync = Some(now);
|
||||
for entry in &mut self.entries {
|
||||
let (enabled, checked) = menus::item_state(app, &entry.path, entry.command.as_deref());
|
||||
if let Some(label) = menus::live_label(app, entry.command.as_deref())
|
||||
&& entry.item.text() != label
|
||||
{
|
||||
entry.item.set_text(label);
|
||||
}
|
||||
if enabled != entry.enabled {
|
||||
entry.item.set_enabled(enabled);
|
||||
entry.enabled = enabled;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ impl Dialogs {
|
|||
let s = soundcraft_engine::score::ScoreSetup::from_session(e.session());
|
||||
Dialog::ScoreSetup { title: s.title, composer: s.composer, bars_per_system: s.bars_per_system, show_track_names: s.show_track_names }
|
||||
}
|
||||
"clip.rename" => {
|
||||
let target = soundcraft_engine::cmd::clip_ids_param(e, &json!({})).first().and_then(|id| e.session().find_clip(*id));
|
||||
let Some((_, clip)) = target else { return false };
|
||||
Dialog::RenameClip { id: clip.id, name: clip.name.clone() }
|
||||
}
|
||||
_ => match path_dialog(id, &home, &name) {
|
||||
Some(d) => d,
|
||||
None => return false,
|
||||
|
|
@ -327,15 +332,13 @@ pub fn show(app: &mut SoundApp, ctx: &egui::Context) {
|
|||
}
|
||||
}
|
||||
Dialog::RenameTrack { id, name } => {
|
||||
let r = ui.text_edit_singleline(name);
|
||||
r.request_focus();
|
||||
name_field(ui, name);
|
||||
if buttons(ui, "OK", enter) {
|
||||
action = Some(("track.rename".into(), json!({"track": id.0, "name": name})));
|
||||
}
|
||||
}
|
||||
Dialog::RenameClip { id, name } => {
|
||||
let r = ui.text_edit_singleline(name);
|
||||
r.request_focus();
|
||||
name_field(ui, name);
|
||||
if buttons(ui, "OK", enter) {
|
||||
action = Some(("clip.rename".into(), json!({"clip": id.0, "name": name})));
|
||||
}
|
||||
|
|
@ -517,6 +520,17 @@ fn buttons(ui: &mut egui::Ui, ok: &str, enter: bool) -> bool {
|
|||
pressed
|
||||
}
|
||||
|
||||
/// A name field that takes the focus with its text selected, so typing replaces the old name.
|
||||
fn name_field(ui: &mut egui::Ui, name: &mut String) {
|
||||
let mut out = egui::TextEdit::singleline(name).show(ui);
|
||||
if !out.response.has_focus() {
|
||||
out.response.request_focus();
|
||||
let all = egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(name.chars().count()));
|
||||
out.state.cursor.set_char_range(Some(all));
|
||||
out.state.store(ui.ctx(), out.response.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The command the tempo-change dialog runs on OK.
|
||||
fn tempo_change_action(at: Samples, bpm: f64) -> (String, Value) {
|
||||
("event.tempo".into(), json!({"bpm": bpm, "at": at}))
|
||||
|
|
@ -596,6 +610,18 @@ mod tests {
|
|||
assert!(matches!(d.open, Some(Dialog::ScoreSetup { bars_per_system: 4, .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clip_rename_opens_the_rename_dialog_for_the_selected_clip() {
|
||||
let mut e = soundcraft_engine::demo::demo_engine();
|
||||
let mut d = Dialogs::default();
|
||||
e.execute("edit.select_none", &json!({})).unwrap();
|
||||
assert!(!d.open_for_command(&e, "clip.rename"));
|
||||
let clip = e.session().track_by_name("Kick").unwrap().clips()[0].clone();
|
||||
e.execute("edit.select", &json!({"clips": [clip.id.0]})).unwrap();
|
||||
assert!(d.open_for_command(&e, "clip.rename"));
|
||||
assert!(matches!(d.open, Some(Dialog::RenameClip { id, ref name }) if id == clip.id && *name == clip.name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tempo_change_dialog_runs_a_real_command_at_its_position() {
|
||||
let (cmd, params) = tempo_change_action(96_000, 133.5);
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ const DIALOG_COMMANDS: &[&str] = &[
|
|||
"event.quantize",
|
||||
"event.transpose",
|
||||
"edit.strip_silence",
|
||||
"clip.rename",
|
||||
];
|
||||
|
||||
/// Shared menu hierarchy for egui and native desktop hosts.
|
||||
|
|
@ -262,8 +263,9 @@ fn menu_node(app: &mut SoundApp, ui: &mut egui::Ui, n: &MenuNode, extra: &[(&str
|
|||
}
|
||||
let id = command_for_path(&n.path, extra);
|
||||
let (enabled, checked) = item_state(app, &n.path, id.as_deref());
|
||||
let label = if checked { format!("✔ {}", n.label) } else { n.label.clone() };
|
||||
let shortcut = id.as_deref().and_then(soundcraft_engine::find_command).and_then(|c| c.shortcut).unwrap_or("");
|
||||
let label = live_label(app, id.as_deref()).unwrap_or_else(|| n.label.clone());
|
||||
let label = if checked { format!("✔ {label}") } else { label };
|
||||
let shortcut = id.as_deref().and_then(shortcut_for).unwrap_or("");
|
||||
let btn = egui::Button::new(label).shortcut_text(crate::shortcuts::shortcut_label(shortcut, ui.ctx().os().is_mac()));
|
||||
if ui.add_enabled(enabled, btn).clicked()
|
||||
&& let Some(id) = id
|
||||
|
|
@ -394,6 +396,21 @@ const MENU_WINDOWS: &[(&str, &str)] = &[
|
|||
("Event > MIDI Real-Time Properties", "window.rtp"),
|
||||
];
|
||||
|
||||
/// The shortcut of an engine or window command.
|
||||
pub fn shortcut_for(id: &str) -> Option<&'static str> {
|
||||
soundcraft_engine::find_command(id).map_or_else(|| UI_COMMANDS.iter().find(|c| c.0 == id).and_then(|c| c.3), |c| c.shortcut)
|
||||
}
|
||||
|
||||
/// Undo and Redo name the step they would undo or redo; other items keep their catalog label.
|
||||
pub fn live_label(app: &SoundApp, id: Option<&str>) -> Option<String> {
|
||||
let e = &app.engine;
|
||||
match id? {
|
||||
"edit.undo" => Some(e.undo_label().map_or_else(|| "Can't Undo".into(), |l| format!("Undo {l}"))),
|
||||
"edit.redo" => Some(e.redo_label().map_or_else(|| "Can't Redo".into(), |l| format!("Redo {l}"))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Menu-style invocation: may open a dialog.
|
||||
pub fn invoke_menu(app: &mut SoundApp, id: &str, path: &str) {
|
||||
if let Some((_, w)) = MENU_WINDOWS.iter().find(|(p, _)| *p == path)
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ pub fn handle(app: &mut SoundApp, ctx: &egui::Context) {
|
|||
continue;
|
||||
}
|
||||
let _ = app.run(id, json!({}));
|
||||
} else if let Some(c) =
|
||||
crate::menus::UI_COMMANDS.iter().find(|c| c.3.and_then(parse).is_some_and(|(m, k)| k == key && mods_match(m, mods, mac)))
|
||||
{
|
||||
let _ = app.run(c.0, json!({}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +112,7 @@ fn fixed(app: &mut SoundApp, key: Key, m: Modifiers, mac: bool) -> bool {
|
|||
let _ = app.run("window.search", json!({}));
|
||||
true
|
||||
}
|
||||
Key::Equals if m.command => {
|
||||
Key::Equals if m.command && !(mac && m.ctrl) => {
|
||||
let _ = app.run("window.toggle_mix_edit", json!({}));
|
||||
true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -493,10 +493,13 @@ pub fn shortcuts_window(app: &mut SoundApp, ctx: &egui::Context) {
|
|||
win(ctx, &mut open, "Keyboard Shortcuts", vec2(520.0, 480.0), |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
egui::Grid::new("sc").num_columns(3).striped(true).show(ui, |ui| {
|
||||
for c in soundcraft_engine::command_specs().iter().filter(|c| c.shortcut.is_some()) {
|
||||
ui.label(egui::RichText::new(crate::shortcuts::shortcut_label(c.shortcut.unwrap_or(""), mac)).font(mono(11.0)));
|
||||
ui.label(c.label);
|
||||
ui.label(egui::RichText::new(c.menu.join(" › ")).small());
|
||||
let engine = soundcraft_engine::command_specs().iter().map(|c| (c.shortcut, c.label, c.menu.join(" › ")));
|
||||
let windows = crate::menus::UI_COMMANDS.iter().map(|c| (c.3, c.1, c.2.replace(" > ", " › ")));
|
||||
for (shortcut, label, menu) in engine.chain(windows) {
|
||||
let Some(shortcut) = shortcut else { continue };
|
||||
ui.label(egui::RichText::new(crate::shortcuts::shortcut_label(shortcut, mac)).font(mono(11.0)));
|
||||
ui.label(label);
|
||||
ui.label(egui::RichText::new(menu).small());
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
use serde_json::json;
|
||||
use soundcraft_ui_egui::{Services, SoundApp, menus};
|
||||
|
||||
#[test]
|
||||
fn undo_and_redo_name_the_step_they_would_take() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let label = |app: &SoundApp, id| menus::live_label(app, Some(id));
|
||||
assert_eq!(label(&app, "edit.undo").as_deref(), Some("Can't Undo"));
|
||||
app.run("mix.mute", json!({"track": "Kick"})).unwrap();
|
||||
assert_eq!(label(&app, "edit.undo").as_deref(), Some("Undo Mute"));
|
||||
assert_eq!(label(&app, "edit.redo").as_deref(), Some("Can't Redo"));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(label(&app, "edit.redo").as_deref(), Some("Redo Mute"));
|
||||
assert_eq!(label(&app, "edit.copy"), None);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
use egui::{Event, Key, Modifiers, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
use soundcraft_ui_egui::{Services, SoundApp, menus};
|
||||
|
||||
#[test]
|
||||
fn typing_in_the_rename_clip_dialog_replaces_the_old_name() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let clip = app.engine.session().track_by_name("Kick").unwrap().clips()[0].clone();
|
||||
app.run("edit.select", json!({"clips": [clip.id.0]})).unwrap();
|
||||
menus::invoke_menu(&mut app, "clip.rename", "Clip > Rename...");
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(
|
||||
|ui, app: &mut SoundApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
for _ in 0..3 {
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::Text("Intro".into()));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
let session = h.state().engine.session();
|
||||
let renamed = session.tracks.iter().flat_map(|t| t.clips()).find(|c| c.id == clip.id).map(|c| c.name.clone());
|
||||
assert_eq!(renamed.as_deref(), Some("Intro"));
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
use egui::os::OperatingSystem;
|
||||
use egui::{Key, Modifiers};
|
||||
use soundcraft_ui_egui::{MainWindow, Services, SoundApp, shortcuts};
|
||||
|
||||
const MAC_CMD: Modifiers = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };
|
||||
|
||||
fn press(ctx: &egui::Context, app: &mut SoundApp, key: Key, modifiers: Modifiers) {
|
||||
let events = [true, false].map(|pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }).to_vec();
|
||||
ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| shortcuts::handle(app, ui.ctx())).textures_delta.clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_shortcuts_open_and_close_windows() {
|
||||
let pc_ctrl = Modifiers { ctrl: true, command: true, ..Modifiers::NONE };
|
||||
for (os, cmd) in [(OperatingSystem::Mac, MAC_CMD), (OperatingSystem::Windows, pc_ctrl), (OperatingSystem::Nix, pc_ctrl)] {
|
||||
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_os(os);
|
||||
for key in [Key::Num1, Key::Num3, Key::Num4, Key::Num5, Key::Num9] {
|
||||
press(&ctx, &mut app, key, cmd);
|
||||
}
|
||||
press(&ctx, &mut app, Key::M, Modifiers { alt: true, ..cmd });
|
||||
let ui = &app.ui;
|
||||
assert!(
|
||||
ui.show_transport && ui.show_big_counter && ui.show_automation && ui.show_memory_locations && ui.show_video && ui.narrow_mix,
|
||||
"{os:?}"
|
||||
);
|
||||
press(&ctx, &mut app, Key::W, cmd);
|
||||
assert!(!app.ui.show_transport && !app.ui.show_big_counter && !app.ui.show_memory_locations, "{os:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_ctrl_equals_opens_the_score_editor_on_a_mac() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_os(OperatingSystem::Mac);
|
||||
press(&ctx, &mut app, Key::Equals, Modifiers { ctrl: true, ..MAC_CMD });
|
||||
assert!(app.ui.show_score);
|
||||
assert_eq!(app.ui.window, MainWindow::Edit);
|
||||
press(&ctx, &mut app, Key::Equals, MAC_CMD);
|
||||
assert_eq!(app.ui.window, MainWindow::Mix);
|
||||
}
|
||||
Loading…
Reference in New Issue