mirror of
https://gitlab.freedesktop.org/pipewire/helvum
synced 2026-03-15 03:26:10 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb3b3cf298 | ||
|
|
4549ba6ff5 | ||
|
|
e78d6f5fb4 | ||
|
|
96c079d29e | ||
|
|
5d4931b418 | ||
|
|
b983ade736 | ||
|
|
94d5e95695 | ||
|
|
e1f63ddd28 | ||
|
|
903df21ba3 |
@@ -15,7 +15,7 @@ categories = ["gui", "multimedia"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pipewire = "0.7.1"
|
pipewire = "0.7.1"
|
||||||
adw = { version = "0.5", package = "libadwaita", features = ["v1_3"] }
|
adw = { version = "0.5", package = "libadwaita", features = ["v1_4"] }
|
||||||
glib = { version = "0.18", features = ["log"] }
|
glib = { version = "0.18", features = ["log"] }
|
||||||
|
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
Helvum is a GTK-based patchbay for pipewire, inspired by the JACK tool [catia](https://kx.studio/Applications:Catia).
|
Helvum is a GTK-based patchbay for pipewire, inspired by the JACK tool [catia](https://kx.studio/Applications:Catia).
|
||||||
|
|
||||||
|
> [!caution]
|
||||||
|
> Helvum is currently not actively maintained.
|
||||||
|
> If you are interested in maintaining the project, please see https://gitlab.freedesktop.org/pipewire/helvum/-/issues/137
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<a href="https://flathub.org/apps/details/org.pipewire.Helvum"><img src="https://flathub.org/assets/badges/flathub-badge-en.png" width="300"/></a>
|
<a href="https://flathub.org/apps/details/org.pipewire.Helvum"><img src="https://flathub.org/assets/badges/flathub-badge-en.png" width="300"/></a>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ base_id = 'org.pipewire.Helvum'
|
|||||||
|
|
||||||
dependency('glib-2.0', version: '>= 2.66')
|
dependency('glib-2.0', version: '>= 2.66')
|
||||||
dependency('gtk4', version: '>= 4.4.0')
|
dependency('gtk4', version: '>= 4.4.0')
|
||||||
dependency('libadwaita-1', version: '>= 1.3')
|
dependency('libadwaita-1', version: '>= 1.4')
|
||||||
dependency('libpipewire-0.3')
|
dependency('libpipewire-0.3')
|
||||||
|
|
||||||
desktop_file_validate = find_program('desktop-file-validate', required: false)
|
desktop_file_validate = find_program('desktop-file-validate', required: false)
|
||||||
|
|||||||
@@ -112,9 +112,12 @@ mod imp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn show_about_dialog(&self) {
|
fn show_about_dialog(&self) {
|
||||||
|
let obj = &*self.obj();
|
||||||
|
let window = obj.active_window().unwrap();
|
||||||
let authors: Vec<&str> = AUTHORS.split(':').collect();
|
let authors: Vec<&str> = AUTHORS.split(':').collect();
|
||||||
|
|
||||||
let about_window = adw::AboutWindow::builder()
|
let about_window = adw::AboutWindow::builder()
|
||||||
|
.transient_for(&window)
|
||||||
.application_icon(APP_ID)
|
.application_icon(APP_ID)
|
||||||
.application_name("Helvum")
|
.application_name("Helvum")
|
||||||
.developer_name("Tom Wagner")
|
.developer_name("Tom Wagner")
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ mod imp {
|
|||||||
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::NodeNameChanged { id, name, media_name } => imp.node_name_changed(id, &name, &media_name),
|
||||||
PipewireMessage::PortAdded { id, node_id, name, direction } => imp.add_port(id, name.as_str(), node_id, direction),
|
PipewireMessage::PortAdded { id, node_id, name, direction } => imp.add_port(id, name.as_str(), node_id, direction),
|
||||||
PipewireMessage::PortFormatChanged { id, media_type } => imp.port_media_type_changed(id, media_type),
|
PipewireMessage::PortFormatChanged { id, media_type } => imp.port_media_type_changed(id, media_type),
|
||||||
PipewireMessage::LinkAdded {
|
PipewireMessage::LinkAdded {
|
||||||
@@ -95,6 +96,23 @@ mod imp {
|
|||||||
self.obj().graph().add_node(node, node_type);
|
self.obj().graph().add_node(node, node_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a node tooltip to the view.
|
||||||
|
fn node_name_changed(&self, id: u32, node_name: &str, media_name: &str) {
|
||||||
|
let items = self.items.borrow();
|
||||||
|
|
||||||
|
let Some(node) = items.get(&id) else {
|
||||||
|
log::warn!("Node (id: {id}) for changed name not found in graph manager");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(node) = node.dynamic_cast_ref::<graph::Node>() else {
|
||||||
|
log::warn!("Graph Manager item under node (id: {id}) is not a node");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
node.set_node_name(node_name);
|
||||||
|
node.set_media_name(media_name);
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove the node with the specified id from the view.
|
/// Remove the node with the specified id from the view.
|
||||||
fn remove_node(&self, id: u32) {
|
fn remove_node(&self, id: u32) {
|
||||||
log::info!("Removing node from graph: id {}", id);
|
log::info!("Removing node from graph: id {}", id);
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ pub enum PipewireMessage {
|
|||||||
name: String,
|
name: String,
|
||||||
node_type: Option<NodeType>,
|
node_type: Option<NodeType>,
|
||||||
},
|
},
|
||||||
|
NodeNameChanged {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
media_name: String,
|
||||||
|
},
|
||||||
PortAdded {
|
PortAdded {
|
||||||
id: u32,
|
id: u32,
|
||||||
node_id: u32,
|
node_id: u32,
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ use std::{
|
|||||||
use adw::glib::{self, clone};
|
use adw::glib::{self, clone};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use pipewire::{
|
use pipewire::{
|
||||||
|
keys,
|
||||||
link::{Link, LinkChangeMask, LinkInfo, LinkListener, LinkState},
|
link::{Link, LinkChangeMask, LinkInfo, LinkListener, LinkState},
|
||||||
|
node::{Node, NodeInfo, NodeListener},
|
||||||
port::{Port, PortChangeMask, PortInfo, PortListener},
|
port::{Port, PortChangeMask, PortInfo, PortListener},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
properties,
|
properties,
|
||||||
@@ -43,6 +45,10 @@ use crate::{GtkMessage, MediaType, NodeType, PipewireMessage};
|
|||||||
use state::{Item, State};
|
use state::{Item, State};
|
||||||
|
|
||||||
enum ProxyItem {
|
enum ProxyItem {
|
||||||
|
Node {
|
||||||
|
_proxy: Node,
|
||||||
|
_listener: NodeListener,
|
||||||
|
},
|
||||||
Port {
|
Port {
|
||||||
proxy: Port,
|
proxy: Port,
|
||||||
_listener: PortListener,
|
_listener: PortListener,
|
||||||
@@ -65,7 +71,9 @@ pub(super) fn thread_main(
|
|||||||
|
|
||||||
while !is_stopped.get() {
|
while !is_stopped.get() {
|
||||||
// Try to connect
|
// Try to connect
|
||||||
let core = match context.connect(None) {
|
let core = match context.connect(Some(properties! {
|
||||||
|
"media.category" => "Manager"
|
||||||
|
})) {
|
||||||
Ok(core) => Rc::new(core),
|
Ok(core) => Rc::new(core),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
if !is_connecting {
|
if !is_connecting {
|
||||||
@@ -147,7 +155,7 @@ pub(super) fn thread_main(
|
|||||||
.add_listener_local()
|
.add_listener_local()
|
||||||
.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, ®istry, &proxies, &state),
|
||||||
ObjectType::Port => handle_port(global, >k_sender, ®istry, &proxies, &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),
|
||||||
_ => {
|
_ => {
|
||||||
@@ -178,10 +186,21 @@ pub(super) fn thread_main(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the nicest possible name for the node, using a fallback chain of possible name attributes
|
||||||
|
fn get_node_name(props: &ForeignDict) -> &str {
|
||||||
|
props
|
||||||
|
.get(&keys::NODE_DESCRIPTION)
|
||||||
|
.or_else(|| props.get(&keys::NODE_NICK))
|
||||||
|
.or_else(|| props.get(&keys::NODE_NAME))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle a new node being added
|
/// Handle a new node being added
|
||||||
fn handle_node(
|
fn handle_node(
|
||||||
node: &GlobalObject<ForeignDict>,
|
node: &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 = node
|
let props = node
|
||||||
@@ -189,15 +208,7 @@ fn handle_node(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("Node object is missing properties");
|
.expect("Node object is missing properties");
|
||||||
|
|
||||||
// Get the nicest possible name for the node, using a fallback chain of possible name attributes.
|
let name = get_node_name(props).to_string();
|
||||||
let name = String::from(
|
|
||||||
props
|
|
||||||
.get("node.description")
|
|
||||||
.or_else(|| props.get("node.nick"))
|
|
||||||
.or_else(|| props.get("node.name"))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -228,6 +239,50 @@ fn handle_node(
|
|||||||
node_type,
|
node_type,
|
||||||
})
|
})
|
||||||
.expect("Failed to send message");
|
.expect("Failed to send message");
|
||||||
|
|
||||||
|
let proxy: Node = registry.bind(node).expect("Failed to bind to node proxy");
|
||||||
|
let listener = proxy
|
||||||
|
.add_listener_local()
|
||||||
|
.info(clone!(@strong sender, @strong proxies => move |info| {
|
||||||
|
handle_node_info(info, &sender, &proxies);
|
||||||
|
}))
|
||||||
|
.register();
|
||||||
|
|
||||||
|
proxies.borrow_mut().insert(
|
||||||
|
node.id,
|
||||||
|
ProxyItem::Node {
|
||||||
|
_proxy: proxy,
|
||||||
|
_listener: listener,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_node_info(
|
||||||
|
info: &NodeInfo,
|
||||||
|
sender: &glib::Sender<PipewireMessage>,
|
||||||
|
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
|
||||||
|
) {
|
||||||
|
debug!("Received node info: {:?}", info);
|
||||||
|
|
||||||
|
let id = info.id();
|
||||||
|
let proxies = proxies.borrow();
|
||||||
|
let Some(ProxyItem::Node { .. }) = proxies.get(&id) else {
|
||||||
|
error!("Received info on unknown node with id {id}");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let props = info.props().expect("NodeInfo object is missing properties");
|
||||||
|
if let Some(media_name) = props.get(&keys::MEDIA_NAME) {
|
||||||
|
let name = get_node_name(props).to_string();
|
||||||
|
|
||||||
|
sender
|
||||||
|
.send(PipewireMessage::NodeNameChanged {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
media_name: media_name.to_string(),
|
||||||
|
})
|
||||||
|
.expect("Failed to send message");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a new port being added
|
/// Handle a new port being added
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ node {
|
|||||||
background-color: @headerbar_bg_color;
|
background-color: @headerbar_bg_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
node label.heading {
|
node .node-title {
|
||||||
padding: 4px 7px;
|
padding: 4px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,3 +53,20 @@ port-handle {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: @media-type-unknown;
|
background-color: @media-type-unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.rounded {
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.rounded {
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.rounded > :first-child {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.rounded > :nth-child(2) {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ mod imp {
|
|||||||
// Memorized data for an in-progress zoom gesture
|
// Memorized data for an in-progress zoom gesture
|
||||||
pub zoom_gesture_initial_zoom: Cell<Option<f64>>,
|
pub zoom_gesture_initial_zoom: Cell<Option<f64>>,
|
||||||
pub zoom_gesture_anchor: Cell<Option<(f64, f64)>>,
|
pub zoom_gesture_anchor: Cell<Option<(f64, f64)>>,
|
||||||
|
|
||||||
|
// This keeps track of an ongoing move view gesture.
|
||||||
|
pub move_view_state: Cell<(f64, f64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GraphView {
|
impl Default for GraphView {
|
||||||
@@ -108,6 +111,7 @@ mod imp {
|
|||||||
port_drag_cursor: Cell::new(Point::new(0.0, 0.0)),
|
port_drag_cursor: Cell::new(Point::new(0.0, 0.0)),
|
||||||
zoom_gesture_initial_zoom: Default::default(),
|
zoom_gesture_initial_zoom: Default::default(),
|
||||||
zoom_gesture_anchor: Default::default(),
|
zoom_gesture_anchor: Default::default(),
|
||||||
|
move_view_state: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +140,7 @@ mod imp {
|
|||||||
self.setup_port_drag_and_drop();
|
self.setup_port_drag_and_drop();
|
||||||
self.setup_scroll_zooming();
|
self.setup_scroll_zooming();
|
||||||
self.setup_zoom_gesture();
|
self.setup_zoom_gesture();
|
||||||
|
self.setup_move_view();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispose(&self) {
|
fn dispose(&self) {
|
||||||
@@ -465,6 +470,48 @@ mod imp {
|
|||||||
self.obj().add_controller(zoom_gesture);
|
self.obj().add_controller(zoom_gesture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup_move_view(&self) {
|
||||||
|
let drag_controller = gtk::GestureDrag::new();
|
||||||
|
|
||||||
|
drag_controller.set_button(gtk::gdk::BUTTON_MIDDLE);
|
||||||
|
|
||||||
|
// TODO: set `all-scroll` cursor while dragging view
|
||||||
|
|
||||||
|
drag_controller.connect_drag_begin(|drag_controller, _, _| {
|
||||||
|
let widget = drag_controller
|
||||||
|
.widget()
|
||||||
|
.downcast::<super::GraphView>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
widget.imp().move_view_state.set((0.0, 0.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
drag_controller.connect_drag_update(|drag_controller, x, y| {
|
||||||
|
let widget = drag_controller
|
||||||
|
.widget()
|
||||||
|
.downcast::<super::GraphView>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let imp = widget.imp();
|
||||||
|
let state = imp.move_view_state.replace((x, y));
|
||||||
|
let delta_x = state.0 - x;
|
||||||
|
let delta_y = state.1 - y;
|
||||||
|
|
||||||
|
let hadjustment_ref = imp.hadjustment.borrow();
|
||||||
|
let vadjustment_ref = imp.vadjustment.borrow();
|
||||||
|
let hadjustment = hadjustment_ref.as_ref().unwrap();
|
||||||
|
let vadjustment = vadjustment_ref.as_ref().unwrap();
|
||||||
|
|
||||||
|
let new_hadjustment = hadjustment.value() + delta_x;
|
||||||
|
let new_vadjustment = vadjustment.value() + delta_y;
|
||||||
|
|
||||||
|
hadjustment.set_value(new_hadjustment);
|
||||||
|
vadjustment.set_value(new_vadjustment);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.obj().add_controller(drag_controller);
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_link(
|
fn draw_link(
|
||||||
&self,
|
&self,
|
||||||
link_cr: &cairo::Context,
|
link_cr: &cairo::Context,
|
||||||
|
|||||||
@@ -34,15 +34,26 @@ mod imp {
|
|||||||
#[property(get, set, construct_only)]
|
#[property(get, set, construct_only)]
|
||||||
pub(super) pipewire_id: Cell<u32>,
|
pub(super) pipewire_id: Cell<u32>,
|
||||||
#[property(
|
#[property(
|
||||||
name = "name", type = String,
|
name = "node-name", type = String,
|
||||||
get = |this: &Self| this.label.text().to_string(),
|
get = |this: &Self| this.node_name.text().to_string(),
|
||||||
set = |this: &Self, val| {
|
set = |this: &Self, val| {
|
||||||
this.label.set_text(val);
|
this.node_name.set_text(val);
|
||||||
this.label.set_tooltip_text(Some(val));
|
this.node_name.set_tooltip_text(Some(val));
|
||||||
}
|
}
|
||||||
)]
|
)]
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub(super) label: TemplateChild<gtk::Label>,
|
pub(super) node_name: TemplateChild<gtk::Label>,
|
||||||
|
#[property(
|
||||||
|
name = "media-name", type = String,
|
||||||
|
get = |this: &Self| this.media_name.text().to_string(),
|
||||||
|
set = |this: &Self, val| {
|
||||||
|
this.media_name.set_text(val);
|
||||||
|
this.media_name.set_tooltip_text(Some(val));
|
||||||
|
this.media_name.set_visible(!val.is_empty());
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
#[template_child]
|
||||||
|
pub(super) media_name: TemplateChild<gtk::Label>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub(super) separator: TemplateChild<gtk::Separator>,
|
pub(super) separator: TemplateChild<gtk::Separator>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
@@ -74,8 +85,11 @@ mod imp {
|
|||||||
fn constructed(&self) {
|
fn constructed(&self) {
|
||||||
self.parent_constructed();
|
self.parent_constructed();
|
||||||
|
|
||||||
|
// Force left-to-right direction for the ports grid to avoid messed up UI when defaulting to right-to-left
|
||||||
|
self.port_grid.set_direction(gtk::TextDirection::Ltr);
|
||||||
|
|
||||||
// Display a grab cursor when the mouse is over the label so the user knows the node can be dragged.
|
// Display a grab cursor when the mouse is over the label so the user knows the node can be dragged.
|
||||||
self.label
|
self.node_name
|
||||||
.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
|
.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +155,7 @@ glib::wrapper! {
|
|||||||
impl Node {
|
impl Node {
|
||||||
pub fn new(name: &str, pipewire_id: u32) -> Self {
|
pub fn new(name: &str, pipewire_id: u32) -> Self {
|
||||||
glib::Object::builder()
|
glib::Object::builder()
|
||||||
.property("name", name)
|
.property("node-name", name)
|
||||||
.property("pipewire-id", pipewire_id)
|
.property("pipewire-id", pipewire_id)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,36 @@
|
|||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label">
|
<object class="GtkBox">
|
||||||
<style>
|
<style>
|
||||||
<class name="heading"></class>
|
<class name="node-title"></class>
|
||||||
</style>
|
</style>
|
||||||
<property name="wrap">true</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
|
<property name="spacing">1</property>
|
||||||
<property name="lines">2</property>
|
<child>
|
||||||
<property name="max-width-chars">20</property>
|
<object class="GtkLabel" id="node_name">
|
||||||
|
<style>
|
||||||
|
<class name="heading"></class>
|
||||||
|
</style>
|
||||||
|
<property name="wrap">true</property>
|
||||||
|
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
|
||||||
|
<property name="lines">2</property>
|
||||||
|
<property name="max-width-chars">20</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="media_name">
|
||||||
|
<style>
|
||||||
|
<class name="dim-label"></class>
|
||||||
|
<class name="caption"></class>
|
||||||
|
</style>
|
||||||
|
<property name="visible">false</property>
|
||||||
|
<property name="wrap">true</property>
|
||||||
|
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
|
||||||
|
<property name="lines">2</property>
|
||||||
|
<property name="max-width-chars">20</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ mod imp {
|
|||||||
fn constructed(&self) {
|
fn constructed(&self) {
|
||||||
self.parent_constructed();
|
self.parent_constructed();
|
||||||
|
|
||||||
|
// Force left-to-right direction for the ports grid to avoid messed up UI when defaulting to right-to-left
|
||||||
|
self.obj().set_direction(gtk::TextDirection::Ltr);
|
||||||
|
|
||||||
// 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.
|
||||||
self.obj()
|
self.obj()
|
||||||
.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
|
.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ mod imp {
|
|||||||
menu.append(Some("200%"), Some("win.set-zoom(2.0)"));
|
menu.append(Some("200%"), Some("win.set-zoom(2.0)"));
|
||||||
menu.append(Some("300%"), Some("win.set-zoom(3.0)"));
|
menu.append(Some("300%"), Some("win.set-zoom(3.0)"));
|
||||||
let popover = gtk::PopoverMenu::from_model(Some(&menu));
|
let popover = gtk::PopoverMenu::from_model(Some(&menu));
|
||||||
|
popover.set_position(gtk::PositionType::Top);
|
||||||
|
|
||||||
ZoomEntry {
|
ZoomEntry {
|
||||||
graphview: Default::default(),
|
graphview: Default::default(),
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<interface>
|
<interface>
|
||||||
<template class="HelvumZoomEntry" parent="GtkBox">
|
<template class="HelvumZoomEntry" parent="GtkBox">
|
||||||
<child>
|
<property name="spacing">12</property>
|
||||||
<object class="GtkButton" id="zoom_out_button">
|
|
||||||
<property name="icon-name">zoom-out-symbolic</property>
|
|
||||||
<property name="tooltip-text">Zoom out</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkEntry" id="entry">
|
<object class="GtkEntry" id="entry">
|
||||||
<property name="secondary-icon-name">go-down-symbolic</property>
|
<property name="secondary-icon-name">go-down-symbolic</property>
|
||||||
<property name="input-purpose">digits</property>
|
<property name="input-purpose">digits</property>
|
||||||
|
<property name="max-width-chars">5</property>
|
||||||
|
<style>
|
||||||
|
<class name="osd"/>
|
||||||
|
<class name="rounded"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="zoom_out_button">
|
||||||
|
<property name="icon-name">zoom-out-symbolic</property>
|
||||||
|
<property name="tooltip-text">Zoom out</property>
|
||||||
|
<style>
|
||||||
|
<class name="osd"/>
|
||||||
|
<class name="rounded"/>
|
||||||
|
</style>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="zoom_in_button">
|
<object class="GtkButton" id="zoom_in_button">
|
||||||
<property name="icon-name">zoom-in-symbolic</property>
|
<property name="icon-name">zoom-in-symbolic</property>
|
||||||
<property name="tooltip-text">Zoom in</property>
|
<property name="tooltip-text">Zoom in</property>
|
||||||
|
<style>
|
||||||
|
<class name="osd"/>
|
||||||
|
<class name="rounded"/>
|
||||||
|
</style>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<style>
|
|
||||||
<class name="linked"/>
|
|
||||||
</style>
|
|
||||||
</template>
|
</template>
|
||||||
</interface>
|
</interface>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<interface>
|
<interface>
|
||||||
<requires lib="gtk" version="4.0"/>
|
<requires lib="gtk" version="4.0"/>
|
||||||
<requires lib="Adw" version="1.0"/>
|
<requires lib="Adw" version="1.4"/>
|
||||||
<menu id="primary_menu">
|
<menu id="primary_menu">
|
||||||
<section>
|
<section>
|
||||||
<item>
|
<item>
|
||||||
@@ -15,44 +15,51 @@
|
|||||||
<property name="default-height">720</property>
|
<property name="default-height">720</property>
|
||||||
<property name="title">Helvum - Pipewire Patchbay</property>
|
<property name="title">Helvum - Pipewire Patchbay</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="AdwToolbarView">
|
||||||
<property name="orientation">vertical</property>
|
<child type="top">
|
||||||
<child>
|
|
||||||
<object class="AdwHeaderBar" id="header_bar">
|
<object class="AdwHeaderBar" id="header_bar">
|
||||||
<child type="end">
|
<child type="end">
|
||||||
<object class="GtkBox">
|
<object class="GtkMenuButton">
|
||||||
<property name="spacing">6</property>
|
<property name="icon-name">open-menu-symbolic</property>
|
||||||
|
<property name="menu-model">primary_menu</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<property name="content">
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<object class="AdwBanner" id="connection_banner">
|
||||||
|
<property name="title" translatable="yes">Disconnected</property>
|
||||||
|
<property name="revealed">false</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkOverlay">
|
||||||
<child>
|
<child>
|
||||||
|
<object class="GtkScrolledWindow">
|
||||||
|
<child>
|
||||||
|
<object class="HelvumGraphView" id="graph">
|
||||||
|
<property name="hexpand">true</property>
|
||||||
|
<property name="vexpand">true</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child type="overlay">
|
||||||
<object class="HelvumZoomEntry">
|
<object class="HelvumZoomEntry">
|
||||||
<property name="zoomed-widget">graph</property>
|
<property name="zoomed-widget">graph</property>
|
||||||
</object>
|
<property name="halign">end</property>
|
||||||
</child>
|
<property name="valign">end</property>
|
||||||
<child>
|
<property name="margin-end">24</property>
|
||||||
<object class="GtkMenuButton">
|
<property name="margin-bottom">24</property>
|
||||||
<property name="icon-name">open-menu-symbolic</property>
|
|
||||||
<property name="menu-model">primary_menu</property>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</property>
|
||||||
<child>
|
|
||||||
<object class="AdwBanner" id="connection_banner">
|
|
||||||
<property name="title" translatable="yes">Disconnected</property>
|
|
||||||
<property name="revealed">false</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkScrolledWindow">
|
|
||||||
<child>
|
|
||||||
<object class="HelvumGraphView" id="graph">
|
|
||||||
<property name="hexpand">true</property>
|
|
||||||
<property name="vexpand">true</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user