working manual change of tracking id
This commit is contained in:
parent
22a81ee44e
commit
b5c7afadcb
10 changed files with 284 additions and 153 deletions
|
@ -11,16 +11,15 @@ use futures_util::{
|
||||||
stream::{SplitSink, SplitStream},
|
stream::{SplitSink, SplitStream},
|
||||||
SinkExt, StreamExt,
|
SinkExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gstreamer::State;
|
|
||||||
use gstreamer::prelude::ElementExt;
|
use gstreamer::prelude::ElementExt;
|
||||||
use log::{error, info};
|
use gstreamer::State;
|
||||||
|
use log::{error, info, debug};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
|
use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream};
|
||||||
|
|
||||||
use crate::remote_sources::TrackerState;
|
use crate::remote_sources::TrackerState;
|
||||||
use crate::{gstreamer_pipeline, remote_sources};
|
use crate::{gstreamer_pipeline, remote_sources};
|
||||||
use crate::ui::NormalizedBoxCoords;
|
|
||||||
use crate::{joystick_source::joystick_loop, ui::GuiUpdate};
|
use crate::{joystick_source::joystick_loop, ui::GuiUpdate};
|
||||||
|
|
||||||
const PRIORITY_TIMEOUT: Duration = Duration::from_secs(2);
|
const PRIORITY_TIMEOUT: Duration = Duration::from_secs(2);
|
||||||
|
@ -48,7 +47,6 @@ pub enum ApplicationEvent {
|
||||||
struct CoordState<'a> {
|
struct CoordState<'a> {
|
||||||
pub sck_outbound: Option<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>,
|
pub sck_outbound: Option<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>,
|
||||||
pub sck_alive_server: Arc<AtomicBool>,
|
pub sck_alive_server: Arc<AtomicBool>,
|
||||||
pub identity_boxes: Arc<Mutex<Vec<NormalizedBoxCoords>>>,
|
|
||||||
pub sck_alive_recvr: Arc<AtomicBool>,
|
pub sck_alive_recvr: Arc<AtomicBool>,
|
||||||
pub joystick_loop_alive: Arc<AtomicBool>,
|
pub joystick_loop_alive: Arc<AtomicBool>,
|
||||||
|
|
||||||
|
@ -72,14 +70,12 @@ impl<'a> CoordState<'a> {
|
||||||
to_mec: Sender<ApplicationEvent>,
|
to_mec: Sender<ApplicationEvent>,
|
||||||
to_gui: Sender<GuiUpdate>,
|
to_gui: Sender<GuiUpdate>,
|
||||||
rt: Handle,
|
rt: Handle,
|
||||||
identity_boxes: Arc<Mutex<Vec<NormalizedBoxCoords>>>,
|
|
||||||
tracker_state: Arc<Mutex<TrackerState>>,
|
tracker_state: Arc<Mutex<TrackerState>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let this = CoordState {
|
let this = CoordState {
|
||||||
sck_outbound: None,
|
sck_outbound: None,
|
||||||
sck_alive_recvr: Arc::new(AtomicBool::new(false)),
|
sck_alive_recvr: Arc::new(AtomicBool::new(false)),
|
||||||
sck_alive_server: Arc::new(AtomicBool::new(false)),
|
sck_alive_server: Arc::new(AtomicBool::new(false)),
|
||||||
identity_boxes,
|
|
||||||
joystick_loop_alive: Arc::new(AtomicBool::new(false)),
|
joystick_loop_alive: Arc::new(AtomicBool::new(false)),
|
||||||
|
|
||||||
current_priority: ConnectionType::Local,
|
current_priority: ConnectionType::Local,
|
||||||
|
@ -95,13 +91,14 @@ impl<'a> CoordState<'a> {
|
||||||
|
|
||||||
tracker_state,
|
tracker_state,
|
||||||
};
|
};
|
||||||
this.rt.spawn(crate::remote_sources::shared_video_pipe::create_outbound_pipe(
|
this.rt.spawn(
|
||||||
this.pipeline.sink_frame.clone(),
|
crate::remote_sources::shared_video_pipe::create_outbound_pipe(
|
||||||
this.to_mec.clone(),
|
this.pipeline.sink_frame.clone(),
|
||||||
this.keep_windows_pipe_alive.clone(),
|
this.to_mec.clone(),
|
||||||
this.identity_boxes.clone(),
|
this.keep_windows_pipe_alive.clone(),
|
||||||
this.tracker_state.clone(),
|
this.tracker_state.clone(),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,21 +196,27 @@ pub async fn start_coordinator(
|
||||||
to_mec: Sender<ApplicationEvent>,
|
to_mec: Sender<ApplicationEvent>,
|
||||||
to_gui: Sender<GuiUpdate>,
|
to_gui: Sender<GuiUpdate>,
|
||||||
runtime: Handle,
|
runtime: Handle,
|
||||||
identity_boxes: Arc<Mutex<Vec<NormalizedBoxCoords>>>,
|
|
||||||
tracker_state: Arc<Mutex<TrackerState>>,
|
tracker_state: Arc<Mutex<TrackerState>>,
|
||||||
) {
|
) {
|
||||||
info!("Starting coordinator!");
|
info!("Starting coordinator!");
|
||||||
|
|
||||||
let mec = pin!(mec);
|
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
|
.pipeline
|
||||||
.set_state(State::Playing)
|
.set_state(State::Playing)
|
||||||
.expect("Could not set pipeline state to 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}");
|
error!("Could not send new paintable to GUI: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,10 +239,11 @@ pub async fn start_coordinator(
|
||||||
state.socket_send(socket_message).await;
|
state.socket_send(socket_message).await;
|
||||||
}
|
}
|
||||||
ApplicationEvent::EnableAutomatic(do_enable) => {
|
ApplicationEvent::EnableAutomatic(do_enable) => {
|
||||||
|
debug!("Trying to get lock on tracker_state for enable automatic");
|
||||||
if let Ok(mut ts) = state.tracker_state.lock() {
|
if let Ok(mut ts) = state.tracker_state.lock() {
|
||||||
ts.enabled = do_enable;
|
ts.enabled = do_enable;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
ApplicationEvent::MoveEvent(coord, priority) => {
|
ApplicationEvent::MoveEvent(coord, priority) => {
|
||||||
// If Automatic control, but local event happens, override the automatice events for 2 seconds
|
// If Automatic control, but local event happens, override the automatice events for 2 seconds
|
||||||
if priority <= state.current_priority
|
if priority <= state.current_priority
|
||||||
|
@ -268,7 +272,8 @@ pub async fn start_coordinator(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.pipeline
|
state
|
||||||
|
.pipeline
|
||||||
.pipeline
|
.pipeline
|
||||||
.set_state(State::Null)
|
.set_state(State::Null)
|
||||||
.expect("Could not set pipeline state to playing");
|
.expect("Could not set pipeline state to playing");
|
||||||
|
|
|
@ -51,9 +51,11 @@ impl WebcamPipeline {
|
||||||
.build()
|
.build()
|
||||||
.expect("Could not build videoscale for GStreamer");
|
.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 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")
|
// let sink_frame = ElementFactory::make("appsink")
|
||||||
|
@ -76,16 +78,18 @@ impl WebcamPipeline {
|
||||||
|
|
||||||
sink_frame.set_property("caps", &appsrc_caps.to_value());
|
sink_frame.set_property("caps", &appsrc_caps.to_value());
|
||||||
|
|
||||||
pipeline.add_many(&[
|
pipeline
|
||||||
&source,
|
.add_many(&[
|
||||||
&convert,
|
&source,
|
||||||
&tee,
|
&convert,
|
||||||
&queue_app,
|
&tee,
|
||||||
&sink_paintable,
|
&queue_app,
|
||||||
&resize,
|
&sink_paintable,
|
||||||
&queue,
|
&resize,
|
||||||
&sink_frame.upcast_ref(),
|
&queue,
|
||||||
]).expect("Could not link the elements to the pipeline");
|
&sink_frame.upcast_ref(),
|
||||||
|
])
|
||||||
|
.expect("Could not link the elements to the pipeline");
|
||||||
|
|
||||||
source
|
source
|
||||||
.link(&convert)
|
.link(&convert)
|
||||||
|
@ -115,14 +119,11 @@ impl WebcamPipeline {
|
||||||
.link(&sink_frameoutput_sinkpad)
|
.link(&sink_frameoutput_sinkpad)
|
||||||
.expect("Could not link tee srcpad 2 to frame output sink pad");
|
.expect("Could not link tee srcpad 2 to frame output sink pad");
|
||||||
|
|
||||||
queue
|
queue.link(&resize).expect("Could not link queue to resize");
|
||||||
.link(&resize)
|
|
||||||
.expect("Could not link queue to resize");
|
|
||||||
resize
|
resize
|
||||||
.link(&sink_frame)
|
.link(&sink_frame)
|
||||||
.expect("Could not bind resize to appsrc");
|
.expect("Could not bind resize to appsrc");
|
||||||
|
|
||||||
|
|
||||||
WebcamPipeline {
|
WebcamPipeline {
|
||||||
pipeline,
|
pipeline,
|
||||||
src: source,
|
src: source,
|
||||||
|
|
|
@ -8,9 +8,9 @@ use tokio::runtime;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod coordinator;
|
mod coordinator;
|
||||||
|
mod gstreamer_pipeline;
|
||||||
mod joystick_source;
|
mod joystick_source;
|
||||||
mod remote_sources;
|
mod remote_sources;
|
||||||
mod gstreamer_pipeline;
|
|
||||||
mod ui;
|
mod ui;
|
||||||
const APP_ID: &str = "net.nickiel.joystick-controller-client";
|
const APP_ID: &str = "net.nickiel.joystick-controller-client";
|
||||||
|
|
||||||
|
|
|
@ -24,12 +24,16 @@ mod process_box_string;
|
||||||
mod remote_source;
|
mod remote_source;
|
||||||
pub mod shared_video_pipe;
|
pub mod shared_video_pipe;
|
||||||
|
|
||||||
use crate::coordinator::{ApplicationEvent, ConnectionType};
|
use crate::{coordinator::{ApplicationEvent, ConnectionType}, ui::NormalizedBoxCoords};
|
||||||
|
|
||||||
pub struct TrackerState {
|
pub struct TrackerState {
|
||||||
pub tracking_id: u32,
|
pub tracking_id: u32,
|
||||||
pub last_detect: Instant,
|
pub last_detect: Instant,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|
||||||
|
pub update_ids: bool,
|
||||||
|
|
||||||
|
pub identity_boxes: Vec<NormalizedBoxCoords>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_socketserver(
|
pub async fn start_socketserver(
|
||||||
|
@ -42,7 +46,6 @@ pub async fn start_socketserver(
|
||||||
let listener = TcpListener::bind(&addr).await.expect("Can't listen");
|
let listener = TcpListener::bind(&addr).await.expect("Can't listen");
|
||||||
info!("Listening on: {}", addr);
|
info!("Listening on: {}", addr);
|
||||||
|
|
||||||
|
|
||||||
while let Ok((stream, _)) = listener.accept().await {
|
while let Ok((stream, _)) = listener.accept().await {
|
||||||
let peer = stream
|
let peer = stream
|
||||||
.peer_addr()
|
.peer_addr()
|
||||||
|
@ -66,9 +69,7 @@ async fn accept_connection(
|
||||||
mec: Sender<ApplicationEvent>,
|
mec: Sender<ApplicationEvent>,
|
||||||
tracker_state: Arc<Mutex<TrackerState>>,
|
tracker_state: Arc<Mutex<TrackerState>>,
|
||||||
) {
|
) {
|
||||||
if let Err(e) =
|
if let Err(e) = handle_connection(peer, stream, mec.clone(), tracker_state).await {
|
||||||
handle_connection(peer, stream, mec.clone(), tracker_state).await
|
|
||||||
{
|
|
||||||
match e {
|
match e {
|
||||||
Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8 => (),
|
Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8 => (),
|
||||||
err => error!("Error processing connection: {}", err),
|
err => error!("Error processing connection: {}", err),
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use crate:: ui::NormalizedBoxCoords;
|
use crate::ui::NormalizedBoxCoords;
|
||||||
|
use super::TrackerState;
|
||||||
|
|
||||||
pub fn process_incoming_string(
|
pub fn process_incoming_string(
|
||||||
message: String,
|
message: String,
|
||||||
identity_boxes: &Arc<Mutex<Vec<NormalizedBoxCoords>>>, // This goes all the way back to the GUI thread for drawing boxes
|
identity_boxes: &Arc<Mutex<TrackerState>>, // This goes all the way back to the GUI thread for drawing boxes
|
||||||
) -> core::result::Result<(), String> {
|
) -> core::result::Result<(), String> {
|
||||||
let mut boxes: Vec<NormalizedBoxCoords> = Vec::new();
|
let mut boxes: Vec<NormalizedBoxCoords> = Vec::new();
|
||||||
|
|
||||||
|
@ -37,22 +36,26 @@ pub fn process_incoming_string(
|
||||||
let x2: u32 = coords2[0].parse().map_err(|_| "Invalid width")?;
|
let x2: u32 = coords2[0].parse().map_err(|_| "Invalid width")?;
|
||||||
let y2: u32 = coords2[1].parse().map_err(|_| "Invalid width")?;
|
let y2: u32 = coords2[1].parse().map_err(|_| "Invalid width")?;
|
||||||
|
|
||||||
boxes.push(
|
boxes.push(NormalizedBoxCoords {
|
||||||
NormalizedBoxCoords
|
id,
|
||||||
{
|
x1: (x1 as f32 / 1000.0),
|
||||||
id,
|
x2: (x2 as f32 / 1000.0),
|
||||||
x1: (x1 as f32 / 1000.0),
|
y1: (y1 as f32 / 1000.0),
|
||||||
x2: (x2 as f32 / 1000.0),
|
y2: (y2 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
|
// Replace the memory address in the mutex guard with that of the created vec above
|
||||||
if let Ok(mut ib) = identity_boxes.lock() {
|
if let Ok(mut ib) = identity_boxes.lock() {
|
||||||
|
let mut old_ids: Vec<u32> = ib.identity_boxes.iter().map(|x| x.id).collect();
|
||||||
|
old_ids.sort();
|
||||||
|
let mut new_ids: Vec<u32> = 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
|
// Replace the memory address in the mutex guard with that of the created vec above
|
||||||
*ib = boxes;
|
ib.identity_boxes = boxes;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use std::{
|
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;
|
use async_channel::Sender;
|
||||||
|
@ -9,17 +11,19 @@ use interprocess::os::windows::named_pipe::{
|
||||||
pipe_mode::{self, Bytes},
|
pipe_mode::{self, Bytes},
|
||||||
tokio::{DuplexPipeStream, PipeStream},
|
tokio::{DuplexPipeStream, PipeStream},
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info, debug};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
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;
|
use super::TrackerState;
|
||||||
|
|
||||||
struct LoopState {
|
struct LoopState {
|
||||||
pub appsink: Arc<Mutex<AppSink>>,
|
pub appsink: Arc<Mutex<AppSink>>,
|
||||||
pub mec: Sender<ApplicationEvent>,
|
pub mec: Sender<ApplicationEvent>,
|
||||||
pub identity_boxes: Arc<Mutex<Vec<NormalizedBoxCoords>>>, // This goes all the way back to the GUI thread for drawing boxes
|
|
||||||
pub tracker_state: Arc<Mutex<TrackerState>>,
|
pub tracker_state: Arc<Mutex<TrackerState>>,
|
||||||
|
|
||||||
pub pipe: PipeStream<Bytes, Bytes>,
|
pub pipe: PipeStream<Bytes, Bytes>,
|
||||||
|
@ -31,48 +35,59 @@ struct LoopState {
|
||||||
|
|
||||||
impl LoopState {
|
impl LoopState {
|
||||||
fn get_video_frame(&mut self) -> Result<VideoFrame<Readable>, String> {
|
fn get_video_frame(&mut self) -> Result<VideoFrame<Readable>, 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();
|
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<String, String> {
|
async fn read_return_message(&mut self) -> Result<String, String> {
|
||||||
// Read message size from the pipe
|
// Read message size from the pipe
|
||||||
if let Err(e) = self.pipe.read_exact(&mut self.len_buf).await {
|
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);
|
let length = u32::from_le_bytes(self.len_buf);
|
||||||
self.byte_buffer.resize(length as usize, 0);
|
self.byte_buffer.resize(length as usize, 0);
|
||||||
// Read the message of message length from the pipe
|
// Read the message of message length from the pipe
|
||||||
if let Err(e) = self.pipe.read_exact(&mut self.byte_buffer).await {
|
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())
|
Ok(String::from_utf8_lossy(&self.byte_buffer).to_string())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_outbound_pipe(
|
pub async fn create_outbound_pipe(
|
||||||
appsink: Arc<Mutex<AppSink>>,
|
appsink: Arc<Mutex<AppSink>>,
|
||||||
mec: Sender<ApplicationEvent>,
|
mec: Sender<ApplicationEvent>,
|
||||||
keep_alive: Arc<AtomicBool>,
|
keep_alive: Arc<AtomicBool>,
|
||||||
identity_boxes: Arc<Mutex<Vec<NormalizedBoxCoords>>>, // This goes all the way back to the GUI thread for drawing boxes
|
|
||||||
tracker_state: Arc<Mutex<TrackerState>>,
|
tracker_state: Arc<Mutex<TrackerState>>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if let Ok(pipe) =
|
if let Ok(pipe) =
|
||||||
DuplexPipeStream::<pipe_mode::Bytes>::connect_by_path(r"\\.\pipe\example_pipe").await
|
DuplexPipeStream::<pipe_mode::Bytes>::connect_by_path(r"\\.\pipe\example_pipe").await
|
||||||
{
|
{
|
||||||
let mut state = LoopState {
|
let mut state = LoopState {
|
||||||
appsink,
|
appsink,
|
||||||
mec,
|
mec,
|
||||||
identity_boxes,
|
|
||||||
tracker_state,
|
tracker_state,
|
||||||
|
|
||||||
pipe,
|
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(),
|
byte_buffer: Vec::new(),
|
||||||
len_buf: [0; 4],
|
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());
|
// 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
|
// 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}");
|
error!("Error in sending to the pipe: {e}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -119,13 +134,15 @@ pub async fn create_outbound_pipe(
|
||||||
let y_off: i32;
|
let y_off: i32;
|
||||||
|
|
||||||
// Load the tracking boxes into identity_boxes, then do the adjustment calcuations on the updated tracking info (side-effects)
|
// 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)
|
(x_off, y_off) = process_box_string::process_incoming_string(
|
||||||
.and_then(|_| calculate_tracking(&state.tracker_state, &state.identity_boxes))
|
message.to_string(),
|
||||||
.unwrap_or((0, 0));
|
&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(
|
.send(ApplicationEvent::MoveEvent(
|
||||||
MoveEvent { x: x_off, y: y_off },
|
MoveEvent { x: x_off, y: y_off },
|
||||||
crate::coordinator::ConnectionType::Automated,
|
crate::coordinator::ConnectionType::Automated,
|
||||||
|
@ -148,7 +165,7 @@ pub async fn create_outbound_pipe(
|
||||||
|
|
||||||
async fn send_to_pipe<'a>(
|
async fn send_to_pipe<'a>(
|
||||||
pipe: &mut PipeStream<pipe_mode::Bytes, pipe_mode::Bytes>,
|
pipe: &mut PipeStream<pipe_mode::Bytes, pipe_mode::Bytes>,
|
||||||
message: &'a [u8]
|
message: &'a [u8],
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
pipe.write_all(message).await?;
|
pipe.write_all(message).await?;
|
||||||
// pipe.shutdown().await?;
|
// pipe.shutdown().await?;
|
||||||
|
@ -156,32 +173,26 @@ async fn send_to_pipe<'a>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn calculate_tracking(
|
fn calculate_tracking(
|
||||||
tracker_state: &Arc<Mutex<TrackerState>>,
|
tracker_state: &Arc<Mutex<TrackerState>>,
|
||||||
identity_boxes: &Arc<Mutex<Vec<NormalizedBoxCoords>>>, // This goes all the way back to the GUI thread for drawing boxes
|
|
||||||
) -> core::result::Result<(i32, i32), String> {
|
) -> 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 Some(target_box) = ts.identity_boxes.iter().find(|e| e.id == ts.tracking_id) {
|
||||||
if let Ok(mut ts) = tracker_state.lock() {
|
let x_adjust = calc_x_adjust(target_box.x1, target_box.x2);
|
||||||
if ts.last_detect + Duration::from_secs(2) < Instant::now() && !boxes.is_empty() {
|
let y_adjust = calc_y_adjust(target_box.y1);
|
||||||
info!("Setting new target: {}", boxes[0].id);
|
ts.last_detect = Instant::now();
|
||||||
ts.tracking_id = boxes[0].id;
|
Ok((x_adjust, y_adjust))
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Err("Couldn't lock tracker state".to_string())
|
Err("Couldn't find target in results".to_string())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err("Couldn't lock identity boxes".to_string())
|
Err("Couldn't lock tracker state".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
106
src/ui/mod.rs
106
src/ui/mod.rs
|
@ -5,7 +5,7 @@ use gtk::cairo::Context;
|
||||||
use gtk::gdk::Paintable;
|
use gtk::gdk::Paintable;
|
||||||
use gtk::{glib, prelude::*, AspectFrame, Label, ListBox};
|
use gtk::{glib, prelude::*, AspectFrame, Label, ListBox};
|
||||||
use gtk::{Application, ApplicationWindow};
|
use gtk::{Application, ApplicationWindow};
|
||||||
use log::{info, error};
|
use log::{error, info, debug};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
|
|
||||||
|
@ -85,20 +85,20 @@ pub fn build_ui(app: &Application, runtime: Handle) {
|
||||||
// Main Event Channel
|
// Main Event Channel
|
||||||
let (to_mec, mec) = async_channel::unbounded::<ApplicationEvent>();
|
let (to_mec, mec) = async_channel::unbounded::<ApplicationEvent>();
|
||||||
let (to_gui, gui_recv) = async_channel::bounded::<GuiUpdate>(10);
|
let (to_gui, gui_recv) = async_channel::bounded::<GuiUpdate>(10);
|
||||||
let identity_boxes: Arc<Mutex<Vec<NormalizedBoxCoords>>> = Arc::new(Mutex::new(vec![]));
|
|
||||||
let tracker_state = Arc::new(Mutex::new(TrackerState {
|
let tracker_state = Arc::new(Mutex::new(TrackerState {
|
||||||
tracking_id: 0,
|
tracking_id: 0,
|
||||||
last_detect: Instant::now(),
|
last_detect: Instant::now(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}));
|
|
||||||
|
|
||||||
|
identity_boxes: vec![],
|
||||||
|
update_ids: false,
|
||||||
|
}));
|
||||||
|
|
||||||
runtime.spawn(start_coordinator(
|
runtime.spawn(start_coordinator(
|
||||||
mec,
|
mec,
|
||||||
to_mec.clone(),
|
to_mec.clone(),
|
||||||
to_gui,
|
to_gui,
|
||||||
runtime.clone(),
|
runtime.clone(),
|
||||||
identity_boxes.clone(),
|
|
||||||
tracker_state.clone(),
|
tracker_state.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -108,17 +108,21 @@ pub fn build_ui(app: &Application, runtime: Handle) {
|
||||||
.can_focus(true)
|
.can_focus(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let tabpanel = gtk::Notebook::builder()
|
let tabpanel = gtk::Notebook::builder().enable_popup(true).build();
|
||||||
.enable_popup(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let socket_panel = Arc::new(SocketPanel::new(&initial_settings));
|
let socket_panel = Arc::new(SocketPanel::new(&initial_settings));
|
||||||
socket_panel.connect_button_callback(to_mec.clone());
|
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());
|
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()
|
let axis_label = Label::builder()
|
||||||
.label("X: 0 Y: )")
|
.label("X: 0 Y: )")
|
||||||
|
@ -126,39 +130,63 @@ pub fn build_ui(app: &Application, runtime: Handle) {
|
||||||
.css_classes(vec!["JoystickCurrent"])
|
.css_classes(vec!["JoystickCurrent"])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
||||||
left_box.append(&conn_status_label);
|
left_box.append(&conn_status_label);
|
||||||
left_box.append(&tabpanel);
|
left_box.append(&tabpanel);
|
||||||
left_box.append(&axis_label);
|
left_box.append(&axis_label);
|
||||||
|
|
||||||
main_box.append(&left_box);
|
main_box.append(&left_box);
|
||||||
|
|
||||||
let webcam_picture = gtk::Picture::builder()
|
let webcam_picture = gtk::Picture::builder().can_focus(false).build();
|
||||||
.can_focus(false)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let overlay_box = gtk::Overlay::builder()
|
let overlay_box = gtk::Overlay::builder().build();
|
||||||
.build();
|
|
||||||
let aspect = AspectFrame::builder()
|
let aspect = AspectFrame::builder()
|
||||||
.ratio(16.0/9.0)
|
.ratio(16.0 / 9.0)
|
||||||
.obey_child(false)
|
.obey_child(false)
|
||||||
.child(&overlay_box)
|
.child(&overlay_box)
|
||||||
.build();
|
.build();
|
||||||
main_box.append(&aspect);
|
main_box.append(&aspect);
|
||||||
|
|
||||||
let drawable = gtk::DrawingArea::builder()
|
let drawable = gtk::DrawingArea::builder()
|
||||||
// .content_height(480)
|
|
||||||
// .content_width(640)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
let drawable_ts = tracker_state.clone();
|
||||||
drawable.set_draw_func(move |_, ctx, width, height| {
|
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.set_child(Some(&webcam_picture));
|
||||||
overlay_box.add_overlay(&drawable);
|
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<Vec<String>> = 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::<Vec<&str>>()[0..])
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::ControlFlow::Continue
|
||||||
|
}));
|
||||||
|
|
||||||
glib::spawn_future_local(
|
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 {
|
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"
|
"Currently Connected"
|
||||||
} else {
|
} else {
|
||||||
socket_panel.set_sensitive(true);
|
socket_panel.set_sensitive(true);
|
||||||
tabpanel.set_page(0);
|
// tabpanel.set_page(0);
|
||||||
// tabpanel.set_show_tabs(false);
|
// tabpanel.set_show_tabs(false);
|
||||||
"Currently Disconnected"
|
"Currently Disconnected"
|
||||||
}
|
}
|
||||||
|
@ -210,19 +238,24 @@ pub fn build_ui(app: &Application, runtime: Handle) {
|
||||||
window.present();
|
window.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_boxes(width: i32, height: i32, ctx: &Context, boxes: Arc<Mutex<Vec<NormalizedBoxCoords>>>, tracker_state: Arc<Mutex<TrackerState>>) {
|
fn draw_boxes(
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
ctx: &Context,
|
||||||
|
tracker_state: &Arc<Mutex<TrackerState>>,
|
||||||
|
) {
|
||||||
ctx.set_line_width(5.0);
|
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);
|
ctx.set_font_size(24.0);
|
||||||
|
|
||||||
let active: u32 = match tracker_state.lock() {
|
debug!("Getting tracker state for drawing boxes");
|
||||||
Ok(e) => e.tracking_id,
|
if let Ok(ts) = tracker_state.lock() {
|
||||||
Err(_) => 0
|
let active = ts.tracking_id;
|
||||||
};
|
for nb in ts.identity_boxes.iter() {
|
||||||
|
|
||||||
|
|
||||||
if let Ok(bxs) = boxes.lock() {
|
|
||||||
for nb in bxs.iter() {
|
|
||||||
if nb.id == active {
|
if nb.id == active {
|
||||||
ctx.set_source_rgb(1.0, 0.0, 0.0);
|
ctx.set_source_rgb(1.0, 0.0, 0.0);
|
||||||
} else {
|
} else {
|
||||||
|
@ -244,7 +277,8 @@ fn draw_boxes(width: i32, height: i32, ctx: &Context, boxes: Arc<Mutex<Vec<Norma
|
||||||
|
|
||||||
ctx.move_to(b.x1 as f64 + 5.0, b.y1 as f64 + 18.0);
|
ctx.move_to(b.x1 as f64 + 5.0, b.y1 as f64 + 18.0);
|
||||||
ctx.set_source_rgb(0.0, 0.0, 0.0);
|
ctx.set_source_rgb(0.0, 0.0, 0.0);
|
||||||
ctx.show_text(&format!("[{}]", b.id)).expect("Couldn't show text!");
|
ctx.show_text(&format!("[{}]", b.id))
|
||||||
|
.expect("Couldn't show text!");
|
||||||
|
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
.expect("GTK Cario context did not have a stroke!");
|
.expect("GTK Cario context did not have a stroke!");
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
use async_channel::Sender;
|
use async_channel::Sender;
|
||||||
use gtk::{glib, prelude::{BoxExt, ButtonExt, EditableExt, WidgetExt}, Box, Button, Entry};
|
use gtk::{
|
||||||
|
glib,
|
||||||
|
prelude::{BoxExt, ButtonExt, EditableExt, WidgetExt},
|
||||||
|
Box, Button, Entry,
|
||||||
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
use crate::{config::save_config, coordinator::ApplicationEvent};
|
use crate::{config::save_config, coordinator::ApplicationEvent};
|
||||||
|
@ -16,7 +20,6 @@ pub struct SocketPanel {
|
||||||
|
|
||||||
impl SocketPanel {
|
impl SocketPanel {
|
||||||
pub fn new(initial_settings: &crate::ui::AppState) -> SocketPanel {
|
pub fn new(initial_settings: &crate::ui::AppState) -> SocketPanel {
|
||||||
|
|
||||||
let content_box = Box::builder()
|
let content_box = Box::builder()
|
||||||
.orientation(gtk::Orientation::Vertical)
|
.orientation(gtk::Orientation::Vertical)
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
|
@ -64,7 +67,6 @@ impl SocketPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect_button_callback(&self, to_mec: Sender<ApplicationEvent>) {
|
pub fn connect_button_callback(&self, to_mec: Sender<ApplicationEvent>) {
|
||||||
|
|
||||||
let ip_entry = &self.ip_entry;
|
let ip_entry = &self.ip_entry;
|
||||||
let port_entry = &self.port_entry;
|
let port_entry = &self.port_entry;
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,73 @@
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use async_channel::Sender;
|
use async_channel::Sender;
|
||||||
use gtk::{prelude::{BoxExt, ButtonExt, ToggleButtonExt}, Box, Label, ToggleButton};
|
use gtk::{
|
||||||
use log::error;
|
glib::object::CastNone, prelude::{
|
||||||
|
BoxExt, ButtonExt, Cast, GObjectPropertyExpressionExt, ListItemExt, ListModelExt, ToggleButtonExt
|
||||||
use crate::coordinator::ApplicationEvent;
|
}, 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 {
|
pub struct TrackerPanel {
|
||||||
top_level: Box,
|
top_level: Box,
|
||||||
|
|
||||||
enable_disable: ToggleButton,
|
enable_disable: ToggleButton,
|
||||||
current_id: Label,
|
current_id: Label,
|
||||||
|
scrolled_window: ScrolledWindow,
|
||||||
|
|
||||||
|
pub items: StringList,
|
||||||
|
list_view: ListView,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackerPanel {
|
impl TrackerPanel {
|
||||||
pub fn new() -> TrackerPanel {
|
pub fn new(tracker_state: Arc<Mutex<TrackerState>>) -> TrackerPanel {
|
||||||
|
let factory = SignalListItemFactory::new();
|
||||||
|
factory.connect_setup(move |_, list_item| {
|
||||||
|
let list_item = list_item
|
||||||
|
.downcast_ref::<ListItem>()
|
||||||
|
.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::<StringObject>("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::<StringObject>();
|
||||||
|
|
||||||
|
if let Some(item) = item {
|
||||||
|
if let Ok(id) = item.string().parse::<u32>() {
|
||||||
|
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()
|
let top_level = Box::builder()
|
||||||
.orientation(gtk::Orientation::Vertical)
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
@ -28,22 +81,29 @@ impl TrackerPanel {
|
||||||
let enable_disable = ToggleButton::with_label("Enable Automatic Tracking");
|
let enable_disable = ToggleButton::with_label("Enable Automatic Tracking");
|
||||||
|
|
||||||
let current_id = Label::builder()
|
let current_id = Label::builder()
|
||||||
.label("")
|
.label("Not Tracking")
|
||||||
.can_focus(false)
|
.can_focus(false)
|
||||||
.can_target(false)
|
.can_target(false)
|
||||||
|
.css_classes(["current-id"])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
top_level.append(&enable_disable);
|
top_level.append(&enable_disable);
|
||||||
top_level.append(¤t_id);
|
top_level.append(¤t_id);
|
||||||
|
top_level.append(&scrolled_window);
|
||||||
|
|
||||||
TrackerPanel {
|
TrackerPanel {
|
||||||
top_level,
|
top_level,
|
||||||
|
|
||||||
enable_disable,
|
enable_disable,
|
||||||
current_id,
|
current_id,
|
||||||
|
scrolled_window,
|
||||||
|
|
||||||
|
items,
|
||||||
|
list_view,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn get_top_level(&self) -> &Box {
|
pub fn get_top_level(&self) -> &Box {
|
||||||
&self.top_level
|
&self.top_level
|
||||||
}
|
}
|
||||||
|
@ -51,7 +111,9 @@ impl TrackerPanel {
|
||||||
pub fn connect_button_callback(&self, to_mec: Sender<ApplicationEvent>) {
|
pub fn connect_button_callback(&self, to_mec: Sender<ApplicationEvent>) {
|
||||||
|
|
||||||
self.enable_disable.connect_clicked(move |button| {
|
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}");
|
error!("Could not send message to the MEC: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
14
style.css
14
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 {
|
entry {
|
||||||
font-size: 16pt;
|
font-size: 16pt;
|
||||||
|
@ -27,5 +39,5 @@ label.JoystickCurrent {
|
||||||
|
|
||||||
button {
|
button {
|
||||||
color: black;
|
color: black;
|
||||||
font-size: 16pt;
|
font-size: 10pt;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue