feat(core): triggering delete action in ChoicePage after 1 second even without release

[no changelog]
pull/3295/head
grdddj 8 months ago committed by Jiří Musil
parent ed9fd35018
commit 65c9380ab7

@ -349,6 +349,7 @@ pub struct ButtonDetails<T> {
with_arms: bool,
fixed_width: Option<i16>,
offset: Offset,
pub send_long_press: bool,
}
impl<T> ButtonDetails<T>
@ -364,6 +365,7 @@ where
with_arms: false,
fixed_width: None,
offset: Offset::zero(),
send_long_press: false,
}
}
@ -376,6 +378,7 @@ where
with_arms: false,
fixed_width: None,
offset: Offset::zero(),
send_long_press: false,
}
}

@ -39,9 +39,13 @@ enum ButtonState {
}
pub enum ButtonControllerMsg {
/// Button was pressed down.
Pressed(ButtonPos),
/// Which button was triggered, and whether it was pressed for a longer time
/// Which button was triggered, and whether it was pressed for a longer
/// time before releasing.
Triggered(ButtonPos, bool),
/// Button was pressed and held for longer time (not released yet).
LongPressed(ButtonPos),
}
/// Defines what kind of button should be currently used.
@ -109,8 +113,13 @@ where
/// Holds the timestamp of when the button was pressed.
pressed_since: Option<Instant>,
/// How long the button should be pressed to send `long_press=true` in
/// `Triggered`
/// `ButtonControllerMsg::Triggered`
long_press_ms: u32,
/// Timer for sending `ButtonControllerMsg::LongPressed`
long_pressed_timer: Option<TimerToken>,
/// Whether it should even send `ButtonControllerMsg::LongPressed` events
/// (optional)
send_long_press: bool,
}
impl<T> ButtonContainer<T>
@ -121,11 +130,16 @@ where
/// (it can be later activated in `set()`).
pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
const DEFAULT_LONG_PRESS_MS: u32 = 1000;
let send_long_press = btn_details
.as_ref()
.map_or(false, |btn| btn.send_long_press);
Self {
pos,
button_type: ButtonType::from_button_details(pos, btn_details),
pressed_since: None,
long_press_ms: DEFAULT_LONG_PRESS_MS,
long_pressed_timer: None,
send_long_press,
}
}
@ -133,6 +147,9 @@ where
///
/// Passing `None` as `btn_details` will mark the button as inactive.
pub fn set(&mut self, btn_details: Option<ButtonDetails<T>>, button_area: Rect) {
self.send_long_press = btn_details
.as_ref()
.map_or(false, |btn| btn.send_long_press);
self.button_type = ButtonType::from_button_details(self.pos, btn_details);
self.button_type.place(button_area);
}
@ -166,6 +183,7 @@ where
Instant::now().saturating_duration_since(since).to_millis() > self.long_press_ms
});
self.pressed_since = None;
self.long_pressed_timer = None;
Some(ButtonControllerMsg::Triggered(self.pos, long_press))
}
_ => {
@ -186,8 +204,24 @@ where
}
/// Saving the timestamp of when the button was pressed.
pub fn got_pressed(&mut self) {
/// Also requesting a timer for long-press if wanted.
pub fn got_pressed(&mut self, ctx: &mut EventCtx) {
self.pressed_since = Some(Instant::now());
if self.send_long_press {
self.long_pressed_timer =
Some(ctx.request_timer(Duration::from_millis(self.long_press_ms)));
}
}
/// Reset the pressed information.
pub fn reset(&mut self) {
self.pressed_since = None;
self.long_pressed_timer = None;
}
/// Whether token matches what we have
pub fn is_timer_token(&self, token: TimerToken) -> bool {
self.long_pressed_timer == Some(token)
}
/// Registering hold event.
@ -316,6 +350,51 @@ where
}
None
}
fn reset_button_presses(&mut self) {
self.left_btn.reset();
self.middle_btn.reset();
self.right_btn.reset();
}
fn got_pressed(&mut self, ctx: &mut EventCtx, pos: ButtonPos) {
// Only one (virtual) button can be pressed at the same time
self.reset_button_presses();
match pos {
ButtonPos::Left => {
self.left_btn.got_pressed(ctx);
}
ButtonPos::Middle => {
self.middle_btn.got_pressed(ctx);
}
ButtonPos::Right => {
self.right_btn.got_pressed(ctx);
}
}
}
fn handle_long_press_timer_token(&mut self, token: TimerToken) -> Option<ButtonPos> {
if self.left_btn.is_timer_token(token) {
return Some(ButtonPos::Left);
}
if self.middle_btn.is_timer_token(token) {
return Some(ButtonPos::Middle);
}
if self.right_btn.is_timer_token(token) {
return Some(ButtonPos::Right);
}
None
}
/// Resetting the state of the controller.
pub fn reset_state(&mut self, ctx: &mut EventCtx) {
self.state = ButtonState::Nothing;
self.reset_button_presses();
self.set_pressed(ctx, false, false, false);
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.reset();
}
}
}
impl<T> Component for ButtonController<T>
@ -346,13 +425,13 @@ where
match which {
// ▼ *
PhysicalButton::Left => {
self.left_btn.got_pressed();
self.got_pressed(ctx, ButtonPos::Left);
self.left_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
}
// * ▼
PhysicalButton::Right => {
self.right_btn.got_pressed();
self.got_pressed(ctx, ButtonPos::Right);
self.right_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
}
@ -392,7 +471,7 @@ where
return None;
}
}
self.middle_btn.got_pressed();
self.got_pressed(ctx, ButtonPos::Middle);
self.middle_hold_started(ctx);
(
// ↓ ↓
@ -471,6 +550,9 @@ where
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.handle_timer_token(token);
}
if let Some(pos) = self.handle_long_press_timer_token(token) {
return Some(ButtonControllerMsg::LongPressed(pos));
}
self.handle_htc_expiration(ctx, event)
}
_ => None,
@ -568,6 +650,13 @@ impl IgnoreButtonDelay {
self.right_clickable_timer = None;
}
}
pub fn reset(&mut self) {
self.left_clickable = true;
self.right_clickable = true;
self.left_clickable_timer = None;
self.right_clickable_timer = None;
}
}
/// Component allowing for automatically moving through items (e.g. Choice

@ -29,6 +29,12 @@ pub trait Choice<T: StringType> {
fn btn_layout(&self) -> ButtonLayout<T> {
ButtonLayout::default_three_icons()
}
/// Whether it is possible to do the middle action event without
/// releasing the button - after long-press duration is reached.
fn trigger_middle_without_release(&self) -> bool {
false
}
}
/// Interface for a specific component efficiently giving
@ -130,7 +136,7 @@ where
/// Need to update the initial button layout.
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
self.page_counter = page_counter;
let initial_btn_layout = self.get_current_choice().0.btn_layout();
let initial_btn_layout = self.get_current_item().btn_layout();
self.buttons = Child::new(
ButtonController::new(initial_btn_layout)
.with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS),
@ -235,7 +241,7 @@ where
}
// Getting the remaining left and right areas.
let center_width = self.get_current_choice().0.width_center();
let center_width = self.get_current_item().width_center();
let (left_area, _center_area, right_area) = center_row_area.split_center(center_width);
// Possibly drawing on the left side.
@ -277,14 +283,23 @@ where
}
/// Getting the choice on the current index
pub fn get_current_choice(&self) -> (<F as ChoiceFactory<T>>::Item, A) {
fn get_current_choice(&self) -> (<F as ChoiceFactory<T>>::Item, A) {
self.choices.get(self.page_counter)
}
/// Getting the current item
pub fn get_current_item(&self) -> <F as ChoiceFactory<T>>::Item {
self.get_current_choice().0
}
/// Getting the current action
pub fn get_current_action(&self) -> A {
self.get_current_choice().1
}
/// Display the current choice in the middle.
fn show_current_choice(&mut self, area: Rect) {
self.get_current_choice()
.0
self.get_current_item()
.paint_center(area, self.inverse_selected_item);
// Color inversion is just one-time thing.
@ -406,7 +421,7 @@ where
/// If defined in the current choice, setting their text,
/// whether they are long-pressed, and painting them.
fn set_buttons(&mut self, ctx: &mut EventCtx) {
let btn_layout = self.get_current_choice().0.btn_layout();
let btn_layout = self.get_current_item().btn_layout();
self.buttons.mutate(ctx, |ctx, buttons| {
buttons.set(btn_layout);
// When user holds one of the buttons, highlighting it.
@ -549,10 +564,22 @@ where
ButtonPos::Middle => {
// Clicked SELECT. Send current choice index with information about long-press
self.clear_and_repaint(ctx);
return Some((self.get_current_choice().1, long_press));
return Some((self.get_current_action(), long_press));
}
}
};
// The middle button was pressed for longer time - sending the Event with long
// press. Also resetting the functional and visual state of the buttons.
// Only doing this when the item is configured to do so
if let Some(ButtonControllerMsg::LongPressed(ButtonPos::Middle)) = button_event {
if self.get_current_item().trigger_middle_without_release() {
self.buttons.mutate(ctx, |ctx, buttons| {
buttons.reset_state(ctx);
});
self.clear_and_repaint(ctx);
return Some((self.get_current_action(), true));
}
};
// The middle button was pressed, highlighting the current choice by color
// inversion.
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {

@ -19,6 +19,7 @@ pub struct ChoiceItem<T: StringType> {
icon: Option<Icon>,
btn_layout: ButtonLayout<T>,
font: Font,
middle_action_without_release: bool,
}
impl<T: StringType> ChoiceItem<T> {
@ -28,6 +29,7 @@ impl<T: StringType> ChoiceItem<T> {
icon: None,
btn_layout,
font: theme::FONT_CHOICE_ITEMS,
middle_action_without_release: false,
}
}
@ -43,6 +45,15 @@ impl<T: StringType> ChoiceItem<T> {
self
}
/// Allows for middle action without release.
pub fn with_middle_action_without_release(mut self) -> Self {
self.middle_action_without_release = true;
if let Some(middle) = self.btn_layout.btn_middle.as_mut() {
middle.send_long_press = true;
}
self
}
/// Setting left button.
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) {
self.btn_layout.btn_left = btn_left;
@ -117,6 +128,11 @@ where
fn btn_layout(&self) -> ButtonLayout<T> {
self.btn_layout.clone()
}
/// Whether to do middle action without release
fn trigger_middle_without_release(&self) -> bool {
self.middle_action_without_release
}
}
fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) {

@ -43,50 +43,63 @@ const DIGITS_INDEX: usize = 5;
const SPECIAL_INDEX: usize = 6;
const SPACE_INDEX: usize = 7;
// Menu text, action, icon data, middle button with CONFIRM
const MENU: [(&str, PassphraseAction, Option<Icon>, bool); MENU_LENGTH] = [
("SHOW", PassphraseAction::Show, Some(theme::ICON_EYE), true),
/// Menu text, action, icon data, middle button with CONFIRM, without_release
const MENU: [(&str, PassphraseAction, Option<Icon>, bool, bool); MENU_LENGTH] = [
(
"SHOW",
PassphraseAction::Show,
Some(theme::ICON_EYE),
true,
false,
),
(
"CANCEL_OR_DELETE", // will be chosen dynamically
PassphraseAction::CancelOrDelete,
None,
true,
true, // without_release
),
(
"ENTER",
PassphraseAction::Enter,
Some(theme::ICON_TICK),
true,
false,
),
(
"abc",
PassphraseAction::Category(ChoiceCategory::LowercaseLetter),
None,
false,
false,
),
(
"ABC",
PassphraseAction::Category(ChoiceCategory::UppercaseLetter),
None,
false,
false,
),
(
"123",
PassphraseAction::Category(ChoiceCategory::Digit),
None,
false,
false,
),
(
"#$!",
PassphraseAction::Category(ChoiceCategory::SpecialSymbol),
None,
false,
false,
),
(
"SPACE",
PassphraseAction::Character(' '),
Some(theme::ICON_SPACE),
false,
false,
),
];
@ -164,7 +177,7 @@ impl ChoiceFactoryPassphrase {
choice_index: usize,
) -> (ChoiceItem<T>, PassphraseAction) {
// More options for CANCEL/DELETE button
let (mut text, action, mut icon, show_confirm) = MENU[choice_index];
let (mut text, action, mut icon, show_confirm, without_release) = MENU[choice_index];
if matches!(action, PassphraseAction::CancelOrDelete) {
if self.is_empty {
text = "CANCEL";
@ -183,6 +196,11 @@ impl ChoiceFactoryPassphrase {
menu_item.set_middle_btn(Some(confirm_btn));
}
// Making middle button create LongPress events
if without_release {
menu_item = menu_item.with_middle_action_without_release();
}
if let Some(icon) = icon {
menu_item = menu_item.with_icon(icon);
}

@ -27,20 +27,22 @@ const EMPTY_PIN_STR: &str = "_";
const CHOICE_LENGTH: usize = 13;
const NUMBER_START_INDEX: usize = 3;
const CHOICES: [(&str, PinAction, Option<Icon>); CHOICE_LENGTH] = [
("DELETE", PinAction::Delete, Some(theme::ICON_DELETE)),
("SHOW", PinAction::Show, Some(theme::ICON_EYE)),
("ENTER", PinAction::Enter, Some(theme::ICON_TICK)),
("0", PinAction::Digit('0'), None),
("1", PinAction::Digit('1'), None),
("2", PinAction::Digit('2'), None),
("3", PinAction::Digit('3'), None),
("4", PinAction::Digit('4'), None),
("5", PinAction::Digit('5'), None),
("6", PinAction::Digit('6'), None),
("7", PinAction::Digit('7'), None),
("8", PinAction::Digit('8'), None),
("9", PinAction::Digit('9'), None),
/// Text, action, icon, without_release
const CHOICES: [(&str, PinAction, Option<Icon>, bool); CHOICE_LENGTH] = [
// DELETE should be triggerable without release (after long-press)
("DELETE", PinAction::Delete, Some(theme::ICON_DELETE), true),
("SHOW", PinAction::Show, Some(theme::ICON_EYE), false),
("ENTER", PinAction::Enter, Some(theme::ICON_TICK), false),
("0", PinAction::Digit('0'), None, false),
("1", PinAction::Digit('1'), None, false),
("2", PinAction::Digit('2'), None, false),
("3", PinAction::Digit('3'), None, false),
("4", PinAction::Digit('4'), None, false),
("5", PinAction::Digit('5'), None, false),
("6", PinAction::Digit('6'), None, false),
("7", PinAction::Digit('7'), None, false),
("8", PinAction::Digit('8'), None, false),
("9", PinAction::Digit('9'), None, false),
];
fn get_random_digit_position() -> usize {
@ -54,7 +56,7 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPIN {
type Item = ChoiceItem<T>;
fn get(&self, choice_index: usize) -> (Self::Item, Self::Action) {
let (choice_str, action, icon) = CHOICES[choice_index];
let (choice_str, action, icon, without_release) = CHOICES[choice_index];
let mut choice_item = ChoiceItem::new(choice_str, ButtonLayout::default_three_icons());
@ -64,6 +66,11 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPIN {
choice_item.set_middle_btn(Some(confirm_btn));
}
// Making middle button create LongPress events
if without_release {
choice_item = choice_item.with_middle_action_without_release();
}
// Adding icons for appropriate items
if let Some(icon) = icon {
choice_item = choice_item.with_icon(icon);

@ -95,7 +95,8 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryWordlist {
if choice_index == DELETE_INDEX {
return (
ChoiceItem::new("DELETE", ButtonLayout::arrow_armed_arrow("CONFIRM".into()))
.with_icon(theme::ICON_DELETE),
.with_icon(theme::ICON_DELETE)
.with_middle_action_without_release(),
WordlistAction::Delete,
);
}

@ -649,6 +649,15 @@ class DebugLink:
physical_button=messages.DebugPhysicalButton.MIDDLE_BTN, wait=wait
)
def press_middle_htc(
self, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
return self.press_htc(
button=messages.DebugPhysicalButton.MIDDLE_BTN,
hold_ms=hold_ms,
extra_ms=extra_ms,
)
@overload
def press_right(self) -> None:
...
@ -664,10 +673,19 @@ class DebugLink:
def press_right_htc(
self, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
return self.press_htc(
button=messages.DebugPhysicalButton.RIGHT_BTN,
hold_ms=hold_ms,
extra_ms=extra_ms,
)
def press_htc(
self, button: messages.DebugPhysicalButton, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
hold_ms = hold_ms + extra_ms # safety margin
result = self.input(
physical_button=messages.DebugPhysicalButton.RIGHT_BTN,
physical_button=button,
hold_ms=hold_ms,
)
# sleeping little longer for UI to update

@ -70,6 +70,7 @@ def navigate_to_action_and_press(
wanted_action: str,
all_actions: list[str],
is_carousel: bool = True,
hold_ms: int = 0,
) -> None:
"""Navigate to the button with certain action and press it"""
# Orient
@ -99,8 +100,11 @@ def navigate_to_action_and_press(
is_carousel=is_carousel,
)
# Press
debug.press_middle(wait=True)
# Press or hold
if hold_ms:
debug.press_middle_htc(1000)
else:
debug.press_middle(wait=True)
def _get_action_index(wanted_action: str, all_actions: list[str]) -> int:

@ -169,6 +169,18 @@ def _delete_pin(debug: "DebugLink", digits_to_delete: int, check: bool = True) -
assert before[:-digits_to_delete] == after
def _delete_all(debug: "DebugLink", check: bool = True) -> None:
"""Navigate to "DELETE" and hold it until all digits are deleted"""
if debug.model == "T":
debug.click_hold(buttons.pin_passphrase_grid(9), hold_ms=1500)
elif debug.model == "R":
navigate_to_action_and_press(debug, "DELETE", TR_PIN_ACTIONS, hold_ms=1000)
if check:
after = debug.read_layout().pin()
assert after == ""
def _cancel_pin(debug: "DebugLink") -> None:
"""Navigate to "CANCEL" and press it"""
# It is the same button as DELETE
@ -224,6 +236,17 @@ def test_pin_long_delete(device_handler: "BackgroundDeviceHandler"):
_input_see_confirm(debug, PIN24[-10:])
@pytest.mark.setup_client(pin=PIN4)
def test_pin_delete_hold(device_handler: "BackgroundDeviceHandler"):
with prepare(device_handler) as debug:
_input_pin(debug, PIN4)
_see_pin(debug)
_delete_all(debug)
_input_see_confirm(debug, PIN4)
@pytest.mark.setup_client(pin=PIN60[:50])
def test_pin_longer_than_max(device_handler: "BackgroundDeviceHandler"):
with prepare(device_handler) as debug:

Loading…
Cancel
Save