diff --git a/src/actions/sets.ts b/src/actions/sets.ts index 050ba6b..9d46758 100644 --- a/src/actions/sets.ts +++ b/src/actions/sets.ts @@ -1,4 +1,4 @@ -import { writePacket } from "osc"; +import { OSCArgument, writePacket } from "osc"; import { oscQueryBridge } from "../controller/oscqueryBridgeController"; import { ActionBase, AppThunk } from "../lib/store"; import { GraphSetRecord, GraphSetViewRecord } from "../models/set"; @@ -469,14 +469,18 @@ export const updateSetViewParameterListOnRemote = (setView: GraphSetViewRecord, } }; -export const removeParameterFromSetView = (setView: GraphSetViewRecord, param: ParameterRecord): AppThunk => +export const decreaseParameterIndexInSetView = (setView: GraphSetViewRecord, param: ParameterRecord): AppThunk => (dispatch) => { try { - const params = setView.params.toArray().filter(entry => param.instanceIndex !== entry.instanceIndex || param.index !== entry.paramIndex); + const currentIndex = setView.params.findIndex(entry => entry.instanceIndex === param.instanceIndex && entry.paramIndex === param.index); + if (currentIndex <= 0) return; + const newList = setView.params + .delete(currentIndex) + .insert(currentIndex - 1, { instanceIndex: param.instanceIndex, paramIndex: param.index }); const message = { address: `/rnbo/inst/control/sets/views/list/${setView.id}/params`, - args: params.map(p => ({ type: "s", value: instanceAndParamIndicesToSetViewEntry(p.instanceIndex, p.paramIndex) })) + args: newList.toArray().map(p => ({ type: "s", value: instanceAndParamIndicesToSetViewEntry(p.instanceIndex, p.paramIndex) })) }; oscQueryBridge.sendPacket(writePacket(message)); } catch (err) { @@ -489,15 +493,16 @@ export const removeParameterFromSetView = (setView: GraphSetViewRecord, param: P } }; -export const decreaseParameterIndexInSetView = (setView: GraphSetViewRecord, param: ParameterRecord): AppThunk => +export const increaseParameterIndexInSetView = (setView: GraphSetViewRecord, param: ParameterRecord): AppThunk => (dispatch) => { try { const currentIndex = setView.params.findIndex(entry => entry.instanceIndex === param.instanceIndex && entry.paramIndex === param.index); - if (currentIndex <= 0) return; + if (currentIndex >= setView.params.size) return; const newList = setView.params .delete(currentIndex) - .insert(currentIndex - 1, { instanceIndex: param.instanceIndex, paramIndex: param.index }); + .insert(currentIndex + 1, { instanceIndex: param.instanceIndex, paramIndex: param.index }); + const message = { address: `/rnbo/inst/control/sets/views/list/${setView.id}/params`, args: newList.toArray().map(p => ({ type: "s", value: instanceAndParamIndicesToSetViewEntry(p.instanceIndex, p.paramIndex) })) @@ -513,19 +518,92 @@ export const decreaseParameterIndexInSetView = (setView: GraphSetViewRecord, par } }; -export const increaseParameterIndexInSetView = (setView: GraphSetViewRecord, param: ParameterRecord): AppThunk => +export const removeParameterFromSetView = (setView: GraphSetViewRecord, param: ParameterRecord): AppThunk => (dispatch) => { try { - const currentIndex = setView.params.findIndex(entry => entry.instanceIndex === param.instanceIndex && entry.paramIndex === param.index); - if (currentIndex >= setView.params.size) return; + if (!setView.paramIds.has(param.setViewId)) return; + const params = setView.paramIds.remove(param.setViewId).toArray().map(pId => ({ type: "s", value: pId })); - const newList = setView.params - .delete(currentIndex) - .insert(currentIndex + 1, { instanceIndex: param.instanceIndex, paramIndex: param.index }) + const message = { + address: `/rnbo/inst/control/sets/views/list/${setView.id}/params`, + args: params + }; + oscQueryBridge.sendPacket(writePacket(message)); + } catch (err) { + dispatch(showNotification({ + level: NotificationLevel.error, + title: `Error while trying to update parameter list of SetView "${setView.name}"`, + message: "Please check the console for further details." + })); + console.log(err); + } + }; +export const removeAllParamtersFromSetView = (setView: GraphSetViewRecord): AppThunk => + (dispatch) => { + try { const message = { address: `/rnbo/inst/control/sets/views/list/${setView.id}/params`, - args: newList.toArray().map(p => ({ type: "s", value: instanceAndParamIndicesToSetViewEntry(p.instanceIndex, p.paramIndex) })) + args: [] as OSCArgument[] + }; + oscQueryBridge.sendPacket(writePacket(message)); + } catch (err) { + dispatch(showNotification({ + level: NotificationLevel.error, + title: `Error while trying to update parameter list of SetView "${setView.name}"`, + message: "Please check the console for further details." + })); + console.log(err); + } + }; + +export const addParameterToSetView = (setView: GraphSetViewRecord, param: ParameterRecord): AppThunk => + (dispatch) => { + try { + if (setView.paramIds.has(param.setViewId)) return; + const params = setView.paramIds.toArray().map(pId => ({ type: "s", value: pId })); + params.push({ type: "s", value: instanceAndParamIndicesToSetViewEntry(param.instanceIndex, param.index) }); + + const message = { + address: `/rnbo/inst/control/sets/views/list/${setView.id}/params`, + args: params + }; + oscQueryBridge.sendPacket(writePacket(message)); + } catch (err) { + dispatch(showNotification({ + level: NotificationLevel.error, + title: `Error while trying to update parameter list of SetView "${setView.name}"`, + message: "Please check the console for further details." + })); + console.log(err); + } + }; + +export const addAllParamtersToSetView = (setView: GraphSetViewRecord): AppThunk => + (dispatch, getState) => { + try { + const state = getState(); + const params = setView.params.withMutations(list => { + getPatcherInstanceParameters(state) + .valueSeq() + .sort((a, b) => { + if (a.instanceIndex < b.instanceIndex) return -1; + if (a.instanceIndex > b.instanceIndex) return 1; + if (a.index < b.index) return -1; + if (a.index > b.index) return 1; + return 0; + }) + .forEach(param => { + if (!setView.paramIds.has(param.setViewId)) { + list.push({ instanceIndex: param.instanceIndex, paramIndex: param.index }); + } + }); + }).toArray(); + + + const message = { + address: `/rnbo/inst/control/sets/views/list/${setView.id}/params`, + args: params.map(p => ({ type: "s", value: instanceAndParamIndicesToSetViewEntry(p.instanceIndex, p.paramIndex) })) }; oscQueryBridge.sendPacket(writePacket(message)); } catch (err) { diff --git a/src/components/instance/paramTab.tsx b/src/components/instance/paramTab.tsx index c8e585a..8eaf9c3 100644 --- a/src/components/instance/paramTab.tsx +++ b/src/components/instance/paramTab.tsx @@ -165,7 +165,7 @@ const InstanceParameterTab: FunctionComponent = memo( const onClearParameterMidiMapping = useCallback((param: ParameterRecord) => { dispatch(clearParameterMIDIMappingOnRemote(param)); - }, [dispatch, instance]); + }, [dispatch]); const onSearch = useDebouncedCallback((query: string) => { setSearchValue(query); diff --git a/src/components/midi/mappedParameterItem.tsx b/src/components/midi/mappedParameterItem.tsx index 573605c..8366fa5 100644 --- a/src/components/midi/mappedParameterItem.tsx +++ b/src/components/midi/mappedParameterItem.tsx @@ -70,7 +70,7 @@ const MIDIMappedParameter: FC = memo(function WrappedMIDIM const onUpdateMapping = useCallback((value: string) => { onUpdateMIDIMapping(param, value); - }, [instance, param, onUpdateMIDIMapping]); + }, [param, onUpdateMIDIMapping]); return ( diff --git a/src/components/nav/index.tsx b/src/components/nav/index.tsx index 1e1728b..a5134da 100644 --- a/src/components/nav/index.tsx +++ b/src/components/nav/index.tsx @@ -9,7 +9,7 @@ import { getShowSettingsModal } from "../../selectors/settings"; import { ExternalNavLink, NavLink } from "./link"; import { useRouter } from "next/router"; import { getFirstPatcherNodeIndex } from "../../selectors/graph"; -import { mdiChartSankeyVariant, mdiCog, mdiFileMusic, mdiHelpCircle, mdiKnob, mdiMidiPort, mdiVectorSquare } from "@mdi/js"; +import { mdiChartSankeyVariant, mdiCog, mdiFileMusic, mdiHelpCircle, mdiMidiPort, mdiVectorSquare, mdiTableEye } from "@mdi/js"; const AppNav: FunctionComponent = memo(function WrappedNav() { @@ -52,7 +52,7 @@ const AppNav: FunctionComponent = memo(function WrappedNav() { isActive={ pathname === "/files" } /> = memo(function WrappedParameter({ disabled = false, param, - onSetNormalizedValue, + onSetNormalizedValue }: ParameterItemProps) { const [localValue, setLocalValue] = useState(param.normalizedValue); diff --git a/src/components/parameter/parameters.module.css b/src/components/parameter/parameters.module.css index 1def4a9..f9b658a 100644 --- a/src/components/parameter/parameters.module.css +++ b/src/components/parameter/parameters.module.css @@ -1,45 +1,59 @@ .parameterWrap { - background-color: var(--parameter-bg-color); break-inside: avoid-column; + flex: 1; + outline-style: solid; + outline-width: 3px; page-break-inside: avoid; padding: 2px 2px var(--mantine-spacing-xl) 2px; - margin-bottom: 6px; + margin-bottom: 6; } .paramWithMIDIMapping { + background-color: var(--parameter-bg-color); outline-color: transparent; - outline-style: solid; - outline-width: 4px; + + &[data-instance-mapping="true"] { + cursor: pointer; + background-color: var(--parameter-mapping-bg-color); + + &:hover { + background-color: var(--parameter-active-mapping-bg-color); + } + + > * { + pointer-events: none; + } + } &[data-active-mappping="true"] { - outline-color:var(--parameter-active-midi-outline); + background-color: var(--parameter-active-mapping-bg-color); + outline-color: var(--parameter-active-mapping-outline); } } -.paramWithActiveInstanceMapping { - cursor: pointer; +.paramWithSetViewWrap { - .parameterItem { - background-color: var(--parameter-active-midi-bg-color); - pointer-events: none; - } + background-color: var(--parameter-bg-color); + outline-color: transparent; } .parameterList { - gap: var(--mantine-spacing-xl); + column-gap: var(--mantine-spacing-xl); min-height: 100%; &[data-color-scheme="light"] { --parameter-bg-color: transparent; - --parameter-active-midi-bg-color: var(--mantine-color-violet-1); - --parameter-active-midi-outline: var(--mantine-color-violet-4); + --parameter-mapping-bg-color: var(--mantine-color-violet-1); + --parameter-active-mapping-bg-color: var(--mantine-color-violet-2); + --parameter-active-mapping-outline: var(--mantine-color-violet-5); } &[data-color-scheme="dark"] { --parameter-bg-color: transparent; - --parameter-active-midi-bg-color: var(--mantine-color-violet-9); - --parameter-active-midi-outline: var(--mantine-color-violet-3); + --parameter-mapping-bg-color: var(--mantine-color-violet-9); + --parameter-active-mapping-bg-color: var(--mantine-color-violet-4); + --parameter-active-mapping-outline: var(--mantine-color-violet-2); } } diff --git a/src/components/parameter/withMidiMapping.tsx b/src/components/parameter/withMidiMapping.tsx index 07856b7..235ab5b 100644 --- a/src/components/parameter/withMidiMapping.tsx +++ b/src/components/parameter/withMidiMapping.tsx @@ -65,7 +65,8 @@ export function withParameterMIDIMapping( return (
@@ -98,14 +99,13 @@ export function withParameterMIDIMapping( - - + + - Parameter Actions } onClick={ toggleMetaEditor }> Edit Metadata diff --git a/src/components/parameter/withSetViewWrap.tsx b/src/components/parameter/withSetViewWrap.tsx index 82c7d8e..ade7aed 100644 --- a/src/components/parameter/withSetViewWrap.tsx +++ b/src/components/parameter/withSetViewWrap.tsx @@ -5,7 +5,7 @@ import classes from "./parameters.module.css"; import { ActionIcon, Group, Indicator, Menu, Tooltip } from "@mantine/core"; import { formatMIDIMappingToDisplay } from "../../lib/util"; import { MIDIMetaMappingType } from "../../lib/constants"; -import { mdiArrowDown, mdiArrowUp, mdiDotsVertical, mdiMinus } from "@mdi/js"; +import { mdiArrowDown, mdiArrowUp, mdiDotsVertical, mdiMinusBox } from "@mdi/js"; import { IconElement } from "../elements/icon"; export type ParameterSetViewWrapProps = { @@ -48,9 +48,7 @@ export function withParameterSetViewWrap( : undefined; return ( -
+
- - + + - SetView Parameter Actions - } onClick={ onTriggerMoveUp } disabled={ index === 0 } > + } onClick={ onTriggerMoveUp } disabled={ index === 0 } > Move Up - } onClick={ onTriggerMoveDown } disabled={ index === listSize - 1 } > + } onClick={ onTriggerMoveDown } disabled={ index === listSize - 1} > Move Down - } onClick={ onTriggerRemoveFromSetView } > + } onClick={ onTriggerRemoveFromSetView } > Remove from SetView diff --git a/src/components/setViews/drawer.tsx b/src/components/setViews/drawer.tsx index e384b17..c162202 100644 --- a/src/components/setViews/drawer.tsx +++ b/src/components/setViews/drawer.tsx @@ -2,7 +2,7 @@ import { Map as ImmuMap }from "immutable"; import { Divider, Drawer, Group, Stack, Text } from "@mantine/core"; import { FC, useCallback } from "react"; import { IconElement } from "../elements/icon"; -import { mdiKnob } from "@mdi/js"; +import { mdiTableEye } from "@mdi/js"; import { DrawerSectionTitle } from "../page/drawer"; import { CreateSetViewForm } from "./create"; import { GraphSetViewRecord } from "../../models/set"; @@ -55,7 +55,7 @@ const SetViewDrawer: FC = ({ opened={ open } onClose={ onClose } position="right" - title={ SetViews } + title={ SetViews } > diff --git a/src/components/setViews/paramModal.tsx b/src/components/setViews/paramModal.tsx new file mode 100644 index 0000000..e636869 --- /dev/null +++ b/src/components/setViews/paramModal.tsx @@ -0,0 +1,193 @@ +import { Accordion, ActionIcon, Group, Modal, Stack, Text, Title } from "@mantine/core"; +import { FC, memo, useCallback } from "react"; +import { useIsMobileDevice } from "../../hooks/useIsMobileDevice"; +import { mdiMinusBox, mdiMinusBoxMultiple, mdiPlusBox, mdiPlusBoxMultiple, mdiTune } from "@mdi/js"; +import { IconElement } from "../elements/icon"; +import { GraphSetViewRecord } from "../../models/set"; +import { useAppDispatch, useAppSelector } from "../../hooks/useAppDispatch"; +import { RootStateType } from "../../lib/store"; +import { getPatcherInstancesAndParameters } from "../../selectors/patchers"; +import { PatcherInstanceRecord } from "../../models/instance"; +import { OrderedSet as ImmuOrderedSet, Seq } from "immutable"; +import { ParameterRecord } from "../../models/parameter"; +import { ResponsiveButton } from "../elements/responsiveButton"; +import { addAllParamtersToSetView, addParameterToSetView, removeAllParamtersFromSetView, removeParameterFromSetView } from "../../actions/sets"; +import classes from "./setviews.module.css"; +import { modals } from "@mantine/modals"; + +export type SetViewParameterModalProps = { + onClose: () => void; + setView: GraphSetViewRecord; +}; + +type ParamEntryProps = { + isInSet: boolean; + onAdd: (param: ParameterRecord) => void; + onRemove: (param: ParameterRecord) => void; + parameter: ParameterRecord; +}; + +const ParameterEntry: FC = memo(function WrappedParameterEntry({ + isInSet, + onAdd, + onRemove, + parameter +}) { + + const onTriggerAdd = () => onAdd(parameter); + const onTriggerRemove = () => onRemove(parameter); + + return ( +
  • + +
    + { parameter.name } +
    + + + + + + + + +
    +
  • + ); +}); + +type InstanceEntryProps = { + instance: PatcherInstanceRecord; + onAddParameter: ParamEntryProps["onAdd"]; + onRemoveParameter: ParamEntryProps["onRemove"]; + parameters: Seq.Indexed; + setViewParamIds: ImmuOrderedSet; +}; + +const InstanceEntry: FC = memo(function WrapedInstanceEntry({ + instance, + onAddParameter, + onRemoveParameter, + parameters, + setViewParamIds +}) { + return ( + + + { instance.displayName } + + +
      + { + parameters.map(p => ( + + )) + } +
    +
    +
    + ); +}); + +export const SetViewParameterModal: FC = memo(function WrappedSetViewParameterModal({ + onClose, + setView +}) { + + const showFullScreen = useIsMobileDevice(); + const dispatch = useAppDispatch(); + const [ + instanceParamInfo + ] = useAppSelector((state: RootStateType) => [ + getPatcherInstancesAndParameters(state) + ]); + + const onAddAllParametersToSetView = useCallback(() => { + modals.openConfirmModal({ + title: "Include all parameters", + centered: true, + children: ( + + Are you sure you want to append all missing parameters from all instances to the SetView { `"${setView.name}"` }? This action cannot be undone. + + ), + labels: { confirm: "Add", cancel: "Cancel" }, + onConfirm: () => dispatch(addAllParamtersToSetView(setView)) + }); + + }, [dispatch, setView]); + + const onRemoveAllParametersFromSetView = useCallback(() => { + modals.openConfirmModal({ + title: "Remove all parameters", + centered: true, + children: ( + + Are you sure you want to remove all parameters from the SetView { `"${setView.name}"` }? This action cannot be undone. + + ), + labels: { confirm: "Remove", cancel: "Cancel" }, + confirmProps: { color: "red" }, + onConfirm: () => dispatch(removeAllParamtersFromSetView(setView)) + }); + }, [dispatch, setView]); + + const onAddParameterToSetView = useCallback((param: ParameterRecord) => { + dispatch(addParameterToSetView(setView, param)); + }, [dispatch, setView]); + + const onRemoveParamterFromSetView = useCallback((param: ParameterRecord) => { + dispatch(removeParameterFromSetView(setView, param)); + }, [dispatch, setView]); + + return ( + + + + + + + + Manage SetView Parameters + + + + + + + + + + + + { + instanceParamInfo.valueSeq().map(({ instance, parameters}) => ( + + )) + } + + + + + + ); +}); diff --git a/src/components/setViews/setviews.module.css b/src/components/setViews/setviews.module.css index f33ce13..f051508 100644 --- a/src/components/setViews/setviews.module.css +++ b/src/components/setViews/setviews.module.css @@ -5,3 +5,29 @@ .setViewNameInput { flex: 1; } + +.instanceParameterList { + border: 1px solid var(--mantine-color-default-border); + border-radius: var(--mantine-radius-sm); + margin: 0; + padding: 0; + list-style: none; +} + +.instanceParameterEntry { + border-bottom: 1px solid var(--mantine-color-default-border); + font-size: var(--mantine-font-size-sm); + padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm); + + &:last-child { + border-bottom: none; + } +} + +.instanceParameterEntryName { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + +} diff --git a/src/models/set.ts b/src/models/set.ts index 925475b..c029015 100644 --- a/src/models/set.ts +++ b/src/models/set.ts @@ -1,4 +1,4 @@ -import { Record as ImmuRecord, List as ImmuList } from "immutable"; +import { Record as ImmuRecord, List as ImmuList, OrderedSet as ImmuOrderedSet } from "immutable"; import { OSCQueryRNBOSetView } from "../lib/types"; import { PatcherInstanceRecord } from "./instance"; import { ParameterRecord } from "./parameter"; @@ -35,6 +35,7 @@ export type GraphSetViewRecordProps = { id: string; name: string; params: ImmuList; + paramIds: ImmuOrderedSet; sortOrder: number; }; @@ -42,6 +43,7 @@ export class GraphSetViewRecord extends ImmuRecord({ id: "0", name: "", params: ImmuList(), + paramIds: ImmuOrderedSet(), sortOrder: 0 }) { @@ -64,6 +66,7 @@ export class GraphSetViewRecord extends ImmuRecord({ id, name: "", params: ImmuList(), + paramIds: ImmuOrderedSet(), sortOrder: 0 }); } @@ -72,6 +75,7 @@ export class GraphSetViewRecord extends ImmuRecord({ return new GraphSetViewRecord({ id, name: desc.CONTENTS.name.VALUE, + paramIds: ImmuOrderedSet(desc.CONTENTS.params.VALUE || []), params: this.getParamListFromDesc(desc.CONTENTS.params.VALUE), sortOrder: desc.CONTENTS.sort_order.VALUE }); @@ -83,7 +87,9 @@ export class GraphSetViewRecord extends ImmuRecord({ public setParams(params: string[]): GraphSetViewRecord { const list = GraphSetViewRecord.getParamListFromDesc(params); - return this.set("params", list); + return this + .set("params", list) + .set("paramIds", ImmuOrderedSet(params)); } public setSortOrder(sortOrder: number): GraphSetViewRecord { diff --git a/src/pages/setviews.tsx b/src/pages/setviews.tsx index ef57a85..1301e31 100644 --- a/src/pages/setviews.tsx +++ b/src/pages/setviews.tsx @@ -4,19 +4,21 @@ import { getGraphSetViewsBySortOrder, getSelectedGraphSetView } from "../selecto import { useCallback } from "react"; import { ResponsiveButton } from "../components/elements/responsiveButton"; import { Group, Stack, Title } from "@mantine/core"; -import { mdiKnob } from "@mdi/js"; +import { mdiTableEye, mdiTune } from "@mdi/js"; import { useDisclosure } from "@mantine/hooks"; -import { createSetViewOnRemote, destroySetViewOnRemote, loadSetView, decreaseParameterIndexInSetView, increaseParameterIndexInSetView, removeParameterFromSetView, renameSetViewOnRemote } from "../actions/sets"; +import { createSetViewOnRemote, destroySetViewOnRemote, loadSetView, decreaseParameterIndexInSetView, increaseParameterIndexInSetView, removeParameterFromSetView, renameSetViewOnRemote } from "../actions/sets"; import SetViewDrawer from "../components/setViews/drawer"; import { GraphSetViewRecord } from "../models/set"; import { getPatcherInstanceParametersBySetView } from "../selectors/patchers"; import { SetViewParameterList } from "../components/setViews/parameterList"; import { ParameterRecord } from "../models/parameter"; import { setInstanceParameterValueNormalizedOnRemote } from "../actions/patchers"; +import { SetViewParameterModal } from "../components/setViews/paramModal"; export default function SetViews() { - const [setViewDrawerOpen, { open: openSetViewDrawer, close: closeSetViewDrawer }] = useDisclosure(); + const [setViewDrawerOpen, { open: openSetViewDrawer, close: closeSetViewDrawer }] = useDisclosure(false); + const [addParametersViewOpen, { open: openAddParametersView, close: closeAddParamtersView }] = useDisclosure(false); const dispatch = useAppDispatch(); const [ @@ -79,10 +81,16 @@ export default function SetViews() {
    + @@ -111,6 +119,15 @@ export default function SetViews() { currentSetView={ currentSetView } setViews={ setViews } /> + { + currentSetView && addParametersViewOpen ? ( + + ) : null + } + ); } diff --git a/src/selectors/patchers.ts b/src/selectors/patchers.ts index 89d933b..eb6d331 100644 --- a/src/selectors/patchers.ts +++ b/src/selectors/patchers.ts @@ -129,6 +129,23 @@ export const getPatcherInstanceParametersByInstanceIndex = createSelector( } ); +export const getPatcherInstancesAndParameters = createSelector( + [ + getPatcherInstances, + getPatcherInstanceParameters + ], + (instances, parameters): ImmuMap; }> => { + return ImmuMap; }>() + .withMutations(map => { + instances.valueSeq().forEach(instance => { + map.set(instance.id, { + instance, + parameters: parameters.filter(p => p.instanceIndex === instance.index).valueSeq() + }); + }); + }); + } +); export const getPatcherInstanceParametersByInstanceIndexAndName = createSelector( [