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};
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
32struct 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 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 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 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}