1use crate::{
2 cfg::ExportPathConnection,
3 control::{Control, Info},
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 ui_util::text_edit_singleline,
13 },
14 tools::ToolState,
15 tools_data::{ToolSpecifics, ToolsDataMap},
16 util::version_label,
17};
18use egui::{Context, Popup, Response, RichText, Ui};
19use rvimage_domain::{rverr, RvResult};
20use std::{
21 f32, mem,
22 path::{Path, PathBuf},
23};
24
25use super::{
26 file_counts::Counts,
27 tools_menus::{attributes_menu, bbox_menu, brush_menu},
28};
29
30fn show_popup(msg: &str, icon: &str, info_message: Info, response: &Response) -> Info {
31 let mut new_msg = Info::None;
32 Popup::from_response(response)
33 .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
34 .show(|ui| {
35 let max_msg_len = 500;
36 let shortened_msg = if msg.len() > max_msg_len {
37 &msg[..max_msg_len]
38 } else {
39 msg
40 };
41 let mut txt = format!("{icon} {shortened_msg}");
42 ui.text_edit_multiline(&mut txt);
43 new_msg = if ui.button("Close").clicked() {
44 Info::None
45 } else {
46 info_message
47 }
48 });
49 new_msg
50}
51
52macro_rules! handle_error {
57 ($f_effect:expr, $f_err_cleanup:expr, $result:expr, $self:expr) => {
58 match $result {
59 Ok(r) => {
60 #[allow(clippy::redundant_closure_call)]
61 $f_effect(r);
62 }
63 Err(e) => {
64 #[allow(clippy::redundant_closure_call)]
65 $f_err_cleanup();
66 tracing::error!("{e:?}");
67 $self.info_message = Info::Error(e.to_string());
68 }
69 }
70 };
71 ($effect:expr, $result:expr, $self:expr) => {
72 handle_error!($effect, || (), $result, $self)
73 };
74 ($result:expr, $self:expr) => {
75 handle_error!(|_| {}, $result, $self);
76 };
77}
78
79pub struct ToolSelectMenu {
80 are_tools_active: bool, recently_activated_tool: Option<usize>,
82}
83impl ToolSelectMenu {
84 fn new() -> Self {
85 Self {
86 are_tools_active: true,
87 recently_activated_tool: None,
88 }
89 }
90 pub fn recently_clicked_tool(&self) -> Option<usize> {
91 self.recently_activated_tool
92 }
93 pub fn ui(
94 &mut self,
95 ui: &mut Ui,
96 tools: &mut [ToolState],
97 tools_menu_map: &mut ToolsDataMap,
98 ) -> RvResult<()> {
99 ui.horizontal_top(|ui| {
100 self.recently_activated_tool = tools
101 .iter_mut()
102 .enumerate()
103 .filter(|(_, t)| !t.is_always_active())
104 .find(|(_, t)| ui.selectable_label(t.is_active(), t.button_label).clicked())
105 .map(|(i, _)| i);
106 });
107 for v in tools_menu_map.values_mut().filter(|v| v.menu_active) {
108 let mut v_result = Err(rverr!("Tool menu not implemented"));
109 egui::ScrollArea::vertical()
110 .auto_shrink([false, false])
111 .max_height(f32::INFINITY)
112 .show(ui, |ui| {
113 let tmp = match &mut v.specifics {
114 ToolSpecifics::Bbox(x) => bbox_menu(
115 ui,
116 v.menu_active,
117 mem::take(x),
118 &mut self.are_tools_active,
119 v.visible_inactive_tools.clone(),
120 ),
121 ToolSpecifics::Brush(x) => brush_menu(
122 ui,
123 v.menu_active,
124 mem::take(x),
125 &mut self.are_tools_active,
126 v.visible_inactive_tools.clone(),
127 ),
128 ToolSpecifics::Attributes(x) => attributes_menu(
129 ui,
130 v.menu_active,
131 mem::take(x),
132 &mut self.are_tools_active,
133 ),
134 _ => Ok(mem::take(v)),
135 };
136 v_result = tmp;
137 });
138 *v = v_result?;
139 }
140 Ok(())
141 }
142}
143impl Default for ToolSelectMenu {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149fn save_dialog_in_prjfolder(prj_path: &Path, opened_folder: Option<&str>) -> Option<PathBuf> {
150 let filename = get_prj_name(prj_path, opened_folder);
151 let dialog = rfd::FileDialog::new();
152 let dialog = if let Some(folder) = prj_path.parent() {
153 dialog.set_directory(folder)
154 } else {
155 dialog
156 };
157 dialog
158 .add_filter("project files", &["json", "rvi"])
159 .set_file_name(filename)
160 .save_file()
161}
162
163#[derive(Default)]
164pub struct TextBuffers {
165 pub filter_string: String,
166 pub label_propagation: String,
167 pub label_deletion: String,
168 pub import_coco_from_ssh_path: String,
169}
170
171pub struct Menu {
172 window_open: bool, info_message: Info,
174 are_tools_active: bool,
175 toggle_clear_cache_on_close: bool,
176 scroll_offset: f32,
177 stats: Counts,
178 text_buffers: TextBuffers,
179 show_file_idx: bool,
180 annotations_menu_params: AnnotationsParams,
181 import_coco_from_ssh: bool,
182 new_file_idx_annoplot: Option<usize>,
183}
184
185impl Menu {
186 fn new() -> Self {
187 let text_buffers = TextBuffers {
188 filter_string: "".to_string(),
189 label_propagation: "".to_string(),
190 label_deletion: "".to_string(),
191 import_coco_from_ssh_path: "path on ssh server".to_string(),
192 };
193 Self {
194 window_open: true,
195 info_message: Info::None,
196 are_tools_active: true,
197 toggle_clear_cache_on_close: false,
198 scroll_offset: 0.0,
199 stats: Counts::default(),
200 text_buffers,
201 show_file_idx: true,
202 annotations_menu_params: AnnotationsParams::default(),
203 import_coco_from_ssh: false,
204 new_file_idx_annoplot: None,
205 }
206 }
207 pub fn popup(&mut self, info: Info) {
208 self.info_message = info;
209 }
210
211 pub fn toggle(&mut self) {
212 if self.window_open {
213 self.are_tools_active = true;
214 self.window_open = false;
215 } else {
216 self.window_open = true;
217 }
218 }
219
220 pub fn reload_opened_folder(&mut self, ctrl: &mut Control) {
221 if let Err(e) = ctrl.load_opened_folder_content(ctrl.cfg.prj.sort_params) {
222 self.info_message = Info::Error(format!("{e:?}"));
223 }
224 }
225
226 pub fn show_info(&mut self, msg: Info) {
227 self.info_message = msg;
228 }
229
230 pub fn ui(
232 &mut self,
233 ctx: &Context,
234 ctrl: &mut Control,
235 tools_data_map: &mut ToolsDataMap,
236 active_tool_name: Option<&str>,
237 ) -> bool {
238 let mut project_loaded = false;
239 egui::TopBottomPanel::top("top-menu-bar").show(ctx, |ui| {
240 egui::MenuBar::new().ui(ui, |ui| {
242 let of_response = ui.button("Open Folder");
243 let pick_result = open_folder::pick_by_connection(ctrl, &of_response);
244 handle_error!(pick_result, self);
245 ui.menu_button("Project", |ui| {
246 if ui
247 .button("New")
248 .on_hover_text(
249 "Right click, old project will be closed, unsaved data will get lost",
250 )
251 .secondary_clicked()
252 {
253 *tools_data_map = ctrl.new_prj();
254 ui.close();
255 }
256 if ui.button("Load").clicked() {
257 let prj_path = rfd::FileDialog::new()
258 .add_filter("project files", &["json", "rvi"])
259 .pick_file();
260 if let Some(prj_path) = prj_path {
261 handle_error!(
262 |tdm| {
263 *tools_data_map = tdm;
264 project_loaded = true;
265 },
266 ctrl.load(prj_path),
267 self
268 );
269 }
270 ui.close();
271 }
272 if ui.button("Save").clicked() {
273 let prj_path = save_dialog_in_prjfolder(
274 ctrl.cfg.current_prj_path(),
275 ctrl.opened_folder_label(),
276 );
277
278 if let Some(prj_path) = prj_path {
279 handle_error!(ctrl.save(prj_path, tools_data_map, true), self);
280 }
281 ui.close();
282 }
283 ui.separator();
284 ui.label("Import ...");
285 if ui.button("... Annotations").clicked() {
286 let prj_path = rfd::FileDialog::new()
287 .set_title("Import Annotations from Project")
288 .add_filter("project files", &["json", "rvi"])
289 .pick_file();
290 if let Some(prj_path) = prj_path {
291 handle_error!(
292 |()| {
293 project_loaded = true;
294 },
295 ctrl.import_annos(&prj_path, tools_data_map),
296 self
297 );
298 }
299 ui.close();
300 }
301 if ui.button("... Settings").clicked() {
302 let prj_path = rfd::FileDialog::new()
303 .set_title("Import Settings from Project")
304 .add_filter("project files", &["json", "rvi"])
305 .pick_file();
306 if let Some(prj_path) = prj_path {
307 handle_error!(
308 |()| {
309 project_loaded = true;
310 },
311 ctrl.import_settings(&prj_path),
312 self
313 );
314 }
315 ui.close();
316 }
317 if ui.button("... Annotations and Settings").clicked() {
318 let prj_path = rfd::FileDialog::new()
319 .set_title("Import Annotations and Settings from Project")
320 .add_filter("project files", &["json", "rvi"])
321 .pick_file();
322 if let Some(prj_path) = prj_path {
323 handle_error!(
324 |()| {
325 project_loaded = true;
326 },
327 ctrl.import_both(&prj_path, tools_data_map),
328 self
329 );
330 }
331 ui.close();
332 }
333 ui.horizontal(|ui| {
334 if ui.button("... Annotations from COCO file").clicked() {
335 let coco_path = if !self.import_coco_from_ssh {
336 rfd::FileDialog::new()
337 .set_title("Annotations from COCO file")
338 .add_filter("coco files", &["json"])
339 .pick_file()
340 .and_then(|p| path_to_str(&p).ok().map(|s| s.to_string()))
341 } else {
342 Some(self.text_buffers.import_coco_from_ssh_path.clone())
343 };
344 if let Some(coco_path) = coco_path {
345 handle_error!(
346 |()| {
347 project_loaded = true;
348 },
349 ctrl.import_from_coco(
350 &coco_path,
351 tools_data_map,
352 if self.import_coco_from_ssh {
353 ExportPathConnection::Ssh
354 } else {
355 ExportPathConnection::Local
356 }
357 ),
358 self
359 );
360 }
361 ui.close();
362 }
363 ui.checkbox(&mut self.import_coco_from_ssh, "ssh")
364 });
365
366 if self.import_coco_from_ssh {
367 text_edit_singleline(
368 ui,
369 &mut self.text_buffers.import_coco_from_ssh_path,
370 &mut self.are_tools_active,
371 );
372 }
373 });
374
375 let autosave_gui = AutosaveMenu::new(
376 ctrl,
377 tools_data_map,
378 &mut project_loaded,
379 &mut self.are_tools_active,
380 &mut self.annotations_menu_params,
381 &mut self.new_file_idx_annoplot,
382 );
383 ui.add(autosave_gui);
384 ctrl.paths_navigator
385 .select_label_idx(self.new_file_idx_annoplot);
386
387 let cfg_gui = CfgMenu::new(
388 &mut ctrl.cfg,
389 &mut self.are_tools_active,
390 &mut self.toggle_clear_cache_on_close,
391 );
392 ui.add(cfg_gui);
393 if self.toggle_clear_cache_on_close {
394 if let Some(reader) = &mut ctrl.reader {
395 reader.toggle_clear_cache_on_close();
396 }
397 self.toggle_clear_cache_on_close = false;
398 }
399
400 ui.menu_button("Help", |ui| {
401 ui.label("RV Image\n");
402 const CODE: &str = env!("CARGO_PKG_REPOSITORY");
403 let version_label = version_label();
404 ui.label(version_label);
405 if let Some(reader) = &mut ctrl.reader {
406 ui.label("cache size in mb");
407 ui.label(
408 egui::RichText::new(format!("{:.3}", reader.cache_size_in_mb()))
409 .monospace(),
410 );
411 ui.label("Hit F5 to clear the cache.");
412 ui.label("");
413 }
414 ui.hyperlink_to("Docs, License, and Code", CODE);
415 if ui.button("Export Logs").clicked() {
416 let log_export_dst = rfd::FileDialog::new()
417 .add_filter("zip", &["zip"])
418 .set_file_name("logs.zip")
419 .save_file();
420
421 ctrl.log_export_path = log_export_dst;
422 ui.close();
423 }
424 let resp_close = ui.button("Close");
425 if resp_close.clicked() {
426 ui.close();
427 }
428 });
429 });
430 });
431 egui::SidePanel::left("left-main-menu").show(ctx, |ui| {
432 let mut connected = false;
433 handle_error!(
434 |con| {
435 connected = con;
436 },
437 ctrl.check_if_connected(ctrl.cfg.prj.sort_params),
438 self
439 );
440 if connected {
441 ui.label(
442 RichText::from(ctrl.opened_folder_label().unwrap_or(""))
443 .text_style(egui::TextStyle::Monospace),
444 );
445 } else {
446 ui.label(RichText::from("Connecting...").text_style(egui::TextStyle::Monospace));
447 }
448
449 let filter_txt_field = text_edit_singleline(
450 ui,
451 &mut self.text_buffers.filter_string,
452 &mut self.are_tools_active,
453 );
454
455 if filter_txt_field.changed() {
456 handle_error!(
457 ctrl.paths_navigator.filter(
458 &self.text_buffers.filter_string,
459 tools_data_map,
460 active_tool_name
461 ),
462 self
463 );
464 }
465 self.info_message = match &self.info_message {
467 Info::Warning(msg) => {
468 show_popup(msg, "❕", self.info_message.clone(), &filter_txt_field)
469 }
470 Info::Error(msg) => {
471 show_popup(msg, "❌", self.info_message.clone(), &filter_txt_field)
472 }
473 Info::None => Info::None,
474 };
475
476 let scroll_to_selected = ctrl.paths_navigator.scroll_to_selected_label();
478 let mut filtered_label_selected_idx = ctrl.paths_navigator.file_label_selected_idx();
479 if let Some(ps) = &ctrl.paths_navigator.paths_selector() {
480 ui.checkbox(&mut self.show_file_idx, "show file index");
481
482 self.scroll_offset = menu::scroll_area::scroll_area_file_selector(
483 ui,
484 &mut filtered_label_selected_idx,
485 ps,
486 ctrl.file_info_selected.as_deref(),
487 scroll_to_selected,
488 self.scroll_offset,
489 self.show_file_idx,
490 );
491 ctrl.paths_navigator.deactivate_scroll_to_selected_label();
492 if ctrl.paths_navigator.file_label_selected_idx() != filtered_label_selected_idx {
493 ctrl.paths_navigator
494 .select_label_idx(filtered_label_selected_idx);
495 }
496 }
497
498 ui.separator();
499 let mut sort_params = ctrl.cfg.prj.sort_params;
500 handle_error!(
501 labels_and_sorting(
502 ui,
503 &mut sort_params,
504 ctrl,
505 tools_data_map,
506 &mut self.text_buffers,
507 &mut self.stats,
508 ),
509 self
510 );
511 ctrl.cfg.prj.sort_params = sort_params;
512 });
513 project_loaded
514 }
515}
516
517impl Default for Menu {
518 fn default() -> Self {
519 Self::new()
520 }
521}
522
523pub fn are_tools_active(menu: &Menu, tsm: &ToolSelectMenu) -> bool {
524 menu.are_tools_active && tsm.are_tools_active
525}