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