mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-27 02:27:18 +00:00
progress
This commit is contained in:
1 parent
41b9116c6a
commit
97d597e139
181 files changed
+1970
-2643
No files matched your search
@@ -0,0 +1,288 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, CommandError, FileId, Files, Selection, State, Waypoint};
|
||||
|
||||
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
|
||||
/// are deleted instead, even if only a track or a waypoint is selected.
|
||||
#[derive(Debug)]
|
||||
pub struct Delete {
|
||||
pub whole_files: bool,
|
||||
}
|
||||
|
||||
impl Apply for Delete {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let files = state.selection.to_files();
|
||||
let selection = if self.whole_files {
|
||||
&files
|
||||
} else {
|
||||
&*state.selection
|
||||
};
|
||||
let next = match selection {
|
||||
Selection::File { file_ids } => {
|
||||
state.files.retain(|id, _| !file_ids.contains(id));
|
||||
state.order.0.retain(|id| !file_ids.contains(id));
|
||||
Selection::Empty
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let file = Rc::make_mut(file);
|
||||
let len = file.trk.len();
|
||||
file.trk.retain(|trk| !trk_ids.contains(&trk.id));
|
||||
if file.trk.len() == len {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
}
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let file = state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let file = Rc::make_mut(file);
|
||||
let trk = file
|
||||
.trk
|
||||
.iter_mut()
|
||||
.find(|trk| trk.id == *trk_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let len = trk.trkseg.len();
|
||||
trk.trkseg.retain(|seg| !trkseg_ids.contains(&seg.id));
|
||||
if trk.trkseg.len() == len {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids: [*trk_id].into(),
|
||||
}
|
||||
}
|
||||
Selection::Waypoints { file_id } => delete_waypoints(state.files, *file_id, |_| true)?,
|
||||
Selection::Waypoint { file_id, wpt_ids } => {
|
||||
delete_waypoints(state.files, *file_id, |wpt| wpt_ids.contains(&wpt.id))?
|
||||
}
|
||||
Selection::Empty => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
*state.selection = next;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn delete_waypoints(
|
||||
files: &mut Files,
|
||||
file_id: FileId,
|
||||
filter: impl Fn(&Waypoint) -> bool,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let mut file = (**file).clone();
|
||||
let changed = file.wpt.edit(&filter, |wpts| {
|
||||
wpts.retain(|wpt| !filter(wpt));
|
||||
true
|
||||
});
|
||||
if !changed {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
files.insert(file_id, Rc::new(file));
|
||||
Ok(Selection::File {
|
||||
file_ids: [file_id].into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
use crate::{FileId, Load};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded() -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_nothing_selected() {
|
||||
let mut fx = Fixture::default();
|
||||
assert_eq!(
|
||||
Delete { whole_files: false }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_selected_files() {
|
||||
let (mut fx, id) = loaded();
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let keep = fx.order.0[1];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
Delete { whole_files: false }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0, vec![keep]);
|
||||
assert!(fx.files.contains_key(&keep) && !fx.files.contains_key(&id));
|
||||
assert!(matches!(fx.selection, Selection::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_selected_tracks() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].trk.len();
|
||||
let trk_id = fx.files[&id].trk[0].id;
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: HashSet::from([trk_id]),
|
||||
};
|
||||
Delete { whole_files: false }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = &fx.files[&id];
|
||||
assert_eq!(file.trk.len(), before - 1);
|
||||
assert!(file.trk.iter().all(|t| t.id != trk_id));
|
||||
assert_eq!(fx.selected_files(), HashSet::from([id]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_selected_segments() {
|
||||
let (mut fx, id) = loaded();
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
let (trk_id, seg_id, before) = (trk.id, trk.trkseg[0].id, trk.trkseg.len());
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id,
|
||||
trkseg_ids: HashSet::from([seg_id]),
|
||||
};
|
||||
Delete { whole_files: false }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
assert_eq!(trk.trkseg.len(), before - 1);
|
||||
assert!(trk.trkseg.iter().all(|s| s.id != seg_id));
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::Track { trk_ids, .. } if trk_ids.contains(&trk_id))
|
||||
);
|
||||
}
|
||||
|
||||
fn with_waypoints(fx: &mut Fixture, id: FileId, n: usize) -> Vec<crate::WaypointId> {
|
||||
let wpts: Vec<_> = (0..n).map(|_| Waypoint::default()).collect();
|
||||
let ids = wpts.iter().map(|w| w.id).collect();
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
file.wpt = crate::Waypoints::new([crate::WaypointChunk {
|
||||
wpt: wpts,
|
||||
..Default::default()
|
||||
}]);
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
ids
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_selected_waypoints() {
|
||||
let (mut fx, id) = loaded();
|
||||
let ids = with_waypoints(&mut fx, id, 3);
|
||||
fx.selection = Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: HashSet::from([ids[1]]),
|
||||
};
|
||||
let rev = fx.files[&id].wpt.rev_id;
|
||||
Delete { whole_files: false }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_ne!(fx.files[&id].wpt.rev_id, rev);
|
||||
let left: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
|
||||
assert_eq!(left, vec![ids[0], ids[2]]);
|
||||
assert_eq!(fx.selected_files(), HashSet::from([id]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_all_waypoints_of_file() {
|
||||
let (mut fx, id) = loaded();
|
||||
with_waypoints(&mut fx, id, 3);
|
||||
fx.selection = Selection::Waypoints { file_id: id };
|
||||
Delete { whole_files: false }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert!(fx.files[&id].wpt.is_empty());
|
||||
fx.selection = Selection::Waypoints { file_id: id };
|
||||
assert_eq!(
|
||||
Delete { whole_files: false }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_unknown_ids_is_nothing_to_do() {
|
||||
let (mut fx, id) = loaded();
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: HashSet::from([Default::default()]),
|
||||
};
|
||||
assert_eq!(
|
||||
Delete { whole_files: false }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_whole_files_of_the_selected_elements() {
|
||||
for selection in [
|
||||
|id: FileId, fx: &Fixture| Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: HashSet::from([fx.files[&id].trk[0].id]),
|
||||
},
|
||||
|id: FileId, fx: &Fixture| Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: fx.files[&id].trk[0].id,
|
||||
trkseg_ids: HashSet::from([fx.files[&id].trk[0].trkseg[0].id]),
|
||||
},
|
||||
|id: FileId, _: &Fixture| Selection::Waypoints { file_id: id },
|
||||
|id: FileId, _: &Fixture| Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
},
|
||||
] {
|
||||
let (mut fx, id) = loaded();
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let keep = fx.order.0[1];
|
||||
fx.selection = selection(id, &fx);
|
||||
Delete { whole_files: true }.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.order.0, vec![keep]);
|
||||
assert!(!fx.files.contains_key(&id));
|
||||
assert!(matches!(fx.selection, Selection::Empty));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_whole_files_nothing_selected() {
|
||||
let mut fx = Fixture::default();
|
||||
assert_eq!(
|
||||
Delete { whole_files: true }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::{Apply, CommandError, Selection, State};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DeleteAll;
|
||||
|
||||
impl Apply for DeleteAll {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
if state.files.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
state.files.clear();
|
||||
state.order.0.clear();
|
||||
*state.selection = Selection::Empty;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_delete_all() {
|
||||
let mut fx = Fixture::default();
|
||||
assert_eq!(
|
||||
DeleteAll.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
crate::New {
|
||||
name: "a",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
crate::New {
|
||||
name: "b",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert!(DeleteAll.apply(&mut fx.state()).is_ok());
|
||||
assert!(fx.files.is_empty());
|
||||
assert!(fx.order.0.is_empty());
|
||||
assert!(matches!(fx.selection, Selection::Empty));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use crate::{Apply, CommandError, FileId, Selection, State, WaypointId, delete_waypoints};
|
||||
|
||||
/// Deletes a waypoint of a file, whatever is selected (what the delete button of its popup does).
|
||||
/// If it was the only selected element, its file gets selected.
|
||||
#[derive(Debug)]
|
||||
pub struct DeleteWaypoint {
|
||||
pub file_id: FileId,
|
||||
pub waypoint_id: WaypointId,
|
||||
}
|
||||
|
||||
impl Apply for DeleteWaypoint {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
delete_waypoints(state.files, self.file_id, |wpt| wpt.id == self.waypoint_id)?;
|
||||
if let Selection::Waypoint { file_id, wpt_ids } = &*state.selection
|
||||
&& *file_id == self.file_id
|
||||
&& wpt_ids.iter().all(|id| *id == self.waypoint_id)
|
||||
{
|
||||
*state.selection = Selection::File {
|
||||
file_ids: [self.file_id].into(),
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
File, Selection, Waypoint, WaypointChunk, engine::command::fixture::Fixture, waypoint_ids,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deletes_one_waypoint_without_touching_the_selection() {
|
||||
let mut fx = Fixture::default();
|
||||
let mut file = File::default();
|
||||
file.wpt.push(WaypointChunk {
|
||||
wpt: (0..3)
|
||||
.map(|i| Waypoint {
|
||||
name: Some(format!("w{i}")),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
});
|
||||
let id = file.id;
|
||||
let ids: Vec<_> = waypoint_ids(&file).collect();
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
fx.selection = Selection::File {
|
||||
file_ids: [id].into(),
|
||||
};
|
||||
|
||||
DeleteWaypoint {
|
||||
file_id: id,
|
||||
waypoint_id: ids[1],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let names: Vec<_> = fx.files[&id]
|
||||
.wpt
|
||||
.iter()
|
||||
.map(|wpt| wpt.name.clone().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, ["w0", "w2"]);
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::File {
|
||||
file_ids: [id].into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deleting_the_selected_waypoint_selects_its_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let mut file = File::default();
|
||||
file.wpt.push(WaypointChunk {
|
||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||
..Default::default()
|
||||
});
|
||||
let id = file.id;
|
||||
let ids: Vec<_> = waypoint_ids(&file).collect();
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.selection = Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: [ids[0]].into(),
|
||||
};
|
||||
DeleteWaypoint {
|
||||
file_id: id,
|
||||
waypoint_id: ids[0],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::File {
|
||||
file_ids: [id].into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_waypoint_is_nothing_to_do() {
|
||||
let mut fx = Fixture::default();
|
||||
let file = File::default();
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
let result = DeleteWaypoint {
|
||||
file_id: id,
|
||||
waypoint_id: WaypointId::default(),
|
||||
}
|
||||
.apply(&mut fx.state());
|
||||
assert!(matches!(result, Err(CommandError::NothingToDo)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_segment,
|
||||
copy_track, copy_waypoint,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Duplicate;
|
||||
|
||||
impl Apply for Duplicate {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let next = match &*state.selection {
|
||||
Selection::File { file_ids } => {
|
||||
let mut copies = HashSet::new();
|
||||
let mut order = Vec::new();
|
||||
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
||||
let Some(file) = state.files.get(id) else {
|
||||
continue;
|
||||
};
|
||||
let copy = copy_file(file);
|
||||
order.push(copy.id);
|
||||
copies.insert(copy.id);
|
||||
state.files.insert(copy.id, Rc::new(copy));
|
||||
}
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let at = state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.rposition(|id| file_ids.contains(id))
|
||||
.map_or(state.order.0.len(), |i| i + 1);
|
||||
state.order.0.splice(at..at, order);
|
||||
Selection::File { file_ids: copies }
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let file = Rc::make_mut(file);
|
||||
let mut copies = HashSet::new();
|
||||
append_copies(
|
||||
&mut file.trk,
|
||||
|trk| trk_ids.contains(&trk.id),
|
||||
|trk| {
|
||||
let copy = copy_track(trk);
|
||||
copies.insert(copy.id);
|
||||
copy
|
||||
},
|
||||
);
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids: copies,
|
||||
}
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let file = state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let file = Rc::make_mut(file);
|
||||
let trk = file
|
||||
.trk
|
||||
.iter_mut()
|
||||
.find(|trk| trk.id == *trk_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let mut copies = HashSet::new();
|
||||
append_copies(
|
||||
&mut trk.trkseg,
|
||||
|seg| trkseg_ids.contains(&seg.id),
|
||||
|seg| {
|
||||
let copy = copy_segment(seg);
|
||||
copies.insert(copy.id);
|
||||
copy
|
||||
},
|
||||
);
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id: *file_id,
|
||||
trk_id: *trk_id,
|
||||
trkseg_ids: copies,
|
||||
}
|
||||
}
|
||||
Selection::Waypoints { file_id } => {
|
||||
duplicate_waypoints(state.files, *file_id, |_| true)?
|
||||
}
|
||||
Selection::Waypoint { file_id, wpt_ids } => {
|
||||
duplicate_waypoints(state.files, *file_id, |wpt| wpt_ids.contains(&wpt.id))?
|
||||
}
|
||||
Selection::Empty => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
*state.selection = next;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn duplicate_waypoints(
|
||||
files: &mut Files,
|
||||
file_id: FileId,
|
||||
filter: impl Fn(&Waypoint) -> bool,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let selected: Vec<&Waypoint> = file.wpt.iter().filter(|wpt| filter(wpt)).collect();
|
||||
let last = selected.last().ok_or(CommandError::NothingToDo)?.id;
|
||||
let copies: Vec<Waypoint> = selected.into_iter().map(copy_waypoint).collect();
|
||||
let copy_ids = copies.iter().map(|wpt| wpt.id).collect();
|
||||
|
||||
// The copies go right after the last selected waypoint.
|
||||
let mut file = (**file).clone();
|
||||
file.wpt.insert_after(Some(last), copies);
|
||||
files.insert(file_id, Rc::new(file));
|
||||
Ok(Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: copy_ids,
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts a copy of each item matching `filter`, as a block after the last matching item.
|
||||
fn append_copies<T>(items: &mut Vec<T>, filter: impl Fn(&T) -> bool, copy: impl FnMut(&T) -> T) {
|
||||
let Some(last) = items.iter().rposition(&filter) else {
|
||||
return;
|
||||
};
|
||||
let copies: Vec<T> = items.iter().filter(|item| filter(item)).map(copy).collect();
|
||||
items.splice(last + 1..last + 1, copies);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
use crate::{FileId, Load};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded() -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_nothing_selected() {
|
||||
let mut fx = Fixture::default();
|
||||
assert_eq!(
|
||||
Duplicate.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_file() {
|
||||
let (mut fx, id) = loaded();
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.files.len(), 2);
|
||||
let copy_id = fx.order.0[1];
|
||||
assert_eq!(fx.selected_files(), [copy_id].into());
|
||||
let (orig, copy) = (&fx.files[&id], &fx.files[©_id]);
|
||||
assert_ne!(copy_id, id);
|
||||
assert_eq!(copy.info.name, orig.info.name);
|
||||
assert_eq!(copy.trk.len(), orig.trk.len());
|
||||
for (a, b) in orig.trk.iter().zip(©.trk) {
|
||||
assert_ne!(a.id, b.id);
|
||||
assert_eq!(a.info, b.info);
|
||||
for (a, b) in a.trkseg.iter().zip(&b.trkseg) {
|
||||
assert_ne!(a.id, b.id);
|
||||
assert_eq!(a.len(), b.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_files_go_after_last_selected() {
|
||||
let mut fx = Fixture::default();
|
||||
for name in ["a", "b", "c"] {
|
||||
crate::New {
|
||||
name,
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
}
|
||||
let [a, b, c] = [fx.order.0[0], fx.order.0[1], fx.order.0[2]];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: [a, b].into(),
|
||||
};
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.order.0.len(), 5);
|
||||
assert_eq!(fx.order.0[..2], [a, b]);
|
||||
assert_eq!(fx.order.0[4], c);
|
||||
assert_eq!(
|
||||
fx.selected_files(),
|
||||
fx.order.0[2..4].iter().copied().collect()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_track_appends_copy() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].trk.len();
|
||||
let (trk_id, second_id) = (fx.files[&id].trk[0].id, fx.files[&id].trk[1].id);
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [trk_id].into(),
|
||||
};
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
let file = &fx.files[&id];
|
||||
assert_eq!(file.trk.len(), before + 1);
|
||||
assert_eq!(file.trk[0].id, trk_id);
|
||||
assert_ne!(file.trk[1].id, trk_id);
|
||||
// copies go right after the selected track, not at the end
|
||||
assert_eq!(file.trk[2].id, second_id);
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::Track { trk_ids, .. } if trk_ids.contains(&file.trk[1].id))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_selected_waypoints() {
|
||||
let (mut fx, id) = loaded();
|
||||
let wpts: Vec<_> = (0..3).map(|_| Waypoint::default()).collect();
|
||||
let ids: Vec<_> = wpts.iter().map(|w| w.id).collect();
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
file.wpt = crate::Waypoints::new([crate::WaypointChunk {
|
||||
wpt: wpts,
|
||||
..Default::default()
|
||||
}]);
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
let original_chunk = fx.files[&id].wpt.chunks()[0].clone();
|
||||
|
||||
fx.selection = Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: HashSet::from([ids[0]]),
|
||||
};
|
||||
let rev = fx.files[&id].wpt.rev_id;
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
assert_ne!(fx.files[&id].wpt.rev_id, rev);
|
||||
let all: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
|
||||
assert_eq!(all.len(), 4);
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::Waypoint { wpt_ids, .. } if wpt_ids == &HashSet::from([all[1]]))
|
||||
);
|
||||
// the copy comes right after the selected waypoint, in a new chunk
|
||||
assert_eq!(all[..], [ids[0], all[1], ids[1], ids[2]]);
|
||||
assert_eq!(fx.files[&id].wpt.chunks().len(), 1);
|
||||
assert_ne!(fx.files[&id].wpt.chunks()[0].id, original_chunk.id);
|
||||
|
||||
// selecting the last waypoint: the copy goes at the end
|
||||
let last = fx.files[&id].wpt.iter().last().unwrap().id;
|
||||
fx.selection = Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: HashSet::from([last]),
|
||||
};
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
let all: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
|
||||
assert_eq!(all.len(), 5);
|
||||
assert_eq!(all[..4], [ids[0], all[1], ids[1], ids[2]]);
|
||||
|
||||
fx.selection = Selection::Waypoints { file_id: id };
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.files[&id].wpt.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_segment_appends_copy() {
|
||||
let (mut fx, id) = loaded();
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
let (trk_id, seg_id, before) = (trk.id, trk.trkseg[0].id, trk.trkseg.len());
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id,
|
||||
trkseg_ids: [seg_id].into(),
|
||||
};
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
assert_eq!(trk.trkseg.len(), before + 1);
|
||||
assert_eq!(trk.trkseg[0].id, seg_id);
|
||||
assert_ne!(trk.trkseg[1].id, seg_id);
|
||||
assert_eq!(trk.trkseg[0].len(), trk.trkseg[1].len());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
use crate::{Apply, CommandError, File, State, TrackpointCategories, parse, produce};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Load<'a> {
|
||||
pub data: &'a [u8],
|
||||
/// Name of the file when the data has none (typically the name of the file on disk, without
|
||||
/// its extension).
|
||||
pub name: &'a str,
|
||||
}
|
||||
|
||||
impl Load<'_> {
|
||||
fn parse(self, categories: &mut TrackpointCategories) -> Result<File, CommandError> {
|
||||
let mut file = parse(self.data, categories)
|
||||
.map_err(|err| CommandError::InvalidData(err.to_string()))?;
|
||||
if file.info.name.trim().is_empty() {
|
||||
file.info.name = self.name.to_owned();
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl Apply for Load<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
LoadFiles { files: vec![self] }.apply(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads several files at once, as a single command (one undo step): the files are added at the
|
||||
/// end of the list, in the given order, and the first one is selected.
|
||||
///
|
||||
/// The files that cannot be read are skipped, unless none of them can: then nothing is loaded
|
||||
/// and the error is the one of the first file.
|
||||
#[derive(Debug)]
|
||||
pub struct LoadFiles<'a> {
|
||||
pub files: Vec<Load<'a>>,
|
||||
}
|
||||
|
||||
impl Apply for LoadFiles<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let mut files = Vec::with_capacity(self.files.len());
|
||||
let mut first_error = None;
|
||||
for load in self.files {
|
||||
match load.parse(state.categories) {
|
||||
Ok(file) => files.push(file),
|
||||
Err(error) => {
|
||||
first_error.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if files.is_empty() {
|
||||
return Err(first_error.unwrap_or(CommandError::NothingToDo));
|
||||
}
|
||||
produce(state, |_| files);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_load() {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks.gpx").unwrap();
|
||||
assert!(
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert!(!file.trk.is_empty());
|
||||
assert_eq!(fx.selected_files(), [file.id].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_invalid_file_changes_nothing() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let selected = fx.selected_files();
|
||||
|
||||
let result = Load {
|
||||
data: b"<gpx><trk></gpx>",
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state());
|
||||
assert!(matches!(result, Err(CommandError::InvalidData(_))));
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
assert_eq!(fx.order.0.len(), 1);
|
||||
assert_eq!(fx.selected_files(), selected);
|
||||
}
|
||||
|
||||
fn load_with_name(data: &[u8]) -> String {
|
||||
let mut fx = Fixture::default();
|
||||
Load {
|
||||
data,
|
||||
name: "from disk",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
fx.files.values().next().unwrap().info.name.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_keeps_the_name_of_the_file() {
|
||||
let data = br#"<gpx version="1.1"><metadata><name>in the file</name></metadata></gpx>"#;
|
||||
assert_eq!(load_with_name(data), "in the file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_without_name_uses_the_given_name() {
|
||||
let without = br#"<gpx version="1.1"><trk><trkseg></trkseg></trk></gpx>"#;
|
||||
assert_eq!(load_with_name(without), "from disk");
|
||||
let empty = br#"<gpx version="1.1"><metadata><name> </name></metadata></gpx>"#;
|
||||
assert_eq!(load_with_name(empty), "from disk");
|
||||
}
|
||||
|
||||
fn gpx(name: &str) -> Vec<u8> {
|
||||
format!(r#"<gpx version="1.1"><metadata><name>{name}</name></metadata></gpx>"#).into_bytes()
|
||||
}
|
||||
|
||||
fn load<'a>(data: &'a [u8], name: &'a str) -> Load<'a> {
|
||||
Load { data, name }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_files() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New {
|
||||
name: "before",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let before = fx.order.0[0];
|
||||
let (a, b, c) = (gpx("a"), gpx("b"), gpx("c"));
|
||||
|
||||
LoadFiles {
|
||||
files: vec![load(&a, "x"), load(&b, "x"), load(&c, "x")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// added after the others, in the order they were given
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["before", "a", "b", "c"]);
|
||||
assert_eq!(fx.order.0[0], before);
|
||||
// the first one is selected
|
||||
assert_eq!(fx.selected_files(), [fx.order.0[1]].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_files_skips_the_invalid_ones() {
|
||||
let mut fx = Fixture::default();
|
||||
let (a, c) = (gpx("a"), gpx("c"));
|
||||
LoadFiles {
|
||||
files: vec![load(&a, "x"), load(b"<gpx><trk></gpx>", "x"), load(&c, "x")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["a", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_invalid_files_changes_nothing() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let selected = fx.selected_files();
|
||||
|
||||
let result = LoadFiles {
|
||||
files: vec![
|
||||
load(b"<gpx><trk></gpx>", "x"),
|
||||
load(b"<gpx><wpt></gpx>", "y"),
|
||||
],
|
||||
}
|
||||
.apply(&mut fx.state());
|
||||
assert!(matches!(result, Err(CommandError::InvalidData(_))));
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
assert_eq!(fx.order.0.len(), 1);
|
||||
assert_eq!(fx.selected_files(), selected);
|
||||
|
||||
// nothing to load
|
||||
assert_eq!(
|
||||
LoadFiles { files: vec![] }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_each_file_without_name_gets_its_own_name() {
|
||||
let mut fx = Fixture::default();
|
||||
let unnamed = br#"<gpx version="1.1"><trk><trkseg></trkseg></trk></gpx>"#;
|
||||
LoadFiles {
|
||||
files: vec![load(unnamed, "first"), load(unnamed, "second")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["first", "second"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
mod delete;
|
||||
mod delete_all;
|
||||
mod delete_waypoint;
|
||||
mod duplicate;
|
||||
mod load;
|
||||
mod move_elements;
|
||||
mod new;
|
||||
mod paste;
|
||||
mod transfer;
|
||||
|
||||
pub use delete::*;
|
||||
pub use delete_all::*;
|
||||
pub use delete_waypoint::*;
|
||||
pub use duplicate::*;
|
||||
pub use load::*;
|
||||
pub use move_elements::*;
|
||||
pub use new::*;
|
||||
pub use paste::*;
|
||||
pub use transfer::*;
|
||||
@@ -0,0 +1,447 @@
|
||||
use crate::{
|
||||
Apply, Clipboard, CommandError, Destination, FileId, Place, Selection, State, TrackId, transfer,
|
||||
};
|
||||
|
||||
/// Where moved elements go: a list of the file tree, and the position in it. The position is
|
||||
/// counted among the elements of the list that are not moved, like the position of a dragged
|
||||
/// element in the list it is dropped in.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MoveTarget {
|
||||
/// The list of files.
|
||||
Files { index: usize },
|
||||
/// The tracks of a file.
|
||||
Tracks { file_id: FileId, index: usize },
|
||||
/// The segments of a track.
|
||||
Segments {
|
||||
file_id: FileId,
|
||||
trk_id: TrackId,
|
||||
index: usize,
|
||||
},
|
||||
/// The waypoints of a file.
|
||||
Waypoints { file_id: FileId, index: usize },
|
||||
}
|
||||
|
||||
/// Moves elements to a place of the file tree (what drag and drop does), with their ids. The
|
||||
/// moved elements are selected.
|
||||
///
|
||||
/// What can go where:
|
||||
/// - files: among the files;
|
||||
/// - tracks: among the files (each track becomes a file), or among the tracks of a file;
|
||||
/// - segments: among the files (each segment becomes a file), among the tracks of a file (each
|
||||
/// segment becomes a track), or among the segments of a track;
|
||||
/// - waypoints, or all the waypoints of a file (the waypoints node): among the waypoints of a
|
||||
/// file.
|
||||
#[derive(Debug)]
|
||||
pub struct Move {
|
||||
pub what: Selection,
|
||||
pub to: MoveTarget,
|
||||
}
|
||||
|
||||
impl From<MoveTarget> for Destination {
|
||||
fn from(target: MoveTarget) -> Self {
|
||||
match target {
|
||||
MoveTarget::Files { index } => Destination::Files(Place::Index(index)),
|
||||
MoveTarget::Tracks { file_id, index } => {
|
||||
Destination::Tracks(file_id, Place::Index(index))
|
||||
}
|
||||
MoveTarget::Segments {
|
||||
file_id,
|
||||
trk_id,
|
||||
index,
|
||||
} => Destination::Segments(file_id, trk_id, Place::Index(index)),
|
||||
MoveTarget::Waypoints { file_id, index } => {
|
||||
Destination::Waypoints(file_id, Place::Index(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Apply for Move {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let clipboard = Clipboard::new(&self.what, state.files, &state.order.0, true)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
*state.selection = transfer(state, &clipboard.content, true, self.to.into())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
File, Track, TrackInfo, TrackSegment, TrackSegmentId, Waypoint, WaypointChunk, WaypointId,
|
||||
Waypoints, engine::command::fixture::Fixture, new_file, waypoint_ids,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A file with the given tracks (name, number of segments) and number of waypoints.
|
||||
fn file(name: &str, tracks: &[(&str, usize)], waypoints: usize) -> File {
|
||||
let mut file = new_file(
|
||||
name,
|
||||
tracks
|
||||
.iter()
|
||||
.map(|(name, segments)| Track {
|
||||
info: TrackInfo {
|
||||
name: Some((*name).to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: (0..*segments).map(|_| TrackSegment::default()).collect(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if waypoints > 0 {
|
||||
file.wpt = Waypoints::new([WaypointChunk {
|
||||
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
||||
..Default::default()
|
||||
}]);
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn add(fx: &mut Fixture, file: File) -> FileId {
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn track_ids(fx: &Fixture, id: FileId) -> Vec<TrackId> {
|
||||
fx.files[&id].trk.iter().map(|trk| trk.id).collect()
|
||||
}
|
||||
|
||||
fn segment_ids(fx: &Fixture, id: FileId, trk: usize) -> Vec<TrackSegmentId> {
|
||||
fx.files[&id].trk[trk].trkseg.iter().map(|s| s.id).collect()
|
||||
}
|
||||
|
||||
fn wpt_ids(fx: &Fixture, id: FileId) -> Vec<WaypointId> {
|
||||
waypoint_ids(&fx.files[&id]).collect()
|
||||
}
|
||||
|
||||
fn do_move(fx: &mut Fixture, what: Selection, to: MoveTarget) -> Result<(), CommandError> {
|
||||
Move { what, to }.apply(&mut fx.state())
|
||||
}
|
||||
|
||||
fn tracks(file_id: FileId, ids: &[TrackId]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn segments(file_id: FileId, trk_id: TrackId, ids: &[TrackSegmentId]) -> Selection {
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoints(file_id: FileId, ids: &[WaypointId]) -> Selection {
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn files(ids: &[FileId]) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reorder_tracks_in_a_file() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[("0", 1), ("1", 1), ("2", 1), ("3", 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
(fx, a, t)
|
||||
};
|
||||
let at = |a, index| MoveTarget::Tracks { file_id: a, index };
|
||||
|
||||
// the position counts the tracks that stay
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[0]]), at(a, 1)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[0], t[2], t[3]]);
|
||||
assert_eq!(fx.selection, tracks(a, &[t[0]]));
|
||||
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[3]]), at(a, 0)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[3], t[0], t[1], t[2]]);
|
||||
|
||||
// several at once, as a block, in their order; past the end is the end
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[1], t[0]]), at(a, 99)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[2], t[3], t[0], t[1]]);
|
||||
assert_eq!(fx.selection, tracks(a, &[t[0], t[1]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_tracks_to_another_file_or_to_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t0", 1), ("t1", 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[("b0", 1), ("b1", 1)], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
let b_tracks = track_ids(&fx, b);
|
||||
|
||||
do_move(
|
||||
&mut fx,
|
||||
tracks(a, &[t[0]]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: b,
|
||||
index: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![b_tracks[0], t[0], b_tracks[1]]);
|
||||
assert_eq!(fx.selection, tracks(b, &[t[0]]));
|
||||
|
||||
// as a new file, at a position of the list of files
|
||||
do_move(&mut fx, tracks(a, &[t[1]]), MoveTarget::Files { index: 1 }).unwrap();
|
||||
assert!(track_ids(&fx, a).is_empty());
|
||||
assert_eq!(fx.order.0.len(), 4);
|
||||
let new = fx.order.0[1];
|
||||
assert_eq!(fx.order.0[0], a);
|
||||
assert_eq!(fx.order.0[2..], [b, c]);
|
||||
assert_eq!(track_ids(&fx, new), vec![t[1]]);
|
||||
assert_eq!(fx.files[&new].info.name, "t1");
|
||||
assert_eq!(fx.selection, files(&[new]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_segments() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t0", 3), ("t1", 1)], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
(fx, a, t)
|
||||
};
|
||||
|
||||
// within a track
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[2]]),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[0],
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2], s[0], s[1]]);
|
||||
assert_eq!(fx.selection, segments(a, t[0], &[s[2]]));
|
||||
|
||||
// to another track
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let other = segment_ids(&fx, a, 1);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[0], s[1]]),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[1],
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2]]);
|
||||
assert_eq!(segment_ids(&fx, a, 1), vec![s[0], s[1], other[0]]);
|
||||
|
||||
// as new tracks of a file
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[1]]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fx.files[&a].trk.len(), 3);
|
||||
assert_eq!(segment_ids(&fx, a, 1), vec![s[1]]);
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[0], s[2]]);
|
||||
assert_eq!(fx.files[&a].trk[0].id, t[0]);
|
||||
assert_eq!(fx.files[&a].trk[2].id, t[1]);
|
||||
|
||||
// as new files
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[0], s[1]]),
|
||||
MoveTarget::Files { index: 0 },
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0.len(), 3);
|
||||
assert_eq!(fx.order.0[2], a);
|
||||
assert_eq!(fx.files[&fx.order.0[0]].info.name, "t0");
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_waypoints() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 4));
|
||||
let b = add(&mut fx, file("b", &[], 2));
|
||||
(fx, a, b)
|
||||
};
|
||||
let at = |file_id, index| MoveTarget::Waypoints { file_id, index };
|
||||
|
||||
// within a file
|
||||
let (mut fx, a, _) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
do_move(&mut fx, waypoints(a, &[w[3]]), at(a, 1)).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), vec![w[0], w[3], w[1], w[2]]);
|
||||
assert_eq!(fx.selection, waypoints(a, &[w[3]]));
|
||||
|
||||
// to the start of another file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
do_move(&mut fx, waypoints(a, &[w[0], w[1]]), at(b, 0)).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w[2..].to_vec());
|
||||
assert_eq!(wpt_ids(&fx, b), [&w[..2], &before[..]].concat());
|
||||
|
||||
// all the waypoints of a file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
do_move(&mut fx, Selection::Waypoints { file_id: a }, at(b, 1)).unwrap();
|
||||
assert!(wpt_ids(&fx, a).is_empty());
|
||||
assert_eq!(
|
||||
wpt_ids(&fx, b),
|
||||
[&before[..1], &w[..], &before[1..]].concat()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
do_move(&mut fx, files(&[c]), MoveTarget::Files { index: 0 }).unwrap();
|
||||
assert_eq!(fx.order.0, vec![c, a, b]);
|
||||
do_move(&mut fx, files(&[c, a]), MoveTarget::Files { index: 1 }).unwrap();
|
||||
assert_eq!(fx.order.0, vec![b, c, a]);
|
||||
assert_eq!(fx.selection, files(&[c, a]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_what_cannot_be_moved() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t", 1)], 1));
|
||||
let t = track_ids(&fx, a);
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = fx.files[&a].clone();
|
||||
let selection = fx.selection.clone();
|
||||
|
||||
let invalid = [
|
||||
// the wrong kind of list
|
||||
(
|
||||
files(&[a]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[0],
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
segments(a, t[0], &s),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
waypoints(a, &w),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(waypoints(a, &w), MoveTarget::Files { index: 0 }),
|
||||
(
|
||||
Selection::Waypoints { file_id: a },
|
||||
MoveTarget::Files { index: 0 },
|
||||
),
|
||||
(Selection::Empty, MoveTarget::Files { index: 0 }),
|
||||
// elements that do not exist
|
||||
(
|
||||
tracks(a, &[TrackId::default()]),
|
||||
MoveTarget::Files { index: 0 },
|
||||
),
|
||||
(
|
||||
waypoints(a, &[WaypointId::default()]),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(files(&[FileId::default()]), MoveTarget::Files { index: 0 }),
|
||||
// a place that does not exist
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Tracks {
|
||||
file_id: FileId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
segments(a, t[0], &s),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: TrackId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
waypoints(a, &w),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: FileId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
];
|
||||
for (what, to) in invalid {
|
||||
assert_eq!(
|
||||
do_move(&mut fx, what.clone(), to),
|
||||
Err(CommandError::NothingToDo),
|
||||
"{what:?} to {to:?}"
|
||||
);
|
||||
// nothing changed
|
||||
assert!(Rc::ptr_eq(&fx.files[&a], &before));
|
||||
assert_eq!(fx.order.0, vec![a]);
|
||||
assert_eq!(fx.selection, selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use crate::{Apply, CommandError, File, LngLat, State, Track, TrackSegment, Trackpoint, produce};
|
||||
|
||||
/// Creates an empty file and selects it. With a `trackpoint`, the file starts with a track and a
|
||||
/// segment that hold it: it is how the routing tool starts a route on an empty map, in a single
|
||||
/// undo step.
|
||||
#[derive(Debug)]
|
||||
pub struct New<'a> {
|
||||
pub name: &'a str,
|
||||
pub trackpoint: Option<NewTrackpoint>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NewTrackpoint {
|
||||
pub lng: f64,
|
||||
pub lat: f64,
|
||||
pub ele: f64,
|
||||
}
|
||||
|
||||
impl Apply for New<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
produce(state, |_| {
|
||||
let mut file = File::default();
|
||||
file.info.name = self.name.to_owned();
|
||||
if let Some(point) = self.trackpoint {
|
||||
let mut segment = TrackSegment::default();
|
||||
// the ends of a segment are anchors of the routing tool
|
||||
segment.splice(
|
||||
0,
|
||||
0,
|
||||
vec![Trackpoint {
|
||||
coordinates: LngLat {
|
||||
lng: point.lng,
|
||||
lat: point.lat,
|
||||
},
|
||||
ele: point.ele,
|
||||
..Default::default()
|
||||
}],
|
||||
);
|
||||
file.trk.push(Track {
|
||||
trkseg: vec![segment],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
vec![file]
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
let mut fx = Fixture::default();
|
||||
assert!(
|
||||
New {
|
||||
name: "new",
|
||||
trackpoint: None
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!(file.info.name, "new");
|
||||
assert!(file.trk.is_empty());
|
||||
assert_eq!(fx.selected_files(), [file.id].into());
|
||||
assert_eq!(fx.order.0, vec![file.id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_with_a_trackpoint() {
|
||||
let mut fx = Fixture::default();
|
||||
let point = NewTrackpoint {
|
||||
lng: 1.0,
|
||||
lat: 2.0,
|
||||
ele: 3.0,
|
||||
};
|
||||
New {
|
||||
name: "new",
|
||||
trackpoint: Some(point),
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!(file.trk.len(), 1);
|
||||
assert_eq!(file.trk[0].trkseg.len(), 1);
|
||||
let segment = &file.trk[0].trkseg[0];
|
||||
assert_eq!(segment.len(), 1);
|
||||
assert_eq!(segment[0].coordinates.lng, 1.0);
|
||||
assert_eq!(segment[0].coordinates.lat, 2.0);
|
||||
assert_eq!(segment[0].ele, 3.0);
|
||||
// the only trackpoint is an anchor of the routing tool
|
||||
assert_eq!(segment[0].anchor, Some(0));
|
||||
assert_eq!(fx.selected_files(), [file.id].into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,789 @@
|
||||
use std::{collections::HashSet, hash::Hash};
|
||||
|
||||
use crate::{
|
||||
Apply, ClipboardContent, CommandError, Destination, FileId, Place, Selection, State, TrackId,
|
||||
TrackSegmentId, WaypointId, transfer, waypoint_ids,
|
||||
};
|
||||
|
||||
/// Pastes what was copied or cut (see [`crate::Clipboard`]).
|
||||
///
|
||||
/// Where it goes depends on the selection, see [`crate::Clipboard::can_paste`]:
|
||||
/// - nothing selected: the files are copied, and tracks and segments become new files (named
|
||||
/// like their track, or like the file they come from);
|
||||
/// - a file: the tracks are added to it, segments become new tracks in it, and the waypoints are
|
||||
/// added to its own;
|
||||
/// - a track: the segments are added to it, tracks go after it in its file;
|
||||
/// - a segment: the segments go after it in its track;
|
||||
/// - the waypoints or a waypoint: the waypoints are added to their file, after the waypoint.
|
||||
///
|
||||
/// Copies are made, with new ids, and the pasted elements are selected. What was cut is moved
|
||||
/// instead, with its ids, the first time: what is still where it was is removed.
|
||||
///
|
||||
/// What is pasted is always the elements as they were when they were copied or cut, even if they
|
||||
/// were changed or deleted since. The clipboard is kept so that it can be pasted again: after
|
||||
/// what was cut was moved it is not cut anymore, and the next pastes make copies.
|
||||
#[derive(Debug)]
|
||||
pub struct Paste;
|
||||
|
||||
impl Apply for Paste {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let clipboard = state
|
||||
.clipboard
|
||||
.clone()
|
||||
.filter(|clipboard| clipboard.can_paste(state.selection))
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let cut = clipboard.cut;
|
||||
// always the elements as they were when they were copied or cut
|
||||
let to = destination(state, &clipboard.content)?;
|
||||
let next = transfer(state, &clipboard.content, cut, to)?;
|
||||
|
||||
if cut && let Some(clipboard) = state.clipboard.as_mut() {
|
||||
// moved: the next pastes make copies
|
||||
clipboard.cut = false;
|
||||
}
|
||||
*state.selection = next;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn track_ids_of(state: &State, file_id: FileId) -> Vec<TrackId> {
|
||||
state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.map(|file| file.trk.iter().map(|trk| trk.id).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn segment_ids_of(state: &State, file_id: FileId, trk_id: TrackId) -> Vec<TrackSegmentId> {
|
||||
state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.and_then(|file| file.trk.iter().find(|trk| trk.id == trk_id))
|
||||
.map(|trk| trk.trkseg.iter().map(|seg| seg.id).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn waypoint_ids_of(state: &State, file_id: FileId) -> Vec<WaypointId> {
|
||||
state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.map(|file| waypoint_ids(file).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Where the content goes, depending on what is selected.
|
||||
fn destination(state: &State, content: &ClipboardContent) -> Result<Destination, CommandError> {
|
||||
let nothing = CommandError::NothingToDo;
|
||||
let last_file = |file_ids: &HashSet<FileId>| {
|
||||
last_selected(state.order.0.iter().copied(), file_ids).ok_or(CommandError::NothingToDo)
|
||||
};
|
||||
Ok(match (content, &*state.selection) {
|
||||
(ClipboardContent::Files(_), _) => Destination::Files(Place::End),
|
||||
(ClipboardContent::Tracks(_) | ClipboardContent::Segments(_), Selection::Empty) => {
|
||||
Destination::Files(Place::End)
|
||||
}
|
||||
(
|
||||
ClipboardContent::Tracks(_) | ClipboardContent::Segments(_),
|
||||
Selection::File { file_ids },
|
||||
) => Destination::Tracks(last_file(file_ids)?, Place::End),
|
||||
(ClipboardContent::Tracks(_), Selection::Track { file_id, trk_ids }) => {
|
||||
let last =
|
||||
last_selected(track_ids_of(state, *file_id).into_iter(), trk_ids).ok_or(nothing)?;
|
||||
Destination::Tracks(*file_id, Place::After(last))
|
||||
}
|
||||
(ClipboardContent::Segments(_), Selection::Track { file_id, trk_ids }) => {
|
||||
let last =
|
||||
last_selected(track_ids_of(state, *file_id).into_iter(), trk_ids).ok_or(nothing)?;
|
||||
Destination::Segments(*file_id, last, Place::End)
|
||||
}
|
||||
(
|
||||
ClipboardContent::Segments(_),
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
},
|
||||
) => {
|
||||
let last = last_selected(
|
||||
segment_ids_of(state, *file_id, *trk_id).into_iter(),
|
||||
trkseg_ids,
|
||||
)
|
||||
.ok_or(nothing)?;
|
||||
Destination::Segments(*file_id, *trk_id, Place::After(last))
|
||||
}
|
||||
(ClipboardContent::Waypoints(_), Selection::File { file_ids }) => {
|
||||
Destination::Waypoints(last_file(file_ids)?, Place::End)
|
||||
}
|
||||
(ClipboardContent::Waypoints(_), Selection::Waypoints { file_id }) => {
|
||||
Destination::Waypoints(*file_id, Place::End)
|
||||
}
|
||||
(ClipboardContent::Waypoints(_), Selection::Waypoint { file_id, wpt_ids }) => {
|
||||
let last = last_selected(waypoint_ids_of(state, *file_id).into_iter(), wpt_ids)
|
||||
.ok_or(nothing)?;
|
||||
Destination::Waypoints(*file_id, Place::After(last))
|
||||
}
|
||||
_ => return Err(nothing),
|
||||
})
|
||||
}
|
||||
|
||||
/// The last of the `selected` ids in the order of `all`: the element a paste goes after.
|
||||
fn last_selected<T: Copy + Eq + Hash>(
|
||||
all: impl Iterator<Item = T>,
|
||||
selected: &HashSet<T>,
|
||||
) -> Option<T> {
|
||||
all.filter(|id| selected.contains(id)).last()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Clipboard, ClipboardIds, File, FileId, Track, TrackId, TrackInfo, TrackSegment,
|
||||
TrackSegmentId, Waypoint, WaypointId, Waypoints, engine::command::fixture::Fixture,
|
||||
new_file,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A file with the given tracks (name, number of segments) and number of waypoints.
|
||||
fn file(name: &str, tracks: &[(Option<&str>, usize)], waypoints: usize) -> File {
|
||||
let mut file = new_file(
|
||||
name,
|
||||
tracks
|
||||
.iter()
|
||||
.map(|(name, segments)| Track {
|
||||
info: TrackInfo {
|
||||
name: name.map(str::to_owned),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: (0..*segments).map(|_| TrackSegment::default()).collect(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if waypoints > 0 {
|
||||
file.wpt = Waypoints::new([crate::WaypointChunk {
|
||||
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
||||
..Default::default()
|
||||
}]);
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn add(fx: &mut Fixture, file: File) -> FileId {
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn track_ids(fx: &Fixture, id: FileId) -> Vec<TrackId> {
|
||||
fx.files[&id].trk.iter().map(|trk| trk.id).collect()
|
||||
}
|
||||
|
||||
fn segment_ids(fx: &Fixture, id: FileId, trk: usize) -> Vec<TrackSegmentId> {
|
||||
fx.files[&id].trk[trk].trkseg.iter().map(|s| s.id).collect()
|
||||
}
|
||||
|
||||
fn wpt_ids(fx: &Fixture, id: FileId) -> Vec<WaypointId> {
|
||||
waypoint_ids(&fx.files[&id]).collect()
|
||||
}
|
||||
|
||||
fn names(fx: &Fixture) -> Vec<String> {
|
||||
fx.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn copy(fx: &mut Fixture, selection: Selection, cut: bool) {
|
||||
fx.clipboard = Clipboard::new(&selection, &fx.files, &fx.order.0, cut);
|
||||
}
|
||||
|
||||
/// The ids of what is in the clipboard, and whether it is cut.
|
||||
fn clipboard_state(fx: &Fixture) -> Option<(ClipboardIds, bool)> {
|
||||
fx.clipboard.as_ref().map(|c| (c.content.ids(), c.cut))
|
||||
}
|
||||
|
||||
fn paste(fx: &mut Fixture) -> Result<(), CommandError> {
|
||||
Paste.apply(&mut fx.state())
|
||||
}
|
||||
|
||||
fn tracks(file_id: FileId, ids: &[TrackId]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn segments(file_id: FileId, trk_id: TrackId, ids: &[TrackSegmentId]) -> Selection {
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoints(file_id: FileId, ids: &[WaypointId]) -> Selection {
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn files(ids: &[FileId]) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_copied_tracks_into_a_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("t0"), 1), (Some("t1"), 2), (None, 1)], 0),
|
||||
);
|
||||
let b = add(&mut fx, file("b", &[(Some("b0"), 1)], 0));
|
||||
let source = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[source[0], source[1]]), false);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
// the source is untouched, copies with new ids are added at the end
|
||||
assert_eq!(track_ids(&fx, a), source);
|
||||
let in_b = track_ids(&fx, b);
|
||||
assert_eq!(in_b.len(), 3);
|
||||
assert!(!source.contains(&in_b[1]) && !source.contains(&in_b[2]));
|
||||
let names: Vec<_> = fx.files[&b]
|
||||
.trk
|
||||
.iter()
|
||||
.map(|t| t.info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
[Some("b0".into()), Some("t0".into()), Some("t1".into())]
|
||||
);
|
||||
// the segments are copies too
|
||||
assert_eq!(fx.files[&b].trk[2].trkseg.len(), 2);
|
||||
assert_ne!(segment_ids(&fx, b, 2), segment_ids(&fx, a, 1));
|
||||
// the pasted tracks are selected, and the clipboard is still there
|
||||
assert_eq!(fx.selection, tracks(b, &in_b[1..]));
|
||||
assert_eq!(
|
||||
clipboard_state(&fx),
|
||||
Some((ClipboardIds::Tracks(vec![source[0], source[1]]), false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_cut_tracks_moves_them() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let source = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[source[0]]), true);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
assert_eq!(track_ids(&fx, a), vec![source[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![source[0]]);
|
||||
assert_eq!(fx.selection, tracks(b, &[source[0]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_tracks_after_the_selected_track() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file(
|
||||
"a",
|
||||
&[
|
||||
(Some("0"), 1),
|
||||
(Some("1"), 1),
|
||||
(Some("2"), 1),
|
||||
(Some("3"), 1),
|
||||
],
|
||||
0,
|
||||
),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
// copy: the block goes after the selected track
|
||||
copy(&mut fx, tracks(a, &[t[0], t[1]]), false);
|
||||
fx.selection = tracks(a, &[t[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
let names: Vec<_> = fx.files[&a]
|
||||
.trk
|
||||
.iter()
|
||||
.map(|t| t.info.name.clone().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, ["0", "1", "2", "0", "1", "3"]);
|
||||
|
||||
// cut: moved after the selected track
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("0"), 1), (Some("1"), 1), (Some("2"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = tracks(a, &[t[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[2], t[0]]);
|
||||
|
||||
// the selected track is cut itself: the tracks go at the end
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("0"), 1), (Some("1"), 1), (Some("2"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = tracks(a, &[t[0]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[2], t[0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_tracks_as_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("source", &[(Some("named"), 1), (None, 1)], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0], t[1]]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
// named like the track, or like the file it comes from
|
||||
assert_eq!(names(&fx), ["source", "named", "source"]);
|
||||
assert_eq!(fx.files.len(), 3);
|
||||
let new = &fx.order.0[1..];
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::File { file_ids } if file_ids.len() == 2 && new.iter().all(|id| file_ids.contains(id)))
|
||||
);
|
||||
assert_eq!(fx.files[&new[0]].trk.len(), 1);
|
||||
// the source keeps its tracks
|
||||
assert_eq!(track_ids(&fx, a), t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_cut_tracks_as_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("source", &[(Some("one"), 1), (Some("two"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
let new = fx.order.0[1];
|
||||
assert_eq!(track_ids(&fx, new), vec![t[0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_segments() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 3), (Some("t1"), 1)], 0));
|
||||
(fx, a)
|
||||
};
|
||||
|
||||
// at the end of the selected track
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0]]), false);
|
||||
fx.selection = tracks(a, &[t[1]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), s);
|
||||
let in_t1 = segment_ids(&fx, a, 1);
|
||||
assert_eq!(in_t1.len(), 2);
|
||||
assert_ne!(in_t1[1], s[0]);
|
||||
assert_eq!(fx.selection, segments(a, t[1], &in_t1[1..]));
|
||||
|
||||
// after the selected segment, moved
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0]]), true);
|
||||
fx.selection = segments(a, t[0], &[s[1]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[1], s[0], s[2]]);
|
||||
|
||||
// each segment becomes a track of the selected file
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0], s[1]]), false);
|
||||
fx.selection = files(&[a]);
|
||||
paste(&mut fx).unwrap();
|
||||
let tracks_now = track_ids(&fx, a);
|
||||
assert_eq!(tracks_now.len(), 4);
|
||||
assert!(
|
||||
fx.files[&a].trk[2..]
|
||||
.iter()
|
||||
.all(|trk| trk.trkseg.len() == 1)
|
||||
);
|
||||
assert_eq!(fx.selection, tracks(a, &tracks_now[2..]));
|
||||
|
||||
// each segment becomes a file, named like its track
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0], s[1]]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(names(&fx), ["a", "t0", "t0"]);
|
||||
assert!(
|
||||
fx.order.0[1..]
|
||||
.iter()
|
||||
.all(|id| fx.files[id].trk[0].trkseg.len() == 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_waypoints() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 4));
|
||||
let b = add(&mut fx, file("b", &[], 2));
|
||||
(fx, a, b)
|
||||
};
|
||||
|
||||
// to the end of another file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
copy(&mut fx, waypoints(a, &[w[1], w[2]]), false);
|
||||
fx.selection = files(&[b]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w);
|
||||
let now = wpt_ids(&fx, b);
|
||||
assert_eq!(now.len(), 4);
|
||||
assert_eq!(&now[..2], &before[..]);
|
||||
assert!(!w.contains(&now[2]));
|
||||
assert_eq!(fx.selection, waypoints(b, &now[2..]));
|
||||
|
||||
// after the selected waypoint
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
copy(&mut fx, waypoints(a, &[w[0]]), true);
|
||||
fx.selection = waypoints(b, &[before[0]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w[1..].to_vec());
|
||||
assert_eq!(wpt_ids(&fx, b), vec![before[0], w[0], before[1]]);
|
||||
|
||||
// within the file, after a waypoint
|
||||
let (mut fx, a, _) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, waypoints(a, &[w[0], w[1]]), true);
|
||||
fx.selection = waypoints(a, &[w[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), vec![w[2], w[0], w[1], w[3]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_all_the_waypoints_of_a_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 3));
|
||||
let b = add(&mut fx, file("b", &[], 1));
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, Selection::Waypoints { file_id: a }, false);
|
||||
fx.selection = Selection::Waypoints { file_id: b };
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w);
|
||||
assert_eq!(wpt_ids(&fx, b).len(), 4);
|
||||
|
||||
// moved: the source has none left
|
||||
copy(&mut fx, Selection::Waypoints { file_id: a }, true);
|
||||
fx.selection = files(&[b]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert!(wpt_ids(&fx, a).is_empty());
|
||||
assert_eq!(&wpt_ids(&fx, b)[4..], &w[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 1)], 1));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
copy(&mut fx, files(&[b, a]), false);
|
||||
fx.selection = files(&[c]);
|
||||
paste(&mut fx).unwrap();
|
||||
// copies, in the order of the files, at the end
|
||||
assert_eq!(names(&fx), ["a", "b", "c", "a", "b"]);
|
||||
assert_eq!(fx.files.len(), 5);
|
||||
assert_eq!(fx.files[&fx.order.0[3]].trk.len(), 1);
|
||||
assert_ne!(track_ids(&fx, fx.order.0[3]), track_ids(&fx, a));
|
||||
assert!(matches!(&fx.selection, Selection::File { file_ids } if file_ids.len() == 2));
|
||||
|
||||
// cut: moved to the end, they keep their ids
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
copy(&mut fx, files(&[a]), true);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.order.0, vec![b, c, a]);
|
||||
assert_eq!(fx.selection, files(&[a]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_paste() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 1)], 1));
|
||||
let t = track_ids(&fx, a);
|
||||
let w = wpt_ids(&fx, a);
|
||||
fx.selection = files(&[a]);
|
||||
|
||||
// an empty clipboard
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
|
||||
// a target that does not accept it (tracks cannot go to waypoints), the clipboard stays
|
||||
copy(&mut fx, tracks(a, &t), false);
|
||||
fx.selection = waypoints(a, &w);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
assert!(fx.clipboard.is_some());
|
||||
assert_eq!(fx.selection, waypoints(a, &w));
|
||||
|
||||
// nothing to copy: what does not exist is not put in the clipboard
|
||||
copy(&mut fx, tracks(a, &[TrackId::default()]), false);
|
||||
fx.selection = files(&[a]);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
copy(&mut fx, waypoints(a, &[WaypointId::default()]), false);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
copy(&mut fx, files(&[FileId::default()]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
assert_eq!(fx.order.0, vec![a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_a_copy_several_times() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 1));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), false);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
// each paste made a copy with new ids, and the source is untouched
|
||||
let in_b = track_ids(&fx, b);
|
||||
assert_eq!(in_b.len(), 3);
|
||||
assert_eq!(in_b.iter().collect::<HashSet<_>>().len(), 3);
|
||||
assert!(in_b.iter().all(|id| !t.contains(id)));
|
||||
assert_eq!(track_ids(&fx, a), t);
|
||||
assert!(fx.clipboard.is_some());
|
||||
|
||||
// the selection is the last copy: the next paste goes after it
|
||||
assert_eq!(fx.selection, tracks(b, &[in_b[2]]));
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, b).len(), 4);
|
||||
|
||||
// the same with other elements
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, waypoints(a, &w), false);
|
||||
fx.selection = files(&[b]);
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, b).len(), 2);
|
||||
assert_eq!(wpt_ids(&fx, a), w);
|
||||
|
||||
copy(&mut fx, files(&[b]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_a_cut_moves_the_first_time_then_copies() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
// moved with its id, and what is in the clipboard now is a copy of it
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![t[0]]);
|
||||
assert_eq!(
|
||||
clipboard_state(&fx),
|
||||
Some((ClipboardIds::Tracks(vec![t[0]]), false))
|
||||
);
|
||||
|
||||
fx.selection = files(&[c]);
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
// copies: the moved track stays where it was moved to
|
||||
assert_eq!(track_ids(&fx, b), vec![t[0]]);
|
||||
let in_c = track_ids(&fx, c);
|
||||
assert_eq!(in_c.len(), 2);
|
||||
assert!(!in_c.contains(&t[0]));
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_cut_files_several_times() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
copy(&mut fx, files(&[a]), true);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.order.0, vec![b, a]);
|
||||
// the next pastes copy it
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files.len(), 3);
|
||||
assert_eq!(fx.order.0[..2], [b, a]);
|
||||
}
|
||||
|
||||
/// Renames the first track of the file, as an edit would.
|
||||
fn rename_track(fx: &mut Fixture, id: FileId, name: &str) {
|
||||
let file = std::rc::Rc::make_mut(fx.files.get_mut(&id).unwrap());
|
||||
file.trk[0].info.name = Some(name.to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_the_elements_as_they_were_when_copied() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("original"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), false);
|
||||
rename_track(&mut fx, a, "edited");
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
let pasted = &fx.files[&b].trk[0];
|
||||
assert_eq!(pasted.info.name.as_deref(), Some("original"));
|
||||
// the source keeps what was done to it
|
||||
assert_eq!(fx.files[&a].trk[0].info.name.as_deref(), Some("edited"));
|
||||
|
||||
// and each time
|
||||
paste(&mut fx).unwrap();
|
||||
assert!(
|
||||
fx.files[&b]
|
||||
.trk
|
||||
.iter()
|
||||
.all(|trk| trk.info.name.as_deref() == Some("original"))
|
||||
);
|
||||
assert_eq!(fx.files[&b].trk.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_a_cut_as_it_was_when_cut() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("original"), 1), (Some("other"), 1)], 0),
|
||||
);
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
rename_track(&mut fx, a, "edited");
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
// what is in the clipboard is moved, the edited version is not what is pasted
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![t[0]]);
|
||||
assert_eq!(fx.files[&b].trk[0].info.name.as_deref(), Some("original"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_after_the_elements_were_deleted() {
|
||||
let delete_file = |fx: &mut Fixture, id: FileId| {
|
||||
fx.files.remove(&id);
|
||||
fx.order.0.retain(|other| *other != id);
|
||||
};
|
||||
|
||||
// copied tracks, waypoints, segments and files are still there to be pasted
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
|
||||
copy(&mut fx, tracks(a, &t), false);
|
||||
fx.selection = files(&[b]);
|
||||
delete_file(&mut fx, a);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files[&b].trk.len(), 1);
|
||||
assert_eq!(fx.files[&b].trk[0].trkseg.len(), 2);
|
||||
// the copy of a track comes from a file that is gone: named like the track
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(names(&fx), ["b", "t"]);
|
||||
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let (t, s) = (track_ids(&fx, a), segment_ids(&fx, a, 0));
|
||||
copy(&mut fx, segments(a, t[0], &s), false);
|
||||
fx.selection = files(&[b]);
|
||||
delete_file(&mut fx, a);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files[&b].trk.len(), 2);
|
||||
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, waypoints(a, &w), false);
|
||||
fx.selection = files(&[b]);
|
||||
delete_file(&mut fx, a);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, b).len(), 2);
|
||||
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 1)], 0));
|
||||
copy(&mut fx, files(&[a]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
delete_file(&mut fx, a);
|
||||
assert!(fx.files.is_empty());
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files.len(), 2);
|
||||
assert_eq!(names(&fx), ["a", "a"]);
|
||||
let _ = (s, w);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_a_cut_after_the_elements_were_deleted() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
// deleted before it is pasted
|
||||
std::rc::Rc::make_mut(fx.files.get_mut(&a).unwrap())
|
||||
.trk
|
||||
.retain(|trk| trk.id != t[0]);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
// nothing to remove, the track is where it was pasted, with its id
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![t[0]]);
|
||||
assert_eq!(
|
||||
clipboard_state(&fx),
|
||||
Some((ClipboardIds::Tracks(vec![t[0]]), false))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
//! Copying or moving elements to a place of the file tree: what paste and move are made of.
|
||||
//! The elements come as the content of a clipboard, and a destination says where they go among
|
||||
//! the elements already there.
|
||||
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
|
||||
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
|
||||
copy_segment, copy_track, copy_waypoint,
|
||||
};
|
||||
|
||||
/// Where elements are put, among the others of the same list.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Place<T> {
|
||||
/// After all of them.
|
||||
End,
|
||||
/// Right after the one with this id, or at the end if it is not there (anymore).
|
||||
After(T),
|
||||
/// At this position (at the end if it is past it).
|
||||
Index(usize),
|
||||
}
|
||||
|
||||
impl<T: Copy + PartialEq> Place<T> {
|
||||
/// The position in the list of `ids`.
|
||||
pub fn position(&self, ids: &[T]) -> usize {
|
||||
match self {
|
||||
Place::End => ids.len(),
|
||||
Place::After(id) => ids
|
||||
.iter()
|
||||
.position(|other| other == id)
|
||||
.map_or(ids.len(), |i| i + 1),
|
||||
Place::Index(index) => (*index).min(ids.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where elements go. What can go where:
|
||||
/// - files: among the files;
|
||||
/// - tracks: among the files (each track becomes a file, named like the track, or like the file
|
||||
/// it comes from), or among the tracks of a file;
|
||||
/// - segments: among the files (each segment becomes a file with a track, named like its track,
|
||||
/// or like its file), among the tracks of a file (each segment becomes a track), or among the
|
||||
/// segments of a track;
|
||||
/// - waypoints: among the waypoints of a file.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Destination {
|
||||
Files(Place<FileId>),
|
||||
Tracks(FileId, Place<TrackId>),
|
||||
Segments(FileId, TrackId, Place<TrackSegmentId>),
|
||||
Waypoints(FileId, Place<WaypointId>),
|
||||
}
|
||||
|
||||
pub fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
|
||||
file.wpt.iter().map(|wpt| wpt.id)
|
||||
}
|
||||
|
||||
fn file_mut<'a>(state: &'a mut State, id: FileId) -> Result<&'a mut File, CommandError> {
|
||||
state
|
||||
.files
|
||||
.get_mut(&id)
|
||||
.map(Rc::make_mut)
|
||||
.ok_or(CommandError::NothingToDo)
|
||||
}
|
||||
|
||||
pub fn new_file(name: &str, trk: Vec<Track>) -> File {
|
||||
let mut file = File {
|
||||
trk,
|
||||
..Default::default()
|
||||
};
|
||||
file.info.name = name.to_owned();
|
||||
file
|
||||
}
|
||||
|
||||
/// Adds the files to the state, at `place` in the order of the files.
|
||||
fn add_files(state: &mut State, files: Vec<Rc<File>>, place: Place<FileId>) -> Selection {
|
||||
let at = place.position(&state.order.0);
|
||||
let ids: Vec<FileId> = files.iter().map(|file| file.id).collect();
|
||||
for file in files {
|
||||
state.files.insert(file.id, file);
|
||||
}
|
||||
state.order.0.splice(at..at, ids.iter().copied());
|
||||
Selection::File {
|
||||
file_ids: ids.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the elements of the content from the files, wherever they are.
|
||||
pub fn remove_elements(state: &mut State, content: &ClipboardContent) {
|
||||
match content {
|
||||
ClipboardContent::Files(files) => {
|
||||
let ids: HashSet<FileId> = files.iter().map(|file| file.id).collect();
|
||||
state.files.retain(|id, _| !ids.contains(id));
|
||||
state.order.0.retain(|id| !ids.contains(id));
|
||||
}
|
||||
ClipboardContent::Tracks(tracks) => {
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.track.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
if file.trk.iter().any(|trk| ids.contains(&trk.id)) {
|
||||
Rc::make_mut(file).trk.retain(|trk| !ids.contains(&trk.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
ClipboardContent::Segments(segments) => {
|
||||
let ids: HashSet<TrackSegmentId> = segments.iter().map(|seg| seg.segment.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
let contains = |trk: &Track| trk.trkseg.iter().any(|seg| ids.contains(&seg.id));
|
||||
if file.trk.iter().any(contains) {
|
||||
for trk in Rc::make_mut(file).trk.iter_mut() {
|
||||
trk.trkseg.retain(|seg| !ids.contains(&seg.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ClipboardContent::Waypoints(waypoints) => {
|
||||
let ids: HashSet<WaypointId> = waypoints.iter().map(|wpt| wpt.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
if waypoint_ids(file).any(|id| ids.contains(&id)) {
|
||||
Rc::make_mut(file).wpt.edit(
|
||||
|wpt| ids.contains(&wpt.id),
|
||||
|wpts| {
|
||||
wpts.retain(|wpt| !ids.contains(&wpt.id));
|
||||
true
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts the elements of the content at the destination: copies of them, or the elements
|
||||
/// themselves, with their ids, when `cut` (they are removed from where they are first). Returns
|
||||
/// the selection of what was put.
|
||||
pub fn transfer(
|
||||
state: &mut State,
|
||||
content: &ClipboardContent,
|
||||
cut: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
// the destination has to be there, before anything is changed
|
||||
let exists = match to {
|
||||
Destination::Files(_) => true,
|
||||
Destination::Tracks(file_id, _) => state.files.contains_key(&file_id),
|
||||
Destination::Segments(file_id, trk_id, _) => state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.is_some_and(|file| file.trk.iter().any(|trk| trk.id == trk_id)),
|
||||
Destination::Waypoints(file_id, _) => state.files.contains_key(&file_id),
|
||||
};
|
||||
let valid = matches!(
|
||||
(content, to),
|
||||
(ClipboardContent::Files(_), Destination::Files(_))
|
||||
| (
|
||||
ClipboardContent::Tracks(_),
|
||||
Destination::Files(_) | Destination::Tracks(..)
|
||||
)
|
||||
| (
|
||||
ClipboardContent::Segments(_),
|
||||
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
|
||||
)
|
||||
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..))
|
||||
);
|
||||
let empty = match content {
|
||||
ClipboardContent::Files(files) => files.is_empty(),
|
||||
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||
ClipboardContent::Segments(segments) => segments.is_empty(),
|
||||
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
||||
};
|
||||
if !exists || !valid || empty {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
if cut {
|
||||
remove_elements(state, content);
|
||||
}
|
||||
let copy = !cut;
|
||||
match (content, to) {
|
||||
(ClipboardContent::Files(files), Destination::Files(place)) => {
|
||||
let files = files
|
||||
.iter()
|
||||
.map(|file| {
|
||||
if copy {
|
||||
Rc::new(copy_file(file))
|
||||
} else {
|
||||
file.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
(ClipboardContent::Tracks(tracks), to) => transfer_tracks(state, tracks, copy, to),
|
||||
(ClipboardContent::Segments(segments), to) => transfer_segments(state, segments, copy, to),
|
||||
(ClipboardContent::Waypoints(waypoints), Destination::Waypoints(file_id, place)) => {
|
||||
transfer_waypoints(state, waypoints, copy, file_id, place)
|
||||
}
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_tracks(
|
||||
state: &mut State,
|
||||
tracks: &[ClipboardTrack],
|
||||
copy: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let tracks: Vec<(Track, &str)> = tracks
|
||||
.iter()
|
||||
.map(|clip| {
|
||||
let track = if copy {
|
||||
copy_track(&clip.track)
|
||||
} else {
|
||||
clip.track.clone()
|
||||
};
|
||||
(track, clip.file_name.as_str())
|
||||
})
|
||||
.collect();
|
||||
match to {
|
||||
Destination::Files(place) => {
|
||||
let files = tracks
|
||||
.into_iter()
|
||||
.map(|(track, file_name)| {
|
||||
let name = track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file_name.to_owned());
|
||||
Rc::new(new_file(&name, vec![track]))
|
||||
})
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Tracks(file_id, place) => {
|
||||
let tracks: Vec<Track> = tracks.into_iter().map(|(track, _)| track).collect();
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.trk.splice(at..at, tracks);
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
})
|
||||
}
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_segments(
|
||||
state: &mut State,
|
||||
segments: &[ClipboardSegment],
|
||||
copy: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let segments: Vec<(TrackSegment, &str)> = segments
|
||||
.iter()
|
||||
.map(|clip| {
|
||||
let segment = if copy {
|
||||
copy_segment(&clip.segment)
|
||||
} else {
|
||||
clip.segment.clone()
|
||||
};
|
||||
(segment, clip.name.as_str())
|
||||
})
|
||||
.collect();
|
||||
// a segment on its own needs a track
|
||||
let own_track = |segment: TrackSegment| Track {
|
||||
trkseg: vec![segment],
|
||||
..Default::default()
|
||||
};
|
||||
match to {
|
||||
Destination::Files(place) => {
|
||||
let files = segments
|
||||
.into_iter()
|
||||
.map(|(segment, name)| Rc::new(new_file(name, vec![own_track(segment)])))
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Tracks(file_id, place) => {
|
||||
let tracks: Vec<Track> = segments
|
||||
.into_iter()
|
||||
.map(|(segment, _)| own_track(segment))
|
||||
.collect();
|
||||
let ids = tracks.iter().map(|trk| trk.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.trk.splice(at..at, tracks);
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
})
|
||||
}
|
||||
Destination::Segments(file_id, trk_id, place) => {
|
||||
let segments: Vec<TrackSegment> =
|
||||
segments.into_iter().map(|(segment, _)| segment).collect();
|
||||
let ids: HashSet<TrackSegmentId> = segments.iter().map(|seg| seg.id).collect();
|
||||
let trk = file_mut(state, file_id)?
|
||||
.trk
|
||||
.iter_mut()
|
||||
.find(|trk| trk.id == trk_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let existing: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
|
||||
let at = place.position(&existing);
|
||||
trk.trkseg.splice(at..at, segments);
|
||||
Ok(Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids,
|
||||
})
|
||||
}
|
||||
Destination::Waypoints(..) => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_waypoints(
|
||||
state: &mut State,
|
||||
waypoints: &[Waypoint],
|
||||
copy: bool,
|
||||
file_id: FileId,
|
||||
place: Place<WaypointId>,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let waypoints: Vec<Waypoint> = waypoints
|
||||
.iter()
|
||||
.map(|wpt| {
|
||||
if copy {
|
||||
copy_waypoint(wpt)
|
||||
} else {
|
||||
wpt.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let ids = waypoints.iter().map(|wpt| wpt.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<WaypointId> = waypoint_ids(file).collect();
|
||||
file.wpt.insert_at(place.position(&existing), waypoints);
|
||||
Ok(Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_place_position() {
|
||||
let ids = [1, 2, 3];
|
||||
assert_eq!(Place::End.position(&ids), 3);
|
||||
assert_eq!(Place::After(1).position(&ids), 1);
|
||||
assert_eq!(Place::After(3).position(&ids), 3);
|
||||
// not there: at the end
|
||||
assert_eq!(Place::After(9).position(&ids), 3);
|
||||
assert_eq!(Place::Index(0).position(&ids), 0);
|
||||
assert_eq!(Place::Index(2).position(&ids), 2);
|
||||
assert_eq!(Place::Index(10).position(&ids), 3);
|
||||
assert_eq!(Place::<i32>::End.position(&[]), 0);
|
||||
assert_eq!(Place::<i32>::Index(4).position(&[]), 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user