diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index cbaa7bd7e4d..4928aeed7a7 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -1774,12 +1774,10 @@ fn generate_item_tree( // Repeaters run their user_init() code from Repeater::ensure_updated() after update() initialized model_data/index. // And in PopupWindow this is also called by the runtime - if parent_ctx.is_none() { + if parent_ctx.is_none() && !is_popup_menu { create_code.push("self->user_init();".to_string()); - if !is_popup_menu { - // initialize the Window in this point to be consistent with Rust - create_code.push("self->window();".to_string()) - } + // initialize the Window in this point to be consistent with Rust + create_code.push("self->window();".to_string()) } create_code @@ -3775,6 +3773,7 @@ fn compile_builtin_function_call( let access_entries = access_member(&popup.entries, &popup_ctx); let access_sub_menu = access_member(&popup.sub_menu, &popup_ctx); let access_activated = access_member(&popup.activated, &popup_ctx); + let access_close = access_member(&popup.close, &popup_ctx); let init = if let llr::Expression::NumberLiteral(tree_index) = entries { // We have an MenuItem tree let current_sub_component = ctx.current_sub_component().unwrap(); @@ -3787,26 +3786,36 @@ fn compile_builtin_function_call( ") } else { - let forward_callback = |access, cb| { + let forward_callback = |access, cb, default| { format!("{access}.set_handler( - [context_menu](const auto &entry) {{ - return context_menu->{cb}.call(entry); + [context_menu, parent_weak](const auto &entry) {{ + if(auto lock = parent_weak.lock()) {{ + return context_menu->{cb}.call(entry); + }} else {{ + return {default}; + }} }});") }; - let fw_sub_menu = forward_callback(access_sub_menu, "sub_menu"); - let fw_activated = forward_callback(access_activated, "activated"); + let fw_sub_menu = forward_callback(access_sub_menu, "sub_menu", "std::shared_ptr>()"); + let fw_activated = forward_callback(access_activated, "activated", ""); let entries = compile_expression(entries, ctx); format!(r" - const slint::cbindgen_private::ContextMenu *context_menu = &({context_menu}); auto entries = {entries}; - {{ - auto self = popup_menu; - {access_entries}.set(std::move(entries)); - {fw_sub_menu} - {fw_activated} - }}") + const slint::cbindgen_private::ContextMenu *context_menu = &({context_menu}); + auto self = popup_menu; + {access_entries}.set(std::move(entries)); + {fw_sub_menu} + {fw_activated} + ") }; - format!("{window}.close_popup({context_menu}.popup_id); {context_menu}.popup_id = {window}.show_popup_menu<{popup_id}>({globals}, {position}, {{ {context_menu_rc} }}, [self](auto popup_menu) {{ {init} }})", globals = ctx.generator_state.global_access) + format!(r" + {window}.close_popup({context_menu}.popup_id); + {context_menu}.popup_id = {window}.show_popup_menu<{popup_id}>({globals}, {position}, {{ {context_menu_rc} }}, [self](auto popup_menu) {{ + auto parent_weak = self->self_weak; + auto self_ = self; + {init} + {access_close}.set_handler([parent_weak,self = self_] {{ if(auto lock = parent_weak.lock()) {{ {window}.close_popup({context_menu}.popup_id); }} }}); + }})", globals = ctx.generator_state.global_access) } BuiltinFunction::SetSelectionOffsets => { if let [llr::Expression::PropertyReference(pr), from, to] = arguments { diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index db5c7a7ba65..663b27a4699 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -2783,6 +2783,13 @@ fn compile_builtin_function_call( let access_entries = access_member(&popup.entries, &popup_ctx).unwrap(); let access_sub_menu = access_member(&popup.sub_menu, &popup_ctx).unwrap(); let access_activated = access_member(&popup.activated, &popup_ctx).unwrap(); + let access_close = access_member(&popup.close, &popup_ctx).unwrap(); + + let close_popup = context_menu.clone().then(|context_menu| quote!{ + if let Some(current_id) = #context_menu.popup_id.take() { + sp::WindowInner::from_pub(#window_adapter_tokens.window()).close_popup(current_id); + } + }); let init_popup = if let Expression::NumberLiteral(tree_index) = entries { // We have an MenuItem tree @@ -2807,6 +2814,12 @@ fn compile_builtin_function_call( #access_activated.set_handler(move |entry| { sp::Menu::activate(&*context_menu_item_tree, &entry.0); }); + let self_weak = parent_weak.clone(); + #access_close.set_handler(move |()| { + let self_rc = self_weak.upgrade().unwrap(); + let _self = self_rc.as_pin_ref(); + #close_popup + }); }) } else { // entries should be an expression of type array of MenuEntry @@ -2836,6 +2849,12 @@ fn compile_builtin_function_call( #access_entries.set(entries); #fw_sub_menu #fw_activated + let self_weak = parent_weak.clone(); + #access_close.set_handler(move |()| { + let Some(self_rc) = self_weak.upgrade() else { return }; + let _self = self_rc.as_pin_ref(); + #close_popup + }); } } }; @@ -2846,9 +2865,7 @@ fn compile_builtin_function_call( let popup_instance_vrc = sp::VRc::map(popup_instance.clone(), |x| x); let parent_weak = _self.self_weak.get().unwrap().clone(); #init_popup - if let Some(current_id) = #context_menu.popup_id.take() { - sp::WindowInner::from_pub(#window_adapter_tokens.window()).close_popup(current_id); - } + #close_popup let id = sp::WindowInner::from_pub(#window_adapter_tokens.window()).show_popup( &sp::VRc::into_dyn(popup_instance.into()), position, diff --git a/internal/compiler/llr/item_tree.rs b/internal/compiler/llr/item_tree.rs index 8c3c2261029..4464136d473 100644 --- a/internal/compiler/llr/item_tree.rs +++ b/internal/compiler/llr/item_tree.rs @@ -297,6 +297,7 @@ pub struct PopupMenu { pub item_tree: ItemTree, pub sub_menu: PropertyReference, pub activated: PropertyReference, + pub close: PropertyReference, pub entries: PropertyReference, } diff --git a/internal/compiler/llr/lower_to_item_tree.rs b/internal/compiler/llr/lower_to_item_tree.rs index 0a813de687c..ebb8d54e153 100644 --- a/internal/compiler/llr/lower_to_item_tree.rs +++ b/internal/compiler/llr/lower_to_item_tree.rs @@ -78,6 +78,10 @@ pub fn lower_to_item_tree( &NamedReference::new(&c.root_element, SmolStr::new_static("activated")), &state, ); + let close = sc.mapping.map_property_reference( + &NamedReference::new(&c.root_element, SmolStr::new_static("close")), + &state, + ); let entries = sc.mapping.map_property_reference( &NamedReference::new(&c.root_element, SmolStr::new_static("entries")), &state, @@ -87,7 +91,7 @@ pub fn lower_to_item_tree( root: state.push_sub_component(sc), parent_context: None, }; - PopupMenu { item_tree, sub_menu, activated, entries } + PopupMenu { item_tree, sub_menu, activated, close, entries } }); let root = CompilationUnit { diff --git a/internal/compiler/widgets/common/menus.slint b/internal/compiler/widgets/common/menus.slint index 079f3a47f79..ddc660f3f62 100644 --- a/internal/compiler/widgets/common/menus.slint +++ b/internal/compiler/widgets/common/menus.slint @@ -10,6 +10,7 @@ export component PopupMenuImpl inherits Window { in property <[MenuEntry]> entries: []; callback sub-menu(MenuEntry) -> [MenuEntry]; callback activated(MenuEntry); + callback close(); private property current: -1; forward-focus: fs; @@ -48,10 +49,17 @@ export component PopupMenuImpl inherits Window { } TouchArea { pointer-event(event) => { - fs.focus(); - if event.kind == PointerEventKind.move { + if event.kind == PointerEventKind.move && current != idx { + fs.focus(); current = idx; open-sub-menu-after-timeout.running = true; + } else if event.kind == PointerEventKind.down && entry.has-sub-menu { + activate(entry, self.absolute-position.y); + } else if event.kind == PointerEventKind.up + && self.mouse-y > 0 && self.mouse-y < self.height + && self.mouse-x > 0 && self.mouse-x < self.width { + // can't put this in `clicked` because then the menu would close causing a panic in the pointer-event + activate(entry, self.absolute-position.y); } } @@ -60,10 +68,6 @@ export component PopupMenuImpl inherits Window { current = -1 } } - - clicked => { - activate(entry, self.absolute-position.y); - } } } } @@ -98,6 +102,7 @@ export component PopupMenuImpl inherits Window { return accept; } else if event.text == Key.LeftArrow { // TODO: should close only if this menu is a sub menu + root.close(); } return reject; } @@ -121,7 +126,7 @@ export component PopupMenuImpl inherits Window { subMenu := ContextMenuInternal { x: 0; y: 0; width: 0; height: 0; sub-menu(entry) => { root.sub-menu(entry); } - activated(entry) => { root.activated(entry); } + activated(entry) => { root.activated(entry); root.close(); } } function activate(entry : MenuEntry, y: length) { @@ -134,6 +139,7 @@ export component PopupMenuImpl inherits Window { }); } else { activated(entry); + close(); } } } @@ -185,7 +191,7 @@ export component MenuBarImpl { cm := ContextMenuInternal { - activated(entry) => { root.activated(entry); } + activated(entry) => { root.activated(entry); self.close(); } sub-menu(entry) => { root.sub-menu(entry); } } } diff --git a/internal/core/window.rs b/internal/core/window.rs index ebb624c22ae..bcfca0cd831 100644 --- a/internal/core/window.rs +++ b/internal/core/window.rs @@ -598,7 +598,7 @@ impl WindowInner { self.had_popup_on_press.set(!self.active_popups.borrow().is_empty()); } - let popup_to_close = self.active_popups.borrow().last().and_then(|popup| { + let mut popup_to_close = self.active_popups.borrow().last().and_then(|popup| { let mouse_inside_popup = || { if let PopupWindowLocation::ChildWindow(coordinates) = &popup.location { event.position().map_or(true, |pos| { @@ -644,6 +644,9 @@ impl WindowInner { if !popup.is_menu { break; + } else if popup_to_close.is_some() { + // clicking outside of a popup menu should close all the menus + popup_to_close = Some(popup.popup_id); } } @@ -1224,6 +1227,13 @@ impl WindowInner { let p = active_popups.remove(popup_index); drop(active_popups); self.close_popup_impl(&p); + if p.is_menu { + // close all sub-menus + while self.active_popups.borrow().get(popup_index).is_some_and(|p| p.is_menu) { + let p = self.active_popups.borrow_mut().remove(popup_index); + self.close_popup_impl(&p); + } + } } } diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index 090c51bb3b8..d3fb1c4799e 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -6,9 +6,10 @@ use crate::dynamic_item_tree::{CallbackHandler, InstanceRef}; use core::pin::Pin; use corelib::graphics::{GradientStop, LinearGradientBrush, PathElement, RadialGradientBrush}; use corelib::items::{ColorScheme, ItemRef, MenuEntry, PropertyAnimation}; -use corelib::menus::{Menu, MenuVTable}; +use corelib::menus::{Menu, MenuFromItemTree, MenuVTable}; use corelib::model::{Model, ModelExt, ModelRc, VecModel}; use corelib::rtti::AnimatedBindingKind; +use corelib::window::WindowInner; use corelib::{Brush, Color, PathData, SharedString, SharedVector}; use i_slint_compiler::expression_tree::{ BuiltinFunction, Callable, EasingCurve, Expression, MinMaxOp, Path as ExprPath, @@ -18,7 +19,6 @@ use i_slint_compiler::langtype::Type; use i_slint_compiler::namedreference::NamedReference; use i_slint_compiler::object_tree::ElementRc; use i_slint_core as corelib; -use i_slint_core::menus::MenuFromItemTree; use smol_str::SmolStr; use std::collections::HashMap; use std::rc::Rc; @@ -762,6 +762,26 @@ fn call_builtin_function( ) .unwrap(); } + let item_weak = item_rc.downgrade(); + compiled + .set_callback_handler( + inst_ref.borrow(), + "close", + Box::new(move |_args: &[Value]| -> Value { + let Some(item_rc) = item_weak.upgrade() else { return Value::Void }; + if let Some(id) = item_rc + .downcast::() + .unwrap() + .popup_id + .take() + { + WindowInner::from_pub(item_rc.window_adapter().unwrap().window()) + .close_popup(id); + } + Value::Void + }), + ) + .unwrap(); component.access_window(|window| { let context_menu_elem = item_rc.downcast::().unwrap(); if let Some(old_id) = context_menu_elem.popup_id.take() { diff --git a/tests/cases/widgets/contextmenu.slint b/tests/cases/widgets/contextmenu.slint index 52255c1add7..13039e0263a 100644 --- a/tests/cases/widgets/contextmenu.slint +++ b/tests/cases/widgets/contextmenu.slint @@ -6,51 +6,40 @@ import { AboutSlint, Button } from "std-widgets.slint"; export component TestCase inherits Window { width: 700px; height: 700px; - ContextMenu { - /*entries: [ - { title: "Entry1" }, - { title: "Entry2", has-sub-menu: true }, - ]; - sub-menu(entry) => { - if entry.title == "Entry2" { - return [ - { title: "New" }, - { title: "Open" }, - { title: "Xxx", has-sub-menu: true}, - ]; - } else if entry.title == "Xxx" { - return [ - { title: "Aaa" }, - { title: "Xxx", has-sub-menu: true }, - { title: "Yyy" }, - ]; - } else { - return []; - } + + forward-focus: fs; + + in-out property result; + Text {text: result;} + + Rectangle { + background: Colors.red; + Rectangle { + background: Colors.blue.transparentize(0.5); } - activated(entry) => { - debug("Activated", entry); - }*/ + } + + ContextMenu { MenuItem { title: "Entry1"; - activated => { debug("Entry1"); } + activated => { debug("Entry1"); result += "Entry1"; } } MenuItem { title: "Entry2"; MenuItem { title: "New"; - activated => { debug("New"); } + activated => { debug("New"); result += "New"; } } MenuItem { title: "Open"; MenuItem { title: "Open 1"; - activated => { debug("1"); } + activated => { debug("1"); result += "Open1"; } } MenuItem { title: "Open 2"; - activated => { debug("2"); } + activated => { debug("2"); result += "Open2"; } } } } @@ -65,13 +54,58 @@ export component TestCase inherits Window { /* ```rust +use slint::{platform::WindowEvent, platform::PointerEventButton, platform::Key, LogicalPosition, SharedString}; + let instance = TestCase::new().unwrap(); assert!(instance.get_test()); + +// right click to open the menu +instance.window().dispatch_event(WindowEvent::PointerPressed { position: LogicalPosition::new(15.0, 15.0), button: PointerEventButton::Right }); +instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(15.0, 15.0), button: PointerEventButton::Right }); +// press on entry1 +instance.window().dispatch_event(WindowEvent::PointerPressed { position: LogicalPosition::new(25.0, 25.0), button: PointerEventButton::Left }); +instance.window().dispatch_event(WindowEvent::PointerReleased { position: LogicalPosition::new(25.0, 25.0), button: PointerEventButton::Left }); +assert_eq!(instance.get_result(), "Entry1"); + +// use the keyboard +instance.set_result("".into()); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::Menu)); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::DownArrow)); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::DownArrow)); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::RightArrow)); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::DownArrow)); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::DownArrow)); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::Return)); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::UpArrow)); +slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::Return)); +assert_eq!(instance.get_result(), "Open2"); ``` ```cpp auto handle = TestCase::create(); const TestCase &instance = *handle; assert(instance.get_test()); + +// right click to open the menu +instance.window().dispatch_pointer_press_event(slint::LogicalPosition({15.0, 15.0}), slint::PointerEventButton::Right); +instance.window().dispatch_pointer_release_event(slint::LogicalPosition({15.0, 15.0}), slint::PointerEventButton::Right); +// press on entry1 +instance.window().dispatch_pointer_press_event(slint::LogicalPosition({25.0, 25.0}), slint::PointerEventButton::Left); +instance.window().dispatch_pointer_release_event(slint::LogicalPosition({25.0, 25.0}), slint::PointerEventButton::Left); +assert_eq(instance.get_result(), "Entry1"); + +// use the keyboard +instance.set_result(""); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::Menu); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::DownArrow); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::DownArrow); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::RightArrow); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::DownArrow); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::DownArrow); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::Return); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::UpArrow); +slint_testing::send_keyboard_string_sequence(&instance, slint::platform::key_codes::Return); +assert_eq(instance.get_result(), "Open2"); + ``` */ diff --git a/tests/driver/cpp/cppdriver.rs b/tests/driver/cpp/cppdriver.rs index 330f141d1b7..3b7108fd9ae 100644 --- a/tests/driver/cpp/cppdriver.rs +++ b/tests/driver/cpp/cppdriver.rs @@ -153,6 +153,9 @@ namespace slint_testing = slint::private_api::testing; let mut cmd; if std::env::var("USE_VALGRIND").is_ok() { cmd = std::process::Command::new("valgrind"); + cmd.arg("--exit-on-first-error=yes"); + cmd.arg("--error-exitcode=1"); + cmd.arg("--num-callers=50"); cmd.arg(binary_path.deref()); } else { cmd = std::process::Command::new(binary_path.deref());