use std::cell::{OnceCell, RefCell};
use adw::subclass::window::AdwWindowImpl;
use gtk::{
glib::{self, Object, Variant},
prelude::*,
subclass::{
prelude::*,
widget::{
CompositeTemplateCallbacksClass, CompositeTemplateClass,
CompositeTemplateInitializingExt, WidgetImpl,
},
},
};
use tracing::{debug, error, info, warn};
use crate::{
systemd::{self, runtime},
systemd_gui::new_settings,
upgrade,
widget::{
close_window_shortcut,
preferences::data::KEY_PREF_PROP_ORIENTATION_MODE,
unit_list::{UnitListPanel, column::SysdColumn},
unit_properties_selector::{
data_browser::{INTERFACE_NAME, PropertyBrowseItem, SPECIAL_INTERFACE_NAME},
unit_properties_selection::UnitPropertiesSelectionPanel,
},
},
};
use super::UnitPropertiesSelectorDialog;
const WINDOW_SIZE: &str = "unit-property-window-size";
const PANED_SEPARATOR_POSITION: &str = "unit-property-paned-separator-position";
#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/unit_properties_selector.ui")]
pub struct UnitPropertiesSelectorDialogImp {
#[template_child]
properties_selector: TemplateChild<gtk::ColumnView>,
#[template_child]
interface_column: TemplateChild<gtk::ColumnViewColumn>,
#[template_child]
property_column: TemplateChild<gtk::ColumnViewColumn>,
#[template_child]
signature_column: TemplateChild<gtk::ColumnViewColumn>,
#[template_child]
access_column: TemplateChild<gtk::ColumnViewColumn>,
#[template_child]
search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
toogle_button: TemplateChild<gtk::ToggleButton>,
#[template_child]
unit_properties_selection_panel: TemplateChild<UnitPropertiesSelectionPanel>,
#[template_child]
paned: TemplateChild<gtk::Paned>,
#[template_child]
orientation_button: TemplateChild<gtk::Button>,
#[template_child]
toast_overlay: TemplateChild<adw::ToastOverlay>,
last_filter_string: RefCell<String>,
custom_filter: OnceCell<gtk::CustomFilter>,
tree_list_model: OnceCell<gtk::TreeListModel>,
}
#[gtk::template_callbacks]
impl UnitPropertiesSelectorDialogImp {
#[template_callback]
fn search_entry_changed(&self, search_entry: >k::SearchEntry) {
let text: glib::GString = search_entry.text();
let mut last_filter = self.last_filter_string.borrow_mut();
let text_is_empty = text.is_empty();
if !text_is_empty {
self.toogle_button.set_active(true);
}
let change_type = if text_is_empty {
gtk::FilterChange::LessStrict
} else if text.len() > last_filter.len() && text.contains(last_filter.as_str()) {
gtk::FilterChange::MoreStrict
} else if text.len() < last_filter.len() && last_filter.contains(text.as_str()) {
gtk::FilterChange::LessStrict
} else {
gtk::FilterChange::Different
};
debug!("Search text. Current \"{text}\" Prev \"{last_filter}\"");
last_filter.replace_range(.., text.as_str());
if let Some(custom_filter) = self.custom_filter.get() {
custom_filter.changed(change_type);
}
}
#[template_callback]
fn expand_toggled(&self, toogle_button: >k::ToggleButton) {
info!("expand_toggled {}", toogle_button.is_active());
self.expand_interfaces(toogle_button.is_active());
if toogle_button.is_active() {
toogle_button.set_icon_name("go-down-symbolic");
toogle_button.set_tooltip_text(Some("Collapse all interfaces"));
} else {
toogle_button.set_icon_name("go-next-symbolic");
toogle_button.set_tooltip_text(Some("Expand all interfaces"));
}
}
#[template_callback]
fn orientation_toggled(&self, _orientation_button: >k::Button) {
info!("orientation_toggled clicked ");
let orientation = self.paned.orientation();
let new_orientation = self.handle_paned_orientation(orientation);
warn!("New orientation {new_orientation:?}, Old {orientation:?}");
self.paned.set_orientation(new_orientation);
}
fn handle_paned_orientation(&self, orientation: gtk::Orientation) -> gtk::Orientation {
if orientation == gtk::Orientation::Vertical {
self.orientation_button.set_icon_name("top-down");
self.orientation_button
.set_tooltip_text(Some("Switch to top down layout"));
gtk::Orientation::Horizontal
} else {
self.orientation_button.set_icon_name("side-by-side");
self.orientation_button
.set_tooltip_text(Some("Switch to side by side layout"));
gtk::Orientation::Vertical
}
}
fn expand_interfaces(&self, expand: bool) {
let Some(tree_list_model) = self.tree_list_model.get() else {
warn!("Can't find tree list model");
return;
};
let nb_item = tree_list_model.model().n_items();
warn!("tree_list_model {}", nb_item);
for i in 0..nb_item {
if let Some(row) = tree_list_model.child_row(i) {
warn!("set_expanded {}", i);
row.set_expanded(expand);
}
}
}
fn create_filter(&self) -> gtk::CustomFilter {
let search_entry = self.search_entry.clone();
gtk::CustomFilter::new(move |object| {
let text_gs = search_entry.text();
if text_gs.is_empty() {
return true;
}
let Some(tree_list_row) = object.downcast_ref::<gtk::TreeListRow>() else {
error!("some wrong downcast_ref {object:?}");
return false;
};
debug!("Depth {} ", tree_list_row.depth());
if let Some(children) = tree_list_row.children() {
let item = tree_list_row.item();
if let Some(prop_selector) = item.and_downcast_ref::<PropertyBrowseItem>() {
debug!(
"Child model {} {}",
children.n_items(),
prop_selector.interface()
);
} else {
error!("some wrong downcast_ref {object:?}");
};
return true;
}
let item = tree_list_row.item();
let Some(prop_selector) = item.and_downcast_ref::<PropertyBrowseItem>() else {
error!("some wrong downcast_ref {object:?}");
return false;
};
debug!(
"Interface {:?} Prop {:?}",
prop_selector.interface(),
prop_selector.unit_property()
);
let texts = text_gs.as_str();
if text_gs.chars().any(|c| c.is_ascii_uppercase()) {
prop_selector.unit_property().contains(texts)
} else {
prop_selector
.unit_property()
.to_ascii_lowercase()
.contains(texts)
}
})
}
pub(super) fn set_unit_list(&self, unit_list_panel: &UnitListPanel, column_id: Option<String>) {
self.unit_properties_selection_panel
.set_unit_list(unit_list_panel, column_id);
let Some(tree_list_model) = self.tree_list_model.get() else {
warn!("Not None");
return;
};
let default = PropertyBrowseItem::new_interface(INTERFACE_NAME.to_owned());
for default_column in unit_list_panel.default_displayed_columns() {
let new_property_object = PropertyBrowseItem::from_column(&default_column.column());
default.add_child(new_property_object);
}
let model = tree_list_model.model();
let store = model.downcast_ref::<gio::ListStore>().unwrap();
store.append(&default);
let default = PropertyBrowseItem::new_interface(SPECIAL_INTERFACE_NAME.to_owned());
for default_column in SysdColumn::specials() {
let new_property_object = PropertyBrowseItem::from_sysd(&default_column);
default.add_child(new_property_object);
}
let model = tree_list_model.model();
let store = model.downcast_ref::<gio::ListStore>().unwrap();
store.append(&default);
}
fn load_window_size(&self) {
let settings = new_settings();
let size = settings.value(WINDOW_SIZE);
let (mut width, mut height) = size.get::<(i32, i32)>().unwrap();
let mut separator_position = settings.int(PANED_SEPARATOR_POSITION);
info!(
"Window settings: width {width}, height {height}, panes position {separator_position}"
);
let obj = self.obj();
let (def_width, def_height) = obj.default_size();
if width < 0 {
width = def_width;
if width < 0 {
width = 1280;
}
}
if height < 0 {
height = def_height;
if height < 0 {
height = 720;
}
}
obj.set_default_size(width, height);
if separator_position < 0 {
separator_position = width / 2;
}
self.paned.set_position(separator_position);
let (orientation, invert_orientation) = if settings.boolean(KEY_PREF_PROP_ORIENTATION_MODE)
{
(gtk::Orientation::Vertical, gtk::Orientation::Horizontal)
} else {
(gtk::Orientation::Horizontal, gtk::Orientation::Vertical)
};
self.handle_paned_orientation(invert_orientation);
self.paned.set_orientation(orientation);
}
pub fn save_window_context(&self) -> Result<(), glib::BoolError> {
let obj = self.obj();
let size = obj.default_size();
let settings = new_settings();
let value: Variant = size.into();
settings.set_value(WINDOW_SIZE, &value)?;
let separator_position = self.paned.position();
settings.set_int(PANED_SEPARATOR_POSITION, separator_position)?;
settings.set_boolean(
KEY_PREF_PROP_ORIENTATION_MODE,
self.paned.orientation() == gtk::Orientation::Vertical,
)?;
Ok(())
}
fn add_toast_message(&self, message: &str, use_markup: bool) {
let toast = adw::Toast::builder()
.title(message)
.use_markup(use_markup)
.build();
self.toast_overlay.dismiss_all();
self.toast_overlay.add_toast(toast);
}
}
#[glib::object_subclass]
impl ObjectSubclass for UnitPropertiesSelectorDialogImp {
const NAME: &'static str = "UnitPropertiesSelectorDialog";
type Type = UnitPropertiesSelectorDialog;
type ParentType = adw::Window;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for UnitPropertiesSelectorDialogImp {
fn constructed(&self) {
self.parent_constructed();
close_window_shortcut(self.obj().as_ref());
self.load_window_size();
let list_store = gio::ListStore::new::<PropertyBrowseItem>();
let tree_list_model =
gtk::TreeListModel::new(list_store.clone(), false, false, add_tree_node);
self.tree_list_model
.set(tree_list_model.clone())
.expect("set once only");
let filter = self.create_filter();
self.custom_filter
.set(filter.clone())
.expect("custom filter set once");
let filtering_model = gtk::FilterListModel::new(Some(tree_list_model), Some(filter));
let selection_model = gtk::SingleSelection::builder()
.model(&filtering_model)
.can_unselect(true)
.autoselect(false)
.build();
self.properties_selector.set_model(Some(&selection_model));
let factory_interface = gtk::SignalListItemFactory::new();
factory_interface.connect_setup(|_fac, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
item.set_selectable(true);
let label = gtk::Label::builder().xalign(0.0).build();
let expander = gtk::TreeExpander::builder().child(&label).build();
item.set_child(Some(&expander));
});
factory_interface.connect_bind(|_fac, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let expander = item
.child()
.unwrap()
.downcast::<gtk::TreeExpander>()
.unwrap();
let label = expander.child().unwrap().downcast::<gtk::Label>().unwrap();
let tree_list_row = item.item().unwrap().downcast::<gtk::TreeListRow>().unwrap();
expander.set_list_row(Some(&tree_list_row));
let property_object = tree_list_row
.item()
.and_downcast::<PropertyBrowseItem>()
.unwrap();
let interface = property_object.interface();
let val = if tree_list_row.depth() == 0 {
interface.split('.').next_back().unwrap_or_default()
} else {
""
};
label.set_text(val);
});
self.interface_column.set_factory(Some(&factory_interface));
let factory_property = gtk::SignalListItemFactory::new();
factory_property.connect_setup(setup);
factory_property.connect_bind(|_fac, item| {
bind(item, PropertyBrowseItem::unit_property);
});
self.property_column.set_factory(Some(&factory_property));
let signature_factory = gtk::SignalListItemFactory::new();
signature_factory.connect_setup(setup);
signature_factory.connect_bind(|_fac, item| {
bind(item, |property_object: &PropertyBrowseItem| {
PropertyBrowseItem::signature(property_object).unwrap_or_default()
});
});
self.signature_column.set_factory(Some(&signature_factory));
let access_factory = gtk::SignalListItemFactory::new();
access_factory.connect_setup(setup);
access_factory.connect_bind(|_fac, item| {
bind(item, |property_object: &PropertyBrowseItem| {
PropertyBrowseItem::access(property_object).unwrap_or_default()
});
});
self.access_column.set_factory(Some(&access_factory));
let unit_properties_selection = self.unit_properties_selection_panel.downgrade();
let unit_properties_selector = self.obj().downgrade();
selection_model.connect_selected_item_notify(move |single_selection| {
let unit_properties_selection = upgrade!(unit_properties_selection);
let unit_properties_selector = upgrade!(unit_properties_selector);
debug!(
"connect_selected_notify idx {}",
single_selection.selected()
);
let Some(object) = single_selection.selected_item() else {
return;
};
let tree_list_row = object.downcast::<gtk::TreeListRow>().unwrap();
if tree_list_row.is_expandable() {
tree_list_row.set_expanded(!tree_list_row.is_expanded());
}
let property_object = tree_list_row
.item()
.and_downcast::<PropertyBrowseItem>()
.unwrap();
if property_object.unit_property().is_empty() {
single_selection.unselect_item(single_selection.selected());
debug!("Can't select interface {property_object:?}");
return;
}
info!("Select {property_object:?}");
let interface = tree_list_row
.parent()
.expect("has a parent")
.item()
.and_downcast::<PropertyBrowseItem>()
.unwrap();
let new_property_object = PropertyBrowseItem::from_parent(interface, property_object);
let new_property = new_property_object.unit_property();
unit_properties_selection.add_new_property(new_property_object);
unit_properties_selector
.imp()
.add_toast_message(&format!("New property {} added", new_property), false);
});
glib::spawn_future_local(async move {
let (sender, receiver) = tokio::sync::oneshot::channel();
runtime().spawn(async move {
match systemd::fetch_unit_interface_properties().await {
Ok(map) => {
if let Err(e) = sender.send(map) {
error!("Channel closed unexpectedly: {e:?}");
}
}
Err(err) => error!("Fetch unit properties {err:?}"),
}
});
let Ok(unit_properties_map) = receiver
.await
.inspect_err(|err| error!("Tokio channel dropped {err:?}"))
else {
return;
};
for (inteface, mut properties) in unit_properties_map
.into_iter()
.filter(|(k, _)| k.starts_with("org.freedesktop.systemd1"))
{
let obj = PropertyBrowseItem::new_interface(inteface);
properties.sort();
for property in properties {
let prop_object = PropertyBrowseItem::from(property);
obj.add_child(prop_object);
}
list_store.append(&obj);
}
});
}
}
fn setup(_fac: >k::SignalListItemFactory, item: &Object) {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let label = gtk::Label::builder().xalign(0.0).build();
item.set_child(Some(&label));
}
fn bind(item: &Object, func: fn(&PropertyBrowseItem) -> String) {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let widget = item.child();
let label = widget.and_downcast_ref::<gtk::Label>().unwrap();
let tree_list_row = item.item().unwrap().downcast::<gtk::TreeListRow>().unwrap();
let property_object = tree_list_row
.item()
.and_downcast::<PropertyBrowseItem>()
.unwrap();
let value = func(&property_object);
label.set_text(&value)
}
fn add_tree_node(object: &Object) -> Option<gio::ListModel> {
let Some(prop_selector) = object.downcast_ref::<PropertyBrowseItem>() else {
warn!("object type: {:?} {object:?}", object.type_());
return None;
};
let store = gio::ListStore::new::<PropertyBrowseItem>();
let binding = prop_selector.children();
let children = (*binding).as_ref()?;
for child in children.iter() {
store.append(child)
}
Some(store.into())
}
impl WidgetImpl for UnitPropertiesSelectorDialogImp {}
impl WindowImpl for UnitPropertiesSelectorDialogImp {
fn close_request(&self) -> glib::Propagation {
debug!("Close window");
if let Err(_err) = self.save_window_context() {
error!("Failed to save window state");
}
self.parent_close_request();
glib::Propagation::Proceed
}
}
impl AdwWindowImpl for UnitPropertiesSelectorDialogImp {}
#[cfg(test)]
mod test {
#[test]
fn test_last() {
let var = "org.freedesktop.systemd1.some_stuff";
let token = var.split('.').next_back();
assert_eq!(token, Some("some_stuff"))
}
}