Compare commits
2 commits
81a4f04d6b
...
8924d2ab8f
Author | SHA1 | Date | |
---|---|---|---|
|
8924d2ab8f | ||
|
a37f5ed266 |
5 changed files with 159 additions and 73 deletions
|
@ -17,9 +17,9 @@ use tokio_tungstenite::{tungstenite::Message, MaybeTlsStream, WebSocketStream};
|
||||||
use tracing::{debug, error, info, instrument};
|
use tracing::{debug, error, info, instrument};
|
||||||
|
|
||||||
mod coord_state;
|
mod coord_state;
|
||||||
|
mod perf_state;
|
||||||
mod process_box_string;
|
mod process_box_string;
|
||||||
mod remote_video_processor;
|
mod remote_video_processor;
|
||||||
mod perf_state;
|
|
||||||
|
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::remote_sources::TrackerState;
|
use crate::remote_sources::TrackerState;
|
||||||
|
@ -63,7 +63,15 @@ pub async fn start_coordinator(
|
||||||
|
|
||||||
let mec = pin!(mec);
|
let mec = pin!(mec);
|
||||||
|
|
||||||
let mut state = CoordState::new(mec, to_mec, to_gui, runtime, tracker_state, settings, tracker_header);
|
let mut state = CoordState::new(
|
||||||
|
mec,
|
||||||
|
to_mec,
|
||||||
|
to_gui,
|
||||||
|
runtime,
|
||||||
|
tracker_state,
|
||||||
|
settings,
|
||||||
|
tracker_header,
|
||||||
|
);
|
||||||
|
|
||||||
state
|
state
|
||||||
.pipeline
|
.pipeline
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{sync, time::Duration, collections::VecDeque};
|
use std::{collections::VecDeque, sync, time::Duration};
|
||||||
|
|
||||||
const MAX_RECORDED_TIMES: usize = 10;
|
const MAX_RECORDED_TIMES: usize = 10;
|
||||||
const DEGRADED_TRACKER_TIME: u128 = 100;
|
const DEGRADED_TRACKER_TIME: u128 = 100;
|
||||||
|
@ -7,51 +7,76 @@ const DEGRADED_TRACKER_TIME: u128 = 100;
|
||||||
pub struct TrackerMetrics {
|
pub struct TrackerMetrics {
|
||||||
pub header_text: sync::Arc<sync::RwLock<String>>,
|
pub header_text: sync::Arc<sync::RwLock<String>>,
|
||||||
tracker_times: VecDeque<u128>,
|
tracker_times: VecDeque<u128>,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackerMetrics {
|
impl TrackerMetrics {
|
||||||
pub fn new(text_reference: sync::Arc<sync::RwLock<String>>) -> Self {
|
pub fn new(text_reference: sync::Arc<sync::RwLock<String>>) -> Self {
|
||||||
TrackerMetrics {
|
let mut ret = TrackerMetrics {
|
||||||
header_text: text_reference,
|
header_text: text_reference,
|
||||||
|
|
||||||
tracker_times: VecDeque::with_capacity(MAX_RECORDED_TIMES),
|
tracker_times: VecDeque::with_capacity(MAX_RECORDED_TIMES),
|
||||||
|
};
|
||||||
|
ret.clear_times();
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn starting_connection(&mut self, fail_count: Option<usize>) {
|
||||||
|
self.clear_times();
|
||||||
|
|
||||||
|
if let Ok(mut writer) = self.header_text.write() {
|
||||||
|
writer.clear();
|
||||||
|
match fail_count {
|
||||||
|
None => writer.push_str("Status: Connecting ..."),
|
||||||
|
Some(v) => writer.push_str(&format!("Status: Attempt {}/5", v)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_times(&mut self) {
|
pub fn clear_times(&mut self) {
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
self.tracker_times.pop_front();
|
self.tracker_times.pop_front();
|
||||||
}
|
}
|
||||||
self.insert_time(Duration::new(0, 0));
|
self.insert_time(Duration::new(0, 0));
|
||||||
|
if let Ok(mut writer) = self.header_text.write() {
|
||||||
|
writer.clear();
|
||||||
|
writer.push_str("Status: Disconnected");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_time(&mut self, new_measurement: Duration) {
|
pub fn insert_time(&mut self, new_measurement: Duration) {
|
||||||
if self.tracker_times.len() > MAX_RECORDED_TIMES {
|
if self.tracker_times.len() > MAX_RECORDED_TIMES {
|
||||||
let _ = self.tracker_times.pop_front();
|
let _ = self.tracker_times.pop_front();
|
||||||
}
|
}
|
||||||
self.tracker_times.push_back(new_measurement.as_millis());
|
self.tracker_times.push_back(new_measurement.as_millis());
|
||||||
|
|
||||||
let avg_time = self.tracker_times.iter().sum::<u128>() / self.tracker_times.len() as u128;
|
let avg_time = self.tracker_times.iter().sum::<u128>() / self.tracker_times.len() as u128;
|
||||||
|
|
||||||
if avg_time == 0 {
|
if avg_time == 0 {
|
||||||
if let Ok(mut writer) = self.header_text.write() {
|
if let Ok(mut writer) = self.header_text.write() {
|
||||||
writer.clear();
|
writer.clear();
|
||||||
writer.push_str(&format!("Status: Failed Avg Response: {} ms", avg_time.to_string()));
|
writer.push_str(&format!(
|
||||||
|
"Status: Failed Avg Response: {} ms",
|
||||||
|
avg_time.to_string()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if avg_time > DEGRADED_TRACKER_TIME {
|
if avg_time > DEGRADED_TRACKER_TIME {
|
||||||
if let Ok(mut writer) = self.header_text.write() {
|
if let Ok(mut writer) = self.header_text.write() {
|
||||||
writer.clear();
|
writer.clear();
|
||||||
writer.push_str(&format!("Status: Degraded Avg Response: {} ms", avg_time.to_string()));
|
writer.push_str(&format!(
|
||||||
|
"Status: Degraded Avg Response: {} ms",
|
||||||
|
avg_time.to_string()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Ok(mut writer) = self.header_text.write() {
|
if let Ok(mut writer) = self.header_text.write() {
|
||||||
writer.clear();
|
writer.clear();
|
||||||
writer.push_str(&format!("Status: Nominal Avg Response: {} ms", avg_time.to_string()));
|
writer.push_str(&format!(
|
||||||
|
"Status: Nominal Avg Response: {} ms",
|
||||||
|
avg_time.to_string()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,10 @@ use tracing::{error, info, instrument};
|
||||||
|
|
||||||
use crate::remote_sources::TrackerState;
|
use crate::remote_sources::TrackerState;
|
||||||
|
|
||||||
use super::{perf_state::TrackerMetrics, process_box_string::process_incoming_string, ApplicationEvent, SocketState};
|
use super::{
|
||||||
|
perf_state::TrackerMetrics, process_box_string::process_incoming_string, ApplicationEvent,
|
||||||
|
SocketState,
|
||||||
|
};
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub async fn remote_video_loop(
|
pub async fn remote_video_loop(
|
||||||
|
@ -34,16 +37,33 @@ pub async fn remote_video_loop(
|
||||||
"Starting remote tracker processing connection to: {}",
|
"Starting remote tracker processing connection to: {}",
|
||||||
conn_string
|
conn_string
|
||||||
);
|
);
|
||||||
|
|
||||||
let video_info =
|
let video_info =
|
||||||
gstreamer_video::VideoInfo::builder(gstreamer_video::VideoFormat::Rgb, 640, 480)
|
gstreamer_video::VideoInfo::builder(gstreamer_video::VideoFormat::Rgb, 640, 480)
|
||||||
.build()
|
.build()
|
||||||
.expect("Could not build video info!");
|
.expect("Could not build video info!");
|
||||||
|
|
||||||
|
let mut fail_count = 0;
|
||||||
|
{
|
||||||
|
let mut tm = tracker_metrics.lock().await;
|
||||||
|
tm.starting_connection(None);
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
socket_state.is_connected.store(true, Ordering::SeqCst);
|
socket_state.is_connected.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
match connect_async(&conn_string).await {
|
match connect_async(&conn_string).await {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
fail_count += 1;
|
||||||
|
{
|
||||||
|
let mut tm = tracker_metrics.lock().await;
|
||||||
|
tm.starting_connection(Some(fail_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if fail_count >= 5 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
error!("Could not connect to remote video loop! Trying again in 1 seconds: {e}");
|
error!("Could not connect to remote video loop! Trying again in 1 seconds: {e}");
|
||||||
sleep_until(Instant::now() + Duration::from_secs(1)).await;
|
sleep_until(Instant::now() + Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
|
@ -56,7 +76,7 @@ pub async fn remote_video_loop(
|
||||||
tracker_state.clone(),
|
tracker_state.clone(),
|
||||||
socket_state.clone(),
|
socket_state.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut last_iter: Instant;
|
let mut last_iter: Instant;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -105,7 +125,7 @@ pub async fn remote_video_loop(
|
||||||
info!("Shutting down remote video loop");
|
info!("Shutting down remote video loop");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut tm = tracker_metrics.lock().await;
|
let mut tm = tracker_metrics.lock().await;
|
||||||
tm.insert_time(Instant::now() - last_iter);
|
tm.insert_time(Instant::now() - last_iter);
|
||||||
|
@ -117,10 +137,15 @@ pub async fn remote_video_loop(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !socket_state.stay_connected.load(Ordering::SeqCst) {
|
if !socket_state.stay_connected.load(Ordering::SeqCst) {
|
||||||
info!("Shutting down remote video loop");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
info!("Shutting down remote video loop");
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut tm = tracker_metrics.lock().await;
|
||||||
|
tm.clear_times();
|
||||||
|
}
|
||||||
socket_state.is_connected.store(false, Ordering::SeqCst);
|
socket_state.is_connected.store(false, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +191,7 @@ async fn listen_to_messages(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
"Stopping tracker connection listen with keep alive: {}",
|
"Stopping tracker listen connection with keep alive: {}",
|
||||||
socket_state.stay_connected.load(Ordering::SeqCst)
|
socket_state.stay_connected.load(Ordering::SeqCst)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use gtk::{gdk::Paintable, prelude::BoxExt, AspectFrame, Box, DrawingArea, Label, Overlay, Picture};
|
use gtk::{
|
||||||
|
gdk::Paintable, prelude::BoxExt, AspectFrame, Box, DrawingArea, Label, Overlay, Picture,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct LiveViewPanel {
|
pub struct LiveViewPanel {
|
||||||
top_level: gtk::Box,
|
top_level: gtk::Box,
|
||||||
|
|
||||||
pub tracker_status_label: Label,
|
pub tracker_status_label: Label,
|
||||||
pub cam_status_label: Label,
|
pub cam_status_label: Label,
|
||||||
pub adjustment_label: Label,
|
pub adjustment_label: Label,
|
||||||
|
@ -13,22 +14,22 @@ pub struct LiveViewPanel {
|
||||||
|
|
||||||
impl LiveViewPanel {
|
impl LiveViewPanel {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
|
||||||
let right_box = gtk::Box::builder()
|
let right_box = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Vertical)
|
.orientation(gtk::Orientation::Vertical)
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.valign(gtk::Align::Center)
|
.valign(gtk::Align::Center)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let tracker_status_label = Label::builder()
|
let tracker_status_label = Label::builder()
|
||||||
.label("No Status Yet".to_string())
|
.label("No Status Yet".to_string())
|
||||||
.can_focus(true)
|
.can_focus(true)
|
||||||
.css_classes(vec!["large-label"])
|
.css_classes(vec!["large-label", "NoConnection"])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// let conn_status_label = Label::new(Some(&"No Connection".to_string()));
|
// let conn_status_label = Label::new(Some(&"No Connection".to_string()));
|
||||||
let cam_status_label = Label::builder()
|
let cam_status_label = Label::builder()
|
||||||
.label("No Connection".to_string())
|
.label("No Connection".to_string())
|
||||||
|
.css_classes(vec!["NoConnection"])
|
||||||
.can_focus(true)
|
.can_focus(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ impl LiveViewPanel {
|
||||||
right_box.append(&aspect);
|
right_box.append(&aspect);
|
||||||
right_box.append(&cam_status_label);
|
right_box.append(&cam_status_label);
|
||||||
right_box.append(&adjustment_label);
|
right_box.append(&adjustment_label);
|
||||||
|
|
||||||
LiveViewPanel {
|
LiveViewPanel {
|
||||||
adjustment_label,
|
adjustment_label,
|
||||||
tracker_status_label,
|
tracker_status_label,
|
||||||
|
@ -63,16 +64,16 @@ impl LiveViewPanel {
|
||||||
top_level: right_box,
|
top_level: right_box,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_top_level(&self) -> &Box {
|
pub fn get_top_level(&self) -> &Box {
|
||||||
&self.top_level
|
&self.top_level
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_drawable(&self, new_drawable: &DrawingArea) {
|
pub fn set_drawable(&self, new_drawable: &DrawingArea) {
|
||||||
self.overlay.add_overlay(new_drawable);
|
self.overlay.add_overlay(new_drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_paintable(&self, new_paintable: &Paintable) {
|
pub fn set_paintable(&self, new_paintable: &Paintable) {
|
||||||
self.picture.set_paintable(Some(new_paintable));
|
self.picture.set_paintable(Some(new_paintable));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
107
src/ui/mod.rs
107
src/ui/mod.rs
|
@ -16,8 +16,8 @@ use crate::coordinator::{start_coordinator, ApplicationEvent, MoveEvent};
|
||||||
use crate::remote_sources::TrackerState;
|
use crate::remote_sources::TrackerState;
|
||||||
|
|
||||||
mod control_panel;
|
mod control_panel;
|
||||||
mod settings_modal;
|
|
||||||
mod liveview_panel;
|
mod liveview_panel;
|
||||||
|
mod settings_modal;
|
||||||
|
|
||||||
use control_panel::ControlPanel;
|
use control_panel::ControlPanel;
|
||||||
use liveview_panel::LiveViewPanel;
|
use liveview_panel::LiveViewPanel;
|
||||||
|
@ -152,7 +152,7 @@ pub fn build_ui(app: &Application, config: Arc<RwLock<AppConfig>>, runtime: Hand
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let coord_config = config.clone();
|
let coord_config = config.clone();
|
||||||
|
|
||||||
let tracker_header = Arc::new(std::sync::RwLock::new(String::new()));
|
let tracker_header = Arc::new(std::sync::RwLock::new(String::new()));
|
||||||
|
|
||||||
runtime.spawn(start_coordinator(
|
runtime.spawn(start_coordinator(
|
||||||
|
@ -165,7 +165,7 @@ pub fn build_ui(app: &Application, config: Arc<RwLock<AppConfig>>, runtime: Hand
|
||||||
tracker_header.clone(),
|
tracker_header.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
let control_panel = Arc::new(ControlPanel::new(tracker_state.clone()));
|
let control_panel = ControlPanel::new(tracker_state.clone());
|
||||||
control_panel.connect_button_callbacks(to_mec.clone());
|
control_panel.connect_button_callbacks(to_mec.clone());
|
||||||
|
|
||||||
// left_box.append(&conn_status_label);
|
// left_box.append(&conn_status_label);
|
||||||
|
@ -221,52 +221,79 @@ pub fn build_ui(app: &Application, config: Arc<RwLock<AppConfig>>, runtime: Hand
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
glib::spawn_future_local(
|
let tracker_status_label = liveview_panel.tracker_status_label.clone();
|
||||||
glib::clone!(@weak drawable => async move {
|
let tracker_enable_toggle = control_panel
|
||||||
while let Ok(d) = gui_recv.recv().await {
|
.connection_buttons
|
||||||
drawable.queue_draw();
|
.tracker_enable_toggle
|
||||||
if let Ok(reader) = tracker_header.read() {
|
.clone();
|
||||||
liveview_panel.tracker_status_label.set_text(reader.as_str());
|
|
||||||
|
glib::timeout_add_seconds_local(1, move || {
|
||||||
|
if let Ok(reader) = tracker_header.read() {
|
||||||
|
tracker_status_label.set_text(reader.as_str());
|
||||||
|
if reader.contains("Failed") || reader.contains("Disconnected") {
|
||||||
|
tracker_status_label.set_css_classes(&["NoConnection"]);
|
||||||
|
|
||||||
|
tracker_enable_toggle.set_label("Press to Connect Tracker");
|
||||||
|
tracker_enable_toggle.set_active(false);
|
||||||
|
} else if reader.contains("Degraded") || reader.contains("Connecting") {
|
||||||
|
tracker_status_label.set_css_classes(&["LoadingConnection"]);
|
||||||
|
|
||||||
|
tracker_enable_toggle.set_label("Press to Connect");
|
||||||
|
tracker_enable_toggle.set_active(false);
|
||||||
|
} else if reader.contains("Nominal") {
|
||||||
|
tracker_status_label.set_css_classes(&["YesConnection"]);
|
||||||
|
|
||||||
|
tracker_enable_toggle.set_label("Press to Disconnect");
|
||||||
|
tracker_enable_toggle.set_active(true);
|
||||||
|
}
|
||||||
|
glib::ControlFlow::Continue
|
||||||
|
} else {
|
||||||
|
error!("Couldn't get rwlock on metrics");
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
glib::spawn_future_local(glib::clone!(@weak drawable => async move {
|
||||||
|
while let Ok(d) = gui_recv.recv().await {
|
||||||
|
drawable.queue_draw();
|
||||||
|
match d {
|
||||||
|
GuiUpdate::MoveEvent(msg) => {
|
||||||
|
liveview_panel.adjustment_label.set_text(
|
||||||
|
format!("X: {:>4} Y: {:>4}", msg.x, msg.y).as_str()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
match d {
|
GuiUpdate::SocketConnected => {
|
||||||
GuiUpdate::MoveEvent(msg) => {
|
control_panel.connection_buttons.camera_connection.set_sensitive(true);
|
||||||
liveview_panel.adjustment_label.set_text(
|
control_panel.connection_buttons.camera_connection.set_label("Press to Disconnect");
|
||||||
format!("X: {:>4} Y: {:>4}", msg.x, msg.y).as_str()
|
liveview_panel.cam_status_label.set_label("Connected");
|
||||||
);
|
|
||||||
}
|
|
||||||
GuiUpdate::SocketConnected => {
|
|
||||||
control_panel.connection_buttons.camera_connection.set_sensitive(true);
|
|
||||||
control_panel.connection_buttons.camera_connection.set_label("Press to Disconnect");
|
|
||||||
liveview_panel.cam_status_label.set_label("Connected");
|
|
||||||
|
|
||||||
liveview_panel.cam_status_label.set_css_classes(&["YesConnection"]);
|
liveview_panel.cam_status_label.set_css_classes(&["YesConnection"]);
|
||||||
|
|
||||||
},
|
},
|
||||||
GuiUpdate::SocketConnecting => {
|
GuiUpdate::SocketConnecting => {
|
||||||
control_panel.connection_buttons.camera_connection.set_sensitive(false);
|
control_panel.connection_buttons.camera_connection.set_sensitive(false);
|
||||||
control_panel.connection_buttons.camera_connection.set_label("Please wait");
|
control_panel.connection_buttons.camera_connection.set_label("Please wait");
|
||||||
liveview_panel.cam_status_label.set_label("Connecting");
|
liveview_panel.cam_status_label.set_label("Connecting");
|
||||||
|
|
||||||
liveview_panel.cam_status_label.set_css_classes(&["LoadingConnection"]);
|
liveview_panel.cam_status_label.set_css_classes(&["LoadingConnection"]);
|
||||||
|
|
||||||
},
|
},
|
||||||
GuiUpdate::SocketDisconnected => {
|
GuiUpdate::SocketDisconnected => {
|
||||||
control_panel.connection_buttons.camera_connection.set_sensitive(true);
|
control_panel.connection_buttons.camera_connection.set_sensitive(true);
|
||||||
control_panel.connection_buttons.camera_connection.set_label("Press to Connect to Camera");
|
control_panel.connection_buttons.camera_connection.set_label("Press to Connect to Camera");
|
||||||
liveview_panel.cam_status_label.set_label("Not Connected to Camera");
|
liveview_panel.cam_status_label.set_label("Not Connected to Camera");
|
||||||
|
|
||||||
liveview_panel.cam_status_label.set_css_classes(&["NoConnection"]);
|
liveview_panel.cam_status_label.set_css_classes(&["NoConnection"]);
|
||||||
}
|
}
|
||||||
GuiUpdate::UpdatePaintable(sink) => {
|
GuiUpdate::UpdatePaintable(sink) => {
|
||||||
let paintable = sink.property::<Paintable>("paintable");
|
let paintable = sink.property::<Paintable>("paintable");
|
||||||
|
|
||||||
liveview_panel.set_paintable(&paintable);
|
liveview_panel.set_paintable(&paintable);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!("Closing update loop");
|
}
|
||||||
}),
|
info!("Closing update loop");
|
||||||
);
|
}));
|
||||||
|
|
||||||
window.connect_close_request(move |_| glib::Propagation::Proceed);
|
window.connect_close_request(move |_| glib::Propagation::Proceed);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue