changes alt to ctrl for copying

This commit is contained in:
André Michelle
2026-01-09 16:22:47 +01:00
parent eb90115dc8
commit 3762abbe35
13 changed files with 139 additions and 116 deletions
+3 -2
View File
@@ -3,8 +3,9 @@
## Coding Style
- **Never use single-letter abbreviations in lambdas.** Use descriptive names like `entry`, `text`, `value`, `event`, etc.
- **Use types from `@opendaw/lib-std` instead of inline union types:**
- **Use types and functions from `@opendaw/lib-std` instead of inline checks:**
- Use `Optional<T>` instead of `T | undefined`
- Use `Nullable<T>` instead of `T | null`
- Use `isDefined()` instead of `!== undefined`
- Use `isDefined(value)` instead of `value !== undefined` or `value !== null`
- Use `!isDefined(value)` instead of `value === undefined` or `value === null`
- Never write `| null` or `| undefined` inline - always use the lib-std types.
+2 -2
View File
@@ -10,7 +10,7 @@ import {
Terminable
} from "@opendaw/lib-std"
import {AnyDragData, DragFile} from "@/ui/AnyDragData"
import {Events, Keyboard} from "@opendaw/lib-dom"
import {Events} from "@opendaw/lib-dom"
export namespace DragAndDrop {
let dragging: Option<AnyDragData> = Option.None
@@ -86,7 +86,7 @@ export namespace DragAndDrop {
some: data => {
if (process.drag(event, data)) {
event.preventDefault()
dataTransfer.dropEffect = Keyboard.isCopyKey(event) || data.copy === true ? "copy" : "move"
dataTransfer.dropEffect = event.altKey || data.copy === true ? "copy" : "move"
}
}
})
@@ -71,8 +71,7 @@ const createInstrumentList = (lifecycle: Lifecycle, project: Project) => (
lifecycle.ownAll(
DragAndDrop.installSource(element, () => ({
type: "instrument",
device: key as InstrumentFactories.Keys,
copy: true
device: key as InstrumentFactories.Keys
} satisfies DragDevice)),
TextTooltip.simple(element, () => {
const {bottom, left} = element.getBoundingClientRect()
@@ -85,7 +85,7 @@ export const BusySlot = ({
<Icon symbol={IconSymbol.Focus} className="edit"/>
)
const element: HTMLElement = (
<div className={className} tabIndex={-1} draggable>
<div className={className} tabIndex={-1} data-slot-index={octave.getValue() * 12 + semitone}>
{header}
<div className="waveform">
{waveform}
@@ -150,12 +150,19 @@ export const BusySlot = ({
}
}
})),
SlotDragAndDrop.install({
octave.catchupAndSubscribe(owner => {
const slotIndex = owner.getValue() * 12 + semitone
element.setAttribute("data-slot-index", String(slotIndex))
}),
SlotDragAndDrop.installSource({
element,
sample,
getSlotIndex: () => octave.getValue() * 12 + semitone
}),
SlotDragAndDrop.installTarget({
element,
project: service.project,
sample: Option.wrap(sample),
octave,
semitone
getSlotIndex: () => octave.getValue() * 12 + semitone
}),
Events.subscribe(iconEdit, "click", () => userEditingManager.audioUnit.edit(sample.box)),
Events.subscribe(header, "pointerdown", (event: PointerEvent) => {
@@ -1,6 +1,6 @@
import css from "./EmptySlot.sass?inline"
import {Html} from "@opendaw/lib-dom"
import {int, Lifecycle, ObservableValue, Option} from "@opendaw/lib-std"
import {int, Lifecycle, ObservableValue} from "@opendaw/lib-std"
import {createElement} from "@opendaw/lib-jsx"
import {StudioService} from "@/service/StudioService.ts"
import {SampleSelector} from "@/ui/devices/SampleSelector"
@@ -30,7 +30,7 @@ export const EmptySlot = (
</div>
)
const element: HTMLElement = (
<div className={className}>
<div className={className} data-slot-index={octave.getValue() * 12 + semitone}>
<header/>
{browseButton}
<footer>
@@ -39,12 +39,14 @@ export const EmptySlot = (
</div>
)
lifecycle.ownAll(
SlotDragAndDrop.install({
octave.catchupAndSubscribe(owner => {
const slotIndex = owner.getValue() * 12 + semitone
element.setAttribute("data-slot-index", String(slotIndex))
}),
SlotDragAndDrop.installTarget({
element,
project,
sample: Option.None,
octave,
semitone
getSlotIndex: () => octave.getValue() * 12 + semitone
}),
sampleSelector.configureDrop(element),
sampleSelector.configureBrowseClick(browseButton),
@@ -1,84 +1,99 @@
import {asInstanceOf, int, ObservableValue, Option, Terminable, UUID} from "@opendaw/lib-std"
import {DragAndDrop} from "@/ui/DragAndDrop"
import {AnyDragData} from "@/ui/AnyDragData"
import {PlayfieldSampleBox} from "@opendaw/studio-boxes"
import {Keyboard} from "@opendaw/lib-dom"
import {PlayfieldSampleBoxAdapter} from "@opendaw/studio-adapters"
import {byte, int, Option, Terminable, UUID} from "@opendaw/lib-std"
import {AudioFileBox, PlayfieldDeviceBox, PlayfieldSampleBox} from "@opendaw/studio-boxes"
import {Project} from "@opendaw/studio-core"
import {DragAndDrop} from "@/ui/DragAndDrop"
import {AnyDragData, DragDevice} from "@/ui/AnyDragData"
import {PlayfieldSampleBoxAdapter} from "@opendaw/studio-adapters"
export namespace SlotDragAndDrop {
type Construct = {
element: HTMLElement
project: Project
sample: Option<PlayfieldSampleBoxAdapter>
octave: ObservableValue<int>
semitone: int
const findSampleByIndex = (project: Project, index: int): Option<PlayfieldSampleBox> => {
for (const box of project.boxGraph.boxes()) {
if (box instanceof PlayfieldSampleBox && box.index.getValue() === index) {
return Option.wrap(box)
}
}
return Option.None
}
export const install = ({element, project, sample, octave, semitone}: Construct): Terminable => {
const uuid = sample.mapOr(({address}) => address.toString(), "")
return Terminable.many(
DragAndDrop.installSource(element, () => ({
type: "playfield-slot",
index: octave.getValue() * 12 + semitone,
uuid
})),
DragAndDrop.installTarget(element, {
drag: (_event: DragEvent, dragData: AnyDragData): boolean => {
if (dragData.type !== "playfield-slot") {return false}
return uuid !== dragData.uuid
},
drop: (event: DragEvent, dragData: AnyDragData): void => {
if (dragData.type !== "playfield-slot") {return}
if (uuid === dragData.uuid) {return}
const {editing, boxGraph, boxAdapters} = project
const copyMode = Keyboard.isCopyKey(event)
const resolveBox = (uuid: string): Option<PlayfieldSampleBox> => uuid === ""
? Option.None
: boxGraph
.findBox(UUID.parse(uuid))
.assert(() => `Could not find box for ${uuid}`)
.map(box => asInstanceOf(box, PlayfieldSampleBox))
const target = resolveBox(uuid)
const source = resolveBox(dragData.uuid)
const newIndex = octave.getValue() * 12 + semitone
if (target.isEmpty()) {
if (source.nonEmpty()) {
if (copyMode) {
editing.modify(() => boxAdapters.adapterFor(source.unwrap(), PlayfieldSampleBoxAdapter)
.copyToIndex(newIndex))
} else {
editing.modify(() => source.unwrap().index.setValue(newIndex))
}
} else {
// else: move or copy empty slot to empty slot has no effect
}
} else {
if (source.isEmpty()) {
if (copyMode) {
editing.modify(() => target.unwrap().delete())
} else {
editing.modify(() => target.unwrap().index.setValue(dragData.index))
}
} else {
editing.modify(() => {
if (copyMode) {
target.unwrap().delete()
boxAdapters.adapterFor(source.unwrap(), PlayfieldSampleBoxAdapter)
.copyToIndex(newIndex)
} else {
source.unwrap().index.setValue(newIndex)
target.unwrap().index.setValue(dragData.index)
}
})
}
}
},
enter: (allowDrop: boolean): void => {
if (allowDrop) {element.classList.add("swap")}
},
leave: (): void => element.classList.remove("swap")
const executeCopy = (project: Project, sourceIndex: int, targetIndex: int): void => {
if (sourceIndex === targetIndex) {return}
const {editing, boxGraph} = project
const source = findSampleByIndex(project, sourceIndex)
const target = findSampleByIndex(project, targetIndex)
source.ifSome(sourceBox => {
editing.modify(() => {
// If target has a sample, delete it first
target.ifSome(targetBox => targetBox.delete())
// Get the source file (AudioFileBox)
const sourceFile = sourceBox.file.targetVertex
.map(vertex => vertex.box instanceof AudioFileBox ? vertex.box : null)
// Get the device box and its samples hub
const deviceBox = sourceBox.device.targetVertex
.map(vertex => vertex.box instanceof PlayfieldDeviceBox ? vertex.box : null)
if (sourceFile.nonEmpty() && deviceBox.nonEmpty()) {
PlayfieldSampleBox.create(boxGraph, UUID.generate(), box => {
box.file.refer(sourceFile.unwrap())
box.device.refer(deviceBox.unwrap().samples)
box.index.setValue(targetIndex)
})
}
})
)
})
}
}
const executeSwap = (project: Project, sourceIndex: int, targetIndex: int): void => {
if (sourceIndex === targetIndex) {return}
const {editing} = project
const source = findSampleByIndex(project, sourceIndex)
const target = findSampleByIndex(project, targetIndex)
editing.modify(() => {
if (source.nonEmpty() && target.isEmpty()) {
source.unwrap().index.setValue(targetIndex)
} else if (source.isEmpty() && target.nonEmpty()) {
target.unwrap().index.setValue(sourceIndex)
} else if (source.nonEmpty() && target.nonEmpty()) {
source.unwrap().index.setValue(targetIndex)
target.unwrap().index.setValue(sourceIndex)
}
})
}
type SourceConstruct = {
element: HTMLElement
sample: PlayfieldSampleBoxAdapter
getSlotIndex: () => int
}
export const installSource = ({element, sample, getSlotIndex}: SourceConstruct): Terminable => {
return DragAndDrop.installSource(element, () => ({
type: "playfield-slot",
index: getSlotIndex() as byte,
uuid: sample.address.uuid.toString()
} satisfies DragDevice))
}
type TargetConstruct = {
element: HTMLElement
project: Project
getSlotIndex: () => int
}
export const installTarget = ({element, project, getSlotIndex}: TargetConstruct): Terminable => {
return DragAndDrop.installTarget(element, {
drag: (_event: DragEvent, data: AnyDragData): boolean => {
return data.type === "playfield-slot" && data.index !== getSlotIndex()
},
drop: (event: DragEvent, data: AnyDragData): void => {
if (data.type !== "playfield-slot") {return}
const targetIndex = getSlotIndex()
if (event.altKey) {
executeCopy(project, data.index, targetIndex)
} else {
executeSwap(project, data.index, targetIndex)
}
},
enter: (allowDrop: boolean) => element.classList.toggle("drop-target", allowDrop),
leave: () => element.classList.remove("drop-target")
})
}
}
@@ -101,7 +101,7 @@ export class NoteMoveModifier implements NoteModifier {
selectedModifyStrategy(): NoteModifyStrategy {return this.#selectedModifyStrategy}
unselectedModifyStrategy(): NoteModifyStrategy {return NoteModifyStrategy.Identity}
update({clientX, clientY, altKey, shiftKey}: Dragging.Event): void {
update({clientX, clientY, ctrlKey, shiftKey}: Dragging.Event): void {
const clientRect = this.#element.getBoundingClientRect()
const deltaPitch: int = this.#positioner
.yToPitch(clientY - clientRect.top) - this.#pointerPitch
@@ -117,8 +117,8 @@ export class NoteMoveModifier implements NoteModifier {
this.#pitchChanged.notify(this.#reference.pitch + deltaPitch)
change = true
}
if (this.#copy !== altKey) {
this.#copy = altKey
if (this.#copy !== ctrlKey) {
this.#copy = ctrlKey
change = true
}
if (change) {this.#dispatchChange()}
@@ -38,7 +38,7 @@ import {NoteEventBox} from "@opendaw/studio-boxes"
import {NoteCreateModifier} from "@/ui/timeline/editors/notes/NoteCreateModifier.ts"
import {CanvasPainter} from "@/ui/canvas/painter.ts"
import {NoteEventOwnerReader} from "@/ui/timeline/editors/EventOwnerReader.ts"
import {CssUtils, Dragging, Events, Html, Keyboard, ShortcutManager} from "@opendaw/lib-dom"
import {CssUtils, Dragging, Events, Html, ShortcutManager} from "@opendaw/lib-dom"
import {PPQN, ppqn} from "@opendaw/lib-dsp"
import {Surface} from "@/ui/surface/Surface"
import {NoteEditorShortcuts} from "@/ui/shortcuts/NoteEditorShortcuts"
@@ -116,7 +116,7 @@ export const PitchEditor = ({
}))
}, {permanentUpdates: true}),
Dragging.attach(canvas, event => {
if (!Keyboard.isControlKey(event)) {return Option.None}
if (!event.altKey) {return Option.None}
const target = capturing.captureEvent(event)
if (target !== null) {return Option.None}
const clientRect = canvas.getBoundingClientRect()
@@ -281,7 +281,7 @@ export const PitchEditor = ({
modifyContext.subscribeUpdate(renderer.requestUpdate),
Events.subscribe(canvas, "pointermove", event => {
canvas.focus({preventScroll: true})
if (Keyboard.isControlKey(event) && event.buttons === 0) {
if (event.altKey && event.buttons === 0) {
updatePreview()
}
}),
@@ -291,7 +291,7 @@ export const PitchEditor = ({
}),
installCursor(canvas, capturing, {
get: (target, event) =>
target === null ? Keyboard.isControlKey(event) && event.buttons === 0
target === null ? event.altKey && event.buttons === 0
? Cursor.Pencil
: null : CursorMap[target.type]
}),
@@ -300,7 +300,7 @@ export const PitchEditor = ({
renderer.requestUpdate()
}),
Events.subscribe(canvas, "keydown", event => {
if (Keyboard.isControlKey(event)) {
if (event.altKey) {
updatePreview()
return
}
@@ -34,7 +34,7 @@ import {installValueInput} from "@/ui/timeline/editors/ValueInput.ts"
import {ValueEventOwnerReader} from "@/ui/timeline/editors/EventOwnerReader.ts"
import {installEditorBody} from "../EditorBody"
import {ValueContentDurationModifier} from "./ValueContentDurationModifier"
import {Dragging, Events, Html, Keyboard, ShortcutManager} from "@opendaw/lib-dom"
import {Dragging, Events, Html, ShortcutManager} from "@opendaw/lib-dom"
import {ValueTooltip} from "./ValueTooltip"
import {ValueEventEditing} from "./ValueEventEditing"
import {TimelineRange} from "@opendaw/studio-core"
@@ -107,7 +107,7 @@ export const ValueEditor = ({lifecycle, service, range, snapping, eventMapping,
let lastDownTime = 0
return (event: PointerEvent) => {
const target: Nullable<ValueCaptureTarget> = capturing.captureEvent(event)
const controlKey = Keyboard.isControlKey(event)
const altKey = event.altKey
const now = Date.now()
const dblclck = now - lastDownTime < Events.DOUBLE_DOWN_THRESHOLD
lastDownTime = now
@@ -150,7 +150,7 @@ export const ValueEditor = ({lifecycle, service, range, snapping, eventMapping,
}
}
if (target === null) {
if (controlKey) {
if (altKey) {
return modifyContext.startModifier(ValuePaintModifier.create({
element: canvas,
reader,
@@ -260,7 +260,7 @@ export const ValueEditor = ({lifecycle, service, range, snapping, eventMapping,
installCursor(canvas, capturing, {
get: (target, event) => {
const onCurve = target?.type === "curve" || target?.type === "midpoint"
const controlKey = Keyboard.isControlKey(event) && event.buttons === 0
const controlKey = event.altKey && event.buttons === 0
if (target === null) {
if (controlKey) {return Cursor.Pencil}
} else if (target.type === "event") {
@@ -125,7 +125,7 @@ export class ValueMoveModifier implements ValueModifier {
}
readContentDuration(owner: ValueEventOwnerReader): number {return owner.contentDuration}
update(event: Dragging.Event): void {
const {clientX, clientY, altKey, ctrlKey: freezeMode, shiftKey} = event
const {clientX, clientY, altKey: freezeMode, ctrlKey, shiftKey} = event
const clientRect = this.#element.getBoundingClientRect()
const localX = clientX - clientRect.left
const localY = clientY - clientRect.top
@@ -159,8 +159,8 @@ export class ValueMoveModifier implements ValueModifier {
this.#deltaValue = deltaValue
change = true
}
if (this.#copy !== altKey) {
this.#copy = altKey
if (this.#copy !== ctrlKey) {
this.#copy = ctrlKey
change = true
}
if (this.#snapValue !== snapValue) {
@@ -101,7 +101,7 @@ export class RegionMoveModifier implements RegionModifier {
selectedModifyStrategy(): RegionModifyStrategy {return this.#selectedModifyStrategy}
unselectedModifyStrategy(): RegionModifyStrategy {return this.#unselectedModifyStrategy}
update({clientX, clientY, altKey, shiftKey}: Dragging.Event): void {
update({clientX, clientY, ctrlKey, shiftKey}: Dragging.Event): void {
const adapters = this.#selection.selected().filter(adapter => adapter.trackBoxAdapter.nonEmpty())
if (adapters.length === 0) {return}
const maxIndex = this.#manager.numTracks() - 1
@@ -123,8 +123,8 @@ export class RegionMoveModifier implements RegionModifier {
this.#deltaIndex = deltaIndex
change = true
}
if (this.#copy !== altKey) {
this.#copy = altKey
if (this.#copy !== ctrlKey) {
this.#copy = ctrlKey
change = true
}
if (this.#mirroredCopy !== shiftKey) {
@@ -140,7 +140,7 @@ export const RegionsArea = ({lifecycle, service, manager, scrollModel, scrollCon
} else {
return Option.None
}
} else if (target.type === "region" && Keyboard.isControlKey(event)) {
} else if (target.type === "region" && event.altKey) {
if (!regionSelection.isSelected(target.region)) {
regionSelection.deselectAll()
regionSelection.select(target.region)
@@ -222,13 +222,13 @@ export const RegionsArea = ({lifecycle, service, manager, scrollModel, scrollCon
get: (target: Nullable<RegionCaptureTarget>, event: CursorEvent) => {
const units = snapping.xToUnitRound(event.clientX - element.getBoundingClientRect().left)
markerPosition.setValue(
Keyboard.isControlKey(event) && target !== null && target.type === "region"
event.altKey && target !== null && target.type === "region"
&& target.region.position < units && units < target.region.complete
? units
: null)
return target === null || target.type === "track"
? null
: Keyboard.isControlKey(event)
: event.altKey
? Cursor.Scissors
: CursorMap[target.part]
},
-1
View File
@@ -5,7 +5,6 @@ export namespace Keyboard {
ctrlKey: boolean,
metaKey: boolean
}) => Browser.isMacOS() ? metaKey : ctrlKey
export const isCopyKey = ({altKey}: { altKey: boolean }) => altKey
export const isDelete = (event: KeyboardEvent) => event.code === "Delete" || event.code === "Backspace"
export const isSelectAll = (event: KeyboardEvent) => isControlKey(event) && !event.shiftKey && event.code === "KeyA"
export const isDeselectAll = (event: KeyboardEvent) => isControlKey(event) && event.shiftKey && event.code === "KeyA"