From 9a5618050978ddba59d276531e179532446db5af Mon Sep 17 00:00:00 2001 From: futpib Date: Sun, 18 Nov 2018 23:16:30 +0300 Subject: [PATCH] Add app menu and context menu --- components/graph/index.js | 158 ++++++++++++++++++++++++++------------ components/menu/index.js | 63 +++++++++++++++ package.json | 1 + renderer.js | 9 ++- yarn.lock | 40 +++++++++- 5 files changed, 216 insertions(+), 55 deletions(-) create mode 100644 components/menu/index.js diff --git a/components/graph/index.js b/components/graph/index.js index e898499..d6f8033 100644 --- a/components/graph/index.js +++ b/components/graph/index.js @@ -33,6 +33,8 @@ const { bindActionCreators } = require('redux'); const { HotKeys } = require('react-hotkeys'); +const { PopupMenu, MenuItem } = require('@futpib/react-electron-menu'); + const d = require('../../utils/d'); const memoize = require('../../utils/memoize'); @@ -520,6 +522,19 @@ const renderEdgeText = state => ({ data: dgo, transform, selected }) => { const layoutEngine = new LayoutEngine(); +class GraphContextMenu extends React.PureComponent { + render() { + return r(PopupMenu, { + onClose: this.props.onClose, + }, [ + r(MenuItem, { + label: 'Delete', + onClick: this.props.onDelete, + }), + ]); + } +} + class Graph extends React.Component { constructor(props) { super(props); @@ -543,6 +558,9 @@ class Graph extends React.Component { onSwapEdge: this.onSwapEdge.bind(this), onDeleteEdge: this.onDeleteEdge.bind(this), onEdgeMouseDown: this.onEdgeMouseDown.bind(this), + + onContextMenuDelete: this.onContextMenuDelete.bind(this), + onContextMenuClose: this.onContextMenuClose.bind(this), }); } @@ -622,7 +640,11 @@ class Graph extends React.Component { dgoToPai.set(edge, pai); }); - let { selected, moved } = state; + let { selected, moved, contexted } = state; + + if (contexted && selected !== contexted) { + contexted = null; + } if (selected) { selected = find(x => x.id === selected.id, nodes) || @@ -634,12 +656,18 @@ class Graph extends React.Component { find(x => x.id === moved.id, edges); } + if (contexted) { + contexted = find(x => x.id === contexted.id, nodes) || + find(x => x.id === contexted.id, edges); + } + return { nodes, edges, selected, moved, + contexted, }; } @@ -650,6 +678,7 @@ class Graph extends React.Component { (nextProps.preferences === this.props.preferences) && (nextProps.icons === this.props.icons) && (nextState.selected === this.state.selected) && + (nextState.contexted === this.state.contexted) && (nextState.moved === this.state.moved) ); } @@ -693,19 +722,7 @@ class Graph extends React.Component { } onDeleteNode(selected) { - const pai = dgoToPai.get(selected); - - if (selected.type === 'client') { - this.props.killClientByIndex(selected.index); - } else if (selected.type === 'module') { - this.props.unloadModuleByIndex(selected.index); - } else if ( - (selected.type === 'sink' || selected.type === 'source') && - pai && - typeof pai.moduleIndex === 'number' - ) { - this.props.unloadModuleByIndex(pai.moduleIndex); - } + this.onDelete(selected); } onNodeMouseDown(event, data) { @@ -718,6 +735,11 @@ class Graph extends React.Component { ) { this.toggleMute(pai); } + } else if (pai && event.button === 2) { + this.setState({ + selected: data, + contexted: data, + }); } } @@ -737,11 +759,7 @@ class Graph extends React.Component { } onDeleteEdge(selected) { - if (selected.type === 'sinkInput') { - this.props.killSinkInputByIndex(selected.index); - } else if (selected.type === 'sourceOutput') { - this.props.killSourceOutputByIndex(selected.index); - } + this.onDelete(selected); } onEdgeMouseDown(event, data) { @@ -752,6 +770,11 @@ class Graph extends React.Component { ) { this.toggleMute(pai); } + } else if (pai && event.button === 2) { + this.setState({ + selected: data, + contexted: data, + }); } } @@ -798,6 +821,36 @@ class Graph extends React.Component { } } + onDelete(selected) { + const pai = dgoToPai.get(selected); + + if (selected.type === 'client') { + this.props.killClientByIndex(selected.index); + } else if (selected.type === 'module') { + this.props.unloadModuleByIndex(selected.index); + } else if (selected.type === 'sinkInput') { + this.props.killSinkInputByIndex(selected.index); + } else if (selected.type === 'sourceOutput') { + this.props.killSourceOutputByIndex(selected.index); + } else if ( + (selected.type === 'sink' || selected.type === 'source') && + pai && + typeof pai.moduleIndex === 'number' + ) { + this.props.unloadModuleByIndex(pai.moduleIndex); + } + } + + onContextMenuDelete() { + this.onDelete(this.state.selected); + } + + onContextMenuClose() { + this.setState({ + contexted: null, + }); + } + focus() { this.graphViewElement.focus(); } @@ -1054,48 +1107,55 @@ class Graph extends React.Component { handlers: map(f => bind(f, this), pick(keys(keyMap), this)), }, r.div({ id: 'graph', - }, r(GraphView, { - nodeKey: 'id', - edgeKey: 'id', + }, [ + r(GraphView, { + nodeKey: 'id', + edgeKey: 'id', - nodes, - edges, + nodes, + edges, - selected: this.state.selected, - moved: this.state.moved, + selected: this.state.selected, + moved: this.state.moved, - nodeTypes: {}, - nodeSubtypes: {}, - edgeTypes: {}, + nodeTypes: {}, + nodeSubtypes: {}, + edgeTypes: {}, - onSelectNode: this.onSelectNode, - onCreateNode: this.onCreateNode, - onUpdateNode: this.onUpdateNode, - onDeleteNode: this.onDeleteNode, - onNodeMouseDown: this.onNodeMouseDown, + onSelectNode: this.onSelectNode, + onCreateNode: this.onCreateNode, + onUpdateNode: this.onUpdateNode, + onDeleteNode: this.onDeleteNode, + onNodeMouseDown: this.onNodeMouseDown, - onSelectEdge: this.onSelectEdge, - onCreateEdge: this.onCreateEdge, - onSwapEdge: this.onSwapEdge, - onDeleteEdge: this.onDeleteEdge, - onEdgeMouseDown: this.onEdgeMouseDown, + onSelectEdge: this.onSelectEdge, + onCreateEdge: this.onCreateEdge, + onSwapEdge: this.onSwapEdge, + onDeleteEdge: this.onDeleteEdge, + onEdgeMouseDown: this.onEdgeMouseDown, - showGraphControls: false, + showGraphControls: false, - edgeArrowSize: 64, + edgeArrowSize: 64, - layoutEngine, + layoutEngine, - backgroundFillId: '#background-pattern', + backgroundFillId: '#background-pattern', - renderDefs, + renderDefs, - renderNode, - renderNodeText: renderNodeText(this.props), + renderNode, + renderNodeText: renderNodeText(this.props), - renderEdge, - renderEdgeText: renderEdgeText(this.props), - }))); + renderEdge, + renderEdgeText: renderEdgeText(this.props), + }), + + this.state.contexted && r(GraphContextMenu, { + onClose: this.onContextMenuClose, + onDelete: this.onContextMenuDelete, + }), + ])); } } diff --git a/components/menu/index.js b/components/menu/index.js new file mode 100644 index 0000000..adbc376 --- /dev/null +++ b/components/menu/index.js @@ -0,0 +1,63 @@ + +const electron = require('electron'); + +const React = require('react'); + +const r = require('r-dom'); + +const { + WindowMenu: WindowMenuBase, + MenuItem, + Provider, +} = require('@futpib/react-electron-menu'); + +const MenuProvider = ({ children }) => r(Provider, { electron }, r(React.Fragment, {}, [ + r(WindowMenu), + ...[].concat(children), +])); + +const WindowMenu = () => r(WindowMenuBase, [ + r(MenuItem, { + label: 'App', + }, [ + r(MenuItem, { + label: 'Quit', + role: 'quit', + }), + ]), + + r(MenuItem, { + label: 'View', + }, [ + r(MenuItem, { + label: 'Reload', + role: 'reload', + }), + r(MenuItem, { + label: 'Force Reload', + role: 'forcereload', + }), + r(MenuItem, { + label: 'Toggle Developer Tools', + role: 'toggledevtools', + }), + + r(MenuItem.Separator), + + r(MenuItem, { + label: 'Toggle Full Screen', + role: 'togglefullscreen', + }), + ]), + + r(MenuItem, { + label: 'Help', + }, [ + r(MenuItem, { + label: 'Documentation', + onClick: () => electron.shell.openExternal('https://github.com/futpib/pagraphcontrol#readme'), + }), + ]), +]); + +module.exports = { MenuProvider }; diff --git a/package.json b/package.json index e69ddd5..001c7dc 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@futpib/paclient": "^0.0.7", + "@futpib/react-electron-menu": "^0.3.0", "bluebird": "^3.5.3", "camelcase": "^5.0.0", "d3": "^5.7.0", diff --git a/renderer.js b/renderer.js index 57cbd78..78ae18b 100644 --- a/renderer.js +++ b/renderer.js @@ -4,7 +4,7 @@ const r = require('r-dom'); const { render } = require('react-dom'); -const { Provider } = require('react-redux'); +const { Provider: ReduxProvider } = require('react-redux'); const createStore = require('./store'); @@ -12,16 +12,17 @@ const Graph = require('./components/graph'); const Cards = require('./components/cards'); const Preferences = require('./components/preferences'); const { HotKeys } = require('./components/hot-keys'); +const { MenuProvider } = require('./components/menu'); const theme = require('./utils/theme'); -const Root = () => r(Provider, { +const Root = () => r(ReduxProvider, { store: createStore(), -}, r(HotKeys, {}, ({ graphRef, cardsRef, preferencesRef }) => [ +}, r(MenuProvider, {}, r(HotKeys, {}, ({ graphRef, cardsRef, preferencesRef }) => [ r(Graph, { ref: graphRef }), r(Cards, { ref: cardsRef }), r(Preferences, { ref: preferencesRef }), -])); +]))); Object.entries(theme.colors).forEach(([ key, value ]) => { document.body.style.setProperty('--' + key, value); diff --git a/yarn.lock b/yarn.lock index c1635ad..397d2ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -89,6 +89,14 @@ resolved "https://registry.yarnpkg.com/@futpib/paclient/-/paclient-0.0.7.tgz#d8957135ba81888f5e92812d8e9e4e8e1ebf935f" integrity sha512-fjpJaS3LHuo+51/7g3dqpZBGO2wZtnLAWYKVk5CIBsfqn3345xJaEe0HfLpBxPAdpAHRTcTz5aWXlhOWsBClHA== +"@futpib/react-electron-menu@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@futpib/react-electron-menu/-/react-electron-menu-0.3.0.tgz#33a9f5bb21823805a9782daf8ba2f8df02cde8f7" + integrity sha512-RqlD74LUvDTP5gvHwUy1qGwBLvNnjPmgbDzl8+n2t9Z5WkHsCMAXnsYfEyuKxWMkivCrEYeLBwYEardtm6hRiA== + dependencies: + react "^15.4.2" + react-test-renderer "^15.4.2" + "@ladjs/time-require@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@ladjs/time-require/-/time-require-0.1.4.tgz#5c615d75fd647ddd5de9cf6922649558856b21a1" @@ -1394,6 +1402,15 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" +create-react-class@^15.6.0: + version "15.6.3" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" + integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg== + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -2502,7 +2519,7 @@ fast-levenshtein@~2.0.4: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fbjs@^0.8.1: +fbjs@^0.8.1, fbjs@^0.8.9: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= @@ -4915,7 +4932,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: +prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== @@ -5093,6 +5110,25 @@ react-redux@^5.1.0: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-test-renderer@^15.4.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8" + integrity sha1-0DM0NPwsQ4CSaWyncNpe1IA376g= + dependencies: + fbjs "^0.8.9" + object-assign "^4.1.0" + +react@^15.4.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" + integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI= + dependencies: + create-react-class "^15.6.0" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + react@^16.6.0: version "16.6.0" resolved "https://registry.yarnpkg.com/react/-/react-16.6.0.tgz#b34761cfaf3e30f5508bc732fb4736730b7da246"