From a6caad248952cb8e0cca511104d39801d23addfe Mon Sep 17 00:00:00 2001 From: futpib Date: Tue, 20 Nov 2018 18:02:26 +0300 Subject: [PATCH] Add background context menu & keyboard `load-module` actions --- components/graph/base.js | 15 ++++ components/graph/index.js | 120 +++++++++++++++++++++++--- components/hot-keys/index.js | 2 + components/modals/index.js | 27 ++++++ components/modals/load-module.js | 93 ++++++++++++++++++++ components/modals/new-graph-object.js | 38 ++++++++ 6 files changed, 283 insertions(+), 12 deletions(-) create mode 100644 components/modals/load-module.js create mode 100644 components/modals/new-graph-object.js diff --git a/components/graph/base.js b/components/graph/base.js index 349a793..7595e3b 100644 --- a/components/graph/base.js +++ b/components/graph/base.js @@ -28,6 +28,9 @@ class GraphView extends GraphViewBase { } Object.assign(this, { + _super_renderBackground: this.renderBackground, + renderBackground: this.constructor.prototype.renderBackground.bind(this), + _super_handleNodeMove: this.handleNodeMove, handleNodeMove: this.constructor.prototype.handleNodeMove.bind(this), @@ -136,6 +139,18 @@ class GraphView extends GraphViewBase { return super.getMouseCoordinates(); } + renderBackground() { + const { gridSize, backgroundFillId, renderBackground, onBackgroundMouseDown } = this.props; + if (renderBackground) { + return renderBackground({ + gridSize, + backgroundFillId, + onMouseDown: onBackgroundMouseDown, + }); + } + return this._super_renderBackground(); + } + getNodeComponent(id, node) { const { nodeTypes, nodeSubtypes, nodeSize, renderNode, renderNodeText, nodeKey } = this.props; return r(Node, { diff --git a/components/graph/index.js b/components/graph/index.js index 7316bdd..3fda38b 100644 --- a/components/graph/index.js +++ b/components/graph/index.js @@ -262,6 +262,19 @@ const renderDefs = () => r(React.Fragment, [ }), ]); +const renderBackground = ({ + gridSize = 40960, + onMouseDown, +}) => r.rect({ + className: 'background', + x: -(gridSize || 0) / 4, + y: -(gridSize || 0) / 4, + width: gridSize, + height: gridSize, + fill: 'url(#background-pattern)', + onMouseDown, +}); + const renderNode = (nodeRef, data, key, selected, hovered) => r({ sink: Sink, source: Source, @@ -562,7 +575,39 @@ const renderEdgeText = state => ({ data: dgo, transform, selected }) => { const layoutEngine = new LayoutEngine(); -class GraphContextMenu extends React.PureComponent { +class BackgroundContextMenu extends React.PureComponent { + render() { + return r(PopupMenu, { + onClose: this.props.onClose, + }, [ + r(MenuItem, { + label: 'Create', + }, [ + r(MenuItem, { + label: 'Loopback', + onClick: this.props.onLoadModuleLoopback, + }), + + r(MenuItem, { + label: 'Simultaneous output', + onClick: this.props.onLoadModuleCombineSink, + }), + + r(MenuItem, { + label: 'Null output', + onClick: this.props.onLoadModuleNullSink, + }), + ]), + + r(MenuItem, { + label: 'Load a module...', + onClick: this.props.onLoadModule, + }), + ]); + } +} + +class GraphObjectContextMenu extends React.PureComponent { render() { return r(PopupMenu, { onClose: this.props.onClose, @@ -575,7 +620,7 @@ class GraphContextMenu extends React.PureComponent { r(MenuItem.Separator), ]), - r(MenuItem, { + this.props.canDelete() && r(MenuItem, { label: 'Delete', onClick: this.props.onDelete, }), @@ -583,6 +628,8 @@ class GraphContextMenu extends React.PureComponent { } } +const backgroundSymbol = Symbol('graph.backgroundSymbol'); + class Graph extends React.Component { constructor(props) { super(props); @@ -590,11 +637,14 @@ class Graph extends React.Component { this.state = { selected: null, moved: null, + contexted: null, }; this._requestedIcons = new Set(); Object.assign(this, { + onBackgroundMouseDown: this.onBackgroundMouseDown.bind(this), + onSelectNode: this.onSelectNode.bind(this), onCreateNode: this.onCreateNode.bind(this), onUpdateNode: this.onUpdateNode.bind(this), @@ -613,7 +663,12 @@ class Graph extends React.Component { canContextMenuSetAsDefault: this.canContextMenuSetAsDefault.bind(this), onContextMenuSetAsDefault: this.onContextMenuSetAsDefault.bind(this), + canContextMenuDelete: this.canContextMenuDelete.bind(this), onContextMenuDelete: this.onContextMenuDelete.bind(this), + + onLoadModuleLoopback: this.onLoadModuleLoopback.bind(this), + onLoadModuleCombineSink: this.onLoadModuleCombineSink.bind(this), + onLoadModuleNullSink: this.onLoadModuleNullSink.bind(this), }); } @@ -695,7 +750,7 @@ class Graph extends React.Component { let { selected, moved, contexted } = state; - if (contexted && selected !== contexted) { + if (contexted && contexted !== backgroundSymbol && selected !== contexted) { contexted = null; } @@ -709,7 +764,7 @@ class Graph extends React.Component { find(x => x.id === moved.id, edges); } - if (contexted) { + if (contexted && contexted !== backgroundSymbol) { contexted = find(x => x.id === contexted.id, nodes) || find(x => x.id === contexted.id, edges); } @@ -765,6 +820,12 @@ class Graph extends React.Component { this._requestedIcons.add(icon); } + onBackgroundMouseDown() { + this.setState({ + contexted: backgroundSymbol, + }); + } + onSelectNode(selected) { this.setState({ selected }); } @@ -916,8 +977,12 @@ class Graph extends React.Component { } } + canContextMenuDelete() { + return this.state.contexted !== backgroundSymbol; + } + onContextMenuDelete() { - this.onDelete(this.state.selected); + this.onDelete(this.state.contexted); } onContextMenuClose() { @@ -1214,6 +1279,22 @@ class Graph extends React.Component { }); } + hotKeyAdd() { + this.props.openNewGraphObjectModal(); + } + + onLoadModuleLoopback() { + this.props.loadModule('module-loopback', ''); + } + + onLoadModuleCombineSink() { + this.props.loadModule('module-combine-sink', ''); + } + + onLoadModuleNullSink() { + this.props.loadModule('module-null-sink', ''); + } + render() { const { nodes, edges } = this.state; @@ -1236,6 +1317,8 @@ class Graph extends React.Component { nodeSubtypes: {}, edgeTypes: {}, + onBackgroundMouseDown: this.onBackgroundMouseDown, + onSelectNode: this.onSelectNode, onCreateNode: this.onCreateNode, onUpdateNode: this.onUpdateNode, @@ -1255,7 +1338,7 @@ class Graph extends React.Component { layoutEngine, - backgroundFillId: '#background-pattern', + renderBackground, renderDefs, @@ -1266,14 +1349,27 @@ class Graph extends React.Component { renderEdgeText: renderEdgeText(this.props), }), - this.state.contexted && r(GraphContextMenu, { - onClose: this.onContextMenuClose, + this.state.contexted && ( + this.state.contexted === backgroundSymbol ? + r(BackgroundContextMenu, { + onClose: this.onContextMenuClose, - canSetAsDefault: this.canContextMenuSetAsDefault, - onSetAsDefault: this.onContextMenuSetAsDefault, + onLoadModule: this.props.openLoadModuleModal, - onDelete: this.onContextMenuDelete, - }), + onLoadModuleLoopback: this.onLoadModuleLoopback, + onLoadModuleCombineSink: this.onLoadModuleCombineSink, + onLoadModuleNullSink: this.onLoadModuleNullSink, + }) : + r(GraphObjectContextMenu, { + onClose: this.onContextMenuClose, + + canSetAsDefault: this.canContextMenuSetAsDefault, + onSetAsDefault: this.onContextMenuSetAsDefault, + + canDelete: this.canContextMenuDelete, + onDelete: this.onContextMenuDelete, + }) + ), ])); } } diff --git a/components/hot-keys/index.js b/components/hot-keys/index.js index 14ff598..a11c23d 100644 --- a/components/hot-keys/index.js +++ b/components/hot-keys/index.js @@ -33,6 +33,8 @@ const keyMap = { hotKeyShiftMute: 'shift+space', hotKeySetAsDefault: 'f', + + hotKeyAdd: 'a', }; class MyHotKeys extends React.Component { diff --git a/components/modals/index.js b/components/modals/index.js index 3706c70..72d3608 100644 --- a/components/modals/index.js +++ b/components/modals/index.js @@ -28,6 +28,8 @@ const { modules } = require('../../constants/pulse'); const ConnectToServerModal = require('./connect-to-server'); const ConfirmationModal = require('./confirmation'); +const NewGraphObjectModal = require('./new-graph-object'); +const LoadModuleModal = require('./load-module'); Modal.setAppElement('#root'); @@ -46,9 +48,14 @@ class Modals extends React.PureComponent { continuation: null, connectToServerModalOpen: false, + newGraphObjectModalOpen: false, + loadModuleModalOpen: false, actions: { openConnectToServerModal: this.openConnectToServerModal.bind(this), + + openNewGraphObjectModal: this.openNewGraphObjectModal.bind(this), + openLoadModuleModal: this.openLoadModuleModal.bind(this), }, }; this.state = this.initialState; @@ -97,6 +104,14 @@ class Modals extends React.PureComponent { this.setState({ connectToServerModalOpen: true }); } + openNewGraphObjectModal() { + this.setState({ newGraphObjectModalOpen: true }); + } + + openLoadModuleModal() { + this.setState({ loadModuleModalOpen: true }); + } + handleCancel() { this.setState(this.initialState); } @@ -122,6 +137,18 @@ class Modals extends React.PureComponent { isOpen: this.state.connectToServerModalOpen, onRequestClose: this.handleCancel, }), + + r(NewGraphObjectModal, { + isOpen: this.state.newGraphObjectModalOpen, + onRequestClose: this.handleCancel, + + openLoadModuleModal: this.state.actions.openLoadModuleModal, + }), + + r(LoadModuleModal, { + isOpen: this.state.loadModuleModalOpen, + onRequestClose: this.handleCancel, + }), ]); } } diff --git a/components/modals/load-module.js b/components/modals/load-module.js new file mode 100644 index 0000000..2afe9a5 --- /dev/null +++ b/components/modals/load-module.js @@ -0,0 +1,93 @@ + +const r = require('r-dom'); + +const React = require('react'); + +const { connect } = require('react-redux'); +const { bindActionCreators } = require('redux'); + +const Modal = require('react-modal'); + +const Button = require('../button'); +const Label = require('../label'); +const Input = require('../input'); + +const { + pulse: pulseActions, +} = require('../../actions'); + +class LoadModuleModal extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + name: '', + args: '', + }; + + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleSubmit(e) { + e.preventDefault(); + + const { name, args } = this.state; + this.props.loadModule(name, args); + this.props.onRequestClose(); + } + + render() { + const { isOpen, onRequestClose } = this.props; + + return r(Modal, { + isOpen, + onRequestClose, + }, [ + r.h3('Load a module'), + + r.form({ + onSubmit: this.handleSubmit, + }, [ + r(Label, [ + r.div('Module name:'), + r.p([ + r(Input, { + style: { width: '100%' }, + autoFocus: true, + value: this.state.name, + onChange: e => this.setState({ name: e.target.value }), + }), + ]), + ]), + + r(Label, [ + r.div('Arguments:'), + r.p([ + r(Input, { + style: { width: '100%' }, + value: this.state.args, + onChange: e => this.setState({ args: e.target.value }), + }), + ]), + ]), + + r.div({ + className: 'button-group', + }, [ + r(Button, { + onClick: onRequestClose, + }, 'Cancel'), + + r(Button, { + type: 'submit', + }, 'Confirm'), + ]), + ]), + ]); + } +} + +module.exports = connect( + null, + dispatch => bindActionCreators(pulseActions, dispatch), +)(LoadModuleModal); diff --git a/components/modals/new-graph-object.js b/components/modals/new-graph-object.js new file mode 100644 index 0000000..c57ac3c --- /dev/null +++ b/components/modals/new-graph-object.js @@ -0,0 +1,38 @@ + +const r = require('r-dom'); + +const React = require('react'); + +const Modal = require('react-modal'); + +const Button = require('../button'); + +class NewGraphObjectModal extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + name: '', + args: '', + }; + } + + render() { + const { isOpen, onRequestClose, openLoadModuleModal } = this.props; + + return r(Modal, { + isOpen, + onRequestClose, + }, [ + r.h3('Add something'), + + r(Button, { + style: { width: '100%' }, + onClick: openLoadModuleModal, + autoFocus: true, + }, 'Load a module...'), + ]); + } +} + +module.exports = NewGraphObjectModal;