Skip to main content

ferroui_controls/
hotkey_manager.rs

1use crate::i_clickable_control::as_clickable_control;
2use crate::i_command_source::as_command_source;
3use crate::utils::AncestorFinder;
4use crate::{Control, TopLevel};
5use ferroui_base::input::{ICommand, KeyBinding, KeyGesture};
6use ferroui_base::logging::{LogArea, LogEventLevel, Logger};
7use ferroui_base::reactive::{Disposable, IDisposable, ObservableExt};
8use ferroui_base::{
9    ferro_property, AttachedProperty, BoxedValue, FerroObject, FerroProperty, Ref, WeakRef,
10};
11use std::cell::{Cell, RefCell};
12use std::rc::{Rc, Weak};
13
14/// Defines the `HotKey` attached property: a key gesture that clicks a
15/// control, or executes its command, wherever the keyboard focus is within
16/// the top-level the control is in.
17pub struct HotKeyManager;
18
19ferroui_base::ferro_static_type!(HotKeyManager);
20
21/// The command of the key binding registered for a control with a hot key.
22struct HotkeyCommandWrapper {
23    this: Weak<HotkeyCommandWrapper>,
24    reference: WeakRef<Control>,
25}
26
27impl HotkeyCommandWrapper {
28    fn new(control: &Ref<Control>) -> Rc<Self> {
29        Rc::new_cyclic(|this| Self { this: this.clone(), reference: control.downgrade() })
30    }
31
32    /// The command a hot key press executes: the command of the control,
33    /// or this wrapper when the control is only clickable.
34    #[allow(dead_code)]
35    fn get_command(&self) -> Option<Rc<dyn ICommand>> {
36        if let Some(target) = self.reference.upgrade() {
37            if let Some(command) = as_command_source(&target).and_then(|source| source.command()) {
38                return Some(command);
39            } else if as_clickable_control(&target).is_some() {
40                return self.this.upgrade().map(|this| this as Rc<dyn ICommand>);
41            }
42        }
43
44        None
45    }
46}
47
48impl ICommand for HotkeyCommandWrapper {
49    fn can_execute(&self, _parameter: Option<&BoxedValue>) -> bool {
50        if let Some(target) = self.reference.upgrade() {
51            if let Some(command_source) = as_command_source(&target) {
52                if let Some(command) = command_source.command() {
53                    return command_source.is_effectively_enabled()
54                        && command.can_execute(command_source.command_parameter().as_ref());
55                }
56            }
57
58            if let Some(clickable) = as_clickable_control(&target) {
59                return clickable.is_effectively_enabled();
60            }
61        }
62
63        false
64    }
65
66    fn execute(&self, _parameter: Option<&BoxedValue>) {
67        if let Some(target) = self.reference.upgrade() {
68            if let Some(command_source) = as_command_source(&target) {
69                if let Some(command) = command_source.command() {
70                    command.execute(command_source.command_parameter().as_ref());
71                    return;
72                }
73            }
74
75            if let Some(clickable) = as_clickable_control(&target) {
76                if clickable.is_effectively_enabled() {
77                    clickable.raise_click();
78                }
79            }
80        }
81    }
82
83    fn can_execute_changed(&self, _handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
84        // The event is never raised.
85        Disposable::empty()
86    }
87}
88
89/// Keeps the key binding of one control with a hot key registered on the
90/// top-level the control is in.
91struct Manager {
92    control: WeakRef<Control>,
93    root: RefCell<Option<WeakRef<TopLevel>>>,
94    parent_sub: RefCell<Option<Rc<dyn IDisposable>>>,
95    hotkey_sub: RefCell<Option<Rc<dyn IDisposable>>>,
96    hotkey: Cell<Option<KeyGesture>>,
97    wrapper: Rc<HotkeyCommandWrapper>,
98    binding: RefCell<Option<Ref<KeyBinding>>>,
99}
100
101impl Manager {
102    fn new(control: &Ref<Control>) -> Rc<Self> {
103        Rc::new(Self {
104            control: control.downgrade(),
105            root: RefCell::new(None),
106            parent_sub: RefCell::new(None),
107            hotkey_sub: RefCell::new(None),
108            hotkey: Cell::new(None),
109            wrapper: HotkeyCommandWrapper::new(control),
110            binding: RefCell::new(None),
111        })
112    }
113
114    fn init(self: &Rc<Self>) {
115        let Some(control) = self.control.upgrade() else { return };
116
117        // The control owns the manager through this change handler; every
118        // other reference between the two is weak.
119        let property = HotKeyManager::hot_key_property().as_property();
120        let this = self.clone();
121        let hotkey_sub = control.property_changed(move |e| {
122            if e.property() == property {
123                this.on_hotkey_changed(e.get_new_value::<Option<KeyGesture>>());
124            }
125        });
126        *self.hotkey_sub.borrow_mut() = Some(hotkey_sub);
127        self.on_hotkey_changed(control.get_value(HotKeyManager::hot_key_property()));
128
129        let weak = Rc::downgrade(self);
130        let parent_sub = AncestorFinder::create::<TopLevel>(&control.clone().upcast()).subscribe_fn(
131            move |root: Option<Ref<TopLevel>>| {
132                if let Some(this) = weak.upgrade() {
133                    this.on_parent_changed(root);
134                }
135            },
136        );
137        *self.parent_sub.borrow_mut() = Some(parent_sub);
138    }
139
140    fn on_parent_changed(&self, control: Option<Ref<TopLevel>>) {
141        self.unregister();
142        *self.root.borrow_mut() = control.map(|root| root.downgrade());
143        self.register();
144    }
145
146    fn on_hotkey_changed(&self, hotkey: Option<KeyGesture>) {
147        match hotkey {
148            // The subscription will be recreated by the static property
149            // watcher.
150            None => self.stop(),
151            Some(hotkey) => {
152                self.unregister();
153                self.hotkey.set(Some(hotkey));
154                self.register();
155            }
156        }
157    }
158
159    fn root(&self) -> Option<Ref<TopLevel>> {
160        self.root.borrow().as_ref().and_then(WeakRef::upgrade)
161    }
162
163    fn unregister(&self) {
164        let binding = self.binding.borrow_mut().take();
165        if let (Some(root), Some(binding)) = (self.root(), binding) {
166            root.key_bindings().remove(&binding);
167        }
168    }
169
170    fn register(&self) {
171        if let (Some(root), Some(hotkey)) = (self.root(), self.hotkey.get()) {
172            let binding = KeyBinding::new();
173            binding.set_gesture(Some(hotkey));
174            binding.set_command(Some(self.wrapper.clone()));
175            *self.binding.borrow_mut() = Some(binding.clone());
176            root.key_bindings().add(binding);
177        }
178    }
179
180    fn stop(&self) {
181        self.unregister();
182        let parent_sub = self.parent_sub.borrow_mut().take();
183        if let Some(parent_sub) = parent_sub {
184            parent_sub.dispose();
185        }
186        let hotkey_sub = self.hotkey_sub.borrow_mut().take();
187        if let Some(hotkey_sub) = hotkey_sub {
188            hotkey_sub.dispose();
189        }
190    }
191}
192
193/// A manager that goes away with its control takes its key binding with it.
194impl Drop for Manager {
195    fn drop(&mut self) {
196        self.stop();
197    }
198}
199
200ferroui_base::ferro_properties! { impl HotKeyManager {
201    ferro_property!(
202        /// Defines the `HotKey` attached property.
203        pub fn hot_key_property() -> AttachedProperty<Option<KeyGesture>> {
204            FerroProperty::register_attached::<HotKeyManager, Control, _>("HotKey", None)
205        }
206    );
207} }
208
209impl HotKeyManager {
210    fn static_constructor() {
211        Self::hot_key_property().changed().subscribe(|args| {
212            let Some(hotkey) = args.get_new_value::<Option<KeyGesture>>() else { return };
213
214            let control = args.sender().downcast_ref::<Control>();
215            let supported = control
216                .is_some_and(|control| as_clickable_control(control).is_some() || as_command_source(control).is_some());
217
218            let Some(control) = control.filter(|_| supported) else {
219                if let Some(logger) = Logger::try_get(LogEventLevel::Warning, LogArea::CONTROL) {
220                    logger.log(
221                        None,
222                        &format!(
223                            "The element {} does not implement IClickableControl nor ICommandSource and does not \
224                             support binding a HotKey ({}).",
225                            args.sender().get_type().name(),
226                            hotkey
227                        ),
228                    );
229                }
230                return;
231            };
232
233            Manager::new(&control.to_ref()).init();
234        });
235    }
236
237    /// Sets the value of the `HotKey` attached property on an object.
238    pub fn set_hot_key(target: &FerroObject, value: Option<KeyGesture>) {
239        target.set_value(Self::hot_key_property(), value)
240    }
241
242    /// Gets the value of the `HotKey` attached property on an object.
243    pub fn get_hot_key(target: &FerroObject) -> Option<KeyGesture> {
244        target.get_value(Self::hot_key_property())
245    }
246}