Skip to content

Commit b0f2d38

Browse files
committed
First step to a track sidebar
Signed-off-by: Mike Lischke <mike@lischke-online.de>
1 parent 73dc0ba commit b0f2d38

18 files changed

Lines changed: 367 additions & 12 deletions

src/App.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ export class App extends UIComponent<{}, IAppState> {
504504
dataModel={this.dataModel}
505505
services={this.services}
506506
undoManager={this.undoManager!}
507-
touchEditingEnabled={editMode}
507+
inEditMode={editMode}
508508
/>
509509
}
510510
forceExpanded={headerPinned || editMode}
@@ -1586,6 +1586,11 @@ export class App extends UIComponent<{}, IAppState> {
15861586
if (phase === AppPhase.Running) {
15871587
this.updateStatsItem();
15881588
}
1589+
1590+
const settings = AppStorage.loadUISettings();
1591+
if (settings?.editMode && !this.state.editMode) {
1592+
void requisitions.execute("editModeChanged", true);
1593+
}
15891594
}
15901595

15911596
private initEventHandlers(): void {
@@ -1661,6 +1666,8 @@ export class App extends UIComponent<{}, IAppState> {
16611666
};
16621667

16631668
private handleEditModeChanged = async (enabled: boolean): Promise<boolean> => {
1669+
AppStorage.saveSetting("editMode", enabled);
1670+
16641671
if (enabled) {
16651672
const arrangement = this.dataModel.arrangement;
16661673

src/components/styles.scss

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,138 @@
296296
}
297297
}
298298

299+
.trackEditSidebar {
300+
--edit-sidebar-width: clamp(160px, 20vw, 260px);
301+
302+
overflow: hidden;
303+
gap: 16px;
304+
305+
flex: 0 0 var(--edit-sidebar-width);
306+
width: var(--edit-sidebar-width);
307+
min-width: 0;
308+
min-height: 0;
309+
padding: 7px 10px;
310+
margin-bottom: 20px;
311+
z-index: 1;
312+
313+
background: linear-gradient(to left, var(--color-base-100), var(--color-base-100) 70%, transparent 100%);
314+
315+
transition: width 0.2s ease, flex-basis 0.2s ease;
316+
317+
.trackEditSidebarHeader {
318+
height: 30px;
319+
min-height: 30px;
320+
min-width: 0;
321+
display: flex;
322+
align-items: center;
323+
font-size: 0.75rem;
324+
font-weight: 600;
325+
text-transform: uppercase;
326+
letter-spacing: 0.05em;
327+
color: var(--color-base-content);
328+
opacity: 0.7;
329+
user-select: none;
330+
}
331+
332+
.trackEditSidebarPanel {
333+
min-width: 0;
334+
335+
.trackEditEmpty {
336+
padding: 24px 12px;
337+
font-size: 0.8rem;
338+
color: var(--color-base-content);
339+
opacity: 0.5;
340+
text-align: center;
341+
user-select: none;
342+
}
343+
}
344+
}
345+
346+
.trackEditRow {
347+
align-self: stretch;
348+
height: var(--note-height);
349+
margin-bottom: 20px;
350+
padding: 0 8px;
351+
cursor: default;
352+
flex-shrink: 0;
353+
354+
&:last-child {
355+
margin-bottom: 0;
356+
}
357+
}
358+
359+
.trackEditActions {
360+
flex-shrink: 0;
361+
gap: 2px;
362+
}
363+
364+
.trackEditAction {
365+
display: flex;
366+
align-items: center;
367+
justify-content: center;
368+
width: 24px;
369+
height: 24px;
370+
padding: 0;
371+
border: none;
372+
border-radius: 6px;
373+
background: transparent;
374+
cursor: pointer;
375+
color: var(--color-base-content);
376+
opacity: 0.5;
377+
transition: opacity 0.15s ease, background 0.15s ease, transform 0.15s ease;
378+
379+
&:hover {
380+
opacity: 1;
381+
transform: scale(1.15);
382+
}
383+
384+
.icon {
385+
width: 14px;
386+
height: 14px;
387+
background-color: var(--color-base-content);
388+
}
389+
}
390+
391+
.trackEditActionDelete {
392+
&:hover {
393+
background: color-mix(in srgb, var(--color-error) 20%, transparent);
394+
395+
.icon {
396+
background-color: var(--color-error);
397+
}
398+
}
399+
}
400+
401+
.trackEditActionInsert {
402+
&:hover {
403+
background: color-mix(in srgb, var(--color-success) 20%, transparent);
404+
405+
.icon {
406+
background-color: var(--color-success);
407+
}
408+
}
409+
}
410+
411+
.trackEditActionDuplicate {
412+
&:hover {
413+
background: color-mix(in srgb, var(--color-info) 20%, transparent);
414+
415+
.icon {
416+
background-color: var(--color-info);
417+
}
418+
}
419+
}
420+
421+
.trackEditActionClear {
422+
&:hover {
423+
background: color-mix(in srgb, var(--color-warning) 20%, transparent);
424+
425+
.icon {
426+
background-color: var(--color-warning);
427+
}
428+
}
429+
}
430+
299431
.guiderail {
300432
height: 20pt;
301433
margin-left: 2px;

src/components/ui/Arrangement/ArrangementViewer.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import { AppStorage, type IUISettings } from "../../../core/AppStorage.js";
99
import type { RealTime, ScoreBookDataModel } from "../../../core/ScoreBookDataModel.js";
1010
import type { UndoManager } from "../../../core/UndoManager.js";
1111
import { clampValue } from "../../../core/utils.js";
12-
import type { ArrangementPlayer } from "../../../player/ArrangementPlayer.js";
13-
import type { PlayerPlayState } from "../../../player/ArrangementPlayer.js";
12+
import type { ArrangementPlayer, PlayerPlayState } from "../../../player/ArrangementPlayer.js";
1413
import type { ScoreBookUiServices } from "../../../player/types.js";
1514
import { requisitions } from "../../../supplement/Requisitions.js";
1615
import { GridMeasureViewer } from "../Bar/Grid/GridMeasureViewer.js";
@@ -21,13 +20,14 @@ import { ChildAlignment, Orientation } from "../framework/ui-types.js";
2120
import { UIComponent, type ICommonUIProperties } from "../framework/UIComponent.js";
2221
import { Minimap, type IVisibleBarRange } from "../Minimap/Minimap.js";
2322
import { TrackControls } from "./TrackControls.js";
23+
import { TrackEditSidebar } from "./TrackEditSidebar.js";
2424

2525
export interface IArrangementViewerProps extends ICommonUIProperties {
2626
arrangementPlayer: ArrangementPlayer;
2727
dataModel: ScoreBookDataModel;
2828
services: ScoreBookUiServices;
2929
undoManager: UndoManager;
30-
touchEditingEnabled: boolean;
30+
inEditMode: boolean;
3131
}
3232

3333
interface IArrangementViewerState {
@@ -159,7 +159,7 @@ export class ArrangementViewer extends UIComponent<IArrangementViewerProps, IArr
159159
}
160160

161161
public override render(): JSX.Element {
162-
const { arrangementPlayer, dataModel, services, touchEditingEnabled, undoManager } = this.props;
162+
const { arrangementPlayer, dataModel, services, inEditMode, undoManager } = this.props;
163163
const { autoFollowIsOn, trackViewMode, viewerZoom } = this.state;
164164

165165
const arrangement = dataModel.arrangement!;
@@ -185,7 +185,7 @@ export class ArrangementViewer extends UIComponent<IArrangementViewerProps, IArr
185185
barNumber,
186186
arrangement,
187187
arrangementPlayer,
188-
touchEditingEnabled,
188+
inEditMode,
189189
services,
190190
undoManager,
191191
dataModel,
@@ -235,6 +235,10 @@ export class ArrangementViewer extends UIComponent<IArrangementViewerProps, IArr
235235
</>
236236
);
237237

238+
const editSidebar = inEditMode ? (
239+
<TrackEditSidebar tracks={arrangement.tracks} undoManager={undoManager} />
240+
) : undefined;
241+
238242
return (
239243
<Container
240244
className="arrangementViewer"
@@ -268,6 +272,7 @@ export class ArrangementViewer extends UIComponent<IArrangementViewerProps, IArr
268272
{contentHostContent}
269273
</Container>
270274
</Container>
275+
{editSidebar}
271276
</Container>
272277
<Minimap
273278
ref={this.minimapRef}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright (c) Mike Lischke. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*/
5+
6+
import type { ComponentChild } from "preact";
7+
8+
import type { ISbDmTrack } from "../../../core/ScoreBookDataModel.js";
9+
import type { UndoManager } from "../../../core/UndoManager.js";
10+
import { requisitions } from "../../../supplement/Requisitions.js";
11+
import { Container } from "../framework/Container.js";
12+
import { Icon } from "../framework/Icon.js";
13+
import { UIIcon } from "../framework/UIIcon.js";
14+
import { ChildAlignment, Orientation } from "../framework/ui-types.js";
15+
import { UIComponent, type ICommonUIProperties } from "../framework/UIComponent.js";
16+
17+
export interface ITrackEditSidebarProps extends ICommonUIProperties {
18+
tracks: ISbDmTrack[];
19+
undoManager: UndoManager;
20+
}
21+
22+
/**
23+
* A sidebar that appears on the right side of the arrangement viewer when edit mode is active.
24+
* Each track row shows icon-only action buttons: delete, insert below, duplicate, clear.
25+
*/
26+
export class TrackEditSidebar extends UIComponent<ITrackEditSidebarProps> {
27+
public render(): ComponentChild {
28+
const { tracks } = this.props;
29+
30+
const className = this.generateFinalClassName([
31+
"trackEditSidebar",
32+
"rounded-xl shadow-md border border-base-200",
33+
]);
34+
35+
const trackRows = tracks.map((track) => {
36+
return (
37+
<Container
38+
key={track.id}
39+
className="trackEditRow"
40+
data-track={track.id}
41+
orientation={Orientation.LeftToRight}
42+
crossAlignment={ChildAlignment.Center}
43+
>
44+
<Container
45+
className="trackEditActions"
46+
orientation={Orientation.LeftToRight}
47+
crossAlignment={ChildAlignment.Center}
48+
>
49+
<button
50+
className="trackEditAction trackEditActionDelete"
51+
title="Delete track"
52+
onClick={(e) => {
53+
this.handleDelete(track);
54+
e.stopPropagation();
55+
}}
56+
>
57+
<Icon src={UIIcon.Trash} width={14} height={14} alt="Delete track" />
58+
</button>
59+
<button
60+
className="trackEditAction trackEditActionInsert"
61+
title="Insert track below"
62+
onClick={(e) => {
63+
this.handleInsert(track);
64+
e.stopPropagation();
65+
}}
66+
>
67+
<Icon src={UIIcon.Add} width={14} height={14} alt="Insert track" />
68+
</button>
69+
<button
70+
className="trackEditAction trackEditActionDuplicate"
71+
title="Duplicate track"
72+
onClick={(e) => {
73+
this.handleDuplicate(track);
74+
e.stopPropagation();
75+
}}
76+
>
77+
<Icon src={UIIcon.Copy} width={14} height={14} alt="Duplicate track" />
78+
</button>
79+
<button
80+
className="trackEditAction trackEditActionClear"
81+
title="Clear track"
82+
onClick={(e) => {
83+
this.handleClear(track);
84+
e.stopPropagation();
85+
}}
86+
>
87+
<Icon src={UIIcon.ClearAll} width={14} height={14} alt="Clear track" />
88+
</button>
89+
</Container>
90+
</Container>
91+
);
92+
});
93+
94+
const emptyState = tracks.length === 0 ? (
95+
<div className="trackEditEmpty">No tracks</div>
96+
) : undefined;
97+
98+
return (
99+
<Container
100+
className={className}
101+
orientation={Orientation.TopDown}
102+
crossAlignment={ChildAlignment.Stretch}
103+
>
104+
<div className="trackEditSidebarHeader">Track Actions</div>
105+
<Container
106+
className="trackEditSidebarPanel"
107+
orientation={Orientation.TopDown}
108+
crossAlignment={ChildAlignment.Stretch}
109+
>
110+
{trackRows}
111+
{emptyState}
112+
</Container>
113+
</Container>
114+
);
115+
}
116+
117+
private handleDelete = (track: ISbDmTrack): void => {
118+
const { undoManager } = this.props;
119+
120+
undoManager.edit({
121+
type: "EditCommand_ArrangementRemoveTrack",
122+
arrangement: track.arrangement,
123+
removeTrack: track,
124+
});
125+
};
126+
127+
private handleInsert = (track: ISbDmTrack): void => {
128+
// Fire a requisition so the parent can open the instrument browser.
129+
// The track reference indicates where to insert after.
130+
void requisitions.execute("insertTrackRequested", track);
131+
};
132+
133+
private handleDuplicate = (track: ISbDmTrack): void => {
134+
const { undoManager } = this.props;
135+
136+
undoManager.edit({
137+
type: "EditCommand_ArrangementDuplicateTrack",
138+
arrangement: track.arrangement,
139+
duplicateTrack: track,
140+
});
141+
};
142+
143+
private handleClear = (track: ISbDmTrack): void => {
144+
const { undoManager } = this.props;
145+
146+
undoManager.edit({
147+
type: "EditCommand_TrackClear",
148+
track,
149+
command: "clear",
150+
});
151+
};
152+
}

src/components/ui/Bar/Staff/StaffBarTrackRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface IStaffBarTrackRowProps extends ICommonUIProperties {
2424

2525
trackPlayer: TrackPlayer;
2626
arrangementPlayer: ArrangementPlayer;
27-
touchEditingEnabled: boolean;
27+
inEditMode: boolean;
2828
services: ScoreBookUiServices;
2929
undoManager: UndoManager;
3030
dataModel: ScoreBookDataModel;

0 commit comments

Comments
 (0)