volume: auto-show per-app and device selector, remove config options

parent 113863fd
.widget-volume {
padding: 0px 10px;
padding: 0;
margin: 5px 15px;
border-radius: var(--window-radius);
......@@ -8,26 +8,22 @@
box-shadow: 0px 0px 10px var(--headerbar-shade-color);
border: 1px solid var(--border-color);
}
&>box {
padding: 0 10px;
margin: 0 0 0 3px;
}
}
.widget-volume .image-button {
background: transparent;
}
.widget-volume>box {
margin: 0 0 0 3px;
}
.widget-volume>box>button {}
.widget-volume revealer {
padding: 0;
margin: 0;
}
.per-app-volume {
background: transparent;
padding: 0;
padding: 0 10px;
margin: 0;
border-radius: var(--border-radius);
}
......@@ -35,3 +31,7 @@
.per-app-volume row {
background: transparent;
}
.sink-selector {
padding: 0;
}
......@@ -339,9 +339,6 @@
"^slider(#[a-zA-Z0-9_-]{1,}){0,1}?$": {
"$ref": "#/widgets/slider"
},
"^volume(#[a-zA-Z0-9_-]{1,}){0,1}?$": {
"$ref": "#/widgets/volume"
},
"^backlight(#[a-zA-Z0-9_-]{1,}){0,1}?$": {
"$ref": "#/widgets/backlight"
},
......@@ -484,59 +481,6 @@
}
}
},
"volume": {
"type": "object",
"description": "Slider to control pulse volume",
"additionalProperties": false,
"properties": {
"show-per-app": {
"type": "boolean",
"default": false,
"description": "Show per app volume control"
},
"show-per-app-icon": {
"type": "boolean",
"default": true,
"description": "Show application icon in per app control"
},
"show-per-app-label": {
"type": "boolean",
"default": false,
"description": "Show application name in per app control"
},
"expand-per-app": {
"type": "boolean",
"default": false,
"description": "If the per app section should start expanded"
},
"empty-list-label": {
"type": "string",
"default": "No active sink input",
"description": "Text displayed when there are not active sink inputs"
},
"expand-button-label": {
"type": "string",
"default": "⇧",
"description": "Label displayed on button to show per app volume control"
},
"collapse-button-label": {
"type": "string",
"default": "⇩",
"description": "Label displayed on button to hide per app volume control"
},
"animation-type": {
"type": "string",
"default": "slide_down",
"description": "Animation type for menu",
"enum": ["slide_down", "slide_up", "none"]
},
"animation-duration": {
"type": "integer",
"default": 250,
"description": "Duration of animation in milliseconds"
}
}
},
"backlight": {
"type": "object",
"description": "Unified brightness control widget. Auto-detects display backlights (/sys/class/backlight), DDC/CI monitors, and keyboard backlights (/sys/class/leds/*kbd*).",
......
......@@ -2,21 +2,16 @@ namespace XimperShellNotificationCenter.Widgets {
public class SinkInputRow : Gtk.ListBoxRow {
Gtk.Box container;
Gtk.Image icon = new Gtk.Image ();
Gtk.Label label = new Gtk.Label (null);
Gtk.Scale scale = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
Gtk.Scale scale = new Gtk.Scale.with_range (
Gtk.Orientation.HORIZONTAL, 0, 100, 1);
public unowned PulseSinkInput sink_input;
private unowned PulseDaemon client;
private bool show_per_app_icon;
private bool show_per_app_label;
public SinkInputRow (PulseSinkInput sink_input, PulseDaemon client,
bool show_per_app_icon, bool show_per_app_label) {
public SinkInputRow (PulseSinkInput sink_input,
PulseDaemon client) {
this.client = client;
this.show_per_app_icon = show_per_app_icon;
this.show_per_app_label = show_per_app_label;
set_activatable (false);
......@@ -25,49 +20,41 @@ namespace XimperShellNotificationCenter.Widgets {
scale.draw_value = false;
scale.set_hexpand (true);
container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
if (show_per_app_icon) {
container.append (icon);
}
if (show_per_app_label) {
container.append (label);
}
container = new Gtk.Box (
Gtk.Orientation.HORIZONTAL, 0);
container.append (icon);
container.append (scale);
set_child (container);
scale.value_changed.connect (() => {
client.set_sink_input_volume (sink_input, (float) scale.get_value ());
scale.tooltip_text = ((int) scale.get_value ()).to_string ();
client.set_sink_input_volume (
sink_input, (float) scale.get_value ());
scale.tooltip_text =
((int) scale.get_value ()).to_string ();
});
}
public void update (PulseSinkInput sink_input) {
this.sink_input = sink_input;
if (show_per_app_icon) {
string icon_name;
if (sink_input.application_icon_name != null) {
icon_name = sink_input.application_icon_name;
} else {
string icon_name = "application-x-executable";
if (sink_input.application_icon_name != null) {
icon_name = sink_input.application_icon_name;
} else if (sink_input.application_binary != null) {
var theme = Gtk.IconTheme.get_for_display (
Gdk.Display.get_default ());
if (theme.has_icon (
sink_input.application_binary)) {
icon_name = sink_input.application_binary;
}
var theme = Gtk.IconTheme.get_for_display (Gdk.Display.get_default ());
if (theme.has_icon (icon_name)) {
icon.set_from_icon_name (icon_name);
} else {
icon.set_from_icon_name ("application-x-executable");
}
}
if (show_per_app_label) {
label.set_text (this.sink_input.name);
}
icon.set_from_icon_name (icon_name);
icon.set_tooltip_text (sink_input.name);
scale.set_value (sink_input.volume);
scale.tooltip_text = ((int) scale.get_value ()).to_string ();
scale.tooltip_text =
((int) scale.get_value ()).to_string ();
}
}
}
......@@ -6,44 +6,43 @@ namespace XimperShellNotificationCenter.Widgets {
}
}
Gtk.Box main_volume_slider_container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
Gtk.Box main_volume_slider_container = new Gtk.Box (
Gtk.Orientation.HORIZONTAL, 0);
Gtk.Image icon_widget = new Gtk.Image ();
Gtk.Scale slider = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 100, 1);
Gtk.Scale slider = new Gtk.Scale.with_range (
Gtk.Orientation.HORIZONTAL, 0, 100, 1);
// Per app volume control
Gtk.ListBox levels_listbox;
IterListBoxController list_box_controller;
// Expandable section
Gtk.ToggleButton reveal_button;
Gtk.Revealer revealer;
Gtk.Label no_sink_inputs_label;
string empty_label;
string ?expand_label = null;
string ?collapse_label = null;
Gtk.Box revealer_content;
// Per app volume
Gtk.ListBox levels_listbox;
IterListBoxController list_box_controller;
Gtk.RevealerTransitionType revealer_type = Gtk.RevealerTransitionType.SLIDE_DOWN;
int revealer_duration = 250;
// Sink selector
Gtk.Separator sink_separator;
Gtk.Box sink_list_box;
uint sink_rebuild_timeout = 0;
private PulseDevice ?default_sink = null;
private PulseDaemon client = new PulseDaemon ();
private bool show_per_app = false;
private bool show_per_app_icon = true;
private bool show_per_app_label = false;
private bool expand_per_app = false;
construct {
this.client.change_default_device.connect (default_device_changed);
this.client.change_default_device.connect (
default_device_changed);
icon_widget.set_from_icon_name ("audio-volume-high-symbolic");
icon_widget.set_from_icon_name (
"audio-volume-high-symbolic");
slider.value_changed.connect (() => {
if (default_sink != null) {
this.client.set_device_volume (
default_sink,
(float) slider.get_value ());
slider.tooltip_text = ((int) slider.get_value ()).to_string ();
slider.tooltip_text =
((int) slider.get_value ()).to_string ();
}
update_volume_icon ((int) slider.get_value ());
});
......@@ -51,131 +50,93 @@ namespace XimperShellNotificationCenter.Widgets {
public Volume (string suffix) {
base (suffix);
empty_label = _("No active sink input");
Json.Object ?config = get_config (this);
if (config != null) {
bool show_per_app_found;
bool ?show_per_app = get_prop<bool> (config, "show-per-app",
out show_per_app_found);
if (show_per_app_found) {
this.show_per_app = show_per_app;
}
bool show_per_app_icon_found;
bool ?show_per_app_icon = get_prop<bool> (config, "show-per-app-icon",
out show_per_app_icon_found);
if (show_per_app_icon_found) {
this.show_per_app_icon = show_per_app_icon;
}
bool show_per_app_label_found;
bool ?show_per_app_label = get_prop<bool> (config, "show-per-app-label",
out show_per_app_label_found);
if (show_per_app_label_found) {
this.show_per_app_label = show_per_app_label;
}
bool expand_per_app_found;
bool ?expand_per_app = get_prop<bool> (config, "expand-per-app",
out expand_per_app_found);
if (expand_per_app_found) {
this.expand_per_app = expand_per_app;
}
string ?el = get_prop<string> (config, "empty-list-label");
if (el != null) {
empty_label = el;
}
string ?l1 = get_prop<string> (config, "expand-button-label");
if (l1 != null) {
expand_label = l1;
}
string ?l2 = get_prop<string> (config, "collapse-button-label");
if (l2 != null) {
collapse_label = l2;
}
revealer_duration = int.max (0, get_prop<int> (config, "animation-duration"));
if (revealer_duration == 0) {
revealer_duration = 250;
}
string ?animation_type = get_prop<string> (config, "animation-type");
if (animation_type != null) {
switch (animation_type) {
default :
case "none" :
revealer_type = Gtk.RevealerTransitionType.NONE;
break;
case "slide_up":
revealer_type = Gtk.RevealerTransitionType.SLIDE_UP;
break;
case "slide_down":
revealer_type = Gtk.RevealerTransitionType.SLIDE_DOWN;
break;
}
}
}
this.orientation = Gtk.Orientation.VERTICAL;
// Main slider
slider.draw_value = false;
slider.set_hexpand (true);
main_volume_slider_container.append (icon_widget);
main_volume_slider_container.append (slider);
append (main_volume_slider_container);
if (show_per_app) {
revealer = new Gtk.Revealer ();
revealer.transition_type = revealer_type;
revealer.transition_duration = revealer_duration;
levels_listbox = new Gtk.ListBox ();
levels_listbox.add_css_class ("per-app-volume");
levels_listbox.set_activate_on_single_click (true);
levels_listbox.set_selection_mode (Gtk.SelectionMode.NONE);
revealer.set_child (levels_listbox);
list_box_controller = new IterListBoxController (levels_listbox);
if (this.client.active_sinks.size == 0) {
no_sink_inputs_label = new Gtk.Label (empty_label);
list_box_controller.append (no_sink_inputs_label);
}
foreach (var item in this.client.active_sinks.values) {
SinkInputRow row = new SinkInputRow (item, client,
show_per_app_icon,
show_per_app_label);
list_box_controller.append (row);
}
this.client.change_active_sink.connect (active_sink_change);
this.client.new_active_sink.connect (active_sink_added);
this.client.remove_active_sink.connect (active_sink_removed);
reveal_button = new Gtk.ToggleButton ();
reveal_button.set_active (expand_per_app);
revealer.set_reveal_child (expand_per_app);
// Reveal button
reveal_button = new Gtk.ToggleButton ();
set_button_icon ();
reveal_button.toggled.connect (() => {
set_button_icon ();
reveal_button.toggled.connect (() => {
set_button_icon ();
revealer.set_reveal_child (reveal_button.active);
if (reveal_button.active) {
add_css_class ("expanded");
} else {
remove_css_class ("expanded");
}
});
if (expand_per_app) {
revealer.set_reveal_child (reveal_button.active);
if (reveal_button.active) {
add_css_class ("expanded");
} else {
remove_css_class ("expanded");
}
main_volume_slider_container.append (reveal_button);
append (revealer);
});
main_volume_slider_container.append (reveal_button);
append (main_volume_slider_container);
// Revealer with per-app + sink selector
revealer = new Gtk.Revealer ();
revealer.transition_type =
Gtk.RevealerTransitionType.SLIDE_DOWN;
revealer.transition_duration = 200;
revealer_content = new Gtk.Box (
Gtk.Orientation.VERTICAL, 0);
revealer.set_child (revealer_content);
// Per-app volume list
levels_listbox = new Gtk.ListBox ();
levels_listbox.add_css_class ("per-app-volume");
levels_listbox.set_activate_on_single_click (true);
levels_listbox.set_selection_mode (
Gtk.SelectionMode.NONE);
list_box_controller = new IterListBoxController (
levels_listbox);
foreach (var item in this.client.active_sinks.values) {
SinkInputRow row = new SinkInputRow (
item, client);
list_box_controller.append (row);
}
this.client.change_active_sink.connect (
active_sink_change);
this.client.new_active_sink.connect (
active_sink_added);
this.client.remove_active_sink.connect (
active_sink_removed);
revealer_content.append (levels_listbox);
// Separator between per-app and sinks
sink_separator = new Gtk.Separator (
Gtk.Orientation.HORIZONTAL);
sink_separator.set_margin_top (8);
sink_separator.set_margin_bottom (8);
sink_separator.set_visible (false);
revealer_content.append (sink_separator);
// Sink selector
sink_list_box = new Gtk.Box (
Gtk.Orientation.VERTICAL, 0);
sink_list_box.add_css_class ("sink-selector");
sink_list_box.set_margin_start (4);
sink_list_box.set_margin_end (4);
sink_list_box.set_margin_top (4);
sink_list_box.set_margin_bottom (4);
sink_list_box.set_visible (false);
revealer_content.append (sink_list_box);
append (revealer);
// Device signals
this.client.new_device.connect (on_device_changed);
this.client.change_device.connect (
on_device_changed);
this.client.remove_device.connect (
on_device_changed);
update_reveal_visibility ();
}
private void update_volume_icon (int volume) {
......@@ -194,18 +155,37 @@ namespace XimperShellNotificationCenter.Widgets {
private void set_button_icon () {
if (!reveal_button.active) {
if (expand_label == null) {
reveal_button.set_icon_name ("ximper-shell-notification-center-up-small-symbolic");
} else {
reveal_button.set_label (expand_label);
}
reveal_button.set_icon_name (
"ximper-shell-notification-center-up-small-symbolic");
} else {
if (collapse_label == null) {
reveal_button.set_icon_name ("ximper-shell-notification-center-down-small-symbolic");
} else {
reveal_button.set_label (collapse_label);
reveal_button.set_icon_name (
"ximper-shell-notification-center-down-small-symbolic");
}
}
private void update_reveal_visibility () {
bool has_apps = client.active_sinks.size > 0;
bool has_sinks = count_output_sinks () > 1;
// Show/hide components
levels_listbox.set_visible (has_apps);
sink_separator.set_visible (has_apps && has_sinks);
// Show reveal button only if there's something
reveal_button.set_visible (has_apps || has_sinks);
}
private int count_output_sinks () {
int count = 0;
foreach (var device in client.sinks.values) {
if (device.removed) continue;
if (device.direction
!= PulseAudio.Direction.OUTPUT) {
continue;
}
count++;
}
return count;
}
public override void on_cc_visibility_change (bool val) {
......@@ -213,24 +193,23 @@ namespace XimperShellNotificationCenter.Widgets {
this.client.start ();
} else {
this.client.close ();
}
if (show_per_app) {
reveal_button.set_active (expand_per_app);
reveal_button.set_active (false);
}
}
private void default_device_changed (PulseDevice device) {
if (device != null && device.direction == PulseAudio.Direction.OUTPUT) {
if (device != null
&& device.direction
== PulseAudio.Direction.OUTPUT) {
this.default_sink = device;
slider.set_value (device.volume);
}
}
private void active_sink_change (PulseSinkInput sink) {
foreach (unowned Gtk.Widget row in list_box_controller.get_children ()) {
if (!(row is SinkInputRow)) {
continue;
}
foreach (unowned Gtk.Widget row
in list_box_controller.get_children ()) {
if (!(row is SinkInputRow)) continue;
var s = (SinkInputRow) row;
if (s.sink_input.cmp (sink)) {
s.update (sink);
......@@ -240,31 +219,128 @@ namespace XimperShellNotificationCenter.Widgets {
}
private void active_sink_added (PulseSinkInput sink) {
// one element added -> remove the empty label
if (this.client.active_sinks.size == 1) {
var label = levels_listbox.get_first_child ();
list_box_controller.remove ((Gtk.Widget) label);
}
SinkInputRow row = new SinkInputRow (sink, client,
show_per_app_icon,
show_per_app_label);
SinkInputRow row = new SinkInputRow (
sink, client);
list_box_controller.append (row);
update_reveal_visibility ();
}
private void active_sink_removed (PulseSinkInput sink) {
foreach (unowned Gtk.Widget row in list_box_controller.get_children ()) {
if (!(row is SinkInputRow)) {
continue;
}
foreach (unowned Gtk.Widget row
in list_box_controller.get_children ()) {
if (!(row is SinkInputRow)) continue;
var s = (SinkInputRow) row;
if (s.sink_input.cmp (sink)) {
list_box_controller.remove (row);
break;
}
}
if (levels_listbox.get_first_child () == null) {
list_box_controller.append (no_sink_inputs_label);
update_reveal_visibility ();
}
private static string get_sink_icon (string ?name) {
if (name == null || name.length == 0) {
return "audio-card-symbolic";
}
var theme = Gtk.IconTheme.get_for_display (
Gdk.Display.get_default ());
string[] candidates = { name, name + "-symbolic" };
string shortened = name;
while (shortened.last_index_of ("-") > 0) {
shortened = shortened.substring (
0, shortened.last_index_of ("-"));
candidates += shortened;
candidates += shortened + "-symbolic";
}
foreach (string candidate in candidates) {
if (theme.has_icon (candidate)) {
return candidate;
}
}
return "audio-card-symbolic";
}
private void on_device_changed (PulseDevice device) {
if (sink_rebuild_timeout != 0) {
Source.remove (sink_rebuild_timeout);
}
sink_rebuild_timeout = Timeout.add (100, () => {
sink_rebuild_timeout = 0;
rebuild_sink_list ();
return Source.REMOVE;
});
}
private void rebuild_sink_list () {
// Clear existing
for (var child = sink_list_box.get_first_child ();
child != null;) {
var next = child.get_next_sibling ();
sink_list_box.remove (child);
child = next;
}
// Collect OUTPUT sinks
int count = 0;
foreach (var device in client.sinks.values) {
if (device.removed) continue;
if (device.direction
!= PulseAudio.Direction.OUTPUT) {
continue;
}
count++;
var row = new Gtk.Box (
Gtk.Orientation.HORIZONTAL, 8);
row.add_css_class ("sink-row");
string sink_icon = get_sink_icon (
device.icon_name);
var row_icon = new Gtk.Image.from_icon_name (
sink_icon);
string ?name = device.get_display_name ();
if (name == null || name == "") {
name = device.device_description
?? "Unknown";
}
var row_label = new Gtk.Label (name);
row_label.set_ellipsize (
Pango.EllipsizeMode.END);
row_label.set_hexpand (true);
row_label.set_halign (Gtk.Align.START);
row.append (row_icon);
row.append (row_label);
bool is_active = default_sink != null
&& device.device_name
== default_sink.device_name
&& device.port_name
== default_sink.port_name;
if (is_active) {
var check = new Gtk.Image.from_icon_name (
"object-select-symbolic");
row.append (check);
}
var btn = new Gtk.Button ();
btn.set_child (row);
btn.add_css_class ("flat");
btn.clicked.connect (() => {
client.set_default_device.begin (device);
});
sink_list_box.append (btn);
}
bool show = count > 1;
sink_list_box.set_visible (show);
update_reveal_visibility ();
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment