PopupWindow: added close-policy property (#6614)

* Update api/cpp/include/slint_window.h

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update docs/reference/src/language/builtins/elements.md

Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev>

* Update internal/core/window.rs

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update internal/interpreter/eval.rs

Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev>

* Update internal/backends/qt/qt_window.rs

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update internal/interpreter/dynamic_item_tree.rs

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update internal/compiler/passes/materialize_fake_properties.rs

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev>
This commit is contained in:
FloVanGH 2024-10-24 11:27:39 +02:00 committed by GitHub
parent 0bf924cf0d
commit 14c7910d49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 657 additions and 61 deletions

View file

@ -106,13 +106,14 @@ public:
} }
template<typename Component, typename Parent, typename PosGetter> template<typename Component, typename Parent, typename PosGetter>
void show_popup(const Parent *parent_component, PosGetter pos, bool close_on_click, void show_popup(const Parent *parent_component, PosGetter pos,
cbindgen_private::PopupClosePolicy close_policy,
cbindgen_private::ItemRc parent_item) const cbindgen_private::ItemRc parent_item) const
{ {
auto popup = Component::create(parent_component); auto popup = Component::create(parent_component);
cbindgen_private::Point p = pos(popup); cbindgen_private::Point p = pos(popup);
auto popup_dyn = popup.into_dyn(); auto popup_dyn = popup.into_dyn();
cbindgen_private::slint_windowrc_show_popup(&inner, &popup_dyn, p, close_on_click, cbindgen_private::slint_windowrc_show_popup(&inner, &popup_dyn, p, close_policy,
&parent_item); &parent_item);
} }

View file

@ -606,13 +606,13 @@ Note: It isn't allowed to access properties of elements within the popup from ou
### Properties ### Properties
- **`close-on-click`** (_in_ _bool_): By default, a PopupWindow closes when the user clicks. Set this - **`close-policy`** (_in_ _enum [`PopupClosePolicy`](../builtins/enums.md#closepolicy)_): By default, a PopupWindow closes when the user clicks (`close-on-click`). Set this
to false to prevent that behavior and close it manually using the `close()` function. (default value: true) to `off` to prevent that behavior and close it manually using the `close()` function. Set to `close-on-click-outside` to close the PopupWindow when the user clicks outside of the popup.
### Functions ### Functions
- **`show()`** Show the popup on the screen. - **`show()`** Show the popup on the screen.
- **`close()`** Closes the popup. Use this if you set the `close-on-click` property to false. - **`close()`** Closes the popup. Use this if you set the `close-policy` property to `no-auto-close`.
### Example ### Example

View file

@ -33,7 +33,7 @@ export component Example inherits Window {
date-picker := DatePickerPopup { date-picker := DatePickerPopup {
width: 360px; width: 360px;
height: 524px; height: 524px;
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
accepted(date) => { accepted(date) => {
date-picker.close(); date-picker.close();

View file

@ -18,7 +18,7 @@ use i_slint_core::item_rendering::{
use i_slint_core::item_tree::{ItemTreeRc, ItemTreeRef}; use i_slint_core::item_tree::{ItemTreeRc, ItemTreeRef};
use i_slint_core::items::{ use i_slint_core::items::{
self, ColorScheme, FillRule, ImageRendering, ItemRc, ItemRef, Layer, MouseCursor, Opacity, self, ColorScheme, FillRule, ImageRendering, ItemRc, ItemRef, Layer, MouseCursor, Opacity,
PointerEventButton, RenderingResult, TextOverflow, TextStrokeStyle, TextWrap, PointerEventButton, PopupClosePolicy, RenderingResult, TextOverflow, TextStrokeStyle, TextWrap,
}; };
use i_slint_core::layout::Orientation; use i_slint_core::layout::Orientation;
use i_slint_core::lengths::{ use i_slint_core::lengths::{
@ -160,10 +160,12 @@ cpp! {{
void *parent_of_popup_to_close = nullptr; void *parent_of_popup_to_close = nullptr;
if (auto p = dynamic_cast<const SlintWidget*>(parent())) { if (auto p = dynamic_cast<const SlintWidget*>(parent())) {
void *parent_window = p->rust_window; void *parent_window = p->rust_window;
bool close_popup = rust!(Slint_mouseReleaseEventPopup [parent_window: &QtWindow as "void*"] -> bool as "bool" { bool close_on_click = rust!(Slint_mouseReleaseEventPopup [parent_window: &QtWindow as "void*"] -> bool as "bool" {
parent_window.close_popup_on_click() let close_policy = parent_window.close_policy();
close_policy == PopupClosePolicy::CloseOnClick || close_policy == PopupClosePolicy::CloseOnClickOutside
}); });
if (close_popup) { if (close_on_click) {
parent_of_popup_to_close = parent_window; parent_of_popup_to_close = parent_window;
} }
} }
@ -1745,8 +1747,8 @@ impl QtWindow {
WindowInner::from_pub(&self.window).close_popup(); WindowInner::from_pub(&self.window).close_popup();
} }
fn close_popup_on_click(&self) -> bool { fn close_policy(&self) -> PopupClosePolicy {
WindowInner::from_pub(&self.window).close_popup_on_click() WindowInner::from_pub(&self.window).close_policy()
} }
fn window_state_event(&self) { fn window_state_event(&self) {

View file

@ -431,6 +431,18 @@ macro_rules! for_each_enums {
/// Scrollbar always visible /// Scrollbar always visible
AlwaysOn, AlwaysOn,
} }
// This enum describes the close behaviour of [`PopupWindow`](elements.md#popupwindow)
enum PopupClosePolicy {
/// Closes the `PopupWindow` when user clicks.
CloseOnClick,
/// Closed the `PopupWindow` when user clicks outside of the popup.
CloseOnClickOutside,
/// Does not close the `PopupWindow` automatically when user clicks
NoAutoClose,
}
]; ];
}; };
} }

View file

@ -417,7 +417,8 @@ export component PopupWindow {
in property <length> anchor_y; in property <length> anchor_y;
in property <length> anchor_height; in property <length> anchor_height;
in property <length> anchor_width;*/ in property <length> anchor_width;*/
in property <bool> close-on-click: true; // constexpr hardcoded in typeregister.rs in property <bool> close-on-click;
in property <PopupClosePolicy> close-policy; // constexpr hardcoded in typeregister.rs
//show() is hardcoded in typeregister.rs //show() is hardcoded in typeregister.rs
} }

View file

@ -3560,7 +3560,7 @@ fn compile_builtin_function_call(
format!("{}.text_input_focused()", access_window_field(ctx)) format!("{}.text_input_focused()", access_window_field(ctx))
} }
BuiltinFunction::ShowPopupWindow => { BuiltinFunction::ShowPopupWindow => {
if let [llr::Expression::NumberLiteral(popup_index), close_on_click, llr::Expression::PropertyReference(parent_ref)] = if let [llr::Expression::NumberLiteral(popup_index), close_policy, llr::Expression::PropertyReference(parent_ref)] =
arguments arguments
{ {
let mut parent_ctx = ctx; let mut parent_ctx = ctx;
@ -3586,9 +3586,9 @@ fn compile_builtin_function_call(
Some(ParentCtx::new(&ctx, None)), Some(ParentCtx::new(&ctx, None)),
); );
let position = compile_expression(&popup.position.borrow(), &popup_ctx); let position = compile_expression(&popup.position.borrow(), &popup_ctx);
let close_on_click = compile_expression(close_on_click, ctx); let close_policy = compile_expression(close_policy, ctx);
format!( format!(
"{window}.show_popup<{popup_window_id}>({component_access}, [=](auto self) {{ return {position}; }}, {close_on_click}, {{ {parent_component} }})" "{window}.show_popup<{popup_window_id}>({component_access}, [=](auto self) {{ return {position}; }}, {close_policy}, {{ {parent_component} }})"
) )
} else { } else {
panic!("internal error: invalid args to ShowPopupWindow {:?}", arguments) panic!("internal error: invalid args to ShowPopupWindow {:?}", arguments)

View file

@ -2605,7 +2605,7 @@ fn compile_builtin_function_call(
} }
} }
BuiltinFunction::ShowPopupWindow => { BuiltinFunction::ShowPopupWindow => {
if let [Expression::NumberLiteral(popup_index), close_on_click, Expression::PropertyReference(parent_ref)] = if let [Expression::NumberLiteral(popup_index), close_policy, Expression::PropertyReference(parent_ref)] =
arguments arguments
{ {
let mut parent_ctx = ctx; let mut parent_ctx = ctx;
@ -2630,7 +2630,7 @@ fn compile_builtin_function_call(
); );
let position = compile_expression(&popup.position.borrow(), &popup_ctx); let position = compile_expression(&popup.position.borrow(), &popup_ctx);
let close_on_click = compile_expression(close_on_click, ctx); let close_policy = compile_expression(close_policy, ctx);
let window_adapter_tokens = access_window_adapter_field(ctx); let window_adapter_tokens = access_window_adapter_field(ctx);
quote!({ quote!({
let popup_instance = #popup_window_id::new(#component_access_tokens.self_weak.get().unwrap().clone()).unwrap(); let popup_instance = #popup_window_id::new(#component_access_tokens.self_weak.get().unwrap().clone()).unwrap();
@ -2640,7 +2640,7 @@ fn compile_builtin_function_call(
sp::WindowInner::from_pub(#window_adapter_tokens.window()).show_popup( sp::WindowInner::from_pub(#window_adapter_tokens.window()).show_popup(
&sp::VRc::into_dyn(popup_instance.into()), &sp::VRc::into_dyn(popup_instance.into()),
position, position,
#close_on_click, #close_policy,
#parent_component #parent_component
) )
}) })

View file

@ -674,6 +674,7 @@ impl<'a, T> TypeResolutionContext for EvaluationContext<'a, T> {
for i in sub_component_path { for i in sub_component_path {
sub_component = &sub_component.sub_components[*i].ty; sub_component = &sub_component.sub_components[*i].ty;
} }
sub_component.items[*item_index as usize].ty.lookup_property(prop_name).unwrap() sub_component.items[*item_index as usize].ty.lookup_property(prop_name).unwrap()
} }
PropertyReference::InParent { level, parent_reference } => { PropertyReference::InParent { level, parent_reference } => {

View file

@ -379,11 +379,12 @@ fn lower_show_popup(args: &[tree_Expression], ctx: &ExpressionContext) -> llr_Ex
&tree_Expression::ElementReference(Rc::downgrade(&popup.parent_element)), &tree_Expression::ElementReference(Rc::downgrade(&popup.parent_element)),
ctx, ctx,
); );
llr_Expression::BuiltinFunctionCall { llr_Expression::BuiltinFunctionCall {
function: BuiltinFunction::ShowPopupWindow, function: BuiltinFunction::ShowPopupWindow,
arguments: vec![ arguments: vec![
llr_Expression::NumberLiteral(popup_index as _), llr_Expression::NumberLiteral(popup_index as _),
llr_Expression::BoolLiteral(popup.close_on_click), llr_Expression::EnumerationValue(popup.close_policy.clone()),
item_ref, item_ref,
], ],
} }

View file

@ -9,6 +9,7 @@
use crate::diagnostics::{BuildDiagnostics, SourceLocation, Spanned}; use crate::diagnostics::{BuildDiagnostics, SourceLocation, Spanned};
use crate::expression_tree::{self, BindingExpression, Expression, Unit}; use crate::expression_tree::{self, BindingExpression, Expression, Unit};
use crate::langtype::EnumerationValue;
use crate::langtype::{BuiltinElement, BuiltinPropertyDefault, Enumeration, NativeClass, Type}; use crate::langtype::{BuiltinElement, BuiltinPropertyDefault, Enumeration, NativeClass, Type};
use crate::langtype::{ElementType, PropertyLookupResult}; use crate::langtype::{ElementType, PropertyLookupResult};
use crate::layout::{LayoutConstraints, Orientation}; use crate::layout::{LayoutConstraints, Orientation};
@ -271,7 +272,7 @@ pub struct PopupWindow {
pub component: Rc<Component>, pub component: Rc<Component>,
pub x: NamedReference, pub x: NamedReference,
pub y: NamedReference, pub y: NamedReference,
pub close_on_click: bool, pub close_policy: EnumerationValue,
pub parent_element: ElementRc, pub parent_element: ElementRc,
} }

View file

@ -445,7 +445,7 @@ fn duplicate_popup(p: &PopupWindow, mapping: &mut Mapping, priority_delta: i32)
PopupWindow { PopupWindow {
x: p.x.clone(), x: p.x.clone(),
y: p.y.clone(), y: p.y.clone(),
close_on_click: p.close_on_click, close_policy: p.close_policy.clone(),
component: duplicate_sub_component(&p.component, &parent, mapping, priority_delta), component: duplicate_sub_component(&p.component, &parent, mapping, priority_delta),
parent_element: mapping parent_element: mapping
.get(&element_key(p.parent_element.clone())) .get(&element_key(p.parent_element.clone()))

View file

@ -5,7 +5,7 @@
use crate::diagnostics::BuildDiagnostics; use crate::diagnostics::BuildDiagnostics;
use crate::expression_tree::{Expression, NamedReference}; use crate::expression_tree::{Expression, NamedReference};
use crate::langtype::{ElementType, Type}; use crate::langtype::{ElementType, EnumerationValue, Type};
use crate::object_tree::*; use crate::object_tree::*;
use crate::typeregister::TypeRegister; use crate::typeregister::TypeRegister;
use smol_str::format_smolstr; use smol_str::format_smolstr;
@ -78,8 +78,15 @@ fn lower_popup_window(
const CLOSE_ON_CLICK: &str = "close-on-click"; const CLOSE_ON_CLICK: &str = "close-on-click";
let close_on_click = popup_window_element.borrow_mut().bindings.remove(CLOSE_ON_CLICK); let close_on_click = popup_window_element.borrow_mut().bindings.remove(CLOSE_ON_CLICK);
// Take a reference to the close on click
let close_on_click = close_on_click let close_on_click = close_on_click
.map(|b| { .map(|b| {
diag.push_property_deprecation_warning(
"close-on-click",
"close-policy",
&b.borrow().span,
);
let b = b.into_inner(); let b = b.into_inner();
(b.expression, b.span) (b.expression, b.span)
}) })
@ -95,9 +102,38 @@ fn lower_popup_window(
None None
}); });
const CLOSE_POLICY: &str = "close-policy";
let close_policy = popup_window_element.borrow_mut().bindings.remove(CLOSE_POLICY);
// Take a reference to the close policy
let close_policy = close_policy
.map(|b| {
let b = b.into_inner();
(b.expression, b.span)
})
.or_else(|| {
let mut base = popup_window_element.borrow().base_type.clone();
while let ElementType::Component(b) = base {
base = b.root_element.borrow().base_type.clone();
if let Some(binding) = b.root_element.borrow().bindings.get(CLOSE_POLICY) {
let b = binding.borrow();
return Some((b.expression.clone(), b.span.clone()));
}
}
None
});
if close_policy.is_some() && close_on_click.is_some() {
diag.push_error(
"close-policy and close-on-click cannot be set at the same time".into(),
&close_on_click.unwrap().1,
);
return;
}
let close_on_click = match close_on_click { let close_on_click = match close_on_click {
Some((expr, location)) => match expr { Some((expr, location)) => match expr {
Expression::BoolLiteral(value) => value, Expression::BoolLiteral(value) => Some(value),
_ => { _ => {
diag.push_error( diag.push_error(
"The close-on-click property only supports constants at the moment".into(), "The close-on-click property only supports constants at the moment".into(),
@ -106,7 +142,35 @@ fn lower_popup_window(
return; return;
} }
}, },
None => true, None => None,
};
let close_policy = match close_policy {
Some((expr, location)) => match expr {
Expression::EnumerationValue(value) => value,
_ => {
diag.push_error(
"The close-policy property only supports constants at the moment".into(),
&location,
);
return;
}
},
None => {
let enum_ty = crate::typeregister::BUILTIN_ENUMS.with(|e| e.PopupClosePolicy.clone());
let mut value = String::from("close-on-click");
if let Some(close_on_click) = close_on_click {
if !close_on_click {
value = "no-auto-close".into()
}
}
EnumerationValue {
value: enum_ty.values.iter().position(|v| v == value.as_str()).unwrap(),
enumeration: enum_ty,
}
}
}; };
let popup_comp = Rc::new(Component { let popup_comp = Rc::new(Component {
@ -166,7 +230,7 @@ fn lower_popup_window(
component: popup_comp, component: popup_comp,
x: coord_x, x: coord_x,
y: coord_y, y: coord_y,
close_on_click, close_policy,
parent_element: parent_element.clone(), parent_element: parent_element.clone(),
}); });
} }

View file

@ -7,7 +7,7 @@
use crate::diagnostics::Spanned; use crate::diagnostics::Spanned;
use crate::expression_tree::{BindingExpression, Expression, Unit}; use crate::expression_tree::{BindingExpression, Expression, Unit};
use crate::langtype::{ElementType, Type}; use crate::langtype::{ElementType, Enumeration, Type};
use crate::layout::Orientation; use crate::layout::Orientation;
use crate::namedreference::NamedReference; use crate::namedreference::NamedReference;
use crate::object_tree::*; use crate::object_tree::*;
@ -113,6 +113,18 @@ fn should_materialize(
} else if prop == "close-on-click" { } else if prop == "close-on-click" {
// PopupWindow::close-on-click // PopupWindow::close-on-click
return Some(Type::Bool); return Some(Type::Bool);
} else if prop == "close-policy" {
// PopupWindow::close-policy
return Some(Type::Enumeration(Rc::new(Enumeration {
name: "PopupClosePolicy".into(),
values: vec![
"close-on-click".into(),
"close-on-click-outside".into(),
"no-auto-close".into(),
],
default_value: 0,
node: None,
})));
} }
} }
None None

View file

@ -6,6 +6,7 @@ export component Bar {
in property <bool> external; in property <bool> external;
xx := PopupWindow { xx := PopupWindow {
close-on-click: true; close-on-click: true;
// ^warning{The property 'close-on-click' has been deprecated. Please use 'close-policy' instead}
init => { init => {
xx.close-on-click = true; xx.close-on-click = true;
// ^error{The property must be known at compile time and cannot be changed at runtime} // ^error{The property must be known at compile time and cannot be changed at runtime}
@ -13,7 +14,8 @@ export component Bar {
} }
PopupWindow { PopupWindow {
close-on-click: root.external; close-on-click: root.external;
// ^error{The close-on-click property only supports constants at the moment} // ^warning{The property 'close-on-click' has been deprecated. Please use 'close-policy' instead}
// ^^error{The close-on-click property only supports constants at the moment}
} }

View file

@ -0,0 +1,20 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export component Bar {
in property <PopupClosePolicy> external;
xx := PopupWindow {
close-policy: PopupClosePolicy.close-on-click;
init => {
xx.close-policy = PopupClosePolicy.close-on-click;
// ^error{The property must be known at compile time and cannot be changed at runtime}
}
}
PopupWindow {
close-policy: root.external;
// ^error{The close-policy property only supports constants at the moment}
}
}

View file

@ -588,7 +588,7 @@ impl Snapshotter {
.expect("Looking at a known component"), .expect("Looking at a known component"),
x: popup_window.x.snapshot(self), x: popup_window.x.snapshot(self),
y: popup_window.y.snapshot(self), y: popup_window.y.snapshot(self),
close_on_click: popup_window.close_on_click, close_policy: popup_window.close_policy.clone(),
parent_element: self.use_element(&popup_window.parent_element), parent_element: self.use_element(&popup_window.parent_element),
} }
} }

View file

@ -369,6 +369,9 @@ impl TypeRegister {
popup.properties.get_mut("close-on-click").unwrap().property_visibility = popup.properties.get_mut("close-on-click").unwrap().property_visibility =
PropertyVisibility::Constexpr; PropertyVisibility::Constexpr;
popup.properties.get_mut("close-policy").unwrap().property_visibility =
PropertyVisibility::Constexpr;
} }
_ => unreachable!(), _ => unreachable!(),

View file

@ -19,7 +19,7 @@ export component DatePickerPopup inherits PopupWindow {
width: 360px; width: 360px;
height: 524px; height: 524px;
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := MenuBorder { background-layer := MenuBorder {
width: dialog.width; width: dialog.width;

View file

@ -19,7 +19,7 @@ export component TimePickerPopup inherits PopupWindow {
callback canceled(); callback canceled();
callback accepted(/* current-time */ Time); callback accepted(/* current-time */ Time);
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := MenuBorder { background-layer := MenuBorder {
width: dialog.width; width: dialog.width;

View file

@ -19,7 +19,7 @@ export component DatePickerPopup inherits PopupWindow {
width: 360px; width: 360px;
height: 524px; height: 524px;
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := MenuBorder { background-layer := MenuBorder {
width: dialog.width; width: dialog.width;

View file

@ -18,7 +18,7 @@ export component TimePickerPopup inherits PopupWindow {
callback canceled(); callback canceled();
callback accepted(/* current-time */ Time); callback accepted(/* current-time */ Time);
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := MenuBorder { background-layer := MenuBorder {
width: dialog.width; width: dialog.width;

View file

@ -19,7 +19,7 @@ export component DatePickerPopup inherits PopupWindow {
width: 368px; width: 368px;
height: 524px; height: 524px;
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := MenuBorder { background-layer := MenuBorder {
width: dialog.width; width: dialog.width;

View file

@ -18,7 +18,7 @@ export component TimePickerPopup inherits PopupWindow {
callback canceled(); callback canceled();
callback accepted(/* current-time */ Time); callback accepted(/* current-time */ Time);
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := MenuBorder { background-layer := MenuBorder {
width: dialog.width; width: dialog.width;

View file

@ -17,7 +17,7 @@ export component DatePickerPopup inherits PopupWindow {
width: 360px; width: 360px;
height: 524px; height: 524px;
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := Rectangle { background-layer := Rectangle {
width: dialog.width; width: dialog.width;

View file

@ -17,7 +17,7 @@ export component TimePickerPopup inherits PopupWindow {
callback canceled(); callback canceled();
callback accepted(/* current-time */ Time); callback accepted(/* current-time */ Time);
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := Rectangle { background-layer := Rectangle {
width: dialog.width; width: dialog.width;

View file

@ -21,7 +21,7 @@ export component DatePickerPopup inherits PopupWindow {
width: 360px; width: 360px;
height: 524px; height: 524px;
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := Rectangle { background-layer := Rectangle {
border-radius: 8px; border-radius: 8px;

View file

@ -18,7 +18,7 @@ export component TimePickerPopup inherits PopupWindow {
callback canceled(); callback canceled();
callback accepted(/* current-time */ Time); callback accepted(/* current-time */ Time);
close-on-click: false; close-policy: PopupClosePolicy.no-auto-close;
background-layer := Rectangle { background-layer := Rectangle {
width: dialog.width; width: dialog.width;

View file

@ -17,6 +17,7 @@ use crate::input::{
}; };
use crate::item_tree::ItemRc; use crate::item_tree::ItemRc;
use crate::item_tree::{ItemTreeRc, ItemTreeRef, ItemTreeVTable, ItemTreeWeak}; use crate::item_tree::{ItemTreeRc, ItemTreeRef, ItemTreeVTable, ItemTreeWeak};
use crate::items::PopupClosePolicy;
use crate::items::{ColorScheme, InputType, ItemRef, MouseCursor}; use crate::items::{ColorScheme, InputType, ItemRef, MouseCursor};
use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, SizeLengths}; use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, SizeLengths};
use crate::properties::{Property, PropertyTracker}; use crate::properties::{Property, PropertyTracker};
@ -386,9 +387,8 @@ struct PopupWindow {
location: PopupWindowLocation, location: PopupWindowLocation,
/// The component that is responsible for providing the popup content. /// The component that is responsible for providing the popup content.
component: ItemTreeRc, component: ItemTreeRc,
/// If true, Slint will close the popup after any mouse click within the popup. //// Defines the close behaviour of the popup.
/// Set to false and call close() on the PopupWindow to close it manually. close_policy: PopupClosePolicy,
close_on_click: bool,
} }
#[pin_project::pin_project] #[pin_project::pin_project]
@ -578,7 +578,7 @@ impl WindowInner {
self.had_popup_on_press.set(self.active_popup.borrow().is_some()); self.had_popup_on_press.set(self.active_popup.borrow().is_some());
} }
let close_popup_on_click = self.close_popup_on_click(); let close_policy = self.close_policy();
let mut mouse_inside_popup = false; let mut mouse_inside_popup = false;
mouse_input_state = if let Some(mut event) = mouse_input_state = if let Some(mut event) =
@ -637,12 +637,21 @@ impl WindowInner {
self.mouse_input_state.set(mouse_input_state); self.mouse_input_state.set(mouse_input_state);
if close_popup_on_click match close_policy {
&& ((mouse_inside_popup && released_event && self.had_popup_on_press.get()) PopupClosePolicy::CloseOnClick => {
|| (!mouse_inside_popup && pressed_event)) if (mouse_inside_popup && released_event && self.had_popup_on_press.get())
{ || (!mouse_inside_popup && pressed_event)
self.close_popup(); {
} self.close_popup();
}
}
PopupClosePolicy::CloseOnClickOutside => {
if !mouse_inside_popup && pressed_event {
self.close_popup();
}
}
PopupClosePolicy::NoAutoClose => {}
};
crate::properties::ChangeTracker::run_change_handlers(); crate::properties::ChangeTracker::run_change_handlers();
} }
@ -968,7 +977,7 @@ impl WindowInner {
&self, &self,
popup_componentrc: &ItemTreeRc, popup_componentrc: &ItemTreeRc,
position: Point, position: Point,
close_on_click: bool, close_policy: PopupClosePolicy,
parent_item: &ItemRc, parent_item: &ItemRc,
) { ) {
let position = parent_item.map_to_window( let position = parent_item.map_to_window(
@ -1036,7 +1045,7 @@ impl WindowInner {
self.active_popup.replace(Some(PopupWindow { self.active_popup.replace(Some(PopupWindow {
location, location,
component: popup_componentrc.clone(), component: popup_componentrc.clone(),
close_on_click, close_policy,
})); }));
} }
@ -1066,9 +1075,12 @@ impl WindowInner {
} }
} }
/// Returns true if the currently active popup is configured to close on click. None if there is no active popup. /// Returns the close policy of the active popup. PopupClosePolicy::NoAutoClose if there is no active popup.
pub fn close_popup_on_click(&self) -> bool { pub fn close_policy(&self) -> PopupClosePolicy {
self.active_popup.borrow().as_ref().map_or(false, |popup| popup.close_on_click) self.active_popup
.borrow()
.as_ref()
.map_or(PopupClosePolicy::NoAutoClose, |popup| popup.close_policy)
} }
/// Returns the scale factor set on the window, as provided by the windowing system. /// Returns the scale factor set on the window, as provided by the windowing system.
@ -1329,14 +1341,14 @@ pub mod ffi {
handle: *const WindowAdapterRcOpaque, handle: *const WindowAdapterRcOpaque,
popup: &ItemTreeRc, popup: &ItemTreeRc,
position: crate::graphics::Point, position: crate::graphics::Point,
close_on_click: bool, close_policy: PopupClosePolicy,
parent_item: &ItemRc, parent_item: &ItemRc,
) { ) {
let window_adapter = &*(handle as *const Rc<dyn WindowAdapter>); let window_adapter = &*(handle as *const Rc<dyn WindowAdapter>);
WindowInner::from_pub(window_adapter.window()).show_popup( WindowInner::from_pub(window_adapter.window()).show_popup(
popup, popup,
position, position,
close_on_click, close_policy,
parent_item, parent_item,
); );
} }

View file

@ -22,7 +22,9 @@ use i_slint_core::item_tree::{
ItemRc, ItemTreeNode, ItemVisitorRefMut, ItemVisitorVTable, ItemWeak, TraversalOrder, ItemRc, ItemTreeNode, ItemVisitorRefMut, ItemVisitorVTable, ItemWeak, TraversalOrder,
VisitChildrenResult, VisitChildrenResult,
}; };
use i_slint_core::items::{AccessibleRole, ItemRef, ItemVTable, PropertyAnimation}; use i_slint_core::items::{
AccessibleRole, ItemRef, ItemVTable, PopupClosePolicy, PropertyAnimation,
};
use i_slint_core::layout::{BoxLayoutCellData, LayoutInfo, Orientation}; use i_slint_core::layout::{BoxLayoutCellData, LayoutInfo, Orientation};
use i_slint_core::lengths::{LogicalLength, LogicalRect}; use i_slint_core::lengths::{LogicalLength, LogicalRect};
use i_slint_core::model::RepeatedItemTree; use i_slint_core::model::RepeatedItemTree;
@ -2328,7 +2330,7 @@ impl<'a, 'id> InstanceRef<'a, 'id> {
pub fn show_popup( pub fn show_popup(
popup: &object_tree::PopupWindow, popup: &object_tree::PopupWindow,
pos_getter: impl FnOnce(InstanceRef<'_, '_>) -> i_slint_core::graphics::Point, pos_getter: impl FnOnce(InstanceRef<'_, '_>) -> i_slint_core::graphics::Point,
close_on_click: bool, close_policy: PopupClosePolicy,
parent_comp: ErasedItemTreeBoxWeak, parent_comp: ErasedItemTreeBoxWeak,
parent_window_adapter: WindowAdapterRc, parent_window_adapter: WindowAdapterRc,
parent_item: &ItemRc, parent_item: &ItemRc,
@ -2354,7 +2356,7 @@ pub fn show_popup(
WindowInner::from_pub(parent_window_adapter.window()).show_popup( WindowInner::from_pub(parent_window_adapter.window()).show_popup(
&vtable::VRc::into_dyn(inst), &vtable::VRc::into_dyn(inst),
pos, pos,
close_on_click, close_policy,
parent_item, parent_item,
); );
} }

View file

@ -606,6 +606,13 @@ fn call_builtin_function(
parent_item_info.item_index(), parent_item_info.item_index(),
); );
let close_policy = Value::EnumerationValue(
popup.close_policy.enumeration.name.to_string(),
popup.close_policy.to_string(),
)
.try_into()
.expect("Invalid internal enumeration representation for close policy");
crate::dynamic_item_tree::show_popup( crate::dynamic_item_tree::show_popup(
popup, popup,
|instance_ref| { |instance_ref| {
@ -619,7 +626,7 @@ fn call_builtin_function(
y.try_into().unwrap(), y.try_into().unwrap(),
) )
}, },
popup.close_on_click, close_policy,
enclosing_component.self_weak().get().unwrap().clone(), enclosing_component.self_weak().get().unwrap().clone(),
component.window_adapter(), component.window_adapter(),
&parent_item, &parent_item,

View file

@ -0,0 +1,455 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export component TestCase {
width: 300px;
height: 300px;
in-out property <bool> popup-created;
in-out property <int> click-count;
in-out property <int> popup-selector: 0;
in-out property <int> popup-clicked;
out property <length> last-underneath-mouse-x: ta.mouse-x;
out property <length> last-underneath-mouse-y: ta.mouse-y;
callback do-close;
do-close => {
close-from-outside.close();
}
default-popup := PopupWindow {
x: 10px;
y: 10px;
width: parent.width - 20px;
height: parent.height - 20px;
Text {
text: "I'm a default";
}
init => {
root.popup-created = true;
}
TouchArea {
width: 7px; x: 0px;
clicked => {
popup-clicked += 1;
}
}
}
self-closing-popup := PopupWindow {
close-policy: no-auto-close;
x: 10px;
y: 10px;
width: parent.width - 20px;
height: parent.height - 20px;
Text {
text: "I'm a self-closing popup";
}
TouchArea {
clicked => {
self-closing-popup.close();
}
}
init => {
root.popup-created = true;
}
}
close-from-outside := PopupWindow {
close-policy: no-auto-close;
x: 10px;
y: 10px;
width: parent.width - 20px;
height: parent.height - 20px;
Text {
text: "I'm a popup that only close from outside";
}
TouchArea {
clicked => {
popup-clicked += 1000;
}
}
init => {
root.popup-created = true;
}
}
close-click-outside := PopupWindow {
x: 10px;
y: 10px;
width: parent.width - 20px;
height: parent.height - 20px;
close-policy: close-on-click-outside;
Text {
text: "I close on outside click";
}
init => {
root.popup-created = true;
}
TouchArea {
width: 7px; x: 0px;
clicked => {
popup-clicked += 2;
}
}
}
ta := TouchArea {
clicked => {
root.click-count = root.click-count + 1;
if (root.popup-selector == 0) {
root.popup-selector = 4;
default-popup.show();
} else if (root.popup-selector == 1) {
root.popup-selector = 4;
self-closing-popup.show();
} else if (root.popup-selector == 2) {
root.popup-selector = 4;
close-from-outside.show();
} else if (root.popup-selector == 3) {
root.popup-selector = 4;
close-click-outside.show();
}
}
}
}
/*
```rust
use slint::{platform::WindowEvent, platform::PointerEventButton, LogicalPosition};
let instance = TestCase::new().unwrap();
assert_eq!(instance.get_click_count(), 0);
assert_eq!(instance.get_popup_created(), false);
// --------- Default popup
instance.set_popup_selector(0);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_created(), true);
assert_eq!(instance.get_popup_clicked(), 0);
// Click to close
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 1);
// Subsequent click to verify that it was closed
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 2);
assert_eq!(instance.get_popup_clicked(), 1);
// --------- Default popup but verify closed on press when outside
instance.set_popup_selector(0);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_last_underneath_mouse_x(), 15.);
assert_eq!(instance.get_last_underneath_mouse_y(), 15.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_created(), true);
assert_eq!(instance.get_popup_clicked(), 1);
// mouse grabbed, underneath won't notice
instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(1.0, 1.0) });
assert_eq!(instance.get_last_underneath_mouse_x(), 15.);
assert_eq!(instance.get_last_underneath_mouse_y(), 15.);
// press should close
instance.window().dispatch_event(WindowEvent::PointerPressed { position: LogicalPosition::new(1.0, 1.0), button: PointerEventButton::Left });
// if it was closed, the underneath should receive the move event
instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(12.0, 12.0) });
assert_eq!(instance.get_last_underneath_mouse_x(), 12.);
assert_eq!(instance.get_last_underneath_mouse_y(), 12.);
slint_testing::mock_elapsed_time(50);
instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(12.0, 12.0), button: PointerEventButton::Left });
assert_eq!(instance.get_click_count(), 1);
// Subsequent click to verify that it was closed
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 2);
assert_eq!(instance.get_popup_clicked(), 1);
// --------- Popup with close-policy: no-auto-close
instance.set_popup_selector(1);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_created(), true);
assert_eq!(instance.get_popup_clicked(), 1);
// Click outside, nothing happens
slint_testing::send_mouse_click(&instance, 1., 1.);
assert_eq!(instance.get_click_count(), 1);
// Click outside again, nothing happens
slint_testing::send_mouse_click(&instance, 295., 295.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_clicked(), 1);
// Click on the popup, it's registered and the custom TouchArea calls close()
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 1);
// Subsequent click to verify that it was closed
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 2);
assert_eq!(instance.get_popup_clicked(), 1);
// --------- Popup with close-policy: no-auto-close closed externally
instance.set_popup_selector(2);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_created(), true);
assert_eq!(instance.get_popup_clicked(), 1);
// Click outside, nothing happens
slint_testing::send_mouse_click(&instance, 1., 1.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_clicked(), 1);
// Click on the popup, it's registered but nothing is done
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_clicked(), 1001);
// Click again to verify that it was _not_ closed
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_clicked(), 2001);
// Close manually and verify that subsequent click is passed through
instance.invoke_do_close();
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 2);
instance.invoke_do_close();
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 3);
assert_eq!(instance.get_popup_clicked(), 2001);
// --------- Close outside click popup
instance.set_popup_selector(3);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_created(), true);
assert_eq!(instance.get_popup_clicked(), 2001);
// click inside
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq!(instance.get_popup_clicked(), 2003);
// Click outside to close
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 1);
// Subsequent click to verify that it was closed
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 2);
assert_eq!(instance.get_popup_clicked(), 2003);
```
```cpp
auto handle = TestCase::create();
const TestCase &instance = *handle;
assert_eq(instance.get_click_count(), 0);
assert_eq(instance.get_popup_created(), false);
// --------- Default popup
instance.set_popup_selector(0);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_created(), true);
assert_eq(instance.get_popup_clicked(), 0);
// Click to close
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 1);
// Subsequent click to verify that it was closed
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 2);
assert_eq(instance.get_popup_clicked(), 1);
// --------- Default popup but verify closed on press when outside
instance.set_popup_selector(0);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_last_underneath_mouse_x(), 15.);
assert_eq(instance.get_last_underneath_mouse_y(), 15.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_created(), true);
assert_eq(instance.get_popup_clicked(), 1);
// mouse grabbed, underneath won't notice
instance.window().dispatch_pointer_move_event(slint::LogicalPosition({1.0, 1.0}));
assert_eq(instance.get_last_underneath_mouse_x(), 15.);
assert_eq(instance.get_last_underneath_mouse_y(), 15.);
// press should close
instance.window().dispatch_pointer_press_event(slint::LogicalPosition({1.0, 1.0}), slint::PointerEventButton::Left);
// if it was closed, the underneath should receive the move event
instance.window().dispatch_pointer_move_event(slint::LogicalPosition({12.0, 12.0}));
assert_eq(instance.get_last_underneath_mouse_x(), 12.);
assert_eq(instance.get_last_underneath_mouse_y(), 12.);
slint_testing::mock_elapsed_time(50);
instance.window().dispatch_pointer_release_event(slint::LogicalPosition({12.0, 12.0}), slint::PointerEventButton::Left);
assert_eq(instance.get_click_count(), 1);
// Subsequent click to verify that it was closed
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 2);
assert_eq(instance.get_popup_clicked(), 1);
// --------- Popup with close-policy: no-auto-close
instance.set_popup_selector(1);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_created(), true);
assert_eq(instance.get_popup_clicked(), 1);
// Click outside, nothing happens
slint_testing::send_mouse_click(&instance, 1., 1.);
assert_eq(instance.get_click_count(), 1);
// Click outside again, nothing happens
slint_testing::send_mouse_click(&instance, 295., 295.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_clicked(), 1);
// Click on the popup, it's registered and the custom TouchArea calls close()
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 1);
// Subsequent click to verify that it was closed
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 2);
assert_eq(instance.get_popup_clicked(), 1);
// --------- Popup with close-policy: no-auto-close closed externally
instance.set_popup_selector(2);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_created(), true);
assert_eq(instance.get_popup_clicked(), 1);
// Click outside, nothing happens
slint_testing::send_mouse_click(&instance, 1., 1.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_clicked(), 1);
// Click on the popup, it's registered but nothing is done
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_clicked(), 1001);
// Click again to verify that it was _not_ closed
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_clicked(), 2001);
// Close manually and verify that subsequent click is passed through
instance.invoke_do_close();
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 2);
instance.invoke_do_close();
slint_testing::send_mouse_click(&instance, 15., 15.);
assert_eq(instance.get_click_count(), 3);
assert_eq(instance.get_popup_clicked(), 2001);
```
```disable-because-nodejs-runs-with-qt-and-send-mouse-click-wont-send-to-popup-qwindow
var instance = new slint.TestCase({});
assert.equal(instance.click_count, 0);
assert.equal(instance.popup_created, false);
instance.popup_selector = 0;
instance.popup_created = false;
instance.click_count = 0;
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 1);
assert.equal(instance.popup_created, true);
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 1);
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 2);
instance.popup_selector = 1;
instance.popup_created = false;
instance.click_count = 0;
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 1);
assert.equal(instance.popup_created, true);
slintlib.private_api.send_mouse_click(instance, 1., 1.);
assert.equal(instance.click_count, 1);
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 1);
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 2);
instance.popup_selector = 2;
instance.popup_created = false;
instance.click_count = 0;
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 1);
assert.equal(instance.popup_created, true);
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 1);
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 1);
instance.do_close();
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 2);
instance.do_close();
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.click_count, 3);
// --------- Close outside click popup
instance.set_popup_selector(3);
instance.set_popup_created(false);
instance.set_click_count(0);
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.get_click_count(), 1);
assert.equal(instance.get_popup_created(), true);
assert.equal(instance.get_popup_clicked(), 2001);
// click inside
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.get_popup_clicked(), 2003);
// Click outside to close
slintlib.private_api.send_mouse_click(instance, 5., 5.);
assert.equal(instance.get_click_count(), 1);
// Subsequent click to verify that it was closed
slintlib.private_api.send_mouse_click(instance, 15., 15.);
assert.equal(instance.get_click_count(), 2);
assert.equal(instance.get_popup_clicked(), 2003);
```
*/

View file

@ -18,7 +18,7 @@ component MyPopup inherits PopupWindow {
} }
component MyPopup2 inherits MyPopup { component MyPopup2 inherits MyPopup {
close-on-click: false; close-policy: no-auto-close;
} }
export component TestCase { export component TestCase {