@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/dist/sprite-tabs.js
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { createDockview, DefaultTab, themeLight } from 'dockview';
|
|
4
|
+
import { createSpriteLayoutMenu } from "./sprite-layout-menu.js";
|
|
5
|
+
import { SpriteAssetPane } from "./sprite-asset-pane.js";
|
|
6
|
+
import { SpriteEditorToolbar, spriteEditorSections } from "./sprite-editor-toolbar.js";
|
|
7
|
+
const variableSection = spriteEditorSections.indexOf('variables');
|
|
8
|
+
function createAssetSelections(target, source) {
|
|
9
|
+
const selected = new Map(source?.selected);
|
|
10
|
+
const layouts = new Map(source?.layouts);
|
|
11
|
+
const scroll = new Map(source?.scroll);
|
|
12
|
+
const selectors = new Map();
|
|
13
|
+
const forKind = (kind) => ({
|
|
14
|
+
restore(current, assets) {
|
|
15
|
+
if (current !== target)
|
|
16
|
+
return;
|
|
17
|
+
const index = assets.indexOf(selected.get(kind));
|
|
18
|
+
if (index >= 0)
|
|
19
|
+
return index;
|
|
20
|
+
},
|
|
21
|
+
remember(current, asset, select) {
|
|
22
|
+
if (current !== target)
|
|
23
|
+
return;
|
|
24
|
+
if (asset)
|
|
25
|
+
selected.set(kind, asset);
|
|
26
|
+
else
|
|
27
|
+
selected.delete(kind);
|
|
28
|
+
if (select)
|
|
29
|
+
selectors.set(kind, select);
|
|
30
|
+
},
|
|
31
|
+
rememberLayout(render) {
|
|
32
|
+
layouts.set(kind, render);
|
|
33
|
+
},
|
|
34
|
+
getScrollTop: () => scroll.get(kind) ?? 0,
|
|
35
|
+
rememberScrollTop(position) {
|
|
36
|
+
scroll.set(kind, position);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const ports = { costumes: forKind('costumes'), sounds: forKind('sounds') };
|
|
40
|
+
return { selected, ports, layouts, selectors, scroll };
|
|
41
|
+
}
|
|
4
42
|
const renameLabels = {
|
|
5
43
|
en: { title: 'Rename sprite', name: 'Sprite name', cancel: 'Cancel', save: 'Rename' },
|
|
6
44
|
ja: { title: 'スプライト名の変更', name: 'スプライト名', cancel: 'キャンセル', save: '変更' }
|
|
@@ -28,33 +66,96 @@ class SpriteTab extends DefaultTab {
|
|
|
28
66
|
}
|
|
29
67
|
}
|
|
30
68
|
export function mountSpriteTabs(root, content, options) {
|
|
31
|
-
const { vm } = options;
|
|
69
|
+
const { vm, multiView = false, locale = 'en' } = options;
|
|
70
|
+
const viewAdapter = multiView ? options.viewAdapter : undefined;
|
|
71
|
+
const usesNativeSection = (section) => section !== variableSection || !options.variables;
|
|
32
72
|
const document = root.ownerDocument;
|
|
73
|
+
const editLabel = /^ja(?:-|$)/i.test(locale) ? 'クリックしてこのビューで編集' : 'Activate this view to edit';
|
|
33
74
|
const slots = new Map();
|
|
34
|
-
const
|
|
75
|
+
const views = new Map();
|
|
35
76
|
const costumeIcons = new WeakMap();
|
|
36
|
-
const
|
|
77
|
+
const remembered = new WeakMap();
|
|
78
|
+
const lastUsed = new WeakMap();
|
|
37
79
|
const closed = new WeakSet();
|
|
38
80
|
let active;
|
|
39
81
|
let observed = vm.editingTarget;
|
|
40
82
|
let syncing = false;
|
|
83
|
+
let nextView = 0;
|
|
41
84
|
let renameDialog;
|
|
42
85
|
const text = options.renameLabels ?? renameLabels.en;
|
|
43
86
|
const dockview = createDockview(root, {
|
|
44
87
|
theme: themeLight,
|
|
45
88
|
disableFloatingGroups: true,
|
|
89
|
+
...(multiView && { createRightHeaderActionComponent: createLayoutMenu }),
|
|
46
90
|
defaultTabComponent: 'sprite',
|
|
47
91
|
createTabComponent: () => new SpriteTab(document),
|
|
48
92
|
getTabContextMenuItems: ({ panel }) => {
|
|
49
|
-
const target =
|
|
50
|
-
if (!target || target.isStage)
|
|
93
|
+
const target = views.get(panel.id)?.target;
|
|
94
|
+
if (!target || target.isStage || !vm.renameSprite)
|
|
51
95
|
return [];
|
|
52
96
|
return [{ label: text.title, action: () => openRenameDialog(target) }];
|
|
53
97
|
},
|
|
54
98
|
createComponent: () => {
|
|
55
99
|
const element = document.createElement('div');
|
|
56
100
|
element.className = 'tw-plus-sprite-tab-slot';
|
|
57
|
-
|
|
101
|
+
const body = multiView ? document.createElement('div') : element;
|
|
102
|
+
const toolbar = multiView && options.onToolbarSlots ? document.createElement('div') : undefined;
|
|
103
|
+
const assetPreview = multiView && options.onAssetPreviewSlots ? document.createElement('div') : undefined;
|
|
104
|
+
let id;
|
|
105
|
+
let dimensions;
|
|
106
|
+
if (multiView) {
|
|
107
|
+
element.classList.add('tw-plus-sprite-tab-slot--split');
|
|
108
|
+
body.className = 'tw-plus-sprite-tab-body';
|
|
109
|
+
if (toolbar)
|
|
110
|
+
body.setAttribute('role', 'tabpanel');
|
|
111
|
+
if (toolbar)
|
|
112
|
+
element.append(toolbar);
|
|
113
|
+
if (assetPreview) {
|
|
114
|
+
assetPreview.className = 'tw-plus-sprite-tab-asset-preview';
|
|
115
|
+
assetPreview.hidden = true;
|
|
116
|
+
assetPreview.tabIndex = 0;
|
|
117
|
+
assetPreview.setAttribute('role', 'group');
|
|
118
|
+
assetPreview.setAttribute('aria-label', editLabel);
|
|
119
|
+
assetPreview.addEventListener('keydown', event => {
|
|
120
|
+
if (event.target !== assetPreview || (event.key !== 'Enter' && event.key !== ' '))
|
|
121
|
+
return;
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
activateView(id);
|
|
125
|
+
});
|
|
126
|
+
body.append(assetPreview);
|
|
127
|
+
}
|
|
128
|
+
element.append(body);
|
|
129
|
+
body.addEventListener('click', event => {
|
|
130
|
+
if (active?.id === id)
|
|
131
|
+
return;
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
event.stopPropagation();
|
|
134
|
+
activateView(id);
|
|
135
|
+
}, { capture: true });
|
|
136
|
+
}
|
|
137
|
+
const slot = { body, toolbar, assetPreview };
|
|
138
|
+
return {
|
|
139
|
+
element,
|
|
140
|
+
init(params) {
|
|
141
|
+
id = params.api.id;
|
|
142
|
+
slots.set(id, slot);
|
|
143
|
+
if (toolbar)
|
|
144
|
+
body.id = id + '-content';
|
|
145
|
+
if (multiView) {
|
|
146
|
+
dimensions = params.api.onDidDimensionsChange(() => {
|
|
147
|
+
if (active?.id === id && body.contains(content))
|
|
148
|
+
options.resize();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
dispose() {
|
|
153
|
+
dimensions?.dispose();
|
|
154
|
+
clearInactive(slot);
|
|
155
|
+
if (slots.get(id) === slot)
|
|
156
|
+
slots.delete(id);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
58
159
|
},
|
|
59
160
|
createWatermarkComponent: () => {
|
|
60
161
|
const button = document.createElement('button');
|
|
@@ -67,39 +168,26 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
67
168
|
return { element: button, init() { } };
|
|
68
169
|
}
|
|
69
170
|
});
|
|
70
|
-
// The host has one editable surface. Keep reordering inside its tab group.
|
|
71
171
|
const preventSplit = (event) => {
|
|
72
172
|
if (event.kind === 'edge' || event.kind === 'content' && event.position !== 'center')
|
|
73
173
|
event.preventDefault();
|
|
74
174
|
};
|
|
75
|
-
const overlay = dockview.onWillShowOverlay(preventSplit);
|
|
76
|
-
const drop = dockview.onWillDrop(preventSplit);
|
|
77
|
-
function
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
else
|
|
89
|
-
content.remove();
|
|
90
|
-
options.selectTarget('');
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
slots.get(next.id).appendChild(content);
|
|
94
|
-
if (vm.editingTarget !== next)
|
|
95
|
-
vm.setEditingTarget(next.id);
|
|
96
|
-
observed = vm.editingTarget;
|
|
97
|
-
options.activateSection(section);
|
|
98
|
-
options.selectTarget(next.id);
|
|
99
|
-
options.resize();
|
|
175
|
+
const overlay = !multiView && dockview.onWillShowOverlay(preventSplit);
|
|
176
|
+
const drop = !multiView && dockview.onWillDrop(preventSplit);
|
|
177
|
+
function createLayoutMenu(group) {
|
|
178
|
+
const menu = createSpriteLayoutMenu(document, locale, {
|
|
179
|
+
splitRight: () => { if (group.activePanel)
|
|
180
|
+
split('right', group.activePanel.id); },
|
|
181
|
+
splitBelow: () => { if (group.activePanel)
|
|
182
|
+
split('below', group.activePanel.id); },
|
|
183
|
+
close: () => { if (group.activePanel)
|
|
184
|
+
closeSplit(group.activePanel.id); },
|
|
185
|
+
canClose: () => dockview.groups.length > 1
|
|
186
|
+
});
|
|
187
|
+
return { ...menu, init() { } };
|
|
100
188
|
}
|
|
101
189
|
function isOpen(target) {
|
|
102
|
-
return
|
|
190
|
+
return [...views.values()].some(view => view.target === target) && vm.runtime.targets.includes(target);
|
|
103
191
|
}
|
|
104
192
|
function openRenameDialog(target) {
|
|
105
193
|
if (renameDialog || !isOpen(target))
|
|
@@ -132,7 +220,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
132
220
|
renameDialog = undefined;
|
|
133
221
|
if (dialog.returnValue === 'save' && input.value.trim() && isOpen(target) &&
|
|
134
222
|
input.value !== target.getName())
|
|
135
|
-
vm.renameSprite(target.id, input.value);
|
|
223
|
+
vm.renameSprite?.(target.id, input.value);
|
|
136
224
|
}, { once: true });
|
|
137
225
|
renameDialog = dialog;
|
|
138
226
|
root.append(dialog);
|
|
@@ -147,19 +235,211 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
147
235
|
costumeIcons.set(asset, asset.encodeDataURI());
|
|
148
236
|
return costumeIcons.get(asset);
|
|
149
237
|
}
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
238
|
+
function describe(view, panel = dockview.getPanel(view.id)) {
|
|
239
|
+
return {
|
|
240
|
+
id: view.id, targetId: view.target.id, isStage: view.target.isStage,
|
|
241
|
+
section: view.section, groupId: panel?.api.group.id ?? '',
|
|
242
|
+
active: active === view, groupActive: panel?.api.isGroupActive ?? false
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function clearInactive(slot) {
|
|
246
|
+
if (slot.assetPreview)
|
|
247
|
+
slot.assetPreview.hidden = true;
|
|
248
|
+
if (slot.inactiveSection === undefined)
|
|
249
|
+
return;
|
|
250
|
+
slot.clearInactive?.();
|
|
251
|
+
slot.clearInactive = undefined;
|
|
252
|
+
slot.inactiveSection = undefined;
|
|
253
|
+
// Keep the React portal host mounted while its children reconcile.
|
|
254
|
+
slot.body.replaceChildren(...(slot.assetPreview ? [slot.assetPreview] : []));
|
|
255
|
+
}
|
|
256
|
+
function refreshSurfaces() {
|
|
257
|
+
if (!multiView)
|
|
258
|
+
return;
|
|
259
|
+
const toolbars = [];
|
|
260
|
+
const assetPreviews = [];
|
|
261
|
+
for (const [id, slot] of slots) {
|
|
262
|
+
const view = views.get(id);
|
|
263
|
+
const panel = dockview.getPanel(id);
|
|
264
|
+
if (!view || !panel || panel.api.group.activePanel !== panel) {
|
|
265
|
+
clearInactive(slot);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const descriptor = describe(view, panel);
|
|
269
|
+
if (slot.toolbar) {
|
|
270
|
+
toolbars.push({ view: descriptor, element: slot.toolbar });
|
|
271
|
+
slot.body.setAttribute('aria-labelledby', id + '-section-' + (spriteEditorSections[view.section] ?? 'code'));
|
|
272
|
+
}
|
|
273
|
+
if (view.section === variableSection && options.variables && slot.assetPreview) {
|
|
274
|
+
slot.assetPreview.hidden = false;
|
|
275
|
+
assetPreviews.push({ id, targetId: descriptor.targetId, kind: 'variables', editable: active === view,
|
|
276
|
+
element: slot.assetPreview });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (active === view)
|
|
280
|
+
continue;
|
|
281
|
+
const kind = view.section === 1 ? 'costumes' : view.section === 2 ? 'sounds' : undefined;
|
|
282
|
+
if (kind && slot.assetPreview) {
|
|
283
|
+
clearInactive(slot);
|
|
284
|
+
slot.assetPreview.hidden = false;
|
|
285
|
+
assetPreviews.push({ id, targetId: descriptor.targetId, kind, element: slot.assetPreview,
|
|
286
|
+
selected: view.assets.selected.get(kind), editable: false, renderLayout: view.assets.layouts.get(kind), scrollTop: view.assets.scroll.get(kind) });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (slot.inactiveSection === view.section)
|
|
290
|
+
continue;
|
|
291
|
+
clearInactive(slot);
|
|
292
|
+
slot.inactiveSection = view.section;
|
|
293
|
+
if (viewAdapter) {
|
|
294
|
+
const cleanup = viewAdapter.renderInactive(slot.body, descriptor, view.snapshots?.get(view.section)?.snapshot);
|
|
295
|
+
if (cleanup)
|
|
296
|
+
slot.clearInactive = cleanup;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const button = slot.body.ownerDocument.createElement('button');
|
|
300
|
+
button.type = 'button';
|
|
301
|
+
button.textContent = editLabel;
|
|
302
|
+
slot.body.append(button);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
options.onToolbarSlots?.(toolbars);
|
|
306
|
+
options.onAssetPreviewSlots?.(assetPreviews);
|
|
307
|
+
}
|
|
308
|
+
function remember(view) {
|
|
309
|
+
if (usesNativeSection(view.section))
|
|
310
|
+
view.section = options.getSection();
|
|
311
|
+
remembered.set(view.target, view.section);
|
|
312
|
+
if (!viewAdapter || !usesNativeSection(view.section) || !slots.get(view.id)?.body.contains(content))
|
|
313
|
+
return;
|
|
314
|
+
const descriptor = describe(view);
|
|
315
|
+
const snapshot = viewAdapter.capture(descriptor);
|
|
316
|
+
view.snapshots ??= new Map();
|
|
317
|
+
const previous = view.snapshots.get(view.section);
|
|
318
|
+
if (previous && snapshot !== previous.snapshot)
|
|
319
|
+
viewAdapter.dispose?.(previous.view, previous.snapshot);
|
|
320
|
+
view.snapshots.set(view.section, { snapshot, view: descriptor });
|
|
321
|
+
}
|
|
322
|
+
function show(panel) {
|
|
323
|
+
const next = views.get(panel?.id);
|
|
324
|
+
if (active === next) {
|
|
325
|
+
refreshSurfaces();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (active && views.get(active.id) === active)
|
|
329
|
+
remember(active);
|
|
330
|
+
active = next;
|
|
331
|
+
if (!next) {
|
|
332
|
+
if (options.park)
|
|
333
|
+
options.park();
|
|
334
|
+
else
|
|
335
|
+
content.remove();
|
|
336
|
+
options.selectTarget('');
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
if (!multiView)
|
|
340
|
+
next.section = remembered.get(next.target) ?? options.getSection();
|
|
341
|
+
const slot = slots.get(next.id);
|
|
342
|
+
clearInactive(slot);
|
|
343
|
+
slot.body.appendChild(content);
|
|
344
|
+
if (vm.editingTarget !== next.target)
|
|
345
|
+
vm.setEditingTarget(next.target.id);
|
|
346
|
+
observed = vm.editingTarget;
|
|
347
|
+
options.selectTarget(next.target.id, multiView ? next.assets.ports : undefined);
|
|
348
|
+
content.hidden = !usesNativeSection(next.section);
|
|
349
|
+
if (usesNativeSection(next.section))
|
|
350
|
+
options.activateSection(next.section);
|
|
351
|
+
viewAdapter?.restore(describe(next, panel), next.snapshots?.get(next.section)?.snapshot);
|
|
352
|
+
options.resize();
|
|
353
|
+
}
|
|
354
|
+
refreshSurfaces();
|
|
355
|
+
}
|
|
356
|
+
function add(target, splitPosition, section) {
|
|
357
|
+
let id = target.id;
|
|
358
|
+
if (splitPosition || views.has(id)) {
|
|
359
|
+
do {
|
|
360
|
+
id = 'sprite-view-' + ++nextView;
|
|
361
|
+
} while (views.has(id) || vm.runtime.targets.some(item => item.id === id));
|
|
362
|
+
}
|
|
363
|
+
const source = splitPosition && views.get(splitPosition.referencePanel);
|
|
364
|
+
views.set(id, { id, target, section: section ?? remembered.get(target) ?? options.getSection(),
|
|
365
|
+
assets: createAssetSelections(target, source?.assets) });
|
|
366
|
+
try {
|
|
367
|
+
return dockview.addPanel({ id, title: target.getName(), component: 'sprite', inactive: !splitPosition,
|
|
368
|
+
position: splitPosition, params: { icon: getIcon(target) } });
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
views.delete(id);
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function activateView(id) {
|
|
376
|
+
const view = views.get(id);
|
|
377
|
+
if (!view)
|
|
378
|
+
return false;
|
|
379
|
+
lastUsed.set(view.target, view);
|
|
380
|
+
dockview.getPanel(id)?.api.setActive();
|
|
381
|
+
return true;
|
|
156
382
|
}
|
|
157
383
|
function select(id) {
|
|
158
384
|
const target = vm.runtime.targets.find(item => item.id === id && item.isOriginal);
|
|
159
385
|
if (!target)
|
|
160
386
|
return;
|
|
161
387
|
closed.delete(target);
|
|
162
|
-
(
|
|
388
|
+
if (active?.target === target && views.get(active.id) === active) {
|
|
389
|
+
activateView(active.id);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const recent = lastUsed.get(target);
|
|
393
|
+
const view = recent && views.get(recent.id) === recent ? recent :
|
|
394
|
+
[...views.values()].find(item => item.target === target);
|
|
395
|
+
activateView(view?.id ?? add(target).id);
|
|
396
|
+
}
|
|
397
|
+
function split(direction, id = active?.id) {
|
|
398
|
+
const reference = views.get(id);
|
|
399
|
+
if (!multiView || !reference)
|
|
400
|
+
return;
|
|
401
|
+
const section = reference === active && usesNativeSection(reference.section) ? options.getSection() : reference.section;
|
|
402
|
+
const panel = add(reference.target, { referencePanel: id, direction }, section);
|
|
403
|
+
activateView(panel.id);
|
|
404
|
+
return panel.id;
|
|
405
|
+
}
|
|
406
|
+
function closeSplit(id = active?.id) {
|
|
407
|
+
const panel = dockview.getPanel(id);
|
|
408
|
+
if (!multiView || !panel || dockview.groups.length <= 1)
|
|
409
|
+
return false;
|
|
410
|
+
syncing = true;
|
|
411
|
+
try {
|
|
412
|
+
dockview.removeGroup(panel.api.group);
|
|
413
|
+
show(dockview.activePanel);
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
syncing = false;
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
function setViewSection(id, section) {
|
|
421
|
+
const view = views.get(id);
|
|
422
|
+
if (!view || !Number.isInteger(section) || section < 0 || section > (options.variables ? variableSection : 2) || view.section === section)
|
|
423
|
+
return false;
|
|
424
|
+
if (view === active && viewAdapter)
|
|
425
|
+
remember(view);
|
|
426
|
+
view.section = section;
|
|
427
|
+
if (view === active) {
|
|
428
|
+
content.hidden = !usesNativeSection(section);
|
|
429
|
+
if (usesNativeSection(section)) {
|
|
430
|
+
options.activateSection(section);
|
|
431
|
+
options.resize();
|
|
432
|
+
}
|
|
433
|
+
viewAdapter?.restore(describe(view), view.snapshots?.get(section)?.snapshot);
|
|
434
|
+
}
|
|
435
|
+
refreshSurfaces();
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
function syncActiveSection(section) {
|
|
439
|
+
if (!active || !usesNativeSection(active.section) || active.section === section)
|
|
440
|
+
return;
|
|
441
|
+
active.section = section;
|
|
442
|
+
refreshSurfaces();
|
|
163
443
|
}
|
|
164
444
|
function refresh() {
|
|
165
445
|
if (syncing)
|
|
@@ -168,23 +448,24 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
168
448
|
try {
|
|
169
449
|
const available = new Set(vm.runtime.targets.filter(target => target.isOriginal));
|
|
170
450
|
const editing = vm.editingTarget;
|
|
171
|
-
for (const [id,
|
|
172
|
-
if (!available.has(target))
|
|
451
|
+
for (const [id, view] of views) {
|
|
452
|
+
if (!available.has(view.target))
|
|
173
453
|
dockview.getPanel(id)?.api.close();
|
|
174
454
|
}
|
|
455
|
+
const represented = new Set();
|
|
456
|
+
for (const [id, view] of views) {
|
|
457
|
+
represented.add(view.target);
|
|
458
|
+
const panel = dockview.getPanel(id);
|
|
459
|
+
if (panel && panel.title !== view.target.getName())
|
|
460
|
+
panel.api.setTitle(view.target.getName());
|
|
461
|
+
const icon = getIcon(view.target);
|
|
462
|
+
if (panel && panel.api.getParameters().icon !== icon)
|
|
463
|
+
panel.api.updateParameters({ icon });
|
|
464
|
+
}
|
|
175
465
|
for (const target of available) {
|
|
176
|
-
if (target.isStage
|
|
177
|
-
continue;
|
|
178
|
-
const panel = dockview.getPanel(target.id);
|
|
179
|
-
if (panel) {
|
|
180
|
-
if (panel.title !== target.getName())
|
|
181
|
-
panel.api.setTitle(target.getName());
|
|
182
|
-
const icon = getIcon(target);
|
|
183
|
-
if (panel.api.getParameters().icon !== icon)
|
|
184
|
-
panel.api.updateParameters({ icon });
|
|
185
|
-
}
|
|
186
|
-
else if (!closed.has(target))
|
|
466
|
+
if (!represented.has(target) && (!target.isStage || target === editing) && !closed.has(target)) {
|
|
187
467
|
add(target);
|
|
468
|
+
}
|
|
188
469
|
}
|
|
189
470
|
if (editing && (observed !== editing || !active && !closed.has(editing))) {
|
|
190
471
|
observed = editing;
|
|
@@ -196,6 +477,15 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
196
477
|
syncing = false;
|
|
197
478
|
}
|
|
198
479
|
}
|
|
480
|
+
function disposeView(view) {
|
|
481
|
+
for (const stored of view.snapshots?.values() ?? [])
|
|
482
|
+
viewAdapter?.dispose?.(stored.view, stored.snapshot);
|
|
483
|
+
const slot = slots.get(view.id);
|
|
484
|
+
if (slot)
|
|
485
|
+
clearInactive(slot);
|
|
486
|
+
slots.delete(view.id);
|
|
487
|
+
views.delete(view.id);
|
|
488
|
+
}
|
|
199
489
|
const selection = dockview.onDidActivePanelChange(({ panel }) => {
|
|
200
490
|
if (syncing)
|
|
201
491
|
return;
|
|
@@ -208,29 +498,76 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
208
498
|
}
|
|
209
499
|
});
|
|
210
500
|
const removal = dockview.onDidRemovePanel(panel => {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
501
|
+
const view = views.get(panel.id);
|
|
502
|
+
if (!view)
|
|
503
|
+
return;
|
|
504
|
+
if (view === active)
|
|
505
|
+
remembered.set(view.target, usesNativeSection(view.section) ? options.getSection() : view.section);
|
|
506
|
+
disposeView(view);
|
|
507
|
+
if (![...views.values()].some(item => item.target === view.target))
|
|
508
|
+
closed.add(view.target);
|
|
216
509
|
});
|
|
510
|
+
const layout = multiView && dockview.onDidLayoutChange(refreshSurfaces);
|
|
217
511
|
vm.on('targetsUpdate', refresh);
|
|
218
512
|
refresh();
|
|
219
513
|
return {
|
|
220
|
-
select,
|
|
514
|
+
select, split, closeSplit, activateView, setViewSection, syncActiveSection, refreshSurfaces,
|
|
515
|
+
selectAsset(id, kind, index) {
|
|
516
|
+
if (active?.id === id && active.section === (kind === 'costumes' ? 1 : 2)) {
|
|
517
|
+
active.assets.selectors.get(kind)?.(index);
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
currentView: () => active && describe(active),
|
|
521
|
+
views: () => [...views.values()].map(view => describe(view)),
|
|
221
522
|
dispose() {
|
|
222
523
|
renameDialog?.remove();
|
|
223
524
|
renameDialog = undefined;
|
|
224
525
|
vm.removeListener('targetsUpdate', refresh);
|
|
225
526
|
selection.dispose();
|
|
226
527
|
removal.dispose();
|
|
227
|
-
overlay
|
|
228
|
-
|
|
528
|
+
if (overlay)
|
|
529
|
+
overlay.dispose();
|
|
530
|
+
if (drop)
|
|
531
|
+
drop.dispose();
|
|
532
|
+
if (layout)
|
|
533
|
+
layout.dispose();
|
|
534
|
+
for (const view of views.values())
|
|
535
|
+
disposeView(view);
|
|
229
536
|
content.remove();
|
|
230
537
|
dockview.dispose();
|
|
231
538
|
}
|
|
232
539
|
};
|
|
233
540
|
}
|
|
541
|
+
function AssetPreview({ locale, data, kind, selected, renderLayout, scrollTop }) {
|
|
542
|
+
const chosen = data.items.find(item => item.asset === selected) ??
|
|
543
|
+
data.items.find(item => item.asset === data.defaultAsset) ?? data.items[0];
|
|
544
|
+
const peaks = React.useMemo(() => {
|
|
545
|
+
if (!chosen?.buffer?.length)
|
|
546
|
+
return undefined;
|
|
547
|
+
const samples = chosen.buffer.getChannelData(0);
|
|
548
|
+
const chunk = Math.ceil(samples.length / 192);
|
|
549
|
+
const levels = [];
|
|
550
|
+
for (let offset = 0; offset < samples.length; offset += chunk) {
|
|
551
|
+
let peak = 0;
|
|
552
|
+
for (let i = offset; i < Math.min(offset + chunk, samples.length); i++) {
|
|
553
|
+
peak = Math.max(peak, Math.abs(samples[i]));
|
|
554
|
+
}
|
|
555
|
+
levels.push(peak);
|
|
556
|
+
}
|
|
557
|
+
return levels;
|
|
558
|
+
}, [chosen?.buffer]);
|
|
559
|
+
const index = chosen ? data.items.indexOf(chosen) : -1;
|
|
560
|
+
const pane = React.createElement(SpriteAssetPane, {
|
|
561
|
+
locale, kind, editable: false, onSelect() { },
|
|
562
|
+
detailOnly: Boolean(renderLayout),
|
|
563
|
+
selectedId: chosen ? String(index) : null,
|
|
564
|
+
items: data.items.map((item, position) => ({
|
|
565
|
+
id: String(position), name: item.name, imageUrl: item.imageUrl,
|
|
566
|
+
duration: item.duration ?? item.buffer?.duration, peaks: item === chosen ? peaks : undefined
|
|
567
|
+
}))
|
|
568
|
+
});
|
|
569
|
+
return renderLayout ? renderLayout(data.items, index, pane, scrollTop ?? 0) : pane;
|
|
570
|
+
}
|
|
234
571
|
function SpriteTabs(props) {
|
|
235
572
|
const root = React.useRef(null);
|
|
236
573
|
const sections = React.useRef(null);
|
|
@@ -242,6 +579,12 @@ function SpriteTabs(props) {
|
|
|
242
579
|
node.className = 'tw-plus-sprite-tab-editor';
|
|
243
580
|
return node;
|
|
244
581
|
});
|
|
582
|
+
const [toolbarSlots, setToolbarSlots] = React.useState([]);
|
|
583
|
+
const [assetPreviewSlots, setAssetPreviewSlots] = React.useState([]);
|
|
584
|
+
const controller = React.useRef(null);
|
|
585
|
+
const hasVariables = Boolean(props.getVariables);
|
|
586
|
+
const hasToolbar = Boolean(props.renderToolbar) || hasVariables;
|
|
587
|
+
const hasAssetPreview = Boolean(props.getAssetPreview) || hasVariables;
|
|
245
588
|
React.useLayoutEffect(() => {
|
|
246
589
|
sections.current.append(...props.slots);
|
|
247
590
|
parking.current.appendChild(content);
|
|
@@ -252,19 +595,26 @@ function SpriteTabs(props) {
|
|
|
252
595
|
}, [content, props.slots]);
|
|
253
596
|
React.useLayoutEffect(() => {
|
|
254
597
|
if (!props.enabled) {
|
|
598
|
+
setToolbarSlots([]);
|
|
599
|
+
setAssetPreviewSlots([]);
|
|
600
|
+
content.hidden = false;
|
|
255
601
|
root.current.appendChild(content);
|
|
256
602
|
latest.current.resize();
|
|
257
603
|
return () => content.remove();
|
|
258
604
|
}
|
|
259
|
-
const park = () => parking.current?.appendChild(content);
|
|
260
|
-
park();
|
|
261
605
|
const frame = content.ownerDocument.defaultView;
|
|
262
606
|
let restoreFrame;
|
|
263
|
-
const
|
|
607
|
+
const park = () => parking.current?.appendChild(content);
|
|
608
|
+
park();
|
|
609
|
+
const instance = mountSpriteTabs(root.current, content, {
|
|
264
610
|
vm: props.vm,
|
|
265
611
|
getSection: () => latest.current.activeSection,
|
|
266
612
|
activateSection(section) {
|
|
267
613
|
frame.cancelAnimationFrame(restoreFrame);
|
|
614
|
+
if (latest.current.activateSection) {
|
|
615
|
+
latest.current.activateSection(section);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
268
618
|
// Dockview attaches the active panel after dispatching the selection event.
|
|
269
619
|
restoreFrame = frame.requestAnimationFrame(() => {
|
|
270
620
|
if (content.isConnected && section !== latest.current.activeSection) {
|
|
@@ -272,29 +622,96 @@ function SpriteTabs(props) {
|
|
|
272
622
|
}
|
|
273
623
|
});
|
|
274
624
|
},
|
|
275
|
-
selectTarget: id => latest.current.selectTarget(id),
|
|
276
|
-
resize: () => latest.current.resize(),
|
|
277
625
|
park,
|
|
626
|
+
renameLabels: props.renameLabels,
|
|
627
|
+
selectTarget: (id, assets) => latest.current.selectTarget(id, assets),
|
|
628
|
+
resize: () => latest.current.resize(),
|
|
278
629
|
reopenLabel: props.reopenLabel,
|
|
279
|
-
|
|
630
|
+
multiView: props.multiView,
|
|
631
|
+
variables: hasVariables,
|
|
632
|
+
locale: props.locale,
|
|
633
|
+
viewAdapter: props.viewAdapter,
|
|
634
|
+
onToolbarSlots: hasToolbar ? setToolbarSlots : undefined,
|
|
635
|
+
onAssetPreviewSlots: hasAssetPreview ? setAssetPreviewSlots : undefined
|
|
280
636
|
});
|
|
281
|
-
|
|
637
|
+
controller.current = instance;
|
|
638
|
+
latest.current.connect(instance);
|
|
282
639
|
return () => {
|
|
283
640
|
frame.cancelAnimationFrame(restoreFrame);
|
|
641
|
+
controller.current = null;
|
|
284
642
|
latest.current.connect(null);
|
|
285
|
-
|
|
643
|
+
instance.dispose();
|
|
286
644
|
};
|
|
287
|
-
}, [content, props.enabled, props.vm, props.reopenLabel, props.
|
|
645
|
+
}, [content, props.enabled, props.vm, props.reopenLabel, props.multiView, props.locale,
|
|
646
|
+
props.viewAdapter, props.renameLabels, hasVariables, hasToolbar, hasAssetPreview]);
|
|
647
|
+
React.useLayoutEffect(() => {
|
|
648
|
+
controller.current?.syncActiveSection(props.activeSection);
|
|
649
|
+
}, [props.activeSection]);
|
|
650
|
+
const workspace = controller.current;
|
|
288
651
|
// The native tab controller also reacts to bubbled events from elements with role="tab".
|
|
289
652
|
const isolateDockviewTabs = (event) => {
|
|
290
653
|
if (!content.contains(event.target))
|
|
291
654
|
event.stopPropagation();
|
|
292
655
|
};
|
|
293
|
-
return React.createElement('div', { className: props.className
|
|
656
|
+
return React.createElement('div', { className: props.className,
|
|
657
|
+
'data-multi-view': props.enabled && props.multiView || undefined }, React.createElement('div', { ref: root, onClick: isolateDockviewTabs, onKeyDown: isolateDockviewTabs }), React.createElement('div', { ref: parking, hidden: true }), createPortal(React.createElement(React.Fragment, null, props.multiView && props.getVariables ? React.createElement('div', { hidden: true }, props.children) : props.children, React.createElement('div', { ref: sections, className: 'tw-plus-sprite-tab-sections' })), content), toolbarSlots.map(({ view, element }) => {
|
|
658
|
+
const actions = {
|
|
659
|
+
activate: () => { workspace?.activateView(view.id); },
|
|
660
|
+
setSection: (section) => {
|
|
661
|
+
workspace?.setViewSection(view.id, section);
|
|
662
|
+
workspace?.activateView(view.id);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
const currentSection = spriteEditorSections[view.section] ?? 'code';
|
|
666
|
+
const toolbar = props.renderToolbar?.(view, actions) ?? (props.getVariables &&
|
|
667
|
+
React.createElement(SpriteEditorToolbar, {
|
|
668
|
+
viewId: view.id, locale: props.locale, section: currentSection, isStage: view.isStage,
|
|
669
|
+
activeGroup: view.groupActive,
|
|
670
|
+
actions: view.active && currentSection !== 'variables' ? props.activeTools : undefined,
|
|
671
|
+
onSectionChange: section => actions.setSection(spriteEditorSections.indexOf(section)),
|
|
672
|
+
search: {
|
|
673
|
+
onFocus: actions.activate,
|
|
674
|
+
find(query, section) {
|
|
675
|
+
let items = [];
|
|
676
|
+
if (section === 'code')
|
|
677
|
+
items = props.getCodeSearchItems?.(view.targetId) ?? [];
|
|
678
|
+
else if (section === 'variables') {
|
|
679
|
+
items = props.getVariables(view.targetId).map(variable => ({ id: variable.id, label: variable.name }));
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
items = props.getAssetPreview?.(view.targetId, section).items.map((item, index) => ({ id: String(index), label: item.name })) ?? [];
|
|
683
|
+
}
|
|
684
|
+
const text = query.toLocaleLowerCase();
|
|
685
|
+
return items.filter(item => item.label.toLocaleLowerCase().includes(text));
|
|
686
|
+
},
|
|
687
|
+
select(result, section) {
|
|
688
|
+
actions.activate();
|
|
689
|
+
if (section === 'code')
|
|
690
|
+
props.openCodeSearchResult?.(view.targetId, result.id);
|
|
691
|
+
else if (section === 'variables') {
|
|
692
|
+
const field = assetPreviewSlots.find(slot => slot.id === view.id)?.element;
|
|
693
|
+
[...(field?.querySelectorAll('input[data-variable-id]') ?? [])]
|
|
694
|
+
.find(input => input.dataset.variableId === result.id)?.focus();
|
|
695
|
+
}
|
|
696
|
+
else
|
|
697
|
+
workspace?.selectAsset(view.id, section, Number(result.id));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}));
|
|
701
|
+
return createPortal(toolbar, element, view.id);
|
|
702
|
+
}), assetPreviewSlots.map(({ id, targetId, kind, element, editable, selected, renderLayout, scrollTop }) => createPortal(kind === 'variables' ? React.createElement(SpriteAssetPane, {
|
|
703
|
+
kind, locale: props.locale, editable, variables: props.getVariables?.(targetId) ?? [],
|
|
704
|
+
onWrite(variableId, value) {
|
|
705
|
+
props.writeVariable?.(targetId, variableId, value);
|
|
706
|
+
workspace?.refreshSurfaces();
|
|
707
|
+
}
|
|
708
|
+
}) : props.getAssetPreview && React.createElement(AssetPreview, {
|
|
709
|
+
locale: props.locale, kind, selected, renderLayout, scrollTop, data: props.getAssetPreview(targetId, kind)
|
|
710
|
+
}), element, id)));
|
|
294
711
|
}
|
|
295
712
|
export function useSpriteTabs({ available, enabled, locale, ...props }) {
|
|
296
713
|
const controller = React.useRef(null);
|
|
297
|
-
const [
|
|
714
|
+
const [selection, setSelection] = React.useState({ id: '' });
|
|
298
715
|
const [ready, setReady] = React.useState(false);
|
|
299
716
|
const [slots] = React.useState(() => available ? Array.from({ length: 3 }, () => {
|
|
300
717
|
const slot = document.createElement('div');
|
|
@@ -311,9 +728,12 @@ export function useSpriteTabs({ available, enabled, locale, ...props }) {
|
|
|
311
728
|
const japanese = /^ja(?:-|$)/i.test(locale);
|
|
312
729
|
const select = (id) => controller.current?.select(id);
|
|
313
730
|
return {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
731
|
+
controller,
|
|
732
|
+
assetSelections: enabled ? selection.assets : undefined,
|
|
733
|
+
context: enabled ? { selectedId: selection.id, openLabel: japanese ? '開く' : 'Open', select, open: select } : null,
|
|
734
|
+
renderEditorTabRow: available ? (children) => React.createElement((SpriteTabs), {
|
|
735
|
+
...props, enabled, locale, children, slots, onAttach: () => setReady(true),
|
|
736
|
+
selectTarget: (id, assets) => setSelection({ id, assets }),
|
|
317
737
|
connect: next => { controller.current = next; },
|
|
318
738
|
reopenLabel: japanese ? '編集中のスプライトを開く' : 'Open the editing sprite',
|
|
319
739
|
renameLabels: japanese ? renameLabels.ja : renameLabels.en
|