diff --git a/docs/feature_roadmap.md b/docs/feature_roadmap.md index 12c08137..29174e09 100644 --- a/docs/feature_roadmap.md +++ b/docs/feature_roadmap.md @@ -29,10 +29,9 @@ Screens will be functional but not necessarily in their final presentation state * 12th/24th word calc * SeedQR/CompactSeedQR manual transcription UI w/configurable UI style (dots vs grid) * Single sig address scan and verification -* QR display framerate speed controls, pop up guide UI * SettingsQR standalone UI refinement * Fix broken tests -* Scrollable ListButtonView w/upper canvas area +* All GUI Components support scrollable Screens ## v0.5.0 Pre-Release 3.x diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 330b1a55..90d7b3d3 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -142,7 +142,7 @@ class Controller(Singleton): title="Warning", warning_headline="Pre-Release Code", warning_text="Do not use this with real funds or to create new secure keys!", - show_top_nav_back_button=False, + show_top_nav_left_button=False, ).display() diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 62604224..1ab526a9 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -835,8 +835,12 @@ class TopNav(BaseComponent): font_name: str = GUIConstants.TOP_NAV_TITLE_FONT_NAME font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE font_color: str = "#fcfcfc" - show_back_button: bool = True - show_power_button: bool = False + show_left_button: bool = True + left_button_icon_name: str = SeedSignerCustomIconConstants.LARGE_CHEVRON_LEFT + left_button_icon_color: str = GUIConstants.BUTTON_FONT_COLOR + show_right_button: bool = False + right_button_icon_name: str = FontAwesomeIconConstants.POWER_OFF + right_button_icon_color: str = GUIConstants.BUTTON_FONT_COLOR is_selected: bool = False @@ -847,20 +851,22 @@ class TopNav(BaseComponent): self.font = Fonts.get_font(self.font_name, self.font_size) - if self.show_back_button: + if self.show_left_button: self.back_button = IconButton( - icon_name=SeedSignerCustomIconConstants.LARGE_CHEVRON_LEFT, + icon_name=self.left_button_icon_name, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, + icon_color=self.left_button_icon_color, screen_x=GUIConstants.EDGE_PADDING, screen_y=GUIConstants.EDGE_PADDING, width=GUIConstants.TOP_NAV_BUTTON_SIZE, height=GUIConstants.TOP_NAV_BUTTON_SIZE, ) - if self.show_power_button: + if self.show_right_button: self.power_button = IconButton( - icon_name=FontAwesomeIconConstants.POWER_OFF, + icon_name=self.right_button_icon_name, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, + icon_color=self.right_button_icon_color, screen_x=self.width - GUIConstants.TOP_NAV_BUTTON_SIZE - GUIConstants.EDGE_PADDING, screen_y=GUIConstants.EDGE_PADDING, width=GUIConstants.TOP_NAV_BUTTON_SIZE, @@ -890,7 +896,7 @@ class TopNav(BaseComponent): start_y=0 ) - if self.show_back_button: + if self.show_left_button: # Don't let the title intrude on the BACK button min_x = self.back_button.screen_x + self.back_button.width + GUIConstants.COMPONENT_PADDING if self.text_x < min_x: @@ -902,17 +908,17 @@ class TopNav(BaseComponent): from .screens import RET_CODE__BACK_BUTTON, RET_CODE__POWER_BUTTON if not self.is_selected: return None - if self.show_back_button: + if self.show_left_button: return RET_CODE__BACK_BUTTON - if self.show_power_button: + if self.show_right_button: return RET_CODE__POWER_BUTTON def render(self): - if self.show_back_button: + if self.show_left_button: self.back_button.is_selected = self.is_selected self.back_button.render() - if self.show_power_button: + if self.show_right_button: self.power_button.is_selected = self.is_selected self.power_button.render() diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index 2cc7f5db..456b49c0 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -18,6 +18,8 @@ class PSBTOverviewScreen(ButtonListScreen): change_amount: int = 0 fee_amount: int = 0 num_inputs: int = 0 + num_self_transfer_outputs: int = 0 + num_change_outputs: int = 0 destination_addresses: List[str] = None @@ -131,11 +133,7 @@ class PSBTOverviewScreen(ButtonListScreen): destination_column = [] - if not self.destination_addresses: - # This is an internal transfer; no external addresses - destination_column.append(f"self transfer") - - elif len(self.destination_addresses) <= 3: + if len(self.destination_addresses) <= 3: for addr in self.destination_addresses: destination_column.append(truncate_destination_addr(addr)) else: @@ -144,10 +142,15 @@ class PSBTOverviewScreen(ButtonListScreen): destination_column.append(f"[ ... ]") destination_column.append(f"recipient {len(self.destination_addresses)}") + if self.num_self_transfer_outputs > 0: + for i in range(0, self.num_self_transfer_outputs): + destination_column.append(f"self transfer") + destination_column.append(f"fee") - if self.change_amount > 0 and self.destination_addresses: - destination_column.append("change") + if self.num_change_outputs > 0: + for i in range(0, self.num_change_outputs): + destination_column.append("change") max_destination_text_width = 0 for destination in destination_column: diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 76026c3c..8900969d 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -9,7 +9,7 @@ from seedsigner.helpers.threads import BaseThread from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.settings import SettingsConstants -from ..components import (CheckedSelectionButton, GUIConstants, BaseComponent, Button, Icon, IconButton, LargeIconButton, SeedSignerCustomIconConstants, TopNav, +from ..components import (CheckedSelectionButton, FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton, LargeIconButton, SeedSignerCustomIconConstants, TopNav, TextArea, load_icon, load_image) from seedsigner.helpers import B, Buttons @@ -180,8 +180,10 @@ class LoadingScreenThread(BaseThread): class BaseTopNavScreen(BaseScreen): title: str = "Screen Title" title_font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE - show_top_nav_back_button: bool = True - show_top_nav_power_button: bool = False + show_top_nav_left_button: bool = True + top_nav_left_button_icon_name: str = SeedSignerCustomIconConstants.LARGE_CHEVRON_LEFT + show_top_nav_right_button: bool = False + top_nav_right_button_icon_name: str = FontAwesomeIconConstants.POWER_OFF def __post_init__(self): super().__post_init__() @@ -190,8 +192,10 @@ class BaseTopNavScreen(BaseScreen): font_size=self.title_font_size, width=self.canvas_width, height=GUIConstants.TOP_NAV_HEIGHT, - show_back_button=self.show_top_nav_back_button, - show_power_button=self.show_top_nav_power_button, + show_left_button=self.show_top_nav_left_button, + left_button_icon_name=self.top_nav_left_button_icon_name, + show_right_button=self.show_top_nav_right_button, + right_button_icon_name=self.top_nav_right_button_icon_name, ) self.is_input_in_top_nav = False @@ -234,7 +238,7 @@ class TextTopNavScreen(BaseTopNavScreen): if user_input == B.KEY_UP: if not self.top_nav.is_selected: # Only move navigation up there if there's something to select - if self.top_nav.show_back_button or self.top_nav.show_power_button: + if self.top_nav.show_left_button or self.top_nav.show_right_button: self.top_nav.is_selected = True self.top_nav.render() @@ -405,7 +409,7 @@ class ButtonListScreen(BaseTopNavScreen): # OR keyed UP from the top of the list. # Move selection up to top_nav # Only move navigation up there if there's something to select - if self.top_nav.show_back_button or self.top_nav.show_power_button: + if self.top_nav.show_left_button or self.top_nav.show_right_button: self.buttons[self.selected_button].is_selected = False self.buttons[self.selected_button].render() @@ -580,7 +584,7 @@ class LargeButtonScreen(BaseTopNavScreen): if self.selected_button in [0, 2]: swap_selected_button(self.selected_button + 1) - elif user_input == B.KEY_RIGHT and self.top_nav.is_selected and not self.top_nav.show_power_button: + elif user_input == B.KEY_RIGHT and self.top_nav.is_selected and not self.top_nav.show_right_button: self.top_nav.is_selected = False self.top_nav.render() @@ -592,7 +596,7 @@ class LargeButtonScreen(BaseTopNavScreen): swap_selected_button(self.selected_button - 1) else: # Left from the far edge takes us up to the BACK arrow - if self.top_nav.show_back_button: + if self.top_nav.show_left_button: self.top_nav.is_selected = True self.top_nav.render() diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index ca777b2a..87de9c69 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -9,12 +9,11 @@ from seedsigner.helpers.threads import BaseThread, ThreadsafeCounter from seedsigner.models.seed import Seed -from .screen import BaseScreen, BaseTopNavScreen, ButtonListScreen, WarningScreenMixin +from .screen import BaseTopNavScreen, ButtonListScreen, WarningScreenMixin from ..components import FontAwesomeIconConstants, Fonts, FormattedAddress, IconTextLine, SeedSignerCustomIconConstants, TextArea, GUIConstants, TextDoesNotFitException, calc_text_centering from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.helpers import B -from seedsigner.models.encode_qr import EncodeQR @@ -27,7 +26,8 @@ class SeedValidScreen(ButtonListScreen): def __post_init__(self): # TODO: Replace this with explicit "Continue"/"Discard" options - self.show_top_nav_back_button = True + self.show_top_nav_left_button = True + self.top_nav_left_button_icon_name = FontAwesomeIconConstants.X super().__post_init__() diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 0287af56..0a868e4f 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -86,11 +86,33 @@ class PSBTOverviewView(View): def run(self): psbt_parser = self.controller.psbt_parser + change_data = psbt_parser.change_data + """ + change_data = [ + { + 'address': 'bc1q............', + 'amount': 397621401, + 'fingerprint': ['22bde1a9', '73c5da0a'], + 'derivation_path': ['m/48h/1h/0h/2h/1/0', 'm/48h/1h/0h/2h/1/0'] + }, {}, + ] + """ + num_change_outputs = 0 + num_self_transfer_outputs = 0 + for change_output in change_data: + print(f"""{change_output["derivation_path"][0]}""") + if change_output["derivation_path"][0].split("/")[-2] == "1": + num_change_outputs += 1 + else: + num_self_transfer_outputs += 1 + screen = psbt_screens.PSBTOverviewScreen( spend_amount=psbt_parser.spend_amount, change_amount=psbt_parser.change_amount, fee_amount=psbt_parser.fee_amount, num_inputs=psbt_parser.num_inputs, + num_self_transfer_outputs=num_self_transfer_outputs, + num_change_outputs=num_change_outputs, destination_addresses=psbt_parser.destination_addresses, ) @@ -255,9 +277,17 @@ class PSBTChangeDetailsView(View): is_change_derivation_path = int(derivation_path.split("/")[-2]) == 1 derivation_path_addr_index = int(derivation_path.split("/")[-1]) - VERIFY_MULTISIG = "Verify Multisig Change" NEXT = "Next" + if is_change_derivation_path: + title = "Your Change" + VERIFY_MULTISIG = "Verify Multisig Change" + else: + title = "Self-Transfer" + VERIFY_MULTISIG = "Verify Multisig Addr" + # if psbt_parser.num_change_outputs > 1: + # title += f" (#{self.change_address_num + 1})" + is_change_addr_verified = False if psbt_parser.is_multisig: # TODO: @@ -278,11 +308,7 @@ class PSBTChangeDetailsView(View): # change_data["address"] # Save for Nick is_change_addr_verified = True - button_data = ["Next"] - - title = "Your Change" - if psbt_parser.num_change_outputs > 1: - title += f" (#{self.change_address_num + 1})" + button_data = [NEXT] selected_menu_num = psbt_screens.PSBTChangeDetailsScreen( title=title, @@ -297,7 +323,10 @@ class PSBTChangeDetailsView(View): is_change_addr_verified=is_change_addr_verified, ).display() - if button_data[selected_menu_num] == NEXT: + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] == NEXT: if self.change_address_num < psbt_parser.num_change_outputs - 1: return Destination(PSBTChangeDetailsView, view_args={"change_address_num": self.change_address_num + 1}) else: @@ -307,9 +336,6 @@ class PSBTChangeDetailsView(View): elif button_data[selected_menu_num] == VERIFY_MULTISIG: return Destination(NotYetImplementedView) - elif selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - class PSBTFinalizeView(View): diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 1a150250..b1a8987a 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -505,23 +505,21 @@ class SeedValidView(View): def run(self): from .psbt_views import PSBTOverviewView - SIGN_PSBT = "Sign PSBT (?)" + SIGN_PSBT = "Review PSBT" SCAN_PSBT = ("Scan a PSBT", FontAwesomeIconConstants.QRCODE) PASSPHRASE = ("Add Passphrase", FontAwesomeIconConstants.UNLOCK) SEED_TOOLS = "Seed Options" button_data = [] - # Can we auto-route past this screen entirely? if self.controller.psbt: - if PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): - # The Seed we just entered can sign the psbt we have in memory. - SIGN_PSBT = "Sign PSBT" - # # Immediately forward on to the PSBT Overview. - # seed_num = self.controller.storage.finalize_pending_seed() - # self.controller.psbt_seed = self.seed - # return Destination(PSBTOverviewView, clear_history=True) - - # Don't auto-route to a signable psbt. Just display it. + if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): + # Seed doesn't match any input fingerprints + # TODO: Is there ever a use-case for letting someone try to sign with a + # seed that doesn't match? + SIGN_PSBT += " (?)" + else: + # Don't auto-route to a signable psbt. Just display the button. + pass button_data.append(SIGN_PSBT) else: button_data.append(SCAN_PSBT) @@ -542,12 +540,8 @@ class SeedValidView(View): ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: - # Back button should clear out the pending seed - self.controller.storage.clear_pending_seed() - - # BACK means exit/cancel; returning to MainMenuView also automatically wipes - # the back_stack history. - return Destination(MainMenuView) + # Warning message that this will discard the pending seed + return Destination(SeedDiscardView) elif button_data[selected_menu_num] == SIGN_PSBT: self.controller.storage.finalize_pending_seed() @@ -570,6 +564,27 @@ class SeedValidView(View): +class SeedDiscardView(View): + def run(self): + YES = "Yes" + NO = "No" + button_data = [YES, NO] + + selected_menu_num = LargeButtonScreen( + title="Discard Seed?", + button_data=button_data, + show_top_nav_left_button=False, + ).display() + + if button_data[selected_menu_num] == YES: + self.controller.storage.clear_pending_seed() + return Destination(MainMenuView) + + elif button_data[selected_menu_num] == NO: + return Destination(SeedValidView) + + + class SeedAddPassphrasePromptView(View): def run(self): YES = "Yes" @@ -640,7 +655,7 @@ class SeedReviewPassphraseView(View): fingerprint_with=fingerprint_with, passphrase=self.seed.passphrase, button_data=button_data, - show_top_nav_back_button=False, + show_top_nav_left_button=False, ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index c4682df4..38b2cb3e 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -115,8 +115,8 @@ class MainMenuView(View): title="Home", title_font_size=26, button_data=[entry[0] for entry in menu_items], - show_top_nav_back_button=False, - show_top_nav_power_button=True, + show_top_nav_left_button=False, + show_top_nav_right_button=True, ) selected_menu_num = screen.display() @@ -134,7 +134,7 @@ class PowerOptionsView(View): button_data = [RESET, POWER_OFF] selected_menu_num = LargeButtonScreen( title="Reset / Power", - show_top_nav_back_button=True, + show_top_nav_left_button=True, button_data=button_data ).display() @@ -156,7 +156,7 @@ class ResetView(View): TextTopNavScreen( title="Resetting", text="SeedSigner is restarting. All data will be wiped.", - show_top_nav_back_button=False, + show_top_nav_left_button=False, ).display() @@ -178,7 +178,7 @@ class PowerOffView(View): TextTopNavScreen( title="Powering Off", text="Please wait about 30 seconds before disconnecting power.", - show_top_nav_back_button=False, + show_top_nav_left_button=False, ).display()