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::text_edit_singleline,
14 wand_many::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_inside(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 self.show_wandmany = true;
418 }
419 ui.separator();
420 if ui.button("Start Wand Server").clicked() {
421 handle_error!(ctrl.start_wandserver(), self);
422 }
423 if ui.button("Cleanup Wand Server").clicked() {
424 handle_error!(ctrl.cleanup_wandserver(), self);
425 }
426 });
427 if self.show_wandmany
428 && let Some((files, folders_to_exclude)) = wand_many_menu(
429 ui,
430 &mut ctrl.data.wand_many,
431 &mut ctrl.cfg.prj.wand_many,
432 &mut self.are_tools_active,
433 &mut self.text_buffers.wand_many_buffers,
434 &mut self.show_wandmany,
435 ctrl.paths_navigator.paths_selector(),
436 )
437 {
438 ctrl.submit_files_to_wand(
439 tools_data_map,
440 &files,
441 ctrl.file_selected_idx,
442 &folders_to_exclude,
443 );
444 }
445 ui.menu_button("Help", |ui| {
446 ui.label("RV Image\n");
447 const CODE: &str = env!("CARGO_PKG_REPOSITORY");
448 let version_label = version_label();
449 ui.label(version_label);
450 if let Some(reader) = &mut ctrl.reader {
451 ui.label("cache size in mb");
452 ui.label(
453 egui::RichText::new(format!("{:.3}", reader.cache_size_in_mb()))
454 .monospace(),
455 );
456 ui.label("Hit F5 to clear the cache.");
457 ui.label("");
458 }
459 ui.hyperlink_to("Docs, License, and Code", CODE);
460 if ui.button("Export Logs").clicked() {
461 let log_export_dst = rfd::FileDialog::new()
462 .add_filter("zip", &["zip"])
463 .set_file_name("logs.zip")
464 .save_file();
465
466 ctrl.log_export_path = log_export_dst;
467 ui.close();
468 }
469 let resp_close = ui.button("Close");
470 if resp_close.clicked() {
471 ui.close();
472 }
473 });
474 });
475 });
476 if self.prj_import_path.is_some() {
478 egui::modal::Modal::new(egui::Id::new("prj-import-section")).show(ui.ctx(), |ui| {
479 ui.label("Project Settings Import");
480 if let Some(p) = &self.prj_import_path {
481 ui.label(RichText::new(format!("{}", p.display())).monospace());
482 }
483 let mut changed = false;
484 ui.horizontal(|ui| {
485 ui.vertical(|ui| {
486 if ui
487 .radio_value(
488 &mut self.prj_import_section,
489 PrjSettingImportSection::All,
490 "All",
491 )
492 .clicked()
493 {
494 changed = true;
495 }
496 if ui
497 .radio_value(
498 &mut self.prj_import_section,
499 PrjSettingImportSection::Connection,
500 "Connection",
501 )
502 .clicked()
503 {
504 changed = true;
505 }
506 if ui
507 .radio_value(
508 &mut self.prj_import_section,
509 PrjSettingImportSection::WandServer,
510 "Wand Server",
511 )
512 .clicked()
513 {
514 changed = true;
515 }
516 });
517 if let Some(p) = &self.prj_import_path {
518 if self.prj_settings_for_display.is_none() || changed {
519 self.prj_settings_for_display =
520 Some(ctrl.show_settings(p, self.prj_import_section));
521 }
522 if let Some(settings_str) = &mut self.prj_settings_for_display {
523 egui::ScrollArea::vertical()
524 .min_scrolled_height(500.0)
525 .show(ui, |ui| {
526 ui.add(
527 egui::TextEdit::multiline(settings_str)
528 .font(egui::FontSelection::Style(
529 egui::TextStyle::Monospace,
530 ))
531 .desired_width(f32::INFINITY)
532 .desired_rows(20) .interactive(false), );
535 });
536 }
537 }
538 });
539 ui.horizontal(|ui| {
540 let import_enabled = self.prj_import_path.is_some();
541 if import_enabled
542 && ui.button("Import").clicked()
543 && let Some(prj_path) = self.prj_import_path.take()
544 {
545 handle_error!(
546 |()| {
547 project_loaded = true;
548 },
549 ctrl.import_settings(&prj_path, self.prj_import_section),
550 self
551 );
552 }
553 if ui.button("Cancel").clicked() {
554 self.prj_import_path = None;
555 }
556 });
557 });
558 }
559 egui::Panel::left("left-main-menu").show_inside(ui, |ui| {
560 let mut connected = false;
561 handle_error!(
562 |con| {
563 connected = con;
564 },
565 ctrl.check_if_connected(ctrl.cfg.prj.sort_params),
566 self
567 );
568 if connected {
569 ui.label(
570 RichText::from(ctrl.opened_folder_label().unwrap_or(""))
571 .text_style(egui::TextStyle::Monospace),
572 );
573 } else {
574 ui.label(RichText::from("Connecting...").text_style(egui::TextStyle::Monospace));
575 }
576
577 let filter_txt_field =
578 text_edit_singleline(ui, &mut ctrl.data.filter_buffer, &mut self.are_tools_active);
579
580 if filter_txt_field.changed() {
581 handle_error!(
582 ctrl.paths_navigator.filter(
583 &ctrl.data.filter_buffer,
584 tools_data_map,
585 active_tool_name
586 ),
587 self
588 );
589 }
590 self.info_message = match &self.info_message {
592 Info::Warning(msg) => {
593 show_popup(msg, "❕", self.info_message.clone(), &filter_txt_field)
594 }
595 Info::Error(msg) => {
596 show_popup(msg, "❌", self.info_message.clone(), &filter_txt_field)
597 }
598 Info::None => Info::None,
599 };
600
601 let scroll_to_selected = ctrl.paths_navigator.scroll_to_selected_label();
603 let mut filtered_label_selected_idx = ctrl.paths_navigator.file_label_selected_idx();
604 if let Some(ps) = &ctrl.paths_navigator.paths_selector() {
605 ui.checkbox(&mut self.show_file_options.idx, "show file index");
606 ui.checkbox(
607 &mut self.show_file_options.parentfolder,
608 "show parent folder",
609 );
610
611 self.scroll_offset = menu::scroll_area::scroll_area_file_selector(
612 ui,
613 &mut filtered_label_selected_idx,
614 ps,
615 ctrl.file_info_selected.as_deref(),
616 scroll_to_selected,
617 self.scroll_offset,
618 self.show_file_options,
619 );
620 ctrl.paths_navigator.deactivate_scroll_to_selected_label();
621 if ctrl.paths_navigator.file_label_selected_idx() != filtered_label_selected_idx {
622 ctrl.paths_navigator
623 .select_label_idx(filtered_label_selected_idx);
624 }
625 }
626
627 ui.separator();
628 let mut sort_params = ctrl.cfg.prj.sort_params;
629 handle_error!(
630 labels_and_sorting(ui, &mut sort_params, ctrl, tools_data_map, &mut self.stats,),
631 self
632 );
633 ctrl.cfg.prj.sort_params = sort_params;
634 if ui
635 .button("Pre-cache filtered images")
636 .on_hover_text("double click")
637 .double_clicked()
638 {
639 self.cache_all_progress = Some(0.0);
640 }
641 if self.cache_all_progress.is_some() {
642 handle_error!(
643 |prgs| {
644 self.cache_all_progress = prgs;
645 },
646 ctrl.cache_all_filtered(),
647 self
648 );
649 if let Some(prgs) = &self.cache_all_progress {
650 ui.add(
651 egui::ProgressBar::new(*prgs).text(
652 RichText::new(format!(
653 "loading images into cache {:2}%",
654 (prgs * 100.0).floor() as u8
655 ))
656 .monospace(),
657 ),
658 );
659 }
660 if self.cache_all_progress > Some(0.999) {
661 self.cache_all_progress = None;
662 }
663 }
664 });
665 project_loaded
666 }
667}
668
669impl Default for Menu {
670 fn default() -> Self {
671 Self::new()
672 }
673}
674
675pub fn are_tools_active(menu: &Menu, tsm: &ToolSelectMenu) -> bool {
676 menu.are_tools_active && tsm.are_tools_active
677}