mirror of
https://gitlab.freedesktop.org/pipewire/helvum
synced 2026-03-15 03:26:10 +08:00
Color links and ports according to their formats
Format params for links and ports are now being watched for in the pipewire connection code. The parsed media type is then set on the port widget / link object and they are colored accordingly. For ports, which were already colored before, this new method of determining the media type should be more reliable and accurate as this uses the real Format/EnumFormat params instead of parsing optional properties.
This commit is contained in:
@@ -55,10 +55,14 @@ mod imp {
|
|||||||
@weak self as imp => @default-return glib::ControlFlow::Continue,
|
@weak self as imp => @default-return glib::ControlFlow::Continue,
|
||||||
move |msg| {
|
move |msg| {
|
||||||
match msg {
|
match msg {
|
||||||
PipewireMessage::NodeAdded{ id, name, node_type } => imp.add_node(id, name.as_str(), node_type),
|
PipewireMessage::NodeAdded { id, name, node_type } => imp.add_node(id, name.as_str(), node_type),
|
||||||
PipewireMessage::PortAdded{ id, node_id, name, direction, media_type } => imp.add_port(id, name.as_str(), node_id, direction, media_type),
|
PipewireMessage::PortAdded { id, node_id, name, direction } => imp.add_port(id, name.as_str(), node_id, direction),
|
||||||
PipewireMessage::LinkAdded{ id, port_from, port_to, active} => imp.add_link(id, port_from, port_to, active),
|
PipewireMessage::PortFormatChanged { id, media_type } => imp.port_media_type_changed(id, media_type),
|
||||||
|
PipewireMessage::LinkAdded {
|
||||||
|
id, port_from, port_to, active, media_type
|
||||||
|
} => imp.add_link(id, port_from, port_to, active, media_type),
|
||||||
PipewireMessage::LinkStateChanged { id, active } => imp.link_state_changed(id, active),
|
PipewireMessage::LinkStateChanged { id, active } => imp.link_state_changed(id, active),
|
||||||
|
PipewireMessage::LinkFormatChanged { id, media_type } => imp.link_format_changed(id, media_type),
|
||||||
PipewireMessage::NodeRemoved { id } => imp.remove_node(id),
|
PipewireMessage::NodeRemoved { id } => imp.remove_node(id),
|
||||||
PipewireMessage::PortRemoved { id, node_id } => imp.remove_port(id, node_id),
|
PipewireMessage::PortRemoved { id, node_id } => imp.remove_port(id, node_id),
|
||||||
PipewireMessage::LinkRemoved { id } => imp.remove_link(id)
|
PipewireMessage::LinkRemoved { id } => imp.remove_link(id)
|
||||||
@@ -96,14 +100,7 @@ mod imp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new port to the view.
|
/// Add a new port to the view.
|
||||||
fn add_port(
|
fn add_port(&self, id: u32, name: &str, node_id: u32, direction: pipewire::spa::Direction) {
|
||||||
&self,
|
|
||||||
id: u32,
|
|
||||||
name: &str,
|
|
||||||
node_id: u32,
|
|
||||||
direction: pipewire::spa::Direction,
|
|
||||||
media_type: Option<MediaType>,
|
|
||||||
) {
|
|
||||||
log::info!("Adding port to graph: id {}", id);
|
log::info!("Adding port to graph: id {}", id);
|
||||||
|
|
||||||
let mut items = self.items.borrow_mut();
|
let mut items = self.items.borrow_mut();
|
||||||
@@ -117,7 +114,7 @@ mod imp {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let port = graph::Port::new(id, name, direction, media_type);
|
let port = graph::Port::new(id, name, direction);
|
||||||
|
|
||||||
// Create or delete a link if the widget emits the "port-toggled" signal.
|
// Create or delete a link if the widget emits the "port-toggled" signal.
|
||||||
port.connect_local(
|
port.connect_local(
|
||||||
@@ -139,6 +136,21 @@ mod imp {
|
|||||||
node.add_port(port);
|
node.add_port(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn port_media_type_changed(&self, id: u32, media_type: MediaType) {
|
||||||
|
let items = self.items.borrow();
|
||||||
|
|
||||||
|
let Some(port) = items.get(&id) else {
|
||||||
|
log::warn!("Port (id: {id}) for changed media type not found in graph manager");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(port) = port.dynamic_cast_ref::<graph::Port>() else {
|
||||||
|
log::warn!("Graph Manager item under port id {id} is not a port");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
port.set_media_type(media_type.as_raw())
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove the port with the id `id` from the node with the id `node_id`
|
/// Remove the port with the id `id` from the node with the id `node_id`
|
||||||
/// from the view.
|
/// from the view.
|
||||||
fn remove_port(&self, id: u32, node_id: u32) {
|
fn remove_port(&self, id: u32, node_id: u32) {
|
||||||
@@ -167,7 +179,14 @@ mod imp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new link to the view.
|
/// Add a new link to the view.
|
||||||
fn add_link(&self, id: u32, output_port_id: u32, input_port_id: u32, active: bool) {
|
fn add_link(
|
||||||
|
&self,
|
||||||
|
id: u32,
|
||||||
|
output_port_id: u32,
|
||||||
|
input_port_id: u32,
|
||||||
|
active: bool,
|
||||||
|
media_type: MediaType,
|
||||||
|
) {
|
||||||
log::info!("Adding link to graph: id {}", id);
|
log::info!("Adding link to graph: id {}", id);
|
||||||
|
|
||||||
let mut items = self.items.borrow_mut();
|
let mut items = self.items.borrow_mut();
|
||||||
@@ -193,6 +212,7 @@ mod imp {
|
|||||||
link.set_output_port(Some(&output_port));
|
link.set_output_port(Some(&output_port));
|
||||||
link.set_input_port(Some(&input_port));
|
link.set_input_port(Some(&input_port));
|
||||||
link.set_active(active);
|
link.set_active(active);
|
||||||
|
link.set_media_type(media_type);
|
||||||
|
|
||||||
items.insert(id, link.clone().upcast());
|
items.insert(id, link.clone().upcast());
|
||||||
|
|
||||||
@@ -223,6 +243,20 @@ mod imp {
|
|||||||
link.set_active(active);
|
link.set_active(active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn link_format_changed(&self, id: u32, media_type: pipewire::spa::format::MediaType) {
|
||||||
|
let items = self.items.borrow();
|
||||||
|
|
||||||
|
let Some(link) = items.get(&id) else {
|
||||||
|
log::warn!("Link (id: {id}) for changed media type not found in graph manager");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(link) = link.dynamic_cast_ref::<graph::Link>() else {
|
||||||
|
log::warn!("Graph Manager item under link id {id} is not a link");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
link.set_media_type(media_type);
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle a link between the two specified ports on the remote pipewire server.
|
// Toggle a link between the two specified ports on the remote pipewire server.
|
||||||
fn toggle_link(&self, port_from: u32, port_to: u32) {
|
fn toggle_link(&self, port_from: u32, port_to: u32) {
|
||||||
let sender = self.pw_sender.get().expect("pw_sender shoud be set");
|
let sender = self.pw_sender.get().expect("pw_sender shoud be set");
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@@ -20,7 +20,7 @@ mod pipewire_connection;
|
|||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use pipewire::spa::Direction;
|
use pipewire::spa::{format::MediaType, Direction};
|
||||||
|
|
||||||
/// Messages sent by the GTK thread to notify the pipewire thread.
|
/// Messages sent by the GTK thread to notify the pipewire thread.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -44,18 +44,26 @@ pub enum PipewireMessage {
|
|||||||
node_id: u32,
|
node_id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
media_type: Option<MediaType>,
|
},
|
||||||
|
PortFormatChanged {
|
||||||
|
id: u32,
|
||||||
|
media_type: MediaType,
|
||||||
},
|
},
|
||||||
LinkAdded {
|
LinkAdded {
|
||||||
id: u32,
|
id: u32,
|
||||||
port_from: u32,
|
port_from: u32,
|
||||||
port_to: u32,
|
port_to: u32,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
media_type: MediaType,
|
||||||
},
|
},
|
||||||
LinkStateChanged {
|
LinkStateChanged {
|
||||||
id: u32,
|
id: u32,
|
||||||
active: bool,
|
active: bool,
|
||||||
},
|
},
|
||||||
|
LinkFormatChanged {
|
||||||
|
id: u32,
|
||||||
|
media_type: MediaType,
|
||||||
|
},
|
||||||
NodeRemoved {
|
NodeRemoved {
|
||||||
id: u32,
|
id: u32,
|
||||||
},
|
},
|
||||||
@@ -74,13 +82,6 @@ pub enum NodeType {
|
|||||||
Output,
|
Output,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum MediaType {
|
|
||||||
Audio,
|
|
||||||
Video,
|
|
||||||
Midi,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PipewireLink {
|
pub struct PipewireLink {
|
||||||
pub node_from: u32,
|
pub node_from: u32,
|
||||||
|
|||||||
@@ -21,11 +21,15 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
|||||||
use gtk::glib::{self, clone};
|
use gtk::glib::{self, clone};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use pipewire::{
|
use pipewire::{
|
||||||
link::{Link, LinkChangeMask, LinkListener, LinkState},
|
link::{Link, LinkChangeMask, LinkInfo, LinkListener, LinkState},
|
||||||
|
port::{Port, PortChangeMask, PortInfo, PortListener},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
properties,
|
properties,
|
||||||
registry::{GlobalObject, Registry},
|
registry::{GlobalObject, Registry},
|
||||||
spa::{Direction, ForeignDict},
|
spa::{
|
||||||
|
param::{ParamInfoFlags, ParamType},
|
||||||
|
ForeignDict,
|
||||||
|
},
|
||||||
types::ObjectType,
|
types::ObjectType,
|
||||||
Context, Core, MainLoop,
|
Context, Core, MainLoop,
|
||||||
};
|
};
|
||||||
@@ -34,6 +38,10 @@ use crate::{GtkMessage, MediaType, NodeType, PipewireMessage};
|
|||||||
use state::{Item, State};
|
use state::{Item, State};
|
||||||
|
|
||||||
enum ProxyItem {
|
enum ProxyItem {
|
||||||
|
Port {
|
||||||
|
proxy: Port,
|
||||||
|
_listener: PortListener,
|
||||||
|
},
|
||||||
Link {
|
Link {
|
||||||
_proxy: Link,
|
_proxy: Link,
|
||||||
_listener: LinkListener,
|
_listener: LinkListener,
|
||||||
@@ -67,7 +75,7 @@ pub(super) fn thread_main(
|
|||||||
.global(clone!(@strong gtk_sender, @weak registry, @strong proxies, @strong state =>
|
.global(clone!(@strong gtk_sender, @weak registry, @strong proxies, @strong state =>
|
||||||
move |global| match global.type_ {
|
move |global| match global.type_ {
|
||||||
ObjectType::Node => handle_node(global, >k_sender, &state),
|
ObjectType::Node => handle_node(global, >k_sender, &state),
|
||||||
ObjectType::Port => handle_port(global, >k_sender, &state),
|
ObjectType::Port => handle_port(global, >k_sender, ®istry, &proxies, &state),
|
||||||
ObjectType::Link => handle_link(global, >k_sender, ®istry, &proxies, &state),
|
ObjectType::Link => handle_link(global, >k_sender, ®istry, &proxies, &state),
|
||||||
_ => {
|
_ => {
|
||||||
// Other objects are not interesting to us
|
// Other objects are not interesting to us
|
||||||
@@ -115,19 +123,6 @@ fn handle_node(
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// FIXME: Instead of checking these props, the "EnumFormat" parameter should be checked instead.
|
|
||||||
let media_type = props.get("media.class").and_then(|class| {
|
|
||||||
if class.contains("Audio") {
|
|
||||||
Some(MediaType::Audio)
|
|
||||||
} else if class.contains("Video") {
|
|
||||||
Some(MediaType::Video)
|
|
||||||
} else if class.contains("Midi") {
|
|
||||||
Some(MediaType::Midi)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let media_class = |class: &str| {
|
let media_class = |class: &str| {
|
||||||
if class.contains("Sink") || class.contains("Input") {
|
if class.contains("Sink") || class.contains("Input") {
|
||||||
Some(NodeType::Input)
|
Some(NodeType::Input)
|
||||||
@@ -149,13 +144,7 @@ fn handle_node(
|
|||||||
})
|
})
|
||||||
.or_else(|| props.get("media.class").and_then(media_class));
|
.or_else(|| props.get("media.class").and_then(media_class));
|
||||||
|
|
||||||
state.borrow_mut().insert(
|
state.borrow_mut().insert(node.id, Item::Node);
|
||||||
node.id,
|
|
||||||
Item::Node {
|
|
||||||
// widget: node_widget,
|
|
||||||
media_type,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
sender
|
sender
|
||||||
.send(PipewireMessage::NodeAdded {
|
.send(PipewireMessage::NodeAdded {
|
||||||
@@ -170,44 +159,106 @@ fn handle_node(
|
|||||||
fn handle_port(
|
fn handle_port(
|
||||||
port: &GlobalObject<ForeignDict>,
|
port: &GlobalObject<ForeignDict>,
|
||||||
sender: &glib::Sender<PipewireMessage>,
|
sender: &glib::Sender<PipewireMessage>,
|
||||||
|
registry: &Rc<Registry>,
|
||||||
|
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
|
||||||
state: &Rc<RefCell<State>>,
|
state: &Rc<RefCell<State>>,
|
||||||
) {
|
) {
|
||||||
let props = port
|
let port_id = port.id;
|
||||||
.props
|
let proxy: Port = registry.bind(port).expect("Failed to bind to port proxy");
|
||||||
.as_ref()
|
let listener = proxy
|
||||||
.expect("Port object is missing properties");
|
.add_listener_local()
|
||||||
let name = props.get("port.name").unwrap_or_default().to_string();
|
.info(
|
||||||
let node_id: u32 = props
|
clone!(@strong proxies, @strong state, @strong sender => move |info| {
|
||||||
.get("node.id")
|
handle_port_info(info, &proxies, &state, &sender);
|
||||||
.expect("Port has no node.id property!")
|
}),
|
||||||
.parse()
|
)
|
||||||
.expect("Could not parse node.id property");
|
.param(clone!(@strong sender => move |_, param_id, _, _, param| {
|
||||||
let direction = if matches!(props.get("port.direction"), Some("in")) {
|
if param_id == ParamType::EnumFormat {
|
||||||
Direction::Input
|
handle_port_enum_format(port_id, param, &sender)
|
||||||
} else {
|
}
|
||||||
Direction::Output
|
}))
|
||||||
|
.register();
|
||||||
|
|
||||||
|
proxies.borrow_mut().insert(
|
||||||
|
port.id,
|
||||||
|
ProxyItem::Port {
|
||||||
|
proxy,
|
||||||
|
_listener: listener,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_port_info(
|
||||||
|
info: &PortInfo,
|
||||||
|
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
|
||||||
|
state: &Rc<RefCell<State>>,
|
||||||
|
sender: &glib::Sender<PipewireMessage>,
|
||||||
|
) {
|
||||||
|
debug!("Received port info: {:?}", info);
|
||||||
|
|
||||||
|
let id = info.id();
|
||||||
|
let proxies = proxies.borrow();
|
||||||
|
let Some(ProxyItem::Port { proxy, .. }) = proxies.get(&id) else {
|
||||||
|
log::error!("Received info on unknown port with id {id}");
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find out the nodes media type so that the port can be colored.
|
let mut state = state.borrow_mut();
|
||||||
let media_type = if let Some(Item::Node { media_type, .. }) = state.borrow().get(node_id) {
|
|
||||||
media_type.to_owned()
|
|
||||||
} else {
|
|
||||||
warn!("Node not found for Port {}", port.id);
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save node_id so we can delete this port easily.
|
if let Some(Item::Port { .. }) = state.get(id) {
|
||||||
state.borrow_mut().insert(port.id, Item::Port { node_id });
|
// Info was an update, figure out if we should notify the GTK thread
|
||||||
|
if info.change_mask().contains(PortChangeMask::PARAMS) {
|
||||||
|
// TODO: React to param changes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First time we get info. We can now notify the gtk thread of a new link.
|
||||||
|
let props = info.props().expect("Port object is missing properties");
|
||||||
|
let name = props.get("port.name").unwrap_or_default().to_string();
|
||||||
|
let node_id: u32 = props
|
||||||
|
.get("node.id")
|
||||||
|
.expect("Port has no node.id property!")
|
||||||
|
.parse()
|
||||||
|
.expect("Could not parse node.id property");
|
||||||
|
|
||||||
|
state.insert(id, Item::Port { node_id });
|
||||||
|
|
||||||
|
let params = info.params();
|
||||||
|
let enum_format_info = params
|
||||||
|
.iter()
|
||||||
|
.find(|param| param.id() == ParamType::EnumFormat);
|
||||||
|
if let Some(enum_format_info) = enum_format_info {
|
||||||
|
if enum_format_info.flags().contains(ParamInfoFlags::READ) {
|
||||||
|
proxy.enum_params(0, Some(ParamType::EnumFormat), 0, u32::MAX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sender
|
||||||
|
.send(PipewireMessage::PortAdded {
|
||||||
|
id,
|
||||||
|
node_id,
|
||||||
|
name,
|
||||||
|
direction: info.direction(),
|
||||||
|
})
|
||||||
|
.expect("Failed to send message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_port_enum_format(
|
||||||
|
port_id: u32,
|
||||||
|
param: Option<&pipewire::spa::pod::Pod>,
|
||||||
|
sender: &glib::Sender<PipewireMessage>,
|
||||||
|
) {
|
||||||
|
let media_type = param
|
||||||
|
.and_then(|param| pipewire::spa::param::format_utils::parse_format(param).ok())
|
||||||
|
.map(|(media_type, _media_subtype)| media_type)
|
||||||
|
.unwrap_or(MediaType::Unknown);
|
||||||
|
|
||||||
sender
|
sender
|
||||||
.send(PipewireMessage::PortAdded {
|
.send(PipewireMessage::PortFormatChanged {
|
||||||
id: port.id,
|
id: port_id,
|
||||||
node_id,
|
|
||||||
name,
|
|
||||||
direction,
|
|
||||||
media_type,
|
media_type,
|
||||||
})
|
})
|
||||||
.expect("Failed to send message");
|
.expect("Failed to send message")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a new link being added
|
/// Handle a new link being added
|
||||||
@@ -227,38 +278,7 @@ fn handle_link(
|
|||||||
let listener = proxy
|
let listener = proxy
|
||||||
.add_listener_local()
|
.add_listener_local()
|
||||||
.info(clone!(@strong state, @strong sender => move |info| {
|
.info(clone!(@strong state, @strong sender => move |info| {
|
||||||
debug!("Received link info: {:?}", info);
|
handle_link_info(info, &state, &sender);
|
||||||
|
|
||||||
let id = info.id();
|
|
||||||
|
|
||||||
let mut state = state.borrow_mut();
|
|
||||||
if let Some(Item::Link { .. }) = state.get(id) {
|
|
||||||
// Info was an update - figure out if we should notify the gtk thread
|
|
||||||
if info.change_mask().contains(LinkChangeMask::STATE) {
|
|
||||||
sender.send(PipewireMessage::LinkStateChanged {
|
|
||||||
id,
|
|
||||||
active: matches!(info.state(), LinkState::Active)
|
|
||||||
}).expect("Failed to send message");
|
|
||||||
}
|
|
||||||
// TODO -- check other values that might have changed
|
|
||||||
} else {
|
|
||||||
// First time we get info. We can now notify the gtk thread of a new link.
|
|
||||||
let port_from = info.output_port_id();
|
|
||||||
let port_to = info.input_port_id();
|
|
||||||
|
|
||||||
state.insert(id, Item::Link {
|
|
||||||
port_from, port_to
|
|
||||||
});
|
|
||||||
|
|
||||||
sender.send(PipewireMessage::LinkAdded {
|
|
||||||
id,
|
|
||||||
port_from,
|
|
||||||
port_to,
|
|
||||||
active: matches!(info.state(), LinkState::Active)
|
|
||||||
}).expect(
|
|
||||||
"Failed to send message"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
.register();
|
.register();
|
||||||
|
|
||||||
@@ -271,6 +291,53 @@ fn handle_link(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_link_info(
|
||||||
|
info: &LinkInfo,
|
||||||
|
state: &Rc<RefCell<State>>,
|
||||||
|
sender: &glib::Sender<PipewireMessage>,
|
||||||
|
) {
|
||||||
|
debug!("Received link info: {:?}", info);
|
||||||
|
|
||||||
|
let id = info.id();
|
||||||
|
|
||||||
|
let mut state = state.borrow_mut();
|
||||||
|
if let Some(Item::Link { .. }) = state.get(id) {
|
||||||
|
// Info was an update - figure out if we should notify the gtk thread
|
||||||
|
if info.change_mask().contains(LinkChangeMask::STATE) {
|
||||||
|
sender
|
||||||
|
.send(PipewireMessage::LinkStateChanged {
|
||||||
|
id,
|
||||||
|
active: matches!(info.state(), LinkState::Active),
|
||||||
|
})
|
||||||
|
.expect("Failed to send message");
|
||||||
|
}
|
||||||
|
if info.change_mask().contains(LinkChangeMask::FORMAT) {
|
||||||
|
sender
|
||||||
|
.send(PipewireMessage::LinkFormatChanged {
|
||||||
|
id,
|
||||||
|
media_type: get_link_media_type(info),
|
||||||
|
})
|
||||||
|
.expect("Failed to send message");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First time we get info. We can now notify the gtk thread of a new link.
|
||||||
|
let port_from = info.output_port_id();
|
||||||
|
let port_to = info.input_port_id();
|
||||||
|
|
||||||
|
state.insert(id, Item::Link { port_from, port_to });
|
||||||
|
|
||||||
|
sender
|
||||||
|
.send(PipewireMessage::LinkAdded {
|
||||||
|
id,
|
||||||
|
port_from,
|
||||||
|
port_to,
|
||||||
|
active: matches!(info.state(), LinkState::Active),
|
||||||
|
media_type: get_link_media_type(info),
|
||||||
|
})
|
||||||
|
.expect("Failed to send message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle a link between the two specified ports.
|
/// Toggle a link between the two specified ports.
|
||||||
fn toggle_link(
|
fn toggle_link(
|
||||||
port_from: u32,
|
port_from: u32,
|
||||||
@@ -312,3 +379,13 @@ fn toggle_link(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_link_media_type(link_info: &LinkInfo) -> MediaType {
|
||||||
|
let media_type = link_info
|
||||||
|
.format()
|
||||||
|
.and_then(|format| pipewire::spa::param::format_utils::parse_format(format).ok())
|
||||||
|
.map(|(media_type, _media_subtype)| media_type)
|
||||||
|
.unwrap_or(MediaType::Unknown);
|
||||||
|
|
||||||
|
media_type
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,15 +16,10 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::MediaType;
|
|
||||||
|
|
||||||
/// Any pipewire item we need to keep track of.
|
/// Any pipewire item we need to keep track of.
|
||||||
/// These will be saved in the `State` struct associated with their id.
|
/// These will be saved in the `State` struct associated with their id.
|
||||||
pub(super) enum Item {
|
pub(super) enum Item {
|
||||||
Node {
|
Node,
|
||||||
// Keep track of the nodes media type to color ports on it.
|
|
||||||
media_type: Option<MediaType>,
|
|
||||||
},
|
|
||||||
Port {
|
Port {
|
||||||
// Save the id of the node this is on so we can remove the port from it
|
// Save the id of the node this is on so we can remove the port from it
|
||||||
// when it is deleted.
|
// when it is deleted.
|
||||||
|
|||||||
@@ -15,23 +15,23 @@
|
|||||||
SPDX-License-Identifier: GPL-3.0-only
|
SPDX-License-Identifier: GPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@define-color audio rgb(50,100,240);
|
@define-color media-type-audio rgb( 50, 100, 240);
|
||||||
@define-color video rgb(200,200,0);
|
@define-color media-type-video rgb(200, 200, 0);
|
||||||
@define-color midi rgb(200,0,50);
|
@define-color media-type-midi rgb(200, 0, 50);
|
||||||
@define-color graphview-link #808080;
|
@define-color media-type-unknown rgb(128, 128, 128);
|
||||||
|
|
||||||
.audio {
|
.audio {
|
||||||
background: @audio;
|
background: @media-type-audio;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video {
|
.video {
|
||||||
background: @video;
|
background: @media-type-video;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.midi {
|
.midi {
|
||||||
background: @midi;
|
background: @media-type-midi;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,27 @@ mod imp {
|
|||||||
};
|
};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use pipewire::spa::format::MediaType;
|
||||||
use pipewire::spa::Direction;
|
use pipewire::spa::Direction;
|
||||||
|
|
||||||
|
pub struct Colors {
|
||||||
|
audio: gdk::RGBA,
|
||||||
|
video: gdk::RGBA,
|
||||||
|
midi: gdk::RGBA,
|
||||||
|
unknown: gdk::RGBA,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Colors {
|
||||||
|
pub fn color_for_media_type(&self, media_type: MediaType) -> &gdk::RGBA {
|
||||||
|
match media_type {
|
||||||
|
MediaType::Audio => &self.audio,
|
||||||
|
MediaType::Video => &self.video,
|
||||||
|
MediaType::Stream | MediaType::Application => &self.midi,
|
||||||
|
_ => &self.unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DragState {
|
pub struct DragState {
|
||||||
node: glib::WeakRef<Node>,
|
node: glib::WeakRef<Node>,
|
||||||
/// This stores the offset of the pointer to the origin of the node,
|
/// This stores the offset of the pointer to the origin of the node,
|
||||||
@@ -510,6 +529,7 @@ mod imp {
|
|||||||
output_anchor: &Point,
|
output_anchor: &Point,
|
||||||
input_anchor: &Point,
|
input_anchor: &Point,
|
||||||
active: bool,
|
active: bool,
|
||||||
|
color: &gdk::RGBA,
|
||||||
) {
|
) {
|
||||||
let output_x: f64 = output_anchor.x().into();
|
let output_x: f64 = output_anchor.x().into();
|
||||||
let output_y: f64 = output_anchor.y().into();
|
let output_y: f64 = output_anchor.y().into();
|
||||||
@@ -523,6 +543,13 @@ mod imp {
|
|||||||
link_cr.set_dash(&[10.0, 5.0], 0.0);
|
link_cr.set_dash(&[10.0, 5.0], 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
link_cr.set_source_rgba(
|
||||||
|
color.red().into(),
|
||||||
|
color.green().into(),
|
||||||
|
color.blue().into(),
|
||||||
|
color.alpha().into(),
|
||||||
|
);
|
||||||
|
|
||||||
// If the output port is farther right than the input port and they have
|
// If the output port is farther right than the input port and they have
|
||||||
// a similar y coordinate, apply a y offset to the control points
|
// a similar y coordinate, apply a y offset to the control points
|
||||||
// so that the curve sticks out a bit.
|
// so that the curve sticks out a bit.
|
||||||
@@ -551,7 +578,7 @@ mod imp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_dragged_link(&self, port: &Port, link_cr: &cairo::Context) {
|
fn draw_dragged_link(&self, port: &Port, link_cr: &cairo::Context, colors: &Colors) {
|
||||||
let Some(port_anchor) = port.compute_point(&*self.obj(), &port.link_anchor()) else {
|
let Some(port_anchor) = port.compute_point(&*self.obj(), &port.link_anchor()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -579,7 +606,9 @@ mod imp {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.draw_link(link_cr, output_anchor, input_anchor, false);
|
let color = &colors.color_for_media_type(MediaType::from_raw(port.media_type()));
|
||||||
|
|
||||||
|
self.draw_link(link_cr, output_anchor, input_anchor, false, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_links(&self, widget: &super::GraphView, snapshot: >k::Snapshot) {
|
fn snapshot_links(&self, widget: &super::GraphView, snapshot: >k::Snapshot) {
|
||||||
@@ -594,30 +623,45 @@ mod imp {
|
|||||||
|
|
||||||
link_cr.set_line_width(2.0 * self.zoom_factor.get());
|
link_cr.set_line_width(2.0 * self.zoom_factor.get());
|
||||||
|
|
||||||
let rgba = widget
|
let colors = Colors {
|
||||||
.style_context()
|
audio: widget
|
||||||
.lookup_color("graphview-link")
|
.style_context()
|
||||||
.unwrap_or(gtk::gdk::RGBA::BLACK);
|
.lookup_color("media-type-audio")
|
||||||
|
.expect("color not found"),
|
||||||
link_cr.set_source_rgba(
|
video: widget
|
||||||
rgba.red().into(),
|
.style_context()
|
||||||
rgba.green().into(),
|
.lookup_color("media-type-video")
|
||||||
rgba.blue().into(),
|
.expect("color not found"),
|
||||||
rgba.alpha().into(),
|
midi: widget
|
||||||
);
|
.style_context()
|
||||||
|
.lookup_color("media-type-midi")
|
||||||
|
.expect("color not found"),
|
||||||
|
unknown: widget
|
||||||
|
.style_context()
|
||||||
|
.lookup_color("media-type-unknown")
|
||||||
|
.expect("color not found"),
|
||||||
|
};
|
||||||
|
|
||||||
for link in self.links.borrow().iter() {
|
for link in self.links.borrow().iter() {
|
||||||
|
let color = &colors.color_for_media_type(link.media_type());
|
||||||
|
|
||||||
// TODO: Do not draw links when they are outside the view
|
// TODO: Do not draw links when they are outside the view
|
||||||
let Some((output_anchor, input_anchor)) = self.get_link_coordinates(link) else {
|
let Some((output_anchor, input_anchor)) = self.get_link_coordinates(link) else {
|
||||||
warn!("Could not get allocation of ports of link: {:?}", link);
|
warn!("Could not get allocation of ports of link: {:?}", link);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.draw_link(&link_cr, &output_anchor, &input_anchor, link.active());
|
self.draw_link(
|
||||||
|
&link_cr,
|
||||||
|
&output_anchor,
|
||||||
|
&input_anchor,
|
||||||
|
link.active(),
|
||||||
|
color,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(port) = self.dragged_port.upgrade() {
|
if let Some(port) = self.dragged_port.upgrade() {
|
||||||
self.draw_dragged_link(&port, &link_cr);
|
self.draw_dragged_link(&port, &link_cr, &colors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,6 +837,12 @@ impl GraphView {
|
|||||||
graph.queue_draw();
|
graph.queue_draw();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
link.connect_notify_local(
|
||||||
|
Some("media-type"),
|
||||||
|
glib::clone!(@weak self as graph => move |_, _| {
|
||||||
|
graph.queue_draw();
|
||||||
|
}),
|
||||||
|
);
|
||||||
self.imp().links.borrow_mut().insert(link);
|
self.imp().links.borrow_mut().insert(link);
|
||||||
self.queue_draw();
|
self.queue_draw();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||||
|
use pipewire::spa::format::MediaType;
|
||||||
|
|
||||||
use super::Port;
|
use super::Port;
|
||||||
|
|
||||||
@@ -25,11 +26,22 @@ mod imp {
|
|||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Link {
|
pub struct Link {
|
||||||
pub output_port: glib::WeakRef<Port>,
|
pub output_port: glib::WeakRef<Port>,
|
||||||
pub input_port: glib::WeakRef<Port>,
|
pub input_port: glib::WeakRef<Port>,
|
||||||
pub active: Cell<bool>,
|
pub active: Cell<bool>,
|
||||||
|
pub media_type: Cell<MediaType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Link {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
output_port: glib::WeakRef::default(),
|
||||||
|
input_port: glib::WeakRef::default(),
|
||||||
|
active: Cell::default(),
|
||||||
|
media_type: Cell::new(MediaType::Unknown),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
@@ -53,6 +65,10 @@ mod imp {
|
|||||||
.default_value(false)
|
.default_value(false)
|
||||||
.flags(glib::ParamFlags::READWRITE)
|
.flags(glib::ParamFlags::READWRITE)
|
||||||
.build(),
|
.build(),
|
||||||
|
glib::ParamSpecUInt::builder("media-type")
|
||||||
|
.default_value(MediaType::Unknown.as_raw())
|
||||||
|
.flags(glib::ParamFlags::READWRITE)
|
||||||
|
.build(),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,6 +80,7 @@ mod imp {
|
|||||||
"output-port" => self.output_port.upgrade().to_value(),
|
"output-port" => self.output_port.upgrade().to_value(),
|
||||||
"input-port" => self.input_port.upgrade().to_value(),
|
"input-port" => self.input_port.upgrade().to_value(),
|
||||||
"active" => self.active.get().to_value(),
|
"active" => self.active.get().to_value(),
|
||||||
|
"media-type" => self.media_type.get().as_raw().to_value(),
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +90,9 @@ mod imp {
|
|||||||
"output-port" => self.output_port.set(value.get().unwrap()),
|
"output-port" => self.output_port.set(value.get().unwrap()),
|
||||||
"input-port" => self.input_port.set(value.get().unwrap()),
|
"input-port" => self.input_port.set(value.get().unwrap()),
|
||||||
"active" => self.active.set(value.get().unwrap()),
|
"active" => self.active.set(value.get().unwrap()),
|
||||||
|
"media-type" => self
|
||||||
|
.media_type
|
||||||
|
.set(MediaType::from_raw(value.get().unwrap())),
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +131,14 @@ impl Link {
|
|||||||
pub fn set_active(&self, active: bool) {
|
pub fn set_active(&self, active: bool) {
|
||||||
self.set_property("active", active);
|
self.set_property("active", active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn media_type(&self) -> MediaType {
|
||||||
|
MediaType::from_raw(self.property("media-type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_media_type(&self, media_type: MediaType) {
|
||||||
|
self.set_property("media-type", media_type.as_raw())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Link {
|
impl Default for Link {
|
||||||
|
|||||||
@@ -23,20 +23,26 @@ use gtk::{
|
|||||||
};
|
};
|
||||||
use pipewire::spa::Direction;
|
use pipewire::spa::Direction;
|
||||||
|
|
||||||
use crate::MediaType;
|
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
use once_cell::{sync::Lazy, unsync::OnceCell};
|
use once_cell::{sync::Lazy, unsync::OnceCell};
|
||||||
use pipewire::spa::Direction;
|
use pipewire::spa::{format::MediaType, Direction};
|
||||||
|
|
||||||
/// Graphical representation of a pipewire port.
|
/// Graphical representation of a pipewire port.
|
||||||
#[derive(Default, glib::Properties)]
|
#[derive(glib::Properties)]
|
||||||
#[properties(wrapper_type = super::Port)]
|
#[properties(wrapper_type = super::Port)]
|
||||||
pub struct Port {
|
pub struct Port {
|
||||||
#[property(get, set, construct_only)]
|
#[property(get, set, construct_only)]
|
||||||
pub(super) pipewire_id: OnceCell<u32>,
|
pub(super) pipewire_id: OnceCell<u32>,
|
||||||
|
#[property(
|
||||||
|
type = u32,
|
||||||
|
get = |_| self.media_type.get().as_raw(),
|
||||||
|
set = Self::set_media_type
|
||||||
|
)]
|
||||||
|
pub(super) media_type: Cell<MediaType>,
|
||||||
#[property(
|
#[property(
|
||||||
name = "name", type = String,
|
name = "name", type = String,
|
||||||
get = |this: &Self| this.label.text().to_string(),
|
get = |this: &Self| this.label.text().to_string(),
|
||||||
@@ -49,6 +55,17 @@ mod imp {
|
|||||||
pub(super) direction: OnceCell<Direction>,
|
pub(super) direction: OnceCell<Direction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Port {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pipewire_id: OnceCell::default(),
|
||||||
|
media_type: Cell::new(MediaType::Unknown),
|
||||||
|
label: gtk::Label::default(),
|
||||||
|
direction: OnceCell::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
impl ObjectSubclass for Port {
|
impl ObjectSubclass for Port {
|
||||||
const NAME: &'static str = "HelvumPort";
|
const NAME: &'static str = "HelvumPort";
|
||||||
@@ -184,6 +201,26 @@ mod imp {
|
|||||||
obj.add_controller(drop_target);
|
obj.add_controller(drop_target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Port {
|
||||||
|
fn set_media_type(&self, media_type: u32) {
|
||||||
|
let media_type = MediaType::from_raw(media_type);
|
||||||
|
|
||||||
|
self.media_type.set(media_type);
|
||||||
|
|
||||||
|
for css_class in ["video", "audio", "midi"] {
|
||||||
|
self.obj().remove_css_class(css_class)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color the port according to its media type.
|
||||||
|
match media_type {
|
||||||
|
MediaType::Video => self.obj().add_css_class("video"),
|
||||||
|
MediaType::Audio => self.obj().add_css_class("audio"),
|
||||||
|
MediaType::Application | MediaType::Stream => self.obj().add_css_class("midi"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
@@ -192,7 +229,7 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Port {
|
impl Port {
|
||||||
pub fn new(id: u32, name: &str, direction: Direction, media_type: Option<MediaType>) -> Self {
|
pub fn new(id: u32, name: &str, direction: Direction) -> Self {
|
||||||
// Create the widget and initialize needed fields
|
// Create the widget and initialize needed fields
|
||||||
let res: Self = glib::Object::builder()
|
let res: Self = glib::Object::builder()
|
||||||
.property("pipewire-id", id)
|
.property("pipewire-id", id)
|
||||||
@@ -208,14 +245,6 @@ impl Port {
|
|||||||
// Display a grab cursor when the mouse is over the port so the user knows it can be dragged to another port.
|
// Display a grab cursor when the mouse is over the port so the user knows it can be dragged to another port.
|
||||||
res.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
|
res.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
|
||||||
|
|
||||||
// Color the port according to its media type.
|
|
||||||
match media_type {
|
|
||||||
Some(MediaType::Video) => res.add_css_class("video"),
|
|
||||||
Some(MediaType::Audio) => res.add_css_class("audio"),
|
|
||||||
Some(MediaType::Midi) => res.add_css_class("midi"),
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user