Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Global MIDI Mapping View #191

Merged
merged 17 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions src/actions/patchers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Router from "next/router";
import { ActionBase, AppThunk } from "../lib/store";
import { OSCQueryRNBOInstance, OSCQueryRNBOInstancePresetEntries, OSCQueryRNBOPatchersState, OSCValue } from "../lib/types";
import { MIDIMetaMapping, OSCQueryRNBOInstance, OSCQueryRNBOInstancePresetEntries, OSCQueryRNBOPatchersState, OSCValue, ParameterMetaJsonMap } from "../lib/types";
import { PatcherInstanceRecord } from "../models/instance";
import { getPatcherInstanceByIndex, getPatcherInstance, getPatcherInstanceParametersByInstanceIndex, getPatcherInstanceParameter, getPatcherInstanceMessageInportsByInstanceIndex, getPatcherInstanceMesssageOutportsByInstanceIndex, getPatcherInstanceMessageInportByPath, getPatcherInstanceMessageOutportByPath, getPatcherInstanceMesssageOutportsByInstanceIndexAndTag, getPatcherInstanceParameterByPath, getPatcherInstanceParametersByInstanceIndexAndName, getPatcherInstanceMessageInportsByInstanceIndexAndTag } from "../selectors/patchers";
import { getAppSetting } from "../selectors/settings";
Expand All @@ -16,6 +16,8 @@ import { AppSetting } from "../models/settings";
import { DataRefRecord } from "../models/dataref";
import { DataFileRecord } from "../models/datafile";
import { PatcherExportRecord } from "../models/patcher";
import { cloneJSON, InvalidMIDIFormatError, parseMIDIMappingDisplayValue, UnknownMIDIFormatError } from "../lib/util";
import { MIDIMetaMappingType } from "../lib/constants";

export enum PatcherActionType {
INIT_PATCHERS = "INIT_PATCHERS",
Expand Down Expand Up @@ -552,7 +554,7 @@ export const activateParameterMIDIMappingFocus = (instance: PatcherInstanceRecor
));
};

export const clearParameterMidiMappingOnRemote = (id: PatcherInstanceRecord["id"], paramId: ParameterRecord["id"]): AppThunk =>
export const clearParameterMIDIMappingOnRemote = (id: PatcherInstanceRecord["id"], paramId: ParameterRecord["id"]): AppThunk =>
(_dispatch, getState) => {
const state = getState();
const instance = getPatcherInstance(state, id);
Expand All @@ -561,8 +563,9 @@ export const clearParameterMidiMappingOnRemote = (id: PatcherInstanceRecord["id"
const param = getPatcherInstanceParameter(state, paramId);
if (!param) return;

const meta = param.getParsedMetaObject();
const meta = cloneJSON(param.meta);
delete meta.midi;

const message = {
address: `${param.path}/meta`,
args: [
Expand All @@ -573,6 +576,59 @@ export const clearParameterMidiMappingOnRemote = (id: PatcherInstanceRecord["id"
oscQueryBridge.sendPacket(writePacket(message));
};

export const setParameterMIDIMappingOnRemote = (id: PatcherInstanceRecord["id"], paramId: ParameterRecord["id"], type: MIDIMetaMappingType, mapping: MIDIMetaMapping): AppThunk =>
(_dispatch, getState) => {
const state = getState();
const instance = getPatcherInstance(state, id);
if (!instance) return;

const param = getPatcherInstanceParameter(state, paramId);
if (!param) return;

const meta: ParameterMetaJsonMap = cloneJSON(param.meta);
meta.midi = { ...mapping };

const message = {
address: `${param.path}/meta`,
args: [
{ type: "s", value: JSON.stringify(meta) }
]
};

oscQueryBridge.sendPacket(writePacket(message));
};

export const setParameterMIDIMappingOnRemoteFromDisplayValue = (id: PatcherInstanceRecord["id"], paramId: ParameterRecord["id"], value: string): AppThunk =>
(dispatch) => {
try {
const parsed = parseMIDIMappingDisplayValue(value);
dispatch(setParameterMIDIMappingOnRemote(id, paramId, parsed.type, parsed.mapping));
} catch (err: unknown) {
let notification: { level: NotificationLevel; message: string; title: string };
if (err instanceof InvalidMIDIFormatError) {
notification = {
title: err.message,
message: `"${value}" is not a valid MIDI mapping value`,
level: NotificationLevel.error
};
} else if (err instanceof UnknownMIDIFormatError) {
notification = {
title: err.message,
message: `"${value}" is an unknown MIDI mapping format. Please use the parameter meta editor to set this mapping.`,
level: NotificationLevel.warn
};
} else {
notification = {
title: "Unexpected Error",
message: `Encountered an unexpected error while trying to set "${value}" as the MIDI mapping`,
level: NotificationLevel.error
};
console.error(err);
}
return void dispatch(showNotification(notification));
}
};

export const setInstanceMessagePortMetaOnRemote = (_instance: PatcherInstanceRecord, port: MessagePortRecord, value: string): AppThunk =>
() => {
const message = {
Expand Down Expand Up @@ -848,7 +904,7 @@ export const updateInstanceMIDILastValue = (index: number, value: string): AppTh
const parameters: ParameterRecord[] = [];
getPatcherInstanceParametersByInstanceIndex(state, instance.index).forEach(param => {
if (param.waitingForMidiMapping) {
const meta = param.getParsedMetaObject();
const meta = cloneJSON(param.meta);
meta.midi = midiMeta;

const message = {
Expand Down
4 changes: 1 addition & 3 deletions src/components/editor/patcherNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ const EditorPatcherNode: FunctionComponent<EditorNodeProps> = memo(function Wrap
return (
<Paper className={ classes.node } shadow="md" withBorder data-selected={ selected } >
<div className={ classes.nodeHeader } >
<div>
{ (node as GraphPatcherNodeRecord).index }: { (node as GraphPatcherNodeRecord).patcher }
</div>
<div>{ (node as GraphPatcherNodeRecord).displayName }</div>
<div>
<Tooltip label="Open Patcher Instance Control">
<ActionIcon
Expand Down
154 changes: 154 additions & 0 deletions src/components/elements/editableTableCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { NumberInput, Table, TextInput } from "@mantine/core";
import { ChangeEvent, FC, KeyboardEvent, memo, useCallback, useEffect, useState } from "react";
import classes from "./elements.module.css";

export type EditableTableNumberCellProps = {
className?: string;
min: number;
max: number;
name: string;
onUpdate: (val: number) => void;
prefix?: string;
value: number;
};

export const EditableTableNumberCell: FC<EditableTableNumberCellProps> = memo(function WrappedEditableNumberField({
className = "",
min,
max,
name,
onUpdate,
prefix,
value
}) {
const [isEditing, setIsEditing] = useState<boolean>(false);
const [currentValue, setCurrentValue] = useState<number>(value);

const onTriggerEdit = useCallback(() => {
if (isEditing) return;
setIsEditing(true);
setCurrentValue(value);
}, [isEditing, setIsEditing, setCurrentValue, value]);

const onChange = useCallback((val: number) => {
setCurrentValue(val);
}, [setCurrentValue]);

const onBlur = useCallback(() => {
setIsEditing(false);
if (currentValue === value) return;
onUpdate(currentValue);
}, [setIsEditing, value, currentValue, onUpdate]);

const onKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === "Escape") {
setIsEditing(false);
setCurrentValue(value);
return void e.preventDefault();
} else if (e.key === "Enter") {
setIsEditing(false);
if (currentValue === value) return;
onUpdate(currentValue);
return void e.preventDefault();
}
}, [setIsEditing, setCurrentValue, value, currentValue, onUpdate]);

useEffect(() => {
setCurrentValue(value);
}, [value, setCurrentValue]);

return (
<Table.Td className={ className } onClick={ onTriggerEdit } py={ 0 } >
{
isEditing ? (
<NumberInput
autoFocus
className={ classes.editableTableCellInput }
variant="unstyled"
onBlur={ onBlur }
onChange={ onChange }
onKeyDown={ onKeyDown }
name={ name }
min={ min }
max={ max }
prefix={ prefix }
size="xs"
value={ currentValue }
/>
) : `${prefix || ""}${value}`
}

</Table.Td>
);
});

export type EditableTableTextCellProps = {
className?: string;
name: string;
onUpdate: (val: string) => void;
value: string;
};

export const EditableTableTextCell: FC<EditableTableTextCellProps> = memo(function WrappedEditableTextField({
className = "",
name,
onUpdate,
value
}) {
const [isEditing, setIsEditing] = useState<boolean>(false);
const [currentValue, setCurrentValue] = useState<string>(value);

const onTriggerEdit = useCallback(() => {
if (isEditing) return;
setIsEditing(true);
setCurrentValue(value);
}, [isEditing, setIsEditing, setCurrentValue, value]);

const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setCurrentValue(e.target.value);
}, [setCurrentValue]);

const onBlur = useCallback(() => {
setIsEditing(false);
if (currentValue === value) return;
onUpdate(currentValue);
}, [setIsEditing, value, currentValue, onUpdate]);

const onKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === "Escape") {
setIsEditing(false);
setCurrentValue(value);
return void e.preventDefault();
} else if (e.key === "Enter") {
setIsEditing(false);
if (currentValue === value) return;
onUpdate(currentValue);
return void e.preventDefault();
}
}, [setIsEditing, setCurrentValue, value, currentValue, onUpdate]);

useEffect(() => {
setCurrentValue(value);
}, [value, setCurrentValue]);

return (
<Table.Td className={ className } onClick={ onTriggerEdit } py={ 0 } >
{
isEditing ? (
<TextInput
autoFocus
className={ classes.editableTableCellInput }
variant="unstyled"
onBlur={ onBlur }
onChange={ onChange }
onKeyDown={ onKeyDown }
name={ name }
size="xs"
value={ currentValue }
/>
) : value
}

</Table.Td>
);
});
4 changes: 4 additions & 0 deletions src/components/elements/elements.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.icon {
width: 1.3em;
}

.editableTableCellInput {
border-bottom: 1px solid var(--mantine-primary-color-filled);
}
57 changes: 57 additions & 0 deletions src/components/elements/tableHeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Group, MantineFontSize, Table, Text, UnstyledButton } from "@mantine/core";
import { FC, PropsWithChildren, useCallback } from "react";
import { SortOrder } from "../../lib/constants";
import { IconElement } from "./icon";
import { mdiChevronDown, mdiChevronUp, mdiUnfoldMoreHorizontal } from "@mdi/js";

export type TableHeaderCellProps = PropsWithChildren<{
className?: string;
fz?: MantineFontSize;

onSort?: (sortKey: string) => void;
sorted?: boolean;
sortKey?: string;
sortOrder?: SortOrder;
}>;

export const TableHeaderCell: FC<TableHeaderCellProps> = ({
children,
className,
fz = "sm",

onSort,
sorted = false,
sortKey,
sortOrder = SortOrder.Asc

}) => {

const onTriggerSort = useCallback(() => {
onSort?.(sortKey);
}, [onSort, sortKey]);

return (
<Table.Th className={ className } >
{
onSort ? (
<UnstyledButton onClick={ onTriggerSort } >
<Group justify="space-between">
<Text fw="bold" fz={ fz } >
{ children }
</Text>
{
sorted
? <IconElement size={ 0.7 } path={ sortOrder === SortOrder.Asc ? mdiChevronDown : mdiChevronUp } />
: <IconElement size={ 0.5 } path={ mdiUnfoldMoreHorizontal } color={ "gray.6" } />
}
</Group>
</UnstyledButton>
) : (
<Text fw="bold" fz={ fz } >
{ children }
</Text>
)
}
</Table.Th>
);
};
4 changes: 2 additions & 2 deletions src/components/instance/paramTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { PatcherInstanceRecord } from "../../models/instance";
import {
restoreDefaultParameterMetaOnRemote, setInstanceParameterMetaOnRemote,
setInstanceParameterValueNormalizedOnRemote,
setInstanceWaitingForMidiMappingOnRemote, clearParameterMidiMappingOnRemote,
setInstanceWaitingForMidiMappingOnRemote, clearParameterMIDIMappingOnRemote,
activateParameterMIDIMappingFocus
} from "../../actions/patchers";
import { OrderedSet as ImmuOrderedSet, Map as ImmuMap } from "immutable";
Expand Down Expand Up @@ -159,7 +159,7 @@ const InstanceParameterTab: FunctionComponent<InstanceParameterTabProps> = memo(
}, [dispatch, instance]);

const onClearParameterMidiMapping = useCallback((param: ParameterRecord) => {
dispatch(clearParameterMidiMappingOnRemote(instance.id, param.id));
dispatch(clearParameterMIDIMappingOnRemote(instance.id, param.id));
}, [dispatch, instance]);

const onSearch = useDebouncedCallback((query: string) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/messages/inport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const MessageInportEntry: FunctionComponent<MessageInportEntryProps> = memo(func
onClose={ closeMetaEditor }
onRestore={ onRestoreMeta }
onSaveMeta={ onSaveMeta }
meta={ port.meta }
meta={ port.metaString }
name={ port.name }
scope={ MetadataScope.Inport }
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/messages/outport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const MessageOutportEntry: FunctionComponent<MessageOutportEntryProps> = memo(fu
onClose={ closeMetaEditor }
onRestore={ onRestoreMeta }
onSaveMeta={ onSaveMeta }
meta={ port.meta }
meta={ port.metaString }
name={ port.name }
scope={ MetadataScope.Outport }
/>
Expand Down
Loading
Loading