diff --git a/src/coordinator/mod.rs b/src/coordinator/mod.rs index 806791e..b578433 100644 --- a/src/coordinator/mod.rs +++ b/src/coordinator/mod.rs @@ -11,16 +11,15 @@ use futures_util::{ stream::{SplitSink, SplitStream}, SinkExt, StreamExt, }; -use gstreamer::State; use gstreamer::prelude::ElementExt; -use log::{error, info}; +use gstreamer::State; +use log::{error, info, debug}; use tokio::net::TcpStream; use tokio::runtime::Handle; use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream}; use crate::remote_sources::TrackerState; use crate::{gstreamer_pipeline, remote_sources}; -use crate::ui::NormalizedBoxCoords; use crate::{joystick_source::joystick_loop, ui::GuiUpdate}; const PRIORITY_TIMEOUT: Duration = Duration::from_secs(2); @@ -48,7 +47,6 @@ pub enum ApplicationEvent { struct CoordState<'a> { pub sck_outbound: Option>, Message>>, pub sck_alive_server: Arc, - pub identity_boxes: Arc>>, pub sck_alive_recvr: Arc, pub joystick_loop_alive: Arc, @@ -72,14 +70,12 @@ impl<'a> CoordState<'a> { to_mec: Sender, to_gui: Sender, rt: Handle, - identity_boxes: Arc>>, tracker_state: Arc>, ) -> Self { let this = CoordState { sck_outbound: None, sck_alive_recvr: Arc::new(AtomicBool::new(false)), sck_alive_server: Arc::new(AtomicBool::new(false)), - identity_boxes, joystick_loop_alive: Arc::new(AtomicBool::new(false)), current_priority: ConnectionType::Local, @@ -95,13 +91,14 @@ impl<'a> CoordState<'a> { tracker_state, }; - this.rt.spawn(crate::remote_sources::shared_video_pipe::create_outbound_pipe( - this.pipeline.sink_frame.clone(), - this.to_mec.clone(), - this.keep_windows_pipe_alive.clone(), - this.identity_boxes.clone(), - this.tracker_state.clone(), - )); + this.rt.spawn( + crate::remote_sources::shared_video_pipe::create_outbound_pipe( + this.pipeline.sink_frame.clone(), + this.to_mec.clone(), + this.keep_windows_pipe_alive.clone(), + this.tracker_state.clone(), + ), + ); this } @@ -199,21 +196,27 @@ pub async fn start_coordinator( to_mec: Sender, to_gui: Sender, runtime: Handle, - identity_boxes: Arc>>, tracker_state: Arc>, ) { info!("Starting coordinator!"); let mec = pin!(mec); - let mut state = CoordState::new(mec, to_mec, to_gui, runtime, identity_boxes, tracker_state); + let mut state = CoordState::new(mec, to_mec, to_gui, runtime, tracker_state); - state.pipeline + state + .pipeline .pipeline .set_state(State::Playing) .expect("Could not set pipeline state to playing"); - if let Err(e) = state.to_gui.send(GuiUpdate::UpdatePaintable(state.pipeline.sink_paintable.clone())).await { + if let Err(e) = state + .to_gui + .send(GuiUpdate::UpdatePaintable( + state.pipeline.sink_paintable.clone(), + )) + .await + { error!("Could not send new paintable to GUI: {e}"); } @@ -236,10 +239,11 @@ pub async fn start_coordinator( state.socket_send(socket_message).await; } ApplicationEvent::EnableAutomatic(do_enable) => { + debug!("Trying to get lock on tracker_state for enable automatic"); if let Ok(mut ts) = state.tracker_state.lock() { ts.enabled = do_enable; } - }, + } ApplicationEvent::MoveEvent(coord, priority) => { // If Automatic control, but local event happens, override the automatice events for 2 seconds if priority <= state.current_priority @@ -268,7 +272,8 @@ pub async fn start_coordinator( } } - state.pipeline + state + .pipeline .pipeline .set_state(State::Null) .expect("Could not set pipeline state to playing"); diff --git a/src/gstreamer_pipeline.rs b/src/gstreamer_pipeline.rs index 79001ca..b9dc810 100644 --- a/src/gstreamer_pipeline.rs +++ b/src/gstreamer_pipeline.rs @@ -51,11 +51,13 @@ impl WebcamPipeline { .build() .expect("Could not build videoscale for GStreamer"); - let caps_string = String::from("video/x-raw,format=RGB,width=640,height=480,max-buffers=1,drop=true"); + let caps_string = + String::from("video/x-raw,format=RGB,width=640,height=480,max-buffers=1,drop=true"); // let caps_string = String::from("video/x-raw,format=RGB,max-buffers=1,drop=true"); - let appsrc_caps = gstreamer::Caps::from_str(&caps_string).expect("Couldn't create appsrc caps"); + let appsrc_caps = + gstreamer::Caps::from_str(&caps_string).expect("Couldn't create appsrc caps"); - /* + /* // let sink_frame = ElementFactory::make("appsink") // .name("frame_output") // .property("sync", &false) @@ -76,16 +78,18 @@ impl WebcamPipeline { sink_frame.set_property("caps", &appsrc_caps.to_value()); - pipeline.add_many(&[ - &source, - &convert, - &tee, - &queue_app, - &sink_paintable, - &resize, - &queue, - &sink_frame.upcast_ref(), - ]).expect("Could not link the elements to the pipeline"); + pipeline + .add_many(&[ + &source, + &convert, + &tee, + &queue_app, + &sink_paintable, + &resize, + &queue, + &sink_frame.upcast_ref(), + ]) + .expect("Could not link the elements to the pipeline"); source .link(&convert) @@ -115,14 +119,11 @@ impl WebcamPipeline { .link(&sink_frameoutput_sinkpad) .expect("Could not link tee srcpad 2 to frame output sink pad"); - queue - .link(&resize) - .expect("Could not link queue to resize"); + queue.link(&resize).expect("Could not link queue to resize"); resize .link(&sink_frame) .expect("Could not bind resize to appsrc"); - WebcamPipeline { pipeline, src: source, diff --git a/src/main.rs b/src/main.rs index 95c6861..be8feb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,9 @@ use tokio::runtime; mod config; mod coordinator; +mod gstreamer_pipeline; mod joystick_source; mod remote_sources; -mod gstreamer_pipeline; mod ui; const APP_ID: &str = "net.nickiel.joystick-controller-client"; diff --git a/src/remote_sources/mod.rs b/src/remote_sources/mod.rs index c4079f2..bb169ab 100644 --- a/src/remote_sources/mod.rs +++ b/src/remote_sources/mod.rs @@ -24,12 +24,16 @@ mod process_box_string; mod remote_source; pub mod shared_video_pipe; -use crate::coordinator::{ApplicationEvent, ConnectionType}; +use crate::{coordinator::{ApplicationEvent, ConnectionType}, ui::NormalizedBoxCoords}; pub struct TrackerState { pub tracking_id: u32, pub last_detect: Instant, pub enabled: bool, + + pub update_ids: bool, + + pub identity_boxes: Vec, } pub async fn start_socketserver( @@ -42,7 +46,6 @@ pub async fn start_socketserver( let listener = TcpListener::bind(&addr).await.expect("Can't listen"); info!("Listening on: {}", addr); - while let Ok((stream, _)) = listener.accept().await { let peer = stream .peer_addr() @@ -66,9 +69,7 @@ async fn accept_connection( mec: Sender, tracker_state: Arc>, ) { - if let Err(e) = - handle_connection(peer, stream, mec.clone(), tracker_state).await - { + if let Err(e) = handle_connection(peer, stream, mec.clone(), tracker_state).await { match e { Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8 => (), err => error!("Error processing connection: {}", err), diff --git a/src/remote_sources/process_box_string.rs b/src/remote_sources/process_box_string.rs index 58988c8..153b409 100644 --- a/src/remote_sources/process_box_string.rs +++ b/src/remote_sources/process_box_string.rs @@ -1,12 +1,11 @@ - use std::sync::{Arc, Mutex}; -use crate:: ui::NormalizedBoxCoords; - +use crate::ui::NormalizedBoxCoords; +use super::TrackerState; pub fn process_incoming_string( message: String, - identity_boxes: &Arc>>, // This goes all the way back to the GUI thread for drawing boxes + identity_boxes: &Arc>, // This goes all the way back to the GUI thread for drawing boxes ) -> core::result::Result<(), String> { let mut boxes: Vec = Vec::new(); @@ -37,23 +36,27 @@ pub fn process_incoming_string( let x2: u32 = coords2[0].parse().map_err(|_| "Invalid width")?; let y2: u32 = coords2[1].parse().map_err(|_| "Invalid width")?; - boxes.push( - NormalizedBoxCoords - { - id, - x1: (x1 as f32 / 1000.0), - x2: (x2 as f32 / 1000.0), - y1: (y1 as f32 / 1000.0), - y2: (y2 as f32 / 1000.0), - } - ); + boxes.push(NormalizedBoxCoords { + id, + x1: (x1 as f32 / 1000.0), + x2: (x2 as f32 / 1000.0), + y1: (y1 as f32 / 1000.0), + y2: (y2 as f32 / 1000.0), + }); } // Replace the memory address in the mutex guard with that of the created vec above if let Ok(mut ib) = identity_boxes.lock() { + let mut old_ids: Vec = ib.identity_boxes.iter().map(|x| x.id).collect(); + old_ids.sort(); + let mut new_ids: Vec = boxes.iter().map(|x| x.id).collect(); + new_ids.sort(); + + ib.update_ids = new_ids == old_ids; + // Replace the memory address in the mutex guard with that of the created vec above - *ib = boxes; + ib.identity_boxes = boxes; } Ok(()) -} \ No newline at end of file +} diff --git a/src/remote_sources/shared_video_pipe.rs b/src/remote_sources/shared_video_pipe.rs index fc81336..7a0886e 100644 --- a/src/remote_sources/shared_video_pipe.rs +++ b/src/remote_sources/shared_video_pipe.rs @@ -1,5 +1,7 @@ use std::{ - cmp::{max, min}, sync::{atomic::AtomicBool, Arc, Mutex}, time::{Duration, Instant} + cmp::{max, min}, + sync::{atomic::AtomicBool, Arc, Mutex}, + time::{Duration, Instant}, }; use async_channel::Sender; @@ -9,17 +11,19 @@ use interprocess::os::windows::named_pipe::{ pipe_mode::{self, Bytes}, tokio::{DuplexPipeStream, PipeStream}, }; -use log::{error, info}; +use log::{error, info, debug}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use crate::{coordinator::{ApplicationEvent, MoveEvent}, remote_sources::process_box_string, ui::NormalizedBoxCoords}; +use crate::{ + coordinator::{ApplicationEvent, MoveEvent}, + remote_sources::process_box_string, +}; use super::TrackerState; struct LoopState { pub appsink: Arc>, - pub mec: Sender, - pub identity_boxes: Arc>>, // This goes all the way back to the GUI thread for drawing boxes + pub mec: Sender, pub tracker_state: Arc>, pub pipe: PipeStream, @@ -31,48 +35,59 @@ struct LoopState { impl LoopState { fn get_video_frame(&mut self) -> Result, String> { - let sample = self.appsink.lock().map_err(|e| format!("Could not get a lock on the appsink: {e}"))?.pull_sample().map_err(|e| format!("Could not pull appsink sample: {e}"))?; + let sample = self + .appsink + .lock() + .map_err(|e| format!("Could not get a lock on the appsink: {e}"))? + .pull_sample() + .map_err(|e| format!("Could not pull appsink sample: {e}"))?; let buffer = sample.buffer_owned().unwrap(); - gstreamer_video::VideoFrame::from_buffer_readable(buffer, &self.video_info).map_err(|_| format!("Unable to make video frame from buffer!")) + gstreamer_video::VideoFrame::from_buffer_readable(buffer, &self.video_info) + .map_err(|_| format!("Unable to make video frame from buffer!")) } async fn read_return_message(&mut self) -> Result { // Read message size from the pipe if let Err(e) = self.pipe.read_exact(&mut self.len_buf).await { - return Err(format!("Couldn't read message length from the windows pipe: {e}")); + return Err(format!( + "Couldn't read message length from the windows pipe: {e}" + )); } let length = u32::from_le_bytes(self.len_buf); self.byte_buffer.resize(length as usize, 0); // Read the message of message length from the pipe if let Err(e) = self.pipe.read_exact(&mut self.byte_buffer).await { - return Err(format!("Couldn't read the message from the windows pipe: {e}")); + return Err(format!( + "Couldn't read the message from the windows pipe: {e}" + )); } Ok(String::from_utf8_lossy(&self.byte_buffer).to_string()) - } - } pub async fn create_outbound_pipe( appsink: Arc>, - mec: Sender, + mec: Sender, keep_alive: Arc, - identity_boxes: Arc>>, // This goes all the way back to the GUI thread for drawing boxes tracker_state: Arc>, ) { - if let Ok(pipe) = DuplexPipeStream::::connect_by_path(r"\\.\pipe\example_pipe").await { let mut state = LoopState { appsink, mec, - identity_boxes, tracker_state, pipe, - video_info: gstreamer_video::VideoInfo::builder(gstreamer_video::VideoFormat::Rgb, 640, 480) .build() .expect("Couldn't build video info!"), + video_info: gstreamer_video::VideoInfo::builder( + gstreamer_video::VideoFormat::Rgb, + 640, + 480, + ) + .build() + .expect("Couldn't build video info!"), byte_buffer: Vec::new(), len_buf: [0; 4], }; @@ -100,9 +115,9 @@ pub async fn create_outbound_pipe( }; // info!("Video frame {}x{} with stride of {}, is this many bytes: {}", video_frame.width(), video_frame.height(), video_frame.plane_stride()[0], video_frame.plane_data(0).unwrap().len()); - // Send video frame to pipe - if let Err(e) = send_to_pipe(&mut state.pipe, video_frame.plane_data(0).unwrap()).await { + if let Err(e) = send_to_pipe(&mut state.pipe, video_frame.plane_data(0).unwrap()).await + { error!("Error in sending to the pipe: {e}"); break; } @@ -119,13 +134,15 @@ pub async fn create_outbound_pipe( let y_off: i32; // Load the tracking boxes into identity_boxes, then do the adjustment calcuations on the updated tracking info (side-effects) - (x_off, y_off) = process_box_string::process_incoming_string(message.to_string(), &state.identity_boxes) - .and_then(|_| calculate_tracking(&state.tracker_state, &state.identity_boxes)) - .unwrap_or((0, 0)); - + (x_off, y_off) = process_box_string::process_incoming_string( + message.to_string(), + &state.tracker_state, + ) + .and_then(|_| calculate_tracking(&state.tracker_state)) + .unwrap_or((0, 0)); - - if let Err(e) = state.mec + if let Err(e) = state + .mec .send(ApplicationEvent::MoveEvent( MoveEvent { x: x_off, y: y_off }, crate::coordinator::ConnectionType::Automated, @@ -148,7 +165,7 @@ pub async fn create_outbound_pipe( async fn send_to_pipe<'a>( pipe: &mut PipeStream, - message: &'a [u8] + message: &'a [u8], ) -> Result<(), Box> { pipe.write_all(message).await?; // pipe.shutdown().await?; @@ -156,32 +173,26 @@ async fn send_to_pipe<'a>( Ok(()) } - fn calculate_tracking( tracker_state: &Arc>, - identity_boxes: &Arc>>, // This goes all the way back to the GUI thread for drawing boxes ) -> core::result::Result<(i32, i32), String> { + debug!("Getting lock on tracker state for caculate tracking"); + if let Ok(mut ts) = tracker_state.lock() { + if ts.last_detect + Duration::from_secs(2) < Instant::now() && !ts.identity_boxes.is_empty() { + info!("Setting new target: {}", ts.identity_boxes[0].id); + ts.tracking_id = ts.identity_boxes[0].id; + } - if let Ok(boxes) = identity_boxes.lock() { - if let Ok(mut ts) = tracker_state.lock() { - if ts.last_detect + Duration::from_secs(2) < Instant::now() && !boxes.is_empty() { - info!("Setting new target: {}", boxes[0].id); - ts.tracking_id = boxes[0].id; - } - - if let Some(target_box) = boxes.iter().find(|e| e.id == ts.tracking_id) { - let x_adjust = calc_x_adjust(target_box.x1, target_box.x2); - let y_adjust = calc_y_adjust(target_box.y1); - ts.last_detect = Instant::now(); - Ok((x_adjust, y_adjust)) - } else { - Err("Couldn't find target in results".to_string()) - } + if let Some(target_box) = ts.identity_boxes.iter().find(|e| e.id == ts.tracking_id) { + let x_adjust = calc_x_adjust(target_box.x1, target_box.x2); + let y_adjust = calc_y_adjust(target_box.y1); + ts.last_detect = Instant::now(); + Ok((x_adjust, y_adjust)) } else { - Err("Couldn't lock tracker state".to_string()) + Err("Couldn't find target in results".to_string()) } } else { - Err("Couldn't lock identity boxes".to_string()) + Err("Couldn't lock tracker state".to_string()) } } @@ -205,4 +216,4 @@ fn calc_y_adjust(y1: f32) -> i32 { y_adjust = (y_adjust as f32 * 0.75) as i32; } min(max(y_adjust, -100), 100) -} \ No newline at end of file +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a3c4489..bbde2b1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,7 +5,7 @@ use gtk::cairo::Context; use gtk::gdk::Paintable; use gtk::{glib, prelude::*, AspectFrame, Label, ListBox}; use gtk::{Application, ApplicationWindow}; -use log::{info, error}; +use log::{error, info, debug}; use serde::{Deserialize, Serialize}; use tokio::runtime::Handle; @@ -85,20 +85,20 @@ pub fn build_ui(app: &Application, runtime: Handle) { // Main Event Channel let (to_mec, mec) = async_channel::unbounded::(); let (to_gui, gui_recv) = async_channel::bounded::(10); - let identity_boxes: Arc>> = Arc::new(Mutex::new(vec![])); let tracker_state = Arc::new(Mutex::new(TrackerState { - tracking_id: 0, - last_detect: Instant::now(), - enabled: true, - })); + tracking_id: 0, + last_detect: Instant::now(), + enabled: true, + identity_boxes: vec![], + update_ids: false, + })); runtime.spawn(start_coordinator( mec, to_mec.clone(), to_gui, runtime.clone(), - identity_boxes.clone(), tracker_state.clone(), )); @@ -108,17 +108,21 @@ pub fn build_ui(app: &Application, runtime: Handle) { .can_focus(true) .build(); - let tabpanel = gtk::Notebook::builder() - .enable_popup(true) - .build(); + let tabpanel = gtk::Notebook::builder().enable_popup(true).build(); let socket_panel = Arc::new(SocketPanel::new(&initial_settings)); socket_panel.connect_button_callback(to_mec.clone()); - tabpanel.append_page(socket_panel.get_top_level(), Some(>k::Label::new(Some("Cam Connection")))); + tabpanel.append_page( + socket_panel.get_top_level(), + Some(>k::Label::new(Some("Cam Connection"))), + ); - let tracker_panel = TrackerPanel::new(); + let tracker_panel = TrackerPanel::new(tracker_state.clone()); tracker_panel.connect_button_callback(to_mec.clone()); - tabpanel.append_page(tracker_panel.get_top_level(), Some(&Label::new(Some("Auto Settings")))); + tabpanel.append_page( + tracker_panel.get_top_level(), + Some(&Label::new(Some("Auto Settings"))), + ); let axis_label = Label::builder() .label("X: 0 Y: )") @@ -126,39 +130,63 @@ pub fn build_ui(app: &Application, runtime: Handle) { .css_classes(vec!["JoystickCurrent"]) .build(); - left_box.append(&conn_status_label); left_box.append(&tabpanel); left_box.append(&axis_label); main_box.append(&left_box); - let webcam_picture = gtk::Picture::builder() - .can_focus(false) - .build(); + let webcam_picture = gtk::Picture::builder().can_focus(false).build(); - let overlay_box = gtk::Overlay::builder() - .build(); + let overlay_box = gtk::Overlay::builder().build(); let aspect = AspectFrame::builder() - .ratio(16.0/9.0) + .ratio(16.0 / 9.0) .obey_child(false) .child(&overlay_box) .build(); main_box.append(&aspect); let drawable = gtk::DrawingArea::builder() - // .content_height(480) - // .content_width(640) .build(); + let drawable_ts = tracker_state.clone(); drawable.set_draw_func(move |_, ctx, width, height| { - draw_boxes(width, height, &ctx, identity_boxes.clone(), tracker_state.clone()); + draw_boxes( + width, + height, + &ctx, + &drawable_ts, + ); }); - overlay_box.set_child(Some(&webcam_picture)); overlay_box.add_overlay(&drawable); + let items = tracker_panel.items.clone(); + + glib::timeout_add_seconds_local(1, + glib::clone!(@strong items => move || { + debug!("Getting lock on tracker state for checking identity boxes"); + + // don't update the stringlist until after letting go of the tracker state + // due to async interweaving causing a mutex deadlock + let mut ids: Option> = None; + if let Ok(ts) = tracker_state.lock() { + if ts.update_ids { + ids = Some(ts.identity_boxes + .iter() + .map(|t| t.id.to_string()) + .collect()); + } + } + + if let Some (ids) = ids { + let old_len = items.n_items(); + items.splice(0, old_len, &ids.iter().map(|x| x.as_str()).collect::>()[0..]) + } + + glib::ControlFlow::Continue + })); glib::spawn_future_local( glib::clone!(@weak axis_label, @weak conn_status_label, @weak tabpanel, @strong socket_panel, @strong gui_recv, @weak drawable => async move { @@ -178,7 +206,7 @@ pub fn build_ui(app: &Application, runtime: Handle) { "Currently Connected" } else { socket_panel.set_sensitive(true); - tabpanel.set_page(0); + // tabpanel.set_page(0); // tabpanel.set_show_tabs(false); "Currently Disconnected" } @@ -210,19 +238,24 @@ pub fn build_ui(app: &Application, runtime: Handle) { window.present(); } -fn draw_boxes(width: i32, height: i32, ctx: &Context, boxes: Arc>>, tracker_state: Arc>) { +fn draw_boxes( + width: i32, + height: i32, + ctx: &Context, + tracker_state: &Arc>, +) { ctx.set_line_width(5.0); - ctx.select_font_face("Arial", gtk::cairo::FontSlant::Normal, gtk::cairo::FontWeight::Bold); + ctx.select_font_face( + "Arial", + gtk::cairo::FontSlant::Normal, + gtk::cairo::FontWeight::Bold, + ); ctx.set_font_size(24.0); - let active: u32 = match tracker_state.lock() { - Ok(e) => e.tracking_id, - Err(_) => 0 - }; - - - if let Ok(bxs) = boxes.lock() { - for nb in bxs.iter() { + debug!("Getting tracker state for drawing boxes"); + if let Ok(ts) = tracker_state.lock() { + let active = ts.tracking_id; + for nb in ts.identity_boxes.iter() { if nb.id == active { ctx.set_source_rgb(1.0, 0.0, 0.0); } else { @@ -244,7 +277,8 @@ fn draw_boxes(width: i32, height: i32, ctx: &Context, boxes: Arc SocketPanel { - let content_box = Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(10) @@ -64,7 +67,6 @@ impl SocketPanel { } pub fn connect_button_callback(&self, to_mec: Sender) { - let ip_entry = &self.ip_entry; let port_entry = &self.port_entry; @@ -93,4 +95,4 @@ impl SocketPanel { } })); } -} \ No newline at end of file +} diff --git a/src/ui/tracker_panel.rs b/src/ui/tracker_panel.rs index ee4270c..ce4b9aa 100644 --- a/src/ui/tracker_panel.rs +++ b/src/ui/tracker_panel.rs @@ -1,20 +1,73 @@ +use std::sync::{Arc, Mutex}; + use async_channel::Sender; -use gtk::{prelude::{BoxExt, ButtonExt, ToggleButtonExt}, Box, Label, ToggleButton}; -use log::error; - -use crate::coordinator::ApplicationEvent; +use gtk::{ + glib::object::CastNone, prelude::{ + BoxExt, ButtonExt, Cast, GObjectPropertyExpressionExt, ListItemExt, ListModelExt, ToggleButtonExt + }, Box, Label, ListItem, ListView, ScrolledWindow, SignalListItemFactory, SingleSelection, StringList, StringObject, ToggleButton, Widget +}; +use log::{debug, error}; +use crate::{coordinator::ApplicationEvent, remote_sources::TrackerState}; pub struct TrackerPanel { top_level: Box, enable_disable: ToggleButton, current_id: Label, - + scrolled_window: ScrolledWindow, + + pub items: StringList, + list_view: ListView, } impl TrackerPanel { - pub fn new() -> TrackerPanel { + pub fn new(tracker_state: Arc>) -> TrackerPanel { + let factory = SignalListItemFactory::new(); + factory.connect_setup(move |_, list_item| { + let list_item = list_item + .downcast_ref::() + .expect("Needs to be a List Item"); + + let label = Label::new(None); + list_item.set_child(Some(&label)); + + list_item + .property_expression("item") + .chain_property::("string") + .bind(&label, "label", Widget::NONE); + }); + + let items = StringList::new(&["item1", "item2", "item3"]); + + let model = SingleSelection::builder().model(&items).build(); + + model.connect_selected_item_notify(move |x| { + let item = x + .selected_item() + .and_downcast::(); + + if let Some(item) = item { + if let Ok(id) = item.string().parse::() { + debug!("Getting lock on tracker state for setting active tracking id!"); + if let Ok(mut ts) = tracker_state.lock() { + ts.tracking_id = id; + } + } else { + error!("An unparsable ID was clicked"); + } + } else { + error!("An invalid id was selected from the selection"); + } + }); + + let list_view = ListView::new(Some(model), Some(factory)); + + let scrolled_window = ScrolledWindow::builder() + .child(&list_view) + .hscrollbar_policy(gtk::PolicyType::Never) + .height_request(200) + .build(); let top_level = Box::builder() .orientation(gtk::Orientation::Vertical) @@ -28,22 +81,29 @@ impl TrackerPanel { let enable_disable = ToggleButton::with_label("Enable Automatic Tracking"); let current_id = Label::builder() - .label("") + .label("Not Tracking") .can_focus(false) .can_target(false) + .css_classes(["current-id"]) .build(); top_level.append(&enable_disable); top_level.append(¤t_id); + top_level.append(&scrolled_window); TrackerPanel { top_level, enable_disable, current_id, + scrolled_window, + + items, + list_view, } } + pub fn get_top_level(&self) -> &Box { &self.top_level } @@ -51,9 +111,11 @@ impl TrackerPanel { pub fn connect_button_callback(&self, to_mec: Sender) { self.enable_disable.connect_clicked(move |button| { - if let Err(e) = to_mec.send_blocking(ApplicationEvent::EnableAutomatic(button.is_active())) { + if let Err(e) = + to_mec.send_blocking(ApplicationEvent::EnableAutomatic(button.is_active())) + { error!("Could not send message to the MEC: {e}"); } }); } -} \ No newline at end of file +} diff --git a/style.css b/style.css index 90dca07..7a11357 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,15 @@ +scrolledwindow > listview { + font-size: 10pt; +} + +label.current-id { + font-size: 12pt; + color: black; + background-color: cornsilk; + border-radius: 5px; + margin-top: 4px; + margin-bottom: 4px; +} entry { font-size: 16pt; @@ -27,5 +39,5 @@ label.JoystickCurrent { button { color: black; - font-size: 16pt; + font-size: 10pt; }