diff --git a/.gitignore b/.gitignore index dcf343d8c..f2fbb160e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ src/seedsigner/models/settings_definition.json .idea *.mo .coverage +seedsigner-screenshots \ No newline at end of file diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index c04a15ac5..7aafcbcb8 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -20,18 +20,19 @@ class GUIConstants: BACKGROUND_COLOR = "black" WARNING_COLOR = "#FFD60A" - DIRE_WARNING_COLOR = "red" - SUCCESS_COLOR = "#00dd00" - BITCOIN_ORANGE = "#ff9416" - ACCENT_COLOR = "orange" - TESTNET_COLOR = "#00f100" - REGTEST_COLOR = "#00caf1" + DIRE_WARNING_COLOR = "#FF453A" + SUCCESS_COLOR = "#30D158" + INFO_COLOR = "#0084FF" + ACCENT_COLOR = "#FF9F0A" + BITCOIN_ORANGE = "#FF9416" + TESTNET_COLOR = "#00F100" + 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 + ICON_LARGE_BUTTON_SIZE = 48 ICON_PRIMARY_SCREEN_SIZE = 50 TOP_NAV_TITLE_FONT_NAME = "OpenSans-SemiBold" @@ -43,23 +44,23 @@ class GUIConstants: BODY_FONT_SIZE = 17 BODY_FONT_MAX_SIZE = TOP_NAV_TITLE_FONT_SIZE BODY_FONT_MIN_SIZE = 15 - BODY_FONT_COLOR = "#f8f8f8" + BODY_FONT_COLOR = "#FCFCFC" BODY_LINE_SPACING = COMPONENT_PADDING FIXED_WIDTH_FONT_NAME = "Inconsolata-Regular" FIXED_WIDTH_EMPHASIS_FONT_NAME = "Inconsolata-SemiBold" LABEL_FONT_SIZE = BODY_FONT_MIN_SIZE - LABEL_FONT_COLOR = "#777" + LABEL_FONT_COLOR = "#B3B3B3" BUTTON_FONT_NAME = "OpenSans-SemiBold" BUTTON_FONT_SIZE = 18 - BUTTON_FONT_COLOR = "#e8e8e8" - BUTTON_BACKGROUND_COLOR = "#2c2c2c" + BUTTON_FONT_COLOR = "#FCFCFC" + BUTTON_BACKGROUND_COLOR = "#2C2C2C" BUTTON_HEIGHT = 32 - BUTTON_SELECTED_FONT_COLOR = "black" + BUTTON_SELECTED_FONT_COLOR = BACKGROUND_COLOR - NOTIFICATION_COLOR = "#00f100" + NOTIFICATION_COLOR = "#00F100" @@ -69,13 +70,8 @@ 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" CIRCLE = "\uf111" CIRCLE_CHEVRON_RIGHT = "\uf138" DICE = "\uf522" @@ -85,46 +81,65 @@ 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" - 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" - - MIN_VALUE = LARGE_CHEVRON_LEFT - MAX_VALUE = BITCOIN_LOGO_TILTED +class SeedSignerIconConstants: + # Menu icons + SCAN = "\ue900" + SEEDS = "\ue901" + SETTINGS = "\ue902" + TOOLS = "\ue903" + + # Utility icons + BACK = "\ue904" + CHECK = "\ue905" + CHECKBOX = "\ue906" + CHECKBOX_SELECTED = "\ue907" + CHEVRON_DOWN = "\ue908" + CHEVRON_LEFT = "\ue909" + CHEVRON_RIGHT = "\ue90a" + CHEVRON_UP = "\ue90b" + CLOSE = "\ue90c" + PAGE_DOWN = "\ue90d" + PAGE_UP = "\ue90e" + PLUS = "\ue90f" + POWER = "\ue910" + RESTART = "\ue911" + + # Messaging icons + ERROR = "\ue912" + SUCCESS = "\ue913" + WARNING = "\ue914" + + # Informational icons + ADDRESS = "\ue915" + CHANGE = "\ue916" + DERIVATION = "\ue917" + FEE = "\ue918" + FINGERPRINT = "\ue919" + PASSPHRASE = "\ue91a" + + # Misc icons + BITCOIN = "\ue91b" + BITCOIN_ALT = "\ue91c" + BRIGHTNESS = "\ue91d" + MICROSD = "\ue91e" + QRCODE = "\ue91f" + SDCARD = "\ue920" + + MIN_VALUE = SCAN + MAX_VALUE = QRCODE @@ -425,14 +440,14 @@ def render(self): 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 def __post_init__(self): super().__post_init__() - if SeedSignerCustomIconConstants.MIN_VALUE <= self.icon_name and self.icon_name <= SeedSignerCustomIconConstants.MAX_VALUE: + if SeedSignerIconConstants.MIN_VALUE <= self.icon_name and self.icon_name <= SeedSignerIconConstants.MAX_VALUE: self.icon_font = Fonts.get_font(GUIConstants.ICON_FONT_NAME__SEEDSIGNER, self.icon_size, file_extension="otf") else: self.icon_font = Fonts.get_font(GUIConstants.ICON_FONT_NAME__FONT_AWESOME, self.icon_size, file_extension="otf") @@ -501,7 +516,7 @@ def __post_init__(self): canvas=self.canvas, text=self.label_text, font_size=GUIConstants.BODY_FONT_SIZE - 2, - font_color="#666", + font_color=GUIConstants.LABEL_FONT_COLOR, edge_padding=0, is_text_centered=self.is_text_centered if not self.icon_name else False, auto_line_break=False, @@ -863,7 +878,7 @@ def __post_init__(self): 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, @@ -951,7 +966,7 @@ def __post_init__(self): 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, @@ -1207,8 +1222,8 @@ class CheckedSelectionButton(Button): def __post_init__(self): self.is_text_centered = False - self.icon_name = FontAwesomeIconConstants.SOLID_CIRCLE_CHECK - self.icon_color = "#00dd00" + self.icon_name = SeedSignerIconConstants.CHECK + self.icon_color = GUIConstants.SUCCESS_COLOR super().__post_init__() if not self.is_checked: @@ -1226,10 +1241,10 @@ class CheckboxButton(Button): def __post_init__(self): self.is_text_centered = False if self.is_checked: - self.icon_name = FontAwesomeIconConstants.SQUARE_CHECK - self.icon_color = "#00dd00" + self.icon_name = SeedSignerIconConstants.CHECKBOX_SELECTED + self.icon_color = GUIConstants.SUCCESS_COLOR else: - self.icon_name = FontAwesomeIconConstants.SQUARE + self.icon_name = SeedSignerIconConstants.CHECKBOX self.icon_color = GUIConstants.BODY_FONT_COLOR super().__post_init__() @@ -1268,7 +1283,7 @@ class TopNav(BaseComponent): icon_color: str = GUIConstants.BODY_FONT_COLOR font_name: str = GUIConstants.TOP_NAV_TITLE_FONT_NAME font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE - font_color: str = "#fcfcfc" + font_color: str = GUIConstants.BODY_FONT_COLOR show_back_button: bool = True show_power_button: bool = False is_selected: bool = False @@ -1283,7 +1298,7 @@ def __post_init__(self): 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, @@ -1293,7 +1308,7 @@ def __post_init__(self): 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 1ca904c09..76686039d 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -7,7 +7,7 @@ from seedsigner.models.threads import BaseThread from .screen import ButtonListScreen, WarningScreen -from ..components import (BtcAmount, Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerCustomIconConstants, TextArea, +from ..components import (BtcAmount, Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerIconConstants, TextArea, calc_bezier_curve, linear_interp) @@ -655,8 +655,8 @@ def __post_init__(self): # Adjust the vertical spacing screen_y -= GUIConstants.COMPONENT_PADDING self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, 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, screen_x=GUIConstants.EDGE_PADDING, @@ -665,8 +665,8 @@ def __post_init__(self): if self.is_change_addr_verified: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.CIRCLE_CHECK, - icon_color="#00dd00", + icon_name=SeedSignerIconConstants.SUCCESS, + icon_color=GUIConstants.SUCCESS_COLOR, value_text="Address verified!", is_text_centered=False, screen_x=GUIConstants.EDGE_PADDING, @@ -685,7 +685,7 @@ def __post_init__(self): icon = Icon( icon_name=FontAwesomeIconConstants.PAPER_PLANE, - icon_color=GUIConstants.SUCCESS_COLOR, + icon_color=GUIConstants.INFO_COLOR, icon_size=GUIConstants.ICON_LARGE_BUTTON_SIZE, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING ) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index e3eb0e445..306ba968e 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -10,7 +10,8 @@ from seedsigner.models.settings import SettingsConstants from ..components import (FontAwesomeIconConstants, GUIConstants, BaseComponent, Button, Icon, IconButton, - LargeIconButton, SeedSignerCustomIconConstants, TopNav, TextArea, load_image, ToastOverlay) + LargeIconButton, SeedSignerIconConstants, TopNav, TextArea, load_image, ToastOverlay, + Fonts) from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons @@ -690,7 +691,7 @@ def add_brightness_tips(self, image: Image.Image) -> None: 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() @@ -700,7 +701,7 @@ def add_brightness_tips(self, image: Image.Image) -> None: 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() @@ -822,7 +823,7 @@ def _run(self): @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 @@ -946,7 +947,7 @@ def __post_init__(self): @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 @@ -1057,7 +1058,7 @@ def __post_init__(self): # Render the right button panel (only has a Key3 "Save" button) self.save_button = IconButton( - icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK, + icon_name=SeedSignerIconConstants.CHECK, icon_color=GUIConstants.SUCCESS_COLOR, width=right_panel_buttons_width, screen_x=hw_button_x, @@ -1240,7 +1241,7 @@ def _render(self): 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" ) @@ -1248,7 +1249,7 @@ def _render(self): 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 484f368c8..ac2230b83 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -11,7 +11,8 @@ from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen, WarningEdgesMixin from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddress, IconButton, - IconTextLine, SeedSignerCustomIconConstants, TextArea, GUIConstants) + IconTextLine, SeedSignerIconConstants, TextArea, GUIConstants, + calc_text_centering) from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants @@ -416,8 +417,8 @@ def __post_init__(self): super().__post_init__() self.fingerprint_icontl = IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, icon_size=GUIConstants.ICON_FONT_SIZE + 12, label_text="fingerprint", value_text=self.fingerprint, @@ -436,8 +437,8 @@ class SeedOptionsScreen(ButtonListScreen): has_passphrase: bool = False def __post_init__(self): - self.top_nav_icon_name = SeedSignerCustomIconConstants.FINGERPRINT - self.top_nav_icon_color = "blue" + self.top_nav_icon_name = SeedSignerIconConstants.FINGERPRINT + self.top_nav_icon_color = GUIConstants.INFO_COLOR self.title = self.fingerprint self.is_button_text_centered = False self.is_bottom_list = True @@ -497,7 +498,7 @@ def __post_init__(self): for index, word in enumerate(self.words): draw.rounded_rectangle( (number_box_x, number_box_y, number_box_x + number_box_width, number_box_y + number_box_height), - fill="#202020", + fill=GUIConstants.BUTTON_BACKGROUND_COLOR, radius=5 * supersampling_factor ) baseline_y = number_box_y + number_box_height - int((number_box_height - number_height)/2) @@ -505,7 +506,7 @@ def __post_init__(self): (number_box_x + int(number_box_width/2), baseline_y), font=number_font, text=str(self.page_index * words_per_page + index + 1), - fill="#0084ff", + fill=GUIConstants.INFO_COLOR, anchor="ms" # Middle (centered), baSeline ) @@ -579,8 +580,8 @@ def __post_init__(self): # Set up the fingerprint and passphrase displays self.fingerprint_line = IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, label_text="Fingerprint", value_text=self.fingerprint, screen_x=GUIConstants.COMPONENT_PADDING, @@ -589,7 +590,8 @@ def __post_init__(self): self.components.append(self.fingerprint_line) self.derivation_line = IconTextLine( - icon_name=SeedSignerCustomIconConstants.PATH, + icon_name=SeedSignerIconConstants.DERIVATION, + icon_color=GUIConstants.INFO_COLOR, label_text="Derivation", value_text=self.derivation_path, screen_x=GUIConstants.COMPONENT_PADDING, @@ -599,6 +601,7 @@ def __post_init__(self): self.xpub_line = IconTextLine( icon_name=FontAwesomeIconConstants.X, + icon_color=GUIConstants.INFO_COLOR, label_text="Xpub", value_text=f"{self.xpub[:18]}...", font_name=GUIConstants.FIXED_WIDTH_FONT_NAME, @@ -788,7 +791,7 @@ def __post_init__(self): ) self.hw_button3 = IconButton( - icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK, + icon_name=SeedSignerIconConstants.CHECK, icon_color=GUIConstants.SUCCESS_COLOR, width=self.right_panel_buttons_width, screen_x=hw_button_x, @@ -1004,8 +1007,8 @@ def __post_init__(self): super().__post_init__() self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, label_text="changes fingerprint", value_text=f"{self.fingerprint_without} >> {self.fingerprint_with}", is_text_centered=True, diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index 65ffcc3db..b2188e0dd 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -3,7 +3,7 @@ from PIL.Image import Image from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.camera import Camera -from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, FormattedAddress, GUIConstants, IconTextLine, SeedSignerCustomIconConstants, TextArea +from seedsigner.gui.components import FontAwesomeIconConstants, Fonts, FormattedAddress, GUIConstants, IconTextLine, SeedSignerIconConstants, TextArea from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, KeyboardScreen from seedsigner.hardware.buttons import HardwareButtonsConstants @@ -362,8 +362,8 @@ def __post_init__(self): )) self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, label_text="fingerprint", value_text=self.fingerprint, is_text_centered=True, @@ -386,8 +386,8 @@ def __post_init__(self): if self.fingerprint: self.components.append(IconTextLine( - icon_name=SeedSignerCustomIconConstants.FINGERPRINT, - icon_color="blue", + icon_name=SeedSignerIconConstants.FINGERPRINT, + icon_color=GUIConstants.INFO_COLOR, label_text="Fingerprint", value_text=self.fingerprint, screen_x=GUIConstants.EDGE_PADDING, @@ -396,7 +396,7 @@ def __post_init__(self): 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 @@ def __post_init__(self): )) 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 63a0008cd..000000000 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 000000000..ef4292b28 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 b09cb0195..481390651 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -3,7 +3,7 @@ from embit.networks import NETWORKS from seedsigner.controller import Controller -from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.psbt_parser import PSBTParser from seedsigner.models.qr_type import QRType @@ -15,7 +15,7 @@ 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) @@ -36,7 +36,7 @@ def run(self): # Doesn't look like this seed can sign the current PSBT button_str += " (?)" - button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT)) button_data.append(self.SCAN_SEED) button_data.append(self.TYPE_12WORD) @@ -522,7 +522,7 @@ def run(self): # 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 cd09b7000..0570b143d 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -9,7 +9,8 @@ from typing import List from seedsigner.controller import Controller -from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants +from seedsigner.helpers import embit_utils from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, seed_screens) from seedsigner.gui.screens.screen import LargeIconStatusScreen, QRDisplayScreen @@ -46,7 +47,7 @@ def run(self): button_data = [] for seed in self.seeds: - button_data.append((seed["fingerprint"], SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + button_data.append((seed["fingerprint"], SeedSignerIconConstants.FINGERPRINT)) button_data.append("Load a seed") selected_menu_num = self.run_screen( @@ -71,10 +72,10 @@ def run(self): 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 = [ @@ -337,11 +338,11 @@ def run(self): 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") @@ -1351,7 +1352,7 @@ def __init__(self, seed_num: int): def run(self): - SCAN = ("Confirm SeedQR", FontAwesomeIconConstants.QRCODE) + SCAN = ("Confirm SeedQR", SeedSignerIconConstants.QRCODE) DONE = "Done" button_data = [SCAN, DONE] @@ -1526,7 +1527,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 = [] @@ -1535,7 +1536,7 @@ def run(self): 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)) text = "Select seed to verify" button_data.append(SCAN_SEED) @@ -1781,7 +1782,7 @@ def run(self): 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 bc02281b2..ca9c36dcf 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -1,5 +1,5 @@ import logging -from seedsigner.gui.components import SeedSignerCustomIconConstants +from seedsigner.gui.components import SeedSignerIconConstants from .view import View, Destination, MainMenuView @@ -40,7 +40,7 @@ def run(self): 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 @@ def run(self): 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 6aa220c42..8e9b5ef9b 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -8,7 +8,7 @@ from PIL.ImageOps import autocontrast from seedsigner.controller import Controller from seedsigner.hardware.camera import Camera -from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerCustomIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen from seedsigner.helpers import embit_utils, mnemonic_generation @@ -431,8 +431,8 @@ def run(self): 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) @@ -442,7 +442,7 @@ def run(self): 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)) button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD] selected_menu_num = self.run_screen( @@ -640,7 +640,7 @@ def run(self): 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 5a9c65cdb..3bf92beea 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Type -from seedsigner.gui.components import FontAwesomeIconConstants +from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen from seedsigner.models.settings import Settings @@ -136,10 +136,10 @@ def __ne__(self, obj): # ######################################################################################### 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] @@ -174,8 +174,8 @@ def run(self): class PowerOptionsView(View): - RESET = ("Restart", FontAwesomeIconConstants.ROTATE_RIGHT) - POWER_OFF = ("Power Off", FontAwesomeIconConstants.POWER_OFF) + RESET = ("Restart", SeedSignerIconConstants.RESTART) + 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 000000000..f25b163db --- /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 +``` + +Writes the screenshots to a dir in the project root: `seedsigner-screenshots`. diff --git a/tests/screenshot_generator/__init__.py b/tests/screenshot_generator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/screenshot_generator/conftest.py b/tests/screenshot_generator/conftest.py new file mode 100644 index 000000000..43f690b84 --- /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 000000000..037a79055 --- /dev/null +++ b/tests/screenshot_generator/generator.py @@ -0,0 +1,240 @@ +import embit +import os +import sys +from mock import Mock, patch, MagicMock + +# Prevent importing modules w/Raspi hardware dependencies. +# These must precede any SeedSigner imports. +sys.modules['seedsigner.hardware.ST7789'] = MagicMock() +sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock() +sys.modules['seedsigner.views.screensaver'] = MagicMock() +sys.modules['seedsigner.hardware.buttons'] = MagicMock() +sys.modules['seedsigner.hardware.camera'] = MagicMock() +sys.modules['seedsigner.hardware.microsd'] = MagicMock() + + +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 (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView, + 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`. + """ + # Prep the ScreenshotRenderer that will be patched over the normal Renderer + screenshot_root = os.path.join(os.getcwd(), "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) + UnhandledExceptionViewFood = ["IndexError", "line 1, in some_buggy_code.py", "list index out of range"] + + # 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) + + 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) + # so we get a choice for transcribe seed qr format + controller.settings.set_value( + attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR, + value=SettingsConstants.OPTION__ENABLED + ) + 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": [ + MainMenuView, + PowerOptionsView, + RestartView, + #PowerOffView # this test is too real; pi will power-off + NotYetImplementedView, + (UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)), + (settings_views.SettingsIngestSettingsQRView, dict(data="settings::v1 name=factory_reset")) + + ], + "Seed Views": [ + seed_views.SeedsMenuView, + seed_views.LoadSeedView, + seed_views.SeedMnemonicEntryView, + seed_views.SeedMnemonicInvalidView, + seed_views.SeedFinalizeView, + seed_views.SeedAddPassphraseView, + seed_views.SeedReviewPassphraseView, + + (seed_views.SeedOptionsView, dict(seed_num=0)), + (seed_views.SeedBackupView, dict(seed_num=0)), + (seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)), + (seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")), + (seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")), + (seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")), + (seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")), + (seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")), + #SeedExportXpubQRDisplayView, + (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.SeedBIP85ApplicationModeView, dict(seed_num=0)), + (seed_views.SeedBIP85SelectChildIndexView, dict(seed_num=0, num_words=24)), + (seed_views.SeedBIP85InvalidChildIndexView, dict(seed_num=0, num_words=12)), + (seed_views.SeedWordsBackupTestPromptView, dict(seed_num=0)), + (seed_views.SeedWordsBackupTestView, dict(seed_num=0)), + (seed_views.SeedWordsBackupTestMistakeView, dict(seed_num=0, cur_index=7, wrong_word="unlucky")), + (seed_views.SeedWordsBackupTestSuccessView, dict(seed_num=0)), + (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.AddressVerificationStartView, dict(address=, script_type="nat", network="M")), + #seed_views.AddressVerificationSigTypeView, + #seed_views.SeedSingleSigAddressVerificationSelectSeedView, + #seed_views.SeedAddressVerificationView, + #seed_views.AddressVerificationSuccessView, + + seed_views.LoadMultisigWalletDescriptorView, + seed_views.MultisigWalletDescriptorView, + (seed_views.SeedDiscardView, dict(seed_num=0)), + ], + "PSBT Views": [ + psbt_views.PSBTSelectSeedView, # this will fail, be rerun below + 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, + #PSBTSignedQRDisplayView + psbt_views.PSBTSigningErrorView, + ], + "Tools Views": [ + tools_views.ToolsMenuView, + #ToolsImageEntropyLivePreviewView + #ToolsImageEntropyFinalImageView + tools_views.ToolsImageEntropyMnemonicLengthView, + tools_views.ToolsDiceEntropyMnemonicLengthView, + (tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)), + tools_views.ToolsCalcFinalWordNumWordsView, + tools_views.ToolsCalcFinalWordFinalizePromptView, + tools_views.ToolsCalcFinalWordCoinFlipsView, + #(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips=3)), + #tools_views.ToolsCalcFinalWordDoneView, + tools_views.ToolsAddressExplorerSelectSourceView, + tools_views.ToolsAddressExplorerAddressTypeView, + tools_views.ToolsAddressExplorerAddressListView, + #tools_views.ToolsAddressExplorerAddressView, + ], + "Settings Views": settings_views_list, + } + + + screenshot_renderer.set_screenshot_path(screenshot_root) + + readme = f"""# SeedSigner Screenshots\n""" + + for section_name, screenshot_list in screenshot_sections.items(): + readme += "\n\n---\n\n" + readme += f"## {section_name}\n\n" + readme += """""" + readme += f"""
\n""" + 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) + readme += """ """ + readme += f"""""" + readme += """
{view_name}

\n""" + + readme += "
" + + # many screens don't work, leaving a missing image, re-run here for now + controller.psbt_seed = None + screencap_view(psbt_views.PSBTSelectSeedView, 'PSBTSelectSeedView', {}) + + with open(os.path.join(screenshot_renderer.screenshot_path, "README.md"), 'w') as readme_file: + readme_file.write(readme) diff --git a/tests/screenshot_generator/utils.py b/tests/screenshot_generator/utils.py new file mode 100644 index 000000000..703bbf61a --- /dev/null +++ b/tests/screenshot_generator/utils.py @@ -0,0 +1,55 @@ +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() +