From 7fae96503ce554a060fb7f9ac67e92703190fb5b Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sat, 15 Nov 2025 16:04:20 -0300 Subject: [PATCH 01/29] Menu item with context arrows and custom color + other UI improvements --- i18n/translations/de-DE.json | 2 +- i18n/translations/es-MX.json | 2 +- i18n/translations/fr-FR.json | 2 +- i18n/translations/ja-JP.json | 2 +- i18n/translations/ko-KR.json | 2 +- i18n/translations/nl-NL.json | 2 +- i18n/translations/pt-BR.json | 2 +- i18n/translations/ru-RU.json | 2 +- i18n/translations/tr-TR.json | 2 +- i18n/translations/vi-VN.json | 2 +- i18n/translations/zh-CN.json | 2 +- src/krux/krux_settings.py | 7 ++- src/krux/pages/__init__.py | 18 +++++--- src/krux/pages/datum_tool.py | 14 +++--- src/krux/pages/encryption_ui.py | 21 ++++++--- src/krux/pages/flash_tools.py | 2 +- src/krux/pages/home_pages/addresses.py | 8 ++-- src/krux/pages/home_pages/bip85.py | 6 +-- src/krux/pages/home_pages/home.py | 23 +++++----- src/krux/pages/home_pages/mnemonic_backup.py | 11 ++--- src/krux/pages/home_pages/pub_key_view.py | 6 ++- src/krux/pages/keypads.py | 32 +++++++------- src/krux/pages/login.py | 27 +++++++----- src/krux/pages/mnemonic_loader.py | 23 ++++++---- src/krux/pages/settings_page.py | 15 ++++++- src/krux/pages/tools.py | 14 ++++-- src/krux/pages/wallet_settings.py | 46 ++++++++++++-------- src/krux/settings.py | 2 + src/krux/touch.py | 10 ++--- src/krux/translations/__init__.py | 2 +- src/krux/translations/de.py | 2 +- src/krux/translations/es.py | 2 +- src/krux/translations/fr.py | 2 +- src/krux/translations/ja.py | 2 +- src/krux/translations/ko.py | 2 +- src/krux/translations/nl.py | 2 +- src/krux/translations/pt.py | 2 +- src/krux/translations/ru.py | 2 +- src/krux/translations/tr.py | 2 +- src/krux/translations/vi.py | 2 +- src/krux/translations/zh.py | 2 +- tests/pages/test_datum_tool.py | 13 +++--- tests/test_input.py | 7 ++- 43 files changed, 207 insertions(+), 144 deletions(-) diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json index 76ddc6c66..fef90ec73 100644 --- a/i18n/translations/de-DE.json +++ b/i18n/translations/de-DE.json @@ -70,7 +70,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", @@ -290,6 +289,7 @@ "Store on SD Card": "Auf der SD-Karte speichern", "Strength": "Stärke", "Strong": "Stark", + "Swipe Threshold": "Wischschwelle", "Swipe to change mode": "Wischen um den Modus zu ändern", "TC Flash Hash": "TC Flash-Hash", "TC Flash Hash at Boot": "TC Flash-Hash beim Start", diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json index 594155858..588bce517 100644 --- a/i18n/translations/es-MX.json +++ b/i18n/translations/es-MX.json @@ -70,7 +70,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", @@ -290,6 +289,7 @@ "Store on SD Card": "Almacenar en la Tarjeta SD", "Strength": "Fuerza", "Strong": "Fuerte", + "Swipe Threshold": "Umbral de paso", "Swipe to change mode": "Deslizar para cambiar de modo", "TC Flash Hash": "TC Hash Flash", "TC Flash Hash at Boot": "TC Flash Hash al arranque", diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json index a0a36f04b..56ecaa83a 100644 --- a/i18n/translations/fr-FR.json +++ b/i18n/translations/fr-FR.json @@ -70,7 +70,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", @@ -290,6 +289,7 @@ "Store on SD Card": "Stocker sur la carte SD", "Strength": "Force", "Strong": "Fort", + "Swipe Threshold": "Seuil de balayage", "Swipe to change mode": "Faites glisser pour changer de mode", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "TC Flash Hash au démarrage", diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json index 4c2d7b420..9c2b0129b 100644 --- a/i18n/translations/ja-JP.json +++ b/i18n/translations/ja-JP.json @@ -70,7 +70,6 @@ "Device Tests": "デバイステスト", "Display": "ディスプレイ", "Do not power off, it may take a while to complete.": "完了するまで電源を切らないでください.", - "Done Converting": "変換を完了する", "Done?": "完了?", "Double mnemonic": "ダブルニーモニック", "Driver": "ドライバー", @@ -290,6 +289,7 @@ "Store on SD Card": "SDカードに保存する", "Strength": "強度", "Strong": "強力", + "Swipe Threshold": "スワイプしきい値", "Swipe to change mode": "スワイプしてモードを変更する", "TC Flash Hash": "TCフラッシュハッシュ", "TC Flash Hash at Boot": "起動時のTCフラッシュハッシュ", diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json index 0cf4250a0..7d974487a 100644 --- a/i18n/translations/ko-KR.json +++ b/i18n/translations/ko-KR.json @@ -70,7 +70,6 @@ "Device Tests": "장치 테스트", "Display": "디스플레이", "Do not power off, it may take a while to complete.": "전원을 끄지 마십시오. 완료하는 데 시간이 걸릴 수 있습니다.", - "Done Converting": "변환 완료", "Done?": "완료되었습니까?", "Double mnemonic": "이중 니모닉", "Driver": "드라이버", @@ -290,6 +289,7 @@ "Store on SD Card": "SD카드에 저장", "Strength": "강력", "Strong": "강한", + "Swipe Threshold": "스와이프 임계값", "Swipe to change mode": "모드를 변경하려면 화면을 옆으로 쓸어내리세요", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "부팅 시 플래시 탬퍼 확인 해시", diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json index 0b997df1e..d42328903 100644 --- a/i18n/translations/nl-NL.json +++ b/i18n/translations/nl-NL.json @@ -70,7 +70,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", @@ -290,6 +289,7 @@ "Store on SD Card": "Opslaan op SD kaart", "Strength": "Sterkte", "Strong": "Sterk", + "Swipe Threshold": "Veegdrempel", "Swipe to change mode": "Verander modus", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Hash Flash bij het opstarten", diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json index 09d5e3a61..6954b2d8b 100644 --- a/i18n/translations/pt-BR.json +++ b/i18n/translations/pt-BR.json @@ -70,7 +70,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", @@ -290,6 +289,7 @@ "Store on SD Card": "Armazenar no cartão SD", "Strength": "Força", "Strong": "Forte", + "Swipe Threshold": "Limite de deslizamento", "Swipe to change mode": "Deslize para mudar de modo", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "TC Flash Hash na Inicialização", diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json index 6acb2b15c..a229644e6 100644 --- a/i18n/translations/ru-RU.json +++ b/i18n/translations/ru-RU.json @@ -70,7 +70,6 @@ "Device Tests": "Испытания устройства", "Display": "Дисплеи", "Do not power off, it may take a while to complete.": "Не выключайте питание, это может занять некоторое время.", - "Done Converting": "Конвертация завершена", "Done?": "Готово?", "Double mnemonic": "Двойная мнемоника", "Driver": "Драйвер", @@ -290,6 +289,7 @@ "Store on SD Card": "Сохранить на SD Карту", "Strength": "Сила", "Strong": "Сильный", + "Swipe Threshold": "Порог смахивания", "Swipe to change mode": "Свайпните, чтобы сменить режим", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Проверка хэша Flash при загрузке", diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json index 588ec91df..6fba55dd2 100644 --- a/i18n/translations/tr-TR.json +++ b/i18n/translations/tr-TR.json @@ -70,7 +70,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ü", @@ -290,6 +289,7 @@ "Store on SD Card": "SD Kartta Sakla", "Strength": "Güç", "Strong": "Güçlü", + "Swipe Threshold": "Kaydırma Eşiği", "Swipe to change mode": "Modu değiştirmek için kaydırın", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Önyüklemede TC Flash Hash", diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json index 7a86932ce..84f0bff5b 100644 --- a/i18n/translations/vi-VN.json +++ b/i18n/translations/vi-VN.json @@ -70,7 +70,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", @@ -290,6 +289,7 @@ "Store on SD Card": "Lưu trữ trên thẻ SD", "Strength": "Độ mạnh", "Strong": "Mạnh", + "Swipe Threshold": "Vuốt Ngưỡng", "Swipe to change mode": "Vuốt để thay đổi chế độ", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Hash Flash TC khi khởi động", diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json index 212e24e7f..d0c70676c 100644 --- a/i18n/translations/zh-CN.json +++ b/i18n/translations/zh-CN.json @@ -70,7 +70,6 @@ "Device Tests": "设备测试", "Display": "显示", "Do not power off, it may take a while to complete.": "请勿断电,可能需要一段时间完成.", - "Done Converting": "完成转换", "Done?": "完成了吗?", "Double mnemonic": "双重助记词", "Driver": "驱动程序", @@ -290,6 +289,7 @@ "Store on SD Card": "存储到 SD 卡", "Strength": "强度", "Strong": "强", + "Swipe Threshold": "滑动阈值", "Swipe to change mode": "滑动切换模式", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "启动时的 TC Flash Hash", diff --git a/src/krux/krux_settings.py b/src/krux/krux_settings.py index b10719f74..5dd2ee143 100644 --- a/src/krux/krux_settings.py +++ b/src/krux/krux_settings.py @@ -300,7 +300,8 @@ class ButtonsSettings(SettingsNamespace): """Buttons debounce settings""" namespace = "settings.buttons" - debounce = NumberSetting(int, "debounce", 100, [100, 500]) + default_deb = 50 if kboard.is_m5stickv else 80 + debounce = NumberSetting(int, "debounce", default_deb, [20, 500]) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" @@ -314,12 +315,14 @@ class TouchSettings(SettingsNamespace): namespace = "settings.touchscreen" default_th = 40 if kboard.is_wonder_k else 22 - threshold = NumberSetting(int, "threshold", default_th, [10, 200]) + threshold = NumberSetting(int, "threshold", default_th, [2, 200]) + swipe_threshold = NumberSetting(int, "swipe_threshold", 50, [15, 150]) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" return { "threshold": t("Touch Threshold"), + "swipe_threshold": t("Swipe Threshold"), }[attr] diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 9da6dc759..632a23044 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -51,6 +51,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 @@ -656,7 +657,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 ) @@ -925,6 +926,13 @@ def draw_network_indicator(self): theme.info_bg_color, ) + 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): # map regions with dynamic height to fill screen self.ctx.input.touch.clear_regions() @@ -960,9 +968,7 @@ 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 - ) + fg_color = self._get_menu_item_color(menu_item) if selected_item_index == i and self.ctx.input.buttons_active: self.ctx.display.fill_rectangle( 0, @@ -1005,9 +1011,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 95663db1a..59069ceab 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" @@ -733,7 +734,7 @@ 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("Convert Datum") + CONTEXT_ARROW, lambda: "convert_begin")) menu.append((t("QR Code"), lambda: "export_qr")) # when not sensitive, allow export to sd @@ -756,7 +757,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: @@ -785,8 +786,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): @@ -816,17 +815,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 + 1) * FONT_HEIGHT + DEFAULT_PADDING + 2, - **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/encryption_ui.py b/src/krux/pages/encryption_ui.py index e0bd4c14d..f20782ddc 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 @@ -559,16 +562,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() @@ -648,6 +651,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): @@ -659,6 +663,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) @@ -699,7 +704,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 4b4a59c68..b25e67287 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 efacd436a..d961b64da 100644 --- a/src/krux/pages/home_pages/home.py +++ b/src/krux/pages/home_pages/home.py @@ -35,6 +35,7 @@ from ...format import replace_decimal_separator from ...key import TYPE_SINGLESIG from ...kboard import kboard +from ...settings import CONTEXT_ARROW class Home(Page): @@ -48,16 +49,16 @@ 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), ], @@ -187,11 +188,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 +210,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..9b2b3bea9 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -200,12 +200,13 @@ def draw_keys(self): 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, - ) + for i in range(1, 3): + self.ctx.display.outline( + offset_x + i, + y + i, + self.layout.key_h_spacing - i * 2, + self.layout.key_v_spacing - i * 2, + ) else: self.ctx.display.outline( offset_x - 2, @@ -219,18 +220,19 @@ 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, ) diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py index 82e32cfca..1225c4436 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,8 @@ POLICY_TYPE_IDS, NAME_MULTISIG, ) -from ..krux_settings import t from ..kboard import kboard +from ..settings import CONTEXT_ARROW DIGITS_HEX = "0123456789ABCDEF" @@ -67,13 +67,13 @@ 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: @@ -95,10 +95,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() @@ -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_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/settings_page.py b/src/krux/pages/settings_page.py index 16045a923..b9ab99cde 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,6 +41,7 @@ ButtonsSettings, t, locale_control, + ThermalSettings, ) from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH from ..sd_card import SDHandler @@ -255,7 +257,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 +287,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 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 bc3e9670d..6ae1b0f38 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 @@ -210,9 +208,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, @@ -234,7 +232,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 @@ -254,9 +252,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() @@ -303,15 +301,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 c7d5dc095..fe90aa292 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -30,7 +30,6 @@ PRESSED = 1 RELEASED = 2 -SWIPE_THRESHOLD = 50 SWIPE_RIGHT = 1 SWIPE_LEFT = 2 SWIPE_UP = 3 @@ -227,20 +226,21 @@ def current_state(self): self.state = IDLE elif self.state == PRESSED: if self.release_point is not None: + swipe_threshold = Settings().hardware.touch.threshold lateral_lenght = self.release_point[0] - self.press_point[0][0] - if lateral_lenght > SWIPE_THRESHOLD: + if lateral_lenght > swipe_threshold: self.gesture = SWIPE_RIGHT - elif -lateral_lenght > SWIPE_THRESHOLD: + 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 + vertical_lenght > swipe_threshold and vertical_lenght > lateral_lenght ): self.gesture = SWIPE_DOWN elif ( - -vertical_lenght > SWIPE_THRESHOLD + -vertical_lenght > swipe_threshold and -vertical_lenght > lateral_lenght ): self.gesture = SWIPE_UP diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py index e0900ccb1..5c6e8a76a 100644 --- a/src/krux/translations/__init__.py +++ b/src/krux/translations/__init__.py @@ -104,7 +104,6 @@ 4150351825, 3278654271, 3895447625, - 1664326073, 3836852788, 690625786, 382368239, @@ -324,6 +323,7 @@ 720041451, 802128782, 2644495149, + 2347057242, 3514476519, 2596024031, 2440924821, diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py index 8802adf11..179077325 100644 --- a/src/krux/translations/de.py +++ b/src/krux/translations/de.py @@ -92,7 +92,6 @@ "Gerätetests", "Bildschirm", "Schalten Sie das Gerät nicht aus, es kann eine Weile dauern.", - "Konvertierung abgeschlossen", "Fertig?", "Doppelte Gedächtnisstütze", "Driver", @@ -312,6 +311,7 @@ "Auf der SD-Karte speichern", "Stärke", "Stark", + "Wischschwelle", "Wischen um den Modus zu ändern", "TC Flash-Hash", "TC Flash-Hash beim Start", diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py index 835afe873..ec4a96ef0 100644 --- a/src/krux/translations/es.py +++ b/src/krux/translations/es.py @@ -92,7 +92,6 @@ "Pruebas del dispositivo", "Pantalla", "No apagues el dispositivo, puede tardar un tiempo en completarse.", - "Listo para convertir", "¿Listo?", "Doble mnemónico", "Operador", @@ -312,6 +311,7 @@ "Almacenar en la Tarjeta SD", "Fuerza", "Fuerte", + "Umbral de paso", "Deslizar para cambiar de modo", "TC Hash Flash", "TC Flash Hash al arranque", diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py index 5bb187cd4..c992370c1 100644 --- a/src/krux/translations/fr.py +++ b/src/krux/translations/fr.py @@ -92,7 +92,6 @@ "Tests de l'appareil", "Affichage", "Ne pas éteindre, cela peut prendre un certain temps.", - "Conversion terminée", "Terminé\u2009?", "Double mnémonique", "Pilote", @@ -312,6 +311,7 @@ "Stocker sur la carte SD", "Force", "Fort", + "Seuil de balayage", "Faites glisser pour changer de mode", "TC Flash Hash", "TC Flash Hash au démarrage", diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py index 85bd471ce..a293657dc 100644 --- a/src/krux/translations/ja.py +++ b/src/krux/translations/ja.py @@ -92,7 +92,6 @@ "デバイステスト", "ディスプレイ", "完了するまで電源を切らないでください.", - "変換を完了する", "完了?", "ダブルニーモニック", "ドライバー", @@ -312,6 +311,7 @@ "SDカードに保存する", "強度", "強力", + "スワイプしきい値", "スワイプしてモードを変更する", "TCフラッシュハッシュ", "起動時のTCフラッシュハッシュ", diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py index 3007c6ea2..8e5c00030 100644 --- a/src/krux/translations/ko.py +++ b/src/krux/translations/ko.py @@ -92,7 +92,6 @@ "장치 테스트", "디스플레이", "전원을 끄지 마십시오. 완료하는 데 시간이 걸릴 수 있습니다.", - "변환 완료", "완료되었습니까?", "이중 니모닉", "드라이버", @@ -312,6 +311,7 @@ "SD카드에 저장", "강력", "강한", + "스와이프 임계값", "모드를 변경하려면 화면을 옆으로 쓸어내리세요", "TC Flash Hash", "부팅 시 플래시 탬퍼 확인 해시", diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py index cab09792e..15286a0d7 100644 --- a/src/krux/translations/nl.py +++ b/src/krux/translations/nl.py @@ -92,7 +92,6 @@ "Apparaattests", "Weergave", "Schakel het apparaat niet uit, het kan even duren voordat het klaar is.", - "Converteren gedaan.\r", "Klaar?", "Dubbel geheugensteuntje", "Driver", @@ -312,6 +311,7 @@ "Opslaan op SD kaart", "Sterkte", "Sterk", + "Veegdrempel", "Verander modus", "TC Flash Hash", "Hash Flash bij het opstarten", diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py index 04a718f74..665b93fae 100644 --- a/src/krux/translations/pt.py +++ b/src/krux/translations/pt.py @@ -92,7 +92,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", @@ -312,6 +311,7 @@ "Armazenar no cartão SD", "Força", "Forte", + "Limite de deslizamento", "Deslize para mudar de modo", "TC Flash Hash", "TC Flash Hash na Inicialização", diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py index a72889823..dc7b25e6f 100644 --- a/src/krux/translations/ru.py +++ b/src/krux/translations/ru.py @@ -92,7 +92,6 @@ "Испытания устройства", "Дисплеи", "Не выключайте питание, это может занять некоторое время.", - "Конвертация завершена", "Готово?", "Двойная мнемоника", "Драйвер", @@ -312,6 +311,7 @@ "Сохранить на SD Карту", "Сила", "Сильный", + "Порог смахивания", "Свайпните, чтобы сменить режим", "TC Flash Hash", "Проверка хэша Flash при загрузке", diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py index 0efac3498..2cada6791 100644 --- a/src/krux/translations/tr.py +++ b/src/krux/translations/tr.py @@ -92,7 +92,6 @@ "Cihaz Testleri", "Ekran", "Kapatmayın, tamamlanması biraz zaman alabilir.", - "Dönüştürme Tamamlandı", "Tamamlandı mı?", "Çifte anımsatıcı", "Sürücü", @@ -312,6 +311,7 @@ "SD Kartta Sakla", "Güç", "Güçlü", + "Kaydırma Eşiği", "Modu değiştirmek için kaydırın", "TC Flash Hash", "Önyüklemede TC Flash Hash", diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py index 6423f5264..e89e754b3 100644 --- a/src/krux/translations/vi.py +++ b/src/krux/translations/vi.py @@ -92,7 +92,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", @@ -312,6 +311,7 @@ "Lưu trữ trên thẻ SD", "Độ mạnh", "Mạnh", + "Vuốt Ngưỡng", "Vuốt để thay đổi chế độ", "TC Flash Hash", "Hash Flash TC khi khởi động", diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py index 44ba6c228..f9541e14f 100644 --- a/src/krux/translations/zh.py +++ b/src/krux/translations/zh.py @@ -92,7 +92,6 @@ "设备测试", "显示", "请勿断电,可能需要一段时间完成.", - "完成转换", "完成了吗?", "双重助记词", "驱动程序", @@ -312,6 +311,7 @@ "存储到 SD 卡", "强度", "强", + "滑动阈值", "滑动切换模式", "TC Flash Hash", "启动时的 TC Flash Hash", diff --git a/tests/pages/test_datum_tool.py b/tests/pages/test_datum_tool.py index 0f9912c33..2668cb193 100644 --- a/tests/pages/test_datum_tool.py +++ b/tests/pages/test_datum_tool.py @@ -840,6 +840,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" @@ -853,7 +854,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", ] @@ -865,7 +866,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, []) @@ -881,7 +882,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 @@ -896,7 +896,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 @@ -912,8 +911,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 @@ -930,8 +928,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/test_input.py b/tests/test_input.py index 38e67cbb6..bd64a3310 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -457,6 +457,7 @@ def test_debounce_presses_with_greater_interval(mocker, m5stickv): def test_debounce_presses_with_smaller_interval(mocker, m5stickv): from krux.input import Input, RELEASED, PRESSED + from krux.krux_settings import Settings input = Input() interval = 10 # ms @@ -473,9 +474,11 @@ def test_debounce_presses_with_smaller_interval(mocker, m5stickv): btn = input.wait_for_button() assert btn == 0 assert input.entropy > 0 - # Assert that the flush_events was called 10 times + # Assert that the flush_events was called debounce / interval times # meaning that the debounce time was respected - assert input.flush_events.call_count == 10 + assert ( + input.flush_events.call_count == Settings().hardware.buttons.debounce / interval + ) def test_wait_for_button_blocks_until_enter_released(mocker, m5stickv): From 93bd168888a6ebfc9e4b2ad107178039a74ee1f5 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sun, 16 Nov 2025 21:06:24 -0300 Subject: [PATCH 02/29] Vertical bar for menus with more than one page + changelog --- CHANGELOG.md | 9 +++++- src/krux/pages/__init__.py | 46 +++++++++++++++++++++++++++++++ src/krux/pages/home_pages/home.py | 3 +- src/krux/pages/keypads.py | 5 ++-- src/krux/pages/login.py | 4 +-- src/krux/pages/stack_1248.py | 4 ++- src/krux/pages/tiny_seed.py | 2 +- tests/pages/test_encryption_ui.py | 12 ++++++++ 8 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d195636..ca2fde83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,17 @@ This device shares similarities with the WonderMV but stands out with its larger ### New Device Support: WonderK PRO From the wonderful land of Korea, a new creation arrives: the WonderK PRO. Created by an entrepreneur who loves the Krux project, the WonderK follows in the footsteps of the WonderMV, but boasts a larger 2.8" display! Computer simulator for the WonderK device is also included. +### Improved UI +Added new context arrows, customizable colors, and a page index indicator for menu items. Enhanced keypad visuals with a clearer keyset index indicator and a double-outline highlight when navigating with buttons on touch devices. + ### Other Bug Fixes and Improvements -- Added backtick ` to keypad +- Settings: Added a new _Swipe Threshold_ +- Settings: Reduced default _Buttons Debounce_ value (with an even lower default on _M5StickV_) +- Settings: Expanded value ranges for _Touch Threshold_ and _Buttons Debounce_ +- Keypad: Added backtick **`** - Bugfix: Screensaver not activating in menu pages without statusbar - Embit: Improved BIP39 mnemonic validation +- UI: Other small changes # Changelog 25.10.1 - October 2025 diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 632a23044..4e2063568 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -44,6 +44,7 @@ MINIMAL_PADDING, FLASH_MSG_TIME, FONT_HEIGHT, + FONT_WIDTH, STATUS_BAR_HEIGHT, BOTTOM_LINE, ) @@ -638,6 +639,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, @@ -722,6 +731,42 @@ 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 + print( + self.menu_offset, + total_pages, + self.menu_view.curr_page(), + bar_height, + y_offset, + ) + 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 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 @@ -749,6 +794,7 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None 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) diff --git a/src/krux/pages/home_pages/home.py b/src/krux/pages/home_pages/home.py index d961b64da..54c58abe2 100644 --- a/src/krux/pages/home_pages/home.py +++ b/src/krux/pages/home_pages/home.py @@ -36,6 +36,7 @@ from ...key import TYPE_SINGLESIG from ...kboard import kboard from ...settings import CONTEXT_ARROW +from ...themes import theme class Home(Page): @@ -60,7 +61,7 @@ def __init__(self, ctx): (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, ), diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index 9b2b3bea9..6f15ee324 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -22,6 +22,7 @@ import math import lcd +from ..context import Context from ..krux_settings import t from ..themes import theme from ..input import ( @@ -44,7 +45,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 +73,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 diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py index 1225c4436..00147a77c 100644 --- a/src/krux/pages/login.py +++ b/src/krux/pages/login.py @@ -49,6 +49,7 @@ ) from ..kboard import kboard from ..settings import CONTEXT_ARROW +from ..themes import theme DIGITS_HEX = "0123456789ABCDEF" @@ -79,7 +80,7 @@ def __init__(self, ctx): 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, @@ -263,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) diff --git a/src/krux/pages/stack_1248.py b/src/krux/pages/stack_1248.py index 0eaa700f2..726cf29b6 100644 --- a/src/krux/pages/stack_1248.py +++ b/src/krux/pages/stack_1248.py @@ -221,7 +221,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) diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 8cbf29ee0..819a92040 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -93,7 +93,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() diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py index 92f83abff..854b3a74d 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 From 6bae18c53cb59791289c8cccfd84a043adf2c4c0 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Mon, 17 Nov 2025 00:02:48 -0300 Subject: [PATCH 03/29] Add missing context arrow --- src/krux/pages/__init__.py | 7 ------ src/krux/pages/datum_tool.py | 40 +++++++++++++++++++++++----------- src/krux/pages/device_tests.py | 3 +-- src/krux/pages/qr_view.py | 6 ++--- tests/pages/test_datum_tool.py | 10 +++++---- 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 4e2063568..857bb0327 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -746,13 +746,6 @@ def draw_vertical_bar(self): y_offset = ( self.ctx.display.height() - ((bar_height + bar_padding) * total_pages) ) // 2 + (bar_padding + self.menu_offset) // 2 - print( - self.menu_offset, - total_pages, - self.menu_view.curr_page(), - bar_height, - y_offset, - ) for i in range(total_pages): color = ( theme.toggle_color diff --git a/src/krux/pages/datum_tool.py b/src/krux/pages/datum_tool.py index 59069ceab..30c98b343 100644 --- a/src/krux/pages/datum_tool.py +++ b/src/krux/pages/datum_tool.py @@ -412,23 +412,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 @@ -491,11 +496,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] @@ -734,8 +745,11 @@ def _build_options_menu(self, offer_convert=False, offer_show=True): menu.append((t("Show Datum"), lambda: "show")) if not offer_convert: + 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"), lambda: "export_qr")) + menu.append((t("QR Code") + qr_context, lambda: "export_qr")) # when not sensitive, allow export to sd if not self.sensitive: 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/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/tests/pages/test_datum_tool.py b/tests/pages/test_datum_tool.py index 2668cb193..2d758ea6a 100644 --- a/tests/pages/test_datum_tool.py +++ b/tests/pages/test_datum_tool.py @@ -505,8 +505,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) @@ -518,6 +517,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 @@ -532,6 +532,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 @@ -547,6 +548,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 @@ -563,6 +565,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 @@ -593,9 +596,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 ) From d8762ead9b27dbb888381952223ed3d54c22b402 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Wed, 19 Nov 2025 09:44:32 -0300 Subject: [PATCH 04/29] Remove swipe_threshold in favor of industry standard --- i18n/translations/de-DE.json | 1 - i18n/translations/es-MX.json | 1 - i18n/translations/fr-FR.json | 1 - i18n/translations/ja-JP.json | 1 - i18n/translations/ko-KR.json | 1 - i18n/translations/nl-NL.json | 1 - i18n/translations/pt-BR.json | 1 - i18n/translations/ru-RU.json | 1 - i18n/translations/tr-TR.json | 1 - i18n/translations/vi-VN.json | 1 - i18n/translations/zh-CN.json | 1 - src/krux/krux_settings.py | 2 -- src/krux/touch.py | 11 ++++++----- src/krux/translations/__init__.py | 1 - src/krux/translations/de.py | 1 - src/krux/translations/es.py | 1 - src/krux/translations/fr.py | 1 - src/krux/translations/ja.py | 1 - src/krux/translations/ko.py | 1 - src/krux/translations/nl.py | 1 - src/krux/translations/pt.py | 1 - src/krux/translations/ru.py | 1 - src/krux/translations/tr.py | 1 - src/krux/translations/vi.py | 1 - src/krux/translations/zh.py | 1 - 25 files changed, 6 insertions(+), 30 deletions(-) diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json index 93d054625..88c8a2b3f 100644 --- a/i18n/translations/de-DE.json +++ b/i18n/translations/de-DE.json @@ -290,7 +290,6 @@ "Store on SD Card": "Auf der SD-Karte speichern", "Strength": "Stärke", "Strong": "Stark", - "Swipe Threshold": "Wischschwelle", "Swipe to change mode": "Wischen um den Modus zu ändern", "TC Flash Hash": "TC Flash-Hash", "TC Flash Hash at Boot": "TC Flash-Hash beim Start", diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json index 0dd8452bc..81a1f2bf1 100644 --- a/i18n/translations/es-MX.json +++ b/i18n/translations/es-MX.json @@ -290,7 +290,6 @@ "Store on SD Card": "Almacenar en la Tarjeta SD", "Strength": "Fuerza", "Strong": "Fuerte", - "Swipe Threshold": "Umbral de paso", "Swipe to change mode": "Deslizar para cambiar de modo", "TC Flash Hash": "TC Hash Flash", "TC Flash Hash at Boot": "TC Flash Hash al arranque", diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json index cb6bd848b..418c8790a 100644 --- a/i18n/translations/fr-FR.json +++ b/i18n/translations/fr-FR.json @@ -290,7 +290,6 @@ "Store on SD Card": "Stocker sur la carte SD", "Strength": "Force", "Strong": "Fort", - "Swipe Threshold": "Seuil de balayage", "Swipe to change mode": "Faites glisser pour changer de mode", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "TC Flash Hash au démarrage", diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json index 6145d2edc..fe5eb4f8a 100644 --- a/i18n/translations/ja-JP.json +++ b/i18n/translations/ja-JP.json @@ -290,7 +290,6 @@ "Store on SD Card": "SDカードに保存する", "Strength": "強度", "Strong": "強力", - "Swipe Threshold": "スワイプしきい値", "Swipe to change mode": "スワイプしてモードを変更する", "TC Flash Hash": "TCフラッシュハッシュ", "TC Flash Hash at Boot": "起動時のTCフラッシュハッシュ", diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json index a55958a97..3c70e8214 100644 --- a/i18n/translations/ko-KR.json +++ b/i18n/translations/ko-KR.json @@ -290,7 +290,6 @@ "Store on SD Card": "SD카드에 저장", "Strength": "강력", "Strong": "강한", - "Swipe Threshold": "스와이프 임계값", "Swipe to change mode": "모드를 변경하려면 화면을 옆으로 쓸어내리세요", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "부팅 시 플래시 탬퍼 확인 해시", diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json index 6cb6d3382..7a688b299 100644 --- a/i18n/translations/nl-NL.json +++ b/i18n/translations/nl-NL.json @@ -290,7 +290,6 @@ "Store on SD Card": "Opslaan op SD kaart", "Strength": "Sterkte", "Strong": "Sterk", - "Swipe Threshold": "Veegdrempel", "Swipe to change mode": "Verander modus", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Hash Flash bij het opstarten", diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json index c4509049d..4de029d9f 100644 --- a/i18n/translations/pt-BR.json +++ b/i18n/translations/pt-BR.json @@ -290,7 +290,6 @@ "Store on SD Card": "Armazenar no cartão SD", "Strength": "Força", "Strong": "Forte", - "Swipe Threshold": "Limite de deslizamento", "Swipe to change mode": "Deslize para mudar de modo", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "TC Flash Hash na Inicialização", diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json index 84065b485..f7113caf9 100644 --- a/i18n/translations/ru-RU.json +++ b/i18n/translations/ru-RU.json @@ -290,7 +290,6 @@ "Store on SD Card": "Сохранить на SD Карту", "Strength": "Сила", "Strong": "Сильный", - "Swipe Threshold": "Порог смахивания", "Swipe to change mode": "Свайпните, чтобы сменить режим", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Проверка хэша Flash при загрузке", diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json index 3f26594b4..8c8069b63 100644 --- a/i18n/translations/tr-TR.json +++ b/i18n/translations/tr-TR.json @@ -290,7 +290,6 @@ "Store on SD Card": "SD Kartta Sakla", "Strength": "Güç", "Strong": "Güçlü", - "Swipe Threshold": "Kaydırma Eşiği", "Swipe to change mode": "Modu değiştirmek için kaydırın", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Önyüklemede TC Flash Hash", diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json index 4295c2f7f..1a6e3ba50 100644 --- a/i18n/translations/vi-VN.json +++ b/i18n/translations/vi-VN.json @@ -290,7 +290,6 @@ "Store on SD Card": "Lưu trữ trên thẻ SD", "Strength": "Độ mạnh", "Strong": "Mạnh", - "Swipe Threshold": "Vuốt Ngưỡng", "Swipe to change mode": "Vuốt để thay đổi chế độ", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "Hash Flash TC khi khởi động", diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json index 3eeccf3e3..ee23430a3 100644 --- a/i18n/translations/zh-CN.json +++ b/i18n/translations/zh-CN.json @@ -290,7 +290,6 @@ "Store on SD Card": "存储到 SD 卡", "Strength": "强度", "Strong": "强", - "Swipe Threshold": "滑动阈值", "Swipe to change mode": "滑动切换模式", "TC Flash Hash": "TC Flash Hash", "TC Flash Hash at Boot": "启动时的 TC Flash Hash", diff --git a/src/krux/krux_settings.py b/src/krux/krux_settings.py index 5dd2ee143..c78150501 100644 --- a/src/krux/krux_settings.py +++ b/src/krux/krux_settings.py @@ -316,13 +316,11 @@ class TouchSettings(SettingsNamespace): namespace = "settings.touchscreen" default_th = 40 if kboard.is_wonder_k else 22 threshold = NumberSetting(int, "threshold", default_th, [2, 200]) - swipe_threshold = NumberSetting(int, "swipe_threshold", 50, [15, 150]) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" return { "threshold": t("Touch Threshold"), - "swipe_threshold": t("Swipe Threshold"), }[attr] diff --git a/src/krux/touch.py b/src/krux/touch.py index fe90aa292..3f0a09916 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -22,6 +22,7 @@ # pylint: disable=R0902 import time +import math from .kboard import kboard from .krux_settings import Settings @@ -30,6 +31,7 @@ PRESSED = 1 RELEASED = 2 +SWIPE_THRESHOLD = 35 SWIPE_RIGHT = 1 SWIPE_LEFT = 2 SWIPE_UP = 3 @@ -226,21 +228,20 @@ def current_state(self): self.state = IDLE elif self.state == PRESSED: if self.release_point is not None: - swipe_threshold = Settings().hardware.touch.threshold lateral_lenght = self.release_point[0] - self.press_point[0][0] - if lateral_lenght > swipe_threshold: + if lateral_lenght > SWIPE_THRESHOLD: self.gesture = SWIPE_RIGHT - elif -lateral_lenght > swipe_threshold: + 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 + vertical_lenght > SWIPE_THRESHOLD and vertical_lenght > lateral_lenght ): self.gesture = SWIPE_DOWN elif ( - -vertical_lenght > swipe_threshold + -vertical_lenght > SWIPE_THRESHOLD and -vertical_lenght > lateral_lenght ): self.gesture = SWIPE_UP diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py index 7c377776e..5624cce5e 100644 --- a/src/krux/translations/__init__.py +++ b/src/krux/translations/__init__.py @@ -324,7 +324,6 @@ 720041451, 802128782, 2644495149, - 2347057242, 3514476519, 2596024031, 2440924821, diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py index 45652f027..a05cc1607 100644 --- a/src/krux/translations/de.py +++ b/src/krux/translations/de.py @@ -312,7 +312,6 @@ "Auf der SD-Karte speichern", "Stärke", "Stark", - "Wischschwelle", "Wischen um den Modus zu ändern", "TC Flash-Hash", "TC Flash-Hash beim Start", diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py index c431d2cb9..3f9dd7e8e 100644 --- a/src/krux/translations/es.py +++ b/src/krux/translations/es.py @@ -312,7 +312,6 @@ "Almacenar en la Tarjeta SD", "Fuerza", "Fuerte", - "Umbral de paso", "Deslizar para cambiar de modo", "TC Hash Flash", "TC Flash Hash al arranque", diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py index 8b2c4703e..84b7dc35b 100644 --- a/src/krux/translations/fr.py +++ b/src/krux/translations/fr.py @@ -312,7 +312,6 @@ "Stocker sur la carte SD", "Force", "Fort", - "Seuil de balayage", "Faites glisser pour changer de mode", "TC Flash Hash", "TC Flash Hash au démarrage", diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py index 97e057c2a..ae77becd7 100644 --- a/src/krux/translations/ja.py +++ b/src/krux/translations/ja.py @@ -312,7 +312,6 @@ "SDカードに保存する", "強度", "強力", - "スワイプしきい値", "スワイプしてモードを変更する", "TCフラッシュハッシュ", "起動時のTCフラッシュハッシュ", diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py index 2704f44ce..0abc15979 100644 --- a/src/krux/translations/ko.py +++ b/src/krux/translations/ko.py @@ -312,7 +312,6 @@ "SD카드에 저장", "강력", "강한", - "스와이프 임계값", "모드를 변경하려면 화면을 옆으로 쓸어내리세요", "TC Flash Hash", "부팅 시 플래시 탬퍼 확인 해시", diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py index 92871d0ef..ce58305a6 100644 --- a/src/krux/translations/nl.py +++ b/src/krux/translations/nl.py @@ -312,7 +312,6 @@ "Opslaan op SD kaart", "Sterkte", "Sterk", - "Veegdrempel", "Verander modus", "TC Flash Hash", "Hash Flash bij het opstarten", diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py index 1a13835f0..4e3548a7c 100644 --- a/src/krux/translations/pt.py +++ b/src/krux/translations/pt.py @@ -312,7 +312,6 @@ "Armazenar no cartão SD", "Força", "Forte", - "Limite de deslizamento", "Deslize para mudar de modo", "TC Flash Hash", "TC Flash Hash na Inicialização", diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py index 94db625f9..075809293 100644 --- a/src/krux/translations/ru.py +++ b/src/krux/translations/ru.py @@ -312,7 +312,6 @@ "Сохранить на SD Карту", "Сила", "Сильный", - "Порог смахивания", "Свайпните, чтобы сменить режим", "TC Flash Hash", "Проверка хэша Flash при загрузке", diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py index 69d6a6cfa..8140aaf80 100644 --- a/src/krux/translations/tr.py +++ b/src/krux/translations/tr.py @@ -312,7 +312,6 @@ "SD Kartta Sakla", "Güç", "Güçlü", - "Kaydırma Eşiği", "Modu değiştirmek için kaydırın", "TC Flash Hash", "Önyüklemede TC Flash Hash", diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py index 5aa9ce4c4..fdd996af2 100644 --- a/src/krux/translations/vi.py +++ b/src/krux/translations/vi.py @@ -312,7 +312,6 @@ "Lưu trữ trên thẻ SD", "Độ mạnh", "Mạnh", - "Vuốt Ngưỡng", "Vuốt để thay đổi chế độ", "TC Flash Hash", "Hash Flash TC khi khởi động", diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py index 48eef62e1..67363d1ea 100644 --- a/src/krux/translations/zh.py +++ b/src/krux/translations/zh.py @@ -312,7 +312,6 @@ "存储到 SD 卡", "强度", "强", - "滑动阈值", "滑动切换模式", "TC Flash Hash", "启动时的 TC Flash Hash", From 885a83caa4c704d011dec224b8dee74647ae6777 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Wed, 19 Nov 2025 14:12:49 -0300 Subject: [PATCH 05/29] Discard diagonal swipes --- CHANGELOG.md | 2 +- src/krux/input.py | 33 +++++++++++++++---------- src/krux/touch.py | 61 +++++++++++++++++++++-------------------------- 3 files changed, 49 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65b6c07f6..6ec0fedb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,9 @@ Krux now displays a warning instead of blocking QR-encoded passphrases that cont Added new context arrows, customizable colors, and a page index indicator for menu items. Enhanced keypad visuals with a clearer keyset index indicator and a double-outline highlight when navigating with buttons on touch devices. ### Other Bug Fixes and Improvements -- Settings: Added a new _Swipe Threshold_ - 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: Diagonal swipes are now discarded, and the swipe detection threshold has been slightly reduced - Keypad: Added backtick **`** - Bugfix: Screensaver not activating in menu pages without statusbar - Embit: Improved BIP39 mnemonic validation diff --git a/src/krux/input.py b/src/krux/input.py index 0eb076203..fdb0f56e0 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 @@ -175,29 +177,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 +313,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 ACTIVATING_BUTTONS if self.swipe_right_value() == PRESSED: return SWIPE_RIGHT if self.swipe_left_value() == PRESSED: @@ -318,6 +325,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/touch.py b/src/krux/touch.py index 3f0a09916..fe979426c 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -22,7 +22,6 @@ # pylint: disable=R0902 import time -import math from .kboard import kboard from .krux_settings import Settings @@ -36,6 +35,7 @@ SWIPE_LEFT = 2 SWIPE_UP = 3 SWIPE_DOWN = 4 +SWIPE_NONE = 5 TOUCH_S_PERIOD = 20 # Touch sample period - Min = 10 @@ -227,25 +227,20 @@ def current_state(self): 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 self.state = RELEASED + 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: + # 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: + # undetermined diagonal swipe + self.gesture = SWIPE_NONE else: print("Touch error") return self.state @@ -269,33 +264,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""" From 97c743d6d0d02b4d4774a262352e7a339f6545ff Mon Sep 17 00:00:00 2001 From: tadeubas Date: Wed, 19 Nov 2025 21:07:46 -0300 Subject: [PATCH 06/29] Discard long-hold swipes --- CHANGELOG.md | 2 +- src/krux/touch.py | 36 +++++++++++++++++++++++++----------- tests/test_input.py | 1 + tests/test_touch.py | 1 + 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec0fedb7..ffe8cd907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Added new context arrows, customizable colors, and a page index indicator for me ### 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: Diagonal swipes are now discarded, and the swipe 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 - Keypad: Added backtick **`** - Bugfix: Screensaver not activating in menu pages without statusbar - Embit: Improved BIP39 mnemonic validation diff --git a/src/krux/touch.py b/src/krux/touch.py index fe979426c..aeebbdc99 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -30,6 +30,7 @@ PRESSED = 1 RELEASED = 2 +SWIPE_DURATION_MS = 750 SWIPE_THRESHOLD = 35 SWIPE_RIGHT = 1 SWIPE_LEFT = 2 @@ -48,6 +49,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 @@ -223,26 +225,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: + return self.state + + if self.state == PRESSED: self.state = RELEASED + 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: - # 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 + # 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: - # undetermined diagonal swipe - self.gesture = SWIPE_NONE - else: - print("Touch error") + 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): @@ -257,6 +270,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 diff --git a/tests/test_input.py b/tests/test_input.py index bd64a3310..ae6106892 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -691,6 +691,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() diff --git a/tests/test_touch.py b/tests/test_touch.py index 3f03ff734..4ddecb473 100644 --- a/tests/test_touch.py +++ b/tests/test_touch.py @@ -308,6 +308,7 @@ 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() From ebdcca7ef8ae34e9b1e83d0065093fd6d9f03f76 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Wed, 19 Nov 2025 22:33:12 -0300 Subject: [PATCH 07/29] Highlight the touched menu item --- src/krux/pages/__init__.py | 62 +++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 857bb0327..101f1386f 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -87,6 +87,8 @@ BASE_HEX_SUFFIX = "HEX" BASE_OCT_SUFFIX = "OCT" +TOUCH_HIGHLIGHT_MS = 100 + class Page: """Represents a page in the app, with helper methods for common display and @@ -760,6 +762,19 @@ def draw_vertical_bar(self): 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 @@ -771,19 +786,11 @@ 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() @@ -796,6 +803,7 @@ 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 + was_btn_active = self.ctx.input.buttons_active btn = self.ctx.input.wait_for_fastnav_button( # Block if screen saver not active screensaver_time == 0, @@ -804,6 +812,20 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None if kboard.has_touchscreen: if btn == BUTTON_TOUCH: selected_item_index = self.ctx.input.touch.current_index() + + # highlight selected index before continue + if was_btn_active: + # need to clear screen if button was used just before + # because an item will be highlighted already + 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 to see item highlighted btn = BUTTON_ENTER self.ctx.input.touch.clear_regions() if btn == BUTTON_ENTER: @@ -972,7 +994,9 @@ def _get_menu_item_color(self, menu_item): return menu_item[2] return theme.fg_color - def _draw_touch_menu(self, selected_item_index): + 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 @@ -994,8 +1018,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 ) @@ -1008,7 +1032,9 @@ def _draw_touch_menu(self, selected_item_index): region_height - len(menu_item_lines) * FONT_HEIGHT ) // 2 + y_keypad_map[i] fg_color = self._get_menu_item_color(menu_item) - 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.fill_rectangle( 0, offset_y_item + 1 - FONT_HEIGHT // 2, @@ -1017,7 +1043,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 ) From daad4a1b8c9237f21ba6799aa15b56a49193fe8b Mon Sep 17 00:00:00 2001 From: tadeubas Date: Thu, 20 Nov 2025 23:02:01 -0300 Subject: [PATCH 08/29] Touch-feedback highlight and better touch handling on keypads --- CHANGELOG.md | 6 ++-- src/krux/input.py | 4 ++- src/krux/pages/__init__.py | 56 ++++++++++++++++++------------- src/krux/pages/keypads.py | 44 +++++++++++++++++++----- src/krux/pages/mnemonic_editor.py | 25 ++++++++------ src/krux/pages/settings_page.py | 20 ++++++++--- src/krux/pages/stack_1248.py | 15 ++++++--- src/krux/pages/tiny_seed.py | 14 +++++--- src/krux/touch.py | 52 ++++++++++++++++++++++++---- 9 files changed, 172 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe8cd907..da361053b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,18 @@ From the wonderful land of Korea, a new creation arrives: the WonderK PRO. Creat Krux now displays a warning instead of blocking QR-encoded passphrases that contain non-ASCII characters. Users are encouraged to use QR codes containing only ASCII passphrases or non-ASCII that have already been normalized to NFKD. ### Improved UI -Added new context arrows, customizable colors, and a page index indicator for menu items. Enhanced keypad visuals with a clearer keyset index indicator and a double-outline highlight when navigating with buttons on touch devices. +- 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 a double-outline highlight on touch devices for both touch feedback and button navigation. ### 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: 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 -- UI: Other small changes +- UI: Other small changes and optimizations # Changelog 25.10.1 - October 2025 diff --git a/src/krux/input.py b/src/krux/input.py index fdb0f56e0..1965bdd3a 100644 --- a/src/krux/input.py +++ b/src/krux/input.py @@ -37,6 +37,7 @@ SWIPE_LEFT = 5 SWIPE_UP = 6 SWIPE_DOWN = 7 +SWIPE_FAIL = 99 FAST_FORWARD = 8 FAST_BACKWARD = 9 @@ -49,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 @@ -316,7 +318,7 @@ def _handle_touch_input(): # Check if was a swipe if self.swipe_none_value() == PRESSED: - return ACTIVATING_BUTTONS + return SWIPE_FAIL if self.swipe_right_value() == PRESSED: return SWIPE_RIGHT if self.swipe_left_value() == PRESSED: diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 101f1386f..758ef7bf8 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -33,11 +33,13 @@ 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, @@ -87,8 +89,6 @@ BASE_HEX_SUFFIX = "HEX" BASE_OCT_SUFFIX = "OCT" -TOUCH_HIGHLIGHT_MS = 100 - class Page: """Represents a page in the app, with helper methods for common display and @@ -175,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 @@ -472,7 +475,18 @@ def prompt(self, text, offset_y=0, highlight_prefix=""): 2 * FONT_HEIGHT, theme.frame_color, ) - btn = self.ctx.input.wait_for_button() + # 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) + index = self.ctx.input.touch.current_index() + if index == 1: + return True + if index == 0: + return False if btn in (BUTTON_PAGE, BUTTON_PAGE_PREV): answer = not answer # erase yes/no area for next loop @@ -483,13 +497,6 @@ def prompt(self, text, offset_y=0, highlight_prefix=""): 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 # BUTTON_ENTER return answer @@ -804,14 +811,17 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None else: screensaver_time = Settings().appearance.screensaver_time was_btn_active = self.ctx.input.buttons_active - 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: + 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: @@ -823,11 +833,11 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None draw_dividers=not was_btn_active, highlight=True, ) - time.sleep_ms( - TOUCH_HIGHLIGHT_MS - ) # wait a little to see item highlighted + # wait a little to see item highlighted + time.sleep_ms(TOUCH_HIGHLIGHT_MS) btn = BUTTON_ENTER - self.ctx.input.touch.clear_regions() + if kboard.has_touchscreen: + self.ctx.input.touch.clear_regions() if btn == BUTTON_ENTER: status = self._clicked_item(selected_item_index) if status != MENU_CONTINUE: diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index 6f15ee324..14171a2ba 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -22,6 +22,7 @@ import math import lcd +import time from ..context import Context from ..krux_settings import t from ..themes import theme @@ -29,12 +30,14 @@ 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 @@ -152,7 +155,7 @@ def draw_keys(self): for x in self.layout.x_keypad_map[:-1]: x = MINIMAL_PADDING if x == 0 else x key = None - custom_color = None + custom_color = theme.fg_color if key_index < len(self.keys): key = self.keys[key_index] elif key_index == self.del_index: @@ -190,12 +193,9 @@ def draw_keys(self): 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) + self.ctx.display.draw_string( + key_offset_x, offset_y, key, custom_color + ) if ( key_index == self.cur_key_index and self.ctx.input.buttons_active @@ -259,14 +259,40 @@ def get_valid_index(self): def touch_to_physical(self): """Convert a touch press in button press""" 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) + + if self.cur_key_index < len(self.keys) or self.cur_key_index in special_keys: + key_index = 0 + for y in self.layout.y_keypad_map[:-1]: + for x in self.layout.x_keypad_map[:-1]: + if key_index == self.cur_key_index: + offset_x = MINIMAL_PADDING if x == 0 else x + for i in range(1, 3): + self.ctx.display.outline( + offset_x + i, + y + i, + self.layout.key_h_spacing - i * 2, + self.layout.key_v_spacing - i * 2, + ) + # wait a little to see item outlined + time.sleep_ms(TOUCH_HIGHLIGHT_MS) + key_index += 1 + + 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 + return actual_button def navigate(self, btn): diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index 916ead397..e2df5475c 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -33,6 +33,7 @@ BUTTON_PAGE_PREV, FAST_FORWARD, FAST_BACKWARD, + SWIPE_FAIL, ) from ..key import Key from ..kboard import kboard @@ -297,16 +298,20 @@ 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 //= 2 + button_index += 12 + else: + button_index //= 2 + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if button_index == GO_INDEX: if self.mnemonic_length == 24 and kboard.is_m5stickv and page == 0: diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py index b9ab99cde..f06d7a375 100644 --- a/src/krux/pages/settings_page.py +++ b/src/krux/pages/settings_page.py @@ -43,7 +43,13 @@ locale_control, ThermalSettings, ) -from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH +from ..input import ( + BUTTON_ENTER, + BUTTON_PAGE, + BUTTON_PAGE_PREV, + BUTTON_TOUCH, + SWIPE_FAIL, +) from ..sd_card import SDHandler from . import ( Page, @@ -111,6 +117,8 @@ def _draw_settings_pad(self): 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: @@ -385,9 +393,13 @@ def category_setting(self, settings_namespace, setting): 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()) + + # 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: break diff --git a/src/krux/pages/stack_1248.py b/src/krux/pages/stack_1248.py index 726cf29b6..6f547b004 100644 --- a/src/krux/pages/stack_1248.py +++ b/src/krux/pages/stack_1248.py @@ -32,6 +32,7 @@ BUTTON_TOUCH, FAST_FORWARD, FAST_BACKWARD, + SWIPE_FAIL, ) from ..kboard import kboard @@ -425,10 +426,16 @@ def enter_1248(self): self._draw_index(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: + continue + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if index >= STACKBIT_GO_INDEX: # go word = self.digits_to_word(digits) diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 819a92040..96b6a28de 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -43,6 +43,7 @@ BUTTON_TOUCH, FAST_FORWARD, FAST_BACKWARD, + SWIPE_FAIL, ) from ..bip39 import entropy_checksum from ..kboard import kboard @@ -375,10 +376,15 @@ def _editable_bit(): 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() + if index < 0: + continue + 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/touch.py b/src/krux/touch.py index aeebbdc99..dfc39ad47 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -39,6 +39,7 @@ SWIPE_NONE = 5 TOUCH_S_PERIOD = 20 # Touch sample period - Min = 10 +EDGE_PIXELS = 2 # The ammount of pixels to determine the edges of a region class Touch: @@ -152,21 +153,58 @@ def _extract_index(self, data): x_index = 0 # 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 - + if self.y_regions: + for region in self.y_regions: + if data[1] > region: + y_index += 1 + + # If touch was between adjacent regions, discard it as an edge touch + if ( + (0 < y_index < len(self.y_regions) - 1) + and ( + data[1] - EDGE_PIXELS + <= self.y_regions[y_index] + <= data[1] + EDGE_PIXELS + ) + ) or ( + (0 < y_index - 1) + and ( + data[1] - EDGE_PIXELS + <= self.y_regions[y_index - 1] + <= data[1] + EDGE_PIXELS + ) + ): + return -1 + y_index -= 1 if y_index > 0 else 0 + + index = y_index # Calculate x index if x regions are defined (2D array) 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 + + # If touch was between adjacent regions, discard it as an edge touch + if ( + (0 < x_index < len(self.x_regions) - 1) + and ( + data[0] - EDGE_PIXELS + <= self.x_regions[x_index] + <= data[0] + EDGE_PIXELS + ) + ) or ( + (0 < x_index - 1) + and ( + data[0] - EDGE_PIXELS + <= self.x_regions[x_index - 1] + <= data[0] + EDGE_PIXELS + ) + ): + return -1 + # Combine y and x indices to get the final index index = y_index * (len(self.x_regions) - 1) + x_index - else: - index = y_index # self.highlight_region(x_index, y_index) return index From b3780a90d55f393086acb1eb3db91102f6247d57 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Fri, 21 Nov 2025 23:12:14 -0300 Subject: [PATCH 09/29] Fix keypad highlight --- src/krux/pages/keypads.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index 14171a2ba..996f8c695 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -267,7 +267,11 @@ def touch_to_physical(self): if self.has_more_key(): special_keys.append(self.more_index) - if self.cur_key_index < len(self.keys) or self.cur_key_index in special_keys: + # Highlight the touched key + if ( + self.cur_key_index < len(self.keys) + and self.keys[self.cur_key_index] in self.possible_keys + ) or self.cur_key_index in special_keys: key_index = 0 for y in self.layout.y_keypad_map[:-1]: for x in self.layout.x_keypad_map[:-1]: From f8aac05711fff49bac65cda5d282eb6c67d5b0df Mon Sep 17 00:00:00 2001 From: tadeubas Date: Fri, 21 Nov 2025 23:19:59 -0300 Subject: [PATCH 10/29] prompt highlight + code reuse --- src/krux/pages/__init__.py | 152 +++++++++++++++--------------- tests/pages/test_encryption_ui.py | 47 +++++---- 2 files changed, 106 insertions(+), 93 deletions(-) diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 758ef7bf8..fd82eaba4 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -411,70 +411,34 @@ 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) - 2 * FONT_HEIGHT + self.y_keypad_map = [touch_offset_y, touch_offset_y + 4 * 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, - ) + 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): @@ -482,23 +446,21 @@ def prompt(self, text, offset_y=0, highlight_prefix=""): if btn == BUTTON_TOUCH: self.ctx.input.touch.clear_regions() # index 0 is No / 1 is Yes / -1 is Edge touch (ignore) - index = self.ctx.input.touch.current_index() - if index == 1: - return True - if index == 0: + 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 + ) + # wait a little to see item highlighted + time.sleep_ms(TOUCH_HIGHLIGHT_MS) + 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, - ) + 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, @@ -548,8 +510,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 = ( @@ -558,37 +532,63 @@ 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: + 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 + if menu_index == 0 and (self.ctx.input.buttons_active or highlight): self.ctx.display.outline( DEFAULT_PADDING, - go_esc_y_offset - FONT_HEIGHT // 2, + go_esc_box_y_offset, self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, FONT_HEIGHT + FONT_HEIGHT, theme.no_esc_color, ) + else: + # erase outline + self.ctx.display.outline( + DEFAULT_PADDING, + go_esc_box_y_offset, + self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, + FONT_HEIGHT + FONT_HEIGHT, + theme.bg_color, + ) self.ctx.display.draw_string( esc_x_offset, go_esc_y_offset, esc_txt, theme.no_esc_color ) - if menu_index == 1 and self.ctx.input.buttons_active: + if menu_index == 1 and (self.ctx.input.buttons_active or highlight): self.ctx.display.outline( - self.ctx.display.width() // 2 + DEFAULT_PADDING, - go_esc_y_offset - FONT_HEIGHT // 2, + esc_box_x_offset, + go_esc_box_y_offset, self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, FONT_HEIGHT + FONT_HEIGHT, theme.go_color, ) + else: + # erase outline + self.ctx.display.outline( + esc_box_x_offset, + go_esc_box_y_offset, + self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, + FONT_HEIGHT + FONT_HEIGHT, + theme.bg_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: + if not (self.ctx.input.buttons_active or highlight): self.ctx.display.draw_vline( self.ctx.display.width() // 2, go_esc_y_offset, FONT_HEIGHT, theme.frame_color, ) + else: + # erase vline + self.ctx.display.draw_vline( + self.ctx.display.width() // 2, + go_esc_y_offset, + FONT_HEIGHT, + theme.bg_color, + ) def choose_len_mnemonic(self, extra_option=""): """Reusable '12 or 24 words?" menu choice""" diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py index 854b3a74d..3cba8141f 100644 --- a/tests/pages/test_encryption_ui.py +++ b/tests/pages/test_encryption_ui.py @@ -694,6 +694,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)])) @@ -708,7 +709,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, []) @@ -717,7 +720,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) @@ -726,7 +729,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, []) @@ -735,7 +740,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, []) @@ -744,7 +749,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) @@ -753,7 +758,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, []) @@ -762,7 +769,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, []) @@ -771,7 +778,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) @@ -780,7 +787,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, []) @@ -789,7 +798,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, []) @@ -798,7 +807,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) @@ -807,7 +816,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, []) @@ -816,7 +827,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, []) @@ -825,7 +836,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) @@ -834,7 +845,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, []) @@ -843,7 +856,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) @@ -852,7 +865,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): From 8c7064a43f543e5870b2ab8b2e089ba359d527d8 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sat, 22 Nov 2025 01:17:10 -0300 Subject: [PATCH 11/29] Add highlight to stackbit --- src/krux/pages/stack_1248.py | 48 +++++++++++++++++------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/krux/pages/stack_1248.py b/src/krux/pages/stack_1248.py index 6f547b004..694fe4284 100644 --- a/src/krux/pages/stack_1248.py +++ b/src/krux/pages/stack_1248.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.wordlists.bip39 import WORDLIST from . import Page from ..themes import theme @@ -33,6 +34,7 @@ FAST_FORWARD, FAST_BACKWARD, SWIPE_FAIL, + TOUCH_HIGHLIGHT_MS, ) from ..kboard import kboard @@ -302,6 +304,15 @@ def _draw_index(self, index): color, ) + def _draw_esc_go_vline(self, color=theme.frame_color): + """draw line between ESC and GO for touch devices""" + self.ctx.display.draw_vline( + self.x_offset + 4 * self.x_pad, + self.y_offset + 5 * self.y_pad + FONT_HEIGHT // 2, + FONT_HEIGHT, + color, + ) + def _draw_menu(self): """Draws options to leave and proceed""" y_offset = self.y_offset + 5 * self.y_pad @@ -319,31 +330,8 @@ def _draw_menu(self): 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, - 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 + if not self.ctx.input.buttons_active: + self._draw_esc_go_vline() def digits_to_word(self, digits): """Returns seed word respective to digits BIP39 dictionaty position""" @@ -433,8 +421,16 @@ def enter_1248(self): btn = self.ctx.input.wait_for_fastnav_button() if btn == BUTTON_TOUCH: index = self.ctx.input.touch.current_index() - if index < 0: + if index < 0 or 13 < index < STACKBIT_ESC_INDEX: continue + + # Highlight the touched btn + self._draw_index(index) + # erase vline + self._draw_esc_go_vline(theme.bg_color) + # wait a little to see item highlighted + time.sleep_ms(TOUCH_HIGHLIGHT_MS) + btn = BUTTON_ENTER if btn == BUTTON_ENTER: if index >= STACKBIT_GO_INDEX: # go From d336ab8c07b14dbe9b346c29dd61c5071eef4804 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sat, 22 Nov 2025 01:47:57 -0300 Subject: [PATCH 12/29] Add highlight to tinyseed --- CHANGELOG.md | 1 + src/krux/pages/__init__.py | 72 +++++++++++++++++-------------------- src/krux/pages/tiny_seed.py | 22 ++++++++++-- 3 files changed, 53 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da361053b..8cef60bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Krux now displays a warning instead of blocking QR-encoded passphrases that cont ### 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 a double-outline highlight on touch devices for both touch feedback and button navigation. +- Added touch-feedback highlighting to confirmation prompts, Stackbit 1248, Tinyseed ### Other Bug Fixes and Improvements - Settings: Reduced default _Buttons Debounce_ value (with an even lower default on _M5StickV_) diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index fd82eaba4..c3a345024 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -516,6 +516,32 @@ def proceed_menu_text_y_offset(self, y_offset): self.ctx.display.height() - (y_offset + FONT_HEIGHT + MINIMAL_PADDING) ) // 2 + y_offset + def _draw_procced_outline_esc(self, offset_y, color=theme.no_esc_color): + self.ctx.display.outline( + DEFAULT_PADDING, + offset_y, + self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, + FONT_HEIGHT * 2, + color, + ) + + def _draw_proceed_outline_go(self, offset_x, offset_y, color=theme.go_color): + self.ctx.display.outline( + offset_x, + offset_y, + self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, + FONT_HEIGHT * 2, + color, + ) + + def _draw_proceed_vline(self, offset_y, color=theme.frame_color): + self.ctx.display.draw_vline( + self.ctx.display.width() // 2, + offset_y, + FONT_HEIGHT, + color, + ) + def draw_proceed_menu( self, go_txt, @@ -536,59 +562,27 @@ def draw_proceed_menu( go_esc_box_y_offset = go_esc_y_offset - FONT_HEIGHT // 2 esc_box_x_offset = self.ctx.display.width() // 2 + DEFAULT_PADDING if menu_index == 0 and (self.ctx.input.buttons_active or highlight): - self.ctx.display.outline( - DEFAULT_PADDING, - go_esc_box_y_offset, - self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, - FONT_HEIGHT + FONT_HEIGHT, - theme.no_esc_color, - ) + self._draw_procced_outline_esc(go_esc_box_y_offset) else: # erase outline - self.ctx.display.outline( - DEFAULT_PADDING, - go_esc_box_y_offset, - self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, - FONT_HEIGHT + FONT_HEIGHT, - theme.bg_color, - ) + self._draw_procced_outline_esc(go_esc_box_y_offset, theme.bg_color) self.ctx.display.draw_string( esc_x_offset, go_esc_y_offset, esc_txt, theme.no_esc_color ) if menu_index == 1 and (self.ctx.input.buttons_active or highlight): - self.ctx.display.outline( - esc_box_x_offset, - go_esc_box_y_offset, - self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, - FONT_HEIGHT + FONT_HEIGHT, - theme.go_color, - ) + self._draw_proceed_outline_go(esc_box_x_offset, go_esc_box_y_offset) else: # erase outline - self.ctx.display.outline( - esc_box_x_offset, - go_esc_box_y_offset, - self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, - FONT_HEIGHT + FONT_HEIGHT, - theme.bg_color, + self._draw_proceed_outline_go( + esc_box_x_offset, go_esc_box_y_offset, theme.bg_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 or highlight): - self.ctx.display.draw_vline( - self.ctx.display.width() // 2, - go_esc_y_offset, - FONT_HEIGHT, - theme.frame_color, - ) + self._draw_proceed_vline(go_esc_y_offset) else: # erase vline - self.ctx.display.draw_vline( - self.ctx.display.width() // 2, - go_esc_y_offset, - FONT_HEIGHT, - theme.bg_color, - ) + self._draw_proceed_vline(go_esc_y_offset, theme.bg_color) def choose_len_mnemonic(self, extra_option=""): """Reusable '12 or 24 words?" menu choice""" diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 96b6a28de..4feca1729 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -44,6 +44,7 @@ FAST_FORWARD, FAST_BACKWARD, SWIPE_FAIL, + TOUCH_HIGHLIGHT_MS, ) from ..bip39 import entropy_checksum from ..kboard import kboard @@ -360,6 +361,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() @@ -367,12 +370,12 @@ 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) @@ -384,6 +387,19 @@ def _editable_bit(): index = self.ctx.input.touch.current_index() if index < 0: 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, + ) + # wait a little to see item highlighted + time.sleep_ms(TOUCH_HIGHLIGHT_MS) btn = BUTTON_ENTER if btn == BUTTON_ENTER: if index > TS_ESC_END_POSITION: # "Go" From 06c012458e3b25678a77d1f428f4378bdfa5c442 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sat, 22 Nov 2025 11:53:28 -0300 Subject: [PATCH 13/29] Tinyseed avoid highlight disabled bits --- src/krux/pages/tiny_seed.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 4feca1729..8bb5598a5 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -250,7 +250,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) @@ -385,8 +385,16 @@ def _editable_bit(): btn = self.ctx.input.wait_for_fastnav_button() if btn == BUTTON_TOUCH: index = self.ctx.input.touch.current_index() - if index < 0: + # 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) From b4bde201004432f635212d34a9afea9a9ba5ff2b Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sat, 22 Nov 2025 20:01:36 -0300 Subject: [PATCH 14/29] Add touch-feedback highlighting to mnemonic editor --- CHANGELOG.md | 2 +- src/krux/pages/mnemonic_editor.py | 25 ++++++++++++---- src/krux/pages/settings_page.py | 48 +++++++++++++++---------------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cef60bce..ed91ed476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Krux now displays a warning instead of blocking QR-encoded passphrases that cont ### 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 a double-outline highlight on touch devices for both touch feedback and button navigation. -- Added touch-feedback highlighting to confirmation prompts, Stackbit 1248, Tinyseed +- Added touch-feedback highlighting to confirmation prompts, Stackbit 1248, Tinyseed and Mnemonic editor. ### Other Bug Fixes and Improvements - Settings: Reduced default _Buttons Debounce_ value (with an even lower default on _M5StickV_) diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index e2df5475c..c25eaa912 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 @@ -34,6 +35,7 @@ FAST_FORWARD, FAST_BACKWARD, SWIPE_FAIL, + TOUCH_HIGHLIGHT_MS, ) from ..key import Key from ..kboard import kboard @@ -160,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): @@ -211,7 +213,9 @@ def word_color(index): x_padding = MINIMAL_PADDING while word_index < 12: paged_index = word_index + page * 12 - if word_index == button_index and self.ctx.input.buttons_active: + if word_index == button_index and ( + self.ctx.input.buttons_active or highlight + ): self.ctx.display.draw_string( x_padding, y_region, @@ -227,7 +231,9 @@ def word_color(index): word_color(paged_index), ) if self.mnemonic_length == 24 and not kboard.is_m5stickv: - 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 + ): self.ctx.display.draw_string( MINIMAL_PADDING + self.ctx.display.width() // 2, y_region, @@ -255,10 +261,15 @@ def word_color(index): go_txt = t("Go") esc_txt = t("Esc") menu_index = None - if self.ctx.input.buttons_active and button_index >= ESC_INDEX: + 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, menu_index, self.valid_checksum + go_txt, + esc_txt, + y_region, + menu_index, + self.valid_checksum, + highlight=highlight, ) def edit_word(self, index): @@ -311,6 +322,10 @@ def edit(self): button_index += 12 else: button_index //= 2 + # Highlight the touched btn + self._map_words(button_index, page, highlight=True) + # wait a little to see item highlighted + time.sleep_ms(TOUCH_HIGHLIGHT_MS) btn = BUTTON_ENTER if btn == BUTTON_ENTER: if button_index == GO_INDEX: diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py index f06d7a375..df2deb86e 100644 --- a/src/krux/pages/settings_page.py +++ b/src/krux/pages/settings_page.py @@ -91,29 +91,28 @@ def settings(self): def _draw_settings_pad(self): """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 + self.ctx.display.draw_string( + offset_x, offset_y, keys[i], theme.fg_color, theme.bg_color + ) def _touch_to_physical(self, index): """Mimics touch presses into physical button presses""" @@ -392,7 +391,8 @@ def category_setting(self, settings_namespace, setting): color, theme.bg_color, ) - self._draw_settings_pad() + if kboard.has_touchscreen: + self._draw_settings_pad() # wait until valid input is captured btn = BUTTON_TOUCH From 8c9e466552ee1c3d007c2fa25301b8eedbc24990 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sat, 22 Nov 2025 22:12:23 -0300 Subject: [PATCH 15/29] Changed highlight style for prompt --- CHANGELOG.md | 2 +- src/krux/pages/__init__.py | 75 ++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed91ed476..0247f872d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Krux now displays a warning instead of blocking QR-encoded passphrases that cont ### 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 a double-outline highlight on touch devices for both touch feedback and button navigation. +- Refined keypad visuals with a clearer keyset index and touch-feedback highlighting. - Added touch-feedback highlighting to confirmation prompts, Stackbit 1248, Tinyseed and Mnemonic editor. ### Other Bug Fixes and Improvements diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index c3a345024..74ea11e3b 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -516,32 +516,6 @@ def proceed_menu_text_y_offset(self, y_offset): self.ctx.display.height() - (y_offset + FONT_HEIGHT + MINIMAL_PADDING) ) // 2 + y_offset - def _draw_procced_outline_esc(self, offset_y, color=theme.no_esc_color): - self.ctx.display.outline( - DEFAULT_PADDING, - offset_y, - self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, - FONT_HEIGHT * 2, - color, - ) - - def _draw_proceed_outline_go(self, offset_x, offset_y, color=theme.go_color): - self.ctx.display.outline( - offset_x, - offset_y, - self.ctx.display.width() // 2 - 2 * DEFAULT_PADDING, - FONT_HEIGHT * 2, - color, - ) - - def _draw_proceed_vline(self, offset_y, color=theme.frame_color): - self.ctx.display.draw_vline( - self.ctx.display.width() // 2, - offset_y, - FONT_HEIGHT, - color, - ) - def draw_proceed_menu( self, go_txt, @@ -561,28 +535,43 @@ def draw_proceed_menu( 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): - self._draw_procced_outline_esc(go_esc_box_y_offset) - else: - # erase outline - self._draw_procced_outline_esc(go_esc_box_y_offset, theme.bg_color) + 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 ) + 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): - self._draw_proceed_outline_go(esc_box_x_offset, go_esc_box_y_offset) - else: - # erase outline - self._draw_proceed_outline_go( - esc_box_x_offset, go_esc_box_y_offset, theme.bg_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) + 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._draw_proceed_vline(go_esc_y_offset) - else: - # erase vline - self._draw_proceed_vline(go_esc_y_offset, theme.bg_color) + self.ctx.display.draw_vline( + self.ctx.display.width() // 2, + go_esc_y_offset, + FONT_HEIGHT, + theme.frame_color, + ) def choose_len_mnemonic(self, extra_option=""): """Reusable '12 or 24 words?" menu choice""" From 251aa77da932e28ce065a2e30792b44b65bd0491 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sun, 23 Nov 2025 00:23:17 -0300 Subject: [PATCH 16/29] Change highlight style for mnemonic_editor --- src/krux/pages/mnemonic_editor.py | 47 ++++++++++++++++++------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index c25eaa912..6535bb23b 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -209,13 +209,35 @@ 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() //2 + x_padding_24w = MINIMAL_PADDING + self.ctx.display.width() // 2 + + # 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 if word_index == button_index and ( self.ctx.input.buttons_active or highlight ): + self.ctx.display.fill_rectangle(0, y_region-2, rect_width, word_v_padding+1, word_color(paged_index)) self.ctx.display.draw_string( x_padding, y_region, @@ -234,8 +256,10 @@ def word_color(index): if word_index + 12 == button_index and ( self.ctx.input.buttons_active or highlight ): + # x_padding = MINIMAL_PADDING + self.ctx.display.width() // 2 + self.ctx.display.fill_rectangle(self.ctx.display.width() // 2, y_region-2, rect_width, word_v_padding+1, word_color(paged_index)) self.ctx.display.draw_string( - MINIMAL_PADDING + self.ctx.display.width() // 2, + x_padding_24w, y_region, str(word_index + 13) + "." @@ -245,7 +269,7 @@ def word_color(index): ) else: self.ctx.display.draw_string( - MINIMAL_PADDING + self.ctx.display.width() // 2, + x_padding_24w, y_region, str(word_index + 13) + "." @@ -255,23 +279,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 or highlight) 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, - highlight=highlight, - ) - def edit_word(self, index): """Edit a word""" word_txt = str(index + 1) + ". " + self.current_mnemonic[index] @@ -322,6 +329,8 @@ def edit(self): button_index += 12 else: button_index //= 2 + # clear screen + 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) # Highlight the touched btn self._map_words(button_index, page, highlight=True) # wait a little to see item highlighted From ddcb7b4b4a2de7fbcefcfe6cede68daf72aefa18 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sun, 23 Nov 2025 00:38:12 -0300 Subject: [PATCH 17/29] Hold enter on tinyseed change index to Go --- src/krux/pages/mnemonic_editor.py | 28 +++++++++++++++++++++++----- src/krux/pages/tiny_seed.py | 3 +++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index 6535bb23b..a29d2ca17 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -212,7 +212,7 @@ def word_color(index): rect_width = self.ctx.display.width() else: x_padding = MINIMAL_PADDING - rect_width = self.ctx.display.width() //2 + rect_width = self.ctx.display.width() // 2 x_padding_24w = MINIMAL_PADDING + self.ctx.display.width() // 2 # draw Go and Esc before because they can overlap @@ -226,7 +226,7 @@ def word_color(index): self.draw_proceed_menu( go_txt, esc_txt, - y_region + 12*word_v_padding, + y_region + 12 * word_v_padding, menu_index, self.valid_checksum, highlight=highlight, @@ -237,7 +237,13 @@ def word_color(index): if word_index == button_index and ( self.ctx.input.buttons_active or highlight ): - self.ctx.display.fill_rectangle(0, y_region-2, rect_width, word_v_padding+1, word_color(paged_index)) + self.ctx.display.fill_rectangle( + 0, + y_region - 2, + rect_width, + word_v_padding + 1, + word_color(paged_index), + ) self.ctx.display.draw_string( x_padding, y_region, @@ -257,7 +263,13 @@ def word_color(index): self.ctx.input.buttons_active or highlight ): # x_padding = MINIMAL_PADDING + self.ctx.display.width() // 2 - self.ctx.display.fill_rectangle(self.ctx.display.width() // 2, y_region-2, rect_width, word_v_padding+1, word_color(paged_index)) + self.ctx.display.fill_rectangle( + self.ctx.display.width() // 2, + y_region - 2, + rect_width, + word_v_padding + 1, + word_color(paged_index), + ) self.ctx.display.draw_string( x_padding_24w, y_region, @@ -330,7 +342,13 @@ def edit(self): else: button_index //= 2 # clear screen - 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.ctx.display.fill_rectangle( + 0, + self.header_offset, + self.ctx.display.width(), + self.ctx.display.height() * 3 // 4 + FONT_HEIGHT // 2, + theme.bg_color, + ) # Highlight the touched btn self._map_words(button_index, page, highlight=True) # wait a little to see item highlighted diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 8bb5598a5..9765b0bca 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -44,6 +44,7 @@ FAST_FORWARD, FAST_BACKWARD, SWIPE_FAIL, + SWIPE_LEFT, TOUCH_HIGHLIGHT_MS, ) from ..bip39 import entropy_checksum @@ -341,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): From e12ad5dca800bbce12cecc5787da3a66d31c405e Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sun, 23 Nov 2025 14:18:39 -0300 Subject: [PATCH 18/29] Change highlight style on keypad --- src/krux/pages/keypads.py | 149 +++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 76 deletions(-) diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index 996f8c695..01b7eae40 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -147,74 +147,85 @@ 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 = theme.fg_color + 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 - ) - 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, - ) - self.ctx.display.draw_string( - key_offset_x, offset_y, key, custom_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 + 1, + y + 1, + 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, ) - if ( - key_index == self.cur_key_index - and self.ctx.input.buttons_active - ): - if kboard.has_touchscreen: - for i in range(1, 3): - self.ctx.display.outline( - offset_x + i, - y + i, - self.layout.key_h_spacing - i * 2, - self.layout.key_v_spacing - i * 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): @@ -258,6 +269,7 @@ 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() if self.cur_key_index < 0: self.cur_key_index = 0 @@ -267,27 +279,6 @@ def touch_to_physical(self): if self.has_more_key(): special_keys.append(self.more_index) - # Highlight the touched key - if ( - self.cur_key_index < len(self.keys) - and self.keys[self.cur_key_index] in self.possible_keys - ) or self.cur_key_index in special_keys: - key_index = 0 - for y in self.layout.y_keypad_map[:-1]: - for x in self.layout.x_keypad_map[:-1]: - if key_index == self.cur_key_index: - offset_x = MINIMAL_PADDING if x == 0 else x - for i in range(1, 3): - self.ctx.display.outline( - offset_x + i, - y + i, - self.layout.key_h_spacing - i * 2, - self.layout.key_v_spacing - i * 2, - ) - # wait a little to see item outlined - time.sleep_ms(TOUCH_HIGHLIGHT_MS) - key_index += 1 - actual_button = BUTTON_TOUCH if self.cur_key_index < len(self.keys): if self.keys[self.cur_key_index] in self.possible_keys: @@ -297,6 +288,12 @@ def touch_to_physical(self): else: self.cur_key_index = 0 + if actual_button == BUTTON_ENTER: + # Highlight the touched key + self.draw_keys(prev_index=prev_index) + # wait a little to see item highlighted + time.sleep_ms(TOUCH_HIGHLIGHT_MS) + return actual_button def navigate(self, btn): From d5634a269ad12d7fc77cbe45358db65bcbe7ceea Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sun, 23 Nov 2025 19:49:53 -0300 Subject: [PATCH 19/29] change highlight style on stack1248 --- src/krux/pages/mnemonic_editor.py | 3 +- src/krux/pages/stack_1248.py | 96 ++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index a29d2ca17..1ed73ee5c 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -262,7 +262,6 @@ def word_color(index): if word_index + 12 == button_index and ( self.ctx.input.buttons_active or highlight ): - # x_padding = MINIMAL_PADDING + self.ctx.display.width() // 2 self.ctx.display.fill_rectangle( self.ctx.display.width() // 2, y_region - 2, @@ -341,7 +340,7 @@ def edit(self): button_index += 12 else: button_index //= 2 - # clear screen + # clear words area to remove any highligh from btn self.ctx.display.fill_rectangle( 0, self.header_offset, diff --git a/src/krux/pages/stack_1248.py b/src/krux/pages/stack_1248.py index 694fe4284..eeb2e1daa 100644 --- a/src/krux/pages/stack_1248.py +++ b/src/krux/pages/stack_1248.py @@ -23,6 +23,7 @@ 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 @@ -46,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 @@ -278,30 +279,38 @@ 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_esc_go_vline(self, color=theme.frame_color): @@ -313,25 +322,42 @@ def _draw_esc_go_vline(self, color=theme.frame_color): 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, ) - if not self.ctx.input.buttons_active: - self._draw_esc_go_vline() + + self._draw_esc_go_vline() def digits_to_word(self, digits): """Returns seed word respective to digits BIP39 dictionaty position""" @@ -383,9 +409,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""" @@ -409,9 +436,9 @@ 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) @@ -426,8 +453,7 @@ def enter_1248(self): # Highlight the touched btn self._draw_index(index) - # erase vline - self._draw_esc_go_vline(theme.bg_color) + self._draw_menu(index, touch_highlight=True) # wait a little to see item highlighted time.sleep_ms(TOUCH_HIGHLIGHT_MS) From e74d807a67f90a54310a51f51684ef2aa15e38fb Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sun, 23 Nov 2025 21:03:58 -0300 Subject: [PATCH 20/29] optimize touch region detection --- src/krux/touch.py | 88 +++++++++++++++++------------------------------ 1 file changed, 32 insertions(+), 56 deletions(-) diff --git a/src/krux/touch.py b/src/krux/touch.py index dfc39ad47..0be20dcb6 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -39,7 +39,7 @@ SWIPE_NONE = 5 TOUCH_S_PERIOD = 20 # Touch sample period - Min = 10 -EDGE_PIXELS = 2 # The ammount of pixels to determine the edges of a region +EDGE_PIXELS = 1 # The ammount of pixels to determine the edges of a region class Touch: @@ -147,67 +147,43 @@ 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 - - # Calculate y index - if self.y_regions: - for region in self.y_regions: - if data[1] > region: - y_index += 1 - - # If touch was between adjacent regions, discard it as an edge touch - if ( - (0 < y_index < len(self.y_regions) - 1) - and ( - data[1] - EDGE_PIXELS - <= self.y_regions[y_index] - <= data[1] + EDGE_PIXELS - ) - ) or ( - (0 < y_index - 1) - and ( - data[1] - EDGE_PIXELS - <= self.y_regions[y_index - 1] - <= data[1] + EDGE_PIXELS - ) - ): + x, y = data + + # Helper to deal with X/Y regions + def _compute_axis_index(pos, regions): + if not regions: + return 0 + + # Count how many region boundaries pos passed + idx = sum(pos >= r for r in regions) + + # Edge detection: touching either adjacent boundary + # Check boundary at idx (right-side) + if idx < len(regions) and abs(pos - regions[idx]) <= EDGE_PIXELS: return -1 - y_index -= 1 if y_index > 0 else 0 - index = y_index - # Calculate x index if x regions are defined (2D array) - 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 - - # If touch was between adjacent regions, discard it as an edge touch - if ( - (0 < x_index < len(self.x_regions) - 1) - and ( - data[0] - EDGE_PIXELS - <= self.x_regions[x_index] - <= data[0] + EDGE_PIXELS - ) - ) or ( - (0 < x_index - 1) - and ( - data[0] - EDGE_PIXELS - <= self.x_regions[x_index - 1] - <= data[0] + EDGE_PIXELS - ) - ): + # Check boundary at idx-1 (left-side) + if idx > 0 and abs(pos - regions[idx - 1]) <= EDGE_PIXELS: return -1 - # Combine y and x indices to get the final index - index = y_index * (len(self.x_regions) - 1) + x_index + # Valid index never below 0 + return max(idx - 1, 0) + + # Y index + y_index = _compute_axis_index(y, self.y_regions) + if y_index < 0: + return -1 + + # X index + if self.x_regions: + x_index = _compute_axis_index(x, self.x_regions) + if x_index < 0: + return -1 + return y_index * (len(self.x_regions) - 1) + x_index - # self.highlight_region(x_index, y_index) - return index + return y_index def set_regions(self, x_list=None, y_list=None): """Set buttons map regions x and y""" From 7aab01d54b033d8345ddc36432a9b64f3a76f242 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sun, 23 Nov 2025 21:28:10 -0300 Subject: [PATCH 21/29] Optimization: right-shift instead of // 2 mnemonicEditor --- src/krux/pages/mnemonic_editor.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index 1ed73ee5c..f7a2c5dd0 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -176,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, @@ -212,8 +211,8 @@ def word_color(index): rect_width = self.ctx.display.width() else: x_padding = MINIMAL_PADDING - rect_width = self.ctx.display.width() // 2 - x_padding_24w = MINIMAL_PADDING + self.ctx.display.width() // 2 + rect_width = self.ctx.display.width() >> 1 + x_padding_24w = MINIMAL_PADDING + (self.ctx.display.width() >> 1) # draw Go and Esc before because they can overlap go_txt = t("Go") @@ -263,7 +262,7 @@ def word_color(index): self.ctx.input.buttons_active or highlight ): self.ctx.display.fill_rectangle( - self.ctx.display.width() // 2, + self.ctx.display.width() >> 1, y_region - 2, rect_width, word_v_padding + 1, @@ -336,10 +335,9 @@ def edit(self): continue if button_index < ESC_INDEX: if self.mnemonic_length == 24 and button_index % 2 == 1: - button_index //= 2 - button_index += 12 + button_index = (button_index >> 1) + 12 else: - button_index //= 2 + button_index >>= 1 # clear words area to remove any highligh from btn self.ctx.display.fill_rectangle( 0, @@ -365,7 +363,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) @@ -373,7 +371,7 @@ def edit(self): self.ctx.display.clear() if self.prompt( str(button_index + 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() From 1bfaac536fc43293c914dde313762b21a96355f2 Mon Sep 17 00:00:00 2001 From: tadeubas Date: Sun, 23 Nov 2025 22:31:30 -0300 Subject: [PATCH 22/29] minor UI changes for small screens --- src/krux/pages/keypads.py | 4 ++-- src/krux/pages/mnemonic_editor.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index 01b7eae40..fb92ea29c 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -194,9 +194,9 @@ def draw_keys(self, prev_index=None): self.ctx.input.buttons_active or prev_index is not None ): self.ctx.display.fill_rectangle( - offset_x + 1, + offset_x if kboard.is_m5stickv else offset_x + 1, y + 1, - self.layout.key_h_spacing - 2, + self.layout.key_h_spacing - 1 if kboard.is_m5stickv else self.layout.key_h_spacing - 2, self.layout.key_v_spacing - 2, color, ) diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index f7a2c5dd0..dfd14f367 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -238,9 +238,9 @@ def word_color(index): ): self.ctx.display.fill_rectangle( 0, - y_region - 2, + y_region if kboard.has_minimal_display else y_region - 2, rect_width, - word_v_padding + 1, + word_v_padding if kboard.has_minimal_display else word_v_padding + 1, word_color(paged_index), ) self.ctx.display.draw_string( @@ -263,9 +263,9 @@ def word_color(index): ): self.ctx.display.fill_rectangle( self.ctx.display.width() >> 1, - y_region - 2, + y_region if kboard.has_minimal_display else y_region - 2, rect_width, - word_v_padding + 1, + word_v_padding if kboard.has_minimal_display else word_v_padding + 1, word_color(paged_index), ) self.ctx.display.draw_string( From 365b3a8f758101c975a813e678626b6af953992a Mon Sep 17 00:00:00 2001 From: tadeubas Date: Mon, 24 Nov 2025 09:23:39 -0300 Subject: [PATCH 23/29] black --- src/krux/pages/keypads.py | 6 +++++- src/krux/pages/mnemonic_editor.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index fb92ea29c..75e4b74a9 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -196,7 +196,11 @@ def draw_keys(self, prev_index=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_h_spacing - 1 + if kboard.is_m5stickv + else self.layout.key_h_spacing - 2 + ), self.layout.key_v_spacing - 2, color, ) diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index dfd14f367..4455a8189 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -240,7 +240,11 @@ def word_color(index): 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, + ( + word_v_padding + if kboard.has_minimal_display + else word_v_padding + 1 + ), word_color(paged_index), ) self.ctx.display.draw_string( @@ -265,7 +269,11 @@ def word_color(index): 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, + ( + word_v_padding + if kboard.has_minimal_display + else word_v_padding + 1 + ), word_color(paged_index), ) self.ctx.display.draw_string( From 0a8d660f667ab59f2165c0bad5c9ce545c73a53b Mon Sep 17 00:00:00 2001 From: tadeubas Date: Mon, 24 Nov 2025 23:06:31 -0300 Subject: [PATCH 24/29] Test coverage --- src/krux/touch.py | 11 +++-------- tests/pages/test_keypads.py | 10 ++++++++++ tests/pages/test_menu.py | 10 ++++++++++ tests/pages/test_mnemonic_editor.py | 1 + tests/pages/test_settings_page.py | 4 ++++ tests/pages/test_stackbit.py | 18 ++++++++++++++++-- tests/pages/test_tiny_seed.py | 12 ++++++++++++ tests/test_input.py | 24 +++++++++++++++++++++++- tests/test_touch.py | 28 ++++++++++++++++++---------- 9 files changed, 97 insertions(+), 21 deletions(-) diff --git a/src/krux/touch.py b/src/krux/touch.py index 0be20dcb6..5eac03bd3 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -157,15 +157,10 @@ def _compute_axis_index(pos, regions): return 0 # Count how many region boundaries pos passed - idx = sum(pos >= r for r in regions) + idx = sum(pos + EDGE_PIXELS >= r for r in regions) - # Edge detection: touching either adjacent boundary - # Check boundary at idx (right-side) - if idx < len(regions) and abs(pos - regions[idx]) <= EDGE_PIXELS: - return -1 - - # Check boundary at idx-1 (left-side) - if idx > 0 and abs(pos - regions[idx - 1]) <= EDGE_PIXELS: + # # Check boundary at idx-1 (left-side) + if 1 < idx < len(regions) and abs(pos - regions[idx - 1]) <= EDGE_PIXELS: return -1 # Valid index never below 0 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 ae6106892..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) @@ -721,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 4ddecb473..43a46d2c7 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 @@ -206,6 +196,24 @@ def test_valid_position( ([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 + ([0, 100, 200], [], (100, 100), -1), # boundary y region: index -1 + ([0, 100, 200], [], (100, 200), 2), # last boundary y region ignore: index 2 + ([], [10, 100, 200], (100, 100), -1), # boundary x region: index -1 + ([], [0, 100, 200], (201, 100), 2), # second boundary x region: index -1 + ( + [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), 1), # edge of last y region + ([60, 120], [50, 100, 150], (70, 130), 2), # normal 2D index + ([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( From b4a5a9a22c8b3341365b4e036648c3bf6c549aaa Mon Sep 17 00:00:00 2001 From: tadeubas Date: Tue, 25 Nov 2025 00:46:56 -0300 Subject: [PATCH 25/29] Fix edge case for touch index --- src/krux/touch.py | 1 + tests/test_touch.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/krux/touch.py b/src/krux/touch.py index 5eac03bd3..3dc0fb0fa 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -174,6 +174,7 @@ def _compute_axis_index(pos, regions): # X index if self.x_regions: x_index = _compute_axis_index(x, self.x_regions) + x_index = x_index - 1 if x_index == len(self.x_regions) - 1 else x_index if x_index < 0: return -1 return y_index * (len(self.x_regions) - 1) + x_index diff --git a/tests/test_touch.py b/tests/test_touch.py index 43a46d2c7..1410f9a18 100644 --- a/tests/test_touch.py +++ b/tests/test_touch.py @@ -195,11 +195,24 @@ def test_valid_position( ([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), + 2, + ), # y=130 > 120: index 2 (y for last index is its value) ([0, 100, 200], [], (100, 100), -1), # boundary y region: index -1 ([0, 100, 200], [], (100, 200), 2), # last boundary y region ignore: index 2 + ([], [10, 100, 200], (0, 100), 0), # x=0 < 10: index 0 + ([], [10, 100, 200], (50, 100), 0), # x=50 < 100: index 0 ([], [10, 100, 200], (100, 100), -1), # boundary x region: index -1 - ([], [0, 100, 200], (201, 100), 2), # second 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 (x for last index is less 1) ( [0, 100, 200], [0, 100, 200], From 3954ed83bd2c8d27f8439e270329226c75989f6c Mon Sep 17 00:00:00 2001 From: tadeubas Date: Tue, 25 Nov 2025 10:08:39 -0300 Subject: [PATCH 26/29] Stack1248: v_line only for touch devices --- src/krux/pages/stack_1248.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/krux/pages/stack_1248.py b/src/krux/pages/stack_1248.py index eeb2e1daa..0fb465bd1 100644 --- a/src/krux/pages/stack_1248.py +++ b/src/krux/pages/stack_1248.py @@ -313,15 +313,6 @@ def _draw_index(self, index): x_pos + 2, y_pos + 1, 3 * self.x_pad - 4, self.y_pad, color ) - def _draw_esc_go_vline(self, color=theme.frame_color): - """draw line between ESC and GO for touch devices""" - self.ctx.display.draw_vline( - self.x_offset + 4 * self.x_pad, - self.y_offset + 5 * self.y_pad + FONT_HEIGHT // 2, - FONT_HEIGHT, - color, - ) - def _draw_menu(self, index, touch_highlight=False): """Draws options to leave and proceed""" y_offset = self.y_offset + 5 * self.y_pad @@ -357,7 +348,13 @@ def _draw_button(cond, x_mul, label, base_color): theme.go_color, ) - self._draw_esc_go_vline() + if kboard.has_touchscreen: + 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, + ) def digits_to_word(self, digits): """Returns seed word respective to digits BIP39 dictionaty position""" @@ -486,13 +483,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: From fe2a4706d99e5d97349d19b8470cc288421e126b Mon Sep 17 00:00:00 2001 From: tadeubas Date: Tue, 25 Nov 2025 10:27:04 -0300 Subject: [PATCH 27/29] test coverage 100% --- tests/test_touch.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_touch.py b/tests/test_touch.py index 1410f9a18..c819a69f9 100644 --- a/tests/test_touch.py +++ b/tests/test_touch.py @@ -312,6 +312,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( @@ -336,6 +337,29 @@ def test_gesture_detection( 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 From bc9c7abbc1112fcf9c93bf04cb5267f9a0b9535e Mon Sep 17 00:00:00 2001 From: tadeubas Date: Tue, 25 Nov 2025 12:35:20 -0300 Subject: [PATCH 28/29] Category Settings touch/btn highlight --- CHANGELOG.md | 2 +- src/krux/pages/__init__.py | 7 ++----- src/krux/pages/keypads.py | 6 ++---- src/krux/pages/mnemonic_editor.py | 6 ++---- src/krux/pages/settings_page.py | 30 +++++++++++++++++++++++++++--- src/krux/pages/stack_1248.py | 3 +-- src/krux/pages/tiny_seed.py | 3 +-- 7 files changed, 36 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b63cb3146..341004c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Exported Uniform Resource (UR) QR codes, a widely adopted standard for exchangin ### 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, Stackbit 1248, Tinyseed and Mnemonic editor. +- 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_) diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 74ea11e3b..dd5881aef 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -452,8 +452,7 @@ def prompt(self, text, offset_y=0, highlight_prefix=""): self.draw_proceed_menu( go_str, no_str, offset_y, new_index, highlight=True ) - # wait a little to see item highlighted - time.sleep_ms(TOUCH_HIGHLIGHT_MS) + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little if new_index == 1: return True return False @@ -809,15 +808,13 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None # highlight selected index before continue if was_btn_active: # need to clear screen if button was used just before - # because an item will be highlighted already self._clear_menu_display() self._draw_touch_menu( selected_item_index, draw_dividers=not was_btn_active, highlight=True, ) - # wait a little to see item highlighted - time.sleep_ms(TOUCH_HIGHLIGHT_MS) + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little btn = BUTTON_ENTER if kboard.has_touchscreen: self.ctx.input.touch.clear_regions() diff --git a/src/krux/pages/keypads.py b/src/krux/pages/keypads.py index 75e4b74a9..c7ac549f8 100644 --- a/src/krux/pages/keypads.py +++ b/src/krux/pages/keypads.py @@ -293,10 +293,8 @@ def touch_to_physical(self): self.cur_key_index = 0 if actual_button == BUTTON_ENTER: - # Highlight the touched key - self.draw_keys(prev_index=prev_index) - # wait a little to see item highlighted - time.sleep_ms(TOUCH_HIGHLIGHT_MS) + self.draw_keys(prev_index=prev_index) # highlight + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little return actual_button diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index 4455a8189..dc7db8d26 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -354,10 +354,8 @@ def edit(self): self.ctx.display.height() * 3 // 4 + FONT_HEIGHT // 2, theme.bg_color, ) - # Highlight the touched btn - self._map_words(button_index, page, highlight=True) - # wait a little to see item highlighted - time.sleep_ms(TOUCH_HIGHLIGHT_MS) + 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: diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py index df2deb86e..57bdb3c8a 100644 --- a/src/krux/pages/settings_page.py +++ b/src/krux/pages/settings_page.py @@ -49,11 +49,13 @@ BUTTON_PAGE_PREV, BUTTON_TOUCH, SWIPE_FAIL, + TOUCH_HIGHLIGHT_MS, ) from ..sd_card import SDHandler from . import ( Page, Menu, + Context, DIGITS, LETTERS, UPPERCASE_LETTERS, @@ -65,6 +67,7 @@ DEFAULT_PADDING, ) import os +import time from ..kboard import kboard PERSIST_MSG_TIME = 2500 @@ -81,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 @@ -89,7 +92,7 @@ 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""" self.ctx.input.touch.clear_regions() offset_y = self.ctx.display.height() * 2 // 3 @@ -110,8 +113,20 @@ def _draw_settings_pad(self): ) 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], theme.fg_color, theme.bg_color + offset_x, offset_y, keys[i], fg_color, bg_color ) def _touch_to_physical(self, index): @@ -401,14 +416,23 @@ def category_setting(self, settings_namespace, setting): 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 0fb465bd1..5a5d09b56 100644 --- a/src/krux/pages/stack_1248.py +++ b/src/krux/pages/stack_1248.py @@ -451,8 +451,7 @@ def enter_1248(self): # Highlight the touched btn self._draw_index(index) self._draw_menu(index, touch_highlight=True) - # wait a little to see item highlighted - time.sleep_ms(TOUCH_HIGHLIGHT_MS) + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little btn = BUTTON_ENTER if btn == BUTTON_ENTER: diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 9765b0bca..08f921b9a 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -409,8 +409,7 @@ def _editable_bit(): 1 if index > TS_ESC_END_POSITION else 0, highlight=True, ) - # wait a little to see item highlighted - time.sleep_ms(TOUCH_HIGHLIGHT_MS) + time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little btn = BUTTON_ENTER if btn == BUTTON_ENTER: if index > TS_ESC_END_POSITION: # "Go" From 688a297c7e54b167db9cdf248ea3faff2c282bfb Mon Sep 17 00:00:00 2001 From: tadeubas Date: Thu, 27 Nov 2025 22:36:29 -0300 Subject: [PATCH 29/29] Fix edge cases for touch --- src/krux/pages/__init__.py | 25 +++++++++++-------------- src/krux/touch.py | 9 ++++++--- tests/test_touch.py | 23 ++++++++++++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index dd5881aef..1a7e272c6 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -161,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() @@ -200,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): @@ -219,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() @@ -428,8 +427,8 @@ def prompt(self, text, offset_y=0, highlight_prefix=""): self.ctx.display.width() // 2, self.ctx.display.width() - DEFAULT_PADDING, ] - touch_offset_y = self.proceed_menu_text_y_offset(offset_y) - 2 * FONT_HEIGHT - self.y_keypad_map = [touch_offset_y, touch_offset_y + 4 * FONT_HEIGHT] + 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) @@ -816,8 +815,8 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None ) time.sleep_ms(TOUCH_HIGHLIGHT_MS) # wait a little btn = BUTTON_ENTER - if kboard.has_touchscreen: - self.ctx.input.touch.clear_regions() + if kboard.has_touchscreen: + self.ctx.input.touch.clear_regions() if btn == BUTTON_ENTER: status = self._clicked_item(selected_item_index) if status != MENU_CONTINUE: @@ -842,14 +841,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() diff --git a/src/krux/touch.py b/src/krux/touch.py index 3dc0fb0fa..71506711f 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -163,8 +163,8 @@ def _compute_axis_index(pos, regions): if 1 < idx < len(regions) and abs(pos - regions[idx - 1]) <= EDGE_PIXELS: return -1 - # Valid index never below 0 - return max(idx - 1, 0) + # 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) @@ -174,11 +174,14 @@ def _compute_axis_index(pos, regions): # X index if self.x_regions: x_index = _compute_axis_index(x, self.x_regions) - x_index = x_index - 1 if x_index == len(self.x_regions) - 1 else x_index 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(0, y_index) return y_index def set_regions(self, x_list=None, y_list=None): diff --git a/tests/test_touch.py b/tests/test_touch.py index c819a69f9..92c2f544b 100644 --- a/tests/test_touch.py +++ b/tests/test_touch.py @@ -191,7 +191,7 @@ 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 @@ -199,12 +199,12 @@ def test_valid_position( [40, 80, 120], [], (50, 130), - 2, - ), # y=130 > 120: index 2 (y for last index is its value) + 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), 2), # last boundary y region ignore: index 2 - ([], [10, 100, 200], (0, 100), 0), # x=0 < 10: index 0 - ([], [10, 100, 200], (50, 100), 0), # x=50 < 100: index 0 + ([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 ( @@ -212,7 +212,7 @@ def test_valid_position( [0, 100, 200], (201, 100), 1, - ), # x=201 > 200: index 1 (x for last index is less 1) + ), # x=201 > 200: index 1 (max==len(regions)-2) ( [0, 100, 200], [0, 100, 200], @@ -223,8 +223,13 @@ def test_valid_position( ([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), 1), # edge of last y region - ([60, 120], [50, 100, 150], (70, 130), 2), # normal 2D index + ([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 ],