mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 23:18:25 +00:00
Merge pull request #620 from kdmukai/initial_multilanguage_0.8.0
Initial l10n / multilanguage support
This commit is contained in:
@@ -27,6 +27,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# Needs to also pull the seedsigner-translations repo
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
@@ -45,16 +48,20 @@ jobs:
|
||||
--cov=seedsigner \
|
||||
--cov-append \
|
||||
--cov-branch \
|
||||
--cov-report term \
|
||||
--cov-report html \
|
||||
--cov-report html:./artifacts/cov_html \
|
||||
--cov-report xml \
|
||||
--durations 5 \
|
||||
-vv
|
||||
- name: Generate screenshots
|
||||
run: |
|
||||
python -m pytest tests/screenshot_generator/generator.py
|
||||
python -m pytest tests/screenshot_generator/generator.py \
|
||||
--color=yes \
|
||||
--cov=seedsigner \
|
||||
--cov-append \
|
||||
--cov-branch \
|
||||
--cov-report html:./artifacts/cov_html \
|
||||
-vv
|
||||
cp -r ./seedsigner-screenshots ./artifacts/
|
||||
- name: Coverage report
|
||||
run: coverage report
|
||||
- name: Archive CI Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
+4
-3
@@ -5,6 +5,7 @@ src/seedsigner.egg-info/
|
||||
.vscode
|
||||
src/seedsigner/models/settings_definition.json
|
||||
.idea
|
||||
*.mo
|
||||
.coverage
|
||||
seedsigner-screenshots
|
||||
.coverage*
|
||||
|
||||
*.po
|
||||
*.mo
|
||||
@@ -0,0 +1,9 @@
|
||||
[submodule "src/seedsigner/resources/seedsigner-translations"]
|
||||
path = src/seedsigner/resources/seedsigner-translations
|
||||
url = https://github.com/SeedSigner/seedsigner-translations.git
|
||||
branch = dev
|
||||
[submodule "seedsigner-screenshots"]
|
||||
path = seedsigner-screenshots
|
||||
url = https://github.com/SeedSigner/seedsigner-screenshots.git
|
||||
branch = dev
|
||||
update = none
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
# Localization (l10n) Developer Notes
|
||||
|
||||
## High-level overview
|
||||
1. Python code indicates text that needs to be translated.
|
||||
1. Those marked strings are extracted into a master `messages.pot` file.
|
||||
1. That file is uploaded [Transifex](https://app.transifex.com/seedsigner/seedsigner).
|
||||
1. Translators work within Transifex on their respective languages.
|
||||
1. Completed translations are downloaded as `messages.po` files for each language.
|
||||
1. Python "compiles" them into `messages.mo` files ready for use.
|
||||
1. The `*.po` and `*.mo` files are written to the [seedsigner-translations](https://github.com/SeedSigner/seedsigner-translations) repo.
|
||||
1. That repo is linked as a submodule here as `seedsigner.resources.seedsigner-translations`.
|
||||
1. Python code retrieves a translation on demand.
|
||||
|
||||
|
||||
## "Wrapping" text for translation
|
||||
Any text that we want to be presented in multiple languages needs to "wrapped".
|
||||
|
||||
The CORE CONCEPT to understand is that wrapping is used in TWO different contexts:
|
||||
1. Pre-translation: This is how we identify text that translators need to translate. Any wrapped string literals will appear in translators' Transifex UI.
|
||||
2. Post-translation: Return the locale-specific translation for that source string (defaults to the English string if no translation is found).
|
||||
|
||||
We have three techniques to wrap code, depending on which of the above contexts we're in and where we are in the code:
|
||||
|
||||
|
||||
#### Technique 1: `ButtonOption`
|
||||
Most `View` classes will render themselves via some variation of the `ButtonListScreen` which takes a `button_data` list as an input. Each entry in
|
||||
`button_data` must be a `ButtonOption`. The first argument for `ButtonOption` is the `button_label` string. This is the English string literal that
|
||||
is displayed in that button. If you look at `setup.cfg` you'll see that `ButtonOption` is listed as a keyword in `extract_messages`. That means
|
||||
that the first argument for `ButtonOption` -- its `button_label` string -- will be marked for translation (by default the `extract_messages`
|
||||
integration will only look at the first argument of any method listed in `keywords`).
|
||||
|
||||
```python
|
||||
class SomeView(View):
|
||||
# These string literals will be marked for translation
|
||||
OPTION_1 = ButtonOption("Option 1!")
|
||||
OPTION_2 = ButtonOption("Option 2!")
|
||||
|
||||
def run(self):
|
||||
button_data = [self.OPTION_1, self.OPTION_2]
|
||||
|
||||
# No way for `extract_messages` to know what's in `some_var`; won't be marked for
|
||||
# translation unless it's specified elsewhere.
|
||||
some_var = some_value
|
||||
button_data.append(ButtonOption(some_var))
|
||||
```
|
||||
|
||||
These `ButtonOption` values are generally specified in class-level attributes, as in the above example. Classes in python are imported once, after
|
||||
which class-level attributes are never reinterpreted again; **the value at import time for a class-level attribute is its value for the duration of
|
||||
the program execution.**
|
||||
|
||||
This means that we must assume that `ButtonOption.button_label` strings are ALWAYS the original English string. This is crucial because the English
|
||||
values are the lookup keys for the translations:
|
||||
|
||||
* `ButtonOption.button_label` = "Hello!" in the python code.
|
||||
* Run the code, the class that contains our `ButtonOption` as a class-level attribute is imported.
|
||||
* Regardless of language selection, that `ButtonOption` will always return "Hello!".
|
||||
* `Screen` then uses "Hello!" as a key to find the translation "¡Hola!".
|
||||
* User sees "¡Hola!".
|
||||
|
||||
IF `ButtonOption` were wired to return the translated string, we'd have a problem:
|
||||
* User sets their language to Spanish and enables persistent settings.
|
||||
* Launch SeedSigner. At import time the `button_label`'s value is translated to "¡Hola!".
|
||||
* User sees "¡Hola!" in the UI. All good.
|
||||
* User changes language to English (or any other language).
|
||||
* Now the `Screen` must find the matching string in a different translation file.
|
||||
* But the `button_label` value was fixed at import time; it's still providing "¡Hola!" as the lookup key.
|
||||
* Since all the translation files map English -> translation, no such "¡Hola!" match exists in any translation file.
|
||||
* So the translation falls back to just displaying the unmatched key: "¡Hola!"
|
||||
|
||||
tldr: `ButtonOption` marks its `button_label` English string literal for translation, but NEVER provides a translated value.
|
||||
|
||||
---
|
||||
|
||||
#### Technique 2: `seedsigner.helpers.l10n.mark_for_translation`
|
||||
You'll see that `mark_for_translation` is imported as `_mft` for short.
|
||||
|
||||
As far as translations are concerned, `_mft` serves the same purpose as `ButtonOption`. The only difference is that `_mft` is for all other
|
||||
(non-`button_data`) class-level attributes.
|
||||
|
||||
```python
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
|
||||
@classmethod
|
||||
class SomeView(View):
|
||||
title: str = _mft("Default Title")
|
||||
text: str = _mft("My default body text")
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
SomeScreen,
|
||||
title=self.title,
|
||||
text=self.text
|
||||
)
|
||||
```
|
||||
|
||||
In general we try to avoid using `_mft` at all, but some class-level attributes just can't be avoided.
|
||||
|
||||
---
|
||||
|
||||
#### Technique 3: `gettext`, aka `_()`
|
||||
This is the way you'll see text wrapping handled in the vast majority of tutorials.
|
||||
|
||||
```python
|
||||
from gettext import gettext as _
|
||||
|
||||
my_text = _("Hello!")
|
||||
|
||||
# Specify Spanish
|
||||
os.environ['LANGUAGE'] = "es"
|
||||
print(my_text)
|
||||
>> ¡Hola!
|
||||
|
||||
# Specify English
|
||||
os.environ['LANGUAGE'] = "en"
|
||||
print(my_text)
|
||||
>> Hello!
|
||||
```
|
||||
|
||||
This approach marks string literals for translation AND retrieves the translated text.
|
||||
|
||||
We do the same in SeedSigner code, but only when the string literal is in a part of the code that is dynamically evaluated:
|
||||
|
||||
```python
|
||||
from gettext import gettext as _
|
||||
|
||||
class SomeView(View):
|
||||
def __init__(self):
|
||||
# Mark string literal for translation AND dynamically retrieve its translated value
|
||||
self.some_var = _("I will be dynamically fetched")
|
||||
```
|
||||
|
||||
Though note that there are times when we use `_()` only for the retrieval side:
|
||||
|
||||
```python
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
|
||||
class SomeView(View):
|
||||
message = _mft("Hello!") # mark for translation, but always return "Hello!"
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
SomeScreen,
|
||||
message=self.title
|
||||
)
|
||||
|
||||
# elsewhere...
|
||||
@dataclass
|
||||
class SomeScreen(Screen):
|
||||
message: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
message_display = TextArea(
|
||||
text=_(self.message) # The _() wrapping here now retrieves the translated value, if one is available
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic rules
|
||||
* English string literals in class-level attributes should be wrapped with either `ButtonOption` (for `button_data` entries) or `_mft` (for misc class-level attrs) so they'll be picked up for translation.
|
||||
* English string literals anywhere else should be wrapped with `_()` to be marked for translation AND provide the dynamic translated value.
|
||||
* In general, don't go out of your way to translate text before passing it into `Screen` classes.
|
||||
* The `Screen` itself should do most of the `_()` calls to fetch translations for final display.
|
||||
* Minor risk of double-translation weirdness otherwise.
|
||||
|
||||
Mark for translation in the `View`. Retrieve translated values in the `Screen`. Pass final display text into the basic gui `Component`s.
|
||||
|
||||
---
|
||||
|
||||
## Provide translation context hints
|
||||
In many cases the English string literal on its own does not provide enough context for translators to understand how the word is being used.
|
||||
|
||||
For example, is "change" referring to altering a value OR is it the amount coming back to you in a transaction?
|
||||
|
||||
Whenever necessary, add explanatory context as a comment. This applies to all three ways of marking strings for translation.
|
||||
|
||||
The `extract_messages` command is explictly looking for the exact string: `# TRANSLATOR_NOTE:` in comments.
|
||||
|
||||
```python
|
||||
class SeedAddressVerificationView(View):
|
||||
# TRANSLATOR_NOTE: Option when scanning for a matching address; skips ten addresses ahead
|
||||
SKIP_10 = ButtonOption("Skip 10")
|
||||
```
|
||||
|
||||
Note that the comment MUST be on the preceding line of executable code for it to work:
|
||||
|
||||
```python
|
||||
class SettingsConstants
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High <-- ✅ Correct way to add context
|
||||
density_low = _mft("Low")
|
||||
|
||||
ALL_DENSITIES = [
|
||||
(DENSITY__LOW, density_low),
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High <-- ❌ Note will NOT be picked up
|
||||
(DENSITY__MEDIUM, "Medium"),
|
||||
(DENSITY__HIGH, "High"),
|
||||
]
|
||||
```
|
||||
|
||||
```python
|
||||
# TRANSLATOR_NOTE: Refers to the user's change output in a psbt
|
||||
some_var = _("change")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `_()` Wrapping syntax details
|
||||
* Use `.format()` to wrap strings with variable injections. Note that `.format()` is OUTSIDE the `_()` wrapping.
|
||||
```python
|
||||
mystr = f"My dad's name is {dad.name} and my name is {self.name}."
|
||||
mystr = _("My dad's name is {} and my name is {}").format(dad.name, self.name)
|
||||
```
|
||||
|
||||
The translators will only see: "My dad's name is {} and my name is {}" in Transifex. Often the English string literal is
|
||||
basically incomprehensible on its own so always provide an explanation for what is being injected:
|
||||
|
||||
```python
|
||||
# TRANSLATOR_NOTE: Address verification success message (e.g. "bc1qabc = seed 12345678's receive address #0.")
|
||||
text = _("{} = {}'s {} address #{}.").format(...)
|
||||
```
|
||||
|
||||
If there are a lot of variables to inject, placeholder names can be used (TODO: how does Transifex display this?):
|
||||
```python
|
||||
mystr = _("My dad's name is {dad_name} and my name is {my_name}").format(dad_name=dad.name, my_name=self.name)
|
||||
```
|
||||
* Use `ngettext` to dynamically handle singular vs plural forms based on an integer quantity:
|
||||
```python
|
||||
n = 1
|
||||
print(ngettext("apple", "apples", n))
|
||||
>> apple
|
||||
|
||||
n = 5
|
||||
print(ngettext("apple", "apples", n))
|
||||
>> apples
|
||||
```
|
||||
|
||||
Transifex will ask translators to provide the singular and plural forms on a language-specific basis (e.g. Arabic as THREE plural forms!).
|
||||
|
||||
---
|
||||
|
||||
## Set up localization dependencies
|
||||
```bash
|
||||
pip install -r l10n/requirements-l10n.txt
|
||||
```
|
||||
|
||||
Make sure that your local repo has fetched the `seedsigner-translations` submodule. It's configured to add it in src/seedsigner/resources.
|
||||
```bash
|
||||
# Need --remote in order to respect the target branch listed in .gitmodules
|
||||
git submodule update --remote
|
||||
```
|
||||
|
||||
|
||||
### Pre-configured `babel` commands
|
||||
The `setup.cfg` file in the project root specifies params for the various `babel` commands discussed below.
|
||||
|
||||
You should have already added the local code as an editable project in pip:
|
||||
```bash
|
||||
# From the repo root
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
|
||||
### Rescanning for text that needs translations
|
||||
Re-generate the `messages.pot` file:
|
||||
|
||||
```bash
|
||||
python setup.py extract_messages
|
||||
```
|
||||
|
||||
This will rescan all wrapped text, picking up new strings as well as updating existings strings that have been edited.
|
||||
|
||||
_TODO: Github Action to auto-generate messages.pot and fail a PR update if the PR has an out of date messages.pot?_
|
||||
|
||||
|
||||
### Making new text available to translators
|
||||
Upload the master `messages.pot` to Transifex. It will automatically update each language with the new or changed source strings.
|
||||
|
||||
_TODO: Look into Transifex options to automatically pull updates?_
|
||||
|
||||
|
||||
### Once new translations are complete
|
||||
The translation file for each language will need to be downloaded via Transifex's "Download for use" option (sends you a `messages.po` file for that language).
|
||||
|
||||
This updated `messages.po` should be added to the seedsigner-translations repo in l10n/`{TARGET_LOCALE}`/LC_MESSAGES.
|
||||
|
||||
|
||||
### Compile all the translations
|
||||
The `messages.po` files must be compiled into `*.mo` files:
|
||||
|
||||
```bash
|
||||
python setup.py compile_catalog
|
||||
|
||||
# Or target a specific language code:
|
||||
python setup.py compile_catalog -l es
|
||||
```
|
||||
|
||||
### Unused babel commands
|
||||
Transifex eliminates the need for the `init_catalog` and `update_catalog` commands.
|
||||
|
||||
|
||||
## Keep the seedsigner-translations repo up to date
|
||||
The *.po files for each language and their compiled *.mo files should all be kept up to date in the seedsigner-translations repo.
|
||||
|
||||
_TODO: Github Actions automation to regenerate / verify that the *.mo files have been updated after *.po changes._
|
||||
|
||||
---
|
||||
|
||||
## Generate screenshots in each language
|
||||
Simply run the screenshot generator:
|
||||
|
||||
```bash
|
||||
pytest tests/screenshot_generator/generator.py
|
||||
|
||||
# Or target a specific language code:
|
||||
pytest tests/screenshot_generator/generator.py --locale es
|
||||
```
|
||||
+1489
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
Babel==2.16.0
|
||||
+3
-4
@@ -13,7 +13,7 @@ description = "Build an offline, airgapped Bitcoin signing device for less than
|
||||
name = "seedsigner"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "0.8.0"
|
||||
version = "0.8.5"
|
||||
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues"
|
||||
@@ -47,13 +47,12 @@ exclude_lines = [
|
||||
omit = [
|
||||
"*/__init__.py",
|
||||
"*/tests/*",
|
||||
"*/pyzbar/*",
|
||||
"*/gui/*"
|
||||
]
|
||||
skip_covered = true
|
||||
skip_covered = false
|
||||
skip_empty = true
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src"]
|
||||
branch = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
Submodule
+1
Submodule seedsigner-screenshots added at 23cf46e148
@@ -0,0 +1,12 @@
|
||||
[extract_messages]
|
||||
keywords = _ _mft mark_for_translation ButtonOption
|
||||
add_comments = TRANSLATOR_NOTE:
|
||||
strip_comments = yes
|
||||
add_location = file
|
||||
input_dirs = src
|
||||
output_file = l10n/messages.pot
|
||||
|
||||
|
||||
[compile_catalog]
|
||||
use_fuzzy = yes
|
||||
directory = src/seedsigner/resources/seedsigner-translations/l10n
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Empty setup.py necessary for the python-babel integration (e.g. python setup.py extract_messages).
|
||||
|
||||
See the configuration in setup.cfg.
|
||||
"""
|
||||
import setuptools
|
||||
|
||||
setuptools.setup()
|
||||
@@ -99,7 +99,7 @@ class Controller(Singleton):
|
||||
rather than at the top in order avoid circular imports.
|
||||
"""
|
||||
|
||||
VERSION = "0.8.0"
|
||||
VERSION = "0.8.5"
|
||||
|
||||
# Declare class member vars with type hints to enable richer IDE support throughout
|
||||
# the code.
|
||||
@@ -147,20 +147,20 @@ class Controller(Singleton):
|
||||
else:
|
||||
# Instantiate the one and only Controller instance
|
||||
return cls.configure_instance()
|
||||
|
||||
|
||||
@classmethod
|
||||
def reset_instance(cls):
|
||||
"""
|
||||
Currently used by the screenshot generator, but could potentially be used to
|
||||
wipe and reset the state of the device.
|
||||
"""
|
||||
cls._instance = None
|
||||
cls.configure_instance()
|
||||
|
||||
|
||||
@classmethod
|
||||
def configure_instance(cls, disable_hardware=False):
|
||||
"""
|
||||
- `disable_hardware` is only meant to be used by the test suite so that it
|
||||
can keep re-initializing a Controller in however many tests it needs to. But
|
||||
this is only possible if the hardware isn't already being reserved. Without
|
||||
this you get:
|
||||
|
||||
RuntimeError: Conflicting edge detection already enabled for this GPIO channel
|
||||
|
||||
each time you try to re-initialize a Controller.
|
||||
"""
|
||||
def configure_instance(cls):
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
|
||||
@@ -248,10 +248,10 @@ class Controller(Singleton):
|
||||
used. Only used by the test suite.
|
||||
"""
|
||||
from seedsigner.views import MainMenuView, BackStackView
|
||||
from seedsigner.views.screensaver import OpeningSplashScreen
|
||||
from seedsigner.views.screensaver import OpeningSplashView
|
||||
from seedsigner.gui.toast import RemoveSDCardToastManagerThread
|
||||
|
||||
OpeningSplashScreen().start()
|
||||
OpeningSplashView().run()
|
||||
|
||||
""" Class references can be stored as variables in python!
|
||||
|
||||
|
||||
+599
-110
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from typing import Tuple
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import Fonts, GUIConstants
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
@@ -8,6 +9,10 @@ from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
|
||||
|
||||
class Keyboard:
|
||||
"""
|
||||
Note that it is up to the calling Screen to manage the Renderer.lock. The Keyboard
|
||||
and its child Keys should NOT attempt to manage the lock.
|
||||
"""
|
||||
WRAP_TOP = "wrap_top"
|
||||
WRAP_BOTTOM = "wrap_bottom"
|
||||
WRAP_LEFT = "wrap_left"
|
||||
@@ -27,39 +32,43 @@ class Keyboard:
|
||||
REGULAR_KEY_FONT = "regular"
|
||||
COMPACT_KEY_FONT = "compact"
|
||||
|
||||
# TRANSLATOR_NOTE: The abbreviated label for the special key <del> on a standard keyboard.
|
||||
del_label = _("del")
|
||||
KEY_BACKSPACE = {
|
||||
"code": "DEL",
|
||||
"letter": "del",
|
||||
"letter": del_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 2,
|
||||
}
|
||||
# TRANSLATOR_NOTE: The abbreviated label for the special key <space> on a standard keyboard.
|
||||
space_label = _("space")
|
||||
KEY_SPACE = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 1,
|
||||
}
|
||||
KEY_SPACE_2 = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 2,
|
||||
}
|
||||
KEY_SPACE_3 = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 3,
|
||||
}
|
||||
KEY_SPACE_4 = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 4,
|
||||
}
|
||||
KEY_SPACE_5 = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 5,
|
||||
}
|
||||
@@ -158,7 +167,7 @@ class Keyboard:
|
||||
self.screen_x + int(self.keyboard.key_width * self.size / 2),
|
||||
self.screen_y + self.keyboard.key_height - int((self.keyboard.key_height - text_height)/2)
|
||||
),
|
||||
self.letter,
|
||||
_(self.letter),
|
||||
fill=font_color,
|
||||
font=font,
|
||||
anchor="ms"
|
||||
@@ -208,6 +217,7 @@ class Keyboard:
|
||||
|
||||
# Set up the rendering and state params
|
||||
self.active_keys = list(self.charset)
|
||||
|
||||
self.additonal_key_compact_font = Fonts.get_font("RobotoCondensed-Bold", 18)
|
||||
self.x_start = rect[0]
|
||||
self.y_start = rect[1]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from PIL import Image, ImageDraw
|
||||
from threading import Lock
|
||||
|
||||
from seedsigner.gui.components import Fonts, GUIConstants
|
||||
from seedsigner.hardware.ST7789 import ST7789
|
||||
from seedsigner.models.singleton import ConfigurableSingleton
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
from typing import List
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from gettext import ngettext
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
from seedsigner.gui.components import (BtcAmount, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerIconConstants, TextArea,
|
||||
calc_bezier_curve, linear_interp)
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.models.threads import BaseThread
|
||||
|
||||
from .screen import ButtonListScreen, WarningScreen
|
||||
from ..components import (BtcAmount, Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerIconConstants, TextArea,
|
||||
calc_bezier_curve, linear_interp)
|
||||
from .screen import ButtonListScreen, ButtonOption
|
||||
|
||||
|
||||
|
||||
@@ -21,15 +23,15 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
num_inputs: int = 0
|
||||
num_self_transfer_outputs: int = 0
|
||||
num_change_outputs: int = 0
|
||||
destination_addresses: List[str] = None
|
||||
destination_addresses: list[str] = None
|
||||
has_op_return: bool = False
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = "Review PSBT"
|
||||
self.title = _("Review PSBT")
|
||||
self.is_bottom_list = True
|
||||
self.button_data = ["Review Details"]
|
||||
self.button_data = [ButtonOption("Review Details")]
|
||||
|
||||
# This screen can take a while to load while parsing the PSBT
|
||||
self.show_loading_screen = True
|
||||
@@ -69,7 +71,7 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
font_size = GUIConstants.BODY_FONT_MIN_SIZE * ssf
|
||||
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, font_size)
|
||||
font = Fonts.get_font(GUIConstants.get_body_font_name(), font_size)
|
||||
|
||||
(left, top, right, bottom) = font.getbbox(text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890[]", anchor="lt")
|
||||
chart_text_height = bottom
|
||||
@@ -86,16 +88,18 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
# First calculate how wide the inputs col will be
|
||||
inputs_column = []
|
||||
if self.num_inputs == 1:
|
||||
inputs_column.append("1 input")
|
||||
inputs_column.append(_("1 input"))
|
||||
elif self.num_inputs > 5:
|
||||
inputs_column.append("input 1")
|
||||
inputs_column.append("input 2")
|
||||
inputs_column.append("[ ... ]")
|
||||
inputs_column.append(f"input {self.num_inputs-1}")
|
||||
inputs_column.append(f"input {self.num_inputs}")
|
||||
inputs_column.append(_("input 1"))
|
||||
inputs_column.append(_("input 2"))
|
||||
# TRANSLATOR_NOTE: Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], 8"
|
||||
inputs_column.append(_("[ ... ]"))
|
||||
# TRANSLATOR_NOTE: Input number will be inserted (e.g. "input 3")
|
||||
inputs_column.append(_("input {}").format(self.num_inputs-1))
|
||||
inputs_column.append(_("input {}").format(self.num_inputs))
|
||||
else:
|
||||
for i in range(0, self.num_inputs):
|
||||
inputs_column.append(f"input {i+1}")
|
||||
inputs_column.append(_("input {}").format(i+1))
|
||||
|
||||
max_inputs_text_width = 0
|
||||
for input in inputs_column:
|
||||
@@ -123,11 +127,12 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
# Now let's maximize the actual destination col by adjusting our addr truncation
|
||||
def calculate_destination_col_width(truncate_at: int = 0):
|
||||
def truncate_destination_addr(addr):
|
||||
# TODO: Properly handle the ellipsis truncation in different languages
|
||||
if len(addr) <= truncate_at + len("..."):
|
||||
# TRANSLATOR_NOTE: Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...")
|
||||
if len(addr) <= truncate_at + len(_("...")):
|
||||
# No point in truncating
|
||||
return addr
|
||||
return f"{addr[:truncate_at]}..."
|
||||
|
||||
return addr[:truncate_at] + _("...")
|
||||
|
||||
destination_column = []
|
||||
|
||||
@@ -136,21 +141,25 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
destination_column.append(truncate_destination_addr(addr))
|
||||
|
||||
for i in range(0, self.num_self_transfer_outputs):
|
||||
destination_column.append(truncate_destination_addr("self-transfer"))
|
||||
destination_column.append(truncate_destination_addr(_("self-transfer")))
|
||||
else:
|
||||
# destination_column.append(f"{len(self.destination_addresses)} recipients")
|
||||
destination_column.append(f"recipient 1")
|
||||
destination_column.append(f"[ ... ]")
|
||||
destination_column.append(f"recipient {len(self.destination_addresses) + self.num_self_transfer_outputs}")
|
||||
destination_column.append(_("recipient 1"))
|
||||
# TRANSLATOR_NOTE: Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], 8"
|
||||
destination_column.append(_("[ ... ]"))
|
||||
# TRANSLATOR_NOTE: Inserts the recipient number (e.g. the fifth one is: "recipient 5")
|
||||
destination_column.append(_("recipient {}").format(len(self.destination_addresses) + self.num_self_transfer_outputs))
|
||||
|
||||
destination_column.append(f"fee")
|
||||
destination_column.append(_("fee"))
|
||||
|
||||
if self.has_op_return:
|
||||
destination_column.append("OP_RETURN")
|
||||
# TRANSLATOR_NOTE: Technical term, should probably NOT be translated in most languages
|
||||
destination_column.append(_("OP_RETURN"))
|
||||
|
||||
if self.num_change_outputs > 0:
|
||||
for i in range(0, self.num_change_outputs):
|
||||
destination_column.append("change")
|
||||
# TRANSLATOR_NOTE: Label for a change output in the PSBT Overview flow diagram
|
||||
destination_column.append(_("change"))
|
||||
|
||||
max_destination_text_width = 0
|
||||
for destination in destination_column:
|
||||
@@ -164,10 +173,16 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
# We're not going to display any destination addrs so truncation doesn't matter
|
||||
(destination_text_width, destination_column) = calculate_destination_col_width()
|
||||
else:
|
||||
destination_text_width = None
|
||||
destination_column = None
|
||||
# Steadliy widen out the destination column until we run out of space
|
||||
for i in range(6, 14):
|
||||
(new_width, new_col_text) = calculate_destination_col_width(truncate_at=i)
|
||||
if new_width > max_destination_col_width:
|
||||
if not destination_text_width:
|
||||
destination_text_width = new_width
|
||||
if not destination_column:
|
||||
destination_column = new_col_text
|
||||
break
|
||||
destination_text_width = new_width
|
||||
destination_column = new_col_text
|
||||
@@ -349,7 +364,7 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
destination_y += destination_y_spacing
|
||||
|
||||
# Resize to target and sharpen final image
|
||||
image = image.resize((self.canvas_width, chart_height), Image.LANCZOS)
|
||||
image = image.resize((self.canvas_width, chart_height), Image.Resampling.LANCZOS)
|
||||
self.paste_images.append((image.filter(ImageFilter.SHARPEN), (self.chart_x, self.chart_y)))
|
||||
|
||||
# Pass input and output curves to the animation thread
|
||||
@@ -463,14 +478,14 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = "PSBT Math"
|
||||
self.button_data = ["Review Recipients"]
|
||||
self.title = _("PSBT Math")
|
||||
self.button_data = [ButtonOption("Review Recipients")]
|
||||
self.is_bottom_list = True
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
if self.input_amount > 1e6:
|
||||
denomination = "btc"
|
||||
denomination = _("btc")
|
||||
self.input_amount /= 1e8
|
||||
self.spend_amount /= 1e8
|
||||
self.change_amount /= 1e8
|
||||
@@ -482,7 +497,7 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
# lines up properly.
|
||||
self.fee_amount = f"{self.fee_amount:10}"
|
||||
else:
|
||||
denomination = "sats"
|
||||
denomination = _("sats")
|
||||
self.input_amount = f"{self.input_amount:,}"
|
||||
self.spend_amount = f"{self.spend_amount:,}"
|
||||
self.fee_amount = f"{self.fee_amount:,}"
|
||||
@@ -509,9 +524,9 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
image = Image.new("RGB", (body_width*ssf, body_height*ssf))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
body_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, (GUIConstants.BODY_FONT_SIZE)*ssf)
|
||||
fixed_width_font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, (GUIConstants.BODY_FONT_SIZE + 6)*ssf)
|
||||
left, top, right, bottom = fixed_width_font.getbbox(self.input_amount + "Q")
|
||||
body_font = Fonts.get_font(GUIConstants.get_body_font_name(), (GUIConstants.get_body_font_size())*ssf)
|
||||
fixed_width_font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, (GUIConstants.get_body_font_size() + 6)*ssf)
|
||||
left, top, right, bottom = fixed_width_font.getbbox(self.input_amount + "+")
|
||||
digits_width, digits_height = right - left, bottom - top
|
||||
|
||||
# Draw each line of the equation
|
||||
@@ -524,7 +539,7 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
# secondary_digit_color = GUIConstants.BODY_FONT_COLOR
|
||||
# tertiary_digit_color = GUIConstants.BODY_FONT_COLOR
|
||||
# digit_group_spacing = 0
|
||||
if denomination == 'btc':
|
||||
if denomination == _('btc'):
|
||||
display_str = amount_str
|
||||
main_zone = display_str[:-6]
|
||||
mid_zone = display_str[-6:-3]
|
||||
@@ -538,13 +553,12 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
draw.text((main_zone_width + digit_group_spacing + mid_zone_width + digit_group_spacing, cur_y), text=end_zone, font=fixed_width_font, fill=tertiary_digit_color)
|
||||
else:
|
||||
draw.text((0, cur_y), text=amount_str, font=fixed_width_font, fill=GUIConstants.BODY_FONT_COLOR)
|
||||
draw.text((digits_width + 2*digit_group_spacing, cur_y), text=info_text, font=body_font, fill=info_text_color)
|
||||
draw.text((digits_width + 3*digit_group_spacing, cur_y), text=info_text, font=body_font, fill=info_text_color)
|
||||
|
||||
render_amount(
|
||||
cur_y,
|
||||
f" {self.input_amount}",
|
||||
# info_text=f""" {self.num_inputs} input{"s" if self.num_inputs > 1 else ""}""",
|
||||
info_text=f""" input{"s" if self.num_inputs > 1 else ""}""",
|
||||
info_text=ngettext("input", "inputs", self.num_inputs),
|
||||
)
|
||||
|
||||
# spend_amount will be zero on self-transfers; only display when there's an
|
||||
@@ -554,15 +568,14 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
render_amount(
|
||||
cur_y,
|
||||
f"-{self.spend_amount}",
|
||||
# info_text=f""" {self.num_recipients} recipient{"s" if self.num_recipients > 1 else ""}""",
|
||||
info_text=f""" recipient{"s" if self.num_recipients > 1 else ""}""",
|
||||
info_text=ngettext("recipient", "recipients", self.num_recipients),
|
||||
)
|
||||
|
||||
cur_y += digits_height + GUIConstants.BODY_LINE_SPACING * ssf
|
||||
render_amount(
|
||||
cur_y,
|
||||
f"-{self.fee_amount}",
|
||||
info_text=f""" fee""",
|
||||
info_text=_("fee"),
|
||||
)
|
||||
|
||||
cur_y += digits_height + GUIConstants.BODY_LINE_SPACING * ssf
|
||||
@@ -572,12 +585,13 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
render_amount(
|
||||
cur_y,
|
||||
f" {self.change_amount}",
|
||||
info_text=f" {denomination} change",
|
||||
# TRANSLATOR_NOTE: Denonination is inserted (e.g. your "btc change" or "sats change")
|
||||
info_text=_("{} change").format(denomination),
|
||||
info_text_color="darkorange" # super-sampling alters the perceived color
|
||||
)
|
||||
|
||||
# Resize to target and sharpen final image
|
||||
image = image.resize((body_width, body_height), Image.LANCZOS)
|
||||
image = image.resize((body_width, body_height), Image.Resampling.LANCZOS)
|
||||
self.paste_images.append((image.filter(ImageFilter.SHARPEN), (GUIConstants.EDGE_PADDING, self.top_nav.height + GUIConstants.COMPONENT_PADDING)))
|
||||
|
||||
|
||||
@@ -635,7 +649,6 @@ class PSBTAddressDetailsScreen(ButtonListScreen):
|
||||
|
||||
@dataclass
|
||||
class PSBTChangeDetailsScreen(ButtonListScreen):
|
||||
title: str = "Your Change"
|
||||
amount: int = 0
|
||||
address: str = None
|
||||
is_multisig: bool = False
|
||||
@@ -662,10 +675,20 @@ class PSBTChangeDetailsScreen(ButtonListScreen):
|
||||
))
|
||||
|
||||
screen_y = self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING
|
||||
|
||||
change_type = _("Multisig") if self.is_multisig else self.fingerprint
|
||||
|
||||
if self.is_change_derivation_path :
|
||||
addr_type = _("Change")
|
||||
else:
|
||||
# TRANSLATOR_NOTE: Abbreviation for receive address
|
||||
addr_type = _("Addr")
|
||||
|
||||
value_text = "{}: {} #{}".format(change_type, addr_type, self.derivation_path_addr_index)
|
||||
self.components.append(IconTextLine(
|
||||
value_text=value_text,
|
||||
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,
|
||||
screen_y=screen_y,
|
||||
@@ -675,7 +698,7 @@ class PSBTChangeDetailsScreen(ButtonListScreen):
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.SUCCESS,
|
||||
icon_color=GUIConstants.SUCCESS_COLOR,
|
||||
value_text="Address verified!",
|
||||
value_text=_("Address verified!"),
|
||||
is_text_centered=False,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
|
||||
@@ -697,7 +720,7 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
# Simple case: display human-readable text
|
||||
self.components.append(TextArea(
|
||||
text=self.op_return_data.decode(errors="strict"), # "strict" is a good enough heuristic to decide if it's human readable
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
|
||||
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||
is_text_centered=True,
|
||||
allow_text_overflow=True,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
@@ -707,7 +730,7 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
except UnicodeDecodeError:
|
||||
# Contains data that can't be converted to UTF-8; probably encoded and not
|
||||
# meant to be human readable.
|
||||
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, size=GUIConstants.BODY_FONT_SIZE)
|
||||
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, size=GUIConstants.get_body_font_size())
|
||||
(left, top, right, bottom) = font.getbbox("X", anchor="ls")
|
||||
chars_per_line = int((self.canvas_width - 2*GUIConstants.EDGE_PADDING) / (right - left))
|
||||
decoded_str = self.op_return_data.hex()
|
||||
@@ -717,8 +740,10 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
text += (decoded_str[i*chars_per_line:(i+1)*chars_per_line]) + "\n"
|
||||
text = text[:-1]
|
||||
|
||||
# TRANSLATOR_NOTE: Shown when displaying OP_RETURN as non-human-readable hexadecimal data
|
||||
hex_label = _("raw hex data")
|
||||
label = TextArea(
|
||||
text="raw hex data",
|
||||
text=hex_label,
|
||||
font_color=GUIConstants.LABEL_FONT_COLOR,
|
||||
font_size=GUIConstants.LABEL_FONT_SIZE,
|
||||
screen_y=self.top_nav.height,
|
||||
@@ -728,7 +753,7 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
self.components.append(TextArea(
|
||||
text=text,
|
||||
font_name=GUIConstants.FIXED_WIDTH_FONT_NAME,
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_size=GUIConstants.get_body_font_size(),
|
||||
screen_y=label.screen_y + label.height + GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
|
||||
@@ -738,7 +763,7 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
class PSBTFinalizeScreen(ButtonListScreen):
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = "Sign PSBT"
|
||||
self.title = _("Sign PSBT")
|
||||
self.is_bottom_list = True
|
||||
super().__post_init__()
|
||||
|
||||
@@ -752,6 +777,6 @@ class PSBTFinalizeScreen(ButtonListScreen):
|
||||
self.components.append(icon)
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="Click to authorize this transaction",
|
||||
screen_y=icon.screen_y + icon.height + GUIConstants.COMPONENT_PADDING
|
||||
text=_("Click to approve this transaction"),
|
||||
screen_y=icon.screen_y + icon.height + 2*GUIConstants.COMPONENT_PADDING
|
||||
))
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from seedsigner.gui import renderer
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
||||
from seedsigner.gui.components import GUIConstants, Fonts
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
|
||||
from .screen import BaseScreen
|
||||
from ..components import GUIConstants, Fonts, SeedSignerIconConstants
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +55,8 @@ class ScanScreen(BaseScreen):
|
||||
# Initialize the base class
|
||||
super().__post_init__()
|
||||
|
||||
self.instructions_text = "< back | " + self.instructions_text
|
||||
# TODO: Arrange this with UI elements rather than text
|
||||
self.instructions_text = "< " + _("back") + " | " + _(self.instructions_text)
|
||||
|
||||
self.camera = Camera.get_instance()
|
||||
self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb")
|
||||
@@ -65,7 +65,6 @@ class ScanScreen(BaseScreen):
|
||||
self.frames_decoded_counter = ThreadsafeCounter()
|
||||
|
||||
self.threads.append(ScanScreen.LivePreviewThread(
|
||||
camera=self.camera,
|
||||
decoder=self.decoder,
|
||||
renderer=self.renderer,
|
||||
instructions_text=self.instructions_text,
|
||||
@@ -76,8 +75,10 @@ class ScanScreen(BaseScreen):
|
||||
|
||||
|
||||
class LivePreviewThread(BaseThread):
|
||||
def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int], frame_decode_status: ThreadsafeCounter, frames_decoded_counter: ThreadsafeCounter):
|
||||
self.camera = camera
|
||||
def __init__(self, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int], frame_decode_status: ThreadsafeCounter, frames_decoded_counter: ThreadsafeCounter):
|
||||
from seedsigner.hardware.camera import Camera
|
||||
|
||||
self.camera = Camera.get_instance()
|
||||
self.decoder = decoder
|
||||
self.renderer = renderer
|
||||
self.instructions_text = instructions_text
|
||||
@@ -96,10 +97,12 @@ class ScanScreen(BaseScreen):
|
||||
|
||||
|
||||
def run(self):
|
||||
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
from timeit import default_timer as timer
|
||||
|
||||
instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size())
|
||||
|
||||
# pre-calculate how big the animated QR percent display can be
|
||||
left, _, right, _ = instructions_font.getbbox("100%")
|
||||
left, top, right, bottom = instructions_font.getbbox("100%")
|
||||
progress_text_width = right - left
|
||||
|
||||
start_time = time.time()
|
||||
@@ -201,11 +204,12 @@ class ScanScreen(BaseScreen):
|
||||
radius=8
|
||||
)
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts the percentage value of the animated QR scan progress
|
||||
text = _("{}%").format(progress_percentage)
|
||||
|
||||
draw.text(
|
||||
xy=(rectangle.width - GUIConstants.EDGE_PADDING, int(rectangle.height / 2)),
|
||||
text=f"{progress_percentage}%",
|
||||
# text=f"100%",
|
||||
text=text,
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
anchor="rm", # right-justified, middle
|
||||
@@ -248,6 +252,9 @@ class ScanScreen(BaseScreen):
|
||||
Screen. Once interaction starts, the display updates have to be managed in
|
||||
_run(). The live preview is an extra-complex case.
|
||||
"""
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.models.decode_qr import DecodeQRStatus
|
||||
|
||||
num_frames = 0
|
||||
start_time = time.time()
|
||||
while True:
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from gettext import gettext as _
|
||||
from PIL import Image, ImageDraw, ImageColor
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.gui.components import (GUIConstants,
|
||||
BaseComponent, Button, Icon, IconButton, LargeIconButton,
|
||||
SeedSignerIconConstants, TopNav, TextArea, load_image)
|
||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
|
||||
from seedsigner.models.encode_qr import BaseQrEncoder
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
@@ -43,6 +44,13 @@ class BaseScreen(BaseComponent):
|
||||
|
||||
# Tracks position on scrollable pages, determines which elements are visible.
|
||||
self.scroll_y = 0
|
||||
|
||||
|
||||
def get_threads(self) -> List[BaseThread]:
|
||||
threads = self.threads.copy()
|
||||
for component in self.components:
|
||||
threads += component.threads
|
||||
return threads
|
||||
|
||||
|
||||
def display(self) -> Any:
|
||||
@@ -51,15 +59,16 @@ class BaseScreen(BaseComponent):
|
||||
self._render()
|
||||
self.renderer.show_image()
|
||||
|
||||
for t in self.threads:
|
||||
t.start()
|
||||
for t in self.get_threads():
|
||||
if not t.is_alive():
|
||||
t.start()
|
||||
|
||||
return self._run()
|
||||
except Exception as e:
|
||||
repr(e)
|
||||
raise e
|
||||
finally:
|
||||
for t in self.threads:
|
||||
for t in self.get_threads():
|
||||
t.stop()
|
||||
|
||||
|
||||
@@ -99,19 +108,17 @@ class BaseScreen(BaseComponent):
|
||||
|
||||
For example: A basic menu screen where the user can key up and down. The
|
||||
Screen can handle the UI updates to light up the currently selected menu item
|
||||
on its own. Only when the user clicks to make a selection would run() exit
|
||||
and returns the selected option.
|
||||
on its own. Only when the user clicks to make a selection would _run() exit
|
||||
and return the selected option.
|
||||
|
||||
But an alternate use case returns immediately after each user input so the
|
||||
View can update its controlling logic accordingly (e.g. as the user joysticks
|
||||
over different letters in the keyboard UI, we need to make matching changes
|
||||
to the list of mnemonic seed words that match the new letter).
|
||||
In general, _run() will be implemented as a continuous loop waiting for user
|
||||
input and redrawing the screen as needed. When it redraws, it must claim
|
||||
the `Renderer.lock` to ensure that its updates don't conflict with any other
|
||||
threads that might be updating the screen at the same time (e.g. flashing
|
||||
warning edges, auto-scrolling long titles or buttons, etc).
|
||||
|
||||
In this case, it would be called repeatedly in a loop:
|
||||
* run() and wait for it to handle user input
|
||||
* run() exits and returns the user input (e.g. KEY_UP)
|
||||
* View updates its state of the world accordingly
|
||||
* loop and call run() again
|
||||
Just note that this loop cannot hold the lock indefinitely! Each iteration
|
||||
through the loop should claim the lock, render, and then release it.
|
||||
"""
|
||||
raise Exception("Must implement in a child class")
|
||||
|
||||
@@ -124,6 +131,7 @@ class LoadingScreenThread(BaseThread):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
renderer: Renderer = Renderer.get_instance()
|
||||
|
||||
center_image = load_image("btc_logo_60x60.png")
|
||||
@@ -147,7 +155,7 @@ class LoadingScreenThread(BaseThread):
|
||||
if self.text:
|
||||
TextArea(
|
||||
text=self.text,
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
|
||||
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||
screen_y=int((renderer.canvas_height - bounding_box[3])/2),
|
||||
).render()
|
||||
|
||||
@@ -190,7 +198,7 @@ class BaseTopNavScreen(BaseScreen):
|
||||
top_nav_icon_name: str = None
|
||||
top_nav_icon_color: str = None
|
||||
title: str = "Screen Title"
|
||||
title_font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE
|
||||
title_font_size: int = GUIConstants.get_top_nav_title_font_size()
|
||||
show_back_button: bool = True
|
||||
show_power_button: bool = False
|
||||
|
||||
@@ -199,7 +207,7 @@ class BaseTopNavScreen(BaseScreen):
|
||||
self.top_nav = TopNav(
|
||||
icon_name=self.top_nav_icon_name,
|
||||
icon_color=self.top_nav_icon_color,
|
||||
text=self.title,
|
||||
text=_(self.title), # Wrap here for just-in-time translations
|
||||
font_size=self.title_font_size,
|
||||
width=self.canvas_width,
|
||||
height=GUIConstants.TOP_NAV_HEIGHT,
|
||||
@@ -251,14 +259,29 @@ class BaseTopNavScreen(BaseScreen):
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ButtonOption:
|
||||
"""
|
||||
Note: The babel config in setup.cfg will extract the `button_label` string for translation
|
||||
"""
|
||||
button_label: str
|
||||
icon_name: str = None
|
||||
icon_color: str = None
|
||||
right_icon_name: str = None
|
||||
button_label_color: str = None
|
||||
return_data: Any = None
|
||||
active_button_label: str = None # Changes displayed button label when button is active
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ButtonListScreen(BaseTopNavScreen):
|
||||
button_data: list = None # list can be a mix of str or tuple(label: str, icon_name: str)
|
||||
button_data: list[ButtonOption] = None
|
||||
selected_button: int = 0
|
||||
is_button_text_centered: bool = True
|
||||
is_bottom_list: bool = False
|
||||
button_font_name: str = GUIConstants.BUTTON_FONT_NAME
|
||||
button_font_size: int = GUIConstants.BUTTON_FONT_SIZE
|
||||
button_font_name: str = None
|
||||
button_font_size: int = None
|
||||
button_selected_color: str = GUIConstants.ACCENT_COLOR
|
||||
|
||||
# Params for version of list used for Settings
|
||||
@@ -270,7 +293,12 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.button_font_name:
|
||||
self.button_font_name = GUIConstants.get_button_font_name()
|
||||
if not self.button_font_size:
|
||||
self.button_font_size = GUIConstants.get_button_font_size()
|
||||
super().__post_init__()
|
||||
|
||||
button_height = GUIConstants.BUTTON_HEIGHT
|
||||
if len(self.button_data) == 1:
|
||||
button_list_height = button_height
|
||||
@@ -289,29 +317,26 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
self.has_scroll_arrows = True
|
||||
|
||||
self.buttons: List[Button] = []
|
||||
for i, button_label in enumerate(self.button_data):
|
||||
for i, button_option in enumerate(self.button_data):
|
||||
icon_name = None
|
||||
icon_color = None
|
||||
right_icon_name = None
|
||||
button_label_color = None
|
||||
|
||||
# TODO: Define an actual class for button_data?
|
||||
if type(button_label) == tuple:
|
||||
if len(button_label) == 2:
|
||||
(button_label, icon_name) = button_label
|
||||
icon_color = GUIConstants.BUTTON_FONT_COLOR
|
||||
|
||||
elif len(button_label) == 3:
|
||||
(button_label, icon_name, icon_color) = button_label
|
||||
|
||||
elif len(button_label) == 4:
|
||||
(button_label, icon_name, icon_color, button_label_color) = button_label
|
||||
|
||||
elif len(button_label) == 5:
|
||||
(button_label, icon_name, icon_color, button_label_color, right_icon_name) = button_label
|
||||
if type(button_option) == ButtonOption:
|
||||
button_label = button_option.button_label
|
||||
icon_name = button_option.icon_name
|
||||
icon_color = button_option.icon_color
|
||||
right_icon_name = button_option.right_icon_name
|
||||
button_label_color = button_option.button_label_color
|
||||
active_button_label = button_option.active_button_label
|
||||
|
||||
else:
|
||||
raise Exception("Refactor needed!")
|
||||
|
||||
button_kwargs = dict(
|
||||
text=button_label,
|
||||
text=_(button_label), # Wrap here for just-in-time translations
|
||||
active_text=_(active_button_label), # Wrap here for just-in-time translations
|
||||
icon_name=icon_name,
|
||||
icon_color=icon_color if icon_color else GUIConstants.BUTTON_FONT_COLOR,
|
||||
is_icon_inline=True,
|
||||
@@ -325,13 +350,14 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
font_name=self.button_font_name,
|
||||
font_size=self.button_font_size,
|
||||
font_color=button_label_color if button_label_color else GUIConstants.BUTTON_FONT_COLOR,
|
||||
selected_color=self.button_selected_color
|
||||
selected_color=self.button_selected_color,
|
||||
is_scrollable_text=True, # We need to use the ScrollableText class for long button labels
|
||||
)
|
||||
if self.checked_buttons and i in self.checked_buttons:
|
||||
button_kwargs["is_checked"] = True
|
||||
button = self.Button_cls(**button_kwargs)
|
||||
self.buttons.append(button)
|
||||
|
||||
|
||||
if self.has_scroll_arrows:
|
||||
self.arrow_half_width = 10
|
||||
self.cur_scroll_y = self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0
|
||||
@@ -352,10 +378,21 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
cur_selected_button.is_selected = True
|
||||
|
||||
|
||||
def get_threads(self) -> List[BaseThread]:
|
||||
threads = super().get_threads()
|
||||
for button in self.buttons:
|
||||
if button.is_scrollable_text:
|
||||
threads += button.threads
|
||||
return threads
|
||||
|
||||
|
||||
def _render(self):
|
||||
super()._render()
|
||||
self._render_visible_buttons()
|
||||
|
||||
# Write the screen updates
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
def _render_visible_buttons(self):
|
||||
if self.has_scroll_arrows:
|
||||
@@ -510,13 +547,18 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
|
||||
@dataclass
|
||||
class LargeButtonScreen(BaseTopNavScreen):
|
||||
button_data: list = None # list can be a mix of str or tuple(label: str, icon_name: str)
|
||||
button_font_name: str = GUIConstants.BUTTON_FONT_NAME
|
||||
button_font_size: int = 20
|
||||
button_data: list = None
|
||||
button_font_name: str = None
|
||||
button_font_size: int = None
|
||||
button_selected_color: str = GUIConstants.ACCENT_COLOR
|
||||
selected_button: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.button_font_name:
|
||||
self.button_font_name = GUIConstants.get_button_font_name()
|
||||
if not self.button_font_size:
|
||||
self.button_font_size = GUIConstants.get_button_font_size() + 2
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
if len(self.button_data) not in [2, 4]:
|
||||
@@ -533,11 +575,20 @@ class LargeButtonScreen(BaseTopNavScreen):
|
||||
button_start_y = self.top_nav.height + int((self.canvas_height - (self.top_nav.height + GUIConstants.COMPONENT_PADDING) - (2 * button_height) - GUIConstants.COMPONENT_PADDING) / 2)
|
||||
|
||||
self.buttons = []
|
||||
for i, button_label in enumerate(self.button_data):
|
||||
if type(button_label) == tuple:
|
||||
(button_label, icon_name) = button_label
|
||||
for i, button_option in enumerate(self.button_data):
|
||||
if type(button_option) == ButtonOption:
|
||||
button_label = button_option.button_label
|
||||
icon_name = button_option.icon_name
|
||||
else:
|
||||
icon_name = None
|
||||
raise Exception("Refactor needed!")
|
||||
|
||||
# elif type(button_option) == str:
|
||||
# button_label = button_option
|
||||
# icon_name = None
|
||||
# elif type(button_option) == tuple:
|
||||
# (button_label, icon_name) = button_option
|
||||
# else:
|
||||
# print(type(button_option))
|
||||
|
||||
if i % 2 == 0:
|
||||
button_start_x = GUIConstants.EDGE_PADDING
|
||||
@@ -545,7 +596,7 @@ class LargeButtonScreen(BaseTopNavScreen):
|
||||
button_start_x = GUIConstants.EDGE_PADDING + button_width + GUIConstants.COMPONENT_PADDING
|
||||
|
||||
button_args = {
|
||||
"text": button_label,
|
||||
"text": _(button_label), # Wrap here for just-in-time translations
|
||||
"screen_x": button_start_x,
|
||||
"screen_y": button_start_y,
|
||||
"width": button_width,
|
||||
@@ -661,12 +712,12 @@ class QRDisplayScreen(BaseScreen):
|
||||
qr_encoder: BaseQrEncoder = None
|
||||
|
||||
class QRDisplayThread(BaseThread):
|
||||
def __init__(self, qr_encoder: BaseQrEncoder, qr_brightness: ThreadsafeCounter, renderer: Renderer,
|
||||
tips_start_time: ThreadsafeCounter):
|
||||
def __init__(self, qr_encoder: BaseQrEncoder, qr_brightness: ThreadsafeCounter, tips_start_time: ThreadsafeCounter):
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
super().__init__()
|
||||
self.qr_encoder = qr_encoder
|
||||
self.qr_brightness = qr_brightness
|
||||
self.renderer = renderer
|
||||
self.renderer = Renderer.get_instance()
|
||||
self.tips_start_time = tips_start_time
|
||||
|
||||
|
||||
@@ -676,7 +727,7 @@ class QRDisplayScreen(BaseScreen):
|
||||
|
||||
# Instantiate a temp Image and ImageDraw object to draw on
|
||||
rectangle_width = image.width
|
||||
rectangle_height = GUIConstants.COMPONENT_PADDING * 2 + GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING
|
||||
rectangle_height = GUIConstants.COMPONENT_PADDING * 2 + GUIConstants.get_body_font_size() * 2 + GUIConstants.BODY_LINE_SPACING
|
||||
rectangle = Image.new('RGBA', (rectangle_width, rectangle_height), (0, 0, 0, 0))
|
||||
img_draw = ImageDraw.Draw(rectangle)
|
||||
|
||||
@@ -691,7 +742,7 @@ class QRDisplayScreen(BaseScreen):
|
||||
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=SeedSignerIconConstants.CHEVRON_UP,
|
||||
icon_size=GUIConstants.BODY_FONT_SIZE,
|
||||
icon_size=GUIConstants.get_body_font_size(),
|
||||
)
|
||||
chevron_up_icon.render()
|
||||
|
||||
@@ -705,12 +756,14 @@ class QRDisplayScreen(BaseScreen):
|
||||
)
|
||||
chevron_down_icon.render()
|
||||
|
||||
# TRANSLATOR_NOTE: Increase QR code screen brightness
|
||||
text = _("Brighter")
|
||||
TextArea(
|
||||
image_draw=img_draw,
|
||||
canvas=rectangle,
|
||||
text="Brighter",
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_name=GUIConstants.BUTTON_FONT_NAME,
|
||||
text=text,
|
||||
font_size=GUIConstants.get_body_font_size(),
|
||||
font_name=GUIConstants.get_button_font_name(),
|
||||
background_color=(0, 0, 0, overlay_opacity),
|
||||
edge_padding=0,
|
||||
is_text_centered=False,
|
||||
@@ -721,12 +774,14 @@ class QRDisplayScreen(BaseScreen):
|
||||
allow_text_overflow=False
|
||||
).render()
|
||||
|
||||
# TRANSLATOR_NOTE: Decrease QR code screen brightness
|
||||
text = _("Darker")
|
||||
TextArea(
|
||||
image_draw=img_draw,
|
||||
canvas=rectangle,
|
||||
text="Darker",
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_name=GUIConstants.BUTTON_FONT_NAME,
|
||||
text=text,
|
||||
font_size=GUIConstants.get_body_font_size(),
|
||||
font_name=GUIConstants.get_button_font_name(),
|
||||
background_color=(0, 0, 0, overlay_opacity),
|
||||
edge_padding=0,
|
||||
is_text_centered=False,
|
||||
@@ -789,7 +844,6 @@ class QRDisplayScreen(BaseScreen):
|
||||
self.threads.append(QRDisplayScreen.QRDisplayThread(
|
||||
qr_encoder=self.qr_encoder,
|
||||
qr_brightness=self.qr_brightness,
|
||||
renderer=self.renderer,
|
||||
tips_start_time=self.tips_start_time
|
||||
))
|
||||
|
||||
@@ -831,11 +885,11 @@ class QRDisplayScreen(BaseScreen):
|
||||
|
||||
@dataclass
|
||||
class LargeIconStatusScreen(ButtonListScreen):
|
||||
title: str = "Success!"
|
||||
title: str = _mft("Success!")
|
||||
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
|
||||
status_headline: str = None
|
||||
text: str = "" # The body text of the screen
|
||||
text_edge_padding: int = GUIConstants.EDGE_PADDING
|
||||
button_data: list = None
|
||||
@@ -843,9 +897,9 @@ class LargeIconStatusScreen(ButtonListScreen):
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
self.is_bottom_list: bool = True
|
||||
if not self.button_data:
|
||||
self.button_data = ["OK"]
|
||||
self.button_data = [ButtonOption("OK")]
|
||||
self.is_bottom_list = True
|
||||
super().__post_init__()
|
||||
|
||||
self.status_icon = Icon(
|
||||
@@ -860,22 +914,21 @@ class LargeIconStatusScreen(ButtonListScreen):
|
||||
next_y = self.status_icon.screen_y + self.status_icon.height + int(GUIConstants.COMPONENT_PADDING/2)
|
||||
if self.status_headline:
|
||||
self.warning_headline_textarea = TextArea(
|
||||
text=self.status_headline,
|
||||
text=_(self.status_headline), # Wrap here for just-in-time translations
|
||||
width=self.canvas_width,
|
||||
screen_y=next_y,
|
||||
font_color=self.status_color,
|
||||
allow_text_overflow=self.allow_text_overflow,
|
||||
auto_line_break=False, # Force headline to be on one line
|
||||
)
|
||||
self.components.append(self.warning_headline_textarea)
|
||||
next_y = next_y + self.warning_headline_textarea.height
|
||||
|
||||
self.components.append(TextArea(
|
||||
height=self.buttons[0].screen_y - next_y,
|
||||
text=self.text,
|
||||
text=_(self.text),
|
||||
width=self.canvas_width,
|
||||
edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges
|
||||
screen_y=next_y,
|
||||
allow_text_overflow=self.allow_text_overflow,
|
||||
))
|
||||
|
||||
|
||||
@@ -958,22 +1011,17 @@ class WarningEdgesMixin:
|
||||
|
||||
@dataclass
|
||||
class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen):
|
||||
title: str = "Caution"
|
||||
title: str = _mft("Caution")
|
||||
status_icon_name: str = SeedSignerIconConstants.WARNING
|
||||
status_color: str = "yellow"
|
||||
status_headline: str = "Privacy Leak!" # The colored text under the alert icon
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.button_data:
|
||||
self.button_data = ["I Understand"]
|
||||
|
||||
super().__post_init__()
|
||||
status_headline: str = _mft("Privacy Leak!") # The colored text under the alert icon
|
||||
button_data: list = field(default_factory=lambda: [ButtonOption("I Understand")])
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class DireWarningScreen(WarningScreen):
|
||||
status_headline: str = "Classified Info!" # The colored text under the alert icon
|
||||
status_headline: str = _mft("Classified Info!") # The colored text under the alert icon
|
||||
status_color: str = GUIConstants.DIRE_WARNING_COLOR
|
||||
|
||||
|
||||
@@ -981,12 +1029,12 @@ class DireWarningScreen(WarningScreen):
|
||||
@dataclass
|
||||
class ResetScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "Restarting"
|
||||
self.title = _("Restarting")
|
||||
self.show_back_button = False
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="SeedSigner is restarting.\n\nAll in-memory data will be wiped.",
|
||||
text=_("SeedSigner is restarting.\n\nAll in-memory data will be wiped."),
|
||||
screen_y=self.top_nav.height,
|
||||
height=self.canvas_height - self.top_nav.height,
|
||||
))
|
||||
@@ -996,12 +1044,12 @@ class ResetScreen(BaseTopNavScreen):
|
||||
@dataclass
|
||||
class PowerOffScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "Powering Off"
|
||||
self.title = _("Powering Off")
|
||||
self.show_back_button = False
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="Please wait about 30 seconds before disconnecting power.",
|
||||
text=_("Please wait about 30 seconds before disconnecting power."),
|
||||
screen_y=self.top_nav.height,
|
||||
height=self.canvas_height - self.top_nav.height,
|
||||
))
|
||||
@@ -1011,12 +1059,12 @@ class PowerOffScreen(BaseTopNavScreen):
|
||||
@dataclass
|
||||
class PowerOffNotRequiredScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "Just Unplug It"
|
||||
self.title = _("Just Unplug It")
|
||||
self.show_back_button = True
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="It is safe to disconnect power at any time.",
|
||||
text=_("It is safe to disconnect power at any time."),
|
||||
screen_y=self.top_nav.height,
|
||||
height=self.canvas_height - self.top_nav.height,
|
||||
))
|
||||
@@ -1044,7 +1092,7 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
rows: int = None
|
||||
cols: int = None
|
||||
keyboard_font_name: str = GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME
|
||||
keyboard_font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2
|
||||
keyboard_font_size: int = None
|
||||
key_height: int = None
|
||||
keys_charset: str = None
|
||||
keys_to_values: dict = None
|
||||
@@ -1053,6 +1101,9 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
initial_value: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if self.keyboard_font_size is None:
|
||||
self.keyboard_font_size = GUIConstants.get_top_nav_title_font_size() + 2
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
if self.initial_value:
|
||||
@@ -1132,7 +1183,7 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
self.text_entry_display.render()
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
|
||||
def _run(self):
|
||||
self.cursor_position = len(self.user_input)
|
||||
@@ -1144,84 +1195,85 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
check_release=True,
|
||||
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
|
||||
)
|
||||
|
||||
# Check possible exit conditions
|
||||
if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
|
||||
return RET_CODE__BACK_BUTTON
|
||||
|
||||
elif self.show_save_button and input == HardwareButtonsConstants.KEY3:
|
||||
# Save!
|
||||
if len(self.user_input) == 0:
|
||||
# Don't try to submit zero input
|
||||
|
||||
with self.renderer.lock:
|
||||
# Check possible exit conditions
|
||||
if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
|
||||
return RET_CODE__BACK_BUTTON
|
||||
|
||||
elif self.show_save_button and input == HardwareButtonsConstants.KEY3:
|
||||
# Save!
|
||||
if len(self.user_input) == 0:
|
||||
# Don't try to submit zero input
|
||||
continue
|
||||
|
||||
# First show the save button reacting to the click
|
||||
self.save_button.is_selected = True
|
||||
self.save_button.render()
|
||||
self.renderer.show_image()
|
||||
|
||||
# Then return the input to the View
|
||||
return self.user_input.strip()
|
||||
|
||||
# Process normal input
|
||||
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
|
||||
# We're navigating off the previous button
|
||||
self.top_nav.is_selected = False
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
# Override the actual input w/an ENTER signal for the Keyboard
|
||||
if input == HardwareButtonsConstants.KEY_DOWN:
|
||||
input = Keyboard.ENTER_TOP
|
||||
else:
|
||||
input = Keyboard.ENTER_BOTTOM
|
||||
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
|
||||
# ignore
|
||||
continue
|
||||
|
||||
# First show the save button reacting to the click
|
||||
self.save_button.is_selected = True
|
||||
self.save_button.render()
|
||||
self.renderer.show_image()
|
||||
ret_val = self.keyboard.update_from_input(input)
|
||||
|
||||
# Then return the input to the View
|
||||
return self.user_input.strip()
|
||||
|
||||
# Process normal input
|
||||
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
|
||||
# We're navigating off the previous button
|
||||
self.top_nav.is_selected = False
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
# Override the actual input w/an ENTER signal for the Keyboard
|
||||
if input == HardwareButtonsConstants.KEY_DOWN:
|
||||
input = Keyboard.ENTER_TOP
|
||||
else:
|
||||
input = Keyboard.ENTER_BOTTOM
|
||||
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
|
||||
# ignore
|
||||
continue
|
||||
|
||||
ret_val = self.keyboard.update_from_input(input)
|
||||
|
||||
# Now process the result from the keyboard
|
||||
if ret_val in Keyboard.EXIT_DIRECTIONS:
|
||||
self.top_nav.is_selected = True
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
|
||||
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
|
||||
if len(self.user_input) > 0:
|
||||
self.user_input = self.user_input[:-1]
|
||||
self.cursor_position -= 1
|
||||
|
||||
elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
|
||||
# User has locked in the current letter
|
||||
if self.keys_to_values:
|
||||
# Map the Key display char to its output value (e.g. dice icon to digit)
|
||||
ret_val = self.keys_to_values[ret_val]
|
||||
self.user_input += ret_val
|
||||
self.cursor_position += 1
|
||||
|
||||
if self.cursor_position == self.return_after_n_chars:
|
||||
return self.user_input
|
||||
|
||||
# Render a new TextArea over the TopNav title bar
|
||||
if self.update_title():
|
||||
TextArea(
|
||||
text=self.title,
|
||||
font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME,
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
|
||||
height=self.top_nav.height,
|
||||
).render()
|
||||
# Now process the result from the keyboard
|
||||
if ret_val in Keyboard.EXIT_DIRECTIONS:
|
||||
self.top_nav.is_selected = True
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
|
||||
# Live joystick movement; haven't locked this new letter in yet.
|
||||
# Leave current spot blank for now. Only update the active keyboard keys
|
||||
# when a selection has been locked in (KEY_PRESS) or removed ("del").
|
||||
pass
|
||||
|
||||
# Render the text entry display and cursor block
|
||||
self.text_entry_display.render(self.user_input)
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
|
||||
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
|
||||
if len(self.user_input) > 0:
|
||||
self.user_input = self.user_input[:-1]
|
||||
self.cursor_position -= 1
|
||||
|
||||
elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
|
||||
# User has locked in the current letter
|
||||
if self.keys_to_values:
|
||||
# Map the Key display char to its output value (e.g. dice icon to digit)
|
||||
ret_val = self.keys_to_values[ret_val]
|
||||
self.user_input += ret_val
|
||||
self.cursor_position += 1
|
||||
|
||||
if self.cursor_position == self.return_after_n_chars:
|
||||
return self.user_input
|
||||
|
||||
# Render a new TextArea over the TopNav title bar
|
||||
if self.update_title():
|
||||
TextArea(
|
||||
text=self.title,
|
||||
font_name=GUIConstants.get_top_nav_title_font_name(),
|
||||
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||
height=self.top_nav.height,
|
||||
).render()
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
|
||||
# Live joystick movement; haven't locked this new letter in yet.
|
||||
# Leave current spot blank for now. Only update the active keyboard keys
|
||||
# when a selection has been locked in (KEY_PRESS) or removed ("del").
|
||||
pass
|
||||
|
||||
# Render the text entry display and cursor block
|
||||
self.text_entry_display.render(self.user_input)
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
def update_title(self) -> bool:
|
||||
@@ -1229,7 +1281,7 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
Optionally update the self.title after each completed key input.
|
||||
|
||||
e.g. to increment the dice roll count:
|
||||
self.title = f"Roll {self.cursor_position + 1}"
|
||||
self.title = _("Roll {}".format(self.cursor_position + 1))
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,20 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from PIL.ImageOps import autocontrast
|
||||
from typing import List
|
||||
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, Fonts, GUIConstants, Icon, IconButton, IconTextLine, TextArea
|
||||
from seedsigner.gui.screens.scan_screens import ScanScreen
|
||||
|
||||
from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen
|
||||
from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
|
||||
display_name: str = None
|
||||
@@ -21,7 +24,7 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
|
||||
selected_button: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
self.title = "Settings"
|
||||
self.title = _("Settings")
|
||||
self.is_bottom_list = True
|
||||
self.use_checked_selection_buttons = True
|
||||
if self.settings_entry_type == SettingsConstants.TYPE__MULTISELECT:
|
||||
@@ -31,20 +34,21 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=self.display_name,
|
||||
text=_(self.display_name),
|
||||
font_size=GUIConstants.BODY_FONT_MAX_SIZE,
|
||||
is_text_centered=True,
|
||||
auto_line_break=False,
|
||||
auto_line_break=True,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING
|
||||
))
|
||||
|
||||
if self.help_text:
|
||||
prev_component_bottom = self.components[-1].screen_y + self.components[-1].height
|
||||
self.components.append(TextArea(
|
||||
text=self.help_text,
|
||||
text=_(self.help_text),
|
||||
font_color=GUIConstants.LABEL_FONT_COLOR,
|
||||
is_text_centered=True,
|
||||
screen_y=prev_component_bottom + GUIConstants.COMPONENT_PADDING,
|
||||
auto_line_break=True,
|
||||
))
|
||||
|
||||
|
||||
@@ -52,7 +56,8 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
|
||||
@dataclass
|
||||
class IOTestScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "I/O Test"
|
||||
# TRANSLATOR_NOTE: Short for "Input/Output"; screen to make sure the buttons and camera are working properly
|
||||
self.title = _("I/O Test")
|
||||
self.show_back_button = False
|
||||
self.resolution = (96, 96)
|
||||
self.framerate = 10
|
||||
@@ -122,8 +127,8 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
self.components.append(self.joystick_right_button)
|
||||
|
||||
# Hardware keys UI
|
||||
font = Fonts.get_font(GUIConstants.BUTTON_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
(left, top, text_width, bottom) = font.getbbox(text="Clear", anchor="ls")
|
||||
font = Fonts.get_font(GUIConstants.get_button_font_name(), GUIConstants.get_button_font_size())
|
||||
(left, top, text_width, bottom) = font.getbbox(text=_("Clear"), anchor="ls")
|
||||
icon = Icon(
|
||||
icon_name=FontAwesomeIconConstants.CAMERA,
|
||||
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
|
||||
@@ -133,12 +138,14 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
key2_y = int(self.canvas_height/2) - int(key_button_height/2)
|
||||
|
||||
self.key2_button = Button(
|
||||
text="Clear", # Initialize with text to set vertical centering
|
||||
# TRANSLATOR_NOTE: Blank the screen
|
||||
text=_("Clear"), # Initialize with text to set vertical centering
|
||||
width=key_button_width,
|
||||
height=key_button_height,
|
||||
screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING,
|
||||
screen_y=key2_y,
|
||||
outline_color=GUIConstants.ACCENT_COLOR,
|
||||
is_scrollable_text=False, # Text has to dynamically update, can't use scrollable Button
|
||||
)
|
||||
self.key2_button.text = " " # but default state is empty
|
||||
self.components.append(self.key2_button)
|
||||
@@ -154,12 +161,13 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
self.components.append(self.key1_button)
|
||||
|
||||
self.key3_button = Button(
|
||||
text="Exit",
|
||||
text=_("Exit"),
|
||||
width=key_button_width,
|
||||
height=key_button_height,
|
||||
screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING,
|
||||
screen_y=key2_y + 3*GUIConstants.COMPONENT_PADDING + key_button_height,
|
||||
outline_color=GUIConstants.ACCENT_COLOR,
|
||||
is_scrollable_text=False, # No help for l10n, but currently ScrollableTextLine interferes with the small button's left edge. (TODO:)
|
||||
)
|
||||
self.components.append(self.key3_button)
|
||||
|
||||
@@ -168,8 +176,8 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
cur_selected_button = self.key1_button
|
||||
msg_height = GUIConstants.ICON_LARGE_BUTTON_SIZE + 2*GUIConstants.COMPONENT_PADDING
|
||||
camera_message = TextArea(
|
||||
text="Capturing image...",
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
|
||||
text=_("Capturing image..."),
|
||||
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||
is_text_centered=True,
|
||||
height=msg_height,
|
||||
screen_y=int((self.canvas_height - msg_height)/ 2),
|
||||
@@ -178,21 +186,25 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS, check_release=False)
|
||||
|
||||
if input == HardwareButtonsConstants.KEY1:
|
||||
# Note that there are three distinct screen updates that happen at
|
||||
# different times, therefore we claim the `Renderer.lock` three separate
|
||||
# times.
|
||||
cur_selected_button = self.key1_button
|
||||
|
||||
with self.renderer.lock:
|
||||
cur_selected_button.is_selected = True
|
||||
cur_selected_button.render()
|
||||
camera_message.render()
|
||||
# Render edges around message box
|
||||
self.image_draw.rectangle(
|
||||
(
|
||||
-1, int((self.canvas_height - msg_height)/ 2) - 1,
|
||||
self.canvas_width + 1, int((self.canvas_height + msg_height)/ 2) + 1
|
||||
),
|
||||
fill="black",
|
||||
outline=GUIConstants.ACCENT_COLOR,
|
||||
width=1,
|
||||
)
|
||||
cur_selected_button.is_selected = True
|
||||
cur_selected_button.render()
|
||||
camera_message.render()
|
||||
self.renderer.show_image()
|
||||
|
||||
# Snap a pic, render it as the background, re-render all onscreen elements
|
||||
@@ -214,7 +226,7 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
)
|
||||
with self.renderer.lock:
|
||||
self.canvas.paste(display_version, (0, self.top_nav.height))
|
||||
self.key2_button.text = "Clear"
|
||||
self.key2_button.text = _("Clear")
|
||||
for component in self.components:
|
||||
component.render()
|
||||
self.renderer.show_image()
|
||||
@@ -281,17 +293,19 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
@dataclass
|
||||
class DonateScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "Donate"
|
||||
self.title = _("Donate")
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="SeedSigner is 100% free & open source, funded solely by the Bitcoin community.\n\nDonate onchain or LN at:",
|
||||
# TRANSLATOR_NOTE: If your language uses the percent sign ("%"), your translation must also use two percent signs ("%%") due to python formatting oddities. "100%%" will be rendered as "100%".
|
||||
text=_("SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n\nDonate onchain or LN at:").replace("%%", "%"),
|
||||
screen_y=self.top_nav.height + 3*GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="seedsigner.com",
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 8,
|
||||
font_name=GUIConstants.get_body_font_name(),
|
||||
font_size=28,
|
||||
font_color=GUIConstants.ACCENT_COLOR,
|
||||
supersampling_factor=1,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||
@@ -302,20 +316,20 @@ class DonateScreen(BaseTopNavScreen):
|
||||
@dataclass
|
||||
class SettingsQRConfirmationScreen(ButtonListScreen):
|
||||
config_name: str = None
|
||||
title: str = "Settings QR"
|
||||
status_message: str = "Settings updated..."
|
||||
title: str = _mft("Settings QR")
|
||||
status_message: str = _mft("Settings updated...")
|
||||
is_bottom_list: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.button_data = ["Home"]
|
||||
self.button_data = [ButtonOption("Home")]
|
||||
self.show_back_button = False
|
||||
super().__post_init__()
|
||||
|
||||
start_y = self.top_nav.height + 20
|
||||
if self.config_name:
|
||||
self.config_name_textarea = TextArea(
|
||||
text=f'"{self.config_name}"',
|
||||
text=f'"{self.config_name}"', # User-supplied string (from SettingsQR); don't wrap to translate
|
||||
is_text_centered=True,
|
||||
auto_line_break=True,
|
||||
screen_y=start_y
|
||||
@@ -324,7 +338,7 @@ class SettingsQRConfirmationScreen(ButtonListScreen):
|
||||
start_y = self.config_name_textarea.screen_y + 50
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=self.status_message,
|
||||
text=_(self.status_message),
|
||||
is_text_centered=True,
|
||||
auto_line_break=True,
|
||||
screen_y=start_y
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from typing import Any
|
||||
from PIL.Image import Image
|
||||
from seedsigner.hardware.camera import Camera
|
||||
@@ -15,10 +16,6 @@ from seedsigner.models.settings_definition import SettingsConstants, SettingsDef
|
||||
@dataclass
|
||||
class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = "Initializing Camera..."
|
||||
|
||||
# Initialize the base class
|
||||
super().__post_init__()
|
||||
|
||||
self.camera = Camera.get_instance()
|
||||
@@ -29,10 +26,9 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
# save preview image frames to use as additional entropy below
|
||||
preview_images = []
|
||||
max_entropy_frames = 50
|
||||
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size())
|
||||
|
||||
while True:
|
||||
# Check for BACK button press
|
||||
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT):
|
||||
# Have to manually update last input time since we're not in a wait_for loop
|
||||
self.hw_inputs.update_last_input_time()
|
||||
@@ -53,6 +49,27 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
self.hw_inputs.update_last_input_time()
|
||||
self.camera.stop_video_stream_mode()
|
||||
|
||||
with self.renderer.lock:
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text=_("Capturing image..."),
|
||||
fill=GUIConstants.ACCENT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
return preview_images
|
||||
|
||||
# If we're still here, it's just another preview frame loop
|
||||
with self.renderer.lock:
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
@@ -60,8 +77,8 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text="Capturing image...",
|
||||
fill=GUIConstants.ACCENT_COLOR,
|
||||
text="< " + _("back") + " | " + _("click joystick"), # TODO: Render with UI elements instead of text
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
@@ -69,25 +86,6 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
return preview_images
|
||||
|
||||
# If we're still here, it's just another preview frame loop
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text="< back | click joystick",
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
if len(preview_images) == max_entropy_frames:
|
||||
# Keep a moving window of the last n preview frames; pop the oldest
|
||||
# before we add the currest frame.
|
||||
@@ -101,22 +99,29 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen):
|
||||
final_image: Image = None
|
||||
|
||||
def _run(self):
|
||||
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size())
|
||||
|
||||
self.renderer.canvas.paste(self.final_image)
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text=" < reshoot | accept > ",
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
with self.renderer.lock:
|
||||
self.renderer.canvas.paste(self.final_image)
|
||||
|
||||
# TRANSLATOR_NOTE: A prompt to the user to either accept or reshoot the image
|
||||
reshoot = _("reshoot")
|
||||
|
||||
# TRANSLATOR_NOTE: A prompt to the user to either accept or reshoot the image
|
||||
accept = _("accept")
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text=" < " + reshoot + " | " + accept + " > ",
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
input = self.hw_inputs.wait_for([HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT])
|
||||
if input == HardwareButtonsConstants.KEY_LEFT:
|
||||
@@ -126,9 +131,10 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen):
|
||||
|
||||
@dataclass
|
||||
class ToolsDiceEntropyEntryScreen(KeyboardScreen):
|
||||
|
||||
def __post_init__(self):
|
||||
# Override values set by the parent class
|
||||
self.title = f"Dice Roll 1/{self.return_after_n_chars}"
|
||||
# TRANSLATOR_NOTE: current roll number vs total rolls (e.g. roll 7 of 50)
|
||||
self.title = _("Dice Roll {}/{}").format(1, self.return_after_n_chars)
|
||||
|
||||
# Specify the keys in the keyboard
|
||||
self.rows = 3
|
||||
@@ -159,7 +165,7 @@ class ToolsDiceEntropyEntryScreen(KeyboardScreen):
|
||||
|
||||
|
||||
def update_title(self) -> bool:
|
||||
self.title = f"Dice Roll {self.cursor_position + 1}/{self.return_after_n_chars}"
|
||||
self.title = _("Dice Roll {}/{}").format(self.cursor_position + 1, self.return_after_n_chars)
|
||||
return True
|
||||
|
||||
|
||||
@@ -170,13 +176,15 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
|
||||
num_entropy_bits: int = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.title = "Build Final Word"
|
||||
# TRANSLATOR_NOTE: Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase.
|
||||
self.title = _("Build Final Word")
|
||||
self.is_bottom_list = True
|
||||
self.is_button_text_centered = True
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=f"The {self.mnemonic_length}th word is built from {self.num_entropy_bits} more entropy bits plus auto-calculated checksum.",
|
||||
# TRANSLATOR_NOTE: Number of BIP-39 seed words, and the entropy -- in bits, contained within.
|
||||
text=_("The {}th word is built from {} more entropy bits plus auto-calculated checksum.").format(self.mnemonic_length, self.num_entropy_bits),
|
||||
screen_y=self.top_nav.height + int(GUIConstants.COMPONENT_PADDING/2),
|
||||
))
|
||||
|
||||
@@ -186,29 +194,33 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
|
||||
class ToolsCoinFlipEntryScreen(KeyboardScreen):
|
||||
def __post_init__(self):
|
||||
# Override values set by the parent class
|
||||
self.title = f"Coin Flip 1/{self.return_after_n_chars}"
|
||||
# TRANSLATOR_NOTE: current coin-flip number vs total flips (e.g. flip 3 of 4)
|
||||
self.title = _("Coin Flip {}/{}").format(1, self.return_after_n_chars)
|
||||
|
||||
# Specify the keys in the keyboard
|
||||
self.rows = 1
|
||||
self.cols = 4
|
||||
self.key_height = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2 + 2*GUIConstants.EDGE_PADDING
|
||||
self.key_height = GUIConstants.get_top_nav_title_font_size() + 2 + 2*GUIConstants.EDGE_PADDING
|
||||
self.keys_charset = "10"
|
||||
|
||||
# Now initialize the parent class
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="Heads = 1",
|
||||
# TRANSLATOR_NOTE: How we call the "front" side result during a coin toss.
|
||||
text=_("Heads = 1"),
|
||||
screen_y = self.keyboard.rect[3] + 4*GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
self.components.append(TextArea(
|
||||
text="Tails = 0",
|
||||
# TRANSLATOR_NOTE: How we call the "back" side result during a coin toss.
|
||||
text=_("Tails = 0"),
|
||||
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
|
||||
|
||||
def update_title(self) -> bool:
|
||||
self.title = f"Coin Flip {self.cursor_position + 1}/{self.return_after_n_chars}"
|
||||
# l10n_note already done.
|
||||
self.title = _("Coin Flip {}/{}").format(self.cursor_position + 1, self.return_after_n_chars)
|
||||
return True
|
||||
|
||||
|
||||
@@ -225,7 +237,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
super().__post_init__()
|
||||
|
||||
# First what's the total bit display width and where do the checksum bits start?
|
||||
bit_font_size = GUIConstants.BUTTON_FONT_SIZE + 2
|
||||
bit_font_size = GUIConstants.get_button_font_size() + 2
|
||||
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, bit_font_size)
|
||||
(left, top, bit_display_width, bit_font_height) = font.getbbox("0" * 11, anchor="lt")
|
||||
(left, top, checksum_x, bottom) = font.getbbox("0" * (11 - len(self.checksum_bits)), anchor="lt")
|
||||
@@ -249,8 +261,10 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
# significant n bits always rendered in same column)
|
||||
discard_selected_bits = "_" * (len(self.checksum_bits))
|
||||
|
||||
# TRANSLATOR_NOTE: The additional entropy the user supplied (e.g. coin flips)
|
||||
your_input = _('Your input: "{}"').format(selection_text)
|
||||
self.components.append(TextArea(
|
||||
text=f"""Your input: \"{selection_text}\"""",
|
||||
text=your_input,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING - 2, # Nudge to last line doesn't get too close to "Next" button
|
||||
height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline
|
||||
))
|
||||
@@ -285,7 +299,8 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
|
||||
# Show the checksum..
|
||||
self.components.append(TextArea(
|
||||
text="Checksum",
|
||||
# TRANSLATOR_NOTE: A function of "x" to be used for detecting errors in "x"
|
||||
text=_("Checksum"),
|
||||
edge_padding=0,
|
||||
screen_y=first_bits_line.screen_y + first_bits_line.height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
@@ -321,7 +336,8 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
|
||||
# And now the *actual* final word after merging the bit data
|
||||
self.components.append(TextArea(
|
||||
text=f"""Final Word: \"{self.actual_final_word}\"""",
|
||||
# TRANSLATOR_NOTE: labeled presentation of the last word in a BIP-39 mnemonic seed phrase.
|
||||
text=_('Final Word: "{}"').format(self.actual_final_word),
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline
|
||||
))
|
||||
@@ -361,15 +377,20 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
|
||||
fingerprint: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = f"{self.mnemonic_word_length}th Word"
|
||||
# Manually specify 12 vs 24 case for easier ordinal translation
|
||||
if self.mnemonic_word_length == 12:
|
||||
# TRANSLATOR_NOTE: a label for the last word of a 12-word BIP-39 mnemonic seed phrase
|
||||
self.title = _("12th Word")
|
||||
else:
|
||||
# TRANSLATOR_NOTE: a label for the last word of a 24-word BIP-39 mnemonic seed phrase
|
||||
self.title = _("24th Word")
|
||||
self.is_bottom_list = True
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=f"""\"{self.final_word}\"""",
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 6,
|
||||
font_size=26,
|
||||
is_text_centered=True,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
@@ -377,7 +398,8 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.FINGERPRINT,
|
||||
icon_color=GUIConstants.INFO_COLOR,
|
||||
label_text="fingerprint",
|
||||
# TRANSLATOR_NOTE: a label for the shortened Key-id of a BIP-32 master HD wallet
|
||||
label_text=_("fingerprint"),
|
||||
value_text=self.fingerprint,
|
||||
is_text_centered=True,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 3*GUIConstants.COMPONENT_PADDING,
|
||||
@@ -393,7 +415,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
custom_derivation_path: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.title = "Address Explorer"
|
||||
# TRANSLATOR_NOTE: a label for the tool to explore public addresses for this seed.
|
||||
self.title = _("Address Explorer")
|
||||
self.is_bottom_list = True
|
||||
super().__post_init__()
|
||||
|
||||
@@ -401,7 +424,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.FINGERPRINT,
|
||||
icon_color=GUIConstants.INFO_COLOR,
|
||||
label_text="Fingerprint",
|
||||
# TRANSLATOR_NOTE: a label for the shortened Key-id of a BIP-32 master HD wallet
|
||||
label_text=_("Fingerprint"),
|
||||
value_text=self.fingerprint,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
@@ -410,7 +434,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
if self.script_type != SettingsConstants.CUSTOM_DERIVATION:
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.DERIVATION,
|
||||
label_text="Derivation",
|
||||
# TRANSLATOR_NOTE: a label for the derivation-path into a BIP-32 HD wallet
|
||||
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,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
@@ -418,7 +443,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
else:
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.DERIVATION,
|
||||
label_text="Derivation",
|
||||
# l10n_note already exists.
|
||||
label_text=_("Derivation"),
|
||||
value_text=self.custom_derivation_path,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
@@ -426,7 +452,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
|
||||
else:
|
||||
self.components.append(IconTextLine(
|
||||
label_text="Wallet descriptor",
|
||||
# TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor
|
||||
label_text=_("Wallet descriptor"),
|
||||
value_text=self.wallet_descriptor_display_name,
|
||||
is_text_centered=True,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import BaseComponent, GUIConstants, Icon, SeedSignerIconConstants, TextArea
|
||||
from seedsigner.models.threads import BaseThread
|
||||
|
||||
@@ -187,21 +189,26 @@ class BaseToastOverlayManagerThread(BaseThread):
|
||||
|
||||
|
||||
class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread):
|
||||
def __init__(self, activation_delay=3):
|
||||
# Note: activation_delay is configurable so the screenshot generator can get the
|
||||
# toast to immediately render.
|
||||
def __init__(self, activation_delay: int = 3, duration: int = 1e6):
|
||||
"""
|
||||
* activation_delay: configurable so the screenshot generator can get the
|
||||
toast to immediately render.
|
||||
* duration: default value is essentially forever. Overrideable for the
|
||||
screenshot generator.
|
||||
"""
|
||||
super().__init__(
|
||||
activation_delay=activation_delay, # seconds
|
||||
duration=1e6, # seconds ("forever")
|
||||
activation_delay=activation_delay,
|
||||
duration=duration,
|
||||
)
|
||||
|
||||
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
body_font_size = GUIConstants.get_body_font_size()
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.MICROSD,
|
||||
label_text="You can remove\nthe SD card now",
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING,
|
||||
label_text=_("You can remove\nthe SD card now"),
|
||||
font_size=body_font_size,
|
||||
height=body_font_size * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING,
|
||||
)
|
||||
|
||||
|
||||
@@ -219,7 +226,7 @@ class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread):
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
if action not in [MicroSD.ACTION__INSERTED, MicroSD.ACTION__REMOVED]:
|
||||
raise Exception(f"Invalid MicroSD action: {action}")
|
||||
self.message = "SD card removed" if action == MicroSD.ACTION__REMOVED else "SD card inserted"
|
||||
self.message = _("SD card removed") if action == MicroSD.ACTION__REMOVED else _("SD card inserted")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -65,6 +65,15 @@ class HardwareButtons(Singleton):
|
||||
return cls._instance
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_instance_no_hardware(cls):
|
||||
# This is the only way to access the one and only instance
|
||||
if cls._instance is None:
|
||||
cls._instance = cls.__new__(cls)
|
||||
|
||||
|
||||
|
||||
def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int:
|
||||
# TODO: Refactor to keep control in the Controller and not here
|
||||
from seedsigner.controller import Controller
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import io
|
||||
|
||||
from picamera import PiCamera
|
||||
from PIL import Image
|
||||
from seedsigner.hardware.pivideostream import PiVideoStream
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
@@ -23,6 +22,7 @@ class Camera(Singleton):
|
||||
|
||||
|
||||
def start_video_stream_mode(self, resolution=(512, 384), framerate=12, format="bgr"):
|
||||
from seedsigner.hardware.pivideostream import PiVideoStream
|
||||
if self._video_stream is not None:
|
||||
self.stop_video_stream_mode()
|
||||
|
||||
@@ -49,6 +49,7 @@ class Camera(Singleton):
|
||||
|
||||
|
||||
def start_single_frame_mode(self, resolution=(720, 480)):
|
||||
from picamera import PiCamera
|
||||
if self._video_stream is not None:
|
||||
self.stop_video_stream_mode()
|
||||
if self._picamera is not None:
|
||||
|
||||
@@ -50,7 +50,7 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle
|
||||
elif script_type == SettingsConstants.NATIVE_SEGWIT:
|
||||
return f"m/48'/{network_path}/0'/2'"
|
||||
elif script_type == SettingsConstants.TAPROOT:
|
||||
raise Exception("Taproot multisig/musig not yet supported")
|
||||
raise Exception("Taproot multisig not yet supported")
|
||||
else:
|
||||
raise Exception("Unexpected script type")
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
def mark_for_translation(message: str) -> str:
|
||||
# Wraps the target string literal for translation but does NOT return the translated string.
|
||||
return message
|
||||
@@ -102,6 +102,6 @@ class QR:
|
||||
# if qrencode fails, fall back to only encoder
|
||||
if rv != 0:
|
||||
return self.qrimage(data,width,height,border)
|
||||
img = Image.open("/tmp/qrcode.png").resize((width,height), Image.NEAREST).convert("RGBA")
|
||||
img = Image.open("/tmp/qrcode.png").resize((width,height), Image.Resampling.NEAREST).convert("RGBA")
|
||||
|
||||
return img
|
||||
|
||||
@@ -78,6 +78,11 @@ class Seed:
|
||||
return unicodedata.normalize("NFC", " ".join(self._mnemonic)).split()
|
||||
|
||||
|
||||
@property
|
||||
def has_passphrase(self):
|
||||
return self._passphrase != ""
|
||||
|
||||
|
||||
@property
|
||||
def passphrase(self):
|
||||
return self._passphrase
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import gettext
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
|
||||
from typing import List
|
||||
@@ -36,6 +38,19 @@ class Settings(Singleton):
|
||||
with open(Settings.SETTINGS_FILENAME) as settings_file:
|
||||
settings.update(json.load(settings_file))
|
||||
|
||||
# Setup multilanguage support
|
||||
path = os.path.join(
|
||||
pathlib.Path(__file__).parent.resolve().parent.resolve(),
|
||||
"resources",
|
||||
"seedsigner-translations",
|
||||
"l10n"
|
||||
)
|
||||
gettext.bindtextdomain('messages', localedir=path)
|
||||
gettext.textdomain('messages')
|
||||
|
||||
# Load default/persistent locale setting
|
||||
settings.load_locale()
|
||||
|
||||
return cls._instance
|
||||
|
||||
|
||||
@@ -141,11 +156,8 @@ class Settings(Singleton):
|
||||
# Break comma-separated SettingsQR input into List
|
||||
new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",")
|
||||
|
||||
# Can't just merge the _data dict; have to replace keys they have in common
|
||||
# (otherwise list values will be merged instead of replaced).
|
||||
for key, value in new_settings.items():
|
||||
self._data.pop(key, None)
|
||||
self._data[key] = value
|
||||
self.set_value(key, value)
|
||||
|
||||
|
||||
def set_value(self, attr_name: str, value: any):
|
||||
@@ -155,7 +167,9 @@ class Settings(Singleton):
|
||||
Note that for multiselect, the value must be a List.
|
||||
"""
|
||||
if attr_name not in self._data:
|
||||
raise Exception(f"Setting for {attr_name} not found")
|
||||
# Outdated settings
|
||||
print(f"Setting {attr_name} not recognized. Ignoring.")
|
||||
return
|
||||
|
||||
if SettingsDefinition.get_settings_entry(attr_name).type == SettingsConstants.TYPE__MULTISELECT:
|
||||
if type(value) != list:
|
||||
@@ -171,7 +185,11 @@ class Settings(Singleton):
|
||||
|
||||
self._data[attr_name] = value
|
||||
self.save()
|
||||
|
||||
|
||||
# Special handling for localization
|
||||
if attr_name == SettingsConstants.SETTING__LOCALE:
|
||||
self.load_locale()
|
||||
|
||||
|
||||
def get_value(self, attr_name: str):
|
||||
"""
|
||||
@@ -222,13 +240,22 @@ class Settings(Singleton):
|
||||
return display_names
|
||||
|
||||
|
||||
def load_locale(self):
|
||||
locale = self.get_value(SettingsConstants.SETTING__LOCALE)
|
||||
os.environ['LANGUAGE'] = locale
|
||||
|
||||
# Re-initialize with the new locale
|
||||
print(f"Set LANGUAGE locale to {os.environ['LANGUAGE']}")
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Intentionally keeping the properties very limited to avoid an expectation of
|
||||
boilerplate property code for every SettingsEntry.
|
||||
|
||||
It's more cumbersome, but instead use:
|
||||
|
||||
settings.get_value(SettingsConstants.SETTING__MY_SETTING_ATTR)
|
||||
Settings.get_instance().get_value(SettingsConstants.SETTING__MY_SETTING_ATTR)
|
||||
"""
|
||||
@property
|
||||
def debug(self) -> bool:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List
|
||||
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
|
||||
|
||||
class SettingsConstants:
|
||||
@@ -10,25 +11,25 @@ class SettingsConstants:
|
||||
OPTION__PROMPT = "P"
|
||||
OPTION__REQUIRED = "R"
|
||||
OPTIONS__ENABLED_DISABLED = [
|
||||
(OPTION__ENABLED, "Enabled"),
|
||||
(OPTION__DISABLED, "Disabled"),
|
||||
(OPTION__ENABLED, _mft("Enabled")),
|
||||
(OPTION__DISABLED, _mft("Disabled")),
|
||||
]
|
||||
OPTIONS__ONLY_DISABLED = [
|
||||
(OPTION__DISABLED, "Disabled"),
|
||||
(OPTION__DISABLED, _mft("Disabled")),
|
||||
]
|
||||
OPTIONS__PROMPT_REQUIRED_DISABLED = [
|
||||
(OPTION__PROMPT, "Prompt"),
|
||||
(OPTION__REQUIRED, "Required"),
|
||||
(OPTION__DISABLED, "Disabled"),
|
||||
(OPTION__PROMPT, _mft("Prompt")),
|
||||
(OPTION__REQUIRED, _mft("Required")),
|
||||
(OPTION__DISABLED, _mft("Disabled")),
|
||||
]
|
||||
OPTIONS__ENABLED_DISABLED_REQUIRED = OPTIONS__ENABLED_DISABLED +[
|
||||
(OPTION__REQUIRED, "Required"),
|
||||
(OPTION__REQUIRED, _mft("Required")),
|
||||
]
|
||||
OPTIONS__ENABLED_DISABLED_PROMPT = OPTIONS__ENABLED_DISABLED + [
|
||||
(OPTION__PROMPT, "Prompt"),
|
||||
(OPTION__PROMPT, _mft("Prompt")),
|
||||
]
|
||||
ALL_OPTIONS = OPTIONS__ENABLED_DISABLED_PROMPT + [
|
||||
(OPTION__REQUIRED, "Required"),
|
||||
(OPTION__REQUIRED, _mft("Required")),
|
||||
]
|
||||
|
||||
# User-facing selection options
|
||||
@@ -45,9 +46,30 @@ class SettingsConstants:
|
||||
(COORDINATOR__KEEPER, "Keeper"),
|
||||
]
|
||||
|
||||
LANGUAGE__ENGLISH = "en"
|
||||
ALL_LANGUAGES = [
|
||||
(LANGUAGE__ENGLISH, "English"),
|
||||
LOCALE__ARABIC = "ar"
|
||||
LOCALE__CZECH = "cs"
|
||||
LOCALE__ENGLISH = "en"
|
||||
LOCALE__FRENCH = "fr"
|
||||
LOCALE__GERMAN = "de"
|
||||
LOCALE__HEBREW = "he"
|
||||
LOCALE__JAPANESE = "ja"
|
||||
LOCALE__KOREAN = "kr"
|
||||
LOCALE__PORTUGUESE = "pt"
|
||||
LOCALE__RUSSIAN = "ru"
|
||||
LOCALE__SPANISH = "es"
|
||||
# Do not wrap for translation; always present each language in its native form
|
||||
ALL_LOCALES = [
|
||||
# (LOCALE__ARABIC, "Arabic"),
|
||||
# (LOCALE__CZECH, "čeština"),
|
||||
(LOCALE__ENGLISH, "English"),
|
||||
# (LOCALE__FRENCH, "Français"),
|
||||
# (LOCALE__GERMAN, "Deutsch"),
|
||||
# (LOCALE__HEBREW, "Hebrew"),
|
||||
# (LOCALE__JAPANESE, "Japanese"),
|
||||
# (LOCALE__KOREAN, "Korean"),
|
||||
# (LOCALE__PORTUGUESE, "Português"),
|
||||
# (LOCALE__RUSSIAN, "русский"),
|
||||
(LOCALE__SPANISH, "Español"),
|
||||
]
|
||||
|
||||
BTC_DENOMINATION__BTC = "btc"
|
||||
@@ -55,10 +77,10 @@ class SettingsConstants:
|
||||
BTC_DENOMINATION__THRESHOLD = "thr"
|
||||
BTC_DENOMINATION__BTCSATSHYBRID = "hyb"
|
||||
ALL_BTC_DENOMINATIONS = [
|
||||
(BTC_DENOMINATION__BTC, "BTC"),
|
||||
(BTC_DENOMINATION__SATS, "sats"),
|
||||
(BTC_DENOMINATION__THRESHOLD, "Threshold at 0.01"),
|
||||
(BTC_DENOMINATION__BTCSATSHYBRID, "BTC | sats hybrid"),
|
||||
(BTC_DENOMINATION__BTC, _mft("BTC")),
|
||||
(BTC_DENOMINATION__SATS, _mft("sats")),
|
||||
(BTC_DENOMINATION__THRESHOLD, _mft("Threshold at 0.01")),
|
||||
(BTC_DENOMINATION__BTCSATSHYBRID, _mft("BTC | sats hybrid")),
|
||||
]
|
||||
|
||||
CAMERA_ROTATION__0 = 0
|
||||
@@ -66,20 +88,26 @@ class SettingsConstants:
|
||||
CAMERA_ROTATION__180 = 180
|
||||
CAMERA_ROTATION__270 = 270
|
||||
ALL_CAMERA_ROTATIONS = [
|
||||
(CAMERA_ROTATION__0, "0°"),
|
||||
(CAMERA_ROTATION__90, "90°"),
|
||||
(CAMERA_ROTATION__180, "180°"),
|
||||
(CAMERA_ROTATION__270, "270°"),
|
||||
(CAMERA_ROTATION__0, _mft("0°")),
|
||||
(CAMERA_ROTATION__90, _mft("90°")),
|
||||
(CAMERA_ROTATION__180, _mft("180°")),
|
||||
(CAMERA_ROTATION__270, _mft("270°")),
|
||||
]
|
||||
|
||||
# QR code constants
|
||||
DENSITY__LOW = "L"
|
||||
DENSITY__MEDIUM = "M"
|
||||
DENSITY__HIGH = "H"
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High
|
||||
density_low = _mft("Low")
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High
|
||||
density_medium = _mft("Medium")
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High
|
||||
density_high = _mft("High")
|
||||
ALL_DENSITIES = [
|
||||
(DENSITY__LOW, "Low"),
|
||||
(DENSITY__MEDIUM, "Medium"),
|
||||
(DENSITY__HIGH, "High"),
|
||||
(DENSITY__LOW, density_low),
|
||||
(DENSITY__MEDIUM, density_medium),
|
||||
(DENSITY__HIGH, density_high),
|
||||
]
|
||||
|
||||
# Seed-related constants
|
||||
@@ -87,13 +115,14 @@ class SettingsConstants:
|
||||
TESTNET = "T"
|
||||
REGTEST = "R"
|
||||
ALL_NETWORKS = [
|
||||
(MAINNET, "Mainnet"),
|
||||
(TESTNET, "Testnet"),
|
||||
(REGTEST, "Regtest")
|
||||
(MAINNET, _mft("Mainnet")),
|
||||
(TESTNET, _mft("Testnet")),
|
||||
(REGTEST, _mft("Regtest"))
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def map_network_to_embit(cls, network) -> str:
|
||||
# Note these are `embit` constants; do not wrap for translation
|
||||
if network == SettingsConstants.MAINNET:
|
||||
return "main"
|
||||
elif network == SettingsConstants.TESTNET:
|
||||
@@ -101,14 +130,14 @@ class SettingsConstants:
|
||||
if network == SettingsConstants.REGTEST:
|
||||
return "regtest"
|
||||
|
||||
PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT = "Store Settings on SD card"
|
||||
PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT = "Insert SD card to enable"
|
||||
PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT = _mft("Store Settings on SD card")
|
||||
PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT = _mft("Insert SD card to enable")
|
||||
|
||||
SINGLE_SIG = "ss"
|
||||
MULTISIG = "ms"
|
||||
ALL_SIG_TYPES = [
|
||||
(SINGLE_SIG, "Single Sig"),
|
||||
(MULTISIG, "Multisig"),
|
||||
(SINGLE_SIG, _mft("Single Sig")),
|
||||
(MULTISIG, _mft("Multisig")),
|
||||
]
|
||||
|
||||
LEGACY_P2PKH = "leg"
|
||||
@@ -117,11 +146,11 @@ class SettingsConstants:
|
||||
TAPROOT = "tr"
|
||||
CUSTOM_DERIVATION = "cus"
|
||||
ALL_SCRIPT_TYPES = [
|
||||
(NATIVE_SEGWIT, "Native Segwit"),
|
||||
(NESTED_SEGWIT, "Nested Segwit"),
|
||||
(LEGACY_P2PKH, "Legacy"),
|
||||
(TAPROOT, "Taproot"),
|
||||
(CUSTOM_DERIVATION, "Custom Derivation"),
|
||||
(NATIVE_SEGWIT, _mft("Native Segwit")),
|
||||
(NESTED_SEGWIT, _mft("Nested Segwit")),
|
||||
(LEGACY_P2PKH, _mft("Legacy")),
|
||||
(TAPROOT, _mft("Taproot")),
|
||||
(CUSTOM_DERIVATION, _mft("Custom Derivation")),
|
||||
]
|
||||
|
||||
WORDLIST_LANGUAGE__ENGLISH = "en"
|
||||
@@ -145,7 +174,8 @@ class SettingsConstants:
|
||||
|
||||
|
||||
# Individual SettingsEntry attr_names
|
||||
SETTING__LANGUAGE = "language"
|
||||
# Note: attr_names are internal constants; do not wrap for translation
|
||||
SETTING__LOCALE = "locale"
|
||||
SETTING__WORDLIST_LANGUAGE = "wordlist_language"
|
||||
SETTING__PERSISTENT_SETTINGS = "persistent_settings"
|
||||
SETTING__COORDINATORS = "coordinators"
|
||||
@@ -207,8 +237,10 @@ class SettingsConstants:
|
||||
ELECTRUM_PBKDF2_ROUNDS=2048
|
||||
|
||||
# Label strings
|
||||
LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase"
|
||||
LABEL__CUSTOM_EXTENSION = "Custom Extension" # Terminology used by Electrum seeds
|
||||
LABEL__BIP39_PASSPHRASE = _mft("BIP-39 Passphrase")
|
||||
# TRANSLATOR_NOTE: Terminology used by Electrum seeds; equivalent to bip39 passphrase
|
||||
custom_extension = _mft("Custom Extension")
|
||||
LABEL__CUSTOM_EXTENSION = custom_extension
|
||||
|
||||
|
||||
|
||||
@@ -279,7 +311,7 @@ class SettingsEntry:
|
||||
option_value = option
|
||||
display_name = option
|
||||
if option_value == value:
|
||||
return display_name
|
||||
return _mft(display_name)
|
||||
|
||||
|
||||
def get_selection_option_value_by_display_name(self, display_name: str):
|
||||
@@ -345,21 +377,19 @@ class SettingsDefinition:
|
||||
settings_entries: List[SettingsEntry] = [
|
||||
# General options
|
||||
|
||||
# TODO: Full babel multilanguage support! Until then, type == HIDDEN
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__LANGUAGE,
|
||||
attr_name=SettingsConstants.SETTING__LOCALE,
|
||||
abbreviated_name="lang",
|
||||
display_name="Language",
|
||||
display_name=_mft("Language"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
selection_options=SettingsConstants.ALL_LANGUAGES,
|
||||
default_value=SettingsConstants.LANGUAGE__ENGLISH),
|
||||
selection_options=SettingsConstants.ALL_LOCALES,
|
||||
default_value=SettingsConstants.LOCALE__ENGLISH),
|
||||
|
||||
# TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__WORDLIST_LANGUAGE,
|
||||
abbreviated_name="wordlist_lang",
|
||||
display_name="Mnemonic language",
|
||||
display_name=_mft("Mnemonic language"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
selection_options=SettingsConstants.ALL_WORDLIST_LANGUAGES,
|
||||
@@ -368,14 +398,14 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS,
|
||||
abbreviated_name="persistent",
|
||||
display_name="Persistent settings",
|
||||
display_name=_mft("Persistent settings"),
|
||||
help_text=SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__WALLET,
|
||||
attr_name=SettingsConstants.SETTING__COORDINATORS,
|
||||
abbreviated_name="coords",
|
||||
display_name="Coordinator software",
|
||||
display_name=_mft("Coordinator software"),
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
selection_options=SettingsConstants.ALL_COORDINATORS,
|
||||
default_value=[
|
||||
@@ -388,7 +418,7 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__BTC_DENOMINATION,
|
||||
abbreviated_name="denom",
|
||||
display_name="Denomination display",
|
||||
display_name=_mft("Denomination display"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
selection_options=SettingsConstants.ALL_BTC_DENOMINATIONS,
|
||||
default_value=SettingsConstants.BTC_DENOMINATION__THRESHOLD),
|
||||
@@ -397,7 +427,7 @@ class SettingsDefinition:
|
||||
# Advanced options
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__NETWORK,
|
||||
display_name="Bitcoin network",
|
||||
display_name=_mft("Bitcoin network"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_NETWORKS,
|
||||
@@ -405,7 +435,7 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__QR_DENSITY,
|
||||
display_name="QR code density",
|
||||
display_name=_mft("QR code density"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_DENSITIES,
|
||||
@@ -413,14 +443,14 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__XPUB_EXPORT,
|
||||
display_name="Xpub export",
|
||||
display_name=_mft("Xpub export"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__SIG_TYPES,
|
||||
abbreviated_name="sigs",
|
||||
display_name="Sig types",
|
||||
display_name=_mft("Sig types"),
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_SIG_TYPES,
|
||||
@@ -429,7 +459,7 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__SCRIPT_TYPES,
|
||||
abbreviated_name="scripts",
|
||||
display_name="Script types",
|
||||
display_name=_mft("Script types"),
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_SCRIPT_TYPES,
|
||||
@@ -437,13 +467,13 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__XPUB_DETAILS,
|
||||
display_name="Show xpub details",
|
||||
display_name=_mft("Show xpub details"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__PASSPHRASE,
|
||||
display_name="BIP-39 passphrase",
|
||||
display_name=_mft("BIP-39 passphrase"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.OPTIONS__ENABLED_DISABLED_REQUIRED,
|
||||
@@ -452,7 +482,7 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__CAMERA_ROTATION,
|
||||
abbreviated_name="camera",
|
||||
display_name="Camera rotation",
|
||||
display_name=_mft("Camera rotation"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_CAMERA_ROTATIONS,
|
||||
@@ -460,14 +490,14 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
||||
display_name="CompactSeedQR",
|
||||
display_name=_mft("Compact SeedQR"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS,
|
||||
abbreviated_name="bip85",
|
||||
display_name="BIP-85 child seeds",
|
||||
display_name=_mft("BIP-85 child seeds"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
@@ -475,40 +505,40 @@ class SettingsDefinition:
|
||||
attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
abbreviated_name="electrum",
|
||||
display_name="Electrum seeds",
|
||||
help_text="Native Segwit only",
|
||||
help_text=_mft("Native Segwit only"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING,
|
||||
display_name="Message signing",
|
||||
display_name=_mft("Message signing"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS,
|
||||
abbreviated_name="priv_warn",
|
||||
display_name="Show privacy warnings",
|
||||
display_name=_mft("Show privacy warnings"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__DIRE_WARNINGS,
|
||||
abbreviated_name="dire_warn",
|
||||
display_name="Show dire warnings",
|
||||
display_name=_mft("Show dire warnings"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS_TIPS,
|
||||
display_name="Show QR brightness tips",
|
||||
display_name=_mft("Show QR brightness tips"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__PARTNER_LOGOS,
|
||||
abbreviated_name="partners",
|
||||
display_name="Show partner logos",
|
||||
display_name=_mft("Show partner logos"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
@@ -524,7 +554,7 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS,
|
||||
abbreviated_name="qr_brightness",
|
||||
display_name="QR background color",
|
||||
display_name=_mft("QR background color"),
|
||||
type=SettingsConstants.TYPE__FREE_ENTRY,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
default_value=62),
|
||||
|
||||
@@ -21,7 +21,7 @@ class ConfigurableSingleton(Singleton):
|
||||
if cls._instance:
|
||||
return cls._instance
|
||||
else:
|
||||
raise Exception("Must call %s.configure_instance(config) first" % cls.__name__)
|
||||
raise Exception("Must call %s.configure_instance() first" % cls.__name__)
|
||||
|
||||
|
||||
@classmethod
|
||||
|
||||
+1
Submodule src/seedsigner/resources/seedsigner-translations added at 3de3aefb96
@@ -1,26 +1,23 @@
|
||||
from embit.psbt import PSBT
|
||||
from embit import script
|
||||
from embit.networks import NETWORKS
|
||||
from seedsigner.controller import Controller
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
||||
from seedsigner.models.encode_qr import UrPsbtQrEncoder
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOpReturnScreen, PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen)
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, ButtonOption, WarningScreen, DireWarningScreen, QRDisplayScreen)
|
||||
from seedsigner.views.view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
|
||||
|
||||
|
||||
|
||||
class PSBTSelectSeedView(View):
|
||||
SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
SCAN_SEED = ButtonOption("Scan a seed", SeedSignerIconConstants.QRCODE)
|
||||
TYPE_12WORD = ButtonOption("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ButtonOption("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.controller import Controller
|
||||
|
||||
# Note: we can't just autoroute to the PSBT Overview because we might have a
|
||||
# multisig where we want to sign with more than one key on this device.
|
||||
if not self.controller.psbt:
|
||||
@@ -38,9 +35,10 @@ class PSBTSelectSeedView(View):
|
||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
# Doesn't look like this seed can sign the current PSBT
|
||||
button_str += " (?)"
|
||||
# TRANSLATOR_NOTE: Inserts fingerprint w/"?" to indicate that this seed can't sign the current PSBT
|
||||
button_str = _("{} (?)").format(button_str)
|
||||
|
||||
button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT))
|
||||
button_data.append(ButtonOption(button_str, SeedSignerIconConstants.FINGERPRINT))
|
||||
|
||||
button_data.append(self.SCAN_SEED)
|
||||
button_data.append(self.TYPE_12WORD)
|
||||
@@ -50,7 +48,7 @@ class PSBTSelectSeedView(View):
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Select Signer",
|
||||
title=_("Select Signer"),
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
)
|
||||
@@ -94,7 +92,7 @@ class PSBTOverviewView(View):
|
||||
# The PSBTParser takes a while to read the PSBT. Run the loading screen while
|
||||
# we wait.
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
|
||||
self.loading_screen = LoadingScreenThread(text=_("Parsing PSBT..."))
|
||||
self.loading_screen.start()
|
||||
|
||||
try:
|
||||
@@ -109,6 +107,7 @@ class PSBTOverviewView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen
|
||||
psbt_parser = self.controller.psbt_parser
|
||||
|
||||
change_data = psbt_parser.change_data
|
||||
@@ -168,9 +167,9 @@ class PSBTOverviewView(View):
|
||||
class PSBTUnsupportedScriptTypeWarningView(View):
|
||||
def run(self):
|
||||
selected_menu_num = WarningScreen(
|
||||
status_headline="Unsupported Script Type!",
|
||||
text="PSBT has unsupported input script type, please verify your change addresses.",
|
||||
button_data=["Continue"],
|
||||
status_headline=_("Unsupported Script Type!"),
|
||||
text=_("PSBT has unsupported input script type, please verify your change addresses."),
|
||||
button_data=[ButtonOption("Continue")],
|
||||
).display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
@@ -188,9 +187,10 @@ class PSBTUnsupportedScriptTypeWarningView(View):
|
||||
class PSBTNoChangeWarningView(View):
|
||||
def run(self):
|
||||
selected_menu_num = WarningScreen(
|
||||
status_headline="Full Spend!",
|
||||
text="This PSBT spends its entire input value. No change is coming back to your wallet.",
|
||||
button_data=["Continue"],
|
||||
# TRANSLATOR_NOTE: User will receive no change back; the inputs to this transaction are fully spent
|
||||
status_headline=_("Full Spend!"),
|
||||
text=_("This PSBT spends its entire input value. No change is coming back to your wallet."),
|
||||
button_data=[ButtonOption("Continue")],
|
||||
).display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
@@ -214,6 +214,7 @@ class PSBTMathView(View):
|
||||
+ change value
|
||||
"""
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTMathScreen
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
if not psbt_parser:
|
||||
# Should not be able to get here
|
||||
@@ -250,20 +251,24 @@ class PSBTAddressDetailsView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTAddressDetailsScreen
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
|
||||
if not psbt_parser:
|
||||
# Should not be able to get here
|
||||
raise Exception("Routing error")
|
||||
|
||||
title = "Will Send"
|
||||
# TRANSLATOR_NOTE: Future-tense used to indicate that this transaction will send this amount, as opposed to "Send" on its own which could be misread as an instant command (e.g. "Send Now").
|
||||
title = _("Will Send")
|
||||
if psbt_parser.num_destinations > 1:
|
||||
title += f" (#{self.address_num + 1})"
|
||||
|
||||
button_data = []
|
||||
if self.address_num < psbt_parser.num_destinations - 1:
|
||||
button_data = ["Next Recipient"]
|
||||
button_data.append(ButtonOption("Next Recipient"))
|
||||
else:
|
||||
button_data = ["Next"]
|
||||
# TRANSLATOR_NOTE: Short for "Next step"
|
||||
button_data.append(ButtonOption("Next"))
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTAddressDetailsScreen,
|
||||
@@ -294,10 +299,9 @@ class PSBTAddressDetailsView(View):
|
||||
|
||||
|
||||
class PSBTChangeDetailsView(View):
|
||||
NEXT = "Next"
|
||||
SKIP_VERIFICATION = "Skip Verification"
|
||||
VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
|
||||
NEXT = ButtonOption("Next")
|
||||
SKIP_VERIFICATION = ButtonOption("Skip Verification")
|
||||
VERIFY_MULTISIG = ButtonOption("Verify Multisig Change")
|
||||
|
||||
def __init__(self, change_address_num):
|
||||
super().__init__()
|
||||
@@ -305,6 +309,7 @@ class PSBTChangeDetailsView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTChangeDetailsScreen
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
|
||||
if not psbt_parser:
|
||||
@@ -339,11 +344,11 @@ class PSBTChangeDetailsView(View):
|
||||
derivation_path_addr_index = int(derivation_path.split("/")[-1])
|
||||
|
||||
if is_change_derivation_path:
|
||||
title = "Your Change"
|
||||
self.VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
# TRANSLATOR_NOTE: The amount you're receiving back from the transaction
|
||||
title = _("Your Change")
|
||||
else:
|
||||
title = "Self-Transfer"
|
||||
self.VERIFY_MULTISIG = "Verify Multisig Addr"
|
||||
title = _("Self-Transfer")
|
||||
self.VERIFY_MULTISIG.button_label = _("Verify Multisig Addr")
|
||||
# if psbt_parser.num_change_outputs > 1:
|
||||
# title += f" (#{self.change_address_num + 1})"
|
||||
|
||||
@@ -361,10 +366,13 @@ class PSBTChangeDetailsView(View):
|
||||
else:
|
||||
# Single sig
|
||||
try:
|
||||
from embit import script
|
||||
from embit.networks import NETWORKS
|
||||
|
||||
if is_change_derivation_path:
|
||||
loading_screen_text = "Verifying Change..."
|
||||
loading_screen_text = _("Verifying Change...")
|
||||
else:
|
||||
loading_screen_text = "Verifying Self-Transfer..."
|
||||
loading_screen_text = _("Verifying Self-Transfer...")
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
loading_screen = LoadingScreenThread(text=loading_screen_text)
|
||||
loading_screen.start()
|
||||
@@ -436,6 +444,7 @@ class PSBTChangeDetailsView(View):
|
||||
return Destination(PSBTFinalizeView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.VERIFY_MULTISIG:
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.views.seed_views import LoadMultisigWalletDescriptorView
|
||||
self.controller.resume_main_flow = Controller.FLOW__PSBT
|
||||
return Destination(LoadMultisigWalletDescriptorView)
|
||||
@@ -451,17 +460,19 @@ class PSBTAddressVerificationFailedView(View):
|
||||
|
||||
def run(self):
|
||||
if self.is_multisig:
|
||||
title = "Caution"
|
||||
text = f"""PSBT's {"change" if self.is_change else "self-transfer"} address could not be verified with your multisig wallet descriptor."""
|
||||
title = _("Caution")
|
||||
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer".
|
||||
text = _("PSBT's {} address could not be verified with your multisig wallet descriptor.").format(_("change") if self.is_change else _("self-transfer"))
|
||||
else:
|
||||
title = "Suspicious PSBT"
|
||||
text = f"""PSBT's {"change" if self.is_change else "self-transfer"} address could not be generated from your seed."""
|
||||
title = _("Suspicious PSBT")
|
||||
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer".
|
||||
text = _("PSBT's {} address could not be generated from your seed.").format(_("change") if self.is_change else _("self-transfer"))
|
||||
|
||||
DireWarningScreen(
|
||||
title=title,
|
||||
status_headline="Address Verification Failed",
|
||||
status_headline=_("Address Verification Failed"),
|
||||
text=text,
|
||||
button_data=["Discard PSBT"],
|
||||
button_data=[ButtonOption("Discard PSBT")],
|
||||
show_back_button=False,
|
||||
).display()
|
||||
|
||||
@@ -476,14 +487,15 @@ class PSBTOpReturnView(View):
|
||||
Shows the OP_RETURN data
|
||||
"""
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOpReturnScreen
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
|
||||
if not psbt_parser:
|
||||
# Should not be able to get here
|
||||
raise Exception("Routing error")
|
||||
|
||||
title = "OP_RETURN"
|
||||
button_data = ["Next"]
|
||||
title = _("OP_RETURN")
|
||||
button_data = [ButtonOption("Next")]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTOpReturnScreen,
|
||||
@@ -502,10 +514,13 @@ class PSBTOpReturnView(View):
|
||||
class PSBTFinalizeView(View):
|
||||
"""
|
||||
"""
|
||||
APPROVE_PSBT = "Approve PSBT"
|
||||
APPROVE_PSBT = ButtonOption("Approve PSBT")
|
||||
|
||||
|
||||
def run(self):
|
||||
from embit.psbt import PSBT
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTFinalizeScreen
|
||||
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
psbt: PSBT = self.controller.psbt
|
||||
|
||||
@@ -541,6 +556,8 @@ class PSBTFinalizeView(View):
|
||||
|
||||
class PSBTSignedQRDisplayView(View):
|
||||
def run(self):
|
||||
from seedsigner.models.encode_qr import UrPsbtQrEncoder
|
||||
|
||||
qr_encoder = UrPsbtQrEncoder(
|
||||
psbt=self.controller.psbt,
|
||||
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
|
||||
@@ -554,7 +571,7 @@ class PSBTSignedQRDisplayView(View):
|
||||
|
||||
|
||||
class PSBTSigningErrorView(View):
|
||||
SELECT_DIFF_SEED = "Select Diff Seed"
|
||||
SELECT_DIFF_SEED = ButtonOption("Select Diff Seed")
|
||||
|
||||
def run(self):
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
@@ -565,10 +582,10 @@ class PSBTSigningErrorView(View):
|
||||
# Just a WarningScreen here; only use DireWarningScreen for true security risks.
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="PSBT Error",
|
||||
title=_("PSBT Error"),
|
||||
status_icon_name=SeedSignerIconConstants.WARNING,
|
||||
status_headline="Signing Failed",
|
||||
text="Signing with this seed did not add a valid signature.",
|
||||
status_headline=_("Signing Failed"),
|
||||
text=_("Signing with this seed did not add a valid signature."),
|
||||
button_data=[self.SELECT_DIFF_SEED]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from embit.descriptor import Descriptor
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.seed import Seed
|
||||
from gettext import gettext as _
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.views.settings_views import SettingsIngestSettingsQRView
|
||||
from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, OptionDisabledView, View, Destination
|
||||
from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, View, Destination
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class ScanView(View):
|
||||
"""
|
||||
The catch-all generic scanning View that will accept any of our supported QR
|
||||
@@ -22,11 +19,13 @@ class ScanView(View):
|
||||
dedicated errors when an unexpected QR type is scanned (e.g. Scan PSBT was
|
||||
selected but a SeedQR was scanned).
|
||||
"""
|
||||
instructions_text = "Scan a QR code"
|
||||
invalid_qr_type_message = "QRCode not recognized or not yet supported."
|
||||
instructions_text = _mft("Scan a QR code")
|
||||
invalid_qr_type_message = _mft("QRCode not recognized or not yet supported.")
|
||||
|
||||
|
||||
def __init__(self):
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
|
||||
super().__init__()
|
||||
# Define the decoder here to make it available to child classes' is_valid_qr_type
|
||||
# checks and so we can inject data into it in the test suite's `before_run()`.
|
||||
@@ -60,10 +59,11 @@ class ScanView(View):
|
||||
# current flow.
|
||||
# Report QR types in more human-readable text (e.g. QRType
|
||||
# `seed__compactseedqr` as "seed: compactseedqr").
|
||||
# TODO: cleanup l10n presentation
|
||||
return Destination(ErrorView, view_args=dict(
|
||||
title="Error",
|
||||
status_headline="Wrong QR Type",
|
||||
text=self.invalid_qr_type_message + f""", received "{self.decoder.qr_type.replace("__", ": ").replace("_", " ")}\" format""",
|
||||
status_headline=_("Wrong QR Type"),
|
||||
text=_(self.invalid_qr_type_message) + f""", received "{self.decoder.qr_type.replace("__", ": ").replace("_", " ")}\" format""",
|
||||
button_text="Back",
|
||||
next_destination=Destination(BackStackView, skip_current_view=True),
|
||||
))
|
||||
@@ -73,10 +73,11 @@ class ScanView(View):
|
||||
|
||||
if not seed_mnemonic:
|
||||
# seed is not valid, Exit if not valid with message
|
||||
raise Exception("Not yet implemented!")
|
||||
return Destination(NotYetImplementedView)
|
||||
else:
|
||||
# Found a valid mnemonic seed! All new seeds should be considered
|
||||
# pending (might set a passphrase, SeedXOR, etc) until finalized.
|
||||
from seedsigner.models.seed import Seed
|
||||
from .seed_views import SeedFinalizeView
|
||||
self.controller.storage.set_pending_seed(
|
||||
Seed(mnemonic=seed_mnemonic, wordlist_language_code=self.wordlist_language_code)
|
||||
@@ -95,10 +96,12 @@ class ScanView(View):
|
||||
return Destination(PSBTSelectSeedView, skip_current_view=True)
|
||||
|
||||
elif self.decoder.is_settings:
|
||||
from seedsigner.views.settings_views import SettingsIngestSettingsQRView
|
||||
data = self.decoder.get_settings_data()
|
||||
return Destination(SettingsIngestSettingsQRView, view_args=dict(data=data))
|
||||
|
||||
elif self.decoder.is_wallet_descriptor:
|
||||
from embit.descriptor import Descriptor
|
||||
from seedsigner.views.seed_views import MultisigWalletDescriptorView
|
||||
descriptor_str = self.decoder.get_wallet_descriptor()
|
||||
|
||||
@@ -161,10 +164,10 @@ class ScanView(View):
|
||||
# start everything over.
|
||||
self.controller.resume_main_flow = None
|
||||
return Destination(ErrorView, view_args=dict(
|
||||
title="Error",
|
||||
status_headline="Unknown QR Type",
|
||||
text="QRCode is invalid or is a data format not yet supported.",
|
||||
button_text="Done",
|
||||
title=_("Error"),
|
||||
status_headline=_("Unknown QR Type"),
|
||||
text=_("QRCode is invalid or is a data format not yet supported."),
|
||||
button_text=_("Done"),
|
||||
next_destination=Destination(MainMenuView, clear_history=True),
|
||||
))
|
||||
|
||||
@@ -173,8 +176,8 @@ class ScanView(View):
|
||||
|
||||
|
||||
class ScanPSBTView(ScanView):
|
||||
instructions_text = "Scan PSBT"
|
||||
invalid_qr_type_message = "Expected a PSBT"
|
||||
instructions_text = _mft("Scan PSBT")
|
||||
invalid_qr_type_message = _mft("Expected a PSBT")
|
||||
|
||||
@property
|
||||
def is_valid_qr_type(self):
|
||||
@@ -183,8 +186,8 @@ class ScanPSBTView(ScanView):
|
||||
|
||||
|
||||
class ScanSeedQRView(ScanView):
|
||||
instructions_text = "Scan SeedQR"
|
||||
invalid_qr_type_message = f"Expected a SeedQR"
|
||||
instructions_text = _mft("Scan SeedQR")
|
||||
invalid_qr_type_message = _mft("Expected a SeedQR")
|
||||
|
||||
@property
|
||||
def is_valid_qr_type(self):
|
||||
@@ -193,8 +196,8 @@ class ScanSeedQRView(ScanView):
|
||||
|
||||
|
||||
class ScanWalletDescriptorView(ScanView):
|
||||
instructions_text = "Scan descriptor"
|
||||
invalid_qr_type_message = "Expected a wallet descriptor QR"
|
||||
instructions_text = _mft("Scan descriptor")
|
||||
invalid_qr_type_message = _mft("Expected a wallet descriptor QR")
|
||||
|
||||
@property
|
||||
def is_valid_qr_type(self):
|
||||
@@ -203,8 +206,8 @@ class ScanWalletDescriptorView(ScanView):
|
||||
|
||||
|
||||
class ScanAddressView(ScanView):
|
||||
instructions_text = "Scan address QR"
|
||||
invalid_qr_type_message = "Expected an address QR"
|
||||
instructions_text = _mft("Scan address QR")
|
||||
invalid_qr_type_message = _mft("Expected an address QR")
|
||||
|
||||
@property
|
||||
def is_valid_qr_type(self):
|
||||
|
||||
@@ -3,16 +3,19 @@ import os
|
||||
import random
|
||||
import time
|
||||
|
||||
from PIL import Image
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import Fonts, GUIConstants, load_image
|
||||
from seedsigner.gui.screens.screen import BaseScreen
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.view import View
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
# TODO: This early code is now outdated vis-a-vis Screen vs View distinctions
|
||||
class LogoScreen(BaseScreen):
|
||||
def __init__(self):
|
||||
@@ -29,49 +32,90 @@ class LogoScreen(BaseScreen):
|
||||
self.partner_logos[partner] = load_image(logo_url)
|
||||
|
||||
|
||||
def _run(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_random_partner(self) -> str:
|
||||
return self.partners[random.randrange(len(self.partners))]
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpeningSplashView(View):
|
||||
is_screenshot_renderer: bool = False
|
||||
force_partner_logos: bool|None = None
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
OpeningSplashScreen,
|
||||
is_screenshot_renderer=self.is_screenshot_renderer,
|
||||
force_partner_logos=self.force_partner_logos
|
||||
)
|
||||
|
||||
|
||||
|
||||
class OpeningSplashScreen(LogoScreen):
|
||||
def start(self):
|
||||
def __init__(self, is_screenshot_renderer=False, force_partner_logos=None):
|
||||
self.is_screenshot_renderer = is_screenshot_renderer
|
||||
self.force_partner_logos = force_partner_logos
|
||||
super().__init__()
|
||||
|
||||
|
||||
def _render(self):
|
||||
from PIL import Image
|
||||
from seedsigner.controller import Controller
|
||||
controller = Controller.get_instance()
|
||||
|
||||
# TODO: Fix for the screenshot generator. When generating screenshots for
|
||||
# multiple locales, there is a button still in the canvas from the previous
|
||||
# screenshot, even though the Renderer has been reconfigured and re-
|
||||
# instantiated. This is a hack to clear the screen for now.
|
||||
self.clear_screen()
|
||||
|
||||
show_partner_logos = Settings.get_instance().get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
|
||||
if self.force_partner_logos is not None:
|
||||
show_partner_logos = self.force_partner_logos
|
||||
|
||||
if show_partner_logos:
|
||||
logo_offset_y = -56
|
||||
else:
|
||||
logo_offset_y = 0
|
||||
|
||||
# Fade in alpha
|
||||
for i in range(250, -1, -25):
|
||||
self.logo.putalpha(255 - i)
|
||||
background = Image.new("RGBA", size=self.logo.size, color="black")
|
||||
self.renderer.canvas.paste(Image.alpha_composite(background, self.logo), (0, logo_offset_y))
|
||||
self.renderer.show_image()
|
||||
background = Image.new("RGBA", size=self.logo.size, color="black")
|
||||
if not self.is_screenshot_renderer:
|
||||
# Fade in alpha
|
||||
for i in range(250, -1, -25):
|
||||
self.logo.putalpha(255 - i)
|
||||
self.renderer.canvas.paste(Image.alpha_composite(background, self.logo), (0, logo_offset_y))
|
||||
self.renderer.show_image()
|
||||
else:
|
||||
# Skip animation for the screenshot generator
|
||||
self.renderer.canvas.paste(self.logo, (0, logo_offset_y))
|
||||
|
||||
# Display version num below SeedSigner logo
|
||||
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.TOP_NAV_TITLE_FONT_SIZE)
|
||||
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_top_nav_title_font_size())
|
||||
version = f"v{controller.VERSION}"
|
||||
(left, top, version_tw, version_th) = font.getbbox(version, anchor="lt")
|
||||
|
||||
# The logo png is 240x240, but the actual logo is 70px tall, vertically centered
|
||||
logo_height = 70
|
||||
version_x = int(self.renderer.canvas_width/2)
|
||||
version_y = int(self.canvas_height/2) + 35 + logo_offset_y + GUIConstants.COMPONENT_PADDING
|
||||
version_y = int(self.canvas_height/2) + int(logo_height/2) + logo_offset_y + GUIConstants.COMPONENT_PADDING
|
||||
self.renderer.draw.text(xy=(version_x, version_y), text=version, font=font, fill=GUIConstants.ACCENT_COLOR, anchor="mt")
|
||||
self.renderer.show_image()
|
||||
|
||||
if not self.is_screenshot_renderer:
|
||||
self.renderer.show_image()
|
||||
|
||||
if show_partner_logos:
|
||||
# Hold on the version num for a moment
|
||||
time.sleep(1)
|
||||
if not self.is_screenshot_renderer:
|
||||
# Hold on the version num for a moment
|
||||
time.sleep(1)
|
||||
|
||||
# Set up the partner logo
|
||||
partner_logo: Image.Image = self.partner_logos[self.get_random_partner()]
|
||||
font = Fonts.get_font(GUIConstants.TOP_NAV_TITLE_FONT_NAME, GUIConstants.BODY_FONT_SIZE)
|
||||
sponsor_text = "With support from:"
|
||||
font = Fonts.get_font(GUIConstants.get_top_nav_title_font_name(), GUIConstants.get_body_font_size())
|
||||
# TRANSLATOR_NOTE: This is on the opening splash screen, displayed above the HRF logo
|
||||
sponsor_text = _("With support from:")
|
||||
(left, top, tw, th) = font.getbbox(sponsor_text, anchor="lt")
|
||||
|
||||
x = int((self.renderer.canvas_width) / 2)
|
||||
@@ -87,12 +131,15 @@ class OpeningSplashScreen(LogoScreen):
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
time.sleep(2)
|
||||
if not self.is_screenshot_renderer:
|
||||
# Hold on the splash screen for a moment
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
|
||||
class ScreensaverScreen(LogoScreen):
|
||||
def __init__(self, buttons):
|
||||
from PIL import Image
|
||||
super().__init__()
|
||||
|
||||
self.buttons = buttons
|
||||
|
||||
+312
-228
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,20 @@
|
||||
import logging
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import SeedSignerIconConstants
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
|
||||
from seedsigner.gui.screens.screen import ButtonOption
|
||||
from seedsigner.models.settings import Settings, SettingsConstants, SettingsDefinition
|
||||
|
||||
from .view import View, Destination, MainMenuView
|
||||
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
|
||||
from seedsigner.models.settings import Settings, SettingsConstants, SettingsDefinition
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class SettingsMenuView(View):
|
||||
IO_TEST = "I/O test"
|
||||
DONATE = "Donate"
|
||||
IO_TEST = ButtonOption("I/O test")
|
||||
DONATE = ButtonOption("Donate")
|
||||
|
||||
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0):
|
||||
super().__init__()
|
||||
@@ -28,7 +29,7 @@ class SettingsMenuView(View):
|
||||
settings_entries = SettingsDefinition.get_settings_entries(
|
||||
visibility=self.visibility
|
||||
)
|
||||
button_data=[e.display_name for e in settings_entries]
|
||||
button_data=[ButtonOption(e.display_name) for e in settings_entries]
|
||||
|
||||
selected_button = 0
|
||||
if self.selected_attr:
|
||||
@@ -38,17 +39,17 @@ class SettingsMenuView(View):
|
||||
break
|
||||
|
||||
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
|
||||
title = "Settings"
|
||||
title = _("Settings")
|
||||
|
||||
# Set up the next nested level of menuing
|
||||
button_data.append(("Advanced", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
button_data.append(ButtonOption("Advanced", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
|
||||
button_data.append(self.IO_TEST)
|
||||
button_data.append(self.DONATE)
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
|
||||
title = "Advanced"
|
||||
title = _("Advanced")
|
||||
|
||||
# So far there are no real Developer options; disabling for now
|
||||
# button_data.append(("Developer Options", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
@@ -56,7 +57,7 @@ class SettingsMenuView(View):
|
||||
next_destination = None
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__DEVELOPER:
|
||||
title = "Dev Options"
|
||||
title = _("Dev Options")
|
||||
next_destination = None
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
@@ -114,8 +115,7 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
value, display_name = value
|
||||
else:
|
||||
display_name = value
|
||||
|
||||
button_data.append(display_name)
|
||||
button_data.append(ButtonOption(display_name))
|
||||
|
||||
if (type(initial_value) == list and value in initial_value) or value == initial_value:
|
||||
checked_buttons.append(i)
|
||||
@@ -191,6 +191,7 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
|
||||
class SettingsIngestSettingsQRView(View):
|
||||
def __init__(self, data: str):
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
super().__init__()
|
||||
|
||||
# May raise an Exception which will bubble up to the Controller to display to the
|
||||
@@ -200,15 +201,16 @@ class SettingsIngestSettingsQRView(View):
|
||||
self.settings.update(settings_update_dict)
|
||||
|
||||
if MicroSD.get_instance().is_inserted and self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED:
|
||||
self.status_message = "Persistent Settings enabled. Settings saved to SD card."
|
||||
self.status_message = _("Persistent Settings enabled. Settings saved to SD card.")
|
||||
else:
|
||||
self.status_message = "Settings updated in temporary memory"
|
||||
self.status_message = _("Settings updated in temporary memory")
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.settings_screens import SettingsQRConfirmationScreen
|
||||
self.run_screen(
|
||||
SettingsQRConfirmationScreen,
|
||||
title=_("Settings QR"),
|
||||
config_name=self.config_name,
|
||||
status_message=self.status_message,
|
||||
)
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from embit.descriptor import Descriptor
|
||||
from PIL import Image
|
||||
from PIL.ImageOps import autocontrast
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen)
|
||||
from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen,
|
||||
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
|
||||
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
|
||||
from seedsigner.helpers import embit_utils, mnemonic_generation
|
||||
from seedsigner.models.encode_qr import GenericStaticQrEncoder
|
||||
from seedsigner.gui.screens import RET_CODE__BACK_BUTTON, ButtonListScreen
|
||||
from seedsigner.gui.screens.screen import ButtonOption
|
||||
from seedsigner.helpers import mnemonic_generation
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView
|
||||
@@ -25,19 +18,20 @@ from .view import View, Destination, BackStackView
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class ToolsMenuView(View):
|
||||
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
|
||||
DICE = ("New seed", FontAwesomeIconConstants.DICE)
|
||||
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
||||
ADDRESS_EXPLORER = "Address Explorer"
|
||||
VERIFY_ADDRESS = "Verify address"
|
||||
IMAGE = ButtonOption("New seed", FontAwesomeIconConstants.CAMERA)
|
||||
DICE = ButtonOption("New seed", FontAwesomeIconConstants.DICE)
|
||||
KEYBOARD = ButtonOption("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
||||
ADDRESS_EXPLORER = ButtonOption("Address Explorer")
|
||||
VERIFY_ADDRESS = ButtonOption("Verify Address")
|
||||
|
||||
def run(self):
|
||||
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Tools",
|
||||
title=_("Tools"),
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
)
|
||||
@@ -68,6 +62,7 @@ class ToolsMenuView(View):
|
||||
****************************************************************************"""
|
||||
class ToolsImageEntropyLivePreviewView(View):
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsImageEntropyLivePreviewScreen
|
||||
self.controller.image_entropy_preview_frames = None
|
||||
ret = ToolsImageEntropyLivePreviewScreen().display()
|
||||
|
||||
@@ -81,6 +76,9 @@ class ToolsImageEntropyLivePreviewView(View):
|
||||
|
||||
class ToolsImageEntropyFinalImageView(View):
|
||||
def run(self):
|
||||
from PIL import Image
|
||||
from PIL.ImageOps import autocontrast
|
||||
from seedsigner.gui.screens.tools_screens import ToolsImageEntropyFinalImageScreen
|
||||
if not self.controller.image_entropy_final_image:
|
||||
from seedsigner.hardware.camera import Camera
|
||||
# Take the final full-res image
|
||||
@@ -99,7 +97,7 @@ class ToolsImageEntropyFinalImageView(View):
|
||||
).crop(
|
||||
(120, 0, 600, 480)
|
||||
).resize(
|
||||
(self.canvas_width, self.canvas_height), Image.BICUBIC
|
||||
(self.canvas_width, self.canvas_height), Image.Resampling.BICUBIC
|
||||
)
|
||||
|
||||
ret = ToolsImageEntropyFinalImageScreen(
|
||||
@@ -116,23 +114,21 @@ class ToolsImageEntropyFinalImageView(View):
|
||||
|
||||
|
||||
class ToolsImageEntropyMnemonicLengthView(View):
|
||||
TWELVE_WORDS = ButtonOption("12 words", return_data=12)
|
||||
TWENTYFOUR_WORDS = ButtonOption("24 words", return_data=24)
|
||||
|
||||
def run(self):
|
||||
TWELVE_WORDS = "12 words"
|
||||
TWENTYFOUR_WORDS = "24 words"
|
||||
button_data = [TWELVE_WORDS, TWENTYFOUR_WORDS]
|
||||
button_data = [self.TWELVE_WORDS, self.TWENTYFOUR_WORDS]
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
title="Mnemonic Length?",
|
||||
title=_("Mnemonic Length?"),
|
||||
button_data=button_data,
|
||||
).display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if button_data[selected_menu_num] == TWELVE_WORDS:
|
||||
mnemonic_length = 12
|
||||
else:
|
||||
mnemonic_length = 24
|
||||
mnemonic_length = button_data[selected_menu_num].return_data
|
||||
|
||||
preview_images = self.controller.image_entropy_preview_frames
|
||||
seed_entropy_image = self.controller.image_entropy_final_image
|
||||
@@ -189,12 +185,20 @@ class ToolsImageEntropyMnemonicLengthView(View):
|
||||
****************************************************************************"""
|
||||
class ToolsDiceEntropyMnemonicLengthView(View):
|
||||
def run(self):
|
||||
TWELVE = f"12 words ({mnemonic_generation.DICE__NUM_ROLLS__12WORD} rolls)"
|
||||
TWENTY_FOUR = f"24 words ({mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls)"
|
||||
|
||||
# Since we're dynamically building the ButtonOption button_labels here, it's too
|
||||
# awkward to use the usual class-level attr approach.
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts the number of dice rolls needed for a 12-word mnemonic
|
||||
twelve = _("12 words ({} rolls)").format(mnemonic_generation.DICE__NUM_ROLLS__12WORD)
|
||||
TWELVE = ButtonOption(twelve, return_data=mnemonic_generation.DICE__NUM_ROLLS__12WORD)
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts the number of dice rolls needed for a 24-word mnemonic
|
||||
twenty_four = _("24 words ({} rolls)").format(mnemonic_generation.DICE__NUM_ROLLS__24WORD)
|
||||
TWENTY_FOUR = ButtonOption(twenty_four, return_data=mnemonic_generation.DICE__NUM_ROLLS__24WORD)
|
||||
|
||||
button_data = [TWELVE, TWENTY_FOUR]
|
||||
selected_menu_num = ButtonListScreen(
|
||||
title="Mnemonic Length",
|
||||
title=_("Mnemonic Length"),
|
||||
is_bottom_list=True,
|
||||
is_button_text_centered=True,
|
||||
button_data=button_data,
|
||||
@@ -218,6 +222,7 @@ class ToolsDiceEntropyEntryView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen
|
||||
ret = ToolsDiceEntropyEntryScreen(
|
||||
return_after_n_chars=self.total_rolls,
|
||||
).display()
|
||||
@@ -240,15 +245,15 @@ class ToolsDiceEntropyEntryView(View):
|
||||
Calc final word Views
|
||||
****************************************************************************"""
|
||||
class ToolsCalcFinalWordNumWordsView(View):
|
||||
TWELVE = "12 words"
|
||||
TWENTY_FOUR = "24 words"
|
||||
TWELVE = ButtonOption("12 words", return_data=12)
|
||||
TWENTY_FOUR = ButtonOption("24 words", return_data=24)
|
||||
|
||||
def run(self):
|
||||
button_data = [self.TWELVE, self.TWENTY_FOUR]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Mnemonic Length",
|
||||
title=_("Mnemonic Length"),
|
||||
is_bottom_list=True,
|
||||
is_button_text_centered=True,
|
||||
button_data=button_data,
|
||||
@@ -257,22 +262,24 @@ class ToolsCalcFinalWordNumWordsView(View):
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TWELVE:
|
||||
self.controller.storage.init_pending_mnemonic(12)
|
||||
self.controller.storage.init_pending_mnemonic(button_data[selected_menu_num].return_data)
|
||||
|
||||
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
|
||||
elif button_data[selected_menu_num] == self.TWENTY_FOUR:
|
||||
self.controller.storage.init_pending_mnemonic(24)
|
||||
|
||||
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
|
||||
|
||||
|
||||
class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
# TRANSLATOR_NOTE: Label to gather entropy through coin tosses
|
||||
COIN_FLIPS = ButtonOption("Coin flip entropy")
|
||||
|
||||
# TRANSLATOR_NOTE: Label to gather entropy through user specified BIP-39 word
|
||||
SELECT_WORD = ButtonOption("Word selection entropy")
|
||||
|
||||
# TRANSLATOR_NOTE: Label to allow user to default entropy as all-zeros
|
||||
ZEROS = ButtonOption("Finalize with zeros")
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordFinalizePromptScreen
|
||||
mnemonic = self.controller.storage.pending_mnemonic
|
||||
mnemonic_length = len(mnemonic)
|
||||
if mnemonic_length == 12:
|
||||
@@ -280,11 +287,7 @@ class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
else:
|
||||
num_entropy_bits = 3
|
||||
|
||||
COIN_FLIPS = "Coin flip entropy"
|
||||
SELECT_WORD = f"Word selection entropy"
|
||||
ZEROS = "Finalize with zeros"
|
||||
|
||||
button_data = [COIN_FLIPS, SELECT_WORD, ZEROS]
|
||||
button_data = [self.COIN_FLIPS, self.SELECT_WORD, self.ZEROS]
|
||||
selected_menu_num = ToolsCalcFinalWordFinalizePromptScreen(
|
||||
mnemonic_length=mnemonic_length,
|
||||
num_entropy_bits=num_entropy_bits,
|
||||
@@ -294,15 +297,15 @@ class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == COIN_FLIPS:
|
||||
elif button_data[selected_menu_num] == self.COIN_FLIPS:
|
||||
return Destination(ToolsCalcFinalWordCoinFlipsView)
|
||||
|
||||
elif button_data[selected_menu_num] == SELECT_WORD:
|
||||
elif button_data[selected_menu_num] == self.SELECT_WORD:
|
||||
# Clear the final word slot, just in case we're returning via BACK button
|
||||
self.controller.storage.update_pending_mnemonic(None, mnemonic_length - 1)
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True, cur_word_index=mnemonic_length - 1))
|
||||
|
||||
elif button_data[selected_menu_num] == ZEROS:
|
||||
elif button_data[selected_menu_num] == self.ZEROS:
|
||||
# User skipped the option to select a final word to provide last bits of
|
||||
# entropy. We'll insert all zeros and piggy-back on the coin flip attr
|
||||
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
@@ -313,6 +316,7 @@ class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
|
||||
class ToolsCalcFinalWordCoinFlipsView(View):
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCoinFlipEntryScreen
|
||||
mnemonic_length = len(self.controller.storage.pending_mnemonic)
|
||||
|
||||
if mnemonic_length == 12:
|
||||
@@ -333,6 +337,8 @@ class ToolsCalcFinalWordCoinFlipsView(View):
|
||||
|
||||
|
||||
class ToolsCalcFinalWordShowFinalWordView(View):
|
||||
NEXT = ButtonOption("Next")
|
||||
|
||||
def __init__(self, coin_flips: str = None):
|
||||
super().__init__()
|
||||
# Construct the actual final word. The user's selected_final_word
|
||||
@@ -385,11 +391,15 @@ class ToolsCalcFinalWordShowFinalWordView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
NEXT = "Next"
|
||||
button_data = [NEXT]
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordScreen
|
||||
button_data = [self.NEXT]
|
||||
|
||||
# TRANSLATOR_NOTE: label to calculate the last word of a BIP-39 mnemonic seed phrase
|
||||
title = _("Final Word Calc")
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ToolsCalcFinalWordScreen,
|
||||
title="Final Word Calc",
|
||||
title=title,
|
||||
button_data=button_data,
|
||||
selected_final_word=self.selected_final_word,
|
||||
selected_final_bits=self.selected_final_bits,
|
||||
@@ -400,20 +410,22 @@ class ToolsCalcFinalWordShowFinalWordView(View):
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == NEXT:
|
||||
elif button_data[selected_menu_num] == self.NEXT:
|
||||
return Destination(ToolsCalcFinalWordDoneView)
|
||||
|
||||
|
||||
|
||||
class ToolsCalcFinalWordDoneView(View):
|
||||
LOAD = ButtonOption("Load seed")
|
||||
DISCARD = ButtonOption("Discard", button_label_color="red")
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen
|
||||
mnemonic = self.controller.storage.pending_mnemonic
|
||||
mnemonic_word_length = len(mnemonic)
|
||||
final_word = mnemonic[-1]
|
||||
|
||||
LOAD = "Load seed"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
button_data = [LOAD, DISCARD]
|
||||
button_data = [self.LOAD, self.DISCARD]
|
||||
|
||||
selected_menu_num = ToolsCalcFinalWordDoneScreen(
|
||||
final_word=final_word,
|
||||
@@ -427,37 +439,39 @@ class ToolsCalcFinalWordDoneView(View):
|
||||
|
||||
self.controller.storage.convert_pending_mnemonic_to_pending_seed()
|
||||
|
||||
if button_data[selected_menu_num] == LOAD:
|
||||
if button_data[selected_menu_num] == self.LOAD:
|
||||
return Destination(SeedFinalizeView)
|
||||
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
elif button_data[selected_menu_num] == self.DISCARD:
|
||||
return Destination(SeedDiscardView)
|
||||
|
||||
|
||||
|
||||
"""****************************************************************************
|
||||
Address Explorer Views
|
||||
****************************************************************************"""
|
||||
class ToolsAddressExplorerSelectSourceView(View):
|
||||
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)
|
||||
TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
|
||||
SCAN_SEED = ButtonOption("Scan a seed", SeedSignerIconConstants.QRCODE)
|
||||
SCAN_DESCRIPTOR = ButtonOption("Scan wallet descriptor", SeedSignerIconConstants.QRCODE)
|
||||
TYPE_12WORD = ButtonOption("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD, return_data=12)
|
||||
TYPE_24WORD = ButtonOption("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD, return_data=24)
|
||||
TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
|
||||
def run(self):
|
||||
from seedsigner.controller import Controller
|
||||
|
||||
seeds = self.controller.storage.seeds
|
||||
button_data = []
|
||||
for seed in seeds:
|
||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT))
|
||||
button_data.append(ButtonOption(button_str, SeedSignerIconConstants.FINGERPRINT))
|
||||
button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD]
|
||||
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(self.TYPE_ELECTRUM)
|
||||
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Address Explorer",
|
||||
title=_("Address Explorer"),
|
||||
button_data=button_data,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
@@ -491,10 +505,7 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
|
||||
elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
if button_data[selected_menu_num] == self.TYPE_12WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||
else:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||
self.controller.storage.init_pending_mnemonic(num_words=button_data[selected_menu_num].return_data)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
@@ -504,8 +515,11 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
|
||||
|
||||
class ToolsAddressExplorerAddressTypeView(View):
|
||||
RECEIVE = "Receive Addresses"
|
||||
CHANGE = "Change Addresses"
|
||||
# TRANSLATOR_NOTE: label for addresses where others send us incoming payments
|
||||
RECEIVE = ButtonOption("Receive Addresses")
|
||||
|
||||
# TRANSLATOR_NOTE: label for addresses that collect the change from our own outgoing payments
|
||||
CHANGE = ButtonOption("Change Addresses")
|
||||
|
||||
|
||||
def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None):
|
||||
@@ -541,6 +555,7 @@ class ToolsAddressExplorerAddressTypeView(View):
|
||||
elif seed_derivation_override:
|
||||
derivation_path = seed_derivation_override
|
||||
else:
|
||||
from seedsigner.helpers import embit_utils
|
||||
derivation_path = embit_utils.get_standard_derivation_path(
|
||||
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
|
||||
wallet_type=SettingsConstants.SINGLE_SIG,
|
||||
@@ -557,11 +572,13 @@ class ToolsAddressExplorerAddressTypeView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsAddressExplorerAddressTypeScreen
|
||||
data = self.controller.address_explorer_data
|
||||
|
||||
wallet_descriptor_display_name = None
|
||||
if "wallet_descriptor" in data:
|
||||
wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "")
|
||||
wallet_descriptor_display_name = " / ".join(wallet_descriptor_display_name.split(" of ")) # i18n w/o l10n since coming from non-l10n embit
|
||||
|
||||
script_type = data["script_type"] if "script_type" in data else None
|
||||
|
||||
@@ -620,7 +637,9 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
else:
|
||||
try:
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Calculating addrs...")
|
||||
from seedsigner.helpers import embit_utils
|
||||
# TRANSLATOR_NOTE: a status message that our payment addresses are being calculated
|
||||
self.loading_screen = LoadingScreenThread(text=_("Calculating addrs..."))
|
||||
self.loading_screen.start()
|
||||
|
||||
if addr_storage_key not in data:
|
||||
@@ -636,9 +655,10 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
data[addr_storage_key].append(address)
|
||||
else:
|
||||
# TODO: Custom derivation path
|
||||
raise Exception("Custom Derivation address explorer not yet implemented")
|
||||
|
||||
raise Exception(_("Custom Derivation address explorer not yet implemented"))
|
||||
|
||||
elif "wallet_descriptor" in data:
|
||||
from embit.descriptor import Descriptor
|
||||
descriptor: Descriptor = data["wallet_descriptor"]
|
||||
if descriptor.is_basic_multisig:
|
||||
for i in range(self.start_index, self.start_index + addrs_per_screen):
|
||||
@@ -647,7 +667,7 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
data[addr_storage_key].append(address)
|
||||
|
||||
else:
|
||||
raise Exception("Single sig descriptors not yet supported")
|
||||
raise Exception(_("Single sig descriptors not yet supported"))
|
||||
finally:
|
||||
# Everything is set. Stop the loading screen
|
||||
self.loading_screen.stop()
|
||||
@@ -663,16 +683,18 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
end_digits = -5
|
||||
else:
|
||||
end_digits = -4
|
||||
button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}")
|
||||
button_data.append(ButtonOption(f"{cur_index}:{address[:8]}...{address[end_digits:]}", active_button_label=f"{cur_index}:{address}"))
|
||||
|
||||
button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
# TRANSLATOR_NOTE: Insert the number of addrs displayed per screen (e.g. "Next 10")
|
||||
button_label = _("Next {}").format(addrs_per_screen)
|
||||
button_data.append(ButtonOption(button_label, right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="{} Addrs".format("Receive" if not self.is_change else "Change"),
|
||||
title=_("Receive Addrs") if not self.is_change else _("Change Addrs"),
|
||||
button_data=button_data,
|
||||
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||
button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4,
|
||||
button_font_size=GUIConstants.get_button_font_size() + 4,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
selected_button=self.selected_button_index,
|
||||
@@ -695,6 +717,7 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
|
||||
|
||||
class ToolsAddressExplorerAddressView(View):
|
||||
# TODO: pull address str from controller.address_explorer_data and pass addr_storage_key and addr_index instead
|
||||
def __init__(self, index: int, address: str, is_change: bool, start_index: int, parent_initial_scroll: int = 0):
|
||||
super().__init__()
|
||||
self.index = index
|
||||
@@ -706,6 +729,8 @@ class ToolsAddressExplorerAddressView(View):
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import QRDisplayScreen
|
||||
from seedsigner.models.encode_qr import GenericStaticQrEncoder
|
||||
|
||||
qr_encoder = GenericStaticQrEncoder(data=self.address)
|
||||
self.run_screen(
|
||||
QRDisplayScreen,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from typing import Type
|
||||
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.gui.components import 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.gui.screens.screen import BaseScreen, ButtonOption, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
from seedsigner.models.settings_definition import SettingsDefinition
|
||||
from seedsigner.models.threads import BaseThread
|
||||
@@ -17,6 +19,7 @@ class BackStackView:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Views contain the biz logic to handle discrete tasks, exactly analogous to a Flask
|
||||
request/response function or a Django View. Each page/screen displayed to the user
|
||||
@@ -181,18 +184,17 @@ class Destination:
|
||||
#
|
||||
#########################################################################################
|
||||
class MainMenuView(View):
|
||||
SCAN = ("Scan", SeedSignerIconConstants.SCAN)
|
||||
SEEDS = ("Seeds", SeedSignerIconConstants.SEEDS)
|
||||
TOOLS = ("Tools", SeedSignerIconConstants.TOOLS)
|
||||
SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS)
|
||||
|
||||
SCAN = ButtonOption("Scan", SeedSignerIconConstants.SCAN)
|
||||
SEEDS = ButtonOption("Seeds", SeedSignerIconConstants.SEEDS)
|
||||
TOOLS = ButtonOption("Tools", SeedSignerIconConstants.TOOLS)
|
||||
SETTINGS = ButtonOption("Settings", SeedSignerIconConstants.SETTINGS)
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import MainMenuScreen
|
||||
button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS]
|
||||
selected_menu_num = self.run_screen(
|
||||
MainMenuScreen,
|
||||
title="Home",
|
||||
title=_("Home"),
|
||||
button_data=button_data,
|
||||
)
|
||||
|
||||
@@ -218,14 +220,14 @@ class MainMenuView(View):
|
||||
|
||||
|
||||
class PowerOptionsView(View):
|
||||
RESET = ("Restart", SeedSignerIconConstants.RESTART)
|
||||
POWER_OFF = ("Power Off", SeedSignerIconConstants.POWER)
|
||||
RESET = ButtonOption("Restart", SeedSignerIconConstants.RESTART)
|
||||
POWER_OFF = ButtonOption("Power Off", SeedSignerIconConstants.POWER)
|
||||
|
||||
def run(self):
|
||||
button_data = [self.RESET, self.POWER_OFF]
|
||||
selected_menu_num = self.run_screen(
|
||||
LargeButtonScreen,
|
||||
title="Reset / Power",
|
||||
title=_("Reset / Power"),
|
||||
show_back_button=True,
|
||||
button_data=button_data
|
||||
)
|
||||
@@ -268,38 +270,26 @@ class RestartView(View):
|
||||
|
||||
class PowerOffView(View):
|
||||
def run(self):
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
self.run_screen(PowerOffNotRequiredScreen)
|
||||
return Destination(BackStackView)
|
||||
else:
|
||||
thread = PowerOffView.PowerOffThread()
|
||||
thread.start()
|
||||
self.run_screen(PowerOffScreen)
|
||||
|
||||
|
||||
class PowerOffThread(BaseThread):
|
||||
def run(self):
|
||||
import time
|
||||
from subprocess import call
|
||||
while self.keep_running:
|
||||
time.sleep(5)
|
||||
call("sudo shutdown --poweroff now", shell=True)
|
||||
self.run_screen(PowerOffNotRequiredScreen)
|
||||
return Destination(BackStackView)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotYetImplementedView(View):
|
||||
text: str = "This is still on our to-do list!"
|
||||
"""
|
||||
Temporary View to use during dev.
|
||||
"""
|
||||
text: str = _mft("This is still on our to-do list!")
|
||||
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Work In Progress",
|
||||
status_headline="Not Yet Implemented",
|
||||
title=_("Work In Progress"),
|
||||
status_headline=_("Not Yet Implemented"),
|
||||
text=self.text,
|
||||
button_data=["Back to Main Menu"],
|
||||
button_data=[ButtonOption("Back to Main Menu")],
|
||||
)
|
||||
|
||||
return Destination(MainMenuView)
|
||||
@@ -308,13 +298,12 @@ class NotYetImplementedView(View):
|
||||
|
||||
@dataclass
|
||||
class ErrorView(View):
|
||||
title: str = "Error"
|
||||
title: str = _mft("Error")
|
||||
show_back_button: bool = True
|
||||
status_headline: str = None
|
||||
text: str = None
|
||||
button_text: str = None
|
||||
next_destination: Destination = field(default_factory=lambda: Destination(MainMenuView, clear_history=True))
|
||||
|
||||
next_destination: Destination = None
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
@@ -322,30 +311,30 @@ class ErrorView(View):
|
||||
title=self.title,
|
||||
status_headline=self.status_headline,
|
||||
text=self.text,
|
||||
button_data=[self.button_text],
|
||||
button_data=[ButtonOption(self.button_text)],
|
||||
show_back_button=self.show_back_button,
|
||||
)
|
||||
|
||||
return self.next_destination
|
||||
return self.next_destination if self.next_destination else Destination(MainMenuView, clear_history=True)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkMismatchErrorView(ErrorView):
|
||||
title: str = "Network Mismatch"
|
||||
show_back_button: bool = False
|
||||
button_text: str = "Change Setting"
|
||||
next_destination: Destination = None
|
||||
|
||||
derivation_path: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
|
||||
self.title: str = _("Network Mismatch")
|
||||
self.show_back_button: bool = False
|
||||
self.button_text: str = _("Change Setting")
|
||||
self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True)
|
||||
super().__post_init__()
|
||||
if not self.text:
|
||||
self.text = f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match current action."
|
||||
|
||||
if not self.next_destination:
|
||||
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
|
||||
self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True)
|
||||
# TRANSLATOR_NOTE: Inserts mainnet/testnet/regtest and derivation path
|
||||
self.text = _("Current network setting ({}) doesn't match {}.").format(
|
||||
self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK),
|
||||
self.derivation_path,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -357,11 +346,9 @@ class UnhandledExceptionView(View):
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
DireWarningScreen,
|
||||
title="System Error",
|
||||
title=_("System Error"),
|
||||
status_headline=self.error[0],
|
||||
text=self.error[1] + "\n" + self.error[2],
|
||||
button_data=["OK"],
|
||||
show_back_button=False,
|
||||
allow_text_overflow=True, # Fit what we can, let the rest go off the edges
|
||||
)
|
||||
|
||||
@@ -371,21 +358,25 @@ class UnhandledExceptionView(View):
|
||||
|
||||
@dataclass
|
||||
class OptionDisabledView(View):
|
||||
UPDATE_SETTING = "Update Setting"
|
||||
DONE = "Done"
|
||||
UPDATE_SETTING = ButtonOption("Update Setting")
|
||||
DONE = ButtonOption("Done")
|
||||
settings_attr: str
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.settings_entry = SettingsDefinition.get_settings_entry(self.settings_attr)
|
||||
self.error_msg = f"\"{self.settings_entry.display_name}\" is currently disabled in Settings."
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts the name of a settings option (e.g. "Persistent Settings" is currently...)
|
||||
self.error_msg = _("\"{}\" is currently disabled in Settings.").format(
|
||||
_(self.settings_entry.display_name),
|
||||
)
|
||||
|
||||
|
||||
def run(self):
|
||||
button_data = [self.UPDATE_SETTING, self.DONE]
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="Option Disabled",
|
||||
title=_("Option Disabled"),
|
||||
status_headline=None,
|
||||
text=self.error_msg,
|
||||
button_data=button_data,
|
||||
@@ -398,26 +389,3 @@ class OptionDisabledView(View):
|
||||
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_attr), clear_history=True)
|
||||
else:
|
||||
return Destination(MainMenuView, clear_history=True)
|
||||
|
||||
|
||||
|
||||
class RemoveMicroSDWarningView(View):
|
||||
"""
|
||||
Warning to remove the microsd
|
||||
"""
|
||||
def __init__(self, next_view: View):
|
||||
super().__init__()
|
||||
self.next_view = next_view
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Security Tip",
|
||||
status_icon_name=FontAwesomeIconConstants.SDCARD,
|
||||
status_headline="",
|
||||
text="For maximum security,\nremove the MicroSD card\nbefore continuing.",
|
||||
show_back_button=False,
|
||||
button_data=["Continue"],
|
||||
)
|
||||
|
||||
return Destination(self.next_view, clear_history=True)
|
||||
|
||||
+27
-6
@@ -2,6 +2,7 @@
|
||||
|
||||
The tests are designed to be run on non-Raspi hardware.
|
||||
|
||||
## Setup
|
||||
On your testing machine you'll have to install:
|
||||
```bash
|
||||
# general dependencies
|
||||
@@ -16,6 +17,14 @@ Then make the `seedsigner` python module visible/importable to the tests by inst
|
||||
pip3 install -e .
|
||||
```
|
||||
|
||||
## Running all tests, calculating overall test coverage
|
||||
tldr: just run the convenience script from the project root:
|
||||
|
||||
```bash
|
||||
./tests/run_full_coverage.sh
|
||||
```
|
||||
|
||||
## Running tests manually
|
||||
Run the whole test suite:
|
||||
```
|
||||
pytest
|
||||
@@ -45,18 +54,30 @@ Annoying complications:
|
||||
* Better idea: use a proper logger in the test file and use one of the above options to display logs
|
||||
|
||||
|
||||
### Test Coverage
|
||||
## Screenshot generator
|
||||
The screenshot generator is meant to mostly be a utility and not really part of the test suite. However,
|
||||
it is actually implemented to be run by `pytest`.
|
||||
|
||||
see: [Screenshot generator README](screenshot_generator/README.md)
|
||||
|
||||
|
||||
## Generate coverage manually
|
||||
Run tests and generate test coverage
|
||||
```
|
||||
```bash
|
||||
coverage run -m pytest
|
||||
```
|
||||
|
||||
Show the resulting test coverage details:
|
||||
The screenshots can generate their own separate coverage report:
|
||||
```bash
|
||||
coverage run -m pytest tests/screenshot_generator/generator.py --locale es
|
||||
```
|
||||
|
||||
Show the resulting test coverage details:
|
||||
```bash
|
||||
coverage report
|
||||
```
|
||||
|
||||
Generate the html overview:
|
||||
```
|
||||
Generate the interactive html report:
|
||||
```bash
|
||||
coverage html
|
||||
```
|
||||
```
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Clean up any prior coverage results
|
||||
coverage erase
|
||||
|
||||
# Run the full test suite; `--parallel` writes coverage results to a separate file
|
||||
# (otherwise it'll be overwritten in the next step)
|
||||
coverage run --parallel -m pytest
|
||||
|
||||
# Generate screenshots (only need to run for one locale to assess coverage)
|
||||
coverage run --parallel -m pytest tests/screenshot_generator/generator.py --locale es
|
||||
|
||||
# Combine the above coverage results
|
||||
coverage combine
|
||||
|
||||
# Show the report in the terminal
|
||||
coverage report
|
||||
|
||||
# Generate the interactive html report
|
||||
coverage html
|
||||
@@ -1,8 +1,21 @@
|
||||
# Screenshot Generator
|
||||
|
||||
From the project root, run:
|
||||
```
|
||||
```bash
|
||||
# Generate screenshots for a specific locale
|
||||
pytest tests/screenshot_generator/generator.py --locale es
|
||||
|
||||
# Generate screenshots for all supported locales
|
||||
pytest tests/screenshot_generator/generator.py
|
||||
```
|
||||
|
||||
You can also run a `coverage` report to see exactly what the screenshots are and are not hitting:
|
||||
```bash
|
||||
coverage erase
|
||||
coverage run -m pytest tests/screenshot_generator/generator.py --locale es && coverage combine && coverage report
|
||||
|
||||
# Generate the interactive html report
|
||||
coverage html
|
||||
```
|
||||
|
||||
Writes the screenshots to a dir in the project root: `seedsigner-screenshots`.
|
||||
|
||||
@@ -1,47 +1,112 @@
|
||||
import embit
|
||||
import pathlib
|
||||
import pytest
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from seedsigner.helpers import embit_utils
|
||||
|
||||
from embit import compact
|
||||
from embit.psbt import PSBT, OutputScope
|
||||
from embit.script import Script
|
||||
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.models.psbt_parser import OPCODES, PSBTParser
|
||||
|
||||
|
||||
# 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.views.screensaver.ScreensaverScreen'] = MagicMock()
|
||||
sys.modules['RPi'] = MagicMock()
|
||||
sys.modules['RPi.GPIO'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
|
||||
# Force the screenshots to mimic Pi Zero's output without libraqm
|
||||
patch('PIL.ImageFont.core.HAVE_RAQM', False).start()
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen
|
||||
from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.psbt_parser import OPCODES, PSBTParser
|
||||
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, seed_views, settings_views, tools_views)
|
||||
from seedsigner.views.screensaver import OpeningSplashView
|
||||
from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View
|
||||
|
||||
from .utils import ScreenshotComplete, ScreenshotRenderer
|
||||
from .utils import ScreenshotComplete, ScreenshotConfig, ScreenshotRenderer
|
||||
|
||||
import warnings; warnings.warn = lambda *args, **kwargs: None
|
||||
|
||||
|
||||
|
||||
def test_generate_screenshots(target_locale):
|
||||
# Dynamically generate a pytest test run for each locale
|
||||
@pytest.mark.parametrize("locale", [x for x, y in SettingsConstants.ALL_LOCALES])
|
||||
def test_generate_all(locale, target_locale):
|
||||
"""
|
||||
`target_locale` is a fixture created in conftest.py via the `--locale` command line arg.
|
||||
|
||||
Optionally skips all other locales.
|
||||
"""
|
||||
if target_locale and locale != target_locale:
|
||||
pytest.skip(f"Skipping {locale}")
|
||||
|
||||
generate_screenshots(locale)
|
||||
|
||||
|
||||
|
||||
"""**************************************************************************************
|
||||
Set up global test data that will be re-used across a variety of screenshots and for
|
||||
all locales.
|
||||
**************************************************************************************"""
|
||||
BASE64_PSBT_1 = """cHNidP8BAP06AQIAAAAC5l4E3oEjI+H0im8t/K2nLmF5iJFdKEiuQs8ESveWJKcAAAAAAP3///8iBZMRhYIq4s/LmnTmKBi79M8ITirmsbO++63evK4utwAAAAAA/f///wZYQuoDAAAAACIAIAW5jm3UnC5fyjKCUZ8LTzjENtb/ioRTaBMXeSXsB3n+bK2fCgAAAAAWABReJY7akT1+d+jx475yBRWORdBd7VxbUgUAAAAAFgAU4wj9I/jB3GjNQudNZAca+7g9R16iWtYOAAAAABYAFIotPApLZlfscg8f3ppKqO3qA5nv7BnMFAAAAAAiACAs6SGc8qv4FwuNl0G0SpMZG8ODUEk5RXiWUcuzzw5iaRSfAhMAAAAAIgAgW0f5QxQIgVCGQqKzsvfkXZjUxdFop5sfez6Pt8mUbmZ1AgAAAAEAkgIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BQIRAgEB/////wJAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQErQL5AJQAAAAAiACCET6KNi75K8K4a2BYS4ZT+N4s8WwOBKOmOohRYkGHV0QEFR1EhArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEIQNDxn7PjUzvsP6KYw4s7dmoZE0qO1K6MaM+2ScRZ7hyxFKuIgYCsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEHCK94akwAACAAQAAgAAAAIACAACAAAAAAAMAAAAAAQCSAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8FAhACAQH/////AkC+QCUAAAAAIgAghE+ijYu+SvCuGtgWEuGU/jeLPFsDgSjpjqIUWJBh1dEAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QAAAAABAStAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAQVHUSECsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQhA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEUq4iBgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDQ8Z+z41M77D+imMOLO3ZqGRNKjtSujGjPtknEWe4csQcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAABAUdRIQJ5XLCBS0hdo4NANq4lNhimzhyHj7dvObmPAwNj8L2xASEC9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI1SriICAnlcsIFLSF2jg0A2riU2GKbOHIePt285uY8DA2PwvbEBHCK94akwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAAAAAAEBR1EhAgpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrIQOwLg0emiQbbxafIh69Xjtpj4eclsMhKq1y/7vYDdE7LVKuIgICCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ouscc8XaCjAAAIABAACAAAAAgAIAAIAAAAAABQAAACICA7AuDR6aJBtvFp8iHr1eO2mPh5yWwyEqrXL/u9gN0TstHCK94akwAACAAQAAgAAAAIACAACAAAAAAAUAAAAAAQFHUSECk50GLh/YhZaLJkDq/dugU3H/WvE6rTgQuY6N57pI4ykhA/H8MdLVP9SA/Hg8l3hvibSaC1bCBzwz7kTW+rsEZ8uFUq4iAgKTnQYuH9iFlosmQOr926BTcf9a8TqtOBC5jo3nukjjKRxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAGAAAAIgID8fwx0tU/1ID8eDyXeG+JtJoLVsIHPDPuRNb6uwRny4UcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAABgAAAAA="""
|
||||
mnemonic_12b = ["abandon"] * 11 + ["about"]
|
||||
seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
|
||||
def add_op_return_to_psbt(psbt: PSBT, raw_payload_data: bytes):
|
||||
data = (compact.to_bytes(OPCODES.OP_RETURN) +
|
||||
compact.to_bytes(OPCODES.OP_PUSHDATA1) +
|
||||
compact.to_bytes(len(raw_payload_data)) +
|
||||
raw_payload_data)
|
||||
script = Script(data)
|
||||
output = OutputScope()
|
||||
output.script_pubkey = script
|
||||
output.value = 0
|
||||
psbt.outputs.append(output)
|
||||
return psbt.to_string()
|
||||
|
||||
# Prep a PSBT with a human-readable OP_RETURN
|
||||
raw_payload_data = "Chancellor on the brink of third bailout for banks".encode()
|
||||
psbt = PSBT.from_base64(BASE64_PSBT_1)
|
||||
|
||||
# Simplify the output side
|
||||
output = psbt.outputs[-1]
|
||||
psbt.outputs.clear()
|
||||
psbt.outputs.append(output)
|
||||
assert len(psbt.outputs) == 1
|
||||
BASE64_PSBT_WITH_OP_RETURN_TEXT = add_op_return_to_psbt(psbt, raw_payload_data)
|
||||
|
||||
# Prep a PSBT with a (repeatably) random 80-byte OP_RETURN
|
||||
random.seed(6102)
|
||||
BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES = add_op_return_to_psbt(PSBT.from_base64(BASE64_PSBT_1), random.randbytes(80))
|
||||
|
||||
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()
|
||||
seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
seed_24 = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
seed_24_w_passphrase = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
|
||||
MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx"""
|
||||
|
||||
|
||||
|
||||
def generate_screenshots(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).
|
||||
@@ -58,323 +123,358 @@ def test_generate_screenshots(target_locale):
|
||||
Renderer.configure_instance = Mock()
|
||||
Renderer.get_instance = Mock(return_value=screenshot_renderer)
|
||||
|
||||
# Additional mocks needed
|
||||
PowerOffView.PowerOffThread = Mock() # Don't let this View actually send the `shutdown` command!
|
||||
|
||||
controller = Controller.get_instance()
|
||||
def setup_screenshots(locale: str) -> dict[str, list[ScreenshotConfig]]:
|
||||
""" Set up some test data that we'll need in the `Controller` for certain Views """
|
||||
# Must reset the Controller so each locale gets a fresh start
|
||||
Controller.reset_instance()
|
||||
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, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
seed_24_w_passphrase = 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.seeds.append(seed_24)
|
||||
controller.storage.set_pending_seed(seed_24_w_passphrase)
|
||||
UnhandledExceptionViewFood = ["IndexError", "line 1, in some_buggy_code.py", "list index out of range"]
|
||||
controller.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [attr for attr, name in SettingsConstants.ALL_SIG_TYPES])
|
||||
controller.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [attr for attr, name in SettingsConstants.ALL_SCRIPT_TYPES])
|
||||
|
||||
# Pending mnemonic for ToolsCalcFinalWordShowFinalWordView
|
||||
controller.storage.init_pending_mnemonic(num_words=12)
|
||||
for i, word in enumerate(mnemonic_12[:11]):
|
||||
controller.storage.update_pending_mnemonic(word=word, index=i)
|
||||
controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet)
|
||||
controller.storage.seeds.append(seed_12)
|
||||
controller.storage.seeds.append(seed_12b)
|
||||
controller.storage.seeds.append(seed_24)
|
||||
controller.storage.set_pending_seed(seed_24_w_passphrase)
|
||||
|
||||
# Load a PSBT into memory
|
||||
BASE64_PSBT_1 = """cHNidP8BAP06AQIAAAAC5l4E3oEjI+H0im8t/K2nLmF5iJFdKEiuQs8ESveWJKcAAAAAAP3///8iBZMRhYIq4s/LmnTmKBi79M8ITirmsbO++63evK4utwAAAAAA/f///wZYQuoDAAAAACIAIAW5jm3UnC5fyjKCUZ8LTzjENtb/ioRTaBMXeSXsB3n+bK2fCgAAAAAWABReJY7akT1+d+jx475yBRWORdBd7VxbUgUAAAAAFgAU4wj9I/jB3GjNQudNZAca+7g9R16iWtYOAAAAABYAFIotPApLZlfscg8f3ppKqO3qA5nv7BnMFAAAAAAiACAs6SGc8qv4FwuNl0G0SpMZG8ODUEk5RXiWUcuzzw5iaRSfAhMAAAAAIgAgW0f5QxQIgVCGQqKzsvfkXZjUxdFop5sfez6Pt8mUbmZ1AgAAAAEAkgIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BQIRAgEB/////wJAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQErQL5AJQAAAAAiACCET6KNi75K8K4a2BYS4ZT+N4s8WwOBKOmOohRYkGHV0QEFR1EhArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEIQNDxn7PjUzvsP6KYw4s7dmoZE0qO1K6MaM+2ScRZ7hyxFKuIgYCsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEHCK94akwAACAAQAAgAAAAIACAACAAAAAAAMAAAAAAQCSAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8FAhACAQH/////AkC+QCUAAAAAIgAghE+ijYu+SvCuGtgWEuGU/jeLPFsDgSjpjqIUWJBh1dEAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QAAAAABAStAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAQVHUSECsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQhA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEUq4iBgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDQ8Z+z41M77D+imMOLO3ZqGRNKjtSujGjPtknEWe4csQcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAABAUdRIQJ5XLCBS0hdo4NANq4lNhimzhyHj7dvObmPAwNj8L2xASEC9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI1SriICAnlcsIFLSF2jg0A2riU2GKbOHIePt285uY8DA2PwvbEBHCK94akwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAAAAAAEBR1EhAgpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrIQOwLg0emiQbbxafIh69Xjtpj4eclsMhKq1y/7vYDdE7LVKuIgICCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ouscc8XaCjAAAIABAACAAAAAgAIAAIAAAAAABQAAACICA7AuDR6aJBtvFp8iHr1eO2mPh5yWwyEqrXL/u9gN0TstHCK94akwAACAAQAAgAAAAIACAACAAAAAAAUAAAAAAQFHUSECk50GLh/YhZaLJkDq/dugU3H/WvE6rTgQuY6N57pI4ykhA/H8MdLVP9SA/Hg8l3hvibSaC1bCBzwz7kTW+rsEZ8uFUq4iAgKTnQYuH9iFlosmQOr926BTcf9a8TqtOBC5jo3nukjjKRxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAGAAAAIgID8fwx0tU/1ID8eDyXeG+JtJoLVsIHPDPuRNb6uwRny4UcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAABgAAAAA="""
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_1)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_seed = seed_12b
|
||||
# Pending mnemonic for ToolsCalcFinalWordShowFinalWordView
|
||||
controller.storage.init_pending_mnemonic(num_words=12)
|
||||
for i, word in enumerate(mnemonic_12[:11]):
|
||||
controller.storage.update_pending_mnemonic(word=word, index=i)
|
||||
controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet)
|
||||
|
||||
def add_op_return_to_psbt(psbt: PSBT, raw_payload_data: bytes):
|
||||
data = (compact.to_bytes(OPCODES.OP_RETURN) +
|
||||
compact.to_bytes(OPCODES.OP_PUSHDATA1) +
|
||||
compact.to_bytes(len(raw_payload_data)) +
|
||||
raw_payload_data)
|
||||
script = Script(data)
|
||||
output = OutputScope()
|
||||
output.script_pubkey = script
|
||||
output.value = 0
|
||||
psbt.outputs.append(output)
|
||||
return psbt.to_string()
|
||||
# Load a PSBT into memory
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_1)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_seed = seed_12b
|
||||
|
||||
# Prep a PSBT with a human-readable OP_RETURN
|
||||
raw_payload_data = "Chancellor on the brink of third bailout for banks".encode()
|
||||
psbt = PSBT.from_base64(BASE64_PSBT_1)
|
||||
# Message signing data
|
||||
derivation_path = "m/84h/0h/0h/0/0"
|
||||
controller.sign_message_data = {
|
||||
"seed_num": 0,
|
||||
"derivation_path": derivation_path,
|
||||
"message": "I attest that I control this bitcoin address blah blah blah",
|
||||
"addr_format": embit_utils.parse_derivation_path(derivation_path)
|
||||
}
|
||||
|
||||
# Simplify the output side
|
||||
output = psbt.outputs[-1]
|
||||
psbt.outputs.clear()
|
||||
psbt.outputs.append(output)
|
||||
assert len(psbt.outputs) == 1
|
||||
BASE64_PSBT_WITH_OP_RETURN_TEXT = add_op_return_to_psbt(psbt, raw_payload_data)
|
||||
# Automatically populate all Settings options Views
|
||||
settings_views_list = []
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView))
|
||||
settings_views_list.append(
|
||||
ScreenshotConfig(
|
||||
settings_views.SettingsMenuView,
|
||||
dict(
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
initial_scroll=240, # Just guessing how many pixels to scroll down
|
||||
),
|
||||
screenshot_name="SettingsMenuView__Advanced"
|
||||
)
|
||||
)
|
||||
|
||||
# Prep a PSBT with a (repeatably) random 80-byte OP_RETURN
|
||||
random.seed(6102)
|
||||
BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES = add_op_return_to_psbt(PSBT.from_base64(BASE64_PSBT_1), random.randbytes(80))
|
||||
# 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(ScreenshotConfig(settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), screenshot_name=f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
||||
|
||||
settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
# Message signing data
|
||||
derivation_path = "m/84h/0h/0h/0/0"
|
||||
controller.sign_message_data = {
|
||||
"seed_num": 0,
|
||||
"derivation_path": derivation_path,
|
||||
"message": "I attest that I control this bitcoin address blah blah blah",
|
||||
"addr_format": embit_utils.parse_derivation_path(derivation_path)
|
||||
}
|
||||
# Set up screenshot-specific callbacks to inject data before the View is run and
|
||||
# reset data after the View is run.
|
||||
def load_basic_psbt_cb():
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_1)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_seed = seed_12b
|
||||
controller.multisig_wallet_descriptor = None
|
||||
|
||||
# Automatically populate all Settings options Views
|
||||
settings_views_list = []
|
||||
settings_views_list.append(settings_views.SettingsMenuView)
|
||||
settings_views_list.append((
|
||||
settings_views.SettingsMenuView,
|
||||
dict(
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
initial_scroll=240, # Just guessing how many pixels to scroll down
|
||||
),
|
||||
"SettingsMenuView__Advanced"
|
||||
))
|
||||
|
||||
# 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
|
||||
def load_multisig_wallet_descriptor_cb():
|
||||
controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR)
|
||||
|
||||
settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
||||
|
||||
|
||||
settingsqr_data_persistent = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
settingsqr_data_not_persistent = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
def load_address_verification_data_cb():
|
||||
controller.unverified_address = dict(
|
||||
# These are all totally fake data
|
||||
address="bc1q6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek",
|
||||
network=SettingsConstants.MAINNET,
|
||||
sig_type=SettingsConstants.SINGLE_SIG,
|
||||
script_type=SettingsConstants.NATIVE_SEGWIT,
|
||||
derivation_path = "m/84h/0h/0h",
|
||||
verified_index=5,
|
||||
verified_index_is_change=False
|
||||
)
|
||||
|
||||
screenshot_sections = {
|
||||
"Main Menu Views": [
|
||||
MainMenuView,
|
||||
(MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_removed', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED)),
|
||||
(MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_inserted', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED)),
|
||||
(MainMenuView, {}, 'MainMenuView_RemoveSDCardToast', RemoveSDCardToastManagerThread(activation_delay=0)),
|
||||
PowerOptionsView,
|
||||
RestartView,
|
||||
PowerOffView,
|
||||
],
|
||||
"Seed Views": [
|
||||
seed_views.SeedsMenuView,
|
||||
seed_views.LoadSeedView,
|
||||
seed_views.SeedMnemonicEntryView,
|
||||
seed_views.SeedMnemonicInvalidView,
|
||||
seed_views.SeedFinalizeView,
|
||||
seed_views.SeedAddPassphraseView,
|
||||
seed_views.SeedAddPassphraseExitDialogView,
|
||||
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__COMPACTSEEDQR, num_modules=21), "SeedTranscribeSeedQRWholeQRView_12_Compact"),
|
||||
(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_12_Standard"),
|
||||
(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_24_Compact"),
|
||||
(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), "SeedTranscribeSeedQRWholeQRView_24_Standard"),
|
||||
|
||||
# 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)),
|
||||
def PSBTSelectSeedView_cb_before():
|
||||
# Have to ensure this is cleared out in order to get the seed selection screen
|
||||
controller.psbt_seed = None
|
||||
|
||||
(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)),
|
||||
|
||||
# Screenshot can't render live preview screens
|
||||
# (seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)),
|
||||
def PSBTOverviewView_op_return_cb_before():
|
||||
controller.psbt_seed = seed_12b
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_TEXT)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
|
||||
|
||||
|
||||
#(seed_views.AddressVerificationStartView, dict(address=, script_type="nat", network="M")),
|
||||
#seed_views.AddressVerificationSigTypeView,
|
||||
#seed_views.SeedSingleSigAddressVerificationSelectSeedView,
|
||||
#seed_views.SeedAddressVerificationView,
|
||||
#seed_views.AddressVerificationSuccessView,
|
||||
def PSBTOpReturnView_raw_hex_data_cb_before():
|
||||
decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
|
||||
|
||||
seed_views.LoadMultisigWalletDescriptorView,
|
||||
seed_views.MultisigWalletDescriptorView,
|
||||
(seed_views.SeedDiscardView, dict(seed_num=0)),
|
||||
|
||||
seed_views.SeedSignMessageConfirmMessageView,
|
||||
seed_views.SeedSignMessageConfirmAddressView,
|
||||
screenshot_sections = {
|
||||
"Main Menu Views": [
|
||||
ScreenshotConfig(OpeningSplashView, dict(is_screenshot_renderer=True, force_partner_logos=True)),
|
||||
ScreenshotConfig(OpeningSplashView, dict(is_screenshot_renderer=True, force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos"),
|
||||
ScreenshotConfig(MainMenuView),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_RemoveSDCardToast', toast_thread=RemoveSDCardToastManagerThread(activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(PowerOptionsView),
|
||||
ScreenshotConfig(RestartView),
|
||||
ScreenshotConfig(PowerOffView),
|
||||
],
|
||||
"Seed Views": [
|
||||
ScreenshotConfig(seed_views.SeedsMenuView),
|
||||
ScreenshotConfig(seed_views.LoadSeedView),
|
||||
ScreenshotConfig(seed_views.SeedMnemonicEntryView),
|
||||
ScreenshotConfig(seed_views.SeedMnemonicInvalidView),
|
||||
ScreenshotConfig(seed_views.SeedFinalizeView),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, screenshot_name="SeedAddPassphraseView_lowercase"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__UPPERCASE_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_uppercase"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__DIGITS_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_digits"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_1_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_symbols_1"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_2_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_symbols_2"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseExitDialogView),
|
||||
ScreenshotConfig(seed_views.SeedReviewPassphraseView),
|
||||
|
||||
ScreenshotConfig(seed_views.SeedOptionsView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedBackupView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")),
|
||||
#ScreenshotConfig(SeedExportXpubQRDisplayView),
|
||||
ScreenshotConfig(seed_views.SeedWordsWarningView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedWordsView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedWordsView, dict(seed_num=0, page_index=2), screenshot_name="SeedWordsView_2"),
|
||||
ScreenshotConfig(seed_views.SeedBIP85ApplicationModeView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedBIP85SelectChildIndexView, dict(seed_num=0, num_words=24)),
|
||||
ScreenshotConfig(seed_views.SeedBIP85InvalidChildIndexView, dict(seed_num=0, num_words=12)),
|
||||
ScreenshotConfig(seed_views.SeedWordsBackupTestPromptView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedWordsBackupTestView, dict(seed_num=0, rand_seed=6102)),
|
||||
ScreenshotConfig(seed_views.SeedWordsBackupTestMistakeView, dict(seed_num=0, cur_index=7, wrong_word="satoshi")),
|
||||
ScreenshotConfig(seed_views.SeedWordsBackupTestSuccessView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRFormatView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWarningView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Standard"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Standard"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, initial_block_x=1, initial_block_y=1), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, initial_block_x=2, initial_block_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"),
|
||||
|
||||
seed_views.SeedElectrumMnemonicStartView,
|
||||
],
|
||||
"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)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)),
|
||||
|
||||
(NotYetImplementedView, {}, "PSBTChangeDetailsView_multisig_unverified"), # Must manually re-run this below
|
||||
(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), "PSBTChangeDetailsView_multisig_verified"),
|
||||
# Screenshot can't render live preview screens
|
||||
# ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)),
|
||||
|
||||
(NotYetImplementedView, {}, "PSBTOverviewView_op_return"), # Placeholder
|
||||
(NotYetImplementedView, {}, "PSBTOpReturnView_text"), # Placeholder
|
||||
(NotYetImplementedView, {}, "PSBTOpReturnView_raw_hex_data"), # Placeholder
|
||||
ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), screenshot_name="SeedSelectSeedView_address_verification"),
|
||||
ScreenshotConfig(seed_views.AddressVerificationSigTypeView),
|
||||
ScreenshotConfig(seed_views.SeedAddressVerificationView, dict(seed_num=0), run_before=load_address_verification_data_cb),
|
||||
ScreenshotConfig(seed_views.SeedAddressVerificationSuccessView, dict(seed_num=0)), # Relies on callback above
|
||||
|
||||
(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, {}, "ToolsCalcFinalWordShowFinalWordView_pick_word"),
|
||||
(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), "ToolsCalcFinalWordShowFinalWordView_coin_flips"),
|
||||
#tools_views.ToolsCalcFinalWordDoneView,
|
||||
tools_views.ToolsAddressExplorerSelectSourceView,
|
||||
tools_views.ToolsAddressExplorerAddressTypeView,
|
||||
tools_views.ToolsAddressExplorerAddressListView,
|
||||
#tools_views.ToolsAddressExplorerAddressView,
|
||||
],
|
||||
"Settings Views": settings_views_list + [
|
||||
settings_views.IOTestView,
|
||||
settings_views.DonateView,
|
||||
(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), "SettingsIngestSettingsQRView_persistent"),
|
||||
(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), "SettingsIngestSettingsQRView_not_persistent"),
|
||||
],
|
||||
"Misc Error Views": [
|
||||
NotYetImplementedView,
|
||||
(UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)),
|
||||
NetworkMismatchErrorView,
|
||||
(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
|
||||
(ErrorView, dict(
|
||||
title="Error",
|
||||
status_headline="Unknown QR Type",
|
||||
text="QRCode is invalid or is a data format not yet supported.",
|
||||
button_text="Back",
|
||||
)),
|
||||
]
|
||||
}
|
||||
ScreenshotConfig(seed_views.LoadMultisigWalletDescriptorView),
|
||||
ScreenshotConfig(seed_views.MultisigWalletDescriptorView, run_before=load_multisig_wallet_descriptor_cb),
|
||||
ScreenshotConfig(seed_views.SeedDiscardView, dict(seed_num=0)),
|
||||
|
||||
readme = f"""# SeedSigner Screenshots\n"""
|
||||
ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__SIGN_MESSAGE), screenshot_name="SeedSelectSeedView_sign_message"),
|
||||
ScreenshotConfig(seed_views.SeedSignMessageConfirmMessageView),
|
||||
ScreenshotConfig(seed_views.SeedSignMessageConfirmAddressView),
|
||||
|
||||
def screencap_view(view_cls: View, view_args: dict = {}, view_name: str = None, toast_thread: BaseToastOverlayManagerThread = None):
|
||||
if not view_name:
|
||||
view_name = view_cls.__name__
|
||||
screenshot_renderer.set_screenshot_filename(f"{view_name}.png")
|
||||
ScreenshotConfig(seed_views.SeedElectrumMnemonicStartView),
|
||||
],
|
||||
"PSBT Views": [
|
||||
ScreenshotConfig(psbt_views.PSBTSelectSeedView, run_before=PSBTSelectSeedView_cb_before),
|
||||
ScreenshotConfig(psbt_views.PSBTOverviewView, run_before=load_basic_psbt_cb),
|
||||
ScreenshotConfig(psbt_views.PSBTUnsupportedScriptTypeWarningView),
|
||||
ScreenshotConfig(psbt_views.PSBTNoChangeWarningView),
|
||||
ScreenshotConfig(psbt_views.PSBTMathView),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressDetailsView, dict(address_num=0)),
|
||||
|
||||
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_unverified", run_before=load_basic_psbt_cb),
|
||||
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_verified", run_before=load_multisig_wallet_descriptor_cb),
|
||||
ScreenshotConfig(psbt_views.PSBTOverviewView, screenshot_name="PSBTOverviewView_op_return", run_before=PSBTOverviewView_op_return_cb_before),
|
||||
ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_text"), # Relies on callback above
|
||||
ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_raw_hex_data", run_before=PSBTOpReturnView_raw_hex_data_cb_before),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), screenshot_name="PSBTAddressVerificationFailedView_singlesig_change"),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), screenshot_name="PSBTAddressVerificationFailedView_singlesig_selftransfer"),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), screenshot_name="PSBTAddressVerificationFailedView_multisig_change"),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=True), screenshot_name="PSBTAddressVerificationFailedView_multisig_selftransfer"),
|
||||
ScreenshotConfig(psbt_views.PSBTFinalizeView),
|
||||
#ScreenshotConfig(PSBTSignedQRDisplayViewScreenshotConfig),
|
||||
ScreenshotConfig(psbt_views.PSBTSigningErrorView),
|
||||
],
|
||||
"Tools Views": [
|
||||
ScreenshotConfig(tools_views.ToolsMenuView),
|
||||
#ScreenshotConfig(ToolsImageEntropyLivePreviewView),
|
||||
#ScreenshotConfig(ToolsImageEntropyFinalImageView),
|
||||
ScreenshotConfig(tools_views.ToolsImageEntropyMnemonicLengthView),
|
||||
ScreenshotConfig(tools_views.ToolsDiceEntropyMnemonicLengthView),
|
||||
ScreenshotConfig(tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordNumWordsView),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordFinalizePromptView),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordCoinFlipsView),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordShowFinalWordView, screenshot_name="ToolsCalcFinalWordShowFinalWordView_pick_word"),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), screenshot_name="ToolsCalcFinalWordShowFinalWordView_coin_flips"),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordDoneView),
|
||||
ScreenshotConfig(tools_views.ToolsAddressExplorerSelectSourceView),
|
||||
ScreenshotConfig(tools_views.ToolsAddressExplorerAddressTypeView),
|
||||
ScreenshotConfig(tools_views.ToolsAddressExplorerAddressListView),
|
||||
# ScreenshotConfig(tools_views.ToolsAddressExplorerAddressView),
|
||||
],
|
||||
"Settings Views": settings_views_list + [
|
||||
ScreenshotConfig(settings_views.IOTestView),
|
||||
ScreenshotConfig(settings_views.DonateView),
|
||||
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
|
||||
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
|
||||
],
|
||||
"Misc Error Views": [
|
||||
ScreenshotConfig(NotYetImplementedView),
|
||||
ScreenshotConfig(UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])),
|
||||
ScreenshotConfig(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")),
|
||||
ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
|
||||
ScreenshotConfig(ErrorView, dict(
|
||||
title="Error",
|
||||
status_headline="Unknown QR Type",
|
||||
text="QRCode is invalid or is a data format not yet supported.",
|
||||
button_text="Back",
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
return screenshot_sections
|
||||
|
||||
|
||||
def screencap_view(screenshot_config: ScreenshotConfig):
|
||||
# Block until we have exclusive access to the screenshot renderer. Without this
|
||||
# we were occasionally running into confusing race conditions where the next
|
||||
# screenshot would begin rendering over the previous one. Claiming the lock
|
||||
# guarantees that the previous screenshot has been fully rendered and saved.
|
||||
with screenshot_renderer.lock:
|
||||
screenshot_renderer.set_screenshot_filename(f"{screenshot_config.screenshot_name}.png")
|
||||
|
||||
controller = Controller.get_instance()
|
||||
toast_thread = screenshot_config.toast_thread
|
||||
try:
|
||||
print(f"Running {view_name}")
|
||||
print(f"Running {screenshot_config.screenshot_name}")
|
||||
try:
|
||||
view_cls(**view_args).run()
|
||||
screenshot_config.run_callback_before()
|
||||
screenshot_config.View_cls(**screenshot_config.view_kwargs).run()
|
||||
except ScreenshotComplete:
|
||||
# The target View has run and its Screen has rendered what it needs to
|
||||
if toast_thread is not None:
|
||||
# Now run the Toast so it can render on top of the current image buffer
|
||||
controller.activate_toast(toast_thread)
|
||||
while controller.toast_notification_thread.is_alive():
|
||||
time.sleep(0.1)
|
||||
# Give the Toast a moment to complete its work
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
# TODO: Necessary now that the lock is in place?
|
||||
# Whenever possible, clean up toast thread HERE before killing the
|
||||
# main thread with ScreenshotComplete.
|
||||
toast_thread.stop()
|
||||
toast_thread.join()
|
||||
raise ScreenshotComplete()
|
||||
except ScreenshotComplete:
|
||||
# Slightly hacky way to exit ScreenshotRenderer as expected
|
||||
pass
|
||||
print(f"Completed {view_name}")
|
||||
print(f"Completed {screenshot_config.screenshot_name}")
|
||||
except Exception as e:
|
||||
# Something else went wrong
|
||||
from traceback import print_exc
|
||||
print_exc()
|
||||
raise e
|
||||
finally:
|
||||
if toast_thread:
|
||||
if toast_thread and toast_thread.is_alive():
|
||||
toast_thread.stop()
|
||||
toast_thread.join()
|
||||
|
||||
for section_name, screenshot_list in screenshot_sections.items():
|
||||
screenshot_config.run_callback_after()
|
||||
|
||||
|
||||
# Parse the main `l10n/messages.pot` for overall stats
|
||||
messages_source_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "l10n", "messages.pot")
|
||||
with open(messages_source_path, 'r') as messages_source_file:
|
||||
num_source_messages = messages_source_file.read().count("msgid \"") - 1
|
||||
|
||||
locale_tuple_list = [locale_tuple for locale_tuple in SettingsConstants.ALL_LOCALES if locale_tuple[0] == locale]
|
||||
if not locale_tuple_list:
|
||||
raise Exception(f"Invalid locale: {locale}")
|
||||
|
||||
locale, display_name = locale_tuple_list[0]
|
||||
|
||||
Settings.get_instance().set_value(SettingsConstants.SETTING__LOCALE, value=locale)
|
||||
|
||||
locale_readme = f"""# SeedSigner Screenshots: {display_name}\n"""
|
||||
|
||||
# Report the translation progress
|
||||
if locale != SettingsConstants.LOCALE__ENGLISH:
|
||||
try:
|
||||
translated_messages_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "src", "seedsigner", "resources", "seedsigner-translations", "l10n", 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
|
||||
|
||||
if locale != "en":
|
||||
locale_readme += f"## Translation progress: {num_locale_translations / num_source_messages:.1%}\n\n"
|
||||
locale_readme += "---\n\n"
|
||||
except Exception as e:
|
||||
from traceback import print_exc
|
||||
print_exc()
|
||||
|
||||
for section_name, screenshot_list in setup_screenshots(locale).items():
|
||||
subdir = section_name.lower().replace(" ", "_")
|
||||
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, subdir))
|
||||
readme += "\n\n---\n\n"
|
||||
readme += f"## {section_name}\n\n"
|
||||
readme += """<table style="border: 0;">"""
|
||||
readme += f"""<tr><td align="center">\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
|
||||
elif len(screenshot) == 4:
|
||||
view_cls, view_args, view_name, toast_thread = screenshot
|
||||
else:
|
||||
view_cls = screenshot
|
||||
view_args = {}
|
||||
view_name = view_cls.__name__
|
||||
toast_thread = None
|
||||
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale, subdir))
|
||||
locale_readme += "\n\n---\n\n"
|
||||
locale_readme += f"## {section_name}\n\n"
|
||||
locale_readme += """<table style="border: 0;">"""
|
||||
locale_readme += f"""<tr><td align="center">"""
|
||||
for screenshot_config in screenshot_list:
|
||||
screencap_view(screenshot_config)
|
||||
locale_readme += """ <table align="left" style="border: 1px solid gray;">"""
|
||||
locale_readme += f"""<tr><td align="center">{screenshot_config.screenshot_name}<br/><br/><img src="{subdir}/{screenshot_config.screenshot_name}.png"></td></tr>"""
|
||||
locale_readme += """</table>\n"""
|
||||
|
||||
screencap_view(view_cls, view_args=view_args, view_name=view_name, toast_thread=toast_thread)
|
||||
readme += """ <table align="left" style="border: 1px solid gray;">"""
|
||||
readme += f"""<tr><td align="center">{view_name}<br/><br/><img src="{subdir}/{view_name}.png"></td></tr>"""
|
||||
readme += """</table>\n"""
|
||||
locale_readme += "</td></tr></table>"
|
||||
|
||||
readme += "</td></tr></table>"
|
||||
with open(os.path.join(screenshot_root, locale, "README.md"), 'w') as readme_file:
|
||||
readme_file.write(locale_readme)
|
||||
|
||||
# Re-render some screens that require more manual intervention / setup than the above
|
||||
# scripting can support.
|
||||
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, "psbt_views"))
|
||||
print(f"Done with locale: {locale}.")
|
||||
|
||||
# Render the PSBTChangeDetailsView_multisig_unverified screenshot
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_1)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_seed = seed_12b
|
||||
controller.multisig_wallet_descriptor = None
|
||||
screencap_view(psbt_views.PSBTChangeDetailsView, view_name='PSBTChangeDetailsView_multisig_unverified', view_args=dict(change_address_num=0))
|
||||
# Write the main README; ensure it writes all locales, not just the one that may
|
||||
# have been specified for this run.
|
||||
with open(os.path.join("tests", "screenshot_generator", "template.md"), 'r') as readme_template:
|
||||
main_readme = readme_template.read()
|
||||
|
||||
controller.psbt_seed = None
|
||||
screencap_view(psbt_views.PSBTSelectSeedView, view_name='PSBTSelectSeedView')
|
||||
|
||||
# Render OP_RETURN screens for real
|
||||
controller.psbt_seed = seed_12b
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_TEXT)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
|
||||
screencap_view(psbt_views.PSBTOverviewView, view_name='PSBTOverviewView_op_return')
|
||||
screencap_view(psbt_views.PSBTOpReturnView, view_name="PSBTOpReturnView_text")
|
||||
|
||||
decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
|
||||
screencap_view(psbt_views.PSBTOpReturnView, view_name="PSBTOpReturnView_raw_hex_data")
|
||||
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(readme)
|
||||
readme_file.write(main_readme)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# SeedSigner Screenshots
|
||||
|
||||
SeedSigner screenshots can be freely used in any tutorial, article, video, etc. As a courtesy, please link back to this repo or the SeedSigner website in your attribution.
|
||||
|
||||
 
|
||||
|
||||
 
|
||||
|
||||
|
||||
## Generating screenshots
|
||||
The screenshot generator is integrated into the SeedSigner test suite and requires a local copy of the SeedSigner repo with the test suite dependencies installed.
|
||||
|
||||
see: https://github.com/SeedSigner/seedsigner/blob/dev/tests/screenshot_generator/README.md
|
||||
|
||||
|
||||
## Currently supported or in-progress languages
|
||||
@@ -1,6 +1,11 @@
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.gui.toast import BaseToastOverlayManagerThread
|
||||
from seedsigner.views.view import View
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +42,7 @@ class ScreenshotRenderer(Renderer):
|
||||
self.screenshot_path = path
|
||||
|
||||
|
||||
def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False):
|
||||
def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False):
|
||||
if is_background_thread:
|
||||
return
|
||||
|
||||
@@ -53,3 +58,30 @@ class ScreenshotRenderer(Renderer):
|
||||
self.canvas.save(os.path.join(self.screenshot_path, self.screenshot_filename))
|
||||
raise ScreenshotComplete()
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenshotConfig:
|
||||
View_cls: View
|
||||
view_kwargs: dict = None
|
||||
screenshot_name: str = None
|
||||
toast_thread: BaseToastOverlayManagerThread = None
|
||||
run_before: callable = None
|
||||
run_after: callable = None
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.view_kwargs:
|
||||
self.view_kwargs = {}
|
||||
if not self.screenshot_name:
|
||||
self.screenshot_name = self.View_cls.__name__
|
||||
|
||||
|
||||
def run_callback_before(self):
|
||||
if self.run_before:
|
||||
self.run_before()
|
||||
|
||||
|
||||
def run_callback_after(self):
|
||||
if self.run_after:
|
||||
self.run_after()
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from seedsigner.models.seed import Seed
|
||||
from embit import bip39
|
||||
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
|
||||
|
||||
def test_derive_child_mnemonic():
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestController(BaseTest):
|
||||
controller = Controller.get_instance()
|
||||
|
||||
# Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__LANGUAGE) == SettingsConstants.LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__LOCALE) == SettingsConstants.LOCALE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from gettext import gettext as _
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.views import settings_views
|
||||
from seedsigner.views.view import MainMenuView
|
||||
|
||||
|
||||
|
||||
class TestL10nFlows(FlowTest):
|
||||
def test_change_locale(self):
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__LOCALE)
|
||||
spanish_display_name = [locale_tuple[1] for locale_tuple in SettingsConstants.ALL_LOCALES if locale_tuple[0] == SettingsConstants.LOCALE__SPANISH][0]
|
||||
|
||||
# Initially we get English
|
||||
assert _(MainMenuView.SCAN.button_label) == "Scan"
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=ButtonOption(spanish_display_name)),
|
||||
])
|
||||
|
||||
# Now we don't get English
|
||||
assert _(MainMenuView.SCAN.button_label) != "Scan"
|
||||
@@ -115,7 +115,7 @@ class TestPSBTFlows(FlowTest):
|
||||
FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED),
|
||||
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="abc")),
|
||||
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
|
||||
+23
-12
@@ -5,7 +5,7 @@ import pytest
|
||||
from base import BaseTest, FlowTest, FlowStep
|
||||
from base import FlowTestInvalidButtonDataSelectionException
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
from seedsigner.models.seed import ElectrumSeed, Seed
|
||||
from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, View, NetworkMismatchErrorView
|
||||
@@ -40,10 +40,10 @@ class TestSeedFlows(FlowTest):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase", is_back_button=True)),
|
||||
FlowStep(seed_views.SeedAddPassphraseExitDialogView, button_data_selection=seed_views.SeedAddPassphraseExitDialogView.DISCARD),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase", is_back_button=True)),
|
||||
FlowStep(seed_views.SeedAddPassphraseExitDialogView, button_data_selection=seed_views.SeedAddPassphraseExitDialogView.EDIT),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase")),
|
||||
@@ -183,15 +183,18 @@ class TestSeedFlows(FlowTest):
|
||||
"""
|
||||
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
|
||||
"""
|
||||
|
||||
def flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple):
|
||||
if sig_tuple[0] == SettingsConstants.SINGLE_SIG:
|
||||
sig_selection = seed_views.SeedExportXpubSigTypeView.SINGLE_SIG
|
||||
else:
|
||||
sig_selection = seed_views.SeedExportXpubSigTypeView.MULTISIG
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coord_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_selection),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=ButtonOption(script_tuple[1], return_data=script_tuple[0])),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=ButtonOption(coord_tuple[1], return_data=coord_tuple[0])),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
@@ -303,10 +306,18 @@ class TestSeedFlows(FlowTest):
|
||||
SettingsConstants.CUSTOM_DERIVATION
|
||||
])
|
||||
|
||||
# get display names to access button choices in the views (ugh: hardcoding, is there a better way?)
|
||||
sig_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SIG_TYPES)[0] # single sig
|
||||
script_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2] # custom derivation
|
||||
coordinator = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3] # specter
|
||||
# Ensure that all coordinators are enabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x, y in SettingsConstants.ALL_COORDINATORS])
|
||||
|
||||
# Set up button_data selections
|
||||
sig_type = seed_views.SeedExportXpubSigTypeView.SINGLE_SIG
|
||||
|
||||
custom_derivation = SettingsConstants.CUSTOM_DERIVATION
|
||||
script_type = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2], return_data=custom_derivation)
|
||||
|
||||
specter = SettingsConstants.COORDINATOR__SPECTER_DESKTOP
|
||||
assert SettingsConstants.ALL_COORDINATORS[3][0] == specter
|
||||
coordinator = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3], return_data=specter)
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
@@ -377,7 +388,7 @@ class TestSeedFlows(FlowTest):
|
||||
|
||||
# Skips past the script type options via redirect
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0]),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0], return_data=SettingsConstants.ALL_COORDINATORS[0][0])),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
|
||||
@@ -8,7 +8,7 @@ from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsDefinition, SettingsConstants
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import scan_views, settings_views
|
||||
@@ -26,8 +26,8 @@ class TestSettingsFlows(FlowTest):
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED)),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=ButtonOption(settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED))),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
@@ -43,7 +43,7 @@ class TestSettingsFlows(FlowTest):
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.views.view import ErrorView, MainMenuView
|
||||
@@ -25,7 +25,7 @@ class TestToolsFlows(FlowTest):
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.ADDRESS_EXPLORER),
|
||||
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, screen_return_value=0), # ret 1st onboard seed
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT)),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=ButtonOption(SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT), return_data=SettingsConstants.NATIVE_SEGWIT)),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=10), # ret NEXT page of addrs
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=4), # ret a specific addr from the list
|
||||
@@ -62,7 +62,7 @@ class TestToolsFlows(FlowTest):
|
||||
# Finalize the new seed w/passphrase
|
||||
self.run_sequence(
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="mypassphrase")),
|
||||
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
@@ -242,6 +242,31 @@ class TestToolsFlows(FlowTest):
|
||||
FlowStep(scan_views.ScanWalletDescriptorView, before_run=load_descriptor_into_decoder), # simulate read descriptor QR
|
||||
FlowStep(seed_views.MultisigWalletDescriptorView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedAddressVerificationView),
|
||||
FlowStep(seed_views.AddressVerificationSuccessView),
|
||||
FlowStep(seed_views.SeedAddressVerificationSuccessView),
|
||||
])
|
||||
|
||||
|
||||
def test__verify_address__singlesig__flow(self):
|
||||
"""
|
||||
Address Explorer should be able to scan a singlesig address and
|
||||
verify it against a loaded key.
|
||||
"""
|
||||
controller = Controller.get_instance()
|
||||
controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
|
||||
controller.storage.finalize_pending_seed()
|
||||
settings = controller.settings
|
||||
settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST)
|
||||
|
||||
def load_address_into_decoder(view: scan_views.ScanView):
|
||||
# Native segwit regtest receive addr @ index 6
|
||||
view.decoder.add_data("bcrt1q4e9q5taxnsvc6m0uxv6h75mkzvnkxeqk6l90u2")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.VERIFY_ADDRESS),
|
||||
FlowStep(scan_views.ScanAddressView, before_run=load_address_into_decoder), # simulate read address QR
|
||||
FlowStep(seed_views.AddressVerificationStartView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedSelectSeedView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedAddressVerificationView),
|
||||
FlowStep(seed_views.SeedAddressVerificationSuccessView),
|
||||
])
|
||||
|
||||
@@ -28,14 +28,6 @@ class TestViewFlows(FlowTest):
|
||||
"""
|
||||
Basic flow from MainMenuView to PowerOffView
|
||||
"""
|
||||
with patch('seedsigner.views.view.PowerOffView.PowerOffThread'):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
|
||||
FlowStep(PowerOffView),
|
||||
])
|
||||
|
||||
# And again, but this time as if we were in the SeedSigner OS
|
||||
Settings.HOSTNAME = Settings.SEEDSIGNER_OS
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
from gettext import gettext as _
|
||||
|
||||
from base import BaseTest
|
||||
from seedsigner.gui.screens.screen import ButtonOption
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.view import MainMenuView
|
||||
|
||||
|
||||
|
||||
class TestGettext(BaseTest):
|
||||
def test_english_as_default(self):
|
||||
# Key is available in other languages, but we get English back
|
||||
assert _("Home") == "Home"
|
||||
|
||||
def test_missing_key_returns_english_key(self):
|
||||
test_str = "This is not in our translation library"
|
||||
assert _(test_str) == test_str
|
||||
|
||||
|
||||
def test_basic_spanish(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
assert _("Home") != "Home"
|
||||
|
||||
|
||||
def test_locale_changes(self):
|
||||
settings = Settings.get_instance()
|
||||
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
spanish_str = _("Home")
|
||||
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__ENGLISH)
|
||||
assert spanish_str != _("Home")
|
||||
assert _("Home") == "Home"
|
||||
|
||||
|
||||
|
||||
class TestButtonOption(BaseTest):
|
||||
def test_english_key_not_translated(self):
|
||||
""" ButtonOption should always return its English key, regardless of current locale setting. """
|
||||
button_option = ButtonOption("Home")
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
assert button_option.button_label == "Home"
|
||||
|
||||
brand_new_button_option = ButtonOption("Tools")
|
||||
assert brand_new_button_option.button_label == "Tools"
|
||||
|
||||
|
||||
def test_class_level_button_option_english_key_not_translated(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
class FooClass:
|
||||
HOME = ButtonOption("Home")
|
||||
|
||||
assert FooClass.HOME.button_label == "Home"
|
||||
|
||||
|
||||
def test_gettext_translates_class_level_button_option(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
class BarClass:
|
||||
HOME = ButtonOption("Home")
|
||||
|
||||
assert _(BarClass.HOME.button_label) != "Home"
|
||||
|
||||
|
||||
|
||||
class TestMarkForTranslation(BaseTest):
|
||||
def test_english_key_not_translated(self):
|
||||
""" _mft() should always return its English key, regardless of current locale setting. """
|
||||
mft_attr = _mft("Home")
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
assert mft_attr == "Home"
|
||||
|
||||
brand_new_mft_attr = _mft("Tools")
|
||||
assert brand_new_mft_attr == "Tools"
|
||||
|
||||
|
||||
def test_class_level_mft_attr_english_key_not_translated(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
class FooClass:
|
||||
home = _mft("Home")
|
||||
|
||||
assert FooClass.home == "Home"
|
||||
|
||||
|
||||
def test_gettext_translates_class_level_mft_attr(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
class BarClass:
|
||||
home = _mft("Home")
|
||||
|
||||
assert _(BarClass.home) != "Home"
|
||||
Reference in New Issue
Block a user