Edit a Parametric EQ in a tab of its own (#370)
The EQ's editor floated over the editor as a window since #367. The user wants it as a tab for now. Whether any tab can float and dock again is weighed apart from this. - A double-click on the node asks for the tab through `EditorState.openEq`, as `openValues` does for a Variables tab. - The tab is a document `eq:<asset>:<node>`, one per EQ node. A second double-click brings the open one up. - Its name is the asset's, with `EQ 2`, `EQ 3` when the asset has more than one. The icon is the sound FX's. - The tab closes with its node, its asset, or a new project, through the same `syncDocuments` as every document. - `EqWindow` becomes `EqEditor`, a `VStack` with a `TabHeader`. The curve takes the room of the tab, 320 x 160 at least. - The window's title bar, dragging and Close button are gone. - Delete in the EQ's tab removes the picked band. It never falls through to the selection, which is still the EQ's node. - The hint below the curve is shorter, it was cut off at 1280 px. Tests: - e2e 34 (melektaus-atx): ALL PASSED, 96 checks, 2 rewritten. The tab opens on a double-click and goes with its node. - Gate (melektaus-atx): CHECK EXIT 0, 274 files, 4231 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db15422372
commit
9ab1bbc507
6 files changed
+150
-129
No files matched your search
+43
-4
@@ -95,6 +95,7 @@ import { PluginsPane } from './plugins/PluginsPane';
|
||||
import { ManagementPane } from './users/ManagementPane';
|
||||
import { UsersPane } from './users/UsersPane';
|
||||
import { RoutingEditor } from './routing/RoutingEditor';
|
||||
import { EqEditor, eqNodeOf, eqTabLabel } from './soundfx/EqEditor';
|
||||
|
||||
export type CentreTab =
|
||||
| 'scene'
|
||||
@@ -117,6 +118,7 @@ export type CentreTab =
|
||||
| 'container'
|
||||
| 'variables'
|
||||
| 'routing'
|
||||
| 'eq'
|
||||
| 'management';
|
||||
|
||||
/** A tab a plugin added; the prefix keeps it apart from every built-in one. */
|
||||
@@ -126,9 +128,9 @@ export type TabId = CentreTab | PluginTabId;
|
||||
/**
|
||||
* An open document (#171): an asset by its id, or what a Variables tab shows (#252) by its
|
||||
* `valuesKey` – as many of those as were opened. The user management (#179) and the master routing
|
||||
* (#351) are no documents, but they open and close like one.
|
||||
* (#351) are no documents, but they open and close like one. A Parametric EQ has a tab per node (#370).
|
||||
*/
|
||||
export type DocumentId = `asset:${string}` | `values:${string}` | 'routing' | 'management';
|
||||
export type DocumentId = `asset:${string}` | `values:${string}` | `eq:${string}` | 'routing' | 'management';
|
||||
/** The account behind this session; the menu for it sits at the far right of the toolbar. */
|
||||
export interface EditorAccount {
|
||||
/** Who is signed in – the menu says so, since the toolbar has no room for a second label. */
|
||||
@@ -146,7 +148,7 @@ export type StripTab = FixedTab | PluginTabId | DocumentId;
|
||||
|
||||
type FixedTab = (typeof FIXED_TABS)[number];
|
||||
/** The kinds whose documents are assets, each shown by one editor that is pointed at them in turn. */
|
||||
type AssetKind = Exclude<CentreTab, FixedTab | 'variables' | 'routing' | 'management'>;
|
||||
type AssetKind = Exclude<CentreTab, FixedTab | 'variables' | 'routing' | 'eq' | 'management'>;
|
||||
|
||||
const FIXED_TABS = ['scene', 'game'] as const;
|
||||
|
||||
@@ -171,6 +173,7 @@ const PANE_ORDER = [
|
||||
'container',
|
||||
'variables',
|
||||
'routing',
|
||||
'eq',
|
||||
'management',
|
||||
] as const;
|
||||
|
||||
@@ -196,6 +199,7 @@ const TAB_LABELS: Record<CentreTab, string> = {
|
||||
container: 'Container',
|
||||
variables: 'Variables',
|
||||
routing: 'Master routing',
|
||||
eq: 'Parametric EQ',
|
||||
management: 'User management',
|
||||
};
|
||||
|
||||
@@ -206,7 +210,13 @@ const FIXED_TAB_ICONS: Partial<Record<FixedTab, string>> = {
|
||||
};
|
||||
|
||||
function isDocument(tab: StripTab): tab is DocumentId {
|
||||
return tab === 'management' || tab === 'routing' || tab.startsWith('asset:') || tab.startsWith('values:');
|
||||
return (
|
||||
tab === 'management' ||
|
||||
tab === 'routing' ||
|
||||
tab.startsWith('asset:') ||
|
||||
tab.startsWith('values:') ||
|
||||
tab.startsWith('eq:')
|
||||
);
|
||||
}
|
||||
|
||||
const MIN_LEFT_WIDTH = 160;
|
||||
@@ -250,8 +260,11 @@ export class Editor implements PlayHost {
|
||||
readonly containerEditor: ContainerEditor;
|
||||
readonly variablesEditor: VariablesEditor;
|
||||
readonly routingEditor: RoutingEditor;
|
||||
readonly eqEditor: EqEditor;
|
||||
/** What each open Variables tab shows, by its document. */
|
||||
private readonly valueTargets = new Map<DocumentId, ValuesTarget>();
|
||||
/** The node behind each EQ tab (#370). */
|
||||
private readonly eqTargets = new Map<DocumentId, { assetId: string; nodeId: string }>();
|
||||
/** The switch per file in `data/plugins/`, in the user management tab (#179). */
|
||||
readonly pluginsPane = new PluginsPane();
|
||||
/** The accounts, above the plugins; null unless an administrator is signed in. */
|
||||
@@ -341,6 +354,7 @@ export class Editor implements PlayHost {
|
||||
this.containerEditor = new ContainerEditor(state);
|
||||
this.variablesEditor = new VariablesEditor(state);
|
||||
this.routingEditor = new RoutingEditor(state);
|
||||
this.eqEditor = new EqEditor(state);
|
||||
this.hierarchy = new HierarchyPanel(state);
|
||||
this.inspector = new Inspector(state);
|
||||
this.tilemapPanel = new TilemapPanel(state);
|
||||
@@ -463,6 +477,7 @@ export class Editor implements PlayHost {
|
||||
...this.assetEditors,
|
||||
variables: this.variablesEditor,
|
||||
routing: this.routingEditor,
|
||||
eq: this.eqEditor,
|
||||
management: this.managementPane,
|
||||
};
|
||||
// Following a call goes on in the same tab: the tab changes its document, not its place.
|
||||
@@ -562,6 +577,14 @@ export class Editor implements PlayHost {
|
||||
if (!this.valueTargets.has(doc)) this.valueTargets.set(doc, target);
|
||||
this.openDocument(doc);
|
||||
});
|
||||
// A tab per Parametric EQ node (#370), opened by a double-click on it.
|
||||
state.on('eq', () => {
|
||||
const target = state.openedEq;
|
||||
if (!target) return;
|
||||
const doc: DocumentId = `eq:${target.assetId}:${target.nodeId}`;
|
||||
if (!this.eqTargets.has(doc)) this.eqTargets.set(doc, target);
|
||||
this.openDocument(doc);
|
||||
});
|
||||
// The tabs play through the buses and the master as the game would (#325).
|
||||
const syncMix = () => void soundLibrary.setMix(mixOf(state.project));
|
||||
syncMix();
|
||||
@@ -577,6 +600,8 @@ export class Editor implements PlayHost {
|
||||
for (const doc of this.documents.docs) this.tabs.removeTab(doc);
|
||||
this.documents = { docs: [], active: null };
|
||||
this.valueTargets.clear();
|
||||
this.eqTargets.clear();
|
||||
this.eqEditor.close();
|
||||
this.showTab('scene');
|
||||
});
|
||||
state.on('mode', () => this.editorRun.modeChanged());
|
||||
@@ -631,6 +656,10 @@ export class Editor implements PlayHost {
|
||||
return this.patternEditor.canvas.deleteSelection();
|
||||
case 'song':
|
||||
return this.songEditor.canvas.deleteSelection();
|
||||
case 'eq':
|
||||
// The selection is the EQ's node, still selected from the double-click: it is never what goes.
|
||||
this.eqEditor.removePicked();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -802,6 +831,7 @@ export class Editor implements PlayHost {
|
||||
if (kind && kind in this.assetEditors)
|
||||
this.assetEditors[kind as AssetKind].forgetView(doc.slice('asset:'.length));
|
||||
this.valueTargets.delete(doc);
|
||||
if (this.eqTargets.delete(doc) && kind === 'eq') this.eqEditor.close();
|
||||
if (doc === 'routing') this.routingEditor.close();
|
||||
if (!wasShowing) return;
|
||||
if (this.documents.active) this.showDocument(this.documents.active);
|
||||
@@ -869,6 +899,10 @@ export class Editor implements PlayHost {
|
||||
return target && valuesTitle(this.state, target) !== null ? 'variables' : null;
|
||||
}
|
||||
if (doc === 'management' || doc === 'routing') return doc;
|
||||
if (doc.startsWith('eq:')) {
|
||||
const target = this.eqTargets.get(doc);
|
||||
return target && eqNodeOf(this.state.project, target.assetId, target.nodeId) ? 'eq' : null;
|
||||
}
|
||||
const type = findAsset(this.state.project, doc.slice('asset:'.length))?.type;
|
||||
const kind = type ? TAB_OF_ASSET[type] : undefined;
|
||||
return kind && kind !== 'scene' ? kind : null;
|
||||
@@ -883,6 +917,8 @@ export class Editor implements PlayHost {
|
||||
// Read on every showing: an account may have been changed from another tab in the meantime.
|
||||
if (kind === 'management') void this.usersPane?.reload();
|
||||
if (kind === 'routing') this.routingEditor.open();
|
||||
const eq = this.eqTargets.get(doc);
|
||||
if (kind === 'eq' && eq) this.eqEditor.show(eq.assetId, eq.nodeId);
|
||||
this.display(kind, doc);
|
||||
}
|
||||
|
||||
@@ -890,6 +926,8 @@ export class Editor implements PlayHost {
|
||||
private labelOf(doc: DocumentId): string {
|
||||
if (doc === 'management') return this.usersPane ? TAB_LABELS.management : 'Plugins';
|
||||
if (doc === 'routing') return TAB_LABELS.routing;
|
||||
const eq = this.eqTargets.get(doc);
|
||||
if (eq) return eqTabLabel(this.state.project, eq.assetId, eq.nodeId);
|
||||
const target = this.valueTargets.get(doc);
|
||||
if (target) return valuesTitle(this.state, target, this.inspector.liveVariables) ?? '';
|
||||
const asset = findAsset(this.state.project, doc.slice('asset:'.length));
|
||||
@@ -900,6 +938,7 @@ export class Editor implements PlayHost {
|
||||
private iconOf(doc: DocumentId): string {
|
||||
if (doc === 'management') return this.usersPane ? USERS_ICON_SVG : PLUGINS_ICON_SVG;
|
||||
if (doc === 'routing') return ASSET_ICON_SVG.bus;
|
||||
if (doc.startsWith('eq:')) return ASSET_ICON_SVG.soundfx;
|
||||
const target = this.valueTargets.get(doc);
|
||||
if (target) return target.kind === 'list' ? LIST_ICON_SVG : ASSET_ICON_SVG.variables;
|
||||
const type = findAsset(this.state.project, doc.slice('asset:'.length))?.type;
|
||||
|
||||
Reference in new issue
Block a user