Skip to content

Commit

Permalink
Merge pull request #191 from Cycling74/fde/midi_mapping_view
Browse files Browse the repository at this point in the history
Add Global MIDI Mapping View
  • Loading branch information
fde31 authored Dec 17, 2024
2 parents dd5b1e2 + b854dc4 commit 5bced2d
Show file tree
Hide file tree
Showing 26 changed files with 964 additions and 78 deletions.
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

0 comments on commit 5bced2d

Please sign in to comment.