Skip to main content

rvlib/tools/
attributes.rs

1use tracing::info;
2
3use super::Manipulate;
4use crate::{
5    annotations_accessor_mut,
6    events::Events,
7    file_util::PathPair,
8    history::{History, Record},
9    make_tool_transform,
10    parameters::{ParamMap, ParamVal},
11    result::trace_ok_err,
12    tools_data::{AttributesToolData, attributes_data::set_attrmap_val},
13    tools_data_accessors,
14    world::World,
15    world_annotations_accessor,
16};
17use std::mem;
18const MISSING_DATA_MSG: &str = "Missing data for Attributes";
19pub const ACTOR_NAME: &str = "Attributes";
20annotations_accessor_mut!(
21    ACTOR_NAME,
22    attributes_mut,
23    "Attribute didn't work",
24    ParamMap
25);
26world_annotations_accessor!(ACTOR_NAME, attributes, "Attribute didn't work", ParamMap);
27tools_data_accessors!(
28    ACTOR_NAME,
29    MISSING_DATA_MSG,
30    attributes_data,
31    AttributesToolData,
32    attributes,
33    attributes_mut
34);
35
36fn propagate_annos(
37    mut annos: ParamMap,
38    attr_names: &[String],
39    to_propagate: &[(usize, ParamVal)],
40) -> ParamMap {
41    for (attr_idx, val) in to_propagate {
42        if let Some(attr_val) = attr_names
43            .get(*attr_idx)
44            .and_then(|attr_name| annos.get_mut(attr_name))
45        {
46            *attr_val = val.clone();
47        }
48    }
49    annos
50}
51
52fn get_buffers(world: &World) -> Vec<String> {
53    let annos = get_annos(world);
54    let data = get_specific(world);
55    if let (Some(data), Some(annos)) = (data, annos) {
56        data.attr_names()
57            .iter()
58            .map(|attr_name| {
59                if let Some(attrval) = annos.get(attr_name) {
60                    attrval.to_string()
61                } else {
62                    "".to_string()
63                }
64            })
65            .collect()
66    } else {
67        vec![]
68    }
69}
70fn propagate_buffer(
71    mut attribute_buffer: Vec<String>,
72    to_propagate: &[(usize, ParamVal)],
73) -> Vec<String> {
74    for (attr_idx, val) in to_propagate {
75        if let Some(ab) = attribute_buffer.get_mut(*attr_idx) {
76            *ab = val.to_string();
77        }
78    }
79    attribute_buffer
80}
81/// Copies a pending attribute edit from the menu (`current_attr_map`) into the
82/// world's annotations. Returns true if an update was applied.
83fn apply_menu_update(world: &mut World) -> bool {
84    let is_update_triggered = get_specific(world).map(|d| d.options.is_update_triggered);
85    if is_update_triggered == Some(true) {
86        info!("update attr");
87        let current_from_menu_clone = get_specific(world).and_then(|d| d.current_attr_map.clone());
88        if let (Some(mut cfm), Some(anno)) = (current_from_menu_clone, get_annos_mut(world)) {
89            *anno = mem::take(&mut cfm);
90        }
91        if let Some(update_current_attr_map) =
92            get_specific_mut(world).map(|d| &mut d.options.is_update_triggered)
93        {
94            *update_current_attr_map = false;
95        }
96        true
97    } else {
98        false
99    }
100}
101
102fn file_change(mut world: World) -> World {
103    use_currentimageshape_for_annos(&mut world);
104    let attr_buffers = get_buffers(&world);
105    let annos = get_annos_mut(&mut world).map(mem::take);
106    let data = get_specific_mut(&mut world);
107
108    if let (Some(data), Some(mut annos)) = (data, annos) {
109        // add all attributes to a new file
110        for (attr_name, attr_val) in data.attr_names().iter().zip(data.attr_vals().iter()) {
111            if !annos.contains(attr_name) {
112                set_attrmap_val(&mut annos, attr_name, attr_val.clone().reset());
113            }
114        }
115
116        // the other way around, check if attributes exist in the data but not as part of the tool
117        // smells like a corrupt project file if this happens
118        for (attr_name, attr_val) in annos.iter() {
119            if !data.attr_names().contains(attr_name) {
120                tracing::warn!(
121                    "Attribute {attr_name} exists in the data but not in the tool data, adding it"
122                );
123                data.push(attr_name.clone(), attr_val.clone().reset());
124            }
125        }
126
127        // put string representations of the attribute values into the buffer
128        let attr_buffers = propagate_buffer(attr_buffers, &data.to_propagate_attr_val);
129        for (i, buffer) in attr_buffers.into_iter().enumerate() {
130            if let Some(attr_buffer) = data.attr_value_buffer_mut(i) {
131                *attr_buffer = buffer;
132            }
133        }
134
135        annos = propagate_annos(annos, data.attr_names(), &data.to_propagate_attr_val);
136
137        if let Some(annos_) = get_annos_mut(&mut world) {
138            *annos_ = annos;
139        }
140    }
141    let current = get_annos(&world).cloned();
142    if let Some(data) = get_specific_mut(&mut world) {
143        data.current_attr_map = current;
144    }
145    world
146}
147fn add_attribute(
148    mut world: World,
149    mut history: History,
150    suppress_exists_err: bool,
151) -> (World, History) {
152    let attr_map_tmp = get_annos_mut(&mut world).map(mem::take);
153    let data = get_specific_mut(&mut world);
154
155    if let (Some(mut attr_map_tmp), Some(data)) = (attr_map_tmp, data) {
156        let new_attr_name = data.new_attr_name.clone();
157        if data.attr_names().contains(&new_attr_name) && !suppress_exists_err {
158            tracing::error!("New attribute {new_attr_name} could not be created, already exists");
159        } else {
160            let new_attr_val = data.new_attr_val.clone();
161            for (_, (val_map, _)) in data.anno_iter_mut() {
162                set_attrmap_val(val_map, &new_attr_name, new_attr_val.clone());
163            }
164            set_attrmap_val(&mut attr_map_tmp, &new_attr_name, new_attr_val.clone());
165            if let Some(a) = get_annos_mut(&mut world) {
166                a.clone_from(&attr_map_tmp);
167            }
168            if let Some(data) = get_specific_mut(&mut world) {
169                data.current_attr_map = Some(attr_map_tmp);
170                data.push(new_attr_name, new_attr_val);
171                history.push(Record::new(world.clone(), ACTOR_NAME));
172            }
173        }
174    }
175    if let Some(data) = get_specific_mut(&mut world) {
176        data.options.is_addition_triggered = false;
177        data.new_attr_name = String::new();
178        data.new_attr_val = ParamVal::default();
179    }
180    (world, history)
181}
182
183fn check_remove(mut world: World, mut history: History) -> (World, History) {
184    if let Some(removal_idx) = get_specific(&world).map(|d| d.options.removal_idx) {
185        let data = get_specific_mut(&mut world);
186        if let (Some(data), Some(removal_idx)) = (data, removal_idx) {
187            data.remove_attr(removal_idx);
188            history.push(Record::new(world.clone(), ACTOR_NAME));
189        }
190        if let Some(removal_idx) = get_specific_mut(&mut world).map(|d| &mut d.options.removal_idx)
191        {
192            *removal_idx = None;
193        }
194    }
195    (world, history)
196}
197
198#[derive(Clone, Copy, Debug)]
199pub struct Attributes;
200
201impl Manipulate for Attributes {
202    fn new() -> Self
203    where
204        Self: Sized,
205    {
206        Self
207    }
208
209    fn on_activate(&mut self, mut world: World) -> World {
210        let data = get_data_mut(&mut world);
211        if let Some(data) = trace_ok_err(data) {
212            data.menu_active = true;
213        }
214        file_change(world)
215    }
216    fn on_deactivate(&mut self, mut world: World) -> World {
217        let data = get_data_mut(&mut world);
218        if let Some(data) = trace_ok_err(data) {
219            data.menu_active = false;
220        }
221        world
222    }
223    fn on_filechange(&mut self, world: World, history: History) -> (World, History) {
224        (file_change(world), history)
225    }
226    fn before_file_change(&mut self, mut world: World) -> World {
227        // Flush an edit that is still pending in the menu (e.g. the text field
228        // kept focus, so the tool events did not run) into the annotations of
229        // the file that is about to be left. Otherwise the edit would be lost.
230        apply_menu_update(&mut world);
231        world
232    }
233    fn events_tf(
234        &mut self,
235        mut world: World,
236        mut history: History,
237        _event: &Events,
238    ) -> (World, History) {
239        let is_addition_triggered = get_specific(&world).map(|d| d.options.is_addition_triggered);
240        if is_addition_triggered == Some(true) {
241            // handle addition triggered in the GUI
242            (world, history) = add_attribute(world, history, false);
243        }
244        let attr_data = get_specific_mut(&mut world);
245        if let Some(attr_data) = attr_data
246            && let Some(rename_src_idx) = attr_data.options.rename_src_idx
247        {
248            let from_name = attr_data.attr_names().get(rename_src_idx).cloned();
249            let to_name = &attr_data.new_attr_name.clone();
250            if let Some(from_name) = from_name {
251                tracing::info!("Rename attribute {from_name} to {to_name}");
252                attr_data.rename(&from_name, to_name);
253                attr_data.options.rename_src_idx = None;
254            } else {
255                tracing::error!("could not rename attribute {from_name:?} to {to_name}");
256            }
257        }
258        (world, history) = check_remove(world, history);
259
260        let is_export_triggered =
261            get_specific(&world).map(|d| d.options.import_export_trigger.export_triggered());
262        if is_export_triggered == Some(true) {
263            let ssh_cfg = world.data.meta_data.ssh_cfg.clone();
264            let attr_data = get_specific(&world);
265            let export_only_opened_folder =
266                attr_data.map(|d| d.options.export_only_opened_folder) == Some(true);
267            let key_filter = if export_only_opened_folder {
268                world
269                    .data
270                    .meta_data
271                    .opened_folder
272                    .as_ref()
273                    .map(PathPair::path_relative)
274            } else {
275                None
276            };
277            let annos_str = get_specific(&world)
278                .and_then(|d| trace_ok_err(d.serialize_annotations(key_filter)));
279            if let (Some(annos_str), Some(data)) = (annos_str, get_specific(&world))
280                && trace_ok_err(data.export_path.conn.write(
281                    &annos_str,
282                    &data.export_path.path,
283                    ssh_cfg.as_ref(),
284                ))
285                .is_some()
286            {
287                info!("exported annotations to {:?}", data.export_path.path);
288            }
289
290            if let Some(export_triggered) =
291                get_specific_mut(&mut world).map(|d| &mut d.options.import_export_trigger)
292            {
293                export_triggered.untrigger_export();
294            }
295        }
296        let is_import_triggered =
297            get_specific(&world).map(|d| d.options.import_export_trigger.import_triggered());
298        if is_import_triggered == Some(true) {
299            tracing::info!("import attr tiggered");
300            let ssh_cfg = world.data.meta_data.ssh_cfg.clone();
301            let cur_prj = world.data.meta_data.prj_path().map(|p| p.to_path_buf());
302            let attr_data = get_specific_mut(&mut world);
303            let imported_map = attr_data.and_then(|data| {
304                let in_path = &data.export_path.path;
305                tracing::info!("importing attributes from {in_path:?}");
306                let json_str = trace_ok_err(data.export_path.conn.read(in_path, ssh_cfg.as_ref()));
307                if let Some(s) = json_str {
308                    trace_ok_err(AttributesToolData::deserialize_annotations(
309                        &s,
310                        cur_prj.as_deref(),
311                    ))
312                } else {
313                    None
314                }
315            });
316            if let Some(imported_map) = &imported_map {
317                // add attributes in case they don't exist
318                for (_, (attr_map, _)) in imported_map.iter() {
319                    for (attr_name, attr_val) in attr_map.iter() {
320                        let data = get_specific_mut(&mut world);
321                        if let Some(d) = data {
322                            d.new_attr_name = attr_name.clone();
323                            d.new_attr_val = attr_val.clone().reset();
324                        }
325                        tracing::debug!("inserting attr {attr_name} with value {attr_val}");
326                        (world, history) = add_attribute(world, history, true);
327                    }
328                }
329            }
330            if let Some(imported_map) = imported_map {
331                let data = get_specific_mut(&mut world);
332                if let Some(d) = data {
333                    d.merge_map(imported_map);
334                }
335            }
336            let annos = get_annos(&world).cloned();
337            let attr_buffer = get_buffers(&world);
338            if let (Some(data), Some(annos)) = (get_specific_mut(&mut world), annos) {
339                data.current_attr_map = Some(annos);
340                data.set_new_attr_value_buffer(attr_buffer);
341            }
342        }
343        if let Some(import_trigger) =
344            get_specific_mut(&mut world).map(|d| &mut d.options.import_export_trigger)
345        {
346            import_trigger.untrigger_import();
347        }
348        make_tool_transform!(self, world, history, event, [])
349    }
350}
351#[cfg(test)]
352use {
353    crate::tracing_setup::init_tracing_for_tests,
354    crate::types::{ThumbIms, ViewImage},
355    image::DynamicImage,
356    std::collections::HashMap,
357    std::fs,
358    std::path::Path,
359};
360#[cfg(test)]
361pub(super) fn test_data() -> (World, History) {
362    use std::path::Path;
363
364    use crate::ToolsDataMap;
365
366    let im_test = DynamicImage::ImageRgb8(ViewImage::new(64, 64));
367    let mut world = World::from_real_im(
368        im_test,
369        ThumbIms::default(),
370        ToolsDataMap::new(),
371        None,
372        Some("superimage.png".to_string()),
373        Path::new("superimage.png"),
374        Some(0),
375    );
376    world.data.meta_data.flags.is_loading_screen_active = Some(false);
377
378    let history = History::default();
379    (world, history)
380}
381#[test]
382fn test_import_export() {
383    init_tracing_for_tests();
384    fn test(testpath: &Path) {
385        let (mut world, history) = test_data();
386        let data = get_specific_mut(&mut world).unwrap();
387        let json_str = fs::read_to_string(testpath).unwrap();
388        let reference_data = AttributesToolData::deserialize_annotations(&json_str, None).unwrap();
389        tracing::debug!("reference_data: {:?}", reference_data);
390        data.export_path.path = testpath.to_path_buf();
391        data.options.import_export_trigger.trigger_import();
392        let events = Events::default();
393        let (world, _) = Attributes {}.events_tf(world, history, &events);
394        let annos = world.data.tools_data_map[ACTOR_NAME]
395            .specifics
396            .attributes()
397            .unwrap()
398            .anno_iter()
399            .collect::<HashMap<_, _>>();
400        tracing::debug!("annos: {:?}", annos);
401        for k in reference_data.keys() {
402            tracing::debug!("k: {:?}", k);
403            let (annos, _) = annos.get(k).unwrap();
404            let (ref_annos, _) = &reference_data[k];
405            assert_eq!(annos, ref_annos);
406        }
407        let current = get_annos(&world).unwrap();
408        for v in current.values() {
409            assert!(v.is_default());
410        }
411    }
412    let testpath = Path::new("resources/test_data/attr_import.json");
413    test(testpath);
414    let testpath = Path::new("resources/test_data/attr_import_untagged.json");
415    test(testpath);
416}
417
418#[test]
419fn test_add() {
420    init_tracing_for_tests();
421    let mut attr_tool = Attributes::new();
422    let events = Events::default();
423    let (mut world, history) = test_data();
424    let attr_data = get_specific_mut(&mut world).unwrap();
425    attr_data.options.is_addition_triggered = true;
426    attr_data.new_attr_name = "a attr".to_string();
427    attr_data.new_attr_val = ParamVal::Int(Some(1));
428    let (mut world, history) = attr_tool.events_tf(world, history, &events);
429    let attr_data = get_specific_mut(&mut world).unwrap();
430    attr_data.options.is_addition_triggered = true;
431    attr_data.new_attr_name = "c attr".to_string();
432    attr_data.new_attr_val = ParamVal::Int(Some(2));
433    let (mut world, history) = attr_tool.events_tf(world, history, &events);
434    let attr_data = get_specific_mut(&mut world).unwrap();
435    attr_data.options.is_addition_triggered = true;
436    attr_data.new_attr_name = "b attr".to_string();
437    attr_data.new_attr_val = ParamVal::Int(Some(3));
438    let (world, _) = attr_tool.events_tf(world, history, &events);
439    let data = get_specific(&world).unwrap();
440    let cam = data.current_attr_map.as_ref().unwrap();
441    let c_attr_val = cam.get("c attr").unwrap();
442    assert_eq!(c_attr_val, &ParamVal::Int(Some(2)));
443    let b_attr_val = cam.get("b attr").unwrap();
444    assert_eq!(b_attr_val, &ParamVal::Int(Some(3)));
445    let a_attr_val = cam.get("a attr").unwrap();
446    assert_eq!(a_attr_val, &ParamVal::Int(Some(1)));
447
448    // cur map is a BTree and hence sorted
449    assert_eq!(data.attr_names(), &["a attr", "b attr", "c attr"]);
450    assert_eq!(
451        data.attr_vals(),
452        &[
453            ParamVal::Int(Some(1)),
454            ParamVal::Int(Some(3)),
455            ParamVal::Int(Some(2)),
456        ]
457    );
458}
459#[test]
460fn test_rm_add() {
461    init_tracing_for_tests();
462    let (mut world, history) = test_data();
463    let attr_data = get_specific_mut(&mut world).unwrap();
464    attr_data.options.is_addition_triggered = true;
465    attr_data.new_attr_name = "test_attr".to_string();
466    attr_data.new_attr_val = ParamVal::Str("123".into());
467    let (mut world, history) = add_attribute(world, history, false);
468    let attr_data = get_specific_mut(&mut world).unwrap();
469    attr_data.options.removal_idx = Some(0);
470    let (mut world, _) = check_remove(world, history);
471    let attr_data = get_specific_mut(&mut world).unwrap();
472    assert!(!attr_data.options.is_addition_triggered);
473    assert!(attr_data.options.removal_idx.is_none());
474    assert_eq!(
475        attr_data.current_attr_map.as_ref().map(|cam| cam.len()),
476        Some(0)
477    );
478}