ferroui_controls/
hotkey_manager.rs1use 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
14pub struct HotKeyManager;
18
19ferroui_base::ferro_static_type!(HotKeyManager);
20
21struct 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 #[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 Disposable::empty()
86 }
87}
88
89struct 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 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 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
193impl Drop for Manager {
195 fn drop(&mut self) {
196 self.stop();
197 }
198}
199
200ferroui_base::ferro_properties! { impl HotKeyManager {
201 ferro_property!(
202 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 pub fn set_hot_key(target: &FerroObject, value: Option<KeyGesture>) {
239 target.set_value(Self::hot_key_property(), value)
240 }
241
242 pub fn get_hot_key(target: &FerroObject) -> Option<KeyGesture> {
244 target.get_value(Self::hot_key_property())
245 }
246}