Add a clear-focus()function on all elements that have a focus() function

This is the counter-part, which removes focus from the element if it's currently focused. The window - if focused - may still be focused towards the windowing system.
This commit is contained in:
Simon Hausmann 2024-04-24 16:45:47 +02:00 committed by Simon Hausmann
parent e10e97c944
commit 31767eb6ab
21 changed files with 268 additions and 23 deletions

View file

@ -25,6 +25,7 @@ All notable changes to this project are documented in this file.
- Fixed compiler panic with state property change involving a state in a parent component. (#5038)
- Fixed interpreter overwriting property named `index`. (#4961)
- Fixed compiler panic when a callback aliases itself. (#4938)
- Added `clear-focus()` function to focusable elements, to allow for programmatic focus clearing.
## Widgets

View file

@ -93,10 +93,10 @@ public:
items, &inner);
}
void set_focus_item(const ItemTreeRc &component_rc, uint32_t item_index)
void set_focus_item(const ItemTreeRc &component_rc, uint32_t item_index, bool set_focus)
{
cbindgen_private::ItemRc item_rc { component_rc, item_index };
cbindgen_private::slint_windowrc_set_focus_item(&inner, &item_rc);
cbindgen_private::slint_windowrc_set_focus_item(&inner, &item_rc, set_focus);
}
template<typename Component>

View file

@ -216,6 +216,7 @@ or it will be mapped to a private unicode character. The mapping of these non-pr
- **`focus()`** Call this function to transfer keyboard focus to this `FocusScope`,
to receive future [`KeyEvent`](structs.md#keyevent)s.
- **`clear-focus()`** Call this function to remove keyboard focus from this `FocusScope` if it currently has the focus.
### Callbacks
@ -680,6 +681,7 @@ When not part of a layout, its width or height defaults to 100% of the parent el
### Functions
- **`focus()`** Call this function to focus the text input and make it receive future keyboard events.
- **`clear-focus()`** Call this function to remove keyboard focus from this `TextInput` if it currently has the focus.
- **`set-selection-offsets(int, int)`** Selects the text between two UTF-8 offsets.
- **`select-all()`** Selects all text.
- **`clear-selection()`** Clears the selection.

View file

@ -19,6 +19,7 @@ a widget able to handle several lines of text.
### Functions
- **`focus()`** Call this function to focus the LineEdit and make it receive future keyboard events.
- **`clear-focus()`** Call this function to remove keyboard focus from this `LineEdit` if it currently has the focus.
- **`set-selection-offsets(int, int)`** Selects the text between two UTF-8 offsets.
- **`select-all()`** Selects all text.
- **`clear-selection()`** Clears the selection.

View file

@ -19,6 +19,7 @@ shortcut will be implemented in a future version: <https://github.com/slint-ui/s
### Functions
- **`focus()`** Call this function to focus the TextEdit and make it receive future keyboard events.
- **`clear-focus()`** Call this function to remove keyboard focus from this `TextEdit` if it currently has the focus.
- **`set-selection-offsets(int, int)`** Selects the text between two UTF-8 offsets.
- **`select-all()`** Selects all text.
- **`clear-selection()`** Clears the selection.

View file

@ -209,7 +209,7 @@ impl Item for NativeSlider {
click_count: _,
} => {
if !self.has_focus() {
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc);
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc, true);
}
data.pressed_x = if vertical { pos.y as f32 } else { pos.x as f32 };
data.pressed = 1;

View file

@ -222,7 +222,7 @@ impl Item for NativeSpinBox {
if let MouseEvent::Pressed { .. } = event {
if !self.has_focus() {
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc);
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc, true);
}
}
InputEventResult::EventAccepted

View file

@ -440,7 +440,7 @@ impl Item for NativeTab {
if matches!(event, MouseEvent::Released { button, .. } if !click_on_press && button == PointerEventButton::Left)
|| matches!(event, MouseEvent::Pressed { button, .. } if click_on_press && button == PointerEventButton::Left)
{
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc);
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc, true);
self.current.set(self.tab_index());
InputEventResult::EventAccepted
} else {

View file

@ -126,7 +126,7 @@ impl AccessKitAdapter {
Action::Default => AccessibilityAction::Default,
Action::Focus => {
if let Some(item) = self.item_rc_for_node_id(request.target) {
WindowInner::from_pub(window_adapter.window()).set_focus_item(&item);
WindowInner::from_pub(window_adapter.window()).set_focus_item(&item, true);
}
return;
}

View file

@ -38,6 +38,7 @@ pub enum BuiltinFunction {
Log,
Pow,
SetFocusItem,
ClearFocusItem,
ShowPopupWindow,
ClosePopupWindow,
SetSelectionOffsets,
@ -133,6 +134,10 @@ impl BuiltinFunction {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference],
},
BuiltinFunction::ClearFocusItem => Type::Function {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference],
},
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => {
Type::Function {
return_type: Box::new(Type::Void),
@ -292,7 +297,7 @@ impl BuiltinFunction {
| BuiltinFunction::Log
| BuiltinFunction::Pow
| BuiltinFunction::ATan => true,
BuiltinFunction::SetFocusItem => false,
BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => false,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
BuiltinFunction::SetSelectionOffsets => false,
BuiltinFunction::ItemMemberFunction(..) => false,
@ -349,7 +354,7 @@ impl BuiltinFunction {
| BuiltinFunction::Log
| BuiltinFunction::Pow
| BuiltinFunction::ATan => true,
BuiltinFunction::SetFocusItem => false,
BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => false,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
BuiltinFunction::SetSelectionOffsets => false,
BuiltinFunction::ItemMemberFunction(..) => false,

View file

@ -3100,11 +3100,20 @@ fn compile_builtin_function_call(
if let [llr::Expression::PropertyReference(pr)] = arguments {
let window = access_window_field(ctx);
let focus_item = access_item_rc(pr, ctx);
format!("{}.set_focus_item({});", window, focus_item)
format!("{}.set_focus_item({}, true);", window, focus_item)
} else {
panic!("internal error: invalid args to SetFocusItem {:?}", arguments)
}
}
BuiltinFunction::ClearFocusItem => {
if let [llr::Expression::PropertyReference(pr)] = arguments {
let window = access_window_field(ctx);
let focus_item = access_item_rc(pr, ctx);
format!("{}.set_focus_item({}, false);", window, focus_item)
} else {
panic!("internal error: invalid args to ClearFocusItem {:?}", arguments)
}
}
/* std::from_chars is unfortunately not yet implemented in gcc
BuiltinFunction::StringIsFloat => {
"[](const auto &a){ double v; auto r = std::from_chars(std::begin(a), std::end(a), v); return r.ptr == std::end(a); }"

View file

@ -2396,12 +2396,23 @@ fn compile_builtin_function_call(
let window_tokens = access_window_adapter_field(ctx);
let focus_item = access_item_rc(pr, ctx);
quote!(
sp::WindowInner::from_pub(#window_tokens.window()).set_focus_item(#focus_item)
sp::WindowInner::from_pub(#window_tokens.window()).set_focus_item(#focus_item, true)
)
} else {
panic!("internal error: invalid args to SetFocusItem {:?}", arguments)
}
}
BuiltinFunction::ClearFocusItem => {
if let [Expression::PropertyReference(pr)] = arguments {
let window_tokens = access_window_adapter_field(ctx);
let focus_item = access_item_rc(pr, ctx);
quote!(
sp::WindowInner::from_pub(#window_tokens.window()).set_focus_item(#focus_item, false)
)
} else {
panic!("internal error: invalid args to ClearFocusItem {:?}", arguments)
}
}
BuiltinFunction::ShowPopupWindow => {
if let [Expression::NumberLiteral(popup_index), x, y, close_on_click, Expression::PropertyReference(parent_ref)] =
arguments

View file

@ -81,7 +81,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::ATan => 10,
BuiltinFunction::Log => 10,
BuiltinFunction::Pow => 10,
BuiltinFunction::SetFocusItem => isize::MAX,
BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => isize::MAX,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => isize::MAX,
BuiltinFunction::SetSelectionOffsets => isize::MAX,
BuiltinFunction::ItemMemberFunction(..) => isize::MAX,

View file

@ -35,7 +35,7 @@ pub fn replace_forward_focus_bindings_with_focus_functions(
// Phase 2: Filter out focus-forward bindings that aren't callable
local_forwards.remove_uncallable_forwards();
// Phase 3: For `focus-forward` in the root element, create a `focus()` function that's callable from the outside
// Phase 3: For `focus-forward` in the root element, create `focus()` and `clear-focus()` functions that are callable from the outside
if let Some((root_focus_forward, focus_forward_location)) =
local_forwards.focus_forward_for_element(&component.root_element)
{
@ -59,6 +59,27 @@ pub fn replace_forward_focus_bindings_with_focus_functions(
.bindings
.insert("focus".into(), RefCell::new(set_focus_code.into()));
}
if let Some(clear_focus_code) =
call_clear_focus_function(&root_focus_forward, Some(&focus_forward_location))
{
component.root_element.borrow_mut().property_declarations.insert(
"clear-focus".into(),
PropertyDeclaration {
property_type: Type::Function {
return_type: Type::Void.into(),
args: vec![],
},
visibility: PropertyVisibility::Public,
..Default::default()
},
);
component
.root_element
.borrow_mut()
.bindings
.insert("clear-focus".into(), RefCell::new(clear_focus_code.into()));
}
}
// Phase 4: All calls to `.focus()` may need to be changed with `focus-forward` resolved or changed from the built-in
@ -249,3 +270,47 @@ fn call_focus_function(
None
}
}
fn call_clear_focus_function(
element: &ElementRc,
source_location: Option<&SourceLocation>,
) -> Option<Expression> {
let declares_focus_function = {
let mut element = element.clone();
loop {
if element.borrow().property_declarations.contains_key("clear-focus") {
break true;
}
let base = element.borrow().base_type.clone();
match base {
ElementType::Component(compo) => element = compo.root_element.clone(),
_ => break false,
}
}
};
let builtin_focus_function =
element.borrow().builtin_type().map_or(false, |ty| ty.accepts_focus);
if declares_focus_function {
Some(Expression::FunctionCall {
function: Box::new(Expression::FunctionReference(
NamedReference::new(element, "clear-focus"),
None,
)),
arguments: vec![],
source_location: source_location.cloned(),
})
} else if builtin_focus_function {
let source_location = source_location.cloned();
Some(Expression::FunctionCall {
function: Box::new(Expression::BuiltinFunctionReference(
BuiltinFunction::ClearFocusItem,
source_location.clone(),
)),
arguments: vec![Expression::ElementReference(Rc::downgrade(element))],
source_location,
})
} else {
None
}
}

View file

@ -92,7 +92,7 @@ fn load_component(component: &Rc<i_slint_compiler::object_tree::Component>) -> C
)
}),
);
// Synthesize focus() as styles written in .slint will have it but the qt style exposes NativeXX directly.
// Synthesize focus() and `clear-focus()` as styles written in .slint will have it but the qt style exposes NativeXX directly.
if b.accepts_focus {
result.properties.insert(
"focus".into(),
@ -102,6 +102,14 @@ fn load_component(component: &Rc<i_slint_compiler::object_tree::Component>) -> C
pure: false,
},
);
result.properties.insert(
"clear-focus".into(),
PropertyInfo {
ty: Type::Function { return_type: Type::Void.into(), args: vec![] },
vis: PropertyVisibility::Public,
pure: false,
},
);
}
break;
}

View file

@ -140,6 +140,7 @@ pub fn reserved_properties() -> impl Iterator<Item = (&'static str, Type, Proper
("absolute-position", logical_point_type(), PropertyVisibility::Output),
("forward-focus", Type::ElementReference, PropertyVisibility::Constexpr),
("focus", BuiltinFunction::SetFocusItem.ty(), PropertyVisibility::Public),
("clear-focus", BuiltinFunction::ClearFocusItem.ty(), PropertyVisibility::Public),
(
"dialog-button-role",
Type::Enumeration(BUILTIN_ENUMS.with(|e| e.DialogButtonRole.clone())),
@ -206,6 +207,7 @@ pub fn reserved_property(name: &str) -> PropertyLookupResult {
pub fn reserved_member_function(name: &str) -> Option<BuiltinFunction> {
for (m, e) in [
("focus", BuiltinFunction::SetFocusItem), // match for callable "focus" property
("clear-focus", BuiltinFunction::ClearFocusItem), // match for callable "clear-focus" property
] {
if m == name {
return Some(e);

View file

@ -775,7 +775,7 @@ impl Item for FocusScope {
self_rc: &ItemRc,
) -> InputEventResult {
if self.enabled() && matches!(event, MouseEvent::Pressed { .. }) && !self.has_focus() {
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc);
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc, true);
InputEventResult::EventAccepted
} else {
InputEventResult::EventIgnored

View file

@ -1476,7 +1476,7 @@ impl TextInput {
self_rc: &ItemRc,
) {
if !self.has_focus() {
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc);
WindowInner::from_pub(window_adapter.window()).set_focus_item(self_rc, true);
} else if !self.read_only() {
if let Some(w) = window_adapter.internal(crate::InternalToken) {
w.input_method_request(InputMethodRequest::Enable(

View file

@ -691,13 +691,24 @@ impl WindowInner {
}
/// Sets the focus to the item pointed to by item_ptr. This will remove the focus from any
/// currently focused item.
pub fn set_focus_item(&self, focus_item: &ItemRc) {
/// currently focused item. If set_focus is false, the focus is cleared.
pub fn set_focus_item(&self, new_focus_item: &ItemRc, set_focus: bool) {
if self.prevent_focus_change.get() {
return;
}
if !set_focus {
let current_focus_item = self.focus_item.borrow().clone();
if let Some(current_focus_item_rc) = current_focus_item.upgrade() {
if current_focus_item_rc != *new_focus_item {
// can't clear focus unless called with currently focused item.
return;
}
}
}
let old = self.take_focus_item();
let new = self.move_focus(focus_item.clone(), next_focus_item);
let new =
if set_focus { self.move_focus(new_focus_item.clone(), next_focus_item) } else { None };
let window_adapter = self.window_adapter();
if let Some(window_adapter) = window_adapter.internal(crate::InternalToken) {
window_adapter.handle_focus_change(old, new);
@ -1250,9 +1261,10 @@ pub mod ffi {
pub unsafe extern "C" fn slint_windowrc_set_focus_item(
handle: *const WindowAdapterRcOpaque,
focus_item: &ItemRc,
set_focus: bool,
) {
let window_adapter = &*(handle as *const Rc<dyn WindowAdapter>);
WindowInner::from_pub(window_adapter.window()).set_focus_item(focus_item)
WindowInner::from_pub(window_adapter.window()).set_focus_item(focus_item, set_focus)
}
/// Associates the window with the given component.

View file

@ -527,16 +527,56 @@ fn call_builtin_function(
enclosing_component.self_weak().get().unwrap().upgrade().unwrap();
component.access_window(|window| {
window.set_focus_item(&corelib::items::ItemRc::new(
vtable::VRc::into_dyn(focus_item_comp),
item_info.item_index(),
))
window.set_focus_item(
&corelib::items::ItemRc::new(
vtable::VRc::into_dyn(focus_item_comp),
item_info.item_index(),
),
true,
)
});
Value::Void
} else {
panic!("internal error: argument to SetFocusItem must be an element")
}
}
BuiltinFunction::ClearFocusItem => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to SetFocusItem")
}
let component = match local_context.component_instance {
ComponentInstance::InstanceRef(c) => c,
ComponentInstance::GlobalComponent(_) => {
panic!("Cannot access the focus item from a global component")
}
};
if let Expression::ElementReference(focus_item) = &arguments[0] {
generativity::make_guard!(guard);
let focus_item = focus_item.upgrade().unwrap();
let enclosing_component =
enclosing_component_for_element(&focus_item, component, guard);
let description = enclosing_component.description;
let item_info = &description.items[focus_item.borrow().id.as_str()];
let focus_item_comp =
enclosing_component.self_weak().get().unwrap().upgrade().unwrap();
component.access_window(|window| {
window.set_focus_item(
&corelib::items::ItemRc::new(
vtable::VRc::into_dyn(focus_item_comp),
item_info.item_index(),
),
false,
)
});
Value::Void
} else {
panic!("internal error: argument to ClearFocusItem must be an element")
}
}
BuiltinFunction::ShowPopupWindow => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to ShowPopupWindow")

View file

@ -0,0 +1,88 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial
export component TestCase inherits Window {
width: 200px;
height: 200px;
le1 := TextInput {
x: 0px;
y: 0px;
width: 100%;
height: 100px;
}
le2 := TextInput {
y: 100px;
x: 0px;
width: 100%;
height: 100px;
}
out property le1-has-focus <=> le1.has-focus;
out property le2-has-focus <=> le2.has-focus;
out property <bool> te-focused: TextInputInterface.text-input-focused;
callback clear-le1-focus();
clear-le1-focus => {
le1.clear-focus();
}
callback clear-le2-focus();
clear-le2-focus => {
le2.clear-focus();
}
callback focus-le1();
focus-le1 => {
le1.focus();
}
callback focus-le2();
focus-le2 => {
le2.focus();
}
}
/*
```rust
let instance = TestCase::new().unwrap();
assert_eq!(instance.get_le1_has_focus(), false);
assert_eq!(instance.get_le2_has_focus(), false);
//assert_eq!(instance.get_te_focused(), false);
// Focus first line edit
eprintln!("send event");
slint_testing::send_mouse_click(&instance, 50., 50.);
assert_eq!(instance.get_le1_has_focus(), true);
assert_eq!(instance.get_le2_has_focus(), false);
assert_eq!(instance.get_te_focused(), true);
// Focus second line edit programmatically
eprintln!("set programmatically");
instance.invoke_focus_le2();
assert_eq!(instance.get_le1_has_focus(), false);
assert_eq!(instance.get_le2_has_focus(), true);
assert_eq!(instance.get_te_focused(), true);
// Clear focus (should fail because item is not focused)
instance.invoke_clear_le1_focus();
assert_eq!(instance.get_le1_has_focus(), false);
assert_eq!(instance.get_le2_has_focus(), true);
assert_eq!(instance.get_te_focused(), true);
// Clear focus on currently focused item
instance.invoke_clear_le2_focus();
assert_eq!(instance.get_le1_has_focus(), false);
assert_eq!(instance.get_le2_has_focus(), false);
assert_eq!(instance.get_te_focused(), false);
```
*/