libsurfer/
menus.rs

1//! Menu handling.
2use 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
28// Button builder. Short name because we use it a ton
29struct 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        /// Helper function to get a new ButtonBuilder.
89        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            // Actual signal. Not one of: divider, timeline, marker.
565            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                // the focused item may not yet be selected, so add it
614                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                // the clicked item may not be selected yet, add it
624                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        // Should not call this unless a variable is selected, and, hence, a VCD is loaded
669        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}