diff --git a/Cargo.toml b/Cargo.toml index 65e2c1e..aed336b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,13 +7,14 @@ repository = "https://github.com/Xetibo/ReSet" license = "GPL-3.0-only" [dependencies] -reset_daemon = "1.0.1" -re_set-lib = "1.0.0" -adw = { version = "0.5.3", package = "libadwaita", features = ["v1_4"] } +#reset_daemon = "1.1.0" +re_set-lib = { git = "https://github.com/Xetibo/ReSet-Lib" } +reset_daemon = { git = "https://github.com/Xetibo/ReSet-Daemon", branch = "dashie" } +adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] } dbus = "0.9.7" -gtk = { version = "0.7.3", package = "gtk4", features = ["v4_12"] } -glib = "0.18.3" -tokio = { version = "1.33.0", features = [ +gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] } +glib = "0.19.3" +tokio = { version = "1.36.0", features = [ "rt", "time", "net", @@ -21,8 +22,8 @@ tokio = { version = "1.33.0", features = [ "rt-multi-thread", "sync", ] } -fork = "0.1.22" +fork = "0.1.23" ipnetwork = "0.20.0" [build-dependencies] -glib-build-tools = "0.18.0" +glib-build-tools = "0.19.0" diff --git a/better_test_plugin/Cargo.toml b/better_test_plugin/Cargo.toml new file mode 100644 index 0000000..cb1a105 --- /dev/null +++ b/better_test_plugin/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "better_test_plugin" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["dylib"] + +[dependencies] +re_set-lib = { git = "https://github.com/Xetibo/ReSet-Lib" } +gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] } +dbus = "0.9.7" +glib = "0.19.3" diff --git a/better_test_plugin/src/lib.rs b/better_test_plugin/src/lib.rs new file mode 100644 index 0000000..c820d9b --- /dev/null +++ b/better_test_plugin/src/lib.rs @@ -0,0 +1,84 @@ +use std::sync::Arc; +use std::time::Duration; + +use dbus::blocking::Connection; +use dbus::Error; +use gtk::{gio, Orientation}; +use gtk::prelude::{BoxExt, ButtonExt}; +use re_set_lib::utils::plugin::{PluginCapabilities, PluginImplementation, PluginTestFunc, SidebarInfo}; + +pub const BASE: &str = "org.Xetibo.ReSet.Daemon"; +pub const DBUS_PATH: &str = "/org/Xebito/ReSet/Plugins/test"; +pub const INTERFACE: &str = "org.Xetibo.ReSet.TestPlugin"; + +#[no_mangle] +#[allow(improper_ctypes_definitions)] +pub extern "C" fn capabilities() -> PluginCapabilities { + println!("frontend capabilities called"); + PluginCapabilities::new(vec!["frontend test"], PluginImplementation::Frontend) +} + +#[no_mangle] +pub extern "C" fn frontend_startup() { + println!("frontend startup called"); +} + +#[no_mangle] +pub extern "C" fn frontend_shutdown() { + println!("frontend shutdown called"); +} + +#[no_mangle] +#[allow(improper_ctypes_definitions)] +pub extern "C" fn frontend_data() -> (SidebarInfo, Vec) { + println!("frontend data called"); + let info = SidebarInfo { + name: "test", + icon_name: "microphone-disabled-symbolic", + parent: None, + }; + let box1 = gtk::Box::builder().orientation(Orientation::Vertical).build(); + let box2 = gtk::Box::builder().orientation(Orientation::Horizontal).build(); + + let label = Arc::new(LabelWrapper { + label: gtk::Label::builder().label("Hello, World!").build(), + }); + + let label2 = gtk::Label::builder().label("Bye, World!").build(); + let button = gtk::Button::builder().label("Click me!").build(); + box1.append(&label.label); + box2.append(&label2); + box2.append(&button); + + button.connect_clicked(move |_| { + let label = Arc::clone(&label); + gio::spawn_blocking(move || { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(String, u32), Error> = proxy.method_call(INTERFACE, "Test", ()); + let (text, age) = res.unwrap(); + label.label.set_text(&format!("Name: {}, Age: {}", text, age)); + }); + }); + + let boxes = vec![ + box1, box2, + ]; + + (info, boxes) +} + +#[no_mangle] +#[allow(improper_ctypes_definitions)] +pub extern "C" fn frontend_tests() -> Vec { + println!("frontend tests called"); + vec![] +} + +pub struct LabelWrapper { + label: gtk::Label, +} + +unsafe impl Send for LabelWrapper {} + +unsafe impl Sync for LabelWrapper {} \ No newline at end of file diff --git a/src/components/input/mod.rs b/src/components/audio/input/mod.rs similarity index 76% rename from src/components/input/mod.rs rename to src/components/audio/input/mod.rs index 79007e9..9ee1b34 100644 --- a/src/components/input/mod.rs +++ b/src/components/audio/input/mod.rs @@ -1,6 +1,8 @@ pub mod output_stream_entry; pub mod output_stream_entry_impl; pub mod source_box; +mod source_box_handlers; pub mod source_box_impl; +mod source_box_utils; pub mod source_entry; pub mod source_entry_impl; diff --git a/src/components/input/output_stream_entry.rs b/src/components/audio/input/output_stream_entry.rs similarity index 84% rename from src/components/input/output_stream_entry.rs rename to src/components/audio/input/output_stream_entry.rs index 2400c19..4cb848c 100644 --- a/src/components/input/output_stream_entry.rs +++ b/src/components/audio/input/output_stream_entry.rs @@ -1,16 +1,17 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; +use crate::components::base::error_impl::show_error; use crate::components::utils::{ create_dropdown_label_factory, set_combo_row_ellipsis, AUDIO, BASE, DBUS_PATH, }; -use adw::glib; use adw::glib::Object; use adw::prelude::{ButtonExt, ComboRowExt, PreferencesRowExt, RangeExt}; use dbus::blocking::Connection; use dbus::Error; use glib::subclass::types::ObjectSubclassIsExt; -use glib::{clone, Cast, Propagation}; +use glib::{clone, Propagation}; +use glib::prelude::Cast; use gtk::{gio, StringObject}; use re_set_lib::audio::audio_structures::OutputStream; @@ -30,6 +31,9 @@ impl OutputStreamEntry { pub fn new(source_box: Arc, stream: OutputStream) -> Self { let obj: Self = Object::builder().build(); // TODO use event callback for progress bar -> this is the "im speaking" indicator + let output_box_volume_ref = source_box.clone(); + let output_box_mute_ref = source_box.clone(); + let output_box_source_ref = source_box.clone(); { let index = stream.index; let box_imp = source_box.imp(); @@ -66,7 +70,7 @@ impl OutputStreamEntry { } *time = Some(SystemTime::now()); } - set_outputstream_volume(value, index, channels); + set_outputstream_volume(value, index, channels, output_box_volume_ref.clone()); Propagation::Proceed }), ); @@ -118,7 +122,7 @@ impl OutputStreamEntry { } let stream = stream.unwrap(); let source = source.unwrap().0; - set_source_of_output_stream(stream.index, source); + set_source_of_output_stream(stream.index, source, output_box_source_ref.clone()); }), ); imp.reset_source_mute @@ -139,53 +143,56 @@ impl OutputStreamEntry { imp.reset_source_mute .set_icon_name("audio-input-microphone-symbolic"); } - toggle_output_stream_mute(index, muted); + toggle_output_stream_mute(index, muted, output_box_mute_ref.clone()); })); } obj } } -fn set_outputstream_volume(value: f64, index: u32, channels: u16) -> bool { +fn set_outputstream_volume( + value: f64, + index: u32, + channels: u16, + input_box: Arc, +) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); - let _: Result<(), Error> = proxy.method_call( + let res: Result<(), Error> = proxy.method_call( AUDIO, "SetOutputStreamVolume", (index, channels, value as u32), ); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + if res.is_err() { + show_error::(input_box.clone(), "Failed to set output stream volume"); + } }); true } -fn toggle_output_stream_mute(index: u32, muted: bool) -> bool { +fn toggle_output_stream_mute(index: u32, muted: bool, input_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); - let _: Result<(), Error> = proxy.method_call(AUDIO, "SetOutputStreamMute", (index, muted)); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + let res: Result<(), Error> = + proxy.method_call(AUDIO, "SetOutputStreamMute", (index, muted)); + if res.is_err() { + show_error::(input_box.clone(), "Failed to mute output stream"); + } }); true } -fn set_source_of_output_stream(stream: u32, source: u32) -> bool { +fn set_source_of_output_stream(stream: u32, source: u32, input_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); - let _: Result<(bool,), Error> = + let res: Result<(bool,), Error> = proxy.method_call(AUDIO, "SetSourceOfOutputStream", (stream, source)); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + if res.is_err() { + show_error::(input_box.clone(), "Failed to set source of output stream"); + } }); true } diff --git a/src/components/input/output_stream_entry_impl.rs b/src/components/audio/input/output_stream_entry_impl.rs similarity index 92% rename from src/components/input/output_stream_entry_impl.rs rename to src/components/audio/input/output_stream_entry_impl.rs index 15687b2..fa368d2 100644 --- a/src/components/input/output_stream_entry_impl.rs +++ b/src/components/audio/input/output_stream_entry_impl.rs @@ -5,9 +5,9 @@ use std::cell::RefCell; use std::sync::Arc; use std::time::SystemTime; -use crate::components::input::output_stream_entry; +use crate::components::audio::input::output_stream_entry; use gtk::subclass::prelude::*; -use gtk::{glib, Button, CompositeTemplate, Label, Scale}; +use gtk::{Button, CompositeTemplate, Label, Scale}; #[derive(Default, CompositeTemplate)] #[template(resource = "/org/Xetibo/ReSet/resetOutputStreamEntry.ui")] diff --git a/src/components/audio/input/source_box.rs b/src/components/audio/input/source_box.rs new file mode 100644 index 0000000..1419984 --- /dev/null +++ b/src/components/audio/input/source_box.rs @@ -0,0 +1,203 @@ +use re_set_lib::signals::{ + OutputStreamAdded, OutputStreamChanged, OutputStreamRemoved, SourceAdded, SourceChanged, + SourceRemoved, +}; +use std::sync::Arc; + +use adw::glib::Object; +use adw::prelude::{ComboRowExt, ListBoxRowExt}; +use dbus::blocking::Connection; +use dbus::message::SignalArgs; +use dbus::Path; +use glib::subclass::prelude::ObjectSubclassIsExt; +use glib::Variant; +use gtk::gio; +use gtk::prelude::ActionableExt; + +use crate::components::audio::input::source_box_impl; +use crate::components::base::error::{self}; +use crate::components::base::error_impl::ReSetErrorImpl; +use crate::components::utils::{ + create_dropdown_label_factory, set_combo_row_ellipsis, BASE, DBUS_PATH, +}; + +use super::source_box_handlers::{ + output_stream_added_handler, output_stream_changed_handler, output_stream_removed_handler, + source_added_handler, source_changed_handler, source_removed_handler, +}; +use super::source_box_utils::{ + get_default_source, get_sources, populate_cards, populate_outputstreams, + populate_source_information, +}; + +glib::wrapper! { + pub struct SourceBox(ObjectSubclass) + @extends gtk::Box, gtk::Widget, + @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; +} + +unsafe impl Send for SourceBox {} +unsafe impl Sync for SourceBox {} + +impl ReSetErrorImpl for SourceBox { + fn error(&self) -> >k::subclass::prelude::TemplateChild { + &self.imp().error + } +} + +impl SourceBox { + pub fn new() -> Self { + let obj: Self = Object::builder().build(); + { + let imp = obj.imp(); + let mut model_index = imp.reset_model_index.write().unwrap(); + *model_index = 0; + } + obj + } + + pub fn setup_callbacks(&self) { + let self_imp = self.imp(); + self_imp.reset_source_row.set_activatable(true); + self_imp + .reset_source_row + .set_action_name(Some("navigation.push")); + self_imp + .reset_source_row + .set_action_target_value(Some(&Variant::from("sources"))); + self_imp.reset_cards_row.set_activatable(true); + self_imp + .reset_cards_row + .set_action_name(Some("navigation.push")); + self_imp + .reset_cards_row + .set_action_target_value(Some(&Variant::from("profileConfiguration"))); + + self_imp.reset_output_stream_button.set_activatable(true); + self_imp + .reset_output_stream_button + .set_action_name(Some("navigation.pop")); + + self_imp.reset_input_cards_back_button.set_activatable(true); + self_imp + .reset_input_cards_back_button + .set_action_name(Some("navigation.pop")); + + self_imp + .reset_source_dropdown + .set_factory(Some(&create_dropdown_label_factory())); + set_combo_row_ellipsis(self_imp.reset_source_dropdown.get()); + } +} + +impl Default for SourceBox { + fn default() -> Self { + Self::new() + } +} + +pub fn populate_sources(source_box: Arc) { + gio::spawn_blocking(move || { + let sources = get_sources(source_box.clone()); + { + let source_box_imp = source_box.imp(); + let list = source_box_imp.reset_model_list.write().unwrap(); + let mut map = source_box_imp.reset_source_map.write().unwrap(); + let mut model_index = source_box_imp.reset_model_index.write().unwrap(); + source_box_imp + .reset_default_source + .replace(get_default_source(source_box.clone())); + for source in sources.iter() { + list.append(&source.alias); + map.insert(source.alias.clone(), (source.index, source.name.clone())); + *model_index += 1; + } + } + + populate_outputstreams(source_box.clone()); + populate_cards(source_box.clone()); + populate_source_information(source_box, sources); + }); +} + +pub fn start_source_box_listener(conn: Connection, source_box: Arc) -> Connection { + let source_added = + SourceAdded::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))).static_clone(); + let source_removed = + SourceRemoved::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))).static_clone(); + let source_changed = + SourceChanged::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))).static_clone(); + let output_stream_added = + OutputStreamAdded::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))) + .static_clone(); + let output_stream_removed = + OutputStreamRemoved::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))) + .static_clone(); + let output_stream_changed = + OutputStreamChanged::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))) + .static_clone(); + + let source_added_box = source_box.clone(); + let source_removed_box = source_box.clone(); + let source_changed_box = source_box.clone(); + let output_stream_added_box = source_box.clone(); + let output_stream_removed_box = source_box.clone(); + let output_stream_changed_box = source_box.clone(); + + let res = conn.add_match(source_added, move |ir: SourceAdded, _, _| { + source_added_handler(source_added_box.clone(), ir) + }); + if res.is_err() { + // TODO: handle this with the log/error macro + println!("fail on source add event"); + return conn; + } + + let res = conn.add_match(source_removed, move |ir: SourceRemoved, _, _| { + source_removed_handler(source_removed_box.clone(), ir) + }); + if res.is_err() { + println!("fail on source remove event"); + return conn; + } + + let res = conn.add_match(source_changed, move |ir: SourceChanged, _, _| { + source_changed_handler(source_changed_box.clone(), ir) + }); + if res.is_err() { + println!("fail on source change event"); + return conn; + } + + let res = conn.add_match(output_stream_added, move |ir: OutputStreamAdded, _, _| { + output_stream_added_handler(output_stream_added_box.clone(), ir) + }); + if res.is_err() { + println!("fail on output stream add event"); + return conn; + } + + let res = conn.add_match( + output_stream_changed, + move |ir: OutputStreamChanged, _, _| { + output_stream_changed_handler(output_stream_changed_box.clone(), ir) + }, + ); + if res.is_err() { + println!("fail on output stream change event"); + return conn; + } + + let res = conn.add_match( + output_stream_removed, + move |ir: OutputStreamRemoved, _, _| { + output_stream_removed_handler(output_stream_removed_box.clone(), ir) + }, + ); + if res.is_err() { + println!("fail on output stream remove event"); + return conn; + } + + conn +} diff --git a/src/components/audio/input/source_box_handlers.rs b/src/components/audio/input/source_box_handlers.rs new file mode 100644 index 0000000..233108b --- /dev/null +++ b/src/components/audio/input/source_box_handlers.rs @@ -0,0 +1,309 @@ +use std::{ + sync::Arc, + time::{Duration, SystemTime}, +}; + +use adw::prelude::{ComboRowExt, PreferencesRowExt}; +use glib::{subclass::types::ObjectSubclassIsExt, ControlFlow, Propagation}; +use glib::prelude::Cast; +use gtk::{ + gio, + prelude::{BoxExt, ButtonExt, CheckButtonExt, ListBoxRowExt, RangeExt}, + StringObject, +}; +use re_set_lib::signals::{ + OutputStreamAdded, OutputStreamChanged, OutputStreamRemoved, SourceAdded, SourceChanged, + SourceRemoved, +}; + +use crate::components::base::list_entry::ListEntry; + +use super::{ + output_stream_entry::OutputStreamEntry, + source_box::SourceBox, + source_box_utils::{get_default_source_name, refresh_default_source}, + source_entry::{set_default_source, set_source_volume, toggle_source_mute, SourceEntry}, +}; + +pub fn source_added_handler(source_box: Arc, ir: SourceAdded) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let source_box = source_box.clone(); + let source_box_imp = source_box.imp(); + let source_index = ir.source.index; + let alias = ir.source.alias.clone(); + let name = ir.source.name.clone(); + let mut is_default = false; + if source_box_imp.reset_default_source.borrow().name == ir.source.name { + is_default = true; + } + let source_entry = Arc::new(SourceEntry::new( + is_default, + source_box_imp.reset_default_check_button.clone(), + ir.source, + source_box.clone(), + )); + let source_clone = source_entry.clone(); + let entry = Arc::new(ListEntry::new(&*source_entry)); + entry.set_activatable(false); + let mut list = source_box_imp.reset_source_list.write().unwrap(); + list.insert(source_index, (entry.clone(), source_clone, alias.clone())); + source_box_imp.reset_sources.append(&*entry); + let mut map = source_box_imp.reset_source_map.write().unwrap(); + let mut index = source_box_imp.reset_model_index.write().unwrap(); + let model_list = source_box_imp.reset_model_list.write().unwrap(); + if model_list.string(*index - 1) == Some("Monitor of Dummy Output".into()) { + model_list.append(&alias); + model_list.remove(*index - 1); + map.insert(alias, (source_index, name)); + source_box_imp.reset_source_dropdown.set_selected(0); + } else { + model_list.append(&alias); + map.insert(alias.clone(), (source_index, name)); + if alias == "Monitor of Dummy Output" { + source_box_imp.reset_source_dropdown.set_selected(0); + } + *index += 1; + } + }); + }); + true +} + +pub fn source_removed_handler(source_box: Arc, ir: SourceRemoved) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let source_box = source_box.clone(); + let source_box_imp = source_box.imp(); + let entry: Option<(Arc, Arc, String)>; + { + let mut list = source_box_imp.reset_source_list.write().unwrap(); + entry = list.remove(&ir.index); + if entry.is_none() { + return; + } + } + source_box_imp + .reset_sources + .remove(&*entry.clone().unwrap().0); + let mut map = source_box_imp.reset_source_map.write().unwrap(); + let alias = entry.unwrap().2; + map.remove(&alias); + let mut index = source_box_imp.reset_model_index.write().unwrap(); + let model_list = source_box_imp.reset_model_list.write().unwrap(); + + if *index == 1 { + model_list.append("Monitor of Dummy Output"); + } + for entry in 0..*index { + if model_list.string(entry) == Some(alias.clone().into()) { + model_list.splice(entry, 1, &[]); + break; + } + } + if *index > 1 { + *index -= 1; + } + }); + }); + true +} + +pub fn source_changed_handler(source_box: Arc, ir: SourceChanged) -> bool { + let default_source = get_default_source_name(source_box.clone()); + glib::spawn_future(async move { + glib::idle_add_once(move || { + let source_box = source_box.clone(); + let source_box_imp = source_box.imp(); + let is_default = ir.source.name == default_source; + let volume = ir.source.volume.first().unwrap_or(&0_u32); + let fraction = (*volume as f64 / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + + let list = source_box_imp.reset_source_list.read().unwrap(); + let entry = list.get(&ir.source.index); + if entry.is_none() { + return; + } + let imp = entry.unwrap().1.imp(); + if is_default { + source_box_imp.reset_volume_percentage.set_text(&percentage); + source_box_imp.reset_volume_slider.set_value(*volume as f64); + source_box_imp + .reset_default_source + .replace(ir.source.clone()); + if ir.source.muted { + source_box_imp + .reset_source_mute + .set_icon_name("microphone-disabled-symbolic"); + } else { + source_box_imp + .reset_source_mute + .set_icon_name("audio-input-microphone-symbolic"); + } + imp.reset_selected_source.set_active(true); + } else { + imp.reset_selected_source.set_active(false); + } + imp.reset_source_name + .set_title(ir.source.alias.clone().as_str()); + imp.reset_volume_percentage.set_text(&percentage); + imp.reset_volume_slider.set_value(*volume as f64); + if ir.source.muted { + imp.reset_source_mute + .set_icon_name("microphone-disabled-symbolic"); + } else { + imp.reset_source_mute + .set_icon_name("audio-input-microphone-symbolic"); + } + }); + }); + true +} + +pub fn output_stream_added_handler(source_box: Arc, ir: OutputStreamAdded) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let source_box = source_box.clone(); + let source_box_imp = source_box.imp(); + let mut list = source_box_imp.reset_output_stream_list.write().unwrap(); + let index = ir.stream.index; + let output_stream = Arc::new(OutputStreamEntry::new(source_box.clone(), ir.stream)); + let entry = Arc::new(ListEntry::new(&*output_stream)); + entry.set_activatable(false); + list.insert(index, (entry.clone(), output_stream.clone())); + source_box_imp.reset_output_streams.append(&*entry); + }); + }); + true +} + +pub fn output_stream_changed_handler(source_box: Arc, ir: OutputStreamChanged) -> bool { + let imp = source_box.imp(); + let alias: String; + { + let source_list = imp.reset_source_list.read().unwrap(); + if let Some(alias_opt) = source_list.get(&ir.stream.source_index) { + alias = alias_opt.2.clone(); + } else { + alias = String::from(""); + } + } + glib::spawn_future(async move { + glib::idle_add_once(move || { + let source_box = source_box.clone(); + let source_box_imp = source_box.imp(); + let entry: Arc; + { + let list = source_box_imp.reset_output_stream_list.read().unwrap(); + let entry_opt = list.get(&ir.stream.index); + if entry_opt.is_none() { + return; + } + entry = entry_opt.unwrap().1.clone(); + } + let imp = entry.imp(); + if ir.stream.muted { + imp.reset_source_mute + .set_icon_name("microphone-disabled-symbolic"); + } else { + imp.reset_source_mute + .set_icon_name("audio-input-microphone-symbolic"); + } + let name = ir.stream.application_name.clone() + ": " + ir.stream.name.as_str(); + imp.reset_source_selection.set_title(name.as_str()); + let volume = ir.stream.volume.first().unwrap_or(&0_u32); + let fraction = (*volume as f64 / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + imp.reset_volume_percentage.set_text(&percentage); + imp.reset_volume_slider.set_value(*volume as f64); + let index = source_box_imp.reset_model_index.read().unwrap(); + let model_list = source_box_imp.reset_model_list.read().unwrap(); + for entry in 0..*index { + if model_list.string(entry) == Some(alias.clone().into()) { + imp.reset_source_selection.set_selected(entry); + break; + } + } + }); + }); + true +} + +pub fn output_stream_removed_handler(source_box: Arc, ir: OutputStreamRemoved) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let source_box = source_box.clone(); + let source_box_imp = source_box.imp(); + let mut list = source_box_imp.reset_output_stream_list.write().unwrap(); + let entry = list.remove(&ir.index); + if entry.is_none() { + return; + } + source_box_imp + .reset_output_streams + .remove(&*entry.unwrap().0); + }); + }); + true +} + +pub fn dropdown_handler(source_box: Arc, dropdown: &adw::ComboRow) -> ControlFlow { + let source_box_imp = source_box.imp(); + let source_box_ref = source_box.clone(); + let selected = dropdown.selected_item(); + if selected.is_none() { + return ControlFlow::Break; + } + let selected = selected.unwrap(); + let selected = selected.downcast_ref::().unwrap(); + let selected = selected.string().to_string(); + let source = source_box_imp.reset_source_map.read().unwrap(); + let source = source.get(&selected); + if source.is_none() { + return ControlFlow::Break; + } + let source = Arc::new(source.unwrap().1.clone()); + gio::spawn_blocking(move || { + let result = set_default_source(source, source_box_ref.clone()); + if result.is_none() { + return ControlFlow::Break; + } + refresh_default_source(result.unwrap(), source_box_ref.clone(), false); + ControlFlow::Continue + }); + ControlFlow::Continue +} + +pub fn volume_slider_handler(source_box: Arc, value: f64) -> Propagation { + let imp = source_box.imp(); + let fraction = (value / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + imp.reset_volume_percentage.set_text(&percentage); + let source = imp.reset_default_source.borrow(); + let index = source.index; + let channels = source.channels; + { + let mut time = imp.volume_time_stamp.borrow_mut(); + if time.is_some() && time.unwrap().elapsed().unwrap() < Duration::from_millis(50) { + return Propagation::Proceed; + } + *time = Some(SystemTime::now()); + } + set_source_volume(value, index, channels, source_box.clone()); + Propagation::Proceed +} + +pub fn mute_clicked_handler(source_box_ref_mute: Arc) { + let imp = source_box_ref_mute.imp(); + let mut source = imp.reset_default_source.borrow_mut(); + source.muted = !source.muted; + if source.muted { + imp.reset_source_mute + .set_icon_name("microphone-disabled-symbolic"); + } else { + imp.reset_source_mute + .set_icon_name("audio-input-microphone-symbolic"); + } + toggle_source_mute(source.index, source.muted, source_box_ref_mute.clone()); +} diff --git a/src/components/input/source_box_impl.rs b/src/components/audio/input/source_box_impl.rs similarity index 93% rename from src/components/input/source_box_impl.rs rename to src/components/audio/input/source_box_impl.rs index da5b78f..619bfa3 100644 --- a/src/components/input/source_box_impl.rs +++ b/src/components/audio/input/source_box_impl.rs @@ -5,11 +5,12 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::time::SystemTime; +use crate::components::audio::input::source_box; +use crate::components::base::error::ReSetError; use crate::components::base::list_entry::ListEntry; -use crate::components::input::source_box; use gtk::subclass::prelude::*; -use gtk::{glib, CheckButton, CompositeTemplate, StringList, TemplateChild}; use gtk::{prelude::*, Button, Label, Scale}; +use gtk::{CheckButton, CompositeTemplate, StringList}; use super::output_stream_entry::OutputStreamEntry; use super::source_entry::SourceEntry; @@ -45,6 +46,8 @@ pub struct SourceBox { pub reset_input_cards_back_button: TemplateChild, #[template_child] pub reset_cards: TemplateChild, + #[template_child] + pub error: TemplateChild, pub reset_default_check_button: Arc, pub reset_default_source: Arc>, pub reset_source_list: SourceEntryMap, diff --git a/src/components/audio/input/source_box_utils.rs b/src/components/audio/input/source_box_utils.rs new file mode 100644 index 0000000..2669229 --- /dev/null +++ b/src/components/audio/input/source_box_utils.rs @@ -0,0 +1,229 @@ +use std::{sync::Arc, time::Duration}; + +use adw::prelude::{ComboRowExt, PreferencesGroupExt}; +use dbus::{blocking::Connection, Error}; +use glib::subclass::types::ObjectSubclassIsExt; +use gtk::{ + gio, + prelude::{BoxExt, ButtonExt, CheckButtonExt, ListBoxRowExt, RangeExt}, +}; +use re_set_lib::audio::audio_structures::{Card, OutputStream, Source}; + +use crate::components::{ + base::{card_entry::CardEntry, error_impl::show_error, list_entry::ListEntry}, + utils::{AUDIO, BASE, DBUS_PATH}, +}; + +use super::{ + output_stream_entry::OutputStreamEntry, + source_box::SourceBox, + source_box_handlers::{dropdown_handler, mute_clicked_handler, volume_slider_handler}, + source_entry::SourceEntry, +}; + +pub fn populate_source_information(source_box: Arc, sources: Vec) { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let source_box_ref_slider = source_box.clone(); + let source_box_ref_toggle = source_box.clone(); + let source_box_ref_mute = source_box.clone(); + let source_box_imp = source_box.imp(); + let default_sink = source_box_imp.reset_default_source.clone(); + let source = default_sink.borrow(); + + if source.muted { + source_box_imp + .reset_source_mute + .set_icon_name("microphone-disabled-symbolic"); + } else { + source_box_imp + .reset_source_mute + .set_icon_name("audio-input-microphone-symbolic"); + } + + let volume = source.volume.first().unwrap_or(&0_u32); + let fraction = (*volume as f64 / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + source_box_imp.reset_volume_percentage.set_text(&percentage); + source_box_imp.reset_volume_slider.set_value(*volume as f64); + let mut list = source_box_imp.reset_source_list.write().unwrap(); + for source in sources { + let index = source.index; + let alias = source.alias.clone(); + let mut is_default = false; + if source_box_imp.reset_default_source.borrow().name == source.name { + is_default = true; + } + let source_entry = Arc::new(SourceEntry::new( + is_default, + source_box_imp.reset_default_check_button.clone(), + source, + source_box.clone(), + )); + let source_clone = source_entry.clone(); + let entry = Arc::new(ListEntry::new(&*source_entry)); + entry.set_activatable(false); + list.insert(index, (entry.clone(), source_clone, alias)); + source_box_imp.reset_sources.append(&*entry); + } + let list = source_box_imp.reset_model_list.read().unwrap(); + source_box_imp.reset_source_dropdown.set_model(Some(&*list)); + let name = source_box_imp.reset_default_source.borrow(); + + let index = source_box_imp.reset_model_index.read().unwrap(); + let model_list = source_box_imp.reset_model_list.read().unwrap(); + for entry in 0..*index { + if model_list.string(entry) == Some(name.alias.clone().into()) { + source_box_imp.reset_source_dropdown.set_selected(entry); + break; + } + } + source_box_imp + .reset_source_dropdown + .connect_selected_notify(move |dropdown| { + dropdown_handler(source_box_ref_toggle.clone(), dropdown); + }); + source_box_imp + .reset_volume_slider + .connect_change_value(move |_, _, value| { + volume_slider_handler(source_box_ref_slider.clone(), value) + }); + + source_box_imp.reset_source_mute.connect_clicked(move |_| { + mute_clicked_handler(source_box_ref_mute.clone()); + }); + }); + }); +} + +pub fn refresh_default_source(new_source: Source, source_box: Arc, entry: bool) { + let volume = *new_source.volume.first().unwrap_or(&0_u32); + let fraction = (volume as f64 / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + glib::spawn_future(async move { + glib::idle_add_once(move || { + let imp = source_box.imp(); + if !entry { + let list = imp.reset_source_list.read().unwrap(); + let entry = list.get(&new_source.index); + if entry.is_none() { + return; + } + let entry_imp = entry.unwrap().1.imp(); + entry_imp.reset_selected_source.set_active(true); + } else { + let model_list = imp.reset_model_list.read().unwrap(); + for entry in 0..*imp.reset_model_index.read().unwrap() { + if model_list.string(entry) == Some(new_source.alias.clone().into()) { + imp.reset_source_dropdown.set_selected(entry); + break; + } + } + } + imp.reset_volume_percentage.set_text(&percentage); + imp.reset_volume_slider.set_value(volume as f64); + if new_source.muted { + imp.reset_source_mute + .set_icon_name("microphone-disabled-symbolic"); + } else { + imp.reset_source_mute + .set_icon_name("audio-input-microphone-symbolic"); + } + imp.reset_default_source.replace(new_source); + }); + }); +} + +pub fn populate_outputstreams(source_box: Arc) { + let source_box_ref = source_box.clone(); + + gio::spawn_blocking(move || { + let streams = get_output_streams(source_box.clone()); + glib::spawn_future(async move { + glib::idle_add_once(move || { + let source_box_imp = source_box_ref.imp(); + let mut list = source_box_imp.reset_output_stream_list.write().unwrap(); + for stream in streams { + let index = stream.index; + let input_stream = Arc::new(OutputStreamEntry::new(source_box.clone(), stream)); + let input_stream_clone = input_stream.clone(); + let entry = Arc::new(ListEntry::new(&*input_stream)); + entry.set_activatable(false); + list.insert(index, (entry.clone(), input_stream_clone)); + source_box_imp.reset_output_streams.append(&*entry); + } + }); + }); + }); +} + +pub fn populate_cards(source_box: Arc) { + gio::spawn_blocking(move || { + let source_box_ref = source_box.clone(); + let cards = get_cards(source_box.clone()); + glib::spawn_future(async move { + glib::idle_add_once(move || { + let imp = source_box_ref.imp(); + for card in cards { + imp.reset_cards.add(&CardEntry::new(card)); + } + }); + }); + }); +} + +pub fn get_output_streams(source_box: Arc) -> Vec { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Vec,), Error> = + proxy.method_call(AUDIO, "ListOutputStreams", ()); + if res.is_err() { + show_error::(source_box.clone(), "Failed to get output streams"); + return Vec::new(); + } + res.unwrap().0 +} + +pub fn get_sources(source_box: Arc) -> Vec { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Vec,), Error> = proxy.method_call(AUDIO, "ListSources", ()); + if res.is_err() { + show_error::(source_box.clone(), "Failed to get sources"); + return Vec::new(); + } + res.unwrap().0 +} + +pub fn get_cards(source_box: Arc) -> Vec { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Vec,), Error> = proxy.method_call(AUDIO, "ListCards", ()); + if res.is_err() { + show_error::(source_box.clone(), "Failed to get profiles"); + return Vec::new(); + } + res.unwrap().0 +} + +pub fn get_default_source_name(source_box: Arc) -> String { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(String,), Error> = proxy.method_call(AUDIO, "GetDefaultSourceName", ()); + if res.is_err() { + show_error::(source_box.clone(), "Failed to get default source name"); + return String::from(""); + } + res.unwrap().0 +} + +pub fn get_default_source(source_box: Arc) -> Source { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Source,), Error> = proxy.method_call(AUDIO, "GetDefaultSource", ()); + if res.is_err() { + show_error::(source_box.clone(), "Failed to get default source"); + return Source::default(); + } + res.unwrap().0 +} diff --git a/src/components/input/source_entry.rs b/src/components/audio/input/source_entry.rs similarity index 71% rename from src/components/input/source_entry.rs rename to src/components/audio/input/source_entry.rs index d822596..9fe131e 100644 --- a/src/components/input/source_entry.rs +++ b/src/components/audio/input/source_entry.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; -use adw::glib; +use crate::components::base::error_impl::show_error; +use crate::components::utils::set_action_row_ellipsis; use adw::glib::Object; use adw::prelude::{ButtonExt, CheckButtonExt, PreferencesRowExt, RangeExt}; use dbus::blocking::Connection; @@ -10,11 +11,11 @@ use glib::subclass::types::ObjectSubclassIsExt; use glib::{clone, Propagation}; use gtk::{gio, CheckButton}; use re_set_lib::audio::audio_structures::Source; -use crate::components::utils::set_action_row_ellipsis; -use crate::components::utils::{BASE, DBUS_PATH, AUDIO}; +use crate::components::utils::{AUDIO, BASE, DBUS_PATH}; -use super::source_box::{refresh_default_source, SourceBox}; +use super::source_box::SourceBox; +use super::source_box_utils::refresh_default_source; use super::source_entry_impl; glib::wrapper! { @@ -43,6 +44,8 @@ impl SourceEntry { let volume = source.volume.first().unwrap_or(&0_u32); let fraction = (*volume as f64 / 655.36).round(); let percentage = (fraction).to_string() + "%"; + let input_box_slider = input_box.clone(); + let input_box_ref = input_box.clone(); imp.reset_volume_percentage.set_text(&percentage); imp.reset_volume_slider.set_value(*volume as f64); imp.source.replace(source); @@ -63,7 +66,7 @@ impl SourceEntry { } *time = Some(SystemTime::now()); } - set_source_volume(value, index, channels); + set_source_volume(value, index, channels, input_box_slider.clone()); Propagation::Proceed }), ); @@ -78,7 +81,7 @@ impl SourceEntry { if button.is_active() { let name = name.clone(); gio::spawn_blocking(move || { - let result = set_default_source(name); + let result = set_default_source(name, input_box.clone()); if result.is_none() { return; } @@ -97,7 +100,7 @@ impl SourceEntry { imp.reset_source_mute .set_icon_name("audio-input-microphone-symbolic"); } - toggle_source_mute(source.index, source.muted); + toggle_source_mute(source.index, source.muted, input_box_ref.clone()); })); set_action_row_ellipsis(imp.reset_source_name.get()); } @@ -105,58 +108,39 @@ impl SourceEntry { } } -pub fn set_source_volume(value: f64, index: u32, channels: u16) -> bool { +pub fn set_source_volume(value: f64, index: u32, channels: u16, input_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy( - BASE, - DBUS_PATH, - Duration::from_millis(1000), - ); - let _: Result<(), Error> = proxy.method_call( - AUDIO, - "SetSourceVolume", - (index, channels, value as u32), - ); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(), Error> = + proxy.method_call(AUDIO, "SetSourceVolume", (index, channels, value as u32)); + if res.is_err() { + // TODO: also log this with LOG/ERROR + show_error::(input_box.clone(), "Failed to set source volume"); + } }); true } -pub fn toggle_source_mute(index: u32, muted: bool) -> bool { +pub fn toggle_source_mute(index: u32, muted: bool, input_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy( - BASE, - DBUS_PATH, - Duration::from_millis(1000), - ); - let _: Result<(), Error> = - proxy.method_call(AUDIO, "SetSourceMute", (index, muted)); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(), Error> = proxy.method_call(AUDIO, "SetSourceMute", (index, muted)); + if res.is_err() { + show_error::(input_box.clone(), "Failed to mute source"); + } }); true } -pub fn set_default_source(name: Arc) -> Option { +pub fn set_default_source(name: Arc, input_box: Arc) -> Option { let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy( - BASE, - DBUS_PATH, - Duration::from_millis(1000), - ); - let res: Result<(Source,), Error> = proxy.method_call( - AUDIO, - "SetDefaultSource", - (name.as_str(),), - ); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Source,), Error> = + proxy.method_call(AUDIO, "SetDefaultSource", (name.as_str(),)); if res.is_err() { + show_error::(input_box.clone(), "Failed to set default source"); return None; } Some(res.unwrap().0) diff --git a/src/components/input/source_entry_impl.rs b/src/components/audio/input/source_entry_impl.rs similarity index 95% rename from src/components/input/source_entry_impl.rs rename to src/components/audio/input/source_entry_impl.rs index 142b8a5..16a9900 100644 --- a/src/components/input/source_entry_impl.rs +++ b/src/components/audio/input/source_entry_impl.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use std::time::SystemTime; use gtk::subclass::prelude::*; -use gtk::{glib, Button, CheckButton, CompositeTemplate, Label, Scale}; +use gtk::{Button, CheckButton, CompositeTemplate, Label, Scale}; use super::source_entry; diff --git a/src/components/audio/mod.rs b/src/components/audio/mod.rs new file mode 100644 index 0000000..7d1a548 --- /dev/null +++ b/src/components/audio/mod.rs @@ -0,0 +1,2 @@ +pub mod input; +pub mod output; diff --git a/src/components/output/input_stream_entry.rs b/src/components/audio/output/input_stream_entry.rs similarity index 84% rename from src/components/output/input_stream_entry.rs rename to src/components/audio/output/input_stream_entry.rs index 52de3ba..c66b26a 100644 --- a/src/components/output/input_stream_entry.rs +++ b/src/components/audio/output/input_stream_entry.rs @@ -1,16 +1,17 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; +use crate::components::base::error_impl::show_error; use crate::components::utils::{ create_dropdown_label_factory, set_combo_row_ellipsis, AUDIO, BASE, DBUS_PATH, }; -use adw::glib; use adw::glib::Object; use adw::prelude::{ButtonExt, ComboRowExt, PreferencesRowExt, RangeExt}; use dbus::blocking::Connection; use dbus::Error; use glib::subclass::types::ObjectSubclassIsExt; -use glib::{clone, Cast, Propagation}; +use glib::{clone, Propagation}; +use glib::prelude::Cast; use gtk::{gio, StringObject}; use re_set_lib::audio::audio_structures::InputStream; @@ -30,6 +31,9 @@ impl InputStreamEntry { pub fn new(sink_box: Arc, stream: InputStream) -> Self { let obj: Self = Object::builder().build(); // TODO use event callback for progress bar -> this is the "im speaking" indicator + let output_box_mute_ref = sink_box.clone(); + let output_box_volume_ref = sink_box.clone(); + let output_box_sink_ref = sink_box.clone(); { let index = stream.sink_index; let box_imp = sink_box.imp(); @@ -75,7 +79,7 @@ impl InputStreamEntry { } *time = Some(SystemTime::now()); } - set_inputstream_volume(value, index, channels); + set_inputstream_volume(value, index, channels, output_box_volume_ref.clone()); Propagation::Proceed }), ); @@ -131,7 +135,7 @@ impl InputStreamEntry { } let stream = stream.unwrap(); let sink = sink.unwrap().0; - set_sink_of_input_stream(stream.index, sink); + set_sink_of_input_stream(stream.index, sink, output_box_sink_ref.clone()); }), ); imp.reset_sink_mute @@ -152,54 +156,50 @@ impl InputStreamEntry { imp.reset_sink_mute .set_icon_name("audio-volume-high-symbolic"); } - toggle_input_stream_mute(index, muted); + toggle_input_stream_mute(index, muted, output_box_mute_ref.clone()); })); } obj } } -fn set_inputstream_volume(value: f64, index: u32, channels: u16) -> bool { +fn set_inputstream_volume(value: f64, index: u32, channels: u16, output_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); - let _: Result<(), Error> = proxy.method_call( + let res: Result<(), Error> = proxy.method_call( AUDIO, "SetInputStreamVolume", (index, channels, value as u32), ); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + if res.is_err() { + show_error::(output_box.clone(), "Failed to set input stream volume"); + } }); true } -fn toggle_input_stream_mute(index: u32, muted: bool) -> bool { +fn toggle_input_stream_mute(index: u32, muted: bool, output_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); - let _: Result<(), Error> = proxy.method_call(AUDIO, "SetInputStreamMute", (index, muted)); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + let res: Result<(), Error> = proxy.method_call(AUDIO, "SetInputStreamMute", (index, muted)); + if res.is_err() { + show_error::(output_box.clone(), "Failed to mute input stream"); + } }); true } -fn set_sink_of_input_stream(stream: u32, sink: u32) -> bool { +fn set_sink_of_input_stream(stream: u32, sink: u32, output_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); - let _: Result<(), Error> = proxy.method_call(AUDIO, "SetSinkOfInputStream", (stream, sink)); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + let res: Result<(), Error> = + proxy.method_call(AUDIO, "SetSinkOfInputStream", (stream, sink)); + if res.is_err() { + show_error::(output_box.clone(), "Failed to set sink of input stream"); + } }); true } - -// TODO propagate error from dbus diff --git a/src/components/output/input_stream_entry_impl.rs b/src/components/audio/output/input_stream_entry_impl.rs similarity index 96% rename from src/components/output/input_stream_entry_impl.rs rename to src/components/audio/output/input_stream_entry_impl.rs index acdf98f..b336409 100644 --- a/src/components/output/input_stream_entry_impl.rs +++ b/src/components/audio/output/input_stream_entry_impl.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use std::time::SystemTime; use gtk::subclass::prelude::*; -use gtk::{glib, Button, CompositeTemplate, Label, Scale}; +use gtk::{Button, CompositeTemplate, Label, Scale}; use super::input_stream_entry; diff --git a/src/components/output/mod.rs b/src/components/audio/output/mod.rs similarity index 77% rename from src/components/output/mod.rs rename to src/components/audio/output/mod.rs index f26058a..5938435 100644 --- a/src/components/output/mod.rs +++ b/src/components/audio/output/mod.rs @@ -1,6 +1,8 @@ pub mod input_stream_entry; pub mod input_stream_entry_impl; pub mod sink_box; +mod sink_box_handlers; pub mod sink_box_impl; +mod sink_box_utils; pub mod sink_entry; pub mod sink_entry_impl; diff --git a/src/components/audio/output/sink_box.rs b/src/components/audio/output/sink_box.rs new file mode 100644 index 0000000..c5ca269 --- /dev/null +++ b/src/components/audio/output/sink_box.rs @@ -0,0 +1,203 @@ +use re_set_lib::signals::InputStreamAdded; +use re_set_lib::signals::InputStreamChanged; +use re_set_lib::signals::InputStreamRemoved; +use re_set_lib::signals::SinkAdded; +use re_set_lib::signals::SinkChanged; +use re_set_lib::signals::SinkRemoved; +use std::sync::Arc; + +use adw::glib::Object; +use adw::prelude::ComboRowExt; +use adw::prelude::ListBoxRowExt; +use dbus::blocking::Connection; +use dbus::message::SignalArgs; +use dbus::Path; +use glib::subclass::prelude::ObjectSubclassIsExt; +use glib::Variant; +use gtk::gio; +use gtk::prelude::ActionableExt; + +use crate::components::base::error_impl::ReSetErrorImpl; +use crate::components::utils::BASE; +use crate::components::utils::DBUS_PATH; +use crate::components::utils::{create_dropdown_label_factory, set_combo_row_ellipsis}; + +use super::sink_box_handlers::input_stream_added_handler; +use super::sink_box_handlers::input_stream_changed_handler; +use super::sink_box_handlers::input_stream_removed_handler; +use super::sink_box_handlers::sink_added_handler; +use super::sink_box_handlers::sink_changed_handler; +use super::sink_box_handlers::sink_removed_handler; +use super::sink_box_impl; +use super::sink_box_utils::get_default_sink; +use super::sink_box_utils::get_sinks; +use super::sink_box_utils::populate_cards; +use super::sink_box_utils::populate_inputstreams; +use super::sink_box_utils::populate_sink_information; + +glib::wrapper! { + pub struct SinkBox(ObjectSubclass) + @extends gtk::Box, gtk::Widget, + @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; +} + +unsafe impl Send for SinkBox {} +unsafe impl Sync for SinkBox {} + +impl ReSetErrorImpl for SinkBox { + fn error( + &self, + ) -> >k::subclass::prelude::TemplateChild { + &self.imp().error + } +} + +impl SinkBox { + pub fn new() -> Self { + let obj: Self = Object::builder().build(); + { + let imp = obj.imp(); + let mut model_index = imp.reset_model_index.write().unwrap(); + *model_index = 0; + } + obj + } + + pub fn setup_callbacks(&self) { + let self_imp = self.imp(); + self_imp.reset_sinks_row.set_activatable(true); + self_imp + .reset_sinks_row + .set_action_name(Some("navigation.push")); + self_imp + .reset_sinks_row + .set_action_target_value(Some(&Variant::from("outputDevices"))); + self_imp.reset_cards_row.set_activatable(true); + self_imp + .reset_cards_row + .set_action_name(Some("navigation.push")); + self_imp + .reset_cards_row + .set_action_target_value(Some(&Variant::from("profileConfiguration"))); + + self_imp.reset_input_stream_button.set_activatable(true); + self_imp + .reset_input_stream_button + .set_action_name(Some("navigation.pop")); + + self_imp.reset_input_cards_back_button.set_activatable(true); + self_imp + .reset_input_cards_back_button + .set_action_name(Some("navigation.pop")); + + self_imp + .reset_sink_dropdown + .set_factory(Some(&create_dropdown_label_factory())); + set_combo_row_ellipsis(self_imp.reset_sink_dropdown.get()); + } +} + +impl Default for SinkBox { + fn default() -> Self { + Self::new() + } +} + +pub fn populate_sinks(sink_box: Arc) { + gio::spawn_blocking(move || { + let sinks = get_sinks(sink_box.clone()); + { + let sink_box_imp = sink_box.imp(); + let list = sink_box_imp.reset_model_list.write().unwrap(); + let mut map = sink_box_imp.reset_sink_map.write().unwrap(); + let mut model_index = sink_box_imp.reset_model_index.write().unwrap(); + sink_box_imp + .reset_default_sink + .replace(get_default_sink(sink_box.clone())); + for sink in sinks.iter() { + list.append(&sink.alias); + map.insert(sink.alias.clone(), (sink.index, sink.name.clone())); + *model_index += 1; + } + } + populate_inputstreams(sink_box.clone()); + populate_cards(sink_box.clone()); + populate_sink_information(sink_box, sinks); + }); +} + +pub fn start_sink_box_listener(conn: Connection, sink_box: Arc) -> Connection { + let sink_added = + SinkAdded::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))).static_clone(); + let sink_removed = + SinkRemoved::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))).static_clone(); + let sink_changed = + SinkChanged::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))).static_clone(); + let input_stream_added = + InputStreamAdded::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))) + .static_clone(); + let input_stream_removed = + InputStreamRemoved::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))) + .static_clone(); + let input_stream_changed = + InputStreamChanged::match_rule(Some(&BASE.into()), Some(&Path::from(DBUS_PATH))) + .static_clone(); + + let sink_added_box = sink_box.clone(); + let sink_removed_box = sink_box.clone(); + let sink_changed_box = sink_box.clone(); + let input_stream_added_box = sink_box.clone(); + let input_stream_removed_box = sink_box.clone(); + let input_stream_changed_box = sink_box.clone(); + + let res = conn.add_match(sink_added, move |ir: SinkAdded, _, _| { + sink_added_handler(sink_added_box.clone(), ir) + }); + if res.is_err() { + // TODO: handle this with the log/error macro + println!("fail on sink add event"); + return conn; + } + + let res = conn.add_match(sink_removed, move |ir: SinkRemoved, _, _| { + sink_removed_handler(sink_removed_box.clone(), ir) + }); + if res.is_err() { + println!("fail on sink remove event"); + return conn; + } + + let res = conn.add_match(sink_changed, move |ir: SinkChanged, _, _| { + sink_changed_handler(sink_changed_box.clone(), ir) + }); + if res.is_err() { + println!("fail on sink change event"); + return conn; + } + + let res = conn.add_match(input_stream_added, move |ir: InputStreamAdded, _, _| { + input_stream_added_handler(input_stream_added_box.clone(), ir) + }); + if res.is_err() { + println!("fail on input stream add event"); + return conn; + } + + let res = conn.add_match(input_stream_removed, move |ir: InputStreamRemoved, _, _| { + input_stream_removed_handler(input_stream_removed_box.clone(), ir) + }); + if res.is_err() { + println!("fail on input stream remove event"); + return conn; + } + + let res = conn.add_match(input_stream_changed, move |ir: InputStreamChanged, _, _| { + input_stream_changed_handler(input_stream_changed_box.clone(), ir) + }); + if res.is_err() { + println!("fail on input stream change event"); + return conn; + } + + conn +} diff --git a/src/components/audio/output/sink_box_handlers.rs b/src/components/audio/output/sink_box_handlers.rs new file mode 100644 index 0000000..ea75d03 --- /dev/null +++ b/src/components/audio/output/sink_box_handlers.rs @@ -0,0 +1,308 @@ +use std::{ + sync::Arc, + time::{Duration, SystemTime}, +}; + +use adw::{ + prelude::{ComboRowExt, PreferencesRowExt}, + ComboRow, +}; +use glib::{subclass::types::ObjectSubclassIsExt, Propagation}; +use glib::prelude::Cast; +use gtk::{ + gio, + prelude::{BoxExt, ButtonExt, CheckButtonExt, ListBoxRowExt, RangeExt}, + StringObject, +}; +use re_set_lib::signals::{ + InputStreamAdded, InputStreamChanged, InputStreamRemoved, SinkAdded, SinkChanged, SinkRemoved, +}; + +use crate::components::base::list_entry::ListEntry; + +use super::{ + input_stream_entry::InputStreamEntry, + sink_box::SinkBox, + sink_box_utils::{get_default_sink_name, refresh_default_sink}, + sink_entry::{set_default_sink, set_sink_volume, toggle_sink_mute, SinkEntry}, +}; + +pub fn drop_down_handler(sink_box: Arc, dropdown: &ComboRow) { + let sink_box_ref = sink_box.clone(); + let sink_box_imp = sink_box.imp(); + let selected = dropdown.selected_item(); + if selected.is_none() { + return; + } + let selected = selected.unwrap(); + let selected = selected.downcast_ref::().unwrap(); + let selected = selected.string().to_string(); + + let sink = sink_box_imp.reset_sink_map.read().unwrap(); + let sink = sink.get(&selected); + if sink.is_none() { + return; + } + let new_sink_name = Arc::new(sink.unwrap().1.clone()); + gio::spawn_blocking(move || { + let result = set_default_sink(new_sink_name, sink_box_ref.clone()); + if result.is_none() { + return; + } + let new_sink = result.unwrap(); + refresh_default_sink(new_sink, sink_box_ref, false); + }); +} + +pub fn volume_slider_handler(sink_box: Arc, value: f64) -> glib::Propagation { + let imp = sink_box.imp(); + let fraction = (value / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + imp.reset_volume_percentage.set_text(&percentage); + let sink = imp.reset_default_sink.borrow(); + let index = sink.index; + let channels = sink.channels; + { + let mut time = imp.volume_time_stamp.borrow_mut(); + if time.is_some() && time.unwrap().elapsed().unwrap() < Duration::from_millis(50) { + return Propagation::Proceed; + } + *time = Some(SystemTime::now()); + } + set_sink_volume(value, index, channels, sink_box.clone()); + Propagation::Proceed +} + +pub fn mute_handler(sink_box: Arc) { + let imp = sink_box.imp(); + let mut stream = imp.reset_default_sink.borrow_mut(); + stream.muted = !stream.muted; + if stream.muted { + imp.reset_sink_mute + .set_icon_name("audio-volume-muted-symbolic"); + } else { + imp.reset_sink_mute + .set_icon_name("audio-volume-high-symbolic"); + } + toggle_sink_mute(stream.index, stream.muted, sink_box.clone()); +} + +pub fn sink_added_handler(sink_box: Arc, ir: SinkAdded) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let sink_box_imp = sink_box.imp(); + let sink_index = ir.sink.index; + let alias = ir.sink.alias.clone(); + let name = ir.sink.name.clone(); + let mut is_default = false; + + if sink_box_imp.reset_default_sink.borrow().name == ir.sink.name { + is_default = true; + } + let sink_entry = Arc::new(SinkEntry::new( + is_default, + sink_box_imp.reset_default_check_button.clone(), + ir.sink, + sink_box.clone(), + )); + let sink_clone = sink_entry.clone(); + let entry = Arc::new(ListEntry::new(&*sink_entry)); + entry.set_activatable(false); + let mut list = sink_box_imp.reset_sink_list.write().unwrap(); + list.insert(sink_index, (entry.clone(), sink_clone, alias.clone())); + sink_box_imp.reset_sinks.append(&*entry); + let mut map = sink_box_imp.reset_sink_map.write().unwrap(); + let mut index = sink_box_imp.reset_model_index.write().unwrap(); + let model_list = sink_box_imp.reset_model_list.write().unwrap(); + if model_list.string(*index - 1) == Some("Dummy Output".into()) { + model_list.append(&alias); + model_list.remove(*index - 1); + map.insert(alias, (sink_index, name)); + sink_box_imp.reset_sink_dropdown.set_selected(0); + } else { + model_list.append(&alias); + map.insert(alias.clone(), (sink_index, name)); + if alias == "Dummy Output" { + sink_box_imp.reset_sink_dropdown.set_selected(0); + } + *index += 1; + } + }); + }); + true +} + +pub fn sink_removed_handler(sink_box: Arc, ir: SinkRemoved) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let sink_box_imp = sink_box.imp(); + + let entry: Option<(Arc, Arc, String)>; + { + let mut list = sink_box_imp.reset_sink_list.write().unwrap(); + entry = list.remove(&ir.index); + if entry.is_none() { + return; + } + } + sink_box_imp.reset_sinks.remove(&*entry.clone().unwrap().0); + let alias = entry.unwrap().2; + let mut index = sink_box_imp.reset_model_index.write().unwrap(); + let model_list = sink_box_imp.reset_model_list.write().unwrap(); + + // add dummy entry when no other devices are available + if *index == 1 { + model_list.append("Dummy Output"); + } + + let mut map = sink_box_imp.reset_sink_map.write().unwrap(); + map.remove(&alias); + + for entry in 0..*index { + if model_list.string(entry) == Some(alias.clone().into()) { + model_list.splice(entry, 1, &[]); + break; + } + } + + // dummy enforces a minimum of 1 + if *index > 1 { + *index -= 1; + } + }); + }); + true +} + +pub fn sink_changed_handler(sink_box: Arc, ir: SinkChanged) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let default_sink = get_default_sink_name(sink_box.clone()); + let sink_box_imp = sink_box.imp(); + let is_default = ir.sink.name == default_sink; + let volume = ir.sink.volume.first().unwrap_or(&0_u32); + let fraction = (*volume as f64 / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + + let list = sink_box_imp.reset_sink_list.read().unwrap(); + let entry = list.get(&ir.sink.index); + if entry.is_none() { + return; + } + let imp = entry.unwrap().1.imp(); + if is_default { + sink_box_imp.reset_volume_percentage.set_text(&percentage); + sink_box_imp.reset_volume_slider.set_value(*volume as f64); + sink_box_imp.reset_default_sink.replace(ir.sink.clone()); + if ir.sink.muted { + sink_box_imp + .reset_sink_mute + .set_icon_name("audio-volume-muted-symbolic"); + } else { + sink_box_imp + .reset_sink_mute + .set_icon_name("audio-volume-high-symbolic"); + } + imp.reset_selected_sink.set_active(true); + } else { + imp.reset_selected_sink.set_active(false); + } + imp.reset_sink_name + .set_title(ir.sink.alias.clone().as_str()); + imp.reset_volume_percentage.set_text(&percentage); + imp.reset_volume_slider.set_value(*volume as f64); + if ir.sink.muted { + imp.reset_sink_mute + .set_icon_name("audio-volume-muted-symbolic"); + } else { + imp.reset_sink_mute + .set_icon_name("audio-volume-high-symbolic"); + } + }); + }); + true +} + +pub fn input_stream_added_handler(sink_box: Arc, ir: InputStreamAdded) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let sink_box_imp = sink_box.imp(); + let mut list = sink_box_imp.reset_input_stream_list.write().unwrap(); + let index = ir.stream.index; + let input_stream = Arc::new(InputStreamEntry::new(sink_box.clone(), ir.stream)); + let entry = Arc::new(ListEntry::new(&*input_stream)); + entry.set_activatable(false); + list.insert(index, (entry.clone(), input_stream.clone())); + sink_box_imp.reset_input_streams.append(&*entry); + }); + }); + true +} + +pub fn input_stream_removed_handler(sink_box: Arc, ir: InputStreamRemoved) -> bool { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let sink_box_imp = sink_box.imp(); + let mut list = sink_box_imp.reset_input_stream_list.write().unwrap(); + let entry = list.remove(&ir.index); + if entry.is_none() { + return; + } + sink_box_imp.reset_input_streams.remove(&*entry.unwrap().0); + }); + }); + true +} + +pub fn input_stream_changed_handler(sink_box: Arc, ir: InputStreamChanged) -> bool { + let imp = sink_box.imp(); + let alias: String; + { + let sink_list = imp.reset_sink_list.read().unwrap(); + if let Some(alias_opt) = sink_list.get(&ir.stream.sink_index) { + alias = alias_opt.2.clone(); + } else { + alias = String::from(""); + } + } + let sink_box = sink_box.clone(); + glib::spawn_future(async move { + glib::idle_add_once(move || { + let sink_box = sink_box.clone(); + let sink_box_imp = sink_box.imp(); + let entry: Arc; + { + let list = sink_box_imp.reset_input_stream_list.read().unwrap(); + let entry_opt = list.get(&ir.stream.index); + if entry_opt.is_none() { + return; + } + entry = entry_opt.unwrap().1.clone(); + } + let imp = entry.imp(); + if ir.stream.muted { + imp.reset_sink_mute + .set_icon_name("audio-volume-muted-symbolic"); + } else { + imp.reset_sink_mute + .set_icon_name("audio-volume-high-symbolic"); + } + let name = ir.stream.application_name.clone() + ": " + ir.stream.name.as_str(); + imp.reset_sink_selection.set_title(name.as_str()); + let volume = ir.stream.volume.first().unwrap_or(&0_u32); + let fraction = (*volume as f64 / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + imp.reset_volume_percentage.set_text(&percentage); + imp.reset_volume_slider.set_value(*volume as f64); + let index = sink_box_imp.reset_model_index.read().unwrap(); + let model_list = sink_box_imp.reset_model_list.read().unwrap(); + for entry in 0..*index { + if model_list.string(entry) == Some(alias.clone().into()) { + imp.reset_sink_selection.set_selected(entry); + break; + } + } + }); + }); + true +} diff --git a/src/components/output/sink_box_impl.rs b/src/components/audio/output/sink_box_impl.rs similarity index 91% rename from src/components/output/sink_box_impl.rs rename to src/components/audio/output/sink_box_impl.rs index 52a7031..deda284 100644 --- a/src/components/output/sink_box_impl.rs +++ b/src/components/audio/output/sink_box_impl.rs @@ -5,11 +5,12 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::time::SystemTime; +use crate::components::audio::output::input_stream_entry::InputStreamEntry; +use crate::components::base::error::ReSetError; use crate::components::base::list_entry::ListEntry; -use crate::components::output::input_stream_entry::InputStreamEntry; use gtk::subclass::prelude::*; -use gtk::{glib, Box, Button, CheckButton, CompositeTemplate, Label, StringList, TemplateChild}; use gtk::{prelude::*, Scale}; +use gtk::{Box, Button, CheckButton, CompositeTemplate, Label, StringList}; use super::sink_box; use super::sink_entry::SinkEntry; @@ -45,6 +46,8 @@ pub struct SinkBox { pub reset_input_cards_back_button: TemplateChild, #[template_child] pub reset_cards: TemplateChild, + #[template_child] + pub error: TemplateChild, pub reset_default_check_button: Arc, pub reset_default_sink: Arc>, pub reset_sink_list: SinkEntryMap, diff --git a/src/components/audio/output/sink_box_utils.rs b/src/components/audio/output/sink_box_utils.rs new file mode 100644 index 0000000..9b32ef5 --- /dev/null +++ b/src/components/audio/output/sink_box_utils.rs @@ -0,0 +1,232 @@ +use std::{sync::Arc, time::Duration}; + +use adw::prelude::{ComboRowExt, PreferencesGroupExt}; +use dbus::{blocking::Connection, Error}; +use glib::subclass::types::ObjectSubclassIsExt; +use gtk::{ + gio, + prelude::{BoxExt, ButtonExt, CheckButtonExt, ListBoxRowExt, RangeExt}, +}; +use re_set_lib::audio::audio_structures::{Card, InputStream, Sink}; + +use crate::components::{ + base::{card_entry::CardEntry, error_impl::show_error, list_entry::ListEntry}, + utils::{AUDIO, BASE, DBUS_PATH}, +}; + +use super::{ + input_stream_entry::InputStreamEntry, + sink_box::SinkBox, + sink_box_handlers::{drop_down_handler, mute_handler, volume_slider_handler}, + sink_entry::SinkEntry, +}; + +pub fn populate_sink_information(sink_box: Arc, sinks: Vec) { + glib::spawn_future(async move { + glib::idle_add_once(move || { + let sink_box_ref_select = sink_box.clone(); + let sink_box_ref_slider = sink_box.clone(); + let sink_box_ref_mute = sink_box.clone(); + let sink_box_ref = sink_box.clone(); + { + let sink_box_imp = sink_box_ref.imp(); + let default_sink = sink_box_imp.reset_default_sink.clone(); + let sink = default_sink.borrow(); + + if sink.muted { + sink_box_imp + .reset_sink_mute + .set_icon_name("audio-volume-muted-symbolic"); + } else { + sink_box_imp + .reset_sink_mute + .set_icon_name("audio-volume-high-symbolic"); + } + + let volume = sink.volume.first().unwrap_or(&0); + let fraction = (*volume as f64 / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + sink_box_imp.reset_volume_percentage.set_text(&percentage); + sink_box_imp.reset_volume_slider.set_value(*volume as f64); + let mut list = sink_box_imp.reset_sink_list.write().unwrap(); + for sink in sinks { + let index = sink.index; + let alias = sink.alias.clone(); + let mut is_default = false; + if sink_box_imp.reset_default_sink.borrow().name == sink.name { + is_default = true; + } + let sink_entry = Arc::new(SinkEntry::new( + is_default, + sink_box_imp.reset_default_check_button.clone(), + sink, + sink_box.clone(), + )); + let sink_clone = sink_entry.clone(); + let entry = Arc::new(ListEntry::new(&*sink_entry)); + entry.set_activatable(false); + list.insert(index, (entry.clone(), sink_clone, alias)); + sink_box_imp.reset_sinks.append(&*entry); + } + let list = sink_box_imp.reset_model_list.read().unwrap(); + sink_box_imp.reset_sink_dropdown.set_model(Some(&*list)); + let name = sink_box_imp.reset_default_sink.borrow(); + + let index = sink_box_imp.reset_model_index.read().unwrap(); + let model_list = sink_box_imp.reset_model_list.read().unwrap(); + for entry in 0..*index { + if model_list.string(entry) == Some(name.alias.clone().into()) { + sink_box_imp.reset_sink_dropdown.set_selected(entry); + break; + } + } + sink_box_imp + .reset_sink_dropdown + .connect_selected_notify(move |dropdown| { + drop_down_handler(sink_box_ref_select.clone(), dropdown); + }); + } + sink_box_ref + .imp() + .reset_volume_slider + .connect_change_value(move |_, _, value| { + volume_slider_handler(sink_box_ref_slider.clone(), value) + }); + sink_box_ref + .imp() + .reset_sink_mute + .connect_clicked(move |_| { + mute_handler(sink_box_ref_mute.clone()); + }); + }); + }); +} + +pub fn refresh_default_sink(new_sink: Sink, sink_box: Arc, entry: bool) { + let volume = *new_sink.volume.first().unwrap_or(&0_u32); + let fraction = (volume as f64 / 655.36).round(); + let percentage = (fraction).to_string() + "%"; + glib::spawn_future(async move { + glib::idle_add_once(move || { + let imp = sink_box.imp(); + if !entry { + let list = imp.reset_sink_list.read().unwrap(); + let entry = list.get(&new_sink.index); + if entry.is_none() { + return; + } + let entry_imp = entry.unwrap().1.imp(); + entry_imp.reset_selected_sink.set_active(true); + } else { + let index = imp.reset_model_index.read().unwrap(); + let model_list = imp.reset_model_list.read().unwrap(); + for entry in 0..*index { + if model_list.string(entry) == Some(new_sink.alias.clone().into()) { + imp.reset_sink_dropdown.set_selected(entry); + break; + } + } + } + imp.reset_volume_percentage.set_text(&percentage); + imp.reset_volume_slider.set_value(volume as f64); + if new_sink.muted { + imp.reset_sink_mute + .set_icon_name("audio-volume-muted-symbolic"); + } else { + imp.reset_sink_mute + .set_icon_name("audio-volume-high-symbolic"); + } + imp.reset_default_sink.replace(new_sink); + }); + }); +} + +pub fn populate_inputstreams(sink_box: Arc) { + gio::spawn_blocking(move || { + let streams = get_input_streams(sink_box.clone()); + glib::spawn_future(async move { + glib::idle_add_once(move || { + let sink_box_imp = sink_box.imp(); + let mut list = sink_box_imp.reset_input_stream_list.write().unwrap(); + for stream in streams { + let index = stream.index; + let input_stream = Arc::new(InputStreamEntry::new(sink_box.clone(), stream)); + let entry = Arc::new(ListEntry::new(&*input_stream)); + entry.set_activatable(false); + list.insert(index, (entry.clone(), input_stream.clone())); + sink_box_imp.reset_input_streams.append(&*entry); + } + }); + }); + }); +} + +pub fn populate_cards(sink_box: Arc) { + gio::spawn_blocking(move || { + let sink_box_ref = sink_box.clone(); + let cards = get_cards(sink_box.clone()); + glib::spawn_future(async move { + glib::idle_add_once(move || { + let imp = sink_box_ref.imp(); + for card in cards { + imp.reset_cards.add(&CardEntry::new(card)); + } + }); + }); + }); +} + +pub fn get_input_streams(sink_box: Arc) -> Vec { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Vec,), Error> = proxy.method_call(AUDIO, "ListInputStreams", ()); + if res.is_err() { + show_error::(sink_box.clone(), "Failed to list input streams"); + return Vec::new(); + } + res.unwrap().0 +} + +pub fn get_sinks(sink_box: Arc) -> Vec { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Vec,), Error> = proxy.method_call(AUDIO, "ListSinks", ()); + if res.is_err() { + show_error::(sink_box.clone(), "Failed to list sinks"); + return Vec::new(); + } + res.unwrap().0 +} + +pub fn get_cards(sink_box: Arc) -> Vec { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Vec,), Error> = proxy.method_call(AUDIO, "ListCards", ()); + if res.is_err() { + show_error::(sink_box.clone(), "Failed to list profiles"); + return Vec::new(); + } + res.unwrap().0 +} + +pub fn get_default_sink_name(sink_box: Arc) -> String { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(String,), Error> = proxy.method_call(AUDIO, "GetDefaultSinkName", ()); + if res.is_err() { + show_error::(sink_box.clone(), "Failed to get default sink name"); + return String::from(""); + } + res.unwrap().0 +} + +pub fn get_default_sink(sink_box: Arc) -> Sink { + let conn = Connection::new_session().unwrap(); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Sink,), Error> = proxy.method_call(AUDIO, "GetDefaultSink", ()); + if res.is_err() { + show_error::(sink_box.clone(), "Failed to get default sink"); + return Sink::default(); + } + res.unwrap().0 +} diff --git a/src/components/output/sink_entry.rs b/src/components/audio/output/sink_entry.rs similarity index 71% rename from src/components/output/sink_entry.rs rename to src/components/audio/output/sink_entry.rs index d8ccdf6..c07ae72 100644 --- a/src/components/output/sink_entry.rs +++ b/src/components/audio/output/sink_entry.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; -use adw::glib; +use crate::components::base::error_impl::show_error; +use crate::components::utils::set_action_row_ellipsis; use adw::glib::Object; use adw::prelude::{ButtonExt, CheckButtonExt, PreferencesRowExt, RangeExt}; use dbus::blocking::Connection; @@ -10,11 +11,11 @@ use glib::subclass::types::ObjectSubclassIsExt; use glib::{clone, Propagation}; use gtk::{gio, CheckButton}; use re_set_lib::audio::audio_structures::Sink; -use crate::components::utils::set_action_row_ellipsis; -use crate::components::utils::{AUDIO, DBUS_PATH, BASE}; +use crate::components::utils::{AUDIO, BASE, DBUS_PATH}; -use super::sink_box::{refresh_default_sink, SinkBox}; +use super::sink_box::SinkBox; +use super::sink_box_utils::refresh_default_sink; use super::sink_entry_impl; glib::wrapper! { @@ -42,6 +43,8 @@ impl SinkEntry { let volume = stream.volume.first().unwrap_or(&0_u32); let fraction = (*volume as f64 / 655.36).round(); let percentage = (fraction).to_string() + "%"; + let output_box_slider = output_box.clone(); + let output_box_ref = output_box.clone(); imp.reset_volume_percentage.set_text(&percentage); imp.reset_volume_slider.set_value(*volume as f64); imp.stream.replace(stream); @@ -60,7 +63,7 @@ impl SinkEntry { } *time = Some(SystemTime::now()); } - set_sink_volume(value, index, channels); + set_sink_volume(value, index, channels, output_box_slider.clone()); Propagation::Proceed }), ); @@ -75,7 +78,7 @@ impl SinkEntry { if button.is_active() { let name = name.clone(); gio::spawn_blocking(move || { - let result = set_default_sink(name); + let result = set_default_sink(name, output_box_ref.clone()); if result.is_none() { return; } @@ -95,7 +98,7 @@ impl SinkEntry { imp.reset_sink_mute .set_icon_name("audio-volume-high-symbolic"); } - toggle_sink_mute(stream.index, stream.muted); + toggle_sink_mute(stream.index, stream.muted, output_box_ref.clone()); })); set_action_row_ellipsis(imp.reset_sink_name.get()); } @@ -103,55 +106,37 @@ impl SinkEntry { } } -pub fn set_sink_volume(value: f64, index: u32, channels: u16) -> bool { +pub fn set_sink_volume(value: f64, index: u32, channels: u16, output_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy( - BASE, - DBUS_PATH, - Duration::from_millis(1000), - ); - let _: Result<(), Error> = proxy.method_call( - AUDIO, - "SetSinkVolume", - (index, channels, value as u32), - ); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(), Error> = + proxy.method_call(AUDIO, "SetSinkVolume", (index, channels, value as u32)); + if res.is_err() { + show_error::(output_box, "Failed to set sink volume") + } }); true } -pub fn toggle_sink_mute(index: u32, muted: bool) -> bool { +pub fn toggle_sink_mute(index: u32, muted: bool, output_box: Arc) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy( - BASE, - DBUS_PATH, - Duration::from_millis(1000), - ); - let _: Result<(), Error> = - proxy.method_call(AUDIO, "SetSinkMute", (index, muted)); - // if res.is_err() { - // return false; - // } - // res.unwrap().0 + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(), Error> = proxy.method_call(AUDIO, "SetSinkMute", (index, muted)); + if res.is_err() { + show_error::(output_box, "Failed to mute sink") + } }); true } -pub fn set_default_sink(name: Arc) -> Option { +pub fn set_default_sink(name: Arc, output_box: Arc) -> Option { let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy( - BASE, - DBUS_PATH, - Duration::from_millis(1000), - ); - let res: Result<(Sink,), Error> = - proxy.method_call(AUDIO, "SetDefaultSink", (name.as_str(),)); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); + let res: Result<(Sink,), Error> = proxy.method_call(AUDIO, "SetDefaultSink", (name.as_str(),)); if res.is_err() { + show_error::(output_box, "Failed to set default sink"); return None; } Some(res.unwrap().0) diff --git a/src/components/output/sink_entry_impl.rs b/src/components/audio/output/sink_entry_impl.rs similarity index 92% rename from src/components/output/sink_entry_impl.rs rename to src/components/audio/output/sink_entry_impl.rs index f51bc7d..84f75c6 100644 --- a/src/components/output/sink_entry_impl.rs +++ b/src/components/audio/output/sink_entry_impl.rs @@ -5,9 +5,9 @@ use std::cell::RefCell; use std::sync::Arc; use std::time::SystemTime; -use crate::components::output::sink_entry; +use crate::components::audio::output::sink_entry; use gtk::subclass::prelude::*; -use gtk::{glib, Button, CheckButton, CompositeTemplate, Label, Scale}; +use gtk::{Button, CheckButton, CompositeTemplate, Label, Scale}; #[derive(Default, CompositeTemplate)] #[template(resource = "/org/Xetibo/ReSet/resetSinkEntry.ui")] diff --git a/src/components/base/card_entry.rs b/src/components/base/card_entry.rs index 17c53ca..d3f9308 100644 --- a/src/components/base/card_entry.rs +++ b/src/components/base/card_entry.rs @@ -1,19 +1,19 @@ use std::time::Duration; -use adw::glib; use adw::glib::Object; use adw::prelude::{ComboRowExt, PreferencesRowExt}; use dbus::blocking::Connection; use dbus::Error; use glib::subclass::types::ObjectSubclassIsExt; -use glib::{clone, Cast}; +use glib::{clone}; +use glib::prelude::Cast; use gtk::{gio, StringList, StringObject}; use components::utils::create_dropdown_label_factory; use re_set_lib::audio::audio_structures::Card; use crate::components; -use crate::components::utils::{BASE, DBUS_PATH, AUDIO}; +use crate::components::utils::{AUDIO, BASE, DBUS_PATH}; use super::card_entry_impl; @@ -66,11 +66,7 @@ impl CardEntry { fn set_card_profile_of_device(device_index: u32, profile_name: String) -> bool { gio::spawn_blocking(move || { let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy( - BASE, - DBUS_PATH, - Duration::from_millis(1000), - ); + let proxy = conn.with_proxy(BASE, DBUS_PATH, Duration::from_millis(1000)); let _: Result<(), Error> = proxy.method_call( AUDIO, "SetCardProfileOfDevice", diff --git a/src/components/base/card_entry_impl.rs b/src/components/base/card_entry_impl.rs index 48adb33..41538c0 100644 --- a/src/components/base/card_entry_impl.rs +++ b/src/components/base/card_entry_impl.rs @@ -6,7 +6,7 @@ use std::cell::RefCell; use std::collections::HashMap; use gtk::subclass::prelude::*; -use gtk::{glib, CompositeTemplate}; +use gtk::CompositeTemplate; use super::card_entry; diff --git a/src/components/base/error.rs b/src/components/base/error.rs new file mode 100644 index 0000000..d7c2dc4 --- /dev/null +++ b/src/components/base/error.rs @@ -0,0 +1,38 @@ +use adw::glib::Object; +use glib::{clone, subclass::types::ObjectSubclassIsExt}; +use gtk::{ + gdk, + prelude::{ButtonExt, PopoverExt}, + Editable, Popover, +}; + +use super::error_impl; + +glib::wrapper! { + pub struct ReSetError(ObjectSubclass) + @extends Popover, gtk::Widget, + @implements Editable,gdk::Popup, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; +} + +unsafe impl Send for ReSetError {} +unsafe impl Sync for ReSetError {} + +impl ReSetError { + pub fn new() -> Self { + let error: ReSetError = Object::builder().build(); + error + .imp() + .reset_error_button + .connect_clicked(clone!(@strong error => move |_| { + println!("pingpangpung"); + error.popdown(); + })); + error + } +} + +impl Default for ReSetError { + fn default() -> Self { + Self::new() + } +} diff --git a/src/components/base/error_impl.rs b/src/components/base/error_impl.rs new file mode 100644 index 0000000..9830b3c --- /dev/null +++ b/src/components/base/error_impl.rs @@ -0,0 +1,74 @@ +use std::sync::Arc; + +use gtk::prelude::{ButtonExt, PopoverExt}; +use gtk::subclass::prelude::*; +use gtk::{Button, CompositeTemplate, Label, Popover}; + +use super::error; + +#[derive(Default, CompositeTemplate)] +#[template(resource = "/org/Xetibo/ReSet/resetError.ui")] +pub struct ReSetError { + #[template_child] + pub reset_error_label: TemplateChild