Skip to content
Snippets Groups Projects

Refactor Cart into checkerboard table

Merged Mateusz Kudela requested to merge mkudela/issue-173 into develop
Compare and Show latest version
1 file
+ 73
72
Compare changes
  • Side-by-side
  • Inline
@@ -52,7 +52,7 @@ class ButtonDelete(CliveButton):
super().__init__("Remove", id_="delete-button", variant="error")
class CartItem(CliveCheckerboardTableRow, CliveWidget, can_focus=True):
class CartItem(CliveCheckerboardTableRow, CliveWidget):
reactive_idx: reactive[int] = reactive(0)
"""Row of CartTable."""
BINDINGS = [
@@ -66,9 +66,10 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget, can_focus=True):
super().__init__()
class Move(Message):
def __init__(self, from_idx: int, to_idx: int) -> None:
def __init__(self, from_idx: int, to_idx: int, focus_button: str | None) -> None:
self.from_idx = from_idx
self.to_idx = to_idx
self.focus_button = focus_button
super().__init__()
class Focus(Message):
@@ -80,11 +81,16 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget, can_focus=True):
super().__init__()
def __init__(self, operation_idx: int) -> None:
self.__idx = operation_idx
assert self.is_valid(self.__idx), "During construction, index has to be valid"
self._idx = operation_idx
self._delete_already_pressed = False
"""This could be a situation where the user is trying to delete an operation that is already deleted
(textual has not yet deleted the widget visually). This situation is possible if the user clicks so quickly
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._index_cell = CliveCheckerBoardTableCell(self.get_operation_index(), classes="index")
super().__init__(
CliveCheckerBoardTableCell(self.get_operation_index(), classes="index"),
self._index_cell,
CliveCheckerBoardTableCell(self.get_operation_name(), classes="operation-type"),
CliveCheckerBoardTableCell(self.get_operation_details(), classes="operation-details"),
CliveCheckerBoardTableCell(
@@ -96,41 +102,41 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget, can_focus=True):
)
def __repr__(self) -> str:
return f"{self.__class__.__name__}(idx={self.__idx})"
return f"{self.__class__.__name__}(idx={self._idx})"
def on_mount(self) -> None:
self.reactive_idx = self.__idx
self.reactive_idx = self._idx
if self.__is_first:
self.unbind("ctrl+up")
elif self.__is_last:
self.unbind("ctrl+down")
def watch_reactive_idx(self, idx: int) -> None:
assert self.is_valid(idx)
self.__idx = idx
self.query_one(".index").update_content(self.get_operation_index()) # type: ignore[attr-defined]
assert self.is_valid(idx), "idx is invalid when trying to update."
self._idx = idx
self._index_cell.update_content(self.get_operation_index())
def is_valid(self, idx: int) -> bool:
return idx < self.__operations_count
def get_operation_index(self) -> str:
return f"{self.__idx + 1}." if self.is_valid(self.idx) else "?"
return f"{self._idx + 1}." if self.is_valid(self.idx) else "?"
def get_operation_name(self) -> str:
return humanize_operation_name(self.operation) if self.is_valid(self.idx) else "?"
def action_select_previous(self) -> None:
focused = self.is_any_button_focused()
self.post_message(self.Focus(target_idx=self.__idx - 1, focus_button=focused))
focused = self.get_focused_button_or_none()
self.post_message(self.Focus(target_idx=self._idx - 1, focus_button=focused))
def action_select_next(self) -> None:
focused = self.is_any_button_focused()
self.post_message(self.Focus(target_idx=self.__idx + 1, focus_button=focused))
focused = self.get_focused_button_or_none()
self.post_message(self.Focus(target_idx=self._idx + 1, focus_button=focused))
def get_operation_details(self) -> str:
return humanize_operation_details(self.operation) if self.is_valid(self.idx) else "?"
def is_any_button_focused(self) -> str | None:
def get_focused_button_or_none(self) -> str | None:
buttons = self.query(CliveButton)
for button in buttons:
if button.has_focus:
@@ -139,12 +145,12 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget, can_focus=True):
@property
def idx(self) -> int:
return self.__idx
return self._idx
@property
def operation(self) -> OperationBaseClass:
assert self.is_valid(self.idx), "cannot get operation, position is invalid"
return self.profile.cart[self.__idx]
assert self.is_valid(self._idx), "cannot get operation, position is invalid"
return self.profile.cart[self._idx]
@property
def __operations_count(self) -> int:
@@ -152,32 +158,34 @@ class CartItem(CliveCheckerboardTableRow, CliveWidget, can_focus=True):
@property
def __is_first(self) -> bool:
return self.__idx == 0
return self._idx == 0
@property
def __is_last(self) -> bool:
return self.__idx == self.__operations_count - 1
return self._idx == self.__operations_count - 1
@on(CliveButton.Pressed, "#move-up-button")
def move_up(self) -> None:
self.post_message(self.Move(from_idx=self.__idx, to_idx=self.__idx - 1))
focused = self.get_focused_button_or_none()
self.post_message(self.Move(from_idx=self._idx, to_idx=self._idx - 1, focus_button=focused))
@on(CliveButton.Pressed, "#move-down-button")
def move_down(self) -> None:
self.post_message(self.Move(from_idx=self.__idx, to_idx=self.__idx + 1))
focused = self.get_focused_button_or_none()
self.post_message(self.Move(from_idx=self._idx, to_idx=self._idx + 1, focus_button=focused))
@on(CliveButton.Pressed, "#delete-button")
def delete(self) -> None:
cart = self.app.query_one(Cart)
if self._delete_already_pressed:
return
if cart.ready_to_delete_item:
cart.toggle_removal_in_progress()
self.post_message(self.Delete(self))
self._delete_already_pressed = True
self.post_message(self.Delete(self))
@on(Move)
def move_item(self, event: CartItem.Move) -> None:
if event.to_idx == self.__idx:
self.__idx = event.from_idx
if event.to_idx == self._idx:
self._idx = event.from_idx
self.app.trigger_profile_watchers()
@@ -194,46 +202,16 @@ class CartTable(CliveCheckerboardTable):
def __init__(self) -> None:
super().__init__(header=CartHeader(), title=Static(""))
self._rows: list[CartItem] = []
def create_static_rows(self, start_index: int = 0, end_index: int | None = None) -> list[CartItem]:
if end_index:
assert (
end_index <= len(self.app.world.profile.cart) - 1
), "End index is greater than cart's last item index"
return [
assert end_index <= len(self.app.world.profile.cart) - 1, "End index is greater than cart's last item index"
self._rows = [
CartItem(idx)
for idx in range(start_index, len(self.app.world.profile.cart) if end_index is None else end_index + 1)
]
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._cart_table = CartTable()
self._removal_in_progress = False
@property
def ready_to_delete_item(self) -> bool:
return not self._removal_in_progress
def toggle_removal_in_progress(self) -> None:
self._removal_in_progress = not self._removal_in_progress
def create_main_panel(self) -> ComposeResult:
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)
return self._rows
def _handle_remove_event(self, triggering_widget: CartItem) -> None:
def devalue_indexes(rows: Sequence[CartItem]) -> None:
@@ -241,13 +219,12 @@ class Cart(BaseScreen):
row.reactive_idx = row.reactive_idx - 1
start_index = triggering_widget.reactive_idx
rows = self.query(CartItem)[start_index:]
devalue_indexes(rows)
self._cart_table.set_evenness_styles(rows, starting_index=start_index)
devalue_indexes(self._rows[start_index:])
self.set_evenness_styles(self._rows[start_index:], starting_index=start_index)
def _update_values_of_swapped_rows(self, from_index: int, to_index: int) -> None:
def get_cells_with_values(row_index: int) -> tuple[list[CliveCheckerBoardTableCell], list[str | Widget]]:
row = self.query(CartItem)[row_index]
row = self._rows[row_index]
start_index = 1 # exclude value of first cell - index
end_index = -1 # exclude value of last cell - horizontal with buttons
cells = row.query(CliveCheckerBoardTableCell)[start_index:end_index]
@@ -263,11 +240,11 @@ class Cart(BaseScreen):
@on(CartItem.Delete)
async def remove_item(self, event: CartItem.Delete) -> None:
await self.query(CliveCheckerboardTableRow)[event.widget.reactive_idx].remove()
self._rows.remove(event.widget)
await event.widget.remove()
self.profile.cart.remove(event.widget.operation)
self._handle_remove_event(triggering_widget=event.widget)
self.app.trigger_profile_watchers()
self._removal_in_progress = False
if len(self.app.world.profile.cart) > 0 and event.widget.reactive_idx == 0:
# disable first ButtomMoveUp if first element was removed
@@ -285,18 +262,42 @@ class Cart(BaseScreen):
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.query(CartItem):
for cart_item in self._rows:
if event.to_idx == cart_item.reactive_idx:
self.app.set_focus(cart_item)
if event.focus_button:
cart_item.query_one(f"#{event.focus_button}").focus()
@on(CartItem.Focus)
def focus_item(self, event: CartItem.Focus) -> None:
for cart_item in self.query(CartItem):
for cart_item in self._rows:
if event.target_idx == cart_item.reactive_idx:
self.app.set_focus(cart_item)
if event.focus_button:
cart_item.query_one(f"#{event.focus_button}").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._cart_table = CartTable()
def create_main_panel(self) -> ComposeResult:
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())
Loading