Skip to content

Commit 16b095c

Browse files
committed
Replace EditCommand system with direct DataModel mutations
Remove the discriminated-union edit command infrastructure entirely. ScoreBookDataModel now exposes focused mutation methods (setTitle, addTrack, removeTrack, clearTrack, setTimeSignature, setTempo, setLength, clearAllTracks, clearSelection) that apply changes directly and fire a parameterless "arrangementMutated" requisition. The UndoManager listens to this event and records snapshots. - Delete src/core/types/edit_commands.ts (~120 lines) - Delete src/core/edit.ts (~170 lines) - Remove UndoRedoStack squash/note-cycling logic (~120 lines) - Simplify IHistoryState to snapshot + timestamp only - Replace editCommandExecuted requisition with arrangementMutated - Add editMode-aware tempo behavior with scoreTempo tracking - Remove dead undoManager prop chain through ArrangementViewer → StaffBarViewer → StaffBarTrackRow and PrintView - Migrate all callers from dataModel.edit({ type: "…", … }) to dataModel.setXxx(…) style Net: -291 lines, zero conceptual abstractions removed. Signed-off-by: Mike Lischke <mike@lischke-online.de>
1 parent b0f2d38 commit 16b095c

21 files changed

Lines changed: 417 additions & 708 deletions

src/App.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ export class App extends UIComponent<{}, IAppState> {
458458
arrangementPlayer={this.arrangementPlayer!}
459459
dataModel={this.dataModel}
460460
services={this.services}
461-
undoManager={this.undoManager!}
461+
editMode={editMode}
462462
data-tutorial="playback"
463463
/>
464464
<Container
@@ -503,7 +503,6 @@ export class App extends UIComponent<{}, IAppState> {
503503
arrangementPlayer={this.arrangementPlayer}
504504
dataModel={this.dataModel}
505505
services={this.services}
506-
undoManager={this.undoManager!}
507506
inEditMode={editMode}
508507
/>
509508
}
@@ -560,7 +559,6 @@ export class App extends UIComponent<{}, IAppState> {
560559
dataModel={this.dataModel}
561560
arrangementPlayer={this.arrangementPlayer}
562561
services={this.services}
563-
undoManager={this.undoManager}
564562
/>
565563
)
566564
}
@@ -609,7 +607,7 @@ export class App extends UIComponent<{}, IAppState> {
609607
className="main-arrangement-title"
610608
style={editMode ? { flex: 1, minWidth: 0 } : undefined}
611609
arrangement={arrangementView}
612-
undoManager={this.undoManager!}
610+
dataModel={this.dataModel}
613611
editMode={editMode}
614612
/>
615613
{
@@ -1105,6 +1103,7 @@ export class App extends UIComponent<{}, IAppState> {
11051103
this.arrangementPlayer = undefined;
11061104
}
11071105

1106+
this.undoManager?.dispose();
11081107
this.undoManager = undefined;
11091108

11101109
// Clear status bar item references — they belong to the old (now-unmounted) Statusbar.
@@ -1527,6 +1526,7 @@ export class App extends UIComponent<{}, IAppState> {
15271526
};
15281527

15291528
private initAppState(): void {
1529+
this.undoManager?.dispose();
15301530
this.undoManager = new UndoManager(this.dataModel);
15311531
this.arrangementPlayer = new ArrangementPlayer(this.dataModel);
15321532
}
@@ -1568,6 +1568,7 @@ export class App extends UIComponent<{}, IAppState> {
15681568
}
15691569
}
15701570

1571+
this.undoManager?.dispose();
15711572
this.undoManager = new UndoManager(this.dataModel);
15721573
this.arrangementPlayer = new ArrangementPlayer(this.dataModel);
15731574
requisitions.register("timeParamsChanged", this.handleTimeParamsChange);
@@ -1620,11 +1621,7 @@ export class App extends UIComponent<{}, IAppState> {
16201621
case "Backspace":
16211622
case "Delete": {
16221623
if (!(event.target instanceof HTMLInputElement)) {
1623-
this.undoManager?.edit({
1624-
type: "EditCommand_ArrangementClearSelection",
1625-
arrangement: this.dataModel.arrangement!,
1626-
clearSelection: new Map()
1627-
});
1624+
this.dataModel.clearSelection(new Map());
16281625
this.services.selectionManager.clearSelection();
16291626
}
16301627

src/components/ui/Arrangement/ArrangementEditControls.tsx

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import type { ISbDmArrangement, ISbDmTrack, ScoreBookDataModel } from "../../../core/ScoreBookDataModel.js";
77
import type { ISelectionDelta } from "../../../ui/selection-types.js";
8-
import type { EditCommand_TimeParamsTimeSignature } from "../../../core/types/edit_commands.js";
98
import type { UndoManager } from "../../../core/UndoManager.js";
109
import type { ScoreBookUiServices } from "../../../player/types.js";
1110
import { requisitions } from "../../../supplement/Requisitions.js";
@@ -77,8 +76,6 @@ export class ArrangementEditControls
7776
const { dataModel, services, undoManager } = this.props;
7877
const { arePolyrhythms } = this.state;
7978

80-
const arrangementView = dataModel.arrangement!;
81-
8279
// TODO: move this to a score creation dialog. Changing that in an existing score makes no sense.
8380
/*const signatureSelect = (
8481
<Container className="time-control" crossAlignment={ChildAlignment.Center}>
@@ -135,10 +132,7 @@ export class ArrangementEditControls
135132
<ExpandingSpacer />
136133
<Button
137134
onClick={() => {
138-
undoManager.edit({
139-
type: "EditCommand_ArrangementClear", arrangement: arrangementView,
140-
command: "clear all tracks"
141-
});
135+
dataModel.clearAllTracks();
142136
Overlay.toggleOverlay("clear_tracks", "hide");
143137
}}
144138
>
@@ -177,7 +171,6 @@ export class ArrangementEditControls
177171
<SelectionControls
178172
dataModel={dataModel}
179173
services={services}
180-
undoManager={undoManager}
181174
/>
182175
</Overlay>
183176

@@ -263,50 +256,41 @@ export class ArrangementEditControls
263256
};
264257

265258
private changeTimeSignature = (event: InputEvent) => {
266-
const { dataModel, undoManager } = this.props;
267-
268-
const arrangementView = dataModel.arrangement!;
259+
const { dataModel } = this.props;
269260

270-
const command: Partial<EditCommand_TimeParamsTimeSignature> = {
271-
type: "EditCommand_TimeParamsTimeSignature",
272-
timeParams: arrangementView.timeParams
273-
};
261+
const value = (event.target as HTMLInputElement).value;
262+
let stepResolution: number;
263+
let pulse: string;
274264

275-
command.timeSignature = (event.target as HTMLInputElement).value;
276-
switch ((event.target as HTMLInputElement).value) {
265+
switch (value) {
277266
case "4/4":
278-
command.stepResolution = 16;
279-
command.pulse = "1/4";
267+
stepResolution = 16;
268+
pulse = "1/4";
280269
break;
281270
case "6/8":
282-
command.stepResolution = 8;
283-
command.pulse = "3/8";
271+
stepResolution = 8;
272+
pulse = "3/8";
284273
break;
285274
case "5/4":
286-
command.stepResolution = 8;
287-
command.pulse = "1/2";
275+
stepResolution = 8;
276+
pulse = "1/2";
288277
break;
289278
case "7/8":
290-
command.stepResolution = 8;
291-
command.pulse = "1/2";
279+
stepResolution = 8;
280+
pulse = "1/2";
292281
break;
282+
default:
283+
return;
293284
}
294285

295-
// XXX: if you need such a cast, it may be a sign that the command type definitions could be improved.
296-
undoManager.edit(command as EditCommand_TimeParamsTimeSignature);
286+
dataModel.setTimeSignature(value, pulse, stepResolution);
297287
};
298288

299289
private handleLengthChange = (newValue: number) => {
300-
const { dataModel, undoManager } = this.props;
301-
302-
const arrangementView = dataModel.arrangement!;
290+
const { dataModel } = this.props;
303291

304292
if (!isNaN(newValue)) {
305-
undoManager.edit({
306-
type: "EditCommand_TimeParamsLength",
307-
timeParams: arrangementView.timeParams,
308-
length: newValue
309-
});
293+
dataModel.setLength(newValue);
310294
}
311295
};
312296

src/components/ui/Arrangement/ArrangementPlayControls.tsx

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { type ComponentChild, createRef } from "preact";
77

88
import { AppStorage } from "../../../core/AppStorage.js";
99
import type { ScoreBookDataModel } from "../../../core/ScoreBookDataModel.js";
10-
import type { UndoManager } from "../../../core/UndoManager.js";
1110
import type { ArrangementPlayer } from "../../../player/ArrangementPlayer.js";
1211
import type { ScoreBookUiServices } from "../../../player/types.js";
1312
import { requisitions } from "../../../supplement/Requisitions.js";
@@ -33,7 +32,7 @@ export interface IArrangementPlayControlsProperties extends ICommonUIProperties
3332
arrangementPlayer: ArrangementPlayer,
3433
dataModel: ScoreBookDataModel;
3534
services: ScoreBookUiServices;
36-
undoManager: UndoManager;
35+
editMode: boolean;
3736
}
3837

3938
interface IArrangementPlayControlsState {
@@ -42,6 +41,10 @@ interface IArrangementPlayControlsState {
4241

4342
currentVolume: number;
4443
currentTempo: number;
44+
45+
/** The tempo as stored in the arrangement (last persisted value). Used to detect playback deviations. */
46+
scoreTempo: number;
47+
4548
recordingInProgress: boolean;
4649
}
4750

@@ -53,11 +56,13 @@ export class ArrangementPlayControls
5356
super(props);
5457

5558
const arrangementView = props.dataModel.arrangement!;
59+
const tempo = arrangementView.timeParams.tempo;
5660
this.state = {
5761
editingTitle: false,
5862
title: arrangementView.title,
5963
currentVolume: arrangementView.mainVolume,
60-
currentTempo: arrangementView.timeParams.tempo,
64+
currentTempo: tempo,
65+
scoreTempo: tempo,
6166
recordingInProgress: false,
6267
};
6368
}
@@ -72,12 +77,30 @@ export class ArrangementPlayControls
7277

7378
public override componentDidUpdate(previousProps: Readonly<IArrangementPlayControlsProperties>,
7479
previousState: Readonly<IArrangementPlayControlsState>): void {
75-
const { dataModel } = this.props;
76-
const { recordingInProgress } = this.state;
80+
const { dataModel, editMode } = this.props;
81+
const { recordingInProgress, scoreTempo } = this.state;
7782

7883
const arrangement = dataModel.arrangement!;
84+
85+
// When entering edit mode, restore the arrangement tempo so the user edits from the saved baseline.
86+
if (!previousProps.editMode && editMode) {
87+
arrangement.timeParams.tempo = scoreTempo;
88+
this.setState({ currentTempo: scoreTempo });
89+
90+
return;
91+
}
92+
93+
// Sync currentTempo when the arrangement tempo changes externally (undo/redo in edit mode,
94+
// or arrangement reload). In edit mode also keep scoreTempo in sync.
7995
if (previousState.currentTempo !== arrangement.timeParams.tempo) {
80-
this.setState({ currentTempo: arrangement.timeParams.tempo });
96+
if (editMode) {
97+
this.setState({
98+
currentTempo: arrangement.timeParams.tempo,
99+
scoreTempo: arrangement.timeParams.tempo,
100+
});
101+
} else {
102+
this.setState({ currentTempo: arrangement.timeParams.tempo });
103+
}
81104
}
82105

83106
if (!previousState.recordingInProgress && recordingInProgress) {
@@ -89,8 +112,8 @@ export class ArrangementPlayControls
89112

90113
public override shouldComponentUpdate(nextProps: Readonly<IArrangementPlayControlsProperties>,
91114
nextState: Readonly<IArrangementPlayControlsState>): boolean {
92-
const { arrangementPlayer, dataModel } = this.props;
93-
const { editingTitle, title, currentVolume, currentTempo, recordingInProgress } = this.state;
115+
const { arrangementPlayer, dataModel, editMode } = this.props;
116+
const { editingTitle, title, currentVolume, currentTempo, scoreTempo, recordingInProgress } = this.state;
94117

95118
if (arrangementPlayer !== nextProps.arrangementPlayer) {
96119
return true;
@@ -100,6 +123,10 @@ export class ArrangementPlayControls
100123
return true;
101124
}
102125

126+
if (editMode !== nextProps.editMode) {
127+
return true;
128+
}
129+
103130
if (editingTitle !== nextState.editingTitle) {
104131
return true;
105132
}
@@ -116,6 +143,10 @@ export class ArrangementPlayControls
116143
return true;
117144
}
118145

146+
if (scoreTempo !== nextState.scoreTempo) {
147+
return true;
148+
}
149+
119150
if (recordingInProgress !== nextState.recordingInProgress) {
120151
return true;
121152
}
@@ -124,10 +155,30 @@ export class ArrangementPlayControls
124155
}
125156

126157
public override render(): ComponentChild {
127-
const { arrangementPlayer, dataModel, undoManager } = this.props;
128-
const { currentVolume, currentTempo, recordingInProgress } = this.state;
158+
const { arrangementPlayer, dataModel, editMode } = this.props;
159+
const { currentVolume, currentTempo, scoreTempo, recordingInProgress } = this.state;
129160

130161
const arrangementView = dataModel.arrangement!;
162+
const tempoDeviates = !editMode && currentTempo !== scoreTempo;
163+
164+
let tempoLabel: ComponentChild;
165+
if (tempoDeviates) {
166+
tempoLabel = (
167+
<Label
168+
caption={`${currentTempo} bpm *`}
169+
style={{ fontSize: "80%", marginTop: "4px", whiteSpace: "nowrap", fontStyle: "italic" }}
170+
data-tooltip={`Playback tempo (${currentTempo} bpm) differs from the arrangement`
171+
+ ` tempo (${scoreTempo} bpm). Enter Edit Mode to persist the change.`}
172+
/>
173+
);
174+
} else {
175+
tempoLabel = (
176+
<Label
177+
caption={`${currentTempo} bpm`}
178+
style={{ fontSize: "80%", marginTop: "4px", whiteSpace: "nowrap" }}
179+
/>
180+
);
181+
}
131182

132183
return (
133184
<Grid id="arrangementPlayControls" columns={[160, "auto"]} {...this.dataAttributes}>
@@ -173,19 +224,16 @@ export class ArrangementPlayControls
173224
data-tooltip="inherit"
174225
className="du-range-xs"
175226
onChange={(value) => {
176-
this.setState({ currentTempo: value });
177-
undoManager.edit({
178-
type: "EditCommand_TimeParamsTempo",
179-
timeParams: arrangementView.timeParams,
180-
tempo: value
181-
});
182-
227+
if (editMode) {
228+
dataModel.setTempo(value);
229+
this.setState({ currentTempo: value, scoreTempo: value });
230+
} else {
231+
this.setState({ currentTempo: value });
232+
arrangementView.timeParams.tempo = value;
233+
}
183234
}}
184235
/>
185-
<Label
186-
caption={`${currentTempo} bpm`}
187-
style={{ fontSize: "80%", marginTop: "4px", whiteSpace: "nowrap" }}
188-
/>
236+
{tempoLabel}
189237
</Container>
190238
<Container
191239
mainAlignment={ChildAlignment.Start}

src/components/ui/Arrangement/ArrangementTitle.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55

66
import { createRef, type ComponentChild } from "preact";
77

8-
import type { ISbDmArrangement } from "../../../core/ScoreBookDataModel.js";
9-
import type { UndoManager } from "../../../core/UndoManager.js";
8+
import type { ISbDmArrangement, ScoreBookDataModel } from "../../../core/ScoreBookDataModel.js";
109
import { KeyboardKeys } from "../../../core/utils.js";
1110
import { Label } from "../framework/Label.js";
1211
import { UIComponent, type ICommonUIProperties } from "../framework/UIComponent.js";
1312

1413
export interface IArrangementTitleProperties extends ICommonUIProperties {
1514
arrangement: Readonly<ISbDmArrangement>;
16-
undoManager: UndoManager;
15+
dataModel: ScoreBookDataModel;
1716
editMode: boolean;
1817
}
1918

@@ -89,7 +88,7 @@ export class ArrangementTitle extends UIComponent<IArrangementTitleProperties, I
8988
};
9089

9190
private handleBlur = (event: FocusEvent) => {
92-
const { undoManager, arrangement } = this.props;
91+
const { dataModel, arrangement } = this.props;
9392
const { title } = this.state;
9493
const newTitle = (event.target as HTMLInputElement).value;
9594

@@ -101,10 +100,7 @@ export class ArrangementTitle extends UIComponent<IArrangementTitleProperties, I
101100
return;
102101
}
103102

104-
undoManager.edit({
105-
type: "EditCommand_ArrangementTitle", arrangement,
106-
newTitle
107-
});
103+
dataModel.setTitle(newTitle);
108104
};
109105

110106
private handleInputKeyDown = (e: KeyboardEvent) => {

0 commit comments

Comments
 (0)