From b29d9cae7b203f38b3f26a699c5b894886b3497b Mon Sep 17 00:00:00 2001 From: Jean Do Date: Tue, 17 Jan 2023 20:53:19 -0400 Subject: [PATCH 01/11] foreach test: fixture sets-up, runs-test, tears-down --- tests/test_controller.py | 67 ++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/tests/test_controller.py b/tests/test_controller.py index b6887369..13f4c62a 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -1,29 +1,34 @@ import configparser import pytest from mock import MagicMock +from seedsigner.hardware.microsd import MicroSD from seedsigner.controller import Controller -from seedsigner.models.settings import Settings from seedsigner.models.settings_definition import SettingsConstants -def test_singleton_init_fails(): +@pytest.fixture() +def reset_controller(): + print("\nEntering reset_controller fixture; setting up...") + Controller.configure_instance(disable_hardware=True) + print("...done setting up; yielding to run a test...") + yield + print("\n...back from the test; tearing down...") + MicroSD._instance = None + Controller._instance = None + print("...done tearing down; leaving reset_controller fixture.") + + +def test_singleton_init_fails(reset_controller): """ The Controller should not allow any code to instantiate it via Controller() """ with pytest.raises(Exception): c = Controller() -def test_singleton_get_instance_preserves_state(): +def test_singleton_get_instance_preserves_state(reset_controller): """ Changes to the Controller singleton should be preserved across calls to get_instance() """ - # Must reset Singleton instances; pytest cannot properly isolate Singletons for us - # automatically. - # TODO: Cleaner solution here would be nice. - Settings._instance = None - Controller._instance = None - # Initialize the instance and verify that it read the config settings - Controller.configure_instance(disable_hardware=True) controller = Controller.get_instance() assert controller.unverified_address is None @@ -35,19 +40,33 @@ def test_singleton_get_instance_preserves_state(): assert controller.unverified_address == "123abc" -def test_missing_settings_get_defaults(): - """ Should gracefully handle any missing fields from `settings.ini` """ - # TODO: This is not complete; currently only handles missing compact_seedqr_enabled. +def test_missing_settings_get_defaults(reset_controller): + """ Should gracefully handle all missing fields from `settings.json` """ - # Must reset Singleton instances; pytest cannot properly isolate Singletons for us - # automatically. - # TODO: Cleaner solution here would be nice. - Settings._instance = None - Controller._instance = None - - # Controller should parse the settings fine, even though a field is missing - Controller.configure_instance(disable_hardware=True) - - # Controller should still have a default value controller = Controller.get_instance() - assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__DISABLED + + # Settings defaults + assert controller.settings.get_value(SettingsConstants.SETTING__LANGUAGE) == SettingsConstants.LANGUAGE__ENGLISH + assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED + assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS] + assert controller.settings.get_value(SettingsConstants.SETTING__BTC_DENOMINATION) == SettingsConstants.BTC_DENOMINATION__THRESHOLD + + # Advanced Settings defaults + assert controller.settings.get_value(SettingsConstants.SETTING__NETWORK) == SettingsConstants.MAINNET + assert controller.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) == SettingsConstants.DENSITY__MEDIUM + assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__SIG_TYPES) == [i for i,j in SettingsConstants.ALL_SIG_TYPES] + assert controller.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) == [SettingsConstants.NATIVE_SEGWIT, SettingsConstants.NESTED_SEGWIT] + assert controller.settings.get_value(SettingsConstants.SETTING__XPUB_DETAILS) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__CAMERA_ROTATION) == SettingsConstants.CAMERA_ROTATION__0 + assert controller.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__DISABLED + assert controller.settings.get_value(SettingsConstants.SETTING__PRIVACY_WARNINGS) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED + assert controller.settings.get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED + + # Hidden Settings defaults + assert controller.settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS) == 189 + From a8e5e3a0fb9f5a3251dde2fe7afa6d139a036c23 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Thu, 19 Jan 2023 15:38:41 -0400 Subject: [PATCH 02/11] verbose print statements removed from reset_controller fixture. --- tests/test_controller.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_controller.py b/tests/test_controller.py index 13f4c62a..99c6f7b3 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -9,14 +9,17 @@ from seedsigner.models.settings_definition import SettingsConstants @pytest.fixture() def reset_controller(): - print("\nEntering reset_controller fixture; setting up...") + """fixture to setup, then yield to run test, then tear down""" + + # setup Controller.configure_instance(disable_hardware=True) - print("...done setting up; yielding to run a test...") + + # yield to run a single test yield - print("\n...back from the test; tearing down...") + + # tear down MicroSD._instance = None Controller._instance = None - print("...done tearing down; leaving reset_controller fixture.") def test_singleton_init_fails(reset_controller): From ef08cd830672600a541c709b6ba44a43af75c579 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Mon, 23 Jan 2023 17:35:36 -0400 Subject: [PATCH 03/11] improved ui: "BIP-39 Passphrase" * while finalizing: button "Add Passphrase" becomes "BIP-39 Passphrase" w/o lock icon * while adding/editing: title "Add Passphrase" becomes "BIP-39 Passphrase" --- src/seedsigner/gui/screens/seed_screens.py | 2 +- src/seedsigner/views/seed_views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index f41e3e8a..ab5d8ed4 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -616,7 +616,7 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen): @dataclass class SeedAddPassphraseScreen(BaseTopNavScreen): - title: str = "Add Passphrase" + title: str = "BIP-39 Passphrase" passphrase: str = "" KEYBOARD__LOWERCASE_BUTTON_TEXT = "abc" diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 39a5a1ad..8c24d851 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -211,7 +211,7 @@ class SeedFinalizeView(View): def run(self): FINALIZE = "Done" - PASSPHRASE = ("Add Passphrase", FontAwesomeIconConstants.LOCK) + PASSPHRASE = "BIP-39 Passphrase" button_data = [] button_data.append(FINALIZE) From 360d4be9d1a7abb21d6ad925454b16105cb3eb2f Mon Sep 17 00:00:00 2001 From: Jean Do Date: Wed, 25 Jan 2023 09:39:57 -0400 Subject: [PATCH 04/11] typo fix / text changes --- docs/recovery.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/recovery.md b/docs/recovery.md index 5725dc75..035bc339 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -23,7 +23,7 @@ Derivation paths for standard script types for mainnet: - Script Type: P2WSH in P2SH - Public Key Encoding: 0x0295b43f - Ypub -Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings is detected based on the derivation path configured. Embit bitcoin library does the detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explination of these standards see a presentation by Stepan of Embit/Specter on this topic: https://youtube.com/watch?v=JCaC5DG2HTM +Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings are detected based on the derivation path configured. The Embit bitcoin library does this detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explanation of these standards see a presentation by Stepan of Embit/Specter on this topic: https://youtube.com/watch?v=JCaC5DG2HTM Changing the network settings from main to test in SeedSigner will change the public key encoding and derivation path following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) standards. @@ -32,4 +32,4 @@ Related Standards: - [bip-0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) - [bip-0048](https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki) - [bip-0049](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) -- [bip-0084](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) \ No newline at end of file +- [bip-0084](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) From ccf8ce1e84743787590e87925268c436231551a5 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Wed, 25 Jan 2023 12:08:22 -0400 Subject: [PATCH 05/11] conditional power-off: 'Powering Off' or 'Just Unplug It' --- src/seedsigner/gui/screens/screen.py | 17 ++++++++++++++++- src/seedsigner/views/view.py | 20 +++++++++----------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 3da014a0..c0e01f3b 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -906,6 +906,21 @@ class PowerOffScreen(BaseTopNavScreen): +@dataclass +class PowerOffNotRequiredScreen(BaseTopNavScreen): + def __post_init__(self): + self.title = "Just Unplug It" + self.show_back_button = True + super().__post_init__() + + self.components.append(TextArea( + text="In SeedSigner-OS, it is safe to disconnect power at any time.", + screen_y=self.top_nav.height, + height=self.canvas_height - self.top_nav.height, + )) + + + @dataclass class KeyboardScreen(BaseTopNavScreen): """ @@ -1149,4 +1164,4 @@ class MicroSDToastScreen(BaseScreen): label_text="MicroSD inserted" ) - self.toast.render() \ No newline at end of file + self.toast.render() diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 50b1c40a..02543c4d 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -3,7 +3,7 @@ from typing import List from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants from seedsigner.gui.screens import RET_CODE__POWER_BUTTON -from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, LargeButtonScreen, PowerOffScreen, ResetScreen, WarningScreen +from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen from seedsigner.models.threads import BaseThread from seedsigner.models import Settings @@ -199,9 +199,13 @@ class RestartView(View): class PowerOffView(View): def run(self): - thread = PowerOffView.PowerOffThread() - thread.start() - PowerOffScreen().display() + if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: + PowerOffNotRequiredScreen().display() + return Destination(BackStackView) + else: + thread = PowerOffView.PowerOffThread() + thread.start() + PowerOffScreen().display() class PowerOffThread(BaseThread): @@ -210,13 +214,7 @@ class PowerOffView(View): from subprocess import call while self.keep_running: time.sleep(5) - if Settings.HOSTNAME == Settings.SEEDSIGNER_OS: - # disable microsd detection before shutdown to prevent display of toast notification during shutdown - from seedsigner.controller import Controller - Controller.get_instance().microsd.stop() - call("poweroff", shell=True) - else: - call("sudo shutdown --poweroff now", shell=True) + call("sudo shutdown --poweroff now", shell=True) From 231357a2db2c64ba66321eff69386ae285a4af6c Mon Sep 17 00:00:00 2001 From: Jean Do Date: Thu, 26 Jan 2023 17:39:52 -0400 Subject: [PATCH 06/11] Embit becomes `embit`; removed Specter --- docs/recovery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recovery.md b/docs/recovery.md index 035bc339..7e8569ff 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -23,7 +23,7 @@ Derivation paths for standard script types for mainnet: - Script Type: P2WSH in P2SH - Public Key Encoding: 0x0295b43f - Ypub -Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings are detected based on the derivation path configured. The Embit bitcoin library does this detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explanation of these standards see a presentation by Stepan of Embit/Specter on this topic: https://youtube.com/watch?v=JCaC5DG2HTM +Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings are detected based on the derivation path configured. The `embit` bitcoin library does this detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explanation of these standards see a presentation by Stepan of `embit` on this topic: https://youtube.com/watch?v=JCaC5DG2HTM Changing the network settings from main to test in SeedSigner will change the public key encoding and derivation path following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) standards. From 98679505a3611f4443616b5c26b55d7ca0fc60ec Mon Sep 17 00:00:00 2001 From: Jean Do Date: Thu, 26 Jan 2023 17:43:32 -0400 Subject: [PATCH 07/11] No mention of OS since it's the new standard. --- src/seedsigner/gui/screens/screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index c0e01f3b..e32bf984 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -914,7 +914,7 @@ class PowerOffNotRequiredScreen(BaseTopNavScreen): super().__post_init__() self.components.append(TextArea( - text="In SeedSigner-OS, it is safe to disconnect power at any time.", + text="It is safe to disconnect power at any time.", screen_y=self.top_nav.height, height=self.canvas_height - self.top_nav.height, )) From 0a377a8db8d0a33f0b354c2ba173b88728172de8 Mon Sep 17 00:00:00 2001 From: Jean Do Date: Fri, 27 Jan 2023 10:30:46 -0400 Subject: [PATCH 08/11] Denomination: Btc-only/Sats-only become BTC/sats --- src/seedsigner/models/settings_definition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 273c78b9..e69478f3 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -53,10 +53,10 @@ class SettingsConstants: BTC_DENOMINATION__THRESHOLD = "thr" BTC_DENOMINATION__BTCSATSHYBRID = "hyb" ALL_BTC_DENOMINATIONS = [ - (BTC_DENOMINATION__BTC, "Btc-only"), - (BTC_DENOMINATION__SATS, "Sats-only"), + (BTC_DENOMINATION__BTC, "BTC"), + (BTC_DENOMINATION__SATS, "sats"), (BTC_DENOMINATION__THRESHOLD, "Threshold at 0.01"), - (BTC_DENOMINATION__BTCSATSHYBRID, "Btc | Sats hybrid"), + (BTC_DENOMINATION__BTCSATSHYBRID, "BTC | sats hybrid"), ] CAMERA_ROTATION__0 = 0 From 7481b45b9f03e2b872db8a487cdb1e3b96082036 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 28 Jan 2023 07:52:13 -0600 Subject: [PATCH 09/11] Preserve list state in Advanced settings --- src/seedsigner/gui/screens/screen.py | 10 +++++----- src/seedsigner/views/settings_views.py | 25 +++++++++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index e32bf984..50a94e70 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -265,6 +265,9 @@ class ButtonListScreen(BaseTopNavScreen): Button_cls = Button checked_buttons: List[int] = None + # Enables returning w/buttons rendered at the same place + scroll_y_initial_offset: int = None + def __post_init__(self): super().__post_init__() @@ -315,6 +318,7 @@ class ButtonListScreen(BaseTopNavScreen): right_icon_name=right_icon_name, screen_x=GUIConstants.EDGE_PADDING, screen_y=button_list_y + i * (button_height + GUIConstants.LIST_ITEM_PADDING), + scroll_y=self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0, width=self.canvas_width - (2 * GUIConstants.EDGE_PADDING), height=button_height, is_text_centered=self.is_button_text_centered, @@ -330,7 +334,7 @@ class ButtonListScreen(BaseTopNavScreen): if self.has_scroll_arrows: self.arrow_half_width = 10 - self.cur_scroll_y = 0 + self.cur_scroll_y = self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0 self.up_arrow_img = Image.new("RGBA", size=(2 * self.arrow_half_width, 8), color="black") self.up_arrow_img_y = self.top_nav.height - 12 arrow_draw = ImageDraw.Draw(self.up_arrow_img) @@ -346,10 +350,6 @@ class ButtonListScreen(BaseTopNavScreen): cur_selected_button = self.buttons[self.selected_button] cur_selected_button.is_selected = True - if self.has_scroll_arrows: - frame_scroll = self.buttons[0].screen_y - cur_selected_button.screen_y - for button in self.buttons: - button.scroll_y -= frame_scroll def _render(self): diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 655b61d1..7d5ef6c0 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -9,11 +9,14 @@ from seedsigner.models.settings import SettingsConstants, SettingsDefinition class SettingsMenuView(View): - def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None): + def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0): super().__init__() self.visibility = visibility self.selected_attr = selected_attr + # Used to preserve the rendering position in the list + self.initial_scroll = initial_scroll + def run(self): IO_TEST = "I/O test" @@ -53,12 +56,17 @@ class SettingsMenuView(View): title = "Dev Options" next = None - selected_menu_num = ButtonListScreen( + screen = ButtonListScreen( title=title, is_button_text_centered=False, button_data=button_data, selected_button=selected_button, - ).display() + scroll_y_initial_offset=self.initial_scroll, + ) + selected_menu_num = screen.display() + + # Preserve our scroll position in this Screen so we can return + initial_scroll = screen.buttons[0].scroll_y if selected_menu_num == RET_CODE__BACK_BUTTON: if self.visibility == SettingsConstants.VISIBILITY__GENERAL: @@ -78,8 +86,7 @@ class SettingsMenuView(View): return Destination(DonateView) else: - # TODO: Free-entry types (are there any?) will need their own SettingsEntryUpdateFreeEntryView(?). - return Destination(SettingsEntryUpdateSelectionView, view_args={"attr_name": settings_entries[selected_menu_num].attr_name}) + return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=settings_entries[selected_menu_num].attr_name, parent_initial_scroll=initial_scroll)) @@ -88,10 +95,11 @@ class SettingsEntryUpdateSelectionView(View): Handles changes to all selection-type settings (Multiselect, SELECT_1, Enabled/Disabled, etc). """ - def __init__(self, attr_name: str): + def __init__(self, attr_name: str, parent_initial_scroll: int = 0): super().__init__() self.settings_entry = SettingsDefinition.get_settings_entry(attr_name) self.selected_button = None + self.parent_initial_scroll = parent_initial_scroll def run(self): @@ -129,7 +137,8 @@ class SettingsEntryUpdateSelectionView(View): SettingsMenuView, view_args={ "visibility": self.settings_entry.visibility, - "selected_attr": self.settings_entry.attr_name + "selected_attr": self.settings_entry.attr_name, + "initial_scroll": self.parent_initial_scroll, } ) @@ -188,4 +197,4 @@ class DonateView(View): def run(self): settings_screens.DonateScreen().display() - return Destination(SettingsMenuView) + return Destination(SettingsMenuView) \ No newline at end of file From 6c3b4016601697b83076ec3b8cea4cbcabe963f9 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 28 Jan 2023 08:01:41 -0600 Subject: [PATCH 10/11] Update settings_views.py --- src/seedsigner/views/settings_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 7d5ef6c0..8a13b43d 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -197,4 +197,4 @@ class DonateView(View): def run(self): settings_screens.DonateScreen().display() - return Destination(SettingsMenuView) \ No newline at end of file + return Destination(SettingsMenuView) From 357cd14e82c8261e0dbe97efded7b6126797bf33 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 28 Jan 2023 08:25:09 -0600 Subject: [PATCH 11/11] Updates AddressExplorer to keep scroll position --- src/seedsigner/views/tools_views.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 14a775d5..2c5fa017 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -559,11 +559,12 @@ class ToolsAddressExplorerAddressTypeView(View): class ToolsAddressExplorerAddressListView(View): - def __init__(self, is_change: bool = False, start_index: int = 0, selected_button_index: int = 0): + def __init__(self, is_change: bool = False, start_index: int = 0, selected_button_index: int = 0, initial_scroll: int = 0): super().__init__() self.is_change = is_change self.start_index = start_index self.selected_button_index = selected_button_index + self.initial_scroll = initial_scroll def run(self): @@ -632,6 +633,7 @@ class ToolsAddressExplorerAddressListView(View): is_button_text_centered=False, is_bottom_list=True, selected_button=self.selected_button_index, + scroll_y_initial_offset=self.initial_scroll, ) finally: # Everything is set. Stop the loading screen @@ -647,20 +649,23 @@ class ToolsAddressExplorerAddressListView(View): # User clicked NEXT return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index + addrs_per_screen)) + # Preserve the list's current scroll so we can return to the same spot + initial_scroll = screen.buttons[0].scroll_y + index = selected_menu_num + self.start_index - return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index), skip_current_view=True) + return Destination(ToolsAddressExplorerAddressView, view_args=dict(index=index, address=addresses[selected_menu_num], is_change=self.is_change, start_index=self.start_index, parent_initial_scroll=initial_scroll), skip_current_view=True) class ToolsAddressExplorerAddressView(View): - def __init__(self, index: int, address: str, is_change: bool, start_index: int): + def __init__(self, index: int, address: str, is_change: bool, start_index: int, parent_initial_scroll: int = 0): super().__init__() self.index = index self.address = address self.is_change = is_change self.start_index = start_index + self.parent_initial_scroll = parent_initial_scroll - data = self.controller.address_explorer_data def run(self): qr_encoder = EncodeQR(qr_type=QRType.BITCOIN_ADDRESS, bitcoin_address=self.address) @@ -669,4 +674,4 @@ class ToolsAddressExplorerAddressView(View): ).display() # Exiting/Cancelling the QR display screen always returns to the list - return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index), skip_current_view=True) + return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.is_change, start_index=self.start_index, selected_button_index=self.index - self.start_index, initial_scroll=self.parent_initial_scroll), skip_current_view=True)