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"""{view_name}
| """
+ readme += """ \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()
+