diff --git a/assets/locales/pending/value-boxes-in-view.json b/assets/locales/pending/value-boxes-in-view.json new file mode 100644 index 0000000000..7455761177 --- /dev/null +++ b/assets/locales/pending/value-boxes-in-view.json @@ -0,0 +1,34 @@ +{ + "ar": { + "prefs.value_boxes_in_view": "إظهار القيم بجانب مقابضها في العرض", + "prefs.value_boxes_in_view_tip": "قيمة الأمر (المسافة، نصف القطر…) في مربع بجانب مقبضه، فوق النموذج. عند الإيقاف: تكتبها في نافذة الأمر، التي تتلقى لوحة المفاتيح بعد التحديد، ولا شيء يغطي النموذج." + }, + "en": { + "prefs.value_boxes_in_view": "Show values beside their handles in the view", + "prefs.value_boxes_in_view_tip": "A command's value (Distance, Radius…) in a box next to its handle, over the model. Off: you type it in the command window, which takes the keyboard after a pick, and nothing covers the model." + }, + "de": { + "prefs.value_boxes_in_view": "Werte in der Ansicht neben ihren Griffen zeigen", + "prefs.value_boxes_in_view_tip": "Der Wert eines Befehls (Abstand, Radius …) in einem Feld neben seinem Griff, über dem Modell. Aus: Sie geben ihn im Befehlsfenster ein, das nach einer Auswahl die Tastatur erhält, und nichts verdeckt das Modell." + }, + "he": { + "prefs.value_boxes_in_view": "הצגת ערכים ליד הידיות שלהם בתצוגה", + "prefs.value_boxes_in_view_tip": "ערך הפקודה (מרחק, רדיוס…) בתיבה ליד הידית שלו, מעל המודל. כבוי: מקלידים אותו בחלון הפקודה, שמקבל את המקלדת אחרי בחירה, ושום דבר לא מסתיר את המודל." + }, + "ja": { + "prefs.value_boxes_in_view": "ビューでハンドルの横に値を表示", + "prefs.value_boxes_in_view_tip": "コマンドの値(距離、半径など)をモデル上のハンドルの横のボックスに表示します。オフにすると、値はコマンドウィンドウに入力します。選択後はそのウィンドウにキーボード入力が入り、モデルを覆うものはありません。" + }, + "ko": { + "prefs.value_boxes_in_view": "뷰에서 핸들 옆에 값 표시", + "prefs.value_boxes_in_view_tip": "명령의 값(거리, 반경 등)을 모델 위 핸들 옆 상자에 표시합니다. 끄면 값은 명령 창에 입력하며, 선택 후 키보드 입력이 그 창으로 가고 모델을 가리는 것이 없습니다." + }, + "vi": { + "prefs.value_boxes_in_view": "Hiện giá trị cạnh tay nắm trong khung nhìn", + "prefs.value_boxes_in_view_tip": "Giá trị của lệnh (Khoảng cách, Bán kính…) trong một ô cạnh tay nắm, phía trên mô hình. Tắt: bạn nhập giá trị trong cửa sổ lệnh, cửa sổ này nhận bàn phím sau khi chọn, và không có gì che mô hình." + }, + "zh": { + "prefs.value_boxes_in_view": "在视图中于手柄旁显示数值", + "prefs.value_boxes_in_view_tip": "命令的数值(距离、半径等)显示在模型上方手柄旁的框中。关闭后,在命令窗口中输入数值,选取后键盘输入会进入该窗口,模型不会被遮挡。" + } +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index f6ac713955..8a7c928596 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -349,7 +349,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj crate::dialogs_move::step_value_field(&mut d, if back { -1 } else { 1 }); d.focus = true; } - let focus = std::mem::take(&mut d.focus); + let focus = take_focus(ui.ctx(), &mut d); if let Some(bs) = at.and_then(|base| proj.to_screen(base)) && let Some((label, _, value)) = d.primary() { @@ -541,7 +541,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj, Kind::SketchModify(crate::sketch_modify_ui::Mod::Offset { mode, .. }) => !crate::sketch_modify_ui::offset_symmetric(*mode), _ => false, }; - let mut focus = std::mem::take(&mut d.focus); + let mut focus = take_focus(ui.ctx(), d); let half_height = app.cam.half_height(); let axis = revolve_axis(app, d); if matches!(d.kind, Kind::Revolve { .. }) { @@ -785,6 +785,28 @@ pub(crate) fn box_place(app: &SolveApp, proj: &Proj) -> BoxPlace { BoxPlace { dialog: app.viewport.dialog_rect, view: Some(proj.rect), handles: None } } +fn boxes_shown_id() -> egui::Id { + egui::Id::new("sc_canvas_value_boxes") +} + +/// Show commands' values in boxes in the view or not, from the preference (the app calls this +/// every frame). +pub(crate) fn set_boxes_shown(ctx: &egui::Context, on: bool) { + ctx.data_mut(|d| d.insert_temp(boxes_shown_id(), on)); +} + +/// Whether a command's values have boxes in the view. Off (Preferences), they are typed in the +/// command window only, and the view stays clear for picking (#9479). +pub(crate) fn boxes_shown(ctx: &egui::Context) -> bool { + ctx.data(|d| d.get_temp::(boxes_shown_id())).unwrap_or(true) +} + +/// Whether the command's value box takes the keyboard this frame: as the window opens, and after +/// a pick. Without boxes in the view, it is left for the command window's value field (#9479). +fn take_focus(ctx: &egui::Context, d: &mut Dialog) -> bool { + boxes_shown(ctx) && std::mem::take(&mut d.focus) +} + /// The command's main value in the viewport, next to the geometry: the keyboard goes here while /// the window is open, so a number typed straight after a pick sets the value. `at` is its /// top-left corner. `select_all` re-selects the whole field, both right after a pick and on every @@ -838,6 +860,10 @@ fn value_box_full( note: Option<&str>, place: BoxPlace, ) -> Option { + // Off in Preferences: the value is typed in the command window only (#9479). + if !boxes_shown(ui.ctx()) { + return None; + } let t = Tokens::get(); // Held in place while the pointer is over it and value stepping is on (see `value_box`): by // its pivot, the point of the box that sits at `at`, which is not always its top-left corner. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index fbc4f98168..dfebd3f98c 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1586,6 +1586,13 @@ impl Dialog { true } + /// Give the keyboard to the form's value field `id`, its text selected, once the pointer is + /// idle: where a pick gives it when values have no boxes in the view (#9479). + pub(crate) fn give_keyboard(&mut self, id: egui::Id) { + self.value_focus = Some(id); + self.restore_value_focus = true; + } + /// A pick in the viewport (already accepted by the active input). pub fn pick(&mut self, s: &Session, sel: Sel) { if self.take_extrude_target(s, &sel) { @@ -3288,6 +3295,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { if let Some(field) = crate::ui_kit::canvas_value_focus(ctx) { value_fields.push(field); } + // Without value boxes in the view (#9479), the keyboard the window's opening or a pick gives + // the box goes to the form's value field, once the form has drawn one. + if d.focus && !crate::canvas::boxes_shown(ctx) && !value_fields.is_empty() { + d.focus = false; + d.restore_value_focus = true; + } // The editor loses the keyboard while the click is still being handled, possibly in a later // pass of the same frame, so give it back once the pointer is idle again. let pointer_busy = ctx.input(|i| i.pointer.any_down() || i.pointer.any_pressed() || i.pointer.any_released()); diff --git a/crates/ui-egui/src/extrude_edit_tests.rs b/crates/ui-egui/src/extrude_edit_tests.rs index 0b271fea88..60acab05b9 100644 --- a/crates/ui-egui/src/extrude_edit_tests.rs +++ b/crates/ui-egui/src/extrude_edit_tests.rs @@ -503,3 +503,21 @@ fn two_sides_with_all_shows_distance_2_and_reverse_swaps_the_sides() { h.step(&json!({"widget":"OK"})).unwrap(); assert_eq!(extent(&h), e); } + +/// Without value boxes in the view (Preferences, #9479), nothing covers the model: the command +/// window's Distance takes the keyboard instead, its text selected, and the arrow still drags it. +#[test] +fn without_value_boxes_in_the_view_the_distance_is_typed_in_the_window() { + let mut h = make(json!({"distance":"4 mm"})); + h.app.preferences.value_boxes_in_view = false; + open(&mut h); + assert!(h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).is_none(), "a value box in the view"); + let focused = h.ctx.memory(|m| m.focused()); + assert!(focused.is_some() && focused != Some(egui::Id::new("sc_canvas_value")), "{focused:?}"); + h.step(&json!({"text":"25 mm"})).unwrap(); + h.frames(2); + assert!((dialog_distance(&h) - 25.0).abs() < 1e-9, "{}", dialog_distance(&h)); + h.step(&json!({"drag":[{"handle":"arrow"},on_arrow(&h, 30.0)]})).unwrap(); + assert!((dialog_distance(&h) - 30.0).abs() < 0.5, "{}", dialog_distance(&h)); + assert!(h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).is_none(), "a value box in the view"); +} diff --git a/crates/ui-egui/src/hole_refs.rs b/crates/ui-egui/src/hole_refs.rs index 768a2348a5..8cd51464e6 100644 --- a/crates/ui-egui/src/hole_refs.rs +++ b/crates/ui-egui/src/hole_refs.rs @@ -245,10 +245,20 @@ pub(crate) fn input_rows(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, names: let Some(k) = d.inputs.get(i).filter(|inp| !inp.items.is_empty()).and_then(|inp| REFERENCES.iter().position(|l| *l == inp.label)) else { continue; }; + let mut keyboard = None; if let Some(refs) = refs_mut(d) && let (Some(label), Some(value)) = (DISTANCES.get(k), refs.distance.get_mut(k)) { enter |= crate::ui_kit::expr_row(ui, label, value); + // Without value boxes in the view (#9479), a newly picked edge's distance takes the + // keyboard here, as its box would. + if refs.focus == Some(k) && !crate::canvas::boxes_shown(ui.ctx()) { + refs.focus = None; + keyboard = crate::ui_kit::last_value_field(ui.ctx()); + } + } + if let Some(id) = keyboard { + d.give_keyboard(id); } } sync(s, d); @@ -342,7 +352,8 @@ pub(crate) fn canvas(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, } dimension(painter, a, b, t.dimension); let (Some(label), Some(refs)) = (DISTANCES.get(*k), refs_mut(d)) else { continue }; - let focus = refs.focus == Some(*k); + // Without boxes in the view, the distance's row in the command window takes it (#9479). + let focus = refs.focus == Some(*k) && crate::canvas::boxes_shown(ui.ctx()); if focus { refs.focus = None; took = true; diff --git a/crates/ui-egui/src/hole_refs_ui_tests.rs b/crates/ui-egui/src/hole_refs_ui_tests.rs index bdc4103057..12d3d64e42 100644 --- a/crates/ui-egui/src/hole_refs_ui_tests.rs +++ b/crates/ui-egui/src/hole_refs_ui_tests.rs @@ -162,6 +162,28 @@ fn a_number_typed_after_an_edge_pick_sets_its_distance() { assert_eq!(diameter, "5 mm", "the diameter is left as it was"); } +/// Without value boxes in the view (#9479), the distance's row in the command window takes the +/// number typed after an edge pick, as its box would. +#[test] +fn without_value_boxes_a_number_typed_after_an_edge_pick_sets_its_distance() { + let mut h = plate(); + h.app.preferences.value_boxes_in_view = false; + h.step(&json!({"start": "solid.hole"})).unwrap(); + let at = screen(&mut h, Vec3::new(30.0, 25.0, 10.0)); + h.step(&json!({"click": at})).unwrap(); + h.step(&json!({"click": {"handle": "dialog:Reference 1"}})).unwrap(); + let left = screen(&mut h, Vec3::new(0.0, 20.0, 10.0)); + h.step(&json!({"click": left})).unwrap(); + h.frames(2); + assert!(h.ctx.memory(|m| m.area_rect(egui::Id::new(("sc_hole_ref_area", 0usize)))).is_none(), "a distance box in the view"); + h.step(&json!({"text": "7 mm"})).unwrap(); + h.frames(2); + assert_eq!(distances(&h)[0], "7 mm"); + assert!((place(&h).x - 7.0).abs() < 1e-6, "the hole is 7 mm from the left edge: {:?}", place(&h)); + let Some(Kind::Hole { diameter, .. }) = h.app.dialog.as_ref().map(|d| &d.kind) else { panic!("the hole dialog") }; + assert_eq!(diameter, "5 mm", "the diameter is left as it was"); +} + /// The open Cylinder dialog's reference distances and where it stands. fn cylinder_dialog(h: &Harness) -> ([String; 2], Vec3) { match h.app.dialog.as_ref().map(|d| &d.kind) { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 855ee14833..5b4bed9111 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2638,6 +2638,7 @@ impl SolveApp { /// Lay out the whole window. pub fn ui(&mut self, ui: &mut egui::Ui) { value_step::set_enabled(ui.ctx(), self.preferences.step_values); + canvas::set_boxes_shown(ui.ctx(), self.preferences.value_boxes_in_view); let t0 = now_ms(); // The first frames draw with the bundled faces while the system's CJK, Hangul and Thai // faces are read on a thread; the fonts are installed again once they are there. diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index fe257e35f7..e01380487d 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -1014,6 +1014,10 @@ preference_settings! { /// Up/Down and the wheel over a focused value field step its plain number (`value_step`, /// #2320). Off by default: the keys move the text cursor and the wheel scrolls, as before. step_values: bool = false, PAGE_DESIGN => check_id("ui.step_values"); + /// A command's value (Extrude's Distance, Fillet's Radius…) in a box beside its handle in the + /// view (`canvas::boxes_shown`). Off: it is typed in the command window only, which takes the + /// keyboard after a pick instead, and nothing covers the model (#9479). + value_boxes_in_view: bool = true, PAGE_DESIGN => check_id("prefs.value_boxes_in_view"); /// A design coming back to its tab reads again the linked designs whose files changed since /// it last read them. Off: they wait for Update Links, as before. update_changed_links: bool = true, PAGE_DESIGN => check_id("settings.update_changed_links"); diff --git a/crates/ui-egui/src/prefs/design.rs b/crates/ui-egui/src/prefs/design.rs index 19b0a0d7f1..a98cf2ea28 100644 --- a/crates/ui-egui/src/prefs/design.rs +++ b/crates/ui-egui/src/prefs/design.rs @@ -39,6 +39,8 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) { let tip = app.tr("ui.step_values_tip").to_string(); let step = setting::step_values(app, ui).on_hover_text(tip); crate::scenario::publish_handle("prefs:step_values", step.rect.center()); + let tip = app.tr("prefs.value_boxes_in_view_tip").to_string(); + setting::value_boxes_in_view(app, ui).on_hover_text(tip); }); ui_kit::settings_section(ui, "orientation", app.tr("prefs.orientation"), |ui| { ui.horizontal_wrapped(|ui| { diff --git a/crates/ui-egui/src/ui_kit/value_focus.rs b/crates/ui-egui/src/ui_kit/value_focus.rs index 1d5754ed3a..684917bf27 100644 --- a/crates/ui-egui/src/ui_kit/value_focus.rs +++ b/crates/ui-egui/src/ui_kit/value_focus.rs @@ -10,6 +10,11 @@ pub(crate) fn value_fields(ctx: &egui::Context, draw: impl FnOnce() -> bool) -> (enter, fields) } +/// The expression editor the form drew last so far, in [`value_fields`]. +pub(crate) fn last_value_field(ctx: &egui::Context) -> Option { + ctx.data(|data| data.get_temp::>(egui::Id::new("sc_value_fields"))).and_then(|fields| fields.last().map(|(id, _)| *id)) +} + /// Only a value box drawn this pass can receive the keyboard after a viewport pick. pub(crate) fn canvas_value_focus(ctx: &egui::Context) -> Option<(egui::Id, usize)> { ctx.data(|data| data.get_temp::<(u64, egui::Id, usize)>(egui::Id::new("sc_canvas_value_focus")))