Decouple ColorsBox from PaintApp

There's no practical utility in this unless I want to make the palette size variable,
and this makes performance worse updating the screen when the palette changes,
but I'm on a mission to remove `from textual_paint.paint import PaintApp`
since it can't be at top level due to cyclic imports, and it has
side effects when importing `textual_paint.args` in turn.
This commit is contained in:
Isaiah Odhner 2023-09-15 23:18:22 -04:00
parent a2cf93ae99
commit 0647e7be8b
2 changed files with 32 additions and 30 deletions

View File

@ -5,12 +5,19 @@ from textual import events
from textual.app import ComposeResult
from textual.containers import Container
from textual.message import Message
from textual.reactive import var
from textual.widgets import Button
from textual_paint.char_input import DOUBLE_CLICK_TIME, CharInput
class ColorsBox(Container):
"""Color palette widget."""
palette: var[tuple[str, ...]] = var(tuple())
"""A tuple of colors to display.
A tuple is used because list mutations can't be watched.
"""
class ColorSelected(Message):
"""Message sent when a color is selected."""
def __init__(self, color: str, as_foreground: bool) -> None:
@ -18,14 +25,15 @@ class ColorsBox(Container):
self.as_foreground = as_foreground
super().__init__()
class EditColor(Message):
"""Message sent when a color is selected."""
def __init__(self, color_index: int, as_foreground: bool) -> None:
self.color_index = color_index
self.as_foreground = as_foreground
super().__init__()
def compose(self) -> ComposeResult:
"""Add our selected color and color well buttons."""
if TYPE_CHECKING:
from textual_paint.paint import PaintApp
assert isinstance(self.app, PaintApp)
# TODO: decouple from PaintApp
# Could accept palette in constructor
palette = self.app.palette
self.color_by_button: dict[Button, str] = {}
with Container(id="palette_selection_box"):
@ -33,27 +41,21 @@ class ColorsBox(Container):
# and showing/editing the current character.
# I haven't settled on naming for this yet.
yield CharInput(id="selected_color_char_input", classes="color_well")
with Container(id="available_colors"):
for color in palette:
button = Button("", classes="color_button color_well")
button.styles.background = color
button.can_focus = False
self.color_by_button[button] = color
yield button
yield Container(id="available_colors")
def update_palette(self) -> None: # , palette: list[str]) -> None:
"""Update the palette with new colors."""
if TYPE_CHECKING:
from textual_paint.paint import PaintApp
assert isinstance(self.app, PaintApp)
# TODO: decouple from PaintApp
# Could accept palette as argument
palette = self.app.palette
for button, color in zip(self.query(".color_button").nodes, palette):
assert isinstance(button, Button)
def watch_palette(self, palette: tuple[str, ...]) -> None:
"""Called when the palette is changed."""
# TODO: optimize; don't remove or add buttons unless the palette size changes
# Side note: the palette size never changes in the current implementation.
self.query(".color_button").remove()
self.color_by_button.clear()
container = self.query_one("#available_colors")
for color in palette:
button = Button("", classes="color_button color_well")
button.styles.background = color
button.can_focus = False
self.color_by_button[button] = color
container.mount(button)
last_click_time = 0
last_click_button: Button | None = None
@ -68,10 +70,6 @@ class ColorsBox(Container):
self.post_message(self.ColorSelected(self.color_by_button[button], secondary))
# Detect double click and open Edit Colors dialog.
if event.time - self.last_click_time < DOUBLE_CLICK_TIME and button == self.last_click_button:
if TYPE_CHECKING:
from textual_paint.paint import PaintApp
assert isinstance(self.app, PaintApp)
# TODO: decouple from PaintApp
self.app.action_edit_colors(self.query(".color_button").nodes.index(button), secondary)
self.post_message(self.EditColor(self.query(".color_button").nodes.index(button), secondary))
self.last_click_time = event.time
self.last_click_button = button

View File

@ -281,7 +281,7 @@ class PaintApp(App[None]):
def watch_palette(self, palette: tuple[str, ...]) -> None:
"""Called when palette changes."""
self.query_one("ColorsBox", ColorsBox).update_palette()
self.query_one("ColorsBox", ColorsBox).palette = palette
def watch_selected_bg_color(self, selected_bg_color: str) -> None:
"""Called when selected_bg_color changes."""
@ -1280,6 +1280,10 @@ class PaintApp(App[None]):
"""Swap the foreground and background colors."""
self.selected_bg_color, self.selected_fg_color = self.selected_fg_color, self.selected_bg_color
def on_colors_box_edit_color(self, event: ColorsBox.EditColor) -> None:
"""Called when a color is double-clicked in the palette."""
self.action_edit_colors(color_palette_index=event.color_index, as_foreground=event.as_foreground)
def action_edit_colors(self, color_palette_index: int|None = None, as_foreground: bool = False) -> None:
"""Show dialog to edit colors."""
self.close_windows("#edit_colors_dialog")