1use color_eyre::eyre::WrapErr;
3use egui::{menu, Button, Context, TextWrapMode, TopBottomPanel, Ui};
4use futures::executor::block_on;
5use itertools::Itertools;
6use std::sync::atomic::Ordering;
7use surfer_translation_types::{TranslationPreference, Translator};
8
9use crate::displayed_item_tree::VisibleItemIndex;
10use crate::hierarchy::HierarchyStyle;
11use crate::message::MessageTarget;
12use crate::wave_container::{FieldRef, VariableRefExt};
13use crate::wave_source::LoadOptions;
14use crate::wcp::{proto::WcpEvent, proto::WcpSCMessage};
15use crate::{
16 clock_highlighting::clock_highlight_type_menu,
17 config::ArrowKeyBindings,
18 displayed_item::{DisplayedFieldRef, DisplayedItem},
19 file_dialog::OpenMode,
20 message::Message,
21 time::{timeformat_menu, timeunit_menu},
22 variable_name_type::VariableNameType,
23 SystemState,
24};
25
26struct ButtonBuilder {
28 text: String,
29 shortcut: Option<String>,
30 message: Message,
31 enabled: bool,
32}
33
34impl ButtonBuilder {
35 fn new(text: impl Into<String>, message: Message) -> Self {
36 Self {
37 text: text.into(),
38 message,
39 shortcut: None,
40 enabled: true,
41 }
42 }
43
44 fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
45 self.shortcut = Some(shortcut.into());
46 self
47 }
48
49 #[cfg_attr(not(feature = "python"), allow(dead_code))]
50 pub fn enabled(mut self, enabled: bool) -> Self {
51 self.enabled = enabled;
52 self
53 }
54
55 pub fn add_leave_menu(self, msgs: &mut Vec<Message>, ui: &mut Ui) {
56 self.add_inner(false, msgs, ui);
57 }
58
59 pub fn add_closing_menu(self, msgs: &mut Vec<Message>, ui: &mut Ui) {
60 self.add_inner(true, msgs, ui);
61 }
62
63 pub fn add_inner(self, close_menu: bool, msgs: &mut Vec<Message>, ui: &mut Ui) {
64 let button = Button::new(self.text);
65 let button = if let Some(s) = self.shortcut {
66 button.shortcut_text(s)
67 } else {
68 button
69 };
70 if ui.add_enabled(self.enabled, button).clicked() {
71 msgs.push(self.message);
72 if close_menu {
73 ui.close_menu();
74 }
75 }
76 }
77}
78
79impl SystemState {
80 pub fn add_menu_panel(&self, ctx: &Context, msgs: &mut Vec<Message>) {
81 TopBottomPanel::top("menu").show(ctx, |ui| {
82 menu::bar(ui, |ui| {
83 self.menu_contents(ui, msgs);
84 });
85 });
86 }
87
88 pub fn menu_contents(&self, ui: &mut Ui, msgs: &mut Vec<Message>) {
89 fn b(text: impl Into<String>, message: Message) -> ButtonBuilder {
91 ButtonBuilder::new(text, message)
92 }
93
94 let waves_loaded = self.user.waves.is_some();
95
96 ui.menu_button("File", |ui| {
97 b("Open file...", Message::OpenFileDialog(OpenMode::Open)).add_closing_menu(msgs, ui);
98 b("Switch file...", Message::OpenFileDialog(OpenMode::Switch))
99 .add_closing_menu(msgs, ui);
100 b(
101 "Reload",
102 Message::ReloadWaveform(self.user.config.behavior.keep_during_reload),
103 )
104 .shortcut("r")
105 .enabled(self.user.waves.is_some())
106 .add_closing_menu(msgs, ui);
107
108 b("Load state...", Message::LoadStateFile(None)).add_closing_menu(msgs, ui);
109 #[cfg(not(target_arch = "wasm32"))]
110 {
111 let save_text = if self.user.state_file.is_some() {
112 "Save state"
113 } else {
114 "Save state..."
115 };
116 b(
117 save_text,
118 Message::SaveStateFile(self.user.state_file.clone()),
119 )
120 .shortcut("Ctrl+s")
121 .add_closing_menu(msgs, ui);
122 }
123 b("Save state as...", Message::SaveStateFile(None)).add_closing_menu(msgs, ui);
124 b(
125 "Open URL...",
126 Message::SetUrlEntryVisible(
127 true,
128 Some(Box::new(|url: String| {
129 Message::LoadWaveformFileFromUrl(url.clone(), LoadOptions::clean())
130 })),
131 ),
132 )
133 .add_closing_menu(msgs, ui);
134 #[cfg(target_arch = "wasm32")]
135 b("Run command file...", Message::OpenCommandFileDialog)
136 .enabled(waves_loaded)
137 .add_closing_menu(msgs, ui);
138 #[cfg(not(target_arch = "wasm32"))]
139 b("Run command file...", Message::OpenCommandFileDialog).add_closing_menu(msgs, ui);
140 b(
141 "Run command file from URL...",
142 Message::SetUrlEntryVisible(
143 true,
144 Some(Box::new(|url: String| {
145 Message::LoadCommandFileFromUrl(url.clone())
146 })),
147 ),
148 )
149 .add_closing_menu(msgs, ui);
150
151 #[cfg(feature = "python")]
152 {
153 b("Add Python translator", Message::OpenPythonPluginDialog)
154 .add_closing_menu(msgs, ui);
155 b("Reload Python translator", Message::ReloadPythonPlugin)
156 .enabled(self.translators.has_python_translator())
157 .add_closing_menu(msgs, ui);
158 }
159 #[cfg(not(target_arch = "wasm32"))]
160 b("Exit", Message::Exit).add_closing_menu(msgs, ui);
161 });
162 ui.menu_button("View", |ui: &mut Ui| {
163 ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
164 b(
165 "Zoom in",
166 Message::CanvasZoom {
167 mouse_ptr: None,
168 delta: 0.5,
169 viewport_idx: 0,
170 },
171 )
172 .shortcut("+")
173 .enabled(waves_loaded)
174 .add_leave_menu(msgs, ui);
175
176 b(
177 "Zoom out",
178 Message::CanvasZoom {
179 mouse_ptr: None,
180 delta: 2.0,
181 viewport_idx: 0,
182 },
183 )
184 .shortcut("-")
185 .enabled(waves_loaded)
186 .add_leave_menu(msgs, ui);
187
188 b("Zoom to fit", Message::ZoomToFit { viewport_idx: 0 })
189 .enabled(waves_loaded)
190 .add_closing_menu(msgs, ui);
191
192 ui.separator();
193
194 b("Go to start", Message::GoToStart { viewport_idx: 0 })
195 .shortcut("s")
196 .enabled(waves_loaded)
197 .add_closing_menu(msgs, ui);
198 b("Go to end", Message::GoToEnd { viewport_idx: 0 })
199 .shortcut("e")
200 .enabled(waves_loaded)
201 .add_closing_menu(msgs, ui);
202 ui.separator();
203 b("Add viewport", Message::AddViewport)
204 .enabled(waves_loaded)
205 .add_closing_menu(msgs, ui);
206 b("Remove viewport", Message::RemoveViewport)
207 .enabled(waves_loaded)
208 .add_closing_menu(msgs, ui);
209 ui.separator();
210
211 b("Toggle side panel", Message::ToggleSidePanel)
212 .shortcut("b")
213 .add_closing_menu(msgs, ui);
214 b("Toggle menu", Message::ToggleMenu)
215 .shortcut("Alt+m")
216 .add_closing_menu(msgs, ui);
217 b("Toggle toolbar", Message::ToggleToolbar)
218 .shortcut("t")
219 .add_closing_menu(msgs, ui);
220 b("Toggle overview", Message::ToggleOverview).add_closing_menu(msgs, ui);
221 b("Toggle statusbar", Message::ToggleStatusbar).add_closing_menu(msgs, ui);
222 b("Toggle timeline", Message::ToggleDefaultTimeline).add_closing_menu(msgs, ui);
223 #[cfg(not(target_arch = "wasm32"))]
224 b("Toggle full screen", Message::ToggleFullscreen)
225 .shortcut("F11")
226 .add_closing_menu(msgs, ui);
227 ui.menu_button("Theme", |ui| {
228 ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
229 b("Default theme", Message::SelectTheme(None)).add_closing_menu(msgs, ui);
230 for theme_name in self.user.config.theme.theme_names.clone() {
231 b(theme_name.clone(), Message::SelectTheme(Some(theme_name)))
232 .add_closing_menu(msgs, ui);
233 }
234 });
235 ui.menu_button("UI zoom factor", |ui| {
236 ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
237 for scale in &self.user.config.layout.zoom_factors {
238 ui.radio(
239 self.ui_zoom_factor() == *scale,
240 format!("{} %", scale * 100.),
241 )
242 .clicked()
243 .then(|| {
244 ui.close_menu();
245 msgs.push(Message::SetUIZoomFactor(*scale))
246 });
247 }
248 });
249 });
250
251 ui.menu_button("Settings", |ui| {
252 ui.menu_button("Clock highlighting", |ui| {
253 clock_highlight_type_menu(ui, msgs, self.user.config.default_clock_highlight_type);
254 });
255 ui.menu_button("Time unit", |ui| {
256 timeunit_menu(ui, msgs, &self.user.wanted_timeunit);
257 });
258 ui.menu_button("Time format", |ui| {
259 timeformat_menu(ui, msgs, &self.get_time_format());
260 });
261 if let Some(waves) = &self.user.waves {
262 let variable_name_type = waves.default_variable_name_type;
263 ui.menu_button("Variable names", |ui| {
264 for name_type in enum_iterator::all::<VariableNameType>() {
265 ui.radio(variable_name_type == name_type, name_type.to_string())
266 .clicked()
267 .then(|| {
268 ui.close_menu();
269 msgs.push(Message::ForceVariableNameTypes(name_type));
270 });
271 }
272 });
273 }
274 ui.menu_button("Variable name alignment", |ui| {
275 let align_right = self
276 .user
277 .align_names_right
278 .unwrap_or_else(|| self.user.config.layout.align_names_right());
279 ui.radio(!align_right, "Left").clicked().then(|| {
280 ui.close_menu();
281 msgs.push(Message::SetNameAlignRight(false));
282 });
283 ui.radio(align_right, "Right").clicked().then(|| {
284 ui.close_menu();
285 msgs.push(Message::SetNameAlignRight(true));
286 });
287 });
288 ui.menu_button("Variable filter type", |ui| {
289 self.variable_filter_type_menu(ui, msgs);
290 });
291
292 ui.menu_button("Hierarchy", |ui| {
293 for style in enum_iterator::all::<HierarchyStyle>() {
294 ui.radio(
295 self.user.config.layout.hierarchy_style == style,
296 style.to_string(),
297 )
298 .clicked()
299 .then(|| {
300 ui.close_menu();
301 msgs.push(Message::SetHierarchyStyle(style));
302 });
303 }
304 });
305
306 ui.menu_button("Arrow keys", |ui| {
307 for binding in enum_iterator::all::<ArrowKeyBindings>() {
308 ui.radio(
309 self.user.config.behavior.arrow_key_bindings == binding,
310 binding.to_string(),
311 )
312 .clicked()
313 .then(|| {
314 ui.close_menu();
315 msgs.push(Message::SetArrowKeyBindings(binding));
316 });
317 }
318 });
319
320 ui.radio(self.show_ticks(), "Show tick lines")
321 .clicked()
322 .then(|| {
323 ui.close_menu();
324 msgs.push(Message::ToggleTickLines);
325 });
326
327 ui.radio(self.show_tooltip(), "Show variable tooltip")
328 .clicked()
329 .then(|| {
330 ui.close_menu();
331 msgs.push(Message::ToggleVariableTooltip);
332 });
333
334 ui.radio(self.show_scope_tooltip(), "Show scope tooltip")
335 .clicked()
336 .then(|| {
337 ui.close_menu();
338 msgs.push(Message::ToggleScopeTooltip);
339 });
340
341 ui.radio(self.show_variable_indices(), "Show variable indices")
342 .clicked()
343 .then(|| {
344 ui.close_menu();
345 msgs.push(Message::ToggleIndices);
346 });
347
348 ui.radio(self.show_variable_direction(), "Show variable direction")
349 .clicked()
350 .then(|| {
351 ui.close_menu();
352 msgs.push(Message::ToggleDirection);
353 });
354
355 ui.radio(self.show_empty_scopes(), "Show empty scopes")
356 .clicked()
357 .then(|| {
358 ui.close_menu();
359 msgs.push(Message::ToggleEmptyScopes);
360 });
361
362 ui.radio(
363 self.show_parameters_in_scopes(),
364 "Show parameters in scopes",
365 )
366 .clicked()
367 .then(|| {
368 ui.close_menu();
369 msgs.push(Message::ToggleParametersInScopes);
370 });
371
372 ui.radio(self.highlight_focused(), "Highlight focused")
373 .clicked()
374 .then(|| {
375 ui.close_menu();
376 msgs.push(Message::SetHighlightFocused(!self.highlight_focused()))
377 });
378
379 ui.radio(self.fill_high_values(), "Fill high values")
380 .clicked()
381 .then(|| {
382 ui.close_menu();
383 msgs.push(Message::SetFillHighValues(!self.fill_high_values()));
384 });
385 });
386 ui.menu_button("Help", |ui| {
387 b("Quick start", Message::SetQuickStartVisible(true)).add_closing_menu(msgs, ui);
388 b("Control keys", Message::SetKeyHelpVisible(true)).add_closing_menu(msgs, ui);
389 b("Mouse gestures", Message::SetGestureHelpVisible(true)).add_closing_menu(msgs, ui);
390
391 ui.separator();
392 b("Show logs", Message::SetLogsVisible(true)).add_closing_menu(msgs, ui);
393
394 ui.separator();
395 b("License information", Message::SetLicenseVisible(true)).add_closing_menu(msgs, ui);
396 ui.separator();
397 b("About", Message::SetAboutVisible(true)).add_closing_menu(msgs, ui);
398 });
399 }
400
401 pub fn item_context_menu(
402 &self,
403 path: Option<&FieldRef>,
404 msgs: &mut Vec<Message>,
405 ui: &mut Ui,
406 vidx: VisibleItemIndex,
407 ) {
408 let Some(waves) = &self.user.waves else {
409 return;
410 };
411
412 let (displayed_item_id, displayed_item) = waves
413 .items_tree
414 .get_visible(vidx)
415 .map(|node| (node.item_ref, &waves.displayed_items[&node.item_ref]))
416 .unwrap();
417
418 let affect_selected = waves
419 .items_tree
420 .iter_visible_selected()
421 .map(|node| node.item_ref)
422 .contains(&displayed_item_id);
423 let affected_vidxs = if affect_selected { None } else { Some(vidx) };
424
425 if let Some(path) = path {
426 let dfr = DisplayedFieldRef {
427 item: displayed_item_id,
428 field: path.field.clone(),
429 };
430 self.add_format_menu(&dfr, displayed_item, path, msgs, ui);
431 }
432
433 ui.menu_button("Color", |ui| {
434 let selected_color = &displayed_item.color().unwrap_or("__nocolor__");
435 for color_name in self.user.config.theme.colors.keys() {
436 ui.radio(selected_color == color_name, color_name)
437 .clicked()
438 .then(|| {
439 ui.close_menu();
440 msgs.push(Message::ItemColorChange(
441 affected_vidxs.into(),
442 Some(color_name.clone()),
443 ));
444 });
445 }
446 ui.separator();
447 ui.radio(*selected_color == "__nocolor__", "Default")
448 .clicked()
449 .then(|| {
450 ui.close_menu();
451 msgs.push(Message::ItemColorChange(
452 MessageTarget::Explicit(vidx),
453 None,
454 ));
455 });
456 });
457
458 ui.menu_button("Background color", |ui| {
459 let selected_color = &displayed_item.background_color().unwrap_or("__nocolor__");
460 for color_name in self.user.config.theme.colors.keys() {
461 ui.radio(selected_color == color_name, color_name)
462 .clicked()
463 .then(|| {
464 ui.close_menu();
465 msgs.push(Message::ItemBackgroundColorChange(
466 affected_vidxs.into(),
467 Some(color_name.clone()),
468 ));
469 });
470 }
471 ui.separator();
472 ui.radio(*selected_color == "__nocolor__", "Default")
473 .clicked()
474 .then(|| {
475 ui.close_menu();
476 msgs.push(Message::ItemBackgroundColorChange(
477 MessageTarget::Explicit(vidx),
478 None,
479 ));
480 });
481 });
482
483 if let DisplayedItem::Variable(variable) = displayed_item {
484 ui.menu_button("Name", |ui| {
485 let variable_name_type = variable.display_name_type;
486 for name_type in enum_iterator::all::<VariableNameType>() {
487 ui.radio(variable_name_type == name_type, name_type.to_string())
488 .clicked()
489 .then(|| {
490 ui.close_menu();
491 msgs.push(Message::ChangeVariableNameType(
492 MessageTarget::Explicit(vidx),
493 name_type,
494 ));
495 });
496 }
497 });
498
499 ui.menu_button("Height", |ui| {
500 let selected_size = displayed_item.height_scaling_factor();
501 for size in &self.user.config.layout.waveforms_line_height_multiples {
502 ui.radio(selected_size == *size, format!("{}", size))
503 .clicked()
504 .then(|| {
505 ui.close_menu();
506 msgs.push(Message::ItemHeightScalingFactorChange(
507 affected_vidxs.into(),
508 *size,
509 ));
510 });
511 }
512 });
513
514 if self.wcp_greeted_signal.load(Ordering::Relaxed) {
515 if self.wcp_client_capabilities.goto_declaration
516 && ui.button("Go to declaration").clicked()
517 {
518 let variable = variable.variable_ref.full_path_string();
519 self.channels.wcp_s2c_sender.as_ref().map(|ch| {
520 block_on(
521 ch.send(WcpSCMessage::event(WcpEvent::goto_declaration { variable })),
522 )
523 });
524 ui.close_menu();
525 }
526 if self.wcp_client_capabilities.add_drivers && ui.button("Add drivers").clicked() {
527 let variable = variable.variable_ref.full_path_string();
528 self.channels.wcp_s2c_sender.as_ref().map(|ch| {
529 block_on(ch.send(WcpSCMessage::event(WcpEvent::add_drivers { variable })))
530 });
531 ui.close_menu();
532 }
533 if self.wcp_client_capabilities.add_loads && ui.button("Add loads").clicked() {
534 let variable = variable.variable_ref.full_path_string();
535 self.channels.wcp_s2c_sender.as_ref().map(|ch| {
536 block_on(ch.send(WcpSCMessage::event(WcpEvent::add_loads { variable })))
537 });
538 ui.close_menu();
539 }
540 }
541 }
542
543 if ui.button("Rename").clicked() {
544 ui.close_menu();
545 msgs.push(Message::RenameItem(Some(vidx)));
546 }
547
548 if ui.button("Remove").clicked() {
549 msgs.push(
550 if waves
551 .items_tree
552 .iter_visible_selected()
553 .map(|node| node.item_ref)
554 .contains(&displayed_item_id)
555 {
556 Message::Batch(vec![
557 Message::RemoveItems(
558 waves
559 .items_tree
560 .iter_visible_selected()
561 .map(|node| node.item_ref)
562 .collect_vec(),
563 ),
564 Message::UnfocusItem,
565 ])
566 } else {
567 Message::RemoveItems(vec![displayed_item_id])
568 },
569 );
570 msgs.push(Message::InvalidateCount);
571 ui.close_menu();
572 }
573 if path.is_some() {
574 ui.menu_button("Copy", |ui| {
576 #[allow(clippy::collapsible_if)]
577 if waves.cursor.is_some() {
578 if ui.button("Value").clicked() {
579 ui.close_menu();
580 msgs.push(Message::VariableValueToClipbord(MessageTarget::Explicit(
581 vidx,
582 )));
583 }
584 }
585 if ui.button("Name").clicked() {
586 ui.close_menu();
587 msgs.push(Message::VariableNameToClipboard(MessageTarget::Explicit(
588 vidx,
589 )));
590 }
591 if ui.button("Full name").clicked() {
592 ui.close_menu();
593 msgs.push(Message::VariableFullNameToClipboard(
594 MessageTarget::Explicit(vidx),
595 ));
596 }
597 });
598 }
599 ui.separator();
600 ui.menu_button("Insert", |ui| {
601 if ui.button("Divider").clicked() {
602 ui.close_menu();
603 msgs.push(Message::AddDivider(None, Some(vidx)));
604 }
605 if ui.button("Timeline").clicked() {
606 ui.close_menu();
607 msgs.push(Message::AddTimeLine(Some(vidx)));
608 }
609 });
610
611 ui.menu_button("Group", |ui| {
612 let info = waves
613 .items_tree
614 .iter_visible_extra()
615 .find(|info| info.node.item_ref == displayed_item_id)
616 .expect("Inconsistent, could not find displayed signal in tree");
617
618 if ui.button("Create").clicked() {
619 ui.close_menu();
620
621 let mut items = if affect_selected {
622 waves
623 .items_tree
624 .iter_visible_selected()
625 .map(|node| node.item_ref)
626 .collect::<Vec<_>>()
627 } else {
628 vec![]
629 };
630 if affect_selected {
632 if let Some(focused_item_node) = waves
633 .focused_item
634 .and_then(|focused_item| waves.items_tree.get_visible(focused_item))
635 {
636 items.push(focused_item_node.item_ref);
637 }
638 }
639
640 items.push(displayed_item_id);
642
643 msgs.push(Message::GroupNew {
644 name: None,
645 before: Some(info.idx),
646 items: Some(items),
647 })
648 }
649 if matches!(displayed_item, DisplayedItem::Group(_)) {
650 if ui.button("Dissolve").clicked() {
651 ui.close_menu();
652 msgs.push(Message::GroupDissolve(Some(displayed_item_id)))
653 }
654
655 let (text, msg, msg_recursive) = if info.node.unfolded {
656 (
657 "Collapse",
658 Message::GroupFold(Some(displayed_item_id)),
659 Message::GroupFoldRecursive(Some(displayed_item_id)),
660 )
661 } else {
662 (
663 "Expand",
664 Message::GroupUnfold(Some(displayed_item_id)),
665 Message::GroupUnfold(Some(displayed_item_id)),
666 )
667 };
668 if ui.button(text).clicked() {
669 ui.close_menu();
670 msgs.push(msg)
671 }
672 if ui.button(text.to_owned() + " recursive").clicked() {
673 ui.close_menu();
674 msgs.push(msg_recursive)
675 }
676 }
677 });
678 }
679
680 fn add_format_menu(
681 &self,
682 displayed_field_ref: &DisplayedFieldRef,
683 displayed_item: &DisplayedItem,
684 path: &FieldRef,
685 msgs: &mut Vec<Message>,
686 ui: &mut Ui,
687 ) {
688 let Some(waves) = &self.user.waves else {
690 return;
691 };
692
693 let (mut preferred_translators, mut bad_translators) = if path.field.is_empty() {
694 self.translators
695 .all_translator_names()
696 .into_iter()
697 .partition(|translator_name| {
698 let t = self.translators.get_translator(translator_name);
699
700 if self
701 .user
702 .blacklisted_translators
703 .contains(&(path.root.clone(), translator_name.to_string()))
704 {
705 false
706 } else {
707 match waves
708 .inner
709 .as_waves()
710 .unwrap()
711 .variable_meta(&path.root)
712 .and_then(|meta| t.translates(&meta))
713 .context(format!(
714 "Failed to check if {translator_name} translates {:?}",
715 path.root.full_path(),
716 )) {
717 Ok(TranslationPreference::Yes) => true,
718 Ok(TranslationPreference::Prefer) => true,
719 Ok(TranslationPreference::No) => false,
720 Err(e) => {
721 msgs.push(Message::BlacklistTranslator(
722 path.root.clone(),
723 translator_name.to_string(),
724 ));
725 msgs.push(Message::Error(e));
726 false
727 }
728 }
729 }
730 })
731 } else {
732 (self.translators.basic_translator_names(), vec![])
733 };
734
735 preferred_translators.sort_by(|a, b| numeric_sort::cmp(a, b));
736 bad_translators.sort_by(|a, b| numeric_sort::cmp(a, b));
737
738 let selected_translator = match displayed_item {
739 DisplayedItem::Variable(var) => Some(var),
740 _ => None,
741 }
742 .and_then(|displayed_variable| displayed_variable.get_format(&displayed_field_ref.field));
743
744 let mut menu_entry = |ui: &mut Ui, name: &str| {
745 ui.radio(selected_translator.is_some_and(|st| st == name), name)
746 .clicked()
747 .then(|| {
748 ui.close_menu();
749 msgs.push(Message::VariableFormatChange(
750 if waves
751 .items_tree
752 .iter_visible_selected()
753 .map(|node| node.item_ref)
754 .contains(&displayed_field_ref.item)
755 {
756 MessageTarget::CurrentSelection
757 } else {
758 MessageTarget::Explicit(displayed_field_ref.clone())
759 },
760 name.to_string(),
761 ));
762 });
763 };
764
765 ui.menu_button("Format", |ui| {
766 for name in preferred_translators {
767 menu_entry(ui, name);
768 }
769 if !bad_translators.is_empty() {
770 ui.separator();
771 ui.menu_button("Not recommended", |ui| {
772 for name in bad_translators {
773 menu_entry(ui, name);
774 }
775 });
776 }
777 });
778 }
779}