# Tables These views show records in a table: a table that the user edits, and a large table that reads its rows from a source. ## A table of records A `TableView` shows text that the app formats itself. A `RecordTableView` shows `attrs` records, with a column per field, such as the records that the `fetch` of a [SQLite model](sqlite-model.md) returns: ```python from composeui.core.view import Column, RecordTableView, T, child runs: RecordTableView[Run] = child( RecordTableView[Run], columns=[Column("id", T("Run")), Column("radius", T("Radius"), editable=True)], ) ``` - Assign `records` to update the table (`table.records = model.runs.above(0.1)`). - Read `selected_record`, or `selected_records`. - Connect to `row_activated` to open a row: a double click, or Enter, emits the row's index in `rows`. A double click on a cell that edits, or on a button or a link, doesn't. On Textual and the web, Enter also selects the row first. - The table parses an edited cell into the type of its field, through the `cattrs` converter of the views. `record_edited` emits the old and the new record. - When a cell doesn't parse, the table refuses the edit and says why in the status bar. The study saves and loads the records by their type. So build the table as `RecordTableView[Run]`, or subclass it. ### Sorting and filtering Mark a column that the user sorts and filters: `Column("radius", T("Radius"), sortable=True, filterable=True)`. On a `TableView`, `sortable_columns` and `filterable_columns` list these columns. A click on a sortable header sorts by its column: ascending, then descending, then back in the app's order. While a column is filterable, a bar above the table has: - a search over the text of the filterable columns, once the user stops typing for `SEARCH_DELAY_MS` (300 ms), or presses Enter; - "+ Filter", which adds a filter on a column. The filter shows as a chip. A click on the chip opens it for editing, and so does Enter, since the focus lands on the chip. A filter accepts these values: - In a text column, ignoring case: - the text: the cells that contain it; - `=kitchen`: the cells equal to it; - `!=kitchen`: the cells not equal to it; - `!kit`: the cells that do not contain it; - `kit*`: the cells that start with it; - `*en`: the cells that end with it; - with the `.*` toggle of the editor on, a regular expression, for example `Room [1-3]`. The chip reads "Room matches Room [1-3]". The app sets this filter as `as_pattern("Room [1-3]")`, between slashes. - In a number or a date column: `>20`, `>=20`, `<20`, `<=20`, `!=20`, a range `18..24` (both ends included), or a bare value, which means `=`. - In a boolean column: `true` or `false`. - In any column: `=` alone, for the empty cells. A `RecordTableView` sorts by the values of the fields, so numbers and dates sort as numbers and dates. A `TableView` sorts its text. The app sets the order and the filters too: `sort_column`, `sort_descending`, `search`, `filters`, or `sort_by(column)`, as a click does. The study saves them. `shown_rows` lists the rows shown, in order, as indexes into `rows`. `selected_rows`, `selection_changed`, and the edited row always use indexes into `rows`, whatever the order. When a search or a filter hides a selected row, the table deselects it. ### Widgets in cells A column can show a widget in each of its cells, such as a check box to keep a variable or a combobox to pick its role. Set the column's `widget` to a `CellWidget`: ```python from composeui.core.view import CellWidget, Column, RecordTableView, T, child variables: RecordTableView[Variable] = child( RecordTableView[Variable], columns=[ Column("is_kept", T("Keep"), editable=True, widget=CellWidget.check_box(check_all=True)), Column("name", T("Variable")), Column("role", T("Role"), editable=True, widget=CellWidget.combobox()), Column("distribution", T("Distribution"), widget=CellWidget.button()), Column("doc", T("Help"), widget=CellWidget.link()), Column("data", T("Data"), editable=True, widget=CellWidget.file_path({"CSV": (".csv",)})), ], ) ``` On a `TableView`, `cell_widgets` maps each column's index to its `CellWidget`. - **`CellWidget.check_box()`** shows a check box. The box is checked when the cell's text is "true", ignoring case. A change sets the text to "True" or "False". - **`CellWidget.combobox(options)`** shows the cell's text in a combobox of `options`. Without options, on an enum field of a `RecordTableView`, it offers the enum's members. - **`CellWidget.button(text)`** shows the cell's text, then a button ("…" by default). A text such as `T("Retry")` follows the app's language. - **`CellWidget.link()`** shows the cell's text as a link. - **`CellWidget.file_path(extensions)`** shows a file's path, then a "…" button that picks a file, as a [file path input](widgets.md#file-path-input) does. The dialog offers the file types of `extensions`, such as `{"CSV": (".csv",)}`; the web refuses a file of another type. Its `text`, "Choose a file" by default, names the button for a tooltip and a screen reader. With `server_roots`, such as `CellWidget.file_path(extensions, server_roots=(studies,))`, the web lists the server's files in those folders, as a [field with server roots](widgets.md#files-on-the-server) does, instead of uploading a copy. - **`CellWidget.folder_path()`** shows a folder's path, then a "…" button that picks a folder, as a [folder path input](widgets.md#folder-path-input) does. - **`CellWidget.save_path(extensions)`** shows a path to save a file to, then a "…" button that picks it, as a [save path input](widgets.md#save-path-input) does. The file may not exist yet. The first of `extensions` is added to a name without one of them. With `confirms_overwrite` (on by default), the user agrees to replace a file that exists before it is picked. Picking a path writes nothing: the app writes the file. - **`CellWidget.path_by(kind_column, editors)`** picks the path widget of each row by the row's kind (see [Paths by the row's kind](#paths-by-the-rows-kind)). The three path widgets take `server_roots` and `base_folder`, as the path inputs do (see [Relative paths](widgets.md#relative-paths)). With `base_folder`, a relative path in the cell names a path in that folder, and a path picked in it shows relative to it. A check box, a combobox, or a path changes its cell only when the column is editable. Otherwise it only shows the cell, and the "…" button picks nothing. The change is an edit, as when the user types in a cell: `cell_edited` emits it, and on a `RecordTableView`, `record_edited` too. The user also types in a path cell, but not in the other cells with a widget. On the web, a path typed outside a column's `server_roots` is refused, `..` and symbolic links followed: the cell keeps its text, and the status bar says why. `file_texts` holds the texts of the list of the server's files and of that message, by what they're for. A click on a button or a link emits `cell_clicked` with the row and the column. The row is an index into `rows`, whatever the order. With `check_all=True`, the column's header starts with a check box. The box is checked when all the shown rows are, and partly checked when only some are. A click checks every shown row, or unchecks them all when all are checked. Each changed cell is an edit. The app does the same with `check_all(column, is_checked=True)`. `column_checked(column)` tells whether the shown rows are all checked (`True`), none (`False`), or some (`None`). The study does not save `cell_widgets`: they are the app's setup. [`pipechart.py`](../../src/examples/pipechart.py) shows or hides each curve with a check box, and picks how to draw it with a combobox. [`taskview.py`](../../src/examples/taskview.py) runs a task again from its Retry button, and opens the error of a failed task from a link. What each backend shows: - **Qt** draws a checkable cell, a combobox frame, a push button, or underlined link text. A click or F2 on an editable combobox cell opens a `QComboBox`. Space toggles the current check box, or clicks the current button or link. A path's "…" button, or Space on its cell, opens a `QFileDialog`: a file, a folder, or a save dialog. It opens on the cell's path, else in its `base_folder`, else in the first of its `server_roots`. - **Textual** shows the cell as text: a box (☑ or ☐), the text and ▾, the text and the button's text in brackets, or underlined text. A click toggles a box, and so does Space on the cursor's row. A click on an editable combobox cell opens a list of its options. A click on a button or a link cell clicks it. A path shows its text and "[…]". A click on an editable path cell opens the picker of a file, a folder, or a file to save, on the cell's path, else in its `base_folder`, else in the first of its `server_roots`. F2's editor types the path. A check-all header shows its box (▣ when partly checked), and a click on the header toggles it instead of sorting. - **The web** shows a check box, a `