mod imp; use std::collections::HashMap; use adw::prelude::*; use adw::subclass::prelude::*; use glib::{clone, Object}; use gtk::{ gio, glib::{self, closure_local}, NoSelection, }; use lan_mouse_ipc::{ ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position, DEFAULT_PORT, }; use crate::{fingerprint_window::FingerprintWindow, key_object::KeyObject, key_row::KeyRow}; use super::{client_object::ClientObject, client_row::ClientRow}; glib::wrapper! { pub struct Window(ObjectSubclass) @extends adw::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub(super) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self { let window: Self = Object::builder().property("application", app).build(); window .imp() .frontend_request_writer .borrow_mut() .replace(conn); window } fn clients(&self) -> gio::ListStore { self.imp() .clients .borrow() .clone() .expect("Could not get clients") } fn authorized(&self) -> gio::ListStore { self.imp() .authorized .borrow() .clone() .expect("Could not get authorized") } fn client_by_idx(&self, idx: u32) -> Option { self.clients().item(idx).map(|o| o.downcast().unwrap()) } fn authorized_by_idx(&self, idx: u32) -> Option { self.authorized().item(idx).map(|o| o.downcast().unwrap()) } fn row_by_idx(&self, idx: i32) -> Option { self.imp() .client_list .get() .row_at_index(idx) .map(|o| o.downcast().expect("expected ClientRow")) } fn setup_authorized(&self) { let store = gio::ListStore::new::(); self.imp().authorized.replace(Some(store)); let selection_model = NoSelection::new(Some(self.authorized())); self.imp().authorized_list.bind_model( Some(&selection_model), clone!( #[weak(rename_to = window)] self, #[upgrade_or_panic] move |obj| { let key_obj = obj.downcast_ref().expect("object of type `KeyObject`"); let row = window.create_key_row(key_obj); row.connect_closure( "request-delete", false, closure_local!( #[strong] window, move |row: KeyRow| { if let Some(key_obj) = window.authorized_by_idx(row.index() as u32) { window.request_fingerprint_remove(key_obj.get_fingerprint()); } } ), ); row.upcast() } ), ) } fn setup_clients(&self) { let model = gio::ListStore::new::(); self.imp().clients.replace(Some(model)); let selection_model = NoSelection::new(Some(self.clients())); self.imp().client_list.bind_model( Some(&selection_model), clone!( #[weak(rename_to = window)] self, #[upgrade_or_panic] move |obj| { let client_object = obj .downcast_ref() .expect("Expected object of type `ClientObject`."); let row = window.create_client_row(client_object); row.connect_closure( "request-hostname-change", false, closure_local!( #[strong] window, move |row: ClientRow, hostname: String| { log::info!("request-hostname-change"); if let Some(client) = window.client_by_idx(row.index() as u32) { let hostname = Some(hostname).filter(|s| !s.is_empty()); /* changed in response to FrontendEvent * -> do not request additional update */ window.request(FrontendRequest::UpdateHostname( client.handle(), hostname, )); } } ), ); row.connect_closure( "request-port-change", false, closure_local!( #[strong] window, move |row: ClientRow, port: u32| { if let Some(client) = window.client_by_idx(row.index() as u32) { window.request(FrontendRequest::UpdatePort( client.handle(), port as u16, )); } } ), ); row.connect_closure( "request-activate", false, closure_local!( #[strong] window, move |row: ClientRow, active: bool| { if let Some(client) = window.client_by_idx(row.index() as u32) { log::info!( "request: {} client", if active { "activating" } else { "deactivating" } ); window.request(FrontendRequest::Activate( client.handle(), active, )); } } ), ); row.connect_closure( "request-delete", false, closure_local!( #[strong] window, move |row: ClientRow| { if let Some(client) = window.client_by_idx(row.index() as u32) { window.request(FrontendRequest::Delete(client.handle())); } } ), ); row.connect_closure( "request-dns", false, closure_local!( #[strong] window, move |row: ClientRow| { if let Some(client) = window.client_by_idx(row.index() as u32) { window.request(FrontendRequest::ResolveDns( client.get_data().handle, )); } } ), ); row.connect_closure( "request-position-change", false, closure_local!( #[strong] window, move |row: ClientRow, pos_idx: u32| { if let Some(client) = window.client_by_idx(row.index() as u32) { let position = match pos_idx { 0 => Position::Left, 1 => Position::Right, 2 => Position::Top, _ => Position::Bottom, }; window.request(FrontendRequest::UpdatePosition( client.handle(), position, )); } } ), ); row.upcast() } ), ); } fn setup_icon(&self) { self.set_icon_name(Some("de.feschber.LanMouse")); } /// workaround for a bug in libadwaita that shows an ugly line beneath /// the last element if a placeholder is set. /// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308 fn update_placeholder_visibility(&self) { let visible = self.clients().n_items() == 0; let placeholder = self.imp().client_placeholder.get(); self.imp().client_list.set_placeholder(match visible { true => Some(&placeholder), false => None, }); } fn update_auth_placeholder_visibility(&self) { let visible = self.authorized().n_items() == 0; let placeholder = self.imp().authorized_placeholder.get(); self.imp().authorized_list.set_placeholder(match visible { true => Some(&placeholder), false => None, }); } fn create_client_row(&self, client_object: &ClientObject) -> ClientRow { let row = ClientRow::new(client_object); row.bind(client_object); row } fn create_key_row(&self, key_object: &KeyObject) -> KeyRow { let row = KeyRow::new(); row.bind(key_object); row } pub(super) fn new_client( &self, handle: ClientHandle, client: ClientConfig, state: ClientState, ) { let client = ClientObject::new(handle, client, state.clone()); self.clients().append(&client); self.update_placeholder_visibility(); self.update_dns_state(handle, !state.ips.is_empty()); } pub(super) fn update_client_list( &self, clients: Vec<(ClientHandle, ClientConfig, ClientState)>, ) { for (handle, client, state) in clients { if self.client_idx(handle).is_some() { self.update_client_config(handle, client); self.update_client_state(handle, state); } else { self.new_client(handle, client, state); } } } pub(super) fn update_port(&self, port: u16, msg: Option) { if let Some(msg) = msg { self.show_toast(msg.as_str()); } self.imp().set_port(port); } fn client_idx(&self, handle: ClientHandle) -> Option { self.clients() .iter::() .position(|c| c.ok().map(|c| c.handle() == handle).unwrap_or_default()) } pub(super) fn delete_client(&self, handle: ClientHandle) { let Some(idx) = self.client_idx(handle) else { log::warn!("could not find client with handle {handle}"); return; }; self.clients().remove(idx as u32); if self.clients().n_items() == 0 { self.update_placeholder_visibility(); } } pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) { let Some(row) = self.row_for_handle(handle) else { log::warn!("could not find row for handle {}", handle); return; }; row.set_hostname(client.hostname); row.set_port(client.port); row.set_position(client.pos); } pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) { let Some(row) = self.row_for_handle(handle) else { log::warn!("could not find row for handle {}", handle); return; }; let Some(client_object) = self.client_object_for_handle(handle) else { log::warn!("could not find row for handle {}", handle); return; }; /* activation state */ row.set_active(state.active); /* dns state */ client_object.set_resolving(state.resolving); self.update_dns_state(handle, !state.ips.is_empty()); let ips = state .ips .into_iter() .map(|ip| ip.to_string()) .collect::>(); client_object.set_ips(ips); } fn client_object_for_handle(&self, handle: ClientHandle) -> Option { self.client_idx(handle) .and_then(|i| self.client_by_idx(i as u32)) } fn row_for_handle(&self, handle: ClientHandle) -> Option { self.client_idx(handle) .and_then(|i| self.row_by_idx(i as i32)) } fn update_dns_state(&self, handle: ClientHandle, resolved: bool) { if let Some(client_row) = self.row_for_handle(handle) { client_row.set_dns_state(resolved); } } fn request_port_change(&self) { let port = self .imp() .port_entry .get() .text() .as_str() .parse::() .unwrap_or(DEFAULT_PORT); self.request(FrontendRequest::ChangePort(port)); } fn request_capture(&self) { self.request(FrontendRequest::EnableCapture); } fn request_emulation(&self) { self.request(FrontendRequest::EnableEmulation); } fn request_client_create(&self) { self.request(FrontendRequest::Create); } fn open_fingerprint_dialog(&self) { let window = FingerprintWindow::new(); window.set_transient_for(Some(self)); window.connect_closure( "confirm-clicked", false, closure_local!( #[strong(rename_to = parent)] self, move |w: FingerprintWindow, desc: String, fp: String| { parent.request_fingerprint_add(desc, fp); w.close(); } ), ); window.present(); } fn request_fingerprint_add(&self, desc: String, fp: String) { self.request(FrontendRequest::AuthorizeKey(desc, fp)); } fn request_fingerprint_remove(&self, fp: String) { self.request(FrontendRequest::RemoveAuthorizedKey(fp)); } fn request(&self, request: FrontendRequest) { let mut requester = self.imp().frontend_request_writer.borrow_mut(); let requester = requester.as_mut().unwrap(); if let Err(e) = requester.request(request) { log::error!("error sending message: {e}"); }; } pub(super) fn show_toast(&self, msg: &str) { let toast = adw::Toast::new(msg); let toast_overlay = &self.imp().toast_overlay; toast_overlay.add_toast(toast); } pub(super) fn set_capture(&self, active: bool) { self.imp().capture_active.replace(active); self.update_capture_emulation_status(); } pub(super) fn set_emulation(&self, active: bool) { self.imp().emulation_active.replace(active); self.update_capture_emulation_status(); } fn update_capture_emulation_status(&self) { let capture = self.imp().capture_active.get(); let emulation = self.imp().emulation_active.get(); self.imp().capture_status_row.set_visible(!capture); self.imp().emulation_status_row.set_visible(!emulation); self.imp() .capture_emulation_group .set_visible(!capture || !emulation); } pub(super) fn set_authorized_keys(&self, fingerprints: HashMap) { let authorized = self.authorized(); // clear list authorized.remove_all(); // insert fingerprints for (fingerprint, description) in fingerprints { let key_obj = KeyObject::new(description, fingerprint); authorized.append(&key_obj); } self.update_auth_placeholder_visibility(); } pub(super) fn set_pk_fp(&self, fingerprint: &str) { self.imp().fingerprint_row.set_subtitle(fingerprint); } }