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