mirror of
https://github.com/flectar/mail.git
synced 2026-11-13 02:24:19 +00:00
fix: let taps reach sidebar rows on Android
This commit is contained in:
1 parent
51c6aed425
commit
55bab3579d
3 files changed
+269
-116
No files matched your search
+27
-31
@@ -3090,64 +3090,60 @@ msgstr ""
|
||||
msgid "{}, {}"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:171
|
||||
msgid "Collapse label"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:171
|
||||
msgid "Expand label"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:172
|
||||
msgid "Collapse folder"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:172
|
||||
msgid "Expand folder"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:187
|
||||
#: ui/components/sidebar-controls.slint:211
|
||||
msgid "New subfolder"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:193 ui/dialogs/folder-dialog.slint:29
|
||||
#: ui/components/sidebar-controls.slint:193 ui/dialogs/folder-dialog.slint:29
|
||||
#: ui/components/sidebar-controls.slint:216 ui/dialogs/folder-dialog.slint:29
|
||||
#: ui/components/sidebar-controls.slint:216 ui/dialogs/folder-dialog.slint:29
|
||||
msgid "Rename folder"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:199
|
||||
#: ui/components/sidebar-controls.slint:221
|
||||
msgid "Delete folder"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:212
|
||||
#: ui/components/sidebar-controls.slint:212
|
||||
#: ui/components/sidebar-controls.slint:226
|
||||
#: ui/components/sidebar-controls.slint:226
|
||||
msgid "Empty Trash…"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:224
|
||||
#: ui/components/sidebar-controls.slint:230
|
||||
msgid "New nested label"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:230
|
||||
#: ui/components/sidebar-controls.slint:230
|
||||
#: ui/components/sidebar-controls.slint:234
|
||||
msgid "Rename global label"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:230
|
||||
#: ui/components/sidebar-controls.slint:230
|
||||
#: ui/components/sidebar-controls.slint:234
|
||||
msgid "Rename label in {}"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:235
|
||||
#: ui/components/sidebar-controls.slint:235
|
||||
#: ui/components/sidebar-controls.slint:238
|
||||
msgid "Delete global label"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:235
|
||||
#: ui/components/sidebar-controls.slint:235
|
||||
#: ui/components/sidebar-controls.slint:238
|
||||
msgid "Delete label from {}"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:264
|
||||
msgid "Collapse label"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:264
|
||||
msgid "Expand label"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:265
|
||||
msgid "Collapse folder"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar-controls.slint:265
|
||||
msgid "Expand folder"
|
||||
msgstr ""
|
||||
|
||||
#: ui/components/sidebar.slint:117 ui/views/mail-list-pane.slint:1430
|
||||
msgid "New email"
|
||||
msgstr ""
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//! Taps, right-clicks and long presses on sidebar rows, which also have menus.
|
||||
use i_slint_core::input::TouchPhase;
|
||||
use i_slint_core::lengths::LogicalPoint;
|
||||
use i_slint_core::platform::InternalEvent;
|
||||
use slint::ComponentHandle;
|
||||
use slint::platform::{
|
||||
Key, Platform, PointerEventButton, WindowAdapter, WindowEvent,
|
||||
software_renderer::{MinimalSoftwareWindow, RepaintBufferType},
|
||||
};
|
||||
use std::{cell::Cell, rc::Rc, time::Duration};
|
||||
|
||||
slint::slint! {
|
||||
import { SidebarItem, SidebarSectionHeader } from "../ui/components/sidebar-controls.slint";
|
||||
|
||||
export component SidebarRowHarness inherits Window {
|
||||
width: 260px;
|
||||
height: 300px;
|
||||
in-out property <string> selected-scope;
|
||||
in-out property <int> folder-toggles;
|
||||
in-out property <int> new-subfolders;
|
||||
in-out property <int> account-toggles;
|
||||
SidebarItem {
|
||||
y: 0px;
|
||||
width: parent.width;
|
||||
label: "Projects";
|
||||
scope: "Folder:5";
|
||||
account-id: 1;
|
||||
folder-id: 5;
|
||||
has-children: true;
|
||||
folder-menu-enabled: true;
|
||||
can-create-child: true;
|
||||
clicked(scope) => { root.selected-scope = scope; }
|
||||
toggled(id, open) => { root.folder-toggles += 1; }
|
||||
new-subfolder(account, folder, name) => { root.new-subfolders += 1; }
|
||||
}
|
||||
SidebarSectionHeader {
|
||||
y: 40px;
|
||||
width: parent.width;
|
||||
label: "Flectar";
|
||||
detail: "alex@flectar.com";
|
||||
toggled(open) => { root.account-toggles += 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Headless {
|
||||
window: Rc<MinimalSoftwareWindow>,
|
||||
clock: Rc<Cell<Duration>>,
|
||||
}
|
||||
|
||||
impl Platform for Headless {
|
||||
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, slint::PlatformError> {
|
||||
Ok(self.window.clone())
|
||||
}
|
||||
|
||||
fn duration_since_start(&self) -> Duration {
|
||||
self.clock.get()
|
||||
}
|
||||
}
|
||||
|
||||
fn click(window: &slint::Window, x: f32, y: f32, button: PointerEventButton) {
|
||||
let position = slint::LogicalPosition::new(x, y);
|
||||
window.dispatch_event(WindowEvent::PointerMoved { position });
|
||||
window.dispatch_event(WindowEvent::PointerPressed { position, button });
|
||||
window.dispatch_event(WindowEvent::PointerReleased { position, button });
|
||||
}
|
||||
|
||||
fn touch(window: &slint::Window, x: f32, y: f32, phase: TouchPhase) {
|
||||
window.dispatch_event(WindowEvent::internal(InternalEvent::Touch {
|
||||
id: 0,
|
||||
position: LogicalPoint::new(x, y),
|
||||
phase,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Runs the open menu's first item, if a menu is open.
|
||||
fn run_first_menu_item(window: &slint::Window) {
|
||||
for key in [Key::DownArrow, Key::Return] {
|
||||
window.dispatch_event(WindowEvent::KeyPressed { text: key.into() });
|
||||
window.dispatch_event(WindowEvent::KeyReleased { text: key.into() });
|
||||
}
|
||||
}
|
||||
|
||||
// Slint's Android build turns a press on a menu's own area into a wait for a
|
||||
// long press, so rows keep their touch area inside it: taps reach the row on
|
||||
// every platform, and the row opens the menu itself.
|
||||
#[test]
|
||||
fn rows_take_taps_and_open_their_menus_on_right_click_or_hold() {
|
||||
let clock = Rc::new(Cell::new(Duration::ZERO));
|
||||
let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer);
|
||||
slint::platform::set_platform(Box::new(Headless {
|
||||
window: window.clone(),
|
||||
clock: clock.clone(),
|
||||
}))
|
||||
.unwrap();
|
||||
let ui = SidebarRowHarness::new().unwrap();
|
||||
ui.show().unwrap();
|
||||
let advance = |ms: u64| {
|
||||
clock.set(clock.get() + Duration::from_millis(ms));
|
||||
slint::platform::update_timers_and_animations();
|
||||
};
|
||||
|
||||
click(ui.window(), 10.0, 14.0, PointerEventButton::Left);
|
||||
assert_eq!(
|
||||
ui.get_folder_toggles(),
|
||||
1,
|
||||
"the disclosure folds the folder"
|
||||
);
|
||||
click(ui.window(), 150.0, 14.0, PointerEventButton::Left);
|
||||
assert_eq!(ui.get_selected_scope(), "Folder:5", "a click selects it");
|
||||
click(ui.window(), 150.0, 60.0, PointerEventButton::Left);
|
||||
assert_eq!(ui.get_account_toggles(), 1, "a click folds the account");
|
||||
|
||||
ui.set_selected_scope("".into());
|
||||
touch(ui.window(), 150.0, 14.0, TouchPhase::Started);
|
||||
advance(100);
|
||||
touch(ui.window(), 150.0, 14.0, TouchPhase::Ended);
|
||||
advance(600);
|
||||
assert_eq!(ui.get_selected_scope(), "Folder:5", "a tap selects it");
|
||||
run_first_menu_item(ui.window());
|
||||
assert_eq!(ui.get_new_subfolders(), 0, "a tap opens no menu");
|
||||
|
||||
// Holding a finger still for half a second opens the menu, as on Android
|
||||
// and iOS, and lifting it afterwards is not a tap.
|
||||
ui.set_selected_scope("".into());
|
||||
touch(ui.window(), 150.0, 14.0, TouchPhase::Started);
|
||||
advance(600);
|
||||
touch(ui.window(), 150.0, 14.0, TouchPhase::Ended);
|
||||
assert_eq!(ui.get_selected_scope(), "");
|
||||
run_first_menu_item(ui.window());
|
||||
assert_eq!(ui.get_new_subfolders(), 1, "the hold opened the menu");
|
||||
|
||||
// A finger that moves is scrolling the sidebar.
|
||||
touch(ui.window(), 150.0, 14.0, TouchPhase::Started);
|
||||
advance(100);
|
||||
touch(ui.window(), 150.0, 30.0, TouchPhase::Moved);
|
||||
advance(600);
|
||||
touch(ui.window(), 150.0, 30.0, TouchPhase::Ended);
|
||||
run_first_menu_item(ui.window());
|
||||
assert_eq!(ui.get_new_subfolders(), 1, "scrolling opens no menu");
|
||||
}
|
||||
@@ -50,6 +50,57 @@ component SidebarCountBadge inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// A row's pointer area, which sits inside the row's ContextMenuArea so taps
|
||||
// reach it first: on Android, Slint holds every press on a menu's own area to
|
||||
// wait for a long press, which swallowed taps on the rows above it. A
|
||||
// right-click opens the menu, and so does holding a finger still for half a
|
||||
// second, as on Android and iOS.
|
||||
component SidebarRowHit inherits TouchArea {
|
||||
in property <bool> menu-enabled;
|
||||
callback activated();
|
||||
callback open-menu(Point);
|
||||
private property <bool> holding: false;
|
||||
private property <bool> held: false;
|
||||
private property <length> hold-x;
|
||||
private property <length> hold-y;
|
||||
|
||||
pointer-event(event) => {
|
||||
if event.kind == PointerEventKind.down {
|
||||
self.held = false;
|
||||
if event.button == PointerEventButton.right && root.menu-enabled {
|
||||
root.open-menu({ x: self.mouse-x, y: self.mouse-y });
|
||||
} else if event.button == PointerEventButton.left && event.touch-finger-id != 0 && root.menu-enabled {
|
||||
self.hold-x = self.mouse-x;
|
||||
self.hold-y = self.mouse-y;
|
||||
self.holding = true;
|
||||
}
|
||||
} else if event.kind == PointerEventKind.move {
|
||||
if self.holding && (abs(self.mouse-x - self.hold-x) > 8px || abs(self.mouse-y - self.hold-y) > 8px) {
|
||||
self.holding = false;
|
||||
}
|
||||
} else {
|
||||
self.holding = false;
|
||||
}
|
||||
}
|
||||
clicked => {
|
||||
// The finger that opened the menu lifts here; it was not a tap.
|
||||
if !self.held {
|
||||
root.activated();
|
||||
}
|
||||
self.held = false;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 500ms;
|
||||
running: root.holding;
|
||||
triggered => {
|
||||
root.holding = false;
|
||||
root.held = true;
|
||||
root.open-menu({ x: root.hold-x, y: root.hold-y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A folder row is shared by unified and account sections so indentation,
|
||||
// selection, hover, and counts remain consistent throughout the tree.
|
||||
export component SidebarItem inherits SidebarRowSurface {
|
||||
@@ -150,14 +201,56 @@ export component SidebarItem inherits SidebarRowSurface {
|
||||
muted-color: root.muted-color;
|
||||
}
|
||||
|
||||
row-hit := TouchArea {
|
||||
enabled: root.interactive;
|
||||
accessible-role: button;
|
||||
accessible-label: root.accessible-name;
|
||||
accessible-checked: root.selected;
|
||||
accessible-action-default => { root.clicked(root.scope); }
|
||||
clicked => { root.clicked(root.scope); }
|
||||
mouse-cursor: pointer;
|
||||
// Folder and label actions. Rows without any keep the area disabled.
|
||||
row-menu := ContextMenuArea {
|
||||
enabled: root.interactive && (root.folder-menu-enabled || root.can-empty-trash || root.label-manageable);
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
Menu {
|
||||
if root.folder-menu-enabled: MenuItem {
|
||||
title: @tr("New subfolder");
|
||||
enabled: root.can-create-child;
|
||||
activated => { root.new-subfolder(root.account-id, root.folder-id, root.label); }
|
||||
}
|
||||
if root.folder-menu-enabled: MenuItem {
|
||||
title: @tr("Rename folder");
|
||||
enabled: root.can-rename;
|
||||
activated => { root.rename-folder(root.folder-id, root.label); }
|
||||
}
|
||||
if root.folder-menu-enabled: MenuItem {
|
||||
title: @tr("Delete folder");
|
||||
enabled: root.can-delete;
|
||||
activated => { root.delete-folder(root.folder-id, root.label); }
|
||||
}
|
||||
if root.can-empty-trash: MenuItem {
|
||||
title: @tr("Empty Trash…");
|
||||
activated => { root.empty-trash(root.account-id); }
|
||||
}
|
||||
if root.label-manageable && root.label-can-create-child: MenuItem {
|
||||
title: @tr("New nested label");
|
||||
activated => { root.new-child-label(root.account-id, root.label-id, root.label-path, root.label-account-name); }
|
||||
}
|
||||
if root.label-manageable: MenuItem {
|
||||
title: root.label-is-global ? @tr("Rename global label") : @tr("Rename label in {}", root.label-account-name);
|
||||
activated => { root.rename-label(root.label-id, root.label-path, root.editable-color, root.label-account-name, root.label-is-global); }
|
||||
}
|
||||
if root.label-manageable: MenuItem {
|
||||
title: root.label-is-global ? @tr("Delete global label") : @tr("Delete label from {}", root.label-account-name);
|
||||
activated => { root.delete-label(root.label-id, root.label-path, root.label-account-name, root.label-is-global, root.has-children); }
|
||||
}
|
||||
}
|
||||
|
||||
row-hit := SidebarRowHit {
|
||||
enabled: root.interactive;
|
||||
menu-enabled: row-menu.enabled;
|
||||
accessible-role: button;
|
||||
accessible-label: root.accessible-name;
|
||||
accessible-checked: root.selected;
|
||||
accessible-action-default => { root.clicked(root.scope); }
|
||||
activated => { root.clicked(root.scope); }
|
||||
open-menu(position) => { row-menu.show(position); }
|
||||
mouse-cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
if root.has-children: TouchArea {
|
||||
@@ -178,83 +271,6 @@ export component SidebarItem inherits SidebarRowSurface {
|
||||
mouse-cursor: pointer;
|
||||
}
|
||||
|
||||
if root.interactive && root.folder-menu-enabled: ContextMenuArea {
|
||||
x: 22px;
|
||||
width: parent.width - 22px;
|
||||
height: parent.height;
|
||||
Menu {
|
||||
MenuItem {
|
||||
title: @tr("New subfolder");
|
||||
enabled: root.can-create-child;
|
||||
activated => { root.new-subfolder(root.account-id, root.folder-id, root.label); }
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
title: @tr("Rename folder");
|
||||
enabled: root.can-rename;
|
||||
activated => { root.rename-folder(root.folder-id, root.label); }
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
title: @tr("Delete folder");
|
||||
enabled: root.can-delete;
|
||||
activated => { root.delete-folder(root.folder-id, root.label); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root.interactive && root.can-empty-trash: ContextMenuArea {
|
||||
x: 22px;
|
||||
width: parent.width - 22px;
|
||||
height: parent.height;
|
||||
Menu {
|
||||
MenuItem {
|
||||
title: @tr("Empty Trash…");
|
||||
activated => { root.empty-trash(root.account-id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root.interactive && root.label-manageable && root.label-can-create-child: ContextMenuArea {
|
||||
x: 22px;
|
||||
width: parent.width - 22px;
|
||||
height: parent.height;
|
||||
Menu {
|
||||
MenuItem {
|
||||
title: @tr("New nested label");
|
||||
enabled: root.label-can-create-child;
|
||||
activated => { root.new-child-label(root.account-id, root.label-id, root.label-path, root.label-account-name); }
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
title: root.label-is-global ? @tr("Rename global label") : @tr("Rename label in {}", root.label-account-name);
|
||||
activated => { root.rename-label(root.label-id, root.label-path, root.editable-color, root.label-account-name, root.label-is-global); }
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
title: root.label-is-global ? @tr("Delete global label") : @tr("Delete label from {}", root.label-account-name);
|
||||
activated => { root.delete-label(root.label-id, root.label-path, root.label-account-name, root.label-is-global, root.has-children); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root.interactive && root.label-manageable && !root.label-can-create-child: ContextMenuArea {
|
||||
x: 22px;
|
||||
width: parent.width - 22px;
|
||||
height: parent.height;
|
||||
Menu {
|
||||
MenuItem {
|
||||
title: root.label-is-global ? @tr("Rename global label") : @tr("Rename label in {}", root.label-account-name);
|
||||
activated => { root.rename-label(root.label-id, root.label-path, root.editable-color, root.label-account-name, root.label-is-global); }
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
title: root.label-is-global ? @tr("Delete global label") : @tr("Delete label from {}", root.label-account-name);
|
||||
activated => { root.delete-label(root.label-id, root.label-path, root.label-account-name, root.label-is-global, root.has-children); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mail-drop-target := DropArea {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
|
||||
Reference in new issue
Block a user