mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-06 22:04:06 +00:00
progress
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, FileId, Place, SegmentsTo, Selection, State, TrackId, TracksTo,
|
Apply, Clipboard, CommandError, Destination, FileId, Place, Selection, State, TrackId, transfer,
|
||||||
transfer_files, transfer_segments, transfer_tracks, transfer_waypoints,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Where moved elements go: a list of the file tree, and the position in it. The position is
|
/// 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,
|
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 {
|
impl Apply for Move {
|
||||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
use MoveTarget as To;
|
let clipboard = Clipboard::new(&self.what, state.files, &state.order.0, true)
|
||||||
let next = match (&self.what, self.to) {
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
(Selection::File { file_ids }, To::Files { index }) => {
|
*state.selection = transfer(state, &clipboard.content, true, self.to.into())?;
|
||||||
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;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::{collections::HashSet, hash::Hash};
|
use std::{collections::HashSet, hash::Hash};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, Place, SegmentsTo, Selection, State, TracksTo, transfer_files,
|
Apply, ClipboardContent, CommandError, Destination, FileId, Place, Selection, State, TrackId,
|
||||||
transfer_segments, transfer_tracks, transfer_waypoints, waypoint_ids,
|
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`]:
|
/// 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
|
/// - 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;
|
/// - 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.
|
/// - 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
|
/// Copies are made, with new ids, and the pasted elements are selected. What was cut is moved
|
||||||
/// selected.
|
/// 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)]
|
#[derive(Debug)]
|
||||||
pub struct Paste;
|
pub struct Paste;
|
||||||
|
|
||||||
@@ -29,91 +33,97 @@ impl Apply for Paste {
|
|||||||
.filter(|clipboard| clipboard.can_paste(state.selection))
|
.filter(|clipboard| clipboard.can_paste(state.selection))
|
||||||
.ok_or(CommandError::NothingToDo)?;
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
let cut = clipboard.cut;
|
let cut = clipboard.cut;
|
||||||
let next = match &clipboard.selection {
|
// always the elements as they were when they were copied or cut
|
||||||
Selection::File { file_ids } => transfer_files(state, file_ids, cut, Place::End)?,
|
let to = destination(state, &clipboard.content)?;
|
||||||
Selection::Track { file_id, trk_ids } => {
|
let next = transfer(state, &clipboard.content, cut, to)?;
|
||||||
let to = match &*state.selection {
|
|
||||||
Selection::Empty => TracksTo::Files(Place::End),
|
if cut && let Some(clipboard) = state.clipboard.as_mut() {
|
||||||
Selection::File { file_ids } => TracksTo::File(
|
// moved: the next pastes make copies
|
||||||
last_selected(state.order.0.iter().copied(), file_ids)
|
clipboard.cut = false;
|
||||||
.ok_or(CommandError::NothingToDo)?,
|
}
|
||||||
Place::End,
|
*state.selection = next;
|
||||||
),
|
Ok(())
|
||||||
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)?;
|
fn track_ids_of(state: &State, file_id: FileId) -> Vec<TrackId> {
|
||||||
TracksTo::File(*file_id, Place::After(last))
|
state
|
||||||
}
|
.files
|
||||||
_ => return Err(CommandError::NothingToDo),
|
.get(&file_id)
|
||||||
};
|
.map(|file| file.trk.iter().map(|trk| trk.id).collect())
|
||||||
transfer_tracks(state, *file_id, trk_ids, cut, to)?
|
.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 {
|
Selection::TrackSegment {
|
||||||
file_id,
|
file_id,
|
||||||
trk_id,
|
trk_id,
|
||||||
trkseg_ids,
|
trkseg_ids,
|
||||||
} => {
|
},
|
||||||
let to = match &*state.selection {
|
) => {
|
||||||
Selection::Empty => SegmentsTo::Files(Place::End),
|
let last = last_selected(
|
||||||
Selection::File { file_ids } => SegmentsTo::Tracks(
|
segment_ids_of(state, *file_id, *trk_id).into_iter(),
|
||||||
last_selected(state.order.0.iter().copied(), file_ids)
|
trkseg_ids,
|
||||||
.ok_or(CommandError::NothingToDo)?,
|
)
|
||||||
Place::End,
|
.ok_or(nothing)?;
|
||||||
),
|
Destination::Segments(*file_id, *trk_id, Place::After(last))
|
||||||
Selection::Track { file_id, trk_ids } => {
|
}
|
||||||
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
|
(ClipboardContent::Waypoints(_), Selection::File { file_ids }) => {
|
||||||
let last = last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
|
Destination::Waypoints(last_file(file_ids)?, Place::End)
|
||||||
.ok_or(CommandError::NothingToDo)?;
|
}
|
||||||
SegmentsTo::Track(*file_id, last, Place::End)
|
(ClipboardContent::Waypoints(_), Selection::Waypoints { file_id }) => {
|
||||||
}
|
Destination::Waypoints(*file_id, Place::End)
|
||||||
Selection::TrackSegment {
|
}
|
||||||
file_id,
|
(ClipboardContent::Waypoints(_), Selection::Waypoint { file_id, wpt_ids }) => {
|
||||||
trk_id,
|
let last = last_selected(waypoint_ids_of(state, *file_id).into_iter(), wpt_ids)
|
||||||
trkseg_ids,
|
.ok_or(nothing)?;
|
||||||
} => {
|
Destination::Waypoints(*file_id, Place::After(last))
|
||||||
let trk = state
|
}
|
||||||
.files
|
_ => return Err(nothing),
|
||||||
.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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The last of the `selected` ids in the order of `all`: the element a paste goes after.
|
/// 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 std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Clipboard, File, FileId, Track, TrackId, TrackInfo, TrackSegment, TrackSegmentId, Waypoint,
|
Clipboard, ClipboardIds, File, FileId, Track, TrackId, TrackInfo, TrackSegment,
|
||||||
WaypointId, engine::command::fixture::Fixture, new_file,
|
TrackSegmentId, Waypoint, WaypointId, engine::command::fixture::Fixture, new_file,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -188,7 +198,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn copy(fx: &mut Fixture, selection: Selection, cut: bool) {
|
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> {
|
fn paste(fx: &mut Fixture) -> Result<(), CommandError> {
|
||||||
@@ -254,9 +269,12 @@ mod tests {
|
|||||||
// the segments are copies too
|
// the segments are copies too
|
||||||
assert_eq!(fx.files[&b].trk[2].trkseg.len(), 2);
|
assert_eq!(fx.files[&b].trk[2].trkseg.len(), 2);
|
||||||
assert_ne!(segment_ids(&fx, b, 2), segment_ids(&fx, a, 1));
|
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.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]
|
#[test]
|
||||||
@@ -535,7 +553,7 @@ mod tests {
|
|||||||
assert!(fx.clipboard.is_some());
|
assert!(fx.clipboard.is_some());
|
||||||
assert_eq!(fx.selection, waypoints(a, &w));
|
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);
|
copy(&mut fx, tracks(a, &[TrackId::default()]), false);
|
||||||
fx.selection = files(&[a]);
|
fx.selection = files(&[a]);
|
||||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||||
@@ -547,4 +565,226 @@ mod tests {
|
|||||||
assert_eq!(fx.files.len(), 1);
|
assert_eq!(fx.files.len(), 1);
|
||||||
assert_eq!(fx.order.0, vec![a]);
|
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
|
//! Copying or moving elements to a place of the file tree: what paste and move are made of.
|
||||||
//! made of. The elements come from where they are found by id, and the place says where they
|
//! The elements come as the content of a clipboard, and a destination says where they go among
|
||||||
//! go among the elements already there.
|
//! the elements already there.
|
||||||
|
|
||||||
use std::{collections::HashSet, rc::Rc};
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CommandError, File, FileId, Selection, State, Track, TrackId, TrackSegment, TrackSegmentId,
|
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
|
||||||
Waypoint, WaypointId, copy_file, copy_segment, copy_track, copy_waypoint, edit_waypoint_chunks,
|
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
|
||||||
insert_waypoints_at,
|
copy_segment, copy_track, copy_waypoint, edit_waypoint_chunks, insert_waypoints_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Where elements are put, among the others of the same list.
|
/// 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> + '_ {
|
pub fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
|
||||||
file.wpt
|
file.wpt
|
||||||
.iter()
|
.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.
|
/// 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 at = place.position(&state.order.0);
|
||||||
let ids: Vec<FileId> = files.iter().map(|file| file.id).collect();
|
let ids: Vec<FileId> = files.iter().map(|file| file.id).collect();
|
||||||
for file in files {
|
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());
|
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`.
|
/// Removes the elements of the content from the files, wherever they are.
|
||||||
pub fn transfer_files(
|
pub fn remove_elements(state: &mut State, content: &ClipboardContent) {
|
||||||
state: &mut State,
|
match content {
|
||||||
source: &HashSet<FileId>,
|
ClipboardContent::Files(files) => {
|
||||||
cut: bool,
|
let ids: HashSet<FileId> = files.iter().map(|file| file.id).collect();
|
||||||
place: Place<FileId>,
|
state.files.retain(|id, _| !ids.contains(id));
|
||||||
) -> Result<Selection, CommandError> {
|
state.order.0.retain(|id| !ids.contains(id));
|
||||||
let ids: Vec<FileId> = state
|
}
|
||||||
.order
|
ClipboardContent::Tracks(tracks) => {
|
||||||
.0
|
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.track.id).collect();
|
||||||
.iter()
|
for file in state.files.values_mut() {
|
||||||
.copied()
|
if file.trk.iter().any(|trk| ids.contains(&trk.id)) {
|
||||||
.filter(|id| source.contains(id) && state.files.contains_key(id))
|
Rc::make_mut(file).trk.retain(|trk| !ids.contains(&trk.id));
|
||||||
.collect();
|
}
|
||||||
if ids.is_empty() {
|
}
|
||||||
return Err(CommandError::NothingToDo);
|
}
|
||||||
|
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 {
|
/// Puts the elements of the content at the destination: copies of them, or the elements
|
||||||
// the position is among the files that stay
|
/// themselves, with their ids, when `cut` (they are removed from where they are first). Returns
|
||||||
let others: Vec<FileId> = state
|
/// the selection of what was put.
|
||||||
.order
|
pub fn transfer(
|
||||||
.0
|
state: &mut State,
|
||||||
.iter()
|
content: &ClipboardContent,
|
||||||
.copied()
|
cut: bool,
|
||||||
.filter(|id| !ids.contains(id))
|
to: Destination,
|
||||||
.collect();
|
) -> Result<Selection, CommandError> {
|
||||||
state.order.move_files(&ids, place.position(&others));
|
// the destination has to be there, before anything is changed
|
||||||
ids.into_iter().collect()
|
let exists = match to {
|
||||||
} else {
|
Destination::Files(_) => true,
|
||||||
let copies = ids.iter().map(|id| copy_file(&state.files[id])).collect();
|
Destination::Tracks(file_id, _) => state.files.contains_key(&file_id),
|
||||||
add_files(state, copies, place)
|
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 })
|
let valid = match (content, to) {
|
||||||
}
|
(ClipboardContent::Files(_), Destination::Files(_))
|
||||||
|
| (ClipboardContent::Tracks(_), Destination::Files(_) | Destination::Tracks(..))
|
||||||
/// Where tracks go.
|
| (
|
||||||
#[derive(Debug, Clone, Copy)]
|
ClipboardContent::Segments(_),
|
||||||
pub enum TracksTo {
|
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
|
||||||
/// Each track becomes a new file, named like the track, or like the file it comes from.
|
)
|
||||||
Files(Place<FileId>),
|
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..)) => true,
|
||||||
/// The tracks go in a file.
|
_ => false,
|
||||||
File(FileId, Place<TrackId>),
|
};
|
||||||
}
|
let empty = match content {
|
||||||
|
ClipboardContent::Files(files) => files.is_empty(),
|
||||||
/// Copies the tracks, or moves them (with their ids) when `cut`.
|
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||||
pub fn transfer_tracks(
|
ClipboardContent::Segments(segments) => segments.is_empty(),
|
||||||
state: &mut State,
|
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
||||||
file_id: FileId,
|
};
|
||||||
source: &HashSet<TrackId>,
|
if !exists || !valid || empty {
|
||||||
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)
|
|
||||||
{
|
|
||||||
return Err(CommandError::NothingToDo);
|
return Err(CommandError::NothingToDo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cut {
|
if cut {
|
||||||
file_mut(state, file_id)?
|
remove_elements(state, content);
|
||||||
.trk
|
|
||||||
.retain(|trk| !source.contains(&trk.id));
|
|
||||||
}
|
}
|
||||||
let tracks: Vec<Track> = if cut {
|
let copy = !cut;
|
||||||
tracks
|
match (content, to) {
|
||||||
} else {
|
(ClipboardContent::Files(files), Destination::Files(place)) => {
|
||||||
tracks.iter().map(copy_track).collect()
|
let files = files
|
||||||
};
|
.iter()
|
||||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
|
.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 {
|
fn transfer_tracks(
|
||||||
TracksTo::Files(place) => {
|
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
|
let files = tracks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|track| {
|
.map(|(track, file_name)| {
|
||||||
let name = track
|
let name = track
|
||||||
.info
|
.info
|
||||||
.name
|
.name
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| source_name.clone());
|
.unwrap_or_else(|| file_name.to_owned());
|
||||||
new_file(&name, vec![track])
|
Rc::new(new_file(&name, vec![track]))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Selection::File {
|
Ok(add_files(state, files, place))
|
||||||
file_ids: add_files(state, files, place),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
TracksTo::File(id, place) => {
|
Destination::Tracks(file_id, place) => {
|
||||||
let file = file_mut(state, id)?;
|
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 existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||||
let at = place.position(&existing);
|
let at = place.position(&existing);
|
||||||
file.trk.splice(at..at, tracks);
|
file.trk.splice(at..at, tracks);
|
||||||
Selection::Track {
|
Ok(Selection::Track {
|
||||||
file_id: id,
|
file_id,
|
||||||
trk_ids: ids,
|
trk_ids: ids,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
})
|
_ => Err(CommandError::NothingToDo),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where segments go.
|
fn transfer_segments(
|
||||||
#[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(
|
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
file_id: FileId,
|
segments: &[ClipboardSegment],
|
||||||
trk_id: TrackId,
|
copy: bool,
|
||||||
source: &HashSet<TrackSegmentId>,
|
to: Destination,
|
||||||
cut: bool,
|
|
||||||
to: SegmentsTo,
|
|
||||||
) -> Result<Selection, CommandError> {
|
) -> Result<Selection, CommandError> {
|
||||||
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
let segments: Vec<(TrackSegment, &str)> = segments
|
||||||
let source_track = source_file
|
|
||||||
.trk
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|trk| trk.id == trk_id)
|
.map(|clip| {
|
||||||
.ok_or(CommandError::NothingToDo)?;
|
let segment = if copy {
|
||||||
let source_name = source_track
|
copy_segment(&clip.segment)
|
||||||
.info
|
} else {
|
||||||
.name
|
clip.segment.clone()
|
||||||
.clone()
|
};
|
||||||
.unwrap_or_else(|| source_file.info.name.clone());
|
(segment, clip.name.as_str())
|
||||||
let segments: Vec<TrackSegment> = source_track
|
})
|
||||||
.trkseg
|
|
||||||
.iter()
|
|
||||||
.filter(|seg| source.contains(&seg.id))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
.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
|
// a segment on its own needs a track
|
||||||
let own_track = |segment: TrackSegment| Track {
|
let own_track = |segment: TrackSegment| Track {
|
||||||
trkseg: vec![segment],
|
trkseg: vec![segment],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
match to {
|
||||||
Ok(match to {
|
Destination::Files(place) => {
|
||||||
SegmentsTo::Files(place) => {
|
|
||||||
let files = segments
|
let files = segments
|
||||||
.into_iter()
|
.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();
|
.collect();
|
||||||
Selection::File {
|
Ok(add_files(state, files, place))
|
||||||
file_ids: add_files(state, files, place),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SegmentsTo::Tracks(id, place) => {
|
Destination::Tracks(file_id, place) => {
|
||||||
let tracks: Vec<Track> = segments.into_iter().map(own_track).collect();
|
let tracks: Vec<Track> = segments
|
||||||
let trk_ids = tracks.iter().map(|trk| trk.id).collect();
|
.into_iter()
|
||||||
let file = file_mut(state, id)?;
|
.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 existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||||
let at = place.position(&existing);
|
let at = place.position(&existing);
|
||||||
file.trk.splice(at..at, tracks);
|
file.trk.splice(at..at, tracks);
|
||||||
Selection::Track {
|
Ok(Selection::Track {
|
||||||
file_id: id,
|
file_id,
|
||||||
trk_ids,
|
trk_ids: ids,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
SegmentsTo::Track(id, trk_id, place) => {
|
Destination::Segments(file_id, trk_id, place) => {
|
||||||
let trk = file_mut(state, id)?
|
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
|
.trk
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|trk| trk.id == trk_id)
|
.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 existing: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
|
||||||
let at = place.position(&existing);
|
let at = place.position(&existing);
|
||||||
trk.trkseg.splice(at..at, segments);
|
trk.trkseg.splice(at..at, segments);
|
||||||
Selection::TrackSegment {
|
Ok(Selection::TrackSegment {
|
||||||
file_id: id,
|
file_id,
|
||||||
trk_id,
|
trk_id,
|
||||||
trkseg_ids: ids,
|
trkseg_ids: ids,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
})
|
Destination::Waypoints(..) => Err(CommandError::NothingToDo),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copies waypoints of a file (all of them when `source` is `None`) to `place` among the
|
fn transfer_waypoints(
|
||||||
/// waypoints of the file `to`, or moves them (with their ids) when `cut`.
|
|
||||||
pub fn transfer_waypoints(
|
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
|
waypoints: &[Waypoint],
|
||||||
|
copy: bool,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
source: Option<&HashSet<WaypointId>>,
|
|
||||||
cut: bool,
|
|
||||||
to: FileId,
|
|
||||||
place: Place<WaypointId>,
|
place: Place<WaypointId>,
|
||||||
) -> Result<Selection, CommandError> {
|
) -> Result<Selection, CommandError> {
|
||||||
let selected = |wpt: &Waypoint| source.is_none_or(|ids| ids.contains(&wpt.id));
|
let waypoints: Vec<Waypoint> = waypoints
|
||||||
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
|
||||||
let waypoints: Vec<Waypoint> = source_file
|
|
||||||
.wpt
|
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
.map(|wpt| {
|
||||||
.filter(|wpt| selected(wpt))
|
if copy {
|
||||||
.cloned()
|
copy_waypoint(wpt)
|
||||||
|
} else {
|
||||||
|
wpt.clone()
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect();
|
.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 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();
|
let existing: Vec<WaypointId> = waypoint_ids(file).collect();
|
||||||
insert_waypoints_at(file, place.position(&existing), waypoints);
|
insert_waypoints_at(file, place.position(&existing), waypoints);
|
||||||
Ok(Selection::Waypoint {
|
Ok(Selection::Waypoint {
|
||||||
file_id: to,
|
file_id,
|
||||||
wpt_ids: ids,
|
wpt_ids: ids,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,9 @@ impl Engine {
|
|||||||
|
|
||||||
/// Puts the selection in the clipboard, if there is one.
|
/// Puts the selection in the clipboard, if there is one.
|
||||||
fn copy(&mut self, cut: bool) {
|
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);
|
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
|
// files coming back after an undo/redo are added at the end of the order
|
||||||
self.order.sync(files);
|
self.order.sync(files);
|
||||||
self.selection.retain_existing(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 => {
|
None => {
|
||||||
self.order.0.clear();
|
self.order.0.clear();
|
||||||
self.selection = Selection::Empty;
|
self.selection = Selection::Empty;
|
||||||
self.clipboard = None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.statistics_cache.update(current);
|
self.statistics_cache.update(current);
|
||||||
@@ -233,7 +227,8 @@ mod tests {
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::{
|
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::*;
|
use super::*;
|
||||||
@@ -902,8 +897,8 @@ mod tests {
|
|||||||
assert!(engine.execute(Action::Copy));
|
assert!(engine.execute(Action::Copy));
|
||||||
assert!(engine.clipboard_changed() && !engine.selection_changed());
|
assert!(engine.clipboard_changed() && !engine.selection_changed());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
engine.clipboard().map(|c| (c.cut, &c.selection)),
|
engine.clipboard().map(|c| (c.content.ids(), c.cut)),
|
||||||
Some((false, &selected_tracks))
|
Some((ClipboardIds::Tracks(tracks.clone()), false))
|
||||||
);
|
);
|
||||||
// tracks cannot be pasted onto themselves... but into a track, a file or the list of files
|
// tracks cannot be pasted onto themselves... but into a track, a file or the list of files
|
||||||
assert!(engine.can_paste());
|
assert!(engine.can_paste());
|
||||||
@@ -921,15 +916,20 @@ mod tests {
|
|||||||
assert!(paste(&mut engine));
|
assert!(paste(&mut engine));
|
||||||
assert_eq!(track_ids(&engine, a), tracks);
|
assert_eq!(track_ids(&engine, a), tracks);
|
||||||
assert_eq!(track_ids(&engine, b).len(), 2);
|
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!(
|
assert!(
|
||||||
matches!(engine.selection(), Selection::Track { file_id, trk_ids } if *file_id == b && trk_ids.len() == 2)
|
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!(engine.can_paste());
|
||||||
assert!(!paste(&mut engine));
|
assert!(paste(&mut engine));
|
||||||
|
assert_eq!(track_ids(&engine, b).len(), 4);
|
||||||
|
assert_eq!(track_ids(&engine, a), tracks);
|
||||||
|
|
||||||
// and it can be undone
|
// and it can be undone
|
||||||
assert!(engine.execute(Action::Undo));
|
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());
|
assert!(track_ids(&engine, b).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,7 +962,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 mut engine = Engine::default();
|
||||||
let a = file_with_tracks(&mut engine, "a", 2);
|
let a = file_with_tracks(&mut engine, "a", 2);
|
||||||
let tracks = track_ids(&engine, a);
|
let tracks = track_ids(&engine, a);
|
||||||
@@ -975,8 +975,10 @@ mod tests {
|
|||||||
SelectMode::Replace,
|
SelectMode::Replace,
|
||||||
);
|
);
|
||||||
engine.execute(Action::Copy);
|
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(
|
select_elements(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
Selection::Track {
|
Selection::Track {
|
||||||
@@ -989,15 +991,23 @@ mod tests {
|
|||||||
&mut engine,
|
&mut engine,
|
||||||
Command::Delete(Delete { whole_files: false })
|
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);
|
select_elements(&mut engine, files(&[a]), SelectMode::Replace);
|
||||||
assert!(edit(&mut engine, Command::DeleteAll(DeleteAll)));
|
assert!(edit(&mut engine, Command::DeleteAll(DeleteAll)));
|
||||||
assert!(engine.clipboard().is_none());
|
assert_eq!(engine.clipboard(), clipboard.as_ref());
|
||||||
assert!(engine.clipboard_changed());
|
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]
|
#[test]
|
||||||
@@ -1010,12 +1020,12 @@ mod tests {
|
|||||||
select_files(&mut engine, &[b]);
|
select_files(&mut engine, &[b]);
|
||||||
assert!(engine.execute(Action::Cut));
|
assert!(engine.execute(Action::Cut));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
engine.clipboard().map(|c| (c.cut, &c.selection)),
|
engine.clipboard().map(|c| (c.content.ids(), c.cut)),
|
||||||
Some((true, &files(&[b])))
|
Some((ClipboardIds::Files(vec![b]), true))
|
||||||
);
|
);
|
||||||
// the same content again: nothing changed
|
// the same elements again are a new copy
|
||||||
assert!(!engine.execute(Action::Cut));
|
assert!(engine.execute(Action::Cut));
|
||||||
assert!(!engine.clipboard_changed());
|
assert!(engine.clipboard_changed());
|
||||||
// only the creations of the files are in the history
|
// only the creations of the files are in the history
|
||||||
assert!(engine.execute(Action::Undo));
|
assert!(engine.execute(Action::Undo));
|
||||||
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
|
use uuid::Uuid;
|
||||||
/// if the files changed in between. Cut elements stay in place until they are pasted.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
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 struct Clipboard {
|
||||||
pub selection: Selection,
|
id: ClipboardId,
|
||||||
|
pub content: ClipboardContent,
|
||||||
pub cut: bool,
|
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 {
|
impl Clipboard {
|
||||||
/// `None` if nothing is selected.
|
/// The clipboard for the selected elements, `None` if nothing is selected (or if what is
|
||||||
pub fn new(selection: Selection, cut: bool) -> Option<Clipboard> {
|
/// selected does not exist).
|
||||||
(selection != Selection::Empty).then_some(Clipboard { selection, cut })
|
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
|
/// 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 |
|
/// | segments | nothing, a file, a track, a segment |
|
||||||
/// | waypoints | a file, the waypoints, a waypoint |
|
/// | waypoints | a file, the waypoints, a waypoint |
|
||||||
pub fn can_paste(&self, target: &Selection) -> bool {
|
pub fn can_paste(&self, target: &Selection) -> bool {
|
||||||
|
use ClipboardContent as C;
|
||||||
use Selection::*;
|
use Selection::*;
|
||||||
matches!(
|
matches!(
|
||||||
(&self.selection, target),
|
(&self.content, target),
|
||||||
(File { .. }, Empty | File { .. })
|
(C::Files(_), Empty | File { .. })
|
||||||
| (Track { .. }, Empty | File { .. } | Track { .. })
|
| (C::Tracks(_), Empty | File { .. } | Track { .. })
|
||||||
| (
|
| (
|
||||||
TrackSegment { .. },
|
C::Segments(_),
|
||||||
Empty | File { .. } | Track { .. } | TrackSegment { .. }
|
Empty | File { .. } | Track { .. } | TrackSegment { .. }
|
||||||
)
|
)
|
||||||
| (
|
| (
|
||||||
Waypoints { .. } | Waypoint { .. },
|
C::Waypoints(_),
|
||||||
File { .. } | Waypoints { .. } | Waypoint { .. }
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::HashSet;
|
use crate::{TrackInfo, WaypointChunk};
|
||||||
|
|
||||||
use crate::{FileId, TrackId, TrackSegmentId, WaypointId};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn file() -> Selection {
|
/// A file with 3 tracks (named "t0", "t1", "t2") of 2 segments and 3 waypoints.
|
||||||
Selection::File {
|
fn file(name: &str) -> File {
|
||||||
file_ids: [FileId::default()].into(),
|
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 {
|
Selection::Track {
|
||||||
file_id: FileId::default(),
|
file_id: file.id,
|
||||||
trk_ids: [TrackId::default()].into(),
|
trk_ids: ids.iter().map(|i| file.trk[*i].id).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn segment() -> Selection {
|
#[test]
|
||||||
Selection::TrackSegment {
|
fn test_the_clipboard_holds_what_is_selected() {
|
||||||
file_id: FileId::default(),
|
let file = file("source");
|
||||||
trk_id: TrackId::default(),
|
let (t0, t2) = (file.trk[0].id, file.trk[2].id);
|
||||||
trkseg_ids: [TrackSegmentId::default()].into(),
|
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 {
|
// tracks, in the order of the file, with the name of their file
|
||||||
Selection::Waypoints {
|
let clipboard =
|
||||||
file_id: FileId::default(),
|
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 {
|
// segments, with the name of their track, or else of their file
|
||||||
Selection::Waypoint {
|
let segments = |trk: usize, ids: &[TrackSegmentId]| Selection::TrackSegment {
|
||||||
file_id: FileId::default(),
|
file_id: file.id,
|
||||||
wpt_ids: HashSet::from([WaypointId::default()]),
|
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]
|
#[test]
|
||||||
fn test_nothing_to_copy() {
|
fn test_nothing_to_copy() {
|
||||||
assert_eq!(Clipboard::new(Selection::Empty, false), None);
|
let file = file("f");
|
||||||
assert!(Clipboard::new(file(), true).is_some_and(|c| c.cut));
|
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]
|
#[test]
|
||||||
fn test_where_things_can_be_pasted() {
|
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 = [
|
let targets = [
|
||||||
Selection::Empty,
|
Selection::Empty,
|
||||||
file(),
|
file_selection.clone(),
|
||||||
track(),
|
track.clone(),
|
||||||
segment(),
|
segment.clone(),
|
||||||
waypoints(),
|
waypoints.clone(),
|
||||||
waypoint(),
|
waypoint.clone(),
|
||||||
];
|
];
|
||||||
let expected = [
|
let expected = [
|
||||||
// nothing, file, track, segment, waypoints, waypoint
|
// nothing, file, track, segment, waypoints, waypoint
|
||||||
(file(), [true, true, false, false, false, false]),
|
(file_selection, [true, true, false, false, false, false]),
|
||||||
(track(), [true, true, true, false, false, false]),
|
(track, [true, true, true, false, false, false]),
|
||||||
(segment(), [true, true, true, true, false, false]),
|
(segment, [true, true, true, true, false, false]),
|
||||||
(waypoints(), [false, true, false, false, true, true]),
|
(waypoints, [false, true, false, false, true, true]),
|
||||||
(waypoint(), [false, true, false, false, true, true]),
|
(waypoint, [false, true, false, false, true, true]),
|
||||||
];
|
];
|
||||||
for (copied, allowed) in expected {
|
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) {
|
for (target, allowed) in targets.iter().zip(allowed) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
clipboard.can_paste(target),
|
clipboard.can_paste(target),
|
||||||
|
|||||||
+17
-5
@@ -372,7 +372,10 @@ pub fn cut() -> bool {
|
|||||||
execute(Action::Cut)
|
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]
|
#[wasm_bindgen]
|
||||||
pub fn paste() -> bool {
|
pub fn paste() -> bool {
|
||||||
edit(Command::Paste(engine::Paste))
|
edit(Command::Paste(engine::Paste))
|
||||||
@@ -610,9 +613,10 @@ export type MoveTarget =
|
|||||||
| { type: 'tracks'; fileId: string; index: number }
|
| { type: 'tracks'; fileId: string; index: number }
|
||||||
| { type: 'segments'; fileId: string; trackId: string; index: number }
|
| { type: 'segments'; fileId: string; trackId: string; index: number }
|
||||||
| { type: 'waypoints'; fileId: 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 {
|
export interface Clipboard {
|
||||||
selection: Selection;
|
type: 'files' | 'tracks' | 'segments' | 'waypoints';
|
||||||
|
ids: string[];
|
||||||
cut: boolean;
|
cut: boolean;
|
||||||
}
|
}
|
||||||
export interface FilesUpdate {
|
export interface FilesUpdate {
|
||||||
@@ -921,13 +925,21 @@ pub fn selection() -> Selection {
|
|||||||
.unchecked_into()
|
.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]
|
#[wasm_bindgen]
|
||||||
pub fn clipboard() -> Clipboard {
|
pub fn clipboard() -> Clipboard {
|
||||||
with_engine(|e| {
|
with_engine(|e| {
|
||||||
e.clipboard().map_or(JsValue::UNDEFINED, |clipboard| {
|
e.clipboard().map_or(JsValue::UNDEFINED, |clipboard| {
|
||||||
let object = Object::new();
|
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);
|
set(&object, "cut", clipboard.cut);
|
||||||
object.into()
|
object.into()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
import { isHidden } from '$lib/file-visibility';
|
import { isHidden } from '$lib/file-visibility';
|
||||||
import {
|
import {
|
||||||
elementId,
|
elementId,
|
||||||
|
isInClipboard,
|
||||||
isSelected,
|
isSelected,
|
||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
selectionSize,
|
selectionSize,
|
||||||
@@ -105,7 +106,7 @@
|
|||||||
|
|
||||||
let hidden = $derived(isHidden(fileState, elementId(node)));
|
let hidden = $derived(isHidden(fileState, elementId(node)));
|
||||||
// cut elements are greyed until they are pasted
|
// cut elements are greyed until they are pasted
|
||||||
let isCut = $derived($clipboard?.cut === true && isSelected($clipboard.selection, node));
|
let isCut = $derived($clipboard?.cut === true && isInClipboard($clipboard, node));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
|||||||
@@ -362,7 +362,11 @@ class Engine {
|
|||||||
* Pastes the clipboard, depending on the selection: the files, tracks and segments become new
|
* Pastes the clipboard, depending on the selection: the files, tracks and segments become new
|
||||||
* files when nothing is selected, tracks and waypoints are added to a selected file,
|
* files when nothing is selected, tracks and waypoints are added to a selected file,
|
||||||
* segments to a selected track, and pasted after a selected track, segment or waypoint of the
|
* segments to a selected track, and pasted after a selected track, segment or waypoint of the
|
||||||
* same kind. The pasted elements are selected and the clipboard is emptied.
|
* same kind. The pasted elements are selected.
|
||||||
|
*
|
||||||
|
* 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 copied after that.
|
||||||
*/
|
*/
|
||||||
paste() {
|
paste() {
|
||||||
return this.run((w) => w.paste());
|
return this.run((w) => w.paste());
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import type { FileStructure, Selection } from 'gpx-rs';
|
import type { Clipboard, FileStructure, Selection } from 'gpx-rs';
|
||||||
import type { VisibilityState } from './file-visibility';
|
import type { VisibilityState } from './file-visibility';
|
||||||
import {
|
import {
|
||||||
elementId,
|
elementId,
|
||||||
hasSelectionWithin,
|
hasSelectionWithin,
|
||||||
isCovered,
|
isCovered,
|
||||||
|
isInClipboard,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
isSelected,
|
isSelected,
|
||||||
isSelectionHidden,
|
isSelectionHidden,
|
||||||
@@ -75,6 +76,26 @@ describe('selection helpers', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tells which nodes are in the clipboard', () => {
|
||||||
|
const clipboard = (type: Clipboard['type'], ids: string[]): Clipboard => ({
|
||||||
|
type,
|
||||||
|
ids,
|
||||||
|
cut: true,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
matching(empty, (_, node) => isInClipboard(undefined, node)),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const inClipboard = (c: Clipboard) =>
|
||||||
|
Object.keys(nodes).filter((name) => isInClipboard(c, nodes[name]));
|
||||||
|
assert.deepEqual(inClipboard(clipboard('files', ['F2'])), ['F2']);
|
||||||
|
assert.deepEqual(inClipboard(clipboard('tracks', ['T2', 'S1'])), ['T2']);
|
||||||
|
assert.deepEqual(inClipboard(clipboard('segments', ['S1', 'S3'])), ['S1', 'S3']);
|
||||||
|
assert.deepEqual(inClipboard(clipboard('waypoints', ['W2'])), ['W2']);
|
||||||
|
// not mixed up with the other kinds of elements
|
||||||
|
assert.deepEqual(inClipboard(clipboard('files', ['T1', 'S1', 'W1'])), []);
|
||||||
|
});
|
||||||
|
|
||||||
it('counts the selected elements', () => {
|
it('counts the selected elements', () => {
|
||||||
assert.equal(selectionSize(empty), 0);
|
assert.equal(selectionSize(empty), 0);
|
||||||
assert.equal(selectionSize(files('F1', 'F2')), 2);
|
assert.equal(selectionSize(files('F1', 'F2')), 2);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Selection } from 'gpx-rs';
|
import type { Clipboard, Selection } from 'gpx-rs';
|
||||||
import { isHidden, waypointsKey, type VisibilityState } from '$lib/file-visibility';
|
import { isHidden, waypointsKey, type VisibilityState } from '$lib/file-visibility';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +51,25 @@ export function selectedFileIds(selection: Selection): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the node is one of the elements in the clipboard. */
|
||||||
|
export function isInClipboard(clipboard: Clipboard | undefined, node: FileTreeNode): boolean {
|
||||||
|
if (!clipboard) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (node.type) {
|
||||||
|
case 'file':
|
||||||
|
return clipboard.type === 'files' && clipboard.ids.includes(node.fileId);
|
||||||
|
case 'track':
|
||||||
|
return clipboard.type === 'tracks' && clipboard.ids.includes(node.trackId);
|
||||||
|
case 'segment':
|
||||||
|
return clipboard.type === 'segments' && clipboard.ids.includes(node.segmentId);
|
||||||
|
case 'waypoint':
|
||||||
|
return clipboard.type === 'waypoints' && clipboard.ids.includes(node.waypointId);
|
||||||
|
case 'waypoints':
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Number of selected elements (the waypoints node counts for one). */
|
/** Number of selected elements (the waypoints node counts for one). */
|
||||||
export function selectionSize(selection: Selection): number {
|
export function selectionSize(selection: Selection): number {
|
||||||
switch (selection.type) {
|
switch (selection.type) {
|
||||||
|
|||||||
Reference in New Issue
Block a user