Parameters: move user parameters up and down (#1323)
Fixes #1187. New parameters.move {name, before|after} reorders user parameters (the design's own params order, so it is saved with no format change, and undoable); the Parameters dialog's user rows get up/down buttons. Label in every catalog. I merged main in. Checked on main + this branch: cargo check --workspace, the engine parameters tests, the UI parameter_order test, and docs/features.md regenerates unchanged.
This commit is contained in:
1 parent
d7a4a29ce3
commit
4dc638109b
14 files changed
+152
-11
No files matched your search
@@ -600,6 +600,7 @@
|
||||
"cmd.parameters_delete": "Parameter löschen",
|
||||
"cmd.parameters_export": "Parameter exportieren",
|
||||
"cmd.parameters_favorite": "Favorisierter Parameter",
|
||||
"cmd.parameters_move": "Parameter verschieben",
|
||||
"cmd.parameters_graph": "Parametergraph",
|
||||
"cmd.parameters_import": "Parameter importieren",
|
||||
"cmd.parameters_list": "Parameter auflisten",
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"cmd.parameters_delete": "Delete Parameter",
|
||||
"cmd.parameters_export": "Export Parameters",
|
||||
"cmd.parameters_favorite": "Favourite Parameter",
|
||||
"cmd.parameters_move": "Move Parameter",
|
||||
"cmd.parameters_graph": "Parameter Graph",
|
||||
"cmd.parameters_import": "Import Parameters",
|
||||
"cmd.parameters_list": "List Parameters",
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"cmd.parameters_delete": "Supprimer le paramètre",
|
||||
"cmd.parameters_export": "Exporter les paramètres",
|
||||
"cmd.parameters_favorite": "Paramètre favori",
|
||||
"cmd.parameters_move": "Déplacer le paramètre",
|
||||
"cmd.parameters_graph": "Graphe des paramètres",
|
||||
"cmd.parameters_import": "Importer des paramètres",
|
||||
"cmd.parameters_list": "Lister les paramètres",
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"cmd.parameters_delete": "パラメータを削除",
|
||||
"cmd.parameters_export": "パラメータをエクスポート",
|
||||
"cmd.parameters_favorite": "お気に入りパラメータ",
|
||||
"cmd.parameters_move": "パラメータを移動",
|
||||
"cmd.parameters_graph": "パラメータ グラフ",
|
||||
"cmd.parameters_import": "パラメータをインポート",
|
||||
"cmd.parameters_list": "パラメータを一覧表示",
|
||||
|
||||
@@ -600,6 +600,7 @@
|
||||
"cmd.parameters_delete": "매개 변수 삭제",
|
||||
"cmd.parameters_export": "매개 변수 내보내기",
|
||||
"cmd.parameters_favorite": "즐겨찾는 매개 변수",
|
||||
"cmd.parameters_move": "매개 변수 이동",
|
||||
"cmd.parameters_graph": "매개 변수 그래프",
|
||||
"cmd.parameters_import": "매개 변수 가져오기",
|
||||
"cmd.parameters_list": "매개 변수 목록",
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"cmd.parameters_delete": "Удалить параметр",
|
||||
"cmd.parameters_export": "Экспортировать параметры",
|
||||
"cmd.parameters_favorite": "Избранный параметр",
|
||||
"cmd.parameters_move": "Переместить параметр",
|
||||
"cmd.parameters_graph": "График параметра",
|
||||
"cmd.parameters_import": "Импортировать параметры",
|
||||
"cmd.parameters_list": "Список параметров",
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"cmd.parameters_delete": "Видалити параметр",
|
||||
"cmd.parameters_export": "Експортувати параметри",
|
||||
"cmd.parameters_favorite": "Обраний параметр",
|
||||
"cmd.parameters_move": "Перемістити параметр",
|
||||
"cmd.parameters_graph": "Граф параметрів",
|
||||
"cmd.parameters_import": "Імпортувати параметри",
|
||||
"cmd.parameters_list": "Список параметрів",
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"cmd.parameters_delete": "删除参数",
|
||||
"cmd.parameters_export": "导出参数",
|
||||
"cmd.parameters_favorite": "收藏参数",
|
||||
"cmd.parameters_move": "移动参数",
|
||||
"cmd.parameters_graph": "参数关系图",
|
||||
"cmd.parameters_import": "导入参数",
|
||||
"cmd.parameters_list": "列出参数",
|
||||
|
||||
@@ -352,6 +352,25 @@ impl Document {
|
||||
rows
|
||||
}
|
||||
|
||||
/// Move user parameter `name` to just before (or with `after`, just after) user parameter
|
||||
/// `target`: the order the Parameters dialog lists them in. Nothing else changes.
|
||||
pub fn move_user_param(&mut self, name: &str, target: &str, after: bool) -> Result<()> {
|
||||
fn user(params: &[crate::Param], n: &str) -> Option<usize> {
|
||||
params.iter().position(|p| p.name == n && !p.model)
|
||||
}
|
||||
let (Some(from), Some(_)) = (user(&self.params, name), user(&self.params, target)) else {
|
||||
let missing = if user(&self.params, name).is_none() { name } else { target };
|
||||
return Err(DocError::Unknown(format!("user parameter `{missing}`")));
|
||||
};
|
||||
if name == target {
|
||||
return Err(DocError::Invalid("a parameter cannot move next to itself".into()));
|
||||
}
|
||||
let moved = self.params.remove(from);
|
||||
let at = user(&self.params, target).map_or(self.params.len(), |i| if after { i + 1 } else { i });
|
||||
self.params.insert(at, moved);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_favorite(&mut self, name: &str, on: bool) -> Result<()> {
|
||||
if !self.name_taken(name) {
|
||||
return Err(DocError::Unknown(format!("parameter `{name}`")));
|
||||
|
||||
@@ -24,6 +24,8 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.params("name, new_name (expressions, feature inputs and dimensions that use it follow)"),
|
||||
CommandSpec::new("parameters.delete", "Delete Parameter", delete).params("name (refused while something uses it; the error lists the users)"),
|
||||
CommandSpec::new("parameters.favorite", "Favourite Parameter", favorite).params("name, favorite?: bool (default true)"),
|
||||
CommandSpec::new("parameters.move", "Move Parameter", move_param)
|
||||
.params("name, before | after: another user parameter; the user parameter is listed just before or after it"),
|
||||
CommandSpec::new("parameters.comment", "Comment Parameter", comment).params("name, comment"),
|
||||
CommandSpec::new("parameters.users", "Parameter Users", users)
|
||||
.noundo()
|
||||
@@ -179,6 +181,19 @@ fn favorite(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"name": name, "favorite": on}))
|
||||
}
|
||||
|
||||
fn move_param(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "parameters.move";
|
||||
let name = param_name(cmd, p)?.to_string();
|
||||
let (target, after) = match (str_(p, "before"), str_(p, "after")) {
|
||||
(Some(t), None) => (t.to_string(), false),
|
||||
(None, Some(t)) => (t.to_string(), true),
|
||||
_ => return Err(bad(cmd, "give one of `before` or `after`: another user parameter")),
|
||||
};
|
||||
s.doc_mut().move_user_param(&name, &target, after)?;
|
||||
let order: Vec<&str> = s.doc.params.iter().filter(|q| !q.model).map(|q| q.name.as_str()).collect();
|
||||
Ok(json!({"name": name, "order": order}))
|
||||
}
|
||||
|
||||
fn comment(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "parameters.comment";
|
||||
let name = param_name(cmd, p)?.to_string();
|
||||
|
||||
@@ -544,3 +544,52 @@ fn switching_configurations_back_reuses_the_later_features() {
|
||||
assert!(rel(volume(&mut s), 40.0 * 30.0 * 10.0) < 1e-9);
|
||||
assert!(hits(&s) > before, "{r}");
|
||||
}
|
||||
|
||||
/// The user parameters' names in the order the Parameters dialog lists them.
|
||||
fn user_order(s: &mut Session) -> Vec<String> {
|
||||
let v = run(s, "parameters.list", json!({"filter": "user"}));
|
||||
v["parameters"].as_array().unwrap().iter().map(|r| r["name"].as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_parameters_reorder_with_undo() {
|
||||
// #1187: move a user parameter before or after another; nothing else changes.
|
||||
let mut s = Session::default();
|
||||
for (n, e) in [("a", "1 mm"), ("b", "2 mm"), ("c", "a + b")] {
|
||||
run(&mut s, "parameters.add", json!({"name": n, "expression": e}));
|
||||
}
|
||||
assert_eq!(user_order(&mut s), ["a", "b", "c"]);
|
||||
let r = run(&mut s, "parameters.move", json!({"name": "c", "before": "a"}));
|
||||
assert_eq!(r["order"], json!(["c", "a", "b"]));
|
||||
assert_eq!(user_order(&mut s), ["c", "a", "b"]);
|
||||
run(&mut s, "parameters.move", json!({"name": "c", "after": "a"}));
|
||||
assert_eq!(user_order(&mut s), ["a", "c", "b"]);
|
||||
// Values and references are untouched; the order is kept in the design file.
|
||||
let list = run(&mut s, "parameters.list", json!({"filter": "user"}));
|
||||
let c = list["parameters"].as_array().unwrap().iter().find(|r| r["name"] == "c").unwrap().clone();
|
||||
assert_eq!(c["expression"], "a + b");
|
||||
assert!((c["value"].as_f64().unwrap() - 3.0).abs() < 1e-9, "{c}");
|
||||
let back = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
let names: Vec<&str> = back.doc.params.iter().filter(|p| !p.model).map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["a", "c", "b"]);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(user_order(&mut s), ["c", "a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_a_parameter_refuses_bad_targets() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "a", "expression": "1 mm"}));
|
||||
run(&mut s, "parameters.add", json!({"name": "b", "expression": "2 mm"}));
|
||||
let before = s.doc.to_json();
|
||||
for p in [
|
||||
json!({"name": "a", "before": "a"}),
|
||||
json!({"name": "a", "before": "nope"}),
|
||||
json!({"name": "nope", "before": "a"}),
|
||||
json!({"name": "a"}),
|
||||
json!({"name": "a", "before": "b", "after": "b"}),
|
||||
] {
|
||||
err(&mut s, "parameters.move", p);
|
||||
}
|
||||
assert_eq!(s.doc.to_json(), before);
|
||||
}
|
||||
@@ -226,12 +226,30 @@ fn table(app: &mut SolveApp, ui: &mut egui::Ui, d: &mut ParamsDialog, rows: &[&R
|
||||
ui.label(RichText::new(h).color(t.text_dim).small());
|
||||
}
|
||||
ui.end_row();
|
||||
for r in rows {
|
||||
// Favourite star.
|
||||
let star = if r.favorite { RichText::new("★").color(t.accent) } else { RichText::new("☆").color(t.text_dim) };
|
||||
if ui.add(egui::Button::new(star).frame(false)).on_hover_text(crate::i18n::tr_label("Favourite")).clicked() {
|
||||
d.error = app.run("parameters.favorite", json!({"name": r.name, "favorite": !r.favorite})).err();
|
||||
}
|
||||
for (i, r) in rows.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
// Favourite star.
|
||||
let star = if r.favorite { RichText::new("★").color(t.accent) } else { RichText::new("☆").color(t.text_dim) };
|
||||
if ui.add(egui::Button::new(star).frame(false)).on_hover_text(crate::i18n::tr_label("Favourite")).clicked() {
|
||||
d.error = app.run("parameters.favorite", json!({"name": r.name, "favorite": !r.favorite})).err();
|
||||
}
|
||||
// Up and down past the neighbouring user parameter (#1187), in the user table only:
|
||||
// the favourites table lists favourites first whatever their order.
|
||||
if salt == "user" {
|
||||
for (glyph, tip, neighbour, side) in
|
||||
[("▲", "Move up", i.checked_sub(1).and_then(|j| rows.get(j)), "before"), ("▼", "Move down", rows.get(i + 1), "after")]
|
||||
{
|
||||
let button = egui::Button::new(RichText::new(glyph).small().color(t.text_dim)).frame_when_inactive(false);
|
||||
let resp = ui.add_enabled(neighbour.is_some(), button).on_hover_text(crate::i18n::tr_label(tip));
|
||||
crate::scenario::publish_handle(&format!("param:{}:{}", side, r.name), resp.rect.center());
|
||||
if resp.clicked()
|
||||
&& let Some(n) = neighbour
|
||||
{
|
||||
d.error = app.run("parameters.move", json!({"name": r.name, side: n.name})).err();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Name: double-click to rename.
|
||||
match &mut d.renaming {
|
||||
Some((old, new)) if *old == r.name => {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//! The Parameters dialog's ▲ / ▼ buttons reorder user parameters (#1187).
|
||||
use serde_json::json;
|
||||
use solidcraft_ui_egui::scenario::Harness;
|
||||
|
||||
fn order(h: &mut Harness) -> Vec<String> {
|
||||
let v = h.app.session.execute("parameters.list", &json!({"filter": "user"})).unwrap();
|
||||
v["parameters"].as_array().unwrap().iter().map(|r| r["name"].as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrows_move_user_parameters_up_and_down() {
|
||||
let mut h = Harness::new();
|
||||
for (n, e) in [("a", "1 mm"), ("b", "2 mm"), ("c", "3 mm")] {
|
||||
h.app.session.execute("parameters.add", &json!({"name": n, "expression": e})).unwrap();
|
||||
}
|
||||
h.step(&json!({"widget":"Parameters"})).unwrap();
|
||||
assert!(h.app.params.is_some());
|
||||
h.frames(3);
|
||||
h.run(&json!([{"click":{"handle":"param:after:a"}}])).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(order(&mut h), ["b", "a", "c"]);
|
||||
h.run(&json!([{"click":{"handle":"param:before:c"}}])).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(order(&mut h), ["b", "c", "a"]);
|
||||
// The first row cannot move up, the last cannot move down.
|
||||
h.run(&json!([{"click":{"handle":"param:before:b"}}, {"click":{"handle":"param:after:a"}}])).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(order(&mut h), ["b", "c", "a"]);
|
||||
assert!(h.app.params.as_ref().unwrap().error.is_none());
|
||||
}
|
||||
+7
-5
@@ -1,6 +1,6 @@
|
||||
# Features
|
||||
|
||||
SolidCraft's commands by area, generated by `cargo xtask features` from the command registry (`solidcraft-cli commands`). **365 commands.** Planned (registered, not built yet): 4. Every command runs the same way from the toolbar, a script, `solidcraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).
|
||||
SolidCraft's commands by area, generated by `cargo xtask features` from the command registry (`solidcraft-cli commands`). **367 commands.** Planned (registered, not built yet): 4. Every command runs the same way from the toolbar, a script, `solidcraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).
|
||||
|
||||
## Sketch (99)
|
||||
|
||||
@@ -315,7 +315,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
| Measure (I) | `inspect.measure` | SOLID › INSPECT |
|
||||
| Section Analysis | `inspect.section` | SOLID › INSPECT |
|
||||
|
||||
## Files (7)
|
||||
## Files (8)
|
||||
|
||||
| Command | Id | Toolbar |
|
||||
|---|---|---|
|
||||
@@ -325,7 +325,8 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
| Save (Ctrl+S) | `file.save` | – |
|
||||
| Save As | `file.save_as` | – |
|
||||
| Export | `file.export` | – |
|
||||
| Save As Mesh | `file.save_mesh` | – |
|
||||
| Save As Mesh | `file.save_mesh` | MESH › MODIFY |
|
||||
| Installed Slicers | `file.slicers` | – |
|
||||
|
||||
## Documents (4)
|
||||
|
||||
@@ -462,7 +463,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
|
||||
| Command | Id | Toolbar |
|
||||
|---|---|---|
|
||||
| Convert Mesh | `mesh.convert` | MESH › MODIFY |
|
||||
| Convert Mesh | `mesh.convert` | – |
|
||||
|
||||
## `model.*` (3)
|
||||
|
||||
@@ -491,7 +492,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
| Move Occurrence | `occurrence.move` | – |
|
||||
| Ground | `occurrence.ground` | – |
|
||||
|
||||
## `parameters.*` (12)
|
||||
## `parameters.*` (13)
|
||||
|
||||
| Command | Id | Toolbar |
|
||||
|---|---|---|
|
||||
@@ -501,6 +502,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
| Rename Parameter | `parameters.rename` | – |
|
||||
| Delete Parameter | `parameters.delete` | – |
|
||||
| Favourite Parameter | `parameters.favorite` | – |
|
||||
| Move Parameter | `parameters.move` | – |
|
||||
| Comment Parameter | `parameters.comment` | – |
|
||||
| Parameter Users | `parameters.users` | – |
|
||||
| Parameter Graph | `parameters.graph` | – |
|
||||
|
||||
Reference in new issue
Block a user