Preferences: an option to keep value boxes out of the view (#9479) (#9547)

* Claim #9479

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Preferences: an option to keep value boxes out of the view (#9479)

Design > "Show values beside their handles in the view", on by default.
Off, a command's value box (Extrude's Distance, Fillet's Radius, a hole's
reference distances...) isn't drawn over the model; the command window's
value field takes the keyboard that opening the window or a pick gives the
box, its text selected, so a number typed after a pick still sets it. The
handles stay and still drag the value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Preferences: the value box option in Arabic, Hebrew and Vietnamese too (#9479)

The complete catalogs must carry every English message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
austin62jake
2026-10-30 18:30:04 +00:00
committed by GitHub
parent a13e0e0ceb
commit aedbf0b1d4
10 changed files with 139 additions and 3 deletions
@@ -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": "命令的数值(距离、半径等)显示在模型上方手柄旁的框中。关闭后,在命令窗口中输入数值,选取后键盘输入会进入该窗口,模型不会被遮挡。"
}
}
+28 -2
View File
@@ -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::<bool>(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<egui::Rect> {
// 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.
+13
View File
@@ -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());
+18
View File
@@ -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");
}
+12 -1
View File
@@ -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;
+22
View File
@@ -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) {
+1
View File
@@ -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.
+4
View File
@@ -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");
+2
View File
@@ -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| {
+5
View File
@@ -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<egui::Id> {
ctx.data(|data| data.get_temp::<Vec<(egui::Id, usize)>>(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")))