diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 4419331..8ac59d2 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -7,6 +7,8 @@ ui/window.ui ui/help-overlay.ui ui/output-dropdown.ui + ui/sinkbox.ui + ui/outputbox.ui ../icons/com.saivert.pwvucontrol.svg diff --git a/data/resources/ui/outputbox.ui b/data/resources/ui/outputbox.ui new file mode 100644 index 0000000..954cadb --- /dev/null +++ b/data/resources/ui/outputbox.ui @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/data/resources/ui/sinkbox.ui b/data/resources/ui/sinkbox.ui new file mode 100644 index 0000000..8a9f14a --- /dev/null +++ b/data/resources/ui/sinkbox.ui @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/data/resources/ui/volumebox.ui b/data/resources/ui/volumebox.ui index 1f980ba..e52da45 100644 --- a/data/resources/ui/volumebox.ui +++ b/data/resources/ui/volumebox.ui @@ -267,21 +267,5 @@ - - on - - - - - 0 - center - - - emblem-default-symbolic - \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 98c6b0b..bea3aaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,9 @@ mod volumebox; mod window; mod manager; mod withdefaultlistmodel; - +mod output_dropdown; +mod sinkbox; +mod outputbox; use std::{ffi::{OsStr, OsString}, path::PathBuf}; @@ -95,7 +97,7 @@ fn main() -> gtk::glib::ExitCode { Some("resources.gresource"), )) .or(gio::Resource::load(RESOURCES_FILE)) - .expect(&gettext("Could not load resources")); + .unwrap_or_else(|_| { panic!("{}", gettext("Could not load resources")) }); // Load resources gio::resources_register(&resources); diff --git a/src/volumebox/output_dropdown.rs b/src/output_dropdown.rs similarity index 83% rename from src/volumebox/output_dropdown.rs rename to src/output_dropdown.rs index b550bcc..ca2625a 100644 --- a/src/volumebox/output_dropdown.rs +++ b/src/output_dropdown.rs @@ -1,19 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use glib::{self, closure_local}; +use crate::{ + withdefaultlistmodel::WithDefaultListModel, + pwnodeobject::PwNodeObject, + application::PwvucontrolApplication, +}; +use glib::closure_local; use gtk::{self, prelude::*, subclass::prelude::*}; use std::cell::{Cell, RefCell}; -use crate::{withdefaultlistmodel::WithDefaultListModel, pwnodeobject::PwNodeObject, application::PwvucontrolApplication}; use wireplumber as wp; mod imp { - use super::*; - use glib::Properties; - use gtk::{self, CompositeTemplate}; - - #[derive(Debug, Default, CompositeTemplate, Properties)] + #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)] #[properties(wrapper_type = super::PwOutputDropDown)] #[template(resource = "/com/saivert/pwvucontrol/gtk/output-dropdown.ui")] pub struct PwOutputDropDown { @@ -65,19 +65,11 @@ mod imp { let manager = app.manager(); - fn setup_handler(item: &glib::Object, ellipsized: bool) { + fn setup_handler(item: &glib::Object) { let item: >k::ListItem = item.downcast_ref().expect("ListItem"); - let box_ = gtk::Box::new(gtk::Orientation::Horizontal, 0); let label = gtk::Label::new(None); - box_.append(&label); label.set_xalign(0.0); - if ellipsized { - label.set_ellipsize(gtk::pango::EllipsizeMode::End); - } else { - let icon = gtk::Image::from_icon_name("object-select-symbolic"); - icon.set_accessible_role(gtk::AccessibleRole::Presentation); - box_.append(&icon); - } + label.set_ellipsize(gtk::pango::EllipsizeMode::End); item.property_expression("item") .chain_closure::>(closure_local!( @@ -96,11 +88,11 @@ mod imp { )) .bind(&label, "label", gtk::Widget::NONE); - item.set_child(Some(&box_)); + item.set_child(Some(&label)); } let factory = gtk::SignalListItemFactory::new(); - factory.connect_setup(|_, item| setup_handler(item, true)); + factory.connect_setup(|_, item| setup_handler(item)); // We need to store the DropDown widget's internal default factory so we can reset the list-factory later // which would otherwise just use the factory we set @@ -113,16 +105,15 @@ mod imp { self.outputdevice_dropdown.set_enable_search(true); + self.outputdevice_dropdown .set_expression(Some(gtk::ClosureExpression::new::>( gtk::Expression::NONE, closure_local!(move |item: glib::Object| { if let Some(item) = item.downcast_ref::() { item.name() - } else if let Some(item) = item.downcast_ref::() { - Some(item.string().to_string()) } else { - None + item.downcast_ref::().map(|item| item.string().to_string()) } }), ))); diff --git a/src/outputbox.rs b/src/outputbox.rs new file mode 100644 index 0000000..4b6e0c5 --- /dev/null +++ b/src/outputbox.rs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::{ + application::PwvucontrolApplication, + pwnodeobject::PwNodeObject, + volumebox::{PwVolumeBox, PwVolumeBoxImpl}, + output_dropdown::PwOutputDropDown, +}; +use glib::{closure_local, clone}; +use gtk::{prelude::*, subclass::prelude::*}; +use wireplumber as wp; + +mod imp { + use super::*; + + #[derive(Default, gtk::CompositeTemplate)] + #[template(resource = "/com/saivert/pwvucontrol/gtk/outputbox.ui")] + pub struct PwOutputBox { + #[template_child] + pub output_dropdown: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PwOutputBox { + const NAME: &'static str = "PwOutputBox"; + type Type = super::PwOutputBox; + type ParentType = PwVolumeBox; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PwOutputBox { + fn constructed(&self) { + + let app = PwvucontrolApplication::default(); + let manager = app.manager(); + + let obj = self.obj(); + let parent: &PwVolumeBox = obj.upcast_ref(); + let item = parent.row_data().expect("nodeobj"); + + parent.add_default_node_change_handler(clone!(@weak self as widget => move || { + widget.obj().update_output_device_dropdown(); + })); + + self.parent_constructed(); + + + if let Some(metadata) = manager.imp().metadata.borrow().as_ref() { + let boundid = item.boundid(); + let widget = self.obj(); + let changed_closure = closure_local!(@watch widget => + move |_obj: &wp::pw::Metadata, id: u32, key: Option, _type: Option, _value: Option| { + let key = key.unwrap_or_default(); + if id == boundid && key.contains("target.") { + wp::log::info!("metadata changed handler id: {boundid} {key:?} {_value:?}!"); + widget.update_output_device_dropdown(); + } + }); + metadata.connect_closure("changed", false, changed_closure); + } + + // Create our custom output dropdown widget and add it to the layout + self.output_dropdown.set_nodeobj(Some(&item)); + + glib::idle_add_local_once(clone!(@weak self as widget => move || { + widget.obj().update_output_device_dropdown(); + })); + } + + } + impl WidgetImpl for PwOutputBox {} + impl ListBoxRowImpl for PwOutputBox {} + impl PwVolumeBoxImpl for PwOutputBox {} + + impl PwOutputBox { + + } +} + +glib::wrapper! { + pub struct PwOutputBox(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, PwVolumeBox, + @implements gtk::Actionable; +} + +impl PwOutputBox { + pub(crate) fn new(row_data: &impl glib::IsA) -> Self { + glib::Object::builder() + .property("row-data", row_data) + // .property( + // "channelmodel", + // gio::ListStore::new::(), + // ) + .build() + } + + pub(crate) fn update_output_device_dropdown(&self) { + let app = PwvucontrolApplication::default(); + let manager = app.manager(); + + let sinkmodel = &manager.imp().sinkmodel; + + let imp = self.imp(); + let parent: &PwVolumeBox = self.upcast_ref(); + + let output_dropdown = imp.output_dropdown.get(); + + let id = parent.imp().default_node.get(); + + let string = if let Ok(node) = sinkmodel.get_node(id) { + format!("Default ({})", node.name().unwrap()) + } else { + "Default".to_string() + }; + output_dropdown.set_default_text(&string); + + let item = parent.row_data().expect("row_data in pwvolumebox"); + + if let Some(deftarget) = item.default_target() { + // let model: gio::ListModel = imp + // .outputdevice_dropdown + // .model() + // .expect("Model from dropdown") + // .downcast() + // .unwrap(); + // let pos = model.iter::().enumerate().find_map(|o| { + // if let Ok(Ok(node)) = o.1.map(|x| x.downcast::()) { + // if node.boundid() == deftarget.boundid() { + // return Some(o.0); + // } + // } + // None + // }); + + if let Some(pos) = sinkmodel.get_node_pos_from_id(deftarget.boundid()) { + wp::log::info!( + "switching to preferred target pos={pos} boundid={} serial={}", + deftarget.boundid(), + deftarget.serial() + ); + output_dropdown.set_selected_no_send(pos+1); + } + } else { + output_dropdown.set_selected_no_send(0); + + // let id = self.imp().default_node.get(); + // wp::log::info!("default_node is {id}"); + // if id != u32::MAX { + // if let Some(pos) = sinkmodel.get_node_pos_from_id(id) { + // wp::log::info!("switching to default target"); + // if true + // /* imp.outputdevice_dropdown.selected() != pos */ + // { + // wp::log::info!("actually switching to default target"); + // imp.outputdevice_dropdown_block_signal.set(true); + // imp.outputdevice_dropdown.set_selected(pos); + // imp.outputdevice_dropdown_block_signal.set(false); + // } + // } + // } + } + } + +} diff --git a/src/sinkbox.rs b/src/sinkbox.rs new file mode 100644 index 0000000..6669d4f --- /dev/null +++ b/src/sinkbox.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::{ + application::PwvucontrolApplication, + pwnodeobject::PwNodeObject, + volumebox::{PwVolumeBox, PwVolumeBoxImpl}, +}; +use glib::clone; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::Cell; +use wireplumber as wp; + +mod imp { + use super::*; + + #[derive(Default, gtk::CompositeTemplate)] + #[template(resource = "/com/saivert/pwvucontrol/gtk/sinkbox.ui")] + pub struct PwSinkBox { + pub(super) block_default_node_toggle_signal: Cell, + + #[template_child] + pub default_sink_toggle: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PwSinkBox { + const NAME: &'static str = "PwSinkBox"; + type Type = super::PwSinkBox; + type ParentType = PwVolumeBox; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PwSinkBox { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + let parent: &PwVolumeBox = obj.upcast_ref(); + + parent.add_default_node_change_handler(clone!(@weak self as widget => move || { + widget.obj().default_node_changed(); + })); + + glib::idle_add_local_once(clone!(@weak self as widget => move || { + widget.obj().default_node_changed(); + })); + } + } + impl WidgetImpl for PwSinkBox {} + impl ListBoxRowImpl for PwSinkBox {} + impl PwVolumeBoxImpl for PwSinkBox {} + + #[gtk::template_callbacks] + impl PwSinkBox { + #[template_callback] + fn default_sink_toggle_toggled(&self, _togglebutton: >k::ToggleButton) { + if self.block_default_node_toggle_signal.get() { + return; + } + let obj = self.obj(); + let parent: &PwVolumeBox = obj.upcast_ref(); + let node = parent.row_data().expect("row data set on volumebox"); + let node_name: String = node.node_property("node.name"); + + let app = PwvucontrolApplication::default(); + let manager = app.manager(); + + let core = manager.imp().wp_core.get().expect("Core"); + let defaultnodesapi = + wp::plugin::Plugin::find(core, "default-nodes-api").expect("Get mixer-api"); + + let result: bool = defaultnodesapi.emit_by_name( + "set-default-configured-node-name", + &[&"Audio/Sink", &node_name], + ); + wp::info!("set-default-configured-node-name result: {result:?}"); + } + } +} + +glib::wrapper! { + pub struct PwSinkBox(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, PwVolumeBox, + @implements gtk::Actionable; +} + +impl PwSinkBox { + pub(crate) fn new(row_data: &impl glib::IsA) -> Self { + glib::Object::builder() + .property("row-data", row_data) + // .property( + // "channelmodel", + // gio::ListStore::new::(), + // ) + .build() + } + + pub(crate) fn default_node_changed(&self) { + let imp = self.imp(); + let parent: &PwVolumeBox = self.upcast_ref(); + let node = parent.row_data().expect("nodeobj"); + let id = parent.imp().default_node.get(); + + imp.block_default_node_toggle_signal.set(true); + self.imp() + .default_sink_toggle + .set_active(node.boundid() == id); + imp.block_default_node_toggle_signal.set(false); + } +} diff --git a/src/volumebox/mod.rs b/src/volumebox/mod.rs index 54cfd59..5051d3f 100644 --- a/src/volumebox/mod.rs +++ b/src/volumebox/mod.rs @@ -1,45 +1,38 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use crate::{application::PwvucontrolApplication, pwnodeobject::PwNodeObject}; - -use glib::{self, clone, ControlFlow, Properties}; +use crate::{ + application::PwvucontrolApplication, + pwnodeobject::PwNodeObject, + channelbox::PwChannelBox, + levelprovider::LevelbarProvider, + pwchannelobject::PwChannelObject +}; + +use glib::{clone, ControlFlow, closure_local, SignalHandlerId}; use gtk::{gio, prelude::*, subclass::prelude::*}; - -use std::cell::RefCell; - +use std::cell::{Cell, RefCell}; +use once_cell::sync::OnceCell; use wireplumber as wp; -mod output_dropdown; - mod imp { - use std::cell::Cell; - - use glib::{closure_local, SignalHandlerId}; - use once_cell::sync::OnceCell; - use super::*; - use output_dropdown::PwOutputDropDown; - use crate::{ - channelbox::PwChannelBox, levelprovider::LevelbarProvider, - pwchannelobject::PwChannelObject, NodeType, - }; - - #[derive(Default, gtk::CompositeTemplate, Properties)] + + #[derive(Default, gtk::CompositeTemplate, glib::Properties)] #[template(resource = "/com/saivert/pwvucontrol/gtk/volumebox.ui")] #[properties(wrapper_type = super::PwVolumeBox)] pub struct PwVolumeBox { #[property(get, set, construct_only)] pub(super) row_data: RefCell>, - #[property(get, set, construct_only)] + // #[property(get, set, construct_only)] channelmodel: OnceCell, metadata_changed_event: Cell>, levelbarprovider: OnceCell, timeoutid: Cell>, pub(super) level: Cell, - pub(super) default_node: Cell, - pub(super) block_default_node_toggle_signal: Cell, + pub default_node: Cell, + pub(super) default_node_changed_handlers: RefCell>>, // Template widgets #[template_child] @@ -68,12 +61,7 @@ mod imp { pub monitorvolumescale: TemplateChild, #[template_child] pub container: TemplateChild, - #[template_child] - pub onlabel: TemplateChild, - #[template_child] - pub default_sink_toggle: TemplateChild, - pub outputdevice_dropdown: RefCell>, } #[glib::object_subclass] @@ -81,6 +69,7 @@ mod imp { const NAME: &'static str = "PwVolumeBox"; type Type = super::PwVolumeBox; type ParentType = gtk::ListBoxRow; + type Interfaces = (gtk::Buildable,); fn class_init(klass: &mut Self::Class) { klass.bind_template(); @@ -104,7 +93,9 @@ mod imp { } self.parent_constructed(); - + + self.channelmodel.set(gio::ListStore::new::()).expect("channelmodel not already set"); + let item = self.row_data.borrow(); let item = item.as_ref().cloned().unwrap(); @@ -177,52 +168,18 @@ mod imp { wp::info!("default-nodes-api changed: new id {id}"); widget.imp().default_node.set(id); - widget.default_node_changed(); + let list = widget.imp().default_node_changed_handlers.borrow(); + for cb in list.iter() { + cb(); + } }); defaultnodesapi_closure.invoke::<()>(&[&defaultnodesapi]); defaultnodesapi.connect_closure("changed", false, defaultnodesapi_closure); - if matches!( - item.nodetype(), - /* NodeType::Input | */ NodeType::Output - ) { - - if let Some(metadata) = manager.imp().metadata.borrow().as_ref() { - let boundid = item.boundid(); - let widget = self.obj(); - let changed_closure = closure_local!(@watch widget => - move |_obj: &wp::pw::Metadata, id: u32, key: Option, _type: Option, _value: Option| { - let key = key.unwrap_or_default(); - if id == boundid && key.contains("target.") { - wp::log::info!("metadata changed handler id: {boundid} {key:?} {_value:?}!"); - widget.update_output_device_dropdown(); - } - }); - metadata.connect_closure("changed", false, changed_closure); - } - - self.container.append(&self.onlabel.get()); - - // Create our custom output dropdown widget and add it to the layout - self.outputdevice_dropdown.replace(Some(PwOutputDropDown::new(Some(&item)))); - let output_dropdown = self.outputdevice_dropdown.borrow(); - let output_dropdown = output_dropdown.as_ref().expect("Dropdown widget"); - self.container.append(output_dropdown); - - glib::idle_add_local_once(clone!(@weak self as widget => move || { - widget.obj().update_output_device_dropdown(); - })); - - } - - if matches!(item.nodetype(), NodeType::Sink) { - self.container.append(&self.default_sink_toggle.get()); - - } - let channelmodel = self.obj().channelmodel(); + let channelmodel = self.channelmodel.get().expect("channel model"); self.channel_listbox.bind_model( - Some(&channelmodel), + Some(channelmodel), clone!(@weak self as widget => @default-panic, move |item| { PwChannelBox::new( item.clone().downcast_ref::() @@ -324,6 +281,22 @@ mod imp { } impl WidgetImpl for PwVolumeBox {} impl ListBoxRowImpl for PwVolumeBox {} + + impl BuildableImpl for PwVolumeBox { + fn add_child(&self, builder: >k::Builder, child: &glib::Object, type_: Option<&str>) { + if type_.unwrap_or_default() == "extra" { + if let Some(widget) = child.downcast_ref::() { + if let Some(container) = self.container.try_get() { + widget.unparent(); + container.append(widget); + container.set_child_visible(true); + } + } + } else { + self.parent_add_child(builder, child, type_); + } + } + } #[gtk::template_callbacks] impl PwVolumeBox { @@ -332,31 +305,14 @@ mod imp { !value } - #[template_callback] - fn default_sink_toggle_toggled(&self, _togglebutton: >k::ToggleButton) { - if self.block_default_node_toggle_signal.get() { - return; - } - let node = self.obj().row_data().expect("row data set on volumebox"); - let node_name: String = node.node_property("node.name"); - - let app = PwvucontrolApplication::default(); - let manager = app.manager(); - - let core = manager.imp().wp_core.get().expect("Core"); - let defaultnodesapi = - wp::plugin::Plugin::find(core, "default-nodes-api").expect("Get mixer-api"); - let result: bool = defaultnodesapi.emit_by_name("set-default-configured-node-name", &[&"Audio/Sink", &node_name]); - wp::info!("set-default-configured-node-name result: {result:?}"); - } } } glib::wrapper! { pub struct PwVolumeBox(ObjectSubclass) @extends gtk::Widget, gtk::ListBoxRow, - @implements gtk::Actionable; + @implements gtk::Actionable, gtk::Buildable; } impl PwVolumeBox { @@ -374,91 +330,18 @@ impl PwVolumeBox { self.imp().level.set(level); } - pub(crate) fn default_node_changed(&self) { - let node = self.row_data().expect("row_data set"); - match node.nodetype() { - crate::NodeType::Output => self.update_output_device_dropdown(), - crate::NodeType::Sink => self.update_default_toggle(), - _ => {}, - } - } - - pub(crate) fn update_default_toggle(&self) { + pub fn add_default_node_change_handler(&self, c: impl Fn() + 'static) { let imp = self.imp(); - let node = self.row_data().unwrap(); - let id = self.imp().default_node.get(); - imp.block_default_node_toggle_signal.set(true); - self.imp().default_sink_toggle.set_active(node.boundid() == id); - imp.block_default_node_toggle_signal.set(false); + let mut list = imp.default_node_changed_handlers.borrow_mut(); + list.push(Box::new(c)); } +} +pub trait PwVolumeBoxImpl: ListBoxRowImpl + ObjectImpl + 'static {} - pub(crate) fn update_output_device_dropdown(&self) { - let app = PwvucontrolApplication::default(); - let manager = app.manager(); - - let sinkmodel = &manager.imp().sinkmodel; - - let imp = self.imp(); - - let output_dropdown = imp.outputdevice_dropdown.borrow(); - - let Some(output_dropdown) = output_dropdown.as_ref() else { - return; - }; - - let string = if let Ok(node) = sinkmodel.get_node(imp.default_node.get()) { - format!("Default ({})", node.name().unwrap()) - } else { - "Default".to_string() - }; - output_dropdown.set_default_text(&string); - - let item = imp.row_data.borrow(); - let item = item.as_ref().cloned().unwrap(); - - if let Some(deftarget) = item.default_target() { - // let model: gio::ListModel = imp - // .outputdevice_dropdown - // .model() - // .expect("Model from dropdown") - // .downcast() - // .unwrap(); - // let pos = model.iter::().enumerate().find_map(|o| { - // if let Ok(Ok(node)) = o.1.map(|x| x.downcast::()) { - // if node.boundid() == deftarget.boundid() { - // return Some(o.0); - // } - // } - // None - // }); - - if let Some(pos) = sinkmodel.get_node_pos_from_id(deftarget.boundid()) { - wp::log::info!( - "switching to preferred target pos={pos} boundid={} serial={}", - deftarget.boundid(), - deftarget.serial() - ); - output_dropdown.set_selected_no_send(pos+1 as u32); - } - } else { - output_dropdown.set_selected_no_send(0); +unsafe impl IsSubclassable for PwVolumeBox { + fn class_init(class: &mut glib::Class) { + Self::parent_class_init::(class.upcast_ref_mut()); - // let id = self.imp().default_node.get(); - // wp::log::info!("default_node is {id}"); - // if id != u32::MAX { - // if let Some(pos) = sinkmodel.get_node_pos_from_id(id) { - // wp::log::info!("switching to default target"); - // if true - // /* imp.outputdevice_dropdown.selected() != pos */ - // { - // wp::log::info!("actually switching to default target"); - // imp.outputdevice_dropdown_block_signal.set(true); - // imp.outputdevice_dropdown.set_selected(pos); - // imp.outputdevice_dropdown_block_signal.set(false); - // } - // } - // } - } } } diff --git a/src/window.rs b/src/window.rs index 9f76963..10b63d0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -110,7 +110,7 @@ mod imp { self.playbacklist.bind_model( Some(filterlistmodel), clone!(@weak window => @default-panic, move |item| { - PwVolumeBox::new( + PwOutputBox::new( item.downcast_ref::() .expect("RowData is of wrong type"), ) @@ -140,7 +140,7 @@ mod imp { self.outputlist.bind_model( Some(sinkmodel), clone!(@weak window => @default-panic, move |item| { - PwVolumeBox::new( + PwSinkBox::new( item.downcast_ref::() .expect("RowData is of wrong type"), )