Skip to main content

ferroui_diagnostics/diagnostics/view_models/
ferro_property_view_model.rs

1//! Port of `Diagnostics/ViewModels/FerroPropertyViewModel.cs` (the class is named after the
2//! framework's property type).
3
4use super::property_view_model::runtime_type_of;
5use super::{PropertyViewModel, PropertyViewModelKey, PropertyViewModelRef};
6use ferroui_base::data::core::{ValueType, ValueTypes};
7use ferroui_base::data::model::{Event, INotifyPropertyChanged};
8use ferroui_base::data::BindingPriority;
9use ferroui_base::diagnostics::FerroObjectDiagnosticExtensions;
10use ferroui_base::{ferro_markup_type, BoxedValue, FerroObject, FerroProperty, Ref};
11use std::cell::RefCell;
12use std::ops::Deref;
13use std::rc::Rc;
14
15/// The view model of a registered property of an object: its value, the priority the value has
16/// and the group the property is shown in.
17pub struct FerroPropertyViewModel {
18    base: PropertyViewModel,
19    target: Ref<FerroObject>,
20    assigned_type: RefCell<ValueType>,
21    value: RefCell<Option<BoxedValue>>,
22    priority: RefCell<String>,
23    group: RefCell<String>,
24    property_type: ValueType,
25    property: &'static FerroProperty,
26    name: String,
27    declaring_type: Option<ValueType>,
28}
29
30impl PartialEq for FerroPropertyViewModel {
31    fn eq(&self, other: &Self) -> bool {
32        std::ptr::eq(self, other)
33    }
34}
35
36impl Deref for FerroPropertyViewModel {
37    type Target = PropertyViewModel;
38
39    fn deref(&self) -> &PropertyViewModel {
40        &self.base
41    }
42}
43
44impl INotifyPropertyChanged for FerroPropertyViewModel {
45    fn property_changed(&self) -> &Event<str> {
46        self.base.property_changed()
47    }
48}
49
50impl FerroPropertyViewModel {
51    pub fn new(o: &FerroObject, property: &'static FerroProperty) -> Rc<Self> {
52        let name = if property.is_attached() {
53            format!("[{}.{}]", property.owner_type().name(), property.name())
54        } else {
55            property.name().to_string()
56        };
57        let owner_type = property.owner_type();
58        let declaring_type = owner_type.handle().map(|handle| ValueType::new(handle, owner_type.name()));
59        let property_type = ValueType::new(property.property_type(), property.property_type_name());
60
61        let this = Rc::new(Self {
62            base: PropertyViewModel::construct(),
63            target: o.to_ref(),
64            assigned_type: RefCell::new(property_type),
65            value: RefCell::new(None),
66            priority: RefCell::new(String::new()),
67            group: RefCell::new(String::new()),
68            property_type,
69            property,
70            name,
71            declaring_type,
72        });
73
74        this.update();
75
76        this
77    }
78
79    /// The handle of the view model typed with the abstract class.
80    pub fn to_property_view_model(self: &Rc<Self>) -> PropertyViewModelRef {
81        PropertyViewModelRef::Ferro(self.clone())
82    }
83
84    pub fn property(&self) -> &'static FerroProperty {
85        self.property
86    }
87
88    pub fn key(&self) -> PropertyViewModelKey {
89        PropertyViewModelKey::Property(self.property)
90    }
91
92    pub fn name(&self) -> String {
93        self.name.clone()
94    }
95
96    pub fn is_attached(&self) -> Option<bool> {
97        Some(self.property.is_attached())
98    }
99
100    pub fn priority(&self) -> String {
101        self.priority.borrow().clone()
102    }
103
104    pub fn assigned_type(&self) -> ValueType {
105        *self.assigned_type.borrow()
106    }
107
108    pub fn value(&self) -> Option<BoxedValue> {
109        self.value.borrow().clone()
110    }
111
112    /// Sets the property of the object to `value` (a local value) and reads it back. A value the
113    /// property does not take is ignored (the managed original swallows the exception).
114    pub fn set_value(&self, value: Option<BoxedValue>) {
115        if self.property.is_read_only() {
116            return;
117        }
118        let property_type = self.property_type;
119        let Some(Some(converted)) = ValueTypes::try_convert(value.as_ref(), property_type) else {
120            return;
121        };
122        if !self.property.is_valid_value(converted.as_any()) {
123            return;
124        }
125
126        self.target.set_value_untyped(self.property, converted.as_any(), BindingPriority::LocalValue);
127        self.update();
128    }
129
130    pub fn group(&self) -> String {
131        if self.is_pinned() {
132            "Pinned".to_string()
133        } else {
134            self.group.borrow().clone()
135        }
136    }
137
138    pub fn declaring_type(&self) -> Option<ValueType> {
139        self.declaring_type
140    }
141
142    pub fn property_type(&self) -> ValueType {
143        self.property_type
144    }
145
146    pub fn is_readonly(&self) -> bool {
147        self.property.is_read_only()
148    }
149
150    pub fn update(&self) {
151        let base = self.base.view_model_base();
152
153        if self.property.is_direct() {
154            // The getter of a direct property cannot fail here (the managed original reports
155            // the exception of a getter as the value).
156            let value = ValueTypes::normalize(self.target.get_value_untyped(self.property));
157            let value_type = runtime_type_of(value.as_ref());
158
159            base.raise_and_set_if_changed(&self.value, value, "Value");
160            base.raise_and_set_if_changed(&self.assigned_type, value_type.unwrap_or(self.property_type), "AssignedType");
161            base.raise_and_set_if_changed(&self.priority, "Direct".to_string(), "Priority");
162
163            *self.group.borrow_mut() = "Properties".to_string();
164        } else {
165            let diag = self.target.get_diagnostic(self.property);
166
167            let value = ValueTypes::normalize(diag.value().clone());
168            let value_type = runtime_type_of(value.as_ref());
169            let priority: Option<BindingPriority> = Some(diag.priority());
170
171            base.raise_and_set_if_changed(&self.value, value, "Value");
172            base.raise_and_set_if_changed(&self.assigned_type, value_type.unwrap_or(self.property_type), "AssignedType");
173
174            if let Some(priority) = priority {
175                // `priority.ToString()`: the name of the priority.
176                base.raise_and_set_if_changed(&self.priority, format!("{priority:?}"), "Priority");
177                let group = if self.is_attached() == Some(true) { "Attached Properties" } else { "Properties" };
178                base.raise_and_set_if_changed(&self.group, group.to_string(), "Group");
179            } else {
180                base.raise_and_set_if_changed(&self.priority, "Unset".to_string(), "Priority");
181                base.raise_and_set_if_changed(&self.group, "Unset".to_string(), "Group");
182            }
183        }
184        base.raise_property_changed("Type");
185    }
186
187    /// The override of `OnPropertyChanged`: pinning the property moves it to another group.
188    pub(crate) fn on_property_changed(&self, property_name: &str) {
189        if property_name == "IsPinned" {
190            self.base.raise_property_changed("Group");
191        }
192    }
193}
194
195ferro_markup_type!(class FerroPropertyViewModel {
196    this: Rc<FerroPropertyViewModel>,
197    handles: [FerroPropertyViewModel, Rc<FerroPropertyViewModel>, Option<Rc<FerroPropertyViewModel>>],
198    properties: [
199        Property: &'static FerroProperty { get: FerroPropertyViewModel::property },
200        Name: String { get: FerroPropertyViewModel::name },
201        Group: String { get: FerroPropertyViewModel::group },
202        Priority: String { get: FerroPropertyViewModel::priority },
203        Value: Option<BoxedValue> { get: FerroPropertyViewModel::value, set: FerroPropertyViewModel::set_value },
204        AssignedType: ValueType { get: FerroPropertyViewModel::assigned_type },
205        PropertyType: ValueType { get: FerroPropertyViewModel::property_type },
206        Type: String { get: |this: &Rc<FerroPropertyViewModel>| this.to_property_view_model().type_() },
207        IsReadonly: bool { get: FerroPropertyViewModel::is_readonly },
208        IsAttached: Option<bool> { get: FerroPropertyViewModel::is_attached },
209        IsPinned: bool {
210            get: |this: &Rc<FerroPropertyViewModel>| this.is_pinned(),
211            set: |this: &Rc<FerroPropertyViewModel>, value: bool| this.to_property_view_model().set_is_pinned(value)
212        },
213        FullName: String { get: |this: &Rc<FerroPropertyViewModel>| this.to_property_view_model().full_name() },
214    ],
215    methods: [
216        fn Update() => FerroPropertyViewModel::update,
217    ],
218    notify_property_changed: FerroPropertyViewModel,
219});