From 3762abbe356ec6ad3299d7e1aee030a047005d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Michelle?= Date: Fri, 9 Jan 2026 16:22:47 +0100 Subject: [PATCH] changes alt to ctrl for copying --- CLAUDE.md | 5 +- packages/app/studio/src/ui/DragAndDrop.ts | 4 +- .../studio/src/ui/browse/DevicesBrowser.tsx | 3 +- .../PlayfieldDeviceEditor/BusySlot.tsx | 17 +- .../PlayfieldDeviceEditor/EmptySlot.tsx | 14 +- .../PlayfieldDeviceEditor/SlotDragAndDrop.ts | 169 ++++++++++-------- .../editors/notes/NoteMoveModifier.ts | 6 +- .../editors/notes/pitch/PitchEditor.tsx | 10 +- .../ui/timeline/editors/value/ValueEditor.tsx | 8 +- .../editors/value/ValueMoveModifier.ts | 6 +- .../audio-unit/regions/RegionMoveModifier.ts | 6 +- .../tracks/audio-unit/regions/RegionsArea.tsx | 6 +- packages/lib/dom/src/keyboard.ts | 1 - 13 files changed, 139 insertions(+), 116 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 555d7270..cadf4b62 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` instead of `T | undefined` - Use `Nullable` 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. diff --git a/packages/app/studio/src/ui/DragAndDrop.ts b/packages/app/studio/src/ui/DragAndDrop.ts index 40bb3f34..39a91f65 100644 --- a/packages/app/studio/src/ui/DragAndDrop.ts +++ b/packages/app/studio/src/ui/DragAndDrop.ts @@ -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 = 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" } } }) diff --git a/packages/app/studio/src/ui/browse/DevicesBrowser.tsx b/packages/app/studio/src/ui/browse/DevicesBrowser.tsx index d24d664f..0df4cb3a 100644 --- a/packages/app/studio/src/ui/browse/DevicesBrowser.tsx +++ b/packages/app/studio/src/ui/browse/DevicesBrowser.tsx @@ -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() diff --git a/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/BusySlot.tsx b/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/BusySlot.tsx index 4c38b811..8b2ed424 100644 --- a/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/BusySlot.tsx +++ b/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/BusySlot.tsx @@ -85,7 +85,7 @@ export const BusySlot = ({ ) const element: HTMLElement = ( -
+
{header}
{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) => { diff --git a/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/EmptySlot.tsx b/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/EmptySlot.tsx index 41f9552a..a2c545a3 100644 --- a/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/EmptySlot.tsx +++ b/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/EmptySlot.tsx @@ -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 = (
) const element: HTMLElement = ( -
+
{browseButton}
@@ -39,12 +39,14 @@ export const EmptySlot = (
) 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), diff --git a/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/SlotDragAndDrop.ts b/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/SlotDragAndDrop.ts index 3130854d..4f668c32 100644 --- a/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/SlotDragAndDrop.ts +++ b/packages/app/studio/src/ui/devices/instruments/PlayfieldDeviceEditor/SlotDragAndDrop.ts @@ -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 - octave: ObservableValue - semitone: int + const findSampleByIndex = (project: Project, index: int): Option => { + 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 => 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) + }) + } }) - ) + }) } -} \ No newline at end of file + + 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") + }) + } +} diff --git a/packages/app/studio/src/ui/timeline/editors/notes/NoteMoveModifier.ts b/packages/app/studio/src/ui/timeline/editors/notes/NoteMoveModifier.ts index 54aae33d..70dbf274 100644 --- a/packages/app/studio/src/ui/timeline/editors/notes/NoteMoveModifier.ts +++ b/packages/app/studio/src/ui/timeline/editors/notes/NoteMoveModifier.ts @@ -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()} diff --git a/packages/app/studio/src/ui/timeline/editors/notes/pitch/PitchEditor.tsx b/packages/app/studio/src/ui/timeline/editors/notes/pitch/PitchEditor.tsx index dc11ddcf..a5d7abf8 100644 --- a/packages/app/studio/src/ui/timeline/editors/notes/pitch/PitchEditor.tsx +++ b/packages/app/studio/src/ui/timeline/editors/notes/pitch/PitchEditor.tsx @@ -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 } diff --git a/packages/app/studio/src/ui/timeline/editors/value/ValueEditor.tsx b/packages/app/studio/src/ui/timeline/editors/value/ValueEditor.tsx index 7f79194a..f17a502f 100644 --- a/packages/app/studio/src/ui/timeline/editors/value/ValueEditor.tsx +++ b/packages/app/studio/src/ui/timeline/editors/value/ValueEditor.tsx @@ -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 = 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") { diff --git a/packages/app/studio/src/ui/timeline/editors/value/ValueMoveModifier.ts b/packages/app/studio/src/ui/timeline/editors/value/ValueMoveModifier.ts index 93643db4..499b496f 100644 --- a/packages/app/studio/src/ui/timeline/editors/value/ValueMoveModifier.ts +++ b/packages/app/studio/src/ui/timeline/editors/value/ValueMoveModifier.ts @@ -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) { diff --git a/packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionMoveModifier.ts b/packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionMoveModifier.ts index 75954bfb..7ac1716b 100644 --- a/packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionMoveModifier.ts +++ b/packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionMoveModifier.ts @@ -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) { diff --git a/packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionsArea.tsx b/packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionsArea.tsx index 0dec6bb5..d02c263a 100644 --- a/packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionsArea.tsx +++ b/packages/app/studio/src/ui/timeline/tracks/audio-unit/regions/RegionsArea.tsx @@ -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, 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] }, diff --git a/packages/lib/dom/src/keyboard.ts b/packages/lib/dom/src/keyboard.ts index 4af11395..34ecc9d8 100644 --- a/packages/lib/dom/src/keyboard.ts +++ b/packages/lib/dom/src/keyboard.ts @@ -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"