@hyperframes/studio 0.7.106 → 0.7.108
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/dist/assets/{hyperframes-player-CvVcx_CV.js → hyperframes-player-QqavTWRK.js} +1 -1
- package/dist/assets/{index-CRzCCuPH.js → index-BQjXA1pz.js} +1 -1
- package/dist/assets/{index-BbYg_isc.js → index-BhHHMprP.js} +1 -1
- package/dist/assets/index-DNkh9mbV.css +1 -0
- package/dist/assets/index-GqONLcOl.js +428 -0
- package/dist/{chunk-OBAG3GWK.js → chunk-AZYHQC6V.js} +6 -7
- package/dist/chunk-AZYHQC6V.js.map +1 -0
- package/dist/{domEditingLayers-AT7G6F4L.js → domEditingLayers-7AMZ7GFI.js} +4 -2
- package/dist/index.d.ts +86 -3
- package/dist/index.html +2 -2
- package/dist/index.js +24325 -17831
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/EditorShell.tsx +0 -2
- package/src/components/StudioErrorBoundary.test.tsx +97 -0
- package/src/components/StudioErrorBoundary.tsx +7 -0
- package/src/components/StudioOverlays.tsx +15 -13
- package/src/components/StudioRightPanel.tsx +18 -1
- package/src/components/TimelineToolbar.test.tsx +46 -0
- package/src/components/TimelineToolbar.tsx +17 -0
- package/src/components/editor/DomEditOverlay.tsx +36 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +85 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +29 -3
- package/src/components/editor/InlineTextToolbar.test.tsx +296 -0
- package/src/components/editor/InlineTextToolbar.tsx +281 -0
- package/src/components/editor/OffCanvasIndicators.tsx +38 -0
- package/src/components/editor/PropertyPanel.test.tsx +5 -1
- package/src/components/editor/PropertyPanel.tsx +7 -23
- package/src/components/editor/PropertyPanelFlat.tsx +25 -0
- package/src/components/editor/audioFxSummary.test.ts +75 -0
- package/src/components/editor/audioFxSummary.ts +36 -0
- package/src/components/editor/audioFxTelemetry.test.ts +129 -0
- package/src/components/editor/audioFxTelemetry.ts +230 -0
- package/src/components/editor/domEditInlineText.test.ts +86 -0
- package/src/components/editor/domEditInlineText.ts +99 -0
- package/src/components/editor/domEditInlineTextElement.test.ts +59 -0
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingLayers.ts +7 -6
- package/src/components/editor/inlineTextStyleRange.test.ts +743 -0
- package/src/components/editor/inlineTextStyleRange.ts +593 -0
- package/src/components/editor/inlineTextStyleRead.ts +47 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +1830 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +299 -0
- package/src/components/editor/propertyPanelAudioFxGroupUtils.ts +21 -0
- package/src/components/editor/propertyPanelAutomation.test.ts +60 -0
- package/src/components/editor/propertyPanelAutomation.ts +97 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +37 -9
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFxAddMenu.tsx +209 -0
- package/src/components/editor/propertyPanelFxBandRuler.tsx +72 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +382 -0
- package/src/components/editor/propertyPanelFxControls.test.tsx +115 -0
- package/src/components/editor/propertyPanelFxControls.tsx +356 -0
- package/src/components/editor/propertyPanelFxEqModule.tsx +196 -0
- package/src/components/editor/propertyPanelFxFamily.ts +83 -0
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +269 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +112 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +339 -0
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +159 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +215 -0
- package/src/components/editor/propertyPanelFxPresetStyle.test.ts +142 -0
- package/src/components/editor/propertyPanelFxPresetStyle.ts +216 -0
- package/src/components/editor/propertyPanelFxRackChain.tsx +202 -0
- package/src/components/editor/propertyPanelFxSection.test.tsx +1747 -0
- package/src/components/editor/propertyPanelFxSection.tsx +550 -0
- package/src/components/editor/propertyPanelFxSectionTypes.ts +91 -0
- package/src/components/editor/propertyPanelTypes.ts +5 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useFxAudition.ts +95 -0
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxChainObserved.ts +83 -0
- package/src/components/editor/useFxLevelling.ts +235 -0
- package/src/components/editor/useInlineTextEditing.tsx +119 -0
- package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
- package/src/components/editor/useVolumeAutomation.ts +54 -0
- package/src/components/feedback/CrashFeedbackPrompt.tsx +27 -0
- package/src/components/feedback/StudioFeedbackCard.tsx +373 -0
- package/src/components/feedback/feedbackTrigger.test.ts +168 -0
- package/src/components/feedback/feedbackTrigger.ts +269 -0
- package/src/components/feedback/projectProvenance.test.ts +120 -0
- package/src/components/feedback/projectProvenance.ts +83 -0
- package/src/components/renders/useRenderQueue.ts +62 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +2 -2
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +19 -2
- package/src/contexts/DomEditContext.tsx +15 -0
- package/src/hooks/domEditPersistFailure.ts +0 -7
- package/src/hooks/useAppHotkeys.test.ts +198 -0
- package/src/hooks/useAppHotkeys.ts +31 -4
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +429 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +371 -0
- package/src/hooks/useDomEditAttributeCommits.ts +90 -10
- package/src/hooks/useDomEditCommits.test.tsx +141 -21
- package/src/hooks/useDomEditCommits.ts +4 -0
- package/src/hooks/useDomEditSession.ts +4 -0
- package/src/hooks/useDomEditTextCommits.ts +112 -11
- package/src/hooks/useElementLifecycleOps.ts +3 -0
- package/src/hooks/useFileTree.ts +8 -3
- package/src/hooks/useInlineTextEdit.test.tsx +555 -0
- package/src/hooks/useInlineTextEdit.ts +320 -0
- package/src/hooks/useLivePlayheadTime.ts +49 -0
- package/src/hooks/usePreviewPersistence.ts +7 -0
- package/src/hooks/useTimelineEditing.ts +3 -0
- package/src/player/components/AutomationSelectionMenu.tsx +76 -0
- package/src/player/components/AutomationValueInput.tsx +51 -0
- package/src/player/components/LayerDisclosureRow.tsx +5 -4
- package/src/player/components/Timeline.test.ts +84 -0
- package/src/player/components/TimelineAutomationLane.test.tsx +1720 -0
- package/src/player/components/TimelineAutomationLane.tsx +674 -0
- package/src/player/components/TimelineAutomationLaneSlot.test.tsx +193 -0
- package/src/player/components/TimelineLanes.test.tsx +24 -5
- package/src/player/components/TimelineLanes.tsx +142 -154
- package/src/player/components/TimelineTrackHeader.test.tsx +212 -1
- package/src/player/components/TimelineTrackHeader.tsx +166 -6
- package/src/player/components/TimelineTrackRow.tsx +8 -1
- package/src/player/components/automationClipboard.test.ts +94 -0
- package/src/player/components/automationClipboard.ts +136 -0
- package/src/player/components/automationGestureKeys.test.ts +49 -0
- package/src/player/components/automationGestureKeys.ts +49 -0
- package/src/player/components/automationLaneData.test.ts +270 -0
- package/src/player/components/automationLaneData.ts +258 -0
- package/src/player/components/automationLaneDragMath.ts +194 -0
- package/src/player/components/automationLaneGeometry.test.ts +239 -0
- package/src/player/components/automationLaneGeometry.ts +287 -0
- package/src/player/components/automationLaneHeight.ts +15 -0
- package/src/player/components/automationLanePointer.ts +20 -0
- package/src/player/components/automationLaneSelection.test.ts +183 -0
- package/src/player/components/automationLaneSelection.ts +135 -0
- package/src/player/components/automationShapes.test.ts +82 -0
- package/src/player/components/automationShapes.ts +70 -0
- package/src/player/components/automationSimplify.test.ts +48 -0
- package/src/player/components/automationSimplify.ts +51 -0
- package/src/player/components/timelineClipGestureHandlers.ts +223 -0
- package/src/player/components/timelineLaneProps.ts +19 -0
- package/src/player/components/timelineLayout.ts +11 -5
- package/src/player/components/useAutomationEdgeStretch.test.ts +27 -0
- package/src/player/components/useAutomationEdgeStretch.ts +269 -0
- package/src/player/components/useAutomationLaneGestures.ts +537 -0
- package/src/player/components/useAutomationLanes.test.tsx +112 -0
- package/src/player/components/useAutomationLanes.ts +170 -0
- package/src/player/components/useAutomationRangeDrag.ts +114 -0
- package/src/player/components/useTimelineTrackLayout.test.ts +107 -1
- package/src/player/components/useTimelineTrackLayout.ts +83 -7
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
- package/src/player/hooks/useTimelinePlayer.ts +13 -1
- package/src/player/lib/automationStoreSync.test.ts +83 -0
- package/src/player/lib/automationStoreSync.ts +56 -0
- package/src/player/lib/playbackShortcuts.ts +5 -4
- package/src/player/lib/timelineDOM.test.ts +33 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/store/automationSelectionSlice.test.ts +30 -0
- package/src/player/store/automationSelectionSlice.ts +54 -0
- package/src/player/store/playerStore.test.ts +15 -0
- package/src/player/store/playerStore.ts +54 -65
- package/src/player/store/timelineElement.ts +78 -0
- package/src/styles/studio.css +89 -0
- package/src/telemetry/agentRuntime.test.ts +45 -0
- package/src/telemetry/agentRuntime.ts +62 -0
- package/src/telemetry/breadcrumbs.test.ts +68 -0
- package/src/telemetry/breadcrumbs.ts +70 -0
- package/src/telemetry/client.ts +5 -0
- package/src/telemetry/events.ts +84 -3
- package/src/telemetry/system.ts +8 -0
- package/src/utils/sourcePatcher.ts +5 -1
- package/src/utils/studioFileHistory.test.ts +49 -0
- package/src/utils/studioFileHistory.ts +17 -1
- package/src/utils/studioHelpers.ts +2 -5
- package/src/utils/studioTelemetry.ts +16 -10
- package/src/utils/timelineDiscovery.ts +3 -24
- package/src/utils/typingTarget.test.ts +54 -0
- package/src/utils/typingTarget.ts +43 -0
- package/dist/assets/index-DerI0ikN.js +0 -428
- package/dist/assets/index-tBPidglp.css +0 -1
- package/dist/chunk-OBAG3GWK.js.map +0 -1
- package/src/components/StudioFeedbackBar.tsx +0 -217
- /package/dist/{domEditingLayers-AT7G6F4L.js.map → domEditingLayers-7AMZ7GFI.js.map} +0 -0
|
@@ -15,6 +15,7 @@ export interface DomEditActionsValue extends Pick<
|
|
|
15
15
|
| "handleDomStyleCommit"
|
|
16
16
|
| "handleDomAttributeCommit"
|
|
17
17
|
| "handleDomAttributeLiveCommit"
|
|
18
|
+
| "handleDomAttributeQuietCommit"
|
|
18
19
|
| "handleDomHtmlAttributeCommit"
|
|
19
20
|
| "handleDomAttributesCommit"
|
|
20
21
|
| "handleDomPathOffsetCommit"
|
|
@@ -24,6 +25,7 @@ export interface DomEditActionsValue extends Pick<
|
|
|
24
25
|
| "handleDomRotationCommit"
|
|
25
26
|
| "handleDomManualEditsReset"
|
|
26
27
|
| "handleDomTextCommit"
|
|
28
|
+
| "handleDomRichTextCommit"
|
|
27
29
|
| "handleDomTextFieldStyleCommit"
|
|
28
30
|
| "handleDomAddTextField"
|
|
29
31
|
| "handleDomRemoveTextField"
|
|
@@ -115,6 +117,13 @@ export function useDomEditSelectionContext(): DomEditSelectionValue {
|
|
|
115
117
|
return ctx;
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
/** Optional counterpart to useDomEditActionsContextOptional — same reason: the
|
|
121
|
+
* player package's own components mount outside a provider in standalone and
|
|
122
|
+
* test trees, where "no dom-edit selection" is the correct answer. */
|
|
123
|
+
export function useDomEditSelectionContextOptional(): DomEditSelectionValue | null {
|
|
124
|
+
return useContext(DomEditSelectionContext);
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
/** @deprecated Prefer useDomEditActionsContext or useDomEditSelectionContext. */
|
|
119
128
|
export function useDomEditContext(): DomEditValue {
|
|
120
129
|
return { ...useDomEditActionsContext(), ...useDomEditSelectionContext() };
|
|
@@ -139,6 +148,7 @@ export function DomEditProvider({
|
|
|
139
148
|
handleDomStyleCommit,
|
|
140
149
|
handleDomAttributeCommit,
|
|
141
150
|
handleDomAttributeLiveCommit,
|
|
151
|
+
handleDomAttributeQuietCommit,
|
|
142
152
|
handleDomHtmlAttributeCommit,
|
|
143
153
|
handleDomAttributesCommit,
|
|
144
154
|
handleDomPathOffsetCommit,
|
|
@@ -149,6 +159,7 @@ export function DomEditProvider({
|
|
|
149
159
|
handleDomManualEditsReset,
|
|
150
160
|
|
|
151
161
|
handleDomTextCommit,
|
|
162
|
+
handleDomRichTextCommit,
|
|
152
163
|
handleDomTextFieldStyleCommit,
|
|
153
164
|
handleDomAddTextField,
|
|
154
165
|
handleDomRemoveTextField,
|
|
@@ -227,6 +238,7 @@ export function DomEditProvider({
|
|
|
227
238
|
handleDomStyleCommit,
|
|
228
239
|
handleDomAttributeCommit,
|
|
229
240
|
handleDomAttributeLiveCommit,
|
|
241
|
+
handleDomAttributeQuietCommit,
|
|
230
242
|
handleDomHtmlAttributeCommit,
|
|
231
243
|
handleDomAttributesCommit,
|
|
232
244
|
handleDomPathOffsetCommit,
|
|
@@ -236,6 +248,7 @@ export function DomEditProvider({
|
|
|
236
248
|
handleDomRotationCommit,
|
|
237
249
|
handleDomManualEditsReset,
|
|
238
250
|
handleDomTextCommit,
|
|
251
|
+
handleDomRichTextCommit,
|
|
239
252
|
handleDomTextFieldStyleCommit,
|
|
240
253
|
handleDomAddTextField,
|
|
241
254
|
handleDomRemoveTextField,
|
|
@@ -296,6 +309,7 @@ export function DomEditProvider({
|
|
|
296
309
|
handleDomStyleCommit,
|
|
297
310
|
handleDomAttributeCommit,
|
|
298
311
|
handleDomAttributeLiveCommit,
|
|
312
|
+
handleDomAttributeQuietCommit,
|
|
299
313
|
handleDomHtmlAttributeCommit,
|
|
300
314
|
handleDomAttributesCommit,
|
|
301
315
|
handleDomPathOffsetCommit,
|
|
@@ -305,6 +319,7 @@ export function DomEditProvider({
|
|
|
305
319
|
handleDomRotationCommit,
|
|
306
320
|
handleDomManualEditsReset,
|
|
307
321
|
handleDomTextCommit,
|
|
322
|
+
handleDomRichTextCommit,
|
|
308
323
|
handleDomTextFieldStyleCommit,
|
|
309
324
|
handleDomAddTextField,
|
|
310
325
|
handleDomRemoveTextField,
|
|
@@ -19,13 +19,6 @@ export class DomEditPersistUnsafeValueError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export class DomEditPersistUnsupportedTextStructureError extends Error {
|
|
23
|
-
constructor() {
|
|
24
|
-
super("Couldn't save this text structure change");
|
|
25
|
-
this.name = "DomEditPersistUnsupportedTextStructureError";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
export type DomEditPersistFailureSelection = Pick<
|
|
30
23
|
DomEditSelection,
|
|
31
24
|
"label" | "hfId" | "id" | "selector" | "selectorIndex" | "sourceFile"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { dispatchModifierKey, dispatchPlainKey } from "./useAppHotkeys";
|
|
4
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
5
|
+
import { clearAutomationClipboard, copyRange } from "../player/components/automationClipboard";
|
|
6
|
+
import { VOLUME_RANGE } from "@hyperframes/core/audio-automation";
|
|
7
|
+
import type { TimelineElement } from "../player/store/timelineElement";
|
|
8
|
+
|
|
9
|
+
/** Minimal valid fixture — TimelineElement only requires these five fields. */
|
|
10
|
+
const bgmElement: TimelineElement = {
|
|
11
|
+
id: "bgm",
|
|
12
|
+
key: "bgm",
|
|
13
|
+
tag: "audio",
|
|
14
|
+
start: 0,
|
|
15
|
+
duration: 6,
|
|
16
|
+
track: 0,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Every callback dispatchPlainKey can reach, so a test can assert which one
|
|
20
|
+
* a key resolved to. Unannotated on purpose: the parameter type is not
|
|
21
|
+
* exported, and structural inference checks it at the call site. */
|
|
22
|
+
function callbacks() {
|
|
23
|
+
return {
|
|
24
|
+
handleTimelineElementDelete: vi.fn(async () => {}),
|
|
25
|
+
handleTimelineElementSplit: vi.fn(async () => {}),
|
|
26
|
+
handleDomEditElementDelete: vi.fn(async () => {}),
|
|
27
|
+
handleUndo: vi.fn(async () => {}),
|
|
28
|
+
handleRedo: vi.fn(async () => {}),
|
|
29
|
+
handleCopy: vi.fn(() => false),
|
|
30
|
+
handlePaste: vi.fn(async () => {}),
|
|
31
|
+
handleCut: vi.fn(async () => false),
|
|
32
|
+
onResetKeyframes: vi.fn(() => true),
|
|
33
|
+
onDeleteSelectedKeyframes: vi.fn(),
|
|
34
|
+
showToast: vi.fn(),
|
|
35
|
+
leftSidebarRef: { current: null },
|
|
36
|
+
domEditSelectionRef: { current: null },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const press = (key: string) =>
|
|
41
|
+
new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true });
|
|
42
|
+
|
|
43
|
+
const chord = (key: string) =>
|
|
44
|
+
new KeyboardEvent("keydown", { key, metaKey: true, bubbles: true, cancelable: true });
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
clearAutomationClipboard();
|
|
48
|
+
usePlayerStore.getState().clearAutomationSelection();
|
|
49
|
+
usePlayerStore.setState({
|
|
50
|
+
elements: [],
|
|
51
|
+
selectedElementId: null,
|
|
52
|
+
selectedElementIds: new Set<string>(),
|
|
53
|
+
selectedKeyframes: new Set<string>(),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("dispatchPlainKey — Delete arbitration", () => {
|
|
58
|
+
const selectBgm = () =>
|
|
59
|
+
usePlayerStore.setState({ elements: [bgmElement], selectedElementId: "bgm" });
|
|
60
|
+
|
|
61
|
+
const selectRange = () =>
|
|
62
|
+
usePlayerStore
|
|
63
|
+
.getState()
|
|
64
|
+
.setAutomationSelection({ elementKey: "bgm", target: "volume", t0: 2, t1: 4 });
|
|
65
|
+
|
|
66
|
+
it("deletes the selected clip when no automation range is active", () => {
|
|
67
|
+
selectBgm();
|
|
68
|
+
const cb = callbacks();
|
|
69
|
+
const e = press("Delete");
|
|
70
|
+
dispatchPlainKey(e, "delete", cb);
|
|
71
|
+
// The pre-existing contract, pinned so the new guard cannot widen.
|
|
72
|
+
expect(cb.handleTimelineElementDelete).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(e.defaultPrevented).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("leaves the clip alone when an automation range is active", () => {
|
|
77
|
+
// The bug: this listener is on window/capture so it runs BEFORE
|
|
78
|
+
// useAutomationSelectionKeyboard's document/capture handler. Without the
|
|
79
|
+
// guard, clearing a 2s automation range deleted the whole audio clip.
|
|
80
|
+
selectBgm();
|
|
81
|
+
selectRange();
|
|
82
|
+
const cb = callbacks();
|
|
83
|
+
const e = press("Delete");
|
|
84
|
+
dispatchPlainKey(e, "delete", cb);
|
|
85
|
+
expect(cb.handleTimelineElementDelete).not.toHaveBeenCalled();
|
|
86
|
+
// Must NOT be consumed: the automation handler downstream still needs it.
|
|
87
|
+
expect(e.defaultPrevented).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("leaves keyframe reset alone when an automation range is active", () => {
|
|
91
|
+
// Backspace's reset-keyframes branch sits below the guard, so it has to be
|
|
92
|
+
// covered too — otherwise Backspace wiped every keyframe on the clip.
|
|
93
|
+
selectBgm();
|
|
94
|
+
usePlayerStore.setState({
|
|
95
|
+
keyframeCache: new Map([["bgm", { targets: [], version: 0 }]]),
|
|
96
|
+
});
|
|
97
|
+
selectRange();
|
|
98
|
+
const cb = callbacks();
|
|
99
|
+
const e = press("Backspace");
|
|
100
|
+
dispatchPlainKey(e, "backspace", cb);
|
|
101
|
+
expect(cb.onResetKeyframes).not.toHaveBeenCalled();
|
|
102
|
+
expect(cb.handleTimelineElementDelete).not.toHaveBeenCalled();
|
|
103
|
+
expect(e.defaultPrevented).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("still lets a keyframe selection win over an automation range", () => {
|
|
107
|
+
// Ordering: the keyframe guard precedes the automation one, so a keyframe
|
|
108
|
+
// selection keeps Delete even with a range showing.
|
|
109
|
+
selectBgm();
|
|
110
|
+
selectRange();
|
|
111
|
+
usePlayerStore.setState({ selectedKeyframes: new Set(["bgm:opacity:0"]) });
|
|
112
|
+
const cb = callbacks();
|
|
113
|
+
const e = press("Delete");
|
|
114
|
+
dispatchPlainKey(e, "delete", cb);
|
|
115
|
+
expect(cb.onDeleteSelectedKeyframes).toHaveBeenCalledTimes(1);
|
|
116
|
+
expect(cb.handleTimelineElementDelete).not.toHaveBeenCalled();
|
|
117
|
+
expect(e.defaultPrevented).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("dispatchModifierKey — Cmd+C/Cmd+V arbitration", () => {
|
|
122
|
+
const clip: TimelineElement = {
|
|
123
|
+
id: "bgm",
|
|
124
|
+
key: "bgm",
|
|
125
|
+
tag: "audio",
|
|
126
|
+
start: 0,
|
|
127
|
+
duration: 6,
|
|
128
|
+
track: 0,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
it("lets the clip clipboard have Cmd+C when no automation range is active", () => {
|
|
132
|
+
usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
|
|
133
|
+
const cb = callbacks();
|
|
134
|
+
dispatchModifierKey(chord("c"), "c", cb);
|
|
135
|
+
expect(cb.handleCopy).toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("keeps Cmd+C from the clip clipboard when an automation range is active", () => {
|
|
139
|
+
// Both clipboards arming on one press double-wrote and toasted "Copied clip".
|
|
140
|
+
usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
|
|
141
|
+
usePlayerStore.getState().setAutomationSelection({
|
|
142
|
+
elementKey: "bgm",
|
|
143
|
+
target: "volume",
|
|
144
|
+
t0: 1,
|
|
145
|
+
t1: 3,
|
|
146
|
+
});
|
|
147
|
+
const cb = callbacks();
|
|
148
|
+
const e = chord("c");
|
|
149
|
+
expect(dispatchModifierKey(e, "c", cb)).toBe(true);
|
|
150
|
+
expect(cb.handleCopy).not.toHaveBeenCalled();
|
|
151
|
+
// No preventDefault: the automation handler downstream still needs the key.
|
|
152
|
+
expect(e.defaultPrevented).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("lets the clip clipboard have Cmd+V when the automation clipboard is empty", () => {
|
|
156
|
+
// Nothing to paste means nothing to claim — the clip paste should still run.
|
|
157
|
+
clearAutomationClipboard();
|
|
158
|
+
usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
|
|
159
|
+
usePlayerStore.getState().setAutomationSelection({
|
|
160
|
+
elementKey: "bgm",
|
|
161
|
+
target: "volume",
|
|
162
|
+
t0: 1,
|
|
163
|
+
t1: 3,
|
|
164
|
+
});
|
|
165
|
+
const cb = callbacks();
|
|
166
|
+
dispatchModifierKey(chord("v"), "v", cb);
|
|
167
|
+
expect(cb.handlePaste).toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("keeps Cmd+V from duplicating the clip while an automation paste is pending", () => {
|
|
171
|
+
clearAutomationClipboard();
|
|
172
|
+
copyRange(
|
|
173
|
+
null,
|
|
174
|
+
{
|
|
175
|
+
target: "volume",
|
|
176
|
+
points: [
|
|
177
|
+
{ t: 1, v: 1 },
|
|
178
|
+
{ t: 3, v: 0.25 },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
VOLUME_RANGE,
|
|
182
|
+
1,
|
|
183
|
+
3,
|
|
184
|
+
);
|
|
185
|
+
usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
|
|
186
|
+
usePlayerStore.getState().setAutomationSelection({
|
|
187
|
+
elementKey: "bgm",
|
|
188
|
+
target: "volume",
|
|
189
|
+
t0: 1,
|
|
190
|
+
t1: 3,
|
|
191
|
+
});
|
|
192
|
+
const cb = callbacks();
|
|
193
|
+
const e = chord("v");
|
|
194
|
+
dispatchModifierKey(e, "v", cb);
|
|
195
|
+
expect(cb.handlePaste).not.toHaveBeenCalled();
|
|
196
|
+
expect(e.defaultPrevented).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { automationOwnsKey } from "./useAutomationSelectionKeyboard";
|
|
2
3
|
import { usePlayerStore } from "../player";
|
|
3
4
|
import type { TimelineElement } from "../player";
|
|
4
5
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
5
6
|
import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
|
|
6
7
|
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
8
|
+
import { isTypingTarget } from "../utils/typingTarget";
|
|
7
9
|
import { isEditableTarget } from "../utils/timelineDiscovery";
|
|
8
10
|
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
|
|
9
11
|
import { canSplitElement } from "../utils/timelineElementSplit";
|
|
@@ -158,7 +160,14 @@ interface HotkeyCallbacks {
|
|
|
158
160
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
/** Exported for tests, like dispatchPlainKey below: lets the Cmd+C/Cmd+V
|
|
164
|
+
* arbitration between an automation range and the clip clipboard be asserted
|
|
165
|
+
* without standing up the whole hook. */
|
|
166
|
+
export function dispatchModifierKey(
|
|
167
|
+
event: KeyboardEvent,
|
|
168
|
+
key: string,
|
|
169
|
+
cb: HotkeyCallbacks,
|
|
170
|
+
): boolean {
|
|
162
171
|
if (
|
|
163
172
|
!shouldIgnoreHistoryShortcut(event.target) &&
|
|
164
173
|
handleUndoRedoKey(
|
|
@@ -188,7 +197,7 @@ function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallba
|
|
|
188
197
|
return true;
|
|
189
198
|
}
|
|
190
199
|
|
|
191
|
-
if (key === "g" && !event.altKey && !
|
|
200
|
+
if (key === "g" && !event.altKey && !isTypingTarget(event.target)) {
|
|
192
201
|
event.preventDefault();
|
|
193
202
|
if (event.shiftKey) cb.onUngroupSelection?.();
|
|
194
203
|
else cb.onGroupSelection?.();
|
|
@@ -196,6 +205,14 @@ function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallba
|
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
if (!event.shiftKey && !event.altKey && !isEditableTarget(event.target)) {
|
|
208
|
+
// An active automation range owns Cmd+C/Cmd+V, the same way it owns Delete
|
|
209
|
+
// below. This listener is on window/capture and runs before
|
|
210
|
+
// useAutomationSelectionKeyboard's document/capture handler, so without
|
|
211
|
+
// this the clip clipboard also claimed the key: Cmd+V duplicated the clip
|
|
212
|
+
// while the automation paste wrote the same file, and Cmd+C armed both
|
|
213
|
+
// clipboards and toasted "Copied clip". Return without preventDefault so
|
|
214
|
+
// the downstream handler still sees the key.
|
|
215
|
+
if (automationOwnsKey(event)) return true;
|
|
199
216
|
if (key === "c") {
|
|
200
217
|
if (cb.handleCopy()) {
|
|
201
218
|
event.preventDefault();
|
|
@@ -222,7 +239,10 @@ function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallba
|
|
|
222
239
|
}
|
|
223
240
|
|
|
224
241
|
// fallow-ignore-next-line complexity
|
|
225
|
-
|
|
242
|
+
/** Exported for tests: the unmodified-key half of the dispatcher, so the
|
|
243
|
+
* Delete arbitration between keyframes, an automation range and the clip can
|
|
244
|
+
* be asserted without standing up the whole hook. */
|
|
245
|
+
export function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks): void {
|
|
226
246
|
if (key === "f" && !event.shiftKey && !event.altKey) {
|
|
227
247
|
event.preventDefault();
|
|
228
248
|
if (document.fullscreenElement) void document.exitFullscreen();
|
|
@@ -288,6 +308,13 @@ function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks
|
|
|
288
308
|
event.preventDefault();
|
|
289
309
|
return;
|
|
290
310
|
}
|
|
311
|
+
// An active automation range owns Delete: useAutomationSelectionKeyboard
|
|
312
|
+
// empties the range in place, pinning the anchors. Fall through WITHOUT
|
|
313
|
+
// preventDefault so that document-level handler still sees the key — this
|
|
314
|
+
// listener is on window/capture, so it runs first and everything below
|
|
315
|
+
// would otherwise win. Without this the press reaches the clip delete
|
|
316
|
+
// below and destroys the whole clip the lane belongs to.
|
|
317
|
+
if (usePlayerStore.getState().automationSelection) return;
|
|
291
318
|
if (event.key === "Backspace") {
|
|
292
319
|
const { selectedElementId, keyframeCache } = usePlayerStore.getState();
|
|
293
320
|
if (selectedElementId && keyframeCache.has(selectedElementId) && cb.onResetKeyframes()) {
|
|
@@ -454,7 +481,7 @@ export function useAppHotkeys({
|
|
|
454
481
|
dispatchModifierKey(event, key, cb);
|
|
455
482
|
return;
|
|
456
483
|
}
|
|
457
|
-
if (!
|
|
484
|
+
if (!isTypingTarget(event.target)) dispatchPlainKey(event, key, cb);
|
|
458
485
|
}, []);
|
|
459
486
|
|
|
460
487
|
// eslint-disable-next-line no-restricted-syntax
|