From 7127c3e77ef677006311703b3eab62888309bf4f Mon Sep 17 00:00:00 2001 From: Nicolai Syvertsen Date: Sun, 28 Apr 2024 13:05:33 +0200 Subject: [PATCH] Add port/route dropdown to sinks. --- data/resources/resources.gresource.xml | 1 + data/resources/ui/route-dropdown.ui | 20 ++ data/resources/ui/sinkbox.ui | 9 + src/backend/mod.rs | 10 +- src/backend/paramavailability.rs | 18 ++ src/backend/pwdeviceobject.rs | 277 ++++++++++++++++++++----- src/backend/pwnodeobject.rs | 18 +- src/backend/pwprofileobject.rs | 27 +-- src/backend/pwroutefiltermodel.rs | 106 ++++++++++ src/backend/pwrouteobject.rs | 55 +++++ src/backend/routedirection.rs | 24 +++ src/ui/mod.rs | 2 + src/ui/profile_dropdown.rs | 8 +- src/ui/profilerow.rs | 13 +- src/ui/route_dropdown.rs | 199 ++++++++++++++++++ src/ui/sinkbox.rs | 14 ++ src/ui/volumebox.rs | 4 +- 17 files changed, 723 insertions(+), 82 deletions(-) create mode 100644 data/resources/ui/route-dropdown.ui create mode 100644 src/backend/paramavailability.rs create mode 100644 src/backend/pwroutefiltermodel.rs create mode 100644 src/backend/pwrouteobject.rs create mode 100644 src/backend/routedirection.rs create mode 100644 src/ui/route_dropdown.rs diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 7e47eb0..dc5f5fe 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -10,6 +10,7 @@ ui/sinkbox.ui ui/outputbox.ui ui/profile-dropdown.ui + ui/route-dropdown.ui ui/devicebox.ui ui/profilerow.ui ui/levelbar.css diff --git a/data/resources/ui/route-dropdown.ui b/data/resources/ui/route-dropdown.ui new file mode 100644 index 0000000..b9fb761 --- /dev/null +++ b/data/resources/ui/route-dropdown.ui @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/data/resources/ui/sinkbox.ui b/data/resources/ui/sinkbox.ui index d3755c7..2b991ee 100644 --- a/data/resources/ui/sinkbox.ui +++ b/data/resources/ui/sinkbox.ui @@ -8,6 +8,15 @@ horizontal 6 + + + Port: + + + + + + 0 diff --git a/src/backend/mod.rs b/src/backend/mod.rs index d916d23..f866ed2 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,13 +1,21 @@ +mod paramavailability; mod pwchannelobject; mod manager; mod pwdeviceobject; mod pwprofileobject; mod pwnodemodel; mod pwnodeobject; +mod pwrouteobject; +mod routedirection; +mod pwroutefiltermodel; +pub use paramavailability::ParamAvailability; pub use pwchannelobject::PwChannelObject; pub use manager::PwvucontrolManager; pub use pwdeviceobject::PwDeviceObject; -pub use pwprofileobject::{PwProfileObject, ProfileAvailability}; +pub use pwprofileobject::PwProfileObject; pub use pwnodemodel::PwNodeModel; pub use pwnodeobject::{PwNodeObject, NodeType}; +pub use pwrouteobject::PwRouteObject; +pub use routedirection::RouteDirection; +pub use pwroutefiltermodel::PwRouteFilterModel; \ No newline at end of file diff --git a/src/backend/paramavailability.rs b/src/backend/paramavailability.rs new file mode 100644 index 0000000..72c4e76 --- /dev/null +++ b/src/backend/paramavailability.rs @@ -0,0 +1,18 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, glib::Enum, Default)] +#[enum_type(name = "ProfileAvailability")] +pub enum ParamAvailability { + #[default] + Unknown, + No, + Yes +} + +impl From for ParamAvailability { + fn from(value: u32) -> Self { + match value { + 1 => ParamAvailability::No, + 2 => ParamAvailability::Yes, + _ => ParamAvailability::Unknown, + } + } +} diff --git a/src/backend/pwdeviceobject.rs b/src/backend/pwdeviceobject.rs index b007ed0..3a54266 100644 --- a/src/backend/pwdeviceobject.rs +++ b/src/backend/pwdeviceobject.rs @@ -1,7 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later use crate::backend::pwprofileobject::PwProfileObject; -use glib::{self, clone, subclass::{prelude::*, Signal}, Object, ObjectExt, ParamSpec, Properties, Value}; +use glib::{ + self, clone, + subclass::{prelude::*, Signal}, + Object, ObjectExt, ParamSpec, Properties, Value, +}; use gtk::{gio, prelude::*}; use wireplumber as wp; use wp::{ @@ -9,28 +13,65 @@ use wp::{ spa::SpaPodBuilder, }; -use once_cell::sync::{OnceCell, Lazy}; -use std::cell::{Cell, RefCell}; use crate::macros::*; +use once_cell::sync::{Lazy, OnceCell}; +use std::cell::{Cell, RefCell}; + +use super::PwRouteObject; +use crate::backend::RouteDirection; +use crate::backend::PwRouteFilterModel; pub mod imp { use super::*; - #[derive(Default, Properties)] + #[derive(Properties)] #[properties(wrapper_type = super::PwDeviceObject)] pub struct PwDeviceObject { #[property(get, set)] name: RefCell>, + #[property(get, set)] icon_name: RefCell, + #[property(get, set)] pub(super) profile_index: Cell, + #[property(get, set)] + pub(super) route_index_input: Cell, + + #[property(get, set)] + pub(super) route_index_output: Cell, + #[property(get, set, construct_only)] pub(super) wpdevice: OnceCell, #[property(get)] - pub(super) profilemodel: OnceCell, + pub(super) profilemodel: gio::ListStore, + + #[property(get)] + pub(super) routemodel_input: PwRouteFilterModel, + + #[property(get)] + pub(super) routemodel_output: PwRouteFilterModel, + + pub(super) routemodel: gio::ListStore, + } + + impl Default for PwDeviceObject { + fn default() -> Self { + Self { + name: Default::default(), + icon_name: Default::default(), + profile_index: Default::default(), + route_index_input: Default::default(), + route_index_output: Default::default(), + wpdevice: Default::default(), + profilemodel: gio::ListStore::new::(), + routemodel_input: PwRouteFilterModel::new(RouteDirection::Input, gio::ListModel::NONE), + routemodel_output: PwRouteFilterModel::new(RouteDirection::Output, gio::ListModel::NONE), + routemodel: gio::ListStore::new::(), + } + } } #[glib::object_subclass] @@ -54,12 +95,14 @@ pub mod imp { } fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = - Lazy::new(|| vec![ - Signal::builder("profiles-changed").build(), - Signal::builder("pre-update").build(), - Signal::builder("post-update").build(), - ]); + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("pre-update-profile").build(), + Signal::builder("post-update-profile").build(), + Signal::builder("pre-update-route").build(), + Signal::builder("post-update-route").build(), + ] + }); SIGNALS.as_ref() } @@ -67,7 +110,8 @@ pub mod imp { fn constructed(&self) { self.parent_constructed(); - self.profilemodel.set(gio::ListStore::new::()).expect("profilemodel not set"); + self.routemodel_input.set_model(Some(self.routemodel.clone())); + self.routemodel_output.set_model(Some(self.routemodel.clone())); let obj = self.obj(); @@ -79,32 +123,38 @@ pub mod imp { obj.set_profile_index(index as u32); } - obj.wpdevice() - .connect_properties_notify(clone!(@weak obj => move |device| { - pwvucontrol_debug!("properties changed! id: {}", device.object_id().unwrap()); - - obj.label_set_name(); - })); - - obj.wpdevice() - .connect_params_changed(clone!(@weak obj => move |device, what| { - pwvucontrol_debug!("params-changed! {what} id: {}", device.object_id().unwrap()); - - match what { - "EnumProfile" => { - obj.update_profiles(); - //obj.emit_by_name::<()>("profiles-changed", &[]); - }, - "Profile" => { - if let Some(index) = obj.get_current_profile_index() { - obj.set_profile_index(index as u32); - } - } - _ => {}, - } + obj.update_routes(); + + obj.wpdevice().connect_properties_notify(clone!(@weak obj => move |device| { + pwvucontrol_debug!("properties changed! id: {}", device.object_id().unwrap()); - })); + obj.label_set_name(); + })); + + obj.wpdevice().connect_params_changed(clone!(@weak obj => move |device, what| { + pwvucontrol_debug!("params-changed! {what} id: {}", device.object_id().unwrap()); + + match what { + "EnumProfile" => { + obj.update_profiles(); + //obj.emit_by_name::<()>("profiles-changed", &[]); + }, + "Profile" => { + if let Some(index) = obj.get_current_profile_index() { + obj.set_profile_index(index as u32); + } + }, + "EnumRoute" => { + obj.update_routes(); + }, + "Route" => { + //obj.update_routes(); + obj.update_current_route_index(); + }, + _ => {}, + } + })); } } @@ -123,7 +173,10 @@ impl PwDeviceObject { pub(crate) fn update_profiles(&self) { let device = self.wpdevice(); - device.enum_params(Some("EnumProfile"), None, gtk::gio::Cancellable::NONE, + device.enum_params( + Some("EnumProfile"), + None, + gtk::gio::Cancellable::NONE, clone!(@weak self as widget => move |res| { let keys = wp::spa::SpaIdTable::from_name("Spa:Pod:Object:Param:Profile").expect("id table"); let index_key = keys.find_value_from_short_name("index").expect("index key"); @@ -132,7 +185,8 @@ impl PwDeviceObject { if let Ok(Some(iter)) = res { let removed = widget.profilemodel().n_items(); - widget.emit_by_name::<()>("pre-update", &[]); + + widget.emit_by_name::<()>("pre-update-profile", &[]); let mut profiles: Vec = Vec::new(); @@ -154,15 +208,14 @@ impl PwDeviceObject { widget.imp().profile_index.set(widget.get_current_profile_index().unwrap() as u32); // Notify update of list model - widget.emit_by_name::<()>("post-update", &[]); - - //widget.emit_by_name::<()>("profiles-changed", &[]); + widget.emit_by_name::<()>("post-update-profile", &[]); } else { if let Err(e) = res { dbg!(e); } } - })); + }), + ); } pub(crate) fn get_current_profile_index(&self) -> Option { @@ -181,7 +234,6 @@ impl PwDeviceObject { let index = pod.find_spa_property(&index_key).expect("Index!").int().expect("Int"); let description = pod.find_spa_property(&description_key).expect("Format!").string().expect("String"); - pwvucontrol_info!("Current profile #{} {}", index, description); return Some(index); @@ -204,12 +256,143 @@ impl PwDeviceObject { } } + pub(crate) fn update_routes(&self) { + let device = self.wpdevice(); + + device.enum_params( + Some("EnumRoute"), + None, + gtk::gio::Cancellable::NONE, + clone!(@weak self as widget => move |res| { + let keys = wp::spa::SpaIdTable::from_name("Spa:Pod:Object:Param:Route").expect("id table"); + let index_key = keys.find_value_from_short_name("index").expect("index key"); + let description_key = keys.find_value_from_short_name("description").expect("decription key"); + let available_key = keys.find_value_from_short_name("available").expect("available key"); + let direction_key = keys.find_value_from_short_name("direction").expect("direction key"); + + if let Ok(Some(iter)) = res { + let removed = widget.imp().routemodel.n_items(); + widget.emit_by_name::<()>("pre-update-route", &[]); + + let mut routes: Vec = Vec::new(); + + for a in iter { + let pod: wp::spa::SpaPod = a.get().unwrap(); + if !pod.is_object() { + continue; + } + + let index = pod.find_spa_property(&index_key).expect("Index").int().expect("Int"); + let description = pod.find_spa_property(&description_key).expect("Format!").string().expect("String"); + let available = pod.find_spa_property(&available_key).expect("Availability!").id().expect("Id"); + let direction = pod.find_spa_property(&direction_key).expect("Direction!").id().expect("Id"); + + routes.push(PwRouteObject::new(index as u32, &description, available, direction)); + } + widget.imp().routemodel.splice(0, removed as u32, &routes); + + // Set route_index property without notify by setting internal storage directly + widget.update_current_route_index(); + + // Notify update of list model + widget.emit_by_name::<()>("post-update-route", &[]); + } else { + if let Err(e) = res { + dbg!(e); + } + } + }), + ); + } + + pub(crate) fn update_current_route_index(&self) { + self.update_current_route_index_for_direction_sync(RouteDirection::Input); + self.update_current_route_index_for_direction_sync(RouteDirection::Output); + } + + pub(crate) fn update_current_route_index_for_direction_sync(&self, direction: RouteDirection) { + let device = self.wpdevice(); + + let keys = wp::spa::SpaIdTable::from_name("Spa:Pod:Object:Param:Route").expect("id table"); + let index_key = keys.find_value_from_short_name("index").expect("index key"); + let description_key = keys.find_value_from_short_name("description").expect("decription key"); + + let podbuilder = SpaPodBuilder::new_object("Spa:Pod:Object:Param:Route", "Route"); + podbuilder.add_property("direction"); + podbuilder.add_id(direction.into()); + let filter_pod = podbuilder.end().expect("pod"); + + if let Some(params) = device.enum_params_sync("Route", Some(&filter_pod)) { + for a in params { + let pod: wp::spa::SpaPod = a.get().unwrap(); + if !pod.is_object() { + continue; + } + + let index = pod.find_spa_property(&index_key).expect("Index").int().expect("Int"); + let description = pod + .find_spa_property(&description_key) + .expect("Description key") + .string() + .expect("String"); + + pwvucontrol_info!("Current route #{} {}", index, description); + + if let Some(modelindex) = self.get_model_index_from_route_index(direction, index) { + match direction { + RouteDirection::Input => self.set_route_index_input(modelindex), + RouteDirection::Output => self.set_route_index_output(modelindex), + _ => unreachable!() + } + break; + } else { + pwvucontrol_critical!("Unable to get model index from route index in update_current_route_index_for_direction_sync"); + }; + } + } + } + + fn get_model_index_from_route_index(&self, direction: RouteDirection, routeindex: i32) -> Option { + let routemodel = self.get_route_model_for_direction(direction); + + for a in routemodel.iter::().enumerate() { + if let Ok(b) = a.1 { + //let b: PwRouteObject = b.downcast().expect("PwRouteObject"); + if b.index() == routeindex as u32 { + return Some(a.0 as u32); + } + } + } + None + } + + pub(crate) fn set_route(&self, index: u32, device_index: i32) { + let device = self.wpdevice(); + + let podbuilder = SpaPodBuilder::new_object("Spa:Pod:Object:Param:Route", "Route"); + + podbuilder.add_property("index"); + podbuilder.add_int(index as i32); + podbuilder.add_property("device"); + podbuilder.add_int(device_index); + // podbuilder.add_property("save"); + // podbuilder.add_boolean(true); + + if let Some(pod) = podbuilder.end() { + device.set_param("Route", 0, pod); + } + } + + fn get_route_model_for_direction(&self, direction: RouteDirection) -> PwRouteFilterModel { + match direction { + RouteDirection::Input => self.routemodel_input(), + RouteDirection::Output => self.routemodel_output(), + _ => unreachable!(), + } + } fn label_set_name(&self) { - let description: String = self - .wpdevice() - .pw_property("device.description") - .expect("device description"); + let description: String = self.wpdevice().pw_property("device.description").expect("device description"); self.set_name(description); } diff --git a/src/backend/pwnodeobject.rs b/src/backend/pwnodeobject.rs index 1c9f9a2..480c77c 100644 --- a/src/backend/pwnodeobject.rs +++ b/src/backend/pwnodeobject.rs @@ -11,7 +11,7 @@ use glib::{self, clone, subclass::{prelude::*, Signal}, ObjectExt, ParamSpec, Pr use once_cell::sync::{Lazy, OnceCell}; use gtk::{gio, prelude::ListModelExt}; use crate::backend::PwChannelObject; -use super::PwvucontrolManager; +use super::{PwDeviceObject, PwvucontrolManager}; use crate::macros::*; mod mixerapi; @@ -485,6 +485,22 @@ impl PwNodeObject { }; } + pub(crate) fn get_device(&self) -> Option { + if let Ok(Some(device_id)) = self.wpnode().device_id() { + let manager = PwvucontrolManager::default(); + return manager.get_device_by_id(device_id); + } + None + } + + pub(crate) fn set_route(&self, index: u32) { + if let Ok(Some(card_profile_device)) = self.wpnode().device_index() { + if let Some(device) = self.get_device() { + device.set_route(index, card_profile_device as i32); + } + } + } + pub(crate) fn default_target(&self) -> Option { let manager = PwvucontrolManager::default(); diff --git a/src/backend/pwprofileobject.rs b/src/backend/pwprofileobject.rs index cc7c523..ec679fc 100644 --- a/src/backend/pwprofileobject.rs +++ b/src/backend/pwprofileobject.rs @@ -7,27 +7,10 @@ use gtk::{ prelude::*, subclass::prelude::* }; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, glib::Enum, Default)] -#[enum_type(name = "ProfileAvailability")] -pub enum ProfileAvailability { - #[default] - Unknown, - No, - Yes -} - -impl From for ProfileAvailability { - fn from(value: u32) -> Self { - match value { - 1 => ProfileAvailability::No, - 2 => ProfileAvailability::Yes, - _ => ProfileAvailability::Unknown, - } - } -} +use super::ParamAvailability; mod imp { + use super::*; #[derive(Default, Properties)] @@ -37,8 +20,8 @@ mod imp { index: Cell, #[property(get, set)] description: RefCell, - #[property(get, set, builder(ProfileAvailability::Unknown))] - availability: Cell, + #[property(get, set, builder(ParamAvailability::Unknown))] + availability: Cell, } #[glib::object_subclass] @@ -62,7 +45,7 @@ impl PwProfileObject { glib::Object::builder() .property("index", index) .property("description", description) - .property("availability", ProfileAvailability::from(availability)) + .property("availability", ParamAvailability::from(availability)) .build() } } diff --git a/src/backend/pwroutefiltermodel.rs b/src/backend/pwroutefiltermodel.rs new file mode 100644 index 0000000..3e5d1cd --- /dev/null +++ b/src/backend/pwroutefiltermodel.rs @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use glib::{Properties, closure_local}; +use glib::subclass::prelude::*; +use gtk::{gio, prelude::*, subclass::prelude::*}; +use std::cell::{Cell, RefCell}; +use im_rc::Vector; +use super::{PwRouteObject, RouteDirection}; + +mod imp { + + use crate::backend::ParamAvailability; + + use super::*; + + #[derive(Debug, Properties, Default)] + #[properties(wrapper_type = super::PwRouteFilterModel)] + pub struct PwRouteFilterModel { + /// Contains the items that matches the filter predicate. + pub(super) filtered_model: RefCell>, + + #[property(get, set, construct_only, builder(RouteDirection::Unknown))] + pub(super) direction: Cell, + + /// The model we are filtering. + #[property(get, set = Self::set_model, nullable )] + pub(super) model: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for PwRouteFilterModel { + const NAME: &'static str = "PwRouteFilterModel"; + type Type = super::PwRouteFilterModel; + type Interfaces = (gio::ListModel,); + } + + #[glib::derived_properties] + impl ObjectImpl for PwRouteFilterModel { + } + + impl ListModelImpl for PwRouteFilterModel { + fn item_type(&self) -> glib::Type { + PwRouteObject::static_type() + } + fn n_items(&self) -> u32 { + self.filtered_model.borrow().len() as u32 + } + fn item(&self, position: u32) -> Option { + self.filtered_model + .borrow() + .get(position as usize) + .map(|o| o.clone().upcast::()) + } + } + + impl PwRouteFilterModel { + pub fn set_model(&self, new_model: Option) { + let removed = self.filtered_model.borrow().len() as u32; + + if let Some(new_model) = new_model { + + assert!(self.item_type().is_a(new_model.item_type())); + + let widget = self.obj(); + let handler = closure_local!(@watch widget => move |listmodel: &gio::ListModel, _position: u32, _removed: u32, _added: u32| { + + let u: Vector = listmodel.iter::() + .map_while(Result::ok) + .filter(|routeobject| { + routeobject.direction() == widget.direction() && routeobject.availability() == ParamAvailability::Yes + }) + .collect(); + + let removed = widget.imp().filtered_model.borrow().len() as u32; + let added = { + let mut filtered_model = widget.imp().filtered_model.borrow_mut(); + filtered_model.clear(); + filtered_model.append(u); + filtered_model.len() as u32 + }; + widget.items_changed(0, removed, added) + }); + handler.invoke::<()>(&[&new_model, &0u32, &0u32, &0u32]); + new_model.connect_closure("items-changed", true, handler); + + self.model.replace(Some(new_model.clone().upcast())); + } else { + self.obj().items_changed(0, removed, 0); + } + } + } +} + +glib::wrapper! { + pub struct PwRouteFilterModel(ObjectSubclass) @implements gio::ListModel; +} + +impl PwRouteFilterModel { + pub(crate) fn new(direction: RouteDirection, model: Option<&impl glib::IsA>) -> Self + { + glib::Object::builder() + .property("model", model) + .property("direction", direction) + .build() + } +} diff --git a/src/backend/pwrouteobject.rs b/src/backend/pwrouteobject.rs new file mode 100644 index 0000000..0be767f --- /dev/null +++ b/src/backend/pwrouteobject.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::cell::{Cell, RefCell}; + +use gtk::{ + glib::{self, Properties}, + prelude::*, + subclass::prelude::* +}; + +use super::ParamAvailability; +use super::RouteDirection; + +mod imp { + use super::*; + + #[derive(Default, Properties)] + #[properties(wrapper_type = super::PwRouteObject)] + pub struct PwRouteObject { + #[property(get, set)] + index: Cell, + #[property(get, set)] + description: RefCell, + #[property(get, set, builder(ParamAvailability::Unknown))] + availability: Cell, + #[property(get, set, builder(RouteDirection::Unknown))] + direction: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for PwRouteObject { + const NAME: &'static str = "PwRouteObject"; + type Type = super::PwRouteObject; + } + + #[glib::derived_properties] + impl ObjectImpl for PwRouteObject {} + + impl PwRouteObject {} +} + +glib::wrapper! { + pub struct PwRouteObject(ObjectSubclass); +} + +impl PwRouteObject { + pub(crate) fn new(index: u32, description: &str, availability: u32, direction: u32) -> Self { + glib::Object::builder() + .property("index", index) + .property("description", format!("{description} ({index})")) + .property("availability", ParamAvailability::from(availability)) + .property("direction", RouteDirection::from(direction)) + .build() + } +} diff --git a/src/backend/routedirection.rs b/src/backend/routedirection.rs new file mode 100644 index 0000000..12c77fb --- /dev/null +++ b/src/backend/routedirection.rs @@ -0,0 +1,24 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, glib::Enum, Default)] +#[enum_type(name = "RouteDirection")] +pub enum RouteDirection { + #[default] + Unknown = 2, + Input = 0, + Output = 1 +} + +impl From for RouteDirection { + fn from(value: u32) -> Self { + match value { + 0 => RouteDirection::Input, + 1 => RouteDirection::Output, + _ => RouteDirection::Unknown, + } + } +} + +impl From for u32 { + fn from(value: RouteDirection) -> Self { + value as u32 + } +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 624567e..84f0111 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -9,6 +9,7 @@ mod outputbox; mod profile_dropdown; mod devicebox; mod profilerow; +mod route_dropdown; pub use window::PwvucontrolWindow; pub use window::PwvucontrolWindowView; @@ -21,3 +22,4 @@ pub use channelbox::PwChannelBox; pub use levelprovider::LevelbarProvider; pub use outputbox::PwOutputBox; pub use profilerow::PwProfileRow; +pub use route_dropdown::PwRouteDropDown; diff --git a/src/ui/profile_dropdown.rs b/src/ui/profile_dropdown.rs index 7868ea7..d05890d 100644 --- a/src/ui/profile_dropdown.rs +++ b/src/ui/profile_dropdown.rs @@ -10,6 +10,8 @@ use wireplumber as wp; use crate::ui::PwProfileRow; use crate::macros::*; mod imp { + use crate::backend::PwProfileObject; + use super::*; #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)] @@ -112,14 +114,14 @@ mod imp { self.block_signal.set(false); - deviceobject.connect_local("pre-update", false, + deviceobject.connect_local("pre-update-profile", false, clone!(@weak self as widget => @default-return None, move |_| { widget.block_signal.set(true); None }) ); - deviceobject.connect_local("post-update", false, + deviceobject.connect_local("post-update-profile", false, clone!(@weak self as widget => @default-return None, move |_| { widget.block_signal.set(false); widget.update_selected(); @@ -151,7 +153,7 @@ mod imp { let item: >k::ListItem = item.downcast_ref().expect("ListItem"); let profilerow = PwProfileRow::new(); - profilerow.setup(item, list); + profilerow.setup::(item, list); item.set_child(Some(&profilerow)); } diff --git a/src/ui/profilerow.rs b/src/ui/profilerow.rs index 4a38be7..a8433ab 100644 --- a/src/ui/profilerow.rs +++ b/src/ui/profilerow.rs @@ -4,7 +4,7 @@ use glib::closure_local; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; -use crate::backend::{PwProfileObject, ProfileAvailability}; +use crate::backend::ParamAvailability; mod imp { use super::*; @@ -54,24 +54,25 @@ impl PwProfileRow { glib::Object::builder().build() } - pub fn setup(&self, item: >k::ListItem, list: bool) { + pub fn setup>(&self, item: >k::ListItem, list: bool) { let label = self.imp().label.get(); let unavailable_icon = self.imp().unavailable_icon.get(); if !list { label.set_ellipsize(gtk::pango::EllipsizeMode::End); + self.imp().checkmark_icon.set_visible(false); } item.property_expression("item") - .chain_property::("description") + .chain_property::("description") .bind(&label, "label", gtk::Widget::NONE); - let icon_closure = closure_local!(|_: Option, availability: ProfileAvailability| { - availability == ProfileAvailability::No + let icon_closure = closure_local!(|_: Option, availability: ParamAvailability| { + availability == ParamAvailability::No }); item.property_expression("item") - .chain_property::("availability") + .chain_property::("availability") .chain_closure::(icon_closure) .bind(&unavailable_icon, "visible", glib::Object::NONE); } diff --git a/src/ui/route_dropdown.rs b/src/ui/route_dropdown.rs new file mode 100644 index 0000000..b2f4c3b --- /dev/null +++ b/src/ui/route_dropdown.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +use glib::closure_local; +use gtk::{self, prelude::*, subclass::prelude::*}; +use glib::clone; +use wp::pw::ProxyExt; +use std::cell::{Cell, RefCell}; +use wireplumber as wp; +use crate::ui::PwProfileRow; +use crate::macros::*; + +mod imp { + use crate::backend::{PwNodeObject, PwRouteObject}; + + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)] + #[properties(wrapper_type = super::PwRouteDropDown)] + #[template(resource = "/com/saivert/pwvucontrol/gtk/route-dropdown.ui")] + pub struct PwRouteDropDown { + #[property(get, set = Self::set_nodeobject, nullable)] + pub(super) nodeobject: RefCell>, + + #[template_child] + pub route_dropdown: TemplateChild, + + pub(super) block_signal: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for PwRouteDropDown { + const NAME: &'static str = "PwRouteDropDown"; + type Type = super::PwRouteDropDown; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl PwRouteDropDown { + pub fn update_selected(&self) { + let nodeobject = self.nodeobject.borrow(); + let nodeobject = nodeobject.as_ref().unwrap(); + + let deviceobject = nodeobject.get_device().expect("device"); + + pwvucontrol_info!("update_selected with index {}", deviceobject.route_index_output()); + self.obj().set_selected_no_send(deviceobject.route_index_output()); + } + + pub fn set_nodeobject(&self, new_nodeobject: Option<&PwNodeObject>) { + self.nodeobject.replace(new_nodeobject.cloned()); + + if let Some(nodeobject) = new_nodeobject { + let deviceobject = nodeobject.get_device().expect("device"); + + self.block_signal.set(true); + pwvucontrol_info!("self.route_dropdown.set_model({});", deviceobject.wpdevice().bound_id()); + self.route_dropdown.set_model(Some(&deviceobject.routemodel_output())); + pwvucontrol_info!("self.route_dropdown.set_selected({});", deviceobject.route_index_output()); + + self.route_dropdown.set_selected(deviceobject.route_index_output()); + + self.block_signal.set(false); + + deviceobject.connect_local("pre-update-route", false, + clone!(@weak self as widget => @default-return None, move |_| { + widget.block_signal.set(true); + + None + }) + ); + + deviceobject.connect_local("post-update-route", false, + clone!(@weak self as widget => @default-return None, move |_| { + widget.block_signal.set(false); + pwvucontrol_info!("About to call widget.update_selected() inside post-update-route handler"); + widget.update_selected(); + + None + }) + ); + + deviceobject.connect_route_index_output_notify( + clone!(@weak self as widget => move |_| widget.update_selected()) + ); + } else { + self.route_dropdown.set_model(gtk::gio::ListModel::NONE); + } + } + } + + #[glib::derived_properties] + impl ObjectImpl for PwRouteDropDown { + fn dispose(&self) { + self.dispose_template(); + } + + fn constructed(&self) { + self.parent_constructed(); + + fn setup_handler(item: &glib::Object, list: bool) { + let item: >k::ListItem = item.downcast_ref().expect("ListItem"); + let profilerow = PwProfileRow::new(); + + profilerow.setup::(item, list); + item.set_child(Some(&profilerow)); + } + + fn bind_handler(item: &glib::Object, dropdown: >k::DropDown) { + let item: >k::ListItem = item.downcast_ref().expect("ListItem"); + let profilerow = item + .child() + .and_downcast::() + .expect("PwProfileRow child"); + + let signal = dropdown.connect_selected_item_notify(clone!(@weak item => move |dropdown| { + let profilerow = item + .child() + .and_downcast::() + .expect("PwProfileRow child"); + profilerow.set_selected(dropdown.selected_item() == item.item()); + })); + profilerow.set_handlerid(Some(signal)); + } + + fn unbind_handler(item: &glib::Object) { + let item: >k::ListItem = item.downcast_ref().expect("ListItem"); + let profilerow = item + .child() + .and_downcast::() + .expect("The child has to be a `PwProfileRow`."); + profilerow.set_handlerid(None); + } + + let dropdown = self.route_dropdown.get(); + + let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(|_, item| setup_handler(item, false)); + + let list_factory = gtk::SignalListItemFactory::new(); + list_factory.connect_setup(|_, item| setup_handler(item, true)); + list_factory.connect_bind(clone!(@weak dropdown => move |_, item| bind_handler(item, &dropdown))); + list_factory.connect_unbind(|_, item| unbind_handler(item)); + + self.route_dropdown.set_factory(Some(&factory)); + self.route_dropdown.set_list_factory(Some(&list_factory)); + + self.route_dropdown.set_enable_search(true); + + let widget = self.obj(); + let selected_handler = closure_local!( + @watch widget => move |dropdown: >k::DropDown, _pspec: &glib::ParamSpec| { + wp::info!("selected"); + if widget.imp().block_signal.get() { + return; + } + + if let Some(nodeobject) = widget.nodeobject() { + pwvucontrol_critical!("Had set profile to {}", dropdown.selected()); + + if let Some(routeobject) = dropdown.selected_item().and_downcast::() { + nodeobject.set_route(routeobject.index()); + } + + } + }); + self.route_dropdown.connect_closure("notify::selected", true, selected_handler); + } + } + + impl WidgetImpl for PwRouteDropDown {} +} + +glib::wrapper! { + pub struct PwRouteDropDown(ObjectSubclass) @extends gtk::Widget; +} + +impl PwRouteDropDown { + + pub fn set_selected_no_send(&self, position: u32) { + let imp = self.imp(); + + imp.block_signal.set(true); + imp.route_dropdown.set_selected(position); + imp.block_signal.set(false); + } +} + +impl Default for PwRouteDropDown { + fn default() -> Self { + glib::Object::new() + } +} diff --git a/src/ui/sinkbox.rs b/src/ui/sinkbox.rs index 5d355b8..89410cf 100644 --- a/src/ui/sinkbox.rs +++ b/src/ui/sinkbox.rs @@ -10,8 +10,11 @@ use gtk::{prelude::*, subclass::prelude::*}; use std::cell::Cell; use wireplumber as wp; use super::volumebox::PwVolumeBoxExt; +use crate::ui::PwRouteDropDown; mod imp { + use crate::pwvucontrol_info; + use super::*; #[derive(Default, gtk::CompositeTemplate)] @@ -21,6 +24,9 @@ mod imp { #[template_child] pub default_sink_toggle: TemplateChild, + + #[template_child] + pub route_dropdown: TemplateChild, } #[glib::object_subclass] @@ -52,6 +58,14 @@ mod imp { glib::idle_add_local_once(clone!(@weak self as widget => move || { widget.obj().default_node_changed(); })); + + let obj = self.obj(); + let parent: &PwVolumeBox = obj.upcast_ref(); + let node = parent.node_object().expect("nodeobj"); + + pwvucontrol_info!("sinkbox set_nodeobject {}", node.name()); + + self.route_dropdown.set_nodeobject(Some(node)); } } impl WidgetImpl for PwSinkBox {} diff --git a/src/ui/volumebox.rs b/src/ui/volumebox.rs index 5cc8a48..5a24cd8 100644 --- a/src/ui/volumebox.rs +++ b/src/ui/volumebox.rs @@ -9,7 +9,7 @@ use crate::{ }; use glib::{clone, ControlFlow, closure_local, SignalHandlerId}; -use gtk::{gio, prelude::*, subclass::prelude::*}; +use gtk::{prelude::*, subclass::prelude::*}; use std::cell::{Cell, RefCell}; use once_cell::sync::OnceCell; use wireplumber as wp; @@ -256,7 +256,7 @@ impl PwVolumeBox { pub(crate) fn new(channel_object: &impl glib::IsA) -> Self { glib::Object::builder() .property("node-object", channel_object) - .property("channelmodel", gio::ListStore::new::()) + //.property("channelmodel", gio::ListStore::new::()) .build() }