forked from 1b-agent/fieldcraft-upstream
ui: File › New starts an empty model (file.new, #144); an unknown sample name opens the samples (#200)
file.new replaces the session with an empty one: no design, mesh, definitions, results or journal, nothing to save, the undo history empty. The File menu and Cmd/Ctrl+N run it, asking first over unsaved changes. A sample name the app does not know (a typo, an old ?sample= link) opens the samples to choose from, and the status names it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
92b6225a72
commit
0625125ed7
7 files changed
+81
-5
No files matched your search
@@ -248,6 +248,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
crate::files::Replace::File(p) => p.clone(),
|
||||
crate::files::Replace::Sample(n) => format!("sample {n}"),
|
||||
crate::files::Replace::Update => "update".to_string(),
|
||||
crate::files::Replace::New => "new".to_string(),
|
||||
}}),
|
||||
crate::files::Prompt::Recover => json!("recover"),
|
||||
crate::files::Prompt::Samples => json!("samples"),
|
||||
|
||||
@@ -359,6 +359,8 @@ pub enum Replace {
|
||||
Sample(&'static str),
|
||||
/// Restarting to finish installing an update ([`crate::updates`]).
|
||||
Update,
|
||||
/// An empty model (`file.new`).
|
||||
New,
|
||||
}
|
||||
|
||||
impl Replace {
|
||||
@@ -367,6 +369,7 @@ impl Replace {
|
||||
Replace::File(p) => file_name(p),
|
||||
Replace::Sample(n) => fieldcraft_engine::sample::find(n).map_or_else(|| (*n).to_string(), |s| format!("the {} sample", s.label)),
|
||||
Replace::Update => "the update".into(),
|
||||
Replace::New => "a new model".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -612,7 +615,9 @@ impl ModelApp {
|
||||
/// would discard unsaved changes.
|
||||
pub fn open_sample(&mut self, name: &str) {
|
||||
let Some(sample) = fieldcraft_engine::sample::find(name) else {
|
||||
self.set_status(format!("no sample `{name}`"), true);
|
||||
// An unknown name (a typo, an old `?sample=` link): the samples to choose from (#200).
|
||||
self.set_status(format!("There is no sample `{name}`: choose one of these"), true);
|
||||
self.prompt = Some(Prompt::Samples);
|
||||
return;
|
||||
};
|
||||
if self.has_unsaved_work() {
|
||||
@@ -622,6 +627,23 @@ impl ModelApp {
|
||||
self.load_sample(sample.name);
|
||||
}
|
||||
|
||||
/// File › New (`file.new`): start an empty model, asking first when that would discard
|
||||
/// unsaved changes.
|
||||
pub fn new_model(&mut self) {
|
||||
if self.has_unsaved_work() {
|
||||
self.prompt = Some(Prompt::Unsaved(Replace::New));
|
||||
return;
|
||||
}
|
||||
self.load_new();
|
||||
}
|
||||
|
||||
fn load_new(&mut self) {
|
||||
if self.run("file.new", json!({})).is_ok() {
|
||||
self.model_changed();
|
||||
self.set_status("New model", false);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_sample(&mut self, name: &str) {
|
||||
let Some(sample) = fieldcraft_engine::sample::find(name) else { return };
|
||||
if self.run("file.sample", json!({"name": sample.name})).is_ok() {
|
||||
@@ -897,6 +919,7 @@ impl ModelApp {
|
||||
}
|
||||
}
|
||||
Replace::Sample(name) => self.load_sample(name),
|
||||
Replace::New => self.load_new(),
|
||||
Replace::Update => self.close_to_update(),
|
||||
}
|
||||
}
|
||||
@@ -916,6 +939,7 @@ impl ModelApp {
|
||||
}
|
||||
"file.recover" | "file.recovery.list" => self.prompt = Some(Prompt::Recover),
|
||||
"file.sample" => self.prompt = Some(Prompt::Samples),
|
||||
"file.new" => self.new_model(),
|
||||
"diagnostics.bug_report" | "diagnostics.crash_reports" | "diagnostics.crash_report" => self.report_bug(None),
|
||||
"solve.run" => self.solve_all(),
|
||||
"deck.write" if can_save => match preset.get("format").and_then(Value::as_str) {
|
||||
@@ -1208,6 +1232,9 @@ pub fn file_menu(app: &mut ModelApp, ui: &mut egui::Ui) {
|
||||
let key = |k: &str| crate::appbar::shortcut(k);
|
||||
let row = |ui: &mut egui::Ui, label: &str, right: Option<&str>| crate::frame::menu_row(ui, &t, label, right, true).clicked();
|
||||
let mut act: Option<Box<dyn FnOnce(&mut ModelApp)>> = None;
|
||||
if row(ui, "New", Some(&key("N"))) {
|
||||
act = Some(Box::new(|app| app.new_model()));
|
||||
}
|
||||
if row(ui, "Open…", Some(&key("O"))) {
|
||||
act = Some(Box::new(|app| app.open_dialog(None)));
|
||||
}
|
||||
@@ -1528,6 +1555,17 @@ mod tests {
|
||||
assert_eq!(app.prompt, Some(Prompt::Unsaved(Replace::Sample("impact"))));
|
||||
app.answer_unsaved(Answer::Discard);
|
||||
assert!(app.session.model.step("Impact").is_some());
|
||||
// An unknown sample name opens the samples to choose from, and says why.
|
||||
app.open_sample("crank-armm");
|
||||
assert_eq!(app.prompt, Some(Prompt::Samples));
|
||||
assert!(app.status.as_ref().is_some_and(|s| s.error && s.text.contains("crank-armm")), "{:?}", app.status);
|
||||
app.prompt = None;
|
||||
// File › New over unsaved changes asks first too, then empties the model.
|
||||
app.run("mesh.node.add", serde_json::json!({"label": 900000, "xyz": [0, 0, 0]})).unwrap();
|
||||
app.start("file.new").unwrap();
|
||||
assert_eq!(app.prompt, Some(Prompt::Unsaved(Replace::New)));
|
||||
app.answer_unsaved(Answer::Discard);
|
||||
assert!(app.session.model.nodes().is_empty() && !app.session.is_dirty());
|
||||
// A model without steps says so instead of solving nothing.
|
||||
let mut empty = ModelApp::new(Session::default());
|
||||
empty.solve_all();
|
||||
|
||||
@@ -1758,12 +1758,13 @@ impl ModelApp {
|
||||
let shortcut = |modifiers, key| egui::KeyboardShortcut::new(modifiers, key);
|
||||
let shift = egui::Modifiers::COMMAND | egui::Modifiers::SHIFT;
|
||||
// Shift first: the plain shortcut matches it too.
|
||||
let (redo, undo, save_as, save, open) = ctx.input_mut(|i| {
|
||||
let (redo, undo, save_as, save, open, new) = ctx.input_mut(|i| {
|
||||
let redo = i.consume_shortcut(&shortcut(shift, egui::Key::Z)) || i.consume_shortcut(&shortcut(egui::Modifiers::COMMAND, egui::Key::Y));
|
||||
let undo = i.consume_shortcut(&shortcut(egui::Modifiers::COMMAND, egui::Key::Z));
|
||||
let save_as = i.consume_shortcut(&shortcut(shift, egui::Key::S));
|
||||
let save = i.consume_shortcut(&shortcut(egui::Modifiers::COMMAND, egui::Key::S));
|
||||
(redo, undo, save_as, save, i.consume_shortcut(&shortcut(egui::Modifiers::COMMAND, egui::Key::O)))
|
||||
let open = i.consume_shortcut(&shortcut(egui::Modifiers::COMMAND, egui::Key::O));
|
||||
(redo, undo, save_as, save, open, i.consume_shortcut(&shortcut(egui::Modifiers::COMMAND, egui::Key::N)))
|
||||
});
|
||||
if redo {
|
||||
let _ = self.run("edit.redo", json!({}));
|
||||
@@ -1777,6 +1778,8 @@ impl ModelApp {
|
||||
}
|
||||
if open {
|
||||
self.open_dialog(None);
|
||||
} else if new {
|
||||
self.new_model();
|
||||
}
|
||||
// Delete (or Backspace) deletes the definition selected in the browser, after asking.
|
||||
if self.prompt.is_none()
|
||||
|
||||
Reference in new issue
Block a user