@kubohiroya/turbowarp-plus-editor-layout 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/document-workspace.d.ts +8 -0
- package/dist/document-workspace.js +20 -0
- package/dist/sprite-tabs.d.ts +9 -0
- package/dist/sprite-tabs.js +162 -6
- package/dist/workspace-layout.d.ts +24 -0
- package/dist/workspace-layout.js +88 -0
- package/package.json +1 -1
- package/src/document-workspace.ts +24 -0
- package/src/sprite-tabs.ts +148 -7
- package/src/workspace-layout.ts +90 -0
|
@@ -9,7 +9,15 @@ export interface DocumentPanelRequest {
|
|
|
9
9
|
dispose(): void;
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
interface DocumentPanelSource {
|
|
13
|
+
getPanelRequest(reference: DocumentPanelRequest['reference']): DocumentPanelRequest | undefined;
|
|
14
|
+
subscribePanelRequests(listener: () => void): () => void;
|
|
15
|
+
}
|
|
12
16
|
export declare function getDocumentWorkspace(vm: object): {
|
|
13
17
|
open(request: DocumentPanelRequest): boolean;
|
|
14
18
|
connect(handler: NonNullable<(request: DocumentPanelRequest) => void>): () => void;
|
|
19
|
+
connectSource(next: DocumentPanelSource): () => void;
|
|
20
|
+
resolve(reference: DocumentPanelRequest["reference"]): DocumentPanelRequest;
|
|
21
|
+
subscribe(listener: () => void): () => void;
|
|
15
22
|
};
|
|
23
|
+
export {};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
function createDocumentWorkspace() {
|
|
2
2
|
let open;
|
|
3
|
+
let source;
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
const notify = () => listeners.forEach(listener => listener());
|
|
3
6
|
return {
|
|
4
7
|
open(request) {
|
|
5
8
|
if (!open)
|
|
@@ -11,6 +14,23 @@ function createDocumentWorkspace() {
|
|
|
11
14
|
open = handler;
|
|
12
15
|
return () => { if (open === handler)
|
|
13
16
|
open = undefined; };
|
|
17
|
+
},
|
|
18
|
+
connectSource(next) {
|
|
19
|
+
source = next;
|
|
20
|
+
const unsubscribe = next.subscribePanelRequests(notify);
|
|
21
|
+
notify();
|
|
22
|
+
return () => {
|
|
23
|
+
unsubscribe();
|
|
24
|
+
if (source === next)
|
|
25
|
+
source = undefined;
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
resolve(reference) {
|
|
29
|
+
return source?.getPanelRequest(reference);
|
|
30
|
+
},
|
|
31
|
+
subscribe(listener) {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => { listeners.delete(listener); };
|
|
14
34
|
}
|
|
15
35
|
};
|
|
16
36
|
}
|
package/dist/sprite-tabs.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ interface Target {
|
|
|
12
12
|
isOriginal: boolean;
|
|
13
13
|
isStage: boolean;
|
|
14
14
|
getName(): string;
|
|
15
|
+
getCostumes?(): {
|
|
16
|
+
assetId?: string;
|
|
17
|
+
}[];
|
|
15
18
|
getCurrentCostume?(): {
|
|
16
19
|
asset?: CostumeAsset;
|
|
17
20
|
} | undefined;
|
|
@@ -74,6 +77,12 @@ interface TabOptions<Snapshot = unknown> {
|
|
|
74
77
|
resize(): void;
|
|
75
78
|
reopenLabel: string;
|
|
76
79
|
multiView?: boolean;
|
|
80
|
+
layoutStorage?: {
|
|
81
|
+
storage: Storage;
|
|
82
|
+
key: string;
|
|
83
|
+
isProjectLoading?(): boolean;
|
|
84
|
+
subscribeProject?(listener: () => void): (() => void) | undefined;
|
|
85
|
+
};
|
|
77
86
|
stagePane?: DockviewStagePane;
|
|
78
87
|
variables?: boolean;
|
|
79
88
|
locale?: string;
|
package/dist/sprite-tabs.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createSpriteLayoutMenu } from "./sprite-layout-menu.js";
|
|
|
5
5
|
import { SpriteAssetPane } from "./sprite-asset-pane.js";
|
|
6
6
|
import { SpriteEditorToolbar, spriteEditorSections } from "./sprite-editor-toolbar.js";
|
|
7
7
|
import { getDocumentWorkspace } from "./document-workspace.js";
|
|
8
|
+
import { readWorkspaceLayout } from "./workspace-layout.js";
|
|
8
9
|
export { getDocumentWorkspace } from "./document-workspace.js";
|
|
9
10
|
export { useDockviewStage } from "./dockview-stage.js";
|
|
10
11
|
const variableSection = spriteEditorSections.indexOf('variables');
|
|
@@ -44,10 +45,15 @@ function createAssetSelections(target, source) {
|
|
|
44
45
|
}
|
|
45
46
|
const stagePanelId = 'tw-plus-stage-display';
|
|
46
47
|
const documentComponent = 'tw-plus-document';
|
|
48
|
+
const documentTitle = ({ filename, reference: { providerId, documentId } }) => `${filename} (${providerId} / ${documentId})`;
|
|
47
49
|
const renameLabels = {
|
|
48
50
|
en: { title: 'Rename sprite', name: 'Sprite name', cancel: 'Cancel', save: 'Rename' },
|
|
49
51
|
ja: { title: 'スプライト名の変更', name: 'スプライト名', cancel: 'キャンセル', save: '変更' }
|
|
50
52
|
};
|
|
53
|
+
const targetName = (target) => target.isStage ? 'Stage' : target.getName();
|
|
54
|
+
const projectSignature = (targets) => JSON.stringify(targets
|
|
55
|
+
.map(target => JSON.stringify([target.isStage, targetName(target), target.getCostumes?.()[0]?.assetId ?? '']))
|
|
56
|
+
.sort());
|
|
51
57
|
class SpriteTab extends DefaultTab {
|
|
52
58
|
icon;
|
|
53
59
|
constructor(document) {
|
|
@@ -83,13 +89,21 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
83
89
|
const costumeIcons = new WeakMap();
|
|
84
90
|
const remembered = new WeakMap();
|
|
85
91
|
const lastUsed = new WeakMap();
|
|
86
|
-
|
|
92
|
+
let closed = new WeakSet();
|
|
87
93
|
let active;
|
|
88
94
|
let observed = vm.editingTarget;
|
|
89
95
|
let syncing = false;
|
|
90
96
|
let nextView = 0;
|
|
91
97
|
let renameDialog;
|
|
92
98
|
const text = options.renameLabels ?? renameLabels.en;
|
|
99
|
+
const persistence = multiView && options.layoutStorage;
|
|
100
|
+
const window = document.defaultView;
|
|
101
|
+
const documentWorkspace = multiView && getDocumentWorkspace(vm);
|
|
102
|
+
let pendingRestore = Boolean(persistence);
|
|
103
|
+
let restoreFrame;
|
|
104
|
+
let restoreTimeout;
|
|
105
|
+
let saveFrame;
|
|
106
|
+
const originalTargets = () => vm.runtime.targets.filter(target => target.isOriginal);
|
|
93
107
|
const dockview = createDockview(root, {
|
|
94
108
|
theme: themeLight,
|
|
95
109
|
disableFloatingGroups: true,
|
|
@@ -384,7 +398,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
384
398
|
if (splitPosition || views.has(id) || id === stagePanelId || documents.has(id)) {
|
|
385
399
|
do {
|
|
386
400
|
id = 'sprite-view-' + ++nextView;
|
|
387
|
-
} while (views.has(id) || vm.runtime.targets.some(item => item.id === id));
|
|
401
|
+
} while (views.has(id) || documents.has(id) || vm.runtime.targets.some(item => item.id === id));
|
|
388
402
|
}
|
|
389
403
|
const source = splitPosition && views.get(splitPosition.referencePanel);
|
|
390
404
|
views.set(id, { id, target, section: section ?? remembered.get(target) ?? options.getSection(),
|
|
@@ -421,6 +435,8 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
421
435
|
activateView(view?.id ?? add(target).id);
|
|
422
436
|
}
|
|
423
437
|
function openDocument(request) {
|
|
438
|
+
if (pendingRestore)
|
|
439
|
+
finishRestore();
|
|
424
440
|
const { providerId, documentId } = request.reference;
|
|
425
441
|
for (const [id, { request: opened }] of documents) {
|
|
426
442
|
if (opened.reference.providerId === providerId && opened.reference.documentId === documentId) {
|
|
@@ -432,7 +448,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
432
448
|
do {
|
|
433
449
|
id = 'document-view-' + ++nextView;
|
|
434
450
|
} while (views.has(id) || documents.has(id) || vm.runtime.targets.some(target => target.id === id));
|
|
435
|
-
const title = request
|
|
451
|
+
const title = documentTitle(request);
|
|
436
452
|
documents.set(id, { request });
|
|
437
453
|
try {
|
|
438
454
|
dockview.addPanel({ id, title, component: documentComponent, renderer: 'always' });
|
|
@@ -463,6 +479,8 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
463
479
|
function openStage() {
|
|
464
480
|
if (!stagePane)
|
|
465
481
|
return;
|
|
482
|
+
if (pendingRestore)
|
|
483
|
+
finishRestore();
|
|
466
484
|
const existing = dockview.getPanel(stagePanelId);
|
|
467
485
|
if (existing) {
|
|
468
486
|
existing.api.setActive();
|
|
@@ -514,6 +532,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
514
532
|
viewAdapter?.restore(describe(view), view.snapshots?.get(section)?.snapshot);
|
|
515
533
|
}
|
|
516
534
|
refreshSurfaces();
|
|
535
|
+
scheduleSave();
|
|
517
536
|
return true;
|
|
518
537
|
}
|
|
519
538
|
function syncActiveSection(section) {
|
|
@@ -521,6 +540,124 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
521
540
|
return;
|
|
522
541
|
active.section = section;
|
|
523
542
|
refreshSurfaces();
|
|
543
|
+
scheduleSave();
|
|
544
|
+
}
|
|
545
|
+
function writeLayout() {
|
|
546
|
+
saveFrame = undefined;
|
|
547
|
+
const targets = originalTargets();
|
|
548
|
+
if (!persistence || pendingRestore || syncing || !targets.length || persistence.isProjectLoading?.())
|
|
549
|
+
return;
|
|
550
|
+
const { grid, activeGroup } = dockview.toJSON();
|
|
551
|
+
if (!activeGroup || grid.width <= 0 || grid.height <= 0)
|
|
552
|
+
return;
|
|
553
|
+
const panels = Object.create(null);
|
|
554
|
+
for (const [id, view] of views) {
|
|
555
|
+
panels[id] = { kind: 'sprite', name: targetName(view.target), stage: view.target.isStage, section: view.section };
|
|
556
|
+
}
|
|
557
|
+
if (dockview.getPanel(stagePanelId))
|
|
558
|
+
panels[stagePanelId] = { kind: 'stage' };
|
|
559
|
+
for (const [id, { request }] of documents)
|
|
560
|
+
panels[id] = { kind: 'document', ...request.reference };
|
|
561
|
+
const snapshot = { version: 1, project: projectSignature(targets), grid, activeGroup,
|
|
562
|
+
editingView: active?.id, panels };
|
|
563
|
+
try {
|
|
564
|
+
persistence.storage.setItem(persistence.key, JSON.stringify(snapshot));
|
|
565
|
+
}
|
|
566
|
+
catch { /* Storage can be unavailable. */ }
|
|
567
|
+
}
|
|
568
|
+
function scheduleSave() {
|
|
569
|
+
if (persistence && !pendingRestore && saveFrame === undefined) {
|
|
570
|
+
saveFrame = window.requestAnimationFrame(writeLayout);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function flushLayout() {
|
|
574
|
+
if (saveFrame !== undefined) {
|
|
575
|
+
window.cancelAnimationFrame(saveFrame);
|
|
576
|
+
writeLayout();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function finishRestore() {
|
|
580
|
+
pendingRestore = false;
|
|
581
|
+
window.clearTimeout(restoreTimeout);
|
|
582
|
+
}
|
|
583
|
+
function restoreLayout() {
|
|
584
|
+
restoreFrame = undefined;
|
|
585
|
+
if (!pendingRestore || !persistence || persistence.isProjectLoading?.())
|
|
586
|
+
return;
|
|
587
|
+
const targets = originalTargets();
|
|
588
|
+
if (!targets.length || !vm.editingTarget)
|
|
589
|
+
return;
|
|
590
|
+
let stored;
|
|
591
|
+
try {
|
|
592
|
+
const raw = persistence.storage.getItem(persistence.key);
|
|
593
|
+
if (raw)
|
|
594
|
+
stored = readWorkspaceLayout(raw, projectSignature(targets));
|
|
595
|
+
}
|
|
596
|
+
catch { /* Storage can be unavailable. */ }
|
|
597
|
+
if (!stored)
|
|
598
|
+
return finishRestore();
|
|
599
|
+
const resolvedSprites = new Map();
|
|
600
|
+
const resolvedDocuments = new Map();
|
|
601
|
+
const panels = Object.create(null);
|
|
602
|
+
for (const [id, reference] of Object.entries(stored.panels)) {
|
|
603
|
+
if (reference.kind === 'sprite') {
|
|
604
|
+
const target = targets.find(item => item.isStage === reference.stage && targetName(item) === reference.name);
|
|
605
|
+
if (!target || id === stagePanelId || (reference.section === variableSection && !options.variables))
|
|
606
|
+
return finishRestore();
|
|
607
|
+
resolvedSprites.set(id, { id, target, section: reference.section, assets: createAssetSelections(target) });
|
|
608
|
+
panels[id] = { id, title: target.getName(), contentComponent: 'sprite', params: { icon: getIcon(target) } };
|
|
609
|
+
}
|
|
610
|
+
else if (reference.kind === 'stage') {
|
|
611
|
+
if (!stagePane || id !== stagePanelId)
|
|
612
|
+
return finishRestore();
|
|
613
|
+
panels[id] = { id, title: stagePane.title, contentComponent: stagePanelId, renderer: 'always' };
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
if (id === stagePanelId)
|
|
617
|
+
return finishRestore();
|
|
618
|
+
const request = documentWorkspace && documentWorkspace.resolve(reference);
|
|
619
|
+
if (!request) {
|
|
620
|
+
// The project can finish loading before the Blockly host publishes its document catalog.
|
|
621
|
+
restoreTimeout ??= window.setTimeout(finishRestore, 2000);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
resolvedDocuments.set(id, request);
|
|
625
|
+
panels[id] = { id, title: documentTitle(request), contentComponent: documentComponent, renderer: 'always' };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
finishRestore();
|
|
629
|
+
syncing = true;
|
|
630
|
+
try {
|
|
631
|
+
dockview.clear();
|
|
632
|
+
active = undefined;
|
|
633
|
+
const represented = new Set([...resolvedSprites.values()].map(view => view.target));
|
|
634
|
+
closed = new WeakSet(targets.filter(target => !represented.has(target)));
|
|
635
|
+
for (const [id, view] of resolvedSprites)
|
|
636
|
+
views.set(id, view);
|
|
637
|
+
for (const [id, request] of resolvedDocuments)
|
|
638
|
+
documents.set(id, { request });
|
|
639
|
+
dockview.fromJSON({ grid: stored.grid, activeGroup: stored.activeGroup, panels });
|
|
640
|
+
const selected = dockview.activePanel;
|
|
641
|
+
show(views.has(selected?.id) ? selected : dockview.getPanel(stored.editingView) ?? selected);
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
dockview.clear();
|
|
645
|
+
for (const view of views.values())
|
|
646
|
+
disposeView(view);
|
|
647
|
+
for (const item of documents.values())
|
|
648
|
+
item.session?.dispose();
|
|
649
|
+
documents.clear();
|
|
650
|
+
closed = new WeakSet();
|
|
651
|
+
active = undefined;
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
syncing = false;
|
|
655
|
+
}
|
|
656
|
+
refresh();
|
|
657
|
+
}
|
|
658
|
+
function scheduleRestore() {
|
|
659
|
+
if (pendingRestore && restoreFrame === undefined)
|
|
660
|
+
restoreFrame = window.requestAnimationFrame(restoreLayout);
|
|
524
661
|
}
|
|
525
662
|
function refresh() {
|
|
526
663
|
if (syncing)
|
|
@@ -557,6 +694,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
557
694
|
finally {
|
|
558
695
|
syncing = false;
|
|
559
696
|
}
|
|
697
|
+
scheduleRestore();
|
|
560
698
|
}
|
|
561
699
|
function disposeView(view) {
|
|
562
700
|
for (const stored of view.snapshots?.values() ?? [])
|
|
@@ -577,6 +715,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
577
715
|
finally {
|
|
578
716
|
syncing = false;
|
|
579
717
|
}
|
|
718
|
+
scheduleSave();
|
|
580
719
|
});
|
|
581
720
|
const removal = dockview.onDidRemovePanel(panel => {
|
|
582
721
|
const item = documents.get(panel.id);
|
|
@@ -598,8 +737,15 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
598
737
|
if (![...views.values()].some(item => item.target === view.target))
|
|
599
738
|
closed.add(view.target);
|
|
600
739
|
});
|
|
601
|
-
const layout = multiView && dockview.onDidLayoutChange(
|
|
602
|
-
|
|
740
|
+
const layout = multiView && dockview.onDidLayoutChange(() => {
|
|
741
|
+
refreshSurfaces();
|
|
742
|
+
scheduleSave();
|
|
743
|
+
});
|
|
744
|
+
const disconnectDocuments = documentWorkspace && documentWorkspace.connect(openDocument);
|
|
745
|
+
const unsubscribeDocuments = persistence && documentWorkspace && documentWorkspace.subscribe(scheduleRestore);
|
|
746
|
+
const unsubscribeProject = persistence && persistence.subscribeProject?.(scheduleRestore);
|
|
747
|
+
if (persistence)
|
|
748
|
+
window.addEventListener('pagehide', flushLayout);
|
|
603
749
|
root.addEventListener('click', guardDocumentClose, true);
|
|
604
750
|
root.addEventListener('keydown', guardDocumentClose, true);
|
|
605
751
|
vm.on('targetsUpdate', refresh);
|
|
@@ -614,6 +760,15 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
614
760
|
currentView: () => active && describe(active),
|
|
615
761
|
views: () => [...views.values()].map(view => describe(view)),
|
|
616
762
|
dispose() {
|
|
763
|
+
flushLayout();
|
|
764
|
+
window.cancelAnimationFrame(restoreFrame);
|
|
765
|
+
window.clearTimeout(restoreTimeout);
|
|
766
|
+
if (persistence)
|
|
767
|
+
window.removeEventListener('pagehide', flushLayout);
|
|
768
|
+
if (unsubscribeDocuments)
|
|
769
|
+
unsubscribeDocuments();
|
|
770
|
+
if (unsubscribeProject)
|
|
771
|
+
unsubscribeProject();
|
|
617
772
|
if (disconnectDocuments)
|
|
618
773
|
disconnectDocuments();
|
|
619
774
|
root.removeEventListener('click', guardDocumentClose, true);
|
|
@@ -730,6 +885,7 @@ function SpriteTabs(props) {
|
|
|
730
885
|
resize: () => latest.current.resize(),
|
|
731
886
|
reopenLabel: props.reopenLabel,
|
|
732
887
|
multiView: props.multiView,
|
|
888
|
+
layoutStorage: props.layoutStorage,
|
|
733
889
|
stagePane: props.stagePane,
|
|
734
890
|
variables: hasVariables,
|
|
735
891
|
locale: props.locale,
|
|
@@ -749,7 +905,7 @@ function SpriteTabs(props) {
|
|
|
749
905
|
instance.dispose();
|
|
750
906
|
};
|
|
751
907
|
}, [content, props.enabled, props.vm, props.reopenLabel, props.multiView, props.locale,
|
|
752
|
-
props.viewAdapter, props.stagePane, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
908
|
+
props.viewAdapter, props.stagePane, props.layoutStorage, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
753
909
|
React.useLayoutEffect(() => {
|
|
754
910
|
controller.current?.syncActiveSection(props.activeSection);
|
|
755
911
|
}, [props.activeSection]);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SerializedDockview } from 'dockview';
|
|
2
|
+
import type { DocumentPanelRequest } from './document-workspace.ts';
|
|
3
|
+
type Grid = SerializedDockview['grid'];
|
|
4
|
+
export type PanelReference = {
|
|
5
|
+
kind: 'sprite';
|
|
6
|
+
name: string;
|
|
7
|
+
stage: boolean;
|
|
8
|
+
section: number;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'stage';
|
|
11
|
+
} | ({
|
|
12
|
+
kind: 'document';
|
|
13
|
+
} & DocumentPanelRequest['reference']);
|
|
14
|
+
export interface WorkspaceLayout {
|
|
15
|
+
version: 1;
|
|
16
|
+
project: string;
|
|
17
|
+
grid: Grid;
|
|
18
|
+
activeGroup: string;
|
|
19
|
+
editingView?: string;
|
|
20
|
+
panels: Record<string, PanelReference>;
|
|
21
|
+
}
|
|
22
|
+
/** Rebuild only the supported docked grid and logical panel references from storage. */
|
|
23
|
+
export declare function readWorkspaceLayout(raw: string, project: string): WorkspaceLayout | undefined;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Orientation } from 'dockview';
|
|
2
|
+
const limit = 256;
|
|
3
|
+
const validId = (value) => typeof value === 'string' && value.length > 0 && value.length <= 1024;
|
|
4
|
+
const size = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100000;
|
|
5
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
/** Rebuild only the supported docked grid and logical panel references from storage. */
|
|
7
|
+
export function readWorkspaceLayout(raw, project) {
|
|
8
|
+
if (raw.length > 1024 * 1024)
|
|
9
|
+
return;
|
|
10
|
+
let input;
|
|
11
|
+
try {
|
|
12
|
+
input = JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!record(input) || input.version !== 1 || input.project !== project || !record(input.grid) ||
|
|
18
|
+
!record(input.panels) || !validId(input.activeGroup))
|
|
19
|
+
return;
|
|
20
|
+
const { width, height, orientation } = input.grid;
|
|
21
|
+
if (!size(width) || !width || !size(height) || !height ||
|
|
22
|
+
(orientation !== Orientation.HORIZONTAL && orientation !== Orientation.VERTICAL))
|
|
23
|
+
return;
|
|
24
|
+
const groups = new Set();
|
|
25
|
+
const ids = new Set();
|
|
26
|
+
let nodes = 0;
|
|
27
|
+
function readNode(node, depth) {
|
|
28
|
+
if (depth > 32 || ++nodes > limit || !record(node) || !size(node.size))
|
|
29
|
+
return;
|
|
30
|
+
if (node.type === 'branch' && Array.isArray(node.data) && node.data.length > 0 && node.data.length <= limit) {
|
|
31
|
+
const data = [];
|
|
32
|
+
for (const child of node.data) {
|
|
33
|
+
const next = readNode(child, depth + 1);
|
|
34
|
+
if (!next)
|
|
35
|
+
return;
|
|
36
|
+
data.push(next);
|
|
37
|
+
}
|
|
38
|
+
return { type: 'branch', size: node.size, data };
|
|
39
|
+
}
|
|
40
|
+
if (node.type !== 'leaf' || !record(node.data))
|
|
41
|
+
return;
|
|
42
|
+
const { id, views, activeView } = node.data;
|
|
43
|
+
if (!validId(id) || groups.has(id) || !Array.isArray(views) || !views.length ||
|
|
44
|
+
views.length > limit || !validId(activeView) || !views.includes(activeView))
|
|
45
|
+
return;
|
|
46
|
+
groups.add(id);
|
|
47
|
+
for (const panel of views) {
|
|
48
|
+
if (!validId(panel) || ids.has(panel) || ids.size >= limit)
|
|
49
|
+
return;
|
|
50
|
+
ids.add(panel);
|
|
51
|
+
}
|
|
52
|
+
return { type: 'leaf', size: node.size, data: { id, views, activeView } };
|
|
53
|
+
}
|
|
54
|
+
const root = readNode(input.grid.root, 0);
|
|
55
|
+
if (root?.type !== 'branch' || !groups.has(input.activeGroup) ||
|
|
56
|
+
ids.size !== Object.keys(input.panels).length)
|
|
57
|
+
return;
|
|
58
|
+
const panels = Object.create(null);
|
|
59
|
+
const documents = new Set();
|
|
60
|
+
let hasStage = false;
|
|
61
|
+
for (const id of ids) {
|
|
62
|
+
const panel = input.panels[id];
|
|
63
|
+
if (!Object.hasOwn(input.panels, id) || !record(panel))
|
|
64
|
+
return;
|
|
65
|
+
if (panel.kind === 'sprite' && validId(panel.name) && typeof panel.stage === 'boolean' &&
|
|
66
|
+
typeof panel.section === 'number' && Number.isInteger(panel.section) && panel.section >= 0 && panel.section <= 3) {
|
|
67
|
+
panels[id] = { kind: 'sprite', name: panel.name, stage: panel.stage, section: panel.section };
|
|
68
|
+
}
|
|
69
|
+
else if (panel.kind === 'stage' && !hasStage) {
|
|
70
|
+
hasStage = true;
|
|
71
|
+
panels[id] = { kind: 'stage' };
|
|
72
|
+
}
|
|
73
|
+
else if (panel.kind === 'document' && validId(panel.providerId) && validId(panel.documentId)) {
|
|
74
|
+
const key = JSON.stringify([panel.providerId, panel.documentId]);
|
|
75
|
+
if (documents.has(key))
|
|
76
|
+
return;
|
|
77
|
+
documents.add(key);
|
|
78
|
+
panels[id] = { kind: 'document', providerId: panel.providerId, documentId: panel.documentId };
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const editingView = input.editingView;
|
|
84
|
+
if (editingView !== undefined && (!validId(editingView) || panels[editingView]?.kind !== 'sprite'))
|
|
85
|
+
return;
|
|
86
|
+
return { version: 1, project, grid: { root, width, height, orientation }, activeGroup: input.activeGroup,
|
|
87
|
+
editingView: typeof editingView === 'string' ? editingView : undefined, panels };
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -4,8 +4,16 @@ export interface DocumentPanelRequest {
|
|
|
4
4
|
mount(element: HTMLElement, close: () => void): {requestClose(): void; dispose(): void};
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
interface DocumentPanelSource {
|
|
8
|
+
getPanelRequest(reference: DocumentPanelRequest['reference']): DocumentPanelRequest | undefined;
|
|
9
|
+
subscribePanelRequests(listener: () => void): () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
function createDocumentWorkspace() {
|
|
8
13
|
let open: ((request: DocumentPanelRequest) => void) | undefined;
|
|
14
|
+
let source: DocumentPanelSource | undefined;
|
|
15
|
+
const listeners = new Set<() => void>();
|
|
16
|
+
const notify = () => listeners.forEach(listener => listener());
|
|
9
17
|
return {
|
|
10
18
|
open(request: DocumentPanelRequest) {
|
|
11
19
|
if (!open) return false;
|
|
@@ -15,6 +23,22 @@ function createDocumentWorkspace() {
|
|
|
15
23
|
connect(handler: NonNullable<typeof open>) {
|
|
16
24
|
open = handler;
|
|
17
25
|
return () => { if (open === handler) open = undefined; };
|
|
26
|
+
},
|
|
27
|
+
connectSource(next: DocumentPanelSource) {
|
|
28
|
+
source = next;
|
|
29
|
+
const unsubscribe = next.subscribePanelRequests(notify);
|
|
30
|
+
notify();
|
|
31
|
+
return () => {
|
|
32
|
+
unsubscribe();
|
|
33
|
+
if (source === next) source = undefined;
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
resolve(reference: DocumentPanelRequest['reference']) {
|
|
37
|
+
return source?.getPanelRequest(reference);
|
|
38
|
+
},
|
|
39
|
+
subscribe(listener: () => void) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
return () => { listeners.delete(listener); };
|
|
18
42
|
}
|
|
19
43
|
};
|
|
20
44
|
}
|
package/src/sprite-tabs.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {createPortal} from 'react-dom';
|
|
3
3
|
import {createDockview, DefaultTab, themeLight} from 'dockview';
|
|
4
|
-
import type {DockviewGroupPanel, GroupPanelPartInitParameters, IDockviewPanel} from 'dockview';
|
|
4
|
+
import type {DockviewGroupPanel, GroupPanelPartInitParameters, IDockviewPanel, SerializedDockview} from 'dockview';
|
|
5
5
|
import {createSpriteLayoutMenu} from './sprite-layout-menu.ts';
|
|
6
6
|
import {SpriteAssetPane} from './sprite-asset-pane.tsx';
|
|
7
7
|
import type {SpriteVariable} from './sprite-asset-pane.tsx';
|
|
@@ -10,6 +10,8 @@ import type {SpriteSearchResult} from './sprite-editor-toolbar.tsx';
|
|
|
10
10
|
import type {DockviewStagePane} from './dockview-stage.ts';
|
|
11
11
|
import {getDocumentWorkspace} from './document-workspace.ts';
|
|
12
12
|
import type {DocumentPanelRequest} from './document-workspace.ts';
|
|
13
|
+
import {readWorkspaceLayout} from './workspace-layout.ts';
|
|
14
|
+
import type {PanelReference, WorkspaceLayout} from './workspace-layout.ts';
|
|
13
15
|
|
|
14
16
|
export {getDocumentWorkspace} from './document-workspace.ts';
|
|
15
17
|
|
|
@@ -24,6 +26,7 @@ interface Target {
|
|
|
24
26
|
isOriginal: boolean;
|
|
25
27
|
isStage: boolean;
|
|
26
28
|
getName(): string;
|
|
29
|
+
getCostumes?(): {assetId?: string}[];
|
|
27
30
|
getCurrentCostume?(): {asset?: CostumeAsset} | undefined;
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -120,6 +123,8 @@ export type SpriteSplitDirection = 'right' | 'below';
|
|
|
120
123
|
|
|
121
124
|
const stagePanelId = 'tw-plus-stage-display';
|
|
122
125
|
const documentComponent = 'tw-plus-document';
|
|
126
|
+
const documentTitle = ({filename, reference: {providerId, documentId}}: DocumentPanelRequest) =>
|
|
127
|
+
`${filename} (${providerId} / ${documentId})`;
|
|
123
128
|
|
|
124
129
|
interface TabOptions<Snapshot = unknown> {
|
|
125
130
|
vm: Project;
|
|
@@ -129,6 +134,12 @@ interface TabOptions<Snapshot = unknown> {
|
|
|
129
134
|
resize(): void;
|
|
130
135
|
reopenLabel: string;
|
|
131
136
|
multiView?: boolean;
|
|
137
|
+
layoutStorage?: {
|
|
138
|
+
storage: Storage;
|
|
139
|
+
key: string;
|
|
140
|
+
isProjectLoading?(): boolean;
|
|
141
|
+
subscribeProject?(listener: () => void): (() => void) | undefined;
|
|
142
|
+
};
|
|
132
143
|
stagePane?: DockviewStagePane;
|
|
133
144
|
variables?: boolean;
|
|
134
145
|
locale?: string;
|
|
@@ -181,6 +192,10 @@ const renameLabels = {
|
|
|
181
192
|
ja: {title: 'スプライト名の変更', name: 'スプライト名', cancel: 'キャンセル', save: '変更'}
|
|
182
193
|
};
|
|
183
194
|
type RenameLabels = typeof renameLabels.en;
|
|
195
|
+
const targetName = (target: Target) => target.isStage ? 'Stage' : target.getName();
|
|
196
|
+
const projectSignature = (targets: readonly Target[]) => JSON.stringify(targets
|
|
197
|
+
.map(target => JSON.stringify([target.isStage, targetName(target), target.getCostumes?.()[0]?.assetId ?? '']))
|
|
198
|
+
.sort());
|
|
184
199
|
|
|
185
200
|
class SpriteTab extends DefaultTab {
|
|
186
201
|
private readonly icon: HTMLImageElement;
|
|
@@ -219,13 +234,21 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
219
234
|
const costumeIcons = new WeakMap<CostumeAsset, string>();
|
|
220
235
|
const remembered = new WeakMap<Target, number>();
|
|
221
236
|
const lastUsed = new WeakMap<Target, View<Snapshot>>();
|
|
222
|
-
|
|
237
|
+
let closed = new WeakSet<Target>();
|
|
223
238
|
let active: View<Snapshot> | undefined;
|
|
224
239
|
let observed = vm.editingTarget;
|
|
225
240
|
let syncing = false;
|
|
226
241
|
let nextView = 0;
|
|
227
242
|
let renameDialog: HTMLDialogElement | undefined;
|
|
228
243
|
const text = options.renameLabels ?? renameLabels.en;
|
|
244
|
+
const persistence = multiView && options.layoutStorage;
|
|
245
|
+
const window = document.defaultView;
|
|
246
|
+
const documentWorkspace = multiView && getDocumentWorkspace(vm);
|
|
247
|
+
let pendingRestore = Boolean(persistence);
|
|
248
|
+
let restoreFrame: number | undefined;
|
|
249
|
+
let restoreTimeout: number | undefined;
|
|
250
|
+
let saveFrame: number | undefined;
|
|
251
|
+
const originalTargets = () => vm.runtime.targets.filter(target => target.isOriginal);
|
|
229
252
|
|
|
230
253
|
const dockview = createDockview(root, {
|
|
231
254
|
theme: themeLight,
|
|
@@ -496,7 +519,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
496
519
|
if (splitPosition || views.has(id) || id === stagePanelId || documents.has(id)) {
|
|
497
520
|
do {
|
|
498
521
|
id = 'sprite-view-' + ++nextView;
|
|
499
|
-
} while (views.has(id) || vm.runtime.targets.some(item => item.id === id));
|
|
522
|
+
} while (views.has(id) || documents.has(id) || vm.runtime.targets.some(item => item.id === id));
|
|
500
523
|
}
|
|
501
524
|
const source = splitPosition && views.get(splitPosition.referencePanel);
|
|
502
525
|
views.set(id, {id, target, section: section ?? remembered.get(target) ?? options.getSection(),
|
|
@@ -533,6 +556,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
533
556
|
}
|
|
534
557
|
|
|
535
558
|
function openDocument(request: DocumentPanelRequest) {
|
|
559
|
+
if (pendingRestore) finishRestore();
|
|
536
560
|
const {providerId, documentId} = request.reference;
|
|
537
561
|
for (const [id, {request: opened}] of documents) {
|
|
538
562
|
if (opened.reference.providerId === providerId && opened.reference.documentId === documentId) {
|
|
@@ -544,7 +568,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
544
568
|
do {
|
|
545
569
|
id = 'document-view-' + ++nextView;
|
|
546
570
|
} while (views.has(id) || documents.has(id) || vm.runtime.targets.some(target => target.id === id));
|
|
547
|
-
const title = request
|
|
571
|
+
const title = documentTitle(request);
|
|
548
572
|
documents.set(id, {request});
|
|
549
573
|
try {
|
|
550
574
|
dockview.addPanel({id, title, component: documentComponent, renderer: 'always'});
|
|
@@ -574,6 +598,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
574
598
|
|
|
575
599
|
function openStage() {
|
|
576
600
|
if (!stagePane) return;
|
|
601
|
+
if (pendingRestore) finishRestore();
|
|
577
602
|
const existing = dockview.getPanel(stagePanelId);
|
|
578
603
|
if (existing) {
|
|
579
604
|
existing.api.setActive();
|
|
@@ -624,6 +649,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
624
649
|
viewAdapter?.restore(describe(view), view.snapshots?.get(section)?.snapshot);
|
|
625
650
|
}
|
|
626
651
|
refreshSurfaces();
|
|
652
|
+
scheduleSave();
|
|
627
653
|
return true;
|
|
628
654
|
}
|
|
629
655
|
|
|
@@ -631,6 +657,106 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
631
657
|
if (!active || !usesNativeSection(active.section) || active.section === section) return;
|
|
632
658
|
active.section = section;
|
|
633
659
|
refreshSurfaces();
|
|
660
|
+
scheduleSave();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function writeLayout() {
|
|
664
|
+
saveFrame = undefined;
|
|
665
|
+
const targets = originalTargets();
|
|
666
|
+
if (!persistence || pendingRestore || syncing || !targets.length || persistence.isProjectLoading?.()) return;
|
|
667
|
+
const {grid, activeGroup} = dockview.toJSON();
|
|
668
|
+
if (!activeGroup || grid.width <= 0 || grid.height <= 0) return;
|
|
669
|
+
const panels: Record<string, PanelReference> = Object.create(null);
|
|
670
|
+
for (const [id, view] of views) {
|
|
671
|
+
panels[id] = {kind: 'sprite', name: targetName(view.target), stage: view.target.isStage, section: view.section};
|
|
672
|
+
}
|
|
673
|
+
if (dockview.getPanel(stagePanelId)) panels[stagePanelId] = {kind: 'stage'};
|
|
674
|
+
for (const [id, {request}] of documents) panels[id] = {kind: 'document', ...request.reference};
|
|
675
|
+
const snapshot: WorkspaceLayout = {version: 1, project: projectSignature(targets), grid, activeGroup,
|
|
676
|
+
editingView: active?.id, panels};
|
|
677
|
+
try { persistence.storage.setItem(persistence.key, JSON.stringify(snapshot)); } catch { /* Storage can be unavailable. */ }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function scheduleSave() {
|
|
681
|
+
if (persistence && !pendingRestore && saveFrame === undefined) {
|
|
682
|
+
saveFrame = window.requestAnimationFrame(writeLayout);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function flushLayout() {
|
|
687
|
+
if (saveFrame !== undefined) {
|
|
688
|
+
window.cancelAnimationFrame(saveFrame);
|
|
689
|
+
writeLayout();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function finishRestore() {
|
|
694
|
+
pendingRestore = false;
|
|
695
|
+
window.clearTimeout(restoreTimeout);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function restoreLayout() {
|
|
699
|
+
restoreFrame = undefined;
|
|
700
|
+
if (!pendingRestore || !persistence || persistence.isProjectLoading?.()) return;
|
|
701
|
+
const targets = originalTargets();
|
|
702
|
+
if (!targets.length || !vm.editingTarget) return;
|
|
703
|
+
let stored: WorkspaceLayout | undefined;
|
|
704
|
+
try {
|
|
705
|
+
const raw = persistence.storage.getItem(persistence.key);
|
|
706
|
+
if (raw) stored = readWorkspaceLayout(raw, projectSignature(targets));
|
|
707
|
+
} catch { /* Storage can be unavailable. */ }
|
|
708
|
+
if (!stored) return finishRestore();
|
|
709
|
+
const resolvedSprites = new Map<string, View<Snapshot>>();
|
|
710
|
+
const resolvedDocuments = new Map<string, DocumentPanelRequest>();
|
|
711
|
+
const panels: SerializedDockview['panels'] = Object.create(null);
|
|
712
|
+
for (const [id, reference] of Object.entries(stored.panels)) {
|
|
713
|
+
if (reference.kind === 'sprite') {
|
|
714
|
+
const target = targets.find(item => item.isStage === reference.stage && targetName(item) === reference.name);
|
|
715
|
+
if (!target || id === stagePanelId || (reference.section === variableSection && !options.variables)) return finishRestore();
|
|
716
|
+
resolvedSprites.set(id, {id, target, section: reference.section, assets: createAssetSelections(target)});
|
|
717
|
+
panels[id] = {id, title: target.getName(), contentComponent: 'sprite', params: {icon: getIcon(target)}};
|
|
718
|
+
} else if (reference.kind === 'stage') {
|
|
719
|
+
if (!stagePane || id !== stagePanelId) return finishRestore();
|
|
720
|
+
panels[id] = {id, title: stagePane.title, contentComponent: stagePanelId, renderer: 'always'};
|
|
721
|
+
} else {
|
|
722
|
+
if (id === stagePanelId) return finishRestore();
|
|
723
|
+
const request = documentWorkspace && documentWorkspace.resolve(reference);
|
|
724
|
+
if (!request) {
|
|
725
|
+
// The project can finish loading before the Blockly host publishes its document catalog.
|
|
726
|
+
restoreTimeout ??= window.setTimeout(finishRestore, 2000);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
resolvedDocuments.set(id, request);
|
|
730
|
+
panels[id] = {id, title: documentTitle(request), contentComponent: documentComponent, renderer: 'always'};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
finishRestore();
|
|
734
|
+
syncing = true;
|
|
735
|
+
try {
|
|
736
|
+
dockview.clear();
|
|
737
|
+
active = undefined;
|
|
738
|
+
const represented = new Set([...resolvedSprites.values()].map(view => view.target));
|
|
739
|
+
closed = new WeakSet(targets.filter(target => !represented.has(target)));
|
|
740
|
+
for (const [id, view] of resolvedSprites) views.set(id, view);
|
|
741
|
+
for (const [id, request] of resolvedDocuments) documents.set(id, {request});
|
|
742
|
+
dockview.fromJSON({grid: stored.grid, activeGroup: stored.activeGroup, panels});
|
|
743
|
+
const selected = dockview.activePanel;
|
|
744
|
+
show(views.has(selected?.id) ? selected : dockview.getPanel(stored.editingView) ?? selected);
|
|
745
|
+
} catch {
|
|
746
|
+
dockview.clear();
|
|
747
|
+
for (const view of views.values()) disposeView(view);
|
|
748
|
+
for (const item of documents.values()) item.session?.dispose();
|
|
749
|
+
documents.clear();
|
|
750
|
+
closed = new WeakSet();
|
|
751
|
+
active = undefined;
|
|
752
|
+
} finally {
|
|
753
|
+
syncing = false;
|
|
754
|
+
}
|
|
755
|
+
refresh();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function scheduleRestore() {
|
|
759
|
+
if (pendingRestore && restoreFrame === undefined) restoreFrame = window.requestAnimationFrame(restoreLayout);
|
|
634
760
|
}
|
|
635
761
|
|
|
636
762
|
function refresh() {
|
|
@@ -663,6 +789,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
663
789
|
} finally {
|
|
664
790
|
syncing = false;
|
|
665
791
|
}
|
|
792
|
+
scheduleRestore();
|
|
666
793
|
}
|
|
667
794
|
|
|
668
795
|
function disposeView(view: View<Snapshot>) {
|
|
@@ -681,6 +808,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
681
808
|
} finally {
|
|
682
809
|
syncing = false;
|
|
683
810
|
}
|
|
811
|
+
scheduleSave();
|
|
684
812
|
});
|
|
685
813
|
const removal = dockview.onDidRemovePanel(panel => {
|
|
686
814
|
const item = documents.get(panel.id);
|
|
@@ -699,8 +827,14 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
699
827
|
disposeView(view);
|
|
700
828
|
if (![...views.values()].some(item => item.target === view.target)) closed.add(view.target);
|
|
701
829
|
});
|
|
702
|
-
const layout = multiView && dockview.onDidLayoutChange(
|
|
703
|
-
|
|
830
|
+
const layout = multiView && dockview.onDidLayoutChange(() => {
|
|
831
|
+
refreshSurfaces();
|
|
832
|
+
scheduleSave();
|
|
833
|
+
});
|
|
834
|
+
const disconnectDocuments = documentWorkspace && documentWorkspace.connect(openDocument);
|
|
835
|
+
const unsubscribeDocuments = persistence && documentWorkspace && documentWorkspace.subscribe(scheduleRestore);
|
|
836
|
+
const unsubscribeProject = persistence && persistence.subscribeProject?.(scheduleRestore);
|
|
837
|
+
if (persistence) window.addEventListener('pagehide', flushLayout);
|
|
704
838
|
root.addEventListener('click', guardDocumentClose, true);
|
|
705
839
|
root.addEventListener('keydown', guardDocumentClose, true);
|
|
706
840
|
vm.on('targetsUpdate', refresh);
|
|
@@ -715,6 +849,12 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
715
849
|
currentView: () => active && describe(active),
|
|
716
850
|
views: () => [...views.values()].map(view => describe(view)),
|
|
717
851
|
dispose() {
|
|
852
|
+
flushLayout();
|
|
853
|
+
window.cancelAnimationFrame(restoreFrame);
|
|
854
|
+
window.clearTimeout(restoreTimeout);
|
|
855
|
+
if (persistence) window.removeEventListener('pagehide', flushLayout);
|
|
856
|
+
if (unsubscribeDocuments) unsubscribeDocuments();
|
|
857
|
+
if (unsubscribeProject) unsubscribeProject();
|
|
718
858
|
if (disconnectDocuments) disconnectDocuments();
|
|
719
859
|
root.removeEventListener('click', guardDocumentClose, true);
|
|
720
860
|
root.removeEventListener('keydown', guardDocumentClose, true);
|
|
@@ -853,6 +993,7 @@ function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
|
|
|
853
993
|
resize: () => latest.current.resize(),
|
|
854
994
|
reopenLabel: props.reopenLabel,
|
|
855
995
|
multiView: props.multiView,
|
|
996
|
+
layoutStorage: props.layoutStorage,
|
|
856
997
|
stagePane: props.stagePane,
|
|
857
998
|
variables: hasVariables,
|
|
858
999
|
locale: props.locale,
|
|
@@ -871,7 +1012,7 @@ function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
|
|
|
871
1012
|
instance.dispose();
|
|
872
1013
|
};
|
|
873
1014
|
}, [content, props.enabled, props.vm, props.reopenLabel, props.multiView, props.locale,
|
|
874
|
-
props.viewAdapter, props.stagePane, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
1015
|
+
props.viewAdapter, props.stagePane, props.layoutStorage, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
875
1016
|
React.useLayoutEffect(() => {
|
|
876
1017
|
controller.current?.syncActiveSection(props.activeSection);
|
|
877
1018
|
}, [props.activeSection]);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {Orientation} from 'dockview';
|
|
2
|
+
import type {SerializedDockview} from 'dockview';
|
|
3
|
+
import type {DocumentPanelRequest} from './document-workspace.ts';
|
|
4
|
+
|
|
5
|
+
type Grid = SerializedDockview['grid'];
|
|
6
|
+
type Node = Grid['root'];
|
|
7
|
+
export type PanelReference =
|
|
8
|
+
| {kind: 'sprite'; name: string; stage: boolean; section: number}
|
|
9
|
+
| {kind: 'stage'}
|
|
10
|
+
| ({kind: 'document'} & DocumentPanelRequest['reference']);
|
|
11
|
+
|
|
12
|
+
export interface WorkspaceLayout {
|
|
13
|
+
version: 1;
|
|
14
|
+
project: string;
|
|
15
|
+
grid: Grid;
|
|
16
|
+
activeGroup: string;
|
|
17
|
+
editingView?: string;
|
|
18
|
+
panels: Record<string, PanelReference>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const limit = 256;
|
|
22
|
+
const validId = (value: unknown): value is string =>
|
|
23
|
+
typeof value === 'string' && value.length > 0 && value.length <= 1024;
|
|
24
|
+
const size = (value: unknown): value is number =>
|
|
25
|
+
typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100000;
|
|
26
|
+
const record = (value: unknown): value is Record<string, unknown> =>
|
|
27
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
/** Rebuild only the supported docked grid and logical panel references from storage. */
|
|
30
|
+
export function readWorkspaceLayout(raw: string, project: string): WorkspaceLayout | undefined {
|
|
31
|
+
if (raw.length > 1024 * 1024) return;
|
|
32
|
+
let input: unknown;
|
|
33
|
+
try { input = JSON.parse(raw); } catch { return; }
|
|
34
|
+
if (!record(input) || input.version !== 1 || input.project !== project || !record(input.grid) ||
|
|
35
|
+
!record(input.panels) || !validId(input.activeGroup)) return;
|
|
36
|
+
const {width, height, orientation} = input.grid;
|
|
37
|
+
if (!size(width) || !width || !size(height) || !height ||
|
|
38
|
+
(orientation !== Orientation.HORIZONTAL && orientation !== Orientation.VERTICAL)) return;
|
|
39
|
+
const groups = new Set<string>();
|
|
40
|
+
const ids = new Set<string>();
|
|
41
|
+
let nodes = 0;
|
|
42
|
+
function readNode(node: unknown, depth: number): Node | undefined {
|
|
43
|
+
if (depth > 32 || ++nodes > limit || !record(node) || !size(node.size)) return;
|
|
44
|
+
if (node.type === 'branch' && Array.isArray(node.data) && node.data.length > 0 && node.data.length <= limit) {
|
|
45
|
+
const data: Node[] = [];
|
|
46
|
+
for (const child of node.data) {
|
|
47
|
+
const next = readNode(child, depth + 1);
|
|
48
|
+
if (!next) return;
|
|
49
|
+
data.push(next);
|
|
50
|
+
}
|
|
51
|
+
return {type: 'branch', size: node.size, data};
|
|
52
|
+
}
|
|
53
|
+
if (node.type !== 'leaf' || !record(node.data)) return;
|
|
54
|
+
const {id, views, activeView} = node.data;
|
|
55
|
+
if (!validId(id) || groups.has(id) || !Array.isArray(views) || !views.length ||
|
|
56
|
+
views.length > limit || !validId(activeView) || !views.includes(activeView)) return;
|
|
57
|
+
groups.add(id);
|
|
58
|
+
for (const panel of views) {
|
|
59
|
+
if (!validId(panel) || ids.has(panel) || ids.size >= limit) return;
|
|
60
|
+
ids.add(panel);
|
|
61
|
+
}
|
|
62
|
+
return {type: 'leaf', size: node.size, data: {id, views, activeView}};
|
|
63
|
+
}
|
|
64
|
+
const root = readNode(input.grid.root, 0);
|
|
65
|
+
if (root?.type !== 'branch' || !groups.has(input.activeGroup) ||
|
|
66
|
+
ids.size !== Object.keys(input.panels).length) return;
|
|
67
|
+
const panels: WorkspaceLayout['panels'] = Object.create(null);
|
|
68
|
+
const documents = new Set<string>();
|
|
69
|
+
let hasStage = false;
|
|
70
|
+
for (const id of ids) {
|
|
71
|
+
const panel = input.panels[id];
|
|
72
|
+
if (!Object.hasOwn(input.panels, id) || !record(panel)) return;
|
|
73
|
+
if (panel.kind === 'sprite' && validId(panel.name) && typeof panel.stage === 'boolean' &&
|
|
74
|
+
typeof panel.section === 'number' && Number.isInteger(panel.section) && panel.section >= 0 && panel.section <= 3) {
|
|
75
|
+
panels[id] = {kind: 'sprite', name: panel.name, stage: panel.stage, section: panel.section};
|
|
76
|
+
} else if (panel.kind === 'stage' && !hasStage) {
|
|
77
|
+
hasStage = true;
|
|
78
|
+
panels[id] = {kind: 'stage'};
|
|
79
|
+
} else if (panel.kind === 'document' && validId(panel.providerId) && validId(panel.documentId)) {
|
|
80
|
+
const key = JSON.stringify([panel.providerId, panel.documentId]);
|
|
81
|
+
if (documents.has(key)) return;
|
|
82
|
+
documents.add(key);
|
|
83
|
+
panels[id] = {kind: 'document', providerId: panel.providerId, documentId: panel.documentId};
|
|
84
|
+
} else return;
|
|
85
|
+
}
|
|
86
|
+
const editingView = input.editingView;
|
|
87
|
+
if (editingView !== undefined && (!validId(editingView) || panels[editingView]?.kind !== 'sprite')) return;
|
|
88
|
+
return {version: 1, project, grid: {root, width, height, orientation}, activeGroup: input.activeGroup,
|
|
89
|
+
editingView: typeof editingView === 'string' ? editingView : undefined, panels};
|
|
90
|
+
}
|