2025-11-07-23-29
This commit is contained in:
parent
7df8124933
commit
97cb8ff476
16
Behavior.txt
Normal file
16
Behavior.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
Full control is taken over everything that is configured
|
||||||
|
and added to Devices.
|
||||||
|
|
||||||
|
On initial creation existing pipewire settings might
|
||||||
|
be copied, but existing configs will not be changed
|
||||||
|
by external interfaces.
|
||||||
|
|
||||||
|
If, for example, a device is configured to use Pro Audio,
|
||||||
|
and you change the device Profile from pavucontrol,
|
||||||
|
it will automatically be changed back.
|
||||||
|
|
||||||
|
This program is supposed to be opinionated with the goal
|
||||||
|
of getting audio devices working as intended. It will
|
||||||
|
not compromise to external software and will force its
|
||||||
|
configurations onto the system. It will provide
|
||||||
|
reproducible endpoints that other programs can attach to.
|
||||||
@ -5,13 +5,15 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
flume = "0.11.1"
|
flume = { version = "0.11.1", features = ["async"] }
|
||||||
gettext-rs = { version = "0.7", features = ["gettext-system"] }
|
gettext-rs = { version = "0.7", features = ["gettext-system"] }
|
||||||
gtk = { version = "0.9", package = "gtk4", features = ["gnome_47"] }
|
gtk = { version = "0.9", package = "gtk4", features = ["gnome_47", "v4_18"] }
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
libspa = "0.8.0"
|
libspa = "0.8.0"
|
||||||
|
libspa-sys = "0.8.0"
|
||||||
pipewire = "0.8.0"
|
pipewire = "0.8.0"
|
||||||
sled = "0.34.7"
|
sled = "0.34.7"
|
||||||
|
uuid = { version = "1.18.0", features = ["v4", "v7"] }
|
||||||
|
|
||||||
[dependencies.adw]
|
[dependencies.adw]
|
||||||
package = "libadwaita"
|
package = "libadwaita"
|
||||||
|
|||||||
@ -95,3 +95,96 @@ __________________________
|
|||||||
| Cancel |
|
| Cancel |
|
||||||
| |
|
| |
|
||||||
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|
||||||
|
|
||||||
|
Device Page
|
||||||
|
|
||||||
|
_______________________________________________________
|
||||||
|
| Devices | Device1 |
|
||||||
|
| | |
|
||||||
|
| Device1 | Pipewire Device Configuration |
|
||||||
|
| | Select audio profiles for specific |
|
||||||
|
| | pipewire devices + |
|
||||||
|
| | _______________________________________ |
|
||||||
|
| | | Device Config 1 | |
|
||||||
|
| | | Device Config 2 | |
|
||||||
|
| | | ... | |
|
||||||
|
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||||
|
| | |
|
||||||
|
| | Inputs |
|
||||||
|
| | Selectable inputs that are mapped |
|
||||||
|
| | to a device + |
|
||||||
|
| | _______________________________________ |
|
||||||
|
| | | Input 1 | |
|
||||||
|
| | | Input 2 | |
|
||||||
|
| | | ... | |
|
||||||
|
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||||
|
| | |
|
||||||
|
| | Outputs |
|
||||||
|
| | Selectable outputs that are mapped |
|
||||||
|
| | to a device + |
|
||||||
|
| | _______________________________________ |
|
||||||
|
| | | Output 1 | |
|
||||||
|
| | | Output 2 | |
|
||||||
|
| | | ... | |
|
||||||
|
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||||
|
| | |
|
||||||
|
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|
||||||
|
|
||||||
|
Inputs General or Device Bound?
|
||||||
|
|
||||||
|
Device Bound:
|
||||||
|
- It's the input of the device,
|
||||||
|
unlike the device config which is
|
||||||
|
something hardware bound
|
||||||
|
- easier workflow
|
||||||
|
- Could use Simple names instead of
|
||||||
|
sifting through a bunch of inputs
|
||||||
|
from other devices
|
||||||
|
- Since each device might have multiple
|
||||||
|
inputs, the selection screen would
|
||||||
|
get confusing quickly
|
||||||
|
|
||||||
|
Input Page
|
||||||
|
Loopback reference: https://docs.pipewire.org/page_module_loopback.html
|
||||||
|
maybe use commandline tool for config: https://docs.pipewire.org/page_man_pw-loopback_1.html
|
||||||
|
|
||||||
|
possible profiles:
|
||||||
|
- Mono
|
||||||
|
[ MONO ]
|
||||||
|
- Stereo
|
||||||
|
[ FL FR ]
|
||||||
|
- Downmix
|
||||||
|
[ FL FR ]
|
||||||
|
- etc.?
|
||||||
|
- Custom?
|
||||||
|
|
||||||
|
Example (design changes based on selected profile):
|
||||||
|
Stereo, mapped from pro audio
|
||||||
|
|
||||||
|
_______________________________________________________
|
||||||
|
| Devices | < Input 1 |Edit Symbol| |
|
||||||
|
| | __________________________ |
|
||||||
|
| Device1 | Profile | Stereo ⌄ | |
|
||||||
|
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||||
|
| | |
|
||||||
|
| | Input Map |
|
||||||
|
| | Map inputs from a device config |
|
||||||
|
| | to inputs in this profile |
|
||||||
|
| | ________________________ |
|
||||||
|
| | Front Left | DeviceConfig1-Aux0 ⌄ | |
|
||||||
|
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||||
|
| | ________________________ |
|
||||||
|
| | Front Right | DeviceConfig1-Aux1 ⌄ | |
|
||||||
|
| | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ |
|
||||||
|
| | |
|
||||||
|
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
|
||||||
|
|
||||||
|
Output Page
|
||||||
|
|
||||||
|
possible profiles:
|
||||||
|
- Mono
|
||||||
|
[
|
||||||
|
- Stereo
|
||||||
|
- Upmix (with param configuration)
|
||||||
|
- etc.?
|
||||||
|
- Custom?
|
||||||
@ -18,14 +18,16 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
*/
|
*/
|
||||||
|
extern crate core;
|
||||||
|
|
||||||
mod application;
|
mod application;
|
||||||
mod config;
|
mod config;
|
||||||
mod window;
|
mod window;
|
||||||
mod components;
|
mod components;
|
||||||
mod pw_manager;
|
mod pipewire_manager;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod state_manager;
|
mod state_manager;
|
||||||
|
mod spa_structs;
|
||||||
|
|
||||||
use self::application::AudioDeviceManagerApplication;
|
use self::application::AudioDeviceManagerApplication;
|
||||||
use self::window::AudioDeviceManagerWindow;
|
use self::window::AudioDeviceManagerWindow;
|
||||||
|
|||||||
676
src/pipewire_manager.rs
Normal file
676
src/pipewire_manager.rs
Normal file
@ -0,0 +1,676 @@
|
|||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::thread;
|
||||||
|
use std::thread::{sleep, JoinHandle};
|
||||||
|
use std::time::Duration;
|
||||||
|
use adw::glib;
|
||||||
|
use adw::glib::{clone, GString, ParamFlags, Value};
|
||||||
|
use adw::prelude::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::glib::ExitCode;
|
||||||
|
use libspa::pod::Pod;
|
||||||
|
use pipewire::{context::Context, main_loop::MainLoop};
|
||||||
|
use pipewire::properties::{properties, Properties};
|
||||||
|
use pipewire::registry::{GlobalObject, Registry};
|
||||||
|
use pipewire::spa::param::ParamType;
|
||||||
|
|
||||||
|
use crate::pipewire_manager::deserialize_pod::deserialize_pod;
|
||||||
|
|
||||||
|
type DeviceId = usize;
|
||||||
|
|
||||||
|
enum DeviceMessage {
|
||||||
|
ProfileDiscovered(DeviceProfile),
|
||||||
|
ActiveProfileSet(ProfileId),
|
||||||
|
PropertyDiscovered(String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Data {
|
||||||
|
DeviceMessage(DeviceId, DeviceMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DeviceCommand {
|
||||||
|
SetProfile(ProfileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Command {
|
||||||
|
DeviceCommand(DeviceId, DeviceCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RegisteredObjects {
|
||||||
|
profile_combo_rows: HashMap<uuid::Uuid, (usize, adw::ComboRow, gtk::StringList)>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegisteredObjects {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
profile_combo_rows: HashMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Device {
|
||||||
|
properties: HashMap<String, String>,
|
||||||
|
profiles: HashMap<usize, DeviceProfile>,
|
||||||
|
active_profile: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Device {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
properties: HashMap::new(),
|
||||||
|
profiles: HashMap::new(),
|
||||||
|
active_profile: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PipewireManager {
|
||||||
|
handle: JoinHandle<ExitCode>,
|
||||||
|
devices: Rc<RefCell<HashMap<usize, Device>>>,
|
||||||
|
registered_objects: Rc<RefCell<RegisteredObjects>>,
|
||||||
|
tx: pipewire::channel::Sender<Command>,
|
||||||
|
rx: flume::Receiver<Data>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PipewireManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
let (handle, tx, rx) = spawn_pipewire_thread();
|
||||||
|
|
||||||
|
let devices = Rc::new(RefCell::new(HashMap::new()));
|
||||||
|
let registered_objects = Rc::new(RefCell::new(RegisteredObjects::new()));
|
||||||
|
|
||||||
|
let rx_clone = rx.clone();
|
||||||
|
|
||||||
|
glib::spawn_future_local(clone!(
|
||||||
|
#[weak]
|
||||||
|
devices,
|
||||||
|
#[weak]
|
||||||
|
registered_objects,
|
||||||
|
async move {
|
||||||
|
handle_registered_objects(rx_clone, devices, registered_objects).await
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let instance = Self {
|
||||||
|
handle,
|
||||||
|
devices,
|
||||||
|
registered_objects,
|
||||||
|
tx,
|
||||||
|
rx
|
||||||
|
};
|
||||||
|
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_registered_objects(
|
||||||
|
rx: flume::Receiver<Data>,
|
||||||
|
devices: Rc<RefCell<HashMap<usize, Device>>>,
|
||||||
|
registered_objects: Rc<RefCell<RegisteredObjects>>
|
||||||
|
) {
|
||||||
|
let mut active_profile_set = None;
|
||||||
|
while let Ok(data) = rx.recv_async().await {
|
||||||
|
match data {
|
||||||
|
Data::DeviceMessage(device_id, message) => {
|
||||||
|
let mut devices_mut = devices.borrow_mut();
|
||||||
|
let device = devices_mut
|
||||||
|
.entry(device_id)
|
||||||
|
.or_insert(Device::default());
|
||||||
|
|
||||||
|
match message {
|
||||||
|
DeviceMessage::ProfileDiscovered(profile) => {
|
||||||
|
let profile_name = profile.description.clone();
|
||||||
|
device.profiles.insert(profile.id, profile);
|
||||||
|
handle_profile_discovered(device_id, registered_objects.clone(), profile_name);
|
||||||
|
}
|
||||||
|
DeviceMessage::ActiveProfileSet(id) => {
|
||||||
|
println!("Active profile set: {}", id);
|
||||||
|
active_profile_set = Some(id);
|
||||||
|
}
|
||||||
|
DeviceMessage::PropertyDiscovered(k, v) => {
|
||||||
|
device.properties.insert(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(id) = active_profile_set {
|
||||||
|
if let Some(profile) = device.profiles.get(&id) {
|
||||||
|
device.active_profile = Some(id);
|
||||||
|
let profile_name = profile.description.clone();
|
||||||
|
handle_active_profile_set(device_id, registered_objects.clone(), profile_name);
|
||||||
|
active_profile_set = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_profile_discovered(device_id: usize, registered_objects: Rc<RefCell<RegisteredObjects>>, profile_name: String) {
|
||||||
|
for (
|
||||||
|
_id,
|
||||||
|
(
|
||||||
|
combo_row_device_id,
|
||||||
|
_combo_row,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
) in &mut registered_objects.borrow_mut().profile_combo_rows {
|
||||||
|
|
||||||
|
if *combo_row_device_id != device_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while let Some(p) = model.string(i) {
|
||||||
|
match p.to_string().cmp(&profile_name) {
|
||||||
|
Ordering::Less => {}
|
||||||
|
Ordering::Equal => return,
|
||||||
|
Ordering::Greater => {break;}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
model.splice(i, 0, &[&profile_name]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_active_profile_set(device_id: usize, registered_objects: Rc<RefCell<RegisteredObjects>>, profile_name: String) {
|
||||||
|
for (
|
||||||
|
_id,
|
||||||
|
(
|
||||||
|
combo_row_device_id,
|
||||||
|
combo_row,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
) in &mut registered_objects.borrow_mut().profile_combo_rows {
|
||||||
|
if *combo_row_device_id != device_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = model.find(&profile_name);
|
||||||
|
|
||||||
|
combo_row.set_selected(i);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PipewireManager {
|
||||||
|
pub(crate) fn get_device_names(&self) -> HashMap<usize, String> {
|
||||||
|
self.devices.borrow()
|
||||||
|
.iter()
|
||||||
|
.map(|(id, d)| {
|
||||||
|
(
|
||||||
|
*id,
|
||||||
|
d.properties
|
||||||
|
.get("device.description")
|
||||||
|
.unwrap_or(&"Error loading device name".to_owned())
|
||||||
|
.to_owned()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_device_properties(&self, id: usize) -> HashMap<String, String> {
|
||||||
|
self.devices.borrow()[&id]
|
||||||
|
.properties
|
||||||
|
.clone()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_device_ids_from_properties(&self, properties: HashMap<String, String>) -> Vec<usize> {
|
||||||
|
self.devices.borrow()
|
||||||
|
.iter()
|
||||||
|
.filter(
|
||||||
|
|(id, d)| {
|
||||||
|
properties
|
||||||
|
.iter()
|
||||||
|
.all(|(k, v)| {
|
||||||
|
let Some(v2) = d.properties.get(k) else { return false; };
|
||||||
|
v == v2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.map(|(id, d)| *id)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn register_profile_combo_row(&self, device_id: usize, combo_row: adw::ComboRow) -> uuid::Uuid {
|
||||||
|
let mut profile_names: Vec<_> = self.devices.borrow()[&device_id].profiles
|
||||||
|
.iter()
|
||||||
|
.map(|(id, p)| {
|
||||||
|
p.description.clone()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
profile_names.sort();
|
||||||
|
|
||||||
|
let active_profile = self.devices.borrow()[&device_id].active_profile
|
||||||
|
.clone() // make sure no lockups happen
|
||||||
|
.and_then(|id| {
|
||||||
|
Some((id, self.devices.borrow()[&device_id].profiles[&id].description.clone()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let model = gtk::StringList::new(&[]);
|
||||||
|
for name in &profile_names {
|
||||||
|
model.append(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
combo_row.set_model(Some(&model));
|
||||||
|
|
||||||
|
combo_row.connect_selected_notify(clone!(
|
||||||
|
#[weak]
|
||||||
|
model,
|
||||||
|
#[weak(rename_to=devices)]
|
||||||
|
self.devices,
|
||||||
|
#[strong(rename_to=tx)]
|
||||||
|
self.tx,
|
||||||
|
move |c| {
|
||||||
|
let model_entry = c.selected();
|
||||||
|
let selected_profile_name = model.string(model_entry).unwrap().to_string();
|
||||||
|
|
||||||
|
let mut selected_profile_id = None;
|
||||||
|
for (profile_id, profile) in &devices.borrow()[&device_id].profiles {
|
||||||
|
if selected_profile_name == profile.description {
|
||||||
|
selected_profile_id = Some(*profile_id);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected_profile_id = selected_profile_id.unwrap();
|
||||||
|
|
||||||
|
tx.send(
|
||||||
|
Command::DeviceCommand(device_id, DeviceCommand::SetProfile(selected_profile_id))
|
||||||
|
).unwrap();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some((_, name)) = active_profile {
|
||||||
|
if let Ok(position) = profile_names.binary_search(&&name) {
|
||||||
|
combo_row.set_selected(position as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let combo_row_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
self.registered_objects.borrow_mut().profile_combo_rows.insert(combo_row_id, (device_id, combo_row, model));
|
||||||
|
|
||||||
|
combo_row_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeviceAvailability {
|
||||||
|
Available,
|
||||||
|
Unavailable,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeviceProfile {
|
||||||
|
id: usize,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
priority: usize,
|
||||||
|
available: DeviceAvailability,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileId = usize;
|
||||||
|
type PodBytes = Vec<u8>;
|
||||||
|
|
||||||
|
// Devices don't have to be connected to current system
|
||||||
|
// Relevant device data has to be stored
|
||||||
|
// User is supposed to be able to pick out which device data is relevant and identifying
|
||||||
|
// default is device.bus-path or device.product.id
|
||||||
|
// could also be device.product.id + device.product.name + device.vendor.name
|
||||||
|
// Interface: List of all device properties and next to them a checkbox for "relevant identifier"
|
||||||
|
// Maybe also allow regex
|
||||||
|
// also maybe allow custom script
|
||||||
|
struct DeviceMessageHandler {
|
||||||
|
pipewire_device: Rc<RefCell<pipewire::device::Device>>,
|
||||||
|
device_listener: pipewire::device::DeviceListener,
|
||||||
|
profiles: Rc<RefCell<HashMap<ProfileId, PodBytes>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceMessageHandler {
|
||||||
|
fn new(pipewire_device: pipewire::device::Device, id: usize, tx: flume::Sender<Data>) -> Self {
|
||||||
|
|
||||||
|
let pipewire_device_rc = Rc::new(RefCell::new(pipewire_device));
|
||||||
|
let profiles = Rc::new(RefCell::new(HashMap::new()));
|
||||||
|
|
||||||
|
let pipewire_device_ref = pipewire_device_rc.borrow();
|
||||||
|
let mut listener_builder = pipewire_device_ref.add_listener_local();
|
||||||
|
|
||||||
|
listener_builder = Self::listen_for_params(id, listener_builder, tx.clone(), profiles.clone());
|
||||||
|
listener_builder = Self::listen_for_info(id, listener_builder, tx, pipewire_device_rc.clone());
|
||||||
|
|
||||||
|
|
||||||
|
let device_listener = listener_builder.register();
|
||||||
|
|
||||||
|
pipewire_device_ref.subscribe_params(&[
|
||||||
|
ParamType::Profile,
|
||||||
|
ParamType::EnumProfile,
|
||||||
|
]);
|
||||||
|
|
||||||
|
drop(pipewire_device_ref);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pipewire_device: pipewire_device_rc,
|
||||||
|
device_listener,
|
||||||
|
profiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listen_for_params(
|
||||||
|
device_id: usize,
|
||||||
|
listener_builder: pipewire::device::DeviceListenerLocalBuilder,
|
||||||
|
tx: flume::Sender<Data>,
|
||||||
|
profiles: Rc<RefCell<HashMap<ProfileId, PodBytes>>>
|
||||||
|
) -> pipewire::device::DeviceListenerLocalBuilder {
|
||||||
|
listener_builder
|
||||||
|
.param(clone!(
|
||||||
|
#[weak]
|
||||||
|
profiles,
|
||||||
|
move |seq, param_type, index, next, param_pod| {
|
||||||
|
|
||||||
|
let Some(pod) = param_pod else { return };
|
||||||
|
|
||||||
|
let Some(param) = deserialize_pod(param_pod) else { return };
|
||||||
|
|
||||||
|
Self::on_profile_discovered(
|
||||||
|
device_id,
|
||||||
|
param,
|
||||||
|
param_type,
|
||||||
|
tx.clone(),
|
||||||
|
profiles,
|
||||||
|
pod
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listen_for_info(
|
||||||
|
device_id: usize,
|
||||||
|
listener_builder: pipewire::device::DeviceListenerLocalBuilder,
|
||||||
|
tx: flume::Sender<Data>,
|
||||||
|
pipewire_device: Rc<RefCell<pipewire::device::Device>>
|
||||||
|
) -> pipewire::device::DeviceListenerLocalBuilder {
|
||||||
|
listener_builder
|
||||||
|
.info(clone!(
|
||||||
|
#[weak]
|
||||||
|
pipewire_device,
|
||||||
|
move |info| {
|
||||||
|
Self::on_info_discovered(
|
||||||
|
device_id,
|
||||||
|
info,
|
||||||
|
tx.clone(),
|
||||||
|
pipewire_device
|
||||||
|
)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_profile_discovered(
|
||||||
|
device_id: usize,
|
||||||
|
param: libspa::pod::Object,
|
||||||
|
param_type: ParamType,
|
||||||
|
tx: flume::Sender<Data>,
|
||||||
|
profiles: Rc<RefCell<HashMap<ProfileId, PodBytes>>>,
|
||||||
|
pod: &Pod
|
||||||
|
) {
|
||||||
|
let mut profile_id = None;
|
||||||
|
let mut profile_name = None;
|
||||||
|
let mut profile_description = None;
|
||||||
|
let mut profile_priority = None;
|
||||||
|
let mut profile_availability = None;
|
||||||
|
|
||||||
|
for property in param.properties {
|
||||||
|
match (property.key, property.value) {
|
||||||
|
(1, libspa::pod::Value::Int(id)) => { profile_id = Some(id as usize); },
|
||||||
|
(2, libspa::pod::Value::String(name)) => { profile_name = Some(name); },
|
||||||
|
(3, libspa::pod::Value::String(description)) => { profile_description = Some(description); },
|
||||||
|
(4, libspa::pod::Value::Int(priority)) => { profile_priority = Some(priority as usize); },
|
||||||
|
(5, libspa::pod::Value::Id(libspa::utils::Id(0))) => { profile_availability = Some(DeviceAvailability::Unknown); },
|
||||||
|
(5, libspa::pod::Value::Id(libspa::utils::Id(1))) => { profile_availability = Some(DeviceAvailability::Unavailable); },
|
||||||
|
(5, libspa::pod::Value::Id(libspa::utils::Id(2))) => { profile_availability = Some(DeviceAvailability::Available); },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Proper error handling
|
||||||
|
|
||||||
|
match param_type {
|
||||||
|
ParamType::Profile => {
|
||||||
|
let id = profile_id.unwrap();
|
||||||
|
tx.send(
|
||||||
|
Data::DeviceMessage(
|
||||||
|
device_id,
|
||||||
|
DeviceMessage::ActiveProfileSet(id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
|
ParamType::EnumProfile => {
|
||||||
|
let id = profile_id.unwrap();
|
||||||
|
|
||||||
|
let pod_bytes = pod.as_bytes().to_vec();
|
||||||
|
profiles.borrow_mut().insert(id, pod_bytes);
|
||||||
|
|
||||||
|
let device_profile = DeviceProfile {
|
||||||
|
id,
|
||||||
|
name: profile_name.unwrap(),
|
||||||
|
description: profile_description.unwrap(),
|
||||||
|
priority: profile_priority.unwrap(),
|
||||||
|
available: profile_availability.unwrap(),
|
||||||
|
};
|
||||||
|
tx.send(
|
||||||
|
Data::DeviceMessage(
|
||||||
|
device_id,
|
||||||
|
DeviceMessage::ProfileDiscovered(device_profile)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
|
_ => return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_info_discovered(
|
||||||
|
device_id: usize,
|
||||||
|
info: &pipewire::device::DeviceInfoRef,
|
||||||
|
tx: flume::Sender<Data>,
|
||||||
|
pipewire_device: Rc<RefCell<pipewire::device::Device>>
|
||||||
|
) {
|
||||||
|
// a bit jank, but works for now
|
||||||
|
// every time an info update is given also give a params update
|
||||||
|
// because subscribed params don't work somehow
|
||||||
|
pipewire_device.borrow().enum_params(1, Some(ParamType::Profile), 0, 1);
|
||||||
|
info.props().unwrap().iter()
|
||||||
|
.for_each(|(k, v)| {
|
||||||
|
tx.send(Data::DeviceMessage(
|
||||||
|
device_id,
|
||||||
|
DeviceMessage::PropertyDiscovered(k.to_owned(), v.to_owned())
|
||||||
|
)).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_profile(&self, profile_id: ProfileId) {
|
||||||
|
let profile_pod_bytes = match self.profiles.borrow().get(&profile_id) {
|
||||||
|
Some(pod_bytes) => pod_bytes.clone(),
|
||||||
|
None => {
|
||||||
|
println!("No profile with id {} found", profile_id);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let profile_pod = Pod::from_bytes(&profile_pod_bytes).unwrap();
|
||||||
|
|
||||||
|
self.pipewire_device.borrow().set_param(ParamType::Profile, ParamFlags::empty().bits(), profile_pod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalsManager {
|
||||||
|
globals: Vec<GlobalObject<Properties>>,
|
||||||
|
devices: HashMap<usize, DeviceMessageHandler>,
|
||||||
|
tx: flume::Sender<Data>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobalsManager {
|
||||||
|
fn new(tx: flume::Sender<Data>) -> Self {
|
||||||
|
Self {
|
||||||
|
globals: Vec::new(),
|
||||||
|
devices: HashMap::new(),
|
||||||
|
tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_global(&mut self, global: GlobalObject<Properties>, registry: &Registry) {
|
||||||
|
self.globals.push(global);
|
||||||
|
|
||||||
|
let global_ref = self.globals.last().unwrap();
|
||||||
|
|
||||||
|
let props = match &global_ref.props {
|
||||||
|
Some(props) => props,
|
||||||
|
None => return
|
||||||
|
};
|
||||||
|
|
||||||
|
match props.get("media.class") {
|
||||||
|
Some("Audio/Device") => {}
|
||||||
|
_ => return
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_id = global_ref.id as usize;
|
||||||
|
|
||||||
|
let device = DeviceMessageHandler::new(registry.bind(global_ref).unwrap(), device_id, self.tx.clone());
|
||||||
|
|
||||||
|
self.devices.insert(device_id, device);
|
||||||
|
|
||||||
|
println!("ADDED DEVICE");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_device(&self, device_id: usize) -> Option<&DeviceMessageHandler> {
|
||||||
|
self.devices.get(&device_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spawn_pipewire_thread() -> (JoinHandle<ExitCode>, pipewire::channel::Sender<Command>, flume::Receiver<Data>) {
|
||||||
|
|
||||||
|
let (tx, remote_rx) = flume::unbounded();
|
||||||
|
let (remote_tx, rx) = pipewire::channel::channel();
|
||||||
|
|
||||||
|
let pw_thread = thread::spawn(move || {
|
||||||
|
// Initialize PipeWire and run the main loop
|
||||||
|
// ...
|
||||||
|
let mainloop = MainLoop::new(None).expect("failed to get mail loop");
|
||||||
|
let context = Context::new(&mainloop).expect("failed to get context");
|
||||||
|
let core = context.connect(None).expect("failed to get core");
|
||||||
|
let registry = Rc::new(core.get_registry().expect("failed to get registry"));
|
||||||
|
|
||||||
|
let globals_manager = Rc::new(RefCell::new(GlobalsManager::new(tx.clone())));
|
||||||
|
|
||||||
|
|
||||||
|
let _listener = registry
|
||||||
|
.add_listener_local()
|
||||||
|
.global(clone!(
|
||||||
|
#[strong]
|
||||||
|
registry,
|
||||||
|
#[strong]
|
||||||
|
globals_manager,
|
||||||
|
move |global|
|
||||||
|
{
|
||||||
|
let owned_global = global.to_owned();
|
||||||
|
|
||||||
|
|
||||||
|
globals_manager.borrow_mut().add_global(owned_global, ®istry);
|
||||||
|
|
||||||
|
/*
|
||||||
|
if global.type_ == ObjectType::Port {
|
||||||
|
let props = global.props.as_ref().unwrap();
|
||||||
|
let port_name = props.get("port.name");
|
||||||
|
let port_alias = props.get("port.alias");
|
||||||
|
let object_path = props.get("object.path");
|
||||||
|
let format_dsp = props.get("format.dsp");
|
||||||
|
let audio_channel = props.get("audio.channel");
|
||||||
|
let port_id = props.get("port.id");
|
||||||
|
let port_direction = props.get("port.direction");
|
||||||
|
println!("Port: Name: {:?} Alias: {:?} Id: {:?} Direction: {:?} AudioChannel: {:?} Object Path: {:?} FormatDsp: {:?}",
|
||||||
|
port_name,
|
||||||
|
port_alias,
|
||||||
|
port_id,port_direction,audio_channel,object_path,format_dsp
|
||||||
|
);
|
||||||
|
} else if global.type_ == ObjectType::Device {
|
||||||
|
let props = global.props.as_ref().unwrap();
|
||||||
|
let device_name = props.get("device.name");
|
||||||
|
let device_nick = props.get("device.nick");
|
||||||
|
let device_description = props.get("device.description");
|
||||||
|
let device_api = props.get("device.api");
|
||||||
|
let media_class = props.get("media.class");
|
||||||
|
println!("Device: Name: {:?} Nick: {:?} Desc: {:?} Api: {:?} MediaClass: {:?}",
|
||||||
|
device_name, device_nick, device_description, device_api, media_class);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.register();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let _receiver = rx.attach(mainloop.loop_(), {
|
||||||
|
let mainloop = mainloop.clone();
|
||||||
|
{
|
||||||
|
let globals_manager_weak = Rc::downgrade(&globals_manager);
|
||||||
|
move |command| {
|
||||||
|
let Some(globals_manager) = globals_manager_weak.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match command {
|
||||||
|
Command::DeviceCommand(device_id, device_command) => {
|
||||||
|
let globals_manager_ref = globals_manager.borrow();
|
||||||
|
let Some(device) = globals_manager_ref.get_device(device_id) else {
|
||||||
|
println!("No device with id {} found", device_id);
|
||||||
|
return
|
||||||
|
};
|
||||||
|
match device_command {
|
||||||
|
DeviceCommand::SetProfile(profile_id) => {
|
||||||
|
device.set_profile(profile_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calling the `destroy_global` method on the registry will destroy the object with the specified id on the remote.
|
||||||
|
// We don't have a specific object to destroy now, so this is commented out.
|
||||||
|
// registry.destroy_global(313).into_result()?;
|
||||||
|
|
||||||
|
|
||||||
|
mainloop.run();
|
||||||
|
return ExitCode::FAILURE; // not sure
|
||||||
|
});
|
||||||
|
|
||||||
|
(pw_thread, remote_tx, remote_rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod deserialize_pod {
|
||||||
|
/// Taken from wiremix: https://github.com/tsowell/wiremix/blob/main/src/wirehose/deserialize.rs#L6
|
||||||
|
use libspa::pod::{deserialize::PodDeserializer, Object, Pod, Value};
|
||||||
|
pub fn deserialize_pod(param: Option<&Pod>) -> Option<Object> {
|
||||||
|
param
|
||||||
|
.and_then(|pod| {
|
||||||
|
PodDeserializer::deserialize_any_from(pod.as_bytes()).ok()
|
||||||
|
})
|
||||||
|
.and_then(|(_, value)| match value {
|
||||||
|
Value::Object(obj) => Some(obj),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,410 +0,0 @@
|
|||||||
use std::cell::{Cell, RefCell};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::{Debug, Formatter};
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::thread;
|
|
||||||
use std::thread::{sleep, JoinHandle};
|
|
||||||
use std::time::Duration;
|
|
||||||
use gtk::glib::ExitCode;
|
|
||||||
use pipewire::{context::Context, main_loop::MainLoop};
|
|
||||||
use pipewire::properties::Properties;
|
|
||||||
use pipewire::registry::{GlobalObject, Registry};
|
|
||||||
use pipewire::spa::param::ParamType;
|
|
||||||
|
|
||||||
use crate::pw_manager::deserialize::deserialize;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum Command {
|
|
||||||
GetDeviceNames,
|
|
||||||
GetDeviceProperties(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Data {
|
|
||||||
DeviceNames(HashMap<usize, String>),
|
|
||||||
DeviceProperties(usize, HashMap<String, String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct PipewireManager {
|
|
||||||
handle: JoinHandle<ExitCode>,
|
|
||||||
tx: pipewire::channel::Sender<Command>,
|
|
||||||
rx: flume::Receiver<Data>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PipewireManager {
|
|
||||||
fn default() -> Self {
|
|
||||||
let (handle, tx, rx) = spawn_pipewire_thread();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
handle,
|
|
||||||
tx,
|
|
||||||
rx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PipewireManager {
|
|
||||||
pub(crate) fn get_device_names(&self) -> HashMap<usize, String> {
|
|
||||||
self.tx.send(Command::GetDeviceNames).unwrap();
|
|
||||||
match self.rx.recv().unwrap() {
|
|
||||||
Data::DeviceNames(names) => names,
|
|
||||||
_ => panic!("Invalid data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_device_properties(&self, id: usize) -> HashMap<String, String> {
|
|
||||||
self.tx.send(Command::GetDeviceProperties(id)).unwrap();
|
|
||||||
match self.rx.recv().unwrap() {
|
|
||||||
Data::DeviceProperties(_, properties) => properties,
|
|
||||||
_ => panic!("Invalid data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DeviceAvailability {
|
|
||||||
Available,
|
|
||||||
Unavailable,
|
|
||||||
Unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DeviceProfile {
|
|
||||||
id: usize,
|
|
||||||
name: String,
|
|
||||||
description: String,
|
|
||||||
priority: usize,
|
|
||||||
available: DeviceAvailability,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devices don't have to be connected to current system
|
|
||||||
// Relevant device data has to be stored
|
|
||||||
// User is supposed to be able to pick out which device data is relevant and identifying
|
|
||||||
// default is device.bus-path or device.product.id
|
|
||||||
// could also be device.product.id + device.product.name + device.vendor.name
|
|
||||||
// Interface: List of all device properties and next to them a checkbox for "relevant identifier"
|
|
||||||
// Maybe also allow regex
|
|
||||||
// also maybe allow custom script
|
|
||||||
struct Device {
|
|
||||||
active_profile: Rc<RefCell<Option<usize>>>,
|
|
||||||
profiles: Rc<RefCell<HashMap<usize, DeviceProfile>>>,
|
|
||||||
name: Rc<RefCell<String>>,
|
|
||||||
id: usize,
|
|
||||||
properties: Rc<RefCell<HashMap<String, String>>>,
|
|
||||||
pipewire_device: pipewire::device::Device,
|
|
||||||
device_listener: pipewire::device::DeviceListener,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Device {
|
|
||||||
fn new(pipewire_device: pipewire::device::Device, id: usize) -> Self {
|
|
||||||
|
|
||||||
let active_profile = Rc::new(RefCell::new(None));
|
|
||||||
let profiles = Rc::new(RefCell::new(HashMap::new()));
|
|
||||||
let name = Rc::new(RefCell::new(String::new()));
|
|
||||||
let properties = Rc::new(RefCell::new(HashMap::new()));
|
|
||||||
|
|
||||||
let mut listener_builder = pipewire_device.add_listener_local();
|
|
||||||
|
|
||||||
listener_builder= Self::listen_for_params(listener_builder, &active_profile, &profiles);
|
|
||||||
listener_builder= Self::listen_for_info(listener_builder, &name, &properties);
|
|
||||||
|
|
||||||
|
|
||||||
let device_listener = listener_builder.register();
|
|
||||||
|
|
||||||
|
|
||||||
pipewire_device.subscribe_params(&[
|
|
||||||
ParamType::Profile,
|
|
||||||
ParamType::EnumProfile
|
|
||||||
]);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
active_profile,
|
|
||||||
profiles,
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
properties,
|
|
||||||
pipewire_device,
|
|
||||||
device_listener
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn listen_for_params<'a>(
|
|
||||||
listener_builder: pipewire::device::DeviceListenerLocalBuilder<'a>,
|
|
||||||
active_profile: &Rc<RefCell<Option<usize>>>,
|
|
||||||
profiles: &Rc<RefCell<HashMap<usize, DeviceProfile>>>,
|
|
||||||
) -> pipewire::device::DeviceListenerLocalBuilder<'a> {
|
|
||||||
listener_builder
|
|
||||||
.param({
|
|
||||||
let active_profile_weak = Rc::downgrade(active_profile);
|
|
||||||
let profiles_weak = Rc::downgrade(profiles);
|
|
||||||
|
|
||||||
move |seq, param_type, index, next, param| {
|
|
||||||
|
|
||||||
let Some(active_profile) = active_profile_weak.upgrade() else { return };
|
|
||||||
let Some(profiles) = profiles_weak.upgrade() else { return };
|
|
||||||
|
|
||||||
let Some(param) = deserialize(param) else { return };
|
|
||||||
|
|
||||||
Self::on_profile_discovered(
|
|
||||||
param,
|
|
||||||
param_type,
|
|
||||||
active_profile,
|
|
||||||
profiles
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn listen_for_info<'a>(
|
|
||||||
listener_builder: pipewire::device::DeviceListenerLocalBuilder<'a>,
|
|
||||||
name: &Rc<RefCell<String>>,
|
|
||||||
properties: &Rc<RefCell<HashMap<String, String>>>,
|
|
||||||
) -> pipewire::device::DeviceListenerLocalBuilder<'a> {
|
|
||||||
listener_builder
|
|
||||||
.info({
|
|
||||||
|
|
||||||
let name_weak = Rc::downgrade(name);
|
|
||||||
let properties_weak = Rc::downgrade(properties);
|
|
||||||
|
|
||||||
move |info| {
|
|
||||||
|
|
||||||
let Some(name) = name_weak.upgrade() else { return };
|
|
||||||
let Some(properties) = properties_weak.upgrade() else { return };
|
|
||||||
|
|
||||||
Self::on_info_discovered(
|
|
||||||
info,
|
|
||||||
name,
|
|
||||||
properties
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_profile_discovered(
|
|
||||||
param: libspa::pod::Object,
|
|
||||||
param_type: ParamType,
|
|
||||||
active_profile: Rc<RefCell<Option<usize>>>,
|
|
||||||
profiles: Rc<RefCell<HashMap<usize, DeviceProfile>>>,
|
|
||||||
) {
|
|
||||||
let mut profile_id = None;
|
|
||||||
let mut profile_name = None;
|
|
||||||
let mut profile_description = None;
|
|
||||||
let mut profile_priority = None;
|
|
||||||
let mut profile_availibility = None;
|
|
||||||
|
|
||||||
for property in param.properties {
|
|
||||||
match (property.key, property.value) {
|
|
||||||
(1, libspa::pod::Value::Int(id)) => { profile_id = Some(id as usize); },
|
|
||||||
(2, libspa::pod::Value::String(name)) => { profile_name = Some(name); },
|
|
||||||
(3, libspa::pod::Value::String(description)) => { profile_description = Some(description); },
|
|
||||||
(4, libspa::pod::Value::Int(priority)) => { profile_priority = Some(priority as usize); },
|
|
||||||
(5, libspa::pod::Value::Id(libspa::utils::Id(0))) => { profile_availibility = Some(DeviceAvailability::Unknown); },
|
|
||||||
(5, libspa::pod::Value::Id(libspa::utils::Id(1))) => { profile_availibility = Some(DeviceAvailability::Unavailable); },
|
|
||||||
(5, libspa::pod::Value::Id(libspa::utils::Id(2))) => { profile_availibility = Some(DeviceAvailability::Available); },
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Proper error handling
|
|
||||||
|
|
||||||
match param_type {
|
|
||||||
ParamType::Profile => {
|
|
||||||
active_profile.borrow_mut().replace(profile_id.unwrap());
|
|
||||||
},
|
|
||||||
ParamType::EnumProfile => {
|
|
||||||
let device_profile = DeviceProfile {
|
|
||||||
id: profile_id.unwrap(),
|
|
||||||
name: profile_name.unwrap(),
|
|
||||||
description: profile_description.unwrap(),
|
|
||||||
priority: profile_priority.unwrap(),
|
|
||||||
available: profile_availibility.unwrap(),
|
|
||||||
};
|
|
||||||
profiles.borrow_mut().insert(profile_id.unwrap(), device_profile);
|
|
||||||
},
|
|
||||||
_ => return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_info_discovered(
|
|
||||||
info: &pipewire::device::DeviceInfoRef,
|
|
||||||
name: Rc<RefCell<String>>,
|
|
||||||
properties: Rc<RefCell<HashMap<String, String>>>,
|
|
||||||
) {
|
|
||||||
info.props().unwrap().iter()
|
|
||||||
.for_each(|(k, v)| {
|
|
||||||
if k == "device.description" {
|
|
||||||
*name.borrow_mut() = v.to_string();
|
|
||||||
}
|
|
||||||
properties.borrow_mut().insert(k.to_string(), v.to_string());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlobalsManager {
|
|
||||||
globals: Vec<GlobalObject<Properties>>,
|
|
||||||
devices: HashMap<usize, Device>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlobalsManager {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
globals: Vec::new(),
|
|
||||||
devices: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_global(&mut self, global: GlobalObject<Properties>, registry: &Registry) {
|
|
||||||
self.globals.push(global);
|
|
||||||
|
|
||||||
let global_ref = self.globals.last().unwrap();
|
|
||||||
|
|
||||||
let props = match &global_ref.props {
|
|
||||||
Some(props) => props,
|
|
||||||
None => return
|
|
||||||
};
|
|
||||||
|
|
||||||
match props.get("media.class") {
|
|
||||||
Some("Audio/Device") => {}
|
|
||||||
_ => return
|
|
||||||
}
|
|
||||||
|
|
||||||
let device_id = global_ref.id as usize;
|
|
||||||
|
|
||||||
let device = Device::new(registry.bind(global_ref).unwrap(), device_id);
|
|
||||||
|
|
||||||
self.devices.insert(device_id, device);
|
|
||||||
|
|
||||||
println!("ADDED DEVICE");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_device_names(&self) -> HashMap<usize, String> {
|
|
||||||
self.devices.iter()
|
|
||||||
.map(|(id, d)| (*id, d.name.borrow().clone()))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_device_properties(&self, id: usize) -> HashMap<String, String> {
|
|
||||||
self.devices.get(&id)
|
|
||||||
.and_then(|d| Some(d.properties.borrow().clone()))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn spawn_pipewire_thread() -> (JoinHandle<ExitCode>, pipewire::channel::Sender<Command>, flume::Receiver<Data>) {
|
|
||||||
|
|
||||||
let (tx, remote_rx) = flume::unbounded();
|
|
||||||
let (remote_tx, rx) = pipewire::channel::channel();
|
|
||||||
|
|
||||||
let pw_thread = thread::spawn(|| {
|
|
||||||
// Initialize PipeWire and run the main loop
|
|
||||||
// ...
|
|
||||||
let mainloop = MainLoop::new(None).expect("failed to get mail loop");
|
|
||||||
let context = Context::new(&mainloop).expect("failed to get context");
|
|
||||||
let core = context.connect(None).expect("failed to get core");
|
|
||||||
let registry = Rc::new(core.get_registry().expect("failed to get registry"));
|
|
||||||
|
|
||||||
let globals_manager = Rc::new(RefCell::new(GlobalsManager::new()));
|
|
||||||
|
|
||||||
|
|
||||||
let _listener = registry
|
|
||||||
.add_listener_local()
|
|
||||||
.global({
|
|
||||||
let registry_weak = Rc::downgrade(®istry);
|
|
||||||
let globals_manager_weak = Rc::downgrade(&globals_manager);
|
|
||||||
move |global|
|
|
||||||
{
|
|
||||||
|
|
||||||
let Some(registry) = registry_weak.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(globals_manager) = globals_manager_weak.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let owned_global = global.to_owned();
|
|
||||||
|
|
||||||
|
|
||||||
globals_manager.borrow_mut().add_global(owned_global, ®istry);
|
|
||||||
|
|
||||||
/*
|
|
||||||
if global.type_ == ObjectType::Port {
|
|
||||||
let props = global.props.as_ref().unwrap();
|
|
||||||
let port_name = props.get("port.name");
|
|
||||||
let port_alias = props.get("port.alias");
|
|
||||||
let object_path = props.get("object.path");
|
|
||||||
let format_dsp = props.get("format.dsp");
|
|
||||||
let audio_channel = props.get("audio.channel");
|
|
||||||
let port_id = props.get("port.id");
|
|
||||||
let port_direction = props.get("port.direction");
|
|
||||||
println!("Port: Name: {:?} Alias: {:?} Id: {:?} Direction: {:?} AudioChannel: {:?} Object Path: {:?} FormatDsp: {:?}",
|
|
||||||
port_name,
|
|
||||||
port_alias,
|
|
||||||
port_id,port_direction,audio_channel,object_path,format_dsp
|
|
||||||
);
|
|
||||||
} else if global.type_ == ObjectType::Device {
|
|
||||||
let props = global.props.as_ref().unwrap();
|
|
||||||
let device_name = props.get("device.name");
|
|
||||||
let device_nick = props.get("device.nick");
|
|
||||||
let device_description = props.get("device.description");
|
|
||||||
let device_api = props.get("device.api");
|
|
||||||
let media_class = props.get("media.class");
|
|
||||||
println!("Device: Name: {:?} Nick: {:?} Desc: {:?} Api: {:?} MediaClass: {:?}",
|
|
||||||
device_name, device_nick, device_description, device_api, media_class);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.register();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let _receiver = rx.attach(mainloop.loop_(), {
|
|
||||||
let mainloop = mainloop.clone();
|
|
||||||
{
|
|
||||||
let globals_manager_weak = Rc::downgrade(&globals_manager);
|
|
||||||
move |command| {
|
|
||||||
let Some(globals_manager) = globals_manager_weak.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
match command {
|
|
||||||
Command::GetDeviceNames => {
|
|
||||||
let device_names = globals_manager.borrow().get_device_names();
|
|
||||||
tx.send(Data::DeviceNames(device_names)).unwrap();
|
|
||||||
}
|
|
||||||
Command::GetDeviceProperties(id) => {
|
|
||||||
let device_properties = globals_manager.borrow().get_device_properties(id);
|
|
||||||
tx.send(Data::DeviceProperties(id, device_properties)).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calling the `destroy_global` method on the registry will destroy the object with the specified id on the remote.
|
|
||||||
// We don't have a specific object to destroy now, so this is commented out.
|
|
||||||
// registry.destroy_global(313).into_result()?;
|
|
||||||
|
|
||||||
|
|
||||||
mainloop.run();
|
|
||||||
return ExitCode::FAILURE; // not sure
|
|
||||||
});
|
|
||||||
|
|
||||||
(pw_thread, remote_tx, remote_rx)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod deserialize {
|
|
||||||
/// Taken from wiremix: https://github.com/tsowell/wiremix/blob/main/src/wirehose/deserialize.rs#L6
|
|
||||||
use libspa::pod::{deserialize::PodDeserializer, Object, Pod, Value};
|
|
||||||
pub fn deserialize(param: Option<&Pod>) -> Option<Object> {
|
|
||||||
param
|
|
||||||
.and_then(|pod| {
|
|
||||||
PodDeserializer::deserialize_any_from(pod.as_bytes()).ok()
|
|
||||||
})
|
|
||||||
.and_then(|(_, value)| match value {
|
|
||||||
Value::Object(obj) => Some(obj),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
151
src/spa_structs.rs
Normal file
151
src/spa_structs.rs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
use libspa_sys::{SPA_AUDIO_CHANNEL_START_Aux, SPA_AUDIO_CHANNEL_START_Custom, SPA_AUDIO_CHANNEL_BC, SPA_AUDIO_CHANNEL_BLC, SPA_AUDIO_CHANNEL_BRC, SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_FCH, SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FLC, SPA_AUDIO_CHANNEL_FLH, SPA_AUDIO_CHANNEL_FLW, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FRC, SPA_AUDIO_CHANNEL_FRH, SPA_AUDIO_CHANNEL_FRW, SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_LFE2, SPA_AUDIO_CHANNEL_LLFE, SPA_AUDIO_CHANNEL_MONO, SPA_AUDIO_CHANNEL_NA, SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RLC, SPA_AUDIO_CHANNEL_RLFE, SPA_AUDIO_CHANNEL_RR, SPA_AUDIO_CHANNEL_RRC, SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, SPA_AUDIO_CHANNEL_TC, SPA_AUDIO_CHANNEL_TFC, SPA_AUDIO_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFLC, SPA_AUDIO_CHANNEL_TFR, SPA_AUDIO_CHANNEL_TFRC, SPA_AUDIO_CHANNEL_TRC, SPA_AUDIO_CHANNEL_TRL, SPA_AUDIO_CHANNEL_TRR, SPA_AUDIO_CHANNEL_TSL, SPA_AUDIO_CHANNEL_TSR, SPA_AUDIO_CHANNEL_UNKNOWN};
|
||||||
|
|
||||||
|
// Modified from https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/include/spa/param/audio/raw.h
|
||||||
|
pub(crate) enum Channel {
|
||||||
|
UNKNOWN, /**< unspecified */
|
||||||
|
NA, /**< N/A, silent */
|
||||||
|
|
||||||
|
MONO, /**< mono stream */
|
||||||
|
|
||||||
|
FL, /**< front left */
|
||||||
|
FR, /**< front right */
|
||||||
|
FC, /**< front center */
|
||||||
|
LFE, /**< LFE */
|
||||||
|
SL, /**< side left */
|
||||||
|
SR, /**< side right */
|
||||||
|
FLC, /**< front left center */
|
||||||
|
FRC, /**< front right center */
|
||||||
|
RC, /**< rear center */
|
||||||
|
RL, /**< rear left */
|
||||||
|
RR, /**< rear right */
|
||||||
|
TC, /**< top center */
|
||||||
|
TFL, /**< top front left */
|
||||||
|
TFC, /**< top front center */
|
||||||
|
TFR, /**< top front right */
|
||||||
|
TRL, /**< top rear left */
|
||||||
|
TRC, /**< top rear center */
|
||||||
|
TRR, /**< top rear right */
|
||||||
|
RLC, /**< rear left center */
|
||||||
|
RRC, /**< rear right center */
|
||||||
|
FLW, /**< front left wide */
|
||||||
|
FRW, /**< front right wide */
|
||||||
|
LFE2, /**< LFE 2 */
|
||||||
|
FLH, /**< front left high */
|
||||||
|
FCH, /**< front center high */
|
||||||
|
FRH, /**< front right high */
|
||||||
|
TFLC, /**< top front left center */
|
||||||
|
TFRC, /**< top front right center */
|
||||||
|
TSL, /**< top side left */
|
||||||
|
TSR, /**< top side right */
|
||||||
|
LLFE, /**< left LFE */
|
||||||
|
RLFE, /**< right LFE */
|
||||||
|
BC, /**< bottom center */
|
||||||
|
BLC, /**< bottom left center */
|
||||||
|
BRC, /**< bottom right center */
|
||||||
|
AUX(u32), // 0 - 4095
|
||||||
|
CUSTOM(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Channel> for libspa_sys::spa_audio_channel {
|
||||||
|
fn from(value: Channel) -> Self {
|
||||||
|
match value {
|
||||||
|
Channel::UNKNOWN => SPA_AUDIO_CHANNEL_UNKNOWN,
|
||||||
|
Channel::NA => SPA_AUDIO_CHANNEL_NA,
|
||||||
|
Channel::MONO => SPA_AUDIO_CHANNEL_MONO,
|
||||||
|
Channel::FL => SPA_AUDIO_CHANNEL_FL,
|
||||||
|
Channel::FR => SPA_AUDIO_CHANNEL_FR,
|
||||||
|
Channel::FC => SPA_AUDIO_CHANNEL_FC,
|
||||||
|
Channel::LFE => SPA_AUDIO_CHANNEL_LFE,
|
||||||
|
Channel::SL => SPA_AUDIO_CHANNEL_SL,
|
||||||
|
Channel::SR => SPA_AUDIO_CHANNEL_SR,
|
||||||
|
Channel::FLC => SPA_AUDIO_CHANNEL_FLC,
|
||||||
|
Channel::FRC => SPA_AUDIO_CHANNEL_FRC,
|
||||||
|
Channel::RC => SPA_AUDIO_CHANNEL_RC,
|
||||||
|
Channel::RL => SPA_AUDIO_CHANNEL_RL,
|
||||||
|
Channel::RR => SPA_AUDIO_CHANNEL_RR,
|
||||||
|
Channel::TC => SPA_AUDIO_CHANNEL_TC,
|
||||||
|
Channel::TFL => SPA_AUDIO_CHANNEL_TFL,
|
||||||
|
Channel::TFC => SPA_AUDIO_CHANNEL_TFC,
|
||||||
|
Channel::TFR => SPA_AUDIO_CHANNEL_TFR,
|
||||||
|
Channel::TRL => SPA_AUDIO_CHANNEL_TRL,
|
||||||
|
Channel::TRC => SPA_AUDIO_CHANNEL_TRC,
|
||||||
|
Channel::TRR => SPA_AUDIO_CHANNEL_TRR,
|
||||||
|
Channel::RLC => SPA_AUDIO_CHANNEL_RLC,
|
||||||
|
Channel::RRC => SPA_AUDIO_CHANNEL_RRC,
|
||||||
|
Channel::FLW => SPA_AUDIO_CHANNEL_FLW,
|
||||||
|
Channel::FRW => SPA_AUDIO_CHANNEL_FRW,
|
||||||
|
Channel::LFE2 => SPA_AUDIO_CHANNEL_LFE2,
|
||||||
|
Channel::FLH => SPA_AUDIO_CHANNEL_FLH,
|
||||||
|
Channel::FCH => SPA_AUDIO_CHANNEL_FCH,
|
||||||
|
Channel::FRH => SPA_AUDIO_CHANNEL_FRH,
|
||||||
|
Channel::TFLC => SPA_AUDIO_CHANNEL_TFLC,
|
||||||
|
Channel::TFRC => SPA_AUDIO_CHANNEL_TFRC,
|
||||||
|
Channel::TSL => SPA_AUDIO_CHANNEL_TSL,
|
||||||
|
Channel::TSR => SPA_AUDIO_CHANNEL_TSR,
|
||||||
|
Channel::LLFE => SPA_AUDIO_CHANNEL_LLFE,
|
||||||
|
Channel::RLFE => SPA_AUDIO_CHANNEL_RLFE,
|
||||||
|
Channel::BC => SPA_AUDIO_CHANNEL_BC,
|
||||||
|
Channel::BLC => SPA_AUDIO_CHANNEL_BLC,
|
||||||
|
Channel::BRC => SPA_AUDIO_CHANNEL_BRC,
|
||||||
|
Channel::AUX(aux) => SPA_AUDIO_CHANNEL_START_Aux + aux,
|
||||||
|
Channel::CUSTOM(custom) => SPA_AUDIO_CHANNEL_START_Custom + custom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/include/spa/param/audio/raw-types.h
|
||||||
|
impl From<Channel> for String {
|
||||||
|
fn from(value: Channel) -> String {
|
||||||
|
match value {
|
||||||
|
Channel::UNKNOWN => "UNK".to_owned(),
|
||||||
|
Channel::NA => "NA".to_owned(),
|
||||||
|
Channel::MONO => "MONO".to_owned(),
|
||||||
|
Channel::FL => "FL".to_owned(),
|
||||||
|
Channel::FR => "FR".to_owned(),
|
||||||
|
Channel::FC => "FC".to_owned(),
|
||||||
|
Channel::LFE => "LFE".to_owned(),
|
||||||
|
Channel::SL => "SL".to_owned(),
|
||||||
|
Channel::SR => "SR".to_owned(),
|
||||||
|
Channel::FLC => "FLC".to_owned(),
|
||||||
|
Channel::FRC => "FRC".to_owned(),
|
||||||
|
Channel::RC => "RC".to_owned(),
|
||||||
|
Channel::RL => "RL".to_owned(),
|
||||||
|
Channel::RR => "RR".to_owned(),
|
||||||
|
Channel::TC => "TC".to_owned(),
|
||||||
|
Channel::TFL => "TFL".to_owned(),
|
||||||
|
Channel::TFC => "TFC".to_owned(),
|
||||||
|
Channel::TFR => "TFR".to_owned(),
|
||||||
|
Channel::TRL => "TRL".to_owned(),
|
||||||
|
Channel::TRC => "TRC".to_owned(),
|
||||||
|
Channel::TRR => "TRR".to_owned(),
|
||||||
|
Channel::RLC => "RLC".to_owned(),
|
||||||
|
Channel::RRC => "RRC".to_owned(),
|
||||||
|
Channel::FLW => "FLW".to_owned(),
|
||||||
|
Channel::FRW => "FRW".to_owned(),
|
||||||
|
Channel::LFE2 => "LFE2".to_owned(),
|
||||||
|
Channel::FLH => "FLH".to_owned(),
|
||||||
|
Channel::FCH => "FCH".to_owned(),
|
||||||
|
Channel::FRH => "FRH".to_owned(),
|
||||||
|
Channel::TFLC => "TFLC".to_owned(),
|
||||||
|
Channel::TFRC => "TFRC".to_owned(),
|
||||||
|
Channel::TSL => "TSL".to_owned(),
|
||||||
|
Channel::TSR => "TSR".to_owned(),
|
||||||
|
Channel::LLFE => "LLFE".to_owned(),
|
||||||
|
Channel::RLFE => "RLFE".to_owned(),
|
||||||
|
Channel::BC => "BC".to_owned(),
|
||||||
|
Channel::BLC => "BLC".to_owned(),
|
||||||
|
Channel::BRC => "BRC".to_owned(),
|
||||||
|
Channel::AUX(aux) => {
|
||||||
|
// > 63 is not official spec
|
||||||
|
if aux > 4095 {
|
||||||
|
panic!("AUX Channel has to be in range 0 - 4095");
|
||||||
|
}
|
||||||
|
format!("AUX{}", aux)
|
||||||
|
},
|
||||||
|
Channel::CUSTOM(custom) => {
|
||||||
|
// not official spec
|
||||||
|
format!("CUSTOM{}", custom)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
|
use adw::prelude::*;
|
||||||
use std::cell::{Ref, RefCell, RefMut};
|
use std::cell::{Ref, RefCell, RefMut};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use adw::gdk::Device;
|
use adw::gdk::Device;
|
||||||
|
use uuid::Uuid;
|
||||||
use crate::state_manager::simple_serde::SimpleSerde;
|
use crate::state_manager::simple_serde::SimpleSerde;
|
||||||
use crate::state_manager::structs::DeviceConfig;
|
use crate::state_manager::structs::DeviceConfig;
|
||||||
|
|
||||||
@ -80,10 +82,13 @@ pub(crate) mod backends {
|
|||||||
|
|
||||||
|
|
||||||
pub(crate) mod structs {
|
pub(crate) mod structs {
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use uuid::{Timestamp, Uuid};
|
||||||
|
use crate::spa_structs::Channel;
|
||||||
use crate::state_manager::StateBackend;
|
use crate::state_manager::StateBackend;
|
||||||
|
|
||||||
pub(crate) struct DeviceConfig {
|
pub(crate) struct DeviceConfig {
|
||||||
|
id: Uuid,
|
||||||
name: String,
|
name: String,
|
||||||
identifying_properties: HashMap<String, String>,
|
identifying_properties: HashMap<String, String>,
|
||||||
available_profiles: Vec<String>,
|
available_profiles: Vec<String>,
|
||||||
@ -99,7 +104,10 @@ pub(crate) mod structs {
|
|||||||
backend: &mut Backend,
|
backend: &mut Backend,
|
||||||
namespace: &Option<String>,
|
namespace: &Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let id = Uuid::now_v7();
|
||||||
|
|
||||||
let instance = Self {
|
let instance = Self {
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
identifying_properties,
|
identifying_properties,
|
||||||
available_profiles,
|
available_profiles,
|
||||||
@ -110,15 +118,26 @@ pub(crate) mod structs {
|
|||||||
|
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
pub(super) fn load<Backend: StateBackend>(name: String, backend: &mut Backend, namespace: &Option<String>) -> Self {
|
|
||||||
|
fn get_namespaces(id: Uuid, namespace: &Option<String>) -> (Option<String>, Option<String>) {
|
||||||
let device_namespace_string = match namespace {
|
let device_namespace_string = match namespace {
|
||||||
Some(ns) => ns.clone() + "/" + &name,
|
Some(ns) => ns.clone() + "/" + &id.to_string(),
|
||||||
None => name.clone()
|
None => id.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let properties_namespace = Some(device_namespace_string.clone() + "/" + "identifying_properties");
|
let properties_namespace = Some(device_namespace_string.clone() + "/" + "identifying_properties");
|
||||||
let device_namespace = Some(device_namespace_string);
|
let device_namespace = Some(device_namespace_string);
|
||||||
|
|
||||||
|
(
|
||||||
|
device_namespace,
|
||||||
|
properties_namespace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn load<Backend: StateBackend>(id: Uuid, backend: &mut Backend, namespace: &Option<String>) -> Self {
|
||||||
|
|
||||||
|
let (device_namespace, properties_namespace) = Self::get_namespaces(id, namespace);
|
||||||
|
|
||||||
let properties = backend.get("properties", &device_namespace).unwrap_or(Vec::new());
|
let properties = backend.get("properties", &device_namespace).unwrap_or(Vec::new());
|
||||||
|
|
||||||
let identifying_properties = properties.into_iter()
|
let identifying_properties = properties.into_iter()
|
||||||
@ -129,32 +148,28 @@ pub(crate) mod structs {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name,
|
id,
|
||||||
|
name: backend.get("name", &device_namespace).expect("name not found"),
|
||||||
available_profiles: backend.get("available_profiles", &device_namespace).expect("available_profiles not found"),
|
available_profiles: backend.get("available_profiles", &device_namespace).expect("available_profiles not found"),
|
||||||
selected_profile: backend.get("selected_profile", &device_namespace),
|
selected_profile: backend.get("selected_profile", &device_namespace),
|
||||||
identifying_properties
|
identifying_properties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(super) fn save<Backend: StateBackend>(&self, backend: &mut Backend, namespace: &Option<String>) -> Result<(), String> {
|
pub(super) fn save<Backend: StateBackend>(&self, backend: &mut Backend, namespace: &Option<String>) -> Result<(), String> {
|
||||||
let mut devices: Vec<String> = backend.get("devices", &namespace)
|
let mut devices: Vec<Uuid> = backend.get("devices", &namespace)
|
||||||
.unwrap_or(Vec::new());
|
.unwrap_or(Vec::new());
|
||||||
|
|
||||||
if devices.contains(&self.name) {
|
if devices.contains(&self.id) {
|
||||||
return Err("Two devices with the same name cannot exist".to_string())
|
Self::remove_by_id(self.id, backend, namespace);
|
||||||
|
} else {
|
||||||
|
devices.push(self.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
devices.push(self.name.clone());
|
|
||||||
|
|
||||||
backend.insert("devices", &devices, &namespace);
|
backend.insert("devices", &devices, &namespace);
|
||||||
|
|
||||||
let device_namespace_string = match namespace {
|
let (device_namespace, properties_namespace) = Self::get_namespaces(self.id, namespace);
|
||||||
Some(ns) => ns.clone() + "/" + &self.name,
|
|
||||||
None => self.name.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let properties_namespace = Some(device_namespace_string.clone() + "/" + "identifying_properties");
|
|
||||||
let device_namespace = Some(device_namespace_string);
|
|
||||||
|
|
||||||
|
backend.insert("name", &self.name, &device_namespace);
|
||||||
backend.insert("available_profiles", &self.available_profiles, &device_namespace);
|
backend.insert("available_profiles", &self.available_profiles, &device_namespace);
|
||||||
backend.insert_option("selected_profile", &self.selected_profile, &device_namespace);
|
backend.insert_option("selected_profile", &self.selected_profile, &device_namespace);
|
||||||
|
|
||||||
@ -171,10 +186,50 @@ pub(crate) mod structs {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn remove_by_id<Backend: StateBackend>(id: Uuid, backend: &mut Backend, namespace: &Option<String>) {
|
||||||
|
let mut devices: Vec<Uuid> = backend.get("devices", &namespace)
|
||||||
|
.unwrap_or(Vec::new());
|
||||||
|
|
||||||
|
devices = devices.into_iter()
|
||||||
|
.filter(|device_id| *device_id != id)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
backend.insert("devices", &devices, &namespace);
|
||||||
|
|
||||||
|
let (device_namespace, properties_namespace) = Self::get_namespaces(id, namespace);
|
||||||
|
|
||||||
|
backend.remove("name", &device_namespace);
|
||||||
|
backend.remove("available_profiles", &device_namespace);
|
||||||
|
backend.remove("selected_profile", &device_namespace);
|
||||||
|
|
||||||
|
let properties: Vec<String> = backend.get("properties", &device_namespace).unwrap_or(Vec::new());
|
||||||
|
|
||||||
|
backend.remove("properties", &device_namespace);
|
||||||
|
|
||||||
|
for name in properties {
|
||||||
|
backend.remove(&name, &properties_namespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn remove<Backend: StateBackend>(&self, backend: &mut Backend, namespace: &Option<String>) {
|
||||||
|
Self::remove_by_id(self.id, backend, namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn change_name<Backend: StateBackend>(&mut self, new_name: String, backend: &mut Backend, namespace: &Option<String>) {
|
||||||
|
|
||||||
|
let (device_namespace, _) = Self::get_namespaces(self.id, namespace);
|
||||||
|
|
||||||
|
backend.insert("name", &new_name, &device_namespace);
|
||||||
|
|
||||||
|
self.name = new_name;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_name(&self) -> &str {
|
pub(crate) fn get_name(&self) -> &str {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_id(&self) -> Uuid { self.id }
|
||||||
|
|
||||||
pub(crate) fn get_identifying_properties(&self) -> &HashMap<String, String> {
|
pub(crate) fn get_identifying_properties(&self) -> &HashMap<String, String> {
|
||||||
&self.identifying_properties
|
&self.identifying_properties
|
||||||
}
|
}
|
||||||
@ -186,11 +241,64 @@ pub(crate) mod structs {
|
|||||||
pub(crate) fn get_selected_profile(&self) -> &Option<String> {
|
pub(crate) fn get_selected_profile(&self) -> &Option<String> {
|
||||||
&self.selected_profile
|
&self.selected_profile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_available_channels(&self) -> HashSet<Channel> {
|
||||||
|
let channels = HashSet::new();
|
||||||
|
|
||||||
|
channels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct InputProfile {
|
||||||
|
id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct OutputProfile {
|
||||||
|
id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoopbackProfile {
|
||||||
|
Input(Uuid),
|
||||||
|
Output(Uuid),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoopbackSource {
|
||||||
|
DeviceConfig(Uuid),
|
||||||
|
Loopback(Uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Loopback {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
profile: LoopbackProfile,
|
||||||
|
virtual_only: bool,
|
||||||
|
// has to be checked for validity everytime it's loaded
|
||||||
|
// If Source changes profile, available channels also change
|
||||||
|
// Perhaps backreference, so that warning can be given?
|
||||||
|
mappings: HashMap<Channel, (LoopbackSource, String)>
|
||||||
|
|
||||||
|
}
|
||||||
|
pub(crate) struct Output {
|
||||||
|
name: String,
|
||||||
|
output_profile: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Device {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
device_configs: Vec<Uuid>,
|
||||||
|
inputs: Vec<Loopback>,
|
||||||
|
outputs: Vec<Loopback>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod simple_serde {
|
pub(crate) mod simple_serde {
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub(crate) trait SimpleSerde {
|
pub(crate) trait SimpleSerde {
|
||||||
fn serialize(&self) -> Vec<u8>;
|
fn serialize(&self) -> Vec<u8>;
|
||||||
@ -266,10 +374,22 @@ pub(crate) mod simple_serde {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SimpleSerde for Uuid {
|
||||||
|
fn serialize(&self) -> Vec<u8> {
|
||||||
|
self.as_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize(bytes: Vec<u8>) -> Self {
|
||||||
|
let uuid_bytes = uuid::Bytes::try_from(bytes).unwrap();
|
||||||
|
Uuid::from_bytes(uuid_bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RootNamespaces {
|
enum RootNamespaces {
|
||||||
None,
|
None,
|
||||||
|
Devices,
|
||||||
DeviceConfigs,
|
DeviceConfigs,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,59 +397,78 @@ impl From<RootNamespaces> for Option<String> {
|
|||||||
fn from(value: RootNamespaces) -> Self {
|
fn from(value: RootNamespaces) -> Self {
|
||||||
match value {
|
match value {
|
||||||
RootNamespaces::None => None,
|
RootNamespaces::None => None,
|
||||||
RootNamespaces::DeviceConfigs => Some("DeviceConfigs".to_string()),
|
RootNamespaces::Devices => Some("Devices".to_owned()),
|
||||||
|
RootNamespaces::DeviceConfigs => Some("DeviceConfigs".to_owned()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct StateManager<Backend: StateBackend> {
|
pub(crate) struct StateManager<Backend: StateBackend> {
|
||||||
db: Backend,
|
db: Backend,
|
||||||
device_configs: HashMap<String, DeviceConfig>
|
selected_device: String,
|
||||||
|
device_configs: HashMap<Uuid, DeviceConfig>,
|
||||||
|
device_config_action_rows: HashMap<String, HashMap<Uuid, Vec<adw::ActionRow>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Backend: StateBackend> StateManager<Backend> {
|
impl<Backend: StateBackend> StateManager<Backend> {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
let mut instance = Self {
|
let mut instance = Self {
|
||||||
db: Backend::new(crate::utils::CONFIG_DIR.clone().join("db")),
|
db: Backend::new(crate::utils::CONFIG_DIR.clone().join("db")),
|
||||||
|
selected_device: String::new(),
|
||||||
device_configs: HashMap::new(),
|
device_configs: HashMap::new(),
|
||||||
|
device_config_action_rows: HashMap::new()
|
||||||
};
|
};
|
||||||
instance.load();
|
instance.load();
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load(&mut self) {
|
pub(crate) fn load(&mut self) {
|
||||||
|
self.selected_device = self.db.get("selected_device", &RootNamespaces::Devices.into())
|
||||||
|
.unwrap_or("".to_owned());
|
||||||
|
|
||||||
let device_config_names: Vec<String> = self.db.get("devices", &RootNamespaces::DeviceConfigs.into())
|
let device_config_ids: Vec<Uuid> = self.db.get("devices", &RootNamespaces::DeviceConfigs.into())
|
||||||
.unwrap_or(Vec::new());
|
.unwrap_or(Vec::new());
|
||||||
|
|
||||||
println!("device_config_names: {:?}", device_config_names);
|
println!("device_config_ids: {:?}", device_config_ids);
|
||||||
|
|
||||||
for device_config_name in device_config_names {
|
for id in device_config_ids {
|
||||||
let device_config = DeviceConfig::load(device_config_name.clone(), &mut self.db, &RootNamespaces::DeviceConfigs.into());
|
let device_config = DeviceConfig::load(id, &mut self.db, &RootNamespaces::DeviceConfigs.into());
|
||||||
self.device_configs.insert(device_config_name, device_config);
|
self.device_configs.insert(id, device_config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_device_config_names(&mut self) -> Vec<String> {
|
pub(crate) fn get_device_config_ids(&self) -> Vec<Uuid> {
|
||||||
self.device_configs.iter()
|
self.device_configs.iter()
|
||||||
.map(|(name, _)| name.clone())
|
.map(|(id, _)| *id)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_device_config_properties(&mut self, name: &str) -> &HashMap<String, String> {
|
pub(crate) fn get_device_config_name(&self, device_config_id: &Uuid) -> String {
|
||||||
self.device_configs.get(name)
|
self.device_configs.get(device_config_id).unwrap()
|
||||||
|
.get_name()
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_device_config_names(&self) -> HashMap<Uuid, String> {
|
||||||
|
self.device_configs.iter()
|
||||||
|
.map(|(id, config)| (*id, config.get_name().to_owned()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_device_config_properties(&self, device_config_id: &Uuid) -> &HashMap<String, String> {
|
||||||
|
self.device_configs.get(device_config_id)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get_identifying_properties()
|
.get_identifying_properties()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_device_config_profiles(&mut self, name: &str) -> &Vec<String> {
|
pub(crate) fn get_device_config_profiles(&self, device_config_id: &Uuid) -> &Vec<String> {
|
||||||
self.device_configs.get(name)
|
self.device_configs.get(device_config_id)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get_available_profiles()
|
.get_available_profiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_device_config_selected_profile(&mut self, name: &str) -> &Option<String> {
|
pub(crate) fn get_device_config_selected_profile(&self, device_config_id: &Uuid) -> &Option<String> {
|
||||||
self.device_configs.get(name)
|
self.device_configs.get(device_config_id)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get_selected_profile()
|
.get_selected_profile()
|
||||||
}
|
}
|
||||||
@ -340,7 +479,7 @@ impl<Backend: StateBackend> StateManager<Backend> {
|
|||||||
identifying_properties: HashMap<String, String>,
|
identifying_properties: HashMap<String, String>,
|
||||||
profiles: Vec<String>,
|
profiles: Vec<String>,
|
||||||
active_profile: Option<String>
|
active_profile: Option<String>
|
||||||
) {
|
) -> Uuid {
|
||||||
let config = DeviceConfig::new(
|
let config = DeviceConfig::new(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
identifying_properties,
|
identifying_properties,
|
||||||
@ -350,7 +489,76 @@ impl<Backend: StateBackend> StateManager<Backend> {
|
|||||||
&RootNamespaces::DeviceConfigs.into()
|
&RootNamespaces::DeviceConfigs.into()
|
||||||
);
|
);
|
||||||
|
|
||||||
self.device_configs.insert(name, config);
|
let config_id = config.get_id();
|
||||||
|
|
||||||
|
self.device_configs.insert(config_id, config);
|
||||||
|
|
||||||
|
config_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register action_rows, so that if the device config name is changed
|
||||||
|
// the action row text is changed too
|
||||||
|
pub(crate) fn register_device_config_action_row(
|
||||||
|
&mut self,
|
||||||
|
device_name: String,
|
||||||
|
device_config_id: Uuid,
|
||||||
|
row: adw::ActionRow
|
||||||
|
) {
|
||||||
|
row.set_title(
|
||||||
|
&self.get_device_config_name(&device_config_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
self.device_config_action_rows
|
||||||
|
.entry(device_name).or_insert(HashMap::new())
|
||||||
|
.entry(device_config_id).or_insert(vec![])
|
||||||
|
.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn change_device_config_name(
|
||||||
|
&mut self,
|
||||||
|
device_config_id: &Uuid,
|
||||||
|
new_name: String,
|
||||||
|
) {
|
||||||
|
let config = self.device_configs.get_mut(device_config_id).unwrap();
|
||||||
|
|
||||||
|
config
|
||||||
|
.change_name(
|
||||||
|
new_name.clone(),
|
||||||
|
&mut self.db,
|
||||||
|
&RootNamespaces::DeviceConfigs.into()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (_, rows_map) in self.device_config_action_rows.iter_mut() {
|
||||||
|
rows_map.get(device_config_id)
|
||||||
|
.map(|rows| {
|
||||||
|
for row in rows {
|
||||||
|
row.set_title(&new_name);
|
||||||
|
};
|
||||||
|
rows
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister device, so that after use all widgets registered with it
|
||||||
|
// can be freed up again
|
||||||
|
pub(crate) fn unregister_device(
|
||||||
|
&mut self,
|
||||||
|
device_name: &str,
|
||||||
|
) {
|
||||||
|
self.device_config_action_rows.remove(device_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_current_device(
|
||||||
|
&mut self,
|
||||||
|
device_name: String,
|
||||||
|
) {
|
||||||
|
self.db.insert("selected_device", &device_name, &RootNamespaces::Devices.into());
|
||||||
|
self.selected_device = device_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_current_device(&self) -> &String {
|
||||||
|
&self.selected_device
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
src/window/message_handler.rs
Normal file
5
src/window/message_handler.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
use crate::window::AudioDeviceManagerWindow;
|
||||||
|
|
||||||
|
async fn message_handler(main_window: &AudioDeviceManagerWindow) {
|
||||||
|
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
mod popups;
|
mod popups;
|
||||||
mod subpages;
|
mod subpages;
|
||||||
|
mod message_handler;
|
||||||
|
|
||||||
use std::cell::{Ref, RefCell, RefMut};
|
use std::cell::{Ref, RefCell, RefMut};
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
@ -31,6 +32,7 @@ use gtk::prelude::*;
|
|||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use gtk::{gio, glib};
|
use gtk::{gio, glib};
|
||||||
|
use uuid::Uuid;
|
||||||
use subpages::device_config;
|
use subpages::device_config;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -60,7 +62,7 @@ impl Default for RcProceduralChildManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use crate::pw_manager::PipewireManager;
|
use crate::pipewire_manager::PipewireManager;
|
||||||
use crate::state_manager::RcStateManager;
|
use crate::state_manager::RcStateManager;
|
||||||
use crate::state_manager::backends::SledBackend;
|
use crate::state_manager::backends::SledBackend;
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -202,7 +204,7 @@ impl AudioDeviceManagerWindow {
|
|||||||
.child()
|
.child()
|
||||||
.and_downcast()
|
.and_downcast()
|
||||||
.expect("No Label in Row");
|
.expect("No Label in Row");
|
||||||
window.select_device(label.text().to_string());
|
window.select_device(label.text().to_owned());
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
*/
|
*/
|
||||||
@ -216,24 +218,30 @@ impl AudioDeviceManagerWindow {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_device_config(&self, config_name: &str) {
|
fn add_device_config(&self, config_id: Uuid) {
|
||||||
self.imp().procedural_child_manager.borrow_mut().device_configs_preferences_group.add(
|
self.imp().procedural_child_manager.borrow_mut().device_configs_preferences_group.add(
|
||||||
&self.build_device_config_row(config_name.to_string())
|
&self.build_device_config_row(config_id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_device_config_row(&self, config_name: String) -> adw::ActionRow {
|
fn build_device_config_row(&self, config_id: Uuid) -> adw::ActionRow {
|
||||||
let row = adw::ActionRow::builder()
|
let row = adw::ActionRow::builder()
|
||||||
.title(config_name)
|
|
||||||
.activatable(true)
|
.activatable(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
self.imp().state_manager.borrow_mut().register_device_config_action_row(
|
||||||
|
String::from(self.imp().device_navigation_page.title()),
|
||||||
|
config_id,
|
||||||
|
row.clone()
|
||||||
|
);
|
||||||
|
|
||||||
row.connect_activated(
|
row.connect_activated(
|
||||||
clone!(
|
clone!(
|
||||||
#[weak(rename_to = window)]
|
#[weak(rename_to = window)]
|
||||||
self,
|
self,
|
||||||
move |row| {
|
move |row| {
|
||||||
println!("Row selected {:?}", row);
|
let device_name = String::from(row.title());
|
||||||
|
device_config::switch_to_device_config_window(&window, config_id);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
89
src/window/popups/change_device_config_name.rs
Normal file
89
src/window/popups/change_device_config_name.rs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
use super::*;
|
||||||
|
use adw::ResponseAppearance;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub(in crate::window) async fn change_device_config_name(
|
||||||
|
main_window: &AudioDeviceManagerWindow,
|
||||||
|
device_config_id: Uuid,
|
||||||
|
) -> Option<String> {
|
||||||
|
let old_name = main_window.imp().state_manager.borrow().get_device_config_name(&device_config_id);
|
||||||
|
|
||||||
|
let entry = gtk::Entry::builder()
|
||||||
|
.placeholder_text("Name")
|
||||||
|
.text(old_name.clone())
|
||||||
|
.activates_default(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let cancel_response = "cancel";
|
||||||
|
let change_response = "change";
|
||||||
|
|
||||||
|
// Create new dialog
|
||||||
|
let dialog = adw::AlertDialog::builder()
|
||||||
|
.heading("Change Configuration Name")
|
||||||
|
.close_response(cancel_response)
|
||||||
|
.default_response(change_response)
|
||||||
|
.extra_child(&entry)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
dialog.add_responses(&[(cancel_response, "Cancel"), (change_response, "Change")]);
|
||||||
|
|
||||||
|
dialog.set_response_appearance(change_response, ResponseAppearance::Suggested);
|
||||||
|
|
||||||
|
let config_names: Vec<String> = main_window.imp().state_manager.borrow_mut()
|
||||||
|
.get_device_config_names()
|
||||||
|
.into_values()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Set entry's css class to "error", when there is no text in it
|
||||||
|
entry.connect_changed(clone!(
|
||||||
|
#[weak]
|
||||||
|
dialog,
|
||||||
|
#[strong]
|
||||||
|
old_name,
|
||||||
|
move |entry| {
|
||||||
|
let text = entry.text();
|
||||||
|
let empty = text.is_empty();
|
||||||
|
let exists = config_names.contains(&String::from(text.clone()));
|
||||||
|
|
||||||
|
let mut err = empty || exists;
|
||||||
|
|
||||||
|
if text == old_name {
|
||||||
|
err = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.set_response_enabled(change_response, !err);
|
||||||
|
|
||||||
|
if err {
|
||||||
|
entry.add_css_class("error");
|
||||||
|
} else {
|
||||||
|
entry.remove_css_class("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let response = dialog.choose_future(main_window).await;
|
||||||
|
|
||||||
|
// Return if the user chose 'cancel_response'
|
||||||
|
|
||||||
|
if response == cancel_response {
|
||||||
|
println!("Cancel");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if response == change_response {
|
||||||
|
let new_name = entry.text().to_string();
|
||||||
|
|
||||||
|
if old_name == new_name {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
main_window.imp().state_manager.borrow_mut().change_device_config_name(
|
||||||
|
&device_config_id,
|
||||||
|
new_name.clone()
|
||||||
|
);
|
||||||
|
|
||||||
|
return Some(new_name);
|
||||||
|
};
|
||||||
|
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
@ -7,6 +7,8 @@ use crate::window::AudioDeviceManagerWindow;
|
|||||||
|
|
||||||
pub(super) mod new_device;
|
pub(super) mod new_device;
|
||||||
pub(super) mod select_device_config;
|
pub(super) mod select_device_config;
|
||||||
mod new_device_config;
|
pub(super) mod new_device_config;
|
||||||
|
pub mod change_device_config_name;
|
||||||
|
mod new_device_config_type;
|
||||||
mod select_pipewire_device;
|
mod select_pipewire_device;
|
||||||
mod new_device_config_name;
|
mod new_device_config_name;
|
||||||
@ -1,51 +1,25 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use crate::window::popups::new_device_config_name::new_device_config_name;
|
||||||
|
use crate::window::subpages;
|
||||||
use super::*;
|
use super::*;
|
||||||
use adw::ResponseAppearance;
|
|
||||||
use crate::window::popups;
|
|
||||||
|
|
||||||
pub(in crate::window) async fn new_device_config(main_window: &AudioDeviceManagerWindow) {
|
pub(in crate::window) async fn new_device_config(
|
||||||
let cancel_response = "cancel";
|
main_window: &AudioDeviceManagerWindow,
|
||||||
let create_from_template_response = "create-from-template";
|
name_suggestion: Option<String>,
|
||||||
let create_new_response = "create-new";
|
properties: HashMap<String, String>,
|
||||||
|
profiles: Vec<String>,
|
||||||
|
selected_profile: Option<String>,
|
||||||
|
|
||||||
// Create new dialog
|
) {
|
||||||
let dialog = adw::AlertDialog::builder()
|
let Some(name) = new_device_config_name(main_window, name_suggestion).await else { return; };
|
||||||
.heading("New Configuration")
|
|
||||||
.close_response(cancel_response)
|
|
||||||
.default_response(create_from_template_response)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
dialog.add_responses(&[
|
let config_id = main_window.imp().state_manager.borrow_mut().create_new_device_config(
|
||||||
(cancel_response, "Cancel"),
|
name.clone(),
|
||||||
(create_new_response, "Create From Scratch"),
|
properties,
|
||||||
(create_from_template_response, "Use Pipewire Device as Template"),
|
profiles,
|
||||||
]);
|
selected_profile
|
||||||
|
);
|
||||||
|
|
||||||
// Make the dialog button insensitive initially
|
main_window.add_device_config(config_id);
|
||||||
dialog.set_response_appearance(create_from_template_response, ResponseAppearance::Suggested);
|
subpages::device_config::switch_to_device_config_window(main_window, config_id);
|
||||||
dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive);
|
|
||||||
|
|
||||||
let response = dialog.choose_future(main_window).await;
|
|
||||||
|
|
||||||
// Return if the user chose 'cancel_response'
|
|
||||||
if response == cancel_response {
|
|
||||||
println!("Cancel");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if response == create_from_template_response {
|
|
||||||
popups::select_pipewire_device::select_pipewire_device(main_window).await;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if response == create_new_response {
|
|
||||||
popups::new_device_config_name::new_device_config_name(
|
|
||||||
main_window,
|
|
||||||
None,
|
|
||||||
HashMap::new(),
|
|
||||||
Vec::new(),
|
|
||||||
None
|
|
||||||
).await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -8,10 +8,7 @@ use super::*;
|
|||||||
pub(in crate::window) async fn new_device_config_name(
|
pub(in crate::window) async fn new_device_config_name(
|
||||||
main_window: &AudioDeviceManagerWindow,
|
main_window: &AudioDeviceManagerWindow,
|
||||||
name_suggestion: Option<String>,
|
name_suggestion: Option<String>,
|
||||||
properties: HashMap<String, String>,
|
) -> Option<String> {
|
||||||
profiles: Vec<String>,
|
|
||||||
selected_profile: Option<String>,
|
|
||||||
) {
|
|
||||||
let entry = gtk::Entry::builder()
|
let entry = gtk::Entry::builder()
|
||||||
.placeholder_text("Name")
|
.placeholder_text("Name")
|
||||||
.activates_default(true)
|
.activates_default(true)
|
||||||
@ -34,7 +31,10 @@ pub(in crate::window) async fn new_device_config_name(
|
|||||||
dialog.set_response_enabled(create_response, false);
|
dialog.set_response_enabled(create_response, false);
|
||||||
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
|
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
|
||||||
|
|
||||||
let config_names = main_window.imp().state_manager.borrow_mut().get_device_config_names();
|
let config_names: Vec<String> = main_window.imp().state_manager.borrow_mut()
|
||||||
|
.get_device_config_names()
|
||||||
|
.into_values()
|
||||||
|
.collect();
|
||||||
|
|
||||||
if let Some(init_name) = name_suggestion {
|
if let Some(init_name) = name_suggestion {
|
||||||
entry.set_text(&init_name);
|
entry.set_text(&init_name);
|
||||||
@ -72,22 +72,14 @@ pub(in crate::window) async fn new_device_config_name(
|
|||||||
|
|
||||||
if response == cancel_response {
|
if response == cancel_response {
|
||||||
println!("Cancel");
|
println!("Cancel");
|
||||||
return;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
if response == create_response {
|
if response == create_response {
|
||||||
let device_config_name = entry.text().to_string();
|
let device_config_name = entry.text().to_string();
|
||||||
|
|
||||||
main_window.imp().state_manager.borrow_mut().create_new_device_config(
|
return Some(device_config_name);
|
||||||
device_config_name.clone(),
|
|
||||||
properties,
|
|
||||||
profiles,
|
|
||||||
selected_profile
|
|
||||||
);
|
|
||||||
|
|
||||||
main_window.add_device_config(&device_config_name);
|
|
||||||
subpages::device_config::switch_to_device_config_window(main_window, device_config_name);
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
unreachable!()
|
||||||
}
|
}
|
||||||
51
src/window/popups/new_device_config_type.rs
Normal file
51
src/window/popups/new_device_config_type.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use super::*;
|
||||||
|
use adw::ResponseAppearance;
|
||||||
|
use crate::window::popups;
|
||||||
|
|
||||||
|
pub(in crate::window) async fn new_device_config_type(main_window: &AudioDeviceManagerWindow) {
|
||||||
|
let cancel_response = "cancel";
|
||||||
|
let create_from_template_response = "create-from-template";
|
||||||
|
let create_new_response = "create-new";
|
||||||
|
|
||||||
|
// Create new dialog
|
||||||
|
let dialog = adw::AlertDialog::builder()
|
||||||
|
.heading("New Configuration")
|
||||||
|
.close_response(cancel_response)
|
||||||
|
.default_response(create_from_template_response)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
dialog.add_responses(&[
|
||||||
|
(cancel_response, "Cancel"),
|
||||||
|
(create_new_response, "Create From Scratch"),
|
||||||
|
(create_from_template_response, "Use Pipewire Device as Template"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Make the dialog button insensitive initially
|
||||||
|
dialog.set_response_appearance(create_from_template_response, ResponseAppearance::Suggested);
|
||||||
|
dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive);
|
||||||
|
|
||||||
|
let response = dialog.choose_future(main_window).await;
|
||||||
|
|
||||||
|
// Return if the user chose 'cancel_response'
|
||||||
|
if response == cancel_response {
|
||||||
|
println!("Cancel");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if response == create_from_template_response {
|
||||||
|
popups::select_pipewire_device::select_pipewire_device(main_window).await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if response == create_new_response {
|
||||||
|
popups::new_device_config::new_device_config(
|
||||||
|
main_window,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
Vec::new(),
|
||||||
|
None
|
||||||
|
).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,17 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::window::popups;
|
use crate::window::popups;
|
||||||
|
|
||||||
use adw::ResponseAppearance;
|
use adw::ResponseAppearance;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceManagerWindow) {
|
pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceManagerWindow) {
|
||||||
let device_config_names = main_window.imp().state_manager.borrow_mut().get_device_config_names();
|
let device_config_names = main_window.imp().state_manager.borrow_mut().get_device_config_names();
|
||||||
|
|
||||||
if device_config_names.is_empty() {
|
if device_config_names.is_empty() {
|
||||||
popups::new_device_config::new_device_config(main_window).await;
|
popups::new_device_config_type::new_device_config_type(main_window).await;
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,10 +35,10 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
|
|||||||
.css_classes(["boxed-list"])
|
.css_classes(["boxed-list"])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let selected_config_name = Rc::new(RefCell::new(String::new()));
|
let selected_config_id = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
let mut device_configs_list_len = 0;
|
let mut device_configs_list_len = 0;
|
||||||
for device_config_row in build_device_config_rows(main_window, &selected_config_name) {
|
for device_config_row in build_device_config_rows(main_window, &selected_config_id) {
|
||||||
device_configs_list_len += 1;
|
device_configs_list_len += 1;
|
||||||
device_configs_list.append(&device_config_row);
|
device_configs_list.append(&device_config_row);
|
||||||
}
|
}
|
||||||
@ -49,15 +49,18 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
|
|||||||
.child(&dialog_body)
|
.child(&dialog_body)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
println!("{}", device_configs_list.height());
|
||||||
|
|
||||||
|
dialog_body.append(&entry);
|
||||||
|
if device_configs_list_len > 3 {
|
||||||
let device_configs_list_scrollable = gtk::ScrolledWindow::builder()
|
let device_configs_list_scrollable = gtk::ScrolledWindow::builder()
|
||||||
.child(&device_configs_list)
|
.child(&device_configs_list)
|
||||||
.vexpand(true)
|
.vexpand(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
println!("{}", device_configs_list.height());
|
|
||||||
|
|
||||||
dialog_body.append(&entry);
|
|
||||||
dialog_body.append(&device_configs_list_scrollable);
|
dialog_body.append(&device_configs_list_scrollable);
|
||||||
|
} else {
|
||||||
|
dialog_body.append(&device_configs_list);
|
||||||
|
}
|
||||||
|
|
||||||
// Create new dialog
|
// Create new dialog
|
||||||
let dialog = adw::AlertDialog::builder()
|
let dialog = adw::AlertDialog::builder()
|
||||||
@ -111,26 +114,27 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
|
|||||||
}
|
}
|
||||||
|
|
||||||
if response == create_new_response {
|
if response == create_new_response {
|
||||||
popups::new_device_config::new_device_config(main_window).await;
|
popups::new_device_config_type::new_device_config_type(main_window).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected_row = device_configs_list.selected_row().unwrap();
|
let selected_row = device_configs_list.selected_row().unwrap();
|
||||||
|
|
||||||
let config_name = selected_config_name.borrow().clone();
|
let config_id = selected_config_id.borrow().clone().unwrap();
|
||||||
|
|
||||||
if response == duplicate_response {
|
if response == duplicate_response {
|
||||||
let mut state_manager = main_window.imp().state_manager.borrow_mut();
|
let mut state_manager = main_window.imp().state_manager.borrow_mut();
|
||||||
|
|
||||||
let properties = state_manager.get_device_config_properties(&config_name).clone();
|
let config_name = state_manager.get_device_config_name(&config_id);
|
||||||
let profiles = state_manager.get_device_config_profiles(&config_name).clone();
|
let properties = state_manager.get_device_config_properties(&config_id).clone();
|
||||||
let selected_profile = state_manager.get_device_config_selected_profile(&config_name).clone();
|
let profiles = state_manager.get_device_config_profiles(&config_id).clone();
|
||||||
|
let selected_profile = state_manager.get_device_config_selected_profile(&config_id).clone();
|
||||||
|
|
||||||
drop(state_manager);
|
drop(state_manager);
|
||||||
|
|
||||||
popups::new_device_config_name::new_device_config_name(
|
popups::new_device_config::new_device_config(
|
||||||
main_window,
|
main_window,
|
||||||
Some(selected_config_name.borrow().clone()),
|
Some(config_name),
|
||||||
properties,
|
properties,
|
||||||
profiles,
|
profiles,
|
||||||
selected_profile
|
selected_profile
|
||||||
@ -139,30 +143,34 @@ pub(in crate::window) async fn select_device_config(main_window: &AudioDeviceMan
|
|||||||
}
|
}
|
||||||
|
|
||||||
if response == select_response {
|
if response == select_response {
|
||||||
main_window.add_device_config(&config_name);
|
main_window.add_device_config(config_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_device_config_rows(
|
fn build_device_config_rows(
|
||||||
main_window: &AudioDeviceManagerWindow,
|
main_window: &AudioDeviceManagerWindow,
|
||||||
selected_config_name: &Rc<RefCell<String>>,
|
selected_config_id: &Rc<RefCell<Option<Uuid>>>,
|
||||||
) -> Vec<adw::ActionRow> {
|
) -> Vec<adw::ActionRow> {
|
||||||
let mut row_vec = Vec::new();
|
let mut row_vec = Vec::new();
|
||||||
|
|
||||||
let device_names = main_window.imp().state_manager.borrow_mut().get_device_config_names();
|
let mut device_names: Vec<_> = main_window.imp().state_manager.borrow_mut()
|
||||||
|
.get_device_config_names()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
device_names.sort_by(|(_, v1), (_, v2)| v1.cmp(v2));
|
||||||
|
|
||||||
for config_name in device_names {
|
for (config_id, config_name) in device_names {
|
||||||
let row = adw::ActionRow::builder()
|
let row = adw::ActionRow::builder()
|
||||||
.title(config_name.clone())
|
.title(config_name.clone())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
row.connect_activate(clone!(
|
row.connect_activate(clone!(
|
||||||
#[weak]
|
#[weak]
|
||||||
selected_config_name,
|
selected_config_id,
|
||||||
move |row| {
|
move |row| {
|
||||||
*selected_config_name.borrow_mut() = config_name.clone();
|
*selected_config_id.borrow_mut() = Some(config_id.clone());
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,6 @@ use std::collections::HashMap;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use adw::ResponseAppearance;
|
use adw::ResponseAppearance;
|
||||||
use crate::window::popups;
|
use crate::window::popups;
|
||||||
use crate::window::subpages::device_config;
|
|
||||||
pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceManagerWindow) {
|
pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceManagerWindow) {
|
||||||
|
|
||||||
let cancel_response = "cancel";
|
let cancel_response = "cancel";
|
||||||
@ -40,15 +39,20 @@ pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceM
|
|||||||
.child(&dialog_body)
|
.child(&dialog_body)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let device_list_scrollable = gtk::ScrolledWindow::builder()
|
|
||||||
.child(&device_list)
|
|
||||||
.vexpand(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
println!("{}", device_list.height());
|
println!("{}", device_list.height());
|
||||||
|
|
||||||
dialog_body.append(&entry);
|
dialog_body.append(&entry);
|
||||||
|
|
||||||
|
if device_list_len > 3 {
|
||||||
|
let device_list_scrollable = gtk::ScrolledWindow::builder()
|
||||||
|
.child(&device_list)
|
||||||
|
.vexpand(true)
|
||||||
|
.build();
|
||||||
dialog_body.append(&device_list_scrollable);
|
dialog_body.append(&device_list_scrollable);
|
||||||
|
} else {
|
||||||
|
dialog_body.append(&device_list);
|
||||||
|
}
|
||||||
|
|
||||||
// Create new dialog
|
// Create new dialog
|
||||||
let dialog = adw::AlertDialog::builder()
|
let dialog = adw::AlertDialog::builder()
|
||||||
@ -72,6 +76,8 @@ pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceM
|
|||||||
dialog.set_response_appearance(select_response, ResponseAppearance::Suggested);
|
dialog.set_response_appearance(select_response, ResponseAppearance::Suggested);
|
||||||
dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive);
|
dialog.set_response_appearance(cancel_response, ResponseAppearance::Destructive);
|
||||||
|
|
||||||
|
let device_name = main_window.imp().device_navigation_page.title();
|
||||||
|
|
||||||
device_list.connect_row_selected(clone!(
|
device_list.connect_row_selected(clone!(
|
||||||
#[weak]
|
#[weak]
|
||||||
dialog,
|
dialog,
|
||||||
@ -116,11 +122,11 @@ pub(in crate::window) async fn select_pipewire_device(main_window: &AudioDeviceM
|
|||||||
let mut identifying_properties = HashMap::new();
|
let mut identifying_properties = HashMap::new();
|
||||||
|
|
||||||
identifying_properties.insert(
|
identifying_properties.insert(
|
||||||
"device.name".to_string(),
|
"device.name".to_owned(),
|
||||||
device_properties.get("device.name").unwrap().clone()
|
device_properties.get("device.name").unwrap().clone()
|
||||||
);
|
);
|
||||||
|
|
||||||
popups::new_device_config_name::new_device_config_name(
|
popups::new_device_config::new_device_config(
|
||||||
main_window,
|
main_window,
|
||||||
Some(device_properties.get("device.description").unwrap().clone()),
|
Some(device_properties.get("device.description").unwrap().clone()),
|
||||||
identifying_properties,
|
identifying_properties,
|
||||||
@ -137,9 +143,12 @@ fn build_pipewire_device_rows(
|
|||||||
) -> Vec<adw::ActionRow> {
|
) -> Vec<adw::ActionRow> {
|
||||||
let mut row_vec = Vec::new();
|
let mut row_vec = Vec::new();
|
||||||
|
|
||||||
let device_names = main_window.imp().pipewire_manager.get_device_names();
|
let mut device_names: Vec<_> = main_window.imp().pipewire_manager
|
||||||
|
.get_device_names()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
println!("{:?}", device_names);
|
device_names.sort_by(|(_, v1), (_, v2)| v1.cmp(v2));
|
||||||
|
|
||||||
for (device_id, device_name) in device_names {
|
for (device_id, device_name) in device_names {
|
||||||
let row = adw::ActionRow::builder()
|
let row = adw::ActionRow::builder()
|
||||||
|
|||||||
@ -1,49 +1,62 @@
|
|||||||
use adw::gdk::pango::EllipsizeMode;
|
use adw::gdk::pango::EllipsizeMode;
|
||||||
use crate::window::AudioDeviceManagerWindow;
|
use crate::window::{popups, AudioDeviceManagerWindow};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use adw::glib::clone;
|
use adw::glib::clone;
|
||||||
use gtk::{glib, gio};
|
use gtk::{glib, gio};
|
||||||
use crate::state_manager::structs::DeviceConfig;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub(in crate::window) fn switch_to_device_config_window(main_window: &AudioDeviceManagerWindow, device_config_name: String) {
|
pub(in crate::window) fn switch_to_device_config_window(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) {
|
||||||
let device_config_window = build_device_config_window(main_window, device_config_name);
|
let device_config_window = build_device_config_window(main_window, device_config_id);
|
||||||
|
|
||||||
main_window.imp().split_view.set_content(Some(&device_config_window));
|
main_window.imp().split_view.set_content(Some(&device_config_window));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_device_config_window(main_window: &AudioDeviceManagerWindow, device_config_name: String) -> adw::NavigationPage {
|
fn build_device_config_window(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> adw::NavigationPage {
|
||||||
|
|
||||||
let (navigation_page, clamp) = build_page_base(main_window, device_config_name);
|
let (navigation_page, preferences_page) = page_base::build_page_base(main_window, device_config_id);
|
||||||
|
|
||||||
|
preferences_page.add(
|
||||||
|
&profile_selection::build_profile_selection(main_window, device_config_id),
|
||||||
|
);
|
||||||
|
|
||||||
navigation_page
|
navigation_page
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
mod page_base {
|
||||||
|
use super::*;
|
||||||
|
pub(super) fn build_page_base(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> (adw::NavigationPage, adw::PreferencesPage) {
|
||||||
|
let preferences_page = adw::PreferencesPage::builder()
|
||||||
|
.build();
|
||||||
|
|
||||||
[top]
|
let clamp = adw::Clamp::builder()
|
||||||
Adw.HeaderBar {
|
.child(&preferences_page)
|
||||||
show-title: true;
|
.maximum_size(640)
|
||||||
[end]
|
.build();
|
||||||
MenuButton {
|
|
||||||
primary: true;
|
let scrolled_window = gtk::ScrolledWindow::builder()
|
||||||
icon-name: "open-menu-symbolic";
|
.child(&clamp)
|
||||||
tooltip-text: _("Main Menu");
|
.build();
|
||||||
menu-model: primary_menu;
|
|
||||||
}
|
|
||||||
|
let toolbar_view = adw::ToolbarView::builder()
|
||||||
|
.content(&scrolled_window)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
let header_bar = build_header_bar(main_window, device_config_id);
|
||||||
|
|
||||||
|
toolbar_view.add_top_bar(&header_bar);
|
||||||
|
|
||||||
|
let navigation_page = adw::NavigationPage::builder()
|
||||||
|
.child(&toolbar_view)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
(navigation_page, preferences_page)
|
||||||
}
|
}
|
||||||
|
|
||||||
content: Gtk.ScrolledWindow {
|
fn build_header_bar(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> adw::HeaderBar {
|
||||||
child: Adw.Clamp device_page_clamp {
|
|
||||||
maximum-size: 640;
|
|
||||||
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
fn build_page_base(main_window: &AudioDeviceManagerWindow, device_name: String) -> (adw::NavigationPage, adw::Clamp) {
|
|
||||||
let back_button= gtk::Button::builder()
|
let back_button= gtk::Button::builder()
|
||||||
.icon_name("go-previous")
|
.icon_name("go-previous")
|
||||||
.build();
|
.build();
|
||||||
@ -60,40 +73,89 @@ fn build_page_base(main_window: &AudioDeviceManagerWindow, device_name: String)
|
|||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
let clamp = adw::Clamp::builder()
|
let header_bar = adw::HeaderBar::builder()
|
||||||
.maximum_size(640)
|
.show_title(true)
|
||||||
.build();
|
|
||||||
|
|
||||||
let scrolled_window = gtk::ScrolledWindow::builder()
|
|
||||||
.child(&clamp)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let header_bar = gtk::HeaderBar::builder()
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
header_bar.pack_start(&back_button);
|
header_bar.pack_start(&back_button);
|
||||||
|
|
||||||
|
let device_config_name = main_window.imp().state_manager.borrow().get_device_config_name(&device_config_id);
|
||||||
|
|
||||||
let title_label = gtk::Label::builder()
|
let title_label = gtk::Label::builder()
|
||||||
.label(&device_name)
|
.label(&device_config_name)
|
||||||
.single_line_mode(true)
|
.single_line_mode(true)
|
||||||
.ellipsize(EllipsizeMode::End)
|
.ellipsize(EllipsizeMode::End)
|
||||||
.width_chars(5)
|
.width_chars(5)
|
||||||
.css_classes(["title"])
|
.css_classes(["title"])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
header_bar.set_title_widget(Some(&title_label));
|
let title_edit_button = gtk::Button::builder()
|
||||||
|
.icon_name("edit")
|
||||||
let toolbar_view = adw::ToolbarView::builder()
|
|
||||||
.content(&scrolled_window)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
toolbar_view.add_top_bar(&header_bar);
|
title_edit_button.connect_clicked(clone!(
|
||||||
|
#[weak]
|
||||||
|
title_label,
|
||||||
|
#[weak]
|
||||||
|
main_window,
|
||||||
|
move |_| {
|
||||||
|
glib::spawn_future_local(clone!(
|
||||||
|
#[weak]
|
||||||
|
main_window,
|
||||||
|
#[weak]
|
||||||
|
title_label,
|
||||||
|
async move {
|
||||||
|
let Some(new_device_name) = popups::change_device_config_name::change_device_config_name(
|
||||||
|
&main_window,
|
||||||
|
device_config_id
|
||||||
|
).await else { return };
|
||||||
|
|
||||||
let navigation_page = adw::NavigationPage::builder()
|
title_label.set_label(&new_device_name)
|
||||||
.child(&toolbar_view)
|
}
|
||||||
.build();
|
));
|
||||||
|
}
|
||||||
println!("device name: {}", device_name);
|
));
|
||||||
|
|
||||||
(navigation_page, clamp)
|
let title = gtk::Box::builder()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
title.append(&title_label);
|
||||||
|
title.append(&title_edit_button);
|
||||||
|
|
||||||
|
header_bar.set_title_widget(Some(&title));
|
||||||
|
|
||||||
|
header_bar
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mod profile_selection {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(super) fn build_profile_selection(main_window: &AudioDeviceManagerWindow, device_config_id: Uuid) -> adw::PreferencesGroup {
|
||||||
|
|
||||||
|
let preferences_group = adw::PreferencesGroup::builder()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let combo_row = adw::ComboRow::builder()
|
||||||
|
.title("Profile")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let profile_dropdown = gtk::DropDown::builder()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let properties = main_window.imp().state_manager.borrow().get_device_config_properties(&device_config_id).clone();
|
||||||
|
|
||||||
|
let device_ids = main_window.imp().pipewire_manager.get_device_ids_from_properties(properties);
|
||||||
|
|
||||||
|
if device_ids.len() == 1 {
|
||||||
|
let id = device_ids[0];
|
||||||
|
|
||||||
|
main_window.imp().pipewire_manager.register_profile_combo_row(id, combo_row.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences_group.add(&combo_row);
|
||||||
|
|
||||||
|
preferences_group
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user