diff --git a/clive/__private/ui/app.py b/clive/__private/ui/app.py index 737a9d1ec1e214f7edee7e10d327470f5cd56c04..78b463b9e835268c3f1c1efaf2fa3082e2561d9f 100644 --- a/clive/__private/ui/app.py +++ b/clive/__private/ui/app.py @@ -25,12 +25,14 @@ from clive.__private.settings import safe_settings from clive.__private.ui.bindings import CLIVE_PREDEFINED_BINDINGS, BindingFileInvalidError, load_custom_bindings from clive.__private.ui.clive_pilot import ClivePilot from clive.__private.ui.dialogs import LoadTransactionFromFileDialog +from clive.__private.ui.dialogs.switch_node_address_dialog import SwitchNodeAddressDialog from clive.__private.ui.forms.create_profile.create_profile_form import CreateProfileForm from clive.__private.ui.get_css import get_relative_css_path from clive.__private.ui.help import Help from clive.__private.ui.screens.dashboard import Dashboard from clive.__private.ui.screens.quit import Quit from clive.__private.ui.screens.settings import Settings +from clive.__private.ui.screens.settings.switch_node_address import SwitchNodeAddress from clive.__private.ui.screens.unlock import Unlock from clive.__private.ui.types import CliveModes from clive.exceptions import ScreenNotFoundError @@ -84,7 +86,9 @@ class Clive(App[int]): CLIVE_PREDEFINED_BINDINGS.app.transaction_summary.create(show=False), CLIVE_PREDEFINED_BINDINGS.app.dashboard.create(show=False), CLIVE_PREDEFINED_BINDINGS.app.settings.create(show=False), + CLIVE_PREDEFINED_BINDINGS.app.switch_node.create(show=False), CLIVE_PREDEFINED_BINDINGS.app.load_transaction_from_file.create(show=False), + CLIVE_PREDEFINED_BINDINGS.app.lock_wallet.create(show=False), ] SCREENS = { @@ -271,6 +275,23 @@ class Clive(App[int]): with self._screen_remove_guard.suppress(), self._screen_remove_guard.guard(): await self.go_to_transaction_summary() + async def action_lock_wallet(self) -> None: + with self._screen_remove_guard.suppress(), self._screen_remove_guard.guard(): + await self.switch_mode_into_locked() + + def action_do_nothing(self) -> None: + """This action is used to override the default action of some bindings, does nothing.""" + + async def action_switch_node(self) -> None: + self.show_switch_node_address_dialog() + + def show_switch_node_address_dialog(self) -> None: + if self.current_mode == "unlock": + return + if isinstance(self.screen, SwitchNodeAddressDialog | SwitchNodeAddress): + return + self.push_screen(SwitchNodeAddressDialog()) + def pause_refresh_alarms_data_interval(self) -> None: self._refresh_alarms_data_interval.pause() self.workers.cancel_group(self, self._ALARMS_DATA_WORKER_GROUP_NAME) @@ -404,6 +425,9 @@ class Clive(App[int]): return AwaitComplete(impl()).call_next(self) async def switch_mode_into_locked(self, *, save_profile: bool = True) -> None: + if not self.world.app_state.is_unlocked: + return + if save_profile: await self.world.commands.save_profile() diff --git a/clive/__private/ui/bindings/clive_bindings.py b/clive/__private/ui/bindings/clive_bindings.py index 96aa1b1d12d2d8b69d44cf0321ab564fdcdcc148..fda9c03af158c358035f18b84c144d1fde705d56 100644 --- a/clive/__private/ui/bindings/clive_bindings.py +++ b/clive/__private/ui/bindings/clive_bindings.py @@ -12,7 +12,6 @@ from clive.__private.ui.bindings.exceptions import BindingFileInvalidError, Bind from clive.__private.ui.bindings.sections import ( App, Dashboard, - FormNavigation, Help, ManageKeyAliases, Operations, @@ -46,7 +45,6 @@ class CliveBindings: app: App = field(default_factory=lambda: App()) dashboard: Dashboard = field(default_factory=lambda: Dashboard()) - form_navigation: FormNavigation = field(default_factory=lambda: FormNavigation()) help: Help = field(default_factory=lambda: Help()) manage_key_aliases: ManageKeyAliases = field(default_factory=lambda: ManageKeyAliases()) operations: Operations = field(default_factory=lambda: Operations()) diff --git a/clive/__private/ui/bindings/sections.py b/clive/__private/ui/bindings/sections.py index 1fd508186df6c378b053a82cad630a9d77abab00..e984247e29435f90faca5d094da647fba2b00c3c 100644 --- a/clive/__private/ui/bindings/sections.py +++ b/clive/__private/ui/bindings/sections.py @@ -15,7 +15,9 @@ class App(CliveBindingSection): load_transaction_from_file: CliveBinding = field( default_factory=lambda: CliveBinding(id="load_transaction_from_file", key="ctrl+o") ) + lock_wallet: CliveBinding = field(default_factory=lambda: CliveBinding(id="lock_wallet", key="ctrl+l")) settings: CliveBinding = field(default_factory=lambda: CliveBinding(id="settings", key="ctrl+s")) + switch_node: CliveBinding = field(default_factory=lambda: CliveBinding(id="switch_node", key="ctrl+n")) transaction_summary: CliveBinding = field( default_factory=lambda: CliveBinding(id="transaction_summary", key="ctrl+t") ) @@ -29,13 +31,6 @@ class Dashboard(CliveBindingSection): default_factory=lambda: CliveBinding(id="switch_working_account", key="w") ) add_account: CliveBinding = field(default_factory=lambda: CliveBinding(id="add_account", key="a")) - lock_wallet: CliveBinding = field(default_factory=lambda: CliveBinding(id="lock_wallet", key="ctrl+l")) - - -@dataclass -class FormNavigation(CliveBindingSection): - next_screen: CliveBinding = field(default_factory=lambda: CliveBinding(id="next_screen", key="ctrl+n")) - previous_screen: CliveBinding = field(default_factory=lambda: CliveBinding(id="previous_screen", key="ctrl+b")) @dataclass diff --git a/clive/__private/ui/forms/create_profile/create_profile_form_screen.py b/clive/__private/ui/forms/create_profile/create_profile_form_screen.py index a5f2cba904f8001afc6dc0fdf30eacc9ed6ed14d..b78890f336cda332164e25f481fbd29ef8d1617a 100644 --- a/clive/__private/ui/forms/create_profile/create_profile_form_screen.py +++ b/clive/__private/ui/forms/create_profile/create_profile_form_screen.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: class CreateProfileFormScreen(FormScreen, ABC): BINDINGS = [ CLIVE_PREDEFINED_BINDINGS.help.toggle_help.create( - action="", description="Help" + action="app.help", description="Help" ), # help is a hidden global binding, but we want to show it here ] diff --git a/clive/__private/ui/forms/create_profile/profile_credentials_form_screen.py b/clive/__private/ui/forms/create_profile/profile_credentials_form_screen.py index 1c197417294d304d4d31a98aa85a9018aa8ac765..106629d50162d2fb46bfb88b5025cb559a72c655 100644 --- a/clive/__private/ui/forms/create_profile/profile_credentials_form_screen.py +++ b/clive/__private/ui/forms/create_profile/profile_credentials_form_screen.py @@ -2,6 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +from clive.__private.ui.bindings import CLIVE_PREDEFINED_BINDINGS from clive.__private.ui.forms.create_profile.create_profile_form_screen import CreateProfileFormScreen from clive.__private.ui.forms.navigation_buttons import NavigationButtons from clive.__private.ui.get_css import get_relative_css_path @@ -23,6 +24,11 @@ class ProfileCredentialsFormScreen(BaseScreen, CreateProfileFormScreen): CSS_PATH = [get_relative_css_path(__file__)] BIG_TITLE = "create profile" SHOW_RAW_HEADER = True + BINDINGS = [ + CLIVE_PREDEFINED_BINDINGS.app.switch_node.create( + action="app.do_nothing", show=False + ), # we want to disable switch_node binding here + ] def __init__(self, owner: CreateProfileForm) -> None: self._profile_name_input = SetProfileNameInput() diff --git a/clive/__private/ui/forms/create_profile/welcome_form_screen.py b/clive/__private/ui/forms/create_profile/welcome_form_screen.py index a808d4ba3b8ebbd4d1bddee56f8fa19febbc35d0..b025c006c8578887a665b35ef2d403061367b654 100644 --- a/clive/__private/ui/forms/create_profile/welcome_form_screen.py +++ b/clive/__private/ui/forms/create_profile/welcome_form_screen.py @@ -25,9 +25,9 @@ class Description(Static): class WelcomeFormScreen(BaseScreen, CreateProfileFormScreen): BINDINGS = [ - CLIVE_PREDEFINED_BINDINGS.form_navigation.previous_screen.create( - action="", description="", show=False - ), # overridden so it does not trigger the action + CLIVE_PREDEFINED_BINDINGS.app.switch_node.create( + action="app.do_nothing", show=False + ), # we want to disable switch_node binding here ] CSS_PATH = [get_relative_css_path(__file__)] SHOW_RAW_HEADER = True diff --git a/clive/__private/ui/forms/form_screen.py b/clive/__private/ui/forms/form_screen.py index 3acb8f74517f8442569931779b44b373cebf2671..84963c87b661216ebc8825cd897566b74f84d96c 100644 --- a/clive/__private/ui/forms/form_screen.py +++ b/clive/__private/ui/forms/form_screen.py @@ -9,7 +9,6 @@ from textual.binding import Binding from textual.reactive import var from clive.__private.core.contextual import Contextual -from clive.__private.ui.bindings import CLIVE_PREDEFINED_BINDINGS from clive.__private.ui.clive_screen import CliveScreen from clive.__private.ui.forms.form_context import FormContextT from clive.__private.ui.forms.navigation_buttons import NextScreenButton, PreviousScreenButton @@ -22,8 +21,6 @@ if TYPE_CHECKING: class FormScreen(CliveScreen, Contextual[FormContextT], ABC): BINDINGS = [ Binding("escape", "previous_screen", "Previous screen", show=False), - CLIVE_PREDEFINED_BINDINGS.form_navigation.previous_screen.create(), - CLIVE_PREDEFINED_BINDINGS.form_navigation.next_screen.create(), ] _should_finish: bool = var(default=False, init=False) # type: ignore[assignment] diff --git a/clive/__private/ui/forms/navigation_buttons.py b/clive/__private/ui/forms/navigation_buttons.py index 7cb8cf28df32ff3479b8bb897ac9856fda2cee0b..49280514884dd21917b0cbb079ae72ed05cc3a60 100644 --- a/clive/__private/ui/forms/navigation_buttons.py +++ b/clive/__private/ui/forms/navigation_buttons.py @@ -52,8 +52,7 @@ class NextScreenButton(CliveButton): self.label = self._determine_label(is_finish=value) def _determine_label(self, *, is_finish: bool) -> str: - key = self.custom_bindings.form_navigation.next_screen.button_display - return f"Finish! ({key})" if is_finish else f"Next ({key}) →" + return "Finish!" if is_finish else "Next" class PreviousScreenButton(OneLineButton): @@ -87,11 +86,7 @@ class PreviousScreenButton(OneLineButton): ) def _get_init_label(self, label: str | None) -> str: - return ( - label - if label is not None - else f"← Previous ({self.custom_bindings.form_navigation.previous_screen.button_display})" - ) + return label if label is not None else "← Previous" class PlaceTaker(Static): diff --git a/clive/__private/ui/screens/base_screen.py b/clive/__private/ui/screens/base_screen.py index e9235e08e6b078eb7e10d909da8626b121de3802..98efdf4bb8d1d39a88dd9c657bd98efcf655dbdb 100644 --- a/clive/__private/ui/screens/base_screen.py +++ b/clive/__private/ui/screens/base_screen.py @@ -3,14 +3,12 @@ from __future__ import annotations from abc import abstractmethod from typing import TYPE_CHECKING, Any, ClassVar -from textual import on from textual.reactive import reactive from textual.widgets import Footer from clive.__private.abstract_class import AbstractClassMessagePump from clive.__private.ui.clive_screen import CliveScreen, ScreenResultT from clive.__private.ui.widgets.clive_basic import CliveHeader, CliveRawHeader -from clive.__private.ui.widgets.clive_basic.clive_header import NodeStatus from clive.__private.ui.widgets.location_indicator import LocationIndicator if TYPE_CHECKING: @@ -35,12 +33,6 @@ class BaseScreen(CliveScreen[ScreenResultT], AbstractClassMessagePump): yield from self.create_main_panel() yield Footer() - @on(NodeStatus.ChangeNodeAddress) - def push_switch_node_address_dialog(self) -> None: - from clive.__private.ui.dialogs.switch_node_address_dialog import SwitchNodeAddressDialog - - self.app.push_screen(SwitchNodeAddressDialog()) - @abstractmethod def create_main_panel(self) -> ComposeResult: """ diff --git a/clive/__private/ui/screens/dashboard/dashboard.py b/clive/__private/ui/screens/dashboard/dashboard.py index 53c8a552dbdffd91d2c8f4404108738d0057e49e..fbfbea5db3ea437c78f0f7a5084b8ca46ce8c712 100644 --- a/clive/__private/ui/screens/dashboard/dashboard.py +++ b/clive/__private/ui/screens/dashboard/dashboard.py @@ -269,15 +269,17 @@ class Dashboard(BaseScreen): BINDINGS = [ CLIVE_PREDEFINED_BINDINGS.help.toggle_help.create( - action="", description="Help" + action="app.help", description="Help" ), # help is a hidden global binding, but we want to show it here CLIVE_PREDEFINED_BINDINGS.dashboard.operations.create(), CLIVE_PREDEFINED_BINDINGS.dashboard.switch_working_account.create(), CLIVE_PREDEFINED_BINDINGS.dashboard.add_account.create(), - CLIVE_PREDEFINED_BINDINGS.dashboard.lock_wallet.create(), CLIVE_PREDEFINED_BINDINGS.app.settings.create( - action="" + action="app.settings" ), # settings is a hidden global binding, but we want to show it here + CLIVE_PREDEFINED_BINDINGS.app.lock_wallet.create( + action="app.lock_wallet" + ), # lock_wallet is a hidden global binding, but we want to show it here ] def __init__(self) -> None: @@ -331,10 +333,6 @@ class Dashboard(BaseScreen): def action_add_account(self) -> None: self.app.push_screen(AddTrackedAccountDialog()) - async def action_lock_wallet(self) -> None: - with self.app._screen_remove_guard.suppress(), self.app._screen_remove_guard.guard(): - await self.app.switch_mode_into_locked() - @property def has_working_account(self) -> bool: return self.profile.accounts.has_working_account diff --git a/clive/__private/ui/screens/transaction_summary/transaction_summary.py b/clive/__private/ui/screens/transaction_summary/transaction_summary.py index 8e4d5665b86965e967e793e280a1086af366fd25..374311298f3aaf630a14bdac7c3597e91e313df2 100644 --- a/clive/__private/ui/screens/transaction_summary/transaction_summary.py +++ b/clive/__private/ui/screens/transaction_summary/transaction_summary.py @@ -150,7 +150,7 @@ class TransactionSummary(BaseScreen): CSS_PATH = [get_relative_css_path(__file__)] BINDINGS = [ Binding("escape", "app.pop_screen", "Back"), - CLIVE_PREDEFINED_BINDINGS.app.load_transaction_from_file.create(action=""), + CLIVE_PREDEFINED_BINDINGS.app.load_transaction_from_file.create(action="app.load_transaction_from_file"), ] # load transaction from file is a hidden global binding, but we want to show it here BIG_TITLE = "transaction summary" diff --git a/clive/__private/ui/widgets/clive_basic/clive_header.py b/clive/__private/ui/widgets/clive_basic/clive_header.py index f886bbd51b28e029280fed3a992b7db84c57c286..c12de29d5806c84b89a96c496f7ce0e7315e2e76 100644 --- a/clive/__private/ui/widgets/clive_basic/clive_header.py +++ b/clive/__private/ui/widgets/clive_basic/clive_header.py @@ -231,9 +231,6 @@ class LockStatus(DynamicOneLineButtonUnfocusable): class NodeStatus(DynamicOneLineButtonUnfocusable): - class ChangeNodeAddress(Message): - """Posted when the user wants to change the node address.""" - def __init__(self) -> None: super().__init__( obj_to_watch=self.world, @@ -252,14 +249,8 @@ class NodeStatus(DynamicOneLineButtonUnfocusable): return "online" @on(OneLineButton.Pressed) - async def post_message_to_change_node_address(self) -> None: - from clive.__private.ui.dialogs.switch_node_address_dialog import SwitchNodeAddressDialog - from clive.__private.ui.screens.settings.switch_node_address import SwitchNodeAddress - - if isinstance(self.app.screen, SwitchNodeAddressDialog | SwitchNodeAddress): - return - - self.screen.post_message(self.ChangeNodeAddress()) + def show_switch_node_address_dialog(self) -> None: + self.app.show_switch_node_address_dialog() class RightPart(Horizontal): diff --git a/pydoclint-errors-baseline.txt b/pydoclint-errors-baseline.txt index 46c17d2a9b78f524a752b08925b88388aaeea9d9..3a06de3ae45aebf9fe0766aa54a3efd2570d9359 100644 --- a/pydoclint-errors-baseline.txt +++ b/pydoclint-errors-baseline.txt @@ -343,7 +343,7 @@ clive/__private/ui/bindings/clive_binding_section.py -------------------- clive/__private/ui/bindings/clive_bindings.py DOC601: Class `CliveBindings`: Class docstring contains fewer class attributes than actual class attributes. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.) - DOC603: Class `CliveBindings`: Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Attributes in the class definition but not in the docstring: [app: App, dashboard: Dashboard, form_navigation: FormNavigation, help: Help, manage_key_aliases: ManageKeyAliases, operations: Operations, transaction_summary: TransactionSummary]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.) + DOC603: Class `CliveBindings`: Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Attributes in the class definition but not in the docstring: [app: App, dashboard: Dashboard, help: Help, manage_key_aliases: ManageKeyAliases, operations: Operations, transaction_summary: TransactionSummary]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.) DOC502: Function `load_custom_bindings` has a "Raises" section in the docstring, but there are not "raise" statements in the body -------------------- clive/__private/ui/clive_screen.py diff --git a/tests/clive-local-tools/clive_local_tools/tui/bindings/custom_bindings.toml b/tests/clive-local-tools/clive_local_tools/tui/bindings/custom_bindings.toml index 59545d515b8de228a1eff113c41dcec9793bcc68..979624ed4d1da5768d1ce2a11400b2c3da64fe54 100644 --- a/tests/clive-local-tools/clive_local_tools/tui/bindings/custom_bindings.toml +++ b/tests/clive-local-tools/clive_local_tools/tui/bindings/custom_bindings.toml @@ -5,23 +5,19 @@ load_transaction_from_file = "c" settings = "d" transaction_summary = "e" quit = "f" +lock_wallet = "g" [dashboard] -operations = "g" -switch_working_account = "h" -add_account = "e" -lock_wallet = "f" - -[form_navigation] -next_screen = "g" -previous_screen = "h" +operations = "h" +switch_working_account = "i" +add_account = "j" [help] -toggle_help = "i" -toggle_table_of_contents = "j" +toggle_help = "k" +toggle_table_of_contents = "l" [manage_key_aliases] -add_new_alias = "k" -load_from_file = "l" +add_new_alias = "m" +load_from_file = "n" # this file intentionally doesn't define all bindings diff --git a/tests/clive-local-tools/clive_local_tools/tui/bindings/default_bindings.toml b/tests/clive-local-tools/clive_local_tools/tui/bindings/default_bindings.toml index bbd60801b499412f96a358afd9e46ade8c98e22e..f2137fec622babc6d531cc644a2f73be7035c910 100644 --- a/tests/clive-local-tools/clive_local_tools/tui/bindings/default_bindings.toml +++ b/tests/clive-local-tools/clive_local_tools/tui/bindings/default_bindings.toml @@ -2,7 +2,9 @@ clear_notifications = "ctrl+x" dashboard = "ctrl+d" load_transaction_from_file = "ctrl+o" +lock_wallet = "ctrl+l" settings = "ctrl+s" +switch_node = "ctrl+n" transaction_summary = "ctrl+t" quit = "ctrl+q" @@ -10,11 +12,6 @@ quit = "ctrl+q" operations = "o" switch_working_account = "w" add_account = "a" -lock_wallet = "ctrl+l" - -[form_navigation] -next_screen = "ctrl+n" -previous_screen = "ctrl+b" [help] toggle_help = "f1,?" diff --git a/tests/tui/test_create_profile.py b/tests/tui/test_create_profile.py index f3c0700e4908265ef66ed43cc8cd932e4396766e..05ad1f0ff14bea9ddbcb6d184ba78bb2d437bf9f 100644 --- a/tests/tui/test_create_profile.py +++ b/tests/tui/test_create_profile.py @@ -29,6 +29,7 @@ from clive_local_tools.tui.checkers import ( from clive_local_tools.tui.clive_quit import clive_quit from clive_local_tools.tui.textual_helpers import ( focus_next, + focus_prev, press_and_wait_for_screen, write_text, ) @@ -87,6 +88,7 @@ async def create_profile_mark_account_as_watched(pilot: ClivePilot) -> None: assert pilot.app.screen.query_exactly_one(WorkingAccountCheckbox).value is False, ( "Expected 'Working account?' to be unchecked!" ) + await focus_prev(pilot) # Go to `Account name` input async def create_profile_set_key_and_alias_name(pilot: ClivePilot, alias_name: str, private_key: str) -> None: @@ -102,7 +104,7 @@ async def create_profile_set_key_and_alias_name(pilot: ClivePilot, alias_name: s async def create_profile_finish(pilot: ClivePilot) -> None: - await press_and_wait_for_screen(pilot, CLIVE_PREDEFINED_BINDINGS.form_navigation.next_screen.key, Dashboard) + await press_and_wait_for_screen(pilot, "enter", Dashboard) assert_is_dashboard(pilot) diff --git a/tests/unit/bindings/test_clive_bindings.py b/tests/unit/bindings/test_clive_bindings.py index 6afb7c3b7eb1fbfb548f96fbb714a97ae893125b..72d45bd8bba496be9c59a1726930fabf5dfd285a 100644 --- a/tests/unit/bindings/test_clive_bindings.py +++ b/tests/unit/bindings/test_clive_bindings.py @@ -118,7 +118,16 @@ def test_load_custom_bindings() -> None: # ASSERT loaded_data = bindings.to_dict() - assert all(loaded_data.get(k) == v for k, v in raw_data.items()), "loaded bindings should be superset of toml file" + for section_name, raw_value in raw_data.items(): + assert section_name in loaded_data, f"binding section `{section_name}` from file `{raw_data}` has invalid name" + loaded_value = loaded_data[section_name] + assert isinstance(raw_value, dict), ( + f"binding section `{section_name}` from file `{raw_data}` should be convertible to dict" + ) + assert loaded_value.items() >= raw_value.items(), ( + f"loaded bindings in section `{section_name}` with default values should be superset of toml file" + f" parsed data: `{loaded_value}` raw data in file: `{raw_value}`" + ) def test_load_empty_bindings() -> None: