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