1587 lines
34 KiB
JavaScript
1587 lines
34 KiB
JavaScript
/* global document */
|
|
|
|
const {
|
|
all,
|
|
allPass,
|
|
bind,
|
|
compose,
|
|
defaultTo,
|
|
filter,
|
|
find,
|
|
flatten,
|
|
forEach,
|
|
keys,
|
|
map,
|
|
max,
|
|
merge,
|
|
min,
|
|
omit,
|
|
path,
|
|
pick,
|
|
prop,
|
|
reduce,
|
|
repeat,
|
|
sortBy,
|
|
values,
|
|
scan,
|
|
range,
|
|
} = require('ramda');
|
|
|
|
const React = require('react');
|
|
|
|
const r = require('r-dom');
|
|
|
|
const {
|
|
connect,
|
|
Provider: ReduxProvider,
|
|
ReactReduxContext: { Consumer: ReduxConsumer },
|
|
} = require('react-redux');
|
|
const { bindActionCreators } = require('redux');
|
|
|
|
const {
|
|
fromRenderProps,
|
|
} = require('recompose');
|
|
|
|
const { HotKeys } = require('react-hotkeys');
|
|
|
|
const { PopupMenu, MenuItem } = require('@futpib/react-electron-menu');
|
|
|
|
const d = require('../../utils/d');
|
|
const memoize = require('../../utils/memoize');
|
|
const {
|
|
forwardRef,
|
|
unforwardRef,
|
|
} = require('../../utils/recompose');
|
|
|
|
const {
|
|
pulse: pulseActions,
|
|
icons: iconsActions,
|
|
} = require('../../actions');
|
|
|
|
const {
|
|
getPaiByTypeAndIndex,
|
|
getPaiByDgoFromInfos,
|
|
|
|
getDerivedMonitorSources,
|
|
|
|
getClientSinkInputs,
|
|
getModuleSinkInputs,
|
|
|
|
getClientSourceOutputs,
|
|
getModuleSourceOutputs,
|
|
|
|
getSinkSinkInputs,
|
|
|
|
getDefaultSinkPai,
|
|
getDefaultSourcePai,
|
|
} = require('../../selectors');
|
|
|
|
const {
|
|
PA_VOLUME_NORM,
|
|
} = require('../../constants/pulse');
|
|
|
|
const { size } = require('../../constants/view');
|
|
|
|
const VolumeSlider = require('../../components/volume-slider');
|
|
|
|
const { primaryPulseServer } = require('../../reducers/pulse');
|
|
|
|
const { keyMap } = require('../hot-keys');
|
|
|
|
const {
|
|
SatellitesGraphView,
|
|
} = require('./satellites-graph');
|
|
|
|
const {
|
|
Edge,
|
|
} = require('./base');
|
|
|
|
const Peaks = require('./peaks');
|
|
|
|
const LayoutEngine = require('./layout-engine');
|
|
|
|
const maximum = reduce(max, -Infinity);
|
|
const clamp = (v, lo, hi) => min(hi, max(lo, v));
|
|
|
|
const leftOf = (x, xs) => {
|
|
const i = ((xs.indexOf(x) + xs.length - 1) % xs.length);
|
|
return xs[i];
|
|
};
|
|
|
|
const rightOf = (x, xs) => {
|
|
const i = ((xs.indexOf(x) + 1) % xs.length);
|
|
return xs[i];
|
|
};
|
|
|
|
const selectionObjectTypes = {
|
|
order: [
|
|
'source',
|
|
'sourceOutput',
|
|
'client|module',
|
|
'sinkInput',
|
|
'sink',
|
|
],
|
|
|
|
left(type) {
|
|
return leftOf(type, this.order);
|
|
},
|
|
|
|
right(type) {
|
|
return rightOf(type, this.order);
|
|
},
|
|
|
|
fromPulseType(type) {
|
|
if (type === 'client' || type === 'module') {
|
|
return 'client|module';
|
|
}
|
|
|
|
return type;
|
|
},
|
|
|
|
toPulsePredicate(type) {
|
|
type = this.fromPulseType(type);
|
|
if (type === 'client|module') {
|
|
return o => (o.type === 'client' || o.type === 'module');
|
|
}
|
|
|
|
return o => o.type === type;
|
|
},
|
|
};
|
|
|
|
const key = pao => `${pao.type}-${pao.index}`;
|
|
|
|
const sourceKey = pai => {
|
|
if (pai.type === 'monitorSource') {
|
|
return `sink-${pai.sinkIndex}`;
|
|
}
|
|
|
|
if (pai.clientIndex === -1) {
|
|
return `module-${pai.moduleIndex}`;
|
|
}
|
|
|
|
return `client-${pai.clientIndex}`;
|
|
};
|
|
|
|
const targetKey = pai => {
|
|
if (pai.type === 'monitorSource') {
|
|
return `source-${pai.sourceIndex}`;
|
|
}
|
|
|
|
if (pai.type === 'sinkInput') {
|
|
return `sink-${pai.sinkIndex}`;
|
|
}
|
|
|
|
return `source-${pai.sourceIndex}`;
|
|
};
|
|
|
|
const paoToNode = memoize(pao => ({
|
|
id: key(pao),
|
|
index: pao.index,
|
|
type: pao.type,
|
|
}));
|
|
|
|
const paoToEdge = memoize(pao => ({
|
|
id: key(pao),
|
|
source: sourceKey(pao),
|
|
target: targetKey(pao),
|
|
index: pao.index,
|
|
type: pao.type,
|
|
}));
|
|
|
|
const getPaiIcon = memoize(pai => {
|
|
return null
|
|
|| path([ 'properties', 'application', 'icon_name' ], pai)
|
|
|| path([ 'properties', 'device', 'icon_name' ], pai);
|
|
});
|
|
|
|
const s2 = size / 2;
|
|
|
|
const Sink = () => r.path({
|
|
d: d()
|
|
.moveTo(-s2, 0)
|
|
.lineTo(-s2 * 1.3, -s2)
|
|
.lineTo(s2, -s2)
|
|
.lineTo(s2, s2)
|
|
.lineTo(-s2 * 1.3, s2)
|
|
.close()
|
|
.toString(),
|
|
});
|
|
|
|
const Source = () => r.path({
|
|
d: d()
|
|
.moveTo(s2 * 1.3, 0)
|
|
.lineTo(s2, s2)
|
|
.lineTo(-s2, s2)
|
|
.lineTo(-s2, -s2)
|
|
.lineTo(s2, -s2)
|
|
.close()
|
|
.toString(),
|
|
});
|
|
|
|
const Client = () => r.path({
|
|
d: d()
|
|
.moveTo(s2 * 1.3, 0)
|
|
.lineTo(s2, s2)
|
|
.lineTo(-s2 * 1.3, s2)
|
|
.lineTo(-s2, 0)
|
|
.lineTo(-s2 * 1.3, -s2)
|
|
.lineTo(s2, -s2)
|
|
.close()
|
|
.toString(),
|
|
});
|
|
|
|
const Module = Client;
|
|
|
|
const gridDotSize = 2;
|
|
const gridSpacing = 36;
|
|
|
|
const Marker = ({ id, d }) => r('marker', {
|
|
id,
|
|
viewBox: '0 -8 18 16',
|
|
refX: '16',
|
|
markerWidth: '16',
|
|
markerHeight: '16',
|
|
orient: 'auto',
|
|
}, r.path({
|
|
className: 'arrow',
|
|
d,
|
|
}));
|
|
|
|
const sourceArrowPathDescription = 'M 16,-8 L 0,0 L 16,8';
|
|
const sinkArrowPathDescription = 'M 2,-8 L 18,0 L 2,8';
|
|
|
|
const renderDefs = () => r(React.Fragment, [
|
|
r.pattern({
|
|
id: 'background-pattern',
|
|
key: 'background-pattern',
|
|
width: gridSpacing,
|
|
height: gridSpacing,
|
|
patternUnits: 'userSpaceOnUse',
|
|
}, r.circle({
|
|
className: 'grid-dot',
|
|
cx: (gridSpacing || 0) / 2,
|
|
cy: (gridSpacing || 0) / 2,
|
|
r: gridDotSize,
|
|
})),
|
|
|
|
r(Marker, {
|
|
id: 'my-source-arrow',
|
|
d: sourceArrowPathDescription,
|
|
}),
|
|
|
|
r(Marker, {
|
|
id: 'my-sink-arrow',
|
|
d: sinkArrowPathDescription,
|
|
}),
|
|
|
|
// WORKAROUND: `context-fill` did not work
|
|
r(Marker, {
|
|
id: 'my-source-arrow-selected',
|
|
d: sourceArrowPathDescription,
|
|
}),
|
|
|
|
r(Marker, {
|
|
id: 'my-sink-arrow-selected',
|
|
d: sinkArrowPathDescription,
|
|
}),
|
|
]);
|
|
|
|
const renderBackground = ({
|
|
gridSize = 40960 / 4,
|
|
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,
|
|
client: Client,
|
|
module: Module,
|
|
}[data.type] || Module, {
|
|
selected,
|
|
hovered,
|
|
});
|
|
|
|
const getVolumesForThumbnail = ({ pai, lockChannelsTogether }) => {
|
|
let volumes = (pai && pai.channelVolumes) || [];
|
|
if (lockChannelsTogether) {
|
|
if (volumes.every(v => v === volumes[0])) {
|
|
volumes = [
|
|
maximum(volumes),
|
|
];
|
|
}
|
|
}
|
|
|
|
return volumes;
|
|
};
|
|
|
|
const VolumeThumbnail = connect(
|
|
state => ({
|
|
hideVolumeThumbnails: state.preferences.hideVolumeThumbnails,
|
|
lockChannelsTogether: state.preferences.lockChannelsTogether,
|
|
}),
|
|
)(({ pai, hideVolumeThumbnails, lockChannelsTogether }) => {
|
|
if (hideVolumeThumbnails) {
|
|
return r(React.Fragment);
|
|
}
|
|
|
|
const normVolume = PA_VOLUME_NORM;
|
|
const baseVolume = defaultTo(normVolume, pai && pai.baseVolume);
|
|
|
|
const volumes = getVolumesForThumbnail({ pai, lockChannelsTogether });
|
|
const muted = !pai || pai.muted;
|
|
|
|
const step = size / 32;
|
|
const padding = 2;
|
|
const width = size - 8;
|
|
const height = ((1 + volumes.length) * step);
|
|
|
|
return r.svg({
|
|
classSet: {
|
|
'volume-thumbnail': true,
|
|
'volume-thumbnail-muted': muted,
|
|
},
|
|
height: (2 * padding) + height,
|
|
}, [
|
|
r.line({
|
|
className: 'volume-thumbnail-ruler-line',
|
|
x1: padding,
|
|
x2: padding,
|
|
y1: padding,
|
|
y2: padding + height,
|
|
}),
|
|
|
|
baseVolume && r.line({
|
|
className: 'volume-thumbnail-ruler-line',
|
|
x1: padding + ((baseVolume / normVolume) * width),
|
|
x2: padding + ((baseVolume / normVolume) * width),
|
|
y1: padding,
|
|
y2: padding + height,
|
|
}),
|
|
|
|
r.line({
|
|
className: 'volume-thumbnail-ruler-line',
|
|
x1: padding + width,
|
|
x2: padding + width,
|
|
y1: padding,
|
|
y2: padding + height,
|
|
}),
|
|
|
|
...volumes.map((v, i) => {
|
|
const a = min(v / normVolume, baseVolume / normVolume);
|
|
const b = min(v / normVolume, 1);
|
|
const c = v / normVolume;
|
|
|
|
return r(React.Fragment, [
|
|
r.line({
|
|
className: 'volume-thumbnail-volume-line',
|
|
x1: padding,
|
|
x2: padding + (a * width),
|
|
y1: padding + ((1 + i) * step),
|
|
y2: padding + ((1 + i) * step),
|
|
}),
|
|
|
|
r.line({
|
|
className: 'volume-thumbnail-volume-line volume-thumbnail-volume-line-warning',
|
|
x1: padding + (a * width),
|
|
x2: padding + (b * width),
|
|
y1: padding + ((1 + i) * step),
|
|
y2: padding + ((1 + i) * step),
|
|
}),
|
|
|
|
r.line({
|
|
className: 'volume-thumbnail-volume-line volume-thumbnail-volume-line-error',
|
|
x1: padding + (b * width),
|
|
x2: padding + (c * width),
|
|
y1: padding + ((1 + i) * step),
|
|
y2: padding + ((1 + i) * step),
|
|
}),
|
|
]);
|
|
}),
|
|
]);
|
|
});
|
|
|
|
const getVolumes = ({ pai, lockChannelsTogether }) => {
|
|
let volumes = (pai && pai.channelVolumes) || [];
|
|
if (lockChannelsTogether) {
|
|
volumes = [
|
|
maximum(volumes),
|
|
];
|
|
}
|
|
|
|
return volumes;
|
|
};
|
|
|
|
const VolumeControls = connect(
|
|
state => pick([
|
|
'maxVolume',
|
|
'volumeStep',
|
|
'lockChannelsTogether',
|
|
], state.preferences),
|
|
dispatch => bindActionCreators(pulseActions, dispatch),
|
|
)(({ pai, maxVolume, volumeStep, lockChannelsTogether, ...props }) => {
|
|
const volumes = getVolumes({ pai, lockChannelsTogether });
|
|
const baseVolume = pai && pai.baseVolume;
|
|
const muted = !pai || pai.muted;
|
|
|
|
return r.div({
|
|
className: 'volume-controls',
|
|
}, [
|
|
...volumes.map((v, channelIndex) => r(VolumeSlider, {
|
|
muted,
|
|
baseVolume,
|
|
normVolume: PA_VOLUME_NORM,
|
|
maxVolume: PA_VOLUME_NORM * maxVolume,
|
|
volumeStep,
|
|
value: v,
|
|
onChange: v => {
|
|
if (pai.type === 'sink') {
|
|
if (lockChannelsTogether) {
|
|
props.setSinkVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
|
} else {
|
|
props.setSinkChannelVolume(pai.index, channelIndex, v);
|
|
}
|
|
} else if (pai.type === 'source') {
|
|
if (lockChannelsTogether) {
|
|
props.setSourceVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
|
} else {
|
|
props.setSourceChannelVolume(pai.index, channelIndex, v);
|
|
}
|
|
} else if (pai.type === 'sinkInput') {
|
|
if (lockChannelsTogether) {
|
|
props.setSinkInputVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
|
} else {
|
|
props.setSinkInputChannelVolume(pai.index, channelIndex, v);
|
|
}
|
|
} else if (pai.type === 'sourceOutput') {
|
|
if (lockChannelsTogether) {
|
|
props.setSourceOutputVolumes(pai.index, repeat(v, pai.sampleSpec.channels));
|
|
} else {
|
|
props.setSourceOutputChannelVolume(pai.index, channelIndex, v);
|
|
}
|
|
}
|
|
},
|
|
})),
|
|
]);
|
|
});
|
|
|
|
const Icon = connect(
|
|
state => ({
|
|
icons: state.icons,
|
|
}),
|
|
)(({ icons, name, title }) => {
|
|
const src = icons[name];
|
|
|
|
if (!src) {
|
|
return r(React.Fragment);
|
|
}
|
|
|
|
return r.img({
|
|
className: 'node-name-icon',
|
|
src,
|
|
title,
|
|
});
|
|
});
|
|
|
|
const RemoteTunnelInfo = ({ pai }) => {
|
|
const fqdn = path([ 'properties', 'tunnel', 'remote', 'fqdn' ], pai);
|
|
|
|
if (!fqdn) {
|
|
return r(React.Fragment);
|
|
}
|
|
|
|
return r.div({
|
|
className: 'node-tunnel-info',
|
|
}, [
|
|
fqdn,
|
|
]);
|
|
};
|
|
|
|
const DebugText = connect(
|
|
state => ({
|
|
showDebugInfo: state.preferences.showDebugInfo,
|
|
}),
|
|
)(({ dgo, pai, showDebugInfo }) => {
|
|
if (!showDebugInfo) {
|
|
return r(React.Fragment);
|
|
}
|
|
|
|
return r.div({
|
|
style: {
|
|
fontSize: '50%',
|
|
},
|
|
}, [
|
|
JSON.stringify(dgo, null, 2),
|
|
JSON.stringify(pai, null, 2),
|
|
]);
|
|
});
|
|
|
|
const SinkText = connect(
|
|
state => ({
|
|
defaultSinkName: state.pulse[primaryPulseServer].serverInfo.defaultSinkName,
|
|
}),
|
|
)(({ dgo, pai, selected, defaultSinkName }) => r(React.Fragment, [
|
|
r.div({
|
|
className: 'node-name',
|
|
}, [
|
|
defaultSinkName === pai.name && r(React.Fragment, [
|
|
r(Icon, {
|
|
name: 'starred',
|
|
title: 'Default sink',
|
|
}),
|
|
' ',
|
|
]),
|
|
r.span({
|
|
title: pai.name,
|
|
}, pai.description),
|
|
]),
|
|
|
|
r.div({
|
|
className: 'node-main',
|
|
}, [
|
|
r(selected ? VolumeControls : VolumeThumbnail, { pai }),
|
|
]),
|
|
|
|
r(RemoteTunnelInfo, { pai }),
|
|
r(DebugText, { dgo, pai }),
|
|
]));
|
|
|
|
const SourceText = connect(
|
|
state => ({
|
|
defaultSourceName: state.pulse[primaryPulseServer].serverInfo.defaultSourceName,
|
|
}),
|
|
)(({ dgo, pai, selected, defaultSourceName }) => r(React.Fragment, [
|
|
r.div({
|
|
className: 'node-name',
|
|
}, [
|
|
defaultSourceName === pai.name && r(React.Fragment, [
|
|
r(Icon, {
|
|
name: 'starred',
|
|
title: 'Default source',
|
|
}),
|
|
' ',
|
|
]),
|
|
r.span({
|
|
title: pai.name,
|
|
}, pai.description),
|
|
]),
|
|
|
|
r.div({
|
|
className: 'node-main',
|
|
}, [
|
|
r(selected ? VolumeControls : VolumeThumbnail, { pai }),
|
|
]),
|
|
|
|
r(RemoteTunnelInfo, { pai }),
|
|
r(DebugText, { dgo, pai }),
|
|
]));
|
|
|
|
const ClientText = connect(
|
|
state => ({
|
|
modules: state.pulse[primaryPulseServer].infos.modules,
|
|
}),
|
|
)(({ dgo, pai, modules }) => {
|
|
let title = path('properties.application.process.binary'.split('.'), pai);
|
|
|
|
const module = modules[pai.moduleIndex];
|
|
if (module && module.name === 'module-native-protocol-tcp') {
|
|
title = path([ 'properties', 'native-protocol', 'peer' ], pai) || title;
|
|
}
|
|
|
|
return r(React.Fragment, [
|
|
r.div({
|
|
className: 'node-name',
|
|
title,
|
|
}, pai.name),
|
|
r(DebugText, { dgo, pai }),
|
|
]);
|
|
});
|
|
|
|
const ModuleText = ({ dgo, pai }) => r(React.Fragment, [
|
|
r.div({
|
|
className: 'node-name',
|
|
title: path([ 'properties', 'module', 'description' ], pai) || pai.name,
|
|
}, pai.name),
|
|
r(DebugText, { dgo, pai }),
|
|
]);
|
|
|
|
const NodeText = connect(
|
|
(state, { dgo }) => ({
|
|
icons: state.icons,
|
|
pai: dgo.type && getPaiByTypeAndIndex(dgo.type, dgo.index)(state),
|
|
}),
|
|
)(({ dgo, pai, selected, icons }) => {
|
|
if (!pai) {
|
|
return r(React.Fragment);
|
|
}
|
|
|
|
return r('foreignObject', {
|
|
x: -s2,
|
|
y: -s2,
|
|
}, r.div({
|
|
className: 'node-text',
|
|
style: {
|
|
width: size,
|
|
height: size,
|
|
|
|
backgroundImage: (icon => icon && `url(${icon})`)(icons[getPaiIcon(pai)]),
|
|
},
|
|
}, r({
|
|
sink: SinkText,
|
|
source: SourceText,
|
|
client: ClientText,
|
|
module: ModuleText,
|
|
}[dgo.type] || ModuleText, {
|
|
dgo,
|
|
pai,
|
|
selected,
|
|
})));
|
|
});
|
|
|
|
const withStorePassthrough = component => store =>
|
|
(...args) => r(ReduxProvider, { store }, component(...args));
|
|
|
|
const renderNodeText = withStorePassthrough((dgo, i, selected) => {
|
|
return r(NodeText, { dgo, selected });
|
|
});
|
|
|
|
const renderEdge = props => r(Edge, {
|
|
classSet: {
|
|
[props.data.type]: true,
|
|
},
|
|
...props,
|
|
});
|
|
|
|
const EdgeText = connect(
|
|
(state, { dgo }) => ({
|
|
pai: dgo.type && getPaiByTypeAndIndex(dgo.type, dgo.index)(state),
|
|
}),
|
|
)(({ dgo, pai, transform, selected }) => r('foreignObject', {
|
|
transform,
|
|
}, r.div({
|
|
className: 'edge-text',
|
|
style: {
|
|
width: size,
|
|
height: size,
|
|
},
|
|
}, [
|
|
pai && (!selected) && r(VolumeThumbnail, { pai }),
|
|
pai && selected && r(VolumeControls, { pai }),
|
|
r(DebugText, { dgo, pai }),
|
|
])));
|
|
|
|
const renderEdgeText = withStorePassthrough(({ data: dgo, transform, selected }) => {
|
|
return r(EdgeText, { dgo, transform, selected });
|
|
});
|
|
|
|
const layoutEngine = new LayoutEngine();
|
|
|
|
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,
|
|
}, [
|
|
this.props.canSetAsDefault() && r(React.Fragment, [
|
|
r(MenuItem, {
|
|
label: 'Set as default',
|
|
onClick: this.props.onSetAsDefault,
|
|
}),
|
|
r(MenuItem.Separator),
|
|
]),
|
|
|
|
this.props.canDelete() && r(MenuItem, {
|
|
label: 'Delete',
|
|
onClick: this.props.onDelete,
|
|
}),
|
|
]);
|
|
}
|
|
}
|
|
|
|
const backgroundSymbol = Symbol('graph.backgroundSymbol');
|
|
|
|
class Graph extends React.PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.satellitesGraphViewRef = React.createRef();
|
|
|
|
this.state = {
|
|
selected: null,
|
|
moved: null,
|
|
contexted: null,
|
|
|
|
isDraggingNode: false,
|
|
isZooming: false,
|
|
};
|
|
|
|
this._requestedIcons = new Set();
|
|
|
|
Object.assign(this, {
|
|
renderBackground: this.renderBackground.bind(this),
|
|
onBackgroundMouseDown: this.onBackgroundMouseDown.bind(this),
|
|
|
|
onZoomStart: this.onZoomStart.bind(this),
|
|
onZoomEnd: this.onZoomEnd.bind(this),
|
|
|
|
onSelectNode: this.onSelectNode.bind(this),
|
|
onCreateNode: this.onCreateNode.bind(this),
|
|
onUpdateNode: this.onUpdateNode.bind(this),
|
|
onDeleteNode: this.onDeleteNode.bind(this),
|
|
onNodeMouseDown: this.onNodeMouseDown.bind(this),
|
|
onNodeDragStart: this.onNodeDragStart.bind(this),
|
|
onNodeDragEnd: this.onNodeDragEnd.bind(this),
|
|
|
|
onSelectEdge: this.onSelectEdge.bind(this),
|
|
canCreateEdge: this.canCreateEdge.bind(this),
|
|
onCreateEdge: this.onCreateEdge.bind(this),
|
|
onSwapEdge: this.onSwapEdge.bind(this),
|
|
onDeleteEdge: this.onDeleteEdge.bind(this),
|
|
onEdgeMouseDown: this.onEdgeMouseDown.bind(this),
|
|
|
|
onContextMenuClose: this.onContextMenuClose.bind(this),
|
|
|
|
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),
|
|
});
|
|
}
|
|
|
|
static getDerivedStateFromProps(props, state) {
|
|
let edges = map(paoToEdge, flatten(map(values, [
|
|
props.objects.sinkInputs,
|
|
props.objects.sourceOutputs,
|
|
props.derivations.monitorSources,
|
|
])));
|
|
|
|
const connectedNodeKeys = new Set();
|
|
edges.forEach(edge => {
|
|
if (edge.type === 'monitorSource') {
|
|
return;
|
|
}
|
|
|
|
connectedNodeKeys.add(edge.source);
|
|
connectedNodeKeys.add(edge.target);
|
|
});
|
|
|
|
const filteredNodeKeys = new Set();
|
|
|
|
const nodes = filter(node => {
|
|
if ((props.preferences.hideDisconnectedClients && node.type === 'client')
|
|
|| (props.preferences.hideDisconnectedModules && node.type === 'module')
|
|
|| (props.preferences.hideDisconnectedSources && node.type === 'source')
|
|
|| (props.preferences.hideDisconnectedSinks && node.type === 'sink')
|
|
) {
|
|
if (!connectedNodeKeys.has(node.id)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const pai = getPaiByDgoFromInfos(node)(props.infos);
|
|
if (pai) {
|
|
if (props.preferences.hideMonitors
|
|
&& pai.properties.device
|
|
&& pai.properties.device.class === 'monitor'
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (props.preferences.hidePulseaudioApps) {
|
|
const binary = path([ 'properties', 'application', 'process', 'binary' ], pai) || '';
|
|
const name = path([ 'properties', 'application', 'name' ], pai) || '';
|
|
if (binary.startsWith('pavucontrol')
|
|
|| binary.startsWith('kmix')
|
|
|| binary === 'pulseaudio'
|
|
|| name === 'papeaks'
|
|
|| name === 'paclient.js'
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
filteredNodeKeys.add(node.id);
|
|
return true;
|
|
}, map(paoToNode, flatten(map(values, [
|
|
props.objects.sinks,
|
|
props.objects.sources,
|
|
props.objects.clients,
|
|
props.objects.modules,
|
|
]))));
|
|
|
|
edges = filter(edge => {
|
|
if (props.preferences.hideMonitorSourceEdges && edge.type === 'monitorSource') {
|
|
return false;
|
|
}
|
|
|
|
return filteredNodeKeys.has(edge.source) && filteredNodeKeys.has(edge.target);
|
|
}, edges);
|
|
|
|
let { selected, moved, contexted } = state;
|
|
|
|
if (contexted && contexted !== backgroundSymbol && selected !== contexted) {
|
|
contexted = null;
|
|
}
|
|
|
|
if (selected) {
|
|
selected = find(x => x.id === selected.id, nodes)
|
|
|| find(x => x.id === selected.id, edges);
|
|
}
|
|
|
|
if (moved) {
|
|
moved = find(x => x.id === moved.id, nodes)
|
|
|| find(x => x.id === moved.id, edges);
|
|
}
|
|
|
|
if (contexted && contexted !== backgroundSymbol) {
|
|
contexted = find(x => x.id === contexted.id, nodes)
|
|
|| find(x => x.id === contexted.id, edges);
|
|
}
|
|
|
|
return {
|
|
nodes,
|
|
edges,
|
|
|
|
selected,
|
|
moved,
|
|
contexted,
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.getIconPath('starred');
|
|
|
|
this.graphViewElement = document.querySelector('#graph .view-wrapper');
|
|
this.graphViewElement.setAttribute('tabindex', '-1');
|
|
|
|
this.props.connect();
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
forEach(pai => {
|
|
const icon = getPaiIcon(pai);
|
|
if (icon) {
|
|
this.getIconPath(icon);
|
|
}
|
|
}, flatten(map(values, [
|
|
this.props.infos.sinks,
|
|
this.props.infos.sources,
|
|
this.props.infos.clients,
|
|
this.props.infos.modules,
|
|
])));
|
|
}
|
|
|
|
getIconPath(icon) {
|
|
if (!this._requestedIcons.has(icon) && !this.props.icons[icon]) {
|
|
this.props.getIconPath(icon, 128);
|
|
}
|
|
|
|
this._requestedIcons.add(icon);
|
|
}
|
|
|
|
onBackgroundMouseDown(event) {
|
|
if (event.button === 1) {
|
|
this.toggleAllMute(this.props.infos.sinks);
|
|
} else if (event.button === 2) {
|
|
this.setState({
|
|
contexted: backgroundSymbol,
|
|
});
|
|
}
|
|
}
|
|
|
|
onSelectNode(selected) {
|
|
this.setState({ selected });
|
|
}
|
|
|
|
onCreateNode() {
|
|
}
|
|
|
|
onUpdateNode() {
|
|
}
|
|
|
|
onDeleteNode(selected) {
|
|
this.onDelete(selected);
|
|
}
|
|
|
|
onNodeMouseDown(event, data) {
|
|
const pai = getPaiByDgoFromInfos(data)(this.props.infos);
|
|
if (pai && event.button === 1) {
|
|
if (pai.type === 'sink'
|
|
|| pai.type === 'source'
|
|
|| pai.type === 'client'
|
|
|| pai.type === 'module'
|
|
) {
|
|
this.toggleMute(pai);
|
|
}
|
|
} else if (pai && event.button === 2) {
|
|
this.setState({
|
|
selected: data,
|
|
contexted: data,
|
|
});
|
|
}
|
|
}
|
|
|
|
onNodeDragStart() {
|
|
this.setState({
|
|
isDraggingNode: true,
|
|
});
|
|
}
|
|
|
|
onNodeDragEnd() {
|
|
this.setState({
|
|
isDraggingNode: false,
|
|
});
|
|
}
|
|
|
|
onSelectEdge(selected) {
|
|
this.setState({ selected });
|
|
}
|
|
|
|
canCreateEdge(source, target) {
|
|
if (!target) {
|
|
return true;
|
|
}
|
|
|
|
if (source.type === 'source' && target.type === 'sink') {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
onCreateEdge(source, target) {
|
|
const sourcePai = getPaiByDgoFromInfos(source)(this.props.infos);
|
|
const targetPai = getPaiByDgoFromInfos(target)(this.props.infos);
|
|
if (sourcePai && targetPai
|
|
&& source.type === 'source' && target.type === 'sink'
|
|
) {
|
|
this.props.loadModule('module-loopback', `source=${sourcePai.name} sink=${targetPai.name}`);
|
|
} else {
|
|
this.forceUpdate();
|
|
}
|
|
}
|
|
|
|
onSwapEdge(sourceNode, targetNode, edge) {
|
|
if (edge.type === 'sinkInput') {
|
|
this.props.moveSinkInput(edge.index, targetNode.index);
|
|
} else if (edge.type === 'sourceOutput') {
|
|
this.props.moveSourceOutput(edge.index, targetNode.index);
|
|
}
|
|
}
|
|
|
|
onDeleteEdge(selected) {
|
|
this.onDelete(selected);
|
|
}
|
|
|
|
onEdgeMouseDown(event, data) {
|
|
const pai = getPaiByDgoFromInfos(data)(this.props.infos);
|
|
if (pai && event.button === 1) {
|
|
if (pai.type === 'sinkInput'
|
|
|| pai.type === 'sourceOutput'
|
|
) {
|
|
this.toggleMute(pai);
|
|
}
|
|
} else if (pai && event.button === 2) {
|
|
this.setState({
|
|
selected: data,
|
|
contexted: data,
|
|
});
|
|
}
|
|
}
|
|
|
|
toggleAllMute(pais) {
|
|
pais = values(pais);
|
|
const allMuted = all(prop('muted'), pais);
|
|
pais.forEach(pai => this.toggleMute(pai, !allMuted));
|
|
}
|
|
|
|
toggleMute(pai, muted = !pai.muted, sourceBiased = false) {
|
|
if (pai.muted === muted) {
|
|
return;
|
|
}
|
|
|
|
if (pai.type === 'sinkInput') {
|
|
this.props.setSinkInputMuteByIndex(pai.index, muted);
|
|
} else if (pai.type === 'sourceOutput') {
|
|
this.props.setSourceOutputMuteByIndex(pai.index, muted);
|
|
} else if (pai.type === 'sink') {
|
|
if (sourceBiased) {
|
|
const sinkInputs = getSinkSinkInputs(pai)(this.props.store.getState());
|
|
this.toggleAllMute(sinkInputs);
|
|
} else {
|
|
this.props.setSinkMute(pai.index, muted);
|
|
}
|
|
} else if (pai.type === 'source') {
|
|
this.props.setSourceMute(pai.index, muted);
|
|
} else if (pai.type === 'client') {
|
|
if (sourceBiased) {
|
|
const sourceOutputs = getClientSourceOutputs(pai)(this.props.store.getState());
|
|
this.toggleAllMute(sourceOutputs);
|
|
} else {
|
|
const sinkInputs = getClientSinkInputs(pai)(this.props.store.getState());
|
|
this.toggleAllMute(sinkInputs);
|
|
}
|
|
} else if (pai.type === 'module') {
|
|
if (sourceBiased) {
|
|
const sourceOutputs = getModuleSourceOutputs(pai)(this.props.store.getState());
|
|
this.toggleAllMute(sourceOutputs);
|
|
} else {
|
|
const sinkInputs = getModuleSinkInputs(pai)(this.props.store.getState());
|
|
this.toggleAllMute(sinkInputs);
|
|
}
|
|
}
|
|
}
|
|
|
|
onDelete(selected) {
|
|
const pai = getPaiByDgoFromInfos(selected)(this.props.infos);
|
|
|
|
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
|
|
&& pai.moduleIndex >= 0
|
|
) {
|
|
this.props.unloadModuleByIndex(pai.moduleIndex);
|
|
}
|
|
}
|
|
|
|
canContextMenuDelete() {
|
|
return this.state.contexted !== backgroundSymbol;
|
|
}
|
|
|
|
onContextMenuDelete() {
|
|
this.onDelete(this.state.contexted);
|
|
}
|
|
|
|
onContextMenuClose() {
|
|
this.setState({
|
|
contexted: null,
|
|
});
|
|
}
|
|
|
|
canContextMenuSetAsDefault() {
|
|
const pai = getPaiByDgoFromInfos(this.state.contexted)(this.props.infos);
|
|
|
|
if (pai && pai.type === 'sink' && pai.name !== this.props.serverInfo.defaultSinkName) {
|
|
return true;
|
|
}
|
|
|
|
if (pai && pai.type === 'source' && pai.name !== this.props.serverInfo.defaultSourceName) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
setAsDefault(data) {
|
|
const pai = getPaiByDgoFromInfos(data)(this.props.infos);
|
|
|
|
if (pai.type === 'sink') {
|
|
this.props.setDefaultSinkByName(pai.name);
|
|
}
|
|
|
|
if (pai.type === 'source') {
|
|
this.props.setDefaultSourceByName(pai.name);
|
|
}
|
|
}
|
|
|
|
onContextMenuSetAsDefault() {
|
|
this.setAsDefault(this.state.contexted);
|
|
}
|
|
|
|
hotKeySetAsDefault() {
|
|
this.setAsDefault(this.state.selected);
|
|
}
|
|
|
|
focus() {
|
|
this.graphViewElement.focus();
|
|
}
|
|
|
|
onZoomStart() {
|
|
this.setState({
|
|
isZooming: true,
|
|
});
|
|
}
|
|
|
|
onZoomEnd() {
|
|
this.setState({
|
|
isZooming: false,
|
|
});
|
|
}
|
|
|
|
hotKeyEscape() {
|
|
const { moved } = this.state;
|
|
|
|
if (moved) {
|
|
this.setState({
|
|
selected: moved,
|
|
moved: null,
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.setState({
|
|
selected: null,
|
|
});
|
|
}
|
|
|
|
hotKeyMute({ shiftKey: sourceBiased, ctrlKey: all }) {
|
|
if (!this.state.selected) {
|
|
if (sourceBiased) {
|
|
if (all) {
|
|
this.toggleAllMute(this.props.infos.sources);
|
|
} else {
|
|
const defaultSource = getDefaultSourcePai(this.props.store.getState());
|
|
this.toggleMute(defaultSource);
|
|
}
|
|
} else {
|
|
if (all) { // eslint-disable-line no-lonely-if
|
|
this.toggleAllMute(this.props.infos.sinks);
|
|
} else {
|
|
const defaultSink = getDefaultSinkPai(this.props.store.getState());
|
|
this.toggleMute(defaultSink);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const pai = getPaiByDgoFromInfos(this.state.selected)(this.props.infos);
|
|
|
|
if (!pai) {
|
|
return;
|
|
}
|
|
|
|
this.toggleMute(pai, undefined, sourceBiased);
|
|
}
|
|
|
|
_volume(pai, direction) {
|
|
const { lockChannelsTogether, maxVolume, volumeStep } = this.props.preferences;
|
|
|
|
const d = direction === 'up' ? 1 : -1;
|
|
|
|
let newVolumes = map(
|
|
v => clamp(v + (d * (volumeStep * PA_VOLUME_NORM)), 0, maxVolume * PA_VOLUME_NORM),
|
|
pai.channelVolumes,
|
|
);
|
|
|
|
if (lockChannelsTogether) {
|
|
const max = maximum(newVolumes);
|
|
newVolumes = map(() => max, newVolumes);
|
|
}
|
|
|
|
if (pai.type === 'sink') {
|
|
this.props.setSinkVolumes(pai.index, newVolumes);
|
|
} else if (pai.type === 'source') {
|
|
this.props.setSourceVolumes(pai.index, newVolumes);
|
|
} else if (pai.type === 'sinkInput') {
|
|
this.props.setSinkInputVolumes(pai.index, newVolumes);
|
|
} else if (pai.type === 'sourceOutput') {
|
|
this.props.setSourceOutputVolumes(pai.index, newVolumes);
|
|
}
|
|
}
|
|
|
|
_volumeAll(pais, direction) {
|
|
forEach(pai => this._volume(pai, direction), values(pais));
|
|
}
|
|
|
|
_hotKeyVolume(direction) {
|
|
let pai;
|
|
|
|
if (this.state.selected) {
|
|
pai = getPaiByDgoFromInfos(this.state.selected)(this.props.infos);
|
|
} else {
|
|
pai = getDefaultSinkPai(this.props.store.getState());
|
|
}
|
|
|
|
if (!pai) {
|
|
return;
|
|
}
|
|
|
|
if (pai.type === 'client') {
|
|
const sinkInputs = getClientSinkInputs(pai)(this.props.store.getState());
|
|
this._volumeAll(sinkInputs, direction);
|
|
return;
|
|
}
|
|
|
|
if (pai.type === 'module') {
|
|
const sinkInputs = getModuleSinkInputs(pai)(this.props.store.getState());
|
|
this._volumeAll(sinkInputs, direction);
|
|
return;
|
|
}
|
|
|
|
if (![ 'sink', 'source', 'sinkInput', 'sourceOutput' ].includes(pai.type)) {
|
|
return;
|
|
}
|
|
|
|
this._volume(pai, direction);
|
|
}
|
|
|
|
hotKeyVolumeDown() {
|
|
this._hotKeyVolume('down');
|
|
}
|
|
|
|
hotKeyVolumeUp() {
|
|
this._hotKeyVolume('up');
|
|
}
|
|
|
|
_findNextObjectForSelection(object, direction) {
|
|
const { type } = object || { type: 'client' };
|
|
const predicate = selectionObjectTypes.toPulsePredicate(type);
|
|
const candidates = compose(
|
|
sortBy(prop('index')),
|
|
filter(predicate),
|
|
)(this.state.nodes.concat(this.state.edges));
|
|
return (direction === 'up' ? leftOf : rightOf)(object, candidates);
|
|
}
|
|
|
|
hotKeyFocusDown() {
|
|
if (this._hotKeyMovePosition('down')) {
|
|
return;
|
|
}
|
|
|
|
const selected = this._findNextObjectForSelection(this.state.selected, 'down');
|
|
this.setState({ selected });
|
|
}
|
|
|
|
hotKeyFocusUp() {
|
|
if (this._hotKeyMovePosition('up')) {
|
|
return;
|
|
}
|
|
|
|
const selected = this._findNextObjectForSelection(this.state.selected, 'up');
|
|
this.setState({ selected });
|
|
}
|
|
|
|
_findAnyObjectForSelection(types, isBest) {
|
|
let node = null;
|
|
for (const type of types) {
|
|
const predicate = selectionObjectTypes.toPulsePredicate(type);
|
|
node
|
|
= (isBest && find(allPass([ predicate, isBest ]), this.state.nodes))
|
|
|| (isBest && find(allPass([ predicate, isBest ]), this.state.edges))
|
|
|| find(predicate, this.state.nodes)
|
|
|| find(predicate, this.state.edges);
|
|
if (node) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
_focusHorizontal(direction) {
|
|
const { selected } = this.state;
|
|
|
|
if (!selected) {
|
|
this.setState({
|
|
selected: this._findAnyObjectForSelection(direction === 'left' ? [
|
|
'sourceOutput',
|
|
'source',
|
|
] : [
|
|
'sinkInput',
|
|
'sink',
|
|
]),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const next = t => selectionObjectTypes[direction](t);
|
|
const types = scan(
|
|
next,
|
|
next(selectionObjectTypes.fromPulseType(selected.type)),
|
|
range(0, 3),
|
|
);
|
|
|
|
const bestSelectionPredicate = x => null
|
|
|| x.source === selected.id
|
|
|| x.target === selected.id
|
|
|| selected.source === x.id
|
|
|| selected.target === x.id;
|
|
|
|
this.setState({
|
|
selected: this._findAnyObjectForSelection(types, bestSelectionPredicate),
|
|
});
|
|
}
|
|
|
|
hotKeyFocusLeft() {
|
|
if (this._hotKeyMovePosition('left')) {
|
|
return;
|
|
}
|
|
|
|
this._focusHorizontal('left');
|
|
}
|
|
|
|
hotKeyFocusRight() {
|
|
if (this._hotKeyMovePosition('right')) {
|
|
return;
|
|
}
|
|
|
|
this._focusHorizontal('right');
|
|
}
|
|
|
|
_hotKeyMovePosition(direction) {
|
|
const { selected, moved } = this.state;
|
|
|
|
if (!selected
|
|
|| selected !== moved
|
|
|| ![ 'sink', 'source', 'client', 'module' ].includes(moved.type)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const x = direction === 'right' ? 1 : (direction === 'left' ? -1 : 0);
|
|
const y = direction === 'down' ? 1 : (direction === 'up' ? -1 : 0);
|
|
|
|
moved.x += x * (size + (size / 12));
|
|
moved.y += y * (size + (size / 12));
|
|
|
|
this.forceUpdate();
|
|
|
|
return true;
|
|
}
|
|
|
|
hotKeyMove() {
|
|
let { selected, moved } = this.state;
|
|
|
|
if (!selected) {
|
|
return;
|
|
}
|
|
|
|
if (moved) {
|
|
this.onSwapEdge(null, selected, moved);
|
|
this.setState({
|
|
selected: moved,
|
|
moved: null,
|
|
});
|
|
return;
|
|
}
|
|
|
|
moved = selected;
|
|
|
|
if (moved.type === 'sinkInput') {
|
|
selected = find(
|
|
node => node.id !== moved.target && node.type === 'sink',
|
|
this.state.nodes,
|
|
);
|
|
} else if (moved.type === 'sourceOutput') {
|
|
selected = find(
|
|
node => node.id !== moved.target && node.type === 'source',
|
|
this.state.nodes,
|
|
);
|
|
}
|
|
|
|
this.setState({
|
|
selected,
|
|
moved,
|
|
});
|
|
}
|
|
|
|
hotKeyAdd() {
|
|
this.props.openNewGraphObjectModal();
|
|
}
|
|
|
|
onLoadModuleLoopback() {
|
|
this.props.loadModule('module-loopback', '');
|
|
}
|
|
|
|
onLoadModuleCombineSink() {
|
|
this.props.loadModule('module-combine-sink', '');
|
|
}
|
|
|
|
onLoadModuleNullSink() {
|
|
this.props.loadModule('module-null-sink', '');
|
|
}
|
|
|
|
renderBackground() {
|
|
return renderBackground({
|
|
onMouseDown: this.onBackgroundMouseDown,
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const { nodes, edges } = this.state;
|
|
|
|
const satellitesGraphViewState = path(
|
|
[ 'current', 'state' ],
|
|
this.satellitesGraphViewRef,
|
|
);
|
|
|
|
return r(HotKeys, {
|
|
handlers: map(f => bind(f, this), pick(keys(keyMap), this)),
|
|
}, r.div({
|
|
id: 'graph',
|
|
}, [
|
|
!this.props.preferences.hideLiveVolumePeaks && r(Peaks, {
|
|
key: 'peaks',
|
|
nodes: defaultTo([], prop('satelliteNodes', satellitesGraphViewState)),
|
|
edges: defaultTo([], prop('satelliteEdges', satellitesGraphViewState)),
|
|
accommodateGraphAnimation: this.state.isDraggingNode || this.state.isZooming,
|
|
peaks: this.props.peaks,
|
|
}),
|
|
|
|
r(SatellitesGraphView, {
|
|
key: 'graph',
|
|
|
|
ref: this.satellitesGraphViewRef,
|
|
|
|
nodeKey: 'id',
|
|
edgeKey: 'id',
|
|
|
|
nodes,
|
|
edges,
|
|
|
|
selected: this.state.selected,
|
|
moved: this.state.moved,
|
|
|
|
nodeTypes: {},
|
|
nodeSubtypes: {},
|
|
edgeTypes: {},
|
|
|
|
onZoomStart: this.onZoomStart,
|
|
onZoomEnd: this.onZoomEnd,
|
|
|
|
onSelectNode: this.onSelectNode,
|
|
onCreateNode: this.onCreateNode,
|
|
onUpdateNode: this.onUpdateNode,
|
|
onDeleteNode: this.onDeleteNode,
|
|
onNodeMouseDown: this.onNodeMouseDown,
|
|
onNodeDragStart: this.onNodeDragStart,
|
|
onNodeDragEnd: this.onNodeDragEnd,
|
|
|
|
onSelectEdge: this.onSelectEdge,
|
|
canCreateEdge: this.canCreateEdge,
|
|
onCreateEdge: this.onCreateEdge,
|
|
onSwapEdge: this.onSwapEdge,
|
|
onDeleteEdge: this.onDeleteEdge,
|
|
onEdgeMouseDown: this.onEdgeMouseDown,
|
|
|
|
showGraphControls: false,
|
|
|
|
edgeArrowSize: 64,
|
|
|
|
layoutEngine,
|
|
|
|
renderBackground: this.renderBackground,
|
|
|
|
renderDefs,
|
|
|
|
renderNode,
|
|
renderNodeText: renderNodeText(this.props.store),
|
|
|
|
renderEdge,
|
|
renderEdgeText: renderEdgeText(this.props.store),
|
|
}),
|
|
|
|
this.state.contexted && (
|
|
this.state.contexted === backgroundSymbol
|
|
? r(BackgroundContextMenu, {
|
|
key: 'background-context-menu',
|
|
|
|
onClose: this.onContextMenuClose,
|
|
|
|
onLoadModule: this.props.openLoadModuleModal,
|
|
|
|
onLoadModuleLoopback: this.onLoadModuleLoopback,
|
|
onLoadModuleCombineSink: this.onLoadModuleCombineSink,
|
|
onLoadModuleNullSink: this.onLoadModuleNullSink,
|
|
})
|
|
: r(GraphObjectContextMenu, {
|
|
key: 'graph-object-context-menu',
|
|
|
|
onClose: this.onContextMenuClose,
|
|
|
|
canSetAsDefault: this.canContextMenuSetAsDefault,
|
|
onSetAsDefault: this.onContextMenuSetAsDefault,
|
|
|
|
canDelete: this.canContextMenuDelete,
|
|
onDelete: this.onContextMenuDelete,
|
|
})
|
|
),
|
|
]));
|
|
}
|
|
}
|
|
|
|
module.exports = compose(
|
|
forwardRef(),
|
|
|
|
connect(
|
|
state => ({
|
|
serverInfo: state.pulse[primaryPulseServer].serverInfo,
|
|
|
|
objects: state.pulse[primaryPulseServer].objects,
|
|
infos: state.pulse[primaryPulseServer].infos,
|
|
|
|
derivations: {
|
|
monitorSources: getDerivedMonitorSources(state),
|
|
},
|
|
|
|
icons: state.icons,
|
|
|
|
preferences: state.preferences,
|
|
}),
|
|
dispatch => bindActionCreators(omit([
|
|
'serverInfo',
|
|
'unloadModuleByIndex',
|
|
], merge(pulseActions, iconsActions)), dispatch),
|
|
),
|
|
|
|
fromRenderProps(
|
|
ReduxConsumer,
|
|
({ store }) => ({ store }),
|
|
),
|
|
|
|
unforwardRef(),
|
|
)(Graph);
|