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.
This commit is contained in:
Tobias Breckle authored and GitHub committed 2026-10-14 17:50:04 -04:00
1 parent b6e87f0c44
commit 0ddd660089
8 files changed
+75 -1

No files matched your search

+2
View File
@@ -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)",
+2
View File
@@ -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",
+2
View File
@@ -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": "フォント",
+2
View File
@@ -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)": "바디 선택(면을 클릭하면 해당 바디 선택)",
+2
View File
@@ -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": "Шрифт",
+2
View File
@@ -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": "字体",
+61
View File
@@ -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<String> {
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() {
+2 -1
View File
@@ -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();