diff --git a/components/graph/index.js b/components/graph/index.js index 47ec0f9..b3b3a70 100644 --- a/components/graph/index.js +++ b/components/graph/index.js @@ -26,6 +26,10 @@ const { const { getPaiByTypeAndIndex } = require('../../selectors'); +const { + PA_VOLUME_NORM, +} = require('../../constants/pulse'); + const { GraphView, } = require('./satellites-graph'); @@ -207,6 +211,51 @@ const renderNode = (nodeRef, data, key, selected, hovered) => r({ hovered, }); +const VolumeThumbnail = ({ pai, state }) => { + if (state.preferences.hideVolumeThumbnails) { + return r(React.Fragment); + } + + const volumes = (pai && pai.channelVolumes) || []; + 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, + }, + }, [ + r.line({ + className: 'volume-thumbnail-ruler-line', + x1: padding, + x2: padding, + 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) => r.line({ + className: 'volume-thumbnail-volume-line', + x1: padding, + x2: padding + ((v / PA_VOLUME_NORM) * width), + y1: padding + ((1 + i) * step), + y2: padding + ((1 + i) * step), + })), + ]); +}; + const DebugText = ({ dgo, pai, state }) => r.div({ style: { fontSize: '50%', @@ -218,20 +267,25 @@ const DebugText = ({ dgo, pai, state }) => r.div({ const SinkText = ({ dgo, pai, state }) => r.div([ r.div({ + className: 'node-name', title: pai.name, }, pai.description), + r(VolumeThumbnail, { pai, state }), r(DebugText, { dgo, pai, state }), ]); const SourceText = ({ dgo, pai, state }) => r.div([ r.div({ + className: 'node-name', title: pai.name, }, pai.description), + r(VolumeThumbnail, { pai, state }), r(DebugText, { dgo, pai, state }), ]); const ClientText = ({ dgo, pai, state }) => r.div([ r.div({ + className: 'node-name', title: path('properties.application.process.binary'.split('.'), pai), }, pai.name), r(DebugText, { dgo, pai, state }), @@ -239,6 +293,7 @@ const ClientText = ({ dgo, pai, state }) => r.div([ const ModuleText = ({ dgo, pai, state }) => r.div([ r.div({ + className: 'node-name', title: pai.properties.module.description, }, pai.name), r(DebugText, { dgo, pai, state }), @@ -248,17 +303,11 @@ const renderNodeText = state => dgo => r('foreignObject', { x: -s2, y: -s2, }, r.div({ + className: 'node-text', style: { width: size, height: size, - padding: 2, - - whiteSpace: 'pre', - - backgroundRepeat: 'no-repeat', - backgroundSize: '60%', - backgroundPosition: 'center', backgroundImage: (icon => icon && `url(${icon})`)(state.icons[getPaiIcon(dgoToPai.get(dgo))]), }, }, r({ @@ -279,28 +328,22 @@ const renderEdge = props => r(Edge, { ...props, }); -const renderEdgeText = state => ({ data, transform }) => r('foreignObject', { - transform, -}, r.div({ - style: { - width: size, - height: size, +const renderEdgeText = state => ({ data: dgo, transform }) => { + const pai = dgo.type && getPaiByTypeAndIndex(dgo.type, dgo.index)({ pulse: state }); - padding: 2, - - whiteSpace: 'pre', - - backgroundRepeat: 'no-repeat', - backgroundSize: '60%', - backgroundPosition: 'center', - }, -}, [ - r(DebugText, { - dgo: data, - pai: data.type && getPaiByTypeAndIndex(data.type, data.index)({ pulse: state }), - state, - }), -])); + return r('foreignObject', { + transform, + }, r.div({ + className: 'edge-text', + style: { + width: size, + height: size, + }, + }, [ + pai && r(VolumeThumbnail, { pai, state }), + r(DebugText, { dgo, pai, state }), + ])); +}; class Graph extends React.Component { constructor(props) { @@ -334,16 +377,16 @@ class Graph extends React.Component { ); } + componentDidMount() { + this.getIconPath('audio-volume-muted'); + } + componentDidUpdate() { forEach(pai => { const icon = getPaiIcon(pai); - if (!icon) { - return; + if (icon) { + this.getIconPath(icon); } - if (!this._requestedIcons.has(icon) && !this.props.icons[icon]) { - this.props.getIconPath(icon, 128); - } - this._requestedIcons.add(icon); }, flatten(map(values, [ this.props.infos.sinks, this.props.infos.sources, @@ -352,6 +395,13 @@ class Graph extends React.Component { ]))); } + getIconPath(icon) { + if (!this._requestedIcons.has(icon) && !this.props.icons[icon]) { + this.props.getIconPath(icon, 128); + } + this._requestedIcons.add(icon); + } + onSelectNode(selected) { this.setState({ selected }); } diff --git a/components/preferences/index.js b/components/preferences/index.js index ae4609e..40915bd 100644 --- a/components/preferences/index.js +++ b/components/preferences/index.js @@ -77,6 +77,13 @@ const Preferences = withStateHandlers( }, 'Hide pulseaudio applications'), ]), + r.div([ + r(Checkbox, { + checked: props.preferences.hideVolumeThumbnails, + onChange: () => props.actions.toggle('hideVolumeThumbnails'), + }, 'Hide volume thumbnails'), + ]), + r.div([ r(Checkbox, { checked: props.preferences.showDebugInfo, diff --git a/constants/pulse.js b/constants/pulse.js index 062a809..f0074bf 100644 --- a/constants/pulse.js +++ b/constants/pulse.js @@ -1,4 +1,6 @@ +const PA_VOLUME_NORM = 0x10000; + const things = [ { method: 'getModules', type: 'module', @@ -30,5 +32,7 @@ const things = [ { } ]; module.exports = { + PA_VOLUME_NORM, + things, }; diff --git a/index.css b/index.css index 44de010..36d693e 100644 --- a/index.css +++ b/index.css @@ -71,30 +71,33 @@ button:active { fill: var(--themeSelectedBgColor); } -.view-wrapper .sourceOutput .edge { +.view-wrapper .edge.edge { + marker-end: none; +} +.view-wrapper .sourceOutput .edge-path { marker-end: url(#my-source-arrow); } -.view-wrapper .sourceOutput .edge.selected { +.view-wrapper .sourceOutput .selected .edge-path { marker-end: url(#my-source-arrow-selected); } -.view-wrapper .sinkInput .edge { +.view-wrapper .sinkInput .edge-path { marker-end: url(#my-sink-arrow); } -.view-wrapper .sinkInput .edge.selected { +.view-wrapper .sinkInput .selected .edge-path { marker-end: url(#my-sink-arrow-selected); } -#edge-custom-container .edge { +#edge-custom-container .edge-path { marker-end: none; } .view-wrapper .graph .edge { - stroke: var(--successColor); + stroke: var(--borders); } .view-wrapper .graph .arrow { - fill: var(--successColor); + fill: var(--borders); } .preferences { @@ -125,3 +128,47 @@ button:active { .view-wrapper .graph .edge-mouse-handler { stroke-width: 30px; } + +.node { + cursor: pointer; +} + +.node-text, .edge-text { + pointer-events: none; + + padding: 2; + + white-space: pre; + + background-repeat: no-repeat; + background-size: 60%; + background-position: center; +} + +.node-name { + overflow: hidden; + text-overflow: ellipsis; +} + +.volume-thumbnail-ruler-line { + stroke-width: 2px; + stroke: var(--borders); +} +.node.selected .volume-thumbnail-ruler-line { + stroke: var(--themeSelectedFgColor) +} +.volume-thumbnail-volume-line { + stroke-width: 2px; + stroke: var(--successColor); +} +.node.selected .volume-thumbnail-volume-line { + stroke: var(--themeSelectedFgColor) +} + +.volume-thumbnail-muted .volume-thumbnail-volume-line { + stroke: var(--borders) +} + +.edge-text { + pointer-events: none; +} diff --git a/reducers/preferences.js b/reducers/preferences.js index e2f0695..15d9698 100644 --- a/reducers/preferences.js +++ b/reducers/preferences.js @@ -16,6 +16,8 @@ const initialState = { hideMonitors: false, hidePulseaudioApps: true, + hideVolumeThumbnails: false, + showDebugInfo: false, };