Skip to content
Snippets Groups Projects

Refactor Cart into checkerboard table

Merged Mateusz Kudela requested to merge mkudela/issue-173 into develop
All threads resolved!
Compare and Show latest version
1 file
+ 49
50
Compare changes
  • Side-by-side
  • Inline
from __future__ import annotations
import contextlib
from typing import TYPE_CHECKING, Sequence
from textual import on
from textual.binding import Binding
from textual.containers import Horizontal
from textual.css.query import NoMatches
from textual.message import Message
from textual.reactive import reactive
from textual.widgets import Static
from typing_extensions import Self
from clive.__private.core.formatters.humanize import humanize_operation_details, humanize_operation_name
from clive.__private.ui.get_css import get_relative_css_path
@@ -68,18 +71,16 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget):
super().__init__()
class Move(Message):
def __init__(self, from_idx: int, to_idx: int, button_to_focus: str | None) -> None:
def __init__(self, from_idx: int, to_idx: int) -> None:
self.from_idx = from_idx
self.to_idx = to_idx
self.button_to_focus = button_to_focus
super().__init__()
class Focus(Message):
"""Message sent when other CartItem should be focused."""
def __init__(self, target_idx: int, button_to_focus: str | None) -> None:
def __init__(self, target_idx: int) -> None:
self.target_idx = target_idx
self.button_to_focus = button_to_focus
super().__init__()
def __init__(self, operation_idx: int) -> None:
@@ -90,15 +91,17 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget):
to remove an operation and there are a large number of operations in the shopping basket."""
assert self.is_valid(self._idx), "During construction, index has to be valid"
self._button_move_up = ButtonMoveUp(disabled=self._is_first)
self._button_move_down = ButtonMoveDown(disabled=self._is_last)
self._button_delete = ButtonDelete()
self._index_cell = CliveCheckerBoardTableCell(self.get_operation_index(), classes="index")
self._horizontal_with_buttons = Horizontal(self._button_move_up, self._button_move_down, self._button_delete)
super().__init__(
self._index_cell,
CliveCheckerBoardTableCell(self.get_operation_name(), classes="operation-type"),
CliveCheckerBoardTableCell(self.get_operation_details(), classes="operation-details"),
CliveCheckerBoardTableCell(
Horizontal(
ButtonMoveUp(disabled=self._is_first), ButtonMoveDown(disabled=self._is_last), ButtonDelete()
),
self._horizontal_with_buttons,
classes="actions",
),
)
@@ -128,22 +131,40 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget):
return humanize_operation_name(self.operation) if self.is_valid(self._idx) else "?"
def action_select_previous(self) -> None:
focused = self.get_focused_button_or_none()
self.post_message(self.Focus(target_idx=self._idx - 1, button_to_focus=focused))
self.post_message(self.Focus(target_idx=self._idx - 1))
def action_select_next(self) -> None:
focused = self.get_focused_button_or_none()
self.post_message(self.Focus(target_idx=self._idx + 1, button_to_focus=focused))
self.post_message(self.Focus(target_idx=self._idx + 1))
def get_operation_details(self) -> str:
return humanize_operation_details(self.operation) if self.is_valid(self._idx) else "?"
def get_focused_button_or_none(self) -> str | None:
buttons = self.query(CliveButton)
for button in buttons:
if button.has_focus:
return button.id
return None
def focus(self, _: bool = True) -> Self: # noqa: FBT001, FBT002
if focused := self.app.focused: # Focus the corresponding button as it was before
assert focused.id, "Previously focused widget has no id!"
with contextlib.suppress(NoMatches):
previous = self._horizontal_with_buttons.get_child_by_id(focused.id)
if previous.focusable:
previous.focus()
return self
for button in reversed(self._horizontal_with_buttons.children): # Focus first focusable
if button.focusable:
button.focus()
return self
@property
def button_move_up(self) -> ButtonMoveUp:
return self._button_move_up
@property
def button_move_down(self) -> ButtonMoveDown:
return self._button_move_down
@property
def button_delete(self) -> ButtonDelete:
return self._button_delete
@property
def operation(self) -> OperationBase:
@@ -164,13 +185,11 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget):
@on(CliveButton.Pressed, "#move-up-button")
def move_up(self) -> None:
focused = self.get_focused_button_or_none()
self.post_message(self.Move(from_idx=self._idx, to_idx=self._idx - 1, button_to_focus=focused))
self.post_message(self.Move(from_idx=self._idx, to_idx=self._idx - 1))
@on(CliveButton.Pressed, "#move-down-button")
def move_down(self) -> None:
focused = self.get_focused_button_or_none()
self.post_message(self.Move(from_idx=self._idx, to_idx=self._idx + 1, button_to_focus=focused))
self.post_message(self.Move(from_idx=self._idx, to_idx=self._idx + 1))
@on(CliveButton.Pressed, "#delete-button")
def delete(self) -> None:
@@ -211,18 +230,13 @@ class CartTable(CliveCheckerboardTable):
]
return self._rows
@staticmethod
def _create_widget_id_to_focus(event: CartItem.Focus | CartItem.Move) -> str:
assert event.button_to_focus is not None, "Button to focus has to be passed to get it's id."
return f"#{event.button_to_focus}"
def _disable_appropriate_button(self, widget: CartItem) -> None:
if len(self.app.world.profile.cart) > 0 and widget.reactive_idx == 0:
# disable first ButtomMoveUp if first element was removed
self.query(ButtonMoveUp)[0].disabled = True
# disable ButtomMoveUp if first element was removed
self._rows[0].button_move_up.disabled = True
if 0 < len(self.app.world.profile.cart) == widget.reactive_idx:
# disable last ButtonMoveDown if only last element was removed
self.query(ButtonMoveDown)[-1].disabled = True
# disable ButtonMoveDown if only last element was removed
self._rows[-1].button_move_down.disabled = True
def _handle_remove_event(self, triggering_widget: CartItem) -> None:
def devalue_indexes(rows: Sequence[CartItem]) -> None:
@@ -270,49 +284,34 @@ class CartTable(CliveCheckerboardTable):
self.profile.cart.swap(event.from_idx, event.to_idx)
self.app.trigger_profile_watchers()
self._update_values_of_swapped_rows(from_index=event.from_idx, to_index=event.to_idx)
# focus item that was moved
for cart_item in self._rows:
if event.to_idx == cart_item.reactive_idx:
self.app.set_focus(cart_item)
if event.button_to_focus:
button_id = self._create_widget_id_to_focus(event)
cart_item.query_one(button_id).focus()
cart_item.focus()
@on(CartItem.Focus)
def focus_item(self, event: CartItem.Focus) -> None:
for cart_item in self._rows:
if event.target_idx == cart_item.reactive_idx:
self.app.set_focus(cart_item)
if event.button_to_focus:
button_id = self._create_widget_id_to_focus(event)
cart_item.query_one(button_id).focus()
cart_item.focus()
class Cart(BaseScreen):
CSS_PATH = [get_relative_css_path(__file__)]
BINDINGS = [
Binding("escape", "app.pop_screen", "Back"),
Binding("f9", "clear_all", "Clear all"),
Binding("f6", "summary", "Summary"),
]
BIG_TITLE = "operations cart"
def __init__(self) -> None:
super().__init__()
self.__scrollable_part = ScrollablePart()
self._scrollable_part = ScrollablePart()
self._cart_table = CartTable()
def create_main_panel(self) -> ComposeResult:
with self.__scrollable_part:
with self._scrollable_part:
yield self._cart_table
async def __rebuild_items(self, from_index: int = 0, to_index: int | None = None) -> None:
await self._cart_table.rebuild(starting_from_element=from_index, ending_with_element=to_index)
def action_summary(self) -> None:
self.app.push_screen(TransactionSummaryFromCart())
async def action_clear_all(self) -> None:
self.profile.cart.clear()
self.app.trigger_profile_watchers()
await self.__rebuild_items()
Loading