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