Skip to main content

ferroui_diagnostics/diagnostics/view_models/
events_page_view_model.rs

1//! Port of `Diagnostics/ViewModels/EventsPageViewModel.cs`.
2//!
3//! Upstream shows the tree of the routed events in a tree view whose items are the nodes, hidden
4//! by the filter through their `IsVisible`, and whose selected item is `SelectedNode`. The port
5//! shows them in a TreeDataGrid: the page holds a hierarchical source of the visible nodes
6//! ([`EventsPageViewModel::nodes_source`]) with one expander column, whose children are the
7//! visible children of a node and whose expansion is `IsExpanded` of a node; the selection of the
8//! rows and `SelectedNode` follow each other (docs/porting/DEVIATIONS.md, Developer tools).
9
10use super::event_tree_node_base::sync_visible_nodes;
11use super::{EventOwnerTreeNode, EventTreeNodeBase, FilterViewModel, FiredEvent, MainViewModel, ViewModelBase};
12use crate::diagnostics::models::EventChainLink;
13use ferroui_base::collections::FerroList;
14use ferroui_base::controls::ResourceKey;
15use ferroui_base::data::core::ValueTypes;
16use ferroui_base::data::model::{Event, INotifyPropertyChanged};
17use ferroui_base::ferro_markup_type;
18use ferroui_base::input::InputElement;
19use ferroui_base::interactivity::{RoutedEvent, RoutedEventRegistry};
20use ferroui_base::TypeInfo;
21use ferroui_controls::{Button, Control, GridLength, GridUnitType};
22use ferroui_controls_treedatagrid::models::{HierarchicalExpanderColumn, IViewColumn, TemplateColumn, TemplateColumnOptions};
23use ferroui_controls_treedatagrid::TreeDataGridSourceRef;
24use ferroui_treedatagrid_core::models::ColumnRef;
25use ferroui_treedatagrid_core::selection::TreeSelectionModelSelectionChangedEventArgsOf;
26use ferroui_treedatagrid_core::{Accessor, HierarchicalTreeDataGridSource, IndexPath, Models};
27use std::cell::{Cell, RefCell};
28use std::collections::HashSet;
29use std::rc::{Rc, Weak};
30
31/// The key of the data template of the cells of the nodes of the tree of the events, a resource
32/// of the view of the events page.
33pub const EVENT_TREE_NODE_CELL_TEMPLATE: &str = "EventTreeNodeCell";
34
35/// The events recorded when the page is created, and again with "Enable
36/// default".
37fn default_events() -> HashSet<RoutedEvent> {
38    HashSet::from([
39        Button::click_event().as_routed_event(),
40        InputElement::key_down_event().as_routed_event(),
41        InputElement::key_up_event().as_routed_event(),
42        InputElement::text_input_event().as_routed_event(),
43        InputElement::pointer_released_event().as_routed_event(),
44        InputElement::pointer_pressed_event().as_routed_event(),
45    ])
46}
47
48/// The view model of the events page: the tree of the registered routed
49/// events grouped by the type that owns them, whose check boxes choose the
50/// events to record, and the recorded events.
51pub struct EventsPageViewModel {
52    base: ViewModelBase,
53    main_view_model: Weak<MainViewModel>,
54    selected_event: RefCell<Option<Rc<FiredEvent>>>,
55    selected_node: RefCell<Option<Rc<EventTreeNodeBase>>>,
56    nodes: FerroList<Rc<EventTreeNodeBase>>,
57    recorded_events: FerroList<Rc<FiredEvent>>,
58    events_filter: Rc<FilterViewModel>,
59    // Deviation (DEVIATIONS.md, Developer tools): the visible nodes (the root rows), the source of
60    // the TreeDataGrid of the page, in place of the items of upstream's tree view, and whether the
61    // selection of its rows is being set from `SelectedNode` (or the reverse).
62    visible_nodes: Rc<FerroList<Rc<EventTreeNodeBase>>>,
63    nodes_source: Rc<HierarchicalTreeDataGridSource<EventTreeNodeBase>>,
64    updating_selection: Cell<bool>,
65}
66
67/// Reference equality, as the class of the original.
68impl PartialEq for EventsPageViewModel {
69    fn eq(&self, other: &Self) -> bool {
70        std::ptr::eq(self, other)
71    }
72}
73
74impl INotifyPropertyChanged for EventsPageViewModel {
75    fn property_changed(&self) -> &Event<str> {
76        self.base.property_changed()
77    }
78}
79
80impl EventsPageViewModel {
81    /// C# `new EventsPageViewModel(MainViewModel mainViewModel)`: the tree of
82    /// the routed events registered on this thread, with the default events
83    /// enabled.
84    ///
85    /// The main view model is held weakly: it holds the page.
86    pub fn new(main_view_model: Weak<MainViewModel>) -> Rc<Self> {
87        // Deviation (DEVIATIONS.md, Developer tools): the static `DefaultEvents` of the original
88        // is initialized before the constructor runs, which registers its events; here they
89        // register on first access, so the set is made before the registered events are read.
90        let _ = default_events();
91        EventTreeNodeBase::register_model();
92        let this = Rc::new_cyclic(|this: &Weak<EventsPageViewModel>| {
93            // `GroupBy(e => e.OwnerType)`: the groups in the order of their first event.
94            let mut groups: Vec<(&'static TypeInfo, Vec<RoutedEvent>)> = Vec::new();
95            for e in RoutedEventRegistry::instance().get_all_registered() {
96                let owner_type = e.owner_type();
97                match groups.iter_mut().find(|(key, _)| std::ptr::eq(*key, owner_type)) {
98                    Some((_, events)) => events.push(e),
99                    None => groups.push((owner_type, vec![e])),
100                }
101            }
102            // `OrderBy(e => e.Key.Name)` is a stable sort.
103            groups.sort_by(|(x, _), (y, _)| x.name().cmp(y.name()));
104
105            let nodes =
106                FerroList::from_items(groups.into_iter().map(|(key, events)| EventOwnerTreeNode::new(key, events, this)));
107            let visible_nodes = Rc::new(FerroList::from_items(nodes.to_vec()));
108            let nodes_source =
109                HierarchicalTreeDataGridSource::new(visible_nodes.clone() as Models<EventTreeNodeBase>);
110            Self::create_nodes_columns(&nodes_source);
111
112            Self {
113                base: ViewModelBase::new(),
114                main_view_model,
115                selected_event: RefCell::new(None),
116                selected_node: RefCell::new(None),
117                nodes,
118                recorded_events: FerroList::new(),
119                events_filter: FilterViewModel::new(),
120                visible_nodes,
121                nodes_source,
122                updating_selection: Cell::new(false),
123            }
124        });
125
126        // Deviation (DEVIATIONS.md, Developer tools): the selected row of the TreeDataGrid is the
127        // selected node (the two-way binding of `SelectedItem` of upstream's tree view).
128        if let Some(row_selection) = this.nodes_source.row_selection() {
129            let weak = Rc::downgrade(&this);
130            row_selection.add_selection_changed(Rc::new(
131                move |_: &TreeSelectionModelSelectionChangedEventArgsOf<EventTreeNodeBase>| {
132                    if let Some(this) = weak.upgrade() {
133                        this.on_row_selection_changed();
134                    }
135                },
136            ));
137        }
138
139        let weak = Rc::downgrade(&this);
140        // As upstream, the handler is never removed: the filter belongs to the page.
141        let _ = this.events_filter.refresh_filter(move || {
142            if let Some(this) = weak.upgrade() {
143                this.update_event_filters();
144            }
145        });
146
147        this.enable_default();
148        this
149    }
150
151    /// The title of the page.
152    pub fn name(&self) -> String {
153        "Events".to_string()
154    }
155
156    /// The nodes of the types that own routed events, ordered by the names
157    /// of the types.
158    pub fn nodes(&self) -> FerroList<Rc<EventTreeNodeBase>> {
159        self.nodes.clone()
160    }
161
162    /// The recorded events, the oldest first.
163    pub fn recorded_events(&self) -> FerroList<Rc<FiredEvent>> {
164        self.recorded_events.clone()
165    }
166
167    pub fn selected_event(&self) -> Option<Rc<FiredEvent>> {
168        self.selected_event.borrow().clone()
169    }
170
171    pub fn set_selected_event(&self, value: Option<Rc<FiredEvent>>) {
172        self.base.raise_and_set_if_changed(&self.selected_event, value, "SelectedEvent");
173    }
174
175    pub fn selected_node(&self) -> Option<Rc<EventTreeNodeBase>> {
176        self.selected_node.borrow().clone()
177    }
178
179    pub fn set_selected_node(&self, value: Option<Rc<EventTreeNodeBase>>) {
180        if self.base.raise_and_set_if_changed(&self.selected_node, value.clone(), "SelectedNode") {
181            // Deviation (DEVIATIONS.md, Developer tools): the row of the node is selected, and the
182            // row of its owner expanded.
183            if !self.updating_selection.get() {
184                self.updating_selection.set(true);
185                self.select_row(value.as_ref());
186                self.updating_selection.set(false);
187            }
188        }
189    }
190
191    /// The column of the TreeDataGrid of the page: the expander of a node, around the cell of the
192    /// node (the data template [`EVENT_TREE_NODE_CELL_TEMPLATE`] of the view: the check box of
193    /// `IsEnabled` with the name of the node, what the item template of upstream's tree view
194    /// shows), with the visible children of the node as the child rows and `IsExpanded` of the
195    /// node as the expansion of its row.
196    ///
197    /// Deviation (DEVIATIONS.md, Developer tools): upstream shows the nodes in a tree view.
198    fn create_nodes_columns(source: &HierarchicalTreeDataGridSource<EventTreeNodeBase>) {
199        let options = TemplateColumnOptions::<EventTreeNodeBase>::new();
200        options.set_can_user_resize_column(Some(false));
201        options.set_can_user_sort_column(Some(false));
202        let cell_column = TemplateColumn::from_resource_keys(
203            None,
204            ResourceKey::from(EVENT_TREE_NODE_CELL_TEMPLATE),
205            None,
206            Some(GridLength::new(1.0, GridUnitType::Star)),
207            Some(Rc::new(options)),
208        );
209        source.columns().add(HierarchicalExpanderColumn::new(
210            cell_column as Rc<dyn IViewColumn<EventTreeNodeBase>>,
211            |node: &Rc<EventTreeNodeBase>| {
212                node.visible_children().map(|children| children as Models<EventTreeNodeBase>)
213            },
214            None,
215            Some(
216                Accessor::member("IsExpanded", |node: &Rc<EventTreeNodeBase>| node.is_expanded())
217                    .with_setter(|node: &Rc<EventTreeNodeBase>, value: bool| node.set_is_expanded(value)),
218            ),
219        ) as ColumnRef<EventTreeNodeBase>);
220    }
221
222    /// The source of the TreeDataGrid of the page: the rows of the visible nodes.
223    pub fn nodes_source(&self) -> Rc<HierarchicalTreeDataGridSource<EventTreeNodeBase>> {
224        self.nodes_source.clone()
225    }
226
227    /// The source of the TreeDataGrid of the page, as the grid takes it.
228    pub fn nodes_source_ref(&self) -> TreeDataGridSourceRef {
229        TreeDataGridSourceRef::of::<EventTreeNodeBase, _>(self.nodes_source.clone())
230    }
231
232    /// The visible nodes of the owners of events: the root rows of the TreeDataGrid of the page.
233    pub fn visible_nodes(&self) -> Rc<FerroList<Rc<EventTreeNodeBase>>> {
234        self.visible_nodes.clone()
235    }
236
237    /// The index path of the row of a node in the source; `None` when the node is not visible.
238    pub fn index_path_of(&self, node: &Rc<EventTreeNodeBase>) -> Option<IndexPath> {
239        match node.parent() {
240            Some(parent) => {
241                let parent_index = self.visible_nodes.index_of(&parent)?;
242                let index = parent.visible_children()?.index_of(node)?;
243                Some(IndexPath::new(parent_index as i32).append(index as i32))
244            }
245            None => self.visible_nodes.index_of(node).map(|index| IndexPath::new(index as i32)),
246        }
247    }
248
249    /// The index of the row of the selected node among the rows the TreeDataGrid shows; `-1`
250    /// when no node is selected or its row is not shown.
251    pub fn selected_row_index(&self) -> i32 {
252        match self.selected_node().and_then(|node| self.index_path_of(&node)) {
253            Some(index) => self.nodes_source.get_row_index(&index, 0),
254            None => -1,
255        }
256    }
257
258    /// Selects the row of a node, expanding the row of its owner; `false` when the node is not
259    /// visible (the selection is cleared then).
260    fn select_row(&self, node: Option<&Rc<EventTreeNodeBase>>) -> bool {
261        let Some(selection) = self.nodes_source.row_selection() else {
262            return false;
263        };
264        match node.and_then(|node| self.index_path_of(node)) {
265            Some(index) => {
266                self.nodes_source.expand(&index.parent());
267                selection.base().set_selected_index(index);
268                true
269            }
270            None => {
271                selection.base().clear();
272                false
273            }
274        }
275    }
276
277    /// The selection of the rows changed: the selected row is the selected node.
278    fn on_row_selection_changed(&self) {
279        if self.updating_selection.get() {
280            return;
281        }
282        let selected = self.nodes_source.row_selection().and_then(|selection| selection.base().selected_item());
283        self.updating_selection.set(true);
284        self.set_selected_node(selected);
285        self.updating_selection.set(false);
286    }
287
288    /// The filter of the tree of the events.
289    pub fn events_filter(&self) -> Rc<FilterViewModel> {
290        self.events_filter.clone()
291    }
292
293    /// Removes the recorded events.
294    pub fn clear(&self) {
295        self.recorded_events.clear();
296    }
297
298    /// Stops recording every event.
299    pub fn disable_all(&self) {
300        self.evaluate_node_enabled(&|_: &EventTreeNodeBase| false);
301    }
302
303    /// Records the default events and no other.
304    pub fn enable_default(&self) {
305        let default_events = default_events();
306        self.evaluate_node_enabled(&|node: &EventTreeNodeBase| {
307            let event = node.as_event_tree_node().expect("a node of an event").event();
308            default_events.contains(&event)
309        });
310    }
311
312    /// Shows the element of a link of the chain of an event in the visual
313    /// tree page.
314    pub fn request_tree_navigate_to(&self, nav_target: &EventChainLink) {
315        if let Some(control) = ValueTypes::as_object(&*nav_target.handler()).and_then(|handler| handler.cast::<Control>())
316        {
317            if let Some(main_view_model) = self.main_view() {
318                main_view_model.request_tree_navigate_to(&control, true);
319            }
320        }
321    }
322
323    /// Selects the node of `evt` in the tree, if it is visible.
324    pub fn select_event_by_type(&self, evt: RoutedEvent) {
325        fn find_node(node: &Rc<EventTreeNodeBase>, event_type: RoutedEvent) -> Option<Rc<EventTreeNodeBase>> {
326            if node.as_event_tree_node().is_some_and(|event_node| event_node.event() == event_type) {
327                return Some(node.clone());
328            }
329
330            if let Some(children) = node.children() {
331                for child in children.to_vec() {
332                    let result = find_node(&child, event_type);
333
334                    if result.is_some() {
335                        return result;
336                    }
337                }
338            }
339
340            None
341        }
342
343        for node in self.nodes.to_vec() {
344            let result = find_node(&node, evt);
345
346            if let Some(result) = result.filter(|result| result.is_visible()) {
347                self.set_selected_node(Some(result));
348
349                break;
350            }
351        }
352    }
353
354    /// Sets `IsEnabled` of every node of an event to `eval` of the node.
355    fn evaluate_node_enabled(&self, eval: &dyn Fn(&EventTreeNodeBase) -> bool) {
356        fn process_node(node: &Rc<EventTreeNodeBase>, eval: &dyn Fn(&EventTreeNodeBase) -> bool) {
357            if node.as_event_tree_node().is_some() {
358                node.set_is_enabled(Some(eval(&**node)));
359            }
360
361            if let Some(children) = node.children() {
362                for child_node in children.to_vec() {
363                    process_node(&child_node, eval);
364                }
365            }
366        }
367
368        for node in self.nodes.to_vec() {
369            process_node(&node, eval);
370        }
371    }
372
373    /// Shows the nodes whose text matches the filter, their ancestors and
374    /// their descendants.
375    fn update_event_filters(&self) {
376        fn filter_node(filter: &FilterViewModel, node: &Rc<EventTreeNodeBase>, is_parent_visible: bool) -> bool {
377            let matches_filter = filter.filter(node.text());
378            let mut has_visible_child = false;
379
380            if let Some(children) = node.children() {
381                for child_node in children.to_vec() {
382                    has_visible_child |= filter_node(filter, &child_node, matches_filter);
383                }
384            }
385
386            node.set_is_visible(has_visible_child || matches_filter || is_parent_visible);
387
388            node.is_visible()
389        }
390
391        for node in self.nodes.to_vec() {
392            filter_node(&self.events_filter, &node, false);
393        }
394
395        // Deviation (DEVIATIONS.md, Developer tools): the rows of the TreeDataGrid are the visible
396        // nodes. As in upstream's tree view, hiding the selected node does not change
397        // `SelectedNode`; its row is selected again when it is shown.
398        self.updating_selection.set(true);
399        for node in self.nodes.to_vec() {
400            node.update_visible_children();
401        }
402        sync_visible_nodes(&self.nodes.to_vec(), &self.visible_nodes);
403        let selected = self.selected_node();
404        self.select_row(selected.as_ref());
405        self.updating_selection.set(false);
406    }
407
408    /// The main view model the page belongs to.
409    pub fn main_view(&self) -> Option<Rc<MainViewModel>> {
410        self.main_view_model.upgrade()
411    }
412}
413
414ferro_markup_type!(class EventsPageViewModel {
415    this: Rc<EventsPageViewModel>,
416    handles: [EventsPageViewModel, Rc<EventsPageViewModel>, Option<Rc<EventsPageViewModel>>],
417    base: ViewModelBase,
418    properties: [
419        Name: String { get: EventsPageViewModel::name },
420        Nodes: FerroList<Rc<EventTreeNodeBase>> { get: EventsPageViewModel::nodes },
421        RecordedEvents: FerroList<Rc<FiredEvent>> { get: EventsPageViewModel::recorded_events },
422        SelectedEvent: Option<Rc<FiredEvent>> {
423            get: EventsPageViewModel::selected_event,
424            set: EventsPageViewModel::set_selected_event
425        },
426        SelectedNode: Option<Rc<EventTreeNodeBase>> {
427            get: EventsPageViewModel::selected_node,
428            set: EventsPageViewModel::set_selected_node
429        },
430        EventsFilter: Rc<FilterViewModel> { get: EventsPageViewModel::events_filter },
431        // Deviation (DEVIATIONS.md, Developer tools): the source of the TreeDataGrid of the page.
432        NodesSource: TreeDataGridSourceRef { get: EventsPageViewModel::nodes_source_ref },
433        MainView: Option<Rc<MainViewModel>> { get: EventsPageViewModel::main_view },
434    ],
435    methods: [
436        fn Clear() => EventsPageViewModel::clear,
437        fn DisableAll() => EventsPageViewModel::disable_all,
438        fn EnableDefault() => EventsPageViewModel::enable_default,
439        fn RequestTreeNavigateTo(Rc<EventChainLink>) =>
440            (|this: &Rc<EventsPageViewModel>, nav_target: Rc<EventChainLink>| this.request_tree_navigate_to(&nav_target)),
441        fn SelectEventByType(RoutedEvent) => EventsPageViewModel::select_event_by_type,
442    ],
443    notify_property_changed: EventsPageViewModel,
444});