@kubohiroya/turbowarp-plus-editor-layout 0.1.1 → 0.1.2
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/README.ja.md +2 -0
- package/README.md +2 -0
- package/dist/sprite-asset-pane.d.ts +37 -0
- package/dist/sprite-asset-pane.js +109 -0
- package/dist/sprite-editor-toolbar.d.ts +25 -0
- package/dist/sprite-editor-toolbar.js +72 -0
- package/dist/sprite-layout-menu.d.ts +11 -0
- package/dist/sprite-layout-menu.js +114 -0
- package/dist/sprite-tabs.d.ts +113 -20
- package/dist/sprite-tabs.js +496 -76
- package/package.json +12 -1
- package/src/editor.css +74 -5
- package/src/sprite-asset-pane.css +169 -0
- package/src/sprite-asset-pane.tsx +168 -0
- package/src/sprite-editor-toolbar.css +108 -0
- package/src/sprite-editor-toolbar.tsx +124 -0
- package/src/sprite-layout-menu.css +54 -0
- package/src/sprite-layout-menu.ts +119 -0
- package/src/sprite-tabs.ts +593 -95
package/src/sprite-tabs.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {createPortal} from 'react-dom';
|
|
3
3
|
import {createDockview, DefaultTab, themeLight} from 'dockview';
|
|
4
|
-
import type {GroupPanelPartInitParameters, IDockviewPanel} from 'dockview';
|
|
4
|
+
import type {DockviewGroupPanel, GroupPanelPartInitParameters, IDockviewPanel} from 'dockview';
|
|
5
|
+
import {createSpriteLayoutMenu} from './sprite-layout-menu.ts';
|
|
6
|
+
import {SpriteAssetPane} from './sprite-asset-pane.tsx';
|
|
7
|
+
import type {SpriteVariable} from './sprite-asset-pane.tsx';
|
|
8
|
+
import {SpriteEditorToolbar, spriteEditorSections} from './sprite-editor-toolbar.tsx';
|
|
9
|
+
import type {SpriteSearchResult} from './sprite-editor-toolbar.tsx';
|
|
5
10
|
|
|
6
11
|
interface CostumeAsset {
|
|
7
12
|
encodeDataURI(): string;
|
|
@@ -19,20 +24,145 @@ interface Project {
|
|
|
19
24
|
editingTarget?: Target;
|
|
20
25
|
runtime: {targets: Target[]};
|
|
21
26
|
setEditingTarget(id: string): void;
|
|
22
|
-
renameSprite(id: string, name: string): void;
|
|
27
|
+
renameSprite?(id: string, name: string): void;
|
|
23
28
|
on(event: 'targetsUpdate', listener: () => void): void;
|
|
24
29
|
removeListener(event: 'targetsUpdate', listener: () => void): void;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
interface
|
|
32
|
+
export interface SpriteTabView {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly targetId: string;
|
|
35
|
+
readonly isStage: boolean;
|
|
36
|
+
readonly section: number;
|
|
37
|
+
readonly groupId: string;
|
|
38
|
+
readonly active: boolean;
|
|
39
|
+
readonly groupActive: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SpriteTabAdapter<Snapshot = unknown> {
|
|
43
|
+
/** Read the state of the mounted surface; the host's selected target may have changed already. */
|
|
44
|
+
capture(view: SpriteTabView): Snapshot;
|
|
45
|
+
renderInactive(container: HTMLElement, view: SpriteTabView, snapshot: Snapshot | undefined): void | (() => void);
|
|
46
|
+
restore(view: SpriteTabView, snapshot: Snapshot | undefined): void;
|
|
47
|
+
dispose?(view: SpriteTabView, snapshot: Snapshot): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Optional bridge used by a native asset editor to restore and report its selection. */
|
|
51
|
+
export interface SpriteAssetSelection {
|
|
52
|
+
restore(target: object, assets: readonly object[]): number | undefined;
|
|
53
|
+
remember(target: object, asset: object | undefined, select?: (index: number) => void): void;
|
|
54
|
+
rememberLayout?(render: SpriteAssetLayout): void;
|
|
55
|
+
getScrollTop?(): number;
|
|
56
|
+
rememberScrollTop?(position: number): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type SpriteAssetKind = 'costumes' | 'sounds';
|
|
60
|
+
type AssetKind = SpriteAssetKind;
|
|
61
|
+
|
|
62
|
+
export interface SpriteAssetPreviewItem {
|
|
63
|
+
asset: object;
|
|
64
|
+
name: string;
|
|
65
|
+
imageUrl?: string;
|
|
66
|
+
buffer?: AudioBuffer;
|
|
67
|
+
duration?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type SpriteAssetLayout = (items: readonly SpriteAssetPreviewItem[], index: number, detail: React.ReactNode, scrollTop: number) => React.ReactNode;
|
|
71
|
+
const variableSection = spriteEditorSections.indexOf('variables');
|
|
72
|
+
|
|
73
|
+
export interface SpriteAssetPreviewData {
|
|
74
|
+
items: readonly SpriteAssetPreviewItem[];
|
|
75
|
+
defaultAsset?: object;
|
|
76
|
+
}
|
|
77
|
+
type AssetSelections = Readonly<Record<AssetKind, SpriteAssetSelection>>;
|
|
78
|
+
|
|
79
|
+
function createAssetSelections(target: Target, source?: {
|
|
80
|
+
selected: Map<AssetKind, object>;
|
|
81
|
+
layouts: Map<AssetKind, SpriteAssetLayout>;
|
|
82
|
+
scroll: Map<AssetKind, number>;
|
|
83
|
+
}) {
|
|
84
|
+
const selected = new Map(source?.selected);
|
|
85
|
+
const layouts = new Map(source?.layouts);
|
|
86
|
+
const scroll = new Map(source?.scroll);
|
|
87
|
+
const selectors = new Map<AssetKind, (index: number) => void>();
|
|
88
|
+
const forKind = (kind: AssetKind): SpriteAssetSelection => ({
|
|
89
|
+
restore(current, assets) {
|
|
90
|
+
if (current !== target) return;
|
|
91
|
+
const index = assets.indexOf(selected.get(kind));
|
|
92
|
+
if (index >= 0) return index;
|
|
93
|
+
},
|
|
94
|
+
remember(current, asset, select) {
|
|
95
|
+
if (current !== target) return;
|
|
96
|
+
if (asset) selected.set(kind, asset);
|
|
97
|
+
else selected.delete(kind);
|
|
98
|
+
if (select) selectors.set(kind, select);
|
|
99
|
+
},
|
|
100
|
+
rememberLayout(render) {
|
|
101
|
+
layouts.set(kind, render);
|
|
102
|
+
},
|
|
103
|
+
getScrollTop: () => scroll.get(kind) ?? 0,
|
|
104
|
+
rememberScrollTop(position) {
|
|
105
|
+
scroll.set(kind, position);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const ports: AssetSelections = {costumes: forKind('costumes'), sounds: forKind('sounds')};
|
|
109
|
+
return {selected, ports, layouts, selectors, scroll};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type SpriteSplitDirection = 'right' | 'below';
|
|
113
|
+
|
|
114
|
+
interface TabOptions<Snapshot = unknown> {
|
|
28
115
|
vm: Project;
|
|
29
116
|
getSection(): number;
|
|
30
117
|
activateSection(section: number): void;
|
|
31
|
-
selectTarget(id: string): void;
|
|
118
|
+
selectTarget(id: string, assetSelections?: AssetSelections): void;
|
|
32
119
|
resize(): void;
|
|
33
|
-
park?(): void;
|
|
34
120
|
reopenLabel: string;
|
|
121
|
+
multiView?: boolean;
|
|
122
|
+
variables?: boolean;
|
|
123
|
+
locale?: string;
|
|
124
|
+
viewAdapter?: SpriteTabAdapter<Snapshot>;
|
|
125
|
+
park?(): void;
|
|
35
126
|
renameLabels?: RenameLabels;
|
|
127
|
+
onToolbarSlots?(slots: readonly ToolbarSlot[]): void;
|
|
128
|
+
onAssetPreviewSlots?(slots: readonly AssetPreviewSlot[]): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ToolbarActions {
|
|
132
|
+
activate(): void;
|
|
133
|
+
setSection(section: number): void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface ToolbarSlot {
|
|
137
|
+
view: SpriteTabView;
|
|
138
|
+
element: HTMLElement;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface AssetPreviewSlot {
|
|
142
|
+
id: string;
|
|
143
|
+
targetId: string;
|
|
144
|
+
kind: AssetKind | 'variables';
|
|
145
|
+
editable: boolean;
|
|
146
|
+
selected?: object;
|
|
147
|
+
renderLayout?: SpriteAssetLayout;
|
|
148
|
+
scrollTop?: number;
|
|
149
|
+
element: HTMLElement;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface TabSlot {
|
|
153
|
+
body: HTMLElement;
|
|
154
|
+
toolbar?: HTMLElement;
|
|
155
|
+
assetPreview?: HTMLElement;
|
|
156
|
+
clearInactive?: () => void;
|
|
157
|
+
inactiveSection?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface View<Snapshot> {
|
|
161
|
+
id: string;
|
|
162
|
+
target: Target;
|
|
163
|
+
section: number;
|
|
164
|
+
assets: ReturnType<typeof createAssetSelections>;
|
|
165
|
+
snapshots?: Map<number, {snapshot: Snapshot; view: SpriteTabView}>;
|
|
36
166
|
}
|
|
37
167
|
|
|
38
168
|
const renameLabels = {
|
|
@@ -65,34 +195,90 @@ class SpriteTab extends DefaultTab {
|
|
|
65
195
|
}
|
|
66
196
|
}
|
|
67
197
|
|
|
68
|
-
export function mountSpriteTabs(root: HTMLElement, content: HTMLElement, options: TabOptions) {
|
|
69
|
-
const {vm} = options;
|
|
198
|
+
export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElement, options: TabOptions<Snapshot>) {
|
|
199
|
+
const {vm, multiView = false, locale = 'en'} = options;
|
|
200
|
+
const viewAdapter = multiView ? options.viewAdapter : undefined;
|
|
201
|
+
const usesNativeSection = (section: number) => section !== variableSection || !options.variables;
|
|
70
202
|
const document = root.ownerDocument;
|
|
71
|
-
const
|
|
72
|
-
const
|
|
203
|
+
const editLabel = /^ja(?:-|$)/i.test(locale) ? 'クリックしてこのビューで編集' : 'Activate this view to edit';
|
|
204
|
+
const slots = new Map<string, TabSlot>();
|
|
205
|
+
const views = new Map<string, View<Snapshot>>();
|
|
73
206
|
const costumeIcons = new WeakMap<CostumeAsset, string>();
|
|
74
|
-
const
|
|
207
|
+
const remembered = new WeakMap<Target, number>();
|
|
208
|
+
const lastUsed = new WeakMap<Target, View<Snapshot>>();
|
|
75
209
|
const closed = new WeakSet<Target>();
|
|
76
|
-
let active:
|
|
210
|
+
let active: View<Snapshot> | undefined;
|
|
77
211
|
let observed = vm.editingTarget;
|
|
78
212
|
let syncing = false;
|
|
213
|
+
let nextView = 0;
|
|
79
214
|
let renameDialog: HTMLDialogElement | undefined;
|
|
80
215
|
const text = options.renameLabels ?? renameLabels.en;
|
|
81
216
|
|
|
82
217
|
const dockview = createDockview(root, {
|
|
83
218
|
theme: themeLight,
|
|
84
219
|
disableFloatingGroups: true,
|
|
220
|
+
...(multiView && {createRightHeaderActionComponent: createLayoutMenu}),
|
|
85
221
|
defaultTabComponent: 'sprite',
|
|
86
222
|
createTabComponent: () => new SpriteTab(document),
|
|
87
223
|
getTabContextMenuItems: ({panel}) => {
|
|
88
|
-
const target =
|
|
89
|
-
if (!target || target.isStage) return [];
|
|
224
|
+
const target = views.get(panel.id)?.target;
|
|
225
|
+
if (!target || target.isStage || !vm.renameSprite) return [];
|
|
90
226
|
return [{label: text.title, action: () => openRenameDialog(target)}];
|
|
91
227
|
},
|
|
92
228
|
createComponent: () => {
|
|
93
229
|
const element = document.createElement('div');
|
|
94
230
|
element.className = 'tw-plus-sprite-tab-slot';
|
|
95
|
-
|
|
231
|
+
const body = multiView ? document.createElement('div') : element;
|
|
232
|
+
const toolbar = multiView && options.onToolbarSlots ? document.createElement('div') : undefined;
|
|
233
|
+
const assetPreview = multiView && options.onAssetPreviewSlots ? document.createElement('div') : undefined;
|
|
234
|
+
let id: string;
|
|
235
|
+
let dimensions: {dispose(): void} | undefined;
|
|
236
|
+
if (multiView) {
|
|
237
|
+
element.classList.add('tw-plus-sprite-tab-slot--split');
|
|
238
|
+
body.className = 'tw-plus-sprite-tab-body';
|
|
239
|
+
if (toolbar) body.setAttribute('role', 'tabpanel');
|
|
240
|
+
if (toolbar) element.append(toolbar);
|
|
241
|
+
if (assetPreview) {
|
|
242
|
+
assetPreview.className = 'tw-plus-sprite-tab-asset-preview';
|
|
243
|
+
assetPreview.hidden = true;
|
|
244
|
+
assetPreview.tabIndex = 0;
|
|
245
|
+
assetPreview.setAttribute('role', 'group');
|
|
246
|
+
assetPreview.setAttribute('aria-label', editLabel);
|
|
247
|
+
assetPreview.addEventListener('keydown', event => {
|
|
248
|
+
if (event.target !== assetPreview || (event.key !== 'Enter' && event.key !== ' ')) return;
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
event.stopPropagation();
|
|
251
|
+
activateView(id);
|
|
252
|
+
});
|
|
253
|
+
body.append(assetPreview);
|
|
254
|
+
}
|
|
255
|
+
element.append(body);
|
|
256
|
+
body.addEventListener('click', event => {
|
|
257
|
+
if (active?.id === id) return;
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
event.stopPropagation();
|
|
260
|
+
activateView(id);
|
|
261
|
+
}, {capture: true});
|
|
262
|
+
}
|
|
263
|
+
const slot: TabSlot = {body, toolbar, assetPreview};
|
|
264
|
+
return {
|
|
265
|
+
element,
|
|
266
|
+
init(params) {
|
|
267
|
+
id = params.api.id;
|
|
268
|
+
slots.set(id, slot);
|
|
269
|
+
if (toolbar) body.id = id + '-content';
|
|
270
|
+
if (multiView) {
|
|
271
|
+
dimensions = params.api.onDidDimensionsChange(() => {
|
|
272
|
+
if (active?.id === id && body.contains(content)) options.resize();
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
dispose() {
|
|
277
|
+
dimensions?.dispose();
|
|
278
|
+
clearInactive(slot);
|
|
279
|
+
if (slots.get(id) === slot) slots.delete(id);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
96
282
|
},
|
|
97
283
|
createWatermarkComponent: () => {
|
|
98
284
|
const button = document.createElement('button');
|
|
@@ -104,36 +290,24 @@ export function mountSpriteTabs(root: HTMLElement, content: HTMLElement, options
|
|
|
104
290
|
return {element: button, init() {}};
|
|
105
291
|
}
|
|
106
292
|
});
|
|
107
|
-
// The host has one editable surface. Keep reordering inside its tab group.
|
|
108
293
|
const preventSplit = (event: {kind: string; position: string; preventDefault(): void}) => {
|
|
109
294
|
if (event.kind === 'edge' || event.kind === 'content' && event.position !== 'center') event.preventDefault();
|
|
110
295
|
};
|
|
111
|
-
const overlay = dockview.onWillShowOverlay(preventSplit);
|
|
112
|
-
const drop = dockview.onWillDrop(preventSplit);
|
|
113
|
-
|
|
114
|
-
function show(panel: IDockviewPanel | undefined) {
|
|
115
|
-
const next = targets.get(panel?.id);
|
|
116
|
-
if (active === next) return;
|
|
117
|
-
if (active) sections.set(active, options.getSection());
|
|
118
|
-
const section = sections.get(next) ?? options.getSection();
|
|
119
|
-
active = next;
|
|
120
|
-
if (!next) {
|
|
121
|
-
if (options.park) options.park();
|
|
122
|
-
else content.remove();
|
|
123
|
-
options.selectTarget('');
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
296
|
+
const overlay = !multiView && dockview.onWillShowOverlay(preventSplit);
|
|
297
|
+
const drop = !multiView && dockview.onWillDrop(preventSplit);
|
|
126
298
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
299
|
+
function createLayoutMenu(group: DockviewGroupPanel) {
|
|
300
|
+
const menu = createSpriteLayoutMenu(document, locale, {
|
|
301
|
+
splitRight: () => { if (group.activePanel) split('right', group.activePanel.id); },
|
|
302
|
+
splitBelow: () => { if (group.activePanel) split('below', group.activePanel.id); },
|
|
303
|
+
close: () => { if (group.activePanel) closeSplit(group.activePanel.id); },
|
|
304
|
+
canClose: () => dockview.groups.length > 1
|
|
305
|
+
});
|
|
306
|
+
return {...menu, init() {}};
|
|
133
307
|
}
|
|
134
308
|
|
|
135
309
|
function isOpen(target: Target) {
|
|
136
|
-
return
|
|
310
|
+
return [...views.values()].some(view => view.target === target) && vm.runtime.targets.includes(target);
|
|
137
311
|
}
|
|
138
312
|
|
|
139
313
|
function openRenameDialog(target: Target) {
|
|
@@ -164,7 +338,7 @@ export function mountSpriteTabs(root: HTMLElement, content: HTMLElement, options
|
|
|
164
338
|
if (renameDialog !== dialog) return;
|
|
165
339
|
renameDialog = undefined;
|
|
166
340
|
if (dialog.returnValue === 'save' && input.value.trim() && isOpen(target) &&
|
|
167
|
-
input.value !== target.getName()) vm.renameSprite(target.id, input.value);
|
|
341
|
+
input.value !== target.getName()) vm.renameSprite?.(target.id, input.value);
|
|
168
342
|
}, {once: true});
|
|
169
343
|
renameDialog = dialog;
|
|
170
344
|
root.append(dialog);
|
|
@@ -179,19 +353,196 @@ export function mountSpriteTabs(root: HTMLElement, content: HTMLElement, options
|
|
|
179
353
|
return costumeIcons.get(asset);
|
|
180
354
|
}
|
|
181
355
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
356
|
+
function describe(view: View<Snapshot>, panel = dockview.getPanel(view.id)): SpriteTabView {
|
|
357
|
+
return {
|
|
358
|
+
id: view.id, targetId: view.target.id, isStage: view.target.isStage,
|
|
359
|
+
section: view.section, groupId: panel?.api.group.id ?? '',
|
|
360
|
+
active: active === view, groupActive: panel?.api.isGroupActive ?? false
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function clearInactive(slot: TabSlot) {
|
|
365
|
+
if (slot.assetPreview) slot.assetPreview.hidden = true;
|
|
366
|
+
if (slot.inactiveSection === undefined) return;
|
|
367
|
+
slot.clearInactive?.();
|
|
368
|
+
slot.clearInactive = undefined;
|
|
369
|
+
slot.inactiveSection = undefined;
|
|
370
|
+
// Keep the React portal host mounted while its children reconcile.
|
|
371
|
+
slot.body.replaceChildren(...(slot.assetPreview ? [slot.assetPreview] : []));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function refreshSurfaces() {
|
|
375
|
+
if (!multiView) return;
|
|
376
|
+
const toolbars: ToolbarSlot[] = [];
|
|
377
|
+
const assetPreviews: AssetPreviewSlot[] = [];
|
|
378
|
+
for (const [id, slot] of slots) {
|
|
379
|
+
const view = views.get(id);
|
|
380
|
+
const panel = dockview.getPanel(id);
|
|
381
|
+
if (!view || !panel || panel.api.group.activePanel !== panel) {
|
|
382
|
+
clearInactive(slot);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const descriptor = describe(view, panel);
|
|
386
|
+
if (slot.toolbar) {
|
|
387
|
+
toolbars.push({view: descriptor, element: slot.toolbar});
|
|
388
|
+
slot.body.setAttribute('aria-labelledby', id + '-section-' + (spriteEditorSections[view.section] ?? 'code'));
|
|
389
|
+
}
|
|
390
|
+
if (view.section === variableSection && options.variables && slot.assetPreview) {
|
|
391
|
+
slot.assetPreview.hidden = false;
|
|
392
|
+
assetPreviews.push({id, targetId: descriptor.targetId, kind: 'variables', editable: active === view,
|
|
393
|
+
element: slot.assetPreview});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (active === view) continue;
|
|
397
|
+
const kind = view.section === 1 ? 'costumes' : view.section === 2 ? 'sounds' : undefined;
|
|
398
|
+
if (kind && slot.assetPreview) {
|
|
399
|
+
clearInactive(slot);
|
|
400
|
+
slot.assetPreview.hidden = false;
|
|
401
|
+
assetPreviews.push({id, targetId: descriptor.targetId, kind, element: slot.assetPreview,
|
|
402
|
+
selected: view.assets.selected.get(kind), editable: false, renderLayout: view.assets.layouts.get(kind), scrollTop: view.assets.scroll.get(kind)});
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (slot.inactiveSection === view.section) continue;
|
|
406
|
+
clearInactive(slot);
|
|
407
|
+
slot.inactiveSection = view.section;
|
|
408
|
+
if (viewAdapter) {
|
|
409
|
+
const cleanup = viewAdapter.renderInactive(slot.body, descriptor, view.snapshots?.get(view.section)?.snapshot);
|
|
410
|
+
if (cleanup) slot.clearInactive = cleanup;
|
|
411
|
+
} else {
|
|
412
|
+
const button = slot.body.ownerDocument.createElement('button');
|
|
413
|
+
button.type = 'button';
|
|
414
|
+
button.textContent = editLabel;
|
|
415
|
+
slot.body.append(button);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
options.onToolbarSlots?.(toolbars);
|
|
419
|
+
options.onAssetPreviewSlots?.(assetPreviews);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function remember(view: View<Snapshot>) {
|
|
423
|
+
if (usesNativeSection(view.section)) view.section = options.getSection();
|
|
424
|
+
remembered.set(view.target, view.section);
|
|
425
|
+
if (!viewAdapter || !usesNativeSection(view.section) || !slots.get(view.id)?.body.contains(content)) return;
|
|
426
|
+
const descriptor = describe(view);
|
|
427
|
+
const snapshot = viewAdapter.capture(descriptor);
|
|
428
|
+
view.snapshots ??= new Map();
|
|
429
|
+
const previous = view.snapshots.get(view.section);
|
|
430
|
+
if (previous && snapshot !== previous.snapshot) viewAdapter.dispose?.(previous.view, previous.snapshot);
|
|
431
|
+
view.snapshots.set(view.section, {snapshot, view: descriptor});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function show(panel: IDockviewPanel | undefined) {
|
|
435
|
+
const next = views.get(panel?.id);
|
|
436
|
+
if (active === next) {
|
|
437
|
+
refreshSurfaces();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (active && views.get(active.id) === active) remember(active);
|
|
441
|
+
active = next;
|
|
442
|
+
if (!next) {
|
|
443
|
+
if (options.park) options.park();
|
|
444
|
+
else content.remove();
|
|
445
|
+
options.selectTarget('');
|
|
446
|
+
} else {
|
|
447
|
+
if (!multiView) next.section = remembered.get(next.target) ?? options.getSection();
|
|
448
|
+
const slot = slots.get(next.id);
|
|
449
|
+
clearInactive(slot);
|
|
450
|
+
slot.body.appendChild(content);
|
|
451
|
+
if (vm.editingTarget !== next.target) vm.setEditingTarget(next.target.id);
|
|
452
|
+
observed = vm.editingTarget;
|
|
453
|
+
options.selectTarget(next.target.id, multiView ? next.assets.ports : undefined);
|
|
454
|
+
content.hidden = !usesNativeSection(next.section);
|
|
455
|
+
if (usesNativeSection(next.section)) options.activateSection(next.section);
|
|
456
|
+
viewAdapter?.restore(describe(next, panel), next.snapshots?.get(next.section)?.snapshot);
|
|
457
|
+
options.resize();
|
|
458
|
+
}
|
|
459
|
+
refreshSurfaces();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function add(target: Target, splitPosition?: {referencePanel: string; direction: SpriteSplitDirection}, section?: number) {
|
|
463
|
+
let id = target.id;
|
|
464
|
+
if (splitPosition || views.has(id)) {
|
|
465
|
+
do {
|
|
466
|
+
id = 'sprite-view-' + ++nextView;
|
|
467
|
+
} while (views.has(id) || vm.runtime.targets.some(item => item.id === id));
|
|
468
|
+
}
|
|
469
|
+
const source = splitPosition && views.get(splitPosition.referencePanel);
|
|
470
|
+
views.set(id, {id, target, section: section ?? remembered.get(target) ?? options.getSection(),
|
|
471
|
+
assets: createAssetSelections(target, source?.assets)});
|
|
472
|
+
try {
|
|
473
|
+
return dockview.addPanel({id, title: target.getName(), component: 'sprite', inactive: !splitPosition,
|
|
474
|
+
position: splitPosition, params: {icon: getIcon(target)}});
|
|
475
|
+
} catch (error) {
|
|
476
|
+
views.delete(id);
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function activateView(id: string) {
|
|
482
|
+
const view = views.get(id);
|
|
483
|
+
if (!view) return false;
|
|
484
|
+
lastUsed.set(view.target, view);
|
|
485
|
+
dockview.getPanel(id)?.api.setActive();
|
|
486
|
+
return true;
|
|
188
487
|
}
|
|
189
488
|
|
|
190
489
|
function select(id: string) {
|
|
191
490
|
const target = vm.runtime.targets.find(item => item.id === id && item.isOriginal);
|
|
192
491
|
if (!target) return;
|
|
193
492
|
closed.delete(target);
|
|
194
|
-
(
|
|
493
|
+
if (active?.target === target && views.get(active.id) === active) {
|
|
494
|
+
activateView(active.id);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const recent = lastUsed.get(target);
|
|
498
|
+
const view = recent && views.get(recent.id) === recent ? recent :
|
|
499
|
+
[...views.values()].find(item => item.target === target);
|
|
500
|
+
activateView(view?.id ?? add(target).id);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function split(direction: SpriteSplitDirection, id = active?.id) {
|
|
504
|
+
const reference = views.get(id);
|
|
505
|
+
if (!multiView || !reference) return;
|
|
506
|
+
const section = reference === active && usesNativeSection(reference.section) ? options.getSection() : reference.section;
|
|
507
|
+
const panel = add(reference.target, {referencePanel: id, direction}, section);
|
|
508
|
+
activateView(panel.id);
|
|
509
|
+
return panel.id;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function closeSplit(id = active?.id) {
|
|
513
|
+
const panel = dockview.getPanel(id);
|
|
514
|
+
if (!multiView || !panel || dockview.groups.length <= 1) return false;
|
|
515
|
+
syncing = true;
|
|
516
|
+
try {
|
|
517
|
+
dockview.removeGroup(panel.api.group);
|
|
518
|
+
show(dockview.activePanel);
|
|
519
|
+
} finally {
|
|
520
|
+
syncing = false;
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function setViewSection(id: string, section: number) {
|
|
526
|
+
const view = views.get(id);
|
|
527
|
+
if (!view || !Number.isInteger(section) || section < 0 || section > (options.variables ? variableSection : 2) || view.section === section) return false;
|
|
528
|
+
if (view === active && viewAdapter) remember(view);
|
|
529
|
+
view.section = section;
|
|
530
|
+
if (view === active) {
|
|
531
|
+
content.hidden = !usesNativeSection(section);
|
|
532
|
+
if (usesNativeSection(section)) {
|
|
533
|
+
options.activateSection(section);
|
|
534
|
+
options.resize();
|
|
535
|
+
}
|
|
536
|
+
viewAdapter?.restore(describe(view), view.snapshots?.get(section)?.snapshot);
|
|
537
|
+
}
|
|
538
|
+
refreshSurfaces();
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function syncActiveSection(section: number) {
|
|
543
|
+
if (!active || !usesNativeSection(active.section) || active.section === section) return;
|
|
544
|
+
active.section = section;
|
|
545
|
+
refreshSurfaces();
|
|
195
546
|
}
|
|
196
547
|
|
|
197
548
|
function refresh() {
|
|
@@ -200,17 +551,21 @@ export function mountSpriteTabs(root: HTMLElement, content: HTMLElement, options
|
|
|
200
551
|
try {
|
|
201
552
|
const available = new Set(vm.runtime.targets.filter(target => target.isOriginal));
|
|
202
553
|
const editing = vm.editingTarget;
|
|
203
|
-
for (const [id,
|
|
204
|
-
if (!available.has(target)) dockview.getPanel(id)?.api.close();
|
|
554
|
+
for (const [id, view] of views) {
|
|
555
|
+
if (!available.has(view.target)) dockview.getPanel(id)?.api.close();
|
|
556
|
+
}
|
|
557
|
+
const represented = new Set<Target>();
|
|
558
|
+
for (const [id, view] of views) {
|
|
559
|
+
represented.add(view.target);
|
|
560
|
+
const panel = dockview.getPanel(id);
|
|
561
|
+
if (panel && panel.title !== view.target.getName()) panel.api.setTitle(view.target.getName());
|
|
562
|
+
const icon = getIcon(view.target);
|
|
563
|
+
if (panel && panel.api.getParameters().icon !== icon) panel.api.updateParameters({icon});
|
|
205
564
|
}
|
|
206
565
|
for (const target of available) {
|
|
207
|
-
if (target.isStage
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (panel.title !== target.getName()) panel.api.setTitle(target.getName());
|
|
211
|
-
const icon = getIcon(target);
|
|
212
|
-
if (panel.api.getParameters().icon !== icon) panel.api.updateParameters({icon});
|
|
213
|
-
} else if (!closed.has(target)) add(target);
|
|
566
|
+
if (!represented.has(target) && (!target.isStage || target === editing) && !closed.has(target)) {
|
|
567
|
+
add(target);
|
|
568
|
+
}
|
|
214
569
|
}
|
|
215
570
|
if (editing && (observed !== editing || !active && !closed.has(editing))) {
|
|
216
571
|
observed = editing;
|
|
@@ -222,6 +577,14 @@ export function mountSpriteTabs(root: HTMLElement, content: HTMLElement, options
|
|
|
222
577
|
}
|
|
223
578
|
}
|
|
224
579
|
|
|
580
|
+
function disposeView(view: View<Snapshot>) {
|
|
581
|
+
for (const stored of view.snapshots?.values() ?? []) viewAdapter?.dispose?.(stored.view, stored.snapshot);
|
|
582
|
+
const slot = slots.get(view.id);
|
|
583
|
+
if (slot) clearInactive(slot);
|
|
584
|
+
slots.delete(view.id);
|
|
585
|
+
views.delete(view.id);
|
|
586
|
+
}
|
|
587
|
+
|
|
225
588
|
const selection = dockview.onDidActivePanelChange(({panel}) => {
|
|
226
589
|
if (syncing) return;
|
|
227
590
|
syncing = true;
|
|
@@ -232,45 +595,98 @@ export function mountSpriteTabs(root: HTMLElement, content: HTMLElement, options
|
|
|
232
595
|
}
|
|
233
596
|
});
|
|
234
597
|
const removal = dockview.onDidRemovePanel(panel => {
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
598
|
+
const view = views.get(panel.id);
|
|
599
|
+
if (!view) return;
|
|
600
|
+
if (view === active) remembered.set(view.target, usesNativeSection(view.section) ? options.getSection() : view.section);
|
|
601
|
+
disposeView(view);
|
|
602
|
+
if (![...views.values()].some(item => item.target === view.target)) closed.add(view.target);
|
|
239
603
|
});
|
|
604
|
+
const layout = multiView && dockview.onDidLayoutChange(refreshSurfaces);
|
|
240
605
|
vm.on('targetsUpdate', refresh);
|
|
241
606
|
refresh();
|
|
242
607
|
return {
|
|
243
|
-
select,
|
|
608
|
+
select, split, closeSplit, activateView, setViewSection, syncActiveSection, refreshSurfaces,
|
|
609
|
+
selectAsset(id: string, kind: AssetKind, index: number) {
|
|
610
|
+
if (active?.id === id && active.section === (kind === 'costumes' ? 1 : 2)) {
|
|
611
|
+
active.assets.selectors.get(kind)?.(index);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
currentView: () => active && describe(active),
|
|
615
|
+
views: () => [...views.values()].map(view => describe(view)),
|
|
244
616
|
dispose() {
|
|
245
617
|
renameDialog?.remove();
|
|
246
618
|
renameDialog = undefined;
|
|
247
619
|
vm.removeListener('targetsUpdate', refresh);
|
|
248
620
|
selection.dispose();
|
|
249
621
|
removal.dispose();
|
|
250
|
-
overlay.dispose();
|
|
251
|
-
drop.dispose();
|
|
622
|
+
if (overlay) overlay.dispose();
|
|
623
|
+
if (drop) drop.dispose();
|
|
624
|
+
if (layout) layout.dispose();
|
|
625
|
+
for (const view of views.values()) disposeView(view);
|
|
252
626
|
content.remove();
|
|
253
627
|
dockview.dispose();
|
|
254
628
|
}
|
|
255
629
|
};
|
|
256
630
|
}
|
|
257
631
|
|
|
258
|
-
|
|
632
|
+
type SpriteTabsController<Snapshot> = ReturnType<typeof mountSpriteTabs<Snapshot>>;
|
|
633
|
+
|
|
634
|
+
type SpriteTabsProps<Snapshot> = Omit<TabOptions<Snapshot>, 'activateSection' | 'getSection' | 'park' | 'onToolbarSlots' | 'onAssetPreviewSlots'> & {
|
|
635
|
+
children: React.ReactNode;
|
|
636
|
+
activateSection?(section: number): void;
|
|
637
|
+
slots: HTMLElement[];
|
|
638
|
+
onAttach(): void;
|
|
259
639
|
enabled: boolean;
|
|
260
640
|
activeSection: number;
|
|
261
|
-
vm: Project;
|
|
262
641
|
className: string;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
642
|
+
connect(controller: SpriteTabsController<Snapshot> | null): void;
|
|
643
|
+
activeTools?: React.ReactNode;
|
|
644
|
+
renderToolbar?(view: SpriteTabView, actions: ToolbarActions): React.ReactNode;
|
|
645
|
+
getAssetPreview?(targetId: string, kind: SpriteAssetKind): SpriteAssetPreviewData;
|
|
646
|
+
getVariables?(targetId: string): readonly SpriteVariable[];
|
|
647
|
+
writeVariable?(targetId: string, id: string, value: SpriteVariable['value']): void;
|
|
648
|
+
getCodeSearchItems?(targetId: string): readonly SpriteSearchResult[];
|
|
649
|
+
openCodeSearchResult?(targetId: string, id: string): void;
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
function AssetPreview({locale, data, kind, selected, renderLayout, scrollTop}: {
|
|
653
|
+
locale: string;
|
|
654
|
+
data: SpriteAssetPreviewData;
|
|
655
|
+
kind: SpriteAssetKind;
|
|
656
|
+
selected?: object;
|
|
657
|
+
renderLayout?: SpriteAssetLayout;
|
|
658
|
+
scrollTop?: number;
|
|
659
|
+
}) {
|
|
660
|
+
const chosen = data.items.find(item => item.asset === selected) ??
|
|
661
|
+
data.items.find(item => item.asset === data.defaultAsset) ?? data.items[0];
|
|
662
|
+
const peaks = React.useMemo(() => {
|
|
663
|
+
if (!chosen?.buffer?.length) return undefined;
|
|
664
|
+
const samples = chosen.buffer.getChannelData(0);
|
|
665
|
+
const chunk = Math.ceil(samples.length / 192);
|
|
666
|
+
const levels: number[] = [];
|
|
667
|
+
for (let offset = 0; offset < samples.length; offset += chunk) {
|
|
668
|
+
let peak = 0;
|
|
669
|
+
for (let i = offset; i < Math.min(offset + chunk, samples.length); i++) {
|
|
670
|
+
peak = Math.max(peak, Math.abs(samples[i]));
|
|
671
|
+
}
|
|
672
|
+
levels.push(peak);
|
|
673
|
+
}
|
|
674
|
+
return levels;
|
|
675
|
+
}, [chosen?.buffer]);
|
|
676
|
+
const index = chosen ? data.items.indexOf(chosen) : -1;
|
|
677
|
+
const pane = React.createElement(SpriteAssetPane, {
|
|
678
|
+
locale, kind, editable: false, onSelect() {},
|
|
679
|
+
detailOnly: Boolean(renderLayout),
|
|
680
|
+
selectedId: chosen ? String(index) : null,
|
|
681
|
+
items: data.items.map((item, position) => ({
|
|
682
|
+
id: String(position), name: item.name, imageUrl: item.imageUrl,
|
|
683
|
+
duration: item.duration ?? item.buffer?.duration, peaks: item === chosen ? peaks : undefined
|
|
684
|
+
}))
|
|
685
|
+
});
|
|
686
|
+
return renderLayout ? renderLayout(data.items, index, pane, scrollTop ?? 0) : pane;
|
|
271
687
|
}
|
|
272
688
|
|
|
273
|
-
function SpriteTabs(props: SpriteTabsProps) {
|
|
689
|
+
function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
|
|
274
690
|
const root = React.useRef<HTMLDivElement>(null);
|
|
275
691
|
const sections = React.useRef<HTMLDivElement>(null);
|
|
276
692
|
const parking = React.useRef<HTMLDivElement>(null);
|
|
@@ -281,6 +697,12 @@ function SpriteTabs(props: SpriteTabsProps) {
|
|
|
281
697
|
node.className = 'tw-plus-sprite-tab-editor';
|
|
282
698
|
return node;
|
|
283
699
|
});
|
|
700
|
+
const [toolbarSlots, setToolbarSlots] = React.useState<readonly ToolbarSlot[]>([]);
|
|
701
|
+
const [assetPreviewSlots, setAssetPreviewSlots] = React.useState<readonly AssetPreviewSlot[]>([]);
|
|
702
|
+
const controller = React.useRef<SpriteTabsController<Snapshot>>(null);
|
|
703
|
+
const hasVariables = Boolean(props.getVariables);
|
|
704
|
+
const hasToolbar = Boolean(props.renderToolbar) || hasVariables;
|
|
705
|
+
const hasAssetPreview = Boolean(props.getAssetPreview) || hasVariables;
|
|
284
706
|
React.useLayoutEffect(() => {
|
|
285
707
|
sections.current.append(...props.slots);
|
|
286
708
|
parking.current.appendChild(content);
|
|
@@ -291,19 +713,26 @@ function SpriteTabs(props: SpriteTabsProps) {
|
|
|
291
713
|
}, [content, props.slots]);
|
|
292
714
|
React.useLayoutEffect(() => {
|
|
293
715
|
if (!props.enabled) {
|
|
716
|
+
setToolbarSlots([]);
|
|
717
|
+
setAssetPreviewSlots([]);
|
|
718
|
+
content.hidden = false;
|
|
294
719
|
root.current.appendChild(content);
|
|
295
720
|
latest.current.resize();
|
|
296
721
|
return () => content.remove();
|
|
297
722
|
}
|
|
298
|
-
const park = () => parking.current?.appendChild(content);
|
|
299
|
-
park();
|
|
300
723
|
const frame = content.ownerDocument.defaultView;
|
|
301
724
|
let restoreFrame: number;
|
|
302
|
-
const
|
|
725
|
+
const park = () => parking.current?.appendChild(content);
|
|
726
|
+
park();
|
|
727
|
+
const instance = mountSpriteTabs(root.current, content, {
|
|
303
728
|
vm: props.vm,
|
|
304
729
|
getSection: () => latest.current.activeSection,
|
|
305
730
|
activateSection(section) {
|
|
306
731
|
frame.cancelAnimationFrame(restoreFrame);
|
|
732
|
+
if (latest.current.activateSection) {
|
|
733
|
+
latest.current.activateSection(section);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
307
736
|
// Dockview attaches the active panel after dispatching the selection event.
|
|
308
737
|
restoreFrame = frame.requestAnimationFrame(() => {
|
|
309
738
|
if (content.isConnected && section !== latest.current.activeSection) {
|
|
@@ -311,38 +740,104 @@ function SpriteTabs(props: SpriteTabsProps) {
|
|
|
311
740
|
}
|
|
312
741
|
});
|
|
313
742
|
},
|
|
314
|
-
selectTarget: id => latest.current.selectTarget(id),
|
|
315
|
-
resize: () => latest.current.resize(),
|
|
316
743
|
park,
|
|
744
|
+
renameLabels: props.renameLabels,
|
|
745
|
+
selectTarget: (id, assets) => latest.current.selectTarget(id, assets),
|
|
746
|
+
resize: () => latest.current.resize(),
|
|
317
747
|
reopenLabel: props.reopenLabel,
|
|
318
|
-
|
|
748
|
+
multiView: props.multiView,
|
|
749
|
+
variables: hasVariables,
|
|
750
|
+
locale: props.locale,
|
|
751
|
+
viewAdapter: props.viewAdapter,
|
|
752
|
+
onToolbarSlots: hasToolbar ? setToolbarSlots : undefined,
|
|
753
|
+
onAssetPreviewSlots: hasAssetPreview ? setAssetPreviewSlots : undefined
|
|
319
754
|
});
|
|
320
|
-
|
|
755
|
+
controller.current = instance;
|
|
756
|
+
latest.current.connect(instance);
|
|
321
757
|
return () => {
|
|
322
758
|
frame.cancelAnimationFrame(restoreFrame);
|
|
759
|
+
controller.current = null;
|
|
323
760
|
latest.current.connect(null);
|
|
324
|
-
|
|
761
|
+
instance.dispose();
|
|
325
762
|
};
|
|
326
|
-
}, [content, props.enabled, props.vm, props.reopenLabel, props.
|
|
763
|
+
}, [content, props.enabled, props.vm, props.reopenLabel, props.multiView, props.locale,
|
|
764
|
+
props.viewAdapter, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
765
|
+
React.useLayoutEffect(() => {
|
|
766
|
+
controller.current?.syncActiveSection(props.activeSection);
|
|
767
|
+
}, [props.activeSection]);
|
|
768
|
+
const workspace = controller.current;
|
|
327
769
|
// The native tab controller also reacts to bubbled events from elements with role="tab".
|
|
328
770
|
const isolateDockviewTabs = (event: React.SyntheticEvent) => {
|
|
329
771
|
if (!content.contains(event.target as Node)) event.stopPropagation();
|
|
330
772
|
};
|
|
331
|
-
return React.createElement('div', {className: props.className
|
|
773
|
+
return React.createElement('div', {className: props.className,
|
|
774
|
+
'data-multi-view': props.enabled && props.multiView || undefined},
|
|
332
775
|
React.createElement('div', {ref: root, onClick: isolateDockviewTabs, onKeyDown: isolateDockviewTabs}),
|
|
333
776
|
React.createElement('div', {ref: parking, hidden: true}),
|
|
334
|
-
createPortal(React.createElement(React.Fragment, null,
|
|
335
|
-
React.createElement('div', {
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
777
|
+
createPortal(React.createElement(React.Fragment, null,
|
|
778
|
+
props.multiView && props.getVariables ? React.createElement('div', {hidden: true}, props.children) : props.children,
|
|
779
|
+
React.createElement('div', {ref: sections, className: 'tw-plus-sprite-tab-sections'})), content), toolbarSlots.map(({view, element}) => {
|
|
780
|
+
const actions = {
|
|
781
|
+
activate: () => { workspace?.activateView(view.id); },
|
|
782
|
+
setSection: (section: number) => {
|
|
783
|
+
workspace?.setViewSection(view.id, section);
|
|
784
|
+
workspace?.activateView(view.id);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
const currentSection = spriteEditorSections[view.section] ?? 'code';
|
|
788
|
+
const toolbar = props.renderToolbar?.(view, actions) ?? (props.getVariables &&
|
|
789
|
+
React.createElement(SpriteEditorToolbar, {
|
|
790
|
+
viewId: view.id, locale: props.locale, section: currentSection, isStage: view.isStage,
|
|
791
|
+
activeGroup: view.groupActive,
|
|
792
|
+
actions: view.active && currentSection !== 'variables' ? props.activeTools : undefined,
|
|
793
|
+
onSectionChange: section => actions.setSection(spriteEditorSections.indexOf(section)),
|
|
794
|
+
search: {
|
|
795
|
+
onFocus: actions.activate,
|
|
796
|
+
find(query, section) {
|
|
797
|
+
let items: readonly SpriteSearchResult[] = [];
|
|
798
|
+
if (section === 'code') items = props.getCodeSearchItems?.(view.targetId) ?? [];
|
|
799
|
+
else if (section === 'variables') {
|
|
800
|
+
items = props.getVariables(view.targetId).map(variable => ({id: variable.id, label: variable.name}));
|
|
801
|
+
} else {
|
|
802
|
+
items = props.getAssetPreview?.(view.targetId, section).items.map((item, index) =>
|
|
803
|
+
({id: String(index), label: item.name})) ?? [];
|
|
804
|
+
}
|
|
805
|
+
const text = query.toLocaleLowerCase();
|
|
806
|
+
return items.filter(item => item.label.toLocaleLowerCase().includes(text));
|
|
807
|
+
},
|
|
808
|
+
select(result, section) {
|
|
809
|
+
actions.activate();
|
|
810
|
+
if (section === 'code') props.openCodeSearchResult?.(view.targetId, result.id);
|
|
811
|
+
else if (section === 'variables') {
|
|
812
|
+
const field = assetPreviewSlots.find(slot => slot.id === view.id)?.element;
|
|
813
|
+
[...(field?.querySelectorAll<HTMLInputElement>('input[data-variable-id]') ?? [])]
|
|
814
|
+
.find(input => input.dataset.variableId === result.id)?.focus();
|
|
815
|
+
} else workspace?.selectAsset(view.id, section, Number(result.id));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}));
|
|
819
|
+
return createPortal(toolbar, element, view.id);
|
|
820
|
+
}), assetPreviewSlots.map(({id, targetId, kind, element, editable, selected, renderLayout, scrollTop}) =>
|
|
821
|
+
createPortal(kind === 'variables' ? React.createElement(SpriteAssetPane, {
|
|
822
|
+
kind, locale: props.locale, editable, variables: props.getVariables?.(targetId) ?? [],
|
|
823
|
+
onWrite(variableId, value) {
|
|
824
|
+
props.writeVariable?.(targetId, variableId, value);
|
|
825
|
+
workspace?.refreshSurfaces();
|
|
826
|
+
}
|
|
827
|
+
}) : props.getAssetPreview && React.createElement(AssetPreview, {
|
|
828
|
+
locale: props.locale, kind, selected, renderLayout, scrollTop, data: props.getAssetPreview(targetId, kind)
|
|
829
|
+
}), element, id)));
|
|
341
830
|
}
|
|
342
831
|
|
|
343
|
-
export function useSpriteTabs({available, enabled, locale, ...props}:
|
|
344
|
-
|
|
345
|
-
|
|
832
|
+
export function useSpriteTabs<Snapshot>({available, enabled, locale, ...props}:
|
|
833
|
+
Omit<SpriteTabsProps<Snapshot>, 'children' | 'connect' | 'selectTarget' | 'reopenLabel' | 'renameLabels' |
|
|
834
|
+
'slots' | 'onAttach' | 'activateSection'> & {
|
|
835
|
+
available: boolean;
|
|
836
|
+
locale: string;
|
|
837
|
+
activateSection?(section: number): void;
|
|
838
|
+
}) {
|
|
839
|
+
const controller = React.useRef<SpriteTabsController<Snapshot>>(null);
|
|
840
|
+
const [selection, setSelection] = React.useState<{id: string; assets?: AssetSelections}>({id: ''});
|
|
346
841
|
const [ready, setReady] = React.useState(false);
|
|
347
842
|
const [slots] = React.useState(() => available ? Array.from({length: 3}, () => {
|
|
348
843
|
const slot = document.createElement('div');
|
|
@@ -358,9 +853,12 @@ export function useSpriteTabs({available, enabled, locale, ...props}: SpriteTabS
|
|
|
358
853
|
const japanese = /^ja(?:-|$)/i.test(locale);
|
|
359
854
|
const select = (id: string) => controller.current?.select(id);
|
|
360
855
|
return {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
856
|
+
controller,
|
|
857
|
+
assetSelections: enabled ? selection.assets : undefined,
|
|
858
|
+
context: enabled ? {selectedId: selection.id, openLabel: japanese ? '開く' : 'Open', select, open: select} : null,
|
|
859
|
+
renderEditorTabRow: available ? (children: React.ReactNode) => React.createElement(SpriteTabs<Snapshot>, {
|
|
860
|
+
...props, enabled, locale, children, slots, onAttach: () => setReady(true),
|
|
861
|
+
selectTarget: (id, assets) => setSelection({id, assets}),
|
|
364
862
|
connect: next => { controller.current = next; },
|
|
365
863
|
reopenLabel: japanese ? '編集中のスプライトを開く' : 'Open the editing sprite',
|
|
366
864
|
renameLabels: japanese ? renameLabels.ja : renameLabels.en
|