diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 4c49d463391261d4890b77b69f2c5d707dda0f27..a5a84bde3ebafd8ad5abe925a34369e75698283a 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -10,6 +10,7 @@ ui/details_view.ui ui/diagram.ui ui/diagram_row.ui + ui/diagram_view.ui ui/durations_page.ui ui/frequencies_page.ui ui/sizes_page.ui diff --git a/data/resources/ui/address_dialog.ui b/data/resources/ui/address_dialog.ui index 3d144ff051bdd05236bd78b06e7a5944692bf6fd..504dbd2be4fc4b80696ce938e03f34d3f2f263fd 100644 --- a/data/resources/ui/address_dialog.ui +++ b/data/resources/ui/address_dialog.ui @@ -16,7 +16,7 @@ Server Address - + diff --git a/data/resources/ui/diagram.ui b/data/resources/ui/diagram.ui index fa09068043f24feb3edd2b7c6baf0226b67397b2..7200068cdc93d08fd98d7b92d960079935293b07 100644 --- a/data/resources/ui/diagram.ui +++ b/data/resources/ui/diagram.ui @@ -6,18 +6,9 @@ - True - - - - True - False - - - + + diff --git a/data/resources/ui/diagram_view.ui b/data/resources/ui/diagram_view.ui new file mode 100644 index 0000000000000000000000000000000000000000..21d2f6ae5376fe7d201276f98c3630d9757bf957 --- /dev/null +++ b/data/resources/ui/diagram_view.ui @@ -0,0 +1,26 @@ + + + + diff --git a/data/resources/ui/window.ui b/data/resources/ui/window.ui index 4c2fc8d20493b3aae6bb951bf0318ef951ddbfd8..7695ab948dd12ed12ec00e3f699afab7259e1824 100644 --- a/data/resources/ui/window.ui +++ b/data/resources/ui/window.ui @@ -116,7 +116,7 @@ Welcome to Bustle - Start recording D-Bus activity, you can also run the command + Start recording D-Bus activity, you can also run the command True @@ -264,6 +264,11 @@ True + + + + + diff --git a/src/diagram.rs b/src/diagram.rs index 496b7c9accc4436d3ed4cc80c5a2e69e07b9e57b..6c644e323fb36dfcd308b5fc0ea0966b7e0c7e64 100644 --- a/src/diagram.rs +++ b/src/diagram.rs @@ -5,13 +5,13 @@ use gtk::{ subclass::prelude::*, }; -use crate::{diagram_item::DiagramItem, diagram_model::DiagramModel, diagram_row::DiagramRow}; +use crate::{ + diagram_filter_list_model::DiagramFilterListModel, diagram_item::DiagramItem, + diagram_view::DiagramView, +}; mod imp { - use std::{ - cell::{Cell, RefCell}, - marker::PhantomData, - }; + use std::{cell::Cell, marker::PhantomData}; use super::*; @@ -19,17 +19,15 @@ mod imp { #[properties(wrapper_type = super::Diagram)] #[template(resource = "/org/freedesktop/Bustle/ui/diagram.ui")] pub struct Diagram { + #[property(get = Self::model, set = Self::set_model, explicit_notify, nullable)] + pub(super) model: PhantomData>, #[property(get = Self::selected_item)] pub(super) selected_item: PhantomData>, #[template_child] pub(super) scrolled_window: TemplateChild, #[template_child] - pub(super) list_view: TemplateChild, - #[template_child] - pub(super) selection_model: TemplateChild, - - pub(super) model: RefCell>, + pub(super) view: TemplateChild, pub(super) is_sticky: Cell, pub(super) is_auto_scrolling: Cell, @@ -43,14 +41,13 @@ mod imp { fn class_init(klass: &mut Self::Class) { klass.bind_template(); + klass.bind_template_instance_callbacks(); klass.add_binding( gdk::Key::Escape, gdk::ModifierType::empty(), |obj, _| { - obj.imp() - .selection_model - .set_selected(gtk::INVALID_LIST_POSITION); + obj.imp().view.unselect(); true }, None, @@ -69,26 +66,9 @@ mod imp { let obj = self.obj(); - let factory = gtk::SignalListItemFactory::new(); - factory.connect_setup(clone!(@weak obj => move |_, list_item| { - let list_item = list_item.downcast_ref::().unwrap(); - - let row = DiagramRow::default(); - - list_item.property_expression("item").bind(&row, "item", glib::Object::NONE); - - list_item.set_child(Some(&row)); - })); - self.list_view.set_factory(Some(&factory)); - - self.selection_model - .connect_selected_item_notify(clone!(@weak obj => move |_| { - obj.notify_selected_item(); - })); - self.is_sticky.set(true); - let vadj = self.list_view.vadjustment().unwrap(); + let vadj = self.view.vadjustment().unwrap(); vadj.connect_value_changed(clone!(@weak obj => move |vadj| { let imp = obj.imp(); @@ -124,33 +104,37 @@ mod imp { impl WidgetImpl for Diagram {} impl Diagram { + fn set_model(&self, model: Option<&DiagramFilterListModel>) { + self.view.set_model(model); + } + + fn model(&self) -> Option { + self.view.model() + } + fn selected_item(&self) -> Option { - self.selection_model.selected_item().and_downcast() + self.view.selected_item() } } } glib::wrapper! { + /// Wrapper to DiagramView pub struct Diagram(ObjectSubclass) @extends gtk::Widget; } +#[gtk::template_callbacks] impl Diagram { - pub fn set_model(&self, model: Option<&DiagramModel>) { - let imp = self.imp(); - - imp.model.replace(model.cloned()); - imp.selection_model.set_model(model); - } - - pub fn model(&self) -> Option { - self.imp().model.borrow().clone() - } - fn scroll_to_end(&self) { let imp = self.imp(); imp.is_auto_scrolling.set(true); imp.scrolled_window .emit_scroll_child(gtk::ScrollType::End, false); } + + #[template_callback] + fn view_selected_item_notify(&self) { + self.notify_selected_item(); + } } diff --git a/src/diagram_filter_list_model.rs b/src/diagram_filter_list_model.rs new file mode 100644 index 0000000000000000000000000000000000000000..6dad35fda2af04ba2616fe9dc9608239cf6c4d77 --- /dev/null +++ b/src/diagram_filter_list_model.rs @@ -0,0 +1,84 @@ +use gtk::{ + gio, + glib::{self, clone}, + prelude::*, + subclass::prelude::*, +}; + +use crate::diagram_model::DiagramModel; + +mod imp { + use crate::diagram_item::DiagramItem; + + use super::*; + + #[derive(Default)] + pub struct DiagramFilterListModel { + pub(super) inner: gtk::FilterListModel, + } + + #[glib::object_subclass] + impl ObjectSubclass for DiagramFilterListModel { + const NAME: &'static str = "BustleDiagramFilterListModel"; + type Type = super::DiagramFilterListModel; + type Interfaces = (gio::ListModel,); + } + + impl ObjectImpl for DiagramFilterListModel { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + + self.inner.connect_items_changed( + clone!(@weak obj => move |_, position, removed, added| { + obj.items_changed(position, removed, added); + }), + ); + + self.inner.set_filter(Some(>k::CustomFilter::new(|item| { + let diagram_item = item.downcast_ref::().unwrap(); + + // Skip messages. TODO: define the exact rules and document why + !diagram_item + .message_destination() + .is_some_and(|d| d == "org.freedesktop.DBus") + && !diagram_item + .message_sender() + .is_some_and(|s| s == "org.freedesktop.DBus") + }))); + } + } + + impl ListModelImpl for DiagramFilterListModel { + fn item_type(&self) -> glib::Type { + DiagramItem::static_type() + } + + fn n_items(&self) -> u32 { + self.inner.n_items() + } + + fn item(&self, position: u32) -> Option { + self.inner.item(position) + } + } +} + +glib::wrapper! { + pub struct DiagramFilterListModel(ObjectSubclass) + @implements gio::ListModel; +} + +impl DiagramFilterListModel { + pub fn set_model(&self, model: Option<&DiagramModel>) { + self.imp().inner.set_model(model); + } + + pub fn model(&self) -> Option { + self.imp() + .inner + .model() + .map(|model| model.downcast().unwrap()) + } +} diff --git a/src/diagram_item.rs b/src/diagram_item.rs index 82d3ab75109a110e64787de3ab61425d8c724f4c..950d2ce0e0462077513c627a3e081475f138dc37 100644 --- a/src/diagram_item.rs +++ b/src/diagram_item.rs @@ -17,13 +17,12 @@ mod imp { #[derive(Debug, Default, glib::Properties)] #[properties(wrapper = super::DiagramItem)] pub struct DiagramItem { - pub(super) message: OnceCell>, #[property(get, set, construct_only, builder(MessageType::Invalid))] pub(super) message_type: Cell, #[property(get, set, construct_only)] pub(super) timestamp: OnceCell, - #[property(get, set, construct_only)] - pub(super) elapsed_time: Cell, + + pub(super) message: OnceCell>, pub(super) associated_item: OnceCell>, } @@ -42,24 +41,16 @@ glib::wrapper! { } impl DiagramItem { - pub fn from_event(event: Event, head_time: Option) -> Self { - let elapsed_time = if let Some(time) = head_time { - event.timestamp - time - } else { - Timestamp::default() - }; - + pub fn from_event(event: Event) -> Self { let this = glib::Object::builder::() .property( "message-type", MessageType::from(event.message.message_type()), ) .property("timestamp", event.timestamp) - .property("elapsed-time", elapsed_time) .build(); - let imp = this.imp(); - imp.message.set(event.message).unwrap(); + this.imp().message.set(event.message).unwrap(); this } @@ -220,6 +211,16 @@ impl DiagramItem { .and_then(|d| d.as_deref()) .cloned() } + + /// Whether this contains the return/error message of the given item's message. + pub fn is_return_of(&self, other: &Self) -> bool { + debug_assert!(self.message_type().is_method_return()); + + other.message_type().is_method_call() + && self.message().reply_serial() + == other.message().primary_header().serial_num().copied() + && self.message_destination() == other.message_sender() + } } impl std::fmt::Display for DiagramItem { diff --git a/src/diagram_model.rs b/src/diagram_model.rs index c58d3673fa34562ab68b151bd30d1c69bc7d2e05..ceaf9fa33f21900a3a74f171209553d9aa11909d 100644 --- a/src/diagram_model.rs +++ b/src/diagram_model.rs @@ -151,80 +151,40 @@ impl DiagramModel { let this = Self::default(); - let mut head_timestamp = None; for event in events { - let item = DiagramItem::from_event(event, head_timestamp); - let was_added = this.push_inner(item.clone()); - if head_timestamp.is_none() && was_added { - head_timestamp = Some(item.timestamp()); - } + this.push_inner(DiagramItem::from_event(event)); } Ok(this) } - pub fn push(&self, diagram_item: DiagramItem) { - if self.push_inner(diagram_item) { + pub fn push(&self, item: DiagramItem) { + if self.push_inner(item) { self.items_changed(self.n_items() - 1, 0, 1); } } - /// Search the items for the associated [`MessageType::MethodCall`] of - /// a [`MessageType::MethodReturn`] or [`MessageType::Error`] - fn call_of_return(&self, return_item: &DiagramItem) -> Option { - debug_assert!(return_item.message_type().is_method_return()); - - for item in self.imp().list.borrow().iter() { - if !item.message_type().is_method_call() { - continue; - } - if return_item.message().reply_serial() - == item.message().primary_header().serial_num().copied() - && return_item.message_destination() == item.message_sender() - { - return Some(item.clone()); - } - } - None - } - - /// Get the first item in the model. - pub fn head(&self) -> Option { + fn push_inner(&self, item: DiagramItem) -> bool { let imp = self.imp(); - let first = imp.list.borrow().first().cloned()?; - - debug_assert_eq!( - imp.list.borrow().iter().map(|item| item.timestamp()).min(), - Some(first.timestamp()) - ); - - Some(first) - } - - fn push_inner(&self, diagram_item: DiagramItem) -> bool { - // Skip messages. TODO: define the exact rules and document why - if diagram_item - .message_destination() - .is_some_and(|d| d == "org.freedesktop.DBus") - || diagram_item - .message_sender() - .is_some_and(|s| s == "org.freedesktop.DBus") - { - return false; - } - - // Before we add the item to the list, we look for it other half - // if we have a [`MessageType::MethodCall`] we would search for it - // [`MessageType::MethodReturn`] or [`MessageType::Error`] - if diagram_item.message_type().is_method_return() { - if let Some(associated_item) = self.call_of_return(&diagram_item) { - diagram_item.set_associated_item(&associated_item); - associated_item.set_associated_item(&diagram_item); + // Before we add the item to the list, we look for the method call half + // of the method return/error message + if item.message_type().is_method_return() { + // Reverse the list so we first look at the most recent call. This speeds up the search + // substantially in the common case where the return is close to the call. + if let Some(associated_item) = imp + .list + .borrow() + .iter() + .rev() + .find(|other_item| item.is_return_of(other_item)) + { + item.set_associated_item(associated_item); + associated_item.set_associated_item(&item); } } - self.imp().list.borrow_mut().push(diagram_item); + imp.list.borrow_mut().push(item); true } } diff --git a/src/diagram_row.rs b/src/diagram_row.rs index 36f049b96a94f79c7e57b133c08d2edec95d3d13..7186f58bf5399debf16d29b12d6089c71b67d75e 100644 --- a/src/diagram_row.rs +++ b/src/diagram_row.rs @@ -1,6 +1,6 @@ use gtk::{glib, prelude::*, subclass::prelude::*}; -use crate::diagram_item::DiagramItem; +use crate::{diagram_item::DiagramItem, timestamp::Timestamp}; mod imp { use std::cell::RefCell; @@ -13,6 +13,8 @@ mod imp { pub struct DiagramRow { #[property(get, set = Self::set_item, explicit_notify, nullable)] pub(super) item: RefCell>, + #[property(get, set = Self::set_head_item, explicit_notify, nullable)] + pub(super) head_item: RefCell>, #[template_child] pub(super) hbox: TemplateChild, @@ -55,9 +57,20 @@ mod imp { } let obj = self.obj(); - obj.update_labels(); + obj.update_timestamp_label(); + obj.update_title_subtitle_labels(); obj.notify_item(); } + + fn set_head_item(&self, head_item: Option) { + if head_item == self.head_item.replace(head_item.clone()) { + return; + } + + let obj = self.obj(); + obj.update_timestamp_label(); + obj.notify_head_item(); + } } } @@ -67,14 +80,31 @@ glib::wrapper! { } impl DiagramRow { - fn update_labels(&self) { + fn update_timestamp_label(&self) { let imp = self.imp(); - if let Some(item) = imp.item.borrow().as_ref() { - let elapsed_ms = item.elapsed_time().as_millis_f64().round(); + if let Some(item) = self.item() { + let elapsed_ms = { + let head_item = self + .head_item() + .expect("there must always be a head item if there is an item"); + if head_item == item { + Timestamp::default() + } else { + item.timestamp() - head_item.timestamp() + } + }; + imp.timestamp_label + .set_label(&format!("{} ms", elapsed_ms.as_millis_f64().round())); + } else { + imp.timestamp_label.set_label(""); + } + } - imp.timestamp_label.set_label(&format!("{elapsed_ms} ms")); + fn update_title_subtitle_labels(&self) { + let imp = self.imp(); + if let Some(item) = self.item() { let title = item.display_path(); if item.message_type().is_method_return() { imp.title_label.set_text(Some(&title)); @@ -89,7 +119,6 @@ impl DiagramRow { .set_markup(Some(&item.markup_member_name(true))); } } else { - imp.timestamp_label.set_label(""); imp.title_label.set_text(None); imp.subtitle_label.set_text(None); } diff --git a/src/diagram_view.rs b/src/diagram_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..1506adc0a3a2fa9ee01f10767daf911ac759b2b2 --- /dev/null +++ b/src/diagram_view.rs @@ -0,0 +1,208 @@ +use gtk::{ + glib::{self, clone}, + prelude::*, + subclass::prelude::*, +}; + +use crate::{ + diagram_filter_list_model::DiagramFilterListModel, diagram_item::DiagramItem, + diagram_row::DiagramRow, +}; + +mod imp { + use std::marker::PhantomData; + + use super::*; + + #[derive(Debug, Default, glib::Properties, gtk::CompositeTemplate)] + #[properties(wrapper_type = super::DiagramView)] + #[template(resource = "/org/freedesktop/Bustle/ui/diagram_view.ui")] + pub struct DiagramView { + #[property(get = Self::selected_item)] + pub(super) selected_item: PhantomData>, + #[property(get = Self::hscroll_policy, set = Self::set_hscroll_policy, override_interface = gtk::Scrollable)] + pub(super) hscroll_policy: PhantomData, + #[property(get = Self::hadjustment, set = Self::set_hadjustment, override_interface = gtk::Scrollable)] + pub(super) hadjustment: PhantomData>, + #[property(get = Self::vscroll_policy, set = Self::set_vscroll_policy, override_interface = gtk::Scrollable)] + pub(super) vscroll_policy: PhantomData, + #[property(get = Self::vadjustment, set = Self::set_vadjustment, override_interface = gtk::Scrollable)] + pub(super) vadjustment: PhantomData>, + + #[template_child] + pub(super) list_view: TemplateChild, + #[template_child] + pub(super) selection_model: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for DiagramView { + const NAME: &'static str = "BustleDiagramView"; + type Type = super::DiagramView; + type ParentType = gtk::Widget; + type Interfaces = (gtk::Scrollable,); + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for DiagramView { + fn dispose(&self) { + self.dispose_template(); + } + } + + impl WidgetImpl for DiagramView { + fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) { + self.list_view.measure(orientation, for_size) + } + + fn size_allocate(&self, width: i32, height: i32, baseline: i32) { + self.list_view + .size_allocate(>k::Allocation::new(0, 0, width, height), baseline) + } + } + + impl ScrollableImpl for DiagramView { + fn border(&self) -> Option { + self.list_view.border() + } + } + + impl DiagramView { + fn selected_item(&self) -> Option { + self.selection_model.selected_item().and_downcast() + } + + fn hscroll_policy(&self) -> gtk::ScrollablePolicy { + self.list_view.hscroll_policy() + } + + fn set_hscroll_policy(&self, policy: gtk::ScrollablePolicy) { + self.list_view.set_hscroll_policy(policy); + } + + fn hadjustment(&self) -> Option { + self.list_view.hadjustment() + } + + fn set_hadjustment(&self, adj: Option<>k::Adjustment>) { + self.list_view.set_hadjustment(adj); + + if let Some(adj) = adj { + let obj = self.obj(); + + adj.connect_value_changed(clone!(@weak obj => move |_| { + obj.queue_draw(); + })); + adj.connect_upper_notify(clone!(@weak obj => move |_| { + obj.queue_draw(); + })); + adj.connect_page_size_notify(clone!(@weak obj => move |_| { + obj.queue_draw(); + })); + } + } + + fn vscroll_policy(&self) -> gtk::ScrollablePolicy { + self.list_view.vscroll_policy() + } + + fn set_vscroll_policy(&self, policy: gtk::ScrollablePolicy) { + self.list_view.set_vscroll_policy(policy); + } + + fn vadjustment(&self) -> Option { + self.list_view.vadjustment() + } + + fn set_vadjustment(&self, adj: Option<>k::Adjustment>) { + self.list_view.set_vadjustment(adj); + + if let Some(adj) = adj { + let obj = self.obj(); + + adj.connect_value_changed(clone!(@weak obj => move |_| { + obj.queue_draw(); + })); + adj.connect_upper_notify(clone!(@weak obj => move |_| { + obj.queue_draw(); + })); + adj.connect_page_size_notify(clone!(@weak obj => move |_| { + obj.queue_draw(); + })); + } + } + } +} + +glib::wrapper! { + pub struct DiagramView(ObjectSubclass) + @extends gtk::Widget; +} + +impl DiagramView { + pub fn set_model(&self, model: Option<&DiagramFilterListModel>) { + self.imp().selection_model.set_model(model); + } + + pub fn model(&self) -> Option { + self.imp() + .selection_model + .model() + .map(|model| model.downcast().unwrap()) + } + + pub fn unselect(&self) { + self.imp() + .selection_model + .set_selected(gtk::INVALID_LIST_POSITION); + } +} + +#[gtk::template_callbacks] +impl DiagramView { + #[template_callback] + fn factory_setup(&self, list_item: &glib::Object) { + let list_item = list_item.downcast_ref::().unwrap(); + let row = DiagramRow::default(); + list_item.set_child(Some(&row)); + } + + #[template_callback] + fn factory_bind(&self, list_item: &glib::Object) { + let list_item = list_item.downcast_ref::().unwrap(); + let row = list_item.child().unwrap().downcast::().unwrap(); + row.set_head_item( + self.model() + .unwrap() + .item(0) + .map(|model| model.downcast::().unwrap()), + ); + row.set_item( + list_item + .item() + .map(|item| item.downcast_ref::().unwrap().clone()), + ); + } + + #[template_callback] + fn factory_unbind(&self, list_item: &glib::Object) { + let list_item = list_item.downcast_ref::().unwrap(); + let row = list_item.child().unwrap().downcast::().unwrap(); + row.set_item(None::); + row.set_head_item(None::); + } + + #[template_callback] + fn selection_model_selected_item_notify(&self) { + self.notify_selected_item(); + } +} diff --git a/src/main.rs b/src/main.rs index a8617e7333a8e0a6cb3b720b4d4724392eb1c35e..06db835201349560c25a60dd3fd50692b12cf198 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,11 @@ mod application; mod config; mod details_view; mod diagram; +mod diagram_filter_list_model; mod diagram_item; mod diagram_model; mod diagram_row; +mod diagram_view; mod i18n; mod message_type; mod monitor; diff --git a/src/statistics/pages/durations.rs b/src/statistics/pages/durations.rs index cad3b83d108b8595f3dbadad87d2b923be62d37a..161ca6f5fa43a81aefc5554deb5705deee585867 100644 --- a/src/statistics/pages/durations.rs +++ b/src/statistics/pages/durations.rs @@ -1,7 +1,10 @@ use adw::subclass::prelude::*; use gtk::{gio, glib, prelude::*}; -use crate::{diagram_item::DiagramItem, diagram_model::DiagramModel, statistics::DurationItem}; +use crate::{ + diagram_filter_list_model::DiagramFilterListModel, diagram_item::DiagramItem, + statistics::DurationItem, +}; mod imp { use std::cell::RefCell; @@ -13,7 +16,7 @@ mod imp { #[template(resource = "/org/freedesktop/Bustle/ui/durations_page.ui")] pub struct DurationsPage { #[property(get, set = Self::set_model)] - pub(super) model: RefCell>, + pub(super) model: RefCell>, #[template_child] pub(super) column_view: TemplateChild, #[template_child] @@ -81,7 +84,7 @@ mod imp { impl BinImpl for DurationsPage {} impl DurationsPage { - fn set_model(&self, model: &DiagramModel) { + fn set_model(&self, model: &DiagramFilterListModel) { let mut durations_map = std::collections::HashMap::new(); let durations = gio::ListStore::new::(); for item in model.iter::() { diff --git a/src/statistics/pages/frequencies.rs b/src/statistics/pages/frequencies.rs index 6ff349d02f8c11d431b1b27893dac1885205c176..19fa59de510be391e3182436ad50b6e1da7044ba 100644 --- a/src/statistics/pages/frequencies.rs +++ b/src/statistics/pages/frequencies.rs @@ -1,7 +1,10 @@ use adw::subclass::prelude::*; use gtk::{gio, glib, prelude::*}; -use crate::{diagram_item::DiagramItem, diagram_model::DiagramModel, statistics::FrequencyItem}; +use crate::{ + diagram_filter_list_model::DiagramFilterListModel, diagram_item::DiagramItem, + statistics::FrequencyItem, +}; mod imp { use std::cell::RefCell; @@ -14,7 +17,7 @@ mod imp { #[template(resource = "/org/freedesktop/Bustle/ui/frequencies_page.ui")] pub struct FrequenciesPage { #[property(get, set = Self::set_model)] - pub(super) model: RefCell>, + pub(super) model: RefCell>, #[template_child] pub(super) column_view: TemplateChild, #[template_child] @@ -69,7 +72,7 @@ mod imp { impl BinImpl for FrequenciesPage {} impl FrequenciesPage { - fn set_model(&self, model: &DiagramModel) { + fn set_model(&self, model: &DiagramFilterListModel) { let mut n_items = model.n_items(); let frequencies = gio::ListStore::new::(); diff --git a/src/statistics/pages/sizes.rs b/src/statistics/pages/sizes.rs index d798213403037f91824cb5d8d950bff795b454ea..3a894eec6088e01dc67ebf9eb815f0779f5e2c1e 100644 --- a/src/statistics/pages/sizes.rs +++ b/src/statistics/pages/sizes.rs @@ -4,8 +4,8 @@ use adw::subclass::prelude::*; use gtk::{gio, glib, prelude::*}; use crate::{ - diagram_item::DiagramItem, diagram_model::DiagramModel, message_type::MessageType, - statistics::SizeItem, + diagram_filter_list_model::DiagramFilterListModel, diagram_item::DiagramItem, + message_type::MessageType, statistics::SizeItem, }; mod imp { @@ -18,7 +18,7 @@ mod imp { #[template(resource = "/org/freedesktop/Bustle/ui/sizes_page.ui")] pub struct SizesPage { #[property(get, set = Self::set_model)] - pub(super) model: RefCell>, + pub(super) model: RefCell>, #[template_child] pub(super) column_view: TemplateChild, #[template_child] @@ -91,7 +91,7 @@ mod imp { impl BinImpl for SizesPage {} impl SizesPage { - fn set_model(&self, model: &DiagramModel) { + fn set_model(&self, model: &DiagramFilterListModel) { let mut sizes_map: HashMap<(MessageType, String), (usize, Vec)> = std::collections::HashMap::new(); let sizes = gio::ListStore::new::(); diff --git a/src/statistics/window.rs b/src/statistics/window.rs index c7a8ff5cd537cd9fcc0d9821034853b1a2c53120..6c4a4da696b50fafa9e174bab620c8235e05f562 100644 --- a/src/statistics/window.rs +++ b/src/statistics/window.rs @@ -1,7 +1,7 @@ use adw::subclass::prelude::*; use gtk::{gdk, glib, prelude::*}; -use crate::diagram_model::DiagramModel; +use crate::diagram_filter_list_model::DiagramFilterListModel; mod imp { use std::cell::OnceCell; @@ -14,7 +14,7 @@ mod imp { #[template(resource = "/org/freedesktop/Bustle/ui/statistics.ui")] pub struct StatisticsWindow { #[property(get, set, construct_only)] - pub(super) model: OnceCell, + pub(super) model: OnceCell, #[template_child] pub(super) durations_page: TemplateChild, #[template_child] @@ -72,7 +72,7 @@ glib::wrapper! { } impl StatisticsWindow { - pub fn new(model: &DiagramModel, parent: &impl IsA) -> Self { + pub fn new(model: &DiagramFilterListModel, parent: &impl IsA) -> Self { glib::Object::builder() .property("model", model) .property("transient-for", parent) diff --git a/src/window.rs b/src/window.rs index 2c55b124b682ee769aaf145ccf7a0def1b89cfed..11377300bb01dd0c9ff79720354c8c300dcda72e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -33,6 +33,8 @@ pub enum View { mod imp { use std::cell::RefCell; + use crate::diagram_filter_list_model::DiagramFilterListModel; + use super::*; #[derive(Debug, gtk::CompositeTemplate)] @@ -63,12 +65,13 @@ mod imp { #[template_child] pub(super) diagram: TemplateChild, #[template_child] + pub(super) diagram_filter_list_model: TemplateChild, + #[template_child] pub(super) details_view: TemplateChild, pub(super) settings: gio::Settings, pub(super) monitor: RefCell>, - pub(super) handler_id: RefCell>, // The currently recorded filename pub(super) filename: RefCell>, } @@ -88,9 +91,9 @@ mod imp { waiting_sub_page: TemplateChild::default(), split_view_sub_page: TemplateChild::default(), diagram: TemplateChild::default(), + diagram_filter_list_model: TemplateChild::default(), details_view: TemplateChild::default(), settings: gio::Settings::new(APP_ID), - handler_id: RefCell::default(), monitor: RefCell::default(), filename: RefCell::default(), } @@ -247,7 +250,7 @@ impl Window { }; let imp = self.imp(); - imp.diagram.set_model(Some(&model)); + imp.diagram_filter_list_model.set_model(Some(&model)); let filename = file.basename().unwrap_or_default().display().to_string(); imp.diagram_title.set_title(&filename); @@ -382,13 +385,11 @@ impl Window { } fn show_statistics(&self) { - let model = self - .imp() - .diagram - .model() - .expect("A model is required for statistics"); - let statistics = StatisticsWindow::new(&model, self); - statistics.present(); + let imp = self.imp(); + + debug_assert!(imp.diagram_filter_list_model.model().is_some()); + + StatisticsWindow::new(&imp.diagram_filter_list_model, self).present(); } fn show_filter_services(&self) { @@ -404,16 +405,6 @@ impl Window { .expect("monitor must be set when recording"); drop(monitor); - let model = imp - .diagram - .model() - .expect("diagram must have a model when recording"); - model.disconnect( - imp.handler_id - .take() - .expect("handler_id must be set when recording"), - ); - let filename = glib::DateTime::now_local() .unwrap() .format("%Y-%m-%d %H:%M:%S") @@ -421,7 +412,7 @@ impl Window { imp.diagram_title.set_title(&format!("*{filename}.pcap")); imp.filename.replace(Some(filename.to_string())); - if model.n_items() != 0 { + if imp.diagram_filter_list_model.n_items() != 0 { self.set_view(View::Diagram); imp.diagram_page_stack .set_visible_child(&*imp.split_view_sub_page); @@ -438,41 +429,13 @@ impl Window { monitor .start(clone!(@weak model => move |event| { - let timestamp = model.head().map(|i| i.timestamp()); - model.push(DiagramItem::from_event(event, timestamp)); + model.push(DiagramItem::from_event(event)); })) .await .context("Failed to start monitor")?; imp.monitor.replace(Some(monitor)); - let handler_id = model.connect_items_changed( - clone!(@weak self as obj => move |model, _, removed, added| { - if removed == 0 && added == 0 { - return; - } - - let imp = obj.imp(); - let n_messages = model.n_items(); - - imp.diagram_title.set_subtitle(&gettext_f( - // Translators: Do NOT translate the contents between '{' and '}', this is a variable name. - "Logged {n_messages} messages", - &[("n_messages", &n_messages.to_string())], - )); - - if n_messages != 0 { - imp.diagram_page_stack - .set_visible_child(&*imp.split_view_sub_page); - } - }), - ); - let prev_handler_id = imp.handler_id.replace(Some(handler_id)); - debug_assert!( - prev_handler_id.is_none(), - "handler id must be removed on stop" - ); - - imp.diagram.set_model(Some(&model)); + imp.diagram_filter_list_model.set_model(Some(&model)); imp.diagram_title.set_title(display_message); imp.diagram_title.set_subtitle(""); @@ -486,7 +449,7 @@ impl Window { async fn open_log(&self) -> anyhow::Result<()> { let filter = gtk::FileFilter::new(); - // TRANSLATORS PCAP is a type of file, do not translate. + // Translators: PCAP is a type of file, do not translate. filter.set_property("name", gettext("PCAP Files")); filter.add_mime_type("application/vnd.tcpdump.pcap"); @@ -507,7 +470,7 @@ impl Window { async fn save(&self) -> anyhow::Result<()> { let imp = self.imp(); let filter = gtk::FileFilter::new(); - // TRANSLATORS PCAP is a type of file, do not translate. + // Translators: PCAP is a type of file, do not translate. filter.set_property("name", gettext("PCAP Files")); filter.add_mime_type("application/vnd.tcpdump.pcap"); @@ -526,7 +489,7 @@ impl Window { let file = chooser.save_future(Some(self)).await?; let path = file.path().unwrap(); let model = imp - .diagram + .diagram_filter_list_model .model() .expect("A model must exists before saving"); model.save_to_file(path).await?; @@ -541,7 +504,7 @@ impl Window { async fn save_as_dot(&self) -> Result<()> { let imp = self.imp(); let filter = gtk::FileFilter::new(); - // TRANSLATORS Dot is a type of file, do not translate. + // Translators: Dot is a type of file, do not translate. filter.set_property("name", gettext("DOT Graph")); filter.add_mime_type("text/vnd.graphviz"); @@ -559,7 +522,7 @@ impl Window { let file = chooser.save_future(Some(self)).await?; let model = imp - .diagram + .diagram_filter_list_model .model() .expect("A model must exists before saving"); model.save_as_dot(&file).await?; @@ -587,6 +550,31 @@ impl Window { self.update_details_view(); } + #[template_callback] + fn diagram_filter_list_model_items_changed(&self, _position: u32, removed: u32, added: u32) { + if removed == 0 && added == 0 { + return; + } + + let imp = self.imp(); + let is_recording = imp.monitor.borrow().is_some(); + + if is_recording { + let n_messages = imp.diagram_filter_list_model.n_items(); + + imp.diagram_title.set_subtitle(&gettext_f( + // Translators: Do NOT translate the contents between '{' and '}', this is a variable name. + "Logged {n_messages} messages", + &[("n_messages", &n_messages.to_string())], + )); + + if n_messages != 0 { + imp.diagram_page_stack + .set_visible_child(&*imp.split_view_sub_page); + } + } + } + #[template_callback] fn copy_command_clicked(&self) { const CMD: &str = "dbus-monitor --pcap";