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