Skip to main content

libsurfer/
menus.rs

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