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