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