impl fingerprint ui logic

This commit is contained in:
Ferdinand Schober
2024-09-27 14:21:36 +02:00
parent 0038178f0d
commit 1c7490c58d
16 changed files with 313 additions and 24 deletions

View File

@@ -274,6 +274,12 @@ impl Cli {
FrontendEvent::EmulationStatus(s) => {
eprintln!("emulation status: {s:?}")
}
FrontendEvent::AuthorizedUpdated(keys) => {
eprintln!("authorized keys changed:");
for key in keys {
eprintln!("{key}");
}
}
}
}

View File

@@ -47,7 +47,8 @@
<property name="orientation">vertical</property>
<property name="halign">center</property>
<child>
<object class="GtkButton">
<object class="GtkButton" id="confirm_button">
<signal name="clicked" handler="handle_confirm" swapped="true"/>
<property name="label" translatable="yes">Confirm</property>
<property name="can-shrink">True</property>
<style>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="KeyRow" parent="AdwActionRow">
<property name="title">hostname</property>
<child type="prefix">
<object class="GtkButton" id="delete_button">
<property name="valign">center</property>
<property name="halign">end</property>
<property name="tooltip-text" translatable="yes">enable</property>
<property name="icon-name">edit-delete-symbolic</property>
<style>
<class name="flat"/>
</style>
</object>
</child>
</template>
</interface>

View File

@@ -4,6 +4,7 @@
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">fingerprint_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">key_row.ui</file>
</gresource>
<gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>

View File

@@ -240,7 +240,7 @@
<signal name="clicked" handler="handle_add_cert_fingerprint" swapped="true"/>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">list-add-symbolic</property>
<property name="icon-name">auth-fingerprint-symbolic</property>
<property name="label" translatable="yes">Add</property>
</object>
</property>
@@ -250,10 +250,10 @@
</object>
</property>
<child>
<object class="GtkListBox" id="fingerprint_list">
<object class="GtkListBox" id="authorized_list">
<property name="selection-mode">none</property>
<child type="placeholder">
<object class="AdwActionRow">
<object class="AdwActionRow" id="authorized_placeholder">
<property name="title">no fingerprints!</property>
<property name="subtitle">add a public key fingerprint via the + button</property>
</object>

View File

@@ -1,13 +1,7 @@
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
use gtk::{
gio,
glib::{self, closure_local},
ListBox, NoSelection,
};
use glib::Object;
use gtk::{gio, glib};
glib::wrapper! {
pub struct FingerprintWindow(ObjectSubclass<imp::FingerprintWindow>)

View File

@@ -1,12 +1,20 @@
use std::sync::OnceLock;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::subclass::InitializingObject;
use gtk::{glib, template_callbacks, CompositeTemplate, Entry};
use gtk::{
glib::{self, subclass::Signal},
template_callbacks, Button, CompositeTemplate, Text,
};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/fingerprint_window.ui")]
pub struct FingerprintWindow {
// #[template_child]
// pub fingerprint_entry: TemplateChild<Entry>,
#[template_child]
pub text: TemplateChild<Text>,
#[template_child]
pub confirm_button: TemplateChild<Button>,
}
#[glib::object_subclass]
@@ -29,13 +37,21 @@ impl ObjectSubclass for FingerprintWindow {
#[template_callbacks]
impl FingerprintWindow {
// #[template_callback]
// fn handle_confirm() {}
#[template_callback]
fn handle_confirm(&self, _button: Button) {
let fp = self.text.text().to_string();
self.obj().emit_by_name("confirm-clicked", &[&fp])
}
}
impl ObjectImpl for FingerprintWindow {
fn constructed(&self) {
self.parent_constructed();
fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![Signal::builder("confirm-clicked")
.param_types([String::static_type()])
.build()]
})
}
}

View File

@@ -0,0 +1,18 @@
mod imp;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
glib::wrapper! {
pub struct KeyObject(ObjectSubclass<imp::KeyObject>);
}
impl KeyObject {
pub fn new(fp: String) -> Self {
Object::builder().property("fingerprint", fp).build()
}
pub fn get_fingerprint(&self) -> String {
self.imp().fingerprint.borrow().clone()
}
}

View File

@@ -0,0 +1,22 @@
use std::cell::RefCell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
#[derive(Properties, Default)]
#[properties(wrapper_type = super::KeyObject)]
pub struct KeyObject {
#[property(name = "fingerprint", get, set, type = String)]
pub fingerprint: RefCell<String>,
}
#[glib::object_subclass]
impl ObjectSubclass for KeyObject {
const NAME: &'static str = "KeyObject";
type Type = super::KeyObject;
}
#[glib::derived_properties]
impl ObjectImpl for KeyObject {}

View File

@@ -0,0 +1,36 @@
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use super::KeyObject;
glib::wrapper! {
pub struct KeyRow(ObjectSubclass<imp::KeyRow>)
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}
impl KeyRow {
pub fn new() -> Self {
Object::builder().build()
}
pub fn bind(&self, key_object: &KeyObject) {
let mut bindings = self.imp().bindings.borrow_mut();
let title_binding = key_object
.bind_property("fingerprint", self, "title")
.sync_create()
.build();
bindings.push(title_binding);
}
pub fn unbind(&self) {
for binding in self.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}

View File

@@ -0,0 +1,68 @@
use std::cell::RefCell;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow};
use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone;
use gtk::glib::subclass::Signal;
use gtk::{glib, Button, CompositeTemplate};
use std::sync::OnceLock;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/key_row.ui")]
pub struct KeyRow {
#[template_child]
pub delete_button: TemplateChild<gtk::Button>,
pub bindings: RefCell<Vec<Binding>>,
}
#[glib::object_subclass]
impl ObjectSubclass for KeyRow {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "KeyRow";
const ABSTRACT: bool = false;
type Type = super::KeyRow;
type ParentType = ActionRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for KeyRow {
fn constructed(&self) {
self.parent_constructed();
self.delete_button.connect_clicked(clone!(
#[weak(rename_to = row)]
self,
move |button| {
row.handle_delete(button);
}
));
}
fn signals() -> &'static [glib::subclass::Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| vec![Signal::builder("request-delete").build()])
}
}
#[gtk::template_callbacks]
impl KeyRow {
#[template_callback]
fn handle_delete(&self, _button: &Button) {
self.obj().emit_by_name::<()>("request-delete", &[]);
}
}
impl WidgetImpl for KeyRow {}
impl BoxImpl for KeyRow {}
impl ListBoxRowImpl for KeyRow {}
impl PreferencesRowImpl for KeyRow {}
impl ActionRowImpl for KeyRow {}

View File

@@ -1,6 +1,8 @@
mod client_object;
mod client_row;
mod fingerprint_window;
mod key_object;
mod key_row;
mod window;
use std::{env, process, str};
@@ -16,6 +18,7 @@ use gtk::{
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use self::key_object::KeyObject;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
@@ -133,6 +136,9 @@ fn build_ui(app: &Application) {
FrontendEvent::EmulationStatus(s) => {
window.set_emulation(s.into());
}
FrontendEvent::AuthorizedUpdated(keys) => {
window.set_authorized_keys(keys);
}
}
}
}

View File

@@ -1,12 +1,14 @@
mod imp;
use std::collections::HashSet;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
use gtk::{
gio,
glib::{self, closure_local},
ListBox, NoSelection, Widget,
ListBox, NoSelection,
};
use lan_mouse_ipc::{
@@ -14,7 +16,7 @@ use lan_mouse_ipc::{
DEFAULT_PORT,
};
use crate::fingerprint_window::FingerprintWindow;
use crate::{fingerprint_window::FingerprintWindow, key_object::KeyObject, key_row::KeyRow};
use super::{client_object::ClientObject, client_row::ClientRow};
@@ -44,10 +46,55 @@ impl Window {
.expect("Could not get clients")
}
pub fn authorized(&self) -> gio::ListStore {
self.imp()
.authorized
.borrow()
.clone()
.expect("Could not get authorized")
}
fn client_by_idx(&self, idx: u32) -> Option<ClientObject> {
self.clients().item(idx).map(|o| o.downcast().unwrap())
}
fn authorized_by_idx(&self, idx: u32) -> Option<KeyObject> {
self.authorized().item(idx).map(|o| o.downcast().unwrap())
}
fn setup_authorized(&self) {
let store = gio::ListStore::new::<KeyObject>();
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::<ClientObject>();
self.imp().clients.replace(Some(model));
@@ -116,7 +163,8 @@ impl Window {
/// 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
pub fn set_placeholder_visible(&self, visible: bool) {
pub 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),
@@ -124,6 +172,15 @@ impl Window {
});
}
pub 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 setup_icon(&self) {
self.set_icon_name(Some("de.feschber.LanMouse"));
}
@@ -134,10 +191,16 @@ impl Window {
row
}
fn create_key_row(&self, key_object: &KeyObject) -> KeyRow {
let row = KeyRow::new();
row.bind(key_object);
row
}
pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
let client = ClientObject::new(handle, client, state.clone());
self.clients().append(&client);
self.set_placeholder_visible(false);
self.update_placeholder_visibility();
self.update_dns_state(handle, !state.ips.is_empty());
}
@@ -159,7 +222,7 @@ impl Window {
self.clients().remove(idx as u32);
if self.clients().n_items() == 0 {
self.set_placeholder_visible(true);
self.update_placeholder_visibility();
}
}
@@ -291,6 +354,18 @@ impl Window {
pub 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, fp: String| {
parent.request_fingerprint_add(fp);
w.close();
}
),
);
window.present();
}
@@ -298,6 +373,10 @@ impl Window {
self.request(FrontendRequest::FingerprintAdd(fp));
}
pub fn request_fingerprint_remove(&self, fp: String) {
self.request(FrontendRequest::FingerprintRemove(fp));
}
pub fn request(&self, request: FrontendRequest) {
let mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap();
@@ -331,4 +410,16 @@ impl Window {
.capture_emulation_group
.set_visible(!capture || !emulation);
}
pub(crate) fn set_authorized_keys(&self, fingerprints: HashSet<String>) {
let authorized = self.authorized();
// clear list
authorized.remove_all();
// insert fingerprints
for fingerprint in fingerprints {
let key_obj = KeyObject::new(fingerprint);
authorized.append(&key_obj);
}
self.update_auth_placeholder_visibility();
}
}

View File

@@ -11,6 +11,8 @@ use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
#[template_child]
pub authorized_placeholder: TemplateChild<ActionRow>,
#[template_child]
pub port_edit_apply: TemplateChild<Button>,
#[template_child]
@@ -35,7 +37,10 @@ pub struct Window {
pub input_emulation_button: TemplateChild<Button>,
#[template_child]
pub input_capture_button: TemplateChild<Button>,
#[template_child]
pub authorized_list: TemplateChild<ListBox>,
pub clients: RefCell<Option<gio::ListStore>>,
pub authorized: RefCell<Option<gio::ListStore>>,
pub frontend_request_writer: RefCell<Option<FrontendRequestWriter>>,
pub port: Cell<u16>,
pub capture_active: Cell<bool>,
@@ -146,6 +151,7 @@ impl ObjectImpl for Window {
let obj = self.obj();
obj.setup_icon();
obj.setup_clients();
obj.setup_authorized();
}
}

View File

@@ -197,6 +197,7 @@ pub enum FrontendEvent {
CaptureStatus(Status),
/// emulation status
EmulationStatus(Status),
AuthorizedUpdated(HashSet<String>),
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]

View File

@@ -270,10 +270,16 @@ impl Server {
fn add_authorized_key(&self, key: String) {
self.authorized_keys.borrow_mut().insert(key);
self.notify_frontend(FrontendEvent::AuthorizedUpdated(
self.authorized_keys.borrow().clone(),
));
}
fn remove_authorized_key(&self, key: String) {
self.authorized_keys.borrow_mut().remove(&key);
self.notify_frontend(FrontendEvent::AuthorizedUpdated(
self.authorized_keys.borrow().clone(),
));
}
fn enumerate(&self) {