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 new_file_idx_annoplot: Option<usize>,
187}
188
189impl Menu {
190 fn new() -> Self {
191 let text_buffers = TextBuffers {
192 filter_string: "".to_string(),
193 label_propagation: "".to_string(),
194 label_deletion: "".to_string(),
195 import_coco_from_ssh_path: "path on ssh server".to_string(),
196 };
197 Self {
198 window_open: true,
199 info_message: Info::None,
200 are_tools_active: true,
201 toggle_clear_cache_on_close: false,
202 scroll_offset: 0.0,
203 open_folder_popup_open: false,
204 stats: Counts::default(),
205 text_buffers,
206 show_file_idx: true,
207 annotations_menu_params: AnnotationsParams::default(),
208 import_coco_from_ssh: false,
209 new_file_idx_annoplot: None,
210 }
211 }
212 pub fn popup(&mut self, info: Info) {
213 self.info_message = info;
214 }
215
216 pub fn toggle(&mut self) {
217 if self.window_open {
218 self.are_tools_active = true;
219 self.window_open = false;
220 } else {
221 self.window_open = true;
222 }
223 }
224
225 pub fn reload_opened_folder(&mut self, ctrl: &mut Control) {
226 if let Err(e) = ctrl.load_opened_folder_content(ctrl.cfg.prj.sort_params) {
227 self.info_message = Info::Error(format!("{e:?}"));
228 }
229 }
230
231 pub fn show_info(&mut self, msg: Info) {
232 self.info_message = msg;
233 }
234
235 pub fn ui(
237 &mut self,
238 ctx: &Context,
239 ctrl: &mut Control,
240 tools_data_map: &mut ToolsDataMap,
241 active_tool_name: Option<&str>,
242 ) -> bool {
243 let mut projected_loaded = false;
244 egui::TopBottomPanel::top("top-menu-bar").show(ctx, |ui| {
245 egui::menu::bar(ui, |ui| {
247 let button_resp = open_folder::button(ui, ctrl, self.open_folder_popup_open);
248 handle_error!(
249 |open| {
250 self.open_folder_popup_open = open;
251 },
252 || self.open_folder_popup_open = false,
253 button_resp,
254 self
255 );
256 ui.menu_button("Project", |ui| {
257 if ui
258 .button("New")
259 .on_hover_text(
260 "Double click, old project will be closed, unsaved data will get lost",
261 )
262 .double_clicked()
263 {
264 *tools_data_map = ctrl.new_prj();
265 ui.close_menu();
266 }
267 if ui.button("Load").clicked() {
268 let prj_path = rfd::FileDialog::new()
269 .add_filter("project files", &["json", "rvi"])
270 .pick_file();
271 if let Some(prj_path) = prj_path {
272 handle_error!(
273 |tdm| {
274 *tools_data_map = tdm;
275 projected_loaded = true;
276 },
277 ctrl.load(prj_path),
278 self
279 );
280 }
281 ui.close_menu();
282 }
283 if ui.button("Save").clicked() {
284 let prj_path = save_dialog_in_prjfolder(
285 ctrl.cfg.current_prj_path(),
286 ctrl.opened_folder_label(),
287 );
288
289 if let Some(prj_path) = prj_path {
290 handle_error!(ctrl.save(prj_path, tools_data_map, true), self);
291 }
292 ui.close_menu();
293 }
294 ui.separator();
295 ui.label("Import ...");
296 if ui.button("... Annotations").clicked() {
297 let prj_path = rfd::FileDialog::new()
298 .set_title("Import Annotations from Project")
299 .add_filter("project files", &["json", "rvi"])
300 .pick_file();
301 if let Some(prj_path) = prj_path {
302 handle_error!(
303 |()| {
304 projected_loaded = true;
305 },
306 ctrl.import_annos(&prj_path, tools_data_map),
307 self
308 );
309 }
310 ui.close_menu();
311 }
312 if ui.button("... Settings").clicked() {
313 let prj_path = rfd::FileDialog::new()
314 .set_title("Import Settings from Project")
315 .add_filter("project files", &["json", "rvi"])
316 .pick_file();
317 if let Some(prj_path) = prj_path {
318 handle_error!(
319 |()| {
320 projected_loaded = true;
321 },
322 ctrl.import_settings(&prj_path),
323 self
324 );
325 }
326 ui.close_menu();
327 }
328 if ui.button("... Annotations and Settings").clicked() {
329 let prj_path = rfd::FileDialog::new()
330 .set_title("Import Annotations and Settings from Project")
331 .add_filter("project files", &["json", "rvi"])
332 .pick_file();
333 if let Some(prj_path) = prj_path {
334 handle_error!(
335 |()| {
336 projected_loaded = true;
337 },
338 ctrl.import_both(&prj_path, tools_data_map),
339 self
340 );
341 }
342 ui.close_menu();
343 }
344 ui.horizontal(|ui| {
345 if ui.button("... Annotations from COCO file").clicked() {
346 let coco_path = if !self.import_coco_from_ssh {
347 rfd::FileDialog::new()
348 .set_title("Annotations from COCO file")
349 .add_filter("coco files", &["json"])
350 .pick_file()
351 .and_then(|p| path_to_str(&p).ok().map(|s| s.to_string()))
352 } else {
353 Some(self.text_buffers.import_coco_from_ssh_path.clone())
354 };
355 if let Some(coco_path) = coco_path {
356 handle_error!(
357 |()| {
358 projected_loaded = true;
359 },
360 ctrl.import_from_coco(
361 &coco_path,
362 tools_data_map,
363 if self.import_coco_from_ssh {
364 ExportPathConnection::Ssh
365 } else {
366 ExportPathConnection::Local
367 }
368 ),
369 self
370 );
371 }
372 ui.close_menu();
373 }
374 ui.checkbox(&mut self.import_coco_from_ssh, "ssh")
375 });
376
377 if self.import_coco_from_ssh {
378 text_edit_singleline(
379 ui,
380 &mut self.text_buffers.import_coco_from_ssh_path,
381 &mut self.are_tools_active,
382 );
383 }
384 });
385
386 let popup_id = ui.make_persistent_id("autosave-popup");
387 let autosave_gui = AutosaveMenu::new(
388 popup_id,
389 ctrl,
390 tools_data_map,
391 &mut projected_loaded,
392 &mut self.are_tools_active,
393 &mut self.annotations_menu_params,
394 &mut self.new_file_idx_annoplot,
395 );
396 ui.add(autosave_gui);
397 ctrl.paths_navigator
398 .select_label_idx(self.new_file_idx_annoplot);
399
400 let popup_id = ui.make_persistent_id("cfg-popup");
401 let cfg_gui = CfgMenu::new(
402 popup_id,
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("Help", |ui| {
416 ui.label("RV Image\n");
417 const CODE: &str = env!("CARGO_PKG_REPOSITORY");
418 let version_label = version_label();
419 ui.label(version_label);
420 if let Some(reader) = &mut ctrl.reader {
421 ui.label("cache size in mb");
422 ui.label(
423 egui::RichText::new(format!("{:.3}", reader.cache_size_in_mb()))
424 .monospace(),
425 );
426 ui.label("Hit F5 to clear the cache.");
427 ui.label("");
428 }
429 ui.hyperlink_to("Docs, License, and Code", CODE);
430 if ui.button("Export Logs").clicked() {
431 let log_export_dst = rfd::FileDialog::new()
432 .add_filter("zip", &["zip"])
433 .set_file_name("logs.zip")
434 .save_file();
435
436 ctrl.log_export_path = log_export_dst;
437 ui.close_menu();
438 }
439 let resp_close = ui.button("Close");
440 if resp_close.clicked() {
441 ui.close_menu();
442 }
443 });
444 });
445 });
446 egui::SidePanel::left("left-main-menu").show(ctx, |ui| {
447 let mut connected = false;
448 handle_error!(
449 |con| {
450 connected = con;
451 },
452 ctrl.check_if_connected(ctrl.cfg.prj.sort_params),
453 self
454 );
455 if connected {
456 ui.label(
457 RichText::from(ctrl.opened_folder_label().unwrap_or(""))
458 .text_style(egui::TextStyle::Monospace),
459 );
460 } else {
461 ui.label(RichText::from("Connecting...").text_style(egui::TextStyle::Monospace));
462 }
463
464 let filter_txt_field = text_edit_singleline(
465 ui,
466 &mut self.text_buffers.filter_string,
467 &mut self.are_tools_active,
468 );
469
470 if filter_txt_field.changed() {
471 handle_error!(
472 ctrl.paths_navigator.filter(
473 &self.text_buffers.filter_string,
474 tools_data_map,
475 active_tool_name
476 ),
477 self
478 );
479 }
480 let popup_id = ui.make_persistent_id("info-popup");
482 self.info_message = match &self.info_message {
483 Info::Warning(msg) => show_popup(
484 ui,
485 msg,
486 "❕",
487 popup_id,
488 self.info_message.clone(),
489 &filter_txt_field,
490 ),
491 Info::Error(msg) => show_popup(
492 ui,
493 msg,
494 "❌",
495 popup_id,
496 self.info_message.clone(),
497 &filter_txt_field,
498 ),
499 Info::None => Info::None,
500 };
501
502 let scroll_to_selected = ctrl.paths_navigator.scroll_to_selected_label();
504 let mut filtered_label_selected_idx = ctrl.paths_navigator.file_label_selected_idx();
505 if let Some(ps) = &ctrl.paths_navigator.paths_selector() {
506 ui.checkbox(&mut self.show_file_idx, "show file index");
507
508 self.scroll_offset = menu::scroll_area::scroll_area_file_selector(
509 ui,
510 &mut filtered_label_selected_idx,
511 ps,
512 ctrl.file_info_selected.as_deref(),
513 scroll_to_selected,
514 self.scroll_offset,
515 self.show_file_idx,
516 );
517 ctrl.paths_navigator.deactivate_scroll_to_selected_label();
518 if ctrl.paths_navigator.file_label_selected_idx() != filtered_label_selected_idx {
519 ctrl.paths_navigator
520 .select_label_idx(filtered_label_selected_idx);
521 }
522 }
523
524 ui.separator();
525 let mut sort_params = ctrl.cfg.prj.sort_params;
526 handle_error!(
527 labels_and_sorting(
528 ui,
529 &mut sort_params,
530 ctrl,
531 tools_data_map,
532 &mut self.text_buffers,
533 &mut self.stats,
534 ),
535 self
536 );
537 ctrl.cfg.prj.sort_params = sort_params;
538 });
539 projected_loaded
540 }
541}
542
543impl Default for Menu {
544 fn default() -> Self {
545 Self::new()
546 }
547}
548
549pub fn are_tools_active(menu: &Menu, tsm: &ToolSelectMenu) -> bool {
550 menu.are_tools_active && tsm.are_tools_active
551}