I'm trying to create a Python Textual Widget that can be reused on a lot of places (basic a search widget). It should have a grid with the title, any number of labels with inputs, and a last line for the buttons (Search and Cancel). While I was able to create the Widget, whenever I reuse it, it seems that the CSS I used is being ignored, and only the first CSS is actually applied, can someone please help me out? Here's the code for the Widget:
from textual.app import ComposeResult
from textual.screen import ModalScreen
from textual.widgets import Button, Static, Input
from textual.containers import Grid
from textual import events
class DSASearchDialog(ModalScreen[dict]):
"""Generic dialog to create a search dialog. If you need anything more complex, please write your own dialog"""
dialog_title = 'Sample Search Dialog'
dialog_inputs = [
{
'name': 'Sample Label 1',
'id': 'dsa-search-sample-name-label-1',
'classes': 'dsa-search-label',
'input-classes': 'optional',
'type': 'text'
},
{
'name': 'Sample Label 2',
'id': 'dsa-search-sample-name-label-2',
'classes': 'dsa-search-label',
'input-classes': 'optional',
'type': 'text'
},
{
'name': 'Sample Label 3',
'id': 'dsa-search-sample-name-label-3',
'classes': 'dsa-search-label',
'input-classes': 'mandatory',
'type': 'text'
}
]
def build_base_css(self) -> None:
"""Build the base css"""
# Each input has a height of 3
input_height = len(self.dialog_inputs)*3
# plus 4 for the buttons and an extra 3 for the title
total_height = input_height + 4 + 5
# Create the rows sizes
grid_rows_txt = "grid-rows: 3" # title
# each input
max_label_length = 10
for i in range(len(self.dialog_inputs)):
grid_rows_txt += " 3"
if len(self.dialog_inputs[i]['name']) > max_label_length:
max_label_length = len(self.dialog_inputs[i]['name'])+3
grid_rows_txt += " 4;" #buttons
self.CSS = ""\
f"#dsa-search-grid"+" {\n" \
f"grid-size: 1 {len(self.dialog_inputs)+2};\n"\
f"{grid_rows_txt}\n"\
"background: $surface;\n"\
"border: thick $background 80%;\n"\
"padding-right: 1;\n"\
"padding-left: 1;\n"\
"width: 80;\n"\
f"height: {total_height};\n"\
"}\n"\
".dsa-input-field {\n"\
"grid-size: 2;\n"\
"align: center middle;\n"\
f"grid-columns: {max_label_length} 1fr;\n"\
"}"\
def compose(self) -> ComposeResult:
"""Dynamically build the dialog based on the amount of inputs"""
inputs_grid = Grid(
Static(self.dialog_title, classes='dsa-search-title'),
id=f"dsa-search-grid"
)
# For each input, create the Static and Input widgets, and mount it on the inputs_grid
for input_data in self.dialog_inputs:
grid_input = Grid(
Static(input_data['name'], classes=input_data['classes'], id='dsa-search-label'),
Input(id=f"{input_data['id']}-input", classes=input_data['input-classes']),
classes='dsa-input-field',
id=input_data['id']
)
inputs_grid.mount(grid_input)
# Create the buttons
buttons_grid = Grid(
Button('Search', id='dsa-search-button', classes='button'),
Button('Cancel', id='dsa-cancel-button', classes='button', variant='error'),
id='dsa-search-buttons'
)
inputs_grid.mount(buttons_grid)
# Now yield everything
yield inputs_grid
def on_key(self, event: events.Key) -> None:
"""When the escape key is pressed, dismiss the modal, when the enter is pressed, execute the search"""
if event.key == 'escape':
self.dismiss(None)
elif event.key == 'enter':
self.on_button_pressed(Button.Pressed(self.query_one('#dsa-search-button'))) # Call the button pressed method to get the information and dismiss the modal
event.stop()
def on_button_pressed(self, event: Button.Pressed) -> None:
"""When the search button is pressed, get the information and dismiss the modal"""
# Get the information
information = {}
for input_data in self.dialog_inputs:
information[input_data['id']] = self.query_one(f'#{input_data["id"]}-input').value
# Dismiss the modal
if event.button.id == 'dsa-search-button':
self.dismiss(information)
else:
self.dismiss(None)
event.stop() # Stop the event propagation so the modal doesn't dismiss itself)
And here's how to use it (just a snippet of course):
def action_search(self) -> None:
"""Start the generic search dialog, and return the data needed"""
dlg = DSASearchDialog()
dlg.dialog_title = "Search Users on DSA"
dlg.dialog_inputs = [
{
'name': 'E-Mail:',
'id': 'dsa-search-username-label',
'classes': 'dsa-search-label',
'input-classes': 'optional',
'type': 'text'
},
{
'name': 'User ID:',
'id': 'dsa-search-user-id-label',
'classes': 'dsa-search-label',
'input-classes': 'optional',
'type': 'number'
}
]
dlg.build_base_css()
self.app.push_screen(dlg, self.__search_return)
If I call that method, it creates the widget correctly, however if I call after this a simpler one (for instance with only one dialog_input), it keeps the same CSS applied, and everything is kind out of order.
I've tried creating a random unique id for each widget (based on the name), and only the first one is still being applied, it seems that the self.CSS = ...
code block is not being called, but the compose is actually trying to call the correct CSS.
Can someone help me out?
Best regards.
Dynamically creating your CSS is not advisable. Some of the work you are doing to build the
CSS
attribute can be done in plain CSS. For instanceheight
can be set toauto
which will calculate an optimal height automatically. You can also useauto
ingrid-columns
to automatically calculate the height.If there is anything which is too dynamic to express in CSS, it is best done in
on_mount
where you can update styles programatically. The reference for each style shows how you can set the style via Python.