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