Skip to main content

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