1use crate::{
2 cfg::ExportPathConnection,
3 control::{Control, Info, PrjSettingImportSection},
4 file_util::{get_prj_name, path_to_str},
5 image_reader::LoadImageForGui,
6 menu::{
7 self,
8 annotations_menu::{AnnotationsParams, AutosaveMenu},
9 cfg_menu::CfgMenu,
10 file_counts::labels_and_sorting,
11 open_folder,
12 scroll_area::ShowFileOptions,
13 ui_util::{button_confirmed, text_edit_singleline},
14 wand_many::{self, WandManyMenuResult, wand_many_menu},
15 },
16 tools::ToolState,
17 tools_data::{ToolSpecifics, ToolsDataMap},
18 util::version_label,
19};
20use core::f32;
21use egui::{Popup, Response, RichText, Ui};
22use rvimage_domain::{RvResult, rverr};
23use std::{
24 mem,
25 path::{Path, PathBuf},
26};
27
28use super::{
29 file_counts::Counts,
30 tools_menus::{attributes_menu, bbox_menu, brush_menu},
31};
32
33fn show_popup(msg: &str, icon: &str, info_message: Info, response: &Response) -> Info {
34 let mut new_msg = Info::None;
35 Popup::from_response(response)
36 .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
37 .show(|ui| {
38 let max_msg_len = 500;
39 let shortened_msg = if msg.len() > max_msg_len {
40 &msg[..max_msg_len]
41 } else {
42 msg
43 };
44 let mut txt = format!("{icon} {shortened_msg}");
45 ui.text_edit_multiline(&mut txt);
46 new_msg = if ui.button("Close").clicked() {
47 Info::None
48 } else {
49 info_message
50 }
51 });
52 new_msg
53}
54
55macro_rules! handle_error {
60 ($f_effect:expr, $f_err_cleanup:expr, $result:expr, $self:expr) => {
61 match $result {
62 Ok(r) => {
63 #[allow(clippy::redundant_closure_call)]
64 $f_effect(r);
65 }
66 Err(e) => {
67 #[allow(clippy::redundant_closure_call)]
68 $f_err_cleanup();
69 tracing::error!("{e:?}");
70 $self.info_message = Info::Error(e.to_string());
71 }
72 }
73 };
74 ($effect:expr, $result:expr, $self:expr) => {
75 handle_error!($effect, || (), $result, $self)
76 };
77 ($result:expr, $self:expr) => {
78 handle_error!(|_| {}, $result, $self);
79 };
80}
81
82pub struct ToolSelectMenu {
83 are_tools_active: bool, recently_activated_tool: Option<usize>,
85}
86impl ToolSelectMenu {
87 fn new() -> Self {
88 Self {
89 are_tools_active: true,
90 recently_activated_tool: None,
91 }
92 }
93 pub fn recently_clicked_tool(&self) -> Option<usize> {
94 self.recently_activated_tool
95 }
96 pub fn ui(
97 &mut self,
98 ui: &mut Ui,
99 tools: &mut [ToolState],
100 tools_menu_map: &mut ToolsDataMap,
101 ) -> RvResult<()> {
102 self.are_tools_active = true;
104 ui.horizontal_top(|ui| {
105 self.recently_activated_tool = tools
106 .iter_mut()
107 .enumerate()
108 .filter(|(_, t)| !t.is_always_active())
109 .find(|(_, t)| ui.selectable_label(t.is_active(), t.button_label).clicked())
110 .map(|(i, _)| i);
111 });
112 for v in tools_menu_map.values_mut().filter(|v| v.menu_active) {
113 let mut v_result = Err(rverr!("Tool menu not implemented"));
114 egui::ScrollArea::vertical()
115 .auto_shrink([false, false])
116 .max_height(f32::INFINITY)
117 .show(ui, |ui| {
118 let tmp = match &mut v.specifics {
119 ToolSpecifics::Bbox(x) => bbox_menu(
120 ui,
121 v.menu_active,
122 mem::take(x),
123 &mut self.are_tools_active,
124 v.visible_inactive_tools.clone(),
125 ),
126 ToolSpecifics::Brush(x) => brush_menu(
127 ui,
128 v.menu_active,
129 mem::take(x),
130 &mut self.are_tools_active,
131 v.visible_inactive_tools.clone(),
132 ),
133 ToolSpecifics::Attributes(x) => attributes_menu(
134 ui,
135 v.menu_active,
136 mem::take(x),
137 &mut self.are_tools_active,
138 ),
139 _ => Ok(mem::take(v)),
140 };
141 v_result = tmp;
142 });
143 *v = v_result?;
144 }
145 Ok(())
146 }
147}
148impl Default for ToolSelectMenu {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154fn save_dialog_in_prjfolder(prj_path: &Path, opened_folder: Option<&str>) -> Option<PathBuf> {
155 let filename = get_prj_name(prj_path, opened_folder);
156 let dialog = rfd::FileDialog::new();
157 let dialog = if let Some(folder) = prj_path.parent() {
158 dialog.set_directory(folder)
159 } else {
160 dialog
161 };
162 dialog
163 .add_filter("project files", &["json", "rvi"])
164 .set_file_name(filename)
165 .save_file()
166}
167
168#[derive(Default)]
169pub struct WandManyMenuBuffers {
170 pub comment: String,
171 pub exclfolder: String,
172 pub timeout: String,
173}
174#[derive(Default)]
175pub struct TextBuffers {
176 pub label_propagation: String,
177 pub label_deletion: String,
178 pub import_coco_from_ssh_path: String,
179 pub wand_many_buffers: WandManyMenuBuffers,
180}
181
182pub struct Menu {
183 window_open: bool, info_message: Info,
185 are_tools_active: bool,
186 toggle_clear_cache_on_close: bool,
187 scroll_offset: f32,
188 stats: Counts,
189 text_buffers: TextBuffers,
190 show_file_options: ShowFileOptions,
191 annotations_menu_params: AnnotationsParams,
192 import_coco_from_ssh: bool,
193 new_file_idx_annoplot: Option<usize>,
194 prj_import_path: Option<PathBuf>,
195 prj_import_section: PrjSettingImportSection,
196 prj_settings_for_display: Option<String>,
197 cache_all_progress: Option<f32>,
198 show_wandmany: bool,
199}
200
201impl Menu {
202 fn new() -> Self {
203 let text_buffers = TextBuffers {
204 label_propagation: "".into(),
205 label_deletion: "".into(),
206 import_coco_from_ssh_path: "path on ssh server".into(),
207 wand_many_buffers: WandManyMenuBuffers::default(),
208 };
209 Self {
210 window_open: true,
211 info_message: Info::None,
212 are_tools_active: true,
213 toggle_clear_cache_on_close: false,
214 scroll_offset: 0.0,
215 stats: Counts::default(),
216 text_buffers,
217 show_file_options: ShowFileOptions::default(),
218 annotations_menu_params: AnnotationsParams::default(),
219 import_coco_from_ssh: false,
220 new_file_idx_annoplot: None,
221 prj_import_path: None,
222 prj_import_section: PrjSettingImportSection::All,
223 prj_settings_for_display: None,
224 cache_all_progress: None,
225 show_wandmany: false,
226 }
227 }
228 pub fn popup(&mut self, info: Info) {
229 self.info_message = info;
230 }
231
232 pub fn toggle(&mut self) {
233 if self.window_open {
234 self.are_tools_active = true;
235 self.window_open = false;
236 } else {
237 self.window_open = true;
238 }
239 }
240
241 pub fn reload_opened_folder(&mut self, ctrl: &mut Control) {
242 if let Err(e) = ctrl.load_opened_folder_content(ctrl.cfg.prj.sort_params) {
243 self.info_message = Info::Error(format!("{e:?}"));
244 }
245 }
246
247 pub fn show_info(&mut self, msg: Info) {
248 self.info_message = msg;
249 }
250
251 pub fn ui(
253 &mut self,
254 ui: &mut egui::Ui,
255 ctrl: &mut Control,
256 tools_data_map: &mut ToolsDataMap,
257 active_tool_name: Option<&str>,
258 ) -> bool {
259 let mut project_loaded = false;
260 self.are_tools_active = true;
262 egui::Panel::top("top-menu-panel").show(ui, |ui| {
263 egui::MenuBar::new().ui(ui, |ui| {
265 let of_response = ui.button("Open Folder");
266 let pick_result = open_folder::pick_by_connection(ctrl, &of_response);
267 handle_error!(pick_result, self);
268 ui.menu_button("Project", |ui| {
269 if ui
270 .button("New")
271 .on_hover_text(
272 "Right click, old project will be closed, unsaved data will get lost",
273 )
274 .secondary_clicked()
275 {
276 *tools_data_map = ctrl.new_prj();
277 ui.close();
278 }
279 if ui.button("Load").clicked() {
280 let prj_path = rfd::FileDialog::new()
281 .add_filter("project files", &["json", "rvi"])
282 .pick_file();
283 if let Some(prj_path) = prj_path {
284 handle_error!(
285 |tdm| {
286 *tools_data_map = tdm;
287 project_loaded = true;
288 },
289 ctrl.load(prj_path),
290 self
291 );
292 }
293 ui.close();
294 }
295 if ui.button("Save").clicked() {
296 let prj_path = save_dialog_in_prjfolder(
297 ctrl.cfg.current_prj_path(),
298 ctrl.opened_folder_label(),
299 );
300
301 if let Some(prj_path) = prj_path {
302 handle_error!(ctrl.save(prj_path, tools_data_map, true), self);
303 }
304 ui.close();
305 }
306 ui.separator();
307 ui.label("Import ...");
308 if ui.button("... Annotations").clicked() {
309 let prj_path = rfd::FileDialog::new()
310 .set_title("Import Annotations from Project")
311 .add_filter("project files", &["json", "rvi"])
312 .pick_file();
313 if let Some(prj_path) = prj_path {
314 handle_error!(
315 |()| {
316 project_loaded = true;
317 },
318 ctrl.import_annos(&prj_path, tools_data_map),
319 self
320 );
321 }
322 ui.close();
323 }
324
325 if ui.button("... Settings").clicked() {
326 if let Some(prj_path) = rfd::FileDialog::new()
328 .set_title("Pick Project to Import Settings From")
329 .add_filter("project files", &["json", "rvi"])
330 .pick_file()
331 {
332 self.prj_import_path = Some(prj_path);
333 }
334 ui.close();
335 }
336 if ui.button("... Annotations and Settings").clicked() {
337 let prj_path = rfd::FileDialog::new()
338 .set_title("Import Annotations and Settings from Project")
339 .add_filter("project files", &["json", "rvi"])
340 .pick_file();
341 if let Some(prj_path) = prj_path {
342 handle_error!(
343 |()| {
344 project_loaded = true;
345 },
346 ctrl.import_both(&prj_path, tools_data_map),
347 self
348 );
349 }
350 ui.close();
351 }
352 ui.horizontal(|ui| {
353 if ui.button("... Annotations from COCO file").clicked() {
354 let coco_path = if !self.import_coco_from_ssh {
355 rfd::FileDialog::new()
356 .set_title("Annotations from COCO file")
357 .add_filter("coco files", &["json"])
358 .pick_file()
359 .and_then(|p| path_to_str(&p).ok().map(|s| s.to_string()))
360 } else {
361 Some(self.text_buffers.import_coco_from_ssh_path.clone())
362 };
363 if let Some(coco_path) = coco_path {
364 handle_error!(
365 |()| {
366 project_loaded = true;
367 },
368 ctrl.import_from_coco(
369 &coco_path,
370 tools_data_map,
371 if self.import_coco_from_ssh {
372 ExportPathConnection::Ssh
373 } else {
374 ExportPathConnection::Local
375 }
376 ),
377 self
378 );
379 }
380 ui.close();
381 }
382 ui.checkbox(&mut self.import_coco_from_ssh, "ssh")
383 });
384
385 if self.import_coco_from_ssh {
386 text_edit_singleline(
387 ui,
388 &mut self.text_buffers.import_coco_from_ssh_path,
389 &mut self.are_tools_active,
390 );
391 }
392 });
393
394 let autosave_gui = AutosaveMenu::new(
395 ctrl,
396 tools_data_map,
397 &mut project_loaded,
398 &mut self.are_tools_active,
399 &mut self.annotations_menu_params,
400 &mut self.new_file_idx_annoplot,
401 );
402 ui.add(autosave_gui);
403 ctrl.paths_navigator
404 .select_label_idx(self.new_file_idx_annoplot);
405
406 let cfg_gui = CfgMenu::new(
407 &mut ctrl.cfg,
408 &mut self.are_tools_active,
409 &mut self.toggle_clear_cache_on_close,
410 );
411 ui.add(cfg_gui);
412 if self.toggle_clear_cache_on_close {
413 if let Some(reader) = &mut ctrl.reader {
414 reader.toggle_clear_cache_on_close();
415 }
416 self.toggle_clear_cache_on_close = false;
417 }
418
419 ui.menu_button("Wand", |ui| {
420 let to_submit = wand_many::predict_button(
421 ui,
422 &ctrl.data.wand_many,
423 ctrl.paths_navigator.paths_selector(),
424 );
425 if let Some(WandManyMenuResult::Submit((files, folders_to_exclude))) = to_submit
426 {
427 ctrl.submit_files_to_wand(
428 tools_data_map,
429 &files,
430 ctrl.file_selected_idx,
431 &folders_to_exclude,
432 );
433 } else if let Some(WandManyMenuResult::Cancel) = to_submit {
434 ctrl.cancel_wandmany();
435 }
436 if ui.button("Settings").clicked() {
437 self.show_wandmany = true;
438 }
439 ui.separator();
440 if ui.button("Start Wand Server").clicked() {
441 handle_error!(ctrl.start_wandserver(), self);
442 }
443 if ui.button("Cleanup Wand Server").clicked() {
444 handle_error!(ctrl.cleanup_wandserver(), self);
445 }
446 });
447 if self.show_wandmany {
448 ctrl.data.wand_many.is_wandmany_running = ctrl.is_wandmany_running();
449 let result = wand_many_menu(
450 ui,
451 &mut ctrl.data.wand_many,
452 &mut ctrl.cfg.prj.wand_many,
453 &mut self.are_tools_active,
454 &mut self.text_buffers.wand_many_buffers,
455 &mut self.show_wandmany,
456 ctrl.paths_navigator.paths_selector(),
457 );
458 if let WandManyMenuResult::Submit((files, folders_to_exclude)) = result {
459 ctrl.submit_files_to_wand(
460 tools_data_map,
461 &files,
462 ctrl.file_selected_idx,
463 &folders_to_exclude,
464 );
465 } else if let WandManyMenuResult::Cancel = result {
466 ctrl.cancel_wandmany();
467 }
468 }
469 ui.menu_button("Help", |ui| {
470 ui.label("RV Image\n");
471 const CODE: &str = env!("CARGO_PKG_REPOSITORY");
472 let version_label = version_label();
473 ui.label(version_label);
474 if let Some(reader) = &mut ctrl.reader {
475 ui.label("cache size in mb");
476 ui.label(
477 egui::RichText::new(format!("{:.3}", reader.cache_size_in_mb()))
478 .monospace(),
479 );
480 ui.label("Hit F5 to clear the cache.");
481 ui.label("");
482 }
483 ui.hyperlink_to("Docs, License, and Code", CODE);
484 if ui.button("Export Logs").clicked() {
485 let log_export_dst = rfd::FileDialog::new()
486 .add_filter("zip", &["zip"])
487 .set_file_name("logs.zip")
488 .save_file();
489
490 ctrl.log_export_path = log_export_dst;
491 ui.close();
492 }
493 let resp_close = ui.button("Close");
494 if resp_close.clicked() {
495 ui.close();
496 }
497 });
498 });
499 });
500 if self.prj_import_path.is_some() {
502 egui::modal::Modal::new(egui::Id::new("prj-import-section")).show(ui.ctx(), |ui| {
503 ui.label("Project Settings Import");
504 if let Some(p) = &self.prj_import_path {
505 ui.label(RichText::new(format!("{}", p.display())).monospace());
506 }
507 let mut changed = false;
508 ui.horizontal(|ui| {
509 ui.vertical(|ui| {
510 if ui
511 .radio_value(
512 &mut self.prj_import_section,
513 PrjSettingImportSection::All,
514 "All",
515 )
516 .clicked()
517 {
518 changed = true;
519 }
520 if ui
521 .radio_value(
522 &mut self.prj_import_section,
523 PrjSettingImportSection::Connection,
524 "Connection",
525 )
526 .clicked()
527 {
528 changed = true;
529 }
530 if ui
531 .radio_value(
532 &mut self.prj_import_section,
533 PrjSettingImportSection::WandServer,
534 "Wand Server",
535 )
536 .clicked()
537 {
538 changed = true;
539 }
540 });
541 if let Some(p) = &self.prj_import_path {
542 if self.prj_settings_for_display.is_none() || changed {
543 self.prj_settings_for_display =
544 Some(ctrl.show_settings(p, self.prj_import_section));
545 }
546 if let Some(settings_str) = &mut self.prj_settings_for_display {
547 egui::ScrollArea::vertical()
548 .min_scrolled_height(500.0)
549 .show(ui, |ui| {
550 ui.add(
551 egui::TextEdit::multiline(settings_str)
552 .font(egui::FontSelection::Style(
553 egui::TextStyle::Monospace,
554 ))
555 .desired_width(f32::INFINITY)
556 .desired_rows(20) .interactive(false), );
559 });
560 }
561 }
562 });
563 ui.horizontal(|ui| {
564 let import_enabled = self.prj_import_path.is_some();
565 if import_enabled
566 && ui.button("Import").clicked()
567 && let Some(prj_path) = self.prj_import_path.take()
568 {
569 handle_error!(
570 |()| {
571 project_loaded = true;
572 },
573 ctrl.import_settings(&prj_path, self.prj_import_section),
574 self
575 );
576 }
577 if ui.button("Cancel").clicked() {
578 self.prj_import_path = None;
579 }
580 });
581 });
582 }
583 egui::Panel::left("left-main-menu").show(ui, |ui| {
584 let mut connected = false;
585 handle_error!(
586 |con| {
587 connected = con;
588 },
589 ctrl.check_if_connected(ctrl.cfg.prj.sort_params),
590 self
591 );
592 if connected {
593 ui.label(
594 RichText::from(ctrl.opened_folder_label().unwrap_or(""))
595 .text_style(egui::TextStyle::Monospace),
596 );
597 } else {
598 ui.label(RichText::from("Connecting...").text_style(egui::TextStyle::Monospace));
599 }
600
601 let filter_txt_field =
602 text_edit_singleline(ui, &mut ctrl.data.filter_buffer, &mut self.are_tools_active);
603
604 if filter_txt_field.changed() {
605 handle_error!(
606 ctrl.paths_navigator.filter(
607 &ctrl.data.filter_buffer,
608 tools_data_map,
609 active_tool_name
610 ),
611 self
612 );
613 }
614 self.info_message = match &self.info_message {
616 Info::Warning(msg) => {
617 show_popup(msg, "❕", self.info_message.clone(), &filter_txt_field)
618 }
619 Info::Error(msg) => {
620 show_popup(msg, "❌", self.info_message.clone(), &filter_txt_field)
621 }
622 Info::None => Info::None,
623 };
624
625 let scroll_to_selected = ctrl.paths_navigator.scroll_to_selected_label();
627 let mut filtered_label_selected_idx = ctrl.paths_navigator.file_label_selected_idx();
628 if let Some(ps) = &ctrl.paths_navigator.paths_selector() {
629 ui.checkbox(&mut self.show_file_options.idx, "show file index");
630 ui.checkbox(
631 &mut self.show_file_options.parentfolder,
632 "show parent folder",
633 );
634
635 self.scroll_offset = menu::scroll_area::scroll_area_file_selector(
636 ui,
637 &mut filtered_label_selected_idx,
638 ps,
639 ctrl.file_info_selected.as_deref(),
640 scroll_to_selected,
641 self.scroll_offset,
642 self.show_file_options,
643 );
644 ctrl.paths_navigator.deactivate_scroll_to_selected_label();
645 if ctrl.paths_navigator.file_label_selected_idx() != filtered_label_selected_idx {
646 ctrl.paths_navigator
647 .select_label_idx(filtered_label_selected_idx);
648 }
649 }
650
651 ui.separator();
652 let mut sort_params = ctrl.cfg.prj.sort_params;
653 handle_error!(
654 labels_and_sorting(ui, &mut sort_params, ctrl, tools_data_map, &mut self.stats,),
655 self
656 );
657 ctrl.cfg.prj.sort_params = sort_params;
658 if button_confirmed(
659 ui,
660 "Pre-cache filtered images",
661 "Pre-cache filtered Images",
662 "Might take a while, are you sure?",
663 ) {
664 self.cache_all_progress = Some(0.0);
665 }
666 if self.cache_all_progress.is_some() {
667 handle_error!(
668 |prgs| {
669 self.cache_all_progress = prgs;
670 },
671 ctrl.cache_all_filtered(),
672 self
673 );
674 if let Some(prgs) = &self.cache_all_progress {
675 ui.add(
676 egui::ProgressBar::new(*prgs).text(
677 RichText::new(format!(
678 "loading images into cache {:2}%",
679 (prgs * 100.0).floor() as u8
680 ))
681 .monospace(),
682 ),
683 );
684 }
685 if self.cache_all_progress > Some(0.999) {
686 self.cache_all_progress = None;
687 }
688 }
689 });
690 project_loaded
691 }
692}
693
694impl Default for Menu {
695 fn default() -> Self {
696 Self::new()
697 }
698}
699
700pub fn are_tools_active(menu: &Menu, tsm: &ToolSelectMenu) -> bool {
701 menu.are_tools_active && tsm.are_tools_active
702}