mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-07 06:14:07 +00:00
progress
This commit is contained in:
@@ -53,7 +53,7 @@
|
||||
import { anySelectedLayer } from '$lib/components/map/layer-control/utils';
|
||||
import { defaultOverlays } from '$lib/assets/layers';
|
||||
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 { mode, setMode } from 'mode-watcher';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
@@ -62,17 +62,17 @@
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import {
|
||||
createFile,
|
||||
fileActions,
|
||||
loadFiles,
|
||||
pasteSelection,
|
||||
triggerFileInput,
|
||||
} from '$lib/logic/file-actions';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { fileActionManager } from '$lib/logic/file-action-manager';
|
||||
import { copied, selection } from '$lib/logic/selection';
|
||||
import { allHidden } from '$lib/logic/hidden';
|
||||
import { engine } from '$lib/engine';
|
||||
import { allHidden } from '$lib/all-hidden';
|
||||
import { selectionSize } from '$lib/selection-helpers';
|
||||
// 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 { tick, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { allowedPastes } from '$lib/components/file-list/sortable-file-list';
|
||||
|
||||
const {
|
||||
@@ -91,8 +91,22 @@
|
||||
routing,
|
||||
} = settings;
|
||||
|
||||
const canUndo = fileActionManager.canUndo;
|
||||
const canRedo = fileActionManager.canRedo;
|
||||
const { files, selection, canUndo, canRedo } = engine;
|
||||
|
||||
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() {
|
||||
[$currentBasemap, $previousBasemap] = [$previousBasemap, $currentBasemap];
|
||||
@@ -153,27 +167,21 @@
|
||||
<Shortcut key="O" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
onclick={fileActions.duplicateSelection}
|
||||
disabled={$selection.size == 0}
|
||||
>
|
||||
<Menubar.Item onclick={() => engine.duplicate()} disabled={selectionCount == 0}>
|
||||
<Copy size="16" />
|
||||
{i18n._('menu.duplicate')}
|
||||
<Shortcut key="D" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
onclick={() => tick().then(fileActions.deleteSelectedFiles)}
|
||||
disabled={$selection.size == 0}
|
||||
onclick={() => engine.delete(true)}
|
||||
disabled={selectionCount == 0}
|
||||
>
|
||||
<FileX size="16" />
|
||||
{i18n._('menu.delete')}
|
||||
<Shortcut key="⌫" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
onclick={fileActions.deleteAllFiles}
|
||||
disabled={fileStateCollection.size == 0}
|
||||
>
|
||||
<Menubar.Item onclick={() => engine.deleteAll()} disabled={noFiles}>
|
||||
<FileX size="16" />
|
||||
{i18n._('menu.delete_all')}
|
||||
<Shortcut key="⌫" ctrl={true} shift={true} />
|
||||
@@ -181,7 +189,7 @@
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
onclick={() => (exportState.current = ExportState.SELECTION)}
|
||||
disabled={$selection.size == 0}
|
||||
disabled={selectionCount == 0}
|
||||
>
|
||||
<Download size="16" />
|
||||
{i18n._('menu.export')}
|
||||
@@ -189,7 +197,7 @@
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
onclick={() => (exportState.current = ExportState.ALL)}
|
||||
disabled={fileStateCollection.size == 0}
|
||||
disabled={noFiles}
|
||||
>
|
||||
<Download size="16" />
|
||||
{i18n._('menu.export_all')}
|
||||
@@ -203,26 +211,19 @@
|
||||
<span class="hidden md:block">{i18n._('menu.edit')}</span>
|
||||
</Menubar.Trigger>
|
||||
<Menubar.Content class="border-none">
|
||||
<Menubar.Item onclick={() => fileActionManager.undo()} disabled={!$canUndo}>
|
||||
<Menubar.Item onclick={() => engine.undo()} disabled={!$canUndo}>
|
||||
<Undo2 size="16" />
|
||||
{i18n._('menu.undo')}
|
||||
<Shortcut key="Z" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Item onclick={() => fileActionManager.redo()} disabled={!$canRedo}>
|
||||
<Menubar.Item onclick={() => engine.redo()} disabled={!$canRedo}>
|
||||
<Redo2 size="16" />
|
||||
{i18n._('menu.redo')}
|
||||
<Shortcut key="Z" ctrl={true} shift={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
disabled={$selection.size !== 1 ||
|
||||
!$selection
|
||||
.getSelected()
|
||||
.every(
|
||||
(item) =>
|
||||
item instanceof ListFileItem ||
|
||||
item instanceof ListTrackItem
|
||||
)}
|
||||
disabled={selectionCount !== 1 || !filesOrTracksSelected}
|
||||
onclick={() => (editMetadata.current = true)}
|
||||
>
|
||||
<Info size="16" />
|
||||
@@ -230,28 +231,15 @@
|
||||
<Shortcut key="I" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
disabled={$selection.size === 0 ||
|
||||
!$selection
|
||||
.getSelected()
|
||||
.every(
|
||||
(item) =>
|
||||
item instanceof ListFileItem ||
|
||||
item instanceof ListTrackItem
|
||||
)}
|
||||
disabled={selectionCount === 0 || !filesOrTracksSelected}
|
||||
onclick={() => (editStyle.current = true)}
|
||||
>
|
||||
<PaintBucket size="16" />
|
||||
{i18n._('menu.style.button')}
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
onclick={() => {
|
||||
if ($allHidden) {
|
||||
fileActions.setHiddenToSelection(false);
|
||||
} else {
|
||||
fileActions.setHiddenToSelection(true);
|
||||
}
|
||||
}}
|
||||
disabled={$selection.size == 0}
|
||||
onclick={() => engine.setSelectionHidden(!$allHidden)}
|
||||
disabled={selectionCount == 0}
|
||||
>
|
||||
{#if $allHidden}
|
||||
<Eye size="16" />
|
||||
@@ -263,31 +251,20 @@
|
||||
<Shortcut key="H" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
{#if $treeFileView}
|
||||
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
|
||||
{#if $selection.type === 'file'}
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
onclick={() =>
|
||||
fileActions.addNewTrack(
|
||||
$selection.getSelected()[0].getFileId()
|
||||
)}
|
||||
disabled={$selection.size !== 1}
|
||||
onclick={() => engine.newTrack()}
|
||||
disabled={selectionCount !== 1}
|
||||
>
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new_track')}
|
||||
</Menubar.Item>
|
||||
{:else if $selection
|
||||
.getSelected()
|
||||
.some((item) => item instanceof ListTrackItem)}
|
||||
{:else if $selection.type === 'track'}
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
onclick={() => {
|
||||
let item = $selection.getSelected()[0];
|
||||
fileActions.addNewSegment(
|
||||
item.getFileId(),
|
||||
item.getTrackIndex()
|
||||
);
|
||||
}}
|
||||
disabled={$selection.size !== 1}
|
||||
onclick={() => engine.newTrackSegment()}
|
||||
disabled={selectionCount !== 1}
|
||||
>
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new_segment')}
|
||||
@@ -295,21 +272,18 @@
|
||||
{/if}
|
||||
{/if}
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
onclick={() => selection.selectAll()}
|
||||
disabled={fileStateCollection.size == 0}
|
||||
>
|
||||
<Menubar.Item onclick={() => engine.selectAll()} disabled={noFiles}>
|
||||
<FileStack size="16" />
|
||||
{i18n._('menu.select_all')}
|
||||
<Shortcut key="A" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
onclick={() => {
|
||||
if ($selection.size > 0) {
|
||||
if (selectionCount > 0) {
|
||||
boundsManager.centerMapOnSelection();
|
||||
}
|
||||
}}
|
||||
disabled={$selection.size == 0}
|
||||
disabled={selectionCount == 0}
|
||||
>
|
||||
<Maximize size="16" />
|
||||
{i18n._('menu.center')}
|
||||
@@ -318,16 +292,16 @@
|
||||
{#if $treeFileView}
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
onclick={() => selection.copySelection()}
|
||||
disabled={$selection.size === 0}
|
||||
onclick={() => oldSelection.copySelection()}
|
||||
disabled={selectionCount === 0}
|
||||
>
|
||||
<ClipboardCopy size="16" />
|
||||
{i18n._('menu.copy')}
|
||||
<Shortcut key="C" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
onclick={() => selection.cutSelection()}
|
||||
disabled={$selection.size === 0}
|
||||
onclick={() => oldSelection.cutSelection()}
|
||||
disabled={selectionCount === 0}
|
||||
>
|
||||
<Scissors size="16" />
|
||||
{i18n._('menu.cut')}
|
||||
@@ -336,10 +310,8 @@
|
||||
<Menubar.Item
|
||||
disabled={$copied === undefined ||
|
||||
$copied.length === 0 ||
|
||||
($selection.size > 0 &&
|
||||
!allowedPastes[$copied[0].level].includes(
|
||||
$selection.getSelected().pop()!.level
|
||||
))}
|
||||
(selectionLevel !== undefined &&
|
||||
!allowedPastes[$copied[0].level].includes(selectionLevel))}
|
||||
onclick={pasteSelection}
|
||||
>
|
||||
<ClipboardPaste size="16" />
|
||||
@@ -348,10 +320,7 @@
|
||||
</Menubar.Item>
|
||||
{/if}
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item
|
||||
onclick={() => tick().then(fileActions.deleteSelection)}
|
||||
disabled={$selection.size == 0}
|
||||
>
|
||||
<Menubar.Item onclick={() => engine.delete()} disabled={selectionCount == 0}>
|
||||
<Trash2 size="16" />
|
||||
{i18n._('menu.delete')}
|
||||
<Shortcut key="⌫" ctrl={true} />
|
||||
@@ -585,16 +554,16 @@
|
||||
triggerFileInput();
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
|
||||
fileActions.duplicateSelection();
|
||||
engine.duplicate();
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
||||
if (!targetInput) {
|
||||
selection.copySelection();
|
||||
oldSelection.copySelection();
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) {
|
||||
if (!targetInput) {
|
||||
selection.cutSelection();
|
||||
oldSelection.cutSelection();
|
||||
e.preventDefault();
|
||||
}
|
||||
} 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)) {
|
||||
if (e.shiftKey) {
|
||||
if (fileStateCollection.size > 0) {
|
||||
if (!noFiles) {
|
||||
exportState.current = ExportState.ALL;
|
||||
}
|
||||
} else if ($selection.size > 0) {
|
||||
} else if (selectionCount > 0) {
|
||||
exportState.current = ExportState.SELECTION;
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
|
||||
if (e.shiftKey) {
|
||||
fileActionManager.redo();
|
||||
engine.redo();
|
||||
} else {
|
||||
fileActionManager.undo();
|
||||
engine.undo();
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
|
||||
if (!targetInput) {
|
||||
if (e.shiftKey) {
|
||||
fileActions.deleteAllFiles();
|
||||
engine.deleteAll();
|
||||
} else {
|
||||
fileActions.deleteSelection();
|
||||
engine.delete();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
||||
if (!targetInput) {
|
||||
selection.selectAll();
|
||||
engine.selectAll();
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) {
|
||||
if (
|
||||
$selection.size === 1 &&
|
||||
$selection
|
||||
.getSelected()
|
||||
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
|
||||
) {
|
||||
if (selectionCount === 1 && filesOrTracksSelected) {
|
||||
editMetadata.current = true;
|
||||
}
|
||||
e.preventDefault();
|
||||
@@ -649,14 +613,10 @@
|
||||
$treeFileView = !$treeFileView;
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
|
||||
if ($allHidden) {
|
||||
fileActions.setHiddenToSelection(false);
|
||||
} else {
|
||||
fileActions.setHiddenToSelection(true);
|
||||
}
|
||||
engine.setSelectionHidden(!$allHidden);
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
if ($selection.size > 0) {
|
||||
if (selectionCount > 0) {
|
||||
boundsManager.centerMapOnSelection();
|
||||
}
|
||||
} else if (e.key === 'F1') {
|
||||
@@ -681,10 +641,7 @@
|
||||
e.key === 'ArrowUp'
|
||||
) {
|
||||
if (!targetInput) {
|
||||
selection.updateFromKey(
|
||||
e.key === 'ArrowRight' || e.key === 'ArrowDown',
|
||||
e.shiftKey
|
||||
);
|
||||
engine.arrowSelect(e.key === 'ArrowRight' || e.key === 'ArrowDown', e.shiftKey);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{#if level !== ListLevel.WAYPOINTS}
|
||||
<ContextMenu.Item onclick={() => engine.selectAllSiblings(node)}>
|
||||
<ContextMenu.Item onclick={() => engine.selectAll()}>
|
||||
<FileStack size="16" />
|
||||
{i18n._('menu.select_all')}
|
||||
<Shortcut key="A" ctrl={true} />
|
||||
|
||||
+27
-33
@@ -197,6 +197,8 @@ class Engine {
|
||||
private _visibility = new Map<string, Visibility>();
|
||||
private _files = writable<Map<string, Writable<FileState>>>(new Map());
|
||||
private _selection = writable<Selection>({ type: 'empty' });
|
||||
private _canUndo = writable(false);
|
||||
private _canRedo = writable(false);
|
||||
private _statistics = writable<SelectionStatistics>(EMPTY_SELECTION_STATISTICS);
|
||||
/** Identifies the statistics currently in the engine's buffers. */
|
||||
private _statisticsVersion = 0;
|
||||
@@ -205,6 +207,8 @@ class Engine {
|
||||
readonly order: Readable<string[]> = { subscribe: this._order.subscribe };
|
||||
/** Statistics of what is currently selected. */
|
||||
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. */
|
||||
readonly selection: Readable<Selection> = { subscribe: this._selection.subscribe };
|
||||
/**
|
||||
@@ -239,8 +243,12 @@ class Engine {
|
||||
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() {
|
||||
@@ -263,8 +271,22 @@ class Engine {
|
||||
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() {
|
||||
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.
|
||||
@@ -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
|
||||
|
||||
newTrack() {
|
||||
@@ -414,6 +406,8 @@ class Engine {
|
||||
/** Reads what the last action changed. */
|
||||
private sync(wasm: Wasm) {
|
||||
const update = wasm.last_update();
|
||||
this._canUndo.set(wasm.can_undo());
|
||||
this._canRedo.set(wasm.can_redo());
|
||||
if (update.selectionChanged) {
|
||||
this._selection.set(wasm.selection());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user