mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 02:16:56 -06:00
M7: Cmd/Ctrl+S saves while a panel field has the keyboard (#1157)
With the keyboard in a Design panel field (X, W, a colour…), the shortcut handler returned before looking at any key, so Cmd/Ctrl+S and Shift+Cmd/Ctrl+S did nothing: no save, no message, the tab kept its unsaved dot. Those two now work from a field: the field lets go of the keyboard, which commits what was typed as clicking away does, and the save runs the next frame.
This commit is contained in:
@@ -370,6 +370,39 @@ fn arithmetic_commits_tab_moves_on_and_escape_restores() {
|
||||
assert_eq!(geom(&u, a).0, 78.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_save_shortcut_saves_from_a_field_with_what_was_typed() {
|
||||
// Cmd/Ctrl+S with the keyboard in a panel field saved nothing (#1157). It commits the field
|
||||
// first (as clicking away does), then saves; the field lets go of the keyboard.
|
||||
let cmd = if cfg!(target_os = "macos") { Modifiers::COMMAND } else { Modifiers::COMMAND.plus(Modifiers::CTRL) };
|
||||
let (mut u, a, _) = setup();
|
||||
let dir = std::env::temp_dir().join(format!("figcraft-save-from-field-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("doc.figcraft");
|
||||
u.app.session.execute("file.saveAs", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
for (shortcut, mods) in [("Cmd/Ctrl+S", cmd), ("Shift+Cmd/Ctrl+S on a file with a path", cmd.plus(Modifiers::SHIFT))] {
|
||||
let (p, _) = field(&mut u, "X");
|
||||
click(&mut u, p);
|
||||
assert!(crate::a11y::field_focused(&u.ctx), "{shortcut}: the field has the keyboard");
|
||||
let x = if mods.shift { "66" } else { "77" };
|
||||
u.frame(vec![Event::Text(x.into())]);
|
||||
key(&mut u, Key::S, mods);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(geom(&u, a).0, x.parse::<f64>().unwrap(), "{shortcut}: what was typed is committed");
|
||||
assert!(!crate::a11y::field_focused(&u.ctx), "{shortcut}: the field let go");
|
||||
if mods.shift {
|
||||
// Save as asks where (a dialog this harness doesn't answer): nothing is written yet.
|
||||
continue;
|
||||
}
|
||||
assert!(!u.app.session.ds().unwrap().is_dirty(), "{shortcut}: saved");
|
||||
let mut back = Session::new();
|
||||
back.execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
let b = back.doc().unwrap().world_bounds(a).unwrap();
|
||||
assert_eq!(b.x0, 77.0, "{shortcut}: the file on disk has the typed value");
|
||||
}
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_values_take_edits_relative_to_each_layer() {
|
||||
let (mut u, a, b) = setup();
|
||||
|
||||
@@ -631,6 +631,9 @@ pub struct UiState {
|
||||
pub paste_event: bool,
|
||||
/// The window got the focus back since the last V release.
|
||||
pub refocused: bool,
|
||||
/// A save shortcut pressed while a panel field had the keyboard: the field lets go (which
|
||||
/// commits what was typed) and the command runs the next frame (`shortcuts::handle`).
|
||||
pub after_field: Option<&'static str>,
|
||||
/// The modifiers held, as of the last modifier change in the raw events: a paste event doesn't
|
||||
/// carry them.
|
||||
pub held: egui::Modifiers,
|
||||
@@ -748,6 +751,7 @@ impl Default for UiState {
|
||||
v_pressed: false,
|
||||
paste_event: false,
|
||||
refocused: false,
|
||||
after_field: None,
|
||||
held: egui::Modifiers::NONE,
|
||||
next_autosave: 0.0,
|
||||
auto_save_at: None,
|
||||
|
||||
@@ -97,8 +97,16 @@ fn cached_tip(label: &'static str, key: Option<&'static str>) -> &'static str {
|
||||
/// `swallowed`: what this frame's Cmd/Ctrl+V chords the shell swallowed ask for ([`swallowed_paste`]).
|
||||
pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, swallowed: Swallowed) {
|
||||
note_platform(ctx);
|
||||
// Text fields in panels own the keyboard (also the key that ends their editing).
|
||||
// A save asked for from a field last frame: the field has committed by now.
|
||||
if let Some(id) = app.ui.after_field.take() {
|
||||
run_binding(app, id);
|
||||
}
|
||||
// Text fields in panels own the keyboard (also the key that ends their editing), except for
|
||||
// the save shortcuts.
|
||||
if typing || crate::a11y::field_focused(ctx) || app.ui.palette.is_some() {
|
||||
if app.ui.palette.is_none() {
|
||||
save_from_field(app, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let events: Vec<Event> = ctx.input(|i| i.events.clone());
|
||||
@@ -137,6 +145,33 @@ pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, swallowed: Swallowe
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands whose shortcut works while a panel field has the keyboard: they don't edit text, and
|
||||
/// a file that can't be saved from a field looks like a file that can't be saved (#1157).
|
||||
const FROM_A_FIELD: [&str; 2] = ["file.save", "file.saveAs"];
|
||||
|
||||
/// Cmd/Ctrl+S (or Shift+Cmd/Ctrl+S) pressed in a panel field: the field lets go of the keyboard,
|
||||
/// which commits what was typed as clicking away does, and the save runs next frame, after that
|
||||
/// commit.
|
||||
fn save_from_field(app: &mut FigApp, ctx: &Context) {
|
||||
let id = ctx.input(|i| {
|
||||
i.events.iter().find_map(|e| match e {
|
||||
Event::Key { key, physical_key, pressed: true, repeat: false, modifiers } if modifiers.command => {
|
||||
lookup(keymap::Context::Canvas, *key, *physical_key, *modifiers).map(|b| b.id).filter(|id| FROM_A_FIELD.contains(id))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(id) = id {
|
||||
ctx.memory_mut(|m| {
|
||||
if let Some(f) = m.focused() {
|
||||
m.surrender_focus(f);
|
||||
}
|
||||
});
|
||||
app.ui.after_field = Some(id);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// The command Cmd/Ctrl+V runs with `m` held too: `edit.paste`, `edit.pasteOver` (Shift),
|
||||
/// `edit.pasteProperties` (Alt), or `None` for a chord nothing is bound to. The keymap decides,
|
||||
/// whether the key press arrived (browser) or only a paste event and the release (desktop).
|
||||
|
||||
Reference in New Issue
Block a user