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,6 @@
|
||||
use crate::{Apply, CommandError, State};
|
||||
use crate::{
|
||||
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata,
|
||||
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata, Move,
|
||||
MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, SpliceTrackpoints,
|
||||
Split, Style, Time,
|
||||
};
|
||||
@@ -17,6 +17,7 @@ pub enum Command<'a> {
|
||||
Duplicate(Duplicate),
|
||||
Delete(Delete),
|
||||
Paste(Paste),
|
||||
Move(Move),
|
||||
DeleteAll(DeleteAll),
|
||||
Metadata(Metadata<'a>),
|
||||
Style(Style<'a>),
|
||||
@@ -43,6 +44,7 @@ impl Apply for Command<'_> {
|
||||
Command::Duplicate(c) => c.apply(state),
|
||||
Command::Delete(c) => c.apply(state),
|
||||
Command::Paste(c) => c.apply(state),
|
||||
Command::Move(c) => c.apply(state),
|
||||
Command::DeleteAll(c) => c.apply(state),
|
||||
Command::Metadata(c) => c.apply(state),
|
||||
Command::Style(c) => c.apply(state),
|
||||
|
||||
@@ -2,12 +2,16 @@ mod delete;
|
||||
mod delete_all;
|
||||
mod duplicate;
|
||||
mod load;
|
||||
mod move_elements;
|
||||
mod new;
|
||||
mod paste;
|
||||
mod transfer;
|
||||
|
||||
pub use delete::*;
|
||||
pub use delete_all::*;
|
||||
pub use duplicate::*;
|
||||
pub use load::*;
|
||||
pub use move_elements::*;
|
||||
pub use new::*;
|
||||
pub use paste::*;
|
||||
pub use transfer::*;
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, Place, SegmentsTo, Selection, State, TrackId, TracksTo,
|
||||
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
|
||||
/// counted among the elements of the list that are not moved, like the position of a dragged
|
||||
/// element in the list it is dropped in.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MoveTarget {
|
||||
/// The list of files.
|
||||
Files { index: usize },
|
||||
/// The tracks of a file.
|
||||
Tracks { file_id: FileId, index: usize },
|
||||
/// The segments of a track.
|
||||
Segments {
|
||||
file_id: FileId,
|
||||
trk_id: TrackId,
|
||||
index: usize,
|
||||
},
|
||||
/// The waypoints of a file.
|
||||
Waypoints { file_id: FileId, index: usize },
|
||||
}
|
||||
|
||||
/// Moves elements to a place of the file tree (what drag and drop does), with their ids. The
|
||||
/// moved elements are selected.
|
||||
///
|
||||
/// What can go where:
|
||||
/// - files: among the files;
|
||||
/// - tracks: among the files (each track becomes a file), or among the tracks of a file;
|
||||
/// - segments: among the files (each segment becomes a file), among the tracks of a file (each
|
||||
/// segment becomes a track), or among the segments of a track;
|
||||
/// - waypoints, or all the waypoints of a file (the waypoints node): among the waypoints of a
|
||||
/// file.
|
||||
#[derive(Debug)]
|
||||
pub struct Move {
|
||||
pub what: Selection,
|
||||
pub to: MoveTarget,
|
||||
}
|
||||
|
||||
impl 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;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
File, Track, TrackInfo, TrackSegment, TrackSegmentId, Waypoint, WaypointChunk, WaypointId,
|
||||
engine::command::fixture::Fixture, new_file, waypoint_ids,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A file with the given tracks (name, number of segments) and number of waypoints.
|
||||
fn file(name: &str, tracks: &[(&str, usize)], waypoints: usize) -> File {
|
||||
let mut file = new_file(
|
||||
name,
|
||||
tracks
|
||||
.iter()
|
||||
.map(|(name, segments)| Track {
|
||||
info: TrackInfo {
|
||||
name: Some((*name).to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: (0..*segments).map(|_| TrackSegment::default()).collect(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if waypoints > 0 {
|
||||
file.wpt = vec![Rc::new(WaypointChunk {
|
||||
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
||||
..Default::default()
|
||||
})];
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn add(fx: &mut Fixture, file: File) -> FileId {
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn track_ids(fx: &Fixture, id: FileId) -> Vec<TrackId> {
|
||||
fx.files[&id].trk.iter().map(|trk| trk.id).collect()
|
||||
}
|
||||
|
||||
fn segment_ids(fx: &Fixture, id: FileId, trk: usize) -> Vec<TrackSegmentId> {
|
||||
fx.files[&id].trk[trk].trkseg.iter().map(|s| s.id).collect()
|
||||
}
|
||||
|
||||
fn wpt_ids(fx: &Fixture, id: FileId) -> Vec<WaypointId> {
|
||||
waypoint_ids(&fx.files[&id]).collect()
|
||||
}
|
||||
|
||||
fn do_move(fx: &mut Fixture, what: Selection, to: MoveTarget) -> Result<(), CommandError> {
|
||||
Move { what, to }.apply(&mut fx.state())
|
||||
}
|
||||
|
||||
fn tracks(file_id: FileId, ids: &[TrackId]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn segments(file_id: FileId, trk_id: TrackId, ids: &[TrackSegmentId]) -> Selection {
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoints(file_id: FileId, ids: &[WaypointId]) -> Selection {
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn files(ids: &[FileId]) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reorder_tracks_in_a_file() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[("0", 1), ("1", 1), ("2", 1), ("3", 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
(fx, a, t)
|
||||
};
|
||||
let at = |a, index| MoveTarget::Tracks { file_id: a, index };
|
||||
|
||||
// the position counts the tracks that stay
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[0]]), at(a, 1)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[0], t[2], t[3]]);
|
||||
assert_eq!(fx.selection, tracks(a, &[t[0]]));
|
||||
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[3]]), at(a, 0)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[3], t[0], t[1], t[2]]);
|
||||
|
||||
// several at once, as a block, in their order; past the end is the end
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[1], t[0]]), at(a, 99)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[2], t[3], t[0], t[1]]);
|
||||
assert_eq!(fx.selection, tracks(a, &[t[0], t[1]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_tracks_to_another_file_or_to_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t0", 1), ("t1", 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[("b0", 1), ("b1", 1)], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
let b_tracks = track_ids(&fx, b);
|
||||
|
||||
do_move(
|
||||
&mut fx,
|
||||
tracks(a, &[t[0]]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: b,
|
||||
index: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![b_tracks[0], t[0], b_tracks[1]]);
|
||||
assert_eq!(fx.selection, tracks(b, &[t[0]]));
|
||||
|
||||
// as a new file, at a position of the list of files
|
||||
do_move(&mut fx, tracks(a, &[t[1]]), MoveTarget::Files { index: 1 }).unwrap();
|
||||
assert!(track_ids(&fx, a).is_empty());
|
||||
assert_eq!(fx.order.0.len(), 4);
|
||||
let new = fx.order.0[1];
|
||||
assert_eq!(fx.order.0[0], a);
|
||||
assert_eq!(fx.order.0[2..], [b, c]);
|
||||
assert_eq!(track_ids(&fx, new), vec![t[1]]);
|
||||
assert_eq!(fx.files[&new].info.name, "t1");
|
||||
assert_eq!(fx.selection, files(&[new]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_segments() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t0", 3), ("t1", 1)], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
(fx, a, t)
|
||||
};
|
||||
|
||||
// within a track
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[2]]),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[0],
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2], s[0], s[1]]);
|
||||
assert_eq!(fx.selection, segments(a, t[0], &[s[2]]));
|
||||
|
||||
// to another track
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let other = segment_ids(&fx, a, 1);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[0], s[1]]),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[1],
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2]]);
|
||||
assert_eq!(segment_ids(&fx, a, 1), vec![s[0], s[1], other[0]]);
|
||||
|
||||
// as new tracks of a file
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[1]]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fx.files[&a].trk.len(), 3);
|
||||
assert_eq!(segment_ids(&fx, a, 1), vec![s[1]]);
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[0], s[2]]);
|
||||
assert_eq!(fx.files[&a].trk[0].id, t[0]);
|
||||
assert_eq!(fx.files[&a].trk[2].id, t[1]);
|
||||
|
||||
// as new files
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[0], s[1]]),
|
||||
MoveTarget::Files { index: 0 },
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0.len(), 3);
|
||||
assert_eq!(fx.order.0[2], a);
|
||||
assert_eq!(fx.files[&fx.order.0[0]].info.name, "t0");
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_waypoints() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 4));
|
||||
let b = add(&mut fx, file("b", &[], 2));
|
||||
(fx, a, b)
|
||||
};
|
||||
let at = |file_id, index| MoveTarget::Waypoints { file_id, index };
|
||||
|
||||
// within a file
|
||||
let (mut fx, a, _) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
do_move(&mut fx, waypoints(a, &[w[3]]), at(a, 1)).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), vec![w[0], w[3], w[1], w[2]]);
|
||||
assert_eq!(fx.selection, waypoints(a, &[w[3]]));
|
||||
|
||||
// to the start of another file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
do_move(&mut fx, waypoints(a, &[w[0], w[1]]), at(b, 0)).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w[2..].to_vec());
|
||||
assert_eq!(wpt_ids(&fx, b), [&w[..2], &before[..]].concat());
|
||||
|
||||
// all the waypoints of a file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
do_move(&mut fx, Selection::Waypoints { file_id: a }, at(b, 1)).unwrap();
|
||||
assert!(wpt_ids(&fx, a).is_empty());
|
||||
assert_eq!(
|
||||
wpt_ids(&fx, b),
|
||||
[&before[..1], &w[..], &before[1..]].concat()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
do_move(&mut fx, files(&[c]), MoveTarget::Files { index: 0 }).unwrap();
|
||||
assert_eq!(fx.order.0, vec![c, a, b]);
|
||||
do_move(&mut fx, files(&[c, a]), MoveTarget::Files { index: 1 }).unwrap();
|
||||
assert_eq!(fx.order.0, vec![b, c, a]);
|
||||
assert_eq!(fx.selection, files(&[c, a]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_what_cannot_be_moved() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t", 1)], 1));
|
||||
let t = track_ids(&fx, a);
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = fx.files[&a].clone();
|
||||
let selection = fx.selection.clone();
|
||||
|
||||
let invalid = [
|
||||
// the wrong kind of list
|
||||
(
|
||||
files(&[a]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[0],
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
segments(a, t[0], &s),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
waypoints(a, &w),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(waypoints(a, &w), MoveTarget::Files { index: 0 }),
|
||||
(
|
||||
Selection::Waypoints { file_id: a },
|
||||
MoveTarget::Files { index: 0 },
|
||||
),
|
||||
(Selection::Empty, MoveTarget::Files { index: 0 }),
|
||||
// elements that do not exist
|
||||
(
|
||||
tracks(a, &[TrackId::default()]),
|
||||
MoveTarget::Files { index: 0 },
|
||||
),
|
||||
(
|
||||
waypoints(a, &[WaypointId::default()]),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(files(&[FileId::default()]), MoveTarget::Files { index: 0 }),
|
||||
// a place that does not exist
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Tracks {
|
||||
file_id: FileId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
segments(a, t[0], &s),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: TrackId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
waypoints(a, &w),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: FileId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
];
|
||||
for (what, to) in invalid {
|
||||
assert_eq!(
|
||||
do_move(&mut fx, what.clone(), to),
|
||||
Err(CommandError::NothingToDo),
|
||||
"{what:?} to {to:?}"
|
||||
);
|
||||
// nothing changed
|
||||
assert!(Rc::ptr_eq(&fx.files[&a], &before));
|
||||
assert_eq!(fx.order.0, vec![a]);
|
||||
assert_eq!(fx.selection, selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::{collections::HashSet, hash::Hash, rc::Rc};
|
||||
use std::{collections::HashSet, hash::Hash};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, FileId, Selection, State, Track, TrackId, TrackSegment,
|
||||
TrackSegmentId, Waypoint, WaypointId, copy_file, copy_segment, copy_track, copy_waypoint,
|
||||
edit_waypoint_chunks, insert_waypoints,
|
||||
Apply, CommandError, Place, SegmentsTo, Selection, State, TracksTo, transfer_files,
|
||||
transfer_segments, transfer_tracks, transfer_waypoints, waypoint_ids,
|
||||
};
|
||||
|
||||
/// Pastes what was copied or cut (see [`crate::Clipboard`]), and empties the clipboard.
|
||||
@@ -31,16 +30,83 @@ impl Apply for Paste {
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let cut = clipboard.cut;
|
||||
let next = match &clipboard.selection {
|
||||
Selection::File { file_ids } => paste_files(state, file_ids, cut)?,
|
||||
Selection::Track { file_id, trk_ids } => paste_tracks(state, *file_id, trk_ids, cut)?,
|
||||
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)?
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => paste_segments(state, *file_id, *trk_id, trkseg_ids, cut)?,
|
||||
Selection::Waypoints { file_id } => paste_waypoints(state, *file_id, None, cut)?,
|
||||
Selection::Waypoint { file_id, wpt_ids } => {
|
||||
paste_waypoints(state, *file_id, Some(wpt_ids), cut)?
|
||||
} => {
|
||||
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),
|
||||
};
|
||||
@@ -58,382 +124,14 @@ fn last_selected<T: Copy + Eq + Hash>(
|
||||
all.filter(|id| selected.contains(id)).last()
|
||||
}
|
||||
|
||||
fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
|
||||
file.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.map(|wpt| wpt.id)
|
||||
}
|
||||
|
||||
fn file_mut<'a>(state: &'a mut State, id: FileId) -> Result<&'a mut File, CommandError> {
|
||||
state
|
||||
.files
|
||||
.get_mut(&id)
|
||||
.map(Rc::make_mut)
|
||||
.ok_or(CommandError::NothingToDo)
|
||||
}
|
||||
|
||||
/// Adds the file to the files, at the end of the order.
|
||||
fn add_file(state: &mut State, file: File) -> FileId {
|
||||
let id = file.id;
|
||||
state.files.insert(id, Rc::new(file));
|
||||
state.order.0.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn new_file(name: &str, trk: Vec<Track>) -> File {
|
||||
let mut file = File {
|
||||
trk,
|
||||
..Default::default()
|
||||
};
|
||||
file.info.name = name.to_owned();
|
||||
file
|
||||
}
|
||||
|
||||
fn paste_files(
|
||||
state: &mut State,
|
||||
source: &HashSet<FileId>,
|
||||
cut: bool,
|
||||
) -> 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);
|
||||
}
|
||||
|
||||
let pasted = if cut {
|
||||
// moved to the end of the list
|
||||
state.order.move_files(&ids, usize::MAX);
|
||||
ids
|
||||
} else {
|
||||
let copies: Vec<File> = ids.iter().map(|id| copy_file(&state.files[id])).collect();
|
||||
copies
|
||||
.into_iter()
|
||||
.map(|file| add_file(state, file))
|
||||
.collect()
|
||||
};
|
||||
Ok(Selection::File {
|
||||
file_ids: pasted.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn paste_tracks(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
source: &HashSet<TrackId>,
|
||||
cut: bool,
|
||||
) -> 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);
|
||||
}
|
||||
|
||||
// where to paste, checked before anything is changed
|
||||
enum To {
|
||||
Files,
|
||||
End(FileId),
|
||||
After(FileId, TrackId),
|
||||
}
|
||||
let to = match &*state.selection {
|
||||
Selection::Empty => To::Files,
|
||||
Selection::File { file_ids } => To::End(
|
||||
last_selected(state.order.0.iter().copied(), file_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
|
||||
To::After(
|
||||
*file_id,
|
||||
last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
)
|
||||
}
|
||||
_ => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
if let To::End(id) | To::After(id, _) = &to
|
||||
&& !state.files.contains_key(id)
|
||||
{
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
if cut {
|
||||
file_mut(state, file_id)?
|
||||
.trk
|
||||
.retain(|trk| !source.contains(&trk.id));
|
||||
}
|
||||
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();
|
||||
|
||||
Ok(match to {
|
||||
To::Files => {
|
||||
let files = tracks
|
||||
.into_iter()
|
||||
.map(|track| {
|
||||
let name = track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| source_name.clone());
|
||||
new_file(&name, vec![track])
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Selection::File {
|
||||
file_ids: files
|
||||
.into_iter()
|
||||
.map(|file| add_file(state, file))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
To::End(id) => {
|
||||
file_mut(state, id)?.trk.extend(tracks);
|
||||
Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: ids,
|
||||
}
|
||||
}
|
||||
To::After(id, after) => {
|
||||
let file = file_mut(state, id)?;
|
||||
// the track may have just been cut away
|
||||
let at = file
|
||||
.trk
|
||||
.iter()
|
||||
.position(|trk| trk.id == after)
|
||||
.map_or(file.trk.len(), |i| i + 1);
|
||||
file.trk.splice(at..at, tracks);
|
||||
Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: ids,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn paste_segments(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
trk_id: TrackId,
|
||||
source: &HashSet<TrackSegmentId>,
|
||||
cut: bool,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let source_track = source_file
|
||||
.trk
|
||||
.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()
|
||||
.collect();
|
||||
if segments.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
enum To {
|
||||
Files,
|
||||
Tracks(FileId),
|
||||
End(FileId, TrackId),
|
||||
After(FileId, TrackId, TrackSegmentId),
|
||||
}
|
||||
let to = match &*state.selection {
|
||||
Selection::Empty => To::Files,
|
||||
Selection::File { file_ids } => To::Tracks(
|
||||
last_selected(state.order.0.iter().copied(), file_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
|
||||
To::End(
|
||||
*file_id,
|
||||
last_selected(file.trk.iter().map(|trk| trk.id), trk_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
)
|
||||
}
|
||||
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)?;
|
||||
To::After(
|
||||
*file_id,
|
||||
*trk_id,
|
||||
last_selected(trk.trkseg.iter().map(|seg| seg.id), trkseg_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
)
|
||||
}
|
||||
_ => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
if let To::Tracks(id) = &to
|
||||
&& !state.files.contains_key(id)
|
||||
{
|
||||
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()
|
||||
};
|
||||
let in_track = |state: &mut State,
|
||||
file_id: FileId,
|
||||
trk_id: TrackId,
|
||||
after: Option<TrackSegmentId>,
|
||||
segments: Vec<TrackSegment>|
|
||||
-> Result<(), CommandError> {
|
||||
let trk = file_mut(state, file_id)?
|
||||
.trk
|
||||
.iter_mut()
|
||||
.find(|trk| trk.id == trk_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
// the segment may have just been cut away
|
||||
let at = after
|
||||
.and_then(|after| trk.trkseg.iter().position(|seg| seg.id == after))
|
||||
.map_or(trk.trkseg.len(), |i| i + 1);
|
||||
trk.trkseg.splice(at..at, segments);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
Ok(match to {
|
||||
To::Files => {
|
||||
let files = segments
|
||||
.into_iter()
|
||||
.map(|segment| new_file(&source_name, vec![own_track(segment)]))
|
||||
.collect::<Vec<_>>();
|
||||
Selection::File {
|
||||
file_ids: files
|
||||
.into_iter()
|
||||
.map(|file| add_file(state, file))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
To::Tracks(id) => {
|
||||
let tracks: Vec<Track> = segments.into_iter().map(own_track).collect();
|
||||
let trk_ids = tracks.iter().map(|trk| trk.id).collect();
|
||||
file_mut(state, id)?.trk.extend(tracks);
|
||||
Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids,
|
||||
}
|
||||
}
|
||||
To::End(file_id, trk_id) => {
|
||||
in_track(state, file_id, trk_id, None, segments)?;
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids,
|
||||
}
|
||||
}
|
||||
To::After(file_id, trk_id, after) => {
|
||||
in_track(state, file_id, trk_id, Some(after), segments)?;
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn paste_waypoints(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
source: Option<&HashSet<WaypointId>>,
|
||||
cut: bool,
|
||||
) -> Result<Selection, CommandError> {
|
||||
// all the waypoints of the file when no waypoints are given
|
||||
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
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.filter(|wpt| selected(wpt))
|
||||
.cloned()
|
||||
.collect();
|
||||
if waypoints.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
let (to_file, after) = match &*state.selection {
|
||||
Selection::File { file_ids } => (
|
||||
last_selected(state.order.0.iter().copied(), file_ids)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
None,
|
||||
),
|
||||
Selection::Waypoints { file_id } => (*file_id, None),
|
||||
Selection::Waypoint { file_id, wpt_ids } => {
|
||||
let file = state.files.get(file_id).ok_or(CommandError::NothingToDo)?;
|
||||
(*file_id, last_selected(waypoint_ids(file), wpt_ids))
|
||||
}
|
||||
_ => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
if !state.files.contains_key(&to_file) {
|
||||
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();
|
||||
insert_waypoints(file_mut(state, to_file)?, after, waypoints);
|
||||
Ok(Selection::Waypoint {
|
||||
file_id: to_file,
|
||||
wpt_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Clipboard, TrackInfo, engine::command::fixture::Fixture};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Clipboard, File, FileId, Track, TrackId, TrackInfo, TrackSegment, TrackSegmentId, Waypoint,
|
||||
WaypointId, engine::command::fixture::Fixture, new_file,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
//! 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.
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
/// Where elements are put, among the others of the same list.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Place<T> {
|
||||
/// After all of them.
|
||||
End,
|
||||
/// Right after the one with this id, or at the end if it is not there (anymore).
|
||||
After(T),
|
||||
/// At this position (at the end if it is past it).
|
||||
Index(usize),
|
||||
}
|
||||
|
||||
impl<T: Copy + PartialEq> Place<T> {
|
||||
/// The position in the list of `ids`.
|
||||
pub fn position(&self, ids: &[T]) -> usize {
|
||||
match self {
|
||||
Place::End => ids.len(),
|
||||
Place::After(id) => ids
|
||||
.iter()
|
||||
.position(|other| other == id)
|
||||
.map_or(ids.len(), |i| i + 1),
|
||||
Place::Index(index) => (*index).min(ids.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
|
||||
file.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.map(|wpt| wpt.id)
|
||||
}
|
||||
|
||||
fn file_mut<'a>(state: &'a mut State, id: FileId) -> Result<&'a mut File, CommandError> {
|
||||
state
|
||||
.files
|
||||
.get_mut(&id)
|
||||
.map(Rc::make_mut)
|
||||
.ok_or(CommandError::NothingToDo)
|
||||
}
|
||||
|
||||
pub fn new_file(name: &str, trk: Vec<Track>) -> File {
|
||||
let mut file = File {
|
||||
trk,
|
||||
..Default::default()
|
||||
};
|
||||
file.info.name = name.to_owned();
|
||||
file
|
||||
}
|
||||
|
||||
/// Adds the files to the state, at `place` in the order of the files.
|
||||
fn add_files(state: &mut State, files: Vec<File>, place: Place<FileId>) -> HashSet<FileId> {
|
||||
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.order.0.splice(at..at, ids.iter().copied());
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
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)
|
||||
{
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
if cut {
|
||||
file_mut(state, file_id)?
|
||||
.trk
|
||||
.retain(|trk| !source.contains(&trk.id));
|
||||
}
|
||||
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();
|
||||
|
||||
Ok(match to {
|
||||
TracksTo::Files(place) => {
|
||||
let files = tracks
|
||||
.into_iter()
|
||||
.map(|track| {
|
||||
let name = track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| source_name.clone());
|
||||
new_file(&name, vec![track])
|
||||
})
|
||||
.collect();
|
||||
Selection::File {
|
||||
file_ids: add_files(state, files, place),
|
||||
}
|
||||
}
|
||||
TracksTo::File(id, place) => {
|
||||
let file = file_mut(state, 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: ids,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 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(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
trk_id: TrackId,
|
||||
source: &HashSet<TrackSegmentId>,
|
||||
cut: bool,
|
||||
to: SegmentsTo,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let source_file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let source_track = source_file
|
||||
.trk
|
||||
.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()
|
||||
.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) => {
|
||||
let files = segments
|
||||
.into_iter()
|
||||
.map(|segment| new_file(&source_name, vec![own_track(segment)]))
|
||||
.collect();
|
||||
Selection::File {
|
||||
file_ids: 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)?;
|
||||
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,
|
||||
}
|
||||
}
|
||||
SegmentsTo::Track(id, trk_id, place) => {
|
||||
let trk = file_mut(state, id)?
|
||||
.trk
|
||||
.iter_mut()
|
||||
.find(|trk| trk.id == trk_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let existing: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
|
||||
let at = place.position(&existing);
|
||||
trk.trkseg.splice(at..at, segments);
|
||||
Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id,
|
||||
trkseg_ids: ids,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 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(
|
||||
state: &mut State,
|
||||
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
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.filter(|wpt| selected(wpt))
|
||||
.cloned()
|
||||
.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 existing: Vec<WaypointId> = waypoint_ids(file).collect();
|
||||
insert_waypoints_at(file, place.position(&existing), waypoints);
|
||||
Ok(Selection::Waypoint {
|
||||
file_id: to,
|
||||
wpt_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_place_position() {
|
||||
let ids = [1, 2, 3];
|
||||
assert_eq!(Place::End.position(&ids), 3);
|
||||
assert_eq!(Place::After(1).position(&ids), 1);
|
||||
assert_eq!(Place::After(3).position(&ids), 3);
|
||||
// not there: at the end
|
||||
assert_eq!(Place::After(9).position(&ids), 3);
|
||||
assert_eq!(Place::Index(0).position(&ids), 0);
|
||||
assert_eq!(Place::Index(2).position(&ids), 2);
|
||||
assert_eq!(Place::Index(10).position(&ids), 3);
|
||||
assert_eq!(Place::<i32>::End.position(&[]), 0);
|
||||
assert_eq!(Place::<i32>::Index(4).position(&[]), 0);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,25 @@ use crate::{File, Waypoint, WaypointChunk, WaypointId};
|
||||
/// Inserts waypoints in the file, right after the waypoint `after`, or at the end if there is
|
||||
/// none (or if it is not in the file).
|
||||
///
|
||||
/// Only the chunk holding `after` is cut (when waypoints follow it), all the other chunks are
|
||||
/// kept as they are.
|
||||
/// See [`insert_waypoints_at`].
|
||||
pub fn insert_waypoints(file: &mut File, after: Option<WaypointId>, waypoints: Vec<Waypoint>) {
|
||||
let index = after
|
||||
.and_then(|after| {
|
||||
file.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.position(|wpt| wpt.id == after)
|
||||
})
|
||||
.map_or(usize::MAX, |i| i + 1);
|
||||
insert_waypoints_at(file, index, waypoints);
|
||||
}
|
||||
|
||||
/// Inserts waypoints in the file, so that the first one is at `index` among the waypoints of
|
||||
/// the file (at the end if the index is past it).
|
||||
///
|
||||
/// Only the chunk holding the waypoint that was at `index` is cut (when the waypoints are not
|
||||
/// inserted between two chunks), all the other chunks are kept as they are.
|
||||
pub fn insert_waypoints_at(file: &mut File, index: usize, waypoints: Vec<Waypoint>) {
|
||||
if waypoints.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -24,36 +40,32 @@ pub fn insert_waypoints(file: &mut File, after: Option<WaypointId>, waypoints: V
|
||||
inserted.push(Rc::new(chunk));
|
||||
}
|
||||
|
||||
let position = after.and_then(|after| {
|
||||
file.wpt.iter().enumerate().find_map(|(i, chunk)| {
|
||||
chunk
|
||||
.wpt
|
||||
.iter()
|
||||
.position(|wpt| wpt.id == after)
|
||||
.map(|j| (i, j + 1))
|
||||
})
|
||||
// the chunk holding the waypoint at `index`, and the position of that waypoint in it
|
||||
let mut start = 0;
|
||||
let position = file.wpt.iter().enumerate().find_map(|(i, chunk)| {
|
||||
let offset = index
|
||||
.checked_sub(start)
|
||||
.filter(|offset| *offset < chunk.wpt.len());
|
||||
start += chunk.wpt.len();
|
||||
offset.map(|offset| (i, offset))
|
||||
});
|
||||
match position {
|
||||
None => file.wpt.extend(inserted),
|
||||
Some((i, split)) => {
|
||||
Some((i, 0)) => {
|
||||
file.wpt.splice(i..i, inserted);
|
||||
}
|
||||
Some((i, offset)) => {
|
||||
let cut = &file.wpt[i];
|
||||
let replacement = if split == cut.wpt.len() {
|
||||
let mut chunks = vec![cut.clone()];
|
||||
chunks.extend(inserted);
|
||||
chunks
|
||||
} else {
|
||||
let part = |wpt: &[Waypoint]| {
|
||||
Rc::new(WaypointChunk {
|
||||
wpt: wpt.to_vec(),
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let mut chunks = vec![part(&cut.wpt[..split])];
|
||||
chunks.extend(inserted);
|
||||
chunks.push(part(&cut.wpt[split..]));
|
||||
chunks
|
||||
let part = |wpt: &[Waypoint]| {
|
||||
Rc::new(WaypointChunk {
|
||||
wpt: wpt.to_vec(),
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
file.wpt.splice(i..=i, replacement);
|
||||
let mut chunks = vec![part(&cut.wpt[..offset])];
|
||||
chunks.extend(inserted);
|
||||
chunks.push(part(&cut.wpt[offset..]));
|
||||
file.wpt.splice(i..=i, chunks);
|
||||
}
|
||||
}
|
||||
file.wpt_rev_id = Default::default();
|
||||
@@ -155,4 +167,52 @@ mod tests {
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(file.wpt[..2].iter().all(|chunk| chunk.is_full()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_at_an_index() {
|
||||
// at the start
|
||||
let (mut file, before) = file_with(&[2, 1]);
|
||||
let first = file.wpt[0].clone();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, 0, vec![one]);
|
||||
assert_eq!(ids(&file), [vec![one_id], before.clone()].concat());
|
||||
// the existing chunks are kept
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(Rc::ptr_eq(&file.wpt[1], &first));
|
||||
|
||||
// between two chunks
|
||||
let (mut file, before) = file_with(&[2, 1]);
|
||||
let (first, last) = (file.wpt[0].clone(), file.wpt[1].clone());
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, 2, vec![one]);
|
||||
assert_eq!(ids(&file), [&before[..2], &[one_id], &before[2..]].concat());
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(Rc::ptr_eq(&file.wpt[0], &first) && Rc::ptr_eq(&file.wpt[2], &last));
|
||||
|
||||
// inside a chunk
|
||||
let (mut file, before) = file_with(&[3]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, 1, vec![one]);
|
||||
assert_eq!(ids(&file), [&before[..1], &[one_id], &before[1..]].concat());
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
|
||||
// at the end, or past it
|
||||
for index in [3, 100] {
|
||||
let (mut file, before) = file_with(&[3]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, index, vec![one]);
|
||||
assert_eq!(ids(&file), [before, vec![one_id]].concat());
|
||||
}
|
||||
|
||||
// in a file without waypoints
|
||||
let mut file = File::default();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, 0, vec![one]);
|
||||
assert_eq!(ids(&file), vec![one_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,4 +1021,44 @@ mod tests {
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(!engine.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_elements_is_an_edit_and_selects_what_moved() {
|
||||
use crate::{Move, MoveTarget};
|
||||
|
||||
let mut engine = Engine::default();
|
||||
let a = file_with_tracks(&mut engine, "a", 3);
|
||||
let tracks = track_ids(&engine, a);
|
||||
let moved = Selection::Track {
|
||||
file_id: a,
|
||||
trk_ids: [tracks[2]].into(),
|
||||
};
|
||||
assert!(edit(
|
||||
&mut engine,
|
||||
Command::Move(Move {
|
||||
what: moved.clone(),
|
||||
to: MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 0
|
||||
},
|
||||
})
|
||||
));
|
||||
assert_eq!(track_ids(&engine, a), vec![tracks[2], tracks[0], tracks[1]]);
|
||||
assert_eq!(engine.selection(), &moved);
|
||||
|
||||
// invalid moves change nothing
|
||||
assert!(!edit(
|
||||
&mut engine,
|
||||
Command::Move(Move {
|
||||
what: moved,
|
||||
to: MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0
|
||||
},
|
||||
})
|
||||
));
|
||||
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert_eq!(track_ids(&engine, a), tracks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +384,107 @@ pub fn can_paste() -> bool {
|
||||
with_engine(|e| e.can_paste()).unwrap_or(false)
|
||||
}
|
||||
|
||||
// Moving elements (drag and drop)
|
||||
//
|
||||
// Unlike the other functions, the arguments are objects, as they are in the `Selection` and
|
||||
// `MoveTarget` types: it happens once per drop, and they are much clearer than a list of ids.
|
||||
|
||||
fn property(object: &JsValue, key: &str) -> Option<JsValue> {
|
||||
Reflect::get(object, &key.into())
|
||||
.ok()
|
||||
.filter(|value| !value.is_undefined())
|
||||
}
|
||||
|
||||
fn uuid_property(object: &JsValue, key: &str) -> Option<uuid::Uuid> {
|
||||
uuid::Uuid::parse_str(&property(object, key)?.as_string()?).ok()
|
||||
}
|
||||
|
||||
fn uuids_property(object: &JsValue, key: &str) -> Option<Vec<uuid::Uuid>> {
|
||||
Array::from(&property(object, key)?)
|
||||
.iter()
|
||||
.map(|id| uuid::Uuid::parse_str(&id.as_string()?).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The inverse of `selection_object`.
|
||||
fn parse_selection(value: &JsValue) -> Option<engine::Selection> {
|
||||
use engine::Selection as S;
|
||||
let file_id = || uuid_property(value, "fileId").map(FileId);
|
||||
Some(match property(value, "type")?.as_string()?.as_str() {
|
||||
"empty" => S::Empty,
|
||||
"file" => S::File {
|
||||
file_ids: uuids_property(value, "fileIds")?
|
||||
.into_iter()
|
||||
.map(FileId)
|
||||
.collect(),
|
||||
},
|
||||
"track" => S::Track {
|
||||
file_id: file_id()?,
|
||||
trk_ids: uuids_property(value, "trackIds")?
|
||||
.into_iter()
|
||||
.map(engine::TrackId)
|
||||
.collect(),
|
||||
},
|
||||
"segment" => S::TrackSegment {
|
||||
file_id: file_id()?,
|
||||
trk_id: engine::TrackId(uuid_property(value, "trackId")?),
|
||||
trkseg_ids: uuids_property(value, "segmentIds")?
|
||||
.into_iter()
|
||||
.map(engine::TrackSegmentId)
|
||||
.collect(),
|
||||
},
|
||||
"waypoints" => S::Waypoints {
|
||||
file_id: file_id()?,
|
||||
},
|
||||
"waypoint" => S::Waypoint {
|
||||
file_id: file_id()?,
|
||||
wpt_ids: uuids_property(value, "waypointIds")?
|
||||
.into_iter()
|
||||
.map(engine::WaypointId)
|
||||
.collect(),
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_move_target(value: &JsValue) -> Option<engine::MoveTarget> {
|
||||
use engine::MoveTarget as T;
|
||||
// a negative index is the start, an infinite one is the end
|
||||
let index = property(value, "index")?.as_f64()?.max(0.0) as usize;
|
||||
let file_id = || uuid_property(value, "fileId").map(FileId);
|
||||
Some(match property(value, "type")?.as_string()?.as_str() {
|
||||
"files" => T::Files { index },
|
||||
"tracks" => T::Tracks {
|
||||
file_id: file_id()?,
|
||||
index,
|
||||
},
|
||||
"segments" => T::Segments {
|
||||
file_id: file_id()?,
|
||||
trk_id: engine::TrackId(uuid_property(value, "trackId")?),
|
||||
index,
|
||||
},
|
||||
"waypoints" => T::Waypoints {
|
||||
file_id: file_id()?,
|
||||
index,
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves elements (given as a `Selection`) to a place of the file tree, keeping their ids, like
|
||||
/// dragging and dropping them does. The moved elements are selected.
|
||||
///
|
||||
/// Files can only go among the files; tracks among the files (each becomes a file) or the tracks
|
||||
/// of a file; segments among the files (each becomes a file), the tracks of a file (each becomes
|
||||
/// a track) or the segments of a track; waypoints, or all the waypoints of a file, among the
|
||||
/// waypoints of a file.
|
||||
#[wasm_bindgen]
|
||||
pub fn move_elements(what: Selection, to: MoveTarget) -> bool {
|
||||
parse_selection(&what)
|
||||
.zip(parse_move_target(&to))
|
||||
.is_some_and(|(what, to)| edit(Command::Move(engine::Move { what, to })))
|
||||
}
|
||||
|
||||
// Selection
|
||||
|
||||
/// Selects files. `file_ids_bytes`: concatenated 16-byte UUIDs. Unknown files are ignored, and
|
||||
@@ -500,6 +601,15 @@ export type Selection =
|
||||
| { type: 'segment'; fileId: string; trackId: string; segmentIds: string[] }
|
||||
| { type: 'waypoints'; fileId: string }
|
||||
| { type: 'waypoint'; fileId: string; waypointIds: string[] };
|
||||
/**
|
||||
* Where moved elements go: a list of the file tree and the position in it, counted among the
|
||||
* elements of the list that are not moved.
|
||||
*/
|
||||
export type MoveTarget =
|
||||
| { type: 'files'; index: number }
|
||||
| { 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. */
|
||||
export interface Clipboard {
|
||||
selection: Selection;
|
||||
@@ -580,6 +690,8 @@ export interface WaypointNode {
|
||||
extern "C" {
|
||||
#[wasm_bindgen(typescript_type = "Selection")]
|
||||
pub type Selection;
|
||||
#[wasm_bindgen(typescript_type = "MoveTarget")]
|
||||
pub type MoveTarget;
|
||||
#[wasm_bindgen(typescript_type = "Clipboard | undefined")]
|
||||
pub type Clipboard;
|
||||
#[wasm_bindgen(typescript_type = "FilesUpdate")]
|
||||
|
||||
@@ -3,13 +3,8 @@ import Sortable, { type Direction } from 'sortablejs/Sortable';
|
||||
import { ListLevel } from './file-list';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { tick } from 'svelte';
|
||||
import { engine } from '$lib/engine';
|
||||
import { engine, type MoveTarget, type Selection } from '$lib/engine';
|
||||
import { isSelected, type FileTreeNode } from '$lib/selection-helpers';
|
||||
// TODO moving elements between parents is not available in the engine yet
|
||||
// import { ListItem, ListRootItem } from './file-list';
|
||||
// import { getFileIds, moveItems } from '$lib/logic/file-actions';
|
||||
// import { settings } from '$lib/logic/settings';
|
||||
// const { fileOrder } = settings;
|
||||
|
||||
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||
[ListLevel.ROOT]: [],
|
||||
@@ -22,6 +17,9 @@ export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||
|
||||
export const dragging = writable<ListLevel | null>(null);
|
||||
|
||||
/** The list of each container, to find the lists a drop is about from its event. */
|
||||
const lists = new WeakMap<HTMLElement, SortableFileList>();
|
||||
|
||||
/**
|
||||
* Makes a list of the file tree sortable and selectable. The elements of the list are the
|
||||
* children of `parent` (the files when `parent` is null), their ids are in their `data-id`.
|
||||
@@ -33,6 +31,8 @@ export class SortableFileList {
|
||||
private _sortable: Sortable | null = null;
|
||||
private _elements: { [id: string]: HTMLElement } = {};
|
||||
private _updatingSelection: boolean = false;
|
||||
/** The elements of the list when a drag from it started, to put them back when it ends. */
|
||||
private _snapshot: ChildNode[] = [];
|
||||
private _unsubscribes: (() => void)[] = [];
|
||||
|
||||
constructor(
|
||||
@@ -45,16 +45,12 @@ export class SortableFileList {
|
||||
this._parent = parent;
|
||||
this._sortableLevel = sortableLevel;
|
||||
this._container = container;
|
||||
// TODO only the order of the files can be changed for now: the other lists neither sort
|
||||
// nor exchange elements (allowedMoves, to be used again once the engine can move elements)
|
||||
const sortable = sortableLevel === ListLevel.FILE;
|
||||
this._sortable = Sortable.create(container, {
|
||||
group: {
|
||||
name: sortableLevel,
|
||||
pull: sortable ? allowedMoves[sortableLevel] : false,
|
||||
put: sortable ? [ListLevel.FILE] : false,
|
||||
pull: allowedMoves[sortableLevel],
|
||||
put: true,
|
||||
},
|
||||
sort: sortable,
|
||||
direction: orientation,
|
||||
forceAutoScrollFallback: true,
|
||||
multiDrag: true,
|
||||
@@ -64,14 +60,15 @@ export class SortableFileList {
|
||||
setTimeout(() => this.updateToSelection(e), 50),
|
||||
onDeselect: (e: Sortable.SortableEvent) =>
|
||||
setTimeout(() => this.updateToSelection(e), 50),
|
||||
onStart: () => dragging.set(sortableLevel),
|
||||
onStart: () => {
|
||||
// all the nodes, not only the elements: the comments are the anchors of the Svelte blocks
|
||||
this._snapshot = Array.from(container.childNodes);
|
||||
dragging.set(sortableLevel);
|
||||
},
|
||||
onEnd: () => dragging.set(null),
|
||||
onSort: (e: Sortable.SortableEvent) => this.onSort(e),
|
||||
});
|
||||
Object.defineProperty(this._sortable, '_waypointRoot', {
|
||||
value: waypointRoot,
|
||||
writable: true,
|
||||
});
|
||||
lists.set(container, this);
|
||||
|
||||
this._unsubscribes.push(
|
||||
engine.selection.subscribe(() => tick().then(() => this.updateFromSelection()))
|
||||
@@ -109,66 +106,92 @@ export class SortableFileList {
|
||||
}
|
||||
}
|
||||
|
||||
/** The elements of the list in the selection form. */
|
||||
private selectionOf(ids: string[]): Selection | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
return { type: 'file', fileIds: ids };
|
||||
case ListLevel.TRACK:
|
||||
return parent ? { type: 'track', fileId: parent.fileId, trackIds: ids } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
return parent?.type === 'track'
|
||||
? {
|
||||
type: 'segment',
|
||||
fileId: parent.fileId,
|
||||
trackId: parent.trackId,
|
||||
segmentIds: ids,
|
||||
}
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId } : undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
return parent
|
||||
? { type: 'waypoint', fileId: parent.fileId, waypointIds: ids }
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** A position in the list, as a place to move elements to. */
|
||||
private targetAt(index: number): MoveTarget | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
return { type: 'files', index };
|
||||
case ListLevel.TRACK:
|
||||
return parent ? { type: 'tracks', fileId: parent.fileId, index } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
return parent?.type === 'track'
|
||||
? { type: 'segments', fileId: parent.fileId, trackId: parent.trackId, index }
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
// the list holds the node standing for the waypoints: they go after the others
|
||||
return parent
|
||||
? { type: 'waypoints', fileId: parent.fileId, index: Infinity }
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId, index } : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
onSort(e: Sortable.SortableEvent) {
|
||||
this.updateToFileOrder();
|
||||
const from = lists.get(e.from);
|
||||
const to = lists.get(e.to);
|
||||
if (!from || !to) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO moving elements between parents: the engine has no command for it yet, the code
|
||||
// below worked on the previous implementation
|
||||
// onSort(e: Sortable.SortableEvent) {
|
||||
// this.updateToFileOrder();
|
||||
if (from === to && this._sortableLevel === ListLevel.FILE) {
|
||||
// the order of the files is the one of the list
|
||||
this.updateToFileOrder();
|
||||
return;
|
||||
}
|
||||
|
||||
// const from = Sortable.get(e.from);
|
||||
// const to = Sortable.get(e.to);
|
||||
// The event is triggered on the source and on the destination list: handle it once.
|
||||
// Nothing to move in the list of the single node of the waypoints.
|
||||
if (to !== this || (from === to && this._sortableLevel === ListLevel.WAYPOINTS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!from || !to) {
|
||||
// return;
|
||||
// }
|
||||
const elements: HTMLElement[] = e.items.length > 0 ? e.items : [e.item];
|
||||
const ids = elements
|
||||
.map((element) => element.getAttribute('data-id'))
|
||||
.filter((id): id is string => id !== null);
|
||||
const newIndices = (
|
||||
e.newIndicies.length > 0
|
||||
? e.newIndicies.map((i: { index: number }) => i.index)
|
||||
: [e.newIndex]
|
||||
).filter((index: number | undefined): index is number => index !== undefined && index >= 0);
|
||||
const what = from.selectionOf(ids);
|
||||
const target = to.targetAt(newIndices.length > 0 ? Math.min(...newIndices) : Infinity);
|
||||
|
||||
// let fromItem = from._item;
|
||||
// let toItem = to._item;
|
||||
// The list is rendered from the state of the engine: put the elements back where they
|
||||
// were, the engine moves them for real.
|
||||
from._snapshot.forEach((node) => e.from.appendChild(node));
|
||||
|
||||
// if (this._item === toItem && !(fromItem instanceof ListRootItem)) {
|
||||
// // Event is triggered on source and destination list, only handle it once
|
||||
// let fromItems = [];
|
||||
// let toItems = [];
|
||||
|
||||
// if (from._waypointRoot) {
|
||||
// fromItems = [fromItem.extend('waypoints')];
|
||||
// } else {
|
||||
// let oldIndices: number[] =
|
||||
// e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
|
||||
// oldIndices = oldIndices.filter((i) => i >= 0);
|
||||
// oldIndices.sort((a, b) => a - b);
|
||||
|
||||
// fromItems = oldIndices.map((i) => fromItem.extend(i));
|
||||
// }
|
||||
|
||||
// if (from._waypointRoot && to._waypointRoot) {
|
||||
// toItems = [toItem.extend('waypoints')];
|
||||
// } else {
|
||||
// if (to._waypointRoot) {
|
||||
// toItem = toItem.extend('waypoints');
|
||||
// }
|
||||
|
||||
// let newIndices: number[] =
|
||||
// e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
|
||||
// newIndices = newIndices.filter((i) => i >= 0);
|
||||
// newIndices.sort((a, b) => a - b);
|
||||
|
||||
// if (toItem instanceof ListRootItem) {
|
||||
// let newFileIds = getFileIds(newIndices.length);
|
||||
// toItems = newIndices.map((i, index) => {
|
||||
// get(fileOrder).splice(i, 0, newFileIds[index]);
|
||||
// return this._item.extend(newFileIds[index]);
|
||||
// });
|
||||
// } else {
|
||||
// toItems = newIndices.map((i) => toItem.extend(i));
|
||||
// }
|
||||
// }
|
||||
|
||||
// moveItems(fromItem, toItem, fromItems, toItems);
|
||||
// }
|
||||
// }
|
||||
if (what && target) {
|
||||
engine.move(what, target);
|
||||
}
|
||||
}
|
||||
|
||||
updateFromSelection() {
|
||||
|
||||
@@ -4,10 +4,11 @@ import { FileColorAllocator, normalizeColor } from '$lib/file-colors';
|
||||
import { setHidden, type Visibility } from '$lib/file-visibility';
|
||||
import { selectedElementIds, type FileTreeNode } from '$lib/selection-helpers';
|
||||
import type { Feature, FeatureCollection, LineString, Point } from 'geojson';
|
||||
import type { Clipboard, GlobalStatistics, FileStructure, Selection } from 'gpx-rs';
|
||||
import type { Clipboard, GlobalStatistics, FileStructure, MoveTarget, Selection } from 'gpx-rs';
|
||||
|
||||
export type {
|
||||
Clipboard,
|
||||
MoveTarget,
|
||||
GlobalStatistics,
|
||||
FileStructure,
|
||||
FilesUpdate,
|
||||
@@ -367,6 +368,20 @@ class Engine {
|
||||
return this.run((w) => w.paste());
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves elements to a place of the file tree, keeping their ids, like dragging and dropping
|
||||
* them does. The position of the target counts the elements of its list that are not moved.
|
||||
* The moved elements are selected.
|
||||
*
|
||||
* Files go among the files; tracks among the files (each becomes a file) or the tracks of a
|
||||
* file; segments among the files (each becomes a file), the tracks of a file (each becomes a
|
||||
* track) or the segments of a track; waypoints, or all the waypoints of a file (the waypoints
|
||||
* node), among the waypoints of a file.
|
||||
*/
|
||||
move(what: Selection, to: MoveTarget) {
|
||||
return this.run((w) => w.move_elements(what, to));
|
||||
}
|
||||
|
||||
// Edits of the selection
|
||||
|
||||
newTrack() {
|
||||
|
||||
Reference in New Issue
Block a user