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