diff --git a/src/AutomatonWithGUI.tsx b/src/AutomatonWithGUI.tsx
index a11c637f..9bcd5901 100644
--- a/src/AutomatonWithGUI.tsx
+++ b/src/AutomatonWithGUI.tsx
@@ -414,7 +414,7 @@ export class AutomatonWithGUI extends Automaton
* @param index Index of the curve
*/
public removeCurve( index: number ): void {
- delete this.__curves[ index ];
+ this.__curves.splice( index, 1 );
this.__emit( 'removeCurve', { index } );
diff --git a/src/view/components/AutomatonStateListener.tsx b/src/view/components/AutomatonStateListener.tsx
index dd016c16..83a52b60 100644
--- a/src/view/components/AutomatonStateListener.tsx
+++ b/src/view/components/AutomatonStateListener.tsx
@@ -369,6 +369,13 @@ const AutomatonStateListener = ( props: AutomatonStateListenerProps ): JSX.Eleme
createCurve( event.index, event.curve );
} );
+ const handleRemoveCurve = automaton.on( 'removeCurve', ( event ) => {
+ dispatch( {
+ type: 'Automaton/RemoveCurve',
+ curve: event.index
+ } );
+ } );
+
const handleChangeShouldSave = automaton.on( 'changeShouldSave', ( event ) => {
dispatch( {
type: 'Automaton/SetShouldSave',
@@ -387,6 +394,7 @@ const AutomatonStateListener = ( props: AutomatonStateListenerProps ): JSX.Eleme
automaton.off( 'createChannel', handleCreateChannel );
automaton.off( 'removeChannel', handleRemoveChannel );
automaton.off( 'createCurve', handleCreateCurve );
+ automaton.off( 'removeCurve', handleRemoveCurve );
automaton.off( 'changeShouldSave', handleChangeShouldSave );
};
},
diff --git a/src/view/components/ChannelList.tsx b/src/view/components/ChannelList.tsx
index 6000939a..b1138174 100644
--- a/src/view/components/ChannelList.tsx
+++ b/src/view/components/ChannelList.tsx
@@ -1,12 +1,35 @@
-import React, { useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
+import { useDispatch, useSelector } from '../states/store';
import { ChannelListEntry } from './ChannelListEntry';
+import { Colors } from '../constants/Colors';
+import { Icons } from '../icons/Icons';
import styled from 'styled-components';
-import { useSelector } from '../states/store';
// == styles =======================================================================================
+const NewChannelIcon = styled.img`
+ fill: ${ Colors.gray };
+ height: 16px;
+`;
+
+const NewChannelButton = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 20px;
+ margin: 2px 0;
+ cursor: pointer;
+ background: ${ Colors.back3 };
+
+ &:active {
+ background: ${ Colors.back4 };
+ }
+`;
+
const StyledChannelListEntry = styled( ChannelListEntry )`
width: 100%;
- margin: 0.125rem 0;
+ height: 20px;
+ margin: 2px 0;
cursor: pointer;
`;
@@ -19,10 +42,57 @@ export interface ChannelListProps {
}
const ChannelList = ( { className }: ChannelListProps ): JSX.Element => {
- const { channelNames } = useSelector( ( state ) => ( {
+ const dispatch = useDispatch();
+ const {
+ automaton,
+ channelNames
+ } = useSelector( ( state ) => ( {
+ automaton: state.automaton.instance,
channelNames: state.automaton.channelNames
} ) );
+ const handleClickNewChannel = useCallback(
+ ( event: React.MouseEvent ) => {
+ if ( !automaton ) { return; }
+
+ dispatch( {
+ type: 'TextPrompt/Open',
+ position: { x: event.clientX, y: event.clientY },
+ placeholder: 'Name for the new channel',
+ checkValid: ( name ) => {
+ if ( name === '' ) { return false; }
+ if ( automaton.getChannel( name ) != null ) { return false; }
+ return true;
+ },
+ callback: ( name ) => {
+ const redo = (): void => {
+ automaton.createChannel( name );
+
+ dispatch( {
+ type: 'Timeline/SelectChannel',
+ channel: name
+ } );
+ };
+
+ const undo = (): void => {
+ automaton.removeChannel( name );
+ };
+
+ dispatch( {
+ type: 'History/Push',
+ entry: {
+ description: `Create Channel: ${ name }`,
+ redo,
+ undo
+ }
+ } );
+ redo();
+ }
+ } );
+ },
+ [ automaton ]
+ );
+
const sortedChannelNames = useMemo(
() => Array.from( channelNames ).sort(),
[ channelNames ]
@@ -36,6 +106,12 @@ const ChannelList = ( { className }: ChannelListProps ): JSX.Element => {
name={ channel }
/>
) ) }
+
+
+
);
};
diff --git a/src/view/components/ChannelListEntry.tsx b/src/view/components/ChannelListEntry.tsx
index cf77812c..f332dc2a 100644
--- a/src/view/components/ChannelListEntry.tsx
+++ b/src/view/components/ChannelListEntry.tsx
@@ -48,7 +48,6 @@ const Icon = styled.img`
const Root = styled.div<{ isSelected: boolean }>`
position: relative;
- height: 1.25rem;
background: ${ ( { isSelected } ) => ( isSelected ? Colors.back4 : Colors.back3 ) };
`;
diff --git a/src/view/components/CurveList.tsx b/src/view/components/CurveList.tsx
index 9e336f80..fef9fb15 100644
--- a/src/view/components/CurveList.tsx
+++ b/src/view/components/CurveList.tsx
@@ -1,14 +1,38 @@
+import React, { useCallback } from 'react';
+import { useDispatch, useSelector } from '../states/store';
+
import { Colors } from '../constants/Colors';
import { CurveListEntry } from './CurveListEntry';
-import React from 'react';
+import { Icons } from '../icons/Icons';
+import { Metrics } from '../constants/Metrics';
import { Scrollable } from './Scrollable';
import styled from 'styled-components';
-import { useSelector } from '../states/store';
// == styles =======================================================================================
+const NewCurveIcon = styled.img`
+ fill: ${ Colors.gray };
+ height: 16px;
+`;
+
+const NewCurveButton = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: ${ Metrics.curveListEntryHeight }px;
+ margin: 2px;
+ cursor: pointer;
+ background: ${ Colors.back3 };
+
+ &:active {
+ background: ${ Colors.back4 };
+ }
+`;
+
const StyledCurveListEntry = styled( CurveListEntry )`
- width: calc( 100% - 0.25rem );
- margin: 0.125rem;
+ width: calc( 100% - 4px );
+ height: ${ Metrics.curveListEntryHeight }px;
+ margin: 2px;
cursor: pointer;
`;
@@ -22,10 +46,50 @@ export interface CurveListProps {
}
const CurveList = ( { className }: CurveListProps ): JSX.Element => {
- const { curves } = useSelector( ( state ) => ( {
+ const dispatch = useDispatch();
+ const { automaton, curves } = useSelector( ( state ) => ( {
+ automaton: state.automaton.instance,
curves: state.automaton.curves
} ) );
+ const handleClickNewCurve = useCallback(
+ () => {
+ if ( !automaton ) { return; }
+
+ const curve = automaton.createCurve();
+ const index = automaton.getCurveIndex( curve );
+
+ const redo = (): void => {
+ const curve = automaton.createCurve();
+ const index = automaton.getCurveIndex( curve );
+
+ dispatch( {
+ type: 'CurveEditor/SelectCurve',
+ curve: index
+ } );
+ };
+
+ const undo = (): void => {
+ automaton.removeCurve( index );
+ };
+
+ dispatch( {
+ type: 'History/Push',
+ entry: {
+ description: 'Create Curve',
+ redo,
+ undo
+ }
+ } );
+
+ dispatch( {
+ type: 'CurveEditor/SelectCurve',
+ curve: index
+ } );
+ },
+ [ automaton ]
+ );
+
return (
{ curves.map( ( curve, iCurve ) => (
@@ -34,6 +98,12 @@ const CurveList = ( { className }: CurveListProps ): JSX.Element => {
index={ iCurve }
/>
) ) }
+
+
+
);
};
diff --git a/src/view/components/CurveListEntry.tsx b/src/view/components/CurveListEntry.tsx
index 39611713..9c44c7a1 100644
--- a/src/view/components/CurveListEntry.tsx
+++ b/src/view/components/CurveListEntry.tsx
@@ -3,7 +3,6 @@ import { useDispatch, useSelector } from '../states/store';
import { Colors } from '../constants/Colors';
import { CurveStatusLevel } from '../../CurveWithGUI';
import { Icons } from '../icons/Icons';
-import { Metrics } from '../constants/Metrics';
import styled from 'styled-components';
// == styles =======================================================================================
@@ -30,7 +29,6 @@ const Icon = styled.img`
const Root = styled.div<{ isSelected: boolean }>`
position: relative;
- height: ${ Metrics.curveListEntryHeight }px;
background: ${ ( { isSelected } ) => ( isSelected ? Colors.back4 : Colors.back3 ) };
`;
diff --git a/src/view/icons/Icons.ts b/src/view/icons/Icons.ts
index 81292bf4..af7d3b13 100644
--- a/src/view/icons/Icons.ts
+++ b/src/view/icons/Icons.ts
@@ -9,6 +9,7 @@ export const Icons = {
Error: require( './error.svg' ).default,
Pause: require( './pause.svg' ).default,
Play: require( './play.svg' ).default,
+ Plus: require( './plus.svg' ).default,
Redo: require( './redo.svg' ).default,
Save: require( './save.svg' ).default,
Snap: require( './snap.svg' ).default,
diff --git a/src/view/icons/plus.svg b/src/view/icons/plus.svg
new file mode 100644
index 00000000..f1c3caf4
--- /dev/null
+++ b/src/view/icons/plus.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/view/states/Automaton.ts b/src/view/states/Automaton.ts
index 870c1d58..504e3f44 100644
--- a/src/view/states/Automaton.ts
+++ b/src/view/states/Automaton.ts
@@ -94,6 +94,9 @@ export type Action = {
curve: number;
length: number;
path: string;
+} | {
+ type: 'Automaton/RemoveCurve';
+ curve: number;
} | {
type: 'Automaton/UpdateCurvePath';
curve: number;
@@ -202,6 +205,8 @@ export const reducer: Reducer = ( state = initialState, action )
previewTime: null,
previewValue: null
};
+ } else if ( action.type === 'Automaton/RemoveCurve' ) {
+ newState.curves.splice( action.curve, 1 );
} else if ( action.type === 'Automaton/UpdateCurvePath' ) {
newState.curves[ action.curve ].path = action.path;
} else if ( action.type === 'Automaton/UpdateCurveStatus' ) {
diff --git a/src/view/states/CurveEditor.ts b/src/view/states/CurveEditor.ts
index d83c5531..415bb2d2 100644
--- a/src/view/states/CurveEditor.ts
+++ b/src/view/states/CurveEditor.ts
@@ -146,6 +146,10 @@ export const reducer: Reducer = ( state = initialState, ac
if ( length < newState.range.t1 ) {
newState.range.t1 = length;
}
+ } else if ( action.type === 'Automaton/RemoveCurve' ) {
+ if ( state.selectedCurve === action.curve ) {
+ newState.selectedCurve = null;
+ }
} else if ( action.type === 'Automaton/RemoveCurveNode' ) {
newState.selectedItems.nodes = arraySetDiff( newState.selectedItems.nodes, [ action.id ] );
} else if ( action.type === 'Automaton/RemoveCurveFx' ) {