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