Skip to content

Commit fa1a968

Browse files
Aron-dxdItsLemmy
andauthored
feat(widgets/volume): add "Hide When Inactive" toggle to volume widget (#3927)
* Add "Hide When Inactive" toggle to volume widget * fix(volume-widget): fix clang-tidy warning and hide_when_inactive output behaviour * fix(volume-widget): trigger rebuildState on PW_NODE_CHANGE_MASK_STATE for programs streams. * feat(widgets/volume): add input-only "hide when inactive" toggle --------- Co-authored-by: Lemmy <studio@quadbyte.net>
1 parent 9c666da commit fa1a968

5 files changed

Lines changed: 69 additions & 2 deletions

File tree

assets/translations/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3522,6 +3522,10 @@
35223522
"description": "Hide the battery widget when fully charged",
35233523
"label": "Hide When Full"
35243524
},
3525+
"hide-when-inactive": {
3526+
"description": "Hide the microphone widget while no application is capturing audio",
3527+
"label": "Hide When Inactive"
3528+
},
35253529
"hide-when-no-connected-device": {
35263530
"description": "Hide the Bluetooth widget when no device is connected",
35273531
"label": "Hide When No Connected Device"

src/shell/bar/widgets/volume_widget.cpp

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ VolumeWidget::VolumeWidget(PipeWireService* audio, EasyEffectsService* easyEffec
2121
m_muteColor(options.muteColor), m_glyphOverride(std::move(options.glyph)),
2222
m_muteGlyphOverride(std::move(options.muteGlyph)),
2323
m_effectsProfileGlyphs(std::move(options.effectsProfileGlyphs)),
24-
m_customImage(widget_custom_image::fromConfig(options.customImage, options.customImageColorize)) {}
24+
m_customImage(widget_custom_image::fromConfig(options.customImage, options.customImageColorize)),
25+
m_hideWhenInactive(options.hideWhenInactive && options.device == VolumeWidgetTarget::Input) {}
2526

2627
void VolumeWidget::create() {
2728
auto area = ui::inputArea({});
@@ -94,6 +95,16 @@ void VolumeWidget::doLayout(Renderer& renderer, float containerWidth, float cont
9495

9596
void VolumeWidget::doUpdate(Renderer& renderer) { syncState(renderer); }
9697

98+
void VolumeWidget::syncWidgetVisibility(bool showWidget) {
99+
if (Node* rootNode = root(); rootNode != nullptr) {
100+
if (rootNode->visible() != showWidget || rootNode->participatesInLayout() != showWidget) {
101+
rootNode->setVisible(showWidget);
102+
rootNode->setParticipatesInLayout(showWidget);
103+
requestUpdate();
104+
}
105+
}
106+
}
107+
97108
void VolumeWidget::syncState(Renderer& renderer) {
98109
if (m_audio == nullptr || (m_glyph == nullptr && m_image == nullptr) || m_label == nullptr) {
99110
return;
@@ -106,18 +117,32 @@ void VolumeWidget::syncState(Renderer& renderer) {
106117
m_target == VolumeWidgetTarget::Input ? AudioEffectsProfileKind::Input : AudioEffectsProfileKind::Output;
107118
const std::string effectsProfile =
108119
m_easyEffects != nullptr ? m_easyEffects->activeEffectsProfile(kind) : std::string{};
120+
// Reuses the privacy signal: a microphone capture exists while an application is linked to a
121+
// source. Skipped entirely for the output widget, where m_hideWhenInactive is always false.
122+
const bool micActive =
123+
m_hideWhenInactive && std::ranges::any_of(m_audio->privacyState().captures, [](const PrivacyCapture& capture) {
124+
return capture.kind == PrivacyCaptureKind::Microphone;
125+
});
109126

110127
if (volume == m_lastVolume
111128
&& muted == m_lastMuted
112129
&& m_isVertical == m_lastVertical
113-
&& effectsProfile == m_lastEffectsProfile) {
130+
&& effectsProfile == m_lastEffectsProfile
131+
&& micActive == m_lastMicActive) {
114132
return;
115133
}
116134

117135
m_lastVolume = volume;
118136
m_lastMuted = muted;
119137
m_lastVertical = m_isVertical;
120138
m_lastEffectsProfile = effectsProfile;
139+
m_lastMicActive = micActive;
140+
141+
const bool showWidget = !m_hideWhenInactive || micActive;
142+
syncWidgetVisibility(showWidget);
143+
if (!showWidget) {
144+
return;
145+
}
121146

122147
if (m_image != nullptr) {
123148
widget_custom_image::sync(

src/shell/bar/widgets/volume_widget.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class VolumeWidget : public Widget {
2929
std::string customImage;
3030
bool customImageColorize = false;
3131
bool showLabel = true;
32+
bool hideWhenInactive = false;
3233
ColorSpec muteColor = colorSpecFromRole(ColorRole::Error);
3334
};
3435

@@ -40,6 +41,7 @@ class VolumeWidget : public Widget {
4041
void doLayout(Renderer& renderer, float containerWidth, float containerHeight) override;
4142
void doUpdate(Renderer& renderer) override;
4243
void syncState(Renderer& renderer);
44+
void syncWidgetVisibility(bool showWidget);
4345
[[nodiscard]] std::string glyphName(float volume, bool muted, const std::string& effectsProfile = {}) const;
4446

4547
PipeWireService* m_audio = nullptr;
@@ -51,6 +53,8 @@ class VolumeWidget : public Widget {
5153
std::string m_muteGlyphOverride;
5254
std::unordered_map<std::string, std::string> m_effectsProfileGlyphs;
5355
WidgetCustomImage m_customImage;
56+
// Only ever true for the input widget: "inactive" means no application is capturing audio.
57+
bool m_hideWhenInactive = false;
5458
Glyph* m_glyph = nullptr;
5559
Image* m_image = nullptr;
5660
Label* m_label = nullptr;
@@ -59,4 +63,5 @@ class VolumeWidget : public Widget {
5963
bool m_lastMuted = false;
6064
bool m_isVertical = false;
6165
bool m_lastVertical = false;
66+
bool m_lastMicActive = false;
6267
};

src/shell/bar/widgets/volume_widget_definition.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ const noctalia::bar::WidgetDefinition<VolumeWidget::Options>& volumeWidgetDefini
5252
field<&Options::muteColor>({
5353
.key = "mute_color",
5454
}),
55+
field<&Options::hideWhenInactive>({
56+
.key = "hide_when_inactive",
57+
.presentation =
58+
settings::WidgetSettingPresentation{
59+
.visibleWhen = settings::WidgetSettingVisibility{"device", {"input"}},
60+
},
61+
}),
5562
},
5663
.glyph = [](const Options& options) -> std::string {
5764
if (!options.glyph.empty()) {

tests/widget_definition_test.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,32 @@ int main() {
274274
if (pickerGlyph("network-download") != "download") {
275275
fail("sysmon", "network receive stat did not select the download picker glyph");
276276
}
277+
278+
// hide_when_inactive keys off microphone capture, so the settings UI must only offer it on a
279+
// volume widget bound to the input device.
280+
WidgetConfig outputVolume;
281+
outputVolume.type = "volume";
282+
glyphConfig.widgets.emplace("speaker", std::move(outputVolume));
283+
const auto hideWhenInactiveVisible = [&](std::string_view widgetName) {
284+
const WidgetConfig& widget = glyphConfig.widgets.at(std::string(widgetName));
285+
const auto specs = settings::widgetSettingSpecs("volume", &widget, "", false);
286+
const auto spec = std::ranges::find(specs, "hide_when_inactive", [](const settings::WidgetSettingSpec& candidate) {
287+
return std::string_view(candidate.schema.key);
288+
});
289+
if (spec == specs.end()) {
290+
fail("volume", "hide_when_inactive is missing from the settings specs");
291+
return false;
292+
}
293+
return settings::widgetSettingIsVisible(
294+
glyphConfig, widgetName, *spec, specs, settings::WidgetSettingCapabilities{}
295+
);
296+
};
297+
if (!hideWhenInactiveVisible("mic")) {
298+
fail("volume", "hide_when_inactive is hidden on an input widget");
299+
}
300+
if (hideWhenInactiveVisible("speaker")) {
301+
fail("volume", "hide_when_inactive is offered on an output widget");
302+
}
277303
checkDefinition("wallpaper", wallpaperWidgetDefinition);
278304
checkDefinition("weather", weatherWidgetDefinition);
279305

0 commit comments

Comments
 (0)