mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-07 06:14:07 +00:00
progress
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, Place, SegmentsTo, Selection, State, TrackId, TracksTo,
|
||||
transfer_files, transfer_segments, transfer_tracks, transfer_waypoints,
|
||||
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
|
||||
@@ -38,67 +37,30 @@ pub struct Move {
|
||||
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> {
|
||||
use MoveTarget as To;
|
||||
let next = match (&self.what, self.to) {
|
||||
(Selection::File { file_ids }, To::Files { index }) => {
|
||||
transfer_files(state, file_ids, true, Place::Index(index))?
|
||||
}
|
||||
(Selection::Track { file_id, trk_ids }, To::Files { index }) => transfer_tracks(
|
||||
state,
|
||||
*file_id,
|
||||
trk_ids,
|
||||
true,
|
||||
TracksTo::Files(Place::Index(index)),
|
||||
)?,
|
||||
(Selection::Track { file_id, trk_ids }, To::Tracks { file_id: to, index }) => {
|
||||
transfer_tracks(
|
||||
state,
|
||||
*file_id,
|
||||
trk_ids,
|
||||
true,
|
||||
TracksTo::File(to, Place::Index(index)),
|
||||
)?
|
||||
}
|
||||
(
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
},
|
||||
to,
|
||||
) => {
|
||||
let to = match to {
|
||||
To::Files { index } => SegmentsTo::Files(Place::Index(index)),
|
||||
To::Tracks { file_id, index } => {
|
||||
SegmentsTo::Tracks(file_id, Place::Index(index))
|
||||
}
|
||||
To::Segments {
|
||||
file_id,
|
||||
trk_id,
|
||||
index,
|
||||
} => SegmentsTo::Track(file_id, trk_id, Place::Index(index)),
|
||||
To::Waypoints { .. } => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
transfer_segments(state, *file_id, *trk_id, trkseg_ids, true, to)?
|
||||
}
|
||||
(Selection::Waypoints { file_id }, To::Waypoints { file_id: to, index }) => {
|
||||
transfer_waypoints(state, *file_id, None, true, to, Place::Index(index))?
|
||||
}
|
||||
(Selection::Waypoint { file_id, wpt_ids }, To::Waypoints { file_id: to, index }) => {
|
||||
transfer_waypoints(
|
||||
state,
|
||||
*file_id,
|
||||
Some(wpt_ids),
|
||||
true,
|
||||
to,
|
||||
Place::Index(index),
|
||||
)?
|
||||
}
|
||||
_ => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
*state.selection = next;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::{collections::HashSet, hash::Hash};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, Place, SegmentsTo, Selection, State, TracksTo, transfer_files,
|
||||
transfer_segments, transfer_tracks, transfer_waypoints, waypoint_ids,
|
||||
Apply, ClipboardContent, CommandError, Destination, FileId, Place, Selection, State, TrackId,
|
||||
TrackSegmentId, WaypointId, transfer, waypoint_ids,
|
||||
};
|
||||
|
||||
/// Pastes what was copied or cut (see [`crate::Clipboard`]), and empties the clipboard.
|
||||
/// 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
|
||||
@@ -16,8 +16,12 @@ use crate::{
|
||||
/// - 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.
|
||||
///
|
||||
/// What is cut is moved, with its ids, otherwise copies are made. The pasted elements are
|
||||
/// selected.
|
||||
/// 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;
|
||||
|
||||
@@ -29,91 +33,97 @@ impl Apply for Paste {
|
||||
.filter(|clipboard| clipboard.can_paste(state.selection))
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let cut = clipboard.cut;
|
||||
let next = match &clipboard.selection {
|
||||
Selection::File { file_ids } => transfer_files(state, file_ids, cut, Place::End)?,
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let to = match &*state.selection {
|
||||
Selection::Empty => TracksTo::Files(Place::End),
|
||||
Selection::File { file_ids } => TracksTo::File(
|
||||
last_selected(state.order.0.iter().copied(), file_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
Place::End,
|
||||
),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let last = last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
TracksTo::File(*file_id, Place::After(last))
|
||||
}
|
||||
_ => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
transfer_tracks(state, *file_id, trk_ids, cut, to)?
|
||||
}
|
||||
// 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 to = match &*state.selection {
|
||||
Selection::Empty => SegmentsTo::Files(Place::End),
|
||||
Selection::File { file_ids } => SegmentsTo::Tracks(
|
||||
last_selected(state.order.0.iter().copied(), file_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
Place::End,
|
||||
),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let last = last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
SegmentsTo::Track(*file_id, last, Place::End)
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let trk = state
|
||||
.files
|
||||
.get(file_id)
|
||||
.and_then(|file| file.trk.iter().find(|trk| trk.id == *trk_id))
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let last = last_selected(trk.trkseg.iter().map(|seg| seg.id), trkseg_ids)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
SegmentsTo::Track(*file_id, *trk_id, Place::After(last))
|
||||
}
|
||||
_ => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
transfer_segments(state, *file_id, *trk_id, trkseg_ids, cut, to)?
|
||||
}
|
||||
Selection::Waypoints { .. } | Selection::Waypoint { .. } => {
|
||||
let (file_id, source) = match &clipboard.selection {
|
||||
Selection::Waypoints { file_id } => (*file_id, None),
|
||||
Selection::Waypoint { file_id, wpt_ids } => (*file_id, Some(wpt_ids)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let (to, place) = match &*state.selection {
|
||||
Selection::File { file_ids } => (
|
||||
last_selected(state.order.0.iter().copied(), file_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
Place::End,
|
||||
),
|
||||
Selection::Waypoints { file_id } => (*file_id, Place::End),
|
||||
Selection::Waypoint { file_id, wpt_ids } => {
|
||||
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let last = last_selected(waypoint_ids(file), wpt_ids)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
(*file_id, Place::After(last))
|
||||
}
|
||||
_ => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
transfer_waypoints(state, file_id, source, cut, to, place)?
|
||||
}
|
||||
Selection::Empty => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
*state.selection = next;
|
||||
*state.clipboard = None;
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
) => {
|
||||
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.
|
||||
@@ -129,8 +139,8 @@ mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Clipboard, File, FileId, Track, TrackId, TrackInfo, TrackSegment, TrackSegmentId, Waypoint,
|
||||
WaypointId, engine::command::fixture::Fixture, new_file,
|
||||
Clipboard, ClipboardIds, File, FileId, Track, TrackId, TrackInfo, TrackSegment,
|
||||
TrackSegmentId, Waypoint, WaypointId, engine::command::fixture::Fixture, new_file,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -188,7 +198,12 @@ mod tests {
|
||||
}
|
||||
|
||||
fn copy(fx: &mut Fixture, selection: Selection, cut: bool) {
|
||||
fx.clipboard = Clipboard::new(selection, cut);
|
||||
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> {
|
||||
@@ -254,9 +269,12 @@ mod tests {
|
||||
// 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 empty
|
||||
// the pasted tracks are selected, and the clipboard is still there
|
||||
assert_eq!(fx.selection, tracks(b, &in_b[1..]));
|
||||
assert_eq!(fx.clipboard, None);
|
||||
assert_eq!(
|
||||
clipboard_state(&fx),
|
||||
Some((ClipboardIds::Tracks(vec![source[0], source[1]]), false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -535,7 +553,7 @@ mod tests {
|
||||
assert!(fx.clipboard.is_some());
|
||||
assert_eq!(fx.selection, waypoints(a, &w));
|
||||
|
||||
// elements that do not exist anymore
|
||||
// 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));
|
||||
@@ -547,4 +565,226 @@ mod tests {
|
||||
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);
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let w = wpt_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, w) = (track_ids(&fx, a), segment_ids(&fx, a, 0), wpt_ids(&fx, a));
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Copying or moving elements of the files to a place of the file tree: what paste and move are
|
||||
//! made of. The elements come from where they are found by id, and the place says where they
|
||||
//! go among the elements already there.
|
||||
//! 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::{
|
||||
CommandError, File, FileId, Selection, State, Track, TrackId, TrackSegment, TrackSegmentId,
|
||||
Waypoint, WaypointId, copy_file, copy_segment, copy_track, copy_waypoint, edit_waypoint_chunks,
|
||||
insert_waypoints_at,
|
||||
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
|
||||
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
|
||||
copy_segment, copy_track, copy_waypoint, edit_waypoint_chunks, insert_waypoints_at,
|
||||
};
|
||||
|
||||
/// Where elements are put, among the others of the same list.
|
||||
@@ -35,6 +35,22 @@ impl<T: Copy + PartialEq> Place<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
@@ -60,223 +76,227 @@ pub fn new_file(name: &str, trk: Vec<Track>) -> File {
|
||||
}
|
||||
|
||||
/// Adds the files to the state, at `place` in the order of the files.
|
||||
fn add_files(state: &mut State, files: Vec<File>, place: Place<FileId>) -> HashSet<FileId> {
|
||||
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, Rc::new(file));
|
||||
state.files.insert(file.id, file);
|
||||
}
|
||||
state.order.0.splice(at..at, ids.iter().copied());
|
||||
ids.into_iter().collect()
|
||||
Selection::File {
|
||||
file_ids: ids.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the files, or moves them in the order of the files when `cut`.
|
||||
pub fn transfer_files(
|
||||
state: &mut State,
|
||||
source: &HashSet<FileId>,
|
||||
cut: bool,
|
||||
place: Place<FileId>,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let ids: Vec<FileId> = state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| source.contains(id) && state.files.contains_key(id))
|
||||
.collect();
|
||||
if ids.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
/// 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)) {
|
||||
edit_waypoint_chunks(
|
||||
Rc::make_mut(file),
|
||||
|wpt| ids.contains(&wpt.id),
|
||||
|wpts| {
|
||||
wpts.retain(|wpt| !ids.contains(&wpt.id));
|
||||
true
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file_ids = if cut {
|
||||
// the position is among the files that stay
|
||||
let others: Vec<FileId> = state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| !ids.contains(id))
|
||||
.collect();
|
||||
state.order.move_files(&ids, place.position(&others));
|
||||
ids.into_iter().collect()
|
||||
} else {
|
||||
let copies = ids.iter().map(|id| copy_file(&state.files[id])).collect();
|
||||
add_files(state, copies, place)
|
||||
/// 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),
|
||||
};
|
||||
Ok(Selection::File { file_ids })
|
||||
}
|
||||
|
||||
/// Where tracks go.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TracksTo {
|
||||
/// Each track becomes a new file, named like the track, or like the file it comes from.
|
||||
Files(Place<FileId>),
|
||||
/// The tracks go in a file.
|
||||
File(FileId, Place<TrackId>),
|
||||
}
|
||||
|
||||
/// Copies the tracks, or moves them (with their ids) when `cut`.
|
||||
pub fn transfer_tracks(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
source: &HashSet<TrackId>,
|
||||
cut: bool,
|
||||
to: TracksTo,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let source_name = source_file.info.name.clone();
|
||||
let tracks: Vec<Track> = source_file
|
||||
.trk
|
||||
.iter()
|
||||
.filter(|trk| source.contains(&trk.id))
|
||||
.cloned()
|
||||
.collect();
|
||||
if tracks.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
if let TracksTo::File(id, _) = to
|
||||
&& !state.files.contains_key(&id)
|
||||
{
|
||||
let valid = match (content, to) {
|
||||
(ClipboardContent::Files(_), Destination::Files(_))
|
||||
| (ClipboardContent::Tracks(_), Destination::Files(_) | Destination::Tracks(..))
|
||||
| (
|
||||
ClipboardContent::Segments(_),
|
||||
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
|
||||
)
|
||||
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..)) => true,
|
||||
_ => false,
|
||||
};
|
||||
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 {
|
||||
file_mut(state, file_id)?
|
||||
.trk
|
||||
.retain(|trk| !source.contains(&trk.id));
|
||||
remove_elements(state, content);
|
||||
}
|
||||
let tracks: Vec<Track> = if cut {
|
||||
tracks
|
||||
} else {
|
||||
tracks.iter().map(copy_track).collect()
|
||||
};
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(match to {
|
||||
TracksTo::Files(place) => {
|
||||
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| {
|
||||
.map(|(track, file_name)| {
|
||||
let name = track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| source_name.clone());
|
||||
new_file(&name, vec![track])
|
||||
.unwrap_or_else(|| file_name.to_owned());
|
||||
Rc::new(new_file(&name, vec![track]))
|
||||
})
|
||||
.collect();
|
||||
Selection::File {
|
||||
file_ids: add_files(state, files, place),
|
||||
}
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
TracksTo::File(id, place) => {
|
||||
let file = file_mut(state, id)?;
|
||||
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);
|
||||
Selection::Track {
|
||||
file_id: id,
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where segments go.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SegmentsTo {
|
||||
/// Each segment becomes a new file with a track, named like the track it comes from.
|
||||
Files(Place<FileId>),
|
||||
/// Each segment becomes a new track of the file.
|
||||
Tracks(FileId, Place<TrackId>),
|
||||
/// The segments go in a track of a file.
|
||||
Track(FileId, TrackId, Place<TrackSegmentId>),
|
||||
}
|
||||
|
||||
/// Copies the segments of a track, or moves them (with their ids) when `cut`.
|
||||
pub fn transfer_segments(
|
||||
fn transfer_segments(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
trk_id: TrackId,
|
||||
source: &HashSet<TrackSegmentId>,
|
||||
cut: bool,
|
||||
to: SegmentsTo,
|
||||
segments: &[ClipboardSegment],
|
||||
copy: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let source_track = source_file
|
||||
.trk
|
||||
let segments: Vec<(TrackSegment, &str)> = segments
|
||||
.iter()
|
||||
.find(|trk| trk.id == trk_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let source_name = source_track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| source_file.info.name.clone());
|
||||
let segments: Vec<TrackSegment> = source_track
|
||||
.trkseg
|
||||
.iter()
|
||||
.filter(|seg| source.contains(&seg.id))
|
||||
.cloned()
|
||||
.map(|clip| {
|
||||
let segment = if copy {
|
||||
copy_segment(&clip.segment)
|
||||
} else {
|
||||
clip.segment.clone()
|
||||
};
|
||||
(segment, clip.name.as_str())
|
||||
})
|
||||
.collect();
|
||||
if segments.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let destination_exists = match to {
|
||||
SegmentsTo::Files(_) => true,
|
||||
SegmentsTo::Tracks(id, _) => state.files.contains_key(&id),
|
||||
SegmentsTo::Track(id, trk, _) => state
|
||||
.files
|
||||
.get(&id)
|
||||
.is_some_and(|file| file.trk.iter().any(|t| t.id == trk)),
|
||||
};
|
||||
if !destination_exists {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
if cut {
|
||||
let file = file_mut(state, file_id)?;
|
||||
if let Some(trk) = file.trk.iter_mut().find(|trk| trk.id == trk_id) {
|
||||
trk.trkseg.retain(|seg| !source.contains(&seg.id));
|
||||
}
|
||||
}
|
||||
let segments: Vec<TrackSegment> = if cut {
|
||||
segments
|
||||
} else {
|
||||
segments.iter().map(copy_segment).collect()
|
||||
};
|
||||
let ids: HashSet<TrackSegmentId> = segments.iter().map(|seg| seg.id).collect();
|
||||
|
||||
// a segment on its own needs a track
|
||||
let own_track = |segment: TrackSegment| Track {
|
||||
trkseg: vec![segment],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(match to {
|
||||
SegmentsTo::Files(place) => {
|
||||
match to {
|
||||
Destination::Files(place) => {
|
||||
let files = segments
|
||||
.into_iter()
|
||||
.map(|segment| new_file(&source_name, vec![own_track(segment)]))
|
||||
.map(|(segment, name)| Rc::new(new_file(name, vec![own_track(segment)])))
|
||||
.collect();
|
||||
Selection::File {
|
||||
file_ids: add_files(state, files, place),
|
||||
}
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
SegmentsTo::Tracks(id, place) => {
|
||||
let tracks: Vec<Track> = segments.into_iter().map(own_track).collect();
|
||||
let trk_ids = tracks.iter().map(|trk| trk.id).collect();
|
||||
let file = file_mut(state, id)?;
|
||||
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);
|
||||
Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids,
|
||||
}
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
})
|
||||
}
|
||||
SegmentsTo::Track(id, trk_id, place) => {
|
||||
let trk = file_mut(state, id)?
|
||||
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)
|
||||
@@ -284,55 +304,39 @@ pub fn transfer_segments(
|
||||
let existing: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
|
||||
let at = place.position(&existing);
|
||||
trk.trkseg.splice(at..at, segments);
|
||||
Selection::TrackSegment {
|
||||
file_id: id,
|
||||
Ok(Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
Destination::Waypoints(..) => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies waypoints of a file (all of them when `source` is `None`) to `place` among the
|
||||
/// waypoints of the file `to`, or moves them (with their ids) when `cut`.
|
||||
pub fn transfer_waypoints(
|
||||
fn transfer_waypoints(
|
||||
state: &mut State,
|
||||
waypoints: &[Waypoint],
|
||||
copy: bool,
|
||||
file_id: FileId,
|
||||
source: Option<&HashSet<WaypointId>>,
|
||||
cut: bool,
|
||||
to: FileId,
|
||||
place: Place<WaypointId>,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let selected = |wpt: &Waypoint| source.is_none_or(|ids| ids.contains(&wpt.id));
|
||||
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let waypoints: Vec<Waypoint> = source_file
|
||||
.wpt
|
||||
let waypoints: Vec<Waypoint> = waypoints
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.filter(|wpt| selected(wpt))
|
||||
.cloned()
|
||||
.map(|wpt| {
|
||||
if copy {
|
||||
copy_waypoint(wpt)
|
||||
} else {
|
||||
wpt.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if waypoints.is_empty() || !state.files.contains_key(&to) {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
if cut {
|
||||
edit_waypoint_chunks(file_mut(state, file_id)?, selected, |wpts| {
|
||||
wpts.retain(|wpt| !selected(wpt));
|
||||
true
|
||||
});
|
||||
}
|
||||
let waypoints: Vec<Waypoint> = if cut {
|
||||
waypoints
|
||||
} else {
|
||||
waypoints.iter().map(copy_waypoint).collect()
|
||||
};
|
||||
let ids = waypoints.iter().map(|wpt| wpt.id).collect();
|
||||
let file = file_mut(state, to)?;
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<WaypointId> = waypoint_ids(file).collect();
|
||||
insert_waypoints_at(file, place.position(&existing), waypoints);
|
||||
Ok(Selection::Waypoint {
|
||||
file_id: to,
|
||||
file_id,
|
||||
wpt_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,7 +177,9 @@ impl Engine {
|
||||
|
||||
/// Puts the selection in the clipboard, if there is one.
|
||||
fn copy(&mut self, cut: bool) {
|
||||
if let Some(clipboard) = Clipboard::new(self.selection.clone(), cut) {
|
||||
if let Some(files) = self.stack.current()
|
||||
&& let Some(clipboard) = Clipboard::new(&self.selection, files, &self.order.0, cut)
|
||||
{
|
||||
self.clipboard = Some(clipboard);
|
||||
}
|
||||
}
|
||||
@@ -202,18 +204,10 @@ impl Engine {
|
||||
// files coming back after an undo/redo are added at the end of the order
|
||||
self.order.sync(files);
|
||||
self.selection.retain_existing(files);
|
||||
// what was copied is not pasted if it does not exist anymore
|
||||
if let Some(clipboard) = &mut self.clipboard {
|
||||
clipboard.selection.retain_existing(files);
|
||||
if clipboard.selection == Selection::Empty {
|
||||
self.clipboard = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.order.0.clear();
|
||||
self.selection = Selection::Empty;
|
||||
self.clipboard = None;
|
||||
}
|
||||
}
|
||||
self.statistics_cache.update(current);
|
||||
@@ -233,7 +227,8 @@ mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
Delete, DeleteAll, Load, Metadata, New, NewTrack, Paste, Style, TrackId, TrackSegmentId,
|
||||
ClipboardIds, Delete, DeleteAll, Load, Metadata, New, NewTrack, Paste, Style, TrackId,
|
||||
TrackSegmentId,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -902,8 +897,8 @@ mod tests {
|
||||
assert!(engine.execute(Action::Copy));
|
||||
assert!(engine.clipboard_changed() && !engine.selection_changed());
|
||||
assert_eq!(
|
||||
engine.clipboard().map(|c| (c.cut, &c.selection)),
|
||||
Some((false, &selected_tracks))
|
||||
engine.clipboard().map(|c| (c.content.ids(), c.cut)),
|
||||
Some((ClipboardIds::Tracks(tracks.clone()), false))
|
||||
);
|
||||
// tracks cannot be pasted onto themselves... but into a track, a file or the list of files
|
||||
assert!(engine.can_paste());
|
||||
@@ -921,15 +916,20 @@ mod tests {
|
||||
assert!(paste(&mut engine));
|
||||
assert_eq!(track_ids(&engine, a), tracks);
|
||||
assert_eq!(track_ids(&engine, b).len(), 2);
|
||||
assert!(engine.clipboard().is_none() && engine.clipboard_changed());
|
||||
// a copy stays in the clipboard, to be pasted again
|
||||
assert!(engine.clipboard().is_some() && !engine.clipboard_changed());
|
||||
assert!(
|
||||
matches!(engine.selection(), Selection::Track { file_id, trk_ids } if *file_id == b && trk_ids.len() == 2)
|
||||
);
|
||||
// the clipboard is empty: nothing more to paste
|
||||
assert!(!paste(&mut engine));
|
||||
assert!(engine.can_paste());
|
||||
assert!(paste(&mut engine));
|
||||
assert_eq!(track_ids(&engine, b).len(), 4);
|
||||
assert_eq!(track_ids(&engine, a), tracks);
|
||||
|
||||
// and it can be undone
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert_eq!(track_ids(&engine, b).len(), 2);
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(track_ids(&engine, b).is_empty());
|
||||
}
|
||||
|
||||
@@ -962,7 +962,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_drops_what_does_not_exist_anymore() {
|
||||
fn test_clipboard_survives_what_happens_to_the_elements() {
|
||||
let mut engine = Engine::default();
|
||||
let a = file_with_tracks(&mut engine, "a", 2);
|
||||
let tracks = track_ids(&engine, a);
|
||||
@@ -975,8 +975,10 @@ mod tests {
|
||||
SelectMode::Replace,
|
||||
);
|
||||
engine.execute(Action::Copy);
|
||||
let clipboard = engine.clipboard().cloned();
|
||||
assert!(clipboard.is_some());
|
||||
|
||||
// one of the tracks is deleted: the other one is still there to paste
|
||||
// one of the tracks is deleted, then the whole file: the clipboard is not affected
|
||||
select_elements(
|
||||
&mut engine,
|
||||
Selection::Track {
|
||||
@@ -989,15 +991,23 @@ mod tests {
|
||||
&mut engine,
|
||||
Command::Delete(Delete { whole_files: false })
|
||||
));
|
||||
assert!(
|
||||
matches!(engine.clipboard().map(|c| &c.selection), Some(Selection::Track { trk_ids, .. }) if trk_ids.len() == 1)
|
||||
);
|
||||
|
||||
// the whole file is deleted: nothing left
|
||||
select_elements(&mut engine, files(&[a]), SelectMode::Replace);
|
||||
assert!(edit(&mut engine, Command::DeleteAll(DeleteAll)));
|
||||
assert!(engine.clipboard().is_none());
|
||||
assert!(engine.clipboard_changed());
|
||||
assert_eq!(engine.clipboard(), clipboard.as_ref());
|
||||
assert!(!engine.clipboard_changed());
|
||||
|
||||
// both tracks are pasted in a new file, as they were when they were copied
|
||||
let b = file_with_tracks(&mut engine, "b", 0);
|
||||
select_files(&mut engine, &[b]);
|
||||
assert!(engine.can_paste());
|
||||
assert!(paste(&mut engine));
|
||||
let pasted = track_ids(&engine, b);
|
||||
assert_eq!(pasted.len(), 2);
|
||||
// copies, with new ids
|
||||
assert!(pasted.iter().all(|id| !tracks.contains(id)));
|
||||
// and again
|
||||
assert!(paste(&mut engine));
|
||||
assert_eq!(track_ids(&engine, b).len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1010,12 +1020,12 @@ mod tests {
|
||||
select_files(&mut engine, &[b]);
|
||||
assert!(engine.execute(Action::Cut));
|
||||
assert_eq!(
|
||||
engine.clipboard().map(|c| (c.cut, &c.selection)),
|
||||
Some((true, &files(&[b])))
|
||||
engine.clipboard().map(|c| (c.content.ids(), c.cut)),
|
||||
Some((ClipboardIds::Files(vec![b]), true))
|
||||
);
|
||||
// the same content again: nothing changed
|
||||
assert!(!engine.execute(Action::Cut));
|
||||
assert!(!engine.clipboard_changed());
|
||||
// the same elements again are a new copy
|
||||
assert!(engine.execute(Action::Cut));
|
||||
assert!(engine.clipboard_changed());
|
||||
// only the creations of the files are in the history
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(engine.execute(Action::Undo));
|
||||
|
||||
@@ -1,17 +1,159 @@
|
||||
use crate::Selection;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// What was copied or cut: the elements, by id, so that they are found again when pasting, even
|
||||
/// if the files changed in between. Cut elements stay in place until they are pasted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
File, FileId, Selection, StackEntry, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint,
|
||||
WaypointId,
|
||||
};
|
||||
|
||||
/// A track in the clipboard, with the name of the file it comes from (the name of the new file
|
||||
/// when it is pasted on its own).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClipboardTrack {
|
||||
pub track: Track,
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
/// A segment in the clipboard, with the name of its track, or of its file when the track has
|
||||
/// none (the name of the new file when it is pasted on its own).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClipboardSegment {
|
||||
pub segment: TrackSegment,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The elements that were copied or cut, in the order of the files.
|
||||
///
|
||||
/// It holds the elements themselves, not references to them: they are cheap to keep (files are
|
||||
/// shared, and so are the trackpoints of tracks and segments), and the clipboard stays valid
|
||||
/// when the elements are changed or deleted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClipboardContent {
|
||||
Files(Vec<Rc<File>>),
|
||||
Tracks(Vec<ClipboardTrack>),
|
||||
Segments(Vec<ClipboardSegment>),
|
||||
Waypoints(Vec<Waypoint>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ClipboardId(Uuid);
|
||||
|
||||
impl Default for ClipboardId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
/// What was copied or cut, to be pasted, as many times as needed.
|
||||
///
|
||||
/// What is pasted is always the elements as they were when they were copied or cut, whatever
|
||||
/// happened to them since, even if they were deleted. Pasting what was copied makes copies, with
|
||||
/// new ids. What was cut is moved, with its ids, the first time it is pasted (the elements that
|
||||
/// are still there are removed): the clipboard is not cut anymore after that, and the next
|
||||
/// pastes make copies.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Clipboard {
|
||||
pub selection: Selection,
|
||||
id: ClipboardId,
|
||||
pub content: ClipboardContent,
|
||||
pub cut: bool,
|
||||
}
|
||||
|
||||
/// Two clipboards are the same when they come from the same copy, and are cut the same way.
|
||||
impl PartialEq for Clipboard {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id && self.cut == other.cut
|
||||
}
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
/// `None` if nothing is selected.
|
||||
pub fn new(selection: Selection, cut: bool) -> Option<Clipboard> {
|
||||
(selection != Selection::Empty).then_some(Clipboard { selection, cut })
|
||||
/// The clipboard for the selected elements, `None` if nothing is selected (or if what is
|
||||
/// selected does not exist).
|
||||
pub fn new(
|
||||
selection: &Selection,
|
||||
files: &StackEntry,
|
||||
order: &[FileId],
|
||||
cut: bool,
|
||||
) -> Option<Clipboard> {
|
||||
let content = match selection {
|
||||
Selection::Empty => return None,
|
||||
Selection::File { file_ids } => ClipboardContent::Files(
|
||||
order
|
||||
.iter()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.filter_map(|id| files.get(id).cloned())
|
||||
.collect(),
|
||||
),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = files.get(file_id)?;
|
||||
ClipboardContent::Tracks(
|
||||
file.trk
|
||||
.iter()
|
||||
.filter(|trk| trk_ids.contains(&trk.id))
|
||||
.map(|trk| ClipboardTrack {
|
||||
track: trk.clone(),
|
||||
file_name: file.info.name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let file = files.get(file_id)?;
|
||||
let trk = file.trk.iter().find(|trk| trk.id == *trk_id)?;
|
||||
let name = trk
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file.info.name.clone());
|
||||
ClipboardContent::Segments(
|
||||
trk.trkseg
|
||||
.iter()
|
||||
.filter(|seg| trkseg_ids.contains(&seg.id))
|
||||
.map(|seg| ClipboardSegment {
|
||||
segment: seg.clone(),
|
||||
name: name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Selection::Waypoints { file_id } => ClipboardContent::Waypoints(
|
||||
files
|
||||
.get(file_id)?
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| chunk.wpt.iter().cloned())
|
||||
.collect(),
|
||||
),
|
||||
Selection::Waypoint { file_id, wpt_ids } => ClipboardContent::Waypoints(
|
||||
files
|
||||
.get(file_id)?
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.filter(|wpt| wpt_ids.contains(&wpt.id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
let clipboard = Clipboard {
|
||||
id: ClipboardId::default(),
|
||||
content,
|
||||
cut,
|
||||
};
|
||||
(!clipboard.is_empty()).then_some(clipboard)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match &self.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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the content can be pasted when `target` is selected: files and tracks can be
|
||||
@@ -25,91 +167,306 @@ impl Clipboard {
|
||||
/// | segments | nothing, a file, a track, a segment |
|
||||
/// | waypoints | a file, the waypoints, a waypoint |
|
||||
pub fn can_paste(&self, target: &Selection) -> bool {
|
||||
use ClipboardContent as C;
|
||||
use Selection::*;
|
||||
matches!(
|
||||
(&self.selection, target),
|
||||
(File { .. }, Empty | File { .. })
|
||||
| (Track { .. }, Empty | File { .. } | Track { .. })
|
||||
(&self.content, target),
|
||||
(C::Files(_), Empty | File { .. })
|
||||
| (C::Tracks(_), Empty | File { .. } | Track { .. })
|
||||
| (
|
||||
TrackSegment { .. },
|
||||
C::Segments(_),
|
||||
Empty | File { .. } | Track { .. } | TrackSegment { .. }
|
||||
)
|
||||
| (
|
||||
Waypoints { .. } | Waypoint { .. },
|
||||
C::Waypoints(_),
|
||||
File { .. } | Waypoints { .. } | Waypoint { .. }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids of the elements of the content, by kind.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ClipboardIds {
|
||||
Files(Vec<FileId>),
|
||||
Tracks(Vec<TrackId>),
|
||||
Segments(Vec<TrackSegmentId>),
|
||||
Waypoints(Vec<WaypointId>),
|
||||
}
|
||||
|
||||
impl ClipboardContent {
|
||||
pub fn ids(&self) -> ClipboardIds {
|
||||
match self {
|
||||
ClipboardContent::Files(files) => {
|
||||
ClipboardIds::Files(files.iter().map(|file| file.id).collect())
|
||||
}
|
||||
ClipboardContent::Tracks(tracks) => {
|
||||
ClipboardIds::Tracks(tracks.iter().map(|trk| trk.track.id).collect())
|
||||
}
|
||||
ClipboardContent::Segments(segments) => {
|
||||
ClipboardIds::Segments(segments.iter().map(|seg| seg.segment.id).collect())
|
||||
}
|
||||
ClipboardContent::Waypoints(waypoints) => {
|
||||
ClipboardIds::Waypoints(waypoints.iter().map(|wpt| wpt.id).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, TrackId, TrackSegmentId, WaypointId};
|
||||
use crate::{TrackInfo, WaypointChunk};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn file() -> Selection {
|
||||
Selection::File {
|
||||
file_ids: [FileId::default()].into(),
|
||||
/// A file with 3 tracks (named "t0", "t1", "t2") of 2 segments and 3 waypoints.
|
||||
fn file(name: &str) -> File {
|
||||
let mut file = File::default();
|
||||
file.info.name = name.to_owned();
|
||||
for i in 0..3 {
|
||||
file.trk.push(Track {
|
||||
info: TrackInfo {
|
||||
name: (i < 2).then(|| format!("t{i}")),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: vec![TrackSegment::default(), TrackSegment::default()],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||
..Default::default()
|
||||
}));
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![Waypoint::default()],
|
||||
..Default::default()
|
||||
}));
|
||||
file
|
||||
}
|
||||
|
||||
fn track() -> Selection {
|
||||
fn files_of(files: Vec<File>) -> StackEntry {
|
||||
files
|
||||
.into_iter()
|
||||
.map(|file| (file.id, Rc::new(file)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn order_of(files: &StackEntry) -> Vec<FileId> {
|
||||
files.keys().copied().collect()
|
||||
}
|
||||
|
||||
fn tracks(file: &File, ids: &[usize]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id: FileId::default(),
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
file_id: file.id,
|
||||
trk_ids: ids.iter().map(|i| file.trk[*i].id).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn segment() -> Selection {
|
||||
Selection::TrackSegment {
|
||||
file_id: FileId::default(),
|
||||
trk_id: TrackId::default(),
|
||||
trkseg_ids: [TrackSegmentId::default()].into(),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_the_clipboard_holds_what_is_selected() {
|
||||
let file = file("source");
|
||||
let (t0, t2) = (file.trk[0].id, file.trk[2].id);
|
||||
let (s0, s1) = (file.trk[1].trkseg[0].id, file.trk[1].trkseg[1].id);
|
||||
let wpt: Vec<_> = file
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|c| c.wpt.iter().map(|w| w.id))
|
||||
.collect();
|
||||
let files = files_of(vec![file.clone()]);
|
||||
|
||||
fn waypoints() -> Selection {
|
||||
Selection::Waypoints {
|
||||
file_id: FileId::default(),
|
||||
// tracks, in the order of the file, with the name of their file
|
||||
let clipboard =
|
||||
Clipboard::new(&tracks(&file, &[2, 0]), &files, &order_of(&files), false).unwrap();
|
||||
assert!(!clipboard.cut);
|
||||
match &clipboard.content {
|
||||
ClipboardContent::Tracks(copied) => {
|
||||
assert_eq!(
|
||||
copied.iter().map(|c| c.track.id).collect::<Vec<_>>(),
|
||||
vec![t0, t2]
|
||||
);
|
||||
assert!(copied.iter().all(|c| c.file_name == "source"));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoint() -> Selection {
|
||||
Selection::Waypoint {
|
||||
file_id: FileId::default(),
|
||||
wpt_ids: HashSet::from([WaypointId::default()]),
|
||||
// segments, with the name of their track, or else of their file
|
||||
let segments = |trk: usize, ids: &[TrackSegmentId]| Selection::TrackSegment {
|
||||
file_id: file.id,
|
||||
trk_id: file.trk[trk].id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
match Clipboard::new(&segments(1, &[s1, s0]), &files, &order_of(&files), true)
|
||||
.unwrap()
|
||||
.content
|
||||
{
|
||||
ClipboardContent::Segments(copied) => {
|
||||
assert_eq!(
|
||||
copied.iter().map(|c| c.segment.id).collect::<Vec<_>>(),
|
||||
vec![s0, s1]
|
||||
);
|
||||
assert!(copied.iter().all(|c| c.name == "t1"));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
let unnamed = file.trk[2].trkseg[0].id;
|
||||
match Clipboard::new(&segments(2, &[unnamed]), &files, &order_of(&files), true)
|
||||
.unwrap()
|
||||
.content
|
||||
{
|
||||
ClipboardContent::Segments(copied) => assert_eq!(copied[0].name, "source"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
|
||||
// waypoints: some of them, or all of them
|
||||
let some = Selection::Waypoint {
|
||||
file_id: file.id,
|
||||
wpt_ids: [wpt[2], wpt[0]].into(),
|
||||
};
|
||||
assert_eq!(
|
||||
Clipboard::new(&some, &files, &order_of(&files), false)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Waypoints(vec![wpt[0], wpt[2]])
|
||||
);
|
||||
assert_eq!(
|
||||
Clipboard::new(
|
||||
&Selection::Waypoints { file_id: file.id },
|
||||
&files,
|
||||
&order_of(&files),
|
||||
false
|
||||
)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Waypoints(wpt)
|
||||
);
|
||||
|
||||
// files
|
||||
let selection = Selection::File {
|
||||
file_ids: [file.id].into(),
|
||||
};
|
||||
assert_eq!(
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Files(vec![file.id])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_copy() {
|
||||
assert_eq!(Clipboard::new(Selection::Empty, false), None);
|
||||
assert!(Clipboard::new(file(), true).is_some_and(|c| c.cut));
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
assert!(Clipboard::new(&Selection::Empty, &files, &order_of(&files), false).is_none());
|
||||
// elements that do not exist
|
||||
let unknown = Selection::Track {
|
||||
file_id: FileId::default(),
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
};
|
||||
assert!(Clipboard::new(&unknown, &files, &order_of(&files), false).is_none());
|
||||
let unknown = Selection::Track {
|
||||
file_id: file.id,
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
};
|
||||
assert!(Clipboard::new(&unknown, &files, &order_of(&files), false).is_none());
|
||||
assert!(
|
||||
Clipboard::new(
|
||||
&Selection::File {
|
||||
file_ids: [FileId::default()].into()
|
||||
},
|
||||
&files,
|
||||
&order_of(&files),
|
||||
false
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
// a file without waypoints
|
||||
let empty = files_of(vec![File::default()]);
|
||||
let id = *empty.keys().next().unwrap();
|
||||
assert!(
|
||||
Clipboard::new(
|
||||
&Selection::Waypoints { file_id: id },
|
||||
&empty,
|
||||
&order_of(&empty),
|
||||
true
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_clipboards_are_the_same_when_they_come_from_the_same_copy() {
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
let selection = tracks(&file, &[0]);
|
||||
let clipboard = Clipboard::new(&selection, &files, &order_of(&files), false).unwrap();
|
||||
assert_eq!(clipboard, clipboard.clone());
|
||||
// the same elements copied again make another clipboard
|
||||
assert_ne!(
|
||||
clipboard,
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false).unwrap()
|
||||
);
|
||||
// and it is not the same once it is not cut
|
||||
let mut copy = clipboard.clone();
|
||||
copy.cut = true;
|
||||
assert_ne!(clipboard, copy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_clipboard_survives_what_happens_to_the_files() {
|
||||
let file = file("f");
|
||||
let mut files = files_of(vec![file.clone()]);
|
||||
let clipboard =
|
||||
Clipboard::new(&tracks(&file, &[0, 1]), &files, &order_of(&files), true).unwrap();
|
||||
files.clear();
|
||||
assert!(!clipboard.is_empty());
|
||||
assert_eq!(
|
||||
clipboard.content.ids(),
|
||||
ClipboardIds::Tracks(vec![file.trk[0].id, file.trk[1].id])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_where_things_can_be_pasted() {
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
let selection_of = |selection: Selection| {
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false).unwrap()
|
||||
};
|
||||
let file_selection = Selection::File {
|
||||
file_ids: [file.id].into(),
|
||||
};
|
||||
let track = tracks(&file, &[0]);
|
||||
let segment = Selection::TrackSegment {
|
||||
file_id: file.id,
|
||||
trk_id: file.trk[0].id,
|
||||
trkseg_ids: [file.trk[0].trkseg[0].id].into(),
|
||||
};
|
||||
let waypoints = Selection::Waypoints { file_id: file.id };
|
||||
let waypoint = Selection::Waypoint {
|
||||
file_id: file.id,
|
||||
wpt_ids: [file.wpt[0].wpt[0].id].into(),
|
||||
};
|
||||
let targets = [
|
||||
Selection::Empty,
|
||||
file(),
|
||||
track(),
|
||||
segment(),
|
||||
waypoints(),
|
||||
waypoint(),
|
||||
file_selection.clone(),
|
||||
track.clone(),
|
||||
segment.clone(),
|
||||
waypoints.clone(),
|
||||
waypoint.clone(),
|
||||
];
|
||||
let expected = [
|
||||
// nothing, file, track, segment, waypoints, waypoint
|
||||
(file(), [true, true, false, false, false, false]),
|
||||
(track(), [true, true, true, false, false, false]),
|
||||
(segment(), [true, true, true, true, false, false]),
|
||||
(waypoints(), [false, true, false, false, true, true]),
|
||||
(waypoint(), [false, true, false, false, true, true]),
|
||||
(file_selection, [true, true, false, false, false, false]),
|
||||
(track, [true, true, true, false, false, false]),
|
||||
(segment, [true, true, true, true, false, false]),
|
||||
(waypoints, [false, true, false, false, true, true]),
|
||||
(waypoint, [false, true, false, false, true, true]),
|
||||
];
|
||||
for (copied, allowed) in expected {
|
||||
let clipboard = Clipboard::new(copied.clone(), false).unwrap();
|
||||
let clipboard = selection_of(copied.clone());
|
||||
for (target, allowed) in targets.iter().zip(allowed) {
|
||||
assert_eq!(
|
||||
clipboard.can_paste(target),
|
||||
|
||||
+17
-5
@@ -372,7 +372,10 @@ pub fn cut() -> bool {
|
||||
execute(Action::Cut)
|
||||
}
|
||||
|
||||
/// Pastes the clipboard according to the selection (see `can_paste`).
|
||||
/// Pastes the clipboard according to the selection (see `can_paste`). What is pasted is always
|
||||
/// the elements as they were when they were copied or cut, even if they were changed or deleted
|
||||
/// since. It can be pasted several times: what was copied is copied again, what was cut is moved
|
||||
/// the first time (and the clipboard is not cut after that), then it is copied.
|
||||
#[wasm_bindgen]
|
||||
pub fn paste() -> bool {
|
||||
edit(Command::Paste(engine::Paste))
|
||||
@@ -610,9 +613,10 @@ export type MoveTarget =
|
||||
| { type: 'tracks'; fileId: string; index: number }
|
||||
| { type: 'segments'; fileId: string; trackId: string; index: number }
|
||||
| { type: 'waypoints'; fileId: string; index: number };
|
||||
/** What was copied or cut, to be pasted. */
|
||||
/** What was copied or cut, to be pasted: the kind of the elements and their ids. */
|
||||
export interface Clipboard {
|
||||
selection: Selection;
|
||||
type: 'files' | 'tracks' | 'segments' | 'waypoints';
|
||||
ids: string[];
|
||||
cut: boolean;
|
||||
}
|
||||
export interface FilesUpdate {
|
||||
@@ -921,13 +925,21 @@ pub fn selection() -> Selection {
|
||||
.unchecked_into()
|
||||
}
|
||||
|
||||
/// What was copied or cut and is waiting to be pasted, `undefined` if nothing.
|
||||
/// What was copied or cut and is waiting to be pasted, `undefined` if nothing: the kind of the
|
||||
/// elements and their ids (UUID strings).
|
||||
#[wasm_bindgen]
|
||||
pub fn clipboard() -> Clipboard {
|
||||
with_engine(|e| {
|
||||
e.clipboard().map_or(JsValue::UNDEFINED, |clipboard| {
|
||||
let object = Object::new();
|
||||
set(&object, "selection", selection_object(&clipboard.selection));
|
||||
let (kind, ids) = match clipboard.content.ids() {
|
||||
engine::ClipboardIds::Files(ids) => ("files", uuids(ids, |id| id.0)),
|
||||
engine::ClipboardIds::Tracks(ids) => ("tracks", uuids(ids, |id| id.0)),
|
||||
engine::ClipboardIds::Segments(ids) => ("segments", uuids(ids, |id| id.0)),
|
||||
engine::ClipboardIds::Waypoints(ids) => ("waypoints", uuids(ids, |id| id.0)),
|
||||
};
|
||||
set(&object, "type", kind);
|
||||
set(&object, "ids", ids);
|
||||
set(&object, "cut", clipboard.cut);
|
||||
object.into()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user