From 0ddd6600893adf58022b3ee5b0f4ff24dd88ccf5 Mon Sep 17 00:00:00 2001 From: Tobias Breckle <25928171+tbreckle@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:50:04 +0200 Subject: [PATCH] Copy a feature's error or warning message from the history menu (#1226) The feature menu in the timeline and Feature History gets a Copy Message item for a feature's error or warning, translated, with a test. --- assets/locales/de.json | 2 + assets/locales/en.json | 2 + assets/locales/ja.json | 2 + assets/locales/ko.json | 2 + assets/locales/ru.json | 2 + assets/locales/zh.json | 2 + crates/ui-egui/src/timeline.rs | 61 +++++++++++++++++++++++++++++ crates/ui-egui/src/timeline_left.rs | 3 +- 8 files changed, 75 insertions(+), 1 deletion(-) diff --git a/assets/locales/de.json b/assets/locales/de.json index bda21b00..11fad25c 100644 --- a/assets/locales/de.json +++ b/assets/locales/de.json @@ -687,6 +687,8 @@ "Drive Joint": "Gelenk antreiben", "Edit Profile Sketch": "Profile-Skizze bearbeiten", "Find in Browser": "Im Browser suchen", + "Copy Error Message": "Fehlermeldung kopieren", + "Copy Warning Message": "Warnmeldung kopieren", "Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen", "Reset Toolbar": "Symbolleiste zurücksetzen", "Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)", diff --git a/assets/locales/en.json b/assets/locales/en.json index 81df1f95..01b8af7a 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -744,6 +744,8 @@ "Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Files from many CAD programs have Y up, so they open lying on their back here (Z up).", "Filter": "Filter", "Find in Browser": "Find in Browser", + "Copy Error Message": "Copy Error Message", + "Copy Warning Message": "Copy Warning Message", "Flat size": "Flat size", "Follow camera (Look At sketches)": "Follow camera (Look At sketches)", "Font": "Font", diff --git a/assets/locales/ja.json b/assets/locales/ja.json index 2f5f8973..2a80e03c 100644 --- a/assets/locales/ja.json +++ b/assets/locales/ja.json @@ -744,6 +744,8 @@ "Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "多くのCADではY軸を上向きとしているため、Z軸を上向きとするSolidCraftでは、読み込んだモデルが横倒しの向きで表示されることがあります。", "Filter": "フィルター", "Find in Browser": "ブラウザで検索", + "Copy Error Message": "エラーメッセージをコピー", + "Copy Warning Message": "警告メッセージをコピー", "Flat size": "展開サイズ", "Follow camera (Look At sketches)": "カメラ追従(スケッチを正面視)", "Font": "フォント", diff --git a/assets/locales/ko.json b/assets/locales/ko.json index 9ef6ccaf..78fde786 100644 --- a/assets/locales/ko.json +++ b/assets/locales/ko.json @@ -687,6 +687,8 @@ "Drive Joint": "조인트 구동", "Edit Profile Sketch": "윤곽 스케치 편집", "Find in Browser": "브라우저에서 찾기", + "Copy Error Message": "오류 메시지 복사", + "Copy Warning Message": "경고 메시지 복사", "Report a bug — review a screenshot and open a GitHub draft": "버그 신고 — 스크린샷을 검토하고 GitHub 초안 열기", "Reset Toolbar": "도구 모음 재설정", "Select bodies (a face picks its body)": "바디 선택(면을 클릭하면 해당 바디 선택)", diff --git a/assets/locales/ru.json b/assets/locales/ru.json index 087d06ee..033fbcb4 100644 --- a/assets/locales/ru.json +++ b/assets/locales/ru.json @@ -736,6 +736,8 @@ "Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Файлы многих CAD-программ ориентированы Y вверх, поэтому здесь они открываются лёжа на спине (Z вверх).", "Filter": "Фильтр", "Find in Browser": "Найти в браузере", + "Copy Error Message": "Скопировать сообщение об ошибке", + "Copy Warning Message": "Скопировать предупреждение", "Flat size": "Размер развёртки", "Follow camera (Look At sketches)": "Следовать за камерой (эскизы «Взгляд на»)", "Font": "Шрифт", diff --git a/assets/locales/zh.json b/assets/locales/zh.json index 4a8d7e74..0a841635 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -742,6 +742,8 @@ "Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "许多 CAD 程序的文件以 Y 为上轴,因此在此处打开时会平躺(Z 朝上)。", "Filter": "筛选", "Find in Browser": "在浏览器中查找", + "Copy Error Message": "复制错误消息", + "Copy Warning Message": "复制警告消息", "Flat size": "平板尺寸", "Follow camera (Look At sketches)": "跟随相机(注视草图)", "Font": "字体", diff --git a/crates/ui-egui/src/timeline.rs b/crates/ui-egui/src/timeline.rs index 65dc6a17..bd9b73c6 100644 --- a/crates/ui-egui/src/timeline.rs +++ b/crates/ui-egui/src/timeline.rs @@ -118,6 +118,35 @@ pub(crate) fn clipboard_buttons(app: &mut SolveApp, ui: &mut egui::Ui, id: u64) } } +/// What a feature's error or warning mark says, as its tooltip shows it (name and type, then +/// the error and the warning), for the clipboard. `None` when it has neither. +pub(crate) fn feature_message(app: &SolveApp, id: u64) -> Option { + let result = app.session.model.result(id)?; + if result.error.is_none() && result.warning.is_none() { + return None; + } + let feature = app.session.doc.features.iter().find(|f| f.id == id)?; + let mut text = format!("{} ({})", feature.name, feature.kind.type_name()); + if let Some(error) = &result.error { + text.push_str(&format!("\nError: {error}")); + } + if let Some(warning) = &result.warning { + text.push_str(&format!("\nWarning: {warning}")); + } + Some(text) +} + +/// Copy Error Message (or Copy Warning Message) in the menu of a feature that has one. +pub(crate) fn message_button(app: &SolveApp, ui: &mut egui::Ui, id: u64) { + let Some(text) = feature_message(app, id) else { return }; + let failed = app.session.model.result(id).is_some_and(|r| r.error.is_some()); + let label = if failed { "Copy Error Message" } else { "Copy Warning Message" }; + if ui.button(crate::i18n::tr_label(label)).clicked() { + ui.ctx().copy_text(text); + ui.close(); + } +} + /// Ctrl+click on a feature: add it to the selection, or take it out. fn toggle_feature(app: &mut SolveApp, id: u64) { let selected = selected_feature_ids(app); @@ -323,6 +352,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) { } ui.separator(); clipboard_buttons(app, ui, *id); + message_button(app, ui, *id); ui.separator(); if ui.button("Delete").clicked() { delete_feature(app, *id); @@ -450,6 +480,37 @@ mod tests { } } + /// A feature with a warning mark offers Copy Warning Message, which copies what its tooltip + /// says; a feature with nothing to report doesn't offer it. In both timelines. + #[test] + fn the_feature_menu_copies_a_warning_message() { + for left in [false, true] { + let mut h = two_boxes(left); + // Too large for the box: reduced to what fits, which the feature's warning says. + h.app.run("solid.fillet", json!({"edges": [[5, 0, 10]], "radius": 20, "fit": "reduce", "body": "One"})).unwrap(); + h.frames(4); + let id = h.app.session.doc.features.last().map(|f| f.id).unwrap(); + let name = h.app.session.doc.features.last().map(|f| f.name.clone()).unwrap(); + let warning = h.app.session.model.result(id).and_then(|r| r.warning.clone()).unwrap(); + h.step(&json!({"click": {"handle": "timeline:First"}, "button": "right"})).unwrap(); + assert!(h.widget("Copy Warning Message").is_none(), "left: {left}"); + h.step(&json!({"key": "Escape"})).unwrap(); + h.step(&json!({"click": {"handle": format!("timeline:{name}")}, "button": "right"})).unwrap(); + let [x, y] = h.widget("Copy Warning Message").unwrap(); + let (request, _reply) = crate::control::ControlRequest::new("ui.click", json!({"x": x, "y": y})); + crate::control::handle(&mut h.app, &h.ctx, &request); + let mut copied = Vec::new(); + for _ in 0..8 { + h.frame(); + copied.extend(h.output.commands.iter().filter_map(|c| match c { + egui::OutputCommand::CopyText(text) => Some(text.clone()), + _ => None, + })); + } + assert!(copied.iter().any(|t| t.starts_with(&name) && t.contains(&format!("Warning: {warning}"))), "left: {left}: {copied:?}"); + } + } + /// Copy on a feature outside the selection copies that feature alone. #[test] fn copy_on_an_unselected_feature_copies_only_it() { diff --git a/crates/ui-egui/src/timeline_left.rs b/crates/ui-egui/src/timeline_left.rs index 4a1568c6..98d3429e 100644 --- a/crates/ui-egui/src/timeline_left.rs +++ b/crates/ui-egui/src/timeline_left.rs @@ -5,7 +5,7 @@ use serde_json::json; use solidcraft_engine::doc::FeatureKind; use crate::theme::Tokens; -use crate::timeline::{clipboard_buttons, delete_feature, delete_features, icon_of, selected_feature_ids, transport_glyph}; +use crate::timeline::{clipboard_buttons, delete_feature, delete_features, icon_of, message_button, selected_feature_ids, transport_glyph}; use crate::{SolveApp, icons}; const FOLDER_INDENT: f32 = 14.0; @@ -791,6 +791,7 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui) { } ui.separator(); clipboard_buttons(app, ui, *id); + message_button(app, ui, *id); ui.separator(); selection_actions(app, ui, &groups); ui.separator();