mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-07 06:14:07 +00:00
progress
This commit is contained in:
@@ -26,6 +26,11 @@ pub enum Action<'a> {
|
||||
down: bool,
|
||||
add: bool,
|
||||
},
|
||||
/// Puts the selected elements in the clipboard, to be pasted by [`crate::Paste`]. Does
|
||||
/// nothing when nothing is selected.
|
||||
Copy,
|
||||
/// Like `Copy`, but the elements are moved instead of copied when they are pasted.
|
||||
Cut,
|
||||
/// Moves the files to `index` among the other files, in the given order. Not undoable.
|
||||
Reorder {
|
||||
file_ids: Vec<FileId>,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{Apply, CommandError, State};
|
||||
use crate::{
|
||||
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata,
|
||||
MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Reverse, SpliceTrackpoints, Split,
|
||||
Style, Time,
|
||||
MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, SpliceTrackpoints,
|
||||
Split, Style, Time,
|
||||
};
|
||||
|
||||
/// A user action that edits the files. Each variant wraps the command's own struct, whose
|
||||
@@ -16,6 +16,7 @@ pub enum Command<'a> {
|
||||
Load(Load<'a>),
|
||||
Duplicate(Duplicate),
|
||||
Delete(Delete),
|
||||
Paste(Paste),
|
||||
DeleteAll(DeleteAll),
|
||||
Metadata(Metadata<'a>),
|
||||
Style(Style<'a>),
|
||||
@@ -41,6 +42,7 @@ impl Apply for Command<'_> {
|
||||
Command::Load(c) => c.apply(state),
|
||||
Command::Duplicate(c) => c.apply(state),
|
||||
Command::Delete(c) => c.apply(state),
|
||||
Command::Paste(c) => c.apply(state),
|
||||
Command::DeleteAll(c) => c.apply(state),
|
||||
Command::Metadata(c) => c.apply(state),
|
||||
Command::Style(c) => c.apply(state),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, FileId, Selection, StackEntry, State, Track, TrackSegment, Waypoint,
|
||||
WaypointChunk,
|
||||
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, copy_file, copy_segment,
|
||||
copy_track, copy_waypoint, insert_waypoints,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -112,60 +112,23 @@ fn duplicate_waypoints(
|
||||
filter: impl Fn(&Waypoint) -> bool,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let mut file = (**file).clone();
|
||||
let last_chunk = file
|
||||
.wpt
|
||||
.iter()
|
||||
.rposition(|chunk| chunk.wpt.iter().any(&filter))
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
|
||||
let mut copies = HashSet::new();
|
||||
let mut inserted = Vec::new();
|
||||
let mut chunk = WaypointChunk::default();
|
||||
for wpt in file
|
||||
let selected: Vec<&Waypoint> = file
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.filter(|wpt| filter(wpt))
|
||||
{
|
||||
let mut copy = wpt.clone();
|
||||
copy.id = Default::default();
|
||||
copies.insert(copy.id);
|
||||
chunk.wpt.push(copy);
|
||||
if chunk.is_full() {
|
||||
inserted.push(Rc::new(std::mem::take(&mut chunk)));
|
||||
}
|
||||
}
|
||||
if !chunk.wpt.is_empty() {
|
||||
inserted.push(Rc::new(chunk));
|
||||
}
|
||||
.collect();
|
||||
let last = selected.last().ok_or(CommandError::NothingToDo)?.id;
|
||||
let copies: Vec<Waypoint> = selected.into_iter().map(copy_waypoint).collect();
|
||||
let copy_ids = copies.iter().map(|wpt| wpt.id).collect();
|
||||
|
||||
// The copies go right after the last selected waypoint: only the chunk holding it is cut
|
||||
// (when waypoints follow it), all the other chunks are kept as they are.
|
||||
let cut = &file.wpt[last_chunk];
|
||||
let split = cut.wpt.iter().rposition(&filter).unwrap() + 1;
|
||||
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
|
||||
};
|
||||
file.wpt.splice(last_chunk..=last_chunk, replacement);
|
||||
file.wpt_rev_id = Default::default();
|
||||
// The copies go right after the last selected waypoint.
|
||||
let mut file = (**file).clone();
|
||||
insert_waypoints(&mut file, Some(last), copies);
|
||||
files.insert(file_id, Rc::new(file));
|
||||
Ok(Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: copies,
|
||||
wpt_ids: copy_ids,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,29 +141,6 @@ fn append_copies<T>(items: &mut Vec<T>, filter: impl Fn(&T) -> bool, copy: impl
|
||||
items.splice(last + 1..last + 1, copies);
|
||||
}
|
||||
|
||||
// Track points are shared chunks, so copies are cheap.
|
||||
fn copy_segment(segment: &TrackSegment) -> TrackSegment {
|
||||
let mut copy = segment.clone();
|
||||
copy.id = Default::default();
|
||||
copy
|
||||
}
|
||||
|
||||
fn copy_track(track: &Track) -> Track {
|
||||
Track {
|
||||
id: Default::default(),
|
||||
trkseg: track.trkseg.iter().map(copy_segment).collect(),
|
||||
..track.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_file(file: &File) -> File {
|
||||
File {
|
||||
id: Default::default(),
|
||||
trk: file.trk.iter().map(copy_track).collect(),
|
||||
..file.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
|
||||
@@ -3,9 +3,11 @@ mod delete_all;
|
||||
mod duplicate;
|
||||
mod load;
|
||||
mod new;
|
||||
mod paste;
|
||||
|
||||
pub use delete::*;
|
||||
pub use delete_all::*;
|
||||
pub use duplicate::*;
|
||||
pub use load::*;
|
||||
pub use new::*;
|
||||
pub use paste::*;
|
||||
|
||||
@@ -0,0 +1,852 @@
|
||||
use std::{collections::HashSet, hash::Hash, rc::Rc};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
/// Pastes what was copied or cut (see [`crate::Clipboard`]), and empties the clipboard.
|
||||
///
|
||||
/// Where it goes depends on the selection, see [`crate::Clipboard::can_paste`]:
|
||||
/// - nothing selected: the files are copied, and tracks and segments become new files (named
|
||||
/// like their track, or like the file they come from);
|
||||
/// - a file: the tracks are added to it, segments become new tracks in it, and the waypoints are
|
||||
/// added to its own;
|
||||
/// - a track: the segments are added to it, tracks go after it in its file;
|
||||
/// - a segment: the segments go after it in its track;
|
||||
/// - the waypoints or a waypoint: the waypoints are added to their file, after the waypoint.
|
||||
///
|
||||
/// What is cut is moved, with its ids, otherwise copies are made. The pasted elements are
|
||||
/// selected.
|
||||
#[derive(Debug)]
|
||||
pub struct Paste;
|
||||
|
||||
impl Apply for Paste {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let clipboard = state
|
||||
.clipboard
|
||||
.clone()
|
||||
.filter(|clipboard| clipboard.can_paste(state.selection))
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let cut = clipboard.cut;
|
||||
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::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)?
|
||||
}
|
||||
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.
|
||||
fn last_selected<T: Copy + Eq + Hash>(
|
||||
all: impl Iterator<Item = T>,
|
||||
selected: &HashSet<T>,
|
||||
) -> Option<T> {
|
||||
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 super::*;
|
||||
|
||||
/// A file with the given tracks (name, number of segments) and number of waypoints.
|
||||
fn file(name: &str, tracks: &[(Option<&str>, usize)], waypoints: usize) -> File {
|
||||
let mut file = new_file(
|
||||
name,
|
||||
tracks
|
||||
.iter()
|
||||
.map(|(name, segments)| Track {
|
||||
info: TrackInfo {
|
||||
name: name.map(str::to_owned),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: (0..*segments).map(|_| TrackSegment::default()).collect(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if waypoints > 0 {
|
||||
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
||||
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
||||
..Default::default()
|
||||
})];
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn add(fx: &mut Fixture, file: File) -> FileId {
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn track_ids(fx: &Fixture, id: FileId) -> Vec<TrackId> {
|
||||
fx.files[&id].trk.iter().map(|trk| trk.id).collect()
|
||||
}
|
||||
|
||||
fn segment_ids(fx: &Fixture, id: FileId, trk: usize) -> Vec<TrackSegmentId> {
|
||||
fx.files[&id].trk[trk].trkseg.iter().map(|s| s.id).collect()
|
||||
}
|
||||
|
||||
fn wpt_ids(fx: &Fixture, id: FileId) -> Vec<WaypointId> {
|
||||
waypoint_ids(&fx.files[&id]).collect()
|
||||
}
|
||||
|
||||
fn names(fx: &Fixture) -> Vec<String> {
|
||||
fx.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn copy(fx: &mut Fixture, selection: Selection, cut: bool) {
|
||||
fx.clipboard = Clipboard::new(selection, cut);
|
||||
}
|
||||
|
||||
fn paste(fx: &mut Fixture) -> Result<(), CommandError> {
|
||||
Paste.apply(&mut fx.state())
|
||||
}
|
||||
|
||||
fn tracks(file_id: FileId, ids: &[TrackId]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn segments(file_id: FileId, trk_id: TrackId, ids: &[TrackSegmentId]) -> Selection {
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoints(file_id: FileId, ids: &[WaypointId]) -> Selection {
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn files(ids: &[FileId]) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_copied_tracks_into_a_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("t0"), 1), (Some("t1"), 2), (None, 1)], 0),
|
||||
);
|
||||
let b = add(&mut fx, file("b", &[(Some("b0"), 1)], 0));
|
||||
let source = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[source[0], source[1]]), false);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
// the source is untouched, copies with new ids are added at the end
|
||||
assert_eq!(track_ids(&fx, a), source);
|
||||
let in_b = track_ids(&fx, b);
|
||||
assert_eq!(in_b.len(), 3);
|
||||
assert!(!source.contains(&in_b[1]) && !source.contains(&in_b[2]));
|
||||
let names: Vec<_> = fx.files[&b]
|
||||
.trk
|
||||
.iter()
|
||||
.map(|t| t.info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
[Some("b0".into()), Some("t0".into()), Some("t1".into())]
|
||||
);
|
||||
// the segments are copies too
|
||||
assert_eq!(fx.files[&b].trk[2].trkseg.len(), 2);
|
||||
assert_ne!(segment_ids(&fx, b, 2), segment_ids(&fx, a, 1));
|
||||
// the pasted tracks are selected, and the clipboard is empty
|
||||
assert_eq!(fx.selection, tracks(b, &in_b[1..]));
|
||||
assert_eq!(fx.clipboard, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_cut_tracks_moves_them() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let source = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[source[0]]), true);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
assert_eq!(track_ids(&fx, a), vec![source[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![source[0]]);
|
||||
assert_eq!(fx.selection, tracks(b, &[source[0]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_tracks_after_the_selected_track() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file(
|
||||
"a",
|
||||
&[
|
||||
(Some("0"), 1),
|
||||
(Some("1"), 1),
|
||||
(Some("2"), 1),
|
||||
(Some("3"), 1),
|
||||
],
|
||||
0,
|
||||
),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
// copy: the block goes after the selected track
|
||||
copy(&mut fx, tracks(a, &[t[0], t[1]]), false);
|
||||
fx.selection = tracks(a, &[t[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
let names: Vec<_> = fx.files[&a]
|
||||
.trk
|
||||
.iter()
|
||||
.map(|t| t.info.name.clone().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, ["0", "1", "2", "0", "1", "3"]);
|
||||
|
||||
// cut: moved after the selected track
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("0"), 1), (Some("1"), 1), (Some("2"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = tracks(a, &[t[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[2], t[0]]);
|
||||
|
||||
// the selected track is cut itself: the tracks go at the end
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("0"), 1), (Some("1"), 1), (Some("2"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = tracks(a, &[t[0]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[2], t[0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_tracks_as_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("source", &[(Some("named"), 1), (None, 1)], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0], t[1]]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
// named like the track, or like the file it comes from
|
||||
assert_eq!(names(&fx), ["source", "named", "source"]);
|
||||
assert_eq!(fx.files.len(), 3);
|
||||
let new = &fx.order.0[1..];
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::File { file_ids } if file_ids.len() == 2 && new.iter().all(|id| file_ids.contains(id)))
|
||||
);
|
||||
assert_eq!(fx.files[&new[0]].trk.len(), 1);
|
||||
// the source keeps its tracks
|
||||
assert_eq!(track_ids(&fx, a), t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_cut_tracks_as_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("source", &[(Some("one"), 1), (Some("two"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
let new = fx.order.0[1];
|
||||
assert_eq!(track_ids(&fx, new), vec![t[0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_segments() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 3), (Some("t1"), 1)], 0));
|
||||
(fx, a)
|
||||
};
|
||||
|
||||
// at the end of the selected track
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0]]), false);
|
||||
fx.selection = tracks(a, &[t[1]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), s);
|
||||
let in_t1 = segment_ids(&fx, a, 1);
|
||||
assert_eq!(in_t1.len(), 2);
|
||||
assert_ne!(in_t1[1], s[0]);
|
||||
assert_eq!(fx.selection, segments(a, t[1], &in_t1[1..]));
|
||||
|
||||
// after the selected segment, moved
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0]]), true);
|
||||
fx.selection = segments(a, t[0], &[s[1]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[1], s[0], s[2]]);
|
||||
|
||||
// each segment becomes a track of the selected file
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0], s[1]]), false);
|
||||
fx.selection = files(&[a]);
|
||||
paste(&mut fx).unwrap();
|
||||
let tracks_now = track_ids(&fx, a);
|
||||
assert_eq!(tracks_now.len(), 4);
|
||||
assert!(
|
||||
fx.files[&a].trk[2..]
|
||||
.iter()
|
||||
.all(|trk| trk.trkseg.len() == 1)
|
||||
);
|
||||
assert_eq!(fx.selection, tracks(a, &tracks_now[2..]));
|
||||
|
||||
// each segment becomes a file, named like its track
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0], s[1]]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(names(&fx), ["a", "t0", "t0"]);
|
||||
assert!(
|
||||
fx.order.0[1..]
|
||||
.iter()
|
||||
.all(|id| fx.files[id].trk[0].trkseg.len() == 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_waypoints() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 4));
|
||||
let b = add(&mut fx, file("b", &[], 2));
|
||||
(fx, a, b)
|
||||
};
|
||||
|
||||
// to the end of another file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
copy(&mut fx, waypoints(a, &[w[1], w[2]]), false);
|
||||
fx.selection = files(&[b]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w);
|
||||
let now = wpt_ids(&fx, b);
|
||||
assert_eq!(now.len(), 4);
|
||||
assert_eq!(&now[..2], &before[..]);
|
||||
assert!(!w.contains(&now[2]));
|
||||
assert_eq!(fx.selection, waypoints(b, &now[2..]));
|
||||
|
||||
// after the selected waypoint
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
copy(&mut fx, waypoints(a, &[w[0]]), true);
|
||||
fx.selection = waypoints(b, &[before[0]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w[1..].to_vec());
|
||||
assert_eq!(wpt_ids(&fx, b), vec![before[0], w[0], before[1]]);
|
||||
|
||||
// within the file, after a waypoint
|
||||
let (mut fx, a, _) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, waypoints(a, &[w[0], w[1]]), true);
|
||||
fx.selection = waypoints(a, &[w[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), vec![w[2], w[0], w[1], w[3]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_all_the_waypoints_of_a_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 3));
|
||||
let b = add(&mut fx, file("b", &[], 1));
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, Selection::Waypoints { file_id: a }, false);
|
||||
fx.selection = Selection::Waypoints { file_id: b };
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w);
|
||||
assert_eq!(wpt_ids(&fx, b).len(), 4);
|
||||
|
||||
// moved: the source has none left
|
||||
copy(&mut fx, Selection::Waypoints { file_id: a }, true);
|
||||
fx.selection = files(&[b]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert!(wpt_ids(&fx, a).is_empty());
|
||||
assert_eq!(&wpt_ids(&fx, b)[4..], &w[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 1)], 1));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
copy(&mut fx, files(&[b, a]), false);
|
||||
fx.selection = files(&[c]);
|
||||
paste(&mut fx).unwrap();
|
||||
// copies, in the order of the files, at the end
|
||||
assert_eq!(names(&fx), ["a", "b", "c", "a", "b"]);
|
||||
assert_eq!(fx.files.len(), 5);
|
||||
assert_eq!(fx.files[&fx.order.0[3]].trk.len(), 1);
|
||||
assert_ne!(track_ids(&fx, fx.order.0[3]), track_ids(&fx, a));
|
||||
assert!(matches!(&fx.selection, Selection::File { file_ids } if file_ids.len() == 2));
|
||||
|
||||
// cut: moved to the end, they keep their ids
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
copy(&mut fx, files(&[a]), true);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.order.0, vec![b, c, a]);
|
||||
assert_eq!(fx.selection, files(&[a]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_paste() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 1)], 1));
|
||||
let t = track_ids(&fx, a);
|
||||
let w = wpt_ids(&fx, a);
|
||||
fx.selection = files(&[a]);
|
||||
|
||||
// an empty clipboard
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
|
||||
// a target that does not accept it (tracks cannot go to waypoints), the clipboard stays
|
||||
copy(&mut fx, tracks(a, &t), false);
|
||||
fx.selection = waypoints(a, &w);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
assert!(fx.clipboard.is_some());
|
||||
assert_eq!(fx.selection, waypoints(a, &w));
|
||||
|
||||
// elements that do not exist anymore
|
||||
copy(&mut fx, tracks(a, &[TrackId::default()]), false);
|
||||
fx.selection = files(&[a]);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
copy(&mut fx, waypoints(a, &[WaypointId::default()]), false);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
copy(&mut fx, files(&[FileId::default()]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
assert_eq!(fx.order.0, vec![a]);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, FileOrder, Selection, StackEntry, State};
|
||||
use crate::{Clipboard, FileId, FileOrder, Selection, StackEntry, State};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Fixture {
|
||||
pub files: StackEntry,
|
||||
pub selection: Selection,
|
||||
pub order: FileOrder,
|
||||
pub clipboard: Option<Clipboard>,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
@@ -17,6 +18,7 @@ impl Fixture {
|
||||
files: &mut self.files,
|
||||
selection: &mut self.selection,
|
||||
order: &mut self.order,
|
||||
clipboard: &mut self.clipboard,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use crate::{File, Track, TrackSegment, Waypoint};
|
||||
|
||||
// The trackpoints of a segment are shared chunks, so copying elements is cheap.
|
||||
|
||||
/// A copy of the segment, with a new id.
|
||||
pub fn copy_segment(segment: &TrackSegment) -> TrackSegment {
|
||||
let mut copy = segment.clone();
|
||||
copy.id = Default::default();
|
||||
copy
|
||||
}
|
||||
|
||||
/// A copy of the track and of its segments, with new ids.
|
||||
pub fn copy_track(track: &Track) -> Track {
|
||||
Track {
|
||||
id: Default::default(),
|
||||
trkseg: track.trkseg.iter().map(copy_segment).collect(),
|
||||
..track.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the file and of its tracks and segments, with new ids.
|
||||
pub fn copy_file(file: &File) -> File {
|
||||
File {
|
||||
id: Default::default(),
|
||||
trk: file.trk.iter().map(copy_track).collect(),
|
||||
..file.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the waypoint, with a new id.
|
||||
pub fn copy_waypoint(waypoint: &Waypoint) -> Waypoint {
|
||||
let mut copy = waypoint.clone();
|
||||
copy.id = Default::default();
|
||||
copy
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
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.
|
||||
pub fn insert_waypoints(file: &mut File, after: Option<WaypointId>, waypoints: Vec<Waypoint>) {
|
||||
if waypoints.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut inserted = Vec::new();
|
||||
let mut chunk = WaypointChunk::default();
|
||||
for wpt in waypoints {
|
||||
chunk.wpt.push(wpt);
|
||||
if chunk.is_full() {
|
||||
inserted.push(Rc::new(std::mem::take(&mut chunk)));
|
||||
}
|
||||
}
|
||||
if !chunk.wpt.is_empty() {
|
||||
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))
|
||||
})
|
||||
});
|
||||
match position {
|
||||
None => file.wpt.extend(inserted),
|
||||
Some((i, split)) => {
|
||||
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
|
||||
};
|
||||
file.wpt.splice(i..=i, replacement);
|
||||
}
|
||||
}
|
||||
file.wpt_rev_id = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn waypoint() -> Waypoint {
|
||||
Waypoint::default()
|
||||
}
|
||||
|
||||
fn file_with(chunks: &[usize]) -> (File, Vec<WaypointId>) {
|
||||
let mut file = File::default();
|
||||
let mut ids = vec![];
|
||||
for n in chunks {
|
||||
let wpt: Vec<_> = (0..*n).map(|_| waypoint()).collect();
|
||||
ids.extend(wpt.iter().map(|w| w.id));
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt,
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
(file, ids)
|
||||
}
|
||||
|
||||
fn ids(file: &File) -> Vec<WaypointId> {
|
||||
file.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| chunk.wpt.iter().map(|w| w.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_at_the_end() {
|
||||
let (mut file, before) = file_with(&[2, 1]);
|
||||
let rev = file.wpt_rev_id;
|
||||
let new = vec![waypoint(), waypoint()];
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
insert_waypoints(&mut file, None, new);
|
||||
assert_eq!(ids(&file), [before.clone(), new_ids].concat());
|
||||
// the chunks that were there are kept
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert_ne!(file.wpt_rev_id, rev);
|
||||
// an unknown waypoint is an insertion at the end too
|
||||
let (mut file, before) = file_with(&[2]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints(&mut file, Some(WaypointId::default()), vec![one]);
|
||||
assert_eq!(ids(&file), [before, vec![one_id]].concat());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_after_the_last_waypoint_of_a_chunk_keeps_the_chunks() {
|
||||
let (mut file, before) = file_with(&[2, 2]);
|
||||
let first = file.wpt[0].clone();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints(&mut file, Some(before[1]), vec![one]);
|
||||
assert_eq!(ids(&file), [&before[..2], &[one_id], &before[2..]].concat());
|
||||
// 2 chunks + the inserted one, nothing was cut
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(Rc::ptr_eq(&file.wpt[0], &first));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_in_the_middle_of_a_chunk_cuts_it() {
|
||||
let (mut file, before) = file_with(&[3, 1]);
|
||||
let last = file.wpt[1].clone();
|
||||
let new = vec![waypoint(), waypoint()];
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
insert_waypoints(&mut file, Some(before[0]), new);
|
||||
assert_eq!(
|
||||
ids(&file),
|
||||
[&before[..1], &new_ids[..], &before[1..]].concat()
|
||||
);
|
||||
// the cut chunk, the inserted one and the end of the cut one, then the untouched chunk
|
||||
assert_eq!(file.wpt.len(), 4);
|
||||
assert!(Rc::ptr_eq(&file.wpt[3], &last));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_nothing() {
|
||||
let (mut file, before) = file_with(&[2]);
|
||||
let rev = file.wpt_rev_id;
|
||||
insert_waypoints(&mut file, Some(before[0]), vec![]);
|
||||
assert_eq!(ids(&file), before);
|
||||
assert_eq!(file.wpt_rev_id, rev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_many_fills_chunks() {
|
||||
let mut file = File::default();
|
||||
let new: Vec<_> = (0..300).map(|_| waypoint()).collect();
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
insert_waypoints(&mut file, None, new);
|
||||
assert_eq!(ids(&file), new_ids);
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(file.wpt[..2].iter().all(|chunk| chunk.is_full()));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
mod copy;
|
||||
mod edit_waypoint_chunks;
|
||||
mod insert_waypoints;
|
||||
mod produce;
|
||||
mod update_selected;
|
||||
|
||||
pub use copy::*;
|
||||
pub use edit_waypoint_chunks::*;
|
||||
pub use insert_waypoints::*;
|
||||
pub use produce::*;
|
||||
pub use update_selected::*;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// TODO license in every file
|
||||
|
||||
use crate::{
|
||||
Action, Apply, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||
Action, Apply, Clipboard, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||
FileStructureCache, GlobalStatistics, SelectMode, Selection, Stack, State, StatisticsBuffer,
|
||||
StatisticsCache, TrackSegmentId,
|
||||
};
|
||||
@@ -18,6 +18,8 @@ pub struct Engine {
|
||||
order_changed: bool,
|
||||
selection_changed: bool,
|
||||
statistics_buffer: StatisticsBuffer,
|
||||
clipboard: Option<Clipboard>,
|
||||
clipboard_changed: bool,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@@ -41,6 +43,23 @@ impl Engine {
|
||||
Some(self.statistics_cache.file_global(file))
|
||||
}
|
||||
|
||||
/// What was copied or cut and is waiting to be pasted.
|
||||
pub fn clipboard(&self) -> Option<&Clipboard> {
|
||||
self.clipboard.as_ref()
|
||||
}
|
||||
|
||||
/// Whether the clipboard can be pasted with the current selection.
|
||||
pub fn can_paste(&self) -> bool {
|
||||
self.clipboard
|
||||
.as_ref()
|
||||
.is_some_and(|clipboard| clipboard.can_paste(&self.selection))
|
||||
}
|
||||
|
||||
/// Whether the last action changed the clipboard.
|
||||
pub fn clipboard_changed(&self) -> bool {
|
||||
self.clipboard_changed
|
||||
}
|
||||
|
||||
/// Whether there is something to undo.
|
||||
pub fn can_undo(&self) -> bool {
|
||||
self.stack.can_undo()
|
||||
@@ -86,6 +105,7 @@ impl Engine {
|
||||
pub fn execute(&mut self, action: Action) -> bool {
|
||||
let selection_before = self.selection.clone();
|
||||
let order_before = self.order.0.clone();
|
||||
let clipboard_before = self.clipboard.clone();
|
||||
// the files change iff there is a diff, but selection and order actions have none
|
||||
self.diff = match action {
|
||||
Action::Edit(command) => self.edit(command),
|
||||
@@ -128,6 +148,14 @@ impl Engine {
|
||||
}
|
||||
None
|
||||
}
|
||||
Action::Copy => {
|
||||
self.copy(false);
|
||||
None
|
||||
}
|
||||
Action::Cut => {
|
||||
self.copy(true);
|
||||
None
|
||||
}
|
||||
Action::Reorder { file_ids, index } => {
|
||||
self.order.move_files(&file_ids, index);
|
||||
None
|
||||
@@ -135,22 +163,32 @@ impl Engine {
|
||||
};
|
||||
let changed = self.diff.is_some()
|
||||
|| self.selection != selection_before
|
||||
|| self.order.0 != order_before;
|
||||
|| self.order.0 != order_before
|
||||
|| self.clipboard != clipboard_before;
|
||||
if changed {
|
||||
self.refresh();
|
||||
}
|
||||
// after the refresh, which also syncs the order and selection with the files (undo, redo)
|
||||
self.order_changed = self.order.0 != order_before;
|
||||
self.selection_changed = self.selection != selection_before;
|
||||
self.clipboard_changed = self.clipboard != clipboard_before;
|
||||
changed
|
||||
}
|
||||
|
||||
/// Puts the selection in the clipboard, if there is one.
|
||||
fn copy(&mut self, cut: bool) {
|
||||
if let Some(clipboard) = Clipboard::new(self.selection.clone(), cut) {
|
||||
self.clipboard = Some(clipboard);
|
||||
}
|
||||
}
|
||||
|
||||
fn edit(&mut self, command: Command) -> Option<Diff> {
|
||||
self.stack.create_and_push_next(|files| {
|
||||
let mut state = State {
|
||||
files,
|
||||
selection: &mut self.selection,
|
||||
order: &mut self.order,
|
||||
clipboard: &mut self.clipboard,
|
||||
};
|
||||
command.apply(&mut state).map_err(|err| err.to_string())
|
||||
})
|
||||
@@ -164,10 +202,18 @@ impl Engine {
|
||||
// files coming back after an undo/redo are added at the end of the order
|
||||
self.order.sync(files);
|
||||
self.selection.retain_existing(files);
|
||||
// what was copied is not pasted if it does not exist anymore
|
||||
if let Some(clipboard) = &mut self.clipboard {
|
||||
clipboard.selection.retain_existing(files);
|
||||
if clipboard.selection == Selection::Empty {
|
||||
self.clipboard = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.order.0.clear();
|
||||
self.selection = Selection::Empty;
|
||||
self.clipboard = None;
|
||||
}
|
||||
}
|
||||
self.statistics_cache.update(current);
|
||||
@@ -186,7 +232,9 @@ impl Engine {
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{Load, Metadata, New, NewTrack, Style, TrackId, TrackSegmentId};
|
||||
use crate::{
|
||||
Delete, DeleteAll, Load, Metadata, New, NewTrack, Paste, Style, TrackId, TrackSegmentId,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -809,4 +857,168 @@ mod tests {
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert_eq!(engine.order().len(), 1);
|
||||
}
|
||||
|
||||
/// A file named `name` with `n` tracks, selected.
|
||||
fn file_with_tracks(engine: &mut Engine, name: &str, n: usize) -> FileId {
|
||||
new(engine, name);
|
||||
for _ in 0..n {
|
||||
edit(engine, Command::NewTrack(NewTrack));
|
||||
}
|
||||
*engine.order().last().unwrap()
|
||||
}
|
||||
|
||||
fn track_ids(engine: &Engine, id: FileId) -> Vec<TrackId> {
|
||||
engine
|
||||
.file_structure(&id)
|
||||
.unwrap()
|
||||
.tracks
|
||||
.iter()
|
||||
.map(|t| t.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn paste(engine: &mut Engine) -> bool {
|
||||
edit(engine, Command::Paste(Paste))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_and_paste() {
|
||||
let mut engine = Engine::default();
|
||||
let a = file_with_tracks(&mut engine, "a", 2);
|
||||
let b = file_with_tracks(&mut engine, "b", 0);
|
||||
assert!(engine.clipboard().is_none() && !engine.can_paste());
|
||||
|
||||
// nothing selected, nothing copied
|
||||
select_elements(&mut engine, Selection::Empty, SelectMode::Replace);
|
||||
assert!(!engine.execute(Action::Copy));
|
||||
assert!(engine.clipboard().is_none());
|
||||
|
||||
let tracks = track_ids(&engine, a);
|
||||
let selected_tracks = Selection::Track {
|
||||
file_id: a,
|
||||
trk_ids: tracks.iter().copied().collect(),
|
||||
};
|
||||
select_elements(&mut engine, selected_tracks.clone(), SelectMode::Replace);
|
||||
assert!(engine.execute(Action::Copy));
|
||||
assert!(engine.clipboard_changed() && !engine.selection_changed());
|
||||
assert_eq!(
|
||||
engine.clipboard().map(|c| (c.cut, &c.selection)),
|
||||
Some((false, &selected_tracks))
|
||||
);
|
||||
// tracks cannot be pasted onto themselves... but into a track, a file or the list of files
|
||||
assert!(engine.can_paste());
|
||||
select_elements(&mut engine, files(&[b]), SelectMode::Replace);
|
||||
assert!(engine.can_paste());
|
||||
select_elements(
|
||||
&mut engine,
|
||||
Selection::Waypoints { file_id: b },
|
||||
SelectMode::Replace,
|
||||
);
|
||||
assert!(!engine.can_paste());
|
||||
select_elements(&mut engine, files(&[b]), SelectMode::Replace);
|
||||
|
||||
// pasting is an edit
|
||||
assert!(paste(&mut engine));
|
||||
assert_eq!(track_ids(&engine, a), tracks);
|
||||
assert_eq!(track_ids(&engine, b).len(), 2);
|
||||
assert!(engine.clipboard().is_none() && engine.clipboard_changed());
|
||||
assert!(
|
||||
matches!(engine.selection(), Selection::Track { file_id, trk_ids } if *file_id == b && trk_ids.len() == 2)
|
||||
);
|
||||
// the clipboard is empty: nothing more to paste
|
||||
assert!(!paste(&mut engine));
|
||||
|
||||
// and it can be undone
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(track_ids(&engine, b).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cut_and_paste_moves_after_the_paste() {
|
||||
let mut engine = Engine::default();
|
||||
let a = file_with_tracks(&mut engine, "a", 2);
|
||||
let b = file_with_tracks(&mut engine, "b", 0);
|
||||
let tracks = track_ids(&engine, a);
|
||||
select_elements(
|
||||
&mut engine,
|
||||
Selection::Track {
|
||||
file_id: a,
|
||||
trk_ids: [tracks[0]].into(),
|
||||
},
|
||||
SelectMode::Replace,
|
||||
);
|
||||
assert!(engine.execute(Action::Cut));
|
||||
assert!(engine.clipboard().is_some_and(|c| c.cut));
|
||||
// nothing moves before the paste
|
||||
assert_eq!(track_ids(&engine, a), tracks);
|
||||
|
||||
select_elements(&mut engine, files(&[b]), SelectMode::Replace);
|
||||
assert!(paste(&mut engine));
|
||||
assert_eq!(track_ids(&engine, a), vec![tracks[1]]);
|
||||
assert_eq!(track_ids(&engine, b), vec![tracks[0]]);
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert_eq!(track_ids(&engine, a), tracks);
|
||||
assert!(track_ids(&engine, b).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_drops_what_does_not_exist_anymore() {
|
||||
let mut engine = Engine::default();
|
||||
let a = file_with_tracks(&mut engine, "a", 2);
|
||||
let tracks = track_ids(&engine, a);
|
||||
select_elements(
|
||||
&mut engine,
|
||||
Selection::Track {
|
||||
file_id: a,
|
||||
trk_ids: tracks.iter().copied().collect(),
|
||||
},
|
||||
SelectMode::Replace,
|
||||
);
|
||||
engine.execute(Action::Copy);
|
||||
|
||||
// one of the tracks is deleted: the other one is still there to paste
|
||||
select_elements(
|
||||
&mut engine,
|
||||
Selection::Track {
|
||||
file_id: a,
|
||||
trk_ids: [tracks[0]].into(),
|
||||
},
|
||||
SelectMode::Replace,
|
||||
);
|
||||
assert!(edit(
|
||||
&mut engine,
|
||||
Command::Delete(Delete { whole_files: false })
|
||||
));
|
||||
assert!(
|
||||
matches!(engine.clipboard().map(|c| &c.selection), Some(Selection::Track { trk_ids, .. }) if trk_ids.len() == 1)
|
||||
);
|
||||
|
||||
// the whole file is deleted: nothing left
|
||||
select_elements(&mut engine, files(&[a]), SelectMode::Replace);
|
||||
assert!(edit(&mut engine, Command::DeleteAll(DeleteAll)));
|
||||
assert!(engine.clipboard().is_none());
|
||||
assert!(engine.clipboard_changed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_replaces_the_clipboard_and_does_not_change_the_history() {
|
||||
let mut engine = Engine::default();
|
||||
let a = file_with_tracks(&mut engine, "a", 0);
|
||||
let b = file_with_tracks(&mut engine, "b", 0);
|
||||
select_files(&mut engine, &[a]);
|
||||
engine.execute(Action::Copy);
|
||||
select_files(&mut engine, &[b]);
|
||||
assert!(engine.execute(Action::Cut));
|
||||
assert_eq!(
|
||||
engine.clipboard().map(|c| (c.cut, &c.selection)),
|
||||
Some((true, &files(&[b])))
|
||||
);
|
||||
// the same content again: nothing changed
|
||||
assert!(!engine.execute(Action::Cut));
|
||||
assert!(!engine.clipboard_changed());
|
||||
// only the creations of the files are in the history
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert!(!engine.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
use crate::Selection;
|
||||
|
||||
/// What was copied or cut: the elements, by id, so that they are found again when pasting, even
|
||||
/// if the files changed in between. Cut elements stay in place until they are pasted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Clipboard {
|
||||
pub selection: Selection,
|
||||
pub cut: bool,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
/// `None` if nothing is selected.
|
||||
pub fn new(selection: Selection, cut: bool) -> Option<Clipboard> {
|
||||
(selection != Selection::Empty).then_some(Clipboard { selection, cut })
|
||||
}
|
||||
|
||||
/// Whether the content can be pasted when `target` is selected: files and tracks can be
|
||||
/// pasted anywhere above them in the file tree, and nothing selected stands for the list of
|
||||
/// files.
|
||||
///
|
||||
/// | copied | pasted when selected |
|
||||
/// |-----------|------------------------------------------|
|
||||
/// | files | nothing, a file |
|
||||
/// | tracks | nothing, a file, a track |
|
||||
/// | segments | nothing, a file, a track, a segment |
|
||||
/// | waypoints | a file, the waypoints, a waypoint |
|
||||
pub fn can_paste(&self, target: &Selection) -> bool {
|
||||
use Selection::*;
|
||||
matches!(
|
||||
(&self.selection, target),
|
||||
(File { .. }, Empty | File { .. })
|
||||
| (Track { .. }, Empty | File { .. } | Track { .. })
|
||||
| (
|
||||
TrackSegment { .. },
|
||||
Empty | File { .. } | Track { .. } | TrackSegment { .. }
|
||||
)
|
||||
| (
|
||||
Waypoints { .. } | Waypoint { .. },
|
||||
File { .. } | Waypoints { .. } | Waypoint { .. }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, TrackId, TrackSegmentId, WaypointId};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn file() -> Selection {
|
||||
Selection::File {
|
||||
file_ids: [FileId::default()].into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn track() -> Selection {
|
||||
Selection::Track {
|
||||
file_id: FileId::default(),
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn segment() -> Selection {
|
||||
Selection::TrackSegment {
|
||||
file_id: FileId::default(),
|
||||
trk_id: TrackId::default(),
|
||||
trkseg_ids: [TrackSegmentId::default()].into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoints() -> Selection {
|
||||
Selection::Waypoints {
|
||||
file_id: FileId::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoint() -> Selection {
|
||||
Selection::Waypoint {
|
||||
file_id: FileId::default(),
|
||||
wpt_ids: HashSet::from([WaypointId::default()]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_copy() {
|
||||
assert_eq!(Clipboard::new(Selection::Empty, false), None);
|
||||
assert!(Clipboard::new(file(), true).is_some_and(|c| c.cut));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_where_things_can_be_pasted() {
|
||||
let targets = [
|
||||
Selection::Empty,
|
||||
file(),
|
||||
track(),
|
||||
segment(),
|
||||
waypoints(),
|
||||
waypoint(),
|
||||
];
|
||||
let expected = [
|
||||
// nothing, file, track, segment, waypoints, waypoint
|
||||
(file(), [true, true, false, false, false, false]),
|
||||
(track(), [true, true, true, false, false, false]),
|
||||
(segment(), [true, true, true, true, false, false]),
|
||||
(waypoints(), [false, true, false, false, true, true]),
|
||||
(waypoint(), [false, true, false, false, true, true]),
|
||||
];
|
||||
for (copied, allowed) in expected {
|
||||
let clipboard = Clipboard::new(copied.clone(), false).unwrap();
|
||||
for (target, allowed) in targets.iter().zip(allowed) {
|
||||
assert_eq!(
|
||||
clipboard.can_paste(target),
|
||||
allowed,
|
||||
"{copied:?} on {target:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
mod clipboard;
|
||||
mod order;
|
||||
mod selection;
|
||||
mod stack;
|
||||
|
||||
pub use clipboard::*;
|
||||
pub use order::*;
|
||||
pub use selection::*;
|
||||
pub use stack::*;
|
||||
@@ -10,4 +12,5 @@ pub struct State<'a> {
|
||||
pub files: &'a mut StackEntry,
|
||||
pub selection: &'a mut Selection,
|
||||
pub order: &'a mut FileOrder,
|
||||
pub clipboard: &'a mut Option<Clipboard>,
|
||||
}
|
||||
|
||||
+62
-6
@@ -358,6 +358,32 @@ pub fn can_redo() -> bool {
|
||||
with_engine(|e| e.can_redo()).unwrap_or(false)
|
||||
}
|
||||
|
||||
// Clipboard
|
||||
|
||||
/// Puts the selected elements in the clipboard. Nothing happens when nothing is selected.
|
||||
#[wasm_bindgen]
|
||||
pub fn copy() -> bool {
|
||||
execute(Action::Copy)
|
||||
}
|
||||
|
||||
/// Like `copy`, but the elements are moved when they are pasted.
|
||||
#[wasm_bindgen]
|
||||
pub fn cut() -> bool {
|
||||
execute(Action::Cut)
|
||||
}
|
||||
|
||||
/// Pastes the clipboard according to the selection (see `can_paste`).
|
||||
#[wasm_bindgen]
|
||||
pub fn paste() -> bool {
|
||||
edit(Command::Paste(engine::Paste))
|
||||
}
|
||||
|
||||
/// Whether the clipboard can be pasted with the current selection.
|
||||
#[wasm_bindgen]
|
||||
pub fn can_paste() -> bool {
|
||||
with_engine(|e| e.can_paste()).unwrap_or(false)
|
||||
}
|
||||
|
||||
// Selection
|
||||
|
||||
/// Selects files. `file_ids_bytes`: concatenated 16-byte UUIDs. Unknown files are ignored, and
|
||||
@@ -474,9 +500,15 @@ export type Selection =
|
||||
| { type: 'segment'; fileId: string; trackId: string; segmentIds: string[] }
|
||||
| { type: 'waypoints'; fileId: string }
|
||||
| { type: 'waypoint'; fileId: string; waypointIds: string[] };
|
||||
/** What was copied or cut, to be pasted. */
|
||||
export interface Clipboard {
|
||||
selection: Selection;
|
||||
cut: boolean;
|
||||
}
|
||||
export interface FilesUpdate {
|
||||
orderChanged: boolean;
|
||||
selectionChanged: boolean;
|
||||
clipboardChanged: boolean;
|
||||
/** Files to read the structure of. */
|
||||
added: string[];
|
||||
/** Files whose structure changed: reread it. */
|
||||
@@ -548,6 +580,8 @@ export interface WaypointNode {
|
||||
extern "C" {
|
||||
#[wasm_bindgen(typescript_type = "Selection")]
|
||||
pub type Selection;
|
||||
#[wasm_bindgen(typescript_type = "Clipboard | undefined")]
|
||||
pub type Clipboard;
|
||||
#[wasm_bindgen(typescript_type = "FilesUpdate")]
|
||||
pub type FilesUpdate;
|
||||
#[wasm_bindgen(typescript_type = "FileStructure | undefined")]
|
||||
@@ -730,12 +764,10 @@ fn uuids<T>(items: impl IntoIterator<Item = T>, uuid: impl Fn(T) -> uuid::Uuid)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The current selection (ids are UUID strings).
|
||||
#[wasm_bindgen]
|
||||
pub fn selection() -> Selection {
|
||||
fn selection_object(selection: &engine::Selection) -> Object {
|
||||
use engine::Selection as S;
|
||||
let object = Object::new();
|
||||
with_engine(|e| match e.selection() {
|
||||
match selection {
|
||||
S::Empty => set(&object, "type", "empty"),
|
||||
S::File { file_ids } => {
|
||||
set(&object, "type", "file");
|
||||
@@ -765,8 +797,31 @@ pub fn selection() -> Selection {
|
||||
set(&object, "fileId", file_id.0.to_string());
|
||||
set(&object, "waypointIds", uuids(wpt_ids, |id| id.0));
|
||||
}
|
||||
});
|
||||
object.unchecked_into()
|
||||
}
|
||||
object
|
||||
}
|
||||
|
||||
/// The current selection (ids are UUID strings).
|
||||
#[wasm_bindgen]
|
||||
pub fn selection() -> Selection {
|
||||
with_engine(|e| selection_object(e.selection()))
|
||||
.unwrap_or_default()
|
||||
.unchecked_into()
|
||||
}
|
||||
|
||||
/// What was copied or cut and is waiting to be pasted, `undefined` if nothing.
|
||||
#[wasm_bindgen]
|
||||
pub fn clipboard() -> Clipboard {
|
||||
with_engine(|e| {
|
||||
e.clipboard().map_or(JsValue::UNDEFINED, |clipboard| {
|
||||
let object = Object::new();
|
||||
set(&object, "selection", selection_object(&clipboard.selection));
|
||||
set(&object, "cut", clipboard.cut);
|
||||
object.into()
|
||||
})
|
||||
})
|
||||
.unwrap_or(JsValue::UNDEFINED)
|
||||
.unchecked_into()
|
||||
}
|
||||
|
||||
/// What the last action changed. Read it right after each action.
|
||||
@@ -777,6 +832,7 @@ pub fn last_update() -> FilesUpdate {
|
||||
let diff = e.last_diff().cloned().unwrap_or_default();
|
||||
set(&update, "orderChanged", e.order_changed());
|
||||
set(&update, "selectionChanged", e.selection_changed());
|
||||
set(&update, "clipboardChanged", e.clipboard_changed());
|
||||
set(&update, "added", ids(&diff.added));
|
||||
set(&update, "modified", ids(&diff.modified));
|
||||
set(&update, "removed", ids(&diff.removed));
|
||||
|
||||
Reference in New Issue
Block a user