mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-06 22:04:06 +00:00
progress
This commit is contained in:
@@ -16,6 +16,16 @@ pub enum Action<'a> {
|
|||||||
selection: Selection,
|
selection: Selection,
|
||||||
mode: SelectMode,
|
mode: SelectMode,
|
||||||
},
|
},
|
||||||
|
/// Selects all the elements of the same kind as the selected ones, in the same place (see
|
||||||
|
/// [`Selection::all_at_level`]). Does nothing when there is nothing more to select.
|
||||||
|
SelectAll,
|
||||||
|
/// Moves the selection to the next (`down`) or previous element of the same kind, like the
|
||||||
|
/// arrow keys do in the file list (see [`Selection::neighbour`]). With `add`, the element is
|
||||||
|
/// added to the selection (shift + arrow), otherwise it replaces it.
|
||||||
|
ArrowSelect {
|
||||||
|
down: bool,
|
||||||
|
add: bool,
|
||||||
|
},
|
||||||
/// Moves the files to `index` among the other files, in the given order. Not undoable.
|
/// Moves the files to `index` among the other files, in the given order. Not undoable.
|
||||||
Reorder {
|
Reorder {
|
||||||
file_ids: Vec<FileId>,
|
file_ids: Vec<FileId>,
|
||||||
|
|||||||
@@ -4,12 +4,22 @@ use crate::{
|
|||||||
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, edit_waypoint_chunks,
|
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, edit_waypoint_chunks,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
|
||||||
|
/// are deleted instead, even if only a track or a waypoint is selected.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Delete;
|
pub struct Delete {
|
||||||
|
pub whole_files: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Apply for Delete {
|
impl Apply for Delete {
|
||||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
let next = match &*state.selection {
|
let files = state.selection.to_files();
|
||||||
|
let selection = if self.whole_files {
|
||||||
|
&files
|
||||||
|
} else {
|
||||||
|
&*state.selection
|
||||||
|
};
|
||||||
|
let next = match selection {
|
||||||
Selection::File { file_ids } => {
|
Selection::File { file_ids } => {
|
||||||
state.files.retain(|id, _| !file_ids.contains(id));
|
state.files.retain(|id, _| !file_ids.contains(id));
|
||||||
state.order.0.retain(|id| !file_ids.contains(id));
|
state.order.0.retain(|id| !file_ids.contains(id));
|
||||||
@@ -112,7 +122,7 @@ mod tests {
|
|||||||
fn test_delete_nothing_selected() {
|
fn test_delete_nothing_selected() {
|
||||||
let mut fx = Fixture::default();
|
let mut fx = Fixture::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Delete.apply(&mut fx.state()),
|
Delete { whole_files: false }.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,7 +135,9 @@ mod tests {
|
|||||||
fx.selection = Selection::File {
|
fx.selection = Selection::File {
|
||||||
file_ids: HashSet::from([id]),
|
file_ids: HashSet::from([id]),
|
||||||
};
|
};
|
||||||
Delete.apply(&mut fx.state()).unwrap();
|
Delete { whole_files: false }
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
assert_eq!(fx.order.0, vec![keep]);
|
assert_eq!(fx.order.0, vec![keep]);
|
||||||
assert!(fx.files.contains_key(&keep) && !fx.files.contains_key(&id));
|
assert!(fx.files.contains_key(&keep) && !fx.files.contains_key(&id));
|
||||||
assert!(matches!(fx.selection, Selection::Empty));
|
assert!(matches!(fx.selection, Selection::Empty));
|
||||||
@@ -140,7 +152,9 @@ mod tests {
|
|||||||
file_id: id,
|
file_id: id,
|
||||||
trk_ids: HashSet::from([trk_id]),
|
trk_ids: HashSet::from([trk_id]),
|
||||||
};
|
};
|
||||||
Delete.apply(&mut fx.state()).unwrap();
|
Delete { whole_files: false }
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
let file = &fx.files[&id];
|
let file = &fx.files[&id];
|
||||||
assert_eq!(file.trk.len(), before - 1);
|
assert_eq!(file.trk.len(), before - 1);
|
||||||
assert!(file.trk.iter().all(|t| t.id != trk_id));
|
assert!(file.trk.iter().all(|t| t.id != trk_id));
|
||||||
@@ -157,7 +171,9 @@ mod tests {
|
|||||||
trk_id,
|
trk_id,
|
||||||
trkseg_ids: HashSet::from([seg_id]),
|
trkseg_ids: HashSet::from([seg_id]),
|
||||||
};
|
};
|
||||||
Delete.apply(&mut fx.state()).unwrap();
|
Delete { whole_files: false }
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
let trk = &fx.files[&id].trk[0];
|
let trk = &fx.files[&id].trk[0];
|
||||||
assert_eq!(trk.trkseg.len(), before - 1);
|
assert_eq!(trk.trkseg.len(), before - 1);
|
||||||
assert!(trk.trkseg.iter().all(|s| s.id != seg_id));
|
assert!(trk.trkseg.iter().all(|s| s.id != seg_id));
|
||||||
@@ -187,7 +203,9 @@ mod tests {
|
|||||||
wpt_ids: HashSet::from([ids[1]]),
|
wpt_ids: HashSet::from([ids[1]]),
|
||||||
};
|
};
|
||||||
let rev = fx.files[&id].wpt_rev_id;
|
let rev = fx.files[&id].wpt_rev_id;
|
||||||
Delete.apply(&mut fx.state()).unwrap();
|
Delete { whole_files: false }
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
||||||
let left: Vec<_> = fx.files[&id]
|
let left: Vec<_> = fx.files[&id]
|
||||||
.wpt
|
.wpt
|
||||||
@@ -203,11 +221,13 @@ mod tests {
|
|||||||
let (mut fx, id) = loaded();
|
let (mut fx, id) = loaded();
|
||||||
with_waypoints(&mut fx, id, 3);
|
with_waypoints(&mut fx, id, 3);
|
||||||
fx.selection = Selection::Waypoints { file_id: id };
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
Delete.apply(&mut fx.state()).unwrap();
|
Delete { whole_files: false }
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
assert!(fx.files[&id].wpt.is_empty());
|
assert!(fx.files[&id].wpt.is_empty());
|
||||||
fx.selection = Selection::Waypoints { file_id: id };
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Delete.apply(&mut fx.state()),
|
Delete { whole_files: false }.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -220,7 +240,44 @@ mod tests {
|
|||||||
trk_ids: HashSet::from([Default::default()]),
|
trk_ids: HashSet::from([Default::default()]),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Delete.apply(&mut fx.state()),
|
Delete { whole_files: false }.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_whole_files_of_the_selected_elements() {
|
||||||
|
for selection in [
|
||||||
|
|id: FileId, fx: &Fixture| Selection::Track {
|
||||||
|
file_id: id,
|
||||||
|
trk_ids: HashSet::from([fx.files[&id].trk[0].id]),
|
||||||
|
},
|
||||||
|
|id: FileId, fx: &Fixture| Selection::TrackSegment {
|
||||||
|
file_id: id,
|
||||||
|
trk_id: fx.files[&id].trk[0].id,
|
||||||
|
trkseg_ids: HashSet::from([fx.files[&id].trk[0].trkseg[0].id]),
|
||||||
|
},
|
||||||
|
|id: FileId, _: &Fixture| Selection::Waypoints { file_id: id },
|
||||||
|
|id: FileId, _: &Fixture| Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||||
|
let keep = fx.order.0[1];
|
||||||
|
fx.selection = selection(id, &fx);
|
||||||
|
Delete { whole_files: true }.apply(&mut fx.state()).unwrap();
|
||||||
|
assert_eq!(fx.order.0, vec![keep]);
|
||||||
|
assert!(!fx.files.contains_key(&id));
|
||||||
|
assert!(matches!(fx.selection, Selection::Empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_whole_files_nothing_selected() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
assert_eq!(
|
||||||
|
Delete { whole_files: true }.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ impl Engine {
|
|||||||
Some(self.statistics_cache.file_global(file))
|
Some(self.statistics_cache.file_global(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether there is something to undo.
|
||||||
|
pub fn can_undo(&self) -> bool {
|
||||||
|
self.stack.can_undo()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether there is something to redo.
|
||||||
|
pub fn can_redo(&self) -> bool {
|
||||||
|
self.stack.can_redo()
|
||||||
|
}
|
||||||
|
|
||||||
/// Which files the last action added, removed or modified.
|
/// Which files the last action added, removed or modified.
|
||||||
pub fn last_diff(&self) -> Option<&Diff> {
|
pub fn last_diff(&self) -> Option<&Diff> {
|
||||||
self.diff.as_ref()
|
self.diff.as_ref()
|
||||||
@@ -98,6 +108,26 @@ impl Engine {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
Action::SelectAll => {
|
||||||
|
if let Some(files) = self.stack.current()
|
||||||
|
&& let Some(all) = self.selection.all_at_level(files, &self.order.0)
|
||||||
|
{
|
||||||
|
self.selection = all;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Action::ArrowSelect { down, add } => {
|
||||||
|
if let Some(files) = self.stack.current()
|
||||||
|
&& let Some(next) = self.selection.neighbour(files, &self.order.0, down)
|
||||||
|
{
|
||||||
|
if add {
|
||||||
|
self.selection.extend(next);
|
||||||
|
} else {
|
||||||
|
self.selection = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
Action::Reorder { file_ids, index } => {
|
Action::Reorder { file_ids, index } => {
|
||||||
self.order.move_files(&file_ids, index);
|
self.order.move_files(&file_ids, index);
|
||||||
None
|
None
|
||||||
@@ -154,6 +184,8 @@ impl Engine {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::{Load, Metadata, New, NewTrack, Style, TrackId, TrackSegmentId};
|
use crate::{Load, Metadata, New, NewTrack, Style, TrackId, TrackSegmentId};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -635,4 +667,146 @@ mod tests {
|
|||||||
engine.execute(Action::Undo);
|
engine.execute(Action::Undo);
|
||||||
assert_eq!(engine.selection(), &Selection::Empty);
|
assert_eq!(engine.selection(), &Selection::Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_undo_and_redo() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
assert!(!engine.can_undo() && !engine.can_redo());
|
||||||
|
new(&mut engine, "a");
|
||||||
|
assert!(engine.can_undo() && !engine.can_redo());
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert!(!engine.can_undo() && engine.can_redo());
|
||||||
|
// selecting does not change the history
|
||||||
|
new(&mut engine, "b");
|
||||||
|
let a = engine.order()[0];
|
||||||
|
select_files(&mut engine, &[a]);
|
||||||
|
assert!(engine.can_undo() && !engine.can_redo());
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert!(engine.execute(Action::Redo));
|
||||||
|
assert!(engine.can_undo() && !engine.can_redo());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_all_follows_the_selected_level() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "a");
|
||||||
|
new(&mut engine, "b");
|
||||||
|
let order = engine.order().to_vec();
|
||||||
|
select_files(&mut engine, &[order[0]]);
|
||||||
|
|
||||||
|
// files
|
||||||
|
assert!(engine.execute(Action::SelectAll));
|
||||||
|
assert_eq!(engine.selection(), &files(&order));
|
||||||
|
// everything is selected already
|
||||||
|
assert!(!engine.execute(Action::SelectAll));
|
||||||
|
|
||||||
|
// the tracks of a file
|
||||||
|
let load = |engine: &mut Engine| load(engine, "data/with_tracks_and_segments.gpx");
|
||||||
|
load(&mut engine);
|
||||||
|
let file = engine
|
||||||
|
.stack
|
||||||
|
.current()
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
.find(|f| !f.trk.is_empty())
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
let tracks: HashSet<_> = file.trk.iter().map(|t| t.id).collect();
|
||||||
|
assert!(tracks.len() >= 2);
|
||||||
|
select_elements(
|
||||||
|
&mut engine,
|
||||||
|
Selection::Track {
|
||||||
|
file_id: file.id,
|
||||||
|
trk_ids: [file.trk[0].id].into(),
|
||||||
|
},
|
||||||
|
SelectMode::Replace,
|
||||||
|
);
|
||||||
|
assert!(engine.execute(Action::SelectAll));
|
||||||
|
assert_eq!(
|
||||||
|
engine.selection(),
|
||||||
|
&Selection::Track {
|
||||||
|
file_id: file.id,
|
||||||
|
trk_ids: tracks
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// nothing selected: all the files
|
||||||
|
select_elements(&mut engine, Selection::Empty, SelectMode::Replace);
|
||||||
|
assert!(engine.execute(Action::SelectAll));
|
||||||
|
assert!(matches!(engine.selection(), Selection::File { file_ids } if file_ids.len() == 3));
|
||||||
|
// the waypoints node: nothing more to select
|
||||||
|
select_elements(
|
||||||
|
&mut engine,
|
||||||
|
Selection::Waypoints { file_id: file.id },
|
||||||
|
SelectMode::Replace,
|
||||||
|
);
|
||||||
|
assert!(!engine.execute(Action::SelectAll));
|
||||||
|
assert_eq!(
|
||||||
|
engine.selection(),
|
||||||
|
&Selection::Waypoints { file_id: file.id }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_arrow_select() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "file");
|
||||||
|
for _ in 0..3 {
|
||||||
|
edit(&mut engine, Command::NewTrack(NewTrack));
|
||||||
|
}
|
||||||
|
let file = engine
|
||||||
|
.stack
|
||||||
|
.current()
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
let ids: Vec<TrackId> = file.trk.iter().map(|t| t.id).collect();
|
||||||
|
assert_eq!(ids.len(), 3);
|
||||||
|
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||||
|
file_id: file.id,
|
||||||
|
trk_ids: ids.iter().copied().collect(),
|
||||||
|
};
|
||||||
|
let arrow =
|
||||||
|
|engine: &mut Engine, down, add| engine.execute(Action::ArrowSelect { down, add });
|
||||||
|
|
||||||
|
select_elements(&mut engine, tracks(&[ids[0]]), SelectMode::Replace);
|
||||||
|
// without shift the next one replaces the selection
|
||||||
|
assert!(arrow(&mut engine, true, false));
|
||||||
|
assert_eq!(engine.selection(), &tracks(&[ids[1]]));
|
||||||
|
assert!(engine.selection_changed());
|
||||||
|
// with shift it is added
|
||||||
|
assert!(arrow(&mut engine, true, true));
|
||||||
|
assert_eq!(engine.selection(), &tracks(&[ids[1], ids[2]]));
|
||||||
|
// the selection grows from its end, and the other way round from its start
|
||||||
|
assert!(arrow(&mut engine, false, true));
|
||||||
|
assert_eq!(engine.selection(), &tracks(&[ids[0], ids[1], ids[2]]));
|
||||||
|
// at the ends nothing happens
|
||||||
|
assert!(!arrow(&mut engine, false, false));
|
||||||
|
assert!(!arrow(&mut engine, true, true));
|
||||||
|
assert_eq!(engine.selection(), &tracks(&[ids[0], ids[1], ids[2]]));
|
||||||
|
// and there is nothing to move without selection
|
||||||
|
select_elements(&mut engine, Selection::Empty, SelectMode::Replace);
|
||||||
|
assert!(!arrow(&mut engine, true, false));
|
||||||
|
assert_eq!(engine.selection(), &Selection::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_arrow_select_does_not_create_undo_steps() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "a");
|
||||||
|
new(&mut engine, "b");
|
||||||
|
let a = engine.order()[0];
|
||||||
|
select_files(&mut engine, &[a]);
|
||||||
|
let before = engine.can_redo();
|
||||||
|
assert!(engine.execute(Action::ArrowSelect {
|
||||||
|
down: true,
|
||||||
|
add: false
|
||||||
|
}));
|
||||||
|
assert_eq!(engine.can_redo(), before);
|
||||||
|
assert_eq!(engine.selection(), &files(&[engine.order()[1]]));
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert_eq!(engine.order().len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,158 @@ impl Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Position of the first and last of the `selected` ids among the `all` ids.
|
||||||
|
fn selected_range<T: PartialEq>(
|
||||||
|
all: impl Iterator<Item = T>,
|
||||||
|
selected: &HashSet<T>,
|
||||||
|
) -> Option<(usize, usize)>
|
||||||
|
where
|
||||||
|
T: std::hash::Hash + Eq,
|
||||||
|
{
|
||||||
|
let mut range: Option<(usize, usize)> = None;
|
||||||
|
for (i, id) in all.enumerate() {
|
||||||
|
if selected.contains(&id) {
|
||||||
|
range = Some(range.map_or((i, i), |(first, _)| (first, i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
range
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The id before or after the selected ones among `ids`, without wrapping around.
|
||||||
|
fn neighbour<T: Copy + PartialEq + std::hash::Hash + Eq>(
|
||||||
|
ids: &[T],
|
||||||
|
selected: &HashSet<T>,
|
||||||
|
down: bool,
|
||||||
|
) -> Option<T> {
|
||||||
|
let (first, last) = selected_range(ids.iter().copied(), selected)?;
|
||||||
|
if down {
|
||||||
|
ids.get(last + 1).copied()
|
||||||
|
} else {
|
||||||
|
first.checked_sub(1).map(|i| ids[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selection {
|
||||||
|
/// The files holding the selected elements, as a selection of files.
|
||||||
|
pub fn to_files(&self) -> Selection {
|
||||||
|
match self {
|
||||||
|
Selection::Empty => Selection::Empty,
|
||||||
|
Selection::File { .. } => self.clone(),
|
||||||
|
Selection::Track { file_id, .. }
|
||||||
|
| Selection::TrackSegment { file_id, .. }
|
||||||
|
| Selection::Waypoints { file_id }
|
||||||
|
| Selection::Waypoint { file_id, .. } => Selection::File {
|
||||||
|
file_ids: [*file_id].into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects all the elements of the same kind as the selected ones, in the same place: all
|
||||||
|
/// the files, all the tracks of the file, all the segments of the track, all the waypoints
|
||||||
|
/// of the file. All the files when nothing is selected. `None` when there is nothing to
|
||||||
|
/// select (the waypoints node, or a file that does not exist).
|
||||||
|
pub fn all_at_level(&self, files: &StackEntry, order: &[FileId]) -> Option<Selection> {
|
||||||
|
match self {
|
||||||
|
Selection::Empty | Selection::File { .. } => Some(Selection::File {
|
||||||
|
file_ids: order.iter().copied().collect(),
|
||||||
|
}),
|
||||||
|
Selection::Track { file_id, .. } => Some(Selection::Track {
|
||||||
|
file_id: *file_id,
|
||||||
|
trk_ids: files.get(file_id)?.trk.iter().map(|trk| trk.id).collect(),
|
||||||
|
}),
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id, trk_id, ..
|
||||||
|
} => {
|
||||||
|
let trk = files
|
||||||
|
.get(file_id)?
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.find(|trk| trk.id == *trk_id)?;
|
||||||
|
Some(Selection::TrackSegment {
|
||||||
|
file_id: *file_id,
|
||||||
|
trk_id: *trk_id,
|
||||||
|
trkseg_ids: trk.trkseg.iter().map(|seg| seg.id).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Selection::Waypoints { .. } => None,
|
||||||
|
Selection::Waypoint { file_id, .. } => Some(Selection::Waypoint {
|
||||||
|
file_id: *file_id,
|
||||||
|
wpt_ids: files
|
||||||
|
.get(file_id)?
|
||||||
|
.wpt
|
||||||
|
.iter()
|
||||||
|
.flat_map(|chunk| &chunk.wpt)
|
||||||
|
.map(|wpt| wpt.id)
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The element next to the selected ones, as a selection of that one element: the next one
|
||||||
|
/// (`down`) or the previous one among the elements of the same kind, in the same place.
|
||||||
|
/// The tracks, segments and waypoints stop at their ends, the files wrap around and skip
|
||||||
|
/// the ones that are already selected. `None` when there is no such element.
|
||||||
|
pub fn neighbour(&self, files: &StackEntry, order: &[FileId], down: bool) -> Option<Selection> {
|
||||||
|
match self {
|
||||||
|
Selection::Empty | Selection::Waypoints { .. } => None,
|
||||||
|
Selection::File { file_ids } => {
|
||||||
|
let (first, last) = selected_range(order.iter(), &file_ids.iter().collect())?;
|
||||||
|
let limit = if down { last } else { first };
|
||||||
|
let n = order.len();
|
||||||
|
let mut i = limit;
|
||||||
|
loop {
|
||||||
|
i = if down { (i + 1) % n } else { (i + n - 1) % n };
|
||||||
|
if i == limit {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !file_ids.contains(&order[i]) {
|
||||||
|
return Some(Selection::File {
|
||||||
|
file_ids: [order[i]].into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Track { file_id, trk_ids } => {
|
||||||
|
let ids: Vec<TrackId> = files.get(file_id)?.trk.iter().map(|t| t.id).collect();
|
||||||
|
Some(Selection::Track {
|
||||||
|
file_id: *file_id,
|
||||||
|
trk_ids: [neighbour(&ids, trk_ids, down)?].into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids,
|
||||||
|
} => {
|
||||||
|
let trk = files
|
||||||
|
.get(file_id)?
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.find(|trk| trk.id == *trk_id)?;
|
||||||
|
let ids: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
|
||||||
|
Some(Selection::TrackSegment {
|
||||||
|
file_id: *file_id,
|
||||||
|
trk_id: *trk_id,
|
||||||
|
trkseg_ids: [neighbour(&ids, trkseg_ids, down)?].into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Selection::Waypoint { file_id, wpt_ids } => {
|
||||||
|
let ids: Vec<WaypointId> = files
|
||||||
|
.get(file_id)?
|
||||||
|
.wpt
|
||||||
|
.iter()
|
||||||
|
.flat_map(|chunk| &chunk.wpt)
|
||||||
|
.map(|wpt| wpt.id)
|
||||||
|
.collect();
|
||||||
|
Some(Selection::Waypoint {
|
||||||
|
file_id: *file_id,
|
||||||
|
wpt_ids: [neighbour(&ids, wpt_ids, down)?].into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -372,4 +524,236 @@ mod tests {
|
|||||||
selection.toggle(files(&[f1]));
|
selection.toggle(files(&[f1]));
|
||||||
assert_eq!(selection, files(&[f2]));
|
assert_eq!(selection, files(&[f2]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A file with 3 tracks of 2 segments each and 3 waypoints, with the ids of everything.
|
||||||
|
struct Tree {
|
||||||
|
files: StackEntry,
|
||||||
|
order: Vec<FileId>,
|
||||||
|
file: FileId,
|
||||||
|
tracks: Vec<TrackId>,
|
||||||
|
segments: Vec<Vec<TrackSegmentId>>,
|
||||||
|
waypoints: Vec<WaypointId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tree() -> Tree {
|
||||||
|
let mut file = File::default();
|
||||||
|
for _ in 0..3 {
|
||||||
|
file.trk.push(Track {
|
||||||
|
trkseg: vec![TrackSegment::default(), TrackSegment::default()],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
file.wpt.push(Rc::new(WaypointChunk {
|
||||||
|
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
file.wpt.push(Rc::new(WaypointChunk {
|
||||||
|
wpt: vec![Waypoint::default()],
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
let tree = Tree {
|
||||||
|
file: file.id,
|
||||||
|
tracks: file.trk.iter().map(|t| t.id).collect(),
|
||||||
|
segments: file
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.trkseg.iter().map(|s| s.id).collect())
|
||||||
|
.collect(),
|
||||||
|
waypoints: file
|
||||||
|
.wpt
|
||||||
|
.iter()
|
||||||
|
.flat_map(|chunk| &chunk.wpt)
|
||||||
|
.map(|w| w.id)
|
||||||
|
.collect(),
|
||||||
|
order: vec![file.id],
|
||||||
|
files: StackEntry::from([(file.id, Rc::new(file))]),
|
||||||
|
};
|
||||||
|
tree
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_at_level() {
|
||||||
|
let t = tree();
|
||||||
|
let other = FileId::default();
|
||||||
|
let order = [other, t.file];
|
||||||
|
let all = |selection: &Selection| selection.all_at_level(&t.files, &order);
|
||||||
|
|
||||||
|
// the files, in the order
|
||||||
|
let all_files = Selection::File {
|
||||||
|
file_ids: [other, t.file].into(),
|
||||||
|
};
|
||||||
|
assert_eq!(all(&Selection::Empty), Some(all_files.clone()));
|
||||||
|
assert_eq!(
|
||||||
|
all(&Selection::File {
|
||||||
|
file_ids: [t.file].into()
|
||||||
|
}),
|
||||||
|
Some(all_files)
|
||||||
|
);
|
||||||
|
|
||||||
|
// the siblings of the selected elements
|
||||||
|
assert_eq!(
|
||||||
|
all(&Selection::Track {
|
||||||
|
file_id: t.file,
|
||||||
|
trk_ids: [t.tracks[1]].into()
|
||||||
|
}),
|
||||||
|
Some(Selection::Track {
|
||||||
|
file_id: t.file,
|
||||||
|
trk_ids: t.tracks.iter().copied().collect()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all(&Selection::TrackSegment {
|
||||||
|
file_id: t.file,
|
||||||
|
trk_id: t.tracks[2],
|
||||||
|
trkseg_ids: [t.segments[2][0]].into()
|
||||||
|
}),
|
||||||
|
Some(Selection::TrackSegment {
|
||||||
|
file_id: t.file,
|
||||||
|
trk_id: t.tracks[2],
|
||||||
|
trkseg_ids: t.segments[2].iter().copied().collect()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all(&Selection::Waypoint {
|
||||||
|
file_id: t.file,
|
||||||
|
wpt_ids: [t.waypoints[0]].into()
|
||||||
|
}),
|
||||||
|
Some(Selection::Waypoint {
|
||||||
|
file_id: t.file,
|
||||||
|
wpt_ids: t.waypoints.iter().copied().collect()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// nothing to select
|
||||||
|
assert_eq!(all(&Selection::Waypoints { file_id: t.file }), None);
|
||||||
|
let unknown = FileId::default();
|
||||||
|
assert_eq!(
|
||||||
|
all(&Selection::Track {
|
||||||
|
file_id: unknown,
|
||||||
|
trk_ids: HashSet::new()
|
||||||
|
}),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_neighbour_of_tracks_stops_at_the_ends() {
|
||||||
|
let t = tree();
|
||||||
|
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||||
|
file_id: t.file,
|
||||||
|
trk_ids: ids.iter().copied().collect(),
|
||||||
|
};
|
||||||
|
let next = |selection: &Selection, down| selection.neighbour(&t.files, &t.order, down);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
next(&tracks(&[t.tracks[1]]), true),
|
||||||
|
Some(tracks(&[t.tracks[2]]))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
next(&tracks(&[t.tracks[1]]), false),
|
||||||
|
Some(tracks(&[t.tracks[0]]))
|
||||||
|
);
|
||||||
|
assert_eq!(next(&tracks(&[t.tracks[2]]), true), None);
|
||||||
|
assert_eq!(next(&tracks(&[t.tracks[0]]), false), None);
|
||||||
|
// from the end of the selected range, whatever its holes
|
||||||
|
let range = tracks(&[t.tracks[0], t.tracks[1]]);
|
||||||
|
assert_eq!(next(&range, true), Some(tracks(&[t.tracks[2]])));
|
||||||
|
assert_eq!(next(&range, false), None);
|
||||||
|
let holes = tracks(&[t.tracks[0], t.tracks[2]]);
|
||||||
|
assert_eq!(next(&holes, true), None);
|
||||||
|
assert_eq!(next(&holes, false), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_neighbour_of_segments_and_waypoints() {
|
||||||
|
let t = tree();
|
||||||
|
let segments = |trk: usize, seg: usize| Selection::TrackSegment {
|
||||||
|
file_id: t.file,
|
||||||
|
trk_id: t.tracks[trk],
|
||||||
|
trkseg_ids: [t.segments[trk][seg]].into(),
|
||||||
|
};
|
||||||
|
let next = |selection: &Selection, down| selection.neighbour(&t.files, &t.order, down);
|
||||||
|
// the segments stay in their track
|
||||||
|
assert_eq!(next(&segments(1, 0), true), Some(segments(1, 1)));
|
||||||
|
assert_eq!(next(&segments(1, 1), true), None);
|
||||||
|
assert_eq!(next(&segments(1, 1), false), Some(segments(1, 0)));
|
||||||
|
assert_eq!(next(&segments(1, 0), false), None);
|
||||||
|
|
||||||
|
// the waypoints are in the order of the file, across its chunks
|
||||||
|
let waypoint = |i: usize| Selection::Waypoint {
|
||||||
|
file_id: t.file,
|
||||||
|
wpt_ids: [t.waypoints[i]].into(),
|
||||||
|
};
|
||||||
|
assert_eq!(next(&waypoint(1), true), Some(waypoint(2)));
|
||||||
|
assert_eq!(next(&waypoint(2), true), None);
|
||||||
|
assert_eq!(next(&waypoint(0), false), None);
|
||||||
|
assert_eq!(next(&waypoint(2), false), Some(waypoint(1)));
|
||||||
|
|
||||||
|
assert_eq!(next(&Selection::Waypoints { file_id: t.file }, true), None);
|
||||||
|
assert_eq!(next(&Selection::Empty, true), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_neighbour_of_files_wraps_around_and_skips_the_selected() {
|
||||||
|
let files = StackEntry::default();
|
||||||
|
let [a, b, c] = [FileId::default(), FileId::default(), FileId::default()];
|
||||||
|
let order = [a, b, c];
|
||||||
|
let select = |ids: &[FileId]| Selection::File {
|
||||||
|
file_ids: ids.iter().copied().collect(),
|
||||||
|
};
|
||||||
|
let next = |ids: &[FileId], down| select(ids).neighbour(&files, &order, down);
|
||||||
|
|
||||||
|
assert_eq!(next(&[a], true), Some(select(&[b])));
|
||||||
|
assert_eq!(next(&[c], true), Some(select(&[a])));
|
||||||
|
assert_eq!(next(&[a], false), Some(select(&[c])));
|
||||||
|
assert_eq!(next(&[b], false), Some(select(&[a])));
|
||||||
|
// from the end of the selection, skipping the files already selected
|
||||||
|
assert_eq!(next(&[a, b], true), Some(select(&[c])));
|
||||||
|
assert_eq!(next(&[a, b], false), Some(select(&[c])));
|
||||||
|
assert_eq!(next(&[c, a], true), Some(select(&[b])));
|
||||||
|
// everything is selected
|
||||||
|
assert_eq!(next(&[a, b, c], true), None);
|
||||||
|
// none of them is in the order
|
||||||
|
assert_eq!(next(&[FileId::default()], true), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_files() {
|
||||||
|
let (file_id, trk_id) = (FileId::default(), TrackId::default());
|
||||||
|
let file = Selection::File {
|
||||||
|
file_ids: [file_id].into(),
|
||||||
|
};
|
||||||
|
assert_eq!(Selection::Empty.to_files(), Selection::Empty);
|
||||||
|
assert_eq!(file.to_files(), file);
|
||||||
|
assert_eq!(
|
||||||
|
Selection::Track {
|
||||||
|
file_id,
|
||||||
|
trk_ids: [trk_id].into()
|
||||||
|
}
|
||||||
|
.to_files(),
|
||||||
|
file
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids: HashSet::new()
|
||||||
|
}
|
||||||
|
.to_files(),
|
||||||
|
file
|
||||||
|
);
|
||||||
|
assert_eq!(Selection::Waypoints { file_id }.to_files(), file);
|
||||||
|
assert_eq!(
|
||||||
|
Selection::Waypoint {
|
||||||
|
file_id,
|
||||||
|
wpt_ids: HashSet::new()
|
||||||
|
}
|
||||||
|
.to_files(),
|
||||||
|
file
|
||||||
|
);
|
||||||
|
let several = Selection::File {
|
||||||
|
file_ids: [file_id, FileId::default()].into(),
|
||||||
|
};
|
||||||
|
assert_eq!(several.to_files(), several);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-2
@@ -182,9 +182,11 @@ pub fn duplicate() -> bool {
|
|||||||
edit(Command::Duplicate(engine::Duplicate))
|
edit(Command::Duplicate(engine::Duplicate))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements are
|
||||||
|
/// deleted instead, even if only a track or a waypoint is selected.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn delete() -> bool {
|
pub fn delete(whole_files: bool) -> bool {
|
||||||
edit(Command::Delete(engine::Delete))
|
edit(Command::Delete(engine::Delete { whole_files }))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -346,6 +348,16 @@ pub fn redo() -> bool {
|
|||||||
execute(Action::Redo)
|
execute(Action::Redo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn can_undo() -> bool {
|
||||||
|
with_engine(|e| e.can_undo()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn can_redo() -> bool {
|
||||||
|
with_engine(|e| e.can_redo()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
|
|
||||||
/// Selects files. `file_ids_bytes`: concatenated 16-byte UUIDs. Unknown files are ignored, and
|
/// Selects files. `file_ids_bytes`: concatenated 16-byte UUIDs. Unknown files are ignored, and
|
||||||
@@ -360,6 +372,22 @@ pub fn select(file_ids_bytes: &[u8], mode: SelectMode) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Selects all the elements of the same kind as the selected ones, in the same place: all the
|
||||||
|
/// files, the tracks of the file, the segments of the track, the waypoints of the file. All the
|
||||||
|
/// files when nothing is selected.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn select_all() -> bool {
|
||||||
|
execute(Action::SelectAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the selection to the next (`down`) or previous element of the same kind, like the arrow
|
||||||
|
/// keys do in the file list. With `add` (shift + arrow), the element is added to the selection,
|
||||||
|
/// otherwise it replaces it.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn arrow_select(down: bool, add: bool) -> bool {
|
||||||
|
execute(Action::ArrowSelect { down, add })
|
||||||
|
}
|
||||||
|
|
||||||
// The elements below cross as the id of their file (UUID string) and, like the files, as
|
// The elements below cross as the id of their file (UUID string) and, like the files, as
|
||||||
// concatenated 16-byte UUIDs. What does not exist is ignored. See `SelectMode`: elements are merged
|
// concatenated 16-byte UUIDs. What does not exist is ignored. See `SelectMode`: elements are merged
|
||||||
// with the selection when it holds elements of the same kind in the same place (same file, same
|
// with the selection when it holds elements of the same kind in the same place (same file, same
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
import { anySelectedLayer } from '$lib/components/map/layer-control/utils';
|
import { anySelectedLayer } from '$lib/components/map/layer-control/utils';
|
||||||
import { defaultOverlays } from '$lib/assets/layers';
|
import { defaultOverlays } from '$lib/assets/layers';
|
||||||
import LayerControlSettings from '$lib/components/map/layer-control/LayerControlSettings.svelte';
|
import LayerControlSettings from '$lib/components/map/layer-control/LayerControlSettings.svelte';
|
||||||
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/file-list';
|
import { ListLevel } from '$lib/components/file-list/file-list';
|
||||||
import Export from '$lib/components/export/Export.svelte';
|
import Export from '$lib/components/export/Export.svelte';
|
||||||
import { mode, setMode } from 'mode-watcher';
|
import { mode, setMode } from 'mode-watcher';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
@@ -62,17 +62,17 @@
|
|||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import {
|
import {
|
||||||
createFile,
|
createFile,
|
||||||
fileActions,
|
|
||||||
loadFiles,
|
loadFiles,
|
||||||
pasteSelection,
|
pasteSelection,
|
||||||
triggerFileInput,
|
triggerFileInput,
|
||||||
} from '$lib/logic/file-actions';
|
} from '$lib/logic/file-actions';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { engine } from '$lib/engine';
|
||||||
import { fileActionManager } from '$lib/logic/file-action-manager';
|
import { allHidden } from '$lib/all-hidden';
|
||||||
import { copied, selection } from '$lib/logic/selection';
|
import { selectionSize } from '$lib/selection-helpers';
|
||||||
import { allHidden } from '$lib/logic/hidden';
|
// TODO the clipboard and centering on the selection still work on the previous implementation
|
||||||
|
import { copied, selection as oldSelection } from '$lib/logic/selection';
|
||||||
import { boundsManager } from '$lib/logic/bounds';
|
import { boundsManager } from '$lib/logic/bounds';
|
||||||
import { tick, onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { allowedPastes } from '$lib/components/file-list/sortable-file-list';
|
import { allowedPastes } from '$lib/components/file-list/sortable-file-list';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -91,8 +91,22 @@
|
|||||||
routing,
|
routing,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
const canUndo = fileActionManager.canUndo;
|
const { files, selection, canUndo, canRedo } = engine;
|
||||||
const canRedo = fileActionManager.canRedo;
|
|
||||||
|
let selectionCount = $derived(selectionSize($selection));
|
||||||
|
let noFiles = $derived($files.size === 0);
|
||||||
|
// metadata and style can be edited for files and tracks only
|
||||||
|
let filesOrTracksSelected = $derived($selection.type === 'file' || $selection.type === 'track');
|
||||||
|
let selectionLevel = $derived(
|
||||||
|
{
|
||||||
|
empty: undefined,
|
||||||
|
file: ListLevel.FILE,
|
||||||
|
track: ListLevel.TRACK,
|
||||||
|
segment: ListLevel.SEGMENT,
|
||||||
|
waypoints: ListLevel.WAYPOINTS,
|
||||||
|
waypoint: ListLevel.WAYPOINT,
|
||||||
|
}[$selection.type]
|
||||||
|
);
|
||||||
|
|
||||||
function switchBasemaps() {
|
function switchBasemaps() {
|
||||||
[$currentBasemap, $previousBasemap] = [$previousBasemap, $currentBasemap];
|
[$currentBasemap, $previousBasemap] = [$previousBasemap, $currentBasemap];
|
||||||
@@ -153,27 +167,21 @@
|
|||||||
<Shortcut key="O" ctrl={true} />
|
<Shortcut key="O" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item onclick={() => engine.duplicate()} disabled={selectionCount == 0}>
|
||||||
onclick={fileActions.duplicateSelection}
|
|
||||||
disabled={$selection.size == 0}
|
|
||||||
>
|
|
||||||
<Copy size="16" />
|
<Copy size="16" />
|
||||||
{i18n._('menu.duplicate')}
|
{i18n._('menu.duplicate')}
|
||||||
<Shortcut key="D" ctrl={true} />
|
<Shortcut key="D" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() => tick().then(fileActions.deleteSelectedFiles)}
|
onclick={() => engine.delete(true)}
|
||||||
disabled={$selection.size == 0}
|
disabled={selectionCount == 0}
|
||||||
>
|
>
|
||||||
<FileX size="16" />
|
<FileX size="16" />
|
||||||
{i18n._('menu.delete')}
|
{i18n._('menu.delete')}
|
||||||
<Shortcut key="⌫" ctrl={true} />
|
<Shortcut key="⌫" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item
|
<Menubar.Item onclick={() => engine.deleteAll()} disabled={noFiles}>
|
||||||
onclick={fileActions.deleteAllFiles}
|
|
||||||
disabled={fileStateCollection.size == 0}
|
|
||||||
>
|
|
||||||
<FileX size="16" />
|
<FileX size="16" />
|
||||||
{i18n._('menu.delete_all')}
|
{i18n._('menu.delete_all')}
|
||||||
<Shortcut key="⌫" ctrl={true} shift={true} />
|
<Shortcut key="⌫" ctrl={true} shift={true} />
|
||||||
@@ -181,7 +189,7 @@
|
|||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() => (exportState.current = ExportState.SELECTION)}
|
onclick={() => (exportState.current = ExportState.SELECTION)}
|
||||||
disabled={$selection.size == 0}
|
disabled={selectionCount == 0}
|
||||||
>
|
>
|
||||||
<Download size="16" />
|
<Download size="16" />
|
||||||
{i18n._('menu.export')}
|
{i18n._('menu.export')}
|
||||||
@@ -189,7 +197,7 @@
|
|||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() => (exportState.current = ExportState.ALL)}
|
onclick={() => (exportState.current = ExportState.ALL)}
|
||||||
disabled={fileStateCollection.size == 0}
|
disabled={noFiles}
|
||||||
>
|
>
|
||||||
<Download size="16" />
|
<Download size="16" />
|
||||||
{i18n._('menu.export_all')}
|
{i18n._('menu.export_all')}
|
||||||
@@ -203,26 +211,19 @@
|
|||||||
<span class="hidden md:block">{i18n._('menu.edit')}</span>
|
<span class="hidden md:block">{i18n._('menu.edit')}</span>
|
||||||
</Menubar.Trigger>
|
</Menubar.Trigger>
|
||||||
<Menubar.Content class="border-none">
|
<Menubar.Content class="border-none">
|
||||||
<Menubar.Item onclick={() => fileActionManager.undo()} disabled={!$canUndo}>
|
<Menubar.Item onclick={() => engine.undo()} disabled={!$canUndo}>
|
||||||
<Undo2 size="16" />
|
<Undo2 size="16" />
|
||||||
{i18n._('menu.undo')}
|
{i18n._('menu.undo')}
|
||||||
<Shortcut key="Z" ctrl={true} />
|
<Shortcut key="Z" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item onclick={() => fileActionManager.redo()} disabled={!$canRedo}>
|
<Menubar.Item onclick={() => engine.redo()} disabled={!$canRedo}>
|
||||||
<Redo2 size="16" />
|
<Redo2 size="16" />
|
||||||
{i18n._('menu.redo')}
|
{i18n._('menu.redo')}
|
||||||
<Shortcut key="Z" ctrl={true} shift={true} />
|
<Shortcut key="Z" ctrl={true} shift={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
disabled={$selection.size !== 1 ||
|
disabled={selectionCount !== 1 || !filesOrTracksSelected}
|
||||||
!$selection
|
|
||||||
.getSelected()
|
|
||||||
.every(
|
|
||||||
(item) =>
|
|
||||||
item instanceof ListFileItem ||
|
|
||||||
item instanceof ListTrackItem
|
|
||||||
)}
|
|
||||||
onclick={() => (editMetadata.current = true)}
|
onclick={() => (editMetadata.current = true)}
|
||||||
>
|
>
|
||||||
<Info size="16" />
|
<Info size="16" />
|
||||||
@@ -230,28 +231,15 @@
|
|||||||
<Shortcut key="I" ctrl={true} />
|
<Shortcut key="I" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
disabled={$selection.size === 0 ||
|
disabled={selectionCount === 0 || !filesOrTracksSelected}
|
||||||
!$selection
|
|
||||||
.getSelected()
|
|
||||||
.every(
|
|
||||||
(item) =>
|
|
||||||
item instanceof ListFileItem ||
|
|
||||||
item instanceof ListTrackItem
|
|
||||||
)}
|
|
||||||
onclick={() => (editStyle.current = true)}
|
onclick={() => (editStyle.current = true)}
|
||||||
>
|
>
|
||||||
<PaintBucket size="16" />
|
<PaintBucket size="16" />
|
||||||
{i18n._('menu.style.button')}
|
{i18n._('menu.style.button')}
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() => {
|
onclick={() => engine.setSelectionHidden(!$allHidden)}
|
||||||
if ($allHidden) {
|
disabled={selectionCount == 0}
|
||||||
fileActions.setHiddenToSelection(false);
|
|
||||||
} else {
|
|
||||||
fileActions.setHiddenToSelection(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={$selection.size == 0}
|
|
||||||
>
|
>
|
||||||
{#if $allHidden}
|
{#if $allHidden}
|
||||||
<Eye size="16" />
|
<Eye size="16" />
|
||||||
@@ -263,31 +251,20 @@
|
|||||||
<Shortcut key="H" ctrl={true} />
|
<Shortcut key="H" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
{#if $treeFileView}
|
{#if $treeFileView}
|
||||||
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
|
{#if $selection.type === 'file'}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() =>
|
onclick={() => engine.newTrack()}
|
||||||
fileActions.addNewTrack(
|
disabled={selectionCount !== 1}
|
||||||
$selection.getSelected()[0].getFileId()
|
|
||||||
)}
|
|
||||||
disabled={$selection.size !== 1}
|
|
||||||
>
|
>
|
||||||
<Plus size="16" />
|
<Plus size="16" />
|
||||||
{i18n._('menu.new_track')}
|
{i18n._('menu.new_track')}
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
{:else if $selection
|
{:else if $selection.type === 'track'}
|
||||||
.getSelected()
|
|
||||||
.some((item) => item instanceof ListTrackItem)}
|
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() => {
|
onclick={() => engine.newTrackSegment()}
|
||||||
let item = $selection.getSelected()[0];
|
disabled={selectionCount !== 1}
|
||||||
fileActions.addNewSegment(
|
|
||||||
item.getFileId(),
|
|
||||||
item.getTrackIndex()
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
disabled={$selection.size !== 1}
|
|
||||||
>
|
>
|
||||||
<Plus size="16" />
|
<Plus size="16" />
|
||||||
{i18n._('menu.new_segment')}
|
{i18n._('menu.new_segment')}
|
||||||
@@ -295,21 +272,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item onclick={() => engine.selectAll()} disabled={noFiles}>
|
||||||
onclick={() => selection.selectAll()}
|
|
||||||
disabled={fileStateCollection.size == 0}
|
|
||||||
>
|
|
||||||
<FileStack size="16" />
|
<FileStack size="16" />
|
||||||
{i18n._('menu.select_all')}
|
{i18n._('menu.select_all')}
|
||||||
<Shortcut key="A" ctrl={true} />
|
<Shortcut key="A" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if ($selection.size > 0) {
|
if (selectionCount > 0) {
|
||||||
boundsManager.centerMapOnSelection();
|
boundsManager.centerMapOnSelection();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={$selection.size == 0}
|
disabled={selectionCount == 0}
|
||||||
>
|
>
|
||||||
<Maximize size="16" />
|
<Maximize size="16" />
|
||||||
{i18n._('menu.center')}
|
{i18n._('menu.center')}
|
||||||
@@ -318,16 +292,16 @@
|
|||||||
{#if $treeFileView}
|
{#if $treeFileView}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() => selection.copySelection()}
|
onclick={() => oldSelection.copySelection()}
|
||||||
disabled={$selection.size === 0}
|
disabled={selectionCount === 0}
|
||||||
>
|
>
|
||||||
<ClipboardCopy size="16" />
|
<ClipboardCopy size="16" />
|
||||||
{i18n._('menu.copy')}
|
{i18n._('menu.copy')}
|
||||||
<Shortcut key="C" ctrl={true} />
|
<Shortcut key="C" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
onclick={() => selection.cutSelection()}
|
onclick={() => oldSelection.cutSelection()}
|
||||||
disabled={$selection.size === 0}
|
disabled={selectionCount === 0}
|
||||||
>
|
>
|
||||||
<Scissors size="16" />
|
<Scissors size="16" />
|
||||||
{i18n._('menu.cut')}
|
{i18n._('menu.cut')}
|
||||||
@@ -336,10 +310,8 @@
|
|||||||
<Menubar.Item
|
<Menubar.Item
|
||||||
disabled={$copied === undefined ||
|
disabled={$copied === undefined ||
|
||||||
$copied.length === 0 ||
|
$copied.length === 0 ||
|
||||||
($selection.size > 0 &&
|
(selectionLevel !== undefined &&
|
||||||
!allowedPastes[$copied[0].level].includes(
|
!allowedPastes[$copied[0].level].includes(selectionLevel))}
|
||||||
$selection.getSelected().pop()!.level
|
|
||||||
))}
|
|
||||||
onclick={pasteSelection}
|
onclick={pasteSelection}
|
||||||
>
|
>
|
||||||
<ClipboardPaste size="16" />
|
<ClipboardPaste size="16" />
|
||||||
@@ -348,10 +320,7 @@
|
|||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
{/if}
|
{/if}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item onclick={() => engine.delete()} disabled={selectionCount == 0}>
|
||||||
onclick={() => tick().then(fileActions.deleteSelection)}
|
|
||||||
disabled={$selection.size == 0}
|
|
||||||
>
|
|
||||||
<Trash2 size="16" />
|
<Trash2 size="16" />
|
||||||
{i18n._('menu.delete')}
|
{i18n._('menu.delete')}
|
||||||
<Shortcut key="⌫" ctrl={true} />
|
<Shortcut key="⌫" ctrl={true} />
|
||||||
@@ -585,16 +554,16 @@
|
|||||||
triggerFileInput();
|
triggerFileInput();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
|
||||||
fileActions.duplicateSelection();
|
engine.duplicate();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
||||||
if (!targetInput) {
|
if (!targetInput) {
|
||||||
selection.copySelection();
|
oldSelection.copySelection();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
} else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) {
|
||||||
if (!targetInput) {
|
if (!targetInput) {
|
||||||
selection.cutSelection();
|
oldSelection.cutSelection();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
|
||||||
@@ -604,41 +573,36 @@
|
|||||||
}
|
}
|
||||||
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
|
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
if (fileStateCollection.size > 0) {
|
if (!noFiles) {
|
||||||
exportState.current = ExportState.ALL;
|
exportState.current = ExportState.ALL;
|
||||||
}
|
}
|
||||||
} else if ($selection.size > 0) {
|
} else if (selectionCount > 0) {
|
||||||
exportState.current = ExportState.SELECTION;
|
exportState.current = ExportState.SELECTION;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
|
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
fileActionManager.redo();
|
engine.redo();
|
||||||
} else {
|
} else {
|
||||||
fileActionManager.undo();
|
engine.undo();
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
|
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
|
||||||
if (!targetInput) {
|
if (!targetInput) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
fileActions.deleteAllFiles();
|
engine.deleteAll();
|
||||||
} else {
|
} else {
|
||||||
fileActions.deleteSelection();
|
engine.delete();
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
||||||
if (!targetInput) {
|
if (!targetInput) {
|
||||||
selection.selectAll();
|
engine.selectAll();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
} else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) {
|
||||||
if (
|
if (selectionCount === 1 && filesOrTracksSelected) {
|
||||||
$selection.size === 1 &&
|
|
||||||
$selection
|
|
||||||
.getSelected()
|
|
||||||
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
|
|
||||||
) {
|
|
||||||
editMetadata.current = true;
|
editMetadata.current = true;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -649,14 +613,10 @@
|
|||||||
$treeFileView = !$treeFileView;
|
$treeFileView = !$treeFileView;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
|
||||||
if ($allHidden) {
|
engine.setSelectionHidden(!$allHidden);
|
||||||
fileActions.setHiddenToSelection(false);
|
|
||||||
} else {
|
|
||||||
fileActions.setHiddenToSelection(true);
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||||
if ($selection.size > 0) {
|
if (selectionCount > 0) {
|
||||||
boundsManager.centerMapOnSelection();
|
boundsManager.centerMapOnSelection();
|
||||||
}
|
}
|
||||||
} else if (e.key === 'F1') {
|
} else if (e.key === 'F1') {
|
||||||
@@ -681,10 +641,7 @@
|
|||||||
e.key === 'ArrowUp'
|
e.key === 'ArrowUp'
|
||||||
) {
|
) {
|
||||||
if (!targetInput) {
|
if (!targetInput) {
|
||||||
selection.updateFromKey(
|
engine.arrowSelect(e.key === 'ArrowRight' || e.key === 'ArrowDown', e.shiftKey);
|
||||||
e.key === 'ArrowRight' || e.key === 'ArrowDown',
|
|
||||||
e.shiftKey
|
|
||||||
);
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,7 +260,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if level !== ListLevel.WAYPOINTS}
|
{#if level !== ListLevel.WAYPOINTS}
|
||||||
<ContextMenu.Item onclick={() => engine.selectAllSiblings(node)}>
|
<ContextMenu.Item onclick={() => engine.selectAll()}>
|
||||||
<FileStack size="16" />
|
<FileStack size="16" />
|
||||||
{i18n._('menu.select_all')}
|
{i18n._('menu.select_all')}
|
||||||
<Shortcut key="A" ctrl={true} />
|
<Shortcut key="A" ctrl={true} />
|
||||||
|
|||||||
+27
-33
@@ -197,6 +197,8 @@ class Engine {
|
|||||||
private _visibility = new Map<string, Visibility>();
|
private _visibility = new Map<string, Visibility>();
|
||||||
private _files = writable<Map<string, Writable<FileState>>>(new Map());
|
private _files = writable<Map<string, Writable<FileState>>>(new Map());
|
||||||
private _selection = writable<Selection>({ type: 'empty' });
|
private _selection = writable<Selection>({ type: 'empty' });
|
||||||
|
private _canUndo = writable(false);
|
||||||
|
private _canRedo = writable(false);
|
||||||
private _statistics = writable<SelectionStatistics>(EMPTY_SELECTION_STATISTICS);
|
private _statistics = writable<SelectionStatistics>(EMPTY_SELECTION_STATISTICS);
|
||||||
/** Identifies the statistics currently in the engine's buffers. */
|
/** Identifies the statistics currently in the engine's buffers. */
|
||||||
private _statisticsVersion = 0;
|
private _statisticsVersion = 0;
|
||||||
@@ -205,6 +207,8 @@ class Engine {
|
|||||||
readonly order: Readable<string[]> = { subscribe: this._order.subscribe };
|
readonly order: Readable<string[]> = { subscribe: this._order.subscribe };
|
||||||
/** Statistics of what is currently selected. */
|
/** Statistics of what is currently selected. */
|
||||||
readonly statistics: Readable<SelectionStatistics> = { subscribe: this._statistics.subscribe };
|
readonly statistics: Readable<SelectionStatistics> = { subscribe: this._statistics.subscribe };
|
||||||
|
readonly canUndo: Readable<boolean> = { subscribe: this._canUndo.subscribe };
|
||||||
|
readonly canRedo: Readable<boolean> = { subscribe: this._canRedo.subscribe };
|
||||||
/** What is currently selected. */
|
/** What is currently selected. */
|
||||||
readonly selection: Readable<Selection> = { subscribe: this._selection.subscribe };
|
readonly selection: Readable<Selection> = { subscribe: this._selection.subscribe };
|
||||||
/**
|
/**
|
||||||
@@ -239,8 +243,12 @@ class Engine {
|
|||||||
return this.run((w) => w.duplicate());
|
return this.run((w) => w.duplicate());
|
||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
/**
|
||||||
return this.run((w) => w._delete());
|
* Deletes the selected elements. With `wholeFiles`, the files holding the selected elements
|
||||||
|
* are deleted instead, even if only a track or a waypoint is selected.
|
||||||
|
*/
|
||||||
|
delete(wholeFiles = false) {
|
||||||
|
return this.run((w) => w._delete(wholeFiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAll() {
|
deleteAll() {
|
||||||
@@ -263,8 +271,22 @@ class Engine {
|
|||||||
return this.run((w) => w.select(idsToBytes(fileIds), selectMode(w, mode)));
|
return this.run((w) => w.select(idsToBytes(fileIds), selectMode(w, mode)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects all the elements of the same kind as the selected ones, in the same place: all the
|
||||||
|
* files, the tracks of the file, the segments of the track, the waypoints of the file. All
|
||||||
|
* the files when nothing is selected.
|
||||||
|
*/
|
||||||
selectAll() {
|
selectAll() {
|
||||||
return this.select(get(this._order));
|
return this.run((w) => w.select_all());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the selection to the next (`down`) or previous element of the same kind, like the
|
||||||
|
* arrow keys do. With `add` (shift + arrow), the element is added to the selection, otherwise
|
||||||
|
* it replaces it.
|
||||||
|
*/
|
||||||
|
arrowSelect(down: boolean, add: boolean) {
|
||||||
|
return this.run((w) => w.arrow_select(down, add));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selecting elements inside a file. Ids that do not exist are ignored.
|
// Selecting elements inside a file. Ids that do not exist are ignored.
|
||||||
@@ -316,36 +338,6 @@ class Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selects the node and its siblings (all the files, the tracks of a file, and so on). */
|
|
||||||
selectAllSiblings(node: FileTreeNode) {
|
|
||||||
const state = get(this._files).get(node.fileId);
|
|
||||||
if (node.type === 'file' || !state) {
|
|
||||||
return this.selectAll();
|
|
||||||
}
|
|
||||||
const { tracks, waypoints } = get(state).structure;
|
|
||||||
switch (node.type) {
|
|
||||||
case 'track':
|
|
||||||
return this.selectTracks(
|
|
||||||
node.fileId,
|
|
||||||
tracks.map((track) => track.id)
|
|
||||||
);
|
|
||||||
case 'segment':
|
|
||||||
return this.selectSegments(
|
|
||||||
node.fileId,
|
|
||||||
node.trackId,
|
|
||||||
tracks.find((track) => track.id === node.trackId)?.segments.map((s) => s.id) ??
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
case 'waypoint':
|
|
||||||
return this.selectWaypoints(
|
|
||||||
node.fileId,
|
|
||||||
waypoints.map((waypoint) => waypoint.id)
|
|
||||||
);
|
|
||||||
case 'waypoints':
|
|
||||||
return this.selectWaypointGroup(node.fileId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edits of the selection
|
// Edits of the selection
|
||||||
|
|
||||||
newTrack() {
|
newTrack() {
|
||||||
@@ -414,6 +406,8 @@ class Engine {
|
|||||||
/** Reads what the last action changed. */
|
/** Reads what the last action changed. */
|
||||||
private sync(wasm: Wasm) {
|
private sync(wasm: Wasm) {
|
||||||
const update = wasm.last_update();
|
const update = wasm.last_update();
|
||||||
|
this._canUndo.set(wasm.can_undo());
|
||||||
|
this._canRedo.set(wasm.can_redo());
|
||||||
if (update.selectionChanged) {
|
if (update.selectionChanged) {
|
||||||
this._selection.set(wasm.selection());
|
this._selection.set(wasm.selection());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user