ContextMenuArea: Intercept long press on Android

This commit is contained in:
Olivier Goffart 2025-02-24 18:27:25 +01:00 committed by GitHub
parent bd80829a44
commit 7390df1b47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 53 additions and 7 deletions

View file

@ -235,7 +235,9 @@ fn default_config() -> cbindgen::Config {
config.defines = [
("target_pointer_width = 64".into(), "SLINT_TARGET_64".into()),
("target_pointer_width = 32".into(), "SLINT_TARGET_32".into()),
("target_arch = wasm32".into(), "SLINT_TARGET_WASM".into()), // Disable any wasm guarded code in C++, too - so that there are no gaps in enums.
// Disable any wasm guarded code in C++, too - so that there are no gaps in enums.
("target_arch = wasm32".into(), "SLINT_TARGET_WASM".into()),
("target_os = android".into(), "__ANDROID__".into()),
]
.iter()
.cloned()

View file

@ -10,6 +10,7 @@ Use the non-visual `ContextMenuArea` element to declare an area where the user c
The context menu is shown if the user right-clicks on the area covered by the `ContextMenuArea` element,
or if the user presses the "Menu" key on their keyboard while a `FocusScope` within the `ContextMenuArea` has focus.
On Android, the menu is shown with a long press.
Call the `show()` function on the `ContextMenuArea` element to programmatically show the context menu.
One of the children of the `ContextMenuArea` must be a `Menu` element, which defines the menu to be shown.

View file

@ -18,6 +18,7 @@ pub use android_activity::AndroidApp;
use android_activity::PollEvent;
use androidwindowadapter::AndroidWindowAdapter;
use core::ops::ControlFlow;
use core::time::Duration;
use i_slint_core::api::{EventLoopError, PlatformError};
use i_slint_core::platform::{Clipboard, WindowAdapter};
use i_slint_renderer_skia::SkiaRendererExt;
@ -100,7 +101,7 @@ impl i_slint_core::platform::Platform for AndroidPlatform {
let mut timeout = i_slint_core::platform::duration_until_next_timer_update();
if self.window.window.has_active_animations() {
// FIXME: we should not hardcode a value here
let frame_duration = std::time::Duration::from_millis(10);
let frame_duration = Duration::from_millis(10);
timeout = Some(match timeout {
Some(x) => x.min(frame_duration),
None => frame_duration,
@ -152,6 +153,10 @@ impl i_slint_core::platform::Platform for AndroidPlatform {
None
}
}
fn long_press_interval(&self, _: i_slint_core::InternalToken) -> Duration {
self.window.java_helper.long_press_timeout().unwrap_or(Duration::from_millis(500))
}
}
enum Event {

View file

@ -1216,6 +1216,8 @@ pub struct ContextMenu {
pub show: Callback<PointArg>,
pub cached_rendering_data: CachedRenderingData,
pub popup_id: Cell<Option<NonZeroU32>>,
#[cfg(target_os = "android")]
long_press_timer: Cell<Option<crate::timers::Timer>>,
}
impl Item for ContextMenu {
@ -1244,11 +1246,40 @@ impl Item for ContextMenu {
_window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> InputEventResult {
if let MouseEvent::Pressed { position, button: PointerEventButton::Right, .. } = event {
self.show.call(&(crate::api::LogicalPosition::from_euclid(position),));
InputEventResult::EventAccepted
} else {
InputEventResult::EventIgnored
match event {
MouseEvent::Pressed { position, button: PointerEventButton::Right, .. } => {
self.show.call(&(crate::api::LogicalPosition::from_euclid(position),));
InputEventResult::EventAccepted
}
#[cfg(target_os = "android")]
MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
let timer = crate::timers::Timer::default();
let self_weak = _self_rc.downgrade();
timer.start(
crate::timers::TimerMode::SingleShot,
WindowInner::from_pub(_window_adapter.window())
.ctx
.platform()
.long_press_interval(crate::InternalToken),
move || {
let Some(self_rc) = self_weak.upgrade() else { return };
let Some(self_) = self_rc.downcast::<ContextMenu>() else { return };
self_.show.call(&(crate::api::LogicalPosition::from_euclid(position),));
},
);
self.long_press_timer.set(Some(timer));
InputEventResult::GrabMouse
}
#[cfg(target_os = "android")]
MouseEvent::Released { .. } | MouseEvent::Exit => {
if let Some(timer) = self.long_press_timer.take() {
timer.stop();
}
InputEventResult::EventIgnored
}
#[cfg(target_os = "android")]
MouseEvent::Moved { .. } => InputEventResult::EventAccepted,
_ => InputEventResult::EventIgnored,
}
}

View file

@ -126,6 +126,13 @@ pub trait Platform {
fn debug_log(&self, _arguments: core::fmt::Arguments) {
crate::tests::default_debug_log(_arguments);
}
#[cfg(target_os = "android")]
#[doc(hidden)]
/// The long press interval before showing a context menu
fn long_press_interval(&self, _: crate::InternalToken) -> core::time::Duration {
core::time::Duration::from_millis(500)
}
}
/// The clip board, used in [`Platform::clipboard_text`] and [Platform::set_clipboard_text`]