diff --git a/CHANGELOG.md b/CHANGELOG.md index 387b26141..490f0f7cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,21 @@ Krux now displays a warning instead of blocking QR-encoded passphrases that cont ### Easier to Scan UR QR Codes Exported Uniform Resource (UR) QR codes, a widely adopted standard for exchanging PSBTs, now use uppercase data to reduce QR density, improving scan reliability without increasing the number of frames. +### Improved UI +- Added new context arrows, customizable colors, touch-feedback highlighting and a page index for menu navigation. +- Refined keypad visuals with a clearer keyset index and touch-feedback highlighting. +- Added touch-feedback highlighting to confirmation prompts, Category Settings, Stackbit 1248, Tinyseed and Mnemonic editor. + ### Other Bug Fixes and Improvements - Settings: Reduced default _Buttons Debounce_ value (with an even lower default on _M5StickV_) - Settings: Expanded value ranges for _Touch Threshold_ and _Buttons Debounce_ -- Swipe handling: Detection threshold has been slightly reduced +- Swipe handling: Diagonal and long-hold swipes are now discarded, and the swipe detection threshold has been slightly reduced +- Touch handling: Discards touches near edges of adjacent regions - Keypad: Added backtick **`** - Bugfix: Screensaver not activating in menu pages without statusbar - Embit: Improved BIP39 mnemonic validation - Bug Fix: Corrected handling of certain binary-encoded QR codes +- UI: Other small changes and optimizations - Fix fingerprint unset warn message for rare case - Improved QR code decoding performance and added inverted color QR code detection diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json index ad6c3aebd..d20074a14 100644 --- a/i18n/translations/de-DE.json +++ b/i18n/translations/de-DE.json @@ -71,7 +71,6 @@ "Device Tests": "Gerätetests", "Display": "Bildschirm", "Do not power off, it may take a while to complete.": "Schalten Sie das Gerät nicht aus, es kann eine Weile dauern.", - "Done Converting": "Konvertierung abgeschlossen", "Done?": "Fertig?", "Double mnemonic": "Doppelte Gedächtnisstütze", "Driver": "Driver", diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json index 9c22e7b41..94ee25fc2 100644 --- a/i18n/translations/es-MX.json +++ b/i18n/translations/es-MX.json @@ -71,7 +71,6 @@ "Device Tests": "Pruebas del dispositivo", "Display": "Pantalla", "Do not power off, it may take a while to complete.": "No apagues el dispositivo, puede tardar un tiempo en completarse.", - "Done Converting": "Listo para convertir", "Done?": "¿Listo?", "Double mnemonic": "Doble mnemónico", "Driver": "Operador", diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json index 84ee289fa..714e61a6a 100644 --- a/i18n/translations/fr-FR.json +++ b/i18n/translations/fr-FR.json @@ -71,7 +71,6 @@ "Device Tests": "Tests de l'appareil", "Display": "Affichage", "Do not power off, it may take a while to complete.": "Ne pas éteindre, cela peut prendre un certain temps.", - "Done Converting": "Conversion terminée", "Done?": "Terminé ?", "Double mnemonic": "Double mnémonique", "Driver": "Pilote", diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json index 9986b567c..3b31dd742 100644 --- a/i18n/translations/ja-JP.json +++ b/i18n/translations/ja-JP.json @@ -71,7 +71,6 @@ "Device Tests": "デバイステスト", "Display": "ディスプレイ", "Do not power off, it may take a while to complete.": "完了するまで電源を切らないでください.", - "Done Converting": "変換を完了する", "Done?": "完了?", "Double mnemonic": "ダブルニーモニック", "Driver": "ドライバー", diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json index 133998843..ea61b0b35 100644 --- a/i18n/translations/ko-KR.json +++ b/i18n/translations/ko-KR.json @@ -71,7 +71,6 @@ "Device Tests": "장치 테스트", "Display": "디스플레이", "Do not power off, it may take a while to complete.": "전원을 끄지 마십시오. 완료하는 데 시간이 걸릴 수 있습니다.", - "Done Converting": "변환 완료", "Done?": "완료되었습니까?", "Double mnemonic": "이중 니모닉", "Driver": "드라이버", diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json index 83c451810..ee3e82610 100644 --- a/i18n/translations/nl-NL.json +++ b/i18n/translations/nl-NL.json @@ -71,7 +71,6 @@ "Device Tests": "Apparaattests", "Display": "Weergave", "Do not power off, it may take a while to complete.": "Schakel het apparaat niet uit, het kan even duren voordat het klaar is.", - "Done Converting": "Converteren gedaan.\r", "Done?": "Klaar?", "Double mnemonic": "Dubbel geheugensteuntje", "Driver": "Driver", diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json index c98b1cf8c..b05a6ccfe 100644 --- a/i18n/translations/pt-BR.json +++ b/i18n/translations/pt-BR.json @@ -71,7 +71,6 @@ "Device Tests": "Testes do Dispositivo", "Display": "Display", "Do not power off, it may take a while to complete.": "Não desligue, pode demorar um pouco para concluir.", - "Done Converting": "Concluída a conversão", "Done?": "Concluído?", "Double mnemonic": "Mnemônico duplo", "Driver": "Driver", diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json index 7f4ef2f73..22c889a5f 100644 --- a/i18n/translations/ru-RU.json +++ b/i18n/translations/ru-RU.json @@ -71,7 +71,6 @@ "Device Tests": "Испытания устройства", "Display": "Дисплеи", "Do not power off, it may take a while to complete.": "Не выключайте питание, это может занять некоторое время.", - "Done Converting": "Конвертация завершена", "Done?": "Готово?", "Double mnemonic": "Двойная мнемоника", "Driver": "Драйвер", diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json index 736020252..1e7e65841 100644 --- a/i18n/translations/tr-TR.json +++ b/i18n/translations/tr-TR.json @@ -71,7 +71,6 @@ "Device Tests": "Cihaz Testleri", "Display": "Ekran", "Do not power off, it may take a while to complete.": "Kapatmayın, tamamlanması biraz zaman alabilir.", - "Done Converting": "Dönüştürme Tamamlandı", "Done?": "Tamamlandı mı?", "Double mnemonic": "Çifte anımsatıcı", "Driver": "Sürücü", diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json index 35bb6f2f3..9912b8876 100644 --- a/i18n/translations/vi-VN.json +++ b/i18n/translations/vi-VN.json @@ -71,7 +71,6 @@ "Device Tests": "Kiểm tra thiết bị", "Display": "Hiển thị", "Do not power off, it may take a while to complete.": "Không được tắt máy, có thể mất một lúc để hoàn thành.", - "Done Converting": "Hoàn tất chuyển đổi", "Done?": "Hoàn tất?", "Double mnemonic": "Từ gợi nhớ kép", "Driver": "Driver", diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json index b803a2df6..e46aad08f 100644 --- a/i18n/translations/zh-CN.json +++ b/i18n/translations/zh-CN.json @@ -71,7 +71,6 @@ "Device Tests": "设备测试", "Display": "显示", "Do not power off, it may take a while to complete.": "请勿断电,可能需要一段时间完成.", - "Done Converting": "完成转换", "Done?": "完成了吗?", "Double mnemonic": "双重助记词", "Driver": "驱动程序", diff --git a/src/krux/input.py b/src/krux/input.py index 0eb076203..1965bdd3a 100644 --- a/src/krux/input.py +++ b/src/krux/input.py @@ -19,6 +19,8 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# pylint: disable=unnecessary-lambda + import time import board from .wdt import wdt @@ -35,6 +37,7 @@ SWIPE_LEFT = 5 SWIPE_UP = 6 SWIPE_DOWN = 7 +SWIPE_FAIL = 99 FAST_FORWARD = 8 FAST_BACKWARD = 9 @@ -47,6 +50,7 @@ QR_ANIM_PERIOD = 300 # milliseconds LONG_PRESS_PERIOD = 1000 # milliseconds KEY_REPEAT_DELAY_MS = 100 +TOUCH_HIGHLIGHT_MS = 100 BUTTON_WAIT_PRESS_DELAY = 10 ONE_MINUTE = 60000 @@ -175,29 +179,30 @@ def touch_event(self, validate_position=True): return self.touch.event(validate_position) return False - def swipe_right_value(self): - """Intermediary method to pull touch gesture, if touch available""" + def _swipe_check_value(self, swipe_fnc): if kboard.has_touchscreen: - return self.touch.swipe_right_value() + return swipe_fnc() return RELEASED + def swipe_none_value(self): + """Intermediary method to pull touch gesture, if touch available""" + return self._swipe_check_value(lambda: self.touch.swipe_none_value()) + + def swipe_right_value(self): + """Intermediary method to pull touch gesture, if touch available""" + return self._swipe_check_value(lambda: self.touch.swipe_right_value()) + def swipe_left_value(self): """Intermediary method to pull touch gesture, if touch available""" - if kboard.has_touchscreen: - return self.touch.swipe_left_value() - return RELEASED + return self._swipe_check_value(lambda: self.touch.swipe_left_value()) def swipe_up_value(self): """Intermediary method to pull touch gesture, if touch available""" - if kboard.has_touchscreen: - return self.touch.swipe_up_value() - return RELEASED + return self._swipe_check_value(lambda: self.touch.swipe_up_value()) def swipe_down_value(self): """Intermediary method to pull touch gesture, if touch available""" - if kboard.has_touchscreen: - return self.touch.swipe_down_value() - return RELEASED + return self._swipe_check_value(lambda: self.touch.swipe_down_value()) def wdt_feed_inc_entropy(self): """Feeds the watchdog and increments the input's entropy""" @@ -310,6 +315,10 @@ def _handle_touch_input(): while self.touch_value() == PRESSED: self.wdt_feed_inc_entropy() self.buttons_active = False + + # Check if was a swipe + if self.swipe_none_value() == PRESSED: + return SWIPE_FAIL if self.swipe_right_value() == PRESSED: return SWIPE_RIGHT if self.swipe_left_value() == PRESSED: @@ -318,6 +327,8 @@ def _handle_touch_input(): return SWIPE_UP if self.swipe_down_value() == PRESSED: return SWIPE_DOWN + + # was a simple touch return BUTTON_TOUCH if btn in [BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV]: diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 5e954c093..7d9ff9634 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -33,17 +33,20 @@ BUTTON_TOUCH, SWIPE_DOWN, SWIPE_UP, + SWIPE_FAIL, FAST_FORWARD, FAST_BACKWARD, SWIPE_LEFT, SWIPE_RIGHT, ONE_MINUTE, + TOUCH_HIGHLIGHT_MS, ) from ..display import ( DEFAULT_PADDING, MINIMAL_PADDING, FLASH_MSG_TIME, FONT_HEIGHT, + FONT_WIDTH, STATUS_BAR_HEIGHT, BOTTOM_LINE, ) @@ -51,6 +54,7 @@ from ..krux_settings import t, Settings from ..sd_card import SDHandler from ..kboard import kboard +from ..settings import BACK_ARROW MENU_CONTINUE = 0 MENU_EXIT = 1 @@ -157,7 +161,7 @@ def capture_from_keypad( """ buffer = starting_buffer pad = Keypad(self.ctx, keysets, possible_keys_fn) - swipe_has_not_been_used = True + swipe_used = False show_swipe_hint = False while True: self.ctx.display.clear() @@ -171,9 +175,12 @@ def capture_from_keypad( show_swipe_hint = False # unless overridden by a particular key, # don't show the swipe hint after a key press - btn = self.ctx.input.wait_for_fastnav_button() - if btn == BUTTON_TOUCH: - btn = pad.touch_to_physical() + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button() + if btn == BUTTON_TOUCH: + btn = pad.touch_to_physical() if btn == BUTTON_ENTER: pad.moving_forward = True changed = False @@ -193,8 +200,7 @@ def capture_from_keypad( elif pad.cur_key_index == pad.go_index: break elif pad.cur_key_index == pad.more_index: - swipeable = kboard.has_touchscreen - if swipeable and swipe_has_not_been_used: + if kboard.has_touchscreen and not swipe_used: show_swipe_hint = True pad.next_keyset() elif pad.cur_key_index < len(pad.keys): @@ -212,7 +218,7 @@ def capture_from_keypad( break else: if btn in (SWIPE_UP, SWIPE_LEFT, SWIPE_DOWN, SWIPE_RIGHT): - swipe_has_not_been_used = False + swipe_used = True pad.navigate(btn) if kboard.has_touchscreen: self.ctx.input.touch.clear_regions() @@ -401,90 +407,55 @@ def print_prompt(self, text, check_printer=True): def prompt(self, text, offset_y=0, highlight_prefix=""): """Prompts user to answer Yes or No""" - lines = self.ctx.display.to_lines(text) - offset_y -= (len(lines) - 1) * FONT_HEIGHT - self.ctx.display.draw_hcentered_text( + lines = self.ctx.display.draw_hcentered_text( text, offset_y, theme.fg_color, theme.bg_color, highlight_prefix=highlight_prefix, ) - self.y_keypad_map = [] - self.x_keypad_map = [] + offset_y = min( + offset_y + lines * FONT_HEIGHT, self.ctx.display.height() - FONT_HEIGHT + ) if kboard.has_minimal_display: return self.ctx.input.wait_for_button() == BUTTON_ENTER - offset_y += (len(lines) + 1) * FONT_HEIGHT - self.x_keypad_map.extend( - [0, self.ctx.display.width() // 2, self.ctx.display.width()] - ) - y_key_map = offset_y - (3 * FONT_HEIGHT // 2) - self.y_keypad_map.append(y_key_map) - y_key_map += 4 * FONT_HEIGHT - self.y_keypad_map.append(min(y_key_map, self.ctx.display.height())) + self.x_keypad_map = [ + DEFAULT_PADDING, + self.ctx.display.width() // 2, + self.ctx.display.width() - DEFAULT_PADDING, + ] + touch_offset_y = self.proceed_menu_text_y_offset(offset_y) - FONT_HEIGHT + self.y_keypad_map = [touch_offset_y, touch_offset_y + 3 * FONT_HEIGHT] if kboard.has_touchscreen: self.ctx.input.touch.set_regions(self.x_keypad_map, self.y_keypad_map) + + go_str = t("Yes") + no_str = t("No") btn = None - answer = True + index = 1 while btn != BUTTON_ENTER: - go_str = t("Yes") - no_str = t("No") - offset_x = (self.ctx.display.width() * 3) // 4 - ( - lcd.string_width_px(go_str) // 2 - ) - self.ctx.display.draw_string( - offset_x, offset_y, go_str, theme.go_color, theme.bg_color - ) - offset_x = self.ctx.display.width() // 4 - ( - lcd.string_width_px(no_str) // 2 - ) - self.ctx.display.draw_string( - offset_x, offset_y, no_str, theme.no_esc_color, theme.bg_color - ) - if self.ctx.input.buttons_active: - if answer: - self.ctx.display.outline( - self.ctx.display.width() // 2, - offset_y - FONT_HEIGHT // 2, - self.ctx.display.usable_width() // 2, - 2 * FONT_HEIGHT - 2, - theme.go_color, - ) - else: - self.ctx.display.outline( - DEFAULT_PADDING, - offset_y - FONT_HEIGHT // 2, - self.ctx.display.usable_width() // 2, - 2 * FONT_HEIGHT - 2, - theme.no_esc_color, - ) - elif kboard.has_touchscreen: - self.ctx.display.draw_vline( - self.ctx.display.width() // 2, - self.y_keypad_map[0] + FONT_HEIGHT, - 2 * FONT_HEIGHT, - theme.frame_color, - ) - btn = self.ctx.input.wait_for_button() + self.draw_proceed_menu(go_str, no_str, offset_y, index) + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_button() + if btn == BUTTON_TOUCH: + self.ctx.input.touch.clear_regions() + # index 0 is No / 1 is Yes / -1 is Edge touch (ignore) + new_index = self.ctx.input.touch.current_index() + if new_index in (0, 1): + # Highlight the touched btn + self.draw_proceed_menu( + go_str, no_str, offset_y, new_index, highlight=True + ) + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little + if new_index == 1: + return True + return False if btn in (BUTTON_PAGE, BUTTON_PAGE_PREV): - answer = not answer - # erase yes/no area for next loop - self.ctx.display.fill_rectangle( - 0, - offset_y - FONT_HEIGHT, - self.ctx.display.width(), - 3 * FONT_HEIGHT, - theme.bg_color, - ) - elif btn == BUTTON_TOUCH: - self.ctx.input.touch.clear_regions() - # index 0 = No - # index 1 = Yes - if self.ctx.input.touch.current_index(): - return True - return False + index = (index + 1) % 2 # BUTTON_ENTER - return answer + return index == 1 def fit_to_line(self, text, prefix="", fixed_chars=0, crop_middle=True): """Fits text with prefix plus fixed_chars at the beginning into one line, @@ -534,8 +505,20 @@ def run(self, start_from_index=None): _, status = self.menu.run_loop(start_from_index) return status != MENU_SHUTDOWN + def proceed_menu_text_y_offset(self, y_offset): + """Y offset for the text on proceed menu""" + return ( + self.ctx.display.height() - (y_offset + FONT_HEIGHT + MINIMAL_PADDING) + ) // 2 + y_offset + def draw_proceed_menu( - self, go_txt, esc_txt, y_offset=0, menu_index=None, go_enabled=True + self, + go_txt, + esc_txt, + y_offset=0, + menu_index=None, + go_enabled=True, + highlight=False, ): """Reusable 'Esc' and 'Go' menu choice""" go_x_offset = ( @@ -544,31 +527,40 @@ def draw_proceed_menu( esc_x_offset = ( self.ctx.display.width() // 2 - lcd.string_width_px(esc_txt) ) // 2 - go_esc_y_offset = ( - self.ctx.display.height() - (y_offset + FONT_HEIGHT + MINIMAL_PADDING) - ) // 2 + y_offset - if menu_index == 0 and self.ctx.input.buttons_active: - self.ctx.display.outline( - DEFAULT_PADDING, - go_esc_y_offset - FONT_HEIGHT // 2, - self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, - FONT_HEIGHT + FONT_HEIGHT, - theme.no_esc_color, - ) + go_esc_y_offset = self.proceed_menu_text_y_offset(y_offset) + go_esc_box_y_offset = go_esc_y_offset - FONT_HEIGHT // 2 + esc_box_x_offset = self.ctx.display.width() // 2 + DEFAULT_PADDING + bg_color = theme.bg_color + fg_color = theme.no_esc_color + if menu_index == 0 and (self.ctx.input.buttons_active or highlight): + bg_color = fg_color + fg_color = theme.bg_color + self.ctx.display.fill_rectangle( + DEFAULT_PADDING, + go_esc_box_y_offset, + self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, + FONT_HEIGHT * 2, + bg_color, + ) self.ctx.display.draw_string( - esc_x_offset, go_esc_y_offset, esc_txt, theme.no_esc_color + esc_x_offset, go_esc_y_offset, esc_txt, fg_color, bg_color ) - if menu_index == 1 and self.ctx.input.buttons_active: - self.ctx.display.outline( - self.ctx.display.width() // 2 + DEFAULT_PADDING, - go_esc_y_offset - FONT_HEIGHT // 2, - self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, - FONT_HEIGHT + FONT_HEIGHT, - theme.go_color, - ) - go_color = theme.go_color if go_enabled else theme.disabled_color - self.ctx.display.draw_string(go_x_offset, go_esc_y_offset, go_txt, go_color) - if not self.ctx.input.buttons_active: + bg_color = theme.bg_color + fg_color = theme.go_color if go_enabled else theme.disabled_color + if menu_index == 1 and (self.ctx.input.buttons_active or highlight): + bg_color = fg_color + fg_color = theme.bg_color + self.ctx.display.fill_rectangle( + esc_box_x_offset, + go_esc_box_y_offset, + self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, + FONT_HEIGHT * 2, + bg_color, + ) + self.ctx.display.draw_string( + go_x_offset, go_esc_y_offset, go_txt, fg_color, bg_color + ) + if not (self.ctx.input.buttons_active or highlight): self.ctx.display.draw_vline( self.ctx.display.width() // 2, go_esc_y_offset, @@ -634,6 +626,14 @@ def index(self, i): """Returns the true index of an element in the underlying list""" return self.offset + i + def total_pages(self): + """Compute how many pages are required to show all items""" + return -(-len(self.list) // self.max_size) # ceil of division + + def curr_page(self): + """Compute the current page number based on the scroll offset""" + return self.offset // self.max_size + class Menu: """Represents a menu that can render itself to the screen, handle item selection, @@ -653,7 +653,7 @@ def __init__( self.menu = menu if back_label: back_label = t("Back") if back_label == "Back" else back_label - self.menu += [("< " + back_label, back_status)] + self.menu += [(BACK_ARROW + back_label, back_status)] self.disable_statusbar = disable_statusbar or ( self.ctx.wallet is None and not kboard.has_battery ) @@ -718,6 +718,48 @@ def _process_swipe_down(self, selected_item_index, swipe_down_fnc=None): selected_item_index = self._move_back() return selected_item_index + def draw_vertical_bar(self): + """Draws a vertical scrolling bar composed of small rectangles.""" + total_pages = self.menu_view.total_pages() + # don't draw if just one page + if total_pages < 2: + return + + bar_padding = -(-FONT_HEIGHT // 3) # ceil of division + bar_width = -(-FONT_WIDTH // 3) # ceil of division + bar_height = ( + self.ctx.display.height() - 2 * DEFAULT_PADDING - self.menu_offset + ) // total_pages - bar_padding + y_offset = ( + self.ctx.display.height() - ((bar_height + bar_padding) * total_pages) + ) // 2 + (bar_padding + self.menu_offset) // 2 + for i in range(total_pages): + color = ( + theme.toggle_color + if i == self.menu_view.curr_page() + else theme.info_bg_color + ) + self.ctx.display.fill_rectangle( + self.ctx.display.width() - bar_width * 2, + y_offset + (bar_height + bar_padding) * i, + bar_width, + bar_height, + color, + ) + + def _clear_menu_display(self): + if self.menu_offset > STATUS_BAR_HEIGHT: + # Clear only the menu area + self.ctx.display.fill_rectangle( + 0, + self.menu_offset, + self.ctx.display.width(), + self.ctx.display.height() - self.menu_offset, + theme.bg_color, + ) + else: + self.ctx.display.clear() + def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None): """Runs the menu loop until one of the menu items returns either a MENU_EXIT or MENU_SHUTDOWN status @@ -729,22 +771,15 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None selected_item_index = start_from_index while True: gc.collect() - if self.menu_offset > STATUS_BAR_HEIGHT: - # Clear only the menu area - self.ctx.display.fill_rectangle( - 0, - self.menu_offset, - self.ctx.display.width(), - self.ctx.display.height() - self.menu_offset, - theme.bg_color, - ) - else: - self.ctx.display.clear() + self._clear_menu_display() if kboard.has_touchscreen: - self._draw_touch_menu(selected_item_index) + self._draw_touch_menu( + selected_item_index, draw_dividers=not self.ctx.input.buttons_active + ) else: self._draw_menu(selected_item_index) self.draw_status_bar() + self.draw_vertical_bar() self.ctx.input.reset_ios_state() if start_from_submenu: status = self._clicked_item(selected_item_index) @@ -753,15 +788,31 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None start_from_submenu = False else: screensaver_time = Settings().appearance.screensaver_time - btn = self.ctx.input.wait_for_fastnav_button( - # Block if screen saver not active - screensaver_time == 0, - screensaver_time * ONE_MINUTE, - ) - if kboard.has_touchscreen: + was_btn_active = self.ctx.input.buttons_active + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button( + # Block if screen saver not active + screensaver_time == 0, + screensaver_time * ONE_MINUTE, + ) if btn == BUTTON_TOUCH: selected_item_index = self.ctx.input.touch.current_index() + if selected_item_index < 0: + continue + + # highlight selected index before continue + if was_btn_active: + # need to clear screen if button was used just before + self._clear_menu_display() + self._draw_touch_menu( + selected_item_index, + draw_dividers=not was_btn_active, + highlight=True, + ) + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little btn = BUTTON_ENTER + if kboard.has_touchscreen: self.ctx.input.touch.clear_regions() if btn == BUTTON_ENTER: status = self._clicked_item(selected_item_index) @@ -787,14 +838,12 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None self.screensaver() def _clicked_item(self, selected_item_index): - item = self.menu_view[selected_item_index] - if item[1] is None: - return MENU_CONTINUE try: + action = self.menu_view[selected_item_index][1] + if action is None: + return MENU_CONTINUE self.ctx.display.clear() - status = item[1]() - if status != MENU_CONTINUE: - return status + return action() except Exception as e: self.ctx.display.to_portrait() self.ctx.display.clear() @@ -922,7 +971,16 @@ def draw_network_indicator(self): theme.info_bg_color, ) - def _draw_touch_menu(self, selected_item_index): + def _get_menu_item_color(self, menu_item): + if menu_item[1] is None: + return theme.disabled_color + if len(menu_item) > 2: + return menu_item[2] + return theme.fg_color + + def _draw_touch_menu( + self, selected_item_index, draw_dividers=True, highlight=False + ): # map regions with dynamic height to fill screen self.ctx.input.touch.clear_regions() offset_y = 0 @@ -944,8 +1002,8 @@ def _draw_touch_menu(self, selected_item_index): self.ctx.input.touch.y_regions = y_keypad_map # Draw dividers - for i, y in enumerate(y_keypad_map[:-1]): - if i and not self.ctx.input.buttons_active: + if draw_dividers: + for y in y_keypad_map[1:-1]: # skip the first and last entries self.ctx.display.draw_line( 0, y, self.ctx.display.width(), y, theme.frame_color ) @@ -957,10 +1015,10 @@ def _draw_touch_menu(self, selected_item_index): offset_y_item = ( region_height - len(menu_item_lines) * FONT_HEIGHT ) // 2 + y_keypad_map[i] - fg_color = ( - theme.fg_color if menu_item[1] is not None else theme.disabled_color - ) - if selected_item_index == i and self.ctx.input.buttons_active: + fg_color = self._get_menu_item_color(menu_item) + if selected_item_index == i and ( + self.ctx.input.buttons_active or highlight + ): self.ctx.display.fill_rectangle( 0, offset_y_item + 1 - FONT_HEIGHT // 2, @@ -969,7 +1027,9 @@ def _draw_touch_menu(self, selected_item_index): fg_color, ) for j, text in enumerate(menu_item_lines): - if selected_item_index == i and self.ctx.input.buttons_active: + if selected_item_index == i and ( + self.ctx.input.buttons_active or highlight + ): self.ctx.display.draw_hcentered_text( text, offset_y_item + FONT_HEIGHT * j, theme.bg_color, fg_color ) @@ -1002,9 +1062,7 @@ def _draw_menu(self, selected_item_index): items_pad //= max(len(self.menu_view) - 1, 1) items_pad = min(items_pad, FONT_HEIGHT) for i, menu_item in enumerate(self.menu_view): - fg_color = ( - theme.fg_color if menu_item[1] is not None else theme.disabled_color - ) + fg_color = self._get_menu_item_color(menu_item) menu_item_lines = self.ctx.display.to_lines(menu_item[0]) delta_y = len(menu_item_lines) * FONT_HEIGHT + items_pad if selected_item_index == i: diff --git a/src/krux/pages/datum_tool.py b/src/krux/pages/datum_tool.py index b0fb7e65b..95f6d38b7 100644 --- a/src/krux/pages/datum_tool.py +++ b/src/krux/pages/datum_tool.py @@ -52,6 +52,7 @@ SWIPE_RIGHT, SWIPE_DOWN, ) +from ..settings import CONTEXT_ARROW DATUM_DESCRIPTOR = "DESC" DATUM_PSBT = "PSBT" @@ -418,23 +419,28 @@ def __init__(self, ctx): self.history = [] self.oneline_viewable = None - def view_qr(self): - """Reusable handler for viewing a QR code""" + def _get_qr_threshold(self): from ..qr import QR_CAPACITY_BYTE, QR_CAPACITY_ALPHANUMERIC, QR_CAPACITY_NUMERIC - from ..bbqr import encode_bbqr - import urtypes - from ur.ur import UR - - # Helper function to check if character is alphanumeric - def is_alnum(c): - return ("A" <= c <= "Z") or ("0" <= c <= "9") or c in (" $%*+-./:") seedqrview_thresh = QR_CAPACITY_BYTE[STATIC_QR_MAX_SIZE] if not isinstance(self.contents, bytes): + # Helper function to check if character is alphanumeric + def is_alnum(c): + return ("A" <= c <= "Z") or ("0" <= c <= "9") or c in (" $%*+-./:") + if all(c.isdigit() for c in self.contents[:SUFFICIENT_SAMPLE_SIZE]): seedqrview_thresh = QR_CAPACITY_NUMERIC[STATIC_QR_MAX_SIZE] elif all(is_alnum(c) for c in self.contents[:SUFFICIENT_SAMPLE_SIZE]): seedqrview_thresh = QR_CAPACITY_ALPHANUMERIC[STATIC_QR_MAX_SIZE] + return seedqrview_thresh + + def view_qr(self): + """Reusable handler for viewing a QR code""" + from ..bbqr import encode_bbqr + import urtypes + from ur.ur import UR + + seedqrview_thresh = self._get_qr_threshold() if len(self.contents) <= seedqrview_thresh: from .encryption_ui import prompt_for_text_update @@ -497,11 +503,17 @@ def is_alnum(c): if isinstance(self.contents, bytes): menu_opts.append(("UR bytes", (FORMAT_UR, "bytes"))) - idx, _ = Menu( + submenu = Menu( self.ctx, [(x[0], lambda: None) for x in menu_opts], - back_label=None, - ).run_loop() + ) + idx, _ = submenu.run_loop() + + if idx == submenu.back_index: + return MENU_CONTINUE + + del submenu + gc.collect() qr_fmt = menu_opts[idx][1][0] @@ -740,8 +752,11 @@ def _build_options_menu(self, offer_convert=False, offer_show=True): menu.append((t("Show Datum"), lambda: "show")) if not offer_convert: - menu.append((t("Convert Datum"), lambda: "convert_begin")) - menu.append((t("QR Code"), lambda: "export_qr")) + qr_context = ( + CONTEXT_ARROW if len(self.contents) > self._get_qr_threshold() else "" + ) + menu.append((t("Convert Datum") + CONTEXT_ARROW, lambda: "convert_begin")) + menu.append((t("QR Code") + qr_context, lambda: "export_qr")) # when not sensitive, allow export to sd if not self.sensitive: @@ -763,7 +778,7 @@ def _build_options_menu(self, offer_convert=False, offer_show=True): menu.append((t("to utf8"), lambda: "utf8")) except: pass - menu.append((t("Encrypt"), lambda: "encrypt")) + menu.append((t("Encrypt") + CONTEXT_ARROW, lambda: "encrypt")) elif isinstance(self.contents, str): if "HEX" in self.encodings: @@ -792,8 +807,6 @@ def _build_options_menu(self, offer_convert=False, offer_show=True): menu[i] = (option[0] + " (" + t("Undo") + ")", lambda: "undo") break - menu.append((t("Done Converting"), lambda: "convert_end")) - return menu def view_contents(self, try_decrypt=True, offer_convert=False): @@ -823,17 +836,16 @@ def view_contents(self, try_decrypt=True, offer_convert=False): info_len = self._info_box() # run todo_menu - back_status = {} - if offer_convert: - back_status = {"back_label": None} menu = Menu( self.ctx, todo_menu, offset=info_len * FONT_HEIGHT + DEFAULT_PADDING, - **back_status ) _, status = menu.run_loop() + # Back case for convert menu + status = "convert_end" if offer_convert and status == MENU_EXIT else status + if status == MENU_EXIT: # if user chose to exit return MENU_CONTINUE diff --git a/src/krux/pages/device_tests.py b/src/krux/pages/device_tests.py index 739f59907..e7fe248ae 100644 --- a/src/krux/pages/device_tests.py +++ b/src/krux/pages/device_tests.py @@ -344,8 +344,7 @@ def hw_acc_hashing(self, interactive=False): churns nonce through sha256() and pbkdf_hmac(sha256,) calls, raises ValueError if calculated results don't match expected results """ - from uhashlib_hw import sha256 as f_hash - from uhashlib_hw import pbkdf2_hmac_sha256 as f_hmac + from uhashlib_hw import sha256 as f_hash, pbkdf2_hmac_sha256 as f_hmac # pylint: disable=C0301 expecteds = [ diff --git a/src/krux/pages/encryption_ui.py b/src/krux/pages/encryption_ui.py index dd13cb864..f4b20ca89 100644 --- a/src/krux/pages/encryption_ui.py +++ b/src/krux/pages/encryption_ui.py @@ -25,6 +25,7 @@ from binascii import hexlify from ..display import DEFAULT_PADDING, FONT_HEIGHT, BOTTOM_PROMPT_LINE from ..krux_settings import t, Settings +from ..settings import CONTEXT_ARROW from ..encryption import QR_CODE_ITER_MULTIPLE from krux import kef from ..themes import theme @@ -447,9 +448,11 @@ def encryption_key(self, creating=False): (t("Type Key"), self.load_key), (t("Scan Key QR Code"), self.load_qr_encryption_key), ], - back_label=None, ) - _, key = submenu.run_loop() + index, key = submenu.run_loop() + + if index == submenu.back_index: + return None try: # encryption key may have been encrypted @@ -564,16 +567,16 @@ def encrypt_menu(self): """Menu with mnemonic encryption output options""" encrypt_outputs_menu = [ - (t("Store on Flash"), self.store_mnemonic_on_memory), + (t("Store on Flash") + CONTEXT_ARROW, self.store_mnemonic_on_memory), ( - t("Store on SD Card"), + t("Store on SD Card") + CONTEXT_ARROW, ( None if not self.has_sd_card() else lambda: self.store_mnemonic_on_memory(True) ), ), - (t("Encrypted QR Code"), self.encrypted_qr_code), + (t("Encrypted QR Code") + CONTEXT_ARROW, self.encrypted_qr_code), ] submenu = Menu(self.ctx, encrypt_outputs_menu) _, _ = submenu.run_loop() @@ -653,6 +656,7 @@ def load_from_storage(self, remove_opt=False): if remove_opt else self._load_encrypted_mnemonic(m_id) ), + theme.no_esc_color if remove_opt else theme.fg_color, ) ) for mnemonic_id in sorted(sd_mnemonics): @@ -664,6 +668,7 @@ def load_from_storage(self, remove_opt=False): if remove_opt else self._load_encrypted_mnemonic(m_id, sd_card=True) ), + theme.no_esc_color if remove_opt else theme.fg_color, ) ) submenu = Menu(self.ctx, mnemonic_ids_menu) @@ -704,7 +709,11 @@ def _remove_encrypted_mnemonic(self, mnemonic_id, sd_card=False): mnemonic_storage = MnemonicStorage() self.ctx.display.clear() - if self.prompt(t("Remove %s?") % mnemonic_id, self.ctx.display.height() // 2): + if self.prompt( + t("Remove %s?") % mnemonic_id, + self.ctx.display.height() // 2, + highlight_prefix="?", + ): mnemonic_storage.del_mnemonic(mnemonic_id, sd_card) message = t("%s removed.") % mnemonic_id message += "\n\n" diff --git a/src/krux/pages/flash_tools.py b/src/krux/pages/flash_tools.py index 4dcb72980..bee2e71df 100644 --- a/src/krux/pages/flash_tools.py +++ b/src/krux/pages/flash_tools.py @@ -47,7 +47,7 @@ def flash_tools_menu(self): [ (t("Flash Map"), self.flash_map), (t("TC Flash Hash"), self.tc_flash_hash), - (t("Erase User's Data"), self.erase_users_data), + (t("Erase User's Data"), self.erase_users_data, theme.no_esc_color), ], ) flash_menu.run_loop() diff --git a/src/krux/pages/home_pages/addresses.py b/src/krux/pages/home_pages/addresses.py index fd1518bd4..00cf20290 100644 --- a/src/krux/pages/home_pages/addresses.py +++ b/src/krux/pages/home_pages/addresses.py @@ -23,7 +23,7 @@ import gc from ...display import BOTTOM_PROMPT_LINE from ...krux_settings import t -from ...settings import THIN_SPACE +from ...settings import THIN_SPACE, CONTEXT_ARROW from ...qr import FORMAT_NONE from .. import ( Page, @@ -54,15 +54,15 @@ def addresses_menu(self): self.ctx, [ ( - t("Scan Address"), + t("Scan Address") + CONTEXT_ARROW, lambda: self._receive_change_menu(self.scan_address), ), ( - t("List Addresses"), + t("List Addresses") + CONTEXT_ARROW, lambda: self._receive_change_menu(self.list_address_type), ), ( - t("Export Addresses"), + t("Export Addresses") + CONTEXT_ARROW, ( None if not self.has_sd_card() diff --git a/src/krux/pages/home_pages/bip85.py b/src/krux/pages/home_pages/bip85.py index c18402487..dc98dcf8c 100644 --- a/src/krux/pages/home_pages/bip85.py +++ b/src/krux/pages/home_pages/bip85.py @@ -24,8 +24,8 @@ from ...sd_card import B64_FILE_EXTENSION from ...baseconv import base_encode from ...display import BOTTOM_PROMPT_LINE, FONT_HEIGHT, DEFAULT_PADDING -from ...krux_settings import t -from ...krux_settings import Settings +from ...krux_settings import t, Settings +from ...settings import CONTEXT_ARROW from .. import ( Menu, Page, @@ -180,7 +180,7 @@ def export(self): submenu = Menu( self.ctx, [ - (t("BIP39 Mnemonic"), self._derive_mnemonic), + (t("BIP39 Mnemonic") + CONTEXT_ARROW, self._derive_mnemonic), (t("Base64 Password"), self._derive_base64_password), ], ) diff --git a/src/krux/pages/home_pages/home.py b/src/krux/pages/home_pages/home.py index a4fdced21..bdc9e1ca0 100644 --- a/src/krux/pages/home_pages/home.py +++ b/src/krux/pages/home_pages/home.py @@ -35,6 +35,8 @@ from ...format import replace_decimal_separator from ...key import TYPE_SINGLESIG from ...kboard import kboard +from ...settings import CONTEXT_ARROW +from ...themes import theme class Home(Page): @@ -48,18 +50,18 @@ def __init__(self, ctx): ctx, [ ( - t("Backup Mnemonic"), + t("Backup Mnemonic") + CONTEXT_ARROW, ( self.backup_mnemonic if not Settings().security.hide_mnemonic else None ), ), - (t("Extended Public Key"), self.public_key), - (t("Wallet"), self.wallet), - (t("Address"), self.addresses_menu), + (t("Extended Public Key") + CONTEXT_ARROW, self.public_key), + (t("Wallet") + CONTEXT_ARROW, self.wallet), + (t("Address") + CONTEXT_ARROW, self.addresses_menu), (t("Sign"), self.sign), - (shtn_reboot_label, self.shutdown), + (shtn_reboot_label, self.shutdown, theme.no_esc_color), ], back_label=None, ), @@ -187,11 +189,11 @@ def wallet(self): submenu = Menu( self.ctx, [ - (t("Wallet Descriptor"), self.wallet_descriptor), - (t("Passphrase"), self.passphrase), - (t("Customize"), self.customize), - ("BIP85", self.bip85), - (t("Mnemonic XOR"), self.mnemonic_xor), + (t("Wallet Descriptor") + CONTEXT_ARROW, self.wallet_descriptor), + (t("Passphrase") + CONTEXT_ARROW, self.passphrase), + (t("Customize") + CONTEXT_ARROW, self.customize), + ("BIP85" + CONTEXT_ARROW, self.bip85), + (t("Mnemonic XOR") + CONTEXT_ARROW, self.mnemonic_xor), ], ) submenu.run_loop() @@ -209,8 +211,8 @@ def sign(self): submenu = Menu( self.ctx, [ - ("PSBT", self.sign_psbt), - (t("Message"), self.sign_message), + ("PSBT" + CONTEXT_ARROW, self.sign_psbt), + (t("Message") + CONTEXT_ARROW, self.sign_message), ], ) submenu.run_loop() diff --git a/src/krux/pages/home_pages/mnemonic_backup.py b/src/krux/pages/home_pages/mnemonic_backup.py index a0581ca76..4c52fd81e 100644 --- a/src/krux/pages/home_pages/mnemonic_backup.py +++ b/src/krux/pages/home_pages/mnemonic_backup.py @@ -22,6 +22,7 @@ from ...display import FONT_HEIGHT from ...krux_settings import t, Settings, THERMAL_ADAFRUIT_TXT +from ...settings import CONTEXT_ARROW from .. import ( Page, Menu, @@ -38,9 +39,9 @@ def mnemonic(self): submenu = Menu( self.ctx, [ - (t("QR Code"), self.qr_code_backup), - (t("Encrypted"), self.encrypt_mnemonic_menu), - (t("Other Formats"), self.other_backup_formats), + (t("QR Code") + CONTEXT_ARROW, self.qr_code_backup), + (t("Encrypted") + CONTEXT_ARROW, self.encrypt_mnemonic_menu), + (t("Other Formats") + CONTEXT_ARROW, self.other_backup_formats), ], ) submenu.run_loop() @@ -54,7 +55,7 @@ def qr_code_backup(self): (t("Plaintext QR"), self.display_standard_qr), ("Compact SeedQR", lambda: self.display_seed_qr(True)), ("SeedQR", self.display_seed_qr), - (t("Encrypted QR Code"), self.encrypt_qr_code), + (t("Encrypted QR Code") + CONTEXT_ARROW, self.encrypt_qr_code), ], ) submenu.run_loop() @@ -71,7 +72,7 @@ def other_backup_formats(self): self.ctx.wallet.key.mnemonic, t("Mnemonic") ), ), - (t("Numbers"), self.display_mnemonic_numbers), + (t("Numbers") + CONTEXT_ARROW, self.display_mnemonic_numbers), ("Stackbit 1248", self.stackbit), ("Tinyseed", self.tiny_seed), ], diff --git a/src/krux/pages/home_pages/pub_key_view.py b/src/krux/pages/home_pages/pub_key_view.py index 9e167fcfe..b212b9a06 100644 --- a/src/krux/pages/home_pages/pub_key_view.py +++ b/src/krux/pages/home_pages/pub_key_view.py @@ -22,6 +22,7 @@ from ...display import FONT_HEIGHT from ...krux_settings import t +from ...settings import CONTEXT_ARROW from .. import ( Page, Menu, @@ -119,7 +120,10 @@ def _pub_key_qr(version): :WALLET_XPUB_START ].upper() pub_key_menu_items.append( - (title + " - " + t("Text"), lambda ver=version: _pub_key_text(ver)) + ( + title + " - " + t("Text") + CONTEXT_ARROW, + lambda ver=version: _pub_key_text(ver), + ) ) pub_key_menu_items.append( (title + " - " + t("QR Code"), lambda ver=version: _pub_key_qr(ver)) diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index f4652583a..c7ac549f8 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -22,18 +22,22 @@ import math import lcd +import time +from ..context import Context from ..krux_settings import t from ..themes import theme from ..input import ( BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, + BUTTON_TOUCH, SWIPE_RIGHT, SWIPE_LEFT, SWIPE_UP, SWIPE_DOWN, FAST_FORWARD, FAST_BACKWARD, + TOUCH_HIGHLIGHT_MS, ) from ..display import DEFAULT_PADDING, MINIMAL_PADDING, FONT_HEIGHT, FONT_WIDTH from ..kboard import kboard @@ -44,7 +48,7 @@ class KeypadLayout: """Groups layout-related attributes for Keypad.""" - def __init__(self, ctx, max_keys_count): + def __init__(self, ctx: Context, max_keys_count): self.width = math.floor(math.sqrt(max_keys_count)) self.height = math.ceil(max_keys_count / self.width) self.max_index = self.width * self.height @@ -72,7 +76,7 @@ def __init__(self, ctx, max_keys_count): class Keypad: """Controls keypad creation and management.""" - def __init__(self, ctx, keysets, possible_keys_fn=None): + def __init__(self, ctx: Context, keysets, possible_keys_fn=None): self.ctx = ctx self.keysets = keysets self.keyset_index = 0 @@ -143,94 +147,108 @@ def compute_possible_keys(self, buffer): if self.possible_keys_fn is not None: self.possible_keys = self.possible_keys_fn(buffer) - def draw_keys(self): + def draw_keys(self, prev_index=None): """Draws keypad on the screen""" key_index = 0 + for y in self.layout.y_keypad_map[:-1]: offset_y = y + (self.layout.key_v_spacing - FONT_HEIGHT) // 2 + for x in self.layout.x_keypad_map[:-1]: - x = MINIMAL_PADDING if x == 0 else x - key = None - custom_color = None + offset_x = MINIMAL_PADDING if x == 0 else x + + # Resolve key + color based on index + color = theme.fg_color if key_index < len(self.keys): key = self.keys[key_index] elif key_index == self.del_index: - key = "<" - custom_color = theme.del_color + key, color = "<", theme.del_color elif key_index == self.esc_index: - key = t("Esc") - custom_color = theme.no_esc_color + key, color = t("Esc"), theme.no_esc_color elif key_index == self.go_index: - key = t("Go") - custom_color = theme.go_color + key, color = t("Go"), theme.go_color elif self.has_more_key() and key_index == self.more_index: key = self.keysets[self._move_keyset_index()][:3] - custom_color = theme.toggle_color - - if key is not None: - offset_x = x - key_offset_x = ( - self.layout.key_h_spacing - lcd.string_width_px(key) - ) // 2 + offset_x - if ( - key_index < len(self.keys) - and self.keys[key_index] not in self.possible_keys - ): - # faded text - self.ctx.display.draw_string( - key_offset_x, offset_y, key, theme.disabled_color + color = theme.toggle_color + else: + key = None + + if key is None: + key_index += 1 + continue + + key_offset_x = offset_x + ( + (self.layout.key_h_spacing - lcd.string_width_px(key)) // 2 + ) + + # Disabled + if key_index < len(self.keys) and key not in self.possible_keys: + self.ctx.display.draw_string( + key_offset_x, offset_y, key, theme.disabled_color + ) + key_index += 1 + continue + + # Highlighted + if key_index == self.cur_key_index and ( + self.ctx.input.buttons_active or prev_index is not None + ): + self.ctx.display.fill_rectangle( + offset_x if kboard.is_m5stickv else offset_x + 1, + y + 1, + ( + self.layout.key_h_spacing - 1 + if kboard.is_m5stickv + else self.layout.key_h_spacing - 2 + ), + self.layout.key_v_spacing - 2, + color, + ) + self.ctx.display.draw_string( + key_offset_x, offset_y, key, theme.bg_color, color + ) + key_index += 1 + continue + + # Touchscreen clear prev btn highlight + lines + if kboard.has_touchscreen: + # clear highlight from previous + if prev_index is not None: + self.ctx.display.fill_rectangle( + offset_x + 1, + y + 1, + self.layout.key_h_spacing - 2, + self.layout.key_v_spacing - 2, + theme.bg_color, ) - else: - if kboard.has_touchscreen: - self.ctx.display.outline( - offset_x + 1, - y + 1, - self.layout.key_h_spacing - 2, - self.layout.key_v_spacing - 2, - theme.frame_color, - ) - if custom_color: - self.ctx.display.draw_string( - key_offset_x, offset_y, key, custom_color - ) - else: - self.ctx.display.draw_string(key_offset_x, offset_y, key) - if ( - key_index == self.cur_key_index - and self.ctx.input.buttons_active - ): - if kboard.has_touchscreen: - self.ctx.display.outline( - offset_x + 1, - y + 1, - self.layout.key_h_spacing - 2, - self.layout.key_v_spacing - 2, - ) - else: - self.ctx.display.outline( - offset_x - 2, - y, - self.layout.key_h_spacing + 1, - self.layout.key_v_spacing - 1, - ) + self.ctx.display.outline( + offset_x + 1, + y + 1, + self.layout.key_h_spacing - 2, + self.layout.key_v_spacing - 2, + theme.frame_color, + ) + + self.ctx.display.draw_string(key_offset_x, offset_y, key, color) key_index += 1 def draw_keyset_index(self): """Indicates the current keyset index with a small rectangle""" if not self.has_more_key(): return - bar_height = FONT_HEIGHT // 6 - bar_length = FONT_WIDTH - bar_padding = FONT_WIDTH // 3 + keyset_len = len(self.keysets) + bar_height = -(-FONT_HEIGHT // 3) # ceil of division + bar_padding = -(-FONT_WIDTH // 3) # ceil of division + bar_width = self.ctx.display.usable_width() // keyset_len - bar_padding x_offset = ( - self.ctx.display.width() - (bar_length + bar_padding) * len(self.keysets) - ) // 2 - for i in range(len(self.keysets)): - color = theme.fg_color if i == self.keyset_index else theme.frame_color + self.ctx.display.width() - ((bar_width + bar_padding) * keyset_len) + ) // 2 + bar_padding // 2 + for i in range(keyset_len): + color = theme.fg_color if i == self.keyset_index else theme.info_bg_color self.ctx.display.fill_rectangle( - x_offset + (bar_length + bar_padding) * i, + x_offset + (bar_width + bar_padding) * i, self.layout.y_keypad_map[-1] + 2, - bar_length, + bar_width, bar_height, color, ) @@ -255,15 +273,29 @@ def get_valid_index(self): def touch_to_physical(self): """Convert a touch press in button press""" + prev_index = self.cur_key_index self.cur_key_index = self.ctx.input.touch.current_index() - actual_button = None + if self.cur_key_index < 0: + self.cur_key_index = 0 + return BUTTON_TOUCH + + special_keys = [self.del_index, self.esc_index, self.go_index] + if self.has_more_key(): + special_keys.append(self.more_index) + + actual_button = BUTTON_TOUCH if self.cur_key_index < len(self.keys): if self.keys[self.cur_key_index] in self.possible_keys: actual_button = BUTTON_ENTER - elif self.cur_key_index < self.layout.max_index: + elif self.cur_key_index in special_keys: actual_button = BUTTON_ENTER else: self.cur_key_index = 0 + + if actual_button == BUTTON_ENTER: + self.draw_keys(prev_index=prev_index) # highlight + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little + return actual_button def navigate(self, btn): diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py index 82e32cfca..00147a77c 100644 --- a/src/krux/pages/login.py +++ b/src/krux/pages/login.py @@ -32,7 +32,7 @@ ) from .mnemonic_loader import MnemonicLoader from ..display import DEFAULT_PADDING, FONT_HEIGHT, BOTTOM_PROMPT_LINE -from ..krux_settings import Settings +from ..krux_settings import Settings, t from ..key import ( Key, P2WPKH, @@ -47,8 +47,9 @@ POLICY_TYPE_IDS, NAME_MULTISIG, ) -from ..krux_settings import t from ..kboard import kboard +from ..settings import CONTEXT_ARROW +from ..themes import theme DIGITS_HEX = "0123456789ABCDEF" @@ -67,19 +68,19 @@ class Login(MnemonicLoader): def __init__(self, ctx): login_menu_items = [ - (t("Load Mnemonic"), self.load_key), + (t("Load Mnemonic") + CONTEXT_ARROW, self.load_key), ( - t("New Mnemonic"), + t("New Mnemonic") + CONTEXT_ARROW, (self.new_key if not Settings().security.hide_mnemonic else None), ), - (t("Settings"), self.settings), - (t("Tools"), self.tools), + (t("Settings") + CONTEXT_ARROW, self.settings), + (t("Tools") + CONTEXT_ARROW, self.tools), (t("About"), self.about), ] if ctx.power_manager is not None: kboard.has_battery = ctx.power_manager.has_battery() if kboard.has_battery: - login_menu_items.append((t("Shutdown"), self.shutdown)) + login_menu_items.append((t("Shutdown"), self.shutdown, theme.no_esc_color)) super().__init__( ctx, @@ -95,10 +96,13 @@ def new_key(self): submenu = Menu( self.ctx, [ - (t("Via Camera"), self.new_key_from_snapshot), - (t("Via Words"), lambda: self.load_key_from_text(new=True)), - (t("Via D6"), self.new_key_from_dice), - (t("Via D20"), lambda: self.new_key_from_dice(True)), + (t("Via Camera") + CONTEXT_ARROW, self.new_key_from_snapshot), + ( + t("Via Words") + CONTEXT_ARROW, + lambda: self.load_key_from_text(new=True), + ), + (t("Via D6") + CONTEXT_ARROW, self.new_key_from_dice), + (t("Via D20") + CONTEXT_ARROW, lambda: self.new_key_from_dice(True)), ], ) index, status = submenu.run_loop() @@ -260,7 +264,6 @@ def _load_key_from_words(self, words, charset=LETTERS, new=False): derivation_path = "" from ..wallet import Wallet - from ..themes import theme from .utils import Utils utils = Utils(self.ctx) @@ -292,8 +295,8 @@ def _load_key_from_words(self, words, charset=LETTERS, new=False): self.ctx, [ (t("Load Wallet"), lambda: None), - (t("Passphrase"), lambda: None), - (t("Customize"), lambda: None), + (t("Passphrase") + CONTEXT_ARROW, lambda: None), + (t("Customize") + CONTEXT_ARROW, lambda: None), ], offset=( self.ctx.display.draw_hcentered_text(wallet_info, info_box=True) diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index e78c2e021..e0ed03c2d 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import time from embit import bip39 from embit.wordlists.bip39 import WORDLIST from . import Page, ESC_KEY, LETTERS @@ -33,6 +34,8 @@ BUTTON_PAGE_PREV, FAST_FORWARD, FAST_BACKWARD, + SWIPE_FAIL, + TOUCH_HIGHLIGHT_MS, ) from ..key import Key from ..kboard import kboard @@ -159,7 +162,7 @@ def _draw_header(self): if kboard.has_minimal_display: self.header_offset -= MINIMAL_PADDING - def _map_words(self, button_index=0, page=0): + def _map_words(self, button_index=0, page=0, highlight=False): """Map words to the screen""" def word_color(index): @@ -173,18 +176,17 @@ def word_color(index): return theme.highlight_color return theme.fg_color - # Words occupy 75% of the screen - word_v_padding = self.ctx.display.height() * 3 // 4 - word_v_padding //= 12 + # Words occupy 75% of the screen 3/4 for 12 words 4*12=48 + word_v_padding = self.ctx.display.height() * 3 // 48 if kboard.has_touchscreen: self.ctx.input.touch.clear_regions() self.ctx.input.touch.x_regions.append(0) - self.ctx.input.touch.x_regions.append(self.ctx.display.width() // 2) + self.ctx.input.touch.x_regions.append(self.ctx.display.width() >> 1) self.ctx.input.touch.x_regions.append(self.ctx.display.width()) if not self.ctx.input.buttons_active and self.mnemonic_length == 24: self.ctx.display.draw_vline( - self.ctx.display.width() // 2, + self.ctx.display.width() >> 1, self.header_offset, 12 * word_v_padding, theme.frame_color, @@ -206,16 +208,49 @@ def word_color(index): word_index = 0 if kboard.is_m5stickv or self.mnemonic_length == 12: x_padding = DEFAULT_PADDING + rect_width = self.ctx.display.width() else: x_padding = MINIMAL_PADDING + rect_width = self.ctx.display.width() >> 1 + + # draw Go and Esc before because they can overlap + go_txt = t("Go") + esc_txt = t("Esc") + menu_index = None + if self.mnemonic_length == 24 and kboard.is_m5stickv and page == 0: + go_txt = "13-24" + if (self.ctx.input.buttons_active or highlight) and button_index >= ESC_INDEX: + menu_index = button_index - ESC_INDEX + self.draw_proceed_menu( + go_txt, + esc_txt, + y_region + 12 * word_v_padding, + menu_index, + self.valid_checksum, + highlight=highlight, + ) + while word_index < 12: paged_index = word_index + page * 12 font_color = word_color(paged_index) bg_color = theme.bg_color - if word_index == button_index and self.ctx.input.buttons_active: + if word_index == button_index and ( + self.ctx.input.buttons_active or highlight + ): # Flip the color values for the selected word bg_color = font_color font_color = theme.bg_color + self.ctx.display.fill_rectangle( + 0, + y_region if kboard.has_minimal_display else y_region - 2, + rect_width, + ( + word_v_padding + if kboard.has_minimal_display + else word_v_padding + 1 + ), + bg_color, + ) self.ctx.display.draw_string( x_padding, y_region, @@ -229,10 +264,23 @@ def word_color(index): # Display is wide enough; render the next 12 words on the right side font_color = word_color(word_index + 12) bg_color = theme.bg_color - if word_index + 12 == button_index and self.ctx.input.buttons_active: + if word_index + 12 == button_index and ( + self.ctx.input.buttons_active or highlight + ): # Flip the color values for the selected word bg_color = font_color font_color = theme.bg_color + self.ctx.display.fill_rectangle( + self.ctx.display.width() >> 1, + y_region if kboard.has_minimal_display else y_region - 2, + rect_width, + ( + word_v_padding + if kboard.has_minimal_display + else word_v_padding + 1 + ), + bg_color, + ) self.ctx.display.draw_string( MINIMAL_PADDING + self.ctx.display.width() // 2, y_region, @@ -245,18 +293,6 @@ def word_color(index): word_index += 1 y_region += word_v_padding - if self.mnemonic_length == 24 and kboard.is_m5stickv and page == 0: - go_txt = "13-24" - else: - go_txt = t("Go") - esc_txt = t("Esc") - menu_index = None - if self.ctx.input.buttons_active and button_index >= ESC_INDEX: - menu_index = button_index - ESC_INDEX - self.draw_proceed_menu( - go_txt, esc_txt, y_region, menu_index, self.valid_checksum - ) - def edit_word(self, index): """Edit a word""" word_txt = str(index + 1) + ". " + self.current_mnemonic[index] @@ -294,16 +330,29 @@ def edit(self): self.ctx.display.clear() self._draw_header() self._map_words(button_index, page) - btn = self.ctx.input.wait_for_fastnav_button() - if btn == BUTTON_TOUCH: - button_index = self.ctx.input.touch.current_index() - if button_index < ESC_INDEX: - if self.mnemonic_length == 24 and button_index % 2 == 1: - button_index //= 2 - button_index += 12 - else: - button_index //= 2 - btn = BUTTON_ENTER + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button() + if btn == BUTTON_TOUCH: + button_index = self.ctx.input.touch.current_index() + if button_index < 0: + continue + if button_index < ESC_INDEX: + if self.mnemonic_length == 24 and button_index % 2 == 1: + button_index = (button_index >> 1) + 12 + else: + button_index >>= 1 + # clear words area to remove any highligh from btn + self.ctx.display.fill_rectangle( + 0, + self.header_offset, + self.ctx.display.width(), + self.ctx.display.height() * 3 // 4 + FONT_HEIGHT // 2, + theme.bg_color, + ) + self._map_words(button_index, page, highlight=True) # highlight + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if button_index == GO_INDEX: if self.mnemonic_length == 24 and kboard.is_m5stickv and page == 0: @@ -316,7 +365,7 @@ def edit(self): if button_index == ESC_INDEX: # Cancel self.ctx.display.clear() - if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2): + if self.prompt(t("Are you sure?"), self.ctx.display.height() >> 1): return None continue new_word = self.edit_word(button_index + page * 12) @@ -324,7 +373,7 @@ def edit(self): self.ctx.display.clear() if self.prompt( str(button_index + page * 12 + 1) + ".\n\n" + new_word + "\n\n", - self.ctx.display.height() // 2, + self.ctx.display.height() >> 1, ): self.current_mnemonic[button_index + page * 12] = new_word self.calculate_checksum() diff --git a/src/krux/pages/mnemonic_loader.py b/src/krux/pages/mnemonic_loader.py index a0c09b81f..9eba83124 100644 --- a/src/krux/pages/mnemonic_loader.py +++ b/src/krux/pages/mnemonic_loader.py @@ -37,6 +37,7 @@ from ..qr import FORMAT_UR from ..key import Key from ..krux_settings import t +from ..settings import CONTEXT_ARROW DIGITS_HEX = "0123456789ABCDEF" @@ -55,9 +56,12 @@ def load_key(self): submenu = Menu( self.ctx, [ - (t("Via Camera"), self.load_key_from_camera), - (t("Via Manual Input"), self.load_key_from_manual_input), - (t("From Storage"), self.load_mnemonic_from_storage), + (t("Via Camera") + CONTEXT_ARROW, self.load_key_from_camera), + ( + t("Via Manual Input") + CONTEXT_ARROW, + self.load_key_from_manual_input, + ), + (t("From Storage") + CONTEXT_ARROW, self.load_mnemonic_from_storage), ], ) index, status = submenu.run_loop() @@ -71,13 +75,16 @@ def load_key_from_camera(self): self.ctx, [ (t("QR Code"), self.load_key_from_qr_code), - ("Tinyseed", lambda: self.load_key_from_tiny_seed_image("Tinyseed")), ( - "OneKey KeyTag", + "Tinyseed" + CONTEXT_ARROW, + lambda: self.load_key_from_tiny_seed_image("Tinyseed"), + ), + ( + "OneKey KeyTag" + CONTEXT_ARROW, lambda: self.load_key_from_tiny_seed_image("OneKey KeyTag"), ), ( - t("Binary Grid"), + t("Binary Grid") + CONTEXT_ARROW, lambda: self.load_key_from_tiny_seed_image("Binary Grid"), ), ], @@ -93,8 +100,8 @@ def load_key_from_manual_input(self): self.ctx, [ (t("Words"), self.load_key_from_text), - (t("Word Numbers"), self.pre_load_key_from_digits), - ("Tinyseed (Bits)", self.load_key_from_tiny_seed), + (t("Word Numbers") + CONTEXT_ARROW, self.pre_load_key_from_digits), + ("Tinyseed (Bits)" + CONTEXT_ARROW, self.load_key_from_tiny_seed), ("Stackbit 1248", self.load_key_from_1248), ], ) diff --git a/src/krux/pages/qr_view.py b/src/krux/pages/qr_view.py index 9abe9c15e..330af7567 100644 --- a/src/krux/pages/qr_view.py +++ b/src/krux/pages/qr_view.py @@ -25,7 +25,7 @@ from . import Page, Menu, MENU_CONTINUE, MENU_EXIT, ESC_KEY from ..themes import theme, WHITE, BLACK, DARKGREY from ..krux_settings import t -from ..settings import THIN_SPACE +from ..settings import THIN_SPACE, CONTEXT_ARROW from ..qr import get_size from ..display import DEFAULT_PADDING, FONT_HEIGHT, M5STICKV_WIDTH from ..input import ( @@ -486,7 +486,7 @@ def save_qr_image_menu(self): lambda: self.save_svg_image(suggested_file_name), ) ) - submenu = Menu(self.ctx, qr_menu, offset=2 * FONT_HEIGHT, back_label=None) + submenu = Menu(self.ctx, qr_menu, offset=2 * FONT_HEIGHT) submenu.run_loop() return MENU_CONTINUE # return MENU_EXIT # Use this to exit QR Viewer after saving @@ -566,7 +566,7 @@ def toggle_brightness(): (t("Return to QR Viewer"), lambda: None), (t("Toggle Brightness"), toggle_brightness), ( - t("Save QR Image to SD Card"), + t("Save QR Image to SD Card") + CONTEXT_ARROW, ( self.save_qr_image_menu if allow_export and self.has_sd_card() diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py index 16045a923..57bdb3c8a 100644 --- a/src/krux/pages/settings_page.py +++ b/src/krux/pages/settings_page.py @@ -31,6 +31,7 @@ SD_PATH, FLASH_PATH, SETTINGS_FILENAME, + CONTEXT_ARROW, ) from ..krux_settings import ( Settings, @@ -40,12 +41,21 @@ ButtonsSettings, t, locale_control, + ThermalSettings, +) +from ..input import ( + BUTTON_ENTER, + BUTTON_PAGE, + BUTTON_PAGE_PREV, + BUTTON_TOUCH, + SWIPE_FAIL, + TOUCH_HIGHLIGHT_MS, ) -from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH from ..sd_card import SDHandler from . import ( Page, Menu, + Context, DIGITS, LETTERS, UPPERCASE_LETTERS, @@ -57,6 +67,7 @@ DEFAULT_PADDING, ) import os +import time from ..kboard import kboard PERSIST_MSG_TIME = 2500 @@ -73,7 +84,7 @@ class SettingsPage(Page): """Class to manage settings interface""" - def __init__(self, ctx): + def __init__(self, ctx: Context): super().__init__(ctx, None) self.ctx = ctx @@ -81,34 +92,47 @@ def settings(self): """Handler for the settings""" return self.namespace(Settings())() - def _draw_settings_pad(self): + def _draw_settings_pad(self, index=-1): """Draws buttons to change settings with touch""" - if kboard.has_touchscreen: - self.ctx.input.touch.clear_regions() - offset_y = self.ctx.display.height() * 2 // 3 - self.ctx.input.touch.add_y_delimiter(offset_y) - self.ctx.input.touch.add_y_delimiter(offset_y + FONT_HEIGHT * 3) - button_width = (self.ctx.display.width() - 2 * DEFAULT_PADDING) // 3 - for i in range(4): - self.ctx.input.touch.add_x_delimiter(DEFAULT_PADDING + button_width * i) - offset_y += FONT_HEIGHT - keys = ["<", t("Go"), ">"] - for i, x in enumerate(self.ctx.input.touch.x_regions[:-1]): - self.ctx.display.outline( - x, - self.ctx.input.touch.y_regions[0], - button_width - 1, - FONT_HEIGHT * 3, - theme.frame_color, - ) - offset_x = x - offset_x += (button_width - lcd.string_width_px(keys[i])) // 2 - self.ctx.display.draw_string( - offset_x, offset_y, keys[i], theme.fg_color, theme.bg_color + self.ctx.input.touch.clear_regions() + offset_y = self.ctx.display.height() * 2 // 3 + self.ctx.input.touch.add_y_delimiter(offset_y) + self.ctx.input.touch.add_y_delimiter(offset_y + FONT_HEIGHT * 3) + button_width = (self.ctx.display.width() - 2 * DEFAULT_PADDING) // 3 + for i in range(4): + self.ctx.input.touch.add_x_delimiter(DEFAULT_PADDING + button_width * i) + offset_y += FONT_HEIGHT + keys = ["<", t("Go"), ">"] + for i, x in enumerate(self.ctx.input.touch.x_regions[:-1]): + self.ctx.display.outline( + x, + self.ctx.input.touch.y_regions[0], + button_width - 1, + FONT_HEIGHT * 3, + theme.frame_color, + ) + offset_x = x + offset_x += (button_width - lcd.string_width_px(keys[i])) // 2 + fg_color = theme.fg_color if i % 2 == 0 else theme.go_color + bg_color = theme.bg_color + if i == index: + self.ctx.display.fill_rectangle( + x + 1, + self.ctx.input.touch.y_regions[0] + 1, + button_width - 2, + FONT_HEIGHT * 3 - 1, + fg_color, ) + bg_color = fg_color + fg_color = theme.bg_color + self.ctx.display.draw_string( + offset_x, offset_y, keys[i], fg_color, bg_color + ) def _touch_to_physical(self, index): """Mimics touch presses into physical button presses""" + if index < 0: + return BUTTON_TOUCH if index == 0: return BUTTON_PAGE_PREV if index == 1: @@ -255,7 +279,14 @@ def handler(): namespace_list = settings_namespace.namespace_list() items = [ ( - settings_namespace.label(ns.namespace.split(".")[-1]), + settings_namespace.label(ns.namespace.split(".")[-1]) + + ( + CONTEXT_ARROW + if isinstance(ns, ThermalSettings) + or len(ns.setting_list()) > 1 + or len(ns.namespace_list()) > 1 + else "" + ), self.namespace(ns), ) for ns in namespace_list @@ -278,7 +309,9 @@ def handler(): back_status = lambda: MENU_EXIT # pylint: disable=C3001 # Case for "Back" on the main Settings if settings_namespace.namespace == Settings.namespace: - items.append((t("Factory Settings"), self.restore_settings)) + items.append( + (t("Factory Settings"), self.restore_settings, theme.no_esc_color) + ) back_status = self._settings_exit_check # Case for security settings @@ -373,19 +406,33 @@ def category_setting(self, settings_namespace, setting): color, theme.bg_color, ) - self._draw_settings_pad() - btn = self.ctx.input.wait_for_button() - if btn == BUTTON_TOUCH: - btn = self._touch_to_physical(self.ctx.input.touch.current_index()) + if kboard.has_touchscreen: + self._draw_settings_pad() + + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_button() + if btn == BUTTON_TOUCH: + btn = self._touch_to_physical(self.ctx.input.touch.current_index()) if btn == BUTTON_ENTER: + if kboard.has_touchscreen: + self._draw_settings_pad(1) # highlight + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little break new_category = current_category for i, category in enumerate(categories): if current_category == category: if btn in (BUTTON_PAGE, None): + if kboard.has_touchscreen: + self._draw_settings_pad(2) # highlight + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little new_category = categories[(i + 1) % len(categories)] elif btn == BUTTON_PAGE_PREV: + if kboard.has_touchscreen: + self._draw_settings_pad(0) # highlight + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little new_category = categories[(i - 1) % len(categories)] setting.__set__(settings_namespace, new_category) break diff --git a/src/krux/pages/stack_1248.py b/src/krux/pages/stack_1248.py index 0eaa700f2..5a5d09b56 100644 --- a/src/krux/pages/stack_1248.py +++ b/src/krux/pages/stack_1248.py @@ -20,8 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import time from embit.wordlists.bip39 import WORDLIST from . import Page +from . import Context from ..themes import theme from ..krux_settings import t from ..display import DEFAULT_PADDING, MINIMAL_PADDING, FONT_HEIGHT, FONT_WIDTH @@ -32,6 +34,8 @@ BUTTON_TOUCH, FAST_FORWARD, FAST_BACKWARD, + SWIPE_FAIL, + TOUCH_HIGHLIGHT_MS, ) from ..kboard import kboard @@ -43,7 +47,7 @@ class Stackbit(Page): """Class for handling Stackbit 1248 fomat""" - def __init__(self, ctx): + def __init__(self, ctx: Context): super().__init__(ctx, None) self.ctx = ctx self.x_offset = DEFAULT_PADDING @@ -221,7 +225,9 @@ def export_1248(self, word_index, y_offset, word): self.y_offset = 2 * FONT_HEIGHT self.y_pad = FONT_HEIGHT - self.ctx.display.draw_hcentered_text("Stackbit 1248") + self.ctx.display.draw_hcentered_text( + "Stackbit 1248", color=theme.highlight_color + ) self._draw_grid(y_offset) self._draw_labels(y_offset, word_index) digits, digits_str = self._word_to_digits(word) @@ -273,74 +279,82 @@ def _toggle_bit(self, digits, index): def _draw_index(self, index): """Outline index respective""" x_offset = self.x_offset + self.x_pad + 1 - width = 3 * self.x_pad - y_position = index // 7 - y_position *= self.y_pad - y_position += self.y_offset - 1 - color = theme.fg_color + y_pos = self.y_offset - 1 + (index // 7) * self.y_pad + + # clear GO / ESC prev highlighted area + color = theme.bg_color + self.ctx.display.fill_rectangle( + x_offset, + self.y_offset + 5 * self.y_pad, + 6 * self.x_pad, + self.y_pad, + color, + ) + + if index < STACKBIT_ESC_INDEX: + x_pos = x_offset + (index % 7) * self.x_pad + self.ctx.display.outline( + x_pos, + y_pos, + self.x_pad - 2, + self.y_pad, + theme.fg_color, + ) + return + + # GO / ESC button if index >= STACKBIT_GO_INDEX: - x_position = x_offset + 3 * self.x_pad - y_position += 1 + x_pos = x_offset + 3 * self.x_pad color = theme.go_color elif index >= STACKBIT_ESC_INDEX: - x_position = x_offset - y_position += 1 + x_pos = x_offset color = theme.no_esc_color - else: - x_position = index % 7 - x_position *= self.x_pad - x_position += x_offset - width = self.x_pad - 2 - self.ctx.display.outline( - x_position, - y_position, - width, - self.y_pad, - color, + self.ctx.display.fill_rectangle( + x_pos + 2, y_pos + 1, 3 * self.x_pad - 4, self.y_pad, color ) - def _draw_menu(self): + def _draw_menu(self, index, touch_highlight=False): """Draws options to leave and proceed""" y_offset = self.y_offset + 5 * self.y_pad label_y_offset = (self.y_pad - FONT_HEIGHT) // 2 x_offset = self.x_offset + self.x_pad - self.ctx.display.draw_string( - x_offset + 1 * self.x_pad, - y_offset + label_y_offset, + + # Helper to compute highlight state + def _draw_button(cond, x_mul, label, base_color): + highlight = ( + cond if self.ctx.input.buttons_active else cond and touch_highlight + ) + fg_color = theme.bg_color if highlight else base_color + bg_color = base_color if highlight else theme.bg_color + self.ctx.display.draw_string( + round(x_offset + x_mul * self.x_pad), + y_offset + label_y_offset, + t(label), + fg_color, + bg_color, + ) + + # ESC / GO button + _draw_button( + STACKBIT_ESC_INDEX <= index < STACKBIT_GO_INDEX, + 1, t("Esc"), theme.no_esc_color, ) - self.ctx.display.draw_string( - round(x_offset + 4.2 * self.x_pad), - y_offset + label_y_offset, + _draw_button( + index >= STACKBIT_GO_INDEX, + 4.2, t("Go"), theme.go_color, ) - # print border around buttons only on touch devices + if kboard.has_touchscreen: - self.ctx.display.draw_line( - x_offset, - y_offset, - x_offset + 6 * self.x_pad, - y_offset, + self.ctx.display.draw_vline( + self.x_offset + 4 * self.x_pad, + self.y_offset + 5 * self.y_pad + FONT_HEIGHT // 2, + FONT_HEIGHT, theme.frame_color, ) - self.ctx.display.draw_line( - x_offset, - y_offset + self.y_pad, - x_offset + 6 * self.x_pad, - y_offset + self.y_pad, - theme.frame_color, - ) - for _ in range(3): - self.ctx.display.draw_line( - x_offset, - y_offset, - x_offset, - y_offset + self.y_pad, - theme.frame_color, - ) - x_offset += 3 * self.x_pad def digits_to_word(self, digits): """Returns seed word respective to digits BIP39 dictionaty position""" @@ -392,9 +406,10 @@ def index(self, index, btn): return STACKBIT_GO_INDEX if index <= STACKBIT_MAX_INDEX: return page_prev_move[index] - if index <= STACKBIT_ESC_INDEX: + if index < STACKBIT_GO_INDEX: return STACKBIT_MAX_INDEX - return STACKBIT_ESC_INDEX + return STACKBIT_ESC_INDEX + return index def enter_1248(self): """UI to manually enter a Stackbit 1248""" @@ -418,15 +433,27 @@ def enter_1248(self): y_offset = self.y_offset self._draw_grid(y_offset) self._draw_labels(y_offset, word_index) - self._draw_menu() if self.ctx.input.buttons_active: self._draw_index(index) + self._draw_menu(index) self.preview_word(digits) self._draw_punched(digits, y_offset) - btn = self.ctx.input.wait_for_fastnav_button() - if btn == BUTTON_TOUCH: - btn = BUTTON_ENTER - index = self.ctx.input.touch.current_index() + + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button() + if btn == BUTTON_TOUCH: + index = self.ctx.input.touch.current_index() + if index < 0 or 13 < index < STACKBIT_ESC_INDEX: + continue + + # Highlight the touched btn + self._draw_index(index) + self._draw_menu(index, touch_highlight=True) + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little + + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if index >= STACKBIT_GO_INDEX: # go word = self.digits_to_word(digits) @@ -455,13 +482,11 @@ def enter_1248(self): self.ctx.display.clear() if self.prompt(t("Done?"), self.ctx.display.height() // 2): break - # self._map_keys_array() #can be removed? word_index += 1 elif index >= STACKBIT_ESC_INDEX: # ESC self.ctx.display.clear() if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2): break - # self._map_keys_array() elif index < 14: digits = self._toggle_bit(digits, index) else: diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 8cbf29ee0..08f921b9a 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -43,6 +43,9 @@ BUTTON_TOUCH, FAST_FORWARD, FAST_BACKWARD, + SWIPE_FAIL, + SWIPE_LEFT, + TOUCH_HIGHLIGHT_MS, ) from ..bip39 import entropy_checksum from ..kboard import kboard @@ -93,7 +96,7 @@ def _draw_grid(self): def _draw_labels(self, page): """Draws labels for import and export Tinyseed UI""" - self.ctx.display.draw_hcentered_text(self.label) + self.ctx.display.draw_hcentered_text(self.label, color=theme.highlight_color) # For non‑minimal displays, show extra bit numbers (rotate to landscape temporarily) if not kboard.has_minimal_display: self.ctx.display.to_landscape() @@ -248,7 +251,7 @@ def _draw_index(self, index): """Outline index position""" height = self.y_pad - 2 y_pos = (index // 12) * self.y_pad + self.y_offset + 1 - if index < TS_LAST_BIT_NO_CS: + if index < TS_ESC_START_POSITION: x_pos = (index % 12) * self.x_pad + self.x_offset + 1 width = self.x_pad - 2 self.ctx.display.outline(x_pos, y_pos, width, height, theme.fg_color) @@ -339,6 +342,8 @@ def _last_editable_bit(): ) elif index <= TS_GO_POSITION: index = TS_ESC_END_POSITION + elif btn == SWIPE_LEFT: + index = TS_GO_POSITION return index def enter_tiny_seed(self, w24=False, seed_numbers=None, scanning_24=False): @@ -359,6 +364,8 @@ def _editable_bit(): self._map_keys_array() page = 0 menu_offset = self.y_offset + 12 * self.y_pad + go_str = t("Go") + no_str = t("Esc") while True: self._draw_labels(page) self._draw_grid() @@ -366,19 +373,44 @@ def _editable_bit(): self._draw_disabled(w24) tiny_seed_numbers = self._auto_checksum(tiny_seed_numbers) self._draw_punched(tiny_seed_numbers, page) - menu_index = ( + esc_go_index = ( 1 - if index >= TS_GO_POSITION + if index > TS_ESC_END_POSITION else (0 if index >= TS_ESC_START_POSITION else None) ) - self.draw_proceed_menu(t("Go"), t("Esc"), menu_offset, menu_index) + self.draw_proceed_menu(go_str, no_str, menu_offset, esc_go_index) if self.ctx.input.buttons_active: self._draw_index(index) - btn = self.ctx.input.wait_for_fastnav_button() - if btn == BUTTON_TOUCH: - btn = BUTTON_ENTER - index = self.ctx.input.touch.current_index() + # wait until valid input is captured + btn = BUTTON_TOUCH + while btn in (BUTTON_TOUCH, SWIPE_FAIL): + btn = self.ctx.input.wait_for_fastnav_button() + if btn == BUTTON_TOUCH: + index = self.ctx.input.touch.current_index() + # Ignore clicks on invalid indexes (avoids redraw screen) + disabled_indexes = 4 if not w24 else (8 if page else 0) + if ( + index < 0 + or TS_LAST_BIT_NO_CS - disabled_indexes + < index + < TS_ESC_START_POSITION + ): + continue + + # Highlight the touched btn + if index < TS_ESC_START_POSITION: + self._draw_index(index) + else: # "Go" or "Esc" + self.draw_proceed_menu( + go_str, + no_str, + menu_offset, + 1 if index > TS_ESC_END_POSITION else 0, + highlight=True, + ) + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if index > TS_ESC_END_POSITION: # "Go" if not w24 or (w24 and (page or scanning_24)): diff --git a/src/krux/pages/tools.py b/src/krux/pages/tools.py index 4e4c65026..b5179cb7b 100644 --- a/src/krux/pages/tools.py +++ b/src/krux/pages/tools.py @@ -32,6 +32,8 @@ # NUM_SPECIAL_2, ) from ..krux_settings import t +from ..settings import CONTEXT_ARROW +from ..themes import theme # TODO: re-enable "Create a QR Code" (and keypads ^^^) once encryption is possible w/o Datum Tool @@ -46,12 +48,16 @@ def __init__(self, ctx): Menu( ctx, [ - (t("Datum Tool"), self.datum_tool), - (t("Device Tests"), self.device_tests), + (t("Datum Tool") + CONTEXT_ARROW, self.datum_tool), + (t("Device Tests") + CONTEXT_ARROW, self.device_tests), # (t("Create QR Code"), self.create_qr), (t("Descriptor Addresses"), self.descriptor_addresses), - (t("Flash Tools"), self.flash_tools), - (t("Remove Mnemonic"), self.rm_stored_mnemonic), + (t("Flash Tools") + CONTEXT_ARROW, self.flash_tools), + ( + t("Remove Mnemonic") + CONTEXT_ARROW, + self.rm_stored_mnemonic, + theme.no_esc_color, + ), ], ), ) diff --git a/src/krux/pages/wallet_settings.py b/src/krux/pages/wallet_settings.py index 459d6f3b3..032d0e82d 100644 --- a/src/krux/pages/wallet_settings.py +++ b/src/krux/pages/wallet_settings.py @@ -39,6 +39,7 @@ SINGLESIG_SCRIPT_PURPOSE, MULTISIG_SCRIPT_PURPOSE, MINISCRIPT_PURPOSE, + MINISCRIPT_SCRIPT_MAP, TYPE_SINGLESIG, TYPE_MULTISIG, TYPE_MINISCRIPT, @@ -47,10 +48,7 @@ NAME_MINISCRIPT, ) -from ..settings import ( - MAIN_TXT, - TEST_TXT, -) +from ..settings import MAIN_TXT, TEST_TXT, CONTEXT_ARROW from ..key import P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR @@ -221,9 +219,9 @@ def customize_wallet(self, key): submenu = Menu( self.ctx, [ - (t("Network"), lambda: None), - (t("Policy Type"), lambda: None), - (t("Script Type"), lambda: None), + (t("Network") + CONTEXT_ARROW, lambda: None), + (t("Policy Type") + CONTEXT_ARROW, lambda: None), + (t("Script Type") + CONTEXT_ARROW, lambda: None), (account_txt, lambda: None), ], offset=info_len * FONT_HEIGHT + DEFAULT_PADDING, @@ -245,7 +243,7 @@ def customize_wallet(self, key): derivation_path = "" network = new_network elif index == 1: - new_policy_type = self._policy_type() + new_policy_type = self._policy_type(policy_type, script_type) if new_policy_type is not None: derivation_path = "" policy_type = new_policy_type @@ -265,9 +263,9 @@ def customize_wallet(self, key): script_type = self._script_type_multisig() script_type = P2WSH if script_type is None else script_type - elif policy_type == TYPE_MINISCRIPT and script_type not in ( - P2WSH, - P2TR, + elif ( + policy_type == TYPE_MINISCRIPT + and script_type not in MINISCRIPT_SCRIPT_MAP.values() ): # If is miniscript, pick P2WSH or P2TR script_type = self._miniscript_type() @@ -314,15 +312,29 @@ def _coin_type(self): return None return NETWORKS[TEST_TXT] if index == 1 else NETWORKS[MAIN_TXT] - def _policy_type(self): + def _policy_type(self, curr_policy, curr_script): """Policy type selection menu""" + items = [ + [NAME_SINGLE_SIG, lambda: MENU_EXIT], + [NAME_MULTISIG, lambda: MENU_EXIT], + [NAME_MINISCRIPT + " (Experimental)", lambda: MENU_EXIT], + ] + # add context arrow where appropriate + if ( + curr_policy != TYPE_SINGLESIG + and curr_script not in SINGLESIG_SCRIPT_PURPOSE + ): + items[TYPE_SINGLESIG][0] = items[TYPE_SINGLESIG][0] + CONTEXT_ARROW + if curr_policy != TYPE_MULTISIG and curr_script not in MULTISIG_SCRIPT_PURPOSE: + items[TYPE_MULTISIG][0] = items[TYPE_MULTISIG][0] + CONTEXT_ARROW + if ( + curr_policy != TYPE_MINISCRIPT + and curr_script not in MINISCRIPT_SCRIPT_MAP.values() + ): + items[TYPE_MINISCRIPT][0] = items[TYPE_MINISCRIPT][0] + CONTEXT_ARROW submenu = Menu( self.ctx, - [ - (NAME_SINGLE_SIG, lambda: MENU_EXIT), - (NAME_MULTISIG, lambda: MENU_EXIT), - (NAME_MINISCRIPT + " (Experimental)", lambda: MENU_EXIT), - ], + items, disable_statusbar=True, ) index, _ = submenu.run_loop() diff --git a/src/krux/settings.py b/src/krux/settings.py index f19eb609e..6ad2faa31 100644 --- a/src/krux/settings.py +++ b/src/krux/settings.py @@ -41,6 +41,8 @@ THIN_SPACE = " " # "\u2009" ELLIPSIS = "…" # "\u2026" +CONTEXT_ARROW = THIN_SPACE + ">" +BACK_ARROW = "<" + THIN_SPACE class SettingsNamespace: diff --git a/src/krux/touch.py b/src/krux/touch.py index 3bdece846..71506711f 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -30,13 +30,16 @@ PRESSED = 1 RELEASED = 2 +SWIPE_DURATION_MS = 750 SWIPE_THRESHOLD = 35 SWIPE_RIGHT = 1 SWIPE_LEFT = 2 SWIPE_UP = 3 SWIPE_DOWN = 4 +SWIPE_NONE = 5 TOUCH_S_PERIOD = 20 # Touch sample period - Min = 10 +EDGE_PIXELS = 1 # The ammount of pixels to determine the edges of a region class Touch: @@ -47,6 +50,7 @@ def __init__(self, width, height, irq_pin=None, res_pin=None): For Krux width = max_y, height = max_x """ self.sample_time = 0 + self.pressed_time = 0 self.y_regions = [] self.x_regions = [] self.index = 0 @@ -143,30 +147,42 @@ def valid_position(self, data): def _extract_index(self, data): """ Gets an index from touched points, x and y delimiters. - The index is calculated based on the position of the touch within the defined regions. + Return index or -1 if touching an edge. """ - y_index = 0 - x_index = 0 + x, y = data - # Calculate y index - for region in self.y_regions: - if data[1] > region: - y_index += 1 - y_index -= 1 if y_index > 0 else 0 + # Helper to deal with X/Y regions + def _compute_axis_index(pos, regions): + if not regions: + return 0 - # Calculate x index if x regions are defined (2D array) + # Count how many region boundaries pos passed + idx = sum(pos + EDGE_PIXELS >= r for r in regions) + + # # Check boundary at idx-1 (left-side) + if 1 < idx < len(regions) and abs(pos - regions[idx - 1]) <= EDGE_PIXELS: + return -1 + + # Valid is 0<= idx <= len(regions) -2 [valid regions] + return max(min(idx - 1, len(regions) - 2), 0) + + # Y index + y_index = _compute_axis_index(y, self.y_regions) + if y_index < 0: + return -1 + + # X index if self.x_regions: - for x_region in self.x_regions: - if data[0] >= x_region: - x_index += 1 - x_index -= 1 # Adjust index to be zero-based - # Combine y and x indices to get the final index - index = y_index * (len(self.x_regions) - 1) + x_index - else: - index = y_index + x_index = _compute_axis_index(x, self.x_regions) + if x_index < 0: + return -1 + # self.highlight_region( + # y_index * (len(self.x_regions) - 1) + x_index, y_index + # ) + return y_index * (len(self.x_regions) - 1) + x_index - # self.highlight_region(x_index, y_index) - return index + # self.highlight_region(0, y_index) + return y_index def set_regions(self, x_list=None, y_list=None): """Set buttons map regions x and y""" @@ -222,31 +238,37 @@ def current_state(self): data = self.touch_driver.current_point() if isinstance(data, tuple): self._store_points(data) - elif data is None: # gets release then return to idle. + return self.state + + if data is None: # gets release then return to idle. if self.state == RELEASED: # On touch release self.state = IDLE - elif self.state == PRESSED: - if self.release_point is not None: - lateral_lenght = self.release_point[0] - self.press_point[0][0] - if lateral_lenght > SWIPE_THRESHOLD: - self.gesture = SWIPE_RIGHT - elif -lateral_lenght > SWIPE_THRESHOLD: - self.gesture = SWIPE_LEFT - lateral_lenght *= -1 # make it positive value - vertical_lenght = self.release_point[1] - self.press_point[0][1] - if ( - vertical_lenght > SWIPE_THRESHOLD - and vertical_lenght > lateral_lenght - ): - self.gesture = SWIPE_DOWN - elif ( - -vertical_lenght > SWIPE_THRESHOLD - and -vertical_lenght > lateral_lenght - ): - self.gesture = SWIPE_UP + return self.state + + if self.state == PRESSED: self.state = RELEASED - else: - print("Touch error") + + if self.release_point is not None: + dx = self.release_point[0] - self.press_point[0][0] + dy = self.release_point[1] - self.press_point[0][1] + + if abs(dx) > SWIPE_THRESHOLD or abs(dy) > SWIPE_THRESHOLD: + # discard swipes that took more than ~1s + if self.sample_time - self.pressed_time < SWIPE_DURATION_MS: + # discards swipes with angle > 27 degrees + if abs(dx) > abs(dy) * 2: + self.gesture = SWIPE_LEFT if dx < 0 else SWIPE_RIGHT + elif abs(dy) > abs(dx) * 2: + self.gesture = SWIPE_UP if dy < 0 else SWIPE_DOWN + else: + self.gesture = SWIPE_NONE # undetermined diagonal swipe + else: + self.gesture = ( + SWIPE_NONE # hold finger on screen for too long + ) + return self.state + + print("Touch error") return self.state def event(self, validate_position=True): @@ -261,6 +283,7 @@ def event(self, validate_position=True): if isinstance(self.touch_driver.irq_point, tuple): if self.valid_position(self.touch_driver.irq_point): self._store_points(self.touch_driver.irq_point) + self.pressed_time = time.ticks_ms() return True return False @@ -268,33 +291,31 @@ def value(self): """Wraps touch states to behave like a regular button""" return 1 if self.current_state() == IDLE else 0 - def swipe_right_value(self): - """Returns detected gestures and clean respective variable""" - if self.gesture == SWIPE_RIGHT: + def _swipe_state_check(self, swipe_type): + if self.gesture == swipe_type: self.gesture = None return 0 return 1 + def swipe_none_value(self): + """Returns detected gestures and clean respective variable""" + return self._swipe_state_check(SWIPE_NONE) + + def swipe_right_value(self): + """Returns detected gestures and clean respective variable""" + return self._swipe_state_check(SWIPE_RIGHT) + def swipe_left_value(self): """Returns detected gestures and clean respective variable""" - if self.gesture == SWIPE_LEFT: - self.gesture = None - return 0 - return 1 + return self._swipe_state_check(SWIPE_LEFT) def swipe_up_value(self): """Returns detected gestures and clean respective variable""" - if self.gesture == SWIPE_UP: - self.gesture = None - return 0 - return 1 + return self._swipe_state_check(SWIPE_UP) def swipe_down_value(self): """Returns detected gestures and clean respective variable""" - if self.gesture == SWIPE_DOWN: - self.gesture = None - return 0 - return 1 + return self._swipe_state_check(SWIPE_DOWN) def current_index(self): """Returns current index of last touched point""" diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py index fffc9af5f..303bc20ce 100644 --- a/src/krux/translations/__init__.py +++ b/src/krux/translations/__init__.py @@ -105,7 +105,6 @@ 4150351825, 3278654271, 3895447625, - 1664326073, 3836852788, 690625786, 382368239, diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py index de8ed8f28..769c377b6 100644 --- a/src/krux/translations/de.py +++ b/src/krux/translations/de.py @@ -93,7 +93,6 @@ "Gerätetests", "Bildschirm", "Schalten Sie das Gerät nicht aus, es kann eine Weile dauern.", - "Konvertierung abgeschlossen", "Fertig?", "Doppelte Gedächtnisstütze", "Driver", diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py index 99c7e042b..44cda1b2a 100644 --- a/src/krux/translations/es.py +++ b/src/krux/translations/es.py @@ -93,7 +93,6 @@ "Pruebas del dispositivo", "Pantalla", "No apagues el dispositivo, puede tardar un tiempo en completarse.", - "Listo para convertir", "¿Listo?", "Doble mnemónico", "Operador", diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py index 171e09fa8..e5a9a2e58 100644 --- a/src/krux/translations/fr.py +++ b/src/krux/translations/fr.py @@ -93,7 +93,6 @@ "Tests de l'appareil", "Affichage", "Ne pas éteindre, cela peut prendre un certain temps.", - "Conversion terminée", "Terminé\u2009?", "Double mnémonique", "Pilote", diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py index f6e79046e..4969a8c1b 100644 --- a/src/krux/translations/ja.py +++ b/src/krux/translations/ja.py @@ -93,7 +93,6 @@ "デバイステスト", "ディスプレイ", "完了するまで電源を切らないでください.", - "変換を完了する", "完了?", "ダブルニーモニック", "ドライバー", diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py index b4f44d73c..995d7913a 100644 --- a/src/krux/translations/ko.py +++ b/src/krux/translations/ko.py @@ -93,7 +93,6 @@ "장치 테스트", "디스플레이", "전원을 끄지 마십시오. 완료하는 데 시간이 걸릴 수 있습니다.", - "변환 완료", "완료되었습니까?", "이중 니모닉", "드라이버", diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py index 2869e8126..8e880053a 100644 --- a/src/krux/translations/nl.py +++ b/src/krux/translations/nl.py @@ -93,7 +93,6 @@ "Apparaattests", "Weergave", "Schakel het apparaat niet uit, het kan even duren voordat het klaar is.", - "Converteren gedaan.\r", "Klaar?", "Dubbel geheugensteuntje", "Driver", diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py index ed6f183f8..1bb7e33fc 100644 --- a/src/krux/translations/pt.py +++ b/src/krux/translations/pt.py @@ -93,7 +93,6 @@ "Testes do Dispositivo", "Display", "Não desligue, pode demorar um pouco para concluir.", - "Concluída a conversão", "Concluído?", "Mnemônico duplo", "Driver", diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py index 5440ebbbb..0b66b4df2 100644 --- a/src/krux/translations/ru.py +++ b/src/krux/translations/ru.py @@ -93,7 +93,6 @@ "Испытания устройства", "Дисплеи", "Не выключайте питание, это может занять некоторое время.", - "Конвертация завершена", "Готово?", "Двойная мнемоника", "Драйвер", diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py index 37b187608..63c6bcccf 100644 --- a/src/krux/translations/tr.py +++ b/src/krux/translations/tr.py @@ -93,7 +93,6 @@ "Cihaz Testleri", "Ekran", "Kapatmayın, tamamlanması biraz zaman alabilir.", - "Dönüştürme Tamamlandı", "Tamamlandı mı?", "Çifte anımsatıcı", "Sürücü", diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py index 11d7f4256..d0c844492 100644 --- a/src/krux/translations/vi.py +++ b/src/krux/translations/vi.py @@ -93,7 +93,6 @@ "Kiểm tra thiết bị", "Hiển thị", "Không được tắt máy, có thể mất một lúc để hoàn thành.", - "Hoàn tất chuyển đổi", "Hoàn tất?", "Từ gợi nhớ kép", "Driver", diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py index bf187d37c..8c8053a4d 100644 --- a/src/krux/translations/zh.py +++ b/src/krux/translations/zh.py @@ -93,7 +93,6 @@ "设备测试", "显示", "请勿断电,可能需要一段时间完成.", - "完成转换", "完成了吗?", "双重助记词", "驱动程序", diff --git a/tests/pages/test_datum_tool.py b/tests/pages/test_datum_tool.py index 36b3c4d51..0b750e836 100644 --- a/tests/pages/test_datum_tool.py +++ b/tests/pages/test_datum_tool.py @@ -502,8 +502,7 @@ def test_datumtool_view_qr(m5stickv, mocker): # with longer text for big pMofN animated qr BTN_SEQUENCE = ( - BUTTON_PAGE, # to pMofN - BUTTON_ENTER, # go pMofN + BUTTON_ENTER, # go pMofN (static not available!) BUTTON_ENTER, # leave QR view ) ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -515,6 +514,7 @@ def test_datumtool_view_qr(m5stickv, mocker): # with longer text for UR-bytes BTN_SEQUENCE = ( + BUTTON_PAGE_PREV, # to < Back BUTTON_PAGE_PREV, # to UR-bytes BUTTON_ENTER, # go UR-bytes BUTTON_ENTER, # leave QR view @@ -529,6 +529,7 @@ def test_datumtool_view_qr(m5stickv, mocker): # with longer text for big UR-psbt BTN_SEQUENCE = ( + BUTTON_PAGE_PREV, # to < Back BUTTON_PAGE_PREV, # to UR-bytes BUTTON_PAGE_PREV, # to UR-psbt BUTTON_ENTER, # go UR-psbt @@ -544,6 +545,7 @@ def test_datumtool_view_qr(m5stickv, mocker): # with longer text for big BBQr BTN_SEQUENCE = ( + BUTTON_PAGE_PREV, # to < Back BUTTON_PAGE_PREV, # to UR-bytes BUTTON_PAGE_PREV, # to UR-psbt BUTTON_PAGE_PREV, # to BBQr @@ -560,6 +562,7 @@ def test_datumtool_view_qr(m5stickv, mocker): # with short latin-1 as a string works BTN_SEQUENCE = ( + BUTTON_PAGE_PREV, # to < Back BUTTON_PAGE_PREV, # deny label update BUTTON_ENTER, # dismiss QR BUTTON_PAGE_PREV, # to Back @@ -590,9 +593,8 @@ def test_datumtool_view_qr(m5stickv, mocker): print(ctx.display.method_calls) # but can work if encoding unicode string as utf-8 bytes + # BIG QR, has options: static, p M of N and ur bytes BTN_SEQUENCE = ( - BUTTON_PAGE_PREV, # deny label update - BUTTON_ENTER, # dismiss QR BUTTON_PAGE_PREV, # to Back BUTTON_ENTER, # leave QR view ) @@ -837,6 +839,7 @@ def test_datumtool__decrypt_as_kef_envelope(m5stickv, mocker): def test_datumtool__build_options_menu(m5stickv, mocker): """With DatumTool already initialized, test ._build_options_menu()""" from krux.pages.datum_tool import DatumTool + from krux.settings import CONTEXT_ARROW some_chars = "This are characters" some_hex_plus = "deadbeef3456" @@ -850,7 +853,7 @@ def test_datumtool__build_options_menu(m5stickv, mocker): assert ctx.input.wait_for_button.call_count == 0 assert [name for name, func in menu] == [ "Show Datum", - "Convert Datum", + "Convert Datum" + CONTEXT_ARROW, "QR Code", "Save to SD card", ] @@ -862,7 +865,7 @@ def test_datumtool__build_options_menu(m5stickv, mocker): page._analyze_contents() menu = page._build_options_menu(offer_convert=True, offer_show=False) assert ctx.input.wait_for_button.call_count == 0 - assert [name for name, func in menu] == ["from utf8", "Done Converting"] + assert [name for name, func in menu] == ["from utf8"] # w/ HEX_plus content, w/ offer_convert and w/o offer_show ctx = create_ctx(mocker, []) @@ -878,7 +881,6 @@ def test_datumtool__build_options_menu(m5stickv, mocker): "from base43", "from base64", "from utf8", - "Done Converting", ] # w/ hex_plus content, w/ offer_convert and w/o offer_show @@ -893,7 +895,6 @@ def test_datumtool__build_options_menu(m5stickv, mocker): "shift case", "from base64", "from utf8", - "Done Converting", ] # w/ bytes content, w/ offer_convert and w/o offer_show @@ -909,8 +910,7 @@ def test_datumtool__build_options_menu(m5stickv, mocker): "to base43", "to base64", "to utf8", - "Encrypt", - "Done Converting", + "Encrypt" + CONTEXT_ARROW, ] # w/ hex_plus-ish bytes content and history, w/ offer_convert and w/o offer_show @@ -927,8 +927,7 @@ def test_datumtool__build_options_menu(m5stickv, mocker): "to base43", "to base64", "to utf8", - "Encrypt", - "Done Converting", + "Encrypt" + CONTEXT_ARROW, ] diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py index 17f5cca7a..06bde7be3 100644 --- a/tests/pages/test_encryption_ui.py +++ b/tests/pages/test_encryption_ui.py @@ -45,6 +45,18 @@ def mock_file_operations(mocker): mocker.patch("builtins.open", mocker.mock_open(read_data="SEEDS_JSON")) +def test_back_load_key_from_keypad(m5stickv, mocker): + from krux.pages.encryption_ui import EncryptionKey + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = [BUTTON_PAGE_PREV] + [BUTTON_ENTER] # go to back # enter back + ctx = create_ctx(mocker, BTN_SEQUENCE) + key_generator = EncryptionKey(ctx) + key = key_generator.encryption_key() + assert key == None + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + + def test_load_key_from_keypad(m5stickv, mocker): from krux.pages.encryption_ui import EncryptionKey from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV @@ -714,6 +726,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): from krux.baseconv import base_encode from krux.pages.encryption_ui import decrypt_kef, KEFEnvelope from krux.input import BUTTON_PAGE_PREV + from krux.themes import theme # setup data: a fake kef envelope, non-kef data, decrypt-evidence, and responding "No" to "Decrypt?" fake_kef = kef.wrap(b"", 0, 10000, bytes([i * 8 for i in range(32)])) @@ -728,7 +741,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test w/ non-kef bytes") ctx = create_ctx(mocker, []) @@ -737,7 +752,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test w/ kef hex") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -746,7 +761,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef hex") ctx = create_ctx(mocker, []) @@ -755,7 +772,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid hex-ish str") ctx = create_ctx(mocker, []) @@ -764,7 +781,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with kef HEX") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -773,7 +790,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef HEX") ctx = create_ctx(mocker, []) @@ -782,7 +801,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid HEX-ish str") ctx = create_ctx(mocker, []) @@ -791,7 +810,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with kef base32") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -800,7 +819,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef base32") ctx = create_ctx(mocker, []) @@ -809,7 +830,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid base32-ish str") ctx = create_ctx(mocker, []) @@ -818,7 +839,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with kef base43") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -827,7 +848,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef base43") ctx = create_ctx(mocker, []) @@ -836,7 +859,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid base43-ish str") ctx = create_ctx(mocker, []) @@ -845,7 +868,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with kef base64") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -854,7 +877,9 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - ctx.display.to_lines.assert_called_with(evidence) + ctx.display.draw_hcentered_text.assert_called_with( + evidence, 120, theme.fg_color, theme.bg_color, highlight_prefix="" + ) print("test with non-kef base64") ctx = create_ctx(mocker, []) @@ -863,7 +888,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() print("test with invalid base64-ish str") ctx = create_ctx(mocker, BTN_SEQUENCE) @@ -872,7 +897,7 @@ def test_decrypt_kef_offers_decrypt_ui_appropriately(m5stickv, mocker): except ValueError: pass assert ctx.input.wait_for_button.call_count == 0 - ctx.display.to_lines.assert_not_called() + ctx.display.draw_hcentered_text.assert_not_called() def test_prompt_for_text_update_dflt_via_yes(m5stickv, mocker): diff --git a/tests/pages/test_keypads.py b/tests/pages/test_keypads.py index c544f5ec8..356f48391 100644 --- a/tests/pages/test_keypads.py +++ b/tests/pages/test_keypads.py @@ -19,3 +19,13 @@ def test_button_turbo(mocker, m5stickv): ctx.input.page_prev_value = mocker.MagicMock(side_effect=[PRESSED, None]) keypad.navigate(FAST_BACKWARD) keypad._previous_key.assert_called() + + +def test_invalid_touch_index(mocker, amigo): + from krux.pages.keypads import Keypad + from krux.input import BUTTON_TOUCH + + ctx = create_ctx(mocker, [BUTTON_TOUCH], touch_seq=[-1]) + keypad = Keypad(ctx, "abc") + btn = keypad.touch_to_physical() + assert keypad.cur_key_index == 0 diff --git a/tests/pages/test_menu.py b/tests/pages/test_menu.py index cf6d821b9..f50092469 100644 --- a/tests/pages/test_menu.py +++ b/tests/pages/test_menu.py @@ -122,6 +122,16 @@ def exception_raiser(): assert status == MENU_SHUTDOWN assert ctx.input.wait_for_fastnav_button.call_count == call_count + # Check invalid touch index don't change result + BTN_SEQUENCE.insert(1, BUTTON_TOUCH) + call_count += len(BTN_SEQUENCE) + # invalid touch index + mocker.patch.object(ctx.input.touch, "current_index", new=lambda: -1) + ctx.input.wait_for_fastnav_button.side_effect = BTN_SEQUENCE + index, status = menu.run_loop() + assert status == MENU_SHUTDOWN + assert ctx.input.wait_for_fastnav_button.call_count == call_count + mocker.patch.object(ctx.input.touch, "current_index", new=lambda: 1) mocker.patch.object(ctx.input, "buttons_active", False) diff --git a/tests/pages/test_mnemonic_editor.py b/tests/pages/test_mnemonic_editor.py index 19c0a292f..f7d71675e 100644 --- a/tests/pages/test_mnemonic_editor.py +++ b/tests/pages/test_mnemonic_editor.py @@ -269,6 +269,7 @@ def test_edit_existing_mnemonic_using_touch(mocker, amigo): 1, 1, 1, # Confirm cabbage + -1, # Try a swipe return invalid index 25, # Try to "Go" with invalid checksum word 23, # index 23 = word 24 22, # Type w, i, t -> witness diff --git a/tests/pages/test_settings_page.py b/tests/pages/test_settings_page.py index 1222640ce..5aff50538 100644 --- a/tests/pages/test_settings_page.py +++ b/tests/pages/test_settings_page.py @@ -266,6 +266,7 @@ def test_settings_on_amigo_tft(amigo, mocker, mocker_printer): PREV_INDEX = 0 GO_INDEX = 1 NEXT_INDEX = 2 + INVALID_INDEX = -1 # SWIPE HARDWARE_INDEX = 2 LOCALE_INDEX = 3 @@ -331,6 +332,9 @@ def test_settings_on_amigo_tft(amigo, mocker, mocker_printer): LOCALE_INDEX, # Change Locale NEXT_INDEX, + NEXT_INDEX, + INVALID_INDEX, + PREV_INDEX, GO_INDEX, ), [ diff --git a/tests/pages/test_stackbit.py b/tests/pages/test_stackbit.py index e695db784..19a661f75 100644 --- a/tests/pages/test_stackbit.py +++ b/tests/pages/test_stackbit.py @@ -122,8 +122,10 @@ def test_enter_stackbit_touch(amigo, mocker): from krux.input import BUTTON_TOUCH YES = 1 - BTN_SEQUENCE = [BUTTON_TOUCH] * 3 * 12 + [BUTTON_TOUCH] - TOUCH_SEQUENCE = [0, STACKBIT_GO_INDEX + 1, YES] * 12 + [YES] + BTN_SEQUENCE = [BUTTON_TOUCH] * 4 * 12 + [BUTTON_TOUCH] + TOUCH_SEQUENCE = [0, -1, STACKBIT_GO_INDEX + 1, YES] * 12 + [ + YES + ] # negative values (invalid touches) should not change the result TEST_12_WORDS = "language language language language language language language language language language language language" ctx = create_ctx(mocker, BTN_SEQUENCE, touch_seq=TOUCH_SEQUENCE) @@ -181,3 +183,15 @@ def test_entering_stackbit_buttons_turbo(mocker, m5stickv): stackbit.enter_1248() stackbit.index.assert_called_with(0, FAST_BACKWARD) + + +def test_stackbit_index_ignore_swipe(mocker, amigo): + from krux.pages.stack_1248 import Stackbit + from krux.input import SWIPE_LEFT, SWIPE_RIGHT, SWIPE_DOWN, SWIPE_UP, SWIPE_FAIL + + ctx = create_ctx(mocker, []) + stackbit = Stackbit(ctx) + tmp = 10 + for swipe in (SWIPE_LEFT, SWIPE_RIGHT, SWIPE_DOWN, SWIPE_UP, SWIPE_FAIL): + new_index = stackbit.index(tmp, swipe) + assert new_index == tmp diff --git a/tests/pages/test_tiny_seed.py b/tests/pages/test_tiny_seed.py index 42fe57273..8f86ef964 100644 --- a/tests/pages/test_tiny_seed.py +++ b/tests/pages/test_tiny_seed.py @@ -122,6 +122,16 @@ def test_enter_tiny_seed_button_turbo(mocker, m5stickv): tiny_seed._new_index.assert_called_with(0, FAST_BACKWARD, False, 0) +def test_next_index(mocker, m5stickv): + from krux.pages.tiny_seed import TinySeed, TS_GO_POSITION + from krux.input import SWIPE_LEFT + + ctx = create_ctx(mocker, []) + tiny = TinySeed(ctx) + index = tiny._new_index(0, SWIPE_LEFT, False, 0) + assert index == TS_GO_POSITION + + def test_enter_tiny_seed_24w_m5stickv(m5stickv, mocker): from krux.pages.tiny_seed import TinySeed from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV @@ -176,6 +186,8 @@ def test_enter_tiny_seed_24w_amigo(amigo, mocker): + [3] # Toggle to last editable bit + [135] + # An invalid index don't change result + + [-1] # Press ESC + [TS_ESC_START_POSITION] # Give up from ESC diff --git a/tests/test_input.py b/tests/test_input.py index bd64a3310..3c1ab6a43 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -373,6 +373,14 @@ def test_swipe_down_value_released_when_none(mocker, m5stickv): assert input.swipe_down_value() == RELEASED +def test_swipe_fail_value_released_when_none(mocker, m5stickv): + from krux.input import Input, RELEASED + + input = Input() + input.touch = None + assert input.swipe_none_value() == RELEASED + + def test_wait_for_release(mocker, m5stickv): import krux from krux.input import Input, RELEASED, PRESSED, BUTTON_ENTER @@ -676,7 +684,14 @@ def mock_points(point1, point2): def test_touch_gestures(mocker, amigo): import krux - from krux.input import Input, SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP, SWIPE_DOWN + from krux.input import ( + Input, + SWIPE_LEFT, + SWIPE_RIGHT, + SWIPE_UP, + SWIPE_DOWN, + SWIPE_FAIL, + ) input = Input() input = reset_input_states(mocker, input) @@ -691,6 +706,7 @@ def mock_points(point1, point2): "current_point", side_effect=[None, point1, point2, None, None], ) + input.touch.pressed_time = time.ticks_ms() # Swipe Right input.touch.clear_regions() @@ -720,6 +736,13 @@ def mock_points(point1, point2): assert btn == SWIPE_DOWN krux.input.wdt.feed.assert_called() + # Swipe Fail + input.touch.clear_regions() + mock_points((75, 50), (150, 100)) + btn = input.wait_for_button(True) + assert btn == SWIPE_FAIL + krux.input.wdt.feed.assert_called() + def test_invalid_touch_delimiter(mocker, amigo): # Tries to add a delimiter outside screen area diff --git a/tests/test_touch.py b/tests/test_touch.py index 3f03ff734..92c2f544b 100644 --- a/tests/test_touch.py +++ b/tests/test_touch.py @@ -14,16 +14,6 @@ def mock_settings(mocker): return mock_settings_obj -@pytest.fixture -def mock_touch_driver(mocker): - """Mock a generic touch driver""" - driver = mocker.MagicMock() - driver.current_point.return_value = None - driver.event.return_value = False - driver.irq_point = None - return driver - - def test_touch_init_ft6x36(mocker, amigo, mock_settings): """Test Touch initialization with FT6X36 driver (default case)""" from krux.touch import Touch @@ -201,11 +191,47 @@ def test_valid_position( [ ([60, 120], [], (100, 50), 0), # y=50 < 60: index 0 ([60, 120], [], (100, 80), 0), # y=80 between 60 and 120: index 0 - ([60, 120], [], (100, 130), 1), # y=130 > 120: index 1 + ([60, 120], [], (100, 130), 0), # y=130 > 120: index 0 (max==len(regions)-2) ([40, 80, 120], [], (50, 30), 0), # y=30 < 40: index 0 ([40, 80, 120], [], (50, 50), 0), # y=50 between 40-80: index 0 ([40, 80, 120], [], (50, 90), 1), # y=90 between 80-120: index 1 - ([40, 80, 120], [], (50, 130), 2), # y=130 > 120: index 2 + ( + [40, 80, 120], + [], + (50, 130), + 1, + ), # y=130 > 120: index 1 (max==len(regions)-2) + ([0, 100, 200], [], (100, 100), -1), # boundary y region: index -1 + ([0, 100, 200], [], (100, 200), 1), # last boundary y region ignore: index 1 + ([], [10, 100, 200], (0, 100), 0), # x=0 < 10: index 0 first region + ([], [10, 100, 200], (50, 100), 0), # x=50 < 100: index 0 still first region + ([], [10, 100, 200], (100, 100), -1), # boundary x region: index -1 + ([], [10, 100, 200], (150, 100), 1), # x=150 > 100: index 1 + ( + [], + [0, 100, 200], + (201, 100), + 1, + ), # x=201 > 200: index 1 (max==len(regions)-2) + ( + [0, 100, 200], + [0, 100, 200], + (100, 100), + -1, + ), # boundary x and y region: index -1 + ([], [], (50, 50), 0), # no regions + ([60, 120], [], (10, 30), 0), # before first y region + ([60, 120], [], (10, 80), 0), # normal y region + ([60, 120], [], (10, 60), 0), # edge of the first y region + ([60, 120], [], (10, 120), 0), # edge of last y region + ( + [60, 120], + [50, 100, 150], + (70, 130), + 0, + ), # y=130 > last region, x=70 > 50 first region + ([60, 120], [50, 100, 150], (100, 130), -1), # boundary x region: index -1 + ([60, 80, 90, 100, 110, 120], [], (0, 91), -1), # boundary with more regions ], ) def test_extract_index( @@ -291,6 +317,7 @@ def test_current_state_released_to_idle(mocker, amigo, mock_settings): ((100, 100), (100, 160), 4), # SWIPE_DOWN (vertical = 60 > lateral) ((100, 160), (100, 100), 3), # SWIPE_UP (vertical = -60 > lateral) ((100, 100), (105, 105), None), # No gesture (< threshold) + ((10, 10), (60, 60), 5), # SWIPE_FAIL diagonal swipe ], ) def test_gesture_detection( @@ -308,12 +335,36 @@ def test_gesture_detection( touch.state = PRESSED touch.press_point = [press_point] touch.release_point = release_point + touch.pressed_time = time.ticks_ms() touch.current_state() assert touch.gesture == expected_gesture +def test_gesture_long_duration_fail( + mocker, + amigo, + mock_settings, +): + """Test swipe gesture fail detection""" + from krux.touch import Touch, PRESSED, SWIPE_NONE + + mock_driver = mocker.MagicMock() + mock_driver.current_point.return_value = None + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", side_effect=[1000, 3000]) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.state = PRESSED + touch.press_point = [(10, 10)] + touch.release_point = (10, 60) + + touch.current_state() + + assert touch.gesture == SWIPE_NONE + + def test_event_with_validation(mocker, amigo, mock_settings): """Test event detection with position validation""" from krux.touch import Touch