Skip to main content

ferroui_diagnostics/diagnostics/view_models/
tree_page_view_model.rs

1//! Port of `Diagnostics/ViewModels/TreePageViewModel.cs`.
2//!
3//! Upstream shows the nodes of a tree page in a tree view whose items are the nodes and whose
4//! selected item is `SelectedNode`. The port shows them in a TreeDataGrid: the page holds a
5//! hierarchical source of the nodes ([`TreePageViewModel::nodes_source`]) with one expander
6//! column, whose children are the children of a node (a
7//! [`TreeNodeCollection`](super::TreeNodeCollection), filled when the row of its node is shown or
8//! expanded) and whose expansion is `IsExpanded` of a node; the selection of the rows and
9//! `SelectedNode` follow each other (docs/porting/DEVIATIONS.md, Developer tools).
10
11use super::{ControlDetailsViewModel, FilterViewModel, MainViewModel, TreeNode, ViewModelBase};
12use ferroui_base::collections::FerroList;
13use ferroui_base::controls::ResourceKey;
14use ferroui_base::data::model::{Event, INotifyPropertyChanged};
15use ferroui_base::metadata::MarkupAssembly;
16use ferroui_base::reactive::{Disposable, IDisposable};
17use ferroui_base::utilities::HandlerList;
18use ferroui_base::{ferro_markup_type, Ref, TypeInfo, Visual};
19use ferroui_controls::{Control, GridLength};
20use ferroui_controls_treedatagrid::models::{HierarchicalExpanderColumn, IViewColumn, TemplateColumn, TemplateColumnOptions};
21use ferroui_controls_treedatagrid::TreeDataGridSourceRef;
22use ferroui_treedatagrid_core::models::ColumnRef;
23use ferroui_treedatagrid_core::selection::TreeSelectionModelSelectionChangedEventArgsOf;
24use ferroui_treedatagrid_core::{Accessor, HierarchicalTreeDataGridSource, IndexPath, Models};
25use std::cell::{Cell, RefCell};
26use std::collections::HashSet;
27use std::rc::{Rc, Weak};
28
29/// The key of the data template of the cells of the nodes, a resource of the view of a tree page.
30pub const TREE_NODE_CELL_TEMPLATE: &str = "TreeNodeCell";
31
32// The list of the root nodes, delivered to the items of the tree view.
33ferroui_controls::ferro_markup_list!(pub TreeNodeList: Rc<TreeNode>);
34
35/// The page of a tree of the developer tools (the logical or the visual
36/// tree): the root nodes, the selected node and the details of its control,
37/// and the commands of the context menu of the tree.
38pub struct TreePageViewModel {
39    base: ViewModelBase,
40    this: Weak<TreePageViewModel>,
41    selected_node: RefCell<Option<Rc<TreeNode>>>,
42    details: RefCell<Option<Rc<ControlDetailsViewModel>>>,
43    pinned_properties: Rc<RefCell<HashSet<String>>>,
44    clipboard_copy_requested: Rc<HandlerList<dyn Fn(&str)>>,
45    main_view: Weak<MainViewModel>,
46    properties_filter: Rc<FilterViewModel>,
47    setters_filter: Rc<FilterViewModel>,
48    nodes: RefCell<FerroList<Rc<TreeNode>>>,
49    filter_subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
50    // Deviation (DEVIATIONS.md, Developer tools): the source of the TreeDataGrid of the page, in
51    // place of the items of upstream's tree view, and whether the selection of its rows is being
52    // set from `SelectedNode` (or the reverse).
53    nodes_source: Rc<HierarchicalTreeDataGridSource<TreeNode>>,
54    updating_selection: Cell<bool>,
55}
56
57impl PartialEq for TreePageViewModel {
58    fn eq(&self, other: &Self) -> bool {
59        std::ptr::eq(self, other)
60    }
61}
62
63impl INotifyPropertyChanged for TreePageViewModel {
64    fn property_changed(&self) -> &Event<str> {
65        self.base.property_changed()
66    }
67}
68
69impl TreePageViewModel {
70    /// `TreePageViewModel(MainViewModel mainView, TreeNode[] nodes, ISet<string> pinnedProperties)`.
71    ///
72    /// The main view model owns its pages, so a page refers to it weakly.
73    /// `pinned_properties` is the set of the pinned properties that the
74    /// pages share.
75    pub fn new(
76        main_view: &Rc<MainViewModel>,
77        nodes: Vec<Rc<TreeNode>>,
78        pinned_properties: Rc<RefCell<HashSet<String>>>,
79    ) -> Rc<Self> {
80        Self::with_weak_main_view(Rc::downgrade(main_view), nodes, pinned_properties)
81    }
82
83    /// The constructor with the main view model given by a weak reference,
84    /// which may be empty: a page without a main view shows the implemented
85    /// interfaces and takes no screenshots.
86    pub fn with_weak_main_view(
87        main_view: Weak<MainViewModel>,
88        nodes: Vec<Rc<TreeNode>>,
89        pinned_properties: Rc<RefCell<HashSet<String>>>,
90    ) -> Rc<Self> {
91        TreeNode::register_model();
92        let nodes = FerroList::from_items(nodes);
93        let nodes_source = HierarchicalTreeDataGridSource::new(Rc::new(nodes.clone()) as Models<TreeNode>);
94        Self::create_nodes_columns(&nodes_source);
95
96        let this = Rc::new_cyclic(|this: &Weak<TreePageViewModel>| TreePageViewModel {
97            base: ViewModelBase::new(),
98            this: this.clone(),
99            selected_node: RefCell::new(None),
100            details: RefCell::new(None),
101            pinned_properties,
102            clipboard_copy_requested: Rc::new(HandlerList::new()),
103            main_view,
104            properties_filter: FilterViewModel::new(),
105            setters_filter: FilterViewModel::new(),
106            nodes: RefCell::new(nodes),
107            filter_subscriptions: RefCell::new(Vec::new()),
108            nodes_source,
109            updating_selection: Cell::new(false),
110        });
111
112        // Deviation (DEVIATIONS.md, Developer tools): the selected row of the TreeDataGrid is the
113        // selected node (the two-way binding of `SelectedItem` of upstream's tree view).
114        if let Some(row_selection) = this.nodes_source.row_selection() {
115            let weak = Rc::downgrade(&this);
116            row_selection.add_selection_changed(Rc::new(
117                move |_: &TreeSelectionModelSelectionChangedEventArgsOf<TreeNode>| {
118                    if let Some(this) = weak.upgrade() {
119                        this.on_row_selection_changed();
120                    }
121                },
122            ));
123        }
124
125        let weak = Rc::downgrade(&this);
126        let properties = this.properties_filter.refresh_filter(move || {
127            let details = weak.upgrade().and_then(|this| this.details());
128            if let Some(properties_view) = details.and_then(|details| details.properties_view()) {
129                properties_view.refresh();
130            }
131        });
132
133        let weak = Rc::downgrade(&this);
134        let setters = this.setters_filter.refresh_filter(move || {
135            if let Some(details) = weak.upgrade().and_then(|this| this.details()) {
136                details.update_style_filters();
137            }
138        });
139
140        this.filter_subscriptions.borrow_mut().extend([properties, setters]);
141        this
142    }
143
144    /// `ClipboardCopyRequested`: raised with a selector to copy to the
145    /// clipboard. Disposing the returned handle removes the handler.
146    pub fn clipboard_copy_requested(&self, handler: impl Fn(&str) + 'static) -> Rc<dyn IDisposable> {
147        let token = self.clipboard_copy_requested.add(Rc::new(handler));
148        let handlers = self.clipboard_copy_requested.clone();
149        Disposable::create(move || {
150            handlers.remove(token);
151        })
152    }
153
154    fn raise_clipboard_copy_requested(&self, selector: &str) {
155        for (_, handler) in self.clipboard_copy_requested.snapshot().iter() {
156            handler(selector);
157        }
158    }
159
160    /// The main view model; `None` once it is gone.
161    pub fn main_view(&self) -> Option<Rc<MainViewModel>> {
162        self.main_view.upgrade()
163    }
164
165    pub fn properties_filter(&self) -> Rc<FilterViewModel> {
166        self.properties_filter.clone()
167    }
168
169    pub fn setters_filter(&self) -> Rc<FilterViewModel> {
170        self.setters_filter.clone()
171    }
172
173    /// The root nodes of the tree.
174    pub fn nodes(&self) -> FerroList<Rc<TreeNode>> {
175        self.nodes.borrow().clone()
176    }
177
178    /// `protected set` of `Nodes`.
179    pub fn set_nodes(&self, value: Vec<Rc<TreeNode>>) {
180        let nodes = FerroList::from_items(value);
181        *self.nodes.borrow_mut() = nodes.clone();
182
183        // Deviation (DEVIATIONS.md, Developer tools): the root rows of the TreeDataGrid, with the
184        // selected node selected again when it is still in the tree.
185        self.updating_selection.set(true);
186        self.nodes_source.set_items(Rc::new(nodes) as Models<TreeNode>);
187        let selected = self.selected_node();
188        let reselected = self.select_row(selected.as_ref());
189        self.updating_selection.set(false);
190        if !reselected {
191            self.set_selected_node(None);
192        }
193    }
194
195    /// The column of the TreeDataGrid of the page: the expander of a node, around the cell of the
196    /// node (the data template [`TREE_NODE_CELL_TEMPLATE`] of the view: what the item template of
197    /// upstream's tree view shows), with the children of the node as the child rows and
198    /// `IsExpanded` of the node as the expansion of its row.
199    ///
200    /// Deviation (DEVIATIONS.md, Developer tools): upstream shows the nodes in a tree view.
201    fn create_nodes_columns(source: &HierarchicalTreeDataGridSource<TreeNode>) {
202        let options = TemplateColumnOptions::<TreeNode>::new();
203        options.set_can_user_resize_column(Some(false));
204        options.set_can_user_sort_column(Some(false));
205        let cell_column = TemplateColumn::from_resource_keys(
206            None,
207            ResourceKey::from(TREE_NODE_CELL_TEMPLATE),
208            None,
209            Some(GridLength::AUTO),
210            Some(Rc::new(options)),
211        );
212        source.columns().add(HierarchicalExpanderColumn::new(
213            cell_column as Rc<dyn IViewColumn<TreeNode>>,
214            // The children of a node are filled the first time they are asked for: when the row
215            // of the node is shown (whether it has children decides its expander) or expanded.
216            |node: &Rc<TreeNode>| Some(node.children() as Models<TreeNode>),
217            None,
218            Some(
219                Accessor::member("IsExpanded", |node: &Rc<TreeNode>| node.is_expanded())
220                    .with_setter(|node: &Rc<TreeNode>, value: bool| node.set_is_expanded(value)),
221            ),
222        ) as ColumnRef<TreeNode>);
223    }
224
225    /// The source of the TreeDataGrid of the page: the rows of the nodes.
226    pub fn nodes_source(&self) -> Rc<HierarchicalTreeDataGridSource<TreeNode>> {
227        self.nodes_source.clone()
228    }
229
230    /// The source of the TreeDataGrid of the page, as the grid takes it.
231    pub fn nodes_source_ref(&self) -> TreeDataGridSourceRef {
232        TreeDataGridSourceRef::of::<TreeNode, _>(self.nodes_source.clone())
233    }
234
235    /// The index path of the row of a node in the source: the index of each of its ancestors
236    /// among the children of its parent, from the root node down to the node; `None` when the
237    /// node is not in the trees of the page.
238    pub fn index_path_of(&self, node: &Rc<TreeNode>) -> Option<IndexPath> {
239        let mut indexes = Vec::new();
240        let mut current = node.clone();
241        loop {
242            match current.parent() {
243                Some(parent) => {
244                    let index = parent.children().index_of(&current);
245                    if index < 0 {
246                        return None;
247                    }
248                    indexes.push(index);
249                    current = parent;
250                }
251                None => {
252                    let index = self.nodes().index_of(&current)?;
253                    indexes.push(index as i32);
254                    break;
255                }
256            }
257        }
258        indexes.reverse();
259        Some(IndexPath::from_indexes(&indexes))
260    }
261
262    /// The index of the row of the selected node among the rows the TreeDataGrid shows; `-1`
263    /// when no node is selected or its row is not shown.
264    pub fn selected_row_index(&self) -> i32 {
265        match self.selected_node().and_then(|node| self.index_path_of(&node)) {
266            Some(index) => self.nodes_source.get_row_index(&index, 0),
267            None => -1,
268        }
269    }
270
271    /// Selects the row of a node, expanding the rows of its ancestors; `false` when the node is
272    /// not in the trees of the page (the selection is cleared then).
273    fn select_row(&self, node: Option<&Rc<TreeNode>>) -> bool {
274        let Some(selection) = self.nodes_source.row_selection() else {
275            return false;
276        };
277        match node.and_then(|node| self.index_path_of(node)) {
278            Some(index) => {
279                self.nodes_source.expand(&index.parent());
280                selection.base().set_selected_index(index);
281                true
282            }
283            None => {
284                selection.base().clear();
285                false
286            }
287        }
288    }
289
290    /// The selection of the rows changed: the selected row is the selected node.
291    fn on_row_selection_changed(&self) {
292        if self.updating_selection.get() {
293            return;
294        }
295        let selected = self.nodes_source.row_selection().and_then(|selection| selection.base().selected_item());
296        self.updating_selection.set(true);
297        self.set_selected_node(selected);
298        self.updating_selection.set(false);
299    }
300
301    pub fn selected_node(&self) -> Option<Rc<TreeNode>> {
302        self.selected_node.borrow().clone()
303    }
304
305    /// Selects a node, and shows the details of its object.
306    pub fn set_selected_node(&self, value: Option<Rc<TreeNode>>) {
307        if self.base.raise_and_set_if_changed(&self.selected_node, value.clone(), "SelectedNode") {
308            let details = match (value.clone(), self.this.upgrade()) {
309                (Some(value), Some(this)) => {
310                    Some(ControlDetailsViewModel::new(&this, &value.visual(), self.pinned_properties.clone()))
311                }
312                _ => None,
313            };
314            self.set_details(details);
315            if let Some(details) = self.details() {
316                details.update_properties_view(self.show_implemented_interfaces());
317                details.update_style_filters();
318            }
319
320            // Deviation (DEVIATIONS.md, Developer tools): the row of the node is selected, and the
321            // rows of its ancestors expanded, also when they were not shown (upstream's tree view
322            // selects the item of a node only when its container exists).
323            if !self.updating_selection.get() {
324                self.updating_selection.set(true);
325                self.select_row(value.as_ref());
326                self.updating_selection.set(false);
327            }
328        }
329    }
330
331    /// The details of the object of the selected node.
332    pub fn details(&self) -> Option<Rc<ControlDetailsViewModel>> {
333        self.details.borrow().clone()
334    }
335
336    fn set_details(&self, value: Option<Rc<ControlDetailsViewModel>>) {
337        let old_value = self.details();
338
339        // `RaiseAndSetIfChanged`: the details are a reference type, equal only to themselves.
340        let changed = match (&old_value, &value) {
341            (Some(old), Some(new)) => !Rc::ptr_eq(old, new),
342            (None, None) => false,
343            _ => true,
344        };
345        if changed {
346            *self.details.borrow_mut() = value;
347            self.base.raise_property_changed("Details");
348            if let Some(old_value) = old_value {
349                old_value.dispose();
350            }
351        }
352    }
353
354    /// `Dispose()`: disposes the nodes and the details.
355    pub fn dispose(&self) {
356        for node in self.nodes().snapshot().iter() {
357            node.dispose();
358        }
359
360        if let Some(details) = self.details() {
361            details.dispose();
362        }
363
364        // Deviation (DEVIATIONS.md, Developer tools): the rows of the TreeDataGrid end their
365        // subscriptions to the nodes.
366        self.nodes_source.dispose();
367    }
368
369    /// The node of `control`, searched in the trees of the root nodes.
370    pub fn find_node(&self, control: &Ref<Control>) -> Option<Rc<TreeNode>> {
371        for node in self.nodes().snapshot().iter() {
372            let result = self.find_node_in(node, control);
373
374            if result.is_some() {
375                return result;
376            }
377        }
378
379        None
380    }
381
382    /// Selects the node of `control`, or of its nearest visual ancestor that
383    /// has one, and expands its ancestors.
384    pub fn select_control(&self, control: &Ref<Control>) {
385        let mut node: Option<Rc<TreeNode>> = None;
386        let mut c: Option<Ref<Control>> = Some(control.clone());
387
388        while node.is_none() {
389            let Some(current) = c.as_ref() else { break };
390            node = self.find_node(current);
391
392            if node.is_none() {
393                c = current.get_visual_parent_of_type::<Control>();
394            }
395        }
396
397        if let Some(node) = node {
398            self.set_selected_node(Some(node.clone()));
399            self.expand_node(node.parent());
400        }
401    }
402
403    /// Requests the copy of the selector of the selected control.
404    pub fn copy_selector(&self) {
405        let current_visual = self.selected_node().and_then(|node| node.visual().cast::<Visual>());
406        if let Some(current_visual) = current_visual {
407            let selector = Self::get_visual_selector(&current_visual);
408
409            self.raise_clipboard_copy_requested(&selector);
410        }
411    }
412
413    /// Requests the copy of the selector of the selected control from the
414    /// outermost templated parent, through the templates.
415    pub fn copy_selector_from_template_parent(&self) {
416        let mut parts = Vec::new();
417
418        let mut current_visual = self.selected_node().and_then(|node| node.visual().cast::<Visual>());
419        while let Some(visual) = current_visual {
420            parts.push(Self::get_visual_selector(&visual));
421
422            current_visual = visual.templated_parent().and_then(|parent| parent.cast::<Visual>());
423        }
424
425        if !parts.is_empty() {
426            parts.reverse();
427            let selector = parts.join(" /template/ ");
428
429            self.raise_clipboard_copy_requested(&selector);
430        }
431    }
432
433    /// Expands the selected node, its ancestors and all its descendants.
434    pub fn expand_recursively(&self) {
435        if let Some(selected_node) = self.selected_node() {
436            self.expand_node(Some(selected_node.clone()));
437
438            let mut stack = vec![selected_node];
439
440            while let Some(item) = stack.pop() {
441                item.set_is_expanded(true);
442                for child in item.children().to_vec() {
443                    stack.push(child);
444                }
445            }
446        }
447    }
448
449    /// Collapses the selected node and all its descendants.
450    pub fn collapse_children(&self) {
451        if let Some(selected_node) = self.selected_node() {
452            let mut stack = vec![selected_node];
453
454            while let Some(item) = stack.pop() {
455                item.set_is_expanded(false);
456                for child in item.children().to_vec() {
457                    stack.push(child);
458                }
459            }
460        }
461    }
462
463    /// Takes a screenshot of the selected control.
464    pub fn capture_node_screenshot(&self) {
465        if let Some(main_view) = self.main_view() {
466            main_view.shot(None);
467        }
468    }
469
470    /// Brings the selected control into view.
471    pub fn bring_into_view(&self) {
472        if let Some(control) = self.selected_node().and_then(|node| node.visual().cast::<Control>()) {
473            control.bring_into_view();
474        }
475    }
476
477    /// Focuses the selected control.
478    pub fn focus(&self) {
479        if let Some(control) = self.selected_node().and_then(|node| node.visual().cast::<Control>()) {
480            let _ = control.focus();
481        }
482    }
483
484    /// The selector of a visual: `{assembly}namespace|Type`, then the name
485    /// (`#name`), the classes (`.class`) and the pseudo-classes.
486    fn get_visual_selector(visual: &Ref<Visual>) -> String {
487        let name = visual.name().filter(|name| !name.is_empty()).map(|name| format!("#{name}")).unwrap_or_default();
488        let classes = visual.classes().snapshot();
489        let pseudo: String = classes.iter().filter(|c| c.starts_with(':')).map(String::as_str).collect();
490        let classes: String = classes.iter().filter(|c| !c.starts_with(':')).map(|c| format!(".{c}")).collect();
491        let type_ = visual.style_key();
492        format!(
493            "{{{}}}{}|{}{name}{classes}{pseudo}",
494            Self::assembly_full_name(type_),
495            type_.namespace(),
496            type_.name()
497        )
498    }
499
500    /// `type.Assembly.FullName`.
501    ///
502    /// Deviation (DEVIATIONS.md, Developer tools): the port has no assembly identity (version,
503    /// culture and public key token); the name of the assembly that declares the type stands for
504    /// it, or the name of its crate when the assembly is not registered.
505    fn assembly_full_name(type_: &'static TypeInfo) -> String {
506        let module_path = type_.module_path();
507        match MarkupAssembly::of_module(module_path) {
508            Some(assembly) => assembly.name.to_string(),
509            None => module_path.split("::").next().unwrap_or_default().to_string(),
510        }
511    }
512
513    fn expand_node(&self, node: Option<Rc<TreeNode>>) {
514        if let Some(node) = node {
515            node.set_is_expanded(true);
516            self.expand_node(node.parent());
517        }
518    }
519
520    fn find_node_in(&self, node: &Rc<TreeNode>, control: &Ref<Control>) -> Option<Rc<TreeNode>> {
521        if node.visual().ptr_eq(control) {
522            return Some(node.clone());
523        } else {
524            for child in node.children().to_vec() {
525                let result = self.find_node_in(&child, control);
526
527                if result.is_some() {
528                    return result;
529                }
530            }
531        }
532
533        None
534    }
535
536    /// Updates the properties of the details with the current setting of
537    /// the main view (shown with the implemented interfaces when there is
538    /// no main view).
539    pub fn update_properties_view(&self) {
540        if let Some(details) = self.details() {
541            details.update_properties_view(self.show_implemented_interfaces());
542        }
543    }
544
545    /// `MainView?.ShowImplementedInterfaces ?? true`.
546    fn show_implemented_interfaces(&self) -> bool {
547        self.main_view().is_none_or(|main_view| main_view.show_implemented_interfaces())
548    }
549}
550
551ferro_markup_type!(class TreePageViewModel {
552    this: Rc<TreePageViewModel>,
553    handles: [TreePageViewModel, Rc<TreePageViewModel>, Option<Rc<TreePageViewModel>>],
554    properties: [
555        MainView: Option<Rc<MainViewModel>> { get: TreePageViewModel::main_view },
556        PropertiesFilter: Rc<FilterViewModel> { get: TreePageViewModel::properties_filter },
557        SettersFilter: Rc<FilterViewModel> { get: TreePageViewModel::setters_filter },
558        Nodes: FerroList<Rc<TreeNode>> { get: TreePageViewModel::nodes },
559        // Deviation (DEVIATIONS.md, Developer tools): the source of the TreeDataGrid of the page.
560        NodesSource: TreeDataGridSourceRef { get: TreePageViewModel::nodes_source_ref },
561        SelectedNode: Option<Rc<TreeNode>> {
562            get: TreePageViewModel::selected_node,
563            set: TreePageViewModel::set_selected_node
564        },
565        Details: Option<Rc<ControlDetailsViewModel>> { get: TreePageViewModel::details },
566    ],
567    methods: [
568        fn CopySelector() => TreePageViewModel::copy_selector,
569        fn CopySelectorFromTemplateParent() => TreePageViewModel::copy_selector_from_template_parent,
570        fn ExpandRecursively() => TreePageViewModel::expand_recursively,
571        fn CollapseChildren() => TreePageViewModel::collapse_children,
572        fn CaptureNodeScreenshot() => TreePageViewModel::capture_node_screenshot,
573        fn BringIntoView() => TreePageViewModel::bring_into_view,
574        fn Focus() => TreePageViewModel::focus,
575    ],
576    notify_property_changed: TreePageViewModel,
577});
578
579/// What the untyped value conversions must know about the view models of
580/// the trees: they are reference types, and the children of a node are
581/// delivered to the items of a tree view. Registered once with
582/// `ValueTypes::register_global` from the `register_types()` of the crate.
583pub fn register_tree_value_types() {
584    use ferroui_base::data::core::ValueTypes;
585    use ferroui_controls::ItemsSource;
586
587    ValueTypes::register_reference::<TreeNode>();
588    ValueTypes::register_reference::<super::TreeNodeCollection>();
589    ValueTypes::register_reference::<TreePageViewModel>();
590    ValueTypes::register_reference::<FilterViewModel>();
591    // The children are delivered as the shared handle, or as the object itself (the untyped
592    // form of a reference type).
593    ItemsSource::register_binding_conversion_with::<Rc<super::TreeNodeCollection>>(|children| {
594        ItemsSource::new(children.clone())
595    });
596    ItemsSource::register_binding_conversion_with::<super::TreeNodeCollection>(|children| {
597        ItemsSource::new(children.to_rc())
598    });
599}