diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 429890f9..34092318 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -28,7 +28,7 @@ class GUIConstants: REGTEST_COLOR = "#00caf1" ICON_FONT_NAME__FONT_AWESOME = "Font_Awesome_6_Free-Solid-900" - ICON_FONT_NAME__SEEDSIGNER = "seedsigner-glyphs" + ICON_FONT_NAME__SEEDSIGNER = "seedsigner-icons" ICON_FONT_SIZE = 22 ICON_INLINE_FONT_SIZE = 24 ICON_LARGE_BUTTON_SIZE = 36 @@ -69,10 +69,6 @@ class FontAwesomeIconConstants: ANGLE_RIGHT = "\uf105" ANGLE_UP = "\uf106" CAMERA = "\uf030" - CARET_DOWN = "\uf0d7" - CARET_LEFT = "\uf0d9" - CARET_RIGHT = "\uf0da" - CARET_UP = "\uf0d8" CHEVRON_UP = "\uf077" CHEVRON_DOWN = "\uf078" SOLID_CIRCLE_CHECK = "\uf058" @@ -85,46 +81,59 @@ class FontAwesomeIconConstants: DICE_FOUR = "\uf524" DICE_FIVE = "\uf523" DICE_SIX = "\uf526" - GEAR = "\uf013" - KEY = "\uf084" KEYBOARD = "\uf11c" LOCK = "\uf023" MAP = "\uf279" PAPER_PLANE = "\uf1d8" PEN = "\uf304" - PLUS = "+" - POWER_OFF = "\uf011" ROTATE_RIGHT = "\uf2f9" - SCREWDRIVER_WRENCH = "\uf7d9" + SDCARD = "\uf7c2" SQUARE = "\uf0c8" SQUARE_CARET_DOWN = "\uf150" SQUARE_CARET_LEFT = "\uf191" SQUARE_CARET_RIGHT = "\uf152" SQUARE_CARET_UP = "\uf151" SQUARE_CHECK = "\uf14a" - TRIANGLE_EXCLAMATION = "\uf071" UNLOCK = "\uf09c" - QRCODE = "\uf029" X = "\u0058" - SDCARD = "\uf7c2" -class SeedSignerCustomIconConstants: - LARGE_CHEVRON_LEFT = "\ue900" - SMALL_CHEVRON_RIGHT = "\ue901" - PAGE_UP = "\ue903" - PAGE_DOWN = "\ue902" - PLUS = "\ue904" - CIRCLE_CHECK = "\ue907" - CIRCLE_EXCLAMATION = "\ue908" - CIRCLE_X = "\ue909" - FINGERPRINT = "\ue90a" - PATH = "\ue90b" - BITCOIN_LOGO_STRAIGHT = "\ue90c" - BITCOIN_LOGO_TILTED = "\ue90d" +class SeedSignerIconConstants: + SCAN = "\ue900" + SEEDS = "\ue901" + SETTINGS = "\ue902" + TOOLS = "\ue903" - MIN_VALUE = LARGE_CHEVRON_LEFT - MAX_VALUE = BITCOIN_LOGO_TILTED + BACK = "\ue904" + CHEVRON_DOWN = "\ue905" + CHEVRON_LEFT = "\ue906" + CHEVRON_RIGHT = "\ue907" + CHEVRON_UP = "\ue908" + CLOSE = "\ue909" + PAGE_DOWN = "\ue90a" + PAGE_UP = "\ue90b" + PLUS = "\ue90c" + POWER = "\ue90d" + + ERROR = "\ue90e" + SUCCESS = "\ue90f" + WARNING = "\ue910" + + ADDRESS = "\ue911" + CHANGE = "\ue912" + DERIVATION = "\ue913" + FEE = "\ue914" + FINGERPRINT = "\ue915" + PASSPHRASE = "\ue916" + + BITCOIN = "\ue917" + BITCOIN_ALT = "\ue918" + BRIGHTNESS = "\ue919" + MICROSD = "\ue91a" + QRCODE = "\ue91b" + + MIN_VALUE = BACK + MAX_VALUE = BITCOIN_ALT @@ -425,7 +434,7 @@ class TextArea(BaseComponent): class Icon(BaseComponent): screen_x: int = 0 screen_y: int = 0 - icon_name: str = SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED + icon_name: str = SeedSignerIconConstants.BITCOIN_ALT icon_size: int = GUIConstants.ICON_FONT_SIZE icon_color: str = GUIConstants.BODY_FONT_COLOR @@ -859,7 +868,7 @@ class BtcAmount(BaseComponent): btc_icon = Icon( image_draw=draw, canvas=self.paste_image, - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED, + icon_name=SeedSignerIconConstants.BITCOIN_ALT, icon_color=btc_color, icon_size=self.icon_size, screen_x=0, @@ -947,7 +956,7 @@ class BtcAmount(BaseComponent): btc_icon = Icon( image_draw=draw, canvas=self.paste_image, - icon_name=SeedSignerCustomIconConstants.BITCOIN_LOGO_TILTED, + icon_name=SeedSignerIconConstants.BITCOIN_ALT, icon_color=btc_color, icon_size=self.icon_size, screen_x=0, @@ -1279,7 +1288,7 @@ class TopNav(BaseComponent): if self.show_back_button: self.left_button = IconButton( - icon_name=SeedSignerCustomIconConstants.LARGE_CHEVRON_LEFT, + icon_name=SeedSignerIconConstants.BACK, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, screen_x=GUIConstants.EDGE_PADDING, screen_y=GUIConstants.EDGE_PADDING, @@ -1289,7 +1298,7 @@ class TopNav(BaseComponent): if self.show_power_button: self.right_button = IconButton( - icon_name=FontAwesomeIconConstants.POWER_OFF, + icon_name=SeedSignerIconConstants.POWER, icon_size=GUIConstants.ICON_INLINE_FONT_SIZE, screen_x=self.width - GUIConstants.TOP_NAV_BUTTON_SIZE - GUIConstants.EDGE_PADDING, screen_y=GUIConstants.EDGE_PADDING, diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index 1ca904c0..ad378fc3 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -655,7 +655,7 @@ class PSBTChangeDetailsScreen(ButtonListScreen): # Adjust the vertical spacing screen_y -= GUIConstants.COMPONENT_PADDING self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, + icon_name=SeedSignerIconConstants.FINGERPRINT, icon_color="blue", value_text=f"""{"Multisig" if self.is_multisig else self.fingerprint}: {"Change" if self.is_change_derivation_path else "Addr"} #{self.derivation_path_addr_index}""", is_text_centered=False, @@ -665,7 +665,7 @@ class PSBTChangeDetailsScreen(ButtonListScreen): if self.is_change_addr_verified: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.CIRCLE_CHECK, + icon_name=SeedSignerIconConstants.SUCCESS, icon_color="#00dd00", value_text="Address verified!", is_text_centered=False, diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index ea08a4f5..0e3ea651 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -692,7 +692,7 @@ class QRDisplayScreen(BaseScreen): canvas=rectangle, screen_x=GUIConstants.EDGE_PADDING*2 + 1, screen_y=GUIConstants.COMPONENT_PADDING + 4, # +4 fudge factor to account for where the chevron is drawn relative to baseline - icon_name=FontAwesomeIconConstants.CHEVRON_UP, + icon_name=SeedSignerIconConstants.CHEVRON_UP, icon_size=GUIConstants.BODY_FONT_SIZE, ) chevron_up_icon.render() @@ -702,7 +702,7 @@ class QRDisplayScreen(BaseScreen): canvas=rectangle, screen_x=chevron_up_icon.screen_x, screen_y=chevron_up_icon.screen_y + chevron_up_icon.icon_size + GUIConstants.BODY_LINE_SPACING, - icon_name=FontAwesomeIconConstants.CHEVRON_DOWN, + icon_name=SeedSignerIconConstants.CHEVRON_DOWN, icon_size=chevron_up_icon.icon_size, ) chevron_down_icon.render() @@ -824,7 +824,7 @@ class QRDisplayScreen(BaseScreen): @dataclass class LargeIconStatusScreen(ButtonListScreen): title: str = "Success!" - status_icon_name: str = SeedSignerCustomIconConstants.CIRCLE_CHECK + status_icon_name: str = SeedSignerIconConstants.SUCCESS status_icon_size: int = GUIConstants.ICON_PRIMARY_SCREEN_SIZE status_color: str = GUIConstants.SUCCESS_COLOR status_headline: str = "Success!" # The colored text under the large icon @@ -948,7 +948,7 @@ class WarningEdgesMixin: @dataclass class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): title: str = "Caution" - status_icon_name: str = SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION + status_icon_name: str = SeedSignerIconConstants.WARNING status_color: str = "yellow" status_headline: str = "Privacy Leak!" # The colored text under the alert icon @@ -1242,7 +1242,7 @@ class MicroSDToastScreen(BaseScreen): if self.action == MicroSD.ACTION__REMOVED: self.toast = ToastOverlay( - icon_name=FontAwesomeIconConstants.SDCARD, + icon_name=SeedSignerIconConstants.MICROSD, color=GUIConstants.NOTIFICATION_COLOR, label_text="MicroSD removed" ) @@ -1250,7 +1250,7 @@ class MicroSDToastScreen(BaseScreen): elif self.action == MicroSD.ACTION__INSERTED: self.toast = ToastOverlay( - icon_name=FontAwesomeIconConstants.SDCARD, + icon_name=SeedSignerIconConstants.MICROSD, color=GUIConstants.NOTIFICATION_COLOR, label_text="MicroSD inserted" ) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 74f0cacf..3e4213eb 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -421,7 +421,7 @@ class SeedFinalizeScreen(ButtonListScreen): super().__post_init__() self.fingerprint_icontl = IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, + icon_name=SeedSignerIconConstants.FINGERPRINT, icon_color="blue", icon_size=GUIConstants.ICON_FONT_SIZE + 12, label_text="fingerprint", @@ -441,7 +441,7 @@ class SeedOptionsScreen(ButtonListScreen): has_passphrase: bool = False def __post_init__(self): - self.top_nav_icon_name = SeedSignerCustomIconConstants.FINGERPRINT + self.top_nav_icon_name = SeedSignerIconConstants.FINGERPRINT self.top_nav_icon_color = "blue" self.title = self.fingerprint self.is_button_text_centered = False @@ -584,7 +584,7 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen): # Set up the fingerprint and passphrase displays self.fingerprint_line = IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, + icon_name=SeedSignerIconConstants.FINGERPRINT, icon_color="blue", label_text="Fingerprint", value_text=self.fingerprint, @@ -594,7 +594,7 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen): self.components.append(self.fingerprint_line) self.derivation_line = IconTextLine( - icon_name=SeedSignerCustomIconConstants.PATH, + icon_name=SeedSignerIconConstants.DERIVATION, label_text="Derivation", value_text=self.derivation_path, screen_x=GUIConstants.COMPONENT_PADDING, @@ -1009,7 +1009,7 @@ class SeedReviewPassphraseScreen(ButtonListScreen): super().__post_init__() self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, + icon_name=SeedSignerIconConstants.FINGERPRINT, icon_color="blue", label_text="changes fingerprint", value_text=f"{self.fingerprint_without} >> {self.fingerprint_with}", diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 65ffcc3d..e27e4949 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -362,7 +362,7 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen): )) self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, + icon_name=SeedSignerIconConstants.FINGERPRINT, icon_color="blue", label_text="fingerprint", value_text=self.fingerprint, @@ -386,7 +386,7 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): if self.fingerprint: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, + icon_name=SeedSignerIconConstants.FINGERPRINT, icon_color="blue", label_text="Fingerprint", value_text=self.fingerprint, @@ -396,7 +396,7 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): if self.script_type != SettingsConstants.CUSTOM_DERIVATION: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.PATH, + icon_name=SeedSignerIconConstants.DERIVATION, label_text="Derivation", value_text=SettingsDefinition.get_settings_entry(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(value=self.script_type), screen_x=GUIConstants.EDGE_PADDING, @@ -404,7 +404,7 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen): )) else: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.PATH, + icon_name=SeedSignerIconConstants.DERIVATION, label_text="Derivation", value_text=self.custom_derivation_path, screen_x=GUIConstants.EDGE_PADDING, diff --git a/src/seedsigner/resources/fonts/seedsigner-glyphs.otf b/src/seedsigner/resources/fonts/seedsigner-glyphs.otf deleted file mode 100644 index 63a0008c..00000000 Binary files a/src/seedsigner/resources/fonts/seedsigner-glyphs.otf and /dev/null differ diff --git a/src/seedsigner/resources/fonts/seedsigner-icons.otf b/src/seedsigner/resources/fonts/seedsigner-icons.otf new file mode 100644 index 00000000..a54b4c59 Binary files /dev/null and b/src/seedsigner/resources/fonts/seedsigner-icons.otf differ diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index fc5b4763..0e3e3576 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) class PSBTSelectSeedView(View): - SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE) + SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) button_data = [] @@ -41,7 +41,7 @@ class PSBTSelectSeedView(View): # Doesn't look like this seed can sign the current PSBT button_str += " (?)" - self.button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + self.button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT, "blue")) self.button_data.append(self.SCAN_SEED) self.button_data.append(self.TYPE_12WORD) @@ -531,7 +531,7 @@ class PSBTSigningErrorView(View): # Just a WarningScreen here; only use DireWarningScreen for true security risks. selected_menu_num = WarningScreen( title="PSBT Error", - status_icon_name=SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION, + status_icon_name=SeedSignerIconConstants.WARNING, status_headline="Signing Failed", text="Signing with this seed did not add a valid signature.", button_data=["Select Diff Seed"], diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 41491d47..28781ae1 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -49,7 +49,7 @@ class SeedsMenuView(View): button_data = [] for seed in self.seeds: - button_data.append((seed["fingerprint"], SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + button_data.append((seed["fingerprint"], SeedSignerIconConstants.FINGERPRINT, "blue")) button_data.append("Load a seed") selected_menu_num = self.run_screen( @@ -74,10 +74,10 @@ class SeedsMenuView(View): Loading seeds, passphrases, etc ****************************************************************************""" class LoadSeedView(View): - SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE) + SEED_QR = (" Scan a SeedQR", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS) + CREATE = (" Create a seed", SeedSignerIconConstants.PLUS) def run(self): button_data = [ @@ -340,11 +340,11 @@ class SeedDiscardView(View): Views for actions on individual seeds: ****************************************************************************""" class SeedOptionsView(View): - SCAN_PSBT = ("Scan PSBT", FontAwesomeIconConstants.QRCODE) + SCAN_PSBT = ("Scan PSBT", SeedSignerIconConstants.QRCODE) VERIFY_ADDRESS = "Verify Addr" EXPORT_XPUB = "Export Xpub" EXPLORER = "Address Explorer" - BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT) + BACKUP = ("Backup Seed", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT) BIP85_CHILD_SEED = "BIP-85 Child Seed" DISCARD = ("Discard Seed", None, None, "red") @@ -1354,7 +1354,7 @@ class SeedTranscribeSeedQRConfirmQRPromptView(View): def run(self): - SCAN = ("Confirm SeedQR", FontAwesomeIconConstants.QRCODE) + SCAN = ("Confirm SeedQR", SeedSignerIconConstants.QRCODE) DONE = "Done" button_data = [SCAN, DONE] @@ -1529,7 +1529,7 @@ class SeedSingleSigAddressVerificationSelectSeedView(View): def run(self): seeds = self.controller.storage.seeds - SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE) + SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) button_data = [] @@ -1538,7 +1538,7 @@ class SeedSingleSigAddressVerificationSelectSeedView(View): for seed in seeds: button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT, "blue")) text = "Select seed to verify" button_data.append(SCAN_SEED) @@ -1784,7 +1784,7 @@ class AddressVerificationSuccessView(View): class LoadMultisigWalletDescriptorView(View): def run(self): - SCAN = ("Scan Descriptor", FontAwesomeIconConstants.QRCODE) + SCAN = ("Scan Descriptor", SeedSignerIconConstants.QRCODE) CANCEL = "Cancel" button_data = [SCAN, CANCEL] selected_menu_num = seed_screens.LoadMultisigWalletDescriptorScreen( diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index bc02281b..b3f6275b 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -40,7 +40,7 @@ class SettingsMenuView(View): title = "Settings" # Set up the next nested level of menuing - button_data.append(("Advanced", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) + button_data.append(("Advanced", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT)) next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED}) button_data.append(self.IO_TEST) @@ -50,7 +50,7 @@ class SettingsMenuView(View): title = "Advanced" # So far there are no real Developer options; disabling for now - # button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) + # button_data.append(("Developer Options", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT)) # next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER}) next_destination = None diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 6cd0c0bd..af53c106 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -425,8 +425,8 @@ class ToolsCalcFinalWordDoneView(View): Address Explorer Views ****************************************************************************""" class ToolsAddressExplorerSelectSourceView(View): - SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE) - SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE) + SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) + SCAN_DESCRIPTOR = ("Scan wallet descriptor", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) @@ -436,7 +436,7 @@ class ToolsAddressExplorerSelectSourceView(View): button_data = [] for seed in seeds: button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT, "blue")) button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD] selected_menu_num = self.run_screen( @@ -630,7 +630,7 @@ class ToolsAddressExplorerAddressListView(View): end_digits = -4 button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}") - button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) + button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT)) selected_menu_num = self.run_screen( ButtonListScreen, diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index de4bac71..57cdeace 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -137,10 +137,10 @@ class Destination: # ######################################################################################### class MainMenuView(View): - SCAN = ("Scan", FontAwesomeIconConstants.QRCODE) - SEEDS = ("Seeds", FontAwesomeIconConstants.KEY) - TOOLS = ("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH) - SETTINGS = ("Settings", FontAwesomeIconConstants.GEAR) + SCAN = ("Scan", SeedSignerIconConstants.SCAN) + SEEDS = ("Seeds", SeedSignerIconConstants.SEEDS) + TOOLS = ("Tools", SeedSignerIconConstants.TOOLS) + SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS) def run(self): button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS] @@ -176,7 +176,7 @@ class MainMenuView(View): class PowerOptionsView(View): RESET = ("Restart", FontAwesomeIconConstants.ROTATE_RIGHT) - POWER_OFF = ("Power Off", FontAwesomeIconConstants.POWER_OFF) + POWER_OFF = ("Power Off", SeedSignerIconConstants.POWER) def run(self): button_data = [self.RESET, self.POWER_OFF] diff --git a/tests/screenshot_generator/README.md b/tests/screenshot_generator/README.md new file mode 100644 index 00000000..b4ccb83b --- /dev/null +++ b/tests/screenshot_generator/README.md @@ -0,0 +1,8 @@ +# Screenshot Generator + +From the project root, run: +``` +pytest tests/screenshot_generator/generator.py --locale en +``` + +By default it writes the screenshots to a dir that's parallel to the project root: `seedsigner-screenshots/`. diff --git a/tests/screenshot_generator/__init__.py b/tests/screenshot_generator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/screenshot_generator/conftest.py b/tests/screenshot_generator/conftest.py new file mode 100644 index 00000000..43f690b8 --- /dev/null +++ b/tests/screenshot_generator/conftest.py @@ -0,0 +1,11 @@ +import pytest + + + +def pytest_addoption(parser): + parser.addoption("--locale", action="store", default=None) + + +@pytest.fixture(scope='session') +def target_locale(request): + return request.config.option.locale diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py new file mode 100644 index 00000000..0315ab16 --- /dev/null +++ b/tests/screenshot_generator/generator.py @@ -0,0 +1,221 @@ +import embit +import os +import pathlib +import pytest +from mock import Mock, patch + +from seedsigner.controller import Controller +from seedsigner.gui.renderer import Renderer +from seedsigner.hardware.buttons import HardwareButtons +from seedsigner.hardware.camera import Camera +from seedsigner.models.decode_qr import DecodeQR +from seedsigner.models.qr_type import QRType +from seedsigner.models.seed import Seed +from seedsigner.models.settings import Settings +from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition +from seedsigner.views import (main_menu_views, psbt_views, scan_views, seed_views, + settings_views, tools_views) +from seedsigner.views.view import View + +from .utils import ScreenshotComplete, ScreenshotRenderer + + + +def test_generate_screenshots(target_locale): + """ + The `Renderer` class is mocked so that calls in the normal code are ignored + (necessary to avoid having it trying to wire up hardware dependencies). + + When the `Renderer` instance is needed, we patch in our own test-only + `ScreenshotRenderer`. + """ + # Disable hardware dependencies by essentially wiping out this class + HardwareButtons.get_instance = Mock() + Camera.get_instance = Mock() + + # Prep the ScreenshotRenderer that will be patched over the normal Renderer + screenshot_root = "/Users/kdmukai/dev/seedsigner-screenshots" + ScreenshotRenderer.configure_instance() + screenshot_renderer: ScreenshotRenderer = ScreenshotRenderer.get_instance() + + # Replace the core `Singleton` calls so that only our ScreenshotRenderer is used. + Renderer.configure_instance = Mock() + Renderer.get_instance = Mock(return_value=screenshot_renderer) + + controller = Controller.get_instance() + + # Set up some test data that we'll need in the `Controller` for certain Views + mnemonic_12 = "forum undo fragile fade shy sign arrest garment culture tube off merit".split() + mnemonic_24 = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() + mnemonic_12b = ["abandon"] * 11 + ["about"] + seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + seed_24 = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + controller.storage.seeds.append(seed_12) + controller.storage.seeds.append(seed_12b) + controller.storage.set_pending_seed(seed_24) + + # Load a PSBT into memory + BASE64_PSBT_1 = """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""" + decoder = DecodeQR() + decoder.add_data(BASE64_PSBT_1) + controller.psbt = decoder.get_psbt() + controller.psbt_seed = seed_12b + + # Multisig wallet descriptor for the multisig in the above PSBT + MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx""" + controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR) + + # Parse the main `babel/messages.pot` for overall stats + messages_source_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "babel", "messages.pot") + with open(messages_source_path, 'r') as messages_source_file: + num_source_messages = messages_source_file.read().count("msgid \"") - 1 + + def screencap_view(view_cls: View, view_name: str, view_args: dict={}): + screenshot_renderer.set_screenshot_filename(f"{view_name}.png") + try: + print(f"Running {view_name}") + view_cls(**view_args).run() + except ScreenshotComplete: + # Slightly hacky way to exit ScreenshotRenderer as expected + pass + print(f"Completed {view_name}") + except Exception as e: + # Something else went wrong + print(repr(e)) + raise e + + # Automatically populate all Settings options Views + settings_views_list = [] + settings_views_list.append(settings_views.SettingsMenuView) + for settings_entry in SettingsDefinition.settings_entries: + if settings_entry.visibility == SettingsConstants.VISIBILITY__HIDDEN: + continue + settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}")) + settings_views_list.append(settings_views.IOTestView) + settings_views_list.append(settings_views.DonateView) + + + screenshot_sections = { + "Main Menu Views": [ + main_menu_views.MainMenuView, + main_menu_views.PowerOffView, + (scan_views.SettingsUpdatedView, dict(config_name="Keith's Settings")), + ], + "Seed Views": [ + seed_views.SeedsMenuView, + seed_views.LoadSeedView, + seed_views.SeedMnemonicEntryView, + seed_views.SeedMnemonicInvalidView, + seed_views.SeedFinalizeView, + seed_views.SeedPassphraseWarningView, + seed_views.SeedAddPassphraseView, + seed_views.SeedReviewPassphraseView, + (seed_views.SeedOptionsView, dict(seed_num=0)), + (seed_views.SeedBackupView, dict(seed_num=0)), + (seed_views.SeedWordsWarningView, dict(seed_num=0)), + (seed_views.SeedWordsView, dict(seed_num=0)), + (seed_views.SeedWordsView, dict(seed_num=0, page_index=2), "SeedWordsView_2"), + (seed_views.SeedTranscribeSeedQRFormatView, dict(seed_num=0)), + (seed_views.SeedTranscribeSeedQRWarningView, dict(seed_num=0)), + (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_12_Standard"), + (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), "SeedTranscribeSeedQRWholeQRView_12_Compact"), + + # Screenshot doesn't render properly due to how the transparency mask is pre-rendered + # (seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR)), + + (seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)), + + # Screenshot can't render live preview screens + # (seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)), + + seed_views.LoadMultisigWalletDescriptorView, + seed_views.MultisigWalletDescriptorView, + ], + "PSBT Views": [ + psbt_views.PSBTSelectSeedView, + psbt_views.PSBTOverviewView, + psbt_views.PSBTUnsupportedScriptTypeWarningView, + psbt_views.PSBTNoChangeWarningView, + psbt_views.PSBTMathView, + (psbt_views.PSBTAddressDetailsView, dict(address_num=0)), + + # TODO: Render Multisig change w/ and w/out the multisig wallet descriptor onboard + (psbt_views.PSBTChangeDetailsView, dict(change_address_num=0)), + (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_change"), + (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_selftransfer"), + (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_change"), + (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_selftransfer"), + psbt_views.PSBTFinalizeView, + psbt_views.PSBTSelectCoordinatorView, + psbt_views.PSBTSigningErrorView, + ], + "Tools Views": [ + tools_views.ToolsMenuView, + tools_views.ToolsDiceEntropyMnemonicLengthView, + (tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)), + ], + "Settings Views": settings_views_list, + } + + + locales = [] + if target_locale is None: + locales = SettingsConstants.ALL_LOCALES + else: + locales = [locale_tuple for locale_tuple in SettingsConstants.ALL_LOCALES if locale_tuple[0] == target_locale] + + if not locales: + raise Exception(f"Invalid locale: {target_locale}") + + for locale, display_name in locales: + Settings.get_instance().set_value(SettingsConstants.SETTING__LOCALE, value=locale) + screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale)) + + locale_readme = f"""# SeedSigner Screenshots: {display_name}\n""" + + # Report the translation progress + if locale != SettingsConstants.LOCALE__ENGLISH: + translated_messages_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "src", "seedsigner", "resources", "babel", locale, "LC_MESSAGES", "messages.po") + with open(translated_messages_path, 'r') as translation_file: + locale_translations = translation_file.read() + num_locale_translations = locale_translations.count("msgid \"") - locale_translations.count("""msgstr ""\n\n""") - 1 + + locale_readme += f"## Translation progress: {num_locale_translations / num_source_messages:.1%}\n\n" + locale_readme += "---\n\n" + + for section_name, screenshot_list in screenshot_sections.items(): + locale_readme += "\n\n---\n\n" + locale_readme += f"## {section_name}\n\n" + locale_readme += """""" + locale_readme += f"""
""" + for screenshot in screenshot_list: + if type(screenshot) == tuple: + if len(screenshot) == 2: + view_cls, view_args = screenshot + view_name = view_cls.__name__ + elif len(screenshot) == 3: + view_cls, view_args, view_name = screenshot + else: + view_cls = screenshot + view_args = {} + view_name = view_cls.__name__ + + screencap_view(view_cls, view_name, view_args) + locale_readme += """""" + locale_readme += f"""""" + locale_readme += """
{view_name}

""" + + locale_readme += "
" + + with open(os.path.join(screenshot_renderer.screenshot_path, "README.md"), 'w') as readme_file: + readme_file.write(locale_readme) + + # Write the main README; ensure it writes all locales, not just the one that may + # have been specified for this run. + main_readme = """# SeedSigner Screenshots \n\n""" + for locale, display_name in SettingsConstants.ALL_LOCALES: + main_readme += f"* [{display_name}]({locale}/README.md)\n" + + with open(os.path.join(screenshot_root, "README.md"), 'w') as readme_file: + readme_file.write(main_readme) diff --git a/tests/screenshot_generator/utils.py b/tests/screenshot_generator/utils.py new file mode 100644 index 00000000..26d232a4 --- /dev/null +++ b/tests/screenshot_generator/utils.py @@ -0,0 +1,53 @@ +import os +from PIL import Image, ImageDraw +from seedsigner.gui.renderer import Renderer + + +class ScreenshotComplete(Exception): + pass + + +class ScreenshotRenderer(Renderer): + screenshot_path: str = None + screenshot_filename: str = None + + @classmethod + def configure_instance(cls): + # Instantiate the one and only Renderer instance + renderer = cls.__new__(cls) + cls._instance = renderer + + # Hard-coding output values for now + renderer.canvas_width = 240 + renderer.canvas_height = 240 + + renderer.canvas = Image.new('RGB', (renderer.canvas_width, renderer.canvas_height)) + renderer.draw = ImageDraw.Draw(renderer.canvas) + + + def set_screenshot_filename(self, filename:str): + self.screenshot_filename = filename + + + def set_screenshot_path(self, path): + if not os.path.exists(path): + os.makedirs(path) + self.screenshot_path = path + + + def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False): + if is_background_thread: + return + + if alpha_overlay: + if image == None: + image = self.canvas + image = Image.alpha_composite(image, alpha_overlay) + + if image: + # Always write to the current canvas, rather than trying to replace it + self.canvas.paste(image) + + self.canvas.save(os.path.join(self.screenshot_path, self.screenshot_filename)) + raise ScreenshotComplete() +