1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-22 22:38:08 +00:00

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

[no changelog]
This commit is contained in:
grdddj 2023-09-20 15:40:11 +02:00 committed by Jiří Musil
parent ed9fd35018
commit 65c9380ab7
10 changed files with 242 additions and 36 deletions

View File

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

View File

@ -39,9 +39,13 @@ enum ButtonState {
} }
pub enum ButtonControllerMsg { pub enum ButtonControllerMsg {
/// Button was pressed down.
Pressed(ButtonPos), 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), 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. /// Defines what kind of button should be currently used.
@ -109,8 +113,13 @@ where
/// Holds the timestamp of when the button was pressed. /// Holds the timestamp of when the button was pressed.
pressed_since: Option<Instant>, pressed_since: Option<Instant>,
/// How long the button should be pressed to send `long_press=true` in /// How long the button should be pressed to send `long_press=true` in
/// `Triggered` /// `ButtonControllerMsg::Triggered`
long_press_ms: u32, 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> impl<T> ButtonContainer<T>
@ -121,11 +130,16 @@ where
/// (it can be later activated in `set()`). /// (it can be later activated in `set()`).
pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self { pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
const DEFAULT_LONG_PRESS_MS: u32 = 1000; const DEFAULT_LONG_PRESS_MS: u32 = 1000;
let send_long_press = btn_details
.as_ref()
.map_or(false, |btn| btn.send_long_press);
Self { Self {
pos, pos,
button_type: ButtonType::from_button_details(pos, btn_details), button_type: ButtonType::from_button_details(pos, btn_details),
pressed_since: None, pressed_since: None,
long_press_ms: DEFAULT_LONG_PRESS_MS, 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. /// Passing `None` as `btn_details` will mark the button as inactive.
pub fn set(&mut self, btn_details: Option<ButtonDetails<T>>, button_area: Rect) { 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 = ButtonType::from_button_details(self.pos, btn_details);
self.button_type.place(button_area); self.button_type.place(button_area);
} }
@ -166,6 +183,7 @@ where
Instant::now().saturating_duration_since(since).to_millis() > self.long_press_ms Instant::now().saturating_duration_since(since).to_millis() > self.long_press_ms
}); });
self.pressed_since = None; self.pressed_since = None;
self.long_pressed_timer = None;
Some(ButtonControllerMsg::Triggered(self.pos, long_press)) Some(ButtonControllerMsg::Triggered(self.pos, long_press))
} }
_ => { _ => {
@ -186,8 +204,24 @@ where
} }
/// Saving the timestamp of when the button was pressed. /// 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()); 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. /// Registering hold event.
@ -316,6 +350,51 @@ where
} }
None 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> impl<T> Component for ButtonController<T>
@ -346,13 +425,13 @@ where
match which { match which {
// ▼ * // ▼ *
PhysicalButton::Left => { PhysicalButton::Left => {
self.left_btn.got_pressed(); self.got_pressed(ctx, ButtonPos::Left);
self.left_btn.hold_started(ctx); self.left_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Left)) Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
} }
// * ▼ // * ▼
PhysicalButton::Right => { PhysicalButton::Right => {
self.right_btn.got_pressed(); self.got_pressed(ctx, ButtonPos::Right);
self.right_btn.hold_started(ctx); self.right_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Right)) Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
} }
@ -392,7 +471,7 @@ where
return None; return None;
} }
} }
self.middle_btn.got_pressed(); self.got_pressed(ctx, ButtonPos::Middle);
self.middle_hold_started(ctx); self.middle_hold_started(ctx);
( (
// ↓ ↓ // ↓ ↓
@ -471,6 +550,9 @@ where
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.handle_timer_token(token); 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) self.handle_htc_expiration(ctx, event)
} }
_ => None, _ => None,
@ -568,6 +650,13 @@ impl IgnoreButtonDelay {
self.right_clickable_timer = None; 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 /// Component allowing for automatically moving through items (e.g. Choice

View File

@ -29,6 +29,12 @@ pub trait Choice<T: StringType> {
fn btn_layout(&self) -> ButtonLayout<T> { fn btn_layout(&self) -> ButtonLayout<T> {
ButtonLayout::default_three_icons() 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 /// Interface for a specific component efficiently giving
@ -130,7 +136,7 @@ where
/// Need to update the initial button layout. /// Need to update the initial button layout.
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self { pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
self.page_counter = page_counter; 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( self.buttons = Child::new(
ButtonController::new(initial_btn_layout) ButtonController::new(initial_btn_layout)
.with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS), .with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS),
@ -235,7 +241,7 @@ where
} }
// Getting the remaining left and right areas. // 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); let (left_area, _center_area, right_area) = center_row_area.split_center(center_width);
// Possibly drawing on the left side. // Possibly drawing on the left side.
@ -277,14 +283,23 @@ where
} }
/// Getting the choice on the current index /// 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) 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. /// Display the current choice in the middle.
fn show_current_choice(&mut self, area: Rect) { fn show_current_choice(&mut self, area: Rect) {
self.get_current_choice() self.get_current_item()
.0
.paint_center(area, self.inverse_selected_item); .paint_center(area, self.inverse_selected_item);
// Color inversion is just one-time thing. // Color inversion is just one-time thing.
@ -406,7 +421,7 @@ where
/// If defined in the current choice, setting their text, /// If defined in the current choice, setting their text,
/// whether they are long-pressed, and painting them. /// whether they are long-pressed, and painting them.
fn set_buttons(&mut self, ctx: &mut EventCtx) { 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| { self.buttons.mutate(ctx, |ctx, buttons| {
buttons.set(btn_layout); buttons.set(btn_layout);
// When user holds one of the buttons, highlighting it. // When user holds one of the buttons, highlighting it.
@ -549,10 +564,22 @@ where
ButtonPos::Middle => { ButtonPos::Middle => {
// Clicked SELECT. Send current choice index with information about long-press // Clicked SELECT. Send current choice index with information about long-press
self.clear_and_repaint(ctx); 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 // The middle button was pressed, highlighting the current choice by color
// inversion. // inversion.
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event { if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {

View File

@ -19,6 +19,7 @@ pub struct ChoiceItem<T: StringType> {
icon: Option<Icon>, icon: Option<Icon>,
btn_layout: ButtonLayout<T>, btn_layout: ButtonLayout<T>,
font: Font, font: Font,
middle_action_without_release: bool,
} }
impl<T: StringType> ChoiceItem<T> { impl<T: StringType> ChoiceItem<T> {
@ -28,6 +29,7 @@ impl<T: StringType> ChoiceItem<T> {
icon: None, icon: None,
btn_layout, btn_layout,
font: theme::FONT_CHOICE_ITEMS, font: theme::FONT_CHOICE_ITEMS,
middle_action_without_release: false,
} }
} }
@ -43,6 +45,15 @@ impl<T: StringType> ChoiceItem<T> {
self 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. /// Setting left button.
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) { pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) {
self.btn_layout.btn_left = btn_left; self.btn_layout.btn_left = btn_left;
@ -117,6 +128,11 @@ where
fn btn_layout(&self) -> ButtonLayout<T> { fn btn_layout(&self) -> ButtonLayout<T> {
self.btn_layout.clone() 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) { fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -169,6 +169,18 @@ def _delete_pin(debug: "DebugLink", digits_to_delete: int, check: bool = True) -
assert before[:-digits_to_delete] == after 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: def _cancel_pin(debug: "DebugLink") -> None:
"""Navigate to "CANCEL" and press it""" """Navigate to "CANCEL" and press it"""
# It is the same button as DELETE # 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:]) _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]) @pytest.mark.setup_client(pin=PIN60[:50])
def test_pin_longer_than_max(device_handler: "BackgroundDeviceHandler"): def test_pin_longer_than_max(device_handler: "BackgroundDeviceHandler"):
with prepare(device_handler) as debug: with prepare(device_handler) as debug: