1use 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
33struct 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 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 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 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}