diff --git a/assets/locales/de.json b/assets/locales/de.json index 9394a574..50cd7a9d 100644 --- a/assets/locales/de.json +++ b/assets/locales/de.json @@ -27,6 +27,7 @@ "Current perspective view": "Aktuelle perspektivische Ansicht", "Curvature (G2)": "Krümmung (G2)", "Do not export hidden edges": "Verdeckte Kanten nicht exportieren", + "Don't ask again": "Nicht mehr fragen", "Drive Joint": "Gelenk antreiben", "Duration": "Dauer", "Edges": "Kanten", @@ -60,6 +61,7 @@ "Picked point": "Gewählter Punkt", "Plot": "Diagramm", "Position (G0)": "Position (G0)", + "Preferences › Design turns the offer back on": "Unter Einstellungen › Design lässt sich das Angebot wieder einschalten", "Profiles/Curves": "Profile/Kurven", "Projection": "Projektion", "Quantity": "Größe", diff --git a/assets/locales/en.json b/assets/locales/en.json index d23e3cb7..b9897704 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -88,6 +88,7 @@ "Determined by To Object. Choose Angle to edit the angular extent.": "Determined by To Object. Choose Angle to edit the angular extent.", "Discard": "Discard", "Do not export hidden edges": "Do not export hidden edges", + "Don't ask again": "Don't ask again", "Double-click to rename": "Double-click to rename", "Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing", "Drive Joint": "Drive Joint", @@ -182,6 +183,7 @@ "Position (0 to 1)": "Position (0 to 1)", "Position (G0)": "Position (G0)", "Position along the edge, 0 at the start end, 1 at the other": "Position along the edge, 0 at the start end, 1 at the other", + "Preferences › Design turns the offer back on": "Preferences › Design turns the offer back on", "Preview Row": "Preview Row", "Preview leaves the current design unchanged.": "Preview leaves the current design unchanged.", "Profiles/Curves": "Profiles/Curves", diff --git a/assets/locales/ja.json b/assets/locales/ja.json index f12aebd1..a9483a34 100644 --- a/assets/locales/ja.json +++ b/assets/locales/ja.json @@ -88,6 +88,7 @@ "Determined by To Object. Choose Angle to edit the angular extent.": "「対象オブジェクトまで」により決定されます。角度範囲を編集するには「角度」を選択してください。", "Discard": "破棄", "Do not export hidden edges": "出力しない", + "Don't ask again": "今後確認しない", "Double-click to rename": "ダブルクリックで名前変更", "Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "ビュー上で中ボタンをダブルクリックした時の動作: ホーム(全体を収めたホームビュー)、全体表示(向きを維持して全体を収める)、または無効", "Drive Joint": "ジョイントを駆動", @@ -182,6 +183,7 @@ "Position (0 to 1)": "位置 (0〜1)", "Position (G0)": "位置 (G0)", "Position along the edge, 0 at the start end, 1 at the other": "エッジ上の位置(始点が0、終点が1)", + "Preferences › Design turns the offer back on": "環境設定 › デザイン で再びオンにできます", "Preview Row": "行をプレビュー", "Preview leaves the current design unchanged.": "プレビューは現在のデザインを変更しません。", "Profiles/Curves": "プロファイル/曲線", diff --git a/assets/locales/ko.json b/assets/locales/ko.json index 7d7f7d5a..43a55ee1 100644 --- a/assets/locales/ko.json +++ b/assets/locales/ko.json @@ -27,6 +27,7 @@ "Current perspective view": "현재 원근 투영 뷰", "Curvature (G2)": "곡률 (G2)", "Do not export hidden edges": "숨겨진 모서리 내보내지 않기", + "Don't ask again": "다시 묻지 않기", "Drive Joint": "조인트 구동", "Duration": "지속 시간", "Edges": "모서리", @@ -60,6 +61,7 @@ "Picked point": "선택한 점", "Plot": "그래프", "Position (G0)": "위치 (G0)", + "Preferences › Design turns the offer back on": "환경 설정 › 디자인에서 다시 켤 수 있습니다", "Profiles/Curves": "프로파일/곡선", "Projection": "투영", "Quantity": "양", diff --git a/assets/locales/zh.json b/assets/locales/zh.json index 2c6899a0..df2c00d6 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -88,6 +88,7 @@ "Determined by To Object. Choose Angle to edit the angular extent.": "由“到对象”确定。选择“角度”可编辑角度范围。", "Discard": "放弃", "Do not export hidden edges": "不导出隐藏线", + "Don't ask again": "不再询问", "Double-click to rename": "双击重命名", "Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "在视图中双击中键:主视图(主视图,框住所显示的内容)、适合窗口(框住所显示的内容,保持方向)或无动作", "Drive Joint": "驱动接头", @@ -182,6 +183,7 @@ "Position (0 to 1)": "位置(0 到 1)", "Position (G0)": "位置 (G0)", "Position along the edge, 0 at the start end, 1 at the other": "沿边的位置,起点端为 0,另一端为 1", + "Preferences › Design turns the offer back on": "可在 首选项 › 设计 中重新开启此提示", "Preview Row": "预览行", "Preview leaves the current design unchanged.": "预览不会更改当前设计。", "Profiles/Curves": "轮廓/曲线", diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 704b9cc8..ac2cb5bd 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -1438,8 +1438,9 @@ fn colour_vision_sample(ui: &mut egui::Ui, t: &Tokens) { } /// The control channel's `ui.prefs`: `open` (a page name) opens the dialog there, `button` -/// presses OK, Cancel, Apply or Restore Defaults, `stand_up: true` answers the stand-up offer; -/// any other keys set preferences. Returns them all, the open page and the offer. +/// presses OK, Cancel, Apply or Restore Defaults, `stand_up: true` answers the stand-up offer +/// (`false` leaves the file as it is, like "Leave it"); any other keys set preferences. Returns +/// them all, the open page and the offer. pub fn control(app: &mut SolveApp, p: &serde_json::Value) -> Result { let mut rest = p.as_object().cloned().unwrap_or_default(); if let Some(page) = rest.remove("open") { @@ -1450,8 +1451,10 @@ pub fn control(app: &mut SolveApp, p: &serde_json::Value) -> Result crate::stand_up::stand_up(app)?, + Some(false) => crate::stand_up::leave(app), + None => {} } let button = rest.remove("button"); let mut out = set(app, &serde_json::Value::Object(rest))?; diff --git a/crates/ui-egui/src/stand_up.rs b/crates/ui-egui/src/stand_up.rs index 83ba676e..83f24fc8 100644 --- a/crates/ui-egui/src/stand_up.rs +++ b/crates/ui-egui/src/stand_up.rs @@ -36,6 +36,12 @@ pub fn stand_up(app: &mut SolveApp) -> Result<(), String> { Ok(()) } +/// Dismiss the offer and keep the file as it opened ("Leave it"); the preference stays on. +pub fn leave(app: &mut SolveApp) { + app.stand_up = None; + app.stand_up_occurrence = None; +} + /// The offer, under the toolbar. pub fn show(app: &mut SolveApp, ctx: &egui::Context) { if app.stand_up.is_none() { @@ -52,13 +58,21 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { ui.label(crate::i18n::tr_label("Stand it up: a quarter turn about X, as one Move feature you can undo.")); ui.add_space(6.0); ui.horizontal(|ui| { - if ui.button(crate::i18n::tr_label("Stand it up")).clicked() { + let stand = ui.button(crate::i18n::tr_label("Stand it up")); + crate::scenario::publish_handle("stand_up:stand", stand.rect.center()); + if stand.clicked() { act = Some("stand"); } - if ui.button(crate::i18n::tr_label("Leave it")).clicked() { + let leave = ui.button(crate::i18n::tr_label("Leave it")); + crate::scenario::publish_handle("stand_up:leave", leave.rect.center()); + if leave.clicked() { act = Some("leave"); } - if ui.button("Don't ask again").on_hover_text("Preferences › Design turns the offer back on").clicked() { + let never = ui + .button(crate::i18n::tr_label("Don't ask again")) + .on_hover_text(crate::i18n::tr_label("Preferences › Design turns the offer back on")); + crate::scenario::publish_handle("stand_up:never", never.rect.center()); + if never.clicked() { act = Some("never"); } }); @@ -69,13 +83,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { app.set_status(e, true); } } - Some("leave") => { - app.stand_up = None; - app.stand_up_occurrence = None; - } + Some("leave") => leave(app), Some("never") => { - app.stand_up = None; - app.stand_up_occurrence = None; + leave(app); app.preferences.offer_stand_up = false; } _ => {} diff --git a/crates/ui-egui/tests/stand_up_offer.rs b/crates/ui-egui/tests/stand_up_offer.rs new file mode 100644 index 00000000..958afa60 --- /dev/null +++ b/crates/ui-egui/tests/stand_up_offer.rs @@ -0,0 +1,70 @@ +//! The "Stand this part up?" offer can be answered by handle (#1905): its buttons are published +//! as `stand_up:stand`, `stand_up:leave` and `stand_up:never`, it stays up through a camera move +//! and a command until answered, and `ui.prefs {"stand_up": false}` leaves the file as it is. +use serde_json::json; +use solidcraft_ui_egui::scenario::Harness; + +/// A harness with a 10 × 40 × 5 mm box (Y up) just opened from STEP: the offer is up. +fn opened() -> Harness { + let mut src = Harness::new(); + let path = src.dir.join("tall_in_y.step").to_string_lossy().into_owned(); + src.app.run("solid.box", json!({"length": 10, "width": 40, "height": 5})).unwrap(); + src.app.run("file.export", json!({"path": path})).unwrap(); + let mut h = Harness::new(); + h.app.open_path(&path); + h.frames(3); + assert_eq!(h.app.stand_up.as_ref().map(Vec::len), Some(1), "the file's body is offered"); + h +} + +fn height(h: &Harness) -> f64 { + let b = solidcraft_engine::view::bounds(&h.app.session); + b.max.z - b.min.z +} + +#[test] +fn the_offer_buttons_are_handles() { + let mut h = opened(); + for name in ["stand_up:stand", "stand_up:leave", "stand_up:never"] { + assert!(h.at(&json!({"handle": name})).is_ok(), "{name} is published"); + } + // Unanswered, it stays up through a camera move and a command. + h.run(&json!([{"camera": {"yaw": 0.6, "pitch": 0.4}}, {"call": "engine.execute", "params": {"command": "solid.box", "params": {"length": 1, "width": 1, "height": 1}}}])) + .unwrap(); + h.frames(3); + assert!(h.app.stand_up.is_some(), "the offer waits for an answer"); + h.run(&json!([{"click": {"handle": "stand_up:stand"}}])).unwrap(); + h.frames(3); + assert!(h.app.stand_up.is_none()); + assert!((height(&h) - 40.0).abs() < 1e-6, "stood up: {}", height(&h)); +} + +#[test] +fn leave_it_keeps_the_file_and_the_preference() { + let mut h = opened(); + h.run(&json!([{"click": {"handle": "stand_up:leave"}}])).unwrap(); + h.frames(3); + assert!(h.app.stand_up.is_none()); + assert!(h.app.preferences.offer_stand_up); + assert!((height(&h) - 5.0).abs() < 1e-6); + assert!(h.at(&json!({"handle": "stand_up:leave"})).is_err(), "the offer is gone"); +} + +#[test] +fn dont_ask_again_turns_the_offer_off() { + let mut h = opened(); + h.run(&json!([{"click": {"handle": "stand_up:never"}}])).unwrap(); + h.frames(3); + assert!(h.app.stand_up.is_none()); + assert!(!h.app.preferences.offer_stand_up); + assert!((height(&h) - 5.0).abs() < 1e-6); +} + +#[test] +fn prefs_stand_up_false_leaves_the_offer() { + let mut h = opened(); + let r = h.call("ui.prefs", json!({"stand_up": false})); + assert_eq!(r["result"]["stand_up_offer"], json!(null), "{r}"); + assert_eq!(r["result"]["offer_stand_up"], json!(true), "{r}"); + assert!((height(&h) - 5.0).abs() < 1e-6); +} diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 13236705..36ccc8bf 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -152,7 +152,7 @@ labels; `prefs:measure_axis_distances` identifies the checkbox. | `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs | | `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) | | `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (opens a report draft for review), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text | -| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_around` (`view_centre` (default: what Pan moves and Fit frames), `origin` (the world origin stays still on screen), `cursor` (the model point under the pointer when the drag began); the older `orbit_cursor` boolean still sets `cursor` when true), `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `show_frame_rate` (default false: frames per second in the view's bottom-right corner; the view redraws every frame while it is on), `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `ui_font` (an installed font family or a font file for the interface; empty for the built-in font), `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection, AI) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | +| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_around` (`view_centre` (default: what Pan moves and Fit frames), `origin` (the world origin stays still on screen), `cursor` (the model point under the pointer when the drag began); the older `orbit_cursor` boolean still sets `cursor` when true), `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `show_frame_rate` (default false: frames per second in the view's bottom-right corner; the view redraws every frame while it is on), `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `ui_font` (an installed font family or a font file for the interface; empty for the built-in font), `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection, AI) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file, `stand_up: false` leaves it as it is ("Leave it": the offer closes and `offer_stand_up` stays on); the offer's buttons are the handles `stand_up:stand`, `stand_up:leave` and `stand_up:never` | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | | `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits | | `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first | | `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it |